chore: save current work
This commit is contained in:
@@ -17,19 +17,20 @@ from pydantic import BaseModel
|
||||
|
||||
from app.cad_agent.application.action_handlers import ActionCommandHandler
|
||||
from app.cad_agent.application.llm_contracts import (
|
||||
CandidateReview, EmptyCommand, FinalReview, GeometryConclusion, NextAction,
|
||||
OperationContractRequest, RequirementsDraftBatch, RequirementsPatchBatch,
|
||||
RequirementsReview, RollbackCheckpoint, TopologyRequest,
|
||||
candidate_review_schema, canonical_json_object, canonical_validate, canonical_validate_schema,
|
||||
final_review_schema, geometry_conclusion_schema, next_action_schema,
|
||||
operation_contract_request_schema, rollback_checkpoint_schema, topology_request_schema,
|
||||
CandidateReview, EmptyCommand, FinalReview, GeometryConclusion, ImageObservation, NextAction,
|
||||
RequirementsAuthorOutput, RollbackCheckpoint, StatelessCandidateReview,
|
||||
StatelessFinalReview, StatelessGeometryConclusion, StatelessNextAction, StatelessRollbackCheckpoint,
|
||||
StatelessTopologyRequest, TopologyRequest,
|
||||
canonical_json_object, canonical_validate, canonical_validate_schema,
|
||||
stateless_final_review_schema,
|
||||
stateless_next_action_schema, stateless_rollback_checkpoint_schema,
|
||||
raw_arguments_hash, validate_one_tool_call,
|
||||
)
|
||||
from app.cad_agent.application.requirements_review import RequirementsCommandHandler
|
||||
from app.cad_agent.application.requirements import RequirementsCommandHandler
|
||||
from app.cad_agent.application.results import Accepted, Rejected, Waiting
|
||||
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, reject_stale_head, retry_resume_event, transition
|
||||
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
|
||||
|
||||
|
||||
@@ -46,7 +47,6 @@ class ModelIdentity:
|
||||
class WorkflowConfig:
|
||||
max_turns: int
|
||||
format_error_limit: int
|
||||
requirements_review_limit: int = 3
|
||||
author_fallbacks: tuple[ModelIdentity, ...] = ()
|
||||
max_author_turns: int | None = None
|
||||
max_reviewer_turns: int | None = None
|
||||
@@ -129,11 +129,12 @@ class WorkflowCoordinator:
|
||||
request: str,
|
||||
*,
|
||||
source_blocks: list[dict[str, Any]] | None = None,
|
||||
image_inputs: list[dict[str, str]] | None = None,
|
||||
) -> TaskState:
|
||||
# The immutable source artifact is safe to create before SQLite state:
|
||||
# an interrupted creation leaves only an unreferenced directory, never
|
||||
# a runnable task without its source index.
|
||||
self.artifacts.initialize_task(task_id, request, source_blocks=source_blocks)
|
||||
self.artifacts.initialize_task(task_id, request, source_blocks=source_blocks, image_inputs=image_inputs)
|
||||
return self.repository.create_task(task_id, request)
|
||||
|
||||
def resume(self, task_id: str) -> bool:
|
||||
@@ -158,15 +159,8 @@ class WorkflowCoordinator:
|
||||
state = self.repository.get_state(task_id)
|
||||
if state is None or state.phase != TaskPhase.WAITING_FOR_USER:
|
||||
return False
|
||||
review = self._requirements_review(task_id, state)
|
||||
is_requirements_pause = isinstance(review, dict) and (
|
||||
review.get("decision") == "waiting_for_user"
|
||||
and any(
|
||||
item.get("finding_type") == "ambiguous_conflict" and item.get("question")
|
||||
for item in review.get("findings") or () if isinstance(item, dict)
|
||||
)
|
||||
)
|
||||
if not is_requirements_pause:
|
||||
clarification_request = self.artifacts.read_json(task_id, state.clarification_path) if state.clarification_path else None
|
||||
if not isinstance(clarification_request, dict) or not str(clarification_request.get("question") or "").strip():
|
||||
return False
|
||||
text = clarification.strip()
|
||||
if not text:
|
||||
@@ -182,7 +176,7 @@ class WorkflowCoordinator:
|
||||
})
|
||||
except OSError:
|
||||
return False
|
||||
resumed = transition(state, "requirements_clarified")
|
||||
resumed = transition(state, "requirements_clarified", clarification_path="")
|
||||
return self.repository.compare_and_swap(resumed, events=[{
|
||||
"event": "user_clarification_received",
|
||||
"message_id": message_id,
|
||||
@@ -271,9 +265,42 @@ class WorkflowCoordinator:
|
||||
return
|
||||
continue
|
||||
if state.phase == TaskPhase.DRAFTING_REQUIREMENTS:
|
||||
draft_schema = self.requirements.draft_schema(task_id)
|
||||
patch_schema = self.requirements.patch_schema(task_id)
|
||||
tools = self._requirements_author_tools(task_id, draft_schema, patch_schema)
|
||||
image_paths = self.artifacts.source_image_paths(task_id)
|
||||
if image_paths and self.artifacts.read_json(task_id, "documents/image-observation.json") is None:
|
||||
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="reviewer")
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
call_budget.record_attempt("reviewer")
|
||||
observation = await self._observe_images(task_id, reviewer, image_paths)
|
||||
if isinstance(observation, WorkflowError):
|
||||
if observation.code == ErrorCode.AUTHOR_FORMAT_INVALID:
|
||||
observation = WorkflowError(
|
||||
ErrorCode.REVIEW_SERVICE_UNAVAILABLE,
|
||||
"Image observation did not return the required structured format.",
|
||||
field_errors=observation.field_errors,
|
||||
retryable=True,
|
||||
)
|
||||
yield self._service_failure(task_id, state, observation)
|
||||
return
|
||||
try:
|
||||
self.artifacts.write_json_once(task_id, "documents/image-observation.json", {
|
||||
"schema_version": "cad.image-observation.v3",
|
||||
**observation.model_dump(mode="json"),
|
||||
})
|
||||
except OSError as error:
|
||||
yield self._storage_failure(task_id, str(error))
|
||||
return
|
||||
observed_state = transition(state, "image_observed")
|
||||
self.repository.compare_and_swap(observed_state, events=[{
|
||||
"event": "image_observation_ready",
|
||||
"path": "documents/image-observation.json",
|
||||
"image_count": len(image_paths),
|
||||
}])
|
||||
yield "image_observation", {"taskId": task_id, "status": "success", "path": "documents/image-observation.json"}
|
||||
continue
|
||||
spec_schema = self.requirements.spec_schema()
|
||||
tools = [self._tool("submit_requirements_spec", spec_schema)]
|
||||
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="author")
|
||||
if terminal:
|
||||
yield terminal
|
||||
@@ -294,98 +321,33 @@ class WorkflowCoordinator:
|
||||
return
|
||||
continue
|
||||
name, raw, usage = result
|
||||
model = RequirementsDraftBatch if name == "submit_requirements_draft_batch" else RequirementsPatchBatch if name == "patch_requirements_draft" else EmptyCommand
|
||||
validation = canonical_validate(raw, model)
|
||||
dynamic_error = canonical_validate_schema(raw, draft_schema if name == "submit_requirements_draft_batch" else patch_schema) if not isinstance(validation, WorkflowError) and name != "finalize_requirements_draft" else None
|
||||
validation = canonical_validate(raw, RequirementsAuthorOutput)
|
||||
dynamic_error = canonical_validate_schema(raw, spec_schema) if not isinstance(validation, WorkflowError) else None
|
||||
if dynamic_error is not None:
|
||||
validation = dynamic_error
|
||||
if isinstance(validation, WorkflowError):
|
||||
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
|
||||
terminal = self._requirements_format_failure(task_id, state, validation, format_errors, feedback)
|
||||
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
invocation_id = self._invocation_id(task_id)
|
||||
command = self.requirements.submit_draft(task_id, validation, invocation_id=invocation_id) if name == "submit_requirements_draft_batch" else self.requirements.patch_draft(task_id, validation, invocation_id=invocation_id) if name == "patch_requirements_draft" else self.requirements.finalize_draft(task_id, invocation_id=invocation_id)
|
||||
command = self.requirements.submit_spec(task_id, validation, invocation_id=invocation_id)
|
||||
if isinstance(command, Rejected):
|
||||
terminal = self._model_rejection_or_service_failure(task_id, state, name, command.error, format_errors, feedback)
|
||||
terminal = self._requirements_rejection(task_id, state, command.error, format_errors, feedback)
|
||||
yield "tool_call", self._event(task_id, name, command.error.payload(), "error", usage)
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
yield "tool_call", self._event(task_id, name, self._result_payload(command), "success", usage)
|
||||
yield "requirements_ready", self._event(task_id, name, self._result_payload(command), "waiting" if isinstance(command, Waiting) else "success", usage)
|
||||
feedback = []
|
||||
continue
|
||||
if state.phase == TaskPhase.REVIEWING_REQUIREMENTS:
|
||||
terminal = self._call_budget_terminal(task_id, state, call_budget, actor="reviewer")
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
call_budget.record_attempt("reviewer")
|
||||
command = await self._review_requirements(task_id, reviewer, feedback)
|
||||
if isinstance(command, WorkflowError):
|
||||
if command.code == ErrorCode.AUTHOR_FORMAT_INVALID:
|
||||
terminal = self._format_failure(task_id, state, "review_requirements", command, format_errors, feedback, actor="reviewer")
|
||||
yield "requirements_review", {"taskId": task_id, "status": "error", "result": command.payload()}
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
terminal = self._service_failure(task_id, state, command)
|
||||
yield terminal
|
||||
return
|
||||
review_result = self.requirements.record_review(task_id, command, invocation_id=self._invocation_id(task_id))
|
||||
if isinstance(review_result, Rejected):
|
||||
terminal = self._model_rejection_or_service_failure(task_id, state, "review_requirements", review_result.error, format_errors, feedback, actor="reviewer")
|
||||
yield "requirements_review", {"taskId": task_id, "status": "error", "result": review_result.error.payload()}
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
yield "requirements_review", {"taskId": task_id, "status": "success", "result": self._result_payload(review_result)}
|
||||
if isinstance(review_result, Accepted) and review_result.payload.get("phase") == TaskPhase.DRAFTING_REQUIREMENTS.value:
|
||||
draft_state = self.repository.get_state(task_id)
|
||||
revision = int(self._requirements_draft(draft_state).get("revision") or 0)
|
||||
# The initial draft is reviewed before any revision.
|
||||
# ``requirements_review_limit`` bounds subsequent
|
||||
# author corrections, so permit exactly that many
|
||||
# patches and park only after they are exhausted.
|
||||
if revision > self.config.requirements_review_limit:
|
||||
if draft_state is not None:
|
||||
failed = transition(draft_state, "failed", error=ErrorCode.REQUIREMENTS_REVIEW_NOT_CONVERGED)
|
||||
review = self._requirements_review(task_id, draft_state) or {}
|
||||
findings = [
|
||||
item for item in review.get("findings") or ()
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
self.repository.compare_and_swap(failed, events=[{
|
||||
"event": "requirements_review_limit_reached",
|
||||
"code": ErrorCode.REQUIREMENTS_REVIEW_NOT_CONVERGED.value,
|
||||
"draft_revision": revision,
|
||||
"review_path": draft_state.requirements_review_path,
|
||||
"findings": findings,
|
||||
}])
|
||||
yield "task_terminal", {
|
||||
"taskId": task_id,
|
||||
"lifecycle": "failed",
|
||||
"code": ErrorCode.REQUIREMENTS_REVIEW_NOT_CONVERGED.value,
|
||||
"message": "Requirements review did not converge within the configured author-revision limit.",
|
||||
"issues": [str(item.get("description") or "") for item in findings if item.get("description")],
|
||||
"blockerType": "requirements_review_not_converged",
|
||||
"userActionRequired": False,
|
||||
}
|
||||
return
|
||||
continue
|
||||
if state.phase == TaskPhase.AWAITING_ACTION:
|
||||
contract = self._requirements_contract(task_id, state) or {}
|
||||
requirement_ids = [str(item.get("requirement_id") or "") for item in contract.get("requirements") or () if isinstance(item, dict) and item.get("requirement_id")]
|
||||
action_schema = next_action_schema(
|
||||
state.working_head,
|
||||
requirement_ids,
|
||||
list(self.actions.available_atomic_ids(task_id, state)),
|
||||
)
|
||||
action_schema = stateless_next_action_schema(list(self.actions.available_atomic_ids(task_id, state)))
|
||||
tools = self._recovery_tools(task_id, state)
|
||||
if not tools:
|
||||
if self._can_complete(task_id, state):
|
||||
@@ -423,11 +385,7 @@ class WorkflowCoordinator:
|
||||
continue
|
||||
command = self.actions.complete_task(task_id, invocation_id=self._invocation_id(task_id))
|
||||
elif name == "record_geometry_conclusion":
|
||||
diagnostic_schema = geometry_conclusion_schema(state.working_head, list(self.actions.diagnostic_evidence_refs(task_id, state)))
|
||||
validation = canonical_validate(raw, GeometryConclusion)
|
||||
dynamic_error = canonical_validate_schema(raw, diagnostic_schema) if not isinstance(validation, WorkflowError) else None
|
||||
if dynamic_error is not None:
|
||||
validation = dynamic_error
|
||||
validation = canonical_validate(raw, StatelessGeometryConclusion)
|
||||
if isinstance(validation, WorkflowError):
|
||||
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
|
||||
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
|
||||
@@ -435,10 +393,17 @@ class WorkflowCoordinator:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
validation = GeometryConclusion(
|
||||
working_head=state.working_head,
|
||||
evidence_refs=list(self.actions.diagnostic_evidence_refs(task_id, state)),
|
||||
root_cause=validation.root_cause,
|
||||
decision=validation.decision,
|
||||
corrective_intent=validation.corrective_intent,
|
||||
)
|
||||
command = self.actions.record_geometry_conclusion(task_id, validation, invocation_id=self._invocation_id(task_id))
|
||||
elif name == "rollback_checkpoint":
|
||||
rollback_schema = rollback_checkpoint_schema(state.working_head, list(self.actions.checkpoint_tokens(task_id, state)))
|
||||
validation = canonical_validate(raw, RollbackCheckpoint)
|
||||
rollback_schema = stateless_rollback_checkpoint_schema(list(self.actions.checkpoint_tokens(task_id, state)))
|
||||
validation = canonical_validate(raw, StatelessRollbackCheckpoint)
|
||||
dynamic_error = canonical_validate_schema(raw, rollback_schema) if not isinstance(validation, WorkflowError) else None
|
||||
if dynamic_error is not None:
|
||||
validation = dynamic_error
|
||||
@@ -449,9 +414,10 @@ class WorkflowCoordinator:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
validation = RollbackCheckpoint(working_head=state.working_head, checkpoint_token=validation.checkpoint_token, reason=validation.reason)
|
||||
command = self.actions.rollback_checkpoint(task_id, validation, invocation_id=self._invocation_id(task_id))
|
||||
else:
|
||||
validation = canonical_validate(raw, NextAction)
|
||||
validation = canonical_validate(raw, StatelessNextAction)
|
||||
dynamic_error = canonical_validate_schema(raw, action_schema) if not isinstance(validation, WorkflowError) else None
|
||||
if dynamic_error is not None:
|
||||
validation = dynamic_error
|
||||
@@ -462,6 +428,13 @@ class WorkflowCoordinator:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
validation = NextAction(
|
||||
working_head=state.working_head,
|
||||
intent=validation.intent,
|
||||
requirement_ids=requirement_ids,
|
||||
atomic_id=validation.operation,
|
||||
expected_change=validation.expected_change,
|
||||
)
|
||||
command = self.actions.propose_next_action(task_id, validation, invocation_id=self._invocation_id(task_id))
|
||||
if isinstance(command, Rejected):
|
||||
terminal = self._model_rejection_or_service_failure(task_id, state, name, command.error, format_errors, feedback)
|
||||
@@ -523,10 +496,7 @@ class WorkflowCoordinator:
|
||||
feedback = []
|
||||
continue
|
||||
if name == "inspect_topology":
|
||||
validation = canonical_validate(raw, TopologyRequest)
|
||||
dynamic_error = canonical_validate_schema(raw, topology_request_schema(state.working_head)) if not isinstance(validation, WorkflowError) else None
|
||||
if dynamic_error is not None:
|
||||
validation = dynamic_error
|
||||
validation = canonical_validate(raw, StatelessTopologyRequest)
|
||||
if isinstance(validation, WorkflowError):
|
||||
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
|
||||
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
|
||||
@@ -534,36 +504,9 @@ class WorkflowCoordinator:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
stale = reject_stale_head(state, validation.working_head)
|
||||
payload = stale.payload() if stale else self._topology_payload(task_id, state, validation.kind, validation.limit)
|
||||
if stale is None:
|
||||
observed.add("topology")
|
||||
yield "tool_call", self._event(task_id, name, payload, "error" if stale else "success", usage)
|
||||
feedback = [*feedback, {"role": "tool", "content": json.dumps({"tool": name, "result": payload}, ensure_ascii=False)}][-2:]
|
||||
continue
|
||||
if name == "get_cdsl_operation_contract":
|
||||
validation = canonical_validate(raw, OperationContractRequest)
|
||||
action = state.pending_action
|
||||
dynamic_error = canonical_validate_schema(raw, operation_contract_request_schema(state.working_head, action.atomic_id)) if not isinstance(validation, WorkflowError) and action is not None else None
|
||||
if dynamic_error is not None:
|
||||
validation = dynamic_error
|
||||
if isinstance(validation, WorkflowError):
|
||||
terminal = self._format_failure(task_id, state, name, validation, format_errors, feedback)
|
||||
yield "tool_call", self._event(task_id, name, validation.payload(), "error", usage)
|
||||
if terminal:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
action = state.pending_action
|
||||
stale = reject_stale_head(state, validation.working_head)
|
||||
if stale:
|
||||
payload = stale.payload()
|
||||
elif action is None or validation.atomic_id != action.atomic_id:
|
||||
payload = WorkflowError(ErrorCode.AUTHOR_DECISION_REJECTED, "Only the pending action's operation contract is available.").payload()
|
||||
else:
|
||||
payload = self._operation_payload(task_id, state)
|
||||
observed.add("contract")
|
||||
yield "tool_call", self._event(task_id, name, payload, "error" if "code" in payload else "success", usage)
|
||||
payload = self._topology_payload(task_id, state, validation.kind, validation.limit)
|
||||
observed.add("topology")
|
||||
yield "tool_call", self._event(task_id, name, payload, "success", usage)
|
||||
feedback = [*feedback, {"role": "tool", "content": json.dumps({"tool": name, "result": payload}, ensure_ascii=False)}][-2:]
|
||||
continue
|
||||
fragment = canonical_json_object(raw)
|
||||
@@ -618,6 +561,29 @@ class WorkflowCoordinator:
|
||||
continue
|
||||
yield self._service_failure(task_id, state, review)
|
||||
return
|
||||
candidate = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate.json") or {}
|
||||
action = state.pending_action
|
||||
if action is None:
|
||||
yield self._storage_failure(task_id, "Candidate action is unavailable during review.")
|
||||
return
|
||||
coverage = []
|
||||
for item in candidate.get("claim_results") or ():
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
status = str(item.get("status") or "pending")
|
||||
coverage.append({
|
||||
"claim_id": str(item.get("claim_id") or ""),
|
||||
"status": status if status in {"pass", "pending", "fail", "not_applicable"} else "fail",
|
||||
"evidence_refs": [],
|
||||
})
|
||||
review = CandidateReview(
|
||||
candidate_id=state.candidate_id,
|
||||
working_head=action.working_head,
|
||||
verdict=review.verdict,
|
||||
claim_coverage=coverage,
|
||||
evidence=review.evidence,
|
||||
issues=review.issues,
|
||||
)
|
||||
command = self.actions.record_candidate_review(task_id, review, invocation_id=self._invocation_id(task_id))
|
||||
if isinstance(command, Rejected):
|
||||
terminal = self._model_rejection_or_service_failure(task_id, state, "review_candidate", command.error, format_errors, feedback, actor="reviewer")
|
||||
@@ -631,6 +597,15 @@ class WorkflowCoordinator:
|
||||
if state.phase == TaskPhase.FINAL_VALIDATION:
|
||||
recovered = self.actions.recover_final_review(task_id)
|
||||
if recovered is not None:
|
||||
if isinstance(recovered, Accepted) and recovered.payload.get("status") == "completed":
|
||||
completed_state = self.repository.get_state(task_id)
|
||||
if completed_state is not None:
|
||||
try:
|
||||
self._ensure_recovered_completion_result(task_id, completed_state)
|
||||
except OSError as error:
|
||||
yield self._storage_failure(task_id, str(error))
|
||||
return
|
||||
yield "completion_result_ready", {"taskId": task_id, "status": "success", "path": "completion-result.md"}
|
||||
yield "final_review", {"taskId": task_id, "status": "success" if isinstance(recovered, Accepted) else "error", "result": self._result_payload(recovered), "recovered": True}
|
||||
if isinstance(recovered, Rejected):
|
||||
yield self._service_failure(task_id, state, recovered.error)
|
||||
@@ -652,6 +627,41 @@ class WorkflowCoordinator:
|
||||
continue
|
||||
yield self._service_failure(task_id, state, review)
|
||||
return
|
||||
facts = self.actions._facts(task_id, state.active_revision)
|
||||
claim_results = self.actions._evaluate_claims(task_id, facts)
|
||||
visual_decisions = iter(review.visual_claims)
|
||||
coverage = []
|
||||
for item in claim_results:
|
||||
if item.get("deterministic"):
|
||||
status = str(item.get("status") or "fail")
|
||||
status = status if status in {"pass", "pending", "fail", "not_applicable"} else "fail"
|
||||
else:
|
||||
status = next(visual_decisions).status
|
||||
coverage.append({"claim_id": str(item.get("claim_id") or ""), "status": status, "evidence_refs": []})
|
||||
stateless_review = review
|
||||
review = FinalReview(
|
||||
working_head=state.working_head,
|
||||
verdict=review.verdict,
|
||||
claim_coverage=coverage,
|
||||
evidence=review.evidence,
|
||||
issues=review.issues,
|
||||
)
|
||||
will_complete = (
|
||||
stateless_review.verdict == "pass"
|
||||
and all(item.get("status") == "pass" for item in claim_results if item.get("deterministic"))
|
||||
and all(item.status == "pass" for item in stateless_review.visual_claims)
|
||||
)
|
||||
if will_complete:
|
||||
try:
|
||||
self.requirements.write_completion_result(
|
||||
task_id,
|
||||
state,
|
||||
claim_results=claim_results,
|
||||
review=stateless_review.model_dump(mode="json"),
|
||||
)
|
||||
except OSError as error:
|
||||
yield self._storage_failure(task_id, str(error))
|
||||
return
|
||||
command = self.actions.record_final_review(task_id, review, invocation_id=self._invocation_id(task_id))
|
||||
if isinstance(command, Rejected):
|
||||
terminal = self._model_rejection_or_service_failure(task_id, state, "review_final", command.error, format_errors, feedback, actor="reviewer")
|
||||
@@ -660,6 +670,8 @@ class WorkflowCoordinator:
|
||||
yield terminal
|
||||
return
|
||||
continue
|
||||
if isinstance(command, Accepted) and command.payload.get("status") == "completed":
|
||||
yield "completion_result_ready", {"taskId": task_id, "status": "success", "path": "completion-result.md"}
|
||||
yield "final_review", {"taskId": task_id, "status": "success", "result": self._result_payload(command)}
|
||||
continue
|
||||
state = self.repository.get_state(task_id)
|
||||
@@ -798,50 +810,50 @@ class WorkflowCoordinator:
|
||||
self.repository.record_usage(task_id, usage)
|
||||
return name, raw, usage
|
||||
|
||||
async def _review_requirements(self, task_id: str, reviewer: ModelIdentity, feedback: list[dict[str, Any]] | None = None) -> RequirementsReview | WorkflowError:
|
||||
state = self.repository.get_state(task_id)
|
||||
evaluation_context = self.requirements.evaluation_review_context(task_id)
|
||||
instruction = "Return only structured findings; the service derives coverage and the overall decision. Omit a draft item when it has no finding. Bind every finding to one current draft_id and only source_ids cited by that draft. Use missing_source_semantics for omitted source meaning, claim_mismatch for an incorrect deterministic claim, verification_gap only when a scoped visual claim covers a property unavailable to deterministic verifiers, derivable_conflict only with a registered normalization payload, and ambiguous_conflict only when multiple reasonable interpretations remain; ambiguous conflicts require one precise answerable question. For a full-circle equally spaced pattern, 360/count is the only registered automatic rule. Every independently measurable number, count, dimension, relationship, orientation, material/unit constraint, and single-body requirement must have executable or scoped visual coverage. Do not demand an invented deterministic claim for a verifier gap and do not invent requirements absent from the sources."
|
||||
if evaluation_context is not None:
|
||||
instruction += " The evaluation-only known_validation_capability_gaps are authoritative facts explicitly unavailable to the current deterministic verifier registry, not author errors. Do not request an invented executable claim for one of those gaps. When an otherwise covered draft item has a scoped visual acceptance claim for a declared gap, emit verification_gap once; the service will freeze it with an explicit risk and require final independent visual review."
|
||||
return await self._review_tool(task_id, reviewer, "review_requirements", RequirementsReview, {
|
||||
"source_index": self.artifacts.read_source_index(task_id), "draft": self._requirements_draft(state), "user_clarifications": self._user_clarifications(task_id),
|
||||
"evaluation_context": evaluation_context,
|
||||
"instruction": instruction,
|
||||
}, schema=self.requirements.review_schema(task_id), feedback=feedback)
|
||||
|
||||
async def _review_candidate(self, task_id: str, reviewer: ModelIdentity, state: TaskState, feedback: list[dict[str, Any]] | None = None) -> CandidateReview | WorkflowError:
|
||||
async def _review_candidate(self, task_id: str, reviewer: ModelIdentity, state: TaskState, feedback: list[dict[str, Any]] | None = None) -> StatelessCandidateReview | WorkflowError:
|
||||
candidate = self.artifacts.read_stage_json(task_id, state.candidate_stage_id, "candidate.json")
|
||||
action = state.pending_action
|
||||
if not isinstance(candidate, dict) or action is None:
|
||||
return WorkflowError(ErrorCode.STORAGE_FAILURE, "Candidate review facts are unavailable.", retryable=True)
|
||||
claim_ids = [str(item.get("claim_id") or "") for item in candidate.get("claim_results") or () if isinstance(item, dict) and item.get("claim_id")]
|
||||
return await self._review_tool(task_id, reviewer, "review_candidate", CandidateReview, {
|
||||
# Candidate review is checkpoint-scoped. The frozen contract is
|
||||
# enough for this decision; source text is retained only for the
|
||||
# final review where it guards against a frozen-contract omission.
|
||||
"requirements_contract": self._requirements_contract(task_id, state),
|
||||
"candidate_id": state.candidate_id, "working_head": action.working_head,
|
||||
"action": {"action_id": action.action_id, "intent": action.intent, "expected_change": action.expected_change, "requirement_ids": list(action.requirement_ids)},
|
||||
"candidate": candidate, "render_manifest": candidate.get("render_manifest") or {},
|
||||
"instruction": "Independently review only this checkpoint against the supplied action, not as the finished model. Return every provided claim ID exactly once. A deterministic claim with status pending is explicitly deferred to a later action: cover it as pending and still return verdict accept unless this candidate contradicts the current action, violates a global invariant, or the renders show this action itself is wrong. In particular, when the action establishes a flange base, the absence of a later through bore is pending and MUST NOT cause rejection. Deterministic results are evidence and cannot be overridden.",
|
||||
}, schema=candidate_review_schema(state.candidate_id, action.working_head, claim_ids), feedback=feedback)
|
||||
return await self._review_tool(task_id, reviewer, "review_candidate", StatelessCandidateReview, {
|
||||
"requirements": self._public_requirements(self._requirements_contract(task_id, state)),
|
||||
"action": {"intent": action.intent, "expected_change": action.expected_change, "operation": action.atomic_id},
|
||||
"candidate_facts": self._public_candidate(candidate),
|
||||
"render_manifest": candidate.get("render_manifest") or {},
|
||||
"instruction": "Review only whether the current checkpoint correctly performs the stated action. Deterministic facts are authoritative. Do not return task, action, candidate, requirement, claim, revision, head, or evidence identifiers.",
|
||||
}, feedback=feedback)
|
||||
|
||||
async def _review_final(self, task_id: str, reviewer: ModelIdentity, state: TaskState, feedback: list[dict[str, Any]] | None = None) -> FinalReview | WorkflowError:
|
||||
async def _observe_images(self, task_id: str, reviewer: ModelIdentity, image_paths: list[str]) -> ImageObservation | WorkflowError:
|
||||
return await self._review_tool(task_id, reviewer, "observe_images", ImageObservation, {
|
||||
"source_requirements": self.artifacts.read_source_requirements(task_id),
|
||||
"reference_image_paths": image_paths,
|
||||
"instruction": (
|
||||
"Inspect every supplied reference image once. Describe visible part geometry, view directions, readable dimensions, holes and profiles, confidence, assumptions, and uncertainties. "
|
||||
"Do not create CAD operations and do not return attachment or runtime identifiers."
|
||||
),
|
||||
})
|
||||
|
||||
async def _review_final(self, task_id: str, reviewer: ModelIdentity, state: TaskState, feedback: list[dict[str, Any]] | None = None) -> StatelessFinalReview | WorkflowError:
|
||||
facts = self.actions._facts(task_id, state.active_revision)
|
||||
results = self.actions._evaluate_claims(task_id, facts)
|
||||
return await self._review_tool(task_id, reviewer, "review_final", FinalReview, {
|
||||
"source_requirements": self.artifacts.read_source_requirements(task_id), "requirements_contract": self._requirements_contract(task_id, state),
|
||||
"revision_id": state.active_revision, "working_head": state.working_head, "claim_results": results, "render_manifest": (facts.get("report") or {}).get("render_manifest") or {},
|
||||
"instruction": "Independently review final model evidence. Return every claim ID exactly once. Deterministic results are final gates and cannot be overridden.",
|
||||
}, schema=final_review_schema(state.working_head, [str(item.get("claim_id") or "") for item in results if item.get("claim_id")]), feedback=feedback)
|
||||
visual_claims = [item for item in results if not item.get("deterministic")]
|
||||
return await self._review_tool(task_id, reviewer, "review_final", StatelessFinalReview, {
|
||||
"source_requirements": self.artifacts.read_source_requirements(task_id),
|
||||
"requirements": self._public_requirements(self._requirements_contract(task_id, state)),
|
||||
"deterministic_results": [self._public_claim_result(item) for item in results if item.get("deterministic")],
|
||||
"visual_claims": [self._public_claim_result(item) for item in visual_claims],
|
||||
"render_manifest": (facts.get("report") or {}).get("render_manifest") or {},
|
||||
"reference_image_paths": self.artifacts.source_image_paths(task_id),
|
||||
"instruction": "Review the final CAD renders against the original reference images and the ordered visual claims. Return exactly one visual_claims decision for each supplied visual claim, in the same order. Deterministic results are final. Do not return any runtime identifiers.",
|
||||
}, schema=stateless_final_review_schema(len(visual_claims)), feedback=feedback)
|
||||
|
||||
async def _review_tool(self, task_id: str, reviewer: ModelIdentity, name: str, model_type: type[T], payload: dict[str, Any], *, schema: dict[str, Any] | None = None, feedback: list[dict[str, Any]] | None = None) -> T | WorkflowError:
|
||||
if feedback:
|
||||
payload = {**payload, "previous_schema_or_state_error": str(feedback[-1].get("content") or "")[:2_000]}
|
||||
try:
|
||||
tool = self._tool(name, schema or model_type)
|
||||
response = await self.review_gateway.review(kind="requirements" if name == "review_requirements" else "candidate" if name == "review_candidate" else "final", payload=payload, tool=tool, provider_id=reviewer.provider_id, model_id=reviewer.model_id)
|
||||
kind = "image_observation" if name == "observe_images" else "candidate" if name == "review_candidate" else "final"
|
||||
response = await self.review_gateway.review(kind=kind, payload=payload, tool=tool, provider_id=reviewer.provider_id, model_id=reviewer.model_id)
|
||||
except AdapterUnavailable as error:
|
||||
error_code = (
|
||||
ErrorCode.RENDER_SERVICE_UNAVAILABLE
|
||||
@@ -1030,7 +1042,7 @@ class WorkflowCoordinator:
|
||||
tokens = self.runtime.selector_tokens(topology)
|
||||
eligible_tokens = self._selector_tokens_for_contract(contract, tokens)
|
||||
if selector_shape == "required" and len(eligible_tokens) > 16 and "topology" not in seen:
|
||||
return [self._tool("inspect_topology", topology_request_schema(state.working_head))]
|
||||
return [self._tool("inspect_topology", StatelessTopologyRequest)]
|
||||
references = self.runtime.reference_tokens(self.artifacts.read_active_cdsl(task_id, state.active_revision))
|
||||
description = "Submit exactly one CDSL feature for the pending action."
|
||||
if action.atomic_id.startswith("hole_"):
|
||||
@@ -1052,66 +1064,36 @@ class WorkflowCoordinator:
|
||||
if not state.repair_required:
|
||||
return []
|
||||
if self.actions.rollback_available(task_id, state):
|
||||
return [self._tool("rollback_checkpoint", rollback_checkpoint_schema(state.working_head, list(self.actions.checkpoint_tokens(task_id, state))))]
|
||||
return [self._tool("rollback_checkpoint", stateless_rollback_checkpoint_schema(list(self.actions.checkpoint_tokens(task_id, state))))]
|
||||
if self.actions.repair_action_ready(task_id, state):
|
||||
return []
|
||||
evidence_refs = list(self.actions.diagnostic_evidence_refs(task_id, state))
|
||||
if not evidence_refs:
|
||||
return []
|
||||
return [self._tool("record_geometry_conclusion", geometry_conclusion_schema(state.working_head, evidence_refs))]
|
||||
|
||||
def _requirements_author_tools(self, task_id: str, draft_schema: dict[str, Any], patch_schema: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""Expose exactly one requirement command from persisted draft facts."""
|
||||
state = self.repository.get_state(task_id)
|
||||
draft = self._requirements_draft(state)
|
||||
review = self._requirements_review(task_id, state)
|
||||
if isinstance(review, dict) and review.get("decision") == "revise":
|
||||
return [self._tool("patch_requirements_draft", patch_schema)]
|
||||
source_ids = set(self.artifacts.read_source_index(task_id))
|
||||
covered = {
|
||||
str(source_id)
|
||||
for item in draft.get("items") or ()
|
||||
if isinstance(item, dict)
|
||||
for source_id in item.get("source_ids") or ()
|
||||
}
|
||||
if draft.get("items") and source_ids.issubset(covered):
|
||||
return [self._tool("finalize_requirements_draft", EmptyCommand)]
|
||||
return [self._tool("submit_requirements_draft_batch", draft_schema)]
|
||||
return [self._tool("record_geometry_conclusion", StatelessGeometryConclusion)]
|
||||
|
||||
def _author_context(self, task_id: str, feedback: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
state = self.repository.get_state(task_id)
|
||||
if state is None:
|
||||
return []
|
||||
if state.phase == TaskPhase.DRAFTING_REQUIREMENTS:
|
||||
draft = self._requirements_draft(state)
|
||||
review = self._requirements_review(task_id, state)
|
||||
if isinstance(review, dict) and review.get("decision") == "revise":
|
||||
current_ids = [str(item.get("draft_id") or "") for item in draft.get("items") or () if isinstance(item, dict) and item.get("draft_id")]
|
||||
example_id = current_ids[0] if current_ids else "draft_001"
|
||||
instruction = (
|
||||
"The independent reviewer requested revisions. Call patch_requirements_draft with exactly one outer patches array. "
|
||||
f"Each patch nests target_draft_id (for example {example_id!r}) inside patches[], alongside op. "
|
||||
"For op=replace, item is the complete replacement RequirementInput and MUST NOT include draft_id; draft_id is server-owned. "
|
||||
"Do not put target_draft_id or item at the top level. The root object must have this exact shape: "
|
||||
f'{{"patches":[{{"target_draft_id":"{example_id}","op":"replace","item":{{...}}}}]}}. '
|
||||
"Then call finalize_requirements_draft for another review. Do not write Markdown."
|
||||
)
|
||||
evaluation_context = self.requirements.evaluation_review_context(task_id)
|
||||
if evaluation_context is not None:
|
||||
instruction += " For a declared evaluation-only validation capability gap, an existing scoped visual claim is valid coverage. Preserve it and do not add a made-up deterministic substitute solely to address that gap."
|
||||
else:
|
||||
covered = {str(source_id) for item in draft.get("items") or () if isinstance(item, dict) for source_id in item.get("source_ids") or ()}
|
||||
source_ids = set(self.artifacts.read_source_index(task_id))
|
||||
instruction = "Every source is represented in the draft. Call finalize_requirements_draft now; do not write Markdown." if draft.get("items") and source_ids.issubset(covered) else "Create at most 8 structured requirement items that cover every source. Do not write Markdown."
|
||||
content = {"protocol": "cad.v3", "phase": state.phase.value, "source_index": self.artifacts.read_source_index(task_id), "source_requirements": self.artifacts.read_source_requirements(task_id), "user_clarifications": self._user_clarifications(task_id), "draft": draft, "review": review, "evaluation_context": self.requirements.evaluation_review_context(task_id), "instruction": instruction}
|
||||
content = {
|
||||
"protocol": "cad.v3.spec.v1",
|
||||
"source_requirements": self.artifacts.read_source_requirements(task_id),
|
||||
"image_observation": self.artifacts.read_json(task_id, "documents/image-observation.json"),
|
||||
"user_clarifications": self._user_clarifications(task_id),
|
||||
"instruction": (
|
||||
"Return one complete bounded requirements specification. Use deterministic claims only when the supplied registry can execute them; otherwise use a scoped visual claim. "
|
||||
"Unspecified design choices are assumptions and must not block generation. Return clarification only when two quoted source statements cannot both be followed and the user must choose; ask exactly one question. "
|
||||
"Never return source, task, draft, requirement, claim, revision, candidate, action, head, or evidence identifiers. Do not solve semantic conflicts by changing a user value."
|
||||
),
|
||||
}
|
||||
else:
|
||||
contract = self._requirements_contract(task_id, state) or {}
|
||||
compact = [{
|
||||
"requirement_id": item.get("requirement_id"),
|
||||
"statement": item.get("statement"),
|
||||
"acceptance_claims": [
|
||||
{
|
||||
"claim_id": claim.get("claim_id"),
|
||||
"claim_kind": claim.get("claim_kind"),
|
||||
"expected": claim.get("expected"),
|
||||
}
|
||||
@@ -1135,18 +1117,10 @@ class WorkflowCoordinator:
|
||||
for token, value in tokens.items() if token in allowed
|
||||
][:16]
|
||||
instruction = "The exact operation contract and eligible selector summary are attached. Submit one fragment; call inspect_topology only when the selector summary is marked truncated."
|
||||
content = {"protocol": "cad.v3", "phase": state.phase.value, "working_head": state.working_head, "requirements": compact, "verification_warnings": contract.get("verification_warnings") or [], "applied_normalizations": contract.get("applied_normalizations") or [], "claim_coverage": self.actions.claim_summary(task_id, state), "model_summary": self.actions.model_summary(task_id, state), "active_revision": state.active_revision, "pending_action": self._pending_context(state), "operation_contract": operation_payload, "selector_summary": selector_summary, "selector_summary_truncated": bool(action is not None and selector_shape == "required" and len(self._selector_tokens_for_contract(operation or {}, self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision)))) > len(selector_summary)), "recent_failures": self._recent_failure_constraints(task_id, state), "recent_ledger": (self.repository.get_task_projection(task_id) or {}).get("action_ledger_summary", [])[-4:], "diagnostic_evidence_refs": list(self.actions.diagnostic_evidence_refs(task_id, state)) if state.repair_required else [], "repair_diagnostics": self.actions.repair_diagnostics(task_id, state), "rollback_checkpoints": list(self.actions.checkpoint_tokens(task_id, state)) if self.actions.rollback_available(task_id, state) else [], "instruction": instruction}
|
||||
content = {"protocol": "cad.v3", "phase": state.phase.value, "requirements": compact, "verification_warnings": contract.get("verification_warnings") or [], "claim_coverage": [self._public_claim_result(item) for item in self.actions.claim_summary(task_id, state)], "model_summary": self.actions.model_summary(task_id, state), "pending_action": self._public_pending_context(state), "operation_contract": self._public_operation_payload(operation_payload), "selector_summary": selector_summary, "selector_summary_truncated": bool(action is not None and selector_shape == "required" and len(self._selector_tokens_for_contract(operation or {}, self.runtime.selector_tokens(self.artifacts.read_topology(task_id, state.active_revision)))) > len(selector_summary)), "recent_failures": self._recent_failure_constraints(task_id, state), "repair_diagnostics": self.actions.repair_diagnostics(task_id, state), "rollback_checkpoints": list(self.actions.checkpoint_tokens(task_id, state)) if self.actions.rollback_available(task_id, state) else [], "instruction": instruction}
|
||||
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:]]
|
||||
|
||||
def _requirements_draft(self, state: TaskState | None) -> dict[str, Any]:
|
||||
if state is None:
|
||||
return {"schema_version": "cad.requirements-draft.v1", "revision": 0, "items": []}
|
||||
return self.artifacts.read_requirements_draft(state.task_id, state.requirements_draft_path)
|
||||
|
||||
def _requirements_review(self, task_id: str, state: TaskState | None) -> dict[str, Any] | None:
|
||||
return self.artifacts.read_requirements_review(task_id, state.requirements_review_path if state is not None else "")
|
||||
|
||||
def _user_clarifications(self, task_id: str) -> list[dict[str, str]]:
|
||||
clarifications: list[dict[str, str]] = []
|
||||
for event in self.repository.ledger_events(task_id):
|
||||
@@ -1162,32 +1136,12 @@ class WorkflowCoordinator:
|
||||
def waiting_for_user_terminal(self, task_id: str, state: TaskState) -> dict[str, Any]:
|
||||
"""Expose the persisted requirement question when a task is parked.
|
||||
|
||||
The review artifact is the durable source of a human decision. The
|
||||
terminal event deliberately carries only its explicit questions, not
|
||||
the full reviewer report, so it remains useful to both SSE clients and
|
||||
conversation history without leaking unrelated review detail.
|
||||
The clarification artifact is the durable source of the single human
|
||||
decision needed to continue this task.
|
||||
"""
|
||||
review = self._requirements_review(task_id, state) or {}
|
||||
questions: list[str] = []
|
||||
unresolved: list[dict[str, str]] = []
|
||||
for finding in review.get("findings") or ():
|
||||
if not isinstance(finding, dict):
|
||||
continue
|
||||
question = str(finding.get("question") or "").strip()
|
||||
finding_type = str(finding.get("finding_type") or "")
|
||||
if finding_type == "ambiguous_conflict" and question and question not in questions:
|
||||
questions.append(question)
|
||||
if finding_type == "ambiguous_conflict":
|
||||
unresolved.append({
|
||||
"draftId": str(finding.get("draft_id") or ""),
|
||||
"reasonCode": finding_type,
|
||||
"question": question,
|
||||
})
|
||||
issues = [
|
||||
str(finding.get("description") or "").strip()
|
||||
for finding in review.get("findings") or ()
|
||||
if isinstance(finding, dict) and str(finding.get("description") or "").strip()
|
||||
]
|
||||
clarification = self.artifacts.read_json(task_id, state.clarification_path) if state.clarification_path else None
|
||||
question = str((clarification or {}).get("question") or "").strip()
|
||||
questions = [question] if question else []
|
||||
if not questions:
|
||||
raise RuntimeError("WAITING_FOR_USER requires at least one answerable requirements question")
|
||||
message = f"Requirements need a user decision. {questions[0]}"
|
||||
@@ -1198,14 +1152,10 @@ class WorkflowCoordinator:
|
||||
"code": state.last_error.value if state.last_error else ErrorCode.WAITING_FOR_USER.value,
|
||||
"message": message,
|
||||
"questions": questions,
|
||||
"reviewPath": state.requirements_review_path,
|
||||
"clarificationPath": state.clarification_path,
|
||||
"blockerType": "requirements_ambiguity",
|
||||
"userActionRequired": True,
|
||||
}
|
||||
if issues:
|
||||
payload["issues"] = issues
|
||||
if unresolved:
|
||||
payload["unresolved"] = unresolved
|
||||
return payload
|
||||
|
||||
def _requirements_contract(self, task_id: str, state: TaskState | None) -> dict[str, Any] | None:
|
||||
@@ -1213,6 +1163,85 @@ class WorkflowCoordinator:
|
||||
return None
|
||||
return self.artifacts.read_requirements_contract(task_id, state.requirements_contract_path)
|
||||
|
||||
def _ensure_recovered_completion_result(self, task_id: str, state: TaskState) -> None:
|
||||
result_path = self.artifacts.artifact_path(task_id, "completion-result.md")
|
||||
if result_path.is_file():
|
||||
return
|
||||
facts = self.actions._facts(task_id, state.active_revision)
|
||||
claim_results = self.actions._evaluate_claims(task_id, facts)
|
||||
raw_review = self.artifacts.read_json(task_id, f"reviews/final/{state.active_revision}/final-review.json") or {}
|
||||
coverage = {
|
||||
str(item.get("claim_id") or ""): item
|
||||
for item in raw_review.get("claim_coverage") or ()
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
visual_claims = [
|
||||
{
|
||||
"status": str(coverage.get(str(item.get("claim_id") or ""), {}).get("status") or "fail"),
|
||||
"evidence": "; ".join(str(value) for value in raw_review.get("evidence") or ()) or "Recovered final review decision.",
|
||||
}
|
||||
for item in claim_results
|
||||
if not item.get("deterministic")
|
||||
]
|
||||
self.requirements.write_completion_result(
|
||||
task_id,
|
||||
state,
|
||||
claim_results=claim_results,
|
||||
review={"visual_claims": visual_claims},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _public_requirements(contract: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"statement": str(item.get("statement") or ""),
|
||||
"assumptions": list(item.get("assumptions") or []),
|
||||
"acceptance_claims": [
|
||||
{
|
||||
"claim_kind": str(claim.get("claim_kind") or ""),
|
||||
"expected": claim.get("expected") or {},
|
||||
"verification_mode": str(claim.get("verification_mode") or ""),
|
||||
}
|
||||
for claim in item.get("acceptance_claims") or ()
|
||||
if isinstance(claim, dict)
|
||||
],
|
||||
}
|
||||
for item in (contract or {}).get("requirements") or ()
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _public_claim_result(item: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
key: value
|
||||
for key, value in item.items()
|
||||
if key not in {"claim_id", "requirement_id", "evidence_refs"}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _public_candidate(cls, candidate: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"claim_results": [cls._public_claim_result(item) for item in candidate.get("claim_results") or () if isinstance(item, dict)],
|
||||
"operation_verifier_results": [cls._public_claim_result(item) for item in candidate.get("operation_verifier_results") or () if isinstance(item, dict)],
|
||||
"health": candidate.get("health") or {},
|
||||
"model_summary": candidate.get("model_summary") or {},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _public_pending_context(state: TaskState) -> dict[str, Any] | None:
|
||||
action = state.pending_action
|
||||
return {"intent": action.intent, "operation": action.atomic_id, "expected_change": action.expected_change} if action else None
|
||||
|
||||
@staticmethod
|
||||
def _public_operation_payload(payload: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
return {
|
||||
"operation": payload.get("atomic_id"),
|
||||
"contract": payload.get("contract"),
|
||||
"fragment_schema": payload.get("fragment_schema"),
|
||||
}
|
||||
|
||||
def _projected_terminal(self, task_id: str, state: TaskState) -> dict[str, Any]:
|
||||
projection = self.repository.get_task_projection(task_id) or {}
|
||||
return {
|
||||
@@ -1227,7 +1256,6 @@ class WorkflowCoordinator:
|
||||
"userActionRequired": bool(projection.get("user_action_required")),
|
||||
"verificationStatus": str(projection.get("verification_status") or "verified"),
|
||||
"verificationWarnings": projection.get("verification_warnings") or [],
|
||||
"appliedNormalizations": projection.get("applied_normalizations") or [],
|
||||
}
|
||||
|
||||
def _operation_payload(self, task_id: str, state: TaskState) -> dict[str, Any]:
|
||||
@@ -1429,6 +1457,48 @@ class WorkflowCoordinator:
|
||||
"field_errors": list(error.field_errors),
|
||||
}
|
||||
|
||||
def _requirements_format_failure(
|
||||
self,
|
||||
task_id: str,
|
||||
state: TaskState,
|
||||
error: WorkflowError,
|
||||
counters: dict[str, int],
|
||||
feedback: list[dict[str, Any]],
|
||||
) -> tuple[str, dict[str, Any]] | None:
|
||||
key = "requirements_spec"
|
||||
counters[key] = counters.get(key, 0) + 1
|
||||
feedback[:] = [self._feedback(error)]
|
||||
if counters[key] < 2:
|
||||
return None
|
||||
failed = transition(state, "failed", error=ErrorCode.REQUIREMENTS_SPEC_INVALID)
|
||||
self.repository.compare_and_swap(failed, events=[{
|
||||
"event": "requirements_spec_invalid",
|
||||
"code": ErrorCode.REQUIREMENTS_SPEC_INVALID.value,
|
||||
"message": error.message,
|
||||
"field_errors": list(error.field_errors),
|
||||
}])
|
||||
return "task_terminal", {
|
||||
"taskId": task_id,
|
||||
"lifecycle": "failed",
|
||||
"code": ErrorCode.REQUIREMENTS_SPEC_INVALID.value,
|
||||
"message": "Requirements specification remained unreadable after one field-level correction.",
|
||||
"tool": "submit_requirements_spec",
|
||||
"field_errors": list(error.field_errors),
|
||||
"userActionRequired": False,
|
||||
}
|
||||
|
||||
def _requirements_rejection(
|
||||
self,
|
||||
task_id: str,
|
||||
state: TaskState,
|
||||
error: WorkflowError,
|
||||
counters: dict[str, int],
|
||||
feedback: list[dict[str, Any]],
|
||||
) -> tuple[str, dict[str, Any]] | None:
|
||||
if error.retryable or error.code == ErrorCode.STORAGE_FAILURE:
|
||||
return self._service_failure(task_id, state, error)
|
||||
return self._requirements_format_failure(task_id, state, error, counters, feedback)
|
||||
|
||||
@staticmethod
|
||||
def _tool(name: str, model: type[BaseModel] | dict[str, Any]) -> dict[str, Any]:
|
||||
parameters = model if isinstance(model, dict) else model.model_json_schema()
|
||||
|
||||
Reference in New Issue
Block a user