@mantyx/sdk 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -1
- package/dist/a2a-server.cjs.map +1 -1
- package/dist/a2a-server.d.cts +1 -1
- package/dist/a2a-server.d.ts +1 -1
- package/dist/a2a-server.js +1 -1
- package/dist/{chunk-DR625E6B.js → chunk-2K4BGJGJ.js} +20 -1
- package/dist/chunk-2K4BGJGJ.js.map +1 -0
- package/dist/{client-Byb0Zdo7.d.cts → client-LQlx7iYY.d.cts} +63 -2
- package/dist/{client-Byb0Zdo7.d.ts → client-LQlx7iYY.d.ts} +63 -2
- package/dist/index.cjs +20 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/agent-runs-protocol.md +81 -18
- package/docs/wire-protocol.md +115 -25
- package/package.json +1 -1
- package/dist/chunk-DR625E6B.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
### Added
|
|
9
9
|
|
|
10
|
+
- Add supervisor
|
|
11
|
+
|
|
12
|
+
## [0.11.0] — 2026-05-18
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
10
16
|
- Update protocol
|
|
11
17
|
|
|
12
18
|
## [0.10.0] — 2026-05-13
|
|
@@ -86,7 +92,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
86
92
|
|
|
87
93
|
## [0.1.0] — 2026-05-02
|
|
88
94
|
|
|
89
|
-
[unreleased]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.
|
|
95
|
+
[unreleased]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.11.0..HEAD
|
|
96
|
+
[0.11.0]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.10.1..v0.11.0
|
|
90
97
|
[0.10.0]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.9.1..v0.10.0
|
|
91
98
|
[0.9.1]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.9.0..v0.9.1
|
|
92
99
|
[0.9.0]: https://github.com/mantyx-io/mantyx-sdk/compare/v0.8.0..v0.9.0
|
package/dist/a2a-server.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/a2a-server.ts","../src/errors.ts","../src/zod-to-json-schema.ts","../src/client.ts"],"sourcesContent":["/**\n * Expose a MANTYX agent over the [Agent2Agent (A2A)](https://google-a2a.github.io/A2A/)\n * protocol so other agents can talk to it as a peer.\n *\n * This module is loaded from a separate sub-export (`@mantyx/sdk/a2a-server`) so\n * apps that don't need it never pay the bundle cost of the official A2A SDK or\n * Express. To use it, install the optional peer deps:\n *\n * npm install @a2a-js/sdk express\n *\n * @example\n * import { MantyxClient } from \"@mantyx/sdk\";\n * import { serveAgentOverA2A } from \"@mantyx/sdk/a2a-server\";\n *\n * const client = new MantyxClient({\n * apiKey: process.env.MANTYX_API_KEY!,\n * workspaceSlug: process.env.MANTYX_WORKSPACE_SLUG!,\n * });\n *\n * const server = await serveAgentOverA2A({\n * client,\n * port: 4000,\n * agent: { agentId: \"agent_cm6abc123\" },\n * agentCard: {\n * name: \"Acme Support\",\n * description: \"Answers billing and account questions.\",\n * protocolVersion: \"0.3.0\",\n * version: \"1.0.0\",\n * url: \"http://localhost:4000\",\n * skills: [{ id: \"support\", name: \"Support\", description: \"Customer support\",\n * tags: [\"support\"] }],\n * capabilities: { streaming: true, pushNotifications: false },\n * defaultInputModes: [\"text\"],\n * defaultOutputModes: [\"text\"],\n * },\n * });\n *\n * console.log(`Listening on ${server.url}`);\n */\n\nimport type {\n AgentCard,\n Message,\n MessageSendParams,\n Part,\n Task,\n TaskArtifactUpdateEvent,\n TaskStatusUpdateEvent,\n} from \"@a2a-js/sdk\";\nimport type {\n AgentExecutor,\n ExecutionEventBus,\n RequestContext,\n} from \"@a2a-js/sdk/server\";\n\nimport {\n AgentSession,\n type MantyxClient,\n type RunResult,\n type SessionSpec,\n type RunSpec,\n} from \"./client.js\";\nimport { MantyxError, MantyxRunError } from \"./errors.js\";\nimport type { ReasoningLevel, ToolRef } from \"./tools.js\";\n\n// --------------------------------------------------------------- Public API\n\n/**\n * Description of the MANTYX agent that should answer A2A requests.\n *\n * Mirrors the existing `runAgent` / `createSession` argument shape:\n * - `agentId` triggers a persisted workspace agent.\n * - `systemPrompt` (with optional `modelId`, `tools`, …) defines an ephemeral\n * agent inline.\n *\n * Either `agentId` or `systemPrompt` is required.\n */\nexport interface MantyxAgentSpec {\n /** Reference to a persisted MANTYX agent. Mutually exclusive with `systemPrompt`. */\n agentId?: string;\n /** System prompt for an inline / ephemeral agent. Mutually exclusive with `agentId`. */\n systemPrompt?: string;\n modelId?: string;\n tools?: ToolRef[];\n reasoningLevel?: ReasoningLevel;\n metadata?: Record<string, string>;\n budgets?: { maxToolTurns?: number };\n /**\n * Optional human-readable display name for runs created against MANTYX.\n * Visible in the dashboard. Has no effect on the A2A side.\n */\n name?: string;\n}\n\nexport interface MantyxAgentExecutorOptions {\n client: MantyxClient;\n agent: MantyxAgentSpec;\n /**\n * How to map an incoming A2A `contextId` onto a MANTYX session.\n *\n * - `\"auto\"` (default): each unique `contextId` opens a MANTYX session on\n * first contact and reuses it for subsequent messages with the same\n * `contextId`. Gives you multi-turn out of the box.\n * - `\"stateless\"`: every A2A message becomes an independent `runAgent`. No\n * conversational memory; simpler resource model.\n */\n conversation?: \"auto\" | \"stateless\";\n /**\n * LRU cap on the in-memory `contextId -> AgentSession` table. When the cap\n * is exceeded the oldest session is `end()`-ed and evicted. Default: 1024.\n * Only consulted when `conversation: \"auto\"`.\n */\n maxSessions?: number;\n /**\n * Receives streaming MANTYX `assistant_delta` text. The default behaviour\n * forwards every delta as a `TaskStatusUpdateEvent` (state: \"working\")\n * containing the delta as a `text` part — this is what enables A2A\n * `message/stream` clients to see real-time tokens. Override only if you\n * need to swallow them or transform the wire shape.\n */\n onAssistantDelta?: (delta: string, ctx: RequestContext, eventBus: ExecutionEventBus) => void;\n}\n\nexport interface ServeAgentOverA2AOptions extends MantyxAgentExecutorOptions {\n /** A2A Agent Card published at `/.well-known/agent-card.json`. */\n agentCard: AgentCard;\n /** TCP port to listen on. Default: 0 (let the OS pick). */\n port?: number;\n /** Bind address. Default: `\"0.0.0.0\"`. */\n host?: string;\n /** Path that serves the Agent Card JSON. Default: `\"/.well-known/agent-card.json\"`. */\n agentCardPath?: string;\n /** Path that serves the JSON-RPC endpoint. Default: `\"/\"`. */\n jsonRpcPath?: string;\n /**\n * Path that serves the HTTP+JSON/REST endpoint. Default: `\"/v1\"`.\n * Set to `false` to disable the REST mount entirely.\n */\n restPath?: string | false;\n}\n\nexport interface ServeAgentOverA2AHandle {\n /** Origin of the running server, e.g. `\"http://localhost:4000\"`. */\n url: string;\n /** Resolved port number (useful when you let the OS pick one). */\n port: number;\n /** Stop the HTTP server, end every cached MANTYX session, and free MCP transports. */\n close: () => Promise<void>;\n}\n\n// --------------------------------------------------------- Implementation\n\n/**\n * Implementation of `@a2a-js/sdk`'s `AgentExecutor` that backs a MANTYX agent.\n *\n * Most callers want `serveAgentOverA2A` instead; reach for this class directly\n * when you need to mount the executor inside an existing Express, Fastify, or\n * Connect app.\n */\nexport class MantyxAgentExecutor implements AgentExecutor {\n readonly client: MantyxClient;\n readonly agent: MantyxAgentSpec;\n readonly conversation: \"auto\" | \"stateless\";\n readonly maxSessions: number;\n readonly onAssistantDelta?: MantyxAgentExecutorOptions[\"onAssistantDelta\"];\n\n /** contextId -> live MANTYX session. Maintained as an LRU map. */\n private readonly sessions = new Map<string, AgentSession>();\n /** taskIds we've been asked to cancel; checked between turns. */\n private readonly cancelled = new Set<string>();\n /** Pending AbortControllers per task, used for cooperative cancel. */\n private readonly inFlight = new Map<string, AbortController>();\n\n constructor(options: MantyxAgentExecutorOptions) {\n if (!options.client) {\n throw new MantyxError(\"MantyxAgentExecutor: `client` is required\");\n }\n validateAgentSpec(options.agent);\n this.client = options.client;\n this.agent = options.agent;\n this.conversation = options.conversation ?? \"auto\";\n this.maxSessions = options.maxSessions ?? 1024;\n if (options.onAssistantDelta) this.onAssistantDelta = options.onAssistantDelta;\n }\n\n async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {\n const { userMessage, taskId, contextId, task } = requestContext;\n const userText = extractText(userMessage);\n\n const abort = new AbortController();\n this.inFlight.set(taskId, abort);\n\n try {\n // Publish initial Task object on the first turn so streaming clients see\n // a stable id; reusing an existing task otherwise.\n if (!task) {\n eventBus.publish({\n kind: \"task\",\n id: taskId,\n contextId,\n status: { state: \"submitted\", timestamp: new Date().toISOString() },\n history: [userMessage],\n } satisfies Task);\n }\n\n eventBus.publish(statusUpdate(taskId, contextId, \"working\", false));\n\n if (this.cancelled.has(taskId)) {\n eventBus.publish(statusUpdate(taskId, contextId, \"canceled\", true));\n eventBus.finished();\n return;\n }\n\n const onDelta = (delta: string) => {\n if (this.onAssistantDelta) {\n this.onAssistantDelta(delta, requestContext, eventBus);\n return;\n }\n eventBus.publish(deltaStatusUpdate(taskId, contextId, delta));\n };\n\n let result: RunResult;\n try {\n result = await this.runOnce(contextId, userText, onDelta, abort.signal);\n } catch (err) {\n eventBus.publish(\n completedStatusUpdate(\n taskId,\n contextId,\n this.cancelled.has(taskId) ? \"canceled\" : \"failed\",\n errorText(err),\n ),\n );\n eventBus.finished();\n return;\n }\n\n eventBus.publish(completedStatusUpdate(taskId, contextId, \"completed\", result.text ?? \"\"));\n eventBus.finished();\n } finally {\n this.inFlight.delete(taskId);\n this.cancelled.delete(taskId);\n }\n }\n\n async cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void> {\n this.cancelled.add(taskId);\n const ctrl = this.inFlight.get(taskId);\n if (ctrl) ctrl.abort();\n // The active `execute()` call publishes the final 'canceled' status\n // itself; we only need to mark the intent here.\n void eventBus;\n }\n\n /**\n * Close every cached session. Idempotent. Safe to call from server shutdown\n * paths.\n */\n async close(): Promise<void> {\n const sessions = Array.from(this.sessions.values());\n this.sessions.clear();\n await Promise.allSettled(sessions.map((s) => s.end()));\n }\n\n // -------------------------------------------------- private session helpers\n\n private async runOnce(\n contextId: string,\n prompt: string,\n onAssistantDelta: (delta: string) => void,\n signal: AbortSignal,\n ): Promise<RunResult> {\n if (this.conversation === \"stateless\") {\n const runSpec: RunSpec = {\n ...specForRun(this.agent),\n prompt,\n onAssistantDelta,\n signal,\n };\n return this.client.runAgent(runSpec);\n }\n\n const session = await this.getOrCreateSession(contextId);\n return session.send(prompt, { onAssistantDelta, signal });\n }\n\n private async getOrCreateSession(contextId: string): Promise<AgentSession> {\n const existing = this.sessions.get(contextId);\n if (existing) {\n // LRU: bump to most-recently-used.\n this.sessions.delete(contextId);\n this.sessions.set(contextId, existing);\n return existing;\n }\n const sessionSpec: SessionSpec = specForSession(this.agent, contextId);\n const session = await this.client.createSession(sessionSpec);\n this.sessions.set(contextId, session);\n await this.evictIfNeeded();\n return session;\n }\n\n private async evictIfNeeded(): Promise<void> {\n while (this.sessions.size > this.maxSessions) {\n const oldestKey = this.sessions.keys().next().value as string | undefined;\n if (!oldestKey) break;\n const oldest = this.sessions.get(oldestKey)!;\n this.sessions.delete(oldestKey);\n try {\n await oldest.end();\n } catch {\n // Eviction is best-effort; swallow errors so the next request still works.\n }\n }\n }\n}\n\n/**\n * Spin up a small HTTP server that exposes a MANTYX agent as an A2A peer.\n * Mounts the Agent Card, JSON-RPC, and (optionally) REST endpoints from the\n * official `@a2a-js/sdk` library.\n *\n * Throws if `express` / `@a2a-js/sdk` aren't installed; install them as peer\n * deps with `npm install express @a2a-js/sdk`.\n */\nexport async function serveAgentOverA2A(\n options: ServeAgentOverA2AOptions,\n): Promise<ServeAgentOverA2AHandle> {\n const a2a = await loadServerSdk();\n const expressMod = await loadExpress();\n\n const executor = new MantyxAgentExecutor(options);\n const requestHandler = new a2a.DefaultRequestHandler(\n options.agentCard,\n new a2a.InMemoryTaskStore(),\n executor,\n );\n\n const app = expressMod();\n app.use(expressMod.json());\n\n const cardPath = options.agentCardPath ?? \"/.well-known/agent-card.json\";\n const jsonRpcPath = options.jsonRpcPath ?? \"/\";\n const restPath = options.restPath === undefined ? \"/v1\" : options.restPath;\n\n app.use(\n cardPath,\n a2a.expressApp.agentCardHandler({ agentCardProvider: requestHandler }),\n );\n if (restPath !== false) {\n app.use(\n restPath,\n a2a.expressApp.restHandler({\n requestHandler,\n userBuilder: a2a.expressApp.UserBuilder.noAuthentication,\n }),\n );\n }\n // Mount JSON-RPC last so it doesn't shadow the well-known and REST paths.\n app.use(\n jsonRpcPath,\n a2a.expressApp.jsonRpcHandler({\n requestHandler,\n userBuilder: a2a.expressApp.UserBuilder.noAuthentication,\n }),\n );\n\n const port = options.port ?? 0;\n const host = options.host ?? \"0.0.0.0\";\n const server = app.listen(port, host);\n\n await new Promise<void>((resolve, reject) => {\n server.once(\"listening\", resolve);\n server.once(\"error\", reject);\n });\n\n const address = server.address();\n if (!address || typeof address === \"string\") {\n server.close();\n throw new MantyxError(\"serveAgentOverA2A: failed to bind HTTP listener\");\n }\n\n return {\n port: address.port,\n url: `http://${displayHost(host)}:${address.port}`,\n close: async () => {\n await new Promise<void>((resolve, reject) =>\n server.close((err) => (err ? reject(err) : resolve())),\n );\n await executor.close();\n },\n };\n}\n\n// ----------------------------------------------------------- A2A event helpers\n\nfunction statusUpdate(\n taskId: string,\n contextId: string,\n state: \"submitted\" | \"working\" | \"completed\" | \"canceled\" | \"failed\",\n final: boolean,\n): TaskStatusUpdateEvent {\n return {\n kind: \"status-update\",\n taskId,\n contextId,\n status: { state, timestamp: new Date().toISOString() },\n final,\n };\n}\n\nfunction deltaStatusUpdate(\n taskId: string,\n contextId: string,\n delta: string,\n): TaskStatusUpdateEvent {\n return {\n kind: \"status-update\",\n taskId,\n contextId,\n status: {\n state: \"working\",\n timestamp: new Date().toISOString(),\n message: {\n kind: \"message\",\n messageId: randomMessageId(),\n role: \"agent\",\n parts: [{ kind: \"text\", text: delta }],\n contextId,\n taskId,\n },\n },\n final: false,\n };\n}\n\nfunction completedStatusUpdate(\n taskId: string,\n contextId: string,\n state: \"completed\" | \"canceled\" | \"failed\",\n text: string,\n): TaskStatusUpdateEvent {\n return {\n kind: \"status-update\",\n taskId,\n contextId,\n status: {\n state,\n timestamp: new Date().toISOString(),\n message: {\n kind: \"message\",\n messageId: randomMessageId(),\n role: \"agent\",\n parts: [{ kind: \"text\", text }],\n contextId,\n taskId,\n },\n },\n final: true,\n };\n}\n\n// --------------------------------------------------------- Utility helpers\n\nfunction extractText(message: Message | undefined): string {\n if (!message) return \"\";\n const parts = (message.parts as Part[] | undefined) ?? [];\n const out: string[] = [];\n for (const p of parts) {\n if ((p as { kind: string }).kind === \"text\") {\n const t = (p as { text?: unknown }).text;\n if (typeof t === \"string\") out.push(t);\n }\n }\n return out.join(\"\\n\");\n}\n\nfunction specForRun(spec: MantyxAgentSpec): RunSpec {\n const out: RunSpec = {};\n if (spec.agentId) out.agentId = spec.agentId;\n if (spec.systemPrompt) out.systemPrompt = spec.systemPrompt;\n if (spec.modelId) out.modelId = spec.modelId;\n if (spec.tools) out.tools = spec.tools;\n if (spec.reasoningLevel !== undefined) out.reasoningLevel = spec.reasoningLevel;\n if (spec.metadata) out.metadata = spec.metadata;\n if (spec.budgets) out.budgets = spec.budgets;\n if (spec.name) out.name = spec.name;\n return out;\n}\n\nfunction specForSession(spec: MantyxAgentSpec, contextId: string): SessionSpec {\n const out: SessionSpec = {};\n if (spec.agentId) out.agentId = spec.agentId;\n if (spec.systemPrompt) out.systemPrompt = spec.systemPrompt;\n if (spec.modelId) out.modelId = spec.modelId;\n if (spec.tools) out.tools = spec.tools;\n if (spec.reasoningLevel !== undefined) out.reasoningLevel = spec.reasoningLevel;\n // Tag the session with the originating A2A contextId so it's filterable\n // in the MANTYX dashboard.\n const meta: Record<string, string> = { ...(spec.metadata ?? {}) };\n if (!meta.a2a_context_id) meta.a2a_context_id = contextId;\n out.metadata = meta;\n if (spec.budgets) out.budgets = spec.budgets;\n if (spec.name) out.name = spec.name;\n return out;\n}\n\nfunction validateAgentSpec(spec: MantyxAgentSpec): void {\n if (!spec.agentId && (!spec.systemPrompt || spec.systemPrompt.length === 0)) {\n throw new MantyxError(\n \"MantyxAgentExecutor: `agent.agentId` or `agent.systemPrompt` is required\",\n );\n }\n}\n\nfunction errorText(err: unknown): string {\n if (err instanceof MantyxRunError) {\n return `MANTYX run failed (${err.subtype ?? \"unknown\"}): ${err.message}`;\n }\n if (err instanceof Error) return err.message;\n try {\n return String(err);\n } catch {\n return \"unknown error\";\n }\n}\n\nfunction randomMessageId(): string {\n if (typeof globalThis.crypto?.randomUUID === \"function\") {\n return globalThis.crypto.randomUUID();\n }\n // Fallback: timestamp + random suffix; A2A only requires uniqueness.\n return `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction displayHost(host: string): string {\n if (host === \"0.0.0.0\" || host === \"::\") return \"localhost\";\n return host;\n}\n\n// --------------------------------------------------- Optional-dep loaders\n\ninterface ExpressLoader {\n (): import(\"express\").Express;\n json(): import(\"express\").RequestHandler;\n}\n\ninterface A2AServerSdk {\n DefaultRequestHandler: typeof import(\"@a2a-js/sdk/server\").DefaultRequestHandler;\n InMemoryTaskStore: typeof import(\"@a2a-js/sdk/server\").InMemoryTaskStore;\n expressApp: typeof import(\"@a2a-js/sdk/server/express\");\n}\n\nasync function loadExpress(): Promise<ExpressLoader> {\n try {\n const mod = (await import(\"express\")) as unknown as\n | ExpressLoader\n | { default: ExpressLoader };\n return \"default\" in mod ? mod.default : mod;\n } catch (err) {\n throw new MantyxError(\n \"serveAgentOverA2A: `express` is required but not installed. Run `npm install express @a2a-js/sdk` to enable the A2A server.\",\n );\n }\n}\n\nasync function loadServerSdk(): Promise<A2AServerSdk> {\n let server: typeof import(\"@a2a-js/sdk/server\");\n let express: typeof import(\"@a2a-js/sdk/server/express\");\n try {\n server = (await import(\"@a2a-js/sdk/server\")) as typeof import(\"@a2a-js/sdk/server\");\n } catch (err) {\n throw new MantyxError(\n \"serveAgentOverA2A: `@a2a-js/sdk` is required but not installed. Run `npm install @a2a-js/sdk express` to enable the A2A server.\",\n );\n }\n try {\n express = (await import(\n \"@a2a-js/sdk/server/express\"\n )) as typeof import(\"@a2a-js/sdk/server/express\");\n } catch (err) {\n throw new MantyxError(\n \"serveAgentOverA2A: `@a2a-js/sdk/server/express` could not be loaded; ensure the installed `@a2a-js/sdk` is at least v0.3.\",\n );\n }\n return {\n DefaultRequestHandler: server.DefaultRequestHandler,\n InMemoryTaskStore: server.InMemoryTaskStore,\n expressApp: express,\n };\n}\n\n// Re-export for callers that just want to compose the executor with their own\n// transport stack (e.g. plug it into Fastify or Cloudflare Workers).\nexport type {\n AgentCard,\n Message,\n MessageSendParams,\n Task,\n TaskArtifactUpdateEvent,\n TaskStatusUpdateEvent,\n} from \"@a2a-js/sdk\";\nexport type { AgentExecutor, ExecutionEventBus, RequestContext } from \"@a2a-js/sdk/server\";\n","/**\n * Error types raised by the MANTYX SDK.\n */\n\nexport class MantyxError extends Error {\n readonly code: string;\n readonly status: number | undefined;\n readonly hint: string | undefined;\n\n constructor(\n message: string,\n opts: { code?: string; status?: number; hint?: string } = {},\n ) {\n super(message);\n this.name = \"MantyxError\";\n this.code = opts.code ?? \"mantyx_error\";\n this.status = opts.status;\n this.hint = opts.hint;\n }\n}\n\nexport class MantyxNetworkError extends MantyxError {\n constructor(message: string, opts: { cause?: unknown } = {}) {\n super(message, { code: \"network\" });\n this.name = \"MantyxNetworkError\";\n if (opts.cause !== undefined) {\n (this as Error & { cause?: unknown }).cause = opts.cause;\n }\n }\n}\n\nexport class MantyxAuthError extends MantyxError {\n constructor(message = \"Invalid or missing API key / OAuth access token\") {\n super(message, { code: \"unauthorized\", status: 401 });\n this.name = \"MantyxAuthError\";\n }\n}\n\n/**\n * Raised on `403 insufficient_scope`, returned when an OAuth access token\n * is missing one of the scopes a route demands (see\n * `docs/agent-runs-protocol.md` §2.2 for the per-endpoint table).\n *\n * `requiredScopes` carries the verbatim `required` value from the\n * server's response — a single scope for most routes, an array when the\n * route demands more than one. The SDK is expected to surface this so\n * callers can drive a re-consent flow (e.g. \"please re-authorise the\n * app with `sessions:write` enabled\").\n *\n * Workspace API keys never trip this error — they carry no granular\n * scopes. It is OAuth-only.\n */\nexport class MantyxScopeError extends MantyxError {\n /**\n * Scope(s) the route demanded. Always at least one entry; usually\n * exactly one. New routes may demand more scopes in the future.\n */\n readonly requiredScopes: readonly string[];\n\n constructor(message: string, requiredScopes: readonly string[]) {\n super(message, { code: \"insufficient_scope\", status: 403 });\n this.name = \"MantyxScopeError\";\n this.requiredScopes = [...requiredScopes];\n }\n}\n\nexport class MantyxToolError extends MantyxError {\n readonly toolName: string;\n\n constructor(toolName: string, message: string) {\n super(`Local tool ${JSON.stringify(toolName)} failed: ${message}`, {\n code: \"local_tool_failed\",\n });\n this.name = \"MantyxToolError\";\n this.toolName = toolName;\n }\n}\n\n/**\n * Per-run token totals attached to terminal `result` / `error`\n * events. See `docs/agent-runs-protocol.md` §7.1 for the per-provider\n * mapping and the relationship between buckets. Re-exported from\n * `client.ts` so error consumers can pattern-match the triple without\n * a second import.\n */\nexport interface MantyxRunErrorTokens {\n inputTokens: number;\n cachedTokens: number;\n reasoningTokens: number;\n outputTokens: number;\n}\n\n/**\n * Resolved model that executed the run. Surfaced on terminal events\n * by MANTYX ≥ 2026-09. See `docs/agent-runs-protocol.md` §7.1. The\n * `provider` empty / undefined is the \"no usage data\" sentinel.\n */\nexport interface MantyxRunErrorModel {\n id: string;\n provider: string;\n vendorModelId: string;\n reasoningEffort?: string;\n}\n\n/**\n * Optional triage attributes the runner attaches to terminal `error`\n * events. Mirrors the wire fields described in\n * `docs/agent-runs-protocol.md` §7 (\"error event payload fields\") so SDK\n * callers can render structured UI status notes (\"model truncated — JSON\n * likely incomplete\") and drive retry policy without re-parsing the\n * human-readable `message`.\n */\nexport interface MantyxRunErrorInit {\n /**\n * Canonical category of failure. One of `\"rate_limit\"`, `\"overloaded\"`,\n * `\"server\"`, `\"context_window\"`, `\"truncation\"`, `\"invalid_request\"`,\n * `\"auth\"`, `\"timeout\"`, `\"local_timeout\"`, `\"upstream_deadline\"`,\n * `\"unknown\"`. New categories may land additively — callers should\n * default-branch to `\"unknown\"` for unrecognized values.\n */\n errorClass?: string;\n /**\n * Canonical lowercase stop reason normalized across providers\n * (`\"max_tokens\"`, `\"refusal\"`, `\"malformed_function_call\"`, …). When\n * present, mirrors the value carried on the last `assistant_message`\n * event preceding the failure.\n */\n finishReason?: string | null;\n /**\n * **Best-effort raw bytes** the model emitted before the failure. For\n * `outputSchema` runs this is likely **incomplete JSON** that will\n * fail `JSON.parse` — treat it as diagnostic data, never as a\n * schema-conformant reply.\n */\n partialText?: string;\n /**\n * Coarse retry hint inherited from the pipeline's error classifier.\n * Informational; the SDK still owns the actual retry decision.\n */\n retryable?: boolean;\n /**\n * Per-run token totals from the terminal event. Present against\n * MANTYX ≥ 2026-09 — see {@link MantyxRunErrorTokens} and\n * `docs/agent-runs-protocol.md` §7.1. Includes the failing model\n * call's usage when the run errored mid-loop.\n */\n tokens?: MantyxRunErrorTokens;\n /** Total model invocations for the run, including the failing call. */\n turns?: number;\n /** Resolved model that executed the run. See {@link MantyxRunErrorModel}. */\n model?: MantyxRunErrorModel;\n}\n\nexport class MantyxRunError extends MantyxError {\n readonly runId: string;\n readonly subtype: string;\n /** See {@link MantyxRunErrorInit.errorClass}. */\n readonly errorClass: string | undefined;\n /** See {@link MantyxRunErrorInit.finishReason}. */\n readonly finishReason: string | null | undefined;\n /** See {@link MantyxRunErrorInit.partialText}. */\n readonly partialText: string | undefined;\n /** See {@link MantyxRunErrorInit.retryable}. */\n readonly retryable: boolean | undefined;\n /** See {@link MantyxRunErrorInit.tokens}. */\n readonly tokens: MantyxRunErrorTokens | undefined;\n /** See {@link MantyxRunErrorInit.turns}. */\n readonly turns: number | undefined;\n /** See {@link MantyxRunErrorInit.model}. */\n readonly model: MantyxRunErrorModel | undefined;\n\n constructor(\n runId: string,\n subtype: string,\n message: string,\n init: MantyxRunErrorInit = {},\n ) {\n super(message, { code: subtype });\n this.name = \"MantyxRunError\";\n this.runId = runId;\n this.subtype = subtype;\n this.errorClass = init.errorClass;\n this.finishReason = init.finishReason;\n this.partialText = init.partialText;\n this.retryable = init.retryable;\n this.tokens = init.tokens;\n this.turns = init.turns;\n this.model = init.model;\n }\n}\n\n/**\n * Thrown by {@link parseRunOutput} when the run's terminal text was supposed\n * to be a JSON document (because `outputSchema` was set on the spec) but\n * either failed to JSON.parse or failed the user-supplied validator.\n *\n * The original `text` is preserved on the `text` field so callers can log\n * the raw model output for debugging.\n */\nexport class MantyxParseError extends MantyxError {\n readonly text: string;\n\n constructor(message: string, text: string, opts: { cause?: unknown } = {}) {\n super(message, { code: \"output_parse_failed\" });\n this.name = \"MantyxParseError\";\n this.text = text;\n if (opts.cause !== undefined) {\n (this as Error & { cause?: unknown }).cause = opts.cause;\n }\n }\n}\n","/**\n * Lightweight Zod → JSON Schema converter for tool parameter definitions.\n *\n * Tries `z.toJSONSchema` (Zod v4+) first; falls back to a hand-rolled walker\n * for v3 schemas so the SDK works on a wide range of zod versions.\n *\n * The output is a JSON-Schema-shaped object with `type: \"object\"`, `properties`,\n * and `required`. The MANTYX server feeds this to LLM providers verbatim, so\n * unsupported zod features (effects, transforms, intersections) degrade to a\n * permissive `\"object\"` description rather than failing.\n */\nimport { z } from \"zod\";\n\ntype JsonSchema = Record<string, unknown>;\n\ninterface ZodLikeWithToJsonSchema {\n toJSONSchema?: (schema: unknown) => JsonSchema;\n}\n\nexport function zodToJsonSchema(schema: z.ZodType<unknown>): JsonSchema {\n const builtIn = (z as unknown as ZodLikeWithToJsonSchema).toJSONSchema;\n if (typeof builtIn === \"function\") {\n try {\n const out = builtIn.call(z, schema) as JsonSchema;\n if (out && typeof out === \"object\") return out;\n } catch {\n // fall through to manual converter\n }\n }\n return convertNode(schema);\n}\n\nfunction convertNode(schema: z.ZodType<unknown>): JsonSchema {\n const def = (schema as unknown as { _def?: { typeName?: string } })._def;\n const typeName = def?.typeName;\n switch (typeName) {\n case \"ZodString\":\n return { type: \"string\" };\n case \"ZodNumber\":\n return { type: \"number\" };\n case \"ZodBoolean\":\n return { type: \"boolean\" };\n case \"ZodNull\":\n return { type: \"null\" };\n case \"ZodLiteral\": {\n const value = (def as { value?: unknown }).value;\n return { const: value, type: typeof value };\n }\n case \"ZodEnum\": {\n const values = (def as { values?: readonly string[] }).values ?? [];\n return { type: \"string\", enum: [...values] };\n }\n case \"ZodArray\": {\n const inner = (def as { type?: z.ZodType<unknown> }).type;\n return {\n type: \"array\",\n items: inner ? convertNode(inner) : {},\n };\n }\n case \"ZodOptional\":\n case \"ZodNullable\": {\n const inner = (def as { innerType?: z.ZodType<unknown> }).innerType;\n return inner ? convertNode(inner) : {};\n }\n case \"ZodDefault\": {\n const inner = (def as { innerType?: z.ZodType<unknown> }).innerType;\n return inner ? convertNode(inner) : {};\n }\n case \"ZodObject\": {\n const shape = (def as { shape?: () => Record<string, z.ZodType<unknown>> }).shape;\n const fields = typeof shape === \"function\" ? shape() : (shape as Record<string, z.ZodType<unknown>> | undefined);\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n if (fields) {\n for (const [key, value] of Object.entries(fields)) {\n properties[key] = convertNode(value);\n const innerDef = (value as unknown as { _def?: { typeName?: string } })._def;\n const innerTypeName = innerDef?.typeName;\n if (innerTypeName !== \"ZodOptional\" && innerTypeName !== \"ZodDefault\") {\n required.push(key);\n }\n }\n }\n const out: JsonSchema = { type: \"object\", properties };\n if (required.length > 0) out.required = required;\n return out;\n }\n default:\n return {};\n }\n}\n\n/**\n * Coerce a JSON-Schema-shaped value into a wire object suitable for the\n * MANTYX local-tool definition payload. Accepts either a Zod schema or an\n * already-shaped JSON Schema object.\n */\nexport function toToolParametersWire(\n parameters: z.ZodType<unknown> | JsonSchema | undefined,\n): JsonSchema {\n if (!parameters) return { type: \"object\", properties: {} };\n if (typeof (parameters as { _def?: unknown })._def !== \"undefined\") {\n return zodToJsonSchema(parameters as z.ZodType<unknown>);\n }\n return parameters as JsonSchema;\n}\n","/**\n * MANTYX SDK client: HTTP plumbing, model catalog, run + session drivers.\n */\nimport {\n MantyxAuthError,\n MantyxError,\n MantyxNetworkError,\n MantyxParseError,\n MantyxRunError,\n MantyxScopeError,\n MantyxToolError,\n} from \"./errors.js\";\nimport type { MantyxRunErrorInit } from \"./errors.js\";\nimport { callA2A, callMcpTool, closeMcpRefs, resolveLocalRefs } from \"./local-resolver.js\";\nimport type { TokenSource } from \"./oauth.js\";\nimport { readSseStream } from \"./sse.js\";\nimport type {\n LocalA2ATool,\n LocalMcpServer,\n LocalTool,\n ReasoningLevel,\n ToolRef,\n} from \"./tools.js\";\nimport { isLocalA2ATool, isLocalMcpServer, isLocalTool, prefixedMcpToolName } from \"./tools.js\";\nimport { toToolParametersWire } from \"./zod-to-json-schema.js\";\n\nexport const DEFAULT_BASE_URL = \"https://app.mantyx.io\";\n\nexport interface MantyxClientOptions {\n /**\n * Workspace API key (token prefix `mantyx_`) **or** a MANTYX OAuth 2.0\n * access token (token prefix `mantyx_at_`). The server resolves either\n * kind by token-prefix, so the SDK uses a single credential code path.\n *\n * Prefer the {@link accessToken} alias when wiring up an OAuth-based\n * application — the two options are semantically identical (the value\n * is forwarded as `Authorization: Bearer <credential>`), but\n * `accessToken` makes the intent obvious at the call site.\n *\n * Exactly one of `apiKey` / `accessToken` must be set. Passing both —\n * even to the same value — throws `MantyxError` at construction time.\n *\n * See `docs/agent-runs-protocol.md` §2 for the full credential table\n * (including which prefix means what, scope semantics, and the\n * `insufficient_scope` 403 SDKs surface via\n * {@link MantyxScopeError}).\n */\n apiKey?: string;\n /**\n * MANTYX OAuth 2.0 access token (token prefix `mantyx_at_…`). Exactly\n * one of {@link apiKey} / `accessToken` / {@link tokenSource} must be\n * set; passing more than one throws `MantyxError` at construction\n * time.\n *\n * Functionally identical to {@link apiKey} — the SDK ships either\n * value verbatim on `Authorization: Bearer <credential>` — but using\n * the OAuth-specific name makes scope-driven applications easier to\n * read.\n *\n * OAuth tokens additionally enforce per-route **scopes**\n * (`runs:read`, `runs:write`, `sessions:read`, `sessions:write`,\n * `models:read`, `mantyx.identity:read`); see\n * `docs/agent-runs-protocol.md` §2.2 for the table. Missing scopes\n * land as {@link MantyxScopeError} so callers can route the user\n * back to a re-consent flow.\n *\n * Static `accessToken` values are 1-hour-lived per `docs/oauth.md`\n * §\"Token lifetimes & lifecycle\" — for long-running processes\n * prefer {@link tokenSource} so the SDK can refresh transparently.\n */\n accessToken?: string;\n /**\n * Dynamic credential provider. The SDK calls it before every request\n * to obtain the current access token, and again with\n * `reason: \"unauthorized\"` after a 401 so it can refresh and retry\n * the request exactly once.\n *\n * Build one via `oauthClient.refreshTokenSource({ refreshToken })`\n * or `oauthClient.clientCredentialsTokenSource()` — see\n * [`./oauth.ts`](./oauth.ts) for the helpers, or pass any function\n * matching the {@link TokenSource} signature for full custom\n * control (e.g. tokens minted by an upstream auth proxy).\n *\n * Exactly one of {@link apiKey} / {@link accessToken} / `tokenSource`\n * must be set.\n */\n tokenSource?: TokenSource;\n workspaceSlug: string;\n /** Defaults to `https://app.mantyx.io`. Override for self-hosted instances. */\n baseUrl?: string;\n /** Optional `fetch` override (e.g. node-fetch wrapper, or a custom HTTP client). */\n fetch?: typeof fetch;\n /** Default per-request timeout in milliseconds. Default: 60s. */\n timeoutMs?: number;\n}\n\nexport interface ModelInfo {\n id: string;\n label: string;\n provider: string;\n vendorModelId: string;\n source: \"workspace_provider\" | \"platform_offering\";\n contextWindowTokens: number | null;\n pricing: {\n inputPer1MUsd: number | null;\n outputPer1MUsd: number | null;\n cacheReadPer1MUsd: number | null;\n } | null;\n}\n\nexport interface ModelCatalog {\n models: ModelInfo[];\n defaultModelId: string | null;\n}\n\nexport interface AgentSpecBase {\n name?: string;\n /**\n * Reference to a persisted MANTYX agent in this workspace. When set, the\n * server hydrates `systemPrompt`, `modelId`, and the agent's own tools\n * (memory, skills, plugin tools, …) from the Agent row at run time, and any\n * `tools` you supply here are merged on top — typically `local` tools the\n * SDK wants the agent to be able to call back into.\n *\n * Either `agentId` or `systemPrompt` must be set.\n */\n agentId?: string;\n /** Required unless `agentId` is set. */\n systemPrompt?: string;\n modelId?: string;\n tools?: ToolRef[];\n /**\n * Provider thinking strength: a string anchor (`\"off\" | \"low\" | \"medium\" |\n * \"high\"`) or an integer in `0..100` (where `0` explicitly disables provider\n * thinking on reasoning models). The server maps this onto each LLM's\n * native dial — see `docs/agent-runs-protocol.md` §4.4.\n *\n * For session-scoped runs the session value sets the default; per-message\n * overrides on `session.send` apply to that single run.\n */\n reasoningLevel?: ReasoningLevel;\n budgets?: { maxToolTurns?: number };\n /**\n * Constrains the model's **final assistant text** to a JSON document\n * matching a JSON Schema. The terminal `result` event still carries the\n * reply as `text: string`, but that string is guaranteed-parseable JSON.\n *\n * `name` (optional) is a stable identifier the server forwards to the\n * provider (OpenAI `text.format.name`, Anthropic synthetic-tool name).\n * Defaults to `\"output\"`. Must match `/^[a-zA-Z0-9_-]{1,64}$/`.\n *\n * `schema` is a JSON Schema describing the final assistant text. Its\n * root must be a JSON **object** — most providers reject array / scalar\n * roots in structured-output mode. The schema is shipped verbatim;\n * MANTYX does not validate its contents (the provider does).\n *\n * Use {@link parseRunOutput} on the resulting `RunResult` to JSON.parse\n * the reply (and optionally re-validate against your own zod / typebox /\n * ajv schema). See `docs/wire-protocol.md` §7.\n */\n outputSchema?: OutputSchema;\n /**\n * Loop-detection guard. Tracks an order-invariant `(toolName, args)`\n * signature for every assistant turn that emits one or more tool calls;\n * when the same signature repeats consecutively the pipeline first injects\n * a steering nudge (\"either deliver a final answer or change strategy\")\n * and eventually forces a tools-disabled finalise turn.\n *\n * Pass an object to override the default thresholds, or `false` to\n * explicitly disable the guard for this run / session. When omitted, the\n * MANTYX runtime defaults apply (`{ consecutiveThreshold: 3,\n * hardCutoffThreshold: 6 }`). See `docs/agent-runs-protocol.md` §4.6.\n *\n * Each intervention emits an observability-only `loop_detected` SSE event\n * the SDK surfaces on the run-event stream (`tools` lists the looping\n * batch; `hardCutoff: false` is the soft nudge round, `true` is the\n * forced finalise). The synthetic skip + nudge are emitted on the normal\n * `tool_result` / `assistant_delta` channels — the SDK does not need to\n * act on the event itself.\n */\n loopDetection?: LoopDetection | false;\n /**\n * Per-tool call caps enforced over the **lifetime of the run** (across\n * every LLM turn). Calls under the cap run normally; calls past the cap\n * are intercepted before execution and returned to the model as a\n * synthetic \"budget exceeded — pivot or finalize\" tool result.\n *\n * Keys are the model-facing tool names (the same string on\n * `local_tool_call.name`); values are `{ maxCalls: number }`. `maxCalls:\n * 0` disables the tool entirely (the first attempt returns the synthetic\n * body). Budgets are **per-tool, not pooled**.\n *\n * Pass `{}` to start from a clean slate (no defaults applied on top —\n * useful for runs that intentionally want unbounded research). Omit\n * entirely to keep the runtime defaults. Each interception emits an\n * observability-only `tool_budget_exceeded` SSE event. See\n * `docs/agent-runs-protocol.md` §4.7.\n */\n toolBudgets?: ToolBudgets;\n /**\n * Flat string→string KV carried alongside the run / session for\n * observability. Use it to tag runs with your own application identifiers\n * (customer id, environment, workflow name, …) — the values are visible in\n * the MANTYX dashboard and can be filtered there.\n *\n * Limits enforced server-side: max 16 entries; keys match\n * `[A-Za-z0-9._-]{1,64}`; values are strings ≤ 256 chars; serialized JSON\n * ≤ 4 KB. For session-scoped runs, the session's metadata is inherited and\n * any per-message override is merged on top.\n */\n metadata?: Record<string, string>;\n}\n\nexport interface RunSpec extends AgentSpecBase {\n prompt?: string;\n messages?: Array<{ role: \"user\" | \"assistant\" | \"system\"; content: string }>;\n /** Receives streaming assistant text deltas. */\n onAssistantDelta?: (delta: string) => void;\n /** Receives raw events (assistant_message, local_tool_call, tool_result, ...) for advanced consumers. */\n onEvent?: (event: RunEvent) => void;\n /** Aborts the run on the client and best-effort cancels server-side. */\n signal?: AbortSignal;\n}\n\nexport type SessionSpec = AgentSpecBase;\n\n/**\n * Constrains the final assistant text to a JSON document matching a\n * JSON Schema. See {@link AgentSpecBase.outputSchema} for the full\n * semantics.\n */\nexport interface OutputSchema {\n /** Optional. Defaults to `\"output\"`. Must match `/^[a-zA-Z0-9_-]{1,64}$/`. */\n name?: string;\n /** Required. JSON Schema describing the final assistant text. Root must be a JSON object. */\n schema: Record<string, unknown>;\n}\n\n/**\n * Loop-detection thresholds. See {@link AgentSpecBase.loopDetection} for the\n * full semantics. Pass `false` (instead of an object) to disable the guard.\n *\n * Both fields are optional; omitted ones inherit the MANTYX runtime\n * defaults (`consecutiveThreshold: 3`, `hardCutoffThreshold: 6`).\n */\nexport interface LoopDetection {\n /**\n * Number of identical consecutive tool-call batches that triggers the\n * **soft nudge** — the pipeline injects a steering message (\"either\n * deliver a final answer or change strategy\"). Default `3`. Must be\n * `>= 2` (one identical batch is just a single tool call, not a loop).\n * Server-side upper bound: `100`.\n */\n consecutiveThreshold?: number;\n /**\n * Number of identical consecutive tool-call batches that triggers the\n * **hard cutoff** — the pipeline forces a tools-disabled finalise turn.\n * Default `6`. Must be strictly greater than `consecutiveThreshold` (so\n * the soft nudge has a chance to land). Server-side upper bound: `100`.\n */\n hardCutoffThreshold?: number;\n}\n\n/**\n * Per-tool call cap. See {@link AgentSpecBase.toolBudgets} for the full\n * semantics.\n */\nexport interface ToolBudget {\n /**\n * Hard cap on executed calls per run. `0` disables the tool entirely\n * (every attempt returns the synthetic \"budget exceeded\" body on the\n * first try). Server-side upper bound: `1000` (functionally unlimited;\n * the in-runtime `maxToolTurns: 100` fires first).\n */\n maxCalls: number;\n}\n\n/**\n * Map of model-facing tool name → cap. See\n * {@link AgentSpecBase.toolBudgets}. Pass an empty object (`{}`) to start\n * from a clean slate (no runtime defaults applied on top); omit the field\n * entirely to keep the defaults.\n */\nexport type ToolBudgets = Record<string, ToolBudget>;\n\n/**\n * Per-run token totals attached to terminal `result` / `error` events\n * (and to the `GET /agent-runs/:runId` snapshot) by MANTYX ≥ 2026-09.\n *\n * Aggregated across every model invocation for the run. See\n * `docs/agent-runs-protocol.md` §7.1 for the per-provider mapping and\n * the relationship between buckets (`inputTokens` / `outputTokens` are\n * the billable totals; `cachedTokens` and `reasoningTokens` are\n * diagnostic breakdowns _inside_ those two totals, not separate\n * additive buckets).\n *\n * Older servers omit the cost-attribution triple entirely; SDK callers\n * detect \"no usage data\" by checking `result.model?.provider` is empty\n * / undefined.\n */\nexport interface RunTokenUsage {\n /**\n * Total billable input tokens — fresh prompt tokens plus the\n * cached-read slice the provider still bills (at a discount) plus\n * any cache-creation tokens plus tool-prompt tokens. Equal to the\n * sum of every provider-reported input bucket for the run.\n */\n inputTokens: number;\n /**\n * The discounted slice of `inputTokens` that came from a prompt\n * cache hit (Anthropic prompt caching, OpenAI cached prompt, Gemini\n * implicit cache). `0` when the provider doesn't report cache reads\n * or the run didn't hit cache.\n */\n cachedTokens: number;\n /**\n * Non-visible thinking tokens. **Already counted inside\n * `outputTokens`** — surfaced separately so dashboards can break out\n * \"thinking cost\" vs visible output. `0` when the model didn't\n * reason or didn't report it.\n */\n reasoningTokens: number;\n /**\n * All tokens the model emitted for this run, visible + reasoning.\n * Matches the provider's \"completion tokens\" / \"output tokens\"\n * billing line.\n */\n outputTokens: number;\n}\n\n/**\n * The resolved model the platform stamped onto the run, surfaced on\n * terminal `result` / `error` events (and `GET /agent-runs/:runId`)\n * by MANTYX ≥ 2026-09. See `docs/agent-runs-protocol.md` §7.1.\n */\nexport interface RunModelInfo {\n /**\n * Catalog id — the same string a caller would pass back as\n * `modelId` to re-select this exact entry (e.g. `\"platform:demo\"`,\n * `\"provider:cmf…\"`). Empty string against legacy fallbacks that\n * didn't synthesise a catalog id.\n */\n id: string;\n /**\n * Lowercase provider id: `\"openai\"`, `\"anthropic\"`, `\"google\"`,\n * `\"azure-openai\"`. Empty string against legacy runners that don't\n * report usage data — SDK callers use that as the \"no usage data\"\n * signal.\n */\n provider: string;\n /**\n * The model id the platform actually sent to the provider (e.g.\n * `\"gpt-5.4-mini\"`, `\"claude-opus-4-7\"`, `\"gemini-2.5-pro\"`).\n */\n vendorModelId: string;\n /**\n * `\"off\" | \"low\" | \"medium\" | \"high\"`. Omitted when the provider\n * doesn't expose a reasoning-level knob or the run didn't request\n * one.\n */\n reasoningEffort?: string;\n}\n\nexport interface RunResult {\n runId: string;\n text: string;\n events: RunEvent[];\n /**\n * Per-run token totals from the terminal event. Undefined against\n * MANTYX servers older than 2026-09 (the \"no usage data\" signal is\n * `result.model?.provider` being empty / undefined). See\n * {@link RunTokenUsage} and `docs/agent-runs-protocol.md` §7.1.\n */\n tokens?: RunTokenUsage;\n /**\n * Total `engine.completeTurn(...)` invocations for the run,\n * including the failing call when a run errored mid-loop. A\n * single-shot run reports `1`; a tool loop is `>= 2`. Undefined\n * against legacy MANTYX servers.\n */\n turns?: number;\n /** Resolved model that executed the run. See {@link RunModelInfo}. */\n model?: RunModelInfo;\n}\n\nexport interface RunEventBase {\n seq: number;\n type: string;\n}\n\nexport interface AssistantDeltaEvent extends RunEventBase {\n type: \"assistant_delta\";\n text: string;\n}\n\nexport interface ThinkingDeltaEvent extends RunEventBase {\n type: \"thinking_delta\";\n text: string;\n}\n\nexport interface AssistantMessageEvent extends RunEventBase {\n type: \"assistant_message\";\n /**\n * Full assistant text for this turn (concatenation of every preceding\n * `assistant_delta` for the turn, plus any non-streaming snapshot the\n * engine appended at close). May be empty when the turn was tool-only.\n */\n text: string;\n /**\n * 0-based tool-turn index this assistant message closes. Useful for\n * SDK clients pairing the message with the subsequent `tool_result`\n * rows.\n */\n turn?: number;\n /**\n * Canonical lowercase stop reason normalized across providers\n * (`\"end_turn\"`, `\"tool_use\"`, `\"max_tokens\"`, `\"refusal\"`,\n * `\"malformed_function_call\"`, …). `null` / omitted when the provider\n * did not report one.\n */\n finishReason?: string | null;\n /**\n * Tool calls the model emitted on this turn. Omitted when the model\n * did not call any tools.\n */\n toolCalls?: Array<{\n id: string;\n name: string;\n input: Record<string, unknown>;\n }>;\n}\n\nexport interface ServerToolResultEvent extends RunEventBase {\n type: \"tool_result\";\n name: string;\n args?: Record<string, unknown>;\n ok?: boolean;\n summary?: string;\n phase?: \"start\" | \"end\";\n}\n\nexport interface LocalToolCallEvent extends RunEventBase {\n type: \"local_tool_call\";\n toolUseId: string;\n /**\n * The model-facing tool name. For `kind: \"mcp_local\"` events this is the\n * `<server>_<tool>` name the SDK declared on the wire; the SDK looks up\n * the local MCP server via `mcpServer` and forwards `mcpToolName` to\n * `tools/call` rather than parsing the prefix itself.\n */\n name: string;\n args: Record<string, unknown>;\n /**\n * Discriminator for which client-resolved handler should run.\n * - `\"local\"` (or omitted) — generic local tool\n * - `\"a2a_local\"` — local Agent2Agent peer\n * - `\"mcp_local\"` — local MCP server tool\n */\n kind?: \"local\" | \"a2a_local\" | \"mcp_local\";\n /**\n * Present on `kind: \"a2a_local\"` — the full A2A Agent Card the SDK shipped\n * with the spec, echoed back unchanged. Surfaced for advanced consumers\n * (`onEvent` / `streamAgent` callers); the built-in dispatcher ignores it\n * because it already has the cached card from the original\n * `defineLocalA2A` resolution.\n */\n agentCard?: { name: string; url?: string; [k: string]: unknown };\n /** Present on `kind: \"mcp_local\"` — server label declared via `defineLocalMcp`. */\n mcpServer?: string;\n /**\n * Present on `kind: \"mcp_local\"` — the model-facing tool name as declared on\n * the wire. Always equals `name`; surfaced as a separate field for the SDK's\n * convenience when dispatching into a local MCP client.\n */\n mcpToolName?: string;\n /**\n * Present on `kind: \"mcp_local\"` — the verbatim `Implementation` block from\n * MCP `Initialize`, echoed back for observability.\n */\n mcpServerInfo?: { name: string; version?: string; [k: string]: unknown };\n}\n\nexport interface LocalToolResultInEvent extends RunEventBase {\n type: \"local_tool_result_in\";\n toolUseId: string;\n result?: string;\n error?: string;\n}\n\n/**\n * Observability event fired when the loop-detection guard intervenes.\n * The synthetic skip + steering nudge are emitted on the normal\n * `tool_result` / `assistant_delta` channels; this event lets the SDK\n * render a status note (`looping — nudged` / `looping — gave up`).\n *\n * `hardCutoff: false` is the soft nudge round; `true` is the forced\n * finalise. The same run may emit one of each.\n */\nexport interface LoopDetectedEvent extends RunEventBase {\n type: \"loop_detected\";\n /** Length of the identical-batch streak that just tripped the threshold. */\n consecutiveCount: number;\n /** `false` for the soft nudge round; `true` once the pipeline forces finalisation. */\n hardCutoff: boolean;\n /** Names of the tool calls in the looping batch (no args). */\n tools: string[];\n}\n\n/**\n * Observability event fired when a tool-budget interception happens. The\n * synthetic \"budget exceeded — pivot or finalize\" tool result lands on the\n * normal `tool_result` channel before this event fires; the SDK uses this\n * event to render UI banners (`memory budget exhausted` etc.) without\n * re-parsing tool-result bodies.\n */\nexport interface ToolBudgetExceededEvent extends RunEventBase {\n type: \"tool_budget_exceeded\";\n /** Logical tool name (matches the key in `spec.toolBudgets`). */\n tool: string;\n /** Configured cap. */\n maxCalls: number;\n /**\n * 1-based count of attempts to call this tool over the run lifetime.\n * Always strictly greater than `maxCalls`.\n */\n callIndex: number;\n}\n\nexport interface ResultEvent extends RunEventBase {\n type: \"result\";\n subtype: string;\n text?: string;\n error?: string;\n /**\n * Per-run token totals. Present against MANTYX ≥ 2026-09 — see\n * {@link RunTokenUsage} and `docs/agent-runs-protocol.md` §7.1.\n */\n tokens?: RunTokenUsage;\n /** Total model invocations for the run. See {@link RunResult.turns}. */\n turns?: number;\n /** Resolved model that executed the run. See {@link RunModelInfo}. */\n model?: RunModelInfo;\n}\n\nexport interface ErrorEvent extends RunEventBase {\n type: \"error\";\n /** Human-readable failure message. */\n error: string;\n /**\n * Legacy alias for {@link errorClass}. Equals `errorClass` when present;\n * otherwise a small lowercase token (`\"error\"`, `\"invalid_spec\"`,\n * `\"worker_error\"`, …).\n */\n code?: string;\n /**\n * Canonical failure category. One of `\"rate_limit\"`, `\"overloaded\"`,\n * `\"server\"`, `\"context_window\"`, `\"truncation\"`, `\"invalid_request\"`,\n * `\"auth\"`, `\"timeout\"`, `\"local_timeout\"`, `\"upstream_deadline\"`,\n * `\"unknown\"`. New categories may land additively. See\n * `docs/agent-runs-protocol.md` §7 for the full list.\n */\n errorClass?: string;\n /**\n * Canonical lowercase stop reason normalized across providers\n * (`\"max_tokens\"`, `\"refusal\"`, `\"malformed_function_call\"`, …). When\n * present, mirrors the value on the last `assistant_message` event.\n */\n finishReason?: string | null;\n /**\n * **Best-effort raw bytes** the model emitted before the failure. For\n * `outputSchema` runs this is likely **incomplete JSON** that will\n * fail `JSON.parse` — see the wire-protocol truncation contract. Also\n * persisted on `EphemeralAgentRun.finalText` so SDKs can recover it\n * via `GET /agent-runs/:runId` after the SSE stream closes.\n */\n partialText?: string;\n /**\n * Coarse retry hint inherited from the pipeline's error classifier.\n * Informational; the SDK still owns the actual retry decision.\n */\n retryable?: boolean;\n /**\n * Per-run token totals. Present against MANTYX ≥ 2026-09 — see\n * {@link RunTokenUsage} and `docs/agent-runs-protocol.md` §7.1.\n * The pipeline counts the failing model call too, so a run that\n * threw on the first turn reports `turns: 1` with that call's\n * tokens already aggregated.\n */\n tokens?: RunTokenUsage;\n /** Total model invocations for the run, including the failing call. */\n turns?: number;\n /** Resolved model that executed the run. See {@link RunModelInfo}. */\n model?: RunModelInfo;\n}\n\nexport interface CancelledEvent extends RunEventBase {\n type: \"cancelled\";\n reason?: string;\n}\n\nexport type RunEvent =\n | AssistantDeltaEvent\n | ThinkingDeltaEvent\n | AssistantMessageEvent\n | ServerToolResultEvent\n | LocalToolCallEvent\n | LocalToolResultInEvent\n | LoopDetectedEvent\n | ToolBudgetExceededEvent\n | ResultEvent\n | ErrorEvent\n | CancelledEvent\n | (RunEventBase & { type: string; [key: string]: unknown });\n\nexport interface SessionInfo {\n id: string;\n name: string;\n status: \"active\" | \"ended\";\n createdAt: string;\n lastUsedAt: string;\n endedAt: string | null;\n agentSpec: AgentSpecBase;\n messages: Array<{ role: \"user\" | \"assistant\" | \"system\"; content: string }>;\n /** Metadata that was attached to the session at create time, returned for observability. */\n metadata: Record<string, string>;\n}\n\nexport class MantyxClient {\n readonly options: Required<Pick<MantyxClientOptions, \"workspaceSlug\" | \"baseUrl\">> & {\n /**\n * Single resolved bearer credential — either a workspace API key\n * (token prefix `mantyx_`) or an OAuth access token (`mantyx_at_…`).\n * The SDK does not need to distinguish them on the wire; the value\n * is forwarded verbatim on `Authorization: Bearer …`.\n *\n * Kept as `apiKey` (instead of e.g. `credential`) for backwards\n * compatibility — older releases exposed it under this name.\n *\n * Empty string when a {@link tokenSource} is configured — every\n * request resolves the bearer from the source instead.\n */\n apiKey: string;\n fetch: typeof fetch;\n timeoutMs: number;\n /**\n * Dynamic credential provider when constructed with\n * `tokenSource` — see {@link MantyxClientOptions.tokenSource}.\n * `null` for static `apiKey` / `accessToken` clients.\n */\n tokenSource: TokenSource | null;\n };\n\n constructor(opts: MantyxClientOptions) {\n const { credential, tokenSource } = resolveCredential(opts);\n if (!opts.workspaceSlug || typeof opts.workspaceSlug !== \"string\") {\n throw new MantyxError(\"workspaceSlug is required\");\n }\n const f = opts.fetch ?? globalThis.fetch;\n if (typeof f !== \"function\") {\n throw new MantyxError(\n \"Global fetch is not available; pass a custom `fetch` implementation in MantyxClientOptions.\",\n );\n }\n this.options = {\n apiKey: credential,\n workspaceSlug: opts.workspaceSlug,\n baseUrl: (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\"),\n fetch: f,\n timeoutMs: opts.timeoutMs ?? 60_000,\n tokenSource,\n };\n }\n\n // -------------------------------------------------------------- Models\n\n async listModels(): Promise<ModelCatalog> {\n return this.request<ModelCatalog>({\n method: \"GET\",\n path: \"/models\",\n });\n }\n\n // ------------------------------------------------------------- One-shot\n\n async runAgent(spec: RunSpec): Promise<RunResult> {\n const tools = spec.tools ?? [];\n // Resolve every `a2a_local` agent card and open every `mcp_local`\n // transport before submitting; the resolver mutates the refs in place\n // so the subsequent `serializeAgentSpec` reads the resolved data.\n await resolveLocalRefs(tools, { fetch: this.options.fetch });\n const handlers = collectLocalHandlers(tools);\n try {\n const created = await this.request<{ runId: string; streamUrl: string }>({\n method: \"POST\",\n path: \"/agent-runs\",\n body: serializeAgentSpec(spec, {\n prompt: spec.prompt,\n messages: spec.messages,\n }),\n });\n return await this.driveRun(created.runId, handlers, {\n ...(spec.onAssistantDelta ? { onAssistantDelta: spec.onAssistantDelta } : {}),\n ...(spec.onEvent ? { onEvent: spec.onEvent } : {}),\n ...(spec.signal ? { signal: spec.signal } : {}),\n });\n } finally {\n // One-shot runs own their MCP transports; close them on exit.\n await closeMcpRefs(tools);\n }\n }\n\n async *streamAgent(spec: RunSpec): AsyncGenerator<RunEvent, void, void> {\n const tools = spec.tools ?? [];\n await resolveLocalRefs(tools, { fetch: this.options.fetch });\n const handlers = collectLocalHandlers(tools);\n try {\n const created = await this.request<{ runId: string; streamUrl: string }>({\n method: \"POST\",\n path: \"/agent-runs\",\n body: serializeAgentSpec(spec, {\n prompt: spec.prompt,\n messages: spec.messages,\n }),\n });\n yield* this.streamRunEvents(created.runId, handlers, spec.signal);\n } finally {\n await closeMcpRefs(tools);\n }\n }\n\n /**\n * Internal registry of client-resolved tool handlers. Exposed for callers\n * who drive the run loop manually via `driveRun` / `streamRunEvents`.\n */\n collectHandlers(tools: ToolRef[]): LocalHandlers {\n return collectLocalHandlers(tools);\n }\n\n // ------------------------------------------------------------- Sessions\n\n async createSession(spec: SessionSpec): Promise<AgentSession> {\n const tools = spec.tools ?? [];\n // Resolve local refs once at session creation; the session keeps the\n // resolved cards / live MCP connections for its lifetime.\n await resolveLocalRefs(tools, { fetch: this.options.fetch });\n const handlers = collectLocalHandlers(tools);\n const created = await this.request<{ sessionId: string; name: string; createdAt: string }>({\n method: \"POST\",\n path: \"/agent-sessions\",\n body: serializeAgentSpec(spec),\n });\n return new AgentSession(this, created.sessionId, handlers, tools);\n }\n\n /**\n * Re-emit a `local_tool_call` event into the right local handler. Useful\n * for tests and for users who consume events via `streamAgent` themselves.\n */\n async dispatchLocalToolFromEvent(\n runId: string,\n ev: LocalToolCallEvent,\n handlers: LocalHandlers,\n ): Promise<void> {\n return this.dispatchLocalTool(runId, ev, handlers);\n }\n\n async resumeSession(\n sessionId: string,\n opts: { tools?: ToolRef[] } = {},\n ): Promise<AgentSession> {\n // Verify the session exists and is still active. Optionally refresh tool defs.\n await this.getSessionInfo(sessionId);\n const tools = opts.tools ?? [];\n if (tools.length > 0) {\n // Resolve before the first send — mirrors createSession.\n await resolveLocalRefs(tools, { fetch: this.options.fetch });\n }\n const handlers = collectLocalHandlers(tools);\n return new AgentSession(this, sessionId, handlers, tools);\n }\n\n async endSession(sessionId: string): Promise<void> {\n await this.request<{ ok: boolean }>({\n method: \"DELETE\",\n path: `/agent-sessions/${encodeURIComponent(sessionId)}`,\n });\n }\n\n async getSessionInfo(sessionId: string): Promise<SessionInfo> {\n return this.request<SessionInfo>({\n method: \"GET\",\n path: `/agent-sessions/${encodeURIComponent(sessionId)}`,\n });\n }\n\n // ----------------------------------------------------------- Internals\n\n /** Drive an existing run to completion (collect events, dispatch local tools). */\n async driveRun(\n runId: string,\n handlers: LocalHandlers,\n opts: {\n onAssistantDelta?: (delta: string) => void;\n onEvent?: (event: RunEvent) => void;\n signal?: AbortSignal;\n } = {},\n ): Promise<RunResult> {\n const collected: RunEvent[] = [];\n let finalText = \"\";\n // Cost-attribution triple, populated from the terminal event when\n // MANTYX ≥ 2026-09 surfaces it. Older runners omit the fields and\n // we leave the result's `tokens` / `turns` / `model` undefined —\n // callers detect \"no usage data\" via `result.model?.provider`.\n let tokens: RunTokenUsage | undefined;\n let turns: number | undefined;\n let modelInfo: RunModelInfo | undefined;\n for await (const ev of this.streamRunEvents(runId, handlers, opts.signal)) {\n collected.push(ev);\n if (opts.onEvent) opts.onEvent(ev);\n if (ev.type === \"assistant_delta\" && opts.onAssistantDelta) {\n opts.onAssistantDelta((ev as AssistantDeltaEvent).text);\n }\n if (ev.type === \"result\") {\n const r = ev as ResultEvent;\n tokens = parseRunTokens(r.tokens) ?? tokens;\n turns = parseRunTurns(r.turns) ?? turns;\n modelInfo = parseRunModel(r.model) ?? modelInfo;\n if (r.subtype === \"success\") {\n finalText = typeof r.text === \"string\" ? r.text : \"\";\n } else {\n const errInit: MantyxRunErrorInit = {};\n if (tokens !== undefined) errInit.tokens = tokens;\n if (turns !== undefined) errInit.turns = turns;\n if (modelInfo !== undefined) errInit.model = modelInfo;\n throw new MantyxRunError(runId, r.subtype, r.error ?? r.subtype, errInit);\n }\n } else if (ev.type === \"error\") {\n const e = ev as ErrorEvent;\n // The wire reports both a coarse `code` (legacy alias) and a\n // canonical `errorClass` triage category; prefer `errorClass`\n // when present so the SDK exposes a stable taxonomy. See\n // `docs/agent-runs-protocol.md` §7.\n const subtype = e.errorClass ?? e.code ?? \"error\";\n const errInit: MantyxRunErrorInit = {};\n if (e.errorClass !== undefined) errInit.errorClass = e.errorClass;\n if (e.finishReason !== undefined) errInit.finishReason = e.finishReason;\n if (typeof e.partialText === \"string\") errInit.partialText = e.partialText;\n if (typeof e.retryable === \"boolean\") errInit.retryable = e.retryable;\n const errTokens = parseRunTokens(e.tokens);\n if (errTokens !== undefined) errInit.tokens = errTokens;\n const errTurns = parseRunTurns(e.turns);\n if (errTurns !== undefined) errInit.turns = errTurns;\n const errModel = parseRunModel(e.model);\n if (errModel !== undefined) errInit.model = errModel;\n throw new MantyxRunError(runId, subtype, e.error, errInit);\n } else if (ev.type === \"cancelled\") {\n throw new MantyxRunError(runId, \"cancelled\", \"Run was cancelled\");\n }\n }\n const result: RunResult = { runId, text: finalText, events: collected };\n if (tokens !== undefined) result.tokens = tokens;\n if (turns !== undefined) result.turns = turns;\n if (modelInfo !== undefined) result.model = modelInfo;\n return result;\n }\n\n async *streamRunEvents(\n runId: string,\n handlers: LocalHandlers,\n signal?: AbortSignal,\n ): AsyncGenerator<RunEvent, void, void> {\n const url = this.absoluteUrl(`/agent-runs/${encodeURIComponent(runId)}/stream`);\n let lastSeq = 0;\n while (true) {\n const reqUrl = lastSeq > 0 ? `${url}?lastSeq=${lastSeq}` : url;\n const res = await this.openSseStream(reqUrl, lastSeq, signal);\n if (!res.ok) {\n throw await this.errorFromResponse(res);\n }\n let terminal = false;\n try {\n for await (const sseEvent of readSseStream(res.body, { ...(signal ? { signal } : {}) })) {\n let data: Record<string, unknown> = {};\n try {\n data = JSON.parse(sseEvent.data || \"{}\") as Record<string, unknown>;\n } catch {\n data = {};\n }\n const evType = sseEvent.event ?? (data.type as string | undefined) ?? \"message\";\n const seq = typeof data.seq === \"number\" ? data.seq : lastSeq;\n if (typeof seq === \"number\" && seq > lastSeq) lastSeq = seq;\n const ev = { seq, type: evType, ...data } as RunEvent;\n yield ev;\n if (evType === \"local_tool_call\") {\n const localEv = ev as LocalToolCallEvent;\n void this.dispatchLocalTool(runId, localEv, handlers).catch((err) => {\n // best-effort logging; the run will surface a `result/error` if the\n // server eventually times out.\n console.error(\"[mantyx-sdk] local tool dispatch failed:\", err);\n });\n }\n if (evType === \"result\" || evType === \"error\" || evType === \"cancelled\") {\n terminal = true;\n return;\n }\n }\n } catch (err) {\n if (signal?.aborted) {\n throw new MantyxRunError(runId, \"cancelled\", \"Run was cancelled by the client\");\n }\n // Network blip — retry after a tiny backoff with `?lastSeq=`.\n await sleep(500);\n continue;\n }\n if (terminal) return;\n // Stream closed without a terminal event (server restart, etc.) — reconnect.\n }\n }\n\n async dispatchLocalTool(\n runId: string,\n ev: LocalToolCallEvent,\n handlers: LocalHandlers,\n ): Promise<void> {\n const kind = ev.kind ?? \"local\";\n try {\n let out: string;\n if (kind === \"a2a_local\") {\n const tool = handlers.a2aTools.get(ev.name);\n if (!tool) {\n await this.postToolResult(runId, ev.toolUseId, {\n error: `No local A2A handler registered for tool ${JSON.stringify(ev.name)}`,\n });\n return;\n }\n const message = typeof ev.args?.message === \"string\" ? (ev.args.message as string) : \"\";\n out = await callA2A(tool, { message }, { fetch: this.options.fetch });\n } else if (kind === \"mcp_local\") {\n const serverName = ev.mcpServer ?? \"\";\n const mcpToolName = ev.mcpToolName ?? \"\";\n const server = handlers.mcpServers.get(serverName);\n if (!server) {\n await this.postToolResult(runId, ev.toolUseId, {\n error: `No local MCP server registered as ${JSON.stringify(serverName)}`,\n });\n return;\n }\n // The wire-prefixed tool name (`<server>_<tool>`) is what the model\n // sees; the upstream MCP server uses the bare name. Strip the prefix\n // before forwarding to `tools/call`.\n const upstreamName = mcpToolName.startsWith(`${serverName}_`)\n ? mcpToolName.slice(serverName.length + 1)\n : mcpToolName;\n out = await callMcpTool(server, upstreamName, ev.args ?? {});\n } else {\n const handler = handlers.localTools.get(ev.name);\n if (!handler) {\n await this.postToolResult(runId, ev.toolUseId, {\n error: `No local handler registered for tool ${JSON.stringify(ev.name)}`,\n });\n return;\n }\n const args = handler.parameters\n ? (handler.parameters.parse?.(ev.args) as Record<string, unknown>) ?? ev.args\n : ev.args;\n const result = await handler.execute(args);\n out = typeof result === \"string\" ? result : JSON.stringify(result);\n }\n await this.postToolResult(runId, ev.toolUseId, { result: out });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const handlerName = describeHandlerName(ev);\n await this.postToolResult(runId, ev.toolUseId, {\n error: new MantyxToolError(handlerName, message).message,\n });\n }\n }\n\n async postToolResult(\n runId: string,\n toolUseId: string,\n payload: { result?: string; error?: string },\n ): Promise<void> {\n await this.request<{ ok: boolean }>({\n method: \"POST\",\n path: `/agent-runs/${encodeURIComponent(runId)}/tool-results`,\n body: { toolUseId, ...payload },\n });\n }\n\n async cancelRun(runId: string): Promise<void> {\n await this.request<{ ok: boolean }>({\n method: \"POST\",\n path: `/agent-runs/${encodeURIComponent(runId)}/cancel`,\n });\n }\n\n // -------------------------------------------------------------- HTTP\n\n private absoluteUrl(path: string): string {\n return `${this.options.baseUrl}/api/v1/workspaces/${encodeURIComponent(this.options.workspaceSlug)}${path}`;\n }\n\n /**\n * Resolve the bearer credential to send on the next request. With a\n * static `apiKey` / `accessToken` this is a synchronous reach into\n * `options.apiKey`; with a {@link TokenSource} it delegates so the\n * source can refresh expired access tokens before we hit the wire.\n *\n * The `reason` is forwarded to the source verbatim. Pass\n * `\"unauthorized\"` immediately after a 401 so the source forces a\n * refresh rather than handing back its (now-invalid) cached value.\n */\n private async resolveBearer(reason: \"initial\" | \"unauthorized\" = \"initial\"): Promise<string> {\n if (this.options.tokenSource) return this.options.tokenSource(reason);\n return this.options.apiKey;\n }\n\n /**\n * Open an SSE stream against `reqUrl` with at-most-one refresh +\n * retry on 401. The caller is responsible for the subsequent\n * `readSseStream` loop; this helper only handles the initial GET.\n * Mid-stream 401s propagate as `MantyxNetworkError` from the read\n * loop and trigger a reconnect via the outer `while` in\n * {@link streamRunEvents}.\n */\n private async openSseStream(\n reqUrl: string,\n lastSeq: number,\n signal: AbortSignal | undefined,\n ): Promise<Response> {\n const openOnce = async (reason: \"initial\" | \"unauthorized\"): Promise<Response> => {\n const auth = await this.authHeaders(reason);\n return this.options.fetch(reqUrl, {\n method: \"GET\",\n headers: {\n ...auth,\n Accept: \"text/event-stream\",\n ...(lastSeq > 0 ? { \"Last-Event-ID\": String(lastSeq) } : {}),\n },\n ...(signal ? { signal } : {}),\n }).catch((err: unknown) => {\n throw new MantyxNetworkError(`Failed to open SSE stream: ${(err as Error).message}`, {\n cause: err,\n });\n });\n };\n const res = await openOnce(\"initial\");\n if (res.status === 401 && this.options.tokenSource !== null) {\n try {\n await res.text();\n } catch {\n // ignore\n }\n return openOnce(\"unauthorized\");\n }\n return res;\n }\n\n private async authHeaders(\n reason: \"initial\" | \"unauthorized\" = \"initial\",\n ): Promise<Record<string, string>> {\n const bearer = await this.resolveBearer(reason);\n return { Authorization: `Bearer ${bearer}` };\n }\n\n async request<T>(args: {\n method: string;\n path: string;\n body?: unknown;\n timeoutMs?: number;\n }): Promise<T> {\n return this.requestWithRetry<T>(args, \"initial\");\n }\n\n private async requestWithRetry<T>(\n args: { method: string; path: string; body?: unknown; timeoutMs?: number },\n reason: \"initial\" | \"unauthorized\",\n ): Promise<T> {\n const url = this.absoluteUrl(args.path);\n const ctrl = new AbortController();\n const t = setTimeout(() => ctrl.abort(), args.timeoutMs ?? this.options.timeoutMs);\n try {\n const auth = await this.authHeaders(reason);\n const res = await this.options.fetch(url, {\n method: args.method,\n headers: {\n ...auth,\n ...(args.body !== undefined ? { \"Content-Type\": \"application/json\" } : {}),\n Accept: \"application/json\",\n },\n ...(args.body !== undefined ? { body: JSON.stringify(args.body) } : {}),\n signal: ctrl.signal,\n }).catch((err: unknown) => {\n if (ctrl.signal.aborted) {\n throw new MantyxNetworkError(`Request timed out after ${args.timeoutMs ?? this.options.timeoutMs}ms`);\n }\n throw new MantyxNetworkError(`Network error: ${(err as Error).message}`, { cause: err });\n });\n if (!res.ok) {\n // 401 with a configured TokenSource: refresh the access token\n // and retry the original request exactly once. Static-credential\n // clients (no source) fall straight through to `MantyxAuthError`.\n if (\n res.status === 401 &&\n this.options.tokenSource !== null &&\n reason === \"initial\"\n ) {\n // Drain the body so the socket can be reused.\n try {\n await res.text();\n } catch {\n // ignore\n }\n clearTimeout(t);\n return this.requestWithRetry<T>(args, \"unauthorized\");\n }\n throw await this.errorFromResponse(res);\n }\n const text = await res.text();\n if (!text) return undefined as unknown as T;\n try {\n return JSON.parse(text) as T;\n } catch (err) {\n throw new MantyxError(`Failed to parse JSON response: ${(err as Error).message}`);\n }\n } finally {\n clearTimeout(t);\n }\n }\n\n private async errorFromResponse(res: Response): Promise<MantyxError> {\n let body: {\n error?: string;\n code?: string;\n hint?: string;\n required?: string | string[];\n } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n // ignore\n }\n if (res.status === 401) {\n return new MantyxAuthError(body.error ?? \"Invalid API key or OAuth access token\");\n }\n // `403 insufficient_scope` is the OAuth \"missing scope\" signal. The\n // server may report `error` or `code` as the discriminator depending\n // on the route; check both. See `docs/agent-runs-protocol.md` §2.3.\n if (res.status === 403 && (body.error === \"insufficient_scope\" || body.code === \"insufficient_scope\")) {\n const required = parseRequiredScopes(body.required, res.headers.get(\"WWW-Authenticate\"));\n const msg = required.length > 0\n ? `Missing OAuth scope${required.length > 1 ? \"s\" : \"\"}: ${required.join(\", \")}`\n : \"OAuth access token is missing a required scope\";\n return new MantyxScopeError(msg, required);\n }\n return new MantyxError(body.error ?? `HTTP ${res.status}`, {\n code: body.code ?? `http_${res.status}`,\n status: res.status,\n ...(body.hint ? { hint: body.hint } : {}),\n });\n }\n}\n\n// ---------------------------------------------------------------- Sessions\n\nexport class AgentSession {\n readonly id: string;\n readonly client: MantyxClient;\n private readonly handlers: LocalHandlers;\n private readonly tools: ToolRef[];\n\n constructor(\n client: MantyxClient,\n id: string,\n handlers: LocalHandlers,\n tools?: ToolRef[],\n ) {\n this.client = client;\n this.id = id;\n this.handlers = handlers;\n this.tools = tools ?? [];\n }\n\n async send(\n prompt: string,\n opts: {\n onAssistantDelta?: (s: string) => void;\n signal?: AbortSignal;\n /**\n * Per-message metadata override. Server-side this is merged on top of\n * the session's metadata at run-creation time (run-level keys win).\n * Useful for tagging individual turns (e.g. `{ \"trace_id\": \"abc\" }`).\n */\n metadata?: Record<string, string>;\n /**\n * Per-message override for `reasoningLevel`. Applies only to this run\n * and does not mutate the session's stored value.\n */\n reasoningLevel?: ReasoningLevel;\n /**\n * Per-message override for `outputSchema`. Applies only to this run\n * and does not mutate the session's stored value.\n */\n outputSchema?: OutputSchema;\n /**\n * Per-message override for `loopDetection`. Applies only to this run\n * and does not mutate the session's stored value. Pass `false` to\n * disable the guard for this single turn.\n */\n loopDetection?: LoopDetection | false;\n /**\n * Per-message override for `toolBudgets`. Applies only to this run\n * and does not mutate the session's stored value.\n */\n toolBudgets?: ToolBudgets;\n } = {},\n ): Promise<RunResult> {\n const created = await this.client.request<{ runId: string; streamUrl: string }>({\n method: \"POST\",\n path: `/agent-sessions/${encodeURIComponent(this.id)}/messages`,\n body: this.buildSessionMessageBody(prompt, opts),\n });\n return this.client.driveRun(created.runId, this.handlers, {\n ...(opts.onAssistantDelta ? { onAssistantDelta: opts.onAssistantDelta } : {}),\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n }\n\n async *stream(\n prompt: string,\n opts: {\n signal?: AbortSignal;\n metadata?: Record<string, string>;\n reasoningLevel?: ReasoningLevel;\n outputSchema?: OutputSchema;\n loopDetection?: LoopDetection | false;\n toolBudgets?: ToolBudgets;\n } = {},\n ): AsyncGenerator<RunEvent, void, void> {\n const created = await this.client.request<{ runId: string; streamUrl: string }>({\n method: \"POST\",\n path: `/agent-sessions/${encodeURIComponent(this.id)}/messages`,\n body: this.buildSessionMessageBody(prompt, opts),\n });\n yield* this.client.streamRunEvents(created.runId, this.handlers, opts.signal);\n }\n\n private buildSessionMessageBody(\n prompt: string,\n opts: {\n metadata?: Record<string, string>;\n reasoningLevel?: ReasoningLevel;\n outputSchema?: OutputSchema;\n loopDetection?: LoopDetection | false;\n toolBudgets?: ToolBudgets;\n },\n ): Record<string, unknown> {\n const body: Record<string, unknown> = { prompt };\n if (this.tools.length > 0) body.tools = serializeToolRefs(this.tools);\n if (opts.metadata && Object.keys(opts.metadata).length > 0) body.metadata = opts.metadata;\n if (opts.reasoningLevel !== undefined) {\n body.reasoningLevel = normalizeReasoningLevel(opts.reasoningLevel);\n }\n if (opts.outputSchema !== undefined) {\n body.outputSchema = normalizeOutputSchema(opts.outputSchema);\n }\n if (opts.loopDetection !== undefined) {\n body.loopDetection = normalizeLoopDetection(opts.loopDetection);\n }\n if (opts.toolBudgets !== undefined) {\n body.toolBudgets = normalizeToolBudgets(opts.toolBudgets);\n }\n return body;\n }\n\n async history(): Promise<Array<{ role: \"user\" | \"assistant\" | \"system\"; content: string }>> {\n const info = await this.client.getSessionInfo(this.id);\n return info.messages;\n }\n\n async info(): Promise<SessionInfo> {\n return this.client.getSessionInfo(this.id);\n }\n\n async end(): Promise<void> {\n try {\n await this.client.endSession(this.id);\n } finally {\n // Close any MCP transports the session opened.\n await closeMcpRefs(this.tools);\n }\n }\n}\n\n// ---------------------------------------------------------------- Helpers\n\nfunction serializeAgentSpec(\n spec: AgentSpecBase,\n extra: { prompt?: string; messages?: Array<{ role: string; content: string }> } = {},\n): Record<string, unknown> {\n if (!spec.agentId && (typeof spec.systemPrompt !== \"string\" || spec.systemPrompt.length === 0)) {\n throw new MantyxError(\"Either `agentId` or `systemPrompt` is required\");\n }\n const body: Record<string, unknown> = {\n tools: serializeToolRefs(spec.tools ?? []),\n };\n if (typeof spec.systemPrompt === \"string\") body.systemPrompt = spec.systemPrompt;\n if (spec.agentId) body.agentId = spec.agentId;\n if (spec.name) body.name = spec.name;\n if (spec.modelId) body.modelId = spec.modelId;\n if (spec.reasoningLevel !== undefined) {\n body.reasoningLevel = normalizeReasoningLevel(spec.reasoningLevel);\n }\n if (spec.outputSchema !== undefined) {\n body.outputSchema = normalizeOutputSchema(spec.outputSchema);\n }\n if (spec.loopDetection !== undefined) {\n body.loopDetection = normalizeLoopDetection(spec.loopDetection);\n }\n if (spec.toolBudgets !== undefined) {\n body.toolBudgets = normalizeToolBudgets(spec.toolBudgets);\n }\n if (spec.budgets) body.budgets = spec.budgets;\n if (spec.metadata && Object.keys(spec.metadata).length > 0) body.metadata = spec.metadata;\n if (extra.prompt !== undefined) body.prompt = extra.prompt;\n if (extra.messages !== undefined) body.messages = extra.messages;\n return body;\n}\n\nfunction serializeToolRefs(tools: ToolRef[]): unknown[] {\n return tools.map((t) => {\n switch (t.kind) {\n case \"mantyx\":\n return { kind: \"mantyx\", id: t.id };\n case \"mantyx_plugin\":\n return { kind: \"mantyx_plugin\", name: t.name };\n case \"local\":\n return {\n kind: \"local\",\n name: t.name,\n description: t.description,\n parameters: toToolParametersWire(t.parameters),\n ...(t.outputSchema !== undefined\n ? { outputSchema: toToolParametersWire(t.outputSchema) }\n : {}),\n ...(t.longRunning ? { longRunning: true } : {}),\n };\n case \"a2a\":\n return {\n kind: \"a2a\",\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n agentCardUrl: t.agentCardUrl,\n ...(t.headers ? { headers: { ...t.headers } } : {}),\n ...(t.contextId ? { contextId: t.contextId } : {}),\n };\n case \"a2a_local\": {\n const card = t._resolvedCard;\n if (!card) {\n throw new MantyxError(\n `defineLocalA2A(${JSON.stringify(t.name)}): agent card has not been resolved yet (was \\`runAgent\\` / \\`createSession\\` skipped?)`,\n );\n }\n return {\n kind: \"a2a_local\",\n name: t.name,\n // The wire ships the resolved A2A Agent Card. Shallow-clone so\n // consumers can mutate the input later without affecting the\n // wire payload.\n agentCard: { ...card },\n };\n }\n case \"mcp\":\n return {\n kind: \"mcp\",\n name: t.name,\n url: t.url,\n ...(t.headers ? { headers: { ...t.headers } } : {}),\n ...(t.toolFilter ? { toolFilter: [...t.toolFilter] } : {}),\n };\n case \"mcp_local\": {\n const resolved = t._resolved;\n if (!resolved) {\n throw new MantyxError(\n `defineLocalMcp(${JSON.stringify(t.name)}): MCP server has not been initialised yet`,\n );\n }\n // The SDK owns naming for `mcp_local` (MANTYX does no prefixing).\n // We auto-prefix each upstream tool name with the server label so\n // the model-facing surface is `<server>_<tool>` — mirroring how\n // MANTYX prefixes for `kind: \"mcp\"`.\n const tools = resolved.tools.map((tool) => {\n const wire: Record<string, unknown> = {\n name: prefixedMcpToolName(t.name, tool.name),\n inputSchema: tool.inputSchema,\n };\n if (typeof tool.description === \"string\") wire.description = tool.description;\n if (tool.annotations) wire.annotations = tool.annotations;\n return wire;\n });\n return {\n kind: \"mcp_local\",\n name: t.name,\n serverInfo: { ...resolved.serverInfo },\n tools,\n };\n }\n }\n });\n}\n\n/** Internal registry of client-resolved handlers, indexed by `kind`. */\nexport interface LocalHandlers {\n /** `kind: \"local\"` — generic local tools, indexed by tool name. */\n localTools: Map<string, LocalTool>;\n /** `kind: \"a2a_local\"` — local A2A peers, indexed by tool name. */\n a2aTools: Map<string, LocalA2ATool>;\n /** `kind: \"mcp_local\"` — local MCP servers, indexed by server name. */\n mcpServers: Map<string, LocalMcpServer>;\n}\n\nfunction collectLocalHandlers(tools: ReadonlyArray<ToolRef>): LocalHandlers {\n const localTools = new Map<string, LocalTool>();\n const a2aTools = new Map<string, LocalA2ATool>();\n const mcpServers = new Map<string, LocalMcpServer>();\n for (const t of tools) {\n if (isLocalTool(t)) {\n localTools.set(t.name, t);\n } else if (isLocalA2ATool(t)) {\n a2aTools.set(t.name, t);\n } else if (isLocalMcpServer(t)) {\n mcpServers.set(t.name, t);\n }\n }\n return { localTools, a2aTools, mcpServers };\n}\n\nfunction describeHandlerName(ev: LocalToolCallEvent): string {\n if (ev.kind === \"mcp_local\" && ev.mcpServer && ev.mcpToolName) {\n return `${ev.mcpServer}/${ev.mcpToolName}`;\n }\n return ev.name;\n}\n\nfunction normalizeReasoningLevel(level: ReasoningLevel): string | number {\n if (typeof level === \"number\") {\n if (!Number.isFinite(level) || level < 0 || level > 100) {\n throw new MantyxError(\n `reasoningLevel must be a string anchor or an integer in 0..100, got ${level}`,\n );\n }\n return Math.trunc(level);\n }\n if (level === \"off\" || level === \"low\" || level === \"medium\" || level === \"high\") {\n return level;\n }\n throw new MantyxError(\n `reasoningLevel must be one of \"off\" | \"low\" | \"medium\" | \"high\" or a number 0..100, got ${JSON.stringify(level)}`,\n );\n}\n\nconst OUTPUT_SCHEMA_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;\nconst OUTPUT_SCHEMA_MAX_BYTES = 32 * 1024;\n\n/**\n * Validate an `OutputSchema` value and return the wire-shaped object.\n *\n * Mirrors the server-side `400 invalid_request` checks (name regex, schema\n * shape, ≤ 32 KB serialized) so callers get an early local error instead of\n * a round-trip rejection.\n */\nfunction normalizeOutputSchema(value: OutputSchema): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new MantyxError(\n `outputSchema must be an object of shape { name?, schema }, got ${JSON.stringify(value)}`,\n );\n }\n const out: Record<string, unknown> = {};\n if (value.name !== undefined) {\n if (typeof value.name !== \"string\" || !OUTPUT_SCHEMA_NAME_RE.test(value.name)) {\n throw new MantyxError(\n `outputSchema.name must match /^[a-zA-Z0-9_-]{1,64}$/, got ${JSON.stringify(value.name)}`,\n );\n }\n out.name = value.name;\n }\n const schema = value.schema;\n if (!schema || typeof schema !== \"object\" || Array.isArray(schema)) {\n throw new MantyxError(\n `outputSchema.schema must be a non-null JSON object (the JSON Schema root)`,\n );\n }\n out.schema = schema;\n let serialized: string;\n try {\n serialized = JSON.stringify(out);\n } catch (err) {\n throw new MantyxError(\n `outputSchema is not JSON-serialisable: ${(err as Error).message ?? String(err)}`,\n );\n }\n if (serialized.length > OUTPUT_SCHEMA_MAX_BYTES) {\n throw new MantyxError(\n `outputSchema serialised JSON is ${serialized.length} bytes; the server enforces a 32 KB limit`,\n );\n }\n return out;\n}\n\nconst LOOP_DETECTION_THRESHOLD_MAX = 100;\n\n/**\n * Validate a {@link LoopDetection} (or `false`) value and return the\n * wire-shaped value. Mirrors the server-side `400 invalid_request` checks\n * (thresholds in range, hard cutoff strictly greater than consecutive) so\n * callers see an early local error.\n */\nfunction normalizeLoopDetection(\n value: LoopDetection | false,\n): false | Record<string, unknown> {\n if (value === false) return false;\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new MantyxError(\n `loopDetection must be an object or the literal \\`false\\`, got ${JSON.stringify(value)}`,\n );\n }\n const out: Record<string, unknown> = {};\n if (value.consecutiveThreshold !== undefined) {\n out.consecutiveThreshold = assertThreshold(\n \"loopDetection.consecutiveThreshold\",\n value.consecutiveThreshold,\n 2,\n );\n }\n if (value.hardCutoffThreshold !== undefined) {\n out.hardCutoffThreshold = assertThreshold(\n \"loopDetection.hardCutoffThreshold\",\n value.hardCutoffThreshold,\n 3,\n );\n }\n if (\n typeof out.consecutiveThreshold === \"number\" &&\n typeof out.hardCutoffThreshold === \"number\" &&\n out.hardCutoffThreshold <= out.consecutiveThreshold\n ) {\n throw new MantyxError(\n `loopDetection.hardCutoffThreshold (${out.hardCutoffThreshold}) must be strictly greater than loopDetection.consecutiveThreshold (${out.consecutiveThreshold})`,\n );\n }\n return out;\n}\n\nfunction assertThreshold(label: string, value: number, min: number): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || !Number.isInteger(value)) {\n throw new MantyxError(`${label} must be an integer, got ${JSON.stringify(value)}`);\n }\n if (value < min) {\n throw new MantyxError(`${label} must be >= ${min}, got ${value}`);\n }\n if (value > LOOP_DETECTION_THRESHOLD_MAX) {\n throw new MantyxError(\n `${label} must be <= ${LOOP_DETECTION_THRESHOLD_MAX} (server-enforced), got ${value}`,\n );\n }\n return value;\n}\n\nconst TOOL_BUDGETS_MAX_ENTRIES = 32;\nconst TOOL_BUDGET_MAX_NAME_LEN = 120;\nconst TOOL_BUDGET_MAX_CALLS = 1000;\n\n/**\n * Validate a {@link ToolBudgets} value and return the wire-shaped object.\n * Mirrors the server-side `400 invalid_request` checks (max 32 entries,\n * key length 1..120, `maxCalls` ≥ 0 and ≤ 1000) so callers see an early\n * local error. An empty object is valid and signals \"clear the runtime\n * defaults\"; pass `undefined` to keep them.\n */\nfunction normalizeToolBudgets(value: ToolBudgets): Record<string, { maxCalls: number }> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new MantyxError(\n `toolBudgets must be an object of shape { [name]: { maxCalls } }, got ${JSON.stringify(value)}`,\n );\n }\n const keys = Object.keys(value);\n if (keys.length > TOOL_BUDGETS_MAX_ENTRIES) {\n throw new MantyxError(\n `toolBudgets has ${keys.length} entries; the server enforces a ${TOOL_BUDGETS_MAX_ENTRIES}-entry limit`,\n );\n }\n const out: Record<string, { maxCalls: number }> = {};\n for (const key of keys) {\n if (typeof key !== \"string\" || key.length < 1 || key.length > TOOL_BUDGET_MAX_NAME_LEN) {\n throw new MantyxError(\n `toolBudgets keys must be 1..${TOOL_BUDGET_MAX_NAME_LEN}-char strings, got ${JSON.stringify(key)}`,\n );\n }\n const entry = value[key];\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new MantyxError(\n `toolBudgets[${JSON.stringify(key)}] must be an object { maxCalls }, got ${JSON.stringify(entry)}`,\n );\n }\n const maxCalls = entry.maxCalls;\n if (\n typeof maxCalls !== \"number\" ||\n !Number.isFinite(maxCalls) ||\n !Number.isInteger(maxCalls) ||\n maxCalls < 0\n ) {\n throw new MantyxError(\n `toolBudgets[${JSON.stringify(key)}].maxCalls must be a non-negative integer, got ${JSON.stringify(maxCalls)}`,\n );\n }\n if (maxCalls > TOOL_BUDGET_MAX_CALLS) {\n throw new MantyxError(\n `toolBudgets[${JSON.stringify(key)}].maxCalls must be <= ${TOOL_BUDGET_MAX_CALLS} (server-enforced), got ${maxCalls}`,\n );\n }\n out[key] = { maxCalls };\n }\n return out;\n}\n\n/**\n * Parse the terminal text of a `RunResult` as JSON.\n *\n * When the run was submitted with `outputSchema`, MANTYX (via the LLM\n * provider) guarantees the reply parses as JSON in the *vast* majority of\n * cases. Transient model errors (refusal text, truncation under\n * `max_tokens` pressure, exotic Unicode) can still produce strings that\n * fail to `JSON.parse` in rare edge cases — this helper centralises that\n * brittle step and surfaces a typed {@link MantyxParseError} on failure\n * with the original text preserved on `err.text`.\n *\n * Pass an optional `validator` (zod's `.parse`, an Ajv compiled validator,\n * or any function) to re-validate against your source-of-truth schema. The\n * validator's return value (or thrown error) is forwarded to the caller.\n *\n * @example\n * ```ts\n * import { z } from \"zod\";\n * import { parseRunOutput } from \"@mantyx/sdk\";\n *\n * const Schema = z.object({ city: z.string(), temperature_c: z.number() });\n * const result = await client.runAgent({\n * systemPrompt: \"...\",\n * prompt: \"What's the weather in SF?\",\n * outputSchema: { name: \"weather_report\", schema: weatherJsonSchema },\n * });\n * const report = parseRunOutput(result, Schema.parse.bind(Schema));\n * // ^? { city: string; temperature_c: number }\n * ```\n */\nexport function parseRunOutput<T = unknown>(\n result: RunResult,\n validator?: (value: unknown) => T,\n): T {\n let parsed: unknown;\n try {\n parsed = JSON.parse(result.text);\n } catch (err) {\n throw new MantyxParseError(\n `Run ${result.runId} returned non-JSON text; cannot satisfy outputSchema`,\n result.text,\n { cause: err },\n );\n }\n if (validator) {\n try {\n return validator(parsed);\n } catch (err) {\n throw new MantyxParseError(\n `Run ${result.runId} output failed validation: ${(err as Error).message ?? String(err)}`,\n result.text,\n { cause: err },\n );\n }\n }\n return parsed as T;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n/**\n * Defensively coerce a wire `tokens` object into {@link RunTokenUsage}.\n *\n * Returns `undefined` when the input is not a JSON object — that keeps\n * the \"no usage data\" sentinel intact against legacy MANTYX servers\n * that omit the field entirely. Unknown / missing buckets default to\n * `0` (the protocol contract is that misbehaving engines clamp to\n * non-negative integers; the SDK mirrors that here so dashboards never\n * see `NaN`).\n */\nfunction parseRunTokens(value: unknown): RunTokenUsage | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const v = value as Record<string, unknown>;\n return {\n inputTokens: toNonNegativeInt(v.inputTokens),\n cachedTokens: toNonNegativeInt(v.cachedTokens),\n reasoningTokens: toNonNegativeInt(v.reasoningTokens),\n outputTokens: toNonNegativeInt(v.outputTokens),\n };\n}\n\n/**\n * Defensively coerce a wire `turns` value into an integer. Returns\n * `undefined` when missing / unparseable — keeps the \"no usage data\"\n * sentinel against legacy servers.\n */\nfunction parseRunTurns(value: unknown): number | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return undefined;\n return Math.max(0, Math.trunc(value));\n}\n\n/**\n * Defensively coerce a wire `model` object into {@link RunModelInfo}.\n *\n * Returns `undefined` when the input is not a JSON object — the\n * \"no usage data\" sentinel for legacy servers. `reasoningEffort` is\n * carried through only when the wire surfaced it (the field is\n * optional on the protocol side).\n */\nfunction parseRunModel(value: unknown): RunModelInfo | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const v = value as Record<string, unknown>;\n const out: RunModelInfo = {\n id: typeof v.id === \"string\" ? v.id : \"\",\n provider: typeof v.provider === \"string\" ? v.provider : \"\",\n vendorModelId: typeof v.vendorModelId === \"string\" ? v.vendorModelId : \"\",\n };\n if (typeof v.reasoningEffort === \"string\" && v.reasoningEffort.length > 0) {\n out.reasoningEffort = v.reasoningEffort;\n }\n return out;\n}\n\nfunction toNonNegativeInt(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return 0;\n return Math.max(0, Math.trunc(value));\n}\n\n/**\n * Pick exactly one of `apiKey` / `accessToken` / `tokenSource` from\n * {@link MantyxClientOptions} and return the resolved bearer credential\n * (plus the optional dynamic source).\n *\n * `apiKey` and `accessToken` are both static workspace bearers — the\n * server resolves whichever credential it sees by token-prefix, so the\n * SDK can use a single header path. `tokenSource` is the dynamic\n * alternative that the HTTP layer calls before every request and on\n * 401 retries; it is mutually exclusive with the static options\n * because mixing them would obscure where the credential actually\n * came from.\n */\nfunction resolveCredential(opts: MantyxClientOptions): {\n credential: string;\n tokenSource: TokenSource | null;\n} {\n const apiKey = typeof opts.apiKey === \"string\" ? opts.apiKey : \"\";\n const accessToken = typeof opts.accessToken === \"string\" ? opts.accessToken : \"\";\n const tokenSource = typeof opts.tokenSource === \"function\" ? opts.tokenSource : null;\n const provided = [apiKey ? \"apiKey\" : \"\", accessToken ? \"accessToken\" : \"\", tokenSource ? \"tokenSource\" : \"\"]\n .filter((s) => s.length > 0);\n if (provided.length > 1) {\n throw new MantyxError(\n `Pass exactly one of \\`apiKey\\`, \\`accessToken\\`, or \\`tokenSource\\` — got ${provided.join(\" + \")}.`,\n );\n }\n if (provided.length === 0) {\n throw new MantyxError(\n \"One of `apiKey` (workspace API key), `accessToken` (OAuth access token), or `tokenSource` (dynamic credential provider) is required\",\n );\n }\n return {\n credential: apiKey || accessToken,\n tokenSource,\n };\n}\n\n/**\n * Extract the list of scopes the server reported as required for the\n * route, from either the response body's `required` field or the\n * `WWW-Authenticate: Bearer error=\"insufficient_scope\", scope=\"…\"` header.\n *\n * The body field can be a single string (most routes) or an array\n * (multi-scope routes). The header carries a space-delimited scope\n * string per RFC 6750. We prefer the body since it's stricter, and\n * fall back to the header so we surface *something* even when the\n * route only returned the header.\n */\nfunction parseRequiredScopes(\n bodyRequired: string | string[] | undefined,\n wwwAuthenticate: string | null,\n): string[] {\n if (Array.isArray(bodyRequired)) {\n return bodyRequired.filter((s): s is string => typeof s === \"string\" && s.length > 0);\n }\n if (typeof bodyRequired === \"string\" && bodyRequired.length > 0) {\n return [bodyRequired];\n }\n if (typeof wwwAuthenticate === \"string\") {\n const m = /scope=\"([^\"]+)\"/i.exec(wwwAuthenticate);\n if (m && m[1]) {\n return m[1].split(/\\s+/).filter((s) => s.length > 0);\n }\n }\n return [];\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/a2a-server.ts","../src/errors.ts","../src/zod-to-json-schema.ts","../src/client.ts"],"sourcesContent":["/**\n * Expose a MANTYX agent over the [Agent2Agent (A2A)](https://google-a2a.github.io/A2A/)\n * protocol so other agents can talk to it as a peer.\n *\n * This module is loaded from a separate sub-export (`@mantyx/sdk/a2a-server`) so\n * apps that don't need it never pay the bundle cost of the official A2A SDK or\n * Express. To use it, install the optional peer deps:\n *\n * npm install @a2a-js/sdk express\n *\n * @example\n * import { MantyxClient } from \"@mantyx/sdk\";\n * import { serveAgentOverA2A } from \"@mantyx/sdk/a2a-server\";\n *\n * const client = new MantyxClient({\n * apiKey: process.env.MANTYX_API_KEY!,\n * workspaceSlug: process.env.MANTYX_WORKSPACE_SLUG!,\n * });\n *\n * const server = await serveAgentOverA2A({\n * client,\n * port: 4000,\n * agent: { agentId: \"agent_cm6abc123\" },\n * agentCard: {\n * name: \"Acme Support\",\n * description: \"Answers billing and account questions.\",\n * protocolVersion: \"0.3.0\",\n * version: \"1.0.0\",\n * url: \"http://localhost:4000\",\n * skills: [{ id: \"support\", name: \"Support\", description: \"Customer support\",\n * tags: [\"support\"] }],\n * capabilities: { streaming: true, pushNotifications: false },\n * defaultInputModes: [\"text\"],\n * defaultOutputModes: [\"text\"],\n * },\n * });\n *\n * console.log(`Listening on ${server.url}`);\n */\n\nimport type {\n AgentCard,\n Message,\n MessageSendParams,\n Part,\n Task,\n TaskArtifactUpdateEvent,\n TaskStatusUpdateEvent,\n} from \"@a2a-js/sdk\";\nimport type {\n AgentExecutor,\n ExecutionEventBus,\n RequestContext,\n} from \"@a2a-js/sdk/server\";\n\nimport {\n AgentSession,\n type MantyxClient,\n type RunResult,\n type SessionSpec,\n type RunSpec,\n} from \"./client.js\";\nimport { MantyxError, MantyxRunError } from \"./errors.js\";\nimport type { ReasoningLevel, ToolRef } from \"./tools.js\";\n\n// --------------------------------------------------------------- Public API\n\n/**\n * Description of the MANTYX agent that should answer A2A requests.\n *\n * Mirrors the existing `runAgent` / `createSession` argument shape:\n * - `agentId` triggers a persisted workspace agent.\n * - `systemPrompt` (with optional `modelId`, `tools`, …) defines an ephemeral\n * agent inline.\n *\n * Either `agentId` or `systemPrompt` is required.\n */\nexport interface MantyxAgentSpec {\n /** Reference to a persisted MANTYX agent. Mutually exclusive with `systemPrompt`. */\n agentId?: string;\n /** System prompt for an inline / ephemeral agent. Mutually exclusive with `agentId`. */\n systemPrompt?: string;\n modelId?: string;\n tools?: ToolRef[];\n reasoningLevel?: ReasoningLevel;\n metadata?: Record<string, string>;\n budgets?: { maxToolTurns?: number };\n /**\n * Optional human-readable display name for runs created against MANTYX.\n * Visible in the dashboard. Has no effect on the A2A side.\n */\n name?: string;\n}\n\nexport interface MantyxAgentExecutorOptions {\n client: MantyxClient;\n agent: MantyxAgentSpec;\n /**\n * How to map an incoming A2A `contextId` onto a MANTYX session.\n *\n * - `\"auto\"` (default): each unique `contextId` opens a MANTYX session on\n * first contact and reuses it for subsequent messages with the same\n * `contextId`. Gives you multi-turn out of the box.\n * - `\"stateless\"`: every A2A message becomes an independent `runAgent`. No\n * conversational memory; simpler resource model.\n */\n conversation?: \"auto\" | \"stateless\";\n /**\n * LRU cap on the in-memory `contextId -> AgentSession` table. When the cap\n * is exceeded the oldest session is `end()`-ed and evicted. Default: 1024.\n * Only consulted when `conversation: \"auto\"`.\n */\n maxSessions?: number;\n /**\n * Receives streaming MANTYX `assistant_delta` text. The default behaviour\n * forwards every delta as a `TaskStatusUpdateEvent` (state: \"working\")\n * containing the delta as a `text` part — this is what enables A2A\n * `message/stream` clients to see real-time tokens. Override only if you\n * need to swallow them or transform the wire shape.\n */\n onAssistantDelta?: (delta: string, ctx: RequestContext, eventBus: ExecutionEventBus) => void;\n}\n\nexport interface ServeAgentOverA2AOptions extends MantyxAgentExecutorOptions {\n /** A2A Agent Card published at `/.well-known/agent-card.json`. */\n agentCard: AgentCard;\n /** TCP port to listen on. Default: 0 (let the OS pick). */\n port?: number;\n /** Bind address. Default: `\"0.0.0.0\"`. */\n host?: string;\n /** Path that serves the Agent Card JSON. Default: `\"/.well-known/agent-card.json\"`. */\n agentCardPath?: string;\n /** Path that serves the JSON-RPC endpoint. Default: `\"/\"`. */\n jsonRpcPath?: string;\n /**\n * Path that serves the HTTP+JSON/REST endpoint. Default: `\"/v1\"`.\n * Set to `false` to disable the REST mount entirely.\n */\n restPath?: string | false;\n}\n\nexport interface ServeAgentOverA2AHandle {\n /** Origin of the running server, e.g. `\"http://localhost:4000\"`. */\n url: string;\n /** Resolved port number (useful when you let the OS pick one). */\n port: number;\n /** Stop the HTTP server, end every cached MANTYX session, and free MCP transports. */\n close: () => Promise<void>;\n}\n\n// --------------------------------------------------------- Implementation\n\n/**\n * Implementation of `@a2a-js/sdk`'s `AgentExecutor` that backs a MANTYX agent.\n *\n * Most callers want `serveAgentOverA2A` instead; reach for this class directly\n * when you need to mount the executor inside an existing Express, Fastify, or\n * Connect app.\n */\nexport class MantyxAgentExecutor implements AgentExecutor {\n readonly client: MantyxClient;\n readonly agent: MantyxAgentSpec;\n readonly conversation: \"auto\" | \"stateless\";\n readonly maxSessions: number;\n readonly onAssistantDelta?: MantyxAgentExecutorOptions[\"onAssistantDelta\"];\n\n /** contextId -> live MANTYX session. Maintained as an LRU map. */\n private readonly sessions = new Map<string, AgentSession>();\n /** taskIds we've been asked to cancel; checked between turns. */\n private readonly cancelled = new Set<string>();\n /** Pending AbortControllers per task, used for cooperative cancel. */\n private readonly inFlight = new Map<string, AbortController>();\n\n constructor(options: MantyxAgentExecutorOptions) {\n if (!options.client) {\n throw new MantyxError(\"MantyxAgentExecutor: `client` is required\");\n }\n validateAgentSpec(options.agent);\n this.client = options.client;\n this.agent = options.agent;\n this.conversation = options.conversation ?? \"auto\";\n this.maxSessions = options.maxSessions ?? 1024;\n if (options.onAssistantDelta) this.onAssistantDelta = options.onAssistantDelta;\n }\n\n async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {\n const { userMessage, taskId, contextId, task } = requestContext;\n const userText = extractText(userMessage);\n\n const abort = new AbortController();\n this.inFlight.set(taskId, abort);\n\n try {\n // Publish initial Task object on the first turn so streaming clients see\n // a stable id; reusing an existing task otherwise.\n if (!task) {\n eventBus.publish({\n kind: \"task\",\n id: taskId,\n contextId,\n status: { state: \"submitted\", timestamp: new Date().toISOString() },\n history: [userMessage],\n } satisfies Task);\n }\n\n eventBus.publish(statusUpdate(taskId, contextId, \"working\", false));\n\n if (this.cancelled.has(taskId)) {\n eventBus.publish(statusUpdate(taskId, contextId, \"canceled\", true));\n eventBus.finished();\n return;\n }\n\n const onDelta = (delta: string) => {\n if (this.onAssistantDelta) {\n this.onAssistantDelta(delta, requestContext, eventBus);\n return;\n }\n eventBus.publish(deltaStatusUpdate(taskId, contextId, delta));\n };\n\n let result: RunResult;\n try {\n result = await this.runOnce(contextId, userText, onDelta, abort.signal);\n } catch (err) {\n eventBus.publish(\n completedStatusUpdate(\n taskId,\n contextId,\n this.cancelled.has(taskId) ? \"canceled\" : \"failed\",\n errorText(err),\n ),\n );\n eventBus.finished();\n return;\n }\n\n eventBus.publish(completedStatusUpdate(taskId, contextId, \"completed\", result.text ?? \"\"));\n eventBus.finished();\n } finally {\n this.inFlight.delete(taskId);\n this.cancelled.delete(taskId);\n }\n }\n\n async cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void> {\n this.cancelled.add(taskId);\n const ctrl = this.inFlight.get(taskId);\n if (ctrl) ctrl.abort();\n // The active `execute()` call publishes the final 'canceled' status\n // itself; we only need to mark the intent here.\n void eventBus;\n }\n\n /**\n * Close every cached session. Idempotent. Safe to call from server shutdown\n * paths.\n */\n async close(): Promise<void> {\n const sessions = Array.from(this.sessions.values());\n this.sessions.clear();\n await Promise.allSettled(sessions.map((s) => s.end()));\n }\n\n // -------------------------------------------------- private session helpers\n\n private async runOnce(\n contextId: string,\n prompt: string,\n onAssistantDelta: (delta: string) => void,\n signal: AbortSignal,\n ): Promise<RunResult> {\n if (this.conversation === \"stateless\") {\n const runSpec: RunSpec = {\n ...specForRun(this.agent),\n prompt,\n onAssistantDelta,\n signal,\n };\n return this.client.runAgent(runSpec);\n }\n\n const session = await this.getOrCreateSession(contextId);\n return session.send(prompt, { onAssistantDelta, signal });\n }\n\n private async getOrCreateSession(contextId: string): Promise<AgentSession> {\n const existing = this.sessions.get(contextId);\n if (existing) {\n // LRU: bump to most-recently-used.\n this.sessions.delete(contextId);\n this.sessions.set(contextId, existing);\n return existing;\n }\n const sessionSpec: SessionSpec = specForSession(this.agent, contextId);\n const session = await this.client.createSession(sessionSpec);\n this.sessions.set(contextId, session);\n await this.evictIfNeeded();\n return session;\n }\n\n private async evictIfNeeded(): Promise<void> {\n while (this.sessions.size > this.maxSessions) {\n const oldestKey = this.sessions.keys().next().value as string | undefined;\n if (!oldestKey) break;\n const oldest = this.sessions.get(oldestKey)!;\n this.sessions.delete(oldestKey);\n try {\n await oldest.end();\n } catch {\n // Eviction is best-effort; swallow errors so the next request still works.\n }\n }\n }\n}\n\n/**\n * Spin up a small HTTP server that exposes a MANTYX agent as an A2A peer.\n * Mounts the Agent Card, JSON-RPC, and (optionally) REST endpoints from the\n * official `@a2a-js/sdk` library.\n *\n * Throws if `express` / `@a2a-js/sdk` aren't installed; install them as peer\n * deps with `npm install express @a2a-js/sdk`.\n */\nexport async function serveAgentOverA2A(\n options: ServeAgentOverA2AOptions,\n): Promise<ServeAgentOverA2AHandle> {\n const a2a = await loadServerSdk();\n const expressMod = await loadExpress();\n\n const executor = new MantyxAgentExecutor(options);\n const requestHandler = new a2a.DefaultRequestHandler(\n options.agentCard,\n new a2a.InMemoryTaskStore(),\n executor,\n );\n\n const app = expressMod();\n app.use(expressMod.json());\n\n const cardPath = options.agentCardPath ?? \"/.well-known/agent-card.json\";\n const jsonRpcPath = options.jsonRpcPath ?? \"/\";\n const restPath = options.restPath === undefined ? \"/v1\" : options.restPath;\n\n app.use(\n cardPath,\n a2a.expressApp.agentCardHandler({ agentCardProvider: requestHandler }),\n );\n if (restPath !== false) {\n app.use(\n restPath,\n a2a.expressApp.restHandler({\n requestHandler,\n userBuilder: a2a.expressApp.UserBuilder.noAuthentication,\n }),\n );\n }\n // Mount JSON-RPC last so it doesn't shadow the well-known and REST paths.\n app.use(\n jsonRpcPath,\n a2a.expressApp.jsonRpcHandler({\n requestHandler,\n userBuilder: a2a.expressApp.UserBuilder.noAuthentication,\n }),\n );\n\n const port = options.port ?? 0;\n const host = options.host ?? \"0.0.0.0\";\n const server = app.listen(port, host);\n\n await new Promise<void>((resolve, reject) => {\n server.once(\"listening\", resolve);\n server.once(\"error\", reject);\n });\n\n const address = server.address();\n if (!address || typeof address === \"string\") {\n server.close();\n throw new MantyxError(\"serveAgentOverA2A: failed to bind HTTP listener\");\n }\n\n return {\n port: address.port,\n url: `http://${displayHost(host)}:${address.port}`,\n close: async () => {\n await new Promise<void>((resolve, reject) =>\n server.close((err) => (err ? reject(err) : resolve())),\n );\n await executor.close();\n },\n };\n}\n\n// ----------------------------------------------------------- A2A event helpers\n\nfunction statusUpdate(\n taskId: string,\n contextId: string,\n state: \"submitted\" | \"working\" | \"completed\" | \"canceled\" | \"failed\",\n final: boolean,\n): TaskStatusUpdateEvent {\n return {\n kind: \"status-update\",\n taskId,\n contextId,\n status: { state, timestamp: new Date().toISOString() },\n final,\n };\n}\n\nfunction deltaStatusUpdate(\n taskId: string,\n contextId: string,\n delta: string,\n): TaskStatusUpdateEvent {\n return {\n kind: \"status-update\",\n taskId,\n contextId,\n status: {\n state: \"working\",\n timestamp: new Date().toISOString(),\n message: {\n kind: \"message\",\n messageId: randomMessageId(),\n role: \"agent\",\n parts: [{ kind: \"text\", text: delta }],\n contextId,\n taskId,\n },\n },\n final: false,\n };\n}\n\nfunction completedStatusUpdate(\n taskId: string,\n contextId: string,\n state: \"completed\" | \"canceled\" | \"failed\",\n text: string,\n): TaskStatusUpdateEvent {\n return {\n kind: \"status-update\",\n taskId,\n contextId,\n status: {\n state,\n timestamp: new Date().toISOString(),\n message: {\n kind: \"message\",\n messageId: randomMessageId(),\n role: \"agent\",\n parts: [{ kind: \"text\", text }],\n contextId,\n taskId,\n },\n },\n final: true,\n };\n}\n\n// --------------------------------------------------------- Utility helpers\n\nfunction extractText(message: Message | undefined): string {\n if (!message) return \"\";\n const parts = (message.parts as Part[] | undefined) ?? [];\n const out: string[] = [];\n for (const p of parts) {\n if ((p as { kind: string }).kind === \"text\") {\n const t = (p as { text?: unknown }).text;\n if (typeof t === \"string\") out.push(t);\n }\n }\n return out.join(\"\\n\");\n}\n\nfunction specForRun(spec: MantyxAgentSpec): RunSpec {\n const out: RunSpec = {};\n if (spec.agentId) out.agentId = spec.agentId;\n if (spec.systemPrompt) out.systemPrompt = spec.systemPrompt;\n if (spec.modelId) out.modelId = spec.modelId;\n if (spec.tools) out.tools = spec.tools;\n if (spec.reasoningLevel !== undefined) out.reasoningLevel = spec.reasoningLevel;\n if (spec.metadata) out.metadata = spec.metadata;\n if (spec.budgets) out.budgets = spec.budgets;\n if (spec.name) out.name = spec.name;\n return out;\n}\n\nfunction specForSession(spec: MantyxAgentSpec, contextId: string): SessionSpec {\n const out: SessionSpec = {};\n if (spec.agentId) out.agentId = spec.agentId;\n if (spec.systemPrompt) out.systemPrompt = spec.systemPrompt;\n if (spec.modelId) out.modelId = spec.modelId;\n if (spec.tools) out.tools = spec.tools;\n if (spec.reasoningLevel !== undefined) out.reasoningLevel = spec.reasoningLevel;\n // Tag the session with the originating A2A contextId so it's filterable\n // in the MANTYX dashboard.\n const meta: Record<string, string> = { ...(spec.metadata ?? {}) };\n if (!meta.a2a_context_id) meta.a2a_context_id = contextId;\n out.metadata = meta;\n if (spec.budgets) out.budgets = spec.budgets;\n if (spec.name) out.name = spec.name;\n return out;\n}\n\nfunction validateAgentSpec(spec: MantyxAgentSpec): void {\n if (!spec.agentId && (!spec.systemPrompt || spec.systemPrompt.length === 0)) {\n throw new MantyxError(\n \"MantyxAgentExecutor: `agent.agentId` or `agent.systemPrompt` is required\",\n );\n }\n}\n\nfunction errorText(err: unknown): string {\n if (err instanceof MantyxRunError) {\n return `MANTYX run failed (${err.subtype ?? \"unknown\"}): ${err.message}`;\n }\n if (err instanceof Error) return err.message;\n try {\n return String(err);\n } catch {\n return \"unknown error\";\n }\n}\n\nfunction randomMessageId(): string {\n if (typeof globalThis.crypto?.randomUUID === \"function\") {\n return globalThis.crypto.randomUUID();\n }\n // Fallback: timestamp + random suffix; A2A only requires uniqueness.\n return `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction displayHost(host: string): string {\n if (host === \"0.0.0.0\" || host === \"::\") return \"localhost\";\n return host;\n}\n\n// --------------------------------------------------- Optional-dep loaders\n\ninterface ExpressLoader {\n (): import(\"express\").Express;\n json(): import(\"express\").RequestHandler;\n}\n\ninterface A2AServerSdk {\n DefaultRequestHandler: typeof import(\"@a2a-js/sdk/server\").DefaultRequestHandler;\n InMemoryTaskStore: typeof import(\"@a2a-js/sdk/server\").InMemoryTaskStore;\n expressApp: typeof import(\"@a2a-js/sdk/server/express\");\n}\n\nasync function loadExpress(): Promise<ExpressLoader> {\n try {\n const mod = (await import(\"express\")) as unknown as\n | ExpressLoader\n | { default: ExpressLoader };\n return \"default\" in mod ? mod.default : mod;\n } catch (err) {\n throw new MantyxError(\n \"serveAgentOverA2A: `express` is required but not installed. Run `npm install express @a2a-js/sdk` to enable the A2A server.\",\n );\n }\n}\n\nasync function loadServerSdk(): Promise<A2AServerSdk> {\n let server: typeof import(\"@a2a-js/sdk/server\");\n let express: typeof import(\"@a2a-js/sdk/server/express\");\n try {\n server = (await import(\"@a2a-js/sdk/server\")) as typeof import(\"@a2a-js/sdk/server\");\n } catch (err) {\n throw new MantyxError(\n \"serveAgentOverA2A: `@a2a-js/sdk` is required but not installed. Run `npm install @a2a-js/sdk express` to enable the A2A server.\",\n );\n }\n try {\n express = (await import(\n \"@a2a-js/sdk/server/express\"\n )) as typeof import(\"@a2a-js/sdk/server/express\");\n } catch (err) {\n throw new MantyxError(\n \"serveAgentOverA2A: `@a2a-js/sdk/server/express` could not be loaded; ensure the installed `@a2a-js/sdk` is at least v0.3.\",\n );\n }\n return {\n DefaultRequestHandler: server.DefaultRequestHandler,\n InMemoryTaskStore: server.InMemoryTaskStore,\n expressApp: express,\n };\n}\n\n// Re-export for callers that just want to compose the executor with their own\n// transport stack (e.g. plug it into Fastify or Cloudflare Workers).\nexport type {\n AgentCard,\n Message,\n MessageSendParams,\n Task,\n TaskArtifactUpdateEvent,\n TaskStatusUpdateEvent,\n} from \"@a2a-js/sdk\";\nexport type { AgentExecutor, ExecutionEventBus, RequestContext } from \"@a2a-js/sdk/server\";\n","/**\n * Error types raised by the MANTYX SDK.\n */\n\nexport class MantyxError extends Error {\n readonly code: string;\n readonly status: number | undefined;\n readonly hint: string | undefined;\n\n constructor(\n message: string,\n opts: { code?: string; status?: number; hint?: string } = {},\n ) {\n super(message);\n this.name = \"MantyxError\";\n this.code = opts.code ?? \"mantyx_error\";\n this.status = opts.status;\n this.hint = opts.hint;\n }\n}\n\nexport class MantyxNetworkError extends MantyxError {\n constructor(message: string, opts: { cause?: unknown } = {}) {\n super(message, { code: \"network\" });\n this.name = \"MantyxNetworkError\";\n if (opts.cause !== undefined) {\n (this as Error & { cause?: unknown }).cause = opts.cause;\n }\n }\n}\n\nexport class MantyxAuthError extends MantyxError {\n constructor(message = \"Invalid or missing API key / OAuth access token\") {\n super(message, { code: \"unauthorized\", status: 401 });\n this.name = \"MantyxAuthError\";\n }\n}\n\n/**\n * Raised on `403 insufficient_scope`, returned when an OAuth access token\n * is missing one of the scopes a route demands (see\n * `docs/agent-runs-protocol.md` §2.2 for the per-endpoint table).\n *\n * `requiredScopes` carries the verbatim `required` value from the\n * server's response — a single scope for most routes, an array when the\n * route demands more than one. The SDK is expected to surface this so\n * callers can drive a re-consent flow (e.g. \"please re-authorise the\n * app with `sessions:write` enabled\").\n *\n * Workspace API keys never trip this error — they carry no granular\n * scopes. It is OAuth-only.\n */\nexport class MantyxScopeError extends MantyxError {\n /**\n * Scope(s) the route demanded. Always at least one entry; usually\n * exactly one. New routes may demand more scopes in the future.\n */\n readonly requiredScopes: readonly string[];\n\n constructor(message: string, requiredScopes: readonly string[]) {\n super(message, { code: \"insufficient_scope\", status: 403 });\n this.name = \"MantyxScopeError\";\n this.requiredScopes = [...requiredScopes];\n }\n}\n\nexport class MantyxToolError extends MantyxError {\n readonly toolName: string;\n\n constructor(toolName: string, message: string) {\n super(`Local tool ${JSON.stringify(toolName)} failed: ${message}`, {\n code: \"local_tool_failed\",\n });\n this.name = \"MantyxToolError\";\n this.toolName = toolName;\n }\n}\n\n/**\n * Per-run token totals attached to terminal `result` / `error`\n * events. See `docs/agent-runs-protocol.md` §7.1 for the per-provider\n * mapping and the relationship between buckets. Re-exported from\n * `client.ts` so error consumers can pattern-match the triple without\n * a second import.\n */\nexport interface MantyxRunErrorTokens {\n inputTokens: number;\n cachedTokens: number;\n reasoningTokens: number;\n outputTokens: number;\n}\n\n/**\n * Resolved model that executed the run. Surfaced on terminal events\n * by MANTYX ≥ 2026-09. See `docs/agent-runs-protocol.md` §7.1. The\n * `provider` empty / undefined is the \"no usage data\" sentinel.\n */\nexport interface MantyxRunErrorModel {\n id: string;\n provider: string;\n vendorModelId: string;\n reasoningEffort?: string;\n}\n\n/**\n * Optional triage attributes the runner attaches to terminal `error`\n * events. Mirrors the wire fields described in\n * `docs/agent-runs-protocol.md` §7 (\"error event payload fields\") so SDK\n * callers can render structured UI status notes (\"model truncated — JSON\n * likely incomplete\") and drive retry policy without re-parsing the\n * human-readable `message`.\n */\nexport interface MantyxRunErrorInit {\n /**\n * Canonical category of failure. One of `\"rate_limit\"`, `\"overloaded\"`,\n * `\"server\"`, `\"context_window\"`, `\"truncation\"`, `\"invalid_request\"`,\n * `\"auth\"`, `\"timeout\"`, `\"local_timeout\"`, `\"upstream_deadline\"`,\n * `\"unknown\"`. New categories may land additively — callers should\n * default-branch to `\"unknown\"` for unrecognized values.\n */\n errorClass?: string;\n /**\n * Canonical lowercase stop reason normalized across providers\n * (`\"max_tokens\"`, `\"refusal\"`, `\"malformed_function_call\"`, …). When\n * present, mirrors the value carried on the last `assistant_message`\n * event preceding the failure.\n */\n finishReason?: string | null;\n /**\n * **Best-effort raw bytes** the model emitted before the failure. For\n * `outputSchema` runs this is likely **incomplete JSON** that will\n * fail `JSON.parse` — treat it as diagnostic data, never as a\n * schema-conformant reply.\n */\n partialText?: string;\n /**\n * Coarse retry hint inherited from the pipeline's error classifier.\n * Informational; the SDK still owns the actual retry decision.\n */\n retryable?: boolean;\n /**\n * Per-run token totals from the terminal event. Present against\n * MANTYX ≥ 2026-09 — see {@link MantyxRunErrorTokens} and\n * `docs/agent-runs-protocol.md` §7.1. Includes the failing model\n * call's usage when the run errored mid-loop.\n */\n tokens?: MantyxRunErrorTokens;\n /** Total model invocations for the run, including the failing call. */\n turns?: number;\n /** Resolved model that executed the run. See {@link MantyxRunErrorModel}. */\n model?: MantyxRunErrorModel;\n}\n\nexport class MantyxRunError extends MantyxError {\n readonly runId: string;\n readonly subtype: string;\n /** See {@link MantyxRunErrorInit.errorClass}. */\n readonly errorClass: string | undefined;\n /** See {@link MantyxRunErrorInit.finishReason}. */\n readonly finishReason: string | null | undefined;\n /** See {@link MantyxRunErrorInit.partialText}. */\n readonly partialText: string | undefined;\n /** See {@link MantyxRunErrorInit.retryable}. */\n readonly retryable: boolean | undefined;\n /** See {@link MantyxRunErrorInit.tokens}. */\n readonly tokens: MantyxRunErrorTokens | undefined;\n /** See {@link MantyxRunErrorInit.turns}. */\n readonly turns: number | undefined;\n /** See {@link MantyxRunErrorInit.model}. */\n readonly model: MantyxRunErrorModel | undefined;\n\n constructor(\n runId: string,\n subtype: string,\n message: string,\n init: MantyxRunErrorInit = {},\n ) {\n super(message, { code: subtype });\n this.name = \"MantyxRunError\";\n this.runId = runId;\n this.subtype = subtype;\n this.errorClass = init.errorClass;\n this.finishReason = init.finishReason;\n this.partialText = init.partialText;\n this.retryable = init.retryable;\n this.tokens = init.tokens;\n this.turns = init.turns;\n this.model = init.model;\n }\n}\n\n/**\n * Thrown by {@link parseRunOutput} when the run's terminal text was supposed\n * to be a JSON document (because `outputSchema` was set on the spec) but\n * either failed to JSON.parse or failed the user-supplied validator.\n *\n * The original `text` is preserved on the `text` field so callers can log\n * the raw model output for debugging.\n */\nexport class MantyxParseError extends MantyxError {\n readonly text: string;\n\n constructor(message: string, text: string, opts: { cause?: unknown } = {}) {\n super(message, { code: \"output_parse_failed\" });\n this.name = \"MantyxParseError\";\n this.text = text;\n if (opts.cause !== undefined) {\n (this as Error & { cause?: unknown }).cause = opts.cause;\n }\n }\n}\n","/**\n * Lightweight Zod → JSON Schema converter for tool parameter definitions.\n *\n * Tries `z.toJSONSchema` (Zod v4+) first; falls back to a hand-rolled walker\n * for v3 schemas so the SDK works on a wide range of zod versions.\n *\n * The output is a JSON-Schema-shaped object with `type: \"object\"`, `properties`,\n * and `required`. The MANTYX server feeds this to LLM providers verbatim, so\n * unsupported zod features (effects, transforms, intersections) degrade to a\n * permissive `\"object\"` description rather than failing.\n */\nimport { z } from \"zod\";\n\ntype JsonSchema = Record<string, unknown>;\n\ninterface ZodLikeWithToJsonSchema {\n toJSONSchema?: (schema: unknown) => JsonSchema;\n}\n\nexport function zodToJsonSchema(schema: z.ZodType<unknown>): JsonSchema {\n const builtIn = (z as unknown as ZodLikeWithToJsonSchema).toJSONSchema;\n if (typeof builtIn === \"function\") {\n try {\n const out = builtIn.call(z, schema) as JsonSchema;\n if (out && typeof out === \"object\") return out;\n } catch {\n // fall through to manual converter\n }\n }\n return convertNode(schema);\n}\n\nfunction convertNode(schema: z.ZodType<unknown>): JsonSchema {\n const def = (schema as unknown as { _def?: { typeName?: string } })._def;\n const typeName = def?.typeName;\n switch (typeName) {\n case \"ZodString\":\n return { type: \"string\" };\n case \"ZodNumber\":\n return { type: \"number\" };\n case \"ZodBoolean\":\n return { type: \"boolean\" };\n case \"ZodNull\":\n return { type: \"null\" };\n case \"ZodLiteral\": {\n const value = (def as { value?: unknown }).value;\n return { const: value, type: typeof value };\n }\n case \"ZodEnum\": {\n const values = (def as { values?: readonly string[] }).values ?? [];\n return { type: \"string\", enum: [...values] };\n }\n case \"ZodArray\": {\n const inner = (def as { type?: z.ZodType<unknown> }).type;\n return {\n type: \"array\",\n items: inner ? convertNode(inner) : {},\n };\n }\n case \"ZodOptional\":\n case \"ZodNullable\": {\n const inner = (def as { innerType?: z.ZodType<unknown> }).innerType;\n return inner ? convertNode(inner) : {};\n }\n case \"ZodDefault\": {\n const inner = (def as { innerType?: z.ZodType<unknown> }).innerType;\n return inner ? convertNode(inner) : {};\n }\n case \"ZodObject\": {\n const shape = (def as { shape?: () => Record<string, z.ZodType<unknown>> }).shape;\n const fields = typeof shape === \"function\" ? shape() : (shape as Record<string, z.ZodType<unknown>> | undefined);\n const properties: Record<string, JsonSchema> = {};\n const required: string[] = [];\n if (fields) {\n for (const [key, value] of Object.entries(fields)) {\n properties[key] = convertNode(value);\n const innerDef = (value as unknown as { _def?: { typeName?: string } })._def;\n const innerTypeName = innerDef?.typeName;\n if (innerTypeName !== \"ZodOptional\" && innerTypeName !== \"ZodDefault\") {\n required.push(key);\n }\n }\n }\n const out: JsonSchema = { type: \"object\", properties };\n if (required.length > 0) out.required = required;\n return out;\n }\n default:\n return {};\n }\n}\n\n/**\n * Coerce a JSON-Schema-shaped value into a wire object suitable for the\n * MANTYX local-tool definition payload. Accepts either a Zod schema or an\n * already-shaped JSON Schema object.\n */\nexport function toToolParametersWire(\n parameters: z.ZodType<unknown> | JsonSchema | undefined,\n): JsonSchema {\n if (!parameters) return { type: \"object\", properties: {} };\n if (typeof (parameters as { _def?: unknown })._def !== \"undefined\") {\n return zodToJsonSchema(parameters as z.ZodType<unknown>);\n }\n return parameters as JsonSchema;\n}\n","/**\n * MANTYX SDK client: HTTP plumbing, model catalog, run + session drivers.\n */\nimport {\n MantyxAuthError,\n MantyxError,\n MantyxNetworkError,\n MantyxParseError,\n MantyxRunError,\n MantyxScopeError,\n MantyxToolError,\n} from \"./errors.js\";\nimport type { MantyxRunErrorInit } from \"./errors.js\";\nimport { callA2A, callMcpTool, closeMcpRefs, resolveLocalRefs } from \"./local-resolver.js\";\nimport type { TokenSource } from \"./oauth.js\";\nimport { readSseStream } from \"./sse.js\";\nimport type {\n LocalA2ATool,\n LocalMcpServer,\n LocalTool,\n ReasoningLevel,\n ToolRef,\n} from \"./tools.js\";\nimport { isLocalA2ATool, isLocalMcpServer, isLocalTool, prefixedMcpToolName } from \"./tools.js\";\nimport { toToolParametersWire } from \"./zod-to-json-schema.js\";\n\nexport const DEFAULT_BASE_URL = \"https://app.mantyx.io\";\n\nexport interface MantyxClientOptions {\n /**\n * Workspace API key (token prefix `mantyx_`) **or** a MANTYX OAuth 2.0\n * access token (token prefix `mantyx_at_`). The server resolves either\n * kind by token-prefix, so the SDK uses a single credential code path.\n *\n * Prefer the {@link accessToken} alias when wiring up an OAuth-based\n * application — the two options are semantically identical (the value\n * is forwarded as `Authorization: Bearer <credential>`), but\n * `accessToken` makes the intent obvious at the call site.\n *\n * Exactly one of `apiKey` / `accessToken` must be set. Passing both —\n * even to the same value — throws `MantyxError` at construction time.\n *\n * See `docs/agent-runs-protocol.md` §2 for the full credential table\n * (including which prefix means what, scope semantics, and the\n * `insufficient_scope` 403 SDKs surface via\n * {@link MantyxScopeError}).\n */\n apiKey?: string;\n /**\n * MANTYX OAuth 2.0 access token (token prefix `mantyx_at_…`). Exactly\n * one of {@link apiKey} / `accessToken` / {@link tokenSource} must be\n * set; passing more than one throws `MantyxError` at construction\n * time.\n *\n * Functionally identical to {@link apiKey} — the SDK ships either\n * value verbatim on `Authorization: Bearer <credential>` — but using\n * the OAuth-specific name makes scope-driven applications easier to\n * read.\n *\n * OAuth tokens additionally enforce per-route **scopes**\n * (`runs:read`, `runs:write`, `sessions:read`, `sessions:write`,\n * `models:read`, `mantyx.identity:read`); see\n * `docs/agent-runs-protocol.md` §2.2 for the table. Missing scopes\n * land as {@link MantyxScopeError} so callers can route the user\n * back to a re-consent flow.\n *\n * Static `accessToken` values are 1-hour-lived per `docs/oauth.md`\n * §\"Token lifetimes & lifecycle\" — for long-running processes\n * prefer {@link tokenSource} so the SDK can refresh transparently.\n */\n accessToken?: string;\n /**\n * Dynamic credential provider. The SDK calls it before every request\n * to obtain the current access token, and again with\n * `reason: \"unauthorized\"` after a 401 so it can refresh and retry\n * the request exactly once.\n *\n * Build one via `oauthClient.refreshTokenSource({ refreshToken })`\n * or `oauthClient.clientCredentialsTokenSource()` — see\n * [`./oauth.ts`](./oauth.ts) for the helpers, or pass any function\n * matching the {@link TokenSource} signature for full custom\n * control (e.g. tokens minted by an upstream auth proxy).\n *\n * Exactly one of {@link apiKey} / {@link accessToken} / `tokenSource`\n * must be set.\n */\n tokenSource?: TokenSource;\n workspaceSlug: string;\n /** Defaults to `https://app.mantyx.io`. Override for self-hosted instances. */\n baseUrl?: string;\n /** Optional `fetch` override (e.g. node-fetch wrapper, or a custom HTTP client). */\n fetch?: typeof fetch;\n /** Default per-request timeout in milliseconds. Default: 60s. */\n timeoutMs?: number;\n}\n\nexport interface ModelInfo {\n id: string;\n label: string;\n provider: string;\n vendorModelId: string;\n source: \"workspace_provider\" | \"platform_offering\";\n contextWindowTokens: number | null;\n pricing: {\n inputPer1MUsd: number | null;\n outputPer1MUsd: number | null;\n cacheReadPer1MUsd: number | null;\n } | null;\n}\n\nexport interface ModelCatalog {\n models: ModelInfo[];\n defaultModelId: string | null;\n}\n\nexport interface AgentSpecBase {\n name?: string;\n /**\n * Reference to a persisted MANTYX agent in this workspace. When set, the\n * server hydrates `systemPrompt`, `modelId`, and the agent's own tools\n * (memory, skills, plugin tools, …) from the Agent row at run time, and any\n * `tools` you supply here are merged on top — typically `local` tools the\n * SDK wants the agent to be able to call back into.\n *\n * Either `agentId` or `systemPrompt` must be set.\n */\n agentId?: string;\n /** Required unless `agentId` is set. */\n systemPrompt?: string;\n modelId?: string;\n tools?: ToolRef[];\n /**\n * Provider thinking strength: a string anchor (`\"off\" | \"low\" | \"medium\" |\n * \"high\"`) or an integer in `0..100` (where `0` explicitly disables provider\n * thinking on reasoning models). The server maps this onto each LLM's\n * native dial — see `docs/agent-runs-protocol.md` §4.4.\n *\n * For session-scoped runs the session value sets the default; per-message\n * overrides on `session.send` apply to that single run.\n */\n reasoningLevel?: ReasoningLevel;\n budgets?: { maxToolTurns?: number };\n /**\n * Constrains the model's **final assistant text** to a JSON document\n * matching a JSON Schema. The terminal `result` event still carries the\n * reply as `text: string`, but that string is guaranteed-parseable JSON.\n *\n * `name` (optional) is a stable identifier the server forwards to the\n * provider (OpenAI `text.format.name`, Anthropic synthetic-tool name).\n * Defaults to `\"output\"`. Must match `/^[a-zA-Z0-9_-]{1,64}$/`.\n *\n * `schema` is a JSON Schema describing the final assistant text. Its\n * root must be a JSON **object** — most providers reject array / scalar\n * roots in structured-output mode. The schema is shipped verbatim;\n * MANTYX does not validate its contents (the provider does).\n *\n * Use {@link parseRunOutput} on the resulting `RunResult` to JSON.parse\n * the reply (and optionally re-validate against your own zod / typebox /\n * ajv schema). See `docs/wire-protocol.md` §7.\n */\n outputSchema?: OutputSchema;\n /**\n * Loop-detection guard. Tracks an order-invariant `(toolName, args)`\n * signature for every assistant turn that emits one or more tool calls;\n * when the same signature repeats consecutively the pipeline first injects\n * a steering nudge (\"either deliver a final answer or change strategy\")\n * and eventually forces a tools-disabled finalise turn.\n *\n * Pass an object to override the default thresholds, or `false` to\n * explicitly disable the guard for this run / session. When omitted, the\n * MANTYX runtime defaults apply (`{ consecutiveThreshold: 3,\n * hardCutoffThreshold: 6 }`). See `docs/agent-runs-protocol.md` §4.6.\n *\n * Each intervention emits an observability-only `loop_detected` SSE event\n * the SDK surfaces on the run-event stream (`tools` lists the looping\n * batch; `hardCutoff: false` is the soft nudge round, `true` is the\n * forced finalise). The synthetic skip + nudge are emitted on the normal\n * `tool_result` / `assistant_delta` channels — the SDK does not need to\n * act on the event itself.\n */\n loopDetection?: LoopDetection | false;\n /**\n * Per-tool call caps enforced over the **lifetime of the run** (across\n * every LLM turn). Calls under the cap run normally; calls past the cap\n * are intercepted before execution and returned to the model as a\n * synthetic \"budget exceeded — pivot or finalize\" tool result.\n *\n * Keys are the model-facing tool names (the same string on\n * `local_tool_call.name`); values are `{ maxCalls: number }`. `maxCalls:\n * 0` disables the tool entirely (the first attempt returns the synthetic\n * body). Budgets are **per-tool, not pooled**.\n *\n * Pass `{}` to start from a clean slate (no defaults applied on top —\n * useful for runs that intentionally want unbounded research). Omit\n * entirely to keep the runtime defaults. Each interception emits an\n * observability-only `tool_budget_exceeded` SSE event. See\n * `docs/agent-runs-protocol.md` §4.7.\n */\n toolBudgets?: ToolBudgets;\n /**\n * Run-supervisor (platform LLM judge). Periodically reviews the agent's\n * transcript and may steer the run (`on_track`, `redirect`, `finalize`).\n *\n * Pass an object to override the review interval, or `false` to explicitly\n * disable the platform judge for this run / session. When omitted on\n * ephemeral API runs, MANTYX enables the supervisor (default interval `5`).\n * SDK-only runs (`runAgent` without the HTTP API) keep the supervisor off\n * unless you pass a value here. See `docs/agent-runs-protocol.md` §4.8.\n *\n * Each review emits an observability-only `supervisor` SSE event — including\n * `on_track` checks — so the SDK can render supervisor activity. When\n * `action` is `redirect` or `finalize`, the pipeline has already applied\n * the verdict by the time the event arrives.\n */\n supervisor?: Supervisor | false;\n /**\n * Flat string→string KV carried alongside the run / session for\n * observability. Use it to tag runs with your own application identifiers\n * (customer id, environment, workflow name, …) — the values are visible in\n * the MANTYX dashboard and can be filtered there.\n *\n * Limits enforced server-side: max 16 entries; keys match\n * `[A-Za-z0-9._-]{1,64}`; values are strings ≤ 256 chars; serialized JSON\n * ≤ 4 KB. For session-scoped runs, the session's metadata is inherited and\n * any per-message override is merged on top.\n */\n metadata?: Record<string, string>;\n}\n\nexport interface RunSpec extends AgentSpecBase {\n prompt?: string;\n messages?: Array<{ role: \"user\" | \"assistant\" | \"system\"; content: string }>;\n /** Receives streaming assistant text deltas. */\n onAssistantDelta?: (delta: string) => void;\n /** Receives raw events (assistant_message, local_tool_call, tool_result, ...) for advanced consumers. */\n onEvent?: (event: RunEvent) => void;\n /** Aborts the run on the client and best-effort cancels server-side. */\n signal?: AbortSignal;\n}\n\nexport type SessionSpec = AgentSpecBase;\n\n/**\n * Constrains the final assistant text to a JSON document matching a\n * JSON Schema. See {@link AgentSpecBase.outputSchema} for the full\n * semantics.\n */\nexport interface OutputSchema {\n /** Optional. Defaults to `\"output\"`. Must match `/^[a-zA-Z0-9_-]{1,64}$/`. */\n name?: string;\n /** Required. JSON Schema describing the final assistant text. Root must be a JSON object. */\n schema: Record<string, unknown>;\n}\n\n/**\n * Loop-detection thresholds. See {@link AgentSpecBase.loopDetection} for the\n * full semantics. Pass `false` (instead of an object) to disable the guard.\n *\n * Both fields are optional; omitted ones inherit the MANTYX runtime\n * defaults (`consecutiveThreshold: 3`, `hardCutoffThreshold: 6`).\n */\nexport interface LoopDetection {\n /**\n * Number of identical consecutive tool-call batches that triggers the\n * **soft nudge** — the pipeline injects a steering message (\"either\n * deliver a final answer or change strategy\"). Default `3`. Must be\n * `>= 2` (one identical batch is just a single tool call, not a loop).\n * Server-side upper bound: `100`.\n */\n consecutiveThreshold?: number;\n /**\n * Number of identical consecutive tool-call batches that triggers the\n * **hard cutoff** — the pipeline forces a tools-disabled finalise turn.\n * Default `6`. Must be strictly greater than `consecutiveThreshold` (so\n * the soft nudge has a chance to land). Server-side upper bound: `100`.\n */\n hardCutoffThreshold?: number;\n}\n\n/**\n * Per-tool call cap. See {@link AgentSpecBase.toolBudgets} for the full\n * semantics.\n */\nexport interface ToolBudget {\n /**\n * Hard cap on executed calls per run. `0` disables the tool entirely\n * (every attempt returns the synthetic \"budget exceeded\" body on the\n * first try). Server-side upper bound: `1000` (functionally unlimited;\n * the in-runtime `maxToolTurns: 100` fires first).\n */\n maxCalls: number;\n}\n\n/**\n * Map of model-facing tool name → cap. See\n * {@link AgentSpecBase.toolBudgets}. Pass an empty object (`{}`) to start\n * from a clean slate (no runtime defaults applied on top); omit the field\n * entirely to keep the defaults.\n */\nexport type ToolBudgets = Record<string, ToolBudget>;\n\n/**\n * Run-supervisor configuration. See {@link AgentSpecBase.supervisor} for the\n * full semantics. Pass `false` (instead of an object) to disable the platform\n * judge for the run / session.\n *\n * `interval` is optional; when omitted the MANTYX runtime default is **5**\n * LLM calls between reviews. Server-side upper bound: `100`.\n */\nexport interface Supervisor {\n /** LLM calls (`completeTurn` invocations) between supervisor reviews. */\n interval?: number;\n}\n\n/** Verdict from a run-supervisor review. */\nexport type SupervisorAction = \"on_track\" | \"redirect\" | \"finalize\";\n\n/**\n * Per-run token totals attached to terminal `result` / `error` events\n * (and to the `GET /agent-runs/:runId` snapshot) by MANTYX ≥ 2026-09.\n *\n * Aggregated across every model invocation for the run. See\n * `docs/agent-runs-protocol.md` §7.1 for the per-provider mapping and\n * the relationship between buckets (`inputTokens` / `outputTokens` are\n * the billable totals; `cachedTokens` and `reasoningTokens` are\n * diagnostic breakdowns _inside_ those two totals, not separate\n * additive buckets).\n *\n * Older servers omit the cost-attribution triple entirely; SDK callers\n * detect \"no usage data\" by checking `result.model?.provider` is empty\n * / undefined.\n */\nexport interface RunTokenUsage {\n /**\n * Total billable input tokens — fresh prompt tokens plus the\n * cached-read slice the provider still bills (at a discount) plus\n * any cache-creation tokens plus tool-prompt tokens. Equal to the\n * sum of every provider-reported input bucket for the run.\n */\n inputTokens: number;\n /**\n * The discounted slice of `inputTokens` that came from a prompt\n * cache hit (Anthropic prompt caching, OpenAI cached prompt, Gemini\n * implicit cache). `0` when the provider doesn't report cache reads\n * or the run didn't hit cache.\n */\n cachedTokens: number;\n /**\n * Non-visible thinking tokens. **Already counted inside\n * `outputTokens`** — surfaced separately so dashboards can break out\n * \"thinking cost\" vs visible output. `0` when the model didn't\n * reason or didn't report it.\n */\n reasoningTokens: number;\n /**\n * All tokens the model emitted for this run, visible + reasoning.\n * Matches the provider's \"completion tokens\" / \"output tokens\"\n * billing line.\n */\n outputTokens: number;\n}\n\n/**\n * The resolved model the platform stamped onto the run, surfaced on\n * terminal `result` / `error` events (and `GET /agent-runs/:runId`)\n * by MANTYX ≥ 2026-09. See `docs/agent-runs-protocol.md` §7.1.\n */\nexport interface RunModelInfo {\n /**\n * Catalog id — the same string a caller would pass back as\n * `modelId` to re-select this exact entry (e.g. `\"platform:demo\"`,\n * `\"provider:cmf…\"`). Empty string against legacy fallbacks that\n * didn't synthesise a catalog id.\n */\n id: string;\n /**\n * Lowercase provider id: `\"openai\"`, `\"anthropic\"`, `\"google\"`,\n * `\"azure-openai\"`. Empty string against legacy runners that don't\n * report usage data — SDK callers use that as the \"no usage data\"\n * signal.\n */\n provider: string;\n /**\n * The model id the platform actually sent to the provider (e.g.\n * `\"gpt-5.4-mini\"`, `\"claude-opus-4-7\"`, `\"gemini-2.5-pro\"`).\n */\n vendorModelId: string;\n /**\n * `\"off\" | \"low\" | \"medium\" | \"high\"`. Omitted when the provider\n * doesn't expose a reasoning-level knob or the run didn't request\n * one.\n */\n reasoningEffort?: string;\n}\n\nexport interface RunResult {\n runId: string;\n text: string;\n events: RunEvent[];\n /**\n * Per-run token totals from the terminal event. Undefined against\n * MANTYX servers older than 2026-09 (the \"no usage data\" signal is\n * `result.model?.provider` being empty / undefined). See\n * {@link RunTokenUsage} and `docs/agent-runs-protocol.md` §7.1.\n */\n tokens?: RunTokenUsage;\n /**\n * Total `engine.completeTurn(...)` invocations for the run,\n * including the failing call when a run errored mid-loop. A\n * single-shot run reports `1`; a tool loop is `>= 2`. Undefined\n * against legacy MANTYX servers.\n */\n turns?: number;\n /** Resolved model that executed the run. See {@link RunModelInfo}. */\n model?: RunModelInfo;\n}\n\nexport interface RunEventBase {\n seq: number;\n type: string;\n}\n\nexport interface AssistantDeltaEvent extends RunEventBase {\n type: \"assistant_delta\";\n text: string;\n}\n\nexport interface ThinkingDeltaEvent extends RunEventBase {\n type: \"thinking_delta\";\n text: string;\n}\n\nexport interface AssistantMessageEvent extends RunEventBase {\n type: \"assistant_message\";\n /**\n * Full assistant text for this turn (concatenation of every preceding\n * `assistant_delta` for the turn, plus any non-streaming snapshot the\n * engine appended at close). May be empty when the turn was tool-only.\n */\n text: string;\n /**\n * 0-based tool-turn index this assistant message closes. Useful for\n * SDK clients pairing the message with the subsequent `tool_result`\n * rows.\n */\n turn?: number;\n /**\n * Canonical lowercase stop reason normalized across providers\n * (`\"end_turn\"`, `\"tool_use\"`, `\"max_tokens\"`, `\"refusal\"`,\n * `\"malformed_function_call\"`, …). `null` / omitted when the provider\n * did not report one.\n */\n finishReason?: string | null;\n /**\n * Tool calls the model emitted on this turn. Omitted when the model\n * did not call any tools.\n */\n toolCalls?: Array<{\n id: string;\n name: string;\n input: Record<string, unknown>;\n }>;\n}\n\nexport interface ServerToolResultEvent extends RunEventBase {\n type: \"tool_result\";\n name: string;\n args?: Record<string, unknown>;\n ok?: boolean;\n summary?: string;\n phase?: \"start\" | \"end\";\n}\n\nexport interface LocalToolCallEvent extends RunEventBase {\n type: \"local_tool_call\";\n toolUseId: string;\n /**\n * The model-facing tool name. For `kind: \"mcp_local\"` events this is the\n * `<server>_<tool>` name the SDK declared on the wire; the SDK looks up\n * the local MCP server via `mcpServer` and forwards `mcpToolName` to\n * `tools/call` rather than parsing the prefix itself.\n */\n name: string;\n args: Record<string, unknown>;\n /**\n * Discriminator for which client-resolved handler should run.\n * - `\"local\"` (or omitted) — generic local tool\n * - `\"a2a_local\"` — local Agent2Agent peer\n * - `\"mcp_local\"` — local MCP server tool\n */\n kind?: \"local\" | \"a2a_local\" | \"mcp_local\";\n /**\n * Present on `kind: \"a2a_local\"` — the full A2A Agent Card the SDK shipped\n * with the spec, echoed back unchanged. Surfaced for advanced consumers\n * (`onEvent` / `streamAgent` callers); the built-in dispatcher ignores it\n * because it already has the cached card from the original\n * `defineLocalA2A` resolution.\n */\n agentCard?: { name: string; url?: string; [k: string]: unknown };\n /** Present on `kind: \"mcp_local\"` — server label declared via `defineLocalMcp`. */\n mcpServer?: string;\n /**\n * Present on `kind: \"mcp_local\"` — the model-facing tool name as declared on\n * the wire. Always equals `name`; surfaced as a separate field for the SDK's\n * convenience when dispatching into a local MCP client.\n */\n mcpToolName?: string;\n /**\n * Present on `kind: \"mcp_local\"` — the verbatim `Implementation` block from\n * MCP `Initialize`, echoed back for observability.\n */\n mcpServerInfo?: { name: string; version?: string; [k: string]: unknown };\n}\n\nexport interface LocalToolResultInEvent extends RunEventBase {\n type: \"local_tool_result_in\";\n toolUseId: string;\n result?: string;\n error?: string;\n}\n\n/**\n * Observability event fired when the loop-detection guard intervenes.\n * The synthetic skip + steering nudge are emitted on the normal\n * `tool_result` / `assistant_delta` channels; this event lets the SDK\n * render a status note (`looping — nudged` / `looping — gave up`).\n *\n * `hardCutoff: false` is the soft nudge round; `true` is the forced\n * finalise. The same run may emit one of each.\n */\nexport interface LoopDetectedEvent extends RunEventBase {\n type: \"loop_detected\";\n /** Length of the identical-batch streak that just tripped the threshold. */\n consecutiveCount: number;\n /** `false` for the soft nudge round; `true` once the pipeline forces finalisation. */\n hardCutoff: boolean;\n /** Names of the tool calls in the looping batch (no args). */\n tools: string[];\n}\n\n/**\n * Observability event fired when a tool-budget interception happens. The\n * synthetic \"budget exceeded — pivot or finalize\" tool result lands on the\n * normal `tool_result` channel before this event fires; the SDK uses this\n * event to render UI banners (`memory budget exhausted` etc.) without\n * re-parsing tool-result bodies.\n */\nexport interface ToolBudgetExceededEvent extends RunEventBase {\n type: \"tool_budget_exceeded\";\n /** Logical tool name (matches the key in `spec.toolBudgets`). */\n tool: string;\n /** Configured cap. */\n maxCalls: number;\n /**\n * 1-based count of attempts to call this tool over the run lifetime.\n * Always strictly greater than `maxCalls`.\n */\n callIndex: number;\n}\n\n/**\n * Observability event fired on every run-supervisor review — including\n * `on_track` checks. When `action` is `redirect` or `finalize`, the pipeline\n * has already injected the steering message or forced a tools-disabled turn\n * by the time this event arrives; the SDK should render a status note and\n * keep consuming the stream.\n */\nexport interface SupervisorEvent extends RunEventBase {\n type: \"supervisor\";\n /** One of `\"on_track\"`, `\"redirect\"`, `\"finalize\"`. */\n action: SupervisorAction;\n /** One- or two-sentence explanation from the judge. */\n reason: string;\n /**\n * Present when `action === \"redirect\"`: the steering user message injected\n * into the conversation. Omitted for `on_track` / `finalize`.\n */\n redirect?: string;\n /**\n * Number of LLM calls completed when this review fired. Matches the\n * pipeline's `modelInvocations` counter at the check boundary.\n */\n llmCalls: number;\n}\n\nexport interface ResultEvent extends RunEventBase {\n type: \"result\";\n subtype: string;\n text?: string;\n error?: string;\n /**\n * Per-run token totals. Present against MANTYX ≥ 2026-09 — see\n * {@link RunTokenUsage} and `docs/agent-runs-protocol.md` §7.1.\n */\n tokens?: RunTokenUsage;\n /** Total model invocations for the run. See {@link RunResult.turns}. */\n turns?: number;\n /** Resolved model that executed the run. See {@link RunModelInfo}. */\n model?: RunModelInfo;\n}\n\nexport interface ErrorEvent extends RunEventBase {\n type: \"error\";\n /** Human-readable failure message. */\n error: string;\n /**\n * Legacy alias for {@link errorClass}. Equals `errorClass` when present;\n * otherwise a small lowercase token (`\"error\"`, `\"invalid_spec\"`,\n * `\"worker_error\"`, …).\n */\n code?: string;\n /**\n * Canonical failure category. One of `\"rate_limit\"`, `\"overloaded\"`,\n * `\"server\"`, `\"context_window\"`, `\"truncation\"`, `\"invalid_request\"`,\n * `\"auth\"`, `\"timeout\"`, `\"local_timeout\"`, `\"upstream_deadline\"`,\n * `\"unknown\"`. New categories may land additively. See\n * `docs/agent-runs-protocol.md` §7 for the full list.\n */\n errorClass?: string;\n /**\n * Canonical lowercase stop reason normalized across providers\n * (`\"max_tokens\"`, `\"refusal\"`, `\"malformed_function_call\"`, …). When\n * present, mirrors the value on the last `assistant_message` event.\n */\n finishReason?: string | null;\n /**\n * **Best-effort raw bytes** the model emitted before the failure. For\n * `outputSchema` runs this is likely **incomplete JSON** that will\n * fail `JSON.parse` — see the wire-protocol truncation contract. Also\n * persisted on `EphemeralAgentRun.finalText` so SDKs can recover it\n * via `GET /agent-runs/:runId` after the SSE stream closes.\n */\n partialText?: string;\n /**\n * Coarse retry hint inherited from the pipeline's error classifier.\n * Informational; the SDK still owns the actual retry decision.\n */\n retryable?: boolean;\n /**\n * Per-run token totals. Present against MANTYX ≥ 2026-09 — see\n * {@link RunTokenUsage} and `docs/agent-runs-protocol.md` §7.1.\n * The pipeline counts the failing model call too, so a run that\n * threw on the first turn reports `turns: 1` with that call's\n * tokens already aggregated.\n */\n tokens?: RunTokenUsage;\n /** Total model invocations for the run, including the failing call. */\n turns?: number;\n /** Resolved model that executed the run. See {@link RunModelInfo}. */\n model?: RunModelInfo;\n}\n\nexport interface CancelledEvent extends RunEventBase {\n type: \"cancelled\";\n reason?: string;\n}\n\nexport type RunEvent =\n | AssistantDeltaEvent\n | ThinkingDeltaEvent\n | AssistantMessageEvent\n | ServerToolResultEvent\n | LocalToolCallEvent\n | LocalToolResultInEvent\n | LoopDetectedEvent\n | ToolBudgetExceededEvent\n | SupervisorEvent\n | ResultEvent\n | ErrorEvent\n | CancelledEvent\n | (RunEventBase & { type: string; [key: string]: unknown });\n\nexport interface SessionInfo {\n id: string;\n name: string;\n status: \"active\" | \"ended\";\n createdAt: string;\n lastUsedAt: string;\n endedAt: string | null;\n agentSpec: AgentSpecBase;\n messages: Array<{ role: \"user\" | \"assistant\" | \"system\"; content: string }>;\n /** Metadata that was attached to the session at create time, returned for observability. */\n metadata: Record<string, string>;\n}\n\nexport class MantyxClient {\n readonly options: Required<Pick<MantyxClientOptions, \"workspaceSlug\" | \"baseUrl\">> & {\n /**\n * Single resolved bearer credential — either a workspace API key\n * (token prefix `mantyx_`) or an OAuth access token (`mantyx_at_…`).\n * The SDK does not need to distinguish them on the wire; the value\n * is forwarded verbatim on `Authorization: Bearer …`.\n *\n * Kept as `apiKey` (instead of e.g. `credential`) for backwards\n * compatibility — older releases exposed it under this name.\n *\n * Empty string when a {@link tokenSource} is configured — every\n * request resolves the bearer from the source instead.\n */\n apiKey: string;\n fetch: typeof fetch;\n timeoutMs: number;\n /**\n * Dynamic credential provider when constructed with\n * `tokenSource` — see {@link MantyxClientOptions.tokenSource}.\n * `null` for static `apiKey` / `accessToken` clients.\n */\n tokenSource: TokenSource | null;\n };\n\n constructor(opts: MantyxClientOptions) {\n const { credential, tokenSource } = resolveCredential(opts);\n if (!opts.workspaceSlug || typeof opts.workspaceSlug !== \"string\") {\n throw new MantyxError(\"workspaceSlug is required\");\n }\n const f = opts.fetch ?? globalThis.fetch;\n if (typeof f !== \"function\") {\n throw new MantyxError(\n \"Global fetch is not available; pass a custom `fetch` implementation in MantyxClientOptions.\",\n );\n }\n this.options = {\n apiKey: credential,\n workspaceSlug: opts.workspaceSlug,\n baseUrl: (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\"),\n fetch: f,\n timeoutMs: opts.timeoutMs ?? 60_000,\n tokenSource,\n };\n }\n\n // -------------------------------------------------------------- Models\n\n async listModels(): Promise<ModelCatalog> {\n return this.request<ModelCatalog>({\n method: \"GET\",\n path: \"/models\",\n });\n }\n\n // ------------------------------------------------------------- One-shot\n\n async runAgent(spec: RunSpec): Promise<RunResult> {\n const tools = spec.tools ?? [];\n // Resolve every `a2a_local` agent card and open every `mcp_local`\n // transport before submitting; the resolver mutates the refs in place\n // so the subsequent `serializeAgentSpec` reads the resolved data.\n await resolveLocalRefs(tools, { fetch: this.options.fetch });\n const handlers = collectLocalHandlers(tools);\n try {\n const created = await this.request<{ runId: string; streamUrl: string }>({\n method: \"POST\",\n path: \"/agent-runs\",\n body: serializeAgentSpec(spec, {\n prompt: spec.prompt,\n messages: spec.messages,\n }),\n });\n return await this.driveRun(created.runId, handlers, {\n ...(spec.onAssistantDelta ? { onAssistantDelta: spec.onAssistantDelta } : {}),\n ...(spec.onEvent ? { onEvent: spec.onEvent } : {}),\n ...(spec.signal ? { signal: spec.signal } : {}),\n });\n } finally {\n // One-shot runs own their MCP transports; close them on exit.\n await closeMcpRefs(tools);\n }\n }\n\n async *streamAgent(spec: RunSpec): AsyncGenerator<RunEvent, void, void> {\n const tools = spec.tools ?? [];\n await resolveLocalRefs(tools, { fetch: this.options.fetch });\n const handlers = collectLocalHandlers(tools);\n try {\n const created = await this.request<{ runId: string; streamUrl: string }>({\n method: \"POST\",\n path: \"/agent-runs\",\n body: serializeAgentSpec(spec, {\n prompt: spec.prompt,\n messages: spec.messages,\n }),\n });\n yield* this.streamRunEvents(created.runId, handlers, spec.signal);\n } finally {\n await closeMcpRefs(tools);\n }\n }\n\n /**\n * Internal registry of client-resolved tool handlers. Exposed for callers\n * who drive the run loop manually via `driveRun` / `streamRunEvents`.\n */\n collectHandlers(tools: ToolRef[]): LocalHandlers {\n return collectLocalHandlers(tools);\n }\n\n // ------------------------------------------------------------- Sessions\n\n async createSession(spec: SessionSpec): Promise<AgentSession> {\n const tools = spec.tools ?? [];\n // Resolve local refs once at session creation; the session keeps the\n // resolved cards / live MCP connections for its lifetime.\n await resolveLocalRefs(tools, { fetch: this.options.fetch });\n const handlers = collectLocalHandlers(tools);\n const created = await this.request<{ sessionId: string; name: string; createdAt: string }>({\n method: \"POST\",\n path: \"/agent-sessions\",\n body: serializeAgentSpec(spec),\n });\n return new AgentSession(this, created.sessionId, handlers, tools);\n }\n\n /**\n * Re-emit a `local_tool_call` event into the right local handler. Useful\n * for tests and for users who consume events via `streamAgent` themselves.\n */\n async dispatchLocalToolFromEvent(\n runId: string,\n ev: LocalToolCallEvent,\n handlers: LocalHandlers,\n ): Promise<void> {\n return this.dispatchLocalTool(runId, ev, handlers);\n }\n\n async resumeSession(\n sessionId: string,\n opts: { tools?: ToolRef[] } = {},\n ): Promise<AgentSession> {\n // Verify the session exists and is still active. Optionally refresh tool defs.\n await this.getSessionInfo(sessionId);\n const tools = opts.tools ?? [];\n if (tools.length > 0) {\n // Resolve before the first send — mirrors createSession.\n await resolveLocalRefs(tools, { fetch: this.options.fetch });\n }\n const handlers = collectLocalHandlers(tools);\n return new AgentSession(this, sessionId, handlers, tools);\n }\n\n async endSession(sessionId: string): Promise<void> {\n await this.request<{ ok: boolean }>({\n method: \"DELETE\",\n path: `/agent-sessions/${encodeURIComponent(sessionId)}`,\n });\n }\n\n async getSessionInfo(sessionId: string): Promise<SessionInfo> {\n return this.request<SessionInfo>({\n method: \"GET\",\n path: `/agent-sessions/${encodeURIComponent(sessionId)}`,\n });\n }\n\n // ----------------------------------------------------------- Internals\n\n /** Drive an existing run to completion (collect events, dispatch local tools). */\n async driveRun(\n runId: string,\n handlers: LocalHandlers,\n opts: {\n onAssistantDelta?: (delta: string) => void;\n onEvent?: (event: RunEvent) => void;\n signal?: AbortSignal;\n } = {},\n ): Promise<RunResult> {\n const collected: RunEvent[] = [];\n let finalText = \"\";\n // Cost-attribution triple, populated from the terminal event when\n // MANTYX ≥ 2026-09 surfaces it. Older runners omit the fields and\n // we leave the result's `tokens` / `turns` / `model` undefined —\n // callers detect \"no usage data\" via `result.model?.provider`.\n let tokens: RunTokenUsage | undefined;\n let turns: number | undefined;\n let modelInfo: RunModelInfo | undefined;\n for await (const ev of this.streamRunEvents(runId, handlers, opts.signal)) {\n collected.push(ev);\n if (opts.onEvent) opts.onEvent(ev);\n if (ev.type === \"assistant_delta\" && opts.onAssistantDelta) {\n opts.onAssistantDelta((ev as AssistantDeltaEvent).text);\n }\n if (ev.type === \"result\") {\n const r = ev as ResultEvent;\n tokens = parseRunTokens(r.tokens) ?? tokens;\n turns = parseRunTurns(r.turns) ?? turns;\n modelInfo = parseRunModel(r.model) ?? modelInfo;\n if (r.subtype === \"success\") {\n finalText = typeof r.text === \"string\" ? r.text : \"\";\n } else {\n const errInit: MantyxRunErrorInit = {};\n if (tokens !== undefined) errInit.tokens = tokens;\n if (turns !== undefined) errInit.turns = turns;\n if (modelInfo !== undefined) errInit.model = modelInfo;\n throw new MantyxRunError(runId, r.subtype, r.error ?? r.subtype, errInit);\n }\n } else if (ev.type === \"error\") {\n const e = ev as ErrorEvent;\n // The wire reports both a coarse `code` (legacy alias) and a\n // canonical `errorClass` triage category; prefer `errorClass`\n // when present so the SDK exposes a stable taxonomy. See\n // `docs/agent-runs-protocol.md` §7.\n const subtype = e.errorClass ?? e.code ?? \"error\";\n const errInit: MantyxRunErrorInit = {};\n if (e.errorClass !== undefined) errInit.errorClass = e.errorClass;\n if (e.finishReason !== undefined) errInit.finishReason = e.finishReason;\n if (typeof e.partialText === \"string\") errInit.partialText = e.partialText;\n if (typeof e.retryable === \"boolean\") errInit.retryable = e.retryable;\n const errTokens = parseRunTokens(e.tokens);\n if (errTokens !== undefined) errInit.tokens = errTokens;\n const errTurns = parseRunTurns(e.turns);\n if (errTurns !== undefined) errInit.turns = errTurns;\n const errModel = parseRunModel(e.model);\n if (errModel !== undefined) errInit.model = errModel;\n throw new MantyxRunError(runId, subtype, e.error, errInit);\n } else if (ev.type === \"cancelled\") {\n throw new MantyxRunError(runId, \"cancelled\", \"Run was cancelled\");\n }\n }\n const result: RunResult = { runId, text: finalText, events: collected };\n if (tokens !== undefined) result.tokens = tokens;\n if (turns !== undefined) result.turns = turns;\n if (modelInfo !== undefined) result.model = modelInfo;\n return result;\n }\n\n async *streamRunEvents(\n runId: string,\n handlers: LocalHandlers,\n signal?: AbortSignal,\n ): AsyncGenerator<RunEvent, void, void> {\n const url = this.absoluteUrl(`/agent-runs/${encodeURIComponent(runId)}/stream`);\n let lastSeq = 0;\n while (true) {\n const reqUrl = lastSeq > 0 ? `${url}?lastSeq=${lastSeq}` : url;\n const res = await this.openSseStream(reqUrl, lastSeq, signal);\n if (!res.ok) {\n throw await this.errorFromResponse(res);\n }\n let terminal = false;\n try {\n for await (const sseEvent of readSseStream(res.body, { ...(signal ? { signal } : {}) })) {\n let data: Record<string, unknown> = {};\n try {\n data = JSON.parse(sseEvent.data || \"{}\") as Record<string, unknown>;\n } catch {\n data = {};\n }\n const evType = sseEvent.event ?? (data.type as string | undefined) ?? \"message\";\n const seq = typeof data.seq === \"number\" ? data.seq : lastSeq;\n if (typeof seq === \"number\" && seq > lastSeq) lastSeq = seq;\n const ev = { seq, type: evType, ...data } as RunEvent;\n yield ev;\n if (evType === \"local_tool_call\") {\n const localEv = ev as LocalToolCallEvent;\n void this.dispatchLocalTool(runId, localEv, handlers).catch((err) => {\n // best-effort logging; the run will surface a `result/error` if the\n // server eventually times out.\n console.error(\"[mantyx-sdk] local tool dispatch failed:\", err);\n });\n }\n if (evType === \"result\" || evType === \"error\" || evType === \"cancelled\") {\n terminal = true;\n return;\n }\n }\n } catch (err) {\n if (signal?.aborted) {\n throw new MantyxRunError(runId, \"cancelled\", \"Run was cancelled by the client\");\n }\n // Network blip — retry after a tiny backoff with `?lastSeq=`.\n await sleep(500);\n continue;\n }\n if (terminal) return;\n // Stream closed without a terminal event (server restart, etc.) — reconnect.\n }\n }\n\n async dispatchLocalTool(\n runId: string,\n ev: LocalToolCallEvent,\n handlers: LocalHandlers,\n ): Promise<void> {\n const kind = ev.kind ?? \"local\";\n try {\n let out: string;\n if (kind === \"a2a_local\") {\n const tool = handlers.a2aTools.get(ev.name);\n if (!tool) {\n await this.postToolResult(runId, ev.toolUseId, {\n error: `No local A2A handler registered for tool ${JSON.stringify(ev.name)}`,\n });\n return;\n }\n const message = typeof ev.args?.message === \"string\" ? (ev.args.message as string) : \"\";\n out = await callA2A(tool, { message }, { fetch: this.options.fetch });\n } else if (kind === \"mcp_local\") {\n const serverName = ev.mcpServer ?? \"\";\n const mcpToolName = ev.mcpToolName ?? \"\";\n const server = handlers.mcpServers.get(serverName);\n if (!server) {\n await this.postToolResult(runId, ev.toolUseId, {\n error: `No local MCP server registered as ${JSON.stringify(serverName)}`,\n });\n return;\n }\n // The wire-prefixed tool name (`<server>_<tool>`) is what the model\n // sees; the upstream MCP server uses the bare name. Strip the prefix\n // before forwarding to `tools/call`.\n const upstreamName = mcpToolName.startsWith(`${serverName}_`)\n ? mcpToolName.slice(serverName.length + 1)\n : mcpToolName;\n out = await callMcpTool(server, upstreamName, ev.args ?? {});\n } else {\n const handler = handlers.localTools.get(ev.name);\n if (!handler) {\n await this.postToolResult(runId, ev.toolUseId, {\n error: `No local handler registered for tool ${JSON.stringify(ev.name)}`,\n });\n return;\n }\n const args = handler.parameters\n ? (handler.parameters.parse?.(ev.args) as Record<string, unknown>) ?? ev.args\n : ev.args;\n const result = await handler.execute(args);\n out = typeof result === \"string\" ? result : JSON.stringify(result);\n }\n await this.postToolResult(runId, ev.toolUseId, { result: out });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const handlerName = describeHandlerName(ev);\n await this.postToolResult(runId, ev.toolUseId, {\n error: new MantyxToolError(handlerName, message).message,\n });\n }\n }\n\n async postToolResult(\n runId: string,\n toolUseId: string,\n payload: { result?: string; error?: string },\n ): Promise<void> {\n await this.request<{ ok: boolean }>({\n method: \"POST\",\n path: `/agent-runs/${encodeURIComponent(runId)}/tool-results`,\n body: { toolUseId, ...payload },\n });\n }\n\n async cancelRun(runId: string): Promise<void> {\n await this.request<{ ok: boolean }>({\n method: \"POST\",\n path: `/agent-runs/${encodeURIComponent(runId)}/cancel`,\n });\n }\n\n // -------------------------------------------------------------- HTTP\n\n private absoluteUrl(path: string): string {\n return `${this.options.baseUrl}/api/v1/workspaces/${encodeURIComponent(this.options.workspaceSlug)}${path}`;\n }\n\n /**\n * Resolve the bearer credential to send on the next request. With a\n * static `apiKey` / `accessToken` this is a synchronous reach into\n * `options.apiKey`; with a {@link TokenSource} it delegates so the\n * source can refresh expired access tokens before we hit the wire.\n *\n * The `reason` is forwarded to the source verbatim. Pass\n * `\"unauthorized\"` immediately after a 401 so the source forces a\n * refresh rather than handing back its (now-invalid) cached value.\n */\n private async resolveBearer(reason: \"initial\" | \"unauthorized\" = \"initial\"): Promise<string> {\n if (this.options.tokenSource) return this.options.tokenSource(reason);\n return this.options.apiKey;\n }\n\n /**\n * Open an SSE stream against `reqUrl` with at-most-one refresh +\n * retry on 401. The caller is responsible for the subsequent\n * `readSseStream` loop; this helper only handles the initial GET.\n * Mid-stream 401s propagate as `MantyxNetworkError` from the read\n * loop and trigger a reconnect via the outer `while` in\n * {@link streamRunEvents}.\n */\n private async openSseStream(\n reqUrl: string,\n lastSeq: number,\n signal: AbortSignal | undefined,\n ): Promise<Response> {\n const openOnce = async (reason: \"initial\" | \"unauthorized\"): Promise<Response> => {\n const auth = await this.authHeaders(reason);\n return this.options.fetch(reqUrl, {\n method: \"GET\",\n headers: {\n ...auth,\n Accept: \"text/event-stream\",\n ...(lastSeq > 0 ? { \"Last-Event-ID\": String(lastSeq) } : {}),\n },\n ...(signal ? { signal } : {}),\n }).catch((err: unknown) => {\n throw new MantyxNetworkError(`Failed to open SSE stream: ${(err as Error).message}`, {\n cause: err,\n });\n });\n };\n const res = await openOnce(\"initial\");\n if (res.status === 401 && this.options.tokenSource !== null) {\n try {\n await res.text();\n } catch {\n // ignore\n }\n return openOnce(\"unauthorized\");\n }\n return res;\n }\n\n private async authHeaders(\n reason: \"initial\" | \"unauthorized\" = \"initial\",\n ): Promise<Record<string, string>> {\n const bearer = await this.resolveBearer(reason);\n return { Authorization: `Bearer ${bearer}` };\n }\n\n async request<T>(args: {\n method: string;\n path: string;\n body?: unknown;\n timeoutMs?: number;\n }): Promise<T> {\n return this.requestWithRetry<T>(args, \"initial\");\n }\n\n private async requestWithRetry<T>(\n args: { method: string; path: string; body?: unknown; timeoutMs?: number },\n reason: \"initial\" | \"unauthorized\",\n ): Promise<T> {\n const url = this.absoluteUrl(args.path);\n const ctrl = new AbortController();\n const t = setTimeout(() => ctrl.abort(), args.timeoutMs ?? this.options.timeoutMs);\n try {\n const auth = await this.authHeaders(reason);\n const res = await this.options.fetch(url, {\n method: args.method,\n headers: {\n ...auth,\n ...(args.body !== undefined ? { \"Content-Type\": \"application/json\" } : {}),\n Accept: \"application/json\",\n },\n ...(args.body !== undefined ? { body: JSON.stringify(args.body) } : {}),\n signal: ctrl.signal,\n }).catch((err: unknown) => {\n if (ctrl.signal.aborted) {\n throw new MantyxNetworkError(`Request timed out after ${args.timeoutMs ?? this.options.timeoutMs}ms`);\n }\n throw new MantyxNetworkError(`Network error: ${(err as Error).message}`, { cause: err });\n });\n if (!res.ok) {\n // 401 with a configured TokenSource: refresh the access token\n // and retry the original request exactly once. Static-credential\n // clients (no source) fall straight through to `MantyxAuthError`.\n if (\n res.status === 401 &&\n this.options.tokenSource !== null &&\n reason === \"initial\"\n ) {\n // Drain the body so the socket can be reused.\n try {\n await res.text();\n } catch {\n // ignore\n }\n clearTimeout(t);\n return this.requestWithRetry<T>(args, \"unauthorized\");\n }\n throw await this.errorFromResponse(res);\n }\n const text = await res.text();\n if (!text) return undefined as unknown as T;\n try {\n return JSON.parse(text) as T;\n } catch (err) {\n throw new MantyxError(`Failed to parse JSON response: ${(err as Error).message}`);\n }\n } finally {\n clearTimeout(t);\n }\n }\n\n private async errorFromResponse(res: Response): Promise<MantyxError> {\n let body: {\n error?: string;\n code?: string;\n hint?: string;\n required?: string | string[];\n } = {};\n try {\n body = (await res.json()) as typeof body;\n } catch {\n // ignore\n }\n if (res.status === 401) {\n return new MantyxAuthError(body.error ?? \"Invalid API key or OAuth access token\");\n }\n // `403 insufficient_scope` is the OAuth \"missing scope\" signal. The\n // server may report `error` or `code` as the discriminator depending\n // on the route; check both. See `docs/agent-runs-protocol.md` §2.3.\n if (res.status === 403 && (body.error === \"insufficient_scope\" || body.code === \"insufficient_scope\")) {\n const required = parseRequiredScopes(body.required, res.headers.get(\"WWW-Authenticate\"));\n const msg = required.length > 0\n ? `Missing OAuth scope${required.length > 1 ? \"s\" : \"\"}: ${required.join(\", \")}`\n : \"OAuth access token is missing a required scope\";\n return new MantyxScopeError(msg, required);\n }\n return new MantyxError(body.error ?? `HTTP ${res.status}`, {\n code: body.code ?? `http_${res.status}`,\n status: res.status,\n ...(body.hint ? { hint: body.hint } : {}),\n });\n }\n}\n\n// ---------------------------------------------------------------- Sessions\n\nexport class AgentSession {\n readonly id: string;\n readonly client: MantyxClient;\n private readonly handlers: LocalHandlers;\n private readonly tools: ToolRef[];\n\n constructor(\n client: MantyxClient,\n id: string,\n handlers: LocalHandlers,\n tools?: ToolRef[],\n ) {\n this.client = client;\n this.id = id;\n this.handlers = handlers;\n this.tools = tools ?? [];\n }\n\n async send(\n prompt: string,\n opts: {\n onAssistantDelta?: (s: string) => void;\n signal?: AbortSignal;\n /**\n * Per-message metadata override. Server-side this is merged on top of\n * the session's metadata at run-creation time (run-level keys win).\n * Useful for tagging individual turns (e.g. `{ \"trace_id\": \"abc\" }`).\n */\n metadata?: Record<string, string>;\n /**\n * Per-message override for `reasoningLevel`. Applies only to this run\n * and does not mutate the session's stored value.\n */\n reasoningLevel?: ReasoningLevel;\n /**\n * Per-message override for `outputSchema`. Applies only to this run\n * and does not mutate the session's stored value.\n */\n outputSchema?: OutputSchema;\n /**\n * Per-message override for `loopDetection`. Applies only to this run\n * and does not mutate the session's stored value. Pass `false` to\n * disable the guard for this single turn.\n */\n loopDetection?: LoopDetection | false;\n /**\n * Per-message override for `toolBudgets`. Applies only to this run\n * and does not mutate the session's stored value.\n */\n toolBudgets?: ToolBudgets;\n /**\n * Per-message override for `supervisor`. Applies only to this run\n * and does not mutate the session's stored value. Pass `false` to\n * disable the platform judge for this single turn.\n */\n supervisor?: Supervisor | false;\n } = {},\n ): Promise<RunResult> {\n const created = await this.client.request<{ runId: string; streamUrl: string }>({\n method: \"POST\",\n path: `/agent-sessions/${encodeURIComponent(this.id)}/messages`,\n body: this.buildSessionMessageBody(prompt, opts),\n });\n return this.client.driveRun(created.runId, this.handlers, {\n ...(opts.onAssistantDelta ? { onAssistantDelta: opts.onAssistantDelta } : {}),\n ...(opts.signal ? { signal: opts.signal } : {}),\n });\n }\n\n async *stream(\n prompt: string,\n opts: {\n signal?: AbortSignal;\n metadata?: Record<string, string>;\n reasoningLevel?: ReasoningLevel;\n outputSchema?: OutputSchema;\n loopDetection?: LoopDetection | false;\n toolBudgets?: ToolBudgets;\n supervisor?: Supervisor | false;\n } = {},\n ): AsyncGenerator<RunEvent, void, void> {\n const created = await this.client.request<{ runId: string; streamUrl: string }>({\n method: \"POST\",\n path: `/agent-sessions/${encodeURIComponent(this.id)}/messages`,\n body: this.buildSessionMessageBody(prompt, opts),\n });\n yield* this.client.streamRunEvents(created.runId, this.handlers, opts.signal);\n }\n\n private buildSessionMessageBody(\n prompt: string,\n opts: {\n metadata?: Record<string, string>;\n reasoningLevel?: ReasoningLevel;\n outputSchema?: OutputSchema;\n loopDetection?: LoopDetection | false;\n toolBudgets?: ToolBudgets;\n supervisor?: Supervisor | false;\n },\n ): Record<string, unknown> {\n const body: Record<string, unknown> = { prompt };\n if (this.tools.length > 0) body.tools = serializeToolRefs(this.tools);\n if (opts.metadata && Object.keys(opts.metadata).length > 0) body.metadata = opts.metadata;\n if (opts.reasoningLevel !== undefined) {\n body.reasoningLevel = normalizeReasoningLevel(opts.reasoningLevel);\n }\n if (opts.outputSchema !== undefined) {\n body.outputSchema = normalizeOutputSchema(opts.outputSchema);\n }\n if (opts.loopDetection !== undefined) {\n body.loopDetection = normalizeLoopDetection(opts.loopDetection);\n }\n if (opts.toolBudgets !== undefined) {\n body.toolBudgets = normalizeToolBudgets(opts.toolBudgets);\n }\n if (opts.supervisor !== undefined) {\n body.supervisor = normalizeSupervisor(opts.supervisor);\n }\n return body;\n }\n\n async history(): Promise<Array<{ role: \"user\" | \"assistant\" | \"system\"; content: string }>> {\n const info = await this.client.getSessionInfo(this.id);\n return info.messages;\n }\n\n async info(): Promise<SessionInfo> {\n return this.client.getSessionInfo(this.id);\n }\n\n async end(): Promise<void> {\n try {\n await this.client.endSession(this.id);\n } finally {\n // Close any MCP transports the session opened.\n await closeMcpRefs(this.tools);\n }\n }\n}\n\n// ---------------------------------------------------------------- Helpers\n\nfunction serializeAgentSpec(\n spec: AgentSpecBase,\n extra: { prompt?: string; messages?: Array<{ role: string; content: string }> } = {},\n): Record<string, unknown> {\n if (!spec.agentId && (typeof spec.systemPrompt !== \"string\" || spec.systemPrompt.length === 0)) {\n throw new MantyxError(\"Either `agentId` or `systemPrompt` is required\");\n }\n const body: Record<string, unknown> = {\n tools: serializeToolRefs(spec.tools ?? []),\n };\n if (typeof spec.systemPrompt === \"string\") body.systemPrompt = spec.systemPrompt;\n if (spec.agentId) body.agentId = spec.agentId;\n if (spec.name) body.name = spec.name;\n if (spec.modelId) body.modelId = spec.modelId;\n if (spec.reasoningLevel !== undefined) {\n body.reasoningLevel = normalizeReasoningLevel(spec.reasoningLevel);\n }\n if (spec.outputSchema !== undefined) {\n body.outputSchema = normalizeOutputSchema(spec.outputSchema);\n }\n if (spec.loopDetection !== undefined) {\n body.loopDetection = normalizeLoopDetection(spec.loopDetection);\n }\n if (spec.toolBudgets !== undefined) {\n body.toolBudgets = normalizeToolBudgets(spec.toolBudgets);\n }\n if (spec.supervisor !== undefined) {\n body.supervisor = normalizeSupervisor(spec.supervisor);\n }\n if (spec.budgets) body.budgets = spec.budgets;\n if (spec.metadata && Object.keys(spec.metadata).length > 0) body.metadata = spec.metadata;\n if (extra.prompt !== undefined) body.prompt = extra.prompt;\n if (extra.messages !== undefined) body.messages = extra.messages;\n return body;\n}\n\nfunction serializeToolRefs(tools: ToolRef[]): unknown[] {\n return tools.map((t) => {\n switch (t.kind) {\n case \"mantyx\":\n return { kind: \"mantyx\", id: t.id };\n case \"mantyx_plugin\":\n return { kind: \"mantyx_plugin\", name: t.name };\n case \"local\":\n return {\n kind: \"local\",\n name: t.name,\n description: t.description,\n parameters: toToolParametersWire(t.parameters),\n ...(t.outputSchema !== undefined\n ? { outputSchema: toToolParametersWire(t.outputSchema) }\n : {}),\n ...(t.longRunning ? { longRunning: true } : {}),\n };\n case \"a2a\":\n return {\n kind: \"a2a\",\n name: t.name,\n ...(t.description !== undefined ? { description: t.description } : {}),\n agentCardUrl: t.agentCardUrl,\n ...(t.headers ? { headers: { ...t.headers } } : {}),\n ...(t.contextId ? { contextId: t.contextId } : {}),\n };\n case \"a2a_local\": {\n const card = t._resolvedCard;\n if (!card) {\n throw new MantyxError(\n `defineLocalA2A(${JSON.stringify(t.name)}): agent card has not been resolved yet (was \\`runAgent\\` / \\`createSession\\` skipped?)`,\n );\n }\n return {\n kind: \"a2a_local\",\n name: t.name,\n // The wire ships the resolved A2A Agent Card. Shallow-clone so\n // consumers can mutate the input later without affecting the\n // wire payload.\n agentCard: { ...card },\n };\n }\n case \"mcp\":\n return {\n kind: \"mcp\",\n name: t.name,\n url: t.url,\n ...(t.headers ? { headers: { ...t.headers } } : {}),\n ...(t.toolFilter ? { toolFilter: [...t.toolFilter] } : {}),\n };\n case \"mcp_local\": {\n const resolved = t._resolved;\n if (!resolved) {\n throw new MantyxError(\n `defineLocalMcp(${JSON.stringify(t.name)}): MCP server has not been initialised yet`,\n );\n }\n // The SDK owns naming for `mcp_local` (MANTYX does no prefixing).\n // We auto-prefix each upstream tool name with the server label so\n // the model-facing surface is `<server>_<tool>` — mirroring how\n // MANTYX prefixes for `kind: \"mcp\"`.\n const tools = resolved.tools.map((tool) => {\n const wire: Record<string, unknown> = {\n name: prefixedMcpToolName(t.name, tool.name),\n inputSchema: tool.inputSchema,\n };\n if (typeof tool.description === \"string\") wire.description = tool.description;\n if (tool.annotations) wire.annotations = tool.annotations;\n return wire;\n });\n return {\n kind: \"mcp_local\",\n name: t.name,\n serverInfo: { ...resolved.serverInfo },\n tools,\n };\n }\n }\n });\n}\n\n/** Internal registry of client-resolved handlers, indexed by `kind`. */\nexport interface LocalHandlers {\n /** `kind: \"local\"` — generic local tools, indexed by tool name. */\n localTools: Map<string, LocalTool>;\n /** `kind: \"a2a_local\"` — local A2A peers, indexed by tool name. */\n a2aTools: Map<string, LocalA2ATool>;\n /** `kind: \"mcp_local\"` — local MCP servers, indexed by server name. */\n mcpServers: Map<string, LocalMcpServer>;\n}\n\nfunction collectLocalHandlers(tools: ReadonlyArray<ToolRef>): LocalHandlers {\n const localTools = new Map<string, LocalTool>();\n const a2aTools = new Map<string, LocalA2ATool>();\n const mcpServers = new Map<string, LocalMcpServer>();\n for (const t of tools) {\n if (isLocalTool(t)) {\n localTools.set(t.name, t);\n } else if (isLocalA2ATool(t)) {\n a2aTools.set(t.name, t);\n } else if (isLocalMcpServer(t)) {\n mcpServers.set(t.name, t);\n }\n }\n return { localTools, a2aTools, mcpServers };\n}\n\nfunction describeHandlerName(ev: LocalToolCallEvent): string {\n if (ev.kind === \"mcp_local\" && ev.mcpServer && ev.mcpToolName) {\n return `${ev.mcpServer}/${ev.mcpToolName}`;\n }\n return ev.name;\n}\n\nfunction normalizeReasoningLevel(level: ReasoningLevel): string | number {\n if (typeof level === \"number\") {\n if (!Number.isFinite(level) || level < 0 || level > 100) {\n throw new MantyxError(\n `reasoningLevel must be a string anchor or an integer in 0..100, got ${level}`,\n );\n }\n return Math.trunc(level);\n }\n if (level === \"off\" || level === \"low\" || level === \"medium\" || level === \"high\") {\n return level;\n }\n throw new MantyxError(\n `reasoningLevel must be one of \"off\" | \"low\" | \"medium\" | \"high\" or a number 0..100, got ${JSON.stringify(level)}`,\n );\n}\n\nconst OUTPUT_SCHEMA_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;\nconst OUTPUT_SCHEMA_MAX_BYTES = 32 * 1024;\n\n/**\n * Validate an `OutputSchema` value and return the wire-shaped object.\n *\n * Mirrors the server-side `400 invalid_request` checks (name regex, schema\n * shape, ≤ 32 KB serialized) so callers get an early local error instead of\n * a round-trip rejection.\n */\nfunction normalizeOutputSchema(value: OutputSchema): Record<string, unknown> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new MantyxError(\n `outputSchema must be an object of shape { name?, schema }, got ${JSON.stringify(value)}`,\n );\n }\n const out: Record<string, unknown> = {};\n if (value.name !== undefined) {\n if (typeof value.name !== \"string\" || !OUTPUT_SCHEMA_NAME_RE.test(value.name)) {\n throw new MantyxError(\n `outputSchema.name must match /^[a-zA-Z0-9_-]{1,64}$/, got ${JSON.stringify(value.name)}`,\n );\n }\n out.name = value.name;\n }\n const schema = value.schema;\n if (!schema || typeof schema !== \"object\" || Array.isArray(schema)) {\n throw new MantyxError(\n `outputSchema.schema must be a non-null JSON object (the JSON Schema root)`,\n );\n }\n out.schema = schema;\n let serialized: string;\n try {\n serialized = JSON.stringify(out);\n } catch (err) {\n throw new MantyxError(\n `outputSchema is not JSON-serialisable: ${(err as Error).message ?? String(err)}`,\n );\n }\n if (serialized.length > OUTPUT_SCHEMA_MAX_BYTES) {\n throw new MantyxError(\n `outputSchema serialised JSON is ${serialized.length} bytes; the server enforces a 32 KB limit`,\n );\n }\n return out;\n}\n\nconst LOOP_DETECTION_THRESHOLD_MAX = 100;\n\n/**\n * Validate a {@link LoopDetection} (or `false`) value and return the\n * wire-shaped value. Mirrors the server-side `400 invalid_request` checks\n * (thresholds in range, hard cutoff strictly greater than consecutive) so\n * callers see an early local error.\n */\nfunction normalizeLoopDetection(\n value: LoopDetection | false,\n): false | Record<string, unknown> {\n if (value === false) return false;\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new MantyxError(\n `loopDetection must be an object or the literal \\`false\\`, got ${JSON.stringify(value)}`,\n );\n }\n const out: Record<string, unknown> = {};\n if (value.consecutiveThreshold !== undefined) {\n out.consecutiveThreshold = assertThreshold(\n \"loopDetection.consecutiveThreshold\",\n value.consecutiveThreshold,\n 2,\n );\n }\n if (value.hardCutoffThreshold !== undefined) {\n out.hardCutoffThreshold = assertThreshold(\n \"loopDetection.hardCutoffThreshold\",\n value.hardCutoffThreshold,\n 3,\n );\n }\n if (\n typeof out.consecutiveThreshold === \"number\" &&\n typeof out.hardCutoffThreshold === \"number\" &&\n out.hardCutoffThreshold <= out.consecutiveThreshold\n ) {\n throw new MantyxError(\n `loopDetection.hardCutoffThreshold (${out.hardCutoffThreshold}) must be strictly greater than loopDetection.consecutiveThreshold (${out.consecutiveThreshold})`,\n );\n }\n return out;\n}\n\nfunction assertThreshold(label: string, value: number, min: number): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || !Number.isInteger(value)) {\n throw new MantyxError(`${label} must be an integer, got ${JSON.stringify(value)}`);\n }\n if (value < min) {\n throw new MantyxError(`${label} must be >= ${min}, got ${value}`);\n }\n if (value > LOOP_DETECTION_THRESHOLD_MAX) {\n throw new MantyxError(\n `${label} must be <= ${LOOP_DETECTION_THRESHOLD_MAX} (server-enforced), got ${value}`,\n );\n }\n return value;\n}\n\nconst TOOL_BUDGETS_MAX_ENTRIES = 32;\nconst TOOL_BUDGET_MAX_NAME_LEN = 120;\nconst TOOL_BUDGET_MAX_CALLS = 1000;\nconst SUPERVISOR_INTERVAL_MAX = 100;\n\n/**\n * Validate a {@link Supervisor} (or `false`) value and return the wire-shaped\n * value. Mirrors the server-side `400 invalid_request` checks (`interval` ≥\n * 1 and ≤ 100) so callers see an early local error.\n */\nfunction normalizeSupervisor(value: Supervisor | false): false | Record<string, unknown> {\n if (value === false) return false;\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new MantyxError(\n `supervisor must be an object or the literal \\`false\\`, got ${JSON.stringify(value)}`,\n );\n }\n const out: Record<string, unknown> = {};\n if (value.interval !== undefined) {\n out.interval = assertThreshold(\"supervisor.interval\", value.interval, 1);\n }\n return out;\n}\n\n/**\n * Validate a {@link ToolBudgets} value and return the wire-shaped object.\n * Mirrors the server-side `400 invalid_request` checks (max 32 entries,\n * key length 1..120, `maxCalls` ≥ 0 and ≤ 1000) so callers see an early\n * local error. An empty object is valid and signals \"clear the runtime\n * defaults\"; pass `undefined` to keep them.\n */\nfunction normalizeToolBudgets(value: ToolBudgets): Record<string, { maxCalls: number }> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new MantyxError(\n `toolBudgets must be an object of shape { [name]: { maxCalls } }, got ${JSON.stringify(value)}`,\n );\n }\n const keys = Object.keys(value);\n if (keys.length > TOOL_BUDGETS_MAX_ENTRIES) {\n throw new MantyxError(\n `toolBudgets has ${keys.length} entries; the server enforces a ${TOOL_BUDGETS_MAX_ENTRIES}-entry limit`,\n );\n }\n const out: Record<string, { maxCalls: number }> = {};\n for (const key of keys) {\n if (typeof key !== \"string\" || key.length < 1 || key.length > TOOL_BUDGET_MAX_NAME_LEN) {\n throw new MantyxError(\n `toolBudgets keys must be 1..${TOOL_BUDGET_MAX_NAME_LEN}-char strings, got ${JSON.stringify(key)}`,\n );\n }\n const entry = value[key];\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new MantyxError(\n `toolBudgets[${JSON.stringify(key)}] must be an object { maxCalls }, got ${JSON.stringify(entry)}`,\n );\n }\n const maxCalls = entry.maxCalls;\n if (\n typeof maxCalls !== \"number\" ||\n !Number.isFinite(maxCalls) ||\n !Number.isInteger(maxCalls) ||\n maxCalls < 0\n ) {\n throw new MantyxError(\n `toolBudgets[${JSON.stringify(key)}].maxCalls must be a non-negative integer, got ${JSON.stringify(maxCalls)}`,\n );\n }\n if (maxCalls > TOOL_BUDGET_MAX_CALLS) {\n throw new MantyxError(\n `toolBudgets[${JSON.stringify(key)}].maxCalls must be <= ${TOOL_BUDGET_MAX_CALLS} (server-enforced), got ${maxCalls}`,\n );\n }\n out[key] = { maxCalls };\n }\n return out;\n}\n\n/**\n * Parse the terminal text of a `RunResult` as JSON.\n *\n * When the run was submitted with `outputSchema`, MANTYX (via the LLM\n * provider) guarantees the reply parses as JSON in the *vast* majority of\n * cases. Transient model errors (refusal text, truncation under\n * `max_tokens` pressure, exotic Unicode) can still produce strings that\n * fail to `JSON.parse` in rare edge cases — this helper centralises that\n * brittle step and surfaces a typed {@link MantyxParseError} on failure\n * with the original text preserved on `err.text`.\n *\n * Pass an optional `validator` (zod's `.parse`, an Ajv compiled validator,\n * or any function) to re-validate against your source-of-truth schema. The\n * validator's return value (or thrown error) is forwarded to the caller.\n *\n * @example\n * ```ts\n * import { z } from \"zod\";\n * import { parseRunOutput } from \"@mantyx/sdk\";\n *\n * const Schema = z.object({ city: z.string(), temperature_c: z.number() });\n * const result = await client.runAgent({\n * systemPrompt: \"...\",\n * prompt: \"What's the weather in SF?\",\n * outputSchema: { name: \"weather_report\", schema: weatherJsonSchema },\n * });\n * const report = parseRunOutput(result, Schema.parse.bind(Schema));\n * // ^? { city: string; temperature_c: number }\n * ```\n */\nexport function parseRunOutput<T = unknown>(\n result: RunResult,\n validator?: (value: unknown) => T,\n): T {\n let parsed: unknown;\n try {\n parsed = JSON.parse(result.text);\n } catch (err) {\n throw new MantyxParseError(\n `Run ${result.runId} returned non-JSON text; cannot satisfy outputSchema`,\n result.text,\n { cause: err },\n );\n }\n if (validator) {\n try {\n return validator(parsed);\n } catch (err) {\n throw new MantyxParseError(\n `Run ${result.runId} output failed validation: ${(err as Error).message ?? String(err)}`,\n result.text,\n { cause: err },\n );\n }\n }\n return parsed as T;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\n/**\n * Defensively coerce a wire `tokens` object into {@link RunTokenUsage}.\n *\n * Returns `undefined` when the input is not a JSON object — that keeps\n * the \"no usage data\" sentinel intact against legacy MANTYX servers\n * that omit the field entirely. Unknown / missing buckets default to\n * `0` (the protocol contract is that misbehaving engines clamp to\n * non-negative integers; the SDK mirrors that here so dashboards never\n * see `NaN`).\n */\nfunction parseRunTokens(value: unknown): RunTokenUsage | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const v = value as Record<string, unknown>;\n return {\n inputTokens: toNonNegativeInt(v.inputTokens),\n cachedTokens: toNonNegativeInt(v.cachedTokens),\n reasoningTokens: toNonNegativeInt(v.reasoningTokens),\n outputTokens: toNonNegativeInt(v.outputTokens),\n };\n}\n\n/**\n * Defensively coerce a wire `turns` value into an integer. Returns\n * `undefined` when missing / unparseable — keeps the \"no usage data\"\n * sentinel against legacy servers.\n */\nfunction parseRunTurns(value: unknown): number | undefined {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return undefined;\n return Math.max(0, Math.trunc(value));\n}\n\n/**\n * Defensively coerce a wire `model` object into {@link RunModelInfo}.\n *\n * Returns `undefined` when the input is not a JSON object — the\n * \"no usage data\" sentinel for legacy servers. `reasoningEffort` is\n * carried through only when the wire surfaced it (the field is\n * optional on the protocol side).\n */\nfunction parseRunModel(value: unknown): RunModelInfo | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const v = value as Record<string, unknown>;\n const out: RunModelInfo = {\n id: typeof v.id === \"string\" ? v.id : \"\",\n provider: typeof v.provider === \"string\" ? v.provider : \"\",\n vendorModelId: typeof v.vendorModelId === \"string\" ? v.vendorModelId : \"\",\n };\n if (typeof v.reasoningEffort === \"string\" && v.reasoningEffort.length > 0) {\n out.reasoningEffort = v.reasoningEffort;\n }\n return out;\n}\n\nfunction toNonNegativeInt(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return 0;\n return Math.max(0, Math.trunc(value));\n}\n\n/**\n * Pick exactly one of `apiKey` / `accessToken` / `tokenSource` from\n * {@link MantyxClientOptions} and return the resolved bearer credential\n * (plus the optional dynamic source).\n *\n * `apiKey` and `accessToken` are both static workspace bearers — the\n * server resolves whichever credential it sees by token-prefix, so the\n * SDK can use a single header path. `tokenSource` is the dynamic\n * alternative that the HTTP layer calls before every request and on\n * 401 retries; it is mutually exclusive with the static options\n * because mixing them would obscure where the credential actually\n * came from.\n */\nfunction resolveCredential(opts: MantyxClientOptions): {\n credential: string;\n tokenSource: TokenSource | null;\n} {\n const apiKey = typeof opts.apiKey === \"string\" ? opts.apiKey : \"\";\n const accessToken = typeof opts.accessToken === \"string\" ? opts.accessToken : \"\";\n const tokenSource = typeof opts.tokenSource === \"function\" ? opts.tokenSource : null;\n const provided = [apiKey ? \"apiKey\" : \"\", accessToken ? \"accessToken\" : \"\", tokenSource ? \"tokenSource\" : \"\"]\n .filter((s) => s.length > 0);\n if (provided.length > 1) {\n throw new MantyxError(\n `Pass exactly one of \\`apiKey\\`, \\`accessToken\\`, or \\`tokenSource\\` — got ${provided.join(\" + \")}.`,\n );\n }\n if (provided.length === 0) {\n throw new MantyxError(\n \"One of `apiKey` (workspace API key), `accessToken` (OAuth access token), or `tokenSource` (dynamic credential provider) is required\",\n );\n }\n return {\n credential: apiKey || accessToken,\n tokenSource,\n };\n}\n\n/**\n * Extract the list of scopes the server reported as required for the\n * route, from either the response body's `required` field or the\n * `WWW-Authenticate: Bearer error=\"insufficient_scope\", scope=\"…\"` header.\n *\n * The body field can be a single string (most routes) or an array\n * (multi-scope routes). The header carries a space-delimited scope\n * string per RFC 6750. We prefer the body since it's stricter, and\n * fall back to the header so we surface *something* even when the\n * route only returned the header.\n */\nfunction parseRequiredScopes(\n bodyRequired: string | string[] | undefined,\n wwwAuthenticate: string | null,\n): string[] {\n if (Array.isArray(bodyRequired)) {\n return bodyRequired.filter((s): s is string => typeof s === \"string\" && s.length > 0);\n }\n if (typeof bodyRequired === \"string\" && bodyRequired.length > 0) {\n return [bodyRequired];\n }\n if (typeof wwwAuthenticate === \"string\") {\n const m = /scope=\"([^\"]+)\"/i.exec(wwwAuthenticate);\n if (m && m[1]) {\n return m[1].split(/\\s+/).filter((s) => s.length > 0);\n }\n }\n return [];\n}\n"],"mappings":"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","names":[]}
|
package/dist/a2a-server.d.cts
CHANGED
|
@@ -2,7 +2,7 @@ import { AgentCard } from '@a2a-js/sdk';
|
|
|
2
2
|
export { AgentCard, Message, MessageSendParams, Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from '@a2a-js/sdk';
|
|
3
3
|
import { AgentExecutor, RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
|
|
4
4
|
export { AgentExecutor, ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
|
|
5
|
-
import { M as MantyxClient, T as ToolRef, R as ReasoningLevel } from './client-
|
|
5
|
+
import { M as MantyxClient, T as ToolRef, R as ReasoningLevel } from './client-LQlx7iYY.cjs';
|
|
6
6
|
import 'zod';
|
|
7
7
|
|
|
8
8
|
/**
|
package/dist/a2a-server.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { AgentCard } from '@a2a-js/sdk';
|
|
|
2
2
|
export { AgentCard, Message, MessageSendParams, Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from '@a2a-js/sdk';
|
|
3
3
|
import { AgentExecutor, RequestContext, ExecutionEventBus } from '@a2a-js/sdk/server';
|
|
4
4
|
export { AgentExecutor, ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server';
|
|
5
|
-
import { M as MantyxClient, T as ToolRef, R as ReasoningLevel } from './client-
|
|
5
|
+
import { M as MantyxClient, T as ToolRef, R as ReasoningLevel } from './client-LQlx7iYY.js';
|
|
6
6
|
import 'zod';
|
|
7
7
|
|
|
8
8
|
/**
|