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.
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.
Your product chooses whether multi-agent behavior is runtime-owned, host-owned, or a durable internal team system.
The same runtime can expose sub-agent tools, parallel SDK sessions, or mailbox-driven team execution depending on which boundary owns coordination.
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.
Use agent for bounded synchronous delegation or spawn_agent for async work that returns a stable agent_id.
Create multiple sdk.Session instances in parallel when the application, not the model, must decide roles, ordering, and synthesis rules.
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.
| Surface | Owned By | Best For | Notes |
|---|---|---|---|
| agent | Runtime tool inside one session | Synchronous 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_agent | Runtime tool family inside one session | Background 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 app | Your Go application | Deterministic 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/team | Seshat core runtime | Durable 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.
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.
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)You want explicit control over role count, concurrency, tool policy, provider choice, retries, and final synthesis instead of leaving topology decisions to the model.
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.
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.
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.
| Area | Current Shape | Why It Matters |
|---|---|---|
| Agent runtime events | The 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 runtime | Profiles, 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 facade | There 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 usage | Go 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.