@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.
@@ -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, DEFAULT_MAX_COLUMN } from "./tools/truncate";
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
- private buffer = "";
33
- private lineEnds: number[] = []; // String index after each \n
34
-
35
- private fileSink?: Bun.FileSink;
36
- private filePath?: string;
37
-
38
- private readonly allocateFilePath: () => string;
39
- private readonly spillThreshold: number;
40
- private readonly maxColumn: number;
41
- private readonly onLine?: (line: string) => void;
42
- private readonly onChunk?: (chunk: string) => void;
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.allocateFilePath = allocateFilePath;
54
- this.spillThreshold = spillThreshold;
55
- this.maxColumn = maxColumn;
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
- private pushLine(line: string, term?: string): void {
61
- while (line.length > this.maxColumn) {
62
- this.pushLine(line.slice(0, this.maxColumn), "--\n");
63
- line = line.slice(this.maxColumn);
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.buffer += line;
67
- if (term) {
68
- this.buffer += term;
69
- }
60
+ const sink = overflow ? await this.#fileSink() : null;
70
61
 
71
- this.lineEnds.push(this.buffer.length);
72
- this.onLine?.(line);
62
+ this.#buffer += data;
63
+ await sink?.write(data);
73
64
 
74
- if (this.buffer.length > this.spillThreshold) {
75
- this.spillHalf();
65
+ if (this.#buffer.length > this.#spillThreshold) {
66
+ this.#buffer = this.#buffer.slice(-this.#spillThreshold);
76
67
  }
77
68
  }
78
69
 
79
- private pushChunk(line: string): void {
80
- this.onChunk?.(line);
81
- this.pushLine(line);
82
- }
83
-
84
- private getFileSink(): Bun.FileSink {
85
- if (!this.fileSink) {
86
- const filePath = this.allocateFilePath();
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.fileSink;
79
+ return this.#file.sink;
91
80
  }
92
81
 
93
- private spillHalf(): void {
94
- const target = this.buffer.length >>> 1;
95
-
96
- // Binary search: first line ending >= target
97
- let lo = 0;
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
- createWritable(): WritableStream<Uint8Array> {
125
- const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
126
- let buf = "";
127
-
128
- const flushLines = () => {
129
- let start = 0;
130
- while (true) {
131
- const nl = buf.indexOf("\n", start);
132
- if (nl === -1) break;
133
- this.pushChunk(buf.slice(start, nl + 1));
134
- start = nl + 1;
135
- }
136
- buf = buf.slice(start);
137
- };
138
-
139
- const finalize = () => {
140
- buf += sanitizeText(decoder.decode());
141
- flushLines();
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
- createStringWritable(): WritableStream<string> {
159
- let buf = "";
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
- dump(annotation?: string): OutputResult {
195
- let output = this.buffer;
196
- if (annotation) {
197
- output += `\n${annotation}\n`;
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
  }
@@ -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, executeBashWithOperations } from "../bash-executor";
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 { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
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, options?: BashToolOptions) {
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
- const startLine = truncation.totalLines - truncation.outputLines + 1;
152
- const endLine = truncation.totalLines;
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 ?? DEFAULT_MAX_BYTES)} limit)`,
232
+ `Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes)} limit)`,
267
233
  );
268
234
  }
269
235
  }
@@ -1,5 +1,5 @@
1
1
  export { AskTool, type AskToolDetails } from "./ask";
2
- export { type BashOperations, BashTool, type BashToolDetails, type BashToolOptions } from "./bash";
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)
@@ -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(".out.md")).map((f) => f.replace(".out.md", ""));
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 = getArtifactsDir(sessionFile);
278
- if (!artifactsDir || !fs.existsSync(artifactsDir)) {
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}.out.md`);
300
-
301
- if (!fs.existsSync(outputPath)) {
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 = fs.readFileSync(outputPath, "utf-8");
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;
@@ -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, formatSize, type TruncationResult, truncateTail } from "./truncate";
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
- const startLine = truncation.totalLines - truncation.outputLines + 1;
247
- const endLine = truncation.totalLines;
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 ?? DEFAULT_MAX_BYTES)} limit)`,
679
+ `Truncated: ${truncation.outputLines} lines shown (${ui.formatBytes(truncation.maxBytes)} limit)`,
689
680
  );
690
681
  }
691
682
  }
@@ -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 { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
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
- const startLine = truncation.totalLines - truncation.outputLines + 1;
198
- const endLine = truncation.totalLines;
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 ?? DEFAULT_MAX_BYTES)} limit)`,
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
- ensureArtifactsDir(options.artifactsDir);
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
- if (artifactPaths) {
1034
+ let outputPath: string | undefined;
1035
+ if (options.artifactsDir) {
1036
+ outputPath = path.join(options.artifactsDir, `${taskId}.md`);
1046
1037
  try {
1047
- await Bun.write(artifactPaths.outputPath, rawOutput);
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
- artifactPaths,
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 ? getArtifactsDir(sessionFile) : null;
280
- const tempArtifactsDir = artifactsDir ? null : createTempArtifactsDir();
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.artifactPaths) {
439
- outputPaths.push(result.artifactPaths.outputPath);
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 cleanupTempDir(tempArtifactsDir);
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
- artifactPaths?: { inputPath: string; outputPath: string; jsonlPath?: string };
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 */