@bastani/atomic 0.8.20-0 → 0.8.21-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +5 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +5 -0
- package/dist/builtin/subagents/agents/code-simplifier.md +78 -22
- package/dist/builtin/subagents/agents/debugger.md +4 -3
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +5 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +25 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/create-spec/SKILL.md +169 -125
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +89 -80
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
- package/dist/builtin/workflows/skills/impeccable/agents/openai.yaml +4 -0
- package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +122 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +38 -12
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +4 -14
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +115 -1
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +109 -6
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +7 -7
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +623 -94
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +16 -14
- package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +172 -0
- package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -6
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +33 -13
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +96 -19
- package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -4
- package/dist/builtin/workflows/skills/impeccable/reference/product.md +1 -3
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +5 -5
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +158 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +225 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +266 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/design-parser.mjs +16 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detect.mjs +21 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1725 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4543 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +12 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2316 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +17 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/is-generated.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +139 -96
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +4491 -526
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-event-validation.mjs +136 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +22 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +288 -110
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +47 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +1443 -100
- package/dist/builtin/workflows/skills/impeccable/scripts/live-session-store.mjs +17 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +17 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +216 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +2 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +633 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +67 -3
- package/dist/builtin/workflows/src/extension/render-result.ts +26 -1
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +227 -3
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +94 -7
- package/dist/builtin/workflows/src/shared/stage-prompt.ts +326 -0
- package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +62 -7
- package/dist/builtin/workflows/src/shared/store-types.ts +43 -0
- package/dist/builtin/workflows/src/shared/store.ts +37 -0
- package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -4
- package/dist/builtin/workflows/src/tui/graph-view.ts +47 -0
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +43 -1
- package/dist/builtin/workflows/src/tui/run-detail.ts +10 -4
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +117 -15
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +9 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +2 -5
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +11 -29
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/docs/quickstart.md +1 -2
- package/package.json +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/cognitive-load.md +0 -106
- package/dist/builtin/workflows/skills/impeccable/reference/color-and-contrast.md +0 -105
- package/dist/builtin/workflows/skills/impeccable/reference/heuristics-scoring.md +0 -234
- package/dist/builtin/workflows/skills/impeccable/reference/motion-design.md +0 -109
- package/dist/builtin/workflows/skills/impeccable/reference/personas.md +0 -179
- package/dist/builtin/workflows/skills/impeccable/reference/responsive-design.md +0 -114
- package/dist/builtin/workflows/skills/impeccable/reference/spatial-design.md +0 -100
- package/dist/builtin/workflows/skills/impeccable/reference/teach.md +0 -156
- package/dist/builtin/workflows/skills/impeccable/reference/typography.md +0 -159
- package/dist/builtin/workflows/skills/impeccable/reference/ux-writing.md +0 -107
- package/dist/builtin/workflows/skills/impeccable/scripts/load-context.mjs +0 -141
|
@@ -199,16 +199,63 @@ function extractMessageText(message: AgentSession["messages"][number]): string {
|
|
|
199
199
|
return "";
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
function
|
|
202
|
+
function lastAssistantTextFromMessages(messages: AgentSession["messages"]): string | undefined {
|
|
203
203
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
204
204
|
const message = messages[index];
|
|
205
|
-
|
|
205
|
+
// Only assistant prose is a valid non-terminating turn output. A tool
|
|
206
|
+
// result is the turn output ONLY when its tool terminated the turn, which
|
|
207
|
+
// is handled separately by `terminatingToolResultText`.
|
|
208
|
+
if (!message || message.role !== "assistant") continue;
|
|
206
209
|
const text = extractMessageText(message).trim();
|
|
207
210
|
if (text) return text;
|
|
208
211
|
}
|
|
209
212
|
return undefined;
|
|
210
213
|
}
|
|
211
214
|
|
|
215
|
+
/**
|
|
216
|
+
* When an agent turn ends on a tool that returned `terminate: true`, control
|
|
217
|
+
* returns with the tool result as the final conversational message and no
|
|
218
|
+
* trailing assistant response (see the structured-output contract in the
|
|
219
|
+
* Atomic extension docs). That tool result is the deterministic output of the
|
|
220
|
+
* turn, so it must win over any prose the model emitted *before* the tool call
|
|
221
|
+
* in the same assistant message (which `getLastAssistantText()` would otherwise
|
|
222
|
+
* surface). This keeps terminating structured-output tools such as the `goal`
|
|
223
|
+
* and `ralph` review gates' `review_decision` tool deterministic regardless of
|
|
224
|
+
* surrounding narration.
|
|
225
|
+
*
|
|
226
|
+
* Returns the trailing tool-result text ONLY when the most recent
|
|
227
|
+
* conversational message is a tool result whose tool call actually returned
|
|
228
|
+
* `terminate: true` (tracked at runtime from the session's tool_execution_end
|
|
229
|
+
* events — see `terminatingToolCallIds`). Returns `undefined` for a turn that
|
|
230
|
+
* ended on an assistant response, OR on a tool result from a non-terminating
|
|
231
|
+
* tool (e.g. a turn aborted/errored right after a tool call) — both fall back
|
|
232
|
+
* to the last assistant message.
|
|
233
|
+
*/
|
|
234
|
+
function terminatingToolResultText(
|
|
235
|
+
messages: AgentSession["messages"],
|
|
236
|
+
terminatingToolCallIds: ReadonlySet<string>,
|
|
237
|
+
): string | undefined {
|
|
238
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
239
|
+
const message = messages[index];
|
|
240
|
+
if (!message) continue;
|
|
241
|
+
if (message.role === "toolResult") {
|
|
242
|
+
// The trailing message is a tool result. It is the deterministic turn
|
|
243
|
+
// output only if THIS tool call returned `terminate: true`; otherwise the
|
|
244
|
+
// turn ended on this tool for another reason (abort/error) and the last
|
|
245
|
+
// assistant message is the real output.
|
|
246
|
+
const toolCallId = (message as { toolCallId?: unknown }).toolCallId;
|
|
247
|
+
if (typeof toolCallId !== "string" || !terminatingToolCallIds.has(toolCallId)) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const text = extractMessageText(message).trim();
|
|
251
|
+
return text.length > 0 ? text : undefined;
|
|
252
|
+
}
|
|
253
|
+
if (message.role === "assistant") return undefined;
|
|
254
|
+
// Skip non-conversational roles (system/user) while locating the tail.
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
212
259
|
function asAgentSession(activeSession: StageSessionRuntime | undefined): AgentSession | undefined {
|
|
213
260
|
if (!activeSession) return undefined;
|
|
214
261
|
const candidate = activeSession as StageSessionRuntime & Partial<Pick<AgentSession, "state" | "sessionManager" | "modelRegistry" | "getContextUsage">>;
|
|
@@ -226,11 +273,21 @@ function asAgentSession(activeSession: StageSessionRuntime | undefined): AgentSe
|
|
|
226
273
|
function lastAssistantTextFromSession(
|
|
227
274
|
activeSession: StageSessionRuntime | undefined,
|
|
228
275
|
fallback: string | undefined,
|
|
276
|
+
terminatingToolCallIds: ReadonlySet<string>,
|
|
229
277
|
): string | undefined {
|
|
230
278
|
if (!activeSession) return fallback;
|
|
279
|
+
// A tool that returned `terminate: true` ends the turn with its tool result
|
|
280
|
+
// as the final message; that result is the deterministic stage output and
|
|
281
|
+
// wins over prose emitted before the terminating tool call. Detection is by
|
|
282
|
+
// the tool call's actual runtime terminate flag — NOT merely "the last
|
|
283
|
+
// message is a tool result".
|
|
284
|
+
const terminatingText = terminatingToolResultText(activeSession.messages, terminatingToolCallIds);
|
|
285
|
+
if (terminatingText !== undefined) return terminatingText;
|
|
286
|
+
// Otherwise the turn output is the last assistant message — never a tool
|
|
287
|
+
// result from a non-terminating tool.
|
|
231
288
|
const direct = activeSession.getLastAssistantText?.();
|
|
232
289
|
if (direct !== undefined && direct.trim()) return direct;
|
|
233
|
-
return
|
|
290
|
+
return lastAssistantTextFromMessages(activeSession.messages) ?? direct ?? fallback;
|
|
234
291
|
}
|
|
235
292
|
|
|
236
293
|
const DEFAULT_MAX_OUTPUT_BYTES = 200 * 1024;
|
|
@@ -378,6 +435,26 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
378
435
|
let session: StageSessionRuntime | undefined;
|
|
379
436
|
let sessionPromise: Promise<StageSessionRuntime> | undefined;
|
|
380
437
|
let lastAssistantText: string | undefined;
|
|
438
|
+
// Tool-call ids whose tool returned `terminate: true` at runtime, observed
|
|
439
|
+
// from the session's `tool_execution_end` events. The SDK ends the turn on a
|
|
440
|
+
// terminating tool, so its tool result is the deterministic stage output; a
|
|
441
|
+
// trailing tool result from any other tool is NOT. See
|
|
442
|
+
// `lastAssistantTextFromSession`. The tool result *message* does not carry the
|
|
443
|
+
// terminate flag, so it must be tracked from the live event stream.
|
|
444
|
+
const terminatingToolCallIds = new Set<string>();
|
|
445
|
+
let unsubscribeTerminateWatcher: (() => void) | undefined;
|
|
446
|
+
const recordTerminatingToolCall = (event: unknown): void => {
|
|
447
|
+
if (event === null || typeof event !== "object") return;
|
|
448
|
+
const record = event as Record<string, unknown>;
|
|
449
|
+
if (record["type"] !== "tool_execution_end") return;
|
|
450
|
+
const result = record["result"];
|
|
451
|
+
if (result === null || typeof result !== "object") return;
|
|
452
|
+
if ((result as Record<string, unknown>)["terminate"] !== true) return;
|
|
453
|
+
const callId = record["toolCallId"];
|
|
454
|
+
if (typeof callId === "string" && callId.length > 0) {
|
|
455
|
+
terminatingToolCallIds.add(callId);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
381
458
|
let adapterMessages: AgentSession["messages"] = [];
|
|
382
459
|
let disposed = false;
|
|
383
460
|
let pendingThinkingLevel: Parameters<StageContext["setThinkingLevel"]>[0] | undefined;
|
|
@@ -463,6 +540,10 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
463
540
|
for (const listener of pendingListeners) {
|
|
464
541
|
listenerUnsubscribes.set(listener, created.subscribe(listener));
|
|
465
542
|
}
|
|
543
|
+
// Track terminating tool calls for this session so the stage result text is
|
|
544
|
+
// derived deterministically from a tool that actually ended the turn.
|
|
545
|
+
unsubscribeTerminateWatcher?.();
|
|
546
|
+
unsubscribeTerminateWatcher = created.subscribe((event) => recordTerminatingToolCall(event));
|
|
466
547
|
return created;
|
|
467
548
|
}
|
|
468
549
|
|
|
@@ -501,6 +582,9 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
501
582
|
sessionPromise = undefined;
|
|
502
583
|
for (const unsubscribe of listenerUnsubscribes.values()) unsubscribe();
|
|
503
584
|
listenerUnsubscribes.clear();
|
|
585
|
+
unsubscribeTerminateWatcher?.();
|
|
586
|
+
unsubscribeTerminateWatcher = undefined;
|
|
587
|
+
terminatingToolCallIds.clear();
|
|
504
588
|
await current?.dispose();
|
|
505
589
|
}
|
|
506
590
|
|
|
@@ -604,7 +688,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
604
688
|
return lastAssistantText;
|
|
605
689
|
}
|
|
606
690
|
await promptWithFallback(text, sdkOptions);
|
|
607
|
-
const rawText = lastAssistantTextFromSession(session, lastAssistantText) ?? "";
|
|
691
|
+
const rawText = lastAssistantTextFromSession(session, lastAssistantText, terminatingToolCallIds) ?? "";
|
|
608
692
|
lastAssistantText = await finalizePromptOutput(rawText, outputOptions, runtimeCwd);
|
|
609
693
|
return lastAssistantText;
|
|
610
694
|
},
|
|
@@ -628,7 +712,7 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
628
712
|
// `ctx.complete()` can still use the stage AgentSession for simple text
|
|
629
713
|
// completions. Completion-specific options require the dedicated adapter.
|
|
630
714
|
await promptWithFallback(text, undefined, "complete");
|
|
631
|
-
lastAssistantText = lastAssistantTextFromSession(session, lastAssistantText) ?? "";
|
|
715
|
+
lastAssistantText = lastAssistantTextFromSession(session, lastAssistantText, terminatingToolCallIds) ?? "";
|
|
632
716
|
return lastAssistantText;
|
|
633
717
|
},
|
|
634
718
|
|
|
@@ -717,15 +801,18 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
|
|
|
717
801
|
for (const unsubscribe of listenerUnsubscribes.values()) unsubscribe();
|
|
718
802
|
listenerUnsubscribes.clear();
|
|
719
803
|
pendingListeners.clear();
|
|
804
|
+
unsubscribeTerminateWatcher?.();
|
|
805
|
+
unsubscribeTerminateWatcher = undefined;
|
|
806
|
+
terminatingToolCallIds.clear();
|
|
720
807
|
await session?.dispose();
|
|
721
808
|
},
|
|
722
809
|
|
|
723
810
|
__getLastAssistantText() {
|
|
724
|
-
return lastAssistantTextFromSession(session, lastAssistantText);
|
|
811
|
+
return lastAssistantTextFromSession(session, lastAssistantText, terminatingToolCallIds);
|
|
725
812
|
},
|
|
726
813
|
|
|
727
814
|
getLastAssistantText() {
|
|
728
|
-
return lastAssistantTextFromSession(session, lastAssistantText);
|
|
815
|
+
return lastAssistantTextFromSession(session, lastAssistantText, terminatingToolCallIds);
|
|
729
816
|
},
|
|
730
817
|
|
|
731
818
|
async __ensureSession() {
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stage-prompt — headless answering for brokered in-stage prompts.
|
|
3
|
+
*
|
|
4
|
+
* A workflow stage's agent can call the `ask_user_question` tool, and the
|
|
5
|
+
* executor raises a deterministic readiness gate after such a turn. Both
|
|
6
|
+
* render their UI through {@link StageUiBroker.requestCustomUi} and resolve the
|
|
7
|
+
* tool's `ctx.ui.custom<QuestionnaireResult>()` promise with a
|
|
8
|
+
* `QuestionnaireResult`-shaped value. That value is normally produced by the
|
|
9
|
+
* interactive TUI component.
|
|
10
|
+
*
|
|
11
|
+
* For programmatic / non-interactive control (e.g. an orchestrating agent
|
|
12
|
+
* answering via `workflow send`), we need to synthesize the same value WITHOUT
|
|
13
|
+
* the TUI. This module:
|
|
14
|
+
* 1. Parses the `ask_user_question` tool args into a serializable
|
|
15
|
+
* {@link StageInputRequest} descriptor (surfaced on the stage snapshot so
|
|
16
|
+
* `workflow send` / status can show the questions + options).
|
|
17
|
+
* 2. Produces a {@link StagePromptAdapter} whose `buildResult` maps a simple
|
|
18
|
+
* answer (free text, an option label, an option index, or a pre-built raw
|
|
19
|
+
* result) into the `QuestionnaireResult` the tool expects.
|
|
20
|
+
*
|
|
21
|
+
* cross-ref:
|
|
22
|
+
* - packages/coding-agent/src/core/tools/ask-user-question/tool/types.ts
|
|
23
|
+
* QuestionnaireResult / QuestionAnswer (the result shape mirrored here)
|
|
24
|
+
* - src/shared/stage-ui-broker.ts provideStagePrompt / answerStagePrompt
|
|
25
|
+
* - src/runs/foreground/executor.ts ask_user_question watcher + readiness gate
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import type {
|
|
29
|
+
StageInputKind,
|
|
30
|
+
StageInputQuestion,
|
|
31
|
+
StageInputRequest,
|
|
32
|
+
} from "./store-types.js";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A simple, transport-friendly answer to a brokered stage prompt. Exactly one
|
|
36
|
+
* of the fields is typically populated:
|
|
37
|
+
* - `raw`: a pre-built `QuestionnaireResult`-shaped value, forwarded verbatim.
|
|
38
|
+
* - `optionLabels`: explicit option label(s) (one for single-select, many for
|
|
39
|
+
* multi-select).
|
|
40
|
+
* - `text`: free text — matched against option labels / 1-based indices, and
|
|
41
|
+
* otherwise treated as a typed ("custom") answer.
|
|
42
|
+
*/
|
|
43
|
+
export interface StageInputAnswer {
|
|
44
|
+
readonly text?: string;
|
|
45
|
+
readonly optionLabels?: readonly string[];
|
|
46
|
+
readonly raw?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Result shape mirrored from the coding-agent ask_user_question tool. */
|
|
50
|
+
interface BuiltAnswer {
|
|
51
|
+
questionIndex: number;
|
|
52
|
+
question: string;
|
|
53
|
+
kind: "option" | "custom" | "chat" | "multi";
|
|
54
|
+
answer: string | null;
|
|
55
|
+
selected?: string[];
|
|
56
|
+
}
|
|
57
|
+
interface BuiltResult {
|
|
58
|
+
answers: BuiltAnswer[];
|
|
59
|
+
cancelled: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Couples the serializable {@link StageInputRequest} descriptor with a
|
|
64
|
+
* `buildResult` that turns a {@link StageInputAnswer} into the value used to
|
|
65
|
+
* resolve the brokered `ctx.ui.custom` promise.
|
|
66
|
+
*/
|
|
67
|
+
export interface StagePromptAdapter {
|
|
68
|
+
readonly prompt: StageInputRequest;
|
|
69
|
+
buildResult(answer: StageInputAnswer): unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeLabel(value: string): string {
|
|
73
|
+
return value.trim().toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function asString(value: unknown): string | undefined {
|
|
77
|
+
return typeof value === "string" ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readStringArray(value: unknown): readonly string[] | undefined {
|
|
81
|
+
if (!Array.isArray(value)) return undefined;
|
|
82
|
+
const out = value.filter(
|
|
83
|
+
(item): item is string => typeof item === "string" && item.trim().length > 0,
|
|
84
|
+
);
|
|
85
|
+
return out.length > 0 ? out : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readFirstString(...values: readonly unknown[]): string | undefined {
|
|
89
|
+
for (const value of values) {
|
|
90
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when the answer carries a value the adapter can resolve. */
|
|
96
|
+
export function hasStageInputAnswerContent(answer: StageInputAnswer): boolean {
|
|
97
|
+
return (
|
|
98
|
+
answer.text !== undefined || answer.optionLabels !== undefined || answer.raw !== undefined
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Coerce a loosely-typed answer payload — as delivered by `workflow send`
|
|
104
|
+
* (`text` / `response` / `message`) — into a normalized {@link StageInputAnswer}
|
|
105
|
+
* the {@link StagePromptAdapter} can resolve against a question's options.
|
|
106
|
+
*
|
|
107
|
+
* Recognized inputs:
|
|
108
|
+
* - a plain string (option label, 1-based index, or free text);
|
|
109
|
+
* - a JSON-encoded string of any structured shape below;
|
|
110
|
+
* - a fully-formed `QuestionnaireResult` (every answer carries a numeric
|
|
111
|
+
* `questionIndex`), forwarded raw;
|
|
112
|
+
* - the orchestrator-friendly `{ answers: [{ answer | label | selected }] }`
|
|
113
|
+
* or `{ questions: [{ answer | label | selected }] }`;
|
|
114
|
+
* - a flat `{ answer | response | label | selected | optionLabels }`;
|
|
115
|
+
* - a string array (multi-select labels).
|
|
116
|
+
*
|
|
117
|
+
* Without this, a structured `response` was forwarded verbatim as the brokered
|
|
118
|
+
* result, violating the `QuestionnaireResult` contract and leaving the readiness
|
|
119
|
+
* gate (and any brokered prompt) unable to resolve to a matching option.
|
|
120
|
+
*/
|
|
121
|
+
export function coerceStageInputAnswer(value: unknown): StageInputAnswer {
|
|
122
|
+
if (typeof value === "string") {
|
|
123
|
+
const trimmed = value.trim();
|
|
124
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
125
|
+
try {
|
|
126
|
+
const parsed: unknown = JSON.parse(trimmed);
|
|
127
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
128
|
+
const coerced = coerceStageInputAnswer(parsed);
|
|
129
|
+
if (hasStageInputAnswerContent(coerced)) return coerced;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Not JSON — fall through and treat the string as a literal answer.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { text: value };
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
const labels = readStringArray(value);
|
|
139
|
+
return labels !== undefined ? { optionLabels: labels } : {};
|
|
140
|
+
}
|
|
141
|
+
if (value === null || typeof value !== "object") return {};
|
|
142
|
+
|
|
143
|
+
const record = value as Record<string, unknown>;
|
|
144
|
+
|
|
145
|
+
// Forward verbatim ONLY a fully-formed QuestionnaireResult — every answer
|
|
146
|
+
// entry must carry a numeric `questionIndex`. A loosely-shaped `answers[]`
|
|
147
|
+
// (e.g. `{ answers: [{ question, answer }] }`) is NOT genuine: forwarding it
|
|
148
|
+
// raw makes the tool envelope match no question index and reply
|
|
149
|
+
// "User declined to answer questions", stranding the prompt.
|
|
150
|
+
const answersArray = Array.isArray(record["answers"])
|
|
151
|
+
? (record["answers"] as readonly unknown[])
|
|
152
|
+
: undefined;
|
|
153
|
+
if (
|
|
154
|
+
answersArray !== undefined &&
|
|
155
|
+
answersArray.length > 0 &&
|
|
156
|
+
answersArray.every(
|
|
157
|
+
(entry) =>
|
|
158
|
+
entry !== null &&
|
|
159
|
+
typeof entry === "object" &&
|
|
160
|
+
typeof (entry as Record<string, unknown>)["questionIndex"] === "number",
|
|
161
|
+
)
|
|
162
|
+
) {
|
|
163
|
+
return { raw: value };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Otherwise locate the answer-bearing record — the first `answers[]` or
|
|
167
|
+
// `questions[]` entry when present, else the object itself — and extract a
|
|
168
|
+
// value the adapter can resolve against the question's options (so it assigns
|
|
169
|
+
// the correct questionIndex), rather than forwarding an unusable result.
|
|
170
|
+
const source = answerBearingRecord(record);
|
|
171
|
+
const labels = readStringArray(source["optionLabels"]) ?? readStringArray(source["selected"]);
|
|
172
|
+
if (labels !== undefined) return { optionLabels: labels };
|
|
173
|
+
|
|
174
|
+
const text = readFirstString(
|
|
175
|
+
source["answer"],
|
|
176
|
+
source["response"],
|
|
177
|
+
source["label"],
|
|
178
|
+
source["text"],
|
|
179
|
+
);
|
|
180
|
+
if (text !== undefined) return { text };
|
|
181
|
+
|
|
182
|
+
// Nothing resolvable — decline rather than forward an unusable result.
|
|
183
|
+
return {};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function firstObjectOf(value: unknown): Record<string, unknown> | undefined {
|
|
187
|
+
const first = Array.isArray(value) ? value[0] : undefined;
|
|
188
|
+
return first !== null && typeof first === "object"
|
|
189
|
+
? (first as Record<string, unknown>)
|
|
190
|
+
: undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function answerBearingRecord(record: Record<string, unknown>): Record<string, unknown> {
|
|
194
|
+
return firstObjectOf(record["answers"]) ?? firstObjectOf(record["questions"]) ?? record;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseOption(value: unknown): StageInputQuestion["options"][number] | undefined {
|
|
198
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
199
|
+
const record = value as Record<string, unknown>;
|
|
200
|
+
const label = asString(record["label"]);
|
|
201
|
+
if (label === undefined) return undefined;
|
|
202
|
+
const description = asString(record["description"]);
|
|
203
|
+
return description !== undefined ? { label, description } : { label };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function parseQuestion(value: unknown): StageInputQuestion | undefined {
|
|
207
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
208
|
+
const record = value as Record<string, unknown>;
|
|
209
|
+
const question = asString(record["question"]);
|
|
210
|
+
if (question === undefined) return undefined;
|
|
211
|
+
const rawOptions = Array.isArray(record["options"]) ? record["options"] : [];
|
|
212
|
+
const options = rawOptions
|
|
213
|
+
.map(parseOption)
|
|
214
|
+
.filter((option): option is StageInputQuestion["options"][number] => option !== undefined);
|
|
215
|
+
const header = asString(record["header"]);
|
|
216
|
+
const multiSelect = record["multiSelect"] === true;
|
|
217
|
+
return {
|
|
218
|
+
question,
|
|
219
|
+
...(header !== undefined ? { header } : {}),
|
|
220
|
+
...(multiSelect ? { multiSelect: true } : {}),
|
|
221
|
+
options,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Parse `ask_user_question` tool args (`{ questions: [...] }`) into the
|
|
227
|
+
* serializable question descriptors. Returns `undefined` when no well-formed
|
|
228
|
+
* question is present.
|
|
229
|
+
*/
|
|
230
|
+
export function parseAskUserQuestionArgs(
|
|
231
|
+
args: unknown,
|
|
232
|
+
): readonly StageInputQuestion[] | undefined {
|
|
233
|
+
if (args === null || typeof args !== "object") return undefined;
|
|
234
|
+
const rawQuestions = (args as Record<string, unknown>)["questions"];
|
|
235
|
+
if (!Array.isArray(rawQuestions)) return undefined;
|
|
236
|
+
const questions = rawQuestions
|
|
237
|
+
.map(parseQuestion)
|
|
238
|
+
.filter((question): question is StageInputQuestion => question !== undefined);
|
|
239
|
+
return questions.length > 0 ? questions : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Resolve a desired answer string against a single-select question's options.
|
|
244
|
+
* Matches a case-insensitive option label, then a 1-based option index, then
|
|
245
|
+
* falls back to a typed ("custom") answer.
|
|
246
|
+
*/
|
|
247
|
+
function answerSingle(question: StageInputQuestion, desired: string): BuiltAnswer {
|
|
248
|
+
const normalized = normalizeLabel(desired);
|
|
249
|
+
const byLabel = question.options.find((option) => normalizeLabel(option.label) === normalized);
|
|
250
|
+
if (byLabel) {
|
|
251
|
+
return { questionIndex: 0, question: question.question, kind: "option", answer: byLabel.label };
|
|
252
|
+
}
|
|
253
|
+
const asIndex = Number.parseInt(desired.trim(), 10);
|
|
254
|
+
if (
|
|
255
|
+
Number.isInteger(asIndex) &&
|
|
256
|
+
asIndex >= 1 &&
|
|
257
|
+
asIndex <= question.options.length &&
|
|
258
|
+
String(asIndex) === desired.trim()
|
|
259
|
+
) {
|
|
260
|
+
const option = question.options[asIndex - 1]!;
|
|
261
|
+
return { questionIndex: 0, question: question.question, kind: "option", answer: option.label };
|
|
262
|
+
}
|
|
263
|
+
return { questionIndex: 0, question: question.question, kind: "custom", answer: desired };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function answerMulti(question: StageInputQuestion, candidates: readonly string[]): BuiltAnswer {
|
|
267
|
+
const selected: string[] = [];
|
|
268
|
+
for (const candidate of candidates) {
|
|
269
|
+
const normalized = normalizeLabel(candidate);
|
|
270
|
+
const byLabel = question.options.find((option) => normalizeLabel(option.label) === normalized);
|
|
271
|
+
if (byLabel) {
|
|
272
|
+
if (!selected.includes(byLabel.label)) selected.push(byLabel.label);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const asIndex = Number.parseInt(candidate.trim(), 10);
|
|
276
|
+
if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= question.options.length) {
|
|
277
|
+
const option = question.options[asIndex - 1]!;
|
|
278
|
+
if (!selected.includes(option.label)) selected.push(option.label);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { questionIndex: 0, question: question.question, kind: "multi", answer: null, selected };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function buildResult(
|
|
285
|
+
questions: readonly StageInputQuestion[],
|
|
286
|
+
answer: StageInputAnswer,
|
|
287
|
+
): unknown {
|
|
288
|
+
if (answer.raw !== undefined) return answer.raw;
|
|
289
|
+
const question = questions[0];
|
|
290
|
+
if (question === undefined) return { answers: [], cancelled: true } satisfies BuiltResult;
|
|
291
|
+
|
|
292
|
+
const multi = question.multiSelect === true;
|
|
293
|
+
if (multi) {
|
|
294
|
+
const candidates =
|
|
295
|
+
answer.optionLabels !== undefined
|
|
296
|
+
? answer.optionLabels
|
|
297
|
+
: (answer.text ?? "").split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
298
|
+
return { answers: [answerMulti(question, candidates)], cancelled: false } satisfies BuiltResult;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const desired = answer.optionLabels?.[0] ?? answer.text;
|
|
302
|
+
if (desired === undefined || desired.length === 0) {
|
|
303
|
+
return { answers: [], cancelled: true } satisfies BuiltResult;
|
|
304
|
+
}
|
|
305
|
+
return { answers: [answerSingle(question, desired)], cancelled: false } satisfies BuiltResult;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Build a {@link StagePromptAdapter} from `ask_user_question` tool args. Returns
|
|
310
|
+
* `undefined` when the args contain no parseable question (the prompt then
|
|
311
|
+
* stays TUI-only).
|
|
312
|
+
*/
|
|
313
|
+
export function buildStagePromptAdapter(
|
|
314
|
+
id: string,
|
|
315
|
+
kind: StageInputKind,
|
|
316
|
+
args: unknown,
|
|
317
|
+
createdAt: number,
|
|
318
|
+
): StagePromptAdapter | undefined {
|
|
319
|
+
const questions = parseAskUserQuestionArgs(args);
|
|
320
|
+
if (questions === undefined) return undefined;
|
|
321
|
+
const prompt: StageInputRequest = { id, kind, questions, createdAt };
|
|
322
|
+
return {
|
|
323
|
+
prompt,
|
|
324
|
+
buildResult: (answer) => buildResult(questions, answer),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import type { Component, Focusable, TUI } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { Store } from "./store.js";
|
|
4
4
|
import { store as defaultStore } from "./store.js";
|
|
5
|
+
import type { StageInputRequest } from "./store-types.js";
|
|
6
|
+
import type { StageInputAnswer, StagePromptAdapter } from "./stage-prompt.js";
|
|
5
7
|
import type { PiCustomOverlayFactory, PiCustomOverlayOptions, PiKeybindings, PiTheme } from "../extension/wiring.js";
|
|
6
8
|
|
|
7
9
|
export interface StageCustomUiRequest<T = unknown> {
|
|
@@ -32,11 +34,61 @@ export class StageUiBroker {
|
|
|
32
34
|
private readonly store: Store;
|
|
33
35
|
private readonly pending = new Map<string, StageCustomUiRequest>();
|
|
34
36
|
private readonly hosts = new Map<string, StageCustomUiHost>();
|
|
37
|
+
// Headless-answer adapters keyed by (runId, stageId). Populated by the
|
|
38
|
+
// executor's ask_user_question watcher and the readiness gate so a brokered
|
|
39
|
+
// custom-UI prompt can be answered programmatically (e.g. via `workflow
|
|
40
|
+
// send`) without a TUI host rendering the interactive component.
|
|
41
|
+
private readonly adapters = new Map<string, StagePromptAdapter>();
|
|
35
42
|
|
|
36
43
|
constructor(store: Store = defaultStore) {
|
|
37
44
|
this.store = store;
|
|
38
45
|
}
|
|
39
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Register the structured descriptor + headless result builder for the
|
|
49
|
+
* prompt a stage is about to raise (or has just raised) via
|
|
50
|
+
* `requestCustomUi`. Surfaces the descriptor on the stage snapshot so
|
|
51
|
+
* `workflow send` / status can see and answer it. Safe to call before or
|
|
52
|
+
* after the matching `requestCustomUi`; the (runId, stageId) key joins them.
|
|
53
|
+
*/
|
|
54
|
+
provideStagePrompt(runId: string, stageId: string, adapter: StagePromptAdapter): void {
|
|
55
|
+
this.adapters.set(key(runId, stageId), adapter);
|
|
56
|
+
this.store.recordStageInputRequest(runId, stageId, adapter.prompt);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Drop a stage's headless-answer adapter and clear its snapshot descriptor. */
|
|
60
|
+
clearStagePrompt(runId: string, stageId: string): void {
|
|
61
|
+
if (this.adapters.delete(key(runId, stageId))) {
|
|
62
|
+
this.store.clearStageInputRequest(runId, stageId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Return the structured descriptor for a stage's brokered prompt when BOTH a
|
|
68
|
+
* headless-answer adapter and a live pending request exist for it — i.e. when
|
|
69
|
+
* `answerStagePrompt` can actually resolve something right now.
|
|
70
|
+
*/
|
|
71
|
+
peekStagePrompt(runId: string, stageId: string): StageInputRequest | undefined {
|
|
72
|
+
const hostKey = key(runId, stageId);
|
|
73
|
+
const adapter = this.adapters.get(hostKey);
|
|
74
|
+
if (adapter && this.pending.has(hostKey)) return adapter.prompt;
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Headlessly answer a stage's pending brokered prompt. Resolves the awaiting
|
|
80
|
+
* `ctx.ui.custom` promise with the adapter-built result. Returns `false` when
|
|
81
|
+
* there is no adapter+request pair for the stage.
|
|
82
|
+
*/
|
|
83
|
+
answerStagePrompt(runId: string, stageId: string, answer: StageInputAnswer): boolean {
|
|
84
|
+
const hostKey = key(runId, stageId);
|
|
85
|
+
const adapter = this.adapters.get(hostKey);
|
|
86
|
+
const request = this.pending.get(hostKey);
|
|
87
|
+
if (!adapter || !request) return false;
|
|
88
|
+
this.resolve(request, adapter.buildResult(answer));
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
40
92
|
private hideHost(host: StageCustomUiHost | undefined, request: StageCustomUiRequest, reason: unknown): void {
|
|
41
93
|
try {
|
|
42
94
|
host?.hideCustomUi?.(request, reason);
|
|
@@ -70,13 +122,12 @@ export class StageUiBroker {
|
|
|
70
122
|
return () => {
|
|
71
123
|
if (this.hosts.get(hostKey) !== host) return;
|
|
72
124
|
this.hosts.delete(hostKey);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
125
|
+
// Unregistering a host means it stops *displaying* the request — not that
|
|
126
|
+
// the request is cancelled. A stage-scoped human-input request (e.g.
|
|
127
|
+
// ask_user_question / readiness gate) outlives any one attached chat:
|
|
128
|
+
// detaching leaves it pending (the stage stays awaiting_input) and a
|
|
129
|
+
// future host re-displays it. The request is settled only by the user
|
|
130
|
+
// answering (resolve) or the run aborting (its AbortSignal -> reject).
|
|
80
131
|
};
|
|
81
132
|
}
|
|
82
133
|
|
|
@@ -132,6 +183,8 @@ export class StageUiBroker {
|
|
|
132
183
|
const hostKey = key(request.runId, request.stageId);
|
|
133
184
|
if (this.pending.get(hostKey)?.id !== request.id) return;
|
|
134
185
|
this.pending.delete(hostKey);
|
|
186
|
+
this.adapters.delete(hostKey);
|
|
187
|
+
this.store.clearStageInputRequest(request.runId, request.stageId);
|
|
135
188
|
this.store.recordStageAwaitingInput(request.runId, request.stageId, false);
|
|
136
189
|
this.hideHost(this.hosts.get(hostKey), request, undefined);
|
|
137
190
|
request.resolve(value);
|
|
@@ -141,6 +194,8 @@ export class StageUiBroker {
|
|
|
141
194
|
const hostKey = key(request.runId, request.stageId);
|
|
142
195
|
if (this.pending.get(hostKey)?.id !== request.id) return;
|
|
143
196
|
this.pending.delete(hostKey);
|
|
197
|
+
this.adapters.delete(hostKey);
|
|
198
|
+
this.store.clearStageInputRequest(request.runId, request.stageId);
|
|
144
199
|
this.store.recordStageAwaitingInput(request.runId, request.stageId, false);
|
|
145
200
|
this.hideHost(this.hosts.get(hostKey), request, reason);
|
|
146
201
|
request.reject(reason);
|
|
@@ -42,6 +42,41 @@ export interface PendingPrompt {
|
|
|
42
42
|
readonly createdAt: number;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** Discriminates the brokered structured-prompt source. */
|
|
46
|
+
export type StageInputKind = "ask_user_question" | "readiness_gate";
|
|
47
|
+
|
|
48
|
+
/** One selectable option in a {@link StageInputQuestion}. */
|
|
49
|
+
export interface StageInputOption {
|
|
50
|
+
readonly label: string;
|
|
51
|
+
readonly description?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One question in a {@link StageInputRequest}. */
|
|
55
|
+
export interface StageInputQuestion {
|
|
56
|
+
readonly question: string;
|
|
57
|
+
readonly header?: string;
|
|
58
|
+
readonly multiSelect?: boolean;
|
|
59
|
+
readonly options: readonly StageInputOption[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Serializable descriptor of an in-stage `ask_user_question` (or readiness
|
|
64
|
+
* gate) prompt brokered through `StageUiBroker`. Unlike {@link PendingPrompt}
|
|
65
|
+
* (the simple input/confirm/select/editor HIL model), this mirrors the richer
|
|
66
|
+
* structured ask_user_question shape so `workflow send` and status inspection
|
|
67
|
+
* can see the questions/options and answer the prompt without the TUI.
|
|
68
|
+
*
|
|
69
|
+
* Resolution lives in `StageUiBroker` (the awaiting `ctx.ui.custom` promise);
|
|
70
|
+
* only this JSON-cloneable descriptor lives on the snapshot.
|
|
71
|
+
*/
|
|
72
|
+
export interface StageInputRequest {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly kind: StageInputKind;
|
|
75
|
+
readonly questions: readonly StageInputQuestion[];
|
|
76
|
+
/** Issue timestamp (ms since epoch). */
|
|
77
|
+
readonly createdAt: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
45
80
|
export interface ToolEvent {
|
|
46
81
|
name: string;
|
|
47
82
|
input?: Record<string, unknown>;
|
|
@@ -95,6 +130,14 @@ export interface StageSnapshot {
|
|
|
95
130
|
awaitingInputSince?: number;
|
|
96
131
|
/** Pending human-in-the-loop prompt owned by this workflow stage/node. */
|
|
97
132
|
pendingPrompt?: PendingPrompt;
|
|
133
|
+
/**
|
|
134
|
+
* Structured descriptor of a brokered ask_user_question / readiness-gate
|
|
135
|
+
* prompt awaiting an answer. Set while the stage's `ctx.ui.custom` promise is
|
|
136
|
+
* pending; resolution lives in `StageUiBroker`. Lets `workflow send` answer
|
|
137
|
+
* the prompt headlessly. Distinct from {@link pendingPrompt}, which models
|
|
138
|
+
* the simpler input/confirm/select/editor HIL prompts.
|
|
139
|
+
*/
|
|
140
|
+
inputRequest?: StageInputRequest;
|
|
98
141
|
blockedByStageId?: string;
|
|
99
142
|
notices?: StageNotice[];
|
|
100
143
|
/**
|