@oh-my-pi/pi-coding-agent 16.2.6 → 16.2.7
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 +23 -0
- package/dist/cli.js +2656 -2651
- package/dist/types/cli/bench-cli.d.ts +3 -3
- package/dist/types/commands/bench.d.ts +1 -1
- package/dist/types/config/service-tier.d.ts +39 -24
- package/dist/types/config/settings-schema.d.ts +36 -36
- package/dist/types/mcp/config-writer.d.ts +48 -0
- package/dist/types/mcp/types.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +33 -13
- package/dist/types/session/messages.d.ts +1 -1
- package/dist/types/session/session-context.d.ts +2 -2
- package/dist/types/session/session-entries.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +2 -2
- package/dist/types/system-prompt.test.d.ts +1 -0
- package/dist/types/task/executor.d.ts +6 -6
- package/dist/types/task/types.d.ts +6 -0
- package/dist/types/task/worktree.d.ts +32 -6
- package/dist/types/tiny/title-client.d.ts +5 -1
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/utils/git.d.ts +17 -0
- package/package.json +12 -12
- package/src/cli/bench-cli.ts +19 -12
- package/src/cli/tiny-models-cli.ts +18 -4
- package/src/commands/bench.ts +3 -3
- package/src/config/mcp-schema.json +10 -1
- package/src/config/service-tier.ts +85 -56
- package/src/config/settings-schema.ts +42 -36
- package/src/config/settings.ts +47 -0
- package/src/eval/agent-bridge.ts +4 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/main.ts +1 -1
- package/src/mcp/config-writer.ts +121 -0
- package/src/mcp/config.ts +10 -6
- package/src/mcp/types.ts +3 -0
- package/src/modes/components/extensions/extension-dashboard.ts +46 -0
- package/src/modes/components/extensions/state-manager.ts +24 -3
- package/src/modes/controllers/event-controller.ts +7 -0
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/sdk.ts +12 -11
- package/src/session/agent-session.ts +186 -76
- package/src/session/messages.ts +1 -1
- package/src/session/session-context.ts +4 -4
- package/src/session/session-entries.ts +2 -2
- package/src/session/session-manager.ts +9 -2
- package/src/slash-commands/builtin-registry.ts +2 -10
- package/src/system-prompt.test.ts +158 -0
- package/src/system-prompt.ts +69 -26
- package/src/task/executor.ts +23 -16
- package/src/task/index.ts +7 -5
- package/src/task/isolation-runner.ts +15 -1
- package/src/task/types.ts +6 -0
- package/src/task/worktree.ts +219 -38
- package/src/tiny/title-client.ts +19 -13
- package/src/tools/index.ts +3 -3
- package/src/tools/irc.ts +9 -3
- package/src/tools/read.ts +28 -28
- package/src/utils/file-mentions.ts +10 -1
- package/src/utils/git.ts +38 -0
- package/src/web/search/providers/duckduckgo.ts +17 -3
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
interface ProbeRunResult {
|
|
7
|
+
elapsedMs: number;
|
|
8
|
+
childElapsedMs: number;
|
|
9
|
+
cached: unknown;
|
|
10
|
+
count: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function runProbeScenario(options: {
|
|
14
|
+
runs: number;
|
|
15
|
+
sleepSeconds?: number;
|
|
16
|
+
holdStdoutOpen?: boolean;
|
|
17
|
+
descendantHoldsStdout?: boolean;
|
|
18
|
+
validOutput?: string;
|
|
19
|
+
}): Promise<ProbeRunResult> {
|
|
20
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-gpu-probe-"));
|
|
21
|
+
try {
|
|
22
|
+
const binDir = path.join(tempRoot, "bin");
|
|
23
|
+
const cacheRoot = path.join(tempRoot, "cache");
|
|
24
|
+
const probeCountPath = path.join(tempRoot, "probe-count");
|
|
25
|
+
await fs.mkdir(binDir, { recursive: true });
|
|
26
|
+
await fs.mkdir(path.join(cacheRoot, "omp"), { recursive: true });
|
|
27
|
+
const lspciPath = path.join(binDir, "lspci");
|
|
28
|
+
await Bun.write(
|
|
29
|
+
lspciPath,
|
|
30
|
+
'#!/usr/bin/env sh\nprintf x >> "$OMP_GPU_PROBE_COUNT"\nif [ -n "$OMP_GPU_PROBE_VALID_OUTPUT" ]; then printf "%s\\n" "$OMP_GPU_PROBE_VALID_OUTPUT"; fi\nif [ "$OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & exit 0; fi\nif [ "$OMP_GPU_PROBE_HOLD_STDOUT_OPEN" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & wait "$!"; fi\nif [ -n "$OMP_GPU_PROBE_SLEEP" ]; then exec sleep "$OMP_GPU_PROBE_SLEEP"; fi\nexit 0\n',
|
|
31
|
+
);
|
|
32
|
+
await fs.chmod(lspciPath, 0o755);
|
|
33
|
+
|
|
34
|
+
const scenarioPath = path.join(tempRoot, "scenario.ts");
|
|
35
|
+
await Bun.write(
|
|
36
|
+
scenarioPath,
|
|
37
|
+
`import { getGpuCachePath, refreshDirsFromEnv } from ${JSON.stringify(path.resolve(import.meta.dir, "../../utils/src/index.ts"))};
|
|
38
|
+
import { buildSystemPrompt } from ${JSON.stringify(path.join(import.meta.dir, "system-prompt.ts"))};
|
|
39
|
+
|
|
40
|
+
refreshDirsFromEnv();
|
|
41
|
+
const buildOptions = {
|
|
42
|
+
contextFiles: [],
|
|
43
|
+
skills: [],
|
|
44
|
+
toolNames: [],
|
|
45
|
+
workspaceTree: {
|
|
46
|
+
rootPath: process.cwd(),
|
|
47
|
+
rendered: "",
|
|
48
|
+
truncated: false,
|
|
49
|
+
totalLines: 0,
|
|
50
|
+
agentsMdFiles: [],
|
|
51
|
+
},
|
|
52
|
+
activeRepoContext: null,
|
|
53
|
+
};
|
|
54
|
+
const startedAt = performance.now();
|
|
55
|
+
for (let index = 0; index < Number(process.env.OMP_GPU_PROBE_RUNS ?? "1"); index += 1) {
|
|
56
|
+
await buildSystemPrompt(buildOptions);
|
|
57
|
+
}
|
|
58
|
+
const cacheFile = Bun.file(getGpuCachePath());
|
|
59
|
+
const cached = await cacheFile.exists() ? await cacheFile.json() : null;
|
|
60
|
+
const countFile = Bun.file(process.env.OMP_GPU_PROBE_COUNT ?? "");
|
|
61
|
+
const count = await countFile.exists() ? (await countFile.text()).length : 0;
|
|
62
|
+
console.log(JSON.stringify({ elapsedMs: Math.round(performance.now() - startedAt), cached, count }));
|
|
63
|
+
`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const env: Record<string, string | undefined> = {
|
|
67
|
+
...process.env,
|
|
68
|
+
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
69
|
+
XDG_CACHE_HOME: cacheRoot,
|
|
70
|
+
OMP_GPU_PROBE_COUNT: probeCountPath,
|
|
71
|
+
OMP_GPU_PROBE_RUNS: String(options.runs),
|
|
72
|
+
};
|
|
73
|
+
// Strip inherited dirs-resolver overrides so XDG_CACHE_HOME above wins and
|
|
74
|
+
// the test cannot touch the developer/CI profile's real gpu_cache.json.
|
|
75
|
+
for (const key of ["PI_CODING_AGENT_DIR", "OMP_PROFILE", "PI_PROFILE", "PI_CONFIG_DIR"]) {
|
|
76
|
+
delete env[key];
|
|
77
|
+
}
|
|
78
|
+
if (options.sleepSeconds === undefined) {
|
|
79
|
+
delete env.OMP_GPU_PROBE_SLEEP;
|
|
80
|
+
} else {
|
|
81
|
+
env.OMP_GPU_PROBE_SLEEP = String(options.sleepSeconds);
|
|
82
|
+
}
|
|
83
|
+
if (options.holdStdoutOpen) {
|
|
84
|
+
env.OMP_GPU_PROBE_HOLD_STDOUT_OPEN = "true";
|
|
85
|
+
} else {
|
|
86
|
+
delete env.OMP_GPU_PROBE_HOLD_STDOUT_OPEN;
|
|
87
|
+
}
|
|
88
|
+
if (options.descendantHoldsStdout) {
|
|
89
|
+
env.OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT = "true";
|
|
90
|
+
} else {
|
|
91
|
+
delete env.OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT;
|
|
92
|
+
}
|
|
93
|
+
if (options.validOutput !== undefined) {
|
|
94
|
+
env.OMP_GPU_PROBE_VALID_OUTPUT = options.validOutput;
|
|
95
|
+
} else {
|
|
96
|
+
delete env.OMP_GPU_PROBE_VALID_OUTPUT;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const childStartedAt = performance.now();
|
|
100
|
+
const child = Bun.spawn([process.execPath, scenarioPath], { stdout: "pipe", stderr: "pipe", env });
|
|
101
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
102
|
+
new Response(child.stdout).text(),
|
|
103
|
+
new Response(child.stderr).text(),
|
|
104
|
+
child.exited,
|
|
105
|
+
]);
|
|
106
|
+
const childElapsedMs = Math.round(performance.now() - childStartedAt);
|
|
107
|
+
if (exitCode !== 0) {
|
|
108
|
+
throw new Error(`GPU probe scenario failed with exit ${exitCode}: ${stderr}`);
|
|
109
|
+
}
|
|
110
|
+
return { ...JSON.parse(stdout.trim()), childElapsedMs };
|
|
111
|
+
} finally {
|
|
112
|
+
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe.skipIf(process.platform !== "linux")("system prompt GPU probe", () => {
|
|
117
|
+
it("caches empty GPU probe results", async () => {
|
|
118
|
+
const result = await runProbeScenario({ runs: 2 });
|
|
119
|
+
|
|
120
|
+
expect(result.cached).toEqual({ gpu: null });
|
|
121
|
+
expect(result.count).toBe(1);
|
|
122
|
+
}, 15_000);
|
|
123
|
+
|
|
124
|
+
it("kills the GPU probe at the prep deadline", async () => {
|
|
125
|
+
const result = await runProbeScenario({ runs: 1, sleepSeconds: 7, holdStdoutOpen: true });
|
|
126
|
+
|
|
127
|
+
expect(result.cached).toEqual({ gpu: null });
|
|
128
|
+
expect(result.elapsedMs).toBeLessThan(6500);
|
|
129
|
+
// Codex#3838: the child process MUST exit shortly after the deadline,
|
|
130
|
+
// not linger until a descendant holding stdout (sleep 7) exits on its own.
|
|
131
|
+
expect(result.childElapsedMs).toBeLessThan(6500);
|
|
132
|
+
}, 15_000);
|
|
133
|
+
|
|
134
|
+
it("does not wait on stdout held by a descendant after a successful probe", async () => {
|
|
135
|
+
const result = await runProbeScenario({ runs: 1, sleepSeconds: 3, descendantHoldsStdout: true });
|
|
136
|
+
|
|
137
|
+
expect(result.cached).toEqual({ gpu: null });
|
|
138
|
+
// Probe exits 0 immediately but leaves a backgrounded sleep holding the stdout
|
|
139
|
+
// pipe. The success path MUST bound the drain wait, not block until sleep exits.
|
|
140
|
+
expect(result.elapsedMs).toBeLessThan(2000);
|
|
141
|
+
expect(result.childElapsedMs).toBeLessThan(2000);
|
|
142
|
+
}, 15_000);
|
|
143
|
+
|
|
144
|
+
it("keeps probe output captured before a descendant delays EOF", async () => {
|
|
145
|
+
const result = await runProbeScenario({
|
|
146
|
+
runs: 1,
|
|
147
|
+
sleepSeconds: 3,
|
|
148
|
+
descendantHoldsStdout: true,
|
|
149
|
+
validOutput: "00:02.0 VGA compatible controller: NVIDIA TestGPU",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Probe exited 0 with valid output before bg sleep held stdout open.
|
|
153
|
+
// Captured stdout MUST be cached, not discarded as if the probe failed.
|
|
154
|
+
expect(result.cached).toEqual({ gpu: "02.0 VGA compatible controller: NVIDIA TestGPU" });
|
|
155
|
+
expect(result.elapsedMs).toBeLessThan(2000);
|
|
156
|
+
expect(result.childElapsedMs).toBeLessThan(2000);
|
|
157
|
+
}, 15_000);
|
|
158
|
+
});
|
package/src/system-prompt.ts
CHANGED
|
@@ -7,7 +7,6 @@ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
|
7
7
|
import type { ToolExample, TSchema } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import { renderToolInventory } from "@oh-my-pi/pi-ai/dialect";
|
|
9
9
|
import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
10
|
-
import { $ } from "bun";
|
|
11
10
|
import { contextFileCapability } from "./capability/context-file";
|
|
12
11
|
import { systemPromptCapability } from "./capability/system-prompt";
|
|
13
12
|
import { findConfigFile } from "./config";
|
|
@@ -112,21 +111,60 @@ function parseWmicTable(output: string, header: string): string | null {
|
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
const SYSTEM_PROMPT_PREP_TIMEOUT_MS = 5000;
|
|
114
|
+
/** Kept below prep timeout so timed-out probes can still write the null cache before fallback. */
|
|
115
|
+
const GPU_PROBE_TIMEOUT_MS = SYSTEM_PROMPT_PREP_TIMEOUT_MS - 500;
|
|
116
|
+
/** Drop stdout from a probe descendant that inherited the pipe after the probe exited. */
|
|
117
|
+
const GPU_PROBE_STDOUT_DRAIN_MS = 250;
|
|
118
|
+
|
|
119
|
+
async function runGpuProbe(cmd: string[]): Promise<string | null> {
|
|
120
|
+
try {
|
|
121
|
+
const proc = Bun.spawn({
|
|
122
|
+
cmd,
|
|
123
|
+
stdout: "pipe",
|
|
124
|
+
stderr: "ignore",
|
|
125
|
+
stdin: "ignore",
|
|
126
|
+
timeout: GPU_PROBE_TIMEOUT_MS,
|
|
127
|
+
// SIGKILL so a probe ignoring SIGTERM (PATH wrapper, wedged WMI) still
|
|
128
|
+
// dies at the deadline and lets getCachedGpu reach the null-cache write.
|
|
129
|
+
killSignal: "SIGKILL",
|
|
130
|
+
});
|
|
131
|
+
const stdoutReader = proc.stdout.getReader();
|
|
132
|
+
let stdout = "";
|
|
133
|
+
const decoder = new TextDecoder();
|
|
134
|
+
const stdoutDone = (async () => {
|
|
135
|
+
while (true) {
|
|
136
|
+
const chunk = await stdoutReader.read();
|
|
137
|
+
if (chunk.done) break;
|
|
138
|
+
stdout += decoder.decode(chunk.value, { stream: true });
|
|
139
|
+
}
|
|
140
|
+
stdout += decoder.decode();
|
|
141
|
+
})();
|
|
142
|
+
const exitCode = await proc.exited;
|
|
143
|
+
// Even on exit 0, a probe wrapper can leave a descendant holding stdout open.
|
|
144
|
+
// Bound the EOF wait so getCachedGpu cannot outlive the probe in either path;
|
|
145
|
+
// keep whatever bytes the reader already captured before cancelling.
|
|
146
|
+
const drained = await Promise.race([
|
|
147
|
+
stdoutDone.then(() => "ok" as const).catch(() => "err" as const),
|
|
148
|
+
Bun.sleep(GPU_PROBE_STDOUT_DRAIN_MS).then(() => "timeout" as const),
|
|
149
|
+
]);
|
|
150
|
+
if (drained !== "ok") {
|
|
151
|
+
await stdoutReader.cancel().catch(() => undefined);
|
|
152
|
+
await stdoutDone.catch(() => undefined);
|
|
153
|
+
}
|
|
154
|
+
return exitCode === 0 ? stdout : null;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
115
159
|
|
|
116
160
|
async function getGpuModel(): Promise<string | null> {
|
|
117
161
|
switch (process.platform) {
|
|
118
162
|
case "win32": {
|
|
119
|
-
const output = await
|
|
120
|
-
.quiet()
|
|
121
|
-
.text()
|
|
122
|
-
.catch(() => null);
|
|
163
|
+
const output = await runGpuProbe(["wmic", "path", "win32_VideoController", "get", "name"]);
|
|
123
164
|
return output ? parseWmicTable(output, "Name") : null;
|
|
124
165
|
}
|
|
125
166
|
case "linux": {
|
|
126
|
-
const output = await
|
|
127
|
-
.quiet()
|
|
128
|
-
.text()
|
|
129
|
-
.catch(() => null);
|
|
167
|
+
const output = await runGpuProbe(["lspci"]);
|
|
130
168
|
if (!output) return null;
|
|
131
169
|
const gpus: Array<{ name: string; priority: number }> = [];
|
|
132
170
|
for (const line of output.split("\n")) {
|
|
@@ -176,20 +214,20 @@ function getTerminalName(): string | undefined {
|
|
|
176
214
|
return term ?? undefined;
|
|
177
215
|
}
|
|
178
216
|
|
|
179
|
-
/** Cached
|
|
217
|
+
/** Cached GPU probe result. */
|
|
180
218
|
interface GpuCache {
|
|
181
|
-
gpu: string;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function getSystemInfoCachePath(): string {
|
|
185
|
-
return getGpuCachePath();
|
|
219
|
+
gpu: string | null;
|
|
186
220
|
}
|
|
187
221
|
|
|
188
222
|
async function loadGpuCache(): Promise<GpuCache | null> {
|
|
189
223
|
try {
|
|
190
|
-
const cachePath =
|
|
224
|
+
const cachePath = getGpuCachePath();
|
|
191
225
|
const content = await Bun.file(cachePath).json();
|
|
192
|
-
|
|
226
|
+
if (content && typeof content === "object" && "gpu" in content) {
|
|
227
|
+
const gpu = content.gpu;
|
|
228
|
+
return { gpu: typeof gpu === "string" ? gpu : null };
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
193
231
|
} catch {
|
|
194
232
|
return null;
|
|
195
233
|
}
|
|
@@ -197,7 +235,7 @@ async function loadGpuCache(): Promise<GpuCache | null> {
|
|
|
197
235
|
|
|
198
236
|
async function saveGpuCache(info: GpuCache): Promise<void> {
|
|
199
237
|
try {
|
|
200
|
-
const cachePath =
|
|
238
|
+
const cachePath = getGpuCachePath();
|
|
201
239
|
await Bun.write(cachePath, JSON.stringify(info, null, "\t"));
|
|
202
240
|
} catch {
|
|
203
241
|
// Silently ignore cache write failures
|
|
@@ -206,15 +244,12 @@ async function saveGpuCache(info: GpuCache): Promise<void> {
|
|
|
206
244
|
|
|
207
245
|
async function getCachedGpu(): Promise<string | undefined> {
|
|
208
246
|
const cached = await logger.time("getCachedGpu:loadGpuCache", loadGpuCache);
|
|
209
|
-
if (cached) return cached.gpu;
|
|
247
|
+
if (cached) return cached.gpu ?? undefined;
|
|
210
248
|
const gpu = await logger.time("getCachedGpu:getGpuModel", getGpuModel);
|
|
211
|
-
|
|
212
|
-
await logger.time("getCachedGpu:saveGpuCache", saveGpuCache, { gpu });
|
|
213
|
-
}
|
|
249
|
+
await logger.time("getCachedGpu:saveGpuCache", saveGpuCache, { gpu });
|
|
214
250
|
return gpu ?? undefined;
|
|
215
251
|
}
|
|
216
|
-
|
|
217
|
-
const gpu = await getCachedGpu();
|
|
252
|
+
function getEnvironmentInfo(gpu: string | undefined): Array<{ label: string; value: string }> {
|
|
218
253
|
let cpuModel: string | undefined;
|
|
219
254
|
try {
|
|
220
255
|
cpuModel = os.cpus()[0]?.model;
|
|
@@ -500,9 +535,13 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
500
535
|
agentsMdFiles: [],
|
|
501
536
|
} satisfies WorkspaceTree,
|
|
502
537
|
activeRepoContext: null as ActiveRepoContext | null,
|
|
538
|
+
gpu: undefined as string | undefined,
|
|
503
539
|
};
|
|
504
540
|
|
|
505
|
-
const deadline =
|
|
541
|
+
const { promise: deadline, resolve: fireDeadline } = Promise.withResolvers<"__timeout__">();
|
|
542
|
+
const deadlineTimer = setTimeout(() => fireDeadline("__timeout__"), SYSTEM_PROMPT_PREP_TIMEOUT_MS);
|
|
543
|
+
// Unref so a fast prep does not hold a one-shot CLI alive waiting for this timer.
|
|
544
|
+
deadlineTimer.unref();
|
|
506
545
|
const timedOut: string[] = [];
|
|
507
546
|
const failed: Array<{ name: string; error: unknown }> = [];
|
|
508
547
|
|
|
@@ -566,6 +605,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
566
605
|
providedActiveRepoContext !== undefined
|
|
567
606
|
? Promise.resolve(providedActiveRepoContext)
|
|
568
607
|
: logger.time("resolveActiveRepoContext", () => resolveActiveRepoContext(resolvedCwd));
|
|
608
|
+
const gpuPromise = logger.time("getCachedGpu", getCachedGpu);
|
|
569
609
|
|
|
570
610
|
const [
|
|
571
611
|
resolvedCustomPrompt,
|
|
@@ -575,6 +615,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
575
615
|
skills,
|
|
576
616
|
workspaceTree,
|
|
577
617
|
activeRepoContext,
|
|
618
|
+
gpu,
|
|
578
619
|
] = await Promise.all([
|
|
579
620
|
withDeadline(
|
|
580
621
|
"customPrompt",
|
|
@@ -597,7 +638,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
597
638
|
withDeadline("loadSkills", skillsPromise, prepDefaults.skills),
|
|
598
639
|
withDeadline("buildWorkspaceTree", workspaceTreePromise, prepDefaults.workspaceTree),
|
|
599
640
|
withDeadline("resolveActiveRepoContext", activeRepoContextPromise, prepDefaults.activeRepoContext),
|
|
641
|
+
withDeadline("getCachedGpu", gpuPromise, prepDefaults.gpu),
|
|
600
642
|
]);
|
|
643
|
+
clearTimeout(deadlineTimer);
|
|
601
644
|
const agentsMdFiles = Array.from(new Set(workspaceTree.agentsMdFiles)).sort().slice(0, AGENTS_MD_LIMIT);
|
|
602
645
|
|
|
603
646
|
if (timedOut.length > 0) {
|
|
@@ -675,7 +718,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
675
718
|
];
|
|
676
719
|
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
677
720
|
|
|
678
|
-
const environment =
|
|
721
|
+
const environment = getEnvironmentInfo(gpu);
|
|
679
722
|
const data = {
|
|
680
723
|
systemPromptCustomization: effectiveSystemPromptCustomization,
|
|
681
724
|
customPrompt: resolvedCustomPrompt,
|
package/src/task/executor.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
|
-
import type { Api, Model,
|
|
10
|
+
import type { Api, Model, ServiceTierByFamily, Usage } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import type { Rule } from "../capability/rule";
|
|
13
13
|
import { ModelRegistry } from "../config/model-registry";
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
resolveModelOverrideWithAuthFallback,
|
|
19
19
|
} from "../config/model-resolver";
|
|
20
20
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
21
|
-
import { resolveSubagentServiceTier } from "../config/service-tier";
|
|
21
|
+
import { buildServiceTierByFamily, resolveSubagentServiceTier } from "../config/service-tier";
|
|
22
22
|
import { Settings } from "../config/settings";
|
|
23
23
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
24
24
|
import type { ToolPathWithSource } from "../extensibility/custom-tools";
|
|
@@ -344,12 +344,12 @@ export interface ExecutorOptions {
|
|
|
344
344
|
modelRegistry?: ModelRegistry;
|
|
345
345
|
settings?: Settings;
|
|
346
346
|
/**
|
|
347
|
-
* Parent session's live
|
|
348
|
-
* subagent whose `
|
|
347
|
+
* Parent session's live per-family service tiers, the source of truth for a
|
|
348
|
+
* subagent whose `tier.subagent` is `"inherit"`. `null` = the parent
|
|
349
349
|
* explicitly has no tier (e.g. `/fast off`); omitted = no live session, so
|
|
350
|
-
* inherit falls back to the configured `
|
|
350
|
+
* inherit falls back to the subagent's configured `tier.*` settings.
|
|
351
351
|
*/
|
|
352
|
-
parentServiceTier?:
|
|
352
|
+
parentServiceTier?: ServiceTierByFamily | null;
|
|
353
353
|
/** Override local:// protocol options so subagent shares parent's local:// root */
|
|
354
354
|
localProtocolOptions?: LocalProtocolOptions;
|
|
355
355
|
/**
|
|
@@ -739,21 +739,28 @@ export function createMCPProxyTools(mcpManager: MCPManager): CustomTool[] {
|
|
|
739
739
|
export function createSubagentSettings(
|
|
740
740
|
baseSettings: Settings,
|
|
741
741
|
overrides?: Partial<Record<SettingPath, unknown>>,
|
|
742
|
-
inheritedServiceTier?:
|
|
742
|
+
inheritedServiceTier?: ServiceTierByFamily | null,
|
|
743
743
|
): Settings {
|
|
744
744
|
const snapshot: Partial<Record<SettingPath, unknown>> = {};
|
|
745
745
|
for (const key of Object.keys(SETTINGS_SCHEMA) as SettingPath[]) {
|
|
746
746
|
snapshot[key] = baseSettings.get(key);
|
|
747
747
|
}
|
|
748
|
-
// Resolve the subagent's
|
|
749
|
-
// match the parent's live
|
|
750
|
-
// configured
|
|
751
|
-
// createAgentSession's
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
748
|
+
// Resolve the subagent's per-family tiers from `tier.subagent` ("inherit" =
|
|
749
|
+
// match the parent's live tiers when a live session supplied them, else the
|
|
750
|
+
// subagent's own configured tier.* settings). The result is stamped back onto
|
|
751
|
+
// the snapshot so createAgentSession's tier.* reads pick it up.
|
|
752
|
+
const inheritedTiers =
|
|
753
|
+
inheritedServiceTier === undefined
|
|
754
|
+
? buildServiceTierByFamily(
|
|
755
|
+
baseSettings.get("tier.openai"),
|
|
756
|
+
baseSettings.get("tier.anthropic"),
|
|
757
|
+
baseSettings.get("tier.google"),
|
|
758
|
+
)
|
|
759
|
+
: (inheritedServiceTier ?? {});
|
|
760
|
+
const subagentTiers = resolveSubagentServiceTier(baseSettings.get("tier.subagent"), inheritedTiers);
|
|
761
|
+
snapshot["tier.openai"] = subagentTiers.openai ?? "none";
|
|
762
|
+
snapshot["tier.anthropic"] = subagentTiers.anthropic ?? "none";
|
|
763
|
+
snapshot["tier.google"] = subagentTiers.google ?? "none";
|
|
757
764
|
return Settings.isolated({
|
|
758
765
|
...snapshot,
|
|
759
766
|
"async.enabled": false,
|
package/src/task/index.ts
CHANGED
|
@@ -1296,11 +1296,13 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1296
1296
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
1297
1297
|
parentEvalSessionId,
|
|
1298
1298
|
parentAgentId: this.session.getAgentId?.() ?? MAIN_AGENT_ID,
|
|
1299
|
-
// Live source of truth for `
|
|
1300
|
-
//
|
|
1301
|
-
// none, e.g. /fast off); otherwise leave undefined so inherit
|
|
1302
|
-
// back to the configured
|
|
1303
|
-
parentServiceTier: this.session.
|
|
1299
|
+
// Live source of truth for `tier.subagent: inherit`. When the session
|
|
1300
|
+
// exposes a tier accessor, pass the per-family map or null (null =
|
|
1301
|
+
// explicit none, e.g. /fast off); otherwise leave undefined so inherit
|
|
1302
|
+
// falls back to the subagent's configured tier.* settings.
|
|
1303
|
+
parentServiceTier: this.session.getServiceTierByFamily
|
|
1304
|
+
? (this.session.getServiceTierByFamily() ?? null)
|
|
1305
|
+
: undefined,
|
|
1304
1306
|
};
|
|
1305
1307
|
|
|
1306
1308
|
const runTask = async (): Promise<SingleResult> => {
|
|
@@ -154,6 +154,7 @@ export async function runIsolatedSubprocess(opts: IsolatedRunOptions): Promise<S
|
|
|
154
154
|
return {
|
|
155
155
|
...result,
|
|
156
156
|
branchName: commitResult?.branchName,
|
|
157
|
+
branchBaseSha: commitResult?.baseSha,
|
|
157
158
|
nestedPatches: commitResult?.nestedPatches,
|
|
158
159
|
};
|
|
159
160
|
} catch (mergeErr) {
|
|
@@ -222,6 +223,14 @@ export async function mergeIsolatedChanges(opts: IsolationMergeOptions): Promise
|
|
|
222
223
|
const { result, repoRoot, mergeMode } = opts;
|
|
223
224
|
try {
|
|
224
225
|
if (mergeMode === "branch") {
|
|
226
|
+
if (!result.branchName && result.exitCode === 0 && !result.aborted && result.error) {
|
|
227
|
+
return {
|
|
228
|
+
summary: `\n\n<system-notification>Branch merge failed before a task branch could be created: ${result.error}\nTask outputs are preserved but changes were not applied.</system-notification>`,
|
|
229
|
+
changesApplied: false,
|
|
230
|
+
hadAnyChanges: false,
|
|
231
|
+
mergedBranchForNestedPatches: false,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
225
234
|
const canApplyNestedOnly =
|
|
226
235
|
!result.branchName && result.exitCode === 0 && !result.aborted && (result.nestedPatches?.length ?? 0) > 0;
|
|
227
236
|
if (!result.branchName || result.exitCode !== 0 || result.aborted) {
|
|
@@ -235,7 +244,12 @@ export async function mergeIsolatedChanges(opts: IsolationMergeOptions): Promise
|
|
|
235
244
|
};
|
|
236
245
|
}
|
|
237
246
|
const mergeResult = await mergeTaskBranches(repoRoot, [
|
|
238
|
-
{
|
|
247
|
+
{
|
|
248
|
+
branchName: result.branchName,
|
|
249
|
+
taskId: result.id,
|
|
250
|
+
description: result.description,
|
|
251
|
+
baseSha: result.branchBaseSha,
|
|
252
|
+
},
|
|
239
253
|
]);
|
|
240
254
|
const mergedBranchForNestedPatches = mergeResult.merged.includes(result.branchName);
|
|
241
255
|
const changesApplied = mergeResult.failed.length === 0;
|
package/src/task/types.ts
CHANGED
|
@@ -383,6 +383,12 @@ export interface SingleResult {
|
|
|
383
383
|
patchPath?: string;
|
|
384
384
|
/** Branch name for isolated branch-mode output */
|
|
385
385
|
branchName?: string;
|
|
386
|
+
/**
|
|
387
|
+
* Baseline commit SHA the task branch was created from. Passed to
|
|
388
|
+
* `mergeTaskBranches` so cherry-pick uses the inclusive range
|
|
389
|
+
* `branchBaseSha..branchName` and preserves every agent commit's message.
|
|
390
|
+
*/
|
|
391
|
+
branchBaseSha?: string;
|
|
386
392
|
/** Nested repo patches to apply after parent merge */
|
|
387
393
|
nestedPatches?: NestedRepoPatch[];
|
|
388
394
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|