DelegationChain and SharedContextStore: Loop Prevention and Cross-Agent Memory

How DelegationChain prevents agent delegation loops — an immutable value object where addStep() returns a new chain, wouldLoop() checks if the target agent appears anywhere as source or destination, and max depth of 3 prevents runaway delegation. SharedContextStore provides cross-agent memory via DB-backed namespace-tagged key-value pairs with TTL support, extended with setWithExtension() (90-day TTL with auto-extend on access), touch(), queryByPrefix(), and getStructured() for agent persistent

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:

  1. KIK decides VajbCoder should write code
  2. DelegationChain: [kik→vajbcoder]
  3. VajbCoder writes code, stores context: set('codebase', 'auth_module_structure', 'Created MVC in moduli/auth/')
  4. VajbCoder delegates to ReviewAgent
  5. DelegationChain: [kik→vajbcoder, vajbcoder→reviewer]
  6. ReviewAgent reads VajbCoder's context: query('codebase', 5) → sees the auth module structure
  7. ReviewAgent can't delegate to KIK or VajbCoder (both in chain)
  8. ReviewAgent returns verdict → flows back up through VajbCoder → KIK
The chain prevents loops. The store shares knowledge. Together, they enable multi-agent collaboration without either agent knowing the other's implementation.

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

Recent Posts