{"version":3,"file":"goal-driver.d.ts","sourceRoot":"","sources":["../../../src/missions/goal-driver.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAiC,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAOtF,MAAM,WAAW,sBAAsB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,YAAY,CAAC;CACpB;AA4GD,wBAAgB,8BAA8B,CAAC,KAAK,EAAE;IACrD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,aAAa,EAAE,CAAC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,sBAAsB,EAAE,CA2C3B","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { RetainedChild } from \"../runs/background/retained-children.ts\";\nimport type { ControlEvent } from \"../shared/types.ts\";\nimport { listMissions, readMission, updateMission } from \"./store.ts\";\nimport type { MissionRecord, MissionRunLink, MissionStoreLocation } from \"./types.ts\";\nimport { missionStatePath } from \"./workflow-state.ts\";\n\nconst TERMINAL_MISSION_STATUSES = new Set([\"completed\", \"failed\", \"cancelled\"]);\nconst ACTIVE_RUN_STATUSES = new Set([\"queued\", \"running\", \"active\"]);\nconst MAX_ACTION_LENGTH = 180;\n\nexport interface GoalContinuationNotice {\n\tmissionId: string;\n\tmessage: string;\n\tevent: ControlEvent;\n}\n\nfunction bounded(value: string): string {\n\tconst normalized = value.replace(/\\s+/g, \" \").trim();\n\treturn normalized.length > MAX_ACTION_LENGTH ? `${normalized.slice(0, MAX_ACTION_LENGTH - 1)}…` : normalized;\n}\n\nfunction tokenUsage(value: unknown): number | undefined {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n\tconst total = (value as { total?: unknown }).total;\n\treturn Number.isSafeInteger(total) && (total as number) >= 0 ? (total as number) : undefined;\n}\n\nfunction readLinkedRun(run: MissionRunLink): MissionRunLink {\n\tif (!run.asyncDir) return run;\n\tconst statusPath = path.join(run.asyncDir, \"status.json\");\n\tif (!fs.existsSync(statusPath)) return run;\n\tconst status = JSON.parse(fs.readFileSync(statusPath, \"utf-8\")) as Record<string, unknown>;\n\tif (typeof status.state !== \"string\" || !status.state.trim())\n\t\tthrow new Error(`Linked run status '${statusPath}' is missing state`);\n\tconst tokens =\n\t\ttokenUsage(status.totalTokens) ??\n\t\t(Array.isArray(status.steps)\n\t\t\t? status.steps.reduce((total, step) => {\n\t\t\t\t\tif (!step || typeof step !== \"object\") return total;\n\t\t\t\t\treturn total + (tokenUsage((step as { tokens?: unknown }).tokens) ?? 0);\n\t\t\t\t}, 0)\n\t\t\t: undefined);\n\treturn {\n\t\t...run,\n\t\tstatus: status.state,\n\t\t...(!ACTIVE_RUN_STATUSES.has(status.state) && !run.completedAt ? { completedAt: new Date().toISOString() } : {}),\n\t\t...(tokens !== undefined ? { usage: { tokens } } : {}),\n\t};\n}\n\nfunction refreshGoalMission(location: MissionStoreLocation, record: MissionRecord): MissionRecord {\n\tconst runs = record.runs.map(readLinkedRun);\n\tconst changed = runs.some((run, index) => JSON.stringify(run) !== JSON.stringify(record.runs[index]));\n\tif (!changed) return record;\n\tconst active = runs.some((run) => run.status && ACTIVE_RUN_STATUSES.has(run.status));\n\treturn updateMission(location, record.id, {\n\t\tstatus: active ? \"active\" : record.goal ? \"active\" : record.status,\n\t\taddRuns: runs,\n\t});\n}\n\nfunction readyActionFromValue(value: unknown, pathLabel = \"state\", depth = 0): string | undefined {\n\tif (depth > 8 || !value || typeof value !== \"object\") return undefined;\n\tif (Array.isArray(value)) {\n\t\tfor (let index = 0; index < value.length; index += 1) {\n\t\t\tconst found = readyActionFromValue(value[index], `${pathLabel}[${index}]`, depth + 1);\n\t\t\tif (found) return found;\n\t\t}\n\t\treturn undefined;\n\t}\n\tconst input = value as Record<string, unknown>;\n\tfor (const key of [\"nextReadyAction\", \"nextAction\"] as const) {\n\t\tif (typeof input[key] === \"string\" && input[key].trim()) return bounded(input[key]);\n\t}\n\tif (input.status === \"ready\") {\n\t\tfor (const key of [\"action\", \"task\", \"title\", \"summary\"] as const) {\n\t\t\tif (typeof input[key] === \"string\" && input[key].trim()) return bounded(input[key]);\n\t\t}\n\t\treturn `Continue ready mission state at ${pathLabel}`;\n\t}\n\tfor (const [key, child] of Object.entries(input)) {\n\t\tconst found = readyActionFromValue(child, `${pathLabel}.${key}`, depth + 1);\n\t\tif (found) return found;\n\t}\n\treturn undefined;\n}\n\nfunction missionStateAction(location: MissionStoreLocation, record: MissionRecord): string | undefined {\n\tconst statePath = missionStatePath(location, record.id);\n\tif (fs.existsSync(statePath)) {\n\t\tconst action = readyActionFromValue(JSON.parse(fs.readFileSync(statePath, \"utf-8\")));\n\t\tif (action) return action;\n\t}\n\tconst decision = record.decisions.find((item) => item.status === \"open\");\n\tif (decision) return bounded(decision.recommendation ?? `Resolve decision: ${decision.title}`);\n\treturn undefined;\n}\n\nfunction retainedResumeTarget(record: MissionRecord, retainedChildren: RetainedChild[]): RetainedChild | undefined {\n\tconst latestRun = record.runs.at(-1);\n\tif (!latestRun) return undefined;\n\treturn retainedChildren.find((child) => child.runId === latestRun.runId || child.parentRunId === latestRun.runId);\n}\n\nfunction nextReadyAction(\n\tlocation: MissionStoreLocation,\n\trecord: MissionRecord,\n\tretainedChildren: RetainedChild[],\n): string {\n\tconst stateAction = missionStateAction(location, record);\n\tconst latestRun = record.runs.at(-1);\n\tconst action =\n\t\tstateAction ??\n\t\t(latestRun?.status === \"failed\" || latestRun?.status === \"paused\"\n\t\t\t? `Inspect linked run ${latestRun.runId} and continue the mission`\n\t\t\t: `Continue objective: ${record.objective}`);\n\tconst retained = retainedResumeTarget(record, retainedChildren);\n\treturn retained\n\t\t? `Resume retained child ${retained.runId} (${retained.agent}) for: ${bounded(action)}`\n\t\t: bounded(action);\n}\n\nexport function collectGoalContinuationNotices(input: {\n\tlocation: MissionStoreLocation;\n\townerSessionId: string;\n\tretainedChildren: RetainedChild[];\n\tturnId: number;\n\tnow?: number;\n}): GoalContinuationNotice[] {\n\tconst notices: GoalContinuationNotice[] = [];\n\tconst seen = new Set<string>();\n\tfor (const listed of listMissions(input.location).records) {\n\t\tif (\n\t\t\tlisted.ownerSessionId !== input.ownerSessionId ||\n\t\t\t!listed.goal ||\n\t\t\tTERMINAL_MISSION_STATUSES.has(listed.status)\n\t\t)\n\t\t\tcontinue;\n\t\tlet record = refreshGoalMission(input.location, readMission(input.location, listed.id));\n\t\tif (!record.goal || record.goal.status !== \"active\" || !record.budget) continue;\n\t\tif ((record.usage?.tokens ?? 0) >= record.budget.tokens) {\n\t\t\trecord = updateMission(input.location, record.id, { usage: record.usage ?? { tokens: 0 } });\n\t\t\tif (record.goal?.status === \"budget-exhausted\") continue;\n\t\t}\n\t\tif (record.runs.some((run) => run.status && ACTIVE_RUN_STATUSES.has(run.status))) continue;\n\t\tif (seen.has(record.id) || !record.budget) continue;\n\t\tseen.add(record.id);\n\t\tconst budget = record.budget.tokens;\n\t\tconst used = record.usage?.tokens ?? 0;\n\t\tconst remaining = Math.max(0, budget - used);\n\t\tconst message = [\n\t\t\t`Goal mission needs attention: ${bounded(record.title)}`,\n\t\t\t`Mission: ${record.id}`,\n\t\t\t`Remaining budget: ${remaining} tokens (${used}/${budget} used)`,\n\t\t\t`Next ready action: ${nextReadyAction(input.location, record, input.retainedChildren)}`,\n\t\t].join(\"\\n\");\n\t\tnotices.push({\n\t\t\tmissionId: record.id,\n\t\t\tmessage,\n\t\t\tevent: {\n\t\t\t\ttype: \"needs_attention\",\n\t\t\t\tto: \"needs_attention\",\n\t\t\t\tts: input.now ?? Date.now(),\n\t\t\t\trunId: `goal-${record.id}-turn-${input.turnId}`,\n\t\t\t\tagent: \"goal mission\",\n\t\t\t\tmessage,\n\t\t\t\treason: \"idle\",\n\t\t\t},\n\t\t});\n\t}\n\treturn notices;\n}\n"]}