@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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as A2AToolRef, a as AgentSession, b as AgentSpecBase, c as AssistantDeltaEvent, d as AssistantMessageEvent, C as CancelledEvent, D as DEFAULT_BASE_URL, e as DEFAULT_OAUTH_BASE_URL, f as DEFAULT_REFRESH_SKEW_MS, g as DefineLocalA2AOptions, h as DefineLocalMcpOptions, i as DefineLocalToolOptions, E as ErrorEvent, L as LocalA2ATool, j as LocalHandlers, k as LocalMcpHttpTransport, l as LocalMcpServer, m as LocalMcpStdioTransport, n as LocalTool, o as LocalToolCallEvent, p as LocalToolResultInEvent, q as LoopDetectedEvent, r as LoopDetection, s as MantyxA2AOptions, t as MantyxAuthError, M as MantyxClient, u as MantyxClientOptions, v as MantyxError, w as MantyxMcpOptions, x as MantyxNetworkError, y as MantyxOAuthClient, z as MantyxOAuthClientOptions, B as MantyxOAuthError, F as MantyxParseError, G as MantyxPluginToolRef, H as MantyxRunError, I as MantyxRunErrorInit, J as MantyxRunErrorModel, K as MantyxRunErrorTokens, N as MantyxScopeError, O as MantyxToolError, P as MantyxToolRef, Q as McpToolRef, S as ModelCatalog, U as ModelInfo, V as OAuthToken, W as OutputSchema, R as ReasoningLevel, X as RefreshOptions, Y as RefreshTokenSourceOptions, Z as ResultEvent, _ as RevokeOptions, $ as RunEvent, a0 as RunEventBase, a1 as RunModelInfo, a2 as RunResult, a3 as RunSpec, a4 as RunTokenUsage, a5 as ServerToolResultEvent, a6 as SessionInfo, a7 as SessionSpec, a8 as ThinkingDeltaEvent, a9 as TokenRequestReason, aa as TokenSource, ab as ToolBudget, ac as ToolBudgetExceededEvent, ad as ToolBudgets, T as ToolRef, ae as ZodLikeObject, af as defineLocalA2A, ag as defineLocalMcp, ah as defineLocalTool, ai as isLocalA2ATool, aj as isLocalMcpServer, ak as isLocalTool, al as mantyxA2A, am as mantyxMcp, an as mantyxPluginTool, ao as mantyxTool, ap as parseRunOutput } from './client-Byb0Zdo7.cjs';
1
+ export { A as A2AToolRef, a as AgentSession, b as AgentSpecBase, c as AssistantDeltaEvent, d as AssistantMessageEvent, C as CancelledEvent, D as DEFAULT_BASE_URL, e as DEFAULT_OAUTH_BASE_URL, f as DEFAULT_REFRESH_SKEW_MS, g as DefineLocalA2AOptions, h as DefineLocalMcpOptions, i as DefineLocalToolOptions, E as ErrorEvent, L as LocalA2ATool, j as LocalHandlers, k as LocalMcpHttpTransport, l as LocalMcpServer, m as LocalMcpStdioTransport, n as LocalTool, o as LocalToolCallEvent, p as LocalToolResultInEvent, q as LoopDetectedEvent, r as LoopDetection, s as MantyxA2AOptions, t as MantyxAuthError, M as MantyxClient, u as MantyxClientOptions, v as MantyxError, w as MantyxMcpOptions, x as MantyxNetworkError, y as MantyxOAuthClient, z as MantyxOAuthClientOptions, B as MantyxOAuthError, F as MantyxParseError, G as MantyxPluginToolRef, H as MantyxRunError, I as MantyxRunErrorInit, J as MantyxRunErrorModel, K as MantyxRunErrorTokens, N as MantyxScopeError, O as MantyxToolError, P as MantyxToolRef, Q as McpToolRef, S as ModelCatalog, U as ModelInfo, V as OAuthToken, W as OutputSchema, R as ReasoningLevel, X as RefreshOptions, Y as RefreshTokenSourceOptions, Z as ResultEvent, _ as RevokeOptions, $ as RunEvent, a0 as RunEventBase, a1 as RunModelInfo, a2 as RunResult, a3 as RunSpec, a4 as RunTokenUsage, a5 as ServerToolResultEvent, a6 as SessionInfo, a7 as SessionSpec, a8 as Supervisor, a9 as SupervisorAction, aa as SupervisorEvent, ab as ThinkingDeltaEvent, ac as TokenRequestReason, ad as TokenSource, ae as ToolBudget, af as ToolBudgetExceededEvent, ag as ToolBudgets, T as ToolRef, ah as ZodLikeObject, ai as defineLocalA2A, aj as defineLocalMcp, ak as defineLocalTool, al as isLocalA2ATool, am as isLocalMcpServer, an as isLocalTool, ao as mantyxA2A, ap as mantyxMcp, aq as mantyxPluginTool, ar as mantyxTool, as as parseRunOutput } from './client-LQlx7iYY.cjs';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -52,6 +52,6 @@ declare function readSseStream(body: ReadableStream<Uint8Array> | null, opts?: S
52
52
  /**
53
53
  * Release version — synced from repo root VERSION (`npm run sync-version`).
54
54
  */
55
- declare const SDK_VERSION = "0.11.0";
55
+ declare const SDK_VERSION = "0.12.0";
56
56
 
57
57
  export { SDK_VERSION, type SseEvent, type SseStreamOptions, readSseStream, toToolParametersWire, zodToJsonSchema };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as A2AToolRef, a as AgentSession, b as AgentSpecBase, c as AssistantDeltaEvent, d as AssistantMessageEvent, C as CancelledEvent, D as DEFAULT_BASE_URL, e as DEFAULT_OAUTH_BASE_URL, f as DEFAULT_REFRESH_SKEW_MS, g as DefineLocalA2AOptions, h as DefineLocalMcpOptions, i as DefineLocalToolOptions, E as ErrorEvent, L as LocalA2ATool, j as LocalHandlers, k as LocalMcpHttpTransport, l as LocalMcpServer, m as LocalMcpStdioTransport, n as LocalTool, o as LocalToolCallEvent, p as LocalToolResultInEvent, q as LoopDetectedEvent, r as LoopDetection, s as MantyxA2AOptions, t as MantyxAuthError, M as MantyxClient, u as MantyxClientOptions, v as MantyxError, w as MantyxMcpOptions, x as MantyxNetworkError, y as MantyxOAuthClient, z as MantyxOAuthClientOptions, B as MantyxOAuthError, F as MantyxParseError, G as MantyxPluginToolRef, H as MantyxRunError, I as MantyxRunErrorInit, J as MantyxRunErrorModel, K as MantyxRunErrorTokens, N as MantyxScopeError, O as MantyxToolError, P as MantyxToolRef, Q as McpToolRef, S as ModelCatalog, U as ModelInfo, V as OAuthToken, W as OutputSchema, R as ReasoningLevel, X as RefreshOptions, Y as RefreshTokenSourceOptions, Z as ResultEvent, _ as RevokeOptions, $ as RunEvent, a0 as RunEventBase, a1 as RunModelInfo, a2 as RunResult, a3 as RunSpec, a4 as RunTokenUsage, a5 as ServerToolResultEvent, a6 as SessionInfo, a7 as SessionSpec, a8 as ThinkingDeltaEvent, a9 as TokenRequestReason, aa as TokenSource, ab as ToolBudget, ac as ToolBudgetExceededEvent, ad as ToolBudgets, T as ToolRef, ae as ZodLikeObject, af as defineLocalA2A, ag as defineLocalMcp, ah as defineLocalTool, ai as isLocalA2ATool, aj as isLocalMcpServer, ak as isLocalTool, al as mantyxA2A, am as mantyxMcp, an as mantyxPluginTool, ao as mantyxTool, ap as parseRunOutput } from './client-Byb0Zdo7.js';
