export interface ChildModelResolutionDiagnostic { agent?: string; model?: string; host?: "parent" | "runner"; /** The launch's resolved capability ceiling, when one is active. */ capabilityCeiling?: { denyExtensions: boolean; sources?: readonly string[] }; } /** * Pi core's model-resolution failure text. `resolveCliModel` reports an * unknown provider or model and cannot know which extension would have * registered it, so these two shapes are the ones that mean "the child's * model registry was missing a provider", not "the provider is unhealthy". */ const MODEL_RESOLUTION_FAILURE_PATTERNS = [ /^Model .+ not found\b/i, /^Unknown provider "/i, ]; /** True for a core model-resolution failure, which a missing extension can explain. */ export function isChildModelResolutionFailure(error: string | undefined): boolean { const text = typeof error === "string" ? error.trim() : ""; return MODEL_RESOLUTION_FAILURE_PATTERNS.some((pattern) => pattern.test(text)); } /** * A `capabilityCeiling.denyExtensions` launch loads no extension at all: * ambient, listed, and MCP provider extensions are all suppressed, so neither * `async: true` nor a frontmatter list can register the provider. Saying only * the ambient-extension rule would direct the operator at remedies the ceiling * discards. */ function capabilityCeilingDeniesExtensions(diagnostic: ChildModelResolutionDiagnostic): boolean { return diagnostic.capabilityCeiling?.denyExtensions === true; } function denyExtensionsClause(diagnostic: ChildModelResolutionDiagnostic): string { const sources = diagnostic.capabilityCeiling?.sources ?? []; return `Capability ceiling from ${sources.length > 0 ? sources.join(", ") : "an unnamed source"} denies extensions`; } /** * Explain a child model that did not resolve because the extension serving its * provider, or registering it as a virtual model, never loaded: a foreground * child never loads the parent's ambient extensions, an explicit `extensions` * list keeps a background child from loading them, and a capability ceiling * denies extensions to both. A foreground child inherits the parent's * providers but not its virtual models, so only the loaded extension can * register one. The caller keeps the core error intact and appends this * explanation, so a genuinely unknown model id still reads as one. */ export function formatChildModelResolutionDiagnostic(diagnostic: ChildModelResolutionDiagnostic): string { const subject = diagnostic.agent ? `Agent '${diagnostic.agent}'` : "Subagent"; const model = diagnostic.model ? `'${diagnostic.model}'` : "this model"; if (capabilityCeilingDeniesExtensions(diagnostic)) { return (diagnostic.host === "parent" ? [ `${subject} ran as a foreground child, which never loads the parent's ambient extensions.`, `${denyExtensionsClause(diagnostic)}, so if ${model} is served by a provider extension or is an extension-registered virtual model, that extension cannot load for this child: \`async: true\` does not help either, and listed \`subagentOnlyExtensions\` or \`extensions\` entries are suppressed.`, "Relax the capability ceiling to allow extensions, or use a model that the parent's model registry already resolves.", ] : [ `${subject} ran as a background child without the ambient extensions.`, `${denyExtensionsClause(diagnostic)}, so if ${model} is served by a provider extension or is an extension-registered virtual model, that extension cannot load for this child: listed \`subagentOnlyExtensions\` or \`extensions\` entries are suppressed, and leaving \`extensions\` unset does not enable ambient loading.`, "Relax the capability ceiling to allow extensions, or use a model that the child's model registry already resolves.", ]).join("\n"); } if (diagnostic.host === "parent") { return [ `${subject} ran as a foreground child, which never loads the parent's ambient extensions. It inherits the providers they registered, but not virtual models registered with \`pi.registerVirtualModel()\`.`, `If ${model} is served by a provider extension, check the parent's \`/model\` list and the child extension diagnostics. If it is an extension-registered virtual model, the extension that registers it must load in this child.`, `Load the extension for this child with \`subagentOnlyExtensions\` or \`extensions\` in the agent frontmatter, or register it from the host with \`registerRequiredChildExtensions\` so it loads for every launch.`, ].join("\n"); } return [ `${subject} ran as a background child without the ambient extensions.`, `If ${model} is served by a provider extension or is an extension-registered virtual model, that extension is not loaded for this child: list it in \`subagentOnlyExtensions\` or \`extensions\` in the agent frontmatter, leave \`extensions\` unset so the child loads the ambient extensions, or register it from the host with \`registerRequiredChildExtensions\` so it loads for every launch.`, ].join("\n"); } /** * Explain a local foreground child that failed before any successful response * on a provider it inherited from the parent's registry. The child got the * provider but not the parent extension that registered it, so a provider that * depends on that extension's session hooks can reject the child's requests. * Whether this provider does is unknown here, hence the conditional wording. */ export function formatInheritedProviderDiagnostic(diagnostic: ChildModelResolutionDiagnostic & { provider: string }): string { const subject = diagnostic.agent ? `Agent '${diagnostic.agent}'` : "Subagent"; const model = diagnostic.model ? `'${diagnostic.model}'` : "this model"; const provider = `'${diagnostic.provider}'`; const facts = `${subject} ran as a foreground child, which never loads the parent's ambient extensions. Provider ${provider} for ${model} was registered by a parent extension; the child inherited the provider but not that extension's session hooks.`; if (capabilityCeilingDeniesExtensions(diagnostic)) { return [ facts, `${denyExtensionsClause(diagnostic)}, so that extension cannot load for this child: \`async: true\` does not help either, and listed \`subagentOnlyExtensions\` or \`extensions\` entries are suppressed.`, `If ${provider} depends on those hooks, relax the capability ceiling to allow extensions, or use a model whose provider does not.`, ].join("\n"); } return [ facts, `If ${provider} depends on those hooks, run this agent with \`async: true\` (a background child loads the ambient extensions unless the agent sets \`extensions\`), or load the extension for this agent with \`subagentOnlyExtensions\` or \`extensions\` in the agent frontmatter.`, ].join("\n"); }