@oh-my-pi/pi-coding-agent 6.8.2 → 6.8.4
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 +30 -0
- package/package.json +6 -6
- package/src/core/agent-session.ts +7 -18
- package/src/core/bash-executor.ts +7 -64
- package/src/core/custom-commands/loader.ts +0 -8
- package/src/core/extensions/types.ts +0 -3
- package/src/core/index.ts +1 -1
- package/src/core/keybindings.ts +10 -2
- package/src/core/python-executor.ts +15 -25
- package/src/core/ssh/ssh-executor.ts +8 -13
- package/src/core/streaming-output.ts +64 -150
- package/src/core/tools/bash.ts +10 -44
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/output.ts +7 -8
- package/src/core/tools/python.ts +6 -15
- package/src/core/tools/ssh.ts +6 -14
- package/src/core/tools/task/executor.ts +7 -16
- package/src/core/tools/task/index.ts +11 -12
- package/src/core/tools/task/types.ts +2 -1
- package/src/core/tools/todo-write.ts +2 -18
- package/src/core/tools/truncate.ts +92 -0
- package/src/core/tools/web-fetch.ts +6 -4
- package/src/core/tools/web-scrapers/utils.ts +7 -30
- package/src/core/tools/web-search/providers/anthropic.ts +0 -1
- package/src/index.ts +0 -1
- package/src/modes/interactive/components/todo-display.ts +1 -8
- package/src/modes/interactive/components/tree-selector.ts +2 -2
- package/src/modes/interactive/interactive-mode.ts +1 -15
- package/src/utils/clipboard.ts +27 -20
- package/src/core/tools/task/artifacts.ts +0 -112
|
@@ -2,7 +2,7 @@ import { tmpdir } from "node:os";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { nanoid } from "nanoid";
|
|
5
|
-
import { DEFAULT_MAX_BYTES
|
|
5
|
+
import { DEFAULT_MAX_BYTES } from "./tools/truncate";
|
|
6
6
|
|
|
7
7
|
export interface OutputResult {
|
|
8
8
|
output: string;
|
|
@@ -14,7 +14,6 @@ export interface OutputSinkOptions {
|
|
|
14
14
|
allocateFilePath?: () => string;
|
|
15
15
|
spillThreshold?: number;
|
|
16
16
|
maxColumn?: number;
|
|
17
|
-
onLine?: (line: string) => void;
|
|
18
17
|
onChunk?: (chunk: string) => void;
|
|
19
18
|
}
|
|
20
19
|
|
|
@@ -29,182 +28,97 @@ function defaultFilePathAllocator(): string {
|
|
|
29
28
|
* When memory limit exceeded, spills ~half to file in one batch operation.
|
|
30
29
|
*/
|
|
31
30
|
export class OutputSink {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
#buffer = "";
|
|
32
|
+
#file?: {
|
|
33
|
+
path: string;
|
|
34
|
+
sink: Bun.FileSink;
|
|
35
|
+
};
|
|
36
|
+
#bytesWritten: number = 0;
|
|
37
|
+
#pending: Promise<void> = Promise.resolve();
|
|
38
|
+
|
|
39
|
+
readonly #allocateFilePath: () => string;
|
|
40
|
+
readonly #spillThreshold: number;
|
|
41
|
+
readonly #onChunk?: (chunk: string) => void;
|
|
43
42
|
|
|
44
43
|
constructor(options?: OutputSinkOptions) {
|
|
45
44
|
const {
|
|
46
45
|
allocateFilePath = defaultFilePathAllocator,
|
|
47
46
|
spillThreshold = DEFAULT_MAX_BYTES,
|
|
48
|
-
maxColumn = DEFAULT_MAX_COLUMN,
|
|
49
|
-
onLine,
|
|
50
47
|
onChunk,
|
|
51
48
|
} = options ?? {};
|
|
52
49
|
|
|
53
|
-
this
|
|
54
|
-
this
|
|
55
|
-
this
|
|
56
|
-
this.onLine = onLine;
|
|
57
|
-
this.onChunk = onChunk;
|
|
50
|
+
this.#allocateFilePath = allocateFilePath;
|
|
51
|
+
this.#spillThreshold = spillThreshold;
|
|
52
|
+
this.#onChunk = onChunk;
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
55
|
+
async #pushSanitized(data: string): Promise<void> {
|
|
56
|
+
this.#onChunk?.(data);
|
|
57
|
+
const dataBytes = Buffer.byteLength(data);
|
|
58
|
+
const overflow = dataBytes + this.#bytesWritten > this.#spillThreshold || this.#file != null;
|
|
65
59
|
|
|
66
|
-
this
|
|
67
|
-
if (term) {
|
|
68
|
-
this.buffer += term;
|
|
69
|
-
}
|
|
60
|
+
const sink = overflow ? await this.#fileSink() : null;
|
|
70
61
|
|
|
71
|
-
this
|
|
72
|
-
|
|
62
|
+
this.#buffer += data;
|
|
63
|
+
await sink?.write(data);
|
|
73
64
|
|
|
74
|
-
if (this
|
|
75
|
-
this.
|
|
65
|
+
if (this.#buffer.length > this.#spillThreshold) {
|
|
66
|
+
this.#buffer = this.#buffer.slice(-this.#spillThreshold);
|
|
76
67
|
}
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
|
|
80
|
-
this
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.filePath = filePath;
|
|
88
|
-
this.fileSink = Bun.file(filePath).writer();
|
|
70
|
+
async #fileSink(): Promise<Bun.FileSink> {
|
|
71
|
+
if (!this.#file) {
|
|
72
|
+
const filePath = this.#allocateFilePath();
|
|
73
|
+
this.#file = {
|
|
74
|
+
path: filePath,
|
|
75
|
+
sink: Bun.file(filePath).writer(),
|
|
76
|
+
};
|
|
77
|
+
await this.#file.sink.write(this.#buffer);
|
|
89
78
|
}
|
|
90
|
-
return this.
|
|
79
|
+
return this.#file.sink;
|
|
91
80
|
}
|
|
92
81
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
let hi = this.lineEnds.length;
|
|
99
|
-
while (lo < hi) {
|
|
100
|
-
const mid = (lo + hi) >>> 1;
|
|
101
|
-
if (this.lineEnds[mid] < target) {
|
|
102
|
-
lo = mid + 1;
|
|
103
|
-
} else {
|
|
104
|
-
hi = mid;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Clamp: evict at least 1 line, keep at least 1 line
|
|
109
|
-
const splitIdx = Math.max(1, Math.min(lo, this.lineEnds.length - 1));
|
|
110
|
-
const splitPos = this.lineEnds[splitIdx - 1];
|
|
111
|
-
|
|
112
|
-
// Write evicted portion to file
|
|
113
|
-
this.getFileSink().write(this.buffer.slice(0, splitPos));
|
|
114
|
-
|
|
115
|
-
// Truncate buffer, shift line positions
|
|
116
|
-
this.buffer = this.buffer.slice(splitPos);
|
|
117
|
-
const remaining = this.lineEnds.length - splitIdx;
|
|
118
|
-
for (let i = 0; i < remaining; i++) {
|
|
119
|
-
this.lineEnds[i] = this.lineEnds[i + splitIdx] - splitPos;
|
|
120
|
-
}
|
|
121
|
-
this.lineEnds.length = remaining;
|
|
82
|
+
async push(chunk: string): Promise<void> {
|
|
83
|
+
chunk = sanitizeText(chunk);
|
|
84
|
+
const op = this.#pending.then(() => this.#pushSanitized(chunk));
|
|
85
|
+
this.#pending = op.catch(() => {});
|
|
86
|
+
await op;
|
|
122
87
|
}
|
|
123
88
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
let
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
buf = buf.trimEnd();
|
|
143
|
-
if (buf) {
|
|
144
|
-
this.pushChunk(`${buf}\n`);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
return new WritableStream<Uint8Array>({
|
|
149
|
-
write: (chunk) => {
|
|
150
|
-
buf += sanitizeText(decoder.decode(chunk, { stream: true }));
|
|
151
|
-
flushLines();
|
|
89
|
+
createInput(): WritableStream<Uint8Array | string> {
|
|
90
|
+
let decoder: TextDecoder | undefined;
|
|
91
|
+
let finalize = async () => {};
|
|
92
|
+
|
|
93
|
+
return new WritableStream<Uint8Array | string>({
|
|
94
|
+
write: async (chunk) => {
|
|
95
|
+
if (typeof chunk === "string") {
|
|
96
|
+
await this.push(chunk);
|
|
97
|
+
} else {
|
|
98
|
+
if (!decoder) {
|
|
99
|
+
const dec = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
100
|
+
decoder = dec;
|
|
101
|
+
finalize = async () => {
|
|
102
|
+
await this.push(dec.decode());
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
await this.push(decoder.decode(chunk, { stream: true }));
|
|
106
|
+
}
|
|
152
107
|
},
|
|
153
108
|
close: finalize,
|
|
154
109
|
abort: finalize,
|
|
155
110
|
});
|
|
156
111
|
}
|
|
157
112
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const flushLines = () => {
|
|
162
|
-
let start = 0;
|
|
163
|
-
while (true) {
|
|
164
|
-
const nl = buf.indexOf("\n", start);
|
|
165
|
-
if (nl === -1) break;
|
|
166
|
-
this.pushChunk(buf.slice(start, nl + 1));
|
|
167
|
-
start = nl + 1;
|
|
168
|
-
}
|
|
169
|
-
buf = buf.slice(start);
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const finalize = () => {
|
|
173
|
-
flushLines();
|
|
174
|
-
buf = buf.trimEnd();
|
|
175
|
-
if (buf) {
|
|
176
|
-
this.pushChunk(`${buf}\n`);
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
return new WritableStream<string>({
|
|
181
|
-
write: (chunk) => {
|
|
182
|
-
buf += sanitizeText(chunk);
|
|
183
|
-
flushLines();
|
|
184
|
-
},
|
|
185
|
-
close: finalize,
|
|
186
|
-
abort: finalize,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async close(): Promise<void> {
|
|
191
|
-
await this.fileSink?.end();
|
|
192
|
-
}
|
|
113
|
+
async dump(notice?: string): Promise<OutputResult> {
|
|
114
|
+
await this.#pending;
|
|
115
|
+
const noticeLine = notice ? `[${notice}]\n` : "";
|
|
193
116
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (!this.filePath) {
|
|
200
|
-
return { output, truncated: false };
|
|
117
|
+
if (this.#file) {
|
|
118
|
+
await this.#file.sink.end();
|
|
119
|
+
return { output: `${noticeLine}...${this.#buffer}`, truncated: true, fullOutputPath: this.#file.path };
|
|
120
|
+
} else {
|
|
121
|
+
return { output: `${noticeLine}${this.#buffer}`, truncated: false };
|
|
201
122
|
}
|
|
202
|
-
this.fileSink!.write(this.buffer);
|
|
203
|
-
this.fileSink!.flush();
|
|
204
|
-
return {
|
|
205
|
-
output,
|
|
206
|
-
truncated: true,
|
|
207
|
-
fullOutputPath: this.filePath,
|
|
208
|
-
};
|
|
209
123
|
}
|
|
210
124
|
}
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -6,14 +6,14 @@ import { Type } from "@sinclair/typebox";
|
|
|
6
6
|
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate";
|
|
7
7
|
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
8
8
|
import bashDescription from "../../prompts/tools/bash.md" with { type: "text" };
|
|
9
|
-
import { type BashExecutorOptions, executeBash
|
|
9
|
+
import { type BashExecutorOptions, executeBash } from "../bash-executor";
|
|
10
10
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
11
11
|
import { renderPromptTemplate } from "../prompt-templates";
|
|
12
12
|
import { checkBashInterception, checkSimpleLsInterception } from "./bash-interceptor";
|
|
13
13
|
import type { ToolSession } from "./index";
|
|
14
14
|
import { resolveToCwd } from "./path-utils";
|
|
15
15
|
import { ToolUIKit } from "./render-utils";
|
|
16
|
-
import {
|
|
16
|
+
import { formatTailTruncationNotice, type TruncationResult, truncateTail } from "./truncate";
|
|
17
17
|
|
|
18
18
|
export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
19
19
|
|
|
@@ -31,32 +31,12 @@ export interface BashToolDetails {
|
|
|
31
31
|
fullOutput?: string;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
* Pluggable operations for bash execution.
|
|
36
|
-
* Override to delegate command execution to remote systems.
|
|
37
|
-
*/
|
|
38
|
-
export interface BashOperations {
|
|
39
|
-
exec: (
|
|
40
|
-
command: string,
|
|
41
|
-
cwd: string,
|
|
42
|
-
options: {
|
|
43
|
-
onData: (data: Buffer) => void;
|
|
44
|
-
signal?: AbortSignal;
|
|
45
|
-
timeout?: number;
|
|
46
|
-
},
|
|
47
|
-
) => Promise<{ exitCode: number | null }>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface BashToolOptions {
|
|
51
|
-
/** Custom operations for command execution. Default: local shell */
|
|
52
|
-
operations?: BashOperations;
|
|
53
|
-
}
|
|
34
|
+
export interface BashToolOptions {}
|
|
54
35
|
|
|
55
36
|
/**
|
|
56
37
|
* Bash tool implementation.
|
|
57
38
|
*
|
|
58
39
|
* Executes bash commands with optional timeout and working directory.
|
|
59
|
-
* Supports custom operations for remote execution.
|
|
60
40
|
*/
|
|
61
41
|
export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
62
42
|
public readonly name = "bash";
|
|
@@ -65,11 +45,9 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
65
45
|
public readonly parameters = bashSchema;
|
|
66
46
|
|
|
67
47
|
private readonly session: ToolSession;
|
|
68
|
-
private readonly options?: BashToolOptions;
|
|
69
48
|
|
|
70
|
-
constructor(session: ToolSession
|
|
49
|
+
constructor(session: ToolSession) {
|
|
71
50
|
this.session = session;
|
|
72
|
-
this.options = options;
|
|
73
51
|
this.description = renderPromptTemplate(bashDescription);
|
|
74
52
|
}
|
|
75
53
|
|
|
@@ -125,12 +103,8 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
125
103
|
},
|
|
126
104
|
};
|
|
127
105
|
|
|
128
|
-
// Use custom operations if provided, otherwise use default local executor
|
|
129
|
-
const result = this.options?.operations
|
|
130
|
-
? await executeBashWithOperations(command, commandCwd, this.options.operations, executorOptions)
|
|
131
|
-
: await executeBash(command, executorOptions);
|
|
132
|
-
|
|
133
106
|
// Handle errors
|
|
107
|
+
const result = await executeBash(command, executorOptions);
|
|
134
108
|
if (result.cancelled) {
|
|
135
109
|
throw new Error(result.output || "Command aborted");
|
|
136
110
|
}
|
|
@@ -147,18 +121,10 @@ export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails> {
|
|
|
147
121
|
fullOutputPath: result.fullOutputPath,
|
|
148
122
|
fullOutput: currentOutput,
|
|
149
123
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (truncation.lastLinePartial) {
|
|
155
|
-
const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
|
|
156
|
-
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${result.fullOutputPath}]`;
|
|
157
|
-
} else if (truncation.truncatedBy === "lines") {
|
|
158
|
-
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${result.fullOutputPath}]`;
|
|
159
|
-
} else {
|
|
160
|
-
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${result.fullOutputPath}]`;
|
|
161
|
-
}
|
|
124
|
+
outputText += formatTailTruncationNotice(truncation, {
|
|
125
|
+
fullOutputPath: result.fullOutputPath,
|
|
126
|
+
originalContent: result.output,
|
|
127
|
+
});
|
|
162
128
|
}
|
|
163
129
|
|
|
164
130
|
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
|
@@ -263,7 +229,7 @@ export const bashToolRenderer = {
|
|
|
263
229
|
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
264
230
|
} else {
|
|
265
231
|
warnings.push(
|
|
266
|
-
`Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes
|
|
232
|
+
`Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes)} limit)`,
|
|
267
233
|
);
|
|
268
234
|
}
|
|
269
235
|
}
|
package/src/core/tools/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { AskTool, type AskToolDetails } from "./ask";
|
|
2
|
-
export {
|
|
2
|
+
export { BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
|
|
3
3
|
export { CalculatorTool, type CalculatorToolDetails } from "./calculator";
|
|
4
4
|
export { CompleteTool } from "./complete";
|
|
5
5
|
// Exa MCP tools (22 tools)
|
package/src/core/tools/output.ts
CHANGED
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
TRUNCATE_LENGTHS,
|
|
26
26
|
truncate,
|
|
27
27
|
} from "./render-utils";
|
|
28
|
-
import { getArtifactsDir } from "./task/artifacts";
|
|
29
28
|
|
|
30
29
|
const outputSchema = Type.Object({
|
|
31
30
|
ids: Type.Array(Type.String(), {
|
|
@@ -164,7 +163,7 @@ function applyQuery(data: unknown, query: string): unknown {
|
|
|
164
163
|
function listAvailableOutputs(artifactsDir: string): string[] {
|
|
165
164
|
try {
|
|
166
165
|
const files = fs.readdirSync(artifactsDir);
|
|
167
|
-
return files.filter((f) => f.endsWith(".
|
|
166
|
+
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(".md", ""));
|
|
168
167
|
} catch {
|
|
169
168
|
return [];
|
|
170
169
|
}
|
|
@@ -274,8 +273,8 @@ export class OutputTool implements AgentTool<typeof outputSchema, OutputToolDeta
|
|
|
274
273
|
};
|
|
275
274
|
}
|
|
276
275
|
|
|
277
|
-
const artifactsDir =
|
|
278
|
-
if (!
|
|
276
|
+
const artifactsDir = sessionFile.slice(0, -6); // strip .jsonl extension
|
|
277
|
+
if (!fs.existsSync(artifactsDir)) {
|
|
279
278
|
return {
|
|
280
279
|
content: [{ type: "text", text: "No artifacts directory found" }],
|
|
281
280
|
details: { outputs: [], notFound: params.ids },
|
|
@@ -296,14 +295,14 @@ export class OutputTool implements AgentTool<typeof outputSchema, OutputToolDeta
|
|
|
296
295
|
const queryResults: Array<{ id: string; value: unknown }> = [];
|
|
297
296
|
|
|
298
297
|
for (const id of params.ids) {
|
|
299
|
-
const outputPath = path.join(artifactsDir, `${id}.
|
|
300
|
-
|
|
301
|
-
if (!
|
|
298
|
+
const outputPath = path.join(artifactsDir, `${id}.md`);
|
|
299
|
+
const file = Bun.file(outputPath);
|
|
300
|
+
if (!(await file.exists())) {
|
|
302
301
|
notFound.push(id);
|
|
303
302
|
continue;
|
|
304
303
|
}
|
|
305
304
|
|
|
306
|
-
const rawContent =
|
|
305
|
+
const rawContent = await file.text();
|
|
307
306
|
const rawLines = rawContent.split("\n");
|
|
308
307
|
const totalLines = rawLines.length;
|
|
309
308
|
const totalChars = rawContent.length;
|
package/src/core/tools/python.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { PreludeHelper, PythonStatusEvent } from "../python-kernel";
|
|
|
14
14
|
import type { ToolSession } from "./index";
|
|
15
15
|
import { resolveToCwd } from "./path-utils";
|
|
16
16
|
import { getTreeBranch, getTreeContinuePrefix, shortenPath, ToolUIKit, truncate } from "./render-utils";
|
|
17
|
-
import { DEFAULT_MAX_BYTES,
|
|
17
|
+
import { DEFAULT_MAX_BYTES, formatTailTruncationNotice, type TruncationResult, truncateTail } from "./truncate";
|
|
18
18
|
|
|
19
19
|
export const PYTHON_DEFAULT_PREVIEW_LINES = 10;
|
|
20
20
|
|
|
@@ -234,7 +234,6 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
234
234
|
let details: PythonToolDetails | undefined;
|
|
235
235
|
|
|
236
236
|
if (truncation.truncated) {
|
|
237
|
-
const fullOutputSuffix = result.fullOutputPath ? ` Full output: ${result.fullOutputPath}` : "";
|
|
238
237
|
details = {
|
|
239
238
|
truncation,
|
|
240
239
|
fullOutputPath: result.fullOutputPath,
|
|
@@ -242,18 +241,10 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
|
|
|
242
241
|
images,
|
|
243
242
|
statusEvents: statusEvents.length > 0 ? statusEvents : undefined,
|
|
244
243
|
};
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (truncation.lastLinePartial) {
|
|
250
|
-
const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
|
|
251
|
-
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize})${fullOutputSuffix}]`;
|
|
252
|
-
} else if (truncation.truncatedBy === "lines") {
|
|
253
|
-
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}${fullOutputSuffix}]`;
|
|
254
|
-
} else {
|
|
255
|
-
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit)${fullOutputSuffix}]`;
|
|
256
|
-
}
|
|
244
|
+
outputText += formatTailTruncationNotice(truncation, {
|
|
245
|
+
fullOutputPath: result.fullOutputPath,
|
|
246
|
+
originalContent: result.output,
|
|
247
|
+
});
|
|
257
248
|
}
|
|
258
249
|
|
|
259
250
|
if (!details && (jsonOutputs.length > 0 || images.length > 0 || statusEvents.length > 0)) {
|
|
@@ -685,7 +676,7 @@ export const pythonToolRenderer = {
|
|
|
685
676
|
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
686
677
|
} else {
|
|
687
678
|
warnings.push(
|
|
688
|
-
`Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes
|
|
679
|
+
`Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes)} limit)`,
|
|
689
680
|
);
|
|
690
681
|
}
|
|
691
682
|
}
|
package/src/core/tools/ssh.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
|
|
|
14
14
|
import { executeSSH } from "../ssh/ssh-executor";
|
|
15
15
|
import type { ToolSession } from "./index";
|
|
16
16
|
import { ToolUIKit } from "./render-utils";
|
|
17
|
-
import {
|
|
17
|
+
import { formatTailTruncationNotice, type TruncationResult, truncateTail } from "./truncate";
|
|
18
18
|
|
|
19
19
|
const sshSchema = Type.Object({
|
|
20
20
|
host: Type.String({ description: "Host name from ssh.json or .ssh.json" }),
|
|
@@ -193,18 +193,10 @@ export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
|
|
|
193
193
|
truncation,
|
|
194
194
|
fullOutputPath: result.fullOutputPath,
|
|
195
195
|
};
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (truncation.lastLinePartial) {
|
|
201
|
-
const lastLineSize = formatSize(Buffer.byteLength(result.output.split("\n").pop() || "", "utf-8"));
|
|
202
|
-
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${result.fullOutputPath}]`;
|
|
203
|
-
} else if (truncation.truncatedBy === "lines") {
|
|
204
|
-
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${result.fullOutputPath}]`;
|
|
205
|
-
} else {
|
|
206
|
-
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${result.fullOutputPath}]`;
|
|
207
|
-
}
|
|
196
|
+
outputText += formatTailTruncationNotice(truncation, {
|
|
197
|
+
fullOutputPath: result.fullOutputPath,
|
|
198
|
+
originalContent: result.output,
|
|
199
|
+
});
|
|
208
200
|
}
|
|
209
201
|
|
|
210
202
|
if (result.exitCode !== 0 && result.exitCode !== undefined) {
|
|
@@ -311,7 +303,7 @@ export const sshToolRenderer = {
|
|
|
311
303
|
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
312
304
|
} else {
|
|
313
305
|
warnings.push(
|
|
314
|
-
`Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes
|
|
306
|
+
`Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes)} limit)`,
|
|
315
307
|
);
|
|
316
308
|
}
|
|
317
309
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Runs each subagent in a Bun Worker and forwards AgentEvents for progress tracking.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import path from "node:path";
|
|
7
8
|
import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
9
|
import type { AuthStorage } from "../../auth-storage";
|
|
9
10
|
import type { EventBus } from "../../event-bus";
|
|
@@ -16,7 +17,6 @@ import type { ToolSession } from "..";
|
|
|
16
17
|
import { LspTool } from "../lsp/index";
|
|
17
18
|
import type { LspParams } from "../lsp/types";
|
|
18
19
|
import { PythonTool } from "../python";
|
|
19
|
-
import { ensureArtifactsDir, getArtifactPaths } from "./artifacts";
|
|
20
20
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
21
21
|
import {
|
|
22
22
|
type AgentDefinition,
|
|
@@ -256,20 +256,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
256
256
|
const fullTask = context ? `${context}\n\n${task}` : task;
|
|
257
257
|
|
|
258
258
|
// Set up artifact paths and write input file upfront if artifacts dir provided
|
|
259
|
-
let artifactPaths: { inputPath: string; outputPath: string; jsonlPath: string } | undefined;
|
|
260
259
|
let subtaskSessionFile: string | undefined;
|
|
261
|
-
|
|
262
260
|
if (options.artifactsDir) {
|
|
263
|
-
|
|
264
|
-
artifactPaths = getArtifactPaths(options.artifactsDir, taskId);
|
|
265
|
-
subtaskSessionFile = artifactPaths.jsonlPath;
|
|
266
|
-
|
|
267
|
-
// Write input file immediately (real-time visibility)
|
|
268
|
-
try {
|
|
269
|
-
await Bun.write(artifactPaths.inputPath, fullTask);
|
|
270
|
-
} catch {
|
|
271
|
-
// Non-fatal, continue without input artifact
|
|
272
|
-
}
|
|
261
|
+
subtaskSessionFile = path.join(options.artifactsDir, `${taskId}.jsonl`);
|
|
273
262
|
}
|
|
274
263
|
|
|
275
264
|
// Add tools if specified
|
|
@@ -1042,9 +1031,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1042
1031
|
// Write output artifact (input and jsonl already written in real-time)
|
|
1043
1032
|
// Compute output metadata for Output tool integration
|
|
1044
1033
|
let outputMeta: { lineCount: number; charCount: number } | undefined;
|
|
1045
|
-
|
|
1034
|
+
let outputPath: string | undefined;
|
|
1035
|
+
if (options.artifactsDir) {
|
|
1036
|
+
outputPath = path.join(options.artifactsDir, `${taskId}.md`);
|
|
1046
1037
|
try {
|
|
1047
|
-
await Bun.write(
|
|
1038
|
+
await Bun.write(outputPath, rawOutput);
|
|
1048
1039
|
outputMeta = {
|
|
1049
1040
|
lineCount: rawOutput.split("\n").length,
|
|
1050
1041
|
charCount: rawOutput.length,
|
|
@@ -1076,7 +1067,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1076
1067
|
error: exitCode !== 0 && stderr ? stderr : undefined,
|
|
1077
1068
|
aborted: wasAborted,
|
|
1078
1069
|
usage: hasUsage ? accumulatedUsage : undefined,
|
|
1079
|
-
|
|
1070
|
+
outputPath,
|
|
1080
1071
|
extractedToolData: progress.extractedToolData,
|
|
1081
1072
|
outputMeta,
|
|
1082
1073
|
};
|
|
@@ -13,13 +13,17 @@
|
|
|
13
13
|
* - Session artifacts for debugging
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
16
19
|
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
17
20
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
21
|
+
import { nanoid } from "nanoid";
|
|
18
22
|
import type { Theme } from "../../../modes/interactive/theme/theme";
|
|
19
23
|
import taskDescriptionTemplate from "../../../prompts/tools/task.md" with { type: "text" };
|
|
20
24
|
import { renderPromptTemplate } from "../../prompt-templates";
|
|
25
|
+
import type { ToolSession } from "..";
|
|
21
26
|
import { formatDuration } from "../render-utils";
|
|
22
|
-
import { cleanupTempDir, createTempArtifactsDir, getArtifactsDir } from "./artifacts";
|
|
23
27
|
import { discoverAgents, getAgent } from "./discovery";
|
|
24
28
|
import { runSubprocess } from "./executor";
|
|
25
29
|
import { mapWithConcurrencyLimit } from "./parallel";
|
|
@@ -36,7 +40,6 @@ import {
|
|
|
36
40
|
|
|
37
41
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
38
42
|
import "../review";
|
|
39
|
-
import type { ToolSession } from "..";
|
|
40
43
|
|
|
41
44
|
/** Format byte count for display */
|
|
42
45
|
function formatBytes(bytes: number): string {
|
|
@@ -276,9 +279,10 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
276
279
|
|
|
277
280
|
// Derive artifacts directory
|
|
278
281
|
const sessionFile = this.session.getSessionFile();
|
|
279
|
-
const artifactsDir = sessionFile ?
|
|
280
|
-
const tempArtifactsDir = artifactsDir ? null :
|
|
282
|
+
const artifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
|
|
283
|
+
const tempArtifactsDir = artifactsDir ? null : path.join(tmpdir(), `omp-task-${nanoid()}`);
|
|
281
284
|
const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
|
|
285
|
+
await mkdir(effectiveArtifactsDir, { recursive: true });
|
|
282
286
|
|
|
283
287
|
// Initialize progress tracking
|
|
284
288
|
const progressMap = new Map<number, AgentProgress>();
|
|
@@ -435,8 +439,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
435
439
|
// Collect output paths (artifacts already written by executor in real-time)
|
|
436
440
|
const outputPaths: string[] = [];
|
|
437
441
|
for (const result of results) {
|
|
438
|
-
if (result.
|
|
439
|
-
outputPaths.push(result.
|
|
442
|
+
if (result.outputPath) {
|
|
443
|
+
outputPaths.push(result.outputPath);
|
|
440
444
|
}
|
|
441
445
|
}
|
|
442
446
|
|
|
@@ -468,7 +472,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
468
472
|
|
|
469
473
|
// Cleanup temp directory if used
|
|
470
474
|
if (tempArtifactsDir) {
|
|
471
|
-
await
|
|
475
|
+
await rm(tempArtifactsDir, { recursive: true, force: true });
|
|
472
476
|
}
|
|
473
477
|
|
|
474
478
|
return {
|
|
@@ -482,11 +486,6 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
482
486
|
},
|
|
483
487
|
};
|
|
484
488
|
} catch (err) {
|
|
485
|
-
// Cleanup temp directory on error
|
|
486
|
-
if (tempArtifactsDir) {
|
|
487
|
-
await cleanupTempDir(tempArtifactsDir);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
489
|
return {
|
|
491
490
|
content: [{ type: "text", text: `Task execution failed: ${err}` }],
|
|
492
491
|
details: {
|
|
@@ -153,7 +153,8 @@ export interface SingleResult {
|
|
|
153
153
|
aborted?: boolean;
|
|
154
154
|
/** Aggregated usage from the subprocess, accumulated incrementally from message_end events. */
|
|
155
155
|
usage?: Usage;
|
|
156
|
-
|
|
156
|
+
/** Output path for the task result */
|
|
157
|
+
outputPath?: string;
|
|
157
158
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
158
159
|
extractedToolData?: Record<string, unknown[]>;
|
|
159
160
|
/** Output metadata for Output tool integration */
|