@oh-my-pi/pi-coding-agent 10.6.1 → 11.0.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 +44 -0
- package/README.md +80 -79
- package/docs/compaction.md +182 -149
- package/docs/config-usage.md +141 -78
- package/docs/custom-tools.md +45 -16
- package/docs/extension-loading.md +56 -954
- package/docs/extensions.md +192 -51
- package/docs/hooks.md +109 -70
- package/docs/python-repl.md +52 -19
- package/docs/rpc.md +43 -19
- package/docs/sdk.md +270 -211
- package/docs/session-tree-plan.md +60 -417
- package/docs/session.md +104 -39
- package/docs/skills.md +59 -95
- package/docs/theme.md +139 -110
- package/docs/tree.md +42 -33
- package/docs/tui.md +226 -80
- package/package.json +8 -9
- package/src/capability/index.ts +3 -4
- package/src/cli/args.ts +4 -4
- package/src/cli/grep-cli.ts +1 -1
- package/src/commit/agentic/index.ts +4 -3
- package/src/commit/git/index.ts +2 -3
- package/src/commit/map-reduce/index.ts +2 -1
- package/src/config/prompt-templates.ts +2 -0
- package/src/config/settings-schema.ts +30 -7
- package/src/config/settings.ts +0 -14
- package/src/config.ts +2 -2
- package/src/discovery/agents.ts +36 -0
- package/src/discovery/index.ts +1 -0
- package/src/exa/mcp-client.ts +3 -3
- package/src/ipy/executor.ts +5 -7
- package/src/ipy/gateway-coordinator.ts +1 -1
- package/src/ipy/kernel.ts +20 -15
- package/src/ipy/prelude.py +1 -1
- package/src/ipy/runtime.ts +7 -6
- package/src/lsp/lspmux.ts +3 -3
- package/src/main.ts +6 -8
- package/src/mcp/tool-bridge.ts +19 -9
- package/src/modes/components/assistant-message.ts +2 -2
- package/src/modes/components/hook-editor.ts +4 -4
- package/src/modes/components/settings-defs.ts +37 -2
- package/src/modes/components/tool-execution.ts +7 -7
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/event-controller.ts +4 -7
- package/src/modes/controllers/input-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +3 -5
- package/src/modes/rpc/rpc-mode.ts +8 -9
- package/src/patch/index.ts +6 -6
- package/src/prompts/agents/explore.md +2 -2
- package/src/prompts/agents/frontmatter.md +5 -5
- package/src/prompts/agents/plan.md +3 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -3
- package/src/sdk.ts +13 -9
- package/src/session/agent-session.ts +6 -4
- package/src/session/compaction/compaction.ts +3 -3
- package/src/session/session-manager.ts +8 -9
- package/src/ssh/connection-manager.ts +4 -4
- package/src/system-prompt.ts +2 -6
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +14 -35
- package/src/task/omp-command.ts +3 -1
- package/src/task/output-manager.ts +20 -6
- package/src/task/parallel.ts +3 -3
- package/src/task/render.ts +16 -2
- package/src/task/types.ts +13 -20
- package/src/task/worktree.ts +3 -3
- package/src/tools/ask.ts +3 -8
- package/src/tools/fetch.ts +2 -2
- package/src/tools/gemini-image.ts +5 -6
- package/src/tools/grep.ts +5 -5
- package/src/tools/index.ts +12 -5
- package/src/tools/read.ts +1 -1
- package/src/tools/todo-write.ts +2 -3
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/image-resize.ts +1 -1
- package/src/utils/timings.ts +3 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/utils.ts +2 -3
- package/src/web/scrapers/youtube.ts +2 -3
- package/src/web/search/auth.ts +5 -6
- package/src/web/search/providers/anthropic.ts +3 -2
- package/src/utils/terminal-notify.ts +0 -37
package/src/task/executor.ts
CHANGED
|
@@ -157,6 +157,8 @@ export interface ExecutorOptions {
|
|
|
157
157
|
modelOverride?: string | string[];
|
|
158
158
|
thinkingLevel?: ThinkingLevel;
|
|
159
159
|
outputSchema?: unknown;
|
|
160
|
+
/** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */
|
|
161
|
+
taskDepth?: number;
|
|
160
162
|
enableLsp?: boolean;
|
|
161
163
|
signal?: AbortSignal;
|
|
162
164
|
onProgress?: (progress: AgentProgress) => void;
|
|
@@ -428,17 +430,25 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
428
430
|
subtaskSessionFile = path.join(options.artifactsDir, `${id}.jsonl`);
|
|
429
431
|
}
|
|
430
432
|
|
|
433
|
+
const settings = options.settings ?? Settings.isolated();
|
|
434
|
+
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
435
|
+
const parentDepth = options.taskDepth ?? 0;
|
|
436
|
+
const childDepth = parentDepth + 1;
|
|
437
|
+
const atMaxDepth = maxRecursionDepth >= 0 && childDepth >= maxRecursionDepth;
|
|
438
|
+
|
|
431
439
|
// Add tools if specified
|
|
432
440
|
let toolNames: string[] | undefined;
|
|
433
441
|
if (agent.tools && agent.tools.length > 0) {
|
|
434
442
|
toolNames = agent.tools;
|
|
435
443
|
// Auto-include task tool if spawns defined but task not in tools
|
|
436
|
-
if (agent.spawns !== undefined && !toolNames.includes("task")) {
|
|
444
|
+
if (agent.spawns !== undefined && !toolNames.includes("task") && !atMaxDepth) {
|
|
437
445
|
toolNames = [...toolNames, "task"];
|
|
438
446
|
}
|
|
439
447
|
}
|
|
440
448
|
|
|
441
|
-
|
|
449
|
+
if (atMaxDepth && toolNames?.includes("task")) {
|
|
450
|
+
toolNames = toolNames.filter(name => name !== "task");
|
|
451
|
+
}
|
|
442
452
|
const pythonToolMode = settings.get("python.toolMode") ?? "both";
|
|
443
453
|
if (toolNames?.includes("exec")) {
|
|
444
454
|
const expanded = toolNames.filter(name => name !== "exec");
|
|
@@ -454,7 +464,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
454
464
|
|
|
455
465
|
const modelPatterns = normalizeModelPatterns(modelOverride ?? agent.model);
|
|
456
466
|
const sessionFile = subtaskSessionFile ?? null;
|
|
457
|
-
const spawnsEnv =
|
|
467
|
+
const spawnsEnv = atMaxDepth
|
|
468
|
+
? ""
|
|
469
|
+
: agent.spawns === undefined
|
|
470
|
+
? ""
|
|
471
|
+
: agent.spawns === "*"
|
|
472
|
+
? "*"
|
|
473
|
+
: agent.spawns.join(",");
|
|
458
474
|
|
|
459
475
|
const lspEnabled = enableLsp ?? true;
|
|
460
476
|
const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("python");
|
|
@@ -877,6 +893,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
877
893
|
sessionManager,
|
|
878
894
|
hasUI: false,
|
|
879
895
|
spawns: spawnsEnv,
|
|
896
|
+
taskDepth: childDepth,
|
|
897
|
+
parentTaskPrefix: id,
|
|
880
898
|
enableLsp: lspEnabled,
|
|
881
899
|
skipPythonPreflight,
|
|
882
900
|
enableMCP,
|
|
@@ -1003,9 +1021,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1003
1021
|
}
|
|
1004
1022
|
|
|
1005
1023
|
const lastMessage = session.state.messages[session.state.messages.length - 1];
|
|
1006
|
-
if (lastMessage?.role === "assistant"
|
|
1007
|
-
|
|
1008
|
-
|
|
1024
|
+
if (lastMessage?.role === "assistant") {
|
|
1025
|
+
if (lastMessage.stopReason === "aborted") {
|
|
1026
|
+
aborted = abortReason === "signal" || abortReason === undefined;
|
|
1027
|
+
exitCode = 1;
|
|
1028
|
+
} else if (lastMessage.stopReason === "error") {
|
|
1029
|
+
exitCode = 1;
|
|
1030
|
+
error ??= lastMessage.errorMessage || "Subagent failed";
|
|
1031
|
+
}
|
|
1009
1032
|
}
|
|
1010
1033
|
} catch (err) {
|
|
1011
1034
|
exitCode = 1;
|
|
@@ -1093,7 +1116,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1093
1116
|
stderr = "";
|
|
1094
1117
|
}
|
|
1095
1118
|
} else {
|
|
1096
|
-
const allowFallback = !done.aborted && !signal?.aborted;
|
|
1119
|
+
const allowFallback = exitCode === 0 && !done.aborted && !signal?.aborted;
|
|
1097
1120
|
const { normalized: normalizedSchema, error: schemaError } = normalizeOutputSchema(outputSchema);
|
|
1098
1121
|
const hasOutputSchema = normalizedSchema !== undefined && !schemaError;
|
|
1099
1122
|
const fallback = allowFallback ? resolveFallbackCompletion(rawOutput, outputSchema) : null;
|
|
@@ -1110,7 +1133,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1110
1133
|
} else if (!hasOutputSchema && allowFallback && rawOutput.trim().length > 0) {
|
|
1111
1134
|
exitCode = 0;
|
|
1112
1135
|
stderr = "";
|
|
1113
|
-
} else {
|
|
1136
|
+
} else if (exitCode === 0) {
|
|
1114
1137
|
const warning = "SYSTEM WARNING: Subagent exited without calling submit_result tool after 3 reminders.";
|
|
1115
1138
|
rawOutput = rawOutput ? `${warning}\n\n${rawOutput}` : warning;
|
|
1116
1139
|
}
|
package/src/task/index.ts
CHANGED
|
@@ -17,8 +17,8 @@ import * as os from "node:os";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
19
19
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
20
|
+
import { $env, Snowflake } from "@oh-my-pi/pi-utils";
|
|
20
21
|
import { $ } from "bun";
|
|
21
|
-
import { nanoid } from "nanoid";
|
|
22
22
|
import type { ToolSession } from "..";
|
|
23
23
|
import { isDefaultModelAlias } from "../config/model-resolver";
|
|
24
24
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
@@ -35,15 +35,7 @@ import { AgentOutputManager } from "./output-manager";
|
|
|
35
35
|
import { mapWithConcurrencyLimit } from "./parallel";
|
|
36
36
|
import { renderCall, renderResult } from "./render";
|
|
37
37
|
import { renderTemplate } from "./template";
|
|
38
|
-
import {
|
|
39
|
-
type AgentProgress,
|
|
40
|
-
MAX_CONCURRENCY,
|
|
41
|
-
MAX_PARALLEL_TASKS,
|
|
42
|
-
type SingleResult,
|
|
43
|
-
type TaskParams,
|
|
44
|
-
type TaskToolDetails,
|
|
45
|
-
taskSchema,
|
|
46
|
-
} from "./types";
|
|
38
|
+
import { type AgentProgress, type SingleResult, type TaskParams, type TaskToolDetails, taskSchema } from "./types";
|
|
47
39
|
import {
|
|
48
40
|
applyBaseline,
|
|
49
41
|
captureBaseline,
|
|
@@ -110,13 +102,12 @@ export { taskSchema } from "./types";
|
|
|
110
102
|
/**
|
|
111
103
|
* Build dynamic tool description listing available agents.
|
|
112
104
|
*/
|
|
113
|
-
async function buildDescription(cwd: string): Promise<string> {
|
|
105
|
+
async function buildDescription(cwd: string, maxConcurrency: number): Promise<string> {
|
|
114
106
|
const { agents } = await discoverAgents(cwd);
|
|
115
107
|
|
|
116
108
|
return renderPromptTemplate(taskDescriptionTemplate, {
|
|
117
109
|
agents,
|
|
118
|
-
|
|
119
|
-
MAX_CONCURRENCY,
|
|
110
|
+
MAX_CONCURRENCY: maxConcurrency,
|
|
120
111
|
});
|
|
121
112
|
}
|
|
122
113
|
|
|
@@ -144,14 +135,15 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
144
135
|
private constructor(session: ToolSession, description: string) {
|
|
145
136
|
this.session = session;
|
|
146
137
|
this.description = description;
|
|
147
|
-
this.blockedAgent =
|
|
138
|
+
this.blockedAgent = $env.PI_BLOCKED_AGENT;
|
|
148
139
|
}
|
|
149
140
|
|
|
150
141
|
/**
|
|
151
142
|
* Create a TaskTool instance with async agent discovery.
|
|
152
143
|
*/
|
|
153
144
|
public static async create(session: ToolSession): Promise<TaskTool> {
|
|
154
|
-
const
|
|
145
|
+
const maxConcurrency = session.settings.get("task.maxConcurrency");
|
|
146
|
+
const description = await buildDescription(session.cwd, maxConcurrency);
|
|
155
147
|
return new TaskTool(session, description);
|
|
156
148
|
}
|
|
157
149
|
|
|
@@ -165,6 +157,8 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
165
157
|
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
|
|
166
158
|
const { agent: agentName, context, schema: outputSchema, isolated } = params;
|
|
167
159
|
const isIsolated = isolated === true;
|
|
160
|
+
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
161
|
+
const taskDepth = this.session.taskDepth ?? 0;
|
|
168
162
|
|
|
169
163
|
// Validate agent exists
|
|
170
164
|
const agent = getAgent(agents, agentName);
|
|
@@ -221,23 +215,6 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
221
215
|
};
|
|
222
216
|
}
|
|
223
217
|
|
|
224
|
-
// Validate task count
|
|
225
|
-
if (params.tasks.length > MAX_PARALLEL_TASKS) {
|
|
226
|
-
return {
|
|
227
|
-
content: [
|
|
228
|
-
{
|
|
229
|
-
type: "text",
|
|
230
|
-
text: `Too many tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
|
231
|
-
},
|
|
232
|
-
],
|
|
233
|
-
details: {
|
|
234
|
-
projectAgentsDir,
|
|
235
|
-
results: [],
|
|
236
|
-
totalDurationMs: 0,
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
218
|
const tasks = params.tasks;
|
|
242
219
|
const missingTaskIndexes: number[] = [];
|
|
243
220
|
const idIndexes = new Map<string, number[]>();
|
|
@@ -313,7 +290,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
313
290
|
// Derive artifacts directory
|
|
314
291
|
const sessionFile = this.session.getSessionFile();
|
|
315
292
|
const artifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
|
|
316
|
-
const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `omp-task-${
|
|
293
|
+
const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `omp-task-${Snowflake.next()}`);
|
|
317
294
|
const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
|
|
318
295
|
|
|
319
296
|
// Initialize progress tracking
|
|
@@ -471,6 +448,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
471
448
|
index,
|
|
472
449
|
id: task.id,
|
|
473
450
|
context: undefined, // Already prepended above
|
|
451
|
+
taskDepth,
|
|
474
452
|
modelOverride,
|
|
475
453
|
thinkingLevel: thinkingLevelOverride,
|
|
476
454
|
outputSchema: effectiveOutputSchema,
|
|
@@ -516,6 +494,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
516
494
|
index,
|
|
517
495
|
id: task.id,
|
|
518
496
|
context: undefined, // Already prepended above
|
|
497
|
+
taskDepth,
|
|
519
498
|
modelOverride,
|
|
520
499
|
thinkingLevel: thinkingLevelOverride,
|
|
521
500
|
outputSchema: effectiveOutputSchema,
|
|
@@ -577,7 +556,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
577
556
|
// Execute in parallel with concurrency limit
|
|
578
557
|
const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
|
|
579
558
|
tasksWithSkills,
|
|
580
|
-
|
|
559
|
+
maxConcurrency,
|
|
581
560
|
runTask,
|
|
582
561
|
signal,
|
|
583
562
|
);
|
|
@@ -658,7 +637,7 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
|
|
658
637
|
if (!combinedPatch.trim()) {
|
|
659
638
|
patchesApplied = true;
|
|
660
639
|
} else {
|
|
661
|
-
const combinedPatchPath = path.join(os.tmpdir(), `omp-task-combined-${
|
|
640
|
+
const combinedPatchPath = path.join(os.tmpdir(), `omp-task-combined-${Snowflake.next()}.patch`);
|
|
662
641
|
try {
|
|
663
642
|
await Bun.write(combinedPatchPath, combinedPatch);
|
|
664
643
|
const checkResult = await $`git apply --check --binary ${combinedPatchPath}`
|
package/src/task/omp-command.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
|
|
3
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
4
|
+
|
|
3
5
|
interface OmpCommand {
|
|
4
6
|
cmd: string;
|
|
5
7
|
args: string[];
|
|
@@ -10,7 +12,7 @@ const DEFAULT_CMD = process.platform === "win32" ? "omp.cmd" : "omp";
|
|
|
10
12
|
const DEFAULT_SHELL = process.platform === "win32";
|
|
11
13
|
|
|
12
14
|
export function resolveOmpCommand(): OmpCommand {
|
|
13
|
-
const envCmd =
|
|
15
|
+
const envCmd = $env.PI_SUBPROCESS_CMD;
|
|
14
16
|
if (envCmd?.trim()) {
|
|
15
17
|
return { cmd: envCmd, args: [], shell: DEFAULT_SHELL };
|
|
16
18
|
}
|
|
@@ -3,24 +3,33 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Ensures unique output IDs across task tool invocations within a session.
|
|
5
5
|
* Prefixes each ID with a sequential number (e.g., "0-AuthProvider", "1-AuthApi").
|
|
6
|
+
* If a parent prefix is provided, IDs are nested (e.g., "0-Auth.1-Subtask").
|
|
6
7
|
*
|
|
7
8
|
* This enables reliable agent:// URL resolution and prevents artifact collisions.
|
|
8
9
|
*/
|
|
9
10
|
import * as fs from "node:fs/promises";
|
|
10
11
|
|
|
12
|
+
function escapeRegExp(value: string): string {
|
|
13
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* Manages agent output ID allocation to ensure uniqueness.
|
|
13
18
|
*
|
|
14
19
|
* Each allocated ID gets a numeric prefix based on allocation order.
|
|
20
|
+
* If configured with a parent prefix, the numeric prefix is appended after
|
|
21
|
+
* the parent (e.g., "0-Parent.0-Child").
|
|
15
22
|
* On resume, scans existing files to find the next available index.
|
|
16
23
|
*/
|
|
17
24
|
export class AgentOutputManager {
|
|
18
25
|
#nextId = 0;
|
|
19
26
|
#initialized = false;
|
|
20
27
|
readonly #getArtifactsDir: () => string | null;
|
|
28
|
+
readonly #parentPrefix: string | undefined;
|
|
21
29
|
|
|
22
|
-
constructor(getArtifactsDir: () => string | null) {
|
|
30
|
+
constructor(getArtifactsDir: () => string | null, options?: { parentPrefix?: string }) {
|
|
23
31
|
this.#getArtifactsDir = getArtifactsDir;
|
|
32
|
+
this.#parentPrefix = options?.parentPrefix;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
/**
|
|
@@ -41,12 +50,15 @@ export class AgentOutputManager {
|
|
|
41
50
|
return; // Directory doesn't exist yet
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
const pattern = this.#parentPrefix
|
|
54
|
+
? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)-.*\\.md$`)
|
|
55
|
+
: /^(\d+)-.*\.md$/;
|
|
56
|
+
|
|
44
57
|
let maxId = -1;
|
|
45
58
|
for (const file of files) {
|
|
46
|
-
|
|
47
|
-
const match = file.match(/^(\d+)-.*\.md$/);
|
|
59
|
+
const match = file.match(pattern);
|
|
48
60
|
if (match) {
|
|
49
|
-
const id = parseInt(match[1], 10);
|
|
61
|
+
const id = Number.parseInt(match[1], 10);
|
|
50
62
|
if (id > maxId) maxId = id;
|
|
51
63
|
}
|
|
52
64
|
}
|
|
@@ -61,7 +73,8 @@ export class AgentOutputManager {
|
|
|
61
73
|
*/
|
|
62
74
|
async allocate(id: string): Promise<string> {
|
|
63
75
|
await this.#ensureInitialized();
|
|
64
|
-
|
|
76
|
+
const prefix = this.#parentPrefix ? `${this.#parentPrefix}.` : "";
|
|
77
|
+
return `${prefix}${this.#nextId++}-${id}`;
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
/**
|
|
@@ -72,7 +85,8 @@ export class AgentOutputManager {
|
|
|
72
85
|
*/
|
|
73
86
|
async allocateBatch(ids: string[]): Promise<string[]> {
|
|
74
87
|
await this.#ensureInitialized();
|
|
75
|
-
|
|
88
|
+
const prefix = this.#parentPrefix ? `${this.#parentPrefix}.` : "";
|
|
89
|
+
return ids.map(id => `${prefix}${this.#nextId++}-${id}`);
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
/**
|
package/src/task/parallel.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parallel execution with concurrency control.
|
|
3
3
|
*/
|
|
4
|
-
import { MAX_CONCURRENCY } from "./types";
|
|
5
|
-
|
|
6
4
|
/** Result of parallel execution */
|
|
7
5
|
export interface ParallelResult<R> {
|
|
8
6
|
/** Results array - undefined entries indicate tasks that were skipped due to abort */
|
|
@@ -31,7 +29,9 @@ export async function mapWithConcurrencyLimit<T, R>(
|
|
|
31
29
|
fn: (item: T, index: number) => Promise<R>,
|
|
32
30
|
signal?: AbortSignal,
|
|
33
31
|
): Promise<ParallelResult<R>> {
|
|
34
|
-
const
|
|
32
|
+
const normalizedConcurrency = Number.isFinite(concurrency) ? Math.floor(concurrency) : items.length;
|
|
33
|
+
const effectiveConcurrency = normalizedConcurrency > 0 ? normalizedConcurrency : items.length;
|
|
34
|
+
const limit = Math.max(1, Math.min(effectiveConcurrency, items.length));
|
|
35
35
|
const results: (R | undefined)[] = new Array(items.length);
|
|
36
36
|
let nextIndex = 0;
|
|
37
37
|
|
package/src/task/render.ts
CHANGED
|
@@ -77,6 +77,18 @@ function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
|
77
77
|
return "";
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function formatTaskId(id: string): string {
|
|
81
|
+
const segments = id.split(".");
|
|
82
|
+
if (segments.length < 2) return id;
|
|
83
|
+
|
|
84
|
+
const parsed = segments.map(segment => segment.match(/^(\d+)-(.+)$/));
|
|
85
|
+
if (parsed.some(match => !match)) return id;
|
|
86
|
+
|
|
87
|
+
const indices = parsed.map(match => match![1]).join(".");
|
|
88
|
+
const labels = parsed.map(match => match![2]).join(">");
|
|
89
|
+
return `${indices} ${labels}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
const MISSING_SUBMIT_RESULT_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling submit_result tool";
|
|
81
93
|
|
|
82
94
|
function extractMissingSubmitResultWarning(output: string): { warning?: string; rest: string } {
|
|
@@ -518,7 +530,8 @@ function renderAgentProgress(
|
|
|
518
530
|
|
|
519
531
|
// Main status line: id: description [status] · stats · ⟨agent⟩
|
|
520
532
|
const description = progress.description?.trim();
|
|
521
|
-
const
|
|
533
|
+
const displayId = formatTaskId(progress.id);
|
|
534
|
+
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
522
535
|
let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
|
|
523
536
|
|
|
524
537
|
// Only show badge for non-running states (spinner already indicates running)
|
|
@@ -748,7 +761,8 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
748
761
|
|
|
749
762
|
// Main status line: id: description [status] · stats · ⟨agent⟩
|
|
750
763
|
const description = result.description?.trim();
|
|
751
|
-
const
|
|
764
|
+
const displayId = formatTaskId(result.id);
|
|
765
|
+
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
752
766
|
let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)} ${formatBadge(
|
|
753
767
|
statusText,
|
|
754
768
|
iconColor,
|
package/src/task/types.ts
CHANGED
|
@@ -1,35 +1,28 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
3
4
|
import { type Static, Type } from "@sinclair/typebox";
|
|
4
5
|
|
|
5
6
|
/** Source of an agent definition */
|
|
6
7
|
export type AgentSource = "bundled" | "user" | "project";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const parseNumber = (value: string | undefined, defaultValue: number): number => {
|
|
10
|
+
if (value) {
|
|
11
|
+
try {
|
|
12
|
+
const number = Number.parseInt(value, 10);
|
|
13
|
+
if (!Number.isNaN(number) && number > 0) {
|
|
14
|
+
return number;
|
|
15
|
+
}
|
|
16
|
+
} catch {}
|
|
12
17
|
}
|
|
13
|
-
try {
|
|
14
|
-
const number = Number.parseInt(value, 10);
|
|
15
|
-
if (!Number.isNaN(number) && number > 0) {
|
|
16
|
-
return number;
|
|
17
|
-
}
|
|
18
|
-
} catch {}
|
|
19
18
|
return defaultValue;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Maximum tasks per call */
|
|
23
|
-
export const MAX_PARALLEL_TASKS = getEnv("OMP_TASK_MAX_PARALLEL", 32);
|
|
24
|
-
|
|
25
|
-
/** Maximum concurrent tasks */
|
|
26
|
-
export const MAX_CONCURRENCY = getEnv("OMP_TASK_MAX_CONCURRENCY", 16);
|
|
19
|
+
};
|
|
27
20
|
|
|
28
21
|
/** Maximum output bytes per agent */
|
|
29
|
-
export const MAX_OUTPUT_BYTES =
|
|
22
|
+
export const MAX_OUTPUT_BYTES = parseNumber($env.PI_TASK_MAX_OUTPUT_BYTES, 500_000);
|
|
30
23
|
|
|
31
24
|
/** Maximum output lines per agent */
|
|
32
|
-
export const MAX_OUTPUT_LINES =
|
|
25
|
+
export const MAX_OUTPUT_LINES = parseNumber($env.PI_TASK_MAX_OUTPUT_LINES, 5000);
|
|
33
26
|
|
|
34
27
|
/** EventBus channel for raw subagent events */
|
|
35
28
|
export const TASK_SUBAGENT_EVENT_CHANNEL = "task:subagent:event";
|
|
@@ -66,7 +59,7 @@ export const taskSchema = Type.Object({
|
|
|
66
59
|
schema: Type.Optional(
|
|
67
60
|
Type.Record(Type.String(), Type.Unknown(), { description: "JTD schema defining expected response structure" }),
|
|
68
61
|
),
|
|
69
|
-
tasks: Type.Array(taskItemSchema, { description: "Tasks to run in parallel"
|
|
62
|
+
tasks: Type.Array(taskItemSchema, { description: "Tasks to run in parallel" }),
|
|
70
63
|
});
|
|
71
64
|
|
|
72
65
|
export type TaskParams = Static<typeof taskSchema>;
|
package/src/task/worktree.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import * as fs from "node:fs/promises";
|
|
3
2
|
import * as os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
4
|
+
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { $ } from "bun";
|
|
6
6
|
|
|
7
7
|
export interface WorktreeBaseline {
|
|
@@ -56,7 +56,7 @@ export async function captureBaseline(repoRoot: string): Promise<WorktreeBaselin
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
async function writeTempPatchFile(patch: string): Promise<string> {
|
|
59
|
-
const tempPath = path.join(os.tmpdir(), `omp-task-patch-${
|
|
59
|
+
const tempPath = path.join(os.tmpdir(), `omp-task-patch-${Snowflake.next()}.patch`);
|
|
60
60
|
await Bun.write(tempPath, patch);
|
|
61
61
|
return tempPath;
|
|
62
62
|
}
|
|
@@ -119,7 +119,7 @@ async function listUntracked(cwd: string): Promise<string[]> {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
export async function captureDeltaPatch(worktreeDir: string, baseline: WorktreeBaseline): Promise<string> {
|
|
122
|
-
const tempIndex = path.join(os.tmpdir(), `omp-task-index-${
|
|
122
|
+
const tempIndex = path.join(os.tmpdir(), `omp-task-index-${Snowflake.next()}`);
|
|
123
123
|
try {
|
|
124
124
|
await $`git read-tree HEAD`.cwd(worktreeDir).env({
|
|
125
125
|
GIT_INDEX_FILE: tempIndex,
|
package/src/tools/ask.ts
CHANGED
|
@@ -16,14 +16,13 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
18
18
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
19
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
19
|
+
import { TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
20
20
|
import { Type } from "@sinclair/typebox";
|
|
21
21
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
22
22
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
23
23
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
24
24
|
import askDescription from "../prompts/tools/ask.md" with { type: "text" };
|
|
25
25
|
import { renderStatusLine } from "../tui";
|
|
26
|
-
import { detectNotificationProtocol, isNotificationSuppressed, sendNotification } from "../utils/terminal-notify";
|
|
27
26
|
import type { ToolSession } from ".";
|
|
28
27
|
import { ToolUIKit } from "./render-utils";
|
|
29
28
|
|
|
@@ -268,13 +267,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
268
267
|
|
|
269
268
|
/** Send terminal notification when ask tool is waiting for input */
|
|
270
269
|
private sendAskNotification(): void {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const method = this.session.settings.get("ask.notification");
|
|
270
|
+
const method = this.session.settings.get("ask.notify");
|
|
274
271
|
if (method === "off") return;
|
|
275
|
-
|
|
276
|
-
const protocol = method === "auto" ? detectNotificationProtocol() : method;
|
|
277
|
-
sendNotification(protocol, "Waiting for input");
|
|
272
|
+
TERMINAL.sendNotification("Waiting for input");
|
|
278
273
|
}
|
|
279
274
|
|
|
280
275
|
public async execute(
|
package/src/tools/fetch.ts
CHANGED
|
@@ -385,7 +385,7 @@ function parseFeedToMarkdown(content: string, maxItems = 10): string {
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
/**
|
|
388
|
-
* Render HTML to markdown using native
|
|
388
|
+
* Render HTML to markdown using native, jina, trafilatura, lynx (in order of preference)
|
|
389
389
|
*/
|
|
390
390
|
async function renderHtmlToText(
|
|
391
391
|
url: string,
|
|
@@ -438,7 +438,7 @@ async function renderHtmlToText(
|
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
// Fall back to native
|
|
441
|
+
// Fall back to native converter (fastest, no network/subprocess)
|
|
442
442
|
try {
|
|
443
443
|
const content = await htmlToMarkdown(html, { cleanContent: true });
|
|
444
444
|
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { getEnvApiKey, StringEnum } from "@oh-my-pi/pi-ai";
|
|
4
|
+
import { $env, ptree, Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { type Static, Type } from "@sinclair/typebox";
|
|
6
|
-
import { nanoid } from "nanoid";
|
|
7
6
|
import type { ModelRegistry } from "../config/model-registry";
|
|
8
7
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
9
8
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
@@ -409,7 +408,7 @@ async function findImageApiKey(modelRegistry?: ModelRegistry): Promise<ImageApiK
|
|
|
409
408
|
if (preferredImageProvider === "gemini") {
|
|
410
409
|
const geminiKey = getEnvApiKey("google");
|
|
411
410
|
if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
|
|
412
|
-
const googleKey =
|
|
411
|
+
const googleKey = $env.GOOGLE_API_KEY;
|
|
413
412
|
if (googleKey) return { provider: "gemini", apiKey: googleKey };
|
|
414
413
|
// Fall through to auto-detect if preferred provider key not found
|
|
415
414
|
} else if (preferredImageProvider === "openrouter") {
|
|
@@ -430,7 +429,7 @@ async function findImageApiKey(modelRegistry?: ModelRegistry): Promise<ImageApiK
|
|
|
430
429
|
const geminiKey = getEnvApiKey("google");
|
|
431
430
|
if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
|
|
432
431
|
|
|
433
|
-
const googleKey =
|
|
432
|
+
const googleKey = $env.GOOGLE_API_KEY;
|
|
434
433
|
if (googleKey) return { provider: "gemini", apiKey: googleKey };
|
|
435
434
|
|
|
436
435
|
return null;
|
|
@@ -487,7 +486,7 @@ function getExtensionForMime(mimeType: string): string {
|
|
|
487
486
|
|
|
488
487
|
async function saveImageToTemp(image: InlineImageData): Promise<string> {
|
|
489
488
|
const ext = getExtensionForMime(image.mimeType);
|
|
490
|
-
const filename = `omp-image-${
|
|
489
|
+
const filename = `omp-image-${Snowflake.next()}.${ext}`;
|
|
491
490
|
const filepath = path.join(os.tmpdir(), filename);
|
|
492
491
|
await Bun.write(filepath, Buffer.from(image.data, "base64"));
|
|
493
492
|
return filepath;
|
package/src/tools/grep.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as nodePath from "node:path";
|
|
2
2
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
|
|
4
|
-
import { grep
|
|
4
|
+
import { grep } from "@oh-my-pi/pi-natives";
|
|
5
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -124,10 +124,10 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
124
124
|
const effectiveOutputMode = "content";
|
|
125
125
|
const effectiveLimit = normalizedLimit ?? DEFAULT_MATCH_LIMIT;
|
|
126
126
|
|
|
127
|
-
// Run
|
|
128
|
-
let result: Awaited<ReturnType<typeof
|
|
127
|
+
// Run grep
|
|
128
|
+
let result: Awaited<ReturnType<typeof grep>>;
|
|
129
129
|
try {
|
|
130
|
-
result = await
|
|
130
|
+
result = await grep({
|
|
131
131
|
pattern: normalizedPattern,
|
|
132
132
|
path: searchPath,
|
|
133
133
|
glob: glob?.trim() || undefined,
|
|
@@ -150,7 +150,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
const formatPath = (filePath: string): string => {
|
|
153
|
-
//
|
|
153
|
+
// returns paths starting with / (the virtual root)
|
|
154
154
|
const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
|
155
155
|
if (isDirectory) {
|
|
156
156
|
return cleanPath.replace(/\\/g, "/");
|
package/src/tools/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $env, logger } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
4
4
|
import type { Settings } from "../config/settings";
|
|
5
5
|
import type { Skill } from "../extensibility/skills";
|
|
@@ -127,6 +127,8 @@ export interface ToolSession {
|
|
|
127
127
|
outputSchema?: unknown;
|
|
128
128
|
/** Whether to include the submit_result tool by default */
|
|
129
129
|
requireSubmitResultTool?: boolean;
|
|
130
|
+
/** Task recursion depth (0 = top-level, 1 = first child, etc.) */
|
|
131
|
+
taskDepth?: number;
|
|
130
132
|
/** Get session file */
|
|
131
133
|
getSessionFile: () => string | null;
|
|
132
134
|
/** Get session ID */
|
|
@@ -192,7 +194,7 @@ export type ToolName = keyof typeof BUILTIN_TOOLS;
|
|
|
192
194
|
export type PythonToolMode = "ipy-only" | "bash-only" | "both";
|
|
193
195
|
|
|
194
196
|
/**
|
|
195
|
-
* Parse
|
|
197
|
+
* Parse PI_PY environment variable to determine Python tool mode.
|
|
196
198
|
* Returns null if not set or invalid.
|
|
197
199
|
*
|
|
198
200
|
* Values:
|
|
@@ -201,7 +203,7 @@ export type PythonToolMode = "ipy-only" | "bash-only" | "both";
|
|
|
201
203
|
* - "mix" or "both" → both
|
|
202
204
|
*/
|
|
203
205
|
function getPythonModeFromEnv(): PythonToolMode | null {
|
|
204
|
-
const value =
|
|
206
|
+
const value = $env.PI_PY?.toLowerCase();
|
|
205
207
|
if (!value) return null;
|
|
206
208
|
|
|
207
209
|
switch (value) {
|
|
@@ -238,7 +240,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
238
240
|
pythonMode !== "bash-only" &&
|
|
239
241
|
(requestedTools === undefined || requestedTools.includes("python"));
|
|
240
242
|
const isTestEnv = process.env.BUN_ENV === "test" || process.env.NODE_ENV === "test";
|
|
241
|
-
const skipPythonWarm = isTestEnv ||
|
|
243
|
+
const skipPythonWarm = isTestEnv || $env.PI_PYTHON_SKIP_CHECK === "1";
|
|
242
244
|
if (shouldCheckPython) {
|
|
243
245
|
const availability = await checkPythonKernelAvailability(session.cwd);
|
|
244
246
|
time("createTools:pythonCheck");
|
|
@@ -287,6 +289,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
287
289
|
if (name === "lsp") return session.settings.get("lsp.enabled");
|
|
288
290
|
if (name === "calc") return session.settings.get("calc.enabled");
|
|
289
291
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
292
|
+
if (name === "task") {
|
|
293
|
+
const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
294
|
+
const currentDepth = session.taskDepth ?? 0;
|
|
295
|
+
return maxDepth < 0 || currentDepth < maxDepth;
|
|
296
|
+
}
|
|
290
297
|
return true;
|
|
291
298
|
};
|
|
292
299
|
if (includeSubmitResult && requestedTools && !requestedTools.includes("submit_result")) {
|
|
@@ -317,7 +324,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
317
324
|
}),
|
|
318
325
|
);
|
|
319
326
|
time("createTools:afterFactories");
|
|
320
|
-
if (slowTools.length > 0 &&
|
|
327
|
+
if (slowTools.length > 0 && $env.PI_TIMING === "1") {
|
|
321
328
|
logger.debug("Tool factory timings", { slowTools });
|
|
322
329
|
}
|
|
323
330
|
const tools = results.filter(r => r.tool !== null).map(r => r.tool as Tool);
|
package/src/tools/read.ts
CHANGED
|
@@ -636,7 +636,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
636
636
|
const base64 = Buffer.from(buffer).toString("base64");
|
|
637
637
|
|
|
638
638
|
if (this.autoResizeImages) {
|
|
639
|
-
// Resize image if needed - catch errors from
|
|
639
|
+
// Resize image if needed - catch errors from Photon
|
|
640
640
|
try {
|
|
641
641
|
const resized = await resizeImage({ type: "image", data: base64, mimeType });
|
|
642
642
|
const dimensionNote = formatDimensionNote(resized);
|