pi-subagents 0.51.0 → 0.52.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 +41 -0
- package/agents/oracle.md +3 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +2 -2
- package/agents/worker.md +2 -1
- package/docs/agents.md +8 -3
- package/docs/extension-api.md +1 -1
- package/docs/models.md +2 -0
- package/docs/observability.md +37 -1
- package/docs/workflows.md +2 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +1 -0
- package/skills/pi-subagents/references/constraints-and-recipes.md +6 -2
- package/skills/pi-subagents/references/execution-controls.md +12 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -0
- package/src/agents/agent-management.ts +24 -6
- package/src/agents/agents.ts +37 -11
- package/src/agents/skills.ts +1 -1
- package/src/api/preflight.ts +5 -2
- package/src/extension/index.ts +1 -0
- package/src/extension/public-execution.ts +3 -1
- package/src/intercom/native-supervisor-channel.ts +2 -1
- package/src/missions/workflow-state.ts +2 -2
- package/src/runs/background/async-execution.ts +19 -9
- package/src/runs/background/async-resume.ts +19 -2
- package/src/runs/background/async-retention.ts +2 -4
- package/src/runs/background/completion-dedupe.ts +5 -1
- package/src/runs/background/completion-replay.ts +22 -12
- package/src/runs/background/fleet-view.ts +68 -19
- package/src/runs/background/inspect-rpc.ts +443 -0
- package/src/runs/background/notify.ts +28 -4
- package/src/runs/background/result-watcher.ts +44 -0
- package/src/runs/background/resume-guidance.ts +8 -5
- package/src/runs/background/scheduled-runs.ts +2 -0
- package/src/runs/background/subagent-runner.ts +3 -2
- package/src/runs/background/subagent-wait.ts +1 -1
- package/src/runs/background/wait-subscriptions.ts +1 -1
- package/src/runs/foreground/execution.ts +1 -1
- package/src/runs/foreground/subagent-executor.ts +239 -76
- package/src/runs/foreground/workflow-detach-reconcile.ts +194 -0
- package/src/runs/shared/acceptance.ts +4 -4
- package/src/runs/shared/external-job-bridge.ts +0 -6
- package/src/runs/shared/model-fallback.ts +77 -34
- package/src/runs/shared/session-lease.ts +0 -6
- package/src/runs/shared/worktree.ts +2 -1
- package/src/shared/types.ts +11 -0
- package/src/slash/slash-bridge.ts +2 -1
- package/src/slash/slash-commands.ts +18 -0
- package/src/watchdog/change-signature.ts +1 -1
- package/src/watchdog/lsp-diagnostics.ts +1 -0
- package/src/workflows/chat-progress.ts +1 -1
- package/src/workflows/scripted-workflow.ts +13 -2
- package/agents/gpt-pro.md +0 -17
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
4
|
+
import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
|
|
5
|
+
import { getSingleResultOutput, readStatus } from "../../shared/utils.ts";
|
|
6
|
+
import {
|
|
7
|
+
DIRS,
|
|
8
|
+
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
9
|
+
type AsyncStatus,
|
|
10
|
+
type IntercomEventBus,
|
|
11
|
+
type SingleResult,
|
|
12
|
+
type SubagentState,
|
|
13
|
+
} from "../../shared/types.ts";
|
|
14
|
+
import { updateActiveRunIndex } from "../background/active-run-index.ts";
|
|
15
|
+
import { resultFilePath, writeAsyncResultFile } from "../background/result-files.ts";
|
|
16
|
+
|
|
17
|
+
function cloneWorkflowStatus(status: AsyncStatus): AsyncStatus {
|
|
18
|
+
return {
|
|
19
|
+
...status,
|
|
20
|
+
steps: status.steps?.map((step) => ({ ...step })),
|
|
21
|
+
workflow: status.workflow ? { ...status.workflow, trace: [...(status.workflow.trace ?? [])] } : status.workflow,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function childSucceeded(result: Pick<SingleResult, "exitCode" | "error">): boolean {
|
|
26
|
+
return result.exitCode === 0 && !result.error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function applyDetachedChildToPausedWorkflow(
|
|
30
|
+
status: AsyncStatus,
|
|
31
|
+
input: { childRunId: string; result: Pick<SingleResult, "exitCode" | "error" | "sessionFile">; workflowKey?: string },
|
|
32
|
+
): AsyncStatus | undefined {
|
|
33
|
+
if (status.mode !== "workflow" || status.state !== "paused") return undefined;
|
|
34
|
+
const next = cloneWorkflowStatus(status);
|
|
35
|
+
const step = next.steps?.find((candidate) => candidate.runId === input.childRunId)
|
|
36
|
+
?? (input.workflowKey ? next.steps?.find((candidate) => candidate.workflowKey === input.workflowKey) : undefined);
|
|
37
|
+
if (!step) return undefined;
|
|
38
|
+
const succeeded = childSucceeded(input.result);
|
|
39
|
+
const updatedAt = Date.now();
|
|
40
|
+
step.status = succeeded ? "completed" : "failed";
|
|
41
|
+
step.endedAt = updatedAt;
|
|
42
|
+
delete step.activityState;
|
|
43
|
+
delete step.currentTool;
|
|
44
|
+
delete step.currentToolStartedAt;
|
|
45
|
+
if (input.result.sessionFile) step.sessionFile = input.result.sessionFile;
|
|
46
|
+
if (succeeded) delete step.error;
|
|
47
|
+
else if (input.result.error) step.error = input.result.error;
|
|
48
|
+
next.lastUpdate = updatedAt;
|
|
49
|
+
const promoted = promotePausedWorkflowIfSettled(next);
|
|
50
|
+
if (promoted?.state === "failed" && input.result.error) promoted.error = input.result.error;
|
|
51
|
+
return promoted ?? next;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function promotePausedWorkflowIfSettled(status: AsyncStatus): AsyncStatus | undefined {
|
|
55
|
+
if (status.mode !== "workflow" || status.state !== "paused") return undefined;
|
|
56
|
+
const next = cloneWorkflowStatus(status);
|
|
57
|
+
const stillOpen = next.steps?.some((candidate) =>
|
|
58
|
+
candidate.status === "running"
|
|
59
|
+
|| (candidate.status === "paused" && candidate.activityState === "needs_attention")
|
|
60
|
+
) === true;
|
|
61
|
+
if (stillOpen || !next.steps?.length) return undefined;
|
|
62
|
+
const failed = next.steps.some((candidate) => candidate.status === "failed");
|
|
63
|
+
const updatedAt = Date.now();
|
|
64
|
+
next.lastUpdate = updatedAt;
|
|
65
|
+
next.state = failed ? "failed" : "complete";
|
|
66
|
+
next.endedAt = updatedAt;
|
|
67
|
+
delete next.activityState;
|
|
68
|
+
if (!failed) delete next.error;
|
|
69
|
+
return next;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function workflowResultChildren(status: AsyncStatus, childRunId: string, result: SingleResult, existingResults: unknown): unknown {
|
|
73
|
+
const output = getSingleResultOutput(result);
|
|
74
|
+
if (Array.isArray(existingResults)) {
|
|
75
|
+
return existingResults.map((entry) => {
|
|
76
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
|
|
77
|
+
const child = entry as Record<string, unknown>;
|
|
78
|
+
if (child.runId !== childRunId) return child;
|
|
79
|
+
return {
|
|
80
|
+
...child,
|
|
81
|
+
success: childSucceeded(result),
|
|
82
|
+
output,
|
|
83
|
+
outputState: output.trim() ? "present" : "absent",
|
|
84
|
+
detached: undefined,
|
|
85
|
+
...(result.error ? { error: result.error } : {}),
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return status.steps?.map((step) => ({
|
|
90
|
+
workflowKey: step.workflowKey,
|
|
91
|
+
agent: step.agent,
|
|
92
|
+
runId: step.runId,
|
|
93
|
+
success: step.status === "completed" || step.status === "complete",
|
|
94
|
+
output: step.runId === childRunId ? output : "",
|
|
95
|
+
outputState: step.runId === childRunId && output.trim() ? "present" : "absent",
|
|
96
|
+
...(step.error ? { error: step.error } : {}),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function publishedWorkflowResult(status: AsyncStatus, childRunId: string, result: SingleResult, asyncDir: string, existing?: Record<string, unknown>): Record<string, unknown> {
|
|
101
|
+
const sessionId = status.sessionId ?? (typeof existing?.sessionId === "string" ? existing.sessionId : undefined);
|
|
102
|
+
const summary = status.state === "complete"
|
|
103
|
+
? `Workflow completed after detached child ${childRunId} finished.`
|
|
104
|
+
: status.error ?? (typeof existing?.summary === "string" ? existing.summary : undefined);
|
|
105
|
+
return {
|
|
106
|
+
...(existing ?? {}),
|
|
107
|
+
id: status.runId,
|
|
108
|
+
runId: status.runId,
|
|
109
|
+
toolCallId: status.toolCallId,
|
|
110
|
+
agent: "workflow",
|
|
111
|
+
mode: "workflow",
|
|
112
|
+
success: status.state === "complete",
|
|
113
|
+
state: status.state,
|
|
114
|
+
summary,
|
|
115
|
+
error: status.error,
|
|
116
|
+
activityState: status.activityState,
|
|
117
|
+
endedAt: status.endedAt,
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
results: workflowResultChildren(status, childRunId, result, existing?.results),
|
|
120
|
+
workflow: status.workflow,
|
|
121
|
+
asyncDir,
|
|
122
|
+
cwd: status.cwd,
|
|
123
|
+
sessionId,
|
|
124
|
+
completionOwnerId: status.completionOwnerId,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function reconcileDetachedWorkflowChildCompletion(input: {
|
|
129
|
+
state: SubagentState;
|
|
130
|
+
workflowRunId: string;
|
|
131
|
+
childRunId: string;
|
|
132
|
+
result: SingleResult;
|
|
133
|
+
events?: IntercomEventBus;
|
|
134
|
+
workflowKey?: string;
|
|
135
|
+
}): boolean {
|
|
136
|
+
const job = input.state.asyncJobs.get(input.workflowRunId);
|
|
137
|
+
const asyncDir = job?.asyncDir ?? path.join(DIRS.async, input.workflowRunId);
|
|
138
|
+
const status = readStatus(asyncDir);
|
|
139
|
+
if (!status) return false;
|
|
140
|
+
const next = applyDetachedChildToPausedWorkflow(status, {
|
|
141
|
+
childRunId: input.childRunId,
|
|
142
|
+
result: input.result,
|
|
143
|
+
workflowKey: input.workflowKey,
|
|
144
|
+
});
|
|
145
|
+
if (!next) return false;
|
|
146
|
+
writeAtomicJson(path.join(asyncDir, "status.json"), next);
|
|
147
|
+
updateActiveRunIndex(asyncDir, next.state, next.toolCallId);
|
|
148
|
+
if (job) {
|
|
149
|
+
job.status = next.state;
|
|
150
|
+
job.updatedAt = next.lastUpdate;
|
|
151
|
+
job.activityState = next.activityState;
|
|
152
|
+
job.steps = next.steps?.map((step, index) => ({ ...step, index }));
|
|
153
|
+
job.workflow = next.workflow;
|
|
154
|
+
}
|
|
155
|
+
const resultPath = resultFilePath(DIRS.results, input.workflowRunId);
|
|
156
|
+
let existing: Record<string, unknown> | undefined;
|
|
157
|
+
try {
|
|
158
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
|
|
159
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) existing = parsed as Record<string, unknown>;
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
162
|
+
}
|
|
163
|
+
const published = publishedWorkflowResult(next, input.childRunId, input.result, asyncDir, existing);
|
|
164
|
+
writeAsyncResultFile(resultPath, published);
|
|
165
|
+
if (next.state === "complete" || next.state === "failed") {
|
|
166
|
+
try {
|
|
167
|
+
fs.appendFileSync(path.join(asyncDir, "events.jsonl"), `${JSON.stringify({
|
|
168
|
+
ts: Date.now(),
|
|
169
|
+
runId: input.workflowRunId,
|
|
170
|
+
type: "subagent.workflow.completed",
|
|
171
|
+
state: next.state,
|
|
172
|
+
...(next.error ? { error: next.error } : {}),
|
|
173
|
+
reconciledFromDetachedChild: input.childRunId,
|
|
174
|
+
})}\n`, "utf-8");
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (!isStorageCapacityError(error)) throw error;
|
|
177
|
+
}
|
|
178
|
+
input.events?.emit(SUBAGENT_ASYNC_COMPLETE_EVENT, {
|
|
179
|
+
id: input.workflowRunId,
|
|
180
|
+
runId: input.workflowRunId,
|
|
181
|
+
source: "async",
|
|
182
|
+
mode: "workflow",
|
|
183
|
+
agent: "workflow",
|
|
184
|
+
success: next.state === "complete",
|
|
185
|
+
state: next.state,
|
|
186
|
+
summary: typeof published.summary === "string" ? published.summary : (next.state === "complete" ? "Workflow completed." : next.error),
|
|
187
|
+
sessionId: next.sessionId,
|
|
188
|
+
completionOwnerId: next.completionOwnerId,
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
triggerTurn: true,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
@@ -946,7 +946,7 @@ function reportEvidenceStatus(report: AcceptanceReport, kind: AcceptanceEvidence
|
|
|
946
946
|
}
|
|
947
947
|
|
|
948
948
|
function checkNoStagedFiles(cwd: string): AcceptanceRuntimeCheck {
|
|
949
|
-
const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8" });
|
|
949
|
+
const result = spawnSync("git", ["status", "--short"], { cwd, encoding: "utf-8", windowsHide: true });
|
|
950
950
|
if (result.status !== 0) {
|
|
951
951
|
return { id: "no-staged-files", status: "not-applicable", message: "git status unavailable; no staged-files check skipped" };
|
|
952
952
|
}
|
|
@@ -1053,11 +1053,11 @@ interface VerifyWorkspaceState {
|
|
|
1053
1053
|
}
|
|
1054
1054
|
|
|
1055
1055
|
function readVerifyWorkspaceState(cwd: string): VerifyWorkspaceState | undefined {
|
|
1056
|
-
const repo = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8" });
|
|
1056
|
+
const repo = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", windowsHide: true });
|
|
1057
1057
|
if (repo.status !== 0 || !repo.stdout.trim()) return undefined;
|
|
1058
1058
|
const repoRoot = fs.realpathSync(repo.stdout.trim());
|
|
1059
|
-
const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf-8" });
|
|
1060
|
-
const diff = spawnSync("git", ["diff", "--binary", "--full-index", "HEAD", "--"], { cwd: repoRoot, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
|
|
1059
|
+
const head = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf-8", windowsHide: true });
|
|
1060
|
+
const diff = spawnSync("git", ["diff", "--binary", "--full-index", "HEAD", "--"], { cwd: repoRoot, encoding: "utf-8", maxBuffer: 50 * 1024 * 1024, windowsHide: true });
|
|
1061
1061
|
if (head.status !== 0 || diff.status !== 0 || !head.stdout.trim()) return undefined;
|
|
1062
1062
|
return {
|
|
1063
1063
|
kind: "git-tracked",
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
2
|
import * as fs from "node:fs";
|
|
4
3
|
import * as os from "node:os";
|
|
5
4
|
import * as path from "node:path";
|
|
@@ -134,11 +133,6 @@ function processStartIdentity(pid: number): string | undefined {
|
|
|
134
133
|
return undefined;
|
|
135
134
|
}
|
|
136
135
|
}
|
|
137
|
-
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
138
|
-
const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf-8" });
|
|
139
|
-
const started = result.status === 0 ? result.stdout.trim() : "";
|
|
140
|
-
return started ? `${process.platform}:${started}` : undefined;
|
|
141
|
-
}
|
|
142
136
|
return undefined;
|
|
143
137
|
}
|
|
144
138
|
|
|
@@ -41,7 +41,7 @@ export function normalizeParentModel(model: unknown): ParentModel | undefined {
|
|
|
41
41
|
/**
|
|
42
42
|
* Normalize a model id or provider segment for fuzzy comparison: case-fold,
|
|
43
43
|
* treat dots/underscores as dashes (so `4.5` matches `4-5`), and collapse
|
|
44
|
-
* repeated separators.
|
|
44
|
+
* repeated separators.
|
|
45
45
|
*/
|
|
46
46
|
export function normalizeModelSegment(segment: string): string {
|
|
47
47
|
return segment
|
|
@@ -58,7 +58,7 @@ function isPlausibleDateStamp(year: string, month: string, day: string): boolean
|
|
|
58
58
|
return yyyy >= 1900 && yyyy <= 2099 && mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** Drop a trailing date stamp (`-20251001` or `-2025-10-01`) so dated and undated ids match.
|
|
61
|
+
/** Drop a trailing date stamp (`-20251001` or `-2025-10-01`) so dated and undated ids match. */
|
|
62
62
|
function stripTrailingDateStamp(segment: string): string {
|
|
63
63
|
const dashed = /^(.*)-(\d{4})-(\d{2})-(\d{2})$/.exec(segment);
|
|
64
64
|
if (dashed && isPlausibleDateStamp(dashed[2]!, dashed[3]!, dashed[4]!)) return dashed[1]!;
|
|
@@ -67,21 +67,65 @@ function stripTrailingDateStamp(segment: string): string {
|
|
|
67
67
|
return segment;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function isRegisteredProvider(provider: string, availableModels: AvailableModelInfo[]): boolean {
|
|
71
|
+
const normalized = normalizeModelSegment(provider);
|
|
72
|
+
return availableModels.some((entry) => normalizeModelSegment(entry.provider) === normalized);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Split `provider/id` only when the first path segment is a registered provider.
|
|
77
|
+
* Hugging Face-style `owner/name` ids therefore stay intact unless `owner` is
|
|
78
|
+
* itself a provider in the active registry. `:` and `.` keep the same rule.
|
|
79
|
+
*/
|
|
80
|
+
function splitQualifiedModelQuery(
|
|
81
|
+
baseModel: string,
|
|
82
|
+
availableModels: AvailableModelInfo[],
|
|
83
|
+
): { queryProvider?: string; queryIdRaw: string } {
|
|
84
|
+
const slashIdx = baseModel.indexOf("/");
|
|
85
|
+
if (slashIdx !== -1) {
|
|
86
|
+
const providerPart = baseModel.slice(0, slashIdx);
|
|
87
|
+
if (isRegisteredProvider(providerPart, availableModels)) {
|
|
88
|
+
return { queryProvider: normalizeModelSegment(providerPart), queryIdRaw: baseModel.slice(slashIdx + 1) };
|
|
89
|
+
}
|
|
90
|
+
return { queryIdRaw: baseModel };
|
|
91
|
+
}
|
|
92
|
+
const providerSeparators = [":", "."];
|
|
93
|
+
for (const separator of providerSeparators) {
|
|
94
|
+
const separatorIdx = baseModel.indexOf(separator);
|
|
95
|
+
if (separatorIdx <= 0) continue;
|
|
96
|
+
const providerPart = baseModel.slice(0, separatorIdx);
|
|
97
|
+
if (!isRegisteredProvider(providerPart, availableModels)) continue;
|
|
98
|
+
return { queryProvider: normalizeModelSegment(providerPart), queryIdRaw: baseModel.slice(separatorIdx + 1) };
|
|
99
|
+
}
|
|
100
|
+
return { queryIdRaw: baseModel };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveExactIdMatches(
|
|
104
|
+
baseModel: string,
|
|
105
|
+
availableModels: AvailableModelInfo[],
|
|
106
|
+
preferredProvider?: string,
|
|
107
|
+
): string | undefined {
|
|
108
|
+
const exactMatches = availableModels.filter((entry) => entry.id === baseModel);
|
|
109
|
+
if (preferredProvider) {
|
|
110
|
+
const preferredMatch = exactMatches.find((entry) => entry.provider === preferredProvider);
|
|
111
|
+
if (preferredMatch) return preferredMatch.fullId;
|
|
112
|
+
}
|
|
113
|
+
if (exactMatches.length === 1) return exactMatches[0]!.fullId;
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
70
117
|
function resolveBaseModelCandidate(
|
|
71
118
|
baseModel: string,
|
|
72
119
|
availableModels: AvailableModelInfo[],
|
|
73
120
|
preferredProvider?: string,
|
|
74
121
|
): string | undefined {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (preferredMatch) return preferredMatch.fullId;
|
|
83
|
-
}
|
|
84
|
-
if (exactMatches.length === 1) return exactMatches[0]!.fullId;
|
|
122
|
+
const exact = availableModels.find((entry) => entry.fullId === baseModel);
|
|
123
|
+
if (exact) return exact.fullId;
|
|
124
|
+
|
|
125
|
+
const { queryProvider } = splitQualifiedModelQuery(baseModel, availableModels);
|
|
126
|
+
if (queryProvider === undefined) {
|
|
127
|
+
const exactId = resolveExactIdMatches(baseModel, availableModels, preferredProvider);
|
|
128
|
+
if (exactId) return exactId;
|
|
85
129
|
}
|
|
86
130
|
|
|
87
131
|
return fuzzyResolveModel(baseModel, availableModels, preferredProvider);
|
|
@@ -90,35 +134,20 @@ function resolveBaseModelCandidate(
|
|
|
90
134
|
/**
|
|
91
135
|
* Fuzzy-resolve a base model id (thinking suffix already stripped) against the
|
|
92
136
|
* registry, tolerating separator, case, and optional date-stamp differences so
|
|
93
|
-
* users do not have to spell provider/model exactly. A
|
|
137
|
+
* users do not have to spell provider/model exactly. A slash is a provider
|
|
138
|
+
* prefix only when that prefix is a registered provider; otherwise the whole
|
|
139
|
+
* string is the model id (Hugging Face `owner/name`). A qualified provider
|
|
94
140
|
* query only matches within the named provider — this never silently switches
|
|
95
141
|
* providers for security/cost-sensitive configs. Returns the matched `fullId`,
|
|
96
142
|
* or `undefined` when there is no match or the match is ambiguous across
|
|
97
|
-
* providers (and no `preferredProvider` disambiguates).
|
|
143
|
+
* providers (and no `preferredProvider` disambiguates).
|
|
98
144
|
*/
|
|
99
145
|
export function fuzzyResolveModel(
|
|
100
146
|
baseModel: string,
|
|
101
147
|
availableModels: AvailableModelInfo[],
|
|
102
148
|
preferredProvider?: string,
|
|
103
149
|
): string | undefined {
|
|
104
|
-
|
|
105
|
-
let queryIdRaw = baseModel;
|
|
106
|
-
const slashIdx = baseModel.indexOf("/");
|
|
107
|
-
if (slashIdx !== -1) {
|
|
108
|
-
queryProvider = normalizeModelSegment(baseModel.slice(0, slashIdx));
|
|
109
|
-
queryIdRaw = baseModel.slice(slashIdx + 1);
|
|
110
|
-
} else {
|
|
111
|
-
const providerSeparators = [":", "."];
|
|
112
|
-
for (const separator of providerSeparators) {
|
|
113
|
-
const separatorIdx = baseModel.indexOf(separator);
|
|
114
|
-
if (separatorIdx <= 0) continue;
|
|
115
|
-
const providerPart = normalizeModelSegment(baseModel.slice(0, separatorIdx));
|
|
116
|
-
if (!availableModels.some((entry) => normalizeModelSegment(entry.provider) === providerPart)) continue;
|
|
117
|
-
queryProvider = providerPart;
|
|
118
|
-
queryIdRaw = baseModel.slice(separatorIdx + 1);
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
150
|
+
const { queryProvider, queryIdRaw } = splitQualifiedModelQuery(baseModel, availableModels);
|
|
122
151
|
const queryId = normalizeModelSegment(queryIdRaw);
|
|
123
152
|
const queryIdNoDate = stripTrailingDateStamp(queryId);
|
|
124
153
|
|
|
@@ -143,7 +172,7 @@ export function fuzzyResolveModel(
|
|
|
143
172
|
* thinking suffix). Exact registry matches win; fuzzy normalization
|
|
144
173
|
* (separator/case/date-stamp via {@link fuzzyResolveModel}) is a fallback so
|
|
145
174
|
* spelling differences still resolve. Never switches providers for a qualified
|
|
146
|
-
* query.
|
|
175
|
+
* query.
|
|
147
176
|
*/
|
|
148
177
|
export function resolveModelCandidate(
|
|
149
178
|
model: string | undefined,
|
|
@@ -272,6 +301,18 @@ export interface BuildModelCandidatesOptions {
|
|
|
272
301
|
/** Fallback models warn by default and throw when strict scope enforcement is enabled. */
|
|
273
302
|
scope?: ModelScopeConfig;
|
|
274
303
|
onWarn?: (violation: ModelScopeViolation) => void;
|
|
304
|
+
/** The primary model came from the running parent session, not configuration. */
|
|
305
|
+
primaryModelFromParent?: boolean;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function inheritsParentModel(
|
|
309
|
+
explicitModel: string | boolean | undefined,
|
|
310
|
+
agentModel: string | boolean | undefined,
|
|
311
|
+
parentModel: ParentModel | undefined,
|
|
312
|
+
): boolean {
|
|
313
|
+
const requestedModel = explicitModel ?? agentModel;
|
|
314
|
+
const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : "";
|
|
315
|
+
return Boolean(parentModel && (!trimmed || trimmed === INHERIT_MODEL));
|
|
275
316
|
}
|
|
276
317
|
|
|
277
318
|
export function buildModelCandidates(
|
|
@@ -289,7 +330,9 @@ export function buildModelCandidates(
|
|
|
289
330
|
if (!raw) continue;
|
|
290
331
|
const model = raw.trim();
|
|
291
332
|
const normalized = index === 0
|
|
292
|
-
?
|
|
333
|
+
? options?.primaryModelFromParent
|
|
334
|
+
? model
|
|
335
|
+
: resolveRequiredSubagentModelCandidate(model, availableModels, preferredProvider)
|
|
293
336
|
: resolveSubagentModelCandidate(model, availableModels, preferredProvider);
|
|
294
337
|
if (!normalized) {
|
|
295
338
|
console.warn(`[pi-subagents] Skipping fallback model '${model}' because it is unavailable in this environment.`);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
2
|
import * as fs from "node:fs";
|
|
4
3
|
import * as os from "node:os";
|
|
@@ -79,11 +78,6 @@ function getProcessStartIdentity(pid: number): string | undefined {
|
|
|
79
78
|
return undefined;
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
|
-
if (process.platform === "darwin" || process.platform === "freebsd") {
|
|
83
|
-
const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf-8" });
|
|
84
|
-
const started = result.status === 0 ? result.stdout.trim() : "";
|
|
85
|
-
return started ? `${process.platform}:${started}` : undefined;
|
|
86
|
-
}
|
|
87
81
|
return undefined;
|
|
88
82
|
}
|
|
89
83
|
|
|
@@ -115,7 +115,7 @@ interface RepoState {
|
|
|
115
115
|
const DEFAULT_WORKTREE_SETUP_HOOK_TIMEOUT_MS = 30000;
|
|
116
116
|
|
|
117
117
|
function runGit(cwd: string, args: string[]): GitResult {
|
|
118
|
-
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
|
|
118
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", windowsHide: true });
|
|
119
119
|
return {
|
|
120
120
|
stdout: result.stdout ?? "",
|
|
121
121
|
stderr: result.stderr ?? "",
|
|
@@ -338,6 +338,7 @@ function runWorktreeSetupHook(
|
|
|
338
338
|
input: WorktreeSetupHookInput,
|
|
339
339
|
): string[] {
|
|
340
340
|
const result = spawnSync(hook.hookPath, [], {
|
|
341
|
+
windowsHide: true,
|
|
341
342
|
cwd: input.worktreePath,
|
|
342
343
|
encoding: "utf-8",
|
|
343
344
|
input: JSON.stringify(input),
|
package/src/shared/types.ts
CHANGED
|
@@ -409,6 +409,12 @@ export type ProcessTerminalV1 =
|
|
|
409
409
|
diagnostic?: string;
|
|
410
410
|
});
|
|
411
411
|
|
|
412
|
+
/** Identifies the durable schedule that launched a run, so its completion is attributable. */
|
|
413
|
+
export interface ScheduleOrigin {
|
|
414
|
+
id: string;
|
|
415
|
+
name?: string;
|
|
416
|
+
}
|
|
417
|
+
|
|
412
418
|
export type SteeringActionState = "delivered" | "scheduled" | "pending" | "partial" | "recovered" | "failed";
|
|
413
419
|
export type SteeringTargetState = "scheduled" | "pending" | "routed" | "queued" | "delivered" | "late" | "failed" | "recovered";
|
|
414
420
|
|
|
@@ -502,6 +508,7 @@ export interface SteeringRecoveryDescriptor {
|
|
|
502
508
|
sessionFile?: string;
|
|
503
509
|
cwd: string;
|
|
504
510
|
model?: string;
|
|
511
|
+
modelOverrideFromParent?: boolean;
|
|
505
512
|
fallbackModels?: string[];
|
|
506
513
|
thinking?: string;
|
|
507
514
|
tools?: string[];
|
|
@@ -1408,6 +1415,8 @@ export interface AsyncStatus {
|
|
|
1408
1415
|
workflow?: Details["workflow"];
|
|
1409
1416
|
parentWorkflowRunId?: string;
|
|
1410
1417
|
workflowKey?: string;
|
|
1418
|
+
/** Set when a durable schedule launched this run, so completions can name their origin. */
|
|
1419
|
+
scheduleOrigin?: ScheduleOrigin;
|
|
1411
1420
|
steps?: Array<{
|
|
1412
1421
|
agent: string;
|
|
1413
1422
|
runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
|
|
@@ -1842,6 +1851,8 @@ export interface RunSyncOptions {
|
|
|
1842
1851
|
nestedRoute?: NestedRouteInfo;
|
|
1843
1852
|
/** Override the agent's default model (format: "provider/id" or just "id") */
|
|
1844
1853
|
modelOverride?: string;
|
|
1854
|
+
/** The override came from the running parent session, not configuration. */
|
|
1855
|
+
modelOverrideFromParent?: boolean;
|
|
1845
1856
|
/** LLM intent arbiter for the completion mutation guard (rescues read-only review runs). */
|
|
1846
1857
|
llmIntentArbiter?: import("../runs/shared/llm-intent-arbiter.ts").TaskMutationArbiter;
|
|
1847
1858
|
/** Override the agent's default thinking level for this run */
|
|
@@ -39,6 +39,7 @@ interface EventBus {
|
|
|
39
39
|
|
|
40
40
|
interface SlashBridgeOptions {
|
|
41
41
|
events: EventBus;
|
|
42
|
+
asyncByDefault: boolean;
|
|
42
43
|
getContext: () => ExtensionContext | null;
|
|
43
44
|
execute: (
|
|
44
45
|
id: string,
|
|
@@ -79,7 +80,7 @@ export function registerSlashSubagentBridge(options: SlashBridgeOptions): {
|
|
|
79
80
|
const request = data as Partial<SlashSubagentRequest>;
|
|
80
81
|
if (typeof request.requestId !== "string" || !request.params) return;
|
|
81
82
|
const { requestId } = request as SlashSubagentRequest;
|
|
82
|
-
const normalized = normalizePublicSubagentExecution((request as SlashSubagentRequest).params);
|
|
83
|
+
const normalized = normalizePublicSubagentExecution((request as SlashSubagentRequest).params, { asyncByDefault: options.asyncByDefault });
|
|
83
84
|
if (!normalized.ok) {
|
|
84
85
|
options.events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
85
86
|
requestId,
|
|
@@ -18,6 +18,7 @@ import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts
|
|
|
18
18
|
import { findModelInfo, toModelInfo } from "../shared/model-info.ts";
|
|
19
19
|
import { formatTokens, shortenPath } from "../shared/formatters.ts";
|
|
20
20
|
import { listAsyncRuns, formatAsyncRunProgressLabel, type AsyncRunSummary } from "../runs/background/async-status.ts";
|
|
21
|
+
import { encodeInspectReply, handleInspectRpcArgs, INSPECT_WIDGET_KEY } from "../runs/background/inspect-rpc.ts";
|
|
21
22
|
import { listScheduledRunSummaries } from "../runs/background/scheduled-runs.ts";
|
|
22
23
|
import { SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
23
24
|
import type { SlashSubagentResponse, SlashSubagentUpdate } from "./slash-bridge.ts";
|
|
@@ -711,6 +712,23 @@ export function registerSlashCommands(
|
|
|
711
712
|
},
|
|
712
713
|
});
|
|
713
714
|
|
|
715
|
+
pi.registerCommand("subagents-inspect-rpc", {
|
|
716
|
+
description: "Host integration bridge: answer an async child inspection request with a correlated widget payload (no model turn)",
|
|
717
|
+
handler: async (args, ctx) => {
|
|
718
|
+
if (ctx.mode === "tui") {
|
|
719
|
+
ctx.ui.notify("Inspection replies are emitted only on RPC surfaces. Use /subagents or subagent({ action: \"status\", view: \"transcript\" }) interactively.", "info");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (!ctx.hasUI) return;
|
|
723
|
+
const reply = handleInspectRpcArgs(args, { state });
|
|
724
|
+
// Emit-then-retract in one handler: stdio delivers the two widget
|
|
725
|
+
// updates in order, the host buffers the payload by requestId, and
|
|
726
|
+
// the dedicated key never accumulates visible state.
|
|
727
|
+
ctx.ui.setWidget(INSPECT_WIDGET_KEY, encodeInspectReply(reply));
|
|
728
|
+
ctx.ui.setWidget(INSPECT_WIDGET_KEY, undefined);
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
|
|
714
732
|
pi.registerCommand("subagents-guide", {
|
|
715
733
|
description: "Show a packaged subagents guide topic",
|
|
716
734
|
getArgumentCompletions: (prefix) => prefix.includes(" ") ? null : SUBAGENT_GUIDE_TOPICS
|
|
@@ -38,7 +38,7 @@ export interface WatchdogRepoChangeSignature {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function git(cwd: string, args: string[]): string | undefined {
|
|
41
|
-
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
|
|
41
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024, windowsHide: true });
|
|
42
42
|
if (result.status !== 0) return undefined;
|
|
43
43
|
return result.stdout;
|
|
44
44
|
}
|
|
@@ -444,6 +444,7 @@ async function collectWithTypeScriptLanguageServer(input: {
|
|
|
444
444
|
}): Promise<WatchdogLspResult> {
|
|
445
445
|
const started = Date.now();
|
|
446
446
|
const child = spawn(input.command.command, input.command.args, {
|
|
447
|
+
windowsHide: true,
|
|
447
448
|
cwd: input.root,
|
|
448
449
|
stdio: "pipe",
|
|
449
450
|
env: { ...process.env, NO_COLOR: "1" },
|
|
@@ -26,7 +26,7 @@ interface ResolveWorkflowChatProgressInput {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function git(cwd: string, args: string[]): string | undefined {
|
|
29
|
-
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
|
|
29
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8", windowsHide: true });
|
|
30
30
|
if (result.status !== 0) return undefined;
|
|
31
31
|
const output = result.stdout.trim();
|
|
32
32
|
return output || undefined;
|
|
@@ -758,7 +758,18 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
758
758
|
const child = children.get(key);
|
|
759
759
|
return child ? [child] : [];
|
|
760
760
|
}) });
|
|
761
|
-
|
|
761
|
+
// Hosts use onTrace to persist a progress journal, and it is invoked from inside
|
|
762
|
+
// the run-promise handlers below. A throw here would reject the child promise the
|
|
763
|
+
// script is awaiting, so a single failed status write could mark a completed child
|
|
764
|
+
// failed and abort its siblings through Promise.all. Telemetry must not decide
|
|
765
|
+
// workflow outcomes, so a failing callback is reported and the run continues.
|
|
766
|
+
const traceChanged = () => {
|
|
767
|
+
try {
|
|
768
|
+
options.onTrace?.([...trace]);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
console.error("Workflow onTrace callback failed:", error);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
762
773
|
|
|
763
774
|
return await new Promise<WorkflowScriptResult>((resolve, reject) => {
|
|
764
775
|
const finish = (outcome: { value: unknown } | { error: Error & { workflowErrorKind?: unknown } }) => {
|
|
@@ -1012,7 +1023,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1012
1023
|
if (!admission) {
|
|
1013
1024
|
const seenKeys = new Set<string>();
|
|
1014
1025
|
const calls = (batch?.calls ?? [{ key, params }]).filter((call) => {
|
|
1015
|
-
if (seenKeys.has(call.key) || launches.has(call.key)
|
|
1026
|
+
if (seenKeys.has(call.key) || launches.has(call.key)) return false;
|
|
1016
1027
|
seenKeys.add(call.key);
|
|
1017
1028
|
return true;
|
|
1018
1029
|
});
|
package/agents/gpt-pro.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: gpt-pro
|
|
3
|
-
description: Surf GPT Pro advisory runner through the external-job provider bridge
|
|
4
|
-
runner:
|
|
5
|
-
type: external-job
|
|
6
|
-
provider: surf-oracle
|
|
7
|
-
async: true
|
|
8
|
-
systemPromptMode: replace
|
|
9
|
-
inheritProjectContext: false
|
|
10
|
-
inheritSkills: false
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
You are a read-only GPT Pro advisor reached through Surf Oracle.
|
|
14
|
-
|
|
15
|
-
Review the supplied task and context.
|
|
16
|
-
Return clear advice, risks, and recommended next steps.
|
|
17
|
-
Do not claim you edited files or ran local tools.
|