@dudousxd/nestjs-agent-core 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,8 +35,10 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
35
35
  - `RunCancelledError` / `AgentLoopHooks.cancelled()` — real cancellation. The loop asks `hooks.cancelled()` at the points where stopping is safe and cheap (between steps, before the next model call, before a turn's tools are dispatched), ALWAYS from inside a checkpoint, so the answer is journaled and a cancel arriving between two replays can never change a branch a replayed position already took; the positions themselves sit behind `hooks.patched('agent:cancellation')`, so a run already in flight keeps its recorded shape. A tool already executing is never interrupted — there is no un-executing a side effect, and abandoning a dispatched step leaves a journal holding a dispatch whose result never lands. Observing a cancel throws `RunCancelledError`, which unwinds through the path a suspend already uses; the RUNNER settles it, recording the run `cancelled` (`AgentStore.recordRunEnd`'s third terminal, never `failed`) and ending the stream on a `{ kind: 'cancelled' }` frame rather than failing it, so a user pressing Stop is not in anybody's error rate.
36
36
  - `isControlFlowSignal(error)` / `isReplayIntegrityError(error)` — recognize a durable suspend and a checkpoint refusal without importing the durable packages (a `Symbol.for` marker and a class name, respectively). Rethrow both untouched from any `catch` in a workflow body.
37
37
  - `settleAll(tasks)` / `SettledTask<T>` — the implementation behind the optional `AgentLoopHooks.parallel`, which is how a turn's `read` tool calls run concurrently. It invokes every task synchronously, in list order, before awaiting any of them (so a runner that takes checkpoint positions on the call keeps them in call order), and resolves only once all of them have settled (so a runner that unwinds a turn by throwing never abandons a sibling mid-dispatch). A runner that assigns positions anywhere else simply omits the hook and the loop stays sequential.
