@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.js CHANGED
@@ -157,6 +157,95 @@ function withTurnFrames(result, frames) {
157
157
  }
158
158
  __name(withTurnFrames, "withTurnFrames");
159
159
 
160
+ // src/tool-ui.ts
161
+ var TOOL_STEP_UI = "@@nestjs-agent/tool-step-ui";
162
+ function wrapToolStepOutput(output, ui) {
163
+ if (ui.length === 0) {
164
+ return output;
165
+ }
166
+ const wrapped = {
167
+ [TOOL_STEP_UI]: 1,
168
+ output,
169
+ ui: [
170
+ ...ui
171
+ ]
172
+ };
173
+ return wrapped;
174
+ }
175
+ __name(wrapToolStepOutput, "wrapToolStepOutput");
176
+ function unwrapToolStepOutput(raw) {
177
+ if (typeof raw === "object" && raw !== null && raw[TOOL_STEP_UI] === 1) {
178
+ const wrapped = raw;
179
+ return {
180
+ output: wrapped.output,
181
+ ui: Array.isArray(wrapped.ui) ? wrapped.ui : []
182
+ };
183
+ }
184
+ return {
185
+ output: raw,
186
+ ui: []
187
+ };
188
+ }
189
+ __name(unwrapToolStepOutput, "unwrapToolStepOutput");
190
+ function createUiCollector(toolCallId, write) {
191
+ const pushed = /* @__PURE__ */ new Map();
192
+ let next = 0;
193
+ const emit2 = /* @__PURE__ */ __name(async (component, props, options = {}) => {
194
+ if (typeof component !== "string" || component.length === 0) {
195
+ throw new Error("emitUi: component must be a non-empty string");
196
+ }
197
+ if (typeof props !== "object" || props === null || Array.isArray(props)) {
198
+ throw new Error("emitUi: props must be a JSON object");
199
+ }
200
+ let id = options.id;
201
+ if (id === void 0) {
202
+ id = `${toolCallId}:ui:${next}`;
203
+ next += 1;
204
+ }
205
+ const entry = {
206
+ id,
207
+ component,
208
+ // Snapshot: the frame and the persisted value are what the tool pushed at THIS moment, not
209
+ // whatever the object it handed over looks like when the step settles.
210
+ props: JSON.parse(JSON.stringify(props)),
211
+ ...options.version !== void 0 ? {
212
+ version: options.version
213
+ } : {},
214
+ toolCallId
215
+ };
216
+ pushed.set(id, entry);
217
+ await write?.({
218
+ kind: "ui",
219
+ ...entry
220
+ });
221
+ return {
222
+ id
223
+ };
224
+ }, "emit");
225
+ return {
226
+ emit: emit2,
227
+ components: /* @__PURE__ */ __name(() => [
228
+ ...pushed.values()
229
+ ], "components"),
230
+ restart: /* @__PURE__ */ __name(() => {
231
+ next = 0;
232
+ }, "restart")
233
+ };
234
+ }
235
+ __name(createUiCollector, "createUiCollector");
236
+ function mergeUi(...lists) {
237
+ const merged = /* @__PURE__ */ new Map();
238
+ for (const list of lists) {
239
+ for (const component of list ?? []) {
240
+ merged.set(component.id, component);
241
+ }
242
+ }
243
+ return [
244
+ ...merged.values()
245
+ ];
246
+ }
247
+ __name(mergeUi, "mergeUi");
248
+
160
249
  // src/spi/pricing-store.ts
