@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/dist/index.js CHANGED
@@ -215,6 +215,63 @@ function truncateDetailContent(content) {
215
215
  }
216
216
  __name(truncateDetailContent, "truncateDetailContent");
217
217
 
218
+ // src/spi/approval-policy.ts
219
+ var REQUESTER_APPROVER = "requester";
220
+ var DefaultApprovalPolicy = class {
221
+ static {
222
+ __name(this, "DefaultApprovalPolicy");
223
+ }
224
+ requirementFor(tool) {
225
+ return {
226
+ required: tool.kind === "action",
227
+ approver: REQUESTER_APPROVER
228
+ };
229
+ }
230
+ };
231
+ function defaultCanDecide(actor, decision) {
232
+ if (decision.approver === REQUESTER_APPROVER) {
233
+ return actor.id === decision.requesterRef;
234
+ }
235
+ return actor.roles?.includes(decision.approver) === true;
236
+ }
237
+ __name(defaultCanDecide, "defaultCanDecide");
238
+ async function mayDecideApproval(policy, actor, decision) {
239
+ if (policy?.canDecide !== void 0) {
240
+ return policy.canDecide(actor, decision);
241
+ }
242
+ return defaultCanDecide(actor, decision);
243
+ }
244
+ __name(mayDecideApproval, "mayDecideApproval");
245
+ function toolCallApprovalFromRow(row) {
246
+ if (row.approver === null || row.approver === void 0) {
247
+ return null;
248
+ }
249
+ const status = row.status === "pending_approval" ? "pending" : row.status === "rejected" ? "rejected" : row.status === "expired" ? "expired" : "approved";
250
+ const expiresAt = row.expiresAt instanceof Date ? row.expiresAt.toISOString() : typeof row.expiresAt === "string" ? row.expiresAt : void 0;
251
+ const decided = status === "approved" || status === "rejected";
252
+ return {
253
+ toolCallId: row.toolCallId,
254
+ approver: row.approver,
255
+ status,
256
+ ...expiresAt !== void 0 ? {
257
+ expiresAt
258
+ } : {},
259
+ ...row.remember === true ? {
260
+ remember: true
261
+ } : {},
262
+ ...decided && typeof row.executedByRef === "string" ? {
263
+ decidedBy: row.executedByRef
264
+ } : {},
265
+ ...decided && typeof row.decidedVia === "string" ? {
266
+ decidedVia: row.decidedVia
267
+ } : {},
268
+ ...status === "rejected" && typeof row.error === "string" ? {
269
+ reason: row.error
270
+ } : {}
271
+ };
272
+ }
273
+ __name(toolCallApprovalFromRow, "toolCallApprovalFromRow");
274
+
218
275
  // src/governance/compute.ts
