@orkestrel/toolbox 0.0.6 → 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.
@@ -6,6 +6,7 @@ import { isRelationError } from "@orkestrel/relation";
6
6
  import { agentResultToJSON } from "@orkestrel/agent";
7
7
  import { createWorkspaceManager, isText, rangeOf } from "@orkestrel/workspace";
8
8
  import { createTool } from "@orkestrel/tool";
9
+ import { createForm, isFormError, isFormValues, parseForm } from "@orkestrel/form";
9
10
  //#region src/core/constants.ts
10
11
  /**
11
12
  * The name {@link import('./factories.js').createAgentTool} advertises by default — the key a
@@ -232,23 +233,30 @@ var PROMPT_TOOL_NAME = "ask";
232
233
  * for the full teaching description; the full text stays retrievable via
233
234
  * {@link import('./factories.js').createDescribeTool}.
234
235
  */
235
- 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.";
236
- /** The full prompt protocol {@link import('./factories.js').createPromptTool} advertises. */
236
+ 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.";
237
+ /** The full form protocol {@link import('./factories.js').createPromptTool} advertises. */
237
238
  var PROMPT_TOOL_DESCRIPTION = [
238
- "Ask another terminal a question and block until it answers. This call does not return until the addressed terminal answers, or the prompt fails.",
239
+ "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.",
239
240
  "",
240
241
  "Required:",
241
- " to - the terminal name to ask.",
242
- " form - the prompt kind: one of \"input\", \"password\", \"confirm\", \"select\", \"checkbox\", \"editor\".",
243
- " message - the question shown to the answering terminal.",
244
- "Optional:",
245
- " options - form-specific options (e.g. choices for \"select\"/\"checkbox\").",
246
- "A cycle (two terminals asking each other) or an expired prompt fails the call with a typed error.",
242
+ " to - the terminal name to ask.",
243
+ " 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.",
244
+ "The result is one values object keyed by those field names. A cycle or expired form fails with a typed error.",
247
245
  "Example:",
248
246
  JSON.stringify({
249
247
  to: "reviewer",
250
- form: "confirm",
251
- message: "Approve the release?"
248
+ schema: {
249
+ label: "Release review",
250
+ fields: [{
251
+ control: "confirm",
252
+ name: "approved",
253
+ label: "Approve the release?"
254
+ }, {
255
+ control: "editor",
256
+ name: "notes",
257
+ label: "Review notes"
258
+ }]
259
+ }
252
260
  })
253
261
  ].join("\n");
254
262
  /**
@@ -263,21 +271,24 @@ var ANSWER_TOOL_NAME = "answer";
263
271
  * for the full teaching description; the full text stays retrievable via
264
272
  * {@link import('./factories.js').createDescribeTool}.
265
273
  */
266
- var ANSWER_TOOL_SUMMARY = "List prompts addressed to this terminal, or answer one by id. Call describe('answer') for the required fields.";
274
+ 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.";
267
275
  /** The pending/answer protocol {@link import('./factories.js').createAnswerTool} advertises. */
268
276
  var ANSWER_TOOL_DESCRIPTION = [
269
- "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.",
277
+ "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.",
270
278
  "",
271
279
  "Operations:",
272
- "- pending { \"operation\": \"pending\" } — list every prompt currently addressed to this terminal (id, form, message, options, time).",
273
- "- 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\").",
274
- "Example — list pending prompts:",
280
+ "- pending { \"operation\": \"pending\" } — list every form currently addressed to this terminal (id, from, schema).",
281
+ "- answer { \"operation\": \"answer\", \"id\": \"<form id>\", \"values\": { \"<field name>\": <field value> } } — answer every required field with the value its control accepts.",
282
+ "Example — list pending forms:",
275
283
  JSON.stringify({ operation: "pending" }),
276
284
  "Example — answer one:",
277
285
  JSON.stringify({
278
286
  operation: "answer",
279
287
  id: "abc123",
280
- value: true
288
+ values: {
289
+ approved: true,
290
+ notes: "Ready"
291
+ }
281
292
  })
282
293
  ].join("\n");
283
294
  /**
@@ -566,104 +577,36 @@ function isToolboxError(value) {
566
577
  //#endregion
567
578
  //#region src/core/shapers.ts
568
579
  /**
569
- * The shape of {@link import('./factories.js').createPromptTool}'s call arguments — `to` (the
570
- * terminal identity to address), `form` (which of the six {@link import('@orkestrel/terminal').PromptType}
571
- * forms to ask), `message`, an optional `timeout` override, and every per-form optional field
572
- * FLATTENED onto one object (mirrors `workspaceToolShape`'s flat-arm style, but a single shared
573
- * shape rather than a discriminated union — `form` alone does not vary the REQUIRED fields, only
574
- * which of the optional ones apply, so a flat shape stays faithful without duplicating `to` /
575
- * `message` / `timeout` across six near-identical arms).
580
+ * The shape of {@link import('./factories.js').createPromptTool}'s call arguments — `to` names the
581
+ * terminal identity and `schema` carries the complete multi-field form document.
576
582
  *
577
583
  * @remarks
578
- * `choices` backs `'select'` / `'checkbox'`; `default` backs `'input'` / `'confirm'` / `'select'`
579
- * (a string for the first two forms' text default, `'true'`/`'false'` string for confirm — the
580
- * contract layer cannot vary a field's type by a sibling field's value, so `default` stays a
581
- * string and the handler coerces per form); `mask` backs `'password'`; `min` / `max` backs
582
- * `'checkbox'`; `validate` (declarative only) backs the four text-shaped forms
583
- * (`'input'` / `'password'` / `'confirm'` / `'editor'`).
584
+ * The contract first requires exact JSON. The handler then delegates schema parsing and semantic
585
+ * validation to `@orkestrel/form`'s `parseForm`, so Toolbox does not duplicate the form schema.
584
586
  */
