{"version":3,"file":"human-approval.mjs","names":[],"sources":["../../../../../../../ai/src/human/human-approval.ts"],"sourcesContent":["import type { AgentMiddleware } from \"../contracts/middleware/middleware.contract\";\nimport type { MiddlewareToolContext } from \"../contracts/middleware/middleware-context.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { Usage } from \"../contracts/result/usage.type\";\nimport type { AIError } from \"../errors/ai-error\";\nimport type { ToolInvokeResult } from \"../tool/tool\";\nimport { generateRunId } from \"../utils/generate-run-id\";\nimport type {\n  ApprovalDecision,\n  ApprovalRequest,\n  HumanApprovalOptions,\n  PolicyContext,\n} from \"./contracts\";\nimport { ApprovalRejectedError, InterruptSuspendedError } from \"./errors\";\nimport { evaluatePolicy } from \"./policy\";\nimport { takeSeededDecision } from \"./resume-seed\";\n\n/** Default middleware name when {@link HumanApprovalOptions.name} is omitted. */\nconst DEFAULT_NAME = \"human-approval\";\n\n/** Zero usage for a synthetic, no-LLM-spend short-circuit result. */\nconst ZERO_USAGE: Usage = Object.freeze({ input: 0, output: 0, total: 0 });\n\n/**\n * Mutable view of {@link MiddlewareToolContext.request} used only to\n * apply an `edit` decision. The context types `request.input` as\n * `readonly`, but the agent dispatch reads `request.input` (the SAME\n * object) when it invokes the real tool *after* the `tool.before`\n * pipeline returns — so reassigning it here is how an edited-args\n * decision reaches the tool. This narrow local type makes that one\n * deliberate write explicit instead of casting away the whole context.\n */\ninterface MutableToolRequest {\n  input: unknown;\n}\n\n/**\n * Derive the read-only {@link PolicyContext} the policy + request are\n * built from out of the wrapping {@link MiddlewareToolContext}.\n */\nfunction toPolicyContext(ctx: MiddlewareToolContext): PolicyContext {\n  return {\n    toolName: ctx.tool.name,\n    toolDescription: ctx.tool.description,\n    args: ctx.request.input,\n    agentName: ctx.agent.name,\n    tripIndex: ctx.tripIndex,\n    sessionId: ctx.options?.sessionId,\n  };\n}\n\n/**\n * Generate a stable, unique id for a pending interrupt. Shaped\n * `${agentName}.${sessionId ?? \"nosession\"}.${tripIndex}.${random}` so a\n * reviewer can eyeball the originating run, while the trailing random\n * segment guarantees per-call uniqueness even within one trip.\n */\nfunction makeInterruptId(ctx: MiddlewareToolContext): string {\n  const session = ctx.options?.sessionId ?? \"nosession\";\n  const random = generateRunId(\"interrupt\");\n\n  return `${ctx.agent.name}.${session}.${ctx.tripIndex}.${random}`;\n}\n\n/**\n * Build the {@link ApprovalRequest} a human rules on, from the tool\n * context and the policy-derived tags.\n */\nfunction buildRequest(\n  ctx: MiddlewareToolContext,\n  interruptId: string,\n  tags: string[] | undefined,\n): ApprovalRequest {\n  return {\n    interruptId,\n    toolName: ctx.tool.name,\n    toolDescription: ctx.tool.description,\n    args: ctx.request.input,\n    context: {\n      agentName: ctx.agent.name,\n      tripIndex: ctx.tripIndex,\n      sessionId: ctx.options?.sessionId,\n      originalInput: ctx.input,\n      ...(tags ? { tags } : {}),\n    },\n    requestedAt: new Date().toISOString(),\n  };\n}\n\n/**\n * Synthesize a failed {@link ToolInvokeResult} carrying a typed error.\n *\n * The approval middleware returns this from `tool.before` to\n * **short-circuit** the real tool without throwing: the pipeline treats a\n * defined return as the tool's result, the agent records a failed\n * `ToolCall`, and the model sees `{ error }` on the next trip — exactly\n * the existing tool-error feedback path. Used for both `reject`\n * (`ApprovalRejectedError`) and durable suspend (`InterruptSuspendedError`).\n */\nfunction failedResult(error: AIError, toolName: string): ToolInvokeResult<unknown> {\n  const runId = generateRunId(\"tool\");\n  const nowIso = new Date().toISOString();\n\n  const report: BaseReport = {\n    runId,\n    rootRunId: runId,\n    name: toolName,\n    type: \"tool\",\n    status: \"failed\",\n    startedAt: nowIso,\n    endedAt: nowIso,\n    duration: 0,\n    usage: ZERO_USAGE,\n    children: [],\n  };\n\n  return { error, usage: ZERO_USAGE, report };\n}\n\n/**\n * Human-in-the-loop approval gate for an agent's tool calls — the\n * middleware behind `ai.human.approval(options)`.\n *\n * **Role.** Pauses *before a specific tool call* and routes it to a human\n * who can **approve** (run the real tool unchanged), **reject** (the model\n * sees a typed error and self-corrects), or **edit** (run the tool with\n * reviewer-replaced args). The dangerous subset is chosen by an\n * {@link import(\"./contracts\").InterruptPolicy} (allowlist / denylist /\n * predicate); every other call passes through untouched.\n *\n * **One hook.** Declares only `tool.before`. On each tool dispatch it:\n * 1. evaluates the policy — not gated → returns `void`, the real tool runs;\n * 2. for a gated call, builds an {@link ApprovalRequest} and calls the\n *    {@link import(\"./contracts\").ApprovalHandler};\n * 3. applies the returned {@link ApprovalDecision}:\n *    - `approve` → returns `void`, the real tool runs;\n *    - `reject` → short-circuits a failed `ToolInvokeResult` carrying an\n *      {@link ApprovalRejectedError} (the reviewer's `reason` reaches the\n *      model);\n *    - `edit` → rewrites `ctx.request.input` to the reviewer's args and\n *      returns `void`, so the real tool runs with the edited args (schema\n *      validation still applies — bad edits surface as a tool error).\n *\n * **Durable mode.** When a `store` is configured and the handler throws\n * {@link InterruptSuspendedError} (after persisting the interrupt\n * out-of-band), the middleware catches its **own** sentinel and\n * short-circuits a failed result carrying it — so the caller reads\n * `result.error.interruptId` and later calls\n * `ai.human.resume(interruptId, decision)`. The middleware **never throws\n * out of the pipeline**: every outcome (skip, approve, reject, edit,\n * suspend) returns normally; only a *handler bug* (a non-sentinel throw)\n * propagates, and even then the agent dispatch funnels it onto\n * `result.error` — `execute()` still never throws.\n *\n * @param options - Policy, handler, optional durable store, optional name.\n * @returns An {@link AgentMiddleware} declaring a single `tool.before` hook.\n *\n * @example\n * const support = ai.agent({\n *   model,\n *   tools: [refundCustomer],\n *   middleware: [\n *     humanApproval({\n *       policy: { type: \"allowlist\", tools: [\"refundCustomer\"], tags: () => [\"money\"] },\n *       handler: async (req) => ui.prompt(req), // { type: \"edit\", args: { amount: 5 } }\n *     }),\n *   ],\n * });\n */\nexport function humanApproval(options: HumanApprovalOptions): AgentMiddleware {\n  const name = options.name ?? DEFAULT_NAME;\n  const { policy, handler } = options;\n\n  return {\n    name,\n    tool: {\n      async before(\n        ctx: MiddlewareToolContext,\n      ): Promise<ToolInvokeResult<unknown> | void> {\n        const verdict = evaluatePolicy(policy, toPolicyContext(ctx));\n\n        // Not gated — let the real tool run unchanged.\n        if (!verdict.requiresApproval) {\n          return;\n        }\n\n        const interruptId = makeInterruptId(ctx);\n        const request = buildRequest(ctx, interruptId, verdict.tags);\n\n        // Durable resume: `ai.human.resume(...)` re-runs this same agent\n        // with the human's decision pre-seeded (keyed by agent name). On a\n        // hit we replay the seeded decision exactly once and skip the\n        // author's handler entirely — the gated call resolves to the\n        // ruling instead of pausing again.\n        const seeded = takeSeededDecision(ctx.agent.name);\n\n        let decision: ApprovalDecision;\n\n        if (seeded !== undefined) {\n          decision = seeded;\n        } else {\n          try {\n            decision = await handler(request);\n          } catch (thrown) {\n            // A durable handler signals suspension by throwing our OWN\n            // sentinel after persisting the interrupt. Recognize it and\n            // short-circuit a failed result carrying it — the caller reads\n            // `error.interruptId` and resumes later. Any OTHER throw is a\n            // handler bug; re-throw so the agent dispatch funnels it onto\n            // `result.error` (we never swallow a bug into silent approval).\n            if (thrown instanceof InterruptSuspendedError) {\n              return failedResult(thrown, ctx.tool.name);\n            }\n\n            throw thrown;\n          }\n        }\n\n        if (decision.type === \"approve\") {\n          // Run the real tool with the model's original args.\n          return;\n        }\n\n        if (decision.type === \"reject\") {\n          const error = new ApprovalRejectedError(\n            `Tool call \"${ctx.tool.name}\" rejected by reviewer — ${decision.reason}`,\n            { reason: decision.reason, toolName: ctx.tool.name },\n          );\n\n          return failedResult(error, ctx.tool.name);\n        }\n\n        // `edit` — rewrite the pending args, then let the real tool run.\n        // The agent dispatch reads `request.input` (this same object) when\n        // it invokes the tool after this hook returns, so the reassignment\n        // takes effect. Bad edits still fail the tool's own schema check.\n        (ctx.request as unknown as MutableToolRequest).input = decision.args;\n\n        return;\n      },\n    },\n  };\n}\n"],"mappings":";;;;;;;AAkBA,MAAM,eAAe;;AAGrB,MAAM,aAAoB,OAAO,OAAO;CAAE,OAAO;CAAG,QAAQ;CAAG,OAAO;AAAE,CAAC;;;;;AAmBzE,SAAS,gBAAgB,KAA2C;CAClE,OAAO;EACL,UAAU,IAAI,KAAK;EACnB,iBAAiB,IAAI,KAAK;EAC1B,MAAM,IAAI,QAAQ;EAClB,WAAW,IAAI,MAAM;EACrB,WAAW,IAAI;EACf,WAAW,IAAI,SAAS;CAC1B;AACF;;;;;;;AAQA,SAAS,gBAAgB,KAAoC;CAC3D,MAAM,UAAU,IAAI,SAAS,aAAa;CAC1C,MAAM,SAAS,cAAc,WAAW;CAExC,OAAO,GAAG,IAAI,MAAM,KAAK,GAAG,QAAQ,GAAG,IAAI,UAAU,GAAG;AAC1D;;;;;AAMA,SAAS,aACP,KACA,aACA,MACiB;CACjB,OAAO;EACL;EACA,UAAU,IAAI,KAAK;EACnB,iBAAiB,IAAI,KAAK;EAC1B,MAAM,IAAI,QAAQ;EAClB,SAAS;GACP,WAAW,IAAI,MAAM;GACrB,WAAW,IAAI;GACf,WAAW,IAAI,SAAS;GACxB,eAAe,IAAI;GACnB,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;EACzB;EACA,8BAAa,IAAI,KAAK,EAAC,CAAC,YAAY;CACtC;AACF;;;;;;;;;;;AAYA,SAAS,aAAa,OAAgB,UAA6C;CACjF,MAAM,QAAQ,cAAc,MAAM;CAClC,MAAM,0BAAS,IAAI,KAAK,EAAC,CAAC,YAAY;CAetC,OAAO;EAAE;EAAO,OAAO;EAAY;GAZjC;GACA,WAAW;GACX,MAAM;GACN,MAAM;GACN,QAAQ;GACR,WAAW;GACX,SAAS;GACT,UAAU;GACV,OAAO;GACP,UAAU,CAAC;EAG2B;CAAE;AAC5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,cAAc,SAAgD;CAC5E,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,EAAE,QAAQ,YAAY;CAE5B,OAAO;EACL;EACA,MAAM,EACJ,MAAM,OACJ,KAC2C;GAC3C,MAAM,UAAU,eAAe,QAAQ,gBAAgB,GAAG,CAAC;GAG3D,IAAI,CAAC,QAAQ,kBACX;GAIF,MAAM,UAAU,aAAa,KADT,gBAAgB,GACQ,GAAG,QAAQ,IAAI;GAO3D,MAAM,SAAS,mBAAmB,IAAI,MAAM,IAAI;GAEhD,IAAI;GAEJ,IAAI,WAAW,QACb,WAAW;QAEX,IAAI;IACF,WAAW,MAAM,QAAQ,OAAO;GAClC,SAAS,QAAQ;IAOf,IAAI,kBAAkB,yBACpB,OAAO,aAAa,QAAQ,IAAI,KAAK,IAAI;IAG3C,MAAM;GACR;GAGF,IAAI,SAAS,SAAS,WAEpB;GAGF,IAAI,SAAS,SAAS,UAMpB,OAAO,aAAa,IALF,sBAChB,cAAc,IAAI,KAAK,KAAK,2BAA2B,SAAS,UAChE;IAAE,QAAQ,SAAS;IAAQ,UAAU,IAAI,KAAK;GAAK,CAG7B,GAAG,IAAI,KAAK,IAAI;GAO1C,AAAC,IAAI,QAA0C,QAAQ,SAAS;EAGlE,EACF;CACF;AACF"}