Skip to content

Agents Overview

Agents are the unit of work in AgentSquad. Each agent owns its LLM call and tool-use loop; the Orchestrator decides which agent handles a given turn and manages chat persistence.

Every agent conforms to AgentProtocol:

public protocol AgentProtocol: Sendable {
var id: String { get } // storage namespace + classifier key; defaults to slugify(name)
var name: String { get }
var description: String { get }
var saveChat: Bool { get } // orchestrator persists turns; defaults to true
var maxToolRounds: Int { get } // tool-loop cap; defaults to 1
func process(
_ input: AgentInput,
history: [ConversationMessage],
context: AgentContext
) -> AsyncThrowingStream<AgentEvent, any Error>
}

The protocol extension provides default implementations for id, saveChat, and maxToolRounds, so a minimal custom agent only needs to implement name, description, and process.

PropertyDefaultNotes
idslugify(name)Used as the storage namespace and classifier routing key.
saveChattrueSet to false to opt the agent out of chat persistence.
maxToolRounds1Override to allow a tool-use loop. A tool-bearing agent that leaves this at 1 will never iterate past the first model call.
public enum AgentInput: Sendable {
case text(String)
}

A convenience property surfaces the string without a pattern match:

let text = input.text // "" when the case is not .text

Carries per-turn identity, arbitrary params, and the live trace span:

public struct AgentContext: Sendable {
public let userId: String
public let sessionId: String
public let params: [String: JSONValue]
public let span: (any SpanHandle)?
public init(
userId: String,
sessionId: String,
params: [String: JSONValue] = [:],
span: (any SpanHandle)? = nil
)
}

Child spans attached to context.span appear nested under the orchestrator’s session span in your tracer. See Tracing for details.

The stream emitted by process yields these cases:

public enum AgentEvent: Sendable {
case thinking(String)
case textDelta(String)
case toolCall(id: String, name: String, arguments: JSONValue)
case widget(UIPayload)
case final(ConversationMessage)
case error(String)
}
CaseNotes
.textDeltaIncremental text chunk; concatenate to build the full response.
.toolCallAnnounced for observability; the tool result is recorded on the trace span, not re-emitted as an event.
.widgetA structured UI payload from a tool (see UI). Only emitted when the agent’s UIPolicy is .forward.
.finalThe completed ConversationMessage; the orchestrator persists this when saveChat is true.
.errorA user-facing message (e.g. “network unavailable”). Real failures are thrown through the stream.

Both built-in agents accept a UIPolicy parameter that controls whether tool-advertised UI payloads reach the caller:

public enum UIPolicy: Sendable {
case forward // emit .widget events (default)
case suppress // fold tool data into text; no .widget emitted
}

See UI for how UIPayload is declared and consumed.

TypeDescription
AgentGeneral-purpose: one LLMClient driving a tool-use loop.
GroundedAgentTwo-LLM anti-hallucination pattern: a Brain gathers tool output; an isolated Presenter speaks only from that output.

Both implement AgentProtocol and are interchangeable at the Orchestrator call site.

Conform to AgentProtocol and implement process. See Custom Agents for a complete example and guidance on overriding maxToolRounds.

  • Orchestrator — how agents are routed and chat is persisted.
  • Tools — building a ToolProvider and defining AgentTools.
  • LLM clients — the LLMClient protocol and available implementations.
  • Messages & eventsConversationMessage, ContentPart, and the full event model.
  • Tracing — attaching spans to AgentContext.
  • UIUIPayload and how .widget events are consumed.
  • Voice — the voice path that sits outside AgentProtocol.