@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/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) {
@@ -2765,15 +2822,33 @@ async function claimToolCall(turn, call) {
2765
2822
  const spec = deps.registry.spec(call.name);
2766
2823
  const kind = call.kind ?? declaredKind(deps, call.name);
2767
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";
2768
2827
  await deps.store.recordToolCall({
2769
2828
  toolCallId: call.id,
2770
2829
  messageId,
2771
2830
  toolName: call.name,
2772
2831
  toolType: awaitsHuman ? "action" : "read",
2773
2832
  input: call.input,
2774
- status: awaitsHuman ? "pending_approval" : "auto_executed",
2775
- 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
+ } : {}
2776
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
+ }
2777
2852
  return {
2778
2853
  kind,
2779
2854
  ...spec?.targetAgent !== void 0 ? {
@@ -2783,6 +2858,9 @@ async function claimToolCall(turn, call) {
2783
2858
  // deployment with no detached edge writes the same bytes here it always has.
2784
2859
  ...spec?.detached === true ? {
2785
2860
  detached: true
2861
+ } : {},
2862
+ ...approval !== void 0 ? {
2863
+ approval
2786
2864
  } : {}
2787
2865
  };
2788
2866
  });
@@ -2802,10 +2880,52 @@ async function claimToolCall(turn, call) {
2802
2880
  ...persisted?.detached === true ? {
2803
2881
  detached: true
2804
2882
  } : {},
2883
+ ...toolType === "action" && persisted?.approval !== void 0 ? {
2884
+ approval: persisted.approval
2885
+ } : {},
2805
2886
  ctx: toolContext(deps, input, hooks)
2806
2887
  };
2807
2888
  }
2808
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");
2809
2929
  async function loadSkillIntoTurn(turn, claimed, startedAt) {
2810
2930
  const { deps, input, hooks } = turn;
2811
2931
  const { call } = claimed;
@@ -2989,17 +3109,50 @@ async function invokeClaimedTool(turn, claimed) {
2989
3109
  }
2990
3110
  }
2991
3111
  __name(invokeClaimedTool, "invokeClaimedTool");
2992
- async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3112
+ async function recordToolOutcome(turn, claimed, outcome, deciderRef, meta) {
2993
3113
  const { deps, hooks } = turn;
2994
3114
  const { call } = claimed;
2995
3115
  const toolType = claimed.toolType === "action" ? "action" : "read";
2996
- if (outcome.status === "failed") {
2997
- await hooks.step(`persist:toolfail:${call.id}`, () => deps.store.updateToolCall({
2998
- toolCallId: call.id,
2999
- status: "failed",
3000
- error: outcome.error,
3001
- 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
+ } : {}
3002
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
+ });
3003
3156
  publishAgentToolCall({
3004
3157
  runId: hooks.runId,
3005
3158
  toolName: call.name,
@@ -3014,15 +3167,19 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3014
3167
  error: outcome.error
3015
3168
  };
3016
3169
  }
3017
- await hooks.step(`persist:toolexec:${call.id}`, () => deps.store.updateToolCall({
3018
- toolCallId: call.id,
3019
- status: "executed",
3020
- output: outcome.output,
3021
- executionMs: outcome.executionMs,
3022
- ...toolType === "action" ? {
3023
- executedByRef: deciderRef
3024
- } : {}
3025
- }));
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
+ });
3026
3183
  publishAgentToolCall({
3027
3184
  runId: hooks.runId,
3028
3185
  toolName: call.name,
@@ -3160,18 +3317,53 @@ async function runClaimedToolCall(turn, claimed) {
3160
3317
  return elicitToolCall(turn, claimed);
3161
3318
  }
3162
3319
  let deciderRef = input.actor.id;
3163
- if (toolType === "action") {
3164
- 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);
3165
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
+ }
3166
3339
  if (!decision.approved) {
3167
- await hooks.step(`persist:toolreject:${call.id}`, () => deps.store.updateToolCall({
3168
- toolCallId: call.id,
3169
- status: "rejected",
3170
- executedByRef: deciderRef,
3171
- ...decision.reason !== void 0 ? {
3172
- error: decision.reason
3173
- } : {}
3174
- }));
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
+ });
3175
3367
  publishAgentToolCall({
3176
3368
  runId: hooks.runId,
3177
3369
  toolName: call.name,
@@ -3194,10 +3386,60 @@ async function runClaimedToolCall(turn, claimed) {
3194
3386
  error: refusalNarrative(decision.reason)
3195
3387
  };
3196
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
+ };
3197
3399
  }
3198
- return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef);
3400
+ return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef, meta);
3199
3401
  }
3200
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");
3201
3443
  function outputFrame(result) {
3202
3444
  if (result.denied === true) {
3203
3445
  const reason = refusalReason(result);
@@ -3870,6 +4112,7 @@ export {
3870
4112
  AGENT_SPAN_EVENTS,
3871
4113
  AGENT_STORE,
3872
4114
  AGENT_TOOL_REGISTRY,
4115
+ APPROVAL_EXPIRED_REASON,
3873
4116
  ASK_TOOL_DESCRIPTION,
3874
4117
  ASK_TOOL_NAME,
3875
4118
  AgentRegistry,
@@ -3884,6 +4127,7 @@ export {
3884
4127
  DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION,
3885
4128
  DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS,
3886
4129
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
4130
+ DefaultApprovalPolicy,
3887
4131
  DefaultRolesPolicy,
3888
4132
  GLOBAL_SCOPE,
3889
4133
  MAX_ASK_QUESTIONS,
@@ -3892,6 +4136,7 @@ export {
3892
4136
  QuotaExceededError,
3893
4137
  REMEMBER_TOOL_DESCRIPTION,
3894
4138
  REMEMBER_TOOL_NAME,
4139
+ REQUESTER_APPROVER,
3895
4140
  RunCancelledError,
3896
4141
  SKILL_TOOL_DESCRIPTION,
3897
4142
  SKILL_TOOL_NAME,
@@ -3919,6 +4164,7 @@ export {
3919
4164
  createIncrementalGate,
3920
4165
  dayBoundsUtc,
3921
4166
  decodeStreamEvent,
4167
+ defaultCanDecide,
3922
4168
  defaultScopeResolver,
3923
4169
  detachedDelivered,
3924
4170
  detachedStarted,
@@ -3939,6 +4185,7 @@ export {
3939
4185
  isToolEnabled,
3940
4186
  isTransientToolError,
3941
4187
  loadSkill,
4188
+ mayDecideApproval,
3942
4189
  memoryForgetVerdict,
3943
4190
  memoryWriteVerdict,
3944
4191
  normalizeDelegation,
@@ -3984,6 +4231,7 @@ export {
3984
4231
  staticSkillProvider,
3985
4232
  summarizeWithModel,
3986
4233
  tenantScope,
4234
+ toolCallApprovalFromRow,
3987
4235
  traceLlmTurn,
3988
4236
  traceToolExecution,
3989
4237
  truncateDetailContent,