@ai-matrx/agents 0.43.18 → 0.43.20

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/content-transfer/index.cjs +26 -10
  3. package/dist/content-transfer/index.cjs.map +1 -1
  4. package/dist/content-transfer/index.js +26 -10
  5. package/dist/content-transfer/index.js.map +1 -1
  6. package/dist/content-transfer/react/index.cjs +26 -10
  7. package/dist/content-transfer/react/index.cjs.map +1 -1
  8. package/dist/content-transfer/react/index.js +26 -10
  9. package/dist/content-transfer/react/index.js.map +1 -1
  10. package/dist/generated/api-types.cjs.map +1 -1
  11. package/dist/generated/api-types.d.cts +16 -0
  12. package/dist/generated/api-types.d.ts +16 -0
  13. package/dist/index.cjs +75 -74
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +75 -74
  18. package/dist/index.js.map +1 -1
  19. package/dist/{keys.generated-BzkXY1sO.d.cts → keys.generated-BuyboLUH.d.cts} +4 -4
  20. package/dist/{keys.generated-BzkXY1sO.d.ts → keys.generated-BuyboLUH.d.ts} +4 -4
  21. package/dist/mandates/index.cjs +2 -2
  22. package/dist/mandates/index.cjs.map +1 -1
  23. package/dist/mandates/index.d.cts +2 -2
  24. package/dist/mandates/index.d.ts +2 -2
  25. package/dist/mandates/index.js +2 -2
  26. package/dist/mandates/index.js.map +1 -1
  27. package/dist/matrx/index.cjs +75 -74
  28. package/dist/matrx/index.cjs.map +1 -1
  29. package/dist/matrx/index.d.cts +1 -1
  30. package/dist/matrx/index.d.ts +1 -1
  31. package/dist/matrx/index.js +75 -74
  32. package/dist/matrx/index.js.map +1 -1
  33. package/dist/portable/index.cjs +26 -10
  34. package/dist/portable/index.cjs.map +1 -1
  35. package/dist/portable/index.js +26 -10
  36. package/dist/portable/index.js.map +1 -1
  37. package/dist/portable/mcp.cjs +24 -10
  38. package/dist/portable/mcp.cjs.map +1 -1
  39. package/dist/portable/mcp.js +24 -10
  40. package/dist/portable/mcp.js.map +1 -1
  41. package/dist/react/index.cjs +60 -46
  42. package/dist/react/index.cjs.map +1 -1
  43. package/dist/react/index.js +60 -46
  44. package/dist/react/index.js.map +1 -1
  45. package/generated/api-types.ts +16 -0
  46. package/mandates/snapshots/keys.0.43.19.json +653 -0
  47. package/mandates/snapshots/keys.0.43.20.json +653 -0
  48. package/package.json +3 -3
@@ -29,6 +29,21 @@ __export(portable_exports, {
29
29
  });
30
30
  module.exports = __toCommonJS(portable_exports);
31
31
 
32
+ // matrx/backend-errors.ts
33
+ var GENERIC_MESSAGE_PATTERNS = [
34
+ /failed unexpectedly/i,
35
+ /^\s*something went wrong/i,
36
+ /please try again(\s+later)?\.?\s*$/i,
37
+ /^\s*request failed\b/i,
38
+ /^\s*unknown (streaming )?error/i,
39
+ /^\s*internal server error\.?\s*$/i
40
+ ];
41
+ function isGenericUserMessage(message) {
42
+ const value = (message ?? "").trim();
43
+ if (!value) return true;
44
+ return GENERIC_MESSAGE_PATTERNS.some((pattern) => pattern.test(value));
45
+ }
46
+
32
47
  // matrx/transport.ts