1
+ export { A as A2AToolRef, a as AgentSession, b as AgentSpecBase, c as AssistantDeltaEvent, d as AssistantMessageEvent, C as CancelledEvent, D as DEFAULT_BASE_URL, e as DEFAULT_OAUTH_BASE_URL, f as DEFAULT_REFRESH_SKEW_MS, g as DefineLocalA2AOptions, h as DefineLocalMcpOptions, i as DefineLocalToolOptions, E as ErrorEvent, L as LocalA2ATool, j as LocalHandlers, k as LocalMcpHttpTransport, l as LocalMcpServer, m as LocalMcpStdioTransport, n as LocalTool, o as LocalToolCallEvent, p as LocalToolResultInEvent, q as LoopDetectedEvent, r as LoopDetection, s as MantyxA2AOptions, t as MantyxAuthError, M as MantyxClient, u as MantyxClientOptions, v as MantyxError, w as MantyxMcpOptions, x as MantyxNetworkError, y as MantyxOAuthClient, z as MantyxOAuthClientOptions, B as MantyxOAuthError, F as MantyxParseError, G as MantyxPluginToolRef, H as MantyxRunError, I as MantyxRunErrorInit, J as MantyxRunErrorModel, K as MantyxRunErrorTokens, N as MantyxScopeError, O as MantyxToolError, P as MantyxToolRef, Q as McpToolRef, S as ModelCatalog, U as ModelInfo, V as OAuthToken, W as OutputSchema, R as ReasoningLevel, X as RefreshOptions, Y as RefreshTokenSourceOptions, Z as ResultEvent, _ as RevokeOptions, $ as RunEvent, a0 as RunEventBase, a1 as RunModelInfo, a2 as RunResult, a3 as RunSpec, a4 as RunTokenUsage, a5 as ServerToolResultEvent, a6 as SessionInfo, a7 as SessionSpec, a8 as Supervisor, a9 as SupervisorAction, aa as SupervisorEvent, ab as ThinkingDeltaEvent, ac as TokenRequestReason, ad as TokenSource, ae as ToolBudget, af as ToolBudgetExceededEvent, ag as ToolBudgets, T as ToolRef, ah as ZodLikeObject, ai as defineLocalA2A, aj as defineLocalMcp, ak as defineLocalTool, al as isLocalA2ATool, am as isLocalMcpServer, an as isLocalTool, ao as mantyxA2A, ap as mantyxMcp, aq as mantyxPluginTool, ar as mantyxTool, as as parseRunOutput } from './client-LQlx7iYY.js';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -52,6 +52,6 @@ declare function readSseStream(body: ReadableStream<Uint8Array> | null, opts?: S
52
52
  /**
53
53
  * Release version — synced from repo root VERSION (`npm run sync-version`).
54
54
  */
55
- declare const SDK_VERSION = "0.11.0";
55
+ declare const SDK_VERSION = "0.12.0";
56
56
 
57
57
  export { SDK_VERSION, type SseEvent, type SseStreamOptions, readSseStream, toToolParametersWire, zodToJsonSchema };
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  readSseStream,
24
24
  toToolParametersWire,
25
25
  zodToJsonSchema
26
- } from "./chunk-DR625E6B.js";
26
+ } from "./chunk-2K4BGJGJ.js";
27
27
 
28
28
  // src/oauth.ts
29
29
  var DEFAULT_OAUTH_BASE_URL = "https://app.mantyx.io";
@@ -245,7 +245,7 @@ function normalizeScope(scope) {
245
245
  }
246
246
 
247
247
  // src/version.ts
