@selesai/code 0.5.22 → 0.5.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/data/capabilities.d.ts +48 -0
- package/dist/data/capabilities.d.ts.map +1 -0
- package/dist/data/capabilities.js +282 -0
- package/dist/data/capabilities.js.map +1 -0
- package/dist/data/navigation.d.ts +3 -0
- package/dist/data/navigation.d.ts.map +1 -0
- package/dist/data/navigation.js +90 -0
- package/dist/data/navigation.js.map +1 -0
- package/dist/extensions/pi-intercom/lifecycle.test.ts +60 -0
- package/dist/extensions/pi-powerline-footer/guide.ts +4 -4
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +0 -4
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +1 -1
- package/dist/extensions/workflow/adapter.ts +20 -5
- package/dist/extensions/workflow/extension.ts +3 -2
- package/dist/extensions/workflow/modes/loop.ts +55 -0
- package/dist/extensions/workflow/modes/quicktype.ts +150 -0
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -3
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/skills/agent-browser/SKILL.md +52 -0
- package/docs/workflows.md +29 -9
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
describe("intercom lifecycle safety", () => {
|
|
8
|
+
it("stale relay delivery cannot become an unhandled rejection", async () => {
|
|
9
|
+
const agentDir = mkdtempSync(path.join(tmpdir(), "selesai-intercom-test-"));
|
|
10
|
+
const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
|
|
11
|
+
process.env.SELESAI_CODING_AGENT_DIR = agentDir;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const { default: piIntercomExtension } = await import("./index.ts");
|
|
15
|
+
const events = new EventEmitter();
|
|
16
|
+
let sendAttempts = 0;
|
|
17
|
+
const pi = {
|
|
18
|
+
getSessionName: () => "orchestrator",
|
|
19
|
+
events: {
|
|
20
|
+
on: (name: string, handler: (payload: unknown) => void) => {
|
|
21
|
+
events.on(name, handler);
|
|
22
|
+
return () => events.off(name, handler);
|
|
23
|
+
},
|
|
24
|
+
emit: (name: string, payload: unknown) => events.emit(name, payload),
|
|
25
|
+
},
|
|
26
|
+
on: () => undefined,
|
|
27
|
+
registerMessageRenderer: () => undefined,
|
|
28
|
+
registerTool: () => undefined,
|
|
29
|
+
registerCommand: () => undefined,
|
|
30
|
+
registerShortcut: () => undefined,
|
|
31
|
+
sendMessage: () => {
|
|
32
|
+
sendAttempts += 1;
|
|
33
|
+
throw new Error("stale extension context");
|
|
34
|
+
},
|
|
35
|
+
appendEntry: () => undefined,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const unhandled: unknown[] = [];
|
|
39
|
+
const onUnhandledRejection = (reason: unknown) => unhandled.push(reason);
|
|
40
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
41
|
+
try {
|
|
42
|
+
piIntercomExtension(pi as never);
|
|
43
|
+
events.emit("subagent:control-intercom", {
|
|
44
|
+
to: "orchestrator",
|
|
45
|
+
message: "delivery races with session replacement",
|
|
46
|
+
});
|
|
47
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
48
|
+
} finally {
|
|
49
|
+
process.off("unhandledRejection", onUnhandledRejection);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
expect(sendAttempts).toBe(1);
|
|
53
|
+
expect(unhandled).toEqual([]);
|
|
54
|
+
} finally {
|
|
55
|
+
if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
|
|
56
|
+
else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
|
|
57
|
+
rmSync(agentDir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -33,10 +33,10 @@ export const GUIDE_FEATURES: readonly GuideFeature[] = [
|
|
|
33
33
|
introducedIn: "0.5.13",
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
id: "workflow-
|
|
36
|
+
id: "workflow-quicktype",
|
|
37
37
|
section: "Start here",
|
|
38
|
-
title: "
|
|
39
|
-
example: '/workflow-
|
|
38
|
+
title: "Quicktype",
|
|
39
|
+
example: '/workflow-quicktype "<goal>"',
|
|
40
40
|
introducedIn: "0.5.13",
|
|
41
41
|
},
|
|
42
42
|
{
|
|
@@ -101,7 +101,7 @@ export const GUIDE_DISMISS_HINT = "Press any key to continue";
|
|
|
101
101
|
|
|
102
102
|
export const GUIDE_COMPACT_LINES = [
|
|
103
103
|
"/guide full · /settings",
|
|
104
|
-
'/workflow-task "<goal>" · /workflow-
|
|
104
|
+
'/workflow-task "<goal>" · /workflow-quicktype "<goal>"',
|
|
105
105
|
'/workflow-prototype "<goal>"',
|
|
106
106
|
'"Ask the researcher to research <topic>."',
|
|
107
107
|
"/skill:* · /undo · /handoff-new <next focus>",
|
|
@@ -653,10 +653,6 @@ export async function openSubagentFleet(ctx: ExtensionContext, state: SubagentSt
|
|
|
653
653
|
try {
|
|
654
654
|
await ctx.ui.custom<undefined>(
|
|
655
655
|
(tui, theme, _keybindings, done) => new SubagentFleetComponent(tui, theme, state, done, options),
|
|
656
|
-
{
|
|
657
|
-
overlay: true,
|
|
658
|
-
overlayOptions: { anchor: "center", width: "95%", minWidth: 60, maxHeight: "85%", margin: 1 },
|
|
659
|
-
},
|
|
660
656
|
);
|
|
661
657
|
} finally {
|
|
662
658
|
state.fleetInspectorOpen = wasOpen;
|
|
@@ -2042,7 +2042,7 @@ describe("subagents-doctor slash command", { skip: !available ? "slash-commands.
|
|
|
2042
2042
|
);
|
|
2043
2043
|
rendered = component.render(100).join("\n");
|
|
2044
2044
|
component.dispose();
|
|
2045
|
-
assert.
|
|
2045
|
+
assert.equal(options, undefined);
|
|
2046
2046
|
return undefined;
|
|
2047
2047
|
};
|
|
2048
2048
|
const pi = {
|
|
@@ -539,7 +539,7 @@ describe("native subagent fleet", () => {
|
|
|
539
539
|
}
|
|
540
540
|
});
|
|
541
541
|
|
|
542
|
-
it("refreshes the roster while the
|
|
542
|
+
it("refreshes the roster while the inspector remains open", async () => {
|
|
543
543
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-fleet-refresh-"));
|
|
544
544
|
try {
|
|
545
545
|
const state = stateForTest();
|
|
@@ -64,6 +64,21 @@ function textFromToolResultContent(content: unknown): string | undefined {
|
|
|
64
64
|
return joined || undefined;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Intercom-enabled foreground subagents replace normal inline tool content
|
|
68
|
+
// with a delivery receipt. Their real inline response remains in the result's
|
|
69
|
+
// output artifact, so the workflow loop must read that before parsing markers.
|
|
70
|
+
async function textFromSubagentResult(event: any): Promise<string | undefined> {
|
|
71
|
+
const inline = textFromToolResultContent(event.content);
|
|
72
|
+
if (inline?.match(LOOP_STATUS_RE)) return inline;
|
|
73
|
+
const outputPath = event.details?.results?.[0]?.artifactPaths?.outputPath;
|
|
74
|
+
if (typeof outputPath !== "string") return inline;
|
|
75
|
+
try {
|
|
76
|
+
return await readFile(outputPath, "utf8");
|
|
77
|
+
} catch {
|
|
78
|
+
return inline;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
// Mutates a workflow subagent invocation to suppress both its configured
|
|
68
83
|
// default output and any caller-provided child output path. `output: false`
|
|
69
84
|
// preserves the normal inline result, which the parent must write explicitly.
|
|
@@ -107,7 +122,7 @@ interface WorkflowController {
|
|
|
107
122
|
|
|
108
123
|
const WORKFLOW_ARTIFACT_TOOL = "write_workflow_artifact";
|
|
109
124
|
const WORKFLOW_CONTROL_MESSAGE = "selesai-workflow-control";
|
|
110
|
-
// ponytail: shared across all module copies so prototype +
|
|
125
|
+
// ponytail: shared across all module copies so prototype + quicktype registration
|
|
111
126
|
// can race. Module-level state alone is not enough when the loader runs each
|
|
112
127
|
// extension in its own module record.
|
|
113
128
|
const WORKFLOW_GLOBAL = Symbol.for("selesai.workflow.registry.v1");
|
|
@@ -284,7 +299,7 @@ async function resumeController(
|
|
|
284
299
|
}
|
|
285
300
|
const ls = controller.loopState;
|
|
286
301
|
continueAgent(pi, ctx, ls.stage === "reviewing"
|
|
287
|
-
? `Resume the loop at review round ${ls.reviewRound + 1}: call the subagent tool now with { agent: "commentator", task: "..." } to review the builder's uncommitted diff against
|
|
302
|
+
? `Resume the loop at review round ${ls.reviewRound + 1}: call the subagent tool now with { agent: "commentator", task: "..." } to review the builder's uncommitted diff against the original goal and parent conversation context. End the review with WORKFLOW_REVIEW_STATUS: clean or WORKFLOW_REVIEW_STATUS: blocking.`
|
|
288
303
|
: `Resume the loop at review round ${ls.reviewRound}: call the subagent tool now with { agent: "builder", task: "..." } to address the feedback in ${sm.snapshot.artifactDir}/${ls.reviewPath ?? "loop-review-<round>.md"}.`);
|
|
289
304
|
} else {
|
|
290
305
|
applyControllerEffect(controller, ctx, current);
|
|
@@ -567,7 +582,7 @@ function controllerForMode(pi: ExtensionAPI, mode: string): WorkflowController |
|
|
|
567
582
|
|
|
568
583
|
function unknownMode(mode: string) {
|
|
569
584
|
return {
|
|
570
|
-
content: [{ type: "text" as const, text: `Unknown workflow mode: ${mode}. Use an installed mode
|
|
585
|
+
content: [{ type: "text" as const, text: `Unknown workflow mode: ${mode}. Use an installed mode.` }],
|
|
571
586
|
details: { rejected: true, mode },
|
|
572
587
|
};
|
|
573
588
|
}
|
|
@@ -628,7 +643,7 @@ function registerSharedWorkflowTools(pi: ExtensionAPI): void {
|
|
|
628
643
|
label: "End Workflow",
|
|
629
644
|
description: "Close a terminal-ready durable workflow. Select its mode.",
|
|
630
645
|
parameters: Type.Object({
|
|
631
|
-
mode: Type.String({ description: "Workflow mode: prototype,
|
|
646
|
+
mode: Type.String({ description: "Workflow mode: prototype, quicktype, task, or another installed mode." }),
|
|
632
647
|
}),
|
|
633
648
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
634
649
|
const controller = controllerForMode(pi, params.mode);
|
|
@@ -748,7 +763,7 @@ export function createWorkflowExtension(
|
|
|
748
763
|
ls.stage = "reviewing";
|
|
749
764
|
} else if (agent === "commentator") {
|
|
750
765
|
const reviewRound = ls.reviewRound + 1;
|
|
751
|
-
const reviewText =
|
|
766
|
+
const reviewText = await textFromSubagentResult(event);
|
|
752
767
|
if (reviewText) {
|
|
753
768
|
const reviewPath = `loop-review-${reviewRound}.md`;
|
|
754
769
|
await mkdir(dir, { recursive: true });
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
import type { ExtensionAPI } from "@selesai/code";
|
|
7
7
|
import { __resetWorkflowRegistryForTests, createWorkflowExtension } from "./adapter.ts";
|
|
8
8
|
import { prototypeMode } from "./modes/prototype.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { quicktypeMode } from "./modes/quicktype.ts";
|
|
10
10
|
import { taskMode } from "./modes/task.ts";
|
|
11
|
+
import { loopMode } from "./modes/loop.ts";
|
|
11
12
|
|
|
12
13
|
export { __resetWorkflowRegistryForTests };
|
|
13
14
|
|
|
14
|
-
const MODES = [prototypeMode,
|
|
15
|
+
const MODES = [prototypeMode, quicktypeMode, taskMode, loopMode] as const;
|
|
15
16
|
|
|
16
17
|
export default function workflowModesExtension(pi: ExtensionAPI): void {
|
|
17
18
|
for (const mode of MODES) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Phase,
|
|
3
|
+
PromptContext,
|
|
4
|
+
WorkflowConfig,
|
|
5
|
+
WorkflowModeRegistration,
|
|
6
|
+
} from "../state-machine.ts";
|
|
7
|
+
import { loopCompleteValidator } from "../validators.ts";
|
|
8
|
+
|
|
9
|
+
// Direct build↔review mode for work whose plan already exists in the parent
|
|
10
|
+
// conversation. The engine persists reviewer feedback and completion; no
|
|
11
|
+
// planning or handoff artifact is needed.
|
|
12
|
+
const phases: Phase[] = ["loop"];
|
|
13
|
+
|
|
14
|
+
const prompts: Partial<Record<Phase, (ctx: PromptContext) => string>> = {
|
|
15
|
+
loop: ({ artifactDir, userPrompt, loopMaxIterations }) =>
|
|
16
|
+
`You are in the LOOP phase of a LOOP workflow. The user and parent already discussed the plan; do not grill, research, create a plan, or create a handoff artifact.
|
|
17
|
+
|
|
18
|
+
Original goal:
|
|
19
|
+
${userPrompt}
|
|
20
|
+
|
|
21
|
+
The workflow ENGINE owns build→review rounds and persists review feedback. Fresh subagents cannot see this conversation, so synthesize the agreed plan, constraints, acceptance criteria, and relevant workspace context into EVERY delegation prompt.
|
|
22
|
+
|
|
23
|
+
1. Call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Direct it to implement the agreed work in the workspace only, run relevant checks, and return an inline completion summary with checks run.
|
|
24
|
+
2. Call the subagent tool with { agent: "commentator", task: "...", output: false }. Give it the same agreed context. Require independent validation: inspect the uncommitted diff, verify agreed acceptance criteria and correctness, run relevant checks where feasible, and report evidence. Its review must end with exactly one machine-readable line:
|
|
25
|
+
WORKFLOW_REVIEW_STATUS: clean
|
|
26
|
+
OR
|
|
27
|
+
WORKFLOW_REVIEW_STATUS: blocking
|
|
28
|
+
3. For blocking feedback, call the builder again with the persisted issues and required fixes. Do not declare success yourself. The engine repeats this for up to ${loopMaxIterations ?? 3} blocking review round(s), then pauses for inspection.
|
|
29
|
+
|
|
30
|
+
When review is clean, the engine writes ${artifactDir}/loop-complete.md and makes the workflow terminal-ready. Do NOT write that file. Verify the result, then call end_workflow with { mode: "loop" } to complete the workflow.`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const config: WorkflowConfig = {
|
|
34
|
+
mode: "loop",
|
|
35
|
+
phases,
|
|
36
|
+
phaseArtifacts: { loop: "loop-complete.md" },
|
|
37
|
+
prompts,
|
|
38
|
+
skipRules: [],
|
|
39
|
+
artifactValidators: { loop: loopCompleteValidator },
|
|
40
|
+
closeValidators: { "loop-complete.md": loopCompleteValidator },
|
|
41
|
+
closeArtifacts: ["loop-complete.md"],
|
|
42
|
+
loopMaxIterations: 3,
|
|
43
|
+
statusKey: "loop",
|
|
44
|
+
entryType: "loop-phase",
|
|
45
|
+
footerLabel: "loop",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const loopMode: WorkflowModeRegistration = {
|
|
49
|
+
config,
|
|
50
|
+
commandName: "workflow-loop",
|
|
51
|
+
commandDescription:
|
|
52
|
+
"Run the direct build↔review workflow for an already-agreed plan",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default loopMode;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Phase,
|
|
3
|
+
PromptContext,
|
|
4
|
+
WorkflowConfig,
|
|
5
|
+
WorkflowModeRegistration,
|
|
6
|
+
} from "../state-machine.ts";
|
|
7
|
+
import {
|
|
8
|
+
handoffValidator,
|
|
9
|
+
loopCompleteValidator,
|
|
10
|
+
planValidator,
|
|
11
|
+
reviewValidator,
|
|
12
|
+
} from "../validators.ts";
|
|
13
|
+
|
|
14
|
+
// ponytail: quicktype workflow — same tool-driven loop as prototype, without research:
|
|
15
|
+
// grill → plan → reuse → handoff → loop → audit.
|
|
16
|
+
// No research phase. skipRules + closeArtifacts wired by the adapter.
|
|
17
|
+
|
|
18
|
+
const phases: Phase[] = [
|
|
19
|
+
"grilling",
|
|
20
|
+
"plan",
|
|
21
|
+
"reuse",
|
|
22
|
+
"handoff",
|
|
23
|
+
"loop",
|
|
24
|
+
"audit",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const prompts: Partial<Record<Phase, (ctx: PromptContext) => string>> = {
|
|
28
|
+
grilling: ({ artifactDir, userPrompt }) =>
|
|
29
|
+
`You are in the GRILLING phase of a QUICKTYPE workflow.
|
|
30
|
+
|
|
31
|
+
User request:
|
|
32
|
+
${userPrompt}
|
|
33
|
+
|
|
34
|
+
Map requirements as a design tree: every decision branches into dependent decisions. Work the tree in rounds. The frontier contains every decision whose prerequisites are settled; do not ask questions that depend on answers still open in the current round.
|
|
35
|
+
|
|
36
|
+
For each round, use one question tool call to ask the ENTIRE frontier. Number every question, give a recommended answer, and wait for the user's answers before the next round. Recompute the frontier after every response. Cover scope, constraints, success criteria, and edge cases; leave no branch silently assumed.
|
|
37
|
+
|
|
38
|
+
Facts are yours to find, never the user's. When a frontier question needs a fact from the workspace or available tools, dispatch a sub-agent to investigate rather than asking the user. Do not block on it: ask other frontier decisions now; defer only decisions that depend on the pending finding.
|
|
39
|
+
|
|
40
|
+
For each question tool call:
|
|
41
|
+
- State all numbered frontier questions concisely, including a recommendation for each.
|
|
42
|
+
- Pass a short context summary when it depends on prior findings.
|
|
43
|
+
- Prefer options when one shared option set fits the whole round. Give each option a clear label and brief description.
|
|
44
|
+
- Always set allowFreeform=true so the user can answer each numbered decision or provide a custom answer.
|
|
45
|
+
- Keep allowMultiple=false unless multiple selections genuinely apply.
|
|
46
|
+
|
|
47
|
+
Only when the frontier is empty, show a concise requirements summary draft and ask exactly: "Approve writing this requirements summary to ${artifactDir}/requirements.md?" This approval confirms shared understanding. Allow "keep grilling" or added details; do not write or otherwise act on the requirements before explicit approval.
|
|
48
|
+
|
|
49
|
+
Artifact target: ${artifactDir}/requirements.md
|
|
50
|
+
- Name that exact full path in the approval question.
|
|
51
|
+
- Do NOT shorten, translate, censor, sanitize, or invent a different path.
|
|
52
|
+
- Only after explicit approval, call write_workflow_artifact with content only. Do not use write.
|
|
53
|
+
|
|
54
|
+
The workflow advances once ${artifactDir}/requirements.md exists.`,
|
|
55
|
+
plan: ({ artifactDir }) =>
|
|
56
|
+
`You are in the PLAN phase.
|
|
57
|
+
|
|
58
|
+
This is a QUICKTYPE workflow — no separate research phase. Use ${artifactDir}/requirements.md to produce the concrete build plan: what to build, how, in order, components, and what the finished prototype looks like.
|
|
59
|
+
|
|
60
|
+
Call the subagent tool with { agent: "architect", task: "...", output: false } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md so the architect plans for THIS task. It must return the complete plan inline; do not tell it to write any artifact.
|
|
61
|
+
|
|
62
|
+
Inspect that result, verify it ends with exactly one machine-readable line on its own:
|
|
63
|
+
WORKFLOW_PLAN_STATUS: ready
|
|
64
|
+
Then immediately call write_workflow_artifact with the complete validated plan as content. Only that parent tool call writes ${artifactDir}/plan.md and advances the workflow.`,
|
|
65
|
+
reuse: ({ artifactDir }) =>
|
|
66
|
+
`You are in the REUSE phase (optional).
|
|
67
|
+
|
|
68
|
+
Decide whether codebase exploration is useful. Skip if the project is empty, the task creates something wholly new, or there is clearly nothing to reuse.
|
|
69
|
+
|
|
70
|
+
If unsure, ask the user one focused question: "Should I explore the existing codebase for reusable patterns before implementing?" Then follow their answer.
|
|
71
|
+
|
|
72
|
+
If YES (or user confirms): call the subagent tool with { agent: "explorer", task: "...", output: false } (do NOT pass a model parameter). Craft the task from ${artifactDir}/requirements.md and ${artifactDir}/plan.md pointing it at relevant areas, patterns, and dependencies. It must return the reuse findings inline; do not tell it to write any artifact. Immediately call write_workflow_artifact with those findings as content for ${artifactDir}/reuse.md.
|
|
73
|
+
|
|
74
|
+
If NO: call write_workflow_artifact with a brief skip note explaining why.
|
|
75
|
+
|
|
76
|
+
The workflow advances once ${artifactDir}/reuse.md exists.`,
|
|
77
|
+
handoff: ({ artifactDir }) =>
|
|
78
|
+
`You are in the HANDOFF phase.
|
|
79
|
+
|
|
80
|
+
Compile a self-contained handoff document so loop-phase sub-agents understand full project context without re-grilling.
|
|
81
|
+
|
|
82
|
+
Draw from all prior phases:
|
|
83
|
+
- ${artifactDir}/requirements.md
|
|
84
|
+
- ${artifactDir}/plan.md
|
|
85
|
+
- ${artifactDir}/reuse.md
|
|
86
|
+
|
|
87
|
+
Call the subagent tool with { agent: "recapper", task: "...", output: false } (do NOT pass a model parameter). Craft the task pointing the recapper at all three artifact files and telling it what the prototype is about. It must return the complete handoff inline; do not tell it to write any artifact.
|
|
88
|
+
|
|
89
|
+
Inspect that result, verify it ends with exactly one machine-readable line on its own:
|
|
90
|
+
WORKFLOW_HANDOFF_STATUS: ready
|
|
91
|
+
Then immediately call write_workflow_artifact with the complete validated handoff as content. Only that parent tool call writes ${artifactDir}/handoff.md and advances the workflow.`,
|
|
92
|
+
loop: ({ artifactDir, loopMaxIterations }) =>
|
|
93
|
+
`You are in the LOOP (orchestration) phase. The workflow ENGINE owns the implement→review loop — you do NOT track iterations or decide when the loop is clean.
|
|
94
|
+
|
|
95
|
+
Use read to inspect ${artifactDir}/plan.md, ${artifactDir}/handoff.md, and ${artifactDir}/reuse.md. Using that context, GENERATE YOUR OWN delegation prompt:
|
|
96
|
+
|
|
97
|
+
Call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Give it plan + handoff + reuse context, tailored to this task. Instruct it to implement every task in plan.md in order. All code changes go in the workspace, never in ${artifactDir}. The builder must return its completion summary inline.
|
|
98
|
+
|
|
99
|
+
After the builder returns, call the subagent tool with { agent: "commentator", task: "...", output: false }. Craft the review task YOURSELF. The commentator must return its review inline. Each commentator review MUST end with exactly one machine-readable line:
|
|
100
|
+
WORKFLOW_REVIEW_STATUS: clean
|
|
101
|
+
OR
|
|
102
|
+
WORKFLOW_REVIEW_STATUS: blocking
|
|
103
|
+
|
|
104
|
+
If a review is blocking, call the builder again with the recorded issues. This repeats up to ${loopMaxIterations ?? 3} round(s). When a review is clean, the engine writes loop-complete.md and advances to audit. Do NOT write loop-complete.md yourself.`,
|
|
105
|
+
audit: ({ artifactDir }) =>
|
|
106
|
+
`You are in the AUDIT (review) phase.
|
|
107
|
+
|
|
108
|
+
Review uncommitted changes for correctness, plan adherence, and over-engineering. Use ponytail-review style: cut bloat, unnecessary abstractions, dead flexibility, and reinvented stdlib/native behavior.
|
|
109
|
+
|
|
110
|
+
1. Call the subagent tool with { agent: "commentator", task: "...", output: false } (do NOT pass a model parameter). Craft the task from the full uncommitted diff and ${artifactDir}/plan.md. It must return the review inline; do not tell it to write any artifact. Verify the review ends with exactly one machine-readable line, WORKFLOW_REVIEW_STATUS: clean or WORKFLOW_REVIEW_STATUS: blocking, then immediately call write_workflow_artifact with that review as content for ${artifactDir}/review.md.
|
|
111
|
+
2. If that review lists actionable issues, call the subagent tool with { agent: "builder", task: "...", output: false } (do NOT pass a model parameter). Instruct it to fix every issue in the workspace, never in ${artifactDir}, then return its completion summary inline.
|
|
112
|
+
3. Re-run the commentator and overwrite the parent-owned review artifact through write_workflow_artifact until it ends with WORKFLOW_REVIEW_STATUS: clean.
|
|
113
|
+
|
|
114
|
+
Once ${artifactDir}/review.md exists and ends with the WORKFLOW_REVIEW_STATUS: clean marker, call end_workflow with { mode: "quicktype" } to complete the workflow.`,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const config: WorkflowConfig = {
|
|
118
|
+
mode: "quicktype",
|
|
119
|
+
phases,
|
|
120
|
+
phaseArtifacts: {
|
|
121
|
+
grilling: "requirements.md",
|
|
122
|
+
plan: "plan.md",
|
|
123
|
+
reuse: "reuse.md",
|
|
124
|
+
handoff: "handoff.md",
|
|
125
|
+
loop: "loop-complete.md",
|
|
126
|
+
audit: "review.md",
|
|
127
|
+
},
|
|
128
|
+
prompts,
|
|
129
|
+
// ponytail: Plan 4 — semantic gates for the critical phases.
|
|
130
|
+
artifactValidators: {
|
|
131
|
+
plan: planValidator,
|
|
132
|
+
handoff: handoffValidator,
|
|
133
|
+
loop: loopCompleteValidator,
|
|
134
|
+
},
|
|
135
|
+
closeValidators: { "review.md": reviewValidator },
|
|
136
|
+
closeArtifacts: ["review.md"],
|
|
137
|
+
loopMaxIterations: 3,
|
|
138
|
+
statusKey: "quicktype",
|
|
139
|
+
entryType: "quicktype-phase",
|
|
140
|
+
footerLabel: "quicktype",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const quicktypeMode: WorkflowModeRegistration = {
|
|
144
|
+
config,
|
|
145
|
+
commandName: "workflow-quicktype",
|
|
146
|
+
commandDescription:
|
|
147
|
+
"Run the quicktype workflow (grill → plan → reuse → handoff → loop → audit)",
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default quicktypeMode;
|
package/dist/modes/index.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export { InteractiveMode, type InteractiveModeOptions } from "./interactive/inte
|
|
|
5
5
|
export { type PrintModeOptions, runPrintMode } from "./print-mode.ts";
|
|
6
6
|
export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client.ts";
|
|
7
7
|
export { runRpcMode } from "./rpc/rpc-mode.ts";
|
|
8
|
-
export type { RpcCommand, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcResponse, RpcSessionState, PrototypePhase,
|
|
8
|
+
export type { RpcCommand, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcResponse, RpcSessionState, PrototypePhase, QuicktypePhase, WorkflowState, } from "./rpc/rpc-types.ts";
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/modes/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AACjG,OAAO,EAAE,KAAK,gBAAgB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,YAAY,EACX,UAAU,EACV,qBAAqB,EACrB,sBAAsB,EACtB,WAAW,EACX,eAAe,EACf,cAAc,EACd,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/modes/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AACjG,OAAO,EAAE,KAAK,gBAAgB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,YAAY,EACX,UAAU,EACV,qBAAqB,EACrB,sBAAsB,EACtB,WAAW,EACX,eAAe,EACf,cAAc,EACd,cAAc,EACd,aAAa,GACb,MAAM,oBAAoB,CAAC","sourcesContent":["/**\n * Run modes for the coding agent.\n */\n\nexport { InteractiveMode, type InteractiveModeOptions } from \"./interactive/interactive-mode.ts\";\nexport { type PrintModeOptions, runPrintMode } from \"./print-mode.ts\";\nexport { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from \"./rpc/rpc-client.ts\";\nexport { runRpcMode } from \"./rpc/rpc-mode.ts\";\nexport type {\n\tRpcCommand,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcResponse,\n\tRpcSessionState,\n\tPrototypePhase,\n\tQuicktypePhase,\n\tWorkflowState,\n} from \"./rpc/rpc-types.ts\";\n"]}
|
package/dist/modes/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/modes/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAA+B,MAAM,mCAAmC,CAAC;AACjG,OAAO,EAAyB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAkB,SAAS,EAAgD,MAAM,qBAAqB,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["/**\n * Run modes for the coding agent.\n */\n\nexport { InteractiveMode, type InteractiveModeOptions } from \"./interactive/interactive-mode.ts\";\nexport { type PrintModeOptions, runPrintMode } from \"./print-mode.ts\";\nexport { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from \"./rpc/rpc-client.ts\";\nexport { runRpcMode } from \"./rpc/rpc-mode.ts\";\nexport type {\n\tRpcCommand,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcResponse,\n\tRpcSessionState,\n\tPrototypePhase,\n\
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/modes/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAA+B,MAAM,mCAAmC,CAAC;AACjG,OAAO,EAAyB,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAkB,SAAS,EAAgD,MAAM,qBAAqB,CAAC;AAC9G,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC","sourcesContent":["/**\n * Run modes for the coding agent.\n */\n\nexport { InteractiveMode, type InteractiveModeOptions } from \"./interactive/interactive-mode.ts\";\nexport { type PrintModeOptions, runPrintMode } from \"./print-mode.ts\";\nexport { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from \"./rpc/rpc-client.ts\";\nexport { runRpcMode } from \"./rpc/rpc-mode.ts\";\nexport type {\n\tRpcCommand,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcResponse,\n\tRpcSessionState,\n\tPrototypePhase,\n\tQuicktypePhase,\n\tWorkflowState,\n} from \"./rpc/rpc-types.ts\";\n"]}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
13
|
import type { AgentSessionRuntime } from "../../core/agent-session-runtime.ts";
|
|
14
|
-
export type { RpcCommand, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcResponse, RpcSessionState, RpcSlashCommand, PrototypePhase,
|
|
14
|
+
export type { RpcCommand, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcResponse, RpcSessionState, RpcSlashCommand, PrototypePhase, QuicktypePhase, WorkflowState, } from "./rpc-types.ts";
|
|
15
15
|
/**
|
|
16
16
|
* Run in RPC mode.
|
|
17
17
|
* Listens for JSON commands on stdin, outputs events and responses on stdout.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-mode.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AA4B/E,YAAY,EACX,UAAU,EACV,qBAAqB,EACrB,sBAAsB,EACtB,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,UAAU,EACV,aAAa,GACb,MAAM,gBAAgB,CAAC;AAExB;;;GAGG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,mBAAmB,GAAG,OAAO,CAAC,KAAK,CAAC,CAkxBjF","sourcesContent":["/**\n * RPC mode: Headless operation with JSON stdin/stdout protocol.\n *\n * Used for embedding the agent in other applications.\n * Receives commands as JSON on stdin, outputs events and responses as JSON on stdout.\n *\n * Protocol:\n * - Commands: JSON objects with `type` field, optional `id` for correlation\n * - Responses: JSON objects with `type: \"response\"`, `command`, `success`, and optional `data`/`error`\n * - Events: AgentSessionEvent objects streamed as they occur\n * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response\n */\n\nimport * as crypto from \"node:crypto\";\nimport type { AgentSessionRuntime } from \"../../core/agent-session-runtime.ts\";\nimport type {\n\tExtensionUIContext,\n\tExtensionUIDialogOptions,\n\tExtensionWidgetOptions,\n\tWorkingIndicatorOptions,\n} from \"../../core/extensions/index.ts\";\nimport {\n\tflushRawStdout,\n\ttakeOverStdout,\n\twaitForRawStdoutBackpressure,\n\twriteRawStdout,\n} from \"../../core/output-guard.ts\";\nimport { killTrackedDetachedChildren } from \"../../utils/shell.ts\";\nimport { type Theme, theme } from \"../interactive/theme/theme.ts\";\nimport { attachJsonlLineReader, serializeJsonLine } from \"./jsonl.ts\";\nimport type {\n\tRpcCommand,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcResponse,\n\tRpcSessionState,\n\tRpcSlashCommand,\n\t\t\t\tPrototypePhase,\n\t\t\t\tQuickPhase,\n} from \"./rpc-types.ts\";\n\n// Re-export types for consumers\nexport type {\n\tRpcCommand,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcResponse,\n\tRpcSessionState,\n\tRpcSlashCommand,\n\tPrototypePhase,\n\tQuickPhase,\n\tWorkflowState,\n} from \"./rpc-types.ts\";\n\n/**\n * Run in RPC mode.\n * Listens for JSON commands on stdin, outputs events and responses on stdout.\n */\nexport async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<never> {\n\ttakeOverStdout();\n\tlet session = runtimeHost.session;\n\tlet unsubscribe: (() => void) | undefined;\n\tlet unsubscribeBackpressure: (() => void) | undefined;\n\n\tconst output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {\n\t\twriteRawStdout(serializeJsonLine(obj));\n\t};\n\n\tconst success = <T extends RpcCommand[\"type\"]>(\n\t\tid: string | undefined,\n\t\tcommand: T,\n\t\tdata?: object | null,\n\t): RpcResponse => {\n\t\tif (data === undefined) {\n\t\t\treturn { id, type: \"response\", command, success: true } as RpcResponse;\n\t\t}\n\t\treturn { id, type: \"response\", command, success: true, data } as RpcResponse;\n\t};\n\n\tconst error = (id: string | undefined, command: string, message: string): RpcResponse => {\n\t\treturn { id, type: \"response\", command, success: false, error: message };\n\t};\n\n\t// Pending extension UI requests waiting for response\n\tconst pendingExtensionRequests = new Map<\n\t\tstring,\n\t\t{ resolve: (value: any) => void; reject: (error: Error) => void }\n\t>();\n\n\t// Shutdown request flag\n\tlet shutdownRequested = false;\n\tlet shuttingDown = false;\n\tconst signalCleanupHandlers: Array<() => void> = [];\n\n\t/** Helper for dialog methods with signal/timeout support */\n\tfunction createDialogPromise<T>(\n\t\topts: ExtensionUIDialogOptions | undefined,\n\t\tdefaultValue: T,\n\t\trequest: Record<string, unknown>,\n\t\tparseResponse: (response: RpcExtensionUIResponse) => T,\n\t): Promise<T> {\n\t\tif (opts?.signal?.aborted) return Promise.resolve(defaultValue);\n\n\t\tconst id = crypto.randomUUID();\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\tpendingExtensionRequests.delete(id);\n\t\t\t};\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(defaultValue);\n\t\t\t};\n\t\t\topts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tif (opts?.timeout) {\n\t\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(defaultValue);\n\t\t\t\t}, opts.timeout);\n\t\t\t}\n\n\t\t\tpendingExtensionRequests.set(id, {\n\t\t\t\tresolve: (response: RpcExtensionUIResponse) => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(parseResponse(response));\n\t\t\t\t},\n\t\t\t\treject,\n\t\t\t});\n\t\t\toutput({ type: \"extension_ui_request\", id, ...request } as RpcExtensionUIRequest);\n\t\t});\n\t}\n\n\t/**\n\t * Create an extension UI context that uses the RPC protocol.\n\t */\n\tconst createExtensionUIContext = (): ExtensionUIContext => ({\n\t\tselect: (title, options, opts) =>\n\t\t\tcreateDialogPromise(opts, undefined, { method: \"select\", title, options, timeout: opts?.timeout }, (r) =>\n\t\t\t\t\"cancelled\" in r && r.cancelled ? undefined : \"value\" in r ? r.value : undefined,\n\t\t\t),\n\n\t\tconfirm: (title, message, opts) =>\n\t\t\tcreateDialogPromise(opts, false, { method: \"confirm\", title, message, timeout: opts?.timeout }, (r) =>\n\t\t\t\t\"cancelled\" in r && r.cancelled ? false : \"confirmed\" in r ? r.confirmed : false,\n\t\t\t),\n\n\t\tinput: (title, placeholder, opts) =>\n\t\t\tcreateDialogPromise(opts, undefined, { method: \"input\", title, placeholder, timeout: opts?.timeout }, (r) =>\n\t\t\t\t\"cancelled\" in r && r.cancelled ? undefined : \"value\" in r ? r.value : undefined,\n\t\t\t),\n\n\t\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void {\n\t\t\t// Fire and forget - no response needed\n\t\t\toutput({\n\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\tmethod: \"notify\",\n\t\t\t\tmessage,\n\t\t\t\tnotifyType: type,\n\t\t\t} as RpcExtensionUIRequest);\n\t\t},\n\n\t\tonTerminalInput(): () => void {\n\t\t\t// Raw terminal input not supported in RPC mode\n\t\t\treturn () => {};\n\t\t},\n\n\t\tsetStatus(key: string, text: string | undefined): void {\n\t\t\t// Fire and forget - no response needed\n\t\t\toutput({\n\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\tmethod: \"setStatus\",\n\t\t\t\tstatusKey: key,\n\t\t\t\tstatusText: text,\n\t\t\t} as RpcExtensionUIRequest);\n\t\t},\n\n\t\tsetWorkingMessage(_message?: string): void {\n\t\t\t// Working message not supported in RPC mode - requires TUI loader access\n\t\t},\n\n\t\tsetWorkingVisible(_visible: boolean): void {\n\t\t\t// Working visibility not supported in RPC mode - requires TUI loader access\n\t\t},\n\n\t\tsetWorkingIndicator(_options?: WorkingIndicatorOptions): void {\n\t\t\t// Working indicator customization not supported in RPC mode - requires TUI loader access\n\t\t},\n\n\t\tsetHiddenThinkingLabel(_label?: string): void {\n\t\t\t// Hidden thinking label not supported in RPC mode - requires TUI message rendering access\n\t\t},\n\n\t\tsetWidget(key: string, content: unknown, options?: ExtensionWidgetOptions): void {\n\t\t\t// Only support string arrays in RPC mode - factory functions are ignored\n\t\t\tif (content === undefined || Array.isArray(content)) {\n\t\t\t\toutput({\n\t\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\t\tmethod: \"setWidget\",\n\t\t\t\t\twidgetKey: key,\n\t\t\t\t\twidgetLines: content as string[] | undefined,\n\t\t\t\t\twidgetPlacement: options?.placement,\n\t\t\t\t} as RpcExtensionUIRequest);\n\t\t\t}\n\t\t\t// Component factories are not supported in RPC mode - would need TUI access\n\t\t},\n\n\t\tsetFooter(_factory: unknown): void {\n\t\t\t// Custom footer not supported in RPC mode - requires TUI access\n\t\t},\n\n\t\tsetHeader(_factory: unknown): void {\n\t\t\t// Custom header not supported in RPC mode - requires TUI access\n\t\t},\n\n\t\tsetTitle(title: string): void {\n\t\t\t// Fire and forget - host can implement terminal title control\n\t\t\toutput({\n\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\tmethod: \"setTitle\",\n\t\t\t\ttitle,\n\t\t\t} as RpcExtensionUIRequest);\n\t\t},\n\n\t\tasync custom() {\n\t\t\t// Custom UI not supported in RPC mode\n\t\t\treturn undefined as never;\n\t\t},\n\n\t\tpasteToEditor(text: string): void {\n\t\t\t// Paste handling not supported in RPC mode - falls back to setEditorText\n\t\t\tthis.setEditorText(text);\n\t\t},\n\n\t\tsetEditorText(text: string): void {\n\t\t\t// Fire and forget - host can implement editor control\n\t\t\toutput({\n\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\tmethod: \"set_editor_text\",\n\t\t\t\ttext,\n\t\t\t} as RpcExtensionUIRequest);\n\t\t},\n\n\t\tgetEditorText(): string {\n\t\t\t// Synchronous method can't wait for RPC response\n\t\t\t// Host should track editor state locally if needed\n\t\t\treturn \"\";\n\t\t},\n\n\t\tasync editor(title: string, prefill?: string): Promise<string | undefined> {\n\t\t\tconst id = crypto.randomUUID();\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tpendingExtensionRequests.set(id, {\n\t\t\t\t\tresolve: (response: RpcExtensionUIResponse) => {\n\t\t\t\t\t\tif (\"cancelled\" in response && response.cancelled) {\n\t\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\t} else if (\"value\" in response) {\n\t\t\t\t\t\t\tresolve(response.value);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\treject,\n\t\t\t\t});\n\t\t\t\toutput({ type: \"extension_ui_request\", id, method: \"editor\", title, prefill } as RpcExtensionUIRequest);\n\t\t\t});\n\t\t},\n\n\t\taddAutocompleteProvider(): void {\n\t\t\t// Autocomplete provider composition is not supported in RPC mode\n\t\t},\n\n\t\tsetEditorComponent(): void {\n\t\t\t// Custom editor components not supported in RPC mode\n\t\t},\n\n\t\tgetEditorComponent() {\n\t\t\t// Custom editor components not supported in RPC mode\n\t\t\treturn undefined;\n\t\t},\n\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\n\t\tgetAllThemes() {\n\t\t\treturn [];\n\t\t},\n\n\t\tgetTheme(_name: string) {\n\t\t\treturn undefined;\n\t\t},\n\n\t\tsetTheme(_theme: string | Theme) {\n\t\t\t// Theme switching not supported in RPC mode\n\t\t\treturn { success: false, error: \"Theme switching not supported in RPC mode\" };\n\t\t},\n\n\t\tgetToolsExpanded() {\n\t\t\t// Tool expansion not supported in RPC mode - no TUI\n\t\t\treturn false;\n\t\t},\n\n\t\tsetToolsExpanded(_expanded: boolean) {\n\t\t\t// Tool expansion not supported in RPC mode - no TUI\n\t\t},\n\t});\n\n\truntimeHost.setRebindSession(async () => {\n\t\tawait rebindSession();\n\t});\n\n\tconst rebindSession = async (): Promise<void> => {\n\t\tsession = runtimeHost.session;\n\t\tawait session.bindExtensions({\n\t\t\tuiContext: createExtensionUIContext(),\n\t\t\tmode: \"rpc\",\n\t\t\tcommandContextActions: {\n\t\t\t\twaitForIdle: () => session.waitForIdle(),\n\t\t\t\tnewSession: async (options) => runtimeHost.newSession(options),\n\t\t\t\tfork: async (entryId, forkOptions) => {\n\t\t\t\t\tconst result = await runtimeHost.fork(entryId, forkOptions);\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tnavigateTree: async (targetId, options) => {\n\t\t\t\t\tconst result = await session.navigateTree(targetId, {\n\t\t\t\t\t\tsummarize: options?.summarize,\n\t\t\t\t\t\tcustomInstructions: options?.customInstructions,\n\t\t\t\t\t\treplaceInstructions: options?.replaceInstructions,\n\t\t\t\t\t\tlabel: options?.label,\n\t\t\t\t\t});\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tswitchSession: async (sessionPath, options) => {\n\t\t\t\t\treturn runtimeHost.switchSession(sessionPath, options);\n\t\t\t\t},\n\t\t\t\treload: async () => {\n\t\t\t\t\tawait session.reload();\n\t\t\t\t},\n\t\t\t},\n\t\t\tshutdownHandler: () => {\n\t\t\t\tshutdownRequested = true;\n\t\t\t},\n\t\t\tonError: (err) => {\n\t\t\t\toutput({ type: \"extension_error\", extensionPath: err.extensionPath, event: err.event, error: err.error });\n\t\t\t},\n\t\t});\n\n\t\tunsubscribe?.();\n\t\tunsubscribeBackpressure?.();\n\t\tunsubscribe = session.subscribe((event) => {\n\t\t\toutput(event);\n\t\t\tif (event.type === \"agent_settled\") {\n\t\t\t\tvoid checkShutdownRequested();\n\t\t\t}\n\t\t});\n\t\tunsubscribeBackpressure = session.agent.subscribe(async () => {\n\t\t\tawait waitForRawStdoutBackpressure();\n\t\t});\n\t};\n\n\tconst registerSignalHandlers = (): void => {\n\t\tconst signals: NodeJS.Signals[] = [\"SIGTERM\"];\n\t\tif (process.platform !== \"win32\") {\n\t\t\tsignals.push(\"SIGHUP\");\n\t\t}\n\n\t\tfor (const signal of signals) {\n\t\t\tconst handler = () => {\n\t\t\t\tkillTrackedDetachedChildren();\n\t\t\t\tvoid shutdown(signal === \"SIGHUP\" ? 129 : 143, signal);\n\t\t\t};\n\t\t\tprocess.on(signal, handler);\n\t\t\tsignalCleanupHandlers.push(() => process.off(signal, handler));\n\t\t}\n\t};\n\n\tawait rebindSession();\n\tregisterSignalHandlers();\n\n\t// Handle a single command\n\tconst handleCommand = async (command: RpcCommand): Promise<RpcResponse | undefined> => {\n\t\tconst id = command.id;\n\n\t\tswitch (command.type) {\n\t\t\t// =================================================================\n\t\t\t// Prompting\n\t\t\t// =================================================================\n\n\t\t\tcase \"prompt\": {\n\t\t\t\t// Start prompt handling immediately, but emit the authoritative response only after\n\t\t\t\t// prompt preflight succeeds. Queued and immediately handled prompts also count as success.\n\t\t\t\tlet preflightSucceeded = false;\n\t\t\t\tvoid session\n\t\t\t\t\t.prompt(command.message, {\n\t\t\t\t\t\timages: command.images,\n\t\t\t\t\t\tstreamingBehavior: command.streamingBehavior,\n\t\t\t\t\t\tsource: \"rpc\",\n\t\t\t\t\t\tpreflightResult: (didSucceed) => {\n\t\t\t\t\t\t\tif (didSucceed) {\n\t\t\t\t\t\t\t\tpreflightSucceeded = true;\n\t\t\t\t\t\t\t\toutput(success(id, \"prompt\"));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\t.catch((e) => {\n\t\t\t\t\t\tif (!preflightSucceeded) {\n\t\t\t\t\t\t\toutput(error(id, \"prompt\", e.message));\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tcase \"steer\": {\n\t\t\t\tawait session.steer(command.message, command.images);\n\t\t\t\treturn success(id, \"steer\");\n\t\t\t}\n\n\t\t\tcase \"follow_up\": {\n\t\t\t\tawait session.followUp(command.message, command.images);\n\t\t\t\treturn success(id, \"follow_up\");\n\t\t\t}\n\n\t\t\tcase \"abort\": {\n\t\t\t\tawait session.abort();\n\t\t\t\treturn success(id, \"abort\");\n\t\t\t}\n\n\t\t\tcase \"new_session\": {\n\t\t\t\tconst options = command.parentSession ? { parentSession: command.parentSession } : undefined;\n\t\t\t\tconst result = await runtimeHost.newSession(options);\n\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\tawait rebindSession();\n\t\t\t\t}\n\t\t\t\treturn success(id, \"new_session\", result);\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// State\n\t\t\t// =================================================================\n\n\t\t\tcase \"get_state\": {\n\t\t\t\tconst state: RpcSessionState = {\n\t\t\t\t\tmodel: session.model,\n\t\t\t\t\tthinkingLevel: session.thinkingLevel,\n\t\t\t\t\tisStreaming: session.isStreaming,\n\t\t\t\t\tisCompacting: session.isCompacting,\n\t\t\t\t\tsteeringMode: session.steeringMode,\n\t\t\t\t\tfollowUpMode: session.followUpMode,\n\t\t\t\t\tsessionFile: session.sessionFile,\n\t\t\t\t\tsessionId: session.sessionId,\n\t\t\t\t\tsessionName: session.sessionName,\n\t\t\t\t\tautoCompactionEnabled: session.autoCompactionEnabled,\n\t\t\t\t\tautoHandoffEnabled: session.autoHandoffEnabled,\n\t\t\t\t\tautoHandoffThresholdTokens: session.autoHandoffThresholdTokens,\n\t\t\t\t\tmessageCount: session.messages.length,\n\t\t\t\t\tpendingMessageCount: session.pendingMessageCount,\n\t\t\t\t};\n\n\t\t\t\t// Attach workflow state from latest workflow-phase entry, if active.\n\t\t\t\t// ponytail: linear scan of entries; workflow is short-lived so O(n) per get_state is fine.\n\t\t\t\ttry {\n\t\t\t\t\tconst entries = session.sessionManager.getEntries();\n\t\t\t\t\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\t\t\t\t\tconst e = entries[i] as { type: string; customType?: string; data?: unknown };\n\t\t\t\t\t\tconst customType = e.customType;\n\t\t\t\t\t\tif (e.type === \"custom\" && (customType === \"prototype-phase\" || customType === \"quick-phase\") && e.data) {\n\t\t\t\t\t\t\tconst d = e.data as { phase?: string; step?: number; done?: boolean };\n\t\t\t\t\t\t\tif (d && typeof d.phase === \"string\" && typeof d.step === \"number\" && !d.done) {\n\t\t\t\t\t\t\t\tstate.workflow = {\n\t\t\t\t\t\t\t\t\tmode: customType === \"prototype-phase\" ? \"prototype\" : \"quick\",\n\t\t\t\t\t\t\t\t\tphase: d.phase as PrototypePhase | QuickPhase,\n\t\t\t\t\t\t\t\t\tstep: d.step,\n\t\t\t\t\t\t\t\t\tdone: false,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch { /* corrupt entry must not crash get_state */ }\n\n\t\t\t\treturn success(id, \"get_state\", state);\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Model\n\t\t\t// =================================================================\n\n\t\t\tcase \"set_model\": {\n\t\t\t\tconst models = await session.modelRuntime.getAvailable();\n\t\t\t\tconst model = models.find((m) => m.provider === command.provider && m.id === command.modelId);\n\t\t\t\tif (!model) {\n\t\t\t\t\treturn error(id, \"set_model\", `Model not found: ${command.provider}/${command.modelId}`);\n\t\t\t\t}\n\t\t\t\tawait session.setModel(model);\n\t\t\t\treturn success(id, \"set_model\", model);\n\t\t\t}\n\n\t\t\tcase \"cycle_model\": {\n\t\t\t\tconst result = await session.cycleModel();\n\t\t\t\tif (!result) {\n\t\t\t\t\treturn success(id, \"cycle_model\", null);\n\t\t\t\t}\n\t\t\t\treturn success(id, \"cycle_model\", result);\n\t\t\t}\n\n\t\t\tcase \"get_available_models\": {\n\t\t\t\tconst models = await session.modelRuntime.getAvailable();\n\t\t\t\treturn success(id, \"get_available_models\", { models });\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Thinking\n\t\t\t// =================================================================\n\n\t\t\tcase \"set_thinking_level\": {\n\t\t\t\tsession.setThinkingLevel(command.level);\n\t\t\t\treturn success(id, \"set_thinking_level\");\n\t\t\t}\n\n\t\t\tcase \"cycle_thinking_level\": {\n\t\t\t\tconst level = session.cycleThinkingLevel();\n\t\t\t\tif (!level) {\n\t\t\t\t\treturn success(id, \"cycle_thinking_level\", null);\n\t\t\t\t}\n\t\t\t\treturn success(id, \"cycle_thinking_level\", { level });\n\t\t\t}\n\n\t\t\tcase \"get_available_thinking_levels\": {\n\t\t\t\tconst levels = session.getAvailableThinkingLevels();\n\t\t\t\treturn success(id, \"get_available_thinking_levels\", { levels });\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Queue Modes\n\t\t\t// =================================================================\n\n\t\t\tcase \"set_steering_mode\": {\n\t\t\t\tsession.setSteeringMode(command.mode);\n\t\t\t\treturn success(id, \"set_steering_mode\");\n\t\t\t}\n\n\t\t\tcase \"set_follow_up_mode\": {\n\t\t\t\tsession.setFollowUpMode(command.mode);\n\t\t\t\treturn success(id, \"set_follow_up_mode\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Compaction\n\t\t\t// =================================================================\n\n\t\t\tcase \"compact\": {\n\t\t\t\tconst result = await session.compact(command.customInstructions);\n\t\t\t\treturn success(id, \"compact\", result);\n\t\t\t}\n\n\t\t\tcase \"set_auto_compaction\": {\n\t\t\t\tsession.setAutoCompactionEnabled(command.enabled);\n\t\t\t\treturn success(id, \"set_auto_compaction\");\n\t\t\t}\n\n\t\t\tcase \"set_auto_handoff\": {\n\t\t\t\tsession.settingsManager.setAutoHandoffEnabled(command.enabled);\n\t\t\t\treturn success(id, \"set_auto_handoff\");\n\t\t\t}\n\n\t\t\tcase \"set_auto_handoff_threshold\": {\n\t\t\t\tconst tokens = command.tokens;\n\t\t\t\tif (!Number.isFinite(tokens) || tokens < 1000) {\n\t\t\t\t\treturn error(id, \"set_auto_handoff_threshold\", \"Threshold must be at least 1000 tokens\");\n\t\t\t\t}\n\t\t\t\tsession.settingsManager.setAutoHandoffThresholdTokens(tokens);\n\t\t\t\treturn success(id, \"set_auto_handoff_threshold\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Retry\n\t\t\t// =================================================================\n\n\t\t\tcase \"set_auto_retry\": {\n\t\t\t\tsession.setAutoRetryEnabled(command.enabled);\n\t\t\t\treturn success(id, \"set_auto_retry\");\n\t\t\t}\n\n\t\t\tcase \"abort_retry\": {\n\t\t\t\tsession.abortRetry();\n\t\t\t\treturn success(id, \"abort_retry\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Bash\n\t\t\t// =================================================================\n\n\t\t\tcase \"bash\": {\n\t\t\t\tconst result = await session.executeBash(command.command, undefined, {\n\t\t\t\t\texcludeFromContext: command.excludeFromContext,\n\t\t\t\t});\n\t\t\t\treturn success(id, \"bash\", result);\n\t\t\t}\n\n\t\t\tcase \"abort_bash\": {\n\t\t\t\tsession.abortBash();\n\t\t\t\treturn success(id, \"abort_bash\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Session\n\t\t\t// =================================================================\n\n\t\t\tcase \"get_session_stats\": {\n\t\t\t\tconst stats = session.getSessionStats();\n\t\t\t\treturn success(id, \"get_session_stats\", stats);\n\t\t\t}\n\n\t\t\tcase \"export_html\": {\n\t\t\t\tconst path = await session.exportToHtml(command.outputPath);\n\t\t\t\treturn success(id, \"export_html\", { path });\n\t\t\t}\n\n\t\t\tcase \"switch_session\": {\n\t\t\t\tconst result = await runtimeHost.switchSession(command.sessionPath);\n\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\tawait rebindSession();\n\t\t\t\t}\n\t\t\t\treturn success(id, \"switch_session\", result);\n\t\t\t}\n\n\t\t\tcase \"fork\": {\n\t\t\t\tconst result = await runtimeHost.fork(command.entryId);\n\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\tawait rebindSession();\n\t\t\t\t}\n\t\t\t\treturn success(id, \"fork\", { text: result.selectedText, cancelled: result.cancelled });\n\t\t\t}\n\n\t\t\tcase \"clone\": {\n\t\t\t\tconst leafId = session.sessionManager.getLeafId();\n\t\t\t\tif (!leafId) {\n\t\t\t\t\treturn error(id, \"clone\", \"Cannot clone session: no current entry selected\");\n\t\t\t\t}\n\t\t\t\tconst result = await runtimeHost.fork(leafId, { position: \"at\" });\n\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\tawait rebindSession();\n\t\t\t\t}\n\t\t\t\treturn success(id, \"clone\", { cancelled: result.cancelled });\n\t\t\t}\n\n\t\t\tcase \"get_fork_messages\": {\n\t\t\t\tconst messages = session.getUserMessagesForForking();\n\t\t\t\treturn success(id, \"get_fork_messages\", { messages });\n\t\t\t}\n\n\t\t\tcase \"get_entries\": {\n\t\t\t\tconst sessionManager = session.sessionManager;\n\t\t\t\tlet entries = sessionManager.getEntries();\n\t\t\t\tif (command.since !== undefined) {\n\t\t\t\t\tconst sinceIndex = entries.findIndex((e) => e.id === command.since);\n\t\t\t\t\tif (sinceIndex === -1) {\n\t\t\t\t\t\treturn error(id, \"get_entries\", `Entry not found: ${command.since}`);\n\t\t\t\t\t}\n\t\t\t\t\tentries = entries.slice(sinceIndex + 1);\n\t\t\t\t}\n\t\t\t\treturn success(id, \"get_entries\", { entries, leafId: sessionManager.getLeafId() });\n\t\t\t}\n\n\t\t\tcase \"get_tree\": {\n\t\t\t\tconst sessionManager = session.sessionManager;\n\t\t\t\treturn success(id, \"get_tree\", { tree: sessionManager.getTree(), leafId: sessionManager.getLeafId() });\n\t\t\t}\n\n\t\t\tcase \"get_last_assistant_text\": {\n\t\t\t\tconst text = session.getLastAssistantText();\n\t\t\t\treturn success(id, \"get_last_assistant_text\", { text });\n\t\t\t}\n\n\t\t\tcase \"set_session_name\": {\n\t\t\t\tconst name = command.name.trim();\n\t\t\t\tif (!name) {\n\t\t\t\t\treturn error(id, \"set_session_name\", \"Session name cannot be empty\");\n\t\t\t\t}\n\t\t\t\tsession.setSessionName(name);\n\t\t\t\treturn success(id, \"set_session_name\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Messages\n\t\t\t// =================================================================\n\n\t\t\tcase \"get_messages\": {\n\t\t\t\treturn success(id, \"get_messages\", { messages: session.messages });\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Commands (available for invocation via prompt)\n\t\t\t// =================================================================\n\n\t\t\tcase \"get_commands\": {\n\t\t\t\tconst commands: RpcSlashCommand[] = [];\n\n\t\t\t\tfor (const command of session.extensionRunner.getRegisteredCommands()) {\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname: command.invocationName,\n\t\t\t\t\t\tdescription: command.description,\n\t\t\t\t\t\tsource: \"extension\",\n\t\t\t\t\t\tsourceInfo: command.sourceInfo,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tfor (const template of session.promptTemplates) {\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname: template.name,\n\t\t\t\t\t\tdescription: template.description,\n\t\t\t\t\t\tsource: \"prompt\",\n\t\t\t\t\t\tsourceInfo: template.sourceInfo,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tfor (const skill of session.resourceLoader.getSkills().skills) {\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname: `skill:${skill.name}`,\n\t\t\t\t\t\tdescription: skill.description,\n\t\t\t\t\t\tsource: \"skill\",\n\t\t\t\t\t\tsourceInfo: skill.sourceInfo,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\treturn success(id, \"get_commands\", { commands });\n\t\t\t}\n\n\t\t\tdefault: {\n\t\t\t\tconst unknownCommand = command as { type: string };\n\t\t\t\treturn error(id, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * Check if shutdown was requested and perform shutdown if so.\n\t * Called after handling each command when waiting for the next command.\n\t */\n\tlet detachInput = () => {};\n\n\tasync function shutdown(exitCode = 0, signal?: NodeJS.Signals): Promise<never> {\n\t\tif (shuttingDown) {\n\t\t\tprocess.exit(exitCode);\n\t\t}\n\t\tshuttingDown = true;\n\t\tfor (const cleanup of signalCleanupHandlers) {\n\t\t\tcleanup();\n\t\t}\n\t\tunsubscribe?.();\n\t\tunsubscribeBackpressure?.();\n\t\tawait runtimeHost.dispose();\n\t\tdetachInput();\n\t\tprocess.stdin.pause();\n\t\tif (signal !== \"SIGTERM\") {\n\t\t\tawait flushRawStdout();\n\t\t}\n\t\tprocess.exit(exitCode);\n\t}\n\n\tasync function checkShutdownRequested(): Promise<void> {\n\t\tif (!shutdownRequested) return;\n\t\tawait shutdown();\n\t}\n\n\tconst handleInputLine = async (line: string) => {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(line);\n\t\t} catch (parseError: unknown) {\n\t\t\toutput(\n\t\t\t\terror(\n\t\t\t\t\tundefined,\n\t\t\t\t\t\"parse\",\n\t\t\t\t\t`Failed to parse command: ${parseError instanceof Error ? parseError.message : String(parseError)}`,\n\t\t\t\t),\n\t\t\t);\n\t\t\tawait waitForRawStdoutBackpressure();\n\t\t\treturn;\n\t\t}\n\n\t\t// Handle extension UI responses\n\t\tif (\n\t\t\ttypeof parsed === \"object\" &&\n\t\t\tparsed !== null &&\n\t\t\t\"type\" in parsed &&\n\t\t\tparsed.type === \"extension_ui_response\"\n\t\t) {\n\t\t\tconst response = parsed as RpcExtensionUIResponse;\n\t\t\tconst pending = pendingExtensionRequests.get(response.id);\n\t\t\tif (pending) {\n\t\t\t\tpendingExtensionRequests.delete(response.id);\n\t\t\t\tpending.resolve(response);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst command = parsed as RpcCommand;\n\t\ttry {\n\t\t\tconst response = await handleCommand(command);\n\t\t\tif (response) {\n\t\t\t\toutput(response);\n\t\t\t\tawait waitForRawStdoutBackpressure();\n\t\t\t}\n\t\t\tawait checkShutdownRequested();\n\t\t} catch (commandError: unknown) {\n\t\t\toutput(\n\t\t\t\terror(\n\t\t\t\t\tcommand.id,\n\t\t\t\t\tcommand.type,\n\t\t\t\t\tcommandError instanceof Error ? commandError.message : String(commandError),\n\t\t\t\t),\n\t\t\t);\n\t\t\tawait waitForRawStdoutBackpressure();\n\t\t}\n\t};\n\n\tconst onInputEnd = () => {\n\t\tvoid shutdown();\n\t};\n\tprocess.stdin.on(\"end\", onInputEnd);\n\n\tdetachInput = (() => {\n\t\tconst detachJsonl = attachJsonlLineReader(process.stdin, (line) => {\n\t\t\tvoid handleInputLine(line);\n\t\t});\n\t\treturn () => {\n\t\t\tdetachJsonl();\n\t\t\tprocess.stdin.off(\"end\", onInputEnd);\n\t\t};\n\t})();\n\n\t// Keep process alive forever\n\treturn new Promise(() => {});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rpc-mode.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AA4B/E,YAAY,EACX,UAAU,EACV,qBAAqB,EACrB,sBAAsB,EACtB,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,EACd,aAAa,GACb,MAAM,gBAAgB,CAAC;AAExB;;;GAGG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,mBAAmB,GAAG,OAAO,CAAC,KAAK,CAAC,CAkxBjF","sourcesContent":["/**\n * RPC mode: Headless operation with JSON stdin/stdout protocol.\n *\n * Used for embedding the agent in other applications.\n * Receives commands as JSON on stdin, outputs events and responses as JSON on stdout.\n *\n * Protocol:\n * - Commands: JSON objects with `type` field, optional `id` for correlation\n * - Responses: JSON objects with `type: \"response\"`, `command`, `success`, and optional `data`/`error`\n * - Events: AgentSessionEvent objects streamed as they occur\n * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response\n */\n\nimport * as crypto from \"node:crypto\";\nimport type { AgentSessionRuntime } from \"../../core/agent-session-runtime.ts\";\nimport type {\n\tExtensionUIContext,\n\tExtensionUIDialogOptions,\n\tExtensionWidgetOptions,\n\tWorkingIndicatorOptions,\n} from \"../../core/extensions/index.ts\";\nimport {\n\tflushRawStdout,\n\ttakeOverStdout,\n\twaitForRawStdoutBackpressure,\n\twriteRawStdout,\n} from \"../../core/output-guard.ts\";\nimport { killTrackedDetachedChildren } from \"../../utils/shell.ts\";\nimport { type Theme, theme } from \"../interactive/theme/theme.ts\";\nimport { attachJsonlLineReader, serializeJsonLine } from \"./jsonl.ts\";\nimport type {\n\tRpcCommand,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcResponse,\n\tRpcSessionState,\n\tRpcSlashCommand,\n\t\t\t\tPrototypePhase,\n\t\t\t\tQuicktypePhase,\n} from \"./rpc-types.ts\";\n\n// Re-export types for consumers\nexport type {\n\tRpcCommand,\n\tRpcExtensionUIRequest,\n\tRpcExtensionUIResponse,\n\tRpcResponse,\n\tRpcSessionState,\n\tRpcSlashCommand,\n\tPrototypePhase,\n\tQuicktypePhase,\n\tWorkflowState,\n} from \"./rpc-types.ts\";\n\n/**\n * Run in RPC mode.\n * Listens for JSON commands on stdin, outputs events and responses on stdout.\n */\nexport async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<never> {\n\ttakeOverStdout();\n\tlet session = runtimeHost.session;\n\tlet unsubscribe: (() => void) | undefined;\n\tlet unsubscribeBackpressure: (() => void) | undefined;\n\n\tconst output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {\n\t\twriteRawStdout(serializeJsonLine(obj));\n\t};\n\n\tconst success = <T extends RpcCommand[\"type\"]>(\n\t\tid: string | undefined,\n\t\tcommand: T,\n\t\tdata?: object | null,\n\t): RpcResponse => {\n\t\tif (data === undefined) {\n\t\t\treturn { id, type: \"response\", command, success: true } as RpcResponse;\n\t\t}\n\t\treturn { id, type: \"response\", command, success: true, data } as RpcResponse;\n\t};\n\n\tconst error = (id: string | undefined, command: string, message: string): RpcResponse => {\n\t\treturn { id, type: \"response\", command, success: false, error: message };\n\t};\n\n\t// Pending extension UI requests waiting for response\n\tconst pendingExtensionRequests = new Map<\n\t\tstring,\n\t\t{ resolve: (value: any) => void; reject: (error: Error) => void }\n\t>();\n\n\t// Shutdown request flag\n\tlet shutdownRequested = false;\n\tlet shuttingDown = false;\n\tconst signalCleanupHandlers: Array<() => void> = [];\n\n\t/** Helper for dialog methods with signal/timeout support */\n\tfunction createDialogPromise<T>(\n\t\topts: ExtensionUIDialogOptions | undefined,\n\t\tdefaultValue: T,\n\t\trequest: Record<string, unknown>,\n\t\tparseResponse: (response: RpcExtensionUIResponse) => T,\n\t): Promise<T> {\n\t\tif (opts?.signal?.aborted) return Promise.resolve(defaultValue);\n\n\t\tconst id = crypto.randomUUID();\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tlet timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\t\topts?.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\tpendingExtensionRequests.delete(id);\n\t\t\t};\n\n\t\t\tconst onAbort = () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolve(defaultValue);\n\t\t\t};\n\t\t\topts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tif (opts?.timeout) {\n\t\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(defaultValue);\n\t\t\t\t}, opts.timeout);\n\t\t\t}\n\n\t\t\tpendingExtensionRequests.set(id, {\n\t\t\t\tresolve: (response: RpcExtensionUIResponse) => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(parseResponse(response));\n\t\t\t\t},\n\t\t\t\treject,\n\t\t\t});\n\t\t\toutput({ type: \"extension_ui_request\", id, ...request } as RpcExtensionUIRequest);\n\t\t});\n\t}\n\n\t/**\n\t * Create an extension UI context that uses the RPC protocol.\n\t */\n\tconst createExtensionUIContext = (): ExtensionUIContext => ({\n\t\tselect: (title, options, opts) =>\n\t\t\tcreateDialogPromise(opts, undefined, { method: \"select\", title, options, timeout: opts?.timeout }, (r) =>\n\t\t\t\t\"cancelled\" in r && r.cancelled ? undefined : \"value\" in r ? r.value : undefined,\n\t\t\t),\n\n\t\tconfirm: (title, message, opts) =>\n\t\t\tcreateDialogPromise(opts, false, { method: \"confirm\", title, message, timeout: opts?.timeout }, (r) =>\n\t\t\t\t\"cancelled\" in r && r.cancelled ? false : \"confirmed\" in r ? r.confirmed : false,\n\t\t\t),\n\n\t\tinput: (title, placeholder, opts) =>\n\t\t\tcreateDialogPromise(opts, undefined, { method: \"input\", title, placeholder, timeout: opts?.timeout }, (r) =>\n\t\t\t\t\"cancelled\" in r && r.cancelled ? undefined : \"value\" in r ? r.value : undefined,\n\t\t\t),\n\n\t\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void {\n\t\t\t// Fire and forget - no response needed\n\t\t\toutput({\n\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\tmethod: \"notify\",\n\t\t\t\tmessage,\n\t\t\t\tnotifyType: type,\n\t\t\t} as RpcExtensionUIRequest);\n\t\t},\n\n\t\tonTerminalInput(): () => void {\n\t\t\t// Raw terminal input not supported in RPC mode\n\t\t\treturn () => {};\n\t\t},\n\n\t\tsetStatus(key: string, text: string | undefined): void {\n\t\t\t// Fire and forget - no response needed\n\t\t\toutput({\n\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\tmethod: \"setStatus\",\n\t\t\t\tstatusKey: key,\n\t\t\t\tstatusText: text,\n\t\t\t} as RpcExtensionUIRequest);\n\t\t},\n\n\t\tsetWorkingMessage(_message?: string): void {\n\t\t\t// Working message not supported in RPC mode - requires TUI loader access\n\t\t},\n\n\t\tsetWorkingVisible(_visible: boolean): void {\n\t\t\t// Working visibility not supported in RPC mode - requires TUI loader access\n\t\t},\n\n\t\tsetWorkingIndicator(_options?: WorkingIndicatorOptions): void {\n\t\t\t// Working indicator customization not supported in RPC mode - requires TUI loader access\n\t\t},\n\n\t\tsetHiddenThinkingLabel(_label?: string): void {\n\t\t\t// Hidden thinking label not supported in RPC mode - requires TUI message rendering access\n\t\t},\n\n\t\tsetWidget(key: string, content: unknown, options?: ExtensionWidgetOptions): void {\n\t\t\t// Only support string arrays in RPC mode - factory functions are ignored\n\t\t\tif (content === undefined || Array.isArray(content)) {\n\t\t\t\toutput({\n\t\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\t\tmethod: \"setWidget\",\n\t\t\t\t\twidgetKey: key,\n\t\t\t\t\twidgetLines: content as string[] | undefined,\n\t\t\t\t\twidgetPlacement: options?.placement,\n\t\t\t\t} as RpcExtensionUIRequest);\n\t\t\t}\n\t\t\t// Component factories are not supported in RPC mode - would need TUI access\n\t\t},\n\n\t\tsetFooter(_factory: unknown): void {\n\t\t\t// Custom footer not supported in RPC mode - requires TUI access\n\t\t},\n\n\t\tsetHeader(_factory: unknown): void {\n\t\t\t// Custom header not supported in RPC mode - requires TUI access\n\t\t},\n\n\t\tsetTitle(title: string): void {\n\t\t\t// Fire and forget - host can implement terminal title control\n\t\t\toutput({\n\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\tmethod: \"setTitle\",\n\t\t\t\ttitle,\n\t\t\t} as RpcExtensionUIRequest);\n\t\t},\n\n\t\tasync custom() {\n\t\t\t// Custom UI not supported in RPC mode\n\t\t\treturn undefined as never;\n\t\t},\n\n\t\tpasteToEditor(text: string): void {\n\t\t\t// Paste handling not supported in RPC mode - falls back to setEditorText\n\t\t\tthis.setEditorText(text);\n\t\t},\n\n\t\tsetEditorText(text: string): void {\n\t\t\t// Fire and forget - host can implement editor control\n\t\t\toutput({\n\t\t\t\ttype: \"extension_ui_request\",\n\t\t\t\tid: crypto.randomUUID(),\n\t\t\t\tmethod: \"set_editor_text\",\n\t\t\t\ttext,\n\t\t\t} as RpcExtensionUIRequest);\n\t\t},\n\n\t\tgetEditorText(): string {\n\t\t\t// Synchronous method can't wait for RPC response\n\t\t\t// Host should track editor state locally if needed\n\t\t\treturn \"\";\n\t\t},\n\n\t\tasync editor(title: string, prefill?: string): Promise<string | undefined> {\n\t\t\tconst id = crypto.randomUUID();\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tpendingExtensionRequests.set(id, {\n\t\t\t\t\tresolve: (response: RpcExtensionUIResponse) => {\n\t\t\t\t\t\tif (\"cancelled\" in response && response.cancelled) {\n\t\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\t} else if (\"value\" in response) {\n\t\t\t\t\t\t\tresolve(response.value);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t\treject,\n\t\t\t\t});\n\t\t\t\toutput({ type: \"extension_ui_request\", id, method: \"editor\", title, prefill } as RpcExtensionUIRequest);\n\t\t\t});\n\t\t},\n\n\t\taddAutocompleteProvider(): void {\n\t\t\t// Autocomplete provider composition is not supported in RPC mode\n\t\t},\n\n\t\tsetEditorComponent(): void {\n\t\t\t// Custom editor components not supported in RPC mode\n\t\t},\n\n\t\tgetEditorComponent() {\n\t\t\t// Custom editor components not supported in RPC mode\n\t\t\treturn undefined;\n\t\t},\n\n\t\tget theme() {\n\t\t\treturn theme;\n\t\t},\n\n\t\tgetAllThemes() {\n\t\t\treturn [];\n\t\t},\n\n\t\tgetTheme(_name: string) {\n\t\t\treturn undefined;\n\t\t},\n\n\t\tsetTheme(_theme: string | Theme) {\n\t\t\t// Theme switching not supported in RPC mode\n\t\t\treturn { success: false, error: \"Theme switching not supported in RPC mode\" };\n\t\t},\n\n\t\tgetToolsExpanded() {\n\t\t\t// Tool expansion not supported in RPC mode - no TUI\n\t\t\treturn false;\n\t\t},\n\n\t\tsetToolsExpanded(_expanded: boolean) {\n\t\t\t// Tool expansion not supported in RPC mode - no TUI\n\t\t},\n\t});\n\n\truntimeHost.setRebindSession(async () => {\n\t\tawait rebindSession();\n\t});\n\n\tconst rebindSession = async (): Promise<void> => {\n\t\tsession = runtimeHost.session;\n\t\tawait session.bindExtensions({\n\t\t\tuiContext: createExtensionUIContext(),\n\t\t\tmode: \"rpc\",\n\t\t\tcommandContextActions: {\n\t\t\t\twaitForIdle: () => session.waitForIdle(),\n\t\t\t\tnewSession: async (options) => runtimeHost.newSession(options),\n\t\t\t\tfork: async (entryId, forkOptions) => {\n\t\t\t\t\tconst result = await runtimeHost.fork(entryId, forkOptions);\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tnavigateTree: async (targetId, options) => {\n\t\t\t\t\tconst result = await session.navigateTree(targetId, {\n\t\t\t\t\t\tsummarize: options?.summarize,\n\t\t\t\t\t\tcustomInstructions: options?.customInstructions,\n\t\t\t\t\t\treplaceInstructions: options?.replaceInstructions,\n\t\t\t\t\t\tlabel: options?.label,\n\t\t\t\t\t});\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tswitchSession: async (sessionPath, options) => {\n\t\t\t\t\treturn runtimeHost.switchSession(sessionPath, options);\n\t\t\t\t},\n\t\t\t\treload: async () => {\n\t\t\t\t\tawait session.reload();\n\t\t\t\t},\n\t\t\t},\n\t\t\tshutdownHandler: () => {\n\t\t\t\tshutdownRequested = true;\n\t\t\t},\n\t\t\tonError: (err) => {\n\t\t\t\toutput({ type: \"extension_error\", extensionPath: err.extensionPath, event: err.event, error: err.error });\n\t\t\t},\n\t\t});\n\n\t\tunsubscribe?.();\n\t\tunsubscribeBackpressure?.();\n\t\tunsubscribe = session.subscribe((event) => {\n\t\t\toutput(event);\n\t\t\tif (event.type === \"agent_settled\") {\n\t\t\t\tvoid checkShutdownRequested();\n\t\t\t}\n\t\t});\n\t\tunsubscribeBackpressure = session.agent.subscribe(async () => {\n\t\t\tawait waitForRawStdoutBackpressure();\n\t\t});\n\t};\n\n\tconst registerSignalHandlers = (): void => {\n\t\tconst signals: NodeJS.Signals[] = [\"SIGTERM\"];\n\t\tif (process.platform !== \"win32\") {\n\t\t\tsignals.push(\"SIGHUP\");\n\t\t}\n\n\t\tfor (const signal of signals) {\n\t\t\tconst handler = () => {\n\t\t\t\tkillTrackedDetachedChildren();\n\t\t\t\tvoid shutdown(signal === \"SIGHUP\" ? 129 : 143, signal);\n\t\t\t};\n\t\t\tprocess.on(signal, handler);\n\t\t\tsignalCleanupHandlers.push(() => process.off(signal, handler));\n\t\t}\n\t};\n\n\tawait rebindSession();\n\tregisterSignalHandlers();\n\n\t// Handle a single command\n\tconst handleCommand = async (command: RpcCommand): Promise<RpcResponse | undefined> => {\n\t\tconst id = command.id;\n\n\t\tswitch (command.type) {\n\t\t\t// =================================================================\n\t\t\t// Prompting\n\t\t\t// =================================================================\n\n\t\t\tcase \"prompt\": {\n\t\t\t\t// Start prompt handling immediately, but emit the authoritative response only after\n\t\t\t\t// prompt preflight succeeds. Queued and immediately handled prompts also count as success.\n\t\t\t\tlet preflightSucceeded = false;\n\t\t\t\tvoid session\n\t\t\t\t\t.prompt(command.message, {\n\t\t\t\t\t\timages: command.images,\n\t\t\t\t\t\tstreamingBehavior: command.streamingBehavior,\n\t\t\t\t\t\tsource: \"rpc\",\n\t\t\t\t\t\tpreflightResult: (didSucceed) => {\n\t\t\t\t\t\t\tif (didSucceed) {\n\t\t\t\t\t\t\t\tpreflightSucceeded = true;\n\t\t\t\t\t\t\t\toutput(success(id, \"prompt\"));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\t.catch((e) => {\n\t\t\t\t\t\tif (!preflightSucceeded) {\n\t\t\t\t\t\t\toutput(error(id, \"prompt\", e.message));\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tcase \"steer\": {\n\t\t\t\tawait session.steer(command.message, command.images);\n\t\t\t\treturn success(id, \"steer\");\n\t\t\t}\n\n\t\t\tcase \"follow_up\": {\n\t\t\t\tawait session.followUp(command.message, command.images);\n\t\t\t\treturn success(id, \"follow_up\");\n\t\t\t}\n\n\t\t\tcase \"abort\": {\n\t\t\t\tawait session.abort();\n\t\t\t\treturn success(id, \"abort\");\n\t\t\t}\n\n\t\t\tcase \"new_session\": {\n\t\t\t\tconst options = command.parentSession ? { parentSession: command.parentSession } : undefined;\n\t\t\t\tconst result = await runtimeHost.newSession(options);\n\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\tawait rebindSession();\n\t\t\t\t}\n\t\t\t\treturn success(id, \"new_session\", result);\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// State\n\t\t\t// =================================================================\n\n\t\t\tcase \"get_state\": {\n\t\t\t\tconst state: RpcSessionState = {\n\t\t\t\t\tmodel: session.model,\n\t\t\t\t\tthinkingLevel: session.thinkingLevel,\n\t\t\t\t\tisStreaming: session.isStreaming,\n\t\t\t\t\tisCompacting: session.isCompacting,\n\t\t\t\t\tsteeringMode: session.steeringMode,\n\t\t\t\t\tfollowUpMode: session.followUpMode,\n\t\t\t\t\tsessionFile: session.sessionFile,\n\t\t\t\t\tsessionId: session.sessionId,\n\t\t\t\t\tsessionName: session.sessionName,\n\t\t\t\t\tautoCompactionEnabled: session.autoCompactionEnabled,\n\t\t\t\t\tautoHandoffEnabled: session.autoHandoffEnabled,\n\t\t\t\t\tautoHandoffThresholdTokens: session.autoHandoffThresholdTokens,\n\t\t\t\t\tmessageCount: session.messages.length,\n\t\t\t\t\tpendingMessageCount: session.pendingMessageCount,\n\t\t\t\t};\n\n\t\t\t\t// Attach workflow state from latest workflow-phase entry, if active.\n\t\t\t\t// ponytail: linear scan of entries; workflow is short-lived so O(n) per get_state is fine.\n\t\t\t\ttry {\n\t\t\t\t\tconst entries = session.sessionManager.getEntries();\n\t\t\t\t\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\t\t\t\t\tconst e = entries[i] as { type: string; customType?: string; data?: unknown };\n\t\t\t\t\t\tconst customType = e.customType;\n\t\t\t\t\t\tif (e.type === \"custom\" && (customType === \"prototype-phase\" || customType === \"quicktype-phase\") && e.data) {\n\t\t\t\t\t\t\tconst d = e.data as { phase?: string; step?: number; done?: boolean };\n\t\t\t\t\t\t\tif (d && typeof d.phase === \"string\" && typeof d.step === \"number\" && !d.done) {\n\t\t\t\t\t\t\t\tstate.workflow = {\n\t\t\t\t\t\t\t\t\tmode: customType === \"prototype-phase\" ? \"prototype\" : \"quicktype\",\n\t\t\t\t\t\t\t\t\tphase: d.phase as PrototypePhase | QuicktypePhase,\n\t\t\t\t\t\t\t\t\tstep: d.step,\n\t\t\t\t\t\t\t\t\tdone: false,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch { /* corrupt entry must not crash get_state */ }\n\n\t\t\t\treturn success(id, \"get_state\", state);\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Model\n\t\t\t// =================================================================\n\n\t\t\tcase \"set_model\": {\n\t\t\t\tconst models = await session.modelRuntime.getAvailable();\n\t\t\t\tconst model = models.find((m) => m.provider === command.provider && m.id === command.modelId);\n\t\t\t\tif (!model) {\n\t\t\t\t\treturn error(id, \"set_model\", `Model not found: ${command.provider}/${command.modelId}`);\n\t\t\t\t}\n\t\t\t\tawait session.setModel(model);\n\t\t\t\treturn success(id, \"set_model\", model);\n\t\t\t}\n\n\t\t\tcase \"cycle_model\": {\n\t\t\t\tconst result = await session.cycleModel();\n\t\t\t\tif (!result) {\n\t\t\t\t\treturn success(id, \"cycle_model\", null);\n\t\t\t\t}\n\t\t\t\treturn success(id, \"cycle_model\", result);\n\t\t\t}\n\n\t\t\tcase \"get_available_models\": {\n\t\t\t\tconst models = await session.modelRuntime.getAvailable();\n\t\t\t\treturn success(id, \"get_available_models\", { models });\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Thinking\n\t\t\t// =================================================================\n\n\t\t\tcase \"set_thinking_level\": {\n\t\t\t\tsession.setThinkingLevel(command.level);\n\t\t\t\treturn success(id, \"set_thinking_level\");\n\t\t\t}\n\n\t\t\tcase \"cycle_thinking_level\": {\n\t\t\t\tconst level = session.cycleThinkingLevel();\n\t\t\t\tif (!level) {\n\t\t\t\t\treturn success(id, \"cycle_thinking_level\", null);\n\t\t\t\t}\n\t\t\t\treturn success(id, \"cycle_thinking_level\", { level });\n\t\t\t}\n\n\t\t\tcase \"get_available_thinking_levels\": {\n\t\t\t\tconst levels = session.getAvailableThinkingLevels();\n\t\t\t\treturn success(id, \"get_available_thinking_levels\", { levels });\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Queue Modes\n\t\t\t// =================================================================\n\n\t\t\tcase \"set_steering_mode\": {\n\t\t\t\tsession.setSteeringMode(command.mode);\n\t\t\t\treturn success(id, \"set_steering_mode\");\n\t\t\t}\n\n\t\t\tcase \"set_follow_up_mode\": {\n\t\t\t\tsession.setFollowUpMode(command.mode);\n\t\t\t\treturn success(id, \"set_follow_up_mode\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Compaction\n\t\t\t// =================================================================\n\n\t\t\tcase \"compact\": {\n\t\t\t\tconst result = await session.compact(command.customInstructions);\n\t\t\t\treturn success(id, \"compact\", result);\n\t\t\t}\n\n\t\t\tcase \"set_auto_compaction\": {\n\t\t\t\tsession.setAutoCompactionEnabled(command.enabled);\n\t\t\t\treturn success(id, \"set_auto_compaction\");\n\t\t\t}\n\n\t\t\tcase \"set_auto_handoff\": {\n\t\t\t\tsession.settingsManager.setAutoHandoffEnabled(command.enabled);\n\t\t\t\treturn success(id, \"set_auto_handoff\");\n\t\t\t}\n\n\t\t\tcase \"set_auto_handoff_threshold\": {\n\t\t\t\tconst tokens = command.tokens;\n\t\t\t\tif (!Number.isFinite(tokens) || tokens < 1000) {\n\t\t\t\t\treturn error(id, \"set_auto_handoff_threshold\", \"Threshold must be at least 1000 tokens\");\n\t\t\t\t}\n\t\t\t\tsession.settingsManager.setAutoHandoffThresholdTokens(tokens);\n\t\t\t\treturn success(id, \"set_auto_handoff_threshold\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Retry\n\t\t\t// =================================================================\n\n\t\t\tcase \"set_auto_retry\": {\n\t\t\t\tsession.setAutoRetryEnabled(command.enabled);\n\t\t\t\treturn success(id, \"set_auto_retry\");\n\t\t\t}\n\n\t\t\tcase \"abort_retry\": {\n\t\t\t\tsession.abortRetry();\n\t\t\t\treturn success(id, \"abort_retry\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Bash\n\t\t\t// =================================================================\n\n\t\t\tcase \"bash\": {\n\t\t\t\tconst result = await session.executeBash(command.command, undefined, {\n\t\t\t\t\texcludeFromContext: command.excludeFromContext,\n\t\t\t\t});\n\t\t\t\treturn success(id, \"bash\", result);\n\t\t\t}\n\n\t\t\tcase \"abort_bash\": {\n\t\t\t\tsession.abortBash();\n\t\t\t\treturn success(id, \"abort_bash\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Session\n\t\t\t// =================================================================\n\n\t\t\tcase \"get_session_stats\": {\n\t\t\t\tconst stats = session.getSessionStats();\n\t\t\t\treturn success(id, \"get_session_stats\", stats);\n\t\t\t}\n\n\t\t\tcase \"export_html\": {\n\t\t\t\tconst path = await session.exportToHtml(command.outputPath);\n\t\t\t\treturn success(id, \"export_html\", { path });\n\t\t\t}\n\n\t\t\tcase \"switch_session\": {\n\t\t\t\tconst result = await runtimeHost.switchSession(command.sessionPath);\n\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\tawait rebindSession();\n\t\t\t\t}\n\t\t\t\treturn success(id, \"switch_session\", result);\n\t\t\t}\n\n\t\t\tcase \"fork\": {\n\t\t\t\tconst result = await runtimeHost.fork(command.entryId);\n\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\tawait rebindSession();\n\t\t\t\t}\n\t\t\t\treturn success(id, \"fork\", { text: result.selectedText, cancelled: result.cancelled });\n\t\t\t}\n\n\t\t\tcase \"clone\": {\n\t\t\t\tconst leafId = session.sessionManager.getLeafId();\n\t\t\t\tif (!leafId) {\n\t\t\t\t\treturn error(id, \"clone\", \"Cannot clone session: no current entry selected\");\n\t\t\t\t}\n\t\t\t\tconst result = await runtimeHost.fork(leafId, { position: \"at\" });\n\t\t\t\tif (!result.cancelled) {\n\t\t\t\t\tawait rebindSession();\n\t\t\t\t}\n\t\t\t\treturn success(id, \"clone\", { cancelled: result.cancelled });\n\t\t\t}\n\n\t\t\tcase \"get_fork_messages\": {\n\t\t\t\tconst messages = session.getUserMessagesForForking();\n\t\t\t\treturn success(id, \"get_fork_messages\", { messages });\n\t\t\t}\n\n\t\t\tcase \"get_entries\": {\n\t\t\t\tconst sessionManager = session.sessionManager;\n\t\t\t\tlet entries = sessionManager.getEntries();\n\t\t\t\tif (command.since !== undefined) {\n\t\t\t\t\tconst sinceIndex = entries.findIndex((e) => e.id === command.since);\n\t\t\t\t\tif (sinceIndex === -1) {\n\t\t\t\t\t\treturn error(id, \"get_entries\", `Entry not found: ${command.since}`);\n\t\t\t\t\t}\n\t\t\t\t\tentries = entries.slice(sinceIndex + 1);\n\t\t\t\t}\n\t\t\t\treturn success(id, \"get_entries\", { entries, leafId: sessionManager.getLeafId() });\n\t\t\t}\n\n\t\t\tcase \"get_tree\": {\n\t\t\t\tconst sessionManager = session.sessionManager;\n\t\t\t\treturn success(id, \"get_tree\", { tree: sessionManager.getTree(), leafId: sessionManager.getLeafId() });\n\t\t\t}\n\n\t\t\tcase \"get_last_assistant_text\": {\n\t\t\t\tconst text = session.getLastAssistantText();\n\t\t\t\treturn success(id, \"get_last_assistant_text\", { text });\n\t\t\t}\n\n\t\t\tcase \"set_session_name\": {\n\t\t\t\tconst name = command.name.trim();\n\t\t\t\tif (!name) {\n\t\t\t\t\treturn error(id, \"set_session_name\", \"Session name cannot be empty\");\n\t\t\t\t}\n\t\t\t\tsession.setSessionName(name);\n\t\t\t\treturn success(id, \"set_session_name\");\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Messages\n\t\t\t// =================================================================\n\n\t\t\tcase \"get_messages\": {\n\t\t\t\treturn success(id, \"get_messages\", { messages: session.messages });\n\t\t\t}\n\n\t\t\t// =================================================================\n\t\t\t// Commands (available for invocation via prompt)\n\t\t\t// =================================================================\n\n\t\t\tcase \"get_commands\": {\n\t\t\t\tconst commands: RpcSlashCommand[] = [];\n\n\t\t\t\tfor (const command of session.extensionRunner.getRegisteredCommands()) {\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname: command.invocationName,\n\t\t\t\t\t\tdescription: command.description,\n\t\t\t\t\t\tsource: \"extension\",\n\t\t\t\t\t\tsourceInfo: command.sourceInfo,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tfor (const template of session.promptTemplates) {\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname: template.name,\n\t\t\t\t\t\tdescription: template.description,\n\t\t\t\t\t\tsource: \"prompt\",\n\t\t\t\t\t\tsourceInfo: template.sourceInfo,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tfor (const skill of session.resourceLoader.getSkills().skills) {\n\t\t\t\t\tcommands.push({\n\t\t\t\t\t\tname: `skill:${skill.name}`,\n\t\t\t\t\t\tdescription: skill.description,\n\t\t\t\t\t\tsource: \"skill\",\n\t\t\t\t\t\tsourceInfo: skill.sourceInfo,\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\treturn success(id, \"get_commands\", { commands });\n\t\t\t}\n\n\t\t\tdefault: {\n\t\t\t\tconst unknownCommand = command as { type: string };\n\t\t\t\treturn error(id, unknownCommand.type, `Unknown command: ${unknownCommand.type}`);\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * Check if shutdown was requested and perform shutdown if so.\n\t * Called after handling each command when waiting for the next command.\n\t */\n\tlet detachInput = () => {};\n\n\tasync function shutdown(exitCode = 0, signal?: NodeJS.Signals): Promise<never> {\n\t\tif (shuttingDown) {\n\t\t\tprocess.exit(exitCode);\n\t\t}\n\t\tshuttingDown = true;\n\t\tfor (const cleanup of signalCleanupHandlers) {\n\t\t\tcleanup();\n\t\t}\n\t\tunsubscribe?.();\n\t\tunsubscribeBackpressure?.();\n\t\tawait runtimeHost.dispose();\n\t\tdetachInput();\n\t\tprocess.stdin.pause();\n\t\tif (signal !== \"SIGTERM\") {\n\t\t\tawait flushRawStdout();\n\t\t}\n\t\tprocess.exit(exitCode);\n\t}\n\n\tasync function checkShutdownRequested(): Promise<void> {\n\t\tif (!shutdownRequested) return;\n\t\tawait shutdown();\n\t}\n\n\tconst handleInputLine = async (line: string) => {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(line);\n\t\t} catch (parseError: unknown) {\n\t\t\toutput(\n\t\t\t\terror(\n\t\t\t\t\tundefined,\n\t\t\t\t\t\"parse\",\n\t\t\t\t\t`Failed to parse command: ${parseError instanceof Error ? parseError.message : String(parseError)}`,\n\t\t\t\t),\n\t\t\t);\n\t\t\tawait waitForRawStdoutBackpressure();\n\t\t\treturn;\n\t\t}\n\n\t\t// Handle extension UI responses\n\t\tif (\n\t\t\ttypeof parsed === \"object\" &&\n\t\t\tparsed !== null &&\n\t\t\t\"type\" in parsed &&\n\t\t\tparsed.type === \"extension_ui_response\"\n\t\t) {\n\t\t\tconst response = parsed as RpcExtensionUIResponse;\n\t\t\tconst pending = pendingExtensionRequests.get(response.id);\n\t\t\tif (pending) {\n\t\t\t\tpendingExtensionRequests.delete(response.id);\n\t\t\t\tpending.resolve(response);\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst command = parsed as RpcCommand;\n\t\ttry {\n\t\t\tconst response = await handleCommand(command);\n\t\t\tif (response) {\n\t\t\t\toutput(response);\n\t\t\t\tawait waitForRawStdoutBackpressure();\n\t\t\t}\n\t\t\tawait checkShutdownRequested();\n\t\t} catch (commandError: unknown) {\n\t\t\toutput(\n\t\t\t\terror(\n\t\t\t\t\tcommand.id,\n\t\t\t\t\tcommand.type,\n\t\t\t\t\tcommandError instanceof Error ? commandError.message : String(commandError),\n\t\t\t\t),\n\t\t\t);\n\t\t\tawait waitForRawStdoutBackpressure();\n\t\t}\n\t};\n\n\tconst onInputEnd = () => {\n\t\tvoid shutdown();\n\t};\n\tprocess.stdin.on(\"end\", onInputEnd);\n\n\tdetachInput = (() => {\n\t\tconst detachJsonl = attachJsonlLineReader(process.stdin, (line) => {\n\t\t\tvoid handleInputLine(line);\n\t\t});\n\t\treturn () => {\n\t\t\tdetachJsonl();\n\t\t\tprocess.stdin.off(\"end\", onInputEnd);\n\t\t};\n\t})();\n\n\t// Keep process alive forever\n\treturn new Promise(() => {});\n}\n"]}
|
|
@@ -364,11 +364,11 @@ export async function runRpcMode(runtimeHost) {
|
|
|
364
364
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
365
365
|
const e = entries[i];
|
|
366
366
|
const customType = e.customType;
|
|
367
|
-
if (e.type === "custom" && (customType === "prototype-phase" || customType === "
|
|
367
|
+
if (e.type === "custom" && (customType === "prototype-phase" || customType === "quicktype-phase") && e.data) {
|
|
368
368
|
const d = e.data;
|
|
369
369
|
if (d && typeof d.phase === "string" && typeof d.step === "number" && !d.done) {
|
|
370
370
|
state.workflow = {
|
|
371
|
-
mode: customType === "prototype-phase" ? "prototype" : "
|
|
371
|
+
mode: customType === "prototype-phase" ? "prototype" : "quicktype",
|
|
372
372
|
phase: d.phase,
|
|
373
373
|
step: d.step,
|
|
374
374
|
done: false,
|