pi-crew 0.9.59 → 0.9.60

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.
@@ -13,7 +13,16 @@ import type { WorkflowStep } from "../../workflows/workflow-config.ts";
13
13
  import { createIrcTool } from "../custom-tools/irc-tool.ts";
14
14
  import { createSubmitResultTool } from "../custom-tools/submit-result-tool.ts";
15
15
  import { buildMcpProxyFromSession } from "../mcp-proxy.ts";
16
- import { buildConfiguredModelRouting } from "../model/model-fallback.ts";
16
+ import {
17
+ availableModelInfosFromRegistry,
18
+ buildConfiguredModelRouting,
19
+ type ModelFallbackPolicy,
20
+ modelRefToString,
21
+ providerOfModelRef,
22
+ resolveDefaultSubagentModel,
23
+ resolveModelFallbackPolicy,
24
+ warnOutOfScopeSoft,
25
+ } from "../model/model-fallback.ts";
17
26
  import { readEnabledModelsPatterns } from "../model/model-scope.ts";
18
27
  import { isLiveSessionRuntimeAvailable } from "../model/runtime-resolver.ts";
19
28
  import { awaitRuntimeWarmup } from "../model/runtime-warmup.ts";
@@ -94,6 +103,8 @@ export interface LiveSessionSpawnInput {
94
103
  modelRegistry?: unknown;
95
104
  modelOverride?: string;
96
105
  teamRoleModel?: string;
106
+ teamRoleFallbackModels?: string[];
107
+ teamRoleThinking?: string;
97
108
  isCurrent?: () => boolean;
98
109
  /** Workspace where this run was initiated — used for session-scoped live-agent visibility. */
99
110
  workspaceId: string;
@@ -111,6 +122,15 @@ export interface LiveSessionRunResult {
111
122
  error?: string;
112
123
  /** Phase 1: Extracted yield result from submit_result tool call. */
113
124
  yieldResult?: YieldResult;
125
+ /** Model routing diagnostics for the task state. */
126
+ modelRouting?: {
127
+ requested?: string;
128
+ resolved: string;
129
+ fallbackChain: string[];
130
+ reason?: string;
131
+ droppedRequested?: string;
132
+ autoFallbackCount?: number;
133
+ };
114
134
  }
115
135
 
116
136
  export interface LiveSessionUnavailableResult {
@@ -263,6 +283,24 @@ async function resolveScopeModelsPatterns(cwd: string, agentDir?: string): Promi
263
283
  return readEnabledModelsPatterns(cwd, agentDir);
264
284
  }
265
285
 
286
+ /** Resolve model fallback policy from crew config + env (best-effort). */
287
+ function resolveLiveModelFallbackPolicy(cwd: string): ModelFallbackPolicy | undefined {
288
+ try {
289
+ return resolveModelFallbackPolicy(loadConfig(cwd).config.runtime?.modelFallback);
290
+ } catch {
291
+ return undefined;
292
+ }
293
+ }
294
+
295
+ /** Resolve the default subagent model from crew config + env. */
296
+ function resolveLiveDefaultSubagentModel(cwd: string): string | undefined {
297
+ try {
298
+ return resolveDefaultSubagentModel(loadConfig(cwd).config.runtime?.modelFallback);
299
+ } catch {
300
+ return undefined;
301
+ }
302
+ }
303
+
266
304
  function modelFromRegistry(modelRegistry: unknown, modelId: string | undefined): unknown {
267
305
  if (!modelId?.includes("/")) return undefined;
268
306
  const registry = asRecord(modelRegistry);
@@ -292,7 +330,11 @@ function modelFromRegistry(modelRegistry: unknown, modelId: string | undefined):
292
330
  * raw `parentModel` unchanged so the caller surfaces E008.
293
331
  */
294
332
  export function resolveParentModelFromRegistry(modelRegistry: unknown, rawParentModel: unknown): string | undefined {
295
- const raw = typeof rawParentModel === "string" ? rawParentModel.trim() : undefined;
333
+ // `ctx.model` is a pi `Model` OBJECT, not a string. The old string-only
334
+ // guard silently discarded it, so every live-session subagent that inherited
335
+ // the parent model landed on `getAvailable()[0]` instead — i.e. the parent
336
+ // model was never actually honoured on this path.
337
+ const raw = modelRefToString(rawParentModel);
296
338
  if (raw) {
297
339
  const candidate = raw.includes("/")
298
340
  ? raw
@@ -305,19 +347,13 @@ export function resolveParentModelFromRegistry(modelRegistry: unknown, rawParent
305
347
  })();
306
348
  if (candidate && modelFromRegistry(modelRegistry, candidate)) return candidate;
307
349
  }
308
- const registry = modelRegistry as { getAvailable?: () => unknown[] } | undefined;
309
- if (registry && typeof registry.getAvailable === "function") {
310
- try {
311
- const available = registry.getAvailable();
312
- if (Array.isArray(available) && available.length > 0) {
313
- const first = available[0] as { provider?: unknown; id?: unknown } | undefined;
314
- if (first && typeof first.provider === "string" && typeof first.id === "string") {
315
- return `${first.provider}/${first.id}`;
316
- }
317
- }
318
- } catch {
319
- // ignore — fall through to raw
320
- }
350
+ const available = availableModelInfosFromRegistry(modelRegistry);
351
+ if (available && available.length > 0) {
352
+ // Staying on the parent's provider keeps the same auth + cost profile;
353
+ // only cross providers when the parent's provider has nothing available.
354
+ const parentProvider = providerOfModelRef(raw);
355
+ const sameProvider = parentProvider ? available.find((entry) => entry.provider === parentProvider) : undefined;
356
+ return (sameProvider ?? available[0]).fullId;
321
357
  }
322
358
  return raw;
323
359
  }
@@ -657,15 +693,38 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
657
693
  overrideModel: input.modelOverride,
658
694
  stepModel: input.step.model,
659
695
  teamRoleModel: input.teamRoleModel,
696
+ teamRoleFallbackModels: input.teamRoleFallbackModels,
660
697
  agentModel: input.agent.model,
698
+ defaultSubagentModel: resolveLiveDefaultSubagentModel(input.manifest.cwd),
661
699
  fallbackModels: input.agent.fallbackModels,
662
700
  parentModel: effectiveParentModel,
663
701
  modelRegistry: input.modelRegistry,
664
702
  cwd: input.manifest.cwd,
703
+ policy: resolveLiveModelFallbackPolicy(input.manifest.cwd),
665
704
  scopeModelsPatterns: await resolveScopeModelsPatterns(input.manifest.cwd),
666
705
  });
