{"version":3,"file":"action-decoder.d.ts","sourceRoot":"","sources":["../../../src/core/reliability/action-decoder.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,WAAW,EAAkB,MAAM,YAAY,CAAC;AAEvF,MAAM,MAAM,kBAAkB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,uBAAuB,CAAA;CAAE,CAAC;AA8JrH;;;GAGG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAoC7D","sourcesContent":["/**\n * Action Decoder — normalized model-output boundary.\n *\n * Normalizes heterogeneous provider/model output (native tool calls,\n * structured JSON envelopes, plain-text fallback) into the finite\n * Jensen-owned AgentAction union. The rest of the runtime never inspects raw\n * provider output directly.\n */\n\nimport type { ActionValidationFailure, AgentAction, ToolCallAction } from \"./types.js\";\n\nexport type ActionDecodeResult = { ok: true; action: AgentAction } | { ok: false; failure: ActionValidationFailure };\n\ninterface RawNativeToolCall {\n\tname?: unknown;\n\tid?: unknown;\n\ttoolCallId?: unknown;\n\ttool_call_id?: unknown;\n\targuments?: unknown;\n\targumentsString?: unknown;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction asString(value: unknown): string | undefined {\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction decodeNativeToolCall(raw: RawNativeToolCall): ToolCallAction | null {\n\tconst name = asString(raw.name);\n\tif (!name) return null;\n\n\tlet args: Record<string, unknown> = {};\n\tif (isRecord(raw.arguments)) {\n\t\targs = raw.arguments;\n\t} else if (typeof raw.argumentsString === \"string\" && raw.argumentsString.trim().length > 0) {\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(raw.argumentsString);\n\t\t\tif (isRecord(parsed)) args = parsed;\n\t\t\telse return null;\n\t\t} catch {\n\t\t\t// Unparseable argument string — leave args empty; validation will\n\t\t\t// reject missing required arguments. Never fabricate args here.\n\t\t}\n\t}\n\n\tconst id = asString(raw.toolCallId) ?? asString(raw.id) ?? asString(raw.tool_call_id) ?? `tc_${name}`;\n\n\treturn { type: \"tool_call\", tool: name, toolCallId: id, arguments: args };\n}\n\nfunction decodeEnvelope(raw: Record<string, unknown>): AgentAction | null {\n\tconst type = asString(raw.type);\n\tswitch (type) {\n\t\tcase \"tool_call\": {\n\t\t\tconst tool = asString(raw.tool);\n\t\t\tif (!tool) return null;\n\t\t\tconst args = isRecord(raw.arguments) ? raw.arguments : {};\n\t\t\treturn {\n\t\t\t\ttype: \"tool_call\",\n\t\t\t\ttool,\n\t\t\t\ttoolCallId: asString(raw.toolCallId) ?? `tc_${tool}`,\n\t\t\t\targuments: args,\n\t\t\t\treason: asString(raw.reason),\n\t\t\t};\n\t\t}\n\t\tcase \"request_context\":\n\t\t\treturn { type: \"request_context\", query: asString(raw.query) ?? \"\" };\n\t\tcase \"mission_update\":\n\t\t\treturn {\n\t\t\t\ttype: \"mission_update\",\n\t\t\t\tobjective: asString(raw.objective),\n\t\t\t\tcompletedObjectives: Array.isArray(raw.completedObjectives)\n\t\t\t\t\t? raw.completedObjectives.filter((x): x is string => typeof x === \"string\")\n\t\t\t\t\t: undefined,\n\t\t\t\tnote: asString(raw.note),\n\t\t\t};\n\t\tcase \"final_candidate\":\n\t\t\treturn {\n\t\t\t\ttype: \"final_candidate\",\n\t\t\t\tsummary: asString(raw.summary),\n\t\t\t\tclaimedCriterionIds: Array.isArray(raw.claimedCriterionIds)\n\t\t\t\t\t? raw.claimedCriterionIds.filter((x): x is string => typeof x === \"string\")\n\t\t\t\t\t: undefined,\n\t\t\t};\n\t\tcase \"blocked\":\n\t\t\treturn { type: \"blocked\", reason: asString(raw.reason) ?? \"model reported blocked\" };\n\t\tcase \"no_op\":\n\t\t\treturn { type: \"no_op\", reason: asString(raw.reason) };\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\n/** Recognize a plain-text completion/blocked signal for text-only fallback. */\nfunction decodeText(text: string): AgentAction | null {\n\tconst trimmed = text.trim();\n\tif (/\\bFINAL_CANDIDATE\\b/i.test(trimmed) || /\\bMISSION_COMPLETE\\b/i.test(trimmed)) {\n\t\treturn { type: \"final_candidate\", summary: trimmed.slice(0, 512) };\n\t}\n\tif (/^BLOCKED:?\\s*/i.test(trimmed)) {\n\t\treturn { type: \"blocked\", reason: trimmed.replace(/^BLOCKED:?\\s*/i, \"\").slice(0, 512) };\n\t}\n\t// Local models frequently wrap structured output in fenced code blocks.\n\t// Extract the first JSON object/array and decode it as an envelope.\n\tconst json = extractJsonPayload(trimmed);\n\tif (json !== undefined) {\n\t\treturn decodeEnvelope(json);\n\t}\n\treturn null;\n}\n\n/**\n * Extract the first balanced JSON object/array from free-form text (including\n * ```json fenced blocks and surrounding prose). Uses a brace/string scanner so\n * nested objects are handled correctly. Returns undefined when no balanced JSON\n * payload is present.\n */\nfunction extractJsonPayload(text: string): Record<string, unknown> | undefined {\n\t// Prefer the contents of a fenced code block when present.\n\tconst fenced = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/i);\n\tconst candidate = fenced ? fenced[1] : text;\n\n\tconst open = candidate.search(/[{[]/);\n\tif (open === -1) return undefined;\n\n\tconst stack: string[] = [];\n\tlet inString = false;\n\tlet escaped = false;\n\n\tfor (let i = open; i < candidate.length; i++) {\n\t\tconst ch = candidate[i];\n\t\tif (inString) {\n\t\t\tif (escaped) {\n\t\t\t\tescaped = false;\n\t\t\t} else if (ch === \"\\\\\") {\n\t\t\t\tescaped = true;\n\t\t\t} else if (ch === '\"') {\n\t\t\t\tinString = false;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (ch === '\"') {\n\t\t\tinString = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (ch === \"{\" || ch === \"[\") {\n\t\t\tstack.push(ch);\n\t\t} else if (ch === \"}\" || ch === \"]\") {\n\t\t\tconst top = stack.pop();\n\t\t\tif ((ch === \"}\" && top !== \"{\") || (ch === \"]\" && top !== \"[\")) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\tif (stack.length === 0) {\n\t\t\t\tconst slice = candidate.slice(open, i + 1);\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed: unknown = JSON.parse(slice);\n\t\t\t\t\tif (isRecord(parsed)) return parsed;\n\t\t\t\t} catch {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Decode raw model output into an AgentAction. Returns a structured failure\n * (never throws) when the output cannot be normalized.\n */\nexport function decodeAction(raw: unknown): ActionDecodeResult {\n\tif (isRecord(raw)) {\n\t\tconst record = raw as RawNativeToolCall;\n\t\t// Native tool-call shape takes precedence when a tool name is present.\n\t\tconst native = decodeNativeToolCall(record);\n\t\tif (native) return { ok: true, action: native };\n\n\t\tconst envelope = decodeEnvelope(record as Record<string, unknown>);\n\t\tif (envelope) return { ok: true, action: envelope };\n\n\t\treturn {\n\t\t\tok: false,\n\t\t\tfailure: {\n\t\t\t\tcategory: \"UNKNOWN_ACTION_TYPE\",\n\t\t\t\tmessage: `Unrecognized action shape: ${Object.keys(raw).join(\", \") || \"(empty object)\"}`,\n\t\t\t\trecoverable: true,\n\t\t\t\tdetails: { keys: Object.keys(raw) },\n\t\t\t},\n\t\t};\n\t}\n\n\tif (typeof raw === \"string\") {\n\t\tconst textAction = decodeText(raw);\n\t\tif (textAction) return { ok: true, action: textAction };\n\t\t// Plain text with no tool call and no signal is a non-executable turn.\n\t\treturn { ok: true, action: { type: \"no_op\", reason: \"text-only turn\" } };\n\t}\n\n\treturn {\n\t\tok: false,\n\t\tfailure: {\n\t\t\tcategory: \"UNKNOWN_ACTION_TYPE\",\n\t\t\tmessage: `Expected an object or string action, got ${typeof raw}`,\n\t\t\trecoverable: true,\n\t\t},\n\t};\n}\n"]}