585
587
  var promptToolShape = objectShape({
586
588
  to: stringShape({
587
589
  min: 1,
588
- description: "The terminal identity to address the prompt to."
590
+ description: "The terminal identity to address the form to."
589
591
  }),
590
- form: literalShape([
591
- "input",
592
- "password",
593
- "confirm",
594
- "select",
595
- "checkbox",
596
- "editor"
597
- ], { description: "Which prompt form to ask." }),
598
- message: stringShape({
599
- min: 1,
600
- description: "The prompt's question."
601
- }),
602
- default: optionalShape(stringShape({ description: "The default answer if the responder submits blank — 'input' / 'editor' text, 'confirm' 'true'/'false', or a 'select' choice value." })),
603
- choices: optionalShape(arrayShape(objectShape({
604
- name: stringShape({
605
- min: 1,
606
- description: "The choice label shown to the answering party."
607
- }),
608
- value: stringShape({
609
- min: 1,
610
- description: "The value submitted when this choice is picked."
611
- }),
612
- description: optionalShape(stringShape({ description: "An optional one-line elaboration." }))
613
- }), { description: "The selectable choices for 'select' / 'checkbox'." })),
614
- mask: optionalShape(stringShape({
615
- min: 1,
616
- description: "The mask character 'password' renders in place of input."
617
- })),
618
- min: optionalShape(integerShape({
619
- min: 0,
620
- description: "The minimum number of 'checkbox' selections required."
621
- })),
622
- max: optionalShape(integerShape({
623
- min: 0,
624
- description: "The maximum number of 'checkbox' selections allowed."
625
- })),
626
- validate: optionalShape(objectShape({
627
- required: optionalShape(booleanShape({ description: "Reject an empty (trimmed) input." })),
628
- minimum: optionalShape(integerShape({
629
- min: 0,
630
- description: "Reject an input shorter than this many characters."
631
- })),
632
- maximum: optionalShape(integerShape({
633
- min: 0,
634
- description: "Reject an input longer than this many characters."
635
- })),
636
- pattern: optionalShape(stringShape({ description: "Reject an input that fails this regular-expression source." })),
637
- email: optionalShape(booleanShape({ description: "Require a valid email-address shape." })),
638
- url: optionalShape(booleanShape({ description: "Require a valid URL shape." })),
639
- numeric: optionalShape(booleanShape({ description: "Require a numeric value." })),
640
- integer: optionalShape(booleanShape({ description: "Require an integer value." })),
641
- alphanumeric: optionalShape(booleanShape({ description: "Require letters and digits only." }))
642
- })),
643
- timeout: optionalShape(integerShape({
644
- min: 0,
645
- description: "Milliseconds to wait before the prompt expires."
646
- }))
592
+ schema: jsonShape({ description: "The @orkestrel/form schema to ask, with form metadata and one or more typed fields." })
647
593
  });
