Skip to content

manifest

Manifest barrel — agent config conversion and tool schema handling.

Used by aai-cli (bundler) and aai-server (rpc-schemas). Generated bundle entries call toAgentConfig, which is why this subpath is published.

agentToolsToSchemas(tools): ToolSchema[]

Readonly<Record<string, ToolDef>>

ToolSchema[]


normalizeToolMessages(input): ToolMessages | undefined

Author input → the wire shape, dropping every kind the tool did not declare.

Answers undefined for a tool with nothing to say, so a schema for an ordinary tool is byte-identical to what it was before this field existed — which is what keeps messages off every deployed agent’s tool declarations and out of every snapshot that did not opt in.

ToolMessagesInput | undefined

ToolMessages | undefined


toAgentConfig(source): object

Convert an agent definition into its serializable AgentConfig, injecting the default providers, deriving the session mode, and running the cross-field validation rules. Called from generated bundle entries and the runtime.

AgentConfigSource

object

optional builtinTools?: readonly string[]

optional deadAirCoverMs?: number

optional description?: string

optional errorPhrase?: string

greeting: string

optional idleTimeoutMs?: number

optional interruptionBackoffMs?: number

optional interruptionMinDurationMs?: number

{ kind: string; options: z.ZodRecord<z.ZodString, z.ZodUnknown>; }

optional maxOutputTokens?: number

optional maxRetries?: number

optional maxSteps?: number

optional mcpServers?: Record<string, { pinnedTools?: Record<string, string>; tokenEnv?: string; url: string; }>

optional minBargeInWords?: number

optional mode?: "s2s" | "text" | "pipeline"

name: string

optional page?: "voice" | "static"

optional preemptiveGeneration?: boolean

optional requiredEnv?: readonly string[]

optional resetToolChoice?: boolean

optional resumeFalseInterruption?: boolean

{ kind: string; options: z.ZodRecord<z.ZodString, z.ZodUnknown>; }

optional silencePrompt?: string

optional silenceTimeoutMs?: number

optional startFailurePhrase?: string

optional startSpeakingFloorMs?: number

{ kind: string; options: z.ZodRecord<z.ZodString, z.ZodUnknown>; }

optional sttPrompt?: string

systemPrompt: string

optional telephony?: boolean | readonly string[]

optional temperature?: number

optional text?: true

optional toolChoice?: "auto" | "required" | "none" | { toolName: string; type: "tool"; }

{ kind: string; options: z.ZodRecord<z.ZodString, z.ZodUnknown>; }

optional turnDetection?: string

{ totalTokens?: number; }

{ maxDurationMs?: number; maxWords?: number; }

optional voicePresets?: readonly string[]


toolRegistry(modules): ToolRegistry

Build a checked registry from already-loaded modules.

Synchronous, because the caller that matters most — the generated worker entry — has the modules statically imported already, and a Promise there would put top-level await in a bundle the guest loads.

ToolModules

ToolRegistry


withTools<D>(def, registry): D

Attach a registry to an agent definition, returning the def the runtime runs.

A NEW object rather than a mutation: the def a module default-exports is shared (a spec imports the same one the entry does), and a loader quietly rewriting it makes the order of two imports decide what an agent can do.

It is also the seam every registry NOT assembled by a bundler goes on through. Two do. withToolsDir (@alexkroman1/aai-runtime) scans a real directory for a self-hosted process and comes back here. And the studio’s own coding agent resolves its tool families from a session (aai-guest/studio-agent.ts), which is what makes that honest rather than an exception: a registry resolved from a session instead of from a directory, attached the same way.

Closing over a directory is NOT what puts a registry here — this said so, and templates/coding-agent/ disproves it: nine tools that all close over one directory, shipped as FILES, each re-exporting an entry from a registry shared.ts builds once. What the studio has that a template does not is a directory chosen per SESSION and re-materialized under a running process, where a file’s default export is evaluated once at import. The distinction is LIFETIME, not closure, and it matters because the closure reading would tell an author their tools cannot be files when they can.

A name the def ALREADY holds is an error. Through agent() that is now unreachable — it returns an empty table and refuses a tools argument — so what this catches is a hand-written export default { … tools: {…} } that skipped agent(), and a second withTools over a def that already has one.