248
- var SDK_VERSION = "0.11.0";
248
+ var SDK_VERSION = "0.12.0";
249
249
  export {
250
250
  AgentSession,
251
251
  DEFAULT_BASE_URL,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/oauth.ts","../src/version.ts"],"sourcesContent":["/**\n * MANTYX OAuth 2.0 refresh client: trade a stored refresh token for\n * short-lived access tokens, revoke tokens at sign-out, and expose\n * a {@link TokenSource} the {@link MantyxClient} HTTP layer calls\n * before every request (and again on 401).\n *\n * The library is intentionally **refresh-only**. It assumes the caller\n * already obtained the refresh token through their own sign-in flow\n * (Authorization Code + PKCE in a browser, native redirect, server-\n * side exchange — whatever fits the host application). The SDK does\n * not drive consent, does not initiate auth-code exchanges, and does\n * not bundle PKCE helpers.\n *\n * Wire contract (`docs/oauth.md`):\n *\n * - Token endpoint: `POST <baseUrl>/api/oauth/token`, form-encoded,\n * `grant_type=refresh_token`. Echoes back the same `refresh_token`\n * the client sent (refresh tokens are persistent and non-rotating).\n * - Revoke endpoint: `POST <baseUrl>/api/oauth/revoke`, form-encoded.\n * - Access tokens (`mantyx_at_…`) live 1 hour (`expires_in: 3600`).\n * - Refresh tokens (`mantyx_rt_…`) are long-lived; the caller persists\n * them once at first sign-in (encrypted at rest) and the SDK re-mints\n * access tokens from the same value on demand.\n */\n\nimport { MantyxError, MantyxNetworkError } from \"./errors.js\";\n\nexport const DEFAULT_OAUTH_BASE_URL = \"https://app.mantyx.io\";\n\n/** Skew (ms) before `expiresAt` at which a TokenSource will pre-emptively refresh. Default 60s. */\nexport const DEFAULT_REFRESH_SKEW_MS = 60_000;\n\n/**\n * Raised on a non-2xx response from `POST /api/oauth/token` or\n * `POST /api/oauth/revoke`. Carries the RFC 6749 `error` discriminator\n * (`\"invalid_grant\"`, `\"invalid_client\"`, `\"unsupported_grant_type\"`,\n * …) and the optional `error_description` so callers can branch on\n * machine-readable values without parsing the human message.\n *\n * `invalid_grant` from the refresh path specifically signals that the\n * refresh token has been revoked (or the OAuth grant / application\n * was deleted). The SDK never loops on this — callers should route\n * the user back to a fresh sign-in.\n */\nexport class MantyxOAuthError extends MantyxError {\n readonly oauthError: string;\n readonly oauthErrorDescription: string | undefined;\n\n constructor(\n oauthError: string,\n oauthErrorDescription: string | undefined,\n status: number,\n ) {\n const message = oauthErrorDescription\n ? `OAuth ${oauthError}: ${oauthErrorDescription}`\n : `OAuth ${oauthError}`;\n super(message, { code: oauthError, status });\n this.name = \"MantyxOAuthError\";\n this.oauthError = oauthError;\n this.oauthErrorDescription = oauthErrorDescription;\n }\n}\n\n/**\n * Decoded `POST /api/oauth/token` response, augmented with an absolute\n * `expiresAt` timestamp the SDK uses to decide when to refresh.\n *\n * On the refresh grant the response's `refreshToken` is identical to\n * the value the client just sent (refresh tokens never rotate). The\n * field is surfaced for symmetry with whatever the calling app's\n * sign-in flow already does.\n */\nexport interface OAuthToken {\n readonly accessToken: string;\n readonly refreshToken: string | undefined;\n readonly tokenType: string;\n readonly expiresIn: number;\n /** Absolute Unix-ms timestamp set when the SDK parsed the response. */\n readonly expiresAt: number;\n readonly scope: string | undefined;\n}\n\n/** Why the SDK asked the {@link TokenSource} for the current access token. */\nexport type TokenRequestReason = \"initial\" | \"expired\" | \"unauthorized\";\n\n/**\n * A `TokenSource` produces the current access token on demand. The\n * {@link MantyxClient} HTTP layer calls it before every request. When\n * called with `reason: \"unauthorized\"` the source MUST force a refresh\n * (do not return a cached value); this is how the SDK recovers from\n * 401s caused by a token that the server already invalidated.\n *\n * Implementations should be safe to call from many concurrent requests.\n */\nexport type TokenSource = (reason?: TokenRequestReason) => Promise<string>;\n\n/** Caller-supplied options for `MantyxOAuthClient`. */\nexport interface MantyxOAuthClientOptions {\n /**\n * OAuth `client_id` issued at app registration (token prefix\n * `mantyx_oa_`).\n */\n clientId: string;\n /**\n * OAuth `client_secret` issued at app registration (token prefix\n * `mantyx_oas_`). Every MANTYX OAuth app is a confidential client,\n * so this is always required for token + revoke calls. Treat as a\n * deployment secret — do not bundle into browser builds.\n */\n clientSecret: string;\n /**\n * Origin of the MANTYX deployment. Defaults to `https://app.mantyx.io`.\n * The OAuth endpoints are mounted at `<baseUrl>/api/oauth/...`.\n */\n baseUrl?: string;\n /** Optional `fetch` override (e.g. node-fetch wrapper). Default: global `fetch`. */\n fetch?: typeof fetch;\n /** Default per-request timeout in milliseconds. Default: 30s. */\n timeoutMs?: number;\n}\n\nexport interface RefreshOptions {\n refreshToken: string;\n /**\n * Optional scope narrowing. Must be a subset of the scopes already\n * granted to the refresh token (server enforces this). Useful when\n * an SDK consumer wants a short-scope access token for a specific\n * sub-operation.\n */\n scope?: string | readonly string[];\n}\n\nexport interface RevokeOptions {\n token: string;\n}\n\nexport interface RefreshTokenSourceOptions {\n refreshToken: string;\n /** Optional scope narrowing applied on every refresh. */\n scope?: string | readonly string[];\n /**\n * How many ms before `expiresAt` the source proactively refreshes.\n * Defaults to {@link DEFAULT_REFRESH_SKEW_MS} (60s).\n */\n refreshSkewMs?: number;\n /**\n * Optional initial access token + expiry to seed the source's cache\n * with (e.g. the token already in hand from the host application's\n * sign-in flow). When omitted, the source mints one on the first\n * call.\n */\n initialToken?: OAuthToken;\n}\n\n/**\n * Refresh-only wrapper around the MANTYX OAuth 2.0 authorization-server\n * endpoints. App-scoped (one per `{clientId, clientSecret}` pair);\n * construct independently of {@link MantyxClient}, then either call\n * {@link refresh} / {@link revoke} directly or hand a `TokenSource`\n * produced by {@link refreshTokenSource} to `MantyxClient` for fully\n * transparent refresh on every request.\n *\n * The client deliberately does **not** drive the authorization-code\n * exchange or any other \"initiate sign-in\" grant. The caller is\n * expected to obtain the refresh token through their own consent flow\n * and persist it before constructing this client.\n */\nexport class MantyxOAuthClient {\n readonly clientId: string;\n readonly baseUrl: string;\n private readonly clientSecret: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n\n constructor(opts: MantyxOAuthClientOptions) {\n if (!opts.clientId) {\n throw new MantyxError(\"`clientId` is required for MantyxOAuthClient\");\n }\n if (!opts.clientSecret) {\n throw new MantyxError(\"`clientSecret` is required for MantyxOAuthClient\");\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 MantyxOAuthClientOptions.\",\n );\n }\n this.clientId = opts.clientId;\n this.clientSecret = opts.clientSecret;\n this.baseUrl = (opts.baseUrl ?? DEFAULT_OAUTH_BASE_URL).replace(/\\/+$/, \"\");\n this.fetchImpl = f;\n this.timeoutMs = opts.timeoutMs ?? 30_000;\n }\n\n /**\n * Mint a fresh access token from a stored refresh token. The\n * returned `refreshToken` is identical to the input — refresh\n * tokens are persistent and non-rotating, so the field is\n * surfaced only for symmetry with the response shape.\n *\n * On `400 invalid_grant` the refresh token has been revoked (or its\n * grant / app was deleted); the SDK surfaces a\n * {@link MantyxOAuthError} and callers must drive a fresh sign-in.\n */\n async refresh(opts: RefreshOptions): Promise<OAuthToken> {\n if (!opts.refreshToken) {\n throw new MantyxError(\"`refreshToken` is required for MantyxOAuthClient.refresh\");\n }\n const body: Record<string, string> = {\n grant_type: \"refresh_token\",\n refresh_token: opts.refreshToken,\n };\n const scope = normalizeScope(opts.scope);\n if (scope !== undefined) body.scope = scope;\n return this.token(body);\n }\n\n /**\n * Revoke an access or refresh token (RFC 7009). The server always\n * returns 200, even for unknown tokens. Revoking a **refresh**\n * token kills the refresh and every live access token tied to its\n * grant; revoking an **access** token kills only that one.\n */\n async revoke(opts: RevokeOptions): Promise<void> {\n if (!opts.token) {\n throw new MantyxError(\"`token` is required for MantyxOAuthClient.revoke\");\n }\n await this.formPost(\"/api/oauth/revoke\", {\n token: opts.token,\n });\n }\n\n /**\n * Build a long-lived {@link TokenSource} that re-mints access\n * tokens from the supplied refresh token. Pass the returned source\n * to `new MantyxClient({ tokenSource, workspaceSlug, ... })`. The\n * source caches the access token in-memory and refreshes\n * proactively when the cached value is within `refreshSkewMs` of\n * `expiresAt`, or eagerly when `MantyxClient` reports a 401.\n *\n * Pass `initialToken` if the calling app already has a non-expired\n * access token in hand (e.g. straight out of the sign-in flow) to\n * avoid an extra round-trip on the first request.\n */\n refreshTokenSource(opts: RefreshTokenSourceOptions): TokenSource {\n if (!opts.refreshToken) {\n throw new MantyxError(\"`refreshToken` is required for MantyxOAuthClient.refreshTokenSource\");\n }\n const skew = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;\n const cache: TokenCache = { token: opts.initialToken, inflight: null };\n const refreshToken = opts.refreshToken;\n return makeTokenSource(cache, skew, async () => {\n return this.refresh({ refreshToken, scope: opts.scope });\n });\n }\n\n // -------------------------------------------------------------- internals\n\n /**\n * POST `application/x-www-form-urlencoded` to `/api/oauth/token` and\n * decode the {@link OAuthToken} response. Always injects `client_id`\n * + `client_secret` from the constructor.\n */\n private async token(body: Record<string, string>): Promise<OAuthToken> {\n const res = await this.formPost(\"/api/oauth/token\", body);\n let parsed: Record<string, unknown> = {};\n try {\n parsed = (await res.json()) as Record<string, unknown>;\n } catch {\n throw new MantyxOAuthError(\n \"invalid_response\",\n \"Token endpoint returned a non-JSON response\",\n res.status,\n );\n }\n const accessToken = typeof parsed.access_token === \"string\" ? parsed.access_token : \"\";\n if (!accessToken) {\n throw new MantyxOAuthError(\n \"invalid_response\",\n \"Token endpoint response is missing `access_token`\",\n res.status,\n );\n }\n const expiresIn = typeof parsed.expires_in === \"number\" ? parsed.expires_in : 3600;\n return {\n accessToken,\n refreshToken: typeof parsed.refresh_token === \"string\" ? parsed.refresh_token : undefined,\n tokenType: typeof parsed.token_type === \"string\" ? parsed.token_type : \"Bearer\",\n expiresIn,\n expiresAt: Date.now() + expiresIn * 1000,\n scope: typeof parsed.scope === \"string\" ? parsed.scope : undefined,\n };\n }\n\n private async formPost(path: string, body: Record<string, string>): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n const params = new URLSearchParams({\n ...body,\n client_id: this.clientId,\n client_secret: this.clientSecret,\n });\n const ctrl = new AbortController();\n const t = setTimeout(() => ctrl.abort(), this.timeoutMs);\n let res: Response;\n try {\n res = await this.fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: params.toString(),\n signal: ctrl.signal,\n });\n } catch (err) {\n if (ctrl.signal.aborted) {\n throw new MantyxNetworkError(`OAuth request timed out after ${this.timeoutMs}ms`);\n }\n throw new MantyxNetworkError(`OAuth network error: ${(err as Error).message}`, {\n cause: err,\n });\n } finally {\n clearTimeout(t);\n }\n if (!res.ok) {\n let errBody: { error?: unknown; error_description?: unknown } = {};\n try {\n errBody = (await res.json()) as typeof errBody;\n } catch {\n // ignore\n }\n const oauthError = typeof errBody.error === \"string\" ? errBody.error : `http_${res.status}`;\n const desc =\n typeof errBody.error_description === \"string\" ? errBody.error_description : undefined;\n throw new MantyxOAuthError(oauthError, desc, res.status);\n }\n return res;\n }\n}\n\n// -------------------------------------------------------------- internals\n\ninterface TokenCache {\n token: OAuthToken | undefined;\n inflight: Promise<OAuthToken> | null;\n}\n\n/**\n * Wrap a `mintToken` thunk into a single-flight {@link TokenSource}\n * with a cache + proactive-refresh skew. The cache is overwritten\n * atomically on every successful mint; the in-flight promise\n * collapses N concurrent expired-token observers into one mint call.\n *\n * Single-flight is an efficiency, not a correctness requirement —\n * `docs/oauth.md` explicitly allows multiple concurrent refreshes\n * against the same refresh token — but it keeps the token-endpoint\n * QPS reasonable when an SDK consumer fans out work in parallel.\n */\nfunction makeTokenSource(\n cache: TokenCache,\n skewMs: number,\n mint: () => Promise<OAuthToken>,\n): TokenSource {\n return async (reason: TokenRequestReason = \"initial\"): Promise<string> => {\n if (reason !== \"unauthorized\" && cache.token && !isExpiring(cache.token, skewMs)) {\n return cache.token.accessToken;\n }\n if (cache.inflight) {\n const t = await cache.inflight;\n if (reason === \"unauthorized\" && t === cache.token) {\n // If the inflight refresh was triggered by a benign cache miss\n // and we observed an unauthorized hint after it started, fall\n // through and mint again so the caller never gets a stale token.\n } else {\n return t.accessToken;\n }\n }\n cache.inflight = mint().then(\n (t) => {\n cache.token = t;\n return t;\n },\n (err: unknown) => {\n throw err;\n },\n );\n try {\n const t = await cache.inflight;\n return t.accessToken;\n } finally {\n cache.inflight = null;\n }\n };\n}\n\nfunction isExpiring(token: OAuthToken, skewMs: number): boolean {\n return token.expiresAt - Date.now() <= skewMs;\n}\n\nfunction normalizeScope(scope: string | readonly string[] | undefined): string | undefined {\n if (scope === undefined) return undefined;\n if (typeof scope === \"string\") {\n const trimmed = scope.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n }\n const joined = scope.filter((s) => typeof s === \"string\" && s.length > 0).join(\" \");\n return joined.length > 0 ? joined : undefined;\n}\n","/**\n * Release version — synced from repo root VERSION (`npm run sync-version`).\n */\nexport const SDK_VERSION = \"0.11.0\";\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/oauth.ts","../src/version.ts"],"sourcesContent":["/**\n * MANTYX OAuth 2.0 refresh client: trade a stored refresh token for\n * short-lived access tokens, revoke tokens at sign-out, and expose\n * a {@link TokenSource} the {@link MantyxClient} HTTP layer calls\n * before every request (and again on 401).\n *\n * The library is intentionally **refresh-only**. It assumes the caller\n * already obtained the refresh token through their own sign-in flow\n * (Authorization Code + PKCE in a browser, native redirect, server-\n * side exchange — whatever fits the host application). The SDK does\n * not drive consent, does not initiate auth-code exchanges, and does\n * not bundle PKCE helpers.\n *\n * Wire contract (`docs/oauth.md`):\n *\n * - Token endpoint: `POST <baseUrl>/api/oauth/token`, form-encoded,\n * `grant_type=refresh_token`. Echoes back the same `refresh_token`\n * the client sent (refresh tokens are persistent and non-rotating).\n * - Revoke endpoint: `POST <baseUrl>/api/oauth/revoke`, form-encoded.\n * - Access tokens (`mantyx_at_…`) live 1 hour (`expires_in: 3600`).\n * - Refresh tokens (`mantyx_rt_…`) are long-lived; the caller persists\n * them once at first sign-in (encrypted at rest) and the SDK re-mints\n * access tokens from the same value on demand.\n */\n\nimport { MantyxError, MantyxNetworkError } from \"./errors.js\";\n\nexport const DEFAULT_OAUTH_BASE_URL = \"https://app.mantyx.io\";\n\n/** Skew (ms) before `expiresAt` at which a TokenSource will pre-emptively refresh. Default 60s. */\nexport const DEFAULT_REFRESH_SKEW_MS = 60_000;\n\n/**\n * Raised on a non-2xx response from `POST /api/oauth/token` or\n * `POST /api/oauth/revoke`. Carries the RFC 6749 `error` discriminator\n * (`\"invalid_grant\"`, `\"invalid_client\"`, `\"unsupported_grant_type\"`,\n * …) and the optional `error_description` so callers can branch on\n * machine-readable values without parsing the human message.\n *\n * `invalid_grant` from the refresh path specifically signals that the\n * refresh token has been revoked (or the OAuth grant / application\n * was deleted). The SDK never loops on this — callers should route\n * the user back to a fresh sign-in.\n */\nexport class MantyxOAuthError extends MantyxError {\n readonly oauthError: string;\n readonly oauthErrorDescription: string | undefined;\n\n constructor(\n oauthError: string,\n oauthErrorDescription: string | undefined,\n status: number,\n ) {\n const message = oauthErrorDescription\n ? `OAuth ${oauthError}: ${oauthErrorDescription}`\n : `OAuth ${oauthError}`;\n super(message, { code: oauthError, status });\n this.name = \"MantyxOAuthError\";\n this.oauthError = oauthError;\n this.oauthErrorDescription = oauthErrorDescription;\n }\n}\n\n/**\n * Decoded `POST /api/oauth/token` response, augmented with an absolute\n * `expiresAt` timestamp the SDK uses to decide when to refresh.\n *\n * On the refresh grant the response's `refreshToken` is identical to\n * the value the client just sent (refresh tokens never rotate). The\n * field is surfaced for symmetry with whatever the calling app's\n * sign-in flow already does.\n */\nexport interface OAuthToken {\n readonly accessToken: string;\n readonly refreshToken: string | undefined;\n readonly tokenType: string;\n readonly expiresIn: number;\n /** Absolute Unix-ms timestamp set when the SDK parsed the response. */\n readonly expiresAt: number;\n readonly scope: string | undefined;\n}\n\n/** Why the SDK asked the {@link TokenSource} for the current access token. */\nexport type TokenRequestReason = \"initial\" | \"expired\" | \"unauthorized\";\n\n/**\n * A `TokenSource` produces the current access token on demand. The\n * {@link MantyxClient} HTTP layer calls it before every request. When\n * called with `reason: \"unauthorized\"` the source MUST force a refresh\n * (do not return a cached value); this is how the SDK recovers from\n * 401s caused by a token that the server already invalidated.\n *\n * Implementations should be safe to call from many concurrent requests.\n */\nexport type TokenSource = (reason?: TokenRequestReason) => Promise<string>;\n\n/** Caller-supplied options for `MantyxOAuthClient`. */\nexport interface MantyxOAuthClientOptions {\n /**\n * OAuth `client_id` issued at app registration (token prefix\n * `mantyx_oa_`).\n */\n clientId: string;\n /**\n * OAuth `client_secret` issued at app registration (token prefix\n * `mantyx_oas_`). Every MANTYX OAuth app is a confidential client,\n * so this is always required for token + revoke calls. Treat as a\n * deployment secret — do not bundle into browser builds.\n */\n clientSecret: string;\n /**\n * Origin of the MANTYX deployment. Defaults to `https://app.mantyx.io`.\n * The OAuth endpoints are mounted at `<baseUrl>/api/oauth/...`.\n */\n baseUrl?: string;\n /** Optional `fetch` override (e.g. node-fetch wrapper). Default: global `fetch`. */\n fetch?: typeof fetch;\n /** Default per-request timeout in milliseconds. Default: 30s. */\n timeoutMs?: number;\n}\n\nexport interface RefreshOptions {\n refreshToken: string;\n /**\n * Optional scope narrowing. Must be a subset of the scopes already\n * granted to the refresh token (server enforces this). Useful when\n * an SDK consumer wants a short-scope access token for a specific\n * sub-operation.\n */\n scope?: string | readonly string[];\n}\n\nexport interface RevokeOptions {\n token: string;\n}\n\nexport interface RefreshTokenSourceOptions {\n refreshToken: string;\n /** Optional scope narrowing applied on every refresh. */\n scope?: string | readonly string[];\n /**\n * How many ms before `expiresAt` the source proactively refreshes.\n * Defaults to {@link DEFAULT_REFRESH_SKEW_MS} (60s).\n */\n refreshSkewMs?: number;\n /**\n * Optional initial access token + expiry to seed the source's cache\n * with (e.g. the token already in hand from the host application's\n * sign-in flow). When omitted, the source mints one on the first\n * call.\n */\n initialToken?: OAuthToken;\n}\n\n/**\n * Refresh-only wrapper around the MANTYX OAuth 2.0 authorization-server\n * endpoints. App-scoped (one per `{clientId, clientSecret}` pair);\n * construct independently of {@link MantyxClient}, then either call\n * {@link refresh} / {@link revoke} directly or hand a `TokenSource`\n * produced by {@link refreshTokenSource} to `MantyxClient` for fully\n * transparent refresh on every request.\n *\n * The client deliberately does **not** drive the authorization-code\n * exchange or any other \"initiate sign-in\" grant. The caller is\n * expected to obtain the refresh token through their own consent flow\n * and persist it before constructing this client.\n */\nexport class MantyxOAuthClient {\n readonly clientId: string;\n readonly baseUrl: string;\n private readonly clientSecret: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n\n constructor(opts: MantyxOAuthClientOptions) {\n if (!opts.clientId) {\n throw new MantyxError(\"`clientId` is required for MantyxOAuthClient\");\n }\n if (!opts.clientSecret) {\n throw new MantyxError(\"`clientSecret` is required for MantyxOAuthClient\");\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 MantyxOAuthClientOptions.\",\n );\n }\n this.clientId = opts.clientId;\n this.clientSecret = opts.clientSecret;\n this.baseUrl = (opts.baseUrl ?? DEFAULT_OAUTH_BASE_URL).replace(/\\/+$/, \"\");\n this.fetchImpl = f;\n this.timeoutMs = opts.timeoutMs ?? 30_000;\n }\n\n /**\n * Mint a fresh access token from a stored refresh token. The\n * returned `refreshToken` is identical to the input — refresh\n * tokens are persistent and non-rotating, so the field is\n * surfaced only for symmetry with the response shape.\n *\n * On `400 invalid_grant` the refresh token has been revoked (or its\n * grant / app was deleted); the SDK surfaces a\n * {@link MantyxOAuthError} and callers must drive a fresh sign-in.\n */\n async refresh(opts: RefreshOptions): Promise<OAuthToken> {\n if (!opts.refreshToken) {\n throw new MantyxError(\"`refreshToken` is required for MantyxOAuthClient.refresh\");\n }\n const body: Record<string, string> = {\n grant_type: \"refresh_token\",\n refresh_token: opts.refreshToken,\n };\n const scope = normalizeScope(opts.scope);\n if (scope !== undefined) body.scope = scope;\n return this.token(body);\n }\n\n /**\n * Revoke an access or refresh token (RFC 7009). The server always\n * returns 200, even for unknown tokens. Revoking a **refresh**\n * token kills the refresh and every live access token tied to its\n * grant; revoking an **access** token kills only that one.\n */\n async revoke(opts: RevokeOptions): Promise<void> {\n if (!opts.token) {\n throw new MantyxError(\"`token` is required for MantyxOAuthClient.revoke\");\n }\n await this.formPost(\"/api/oauth/revoke\", {\n token: opts.token,\n });\n }\n\n /**\n * Build a long-lived {@link TokenSource} that re-mints access\n * tokens from the supplied refresh token. Pass the returned source\n * to `new MantyxClient({ tokenSource, workspaceSlug, ... })`. The\n * source caches the access token in-memory and refreshes\n * proactively when the cached value is within `refreshSkewMs` of\n * `expiresAt`, or eagerly when `MantyxClient` reports a 401.\n *\n * Pass `initialToken` if the calling app already has a non-expired\n * access token in hand (e.g. straight out of the sign-in flow) to\n * avoid an extra round-trip on the first request.\n */\n refreshTokenSource(opts: RefreshTokenSourceOptions): TokenSource {\n if (!opts.refreshToken) {\n throw new MantyxError(\"`refreshToken` is required for MantyxOAuthClient.refreshTokenSource\");\n }\n const skew = opts.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;\n const cache: TokenCache = { token: opts.initialToken, inflight: null };\n const refreshToken = opts.refreshToken;\n return makeTokenSource(cache, skew, async () => {\n return this.refresh({ refreshToken, scope: opts.scope });\n });\n }\n\n // -------------------------------------------------------------- internals\n\n /**\n * POST `application/x-www-form-urlencoded` to `/api/oauth/token` and\n * decode the {@link OAuthToken} response. Always injects `client_id`\n * + `client_secret` from the constructor.\n */\n private async token(body: Record<string, string>): Promise<OAuthToken> {\n const res = await this.formPost(\"/api/oauth/token\", body);\n let parsed: Record<string, unknown> = {};\n try {\n parsed = (await res.json()) as Record<string, unknown>;\n } catch {\n throw new MantyxOAuthError(\n \"invalid_response\",\n \"Token endpoint returned a non-JSON response\",\n res.status,\n );\n }\n const accessToken = typeof parsed.access_token === \"string\" ? parsed.access_token : \"\";\n if (!accessToken) {\n throw new MantyxOAuthError(\n \"invalid_response\",\n \"Token endpoint response is missing `access_token`\",\n res.status,\n );\n }\n const expiresIn = typeof parsed.expires_in === \"number\" ? parsed.expires_in : 3600;\n return {\n accessToken,\n refreshToken: typeof parsed.refresh_token === \"string\" ? parsed.refresh_token : undefined,\n tokenType: typeof parsed.token_type === \"string\" ? parsed.token_type : \"Bearer\",\n expiresIn,\n expiresAt: Date.now() + expiresIn * 1000,\n scope: typeof parsed.scope === \"string\" ? parsed.scope : undefined,\n };\n }\n\n private async formPost(path: string, body: Record<string, string>): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n const params = new URLSearchParams({\n ...body,\n client_id: this.clientId,\n client_secret: this.clientSecret,\n });\n const ctrl = new AbortController();\n const t = setTimeout(() => ctrl.abort(), this.timeoutMs);\n let res: Response;\n try {\n res = await this.fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: params.toString(),\n signal: ctrl.signal,\n });\n } catch (err) {\n if (ctrl.signal.aborted) {\n throw new MantyxNetworkError(`OAuth request timed out after ${this.timeoutMs}ms`);\n }\n throw new MantyxNetworkError(`OAuth network error: ${(err as Error).message}`, {\n cause: err,\n });\n } finally {\n clearTimeout(t);\n }\n if (!res.ok) {\n let errBody: { error?: unknown; error_description?: unknown } = {};\n try {\n errBody = (await res.json()) as typeof errBody;\n } catch {\n // ignore\n }\n const oauthError = typeof errBody.error === \"string\" ? errBody.error : `http_${res.status}`;\n const desc =\n typeof errBody.error_description === \"string\" ? errBody.error_description : undefined;\n throw new MantyxOAuthError(oauthError, desc, res.status);\n }\n return res;\n }\n}\n\n// -------------------------------------------------------------- internals\n\ninterface TokenCache {\n token: OAuthToken | undefined;\n inflight: Promise<OAuthToken> | null;\n}\n\n/**\n * Wrap a `mintToken` thunk into a single-flight {@link TokenSource}\n * with a cache + proactive-refresh skew. The cache is overwritten\n * atomically on every successful mint; the in-flight promise\n * collapses N concurrent expired-token observers into one mint call.\n *\n * Single-flight is an efficiency, not a correctness requirement —\n * `docs/oauth.md` explicitly allows multiple concurrent refreshes\n * against the same refresh token — but it keeps the token-endpoint\n * QPS reasonable when an SDK consumer fans out work in parallel.\n */\nfunction makeTokenSource(\n cache: TokenCache,\n skewMs: number,\n mint: () => Promise<OAuthToken>,\n): TokenSource {\n return async (reason: TokenRequestReason = \"initial\"): Promise<string> => {\n if (reason !== \"unauthorized\" && cache.token && !isExpiring(cache.token, skewMs)) {\n return cache.token.accessToken;\n }\n if (cache.inflight) {\n const t = await cache.inflight;\n if (reason === \"unauthorized\" && t === cache.token) {\n // If the inflight refresh was triggered by a benign cache miss\n // and we observed an unauthorized hint after it started, fall\n // through and mint again so the caller never gets a stale token.\n } else {\n return t.accessToken;\n }\n }\n cache.inflight = mint().then(\n (t) => {\n cache.token = t;\n return t;\n },\n (err: unknown) => {\n throw err;\n },\n );\n try {\n const t = await cache.inflight;\n return t.accessToken;\n } finally {\n cache.inflight = null;\n }\n };\n}\n\nfunction isExpiring(token: OAuthToken, skewMs: number): boolean {\n return token.expiresAt - Date.now() <= skewMs;\n}\n\nfunction normalizeScope(scope: string | readonly string[] | undefined): string | undefined {\n if (scope === undefined) return undefined;\n if (typeof scope === \"string\") {\n const trimmed = scope.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n }\n const joined = scope.filter((s) => typeof s === \"string\" && s.length > 0).join(\" \");\n return joined.length > 0 ? joined : undefined;\n}\n","/**\n * Release version — synced from repo root VERSION (`npm run sync-version`).\n */\nexport const SDK_VERSION = \"0.12.0\";\n"],"mappings":"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","names":[]}
