@dudousxd/nestjs-agent-core 0.19.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,7 +35,8 @@ 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.
40
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.
41
42
  - `runAgentLoop(deps, input, hooks)` — the loop; the NestJS package drives it from both runners
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-Q2rmGeaG.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-Q2rmGeaG.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) {
@@ -2938,15 +3001,33 @@ async function claimToolCall(turn, call) {
2938
3001
  const spec = deps.registry.spec(call.name);
2939
3002
  const kind = call.kind ?? declaredKind(deps, call.name);
2940
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";
2941
3006
  await deps.store.recordToolCall({
2942
3007
  toolCallId: call.id,
2943
3008
  messageId,
2944
3009
  toolName: call.name,
2945
3010
  toolType: awaitsHuman ? "action" : "read",
2946
3011
  input: call.input,
2947
- status: awaitsHuman ? "pending_approval" : "auto_executed",
2948
- 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
+ } : {}
2949
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
+ }
2950
3031
  return {
2951
3032
  kind,
2952
3033
  ...spec?.targetAgent !== void 0 ? {
@@ -2956,6 +3037,9 @@ async function claimToolCall(turn, call) {
2956
3037
  // deployment with no detached edge writes the same bytes here it always has.
2957
3038
  ...spec?.detached === true ? {
2958
3039
  detached: true
3040
+ } : {},
3041
+ ...approval !== void 0 ? {
3042
+ approval
2959
3043
  } : {}
2960
3044
  };
2961
3045
  });
@@ -2975,10 +3059,52 @@ async function claimToolCall(turn, call) {
2975
3059
  ...persisted?.detached === true ? {
2976
3060
  detached: true
2977
3061
  } : {},
3062
+ ...toolType === "action" && persisted?.approval !== void 0 ? {
3063
+ approval: persisted.approval
3064
+ } : {},
2978
3065
  ctx: toolContext(deps, input, hooks)
2979
3066
  };
2980
3067
  }
2981
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");
2982
3108
  async function loadSkillIntoTurn(turn, claimed, startedAt) {
2983
3109
  const { deps, input, hooks } = turn;
2984
3110
  const { call } = claimed;
@@ -3162,17 +3288,50 @@ async function invokeClaimedTool(turn, claimed) {
3162
3288
  }
3163
3289
  }
3164
3290
  __name(invokeClaimedTool, "invokeClaimedTool");
3165
- async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3291
+ async function recordToolOutcome(turn, claimed, outcome, deciderRef, meta) {
3166
3292
  const { deps, hooks } = turn;
3167
3293
  const { call } = claimed;
3168
3294
  const toolType = claimed.toolType === "action" ? "action" : "read";
3169
- if (outcome.status === "failed") {
3170
- await hooks.step(`persist:toolfail:${call.id}`, () => deps.store.updateToolCall({
3171
- toolCallId: call.id,
3172
- status: "failed",
3173
- error: outcome.error,
3174
- 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
+ } : {}
3175
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
+ });
3176
3335
  publishAgentToolCall({
3177
3336
  runId: hooks.runId,
3178
3337
  toolName: call.name,
@@ -3187,15 +3346,19 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3187
3346
  error: outcome.error
3188
3347
  };
3189
3348
  }
3190
- await hooks.step(`persist:toolexec:${call.id}`, () => deps.store.updateToolCall({
3191
- toolCallId: call.id,
3192
- status: "executed",
3193
- output: outcome.output,
3194
- executionMs: outcome.executionMs,
3195
- ...toolType === "action" ? {
3196
- executedByRef: deciderRef
3197
- } : {}
3198
- }));
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
+ });
3199
3362
  publishAgentToolCall({
3200
3363
  runId: hooks.runId,
3201
3364
  toolName: call.name,
@@ -3333,18 +3496,53 @@ async function runClaimedToolCall(turn, claimed) {
3333
3496
  return elicitToolCall(turn, claimed);
3334
3497
  }
3335
3498
  let deciderRef = input.actor.id;
3336
- if (toolType === "action") {
3337
- 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);
3338
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
+ }
3339
3518
  if (!decision.approved) {
3340
- await hooks.step(`persist:toolreject:${call.id}`, () => deps.store.updateToolCall({
3341
- toolCallId: call.id,
3342
- status: "rejected",
3343
- executedByRef: deciderRef,
3344
- ...decision.reason !== void 0 ? {
3345
- error: decision.reason
3346
- } : {}
3347
- }));
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
+ });
3348
3546
  publishAgentToolCall({
3349
3547
  runId: hooks.runId,
3350
3548
  toolName: call.name,
@@ -3367,10 +3565,60 @@ async function runClaimedToolCall(turn, claimed) {
3367
3565
  error: refusalNarrative(decision.reason)
3368
3566
  };
3369
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
+ };
3370
3578
  }
3371
- return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef);
3579
+ return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef, meta);
3372
3580
  }
3373
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");
3374
3622
  function outputFrame(result) {
3375
3623
  if (result.denied === true) {
3376
3624
  const reason = refusalReason(result);
@@ -4044,6 +4292,7 @@ __name(runAgentLoop, "runAgentLoop");
4044
4292
  AGENT_SPAN_EVENTS,
4045
4293
  AGENT_STORE,
4046
4294
  AGENT_TOOL_REGISTRY,
4295
+ APPROVAL_EXPIRED_REASON,
4047
4296
  ASK_TOOL_DESCRIPTION,
4048
4297
  ASK_TOOL_NAME,
4049
4298
  AgentRegistry,
@@ -4058,6 +4307,7 @@ __name(runAgentLoop, "runAgentLoop");
4058
4307
  DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION,
4059
4308
  DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS,
4060
4309
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
4310
+ DefaultApprovalPolicy,
4061
4311
  DefaultRolesPolicy,
4062
4312
  GLOBAL_SCOPE,
4063
4313
  MAX_ASK_QUESTIONS,
@@ -4066,6 +4316,7 @@ __name(runAgentLoop, "runAgentLoop");
4066
4316
  QuotaExceededError,
4067
4317
  REMEMBER_TOOL_DESCRIPTION,
4068
4318
  REMEMBER_TOOL_NAME,
4319
+ REQUESTER_APPROVER,
4069
4320
  RunCancelledError,
4070
4321
  SKILL_TOOL_DESCRIPTION,
4071
4322
  SKILL_TOOL_NAME,
@@ -4093,6 +4344,7 @@ __name(runAgentLoop, "runAgentLoop");
4093
4344
  createIncrementalGate,
4094
4345
  dayBoundsUtc,
4095
4346
  decodeStreamEvent,
4347
+ defaultCanDecide,
4096
4348
  defaultScopeResolver,
4097
4349
  detachedDelivered,
4098
4350
  detachedStarted,
@@ -4113,6 +4365,7 @@ __name(runAgentLoop, "runAgentLoop");
4113
4365
  isToolEnabled,
4114
4366
  isTransientToolError,
4115
4367
  loadSkill,
4368
+ mayDecideApproval,
4116
4369
  memoryForgetVerdict,
4117
4370
  memoryWriteVerdict,
4118
4371
  normalizeDelegation,
@@ -4158,6 +4411,7 @@ __name(runAgentLoop, "runAgentLoop");
4158
4411
  staticSkillProvider,
4159
4412
  summarizeWithModel,
4160
4413
  tenantScope,
4414
+ toolCallApprovalFromRow,
4161
4415
  traceLlmTurn,
4162
4416
  traceToolExecution,
4163
4417
  truncateDetailContent,