@orkestrel/tool 0.0.1 → 0.0.2

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.
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _orkestrel_contract = require("@orkestrel/contract");
3
+ let _orkestrel_terminal = require("@orkestrel/terminal");
3
4
  let _orkestrel_agent = require("@orkestrel/agent");
4
5
  let _orkestrel_workflow = require("@orkestrel/workflow");
5
6
  //#region src/core/constants.ts
@@ -237,13 +238,74 @@ var DESCRIBE_TOOL_SUMMARY = "Return the full description of a named registered t
237
238
  * schema or multi-step protocol to teach.
238
239
  */
239
240
  var DESCRIBE_TOOL_DESCRIPTION = "Return the full description of a registered tool by its name. Required: name - the registered tool name (see another tool listing for available names).";
241
+ /**
242
+ * The name {@link import('./factories.js').createPromptTool} advertises by default — the key a
243
+ * model calls and the `ToolManagerInterface` (`@orkestrel/agent`) registers under.
244
+ */
245
+ var PROMPT_TOOL_NAME = "ask";
246
+ /**
247
+ * The lean {@link import('@orkestrel/agent').ToolInterface.summary} {@link import('./factories.js').createPromptTool}
248
+ * advertises in place of {@link PROMPT_TOOL_DESCRIPTION} — a `ToolManagerInterface.definitions()`
249
+ * (`@orkestrel/agent`) advertises `summary ?? description`, so this one-sentence text stands in
250
+ * for the full teaching description; the full text stays retrievable via
251
+ * {@link import('./factories.js').createDescribeTool}.
252
+ */
253
+ 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.";
254
+ var PROMPT_TOOL_DESCRIPTION = [
255
+ "Ask another terminal a question and block until it answers. This call does not return until the addressed terminal answers, or the prompt fails.",
256
+ "",
257
+ "Required:",
258
+ " to - the terminal name to ask.",
259
+ " form - the prompt kind: one of \"input\", \"password\", \"confirm\", \"select\", \"checkbox\", \"editor\".",
260
+ " message - the question shown to the answering terminal.",
261
+ "Optional:",
262
+ " options - form-specific options (e.g. choices for \"select\"/\"checkbox\").",
263
+ "A cycle (two terminals asking each other) or an expired prompt fails the call with a typed error.",
264
+ "Example:",
265
+ JSON.stringify({
266
+ to: "reviewer",
267
+ form: "confirm",
268
+ message: "Approve the release?"
269
+ })
270
+ ].join("\n");
271
+ /**
272
+ * The name {@link import('./factories.js').createAnswerTool} advertises by default — the key a
273
+ * model calls and the `ToolManagerInterface` (`@orkestrel/agent`) registers under.
274
+ */
275
+ var ANSWER_TOOL_NAME = "answer";
276
+ /**
277
+ * The lean {@link import('@orkestrel/agent').ToolInterface.summary} {@link import('./factories.js').createAnswerTool}
278
+ * advertises in place of {@link ANSWER_TOOL_DESCRIPTION} — a `ToolManagerInterface.definitions()`
279
+ * (`@orkestrel/agent`) advertises `summary ?? description`, so this one-sentence text stands in
280
+ * for the full teaching description; the full text stays retrievable via
281
+ * {@link import('./factories.js').createDescribeTool}.
282
+ */
283
+ var ANSWER_TOOL_SUMMARY = "List prompts addressed to this terminal, or answer one by id. Call describe('answer') for the required fields.";
284
+ var ANSWER_TOOL_DESCRIPTION = [
285
+ "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.",
286
+ "",
287
+ "Operations:",
288
+ "- pending { \"operation\": \"pending\" } — list every prompt currently addressed to this terminal (id, form, message, options, time).",
289
+ "- 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\").",
290
+ "Example — list pending prompts:",
291
+ JSON.stringify({ operation: "pending" }),
292
+ "Example — answer one:",
293
+ JSON.stringify({
294
+ operation: "answer",
295
+ id: "abc123",
296
+ value: true
297
+ })
298
+ ].join("\n");
240
299
  //#endregion
241
300
  //#region src/core/errors.ts
