@orkestrel/toolbox 0.0.5 → 0.0.7

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.
@@ -7,6 +7,7 @@ let _orkestrel_relation = require("@orkestrel/relation");
7
7
  let _orkestrel_agent = require("@orkestrel/agent");
8
8
  let _orkestrel_workspace = require("@orkestrel/workspace");
9
9
  let _orkestrel_tool = require("@orkestrel/tool");
10
+ let _orkestrel_form = require("@orkestrel/form");
10
11
  //#region src/core/constants.ts
11
12
  /**
12
13
  * The name {@link import('./factories.js').createAgentTool} advertises by default — the key a
@@ -233,23 +234,30 @@ var PROMPT_TOOL_NAME = "ask";
233
234
  * for the full teaching description; the full text stays retrievable via
234
235
  * {@link import('./factories.js').createDescribeTool}.
235
236
  */
236
- var PROMPT_TOOL_SUMMARY = "Ask another terminal a question and BLOCK until it answers; the call resolves with the answered value. Call describe('ask') for the required fields.";
237
- /** The full prompt protocol {@link import('./factories.js').createPromptTool} advertises. */
237
+ var PROMPT_TOOL_SUMMARY = "Ask another terminal a multi-field form and BLOCK until it answers; the call resolves with the values record. Call describe('ask') for the schema.";
238
+ /** The full form protocol {@link import('./factories.js').createPromptTool} advertises. */
238
239
  var PROMPT_TOOL_DESCRIPTION = [
239
- "Ask another terminal a question and block until it answers. This call does not return until the addressed terminal answers, or the prompt fails.",
240
+ "Ask another terminal a multi-field form and block until it answers. This call does not return until the addressed terminal answers, or the form expires.",
240
241
  "",
241
242
  "Required:",
242
- " to - the terminal name to ask.",
243
- " form - the prompt kind: one of \"input\", \"password\", \"confirm\", \"select\", \"checkbox\", \"editor\".",
244
- " message - the question shown to the answering terminal.",
245
- "Optional:",
246
- " options - form-specific options (e.g. choices for \"select\"/\"checkbox\").",
247
- "A cycle (two terminals asking each other) or an expired prompt fails the call with a typed error.",
243
+ " to - the terminal name to ask.",
244
+ " schema - an @orkestrel/form schema: each field declares `control` and `name`, plus optional `label`, `rule`, `default`, and `choices`; `fields` is an ordered array and each `name` must be unique.",
245
+ "The result is one values object keyed by those field names. A cycle or expired form fails with a typed error.",
248
246
  "Example:",
249
247
  JSON.stringify({
250
248
  to: "reviewer",
251
- form: "confirm",
252
- message: "Approve the release?"
249
+ schema: {
250
+ label: "Release review",
251
+ fields: [{
252
+ control: "confirm",
253
+ name: "approved",
254
+ label: "Approve the release?"
255
+ }, {
256
+ control: "editor",
257
+ name: "notes",
258
+ label: "Review notes"
259
+ }]
260
+ }
253
261
  })
254
262
  ].join("\n");
255
263
  /**
@@ -264,21 +272,24 @@ var ANSWER_TOOL_NAME = "answer";
264
272
  * for the full teaching description; the full text stays retrievable via
265
273
  * {@link import('./factories.js').createDescribeTool}.
266
274
  */
267
- var ANSWER_TOOL_SUMMARY = "List prompts addressed to this terminal, or answer one by id. Call describe('answer') for the required fields.";
275
+ var ANSWER_TOOL_SUMMARY = "List forms addressed to this terminal, or answer one by id with a values record. Call describe('answer') for the required fields.";
268
276
  /** The pending/answer protocol {@link import('./factories.js').createAnswerTool} advertises. */
