@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +6 -8
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +55 -32
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +152 -76
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/executor.ts +204 -67
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +34 -11
- package/src/core/tools/task/worker.ts +152 -27
- package/src/core/tools/web-fetch.ts +220 -1657
- package/src/core/tools/web-scrapers/academic.test.ts +239 -0
- package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
- package/src/core/tools/web-scrapers/arxiv.ts +88 -0
- package/src/core/tools/web-scrapers/aur.ts +175 -0
- package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
- package/src/core/tools/web-scrapers/bluesky.ts +284 -0
- package/src/core/tools/web-scrapers/brew.ts +177 -0
- package/src/core/tools/web-scrapers/business.test.ts +82 -0
- package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
- package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/web-scrapers/coingecko.ts +184 -0
- package/src/core/tools/web-scrapers/crates-io.ts +128 -0
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-scrapers/devto.ts +177 -0
- package/src/core/tools/web-scrapers/discogs.ts +308 -0
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
- package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-scrapers/github-gist.ts +68 -0
- package/src/core/tools/web-scrapers/github.ts +455 -0
- package/src/core/tools/web-scrapers/gitlab.ts +456 -0
- package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
- package/src/core/tools/web-scrapers/hackage.ts +94 -0
- package/src/core/tools/web-scrapers/hackernews.ts +208 -0
- package/src/core/tools/web-scrapers/hex.ts +121 -0
- package/src/core/tools/web-scrapers/huggingface.ts +385 -0
- package/src/core/tools/web-scrapers/iacr.ts +86 -0
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/web-scrapers/lobsters.ts +186 -0
- package/src/core/tools/web-scrapers/mastodon.ts +310 -0
- package/src/core/tools/web-scrapers/maven.ts +152 -0
- package/src/core/tools/web-scrapers/mdn.ts +174 -0
- package/src/core/tools/web-scrapers/media.test.ts +138 -0
- package/src/core/tools/web-scrapers/metacpan.ts +253 -0
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/web-scrapers/npm.ts +114 -0
- package/src/core/tools/web-scrapers/nuget.ts +205 -0
- package/src/core/tools/web-scrapers/nvd.ts +243 -0
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
- package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/web-scrapers/osv.ts +189 -0
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
- package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
- package/src/core/tools/web-scrapers/packagist.ts +174 -0
- package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
- package/src/core/tools/web-scrapers/pubmed.ts +178 -0
- package/src/core/tools/web-scrapers/pypi.ts +129 -0
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
- package/src/core/tools/web-scrapers/reddit.ts +104 -0
- package/src/core/tools/web-scrapers/repology.ts +262 -0
- package/src/core/tools/web-scrapers/research.test.ts +107 -0
- package/src/core/tools/web-scrapers/rfc.ts +209 -0
- package/src/core/tools/web-scrapers/rubygems.ts +117 -0
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
- package/src/core/tools/web-scrapers/security.test.ts +103 -0
- package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
- package/src/core/tools/web-scrapers/social.test.ts +259 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/web-scrapers/spotify.ts +218 -0
- package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
- package/src/core/tools/web-scrapers/standards.test.ts +122 -0
- package/src/core/tools/web-scrapers/terraform.ts +304 -0
- package/src/core/tools/web-scrapers/tldr.ts +51 -0
- package/src/core/tools/web-scrapers/twitter.ts +96 -0
- package/src/core/tools/web-scrapers/types.ts +234 -0
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/web-scrapers/vimeo.ts +152 -0
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/web-scrapers/wikidata.ts +357 -0
- package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
- package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
- package/src/core/tools/web-scrapers/youtube.ts +371 -0
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -38
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/src/utils/tools-manager.ts +110 -8
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
package/src/core/tools/review.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Review tools - report_finding
|
|
2
|
+
* Review tools - report_finding for structured code review.
|
|
3
3
|
*
|
|
4
4
|
* Used by the reviewer agent to report findings in a structured way.
|
|
5
|
-
*
|
|
5
|
+
* Hidden by default - only enabled when explicitly listed in agent's tools.
|
|
6
|
+
* Reviewers finish via `complete` tool with SubmitReviewDetails schema.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
9
10
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
10
|
-
import { Container,
|
|
11
|
+
import { Container, Text } from "@oh-my-pi/pi-tui";
|
|
11
12
|
import { Type } from "@sinclair/typebox";
|
|
12
|
-
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
13
|
-
import { theme } from "../../modes/interactive/theme/theme";
|
|
13
|
+
import type { Theme, ThemeColor } from "../../modes/interactive/theme/theme";
|
|
14
14
|
|
|
15
15
|
const PRIORITY_LABELS: Record<number, string> = {
|
|
16
16
|
0: "P0",
|
|
@@ -19,6 +19,24 @@ const PRIORITY_LABELS: Record<number, string> = {
|
|
|
19
19
|
3: "P3",
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
const PRIORITY_META: Record<number, { symbol: "status.error" | "status.warning" | "status.info"; color: ThemeColor }> =
|
|
23
|
+
{
|
|
24
|
+
0: { symbol: "status.error", color: "error" },
|
|
25
|
+
1: { symbol: "status.warning", color: "warning" },
|
|
26
|
+
2: { symbol: "status.warning", color: "muted" },
|
|
27
|
+
3: { symbol: "status.info", color: "accent" },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function getPriorityDisplay(priority: number, theme: Theme): { label: string; icon: string; color: ThemeColor } {
|
|
31
|
+
const label = PRIORITY_LABELS[priority] ?? "P?";
|
|
32
|
+
const meta = PRIORITY_META[priority] ?? { symbol: "status.info", color: "muted" as const };
|
|
33
|
+
return {
|
|
34
|
+
label,
|
|
35
|
+
icon: theme.styledSymbol(meta.symbol, meta.color),
|
|
36
|
+
color: meta.color,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
// report_finding schema
|
|
23
41
|
const ReportFindingParams = Type.Object({
|
|
24
42
|
title: Type.String({
|
|
@@ -53,7 +71,7 @@ interface ReportFindingDetails {
|
|
|
53
71
|
export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFindingDetails, Theme> = {
|
|
54
72
|
name: "report_finding",
|
|
55
73
|
label: "Report Finding",
|
|
56
|
-
description: "Report a code review finding. Use this for each issue found. Call
|
|
74
|
+
description: "Report a code review finding. Use this for each issue found. Call complete when done.",
|
|
57
75
|
parameters: ReportFindingParams,
|
|
58
76
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
59
77
|
const { title, body, priority, confidence, file_path, line_start, line_end } = params;
|
|
@@ -73,11 +91,10 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
|
|
|
73
91
|
},
|
|
74
92
|
|
|
75
93
|
renderCall(args, theme): Component {
|
|
76
|
-
const
|
|
77
|
-
const color = args.priority === 0 ? "error" : args.priority === 1 ? "warning" : "muted";
|
|
94
|
+
const { label, icon, color } = getPriorityDisplay(args.priority as number, theme);
|
|
78
95
|
const titleText = String(args.title).replace(/^\[P\d\]\s*/, "");
|
|
79
96
|
return new Text(
|
|
80
|
-
`${theme.fg("toolTitle", theme.bold("report_finding "))}${theme.fg(color, `[${
|
|
97
|
+
`${theme.fg("toolTitle", theme.bold("report_finding "))}${icon} ${theme.fg(color, `[${label}]`)} ${theme.fg(
|
|
81
98
|
"dim",
|
|
82
99
|
titleText,
|
|
83
100
|
)}`,
|
|
@@ -93,111 +110,31 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
|
|
|
93
110
|
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
94
111
|
}
|
|
95
112
|
|
|
96
|
-
const
|
|
97
|
-
const color = details.priority === 0 ? "error" : details.priority === 1 ? "warning" : "muted";
|
|
113
|
+
const { label, icon, color } = getPriorityDisplay(details.priority, theme);
|
|
98
114
|
const location = `${details.file_path}:${details.line_start}${
|
|
99
115
|
details.line_end !== details.line_start ? `-${details.line_end}` : ""
|
|
100
116
|
}`;
|
|
101
117
|
|
|
102
118
|
return new Text(
|
|
103
|
-
`${theme.fg("success", theme.status.success)} ${theme.fg(color, `[${
|
|
119
|
+
`${theme.fg("success", theme.status.success)} ${icon} ${theme.fg(color, `[${label}]`)} ${theme.fg(
|
|
120
|
+
"dim",
|
|
121
|
+
location,
|
|
122
|
+
)}`,
|
|
104
123
|
0,
|
|
105
124
|
0,
|
|
106
125
|
);
|
|
107
126
|
},
|
|
108
127
|
};
|
|
109
128
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
overall_correctness: Type.Union([Type.Literal("correct"), Type.Literal("incorrect")], {
|
|
113
|
-
description: "Whether the patch is correct (no bugs, tests won't break)",
|
|
114
|
-
}),
|
|
115
|
-
explanation: Type.String({
|
|
116
|
-
description: "1-3 sentence explanation justifying the verdict",
|
|
117
|
-
}),
|
|
118
|
-
confidence: Type.Number({
|
|
119
|
-
minimum: 0,
|
|
120
|
-
maximum: 1,
|
|
121
|
-
description: "Overall confidence score 0.0-1.0",
|
|
122
|
-
}),
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
interface SubmitReviewDetails {
|
|
129
|
+
/** SubmitReviewDetails - used for rendering review results from complete tool */
|
|
130
|
+
export interface SubmitReviewDetails {
|
|
126
131
|
overall_correctness: "correct" | "incorrect";
|
|
127
132
|
explanation: string;
|
|
128
133
|
confidence: number;
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
export const submitReviewTool: AgentTool<typeof SubmitReviewParams, SubmitReviewDetails, Theme> = {
|
|
132
|
-
name: "submit_review",
|
|
133
|
-
label: "Submit Review",
|
|
134
|
-
description: "Submit the final review verdict. Call this after all findings have been reported.",
|
|
135
|
-
parameters: SubmitReviewParams,
|
|
136
|
-
|
|
137
|
-
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
138
|
-
const { overall_correctness, explanation, confidence } = params;
|
|
139
|
-
|
|
140
|
-
let summary = `## Review Summary\n\n`;
|
|
141
|
-
summary += `**Verdict:** ${
|
|
142
|
-
overall_correctness === "correct"
|
|
143
|
-
? `${theme.status.success} Patch is correct`
|
|
144
|
-
: `${theme.status.error} Patch is incorrect`
|
|
145
|
-
}\n`;
|
|
146
|
-
summary += `**Confidence:** ${(confidence * 100).toFixed(0)}%\n\n`;
|
|
147
|
-
summary += explanation;
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
content: [{ type: "text", text: summary }],
|
|
151
|
-
details: { overall_correctness, explanation, confidence },
|
|
152
|
-
};
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
renderCall(args, theme): Component {
|
|
156
|
-
const verdict = args.overall_correctness === "correct" ? "correct" : "incorrect";
|
|
157
|
-
const color = args.overall_correctness === "correct" ? "success" : "error";
|
|
158
|
-
return new Text(
|
|
159
|
-
`${theme.fg("toolTitle", theme.bold("submit_review "))}${theme.fg(color, verdict)} ${theme.fg(
|
|
160
|
-
"dim",
|
|
161
|
-
`(${((args.confidence as number) * 100).toFixed(0)}%)`,
|
|
162
|
-
)}`,
|
|
163
|
-
0,
|
|
164
|
-
0,
|
|
165
|
-
);
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
renderResult(result, { expanded }, theme): Component {
|
|
169
|
-
const { details } = result;
|
|
170
|
-
if (!details) {
|
|
171
|
-
const text = result.content[0];
|
|
172
|
-
return new Text(text?.type === "text" ? text.text : "", 0, 0);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const container = new Container();
|
|
176
|
-
const verdictColor = details.overall_correctness === "correct" ? "success" : "error";
|
|
177
|
-
const verdictIcon = details.overall_correctness === "correct" ? theme.status.success : theme.status.error;
|
|
178
|
-
|
|
179
|
-
container.addChild(
|
|
180
|
-
new Text(
|
|
181
|
-
`${theme.fg(verdictColor, verdictIcon)} Patch is ${theme.fg(
|
|
182
|
-
verdictColor,
|
|
183
|
-
details.overall_correctness,
|
|
184
|
-
)} ${theme.fg("dim", `(${(details.confidence * 100).toFixed(0)}% confidence)`)}`,
|
|
185
|
-
0,
|
|
186
|
-
0,
|
|
187
|
-
),
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
if (expanded) {
|
|
191
|
-
container.addChild(new Spacer(1));
|
|
192
|
-
container.addChild(new Text(theme.fg("dim", details.explanation), 0, 0));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return container;
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
|
|
199
136
|
// Re-export types for external use
|
|
200
|
-
export type { ReportFindingDetails
|
|
137
|
+
export type { ReportFindingDetails };
|
|
201
138
|
|
|
202
139
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
203
140
|
// Subprocess tool handlers - registered for extraction/rendering in task tool
|
|
@@ -211,11 +148,10 @@ subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
|
|
|
211
148
|
extractData: (event) => event.result?.details as ReportFindingDetails | undefined,
|
|
212
149
|
|
|
213
150
|
renderInline: (data, theme) => {
|
|
214
|
-
const
|
|
215
|
-
const color = data.priority === 0 ? "error" : data.priority === 1 ? "warning" : "muted";
|
|
151
|
+
const { label, icon, color } = getPriorityDisplay(data.priority, theme);
|
|
216
152
|
const titleText = data.title.replace(/^\[P\d\]\s*/, "");
|
|
217
153
|
const loc = `${path.basename(data.file_path)}:${data.line_start}`;
|
|
218
|
-
return new Text(`${theme.fg(color, `[${
|
|
154
|
+
return new Text(`${icon} ${theme.fg(color, `[${label}]`)} ${titleText} ${theme.fg("dim", loc)}`, 0, 0);
|
|
219
155
|
},
|
|
220
156
|
|
|
221
157
|
renderFinal: (allData, theme, expanded) => {
|
|
@@ -224,13 +160,12 @@ subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
|
|
|
224
160
|
|
|
225
161
|
for (let i = 0; i < displayCount; i++) {
|
|
226
162
|
const data = allData[i];
|
|
227
|
-
const
|
|
228
|
-
const color = data.priority === 0 ? "error" : data.priority === 1 ? "warning" : "muted";
|
|
163
|
+
const { label, icon, color } = getPriorityDisplay(data.priority, theme);
|
|
229
164
|
const titleText = data.title.replace(/^\[P\d\]\s*/, "");
|
|
230
165
|
const loc = `${path.basename(data.file_path)}:${data.line_start}`;
|
|
231
166
|
|
|
232
167
|
container.addChild(
|
|
233
|
-
new Text(` ${theme.fg(color, `[${
|
|
168
|
+
new Text(` ${icon} ${theme.fg(color, `[${label}]`)} ${titleText} ${theme.fg("dim", loc)}`, 0, 0),
|
|
234
169
|
);
|
|
235
170
|
|
|
236
171
|
if (expanded && data.body) {
|
|
@@ -251,26 +186,3 @@ subprocessToolRegistry.register<ReportFindingDetails>("report_finding", {
|
|
|
251
186
|
return container;
|
|
252
187
|
},
|
|
253
188
|
});
|
|
254
|
-
|
|
255
|
-
// Register submit_review handler
|
|
256
|
-
subprocessToolRegistry.register<SubmitReviewDetails>("submit_review", {
|
|
257
|
-
extractData: (event) => event.result?.details as SubmitReviewDetails | undefined,
|
|
258
|
-
|
|
259
|
-
// Terminate subprocess after review is submitted
|
|
260
|
-
shouldTerminate: () => true,
|
|
261
|
-
|
|
262
|
-
renderInline: (data, theme) => {
|
|
263
|
-
const verdictColor = data.overall_correctness === "correct" ? "success" : "error";
|
|
264
|
-
const verdictIcon = data.overall_correctness === "correct" ? theme.status.success : theme.status.error;
|
|
265
|
-
return new Text(
|
|
266
|
-
`${theme.fg(verdictColor, verdictIcon)} Review: ${theme.fg(verdictColor, data.overall_correctness)} (${(
|
|
267
|
-
data.confidence * 100
|
|
268
|
-
).toFixed(0)}%)`,
|
|
269
|
-
0,
|
|
270
|
-
0,
|
|
271
|
-
);
|
|
272
|
-
},
|
|
273
|
-
|
|
274
|
-
// Note: renderFinal is NOT used for submit_review - we use the combined
|
|
275
|
-
// renderReviewResult in render.ts to show verdict + findings together
|
|
276
|
-
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
+
import { nanoid } from "nanoid";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Derive artifacts directory from session file path.
|
|
@@ -62,14 +63,14 @@ export async function writeArtifacts(
|
|
|
62
63
|
const paths = getArtifactPaths(dir, taskId);
|
|
63
64
|
|
|
64
65
|
// Write input
|
|
65
|
-
await
|
|
66
|
+
await Bun.write(paths.inputPath, input);
|
|
66
67
|
|
|
67
68
|
// Write output
|
|
68
|
-
await
|
|
69
|
+
await Bun.write(paths.outputPath, output);
|
|
69
70
|
|
|
70
71
|
// Write JSONL if events provided
|
|
71
72
|
if (jsonlEvents && jsonlEvents.length > 0) {
|
|
72
|
-
await
|
|
73
|
+
await Bun.write(paths.jsonlPath, jsonlEvents.join("\n"));
|
|
73
74
|
return paths;
|
|
74
75
|
}
|
|
75
76
|
|
|
@@ -80,7 +81,7 @@ export async function writeArtifacts(
|
|
|
80
81
|
* Create a temporary artifacts directory.
|
|
81
82
|
*/
|
|
82
83
|
export function createTempArtifactsDir(runId?: string): string {
|
|
83
|
-
const id = runId ||
|
|
84
|
+
const id = runId || nanoid();
|
|
84
85
|
const dir = path.join(os.tmpdir(), `omp-task-${id}`);
|
|
85
86
|
ensureArtifactsDir(dir);
|
|
86
87
|
return dir;
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Runs each subagent in a Bun Worker and forwards AgentEvents for progress tracking.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { writeFileSync } from "node:fs";
|
|
8
7
|
import type { AgentEvent } from "@oh-my-pi/pi-agent-core";
|
|
9
8
|
import type { EventBus } from "../../event-bus";
|
|
10
9
|
import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
|
|
@@ -50,20 +49,26 @@ function truncateOutput(output: string): { text: string; truncated: boolean } {
|
|
|
50
49
|
|
|
51
50
|
let i = 0;
|
|
52
51
|
let lastNewlineIndex = -1;
|
|
53
|
-
while (i < output.length
|
|
54
|
-
const
|
|
55
|
-
|
|
52
|
+
while (i < output.length) {
|
|
53
|
+
const codePoint = output.codePointAt(i);
|
|
54
|
+
if (codePoint === undefined) break;
|
|
55
|
+
const codeUnitLength = codePoint > 0xffff ? 2 : 1;
|
|
56
|
+
const byteLen = codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
|
|
57
|
+
if (byteBudget - byteLen < 0) {
|
|
58
|
+
truncated = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
byteBudget -= byteLen;
|
|
62
|
+
i += codeUnitLength;
|
|
56
63
|
|
|
57
|
-
if (
|
|
64
|
+
if (codePoint === 0x0a) {
|
|
58
65
|
lineBudget--;
|
|
59
|
-
lastNewlineIndex = i;
|
|
66
|
+
lastNewlineIndex = i - 1;
|
|
60
67
|
if (lineBudget <= 0) {
|
|
61
68
|
truncated = true;
|
|
62
69
|
break;
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
|
-
|
|
66
|
-
i++;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
if (i < output.length) {
|
|
@@ -186,7 +191,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
186
191
|
|
|
187
192
|
// Write input file immediately (real-time visibility)
|
|
188
193
|
try {
|
|
189
|
-
|
|
194
|
+
await Bun.write(artifactPaths.inputPath, fullTask);
|
|
190
195
|
} catch {
|
|
191
196
|
// Non-fatal, continue without input artifact
|
|
192
197
|
}
|
|
@@ -207,13 +212,40 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
207
212
|
const sessionFile = subtaskSessionFile ?? options.sessionFile ?? null;
|
|
208
213
|
const spawnsEnv = agent.spawns === undefined ? "" : agent.spawns === "*" ? "*" : agent.spawns.join(",");
|
|
209
214
|
|
|
210
|
-
|
|
215
|
+
let worker: Worker;
|
|
216
|
+
try {
|
|
217
|
+
worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return {
|
|
220
|
+
index,
|
|
221
|
+
taskId,
|
|
222
|
+
agent: agent.name,
|
|
223
|
+
agentSource: agent.source,
|
|
224
|
+
task,
|
|
225
|
+
description: options.description,
|
|
226
|
+
exitCode: 1,
|
|
227
|
+
output: "",
|
|
228
|
+
stderr: `Failed to create worker: ${err instanceof Error ? err.message : String(err)}`,
|
|
229
|
+
truncated: false,
|
|
230
|
+
durationMs: Date.now() - startTime,
|
|
231
|
+
tokens: 0,
|
|
232
|
+
modelOverride,
|
|
233
|
+
error: `Failed to create worker: ${err instanceof Error ? err.message : String(err)}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
211
236
|
|
|
212
|
-
|
|
237
|
+
const outputChunks: string[] = [];
|
|
238
|
+
const finalOutputChunks: string[] = [];
|
|
213
239
|
let stderr = "";
|
|
214
|
-
let finalOutput = "";
|
|
215
240
|
let resolved = false;
|
|
216
|
-
|
|
241
|
+
type AbortReason = "signal" | "terminate";
|
|
242
|
+
let abortSent = false;
|
|
243
|
+
let abortReason: AbortReason | undefined;
|
|
244
|
+
let terminationScheduled = false;
|
|
245
|
+
let pendingTerminationController: AbortController | null = null;
|
|
246
|
+
let finalize: ((message: Extract<SubagentWorkerResponse, { type: "done" }>) => void) | null = null;
|
|
247
|
+
const listenerController = new AbortController();
|
|
248
|
+
const listenerSignal = listenerController.signal;
|
|
217
249
|
|
|
218
250
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
219
251
|
const accumulatedUsage = {
|
|
@@ -226,25 +258,80 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
226
258
|
};
|
|
227
259
|
let hasUsage = false;
|
|
228
260
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
261
|
+
const scheduleTermination = () => {
|
|
262
|
+
if (terminationScheduled) return;
|
|
263
|
+
terminationScheduled = true;
|
|
264
|
+
const timeoutSignal = AbortSignal.timeout(2000);
|
|
265
|
+
timeoutSignal.addEventListener(
|
|
266
|
+
"abort",
|
|
267
|
+
() => {
|
|
268
|
+
if (resolved) return;
|
|
269
|
+
try {
|
|
270
|
+
worker.terminate();
|
|
271
|
+
} catch {
|
|
272
|
+
// Ignore termination errors
|
|
273
|
+
}
|
|
274
|
+
if (finalize && !resolved) {
|
|
275
|
+
finalize({
|
|
276
|
+
type: "done",
|
|
277
|
+
exitCode: 1,
|
|
278
|
+
durationMs: Date.now() - startTime,
|
|
279
|
+
error: abortReason === "signal" ? "Aborted" : "Worker terminated after tool completion",
|
|
280
|
+
aborted: abortReason === "signal",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
{ once: true, signal: listenerSignal },
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const requestAbort = (reason: AbortReason) => {
|
|
289
|
+
if (abortSent) {
|
|
290
|
+
if (reason === "signal" && abortReason !== "signal") {
|
|
291
|
+
abortReason = "signal";
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (resolved) return;
|
|
232
296
|
abortSent = true;
|
|
297
|
+
abortReason = reason;
|
|
233
298
|
const abortMessage: SubagentWorkerRequest = { type: "abort" };
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
299
|
+
try {
|
|
300
|
+
worker.postMessage(abortMessage);
|
|
301
|
+
} catch {
|
|
302
|
+
// Worker already terminated, nothing to do
|
|
303
|
+
}
|
|
304
|
+
// Cancel pending termination if it exists
|
|
305
|
+
if (pendingTerminationController) {
|
|
306
|
+
pendingTerminationController.abort();
|
|
307
|
+
pendingTerminationController = null;
|
|
308
|
+
}
|
|
309
|
+
scheduleTermination();
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const schedulePendingTermination = () => {
|
|
313
|
+
if (pendingTerminationController || abortSent || terminationScheduled || resolved) return;
|
|
314
|
+
const readyController = new AbortController();
|
|
315
|
+
pendingTerminationController = readyController;
|
|
316
|
+
const pendingSignal = AbortSignal.any([AbortSignal.timeout(2000), readyController.signal]);
|
|
317
|
+
pendingSignal.addEventListener(
|
|
318
|
+
"abort",
|
|
319
|
+
() => {
|
|
320
|
+
pendingTerminationController = null;
|
|
321
|
+
if (!resolved) {
|
|
322
|
+
requestAbort("terminate");
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{ once: true, signal: listenerSignal },
|
|
326
|
+
);
|
|
240
327
|
};
|
|
241
328
|
|
|
242
329
|
// Handle abort signal
|
|
243
330
|
const onAbort = () => {
|
|
244
|
-
if (!resolved) requestAbort();
|
|
331
|
+
if (!resolved) requestAbort("signal");
|
|
245
332
|
};
|
|
246
333
|
if (signal) {
|
|
247
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
334
|
+
signal.addEventListener("abort", onAbort, { once: true, signal: listenerSignal });
|
|
248
335
|
}
|
|
249
336
|
|
|
250
337
|
const emitProgress = () => {
|
|
@@ -347,13 +434,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
347
434
|
})
|
|
348
435
|
) {
|
|
349
436
|
// Don't terminate immediately - wait for message_end to get token counts
|
|
350
|
-
|
|
351
|
-
// Safety timeout in case message_end never arrives
|
|
352
|
-
setTimeout(() => {
|
|
353
|
-
if (!resolved) {
|
|
354
|
-
requestAbort();
|
|
355
|
-
}
|
|
356
|
-
}, 2000);
|
|
437
|
+
schedulePendingTermination();
|
|
357
438
|
}
|
|
358
439
|
}
|
|
359
440
|
break;
|
|
@@ -386,7 +467,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
386
467
|
if (messageContent && Array.isArray(messageContent)) {
|
|
387
468
|
for (const block of messageContent) {
|
|
388
469
|
if (block.type === "text" && block.text) {
|
|
389
|
-
|
|
470
|
+
outputChunks.push(block.text);
|
|
390
471
|
}
|
|
391
472
|
}
|
|
392
473
|
}
|
|
@@ -395,33 +476,29 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
395
476
|
const messageUsage = getMessageUsage(event.message) || (event as AgentEvent & { usage?: unknown }).usage;
|
|
396
477
|
if (messageUsage && typeof messageUsage === "object") {
|
|
397
478
|
// Only count assistant messages (not tool results, etc.)
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
event.message?.stopReason !== "error"
|
|
402
|
-
) {
|
|
403
|
-
const usageRecord = messageUsage as Record<string, number | undefined>;
|
|
404
|
-
const costRecord = (messageUsage as { cost?: Record<string, number | undefined> }).cost;
|
|
479
|
+
if (role === "assistant") {
|
|
480
|
+
const usageRecord = messageUsage as Record<string, unknown>;
|
|
481
|
+
const costRecord = (messageUsage as { cost?: Record<string, unknown> }).cost;
|
|
405
482
|
hasUsage = true;
|
|
406
|
-
accumulatedUsage.input += usageRecord
|
|
407
|
-
accumulatedUsage.output += usageRecord
|
|
408
|
-
accumulatedUsage.cacheRead += usageRecord
|
|
409
|
-
accumulatedUsage.cacheWrite += usageRecord
|
|
410
|
-
accumulatedUsage.totalTokens += usageRecord
|
|
483
|
+
accumulatedUsage.input += getNumberField(usageRecord, "input") ?? 0;
|
|
484
|
+
accumulatedUsage.output += getNumberField(usageRecord, "output") ?? 0;
|
|
485
|
+
accumulatedUsage.cacheRead += getNumberField(usageRecord, "cacheRead") ?? 0;
|
|
486
|
+
accumulatedUsage.cacheWrite += getNumberField(usageRecord, "cacheWrite") ?? 0;
|
|
487
|
+
accumulatedUsage.totalTokens += getNumberField(usageRecord, "totalTokens") ?? 0;
|
|
411
488
|
if (costRecord) {
|
|
412
|
-
accumulatedUsage.cost.input += costRecord
|
|
413
|
-
accumulatedUsage.cost.output += costRecord
|
|
414
|
-
accumulatedUsage.cost.cacheRead += costRecord
|
|
415
|
-
accumulatedUsage.cost.cacheWrite += costRecord
|
|
416
|
-
accumulatedUsage.cost.total += costRecord
|
|
489
|
+
accumulatedUsage.cost.input += getNumberField(costRecord, "input") ?? 0;
|
|
490
|
+
accumulatedUsage.cost.output += getNumberField(costRecord, "output") ?? 0;
|
|
491
|
+
accumulatedUsage.cost.cacheRead += getNumberField(costRecord, "cacheRead") ?? 0;
|
|
492
|
+
accumulatedUsage.cost.cacheWrite += getNumberField(costRecord, "cacheWrite") ?? 0;
|
|
493
|
+
accumulatedUsage.cost.total += getNumberField(costRecord, "total") ?? 0;
|
|
417
494
|
}
|
|
418
495
|
}
|
|
419
496
|
// Accumulate tokens for progress display
|
|
420
497
|
progress.tokens += getUsageTokens(messageUsage);
|
|
421
498
|
}
|
|
422
499
|
// If pending termination, now we have tokens - terminate
|
|
423
|
-
if (
|
|
424
|
-
|
|
500
|
+
if (pendingTerminationController) {
|
|
501
|
+
pendingTerminationController.abort();
|
|
425
502
|
}
|
|
426
503
|
break;
|
|
427
504
|
}
|
|
@@ -435,7 +512,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
435
512
|
if (messageContent && Array.isArray(messageContent)) {
|
|
436
513
|
for (const block of messageContent) {
|
|
437
514
|
if (block.type === "text" && block.text) {
|
|
438
|
-
|
|
515
|
+
finalOutputChunks.push(block.text);
|
|
439
516
|
}
|
|
440
517
|
}
|
|
441
518
|
}
|
|
@@ -469,38 +546,90 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
469
546
|
}
|
|
470
547
|
|
|
471
548
|
const done = await new Promise<Extract<SubagentWorkerResponse, { type: "done" }>>((resolve) => {
|
|
549
|
+
const cleanup = () => {
|
|
550
|
+
pendingTerminationController = null;
|
|
551
|
+
listenerController.abort();
|
|
552
|
+
};
|
|
553
|
+
finalize = (message) => {
|
|
554
|
+
if (resolved) return;
|
|
555
|
+
resolved = true;
|
|
556
|
+
cleanup();
|
|
557
|
+
resolve(message);
|
|
558
|
+
};
|
|
472
559
|
const onMessage = (event: WorkerMessageEvent<SubagentWorkerResponse>) => {
|
|
473
560
|
const message = event.data;
|
|
474
561
|
if (!message || resolved) return;
|
|
475
562
|
if (message.type === "event") {
|
|
476
|
-
|
|
563
|
+
try {
|
|
564
|
+
processEvent(message.event);
|
|
565
|
+
} catch (err) {
|
|
566
|
+
finalize?.({
|
|
567
|
+
type: "done",
|
|
568
|
+
exitCode: 1,
|
|
569
|
+
durationMs: Date.now() - startTime,
|
|
570
|
+
error: `Failed to process worker event: ${err instanceof Error ? err.message : String(err)}`,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
477
573
|
return;
|
|
478
574
|
}
|
|
479
575
|
if (message.type === "done") {
|
|
480
|
-
|
|
481
|
-
resolve(message);
|
|
576
|
+
finalize?.(message);
|
|
482
577
|
}
|
|
483
578
|
};
|
|
484
579
|
const onError = (event: WorkerErrorEvent) => {
|
|
485
|
-
|
|
486
|
-
resolved = true;
|
|
487
|
-
resolve({
|
|
580
|
+
finalize?.({
|
|
488
581
|
type: "done",
|
|
489
582
|
exitCode: 1,
|
|
490
583
|
durationMs: Date.now() - startTime,
|
|
491
584
|
error: event.message,
|
|
492
585
|
});
|
|
493
586
|
};
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
587
|
+
const onMessageError = () => {
|
|
588
|
+
finalize?.({
|
|
589
|
+
type: "done",
|
|
590
|
+
exitCode: 1,
|
|
591
|
+
durationMs: Date.now() - startTime,
|
|
592
|
+
error: "Worker message deserialization failed",
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
const onClose = () => {
|
|
596
|
+
// Worker terminated unexpectedly (crashed or was killed without sending done)
|
|
597
|
+
const abortMessage =
|
|
598
|
+
abortSent && abortReason === "signal"
|
|
599
|
+
? "Worker terminated after abort"
|
|
600
|
+
: abortSent
|
|
601
|
+
? "Worker terminated after tool completion"
|
|
602
|
+
: "Worker terminated unexpectedly";
|
|
603
|
+
finalize?.({
|
|
604
|
+
type: "done",
|
|
605
|
+
exitCode: 1,
|
|
606
|
+
durationMs: Date.now() - startTime,
|
|
607
|
+
error: abortMessage,
|
|
608
|
+
aborted: abortReason === "signal",
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
worker.addEventListener("message", onMessage, { signal: listenerSignal });
|
|
612
|
+
worker.addEventListener("error", onError, { signal: listenerSignal });
|
|
613
|
+
worker.addEventListener("close", onClose, { signal: listenerSignal });
|
|
614
|
+
worker.addEventListener("messageerror", onMessageError, { signal: listenerSignal });
|
|
615
|
+
try {
|
|
616
|
+
worker.postMessage(startMessage);
|
|
617
|
+
} catch (err) {
|
|
618
|
+
finalize({
|
|
619
|
+
type: "done",
|
|
620
|
+
exitCode: 1,
|
|
621
|
+
durationMs: Date.now() - startTime,
|
|
622
|
+
error: `Failed to start worker: ${err instanceof Error ? err.message : String(err)}`,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
497
625
|
});
|
|
498
626
|
|
|
499
627
|
// Cleanup
|
|
500
|
-
|
|
501
|
-
|
|
628
|
+
try {
|
|
629
|
+
worker.terminate();
|
|
630
|
+
} catch {
|
|
631
|
+
// Ignore termination errors
|
|
502
632
|
}
|
|
503
|
-
worker.terminate();
|
|
504
633
|
|
|
505
634
|
let exitCode = done.exitCode;
|
|
506
635
|
if (done.error) {
|
|
@@ -508,7 +637,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
508
637
|
}
|
|
509
638
|
|
|
510
639
|
// Use final output if available, otherwise accumulated output
|
|
511
|
-
let rawOutput =
|
|
640
|
+
let rawOutput = finalOutputChunks.length > 0 ? finalOutputChunks.join("") : outputChunks.join("");
|
|
512
641
|
let abortedViaComplete = false;
|
|
513
642
|
const completeItems = progress.extractedToolData?.complete as
|
|
514
643
|
| Array<{ data?: unknown; status?: "success" | "aborted"; error?: string }>
|
|
@@ -528,7 +657,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
528
657
|
}
|
|
529
658
|
} else {
|
|
530
659
|
// Normal successful completion
|
|
531
|
-
|
|
660
|
+
let completeData = lastComplete?.data ?? null;
|
|
661
|
+
// Handle double-stringified JSON (subagent returned JSON string instead of object)
|
|
662
|
+
if (typeof completeData === "string" && (completeData.startsWith("{") || completeData.startsWith("["))) {
|
|
663
|
+
try {
|
|
664
|
+
completeData = JSON.parse(completeData);
|
|
665
|
+
} catch {
|
|
666
|
+
// Not valid JSON, keep as string
|
|
667
|
+
}
|
|
668
|
+
}
|
|
532
669
|
try {
|
|
533
670
|
rawOutput = JSON.stringify(completeData, null, 2) ?? "null";
|
|
534
671
|
} catch (err) {
|
|
@@ -549,7 +686,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
549
686
|
let outputMeta: { lineCount: number; charCount: number } | undefined;
|
|
550
687
|
if (artifactPaths) {
|
|
551
688
|
try {
|
|
552
|
-
|
|
689
|
+
await Bun.write(artifactPaths.outputPath, rawOutput);
|
|
553
690
|
outputMeta = {
|
|
554
691
|
lineCount: rawOutput.split("\n").length,
|
|
555
692
|
charCount: rawOutput.length,
|