This commit is contained in:
2026-09-04 11:16:23 +08:00
parent 9e27f11bb7
commit 921ba5eab3
36 changed files with 2665 additions and 29 deletions
+45 -7
View File
@@ -33,7 +33,17 @@ from app.cad_agent.domain.feature_plan import FeaturePlan, plan_hash
from app.cad_agent.domain.errors import ErrorCode, WorkflowError
from app.cad_agent.domain.operation_contract import fragment_schema
from app.cad_agent.domain.state import TaskPhase, TaskState, retry_resume_event, transition
from app.cad_agent.ports import AdapterUnavailable, ArtifactStore, CadRuntime, ModelGateway, ReviewGateway, TaskRepository
from app.cad_agent.ports import (
AdapterUnavailable,
ArtifactStore,
AuthorGuidance,
AuthorGuidanceSelection,
CadRuntime,
ModelGateway,
NullAuthorGuidance,
ReviewGateway,
TaskRepository,
)
T = TypeVar("T", bound=BaseModel)
@@ -116,6 +126,7 @@ class WorkflowCoordinator:
review_gateway: ReviewGateway,
requirements: RequirementsCommandHandler,
actions: ActionCommandHandler,
author_guidance: AuthorGuidance | None = None,
) -> None:
self.config = config
self.repository = repository
@@ -125,6 +136,7 @@ class WorkflowCoordinator:
self.review_gateway = review_gateway
self.requirements = requirements
self.actions = actions
self.author_guidance = author_guidance or NullAuthorGuidance()
def create_task(
self,
@@ -1078,14 +1090,14 @@ class WorkflowCoordinator:
"message": message[:1000],
}
async def _author_turn(self, task_id: str, author: ModelIdentity, tools: list[dict[str, Any]], feedback: list[dict[str, Any]]) -> tuple[str, str, dict[str, int]] | WorkflowError:
async def _author_turn(self, task_id: str, author: ModelIdentity, tools: list[dict[str, Any]], feedback: list[dict[str, Any]]) -> tuple[str, str, dict[str, Any]] | WorkflowError:
if len(tools) != 1:
raise RuntimeError("Workflow state must expose exactly one author tool.")
tool = tools[0]
name = str((tool.get("function") or {}).get("name") or "")
if not name:
raise RuntimeError("Workflow exposed an unnamed author tool.")
messages = self._author_context(task_id, feedback)
messages, guidance = self._author_context(task_id, feedback)
try:
response = await self.model_gateway.call_tool(
messages=messages, tool=tool, provider_id=author.provider_id,
@@ -1103,10 +1115,26 @@ class WorkflowCoordinator:
"model_id": author.model_id,
"retry_reason": "provider_unavailable",
"cache_hit": False,
**guidance.usage_metadata(),
})
return WorkflowError(ErrorCode.AUTHOR_TRANSPORT_UNAVAILABLE, str(error)[:1000], retryable=True)
call = validate_one_tool_call(response["tool_calls"], name)
if isinstance(call, WorkflowError):
# A provider response is still a billable author attempt even when
# it violates the one-tool-call protocol. Keep guidance audit
# metadata on that record so A/B reports do not silently omit the
# failures this corpus is intended to reduce.
self.repository.record_usage(task_id, {
**response["usage"],
"context_chars": len(json.dumps(messages, ensure_ascii=False)),
"tool": name,
"provider_id": author.provider_id,
"model_id": author.model_id,
"raw_arguments_hash": "",
"retry_reason": "invalid_tool_call",
"cache_hit": False,
**guidance.usage_metadata(),
})
self._record_rejected_tool_calls(
task_id,
actor="author",
@@ -1132,6 +1160,7 @@ class WorkflowCoordinator:
"raw_arguments_hash": raw_arguments_hash(raw),
"retry_reason": "",
"cache_hit": False,
**guidance.usage_metadata(),
}
self.repository.record_usage(task_id, usage)
return name, raw, usage
@@ -1400,10 +1429,10 @@ class WorkflowCoordinator:
return []
return [self._tool("record_geometry_conclusion", StatelessGeometryConclusion)]
def _author_context(self, task_id: str, feedback: list[dict[str, Any]]) -> list[dict[str, Any]]:
def _author_context(self, task_id: str, feedback: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], AuthorGuidanceSelection]:
state = self.repository.get_state(task_id)
if state is None:
return []
return [], AuthorGuidanceSelection(fallback_reason="task_state_unavailable")
if state.phase == TaskPhase.DRAFTING_REQUIREMENTS_DOCUMENT:
content = {
"protocol": "cad.v3.2.feature-dag",
@@ -1557,8 +1586,17 @@ class WorkflowCoordinator:
"feature_plan_hash": state.feature_plan_hash,
"feature_node_statuses": self._feature_node_statuses(task_id, None),
})
messages: list[dict[str, Any]] = [{"role": "system", "content": "You are the autonomous CAD author. Use exactly one offered structured tool call. Never emit Markdown plans or free-form JSON."}, {"role": "user", "content": json.dumps(content, ensure_ascii=False)}]
return [*messages, *feedback[-2:]]
guidance = self.author_guidance.select(
phase=state.phase,
atomic_id=state.pending_feature.atomic_id if state.pending_feature is not None else "",
repair_required=state.repair_required,
supported_atomic_ids=self.runtime.supported_atomic_ids(),
)
system = "You are the autonomous CAD author. Use exactly one offered structured tool call. Never emit Markdown plans or free-form JSON."
if guidance.content:
system += "\n\nThe following is non-authoritative CDSL author guidance. The current tool schema, operation contract, and server facts take precedence.\n\n" + guidance.content
messages: list[dict[str, Any]] = [{"role": "system", "content": system}, {"role": "user", "content": json.dumps(content, ensure_ascii=False)}]
return [*messages, *feedback[-2:]], guidance
def _user_clarifications(self, task_id: str) -> list[dict[str, str]]:
clarifications: list[dict[str, str]] = []