@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 +3 -1
- package/dist/guardrails/index.d.cts +1 -1
- package/dist/guardrails/index.d.ts +1 -1
- package/dist/index.cjs +297 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +150 -5
- package/dist/index.d.ts +150 -5
- package/dist/index.js +291 -35
- package/dist/index.js.map +1 -1
- package/dist/{tool-CHIw-aTx.d.cts → tool-B2Dq9ZwF.d.cts} +175 -2
- package/dist/{tool-CHIw-aTx.d.ts → tool-B2Dq9ZwF.d.ts} +175 -2
- package/package.json +1 -1
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 `
|
|
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`
|
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:
|
|
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
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
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}`, () =>
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
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
|
-
|
|
3329
|
-
|
|
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}`, () =>
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
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,
|