surgent 0.7.0-alpha.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/LICENSE +21 -0
- package/README.md +407 -0
- package/bin/surgent.js +211 -0
- package/dist/optimizers/LICENSE +21 -0
- package/dist/optimizers/index.js +1984 -0
- package/dist/optimizers/index.js.map +7 -0
- package/dist/optimizers/package.json +31 -0
- package/package.json +45 -0
- package/src/agent/built-in/documenter.md +58 -0
- package/src/agent/built-in/general.md +107 -0
- package/src/agent/built-in/planner.md +73 -0
- package/src/agent/built-in/scout.md +97 -0
- package/src/agent/command.ts +140 -0
- package/src/agent/helpers.ts +95 -0
- package/src/agent/index.ts +9 -0
- package/src/agent/storage.ts +287 -0
- package/src/agent/types.ts +28 -0
- package/src/checkpoint/git.ts +173 -0
- package/src/checkpoint/index.ts +117 -0
- package/src/checkpoint/snapshot.ts +28 -0
- package/src/checkpoint/stage.ts +59 -0
- package/src/checkpoint/store.ts +108 -0
- package/src/cleanup/checkpoint.ts +31 -0
- package/src/cleanup/helpers.ts +24 -0
- package/src/cleanup/index.ts +21 -0
- package/src/cleanup/permission.ts +74 -0
- package/src/cleanup/subsession.ts +46 -0
- package/src/commands/helpers.ts +217 -0
- package/src/commands/index.ts +79 -0
- package/src/commands/render.ts +95 -0
- package/src/commands/types.ts +11 -0
- package/src/mcp-client/call-tool.ts +143 -0
- package/src/mcp-client/client.ts +90 -0
- package/src/mcp-client/command.ts +257 -0
- package/src/mcp-client/helpers.ts +153 -0
- package/src/mcp-client/index.ts +21 -0
- package/src/mcp-client/list-tools.ts +84 -0
- package/src/mcp-client/storage.ts +190 -0
- package/src/mcp-client/types.ts +34 -0
- package/src/mcp-client/validation.ts +115 -0
- package/src/optimizers/compactor/bash.ts +159 -0
- package/src/optimizers/compactor/grep.ts +141 -0
- package/src/optimizers/compactor/index.ts +132 -0
- package/src/optimizers/deduplicator/helpers.ts +75 -0
- package/src/optimizers/deduplicator/index.ts +23 -0
- package/src/optimizers/deduplicator/resources.ts +77 -0
- package/src/optimizers/deduplicator/state.ts +119 -0
- package/src/optimizers/deduplicator/types.ts +14 -0
- package/src/optimizers/entries.ts +104 -0
- package/src/optimizers/index.ts +17 -0
- package/src/optimizers/inspector/helpers.ts +60 -0
- package/src/optimizers/inspector/index.ts +89 -0
- package/src/optimizers/inspector/inspect.ts +88 -0
- package/src/optimizers/inspector/types.ts +7 -0
- package/src/optimizers/languages/go.ts +79 -0
- package/src/optimizers/languages/grammar.ts +200 -0
- package/src/optimizers/languages/index.ts +75 -0
- package/src/optimizers/languages/java.ts +64 -0
- package/src/optimizers/languages/python.ts +63 -0
- package/src/optimizers/languages/rust.ts +71 -0
- package/src/optimizers/languages/symbols.ts +95 -0
- package/src/optimizers/languages/tree-sitter-languages.d.ts +23 -0
- package/src/optimizers/languages/types.ts +134 -0
- package/src/optimizers/languages/typescript.ts +116 -0
- package/src/optimizers/mapper/files.ts +94 -0
- package/src/optimizers/mapper/index.ts +133 -0
- package/src/optimizers/mapper/types.ts +6 -0
- package/src/optimizers/pruner/cleanup.ts +121 -0
- package/src/optimizers/pruner/context.ts +46 -0
- package/src/optimizers/pruner/index.ts +45 -0
- package/src/optimizers/pruner/session.ts +34 -0
- package/src/optimizers/pruner/types.ts +18 -0
- package/src/permission/bash.ts +124 -0
- package/src/permission/command.ts +111 -0
- package/src/permission/components/prompt.ts +255 -0
- package/src/permission/components/rules-list.ts +342 -0
- package/src/permission/constants.ts +48 -0
- package/src/permission/helpers.ts +156 -0
- package/src/permission/index.ts +134 -0
- package/src/permission/pattern.ts +51 -0
- package/src/permission/piignore.ts +148 -0
- package/src/permission/precedence.ts +54 -0
- package/src/permission/resolution.ts +116 -0
- package/src/permission/storage.ts +142 -0
- package/src/permission/types.ts +57 -0
- package/src/questionnaire/component.ts +357 -0
- package/src/questionnaire/helpers.ts +220 -0
- package/src/questionnaire/index.ts +67 -0
- package/src/questionnaire/schemas.ts +50 -0
- package/src/questionnaire/types.ts +47 -0
- package/src/redactor/index.ts +34 -0
- package/src/redactor/patterns.ts +234 -0
- package/src/redactor/secrets.ts +113 -0
- package/src/subagent/helpers.ts +93 -0
- package/src/subagent/index.ts +81 -0
- package/src/subagent/storage.ts +100 -0
- package/src/subagent/subsession.ts +266 -0
- package/src/subagent/types.ts +83 -0
- package/src/subagent/validation.ts +100 -0
- package/src/ui/components/action-select-list.ts +165 -0
- package/src/ui/components/bash-mode.ts +281 -0
- package/src/ui/components/extended-select-list.ts +166 -0
- package/src/ui/components/form-field.ts +184 -0
- package/src/ui/components/form.ts +179 -0
- package/src/ui/components/frame.ts +60 -0
- package/src/ui/components/input-mode-indicator.ts +64 -0
- package/src/ui/components/keybound.ts +150 -0
- package/src/ui/components/lines.ts +27 -0
- package/src/ui/components/placeholder-input.ts +59 -0
- package/src/ui/components/scoped-input.ts +78 -0
- package/src/ui/components/scrollable-view.ts +155 -0
- package/src/ui/index.ts +40 -0
- package/src/utils.ts +206 -0
- package/src/web-tools/index.ts +15 -0
- package/src/web-tools/providers/brave.ts +55 -0
- package/src/web-tools/providers/firecrawl.ts +66 -0
- package/src/web-tools/providers/index.ts +50 -0
- package/src/web-tools/providers/jina.ts +48 -0
- package/src/web-tools/providers/native.ts +57 -0
- package/src/web-tools/providers/tavily.ts +56 -0
- package/src/web-tools/settings.ts +15 -0
- package/src/web-tools/web-fetch/helpers.ts +66 -0
- package/src/web-tools/web-fetch/index.ts +91 -0
- package/src/web-tools/web-fetch/parser.ts +51 -0
- package/src/web-tools/web-fetch/storage.ts +65 -0
- package/src/web-tools/web-fetch/types.ts +8 -0
- package/src/web-tools/web-login/helpers.ts +79 -0
- package/src/web-tools/web-login/index.ts +100 -0
- package/src/web-tools/web-login/types.ts +4 -0
- package/src/web-tools/web-search/helpers.ts +36 -0
- package/src/web-tools/web-search/index.ts +98 -0
- package/src/web-tools/web-search/types.ts +15 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAgentSession,
|
|
3
|
+
DefaultResourceLoader,
|
|
4
|
+
getAgentDir,
|
|
5
|
+
SessionManager,
|
|
6
|
+
type AgentSession,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
createErrorResult,
|
|
10
|
+
createSubsessionBridge,
|
|
11
|
+
formatToolUse,
|
|
12
|
+
getLastAssistantOutput,
|
|
13
|
+
} from "./helpers.js";
|
|
14
|
+
import { validateBuiltInOutput } from "./validation.js";
|
|
15
|
+
import {
|
|
16
|
+
findSubsession,
|
|
17
|
+
findSubsessionFile,
|
|
18
|
+
loadSubsessionOutput,
|
|
19
|
+
resolveRuntime,
|
|
20
|
+
saveSubsession,
|
|
21
|
+
} from "./storage.js";
|
|
22
|
+
import type {
|
|
23
|
+
CreateSubsessionParams,
|
|
24
|
+
ExecuteTurnRequest,
|
|
25
|
+
RuntimeConfig,
|
|
26
|
+
Subsession,
|
|
27
|
+
SubsessionRequest,
|
|
28
|
+
SubsessionResult,
|
|
29
|
+
SubsessionSnapshot,
|
|
30
|
+
} from "./types.js";
|
|
31
|
+
import { getPiPath } from "../utils.js";
|
|
32
|
+
|
|
33
|
+
const DISALLOWED_TOOLS = new Set(["subagent", "call_mcp_tool", "list_mcp_tools"]);
|
|
34
|
+
|
|
35
|
+
async function executeTurn(request: ExecuteTurnRequest): Promise<SubsessionResult> {
|
|
36
|
+
const snapshot: SubsessionSnapshot = {
|
|
37
|
+
id: request.session.sessionId,
|
|
38
|
+
status: "running",
|
|
39
|
+
toolsUsed: [],
|
|
40
|
+
usage: request.usage,
|
|
41
|
+
};
|
|
42
|
+
const toolCounts: Record<string, number> = {};
|
|
43
|
+
let aborted = false;
|
|
44
|
+
let errorMessage = "";
|
|
45
|
+
let lastMessage = "";
|
|
46
|
+
let stoppedWithError = false;
|
|
47
|
+
|
|
48
|
+
request.onSnapshot?.(snapshot);
|
|
49
|
+
const unsubscribe = request.session.subscribe((event) => {
|
|
50
|
+
if (event.type !== "message_end" || event.message.role !== "assistant") return;
|
|
51
|
+
|
|
52
|
+
const message = event.message;
|
|
53
|
+
snapshot.usage.input += message.usage?.input ?? 0;
|
|
54
|
+
snapshot.usage.output += message.usage?.output ?? 0;
|
|
55
|
+
snapshot.usage.cost += message.usage?.cost.total ?? 0;
|
|
56
|
+
snapshot.contextUsage = request.session.getContextUsage();
|
|
57
|
+
|
|
58
|
+
if (message.stopReason === "aborted") {
|
|
59
|
+
aborted = true;
|
|
60
|
+
} else if (message.stopReason === "error") {
|
|
61
|
+
stoppedWithError = true;
|
|
62
|
+
errorMessage ||= message.errorMessage || "Subsession failed";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const contentPart of message.content) {
|
|
66
|
+
if (contentPart.type === "text") {
|
|
67
|
+
lastMessage = contentPart.text;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (contentPart.type !== "toolCall") continue;
|
|
71
|
+
|
|
72
|
+
const toolCount = (toolCounts[contentPart.name] ?? 0) + 1;
|
|
73
|
+
toolCounts[contentPart.name] = toolCount;
|
|
74
|
+
snapshot.usage.toolCalls += 1;
|
|
75
|
+
snapshot.toolsUsed.push(formatToolUse(contentPart.name, contentPart.arguments));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
request.onSnapshot?.(snapshot);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const abortTurn = () => {
|
|
82
|
+
aborted = true;
|
|
83
|
+
void request.session.abort().catch(() => undefined);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
if (request.signal?.aborted) {
|
|
88
|
+
abortTurn();
|
|
89
|
+
} else {
|
|
90
|
+
request.signal?.addEventListener("abort", abortTurn, { once: true });
|
|
91
|
+
await request.session.prompt(request.input);
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
95
|
+
} finally {
|
|
96
|
+
request.signal?.removeEventListener("abort", abortTurn);
|
|
97
|
+
unsubscribe();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const output = lastMessage || getLastAssistantOutput(request.session);
|
|
101
|
+
const errorOutput = errorMessage || output || "Subsession failed";
|
|
102
|
+
const status = aborted ? "aborted" : stoppedWithError || errorMessage ? "error" : "done";
|
|
103
|
+
|
|
104
|
+
snapshot.status = status;
|
|
105
|
+
request.onSnapshot?.(snapshot);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
id: request.session.sessionId,
|
|
109
|
+
status,
|
|
110
|
+
output: status === "error" ? errorOutput : output,
|
|
111
|
+
usage: snapshot.usage,
|
|
112
|
+
toolCounts,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function openSessionManager(request: SubsessionRequest): Promise<SessionManager> {
|
|
117
|
+
if (request.label === "subagent") {
|
|
118
|
+
return SessionManager.inMemory(request.ctx.cwd);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const subsessionsDir = getPiPath("subsessionsDir", request.ctx.cwd);
|
|
122
|
+
if (!request.id) {
|
|
123
|
+
const parentSession = request.ctx.sessionManager.getSessionFile();
|
|
124
|
+
return SessionManager.create(request.ctx.cwd, subsessionsDir, { parentSession });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const subsessionFile = await findSubsessionFile(request.ctx.cwd, request.id);
|
|
128
|
+
if (!subsessionFile) {
|
|
129
|
+
throw new Error(`Subsession file not found: ${request.id}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return SessionManager.open(subsessionFile.path, subsessionFile.dir, request.ctx.cwd);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function createSdkSession(
|
|
136
|
+
request: SubsessionRequest,
|
|
137
|
+
runtime: RuntimeConfig,
|
|
138
|
+
): Promise<AgentSession> {
|
|
139
|
+
const sessionManager = await openSessionManager(request);
|
|
140
|
+
const modelId = runtime.meta.model;
|
|
141
|
+
const model = modelId
|
|
142
|
+
? request.ctx.modelRegistry.find(
|
|
143
|
+
modelId.slice(0, modelId.indexOf("/")),
|
|
144
|
+
modelId.slice(modelId.indexOf("/") + 1),
|
|
145
|
+
)
|
|
146
|
+
: request.ctx.model;
|
|
147
|
+
if (modelId && !model) {
|
|
148
|
+
throw new Error(`Unknown model "${modelId}" in agent config`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
152
|
+
cwd: request.ctx.cwd,
|
|
153
|
+
agentDir: getAgentDir(),
|
|
154
|
+
noExtensions: true,
|
|
155
|
+
systemPromptOverride: () => runtime.systemPrompt,
|
|
156
|
+
extensionFactories: [
|
|
157
|
+
createSubsessionBridge(request.ctx, runtime.meta, sessionManager.getSessionId()),
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
await resourceLoader.reload();
|
|
161
|
+
|
|
162
|
+
const { session } = await createAgentSession({
|
|
163
|
+
cwd: request.ctx.cwd,
|
|
164
|
+
model,
|
|
165
|
+
thinkingLevel:
|
|
166
|
+
runtime.meta.thinking_level ?? (request.id ? undefined : request.ctx.thinkingLevel),
|
|
167
|
+
resourceLoader,
|
|
168
|
+
sessionManager,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const availableTools = session.getAllTools().map((tool) => tool.name);
|
|
172
|
+
const activeTools = runtime.meta.tools ?? availableTools;
|
|
173
|
+
session.setActiveToolsByName(activeTools.filter((name) => !DISALLOWED_TOOLS.has(name)));
|
|
174
|
+
return session;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function createSubsession(params: CreateSubsessionParams): Promise<Subsession> {
|
|
178
|
+
const { cwd, onSnapshot, session, ...rest } = params;
|
|
179
|
+
const subsession: Subsession = {
|
|
180
|
+
...rest,
|
|
181
|
+
async exec(input: string, signal?: AbortSignal) {
|
|
182
|
+
if (!session) {
|
|
183
|
+
subsession.result = createErrorResult("Subsession unavailable");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let validationError: string | undefined;
|
|
188
|
+
const request = { session, input, signal, onSnapshot, usage: subsession.result.usage };
|
|
189
|
+
|
|
190
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
191
|
+
if (attempt === 1) {
|
|
192
|
+
request.input = `Output failed validation: ${validationError}\nReturn only corrected output required by <output_contract>.`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
subsession.result = await executeTurn(request);
|
|
196
|
+
if (!subsession.runtime.builtIn || subsession.result.status !== "done") {
|
|
197
|
+
validationError = undefined;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
validationError = validateBuiltInOutput(
|
|
202
|
+
subsession.runtime.agent,
|
|
203
|
+
subsession.result.output,
|
|
204
|
+
input,
|
|
205
|
+
);
|
|
206
|
+
if (!validationError) break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (validationError) {
|
|
210
|
+
subsession.result.status = "error";
|
|
211
|
+
subsession.result.output = `Output validation failed: ${validationError}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await saveSubsession(cwd, subsession);
|
|
215
|
+
},
|
|
216
|
+
async dispose() {
|
|
217
|
+
session?.dispose();
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
await saveSubsession(cwd, subsession);
|
|
222
|
+
return subsession;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function openSubsession(request: SubsessionRequest): Promise<Subsession> {
|
|
226
|
+
const pid = request.ctx.sessionManager.getSessionId();
|
|
227
|
+
const existing =
|
|
228
|
+
request.label !== "subagent" ? await findSubsession(request.ctx.cwd, request.id, pid) : null;
|
|
229
|
+
const agentName = existing?.agent ?? request.agent;
|
|
230
|
+
const runtime = await resolveRuntime(request.ctx.cwd, agentName);
|
|
231
|
+
|
|
232
|
+
const params: CreateSubsessionParams = {
|
|
233
|
+
cwd: request.ctx.cwd,
|
|
234
|
+
label: request.label,
|
|
235
|
+
pid,
|
|
236
|
+
title: "Untitled",
|
|
237
|
+
result: {
|
|
238
|
+
status: "done",
|
|
239
|
+
output: "",
|
|
240
|
+
usage: { input: 0, output: 0, toolCalls: 0, cost: 0 },
|
|
241
|
+
toolCounts: {},
|
|
242
|
+
},
|
|
243
|
+
runtime,
|
|
244
|
+
onSnapshot: request.onSnapshot,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
if (!existing && request.id) {
|
|
249
|
+
throw Error(`Subsession not found: ${request.id}`);
|
|
250
|
+
}
|
|
251
|
+
params.session = await createSdkSession(request, runtime);
|
|
252
|
+
if (existing && request.id) {
|
|
253
|
+
params.title = existing.title;
|
|
254
|
+
params.result.id = request.id;
|
|
255
|
+
params.result.usage = existing.usage;
|
|
256
|
+
params.result.output = await loadSubsessionOutput(request.ctx.cwd, request.id);
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
params.result = createErrorResult(error instanceof Error ? error.message : String(error));
|
|
260
|
+
if (params.session && !params.result.id) {
|
|
261
|
+
params.result.id = params.session.sessionId;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return createSubsession(params);
|
|
266
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { AgentSession, ContextUsage, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentMeta } from "../agent/types.js";
|
|
3
|
+
|
|
4
|
+
export type SubsessionStatus = "done" | "aborted" | "error";
|
|
5
|
+
export type SubsessionLabel = "plan" | "subagent";
|
|
6
|
+
|
|
7
|
+
export interface SubsessionUsage {
|
|
8
|
+
input: number;
|
|
9
|
+
output: number;
|
|
10
|
+
toolCalls: number;
|
|
11
|
+
cost: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SubsessionSnapshot {
|
|
15
|
+
id: string;
|
|
16
|
+
status: "running" | SubsessionStatus;
|
|
17
|
+
toolsUsed: string[];
|
|
18
|
+
usage: SubsessionUsage;
|
|
19
|
+
contextUsage?: ContextUsage;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SubsessionResult {
|
|
23
|
+
id?: string;
|
|
24
|
+
status: SubsessionStatus;
|
|
25
|
+
output: string;
|
|
26
|
+
usage: SubsessionUsage;
|
|
27
|
+
toolCounts: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RuntimeConfig {
|
|
31
|
+
agent: string;
|
|
32
|
+
builtIn: boolean;
|
|
33
|
+
meta: AgentMeta;
|
|
34
|
+
systemPrompt: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SubsessionRequest {
|
|
38
|
+
ctx: ExtensionContext;
|
|
39
|
+
label: SubsessionLabel;
|
|
40
|
+
agent: string;
|
|
41
|
+
id?: string;
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
onSnapshot: (snapshot: SubsessionSnapshot) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StoredSubsessions {
|
|
47
|
+
[id: string]: {
|
|
48
|
+
agent: string;
|
|
49
|
+
label: SubsessionLabel;
|
|
50
|
+
pid: string;
|
|
51
|
+
title: string;
|
|
52
|
+
usage: SubsessionUsage;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Subsession {
|
|
57
|
+
pid: string;
|
|
58
|
+
label: SubsessionLabel;
|
|
59
|
+
title: string;
|
|
60
|
+
result: SubsessionResult;
|
|
61
|
+
runtime: RuntimeConfig;
|
|
62
|
+
exec(input: string, signal?: AbortSignal): Promise<void>;
|
|
63
|
+
dispose(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ExecuteTurnRequest {
|
|
67
|
+
input: string;
|
|
68
|
+
session: AgentSession;
|
|
69
|
+
usage: SubsessionUsage;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
onSnapshot: (snapshot: SubsessionSnapshot) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CreateSubsessionParams {
|
|
75
|
+
pid: string;
|
|
76
|
+
cwd: string;
|
|
77
|
+
title: string;
|
|
78
|
+
label: SubsessionLabel;
|
|
79
|
+
result: SubsessionResult;
|
|
80
|
+
runtime: RuntimeConfig;
|
|
81
|
+
session?: AgentSession;
|
|
82
|
+
onSnapshot: (snapshot: SubsessionSnapshot) => void;
|
|
83
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
function validatePlannerOutput(output: string): string | undefined {
|
|
2
|
+
const headings = [
|
|
3
|
+
"Objective",
|
|
4
|
+
"Out of scope",
|
|
5
|
+
"Assumptions",
|
|
6
|
+
"Steps",
|
|
7
|
+
"Risks & Mitigations",
|
|
8
|
+
"Handoff Packet",
|
|
9
|
+
"Open Questions",
|
|
10
|
+
];
|
|
11
|
+
const optionalHeadings = ["Assumptions", "Open Questions"];
|
|
12
|
+
const mainHeadings = [...output.matchAll(/^#\s+(.+?)\s*$/gm)];
|
|
13
|
+
const firstLine = output.trimStart().split(/\r?\n/, 1)[0]?.trim();
|
|
14
|
+
if (mainHeadings.length !== 1 || !firstLine || !/^# Plan:\s+\S/.test(firstLine)) {
|
|
15
|
+
return "Plan output must start with '# Plan: [title]'.";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sectionHeadings = [...output.matchAll(/^##\s+(.+?)\s*$/gm)].map((match) =>
|
|
19
|
+
match[1]!.trim(),
|
|
20
|
+
);
|
|
21
|
+
let headingIndex = 0;
|
|
22
|
+
for (const sectionHeading of sectionHeadings) {
|
|
23
|
+
while (
|
|
24
|
+
headingIndex < headings.length &&
|
|
25
|
+
headings[headingIndex] !== sectionHeading &&
|
|
26
|
+
optionalHeadings.includes(headings[headingIndex]!)
|
|
27
|
+
) {
|
|
28
|
+
headingIndex += 1;
|
|
29
|
+
}
|
|
30
|
+
if (headings[headingIndex] !== sectionHeading) {
|
|
31
|
+
return `Plan output has unexpected or out-of-order "## ${sectionHeading}".`;
|
|
32
|
+
}
|
|
33
|
+
headingIndex += 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (; headingIndex < headings.length; headingIndex += 1) {
|
|
37
|
+
if (!optionalHeadings.includes(headings[headingIndex]!)) {
|
|
38
|
+
return `Plan output is missing "## ${headings[headingIndex]}".`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateScoutOutput(output: string, input: string): string | undefined {
|
|
44
|
+
const depthMatch = input.match(/^Depth:\s*(quick|standard|deep)\s*$/im);
|
|
45
|
+
const depth = depthMatch?.[1]?.toLowerCase() ?? "standard";
|
|
46
|
+
const headings =
|
|
47
|
+
depth === "quick"
|
|
48
|
+
? ["Answer", "Evidence", "Gaps"]
|
|
49
|
+
: depth === "deep"
|
|
50
|
+
? [
|
|
51
|
+
"Answer",
|
|
52
|
+
"Execution flow",
|
|
53
|
+
"Relevant locations",
|
|
54
|
+
"Change impact",
|
|
55
|
+
"Verification",
|
|
56
|
+
"Evidence",
|
|
57
|
+
"Gaps",
|
|
58
|
+
]
|
|
59
|
+
: ["Answer", "Execution flow", "Relevant locations", "Evidence", "Gaps"];
|
|
60
|
+
const firstLine = output.trimStart().split(/\r?\n/, 1)[0]?.trim();
|
|
61
|
+
if (firstLine !== "## Answer") {
|
|
62
|
+
return "Scout output must start with '## Answer'.";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sections = [...output.matchAll(/^##\s+(.+?)\s*$/gm)];
|
|
66
|
+
for (let headingIndex = 0; headingIndex < headings.length; headingIndex += 1) {
|
|
67
|
+
const actualHeading = sections[headingIndex]?.[1]?.trim();
|
|
68
|
+
if (!actualHeading) return `Scout output is missing "## ${headings[headingIndex]}".`;
|
|
69
|
+
if (actualHeading !== headings[headingIndex]) {
|
|
70
|
+
return `Scout output has unexpected or out-of-order "## ${actualHeading}".`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (sections.length > headings.length) {
|
|
74
|
+
return `Scout output has unexpected or out-of-order "## ${sections[headings.length]![1]!.trim()}".`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex += 1) {
|
|
78
|
+
const contentStart = (sections[sectionIndex]!.index ?? 0) + sections[sectionIndex]![0].length;
|
|
79
|
+
const contentEnd = sections[sectionIndex + 1]?.index ?? output.length;
|
|
80
|
+
if (!output.slice(contentStart, contentEnd).trim()) {
|
|
81
|
+
return `Scout output section "## ${sections[sectionIndex]![1]!.trim()}" cannot be empty.`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const evidenceIndex = headings.indexOf("Evidence");
|
|
86
|
+
const evidenceStart = (sections[evidenceIndex]!.index ?? 0) + sections[evidenceIndex]![0].length;
|
|
87
|
+
const evidenceEnd = sections[evidenceIndex + 1]?.index ?? output.length;
|
|
88
|
+
if (!/^- `[^`\r\n]+` — `[^`\r\n]+`: \S+/m.test(output.slice(evidenceStart, evidenceEnd))) {
|
|
89
|
+
return "Scout output must include evidence as '- `path:line` — `symbol`: finding'.";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateBuiltInOutput(
|
|
94
|
+
agent: string,
|
|
95
|
+
output: string,
|
|
96
|
+
input: string,
|
|
97
|
+
): string | undefined {
|
|
98
|
+
if (agent === "planner") return validatePlannerOutput(output);
|
|
99
|
+
if (agent === "scout") return validateScoutOutput(output, input);
|
|
100
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Key, matchesKey, type Component, type Focusable, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Lines } from "./lines.js";
|
|
4
|
+
import { PlaceholderInput } from "./placeholder-input.js";
|
|
5
|
+
|
|
6
|
+
export type ActionSelectOption = {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ActionSelectResult =
|
|
12
|
+
| { type: "option"; value: string; index: number }
|
|
13
|
+
| { type: "input"; value: string };
|
|
14
|
+
|
|
15
|
+
export type ActionSelectListOptions = {
|
|
16
|
+
title: string;
|
|
17
|
+
options: ActionSelectOption[];
|
|
18
|
+
placeholder: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class ActionSelectList implements Component, Focusable {
|
|
22
|
+
onSubmit?: (result: ActionSelectResult) => void;
|
|
23
|
+
onCancel?: () => void;
|
|
24
|
+
|
|
25
|
+
private readonly input: PlaceholderInput;
|
|
26
|
+
private readonly options: ActionSelectOption[];
|
|
27
|
+
private readonly title: string;
|
|
28
|
+
|
|
29
|
+
private cursor = 0;
|
|
30
|
+
private _focused = false;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
tui: TUI,
|
|
34
|
+
keybindings: KeybindingsManager,
|
|
35
|
+
private readonly theme: Theme,
|
|
36
|
+
options: ActionSelectListOptions,
|
|
37
|
+
) {
|
|
38
|
+
this.title = options.title;
|
|
39
|
+
this.options = options.options;
|
|
40
|
+
this.input = new PlaceholderInput(tui, keybindings, theme, options.placeholder);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get focused(): boolean {
|
|
44
|
+
return this._focused;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
set focused(value: boolean) {
|
|
48
|
+
this._focused = value;
|
|
49
|
+
this.syncInputFocus();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
render(width: number): string[] {
|
|
53
|
+
const lines = new Lines(width);
|
|
54
|
+
|
|
55
|
+
lines.add(this.theme.bold(this.title));
|
|
56
|
+
for (const [optionIndex, option] of this.options.entries()) {
|
|
57
|
+
lines.add(this.renderOptionLine(optionIndex, option.label));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const inputOptionLines = this.renderInputOptionLines(width);
|
|
61
|
+
for (const inputOptionLine of inputOptionLines) {
|
|
62
|
+
lines.add(inputOptionLine);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return lines.get();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
invalidate() {
|
|
69
|
+
this.input.invalidate();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handleInput(data: string) {
|
|
73
|
+
if (matchesKey(data, Key.escape)) {
|
|
74
|
+
this.onCancel?.();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (this.isInputSelected()) {
|
|
78
|
+
this.handleInputEditingMode(data);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (matchesKey(data, Key.up)) {
|
|
82
|
+
this.moveCursor(-1);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (matchesKey(data, Key.down)) {
|
|
86
|
+
this.moveCursor(1);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (matchesKey(data, Key.enter) && data !== "\n") {
|
|
90
|
+
this.commitSelection();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private handleInputEditingMode(data: string) {
|
|
96
|
+
if (matchesKey(data, Key.up)) {
|
|
97
|
+
this.moveCursor(-1);
|
|
98
|
+
this.syncInputFocus();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (matchesKey(data, Key.enter) && data !== "\n") {
|
|
102
|
+
this.submitInputSelection();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.input.handleInput(data);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private commitSelection() {
|
|
109
|
+
const selected = this.options[this.cursor];
|
|
110
|
+
if (!selected) return;
|
|
111
|
+
this.onSubmit?.({ type: "option", value: selected.value, index: this.cursor });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private submitInputSelection() {
|
|
115
|
+
const trimmedInput = this.input.getText().trim();
|
|
116
|
+
if (trimmedInput.length === 0) return;
|
|
117
|
+
this.onSubmit?.({ type: "input", value: trimmedInput });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private renderOptionLine(optionIndex: number, label: string): string {
|
|
121
|
+
const selected = optionIndex === this.cursor;
|
|
122
|
+
const prefix = selected ? "→" : " ";
|
|
123
|
+
const content = `${prefix} ${optionIndex + 1}. ${label}`;
|
|
124
|
+
return this.theme.fg(selected ? "accent" : "text", content);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private renderInputOptionLines(width: number): string[] {
|
|
128
|
+
const inputOptionIndex = this.options.length;
|
|
129
|
+
const selected = inputOptionIndex === this.cursor;
|
|
130
|
+
const prefix = selected ? "→" : " ";
|
|
131
|
+
const marker = `${prefix} ${inputOptionIndex + 1}. `;
|
|
132
|
+
const markerColor = selected ? "accent" : "text";
|
|
133
|
+
const markerText = this.theme.fg(markerColor, marker);
|
|
134
|
+
|
|
135
|
+
const inputContentWidth = Math.max(1, width - marker.length);
|
|
136
|
+
const inputLines = this.input.render(inputContentWidth);
|
|
137
|
+
if (inputLines.length === 0) {
|
|
138
|
+
return [markerText];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const renderedLines: string[] = [];
|
|
142
|
+
renderedLines.push(`${markerText}${inputLines[0] ?? ""}`);
|
|
143
|
+
|
|
144
|
+
const continuationIndent = marker.length;
|
|
145
|
+
for (const continuationLine of inputLines.slice(1)) {
|
|
146
|
+
renderedLines.push(`${" ".repeat(continuationIndent)}${continuationLine}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return renderedLines;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private moveCursor(delta: number) {
|
|
153
|
+
const lastIndex = this.options.length;
|
|
154
|
+
this.cursor = Math.max(0, Math.min(lastIndex, this.cursor + delta));
|
|
155
|
+
this.syncInputFocus();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private isInputSelected(): boolean {
|
|
159
|
+
return this.cursor === this.options.length;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private syncInputFocus() {
|
|
163
|
+
this.input.focused = this._focused && this.isInputSelected();
|
|
164
|
+
}
|
|
165
|
+
}
|