{"version":3,"file":"model-runner.d.ts","sourceRoot":"","sources":["../../src/reflect/model-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentMessage, ThinkingLevel } from \"@ch1nyzzz/pi-agent-core\";\nimport {\n\ttype AuthStorage,\n\tcreateAgentSessionFromServices,\n\tcreateAgentSessionServices,\n\tcreateFindToolDefinition,\n\tcreateGrepToolDefinition,\n\tcreateLsToolDefinition,\n\tcreateReadToolDefinition,\n\ttype ModelRegistry,\n\tresolveCliModel,\n\tSessionManager,\n\ttype SessionStats,\n\ttype ToolDefinition,\n} from \"@ch1nyzzz/pi-coding-agent\";\n\nexport type ModelRunStreamEvent =\n\t| { type: \"text\" | \"thinking\" | \"tool-arguments\"; delta: string }\n\t| { type: \"tool-call\"; name: string; arguments: Record<string, unknown> }\n\t| { type: \"tool-result\"; name: string; text: string; isError: boolean }\n\t| { type: \"length-recovery\"; attempt: number; maxAttempts: number }\n\t| { type: \"submission-retry\"; attempt: number; maxAttempts: number }\n\t| {\n\t\t\ttype: \"usage\";\n\t\t\tstopReason: string;\n\t\t\tinput: number;\n\t\t\toutput: number;\n\t\t\tcacheRead: number;\n\t\t\tcacheWrite: number;\n\t  }\n\t| { type: \"complete\"; stopReason: string };\n\n/**\n * Typed result channel: the model delivers its result by calling a schema-validated\n * tool instead of emitting text for the orchestrator to parse. Schema violations and\n * `validate` rejections flow back to the model as tool errors, so retries happen\n * inside the session — free text is never a control signal.\n */\nexport interface ModelRunSubmission {\n\ttoolName: string;\n\tdescription: string;\n\tparameters: ToolDefinition[\"parameters\"];\n\t/**\n\t * Optional semantic validation beyond the schema. Throw to reject the submission\n\t * (the model sees the message and can retry). A non-undefined return value\n\t * replaces the stored submission.\n\t */\n\tvalidate?: (params: Record<string, unknown>) => unknown | Promise<unknown>;\n\t/** Reprompts when a run ends without a submission. Default 2; 0 disables reprompting. */\n\tmaxAttempts?: number;\n}\n\nexport interface ModelRunRequest {\n\tcwd: string;\n\tagentDir?: string;\n\tsystemPrompt: string;\n\tprompt: string;\n\tmodel?: string;\n\tthinkingLevel?: ThinkingLevel;\n\thistory?: readonly AgentMessage[];\n\t/** Explicit built-in/custom tool allowlist. Omit to run with no tools. */\n\ttools?: string[];\n\t/**\n\t * Execution budget per built-in read-family tool call (read/grep/find/ls). Headless\n\t * phases have no human to interrupt a runaway filesystem scan, so an over-budget call\n\t * is aborted and returned to the model as a tool error. Defaults to 120s.\n\t */\n\ttoolTimeoutMs?: number;\n\tcustomTools?: ToolDefinition[];\n\t/** Structured result channel; when set, the run's result is the submitted object. */\n\tsubmission?: ModelRunSubmission;\n\t/** Stable provider session identity used to reuse replay prompt caches. */\n\tsessionIdentity?: string;\n\t/**\n\t * Prompt sent to the same session when a run stops with \"length\" (output space or\n\t * context window exhausted). The session compacts before this prompt is submitted,\n\t * so the retry runs against a freed window. Defaults to a generic finish-now prompt.\n\t */\n\trecoveryPrompt?: string;\n\t/** Maximum recovery prompts after \"length\" stops. Defaults to 2; 0 disables recovery. */\n\tmaxLengthRecoveries?: number;\n\tsignal?: AbortSignal;\n\t/** Receives the live headless-agent stream without retaining it in the model context. */\n\tonStreamEvent?: (event: ModelRunStreamEvent) => void;\n\t/**\n\t * Receives the session stats (and active model, when known) exactly once, even when\n\t * the run fails — callers can persist usage for failed runs.\n\t */\n\tonSessionStats?: (stats: SessionStats, model: { provider: string; id: string } | undefined) => void;\n}\n\nexport interface ModelRunResult {\n\ttext: string;\n\t/** Validated object delivered through the submission tool, when one was requested. */\n\tsubmission?: unknown;\n\tstats: SessionStats;\n\tmodel: {\n\t\tprovider: string;\n\t\tid: string;\n\t};\n}\n\nexport interface ModelRunner {\n\trun(request: ModelRunRequest): Promise<ModelRunResult>;\n}\n\nconst DEFAULT_RECOVERY_PROMPT =\n\t\"Your previous response was cut off because it exhausted the available output space. \" +\n\t\"Produce your complete final answer now. Do not call tools. \" +\n\t\"Keep deliberation brief and output the final deliverable directly.\";\n\nconst DEFAULT_TOOL_TIMEOUT_MS = 120_000;\n\n/** Uniform execution surface: the concrete parameter schemas stay with the factories. */\ntype AnyToolDefinition = ToolDefinition<any, any, any>;\n\n/**\n * Read-family tools traverse arbitrary directory trees, and a headless run has no human\n * to interrupt a scan that wanders onto huge or slow filesystems. The runner shadows the\n * built-ins with copies that enforce a hard execution budget: exceeding it aborts the\n * call (signal-aware tools kill their child process) and the model receives a tool error.\n */\nconst READ_FAMILY_TOOL_FACTORIES: Record<string, (cwd: string) => AnyToolDefinition> = {\n\tread: createReadToolDefinition,\n\tgrep: createGrepToolDefinition,\n\tfind: createFindToolDefinition,\n\tls: createLsToolDefinition,\n};\n\nfunction withExecutionTimeout(definition: AnyToolDefinition, timeoutMs: number): AnyToolDefinition {\n\treturn {\n\t\t...definition,\n\t\tasync execute(toolCallId, params, signal, onUpdate, ctx) {\n\t\t\tconst controller = new AbortController();\n\t\t\tconst forwardAbort = () => controller.abort();\n\t\t\tif (signal?.aborted) controller.abort();\n\t\t\telse signal?.addEventListener(\"abort\", forwardAbort, { once: true });\n\t\t\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\t\t\ttry {\n\t\t\t\treturn await Promise.race([\n\t\t\t\t\tdefinition.execute(toolCallId, params, controller.signal, onUpdate, ctx),\n\t\t\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\t\t\tcontroller.signal.addEventListener(\n\t\t\t\t\t\t\t\"abort\",\n\t\t\t\t\t\t\t() => reject(new Error(`${definition.name} execution aborted`)),\n\t\t\t\t\t\t\t{ once: true },\n\t\t\t\t\t\t);\n\t\t\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\t\t\treject(\n\t\t\t\t\t\t\t\tnew Error(`${definition.name} execution exceeded the ${Math.round(timeoutMs / 1000)}s limit`),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tcontroller.abort();\n\t\t\t\t\t\t}, timeoutMs);\n\t\t\t\t\t}),\n\t\t\t\t]);\n\t\t\t} finally {\n\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\tsignal?.removeEventListener(\"abort\", forwardAbort);\n\t\t\t}\n\t\t},\n\t};\n}\n\nexport interface PiModelRunnerOptions {\n\t/** Optional shared auth backend, primarily for embedded runtimes and tests. */\n\tauthStorage?: AuthStorage;\n\t/** Optional shared model registry, primarily for embedded runtimes and tests. */\n\tmodelRegistry?: ModelRegistry;\n}\n\nexport function createPiModelRunner(options: PiModelRunnerOptions = {}): ModelRunner {\n\treturn {\n\t\tasync run(request: ModelRunRequest): Promise<ModelRunResult> {\n\t\t\tconst services = await createAgentSessionServices({\n\t\t\t\tcwd: request.cwd,\n\t\t\t\tagentDir: request.agentDir,\n\t\t\t\tauthStorage: options.authStorage,\n\t\t\t\tmodelRegistry: options.modelRegistry,\n\t\t\t\tresourceLoaderOptions: {\n\t\t\t\t\tnoExtensions: true,\n\t\t\t\t\tnoSkills: true,\n\t\t\t\t\tnoPromptTemplates: true,\n\t\t\t\t\tnoThemes: true,\n\t\t\t\t\tnoContextFiles: true,\n\t\t\t\t\tsystemPrompt: request.systemPrompt,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tconst diagnosticErrors = services.diagnostics.filter((diagnostic) => diagnostic.type === \"error\");\n\t\t\tif (diagnosticErrors.length > 0) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Could not create model runner services: ${diagnosticErrors.map((diagnostic) => diagnostic.message).join(\"; \")}`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst resolvedModel = request.model\n\t\t\t\t? resolveCliModel({\n\t\t\t\t\t\tcliModel: request.model,\n\t\t\t\t\t\tcliThinking: request.thinkingLevel,\n\t\t\t\t\t\tmodelRegistry: services.modelRegistry,\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\t\tif (resolvedModel?.error) {\n\t\t\t\tthrow new Error(resolvedModel.error);\n\t\t\t}\n\n\t\t\tlet submitted: { value: unknown } | undefined;\n\t\t\tconst submissionTool: ToolDefinition | undefined = request.submission\n\t\t\t\t? {\n\t\t\t\t\t\tname: request.submission.toolName,\n\t\t\t\t\t\tlabel: request.submission.toolName,\n\t\t\t\t\t\tdescription: request.submission.description,\n\t\t\t\t\t\tparameters: request.submission.parameters,\n\t\t\t\t\t\tasync execute(_toolCallId, params) {\n\t\t\t\t\t\t\tconst validated = await request.submission?.validate?.(params as Record<string, unknown>);\n\t\t\t\t\t\t\tsubmitted = { value: validated === undefined ? params : validated };\n\t\t\t\t\t\t\treturn { content: [{ type: \"text\", text: \"Submission accepted.\" }], details: {} };\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: undefined;\n\t\t\tconst toolNames = [...(request.tools ?? []), ...(submissionTool ? [submissionTool.name] : [])];\n\t\t\tconst toolTimeoutMs = request.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;\n\t\t\tconst timedReadTools = (request.tools ?? [])\n\t\t\t\t.map((name) => READ_FAMILY_TOOL_FACTORIES[name]?.(request.cwd))\n\t\t\t\t.filter((definition) => definition !== undefined)\n\t\t\t\t.map((definition) => withExecutionTimeout(definition, toolTimeoutMs));\n\t\t\tconst customTools = [\n\t\t\t\t...timedReadTools,\n\t\t\t\t...(request.customTools ?? []),\n\t\t\t\t...(submissionTool ? [submissionTool] : []),\n\t\t\t];\n\n\t\t\tconst sessionManager = SessionManager.inMemory(request.cwd, { id: request.sessionIdentity });\n\t\t\tconst { session, modelFallbackMessage } = await createAgentSessionFromServices({\n\t\t\t\tservices,\n\t\t\t\tsessionManager,\n\t\t\t\tmodel: resolvedModel?.model,\n\t\t\t\tthinkingLevel: request.thinkingLevel ?? resolvedModel?.thinkingLevel,\n\t\t\t\t...(toolNames.length > 0 ? { tools: toolNames } : { noTools: \"all\" as const }),\n\t\t\t\t...(customTools.length > 0 ? { customTools } : {}),\n\t\t\t});\n\t\t\tconst unsubscribe = request.onStreamEvent\n\t\t\t\t? session.subscribe((event) => {\n\t\t\t\t\t\tif (event.type === \"message_end\") {\n\t\t\t\t\t\t\tconst message = event.message;\n\t\t\t\t\t\t\tif (message.role === \"assistant\" && message.usage) {\n\t\t\t\t\t\t\t\trequest.onStreamEvent?.({\n\t\t\t\t\t\t\t\t\ttype: \"usage\",\n\t\t\t\t\t\t\t\t\tstopReason: message.stopReason,\n\t\t\t\t\t\t\t\t\tinput: message.usage.input,\n\t\t\t\t\t\t\t\t\toutput: message.usage.output,\n\t\t\t\t\t\t\t\t\tcacheRead: message.usage.cacheRead,\n\t\t\t\t\t\t\t\t\tcacheWrite: message.usage.cacheWrite,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.type === \"tool_execution_end\") {\n\t\t\t\t\t\t\tconst text = Array.isArray(event.result?.content)\n\t\t\t\t\t\t\t\t? event.result.content\n\t\t\t\t\t\t\t\t\t\t.filter((item: { type?: string }) => item.type === \"text\")\n\t\t\t\t\t\t\t\t\t\t.map((item: { text?: string }) => item.text ?? \"\")\n\t\t\t\t\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t\t\t\t: String(event.result ?? \"\");\n\t\t\t\t\t\t\trequest.onStreamEvent?.({\n\t\t\t\t\t\t\t\ttype: \"tool-result\",\n\t\t\t\t\t\t\t\tname: event.toolName,\n\t\t\t\t\t\t\t\ttext: text.slice(0, 8_000),\n\t\t\t\t\t\t\t\tisError: event.isError,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.type !== \"message_update\") return;\n\t\t\t\t\t\tconst update = event.assistantMessageEvent;\n\t\t\t\t\t\tif (update.type === \"text_delta\") request.onStreamEvent?.({ type: \"text\", delta: update.delta });\n\t\t\t\t\t\telse if (update.type === \"thinking_delta\") {\n\t\t\t\t\t\t\trequest.onStreamEvent?.({ type: \"thinking\", delta: update.delta });\n\t\t\t\t\t\t} else if (update.type === \"toolcall_delta\") {\n\t\t\t\t\t\t\trequest.onStreamEvent?.({ type: \"tool-arguments\", delta: update.delta });\n\t\t\t\t\t\t} else if (update.type === \"toolcall_end\") {\n\t\t\t\t\t\t\trequest.onStreamEvent?.({\n\t\t\t\t\t\t\t\ttype: \"tool-call\",\n\t\t\t\t\t\t\t\tname: update.toolCall.name,\n\t\t\t\t\t\t\t\targuments: update.toolCall.arguments,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\t\tconst abortSession = () => {\n\t\t\t\tvoid session.abort();\n\t\t\t};\n\t\t\trequest.signal?.addEventListener(\"abort\", abortSession, { once: true });\n\n\t\t\ttry {\n\t\t\t\tif (request.signal?.aborted) {\n\t\t\t\t\tthrow request.signal.reason instanceof Error ? request.signal.reason : new Error(\"Model run aborted\");\n\t\t\t\t}\n\t\t\t\tconst activeModel = session.model;\n\t\t\t\tif (!activeModel) {\n\t\t\t\t\tthrow new Error(modelFallbackMessage ?? \"No model is available for the model runner\");\n\t\t\t\t}\n\n\t\t\t\t// The SDK appends deterministic date/cwd metadata to this fixed role prompt.\n\t\t\t\tsession.agent.state.messages = request.history ? [...request.history] : [];\n\n\t\t\t\tawait session.prompt(request.prompt, { expandPromptTemplates: false });\n\n\t\t\t\tlet finalMessage = session.messages.at(-1);\n\t\t\t\tif (!finalMessage || finalMessage.role !== \"assistant\") {\n\t\t\t\t\tthrow new Error(\"Model run completed without a final assistant message\");\n\t\t\t\t}\n\n\t\t\t\t// A provider-level failure ends the turn for good. Surface its message\n\t\t\t\t// immediately: recovery and submission reprompts cannot succeed on a session\n\t\t\t\t// whose requests keep failing, and they masked the real error.\n\t\t\t\tconst throwIfTerminated = (message: { stopReason: string; errorMessage?: string }) => {\n\t\t\t\t\tif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\t\t\t\t\t\tconst detail = message.errorMessage ? `: ${message.errorMessage}` : \"\";\n\t\t\t\t\t\tthrow new Error(`Model run ended with stop reason \"${message.stopReason}\"${detail}`);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tthrowIfTerminated(finalMessage);\n\n\t\t\t\t// A \"length\" stop means the output space or context window ran out. The session\n\t\t\t\t// survives, so recover in place: the pre-prompt compaction check frees the window\n\t\t\t\t// (overflow-shaped usage triggers it) and a fresh turn gets a fresh output budget.\n\t\t\t\tconst maxRecoveries = request.maxLengthRecoveries ?? 2;\n\t\t\t\tlet recoveryAttempt = 0;\n\t\t\t\twhile (\n\t\t\t\t\tfinalMessage.stopReason === \"length\" &&\n\t\t\t\t\trecoveryAttempt < maxRecoveries &&\n\t\t\t\t\t!request.signal?.aborted\n\t\t\t\t) {\n\t\t\t\t\trecoveryAttempt += 1;\n\t\t\t\t\trequest.onStreamEvent?.({\n\t\t\t\t\t\ttype: \"length-recovery\",\n\t\t\t\t\t\tattempt: recoveryAttempt,\n\t\t\t\t\t\tmaxAttempts: maxRecoveries,\n\t\t\t\t\t});\n\t\t\t\t\tawait session.prompt(request.recoveryPrompt ?? DEFAULT_RECOVERY_PROMPT, {\n\t\t\t\t\t\texpandPromptTemplates: false,\n\t\t\t\t\t});\n\t\t\t\t\tconst recovered = session.messages.at(-1);\n\t\t\t\t\tif (!recovered || recovered.role !== \"assistant\") {\n\t\t\t\t\t\tthrow new Error(\"Model run recovery completed without a final assistant message\");\n\t\t\t\t\t}\n\t\t\t\t\tfinalMessage = recovered;\n\t\t\t\t\tthrowIfTerminated(recovered);\n\t\t\t\t}\n\n\t\t\t\t// The submission is the result; a run that ends without one gets bounded,\n\t\t\t\t// explicit reprompts inside the same session.\n\t\t\t\tif (request.submission) {\n\t\t\t\t\tconst maxAttempts = request.submission.maxAttempts ?? 2;\n\t\t\t\t\tlet submissionAttempt = 0;\n\t\t\t\t\twhile (!submitted && submissionAttempt < maxAttempts && !request.signal?.aborted) {\n\t\t\t\t\t\tsubmissionAttempt += 1;\n\t\t\t\t\t\trequest.onStreamEvent?.({ type: \"submission-retry\", attempt: submissionAttempt, maxAttempts });\n\t\t\t\t\t\tawait session.prompt(\n\t\t\t\t\t\t\t`You have not delivered your result yet. Call the ${request.submission.toolName} tool now with your complete final result. Do not output anything else.`,\n\t\t\t\t\t\t\t{ expandPromptTemplates: false },\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst reprompted = session.messages.at(-1);\n\t\t\t\t\t\tif (!reprompted || reprompted.role !== \"assistant\") {\n\t\t\t\t\t\t\tthrow new Error(\"Submission reprompt completed without a final assistant message\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfinalMessage = reprompted;\n\t\t\t\t\t\tthrowIfTerminated(reprompted);\n\t\t\t\t\t}\n\t\t\t\t\tif (!submitted) {\n\t\t\t\t\t\tthrow new Error(`Model run ended without a ${request.submission.toolName} submission`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\trequest.onStreamEvent?.({ type: \"complete\", stopReason: finalMessage.stopReason });\n\t\t\t\tif (finalMessage.stopReason !== \"stop\") {\n\t\t\t\t\tconst detail = finalMessage.errorMessage ? `: ${finalMessage.errorMessage}` : \"\";\n\t\t\t\t\tconst recoveryNote =\n\t\t\t\t\t\trecoveryAttempt > 0\n\t\t\t\t\t\t\t? ` after ${recoveryAttempt} length ${recoveryAttempt === 1 ? \"recovery\" : \"recoveries\"}`\n\t\t\t\t\t\t\t: \"\";\n\t\t\t\t\tthrow new Error(`Model run ended with stop reason \"${finalMessage.stopReason}\"${recoveryNote}${detail}`);\n\t\t\t\t}\n\n\t\t\t\tconst text = finalMessage.content\n\t\t\t\t\t.filter((block) => block.type === \"text\")\n\t\t\t\t\t.map((block) => block.text)\n\t\t\t\t\t.join(\"\")\n\t\t\t\t\t.trim();\n\t\t\t\tif (!text && !submitted) {\n\t\t\t\t\tthrow new Error(\"Model run completed without text output\");\n\t\t\t\t}\n\n\t\t\t\treturn {\n\t\t\t\t\ttext,\n\t\t\t\t\t...(submitted ? { submission: submitted.value } : {}),\n\t\t\t\t\tstats: session.getSessionStats(),\n\t\t\t\t\tmodel: { provider: activeModel.provider, id: activeModel.id },\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\trequest.signal?.removeEventListener(\"abort\", abortSession);\n\t\t\t\ttry {\n\t\t\t\t\tconst activeModel = session.model;\n\t\t\t\t\trequest.onSessionStats?.(\n\t\t\t\t\t\tsession.getSessionStats(),\n\t\t\t\t\t\tactiveModel ? { provider: activeModel.provider, id: activeModel.id } : undefined,\n\t\t\t\t\t);\n\t\t\t\t} catch {\n\t\t\t\t\t// Stats reporting must never mask the primary result or error.\n\t\t\t\t}\n\t\t\t\tunsubscribe?.();\n\t\t\t\tsession.dispose();\n\t\t\t}\n\t\t},\n\t};\n}\n"]}