33
48
  var MatrxApiError = class extends Error {
34
49
  name = "MatrxApiError";
@@ -58,10 +73,12 @@ function nonBlankString(value) {
58
73
  }
59
74
  function extractMatrxErrorMessage(serverDetail) {
60
75
  if (!isRecord(serverDetail)) return void 0;
61
- const userMessage = nonBlankString(serverDetail.user_message);
62
- if (userMessage) return userMessage;
63
- const message = nonBlankString(serverDetail.message);
64
- if (message) return message;
76
+ const candidates = [];
77
+ const push = (value) => {
78
+ if (value) candidates.push(value);
79
+ };
80
+ push(nonBlankString(serverDetail.user_message));
81
+ push(nonBlankString(serverDetail.message));
65
82
  if (Array.isArray(serverDetail.details)) {
66
83
  const messages = serverDetail.details.map((entry) => {
67
84
  if (!isRecord(entry)) return void 0;
@@ -70,21 +87,20 @@ function extractMatrxErrorMessage(serverDetail) {
70
87
  const field = nonBlankString(entry.field);
71
88
  return field ? `${field}: ${detailMessage}` : detailMessage;
72
89
  }).filter((m) => typeof m === "string");
73
- if (messages.length > 0) return messages.join("; ");
90
+ if (messages.length > 0) push(messages.join("; "));
74
91
  }
75
92
  const detail = serverDetail.detail;
76
93
  if (isRecord(detail)) {
77
- const detailMessage = nonBlankString(detail.message) ?? nonBlankString(detail.user_message);
78
- if (detailMessage) return detailMessage;
94
+ push(nonBlankString(detail.message) ?? nonBlankString(detail.user_message));
79
95
  }
80
- if (typeof detail === "string" && detail.trim()) return detail;
96
+ if (typeof detail === "string" && detail.trim()) push(detail);
81
97
  if (Array.isArray(detail)) {
82
98
  const messages = detail.map(
83
99
  (entry) => isRecord(entry) ? nonBlankString(entry.msg) : void 0
84
100
  ).filter((m) => typeof m === "string");
85
- if (messages.length > 0) return messages.join("; ");
101
+ if (messages.length > 0) push(messages.join("; "));
86
102
  }
87
- return void 0;
103
+ return candidates.find((c) => !isGenericUserMessage(c)) ?? candidates[0];
88
104
  }
89
105
  function extractMatrxErrorCode(serverDetail) {
90
106
  if (!isRecord(serverDetail)) return null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../portable/index.ts","../../matrx/transport.ts","../../matrx/internal.ts","../../portable/types.generated.ts"],"sourcesContent":["/**\n * `@ai-matrx/agents/portable` — an AI Matrx agent as pieces a coding CLI loads.\n *\n * `fetchPortableAgent` reads `POST /ai/agents/{id}/portable` (or the version\n * door) and returns the server's `PortableAgentBundle`: the instructions with\n * the person's variable values filled, the skills as Claude `SKILL.md` files,\n * the server tools in MCP shape, and `unavailable[]` — what could not be\n * carried and why. The types are GENERATED from the server's Pydantic models\n * (`types.generated.ts`, `uv run python scripts/generate_types.py portable-agent`\n * in aidream), so the two cannot drift.\n *\n * Pure entry: no React, no Node, no I/O at import. Credentials and the\n * organization travel through the host's `MatrxTransport`\n * (`createMatrxTransport({ credentials, organizationId })`).\n */\n\nimport { encodePathSegment, requestJson } from \"../matrx/internal\";\nimport type { MatrxTransport } from \"../matrx/transport\";\nimport type {\n JsonValue,\n PortableAgentBundle,\n PortableAgentRequest,\n PortableAgentSummary,\n} from \"./types.generated\";\n\nexport * from \"./types.generated\";\n\n/** Which definition to load: the agent's current one, or one immutable version. */\nexport type PortableAgentTarget =\n | { agentId: string; versionId?: undefined }\n | { versionId: string; agentId?: string | undefined };\n\nexport interface FetchPortableAgentOptions {\n /** The person's values, by variable name. Unfilled variables use the agent's defaults. */\n variables?: Record<string, JsonValue>;\n /** The `ui.ui_surface` tools resolve on. Default: the server's coding-session surface. */\n surface?: string;\n signal?: AbortSignal;\n}\n\n/** The server path for a target — exported so a host's tests can assert it. */\nexport function portableAgentPath(target: PortableAgentTarget): string {\n return target.versionId\n ? `/ai/agents/versions/${encodePathSegment(target.versionId)}/portable`\n : `/ai/agents/${encodePathSegment(target.agentId ?? \"\")}/portable`;\n}\n\n/**\n * Load one agent as a portable bundle. Throws `MatrxApiError` on refusal —\n * 401 signed out, 404 not found or not shared with this person.\n */\nexport function fetchPortableAgent(\n transport: MatrxTransport,\n target: PortableAgentTarget,\n options: FetchPortableAgentOptions = {},\n): Promise<PortableAgentBundle> {\n const body: PortableAgentRequest = {\n variables: options.variables ?? {},\n ...(options.surface ? { surface: options.surface } : {}),\n };\n return requestJson<PortableAgentBundle>(transport, portableAgentPath(target), {\n method: \"POST\",\n body,\n ...(options.signal ? { signal: options.signal } : {}),\n });\n}\n\n/**\n * The light read behind an agent picker (`GET /ai/agents/{id}/portable/summary`):\n * name, variable declarations, versions newest first with their hashes. No\n * skills, no tools — call this when a person CHOOSES an agent, and\n * `fetchPortableAgent` only when a session starts.\n */\nexport function fetchPortableAgentSummary(\n transport: MatrxTransport,\n agentId: string,\n options: { signal?: AbortSignal } = {},\n): Promise<PortableAgentSummary> {\n return requestJson<PortableAgentSummary>(\n transport,\n `/ai/agents/${encodePathSegment(agentId)}/portable/summary`,\n { method: \"GET\", ...(options.signal ? { signal: options.signal } : {}) },\n );\n}\n\n/** One saved version of an agent (`AgentVersionInfo` server-side). */\nexport interface PortableAgentVersion {\n id: string;\n agent_id: string;\n version_number: number;\n name: string;\n change_note: string | null;\n changed_at: string | null;\n}\n\n/**\n * The agent's saved versions, oldest first (`GET /agent-service/agents/{id}/versions`,\n * authorized through the same viewer rule as the agent). Feeds a version menu.\n */\nexport function fetchPortableAgentVersions(\n transport: MatrxTransport,\n agentId: string,\n options: { signal?: AbortSignal } = {},\n): Promise<PortableAgentVersion[]> {\n return requestJson<PortableAgentVersion[]>(\n transport,\n `/agent-service/agents/${encodePathSegment(agentId)}/versions`,\n { method: \"GET\", ...(options.signal ? { signal: options.signal } : {}) },\n );\n}\n\n/** What a bundle loaded, counted — what a header chip shows. */\nexport interface PortableAgentLoadSummary {\n instructions: boolean;\n skills: { loaded: number; total: number };\n tools: { loaded: number; total: number };\n}\n\n/** Count what a bundle carries against what the agent declares. */\nexport function summarizePortableAgent(bundle: PortableAgentBundle): PortableAgentLoadSummary {\n const missingSkills = bundle.unavailable.filter((u) => u.kind === \"skill\").length;\n const missingTools = bundle.unavailable.filter((u) => u.kind === \"tool\").length;\n return {\n instructions: bundle.instructions.trim().length > 0,\n skills: { loaded: bundle.skills.length, total: bundle.skills.length + missingSkills },\n tools: { loaded: bundle.tools.length, total: bundle.tools.length + missingTools },\n };\n}\n","/**\n * `@ai-matrx/agents/matrx` — the Matrx transport port.\n *\n * The ONE seam between this package's wire semantics and a host's connection\n * policy. This package owns WHAT is said to the AI Matrx server — paths,\n * methods, JSON bodies, streaming Accept headers, the `Last-Event-ID` cursor\n * header — and the host owns HOW the connection is made:\n *\n * - base-URL / backend-channel resolution (global, sandbox override, local\n * engine, EC2-dedicated — whatever ladder the host runs);\n * - credentials (Supabase JWT `Authorization: Bearer`, guest\n * `X-Fingerprint-ID`, or the API-key lane) — NEVER implemented here;\n * - the `X-Organization-Id` context header;\n * - retry policy, network-level timeouts, and diagnostics capture.\n *\n * A host implements the port in a few lines:\n *\n * ```ts\n * const transport: MatrxTransport = {\n * fetch: (path, init) =>\n * fetch(`${baseUrl}${path}`, {\n * ...init,\n * headers: { ...init.headers, ...authHeaders() },\n * }),\n * };\n * ```\n *\n * The port is deliberately structural and `fetch`-shaped so `@ai-matrx/data`'s\n * `/api` transport can implement it without importing this package.\n */\n\n/**\n * The request this package hands the port. A strict subset of `RequestInit`,\n * so a host can spread it straight into `fetch`.\n */\nexport interface MatrxTransportRequest {\n method: \"GET\" | \"POST\";\n /**\n * Wire-semantic headers the CALL requires (`Content-Type`, `Accept`,\n * `Last-Event-ID`). The host merges its policy headers (auth, org) on top;\n * it must not drop these.\n */\n headers: Record<string, string>;\n /** Pre-serialized JSON body, present on POST calls that carry one. */\n body?: string;\n /** Caller cancellation. The host must wire it to the underlying fetch. */\n signal?: AbortSignal;\n}\n\n/**\n * The transport port. `path` is server-relative and always starts with `/`\n * (`/ai/...`, `/runtime/...`); the host prepends its resolved base URL.\n */\nexport interface MatrxTransport {\n fetch(path: string, init: MatrxTransportRequest): Promise<Response>;\n}\n\n/**\n * A non-2xx response from the Matrx API, with the server's structured error\n * body preserved and its richest human-readable message extracted.\n */\nexport class MatrxApiError extends Error {\n override readonly name = \"MatrxApiError\";\n /** HTTP status of the failed response. */\n readonly status: number;\n /** Machine code from the server body (`code`, or `detail.code`), when present. */\n readonly code: string | null;\n /** The parsed server error body, verbatim (undefined when unparsable). */\n readonly serverDetail: unknown;\n /** The request path the failure came from (server-relative). */\n readonly path: string;\n\n constructor(args: {\n status: number;\n path: string;\n serverDetail?: unknown;\n message?: string;\n }) {\n super(\n args.message ??\n extractMatrxErrorMessage(args.serverDetail) ??\n `HTTP ${args.status}`,\n );\n this.status = args.status;\n this.path = args.path;\n this.serverDetail = args.serverDetail;\n this.code = extractMatrxErrorCode(args.serverDetail);\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction nonBlankString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\n/**\n * Extract the richest human-readable message from a Matrx/FastAPI error body.\n *\n * aidream 4xx validation errors look like\n * `{ error, user_message, details: [{ field, message, help }] }`; hand-raised\n * HTTPExceptions carry `{ detail: { code, message } }`; FastAPI's defaults are\n * `{ detail: string | [{ msg }] }`. Preference order: `user_message` →\n * `message` → joined `details[].message` → `detail.message` →\n * `detail` string → joined `detail[].msg`. Returns undefined for\n * unrecognized bodies so callers fall back to the bare status line.\n */\nexport function extractMatrxErrorMessage(\n serverDetail: unknown,\n): string | undefined {\n if (!isRecord(serverDetail)) return undefined;\n\n const userMessage = nonBlankString(serverDetail.user_message);\n if (userMessage) return userMessage;\n const message = nonBlankString(serverDetail.message);\n if (message) return message;\n\n if (Array.isArray(serverDetail.details)) {\n const messages = serverDetail.details\n .map((entry: unknown) => {\n if (!isRecord(entry)) return undefined;\n const detailMessage = nonBlankString(entry.message);\n if (!detailMessage) return undefined;\n const field = nonBlankString(entry.field);\n return field ? `${field}: ${detailMessage}` : detailMessage;\n })\n .filter((m): m is string => typeof m === \"string\");\n if (messages.length > 0) return messages.join(\"; \");\n }\n\n const detail = serverDetail.detail;\n if (isRecord(detail)) {\n const detailMessage =\n nonBlankString(detail.message) ?? nonBlankString(detail.user_message);\n if (detailMessage) return detailMessage;\n }\n if (typeof detail === \"string\" && detail.trim()) return detail;\n if (Array.isArray(detail)) {\n const messages = detail\n .map((entry: unknown) =>\n isRecord(entry) ? nonBlankString(entry.msg) : undefined,\n )\n .filter((m): m is string => typeof m === \"string\");\n if (messages.length > 0) return messages.join(\"; \");\n }\n return undefined;\n}\n\n/**\n * Extract the machine error code from a Matrx error body: top-level `code`,\n * else `detail.code` (the hand-raised HTTPException shape). Null when absent.\n */\nexport function extractMatrxErrorCode(serverDetail: unknown): string | null {\n if (!isRecord(serverDetail)) return null;\n const topLevel = nonBlankString(serverDetail.code);\n if (topLevel) return topLevel;\n const detail = serverDetail.detail;\n if (isRecord(detail)) {\n const nested = nonBlankString(detail.code);\n if (nested) return nested;\n }\n return null;\n}\n","/**\n * Internal request plumbing for `@ai-matrx/agents/matrx`. Not part of the\n * public surface — `matrx/index.ts` deliberately does not re-export this\n * module. Everything here is pure: no globals, no work at import time.\n */\n\nimport {\n readMatrxNdjsonStream,\n type MatrxNdjsonIssue,\n type MatrxStreamEnvelope,\n type MatrxStreamEnvelopeObservation,\n} from \"../stream/ndjson\";\nimport { MatrxApiError, type MatrxTransport } from \"./transport\";\n\n/** Encode one path segment (an id) safely into a server-relative path. */\nexport function encodePathSegment(value: string): string {\n return encodeURIComponent(value);\n}\n\nexport type QueryValue =\n | string\n | number\n | boolean\n | readonly string[]\n | undefined;\n\n/**\n * Build a query string. Array values repeat the key (`kind=a&kind=b` — the\n * FastAPI repeatable-parameter convention); undefined values are omitted.\n * Returns \"\" or a string starting with \"?\".\n */\nexport function buildQuery(params: Record<string, QueryValue>): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const entry of value) search.append(key, entry);\n } else {\n search.append(key, String(value));\n }\n }\n const encoded = search.toString();\n return encoded ? `?${encoded}` : \"\";\n}\n\nasync function readServerDetail(response: Response): Promise<unknown> {\n try {\n return (await response.json()) as unknown;\n } catch {\n return undefined;\n }\n}\n\nasync function throwApiError(path: string, response: Response): Promise<never> {\n throw new MatrxApiError({\n status: response.status,\n path,\n serverDetail: await readServerDetail(response),\n });\n}\n\nexport interface JsonRequestOptions {\n method: \"GET\" | \"POST\";\n body?: unknown;\n signal?: AbortSignal;\n}\n\n/**\n * Execute a JSON request through the transport. Throws `MatrxApiError` on a\n * non-2xx response; resolves with the parsed JSON body otherwise.\n */\nexport async function requestJson<T>(\n transport: MatrxTransport,\n path: string,\n options: JsonRequestOptions,\n): Promise<T> {\n const hasBody = options.method !== \"GET\" && options.body !== undefined;\n const response = await transport.fetch(path, {\n method: options.method,\n headers: hasBody ? { \"Content-Type\": \"application/json\" } : {},\n ...(hasBody ? { body: JSON.stringify(options.body) } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n });\n if (!response.ok) return throwApiError(path, response);\n return (await response.json()) as T;\n}\n\n/**\n * Options for every streaming call, riding the NDJSON kernel's contract.\n * Public via `./run`'s re-export.\n */\nexport interface MatrxStreamCallOptions {\n /** Abort the fetch and end the events iterator. */\n signal?: AbortSignal;\n /** Bounded background read-ahead (see `stream/ndjson`). */\n maxReadAhead?: number;\n /** Malformed NDJSON is non-fatal but must never disappear silently. */\n onMalformedLine?: (issue: MatrxNdjsonIssue) => void;\n /** Valid JSON with no recognized Matrx envelope. */\n onUnknownEnvelope?: (value: unknown) => void;\n /** Observe every valid envelope in its exact wire form. */\n onValidEnvelope?: (observation: MatrxStreamEnvelopeObservation) => void;\n}\n\n/**\n * A live agent run: the server-assigned ids (from response headers, available\n * BEFORE any event) and the normalized event stream. Public via `./run`.\n */\nexport interface MatrxRunHandle {\n /** `X-Request-ID` — the ONLY id `POST /ai/cancel/{request_id}` accepts. */\n requestId: string | null;\n /** `X-Conversation-ID` — the server's conversation identity. */\n conversationId: string | null;\n /** Normalized `{event, data}` envelopes through the ONE wire kernel. */\n events: AsyncGenerator<MatrxStreamEnvelope, void, undefined>;\n /** The raw response, for hosts that need headers/status beyond the ids. */\n response: Response;\n}\n\n/**\n * Wrap a validated streaming Response into the run handle — the ONE place\n * the id headers are read and the NDJSON kernel is attached (`./run` and\n * `./operations`' rejoin share it).\n */\nexport function toRunHandle(\n response: Response,\n options: MatrxStreamCallOptions,\n): MatrxRunHandle {\n return {\n requestId: response.headers.get(\"X-Request-ID\"),\n conversationId: response.headers.get(\"X-Conversation-ID\"),\n events: readMatrxNdjsonStream(response.body as ReadableStream<Uint8Array>, {\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.maxReadAhead !== undefined\n ? { maxReadAhead: options.maxReadAhead }\n : {}),\n ...(options.onMalformedLine\n ? { onMalformedLine: options.onMalformedLine }\n : {}),\n ...(options.onUnknownEnvelope\n ? { onUnknownEnvelope: options.onUnknownEnvelope }\n : {}),\n ...(options.onValidEnvelope\n ? { onValidEnvelope: options.onValidEnvelope }\n : {}),\n }),\n response,\n };\n}\n\nexport interface StreamRequestOptions {\n method: \"GET\" | \"POST\";\n body?: unknown;\n /** Extra wire-semantic headers (`Accept`, `Last-Event-ID`). */\n headers?: Record<string, string>;\n signal?: AbortSignal;\n}\n\n/**\n * Execute a streaming request. Throws `MatrxApiError` on a non-2xx response\n * (reading the error body as JSON when possible) or when a 2xx response\n * carries no body; resolves with the validated `Response` otherwise.\n */\nexport async function requestStream(\n transport: MatrxTransport,\n path: string,\n options: StreamRequestOptions,\n): Promise<Response> {\n const hasBody = options.method !== \"GET\" && options.body !== undefined;\n const response = await transport.fetch(path, {\n method: options.method,\n headers: {\n ...(hasBody ? { \"Content-Type\": \"application/json\" } : {}),\n ...options.headers,\n },\n ...(hasBody ? { body: JSON.stringify(options.body) } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n });\n if (!response.ok) return throwApiError(path, response);\n if (!response.body) {\n throw new MatrxApiError({\n status: response.status,\n path,\n serverDetail: { code: \"missing_response_body\" },\n message: \"The streaming response carried no body.\",\n });\n }\n return response;\n}\n","// AUTO-GENERATED — do not edit by hand.\n// Source: aidream/services/agent_service/portable.py\n// Run: uv run python scripts/generate_types.py portable-agent\n\nexport const PORTABLE_AGENT_CONTRACT = \"portable-agent.v1\";\n\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\nexport interface PortableAgentRequest {\n variables?: Record<string, JsonValue>;\n surface?: string;\n}\n\nexport interface PortableSkill {\n id: string;\n tier: \"included\" | \"listed\";\n slug: string;\n label: string;\n description: string;\n path: string;\n content: string;\n body: string;\n content_hash: string;\n}\n\nexport interface PortableTool {\n name: string;\n canonical_name: string;\n description: string;\n parameters: Record<string, JsonValue>;\n}\n\nexport interface PortableUnavailable {\n kind: \"tool\" | \"skill\" | \"variable\" | \"message\" | \"mcp_server\" | \"tools\";\n name: string;\n reason: string;\n}\n\nexport interface PortableAgentBundle {\n contract: \"portable-agent.v1\";\n agent_id: string;\n version_id: string | null;\n version_number: number | null;\n is_version: boolean;\n name: string;\n definition_hash: string;\n model_id: string;\n instructions: string;\n variables: Record<string, JsonValue>[];\n filled_variables: string[];\n skills: PortableSkill[];\n tools: PortableTool[];\n surface: string;\n unavailable: PortableUnavailable[];\n}\n\nexport interface PortableAgentVersion {\n id: string;\n version_number: number;\n changed_at: string | null;\n definition_hash: string | null;\n}\n\nexport interface PortableAgentSummary {\n agent_id: string;\n name: string;\n version_id: string | null;\n version_number: number | null;\n definition_hash: string;\n variables: Record<string, JsonValue>[];\n versions: PortableAgentVersion[];\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../portable/index.ts","../../matrx/backend-errors.ts","../../matrx/transport.ts","../../matrx/internal.ts","../../portable/types.generated.ts"],"sourcesContent":["/**\n * `@ai-matrx/agents/portable` — an AI Matrx agent as pieces a coding CLI loads.\n *\n * `fetchPortableAgent` reads `POST /ai/agents/{id}/portable` (or the version\n * door) and returns the server's `PortableAgentBundle`: the instructions with\n * the person's variable values filled, the skills as Claude `SKILL.md` files,\n * the server tools in MCP shape, and `unavailable[]` — what could not be\n * carried and why. The types are GENERATED from the server's Pydantic models\n * (`types.generated.ts`, `uv run python scripts/generate_types.py portable-agent`\n * in aidream), so the two cannot drift.\n *\n * Pure entry: no React, no Node, no I/O at import. Credentials and the\n * organization travel through the host's `MatrxTransport`\n * (`createMatrxTransport({ credentials, organizationId })`).\n */\n\nimport { encodePathSegment, requestJson } from \"../matrx/internal\";\nimport type { MatrxTransport } from \"../matrx/transport\";\nimport type {\n JsonValue,\n PortableAgentBundle,\n PortableAgentRequest,\n PortableAgentSummary,\n} from \"./types.generated\";\n\nexport * from \"./types.generated\";\n\n/** Which definition to load: the agent's current one, or one immutable version. */\nexport type PortableAgentTarget =\n | { agentId: string; versionId?: undefined }\n | { versionId: string; agentId?: string | undefined };\n\nexport interface FetchPortableAgentOptions {\n /** The person's values, by variable name. Unfilled variables use the agent's defaults. */\n variables?: Record<string, JsonValue>;\n /** The `ui.ui_surface` tools resolve on. Default: the server's coding-session surface. */\n surface?: string;\n signal?: AbortSignal;\n}\n\n/** The server path for a target — exported so a host's tests can assert it. */\nexport function portableAgentPath(target: PortableAgentTarget): string {\n return target.versionId\n ? `/ai/agents/versions/${encodePathSegment(target.versionId)}/portable`\n : `/ai/agents/${encodePathSegment(target.agentId ?? \"\")}/portable`;\n}\n\n/**\n * Load one agent as a portable bundle. Throws `MatrxApiError` on refusal —\n * 401 signed out, 404 not found or not shared with this person.\n */\nexport function fetchPortableAgent(\n transport: MatrxTransport,\n target: PortableAgentTarget,\n options: FetchPortableAgentOptions = {},\n): Promise<PortableAgentBundle> {\n const body: PortableAgentRequest = {\n variables: options.variables ?? {},\n ...(options.surface ? { surface: options.surface } : {}),\n };\n return requestJson<PortableAgentBundle>(transport, portableAgentPath(target), {\n method: \"POST\",\n body,\n ...(options.signal ? { signal: options.signal } : {}),\n });\n}\n\n/**\n * The light read behind an agent picker (`GET /ai/agents/{id}/portable/summary`):\n * name, variable declarations, versions newest first with their hashes. No\n * skills, no tools — call this when a person CHOOSES an agent, and\n * `fetchPortableAgent` only when a session starts.\n */\nexport function fetchPortableAgentSummary(\n transport: MatrxTransport,\n agentId: string,\n options: { signal?: AbortSignal } = {},\n): Promise<PortableAgentSummary> {\n return requestJson<PortableAgentSummary>(\n transport,\n `/ai/agents/${encodePathSegment(agentId)}/portable/summary`,\n { method: \"GET\", ...(options.signal ? { signal: options.signal } : {}) },\n );\n}\n\n/** One saved version of an agent (`AgentVersionInfo` server-side). */\nexport interface PortableAgentVersion {\n id: string;\n agent_id: string;\n version_number: number;\n name: string;\n change_note: string | null;\n changed_at: string | null;\n}\n\n/**\n * The agent's saved versions, oldest first (`GET /agent-service/agents/{id}/versions`,\n * authorized through the same viewer rule as the agent). Feeds a version menu.\n */\nexport function fetchPortableAgentVersions(\n transport: MatrxTransport,\n agentId: string,\n options: { signal?: AbortSignal } = {},\n): Promise<PortableAgentVersion[]> {\n return requestJson<PortableAgentVersion[]>(\n transport,\n `/agent-service/agents/${encodePathSegment(agentId)}/versions`,\n { method: \"GET\", ...(options.signal ? { signal: options.signal } : {}) },\n );\n}\n\n/** What a bundle loaded, counted — what a header chip shows. */\nexport interface PortableAgentLoadSummary {\n instructions: boolean;\n skills: { loaded: number; total: number };\n tools: { loaded: number; total: number };\n}\n\n/** Count what a bundle carries against what the agent declares. */\nexport function summarizePortableAgent(bundle: PortableAgentBundle): PortableAgentLoadSummary {\n const missingSkills = bundle.unavailable.filter((u) => u.kind === \"skill\").length;\n const missingTools = bundle.unavailable.filter((u) => u.kind === \"tool\").length;\n return {\n instructions: bundle.instructions.trim().length > 0,\n skills: { loaded: bundle.skills.length, total: bundle.skills.length + missingSkills },\n tools: { loaded: bundle.tools.length, total: bundle.tools.length + missingTools },\n };\n}\n","/**\n * `@ai-matrx/agents/matrx` — the AI Matrx server's error model, as every\n * client reads it: `BackendApiError` (the server's `APIError` body),\n * `StreamTransportError` (the socket died mid-run; the run may still finish\n * and is reattachable), parsing of HTTP / stream / persisted errors, and the\n * ONE sentence a person sees (`getUserMessage`, `describeBackendFailure`).\n *\n * Moved from matrx-frontend `lib/api/errors.ts` (chat-package independence\n * P9); the app re-exports it from here. Pure: no host, no window, no env.\n */\n\n/**\n * Standardized error shape returned by all backend endpoints.\n * Matches the Python `APIError` Pydantic model.\n */\nexport interface BackendApiErrorData {\n /** Machine-readable error code (e.g. \"auth_required\", \"validation_error\") */\n error: string;\n /** Developer-facing detail for debugging */\n message: string;\n /** Safe to display directly in the UI */\n user_message: string;\n /** Extra info (validation errors, etc.) */\n details: unknown | null;\n /** Unique request ID for support/debugging */\n request_id: string;\n}\n\n/** Common backend error codes */\nexport type BackendErrorCode =\n | \"auth_required\"\n | \"token_required\"\n | \"admin_required\"\n | \"validation_error\"\n | \"not_found\"\n | \"internal_error\"\n | \"agent_error\"\n /** The stream socket died mid-run; the server run may still be completing\n * and is reattachable. See `StreamTransportError`. */\n | \"stream_transport_lost\"\n | (string & {});\n\n// ============================================================================\n// ERROR CLASS\n// ============================================================================\n\n/**\n * Typed error thrown by all backend API operations.\n * Contains structured fields matching the Python APIError model.\n *\n * Usage:\n * ```typescript\n * try {\n * await client.post(ENDPOINTS.ai.agentStart(agentId), body);\n * } catch (err) {\n * if (err instanceof BackendApiError) {\n * // Show err.userMessage to the user\n * // Log err.requestId for debugging\n * // Check err.code for programmatic handling\n * }\n * }\n * ```\n */\nexport class BackendApiError extends Error {\n /** Machine-readable error code */\n readonly code: BackendErrorCode;\n /** Developer-facing detail */\n readonly detail: string;\n /** Safe to display directly in the UI */\n readonly userMessage: string;\n /** Extra info (validation errors, etc.) */\n readonly details: unknown | null;\n /** Unique request ID for support/debugging */\n readonly requestId: string;\n /** HTTP status code (if from an HTTP response) */\n readonly status: number | null;\n\n constructor(data: {\n code: BackendErrorCode;\n detail: string;\n userMessage: string;\n details?: unknown | null;\n requestId?: string | undefined;\n status?: number | null | undefined;\n }) {\n super(data.userMessage);\n this.name = \"BackendApiError\";\n this.code = data.code;\n this.detail = data.detail;\n this.userMessage = data.userMessage;\n this.details = data.details ?? null;\n // MATRX-EXCEPTION: requestId is genuinely optional (constructor param);\n // \"\" means \"no request id available\" — a display/log field, not persisted.\n this.requestId = data.requestId ?? \"\";\n this.status = data.status ?? null;\n }\n\n /** Convert to the wire format for logging */\n toJSON(): BackendApiErrorData {\n return {\n error: this.code,\n message: this.detail,\n user_message: this.userMessage,\n details: this.details,\n request_id: this.requestId,\n };\n }\n}\n\n/**\n * The socket carrying a live NDJSON stream broke mid-run.\n *\n * THE DISTINCTION THIS EXISTS TO MAKE: a backend that blows up mid-stream does\n * NOT break the socket — it emits a typed `error` event and closes the body\n * cleanly. So an exception escaping the body reader means the *transport* died,\n * not the run. And aidream streams run `detach_on_disconnect=True`: the server\n * keeps executing and persisting the turn after our connection goes away.\n *\n * The client therefore cannot decide locally whether the answer is lost — it\n * must ASK THE SERVER. That is what `resumable` means here: \"reattach by\n * requestId / conversationId / durable run id and let server truth settle it\",\n * never \"this succeeded\". A server that genuinely died reports `failed` on\n * reattach and the honest record replaces the optimistic copy.\n *\n * Consumers: `run-ai-stream.ts` (chat → `reconnectServerOperation`) and\n * `adopt-foreign-stream.ts` (pipeline runs → the surface's own rejoin).\n */\nexport class StreamTransportError extends BackendApiError {\n /** Always true — reattach and let the server settle the outcome. */\n readonly resumable = true as const;\n\n constructor(data: {\n detail: string;\n details?: unknown | null;\n requestId?: string;\n }) {\n super({\n code: \"stream_transport_lost\",\n detail: data.detail,\n userMessage:\n \"The connection dropped. Your run is still going on the server — reconnecting to it now.\",\n details: data.details ?? null,\n ...(data.requestId !== undefined ? { requestId: data.requestId } : {}),\n });\n this.name = \"StreamTransportError\";\n }\n}\n\n/**\n * True when a failure is a dropped transport rather than a failed run. Use this\n * instead of `instanceof` at boundaries that re-wrap errors (thunk rejections,\n * `callApi` result errors), where the class identity is lost but the code\n * survives.\n */\nexport function isStreamTransportLost(error: unknown): boolean {\n if (error instanceof StreamTransportError) return true;\n if (error instanceof BackendApiError) {\n return error.code === \"stream_transport_lost\";\n }\n if (error && typeof error === \"object\") {\n const code = (error as { code?: unknown }).code;\n if (code === \"stream_transport_lost\") return true;\n const errorType = (error as { error_type?: unknown }).error_type;\n if (errorType === \"transport_lost\") return true;\n }\n return false;\n}\n\n// ============================================================================\n// HTTP ERROR PARSER\n// ============================================================================\n\n/**\n * Parse a non-OK HTTP response into a BackendApiError.\n *\n * Handles the standardized backend shape and falls back gracefully\n * when the response isn't JSON or uses a legacy format.\n */\nexport async function parseHttpError(\n response: Response,\n): Promise<BackendApiError> {\n const status = response.status;\n let text: string;\n\n try {\n // A Response body can only be read once. Capture it before deciding\n // whether the backend sent a structured error or plain text.\n text = await response.text();\n } catch {\n return new BackendApiError({\n code: statusToCode(status),\n detail: `HTTP ${status}`,\n userMessage: `Request failed (${status})`,\n status,\n });\n }\n\n let body: Record<string, unknown> | null;\n try {\n body = JSON.parse(text) as Record<string, unknown> | null;\n } catch {\n return new BackendApiError({\n code: statusToCode(status),\n detail: text || `HTTP ${status}`,\n userMessage: text || `Request failed (${status})`,\n status,\n });\n }\n return parseHttpErrorBody(body, status);\n}\n\n/**\n * Parse an already-decoded JSON error body into a BackendApiError.\n *\n * Exported because XHR callers (upload/download progress paths) have the\n * parsed body in hand and MUST NOT hand-roll a shallower read: a private\n * copy in `python-client.ts` looked only at top-level `error`/`message`, so\n * FastAPI's `{\"detail\": {...}}` envelope — what every matrx-files 500 uses —\n * degraded to the useless `code: \"internal\", detail: \"HTTP 500\"`. That is\n * exactly how an upload failure with a real server-side cause reached the\n * user as `Upload failed (500)` and nothing else. One parser, every transport.\n */\nexport function parseHttpErrorBody(\n body: Record<string, unknown> | null,\n status: number,\n): BackendApiError {\n if (!body) {\n return new BackendApiError({\n code: statusToCode(status),\n detail: `HTTP ${status}`,\n userMessage: `Request failed (${status})`,\n status,\n });\n }\n // Standard backend shape: { error, message, user_message, details, request_id }\n if (typeof body.error === \"string\" && typeof body.user_message === \"string\") {\n return new BackendApiError({\n code: body.error as BackendErrorCode,\n detail: (body.message as string) || `HTTP ${status}`,\n userMessage: body.user_message as string,\n details: body.details ?? null,\n requestId:\n typeof body.request_id === \"string\" ? body.request_id : undefined,\n status,\n });\n }\n\n // Legacy: nested error object with user_visible_message\n if (typeof body.error === \"object\" && body.error !== null) {\n const errorObj = body.error as Record<string, unknown>;\n return new BackendApiError({\n code:\n (errorObj.type as string) ||\n (errorObj.error as string) ||\n statusToCode(status),\n detail: (errorObj.message as string) || `HTTP ${status}`,\n userMessage:\n (errorObj.user_message as string) ||\n (errorObj.user_visible_message as string) ||\n (errorObj.message as string) ||\n `Request failed (${status})`,\n details: errorObj.details ?? null,\n requestId:\n typeof errorObj.request_id === \"string\"\n ? errorObj.request_id\n : undefined,\n status,\n });\n }\n\n // FastAPI 422 validation shape: { detail: [{ loc, msg, type }, ...] }\n if (Array.isArray(body.detail)) {\n const first = body.detail[0] as Record<string, unknown> | undefined;\n const firstMsg = first && typeof first.msg === \"string\" ? first.msg : null;\n return new BackendApiError({\n code: statusToCode(status),\n detail: firstMsg\n ? `Validation error: ${firstMsg}`\n : JSON.stringify(body.detail),\n userMessage: firstMsg || `Request failed (${status})`,\n details: body.detail,\n status,\n });\n }\n\n // FastAPI HTTPException with structured detail: { detail: { code?, error?, message?, user_message?, ... } }\n if (\n typeof body.detail === \"object\" &&\n body.detail !== null &&\n !Array.isArray(body.detail)\n ) {\n const d = body.detail as Record<string, unknown>;\n const code =\n (typeof d.code === \"string\" && d.code) ||\n (typeof d.error === \"string\" && d.error) ||\n statusToCode(status);\n const message =\n (typeof d.message === \"string\" && d.message) ||\n (typeof d.detail === \"string\" && d.detail) ||\n `HTTP ${status}`;\n return new BackendApiError({\n code: code as BackendErrorCode,\n detail: message,\n userMessage:\n (typeof d.user_message === \"string\" && d.user_message) ||\n (typeof d.user_visible_message === \"string\" &&\n d.user_visible_message) ||\n message,\n details: d.details ?? d,\n requestId: typeof d.request_id === \"string\" ? d.request_id : undefined,\n status,\n });\n }\n\n // Legacy: flat { error: string, message: string } or { detail: string }\n return new BackendApiError({\n code: typeof body.error === \"string\" ? body.error : statusToCode(status),\n detail:\n (body.message as string) ||\n (body.detail as string) ||\n (body.error as string) ||\n `HTTP ${status}`,\n userMessage:\n (body.user_message as string) ||\n (body.user_visible_message as string) ||\n (body.message as string) ||\n (body.detail as string) ||\n `Request failed (${status})`,\n details: body.details ?? null,\n requestId:\n typeof body.request_id === \"string\" ? body.request_id : undefined,\n status,\n });\n}\n\n/**\n * Adapt callApi's result-style error into the same canonical error used by\n * direct fetch and streaming consumers.\n *\n * callApi intentionally returns errors instead of throwing them, but its\n * `serverDetail` contains the complete FastAPI body. Sending only\n * `error.message` to a feature discards that body and turns a precise\n * configuration failure into \"HTTP 422\". This adapter keeps one parser and\n * one human-facing explanation path across both client styles.\n */\nexport function parseCallApiError(error: {\n message: string;\n status?: number;\n serverDetail?: unknown;\n}): BackendApiError {\n const status = error.status ?? 500;\n const body =\n error.serverDetail &&\n typeof error.serverDetail === \"object\" &&\n !Array.isArray(error.serverDetail)\n ? (error.serverDetail as Record<string, unknown>)\n : { message: error.message };\n return parseHttpErrorBody(body, status);\n}\n\n// ============================================================================\n// STREAMING ERROR PARSER\n// ============================================================================\n\n/**\n * Parse streaming error event data into a BackendApiError.\n *\n * Handles both new format (`user_message`) and legacy (`user_visible_message`).\n */\nexport function parseStreamError(data: unknown): BackendApiError {\n if (!data || typeof data !== \"object\") {\n return new BackendApiError({\n code: \"internal_error\",\n detail: typeof data === \"string\" ? data : \"Unknown streaming error\",\n userMessage: typeof data === \"string\" ? data : \"Something went wrong\",\n });\n }\n\n const obj = data as Record<string, unknown>;\n const details =\n typeof obj.details === \"object\" && obj.details !== null\n ? (obj.details as Record<string, unknown>)\n : null;\n return new BackendApiError({\n code:\n (obj.code as string) ||\n (obj.error_type as string) ||\n (obj.error as string) ||\n \"internal_error\",\n detail: (obj.message as string) || \"Streaming error\",\n userMessage:\n (obj.user_message as string) ||\n (obj.message as string) ||\n \"Something went wrong\",\n details,\n requestId:\n typeof obj.request_id === \"string\"\n ? obj.request_id\n : typeof details?.request_id === \"string\"\n ? details.request_id\n : undefined,\n });\n}\n\n/**\n * Restore the canonical error shape from a durable backend run row.\n *\n * Provider ledgers often keep an aggregate summary plus a more specific first\n * child failure. Prefer that child so a page refresh does not turn a precise\n * streamed failure back into \"1 request failed\".\n */\nexport function parsePersistedBackendError(\n data: unknown,\n requestId = \"\",\n): BackendApiError | null {\n if (!data || typeof data !== \"object\") return null;\n const error = data as Record<string, unknown>;\n const failures = Array.isArray(error.failures) ? error.failures : [];\n const firstSpecific = failures.find(\n (failure): failure is Record<string, unknown> =>\n typeof failure === \"object\" &&\n failure !== null &&\n typeof (failure as Record<string, unknown>).message === \"string\",\n );\n const summary =\n typeof error.message === \"string\"\n ? error.message\n : \"Backend operation failed\";\n const specific =\n typeof firstSpecific?.message === \"string\"\n ? firstSpecific.message\n : summary;\n const detail = specific === summary ? summary : `${summary}: ${specific}`;\n const code = typeof error.type === \"string\" ? error.type : \"internal_error\";\n // A persisted failure may carry a human-facing line the technical detail\n // cannot express — the load-bearing case being a paid result that survived\n // its persistence failure (`WritePreservedError`, D183): \"it broke\" is true\n // and \"your work is preserved and will be recovered\" is what the user needs.\n // `describeBackendFailure` still overrides a TEMPLATED one with the cause.\n const userMessage =\n typeof error.user_message === \"string\" && error.user_message\n ? error.user_message\n : detail;\n return new BackendApiError({\n code,\n detail,\n userMessage,\n details: error,\n requestId,\n });\n}\n\n// ============================================================================\n// FAILURE EXPLANATION — never let a templated non-answer be the whole story\n// ============================================================================\n\n/**\n * Server messages that carry ZERO diagnostic value. The streaming layer\n * (`matrx-connect/streaming/response.py`) emits the first one for every\n * unclassified crash — \"CanonicalGscSync failed unexpectedly. Please try\n * again or adjust your settings.\" — while the REAL cause travels in the same\n * payload's `message`. Treating those as the answer is what makes failures\n * feel secretive.\n */\nconst GENERIC_MESSAGE_PATTERNS: readonly RegExp[] = [\n /failed unexpectedly/i,\n /^\\s*something went wrong/i,\n /please try again(\\s+later)?\\.?\\s*$/i,\n /^\\s*request failed\\b/i,\n /^\\s*unknown (streaming )?error/i,\n /^\\s*internal server error\\.?\\s*$/i,\n];\n\n/** True when a message tells the reader nothing about what actually broke. */\nexport function isGenericUserMessage(\n message: string | null | undefined,\n): boolean {\n const value = (message ?? \"\").trim();\n if (!value) return true;\n return GENERIC_MESSAGE_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nexport interface UpstreamErrorPayload {\n message: string;\n code: string | null;\n userMessage: string | null;\n requestId: string | null;\n status: number | null;\n}\n\n/**\n * Recover an upstream service's structured error that a downstream service\n * stringified into its own message.\n *\n * Real example (scraper wrapping aidream):\n * `aidream could not resolve GSC credential 7223…: HTTP 409 {\"error\":\"conflict\",\n * \"message\":\"Google connection 7223… has no vault credential — it needs\n * re-authentication\",\"user_message\":\"Something went wrong…\",\"request_id\":\"9002…\"}`\n *\n * Without this, the only actionable sentence on the whole hop is invisible.\n */\nexport function unwrapUpstreamError(\n message: string,\n): UpstreamErrorPayload | null {\n const start = message.indexOf(\"{\");\n const end = message.lastIndexOf(\"}\");\n if (start < 0 || end <= start) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(message.slice(start, end + 1));\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null) return null;\n const body = parsed as Record<string, unknown>;\n const inner =\n (typeof body.message === \"string\" && body.message) ||\n (typeof body.detail === \"string\" && body.detail) ||\n \"\";\n if (!inner) return null;\n const statusMatch = /\\bHTTP (\\d{3})\\b/.exec(message.slice(0, start));\n return {\n message: inner,\n code:\n (typeof body.error_type === \"string\" && body.error_type) ||\n (typeof body.error === \"string\" && body.error) ||\n null,\n userMessage:\n typeof body.user_message === \"string\" ? body.user_message : null,\n requestId: typeof body.request_id === \"string\" ? body.request_id : null,\n status: statusMatch ? Number(statusMatch[1]) : null,\n };\n}\n\nexport interface BackendFailureExplanation {\n /** Machine code from the deepest layer that classified the failure. */\n code: string;\n /** The most specific human-readable cause available — never a template. */\n cause: string;\n /** What to headline in the UI: the cause when the server was generic. */\n headline: string;\n /** True when every user-facing message the server sent was a template. */\n headlineWasGeneric: boolean;\n /** Message chain, outermost (closest service) first. */\n chain: string[];\n /** Deepest request id available, for cross-service log correlation. */\n requestId: string;\n status: number | null;\n}\n\n/**\n * THE anti-secrecy primitive: turn any thrown backend/stream failure into the\n * most specific explanation the payload can support — unwrapping every nested\n * upstream error and refusing to let a templated `user_message` be the answer.\n *\n * Every surface that reports a backend failure to a human should headline\n * `explanation.headline` and always keep `cause` + `requestId` reachable.\n */\nexport function describeBackendFailure(\n error: unknown,\n): BackendFailureExplanation {\n const chain: string[] = [];\n let code = \"internal_error\";\n let requestId = \"\";\n let status: number | null = null;\n let userFacing: string | null = null;\n\n if (error instanceof BackendApiError) {\n code = error.code;\n requestId = error.requestId;\n status = error.status;\n userFacing = error.userMessage;\n if (error.detail) chain.push(error.detail);\n if (error.userMessage && error.userMessage !== error.detail) {\n chain.push(error.userMessage);\n }\n } else if (error instanceof Error) {\n chain.push(error.message);\n userFacing = error.message;\n } else if (typeof error === \"string\") {\n chain.push(error);\n userFacing = error;\n } else {\n chain.push(\"Unknown error\");\n }\n\n // Walk the nesting: ANY message in the chain may have stringified the\n // service above it (the technical `detail` usually does, the templated\n // `user_message` never does), so every layer gets unwrapped.\n for (let cursor = 0; cursor < chain.length && cursor < 12; cursor += 1) {\n const upstream = unwrapUpstreamError(chain[cursor] ?? \"\");\n if (!upstream || chain.includes(upstream.message)) continue;\n chain.push(upstream.message);\n if (upstream.code) code = upstream.code;\n if (upstream.requestId) requestId = upstream.requestId;\n if (upstream.status !== null) status = upstream.status;\n }\n\n const specific = [...chain]\n .reverse()\n .find((message) => !isGenericUserMessage(message));\n const cause = specific ?? chain[0] ?? \"Unknown error\";\n const headlineWasGeneric = isGenericUserMessage(userFacing);\n return {\n code,\n cause,\n headline: headlineWasGeneric ? cause : (userFacing ?? cause),\n headlineWasGeneric,\n chain,\n requestId,\n status,\n };\n}\n\n/**\n * Extract a user-visible message from any error object.\n * Utility for components that just need the display string.\n */\nexport function getUserMessage(error: unknown): string {\n if (error instanceof BackendApiError) {\n return error.userMessage;\n }\n if (error instanceof Error) {\n return error.message;\n }\n if (typeof error === \"string\") {\n return error;\n }\n // A Redux thunk's `.unwrap()` rejects with a SerializedError — a plain\n // object, not an Error — whose `message` is the real reason.\n if (\n typeof error === \"object\" &&\n error !== null &&\n typeof (error as { message?: unknown }).message === \"string\" &&\n (error as { message: string }).message\n ) {\n return (error as { message: string }).message;\n }\n return \"Something went wrong\";\n}\n\n// ============================================================================\n// INTERNAL HELPERS\n// ============================================================================\n\nfunction statusToCode(status: number): BackendErrorCode {\n switch (status) {\n case 401:\n return \"auth_required\";\n case 403:\n return \"admin_required\";\n case 404:\n return \"not_found\";\n case 422:\n return \"validation_error\";\n default:\n return \"internal_error\";\n }\n}\n","/**\n * `@ai-matrx/agents/matrx` — the Matrx transport port.\n *\n * The ONE seam between this package's wire semantics and a host's connection\n * policy. This package owns WHAT is said to the AI Matrx server — paths,\n * methods, JSON bodies, streaming Accept headers, the `Last-Event-ID` cursor\n * header — and the host owns HOW the connection is made:\n *\n * - base-URL / backend-channel resolution (global, sandbox override, local\n * engine, EC2-dedicated — whatever ladder the host runs);\n * - credentials (Supabase JWT `Authorization: Bearer`, guest\n * `X-Fingerprint-ID`, or the API-key lane) — NEVER implemented here;\n * - the `X-Organization-Id` context header;\n * - retry policy, network-level timeouts, and diagnostics capture.\n *\n * A host implements the port in a few lines:\n *\n * ```ts\n * const transport: MatrxTransport = {\n * fetch: (path, init) =>\n * fetch(`${baseUrl}${path}`, {\n * ...init,\n * headers: { ...init.headers, ...authHeaders() },\n * }),\n * };\n * ```\n *\n * The port is deliberately structural and `fetch`-shaped so `@ai-matrx/data`'s\n * `/api` transport can implement it without importing this package.\n */\n\nimport { isGenericUserMessage } from \"./backend-errors\";\n\n/**\n * The request this package hands the port. A strict subset of `RequestInit`,\n * so a host can spread it straight into `fetch`.\n */\nexport interface MatrxTransportRequest {\n method: \"GET\" | \"POST\";\n /**\n * Wire-semantic headers the CALL requires (`Content-Type`, `Accept`,\n * `Last-Event-ID`). The host merges its policy headers (auth, org) on top;\n * it must not drop these.\n */\n headers: Record<string, string>;\n /** Pre-serialized JSON body, present on POST calls that carry one. */\n body?: string;\n /** Caller cancellation. The host must wire it to the underlying fetch. */\n signal?: AbortSignal;\n}\n\n/**\n * The transport port. `path` is server-relative and always starts with `/`\n * (`/ai/...`, `/runtime/...`); the host prepends its resolved base URL.\n */\nexport interface MatrxTransport {\n fetch(path: string, init: MatrxTransportRequest): Promise<Response>;\n}\n\n/**\n * A non-2xx response from the Matrx API, with the server's structured error\n * body preserved and its richest human-readable message extracted.\n */\nexport class MatrxApiError extends Error {\n override readonly name = \"MatrxApiError\";\n /** HTTP status of the failed response. */\n readonly status: number;\n /** Machine code from the server body (`code`, or `detail.code`), when present. */\n readonly code: string | null;\n /** The parsed server error body, verbatim (undefined when unparsable). */\n readonly serverDetail: unknown;\n /** The request path the failure came from (server-relative). */\n readonly path: string;\n\n constructor(args: {\n status: number;\n path: string;\n serverDetail?: unknown;\n message?: string;\n }) {\n super(\n args.message ??\n extractMatrxErrorMessage(args.serverDetail) ??\n `HTTP ${args.status}`,\n );\n this.status = args.status;\n this.path = args.path;\n this.serverDetail = args.serverDetail;\n this.code = extractMatrxErrorCode(args.serverDetail);\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction nonBlankString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\n/**\n * Extract the richest human-readable message from a Matrx/FastAPI error body.\n *\n * aidream 4xx validation errors look like\n * `{ error, user_message, details: [{ field, message, help }] }`; hand-raised\n * HTTPExceptions carry `{ detail: { code, message } }`; FastAPI's defaults are\n * `{ detail: string | [{ msg }] }`. Preference order: `user_message` →\n * `message` → joined `details[].message` → `detail.message` →\n * `detail` string → joined `detail[].msg`. Returns undefined for\n * unrecognized bodies so callers fall back to the bare status line.\n */\nexport function extractMatrxErrorMessage(\n serverDetail: unknown,\n): string | undefined {\n if (!isRecord(serverDetail)) return undefined;\n\n // Candidates in preference order. A generic `user_message` (\"Something went\n // wrong\") only wins when nothing more precise exists in the body.\n const candidates: string[] = [];\n const push = (value: string | undefined) => {\n if (value) candidates.push(value);\n };\n push(nonBlankString(serverDetail.user_message));\n push(nonBlankString(serverDetail.message));\n\n if (Array.isArray(serverDetail.details)) {\n const messages = serverDetail.details\n .map((entry: unknown) => {\n if (!isRecord(entry)) return undefined;\n const detailMessage = nonBlankString(entry.message);\n if (!detailMessage) return undefined;\n const field = nonBlankString(entry.field);\n return field ? `${field}: ${detailMessage}` : detailMessage;\n })\n .filter((m): m is string => typeof m === \"string\");\n if (messages.length > 0) push(messages.join(\"; \"));\n }\n\n const detail = serverDetail.detail;\n if (isRecord(detail)) {\n push(nonBlankString(detail.message) ?? nonBlankString(detail.user_message));\n }\n if (typeof detail === \"string\" && detail.trim()) push(detail);\n if (Array.isArray(detail)) {\n const messages = detail\n .map((entry: unknown) =>\n isRecord(entry) ? nonBlankString(entry.msg) : undefined,\n )\n .filter((m): m is string => typeof m === \"string\");\n if (messages.length > 0) push(messages.join(\"; \"));\n }\n return candidates.find((c) => !isGenericUserMessage(c)) ?? candidates[0];\n}\n\n/**\n * Extract the machine error code from a Matrx error body: top-level `code`,\n * else `detail.code` (the hand-raised HTTPException shape). Null when absent.\n */\nexport function extractMatrxErrorCode(serverDetail: unknown): string | null {\n if (!isRecord(serverDetail)) return null;\n const topLevel = nonBlankString(serverDetail.code);\n if (topLevel) return topLevel;\n const detail = serverDetail.detail;\n if (isRecord(detail)) {\n const nested = nonBlankString(detail.code);\n if (nested) return nested;\n }\n return null;\n}\n","/**\n * Internal request plumbing for `@ai-matrx/agents/matrx`. Not part of the\n * public surface — `matrx/index.ts` deliberately does not re-export this\n * module. Everything here is pure: no globals, no work at import time.\n */\n\nimport {\n readMatrxNdjsonStream,\n type MatrxNdjsonIssue,\n type MatrxStreamEnvelope,\n type MatrxStreamEnvelopeObservation,\n} from \"../stream/ndjson\";\nimport { MatrxApiError, type MatrxTransport } from \"./transport\";\n\n/** Encode one path segment (an id) safely into a server-relative path. */\nexport function encodePathSegment(value: string): string {\n return encodeURIComponent(value);\n}\n\nexport type QueryValue =\n | string\n | number\n | boolean\n | readonly string[]\n | undefined;\n\n/**\n * Build a query string. Array values repeat the key (`kind=a&kind=b` — the\n * FastAPI repeatable-parameter convention); undefined values are omitted.\n * Returns \"\" or a string starting with \"?\".\n */\nexport function buildQuery(params: Record<string, QueryValue>): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const entry of value) search.append(key, entry);\n } else {\n search.append(key, String(value));\n }\n }\n const encoded = search.toString();\n return encoded ? `?${encoded}` : \"\";\n}\n\nasync function readServerDetail(response: Response): Promise<unknown> {\n try {\n return (await response.json()) as unknown;\n } catch {\n return undefined;\n }\n}\n\nasync function throwApiError(path: string, response: Response): Promise<never> {\n throw new MatrxApiError({\n status: response.status,\n path,\n serverDetail: await readServerDetail(response),\n });\n}\n\nexport interface JsonRequestOptions {\n method: \"GET\" | \"POST\";\n body?: unknown;\n signal?: AbortSignal;\n}\n\n/**\n * Execute a JSON request through the transport. Throws `MatrxApiError` on a\n * non-2xx response; resolves with the parsed JSON body otherwise.\n */\nexport async function requestJson<T>(\n transport: MatrxTransport,\n path: string,\n options: JsonRequestOptions,\n): Promise<T> {\n const hasBody = options.method !== \"GET\" && options.body !== undefined;\n const response = await transport.fetch(path, {\n method: options.method,\n headers: hasBody ? { \"Content-Type\": \"application/json\" } : {},\n ...(hasBody ? { body: JSON.stringify(options.body) } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n });\n if (!response.ok) return throwApiError(path, response);\n return (await response.json()) as T;\n}\n\n/**\n * Options for every streaming call, riding the NDJSON kernel's contract.\n * Public via `./run`'s re-export.\n */\nexport interface MatrxStreamCallOptions {\n /** Abort the fetch and end the events iterator. */\n signal?: AbortSignal;\n /** Bounded background read-ahead (see `stream/ndjson`). */\n maxReadAhead?: number;\n /** Malformed NDJSON is non-fatal but must never disappear silently. */\n onMalformedLine?: (issue: MatrxNdjsonIssue) => void;\n /** Valid JSON with no recognized Matrx envelope. */\n onUnknownEnvelope?: (value: unknown) => void;\n /** Observe every valid envelope in its exact wire form. */\n onValidEnvelope?: (observation: MatrxStreamEnvelopeObservation) => void;\n}\n\n/**\n * A live agent run: the server-assigned ids (from response headers, available\n * BEFORE any event) and the normalized event stream. Public via `./run`.\n */\nexport interface MatrxRunHandle {\n /** `X-Request-ID` — the ONLY id `POST /ai/cancel/{request_id}` accepts. */\n requestId: string | null;\n /** `X-Conversation-ID` — the server's conversation identity. */\n conversationId: string | null;\n /** Normalized `{event, data}` envelopes through the ONE wire kernel. */\n events: AsyncGenerator<MatrxStreamEnvelope, void, undefined>;\n /** The raw response, for hosts that need headers/status beyond the ids. */\n response: Response;\n}\n\n/**\n * Wrap a validated streaming Response into the run handle — the ONE place\n * the id headers are read and the NDJSON kernel is attached (`./run` and\n * `./operations`' rejoin share it).\n */\nexport function toRunHandle(\n response: Response,\n options: MatrxStreamCallOptions,\n): MatrxRunHandle {\n return {\n requestId: response.headers.get(\"X-Request-ID\"),\n conversationId: response.headers.get(\"X-Conversation-ID\"),\n events: readMatrxNdjsonStream(response.body as ReadableStream<Uint8Array>, {\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.maxReadAhead !== undefined\n ? { maxReadAhead: options.maxReadAhead }\n : {}),\n ...(options.onMalformedLine\n ? { onMalformedLine: options.onMalformedLine }\n : {}),\n ...(options.onUnknownEnvelope\n ? { onUnknownEnvelope: options.onUnknownEnvelope }\n : {}),\n ...(options.onValidEnvelope\n ? { onValidEnvelope: options.onValidEnvelope }\n : {}),\n }),\n response,\n };\n}\n\nexport interface StreamRequestOptions {\n method: \"GET\" | \"POST\";\n body?: unknown;\n /** Extra wire-semantic headers (`Accept`, `Last-Event-ID`). */\n headers?: Record<string, string>;\n signal?: AbortSignal;\n}\n\n/**\n * Execute a streaming request. Throws `MatrxApiError` on a non-2xx response\n * (reading the error body as JSON when possible) or when a 2xx response\n * carries no body; resolves with the validated `Response` otherwise.\n */\nexport async function requestStream(\n transport: MatrxTransport,\n path: string,\n options: StreamRequestOptions,\n): Promise<Response> {\n const hasBody = options.method !== \"GET\" && options.body !== undefined;\n const response = await transport.fetch(path, {\n method: options.method,\n headers: {\n ...(hasBody ? { \"Content-Type\": \"application/json\" } : {}),\n ...options.headers,\n },\n ...(hasBody ? { body: JSON.stringify(options.body) } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n });\n if (!response.ok) return throwApiError(path, response);\n if (!response.body) {\n throw new MatrxApiError({\n status: response.status,\n path,\n serverDetail: { code: \"missing_response_body\" },\n message: \"The streaming response carried no body.\",\n });\n }\n return response;\n}\n","// AUTO-GENERATED — do not edit by hand.\n// Source: aidream/services/agent_service/portable.py\n// Run: uv run python scripts/generate_types.py portable-agent\n\nexport const PORTABLE_AGENT_CONTRACT = \"portable-agent.v1\";\n\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\nexport interface PortableAgentRequest {\n variables?: Record<string, JsonValue>;\n surface?: string;\n}\n\nexport interface PortableSkill {\n id: string;\n tier: \"included\" | \"listed\";\n slug: string;\n label: string;\n description: string;\n path: string;\n content: string;\n body: string;\n content_hash: string;\n}\n\nexport interface PortableTool {\n name: string;\n canonical_name: string;\n description: string;\n parameters: Record<string, JsonValue>;\n}\n\nexport interface PortableUnavailable {\n kind: \"tool\" | \"skill\" | \"variable\" | \"message\" | \"mcp_server\" | \"tools\";\n name: string;\n reason: string;\n}\n\nexport interface PortableAgentBundle {\n contract: \"portable-agent.v1\";\n agent_id: string;\n version_id: string | null;\n version_number: number | null;\n is_version: boolean;\n name: string;\n definition_hash: string;\n model_id: string;\n instructions: string;\n variables: Record<string, JsonValue>[];\n filled_variables: string[];\n skills: PortableSkill[];\n tools: PortableTool[];\n surface: string;\n unavailable: PortableUnavailable[];\n}\n\nexport interface PortableAgentVersion {\n id: string;\n version_number: number;\n changed_at: string | null;\n definition_hash: string | null;\n}\n\nexport interface PortableAgentSummary {\n agent_id: string;\n name: string;\n version_id: string | null;\n version_number: number | null;\n definition_hash: string;\n variables: Record<string, JsonValue>[];\n versions: PortableAgentVersion[];\n}\n"],"mappings":"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","names":[]}
@@ -1,3 +1,18 @@
1
+ // matrx/backend-errors.ts
2
+ var GENERIC_MESSAGE_PATTERNS = [
3
+ /failed unexpectedly/i,
4
+ /^\s*something went wrong/i,
5
+ /please try again(\s+later)?\.?\s*$/i,
6
+ /^\s*request failed\b/i,
7
+ /^\s*unknown (streaming )?error/i,
8
+ /^\s*internal server error\.?\s*$/i
9
+ ];
10
+ function isGenericUserMessage(message) {
11
+ const value = (message ?? "").trim();
12
+ if (!value) return true;
13
+ return GENERIC_MESSAGE_PATTERNS.some((pattern) => pattern.test(value));
14
+ }
15
+
1
16
  // matrx/transport.ts
2
17
  var MatrxApiError = class extends Error {
3
18
  name = "MatrxApiError";
@@ -27,10 +42,12 @@ function nonBlankString(value) {
27
42
  }
28
43
  function extractMatrxErrorMessage(serverDetail) {
29
44
  if (!isRecord(serverDetail)) return void 0;
30
- const userMessage = nonBlankString(serverDetail.user_message);
31
- if (userMessage) return userMessage;
32
- const message = nonBlankString(serverDetail.message);
33
- if (message) return message;
45
+ const candidates = [];
46
+ const push = (value) => {
47
+ if (value) candidates.push(value);
48
+ };
49
+ push(nonBlankString(serverDetail.user_message));
50
+ push(nonBlankString(serverDetail.message));
34
51
  if (Array.isArray(serverDetail.details)) {
35
52
  const messages = serverDetail.details.map((entry) => {
36
53
  if (!isRecord(entry)) return void 0;
@@ -39,21 +56,20 @@ function extractMatrxErrorMessage(serverDetail) {
39
56
  const field = nonBlankString(entry.field);
40
57
  return field ? `${field}: ${detailMessage}` : detailMessage;
41
58
  }).filter((m) => typeof m === "string");
42
- if (messages.length > 0) return messages.join("; ");
59
+ if (messages.length > 0) push(messages.join("; "));
43
60
  }
44
61
  const detail = serverDetail.detail;
45
62
  if (isRecord(detail)) {
46
- const detailMessage = nonBlankString(detail.message) ?? nonBlankString(detail.user_message);
47
- if (detailMessage) return detailMessage;
63
+ push(nonBlankString(detail.message) ?? nonBlankString(detail.user_message));
48
64
  }
49
- if (typeof detail === "string" && detail.trim()) return detail;
65
+ if (typeof detail === "string" && detail.trim()) push(detail);
50
66
  if (Array.isArray(detail)) {
51
67
  const messages = detail.map(
52
68
  (entry) => isRecord(entry) ? nonBlankString(entry.msg) : void 0
53
69
  ).filter((m) => typeof m === "string");
54
- if (messages.length > 0) return messages.join("; ");
70
+ if (messages.length > 0) push(messages.join("; "));
55
71
  }
56
- return void 0;
72
+ return candidates.find((c) => !isGenericUserMessage(c)) ?? candidates[0];
57
73
  }
58
74
  function extractMatrxErrorCode(serverDetail) {
59
75
  if (!isRecord(serverDetail)) return null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../matrx/transport.ts","../../matrx/internal.ts","../../portable/types.generated.ts","../../portable/index.ts"],"sourcesContent":["/**\n * `@ai-matrx/agents/matrx` — the Matrx transport port.\n *\n * The ONE seam between this package's wire semantics and a host's connection\n * policy. This package owns WHAT is said to the AI Matrx server — paths,\n * methods, JSON bodies, streaming Accept headers, the `Last-Event-ID` cursor\n * header — and the host owns HOW the connection is made:\n *\n * - base-URL / backend-channel resolution (global, sandbox override, local\n * engine, EC2-dedicated — whatever ladder the host runs);\n * - credentials (Supabase JWT `Authorization: Bearer`, guest\n * `X-Fingerprint-ID`, or the API-key lane) — NEVER implemented here;\n * - the `X-Organization-Id` context header;\n * - retry policy, network-level timeouts, and diagnostics capture.\n *\n * A host implements the port in a few lines:\n *\n * ```ts\n * const transport: MatrxTransport = {\n * fetch: (path, init) =>\n * fetch(`${baseUrl}${path}`, {\n * ...init,\n * headers: { ...init.headers, ...authHeaders() },\n * }),\n * };\n * ```\n *\n * The port is deliberately structural and `fetch`-shaped so `@ai-matrx/data`'s\n * `/api` transport can implement it without importing this package.\n */\n\n/**\n * The request this package hands the port. A strict subset of `RequestInit`,\n * so a host can spread it straight into `fetch`.\n */\nexport interface MatrxTransportRequest {\n method: \"GET\" | \"POST\";\n /**\n * Wire-semantic headers the CALL requires (`Content-Type`, `Accept`,\n * `Last-Event-ID`). The host merges its policy headers (auth, org) on top;\n * it must not drop these.\n */\n headers: Record<string, string>;\n /** Pre-serialized JSON body, present on POST calls that carry one. */\n body?: string;\n /** Caller cancellation. The host must wire it to the underlying fetch. */\n signal?: AbortSignal;\n}\n\n/**\n * The transport port. `path` is server-relative and always starts with `/`\n * (`/ai/...`, `/runtime/...`); the host prepends its resolved base URL.\n */\nexport interface MatrxTransport {\n fetch(path: string, init: MatrxTransportRequest): Promise<Response>;\n}\n\n/**\n * A non-2xx response from the Matrx API, with the server's structured error\n * body preserved and its richest human-readable message extracted.\n */\nexport class MatrxApiError extends Error {\n override readonly name = \"MatrxApiError\";\n /** HTTP status of the failed response. */\n readonly status: number;\n /** Machine code from the server body (`code`, or `detail.code`), when present. */\n readonly code: string | null;\n /** The parsed server error body, verbatim (undefined when unparsable). */\n readonly serverDetail: unknown;\n /** The request path the failure came from (server-relative). */\n readonly path: string;\n\n constructor(args: {\n status: number;\n path: string;\n serverDetail?: unknown;\n message?: string;\n }) {\n super(\n args.message ??\n extractMatrxErrorMessage(args.serverDetail) ??\n `HTTP ${args.status}`,\n );\n this.status = args.status;\n this.path = args.path;\n this.serverDetail = args.serverDetail;\n this.code = extractMatrxErrorCode(args.serverDetail);\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction nonBlankString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\n/**\n * Extract the richest human-readable message from a Matrx/FastAPI error body.\n *\n * aidream 4xx validation errors look like\n * `{ error, user_message, details: [{ field, message, help }] }`; hand-raised\n * HTTPExceptions carry `{ detail: { code, message } }`; FastAPI's defaults are\n * `{ detail: string | [{ msg }] }`. Preference order: `user_message` →\n * `message` → joined `details[].message` → `detail.message` →\n * `detail` string → joined `detail[].msg`. Returns undefined for\n * unrecognized bodies so callers fall back to the bare status line.\n */\nexport function extractMatrxErrorMessage(\n serverDetail: unknown,\n): string | undefined {\n if (!isRecord(serverDetail)) return undefined;\n\n const userMessage = nonBlankString(serverDetail.user_message);\n if (userMessage) return userMessage;\n const message = nonBlankString(serverDetail.message);\n if (message) return message;\n\n if (Array.isArray(serverDetail.details)) {\n const messages = serverDetail.details\n .map((entry: unknown) => {\n if (!isRecord(entry)) return undefined;\n const detailMessage = nonBlankString(entry.message);\n if (!detailMessage) return undefined;\n const field = nonBlankString(entry.field);\n return field ? `${field}: ${detailMessage}` : detailMessage;\n })\n .filter((m): m is string => typeof m === \"string\");\n if (messages.length > 0) return messages.join(\"; \");\n }\n\n const detail = serverDetail.detail;\n if (isRecord(detail)) {\n const detailMessage =\n nonBlankString(detail.message) ?? nonBlankString(detail.user_message);\n if (detailMessage) return detailMessage;\n }\n if (typeof detail === \"string\" && detail.trim()) return detail;\n if (Array.isArray(detail)) {\n const messages = detail\n .map((entry: unknown) =>\n isRecord(entry) ? nonBlankString(entry.msg) : undefined,\n )\n .filter((m): m is string => typeof m === \"string\");\n if (messages.length > 0) return messages.join(\"; \");\n }\n return undefined;\n}\n\n/**\n * Extract the machine error code from a Matrx error body: top-level `code`,\n * else `detail.code` (the hand-raised HTTPException shape). Null when absent.\n */\nexport function extractMatrxErrorCode(serverDetail: unknown): string | null {\n if (!isRecord(serverDetail)) return null;\n const topLevel = nonBlankString(serverDetail.code);\n if (topLevel) return topLevel;\n const detail = serverDetail.detail;\n if (isRecord(detail)) {\n const nested = nonBlankString(detail.code);\n if (nested) return nested;\n }\n return null;\n}\n","/**\n * Internal request plumbing for `@ai-matrx/agents/matrx`. Not part of the\n * public surface — `matrx/index.ts` deliberately does not re-export this\n * module. Everything here is pure: no globals, no work at import time.\n */\n\nimport {\n readMatrxNdjsonStream,\n type MatrxNdjsonIssue,\n type MatrxStreamEnvelope,\n type MatrxStreamEnvelopeObservation,\n} from \"../stream/ndjson\";\nimport { MatrxApiError, type MatrxTransport } from \"./transport\";\n\n/** Encode one path segment (an id) safely into a server-relative path. */\nexport function encodePathSegment(value: string): string {\n return encodeURIComponent(value);\n}\n\nexport type QueryValue =\n | string\n | number\n | boolean\n | readonly string[]\n | undefined;\n\n/**\n * Build a query string. Array values repeat the key (`kind=a&kind=b` — the\n * FastAPI repeatable-parameter convention); undefined values are omitted.\n * Returns \"\" or a string starting with \"?\".\n */\nexport function buildQuery(params: Record<string, QueryValue>): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const entry of value) search.append(key, entry);\n } else {\n search.append(key, String(value));\n }\n }\n const encoded = search.toString();\n return encoded ? `?${encoded}` : \"\";\n}\n\nasync function readServerDetail(response: Response): Promise<unknown> {\n try {\n return (await response.json()) as unknown;\n } catch {\n return undefined;\n }\n}\n\nasync function throwApiError(path: string, response: Response): Promise<never> {\n throw new MatrxApiError({\n status: response.status,\n path,\n serverDetail: await readServerDetail(response),\n });\n}\n\nexport interface JsonRequestOptions {\n method: \"GET\" | \"POST\";\n body?: unknown;\n signal?: AbortSignal;\n}\n\n/**\n * Execute a JSON request through the transport. Throws `MatrxApiError` on a\n * non-2xx response; resolves with the parsed JSON body otherwise.\n */\nexport async function requestJson<T>(\n transport: MatrxTransport,\n path: string,\n options: JsonRequestOptions,\n): Promise<T> {\n const hasBody = options.method !== \"GET\" && options.body !== undefined;\n const response = await transport.fetch(path, {\n method: options.method,\n headers: hasBody ? { \"Content-Type\": \"application/json\" } : {},\n ...(hasBody ? { body: JSON.stringify(options.body) } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n });\n if (!response.ok) return throwApiError(path, response);\n return (await response.json()) as T;\n}\n\n/**\n * Options for every streaming call, riding the NDJSON kernel's contract.\n * Public via `./run`'s re-export.\n */\nexport interface MatrxStreamCallOptions {\n /** Abort the fetch and end the events iterator. */\n signal?: AbortSignal;\n /** Bounded background read-ahead (see `stream/ndjson`). */\n maxReadAhead?: number;\n /** Malformed NDJSON is non-fatal but must never disappear silently. */\n onMalformedLine?: (issue: MatrxNdjsonIssue) => void;\n /** Valid JSON with no recognized Matrx envelope. */\n onUnknownEnvelope?: (value: unknown) => void;\n /** Observe every valid envelope in its exact wire form. */\n onValidEnvelope?: (observation: MatrxStreamEnvelopeObservation) => void;\n}\n\n/**\n * A live agent run: the server-assigned ids (from response headers, available\n * BEFORE any event) and the normalized event stream. Public via `./run`.\n */\nexport interface MatrxRunHandle {\n /** `X-Request-ID` — the ONLY id `POST /ai/cancel/{request_id}` accepts. */\n requestId: string | null;\n /** `X-Conversation-ID` — the server's conversation identity. */\n conversationId: string | null;\n /** Normalized `{event, data}` envelopes through the ONE wire kernel. */\n events: AsyncGenerator<MatrxStreamEnvelope, void, undefined>;\n /** The raw response, for hosts that need headers/status beyond the ids. */\n response: Response;\n}\n\n/**\n * Wrap a validated streaming Response into the run handle — the ONE place\n * the id headers are read and the NDJSON kernel is attached (`./run` and\n * `./operations`' rejoin share it).\n */\nexport function toRunHandle(\n response: Response,\n options: MatrxStreamCallOptions,\n): MatrxRunHandle {\n return {\n requestId: response.headers.get(\"X-Request-ID\"),\n conversationId: response.headers.get(\"X-Conversation-ID\"),\n events: readMatrxNdjsonStream(response.body as ReadableStream<Uint8Array>, {\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.maxReadAhead !== undefined\n ? { maxReadAhead: options.maxReadAhead }\n : {}),\n ...(options.onMalformedLine\n ? { onMalformedLine: options.onMalformedLine }\n : {}),\n ...(options.onUnknownEnvelope\n ? { onUnknownEnvelope: options.onUnknownEnvelope }\n : {}),\n ...(options.onValidEnvelope\n ? { onValidEnvelope: options.onValidEnvelope }\n : {}),\n }),\n response,\n };\n}\n\nexport interface StreamRequestOptions {\n method: \"GET\" | \"POST\";\n body?: unknown;\n /** Extra wire-semantic headers (`Accept`, `Last-Event-ID`). */\n headers?: Record<string, string>;\n signal?: AbortSignal;\n}\n\n/**\n * Execute a streaming request. Throws `MatrxApiError` on a non-2xx response\n * (reading the error body as JSON when possible) or when a 2xx response\n * carries no body; resolves with the validated `Response` otherwise.\n */\nexport async function requestStream(\n transport: MatrxTransport,\n path: string,\n options: StreamRequestOptions,\n): Promise<Response> {\n const hasBody = options.method !== \"GET\" && options.body !== undefined;\n const response = await transport.fetch(path, {\n method: options.method,\n headers: {\n ...(hasBody ? { \"Content-Type\": \"application/json\" } : {}),\n ...options.headers,\n },\n ...(hasBody ? { body: JSON.stringify(options.body) } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n });\n if (!response.ok) return throwApiError(path, response);\n if (!response.body) {\n throw new MatrxApiError({\n status: response.status,\n path,\n serverDetail: { code: \"missing_response_body\" },\n message: \"The streaming response carried no body.\",\n });\n }\n return response;\n}\n","// AUTO-GENERATED — do not edit by hand.\n// Source: aidream/services/agent_service/portable.py\n// Run: uv run python scripts/generate_types.py portable-agent\n\nexport const PORTABLE_AGENT_CONTRACT = \"portable-agent.v1\";\n\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\nexport interface PortableAgentRequest {\n variables?: Record<string, JsonValue>;\n surface?: string;\n}\n\nexport interface PortableSkill {\n id: string;\n tier: \"included\" | \"listed\";\n slug: string;\n label: string;\n description: string;\n path: string;\n content: string;\n body: string;\n content_hash: string;\n}\n\nexport interface PortableTool {\n name: string;\n canonical_name: string;\n description: string;\n parameters: Record<string, JsonValue>;\n}\n\nexport interface PortableUnavailable {\n kind: \"tool\" | \"skill\" | \"variable\" | \"message\" | \"mcp_server\" | \"tools\";\n name: string;\n reason: string;\n}\n\nexport interface PortableAgentBundle {\n contract: \"portable-agent.v1\";\n agent_id: string;\n version_id: string | null;\n version_number: number | null;\n is_version: boolean;\n name: string;\n definition_hash: string;\n model_id: string;\n instructions: string;\n variables: Record<string, JsonValue>[];\n filled_variables: string[];\n skills: PortableSkill[];\n tools: PortableTool[];\n surface: string;\n unavailable: PortableUnavailable[];\n}\n\nexport interface PortableAgentVersion {\n id: string;\n version_number: number;\n changed_at: string | null;\n definition_hash: string | null;\n}\n\nexport interface PortableAgentSummary {\n agent_id: string;\n name: string;\n version_id: string | null;\n version_number: number | null;\n definition_hash: string;\n variables: Record<string, JsonValue>[];\n versions: PortableAgentVersion[];\n}\n","/**\n * `@ai-matrx/agents/portable` — an AI Matrx agent as pieces a coding CLI loads.\n *\n * `fetchPortableAgent` reads `POST /ai/agents/{id}/portable` (or the version\n * door) and returns the server's `PortableAgentBundle`: the instructions with\n * the person's variable values filled, the skills as Claude `SKILL.md` files,\n * the server tools in MCP shape, and `unavailable[]` — what could not be\n * carried and why. The types are GENERATED from the server's Pydantic models\n * (`types.generated.ts`, `uv run python scripts/generate_types.py portable-agent`\n * in aidream), so the two cannot drift.\n *\n * Pure entry: no React, no Node, no I/O at import. Credentials and the\n * organization travel through the host's `MatrxTransport`\n * (`createMatrxTransport({ credentials, organizationId })`).\n */\n\nimport { encodePathSegment, requestJson } from \"../matrx/internal\";\nimport type { MatrxTransport } from \"../matrx/transport\";\nimport type {\n JsonValue,\n PortableAgentBundle,\n PortableAgentRequest,\n PortableAgentSummary,\n} from \"./types.generated\";\n\nexport * from \"./types.generated\";\n\n/** Which definition to load: the agent's current one, or one immutable version. */\nexport type PortableAgentTarget =\n | { agentId: string; versionId?: undefined }\n | { versionId: string; agentId?: string | undefined };\n\nexport interface FetchPortableAgentOptions {\n /** The person's values, by variable name. Unfilled variables use the agent's defaults. */\n variables?: Record<string, JsonValue>;\n /** The `ui.ui_surface` tools resolve on. Default: the server's coding-session surface. */\n surface?: string;\n signal?: AbortSignal;\n}\n\n/** The server path for a target — exported so a host's tests can assert it. */\nexport function portableAgentPath(target: PortableAgentTarget): string {\n return target.versionId\n ? `/ai/agents/versions/${encodePathSegment(target.versionId)}/portable`\n : `/ai/agents/${encodePathSegment(target.agentId ?? \"\")}/portable`;\n}\n\n/**\n * Load one agent as a portable bundle. Throws `MatrxApiError` on refusal —\n * 401 signed out, 404 not found or not shared with this person.\n */\nexport function fetchPortableAgent(\n transport: MatrxTransport,\n target: PortableAgentTarget,\n options: FetchPortableAgentOptions = {},\n): Promise<PortableAgentBundle> {\n const body: PortableAgentRequest = {\n variables: options.variables ?? {},\n ...(options.surface ? { surface: options.surface } : {}),\n };\n return requestJson<PortableAgentBundle>(transport, portableAgentPath(target), {\n method: \"POST\",\n body,\n ...(options.signal ? { signal: options.signal } : {}),\n });\n}\n\n/**\n * The light read behind an agent picker (`GET /ai/agents/{id}/portable/summary`):\n * name, variable declarations, versions newest first with their hashes. No\n * skills, no tools — call this when a person CHOOSES an agent, and\n * `fetchPortableAgent` only when a session starts.\n */\nexport function fetchPortableAgentSummary(\n transport: MatrxTransport,\n agentId: string,\n options: { signal?: AbortSignal } = {},\n): Promise<PortableAgentSummary> {\n return requestJson<PortableAgentSummary>(\n transport,\n `/ai/agents/${encodePathSegment(agentId)}/portable/summary`,\n { method: \"GET\", ...(options.signal ? { signal: options.signal } : {}) },\n );\n}\n\n/** One saved version of an agent (`AgentVersionInfo` server-side). */\nexport interface PortableAgentVersion {\n id: string;\n agent_id: string;\n version_number: number;\n name: string;\n change_note: string | null;\n changed_at: string | null;\n}\n\n/**\n * The agent's saved versions, oldest first (`GET /agent-service/agents/{id}/versions`,\n * authorized through the same viewer rule as the agent). Feeds a version menu.\n */\nexport function fetchPortableAgentVersions(\n transport: MatrxTransport,\n agentId: string,\n options: { signal?: AbortSignal } = {},\n): Promise<PortableAgentVersion[]> {\n return requestJson<PortableAgentVersion[]>(\n transport,\n `/agent-service/agents/${encodePathSegment(agentId)}/versions`,\n { method: \"GET\", ...(options.signal ? { signal: options.signal } : {}) },\n );\n}\n\n/** What a bundle loaded, counted — what a header chip shows. */\nexport interface PortableAgentLoadSummary {\n instructions: boolean;\n skills: { loaded: number; total: number };\n tools: { loaded: number; total: number };\n}\n\n/** Count what a bundle carries against what the agent declares. */\nexport function summarizePortableAgent(bundle: PortableAgentBundle): PortableAgentLoadSummary {\n const missingSkills = bundle.unavailable.filter((u) => u.kind === \"skill\").length;\n const missingTools = bundle.unavailable.filter((u) => u.kind === \"tool\").length;\n return {\n instructions: bundle.instructions.trim().length > 0,\n skills: { loaded: bundle.skills.length, total: bundle.skills.length + missingSkills },\n tools: { loaded: bundle.tools.length, total: bundle.tools.length + missingTools },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../matrx/backend-errors.ts","../../matrx/transport.ts","../../matrx/internal.ts","../../portable/types.generated.ts","../../portable/index.ts"],"sourcesContent":["/**\n * `@ai-matrx/agents/matrx` — the AI Matrx server's error model, as every\n * client reads it: `BackendApiError` (the server's `APIError` body),\n * `StreamTransportError` (the socket died mid-run; the run may still finish\n * and is reattachable), parsing of HTTP / stream / persisted errors, and the\n * ONE sentence a person sees (`getUserMessage`, `describeBackendFailure`).\n *\n * Moved from matrx-frontend `lib/api/errors.ts` (chat-package independence\n * P9); the app re-exports it from here. Pure: no host, no window, no env.\n */\n\n/**\n * Standardized error shape returned by all backend endpoints.\n * Matches the Python `APIError` Pydantic model.\n */\nexport interface BackendApiErrorData {\n /** Machine-readable error code (e.g. \"auth_required\", \"validation_error\") */\n error: string;\n /** Developer-facing detail for debugging */\n message: string;\n /** Safe to display directly in the UI */\n user_message: string;\n /** Extra info (validation errors, etc.) */\n details: unknown | null;\n /** Unique request ID for support/debugging */\n request_id: string;\n}\n\n/** Common backend error codes */\nexport type BackendErrorCode =\n | \"auth_required\"\n | \"token_required\"\n | \"admin_required\"\n | \"validation_error\"\n | \"not_found\"\n | \"internal_error\"\n | \"agent_error\"\n /** The stream socket died mid-run; the server run may still be completing\n * and is reattachable. See `StreamTransportError`. */\n | \"stream_transport_lost\"\n | (string & {});\n\n// ============================================================================\n// ERROR CLASS\n// ============================================================================\n\n/**\n * Typed error thrown by all backend API operations.\n * Contains structured fields matching the Python APIError model.\n *\n * Usage:\n * ```typescript\n * try {\n * await client.post(ENDPOINTS.ai.agentStart(agentId), body);\n * } catch (err) {\n * if (err instanceof BackendApiError) {\n * // Show err.userMessage to the user\n * // Log err.requestId for debugging\n * // Check err.code for programmatic handling\n * }\n * }\n * ```\n */\nexport class BackendApiError extends Error {\n /** Machine-readable error code */\n readonly code: BackendErrorCode;\n /** Developer-facing detail */\n readonly detail: string;\n /** Safe to display directly in the UI */\n readonly userMessage: string;\n /** Extra info (validation errors, etc.) */\n readonly details: unknown | null;\n /** Unique request ID for support/debugging */\n readonly requestId: string;\n /** HTTP status code (if from an HTTP response) */\n readonly status: number | null;\n\n constructor(data: {\n code: BackendErrorCode;\n detail: string;\n userMessage: string;\n details?: unknown | null;\n requestId?: string | undefined;\n status?: number | null | undefined;\n }) {\n super(data.userMessage);\n this.name = \"BackendApiError\";\n this.code = data.code;\n this.detail = data.detail;\n this.userMessage = data.userMessage;\n this.details = data.details ?? null;\n // MATRX-EXCEPTION: requestId is genuinely optional (constructor param);\n // \"\" means \"no request id available\" — a display/log field, not persisted.\n this.requestId = data.requestId ?? \"\";\n this.status = data.status ?? null;\n }\n\n /** Convert to the wire format for logging */\n toJSON(): BackendApiErrorData {\n return {\n error: this.code,\n message: this.detail,\n user_message: this.userMessage,\n details: this.details,\n request_id: this.requestId,\n };\n }\n}\n\n/**\n * The socket carrying a live NDJSON stream broke mid-run.\n *\n * THE DISTINCTION THIS EXISTS TO MAKE: a backend that blows up mid-stream does\n * NOT break the socket — it emits a typed `error` event and closes the body\n * cleanly. So an exception escaping the body reader means the *transport* died,\n * not the run. And aidream streams run `detach_on_disconnect=True`: the server\n * keeps executing and persisting the turn after our connection goes away.\n *\n * The client therefore cannot decide locally whether the answer is lost — it\n * must ASK THE SERVER. That is what `resumable` means here: \"reattach by\n * requestId / conversationId / durable run id and let server truth settle it\",\n * never \"this succeeded\". A server that genuinely died reports `failed` on\n * reattach and the honest record replaces the optimistic copy.\n *\n * Consumers: `run-ai-stream.ts` (chat → `reconnectServerOperation`) and\n * `adopt-foreign-stream.ts` (pipeline runs → the surface's own rejoin).\n */\nexport class StreamTransportError extends BackendApiError {\n /** Always true — reattach and let the server settle the outcome. */\n readonly resumable = true as const;\n\n constructor(data: {\n detail: string;\n details?: unknown | null;\n requestId?: string;\n }) {\n super({\n code: \"stream_transport_lost\",\n detail: data.detail,\n userMessage:\n \"The connection dropped. Your run is still going on the server — reconnecting to it now.\",\n details: data.details ?? null,\n ...(data.requestId !== undefined ? { requestId: data.requestId } : {}),\n });\n this.name = \"StreamTransportError\";\n }\n}\n\n/**\n * True when a failure is a dropped transport rather than a failed run. Use this\n * instead of `instanceof` at boundaries that re-wrap errors (thunk rejections,\n * `callApi` result errors), where the class identity is lost but the code\n * survives.\n */\nexport function isStreamTransportLost(error: unknown): boolean {\n if (error instanceof StreamTransportError) return true;\n if (error instanceof BackendApiError) {\n return error.code === \"stream_transport_lost\";\n }\n if (error && typeof error === \"object\") {\n const code = (error as { code?: unknown }).code;\n if (code === \"stream_transport_lost\") return true;\n const errorType = (error as { error_type?: unknown }).error_type;\n if (errorType === \"transport_lost\") return true;\n }\n return false;\n}\n\n// ============================================================================\n// HTTP ERROR PARSER\n// ============================================================================\n\n/**\n * Parse a non-OK HTTP response into a BackendApiError.\n *\n * Handles the standardized backend shape and falls back gracefully\n * when the response isn't JSON or uses a legacy format.\n */\nexport async function parseHttpError(\n response: Response,\n): Promise<BackendApiError> {\n const status = response.status;\n let text: string;\n\n try {\n // A Response body can only be read once. Capture it before deciding\n // whether the backend sent a structured error or plain text.\n text = await response.text();\n } catch {\n return new BackendApiError({\n code: statusToCode(status),\n detail: `HTTP ${status}`,\n userMessage: `Request failed (${status})`,\n status,\n });\n }\n\n let body: Record<string, unknown> | null;\n try {\n body = JSON.parse(text) as Record<string, unknown> | null;\n } catch {\n return new BackendApiError({\n code: statusToCode(status),\n detail: text || `HTTP ${status}`,\n userMessage: text || `Request failed (${status})`,\n status,\n });\n }\n return parseHttpErrorBody(body, status);\n}\n\n/**\n * Parse an already-decoded JSON error body into a BackendApiError.\n *\n * Exported because XHR callers (upload/download progress paths) have the\n * parsed body in hand and MUST NOT hand-roll a shallower read: a private\n * copy in `python-client.ts` looked only at top-level `error`/`message`, so\n * FastAPI's `{\"detail\": {...}}` envelope — what every matrx-files 500 uses —\n * degraded to the useless `code: \"internal\", detail: \"HTTP 500\"`. That is\n * exactly how an upload failure with a real server-side cause reached the\n * user as `Upload failed (500)` and nothing else. One parser, every transport.\n */\nexport function parseHttpErrorBody(\n body: Record<string, unknown> | null,\n status: number,\n): BackendApiError {\n if (!body) {\n return new BackendApiError({\n code: statusToCode(status),\n detail: `HTTP ${status}`,\n userMessage: `Request failed (${status})`,\n status,\n });\n }\n // Standard backend shape: { error, message, user_message, details, request_id }\n if (typeof body.error === \"string\" && typeof body.user_message === \"string\") {\n return new BackendApiError({\n code: body.error as BackendErrorCode,\n detail: (body.message as string) || `HTTP ${status}`,\n userMessage: body.user_message as string,\n details: body.details ?? null,\n requestId:\n typeof body.request_id === \"string\" ? body.request_id : undefined,\n status,\n });\n }\n\n // Legacy: nested error object with user_visible_message\n if (typeof body.error === \"object\" && body.error !== null) {\n const errorObj = body.error as Record<string, unknown>;\n return new BackendApiError({\n code:\n (errorObj.type as string) ||\n (errorObj.error as string) ||\n statusToCode(status),\n detail: (errorObj.message as string) || `HTTP ${status}`,\n userMessage:\n (errorObj.user_message as string) ||\n (errorObj.user_visible_message as string) ||\n (errorObj.message as string) ||\n `Request failed (${status})`,\n details: errorObj.details ?? null,\n requestId:\n typeof errorObj.request_id === \"string\"\n ? errorObj.request_id\n : undefined,\n status,\n });\n }\n\n // FastAPI 422 validation shape: { detail: [{ loc, msg, type }, ...] }\n if (Array.isArray(body.detail)) {\n const first = body.detail[0] as Record<string, unknown> | undefined;\n const firstMsg = first && typeof first.msg === \"string\" ? first.msg : null;\n return new BackendApiError({\n code: statusToCode(status),\n detail: firstMsg\n ? `Validation error: ${firstMsg}`\n : JSON.stringify(body.detail),\n userMessage: firstMsg || `Request failed (${status})`,\n details: body.detail,\n status,\n });\n }\n\n // FastAPI HTTPException with structured detail: { detail: { code?, error?, message?, user_message?, ... } }\n if (\n typeof body.detail === \"object\" &&\n body.detail !== null &&\n !Array.isArray(body.detail)\n ) {\n const d = body.detail as Record<string, unknown>;\n const code =\n (typeof d.code === \"string\" && d.code) ||\n (typeof d.error === \"string\" && d.error) ||\n statusToCode(status);\n const message =\n (typeof d.message === \"string\" && d.message) ||\n (typeof d.detail === \"string\" && d.detail) ||\n `HTTP ${status}`;\n return new BackendApiError({\n code: code as BackendErrorCode,\n detail: message,\n userMessage:\n (typeof d.user_message === \"string\" && d.user_message) ||\n (typeof d.user_visible_message === \"string\" &&\n d.user_visible_message) ||\n message,\n details: d.details ?? d,\n requestId: typeof d.request_id === \"string\" ? d.request_id : undefined,\n status,\n });\n }\n\n // Legacy: flat { error: string, message: string } or { detail: string }\n return new BackendApiError({\n code: typeof body.error === \"string\" ? body.error : statusToCode(status),\n detail:\n (body.message as string) ||\n (body.detail as string) ||\n (body.error as string) ||\n `HTTP ${status}`,\n userMessage:\n (body.user_message as string) ||\n (body.user_visible_message as string) ||\n (body.message as string) ||\n (body.detail as string) ||\n `Request failed (${status})`,\n details: body.details ?? null,\n requestId:\n typeof body.request_id === \"string\" ? body.request_id : undefined,\n status,\n });\n}\n\n/**\n * Adapt callApi's result-style error into the same canonical error used by\n * direct fetch and streaming consumers.\n *\n * callApi intentionally returns errors instead of throwing them, but its\n * `serverDetail` contains the complete FastAPI body. Sending only\n * `error.message` to a feature discards that body and turns a precise\n * configuration failure into \"HTTP 422\". This adapter keeps one parser and\n * one human-facing explanation path across both client styles.\n */\nexport function parseCallApiError(error: {\n message: string;\n status?: number;\n serverDetail?: unknown;\n}): BackendApiError {\n const status = error.status ?? 500;\n const body =\n error.serverDetail &&\n typeof error.serverDetail === \"object\" &&\n !Array.isArray(error.serverDetail)\n ? (error.serverDetail as Record<string, unknown>)\n : { message: error.message };\n return parseHttpErrorBody(body, status);\n}\n\n// ============================================================================\n// STREAMING ERROR PARSER\n// ============================================================================\n\n/**\n * Parse streaming error event data into a BackendApiError.\n *\n * Handles both new format (`user_message`) and legacy (`user_visible_message`).\n */\nexport function parseStreamError(data: unknown): BackendApiError {\n if (!data || typeof data !== \"object\") {\n return new BackendApiError({\n code: \"internal_error\",\n detail: typeof data === \"string\" ? data : \"Unknown streaming error\",\n userMessage: typeof data === \"string\" ? data : \"Something went wrong\",\n });\n }\n\n const obj = data as Record<string, unknown>;\n const details =\n typeof obj.details === \"object\" && obj.details !== null\n ? (obj.details as Record<string, unknown>)\n : null;\n return new BackendApiError({\n code:\n (obj.code as string) ||\n (obj.error_type as string) ||\n (obj.error as string) ||\n \"internal_error\",\n detail: (obj.message as string) || \"Streaming error\",\n userMessage:\n (obj.user_message as string) ||\n (obj.message as string) ||\n \"Something went wrong\",\n details,\n requestId:\n typeof obj.request_id === \"string\"\n ? obj.request_id\n : typeof details?.request_id === \"string\"\n ? details.request_id\n : undefined,\n });\n}\n\n/**\n * Restore the canonical error shape from a durable backend run row.\n *\n * Provider ledgers often keep an aggregate summary plus a more specific first\n * child failure. Prefer that child so a page refresh does not turn a precise\n * streamed failure back into \"1 request failed\".\n */\nexport function parsePersistedBackendError(\n data: unknown,\n requestId = \"\",\n): BackendApiError | null {\n if (!data || typeof data !== \"object\") return null;\n const error = data as Record<string, unknown>;\n const failures = Array.isArray(error.failures) ? error.failures : [];\n const firstSpecific = failures.find(\n (failure): failure is Record<string, unknown> =>\n typeof failure === \"object\" &&\n failure !== null &&\n typeof (failure as Record<string, unknown>).message === \"string\",\n );\n const summary =\n typeof error.message === \"string\"\n ? error.message\n : \"Backend operation failed\";\n const specific =\n typeof firstSpecific?.message === \"string\"\n ? firstSpecific.message\n : summary;\n const detail = specific === summary ? summary : `${summary}: ${specific}`;\n const code = typeof error.type === \"string\" ? error.type : \"internal_error\";\n // A persisted failure may carry a human-facing line the technical detail\n // cannot express — the load-bearing case being a paid result that survived\n // its persistence failure (`WritePreservedError`, D183): \"it broke\" is true\n // and \"your work is preserved and will be recovered\" is what the user needs.\n // `describeBackendFailure` still overrides a TEMPLATED one with the cause.\n const userMessage =\n typeof error.user_message === \"string\" && error.user_message\n ? error.user_message\n : detail;\n return new BackendApiError({\n code,\n detail,\n userMessage,\n details: error,\n requestId,\n });\n}\n\n// ============================================================================\n// FAILURE EXPLANATION — never let a templated non-answer be the whole story\n// ============================================================================\n\n/**\n * Server messages that carry ZERO diagnostic value. The streaming layer\n * (`matrx-connect/streaming/response.py`) emits the first one for every\n * unclassified crash — \"CanonicalGscSync failed unexpectedly. Please try\n * again or adjust your settings.\" — while the REAL cause travels in the same\n * payload's `message`. Treating those as the answer is what makes failures\n * feel secretive.\n */\nconst GENERIC_MESSAGE_PATTERNS: readonly RegExp[] = [\n /failed unexpectedly/i,\n /^\\s*something went wrong/i,\n /please try again(\\s+later)?\\.?\\s*$/i,\n /^\\s*request failed\\b/i,\n /^\\s*unknown (streaming )?error/i,\n /^\\s*internal server error\\.?\\s*$/i,\n];\n\n/** True when a message tells the reader nothing about what actually broke. */\nexport function isGenericUserMessage(\n message: string | null | undefined,\n): boolean {\n const value = (message ?? \"\").trim();\n if (!value) return true;\n return GENERIC_MESSAGE_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nexport interface UpstreamErrorPayload {\n message: string;\n code: string | null;\n userMessage: string | null;\n requestId: string | null;\n status: number | null;\n}\n\n/**\n * Recover an upstream service's structured error that a downstream service\n * stringified into its own message.\n *\n * Real example (scraper wrapping aidream):\n * `aidream could not resolve GSC credential 7223…: HTTP 409 {\"error\":\"conflict\",\n * \"message\":\"Google connection 7223… has no vault credential — it needs\n * re-authentication\",\"user_message\":\"Something went wrong…\",\"request_id\":\"9002…\"}`\n *\n * Without this, the only actionable sentence on the whole hop is invisible.\n */\nexport function unwrapUpstreamError(\n message: string,\n): UpstreamErrorPayload | null {\n const start = message.indexOf(\"{\");\n const end = message.lastIndexOf(\"}\");\n if (start < 0 || end <= start) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(message.slice(start, end + 1));\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null) return null;\n const body = parsed as Record<string, unknown>;\n const inner =\n (typeof body.message === \"string\" && body.message) ||\n (typeof body.detail === \"string\" && body.detail) ||\n \"\";\n if (!inner) return null;\n const statusMatch = /\\bHTTP (\\d{3})\\b/.exec(message.slice(0, start));\n return {\n message: inner,\n code:\n (typeof body.error_type === \"string\" && body.error_type) ||\n (typeof body.error === \"string\" && body.error) ||\n null,\n userMessage:\n typeof body.user_message === \"string\" ? body.user_message : null,\n requestId: typeof body.request_id === \"string\" ? body.request_id : null,\n status: statusMatch ? Number(statusMatch[1]) : null,\n };\n}\n\nexport interface BackendFailureExplanation {\n /** Machine code from the deepest layer that classified the failure. */\n code: string;\n /** The most specific human-readable cause available — never a template. */\n cause: string;\n /** What to headline in the UI: the cause when the server was generic. */\n headline: string;\n /** True when every user-facing message the server sent was a template. */\n headlineWasGeneric: boolean;\n /** Message chain, outermost (closest service) first. */\n chain: string[];\n /** Deepest request id available, for cross-service log correlation. */\n requestId: string;\n status: number | null;\n}\n\n/**\n * THE anti-secrecy primitive: turn any thrown backend/stream failure into the\n * most specific explanation the payload can support — unwrapping every nested\n * upstream error and refusing to let a templated `user_message` be the answer.\n *\n * Every surface that reports a backend failure to a human should headline\n * `explanation.headline` and always keep `cause` + `requestId` reachable.\n */\nexport function describeBackendFailure(\n error: unknown,\n): BackendFailureExplanation {\n const chain: string[] = [];\n let code = \"internal_error\";\n let requestId = \"\";\n let status: number | null = null;\n let userFacing: string | null = null;\n\n if (error instanceof BackendApiError) {\n code = error.code;\n requestId = error.requestId;\n status = error.status;\n userFacing = error.userMessage;\n if (error.detail) chain.push(error.detail);\n if (error.userMessage && error.userMessage !== error.detail) {\n chain.push(error.userMessage);\n }\n } else if (error instanceof Error) {\n chain.push(error.message);\n userFacing = error.message;\n } else if (typeof error === \"string\") {\n chain.push(error);\n userFacing = error;\n } else {\n chain.push(\"Unknown error\");\n }\n\n // Walk the nesting: ANY message in the chain may have stringified the\n // service above it (the technical `detail` usually does, the templated\n // `user_message` never does), so every layer gets unwrapped.\n for (let cursor = 0; cursor < chain.length && cursor < 12; cursor += 1) {\n const upstream = unwrapUpstreamError(chain[cursor] ?? \"\");\n if (!upstream || chain.includes(upstream.message)) continue;\n chain.push(upstream.message);\n if (upstream.code) code = upstream.code;\n if (upstream.requestId) requestId = upstream.requestId;\n if (upstream.status !== null) status = upstream.status;\n }\n\n const specific = [...chain]\n .reverse()\n .find((message) => !isGenericUserMessage(message));\n const cause = specific ?? chain[0] ?? \"Unknown error\";\n const headlineWasGeneric = isGenericUserMessage(userFacing);\n return {\n code,\n cause,\n headline: headlineWasGeneric ? cause : (userFacing ?? cause),\n headlineWasGeneric,\n chain,\n requestId,\n status,\n };\n}\n\n/**\n * Extract a user-visible message from any error object.\n * Utility for components that just need the display string.\n */\nexport function getUserMessage(error: unknown): string {\n if (error instanceof BackendApiError) {\n return error.userMessage;\n }\n if (error instanceof Error) {\n return error.message;\n }\n if (typeof error === \"string\") {\n return error;\n }\n // A Redux thunk's `.unwrap()` rejects with a SerializedError — a plain\n // object, not an Error — whose `message` is the real reason.\n if (\n typeof error === \"object\" &&\n error !== null &&\n typeof (error as { message?: unknown }).message === \"string\" &&\n (error as { message: string }).message\n ) {\n return (error as { message: string }).message;\n }\n return \"Something went wrong\";\n}\n\n// ============================================================================\n// INTERNAL HELPERS\n// ============================================================================\n\nfunction statusToCode(status: number): BackendErrorCode {\n switch (status) {\n case 401:\n return \"auth_required\";\n case 403:\n return \"admin_required\";\n case 404:\n return \"not_found\";\n case 422:\n return \"validation_error\";\n default:\n return \"internal_error\";\n }\n}\n","/**\n * `@ai-matrx/agents/matrx` — the Matrx transport port.\n *\n * The ONE seam between this package's wire semantics and a host's connection\n * policy. This package owns WHAT is said to the AI Matrx server — paths,\n * methods, JSON bodies, streaming Accept headers, the `Last-Event-ID` cursor\n * header — and the host owns HOW the connection is made:\n *\n * - base-URL / backend-channel resolution (global, sandbox override, local\n * engine, EC2-dedicated — whatever ladder the host runs);\n * - credentials (Supabase JWT `Authorization: Bearer`, guest\n * `X-Fingerprint-ID`, or the API-key lane) — NEVER implemented here;\n * - the `X-Organization-Id` context header;\n * - retry policy, network-level timeouts, and diagnostics capture.\n *\n * A host implements the port in a few lines:\n *\n * ```ts\n * const transport: MatrxTransport = {\n * fetch: (path, init) =>\n * fetch(`${baseUrl}${path}`, {\n * ...init,\n * headers: { ...init.headers, ...authHeaders() },\n * }),\n * };\n * ```\n *\n * The port is deliberately structural and `fetch`-shaped so `@ai-matrx/data`'s\n * `/api` transport can implement it without importing this package.\n */\n\nimport { isGenericUserMessage } from \"./backend-errors\";\n\n/**\n * The request this package hands the port. A strict subset of `RequestInit`,\n * so a host can spread it straight into `fetch`.\n */\nexport interface MatrxTransportRequest {\n method: \"GET\" | \"POST\";\n /**\n * Wire-semantic headers the CALL requires (`Content-Type`, `Accept`,\n * `Last-Event-ID`). The host merges its policy headers (auth, org) on top;\n * it must not drop these.\n */\n headers: Record<string, string>;\n /** Pre-serialized JSON body, present on POST calls that carry one. */\n body?: string;\n /** Caller cancellation. The host must wire it to the underlying fetch. */\n signal?: AbortSignal;\n}\n\n/**\n * The transport port. `path` is server-relative and always starts with `/`\n * (`/ai/...`, `/runtime/...`); the host prepends its resolved base URL.\n */\nexport interface MatrxTransport {\n fetch(path: string, init: MatrxTransportRequest): Promise<Response>;\n}\n\n/**\n * A non-2xx response from the Matrx API, with the server's structured error\n * body preserved and its richest human-readable message extracted.\n */\nexport class MatrxApiError extends Error {\n override readonly name = \"MatrxApiError\";\n /** HTTP status of the failed response. */\n readonly status: number;\n /** Machine code from the server body (`code`, or `detail.code`), when present. */\n readonly code: string | null;\n /** The parsed server error body, verbatim (undefined when unparsable). */\n readonly serverDetail: unknown;\n /** The request path the failure came from (server-relative). */\n readonly path: string;\n\n constructor(args: {\n status: number;\n path: string;\n serverDetail?: unknown;\n message?: string;\n }) {\n super(\n args.message ??\n extractMatrxErrorMessage(args.serverDetail) ??\n `HTTP ${args.status}`,\n );\n this.status = args.status;\n this.path = args.path;\n this.serverDetail = args.serverDetail;\n this.code = extractMatrxErrorCode(args.serverDetail);\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction nonBlankString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\n/**\n * Extract the richest human-readable message from a Matrx/FastAPI error body.\n *\n * aidream 4xx validation errors look like\n * `{ error, user_message, details: [{ field, message, help }] }`; hand-raised\n * HTTPExceptions carry `{ detail: { code, message } }`; FastAPI's defaults are\n * `{ detail: string | [{ msg }] }`. Preference order: `user_message` →\n * `message` → joined `details[].message` → `detail.message` →\n * `detail` string → joined `detail[].msg`. Returns undefined for\n * unrecognized bodies so callers fall back to the bare status line.\n */\nexport function extractMatrxErrorMessage(\n serverDetail: unknown,\n): string | undefined {\n if (!isRecord(serverDetail)) return undefined;\n\n // Candidates in preference order. A generic `user_message` (\"Something went\n // wrong\") only wins when nothing more precise exists in the body.\n const candidates: string[] = [];\n const push = (value: string | undefined) => {\n if (value) candidates.push(value);\n };\n push(nonBlankString(serverDetail.user_message));\n push(nonBlankString(serverDetail.message));\n\n if (Array.isArray(serverDetail.details)) {\n const messages = serverDetail.details\n .map((entry: unknown) => {\n if (!isRecord(entry)) return undefined;\n const detailMessage = nonBlankString(entry.message);\n if (!detailMessage) return undefined;\n const field = nonBlankString(entry.field);\n return field ? `${field}: ${detailMessage}` : detailMessage;\n })\n .filter((m): m is string => typeof m === \"string\");\n if (messages.length > 0) push(messages.join(\"; \"));\n }\n\n const detail = serverDetail.detail;\n if (isRecord(detail)) {\n push(nonBlankString(detail.message) ?? nonBlankString(detail.user_message));\n }\n if (typeof detail === \"string\" && detail.trim()) push(detail);\n if (Array.isArray(detail)) {\n const messages = detail\n .map((entry: unknown) =>\n isRecord(entry) ? nonBlankString(entry.msg) : undefined,\n )\n .filter((m): m is string => typeof m === \"string\");\n if (messages.length > 0) push(messages.join(\"; \"));\n }\n return candidates.find((c) => !isGenericUserMessage(c)) ?? candidates[0];\n}\n\n/**\n * Extract the machine error code from a Matrx error body: top-level `code`,\n * else `detail.code` (the hand-raised HTTPException shape). Null when absent.\n */\nexport function extractMatrxErrorCode(serverDetail: unknown): string | null {\n if (!isRecord(serverDetail)) return null;\n const topLevel = nonBlankString(serverDetail.code);\n if (topLevel) return topLevel;\n const detail = serverDetail.detail;\n if (isRecord(detail)) {\n const nested = nonBlankString(detail.code);\n if (nested) return nested;\n }\n return null;\n}\n","/**\n * Internal request plumbing for `@ai-matrx/agents/matrx`. Not part of the\n * public surface — `matrx/index.ts` deliberately does not re-export this\n * module. Everything here is pure: no globals, no work at import time.\n */\n\nimport {\n readMatrxNdjsonStream,\n type MatrxNdjsonIssue,\n type MatrxStreamEnvelope,\n type MatrxStreamEnvelopeObservation,\n} from \"../stream/ndjson\";\nimport { MatrxApiError, type MatrxTransport } from \"./transport\";\n\n/** Encode one path segment (an id) safely into a server-relative path. */\nexport function encodePathSegment(value: string): string {\n return encodeURIComponent(value);\n}\n\nexport type QueryValue =\n | string\n | number\n | boolean\n | readonly string[]\n | undefined;\n\n/**\n * Build a query string. Array values repeat the key (`kind=a&kind=b` — the\n * FastAPI repeatable-parameter convention); undefined values are omitted.\n * Returns \"\" or a string starting with \"?\".\n */\nexport function buildQuery(params: Record<string, QueryValue>): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const entry of value) search.append(key, entry);\n } else {\n search.append(key, String(value));\n }\n }\n const encoded = search.toString();\n return encoded ? `?${encoded}` : \"\";\n}\n\nasync function readServerDetail(response: Response): Promise<unknown> {\n try {\n return (await response.json()) as unknown;\n } catch {\n return undefined;\n }\n}\n\nasync function throwApiError(path: string, response: Response): Promise<never> {\n throw new MatrxApiError({\n status: response.status,\n path,\n serverDetail: await readServerDetail(response),\n });\n}\n\nexport interface JsonRequestOptions {\n method: \"GET\" | \"POST\";\n body?: unknown;\n signal?: AbortSignal;\n}\n\n/**\n * Execute a JSON request through the transport. Throws `MatrxApiError` on a\n * non-2xx response; resolves with the parsed JSON body otherwise.\n */\nexport async function requestJson<T>(\n transport: MatrxTransport,\n path: string,\n options: JsonRequestOptions,\n): Promise<T> {\n const hasBody = options.method !== \"GET\" && options.body !== undefined;\n const response = await transport.fetch(path, {\n method: options.method,\n headers: hasBody ? { \"Content-Type\": \"application/json\" } : {},\n ...(hasBody ? { body: JSON.stringify(options.body) } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n });\n if (!response.ok) return throwApiError(path, response);\n return (await response.json()) as T;\n}\n\n/**\n * Options for every streaming call, riding the NDJSON kernel's contract.\n * Public via `./run`'s re-export.\n */\nexport interface MatrxStreamCallOptions {\n /** Abort the fetch and end the events iterator. */\n signal?: AbortSignal;\n /** Bounded background read-ahead (see `stream/ndjson`). */\n maxReadAhead?: number;\n /** Malformed NDJSON is non-fatal but must never disappear silently. */\n onMalformedLine?: (issue: MatrxNdjsonIssue) => void;\n /** Valid JSON with no recognized Matrx envelope. */\n onUnknownEnvelope?: (value: unknown) => void;\n /** Observe every valid envelope in its exact wire form. */\n onValidEnvelope?: (observation: MatrxStreamEnvelopeObservation) => void;\n}\n\n/**\n * A live agent run: the server-assigned ids (from response headers, available\n * BEFORE any event) and the normalized event stream. Public via `./run`.\n */\nexport interface MatrxRunHandle {\n /** `X-Request-ID` — the ONLY id `POST /ai/cancel/{request_id}` accepts. */\n requestId: string | null;\n /** `X-Conversation-ID` — the server's conversation identity. */\n conversationId: string | null;\n /** Normalized `{event, data}` envelopes through the ONE wire kernel. */\n events: AsyncGenerator<MatrxStreamEnvelope, void, undefined>;\n /** The raw response, for hosts that need headers/status beyond the ids. */\n response: Response;\n}\n\n/**\n * Wrap a validated streaming Response into the run handle — the ONE place\n * the id headers are read and the NDJSON kernel is attached (`./run` and\n * `./operations`' rejoin share it).\n */\nexport function toRunHandle(\n response: Response,\n options: MatrxStreamCallOptions,\n): MatrxRunHandle {\n return {\n requestId: response.headers.get(\"X-Request-ID\"),\n conversationId: response.headers.get(\"X-Conversation-ID\"),\n events: readMatrxNdjsonStream(response.body as ReadableStream<Uint8Array>, {\n ...(options.signal ? { signal: options.signal } : {}),\n ...(options.maxReadAhead !== undefined\n ? { maxReadAhead: options.maxReadAhead }\n : {}),\n ...(options.onMalformedLine\n ? { onMalformedLine: options.onMalformedLine }\n : {}),\n ...(options.onUnknownEnvelope\n ? { onUnknownEnvelope: options.onUnknownEnvelope }\n : {}),\n ...(options.onValidEnvelope\n ? { onValidEnvelope: options.onValidEnvelope }\n : {}),\n }),\n response,\n };\n}\n\nexport interface StreamRequestOptions {\n method: \"GET\" | \"POST\";\n body?: unknown;\n /** Extra wire-semantic headers (`Accept`, `Last-Event-ID`). */\n headers?: Record<string, string>;\n signal?: AbortSignal;\n}\n\n/**\n * Execute a streaming request. Throws `MatrxApiError` on a non-2xx response\n * (reading the error body as JSON when possible) or when a 2xx response\n * carries no body; resolves with the validated `Response` otherwise.\n */\nexport async function requestStream(\n transport: MatrxTransport,\n path: string,\n options: StreamRequestOptions,\n): Promise<Response> {\n const hasBody = options.method !== \"GET\" && options.body !== undefined;\n const response = await transport.fetch(path, {\n method: options.method,\n headers: {\n ...(hasBody ? { \"Content-Type\": \"application/json\" } : {}),\n ...options.headers,\n },\n ...(hasBody ? { body: JSON.stringify(options.body) } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n });\n if (!response.ok) return throwApiError(path, response);\n if (!response.body) {\n throw new MatrxApiError({\n status: response.status,\n path,\n serverDetail: { code: \"missing_response_body\" },\n message: \"The streaming response carried no body.\",\n });\n }\n return response;\n}\n","// AUTO-GENERATED — do not edit by hand.\n// Source: aidream/services/agent_service/portable.py\n// Run: uv run python scripts/generate_types.py portable-agent\n\nexport const PORTABLE_AGENT_CONTRACT = \"portable-agent.v1\";\n\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\nexport interface PortableAgentRequest {\n variables?: Record<string, JsonValue>;\n surface?: string;\n}\n\nexport interface PortableSkill {\n id: string;\n tier: \"included\" | \"listed\";\n slug: string;\n label: string;\n description: string;\n path: string;\n content: string;\n body: string;\n content_hash: string;\n}\n\nexport interface PortableTool {\n name: string;\n canonical_name: string;\n description: string;\n parameters: Record<string, JsonValue>;\n}\n\nexport interface PortableUnavailable {\n kind: \"tool\" | \"skill\" | \"variable\" | \"message\" | \"mcp_server\" | \"tools\";\n name: string;\n reason: string;\n}\n\nexport interface PortableAgentBundle {\n contract: \"portable-agent.v1\";\n agent_id: string;\n version_id: string | null;\n version_number: number | null;\n is_version: boolean;\n name: string;\n definition_hash: string;\n model_id: string;\n instructions: string;\n variables: Record<string, JsonValue>[];\n filled_variables: string[];\n skills: PortableSkill[];\n tools: PortableTool[];\n surface: string;\n unavailable: PortableUnavailable[];\n}\n\nexport interface PortableAgentVersion {\n id: string;\n version_number: number;\n changed_at: string | null;\n definition_hash: string | null;\n}\n\nexport interface PortableAgentSummary {\n agent_id: string;\n name: string;\n version_id: string | null;\n version_number: number | null;\n definition_hash: string;\n variables: Record<string, JsonValue>[];\n versions: PortableAgentVersion[];\n}\n","/**\n * `@ai-matrx/agents/portable` — an AI Matrx agent as pieces a coding CLI loads.\n *\n * `fetchPortableAgent` reads `POST /ai/agents/{id}/portable` (or the version\n * door) and returns the server's `PortableAgentBundle`: the instructions with\n * the person's variable values filled, the skills as Claude `SKILL.md` files,\n * the server tools in MCP shape, and `unavailable[]` — what could not be\n * carried and why. The types are GENERATED from the server's Pydantic models\n * (`types.generated.ts`, `uv run python scripts/generate_types.py portable-agent`\n * in aidream), so the two cannot drift.\n *\n * Pure entry: no React, no Node, no I/O at import. Credentials and the\n * organization travel through the host's `MatrxTransport`\n * (`createMatrxTransport({ credentials, organizationId })`).\n */\n\nimport { encodePathSegment, requestJson } from \"../matrx/internal\";\nimport type { MatrxTransport } from \"../matrx/transport\";\nimport type {\n JsonValue,\n PortableAgentBundle,\n PortableAgentRequest,\n PortableAgentSummary,\n} from \"./types.generated\";\n\nexport * from \"./types.generated\";\n\n/** Which definition to load: the agent's current one, or one immutable version. */\nexport type PortableAgentTarget =\n | { agentId: string; versionId?: undefined }\n | { versionId: string; agentId?: string | undefined };\n\nexport interface FetchPortableAgentOptions {\n /** The person's values, by variable name. Unfilled variables use the agent's defaults. */\n variables?: Record<string, JsonValue>;\n /** The `ui.ui_surface` tools resolve on. Default: the server's coding-session surface. */\n surface?: string;\n signal?: AbortSignal;\n}\n\n/** The server path for a target — exported so a host's tests can assert it. */\nexport function portableAgentPath(target: PortableAgentTarget): string {\n return target.versionId\n ? `/ai/agents/versions/${encodePathSegment(target.versionId)}/portable`\n : `/ai/agents/${encodePathSegment(target.agentId ?? \"\")}/portable`;\n}\n\n/**\n * Load one agent as a portable bundle. Throws `MatrxApiError` on refusal —\n * 401 signed out, 404 not found or not shared with this person.\n */\nexport function fetchPortableAgent(\n transport: MatrxTransport,\n target: PortableAgentTarget,\n options: FetchPortableAgentOptions = {},\n): Promise<PortableAgentBundle> {\n const body: PortableAgentRequest = {\n variables: options.variables ?? {},\n ...(options.surface ? { surface: options.surface } : {}),\n };\n return requestJson<PortableAgentBundle>(transport, portableAgentPath(target), {\n method: \"POST\",\n body,\n ...(options.signal ? { signal: options.signal } : {}),\n });\n}\n\n/**\n * The light read behind an agent picker (`GET /ai/agents/{id}/portable/summary`):\n * name, variable declarations, versions newest first with their hashes. No\n * skills, no tools — call this when a person CHOOSES an agent, and\n * `fetchPortableAgent` only when a session starts.\n */\nexport function fetchPortableAgentSummary(\n transport: MatrxTransport,\n agentId: string,\n options: { signal?: AbortSignal } = {},\n): Promise<PortableAgentSummary> {\n return requestJson<PortableAgentSummary>(\n transport,\n `/ai/agents/${encodePathSegment(agentId)}/portable/summary`,\n { method: \"GET\", ...(options.signal ? { signal: options.signal } : {}) },\n );\n}\n\n/** One saved version of an agent (`AgentVersionInfo` server-side). */\nexport interface PortableAgentVersion {\n id: string;\n agent_id: string;\n version_number: number;\n name: string;\n change_note: string | null;\n changed_at: string | null;\n}\n\n/**\n * The agent's saved versions, oldest first (`GET /agent-service/agents/{id}/versions`,\n * authorized through the same viewer rule as the agent). Feeds a version menu.\n */\nexport function fetchPortableAgentVersions(\n transport: MatrxTransport,\n agentId: string,\n options: { signal?: AbortSignal } = {},\n): Promise<PortableAgentVersion[]> {\n return requestJson<PortableAgentVersion[]>(\n transport,\n `/agent-service/agents/${encodePathSegment(agentId)}/versions`,\n { method: \"GET\", ...(options.signal ? { signal: options.signal } : {}) },\n );\n}\n\n/** What a bundle loaded, counted — what a header chip shows. */\nexport interface PortableAgentLoadSummary {\n instructions: boolean;\n skills: { loaded: number; total: number };\n tools: { loaded: number; total: number };\n}\n\n/** Count what a bundle carries against what the agent declares. */\nexport function summarizePortableAgent(bundle: PortableAgentBundle): PortableAgentLoadSummary {\n const missingSkills = bundle.unavailable.filter((u) => u.kind === \"skill\").length;\n const missingTools = bundle.unavailable.filter((u) => u.kind === \"tool\").length;\n return {\n instructions: bundle.instructions.trim().length > 0,\n skills: { loaded: bundle.skills.length, total: bundle.skills.length + missingSkills },\n tools: { loaded: bundle.tools.length, total: bundle.tools.length + missingTools },\n };\n}\n"],"mappings":"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","names":[]}
@@ -68,6 +68,19 @@ var BackendApiError = class extends Error {
68
68
  };
69
69
  }
70
70
  };
71
+ var GENERIC_MESSAGE_PATTERNS = [
72
+ /failed unexpectedly/i,
73
+ /^\s*something went wrong/i,
74
+ /please try again(\s+later)?\.?\s*$/i,
75
+ /^\s*request failed\b/i,
76
+ /^\s*unknown (streaming )?error/i,
77
+ /^\s*internal server error\.?\s*$/i
78
+ ];
79
+ function isGenericUserMessage(message) {
80
+ const value = (message ?? "").trim();
81
+ if (!value) return true;
82
+ return GENERIC_MESSAGE_PATTERNS.some((pattern) => pattern.test(value));
83
+ }
71
84
 
72
85
  // matrx/org-context.ts
73
86
  var import_uuid = require("@ai-matrx/kit/uuid");
@@ -208,10 +221,12 @@ function nonBlankString(value) {
208
221
  }
209
222
  function extractMatrxErrorMessage(serverDetail) {
210
223
  if (!isRecord(serverDetail)) return void 0;
211
- const userMessage = nonBlankString(serverDetail.user_message);
212
- if (userMessage) return userMessage;
213
- const message = nonBlankString(serverDetail.message);
214
- if (message) return message;
224
+ const candidates = [];
225
+ const push = (value) => {
226
+ if (value) candidates.push(value);
227
+ };
228
+ push(nonBlankString(serverDetail.user_message));
229
+ push(nonBlankString(serverDetail.message));
215
230
  if (Array.isArray(serverDetail.details)) {
216
231
  const messages = serverDetail.details.map((entry) => {
217
232
  if (!isRecord(entry)) return void 0;
@@ -220,21 +235,20 @@ function extractMatrxErrorMessage(serverDetail) {
220
235
  const field = nonBlankString(entry.field);
221
236
  return field ? `${field}: ${detailMessage}` : detailMessage;
222
237
  }).filter((m) => typeof m === "string");
223
- if (messages.length > 0) return messages.join("; ");
238
+ if (messages.length > 0) push(messages.join("; "));
224
239
  }
225
240
  const detail = serverDetail.detail;
226
241
  if (isRecord(detail)) {
227
- const detailMessage = nonBlankString(detail.message) ?? nonBlankString(detail.user_message);
228
- if (detailMessage) return detailMessage;
242
+ push(nonBlankString(detail.message) ?? nonBlankString(detail.user_message));
229
243
  }
230
- if (typeof detail === "string" && detail.trim()) return detail;
244
+ if (typeof detail === "string" && detail.trim()) push(detail);
231
245
  if (Array.isArray(detail)) {
232
246
  const messages = detail.map(
233
247
  (entry) => isRecord(entry) ? nonBlankString(entry.msg) : void 0
234
248
  ).filter((m) => typeof m === "string");
235
- if (messages.length > 0) return messages.join("; ");
249
+ if (messages.length > 0) push(messages.join("; "));
236
250
  }
237
- return void 0;
251
+ return candidates.find((c) => !isGenericUserMessage(c)) ?? candidates[0];
238
252
  }
239
253
  function extractMatrxErrorCode(serverDetail) {
240
254
  if (!isRecord(serverDetail)) return null;