{"version":3,"file":"print-mode.d.ts","sourceRoot":"","sources":["../../src/modes/print-mode.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAU5E;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,yEAAyE;IACzE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAC/B,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,mBAAmB,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqM/G","sourcesContent":["/**\n * Print mode (single-shot): Send prompts, output result, exit.\n *\n * Used for:\n * - `hoocode -p \"prompt\"` - text output\n * - `hoocode --mode json \"prompt\"` - JSON event stream\n */\n\nimport type { AssistantMessage, ImageContent } from \"@kolisachint/hoocode-ai\";\nimport type { AgentSessionRuntime } from \"../core/agent-session-runtime.js\";\nimport { flushRawStdout, writeRawStdout } from \"../core/output-guard.js\";\nimport { SUBAGENT_STDOUT_EVENT_TYPES } from \"../core/subagent-events.js\";\nimport { buildSubagentResult, buildTaskForest, writeSubagentResult } from \"../core/subagent-result.js\";\nimport { taskStore } from \"../core/task-store.js\";\nimport { killTrackedDetachedChildren } from \"../utils/shell.js\";\n\n/** Heartbeat cadence for spawned subagents (parent lifeguard stalls after 60s of silence). */\nconst SUBAGENT_HEARTBEAT_MS = 30000;\n\n/**\n * Options for print mode.\n */\nexport interface PrintModeOptions {\n\t/** Output mode: \"text\" for final response only, \"json\" for all events */\n\tmode: \"text\" | \"json\";\n\t/** Array of additional prompts to send after initialMessage */\n\tmessages?: string[];\n\t/** First message to send (may contain @file content) */\n\tinitialMessage?: string;\n\t/** Images to attach to the initial message */\n\tinitialImages?: ImageContent[];\n\t/** Internal: set when this process is a spawned subagent. Enables heartbeats and result.json. */\n\ttaskId?: string;\n\t/** Hard cap on assistant turns. Near the cap the agent is asked to wrap up; at the cap it is aborted. */\n\tmaxTurns?: number;\n}\n\n/**\n * Run in print (single-shot) mode.\n * Sends prompts to the agent and outputs the result.\n */\nexport async function runPrintMode(runtimeHost: AgentSessionRuntime, options: PrintModeOptions): Promise<number> {\n\tconst { mode, messages = [], initialMessage, initialImages, taskId, maxTurns } = options;\n\tlet exitCode = 0;\n\tlet session = runtimeHost.session;\n\tlet unsubscribe: (() => void) | undefined;\n\tlet disposed = false;\n\n\t// Spawned subagents (json mode + task id) emit a periodic heartbeat so the\n\t// parent lifeguard does not treat a long-thinking turn as a stall.\n\tconst isSubagent = mode === \"json\" && typeof taskId === \"string\" && taskId.length > 0;\n\tlet heartbeat: NodeJS.Timeout | undefined;\n\tif (isSubagent) {\n\t\t// Emit an immediate ping so a child that crashes during startup surfaces its\n\t\t// real error instead of stalling silently until the first delayed heartbeat.\n\t\twriteRawStdout(`${JSON.stringify({ ping: true })}\\n`);\n\t\theartbeat = setInterval(() => {\n\t\t\twriteRawStdout(`${JSON.stringify({ ping: true })}\\n`);\n\t\t}, SUBAGENT_HEARTBEAT_MS);\n\t\theartbeat.unref();\n\t}\n\t// Turn-limit enforcement for spawned subagents: ask the agent to wrap up near\n\t// the cap, then hard-stop at the cap so a runaway agent always terminates.\n\tlet reachedMaxTurns = false;\n\tlet turnLimitUnsub: (() => void) | undefined;\n\tconst signalCleanupHandlers: Array<() => void> = [];\n\n\tconst disposeRuntime = async (): Promise<void> => {\n\t\tif (disposed) return;\n\t\tdisposed = true;\n\t\tunsubscribe?.();\n\t\tawait runtimeHost.dispose();\n\t};\n\n\tconst registerSignalHandlers = (): void => {\n\t\tconst signals: NodeJS.Signals[] = [\"SIGTERM\"];\n\t\tif (process.platform !== \"win32\") {\n\t\t\tsignals.push(\"SIGHUP\");\n\t\t}\n\n\t\tfor (const signal of signals) {\n\t\t\tconst handler = () => {\n\t\t\t\tkillTrackedDetachedChildren();\n\t\t\t\tvoid disposeRuntime().finally(() => {\n\t\t\t\t\tprocess.exit(signal === \"SIGHUP\" ? 129 : 143);\n\t\t\t\t});\n\t\t\t};\n\t\t\tprocess.on(signal, handler);\n\t\t\tsignalCleanupHandlers.push(() => process.off(signal, handler));\n\t\t}\n\t};\n\n\tregisterSignalHandlers();\n\n\truntimeHost.setRebindSession(async () => {\n\t\tawait rebindSession();\n\t});\n\n\tconst rebindSession = async (): Promise<void> => {\n\t\tsession = runtimeHost.session;\n\t\tawait session.bindExtensions({\n\t\t\tcommandContextActions: {\n\t\t\t\twaitForIdle: () => session.agent.waitForIdle(),\n\t\t\t\tnewSession: async (newSessionOptions) => runtimeHost.newSession(newSessionOptions),\n\t\t\t\tfork: async (entryId, forkOptions) => {\n\t\t\t\t\tconst result = await runtimeHost.fork(entryId, forkOptions);\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tnavigateTree: async (targetId, navigateOptions) => {\n\t\t\t\t\tconst result = await session.navigateTree(targetId, {\n\t\t\t\t\t\tsummarize: navigateOptions?.summarize,\n\t\t\t\t\t\tcustomInstructions: navigateOptions?.customInstructions,\n\t\t\t\t\t\treplaceInstructions: navigateOptions?.replaceInstructions,\n\t\t\t\t\t\tlabel: navigateOptions?.label,\n\t\t\t\t\t});\n\t\t\t\t\treturn { cancelled: result.cancelled };\n\t\t\t\t},\n\t\t\t\tswitchSession: async (sessionPath, switchOptions) => {\n\t\t\t\t\treturn runtimeHost.switchSession(sessionPath, switchOptions);\n\t\t\t\t},\n\t\t\t\treload: async () => {\n\t\t\t\t\tawait session.reload();\n\t\t\t\t},\n\t\t\t},\n\t\t\tonError: (err) => {\n\t\t\t\tconsole.error(`Extension error (${err.extensionPath}): ${err.error}`);\n\t\t\t},\n\t\t});\n\n\t\tunsubscribe?.();\n\t\tunsubscribe = session.subscribe((event) => {\n\t\t\tif (mode !== \"json\") return;\n\t\t\t// A spawned subagent's parent only consumes progress events + message_end\n\t\t\t// usage from this stream (see SUBAGENT_STDOUT_EVENT_TYPES); dropping the\n\t\t\t// per-delta firehose at the source keeps it off the parent's UI event\n\t\t\t// loop. A top-level `--json` consumer still receives the full stream.\n\t\t\tif (isSubagent && !SUBAGENT_STDOUT_EVENT_TYPES.has(event.type)) return;\n\t\t\twriteRawStdout(`${JSON.stringify(event)}\\n`);\n\t\t});\n\t};\n\n\ttry {\n\t\tif (mode === \"json\") {\n\t\t\tconst header = session.sessionManager.getHeader();\n\t\t\tif (header) {\n\t\t\t\twriteRawStdout(`${JSON.stringify(header)}\\n`);\n\t\t\t}\n\t\t}\n\n\t\tawait rebindSession();\n\n\t\tif (isSubagent && typeof maxTurns === \"number\" && maxTurns > 0) {\n\t\t\tconst cap = maxTurns;\n\t\t\tconst wrapUpAt = Math.floor(cap * 0.9);\n\t\t\tlet turns = 0;\n\t\t\tlet warned = false;\n\t\t\tturnLimitUnsub = session.subscribe((event) => {\n\t\t\t\tif (event.type !== \"turn_end\") return;\n\t\t\t\tturns += 1;\n\t\t\t\tif (turns >= cap) {\n\t\t\t\t\tif (!reachedMaxTurns) {\n\t\t\t\t\t\treachedMaxTurns = true;\n\t\t\t\t\t\tvoid session.abort();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (!warned && wrapUpAt >= 1 && wrapUpAt < cap && turns >= wrapUpAt) {\n\t\t\t\t\twarned = true;\n\t\t\t\t\tvoid session.steer(\n\t\t\t\t\t\t`You are at turn ${turns} of your ${cap}-turn limit. Stop investigating or making changes now and write your final summary of findings and results in your next message.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tif (initialMessage) {\n\t\t\tawait session.prompt(initialMessage, { images: initialImages });\n\t\t}\n\n\t\tfor (const message of messages) {\n\t\t\tawait session.prompt(message);\n\t\t}\n\n\t\t// Spawned subagents write the audit file the parent pool verifies.\n\t\tif (isSubagent && taskId) {\n\t\t\tconst stats = session.getSessionStats();\n\t\t\tconst result = buildSubagentResult(\n\t\t\t\tsession.state.messages,\n\t\t\t\t{\n\t\t\t\t\tinput: stats.tokens.input,\n\t\t\t\t\toutput: stats.tokens.output,\n\t\t\t\t\tcacheRead: stats.tokens.cacheRead,\n\t\t\t\t\tcacheWrite: stats.tokens.cacheWrite,\n\t\t\t\t\tcost: stats.cost,\n\t\t\t\t},\n\t\t\t\t{ reachedMaxTurns },\n\t\t\t);\n\t\t\t// Propagate this subagent's own task subtree (its TodoWrite plan plus any\n\t\t\t// nested delegations/MCP calls) so the parent can render work deeper than\n\t\t\t// one level under the dispatching task.\n\t\t\tconst tree = buildTaskForest(taskStore.list());\n\t\t\tif (tree.length > 0) result.task_tree = tree;\n\t\t\twriteSubagentResult(session.sessionManager.getCwd(), taskId, result);\n\t\t\tif (result.status === \"failed\") exitCode = 1;\n\t\t}\n\n\t\tif (mode === \"text\") {\n\t\t\tconst state = session.state;\n\t\t\tconst lastMessage = state.messages[state.messages.length - 1];\n\n\t\t\tif (lastMessage?.role === \"assistant\") {\n\t\t\t\tconst assistantMsg = lastMessage as AssistantMessage;\n\t\t\t\tif (assistantMsg.stopReason === \"error\" || assistantMsg.stopReason === \"aborted\") {\n\t\t\t\t\tconsole.error(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);\n\t\t\t\t\texitCode = 1;\n\t\t\t\t} else {\n\t\t\t\t\tfor (const content of assistantMsg.content) {\n\t\t\t\t\t\tif (content.type === \"text\") {\n\t\t\t\t\t\t\twriteRawStdout(`${content.text}\\n`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn exitCode;\n\t} catch (error: unknown) {\n\t\tconsole.error(error instanceof Error ? error.message : String(error));\n\t\treturn 1;\n\t} finally {\n\t\tif (heartbeat) clearInterval(heartbeat);\n\t\tturnLimitUnsub?.();\n\t\tfor (const cleanup of signalCleanupHandlers) {\n\t\t\tcleanup();\n\t\t}\n\t\tawait disposeRuntime();\n\t\tawait flushRawStdout();\n\t}\n}\n"]}