667
706
  const resolvedModel =
668
707
  modelFromRegistry(input.modelRegistry, modelRouting.candidates[0] ?? modelRouting.requested) ?? input.parentModel;
708
+ // Surface a warning when the caller's requested model was silently replaced.
709
+ if (modelRouting.droppedRequested) {
710
+ appendEventFireAndForget(input.manifest.eventsPath, {
711
+ type: "task.model_dropped",
712
+ runId: input.manifest.runId,
713
+ taskId: input.task.id,
714
+ message: `Requested model "${modelRouting.droppedRequested}" is not available; using "${modelRouting.candidates[0] ?? "default"}" instead.`,
715
+ data: {
716
+ requested: modelRouting.droppedRequested,
717
+ resolved: modelRouting.candidates[0],
718
+ fallbackChain: modelRouting.candidates,
719
+ },
720
+ });
721
+ }
722
+ // Sec-M1: surface a non-silent warning when a soft-sourced model is out-of-scope.
723
+ // Only non-caller sources (frontmatter / resolved) get the soft warning —
724
+ // caller sources already throw inside buildConfiguredModelRouting.
725
+ warnOutOfScopeSoft(modelRouting.scopeVerdict, "live-session.model-out-of-scope");
726
+ // H1.a: teamRole.thinking takes precedence over agent.thinking.
727
+ const effectiveThinking = input.teamRoleThinking ?? input.agent.thinking;
669
728
  // Phase 4: MCP proxy — will be determined after session creation
670
729
  // (we check parent's MCP tools and share connections when available)
671
730
  const mcpProxy = buildMcpProxyFromSession([], { shareMcp: true });
@@ -695,7 +754,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
695
754
  : {}),
696
755
  ...(input.modelRegistry ? { modelRegistry: input.modelRegistry } : {}),
697
756
  ...(resolvedModel ? { model: resolvedModel } : {}),
698
- ...(input.agent.thinking ? { thinkingLevel: input.agent.thinking } : {}),
757
+ ...(effectiveThinking ? { thinkingLevel: effectiveThinking } : {}),
699
758
  ...(mcpProxy.enableMcp ? {} : { enableMCP: false }),
700
759
  customTools,
701
760
  });
@@ -1087,6 +1146,14 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
1087
1146
  jsonEvents,
1088
1147
  usage,
1089
1148
  yieldResult,
1149
+ modelRouting: {
1150
+ requested: modelRouting.requested,
1151
+ resolved: modelRefToString(resolvedModel) ?? modelRouting.candidates[0] ?? "default",
1152
+ fallbackChain: modelRouting.candidates,
1153
+ reason: modelRouting.reason,
1154
+ droppedRequested: modelRouting.droppedRequested,
1155
+ autoFallbackCount: modelRouting.autoFallbackCount,
1156
+ },
1090
1157
  };
1091
1158
  } catch (error) {
1092
1159
  const message = error instanceof Error ? error.message : String(error);