@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 +1 -0
- package/dist/guardrails/index.d.cts +1 -1
- package/dist/guardrails/index.d.ts +1 -1
- package/dist/index.cjs +339 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +332 -13
- package/dist/index.js.map +1 -1
- package/dist/{tool-B2Dq9ZwF.d.cts → tool-CwXibnce.d.cts} +83 -2
- package/dist/{tool-B2Dq9ZwF.d.ts → tool-CwXibnce.d.ts} +83 -2
- package/package.json +1 -1
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.
|
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
|
-
|
|
1015
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,
|