@@ -361,8 +361,12 @@ The agent spec is the body shape used by `POST /agent-runs` and `POST
361
361
  "hive_consult_ontology": { "maxCalls": 4 },
362
362
  "scary_tool": { "maxCalls": 0 },
363
363
  },
364
+ "supervisor": {
365
+ // optional, see §4.8 — platform LLM judge; pass false to disable
366
+ "interval": 5,
367
+ },
364
368
  "metadata": {
365
- // optional, see §4.8
369
+ // optional, see §4.9
366
370
  "customer": "acme",
367
371
  "env": "prod",
368
372
  },
@@ -844,7 +848,64 @@ during normal multi-entity reads. The loop-detection guard catches the
844
848
  pathological "same `(name, args)` batch over and over" case for that
845
849
  family without needing per-tool caps.
846
850
 
847
- ### 4.8 `metadata` (developer-supplied KV for filtering)
851
+ ### 4.8 `supervisor` (run judge)
852
+
853
+ `supervisor` controls the optional **run supervisor** — an LLM judge that
854
+ periodically reviews the agent's transcript (reasoning, tool calls, tool
855
+ results, visible text) and may steer the run:
856
+
857
+ - **`on_track`** — no-op; the run continues.
858
+ - **`redirect`** — a steering user message is injected; tools stay available.
859
+ - **`finalize`** — the next turn is forced tools-disabled so the run lands a
860
+ clean final answer.
861
+
862
+ Reviews fire every **`interval` LLM calls** (`completeTurn` invocations) at
863
+ the bottom of tool-emitting rounds. Default interval is **5** when enabled.
864
+
865
+ ```jsonc
866
+ "supervisor": {
867
+ "interval": 5 // optional — LLM calls between reviews; default 5
868
+ }
869
+
870
+ // or:
871
+ "supervisor": false // explicitly disable the platform judge for this run
872
+ ```
873
+
874
+ | Field | Type | Required | Notes |
875
+ | ----------------- | --------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
876
+ | `interval` | integer ≥ 1 | no | Defaults to **5** when the supervisor is enabled and `interval` is omitted. Capped at **100** server-side. |
877
+ | (literal `false`) | `false` | no | Disables the run supervisor for this run. `loopDetection` and `toolBudgets` still apply. |
878
+
879
+ **Defaults.** When `supervisor` is **omitted**, MANTYX enables the platform
880
+ LLM judge on ephemeral runs. Pass `"supervisor": false` to opt out.
881
+
882
+ **SDK-only usage.** When calling `@mantyx/ts-sdk` directly (not via
883
+ `POST /agent-runs`), the supervisor is **off unless explicitly configured**:
884
+ pass `supervisor: { review, interval? }` on `RunAgentOptions` to enable a
885
+ caller-supplied judge, or pass `supervisor: false` (or omit the field) to
886
+ keep it disabled. The wire field above controls the **platform-hosted** judge
887
+ on API/ephemeral runs only.
888
+
889
+ Validation (server-side, `400 invalid_request` on violation):
890
+
891
+ | Constraint | Limit |
892
+ | ----------------- | ----- |
893
+ | `interval` upper bound | `100` |
894
+
895
+ **Inheritance for sessions.**
896
+
897
+ - `POST /agent-sessions { supervisor }` — sets the session-default, applied
898
+ to every subsequent message run.
899
+ - `POST /agent-sessions/:id/messages { supervisor }` — optional per-message
900
+ override; applies to that one run only and does not mutate the session's
901
+ stored value.
902
+
903
+ **Observability.** Each review emits a SSE `supervisor` event (see §7) —
904
+ including `on_track` checks — so SDK clients can render supervisor activity.
905
+ When `action` is `redirect` or `finalize`, the pipeline has already applied
906
+ the verdict by the time the event arrives.
907
+
908
+ ### 4.9 `metadata` (developer-supplied KV for filtering)
848
909
 
849
910
  `metadata` is a flat string→string KV that is **persisted alongside the run /
850
911
  session** and surfaced in the MANTYX dashboard. Use it to tag runs with your
@@ -962,9 +1023,6 @@ data: <utf-8 JSON>
962
1023
  `<type>` and `<data>` shapes:
963
1024
 
964
1025
  ```jsonc