648
594
  /**
649
595
  * The shape of {@link import('./factories.js').createAnswerTool}'s call arguments — discriminated
650
- * by `operation`: `'pending'` lists the prompts addressed to this tool's terminal, `'answer'`
651
- * resolves one by `id` with a `value`.
596
+ * by `operation`: `'pending'` lists the forms addressed to this tool's terminal, while `'answer'`
597
+ * resolves one by `id` with a complete `values` record.
652
598
  *
653
599
  * @remarks
654
- * `value`'s type varies by the ORIGINAL prompt's form (`string` for `'input'` / `'password'` /
655
- * `'select'` / `'editor'`, `boolean` for `'confirm'`, `readonly string[]` for `'checkbox'`) —
656
- * `unionShape(stringShape(), booleanShape(), arrayShape(stringShape()))` expresses that
657
- * union directly, so `value` is typed as the full `string | boolean | readonly string[]` union
658
- * here (no lossy string-only fallback needed).
659
- */
660
- var answerToolShape = unionShape(objectShape({ operation: literalShape(["pending"], { description: "List the prompts currently addressed to this terminal." }) }), objectShape({
661
- operation: literalShape(["answer"], { description: "Answer one pending prompt by id." }),
600
+ * `values` is first bounded to exact JSON. The handler then narrows it with the originating Form
601
+ * package's `isFormValues` guard before the terminal manager applies it to the parked live form.
602
+ */
603
+ var answerToolShape = unionShape(objectShape({ operation: literalShape(["pending"], { description: "List the forms currently addressed to this terminal." }) }), objectShape({
604
+ operation: literalShape(["answer"], { description: "Answer one pending form by id." }),
662
605
  id: stringShape({
663
606
  min: 1,
664
- description: "The id of the pending prompt to answer."
607
+ description: "The id of the pending form to answer."
665
608
  }),
666
- value: unionShape(stringShape({ description: "A text / select / editor answer." }), booleanShape({ description: "A confirm answer." }), arrayShape(stringShape(), { description: "A checkbox answer — the checked values." }))
609
+ values: jsonShape({ description: "The form answers keyed by field name." })
667
610
  }));
668
611
  /**
669
612
  * The shape of {@link import('./types.js').AgentToolArguments} —
@@ -1418,48 +1361,6 @@ function expandSteps(flat) {
1418
1361
  });
1419
1362
  }
1420
1363
  /**
1421
- * Normalize an LLM-supplied answer `value` to the type {@link PromptType} `form` expects, so a
1422
- * caller that only ever emits strings can still answer a typed prompt.
1423
- *
1424
- * @remarks
1425
- * `'confirm'` coerces to a `boolean` — a `boolean` passes through, and the strings `'true'` /
1426
- * `'false'` (case-insensitively) map to it; any other string is truthy-coerced via
1427
- * `Boolean(value)`. `'checkbox'` coerces to `readonly string[]` — an array passes through
1428
- * (stringifying each entry), a comma-separated string splits + trims into one, and any other
1429
- * single (non-comma) string becomes a one-item array. Every other form (`'input'` / `'password'`
1430
- * / `'select'` / `'editor'`) coerces to a plain `string` — a string passes through verbatim; a
1431
- * non-string, non-object scalar (`number` / `boolean`) stringifies via `String(value)`; an
1432
- * object or array (no lossless string form) falls back to `''` rather than serializing garbage.
1433
- * Pure and total — never throws.
1434
- *
1435
- * @param form - The {@link PromptType} the answer is being coerced FOR
1436
- * @param value - The raw, LLM-supplied answer value
1437
- * @returns The coerced answer — `boolean` for `'confirm'`, `readonly string[]` for `'checkbox'`,
1438
- * `string` otherwise
1439
- */
1440
- function coerceAnswer(form, value) {
1441
- if (form === "confirm") {
1442
- if (typeof value === "boolean") return value;
1443
- if (typeof value === "string") {
1444
- const lower = value.trim().toLowerCase();
1445
- if (lower === "true") return true;
1446
- if (lower === "false") return false;
1447
- }
1448
- return Boolean(value);
1449
- }
1450
- if (form === "checkbox") {
1451
- if (Array.isArray(value)) return value.map((entry) => String(entry));
1452
- if (typeof value === "string") {
1453
- if (value.includes(",")) return value.split(",").map((entry) => entry.trim());
1454
- return [value];
1455
- }
1456
- return [String(value)];
1457
- }
1458
- if (typeof value === "string") return value;
1459
- if (typeof value === "object" && value !== null) return "";
1460
- return String(value);
1461
- }
1462
- /**
1463
1364
  * Map a caught error to the {@link ToolboxErrorCode} the terminal-tool factory should throw
1464
1365
  * with — the pure classification step of that factory's error handling.
1465
1366
  *
@@ -1468,8 +1369,8 @@ function coerceAnswer(form, value) {
1468
1369
  * value returns `undefined`, telling the caller this mapper does not apply (rethrow / handle
1469
1370
  * otherwise). For a genuine `TerminalError`, `'DEADLOCK'` maps to `'DEADLOCK'`, `'EXPIRE'` maps
1470
1371
  * to `'EXPIRE'`, and every other {@link import('@orkestrel/terminal').TerminalErrorCode}
1471
- * (`'TARGET'`, `'CANCEL'`, `'DRIVER'`) maps to the generic `'TOOL'` code. The mapper only
1472
- * classifies — the factory performs the actual throw.
1372
+ * (`'TARGET'`, `'LIMIT'`, `'CANCEL'`, `'DRIVER'`, `'DESTROYED'`) maps to the generic `'TOOL'`
1373
+ * code. The mapper only classifies — the factory performs the actual throw.
1473
1374
  *
1474
1375
  * @param error - The value caught from a terminal-manager operation (`ask` / `answer` / …)
1475
1376
  * @returns The mapped {@link ToolboxErrorCode}, or `undefined` if `error` is not a `TerminalError`
@@ -2564,19 +2465,18 @@ function createDescribeTool(tools) {
2564
2465
  });
2565
2466
  }
2566
2467
  /**
2567
- * Build an LLM-callable prompt tool — the ASK side of the terminal seam. Asks
2568
- * {@link import('./types.js').PromptToolOptions.to} a question and BLOCKS until it answers,
2569
- * returning the resolved answer value.
2468
+ * Build an LLM-callable form tool — the ASK side of the terminal seam. Asks
2469
+ * a multi-field form and BLOCKS until it answers, returning the resolved values record.
2570
2470
  *
2571
2471
  * @remarks
2572
2472
  * The universal tool-handler contract (AGENTS §14): validates the call args against
2573
- * {@link import('./shapers.js').promptToolShape}, dispatches to the matching
2574
- * `TerminalManagerInterface.ask` overload (`@orkestrel/terminal`) for the call's `form`, and
2575
- * RETURNS the resolved answer on success. `from` is FIXED at construction
2473
+ * {@link import('./shapers.js').promptToolShape}, parses the call's schema through
2474
+ * `@orkestrel/form`, constructs the live form, and passes it to `TerminalManagerInterface.ask`.
2475
+ * `from` is FIXED at construction
2576
2476
  * ({@link import('./types.js').PromptToolOptions.from}) — never read from the model-supplied
2577
- * args — so a model cannot spoof which terminal is asking. A prompt CYCLE rejects with
2477
+ * args — so a model cannot spoof which terminal is asking. A form CYCLE rejects with
2578
2478
  * `TerminalError('DEADLOCK')`, re-surfaced as a typed `DEADLOCK`
2579
- * {@link import('./errors.js').ToolboxError}; an expired prompt re-surfaces as `EXPIRE`; an
2479
+ * {@link import('./errors.js').ToolboxError}; an expired form re-surfaces as `EXPIRE`; an
2580
2480
  * unknown `to` (or any other `TerminalError`) re-surfaces as `TOOL`, naming the unknown terminal
2581
2481
  * plus the known ones (`manager.terminals()`).
2582
2482
  *
@@ -2609,51 +2509,23 @@ function createPromptTool(options) {
2609
2509
  async execute(args) {
2610
2510
  const call = contract.parse(args);
2611
2511
  if (call === void 0) throw new ToolboxError("TOOL", "malformed ask call", { args });
2612
- if ((call.form === "select" || call.form === "checkbox") && (call.choices ?? []).length === 0) throw new ToolboxError("TOOL", "select/checkbox requires at least one choice", {
2613
- to: call.to,
2614
- form: call.form
2512
+ const schema = parseForm(call.schema);
2513
+ if (schema === void 0) throw new ToolboxError("TOOL", "malformed form schema", { schema: call.schema });
2514
+ 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`, {
2515
+ control: field.control,
2516
+ field: field.name
2615
2517
  });
2616
2518
  try {
2617
- switch (call.form) {
2618
- case "input": return await options.manager.ask(options.from, call.to, call.form, {
2619
- message: call.message,
2620
- ...call.default === void 0 ? {} : { default: call.default },
2621
- ...call.validate === void 0 ? {} : { validate: call.validate }
2622
- });
2623
- case "editor": return await options.manager.ask(options.from, call.to, call.form, {
2624
- message: call.message,
2625
- ...call.default === void 0 ? {} : { default: call.default },
2626
- ...call.validate === void 0 ? {} : { validate: call.validate }
2627
- });
2628
- case "password": return await options.manager.ask(options.from, call.to, call.form, {
2629
- message: call.message,
2630
- ...call.mask === void 0 ? {} : { mask: call.mask },
2631
- ...call.validate === void 0 ? {} : { validate: call.validate }
2632
- });
2633
- case "confirm": return await options.manager.ask(options.from, call.to, call.form, {
2634
- message: call.message,
2635
- ...call.default === void 0 ? {} : { default: call.default === "true" }
2636
- });
2637
- case "select": return await options.manager.ask(options.from, call.to, call.form, {
2638
- message: call.message,
2639
- choices: call.choices ?? [],
2640
- ...call.default === void 0 ? {} : { default: call.default }
2641
- });
2642
- case "checkbox": return await options.manager.ask(options.from, call.to, call.form, {
2643
- message: call.message,
2644
- choices: call.choices ?? [],
2645
- ...call.min === void 0 ? {} : { min: call.min },
2646
- ...call.max === void 0 ? {} : { max: call.max }
2647
- });
2648
- }
2519
+ return await options.manager.ask(options.from, call.to, createForm(schema));
2649
2520
  } catch (error) {
2521
+ if (isFormError(error) && error.code === "ABANDONED") throw new ToolboxError("EXPIRE", `form to '${call.to}' expired before it was answered`, { to: call.to });
2650
2522
  const code = terminalToolCode(error);
2651
2523
  if (code === void 0) throw error;
2652
- if (code === "DEADLOCK") throw new ToolboxError("DEADLOCK", `asking '${call.to}' would form a prompt cycle`, isTerminalError(error) ? error.context : {
2524
+ if (code === "DEADLOCK") throw new ToolboxError("DEADLOCK", `asking '${call.to}' would create a form cycle`, isTerminalError(error) ? error.context : {
2653
2525
  from: options.from,
2654
2526
  to: call.to
2655
2527
  });
2656
- if (code === "EXPIRE") throw new ToolboxError("EXPIRE", `prompt to '${call.to}' expired before it was answered`, { to: call.to });
2528
+ if (code === "EXPIRE") throw new ToolboxError("EXPIRE", `form to '${call.to}' expired before it was answered`, { to: call.to });
2657
2529
  if (isTerminalError(error) && error.code === "TARGET") throw new ToolboxError("TOOL", `unknown terminal '${call.to}'`, {
2658
2530
  to: call.to,
2659
2531
  known: options.manager.terminals()
@@ -2664,24 +2536,22 @@ function createPromptTool(options) {
2664
2536
  });
2665
2537
  }
2666
2538
  /**
2667
- * Build an LLM-callable answer tool — the ANSWER side of the terminal seam. Lists the prompts
2539
+ * Build an LLM-callable answer tool — the ANSWER side of the terminal seam. Lists the forms
2668
2540
  * currently addressed to {@link import('./types.js').AnswerToolOptions.to}, or answers one of
2669
2541
  * them by id.
2670
2542
  *
2671
2543
  * @remarks
2672
2544
  * The universal tool-handler contract (AGENTS §14): validates the call args against
2673
2545
  * {@link import('./shapers.js').answerToolShape} (discriminated by `operation`). `'pending'`
2674
- * returns a compact list (`{ id, from, form, message }`) of every prompt currently addressed to
2675
- * `to` (`TerminalManagerInterface.pending`, `@orkestrel/terminal`). `'answer'` looks the prompt
2676
- * up by `id` (an unknown id throws a typed `ANSWER` {@link import('./errors.js').ToolboxError}),
2677
- * normalizes the model-supplied `value` to the prompt's own form
2678
- * ({@link import('./helpers.js').coerceAnswer}), and applies it via
2546
+ * returns a compact list (`{ id, from, schema }`) of every form currently addressed to `to`.
2547
+ * `'answer'` looks the form up by `id`, narrows the supplied `values` through `@orkestrel/form`,
2548
+ * and applies it via
2679
2549
  * `TerminalManagerInterface.answer` — a rejected / unknown / unresolvable outcome
2680
2550
  * (`TerminalAnswerResult.error`) re-surfaces as a typed `ANSWER` `ToolboxError`; success returns
2681
2551
  * `{ answered: id }`. `to` is FIXED at construction
2682
2552
  * ({@link import('./types.js').AnswerToolOptions.to}) — never read from the model-supplied args —
2683
2553
  * so a model cannot spoof which terminal it is answering for. Concurrent answerers racing on one
2684
- * endpoint are FIRST-WRITE-WINS — a late answer to an already-settled prompt returns a typed
2554
+ * endpoint are FIRST-WRITE-WINS — a late answer to an already-settled form returns a typed
2685
2555
  * `ANSWER` `ToolboxError` (surfaced as a 422 over HTTP).
2686
2556
  *
2687
2557
  * @param options - The live manager, the fixed `to` identity, and advertised overrides (see
@@ -2698,7 +2568,7 @@ function createPromptTool(options) {
2698
2568
  * manager.add('reviewer')
2699
2569
  * const tool = createAnswerTool({ manager, to: 'reviewer' })
2700
2570
  * const tools = createToolManager()
2701
- * tools.add(tool) // the reviewer terminal can now list/answer prompts addressed to it
2571
+ * tools.add(tool) // the reviewer terminal can now list/answer forms addressed to it
2702
2572
  * ```
2703
2573
  */
2704
2574
  function createAnswerTool(options) {
@@ -2712,22 +2582,21 @@ function createAnswerTool(options) {
2712
2582
  async execute(args) {
2713
2583
  const call = contract.parse(args);
2714
2584
  if (call === void 0) throw new ToolboxError("TOOL", "malformed answer call", { args });
2715
- if (call.operation === "pending") return options.manager.pending(options.to).map((prompt) => ({
2716
- id: prompt.id,
2717
- from: prompt.from,
2718
- form: prompt.form,
2719
- message: prompt.message
2585
+ if (call.operation === "pending") return options.manager.pending(options.to).map((form) => ({
2586
+ id: form.id,
2587
+ from: form.from,
2588
+ schema: form.schema
2720
2589
  }));
2721
- const prompt = options.manager.pending(options.to).find((entry) => entry.id === call.id);
2722
- if (prompt === void 0) throw new ToolboxError("ANSWER", `unknown prompt '${call.id}'`, {
2590
+ if (options.manager.pending(options.to).find((entry) => entry.id === call.id) === void 0) throw new ToolboxError("ANSWER", `unknown form '${call.id}'`, {
2723
2591
  id: call.id,
2724
2592
  reason: "unknown"
2725
2593
  });
2726
- const coerced = coerceAnswer(prompt.form, call.value);
2727
- const result = options.manager.answer(options.to, call.id, coerced);
2728
- if (!result.success) throw new ToolboxError("ANSWER", `failed to answer prompt '${call.id}': ${result.error}`, {
2594
+ if (!isFormValues(call.values)) throw new ToolboxError("TOOL", "malformed form values", { values: call.values });
2595
+ const result = options.manager.answer(options.to, call.id, call.values);
2596
+ if (!result.success) throw new ToolboxError("ANSWER", `failed to answer form '${call.id}': ${result.error.reason}`, {
2729
2597
  id: call.id,
2730
- reason: result.error
2598
+ reason: result.error.reason,
2599
+ ...result.error.reason === "rejected" ? { errors: result.error.errors } : {}
2731
2600
  });
2732
2601
  return { answered: call.id };
2733
2602
  }
@@ -3319,6 +3188,6 @@ function createEndpointTool(definition, options) {
3319
3188
  });
3320
3189
  }
3321
3190
  //#endregion
3322
- export { AGENT_TOOL_DEPTH, AGENT_TOOL_DESCRIPTION, AGENT_TOOL_NAME, AGENT_TOOL_SUMMARY, ANSWER_TOOL_DESCRIPTION, ANSWER_TOOL_NAME, ANSWER_TOOL_SUMMARY, DATABASE_TOOL_DESCRIPTION, DATABASE_TOOL_LIMIT, DATABASE_TOOL_MUTATIONS, DATABASE_TOOL_NAME, DATABASE_TOOL_SUMMARY, DESCRIBE_TOOL_DESCRIPTION, DESCRIBE_TOOL_NAME, DESCRIBE_TOOL_SUMMARY, DatabaseDefinitionStore, DatabaseResolver, INFER_TOOL_DESCRIPTION, INFER_TOOL_NAME, INFER_TOOL_SUMMARY, MAX_WORKFLOW_DEPTH, MemoryDefinitionStore, PROMPT_TOOL_DESCRIPTION, PROMPT_TOOL_NAME, PROMPT_TOOL_SUMMARY, RELATION_TOOL_DEPTH, RELATION_TOOL_DESCRIPTION, RELATION_TOOL_LIMIT, RELATION_TOOL_NAME, RELATION_TOOL_SUMMARY, ToolboxError, WORKFLOW_TOOL_DESCRIPTION, WORKFLOW_TOOL_FLAT_EXAMPLE, WORKFLOW_TOOL_NAME, WORKFLOW_TOOL_NESTED_EXAMPLE, WORKFLOW_TOOL_SUMMARY, WORKSPACE_TOOL_DESCRIPTION, WORKSPACE_TOOL_EXAMPLE, WORKSPACE_TOOL_NAME, WORKSPACE_TOOL_SUMMARY, agentTag, agentToolShape, answerToolShape, clampQuery, coerceAnswer, columnKindShape, columnShape, columnSpecShape, completeDraft, completePhaseDraft, completeTaskDraft, conditionShape, createAgentFunction, createAgentTool, createAnswerTool, createDatabaseDefinitionStore, createDatabaseTool, createDescribeTool, createEndpointTool, createInferTool, createMemoryDefinitionStore, createPromptTool, createRelationTool, createToolFunction, createWorkflowDraftContract, createWorkflowFunctions, createWorkflowTool, createWorkspaceTool, databaseToolCode, databaseToolShape, deriveWorkflowDepth, describeToolShape, expandInclude, expandSteps, expandTables, extendLineage, includeShape, inferToolShape, isAgentFunction, isColumnKind, isColumnSpec, isDatabaseDefinition, isToolboxError, isWorkflowLineage, keyShape, kindShape, lineageOf, managerShape, orderShape, phaseDraftShape, promptToolShape, queryOf, queryShape, relationKeyShape, relationManagerOf, relationModelOf, relationToolCode, relationToolShape, rowShape, rowsShape, singleKeyShape, stepShape, tableSpecShape, taskDraftShape, terminalToolCode, workflowDraftShape, workflowStepsShape, workflowTag, workflowToolSummary, workspaceToolShape };
3191
+ export { AGENT_TOOL_DEPTH, AGENT_TOOL_DESCRIPTION, AGENT_TOOL_NAME, AGENT_TOOL_SUMMARY, ANSWER_TOOL_DESCRIPTION, ANSWER_TOOL_NAME, ANSWER_TOOL_SUMMARY, DATABASE_TOOL_DESCRIPTION, DATABASE_TOOL_LIMIT, DATABASE_TOOL_MUTATIONS, DATABASE_TOOL_NAME, DATABASE_TOOL_SUMMARY, DESCRIBE_TOOL_DESCRIPTION, DESCRIBE_TOOL_NAME, DESCRIBE_TOOL_SUMMARY, DatabaseDefinitionStore, DatabaseResolver, INFER_TOOL_DESCRIPTION, INFER_TOOL_NAME, INFER_TOOL_SUMMARY, MAX_WORKFLOW_DEPTH, MemoryDefinitionStore, PROMPT_TOOL_DESCRIPTION, PROMPT_TOOL_NAME, PROMPT_TOOL_SUMMARY, RELATION_TOOL_DEPTH, RELATION_TOOL_DESCRIPTION, RELATION_TOOL_LIMIT, RELATION_TOOL_NAME, RELATION_TOOL_SUMMARY, ToolboxError, WORKFLOW_TOOL_DESCRIPTION, WORKFLOW_TOOL_FLAT_EXAMPLE, WORKFLOW_TOOL_NAME, WORKFLOW_TOOL_NESTED_EXAMPLE, WORKFLOW_TOOL_SUMMARY, WORKSPACE_TOOL_DESCRIPTION, WORKSPACE_TOOL_EXAMPLE, WORKSPACE_TOOL_NAME, WORKSPACE_TOOL_SUMMARY, agentTag, agentToolShape, answerToolShape, clampQuery, columnKindShape, columnShape, columnSpecShape, completeDraft, completePhaseDraft, completeTaskDraft, conditionShape, createAgentFunction, createAgentTool, createAnswerTool, createDatabaseDefinitionStore, createDatabaseTool, createDescribeTool, createEndpointTool, createInferTool, createMemoryDefinitionStore, createPromptTool, createRelationTool, createToolFunction, createWorkflowDraftContract, createWorkflowFunctions, createWorkflowTool, createWorkspaceTool, databaseToolCode, databaseToolShape, deriveWorkflowDepth, describeToolShape, expandInclude, expandSteps, expandTables, extendLineage, includeShape, inferToolShape, isAgentFunction, isColumnKind, isColumnSpec, isDatabaseDefinition, isToolboxError, isWorkflowLineage, keyShape, kindShape, lineageOf, managerShape, orderShape, phaseDraftShape, promptToolShape, queryOf, queryShape, relationKeyShape, relationManagerOf, relationModelOf, relationToolCode, relationToolShape, rowShape, rowsShape, singleKeyShape, stepShape, tableSpecShape, taskDraftShape, terminalToolCode, workflowDraftShape, workflowStepsShape, workflowTag, workflowToolSummary, workspaceToolShape };
3323
3192
 
3324
3193
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _orkestrel_terminal = require("@orkestrel/terminal");
3
+ let _orkestrel_form = require("@orkestrel/form");
4
+ let _orkestrel_contract = require("@orkestrel/contract");
3
5
  let _orkestrel_server = require("@orkestrel/server");
4
6
  //#region src/server/constants.ts
5
7
  /**
@@ -40,7 +42,7 @@ var TerminalConnection = class {
40
42
  /**
41
43
  * Create a terminal stream connection.
42
44
  *
43
- * @param manager - Terminal manager supplying pending prompts and lifecycle events
45
+ * @param manager - Terminal manager supplying pending forms and lifecycle events
44
46
  * @param name - Terminal endpoint streamed by this connection
45
47
  * @param request - Request whose abort signal owns the connection lifetime
46
48
  * @param stream - Open SSE stream
@@ -63,7 +65,7 @@ var TerminalConnection = class {
63
65
  this.#tickHandler = this.#tick.bind(this);
64
66
  }
65
67
  /**
66
- * Open the connection by replaying pending prompts, subscribing, and arming keepalive handling.
68
+ * Open the connection by replaying pending forms, subscribing, and arming keepalive handling.
67
69
  *
68
70
  * @returns The SSE response
69
71
  */
@@ -73,7 +75,7 @@ var TerminalConnection = class {
73
75
  return this.#stream.response;
74
76
  }
75
77
  if (this.#cancel !== void 0) return this.#stream.response;
76
- for (const prompt of this.#manager.pending(this.#name)) this.#write((0, _orkestrel_terminal.serializePending)(prompt));
78
+ for (const form of this.#manager.pending(this.#name)) this.#write((0, _orkestrel_terminal.serializePending)(form));
77
79
  this.#manager.emitter.on("pending", this.#pendingHandler);
78
80
  this.#manager.emitter.on("expire", this.#expireHandler);
79
81
  this.#cancel = this.#timer(this.#tickHandler, this.#keepalive);
@@ -87,13 +89,13 @@ var TerminalConnection = class {
87
89
  ...wire.id === void 0 ? {} : { id: wire.id }
88
90
  });
89
91
  }
90
- #pending(prompt) {
91
- if (prompt.to !== this.#name) return;
92
+ #pending(form) {
93
+ if (form.to !== this.#name) return;
92
94
  if (this.#stream.closed) {
93
95
  this.#destroy();
94
96
  return;
95
97
  }
96
- this.#write((0, _orkestrel_terminal.serializePending)(prompt));
98
+ this.#write((0, _orkestrel_terminal.serializePending)(form));
97
99
  }
98
100
  #expire(to, id) {
99
101
  if (to !== this.#name) return;
@@ -219,11 +221,11 @@ var TerminalRoutes = class {
219
221
  } catch {
220
222
  return new Response(null, { status: 400 });
221
223
  }
222
- if (!(0, _orkestrel_terminal.isAnswerPayload)(body)) return new Response(null, { status: 422 });
223
- const result = this.#manager.answer(name, body.id, body.value);
224
- if (result.success) return new Response(null, { status: 204 });
225
- if (result.error === "terminal") return new Response(result.error, { status: 404 });
226
- return new Response(result.error, { status: 422 });
224
+ if (!(0, _orkestrel_contract.isRecord)(body) || !(0, _orkestrel_contract.isNonEmptyString)(body.id) || !(0, _orkestrel_form.isFormValues)(body.values)) return new Response(null, { status: 422 });
225
+ const result = this.#manager.answer(name, body.id, body.values);
226
+ if (result.success) return Response.json(result);
227
+ if (result.error.reason === "terminal") return Response.json(result, { status: 404 });
228
+ return Response.json(result, { status: 422 });
227
229
  }
228
230
  };
229
231
  //#endregion
@@ -1,4 +1,6 @@
1
- import { HEADER_TOKEN, defaultTimer, isAnswerPayload, serializeExpire, serializePending } from "@orkestrel/terminal";
1
+ import { HEADER_TOKEN, defaultTimer, serializeExpire, serializePending } from "@orkestrel/terminal";
2
+ import { isFormValues } from "@orkestrel/form";
3
+ import { isNonEmptyString, isRecord } from "@orkestrel/contract";
2
4
  import { ContentTooLargeError, DEFAULT_BODY_LIMIT, collectRequestBody, openStream } from "@orkestrel/server";
3
5
  //#region src/server/constants.ts
4
6
  /**
@@ -39,7 +41,7 @@ var TerminalConnection = class {
39
41
  /**
40
42
  * Create a terminal stream connection.
41
43
  *
42
- * @param manager - Terminal manager supplying pending prompts and lifecycle events
44
+ * @param manager - Terminal manager supplying pending forms and lifecycle events
43
45
  * @param name - Terminal endpoint streamed by this connection
44
46
  * @param request - Request whose abort signal owns the connection lifetime
45
47
  * @param stream - Open SSE stream
@@ -62,7 +64,7 @@ var TerminalConnection = class {
62
64
  this.#tickHandler = this.#tick.bind(this);
63
65
  }
64
66
  /**
65
- * Open the connection by replaying pending prompts, subscribing, and arming keepalive handling.
67
+ * Open the connection by replaying pending forms, subscribing, and arming keepalive handling.
66
68
  *
67
69
  * @returns The SSE response
68
70
  */
@@ -72,7 +74,7 @@ var TerminalConnection = class {
72
74
  return this.#stream.response;
73
75
  }
74
76
  if (this.#cancel !== void 0) return this.#stream.response;
75
- for (const prompt of this.#manager.pending(this.#name)) this.#write(serializePending(prompt));
77
+ for (const form of this.#manager.pending(this.#name)) this.#write(serializePending(form));
76
78
  this.#manager.emitter.on("pending", this.#pendingHandler);
77
79
  this.#manager.emitter.on("expire", this.#expireHandler);
78
80
  this.#cancel = this.#timer(this.#tickHandler, this.#keepalive);
@@ -86,13 +88,13 @@ var TerminalConnection = class {
86
88
  ...wire.id === void 0 ? {} : { id: wire.id }
87
89
  });
88
90
  }
89
- #pending(prompt) {
90
- if (prompt.to !== this.#name) return;
91
+ #pending(form) {
92
+ if (form.to !== this.#name) return;
91
93
  if (this.#stream.closed) {
92
94
  this.#destroy();
93
95
  return;
94
96
  }
95
- this.#write(serializePending(prompt));
97
+ this.#write(serializePending(form));
96
98
  }
97
99
  #expire(to, id) {
98
100
  if (to !== this.#name) return;
@@ -218,11 +220,11 @@ var TerminalRoutes = class {
218
220
  } catch {
219
221
  return new Response(null, { status: 400 });
220
222
  }
221
- if (!isAnswerPayload(body)) return new Response(null, { status: 422 });
222
- const result = this.#manager.answer(name, body.id, body.value);
223
- if (result.success) return new Response(null, { status: 204 });
224
- if (result.error === "terminal") return new Response(result.error, { status: 404 });
225
- return new Response(result.error, { status: 422 });
223
+ if (!isRecord(body) || !isNonEmptyString(body.id) || !isFormValues(body.values)) return new Response(null, { status: 422 });
224
+ const result = this.#manager.answer(name, body.id, body.values);
225
+ if (result.success) return Response.json(result);
226
+ if (result.error.reason === "terminal") return Response.json(result, { status: 404 });
227
+ return Response.json(result, { status: 422 });
226
228
  }
227
229
  };
228
230
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkestrel/toolbox",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Concrete LLM-callable tools for the @orkestrel line — workflow authoring, workspace editing, sub-agent delegation, and more, over the @orkestrel/tool runtime, with pluggable stores. Part of the @orkestrel line.",
5
5
  "keywords": [
6
6
  "agent",
@@ -78,21 +78,22 @@
78
78
  "prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
79
79
  },
80
80
  "dependencies": {
81
- "@orkestrel/agent": "^0.0.15",
82
- "@orkestrel/contract": "^0.0.11",
83
- "@orkestrel/database": "^0.0.8",
84
- "@orkestrel/relation": "^0.0.8",
85
- "@orkestrel/server": "^0.0.12",
86
- "@orkestrel/terminal": "^0.0.7",
87
- "@orkestrel/tool": "^0.0.10",
88
- "@orkestrel/workflow": "^0.0.11",
89
- "@orkestrel/workspace": "^0.0.4"
81
+ "@orkestrel/agent": "^0.0.16",
82
+ "@orkestrel/contract": "^0.0.12",
83
+ "@orkestrel/database": "^0.0.10",
84
+ "@orkestrel/form": "^0.0.2",
85
+ "@orkestrel/relation": "^0.0.9",
86
+ "@orkestrel/server": "^0.0.13",
87
+ "@orkestrel/terminal": "^0.0.10",
88
+ "@orkestrel/tool": "^0.0.11",
89
+ "@orkestrel/workflow": "^0.0.13",
90
+ "@orkestrel/workspace": "^0.0.5"
90
91
  },
91
92
  "devDependencies": {
92
93
  "@microsoft/api-extractor": "^7.58.12",
93
- "@orkestrel/guide": "^0.0.11",
94
- "@orkestrel/scaffold": "^0.0.30",
95
- "@orkestrel/test": "^0.0.1",
94
+ "@orkestrel/guide": "^0.0.12",
95
+ "@orkestrel/scaffold": "^0.0.40",
96
+ "@orkestrel/test": "^0.0.6",
96
97
  "@types/node": "^26.1.2",
97
98
  "@vitest/browser-playwright": "^4.1.10",
98
99
  "oxfmt": "^0.62.0",