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.
Files changed (162) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/README.md +5 -0
  4. package/agents/analyst.md +11 -11
  5. package/agents/critic.md +11 -11
  6. package/agents/executor.md +11 -11
  7. package/agents/explorer.md +11 -11
  8. package/agents/planner.md +11 -11
  9. package/agents/reviewer.md +11 -11
  10. package/agents/security-reviewer.md +11 -11
  11. package/agents/test-engineer.md +11 -11
  12. package/agents/verifier.md +11 -11
  13. package/agents/writer.md +11 -11
  14. package/docs/refactor-tasks-phase3.md +394 -394
  15. package/docs/refactor-tasks-phase4.md +564 -564
  16. package/docs/refactor-tasks-phase5.md +402 -402
  17. package/docs/refactor-tasks-phase6.md +662 -662
  18. package/docs/research-extension-examples.md +297 -297
  19. package/docs/research-extension-system.md +324 -324
  20. package/docs/research-optimization-plan.md +548 -548
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/resource-formats.md +10 -8
  24. package/docs/runtime-flow.md +148 -148
  25. package/docs/source-runtime-refactor-map.md +83 -83
  26. package/docs/usage.md +6 -0
  27. package/index.ts +6 -6
  28. package/package.json +3 -3
  29. package/schema.json +2 -2
  30. package/src/agents/agent-serializer.ts +34 -34
  31. package/src/config/config.ts +8 -4
  32. package/src/extension/cross-extension-rpc.ts +82 -82
  33. package/src/extension/import-index.ts +18 -2
  34. package/src/extension/register.ts +11 -1
  35. package/src/extension/registration/compaction-guard.ts +125 -125
  36. package/src/extension/registration/subagent-helpers.ts +30 -6
  37. package/src/extension/registration/subagent-tools.ts +8 -3
  38. package/src/extension/result-watcher.ts +98 -98
  39. package/src/extension/run-import.ts +12 -2
  40. package/src/extension/run-index.ts +12 -2
  41. package/src/extension/run-maintenance.ts +24 -24
  42. package/src/extension/team-tool/api.ts +54 -14
  43. package/src/extension/team-tool/cancel.ts +31 -31
  44. package/src/extension/team-tool/doctor.ts +179 -179
  45. package/src/extension/team-tool/inspect.ts +41 -41
  46. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  47. package/src/extension/team-tool/plan.ts +19 -19
  48. package/src/extension/team-tool/status.ts +73 -73
  49. package/src/observability/correlation.ts +35 -35
  50. package/src/observability/event-to-metric.ts +54 -54
  51. package/src/observability/exporters/adapter.ts +24 -24
  52. package/src/observability/exporters/otlp-exporter.ts +65 -65
  53. package/src/observability/exporters/prometheus-exporter.ts +47 -47
  54. package/src/observability/metric-registry.ts +72 -72
  55. package/src/observability/metric-retention.ts +46 -46
  56. package/src/observability/metric-sink.ts +51 -51
  57. package/src/observability/metrics-primitives.ts +166 -166
  58. package/src/prompt/prompt-runtime.ts +68 -68
  59. package/src/runtime/agent-control.ts +64 -64
  60. package/src/runtime/agent-memory.ts +72 -72
  61. package/src/runtime/agent-observability.ts +114 -113
  62. package/src/runtime/async-marker.ts +26 -26
  63. package/src/runtime/background-runner.ts +53 -53
  64. package/src/runtime/crash-recovery.ts +56 -56
  65. package/src/runtime/crew-agent-records.ts +54 -9
  66. package/src/runtime/crew-agent-runtime.ts +58 -58
  67. package/src/runtime/deadletter.ts +36 -36
  68. package/src/runtime/direct-run.ts +35 -35
  69. package/src/runtime/foreground-control.ts +82 -82
  70. package/src/runtime/green-contract.ts +46 -46
  71. package/src/runtime/group-join.ts +88 -88
  72. package/src/runtime/heartbeat-gradient.ts +28 -28
  73. package/src/runtime/heartbeat-watcher.ts +80 -80
  74. package/src/runtime/live-agent-control.ts +87 -78
  75. package/src/runtime/live-agent-manager.ts +85 -85
  76. package/src/runtime/live-control-realtime.ts +36 -36
  77. package/src/runtime/live-session-runtime.ts +299 -299
  78. package/src/runtime/manifest-cache.ts +248 -212
  79. package/src/runtime/model-fallback.ts +261 -261
  80. package/src/runtime/parallel-research.ts +44 -44
  81. package/src/runtime/parallel-utils.ts +99 -99
  82. package/src/runtime/pi-json-output.ts +111 -111
  83. package/src/runtime/policy-engine.ts +78 -78
  84. package/src/runtime/post-exit-stdio-guard.ts +86 -86
  85. package/src/runtime/process-status.ts +56 -56
  86. package/src/runtime/progress-event-coalescer.ts +43 -43
  87. package/src/runtime/recovery-recipes.ts +74 -74
  88. package/src/runtime/retry-executor.ts +59 -59
  89. package/src/runtime/role-permission.ts +39 -39
  90. package/src/runtime/session-usage.ts +79 -79
  91. package/src/runtime/sidechain-output.ts +28 -28
  92. package/src/runtime/subagent-manager.ts +80 -12
  93. package/src/runtime/task-display.ts +38 -38
  94. package/src/runtime/task-output-context.ts +127 -106
  95. package/src/runtime/task-runner/live-executor.ts +98 -98
  96. package/src/runtime/task-runner/progress.ts +111 -111
  97. package/src/runtime/task-runner/result-utils.ts +14 -14
  98. package/src/runtime/task-runner/state-helpers.ts +22 -22
  99. package/src/runtime/team-runner.ts +1 -1
  100. package/src/runtime/worker-heartbeat.ts +21 -21
  101. package/src/runtime/worker-startup.ts +57 -57
  102. package/src/schema/config-schema.ts +21 -21
  103. package/src/schema/team-tool-schema.ts +100 -100
  104. package/src/state/artifact-store.ts +122 -108
  105. package/src/state/contracts.ts +105 -105
  106. package/src/state/jsonl-writer.ts +77 -77
  107. package/src/state/mailbox.ts +67 -22
  108. package/src/state/state-store.ts +36 -5
  109. package/src/state/task-claims.ts +42 -42
  110. package/src/state/usage.ts +29 -29
  111. package/src/subagents/async-entry.ts +1 -1
  112. package/src/subagents/index.ts +3 -3
  113. package/src/subagents/live/control.ts +1 -1
  114. package/src/subagents/live/manager.ts +1 -1
  115. package/src/subagents/live/realtime.ts +1 -1
  116. package/src/subagents/live/session-runtime.ts +1 -1
  117. package/src/subagents/manager.ts +1 -1
  118. package/src/subagents/spawn.ts +1 -1
  119. package/src/teams/discover-teams.ts +27 -5
  120. package/src/teams/team-serializer.ts +38 -36
  121. package/src/types/diff.d.ts +18 -18
  122. package/src/ui/crew-footer.ts +101 -101
  123. package/src/ui/crew-select-list.ts +111 -111
  124. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  125. package/src/ui/dynamic-border.ts +25 -25
  126. package/src/ui/layout-primitives.ts +106 -106
  127. package/src/ui/loaders.ts +158 -158
  128. package/src/ui/mascot.ts +441 -441
  129. package/src/ui/render-diff.ts +119 -119
  130. package/src/ui/run-dashboard.ts +5 -2
  131. package/src/ui/run-snapshot-cache.ts +19 -8
  132. package/src/ui/spinner.ts +17 -17
  133. package/src/ui/status-colors.ts +54 -54
  134. package/src/ui/syntax-highlight.ts +116 -116
  135. package/src/ui/transcript-viewer.ts +15 -1
  136. package/src/utils/completion-dedupe.ts +63 -63
  137. package/src/utils/file-coalescer.ts +84 -84
  138. package/src/utils/frontmatter.ts +36 -36
  139. package/src/utils/fs-watch.ts +31 -31
  140. package/src/utils/git.ts +262 -262
  141. package/src/utils/ids.ts +12 -12
  142. package/src/utils/names.ts +26 -26
  143. package/src/utils/paths.ts +3 -2
  144. package/src/utils/safe-paths.ts +34 -0
  145. package/src/utils/sleep.ts +32 -32
  146. package/src/utils/timings.ts +31 -31
  147. package/src/utils/visual.ts +159 -159
  148. package/src/workflows/discover-workflows.ts +30 -3
  149. package/src/workflows/validate-workflow.ts +40 -40
  150. package/src/worktree/branch-freshness.ts +45 -45
  151. package/teams/default.team.md +12 -12
  152. package/teams/fast-fix.team.md +11 -11
  153. package/teams/implementation.team.md +18 -18
  154. package/teams/parallel-research.team.md +14 -14
  155. package/teams/research.team.md +11 -11
  156. package/teams/review.team.md +12 -12
  157. package/workflows/default.workflow.md +29 -29
  158. package/workflows/fast-fix.workflow.md +22 -22
  159. package/workflows/implementation.workflow.md +38 -38
  160. package/workflows/parallel-research.workflow.md +46 -46
  161. package/workflows/research.workflow.md +22 -22
  162. 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
+ }