269
277
  var ANSWER_TOOL_DESCRIPTION = [
270
- "List the prompts currently addressed to this terminal, or answer one of them by id. Every call is ONE operation, chosen by the \"operation\" field.",
278
+ "List the forms currently addressed to this terminal, or answer one of them by id. Every call is ONE operation, chosen by the \"operation\" field.",
271
279
  "",
272
280
  "Operations:",
273
- "- pending { \"operation\": \"pending\" } — list every prompt currently addressed to this terminal (id, form, message, options, time).",
274
- "- answer { \"operation\": \"answer\", \"id\": \"<prompt id>\", \"value\": <answer value> } — answer the prompt with that id; \"value\" must match the prompt's form (a string for \"input\"/\"password\"/\"editor\", a boolean for \"confirm\", a choice for \"select\", an array of choices for \"checkbox\").",
275
- "Example — list pending prompts:",
281
+ "- pending { \"operation\": \"pending\" } — list every form currently addressed to this terminal (id, from, schema).",
282
+ "- answer { \"operation\": \"answer\", \"id\": \"<form id>\", \"values\": { \"<field name>\": <field value> } } — answer every required field with the value its control accepts.",
283
+ "Example — list pending forms:",
276
284
  JSON.stringify({ operation: "pending" }),
277
285
  "Example — answer one:",
278
286
  JSON.stringify({
279
287
  operation: "answer",
280
288
  id: "abc123",
281
- value: true
289
+ values: {
290
+ approved: true,
291
+ notes: "Ready"
292
+ }
282
293
  })
283
294
  ].join("\n");
284
295
  /**
@@ -567,104 +578,36 @@ function isToolboxError(value) {
567
578
  //#endregion
568
579
  //#region src/core/shapers.ts
569
580
  /**
570
- * The shape of {@link import('./factories.js').createPromptTool}'s call arguments — `to` (the
571
- * terminal identity to address), `form` (which of the six {@link import('@orkestrel/terminal').PromptType}
572
- * forms to ask), `message`, an optional `timeout` override, and every per-form optional field
573
- * FLATTENED onto one object (mirrors `workspaceToolShape`'s flat-arm style, but a single shared
574
- * shape rather than a discriminated union — `form` alone does not vary the REQUIRED fields, only
575
- * which of the optional ones apply, so a flat shape stays faithful without duplicating `to` /
576
- * `message` / `timeout` across six near-identical arms).
581
+ * The shape of {@link import('./factories.js').createPromptTool}'s call arguments — `to` names the
582
+ * terminal identity and `schema` carries the complete multi-field form document.
577
583
  *
578
584
  * @remarks
579
- * `choices` backs `'select'` / `'checkbox'`; `default` backs `'input'` / `'confirm'` / `'select'`
580
- * (a string for the first two forms' text default, `'true'`/`'false'` string for confirm — the
581
- * contract layer cannot vary a field's type by a sibling field's value, so `default` stays a
582
- * string and the handler coerces per form); `mask` backs `'password'`; `min` / `max` backs
583
- * `'checkbox'`; `validate` (declarative only) backs the four text-shaped forms
584
- * (`'input'` / `'password'` / `'confirm'` / `'editor'`).
585
+ * The contract first requires exact JSON. The handler then delegates schema parsing and semantic
586
+ * validation to `@orkestrel/form`'s `parseForm`, so Toolbox does not duplicate the form schema.
585
587
  */
