{"version":3,"file":"subagent-result.d.ts","sourceRoot":"","sources":["../../src/core/subagent-result.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAIpE,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEpE,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,oFAAoF;IACpF,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC1C,4FAA4F;IAC5F,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,gBAAgB,EAAE,CAsB1E;AAiED,gFAAgF;AAChF,MAAM,WAAW,0BAA0B;IAC1C,iGAAiG;IACjG,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,SAAS,YAAY,EAAE,EACjC,KAAK,CAAC,EAAE,aAAa,EACrB,OAAO,CAAC,EAAE,0BAA0B,GAClC,kBAAkB,CAkCpB;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAUjG","sourcesContent":["/**\n * Writes a subagent's `result.json` audit file.\n *\n * When a subagent runs as a spawned child process (`--task-id <id>`), the parent\n * SubagentPool verifies `.hoocode/dispatch/<task_id>/result.json` against a fixed\n * schema (see OutputVerifier). This module derives that file deterministically\n * from the finished session so subagents never have to write it themselves.\n */\n\nimport { join } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage } from \"@kolisachint/hoocode-ai\";\nimport { getDispatchTaskDir } from \"../config.js\";\nimport { writeFileAtomicSync } from \"../utils/atomic-file.js\";\nimport type { Task, TaskSource, TaskStatus } from \"./task-store.js\";\n\nexport interface SubagentUsage {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/**\n * One node in a subagent's task subtree, propagated to the parent so nested\n * delegation (a subagent that spawns a subagent) is visible above the process\n * boundary. Serialized from the child's task store at session end and merged\n * under the dispatching task by the parent (see finalizeDispatchResult). The\n * `children` are the child's own delegations/MCP calls, recursively.\n */\nexport interface SubagentTaskNode {\n\tid: number;\n\ttitle: string;\n\tstatus: TaskStatus;\n\t/** Origin of the task (subagent/mcp; unset = the subagent's own TodoWrite plan). */\n\tsource?: TaskSource;\n\tsubagentMode?: string;\n\tusage?: SubagentUsage;\n\tchildren: SubagentTaskNode[];\n}\n\nexport interface SubagentResultFile {\n\tsummary: string;\n\tfiles_changed: string[];\n\tconfidence: number;\n\tstatus: \"complete\" | \"partial\" | \"failed\";\n\t/** Token and cost usage for the subagent session (extra field; ignored by the verifier). */\n\tusage?: SubagentUsage;\n\t/**\n\t * The child's own task subtree (its TodoWrite plan, delegations, and MCP calls)\n\t * so the parent can render nested work below the dispatching task. Extra field;\n\t * ignored by the verifier.\n\t */\n\ttask_tree?: SubagentTaskNode[];\n}\n\n/**\n * Build the nested task forest from a flat task list, linking children to\n * parents via `parentTaskId`. Roots are tasks with no `parentTaskId`; each\n * node's children preserve store order. Used to serialize a subagent's task\n * store into its `result.json` for the parent to merge.\n */\nexport function buildTaskForest(tasks: readonly Task[]): SubagentTaskNode[] {\n\tconst childrenByParent = new Map<number, Task[]>();\n\tconst roots: Task[] = [];\n\tfor (const task of tasks) {\n\t\tif (task.parentTaskId === undefined) {\n\t\t\troots.push(task);\n\t\t\tcontinue;\n\t\t}\n\t\tconst siblings = childrenByParent.get(task.parentTaskId);\n\t\tif (siblings) siblings.push(task);\n\t\telse childrenByParent.set(task.parentTaskId, [task]);\n\t}\n\tconst toNode = (task: Task): SubagentTaskNode => ({\n\t\tid: task.id,\n\t\ttitle: task.title,\n\t\tstatus: task.status,\n\t\tsource: task.source,\n\t\tsubagentMode: task.subagentMode,\n\t\tusage: task.usage,\n\t\tchildren: (childrenByParent.get(task.id) ?? []).map(toNode),\n\t});\n\treturn roots.map(toNode);\n}\n\n/** Tool names that mutate files. Their `path`/`file_path` argument is a changed file. */\nconst MUTATING_TOOLS = new Set([\"edit\", \"write\"]);\n\n/** Collect distinct file paths touched by edit/write tool calls across the session. */\nfunction collectChangedFiles(messages: readonly AgentMessage[]): string[] {\n\tconst files = new Set<string>();\n\tfor (const message of messages) {\n\t\tif (message.role !== \"assistant\") continue;\n\t\tfor (const content of (message as AssistantMessage).content) {\n\t\t\tif (content.type !== \"toolCall\") continue;\n\t\t\tif (!MUTATING_TOOLS.has(content.name)) continue;\n\t\t\tconst args = content.arguments ?? {};\n\t\t\tconst path =\n\t\t\t\ttypeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\t\t\tif (path) files.add(path);\n\t\t}\n\t}\n\treturn [...files];\n}\n\n/** Last assistant text, trimmed, as the summary. */\nfunction deriveSummary(messages: readonly AgentMessage[]): string {\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst message = messages[i];\n\t\tif (message.role !== \"assistant\") continue;\n\t\tconst assistant = message as AssistantMessage;\n\t\tlet text = \"\";\n\t\tfor (const content of assistant.content) {\n\t\t\tif (content.type === \"text\") text += content.text;\n\t\t}\n\t\tconst trimmed = text.trim();\n\t\tif (trimmed) return trimmed;\n\t}\n\treturn \"\";\n}\n\n/** Derive the terminal status from the final assistant message. */\nfunction deriveStatus(messages: readonly AgentMessage[]): \"complete\" | \"failed\" {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" || assistant.stopReason === \"aborted\") return \"failed\";\n\t}\n\treturn \"complete\";\n}\n\n/**\n * Concrete error text from the final assistant message when the run ended in an\n * error (e.g. a provider usage/quota or rate-limit message). Surfacing this in\n * the summary lets the parent report the real cause instead of \"subagent failed\".\n */\nfunction deriveErrorMessage(messages: readonly AgentMessage[]): string | undefined {\n\tconst last = messages[messages.length - 1];\n\tif (last?.role === \"assistant\") {\n\t\tconst assistant = last as AssistantMessage;\n\t\tif (assistant.stopReason === \"error\" && assistant.errorMessage) {\n\t\t\tconst trimmed = assistant.errorMessage.trim();\n\t\t\tif (trimmed) return trimmed;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/** Extra build options that override the status derived from the transcript. */\nexport interface BuildSubagentResultOptions {\n\t/** The run was stopped at its turn cap. Report a usable partial result rather than a failure. */\n\treachedMaxTurns?: boolean;\n}\n\n/**\n * Build the `result.json` payload for a finished subagent session.\n *\n * The verifier requires a non-empty summary and confidence >= 0.5, so a\n * successful run with no assistant text still yields a usable summary.\n *\n * When the run was stopped at its turn cap (`reachedMaxTurns`), the result is\n * reported as `partial` with whatever summary the agent managed to produce,\n * instead of the `failed` status an aborted final message would otherwise yield.\n */\nexport function buildSubagentResult(\n\tmessages: readonly AgentMessage[],\n\tusage?: SubagentUsage,\n\toptions?: BuildSubagentResultOptions,\n): SubagentResultFile {\n\tif (options?.reachedMaxTurns) {\n\t\treturn {\n\t\t\tsummary: deriveSummary(messages) || \"Reached the turn limit before completing. Returning partial findings.\",\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.6,\n\t\t\tstatus: \"partial\",\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst status = deriveStatus(messages);\n\tif (status === \"failed\") {\n\t\t// Prefer the provider/model error over any partial assistant text so the\n\t\t// caller sees the real cause (e.g. \"usage limit reached\").\n\t\tconst errorMessage = deriveErrorMessage(messages);\n\t\tconst summary = errorMessage ? `Task failed: ${errorMessage}` : deriveSummary(messages) || \"Task failed.\";\n\t\treturn {\n\t\t\tsummary,\n\t\t\tfiles_changed: collectChangedFiles(messages),\n\t\t\tconfidence: 0.5,\n\t\t\tstatus,\n\t\t\tusage,\n\t\t};\n\t}\n\n\tconst summary = deriveSummary(messages) || \"Task completed with no textual summary.\";\n\treturn {\n\t\tsummary,\n\t\tfiles_changed: collectChangedFiles(messages),\n\t\tconfidence: 0.9,\n\t\tstatus,\n\t\tusage,\n\t};\n}\n\n/** Write `result.json` for a task. Best-effort: never throws. */\nexport function writeSubagentResult(cwd: string, taskId: string, result: SubagentResultFile): void {\n\tconst path = join(getDispatchTaskDir(cwd, taskId), \"result.json\");\n\ttry {\n\t\t// Atomic (temp + same-dir rename): the parent may read this file the moment\n\t\t// it appears, or SIGKILL the child mid-write — a torn result.json would be\n\t\t// parsed as a verification failure and turn a genuine success into a failure.\n\t\twriteFileAtomicSync(path, JSON.stringify(result, null, 2));\n\t} catch {\n\t\t// Audit file is best-effort; the parent treats a missing file as a failure.\n\t}\n}\n"]}