161
250
  async function seedModelPrices(store, prices) {
162
251
  for (const price of prices) {
@@ -831,11 +920,235 @@ ${detail}`;
831
920
  }
832
921
  __name(repairInstruction, "repairInstruction");
833
922
 
834
- // src/elicitation.ts
835
- function optionValues(question) {
836
- return new Set(question.options.map((option) => option.value));
923
+ // src/elicitation-input.ts
924
+ var ELICITATION_INPUT_TYPES = [
925
+ "text",
926
+ "textarea",
927
+ "number",
928
+ "boolean",
929
+ "date",
930
+ "email",
931
+ "url",
932
+ "select"
933
+ ];
934
+ function questionOptions(question) {
935
+ return question.options ?? [];
936
+ }
937
+ __name(questionOptions, "questionOptions");
938
+ function isTypedQuestion(question) {
939
+ return question.input !== void 0 && question.input.type !== "select";
940
+ }
941
+ __name(isTypedQuestion, "isTypedQuestion");
942
+ var DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
943
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
944
+ var NUMBER_PATTERN = /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i;
945
+ function isDate(value) {
946
+ const match = DATE_PATTERN.exec(value);
947
+ if (match === null) {
948
+ return false;
949
+ }
950
+ const [, year, month, day] = match;
951
+ const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
952
+ return date.getUTCFullYear() === Number(year) && date.getUTCMonth() === Number(month) - 1 && date.getUTCDate() === Number(day);
953
+ }
954
+ __name(isDate, "isDate");
955
+ function isHttpUrl(value) {
956
+ try {
957
+ const url = new URL(value);
958
+ return url.protocol === "http:" || url.protocol === "https:";
959
+ } catch {
960
+ return false;
961
+ }
962
+ }
963
+ __name(isHttpUrl, "isHttpUrl");
964
+ function fullMatch(pattern, value) {
965
+ try {
966
+ return new RegExp(`^(?:${pattern})$`, "u").test(value);
967
+ } catch {
968
+ return void 0;
969
+ }
970
+ }
971
+ __name(fullMatch, "fullMatch");
972
+ function asNumber(bound) {
973
+ if (typeof bound === "number" && Number.isFinite(bound)) {
974
+ return bound;
975
+ }
976
+ if (typeof bound === "string" && NUMBER_PATTERN.test(bound.trim())) {
977
+ return Number(bound);
978
+ }
979
+ return void 0;
980
+ }
981
+ __name(asNumber, "asNumber");
982
+ function validateElicitationValue(question, value) {
983
+ const input = question.input;
984
+ if (input === void 0 || input.type === "select") {
985
+ if (question.allowFreeText === true) {
986
+ return null;
987
+ }
988
+ return questionOptions(question).some((option) => option.value === value) ? null : "must be one of the offered options";
989
+ }
990
+ switch (input.type) {
991
+ case "number": {
992
+ if (!NUMBER_PATTERN.test(value.trim()) || !Number.isFinite(Number(value))) {
993
+ return "must be a number";
994
+ }
995
+ const number = Number(value);
996
+ const min = asNumber(input.min);
997
+ const max = asNumber(input.max);
998
+ if (min !== void 0 && number < min) {
999
+ return `must be at least ${min}`;
1000
+ }
1001
+ if (max !== void 0 && number > max) {
1002
+ return `must be at most ${max}`;
1003
+ }
1004
+ return null;
1005
+ }
1006
+ case "boolean":
1007
+ return value === "true" || value === "false" ? null : 'must be "true" or "false"';
1008
+ case "date": {
1009
+ if (!isDate(value)) {
1010
+ return "must be a date as YYYY-MM-DD";
1011
+ }
1012
+ if (typeof input.min === "string" && isDate(input.min) && value < input.min) {
1013
+ return `must be on or after ${input.min}`;
1014
+ }
1015
+ if (typeof input.max === "string" && isDate(input.max) && value > input.max) {
1016
+ return `must be on or before ${input.max}`;
1017
+ }
1018
+ return null;
1019
+ }
1020
+ case "email":
1021
+ if (!EMAIL_PATTERN.test(value)) {
1022
+ return "must be an email address";
1023
+ }
1024
+ break;
1025
+ case "url":
1026
+ if (!isHttpUrl(value)) {
1027
+ return "must be an http(s) URL";
1028
+ }
1029
+ break;
1030
+ case "text":
1031
+ case "textarea": {
1032
+ const min = asNumber(input.min);
1033
+ const max = asNumber(input.max);
1034
+ if (min !== void 0 && value.length < min) {
1035
+ return `must be at least ${min} characters`;
1036
+ }
1037
+ if (max !== void 0 && value.length > max) {
1038
+ return `must be at most ${max} characters`;
1039
+ }
1040
+ break;
1041
+ }
1042
+ }
1043
+ if (input.pattern !== void 0 && fullMatch(input.pattern, value) === false) {
1044
+ return "does not match the expected format";
1045
+ }
1046
+ return null;
1047
+ }
1048
+ __name(validateElicitationValue, "validateElicitationValue");
1049
+ function validateElicitationAnswer(question, values) {
1050
+ const present = values.filter((value) => value !== "");
1051
+ if (present.length === 0) {
1052
+ return question.input?.required === true ? "requires an answer" : null;
1053
+ }
1054
+ if (question.multiple !== true && present.length > 1) {
1055
+ return "takes a single value";
1056
+ }
1057
+ for (const value of present) {
1058
+ const problem = validateElicitationValue(question, value);
1059
+ if (problem !== null) {
1060
+ return problem;
1061
+ }
1062
+ }
1063
+ return null;
1064
+ }
1065
+ __name(validateElicitationAnswer, "validateElicitationAnswer");
1066
+ function readElicitationInput(raw) {
1067
+ if (typeof raw !== "object" || raw === null) {
1068
+ return void 0;
1069
+ }
1070
+ const candidate = raw;
1071
+ const type = candidate.type;
1072
+ if (typeof type !== "string" || !ELICITATION_INPUT_TYPES.includes(type)) {
1073
+ return void 0;
1074
+ }
1075
+ const bound = /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isFinite(value) || typeof value === "string" ? value : void 0, "bound");
1076
+ const min = bound(candidate.min);
1077
+ const max = bound(candidate.max);
1078
+ return {
1079
+ type,
1080
+ ...typeof candidate.placeholder === "string" ? {
1081
+ placeholder: candidate.placeholder
1082
+ } : {},
1083
+ ...candidate.required === true ? {
1084
+ required: true
1085
+ } : {},
1086
+ ...min !== void 0 ? {
1087
+ min
1088
+ } : {},
1089
+ ...max !== void 0 ? {
1090
+ max
1091
+ } : {},
1092
+ ...typeof candidate.pattern === "string" ? {
1093
+ pattern: candidate.pattern
1094
+ } : {}
1095
+ };
1096
+ }
1097
+ __name(readElicitationInput, "readElicitationInput");
1098
+ function readElicitationQuestions(input) {
1099
+ if (typeof input !== "object" || input === null) {
1100
+ return [];
1101
+ }
1102
+ const raw = input.questions;
1103
+ if (!Array.isArray(raw)) {
1104
+ return [];
1105
+ }
1106
+ const questions = [];
1107
+ for (const item of raw) {
1108
+ if (typeof item !== "object" || item === null) {
1109
+ continue;
1110
+ }
1111
+ const candidate = item;
1112
+ if (typeof candidate.id !== "string" || typeof candidate.prompt !== "string") {
1113
+ continue;
1114
+ }
1115
+ const options = Array.isArray(candidate.options) ? candidate.options.flatMap((option) => {
1116
+ if (typeof option !== "object" || option === null) {
1117
+ return [];
1118
+ }
1119
+ const { value, label } = option;
1120
+ return typeof value === "string" && typeof label === "string" ? [
1121
+ {
1122
+ value,
1123
+ label
1124
+ }
1125
+ ] : [];
1126
+ }) : [];
1127
+ const typed = readElicitationInput(candidate.input);
1128
+ const defaults = Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [];
1129
+ questions.push({
1130
+ id: candidate.id,
1131
+ prompt: candidate.prompt,
1132
+ options,
1133
+ ...typed !== void 0 ? {
1134
+ input: typed
1135
+ } : {},
1136
+ ...defaults.length > 0 ? {
1137
+ defaults
1138
+ } : {},
1139
+ ...candidate.multiple === true ? {
1140
+ multiple: true
1141
+ } : {},
1142
+ ...candidate.allowFreeText === true ? {
1143
+ allowFreeText: true
1144
+ } : {}
1145
+ });
1146
+ }
1147
+ return questions;
837
1148
  }
838
- __name(optionValues, "optionValues");
1149
+ __name(readElicitationQuestions, "readElicitationQuestions");
1150
+
1151
+ // src/elicitation.ts
839
1152
  function normalizeElicitationReply(reply) {
840
1153
  if (typeof reply !== "object" || reply === null) {
841
1154
  return {
@@ -871,8 +1184,7 @@ function resolveElicitation(request, raw) {
871
1184
  defaulted.push(question.id);
872
1185
  continue;
873
1186
  }
874
- const allowed = optionValues(question);
875
- const valid = question.allowFreeText === true ? submitted : submitted.filter((value) => allowed.has(value));
1187
+ const valid = submitted.filter((value) => value !== "" && validateElicitationValue(question, value) === null);
876
1188
  answers[question.id] = question.multiple === true ? valid : valid.slice(0, 1);
877
1189
  }
878
1190
  return {
@@ -893,7 +1205,7 @@ __name(settleElicitation, "settleElicitation");
893
1205
  function renderElicitationAnswers(request, outcome) {
894
1206
  const lines = request.questions.map((question) => {
895
1207
  const chosen = outcome.answers[question.id] ?? [];
896
- const labels = chosen.map((value) => question.options.find((option) => option.value === value)?.label ?? value);
1208
+ const labels = chosen.map((value) => questionOptions(question).find((option) => option.value === value)?.label ?? value);
897
1209
  return `${question.prompt} \u2192 ${labels.length > 0 ? labels.join(", ") : "(no answer)"}`;
898
1210
  });
899
1211
  const preface = outcome.skipped ? "The user declined to answer and asked you to proceed on these assumptions:" : "The user answered:";
@@ -959,7 +1271,23 @@ function parseQuestion(raw, path, issues) {
959
1271
  ], "must be a non-empty string"));
960
1272
  return void 0;
961
1273
  }
962
- if (!Array.isArray(candidate.options) || candidate.options.length === 0) {
1274
+ let input;
1275
+ if (candidate.input !== void 0) {
1276
+ input = readElicitationInput(candidate.input);
1277
+ if (input === void 0) {
1278
+ issues.push(issue([
1279
+ ...path,
1280
+ "input",
1281
+ "type"
1282
+ ], `must be one of ${ELICITATION_INPUT_TYPES.join(", ")}`));
1283
+ return void 0;
1284
+ }
1285
+ }
1286
+ const described = typeof candidate.description === "string" && candidate.description.length > 0 ? {
1287
+ description: candidate.description
1288
+ } : {};
1289
+ const typed = input !== void 0 && input.type !== "select";
1290
+ if (!typed && (!Array.isArray(candidate.options) || candidate.options.length === 0)) {
963
1291
  issues.push(issue([
964
1292
  ...path,
965
1293
  "options"
@@ -967,7 +1295,7 @@ function parseQuestion(raw, path, issues) {
967
1295
  return void 0;
968
1296
  }
969
1297
  const options = [];
970
- for (const [index, rawOption] of candidate.options.entries()) {
1298
+ for (const [index, rawOption] of (Array.isArray(candidate.options) ? candidate.options : []).entries()) {
971
1299
  const option = parseOption(rawOption, [
972
1300
  ...path,
973
1301
  "options",
@@ -977,6 +1305,33 @@ function parseQuestion(raw, path, issues) {
977
1305
  options.push(option);
978
1306
  }
979
1307
  }
1308
+ if (typed) {
1309
+ const question = {
1310
+ id: candidate.id,
1311
+ prompt: candidate.prompt,
1312
+ ...described,
1313
+ ...options.length > 0 ? {
1314
+ options
1315
+ } : {},
1316
+ input,
1317
+ ...candidate.multiple === true ? {
1318
+ multiple: true
1319
+ } : {}
1320
+ };
1321
+ const offered2 = Array.isArray(candidate.defaults) ? candidate.defaults.filter((value) => typeof value === "string") : [];
1322
+ const defaults2 = offered2.filter((value) => validateElicitationValue(question, value) === null);
1323
+ if (offered2.length > 0 && defaults2.length === 0) {
1324
+ issues.push(issue([
1325
+ ...path,
1326
+ "defaults"
1327
+ ], `must be valid ${input.type} values`));
1328
+ return void 0;
1329
+ }
1330
+ return defaults2.length > 0 ? {
1331
+ ...question,
1332
+ defaults: defaults2
1333
+ } : question;
1334
+ }
980
1335
  if (!Array.isArray(candidate.defaults) || candidate.defaults.length === 0) {
981
1336
  issues.push(issue([
982
1337
  ...path,
@@ -996,7 +1351,11 @@ function parseQuestion(raw, path, issues) {
996
1351
  return {
997
1352
  id: candidate.id,
998
1353
  prompt: candidate.prompt,
1354
+ ...described,
999
1355
  options,
1356
+ ...input !== void 0 ? {
1357
+ input
1358
+ } : {},
1000
1359
  defaults,
1001
1360
  ...candidate.multiple === true ? {
1002
1361
  multiple: true
@@ -1024,9 +1383,7 @@ var ASK_JSON_SCHEMA = {
1024
1383
  additionalProperties: false,
1025
1384
  required: [
1026
1385
  "id",
1027
- "prompt",
1028
- "options",
1029
- "defaults"
1386
+ "prompt"
1030
1387
  ],
1031
1388
  properties: {
1032
1389
  id: {
@@ -1036,6 +1393,50 @@ var ASK_JSON_SCHEMA = {
1036
1393
  prompt: {
1037
1394
  type: "string"
1038
1395
  },
1396
+ description: {
1397
+ type: "string",
1398
+ description: "A line of help under the prompt."
1399
+ },
1400
+ input: {
1401
+ type: "object",
1402
+ additionalProperties: false,
1403
+ required: [
1404
+ "type"
1405
+ ],
1406
+ 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.',
1407
+ properties: {
1408
+ type: {
1409
+ type: "string",
1410
+ enum: [
1411
+ ...ELICITATION_INPUT_TYPES
1412
+ ]
1413
+ },
1414
+ placeholder: {
1415
+ type: "string"
1416
+ },
1417
+ required: {
1418
+ type: "boolean"
1419
+ },
1420
+ min: {
1421
+ type: [
1422
+ "number",
1423
+ "string"
1424
+ ],
1425
+ description: "number: minimum; text: minimum length; date: earliest YYYY-MM-DD."
1426
+ },
1427
+ max: {
1428
+ type: [
1429
+ "number",
1430
+ "string"
1431
+ ],
1432
+ description: "number: maximum; text: maximum length; date: latest YYYY-MM-DD."
1433
+ },
1434
+ pattern: {
1435
+ type: "string",
1436
+ description: "A regex the whole value must match."
1437
+ }
1438
+ }
1439
+ },
1039
1440
  multiple: {
1040
1441
  type: "boolean",
1041
1442
  description: "Allow more than one option to be chosen."
@@ -1050,7 +1451,7 @@ var ASK_JSON_SCHEMA = {
1050
1451
  items: {
1051
1452
  type: "string"
1052
1453
  },
1053
- description: "REQUIRED. The option values you would pick yourself, so the user can confirm rather than decide."
1454
+ 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."
1054
1455
  },
1055
1456
  options: {
1056
1457
  type: "array",
@@ -2859,6 +3260,11 @@ async function claimToolCall(turn, call) {
2859
3260
  ...spec?.detached === true ? {
2860
3261
  detached: true
2861
3262
  } : {},
3263
+ // Same for `terminal`: absent unless declared, so a deployment with no terminal tool writes
3264
+ // the bytes it always has, and a replay reads the branch back instead of re-deciding it.
3265
+ ...spec?.terminal === true ? {
3266
+ terminal: true
3267
+ } : {},
2862
3268
  ...approval !== void 0 ? {
2863
3269
  approval
2864
3270
  } : {}
@@ -2880,6 +3286,9 @@ async function claimToolCall(turn, call) {
2880
3286
  ...persisted?.detached === true ? {
2881
3287
  detached: true
2882
3288
  } : {},
3289
+ ...persisted?.terminal === true ? {
3290
+ terminal: true
3291
+ } : {},
2883
3292
  ...toolType === "action" && persisted?.approval !== void 0 ? {
2884
3293
  approval: persisted.approval
2885
3294
  } : {},
@@ -3046,7 +3455,7 @@ async function invokeClaimedTool(turn, claimed) {
3046
3455
  if (claimed.toolType === "memory") {
3047
3456
  return await rememberIntoTurn(turn, claimed, startedAt);
3048
3457
  }
3049
- let output;
3458
+ let raw;
3050
3459
  if (hooks.dispatchTool) {
3051
3460
  const stepCtx = {
3052
3461
  actor: input.actor,
@@ -3069,28 +3478,47 @@ async function invokeClaimedTool(turn, claimed) {
3069
3478
  } : {},
3070
3479
  // Numeric-only: the handler applies withToolTimeout AND its own local `classify` — see
3071
3480
  // ToolStepEnvelope.transientRetry.
3072
- transientRetry: resolveToolTransientRetryNumbers(deps.toolTransientRetry)
3481
+ transientRetry: resolveToolTransientRetryNumbers(deps.toolTransientRetry),
3482
+ // The handler hands back what the tool pushed through `ctx.emitUi` with its output.
3483
+ collectUi: true
3073
3484
  };
3074
- output = await hooks.dispatchTool(resolvedCall, envelope);
3485
+ raw = await hooks.dispatchTool(resolvedCall, envelope);
3075
3486
  } else {
3076
- const invocation = hooks.step(`tool:${call.id}`, () => traceToolExecution(hooks.runId, {
3077
- toolCallId: call.id,
3078
- toolName: call.name,
3079
- toolType
3080
- }, () => invokeWithTransientRetry(() => deps.registry.invoke(call.name, call.input, ctx, deps.rolesPolicy), deps.toolTransientRetry ?? {}, {
3081
- ...hooks.isControlFlowError !== void 0 ? {
3082
- isControlFlowError: hooks.isControlFlowError
3083
- } : {},
3084
- onRetry: /* @__PURE__ */ __name((attempt, retryError) => {
3085
- publishAgentToolRetry({
3086
- toolName: call.name,
3087
- toolCallId: call.id,
3088
- attempt,
3089
- message: retryError instanceof Error ? retryError.message : String(retryError)
3090
- });
3091
- }, "onRetry")
3092
- })));
3093
- output = deps.toolTimeoutMs !== void 0 ? await withToolTimeout(invocation, deps.toolTimeoutMs, call.name) : await invocation;
3487
+ const invocation = hooks.step(`tool:${call.id}`, async () => {
3488
+ const ui2 = createUiCollector(call.id, (event) => turn.writer.write(encodeStreamEvent(event)));
3489
+ const output2 = await traceToolExecution(hooks.runId, {
3490
+ toolCallId: call.id,
3491
+ toolName: call.name,
3492
+ toolType
3493
+ }, () => invokeWithTransientRetry(() => {
3494
+ ui2.restart();
3495
+ return deps.registry.invoke(call.name, call.input, {
3496
+ ...ctx,
3497
+ emitUi: ui2.emit
3498
+ }, deps.rolesPolicy);
3499
+ }, deps.toolTransientRetry ?? {}, {
3500
+ ...hooks.isControlFlowError !== void 0 ? {
3501
+ isControlFlowError: hooks.isControlFlowError
3502
+ } : {},
3503
+ onRetry: /* @__PURE__ */ __name((attempt, retryError) => {
3504
+ publishAgentToolRetry({
3505
+ toolName: call.name,
3506
+ toolCallId: call.id,
3507
+ attempt,
3508
+ message: retryError instanceof Error ? retryError.message : String(retryError)
3509
+ });
3510
+ }, "onRetry")
3511
+ }));
3512
+ return wrapToolStepOutput(output2, ui2.components());
3513
+ });
3514
+ raw = deps.toolTimeoutMs !== void 0 ? await withToolTimeout(invocation, deps.toolTimeoutMs, call.name) : await invocation;
3515
+ }
3516
+ const { output, ui } = unwrapToolStepOutput(raw);
3517
+ if (ui.length > 0) {
3518
+ turn.toolUi.set(call.id, ui);
3519
+ }
3520
+ if (claimed.terminal === true) {
3521
+ turn.halt.terminal = true;
3094
3522
  }
3095
3523
  return {
3096
3524
  status: "executed",
@@ -3988,6 +4416,10 @@ ${block}`;
3988
4416
  hooks,
3989
4417
  messageId: assistant.id,
3990
4418
  writer,
4419
+ toolUi: /* @__PURE__ */ new Map(),
4420
+ halt: {
4421
+ terminal: false
4422
+ },
3991
4423
  ...skillOffer !== void 0 ? {
3992
4424
  skills: skillOffer
3993
4425
  } : {},
@@ -4020,6 +4452,10 @@ ${block}`;
4020
4452
  assistantMessage.toolResults = settledResults;
4021
4453
  await hooks.step(`stream:tool-outputs:${i}`, async () => {
4022
4454
  await deps.store.setMessageToolResults(assistant.id, settledResults);
4455
+ const toolUi = toolCallsWithKind.flatMap((call) => turnCalls.toolUi.get(call.id) ?? []);
4456
+ if (toolUi.length > 0) {
4457
+ await deps.store.setMessageUi?.(assistant.id, mergeUi(turn.ui, toolUi));
4458
+ }
4023
4459
  for (const result of results) {
4024
4460
  await writer.write(encodeStreamEvent(outputFrame(result)));
4025
4461
  }
@@ -4034,6 +4470,9 @@ ${block}`;
4034
4470
  } : {}
4035
4471
  }));
