@dudousxd/nestjs-agent-core 0.20.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/README.md CHANGED
@@ -27,6 +27,7 @@ import type { ModelProvider, AgentStore, ToolSpec, RolesPolicy } from '@dudousxd
27
27
  - `InputProcessor` / `OutputProcessor` — the seams on either side of the model call. Input rewrites `{ system, messages }` before every model call; output returns `pass` / `replace` / `reject` on each step's answer. Both run inside a checkpoint (so a processor may call a model), and they own TRANSFORMATION only — `HistoryPolicy` owns which messages are there to transform. Registering an output processor takes the turn's model call off the run's sink; what that costs the reader depends on what the chain declares (`resolveOutputGateMode`). An undeclared processor holds the whole answer — `createFrameBuffer` + `releaseGatedFrames`. A chain where EVERY processor sets `incremental` gates the growing prefix instead and keeps streaming — `createIncrementalGate` + `gateTail`, with the widest `lookbackChars` in the chain (`resolveGateLookback`, default `DEFAULT_INCREMENTAL_LOOKBACK_CHARS`) held back. The whole-answer pass stays authoritative either way, and `gateTail` raises `ProcessorFailedError` when the settled answer is not an extension of the released prefix. The output chain covers every model-written value that reaches a reader, not only the streamed answer: the `outputSchema` formatting pass is gated before its text is validated (`process:output:structured:<step>:<attempt>`), and each follow-up suggestion is gated on its own (`gateFollowUps`, `process:output:followups:<step>`) — where a refused suggestion is DROPPED rather than failing a run whose answer already passed the same chain. Neither is streamed, so an `incremental` declaration does not apply to them. The folded history summary is deliberately NOT gated here: it never reaches a reader, it re-enters the next prompt as a leading `system` message, and `inputProcessors` — the seam that owns prompt text — already see it on every step.
28
28
  - `outputSchema` (on `AgentLoopDeps`) / `StructuredOutputError` — constrain the final answer to a Standard Schema via one journaled formatting pass after the turn's last model step, with bounded repair. The pass is a translation, so it is shown the question (as the input chain left it) and the gated answer, and nothing else off the transcript — `outputFromTranscript` opts an agent back into the whole turn. `validateStructured` and `extractJson` are the pieces; `ModelTurnArgs.outputSchema` and `ModelTurnResult.object` are how a provider opts into constraining generation itself.
29
29
  - `ElicitationRequest` / `ElicitationReply` / `settleElicitation` — putting a structured question set to the USER and waiting for the answer, from either surface (`AgentLoopDeps.intake`, authored on the agent; `AgentLoopDeps.ask`, the model-callable tool). Both persist as one pending tool-call row and park on the `tool:<runId>:<callId>` signal a HITL approval already uses. `settleElicitation` is pure: it fills every unanswered question from the request's own `defaults` and renders the result by option LABEL, so the same values are reached on every replay without a checkpoint of its own. Because the row is parked as a `pending_approval` action, the reply that comes back may be a `Decision` an operator pressed Approve/Reject on rather than answers — `normalizeElicitationReply` reduces one to the other where the reply is CONSUMED (Approve = confirm every pre-picked default, Reject = skip), so the reduction holds on every path rather than only on a host that implemented no `awaitAnswers`. `askToolDefinition()` is the tool as the model sees it — never registered, so its kind can never be decided by a process-local registry lookup.
