pi-crew 0.1.14 → 0.1.15

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.15
4
+
5
+ - Child-process model selection now uses Pi-configured/available models and auto-discovers provider/model entries from Pi settings/models config.
6
+ - Added configured-model fallback chains for worker runs instead of forcing builtin provider hints.
7
+ - Fixed skipped task agent records so they no longer appear queued.
8
+
3
9
  ## 0.1.0
4
10
 
5
11
  - Initial scaffold for `pi-crew`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -307,7 +307,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
307
307
  const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
308
308
  const executeWorkers = runtime.kind === "child-process";
309
309
  const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
310
- const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
310
+ const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
311
311
  const text = [
312
312
  `Created pi-crew run ${executed.manifest.runId}.`,
313
313
  `Team: ${team.name}`,
@@ -430,7 +430,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
430
430
  const loadedConfig = loadConfig(ctx.cwd);
431
431
  const runtime = await resolveCrewRuntime(loadedConfig.config);
432
432
  const executeWorkers = runtime.kind === "child-process";
433
- const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
433
+ const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, modelOverride: params.model, signal: ctx.signal });
434
434
  return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
435
435
  });
436
436
  }
@@ -48,7 +48,7 @@ export interface CrewAgentRecord {
48
48
  export function taskStatusToAgentStatus(status: TeamTaskStatus): CrewAgentStatus {
49
49
  if (status === "completed") return "completed";
50
50
  if (status === "failed") return "failed";
51
- if (status === "cancelled") return "cancelled";
51
+ if (status === "cancelled" || status === "skipped") return "cancelled";
52
52
  if (status === "running") return "running";
53
53
  return "queued";
54
54
  }
@@ -1,3 +1,7 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+
1
5
  export interface AvailableModelInfo {
2
6
  provider: string;
3
7
  id: string;
@@ -11,6 +15,114 @@ export interface ModelAttemptSummary {
11
15
  error?: string;
12
16
  }
13
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
+
14
126
  export function splitThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
15
127
  const colonIdx = model.lastIndexOf(":");
16
128
  if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
@@ -99,3 +211,34 @@ export function buildModelCandidates(
99
211
  }
100
212
  return candidates;
101
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 function buildConfiguredModelCandidates(input: {
223
+ overrideModel?: string;
224
+ stepModel?: string;
225
+ agentModel?: string;
226
+ fallbackModels?: string[];
227
+ parentModel?: unknown;
228
+ modelRegistry?: unknown;
229
+ cwd?: string;
230
+ }): string[] {
231
+ const registryModels = availableModelInfosFromRegistry(input.modelRegistry);
232
+ const configModels = configuredModelInfosFromPiConfig(input.cwd);
233
+ const availableModels = registryModels && registryModels.length > 0 ? registryModels : configModels.length > 0 ? configModels : registryModels;
234
+ const parentModel = modelStringFromUnknown(input.parentModel);
235
+ const preferredProvider = parentModel?.split("/")[0] ?? availableModels?.[0]?.provider;
236
+ if (availableModels && availableModels.length === 0) return [];
237
+ const rawModels = availableModels
238
+ ? [input.overrideModel, input.stepModel, input.agentModel, ...(input.fallbackModels ?? []), parentModel, ...availableModels.map((model) => model.fullId)]
239
+ : [input.overrideModel, parentModel];
240
+ const configuredModels = rawModels
241
+ .filter((model): model is string => Boolean(model?.trim()))
242
+ .filter((model) => isAvailableModel(model.trim(), availableModels));
243
+ return buildModelCandidates(configuredModels[0], configuredModels.slice(1), availableModels, preferredProvider);
244
+ }
@@ -9,7 +9,7 @@ import { createTaskClaim } from "../state/task-claims.ts";
9
9
  import { createWorkerHeartbeat, touchWorkerHeartbeat } from "./worker-heartbeat.ts";
10
10
  import type { WorkflowStep } from "../workflows/workflow-config.ts";
11
11
  import { captureWorktreeDiff, captureWorktreeDiffStat, prepareTaskWorkspace } from "../worktree/worktree-manager.ts";
12
- import { buildModelCandidates, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
12
+ import { buildConfiguredModelCandidates, formatModelAttemptNote, isRetryableModelFailure, type ModelAttemptSummary } from "./model-fallback.ts";
13
13
  import { parsePiJsonOutput, type ParsedPiJsonOutput } from "./pi-json-output.ts";
14
14
  import { runChildPi } from "./child-pi.ts";
15
15
  import { buildTaskPacket, renderTaskPacket } from "./task-packet.ts";
@@ -36,6 +36,7 @@ export interface TaskRunnerInput {
36
36
  parentContext?: string;
37
37
  parentModel?: unknown;
38
38
  modelRegistry?: unknown;
39
+ modelOverride?: string;
39
40
  limits?: CrewLimitsConfig;
40
41
  dependencyContextText?: string;
41
42
  }
@@ -267,8 +268,8 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
267
268
  producer: task.id,
268
269
  });
269
270
  if (runtimeKind === "child-process") {
270
- const candidates = buildModelCandidates(input.step.model ?? input.agent.model, input.agent.fallbackModels, undefined);
271
- const attemptModels = candidates.length > 0 ? candidates : [input.step.model ?? input.agent.model];
271
+ const candidates = buildConfiguredModelCandidates({ overrideModel: input.modelOverride, stepModel: input.step.model, agentModel: input.agent.model, fallbackModels: input.agent.fallbackModels, parentModel: input.parentModel, modelRegistry: input.modelRegistry, cwd: manifest.cwd });
272
+ const attemptModels = candidates.length > 0 ? candidates : [undefined];
272
273
  const logs: string[] = [];
273
274
  let finalStdout = "";
274
275
  let finalStderr = "";
@@ -30,6 +30,7 @@ export interface ExecuteTeamRunInput {
30
30
  parentContext?: string;
31
31
  parentModel?: unknown;
32
32
  modelRegistry?: unknown;
33
+ modelOverride?: string;
33
34
  signal?: AbortSignal;
34
35
  }
35
36
 
@@ -179,7 +180,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
179
180
  const results = await Promise.all(readyBatch.map((task) => {
180
181
  const step = findStep(input.workflow, task);
181
182
  const agent = findAgent(input.agents, task);
182
- return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, limits: input.limits });
183
+ return runTeamTask({ manifest, tasks, task, step, agent, signal: input.signal, executeWorkers: input.executeWorkers, runtimeKind: input.runtime?.kind, runtimeConfig: input.runtimeConfig, parentContext: input.parentContext, parentModel: input.parentModel, modelRegistry: input.modelRegistry, modelOverride: input.modelOverride, limits: input.limits });
183
184
  }));
184
185
  manifest = { ...results.at(-1)!.manifest, artifacts: mergeArtifacts([manifest.artifacts, ...results.map((item) => item.manifest.artifacts)].flat()) };
185
186
  tasks = mergeTaskUpdates(tasks, results);