586
588
  var promptToolShape = (0, _orkestrel_contract.objectShape)({
587
589
  to: (0, _orkestrel_contract.stringShape)({
588
590
  min: 1,
589
- description: "The terminal identity to address the prompt to."
591
+ description: "The terminal identity to address the form to."
590
592
  }),
591
- form: (0, _orkestrel_contract.literalShape)([
592
- "input",
593
- "password",
594
- "confirm",
595
- "select",
596
- "checkbox",
597
- "editor"
598
- ], { description: "Which prompt form to ask." }),
599
- message: (0, _orkestrel_contract.stringShape)({
600
- min: 1,
601
- description: "The prompt's question."
602
- }),
603
- default: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "The default answer if the responder submits blank — 'input' / 'editor' text, 'confirm' 'true'/'false', or a 'select' choice value." })),
604
- choices: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
605
- name: (0, _orkestrel_contract.stringShape)({
606
- min: 1,
607
- description: "The choice label shown to the answering party."
608
- }),
609
- value: (0, _orkestrel_contract.stringShape)({
610
- min: 1,
611
- description: "The value submitted when this choice is picked."
612
- }),
613
- description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "An optional one-line elaboration." }))
614
- }), { description: "The selectable choices for 'select' / 'checkbox'." })),
615
- mask: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
616
- min: 1,
617
- description: "The mask character 'password' renders in place of input."
618
- })),
619
- min: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
620
- min: 0,
621
- description: "The minimum number of 'checkbox' selections required."
622
- })),
623
- max: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
624
- min: 0,
625
- description: "The maximum number of 'checkbox' selections allowed."
626
- })),
627
- validate: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.objectShape)({
628
- required: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Reject an empty (trimmed) input." })),
629
- minimum: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
630
- min: 0,
631
- description: "Reject an input shorter than this many characters."
632
- })),
633
- maximum: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
634
- min: 0,
635
- description: "Reject an input longer than this many characters."
636
- })),
637
- pattern: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "Reject an input that fails this regular-expression source." })),
638
- email: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require a valid email-address shape." })),
639
- url: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require a valid URL shape." })),
640
- numeric: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require a numeric value." })),
641
- integer: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require an integer value." })),
642
- alphanumeric: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require letters and digits only." }))
643
- })),
644
- timeout: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
645
- min: 0,
646
- description: "Milliseconds to wait before the prompt expires."
647
- }))
593
+ schema: (0, _orkestrel_contract.jsonShape)({ description: "The @orkestrel/form schema to ask, with form metadata and one or more typed fields." })
648
594
  });