242
301
  /**
243
302
  * Thrown by {@link import('./factories.js').createAgentTool}'s and
244
303
  * {@link import('./factories.js').createDescribeTool}'s handlers on every failure path — a
245
- * malformed / unresolvable call or an unknown tool name (`TOOL`), or a delegation that would
246
- * exceed the configured depth bound or re-enter an ancestor (`DEPTH`).
304
+ * malformed / unresolvable call or an unknown tool name (`TOOL`), a delegation that would
305
+ * exceed the configured depth bound or re-enter an ancestor (`DEPTH`), a prompt cycle
306
+ * (`DEADLOCK`), a prompt that expired before it was answered (`EXPIRE`), or an answer that
307
+ * failed to apply (`ANSWER`) — the last three thrown by
308
+ * {@link import('./factories.js').createPromptTool} / {@link import('./factories.js').createAnswerTool}.
247
309
  *
248
310
  * @remarks
249
311
  * Carries a machine-readable `code` (see {@link import('./types.js').AgentToolErrorCode}) and
@@ -295,6 +357,106 @@ function isAgentToolError(value) {
295
357
  //#endregion
296
358
  //#region src/core/shapers.ts
297
359
  /**
360
+ * The shape of {@link import('./factories.js').createPromptTool}'s call arguments — `to` (the
361
+ * terminal identity to address), `form` (which of the six {@link import('@orkestrel/terminal').PromptType}
362
+ * forms to ask), `message`, an optional `timeout` override, and every per-form optional field
363
+ * FLATTENED onto one object (mirrors `workspaceToolShape`'s flat-arm style, but a single shared
364
+ * shape rather than a discriminated union — `form` alone does not vary the REQUIRED fields, only
365
+ * which of the optional ones apply, so a flat shape stays faithful without duplicating `to` /
366
+ * `message` / `timeout` across six near-identical arms).
367
+ *
368
+ * @remarks
369
+ * `choices` backs `'select'` / `'checkbox'`; `default` backs `'input'` / `'confirm'` / `'select'`
370
+ * (a string for the first two forms' text default, `'true'`/`'false'` string for confirm — the
371
+ * contract layer cannot vary a field's type by a sibling field's value, so `default` stays a
372
+ * string and the handler coerces per form); `mask` backs `'password'`; `min` / `max` backs
373
+ * `'checkbox'`; `validate` (declarative only) backs the four text-shaped forms
374
+ * (`'input'` / `'password'` / `'confirm'` / `'editor'`).
375
+ */
376
+ var promptToolShape = (0, _orkestrel_contract.objectShape)({
377
+ to: (0, _orkestrel_contract.stringShape)({
378
+ min: 1,
379
+ description: "The terminal identity to address the prompt to."
380
+ }),
381
+ form: (0, _orkestrel_contract.literalShape)([
382
+ "input",
383
+ "password",
384
+ "confirm",
385
+ "select",
386
+ "checkbox",
387
+ "editor"
388
+ ], { description: "Which prompt form to ask." }),
389
+ message: (0, _orkestrel_contract.stringShape)({
390
+ min: 1,
391
+ description: "The prompt's question."
392
+ }),
393
+ 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." })),
394
+ choices: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
395
+ name: (0, _orkestrel_contract.stringShape)({
396
+ min: 1,
397
+ description: "The choice label shown to the answering party."
398
+ }),
399
+ value: (0, _orkestrel_contract.stringShape)({
400
+ min: 1,
401
+ description: "The value submitted when this choice is picked."
402
+ }),
403
+ description: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "An optional one-line elaboration." }))
404
+ }), { description: "The selectable choices for 'select' / 'checkbox'." })),
405
+ mask: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({
406
+ min: 1,
407
+ description: "The mask character 'password' renders in place of input."
408
+ })),
409
+ min: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
410
+ min: 0,
411
+ description: "The minimum number of 'checkbox' selections required."
412
+ })),
413
+ max: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
414
+ min: 0,
415
+ description: "The maximum number of 'checkbox' selections allowed."
416
+ })),
417
+ validate: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.objectShape)({
418
+ required: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Reject an empty (trimmed) input." })),
419
+ minimum: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
420
+ min: 0,
421
+ description: "Reject an input shorter than this many characters."
422
+ })),
423
+ maximum: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
424
+ min: 0,
425
+ description: "Reject an input longer than this many characters."
426
+ })),
427
+ pattern: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)({ description: "Reject an input that fails this regular-expression source." })),
428
+ email: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require a valid email-address shape." })),
429
+ url: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require a valid URL shape." })),
430
+ numeric: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require a numeric value." })),
431
+ integer: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require an integer value." })),
432
+ alphanumeric: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Require letters and digits only." }))
433
+ })),
434
+ timeout: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.integerShape)({
435
+ min: 0,
436
+ description: "Milliseconds to wait before the prompt expires."
437
+ }))
438
+ });
439
+ /**
440
+ * The shape of {@link import('./factories.js').createAnswerTool}'s call arguments — discriminated
441
+ * by `operation`: `'pending'` lists the prompts addressed to this tool's terminal, `'answer'`
442
+ * resolves one by `id` with a `value`.
443
+ *
444
+ * @remarks
445
+ * `value`'s type varies by the ORIGINAL prompt's form (`string` for `'input'` / `'password'` /
446
+ * `'select'` / `'editor'`, `boolean` for `'confirm'`, `readonly string[]` for `'checkbox'`) —
447
+ * `unionShape(stringShape(), booleanShape(), arrayShape(stringShape()))` expresses that
448
+ * union directly, so `value` is typed as the full `string | boolean | readonly string[]` union
449
+ * here (no lossy string-only fallback needed).
450
+ */
451
+ 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)({
452
+ operation: (0, _orkestrel_contract.literalShape)(["answer"], { description: "Answer one pending prompt by id." }),
453
+ id: (0, _orkestrel_contract.stringShape)({
454
+ min: 1,
455
+ description: "The id of the pending prompt to answer."
456
+ }),
457
+ 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." }))
458
+ }));
459
+ /**
298
460
  * The shape of {@link import('./types.js').AgentToolArguments} —
299
461
  * {@link import('./factories.js').createAgentTool}'s advertised `parameters`.
300
462
  *
@@ -656,6 +818,69 @@ function expandSteps(flat) {
656
818
  phases: flat.steps.map((step) => ({ tasks: [{ run: step.name }] }))
657
819
  });
658
820
  }
821
+ /**
822
+ * Normalize an LLM-supplied answer `value` to the type {@link PromptType} `form` expects, so a
823
+ * caller that only ever emits strings can still answer a typed prompt.
824
+ *
825
+ * @remarks
826
+ * `'confirm'` coerces to a `boolean` — a `boolean` passes through, and the strings `'true'` /
827
+ * `'false'` (case-insensitively) map to it; any other string is truthy-coerced via
828
+ * `Boolean(value)`. `'checkbox'` coerces to `readonly string[]` — an array passes through
829
+ * (stringifying each entry), a comma-separated string splits + trims into one, and any other
830
+ * single (non-comma) string becomes a one-item array. Every other form (`'input'` / `'password'`
831
+ * / `'select'` / `'editor'`) coerces to a plain `string` — a string passes through verbatim; a
832
+ * non-string, non-object scalar (`number` / `boolean`) stringifies via `String(value)`; an
833
+ * object or array (no lossless string form) falls back to `''` rather than serializing garbage.
834
+ * Pure and total — never throws.
835
+ *
836
+ * @param form - The {@link PromptType} the answer is being coerced FOR
837
+ * @param value - The raw, LLM-supplied answer value
838
+ * @returns The coerced answer — `boolean` for `'confirm'`, `readonly string[]` for `'checkbox'`,
839
+ * `string` otherwise
840
+ */
841
+ function coerceAnswer(form, value) {
842
+ if (form === "confirm") {
843
+ if (typeof value === "boolean") return value;
844
+ if (typeof value === "string") {
845
+ const lower = value.trim().toLowerCase();
846
+ if (lower === "true") return true;
847
+ if (lower === "false") return false;
848
+ }
849
+ return Boolean(value);
850
+ }
851
+ if (form === "checkbox") {
852
+ if (Array.isArray(value)) return value.map((entry) => String(entry));
853
+ if (typeof value === "string") {
854
+ if (value.includes(",")) return value.split(",").map((entry) => entry.trim());
855
+ return [value];
856
+ }
857
+ return [String(value)];
858
+ }
859
+ if (typeof value === "string") return value;
860
+ if (typeof value === "object" && value !== null) return "";
861
+ return String(value);
862
+ }
863
+ /**
864
+ * Map a caught error to the {@link AgentToolErrorCode} the terminal-tool factory should throw
865
+ * with — the pure classification step of that factory's error handling.
866
+ *
867
+ * @remarks
868
+ * Narrows `error` with {@link isTerminalError} (`@orkestrel/terminal`) first: a non-`TerminalError`
869
+ * value returns `undefined`, telling the caller this mapper does not apply (rethrow / handle
870
+ * otherwise). For a genuine `TerminalError`, `'DEADLOCK'` maps to `'DEADLOCK'`, `'EXPIRE'` maps
871
+ * to `'EXPIRE'`, and every other {@link import('@orkestrel/terminal').TerminalErrorCode}
872
+ * (`'TARGET'`, `'CANCEL'`, `'DRIVER'`) maps to the generic `'TOOL'` code. The mapper only
873
+ * classifies — the factory performs the actual throw.
874
+ *
875
+ * @param error - The value caught from a terminal-manager operation (`ask` / `answer` / …)
876
+ * @returns The mapped {@link AgentToolErrorCode}, or `undefined` if `error` is not a `TerminalError`
877
+ */
878
+ function terminalToolCode(error) {
879
+ if (!(0, _orkestrel_terminal.isTerminalError)(error)) return void 0;
880
+ if (error.code === "DEADLOCK") return "DEADLOCK";
881
+ if (error.code === "EXPIRE") return "EXPIRE";
882
+ return "TOOL";
883
+ }
659
884
  //#endregion