30
+ - `ElicitationInput` / `validateElicitationValue` / `validateElicitationAnswer` / `readElicitationQuestions` — **typed questions.** `ElicitationQuestion` gains `description?` and `input?: { type: 'text' | 'textarea' | 'number' | 'boolean' | 'date' | 'email' | 'url' | 'select', placeholder?, required?, min?, max?, pattern? }`; `options` becomes optional when `input` asks for a typed value (a `select` still picks from them), and a typed question may omit `defaults`. Answers stay `string[]` on the wire in one canonical form per type (decimal, `"true"`/`"false"`, `YYYY-MM-DD`, …). The validators are the one rule set the loop (which drops values it cannot settle), the answer route (which refuses them with `400`) and the React model (which reports them before sending) share.
30
31
  - `Skill` / `SkillProvider` / `ScopeResolver` / `offerSkills` — authored procedures the model pulls in when a task calls for one, instead of every instruction living in the system prompt. A skill is not an agent: an `@Agent` is WHO answers, a skill is HOW one task is done, and any agent may load one. Scoping is by an opaque TOKEN (`actor:u1`, `tenant:berlin`, `global`, or a host's own `depot:north`), and which tokens apply is a host-supplied `ScopeResolver` returning them most-specific-first — so precedence falls out of the order and a new axis is a resolver change, not a schema change. `defaultScopeResolver` covers the tokens derivable from `Actor` alone (actor / tenant / global); `actorScope`, `tenantScope` and `GLOBAL_SCOPE` mint them. This package owns NO skill table: the host owns the rows behind `SkillProvider` (`list(scopes, ctx)` for the catalog, `load(name, scope, ctx)` for one body), so a consumer can relate its own `Sector` entity against the token values in its own read model without writing migrations into a schema the boot-time heal also edits. `staticSkillProvider` and `compositeSkillProvider` are the built-ins. `resolveSkillCatalog` is the pure precedence pass: most specific wins, and the loser's scope is recorded on `shadows` rather than discarded, so the model can say "your setting differs from the org default" instead of choosing silently. What enters the SYSTEM prompt is the catalog only — one line per skill, bounded by `maxSkills` (`DEFAULT_MAX_SKILLS`) — while a BODY arrives as a `skill` tool result on the transcript, where the `HistoryPolicy` ceiling already governs it. The loop spends ONE checkpoint on all of it (`skills:catalog`) holding the whole offer, and serves each load inside the ordinary `tool:<callId>` checkpoint, so both the scopes that applied and the body that entered the prompt are facts the journal holds rather than answers a replaying process's provider would give afresh. `loadSkill` refuses any name the turn's own catalog does not carry, which makes the journaled catalog the authorization boundary as well as the menu. `skillWriteVerdict` is the write rule: your own scope is yours, a wider one needs an elevated HUMAN author, and nothing but a human may ever write above its own scope — an agent that could write a `tenant:` skill is an agent whose prompt anyone in the tenant can edit by talking to it.
31
32
  - `MemoryRecord` / `MemoryProvider` / `offerMemories` / `writeMemory` — what the assistant concluded about a person or an organisation, carried across turns and threads. Scoped by the SAME opaque tokens and the same `ScopeResolver` skills use, so a deployment has one answer to "which scopes does this actor have". A memory is a keyed fact: `{ key, text, scope, origin, updatedAt }`, and the key is what makes a conflict mechanically detectable — two memories sharing a key at different scopes are one question answered twice, and `resolveMemoryDigest` lets the narrower win. Where it does, the entry's `overrides` carries the beaten **text** and its **author**, not merely its scope (a skill's `shadows`): the model is following one procedure either way, but a memory is a VALUE, and an agent that knew only that a wider one existed could tell the user nothing except which it picked. **Not retrieval:** a passage is a document someone authored and can fix at its source, a memory is the agent's own inference about someone who never saw it written — hence `MemoryOrigin` on every record, a block that tells the model these are its own fallible notes, and `forget` being REQUIRED on the provider while `write` is optional. Every provider method and every multi-argument export here takes ONE named object (`ListMemoriesInput`, `StoreMemoryInput`, `ResolveMemoryDigestInput`, …): `key`, `text` and `scope` are all strings, and transposed positional arguments would compile clean and write a fact whose key is its value. `memoryWriteVerdict` carries the same four rules as `skillWriteVerdict`; rule three (nothing but a human may write above its own scope, whatever elevation a host grants) is enforced by SHAPE as well as by check, since `rememberToolDefinition()` takes no scope parameter. `memoryForgetVerdict` is narrower still and takes no `elevated` flag: deleting what the assistant believes about YOU needs nobody's permission. What enters the system prompt is one line per memory bounded by `maxMemories` (`DEFAULT_MAX_MEMORIES`), each capped at `maxFactChars` (`DEFAULT_MAX_FACT_CHARS`) when it is WRITTEN — so the block's ceiling is the product of two numbers an operator set, and there is no body/catalog split because a fact that cannot be stated in a line is a document. **The prompt budget is bounded; the store is not.** Once the applicable set outgrows the block, WHICH memories it carries is a decision, and making it by scope starves the widest scopes first — one person's twentieth note would end every chance their organisation's facts had, leaving only a non-zero `omitted` behind. So a provider MAY implement `search({ scopes, query, limit, ctx })` and the block is filled by relevance to the turn instead; omit it and every turn is served by `list`, selecting narrowest-then-newest as before. Scope remains a hard FILTER that gates before ranking (a record returned outside `scopes` is dropped, so a host's filter bug costs throughput rather than privacy), and `search` must return every record sharing a returned key or precedence inverts. `MemoryRecord.pinned` is the categorical always-on marker — present whatever the turn is about, spending the same budget, never set by the agent (`StoreMemoryInput` has no such field, and the `remember` tool has no such parameter), with `MemoryDigest.pinnedOmitted` naming the one omission that is a misconfiguration rather than a budget. `buildMemoryBlock` frames entries by `origin.author`: what the agent CONCLUDED is hedged ("your own notes … prefer what the user says now"), what a person STATED is not, because telling a model to prefer the user over an organisation's published policy hands any user an override of it by assertion. A `partial` block says so, so the model does not read an absence as evidence. The loop spends ONE checkpoint (`memory:digest`) holding the whole digest — the search included, since a ranking is the most re-derivable decision here — which is both what the block is rendered from and what a later `remember` call is authorized against; the write itself happens inside the ordinary `tool:<callId>` checkpoint, which is what makes it idempotent under replay. The query is the user's own turn text and nothing else: the only thing available before the first model call, already a journaled input to the run, and it fails at a turn with no topic — which is what `pinned` is for.