965
- // running message
966
- { "seq": 1, "type": "started", "data": {} }
967
-
968
1026
  // streamed assistant tokens (zero or more per turn)
969
1027
  { "seq": 2, "type": "assistant_delta", "data": { "text": "Hello" } }
970
1028
 
@@ -1001,6 +1059,11 @@ data: <utf-8 JSON>
1001
1059
  // is observability so SDK clients can render "memory budget exhausted" status notes.
1002
1060
  { "seq": 7, "type": "tool_budget_exceeded", "data": { "tool": "recall", "maxCalls": 4, "callIndex": 5 } }
1003
1061
 
1062
+ // run-supervisor check (see §4.8). Fired on every review — on_track included.
1063
+ { "seq": 7, "type": "supervisor", "data": { "action": "on_track", "reason": "Agent is making progress.", "llmCalls": 5 } }
1064
+ { "seq": 8, "type": "supervisor", "data": { "action": "redirect", "reason": "Stuck re-querying.", "redirect": "Answer from the data you already have.", "llmCalls": 10 } }
1065
+ { "seq": 9, "type": "supervisor", "data": { "action": "finalize", "reason": "Enough to answer.", "llmCalls": 15 } }
1066
+
1004
1067
  // terminal event
1005
1068
  // Every terminal `result` event also carries `tokens`, `turns`, and `model`