660
885
  //#region src/core/factories.ts
661
886
  /**
@@ -1194,16 +1419,190 @@ function createDescribeTool(tools) {
1194
1419
  }
1195
1420
  });
1196
1421
  }
1422
+ /**
1423
+ * Build an LLM-callable prompt tool — the ASK side of the terminal seam. Asks
1424
+ * {@link import('./types.js').PromptToolOptions.to} a question and BLOCKS until it answers,
1425
+ * returning the resolved answer value.
1426
+ *
1427
+ * @remarks
1428
+ * The universal tool-handler contract (AGENTS §14): validates the call args against
1429
+ * {@link import('./shapers.js').promptToolShape}, dispatches to the matching
1430
+ * `TerminalManagerInterface.ask` overload (`@orkestrel/terminal`) for the call's `form`, and
1431
+ * RETURNS the resolved answer on success. `from` is FIXED at construction
1432
+ * ({@link import('./types.js').PromptToolOptions.from}) — never read from the model-supplied
1433
+ * args — so a model cannot spoof which terminal is asking. A prompt CYCLE rejects with
1434
+ * `TerminalError('DEADLOCK')`, re-surfaced as a typed `DEADLOCK`
1435
+ * {@link import('./errors.js').AgentToolError}; an expired prompt re-surfaces as `EXPIRE`; an
1436
+ * unknown `to` (or any other `TerminalError`) re-surfaces as `TOOL`, naming the unknown terminal
1437
+ * plus the known ones (`manager.terminals()`).
1438
+ *
1439
+ * @param options - The live manager, the fixed `from` identity, and advertised overrides (see
1440
+ * {@link import('./types.js').PromptToolOptions})
1441
+ * @returns A `ToolInterface` (named {@link import('./constants.js').PROMPT_TOOL_NAME} by default)
1442
+ *
1443
+ * @example
1444
+ * ```ts
1445
+ * import { createPromptTool } from '@src/core'
1446
+ * import { createTerminalManager, createToolManager } from '@orkestrel/terminal'
1447
+ *
1448
+ * const manager = createTerminalManager()
1449
+ * manager.add('agent')
1450
+ * manager.add('reviewer')
1451
+ * const tool = createPromptTool({ manager, from: 'agent' })
1452
+ * const tools = createToolManager()
1453
+ * tools.add(tool) // the agent can now ask 'reviewer' and block for the answer
1454
+ * ```
1455
+ */
1456
+ function createPromptTool(options) {
1457
+ const contract = (0, _orkestrel_contract.createContract)(promptToolShape);
1458
+ const parameters = (0, _orkestrel_contract.schemaToParameters)(contract.schema);
1459
+ return (0, _orkestrel_agent.createTool)({
1460
+ name: options.name ?? "ask",
1461
+ description: options.description ?? PROMPT_TOOL_DESCRIPTION,
1462
+ summary: PROMPT_TOOL_SUMMARY,
1463
+ parameters,
1464
+ execute: async (args) => {
1465
+ const call = contract.parse(args);
1466
+ if (call === void 0) throw new AgentToolError("TOOL", "malformed ask call", { args });
1467
+ if ((call.form === "select" || call.form === "checkbox") && (call.choices ?? []).length === 0) throw new AgentToolError("TOOL", "select/checkbox requires at least one choice", {
1468
+ to: call.to,
1469
+ form: call.form
1470
+ });
1471
+ try {
1472
+ switch (call.form) {
1473
+ case "input": return await options.manager.ask(options.from, call.to, call.form, {
1474
+ message: call.message,
1475
+ ...call.default === void 0 ? {} : { default: call.default },
1476
+ ...call.validate === void 0 ? {} : { validate: call.validate }
1477
+ });
1478
+ case "editor": return await options.manager.ask(options.from, call.to, call.form, {
1479
+ message: call.message,
1480
+ ...call.default === void 0 ? {} : { default: call.default },
1481
+ ...call.validate === void 0 ? {} : { validate: call.validate }
1482
+ });
1483
+ case "password": return await options.manager.ask(options.from, call.to, call.form, {
1484
+ message: call.message,
1485
+ ...call.mask === void 0 ? {} : { mask: call.mask },
1486
+ ...call.validate === void 0 ? {} : { validate: call.validate }
1487
+ });
1488
+ case "confirm": return await options.manager.ask(options.from, call.to, call.form, {
1489
+ message: call.message,
1490
+ ...call.default === void 0 ? {} : { default: call.default === "true" }
1491
+ });
1492
+ case "select": return await options.manager.ask(options.from, call.to, call.form, {
1493
+ message: call.message,
1494
+ choices: call.choices ?? [],
1495
+ ...call.default === void 0 ? {} : { default: call.default }
1496
+ });
1497
+ case "checkbox": return await options.manager.ask(options.from, call.to, call.form, {
1498
+ message: call.message,
1499
+ choices: call.choices ?? [],
1500
+ ...call.min === void 0 ? {} : { min: call.min },
1501
+ ...call.max === void 0 ? {} : { max: call.max }
1502
+ });
1503
+ }
1504
+ } catch (error) {
1505
+ const code = terminalToolCode(error);
1506
+ if (code === void 0) throw error;
1507
+ if (code === "DEADLOCK") throw new AgentToolError("DEADLOCK", `asking '${call.to}' would form a prompt cycle`, (0, _orkestrel_terminal.isTerminalError)(error) ? error.context : {
1508
+ from: options.from,
1509
+ to: call.to
1510
+ });
1511
+ if (code === "EXPIRE") throw new AgentToolError("EXPIRE", `prompt to '${call.to}' expired before it was answered`, { to: call.to });
1512
+ if ((0, _orkestrel_terminal.isTerminalError)(error) && error.code === "TARGET") throw new AgentToolError("TOOL", `unknown terminal '${call.to}'`, {
1513
+ to: call.to,
1514
+ known: options.manager.terminals()
1515
+ });
1516
+ throw new AgentToolError("TOOL", `asking '${call.to}' failed`, { to: call.to });
1517
+ }
1518
+ }
1519
+ });
1520
+ }
1521
+ /**
1522
+ * Build an LLM-callable answer tool — the ANSWER side of the terminal seam. Lists the prompts
1523
+ * currently addressed to {@link import('./types.js').AnswerToolOptions.to}, or answers one of
1524
+ * them by id.
1525
+ *
1526
+ * @remarks
1527
+ * The universal tool-handler contract (AGENTS §14): validates the call args against
1528
+ * {@link import('./shapers.js').answerToolShape} (discriminated by `operation`). `'pending'`
1529
+ * returns a compact list (`{ id, from, form, message }`) of every prompt currently addressed to
1530
+ * `to` (`TerminalManagerInterface.pending`, `@orkestrel/terminal`). `'answer'` looks the prompt
1531
+ * up by `id` (an unknown id throws a typed `ANSWER` {@link import('./errors.js').AgentToolError}),
1532
+ * normalizes the model-supplied `value` to the prompt's own form
1533
+ * ({@link import('./helpers.js').coerceAnswer}), and applies it via
1534
+ * `TerminalManagerInterface.answer` — a rejected / unknown / unresolvable outcome
1535
+ * (`TerminalAnswerResult.error`) re-surfaces as a typed `ANSWER` `AgentToolError`; success returns
1536
+ * `{ answered: id }`. `to` is FIXED at construction
1537
+ * ({@link import('./types.js').AnswerToolOptions.to}) — never read from the model-supplied args —
1538
+ * so a model cannot spoof which terminal it is answering for. Concurrent answerers racing on one
1539
+ * endpoint are FIRST-WRITE-WINS — a late answer to an already-settled prompt returns a typed
1540
+ * `ANSWER` `AgentToolError` (surfaced as a 422 over HTTP).
1541
+ *
1542
+ * @param options - The live manager, the fixed `to` identity, and advertised overrides (see
1543
+ * {@link import('./types.js').AnswerToolOptions})
1544
+ * @returns A `ToolInterface` (named {@link import('./constants.js').ANSWER_TOOL_NAME} by default)
1545
+ *
1546
+ * @example
1547
+ * ```ts
1548
+ * import { createAnswerTool } from '@src/core'
1549
+ * import { createTerminalManager, createToolManager } from '@orkestrel/terminal'
1550
+ *
1551
+ * const manager = createTerminalManager()
1552
+ * manager.add('reviewer')
1553
+ * const tool = createAnswerTool({ manager, to: 'reviewer' })
1554
+ * const tools = createToolManager()
1555
+ * tools.add(tool) // the reviewer terminal can now list/answer prompts addressed to it
1556
+ * ```
1557
+ */
1558
+ function createAnswerTool(options) {
1559
+ const contract = (0, _orkestrel_contract.createContract)(answerToolShape);
1560
+ const parameters = (0, _orkestrel_contract.schemaToParameters)(contract.schema);
1561
+ return (0, _orkestrel_agent.createTool)({
1562
+ name: options.name ?? "answer",
1563
+ description: options.description ?? ANSWER_TOOL_DESCRIPTION,
1564
+ summary: ANSWER_TOOL_SUMMARY,
1565
+ parameters,
1566
+ execute: async (args) => {
1567
+ const call = contract.parse(args);
1568
+ if (call === void 0) throw new AgentToolError("TOOL", "malformed answer call", { args });
1569
+ if (call.operation === "pending") return options.manager.pending(options.to).map((prompt) => ({
1570
+ id: prompt.id,
1571
+ from: prompt.from,
1572
+ form: prompt.form,
1573
+ message: prompt.message
1574
+ }));
1575
+ const prompt = options.manager.pending(options.to).find((entry) => entry.id === call.id);
1576
+ if (prompt === void 0) throw new AgentToolError("ANSWER", `unknown prompt '${call.id}'`, {
1577
+ id: call.id,
1578
+ reason: "unknown"
1579
+ });
1580
+ const coerced = coerceAnswer(prompt.form, call.value);
1581
+ const result = options.manager.answer(options.to, call.id, coerced);
1582
+ if (!result.success) throw new AgentToolError("ANSWER", `failed to answer prompt '${call.id}': ${result.error}`, {
1583
+ id: call.id,
1584
+ reason: result.error
1585
+ });
1586
+ return { answered: call.id };
1587
+ }
1588
+ });
1589
+ }
1197
1590
  //#endregion