4036
4472
  });
4473
+ if (turnCalls.halt.terminal) {
4474
+ break;
4475
+ }
4037
4476
  }
4038
4477
  if (history.title === "" || history.title === "New chat") {
4039
4478
  await hooks.step("persist:title", () => deps.store.setTitle(input.threadId, deriveTitle(input.userText)));
@@ -4129,6 +4568,7 @@ export {
4129
4568
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
4130
4569
  DefaultApprovalPolicy,
4131
4570
  DefaultRolesPolicy,
4571
+ ELICITATION_INPUT_TYPES,
4132
4572
  GLOBAL_SCOPE,
4133
4573
  MAX_ASK_QUESTIONS,
4134
4574
  OutputRejectedError,
@@ -4162,6 +4602,7 @@ export {
4162
4602
  compositeSkillProvider,
4163
4603
  createFrameBuffer,
4164
4604
  createIncrementalGate,
4605
+ createUiCollector,
4165
4606
  dayBoundsUtc,
4166
4607
  decodeStreamEvent,
4167
4608
  defaultCanDecide,
@@ -4184,10 +4625,12 @@ export {
4184
4625
  isReplayIntegrityError,
4185
4626
  isToolEnabled,
4186
4627
  isTransientToolError,
4628
+ isTypedQuestion,
4187
4629
  loadSkill,
4188
4630
  mayDecideApproval,
4189
4631
  memoryForgetVerdict,
4190
4632
  memoryWriteVerdict,
4633
+ mergeUi,
4191
4634
  normalizeDelegation,
4192
4635
  normalizeElicitationReply,
4193
4636
  observeTurnFrames,
@@ -4205,6 +4648,9 @@ export {
4205
4648
  publishAgentSkillsResolved,
4206
4649
  publishAgentToolCall,
4207
4650
  publishAgentToolRetry,
4651
+ questionOptions,
4652
+ readElicitationInput,
4653
+ readElicitationQuestions,
4208
4654
  releaseGatedFrames,
4209
4655
  rememberInputSchema,
4210
4656
  rememberToolDefinition,
@@ -4235,6 +4681,9 @@ export {
4235
4681
  traceLlmTurn,
4236
4682
  traceToolExecution,
4237
4683
  truncateDetailContent,
4684
+ unwrapToolStepOutput,
4685
+ validateElicitationAnswer,
4686
+ validateElicitationValue,
4238
4687
  validateStructured,
4239
4688
  windowHistory,
4240
4689
  withAskTool,
@@ -4242,6 +4691,7 @@ export {
4242
4691
  withSkillTool,
4243
4692
  withToolTimeout,
4244
4693
  withTurnFrames,
4694
+ wrapToolStepOutput,
4245
4695
  writeMemory
4246
4696
  };
4247
4697
  //# sourceMappingURL=index.js.map