@aaroncql/pim-agent 0.0.1
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 +212 -0
- package/bin/pim.ts +109 -0
- package/package.json +49 -0
- package/src/extensions/_init/index.ts +109 -0
- package/src/extensions/bash/capture.test.ts +126 -0
- package/src/extensions/bash/capture.ts +80 -0
- package/src/extensions/bash/format.test.ts +240 -0
- package/src/extensions/bash/format.ts +76 -0
- package/src/extensions/bash/index.ts +86 -0
- package/src/extensions/bash/run.test.ts +262 -0
- package/src/extensions/bash/run.ts +207 -0
- package/src/extensions/bash/schema.ts +54 -0
- package/src/extensions/command-picker/index.ts +52 -0
- package/src/extensions/command-picker/ranker.test.ts +46 -0
- package/src/extensions/command-picker/ranker.ts +17 -0
- package/src/extensions/edit/edit.test.ts +285 -0
- package/src/extensions/edit/edit.ts +382 -0
- package/src/extensions/edit/index.ts +54 -0
- package/src/extensions/edit/schema.ts +37 -0
- package/src/extensions/file-picker/catalog.test.ts +263 -0
- package/src/extensions/file-picker/catalog.ts +219 -0
- package/src/extensions/file-picker/index.test.ts +168 -0
- package/src/extensions/file-picker/index.ts +119 -0
- package/src/extensions/file-picker/ranker.test.ts +94 -0
- package/src/extensions/file-picker/ranker.ts +76 -0
- package/src/extensions/footer/git.test.ts +76 -0
- package/src/extensions/footer/git.ts +87 -0
- package/src/extensions/footer/index.test.ts +161 -0
- package/src/extensions/footer/index.ts +148 -0
- package/src/extensions/footer/powerline.ts +87 -0
- package/src/extensions/footer/segments.test.ts +164 -0
- package/src/extensions/footer/segments.ts +234 -0
- package/src/extensions/glob/glob.test.ts +171 -0
- package/src/extensions/glob/glob.ts +34 -0
- package/src/extensions/glob/index.test.ts +68 -0
- package/src/extensions/glob/index.ts +136 -0
- package/src/extensions/glob/render.test.ts +126 -0
- package/src/extensions/glob/render.ts +74 -0
- package/src/extensions/glob/schema.ts +52 -0
- package/src/extensions/grep/grep.test.ts +387 -0
- package/src/extensions/grep/grep.ts +215 -0
- package/src/extensions/grep/index.test.ts +68 -0
- package/src/extensions/grep/index.ts +158 -0
- package/src/extensions/grep/render.test.ts +269 -0
- package/src/extensions/grep/render.ts +243 -0
- package/src/extensions/grep/schema.ts +92 -0
- package/src/extensions/read/index.ts +84 -0
- package/src/extensions/read/read.test.ts +177 -0
- package/src/extensions/read/read.ts +206 -0
- package/src/extensions/read/render.test.ts +61 -0
- package/src/extensions/read/render.ts +33 -0
- package/src/extensions/read/schema.ts +27 -0
- package/src/extensions/subagent/index.test.ts +44 -0
- package/src/extensions/subagent/index.ts +30 -0
- package/src/extensions/subagent/render.test.ts +292 -0
- package/src/extensions/subagent/render.ts +359 -0
- package/src/extensions/subagent/schema.ts +9 -0
- package/src/extensions/subagent/subagent.test.ts +315 -0
- package/src/extensions/subagent/subagent.ts +418 -0
- package/src/extensions/system-prompt/index.ts +28 -0
- package/src/extensions/system-prompt/prompt.test.ts +64 -0
- package/src/extensions/system-prompt/prompt.ts +213 -0
- package/src/extensions/todo/index.test.ts +244 -0
- package/src/extensions/todo/index.ts +122 -0
- package/src/extensions/todo/render.test.ts +180 -0
- package/src/extensions/todo/render.ts +172 -0
- package/src/extensions/todo/schema.ts +24 -0
- package/src/extensions/todo/todo.test.ts +222 -0
- package/src/extensions/todo/todo.ts +188 -0
- package/src/extensions/tps/index.test.ts +254 -0
- package/src/extensions/tps/index.ts +136 -0
- package/src/extensions/web-fetch/JinaReaderClient.ts +230 -0
- package/src/extensions/web-fetch/WebViewFetchClient.ts +186 -0
- package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +119 -0
- package/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +511 -0
- package/src/extensions/web-fetch/fetch.test.ts +244 -0
- package/src/extensions/web-fetch/fetch.ts +249 -0
- package/src/extensions/web-fetch/index.ts +107 -0
- package/src/extensions/web-fetch/render.test.ts +56 -0
- package/src/extensions/web-fetch/render.ts +39 -0
- package/src/extensions/web-fetch/schema.ts +23 -0
- package/src/extensions/web-search/ExaMcpClient.test.ts +143 -0
- package/src/extensions/web-search/ExaMcpClient.ts +258 -0
- package/src/extensions/web-search/index.ts +118 -0
- package/src/extensions/web-search/render.test.ts +21 -0
- package/src/extensions/web-search/render.ts +9 -0
- package/src/extensions/web-search/schema.ts +21 -0
- package/src/extensions/web-search/search.test.ts +53 -0
- package/src/extensions/web-search/search.ts +23 -0
- package/src/extensions/working-indicator/index.test.ts +21 -0
- package/src/extensions/working-indicator/index.ts +77 -0
- package/src/extensions/write/index.ts +76 -0
- package/src/extensions/write/render.test.ts +64 -0
- package/src/extensions/write/schema.ts +14 -0
- package/src/extensions/write/write.test.ts +108 -0
- package/src/extensions/write/write.ts +104 -0
- package/src/shared/DiffLines.test.ts +193 -0
- package/src/shared/DiffLines.ts +307 -0
- package/src/shared/DiffRenderer.test.ts +206 -0
- package/src/shared/DiffRenderer.ts +396 -0
- package/src/shared/DiffView.ts +199 -0
- package/src/shared/EditMatcher.test.ts +123 -0
- package/src/shared/EditMatcher.ts +826 -0
- package/src/shared/FileScanner.test.ts +158 -0
- package/src/shared/FileScanner.ts +41 -0
- package/src/shared/Fs.ts +46 -0
- package/src/shared/FsErrors.ts +72 -0
- package/src/shared/FuzzyMatcher.test.ts +114 -0
- package/src/shared/FuzzyMatcher.ts +73 -0
- package/src/shared/GitignoreFilter.test.ts +64 -0
- package/src/shared/GitignoreFilter.ts +142 -0
- package/src/shared/GlobExclusions.ts +23 -0
- package/src/shared/Levenshtein.ts +33 -0
- package/src/shared/Lines.test.ts +25 -0
- package/src/shared/Lines.ts +77 -0
- package/src/shared/McpClient.test.ts +235 -0
- package/src/shared/McpClient.ts +406 -0
- package/src/shared/OutputBudget.test.ts +99 -0
- package/src/shared/OutputBudget.ts +79 -0
- package/src/shared/Paths.test.ts +51 -0
- package/src/shared/Paths.ts +52 -0
- package/src/shared/PimSettings.test.ts +90 -0
- package/src/shared/PimSettings.ts +124 -0
- package/src/shared/Renderer.test.ts +190 -0
- package/src/shared/Renderer.ts +256 -0
- package/src/shared/SpillCache.test.ts +94 -0
- package/src/shared/SpillCache.ts +89 -0
- package/src/shared/Tools.test.ts +392 -0
- package/src/shared/Tools.ts +636 -0
- package/src/telegram/Bot.ts +198 -0
- package/src/telegram/Commands.ts +721 -0
- package/src/telegram/Config.test.ts +275 -0
- package/src/telegram/Config.ts +162 -0
- package/src/telegram/Markdown.test.ts +143 -0
- package/src/telegram/Markdown.ts +177 -0
- package/src/telegram/Message.ts +211 -0
- package/src/telegram/Renderer.test.ts +216 -0
- package/src/telegram/Renderer.ts +713 -0
- package/src/telegram/SendFileSchema.ts +19 -0
- package/src/telegram/SendFileTool.ts +94 -0
- package/src/telegram/Session.ts +579 -0
- package/src/telegram/SessionRegistry.test.ts +89 -0
- package/src/telegram/SessionRegistry.ts +170 -0
- package/src/telegram/Supervisor.ts +357 -0
- package/src/telegram/TaskScheduler.test.ts +278 -0
- package/src/telegram/TaskScheduler.ts +293 -0
- package/src/telegram/TaskSchema.ts +88 -0
- package/src/telegram/TaskStore.ts +73 -0
- package/src/telegram/TaskTool.test.ts +179 -0
- package/src/telegram/TaskTool.ts +159 -0
- package/src/telegram/TypingIndicator.ts +43 -0
- package/src/telegram/index.ts +32 -0
- package/src/themes/pim-dark.json +84 -0
- package/src/themes/pim-light.json +84 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type {
|
|
3
|
+
AgentSessionEvent,
|
|
4
|
+
AgentToolResult,
|
|
5
|
+
AgentToolUpdateCallback,
|
|
6
|
+
ExtensionContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
AuthStorage,
|
|
10
|
+
createAgentSession,
|
|
11
|
+
DefaultResourceLoader,
|
|
12
|
+
getAgentDir,
|
|
13
|
+
SessionManager,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type {
|
|
16
|
+
AssistantMessage,
|
|
17
|
+
TextContent,
|
|
18
|
+
Usage,
|
|
19
|
+
} from "@earendil-works/pi-ai";
|
|
20
|
+
import { formatTopLine } from "./render";
|
|
21
|
+
|
|
22
|
+
export const PER_TASK_OUTPUT_CAP = 32 * 1024;
|
|
23
|
+
export const SUBAGENT_TOOL_NAME = "subagent";
|
|
24
|
+
|
|
25
|
+
const inSubagent = new AsyncLocalStorage<true>();
|
|
26
|
+
|
|
27
|
+
export type SubagentUsage = {
|
|
28
|
+
readonly input: number;
|
|
29
|
+
readonly output: number;
|
|
30
|
+
readonly cacheRead: number;
|
|
31
|
+
readonly cacheWrite: number;
|
|
32
|
+
readonly cost: number;
|
|
33
|
+
readonly turns: number;
|
|
34
|
+
readonly contextTokens: number | undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type SubagentToolCall = {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly isError: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type SubagentDetails = {
|
|
43
|
+
readonly returnedOutput: string;
|
|
44
|
+
readonly fullOutput: string;
|
|
45
|
+
readonly outputTruncated: boolean;
|
|
46
|
+
readonly omittedBytes: number;
|
|
47
|
+
readonly usage: SubagentUsage;
|
|
48
|
+
readonly toolCalls: readonly SubagentToolCall[];
|
|
49
|
+
readonly activeToolNames: readonly string[];
|
|
50
|
+
readonly lastToolName: string | undefined;
|
|
51
|
+
readonly stopReason: string | undefined;
|
|
52
|
+
readonly errorMessage: string | undefined;
|
|
53
|
+
readonly model: string | undefined;
|
|
54
|
+
readonly contextWindow: number | undefined;
|
|
55
|
+
readonly topLine: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type SubagentSnapshot = {
|
|
59
|
+
readonly finalOutput: string;
|
|
60
|
+
readonly usage: SubagentUsage;
|
|
61
|
+
readonly toolCalls: readonly SubagentToolCall[];
|
|
62
|
+
readonly activeToolNames: readonly string[];
|
|
63
|
+
readonly lastToolName: string | undefined;
|
|
64
|
+
readonly stopReason: string | undefined;
|
|
65
|
+
readonly errorMessage: string | undefined;
|
|
66
|
+
readonly model: string | undefined;
|
|
67
|
+
readonly contextWindow: number | undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type SubagentSession = {
|
|
71
|
+
readonly subscribe: (
|
|
72
|
+
listener: (event: AgentSessionEvent) => void
|
|
73
|
+
) => () => void;
|
|
74
|
+
readonly prompt: (prompt: string) => Promise<void>;
|
|
75
|
+
readonly abort: () => Promise<void>;
|
|
76
|
+
readonly dispose: () => void;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type CreateSubagentSession = (
|
|
80
|
+
parentCtx: ExtensionContext,
|
|
81
|
+
activeToolNames: readonly string[] | undefined
|
|
82
|
+
) => Promise<SubagentSession>;
|
|
83
|
+
|
|
84
|
+
export function childToolNames(
|
|
85
|
+
activeToolNames: readonly string[]
|
|
86
|
+
): readonly string[] {
|
|
87
|
+
return activeToolNames.filter((name) => name !== SUBAGENT_TOOL_NAME);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function createSdkSubagentSession(
|
|
91
|
+
parentCtx: ExtensionContext,
|
|
92
|
+
activeToolNames: readonly string[] | undefined
|
|
93
|
+
): Promise<SubagentSession> {
|
|
94
|
+
const loader = new DefaultResourceLoader({
|
|
95
|
+
cwd: parentCtx.cwd,
|
|
96
|
+
agentDir: getAgentDir(),
|
|
97
|
+
});
|
|
98
|
+
await loader.reload();
|
|
99
|
+
|
|
100
|
+
const { session } = await createAgentSession({
|
|
101
|
+
cwd: parentCtx.cwd,
|
|
102
|
+
model: parentCtx.model,
|
|
103
|
+
modelRegistry: parentCtx.modelRegistry,
|
|
104
|
+
authStorage: AuthStorage.create(),
|
|
105
|
+
sessionManager: SessionManager.inMemory(parentCtx.cwd),
|
|
106
|
+
resourceLoader: loader,
|
|
107
|
+
tools: activeToolNames ? [...childToolNames(activeToolNames)] : undefined,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return session;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function runSubagent(
|
|
114
|
+
prompt: string,
|
|
115
|
+
parentCtx: ExtensionContext,
|
|
116
|
+
signal?: AbortSignal,
|
|
117
|
+
onUpdate?: AgentToolUpdateCallback<SubagentDetails>,
|
|
118
|
+
createSession: CreateSubagentSession = createSdkSubagentSession,
|
|
119
|
+
activeToolNames?: readonly string[]
|
|
120
|
+
): Promise<AgentToolResult<SubagentDetails>> {
|
|
121
|
+
// Hard block against subagent recursion
|
|
122
|
+
if (inSubagent.getStore()) {
|
|
123
|
+
throw new Error("subagents cannot call subagent tool");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return inSubagent.run(true, async () => {
|
|
127
|
+
const capture = new SubagentEventCapture(onUpdate, {
|
|
128
|
+
contextWindow: parentCtx.model?.contextWindow,
|
|
129
|
+
model: parentCtx.model?.id,
|
|
130
|
+
});
|
|
131
|
+
let session: SubagentSession | undefined;
|
|
132
|
+
let thrown: unknown;
|
|
133
|
+
let abortRequested = false;
|
|
134
|
+
let abortPromise: Promise<void> | undefined;
|
|
135
|
+
|
|
136
|
+
const ensureAbort = (): Promise<void> => {
|
|
137
|
+
if (!session) {
|
|
138
|
+
return Promise.resolve();
|
|
139
|
+
}
|
|
140
|
+
abortPromise ??= session.abort().catch(() => {});
|
|
141
|
+
return abortPromise;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const onAbort = () => {
|
|
145
|
+
abortRequested = true;
|
|
146
|
+
void ensureAbort();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
session = await createSession(parentCtx, activeToolNames);
|
|
151
|
+
session.subscribe((event) => capture.handle(event));
|
|
152
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
153
|
+
if (signal?.aborted) {
|
|
154
|
+
abortRequested = true;
|
|
155
|
+
throw new Error("subagent aborted before start");
|
|
156
|
+
}
|
|
157
|
+
await session.prompt(prompt);
|
|
158
|
+
if (abortRequested && capture.snapshot().stopReason !== "aborted") {
|
|
159
|
+
capture.markAborted();
|
|
160
|
+
}
|
|
161
|
+
} catch (err) {
|
|
162
|
+
thrown = err;
|
|
163
|
+
} finally {
|
|
164
|
+
signal?.removeEventListener("abort", onAbort);
|
|
165
|
+
await ensureAbort();
|
|
166
|
+
session?.dispose();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const snapshot = capture.snapshot();
|
|
170
|
+
if (thrown !== undefined) {
|
|
171
|
+
throw makeFailureError(
|
|
172
|
+
thrownMessage(thrown),
|
|
173
|
+
undefined,
|
|
174
|
+
snapshot.finalOutput
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (snapshot.stopReason === "error" || snapshot.stopReason === "aborted") {
|
|
178
|
+
throw makeFailureError(
|
|
179
|
+
snapshot.stopReason,
|
|
180
|
+
snapshot.errorMessage,
|
|
181
|
+
snapshot.finalOutput
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const details = capture.details();
|
|
186
|
+
const text =
|
|
187
|
+
details.returnedOutput ||
|
|
188
|
+
"[subagent tool: completed with no text output.]";
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text }],
|
|
191
|
+
details,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export class SubagentEventCapture {
|
|
197
|
+
private finalOutput = "";
|
|
198
|
+
private pendingMessage: AssistantMessage | undefined;
|
|
199
|
+
private readonly usage: MutableUsage = emptyUsage();
|
|
200
|
+
private readonly toolCalls: SubagentToolCall[] = [];
|
|
201
|
+
private readonly activeToolsById = new Map<string, string>();
|
|
202
|
+
private lastToolName: string | undefined;
|
|
203
|
+
private stopReason: string | undefined;
|
|
204
|
+
private errorMessage: string | undefined;
|
|
205
|
+
private model: string | undefined;
|
|
206
|
+
|
|
207
|
+
public constructor(
|
|
208
|
+
private readonly onUpdate?: AgentToolUpdateCallback<SubagentDetails>,
|
|
209
|
+
private readonly options: {
|
|
210
|
+
readonly contextWindow?: number;
|
|
211
|
+
readonly model?: string;
|
|
212
|
+
} = {}
|
|
213
|
+
) {
|
|
214
|
+
this.model = options.model;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public handle(event: AgentSessionEvent): void {
|
|
218
|
+
if (event.type === "message_start" && isAssistantMessage(event.message)) {
|
|
219
|
+
this.finalOutput = "";
|
|
220
|
+
this.pendingMessage = undefined;
|
|
221
|
+
this.emitUpdate();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (event.type === "message_update" && isAssistantMessage(event.message)) {
|
|
226
|
+
this.pendingMessage = event.message;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (event.type === "message_end" && isAssistantMessage(event.message)) {
|
|
231
|
+
this.pendingMessage = undefined;
|
|
232
|
+
this.finalOutput = collectText(event.message);
|
|
233
|
+
addUsage(this.usage, event.message.usage);
|
|
234
|
+
this.usage.turns += 1;
|
|
235
|
+
this.stopReason = event.message.stopReason;
|
|
236
|
+
this.errorMessage = event.message.errorMessage;
|
|
237
|
+
this.model = event.message.model;
|
|
238
|
+
this.emitUpdate();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (event.type === "tool_execution_start") {
|
|
243
|
+
this.activeToolsById.set(event.toolCallId, event.toolName);
|
|
244
|
+
this.lastToolName = event.toolName;
|
|
245
|
+
this.emitUpdate();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (event.type === "tool_execution_end") {
|
|
250
|
+
this.activeToolsById.delete(event.toolCallId);
|
|
251
|
+
this.toolCalls.push({ name: event.toolName, isError: event.isError });
|
|
252
|
+
this.lastToolName = event.toolName;
|
|
253
|
+
this.emitUpdate();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public markAborted(): void {
|
|
258
|
+
this.stopReason = "aborted";
|
|
259
|
+
this.emitUpdate();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public snapshot(): SubagentSnapshot {
|
|
263
|
+
this.materializePending();
|
|
264
|
+
return {
|
|
265
|
+
finalOutput: this.finalOutput,
|
|
266
|
+
usage: freezeUsage(this.usage),
|
|
267
|
+
toolCalls: [...this.toolCalls],
|
|
268
|
+
activeToolNames: Array.from(new Set(this.activeToolsById.values())),
|
|
269
|
+
lastToolName: this.lastToolName,
|
|
270
|
+
stopReason: this.stopReason,
|
|
271
|
+
errorMessage: this.errorMessage,
|
|
272
|
+
model: this.model,
|
|
273
|
+
contextWindow: this.options.contextWindow,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public details(): SubagentDetails {
|
|
278
|
+
const snapshot = this.snapshot();
|
|
279
|
+
const cap = applyOutputCap(snapshot.finalOutput);
|
|
280
|
+
return detailsFromSnapshot(snapshot, cap.text, cap);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private materializePending(): void {
|
|
284
|
+
if (this.pendingMessage) {
|
|
285
|
+
this.finalOutput = collectText(this.pendingMessage);
|
|
286
|
+
this.pendingMessage = undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private emitUpdate(): void {
|
|
291
|
+
if (!this.onUpdate) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const details = this.details();
|
|
295
|
+
this.onUpdate({
|
|
296
|
+
content: [{ type: "text", text: details.topLine }],
|
|
297
|
+
details,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
type MutableUsage = {
|
|
303
|
+
input: number;
|
|
304
|
+
output: number;
|
|
305
|
+
cacheRead: number;
|
|
306
|
+
cacheWrite: number;
|
|
307
|
+
cost: number;
|
|
308
|
+
turns: number;
|
|
309
|
+
contextTokens: number | undefined;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export type OutputCapResult = {
|
|
313
|
+
readonly text: string;
|
|
314
|
+
readonly truncated: boolean;
|
|
315
|
+
readonly omittedBytes: number;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
export function applyOutputCap(
|
|
319
|
+
text: string,
|
|
320
|
+
capBytes = PER_TASK_OUTPUT_CAP
|
|
321
|
+
): OutputCapResult {
|
|
322
|
+
const encoder = new TextEncoder();
|
|
323
|
+
const totalBytes = encoder.encode(text).byteLength;
|
|
324
|
+
if (totalBytes <= capBytes) {
|
|
325
|
+
return { text, truncated: false, omittedBytes: 0 };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const buffer = new Uint8Array(capBytes);
|
|
329
|
+
const { read, written } = encoder.encodeInto(text, buffer);
|
|
330
|
+
const out = text.slice(0, read);
|
|
331
|
+
const omittedBytes = totalBytes - written;
|
|
332
|
+
return {
|
|
333
|
+
text: `${out}\n[subagent: output truncated, ${omittedBytes} bytes omitted; full output preserved in tool details.]`,
|
|
334
|
+
truncated: true,
|
|
335
|
+
omittedBytes,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function makeFailureError(
|
|
340
|
+
reason: string,
|
|
341
|
+
errorMessage: string | undefined,
|
|
342
|
+
partialOutput: string
|
|
343
|
+
): Error {
|
|
344
|
+
const capped = applyOutputCap(partialOutput);
|
|
345
|
+
return new Error(
|
|
346
|
+
`Subagent failed: ${reason}. Error: ${errorMessage ?? "none"}.\n` +
|
|
347
|
+
`Partial output before failure:\n${capped.text}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function detailsFromSnapshot(
|
|
352
|
+
snapshot: SubagentSnapshot,
|
|
353
|
+
returnedOutput: string,
|
|
354
|
+
capResult: OutputCapResult
|
|
355
|
+
): SubagentDetails {
|
|
356
|
+
return {
|
|
357
|
+
returnedOutput,
|
|
358
|
+
fullOutput: snapshot.finalOutput,
|
|
359
|
+
outputTruncated: capResult.truncated,
|
|
360
|
+
omittedBytes: capResult.omittedBytes,
|
|
361
|
+
usage: snapshot.usage,
|
|
362
|
+
toolCalls: snapshot.toolCalls,
|
|
363
|
+
activeToolNames: snapshot.activeToolNames,
|
|
364
|
+
lastToolName: snapshot.lastToolName,
|
|
365
|
+
stopReason: snapshot.stopReason,
|
|
366
|
+
errorMessage: snapshot.errorMessage,
|
|
367
|
+
model: snapshot.model,
|
|
368
|
+
contextWindow: snapshot.contextWindow,
|
|
369
|
+
topLine: formatTopLine(snapshot),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function collectText(message: AssistantMessage): string {
|
|
374
|
+
return message.content
|
|
375
|
+
.filter((part): part is TextContent => part.type === "text")
|
|
376
|
+
.map((part) => part.text)
|
|
377
|
+
.join("");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isAssistantMessage(message: unknown): message is AssistantMessage {
|
|
381
|
+
return (
|
|
382
|
+
typeof message === "object" &&
|
|
383
|
+
message !== null &&
|
|
384
|
+
"role" in message &&
|
|
385
|
+
message.role === "assistant" &&
|
|
386
|
+
"content" in message &&
|
|
387
|
+
Array.isArray(message.content)
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function emptyUsage(): MutableUsage {
|
|
392
|
+
return {
|
|
393
|
+
input: 0,
|
|
394
|
+
output: 0,
|
|
395
|
+
cacheRead: 0,
|
|
396
|
+
cacheWrite: 0,
|
|
397
|
+
cost: 0,
|
|
398
|
+
turns: 0,
|
|
399
|
+
contextTokens: undefined,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function freezeUsage(usage: MutableUsage): SubagentUsage {
|
|
404
|
+
return { ...usage };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function addUsage(target: MutableUsage, usage: Usage): void {
|
|
408
|
+
target.input += usage.input;
|
|
409
|
+
target.output += usage.output;
|
|
410
|
+
target.cacheRead += usage.cacheRead;
|
|
411
|
+
target.cacheWrite += usage.cacheWrite;
|
|
412
|
+
target.cost += usage.cost.total;
|
|
413
|
+
target.contextTokens = usage.totalTokens || target.contextTokens;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function thrownMessage(err: unknown): string {
|
|
417
|
+
return err instanceof Error ? err.message : String(err);
|
|
418
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { buildSystemPrompt } from "./prompt";
|
|
4
|
+
|
|
5
|
+
export default function (pi: ExtensionAPI): void {
|
|
6
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
7
|
+
const {
|
|
8
|
+
cwd,
|
|
9
|
+
contextFiles,
|
|
10
|
+
skills,
|
|
11
|
+
promptGuidelines,
|
|
12
|
+
appendSystemPrompt,
|
|
13
|
+
customPrompt,
|
|
14
|
+
} = event.systemPromptOptions;
|
|
15
|
+
return {
|
|
16
|
+
systemPrompt: buildSystemPrompt({
|
|
17
|
+
model: ctx.model,
|
|
18
|
+
cwd,
|
|
19
|
+
contextFiles: contextFiles ?? [],
|
|
20
|
+
skillsBlock:
|
|
21
|
+
skills && skills.length > 0 ? formatSkillsForPrompt(skills) : "",
|
|
22
|
+
toolGuidelines: promptGuidelines ?? [],
|
|
23
|
+
appendSystemPrompt,
|
|
24
|
+
customPrompt,
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildSystemPrompt, describeOs } from "./prompt";
|
|
3
|
+
|
|
4
|
+
describe("buildSystemPrompt", () => {
|
|
5
|
+
test("emits a best-effort os field instead of process.platform", () => {
|
|
6
|
+
const prompt = buildSystemPrompt({
|
|
7
|
+
cwd: "/repo",
|
|
8
|
+
contextFiles: [],
|
|
9
|
+
skillsBlock: "",
|
|
10
|
+
toolGuidelines: [],
|
|
11
|
+
os: "Ubuntu 24.04.2 LTS",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(prompt).toContain("- os: Ubuntu 24.04.2 LTS");
|
|
15
|
+
expect(prompt).not.toContain("- platform:");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("describeOs", () => {
|
|
20
|
+
test("uses PRETTY_NAME from /etc/os-release on Linux", () => {
|
|
21
|
+
const os = describeOs({
|
|
22
|
+
platform: "linux",
|
|
23
|
+
runCommand: (cmd) =>
|
|
24
|
+
cmd.join(" ") === "cat /etc/os-release"
|
|
25
|
+
? 'NAME="Ubuntu"\nVERSION="24.04.2 LTS"\nPRETTY_NAME="Ubuntu 24.04.2 LTS"\n'
|
|
26
|
+
: undefined,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(os).toBe("Ubuntu 24.04.2 LTS");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("formats macOS from sw_vers", () => {
|
|
33
|
+
const os = describeOs({
|
|
34
|
+
platform: "darwin",
|
|
35
|
+
runCommand: (cmd) =>
|
|
36
|
+
cmd.join(" ") === "sw_vers"
|
|
37
|
+
? "ProductName:\t\tmacOS\nProductVersion:\t15.5\nBuildVersion:\t\t24F74\n"
|
|
38
|
+
: undefined,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(os).toBe("macOS 15.5");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("falls back to process platform when no probe succeeds", () => {
|
|
45
|
+
const os = describeOs({
|
|
46
|
+
platform: "linux",
|
|
47
|
+
runCommand: () => undefined,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(os).toBe("linux");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("falls back to NAME and VERSION when PRETTY_NAME is absent", () => {
|
|
54
|
+
const os = describeOs({
|
|
55
|
+
platform: "linux",
|
|
56
|
+
runCommand: (cmd) =>
|
|
57
|
+
cmd.join(" ") === "cat /etc/os-release"
|
|
58
|
+
? 'NAME=Fedora\nVERSION="40 (Workstation Edition)"'
|
|
59
|
+
: undefined,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(os).toBe("Fedora 40 (Workstation Edition)");
|
|
63
|
+
});
|
|
64
|
+
});
|