@dudousxd/nestjs-agent-core 0.20.0 → 0.22.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
@@ -63,6 +63,7 @@ __export(src_exports, {
63
63
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS: () => DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
64
64
  DefaultApprovalPolicy: () => DefaultApprovalPolicy,
65
65
  DefaultRolesPolicy: () => DefaultRolesPolicy,
66
+ ELICITATION_INPUT_TYPES: () => ELICITATION_INPUT_TYPES,
66
67
  GLOBAL_SCOPE: () => GLOBAL_SCOPE,
67
68
  MAX_ASK_QUESTIONS: () => MAX_ASK_QUESTIONS,
68
69
  OutputRejectedError: () => OutputRejectedError,
@@ -96,6 +97,7 @@ __export(src_exports, {
96
97
  compositeSkillProvider: () => compositeSkillProvider,
97
98
  createFrameBuffer: () => createFrameBuffer,
98
99
  createIncrementalGate: () => createIncrementalGate,
100
+ createUiCollector: () => createUiCollector,
99
101
  dayBoundsUtc: () => dayBoundsUtc,
100
102
  decodeStreamEvent: () => decodeStreamEvent,
101
103
  defaultCanDecide: () => defaultCanDecide,
@@ -118,10 +120,12 @@ __export(src_exports, {
118
120
  isReplayIntegrityError: () => isReplayIntegrityError,
119
121
  isToolEnabled: () => isToolEnabled,
120
122
  isTransientToolError: () => isTransientToolError,
123
+ isTypedQuestion: () => isTypedQuestion,
121
124
  loadSkill: () => loadSkill,
122
125
  mayDecideApproval: () => mayDecideApproval,
123
126
  memoryForgetVerdict: () => memoryForgetVerdict,
124
127
  memoryWriteVerdict: () => memoryWriteVerdict,
128
+ mergeUi: () => mergeUi,
125
129
  normalizeDelegation: () => normalizeDelegation,
126
130
  normalizeElicitationReply: () => normalizeElicitationReply,
127
131
  observeTurnFrames: () => observeTurnFrames,
@@ -139,6 +143,9 @@ __export(src_exports, {
139
143
  publishAgentSkillsResolved: () => publishAgentSkillsResolved,
140
144
  publishAgentToolCall: () => publishAgentToolCall,
141
145
  publishAgentToolRetry: () => publishAgentToolRetry,
146
+ questionOptions: () => questionOptions,
147
+ readElicitationInput: () => readElicitationInput,
148
+ readElicitationQuestions: () => readElicitationQuestions,
142
149
  releaseGatedFrames: () => releaseGatedFrames,
143
150
  rememberInputSchema: () => rememberInputSchema,
144
151
  rememberToolDefinition: () => rememberToolDefinition,
@@ -169,6 +176,9 @@ __export(src_exports, {
169
176
  traceLlmTurn: () => traceLlmTurn,
170
177
  traceToolExecution: () => traceToolExecution,
171
178
  truncateDetailContent: () => truncateDetailContent,
179
+ unwrapToolStepOutput: () => unwrapToolStepOutput,
180
+ validateElicitationAnswer: () => validateElicitationAnswer,
181
+ validateElicitationValue: () => validateElicitationValue,
172
182
  validateStructured: () => validateStructured,
173
183
  windowHistory: () => windowHistory,
174
184
  withAskTool: () => withAskTool,
@@ -176,6 +186,7 @@ __export(src_exports, {
176
186
  withSkillTool: () => withSkillTool,
177
187
  withToolTimeout: () => withToolTimeout,
178
188
  withTurnFrames: () => withTurnFrames,
189
+ wrapToolStepOutput: () => wrapToolStepOutput,
179
190
  writeMemory: () => writeMemory
180
191
  });
181
192
  module.exports = __toCommonJS(src_exports);
@@ -336,6 +347,95 @@ function withTurnFrames(result, frames) {
336
347
  }
337
348
  __name(withTurnFrames, "withTurnFrames");
338
349
 
350
+ // src/tool-ui.ts
351
+ var TOOL_STEP_UI = "@@nestjs-agent/tool-step-ui";
352
+ function wrapToolStepOutput(output, ui) {
353
+ if (ui.length === 0) {
354
+ return output;
355
+ }
356
+ const wrapped = {
357
+ [TOOL_STEP_UI]: 1,
358
+ output,
359
+ ui: [
360
+ ...ui
361
+ ]
362
+ };
363
+ return wrapped;
364
+ }
365
+ __name(wrapToolStepOutput, "wrapToolStepOutput");
366
+ function unwrapToolStepOutput(raw) {
367
+ if (typeof raw === "object" && raw !== null && raw[TOOL_STEP_UI] === 1) {
368
+ const wrapped = raw;
369
+ return {
370
+ output: wrapped.output,
371
+ ui: Array.isArray(wrapped.ui) ? wrapped.ui : []
372
+ };
373
+ }
374
+ return {
375
+ output: raw,
376
+ ui: []
377
+ };
378
+ }
379
+ __name(unwrapToolStepOutput, "unwrapToolStepOutput");
380
+ function createUiCollector(toolCallId, write) {
381
+ const pushed = /* @__PURE__ */ new Map();
382
+ let next = 0;
383
+ const emit2 = /* @__PURE__ */ __name(async (component, props, options = {}) => {
384
+ if (typeof component !== "string" || component.length === 0) {
385
+ throw new Error("emitUi: component must be a non-empty string");
386
+ }
387
+ if (typeof props !== "object" || props === null || Array.isArray(props)) {
388
+ throw new Error("emitUi: props must be a JSON object");
389
+ }
390
+ let id = options.id;
391
+ if (id === void 0) {
392
+ id = `${toolCallId}:ui:${next}`;
393
+ next += 1;
394
+ }
395
+ const entry = {
396
+ id,
397
+ component,
398
+ // Snapshot: the frame and the persisted value are what the tool pushed at THIS moment, not
399
+ // whatever the object it handed over looks like when the step settles.
400
+ props: JSON.parse(JSON.stringify(props)),
401
+ ...options.version !== void 0 ? {
402
+ version: options.version
403
+ } : {},
404
+ toolCallId
405
+ };
406
+ pushed.set(id, entry);
407
+ await write?.({
408
+ kind: "ui",
409
+ ...entry
410
+ });
411
+ return {
412
+ id
413
+ };
414
+ }, "emit");
415
+ return {
416
+ emit: emit2,
417
+ components: /* @__PURE__ */ __name(() => [
418
+ ...pushed.values()
419
+ ], "components"),
420
+ restart: /* @__PURE__ */ __name(() => {
421
+ next = 0;
422
+ }, "restart")
423
+ };
424
+ }
425
+ __name(createUiCollector, "createUiCollector");
426
+ function mergeUi(...lists) {
427
+ const merged = /* @__PURE__ */ new Map();
428
+ for (const list of lists) {
429
+ for (const component of list ?? []) {
430
+ merged.set(component.id, component);
431
+ }
432
+ }
433
+ return [
434
+ ...merged.values()
435
+ ];
436
+ }
437
+ __name(mergeUi, "mergeUi");
438
+
339
439
  // src/spi/pricing-store.ts
340
440
  async function seedModelPrices(store, prices) {
341
441
  for (const price of prices) {
@@ -1010,11 +1110,235 @@ ${detail}`;
1010
1110
  }
1011
1111
  __name(repairInstruction, "repairInstruction");
1012
1112
 
1013
- // src/elicitation.ts
1014
- function optionValues(question) {
1015
- return new Set(question.options.map((option) => option.value));
1113
+ // src/elicitation-input.ts
1114
+ var ELICITATION_INPUT_TYPES = [
1115
+ "text",
1116
+ "textarea",
1117
+ "number",
1118
+ "boolean",
1119
+ "date",
1120
+ "email",
1121
+ "url",
1122
+ "select"
1123
+ ];
1124
+ function questionOptions(question) {
1125
+ return question.options ?? [];
1126
+ }
1127
+ __name(questionOptions, "questionOptions");
1128
+ function isTypedQuestion(question) {
1129
+ return question.input !== void 0 && question.input.type !== "select";
1130
+ }
1131
+ __name(isTypedQuestion, "isTypedQuestion");
1132
+ var DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
1133
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1134
+ var NUMBER_PATTERN = /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i;
1135
+ function isDate(value) {
1136
+ const match = DATE_PATTERN.exec(value);
1137
+ if (match === null) {
1138
+ return false;
1139
+ }
1140
+ const [, year, month, day] = match;
1141
+ const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
1142
+ return date.getUTCFullYear() === Number(year) && date.getUTCMonth() === Number(month) - 1 && date.getUTCDate() === Number(day);
1143
+ }
1144
+ __name(isDate, "isDate");
1145
+ function isHttpUrl(value) {
1146
+ try {
1147
+ const url = new URL(value);
1148
+ return url.protocol === "http:" || url.protocol === "https:";
1149
+ } catch {
1150
+ return false;
1151
+ }
1152
+ }
1153
+ __name(isHttpUrl, "isHttpUrl");
1154
+ function fullMatch(pattern, value) {
1155
+ try {
1156
+ return new RegExp(`^(?:${pattern})$`, "u").test(value);
1157
+ } catch {
1158
+ return void 0;
1159
+ }
1160
+ }
1161
+ __name(fullMatch, "fullMatch");
1162
+ function asNumber(bound) {
1163
+ if (typeof bound === "number" && Number.isFinite(bound)) {
1164
+ return bound;
1165
+ }
1166
+ if (typeof bound === "string" && NUMBER_PATTERN.test(bound.trim())) {
1167
+ return Number(bound);
1168
+ }
1169
+ return void 0;
1170
+ }
1171
+ __name(asNumber, "asNumber");
1172
+ function validateElicitationValue(question, value) {
1173
+ const input = question.input;
1174
+ if (input === void 0 || input.type === "select") {
1175
+ if (question.allowFreeText === true) {
1176
+ return null;
1177
+ }
1178
+ return questionOptions(question).some((option) => option.value === value) ? null : "must be one of the offered options";
1179
+ }
1180
+ switch (input.type) {
1181
+ case "number": {
1182
+ if (!NUMBER_PATTERN.test(value.trim()) || !Number.isFinite(Number(value))) {
1183
+ return "must be a number";
1184
+ }
1185
+ const number = Number(value);
1186
+ const min = asNumber(input.min);
1187
+ const max = asNumber(input.max);
1188
+ if (min !== void 0 && number < min) {
1189
+ return `must be at least ${min}`;
1190
+ }
1191
+ if (max !== void 0 && number > max) {
1192
+ return `must be at most ${max}`;
1193
+ }
1194
+ return null;
1195
+ }
1196
+ case "boolean":
1197
+ return value === "true" || value === "false" ? null : 'must be "true" or "false"';
1198
+ case "date": {
1199
+ if (!isDate(value)) {
1200
+ return "must be a date as YYYY-MM-DD";
1201
+ }
1202
+ if (typeof input.min === "string" && isDate(input.min) && value < input.min) {
1203
+ return `must be on or after ${input.min}`;
1204
+ }
1205
+ if (typeof input.max === "string" && isDate(input.max) && value > input.max) {
1206
+ return `must be on or before ${input.max}`;
1207
+ }
1208
+ return null;
1209
+ }
1210
+ case "email":
1211
+ if (!EMAIL_PATTERN.test(value)) {
1212
+ return "must be an email address";
1213
+ }
1214
+ break;
1215
+ case "url":
1216
+ if (!isHttpUrl(value)) {
1217
+ return "must be an http(s) URL";
1218
+ }
1219
+ break;
1220
+ case "text":
1221
+ case "textarea": {
1222
+ const min = asNumber(input.min);
1223
+ const max = asNumber(input.max);
1224
+ if (min !== void 0 && value.length < min) {
1225
+ return `must be at least ${min} characters`;
1226
+ }
1227
+ if (max !== void 0 && value.length > max) {
1228
+ return `must be at most ${max} characters`;
1229
+ }
1230
+ break;
1231
+ }
1232
+ }
1233
+ if (input.pattern !== void 0 && fullMatch(input.pattern, value) === false) {
1234
+ return "does not match the expected format";
1235
+ }
1236
+ return null;
1237
+ }
1238
+ __name(validateElicitationValue, "validateElicitationValue");
1239
+ function validateElicitationAnswer(question, values) {
1240
+ const present = values.filter((value) => value !== "");
1241
+ if (present.length === 0) {
1242
+ return question.input?.required === true ? "requires an answer" : null;
1243
+ }
1244
+ if (question.multiple !== true && present.length > 1) {
1245
+ return "takes a single value";
1246
+ }
1247
+ for (const value of present) {
1248
+ const problem = validateElicitationValue(question, value);
1249
+ if (problem !== null) {
1250
+ return problem;
1251
+ }
1252
+ }
1253
+ return null;
1254
+ }
1255
+ __name(validateElicitationAnswer, "validateElicitationAnswer");
1256
+ function readElicitationInput(raw) {
1257
+ if (typeof raw !== "object" || raw === null) {
1258
+ return void 0;
1259
+ }
1260
+ const candidate = raw;
1261
+ const type = candidate.type;
1262
+ if (typeof type !== "string" || !ELICITATION_INPUT_TYPES.includes(type)) {
1263
+ return void 0;
1264
+ }
1265
+ const bound = /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isFinite(value) || typeof value === "string" ? value : void 0, "bound");
1266
+ const min = bound(candidate.min);
1267
+ const max = bound(candidate.max);
1268
+ return {
1269
+ type,
1270
+ ...typeof candidate.placeholder === "string" ? {
1271
+ placeholder: candidate.placeholder
1272
+ } : {},
1273
+ ...candidate.required === true ? {
1274
+ required: true
1275
+ } : {},
1276
+ ...min !== void 0 ? {
1277
+ min
1278
+ } : {},
1279
+ ...max !== void 0 ? {
1280
+ max
1281
+ } : {},
1282
+ ...typeof candidate.pattern === "string" ? {
1283
+ pattern: candidate.pattern
1284
+ } : {}
1285
+ };
1286
+ }
1287
+ __name(readElicitationInput, "readElicitationInput");
1288
+ function readElicitationQuestions(input) {
1289
+ if (typeof input !== "object" || input === null) {
1290
+ return [];
1291
+ }
1292
+ const raw = input.questions;
1293
+ if (!Array.isArray(raw)) {
1294
+ return [];
1295
+ }
1296
+ const questions = [];
1297
+ for (const item of raw) {
1298
+ if (typeof item !== "object" || item === null) {
1299
+ continue;
1300
+ }
1301
+ const candidate = item;
1302
+ if (typeof candidate.id !== "string" || typeof candidate.prompt !== "string") {
1303
+ continue;
1304
+ }
1305
+ const options = Array.isArray(candidate.options) ? candidate.options.flatMap((option) => {
1306
+ if (typeof option !== "object" || option === null) {
1307
+ return [];
1308
+ }
1309
+ const { value, label } = option;
1310
+ return typeof value === "string" && typeof label === "string" ? [
1311
+ {
1312
+ value,
1313
+ label
1314
+ }
1315
+ ] : [];
1316
+ }) : [];
1317
+ const typed = readElicitationInput(candidate.input);
1318
+ const defaults = Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [];
1319
+ questions.push({
1320
+ id: candidate.id,
1321
+ prompt: candidate.prompt,
1322
+ options,
1323
+ ...typed !== void 0 ? {
1324
+ input: typed
1325
+ } : {},
1326
+ ...defaults.length > 0 ? {
1327
+ defaults
1328
+ } : {},
1329
+ ...candidate.multiple === true ? {
1330
+ multiple: true
1331
+ } : {},
1332
+ ...candidate.allowFreeText === true ? {
1333
+ allowFreeText: true
1334
+ } : {}
1335
+ });
1336
+ }
1337
+ return questions;
1016
1338
  }
1017
- __name(optionValues, "optionValues");
1339
+ __name(readElicitationQuestions, "readElicitationQuestions");
1340
+
1341
+ // src/elicitation.ts
1018
1342
  function normalizeElicitationReply(reply) {
1019
1343
  if (typeof reply !== "object" || reply === null) {
1020
1344
  return {
@@ -1050,8 +1374,7 @@ function resolveElicitation(request, raw) {
1050
1374
  defaulted.push(question.id);
1051
1375
  continue;
1052
1376
  }
1053
- const allowed = optionValues(question);
1054
- const valid = question.allowFreeText === true ? submitted : submitted.filter((value) => allowed.has(value));
1377
+ const valid = submitted.filter((value) => value !== "" && validateElicitationValue(question, value) === null);
1055
1378
  answers[question.id] = question.multiple === true ? valid : valid.slice(0, 1);
1056
1379
  }
1057
1380
  return {
@@ -1072,7 +1395,7 @@ __name(settleElicitation, "settleElicitation");
1072
1395
  function renderElicitationAnswers(request, outcome) {
1073
1396
  const lines = request.questions.map((question) => {
1074
1397
  const chosen = outcome.answers[question.id] ?? [];
1075
- const labels = chosen.map((value) => question.options.find((option) => option.value === value)?.label ?? value);
1398
+ const labels = chosen.map((value) => questionOptions(question).find((option) => option.value === value)?.label ?? value);
1076
1399
  return `${question.prompt} \u2192 ${labels.length > 0 ? labels.join(", ") : "(no answer)"}`;
1077
1400
  });
1078
1401
  const preface = outcome.skipped ? "The user declined to answer and asked you to proceed on these assumptions:" : "The user answered:";
@@ -1138,7 +1461,23 @@ function parseQuestion(raw, path, issues) {
1138
1461
  ], "must be a non-empty string"));
1139
1462
  return void 0;
1140
1463
  }
1141
- if (!Array.isArray(candidate.options) || candidate.options.length === 0) {
1464
+ let input;
1465
+ if (candidate.input !== void 0) {
1466
+ input = readElicitationInput(candidate.input);
1467
+ if (input === void 0) {
1468
+ issues.push(issue([
1469
+ ...path,
1470
+ "input",
1471
+ "type"
1472
+ ], `must be one of ${ELICITATION_INPUT_TYPES.join(", ")}`));
1473
+ return void 0;
1474
+ }
1475
+ }
1476
+ const described = typeof candidate.description === "string" && candidate.description.length > 0 ? {
1477
+ description: candidate.description
1478
+ } : {};
1479
+ const typed = input !== void 0 && input.type !== "select";
1480
+ if (!typed && (!Array.isArray(candidate.options) || candidate.options.length === 0)) {
1142
1481
  issues.push(issue([
1143
1482
  ...path,
1144
1483
  "options"
@@ -1146,7 +1485,7 @@ function parseQuestion(raw, path, issues) {
1146
1485
  return void 0;
1147
1486
  }
1148
1487
  const options = [];
1149
- for (const [index, rawOption] of candidate.options.entries()) {
1488
+ for (const [index, rawOption] of (Array.isArray(candidate.options) ? candidate.options : []).entries()) {
1150
1489
  const option = parseOption(rawOption, [
1151
1490
  ...path,
1152
1491
  "options",
@@ -1156,6 +1495,33 @@ function parseQuestion(raw, path, issues) {
1156
1495
  options.push(option);
1157
1496
  }
1158
1497
  }
1498
+ if (typed) {
1499
+ const question = {
1500
+ id: candidate.id,
1501
+ prompt: candidate.prompt,
1502
+ ...described,
1503
+ ...options.length > 0 ? {
1504
+ options
1505
+ } : {},
1506
+ input,
1507
+ ...candidate.multiple === true ? {
1508
+ multiple: true
1509
+ } : {}
1510
+ };
1511
+ const offered2 = Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [];
1512
+ const defaults2 = offered2.filter((value) => validateElicitationValue(question, value) === null);
1513
+ if (offered2.length > 0 && defaults2.length === 0) {
1514
+ issues.push(issue([
1515
+ ...path,
1516
+ "defaults"
1517
+ ], `must be valid ${input.type} values`));
1518
+ return void 0;
1519
+ }
1520
+ return defaults2.length > 0 ? {
1521
+ ...question,
1522
+ defaults: defaults2
1523
+ } : question;
1524
+ }
1159
1525
  if (!Array.isArray(candidate.defaults) || candidate.defaults.length === 0) {
1160
1526
  issues.push(issue([
1161
1527
  ...path,
@@ -1175,7 +1541,11 @@ function parseQuestion(raw, path, issues) {
1175
1541
  return {
1176
1542
  id: candidate.id,
1177
1543
  prompt: candidate.prompt,
1544
+ ...described,
1178
1545
  options,
1546
+ ...input !== void 0 ? {
1547
+ input
1548
+ } : {},
1179
1549
  defaults,
1180
1550
  ...candidate.multiple === true ? {
1181
1551
  multiple: true
@@ -1203,9 +1573,7 @@ var ASK_JSON_SCHEMA = {
1203
1573
  additionalProperties: false,
1204
1574
  required: [
1205
1575
  "id",
1206
- "prompt",
1207
- "options",
1208
- "defaults"
1576
+ "prompt"
1209
1577
  ],
1210
1578
  properties: {
1211
1579
  id: {
@@ -1215,6 +1583,50 @@ var ASK_JSON_SCHEMA = {
1215
1583
  prompt: {
1216
1584
  type: "string"
1217
1585
  },
1586
+ description: {
1587
+ type: "string",
1588
+ description: "A line of help under the prompt."
1589
+ },
1590
+ input: {
1591
+ type: "object",
1592
+ additionalProperties: false,
1593
+ required: [
1594
+ "type"
1595
+ ],
1596
+ 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.',
1597
+ properties: {
1598
+ type: {
1599
+ type: "string",
1600
+ enum: [
1601
+ ...ELICITATION_INPUT_TYPES
1602
+ ]
1603
+ },
1604
+ placeholder: {
1605
+ type: "string"
1606
+ },
1607
+ required: {
1608
+ type: "boolean"
1609
+ },
1610
+ min: {
1611
+ type: [
1612
+ "number",
1613
+ "string"
1614
+ ],
1615
+ description: "number: minimum; text: minimum length; date: earliest YYYY-MM-DD."
1616
+ },
1617
+ max: {
1618
+ type: [
1619
+ "number",
1620
+ "string"
1621
+ ],
1622
+ description: "number: maximum; text: maximum length; date: latest YYYY-MM-DD."
1623
+ },
1624
+ pattern: {
1625
+ type: "string",
1626
+ description: "A regex the whole value must match."
1627
+ }
1628
+ }
1629
+ },
1218
1630
  multiple: {
1219
1631
  type: "boolean",
1220
1632
  description: "Allow more than one option to be chosen."
@@ -1229,7 +1641,7 @@ var ASK_JSON_SCHEMA = {
1229
1641
  items: {
1230
1642
  type: "string"
1231
1643
  },
1232
- description: "REQUIRED. The option values you would pick yourself, so the user can confirm rather than decide."
1644
+ 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."
1233
1645
  },
1234
1646
  options: {
1235
1647
  type: "array",
@@ -3038,6 +3450,11 @@ async function claimToolCall(turn, call) {
3038
3450
  ...spec?.detached === true ? {
3039
3451
  detached: true
3040
3452
  } : {},
3453
+ // Same for `terminal`: absent unless declared, so a deployment with no terminal tool writes
3454
+ // the bytes it always has, and a replay reads the branch back instead of re-deciding it.
3455
+ ...spec?.terminal === true ? {
3456
+ terminal: true
3457
+ } : {},
3041
3458
  ...approval !== void 0 ? {
3042
3459
  approval
3043
3460
  } : {}
@@ -3059,6 +3476,9 @@ async function claimToolCall(turn, call) {
3059
3476
  ...persisted?.detached === true ? {
3060
3477
  detached: true
3061
3478
  } : {},
3479
+ ...persisted?.terminal === true ? {
3480
+ terminal: true
3481
+ } : {},
3062
3482
  ...toolType === "action" && persisted?.approval !== void 0 ? {
3063
3483
  approval: persisted.approval
3064
3484
  } : {},
@@ -3225,7 +3645,7 @@ async function invokeClaimedTool(turn, claimed) {
3225
3645
  if (claimed.toolType === "memory") {
3226
3646
  return await rememberIntoTurn(turn, claimed, startedAt);
3227
3647
  }
3228
- let output;
3648
+ let raw;
3229
3649
  if (hooks.dispatchTool) {
3230
3650
  const stepCtx = {
3231
3651
  actor: input.actor,
@@ -3248,28 +3668,47 @@ async function invokeClaimedTool(turn, claimed) {
3248
3668
  } : {},
3249
3669
  // Numeric-only: the handler applies withToolTimeout AND its own local `classify` — see
3250
3670
  // ToolStepEnvelope.transientRetry.
3251
- transientRetry: resolveToolTransientRetryNumbers(deps.toolTransientRetry)
3671
+ transientRetry: resolveToolTransientRetryNumbers(deps.toolTransientRetry),
3672
+ // The handler hands back what the tool pushed through `ctx.emitUi` with its output.
3673
+ collectUi: true
3252
3674
  };
3253
- output = await hooks.dispatchTool(resolvedCall, envelope);
3675
+ raw = await hooks.dispatchTool(resolvedCall, envelope);
3254
3676
  } else {
3255
- const invocation = hooks.step(`tool:${call.id}`, () => traceToolExecution(hooks.runId, {
3256
- toolCallId: call.id,
3257
- toolName: call.name,
3258
- toolType
3259
- }, () => invokeWithTransientRetry(() => deps.registry.invoke(call.name, call.input, ctx, deps.rolesPolicy), deps.toolTransientRetry ?? {}, {
3260
- ...hooks.isControlFlowError !== void 0 ? {
3261
- isControlFlowError: hooks.isControlFlowError
3262
- } : {},
3263
- onRetry: /* @__PURE__ */ __name((attempt, retryError) => {
3264
- publishAgentToolRetry({
3265
- toolName: call.name,
3266
- toolCallId: call.id,
3267
- attempt,
3268
- message: retryError instanceof Error ? retryError.message : String(retryError)
3269
- });
3270
- }, "onRetry")
3271
- })));
3272
- output = deps.toolTimeoutMs !== void 0 ? await withToolTimeout(invocation, deps.toolTimeoutMs, call.name) : await invocation;
3677
+ const invocation = hooks.step(`tool:${call.id}`, async () => {
3678
+ const ui2 = createUiCollector(call.id, (event) => turn.writer.write(encodeStreamEvent(event)));
3679
+ const output2 = await traceToolExecution(hooks.runId, {
3680
+ toolCallId: call.id,
3681
+ toolName: call.name,
3682
+ toolType
3683
+ }, () => invokeWithTransientRetry(() => {
3684
+ ui2.restart();
3685
+ return deps.registry.invoke(call.name, call.input, {
3686
+ ...ctx,
3687
+ emitUi: ui2.emit
3688
+ }, deps.rolesPolicy);
3689
+ }, deps.toolTransientRetry ?? {}, {
3690
+ ...hooks.isControlFlowError !== void 0 ? {
3691
+ isControlFlowError: hooks.isControlFlowError
3692
+ } : {},
3693
+ onRetry: /* @__PURE__ */ __name((attempt, retryError) => {
3694
+ publishAgentToolRetry({
3695
+ toolName: call.name,
3696
+ toolCallId: call.id,
3697
+ attempt,
3698
+ message: retryError instanceof Error ? retryError.message : String(retryError)
3699
+ });
3700
+ }, "onRetry")
3701
+ }));
3702
+ return wrapToolStepOutput(output2, ui2.components());
3703
+ });
3704
+ raw = deps.toolTimeoutMs !== void 0 ? await withToolTimeout(invocation, deps.toolTimeoutMs, call.name) : await invocation;
3705
+ }
3706
+ const { output, ui } = unwrapToolStepOutput(raw);
3707
+ if (ui.length > 0) {
3708
+ turn.toolUi.set(call.id, ui);
3709
+ }
3710
+ if (claimed.terminal === true) {
3711
+ turn.halt.terminal = true;
3273
3712
  }
3274
3713
  return {
3275
3714
  status: "executed",
@@ -4167,6 +4606,10 @@ ${block}`;
4167
4606
  hooks,
4168
4607
  messageId: assistant.id,
4169
4608
  writer,
4609
+ toolUi: /* @__PURE__ */ new Map(),
4610
+ halt: {
4611
+ terminal: false
4612
+ },
4170
4613
  ...skillOffer !== void 0 ? {
4171
4614
  skills: skillOffer
4172
4615
  } : {},
@@ -4199,6 +4642,10 @@ ${block}`;
4199
4642
  assistantMessage.toolResults = settledResults;
4200
4643
  await hooks.step(`stream:tool-outputs:${i}`, async () => {
4201
4644
  await deps.store.setMessageToolResults(assistant.id, settledResults);
4645
+ const toolUi = toolCallsWithKind.flatMap((call) => turnCalls.toolUi.get(call.id) ?? []);
4646
+ if (toolUi.length > 0) {
4647
+ await deps.store.setMessageUi?.(assistant.id, mergeUi(turn.ui, toolUi));
4648
+ }
4202
4649
  for (const result of results) {
4203
4650
  await writer.write(encodeStreamEvent(outputFrame(result)));
4204
4651
  }
@@ -4213,6 +4660,9 @@ ${block}`;
4213
4660
  } : {}
4214
4661
  }));
4215
4662
  });
4663
+ if (turnCalls.halt.terminal) {
4664
+ break;
4665
+ }
4216
4666
  }
4217
4667
  if (history.title === "" || history.title === "New chat") {
4218
4668
  await hooks.step("persist:title", () => deps.store.setTitle(input.threadId, deriveTitle(input.userText)));
@@ -4309,6 +4759,7 @@ __name(runAgentLoop, "runAgentLoop");
4309
4759
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
4310
4760
  DefaultApprovalPolicy,
4311
4761
  DefaultRolesPolicy,
4762
+ ELICITATION_INPUT_TYPES,
4312
4763
  GLOBAL_SCOPE,
4313
4764
  MAX_ASK_QUESTIONS,
4314
4765
  OutputRejectedError,
@@ -4342,6 +4793,7 @@ __name(runAgentLoop, "runAgentLoop");
4342
4793
  compositeSkillProvider,
4343
4794
  createFrameBuffer,
4344
4795
  createIncrementalGate,
4796
+ createUiCollector,
4345
4797
  dayBoundsUtc,
4346
4798
  decodeStreamEvent,
4347
4799
  defaultCanDecide,
@@ -4364,10 +4816,12 @@ __name(runAgentLoop, "runAgentLoop");
4364
4816
  isReplayIntegrityError,
4365
4817
  isToolEnabled,
4366
4818
  isTransientToolError,
4819
+ isTypedQuestion,
4367
4820
  loadSkill,
4368
4821
  mayDecideApproval,
4369
4822
  memoryForgetVerdict,
4370
4823
  memoryWriteVerdict,
4824
+ mergeUi,
4371
4825
  normalizeDelegation,
4372
4826
  normalizeElicitationReply,
4373
4827
  observeTurnFrames,
@@ -4385,6 +4839,9 @@ __name(runAgentLoop, "runAgentLoop");
4385
4839
  publishAgentSkillsResolved,
4386
4840
  publishAgentToolCall,
4387
4841
  publishAgentToolRetry,
4842
+ questionOptions,
4843
+ readElicitationInput,
4844
+ readElicitationQuestions,
4388
4845
  releaseGatedFrames,
4389
4846
  rememberInputSchema,
4390
4847
  rememberToolDefinition,
@@ -4415,6 +4872,9 @@ __name(runAgentLoop, "runAgentLoop");
4415
4872
  traceLlmTurn,
4416
4873
  traceToolExecution,
4417
4874
  truncateDetailContent,
4875
+ unwrapToolStepOutput,
4876
+ validateElicitationAnswer,
4877
+ validateElicitationValue,
4418
4878
  validateStructured,
4419
4879
  windowHistory,
4420
4880
  withAskTool,
@@ -4422,6 +4882,7 @@ __name(runAgentLoop, "runAgentLoop");
4422
4882
  withSkillTool,
4423
4883
  withToolTimeout,
4424
4884
  withTurnFrames,
4885
+ wrapToolStepOutput,
4425
4886
  writeMemory
4426
4887
  });
4427
4888
  //# sourceMappingURL=index.cjs.map