32
33
  - `AgentStore.setMessageToolResults(messageId, results)` — a message's tool CALLS are known when it is appended and their outputs are not, so the loop settles them afterwards with one write of the turn's complete result list (its synthetic `retrieve` / `structured_output` calls included). Both halves live on the message because that is where a thread reader pairs them; a call whose output only ever reaches the `agent_tool_call` table renders as a tool still running. Required, not optional — a store that silently declines it breaks a client with nothing logged.
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-B2Dq9ZwF.cjs';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-CwXibnce.cjs';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-B2Dq9ZwF.js';
1
+ import { A as Actor, I as InputProcessor, O as OutputProcessor, T as ToolHandler } from '../tool-CwXibnce.js';
2
2
  import '@standard-schema/spec';
3
3
 
4
4
  /**
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,
@@ -118,6 +119,7 @@ __export(src_exports, {
118
119
  isReplayIntegrityError: () => isReplayIntegrityError,
119
120
  isToolEnabled: () => isToolEnabled,
120
121
  isTransientToolError: () => isTransientToolError,
122
+ isTypedQuestion: () => isTypedQuestion,
121
123
  loadSkill: () => loadSkill,
122
124
  mayDecideApproval: () => mayDecideApproval,
123
125
  memoryForgetVerdict: () => memoryForgetVerdict,
@@ -139,6 +141,9 @@ __export(src_exports, {
139
141
  publishAgentSkillsResolved: () => publishAgentSkillsResolved,
140
142
  publishAgentToolCall: () => publishAgentToolCall,
141
143
  publishAgentToolRetry: () => publishAgentToolRetry,
144
+ questionOptions: () => questionOptions,
145
+ readElicitationInput: () => readElicitationInput,
146
+ readElicitationQuestions: () => readElicitationQuestions,
142
147
  releaseGatedFrames: () => releaseGatedFrames,
143
148
  rememberInputSchema: () => rememberInputSchema,
144
149
  rememberToolDefinition: () => rememberToolDefinition,
@@ -169,6 +174,8 @@ __export(src_exports, {
169
174
  traceLlmTurn: () => traceLlmTurn,
170
175
  traceToolExecution: () => traceToolExecution,
171
176
  truncateDetailContent: () => truncateDetailContent,
177
+ validateElicitationAnswer: () => validateElicitationAnswer,
178
+ validateElicitationValue: () => validateElicitationValue,
172
179
  validateStructured: () => validateStructured,
173
180
  windowHistory: () => windowHistory,
174
181
  withAskTool: () => withAskTool,
@@ -1010,11 +1017,235 @@ ${detail}`;
1010
1017
  }
1011
1018
  __name(repairInstruction, "repairInstruction");
1012
1019
 
1013
- // src/elicitation.ts
1014
- function optionValues(question) {
1015
- 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;
1016
1161
  }
1017
- __name(optionValues, "optionValues");
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
+ };
1193
+ }
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
1018
1249
  function normalizeElicitationReply(reply) {
1019
1250
  if (typeof reply !== "object" || reply === null) {
1020
1251
  return {
@@ -1050,8 +1281,7 @@ function resolveElicitation(request, raw) {
1050
1281
  defaulted.push(question.id);
1051
1282
  continue;
1052
1283
  }
1053
- const allowed = optionValues(question);
1054
- const valid = question.allowFreeText === true ? submitted : submitted.filter((value) => allowed.has(value));
1284
+ const valid = submitted.filter((value) => value !== "" && validateElicitationValue(question, value) === null);
1055
1285
  answers[question.id] = question.multiple === true ? valid : valid.slice(0, 1);
1056
1286
  }
1057
1287
  return {
@@ -1072,7 +1302,7 @@ __name(settleElicitation, "settleElicitation");
1072
1302
  function renderElicitationAnswers(request, outcome) {
1073
1303
  const lines = request.questions.map((question) => {
1074
1304
  const chosen = outcome.answers[question.id] ?? [];
1075
- 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);
1076
1306
  return `${question.prompt} \u2192 ${labels.length > 0 ? labels.join(", ") : "(no answer)"}`;
1077
1307
  });
1078
1308
  const preface = outcome.skipped ? "The user declined to answer and asked you to proceed on these assumptions:" : "The user answered:";
@@ -1138,7 +1368,23 @@ function parseQuestion(raw, path, issues) {
1138
1368
  ], "must be a non-empty string"));
1139
1369
  return void 0;
1140
1370
  }
1141
- 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)) {
1142
1388
  issues.push(issue([
1143
1389
  ...path,
1144
1390
  "options"
@@ -1146,7 +1392,7 @@ function parseQuestion(raw, path, issues) {
1146
1392
  return void 0;
1147
1393
  }
1148
1394
  const options = [];
1149
- for (const [index, rawOption] of candidate.options.entries()) {
1395
+ for (const [index, rawOption] of (Array.isArray(candidate.options) ? candidate.options : []).entries()) {
1150
1396
  const option = parseOption(rawOption, [
1151
1397
  ...path,
1152
1398
  "options",
@@ -1156,6 +1402,33 @@ function parseQuestion(raw, path, issues) {
1156
1402
  options.push(option);
1157
1403
  }
1158
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
+ }
1159
1432
  if (!Array.isArray(candidate.defaults) || candidate.defaults.length === 0) {
1160
1433
  issues.push(issue([
1161
1434
  ...path,
@@ -1175,7 +1448,11 @@ function parseQuestion(raw, path, issues) {
1175
1448
  return {
1176
1449
  id: candidate.id,
1177
1450
  prompt: candidate.prompt,
1451
+ ...described,
1178
1452
  options,
1453
+ ...input !== void 0 ? {
1454
+ input
1455
+ } : {},
1179
1456
  defaults,
1180
1457
  ...candidate.multiple === true ? {
1181
1458
  multiple: true
@@ -1203,9 +1480,7 @@ var ASK_JSON_SCHEMA = {
1203
1480
  additionalProperties: false,
1204
1481
  required: [
1205
1482
  "id",
1206
- "prompt",
1207
- "options",
1208
- "defaults"
1483
+ "prompt"
1209
1484
  ],
1210
1485
  properties: {
1211
1486
  id: {
@@ -1215,6 +1490,50 @@ var ASK_JSON_SCHEMA = {
1215
1490
  prompt: {
1216
1491
  type: "string"
1217
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
+ },
1218
1537
  multiple: {
1219
1538
  type: "boolean",
1220
1539
  description: "Allow more than one option to be chosen."
@@ -1229,7 +1548,7 @@ var ASK_JSON_SCHEMA = {
1229
1548
  items: {
1230
1549
  type: "string"
1231
1550
  },
1232
- 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."
1233
1552
  },
1234
1553
  options: {
1235
1554
  type: "array",
@@ -4309,6 +4628,7 @@ __name(runAgentLoop, "runAgentLoop");
4309
4628
  DEFAULT_TOOL_TRANSIENT_RETRY_BACKOFF_MS,
4310
4629
  DefaultApprovalPolicy,
4311
4630
  DefaultRolesPolicy,
4631
+ ELICITATION_INPUT_TYPES,
4312
4632
  GLOBAL_SCOPE,
4313
4633
  MAX_ASK_QUESTIONS,
4314
4634
  OutputRejectedError,
@@ -4364,6 +4684,7 @@ __name(runAgentLoop, "runAgentLoop");
4364
4684
  isReplayIntegrityError,
4365
4685
  isToolEnabled,
4366
4686
  isTransientToolError,
4687
+ isTypedQuestion,
4367
4688
  loadSkill,
4368
4689
  mayDecideApproval,
4369
4690
  memoryForgetVerdict,
@@ -4385,6 +4706,9 @@ __name(runAgentLoop, "runAgentLoop");
4385
4706
  publishAgentSkillsResolved,
4386
4707
  publishAgentToolCall,
4387
4708
  publishAgentToolRetry,
4709
+ questionOptions,
4710
+ readElicitationInput,
4711
+ readElicitationQuestions,
4388
4712
  releaseGatedFrames,
4389
4713
  rememberInputSchema,
4390
4714
  rememberToolDefinition,
@@ -4415,6 +4739,8 @@ __name(runAgentLoop, "runAgentLoop");
4415
4739
  traceLlmTurn,
4416
4740
  traceToolExecution,
4417
4741
  truncateDetailContent,
4742
+ validateElicitationAnswer,
4743
+ validateElicitationValue,
4418
4744
  validateStructured,
4419
4745
  windowHistory,
4420
4746
  withAskTool,