1198
1591
  exports.AGENT_TOOL_DEPTH = AGENT_TOOL_DEPTH;
1199
1592
  exports.AGENT_TOOL_DESCRIPTION = AGENT_TOOL_DESCRIPTION;
1200
1593
  exports.AGENT_TOOL_NAME = AGENT_TOOL_NAME;
1201
1594
  exports.AGENT_TOOL_SUMMARY = AGENT_TOOL_SUMMARY;
1595
+ exports.ANSWER_TOOL_DESCRIPTION = ANSWER_TOOL_DESCRIPTION;
1596
+ exports.ANSWER_TOOL_NAME = ANSWER_TOOL_NAME;
1597
+ exports.ANSWER_TOOL_SUMMARY = ANSWER_TOOL_SUMMARY;
1202
1598
  exports.AgentToolError = AgentToolError;
1203
1599
  exports.DESCRIBE_TOOL_DESCRIPTION = DESCRIBE_TOOL_DESCRIPTION;
1204
1600
  exports.DESCRIBE_TOOL_NAME = DESCRIBE_TOOL_NAME;
1205
1601
  exports.DESCRIBE_TOOL_SUMMARY = DESCRIBE_TOOL_SUMMARY;
1206
1602
  exports.MAX_WORKFLOW_DEPTH = MAX_WORKFLOW_DEPTH;