649
595
  /**
650
596
  * The shape of {@link import('./factories.js').createAnswerTool}'s call arguments — discriminated
651
- * by `operation`: `'pending'` lists the prompts addressed to this tool's terminal, `'answer'`
652
- * resolves one by `id` with a `value`.
597
+ * by `operation`: `'pending'` lists the forms addressed to this tool's terminal, while `'answer'`
598
+ * resolves one by `id` with a complete `values` record.
653
599
  *
654
600
  * @remarks
655
- * `value`'s type varies by the ORIGINAL prompt's form (`string` for `'input'` / `'password'` /
656
- * `'select'` / `'editor'`, `boolean` for `'confirm'`, `readonly string[]` for `'checkbox'`) —
657
- * `unionShape(stringShape(), booleanShape(), arrayShape(stringShape()))` expresses that
658
- * union directly, so `value` is typed as the full `string | boolean | readonly string[]` union
659
- * here (no lossy string-only fallback needed).
660
- */
661
- var answerToolShape = (0, _orkestrel_contract.unionShape)((0, _orkestrel_contract.objectShape)({ operation: (0, _orkestrel_contract.literalShape)(["pending"], { description: "List the prompts currently addressed to this terminal." }) }), (0, _orkestrel_contract.objectShape)({
662
- operation: (0, _orkestrel_contract.literalShape)(["answer"], { description: "Answer one pending prompt by id." }),
601
+ * `values` is first bounded to exact JSON. The handler then narrows it with the originating Form
602
+ * package's `isFormValues` guard before the terminal manager applies it to the parked live form.
603
+ */
604
+ var answerToolShape = (0, _orkestrel_contract.unionShape)((0, _orkestrel_contract.objectShape)({ operation: (0, _orkestrel_contract.literalShape)(["pending"], { description: "List the forms currently addressed to this terminal." }) }), (0, _orkestrel_contract.objectShape)({
605
+ operation: (0, _orkestrel_contract.literalShape)(["answer"], { description: "Answer one pending form by id." }),
663
606
  id: (0, _orkestrel_contract.stringShape)({
664
607
  min: 1,
665
- description: "The id of the pending prompt to answer."
608
+ description: "The id of the pending form to answer."
666
609
  }),
667
- value: (0, _orkestrel_contract.unionShape)((0, _orkestrel_contract.stringShape)({ description: "A text / select / editor answer." }), (0, _orkestrel_contract.booleanShape)({ description: "A confirm answer." }), (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.stringShape)(), { description: "A checkbox answer — the checked values." }))
610
+ values: (0, _orkestrel_contract.jsonShape)({ description: "The form answers keyed by field name." })
668
611
  }));
669
612
  /**
670
613
  * The shape of {@link import('./types.js').AgentToolArguments} —
@@ -1419,48 +1362,6 @@ function expandSteps(flat) {
1419
1362
  });
1420
1363
  }
1421
1364
  /**
1422
- * Normalize an LLM-supplied answer `value` to the type {@link PromptType} `form` expects, so a
1423
- * caller that only ever emits strings can still answer a typed prompt.
1424
- *
1425
- * @remarks
1426
- * `'confirm'` coerces to a `boolean` — a `boolean` passes through, and the strings `'true'` /
1427
- * `'false'` (case-insensitively) map to it; any other string is truthy-coerced via
1428
- * `Boolean(value)`. `'checkbox'` coerces to `readonly string[]` — an array passes through
1429
- * (stringifying each entry), a comma-separated string splits + trims into one, and any other
1430
- * single (non-comma) string becomes a one-item array. Every other form (`'input'` / `'password'`
1431
- * / `'select'` / `'editor'`) coerces to a plain `string` — a string passes through verbatim; a
1432
- * non-string, non-object scalar (`number` / `boolean`) stringifies via `String(value)`; an
1433
- * object or array (no lossless string form) falls back to `''` rather than serializing garbage.
1434
- * Pure and total — never throws.
1435
- *
1436
- * @param form - The {@link PromptType} the answer is being coerced FOR
1437
- * @param value - The raw, LLM-supplied answer value
1438
- * @returns The coerced answer — `boolean` for `'confirm'`, `readonly string[]` for `'checkbox'`,
1439
- * `string` otherwise
1440
- */
1441
- function coerceAnswer(form, value) {
1442
- if (form === "confirm") {
1443
- if (typeof value === "boolean") return value;
1444
- if (typeof value === "string") {
1445
- const lower = value.trim().toLowerCase();
1446
- if (lower === "true") return true;
1447
- if (lower === "false") return false;
1448
- }
1449
- return Boolean(value);
1450
- }
1451
- if (form === "checkbox") {
1452
- if (Array.isArray(value)) return value.map((entry) => String(entry));
1453
- if (typeof value === "string") {
1454
- if (value.includes(",")) return value.split(",").map((entry) => entry.trim());
1455
- return [value];
1456
- }
1457
- return [String(value)];
1458
- }
1459
- if (typeof value === "string") return value;
1460
- if (typeof value === "object" && value !== null) return "";
1461
- return String(value);
1462
- }
1463
- /**
1464
1365
  * Map a caught error to the {@link ToolboxErrorCode} the terminal-tool factory should throw
1465
1366
  * with — the pure classification step of that factory's error handling.
1466
1367
  *
@@ -1469,8 +1370,8 @@ function coerceAnswer(form, value) {
1469
1370
  * value returns `undefined`, telling the caller this mapper does not apply (rethrow / handle
1470
1371
  * otherwise). For a genuine `TerminalError`, `'DEADLOCK'` maps to `'DEADLOCK'`, `'EXPIRE'` maps
1471
1372
  * to `'EXPIRE'`, and every other {@link import('@orkestrel/terminal').TerminalErrorCode}
1472
- * (`'TARGET'`, `'CANCEL'`, `'DRIVER'`) maps to the generic `'TOOL'` code. The mapper only
1473
- * classifies — the factory performs the actual throw.
1373
+ * (`'TARGET'`, `'LIMIT'`, `'CANCEL'`, `'DRIVER'`, `'DESTROYED'`) maps to the generic `'TOOL'`
1374
+ * code. The mapper only classifies — the factory performs the actual throw.
1474
1375
  *
1475
1376
  * @param error - The value caught from a terminal-manager operation (`ask` / `answer` / …)
1476
1377
  * @returns The mapped {@link ToolboxErrorCode}, or `undefined` if `error` is not a `TerminalError`
@@ -2565,19 +2466,18 @@ function createDescribeTool(tools) {
2565
2466
  });
2566
2467
  }
2567
2468
  /**
2568
- * Build an LLM-callable prompt tool — the ASK side of the terminal seam. Asks
2569
- * {@link import('./types.js').PromptToolOptions.to} a question and BLOCKS until it answers,
2570
- * returning the resolved answer value.
2469
+ * Build an LLM-callable form tool — the ASK side of the terminal seam. Asks
2470
+ * a multi-field form and BLOCKS until it answers, returning the resolved values record.
2571
2471
  *
2572
2472
  * @remarks
2573
2473
  * The universal tool-handler contract (AGENTS §14): validates the call args against
2574
- * {@link import('./shapers.js').promptToolShape}, dispatches to the matching
2575
- * `TerminalManagerInterface.ask` overload (`@orkestrel/terminal`) for the call's `form`, and
2576
- * RETURNS the resolved answer on success. `from` is FIXED at construction
2474
+ * {@link import('./shapers.js').promptToolShape}, parses the call's schema through
2475
+ * `@orkestrel/form`, constructs the live form, and passes it to `TerminalManagerInterface.ask`.
2476
+ * `from` is FIXED at construction
2577
2477
  * ({@link import('./types.js').PromptToolOptions.from}) — never read from the model-supplied
2578
- * args — so a model cannot spoof which terminal is asking. A prompt CYCLE rejects with
2478
+ * args — so a model cannot spoof which terminal is asking. A form CYCLE rejects with
2579
2479
  * `TerminalError('DEADLOCK')`, re-surfaced as a typed `DEADLOCK`
2580
- * {@link import('./errors.js').ToolboxError}; an expired prompt re-surfaces as `EXPIRE`; an
2480
+ * {@link import('./errors.js').ToolboxError}; an expired form re-surfaces as `EXPIRE`; an
2581
2481
  * unknown `to` (or any other `TerminalError`) re-surfaces as `TOOL`, naming the unknown terminal
2582
2482
  * plus the known ones (`manager.terminals()`).
2583
2483
  *
@@ -2610,51 +2510,23 @@ function createPromptTool(options) {
2610
2510
  async execute(args) {
2611
2511
  const call = contract.parse(args);
2612
2512
  if (call === void 0) throw new ToolboxError("TOOL", "malformed ask call", { args });
2613
- if ((call.form === "select" || call.form === "checkbox") && (call.choices ?? []).length === 0) throw new ToolboxError("TOOL", "select/checkbox requires at least one choice", {
2614
- to: call.to,
2615
- form: call.form
2513
+ const schema = (0, _orkestrel_form.parseForm)(call.schema);
2514
+ if (schema === void 0) throw new ToolboxError("TOOL", "malformed form schema", { schema: call.schema });
2515
+ for (const field of schema.fields) if ((field.control === "select" || field.control === "checkbox") && !field.choices.some((choice) => choice.disabled !== true)) throw new ToolboxError("TOOL", `${field.control} field '${field.name}' has no enabled choices`, {
2516
+ control: field.control,
2517
+ field: field.name
2616
2518
  });
2617
2519
  try {
2618
- switch (call.form) {
2619
- case "input": return await options.manager.ask(options.from, call.to, call.form, {
2620
- message: call.message,
2621
- ...call.default === void 0 ? {} : { default: call.default },
2622
- ...call.validate === void 0 ? {} : { validate: call.validate }
2623
- });
2624
- case "editor": return await options.manager.ask(options.from, call.to, call.form, {
2625
- message: call.message,
2626
- ...call.default === void 0 ? {} : { default: call.default },
2627
- ...call.validate === void 0 ? {} : { validate: call.validate }
2628
- });
2629
- case "password": return await options.manager.ask(options.from, call.to, call.form, {
2630
- message: call.message,
2631
- ...call.mask === void 0 ? {} : { mask: call.mask },
2632
- ...call.validate === void 0 ? {} : { validate: call.validate }
2633
- });
2634
- case "confirm": return await options.manager.ask(options.from, call.to, call.form, {
2635
- message: call.message,
2636
- ...call.default === void 0 ? {} : { default: call.default === "true" }
2637
- });
2638
- case "select": return await options.manager.ask(options.from, call.to, call.form, {
2639
- message: call.message,
2640
- choices: call.choices ?? [],
2641
- ...call.default === void 0 ? {} : { default: call.default }
2642
- });
2643
- case "checkbox": return await options.manager.ask(options.from, call.to, call.form, {
2644
- message: call.message,
2645
- choices: call.choices ?? [],
2646
- ...call.min === void 0 ? {} : { min: call.min },
2647
- ...call.max === void 0 ? {} : { max: call.max }
2648
- });
2649
- }
2520
+ return await options.manager.ask(options.from, call.to, (0, _orkestrel_form.createForm)(schema));
2650
2521
  } catch (error) {
2522
+ if ((0, _orkestrel_form.isFormError)(error) && error.code === "ABANDONED") throw new ToolboxError("EXPIRE", `form to '${call.to}' expired before it was answered`, { to: call.to });
2651
2523
  const code = terminalToolCode(error);
2652
2524
  if (code === void 0) throw error;
2653
- if (code === "DEADLOCK") throw new ToolboxError("DEADLOCK", `asking '${call.to}' would form a prompt cycle`, (0, _orkestrel_terminal.isTerminalError)(error) ? error.context : {
2525
+ if (code === "DEADLOCK") throw new ToolboxError("DEADLOCK", `asking '${call.to}' would create a form cycle`, (0, _orkestrel_terminal.isTerminalError)(error) ? error.context : {
2654
2526
  from: options.from,
2655
2527
  to: call.to
2656
2528
  });
2657
- if (code === "EXPIRE") throw new ToolboxError("EXPIRE", `prompt to '${call.to}' expired before it was answered`, { to: call.to });
2529
+ if (code === "EXPIRE") throw new ToolboxError("EXPIRE", `form to '${call.to}' expired before it was answered`, { to: call.to });
2658
2530
  if ((0, _orkestrel_terminal.isTerminalError)(error) && error.code === "TARGET") throw new ToolboxError("TOOL", `unknown terminal '${call.to}'`, {
2659
2531
  to: call.to,
2660
2532
  known: options.manager.terminals()
@@ -2665,24 +2537,22 @@ function createPromptTool(options) {
2665
2537
  });
2666
2538
  }
2667
2539
  /**
2668
- * Build an LLM-callable answer tool — the ANSWER side of the terminal seam. Lists the prompts
2540
+ * Build an LLM-callable answer tool — the ANSWER side of the terminal seam. Lists the forms
2669
2541
  * currently addressed to {@link import('./types.js').AnswerToolOptions.to}, or answers one of
2670
2542
  * them by id.
2671
2543
  *
2672
2544
  * @remarks
2673
2545
  * The universal tool-handler contract (AGENTS §14): validates the call args against
2674
2546
  * {@link import('./shapers.js').answerToolShape} (discriminated by `operation`). `'pending'`
2675
- * returns a compact list (`{ id, from, form, message }`) of every prompt currently addressed to
2676
- * `to` (`TerminalManagerInterface.pending`, `@orkestrel/terminal`). `'answer'` looks the prompt
2677
- * up by `id` (an unknown id throws a typed `ANSWER` {@link import('./errors.js').ToolboxError}),
2678
- * normalizes the model-supplied `value` to the prompt's own form
2679
- * ({@link import('./helpers.js').coerceAnswer}), and applies it via
2547
+ * returns a compact list (`{ id, from, schema }`) of every form currently addressed to `to`.
2548
+ * `'answer'` looks the form up by `id`, narrows the supplied `values` through `@orkestrel/form`,
2549
+ * and applies it via
2680
2550
  * `TerminalManagerInterface.answer` — a rejected / unknown / unresolvable outcome
2681
2551
  * (`TerminalAnswerResult.error`) re-surfaces as a typed `ANSWER` `ToolboxError`; success returns
2682
2552
  * `{ answered: id }`. `to` is FIXED at construction
2683
2553
  * ({@link import('./types.js').AnswerToolOptions.to}) — never read from the model-supplied args —
2684
2554
  * so a model cannot spoof which terminal it is answering for. Concurrent answerers racing on one
2685
- * endpoint are FIRST-WRITE-WINS — a late answer to an already-settled prompt returns a typed
2555
+ * endpoint are FIRST-WRITE-WINS — a late answer to an already-settled form returns a typed
2686
2556
  * `ANSWER` `ToolboxError` (surfaced as a 422 over HTTP).
2687
2557
  *
2688
2558
  * @param options - The live manager, the fixed `to` identity, and advertised overrides (see
@@ -2699,7 +2569,7 @@ function createPromptTool(options) {
2699
2569
  * manager.add('reviewer')
2700
2570
  * const tool = createAnswerTool({ manager, to: 'reviewer' })
2701
2571
  * const tools = createToolManager()
2702
- * tools.add(tool) // the reviewer terminal can now list/answer prompts addressed to it
2572
+ * tools.add(tool) // the reviewer terminal can now list/answer forms addressed to it
2703
2573
  * ```
2704
2574
  */
2705
2575
  function createAnswerTool(options) {
@@ -2713,22 +2583,21 @@ function createAnswerTool(options) {
2713
2583
  async execute(args) {
2714
2584
  const call = contract.parse(args);
2715
2585
  if (call === void 0) throw new ToolboxError("TOOL", "malformed answer call", { args });
2716
- if (call.operation === "pending") return options.manager.pending(options.to).map((prompt) => ({
2717
- id: prompt.id,
2718
- from: prompt.from,
2719
- form: prompt.form,
2720
- message: prompt.message
2586
+ if (call.operation === "pending") return options.manager.pending(options.to).map((form) => ({
2587
+ id: form.id,
2588
+ from: form.from,
2589
+ schema: form.schema
2721
2590
  }));
2722
- const prompt = options.manager.pending(options.to).find((entry) => entry.id === call.id);
2723
- if (prompt === void 0) throw new ToolboxError("ANSWER", `unknown prompt '${call.id}'`, {
2591
+ if (options.manager.pending(options.to).find((entry) => entry.id === call.id) === void 0) throw new ToolboxError("ANSWER", `unknown form '${call.id}'`, {
2724
2592
  id: call.id,
2725
2593
  reason: "unknown"
2726
2594
  });
2727
- const coerced = coerceAnswer(prompt.form, call.value);
2728
- const result = options.manager.answer(options.to, call.id, coerced);
2729
- if (!result.success) throw new ToolboxError("ANSWER", `failed to answer prompt '${call.id}': ${result.error}`, {
2595
+ if (!(0, _orkestrel_form.isFormValues)(call.values)) throw new ToolboxError("TOOL", "malformed form values", { values: call.values });
2596
+ const result = options.manager.answer(options.to, call.id, call.values);
2597
+ if (!result.success) throw new ToolboxError("ANSWER", `failed to answer form '${call.id}': ${result.error.reason}`, {
2730
2598
  id: call.id,
2731
- reason: result.error
2599
+ reason: result.error.reason,
2600
+ ...result.error.reason === "rejected" ? { errors: result.error.errors } : {}
2732
2601
  });
2733
2602
  return { answered: call.id };
2734
2603
  }
@@ -3364,7 +3233,6 @@ exports.agentTag = agentTag;
3364
3233
  exports.agentToolShape = agentToolShape;
3365
3234
  exports.answerToolShape = answerToolShape;
3366
3235
  exports.clampQuery = clampQuery;
3367
- exports.coerceAnswer = coerceAnswer;
3368
3236
  exports.columnKindShape = columnKindShape;
3369
3237
  exports.columnShape = columnShape;
3370
3238
  exports.columnSpecShape = columnSpecShape;