## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""What one finished turn produced (R04-T03).
|
||||
|
||||
The outcome of a turn is currently spread over three shapes: ``run_cowork``
|
||||
returns the mutated message list, ``task_executors._run_agent`` returns a
|
||||
``(answer_text, timed_out, incomplete_reason)`` tuple, and the UI reconstructs
|
||||
the rest (did it get cancelled? did it hit the ceiling?) from side effects. Each
|
||||
caller therefore knows a slightly different amount about the same turn.
|
||||
|
||||
:class:`AgentResult` is the single answer. Frozen, like the request that started
|
||||
the turn, so a result cannot be edited into disagreeing with what actually
|
||||
happened.
|
||||
|
||||
Layer rules (``docs/architecture/ADR-001-layered-architecture.md``): domain
|
||||
layer — standard library plus sibling domain types only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from .agent_event import PlanStep, TurnCompletedEvent
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentResult:
|
||||
"""The outcome of one conversation turn."""
|
||||
|
||||
# The conversation AFTER the turn (system prompt, history, the new user
|
||||
# message, every assistant reply and tool result).
|
||||
messages: Tuple[Dict[str, Any], ...] = ()
|
||||
steps_used: int = 0 # provider calls this turn consumed
|
||||
cancelled: bool = False # the user pressed Stop
|
||||
budget_exhausted: bool = False # stopped at effective_max_steps
|
||||
# The agent's final checklist, so a caller can ask "did it really finish?"
|
||||
# (``core/plan.py::plan_incomplete_reason``) without replaying the events.
|
||||
plan_steps: Tuple[PlanStep, ...] = ()
|
||||
# Non-empty when the turn ended on a failure. A string rather than the
|
||||
# exception: the domain layer must not depend on where the error came from,
|
||||
# and the message is what every consumer (bubble, error.txt, audit) shows.
|
||||
error: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Freeze the collections the runtime hands over.
|
||||
|
||||
Both arrive as live lists that the caller keeps appending to after the
|
||||
turn (the UI merges messages back into its own history), so copying here
|
||||
is what keeps a result a record rather than a moving target.
|
||||
"""
|
||||
object.__setattr__(self, "messages", tuple(self.messages or ()))
|
||||
object.__setattr__(self, "plan_steps", tuple(self.plan_steps or ()))
|
||||
|
||||
@property
|
||||
def final_text(self) -> str:
|
||||
"""The answer to show the user.
|
||||
|
||||
Scans backwards for the last assistant message with real content, which
|
||||
is not the same as ``messages[-1]``: a turn that was cancelled or that
|
||||
ran out of steps mid-loop ends on a tool message, and a reasoning-only
|
||||
reply leaves a blank assistant message behind. Same rule as
|
||||
``core/task_executors.py::_last_assistant_text``, which this replaces.
|
||||
"""
|
||||
for message in reversed(self.messages):
|
||||
if message.get("role") == "assistant" and (message.get("content") or "").strip():
|
||||
return str(message["content"])
|
||||
return ""
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
"""Whether the turn ran to a normal end.
|
||||
|
||||
Hitting the step ceiling still counts as ok: the agent did work and
|
||||
produced an answer, it just was not allowed to keep going — which the
|
||||
transcript says in its own note rather than by failing the turn.
|
||||
"""
|
||||
return not self.error and not self.cancelled
|
||||
|
||||
def to_turn_completed_event(self) -> TurnCompletedEvent:
|
||||
"""The end-of-turn event carrying this outcome to subscribers."""
|
||||
return TurnCompletedEvent(
|
||||
final_text=self.final_text, steps_used=self.steps_used,
|
||||
cancelled=self.cancelled, budget_exhausted=self.budget_exhausted,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["AgentResult"]
|
||||
Reference in New Issue
Block a user