1006
1069
  // for cost attribution and dashboards — see §7.1. Older platforms (pre-
@@ -1241,18 +1304,18 @@ A reference SDK should:
1241
1304
  - Treat `thinking_delta` events as opt-in callback fodder; many UIs hide
1242
1305
  them by default. Their presence depends on `reasoningLevel > 0` and
1243
1306
  on the active model exposing thought parts.
1244
- - Accept `loopDetection` and `toolBudgets` from the caller and pass
1245
- them through unchanged (see §4.6 / §4.7). Both fields are _additive_:
1246
- omitting them keeps MANTYX's runtime defaults; passing
1247
- `loopDetection: false` opts out; passing `toolBudgets: {}` clears the
1248
- defaults; passing entries layers caller overrides on top of the
1249
- defaults.
1250
- - Treat `loop_detected` and `tool_budget_exceeded` SSE events as
1251
- observability-only — the server already substituted the synthetic
1252
- tool-results / steering nudges, so the SDK's job is just to surface
1253
- the event to the caller (status banner, log line, telemetry). Do
1254
- **not** abort the run on these events; the run continues through
1255
- `result` / `error` / `cancelled` as usual.
1307
+ - Accept `loopDetection`, `toolBudgets`, and `supervisor` from the caller
1308
+ and pass them through unchanged (see §4.6 / §4.7 / §4.8). All three are
1309
+ _additive_: omitting them keeps MANTYX's runtime defaults; passing
1310
+ `loopDetection: false` or `supervisor: false` opts out; passing
1311
+ `toolBudgets: {}` clears the defaults; passing entries layers caller
1312
+ overrides on top of the defaults.
1313
+ - Treat `loop_detected`, `tool_budget_exceeded`, and `supervisor` SSE
1314
+ events as observability-only — the server already substituted synthetic
1315
+ tool-results / steering nudges / supervisor verdicts where applicable, so
1316
+ the SDK's job is just to surface the event to the caller (status banner,
1317
+ log line, telemetry). Do **not** abort the run on these events; the run
1318
+ continues through `result` / `error` / `cancelled` as usual.
1256
1319
  - On terminal `result`, resolve the call. On `error` subtype, throw.
