@dudousxd/nestjs-agent-core 0.19.0 → 0.21.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) {
@@ -774,11 +831,235 @@ ${detail}`;
774
831
  }
775
832
  __name(repairInstruction, "repairInstruction");
776
833
 
777
- // src/elicitation.ts
778
- function optionValues(question) {
779
- return new Set(question.options.map((option) => option.value));
834
+ // src/elicitation-input.ts
835
+ var ELICITATION_INPUT_TYPES = [
836
+ "text",
837
+ "textarea",
838
+ "number",
839
+ "boolean",
840
+ "date",
841
+ "email",
842
+ "url",
843
+ "select"
844
+ ];
845
+ function questionOptions(question) {
846
+ return question.options ?? [];
847
+ }
848
+ __name(questionOptions, "questionOptions");
849
+ function isTypedQuestion(question) {
850
+ return question.input !== void 0 && question.input.type !== "select";
851
+ }
852
+ __name(isTypedQuestion, "isTypedQuestion");
853
+ var DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
854
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
855
+ var NUMBER_PATTERN = /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i;
856
+ function isDate(value) {
857
+ const match = DATE_PATTERN.exec(value);
858
+ if (match === null) {
859
+ return false;
860
+ }
861
+ const [, year, month, day] = match;
862
+ const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
863
+ return date.getUTCFullYear() === Number(year) && date.getUTCMonth() === Number(month) - 1 && date.getUTCDate() === Number(day);
864
+ }
865
+ __name(isDate, "isDate");
866
+ function isHttpUrl(value) {
867
+ try {
868
+ const url = new URL(value);
869
+ return url.protocol === "http:" || url.protocol === "https:";
870
+ } catch {
871
+ return false;
872
+ }
873
+ }
874
+ __name(isHttpUrl, "isHttpUrl");
875
+ function fullMatch(pattern, value) {
876
+ try {
877
+ return new RegExp(`^(?:${pattern})$`, "u").test(value);
878
+ } catch {
879
+ return void 0;
880
+ }
881
+ }
882
+ __name(fullMatch, "fullMatch");
883
+ function asNumber(bound) {
884
+ if (typeof bound === "number" && Number.isFinite(bound)) {
885
+ return bound;
886
+ }
887
+ if (typeof bound === "string" && NUMBER_PATTERN.test(bound.trim())) {
888
+ return Number(bound);
889
+ }
890
+ return void 0;
891
+ }
892
+ __name(asNumber, "asNumber");
893
+ function validateElicitationValue(question, value) {
894
+ const input = question.input;
895
+ if (input === void 0 || input.type === "select") {
896
+ if (question.allowFreeText === true) {
897
+ return null;
898
+ }
899
+ return questionOptions(question).some((option) => option.value === value) ? null : "must be one of the offered options";
900
+ }
901
+ switch (input.type) {
902
+ case "number": {
903
+ if (!NUMBER_PATTERN.test(value.trim()) || !Number.isFinite(Number(value))) {
904
+ return "must be a number";
905
+ }
906
+ const number = Number(value);
907
+ const min = asNumber(input.min);
908
+ const max = asNumber(input.max);
909
+ if (min !== void 0 && number < min) {
910
+ return `must be at least ${min}`;
911
+ }
912
+ if (max !== void 0 && number > max) {
913
+ return `must be at most ${max}`;
914
+ }
915
+ return null;
916
+ }
917
+ case "boolean":
918
+ return value === "true" || value === "false" ? null : 'must be "true" or "false"';
919
+ case "date": {
920
+ if (!isDate(value)) {
921
+ return "must be a date as YYYY-MM-DD";
922
+ }
923
+ if (typeof input.min === "string" && isDate(input.min) && value < input.min) {
924
+ return `must be on or after ${input.min}`;
925
+ }
926
+ if (typeof input.max === "string" && isDate(input.max) && value > input.max) {
927
+ return `must be on or before ${input.max}`;
928
+ }
929
+ return null;
930
+ }
931
+ case "email":
932
+ if (!EMAIL_PATTERN.test(value)) {
933
+ return "must be an email address";
934
+ }
935
+ break;
936
+ case "url":
937
+ if (!isHttpUrl(value)) {
938
+ return "must be an http(s) URL";
939
+ }
940
+ break;
941
+ case "text":
942
+ case "textarea": {
943
+ const min = asNumber(input.min);
944
+ const max = asNumber(input.max);
945
+ if (min !== void 0 && value.length < min) {
946
+ return `must be at least ${min} characters`;
947
+ }
948
+ if (max !== void 0 && value.length > max) {
949
+ return `must be at most ${max} characters`;
950
+ }
951
+ break;
952
+ }
953
+ }
954
+ if (input.pattern !== void 0 && fullMatch(input.pattern, value) === false) {
955
+ return "does not match the expected format";
956
+ }
957
+ return null;
958
+ }
959
+ __name(validateElicitationValue, "validateElicitationValue");
960
+ function validateElicitationAnswer(question, values) {
961
+ const present = values.filter((value) => value !== "");
962
+ if (present.length === 0) {
963
+ return question.input?.required === true ? "requires an answer" : null;
964
+ }
965
+ if (question.multiple !== true && present.length > 1) {
966
+ return "takes a single value";
967
+ }
968
+ for (const value of present) {
969
+ const problem = validateElicitationValue(question, value);
970
+ if (problem !== null) {
971
+ return problem;
972
+ }
973
+ }
974
+ return null;
975
+ }
976
+ __name(validateElicitationAnswer, "validateElicitationAnswer");
977
+ function readElicitationInput(raw) {
978
+ if (typeof raw !== "object" || raw === null) {
979
+ return void 0;
980
+ }
981
+ const candidate = raw;
982
+ const type = candidate.type;
983
+ if (typeof type !== "string" || !ELICITATION_INPUT_TYPES.includes(type)) {
984
+ return void 0;
985
+ }
986
+ const bound = /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isFinite(value) || typeof value === "string" ? value : void 0, "bound");
987
+ const min = bound(candidate.min);
988
+ const max = bound(candidate.max);
989
+ return {
990
+ type,
991
+ ...typeof candidate.placeholder === "string" ? {
992
+ placeholder: candidate.placeholder
993
+ } : {},
994
+ ...candidate.required === true ? {
995
+ required: true
996
+ } : {},
997
+ ...min !== void 0 ? {
998
+ min
999
+ } : {},
1000
+ ...max !== void 0 ? {
1001
+ max
1002
+ } : {},
1003
+ ...typeof candidate.pattern === "string" ? {
1004
+ pattern: candidate.pattern
1005
+ } : {}
1006
+ };
780
1007
  }
781
- __name(optionValues, "optionValues");
1008
+ __name(readElicitationInput, "readElicitationInput");
1009
+ function readElicitationQuestions(input) {
1010
+ if (typeof input !== "object" || input === null) {
1011
+ return [];
1012
+ }
1013
+ const raw = input.questions;
1014
+ if (!Array.isArray(raw)) {
1015
+ return [];
1016
+ }
1017
+ const questions = [];
1018
+ for (const item of raw) {
1019
+ if (typeof item !== "object" || item === null) {
1020
+ continue;
1021
+ }
1022
+ const candidate = item;
1023
+ if (typeof candidate.id !== "string" || typeof candidate.prompt !== "string") {
1024
+ continue;
1025
+ }
1026
+ const options = Array.isArray(candidate.options) ? candidate.options.flatMap((option) => {
1027
+ if (typeof option !== "object" || option === null) {
1028
+ return [];
1029
+ }
1030
+ const { value, label } = option;
1031
+ return typeof value === "string" && typeof label === "string" ? [
1032
+ {
1033
+ value,
1034
+ label
1035
+ }
1036
+ ] : [];
1037
+ }) : [];
1038
+ const typed = readElicitationInput(candidate.input);
1039
+ const defaults = Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [];
1040
+ questions.push({
1041
+ id: candidate.id,
1042
+ prompt: candidate.prompt,
1043
+ options,
1044
+ ...typed !== void 0 ? {
1045
+ input: typed
1046
+ } : {},
1047
+ ...defaults.length > 0 ? {
1048
+ defaults
1049
+ } : {},
1050
+ ...candidate.multiple === true ? {
1051
+ multiple: true
1052
+ } : {},
1053
+ ...candidate.allowFreeText === true ? {
1054
+ allowFreeText: true
1055
+ } : {}
1056
+ });
1057
+ }
1058
+ return questions;
1059
+ }
1060
+ __name(readElicitationQuestions, "readElicitationQuestions");
1061
+
1062
+ // src/elicitation.ts
782
1063
  function normalizeElicitationReply(reply) {
783
1064
  if (typeof reply !== "object" || reply === null) {
784
1065
  return {
@@ -814,8 +1095,7 @@ function resolveElicitation(request, raw) {
814
1095
  defaulted.push(question.id);
815
1096
  continue;
816
1097
  }
817
- const allowed = optionValues(question);
818
- const valid = question.allowFreeText === true ? submitted : submitted.filter((value) => allowed.has(value));
1098
+ const valid = submitted.filter((value) => value !== "" && validateElicitationValue(question, value) === null);
819
1099
  answers[question.id] = question.multiple === true ? valid : valid.slice(0, 1);
820
1100
  }
821
1101
  return {
@@ -836,7 +1116,7 @@ __name(settleElicitation, "settleElicitation");
836
1116
  function renderElicitationAnswers(request, outcome) {
837
1117
  const lines = request.questions.map((question) => {
838
1118
  const chosen = outcome.answers[question.id] ?? [];
839
- const labels = chosen.map((value) => question.options.find((option) => option.value === value)?.label ?? value);
1119
+ const labels = chosen.map((value) => questionOptions(question).find((option) => option.value === value)?.label ?? value);
840
1120
  return `${question.prompt} \u2192 ${labels.length > 0 ? labels.join(", ") : "(no answer)"}`;
841
1121
  });
842
1122
  const preface = outcome.skipped ? "The user declined to answer and asked you to proceed on these assumptions:" : "The user answered:";
@@ -902,7 +1182,23 @@ function parseQuestion(raw, path, issues) {
902
1182
  ], "must be a non-empty string"));
903
1183
  return void 0;
904
1184
  }
905
- if (!Array.isArray(candidate.options) || candidate.options.length === 0) {
1185
+ let input;
1186
+ if (candidate.input !== void 0) {
1187
+ input = readElicitationInput(candidate.input);
1188
+ if (input === void 0) {
1189
+ issues.push(issue([
1190
+ ...path,
1191
+ "input",
1192
+ "type"
1193
+ ], `must be one of ${ELICITATION_INPUT_TYPES.join(", ")}`));
1194
+ return void 0;
1195
+ }
1196
+ }
1197
+ const described = typeof candidate.description === "string" && candidate.description.length > 0 ? {
1198
+ description: candidate.description
1199
+ } : {};
1200
+ const typed = input !== void 0 && input.type !== "select";
1201
+ if (!typed && (!Array.isArray(candidate.options) || candidate.options.length === 0)) {
906
1202
  issues.push(issue([
907
1203
  ...path,
908
1204
  "options"
@@ -910,7 +1206,7 @@ function parseQuestion(raw, path, issues) {
910
1206
  return void 0;
911
1207
  }
912
1208
  const options = [];
913
- for (const [index, rawOption] of candidate.options.entries()) {
1209
+ for (const [index, rawOption] of (Array.isArray(candidate.options) ? candidate.options : []).entries()) {
914
1210
  const option = parseOption(rawOption, [
915
1211
  ...path,
916
1212
  "options",
@@ -920,6 +1216,33 @@ function parseQuestion(raw, path, issues) {
920
1216
  options.push(option);
921
1217
  }
922
1218
  }
1219
+ if (typed) {
1220
+ const question = {
1221
+ id: candidate.id,
1222
+ prompt: candidate.prompt,
1223
+ ...described,
1224
+ ...options.length > 0 ? {
1225
+ options
1226
+ } : {},
1227
+ input,
1228
+ ...candidate.multiple === true ? {
1229
+ multiple: true
1230
+ } : {}
1231
+ };
1232
+ const offered2 = Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [];
1233
+ const defaults2 = offered2.filter((value) => validateElicitationValue(question, value) === null);
1234
+ if (offered2.length > 0 && defaults2.length === 0) {
1235
+ issues.push(issue([
1236
+ ...path,
1237
+ "defaults"
1238
+ ], `must be valid ${input.type} values`));
1239
+ return void 0;
1240
+ }
1241
+ return defaults2.length > 0 ? {
1242
+ ...question,
1243
+ defaults: defaults2
1244
+ } : question;
1245
+ }
923
1246
  if (!Array.isArray(candidate.defaults) || candidate.defaults.length === 0) {
924
1247
  issues.push(issue([
925
1248
  ...path,
@@ -939,7 +1262,11 @@ function parseQuestion(raw, path, issues) {
939
1262
  return {
940
1263
  id: candidate.id,
941
1264
  prompt: candidate.prompt,
1265
+ ...described,
942
1266
  options,
1267
+ ...input !== void 0 ? {
1268
+ input
1269
+ } : {},
943
1270
  defaults,
944
1271
  ...candidate.multiple === true ? {
945
1272
  multiple: true
@@ -967,9 +1294,7 @@ var ASK_JSON_SCHEMA = {
967
1294
  additionalProperties: false,
968
1295
  required: [
969
1296
  "id",
970
- "prompt",
971
- "options",
972
- "defaults"
1297
+ "prompt"
973
1298
  ],
974
1299
  properties: {
975
1300
  id: {
@@ -979,6 +1304,50 @@ var ASK_JSON_SCHEMA = {
979
1304
  prompt: {
980
1305
  type: "string"
981
1306
  },
1307
+ description: {
1308
+ type: "string",
1309
+ description: "A line of help under the prompt."
1310
+ },
1311
+ input: {
1312
+ type: "object",
1313
+ additionalProperties: false,
1314
+ required: [
1315
+ "type"
1316
+ ],
1317
+ description: 'Ask for a typed value instead of offering options. Omit for a pick from options. Answers come back as strings: numbers as decimals, booleans as "true"/"false", dates as YYYY-MM-DD.',
1318
+ properties: {
1319
+ type: {
1320
+ type: "string",
1321
+ enum: [
1322
+ ...ELICITATION_INPUT_TYPES
1323
+ ]
1324
+ },
1325
+ placeholder: {
1326
+ type: "string"
1327
+ },
1328
+ required: {
1329
+ type: "boolean"
1330
+ },
1331
+ min: {
1332
+ type: [
1333
+ "number",
1334
+ "string"
1335
+ ],
1336
+ description: "number: minimum; text: minimum length; date: earliest YYYY-MM-DD."
1337
+ },
1338
+ max: {
1339
+ type: [
1340
+ "number",
1341
+ "string"
1342
+ ],
1343
+ description: "number: maximum; text: maximum length; date: latest YYYY-MM-DD."
1344
+ },
1345
+ pattern: {
1346
+ type: "string",
1347
+ description: "A regex the whole value must match."
1348
+ }
1349
+ }
1350
+ },
982
1351
  multiple: {
983
1352
  type: "boolean",
984
1353
  description: "Allow more than one option to be chosen."
@@ -993,7 +1362,7 @@ var ASK_JSON_SCHEMA = {
993
1362
  items: {
994
1363
  type: "string"
995
1364
  },
996
- description: "REQUIRED. The option values you would pick yourself, so the user can confirm rather than decide."
1365
+ description: "REQUIRED for a pick from options: the option values you would pick yourself, so the user can confirm rather than decide. For a typed input, the value you would enter, when there is a sensible one."
997
1366
  },
998
1367
  options: {
999
1368
  type: "array",
@@ -2765,15 +3134,33 @@ async function claimToolCall(turn, call) {
2765
3134
  const spec = deps.registry.spec(call.name);
2766
3135
  const kind = call.kind ?? declaredKind(deps, call.name);
2767
3136
  const awaitsHuman = kind === "action" || kind === "ask";
3137
+ const approval = kind === "action" ? await claimApproval(turn, call, spec) : void 0;
3138
+ const parks = kind === "ask" || approval?.mode === "ask";
2768
3139
  await deps.store.recordToolCall({
2769
3140
  toolCallId: call.id,
2770
3141
  messageId,
2771
3142
  toolName: call.name,
2772
3143
  toolType: awaitsHuman ? "action" : "read",
2773
3144
  input: call.input,
2774
- status: awaitsHuman ? "pending_approval" : "auto_executed",
2775
- runId: hooks.runId
3145
+ status: parks ? "pending_approval" : "auto_executed",
3146
+ runId: hooks.runId,
3147
+ ...approval !== void 0 && approval.mode !== "auto" ? {
3148
+ approver: approval.approver
3149
+ } : {},
3150
+ ...approval?.mode === "ask" && approval.expiresAt !== void 0 ? {
3151
+ expiresAt: approval.expiresAt
3152
+ } : {}
2776
3153
  });
3154
+ if (approval?.mode === "ask") {
3155
+ await turn.writer.write(encodeStreamEvent({
3156
+ kind: "approval-requested",
3157
+ id: call.id,
3158
+ approver: approval.approver,
3159
+ ...approval.expiresAt !== void 0 ? {
3160
+ expiresAt: approval.expiresAt
3161
+ } : {}
3162
+ }));
3163
+ }
2777
3164
  return {
2778
3165
  kind,
2779
3166
  ...spec?.targetAgent !== void 0 ? {
@@ -2783,6 +3170,9 @@ async function claimToolCall(turn, call) {
2783
3170
  // deployment with no detached edge writes the same bytes here it always has.
2784
3171
  ...spec?.detached === true ? {
2785
3172
  detached: true
3173
+ } : {},
3174
+ ...approval !== void 0 ? {
3175
+ approval
2786
3176
  } : {}
2787
3177
  };
2788
3178
  });
@@ -2802,10 +3192,52 @@ async function claimToolCall(turn, call) {
2802
3192
  ...persisted?.detached === true ? {
2803
3193
  detached: true
2804
3194
  } : {},
3195
+ ...toolType === "action" && persisted?.approval !== void 0 ? {
3196
+ approval: persisted.approval
3197
+ } : {},
2805
3198
  ctx: toolContext(deps, input, hooks)
2806
3199
  };
2807
3200
  }
2808
3201
  __name(claimToolCall, "claimToolCall");
3202
+ async function claimApproval(turn, call, spec) {
3203
+ const { deps, input, hooks } = turn;
3204
+ const policy = deps.approvalPolicy ?? new DefaultApprovalPolicy();
3205
+ const requirement = await policy.requirementFor({
3206
+ name: call.name,
3207
+ kind: "action",
3208
+ ...spec !== void 0 ? {
3209
+ spec
3210
+ } : {}
3211
+ }, input.actor, {
3212
+ threadId: input.threadId,
3213
+ runId: hooks.runId,
3214
+ ...input.agentName !== void 0 ? {
3215
+ agentName: input.agentName
3216
+ } : {}
3217
+ });
3218
+ if (!requirement.required) {
3219
+ return {
3220
+ mode: "auto"
3221
+ };
3222
+ }
3223
+ const remembered = await deps.store.rememberedApprovals?.(input.threadId) ?? [];
3224
+ if (remembered.includes(call.name)) {
3225
+ return {
3226
+ mode: "remembered",
3227
+ approver: requirement.approver
3228
+ };
3229
+ }
3230
+ const ttlMs = requirement.ttlMs !== void 0 && Number.isFinite(requirement.ttlMs) && requirement.ttlMs > 0 ? Math.floor(requirement.ttlMs) : void 0;
3231
+ return {
3232
+ mode: "ask",
3233
+ approver: requirement.approver,
3234
+ ...ttlMs !== void 0 ? {
3235
+ ttlMs,
3236
+ expiresAt: new Date(Date.now() + ttlMs).toISOString()
3237
+ } : {}
3238
+ };
3239
+ }
3240
+ __name(claimApproval, "claimApproval");
2809
3241
  async function loadSkillIntoTurn(turn, claimed, startedAt) {
2810
3242
  const { deps, input, hooks } = turn;
2811
3243
  const { call } = claimed;
@@ -2989,17 +3421,50 @@ async function invokeClaimedTool(turn, claimed) {
2989
3421
  }
2990
3422
  }
2991
3423
  __name(invokeClaimedTool, "invokeClaimedTool");
2992
- async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3424
+ async function recordToolOutcome(turn, claimed, outcome, deciderRef, meta) {
2993
3425
  const { deps, hooks } = turn;
2994
3426
  const { call } = claimed;
2995
3427
  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
3428
+ const decided = meta === void 0 ? {} : {
3429
+ executedByRef: meta.decidedBy,
3430
+ ...meta.decidedVia !== void 0 ? {
3431
+ decidedVia: meta.decidedVia
3432
+ } : {},
3433
+ ...meta.remember === true ? {
3434
+ remember: true
3435
+ } : {}
3436
+ };
3437
+ const settle = /* @__PURE__ */ __name(async () => {
3438
+ if (meta === void 0 || !meta.streamed) {
3439
+ return;
3440
+ }
3441
+ await turn.writer.write(encodeStreamEvent({
3442
+ kind: "approval-settled",
3443
+ id: call.id,
3444
+ status: "approved",
3445
+ ...meta.approver !== void 0 ? {
3446
+ approver: meta.approver
3447
+ } : {},
3448
+ decidedBy: meta.decidedBy,
3449
+ ...meta.decidedVia !== void 0 ? {
3450
+ decidedVia: meta.decidedVia
3451
+ } : {},
3452
+ ...meta.remember === true ? {
3453
+ remember: true
3454
+ } : {}
3002
3455
  }));
3456
+ }, "settle");
3457
+ if (outcome.status === "failed") {
3458
+ await hooks.step(`persist:toolfail:${call.id}`, async () => {
3459
+ await deps.store.updateToolCall({
3460
+ toolCallId: call.id,
3461
+ status: "failed",
3462
+ error: outcome.error,
3463
+ executionMs: outcome.executionMs,
3464
+ ...decided
3465
+ });
3466
+ await settle();
3467
+ });
3003
3468
  publishAgentToolCall({
3004
3469
  runId: hooks.runId,
3005
3470
  toolName: call.name,
@@ -3014,15 +3479,19 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3014
3479
  error: outcome.error
3015
3480
  };
3016
3481
  }
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
- }));
3482
+ await hooks.step(`persist:toolexec:${call.id}`, async () => {
3483
+ await deps.store.updateToolCall({
3484
+ toolCallId: call.id,
3485
+ status: "executed",
3486
+ output: outcome.output,
3487
+ executionMs: outcome.executionMs,
3488
+ ...toolType === "action" ? {
3489
+ executedByRef: deciderRef
3490
+ } : {},
3491
+ ...decided
3492
+ });
3493
+ await settle();
3494
+ });
3026
3495
  publishAgentToolCall({
3027
3496
  runId: hooks.runId,
3028
3497
  toolName: call.name,
@@ -3160,18 +3629,53 @@ async function runClaimedToolCall(turn, claimed) {
3160
3629
  return elicitToolCall(turn, claimed);
3161
3630
  }
3162
3631
  let deciderRef = input.actor.id;
3163
- if (toolType === "action") {
3164
- const decision = await hooks.awaitApproval(call, ctx);
3632
+ let meta;
3633
+ if (toolType === "action" && claimed.approval?.mode === "remembered") {
3634
+ meta = {
3635
+ approver: claimed.approval.approver,
3636
+ decidedBy: input.actor.id,
3637
+ decidedVia: "remembered",
3638
+ remember: true,
3639
+ streamed: true
3640
+ };
3641
+ } else if (toolType === "action" && claimed.approval?.mode !== "auto") {
3642
+ const ttlMs = claimed.approval?.mode === "ask" ? claimed.approval.ttlMs : void 0;
3643
+ const decision = ttlMs !== void 0 ? await hooks.awaitApproval(call, ctx, {
3644
+ timeoutMs: ttlMs
3645
+ }) : await hooks.awaitApproval(call, ctx);
3165
3646
  deciderRef = decision.executedByRef ?? input.actor.id;
3647
+ const streamsSettlement = claimed.approval !== void 0;
3648
+ if (decision.expired === true) {
3649
+ return expireToolCall(turn, claimed);
3650
+ }
3166
3651
  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
- }));
3652
+ await hooks.step(`persist:toolreject:${call.id}`, async () => {
3653
+ await deps.store.updateToolCall({
3654
+ toolCallId: call.id,
3655
+ status: "rejected",
3656
+ executedByRef: deciderRef,
3657
+ ...decision.reason !== void 0 ? {
3658
+ error: decision.reason
3659
+ } : {},
3660
+ ...decision.decidedVia !== void 0 ? {
3661
+ decidedVia: decision.decidedVia
3662
+ } : {}
3663
+ });
3664
+ if (streamsSettlement) {
3665
+ await turn.writer.write(encodeStreamEvent({
3666
+ kind: "approval-settled",
3667
+ id: call.id,
3668
+ status: "rejected",
3669
+ decidedBy: deciderRef,
3670
+ ...decision.decidedVia !== void 0 ? {
3671
+ decidedVia: decision.decidedVia
3672
+ } : {},
3673
+ ...decision.reason !== void 0 ? {
3674
+ reason: decision.reason
3675
+ } : {}
3676
+ }));
3677
+ }
3678
+ });
3175
3679
  publishAgentToolCall({
3176
3680
  runId: hooks.runId,
3177
3681
  toolName: call.name,
@@ -3194,10 +3698,60 @@ async function runClaimedToolCall(turn, claimed) {
3194
3698
  error: refusalNarrative(decision.reason)
3195
3699
  };
3196
3700
  }
3701
+ meta = {
3702
+ decidedBy: deciderRef,
3703
+ ...decision.decidedVia !== void 0 ? {
3704
+ decidedVia: decision.decidedVia
3705
+ } : {},
3706
+ ...decision.remember === true ? {
3707
+ remember: true
3708
+ } : {},
3709
+ streamed: streamsSettlement
3710
+ };
3197
3711
  }
3198
- return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef);
3712
+ return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef, meta);
3199
3713
  }
3200
3714
  __name(runClaimedToolCall, "runClaimedToolCall");
3715
+ async function expireToolCall(turn, claimed) {
3716
+ const { deps, hooks } = turn;
3717
+ const { call } = claimed;
3718
+ await hooks.step(`persist:toolreject:${call.id}`, async () => {
3719
+ await deps.store.updateToolCall({
3720
+ toolCallId: call.id,
3721
+ status: "expired",
3722
+ error: APPROVAL_EXPIRED_REASON
3723
+ });
3724
+ await turn.writer.write(encodeStreamEvent({
3725
+ kind: "approval-settled",
3726
+ id: call.id,
3727
+ status: "expired"
3728
+ }));
3729
+ });
3730
+ publishAgentToolCall({
3731
+ runId: hooks.runId,
3732
+ toolName: call.name,
3733
+ toolType: "action",
3734
+ status: "rejected"
3735
+ });
3736
+ return {
3737
+ id: call.id,
3738
+ name: call.name,
3739
+ output: {
3740
+ rejected: true,
3741
+ expired: true,
3742
+ reason: APPROVAL_EXPIRED_REASON
3743
+ },
3744
+ denied: true,
3745
+ expired: true,
3746
+ error: expiryNarrative()
3747
+ };
3748
+ }
3749
+ __name(expireToolCall, "expireToolCall");
3750
+ var APPROVAL_EXPIRED_REASON = "approval expired";
3751
+ function expiryNarrative() {
3752
+ 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.";
3753
+ }
3754
+ __name(expiryNarrative, "expiryNarrative");
3201
3755
  function outputFrame(result) {
3202
3756
  if (result.denied === true) {
3203
3757
  const reason = refusalReason(result);
@@ -3870,6 +4424,7 @@ export {
3870
4424
  AGENT_SPAN_EVENTS,
3871
4425
  AGENT_STORE,
3872
4426
  AGENT_TOOL_REGISTRY,
4427
+ APPROVAL_EXPIRED_REASON,
3873
4428
  ASK_TOOL_DESCRIPTION,
3874
4429
  ASK_TOOL_NAME,
3875
4430
  AgentRegistry,
@@ -3884,7 +4439,9 @@ export {
3884
4439
  DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION,
3885
4440
  DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS,
3886
4441
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
4442
+ DefaultApprovalPolicy,
3887
4443
  DefaultRolesPolicy,
4444
+ ELICITATION_INPUT_TYPES,
3888
4445
  GLOBAL_SCOPE,
3889
4446
  MAX_ASK_QUESTIONS,
3890
4447
  OutputRejectedError,
@@ -3892,6 +4449,7 @@ export {
3892
4449
  QuotaExceededError,
3893
4450
  REMEMBER_TOOL_DESCRIPTION,
3894
4451
  REMEMBER_TOOL_NAME,
4452
+ REQUESTER_APPROVER,
3895
4453
  RunCancelledError,
3896
4454
  SKILL_TOOL_DESCRIPTION,
3897
4455
  SKILL_TOOL_NAME,
@@ -3919,6 +4477,7 @@ export {
3919
4477
  createIncrementalGate,
3920
4478
  dayBoundsUtc,
3921
4479
  decodeStreamEvent,
4480
+ defaultCanDecide,
3922
4481
  defaultScopeResolver,
3923
4482
  detachedDelivered,
3924
4483
  detachedStarted,
@@ -3938,7 +4497,9 @@ export {
3938
4497
  isReplayIntegrityError,
3939
4498
  isToolEnabled,
3940
4499
  isTransientToolError,
4500
+ isTypedQuestion,
3941
4501
  loadSkill,
4502
+ mayDecideApproval,
3942
4503
  memoryForgetVerdict,
3943
4504
  memoryWriteVerdict,
3944
4505
  normalizeDelegation,
@@ -3958,6 +4519,9 @@ export {
3958
4519
  publishAgentSkillsResolved,
3959
4520
  publishAgentToolCall,
3960
4521
  publishAgentToolRetry,
4522
+ questionOptions,
4523
+ readElicitationInput,
4524
+ readElicitationQuestions,
3961
4525
  releaseGatedFrames,
3962
4526
  rememberInputSchema,
3963
4527
  rememberToolDefinition,
@@ -3984,9 +4548,12 @@ export {
3984
4548
  staticSkillProvider,
3985
4549
  summarizeWithModel,
3986
4550
  tenantScope,
4551
+ toolCallApprovalFromRow,
3987
4552
  traceLlmTurn,
3988
4553
  traceToolExecution,
3989
4554
  truncateDetailContent,
4555
+ validateElicitationAnswer,
4556
+ validateElicitationValue,
3990
4557
  validateStructured,
3991
4558
  windowHistory,
3992
4559
  withAskTool,