| """Slow-path execution + output contracts. |
| |
| The seams between the three slow-path stages: |
| - TaskRunner writes `RunState` (a blackboard of `TaskResult`s) — §8.2. |
| - Assembler reads `RunState` + `BusinessContext` and produces `AssembledOutput` |
| (`chat_answer` + `AnalysisRecord`) — §8.3. |
| |
| `ToolOutput` (the tool -> agent envelope) is reused from the planner contracts so |
| there is exactly one definition across the layer. |
| |
| Note on authorship (§8.3): the Assembler LLM authors only the *narrative* fields |
| (`AssemblerNarrative`). The `AnalysisRecord`'s structured pass-through fields |
| (`results_snapshot`, `tasks_run`) and metadata are copied from `RunState` by code, |
| never re-authored by the model — that is the source of truth the report generator |
| renders from (INV-4). |
| |
| See AGENT_ARCHITECTURE_CONTEXT_new.md §8.2 / §8.3. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from datetime import datetime |
| from typing import Literal |
| from uuid import uuid4 |
|
|
| from pydantic import BaseModel, Field |
|
|
| from ..planner.contracts import ToolOutput |
| from ..planner.schemas import CrispStage |
|
|
| TaskStatus = Literal["success", "partial", "failure"] |
|
|
|
|
| |
| |
| |
|
|
|
|
| class TaskResult(BaseModel): |
| task_id: str |
| stage: CrispStage |
| status: TaskStatus |
| objective: str |
| outputs: list[ToolOutput] = Field(default_factory=list) |
| note: str | None = None |
| error: str | None = None |
|
|
|
|
| class RunState(BaseModel): |
| plan_id: str |
| business_context_id: str |
| results: dict[str, TaskResult] = Field(default_factory=dict) |
| open_questions: list[str] = Field(default_factory=list) |
|
|
|
|
| |
| |
| |
|
|
|
|
| class TaskSummary(BaseModel): |
| task_id: str |
| stage: CrispStage |
| objective: str |
| status: TaskStatus |
| tools_used: list[str] = Field(default_factory=list) |
|
|
|
|
| class AnalysisRecord(BaseModel): |
| |
| |
| |
| |
| |
| record_id: str = Field(default_factory=lambda: uuid4().hex) |
| analysis_id: str | None = None |
| user_id: str | None = None |
| |
| goal_restated: str |
| findings: list[str] = Field(default_factory=list) |
| caveats: list[str] = Field(default_factory=list) |
| data_used: list[str] = Field(default_factory=list) |
| open_questions: list[str] = Field(default_factory=list) |
| |
| tasks_run: list[TaskSummary] = Field(default_factory=list) |
| results_snapshot: dict[str, TaskResult] = Field(default_factory=dict) |
| |
| plan_id: str |
| business_context_id: str |
| created_at: datetime |
|
|
|
|
| class AssembledOutput(BaseModel): |
| chat_answer: str |
| analysis_record: AnalysisRecord |
|
|
|
|
| class AssemblerNarrative(BaseModel): |
| """The subset of `AnalysisRecord` the Assembler LLM actually authors. |
| |
| Kept separate from `AssembledOutput` so the model never emits the structured |
| pass-through fields (which would invite hallucinated numbers); `Assembler` |
| code merges this with the real `RunState` to build the final record. |
| """ |
|
|
| chat_answer: str |
| goal_restated: str |
| findings: list[str] = Field(default_factory=list) |
| caveats: list[str] = Field(default_factory=list) |
| data_used: list[str] = Field(default_factory=list) |
| open_questions: list[str] = Field(default_factory=list) |
|
|