Docs/SDK and API/Go SDK/Multi-Agent Scenarios

Multi-Agent Scenarios

Multi-agent in Seshat is not one feature with one API. There are at least two distinct runtime levels you need to separate clearly, plus one deeper internal team runtime: short-lived delegated sub-agents inside a session, deterministic orchestration across multiple sessions, and persistent mailbox-driven teams inside the core.

Overview

If you blur these levels together, the docs become vague and the integration model becomes misleading. The public Go SDK already gives you enough to build real multi-agent product flows, but the exact approach depends on who should own coordination: the model, the host application, or the durable runtime itself.

Important Distinction

spawn_agent is not the same thing as the mailbox team runtime described in Concepts. One is short-lived delegated execution. The other is durable identity, async transport, routing, and message-driven execution over time.

Runtime Levels

The visual map below is the practical way to think about multi-agent in Seshat today. Start with the lightest layer that solves the problem instead of jumping straight to a durable team system.

Outer host
CLI app, desktop surface, backend, or worker

Your product chooses whether multi-agent behavior is runtime-owned, host-owned, or a durable internal team system.

Mono-run core
One engine, multiple multi-agent levels

The same runtime can expose sub-agent tools, parallel SDK sessions, or mailbox-driven team execution depending on which boundary owns coordination.

Decision point
Do not collapse every collaboration pattern into one “agent team” label

Seshat currently has three useful shapes: model-led delegation inside one session, deterministic orchestration from the host app, and persistent mailbox teams inside the core runtime.

Level 1
In-session delegation

Use agent for bounded synchronous delegation or spawn_agent for async work that returns a stable agent_id.

Level 2
Host-led orchestration

Create multiple sdk.Session instances in parallel when the application, not the model, must decide roles, ordering, and synthesis rules.

Level 3
Persistent team runtime

Profiles, mailbox transport, dispatcher routing, and TeamBus execution are the durable layer for named agents that can receive work over time and across restarts.

Surface Matrix

These surfaces all count as multi-agent in some sense, but they differ sharply in ownership, durability, and how much topology is hidden inside the runtime.

SurfaceOwned ByBest ForNotes
agentRuntime tool inside one sessionSynchronous delegation decided by the model during the current turn.Good when the manager agent should decompose work itself and return one final answer before the turn ends.
spawn_agent + wait_agent + send_agent_message + resume_agentRuntime tool family inside one sessionBackground sub-agents with stable agent ids and resumable session ids.This is the closest public multi-agent surface today when the runtime itself should own delegation, concurrency, and synthesis.
Multiple sdk.Session instances from the host appYour Go applicationDeterministic fan-out where the host decides which roles run, in what order, and how outputs are merged.Best for product workflows, backend orchestration, and explicit governance where you do not want the model to invent the topology.
internal/agent + internal/mailbox + internal/teamSeshat core runtimeDurable identities, async inboxes, role-based routing, and work that moves across sessions and restarts.Important: this path is internal to the Seshat module today, so treat it as first-party or contributor-facing rather than a stable external SDK facade.

Scenario 1: Model-Led Delegation Inside One Session

This is the cleanest path when you want the manager agent to decide whether the task should branch into researcher, implementer, planner, or reviewer work. The host configures the runtime, but the model chooses when to call agent or spawn_agent.

Runtime-owned sub-agents
client, err := sdk.NewClient(&sdk.ClientConfig{
    Model: sdk.ModelIdentifier{
        Provider: sdk.APIProviderAnthropic,
        Model:    "claude-sonnet-4-20250514",
    },
    APIKey:         os.Getenv("ANTHROPIC_API_KEY"),
    WorkingDir:     repoRoot,
    PermissionMode: sdk.PermissionModeOnRequest,
    RuntimeEventFn: func(event sdk.RuntimeEvent) {
        switch string(event.Type) {
        case "agent.spawn.begin", "agent.spawn.end", "agent.wait.begin", "agent.wait.end":
            if event.AgentEvent != nil {
                log.Printf(
                    "agent=%s role=%s status=%s type=%s",
                    event.AgentEvent.AgentID,
                    event.AgentEvent.AgentRole,
                    event.AgentEvent.Status,
                    event.Type,
                )
            }
        }
    },
})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

session, err := client.CreateSession(ctx)
if err != nil {
    log.Fatal(err)
}
defer session.Close()

session.SetAppendSystemPrompt(`If the task benefits from delegation, use the built-in
agent tools. Prefer a researcher, implementer, and reviewer split.
Wait for spawned agents before returning the final synthesis.`)

resp, err := session.SubmitMessage(ctx, `Inspect this repository. Research the bug,
propose the patch, and review the risk before you answer.`)
if err != nil {
    log.Fatal(err)
}

fmt.Println(resp.Content)
  • Use agent when the sub-task should stay tightly bounded and complete synchronously before the manager continues.
  • Use spawn_agent when the runtime should launch background work and later reconcile it with wait_agent or resume_agent.
  • This pattern keeps the whole collaboration inside one durable session, which is usually the right default for coding, research, and analyst-style flows.

Scenario 2: Host-Led Orchestration Across Sessions