38
- - `AgentStreamEvent` / `encodeStreamEvent` / `decodeStreamEvent` — the live-stream vocabulary (text, reasoning, tool calls with optional `parentId` nesting, `elicitation`, `approval-requested` with `approver`/`expiresAt`, server-pushed `ui` components, `title`, `cancelled`). It is also the contract a runner that is not this loop writes to get the React client for free: see [docs/stream-protocol.md](../../docs/stream-protocol.md). `AgentUiComponent` and `AgentApprovalRequest` are the payload types.
38
+ - `AgentStreamEvent` / `encodeStreamEvent` / `decodeStreamEvent` — the live-stream vocabulary (text, reasoning, tool calls with optional `parentId` nesting, `elicitation`, `approval-requested` with `approver`/`expiresAt`, server-pushed `ui` components, `title`, `cancelled`). It is also the contract a runner that is not this loop writes to get the React client for free: see [docs/stream-protocol.md](../../docs/stream-protocol.md). `AgentUiComponent`, `AgentApprovalRequest` and `AgentApprovalSettlement` (the `approval-settled` frame) are the payload types.
39
+ - `ApprovalPolicy` / `DefaultApprovalPolicy` / `mayDecideApproval` — **who approves an `action` call, and for how long.** `requirementFor(tool, actor, thread)` answers `{ required, approver, ttlMs? }`: `required: false` runs the call straight away, `approver` is `'requester'` (the thread's own actor — the default) or a role, `ttlMs` bounds the wait. The answer is taken INSIDE the call's `persist:toolcall` checkpoint (together with the thread's remembered approvals, `AgentStore.rememberedApprovals`), so a replay reads the branch back instead of re-asking a policy that changed while the run was parked. The loop streams `approval-requested` with the approver and `expiresAt`, hands the ttl to `AgentLoopHooks.awaitApproval(call, ctx, { timeoutMs })`, and settles a lapsed wait (`Decision.expired`) as `ToolCallStatus 'expired'` — denied to the model with "the approval expired", streamed as `tool-output-denied` + `approval-settled { status: 'expired' }`. `Decision.remember` approves later calls of the same tool in the same thread; `Decision.decidedVia` records the surface. `StoredMessage.approvals` carries the persisted record on a reloaded thread. A run claimed before this existed keeps waiting on the requester with no timeout.
39
40
  - `observeTurnFrames` / `withTurnFrames` — what a model turn streamed that its result does not carry: the model's thinking (`reasoning` text, and `reasoningMs` summed over each burst of consecutive reasoning frames) and pushed `ui` components (first-seen order, last props per id). The loop and the dispatched `llm` step wrap the provider's sink with it inside the model checkpoint, so every provider that streams the vocabulary gets reasoning persisted (`StoredMessage.reasoning` / `reasoningMs` / `ui`) and a replay reads the journaled numbers back. A provider may set `ModelTurnResult.reasoning` / `reasoningMs` / `ui` itself; its values win. The step's `reasoningMs` also rides the `step-finish` frame.
41
+ - `ToolPresentation` / `ToolResultView` / `ToolCatalogEntry` — how a person-facing surface talks about a tool without naming it (`ToolSpec.presentation`, set by `@AiTool({ presentation })`; never shown to the model). `ToolRegistry.visibleSpecs(actor, policy, allowList)` returns the whole specs behind the same gates `definitionsFor` applies, which is what `GET /agent/tools` serves.
40
42
  - `runAgentLoop(deps, input, hooks)` — the loop; the NestJS package drives it from both runners
41
43
 
42
44
  ## Guardrails — `@dudousxd/nestjs-agent-core/guardrails`
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-CHIw-aTx.cjs';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-B2Dq9ZwF.cjs';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-CHIw-aTx.js';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-B2Dq9ZwF.js';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**
package/dist/index.cjs CHANGED
@@ -46,6 +46,7 @@ __export(src_exports, {
46
46
  AGENT_SPAN_EVENTS: () => AGENT_SPAN_EVENTS,
47
47
  AGENT_STORE: () => AGENT_STORE,
48
48
  AGENT_TOOL_REGISTRY: () => AGENT_TOOL_REGISTRY,
49
+ APPROVAL_EXPIRED_REASON: () => APPROVAL_EXPIRED_REASON,
49
50
  ASK_TOOL_DESCRIPTION: () => ASK_TOOL_DESCRIPTION,
50
51
  ASK_TOOL_NAME: () => ASK_TOOL_NAME,
51
52
  AgentRegistry: () => AgentRegistry,
@@ -60,6 +61,7 @@ __export(src_exports, {
60
61
  DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION: () => DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION,
61
62
  DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS: () => DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS,
62
63
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS: () => DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
64
+ DefaultApprovalPolicy: () => DefaultApprovalPolicy,
63
65
  DefaultRolesPolicy: () => DefaultRolesPolicy,
64
66
  GLOBAL_SCOPE: () => GLOBAL_SCOPE,
65
67
  MAX_ASK_QUESTIONS: () => MAX_ASK_QUESTIONS,
@@ -68,6 +70,7 @@ __export(src_exports, {
68
70
  QuotaExceededError: () => QuotaExceededError,
69
71
  REMEMBER_TOOL_DESCRIPTION: () => REMEMBER_TOOL_DESCRIPTION,
70
72
  REMEMBER_TOOL_NAME: () => REMEMBER_TOOL_NAME,
73
+ REQUESTER_APPROVER: () => REQUESTER_APPROVER,
71
74
  RunCancelledError: () => RunCancelledError,
72
75
  SKILL_TOOL_DESCRIPTION: () => SKILL_TOOL_DESCRIPTION,
73
76
  SKILL_TOOL_NAME: () => SKILL_TOOL_NAME,
@@ -95,6 +98,7 @@ __export(src_exports, {
95
98
  createIncrementalGate: () => createIncrementalGate,
96
99
  dayBoundsUtc: () => dayBoundsUtc,
97
100
  decodeStreamEvent: () => decodeStreamEvent,
101
+ defaultCanDecide: () => defaultCanDecide,
98
102
  defaultScopeResolver: () => defaultScopeResolver,
99
103
  detachedDelivered: () => detachedDelivered,
100
104
  detachedStarted: () => detachedStarted,
@@ -115,6 +119,7 @@ __export(src_exports, {
115
119
  isToolEnabled: () => isToolEnabled,
116
120
  isTransientToolError: () => isTransientToolError,
117
121
  loadSkill: () => loadSkill,
122
+ mayDecideApproval: () => mayDecideApproval,
118
123
  memoryForgetVerdict: () => memoryForgetVerdict,
119
124
  memoryWriteVerdict: () => memoryWriteVerdict,
120
125
  normalizeDelegation: () => normalizeDelegation,
@@ -160,6 +165,7 @@ __export(src_exports, {
160
165
  staticSkillProvider: () => staticSkillProvider,
161
166
  summarizeWithModel: () => summarizeWithModel,
162
167
  tenantScope: () => tenantScope,
168
+ toolCallApprovalFromRow: () => toolCallApprovalFromRow,
163
169
  traceLlmTurn: () => traceLlmTurn,
164
170
  traceToolExecution: () => traceToolExecution,
165
171
  truncateDetailContent: () => truncateDetailContent,
@@ -388,6 +394,63 @@ function truncateDetailContent(content) {
388
394
  }
389
395
  __name(truncateDetailContent, "truncateDetailContent");
390
396
 
397
+ // src/spi/approval-policy.ts
398
+ var REQUESTER_APPROVER = "requester";
399
+ var DefaultApprovalPolicy = class {
400
+ static {
401
+ __name(this, "DefaultApprovalPolicy");
402
+ }
403
+ requirementFor(tool) {
404
+ return {
405
+ required: tool.kind === "action",
406
+ approver: REQUESTER_APPROVER
407
+ };
408
+ }
409
+ };
410
+ function defaultCanDecide(actor, decision) {
411
+ if (decision.approver === REQUESTER_APPROVER) {
412
+ return actor.id === decision.requesterRef;
413
+ }
414
+ return actor.roles?.includes(decision.approver) === true;
415
+ }
416
+ __name(defaultCanDecide, "defaultCanDecide");
417
+ async function mayDecideApproval(policy, actor, decision) {
418
+ if (policy?.canDecide !== void 0) {
419
+ return policy.canDecide(actor, decision);
420
+ }
421
+ return defaultCanDecide(actor, decision);
422
+ }
423
+ __name(mayDecideApproval, "mayDecideApproval");
424
+ function toolCallApprovalFromRow(row) {
425
+ if (row.approver === null || row.approver === void 0) {
426
+ return null;
427
+ }
428
+ const status = row.status === "pending_approval" ? "pending" : row.status === "rejected" ? "rejected" : row.status === "expired" ? "expired" : "approved";
429
+ const expiresAt = row.expiresAt instanceof Date ? row.expiresAt.toISOString() : typeof row.expiresAt === "string" ? row.expiresAt : void 0;
430
+ const decided = status === "approved" || status === "rejected";
431
+ return {
432
+ toolCallId: row.toolCallId,
433
+ approver: row.approver,
434
+ status,
435
+ ...expiresAt !== void 0 ? {
436
+ expiresAt
437
+ } : {},
438
+ ...row.remember === true ? {
439
+ remember: true
440
+ } : {},
441
+ ...decided && typeof row.executedByRef === "string" ? {
442
+ decidedBy: row.executedByRef
443
+ } : {},
444
+ ...decided && typeof row.decidedVia === "string" ? {
445
+ decidedVia: row.decidedVia
446
+ } : {},
447
+ ...status === "rejected" && typeof row.error === "string" ? {
448
+ reason: row.error
449
+ } : {}
450
+ };
451
+ }
452
+ __name(toolCallApprovalFromRow, "toolCallApprovalFromRow");
453
+
391
454
  // src/governance/compute.ts
392
455
  function estimateCost(usage, price) {
393
456
  if (price === void 0) {
@@ -2050,6 +2113,19 @@ var ToolRegistry = class {
2050
2113
  * tool the allow-list has already excluded is a call whose answer nothing reads.
2051
2114
  */
2052
2115
  async definitionsFor(actor, policy, allowedTools) {
2116
+ return (await this.visibleSpecs(actor, policy, allowedTools)).map((spec) => ({
2117
+ name: spec.name,
2118
+ kind: spec.kind,
2119
+ description: spec.description,
2120
+ inputSchema: spec.inputSchema
2121
+ }));
2122
+ }
2123
+ /**
2124
+ * The specs {@link definitionsFor} offers the model, whole — the same four gates (allow-list,
2125
+ * enabled, role, `canUse`) in the same order. For a surface that lists what an actor can reach
2126
+ * (`GET <base>/tools`), which must never disagree with what the model is actually shown.
2127
+ */
2128
+ async visibleSpecs(actor, policy, allowedTools) {
2053
2129
  const pinnedNames = new Set(filterToolsByAllowList(this.allSpecs(), allowedTools).map((spec) => spec.name));
2054
2130
  const pinned = [
2055
2131
  ...this.entries.values()
@@ -2058,12 +2134,7 @@ var ToolRegistry = class {
2058
2134
  const allowedByRole = new Set((await filterToolsByRole(live.map((entry) => entry.spec), actor, policy)).map((spec) => spec.name));
2059
2135
  const roleScoped = live.filter((entry) => allowedByRole.has(entry.spec.name));
2060
2136
  const actorScoped = await filterToolsByCanUse(roleScoped, actor);
2061
- return actorScoped.map(({ spec }) => ({
2062
- name: spec.name,
2063
- kind: spec.kind,
2064
- description: spec.description,
2065
- inputSchema: spec.inputSchema
2066
- }));
2137
+ return actorScoped.map(({ spec }) => spec);
2067
2138
  }
2068
2139
  /**
2069
2140
  * Run a tool. Re-checks that the tool is enabled and that the role allows it (defense-in-depth —
@@ -2930,15 +3001,33 @@ async function claimToolCall(turn, call) {
2930
3001
  const spec = deps.registry.spec(call.name);
2931
3002
  const kind = call.kind ?? declaredKind(deps, call.name);
2932
3003
  const awaitsHuman = kind === "action" || kind === "ask";
3004
+ const approval = kind === "action" ? await claimApproval(turn, call, spec) : void 0;
3005
+ const parks = kind === "ask" || approval?.mode === "ask";
2933
3006
  await deps.store.recordToolCall({
2934
3007
  toolCallId: call.id,
2935
3008
  messageId,
2936
3009
  toolName: call.name,
2937
3010
  toolType: awaitsHuman ? "action" : "read",
2938
3011
  input: call.input,
2939
- status: awaitsHuman ? "pending_approval" : "auto_executed",
2940
- runId: hooks.runId
3012
+ status: parks ? "pending_approval" : "auto_executed",
3013
+ runId: hooks.runId,
3014
+ ...approval !== void 0 && approval.mode !== "auto" ? {
3015
+ approver: approval.approver
3016
+ } : {},
3017
+ ...approval?.mode === "ask" && approval.expiresAt !== void 0 ? {
3018
+ expiresAt: approval.expiresAt
3019
+ } : {}
2941
3020
  });
3021
+ if (approval?.mode === "ask") {
3022
+ await turn.writer.write(encodeStreamEvent({
3023
+ kind: "approval-requested",
3024
+ id: call.id,
3025
+ approver: approval.approver,
3026
+ ...approval.expiresAt !== void 0 ? {
3027
+ expiresAt: approval.expiresAt
3028
+ } : {}
3029
+ }));
3030
+ }
2942
3031
  return {
2943
3032
  kind,
2944
3033
  ...spec?.targetAgent !== void 0 ? {
@@ -2948,6 +3037,9 @@ async function claimToolCall(turn, call) {
2948
3037
  // deployment with no detached edge writes the same bytes here it always has.
2949
3038
  ...spec?.detached === true ? {
2950
3039
  detached: true
3040
+ } : {},
3041
+ ...approval !== void 0 ? {
3042
+ approval
2951
3043
  } : {}
2952
3044
  };
2953
3045
  });
@@ -2967,10 +3059,52 @@ async function claimToolCall(turn, call) {
2967
3059
  ...persisted?.detached === true ? {
2968
3060
  detached: true
2969
3061
  } : {},
3062
+ ...toolType === "action" && persisted?.approval !== void 0 ? {
3063
+ approval: persisted.approval
3064
+ } : {},
2970
3065
  ctx: toolContext(deps, input, hooks)
2971
3066
  };
2972
3067
  }
2973
3068
  __name(claimToolCall, "claimToolCall");
3069
+ async function claimApproval(turn, call, spec) {
3070
+ const { deps, input, hooks } = turn;
3071
+ const policy = deps.approvalPolicy ?? new DefaultApprovalPolicy();
3072
+ const requirement = await policy.requirementFor({
3073
+ name: call.name,
3074
+ kind: "action",
3075
+ ...spec !== void 0 ? {
3076
+ spec
3077
+ } : {}
3078
+ }, input.actor, {
3079
+ threadId: input.threadId,
3080
+ runId: hooks.runId,
3081
+ ...input.agentName !== void 0 ? {
3082
+ agentName: input.agentName
3083
+ } : {}
3084
+ });
3085
+ if (!requirement.required) {
3086
+ return {
3087
+ mode: "auto"
3088
+ };
3089
+ }
3090
+ const remembered = await deps.store.rememberedApprovals?.(input.threadId) ?? [];
3091
+ if (remembered.includes(call.name)) {
3092
+ return {
3093
+ mode: "remembered",
3094
+ approver: requirement.approver
3095
+ };
3096
+ }
3097
+ const ttlMs = requirement.ttlMs !== void 0 && Number.isFinite(requirement.ttlMs) && requirement.ttlMs > 0 ? Math.floor(requirement.ttlMs) : void 0;
3098
+ return {
3099
+ mode: "ask",
3100
+ approver: requirement.approver,
3101
+ ...ttlMs !== void 0 ? {
3102
+ ttlMs,
3103
+ expiresAt: new Date(Date.now() + ttlMs).toISOString()
3104
+ } : {}
3105
+ };
3106
+ }
3107
+ __name(claimApproval, "claimApproval");
2974
3108
  async function loadSkillIntoTurn(turn, claimed, startedAt) {
2975
3109
  const { deps, input, hooks } = turn;
2976
3110
  const { call } = claimed;
@@ -3154,17 +3288,50 @@ async function invokeClaimedTool(turn, claimed) {
3154
3288
  }
3155
3289
  }
3156
3290
  __name(invokeClaimedTool, "invokeClaimedTool");
3157
- async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3291
+ async function recordToolOutcome(turn, claimed, outcome, deciderRef, meta) {
3158
3292
  const { deps, hooks } = turn;
3159
3293
  const { call } = claimed;
3160
3294
  const toolType = claimed.toolType === "action" ? "action" : "read";
3161
- if (outcome.status === "failed") {
3162
- await hooks.step(`persist:toolfail:${call.id}`, () => deps.store.updateToolCall({
3163
- toolCallId: call.id,
3164
- status: "failed",
3165
- error: outcome.error,
3166
- executionMs: outcome.executionMs
3295
+ const decided = meta === void 0 ? {} : {
3296
+ executedByRef: meta.decidedBy,
3297
+ ...meta.decidedVia !== void 0 ? {
3298
+ decidedVia: meta.decidedVia
3299
+ } : {},
3300
+ ...meta.remember === true ? {
3301
+ remember: true
3302
+ } : {}
3303
+ };
3304
+ const settle = /* @__PURE__ */ __name(async () => {
3305
+ if (meta === void 0 || !meta.streamed) {
3306
+ return;
3307
+ }
3308
+ await turn.writer.write(encodeStreamEvent({
3309
+ kind: "approval-settled",
3310
+ id: call.id,
3311
+ status: "approved",
3312
+ ...meta.approver !== void 0 ? {
3313
+ approver: meta.approver
3314
+ } : {},
3315
+ decidedBy: meta.decidedBy,
3316
+ ...meta.decidedVia !== void 0 ? {
3317
+ decidedVia: meta.decidedVia
3318
+ } : {},
3319
+ ...meta.remember === true ? {
3320
+ remember: true
3321
+ } : {}
3167
3322
  }));
3323
+ }, "settle");
3324
+ if (outcome.status === "failed") {
3325
+ await hooks.step(`persist:toolfail:${call.id}`, async () => {
3326
+ await deps.store.updateToolCall({
3327
+ toolCallId: call.id,
3328
+ status: "failed",
3329
+ error: outcome.error,
3330
+ executionMs: outcome.executionMs,
3331
+ ...decided
3332
+ });
3333
+ await settle();
3334
+ });
3168
3335
  publishAgentToolCall({
3169
3336
  runId: hooks.runId,
3170
3337
  toolName: call.name,
@@ -3179,15 +3346,19 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3179
3346
  error: outcome.error
3180
3347
  };
3181
3348
  }
3182
- await hooks.step(`persist:toolexec:${call.id}`, () => deps.store.updateToolCall({
3183
- toolCallId: call.id,
3184
- status: "executed",
3185
- output: outcome.output,
3186
- executionMs: outcome.executionMs,
3187
- ...toolType === "action" ? {
3188
- executedByRef: deciderRef
3189
- } : {}
3190
- }));
3349
+ await hooks.step(`persist:toolexec:${call.id}`, async () => {
3350
+ await deps.store.updateToolCall({
3351
+ toolCallId: call.id,
3352
+ status: "executed",
3353
+ output: outcome.output,
3354
+ executionMs: outcome.executionMs,
3355
+ ...toolType === "action" ? {
3356
+ executedByRef: deciderRef
3357
+ } : {},
3358
+ ...decided
3359
+ });
3360
+ await settle();
3361
+ });
3191
3362
  publishAgentToolCall({
3192
3363
  runId: hooks.runId,
3193
3364
  toolName: call.name,
@@ -3325,18 +3496,53 @@ async function runClaimedToolCall(turn, claimed) {
3325
3496
  return elicitToolCall(turn, claimed);
3326
3497
  }
3327
3498
  let deciderRef = input.actor.id;
3328
- if (toolType === "action") {
3329
- const decision = await hooks.awaitApproval(call, ctx);
3499
+ let meta;
3500
+ if (toolType === "action" && claimed.approval?.mode === "remembered") {
3501
+ meta = {
3502
+ approver: claimed.approval.approver,
3503
+ decidedBy: input.actor.id,
3504
+ decidedVia: "remembered",
3505
+ remember: true,
3506
+ streamed: true
3507
+ };
3508
+ } else if (toolType === "action" && claimed.approval?.mode !== "auto") {
3509
+ const ttlMs = claimed.approval?.mode === "ask" ? claimed.approval.ttlMs : void 0;
3510
+ const decision = ttlMs !== void 0 ? await hooks.awaitApproval(call, ctx, {
3511
+ timeoutMs: ttlMs
3512
+ }) : await hooks.awaitApproval(call, ctx);
3330
3513
  deciderRef = decision.executedByRef ?? input.actor.id;
3514
+ const streamsSettlement = claimed.approval !== void 0;
3515
+ if (decision.expired === true) {
3516
+ return expireToolCall(turn, claimed);
3517
+ }
3331
3518
  if (!decision.approved) {
3332
- await hooks.step(`persist:toolreject:${call.id}`, () => deps.store.updateToolCall({
3333
- toolCallId: call.id,
3334
- status: "rejected",
3335
- executedByRef: deciderRef,
3336
- ...decision.reason !== void 0 ? {
3337
- error: decision.reason
3338
- } : {}
3339
- }));
3519
+ await hooks.step(`persist:toolreject:${call.id}`, async () => {
3520
+ await deps.store.updateToolCall({
3521
+ toolCallId: call.id,
3522
+ status: "rejected",
3523
+ executedByRef: deciderRef,
3524
+ ...decision.reason !== void 0 ? {
3525
+ error: decision.reason
3526
+ } : {},
3527
+ ...decision.decidedVia !== void 0 ? {
3528
+ decidedVia: decision.decidedVia
3529
+ } : {}
3530
+ });
3531
+ if (streamsSettlement) {
3532
+ await turn.writer.write(encodeStreamEvent({
3533
+ kind: "approval-settled",
3534
+ id: call.id,
3535
+ status: "rejected",
3536
+ decidedBy: deciderRef,
3537
+ ...decision.decidedVia !== void 0 ? {
3538
+ decidedVia: decision.decidedVia
3539
+ } : {},
3540
+ ...decision.reason !== void 0 ? {
3541
+ reason: decision.reason
3542
+ } : {}
3543
+ }));
3544
+ }
3545
+ });
3340
3546
  publishAgentToolCall({
3341
3547
  runId: hooks.runId,
3342
3548
  toolName: call.name,
@@ -3359,10 +3565,60 @@ async function runClaimedToolCall(turn, claimed) {
3359
3565
  error: refusalNarrative(decision.reason)
3360
3566
  };
3361
3567
  }
3568
+ meta = {
3569
+ decidedBy: deciderRef,
3570
+ ...decision.decidedVia !== void 0 ? {
3571
+ decidedVia: decision.decidedVia
3572
+ } : {},
3573
+ ...decision.remember === true ? {
3574
+ remember: true
3575
+ } : {},
3576
+ streamed: streamsSettlement
3577
+ };
3362
3578
  }
3363
- return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef);
3579
+ return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef, meta);
3364
3580
  }
3365
3581
  __name(runClaimedToolCall, "runClaimedToolCall");
3582
+ async function expireToolCall(turn, claimed) {
3583
+ const { deps, hooks } = turn;
3584
+ const { call } = claimed;
3585
+ await hooks.step(`persist:toolreject:${call.id}`, async () => {
3586
+ await deps.store.updateToolCall({
3587
+ toolCallId: call.id,
3588
+ status: "expired",
3589
+ error: APPROVAL_EXPIRED_REASON
3590
+ });
3591
+ await turn.writer.write(encodeStreamEvent({
3592
+ kind: "approval-settled",
3593
+ id: call.id,
3594
+ status: "expired"
3595
+ }));
3596
+ });
3597
+ publishAgentToolCall({
3598
+ runId: hooks.runId,
3599
+ toolName: call.name,
3600
+ toolType: "action",
3601
+ status: "rejected"
3602
+ });
3603
+ return {
3604
+ id: call.id,
3605
+ name: call.name,
3606
+ output: {
3607
+ rejected: true,
3608
+ expired: true,
3609
+ reason: APPROVAL_EXPIRED_REASON
3610
+ },
3611
+ denied: true,
3612
+ expired: true,
3613
+ error: expiryNarrative()
3614
+ };
3615
+ }
3616
+ __name(expireToolCall, "expireToolCall");
3617
+ var APPROVAL_EXPIRED_REASON = "approval expired";
3618
+ function expiryNarrative() {
3619
+ return "This action needed approval, and the approval request expired before anyone decided. Nothing ran and nothing changed. Do not run it again on your own; tell the person it was not approved in time and ask whether they still want it.";
3620
+ }
3621
+ __name(expiryNarrative, "expiryNarrative");
3366
3622
  function outputFrame(result) {
3367
3623
  if (result.denied === true) {
3368
3624
  const reason = refusalReason(result);
@@ -4036,6 +4292,7 @@ __name(runAgentLoop, "runAgentLoop");
4036
4292
  AGENT_SPAN_EVENTS,
4037
4293
  AGENT_STORE,
4038
4294
  AGENT_TOOL_REGISTRY,
4295
+ APPROVAL_EXPIRED_REASON,
4039
4296
  ASK_TOOL_DESCRIPTION,
4040
4297
  ASK_TOOL_NAME,
4041
4298
  AgentRegistry,
@@ -4050,6 +4307,7 @@ __name(runAgentLoop, "runAgentLoop");
4050
4307
  DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION,
4051
4308
  DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS,
4052
4309
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
4310
+ DefaultApprovalPolicy,
4053
4311
  DefaultRolesPolicy,
4054
4312
  GLOBAL_SCOPE,
4055
4313
  MAX_ASK_QUESTIONS,
@@ -4058,6 +4316,7 @@ __name(runAgentLoop, "runAgentLoop");
4058
4316
  QuotaExceededError,
4059
4317
  REMEMBER_TOOL_DESCRIPTION,
4060
4318
  REMEMBER_TOOL_NAME,
4319
+ REQUESTER_APPROVER,
4061
4320
  RunCancelledError,
4062
4321
  SKILL_TOOL_DESCRIPTION,
4063
4322
  SKILL_TOOL_NAME,
@@ -4085,6 +4344,7 @@ __name(runAgentLoop, "runAgentLoop");
4085
4344
  createIncrementalGate,
4086
4345
  dayBoundsUtc,
4087
4346
  decodeStreamEvent,
4347
+ defaultCanDecide,
4088
4348
  defaultScopeResolver,
4089
4349
  detachedDelivered,
4090
4350
  detachedStarted,
@@ -4105,6 +4365,7 @@ __name(runAgentLoop, "runAgentLoop");
4105
4365
  isToolEnabled,
4106
4366
  isTransientToolError,
4107
4367
  loadSkill,
4368
+ mayDecideApproval,
4108
4369
  memoryForgetVerdict,
4109
4370
  memoryWriteVerdict,
4110
4371
  normalizeDelegation,
@@ -4150,6 +4411,7 @@ __name(runAgentLoop, "runAgentLoop");
4150
4411
  staticSkillProvider,
4151
4412
  summarizeWithModel,
4152
4413
  tenantScope,
4414
+ toolCallApprovalFromRow,
4153
4415
  traceLlmTurn,
4154
4416
  traceToolExecution,
4155
4417
  truncateDetailContent,