1603
+ exports.PROMPT_TOOL_DESCRIPTION = PROMPT_TOOL_DESCRIPTION;
1604
+ exports.PROMPT_TOOL_NAME = PROMPT_TOOL_NAME;
1605
+ exports.PROMPT_TOOL_SUMMARY = PROMPT_TOOL_SUMMARY;
1207
1606
  exports.WORKFLOW_TOOL_DESCRIPTION = WORKFLOW_TOOL_DESCRIPTION;
1208
1607
  exports.WORKFLOW_TOOL_FLAT_EXAMPLE = WORKFLOW_TOOL_FLAT_EXAMPLE;
1209
1608
  exports.WORKFLOW_TOOL_NAME = WORKFLOW_TOOL_NAME;
@@ -1215,12 +1614,16 @@ exports.WORKSPACE_TOOL_NAME = WORKSPACE_TOOL_NAME;
1215
1614
  exports.WORKSPACE_TOOL_SUMMARY = WORKSPACE_TOOL_SUMMARY;
1216
1615
  exports.agentTag = agentTag;
1217
1616
  exports.agentToolShape = agentToolShape;
1617
+ exports.answerToolShape = answerToolShape;
1618
+ exports.coerceAnswer = coerceAnswer;
1218
1619
  exports.completeDraft = completeDraft;