Sometimes the product must decide the topology explicitly. In that case, create multiple sessions yourself and run them in parallel from the host application. This is often the better choice for backends, workflow engines, API products, and deterministic enterprise flows.

Host-managed parallel sessions
type RoleResult struct {
    Role   string
    Output string
}

runRole := func(ctx context.Context, role string, instruction string) (RoleResult, error) {
    session, err := client.CreateSession(ctx)
    if err != nil {
        return RoleResult{}, err
    }
    defer session.Close()

    session.SetSystemPromptTemplate(fmt.Sprintf(`You are the %s agent.
Stay focused on your role and return structured output.`, role))

    resp, err := session.SubmitMessage(ctx, instruction)
    if err != nil {
        return RoleResult{}, err
    }

    return RoleResult{Role: role, Output: resp.Content}, nil
}

var (
    g       errgroup.Group
    mu      sync.Mutex
    results []RoleResult
)

for role, task := range map[string]string{
    "researcher": "Investigate the bug reports and likely root cause.",
    "engineer":   "Draft the implementation strategy and patch outline.",
    "reviewer":   "List the highest-risk regressions and missing tests.",
} {
    role := role
    task := task
    g.Go(func() error {
        result, err := runRole(ctx, role, task)
        if err != nil {
            return err
        }
        mu.Lock()
        results = append(results, result)
        mu.Unlock()
        return nil
    })
}

if err := g.Wait(); err != nil {
    log.Fatal(err)
}

payload, err := json.MarshalIndent(results, "", "  ")
if err != nil {
    log.Fatal(err)
}

manager, err := client.CreateSession(ctx)
if err != nil {
    log.Fatal(err)
}
defer manager.Close()

manager.SetAppendSystemPrompt("Synthesize specialist outputs into one decisive plan.")

resp, err := manager.SubmitMessage(ctx, string(payload))
if err != nil {
    log.Fatal(err)
}

fmt.Println(resp.Content)
Why choose this path

You want explicit control over role count, concurrency, tool policy, provider choice, retries, and final synthesis instead of leaving topology decisions to the model.

Typical fit

Backend orchestration, workflow services, audited business processes, or product features where each specialist output must be clearly attributable and reproducible.

Scenario 3: Persistent Teams, Mailboxes, And TeamBus

This is the deeper runtime layer already present inside Seshat core: AgentProfile, Mailbox, Dispatcher, and TeamBus. It models named agents that can be offline, receive tasks asynchronously, reply later, and collaborate through durable inboxes.

Current Boundary

This path lives under internal/ packages. That means it is suitable today for core contributors, first-party surfaces, and monorepo work, but not yet a stable public SDK facade for external consumers.

Internal durable team runtime
database, err := db.Open(ctx, db.Config{
    Driver:      db.DriverSQLite,
    DSN:         "seshat.db",
    AutoMigrate: true,
})
if err != nil {
    log.Fatal(err)
}

profiles := agent.NewProfileRegistry(database)
teams := team.NewTeamRegistry(database)
_ = profiles.Seed(ctx)

agentLister := func(ctx context.Context, teamID string) ([]string, error) {
    members, err := teams.Members(ctx, teamID)
    if err != nil {
        return nil, err
    }

    ids := make([]string, 0, len(members))
    for _, member := range members {
        ids = append(ids, member.ID)
    }
    return ids, nil
}

mail := mailbox.New(database, agentLister)
dispatcher := team.NewDispatcher(profiles, mail)

handler := team.NewSessionHandler(engineInstance, nil, profiles, teams)
bus := team.NewTeamBus(profiles, mail, handler, 2*time.Second)
bus.Start(ctx)
defer bus.Stop()

_ = dispatcher.Assign(
    ctx,
    orchestratorID,
    "researcher",
    "alpha",
    "Research competitors",
    "Summarise pricing, positioning, and distribution channels.",
)

For the conceptual explanation of this durable layer, continue with Agent Teams and Mailboxes.

Events, Limits, And Current Shape

The runtime already exposes real collaboration mechanics, but the public surface is still uneven. These are the points worth documenting explicitly so developers know what is stable, what is internal, and where gRPC starts to matter.

AreaCurrent ShapeWhy It Matters
Agent runtime eventsThe engine emits structured agent events, but the Go SDK does not yet re-export named constants for every agent.* variant.Host apps can still observe them through RuntimeEventFn and compare string(event.Type) against raw values like agent.spawn.begin.
Persistent team runtimeProfiles, mailboxes, dispatcher, TeamBus, and session handler live under internal/ packages.External consumers outside the Seshat module cannot import them directly. Today that path is for monorepo work, first-party surfaces, and contributors.
Public SDK facadeThere is no single high-level sdk.Team client yet.If you need a stable public integration today, prefer runtime-owned sub-agent tools or host-managed multi-session orchestration.
Cross-language usageGo is still the richest embedding surface.If another language must own the outer application API, use the gRPC boundary rather than rebuilding orchestration semantics from scratch.

If you need a polyglot boundary after Go, continue with gRPC API. That is the right bridge for future Python, JS, and other SDKs rather than copying the runtime model separately in each language.