A name the def declared as a BUILTIN is an error too, and that one an author can reach. builtinTools: ["calculate"] beside tools/calculate.ts is one file name away at all times, filenames being the only thing here a user picks freely — and it built clean: the runtime’s merge drops the colliding builtin (mergeBuiltinSurface), so the entry the author wrote did nothing and the only trace was one info line in a session log, minted at the first call rather than at the build. That is the same silence discovery was introduced to kill (“forgetting one line was silent”), reached by the other route, and it is the one collision where BOTH halves were declared on purpose — so it is a contradiction to report rather than a precedence to apply.

Structural rather than AgentDef, and it hands back what it was given: a caller keeps whatever else its def carries, and nothing this returns is described by a type the caller did not already name. builtinTools joins the constraint as optional and widened to readonly string[], so a def that carries none still passes and this module still names no builtin catalog.

D extends object

D

ToolRegistry

D

AgentConfig = z.infer<typeof AgentConfigSchema>

JSON-safe subset of the agent definition — the canonical serializable config that flows CLI → server → runtime unchanged.


AgentConfigSource = Omit<AgentConfig, "mode" | "systemPrompt"> & object & { [K in HostOnlyAgentField]?: unknown }

What toAgentConfig accepts: every serializable AgentConfig field (mode excepted — it is derived, never supplied) plus the host-only fields the deny-list strips. AgentDef is assignable to this by construction; the explicit | undefined on the host-only members keeps spread call sites ({...agent, stt: maybeUndefined}) legal under exactOptionalPropertyTypes.

optional systemPrompt?: AgentSystemPrompt

Wider than the config’s own string, because AgentDef.systemPrompt may be a RESOLVER — a function this layer cannot serialize and must not hand onward. Widened here rather than on AgentConfig so AgentDef stays assignable to this by construction, which is what every toAgentConfig(agent) call site relies on. toAgentConfig drops it (see staticSystemPrompt); the runtime holds the agent’s own module and asks the function per request.


HostOnlyAgentField = typeof HOST_ONLY_AGENT_FIELDS[number]

A host-only AgentDef field name stripped by toAgentConfig (tools, events, …).


SessionMode = "s2s" | "pipeline" | "text"

Session mode derived from which provider fields are set.

toAgentConfig, createRuntime, and the server’s IsolateConfigSchema all use assertProviderTriple so there’s one source of truth for the validation.

"text" is the one mode with no audio path at all: the agent is an LLM, a system prompt and its tools, driven by createTextAgent (@alexkroman1/aai-runtime) over a message list rather than by a transport over a socket.


ToolModules = Readonly<Record<string, unknown>>

path → module namespace, which is what both sources produce: Vite’s import.meta.glob (eager) and the static import list the CLI generates.


ToolRegistry = Readonly<Record<string, ToolDef<ToolInputSchema>>>

A checked set of tools, keyed by the name the model calls.


ToolSchema = object

A tool declaration in wire form: name, description, and JSON Schema parameters — the serializable counterpart of ToolDef.

description: string

optional messages?: ToolMessages

The tool’s spoken messages, NORMALIZED — see ToolMessages.

It rides on the wire declaration rather than beside it because that is what makes the feature mean the same thing in every mode: the deployed guest builds this from the agent’s own ToolDefs, and a host-mode client that supplies its own tool declarations gets the behaviour by declaring the field. Nothing here reaches the model — toVercelTools passes name, description and parameters to the provider and reads this itself.

Absent for every tool that declares none, which is what keeps an ordinary tool’s wire declaration byte-identical to what it was before the field existed.

name: string

parameters: JSONSchema7

type: "function"

const HOST_ONLY_AGENT_FIELDS: readonly ["tools", "syncState", "workflows", "subagents", "personas", "dialogs", "events", "inputGuardrails", "outputGuardrails"]

AgentDef fields that must never cross the serialization boundary — the single deny-list toAgentConfig strips. Everything else on the agent definition flows into AgentConfig by default, so a new serializable field works CLI → server → runtime without touching a mapper. A field added to AgentDef must appear either in AgentConfigSchema or here — the type-level guard in the internal-types test enforces that subtraction.

It cannot catch a SUPERFLUOUS entry, which is the other direction and the one that went stale: state sat here after AgentDef.state was deleted with the ctx.state bag, denying a key nothing produces and telling every reader the bag still exists. An entry here is a claim that AgentDef has that field.

Re-exports ToolCompletionMessage


Re-exports ToolDelayedMessage


Re-exports ToolMessageCondition


Re-exports ToolMessages


Re-exports ToolMessagesInput


Re-exports ToolStartMessage