Skip to content

Parallel Agents ​

This tutorial covers Fork and Spawn—AIDK's primitives for running multiple agents concurrently.

Two Primitives ​

AspectForkSpawn
Parent RequiredYesNo
State InheritanceYes (timeline, sections, hooks, context)No (blank slate)
waitUntilCompleteYesYes (but typically false)
Use CaseParallel work needing context/resultsBackground tasks, fire-and-forget

Key difference: inheritance. Fork creates a child that can inherit state from the parent. Spawn creates a completely independent execution with a fresh engine—a true blank slate.

Fork: Parallel Work with Results ​

Fork creates a child execution and optionally waits for it to complete.

Basic Fork ​

tsx
class ResearchAgent extends Component {
  private results = comState<ResearchResult[]>("results", []);

  render() {
    return (
      <>
        <Model model={openai("gpt-5.2")} />
        <System>You are a research coordinator.</System>

        {/* Fork researchers in parallel */}
        <Fork
          root={<TopicResearcher topic="climate change" />}
          waitUntilComplete={true}
          onComplete={(result) => {
            this.results.set([...this.results(), result]);
          }}
        />

        <Fork
          root={<TopicResearcher topic="renewable energy" />}
          waitUntilComplete={true}
          onComplete={(result) => {
            this.results.set([...this.results(), result]);
          }}
        />

        {/* Show collected results */}
        <Grounding title="Research Results">
          {this.results().map((r, i) => (
            <Section key={i} title={r.topic}>
              {r.summary}
            </Section>
          ))}
        </Grounding>
      </>
    );
  }
}

Fork with Children ​

Instead of the root prop, you can use children:

tsx
<Fork waitUntilComplete={true} onComplete={handleResult}>
  <Model model={openai("gpt-5.2")} />
  <System>Analyze this document for key themes.</System>
  <Grounding title="Document">{document}</Grounding>
  <AnalysisTool />
</Fork>

Fork Options ​

tsx
<Fork
  // The component to run (or use children)
  root={<MyComponent />}

  // Wait for completion before continuing parent tick
  waitUntilComplete={true}

  // Callback when fork completes
  onComplete={(result) => handleResult(result)}

  // Callback on error
  onError={(error) => handleError(error)}

  // Initial input for the fork
  input={{ timeline: [userMessage] }}

  // Inherit parent's model if not specified
  inheritModel={true}

  // Maximum ticks before timeout
  maxTicks={50}
/>

Spawn: Independent Execution ​

Spawn creates a completely independent process with no state inheritance. It's typically fire-and-forget, but can use waitUntilComplete={true} when you need to wait for an isolated execution.

Basic Spawn ​

tsx
class ChatAgent extends Component {
  private timeline = comState<COMTimelineEntry[]>("timeline", []);

  onTickStart(com, state) {
    if (state.current?.timeline) {
      this.timeline.set([...this.timeline(), ...state.current.timeline]);
    }
  }

  render(com, state) {
    return (
      <>
        <Model model={openai("gpt-5.2")} />
        <System>You are a helpful assistant.</System>

        {/* Log every interaction in background */}
        <Spawn root={<AuditLogger interaction={state.current} />} />

        {/* Send notifications without blocking */}
        {this.shouldNotify(state) && (
          <Spawn root={<NotificationSender userId={context().user?.id} />} />
        )}

        <Timeline>
          {this.timeline().map(entry => (
            <Message key={entry.id} {...entry.message} />
          ))}
        </Timeline>
      </>
    );
  }
}

Spawn with Children ​

tsx
<Spawn>
  <Model model={openai("gpt-5.2-mini")} />
  <System>Summarize this conversation and store it.</System>
  <Grounding title="Conversation">{conversationText}</Grounding>
  <StorageTool />
</Spawn>

Spawn Options ​

tsx
<Spawn
  // The component to run (or use children)
  root={<BackgroundTask />}

  // Wait for completion (default: false)
  waitUntilComplete={false}

  // Callbacks
  onComplete={(result) => handleResult(result)}
  onError={(error) => handleError(error)}

  // Initial input (must provide timeline since no inheritance)
  input={{ timeline: [] }}

  // Engine configuration for the independent execution
  engineConfig={{ maxTicks: 10 }}
