{"version":3,"file":"as-tool.mjs","names":[],"sources":["../../../../../../../ai/src/orchestrator/as-tool.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type {\n  OrchestratorAsToolOptions,\n  OrchestratorContract,\n  OrchestratorToolSession,\n} from \"../contracts/orchestrator/orchestrator.contract\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\nimport type { ToolContext } from \"../contracts/tool.contract\";\nimport { SupervisorFailedError } from \"../errors\";\nimport { compositeAsTool, type ToolContract } from \"../tool/tool\";\nimport { generateRunId } from \"../utils/generate-run-id\";\n\n/**\n * Shape read out of the validated tool input ONLY under the\n * `unsafeAllowModelSessionId` opt-in — the legacy, model-chosen session\n * path. Everything else on the payload becomes the orchestrator's\n * `execute(input)` argument.\n */\ntype SharedScopePayload = {\n  sessionId?: unknown;\n  history?: unknown;\n  [key: string]: unknown;\n};\n\n/** Resolved per-call session binding for one tool invocation. */\ntype ResolvedToolSession = {\n  sessionId: string;\n  history: Message[];\n  executeInput: SupervisorInput;\n};\n\n/**\n * Wrap an {@link OrchestratorContract} as a {@link ToolContract} so an\n * outer agent can invoke it from its tool-call loop (design §13).\n * Mirrors `supervisor.asTool()` — same `compositeAsTool` composition and\n * error normalization — and adds `sessionScope`.\n *\n * The boundary is OPAQUE (§13, §18.6): the parent's `context` / events do\n * NOT auto-forward. Per-call data the wrapped orchestrator needs rides on\n * the tool's `inputSchema` payload — with ONE deliberate exception, the\n * session binding below, because the payload is written by an LLM.\n *\n * Session continuity:\n * - `\"fresh\"` (default) — each invocation gets a brand-new `sessionId`\n *   (a generated id) and empty history; the session lives only for this\n *   tool call. The whole validated payload is forwarded as the\n *   orchestrator's `execute(input)` argument.\n * - `\"shared\"` — the orchestrator joins an EXISTING session named by the\n *   developer through `options.session`: either a literal id fixed at\n *   construction, or a resolver that reads the invocation's\n *   {@link ToolContext} (`ctx.artifacts`, the out-of-band bag the model\n *   cannot write to). The whole validated payload is forwarded as\n *   `execute(input)`. A `\"shared\"` tool built without `session` throws at\n *   construction.\n *\n * **Why the session id is not a schema field (4.15.0 security fix).**\n * Before this release, `\"shared\"` scope read `sessionId` straight out of\n * the model-generated tool arguments. A `sessionId` is bearer-equivalent\n * — naming one grants read/write on that session's persisted state — so\n * any prompt injection reaching the outer agent (\"continue session\n * `<victim-id>`\") made the nested orchestrator load a stranger's\n * conversation, mutate it, and echo its content back into the attacker's\n * transcript. The binding now lives on channels the model has no access\n * to. The old behavior survives only behind the loudly-named\n * `unsafeAllowModelSessionId` opt-in.\n *\n * On `result.error`, the typed orchestrator error is thrown so the tool\n * wrapper produces a `ToolExecutionError` with `cause` preserved — the\n * outer agent sees one uniform error class.\n *\n * @example\n * const support = ai.orchestrator({ name: \"refund-support\", intents });\n *\n * // Fresh session per call — no continuity, nothing to hijack.\n * const supportTool = support.asTool({\n *   name: \"handle_refund\",\n *   description: \"Handle a refund conversation end-to-end.\",\n *   inputSchema: v.object({ message: v.string() }),\n * });\n *\n * // Continuous session — bound from the authenticated request, never\n * // from the model's arguments.\n * const continuousTool = support.asTool({\n *   name: \"handle_refund\",\n *   inputSchema: v.object({ message: v.string() }),\n *   sessionScope: \"shared\",\n *   session: (ctx) => ({\n *     sessionId: String(ctx?.artifacts?.refundSessionId ?? \"\"),\n *   }),\n * });\n */\nexport function asTool<TOutput, TState, TToolInput>(\n  orchestrator: OrchestratorContract<TOutput, TState>,\n  options: OrchestratorAsToolOptions<TToolInput>,\n): ToolContract<TToolInput, TOutput> {\n  if (!orchestrator.name || typeof orchestrator.name !== \"string\") {\n    throw new SupervisorFailedError(\n      \"orchestrator.asTool(): orchestrator must have a `name` to be wrapped as a tool\",\n    );\n  }\n\n  const sessionScope = options.sessionScope ?? \"fresh\";\n  const allowModelSessionId = options.unsafeAllowModelSessionId === true;\n\n  // Fail closed at construction, not at the first hostile tool call: a\n  // \"shared\" tool with no developer-supplied binding would have to fall\n  // back to the model's payload, which is exactly the hijack path.\n  if (sessionScope === \"shared\" && !options.session && !allowModelSessionId) {\n    throw new SupervisorFailedError(\n      'orchestrator.asTool(): sessionScope \"shared\" requires a `session` binding — ' +\n        \"a session id fixed at construction, or a `(ctx) => sessionId` resolver reading the \" +\n        \"tool context. A model-supplied `sessionId` in the tool payload is bearer-equivalent \" +\n        \"access to that session; pass `unsafeAllowModelSessionId: true` only if the outer \" +\n        \"agent's context is trusted and you verify session ownership yourself\",\n    );\n  }\n\n  return compositeAsTool<TToolInput, TOutput>({\n    name: options.name ?? orchestrator.name,\n    description:\n      options.description ??\n      `Invoke orchestrator \"${orchestrator.name}\" as a tool.`,\n    input: options.inputSchema,\n    execute: async (input, ctx) => {\n      const { sessionId, history, executeInput } = await resolveSession(\n        sessionScope,\n        input,\n        ctx,\n        options.session,\n        allowModelSessionId,\n      );\n\n      const result = await orchestrator.execute(executeInput, {\n        sessionId,\n        history,\n      });\n\n      if (result.error) {\n        // Surface the typed orchestrator error — the outer ToolContract\n        // wraps it as a ToolExecutionError with `cause` preserved.\n        throw result.error;\n      }\n\n      return {\n        data: result.data as TOutput,\n        usage: result.usage,\n        report: result.report,\n      };\n    },\n  });\n}\n\n/**\n * Resolve the per-call `sessionId`, `history`, and the `execute(input)`\n * argument, according to `sessionScope`.\n *\n * For `\"shared\"` scope the session comes from the developer's `session`\n * binding (construction-time literal or `ToolContext` resolver) — the\n * validated payload is never consulted for it unless the caller opted\n * into `unsafeAllowModelSessionId`. Either way `sessionId` / `history`\n * are stripped from the payload before it is forwarded as\n * `execute(input)`, so a model-authored field of that name can't reach\n * the orchestrator's input under a misleading name.\n */\nasync function resolveSession(\n  sessionScope: \"fresh\" | \"shared\",\n  input: unknown,\n  ctx: ToolContext | undefined,\n  session: OrchestratorToolSession | undefined,\n  allowModelSessionId: boolean,\n): Promise<ResolvedToolSession> {\n  if (sessionScope === \"fresh\") {\n    return {\n      sessionId: generateRunId(\"session\"),\n      history: [],\n      executeInput: coerceInput(input),\n    };\n  }\n\n  const payload = (\n    typeof input === \"object\" && input !== null ? input : {}\n  ) as SharedScopePayload;\n\n  const { sessionId: payloadSessionId, history: payloadHistory, ...rest } = payload;\n  const executeInput = coerceInput(rest);\n\n  if (session !== undefined) {\n    const bound = typeof session === \"function\" ? await session(ctx) : session;\n\n    const sessionId = typeof bound === \"string\" ? bound : bound?.sessionId;\n    const history = typeof bound === \"string\" ? undefined : bound?.history;\n\n    if (typeof sessionId !== \"string\" || sessionId.length === 0) {\n      throw new SupervisorFailedError(\n        'orchestrator.asTool(): the `session` binding for sessionScope \"shared\" resolved to no ' +\n          \"session id — return a non-empty string (or `{ sessionId }`) from it, or throw to \" +\n          \"reject the call. The model's payload is never used as a fallback\",\n      );\n    }\n\n    return {\n      sessionId,\n      history: Array.isArray(history) ? history : [],\n      executeInput,\n    };\n  }\n\n  // Legacy, explicitly opted-in path: the session id is whatever the\n  // calling model wrote. Anything that can influence that model chooses\n  // the session — see `unsafeAllowModelSessionId`.\n  if (!allowModelSessionId) {\n    throw new SupervisorFailedError(\n      'orchestrator.asTool(): sessionScope \"shared\" requires a `session` binding',\n    );\n  }\n\n  if (typeof payloadSessionId !== \"string\" || payloadSessionId.length === 0) {\n    throw new SupervisorFailedError(\n      'orchestrator.asTool(): sessionScope \"shared\" requires a `sessionId` string in the tool input payload',\n    );\n  }\n\n  return {\n    sessionId: payloadSessionId,\n    history: Array.isArray(payloadHistory) ? (payloadHistory as Message[]) : [],\n    executeInput,\n  };\n}\n\n/**\n * Coerce a tool-input value into the `SupervisorInput` shape the\n * orchestrator's `execute()` accepts (`string | Record<string,\n * unknown>`). Strings and plain objects pass through; everything else\n * is JSON-stringified so the orchestrator receives a predictable input\n * regardless of how the outer agent shaped its call.\n */\nfunction coerceInput(value: unknown): SupervisorInput {\n  if (typeof value === \"string\") {\n    return value;\n  }\n\n  if (typeof value === \"object\" && value !== null) {\n    return value as Record<string, unknown>;\n  }\n\n  if (value === undefined || value === null) {\n    return \"\";\n  }\n\n  return String(value);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2FA,SAAgB,OACd,cACA,SACmC;CACnC,IAAI,CAAC,aAAa,QAAQ,OAAO,aAAa,SAAS,UACrD,MAAM,IAAI,sBACR,gFACF;CAGF,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,sBAAsB,QAAQ,8BAA8B;CAKlE,IAAI,iBAAiB,YAAY,CAAC,QAAQ,WAAW,CAAC,qBACpD,MAAM,IAAI,sBACR,4YAKF;CAGF,OAAO,gBAAqC;EAC1C,MAAM,QAAQ,QAAQ,aAAa;EACnC,aACE,QAAQ,eACR,wBAAwB,aAAa,KAAK;EAC5C,OAAO,QAAQ;EACf,SAAS,OAAO,OAAO,QAAQ;GAC7B,MAAM,EAAE,WAAW,SAAS,iBAAiB,MAAM,eACjD,cACA,OACA,KACA,QAAQ,SACR,mBACF;GAEA,MAAM,SAAS,MAAM,aAAa,QAAQ,cAAc;IACtD;IACA;GACF,CAAC;GAED,IAAI,OAAO,OAGT,MAAM,OAAO;GAGf,OAAO;IACL,MAAM,OAAO;IACb,OAAO,OAAO;IACd,QAAQ,OAAO;GACjB;EACF;CACF,CAAC;AACH;;;;;;;;;;;;;AAcA,eAAe,eACb,cACA,OACA,KACA,SACA,qBAC8B;CAC9B,IAAI,iBAAiB,SACnB,OAAO;EACL,WAAW,cAAc,SAAS;EAClC,SAAS,CAAC;EACV,cAAc,YAAY,KAAK;CACjC;CAOF,MAAM,EAAE,WAAW,kBAAkB,SAAS,gBAAgB,GAAG,SAH/D,OAAO,UAAU,YAAY,UAAU,OAAO,QAAQ,CAAC;CAIzD,MAAM,eAAe,YAAY,IAAI;CAErC,IAAI,YAAY,QAAW;EACzB,MAAM,QAAQ,OAAO,YAAY,aAAa,MAAM,QAAQ,GAAG,IAAI;EAEnE,MAAM,YAAY,OAAO,UAAU,WAAW,QAAQ,OAAO;EAC7D,MAAM,UAAU,OAAO,UAAU,WAAW,SAAY,OAAO;EAE/D,IAAI,OAAO,cAAc,YAAY,UAAU,WAAW,GACxD,MAAM,IAAI,sBACR,2OAGF;EAGF,OAAO;GACL;GACA,SAAS,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC;GAC7C;EACF;CACF;CAKA,IAAI,CAAC,qBACH,MAAM,IAAI,sBACR,6EACF;CAGF,IAAI,OAAO,qBAAqB,YAAY,iBAAiB,WAAW,GACtE,MAAM,IAAI,sBACR,wGACF;CAGF,OAAO;EACL,WAAW;EACX,SAAS,MAAM,QAAQ,cAAc,IAAK,iBAA+B,CAAC;EAC1E;CACF;AACF;;;;;;;;AASA,SAAS,YAAY,OAAiC;CACpD,IAAI,OAAO,UAAU,UACnB,OAAO;CAGT,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO;CAGT,IAAI,UAAU,UAAa,UAAU,MACnC,OAAO;CAGT,OAAO,OAAO,KAAK;AACrB"}