@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 +2 -1
- package/dist/guardrails/index.d.cts +1 -1
- package/dist/guardrails/index.d.ts +1 -1
- package/dist/index.cjs +283 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -5
- package/dist/index.d.ts +144 -5
- package/dist/index.js +277 -29
- package/dist/index.js.map +1 -1
- package/dist/{tool-Q2rmGeaG.d.cts → tool-B2Dq9ZwF.d.cts} +82 -2
- package/dist/{tool-Q2rmGeaG.d.ts → tool-B2Dq9ZwF.d.ts} +82 -2
- package/package.json +1 -1
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 `
|
|
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
|
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:
|
|
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
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
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}`, () =>
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
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
|
-
|
|
3337
|
-
|
|
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}`, () =>
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
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,
|