1257
1320
  4. Re-emit assistant deltas/events as a stream/iterator for callers who care
1258
1321
  about live output.
@@ -1269,4 +1332,4 @@ A reference SDK should:
1269
1332
 
1270
1333
  The npm package [`@mantyx/sdk`](https://www.npmjs.com/package/@mantyx/sdk) and the Go module
1271
1334
  [`github.com/mantyx/mantyx-go-sdk`](https://github.com/mantyx/mantyx-go-sdk) are reference implementations of this protocol
1272
- (maintained in the official **mantyx-sdk** repositories).
1335
+ (maintained in the official **mantyx-sdk** repositories).
@@ -132,6 +132,10 @@ short-circuit, etc.) see `agent-runs-protocol.md` §4.
132
132
  "recall": { "maxCalls": 4 },
133
133
  "hive_consult_ontology": { "maxCalls": 4 },
134
134
  },
135
+ "supervisor": {
136
+ // optional; see §8.4 — platform LLM judge on ephemeral runs
137
+ "interval": 5,
138
+ },
135
139
  "metadata": { "customer": "acme" }, // optional, free-form k/v
136
140
  }
137
141
  ```
@@ -140,9 +144,9 @@ short-circuit, etc.) see `agent-runs-protocol.md` §4.
140
144
 
141
145
  Same body shape, posted to `POST /agent-sessions/:id/messages`. The session
142
146
  keeps the conversation history; per-message `tools`, `reasoningLevel`,
143
- `outputSchema`, `loopDetection`, and `toolBudgets` _replace_ the session's
144
- defaults for that single run only — the next run falls back to whatever
145
- the session was created with.
147
+ `outputSchema`, `loopDetection`, `toolBudgets`, and `supervisor` _replace_
148
+ the session's defaults for that single run only — the next run falls back to
149
+ whatever the session was created with.
146
150
 
147
151
  ---
148
152
 
@@ -391,6 +395,7 @@ The vocabulary (`EphemeralEventType` in `bus.ts`):
391
395
  | `local_tool_result_in` | M → SDK | Per client-resolved tool call | Informational mirror of the tool-result the SDK just posted, persisted for observability. Re-emitted to late subscribers so they can replay the conversation. |
392
396
  | `loop_detected` | M → SDK | 0–2× per run (soft nudge + optional hard cutoff) | Observability for the loop-detection guard (see §8). The server already substituted the synthetic skip + steering nudge — SDK clients render a status note (`looping — nudged` / `looping — gave up`) and otherwise leave the run alone. |
393
397
  | `tool_budget_exceeded` | M → SDK | Per intercepted tool call | Observability for per-tool call budgets (see §8). The synthetic `tool_result` carrying the "budget exceeded — pivot or finalize" body lands on the normal tool-result channel; this event is purely so SDK clients can surface a UI banner. |
398
+ | `supervisor` | M → SDK | 0–N× per run (every `interval` LLM calls) | Run-supervisor check (see §4.7 / §8.4). Fired on **every** review — including `on_track` — so SDK clients can render supervisor activity. When the judge steers the run (`redirect` / `finalize`), the pipeline has already injected the steering message or forced a tools-disabled finalize turn. |
394
399
  | `assistant_message` | M → SDK | 1× per turn | Final assistant message for the turn (concatenated, persistence-ready). |
395
400
  | `result` | M → SDK | 1× terminal | Successful completion. Carries the final assistant text and run summary. |
396
401
  | `error` | M → SDK | 1× terminal | Failure. Carries `error` (message), `code` / `errorClass` (category), `finishReason`, and an optional `partialText` salvage payload. See §4.7. |
@@ -640,7 +645,45 @@ re-parsing tool-result bodies.
640
645
 
641
646
  See §8 for the wire-spec field that defines budgets.
642
647
 
643
- ### 4.7 Terminal events
648
+ ### 4.7 `supervisor`
649
+
650
+ ```jsonc
651
+ // on_track — the judge reviewed the run and decided not to intervene
652
+ { "seq": 15, "type": "supervisor",
653
+ "data": { "action": "on_track", "reason": "Agent is gathering context via search before answering.", "llmCalls": 5 } }
654
+
655
+ // redirect — a steering user message was injected; the agent keeps its tools
656
+ { "seq": 20, "type": "supervisor",
657
+ "data": { "action": "redirect", "reason": "Repeating the same search with identical args.", "redirect": "Stop re-querying; synthesize an answer from the results you already have.", "llmCalls": 10 } }
658
+
659
+ // finalize — the run was forced to wrap up on a tools-disabled turn
660
+ { "seq": 25, "type": "supervisor",
661
+ "data": { "action": "finalize", "reason": "Enough evidence to answer; further tool use is unlikely to help.", "llmCalls": 15 } }
662
+ ```
663
+
664
+ | Field | Type | Notes |
665
+ | ---------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
666
+ | `action` | string | One of `"on_track"`, `"redirect"`, `"finalize"`. |
667
+ | `reason` | string | One- or two-sentence explanation from the judge. |
668
+ | `redirect` | string | Present when `action === "redirect"`: the steering message injected into the conversation (same text the agent sees as a user message). Omitted for `on_track` / `finalize`. |
669
+ | `llmCalls` | integer | Number of LLM calls (`completeTurn` invocations) completed when this review fired. Matches the pipeline's `modelInvocations` counter at the check boundary. |
670
+
671
+ Observability for the run-supervisor guard (see §8.4). The event fires on
672
+ **every** check, not only when the judge intervenes — `on_track` reviews are
673
+ included so SDK clients can show "supervisor reviewed" activity without
674
+ inferring it from missing events.
675
+
676
+ When `action` is `redirect` or `finalize`, the pipeline has already applied
677
+ the verdict by the time this event arrives: a steering user message was
678
+ appended (`redirect`) or the next turn was forced tools-disabled
679
+ (`finalize`). SDK clients should render a status note and **not** try to
680
+ steer the run themselves.
681
+
682
+ Pass `"supervisor": false` in the spec (§8.4) to disable the platform judge
683
+ for a run. Omission keeps the runtime default (supervisor **enabled** on
684
+ ephemeral runs).
685
+
686
+ ### 4.8 Terminal events
644
687
 
645
688
  ```jsonc
