pi-crew 0.1.37 → 0.1.39
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/AGENTS.md +1 -1
- package/CHANGELOG.md +27 -0
- package/README.md +5 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/resource-formats.md +10 -8
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +6 -0
- package/index.ts +6 -6
- package/package.json +3 -3
- package/schema.json +2 -2
- package/src/agents/agent-serializer.ts +34 -34
- package/src/config/config.ts +8 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +18 -2
- package/src/extension/register.ts +11 -1
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +30 -6
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +12 -2
- package/src/extension/run-index.ts +12 -2
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +54 -14
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -179
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/status.ts +73 -73
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +54 -54
- package/src/observability/exporters/adapter.ts +24 -24
- package/src/observability/exporters/otlp-exporter.ts +65 -65
- package/src/observability/exporters/prometheus-exporter.ts +47 -47
- package/src/observability/metric-registry.ts +72 -72
- package/src/observability/metric-retention.ts +46 -46
- package/src/observability/metric-sink.ts +51 -51
- package/src/observability/metrics-primitives.ts +166 -166
- package/src/prompt/prompt-runtime.ts +68 -68
- package/src/runtime/agent-control.ts +64 -64
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crash-recovery.ts +56 -56
- package/src/runtime/crew-agent-records.ts +54 -9
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +36 -36
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +88 -88
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +80 -80
- package/src/runtime/live-agent-control.ts +87 -78
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +299 -299
- package/src/runtime/manifest-cache.ts +248 -212
- package/src/runtime/model-fallback.ts +261 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +99 -99
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +78 -78
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +59 -59
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +28 -28
- package/src/runtime/subagent-manager.ts +80 -12
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -106
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +1 -1
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +21 -21
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +122 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/mailbox.ts +67 -22
- package/src/state/state-store.ts +36 -5
- package/src/state/task-claims.ts +42 -42
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +27 -5
- package/src/teams/team-serializer.ts +38 -36
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +441 -441
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +5 -2
- package/src/ui/run-snapshot-cache.ts +19 -8
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +15 -1
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +84 -84
- package/src/utils/frontmatter.ts +36 -36
- package/src/utils/fs-watch.ts +31 -31
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +26 -26
- package/src/utils/paths.ts +3 -2
- package/src/utils/safe-paths.ts +34 -0
- package/src/utils/sleep.ts +32 -32
- package/src/utils/timings.ts +31 -31
- package/src/utils/visual.ts +159 -159
- package/src/workflows/discover-workflows.ts +30 -3
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
|
|
5
|
-
export interface AvailableModelInfo {
|
|
6
|
-
provider: string;
|
|
7
|
-
id: string;
|
|
8
|
-
fullId: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ModelAttemptSummary {
|
|
12
|
-
model: string;
|
|
13
|
-
success: boolean;
|
|
14
|
-
exitCode?: number | null;
|
|
15
|
-
error?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ModelLike {
|
|
19
|
-
provider?: unknown;
|
|
20
|
-
id?: unknown;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ModelRegistryLike {
|
|
24
|
-
getAvailable?: () => unknown[];
|
|
25
|
-
getAll?: () => unknown[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface PiSettingsLike {
|
|
29
|
-
defaultProvider?: unknown;
|
|
30
|
-
defaultModel?: unknown;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface PiModelsJsonLike {
|
|
34
|
-
providers?: unknown;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface PiProviderConfigLike {
|
|
38
|
-
models?: unknown;
|
|
39
|
-
modelOverrides?: unknown;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function modelInfoFromUnknown(value: unknown): AvailableModelInfo | undefined {
|
|
43
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
44
|
-
const record = value as ModelLike;
|
|
45
|
-
if (typeof record.provider !== "string" || typeof record.id !== "string") return undefined;
|
|
46
|
-
return { provider: record.provider, id: record.id, fullId: `${record.provider}/${record.id}` };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function availableModelInfosFromRegistry(registry: unknown): AvailableModelInfo[] | undefined {
|
|
50
|
-
if (!registry || typeof registry !== "object" || Array.isArray(registry)) return undefined;
|
|
51
|
-
const candidate = registry as ModelRegistryLike;
|
|
52
|
-
const raw = typeof candidate.getAvailable === "function" ? candidate.getAvailable() : typeof candidate.getAll === "function" ? candidate.getAll() : undefined;
|
|
53
|
-
if (!Array.isArray(raw)) return undefined;
|
|
54
|
-
return raw.map(modelInfoFromUnknown).filter((entry): entry is AvailableModelInfo => entry !== undefined);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function modelStringFromUnknown(model: unknown): string | undefined {
|
|
58
|
-
return modelInfoFromUnknown(model)?.fullId;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function uniqueModelInfos(models: AvailableModelInfo[]): AvailableModelInfo[] {
|
|
62
|
-
const seen = new Set<string>();
|
|
63
|
-
return models.filter((model) => {
|
|
64
|
-
if (seen.has(model.fullId)) return false;
|
|
65
|
-
seen.add(model.fullId);
|
|
66
|
-
return true;
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function readJsonObject(filePath: string): Record<string, unknown> | undefined {
|
|
71
|
-
try {
|
|
72
|
-
if (!fs.existsSync(filePath)) return undefined;
|
|
73
|
-
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
74
|
-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
|
|
75
|
-
} catch {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function piAgentDir(): string {
|
|
81
|
-
const envDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
82
|
-
if (envDir) {
|
|
83
|
-
if (envDir === "~") return os.homedir();
|
|
84
|
-
if (envDir.startsWith("~/")) return path.join(os.homedir(), envDir.slice(2));
|
|
85
|
-
return envDir;
|
|
86
|
-
}
|
|
87
|
-
return path.join(os.homedir(), ".pi", "agent");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function settingsModelInfo(settings: PiSettingsLike | undefined): AvailableModelInfo | undefined {
|
|
91
|
-
if (typeof settings?.defaultProvider !== "string" || typeof settings.defaultModel !== "string") return undefined;
|
|
92
|
-
return { provider: settings.defaultProvider, id: settings.defaultModel, fullId: `${settings.defaultProvider}/${settings.defaultModel}` };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function modelsJsonInfos(modelsJson: PiModelsJsonLike | undefined): AvailableModelInfo[] {
|
|
96
|
-
if (!modelsJson?.providers || typeof modelsJson.providers !== "object" || Array.isArray(modelsJson.providers)) return [];
|
|
97
|
-
const infos: AvailableModelInfo[] = [];
|
|
98
|
-
for (const [provider, rawConfig] of Object.entries(modelsJson.providers as Record<string, unknown>)) {
|
|
99
|
-
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) continue;
|
|
100
|
-
const config = rawConfig as PiProviderConfigLike;
|
|
101
|
-
if (Array.isArray(config.models)) {
|
|
102
|
-
for (const rawModel of config.models) {
|
|
103
|
-
if (!rawModel || typeof rawModel !== "object" || Array.isArray(rawModel)) continue;
|
|
104
|
-
const id = (rawModel as { id?: unknown }).id;
|
|
105
|
-
if (typeof id === "string") infos.push({ provider, id, fullId: `${provider}/${id}` });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (config.modelOverrides && typeof config.modelOverrides === "object" && !Array.isArray(config.modelOverrides)) {
|
|
109
|
-
for (const id of Object.keys(config.modelOverrides)) infos.push({ provider, id, fullId: `${provider}/${id}` });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return infos;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function configuredModelInfosFromPiConfig(cwd?: string): AvailableModelInfo[] {
|
|
116
|
-
const agentDir = piAgentDir();
|
|
117
|
-
const globalSettings = readJsonObject(path.join(agentDir, "settings.json")) as PiSettingsLike | undefined;
|
|
118
|
-
const projectSettings = cwd ? readJsonObject(path.join(cwd, ".pi", "settings.json")) as PiSettingsLike | undefined : undefined;
|
|
119
|
-
const effectiveSettings = { ...(globalSettings ?? {}), ...(projectSettings ?? {}) };
|
|
120
|
-
return uniqueModelInfos([
|
|
121
|
-
...modelsJsonInfos(readJsonObject(path.join(agentDir, "models.json")) as PiModelsJsonLike | undefined),
|
|
122
|
-
...(settingsModelInfo(effectiveSettings) ? [settingsModelInfo(effectiveSettings)!] : []),
|
|
123
|
-
]);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
127
|
-
const colonIdx = model.lastIndexOf(":");
|
|
128
|
-
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
|
129
|
-
return {
|
|
130
|
-
baseModel: model.substring(0, colonIdx),
|
|
131
|
-
thinkingSuffix: model.substring(colonIdx),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function resolveModelCandidate(
|
|
136
|
-
model: string | undefined,
|
|
137
|
-
availableModels: AvailableModelInfo[] | undefined,
|
|
138
|
-
preferredProvider?: string,
|
|
139
|
-
): string | undefined {
|
|
140
|
-
if (!model) return undefined;
|
|
141
|
-
if (model.includes("/")) return model;
|
|
142
|
-
if (!availableModels || availableModels.length === 0) return model;
|
|
143
|
-
|
|
144
|
-
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
145
|
-
const matches = availableModels.filter((entry) => entry.id === baseModel);
|
|
146
|
-
if (preferredProvider) {
|
|
147
|
-
const preferredMatch = matches.find((entry) => entry.provider === preferredProvider);
|
|
148
|
-
if (preferredMatch) return `${preferredMatch.fullId}${thinkingSuffix}`;
|
|
149
|
-
}
|
|
150
|
-
if (matches.length !== 1) return model;
|
|
151
|
-
return `${matches[0]!.fullId}${thinkingSuffix}`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
155
|
-
/rate\s*limit/i,
|
|
156
|
-
/too many requests/i,
|
|
157
|
-
/\b429\b/,
|
|
158
|
-
/quota/i,
|
|
159
|
-
/billing/i,
|
|
160
|
-
/credit/i,
|
|
161
|
-
/auth(?:entication)?/i,
|
|
162
|
-
/unauthori[sz]ed/i,
|
|
163
|
-
/forbidden/i,
|
|
164
|
-
/api key/i,
|
|
165
|
-
/token expired/i,
|
|
166
|
-
/invalid key/i,
|
|
167
|
-
/provider.*unavailable/i,
|
|
168
|
-
/model.*unavailable/i,
|
|
169
|
-
/model.*disabled/i,
|
|
170
|
-
/model.*not found/i,
|
|
171
|
-
/unknown model/i,
|
|
172
|
-
/overloaded/i,
|
|
173
|
-
/service unavailable/i,
|
|
174
|
-
/temporar(?:ily)? unavailable/i,
|
|
175
|
-
/connection refused/i,
|
|
176
|
-
/fetch failed/i,
|
|
177
|
-
/network error/i,
|
|
178
|
-
/socket hang up/i,
|
|
179
|
-
/upstream/i,
|
|
180
|
-
/timed? out/i,
|
|
181
|
-
/timeout/i,
|
|
182
|
-
/\b502\b/,
|
|
183
|
-
/\b503\b/,
|
|
184
|
-
/\b504\b/,
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
export function isRetryableModelFailure(error: string | undefined): boolean {
|
|
188
|
-
if (!error) return false;
|
|
189
|
-
return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
|
|
193
|
-
const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
|
|
194
|
-
return nextModel ? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.` : `[fallback] ${attempt.model} failed: ${failure}.`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function buildModelCandidates(
|
|
198
|
-
primaryModel: string | undefined,
|
|
199
|
-
fallbackModels: string[] | undefined,
|
|
200
|
-
availableModels: AvailableModelInfo[] | undefined,
|
|
201
|
-
preferredProvider?: string,
|
|
202
|
-
): string[] {
|
|
203
|
-
const seen = new Set<string>();
|
|
204
|
-
const candidates: string[] = [];
|
|
205
|
-
for (const raw of [primaryModel, ...(fallbackModels ?? [])]) {
|
|
206
|
-
if (!raw) continue;
|
|
207
|
-
const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);
|
|
208
|
-
if (!normalized || seen.has(normalized)) continue;
|
|
209
|
-
seen.add(normalized);
|
|
210
|
-
candidates.push(normalized);
|
|
211
|
-
}
|
|
212
|
-
return candidates;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function isAvailableModel(model: string, availableModels: AvailableModelInfo[] | undefined): boolean {
|
|
216
|
-
if (!availableModels || availableModels.length === 0) return true;
|
|
217
|
-
const { baseModel } = splitThinkingSuffix(model);
|
|
218
|
-
if (baseModel.includes("/")) return availableModels.some((entry) => entry.fullId === baseModel);
|
|
219
|
-
return availableModels.some((entry) => entry.id === baseModel);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export interface ConfiguredModelRouting {
|
|
223
|
-
requested?: string;
|
|
224
|
-
candidates: string[];
|
|
225
|
-
reason?: string;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function buildConfiguredModelRouting(input: {
|
|
229
|
-
overrideModel?: string;
|
|
230
|
-
stepModel?: string;
|
|
231
|
-
agentModel?: string;
|
|
232
|
-
fallbackModels?: string[];
|
|
233
|
-
parentModel?: unknown;
|
|
234
|
-
modelRegistry?: unknown;
|
|
235
|
-
cwd?: string;
|
|
236
|
-
}): ConfiguredModelRouting {
|
|
237
|
-
const registryModels = availableModelInfosFromRegistry(input.modelRegistry);
|
|
238
|
-
const configModels = configuredModelInfosFromPiConfig(input.cwd);
|
|
239
|
-
const availableModels = registryModels && registryModels.length > 0 ? registryModels : configModels.length > 0 ? configModels : registryModels;
|
|
240
|
-
const parentModel = modelStringFromUnknown(input.parentModel);
|
|
241
|
-
const preferredProvider = parentModel?.split("/")[0] ?? availableModels?.[0]?.provider;
|
|
242
|
-
const requested = [input.overrideModel, input.stepModel, input.agentModel, parentModel].find((model): model is string => Boolean(model?.trim()));
|
|
243
|
-
if (availableModels && availableModels.length === 0) return { requested, candidates: [], reason: "no configured Pi models available" };
|
|
244
|
-
const rawModels = availableModels
|
|
245
|
-
? [input.overrideModel, input.stepModel, input.agentModel, ...(input.fallbackModels ?? []), parentModel, ...availableModels.map((model) => model.fullId)]
|
|
246
|
-
: [input.overrideModel, parentModel];
|
|
247
|
-
const configuredModels = rawModels
|
|
248
|
-
.filter((model): model is string => Boolean(model?.trim()))
|
|
249
|
-
.filter((model) => isAvailableModel(model.trim(), availableModels));
|
|
250
|
-
const candidates = buildModelCandidates(configuredModels[0], configuredModels.slice(1), availableModels, preferredProvider);
|
|
251
|
-
const reason = requested && candidates[0] && resolveModelCandidate(requested, availableModels, preferredProvider) !== candidates[0]
|
|
252
|
-
? "requested model unavailable; selected configured Pi fallback"
|
|
253
|
-
: candidates.length > 1
|
|
254
|
-
? "configured Pi fallback chain"
|
|
255
|
-
: undefined;
|
|
256
|
-
return { requested, candidates, reason };
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function buildConfiguredModelCandidates(input: Parameters<typeof buildConfiguredModelRouting>[0]): string[] {
|
|
260
|
-
return buildConfiguredModelRouting(input).candidates;
|
|
261
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface AvailableModelInfo {
|
|
6
|
+
provider: string;
|
|
7
|
+
id: string;
|
|
8
|
+
fullId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ModelAttemptSummary {
|
|
12
|
+
model: string;
|
|
13
|
+
success: boolean;
|
|
14
|
+
exitCode?: number | null;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ModelLike {
|
|
19
|
+
provider?: unknown;
|
|
20
|
+
id?: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModelRegistryLike {
|
|
24
|
+
getAvailable?: () => unknown[];
|
|
25
|
+
getAll?: () => unknown[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PiSettingsLike {
|
|
29
|
+
defaultProvider?: unknown;
|
|
30
|
+
defaultModel?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PiModelsJsonLike {
|
|
34
|
+
providers?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PiProviderConfigLike {
|
|
38
|
+
models?: unknown;
|
|
39
|
+
modelOverrides?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function modelInfoFromUnknown(value: unknown): AvailableModelInfo | undefined {
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
44
|
+
const record = value as ModelLike;
|
|
45
|
+
if (typeof record.provider !== "string" || typeof record.id !== "string") return undefined;
|
|
46
|
+
return { provider: record.provider, id: record.id, fullId: `${record.provider}/${record.id}` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function availableModelInfosFromRegistry(registry: unknown): AvailableModelInfo[] | undefined {
|
|
50
|
+
if (!registry || typeof registry !== "object" || Array.isArray(registry)) return undefined;
|
|
51
|
+
const candidate = registry as ModelRegistryLike;
|
|
52
|
+
const raw = typeof candidate.getAvailable === "function" ? candidate.getAvailable() : typeof candidate.getAll === "function" ? candidate.getAll() : undefined;
|
|
53
|
+
if (!Array.isArray(raw)) return undefined;
|
|
54
|
+
return raw.map(modelInfoFromUnknown).filter((entry): entry is AvailableModelInfo => entry !== undefined);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function modelStringFromUnknown(model: unknown): string | undefined {
|
|
58
|
+
return modelInfoFromUnknown(model)?.fullId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function uniqueModelInfos(models: AvailableModelInfo[]): AvailableModelInfo[] {
|
|
62
|
+
const seen = new Set<string>();
|
|
63
|
+
return models.filter((model) => {
|
|
64
|
+
if (seen.has(model.fullId)) return false;
|
|
65
|
+
seen.add(model.fullId);
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readJsonObject(filePath: string): Record<string, unknown> | undefined {
|
|
71
|
+
try {
|
|
72
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
73
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
74
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function piAgentDir(): string {
|
|
81
|
+
const envDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
82
|
+
if (envDir) {
|
|
83
|
+
if (envDir === "~") return os.homedir();
|
|
84
|
+
if (envDir.startsWith("~/")) return path.join(os.homedir(), envDir.slice(2));
|
|
85
|
+
return envDir;
|
|
86
|
+
}
|
|
87
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function settingsModelInfo(settings: PiSettingsLike | undefined): AvailableModelInfo | undefined {
|
|
91
|
+
if (typeof settings?.defaultProvider !== "string" || typeof settings.defaultModel !== "string") return undefined;
|
|
92
|
+
return { provider: settings.defaultProvider, id: settings.defaultModel, fullId: `${settings.defaultProvider}/${settings.defaultModel}` };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function modelsJsonInfos(modelsJson: PiModelsJsonLike | undefined): AvailableModelInfo[] {
|
|
96
|
+
if (!modelsJson?.providers || typeof modelsJson.providers !== "object" || Array.isArray(modelsJson.providers)) return [];
|
|
97
|
+
const infos: AvailableModelInfo[] = [];
|
|
98
|
+
for (const [provider, rawConfig] of Object.entries(modelsJson.providers as Record<string, unknown>)) {
|
|
99
|
+
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) continue;
|
|
100
|
+
const config = rawConfig as PiProviderConfigLike;
|
|
101
|
+
if (Array.isArray(config.models)) {
|
|
102
|
+
for (const rawModel of config.models) {
|
|
103
|
+
if (!rawModel || typeof rawModel !== "object" || Array.isArray(rawModel)) continue;
|
|
104
|
+
const id = (rawModel as { id?: unknown }).id;
|
|
105
|
+
if (typeof id === "string") infos.push({ provider, id, fullId: `${provider}/${id}` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (config.modelOverrides && typeof config.modelOverrides === "object" && !Array.isArray(config.modelOverrides)) {
|
|
109
|
+
for (const id of Object.keys(config.modelOverrides)) infos.push({ provider, id, fullId: `${provider}/${id}` });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return infos;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function configuredModelInfosFromPiConfig(cwd?: string): AvailableModelInfo[] {
|
|
116
|
+
const agentDir = piAgentDir();
|
|
117
|
+
const globalSettings = readJsonObject(path.join(agentDir, "settings.json")) as PiSettingsLike | undefined;
|
|
118
|
+
const projectSettings = cwd ? readJsonObject(path.join(cwd, ".pi", "settings.json")) as PiSettingsLike | undefined : undefined;
|
|
119
|
+
const effectiveSettings = { ...(globalSettings ?? {}), ...(projectSettings ?? {}) };
|
|
120
|
+
return uniqueModelInfos([
|
|
121
|
+
...modelsJsonInfos(readJsonObject(path.join(agentDir, "models.json")) as PiModelsJsonLike | undefined),
|
|
122
|
+
...(settingsModelInfo(effectiveSettings) ? [settingsModelInfo(effectiveSettings)!] : []),
|
|
123
|
+
]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
127
|
+
const colonIdx = model.lastIndexOf(":");
|
|
128
|
+
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
|
129
|
+
return {
|
|
130
|
+
baseModel: model.substring(0, colonIdx),
|
|
131
|
+
thinkingSuffix: model.substring(colonIdx),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resolveModelCandidate(
|
|
136
|
+
model: string | undefined,
|
|
137
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
138
|
+
preferredProvider?: string,
|
|
139
|
+
): string | undefined {
|
|
140
|
+
if (!model) return undefined;
|
|
141
|
+
if (model.includes("/")) return model;
|
|
142
|
+
if (!availableModels || availableModels.length === 0) return model;
|
|
143
|
+
|
|
144
|
+
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
145
|
+
const matches = availableModels.filter((entry) => entry.id === baseModel);
|
|
146
|
+
if (preferredProvider) {
|
|
147
|
+
const preferredMatch = matches.find((entry) => entry.provider === preferredProvider);
|
|
148
|
+
if (preferredMatch) return `${preferredMatch.fullId}${thinkingSuffix}`;
|
|
149
|
+
}
|
|
150
|
+
if (matches.length !== 1) return model;
|
|
151
|
+
return `${matches[0]!.fullId}${thinkingSuffix}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
155
|
+
/rate\s*limit/i,
|
|
156
|
+
/too many requests/i,
|
|
157
|
+
/\b429\b/,
|
|
158
|
+
/quota/i,
|
|
159
|
+
/billing/i,
|
|
160
|
+
/credit/i,
|
|
161
|
+
/auth(?:entication)?/i,
|
|
162
|
+
/unauthori[sz]ed/i,
|
|
163
|
+
/forbidden/i,
|
|
164
|
+
/api key/i,
|
|
165
|
+
/token expired/i,
|
|
166
|
+
/invalid key/i,
|
|
167
|
+
/provider.*unavailable/i,
|
|
168
|
+
/model.*unavailable/i,
|
|
169
|
+
/model.*disabled/i,
|
|
170
|
+
/model.*not found/i,
|
|
171
|
+
/unknown model/i,
|
|
172
|
+
/overloaded/i,
|
|
173
|
+
/service unavailable/i,
|
|
174
|
+
/temporar(?:ily)? unavailable/i,
|
|
175
|
+
/connection refused/i,
|
|
176
|
+
/fetch failed/i,
|
|
177
|
+
/network error/i,
|
|
178
|
+
/socket hang up/i,
|
|
179
|
+
/upstream/i,
|
|
180
|
+
/timed? out/i,
|
|
181
|
+
/timeout/i,
|
|
182
|
+
/\b502\b/,
|
|
183
|
+
/\b503\b/,
|
|
184
|
+
/\b504\b/,
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
export function isRetryableModelFailure(error: string | undefined): boolean {
|
|
188
|
+
if (!error) return false;
|
|
189
|
+
return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
|
|
193
|
+
const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
|
|
194
|
+
return nextModel ? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.` : `[fallback] ${attempt.model} failed: ${failure}.`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function buildModelCandidates(
|
|
198
|
+
primaryModel: string | undefined,
|
|
199
|
+
fallbackModels: string[] | undefined,
|
|
200
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
201
|
+
preferredProvider?: string,
|
|
202
|
+
): string[] {
|
|
203
|
+
const seen = new Set<string>();
|
|
204
|
+
const candidates: string[] = [];
|
|
205
|
+
for (const raw of [primaryModel, ...(fallbackModels ?? [])]) {
|
|
206
|
+
if (!raw) continue;
|
|
207
|
+
const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);
|
|
208
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
209
|
+
seen.add(normalized);
|
|
210
|
+
candidates.push(normalized);
|
|
211
|
+
}
|
|
212
|
+
return candidates;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isAvailableModel(model: string, availableModels: AvailableModelInfo[] | undefined): boolean {
|
|
216
|
+
if (!availableModels || availableModels.length === 0) return true;
|
|
217
|
+
const { baseModel } = splitThinkingSuffix(model);
|
|
218
|
+
if (baseModel.includes("/")) return availableModels.some((entry) => entry.fullId === baseModel);
|
|
219
|
+
return availableModels.some((entry) => entry.id === baseModel);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface ConfiguredModelRouting {
|
|
223
|
+
requested?: string;
|
|
224
|
+
candidates: string[];
|
|
225
|
+
reason?: string;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function buildConfiguredModelRouting(input: {
|
|
229
|
+
overrideModel?: string;
|
|
230
|
+
stepModel?: string;
|
|
231
|
+
agentModel?: string;
|
|
232
|
+
fallbackModels?: string[];
|
|
233
|
+
parentModel?: unknown;
|
|
234
|
+
modelRegistry?: unknown;
|
|
235
|
+
cwd?: string;
|
|
236
|
+
}): ConfiguredModelRouting {
|
|
237
|
+
const registryModels = availableModelInfosFromRegistry(input.modelRegistry);
|
|
238
|
+
const configModels = configuredModelInfosFromPiConfig(input.cwd);
|
|
239
|
+
const availableModels = registryModels && registryModels.length > 0 ? registryModels : configModels.length > 0 ? configModels : registryModels;
|
|
240
|
+
const parentModel = modelStringFromUnknown(input.parentModel);
|
|
241
|
+
const preferredProvider = parentModel?.split("/")[0] ?? availableModels?.[0]?.provider;
|
|
242
|
+
const requested = [input.overrideModel, input.stepModel, input.agentModel, parentModel].find((model): model is string => Boolean(model?.trim()));
|
|
243
|
+
if (availableModels && availableModels.length === 0) return { requested, candidates: [], reason: "no configured Pi models available" };
|
|
244
|
+
const rawModels = availableModels
|
|
245
|
+
? [input.overrideModel, input.stepModel, input.agentModel, ...(input.fallbackModels ?? []), parentModel, ...availableModels.map((model) => model.fullId)]
|
|
246
|
+
: [input.overrideModel, parentModel];
|
|
247
|
+
const configuredModels = rawModels
|
|
248
|
+
.filter((model): model is string => Boolean(model?.trim()))
|
|
249
|
+
.filter((model) => isAvailableModel(model.trim(), availableModels));
|
|
250
|
+
const candidates = buildModelCandidates(configuredModels[0], configuredModels.slice(1), availableModels, preferredProvider);
|
|
251
|
+
const reason = requested && candidates[0] && resolveModelCandidate(requested, availableModels, preferredProvider) !== candidates[0]
|
|
252
|
+
? "requested model unavailable; selected configured Pi fallback"
|
|
253
|
+
: candidates.length > 1
|
|
254
|
+
? "configured Pi fallback chain"
|
|
255
|
+
: undefined;
|
|
256
|
+
return { requested, candidates, reason };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function buildConfiguredModelCandidates(input: Parameters<typeof buildConfiguredModelRouting>[0]): string[] {
|
|
260
|
+
return buildConfiguredModelRouting(input).candidates;
|
|
261
|
+
}
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
4
|
-
|
|
5
|
-
export function sourcePiProjects(cwd: string): string[] {
|
|
6
|
-
const sourceDir = path.join(cwd, "Source");
|
|
7
|
-
try {
|
|
8
|
-
return fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
9
|
-
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pi-"))
|
|
10
|
-
.map((entry) => `Source/${entry.name}`)
|
|
11
|
-
.sort();
|
|
12
|
-
} catch {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function chunkProjects(projects: string[], target = 6): string[][] {
|
|
18
|
-
const chunks = Array.from({ length: Math.min(Math.max(1, target), Math.max(1, projects.length)) }, () => [] as string[]);
|
|
19
|
-
projects.forEach((project, index) => chunks[index % chunks.length]!.push(project));
|
|
20
|
-
return chunks.filter((chunk) => chunk.length > 0);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function expandParallelResearchWorkflow(workflow: WorkflowConfig, cwd: string): WorkflowConfig {
|
|
24
|
-
if (workflow.name !== "parallel-research") return workflow;
|
|
25
|
-
const projects = sourcePiProjects(cwd);
|
|
26
|
-
if (projects.length === 0) return workflow;
|
|
27
|
-
const chunks = chunkProjects(projects, Math.min(8, Math.max(4, Math.ceil(projects.length / 3))));
|
|
28
|
-
const exploreSteps: WorkflowStep[] = chunks.map((paths, index) => ({
|
|
29
|
-
id: `explore-shard-${index + 1}`,
|
|
30
|
-
role: "explorer",
|
|
31
|
-
parallelGroup: "explore",
|
|
32
|
-
reads: paths,
|
|
33
|
-
task: [`Explore this dynamic shard for: {goal}`, "", "Paths:", ...paths.map((item) => `- ${item}`), "", "Focus on purpose, architecture, runtime/UI patterns, package config, docs, and lessons for pi-crew."].join("\n"),
|
|
34
|
-
}));
|
|
35
|
-
return {
|
|
36
|
-
...workflow,
|
|
37
|
-
steps: [
|
|
38
|
-
{ id: "discover", role: "explorer", parallelGroup: "inventory", task: `Quickly inventory and validate ${projects.length} pi-* projects for: {goal}\n\nProjects:\n${projects.map((item) => `- ${item}`).join("\n")}\n\nDo not block shard work; summarize routing notes only.` },
|
|
39
|
-
...exploreSteps,
|
|
40
|
-
{ id: "synthesize", role: "analyst", dependsOn: exploreSteps.map((step) => step.id), task: "Synthesize all dynamic shard findings. Identify common patterns, gaps, and concrete recommendations. Use discover output if available, but prioritize completed shard outputs." },
|
|
41
|
-
{ id: "write", role: "writer", dependsOn: ["synthesize"], output: "research-summary.md", task: "Write a concise final summary with evidence, risks, and actionable next steps." },
|
|
42
|
-
],
|
|
43
|
-
};
|
|
44
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { WorkflowConfig, WorkflowStep } from "../workflows/workflow-config.ts";
|
|
4
|
+
|
|
5
|
+
export function sourcePiProjects(cwd: string): string[] {
|
|
6
|
+
const sourceDir = path.join(cwd, "Source");
|
|
7
|
+
try {
|
|
8
|
+
return fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
9
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("pi-"))
|
|
10
|
+
.map((entry) => `Source/${entry.name}`)
|
|
11
|
+
.sort();
|
|
12
|
+
} catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function chunkProjects(projects: string[], target = 6): string[][] {
|
|
18
|
+
const chunks = Array.from({ length: Math.min(Math.max(1, target), Math.max(1, projects.length)) }, () => [] as string[]);
|
|
19
|
+
projects.forEach((project, index) => chunks[index % chunks.length]!.push(project));
|
|
20
|
+
return chunks.filter((chunk) => chunk.length > 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function expandParallelResearchWorkflow(workflow: WorkflowConfig, cwd: string): WorkflowConfig {
|
|
24
|
+
if (workflow.name !== "parallel-research") return workflow;
|
|
25
|
+
const projects = sourcePiProjects(cwd);
|
|
26
|
+
if (projects.length === 0) return workflow;
|
|
27
|
+
const chunks = chunkProjects(projects, Math.min(8, Math.max(4, Math.ceil(projects.length / 3))));
|
|
28
|
+
const exploreSteps: WorkflowStep[] = chunks.map((paths, index) => ({
|
|
29
|
+
id: `explore-shard-${index + 1}`,
|
|
30
|
+
role: "explorer",
|
|
31
|
+
parallelGroup: "explore",
|
|
32
|
+
reads: paths,
|
|
33
|
+
task: [`Explore this dynamic shard for: {goal}`, "", "Paths:", ...paths.map((item) => `- ${item}`), "", "Focus on purpose, architecture, runtime/UI patterns, package config, docs, and lessons for pi-crew."].join("\n"),
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
...workflow,
|
|
37
|
+
steps: [
|
|
38
|
+
{ id: "discover", role: "explorer", parallelGroup: "inventory", task: `Quickly inventory and validate ${projects.length} pi-* projects for: {goal}\n\nProjects:\n${projects.map((item) => `- ${item}`).join("\n")}\n\nDo not block shard work; summarize routing notes only.` },
|
|
39
|
+
...exploreSteps,
|
|
40
|
+
{ id: "synthesize", role: "analyst", dependsOn: exploreSteps.map((step) => step.id), task: "Synthesize all dynamic shard findings. Identify common patterns, gaps, and concrete recommendations. Use discover output if available, but prioritize completed shard outputs." },
|
|
41
|
+
{ id: "write", role: "writer", dependsOn: ["synthesize"], output: "research-summary.md", task: "Write a concise final summary with evidence, risks, and actionable next steps." },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|