/>

Note: Unlike Fork, Spawn has no inherit option because it never inherits—it's always a blank slate.

Patterns ​

Pattern 1: Parallel Research ​

Run multiple researchers, synthesize results:

tsx
class ResearchCoordinator extends Component {
  private sources = signal<string[]>(["arxiv", "pubmed", "google_scholar"]);
  private findings = comState<Finding[]>("findings", []);
  private synthesisReady = signal(false);

  render() {
    const sources = this.sources();
    const findings = this.findings();

    if (!this.synthesisReady()) {
      // Research phase: fork researchers for each source
      return (
        <>
          <Model model={openai("gpt-5.2")} />
          <System>Coordinating research across {sources.length} sources.</System>

          {sources.map(source => (
            <Fork
              key={source}
              waitUntilComplete={true}
              onComplete={(result) => {
                const updated = [...this.findings(), ...result.findings];
                this.findings.set(updated);

                // Check if all sources are done
                if (updated.length >= sources.length * 3) {
                  this.synthesisReady.set(true);
                }
              }}
            >
              <Model model={openai("gpt-5.2-mini")} />
              <System>Search {source} for relevant papers.</System>
              <SearchTool source={source} />
            </Fork>
          ))}
        </>
      );
    }

    // Synthesis phase
    return (
      <>
        <Model model={anthropic("claude-3-5-sonnet")} />
        <System>Synthesize these research findings into a coherent summary.</System>
        <Grounding title="Findings">
          {findings.map((f, i) => (
            <Section key={i} title={f.source}>{f.content}</Section>
          ))}
        </Grounding>
      </>
    );
  }
}

Pattern 2: Verification Pipeline ​

Generate, then verify in parallel:

tsx
class VerifiedGenerator extends Component {
  private draft = signal<string | null>(null);
  private verifications = signal<Verification[]>([]);

  render() {
    const draft = this.draft();
    const verifications = this.verifications();

    if (!draft) {
      // Generation phase
      return (
        <>
          <Model model={openai("gpt-5.2")} />
          <System>Generate a detailed response.</System>
          <GenerateTool onGenerate={(d) => this.draft.set(d)} />
        </>
      );
    }

    const allVerified = verifications.length >= 3;

    if (!allVerified) {
      // Verification phase: multiple verifiers in parallel
      return (
        <>
          <Fork
            waitUntilComplete={true}
            onComplete={(v) => this.verifications.set([...this.verifications(), v])}
          >
            <Model model={anthropic("claude-3-5-sonnet")} />
            <System>Check for factual accuracy.</System>
            <Grounding title="Content">{draft}</Grounding>
            <VerifyTool aspect="accuracy" />
          </Fork>

          <Fork
            waitUntilComplete={true}
            onComplete={(v) => this.verifications.set([...this.verifications(), v])}
          >
            <Model model={openai("gpt-5.2")} />
            <System>Check for logical consistency.</System>
            <Grounding title="Content">{draft}</Grounding>
            <VerifyTool aspect="logic" />
          </Fork>

          <Fork
            waitUntilComplete={true}
            onComplete={(v) => this.verifications.set([...this.verifications(), v])}
          >
            <Model model={openai("gpt-5.2-mini")} />
            <System>Check for clarity and readability.</System>
            <Grounding title="Content">{draft}</Grounding>
            <VerifyTool aspect="clarity" />
          </Fork>
        </>
      );
    }

    // All verified - show results
    const allPassed = verifications.every(v => v.passed);

    return (
      <>
        {allPassed ? (
          <AssistantMessage>{draft}</AssistantMessage>
        ) : (
          <>
            <System>Some verifications failed. Revising...</System>
            <Grounding title="Issues">
              {verifications.filter(v => !v.passed).map((v, i) => (
                <Text key={i}>{v.aspect}: {v.issue}</Text>
              ))}
            </Grounding>
          </>
        )}
      </>
    );
  }
}

Pattern 3: Background Processing ​

Handle long-running tasks without blocking:

tsx
class DocumentProcessor extends Component {
  private processingJobs = signal<Job[]>([]);

  render(com) {
    const jobs = this.processingJobs();
    const pendingJobs = jobs.filter(j => j.status === "pending");

    return (
      <>
        <Model model={openai("gpt-5.2")} />
        <System>
          You are a document processor.
          {pendingJobs.length > 0 && ` ${pendingJobs.length} jobs processing in background.`}
        </System>

        {/* Spawn background processors for new documents */}
        <ProcessDocumentTool
          onNewDocument={(doc) => {
            const job = { id: doc.id, status: "pending" };
            this.processingJobs.set([...jobs, job]);

            // Process in background
            <Spawn
              root={<DocumentAnalyzer document={doc} />}
              onComplete={() => {
                const updated = this.processingJobs().map(j =>
                  j.id === doc.id ? { ...j, status: "complete" } : j
                );
                this.processingJobs.set(updated);
              }}
            />
          }}
        />

        <Grounding title="Job Status">
          {jobs.map(job => (
            <Text key={job.id}>
              {job.id}: {job.status}
            </Text>
          ))}
        </Grounding>
      </>
    );
  }
}

Pattern 4: Hierarchical Agents ​

Agents that manage sub-agents based on tool calls:

tsx
class ProjectManager extends Component {
  private pendingDelegation = signal<{ team: string; task: string } | null>(null);
  private teamResults = comState<Record<string, any>>("teamResults", {});

  render() {
    const delegation = this.pendingDelegation();

    // If there's a pending delegation, fork the appropriate team
    if (delegation) {
      const TeamAgent = {
        frontend: FrontendTeam,
        backend: BackendTeam,
        qa: QATeam,
      }[delegation.team];

      return (
        <Fork
          waitUntilComplete={true}
          onComplete={(result) => {
            this.teamResults.set({
              ...this.teamResults(),
              [delegation.team]: result,
            });
            this.pendingDelegation.set(null);
          }}
        >
          <TeamAgent task={delegation.task} />
        </Fork>
      );
    }

    return (
      <>
        <Model model={openai("gpt-5.2")} />
        <System>You manage a software project with specialized teams.</System>

        <DelegateTool
          onDelegate={(team, task) => this.pendingDelegation.set({ team, task })}
        />

        <Grounding title="Team Results">
          {Object.entries(this.teamResults()).map(([team, result]) => (
            <Section key={team} title={team}>{result}</Section>
          ))}
        </Grounding>
      </>
    );
  }
}

// Tool signals intent; parent component handles the fork
const DelegateTool = createTool({
  name: "delegate",
  description: "Delegate a task to a specialized team",
  input: z.object({
    team: z.enum(["frontend", "backend", "qa"]),
    task: z.string(),
  }),

  // Handler returns the delegation request
  handler: async ({ team, task }) => {
    return [{ type: "text", text: `Delegating "${task}" to ${team} team...` }];
  },
});

Note: Tools can't directly fork agents. Instead, the tool signals intent (via its result or by updating shared state), and the parent component's render handles the fork.

Signal Communication ​

Forked agents can communicate via the parent's signals:

tsx
class Coordinator extends Component {
  private sharedProgress = signal(0);
  private sharedResults = signal<Result[]>([]);

  render() {
    return (
      <>
        <System>Progress: {this.sharedProgress()}%</System>

        <Fork
          root={
            <Worker
              onProgress={(p) => this.sharedProgress.set(p)}
              onResult={(r) => this.sharedResults.set([...this.sharedResults(), r])}
            />
          }
          waitUntilComplete={true}
        />
      </>
    );
  }
}

Key Takeaways ​

  1. Fork for context-aware parallel work: Inherits timeline, sections, hooks, and context from parent
  2. Spawn for isolated background tasks: Completely independent, blank slate execution
  3. Both support waitUntilComplete: Fork typically waits, Spawn typically doesn't (but can)
  4. Agents can nest: Build hierarchical multi-agent systems with Fork
  5. Signals enable coordination: Share state between parent and children via callbacks

Next Steps ​

Released under the MIT License.