646
689
  // Every terminal `result` and `error` event also carries `tokens`, `turns`,
@@ -692,7 +735,7 @@ SDK can re-fetch via `GET /agent-runs/:runId` will have:
692
735
  | `error` | Same string as `data.error`. |
693
736
  | `failureReason` | `{ "errorClass": "truncation", "finishReason": "max_tokens" }` (JSON object, future-proof for additional triage fields). |
694
737
 
695
- ### 4.7.1 Cost-attribution fields (`tokens`, `turns`, `model`)
738
+ ### 4.8.1 Cost-attribution fields (`tokens`, `turns`, `model`)
696
739
 
697
740
  Every terminal `result` and `error` event carries three additional
698
741
  fields so callers can drive cost dashboards, per-turn budgets, and
@@ -1030,20 +1073,67 @@ banners without re-parsing tool-result bodies:
1030
1073
  - `loop_detected` — fired on the soft nudge and again on the hard cutoff
1031
1074
  if reached. See §4.5.
1032
1075
  - `tool_budget_exceeded` — fired each time a call is intercepted. See §4.6.
1076
+ - `supervisor` — fired on every run-supervisor review (`on_track`,
1077
+ `redirect`, or `finalize`). See §4.7.
1078
+
1079
+ Both guard events (`loop_detected`, `tool_budget_exceeded`) are
1080
+ observability-only: the server has already substituted the synthetic
1081
+ tool-result / steering nudge by the time the SDK sees the event. The
1082
+ `supervisor` event is also observability-only when `action` is
1083
+ `redirect` / `finalize` — the pipeline already applied the verdict. The
1084
+ run continues to its terminal `result` / `error` / `cancelled` as usual.
1085
+
1086
+ ### 8.4 `supervisor` (run judge)
1087
+
1088
+ An optional LLM **run supervisor** periodically reviews the agent's
1089
+ transcript (reasoning, tool calls, tool results, visible text) and may
1090
+ steer the run:
1091
+
1092
+ | Verdict | Server action |
1093
+ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
1094
+ | `on_track` | No-op — the run continues unchanged. |
1095
+ | `redirect` | A steering **user message** is injected; tools stay available on the next turn. |
1096
+ | `finalize` | The next turn is forced **tools-disabled** so the run lands a clean final answer (optionally prefaced by the supervisor's message). |
1097
+
1098
+ Reviews fire every **`interval` LLM calls** (`completeTurn` invocations),
1099
+ measured at the bottom of tool-emitting rounds. Default interval is **5**
1100
+ when the field is omitted.
1101
+
1102
+ ```jsonc
1103
+ "supervisor": {
1104
+ "interval": 5 // optional — LLM calls between reviews; default 5
1105
+ }
1106
+
1107
+ // or:
1108
+ "supervisor": false // explicitly disable the platform judge for this run
1109
+ ```
1110
+
1111
+ | Field | Type | Notes |
1112
+ | ---------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
1113
+ | `interval` | integer ≥ 1 | Optional. Default **5** when omitted. Capped at **100** server-side. |
1114
+ | (literal `false`) | `false` | Disables the run supervisor for this run. Loop detection and tool budgets still apply. |
1115
+
1116
+ **Defaults.** When `supervisor` is **omitted**, MANTYX enables the platform
1117
+ LLM judge on ephemeral runs (web chat enables it separately via the chat
1118
+ runner). Pass `"supervisor": false` to opt out.
1119
+
1120
+ **SDK-only runs.** When a caller uses `@mantyx/ts-sdk` directly (not via
1121
+ `POST /agent-runs`), the supervisor is **off unless explicitly configured**:
1122
+ pass a `RunAgentSupervisor` object with a `review` callback to enable it, or
1123
+ pass `supervisor: false` (or omit the field) to keep it disabled. The wire
1124
+ field above controls the **platform-hosted** judge on ephemeral API runs only.
1033
1125
 
1034
- Both events are observability-only: the server has already substituted
1035
- the synthetic tool-result / steering nudge by the time the SDK sees the
1036
- event. The run continues to its terminal `result` / `error` / `cancelled`
1037
- as usual.
1126
+ Each review emits a SSE `supervisor` event (§4.7). Supervisor LLM usage is
1127
+ recorded under the `supervisor` usage surface for cost attribution.
1038
1128
 
1039
- ### 8.4 Session inheritance
1129
+ ### 8.5 Session inheritance
1040
1130
 
1041
- Like `reasoningLevel` and `outputSchema`, both fields support
1131
+ Like `reasoningLevel` and `outputSchema`, the run-guard fields support
1042
1132
  session-default + per-message override:
1043
1133
 
1044
- - `POST /agent-sessions { loopDetection, toolBudgets }` — sets the
1134
+ - `POST /agent-sessions { loopDetection, toolBudgets, supervisor }` — sets the
1045
1135
  session-default applied to every subsequent message run.
1046
- - `POST /agent-sessions/:id/messages { loopDetection, toolBudgets }` —
1136
+ - `POST /agent-sessions/:id/messages { loopDetection, toolBudgets, supervisor }` —
1047
1137
  optional per-message override. Applies to that one run only and does
1048
1138
  not mutate the session's stored value.
1049
1139
 
@@ -1215,17 +1305,17 @@ A reference SDK should:
1215
1305
  source-of-truth schema (Zod / Pydantic / etc.) — the server enforces
1216
1306
  JSON shape via the provider, but transient model errors can still
1217
1307
  produce strings that fail to parse in rare cases.
1218
- - [ ] Accept `loopDetection` and `toolBudgets` from the caller and pass
1219
- them through unchanged (see §8). Both are _additive_ — omitting
1220
- them keeps the runtime defaults; passing `loopDetection: false` opts
1221
- out; passing `toolBudgets: {}` clears the defaults; passing entries
1222
- layers caller overrides on top of the defaults. Do **not** translate
1223
- to vendor-specific knobs.
1224
- - [ ] Treat `loop_detected` and `tool_budget_exceeded` SSE events as
1225
- observability-only (see §4.5 / §4.6). Surface them as status notes
1226
- / log lines / telemetry — the server already substituted the
1227
- synthetic tool-results / steering nudges, so the SDK should keep
1228
- consuming the stream until the terminal event lands.
1308
+ - [ ] Accept `loopDetection`, `toolBudgets`, and `supervisor` from the caller
1309
+ and pass them through unchanged (see §8). All three are _additive_ —
1310
+ omitting them keeps the runtime defaults; passing `loopDetection: false`
1311
+ or `supervisor: false` opts out; passing `toolBudgets: {}` clears the
1312
+ defaults; passing entries layers caller overrides on top of the defaults.
1313
+ Do **not** translate to vendor-specific knobs.
1314
+ - [ ] Treat `loop_detected`, `tool_budget_exceeded`, and `supervisor` SSE
1315
+ events as observability-only (see §4.5 / §4.6 / §4.7). Surface them as
1316
+ status notes / log lines / telemetry — the server already substituted
1317
+ synthetic tool-results / steering nudges / supervisor verdicts, so the
1318
+ SDK should keep consuming the stream until the terminal event lands.
1229
1319
  - [ ] Maintain three local-callback registries (or one tagged-union
1230
1320
  registry), keyed by `name`: - generic local tools (`kind: "local"`), - local A2A peers (`kind: "a2a_local"`, indexed by some Agent Card
1231
1321
  field — typically `agentCard.url`), - local MCP servers (`kind: "mcp_local"`, indexed by the SDK-side
@@ -1262,4 +1352,4 @@ A reference SDK should:
1262
1352
  - [A2A spec](https://google.github.io/A2A/specification/) — canonical
1263
1353
  Agent Card schema.
1264
1354
  - [MCP spec](https://spec.modelcontextprotocol.io/) — canonical `Tool` and
1265
- `Implementation` shapes.
1355
+ `Implementation` shapes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantyx/sdk",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "MANTYX as a hosted agent runtime: define ephemeral agents, mix server-side MANTYX tools with locally-executed tools, run them remotely.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",