fix(company): make delivery-review self-evolution survive the hardened runtime
Approving (or sending feedback on) the delivery review card runs employee self-evolution as company work items, but the finalizer required the turn's FINAL chat text to be bare JSON. The hardened native runtime appends a verification status line to every final text and the manager dispatch guard displaces the final message with a justification, so every reflection died with invalid_self_evolution_json even when the model produced valid patches on all three attempts, and the FAILED items polluted the delivered run's terminal verdict. - Add a submit_self_evolution_patches tool as the authoritative result channel (exposed only on self-evolution work items, approval-exempt). The text parser becomes a fallback that scans fenced blocks and balanced JSON objects, and retry feedback now carries the concrete parse failure plus the tool instruction. - Settle abandoned reflections as CANCELLED (self_evolution_abandoned) and exclude kind=self_evolution from run-lifecycle settlement so an opt-in reflection can never dirty a delivered run. - Claim the review card with a consuming CAS before spawning (duplicate approve/feedback replies answer idempotently instead of re-entering), bound the reflection run with a 40-minute deadline that cancels leftover self-evolution items, and hand the claim back to pending when the consumed run crashes mid-flight. Verified live on real runs: the unfixed code failed the approve path in 90s with zero patches recorded; with the fix both the approve and the feedback paths recorded patches end-to-end (CEO->COO and CEO->CMO cascades, zero retries, human feedback reflected in patch content). tests/: 1924 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,10 @@ from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from opc.core.company_tools import COMPANY_COLLABORATION_TOOL_NAMES
|
||||
from opc.core.company_tools import (
|
||||
COMPANY_COLLABORATION_TOOL_NAMES,
|
||||
is_self_evolution_work_item,
|
||||
)
|
||||
from opc.core.models import (
|
||||
AgentMessage,
|
||||
CommsSemanticType,
|
||||
@@ -861,6 +864,16 @@ _EXTERNAL_BRIDGE_ARGUMENT_EXAMPLES: dict[str, dict[str, Any]] = {
|
||||
"summary": "The user accepted this delivery and no further internal work is required.",
|
||||
"user_message": "Acknowledged. I am closing the human review for this delivery.",
|
||||
},
|
||||
"submit_self_evolution_patches": {
|
||||
"patches": [
|
||||
{
|
||||
"summary": "One-paragraph lesson from the delivered work cycle.",
|
||||
"strengths": ["Concrete behavior worth repeating"],
|
||||
"adjustments": ["Concrete behavior to change next time"],
|
||||
"confidence": 0.8,
|
||||
}
|
||||
],
|
||||
},
|
||||
"send_dm": {
|
||||
"to_agent": "reviewer",
|
||||
"subject": "Need review",
|
||||
@@ -913,6 +926,10 @@ _EXTERNAL_BRIDGE_ARGUMENT_NOTES: dict[str, tuple[str, ...]] = {
|
||||
"Use only when you decide the owner-facing delivery review is complete and should be closed.",
|
||||
"Do not call this for requested changes; revise the board, delegate work, or respond instead.",
|
||||
),
|
||||
"submit_self_evolution_patches": (
|
||||
"Available only on self-evolution work items; the recorded patches are the authoritative result of the turn.",
|
||||
"Pass `patches: []` when no experience update is needed; omit `employee_id` to target this work item's assigned employee.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -923,6 +940,7 @@ _EXTERNAL_CLI_KEY_ARGUMENTS: dict[str, tuple[str, ...]] = {
|
||||
"inbox": ("action", "message_ids", "limit"),
|
||||
"manager_board_read": ("parent_work_item_id", "include_children"),
|
||||
"close_human_review": ("summary", "user_message"),
|
||||
"submit_self_evolution_patches": ("patches",),
|
||||
"send_dm": ("to_agent", "subject", "body", "blocking", "timeout_action", "timeout_seconds"),
|
||||
"reply_message": ("message_id", "body", "subject"),
|
||||
"broadcast_issue": ("to_agents", "subject", "body", "blocking", "timeout_action", "timeout_seconds"),
|
||||
@@ -1602,6 +1620,51 @@ def create_collaboration_tools(
|
||||
"user_message": close_user_message,
|
||||
}
|
||||
|
||||
async def submit_self_evolution_patches(
|
||||
patches: list[dict[str, Any]] | None = None,
|
||||
task: Task | None = None,
|
||||
) -> dict[str, Any]:
|
||||
role_id = _active_role(task)
|
||||
if not task or not role_id:
|
||||
raise ValueError("submit_self_evolution_patches requires an active assigned task")
|
||||
if not is_self_evolution_work_item(task):
|
||||
raise ValueError(
|
||||
"submit_self_evolution_patches is only available on self-evolution work items"
|
||||
)
|
||||
normalized: list[dict[str, Any]] = []
|
||||
for index, patch in enumerate(list(patches or [])):
|
||||
if not isinstance(patch, dict):
|
||||
raise ValueError(f"patches[{index}] must be a JSON object")
|
||||
employee_id = str(patch.get("employee_id", "") or "").strip()
|
||||
if not employee_id:
|
||||
assignment = dict(task.metadata.get("employee_assignment", {}) or {})
|
||||
employee_id = str(assignment.get("employee_id", "") or "").strip()
|
||||
if not employee_id:
|
||||
raise ValueError(
|
||||
f"patches[{index}] needs an employee_id and this work item has no assigned employee"
|
||||
)
|
||||
normalized.append({**patch, "employee_id": employee_id})
|
||||
task.metadata = dict(task.metadata or {})
|
||||
task.context_snapshot = dict(task.context_snapshot or {})
|
||||
record = {
|
||||
"patches": normalized,
|
||||
"submitted_at": datetime.now().isoformat(),
|
||||
"submitted_by_role": role_id,
|
||||
}
|
||||
task.metadata["self_evolution_submitted_patch"] = record
|
||||
task.context_snapshot["self_evolution_submitted_patch"] = dict(record)
|
||||
store = getattr(communication, "store", None)
|
||||
if store is not None and hasattr(store, "save_task"):
|
||||
await store.save_task(task)
|
||||
return {
|
||||
"status": "recorded",
|
||||
"patch_count": len(normalized),
|
||||
"note": (
|
||||
"Patches recorded for this self-evolution work item. "
|
||||
"Finish the turn with a short completion note."
|
||||
),
|
||||
}
|
||||
|
||||
async def delegate_work(
|
||||
items: list[dict[str, Any]],
|
||||
planning_context: str = "",
|
||||
@@ -3064,6 +3127,40 @@ def create_collaboration_tools(
|
||||
func=close_human_review,
|
||||
category="collaboration",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="submit_self_evolution_patches",
|
||||
description=(
|
||||
"Record employee experience patches for the current self-evolution work item. "
|
||||
"This tool is the authoritative channel for self-evolution results: patches recorded "
|
||||
"here are applied regardless of what the final turn text says. Pass an empty patches "
|
||||
"list when no experience update is needed for this role. Each patch targets this work "
|
||||
"item's assigned employee (employee_id is filled in automatically when omitted)."
|
||||
),
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"patches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"employee_id": {"type": "string"},
|
||||
"summary": {"type": "string"},
|
||||
"strengths": {"type": "array", "items": {"type": "string"}},
|
||||
"adjustments": {"type": "array", "items": {"type": "string"}},
|
||||
"avoid_next_time": {"type": "array", "items": {"type": "string"}},
|
||||
"routing_notes": {"type": "string"},
|
||||
"evidence_task_ids": {"type": "array", "items": {"type": "string"}},
|
||||
"confidence": {"type": "number"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["patches"],
|
||||
},
|
||||
func=submit_self_evolution_patches,
|
||||
category="collaboration",
|
||||
),
|
||||
ToolDefinition(
|
||||
name="delegate_work",
|
||||
description=(
|
||||
|
||||
Reference in New Issue
Block a user