1219
1620
  exports.completePhaseDraft = completePhaseDraft;
1220
1621
  exports.completeTaskDraft = completeTaskDraft;
1221
1622
  exports.createAgentFunction = createAgentFunction;
1222
1623
  exports.createAgentTool = createAgentTool;
1624
+ exports.createAnswerTool = createAnswerTool;
1223
1625
  exports.createDescribeTool = createDescribeTool;
1626
+ exports.createPromptTool = createPromptTool;
1224
1627
  exports.createToolFunction = createToolFunction;
1225
1628
  exports.createWorkflowDraftContract = createWorkflowDraftContract;
1226
1629
  exports.createWorkflowTool = createWorkflowTool;
@@ -1229,8 +1632,10 @@ exports.describeToolShape = describeToolShape;
1229
1632
  exports.expandSteps = expandSteps;
1230
1633
  exports.isAgentToolError = isAgentToolError;
1231
1634
  exports.phaseDraftShape = phaseDraftShape;
1635
+ exports.promptToolShape = promptToolShape;
1232
1636
  exports.stepShape = stepShape;
1233
1637
  exports.taskDraftShape = taskDraftShape;
1638
+ exports.terminalToolCode = terminalToolCode;
1234
1639
  exports.workflowDraftShape = workflowDraftShape;
1235
1640
  exports.workflowStepsShape = workflowStepsShape;
1236
1641
  exports.workflowTag = workflowTag;