When KIK delegates to VajbCoder, and VajbCoder decides it needs a code review, it delegates to ReviewAgent. That's a chain: KIK → VajbCoder → ReviewAgent. Three agents deep.
Two questions: How do you prevent KIK → VajbCoder → KIK → VajbCoder → ... ? And how does ReviewAgent know what VajbCoder already found?
DelegationChain handles the first. SharedContextStore handles the second.
DelegationChain: Immutable Loop Prevention
Immutability
class DelegationChain
{
/** @var array<array{from: string, to: string, task: string}> */
private array $steps;
private int $maxDepth;
public function addStep(string $from, string $to, string $task): self
{
$new = clone $this;
$new->steps[] = ['from' => $from, 'to' => $to, 'task' => $task];
return $new;
}
}
addStep() returns a new chain. The original is unchanged. This prevents accidental mutation when the chain is passed through multiple agent executions — each agent gets its own snapshot.
Loop Detection
public function wouldLoop(string $agentName): bool
{
foreach ($this->steps as $step) {
if ($step['from'] === $agentName || $step['to'] === $agentName) {
return true;
}
}
return false;
}
Conservative check: if the target agent appears anywhere in the chain — as source or destination — it's a loop. This prevents both direct loops (A → B → A) and indirect loops (A → B → C → A).
Why check both from and to? Consider: KIK delegates to VajbCoder (chain: [kik→vajbcoder]). VajbCoder tries to delegate back to KIK. Without checking from, KIK would look clean — it's only in the from position, not to. But from means "this agent already participated." It shouldn't participate again.
Max Depth
public function isMaxDepth(): bool
{
return count($this->steps) >= $this->maxDepth; // Default: 3
}
Even without loops, unbounded delegation chains are dangerous. Max depth of 3 means: orchestrator → specialist → sub-specialist. Deeper chains suggest the task should be decomposed differently.
How It Flows
// In DelegateAgentTool::execute()
if ($delegationChain->wouldLoop($targetAgent)) {
return ['success' => false, 'error' => "Agent '{$targetAgent}' already in delegation chain"];
}
if ($delegationChain->isMaxDepth()) {
return ['success' => false, 'error' => 'Maximum delegation depth reached'];
}
$newChain = $delegationChain->addStep($currentAgent, $targetAgent, $task);
$result = $agentManager->run($targetAgent, $task, $onProgress, $newChain);
The tool checks, creates a new chain with the added step, and passes it to the next agent. The new agent inherits the chain — and can't delegate back to anyone in it.
SharedContextStore: Cross-Agent Memory
CREATE TABLE agent_shared_context (
namespace VARCHAR(64) NOT NULL,
key_name VARCHAR(128) NOT NULL,
value TEXT NOT NULL,
agent VARCHAR(64) NOT NULL, -- Who wrote this
expires_at DATETIME NULL, -- TTL
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (namespace, key_name),
INDEX idx_namespace (namespace),
INDEX idx_expires (expires_at)
);
Namespace Isolation
Context is organized by namespace:
// Healer agent stores finding
$store->set('performance', 'n1_invoices_route', 'Fixed eager loading on InvoicesController::index', 'lukagent', 86400);
// PM bridge stores learning
$store->set('pm_learnings', 'module:vajbcoder:auth', 'Routes file must be added before dependencies', 'vajbcoder', 30 * 86400);
// Security agent stores pattern
$store->set('security', 'sqli_attempt_192.168.1.50', 'Repeated SQLi attempts from this IP', 'bornagent', 7 * 86400);
Agents read from namespaces relevant to their task. VajbCoder reads codebase and reviewer namespaces. LukAgent reads performance and bornagent namespaces.
Upsert Pattern
public function set(string $namespace, string $key, string $value, string $agent, ?int $ttl = null): void
{
$expiresAt = $ttl !== null ? date('Y-m-d H:i:s', time() + $ttl) : null;
$this->db->execute(
"INSERT INTO agent_shared_context (namespace, key_name, value, agent, expires_at)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE value = VALUES(value), agent = VALUES(agent),
expires_at = VALUES(expires_at)",
[$namespace, $key, $value, $agent, $expiresAt]
);
}
INSERT ... ON DUPLICATE KEY UPDATE — if the namespace+key already exists, update it. If not, insert. No need for separate exists-check-then-insert logic.
Three Access Patterns
// 1. Point read — get specific key
$store->get('performance', 'n1_invoices_route');
// 2. Namespace scan — get recent entries in a namespace (limit)
$store->query('pm_learnings', 5); // Last 5 entries
// 3. Cross-namespace search — used by LIMAEngine for agent context
// (implemented as multiple namespace queries, merged)
TTL and Cleanup
Expired entries aren't deleted on read — they're filtered:
$rows = $this->db->queryAndFetchAllAssoc(
"SELECT * FROM agent_shared_context WHERE namespace = ? AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY updated_at DESC LIMIT ?",
[$namespace, $limit]
);
A periodic cleanup job (or manual call) removes expired entries. The TTL varies by use case:
- Performance findings: 24 hours (stale after a deploy)
- PM learnings: 30 days (valuable across projects)
- Security patterns: 7 days (IP patterns change)
Extended API: Agent Memory
For persistent agent memory (the memory:{agent}:{module} convention used by LIMAgents), three helpers extend the basic set/get/query trio:
// Write-and-extend: if the key already exists, both update value AND push the TTL forward
$store->setWithExtension('memory:vajbcoder', 'module:auth', $notes, 'vajbcoder');
// default TTL = 129600 minutes = 90 days
// Touch only: extend TTL without rewriting value (used by "agent read this memory again")
$store->touch('memory:vajbcoder', 'module:auth');
// Prefix scan: list all keys under a prefix within a namespace
$rows = $store->queryByPrefix('memory:vajbcoder', 'module:', 20);
// → [{key_name: 'module:auth', …}, {key_name: 'module:payroll', …}, …]
// Structured read: same as get() but json_decode() before returning
$saved = $store->getStructured('memory:pmagent', 'project:42');
The "frequently-accessed knowledge persists longer" pattern is exactly this: a VajbCoder run finds that a module has an unusual routing quirk, writes it with setWithExtension, and every subsequent agent that looks at that module extends the TTL by reading it. Knowledge that's used survives; knowledge that nobody ever looks at ages out after 90 days. queryByPrefix escapes % and _ to avoid accidental LIKE-wildcards in keys.
How They Work Together
In a delegation chain KIK → VajbCoder → ReviewAgent:
- KIK decides VajbCoder should write code
- DelegationChain:
[kik→vajbcoder] - VajbCoder writes code, stores context:
set('codebase', 'auth_module_structure', 'Created MVC in moduli/auth/') - VajbCoder delegates to ReviewAgent
- DelegationChain:
[kik→vajbcoder, vajbcoder→reviewer] - ReviewAgent reads VajbCoder's context:
query('codebase', 5)→ sees the auth module structure - ReviewAgent can't delegate to KIK or VajbCoder (both in chain)
- ReviewAgent returns verdict → flows back up through VajbCoder → KIK
Up Next
Next up: ApprovalGateway: Multi-Channel Patch Review Before Anything Hits Production — how agent results get routed to chat, Telegram, or web dashboard for human approval, and why auto-approval exists for analysis-only agents.
Comments (0)
No comments yet. Be the first to share your thoughts!
Leave a Comment