219
276
  function estimateCost(usage, price) {
220
277
  if (price === void 0) {
@@ -1877,6 +1934,19 @@ var ToolRegistry = class {
1877
1934
  * tool the allow-list has already excluded is a call whose answer nothing reads.
1878
1935
  */
1879
1936
  async definitionsFor(actor, policy, allowedTools) {
1937
+ return (await this.visibleSpecs(actor, policy, allowedTools)).map((spec) => ({
1938
+ name: spec.name,
1939
+ kind: spec.kind,
1940
+ description: spec.description,
1941
+ inputSchema: spec.inputSchema
1942
+ }));
1943
+ }
1944
+ /**
1945
+ * The specs {@link definitionsFor} offers the model, whole — the same four gates (allow-list,
1946
+ * enabled, role, `canUse`) in the same order. For a surface that lists what an actor can reach
1947
+ * (`GET <base>/tools`), which must never disagree with what the model is actually shown.
1948
+ */
1949
+ async visibleSpecs(actor, policy, allowedTools) {
1880
1950
  const pinnedNames = new Set(filterToolsByAllowList(this.allSpecs(), allowedTools).map((spec) => spec.name));
1881
1951
  const pinned = [
1882
1952
  ...this.entries.values()
@@ -1885,12 +1955,7 @@ var ToolRegistry = class {
1885
1955
  const allowedByRole = new Set((await filterToolsByRole(live.map((entry) => entry.spec), actor, policy)).map((spec) => spec.name));
1886
1956
  const roleScoped = live.filter((entry) => allowedByRole.has(entry.spec.name));
1887
1957
  const actorScoped = await filterToolsByCanUse(roleScoped, actor);
1888
- return actorScoped.map(({ spec }) => ({
1889
- name: spec.name,
1890
- kind: spec.kind,
1891
- description: spec.description,
1892
- inputSchema: spec.inputSchema
1893
- }));
1958
+ return actorScoped.map(({ spec }) => spec);
1894
1959
  }
1895
1960
  /**
1896
1961
  * Run a tool. Re-checks that the tool is enabled and that the role allows it (defense-in-depth —
@@ -2757,15 +2822,33 @@ async function claimToolCall(turn, call) {
2757
2822
  const spec = deps.registry.spec(call.name);
2758
2823
  const kind = call.kind ?? declaredKind(deps, call.name);
2759
2824
  const awaitsHuman = kind === "action" || kind === "ask";
2825
+ const approval = kind === "action" ? await claimApproval(turn, call, spec) : void 0;
2826
+ const parks = kind === "ask" || approval?.mode === "ask";
2760
2827
  await deps.store.recordToolCall({
2761
2828
  toolCallId: call.id,
2762
2829
  messageId,
2763
2830
  toolName: call.name,
2764
2831
  toolType: awaitsHuman ? "action" : "read",
2765
2832
  input: call.input,
2766
- status: awaitsHuman ? "pending_approval" : "auto_executed",
2767
- runId: hooks.runId
2833
+ status: parks ? "pending_approval" : "auto_executed",
2834
+ runId: hooks.runId,
2835
+ ...approval !== void 0 && approval.mode !== "auto" ? {
2836
+ approver: approval.approver
2837
+ } : {},
2838
+ ...approval?.mode === "ask" && approval.expiresAt !== void 0 ? {
2839
+ expiresAt: approval.expiresAt
2840
+ } : {}
2768
2841
  });
2842
+ if (approval?.mode === "ask") {
2843
+ await turn.writer.write(encodeStreamEvent({
2844
+ kind: "approval-requested",
2845
+ id: call.id,
2846
+ approver: approval.approver,
2847
+ ...approval.expiresAt !== void 0 ? {
2848
+ expiresAt: approval.expiresAt
2849
+ } : {}
2850
+ }));
2851
+ }
2769
2852
  return {
2770
2853
  kind,
2771
2854
  ...spec?.targetAgent !== void 0 ? {
@@ -2775,6 +2858,9 @@ async function claimToolCall(turn, call) {
2775
2858
  // deployment with no detached edge writes the same bytes here it always has.
2776
2859
  ...spec?.detached === true ? {
2777
2860
  detached: true
2861
+ } : {},
2862
+ ...approval !== void 0 ? {
2863
+ approval
2778
2864
  } : {}
2779
2865
  };
2780
2866
  });
@@ -2794,10 +2880,52 @@ async function claimToolCall(turn, call) {
2794
2880
  ...persisted?.detached === true ? {
2795
2881
  detached: true
2796
2882
  } : {},
2883
+ ...toolType === "action" && persisted?.approval !== void 0 ? {
2884
+ approval: persisted.approval
2885
+ } : {},
2797
2886
  ctx: toolContext(deps, input, hooks)
2798
2887
  };
2799
2888
  }
2800
2889
  __name(claimToolCall, "claimToolCall");
2890
+ async function claimApproval(turn, call, spec) {
2891
+ const { deps, input, hooks } = turn;
2892
+ const policy = deps.approvalPolicy ?? new DefaultApprovalPolicy();
2893
+ const requirement = await policy.requirementFor({
2894
+ name: call.name,
2895
+ kind: "action",
2896
+ ...spec !== void 0 ? {
2897
+ spec
2898
+ } : {}
2899
+ }, input.actor, {
2900
+ threadId: input.threadId,
2901
+ runId: hooks.runId,
2902
+ ...input.agentName !== void 0 ? {
2903
+ agentName: input.agentName
2904
+ } : {}
2905
+ });
2906
+ if (!requirement.required) {
2907
+ return {
2908
+ mode: "auto"
2909
+ };
2910
+ }
2911
+ const remembered = await deps.store.rememberedApprovals?.(input.threadId) ?? [];
2912
+ if (remembered.includes(call.name)) {
2913
+ return {
2914
+ mode: "remembered",
2915
+ approver: requirement.approver
2916
+ };
2917
+ }
2918
+ const ttlMs = requirement.ttlMs !== void 0 && Number.isFinite(requirement.ttlMs) && requirement.ttlMs > 0 ? Math.floor(requirement.ttlMs) : void 0;
2919
+ return {
2920
+ mode: "ask",
2921
+ approver: requirement.approver,
2922
+ ...ttlMs !== void 0 ? {
2923
+ ttlMs,
2924
+ expiresAt: new Date(Date.now() + ttlMs).toISOString()
2925
+ } : {}
2926
+ };
2927
+ }
2928
+ __name(claimApproval, "claimApproval");
2801
2929
  async function loadSkillIntoTurn(turn, claimed, startedAt) {
2802
2930
  const { deps, input, hooks } = turn;
2803
2931
  const { call } = claimed;
@@ -2981,17 +3109,50 @@ async function invokeClaimedTool(turn, claimed) {
2981
3109
  }
2982
3110
  }
2983
3111
  __name(invokeClaimedTool, "invokeClaimedTool");
2984
- async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3112
+ async function recordToolOutcome(turn, claimed, outcome, deciderRef, meta) {
2985
3113
  const { deps, hooks } = turn;
2986
3114
  const { call } = claimed;
2987
3115
  const toolType = claimed.toolType === "action" ? "action" : "read";
2988
- if (outcome.status === "failed") {
2989
- await hooks.step(`persist:toolfail:${call.id}`, () => deps.store.updateToolCall({
2990
- toolCallId: call.id,
2991
- status: "failed",
2992
- error: outcome.error,
2993
- executionMs: outcome.executionMs
3116
+ const decided = meta === void 0 ? {} : {
3117
+ executedByRef: meta.decidedBy,
3118
+ ...meta.decidedVia !== void 0 ? {
3119
+ decidedVia: meta.decidedVia
3120
+ } : {},
3121
+ ...meta.remember === true ? {
3122
+ remember: true
3123
+ } : {}
3124
+ };
3125
+ const settle = /* @__PURE__ */ __name(async () => {
3126
+ if (meta === void 0 || !meta.streamed) {
3127
+ return;
3128
+ }
3129
+ await turn.writer.write(encodeStreamEvent({
3130
+ kind: "approval-settled",
3131
+ id: call.id,
3132
+ status: "approved",
3133
+ ...meta.approver !== void 0 ? {
3134
+ approver: meta.approver
3135
+ } : {},
3136
+ decidedBy: meta.decidedBy,
3137
+ ...meta.decidedVia !== void 0 ? {
3138
+ decidedVia: meta.decidedVia
3139
+ } : {},
3140
+ ...meta.remember === true ? {
3141
+ remember: true
3142
+ } : {}
2994
3143
  }));
3144
+ }, "settle");
3145
+ if (outcome.status === "failed") {
3146
+ await hooks.step(`persist:toolfail:${call.id}`, async () => {
3147
+ await deps.store.updateToolCall({
3148
+ toolCallId: call.id,
3149
+ status: "failed",
3150
+ error: outcome.error,
3151
+ executionMs: outcome.executionMs,
3152
+ ...decided
3153
+ });
3154
+ await settle();
3155
+ });
2995
3156
  publishAgentToolCall({
2996
3157
  runId: hooks.runId,
2997
3158
  toolName: call.name,
@@ -3006,15 +3167,19 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3006
3167
  error: outcome.error
3007
3168
  };
3008
3169
  }
3009
- await hooks.step(`persist:toolexec:${call.id}`, () => deps.store.updateToolCall({
3010
- toolCallId: call.id,
3011
- status: "executed",
3012
- output: outcome.output,
3013
- executionMs: outcome.executionMs,
3014
- ...toolType === "action" ? {
3015
- executedByRef: deciderRef
3016
- } : {}
3017
- }));
3170
+ await hooks.step(`persist:toolexec:${call.id}`, async () => {
3171
+ await deps.store.updateToolCall({
3172
+ toolCallId: call.id,
3173
+ status: "executed",
3174
+ output: outcome.output,
3175
+ executionMs: outcome.executionMs,
3176
+ ...toolType === "action" ? {
3177
+ executedByRef: deciderRef
3178
+ } : {},
3179
+ ...decided
3180
+ });
3181
+ await settle();
3182
+ });
3018
3183
  publishAgentToolCall({
3019
3184
  runId: hooks.runId,
3020
3185
  toolName: call.name,
@@ -3152,18 +3317,53 @@ async function runClaimedToolCall(turn, claimed) {
3152
3317
  return elicitToolCall(turn, claimed);
3153
3318
  }
3154
3319
  let deciderRef = input.actor.id;
3155
- if (toolType === "action") {
3156
- const decision = await hooks.awaitApproval(call, ctx);
3320
+ let meta;
3321
+ if (toolType === "action" && claimed.approval?.mode === "remembered") {
3322
+ meta = {
3323
+ approver: claimed.approval.approver,
3324
+ decidedBy: input.actor.id,
3325
+ decidedVia: "remembered",
3326
+ remember: true,
3327
+ streamed: true
3328
+ };
3329
+ } else if (toolType === "action" && claimed.approval?.mode !== "auto") {
3330
+ const ttlMs = claimed.approval?.mode === "ask" ? claimed.approval.ttlMs : void 0;
3331
+ const decision = ttlMs !== void 0 ? await hooks.awaitApproval(call, ctx, {
3332
+ timeoutMs: ttlMs
3333
+ }) : await hooks.awaitApproval(call, ctx);
3157
3334
  deciderRef = decision.executedByRef ?? input.actor.id;
3335
+ const streamsSettlement = claimed.approval !== void 0;
3336
+ if (decision.expired === true) {
3337
+ return expireToolCall(turn, claimed);
3338
+ }
3158
3339
  if (!decision.approved) {
3159
- await hooks.step(`persist:toolreject:${call.id}`, () => deps.store.updateToolCall({
3160
- toolCallId: call.id,
3161
- status: "rejected",
3162
- executedByRef: deciderRef,
3163
- ...decision.reason !== void 0 ? {
3164
- error: decision.reason
3165
- } : {}
3166
- }));
3340
+ await hooks.step(`persist:toolreject:${call.id}`, async () => {
3341
+ await deps.store.updateToolCall({
3342
+ toolCallId: call.id,
3343
+ status: "rejected",
3344
+ executedByRef: deciderRef,
3345
+ ...decision.reason !== void 0 ? {
3346
+ error: decision.reason
3347
+ } : {},
3348
+ ...decision.decidedVia !== void 0 ? {
3349
+ decidedVia: decision.decidedVia
3350
+ } : {}
3351
+ });
3352
+ if (streamsSettlement) {
3353
+ await turn.writer.write(encodeStreamEvent({
3354
+ kind: "approval-settled",
3355
+ id: call.id,
3356
+ status: "rejected",
3357
+ decidedBy: deciderRef,
3358
+ ...decision.decidedVia !== void 0 ? {
3359
+ decidedVia: decision.decidedVia
3360
+ } : {},
3361
+ ...decision.reason !== void 0 ? {
3362
+ reason: decision.reason
3363
+ } : {}
3364
+ }));
3365
+ }
3366
+ });
3167
3367
  publishAgentToolCall({
3168
3368
  runId: hooks.runId,
3169
3369
  toolName: call.name,
@@ -3186,10 +3386,60 @@ async function runClaimedToolCall(turn, claimed) {
3186
3386
  error: refusalNarrative(decision.reason)
3187
3387
  };
3188
3388
  }
3389
+ meta = {
3390
+ decidedBy: deciderRef,
3391
+ ...decision.decidedVia !== void 0 ? {
3392
+ decidedVia: decision.decidedVia
3393
+ } : {},
3394
+ ...decision.remember === true ? {
3395
+ remember: true
3396
+ } : {},
3397
+ streamed: streamsSettlement
3398
+ };
3189
3399
  }
3190
- return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef);
3400
+ return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef, meta);
3191
3401
  }
3192
3402
  __name(runClaimedToolCall, "runClaimedToolCall");
3403
+ async function expireToolCall(turn, claimed) {
3404
+ const { deps, hooks } = turn;
3405
+ const { call } = claimed;
3406
+ await hooks.step(`persist:toolreject:${call.id}`, async () => {
3407
+ await deps.store.updateToolCall({
3408
+ toolCallId: call.id,
3409
+ status: "expired",
3410
+ error: APPROVAL_EXPIRED_REASON
3411
+ });
3412
+ await turn.writer.write(encodeStreamEvent({
3413
+ kind: "approval-settled",
3414
+ id: call.id,
3415
+ status: "expired"
3416
+ }));
3417
+ });
3418
+ publishAgentToolCall({
3419
+ runId: hooks.runId,
3420
+ toolName: call.name,
3421
+ toolType: "action",
3422
+ status: "rejected"
3423
+ });
3424
+ return {
3425
+ id: call.id,
3426
+ name: call.name,
3427
+ output: {
3428
+ rejected: true,
3429
+ expired: true,
3430
+ reason: APPROVAL_EXPIRED_REASON
3431
+ },
3432
+ denied: true,
3433
+ expired: true,
3434
+ error: expiryNarrative()
3435
+ };
3436
+ }
3437
+ __name(expireToolCall, "expireToolCall");
3438
+ var APPROVAL_EXPIRED_REASON = "approval expired";
3439
+ function expiryNarrative() {
3440
+ 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.";
3441
+ }
3442
+ __name(expiryNarrative, "expiryNarrative");
3193
3443
  function outputFrame(result) {
3194
3444
  if (result.denied === true) {
3195
3445
  const reason = refusalReason(result);
@@ -3862,6 +4112,7 @@ export {
3862
4112
  AGENT_SPAN_EVENTS,
3863
4113
  AGENT_STORE,
3864
4114
  AGENT_TOOL_REGISTRY,
4115
+ APPROVAL_EXPIRED_REASON,
3865
4116
  ASK_TOOL_DESCRIPTION,
3866
4117
  ASK_TOOL_NAME,
3867
4118
  AgentRegistry,
@@ -3876,6 +4127,7 @@ export {
3876
4127
  DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION,
3877
4128
  DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS,
3878
4129
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
4130
+ DefaultApprovalPolicy,
3879
4131
  DefaultRolesPolicy,
3880
4132
  GLOBAL_SCOPE,
3881
4133
  MAX_ASK_QUESTIONS,
@@ -3884,6 +4136,7 @@ export {
3884
4136
  QuotaExceededError,
3885
4137
  REMEMBER_TOOL_DESCRIPTION,
3886
4138
  REMEMBER_TOOL_NAME,
4139
+ REQUESTER_APPROVER,
3887
4140
  RunCancelledError,
3888
4141
  SKILL_TOOL_DESCRIPTION,
3889
4142
  SKILL_TOOL_NAME,
@@ -3911,6 +4164,7 @@ export {
3911
4164
  createIncrementalGate,
3912
4165
  dayBoundsUtc,
3913
4166
  decodeStreamEvent,
4167
+ defaultCanDecide,
3914
4168
  defaultScopeResolver,
3915
4169
  detachedDelivered,
3916
4170
  detachedStarted,
@@ -3931,6 +4185,7 @@ export {
3931
4185
  isToolEnabled,
3932
4186
  isTransientToolError,
3933
4187
  loadSkill,
4188
+ mayDecideApproval,
3934
4189
  memoryForgetVerdict,
3935
4190
  memoryWriteVerdict,
3936
4191
  normalizeDelegation,
@@ -3976,6 +4231,7 @@ export {
3976
4231
  staticSkillProvider,
3977
4232
  summarizeWithModel,
3978
4233
  tenantScope,
4234
+ toolCallApprovalFromRow,
3979
4235
  traceLlmTurn,
3980
4236
  traceToolExecution,
3981
4237
  truncateDetailContent,