@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.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,7 +61,9 @@ __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,
66
+ ELICITATION_INPUT_TYPES: () => ELICITATION_INPUT_TYPES,
64
67
  GLOBAL_SCOPE: () => GLOBAL_SCOPE,
65
68
  MAX_ASK_QUESTIONS: () => MAX_ASK_QUESTIONS,
66
69
  OutputRejectedError: () => OutputRejectedError,
@@ -68,6 +71,7 @@ __export(src_exports, {
68
71
  QuotaExceededError: () => QuotaExceededError,
69
72
  REMEMBER_TOOL_DESCRIPTION: () => REMEMBER_TOOL_DESCRIPTION,
70
73
  REMEMBER_TOOL_NAME: () => REMEMBER_TOOL_NAME,
74
+ REQUESTER_APPROVER: () => REQUESTER_APPROVER,
71
75
  RunCancelledError: () => RunCancelledError,
72
76
  SKILL_TOOL_DESCRIPTION: () => SKILL_TOOL_DESCRIPTION,
73
77
  SKILL_TOOL_NAME: () => SKILL_TOOL_NAME,
@@ -95,6 +99,7 @@ __export(src_exports, {
95
99
  createIncrementalGate: () => createIncrementalGate,
96
100
  dayBoundsUtc: () => dayBoundsUtc,
97
101
  decodeStreamEvent: () => decodeStreamEvent,
102
+ defaultCanDecide: () => defaultCanDecide,
98
103
  defaultScopeResolver: () => defaultScopeResolver,
99
104
  detachedDelivered: () => detachedDelivered,
100
105
  detachedStarted: () => detachedStarted,
@@ -114,7 +119,9 @@ __export(src_exports, {
114
119
  isReplayIntegrityError: () => isReplayIntegrityError,
115
120
  isToolEnabled: () => isToolEnabled,
116
121
  isTransientToolError: () => isTransientToolError,
122
+ isTypedQuestion: () => isTypedQuestion,
117
123
  loadSkill: () => loadSkill,
124
+ mayDecideApproval: () => mayDecideApproval,
118
125
  memoryForgetVerdict: () => memoryForgetVerdict,
119
126
  memoryWriteVerdict: () => memoryWriteVerdict,
120
127
  normalizeDelegation: () => normalizeDelegation,
@@ -134,6 +141,9 @@ __export(src_exports, {
134
141
  publishAgentSkillsResolved: () => publishAgentSkillsResolved,
135
142
  publishAgentToolCall: () => publishAgentToolCall,
136
143
  publishAgentToolRetry: () => publishAgentToolRetry,
144
+ questionOptions: () => questionOptions,
145
+ readElicitationInput: () => readElicitationInput,
146
+ readElicitationQuestions: () => readElicitationQuestions,
137
147
  releaseGatedFrames: () => releaseGatedFrames,
138
148
  rememberInputSchema: () => rememberInputSchema,
139
149
  rememberToolDefinition: () => rememberToolDefinition,
@@ -160,9 +170,12 @@ __export(src_exports, {
160
170
  staticSkillProvider: () => staticSkillProvider,
161
171
  summarizeWithModel: () => summarizeWithModel,
162
172
  tenantScope: () => tenantScope,
173
+ toolCallApprovalFromRow: () => toolCallApprovalFromRow,
163
174
  traceLlmTurn: () => traceLlmTurn,
164
175
  traceToolExecution: () => traceToolExecution,
165
176
  truncateDetailContent: () => truncateDetailContent,
177
+ validateElicitationAnswer: () => validateElicitationAnswer,
178
+ validateElicitationValue: () => validateElicitationValue,
166
179
  validateStructured: () => validateStructured,
167
180
  windowHistory: () => windowHistory,
168
181
  withAskTool: () => withAskTool,
@@ -388,6 +401,63 @@ function truncateDetailContent(content) {
388
401
  }
389
402
  __name(truncateDetailContent, "truncateDetailContent");
390
403
 
404
+ // src/spi/approval-policy.ts
405
+ var REQUESTER_APPROVER = "requester";
406
+ var DefaultApprovalPolicy = class {
407
+ static {
408
+ __name(this, "DefaultApprovalPolicy");
409
+ }
410
+ requirementFor(tool) {
411
+ return {
412
+ required: tool.kind === "action",
413
+ approver: REQUESTER_APPROVER
414
+ };
415
+ }
416
+ };
417
+ function defaultCanDecide(actor, decision) {
418
+ if (decision.approver === REQUESTER_APPROVER) {
419
+ return actor.id === decision.requesterRef;
420
+ }
421
+ return actor.roles?.includes(decision.approver) === true;
422
+ }
423
+ __name(defaultCanDecide, "defaultCanDecide");
424
+ async function mayDecideApproval(policy, actor, decision) {
425
+ if (policy?.canDecide !== void 0) {
426
+ return policy.canDecide(actor, decision);
427
+ }
428
+ return defaultCanDecide(actor, decision);
429
+ }
430
+ __name(mayDecideApproval, "mayDecideApproval");
431
+ function toolCallApprovalFromRow(row) {
432
+ if (row.approver === null || row.approver === void 0) {
433
+ return null;
434
+ }
435
+ const status = row.status === "pending_approval" ? "pending" : row.status === "rejected" ? "rejected" : row.status === "expired" ? "expired" : "approved";
436
+ const expiresAt = row.expiresAt instanceof Date ? row.expiresAt.toISOString() : typeof row.expiresAt === "string" ? row.expiresAt : void 0;
437
+ const decided = status === "approved" || status === "rejected";
438
+ return {
439
+ toolCallId: row.toolCallId,
440
+ approver: row.approver,
441
+ status,
442
+ ...expiresAt !== void 0 ? {
443
+ expiresAt
444
+ } : {},
445
+ ...row.remember === true ? {
446
+ remember: true
447
+ } : {},
448
+ ...decided && typeof row.executedByRef === "string" ? {
449
+ decidedBy: row.executedByRef
450
+ } : {},
451
+ ...decided && typeof row.decidedVia === "string" ? {
452
+ decidedVia: row.decidedVia
453
+ } : {},
454
+ ...status === "rejected" && typeof row.error === "string" ? {
455
+ reason: row.error
456
+ } : {}
457
+ };
458
+ }
459
+ __name(toolCallApprovalFromRow, "toolCallApprovalFromRow");
460
+
391
461
  // src/governance/compute.ts
392
462
  function estimateCost(usage, price) {
393
463
  if (price === void 0) {
@@ -947,11 +1017,235 @@ ${detail}`;
947
1017
  }
948
1018
  __name(repairInstruction, "repairInstruction");
949
1019
 
950
- // src/elicitation.ts
951
- function optionValues(question) {
952
- return new Set(question.options.map((option) => option.value));
1020
+ // src/elicitation-input.ts
1021
+ var ELICITATION_INPUT_TYPES = [
1022
+ "text",
1023
+ "textarea",
1024
+ "number",
1025
+ "boolean",
1026
+ "date",
1027
+ "email",
1028
+ "url",
1029
+ "select"
1030
+ ];
1031
+ function questionOptions(question) {
1032
+ return question.options ?? [];
1033
+ }
1034
+ __name(questionOptions, "questionOptions");
1035
+ function isTypedQuestion(question) {
1036
+ return question.input !== void 0 && question.input.type !== "select";
1037
+ }
1038
+ __name(isTypedQuestion, "isTypedQuestion");
1039
+ var DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
1040
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1041
+ var NUMBER_PATTERN = /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i;
1042
+ function isDate(value) {
1043
+ const match = DATE_PATTERN.exec(value);
1044
+ if (match === null) {
1045
+ return false;
1046
+ }
1047
+ const [, year, month, day] = match;
1048
+ const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
1049
+ return date.getUTCFullYear() === Number(year) && date.getUTCMonth() === Number(month) - 1 && date.getUTCDate() === Number(day);
1050
+ }
1051
+ __name(isDate, "isDate");
1052
+ function isHttpUrl(value) {
1053
+ try {
1054
+ const url = new URL(value);
1055
+ return url.protocol === "http:" || url.protocol === "https:";
1056
+ } catch {
1057
+ return false;
1058
+ }
1059
+ }
1060
+ __name(isHttpUrl, "isHttpUrl");
1061
+ function fullMatch(pattern, value) {
1062
+ try {
1063
+ return new RegExp(`^(?:${pattern})$`, "u").test(value);
1064
+ } catch {
1065
+ return void 0;
1066
+ }
1067
+ }
1068
+ __name(fullMatch, "fullMatch");
1069
+ function asNumber(bound) {
1070
+ if (typeof bound === "number" && Number.isFinite(bound)) {
1071
+ return bound;
1072
+ }
1073
+ if (typeof bound === "string" && NUMBER_PATTERN.test(bound.trim())) {
1074
+ return Number(bound);
1075
+ }
1076
+ return void 0;
1077
+ }
1078
+ __name(asNumber, "asNumber");
1079
+ function validateElicitationValue(question, value) {
1080
+ const input = question.input;
1081
+ if (input === void 0 || input.type === "select") {
1082
+ if (question.allowFreeText === true) {
1083
+ return null;
1084
+ }
1085
+ return questionOptions(question).some((option) => option.value === value) ? null : "must be one of the offered options";
1086
+ }
1087
+ switch (input.type) {
1088
+ case "number": {
1089
+ if (!NUMBER_PATTERN.test(value.trim()) || !Number.isFinite(Number(value))) {
1090
+ return "must be a number";
1091
+ }
1092
+ const number = Number(value);
1093
+ const min = asNumber(input.min);
1094
+ const max = asNumber(input.max);
1095
+ if (min !== void 0 && number < min) {
1096
+ return `must be at least ${min}`;
1097
+ }
1098
+ if (max !== void 0 && number > max) {
1099
+ return `must be at most ${max}`;
1100
+ }
1101
+ return null;
1102
+ }
1103
+ case "boolean":
1104
+ return value === "true" || value === "false" ? null : 'must be "true" or "false"';
1105
+ case "date": {
1106
+ if (!isDate(value)) {
1107
+ return "must be a date as YYYY-MM-DD";
1108
+ }
1109
+ if (typeof input.min === "string" && isDate(input.min) && value < input.min) {
1110
+ return `must be on or after ${input.min}`;
1111
+ }
1112
+ if (typeof input.max === "string" && isDate(input.max) && value > input.max) {
1113
+ return `must be on or before ${input.max}`;
1114
+ }
1115
+ return null;
1116
+ }
1117
+ case "email":
1118
+ if (!EMAIL_PATTERN.test(value)) {
1119
+ return "must be an email address";
1120
+ }
1121
+ break;
1122
+ case "url":
1123
+ if (!isHttpUrl(value)) {
1124
+ return "must be an http(s) URL";
1125
+ }
1126
+ break;
1127
+ case "text":
1128
+ case "textarea": {
1129
+ const min = asNumber(input.min);
1130
+ const max = asNumber(input.max);
1131
+ if (min !== void 0 && value.length < min) {
1132
+ return `must be at least ${min} characters`;
1133
+ }
1134
+ if (max !== void 0 && value.length > max) {
1135
+ return `must be at most ${max} characters`;
1136
+ }
1137
+ break;
1138
+ }
1139
+ }
1140
+ if (input.pattern !== void 0 && fullMatch(input.pattern, value) === false) {
1141
+ return "does not match the expected format";
1142
+ }
1143
+ return null;
1144
+ }
1145
+ __name(validateElicitationValue, "validateElicitationValue");
1146
+ function validateElicitationAnswer(question, values) {
1147
+ const present = values.filter((value) => value !== "");
1148
+ if (present.length === 0) {
1149
+ return question.input?.required === true ? "requires an answer" : null;
1150
+ }
1151
+ if (question.multiple !== true && present.length > 1) {
1152
+ return "takes a single value";
1153
+ }
1154
+ for (const value of present) {
1155
+ const problem = validateElicitationValue(question, value);
1156
+ if (problem !== null) {
1157
+ return problem;
1158
+ }
1159
+ }
1160
+ return null;
1161
+ }
1162
+ __name(validateElicitationAnswer, "validateElicitationAnswer");
1163
+ function readElicitationInput(raw) {
1164
+ if (typeof raw !== "object" || raw === null) {
1165
+ return void 0;
1166
+ }
1167
+ const candidate = raw;
1168
+ const type = candidate.type;
1169
+ if (typeof type !== "string" || !ELICITATION_INPUT_TYPES.includes(type)) {
1170
+ return void 0;
1171
+ }
1172
+ const bound = /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isFinite(value) || typeof value === "string" ? value : void 0, "bound");
1173
+ const min = bound(candidate.min);
1174
+ const max = bound(candidate.max);
1175
+ return {
1176
+ type,
1177
+ ...typeof candidate.placeholder === "string" ? {
1178
+ placeholder: candidate.placeholder
1179
+ } : {},
1180
+ ...candidate.required === true ? {
1181
+ required: true
1182
+ } : {},
1183
+ ...min !== void 0 ? {
1184
+ min
1185
+ } : {},
1186
+ ...max !== void 0 ? {
1187
+ max
1188
+ } : {},
1189
+ ...typeof candidate.pattern === "string" ? {
1190
+ pattern: candidate.pattern
1191
+ } : {}
1192
+ };
953
1193
  }
954
- __name(optionValues, "optionValues");
1194
+ __name(readElicitationInput, "readElicitationInput");
1195
+ function readElicitationQuestions(input) {
1196
+ if (typeof input !== "object" || input === null) {
1197
+ return [];
1198
+ }
1199
+ const raw = input.questions;
1200
+ if (!Array.isArray(raw)) {
1201
+ return [];
1202
+ }
1203
+ const questions = [];
1204
+ for (const item of raw) {
1205
+ if (typeof item !== "object" || item === null) {
1206
+ continue;
1207
+ }
1208
+ const candidate = item;
1209
+ if (typeof candidate.id !== "string" || typeof candidate.prompt !== "string") {
1210
+ continue;
1211
+ }
1212
+ const options = Array.isArray(candidate.options) ? candidate.options.flatMap((option) => {
1213
+ if (typeof option !== "object" || option === null) {
1214
+ return [];
1215
+ }
1216
+ const { value, label } = option;
1217
+ return typeof value === "string" && typeof label === "string" ? [
1218
+ {
1219
+ value,
1220
+ label
1221
+ }
1222
+ ] : [];
1223
+ }) : [];
1224
+ const typed = readElicitationInput(candidate.input);
1225
+ const defaults = Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [];
1226
+ questions.push({
1227
+ id: candidate.id,
1228
+ prompt: candidate.prompt,
1229
+ options,
1230
+ ...typed !== void 0 ? {
1231
+ input: typed
1232
+ } : {},
1233
+ ...defaults.length > 0 ? {
1234
+ defaults
1235
+ } : {},
1236
+ ...candidate.multiple === true ? {
1237
+ multiple: true
1238
+ } : {},
1239
+ ...candidate.allowFreeText === true ? {
1240
+ allowFreeText: true
1241
+ } : {}
1242
+ });
1243
+ }
1244
+ return questions;
1245
+ }
1246
+ __name(readElicitationQuestions, "readElicitationQuestions");
1247
+
1248
+ // src/elicitation.ts
955
1249
  function normalizeElicitationReply(reply) {
956
1250
  if (typeof reply !== "object" || reply === null) {
957
1251
  return {
@@ -987,8 +1281,7 @@ function resolveElicitation(request, raw) {
987
1281
  defaulted.push(question.id);
988
1282
  continue;
989
1283
  }
990
- const allowed = optionValues(question);
991
- const valid = question.allowFreeText === true ? submitted : submitted.filter((value) => allowed.has(value));
1284
+ const valid = submitted.filter((value) => value !== "" && validateElicitationValue(question, value) === null);
992
1285
  answers[question.id] = question.multiple === true ? valid : valid.slice(0, 1);
993
1286
  }
994
1287
  return {
@@ -1009,7 +1302,7 @@ __name(settleElicitation, "settleElicitation");
1009
1302
  function renderElicitationAnswers(request, outcome) {
1010
1303
  const lines = request.questions.map((question) => {
1011
1304
  const chosen = outcome.answers[question.id] ?? [];
1012
- const labels = chosen.map((value) => question.options.find((option) => option.value === value)?.label ?? value);
1305
+ const labels = chosen.map((value) => questionOptions(question).find((option) => option.value === value)?.label ?? value);
1013
1306
  return `${question.prompt} \u2192 ${labels.length > 0 ? labels.join(", ") : "(no answer)"}`;
1014
1307
  });
1015
1308
  const preface = outcome.skipped ? "The user declined to answer and asked you to proceed on these assumptions:" : "The user answered:";
@@ -1075,7 +1368,23 @@ function parseQuestion(raw, path, issues) {
1075
1368
  ], "must be a non-empty string"));
1076
1369
  return void 0;
1077
1370
  }
1078
- if (!Array.isArray(candidate.options) || candidate.options.length === 0) {
1371
+ let input;
1372
+ if (candidate.input !== void 0) {
1373
+ input = readElicitationInput(candidate.input);
1374
+ if (input === void 0) {
1375
+ issues.push(issue([
1376
+ ...path,
1377
+ "input",
1378
+ "type"
1379
+ ], `must be one of ${ELICITATION_INPUT_TYPES.join(", ")}`));
1380
+ return void 0;
1381
+ }
1382
+ }
1383
+ const described = typeof candidate.description === "string" && candidate.description.length > 0 ? {
1384
+ description: candidate.description
1385
+ } : {};
1386
+ const typed = input !== void 0 && input.type !== "select";
1387
+ if (!typed && (!Array.isArray(candidate.options) || candidate.options.length === 0)) {
1079
1388
  issues.push(issue([
1080
1389
  ...path,
1081
1390
  "options"
@@ -1083,7 +1392,7 @@ function parseQuestion(raw, path, issues) {
1083
1392
  return void 0;
1084
1393
  }
1085
1394
  const options = [];
1086
- for (const [index, rawOption] of candidate.options.entries()) {
1395
+ for (const [index, rawOption] of (Array.isArray(candidate.options) ? candidate.options : []).entries()) {
1087
1396
  const option = parseOption(rawOption, [
1088
1397
  ...path,
1089
1398
  "options",
@@ -1093,6 +1402,33 @@ function parseQuestion(raw, path, issues) {
1093
1402
  options.push(option);
1094
1403
  }
1095
1404
  }
1405
+ if (typed) {
1406
+ const question = {
1407
+ id: candidate.id,
1408
+ prompt: candidate.prompt,
1409
+ ...described,
1410
+ ...options.length > 0 ? {
1411
+ options
1412
+ } : {},
1413
+ input,
1414
+ ...candidate.multiple === true ? {
1415
+ multiple: true
1416
+ } : {}
1417
+ };
1418
+ const offered2 = Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [];
1419
+ const defaults2 = offered2.filter((value) => validateElicitationValue(question, value) === null);
1420
+ if (offered2.length > 0 && defaults2.length === 0) {
1421
+ issues.push(issue([
1422
+ ...path,
1423
+ "defaults"
1424
+ ], `must be valid ${input.type} values`));
1425
+ return void 0;
1426
+ }
1427
+ return defaults2.length > 0 ? {
1428
+ ...question,
1429
+ defaults: defaults2
1430
+ } : question;
1431
+ }
1096
1432
  if (!Array.isArray(candidate.defaults) || candidate.defaults.length === 0) {
1097
1433
  issues.push(issue([
1098
1434
  ...path,
@@ -1112,7 +1448,11 @@ function parseQuestion(raw, path, issues) {
1112
1448
  return {
1113
1449
  id: candidate.id,
1114
1450
  prompt: candidate.prompt,
1451
+ ...described,
1115
1452
  options,
1453
+ ...input !== void 0 ? {
1454
+ input
1455
+ } : {},
1116
1456
  defaults,
1117
1457
  ...candidate.multiple === true ? {
1118
1458
  multiple: true
@@ -1140,9 +1480,7 @@ var ASK_JSON_SCHEMA = {
1140
1480
  additionalProperties: false,
1141
1481
  required: [
1142
1482
  "id",
1143
- "prompt",
1144
- "options",
1145
- "defaults"
1483
+ "prompt"
1146
1484
  ],
1147
1485
  properties: {
1148
1486
  id: {
@@ -1152,6 +1490,50 @@ var ASK_JSON_SCHEMA = {
1152
1490
  prompt: {
1153
1491
  type: "string"
1154
1492
  },
1493
+ description: {
1494
+ type: "string",
1495
+ description: "A line of help under the prompt."
1496
+ },
1497
+ input: {
1498
+ type: "object",
1499
+ additionalProperties: false,
1500
+ required: [
1501
+ "type"
1502
+ ],
1503
+ 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.',
1504
+ properties: {
1505
+ type: {
1506
+ type: "string",
1507
+ enum: [
1508
+ ...ELICITATION_INPUT_TYPES
1509
+ ]
1510
+ },
1511
+ placeholder: {
1512
+ type: "string"
1513
+ },
1514
+ required: {
1515
+ type: "boolean"
1516
+ },
1517
+ min: {
1518
+ type: [
1519
+ "number",
1520
+ "string"
1521
+ ],
1522
+ description: "number: minimum; text: minimum length; date: earliest YYYY-MM-DD."
1523
+ },
1524
+ max: {
1525
+ type: [
1526
+ "number",
1527
+ "string"
1528
+ ],
1529
+ description: "number: maximum; text: maximum length; date: latest YYYY-MM-DD."
1530
+ },
1531
+ pattern: {
1532
+ type: "string",
1533
+ description: "A regex the whole value must match."
1534
+ }
1535
+ }
1536
+ },
1155
1537
  multiple: {
1156
1538
  type: "boolean",
1157
1539
  description: "Allow more than one option to be chosen."
@@ -1166,7 +1548,7 @@ var ASK_JSON_SCHEMA = {
1166
1548
  items: {
1167
1549
  type: "string"
1168
1550
  },
1169
- description: "REQUIRED. The option values you would pick yourself, so the user can confirm rather than decide."
1551
+ 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."
1170
1552
  },
1171
1553
  options: {
1172
1554
  type: "array",
@@ -2938,15 +3320,33 @@ async function claimToolCall(turn, call) {
2938
3320
  const spec = deps.registry.spec(call.name);
2939
3321
  const kind = call.kind ?? declaredKind(deps, call.name);
2940
3322
  const awaitsHuman = kind === "action" || kind === "ask";
3323
+ const approval = kind === "action" ? await claimApproval(turn, call, spec) : void 0;
3324
+ const parks = kind === "ask" || approval?.mode === "ask";
2941
3325
  await deps.store.recordToolCall({
2942
3326
  toolCallId: call.id,
2943
3327
  messageId,
2944
3328
  toolName: call.name,
2945
3329
  toolType: awaitsHuman ? "action" : "read",
2946
3330
  input: call.input,
2947
- status: awaitsHuman ? "pending_approval" : "auto_executed",
2948
- runId: hooks.runId
3331
+ status: parks ? "pending_approval" : "auto_executed",
3332
+ runId: hooks.runId,
3333
+ ...approval !== void 0 && approval.mode !== "auto" ? {
3334
+ approver: approval.approver
3335
+ } : {},
3336
+ ...approval?.mode === "ask" && approval.expiresAt !== void 0 ? {
3337
+ expiresAt: approval.expiresAt
3338
+ } : {}
2949
3339
  });
3340
+ if (approval?.mode === "ask") {
3341
+ await turn.writer.write(encodeStreamEvent({
3342
+ kind: "approval-requested",
3343
+ id: call.id,
3344
+ approver: approval.approver,
3345
+ ...approval.expiresAt !== void 0 ? {
3346
+ expiresAt: approval.expiresAt
3347
+ } : {}
3348
+ }));
3349
+ }
2950
3350
  return {
2951
3351
  kind,
2952
3352
  ...spec?.targetAgent !== void 0 ? {
@@ -2956,6 +3356,9 @@ async function claimToolCall(turn, call) {
2956
3356
  // deployment with no detached edge writes the same bytes here it always has.
2957
3357
  ...spec?.detached === true ? {
2958
3358
  detached: true
3359
+ } : {},
3360
+ ...approval !== void 0 ? {
3361
+ approval
2959
3362
  } : {}
2960
3363
  };
2961
3364
  });
@@ -2975,10 +3378,52 @@ async function claimToolCall(turn, call) {
2975
3378
  ...persisted?.detached === true ? {
2976
3379
  detached: true
2977
3380
  } : {},
3381
+ ...toolType === "action" && persisted?.approval !== void 0 ? {
3382
+ approval: persisted.approval
3383
+ } : {},
2978
3384
  ctx: toolContext(deps, input, hooks)
2979
3385
  };
2980
3386
  }
2981
3387
  __name(claimToolCall, "claimToolCall");
3388
+ async function claimApproval(turn, call, spec) {
3389
+ const { deps, input, hooks } = turn;
3390
+ const policy = deps.approvalPolicy ?? new DefaultApprovalPolicy();
3391
+ const requirement = await policy.requirementFor({
3392
+ name: call.name,
3393
+ kind: "action",
3394
+ ...spec !== void 0 ? {
3395
+ spec
3396
+ } : {}
3397
+ }, input.actor, {
3398
+ threadId: input.threadId,
3399
+ runId: hooks.runId,
3400
+ ...input.agentName !== void 0 ? {
3401
+ agentName: input.agentName
3402
+ } : {}
3403
+ });
3404
+ if (!requirement.required) {
3405
+ return {
3406
+ mode: "auto"
3407
+ };
3408
+ }
3409
+ const remembered = await deps.store.rememberedApprovals?.(input.threadId) ?? [];
3410
+ if (remembered.includes(call.name)) {
3411
+ return {
3412
+ mode: "remembered",
3413
+ approver: requirement.approver
3414
+ };
3415
+ }
3416
+ const ttlMs = requirement.ttlMs !== void 0 && Number.isFinite(requirement.ttlMs) && requirement.ttlMs > 0 ? Math.floor(requirement.ttlMs) : void 0;
3417
+ return {
3418
+ mode: "ask",
3419
+ approver: requirement.approver,
3420
+ ...ttlMs !== void 0 ? {
3421
+ ttlMs,
3422
+ expiresAt: new Date(Date.now() + ttlMs).toISOString()
3423
+ } : {}
3424
+ };
3425
+ }
3426
+ __name(claimApproval, "claimApproval");
2982
3427
  async function loadSkillIntoTurn(turn, claimed, startedAt) {
2983
3428
  const { deps, input, hooks } = turn;
2984
3429
  const { call } = claimed;
@@ -3162,17 +3607,50 @@ async function invokeClaimedTool(turn, claimed) {
3162
3607
  }
3163
3608
  }
3164
3609
  __name(invokeClaimedTool, "invokeClaimedTool");
3165
- async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3610
+ async function recordToolOutcome(turn, claimed, outcome, deciderRef, meta) {
3166
3611
  const { deps, hooks } = turn;
3167
3612
  const { call } = claimed;
3168
3613
  const toolType = claimed.toolType === "action" ? "action" : "read";
3169
- if (outcome.status === "failed") {
3170
- await hooks.step(`persist:toolfail:${call.id}`, () => deps.store.updateToolCall({
3171
- toolCallId: call.id,
3172
- status: "failed",
3173
- error: outcome.error,
3174
- executionMs: outcome.executionMs
3614
+ const decided = meta === void 0 ? {} : {
3615
+ executedByRef: meta.decidedBy,
3616
+ ...meta.decidedVia !== void 0 ? {
3617
+ decidedVia: meta.decidedVia
3618
+ } : {},
3619
+ ...meta.remember === true ? {
3620
+ remember: true
3621
+ } : {}
3622
+ };
3623
+ const settle = /* @__PURE__ */ __name(async () => {
3624
+ if (meta === void 0 || !meta.streamed) {
3625
+ return;
3626
+ }
3627
+ await turn.writer.write(encodeStreamEvent({
3628
+ kind: "approval-settled",
3629
+ id: call.id,
3630
+ status: "approved",
3631
+ ...meta.approver !== void 0 ? {
3632
+ approver: meta.approver
3633
+ } : {},
3634
+ decidedBy: meta.decidedBy,
3635
+ ...meta.decidedVia !== void 0 ? {
3636
+ decidedVia: meta.decidedVia
3637
+ } : {},
3638
+ ...meta.remember === true ? {
3639
+ remember: true
3640
+ } : {}
3175
3641
  }));
3642
+ }, "settle");
3643
+ if (outcome.status === "failed") {
3644
+ await hooks.step(`persist:toolfail:${call.id}`, async () => {
3645
+ await deps.store.updateToolCall({
3646
+ toolCallId: call.id,
3647
+ status: "failed",
3648
+ error: outcome.error,
3649
+ executionMs: outcome.executionMs,
3650
+ ...decided
3651
+ });
3652
+ await settle();
3653
+ });
3176
3654
  publishAgentToolCall({
3177
3655
  runId: hooks.runId,
3178
3656
  toolName: call.name,
@@ -3187,15 +3665,19 @@ async function recordToolOutcome(turn, claimed, outcome, deciderRef) {
3187
3665
  error: outcome.error
3188
3666
  };
3189
3667
  }
3190
- await hooks.step(`persist:toolexec:${call.id}`, () => deps.store.updateToolCall({
3191
- toolCallId: call.id,
3192
- status: "executed",
3193
- output: outcome.output,
3194
- executionMs: outcome.executionMs,
3195
- ...toolType === "action" ? {
3196
- executedByRef: deciderRef
3197
- } : {}
3198
- }));
3668
+ await hooks.step(`persist:toolexec:${call.id}`, async () => {
3669
+ await deps.store.updateToolCall({
3670
+ toolCallId: call.id,
3671
+ status: "executed",
3672
+ output: outcome.output,
3673
+ executionMs: outcome.executionMs,
3674
+ ...toolType === "action" ? {
3675
+ executedByRef: deciderRef
3676
+ } : {},
3677
+ ...decided
3678
+ });
3679
+ await settle();
3680
+ });
3199
3681
  publishAgentToolCall({
3200
3682
  runId: hooks.runId,
3201
3683
  toolName: call.name,
@@ -3333,18 +3815,53 @@ async function runClaimedToolCall(turn, claimed) {
3333
3815
  return elicitToolCall(turn, claimed);
3334
3816
  }
3335
3817
  let deciderRef = input.actor.id;
3336
- if (toolType === "action") {
3337
- const decision = await hooks.awaitApproval(call, ctx);
3818
+ let meta;
3819
+ if (toolType === "action" && claimed.approval?.mode === "remembered") {
3820
+ meta = {
3821
+ approver: claimed.approval.approver,
3822
+ decidedBy: input.actor.id,
3823
+ decidedVia: "remembered",
3824
+ remember: true,
3825
+ streamed: true
3826
+ };
3827
+ } else if (toolType === "action" && claimed.approval?.mode !== "auto") {
3828
+ const ttlMs = claimed.approval?.mode === "ask" ? claimed.approval.ttlMs : void 0;
3829
+ const decision = ttlMs !== void 0 ? await hooks.awaitApproval(call, ctx, {
3830
+ timeoutMs: ttlMs
3831
+ }) : await hooks.awaitApproval(call, ctx);
3338
3832
  deciderRef = decision.executedByRef ?? input.actor.id;
3833
+ const streamsSettlement = claimed.approval !== void 0;
3834
+ if (decision.expired === true) {
3835
+ return expireToolCall(turn, claimed);
3836
+ }
3339
3837
  if (!decision.approved) {
3340
- await hooks.step(`persist:toolreject:${call.id}`, () => deps.store.updateToolCall({
3341
- toolCallId: call.id,
3342
- status: "rejected",
3343
- executedByRef: deciderRef,
3344
- ...decision.reason !== void 0 ? {
3345
- error: decision.reason
3346
- } : {}
3347
- }));
3838
+ await hooks.step(`persist:toolreject:${call.id}`, async () => {
3839
+ await deps.store.updateToolCall({
3840
+ toolCallId: call.id,
3841
+ status: "rejected",
3842
+ executedByRef: deciderRef,
3843
+ ...decision.reason !== void 0 ? {
3844
+ error: decision.reason
3845
+ } : {},
3846
+ ...decision.decidedVia !== void 0 ? {
3847
+ decidedVia: decision.decidedVia
3848
+ } : {}
3849
+ });
3850
+ if (streamsSettlement) {
3851
+ await turn.writer.write(encodeStreamEvent({
3852
+ kind: "approval-settled",
3853
+ id: call.id,
3854
+ status: "rejected",
3855
+ decidedBy: deciderRef,
3856
+ ...decision.decidedVia !== void 0 ? {
3857
+ decidedVia: decision.decidedVia
3858
+ } : {},
3859
+ ...decision.reason !== void 0 ? {
3860
+ reason: decision.reason
3861
+ } : {}
3862
+ }));
3863
+ }
3864
+ });
3348
3865
  publishAgentToolCall({
3349
3866
  runId: hooks.runId,
3350
3867
  toolName: call.name,
@@ -3367,10 +3884,60 @@ async function runClaimedToolCall(turn, claimed) {
3367
3884
  error: refusalNarrative(decision.reason)
3368
3885
  };
3369
3886
  }
3887
+ meta = {
3888
+ decidedBy: deciderRef,
3889
+ ...decision.decidedVia !== void 0 ? {
3890
+ decidedVia: decision.decidedVia
3891
+ } : {},
3892
+ ...decision.remember === true ? {
3893
+ remember: true
3894
+ } : {},
3895
+ streamed: streamsSettlement
3896
+ };
3370
3897
  }
3371
- return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef);
3898
+ return recordToolOutcome(turn, claimed, await invokeClaimedTool(turn, claimed), deciderRef, meta);
3372
3899
  }
3373
3900
  __name(runClaimedToolCall, "runClaimedToolCall");
3901
+ async function expireToolCall(turn, claimed) {
3902
+ const { deps, hooks } = turn;
3903
+ const { call } = claimed;
3904
+ await hooks.step(`persist:toolreject:${call.id}`, async () => {
3905
+ await deps.store.updateToolCall({
3906
+ toolCallId: call.id,
3907
+ status: "expired",
3908
+ error: APPROVAL_EXPIRED_REASON
3909
+ });
3910
+ await turn.writer.write(encodeStreamEvent({
3911
+ kind: "approval-settled",
3912
+ id: call.id,
3913
+ status: "expired"
3914
+ }));
3915
+ });
3916
+ publishAgentToolCall({
3917
+ runId: hooks.runId,
3918
+ toolName: call.name,
3919
+ toolType: "action",
3920
+ status: "rejected"
3921
+ });
3922
+ return {
3923
+ id: call.id,
3924
+ name: call.name,
3925
+ output: {
3926
+ rejected: true,
3927
+ expired: true,
3928
+ reason: APPROVAL_EXPIRED_REASON
3929
+ },
3930
+ denied: true,
3931
+ expired: true,
3932
+ error: expiryNarrative()
3933
+ };
3934
+ }
3935
+ __name(expireToolCall, "expireToolCall");
3936
+ var APPROVAL_EXPIRED_REASON = "approval expired";
3937
+ function expiryNarrative() {
3938
+ 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.";
3939
+ }
3940
+ __name(expiryNarrative, "expiryNarrative");
3374
3941
  function outputFrame(result) {
3375
3942
  if (result.denied === true) {
3376
3943
  const reason = refusalReason(result);
@@ -4044,6 +4611,7 @@ __name(runAgentLoop, "runAgentLoop");
4044
4611
  AGENT_SPAN_EVENTS,
4045
4612
  AGENT_STORE,
4046
4613
  AGENT_TOOL_REGISTRY,
4614
+ APPROVAL_EXPIRED_REASON,
4047
4615
  ASK_TOOL_DESCRIPTION,
4048
4616
  ASK_TOOL_NAME,
4049
4617
  AgentRegistry,
@@ -4058,7 +4626,9 @@ __name(runAgentLoop, "runAgentLoop");
4058
4626
  DEFAULT_STRUCTURED_OUTPUT_INSTRUCTION,
4059
4627
  DEFAULT_TOOL_TRANSIENT_RETRY_ATTEMPTS,
4060
4628
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
4629
+ DefaultApprovalPolicy,
4061
4630
  DefaultRolesPolicy,
4631
+ ELICITATION_INPUT_TYPES,
4062
4632
  GLOBAL_SCOPE,
4063
4633
  MAX_ASK_QUESTIONS,
4064
4634
  OutputRejectedError,
@@ -4066,6 +4636,7 @@ __name(runAgentLoop, "runAgentLoop");
4066
4636
  QuotaExceededError,
4067
4637
  REMEMBER_TOOL_DESCRIPTION,
4068
4638
  REMEMBER_TOOL_NAME,
4639
+ REQUESTER_APPROVER,
4069
4640
  RunCancelledError,
4070
4641
  SKILL_TOOL_DESCRIPTION,
4071
4642
  SKILL_TOOL_NAME,
@@ -4093,6 +4664,7 @@ __name(runAgentLoop, "runAgentLoop");
4093
4664
  createIncrementalGate,
4094
4665
  dayBoundsUtc,
4095
4666
  decodeStreamEvent,
4667
+ defaultCanDecide,
4096
4668
  defaultScopeResolver,
4097
4669
  detachedDelivered,
4098
4670
  detachedStarted,
@@ -4112,7 +4684,9 @@ __name(runAgentLoop, "runAgentLoop");
4112
4684
  isReplayIntegrityError,
4113
4685
  isToolEnabled,
4114
4686
  isTransientToolError,
4687
+ isTypedQuestion,
4115
4688
  loadSkill,
4689
+ mayDecideApproval,
4116
4690
  memoryForgetVerdict,
4117
4691
  memoryWriteVerdict,
4118
4692
  normalizeDelegation,
@@ -4132,6 +4706,9 @@ __name(runAgentLoop, "runAgentLoop");
4132
4706
  publishAgentSkillsResolved,
4133
4707
  publishAgentToolCall,
4134
4708
  publishAgentToolRetry,
4709
+ questionOptions,
4710
+ readElicitationInput,
4711
+ readElicitationQuestions,
4135
4712
  releaseGatedFrames,
4136
4713
  rememberInputSchema,
4137
4714
  rememberToolDefinition,
@@ -4158,9 +4735,12 @@ __name(runAgentLoop, "runAgentLoop");
4158
4735
  staticSkillProvider,
4159
4736
  summarizeWithModel,
4160
4737
  tenantScope,
4738
+ toolCallApprovalFromRow,
4161
4739
  traceLlmTurn,
4162
4740
  traceToolExecution,
4163
4741
  truncateDetailContent,
4742
+ validateElicitationAnswer,
4743
+ validateElicitationValue,
4164
4744
  validateStructured,
4165
4745
  windowHistory,
4166
4746
  withAskTool,