Skip to content

Persistent Sub-Agents Example

A self-contained demo of persistent sub-agents (companion tools) on the JS runtime. It runs completely offline — no API key, no Docker, no wrangler — by driving the agents with a RoutingMockLLMAdapter, so npm test is a real, runnable correctness check of the feature.

Source: examples/persistent-subagents

The shape

A coordinator parent declares a persistent child. Declaring persistentAgents is the only wiring needed — the framework auto-injects the companion__* tools onto the parent.

typescript
import { defineAgent } from '@helix-agents/core';
import { ResearcherAgent } from './researcher';

export const CoordinatorAgent = defineAgent({
  name: 'coordinator',
  // Declaring persistentAgents auto-injects companion__spawnAgent / sendMessage /
  // listChildren / getChildStatus / terminateChild (+ waitForResult, because at
  // least one child is `mode: 'blocking'`).
  persistentAgents: [{ agent: ResearcherAgent, mode: 'blocking' }],
  outputSchema: CoordinatorOutputSchema,
  systemPrompt:
    'Use companion__spawnAgent({ agent: "researcher", name, initialMessage }) to ' +
    'delegate a topic. Because the researcher is BLOCKING, the spawn returns its ' +
    'findings inline. When done, call __finish__ with { topic, summary }.',
  llmConfig: { model: openai('gpt-4o-mini') }, // never called in the demo
  maxSteps: 12,
});

Driving it offline

The demo and tests inject a RoutingMockLLMAdapter (routes scripted responses by agentType, so the concurrent parent and child never race on a shared queue):

typescript
const llm = new RoutingMockLLMAdapter();
llm.setResponses('coordinator', [
  spawnStep({ agent: 'researcher', name: 'ai-safety', initialMessage: 'Research AI safety' }),
  finishStep({ topic: 'AI safety', summary: 'Researcher gathered findings.' }),
]);
llm.setResponses('researcher', [
  callStep('gather_facts', { topic: 'AI safety' }),
  finishStep({ findings: 'AI safety is an active area of research...' }),
]);

const executor = new JSAgentExecutor(stateStore, streamManager, llm);
const handle = await executor.execute(CoordinatorAgent, 'Coordinate research on AI safety', {
  sessionId,
});
const result = await handle.result();

What you can then assert (all deterministic — see the test file):

  • A persistent SubSessionRef exists with the deterministic id ${sessionId}-agent-ai-safety, status: 'completed'.
  • The child session persisted its structured output ({ findings }).
  • A blocking spawn delivered the child's findings inline into the parent's conversation (the findings text appears in the parent's messages).

What the example covers

TestDemonstrates
Blocking spawn → inline resultcompanion__spawnAgent (blocking) returns the child's output inline.
Two named children + listMultiple persistent children + companion__listChildren.
Terminate-truthTerminating an already-completed child does not clobber its result.
Non-blocking spawn contrastA non-blocking spawn returns { status: 'spawned' } immediately, with NO inline output (vs the blocking case).
Critic-loop re-consultA maker re-consults the same critic (same name) across rounds. Re-consulting a completed child continues on its preserved session (same subSessionId, both rounds' consults retained, fresh round-2 verdict: 'pass') — proven for blocking spawnAgent and for sendMessagewaitForResult.
Structured-output continuationA completed structured-output agent is re-execute()'d and continued (fresh typed output, round-1 turns retained) with no dangling-__finish__ failure — under a strict mock adapter, so the __finish__ heal has teeth.
Config sanityCompanion tools are auto-injected only when persistentAgents is declared.

Run it

bash
# From the repo root, build the workspace once:
npm run build

cd examples/persistent-subagents
npm test      # offline mock-LLM tests (no API key)
npm run demo  # prints the coordinator → researcher flow

Running on other runtimes

The same persistentAgents config works unchanged on Temporal, Cloudflare (Workflows / Durable Objects), and DBOS — only the executor and stores differ. See the runtime support matrix for per-runtime notes (e.g. workspaces on children are JS / Cloudflare-DO only; DBOS blocking-spawn has a known caveat).

Next steps

Released under the MIT License.