6fc5ad6be9
Align native context management with the Claude Code / Codex model: entry-capped tool results, history frozen below the threshold, one high-quality summary at the wall — instead of the old pipeline that microcompacted old messages from 60% usage and hid everything past 40 messages behind a snip marker with no summary. - context pipeline: history below the hard threshold is never rewritten (model quality and prompt-cache prefixes depend on byte-identical old messages); the 60% tool-aware microcompact and the 40-message history snip move to an emergency-only fallback used under overflow pressure when the summarizer is unavailable or circuit-broken. - durable compaction (was a stub): at usage >= context_guard.hard_threshold (now 0.90, soft_threshold removed) the old span is folded into a 9-section summary via the new HistoryCompactor.summarize_runtime_history, keeping the system head, the seed user request verbatim on every round (injected session-memory/artifact messages shift the stale base_prefix_len, so the fold start is structure-aware), and a pairing-safe recent tail. A previous summary stays foldable, so exactly one summary exists at a time and rounds chain. - token accounting anchors on the provider-reported prompt size of the latest request (max with the local estimate). - reactive_compaction.circuit_breaker_failures (previously unread) now stops repeated summarizer failures; provider overflow errors retry through the same pipeline, summary-first. - tool-result budget clip keeps head and tail instead of tail-chopping. - chat-side transcripts get the same treatment: new MemoryManager.maybe_compact_session_history wires the threshold-gated maybe_compact_session into secretary, office_ui dispatcher, and context_loader before prompt building, closing the unbounded-growth path; dead no-op compactor entries (maybe_compact_after_message, should_compact_prompt) removed. Verified by 13 new tests (history sanctity below threshold, multi-round single-summary/seed-verbatim/chain invariants, breaker, emergency fallback, provider-overflow end-to-end recovery) plus a live-provider probe: multi-round compaction with the model completing correctly from summarized context. Full suite: 1859 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4024 lines
180 KiB
Python
4024 lines
180 KiB
Python
"""Native Runtime V2: streaming LLM loop + structured tool execution."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import time
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from typing import Any, Awaitable, Callable, Optional
|
|
|
|
from loguru import logger
|
|
|
|
from opc.core.config import OPCConfig
|
|
from opc.core.events import EventBus
|
|
from opc.core.models import OPCEvent, PermissionResolution, Task, TaskResult, TaskStatus, VerificationEvidence
|
|
from opc.layer2_organization.collaboration_policy import ownership_guard_violation
|
|
from opc.layer2_organization.work_item_identity import (
|
|
projection_id_for_task,
|
|
result_delivery_identity_payload_for_task,
|
|
turn_type_for_task,
|
|
work_item_identity_payload_for_task,
|
|
)
|
|
from opc.layer3_agent.runtime_v2.permissions import RuntimePermissionAdapter
|
|
from opc.layer3_agent.runtime_v2.streaming_tool_executor import StreamingToolExecutor
|
|
from opc.layer3_agent.runtime_v2.subagents import ChildAgentFactory, SubagentManager
|
|
from opc.layer3_agent.runtime_v2.tool_hooks import RuntimeToolHookBus, RuntimeToolHookContext
|
|
from opc.layer3_agent.runtime_v2.tool_planner import ToolPlanner
|
|
from opc.layer3_agent.prompt_harness import (
|
|
render_runtime_artifact_messages,
|
|
strip_runtime_artifact_messages,
|
|
)
|
|
from opc.layer3_agent.prompt_harness.artifacts import build_runtime_artifact_record
|
|
from opc.layer3_agent.prompt_harness.types import RuntimeArtifact
|
|
from opc.layer4_tools.execution_context import ensure_task_execution_context
|
|
from opc.layer4_tools.output_budget import clip_text
|
|
from opc.layer4_tools.registry import ToolDefinition
|
|
from opc.layer4_tools.registry import ToolRegistry
|
|
from opc.layer6_observability.cost_tracker import CostEntry
|
|
from opc.llm.provider import LLMProvider
|
|
|
|
|
|
ApprovalCallback = Callable[[ToolDefinition, dict[str, Any], Optional[Task], Any], Awaitable[tuple[bool, Any]]]
|
|
PrefetchProvider = Callable[[Task, str, list[dict[str, Any]]], Awaitable[dict[str, str]]]
|
|
|
|
|
|
@dataclass
|
|
class _RuntimePrefetchHandle:
|
|
task: asyncio.Task[dict[str, str]]
|
|
query: str
|
|
consumed: bool = False
|
|
|
|
|
|
class NativeRuntimeV2:
|
|
"""OpenOPC native runtime with structured events and tool batches."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
llm: LLMProvider,
|
|
tool_registry: ToolRegistry,
|
|
event_bus: EventBus | None = None,
|
|
cost_tracker: Any | None = None,
|
|
memory_manager: Any | None = None,
|
|
history_compactor: Any | None = None,
|
|
max_iterations: int = 50,
|
|
compression_threshold: float = 0.85,
|
|
config: OPCConfig | None = None,
|
|
child_agent_factory: ChildAgentFactory | None = None,
|
|
approval_callback: ApprovalCallback | None = None,
|
|
permission_policy: Any | None = None,
|
|
prefetch_provider: PrefetchProvider | None = None,
|
|
) -> None:
|
|
self.llm = llm
|
|
self.tools = tool_registry
|
|
self.event_bus = event_bus
|
|
self.cost_tracker = cost_tracker
|
|
self.memory_manager = memory_manager
|
|
self.history_compactor = history_compactor
|
|
self.max_iterations = max_iterations
|
|
self.compression_threshold = compression_threshold
|
|
self.config = config or OPCConfig()
|
|
self.child_agent_factory = child_agent_factory
|
|
self.approval_callback = approval_callback
|
|
# The single permission policy (ApprovalEngine). Its sync predict()
|
|
# gates every tool call; ASK routes into approval_callback.
|
|
self.permission_policy = permission_policy
|
|
self.prefetch_provider = prefetch_provider
|
|
self._pre_tool_hooks: list[tuple[str, Any]] = []
|
|
self._post_tool_hooks: list[tuple[str, Any]] = []
|
|
self._failure_tool_hooks: list[tuple[str, Any]] = []
|
|
|
|
def register_pre_tool_hook(self, name: str, hook: Any) -> None:
|
|
self._pre_tool_hooks.append((name, hook))
|
|
|
|
def register_post_tool_hook(self, name: str, hook: Any) -> None:
|
|
self._post_tool_hooks.append((name, hook))
|
|
|
|
def register_failure_tool_hook(self, name: str, hook: Any) -> None:
|
|
self._failure_tool_hooks.append((name, hook))
|
|
|
|
async def run(
|
|
self,
|
|
system_prompt: str,
|
|
user_message: str,
|
|
context_messages: list[dict[str, Any]] | None = None,
|
|
attachment_refs: list[dict[str, Any]] | None = None,
|
|
task: Task | None = None,
|
|
allowed_tools: list[str] | None = None,
|
|
on_progress: Any = None,
|
|
inbox_interrupt_provider: Any = None,
|
|
) -> TaskResult:
|
|
if task is not None:
|
|
ensure_task_execution_context(task, self.config)
|
|
runtime_session_id = self._runtime_session_id(task)
|
|
conversation_turn_id = self._conversation_turn_id(task, runtime_session_id)
|
|
user_content = self.llm.prepare_user_message_content(
|
|
user_message,
|
|
attachment_refs=attachment_refs,
|
|
)
|
|
messages, base_prefix_len = await self._bootstrap_messages(
|
|
system_prompt=system_prompt,
|
|
user_content=user_content,
|
|
user_message=user_message,
|
|
context_messages=context_messages,
|
|
task=task,
|
|
)
|
|
tool_schemas = self.llm.get_tool_definitions(self.tools.get_schemas(allowed=allowed_tools))
|
|
planner = ToolPlanner(
|
|
self.tools,
|
|
max_parallel_read_tools=self.config.system.native_runtime.max_parallel_read_tools,
|
|
)
|
|
permission_resolver = RuntimePermissionAdapter(
|
|
self.permission_policy,
|
|
guardian=self.config.autonomy.permissions_v2.guardian,
|
|
)
|
|
todo_state: list[dict[str, Any]] = self._restore_task_ledger(task)
|
|
current_runtime_messages: list[dict[str, Any]] = []
|
|
runtime_status: dict[str, Any] = {
|
|
"current_tool": None,
|
|
"queue_depth": 0,
|
|
"drain_mode": "idle",
|
|
"tool_elapsed_ms": 0,
|
|
"last_tool_summary": "",
|
|
"context_tokens": 0,
|
|
"context_window": self._context_window_limit(),
|
|
"context_remaining_pct": 100,
|
|
"input_tokens": 0,
|
|
"output_tokens": 0,
|
|
"total_tokens": 0,
|
|
"turn_cost_usd": 0.0,
|
|
"session_cost_usd": 0.0,
|
|
"pending_permission_count": 0,
|
|
}
|
|
runtime_notes: dict[str, Any] = {
|
|
"observed_risky_tools": [],
|
|
"mutating_tools": [],
|
|
"verification": {},
|
|
"prefetch_hits": [],
|
|
"task_ledger": list(todo_state),
|
|
"artifact_manifest": list(self._boot_artifact_manifest(task)),
|
|
"artifact_hashes": dict(self._boot_artifact_hashes(task)),
|
|
}
|
|
pending_prefetch: _RuntimePrefetchHandle | None = None
|
|
subagents = SubagentManager(
|
|
parent_task=task,
|
|
config=self.config,
|
|
child_agent_factory=self.child_agent_factory,
|
|
event_bus=self.event_bus,
|
|
store=getattr(self.memory_manager, "store", None),
|
|
runtime_session_id=runtime_session_id,
|
|
)
|
|
hook_bus = self._build_tool_hook_bus(
|
|
runtime_session_id=runtime_session_id,
|
|
task=task,
|
|
permission_resolver=permission_resolver,
|
|
on_progress=on_progress,
|
|
)
|
|
|
|
async def _emit_executor_event(event_type: str, payload: dict[str, Any]) -> None:
|
|
payload = dict(payload or {})
|
|
execution_turn_id = self._runtime_iteration_turn_id(conversation_turn_id, iteration)
|
|
payload.setdefault("iteration", iteration + 1)
|
|
payload.setdefault("turn_id", execution_turn_id)
|
|
payload.setdefault("canonical_turn_id", conversation_turn_id)
|
|
payload.setdefault("conversation_turn_id", conversation_turn_id)
|
|
payload.setdefault("execution_turn_id", execution_turn_id)
|
|
tool_call_id = str(payload.get("tool_call_id", "") or "").strip()
|
|
if tool_call_id:
|
|
prefix = "permission" if event_type.startswith("permission_") else "tool"
|
|
payload.setdefault("item_id", f"{execution_turn_id}:{prefix}:{tool_call_id}")
|
|
payload.setdefault("stream_id", f"{execution_turn_id}:{prefix}:{tool_call_id}")
|
|
if event_type.startswith("permission_"):
|
|
group_key = self._permission_group_key(
|
|
str(payload.get("tool_name", "") or ""),
|
|
dict(payload.get("arguments", {}) or {}),
|
|
)
|
|
if group_key:
|
|
payload.setdefault("permission_group_key", group_key)
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
event_type,
|
|
payload,
|
|
)
|
|
if event_type == "tool_started":
|
|
runtime_status["current_tool"] = str(payload.get("tool_name", "") or "") or None
|
|
runtime_status["queue_depth"] = 1
|
|
runtime_status["drain_mode"] = "smooth"
|
|
runtime_status["tool_elapsed_ms"] = 0
|
|
elif event_type == "tool_progress":
|
|
runtime_status["current_tool"] = str(payload.get("tool_name", "") or runtime_status.get("current_tool") or "") or None
|
|
runtime_status["queue_depth"] = 1 if runtime_status.get("current_tool") else 0
|
|
runtime_status["drain_mode"] = "smooth" if runtime_status.get("current_tool") else "idle"
|
|
runtime_status["tool_elapsed_ms"] = int(payload.get("elapsed_ms", 0) or 0)
|
|
runtime_status["last_tool_summary"] = str(
|
|
payload.get("message", "") or payload.get("text", "") or runtime_status.get("last_tool_summary", "")
|
|
).strip()
|
|
elif event_type == "tool_completed":
|
|
runtime_status["current_tool"] = None
|
|
runtime_status["queue_depth"] = 0
|
|
runtime_status["drain_mode"] = "idle"
|
|
runtime_status["tool_elapsed_ms"] = int(payload.get("elapsed_ms", 0) or 0)
|
|
runtime_status["last_tool_summary"] = str(payload.get("result_summary", "") or "").strip()
|
|
elif event_type == "tool_skipped":
|
|
runtime_status["queue_depth"] = 0
|
|
runtime_status["drain_mode"] = "idle"
|
|
runtime_status["last_tool_summary"] = str(payload.get("reason", "") or "").strip()
|
|
elif event_type == "permission_requested":
|
|
runtime_status["pending_permission_count"] = int(runtime_status.get("pending_permission_count", 0) or 0) + 1
|
|
elif event_type == "permission_resolved":
|
|
runtime_status["pending_permission_count"] = max(
|
|
0,
|
|
int(runtime_status.get("pending_permission_count", 0) or 0) - 1,
|
|
)
|
|
if event_type in {
|
|
"tool_started",
|
|
"tool_progress",
|
|
"tool_completed",
|
|
"tool_skipped",
|
|
"permission_requested",
|
|
"permission_resolved",
|
|
}:
|
|
await self._emit_status_snapshot(runtime_session_id, task, runtime_status)
|
|
|
|
executor = StreamingToolExecutor(
|
|
registry=self.tools,
|
|
planner=planner,
|
|
permission_resolver=permission_resolver,
|
|
hook_bus=hook_bus,
|
|
runtime_tool_handler=lambda tool_name, arguments: self._handle_runtime_tool(
|
|
subagents=subagents,
|
|
tool_name=tool_name,
|
|
arguments=arguments,
|
|
task=task,
|
|
todo_state=todo_state,
|
|
),
|
|
emit_event=_emit_executor_event,
|
|
max_parallel_read_tools=self.config.system.native_runtime.max_parallel_read_tools,
|
|
converge_on_parallel_failure=self.config.system.native_runtime.converge_on_parallel_failure,
|
|
)
|
|
|
|
total_cost = 0.0
|
|
total_usage: dict[str, int] = {"prompt_tokens": 0, "completion_tokens": 0}
|
|
last_observed_prompt_tokens = 0
|
|
aggregated_artifacts: dict[str, Any] = {}
|
|
overflow_retries = 0
|
|
max_overflow_retries = max(
|
|
1,
|
|
int(self.config.system.native_runtime.reactive_compaction.max_overflow_retries or 1),
|
|
) if self.config.system.native_runtime.reactive_compaction.enabled else 1
|
|
# Unclassified provider failures (content filters, transient rejects)
|
|
# get bounded retries with the provider's error text fed back into the
|
|
# conversation so the model can adapt; the counter resets after every
|
|
# successful stream so long runs are not penalized for sporadic blips.
|
|
stream_error_feedback_retries = 0
|
|
max_stream_error_feedback_retries = 2
|
|
stream_error_context_reset_attempted = False
|
|
compaction_boundaries: list[dict[str, Any]] = []
|
|
|
|
await self._save_runtime_session(
|
|
runtime_session_id,
|
|
task,
|
|
"running",
|
|
{
|
|
"allowed_tools": allowed_tools or [],
|
|
"task_ledger": list(todo_state),
|
|
"prefetch_hits": [],
|
|
"artifact_manifest": list(runtime_notes.get("artifact_manifest", []) or []),
|
|
},
|
|
)
|
|
await self._seed_user_turn(
|
|
task,
|
|
user_message,
|
|
runtime_session_id=runtime_session_id,
|
|
conversation_turn_id=conversation_turn_id,
|
|
)
|
|
await self._emit_prompt_prefix_state(
|
|
runtime_session_id=runtime_session_id,
|
|
task=task,
|
|
messages=messages,
|
|
tool_schemas=tool_schemas,
|
|
base_prefix_len=base_prefix_len,
|
|
)
|
|
await self._emit_status_snapshot(runtime_session_id, task, runtime_status)
|
|
|
|
for iteration in range(self.max_iterations):
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"turn_started",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"turn_id": conversation_turn_id,
|
|
"canonical_turn_id": conversation_turn_id,
|
|
"conversation_turn_id": conversation_turn_id,
|
|
"execution_turn_id": self._runtime_iteration_turn_id(conversation_turn_id, iteration),
|
|
},
|
|
)
|
|
await self._emit_status_snapshot(runtime_session_id, task, {**runtime_status, "drain_mode": "smooth"})
|
|
|
|
messages, base_prefix_len, consumed_hits = await self._consume_ready_prefetch(
|
|
pending_prefetch,
|
|
messages=messages,
|
|
base_prefix_len=base_prefix_len,
|
|
runtime_session_id=runtime_session_id,
|
|
task=task,
|
|
)
|
|
if consumed_hits:
|
|
runtime_notes["prefetch_hits"] = [
|
|
*list(runtime_notes.get("prefetch_hits", []) or []),
|
|
*consumed_hits,
|
|
]
|
|
await self._save_runtime_session(
|
|
runtime_session_id,
|
|
task,
|
|
"running",
|
|
self._build_runtime_state_metadata(
|
|
task=task,
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=subagents.list_agents().get("agents", []),
|
|
),
|
|
)
|
|
pending_prefetch = None
|
|
|
|
if pending_prefetch is None:
|
|
pending_prefetch = self._start_runtime_prefetch(task, messages)
|
|
|
|
if inbox_interrupt_provider is not None and task is not None:
|
|
try:
|
|
inbox_messages = await inbox_interrupt_provider(task)
|
|
except Exception as exc: # pragma: no cover - defensive
|
|
logger.debug(f"Runtime inbox provider failed: {exc}")
|
|
inbox_messages = []
|
|
if inbox_messages:
|
|
messages.append({
|
|
"role": "system",
|
|
"content": self._format_interrupt_notice(inbox_messages),
|
|
})
|
|
|
|
messages = await self._apply_context_pipeline(
|
|
messages,
|
|
tool_schemas=tool_schemas,
|
|
task=task,
|
|
base_prefix_len=base_prefix_len,
|
|
runtime_session_id=runtime_session_id,
|
|
compaction_boundaries=compaction_boundaries,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
active_subagents=subagents.list_agents().get("agents", []),
|
|
observed_tokens=last_observed_prompt_tokens,
|
|
)
|
|
context_usage = await self._emit_context_usage(
|
|
runtime_session_id=runtime_session_id,
|
|
task=task,
|
|
messages=messages,
|
|
tool_schemas=tool_schemas,
|
|
phase="pre_llm",
|
|
)
|
|
runtime_status["context_tokens"] = int(context_usage.get("context_tokens", 0) or 0)
|
|
runtime_status["context_window"] = int(context_usage.get("context_window", 0) or 0)
|
|
runtime_status["context_remaining_pct"] = int(context_usage.get("context_remaining_pct", 0) or 0)
|
|
await self._emit_status_snapshot(runtime_session_id, task, runtime_status)
|
|
|
|
assistant_text = ""
|
|
assistant_delta_seq = 0
|
|
thinking_delta_seq = 0
|
|
tool_call_chunks: dict[int, dict[str, Any]] = {}
|
|
early_tool_runs: dict[int, dict[str, Any]] = {}
|
|
try:
|
|
async for event in self.llm.chat_stream(messages, tools=tool_schemas):
|
|
if event.event_type == "assistant_delta":
|
|
delta_text = str(event.payload.get("text", "") or "")
|
|
if delta_text:
|
|
assistant_text += delta_text
|
|
turn_id = self._runtime_iteration_turn_id(conversation_turn_id, iteration)
|
|
assistant_delta_seq += 1
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"assistant_delta",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"turn_id": turn_id,
|
|
"canonical_turn_id": conversation_turn_id,
|
|
"conversation_turn_id": conversation_turn_id,
|
|
"execution_turn_id": turn_id,
|
|
"item_id": f"{turn_id}:assistant",
|
|
"stream_id": f"{turn_id}:assistant",
|
|
"seq": assistant_delta_seq,
|
|
"text": delta_text,
|
|
},
|
|
)
|
|
elif event.event_type == "thinking_delta":
|
|
thinking_text = str(event.payload.get("text", "") or "")
|
|
if thinking_text:
|
|
# Keep the full thinking stream for the final
|
|
# transcript metadata; the UI renders it as a
|
|
# collapsed block after the turn completes.
|
|
runtime_notes["latest_thinking_text"] = (
|
|
str(runtime_notes.get("latest_thinking_text", "") or "") + thinking_text
|
|
)
|
|
turn_id = conversation_turn_id
|
|
execution_turn_id = self._runtime_iteration_turn_id(conversation_turn_id, iteration)
|
|
thinking_delta_seq += 1
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"thinking_delta",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"turn_id": turn_id,
|
|
"canonical_turn_id": conversation_turn_id,
|
|
"conversation_turn_id": conversation_turn_id,
|
|
"execution_turn_id": execution_turn_id,
|
|
# Keyed per iteration (like assistant_delta):
|
|
# thinking_delta_seq resets every iteration, so a
|
|
# turn-scoped stream id makes downstream seq guards
|
|
# drop iteration>=2 deltas and collapses all
|
|
# iterations into one entry (no tool interleaving).
|
|
"item_id": f"{execution_turn_id}:thinking",
|
|
"stream_id": f"{execution_turn_id}:thinking",
|
|
"seq": thinking_delta_seq,
|
|
"text": thinking_text,
|
|
},
|
|
)
|
|
elif event.event_type == "tool_call_delta":
|
|
index = int(event.payload.get("index", 0) or 0)
|
|
bucket = tool_call_chunks.setdefault(index, {
|
|
"id": "",
|
|
"function": "",
|
|
"arguments_chunks": [],
|
|
})
|
|
if event.payload.get("id"):
|
|
bucket["id"] = event.payload["id"]
|
|
if event.payload.get("name"):
|
|
bucket["function"] = event.payload["name"]
|
|
arguments_chunk = str(event.payload.get("arguments", "") or "")
|
|
if arguments_chunk:
|
|
bucket["arguments_chunks"].append(arguments_chunk)
|
|
await self._maybe_start_streaming_tool_calls(
|
|
upto_index=index,
|
|
tool_call_chunks=tool_call_chunks,
|
|
early_tool_runs=early_tool_runs,
|
|
executor=executor,
|
|
planner=planner,
|
|
permission_resolver=permission_resolver,
|
|
task=task,
|
|
on_progress=on_progress,
|
|
runtime_session_id=runtime_session_id,
|
|
)
|
|
elif event.event_type == "usage":
|
|
prompt_tokens = int(event.payload.get("prompt_tokens", 0) or 0)
|
|
completion_tokens = int(event.payload.get("completion_tokens", 0) or 0)
|
|
estimated_cost_delta = float(event.payload.get("estimated_cost_delta", 0.0) or 0.0)
|
|
if prompt_tokens:
|
|
last_observed_prompt_tokens = prompt_tokens
|
|
total_usage["prompt_tokens"] += prompt_tokens
|
|
total_usage["completion_tokens"] += completion_tokens
|
|
total_cost += estimated_cost_delta
|
|
runtime_status["input_tokens"] = total_usage["prompt_tokens"]
|
|
runtime_status["output_tokens"] = total_usage["completion_tokens"]
|
|
runtime_status["total_tokens"] = total_usage["prompt_tokens"] + total_usage["completion_tokens"]
|
|
if self.cost_tracker and (prompt_tokens or completion_tokens or estimated_cost_delta):
|
|
entry = CostEntry(
|
|
task_id=task.id if task else None,
|
|
agent_id=task.assigned_to if task else None,
|
|
org_id=task.org_id if task else None,
|
|
model=str(event.payload.get("model", "") or event.model or ""),
|
|
tokens_in=prompt_tokens,
|
|
tokens_out=completion_tokens,
|
|
cost=estimated_cost_delta,
|
|
)
|
|
await self.cost_tracker.record(entry)
|
|
runtime_status["turn_cost_usd"] = round(total_cost, 6)
|
|
runtime_status["session_cost_usd"] = round(
|
|
float(getattr(self.cost_tracker, "session_total", total_cost) if self.cost_tracker else total_cost),
|
|
6,
|
|
)
|
|
runtime_status["context_window"] = int(
|
|
event.payload.get("context_window", 0) or runtime_status.get("context_window", 0) or 0
|
|
)
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"cost_update",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"model": str(event.payload.get("model", "") or event.model or ""),
|
|
"tokens_in": prompt_tokens,
|
|
"tokens_out": completion_tokens,
|
|
"tokens_total": prompt_tokens + completion_tokens,
|
|
"input_tokens_total": runtime_status["input_tokens"],
|
|
"output_tokens_total": runtime_status["output_tokens"],
|
|
"turn_cost_usd": runtime_status["turn_cost_usd"],
|
|
"session_cost_usd": runtime_status["session_cost_usd"],
|
|
"estimated_cost_delta": estimated_cost_delta,
|
|
},
|
|
)
|
|
await self._emit_status_snapshot(runtime_session_id, task, runtime_status)
|
|
elif event.event_type == "error":
|
|
raise RuntimeError(str(event.payload.get("message", "Unknown LLM stream error")))
|
|
if event.event_type not in {"assistant_delta", "thinking_delta"}:
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
event.event_type,
|
|
{
|
|
**event.payload,
|
|
"iteration": iteration + 1,
|
|
"model": event.model,
|
|
},
|
|
)
|
|
except Exception as exc:
|
|
if self.llm.is_context_overflow_error(exc) and overflow_retries < max_overflow_retries:
|
|
overflow_retries += 1
|
|
messages = await self._apply_context_pipeline(
|
|
messages,
|
|
tool_schemas=tool_schemas,
|
|
task=task,
|
|
base_prefix_len=base_prefix_len,
|
|
runtime_session_id=runtime_session_id,
|
|
compaction_boundaries=compaction_boundaries,
|
|
force_compact=True,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
active_subagents=subagents.list_agents().get("agents", []),
|
|
observed_tokens=last_observed_prompt_tokens,
|
|
)
|
|
continue
|
|
recovered_turn = await self._recover_tool_protocol_stream_error(
|
|
exc=exc,
|
|
messages=messages,
|
|
tool_schemas=tool_schemas,
|
|
base_prefix_len=base_prefix_len,
|
|
runtime_session_id=runtime_session_id,
|
|
task=task,
|
|
iteration=iteration,
|
|
early_tool_runs=early_tool_runs,
|
|
)
|
|
if recovered_turn is not None:
|
|
messages = list(recovered_turn["messages"])
|
|
assistant_text = str(recovered_turn["assistant_text"] or "")
|
|
tool_call_chunks = dict(recovered_turn["tool_call_chunks"] or {})
|
|
usage = dict(recovered_turn.get("usage", {}) or {})
|
|
total_usage["prompt_tokens"] += int(usage.get("prompt_tokens", 0) or 0)
|
|
total_usage["completion_tokens"] += int(usage.get("completion_tokens", 0) or 0)
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"tool_protocol_recovered",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"strategy": str(recovered_turn.get("strategy", "") or ""),
|
|
"tool_call_count": len(tool_call_chunks),
|
|
"model": str(recovered_turn.get("model", "") or ""),
|
|
},
|
|
)
|
|
else:
|
|
await self._cancel_early_tool_runs(early_tool_runs)
|
|
if self.llm.is_tool_protocol_error(exc):
|
|
truncated = self._truncate_to_last_clean_user_turn(messages, base_prefix_len)
|
|
if truncated and len(truncated) > base_prefix_len:
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"tool_protocol_retry",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"strategy": "truncate",
|
|
"message": str(exc),
|
|
},
|
|
)
|
|
messages = truncated
|
|
continue
|
|
elif stream_error_feedback_retries < max_stream_error_feedback_retries:
|
|
stream_error_feedback_retries += 1
|
|
messages.append(self._provider_error_feedback_message(exc))
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"tool_protocol_retry",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"strategy": "provider_error_feedback",
|
|
"attempt": stream_error_feedback_retries,
|
|
"message": str(exc),
|
|
},
|
|
)
|
|
continue
|
|
elif not stream_error_context_reset_attempted:
|
|
stream_error_context_reset_attempted = True
|
|
truncated = self._truncate_to_last_clean_user_turn(messages, base_prefix_len)
|
|
if truncated and base_prefix_len < len(truncated) < len(messages):
|
|
truncated.append(
|
|
self._provider_error_feedback_message(exc, context_reset=True)
|
|
)
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"tool_protocol_retry",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"strategy": "provider_error_context_reset",
|
|
"message": str(exc),
|
|
},
|
|
)
|
|
messages = truncated
|
|
continue
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"turn_failed",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"turn_id": conversation_turn_id,
|
|
"canonical_turn_id": conversation_turn_id,
|
|
"conversation_turn_id": conversation_turn_id,
|
|
"execution_turn_id": self._runtime_iteration_turn_id(conversation_turn_id, iteration),
|
|
"message": str(exc),
|
|
},
|
|
)
|
|
runtime_status["current_tool"] = None
|
|
runtime_status["queue_depth"] = 0
|
|
runtime_status["drain_mode"] = "idle"
|
|
await self._emit_status_snapshot(runtime_session_id, task, runtime_status)
|
|
await self._save_runtime_session(runtime_session_id, task, "failed", {"error": str(exc)})
|
|
self._cancel_prefetch(pending_prefetch)
|
|
return TaskResult(
|
|
status=TaskStatus.FAILED,
|
|
content=f"LLM stream failed: {exc}",
|
|
artifacts={"runtime_session_id": runtime_session_id},
|
|
cost=total_cost,
|
|
token_usage=total_usage,
|
|
)
|
|
|
|
stream_error_feedback_retries = 0
|
|
tool_calls = self._finalize_tool_calls(tool_call_chunks)
|
|
assistant_message = {"role": "assistant", "content": assistant_text}
|
|
if tool_calls:
|
|
assistant_message["tool_calls"] = [
|
|
{
|
|
"id": item["id"],
|
|
"type": "function",
|
|
"function": {
|
|
"name": item["function"],
|
|
"arguments": json.dumps(item["arguments"], ensure_ascii=False, default=str),
|
|
},
|
|
}
|
|
for item in tool_calls
|
|
]
|
|
messages.append(assistant_message)
|
|
current_runtime_messages = [dict(message) for message in messages]
|
|
await self._persist_assistant_turn(
|
|
task,
|
|
assistant_text,
|
|
tool_calls,
|
|
runtime_session_id=runtime_session_id,
|
|
turn_id=(
|
|
self._runtime_iteration_turn_id(conversation_turn_id, iteration)
|
|
if tool_calls
|
|
else conversation_turn_id
|
|
),
|
|
conversation_turn_id=conversation_turn_id,
|
|
iteration=iteration + 1,
|
|
thinking_text=str(runtime_notes.get("latest_thinking_text", "") or ""),
|
|
)
|
|
await self._maybe_update_background_session_memory(
|
|
runtime_session_id=runtime_session_id,
|
|
task=task,
|
|
messages=messages,
|
|
)
|
|
|
|
if not tool_calls:
|
|
if on_progress and assistant_text:
|
|
await self._emit_progress(on_progress, assistant_text, task)
|
|
active_subagents = subagents.list_agents().get("agents", [])
|
|
verification_gate = await self._run_verification_gate(
|
|
runtime_session_id=runtime_session_id,
|
|
task=task,
|
|
subagents=subagents,
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
)
|
|
if verification_gate is not None:
|
|
verification_gate.artifacts = {
|
|
**dict(verification_gate.artifacts or {}),
|
|
"runtime_session_id": runtime_session_id,
|
|
**result_delivery_identity_payload_for_task(
|
|
task,
|
|
canonical_turn_id=conversation_turn_id,
|
|
),
|
|
}
|
|
await self._save_runtime_session(
|
|
runtime_session_id,
|
|
task,
|
|
verification_gate.status.value,
|
|
dict(verification_gate.artifacts or {}),
|
|
)
|
|
self._cancel_prefetch(pending_prefetch)
|
|
return verification_gate
|
|
extraction_artifacts = await self._extract_durable_memory(
|
|
task=task,
|
|
user_message=user_message,
|
|
assistant_text=assistant_text,
|
|
runtime_session_id=runtime_session_id,
|
|
)
|
|
if extraction_artifacts:
|
|
aggregated_artifacts = self._merge_artifacts(aggregated_artifacts, extraction_artifacts)
|
|
final_content, verification_verdict = self._apply_verification_contract(
|
|
assistant_text,
|
|
task=task,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
)
|
|
artifacts = {
|
|
**aggregated_artifacts,
|
|
"runtime_session_id": runtime_session_id,
|
|
**result_delivery_identity_payload_for_task(
|
|
task,
|
|
canonical_turn_id=conversation_turn_id,
|
|
),
|
|
"permission_requests": self._permission_requests_from_results([]),
|
|
"active_subagents": active_subagents,
|
|
"compaction_boundaries": list(compaction_boundaries),
|
|
"compaction_records": list(compaction_boundaries),
|
|
"resume_cursor": len(messages),
|
|
"worktree_path": self._primary_worktree_path(active_subagents),
|
|
"task_ledger": list(todo_state),
|
|
"prefetch_hits": list(runtime_notes.get("prefetch_hits", []) or []),
|
|
"verification": dict(runtime_notes.get("verification", {}) or {}),
|
|
"verification_evidence": dict((runtime_notes.get("verification", {}) or {}).get("evidence", {}) or {}),
|
|
"verification_verdict": verification_verdict,
|
|
"artifact_manifest": list(self._compose_runtime_artifact_manifest(
|
|
task=task,
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
)),
|
|
"resume_state": self._build_resume_state(
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
),
|
|
}
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"turn_completed",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"turn_id": conversation_turn_id,
|
|
"canonical_turn_id": conversation_turn_id,
|
|
"conversation_turn_id": conversation_turn_id,
|
|
"execution_turn_id": self._runtime_iteration_turn_id(conversation_turn_id, iteration),
|
|
"content_preview": final_content[:500],
|
|
},
|
|
)
|
|
runtime_status["current_tool"] = None
|
|
runtime_status["queue_depth"] = 0
|
|
runtime_status["drain_mode"] = "idle"
|
|
await self._emit_status_snapshot(runtime_session_id, task, runtime_status)
|
|
await self._save_runtime_session(runtime_session_id, task, "completed", artifacts)
|
|
self._cancel_prefetch(pending_prefetch)
|
|
return TaskResult(
|
|
status=TaskStatus.DONE,
|
|
content=final_content,
|
|
artifacts=artifacts,
|
|
cost=total_cost,
|
|
token_usage=total_usage,
|
|
)
|
|
|
|
execution_results = await self._collect_execution_results(
|
|
tool_calls=tool_calls,
|
|
early_tool_runs=early_tool_runs,
|
|
executor=executor,
|
|
task=task,
|
|
on_progress=on_progress,
|
|
)
|
|
self._update_runtime_notes(runtime_notes, execution_results)
|
|
runtime_notes["task_ledger"] = list(todo_state)
|
|
active_subagents = subagents.list_agents().get("agents", [])
|
|
await self._persist_task_ledger(
|
|
runtime_session_id=runtime_session_id,
|
|
task=task,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
messages=messages,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
)
|
|
early_return = self._handle_pause_or_peer_wait(
|
|
execution_results,
|
|
aggregated_artifacts,
|
|
runtime_session_id,
|
|
task=task,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
)
|
|
if early_return is not None:
|
|
await self._save_runtime_session(runtime_session_id, task, early_return.status.value, early_return.artifacts)
|
|
self._cancel_prefetch(pending_prefetch)
|
|
return early_return
|
|
|
|
for item in execution_results:
|
|
call = item["tool_call"]
|
|
result = item["result"]
|
|
decision = item.get("permission_decision")
|
|
clipped_result = self._clip_tool_result_for_history(
|
|
str(call.get("function", "") or ""),
|
|
result,
|
|
)
|
|
result_text = json.dumps(clipped_result, ensure_ascii=False, default=str)
|
|
messages.append({
|
|
"role": "tool",
|
|
"tool_call_id": call.get("id"),
|
|
"content": result_text,
|
|
})
|
|
result_payload = result.get("result", {})
|
|
if isinstance(result_payload, dict):
|
|
aggregated_artifacts = self._merge_artifacts(aggregated_artifacts, result_payload)
|
|
await self._persist_tool_result(
|
|
task,
|
|
call,
|
|
clipped_result,
|
|
decision,
|
|
runtime_session_id=runtime_session_id,
|
|
hook_metadata=item.get("hook_metadata", {}),
|
|
)
|
|
|
|
await self._save_runtime_session(runtime_session_id, task, "failed", {"reason": "max_iterations"})
|
|
self._cancel_prefetch(pending_prefetch)
|
|
return TaskResult(
|
|
status=TaskStatus.FAILED,
|
|
content=f"Exceeded maximum iterations ({self.max_iterations})",
|
|
artifacts={"runtime_session_id": runtime_session_id},
|
|
cost=total_cost,
|
|
token_usage=total_usage,
|
|
)
|
|
|
|
async def _handle_runtime_tool(
|
|
self,
|
|
*,
|
|
subagents: SubagentManager,
|
|
tool_name: str,
|
|
arguments: dict[str, Any],
|
|
task: Task | None = None,
|
|
todo_state: list[dict[str, Any]] | None = None,
|
|
) -> dict[str, Any]:
|
|
if tool_name == "todo_write":
|
|
todos_payload = arguments.get("todos", [])
|
|
if isinstance(todos_payload, str):
|
|
try:
|
|
todos_payload = json.loads(todos_payload)
|
|
except json.JSONDecodeError as exc:
|
|
return {"error": f"Invalid todo JSON: {exc}", "success": False}
|
|
normalized_todos = self._normalize_todos(todos_payload)
|
|
todo_state[:] = normalized_todos if todo_state is not None else normalized_todos
|
|
return {
|
|
"success": True,
|
|
"result": {
|
|
"todos": list(todo_state or []),
|
|
"rendered": self._render_todos(todo_state or []),
|
|
"task_ledger": list(todo_state or []),
|
|
},
|
|
}
|
|
if tool_name == "todo_read":
|
|
return {
|
|
"success": True,
|
|
"result": {
|
|
"todos": list(todo_state or []),
|
|
"rendered": self._render_todos(todo_state or []),
|
|
"task_ledger": list(todo_state or []),
|
|
},
|
|
}
|
|
if tool_name == "agent_spawn":
|
|
background = arguments.get("background")
|
|
if "run_in_background" in arguments:
|
|
background = arguments.get("run_in_background")
|
|
resident = bool(arguments.get("resident", False))
|
|
mode = self._normalize_spawn_mode(arguments.get("mode", "default"))
|
|
return await subagents.spawn(
|
|
profile=str(arguments.get("profile") or arguments.get("subagent_type") or "general"),
|
|
prompt=str(arguments.get("prompt", "") or ""),
|
|
description=str(arguments.get("description", "") or ""),
|
|
name=str(arguments.get("name", "") or ""),
|
|
model=str(arguments.get("model", "") or ""),
|
|
mode=mode,
|
|
background=bool(background) if background is not None else None,
|
|
isolation=str(arguments.get("isolation", "") or "").strip() or None,
|
|
resident=resident,
|
|
)
|
|
if tool_name == "agent_wait":
|
|
return await subagents.wait(
|
|
str(arguments.get("agent_id", "") or ""),
|
|
int(arguments.get("timeout_seconds", 300) or 300),
|
|
)
|
|
if tool_name == "agent_send":
|
|
return await subagents.send(
|
|
str(arguments.get("agent_id", "") or ""),
|
|
str(arguments.get("message", "") or ""),
|
|
)
|
|
if tool_name == "agent_list":
|
|
return subagents.list_agents()
|
|
return {"error": f"Unknown runtime-managed tool: {tool_name}", "success": False}
|
|
|
|
def _runtime_session_id(self, task: Task | None) -> str:
|
|
if task:
|
|
runtime_resume = self._runtime_resume_payload(task)
|
|
runtime_session_id = str(
|
|
runtime_resume.get("runtime_session_id")
|
|
or (task.metadata.get("runtime_v2", {}) or {}).get("runtime_session_id")
|
|
or ""
|
|
).strip()
|
|
if runtime_session_id:
|
|
return runtime_session_id
|
|
return f"rt_{uuid.uuid4().hex}"
|
|
|
|
@staticmethod
|
|
def _runtime_resume_payload(task: Task | None) -> dict[str, Any]:
|
|
if task is None:
|
|
return {}
|
|
context_snapshot = getattr(task, "context_snapshot", {}) or {}
|
|
if not isinstance(context_snapshot, dict):
|
|
return {}
|
|
raw_resume = context_snapshot.get("runtime_resume", {})
|
|
return dict(raw_resume) if isinstance(raw_resume, dict) else {}
|
|
|
|
def _conversation_turn_id(self, task: Task | None, runtime_session_id: str) -> str:
|
|
if task:
|
|
metadata = dict(getattr(task, "metadata", {}) or {})
|
|
runtime_meta = dict(metadata.get("runtime_v2", {}) or {})
|
|
for source in (metadata, runtime_meta):
|
|
for key in (
|
|
"conversation_turn_id",
|
|
"current_turn_id",
|
|
"runtime_v2_current_turn_id",
|
|
"canonical_turn_id",
|
|
"turn_id",
|
|
):
|
|
value = str(source.get(key, "") or "").strip()
|
|
if value:
|
|
return value
|
|
return f"{runtime_session_id}:turn:{uuid.uuid4().hex}"
|
|
|
|
@staticmethod
|
|
def _runtime_iteration_turn_id(conversation_turn_id: str, iteration: int) -> str:
|
|
normalized_turn_id = str(conversation_turn_id or "").strip()
|
|
if not normalized_turn_id:
|
|
normalized_turn_id = f"turn:{uuid.uuid4().hex}"
|
|
return f"{normalized_turn_id}:iter:{iteration + 1}"
|
|
|
|
def _build_tool_hook_bus(
|
|
self,
|
|
*,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
permission_resolver: RuntimePermissionAdapter,
|
|
on_progress: Any = None,
|
|
) -> RuntimeToolHookBus:
|
|
hook_bus = RuntimeToolHookBus(
|
|
emit_event=lambda event_type, payload: self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
event_type,
|
|
payload,
|
|
),
|
|
)
|
|
if self.config.system.native_runtime.enable_tool_hooks:
|
|
hook_bus.register_pre_hook(
|
|
"permission_gate",
|
|
lambda context: self._approval_pre_hook(
|
|
context,
|
|
permission_resolver=permission_resolver,
|
|
on_progress=on_progress,
|
|
),
|
|
)
|
|
hook_bus.register_post_hook("approval_metadata", self._approval_post_hook)
|
|
hook_bus.register_post_hook("command_exit", self._command_exit_post_hook)
|
|
hook_bus.register_failure_hook("failure_cascade", self._failure_cascade_hook)
|
|
for name, hook in self._pre_tool_hooks:
|
|
hook_bus.register_pre_hook(name, hook)
|
|
for name, hook in self._post_tool_hooks:
|
|
hook_bus.register_post_hook(name, hook)
|
|
for name, hook in self._failure_tool_hooks:
|
|
hook_bus.register_failure_hook(name, hook)
|
|
return hook_bus
|
|
|
|
async def _approval_pre_hook(
|
|
self,
|
|
context: RuntimeToolHookContext,
|
|
*,
|
|
permission_resolver: RuntimePermissionAdapter,
|
|
on_progress: Any = None,
|
|
) -> dict[str, Any] | None:
|
|
predicted = context.predicted_permission
|
|
if predicted is not None and getattr(predicted, "resolution", None) == PermissionResolution.DENY:
|
|
return {
|
|
"result": permission_resolver.build_blocked_result(
|
|
predicted,
|
|
tool_name=context.tool_name,
|
|
arguments=context.arguments,
|
|
),
|
|
"stop_execution": True,
|
|
"stop_batch_on_failure": True,
|
|
}
|
|
violation = ownership_guard_violation(
|
|
task=context.task,
|
|
tool_name=context.tool_name,
|
|
arguments=context.arguments,
|
|
org_engine=None,
|
|
)
|
|
if violation:
|
|
return {
|
|
"result": {
|
|
"error": f"Tool execution blocked by ownership contract: {violation}",
|
|
"approval": {
|
|
"action": "reject",
|
|
"risk_level": "high",
|
|
"confidence": 0.99,
|
|
"policy_source": "ownership_contract",
|
|
"rationale": violation,
|
|
},
|
|
"success": False,
|
|
},
|
|
"stop_execution": True,
|
|
"stop_batch_on_failure": True,
|
|
}
|
|
if self.approval_callback is None or context.tool is None or getattr(context.tool, "runtime_managed", False):
|
|
return None
|
|
requires_prompt = (
|
|
predicted is not None
|
|
and getattr(predicted, "resolution", None) == PermissionResolution.ASK
|
|
) or bool(getattr(context.tool, "requires_confirmation", False))
|
|
if not requires_prompt:
|
|
return None
|
|
allowed, decision = await self.approval_callback(context.tool, context.arguments, context.task, on_progress)
|
|
approval_payload = {
|
|
"action": getattr(getattr(decision, "action", None), "value", ""),
|
|
"risk_level": getattr(getattr(decision, "risk_level", None), "value", ""),
|
|
"confidence": float(getattr(decision, "confidence", 0.0) or 0.0),
|
|
"policy_source": str(getattr(decision, "policy_source", "") or ""),
|
|
"rationale": str(getattr(decision, "rationale", "") or ""),
|
|
**dict(getattr(decision, "metadata", {}) or {}),
|
|
}
|
|
if not allowed:
|
|
permission_resolver.record_denial(context.tool_name, context.arguments)
|
|
return {
|
|
"approval": approval_payload,
|
|
"result": {
|
|
"error": f"Tool execution blocked by autonomy policy: {getattr(decision, 'rationale', '')}",
|
|
"approval": approval_payload,
|
|
"success": False,
|
|
},
|
|
"stop_execution": True,
|
|
"stop_batch_on_failure": True,
|
|
}
|
|
return {"approval": approval_payload}
|
|
|
|
async def _approval_post_hook(self, context: RuntimeToolHookContext) -> dict[str, Any] | None:
|
|
approval = dict(context.state.get("approval", {}) or {})
|
|
if not approval or not isinstance(context.result, dict):
|
|
return None
|
|
merged = dict(context.result)
|
|
current = dict(merged.get("approval", {}) or {})
|
|
current.update(approval)
|
|
merged["approval"] = current
|
|
return {"result": merged}
|
|
|
|
async def _command_exit_post_hook(self, context: RuntimeToolHookContext) -> dict[str, Any] | None:
|
|
if not isinstance(context.result, dict):
|
|
return None
|
|
payload = context.result.get("result", {})
|
|
if not isinstance(payload, dict):
|
|
return None
|
|
exit_code = payload.get("exit_code")
|
|
if exit_code in (None, 0):
|
|
return None
|
|
merged = dict(context.result)
|
|
merged["success"] = False
|
|
if not merged.get("error"):
|
|
stderr = str(payload.get("stderr", "") or "").strip()
|
|
merged["error"] = stderr or f"{context.tool_name} exited with code {exit_code}"
|
|
return {
|
|
"result": merged,
|
|
"stop_batch_on_failure": True,
|
|
}
|
|
|
|
async def _failure_cascade_hook(self, context: RuntimeToolHookContext) -> dict[str, Any] | None:
|
|
if not isinstance(context.result, dict):
|
|
return None
|
|
if bool(context.result.get("success", True)):
|
|
return None
|
|
return {
|
|
"stop_batch_on_failure": True,
|
|
"metadata": {"failure_source": context.tool_name},
|
|
}
|
|
|
|
async def _bootstrap_messages(
|
|
self,
|
|
*,
|
|
system_prompt: str,
|
|
user_content: Any,
|
|
user_message: str,
|
|
context_messages: list[dict[str, Any]] | None,
|
|
task: Task | None,
|
|
) -> tuple[list[dict[str, Any]], int]:
|
|
runtime_resume = self._runtime_resume_payload(task)
|
|
if runtime_resume and task and getattr(self.memory_manager, "store", None) and task.session_id:
|
|
restored = await self._restore_transcript_messages(task)
|
|
if restored:
|
|
sanitizer = getattr(self.llm, "sanitize_tool_call_history", None)
|
|
if callable(sanitizer):
|
|
restored = sanitizer(restored)
|
|
prefix_messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt}]
|
|
if context_messages:
|
|
prefix_messages.extend(context_messages)
|
|
messages = [*prefix_messages, *restored]
|
|
if self._should_append_resume_user_turn(restored, user_message):
|
|
messages.append({"role": "user", "content": user_content})
|
|
return messages, len(prefix_messages)
|
|
messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt}]
|
|
if context_messages:
|
|
messages.extend(context_messages)
|
|
messages.append({"role": "user", "content": user_content})
|
|
return messages, len(messages)
|
|
|
|
async def _restore_transcript_messages(self, task: Task) -> list[dict[str, Any]]:
|
|
store = getattr(self.memory_manager, "store", None)
|
|
if not store or not hasattr(store, "get_session_transcript") or not task.session_id:
|
|
return []
|
|
transcript = await store.get_session_transcript(task.session_id)
|
|
if not transcript:
|
|
return []
|
|
runtime_messages: list[dict[str, Any]] = []
|
|
seen_tool_call_ids: set[str] = set()
|
|
pending_tool_calls: list[dict[str, Any]] = []
|
|
for message_index, item in enumerate(transcript):
|
|
message = item["message"]
|
|
parts = item["parts"]
|
|
if getattr(message, "summary_flag", False):
|
|
continue
|
|
role = str(getattr(message, "role", "") or "")
|
|
if role == "user":
|
|
content = self._normalize_content_for_resume(parts)
|
|
if content:
|
|
runtime_messages.append({"role": "user", "content": content})
|
|
continue
|
|
if role == "assistant":
|
|
assistant_texts: list[str] = []
|
|
tool_calls: list[dict[str, Any]] = []
|
|
tool_results: list[dict[str, Any]] = []
|
|
for part_index, part in enumerate(parts):
|
|
payload = dict(part.payload or {})
|
|
if part.part_type == "text":
|
|
text = str(payload.get("text", "") or "").strip()
|
|
if text:
|
|
assistant_texts.append(text)
|
|
elif part.part_type == "tool_call":
|
|
tool_name = str(payload.get("tool_name", "") or "").strip()
|
|
tool_call_id = str(payload.get("tool_call_id", "") or "").strip()
|
|
if not tool_call_id:
|
|
tool_call_id = self._synthesize_resume_tool_call_id(
|
|
message_index=message_index,
|
|
part_index=part_index,
|
|
tool_name=tool_name,
|
|
)
|
|
tool_calls.append({
|
|
"id": tool_call_id,
|
|
"type": "function",
|
|
"function": {
|
|
"name": tool_name,
|
|
"arguments": json.dumps(payload.get("arguments", {}), ensure_ascii=False, default=str),
|
|
},
|
|
})
|
|
seen_tool_call_ids.add(tool_call_id)
|
|
pending_tool_calls.append({
|
|
"id": tool_call_id,
|
|
"tool_name": tool_name,
|
|
"consumed": False,
|
|
})
|
|
elif part.part_type in {"tool_output", "tool_result"}:
|
|
tool_results.append(payload)
|
|
if assistant_texts or tool_calls:
|
|
assistant_message: dict[str, Any] = {
|
|
"role": "assistant",
|
|
"content": "\n\n".join(assistant_texts).strip(),
|
|
}
|
|
if tool_calls:
|
|
assistant_message["tool_calls"] = tool_calls
|
|
runtime_messages.append(assistant_message)
|
|
for payload in tool_results:
|
|
output = payload.get("result", payload.get("output", ""))
|
|
tool_call_id = self._resolve_resume_tool_result_id(
|
|
payload,
|
|
seen_tool_call_ids=seen_tool_call_ids,
|
|
pending_tool_calls=pending_tool_calls,
|
|
)
|
|
rendered_output = json.dumps(output, ensure_ascii=False, default=str) if not isinstance(output, str) else output
|
|
if tool_call_id:
|
|
runtime_messages.append({
|
|
"role": "tool",
|
|
"tool_call_id": tool_call_id,
|
|
"content": rendered_output,
|
|
})
|
|
else:
|
|
downgraded = self._render_orphan_tool_result_for_resume(payload, rendered_output)
|
|
if downgraded:
|
|
runtime_messages.append({"role": "assistant", "content": downgraded})
|
|
continue
|
|
rendered = self._normalize_content_for_resume(parts)
|
|
if rendered:
|
|
runtime_messages.append({"role": "assistant", "content": rendered})
|
|
return self._dedupe_transcript_messages(runtime_messages)
|
|
|
|
def _synthesize_resume_tool_call_id(
|
|
self,
|
|
*,
|
|
message_index: int,
|
|
part_index: int,
|
|
tool_name: str,
|
|
) -> str:
|
|
normalized_name = "".join(
|
|
ch if ch.isalnum() else "_"
|
|
for ch in str(tool_name or "tool").strip().lower()
|
|
).strip("_") or "tool"
|
|
return f"resume_call_{message_index}_{part_index}_{normalized_name}"
|
|
|
|
def _resolve_resume_tool_result_id(
|
|
self,
|
|
payload: dict[str, Any],
|
|
*,
|
|
seen_tool_call_ids: set[str],
|
|
pending_tool_calls: list[dict[str, Any]],
|
|
) -> str:
|
|
explicit_id = str(payload.get("tool_call_id", "") or "").strip()
|
|
tool_name = str(payload.get("tool_name", "") or "").strip()
|
|
if explicit_id and explicit_id in seen_tool_call_ids:
|
|
for pending in pending_tool_calls:
|
|
if pending.get("id") == explicit_id and not pending.get("consumed"):
|
|
pending["consumed"] = True
|
|
break
|
|
return explicit_id
|
|
|
|
for pending in pending_tool_calls:
|
|
if pending.get("consumed"):
|
|
continue
|
|
if tool_name and str(pending.get("tool_name", "") or "").strip() != tool_name:
|
|
continue
|
|
pending["consumed"] = True
|
|
return str(pending.get("id", "") or "")
|
|
|
|
for pending in pending_tool_calls:
|
|
if pending.get("consumed"):
|
|
continue
|
|
pending["consumed"] = True
|
|
return str(pending.get("id", "") or "")
|
|
return ""
|
|
|
|
def _render_orphan_tool_result_for_resume(
|
|
self,
|
|
payload: dict[str, Any],
|
|
rendered_output: str,
|
|
) -> str:
|
|
content = str(rendered_output or "").strip()
|
|
if not content:
|
|
return ""
|
|
tool_name = str(payload.get("tool_name", "") or "tool").strip() or "tool"
|
|
return f"Tool result [{tool_name}]\n{content}"
|
|
|
|
def _sanitize_tool_message_sequence(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
sanitized: list[dict[str, Any]] = []
|
|
known_tool_calls: dict[str, str] = {}
|
|
for message in messages:
|
|
role = str(message.get("role", "") or "")
|
|
if role == "assistant":
|
|
sanitized_message = dict(message)
|
|
tool_calls = list(message.get("tool_calls", []) or [])
|
|
valid_tool_calls: list[dict[str, Any]] = []
|
|
for call in tool_calls:
|
|
call_id = str(call.get("id", "") or "").strip()
|
|
if not call_id:
|
|
continue
|
|
valid_tool_calls.append(call)
|
|
known_tool_calls[call_id] = self._tool_call_name(call)
|
|
if valid_tool_calls:
|
|
sanitized_message["tool_calls"] = valid_tool_calls
|
|
else:
|
|
sanitized_message.pop("tool_calls", None)
|
|
sanitized.append(sanitized_message)
|
|
continue
|
|
if role == "tool":
|
|
tool_call_id = str(message.get("tool_call_id", "") or "").strip()
|
|
if tool_call_id and tool_call_id in known_tool_calls:
|
|
sanitized.append(message)
|
|
continue
|
|
downgraded = self._render_orphan_runtime_tool_message(
|
|
message,
|
|
tool_name=known_tool_calls.get(tool_call_id, ""),
|
|
)
|
|
if downgraded:
|
|
sanitized.append({"role": "assistant", "content": downgraded})
|
|
continue
|
|
sanitized.append(message)
|
|
return sanitized
|
|
|
|
@staticmethod
|
|
def _tool_call_name(call: dict[str, Any]) -> str:
|
|
function = call.get("function", {})
|
|
if isinstance(function, dict):
|
|
return str(function.get("name", "") or "").strip()
|
|
return str(call.get("function", "") or "").strip()
|
|
|
|
def _render_orphan_runtime_tool_message(
|
|
self,
|
|
message: dict[str, Any],
|
|
*,
|
|
tool_name: str = "",
|
|
) -> str:
|
|
content = str(message.get("content", "") or "").strip()
|
|
if not content:
|
|
return ""
|
|
call_id = str(message.get("tool_call_id", "") or "").strip()
|
|
label = tool_name or (f"orphan call {call_id}" if call_id else "orphan tool result")
|
|
return f"Tool result [{label}]\n{content}"
|
|
|
|
@staticmethod
|
|
def _normalize_verification_contract_line(line: str) -> str:
|
|
return re.sub(r"^\s*(?:[-*+]\s+|\d+[.)]\s+)", "", str(line or "")).strip()
|
|
|
|
def _normalize_content_for_resume(self, parts: list[Any]) -> str:
|
|
snippets: list[str] = []
|
|
for part in parts:
|
|
payload = dict(part.payload or {})
|
|
if part.part_type == "text":
|
|
text = str(payload.get("text", "") or "").strip()
|
|
if text:
|
|
snippets.append(text)
|
|
elif part.part_type == "tool_output":
|
|
output = str(payload.get("output", "") or "").strip()
|
|
if output:
|
|
snippets.append(f"Tool output [{payload.get('tool_name', 'tool')}]\n{output}")
|
|
elif part.part_type == "subtask_result":
|
|
summary = str(payload.get("summary", "") or "").strip()
|
|
if summary:
|
|
snippets.append(summary)
|
|
return "\n\n".join(snippets).strip()
|
|
|
|
def _should_append_resume_user_turn(self, restored: list[dict[str, Any]], user_message: str) -> bool:
|
|
normalized = " ".join(str(user_message or "").split())
|
|
if not normalized:
|
|
return False
|
|
if not restored:
|
|
return True
|
|
latest = restored[-1]
|
|
if latest.get("role") != "user":
|
|
return True
|
|
latest_normalized = " ".join(str(latest.get("content", "") or "").split())
|
|
return latest_normalized != normalized
|
|
|
|
def _dedupe_transcript_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
deduped: list[dict[str, Any]] = []
|
|
for message in messages:
|
|
fingerprint = hashlib.sha1(
|
|
json.dumps(message, ensure_ascii=False, sort_keys=True, default=str).encode("utf-8")
|
|
).hexdigest()
|
|
if deduped:
|
|
previous = deduped[-1]
|
|
previous_fp = hashlib.sha1(
|
|
json.dumps(previous, ensure_ascii=False, sort_keys=True, default=str).encode("utf-8")
|
|
).hexdigest()
|
|
if previous_fp == fingerprint:
|
|
continue
|
|
deduped.append(message)
|
|
return deduped
|
|
|
|
def _restore_task_ledger(self, task: Task | None) -> list[dict[str, Any]]:
|
|
if not task:
|
|
return []
|
|
runtime_resume = self._runtime_resume_payload(task)
|
|
ledger = runtime_resume.get("task_ledger")
|
|
if not isinstance(ledger, list):
|
|
ledger = dict(task.metadata.get("runtime_v2", {}) or {}).get("task_ledger", [])
|
|
return self._normalize_todos(ledger)
|
|
|
|
def _start_runtime_prefetch(
|
|
self,
|
|
task: Task | None,
|
|
messages: list[dict[str, Any]],
|
|
) -> _RuntimePrefetchHandle | None:
|
|
prefetch_cfg = self.config.system.native_runtime.prefetch
|
|
if not prefetch_cfg.enabled or task is None or self.prefetch_provider is None:
|
|
return None
|
|
query = self._build_prefetch_query(messages)
|
|
if not query:
|
|
return None
|
|
return _RuntimePrefetchHandle(
|
|
task=asyncio.create_task(self.prefetch_provider(task, query, list(messages))),
|
|
query=query,
|
|
)
|
|
|
|
async def _consume_ready_prefetch(
|
|
self,
|
|
handle: _RuntimePrefetchHandle | None,
|
|
*,
|
|
messages: list[dict[str, Any]],
|
|
base_prefix_len: int,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
) -> tuple[list[dict[str, Any]], int, list[str]]:
|
|
if handle is None or handle.consumed or not handle.task.done():
|
|
return messages, base_prefix_len, []
|
|
try:
|
|
payload = handle.task.result() or {}
|
|
except Exception as exc: # pragma: no cover - defensive
|
|
logger.debug(f"Runtime prefetch failed: {exc}")
|
|
handle.consumed = True
|
|
return messages, base_prefix_len, []
|
|
prefetch_parts: list[str] = []
|
|
hits: list[str] = []
|
|
labels = {
|
|
"session_memory": "Session Memory",
|
|
"focused_memory": "Focused Memory",
|
|
"project_memory_candidates": "Project Memory Candidates",
|
|
"skills_summary": "Skills Summary",
|
|
}
|
|
for key, value in payload.items():
|
|
text = str(value or "").strip()
|
|
if not text:
|
|
continue
|
|
prefetch_parts.append(f"## {labels.get(key, key.replace('_', ' ').title())}\n{text}")
|
|
hits.append(key)
|
|
handle.consumed = True
|
|
if not prefetch_parts:
|
|
return messages, base_prefix_len, []
|
|
content = "## Runtime Prefetch\n" + "\n\n".join(prefetch_parts)
|
|
prefetch_message = {"role": "system", "content": content}
|
|
insert_at = base_prefix_len - 1 if base_prefix_len > 1 else 1
|
|
updated_messages = [
|
|
*messages[:insert_at],
|
|
prefetch_message,
|
|
*messages[insert_at:],
|
|
]
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"prefetch_consumed",
|
|
{"query": handle.query[:500], "hits": hits},
|
|
)
|
|
return updated_messages, base_prefix_len + 1, hits
|
|
|
|
@staticmethod
|
|
def _cancel_prefetch(handle: _RuntimePrefetchHandle | None) -> None:
|
|
if handle is not None and not handle.task.done():
|
|
handle.task.cancel()
|
|
|
|
def _build_prefetch_query(self, messages: list[dict[str, Any]]) -> str:
|
|
parts: list[str] = []
|
|
for message in reversed(messages):
|
|
role = str(message.get("role", "") or "")
|
|
if role not in {"user", "assistant"}:
|
|
continue
|
|
content = str(message.get("content", "") or "").strip()
|
|
if not content:
|
|
continue
|
|
parts.append(content[:800])
|
|
if len(parts) >= 2:
|
|
break
|
|
return "\n\n".join(reversed(parts)).strip()
|
|
|
|
async def _maybe_start_streaming_tool_calls(
|
|
self,
|
|
*,
|
|
upto_index: int,
|
|
tool_call_chunks: dict[int, dict[str, Any]],
|
|
early_tool_runs: dict[int, dict[str, Any]],
|
|
executor: StreamingToolExecutor,
|
|
planner: ToolPlanner,
|
|
permission_resolver: RuntimePermissionAdapter,
|
|
task: Task | None,
|
|
on_progress: Any,
|
|
runtime_session_id: str,
|
|
) -> None:
|
|
config = self.config.system.native_runtime.streaming_tool_start
|
|
if not config.enabled:
|
|
return
|
|
for index in sorted(tool_call_chunks):
|
|
if index >= upto_index or index in early_tool_runs:
|
|
continue
|
|
call = self._finalize_tool_calls({index: tool_call_chunks[index]})[0]
|
|
if call.get("arguments_parse_error"):
|
|
continue
|
|
if not self._can_stream_start_tool_call(
|
|
planner=planner,
|
|
permission_resolver=permission_resolver,
|
|
call=call,
|
|
task=task,
|
|
):
|
|
continue
|
|
early_tool_runs[index] = {
|
|
"call": call,
|
|
"task": asyncio.create_task(executor.execute([call], task=task, on_progress=on_progress)),
|
|
}
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"streaming_tool_started",
|
|
{"tool_call_id": call.get("id", ""), "tool_name": call.get("function", "")},
|
|
)
|
|
|
|
def _can_stream_start_tool_call(
|
|
self,
|
|
*,
|
|
planner: ToolPlanner,
|
|
permission_resolver: RuntimePermissionAdapter,
|
|
call: dict[str, Any],
|
|
task: Task | None,
|
|
) -> bool:
|
|
config = self.config.system.native_runtime.streaming_tool_start
|
|
tool = self.tools.get(str(call.get("function", "") or ""))
|
|
if tool is None:
|
|
return False
|
|
if config.safe_read_only_only and not planner.is_concurrency_safe(tool):
|
|
return False
|
|
if config.safe_read_only_only and not planner.is_read_only(tool):
|
|
return False
|
|
if not config.safe_read_only_only and not planner.is_concurrency_safe(tool):
|
|
return False
|
|
if config.require_allow_prediction:
|
|
decision = permission_resolver.predicted_decision(tool, dict(call.get("arguments", {}) or {}), task=task)
|
|
if decision.resolution != PermissionResolution.ALLOW:
|
|
return False
|
|
return not bool(getattr(tool, "requires_confirmation", False))
|
|
|
|
async def _collect_execution_results(
|
|
self,
|
|
*,
|
|
tool_calls: list[dict[str, Any]],
|
|
early_tool_runs: dict[int, dict[str, Any]],
|
|
executor: StreamingToolExecutor,
|
|
task: Task | None,
|
|
on_progress: Any,
|
|
) -> list[dict[str, Any]]:
|
|
ordered_results: list[dict[str, Any]] = []
|
|
remaining_calls: list[dict[str, Any]] = []
|
|
by_id: dict[str, dict[str, Any]] = {}
|
|
for index, call in enumerate(tool_calls):
|
|
early = early_tool_runs.get(index)
|
|
if not early:
|
|
remaining_calls.append(call)
|
|
continue
|
|
early_call = dict(early.get("call", {}) or {})
|
|
if not self._same_tool_call_signature(call, early_call):
|
|
if not early["task"].done():
|
|
early["task"].cancel()
|
|
remaining_calls.append(call)
|
|
continue
|
|
result_items = await early["task"]
|
|
if result_items:
|
|
by_id[str(call.get("id", "") or "")] = result_items[0]
|
|
if remaining_calls:
|
|
for item in await executor.execute(remaining_calls, task=task, on_progress=on_progress):
|
|
by_id[str(item.get("tool_call", {}).get("id", "") or "")] = item
|
|
for call in tool_calls:
|
|
item = by_id.get(str(call.get("id", "") or ""))
|
|
if item is not None:
|
|
ordered_results.append(item)
|
|
return ordered_results
|
|
|
|
def _same_tool_call_signature(self, current: dict[str, Any], started: dict[str, Any]) -> bool:
|
|
if str(current.get("function", "") or "") != str(started.get("function", "") or ""):
|
|
return False
|
|
current_args = dict(current.get("arguments", {}) or {})
|
|
started_args = dict(started.get("arguments", {}) or {})
|
|
return json.dumps(current_args, ensure_ascii=False, sort_keys=True, default=str) == json.dumps(
|
|
started_args,
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
default=str,
|
|
)
|
|
|
|
def _finalize_tool_calls(self, chunks: dict[int, dict[str, Any]]) -> list[dict[str, Any]]:
|
|
finalized: list[dict[str, Any]] = []
|
|
for index in sorted(chunks.keys()):
|
|
item = chunks[index]
|
|
raw_arguments = "".join(item.get("arguments_chunks", []))
|
|
parsed_arguments: Any = {}
|
|
parse_error: str | None = None
|
|
if raw_arguments.strip():
|
|
try:
|
|
parsed_arguments = json.loads(raw_arguments)
|
|
except json.JSONDecodeError as exc:
|
|
parsed_arguments = raw_arguments
|
|
parse_error = f"Invalid tool arguments JSON for `{item.get('function', '')}`: {exc}"
|
|
# Valid JSON that is not an object (e.g. ``[...]`` or ``"x"``) must surface as a
|
|
# parse error too. Otherwise downstream code sees an empty ``arguments`` dict and
|
|
# a falsy ``arguments_parse_error``, executing the tool with no arguments (which
|
|
# can silently wipe state, e.g. todo_write receiving an empty list). Keep the more
|
|
# specific JSONDecodeError message when parsing itself failed.
|
|
if (
|
|
raw_arguments.strip()
|
|
and parse_error is None
|
|
and not isinstance(parsed_arguments, dict)
|
|
):
|
|
parse_error = (
|
|
f"Tool arguments for `{item.get('function', '')}` must be a JSON object; "
|
|
f"got {type(parsed_arguments).__name__}."
|
|
)
|
|
finalized.append({
|
|
"id": item.get("id") or f"tool_{index}",
|
|
"function": item.get("function") or "",
|
|
"arguments": parsed_arguments if isinstance(parsed_arguments, dict) else {},
|
|
"arguments_raw": raw_arguments,
|
|
"arguments_parse_error": parse_error,
|
|
})
|
|
return finalized
|
|
|
|
@staticmethod
|
|
async def _cancel_early_tool_runs(early_tool_runs: dict[int, dict[str, Any]]) -> None:
|
|
tasks: list[asyncio.Task[Any]] = []
|
|
for item in early_tool_runs.values():
|
|
task = item.get("task")
|
|
if isinstance(task, asyncio.Task) and not task.done():
|
|
task.cancel()
|
|
tasks.append(task)
|
|
if tasks:
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
def _tool_call_chunks_from_response(self, tool_calls: list[dict[str, Any]] | None) -> dict[int, dict[str, Any]]:
|
|
chunks: dict[int, dict[str, Any]] = {}
|
|
for index, call in enumerate(tool_calls or []):
|
|
raw_arguments = call.get("arguments_raw")
|
|
if raw_arguments is None:
|
|
arguments = call.get("arguments", {})
|
|
if isinstance(arguments, str):
|
|
raw_arguments = arguments
|
|
else:
|
|
raw_arguments = json.dumps(arguments, ensure_ascii=False, default=str)
|
|
chunks[index] = {
|
|
"id": str(call.get("id", "") or "").strip(),
|
|
"function": str(call.get("function", "") or "").strip(),
|
|
"arguments_chunks": [str(raw_arguments or "")],
|
|
}
|
|
return chunks
|
|
|
|
def _clean_tool_protocol_retry_messages(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
*,
|
|
base_prefix_len: int,
|
|
strategy: str,
|
|
) -> list[dict[str, Any]]:
|
|
if strategy == "sanitize":
|
|
return self.llm.sanitize_tool_call_history(messages)
|
|
prefix = self.llm.sanitize_tool_call_history(messages[:base_prefix_len])
|
|
note = {
|
|
"role": "system",
|
|
"content": (
|
|
"Previous tool-call transcript was reset after a provider tool-calling protocol failure. "
|
|
"Continue from the current task state and regenerate any necessary tool calls cleanly."
|
|
),
|
|
}
|
|
return [*prefix, note]
|
|
|
|
async def _recover_tool_protocol_stream_error(
|
|
self,
|
|
*,
|
|
exc: Exception,
|
|
messages: list[dict[str, Any]],
|
|
tool_schemas: list[dict[str, Any]] | None,
|
|
base_prefix_len: int,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
iteration: int,
|
|
early_tool_runs: dict[int, dict[str, Any]],
|
|
) -> dict[str, Any] | None:
|
|
if not self.llm.is_tool_protocol_error(exc):
|
|
return None
|
|
await self._cancel_early_tool_runs(early_tool_runs)
|
|
last_error: Exception = exc
|
|
attempted_payloads: list[list[dict[str, Any]]] = []
|
|
for strategy in ("sanitize", "clean"):
|
|
retry_messages = self._clean_tool_protocol_retry_messages(
|
|
messages,
|
|
base_prefix_len=base_prefix_len,
|
|
strategy=strategy,
|
|
)
|
|
if attempted_payloads and retry_messages == attempted_payloads[-1]:
|
|
continue
|
|
attempted_payloads.append(retry_messages)
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"tool_protocol_retry",
|
|
{
|
|
"iteration": iteration + 1,
|
|
"strategy": strategy,
|
|
"message": str(last_error),
|
|
},
|
|
)
|
|
try:
|
|
response = await self.llm.chat(retry_messages, tools=tool_schemas if tool_schemas else None)
|
|
return {
|
|
"messages": retry_messages,
|
|
"assistant_text": str(response.get("content", "") or ""),
|
|
"tool_call_chunks": self._tool_call_chunks_from_response(response.get("tool_calls", [])),
|
|
"usage": dict(response.get("usage", {}) or {}),
|
|
"model": str(response.get("model", "") or ""),
|
|
"strategy": strategy,
|
|
}
|
|
except Exception as retry_exc:
|
|
last_error = retry_exc
|
|
if not self.llm.is_tool_protocol_error(retry_exc):
|
|
break
|
|
return None
|
|
|
|
@staticmethod
|
|
def _provider_error_feedback_message(
|
|
exc: Exception,
|
|
*,
|
|
context_reset: bool = False,
|
|
) -> dict[str, str]:
|
|
"""Conversation message telling the model why the last request failed.
|
|
|
|
Unclassified provider rejections (content filters, transient 4xx) never
|
|
produce model output, so without this the model has no way to know the
|
|
request failed or why. Feeding the provider's own error text back lets
|
|
the model decide how to proceed (rephrase, drop a quote, change tack)
|
|
instead of the runtime blindly replaying an identical payload.
|
|
"""
|
|
error_text = " ".join(str(exc).split())[:600]
|
|
if context_reset:
|
|
detail = (
|
|
"The previous LLM request kept failing at the model provider, so the "
|
|
"intermediate steps of the current turn were dropped from the request."
|
|
)
|
|
else:
|
|
detail = (
|
|
"The previous LLM request failed at the model provider before any "
|
|
"output was produced."
|
|
)
|
|
return {
|
|
"role": "system",
|
|
"content": (
|
|
f"[runtime notice] {detail} Provider error: {error_text}. "
|
|
"This was not a user action. Adjust your next step accordingly — for "
|
|
"example rephrase sensitive wording, avoid quoting flagged content "
|
|
"verbatim, or choose another way to make progress — then continue the task."
|
|
),
|
|
}
|
|
|
|
def _truncate_to_last_clean_user_turn(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
base_prefix_len: int,
|
|
) -> list[dict[str, Any]] | None:
|
|
"""Walk backwards to find the last role=user message and truncate there,
|
|
then sanitize to ensure no unpaired tool_call remains."""
|
|
for i in range(len(messages) - 1, base_prefix_len - 1, -1):
|
|
if messages[i].get("role") == "user":
|
|
candidate = messages[: i + 1]
|
|
return self.llm.sanitize_tool_call_history(candidate)
|
|
return None
|
|
|
|
async def _apply_context_pipeline(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
*,
|
|
tool_schemas: list[dict[str, Any]] | None,
|
|
task: Task | None,
|
|
base_prefix_len: int,
|
|
runtime_session_id: str,
|
|
compaction_boundaries: list[dict[str, Any]],
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
active_subagents: list[dict[str, Any]],
|
|
force_compact: bool = False,
|
|
observed_tokens: int = 0,
|
|
) -> list[dict[str, Any]]:
|
|
# History below the hard threshold is never rewritten: model quality
|
|
# and prompt-cache prefixes both depend on old messages staying
|
|
# byte-identical. The only routine mutation is the idempotent
|
|
# per-message tool-result budget (same clip an entry already got).
|
|
bounded = self._apply_tool_result_budget(messages)
|
|
pipeline_steps = ["tool_result_budgeting"]
|
|
compacted = bounded
|
|
durable_applied = False
|
|
wants_compaction = force_compact or self._should_apply_hard_compaction(
|
|
bounded, tool_schemas, observed_tokens=observed_tokens
|
|
)
|
|
if wants_compaction:
|
|
breaker_limit = max(
|
|
1,
|
|
int(self.config.system.native_runtime.reactive_compaction.circuit_breaker_failures or 2),
|
|
)
|
|
failures = int(runtime_notes.get("durable_compaction_failures", 0) or 0)
|
|
if failures < breaker_limit:
|
|
compacted, durable_applied = await self._apply_durable_compaction(
|
|
bounded,
|
|
task=task,
|
|
base_prefix_len=base_prefix_len,
|
|
runtime_session_id=runtime_session_id,
|
|
)
|
|
if durable_applied:
|
|
pipeline_steps.append("durable_compaction")
|
|
runtime_notes["durable_compaction_failures"] = 0
|
|
else:
|
|
runtime_notes["durable_compaction_failures"] = failures + 1
|
|
if not durable_applied and force_compact:
|
|
# Emergency-only mechanical fallback: overflow pressure with
|
|
# the summarizing compactor unavailable or circuit-broken.
|
|
compacted = self._apply_tool_aware_microcompact(compacted, base_prefix_len)
|
|
pipeline_steps.append("emergency_microcompact")
|
|
if compacted != bounded:
|
|
boundary_record = {
|
|
"summary": "Runtime V2 context pipeline compacted persisted history.",
|
|
"message_count": len(compacted),
|
|
"pipeline": [*pipeline_steps, "session_memory_reinjection"],
|
|
}
|
|
compaction_boundaries.append(boundary_record)
|
|
store = getattr(self.memory_manager, "store", None)
|
|
if store and hasattr(store, "save_runtime_compaction_boundary"):
|
|
await store.save_runtime_compaction_boundary(
|
|
boundary_id=f"cb_{uuid.uuid4().hex}",
|
|
runtime_session_id=runtime_session_id,
|
|
task_id=task.id if task else None,
|
|
summary=boundary_record["summary"],
|
|
metadata={"message_count": len(compacted)},
|
|
)
|
|
await self._persist_compaction_boundary(task, boundary_record, runtime_session_id)
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"compaction_applied",
|
|
{"message_count": len(compacted)},
|
|
)
|
|
reinjected = await self._reinject_session_memory(compacted, task=task)
|
|
artifact_reinjected = self._reinject_runtime_artifacts(
|
|
reinjected,
|
|
task=task,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
)
|
|
return self._sanitize_tool_message_sequence(self._dedupe_transcript_messages(artifact_reinjected))
|
|
|
|
def _apply_tool_result_budget(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
guard_budget = int(self.config.system.native_runtime.context_guard.tool_output_char_budget or 12_000)
|
|
runtime_budget = int(self.config.system.native_runtime.tool_result_budget_chars or 20_000)
|
|
budget = min(runtime_budget, guard_budget)
|
|
compacted: list[dict[str, Any]] = []
|
|
for message in messages:
|
|
if message.get("role") == "tool":
|
|
content = str(message.get("content", "") or "")
|
|
if len(content) > budget:
|
|
# Keep head and tail: openings carry the command/context,
|
|
# endings carry the verdict (exit codes, tracebacks).
|
|
head = max(1, budget // 2)
|
|
tail = max(0, budget - head)
|
|
omitted = len(content) - head - tail
|
|
compacted.append({
|
|
**message,
|
|
"content": (
|
|
content[:head]
|
|
+ f"\n[tool result truncated by runtime_v2: {omitted} chars omitted]\n"
|
|
+ (content[-tail:] if tail else "")
|
|
),
|
|
})
|
|
continue
|
|
compacted.append(message)
|
|
return compacted
|
|
|
|
def _apply_tool_aware_microcompact(self, messages: list[dict[str, Any]], base_prefix_len: int) -> list[dict[str, Any]]:
|
|
config = self.config.system.native_runtime.tool_aware_microcompact
|
|
if not config.enabled:
|
|
return self._apply_microcompact(self._apply_history_snip(messages, base_prefix_len), base_prefix_len)
|
|
preserve_recent = max(4, int(config.preserve_recent_messages or 8))
|
|
if len(messages) <= base_prefix_len + preserve_recent:
|
|
return messages
|
|
preserved_tail_start = max(base_prefix_len, len(messages) - preserve_recent)
|
|
compacted: list[dict[str, Any]] = []
|
|
for index, message in enumerate(messages):
|
|
if index < base_prefix_len or index >= preserved_tail_start:
|
|
compacted.append(message)
|
|
continue
|
|
role = str(message.get("role", "") or "")
|
|
if role == "tool":
|
|
compacted.append({
|
|
**message,
|
|
"content": self._summarize_tool_message_content(
|
|
str(message.get("content", "") or ""),
|
|
budget=int(config.tool_result_char_budget or 4000),
|
|
preserve_failure_outputs=bool(config.preserve_failure_outputs),
|
|
),
|
|
})
|
|
continue
|
|
if role == "assistant":
|
|
compacted.append({
|
|
**message,
|
|
"content": self._truncate_text_block(
|
|
str(message.get("content", "") or ""),
|
|
budget=int(config.assistant_char_budget or 3000),
|
|
marker="[assistant context microcompacted]",
|
|
),
|
|
})
|
|
continue
|
|
if role == "system":
|
|
compacted.append({
|
|
**message,
|
|
"content": self._truncate_text_block(
|
|
str(message.get("content", "") or ""),
|
|
budget=int(config.assistant_char_budget or 3000),
|
|
marker="[system context microcompacted]",
|
|
),
|
|
})
|
|
continue
|
|
compacted.append(message)
|
|
return self._apply_history_snip(compacted, base_prefix_len)
|
|
|
|
def _summarize_tool_message_content(
|
|
self,
|
|
content: str,
|
|
*,
|
|
budget: int,
|
|
preserve_failure_outputs: bool,
|
|
) -> str:
|
|
if len(content) <= budget:
|
|
return content
|
|
try:
|
|
parsed = json.loads(content)
|
|
except Exception:
|
|
return self._truncate_text_block(content, budget=budget, marker="[tool result microcompacted]")
|
|
if not isinstance(parsed, dict):
|
|
return self._truncate_text_block(content, budget=budget, marker="[tool result microcompacted]")
|
|
is_failure = not bool(parsed.get("success", True)) or bool(parsed.get("error"))
|
|
if is_failure and preserve_failure_outputs:
|
|
return self._truncate_text_block(content, budget=max(budget, 6000), marker="[tool failure output truncated]")
|
|
result_payload = parsed.get("result", {})
|
|
preview_fields: dict[str, Any] = {
|
|
"success": parsed.get("success", True),
|
|
}
|
|
if parsed.get("error"):
|
|
preview_fields["error"] = str(parsed.get("error", ""))[:800]
|
|
if isinstance(result_payload, dict):
|
|
for key in ("rendered", "summary", "stdout", "stderr", "content", "todos", "task_ledger"):
|
|
value = result_payload.get(key)
|
|
if value in (None, "", [], {}):
|
|
continue
|
|
if isinstance(value, str):
|
|
preview_fields[key] = clip_text(
|
|
value,
|
|
limit=1200,
|
|
marker=f"{key} preview truncated",
|
|
).text
|
|
elif isinstance(value, list):
|
|
preview_fields[key] = value[:6]
|
|
elif isinstance(value, dict):
|
|
preview_fields[key] = {
|
|
inner_key: inner_value
|
|
for inner_key, inner_value in list(value.items())[:6]
|
|
}
|
|
else:
|
|
preview_fields[key] = value
|
|
rendered = json.dumps(preview_fields, ensure_ascii=False, default=str)
|
|
if len(rendered) <= budget:
|
|
return rendered + "\n[tool result microcompacted]"
|
|
return self._truncate_text_block(rendered, budget=budget, marker="[tool result microcompacted]")
|
|
|
|
@staticmethod
|
|
def _truncate_text_block(content: str, *, budget: int, marker: str) -> str:
|
|
marker_text = marker.strip("[]")
|
|
clip = clip_text(content, limit=budget, marker=marker_text)
|
|
exact_marker = f"[{marker_text}]"
|
|
detailed_marker = f"[{marker_text}:"
|
|
if clip.truncated and exact_marker not in clip.text and detailed_marker in clip.text:
|
|
return clip.text.replace(detailed_marker, f"{exact_marker}\n{detailed_marker}", 1)
|
|
return clip.text
|
|
|
|
def _apply_history_snip(self, messages: list[dict[str, Any]], base_prefix_len: int) -> list[dict[str, Any]]:
|
|
trigger = int(self.config.system.native_runtime.history_snip_trigger_messages or 40)
|
|
if len(messages) <= trigger or len(messages) <= base_prefix_len + 12:
|
|
return messages
|
|
preserved_tail = messages[-12:]
|
|
hidden = len(messages) - base_prefix_len - len(preserved_tail)
|
|
if hidden <= 0:
|
|
return messages
|
|
return [
|
|
*messages[:base_prefix_len],
|
|
{
|
|
"role": "system",
|
|
"content": f"[runtime_v2 snip] {hidden} earlier messages hidden after transcript persistence.",
|
|
},
|
|
*preserved_tail,
|
|
]
|
|
|
|
def _apply_microcompact(self, messages: list[dict[str, Any]], base_prefix_len: int) -> list[dict[str, Any]]:
|
|
limit = int(self.config.system.native_runtime.microcompact_chars or 8_000)
|
|
if not limit or len(messages) <= base_prefix_len + 8:
|
|
return messages
|
|
preserved_tail_start = max(base_prefix_len, len(messages) - 8)
|
|
compacted: list[dict[str, Any]] = []
|
|
for index, message in enumerate(messages):
|
|
if index < base_prefix_len or index >= preserved_tail_start:
|
|
compacted.append(message)
|
|
continue
|
|
content = str(message.get("content", "") or "")
|
|
if len(content) <= limit:
|
|
compacted.append(message)
|
|
continue
|
|
compacted.append({
|
|
**message,
|
|
"content": content[:limit] + "\n[runtime_v2 microcompact]",
|
|
})
|
|
return compacted
|
|
|
|
_DURABLE_COMPACTION_MARKER = "[runtime_v2 durable compaction]"
|
|
|
|
async def _apply_durable_compaction(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
*,
|
|
task: Task | None,
|
|
base_prefix_len: int,
|
|
runtime_session_id: str,
|
|
) -> tuple[list[dict[str, Any]], bool]:
|
|
"""Fold old messages into one LLM summary, keeping prefix and tail.
|
|
|
|
Returns (messages, applied). On any summarizer failure the original
|
|
list is returned unchanged so the caller can count failures and the
|
|
model keeps seeing the full history for this round.
|
|
"""
|
|
compactor = self.history_compactor
|
|
summarize = getattr(compactor, "summarize_runtime_history", None) if compactor else None
|
|
if not callable(summarize):
|
|
return messages, False
|
|
preserve_recent = max(
|
|
4,
|
|
int(self.config.system.native_runtime.tool_aware_microcompact.preserve_recent_messages or 8),
|
|
)
|
|
start = max(base_prefix_len, len(messages) - preserve_recent)
|
|
# Never split an assistant tool_calls message from its tool results.
|
|
while start > base_prefix_len and str(messages[start].get("role", "") or "") == "tool":
|
|
start -= 1
|
|
# base_prefix_len goes stale once session-memory/artifact messages are
|
|
# injected into the prefix region, shifting real prefix messages past
|
|
# the boundary. Never fold the system head, and keep the seed user
|
|
# request verbatim on every round (Codex-style): a previous summary is
|
|
# a user message too, but carries the marker and must stay foldable so
|
|
# exactly one summary exists at a time.
|
|
fold_start = base_prefix_len
|
|
while fold_start < start and str(messages[fold_start].get("role", "") or "") == "system":
|
|
fold_start += 1
|
|
if (
|
|
fold_start < start
|
|
and str(messages[fold_start].get("role", "") or "") == "user"
|
|
and self._DURABLE_COMPACTION_MARKER not in str(messages[fold_start].get("content", "") or "")
|
|
and not any(str(item.get("role", "") or "") == "user" for item in messages[:fold_start])
|
|
):
|
|
fold_start += 1
|
|
folded = messages[fold_start:start]
|
|
if len(folded) < 4:
|
|
return messages, False
|
|
try:
|
|
summary = await summarize(
|
|
project_id=str(getattr(task, "project_id", "") or ""),
|
|
session_id=runtime_session_id,
|
|
messages=self._render_messages_for_compaction(folded),
|
|
)
|
|
except Exception as exc:
|
|
logger.warning(f"Durable compaction failed; keeping full history this round: {exc}")
|
|
return messages, False
|
|
summary_text = str(summary or "").strip()
|
|
if not summary_text:
|
|
return messages, False
|
|
summary_message = {
|
|
"role": "user",
|
|
"content": (
|
|
f"{self._DURABLE_COMPACTION_MARKER} Earlier conversation was compacted to stay "
|
|
"within the context window. Continue seamlessly from this summary; the full "
|
|
"transcript remains persisted and queryable.\n\n" + summary_text
|
|
),
|
|
}
|
|
return [*messages[:fold_start], summary_message, *messages[start:]], True
|
|
|
|
def _render_messages_for_compaction(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
per_message_budget = 4_000
|
|
rendered: list[dict[str, Any]] = []
|
|
for message in messages:
|
|
role = str(message.get("role", "") or "assistant")
|
|
content = str(message.get("content", "") or "")
|
|
tool_calls = message.get("tool_calls") or []
|
|
if tool_calls:
|
|
names: list[str] = []
|
|
for call in tool_calls:
|
|
if not isinstance(call, dict):
|
|
continue
|
|
function = call.get("function", "")
|
|
name = function.get("name", "") if isinstance(function, dict) else str(function or "")
|
|
if name:
|
|
names.append(str(name))
|
|
if names:
|
|
content = (content + "\n[called tools: " + ", ".join(names) + "]").strip()
|
|
if role == "tool":
|
|
content = f"[tool result {str(message.get('tool_call_id', '') or '')}] {content}".strip()
|
|
if len(content) > per_message_budget:
|
|
head = per_message_budget // 2
|
|
tail = per_message_budget - head - 100
|
|
content = (
|
|
content[:head]
|
|
+ f"\n[{len(content) - head - tail} chars omitted]\n"
|
|
+ content[-tail:]
|
|
)
|
|
if content:
|
|
rendered.append({"role": role, "content": content})
|
|
return rendered
|
|
|
|
async def _reinject_session_memory(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
*,
|
|
task: Task | None,
|
|
) -> list[dict[str, Any]]:
|
|
if not task or not self.memory_manager or not task.session_id:
|
|
return messages
|
|
session_memory = await self.memory_manager.build_session_memory_context(task.session_id)
|
|
if not session_memory.strip():
|
|
return messages
|
|
memory_message = {"role": "system", "content": session_memory}
|
|
if len(messages) >= 2 and messages[1].get("role") == "system" and "## Session Memory" in str(messages[1].get("content", "")):
|
|
return [messages[0], memory_message, *messages[2:]]
|
|
return [messages[0], memory_message, *messages[1:]]
|
|
|
|
def _boot_artifact_manifest(self, task: Task | None) -> list[dict[str, Any]]:
|
|
if task is None:
|
|
return []
|
|
manifest = list(task.metadata.get("_prompt_harness_boot_artifacts", []) or [])
|
|
return [dict(item) for item in manifest if isinstance(item, dict) and str(item.get("type", "") or "").strip()]
|
|
|
|
def _boot_artifact_hashes(self, task: Task | None) -> dict[str, str]:
|
|
if task is None:
|
|
return {}
|
|
harness_meta = dict(task.metadata.get("prompt_harness", {}) or {})
|
|
hashes = dict(harness_meta.get("artifact_hashes", {}) or {})
|
|
return {str(key): str(value) for key, value in hashes.items() if str(key).strip() and str(value).strip()}
|
|
|
|
def _compact_artifact_text(self, content: str) -> str:
|
|
budget = max(800, int(self.config.system.native_runtime.artifact_compaction.artifact_char_budget or 12_000))
|
|
text = str(content or "").strip()
|
|
if len(text) <= budget:
|
|
return text
|
|
return text[: max(120, budget - 40)].rstrip() + "\n[runtime artifact truncated]"
|
|
|
|
def _task_ledger_artifact_record(self, todo_state: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
if not todo_state:
|
|
return None
|
|
lines = ["Current task ledger:"]
|
|
for item in todo_state[:12]:
|
|
status = str(item.get("status", "") or "pending").strip() or "pending"
|
|
content = str(item.get("content", "") or item.get("title", "") or "").strip()
|
|
active = str(item.get("active_form", "") or item.get("activeForm", "") or "").strip()
|
|
if not content:
|
|
continue
|
|
line = f"- [{status}] {content}"
|
|
if active and active != content:
|
|
line += f" | active: {active}"
|
|
lines.append(line)
|
|
return build_runtime_artifact_record(
|
|
"task_ledger",
|
|
"Task Ledger",
|
|
self._compact_artifact_text("\n".join(lines)),
|
|
metadata={"item_count": len(todo_state)},
|
|
)
|
|
|
|
def _plan_artifact_record(self, task: Task | None) -> dict[str, Any] | None:
|
|
if task is None:
|
|
return None
|
|
plan = task.metadata.get("work_item_runtime_plan")
|
|
if not plan:
|
|
return None
|
|
return build_runtime_artifact_record(
|
|
"plan_attachment",
|
|
"Plan",
|
|
self._compact_artifact_text(str(plan)),
|
|
metadata={"source": "work_item_runtime_plan"},
|
|
)
|
|
|
|
def _skills_artifact_record(self, task: Task | None) -> dict[str, Any] | None:
|
|
if task is None:
|
|
return None
|
|
manifest = self._boot_artifact_manifest(task)
|
|
for item in manifest:
|
|
if str(item.get("type", "") or "").strip() == "skills_delta":
|
|
return dict(item)
|
|
return None
|
|
|
|
def _tool_surface_artifact_record(self, task: Task | None) -> dict[str, Any] | None:
|
|
if task is None:
|
|
return None
|
|
manifest = self._boot_artifact_manifest(task)
|
|
for item in manifest:
|
|
if str(item.get("type", "") or "").strip() == "tool_surface_delta":
|
|
return dict(item)
|
|
return None
|
|
|
|
def _active_subagents_artifact_record(self, active_subagents: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
if not active_subagents or not self.config.system.native_runtime.artifact_compaction.reinject_active_subagents:
|
|
return None
|
|
lines = ["Active subagents:"]
|
|
for item in active_subagents[:8]:
|
|
name = str(item.get("name", "") or item.get("agent_id", "") or "subagent").strip()
|
|
status = str(item.get("status", "") or "unknown").strip()
|
|
description = str(item.get("description", "") or "").strip()
|
|
worktree_path = str(item.get("worktree_path", "") or "").strip()
|
|
line = f"- {name}: {status}"
|
|
if description:
|
|
line += f" | {description}"
|
|
if worktree_path:
|
|
line += f" | worktree={worktree_path}"
|
|
lines.append(line)
|
|
return build_runtime_artifact_record(
|
|
"active_subagents",
|
|
"Active Subagents",
|
|
self._compact_artifact_text("\n".join(lines)),
|
|
metadata={"item_count": len(active_subagents)},
|
|
)
|
|
|
|
def _verification_artifact_record(self, runtime_notes: dict[str, Any]) -> dict[str, Any] | None:
|
|
if not self.config.system.native_runtime.artifact_compaction.reinject_verification_state:
|
|
return None
|
|
verification = dict(runtime_notes.get("verification", {}) or {})
|
|
if not verification:
|
|
return None
|
|
lines = [
|
|
f"- Completed: {bool(verification.get('completed', False))}",
|
|
f"- Passed: {bool(verification.get('passed', False))}",
|
|
f"- Profile: {str(verification.get('profile', '') or '').strip()}",
|
|
]
|
|
verdict = str(verification.get("verdict", "") or "").strip()
|
|
if verdict:
|
|
lines.append(f"- Verdict: {verdict}")
|
|
status_line = str(verification.get("status_line", "") or "").strip()
|
|
if status_line:
|
|
lines.append(f"- Status line: {status_line}")
|
|
return build_runtime_artifact_record(
|
|
"verification_state",
|
|
"Verification",
|
|
self._compact_artifact_text("Verification state:\n" + "\n".join(lines)),
|
|
metadata={"completed": bool(verification.get("completed", False))},
|
|
)
|
|
|
|
def _permission_artifact_record(self, runtime_notes: dict[str, Any]) -> dict[str, Any] | None:
|
|
if not self.config.system.native_runtime.artifact_compaction.reinject_permission_state:
|
|
return None
|
|
details = list(runtime_notes.get("permission_details", []) or [])
|
|
if not details:
|
|
return None
|
|
lines = ["Recent permission decisions:"]
|
|
for item in details[-8:]:
|
|
tool_name = str(item.get("tool_name", "") or "tool").strip()
|
|
resolution = str(item.get("resolution", "") or "allow").strip()
|
|
risk_level = str(item.get("risk_level", "") or "low").strip()
|
|
source = str(item.get("source", "") or "").strip()
|
|
rationale = str(item.get("rationale", "") or "").strip()
|
|
line = f"- {tool_name}: {resolution} | risk={risk_level}"
|
|
if source:
|
|
line += f" | source={source}"
|
|
if rationale:
|
|
line += f" | {rationale}"
|
|
lines.append(line)
|
|
return build_runtime_artifact_record(
|
|
"permission_state",
|
|
"Permission State",
|
|
self._compact_artifact_text("\n".join(lines)),
|
|
metadata={"item_count": len(details)},
|
|
)
|
|
|
|
def _worktree_artifact_record(
|
|
self,
|
|
task: Task | None,
|
|
active_subagents: list[dict[str, Any]],
|
|
) -> dict[str, Any] | None:
|
|
path = self._primary_worktree_path(active_subagents)
|
|
if not path and task is not None:
|
|
path = str(task.metadata.get("target_output_dir", "") or "").strip()
|
|
if not path:
|
|
return None
|
|
return build_runtime_artifact_record(
|
|
"worktree_state",
|
|
"Worktree",
|
|
f"Active worktree path:\n- {path}",
|
|
metadata={"path": path},
|
|
)
|
|
|
|
def _resume_artifact_record(
|
|
self,
|
|
*,
|
|
messages: list[dict[str, Any]],
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
compaction_boundaries: list[dict[str, Any]],
|
|
active_subagents: list[dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
resume_state = self._build_resume_state(
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
)
|
|
lines = [
|
|
f"- Message count: {resume_state.get('message_count', 0)}",
|
|
f"- Task ledger items: {resume_state.get('task_ledger_items', 0)}",
|
|
f"- Compaction records: {len(resume_state.get('compaction_records', []) or [])}",
|
|
f"- Active subagents: {resume_state.get('active_subagents', 0)}",
|
|
]
|
|
verification_status = str(resume_state.get("verification_status", "") or "").strip()
|
|
if verification_status:
|
|
lines.append(f"- Verification: {verification_status}")
|
|
return build_runtime_artifact_record(
|
|
"resume_state",
|
|
"Resume State",
|
|
self._compact_artifact_text("Runtime resume envelope:\n" + "\n".join(lines)),
|
|
metadata=resume_state,
|
|
)
|
|
|
|
@staticmethod
|
|
def _merge_artifact_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
merged: dict[str, dict[str, Any]] = {}
|
|
order: list[str] = []
|
|
for record in records:
|
|
if not isinstance(record, dict):
|
|
continue
|
|
artifact_type = str(record.get("type", "") or "").strip()
|
|
if not artifact_type:
|
|
continue
|
|
if artifact_type not in order:
|
|
order.append(artifact_type)
|
|
merged[artifact_type] = dict(record)
|
|
return [merged[item] for item in order if item in merged]
|
|
|
|
def _compose_runtime_artifact_manifest(
|
|
self,
|
|
*,
|
|
task: Task | None,
|
|
messages: list[dict[str, Any]],
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
compaction_boundaries: list[dict[str, Any]],
|
|
active_subagents: list[dict[str, Any]],
|
|
) -> list[dict[str, Any]]:
|
|
if not self.config.system.native_runtime.artifact_compaction.enabled:
|
|
return list(runtime_notes.get("artifact_manifest", []) or [])
|
|
records: list[dict[str, Any]] = [
|
|
*list(runtime_notes.get("artifact_manifest", []) or []),
|
|
*list(self._boot_artifact_manifest(task)),
|
|
]
|
|
for candidate in (
|
|
self._task_ledger_artifact_record(todo_state),
|
|
self._plan_artifact_record(task),
|
|
self._tool_surface_artifact_record(task) if self.config.system.native_runtime.artifact_compaction.reinject_tool_surface_delta else None,
|
|
self._skills_artifact_record(task) if self.config.system.native_runtime.artifact_compaction.reinject_skills_delta else None,
|
|
self._active_subagents_artifact_record(active_subagents),
|
|
self._verification_artifact_record(runtime_notes),
|
|
self._permission_artifact_record(runtime_notes),
|
|
self._worktree_artifact_record(task, active_subagents),
|
|
self._resume_artifact_record(
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
),
|
|
):
|
|
if candidate is not None:
|
|
records.append(candidate)
|
|
merged = self._merge_artifact_records(records)
|
|
runtime_notes["artifact_manifest"] = list(merged)
|
|
runtime_notes["artifact_hashes"] = {
|
|
str(item.get("type", "") or ""): str(item.get("content_hash", "") or "")
|
|
for item in merged
|
|
if str(item.get("type", "") or "").strip()
|
|
}
|
|
return merged
|
|
|
|
def _record_to_artifact(self, record: dict[str, Any]) -> RuntimeArtifact | None:
|
|
artifact_type = str(record.get("type", "") or "").strip()
|
|
title = str(record.get("title", "") or "").strip()
|
|
content = self._compact_artifact_text(str(record.get("content", "") or "").strip())
|
|
if not artifact_type or not title or not content:
|
|
return None
|
|
metadata = dict(record.get("metadata", {}) or {})
|
|
if record.get("content_hash"):
|
|
metadata["content_hash"] = str(record.get("content_hash", "") or "")
|
|
return RuntimeArtifact(
|
|
artifact_type=artifact_type,
|
|
title=title,
|
|
content=content,
|
|
scope=str(record.get("scope", "runtime") or "runtime"),
|
|
metadata=metadata,
|
|
)
|
|
|
|
def _reinject_runtime_artifacts(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
*,
|
|
task: Task | None,
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
compaction_boundaries: list[dict[str, Any]],
|
|
active_subagents: list[dict[str, Any]],
|
|
) -> list[dict[str, Any]]:
|
|
cfg = self.config.system.native_runtime.prompt_harness
|
|
if not cfg.enabled or not cfg.artifact_messages_enabled or not cfg.reinject_after_compaction:
|
|
return messages
|
|
manifest = self._compose_runtime_artifact_manifest(
|
|
task=task,
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
)
|
|
artifacts = [artifact for item in manifest if (artifact := self._record_to_artifact(item)) is not None]
|
|
artifact_messages = render_runtime_artifact_messages(
|
|
artifacts,
|
|
previous_hashes=self._boot_artifact_hashes(task),
|
|
emit_delta_messages=cfg.emit_delta_messages,
|
|
)
|
|
if not artifact_messages:
|
|
return strip_runtime_artifact_messages(messages)
|
|
base_messages = strip_runtime_artifact_messages(messages)
|
|
insert_at = 1
|
|
while insert_at < len(base_messages) and str(base_messages[insert_at].get("role", "") or "") == "system":
|
|
insert_at += 1
|
|
return [
|
|
*base_messages[:insert_at],
|
|
*artifact_messages,
|
|
*base_messages[insert_at:],
|
|
]
|
|
|
|
def _handle_pause_or_peer_wait(
|
|
self,
|
|
execution_results: list[dict[str, Any]],
|
|
aggregated_artifacts: dict[str, Any],
|
|
runtime_session_id: str,
|
|
*,
|
|
task: Task | None,
|
|
compaction_boundaries: list[dict[str, Any]],
|
|
active_subagents: list[dict[str, Any]],
|
|
messages: list[dict[str, Any]],
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
) -> TaskResult | None:
|
|
permission_requests = self._permission_requests_from_results(execution_results)
|
|
worktree_path = self._primary_worktree_path(active_subagents)
|
|
resume_state = self._build_resume_state(
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
)
|
|
artifact_manifest = self._compose_runtime_artifact_manifest(
|
|
task=task,
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
)
|
|
verification_verdict = str(dict(runtime_notes.get("verification", {}) or {}).get("status_line", "") or "")
|
|
for item in execution_results:
|
|
call = item["tool_call"]
|
|
result = item["result"]
|
|
tool_payload = result.get("result", result if isinstance(result, dict) else {})
|
|
if not isinstance(tool_payload, dict):
|
|
tool_payload = {}
|
|
if call.get("function") == "request_user_input" or tool_payload.get("requires_user_input"):
|
|
reason = tool_payload.get("reason") or f"Need user input before continuing tool `{call.get('function', '')}`."
|
|
return TaskResult(
|
|
status=TaskStatus.AWAITING_HUMAN,
|
|
content=reason,
|
|
artifacts={
|
|
**aggregated_artifacts,
|
|
"runtime_session_id": runtime_session_id,
|
|
"tool_name": call.get("function", ""),
|
|
"tool_args": call.get("arguments", {}),
|
|
"pause_request": tool_payload,
|
|
"permission_requests": permission_requests,
|
|
"active_subagents": active_subagents,
|
|
"compaction_boundaries": list(compaction_boundaries),
|
|
"compaction_records": list(compaction_boundaries),
|
|
"worktree_path": worktree_path,
|
|
"task_ledger": list(todo_state),
|
|
"prefetch_hits": list(runtime_notes.get("prefetch_hits", []) or []),
|
|
"verification_verdict": verification_verdict,
|
|
"artifact_manifest": list(artifact_manifest),
|
|
"resume_state": resume_state,
|
|
},
|
|
)
|
|
if tool_payload.get("requires_peer_wait"):
|
|
reason = tool_payload.get("reason") or f"Waiting for peer coordination for `{call.get('function', '')}`."
|
|
return TaskResult(
|
|
status=TaskStatus.AWAITING_PEER,
|
|
content=reason,
|
|
artifacts={
|
|
**aggregated_artifacts,
|
|
"runtime_session_id": runtime_session_id,
|
|
"tool_name": call.get("function", ""),
|
|
"tool_args": call.get("arguments", {}),
|
|
"pause_request": tool_payload,
|
|
"permission_requests": permission_requests,
|
|
"active_subagents": active_subagents,
|
|
"compaction_boundaries": list(compaction_boundaries),
|
|
"compaction_records": list(compaction_boundaries),
|
|
"worktree_path": worktree_path,
|
|
"task_ledger": list(todo_state),
|
|
"prefetch_hits": list(runtime_notes.get("prefetch_hits", []) or []),
|
|
"verification_verdict": verification_verdict,
|
|
"artifact_manifest": list(artifact_manifest),
|
|
"resume_state": resume_state,
|
|
},
|
|
)
|
|
approval = dict(result.get("approval", {}) or {})
|
|
if approval.get("action") in {"require_input", "escalate"}:
|
|
return TaskResult(
|
|
status=TaskStatus.AWAITING_HUMAN,
|
|
content=str(result.get("error", "") or "Awaiting approval."),
|
|
artifacts={
|
|
**aggregated_artifacts,
|
|
"runtime_session_id": runtime_session_id,
|
|
"tool_name": call.get("function", ""),
|
|
"tool_args": call.get("arguments", {}),
|
|
"approval": approval,
|
|
"permission_requests": permission_requests,
|
|
"active_subagents": active_subagents,
|
|
"compaction_boundaries": list(compaction_boundaries),
|
|
"compaction_records": list(compaction_boundaries),
|
|
"worktree_path": worktree_path,
|
|
"task_ledger": list(todo_state),
|
|
"prefetch_hits": list(runtime_notes.get("prefetch_hits", []) or []),
|
|
"verification_verdict": verification_verdict,
|
|
"artifact_manifest": list(artifact_manifest),
|
|
"resume_state": resume_state,
|
|
},
|
|
)
|
|
return None
|
|
|
|
async def _persist_task_ledger(
|
|
self,
|
|
*,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
messages: list[dict[str, Any]],
|
|
compaction_boundaries: list[dict[str, Any]],
|
|
active_subagents: list[dict[str, Any]],
|
|
) -> None:
|
|
ledger_cfg = self.config.system.native_runtime.task_ledger
|
|
if not ledger_cfg.enabled:
|
|
return
|
|
normalized = self._normalize_todos(todo_state)[: max(1, int(ledger_cfg.max_items or 24))]
|
|
runtime_notes["task_ledger"] = list(normalized)
|
|
if task is not None and ledger_cfg.persist_to_task_metadata:
|
|
task.metadata = dict(task.metadata)
|
|
runtime_meta = dict(task.metadata.get("runtime_v2", {}) or {})
|
|
runtime_meta["task_ledger"] = list(normalized)
|
|
task.metadata["runtime_v2"] = runtime_meta
|
|
task.context_snapshot = dict(task.context_snapshot)
|
|
task.context_snapshot["runtime_v2"] = runtime_meta
|
|
if ledger_cfg.persist_to_runtime_session:
|
|
await self._save_runtime_session(
|
|
runtime_session_id,
|
|
task,
|
|
"running",
|
|
self._build_runtime_state_metadata(
|
|
task=task,
|
|
messages=messages,
|
|
todo_state=normalized,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
),
|
|
)
|
|
if ledger_cfg.emit_runtime_events:
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"task_ledger_updated",
|
|
{"task_ledger": list(normalized), "item_count": len(normalized)},
|
|
)
|
|
|
|
def _build_runtime_state_metadata(
|
|
self,
|
|
*,
|
|
task: Task | None,
|
|
messages: list[dict[str, Any]],
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
compaction_boundaries: list[dict[str, Any]],
|
|
active_subagents: list[dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
artifact_manifest = self._compose_runtime_artifact_manifest(
|
|
task=task,
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
)
|
|
return {
|
|
"task_ledger": list(todo_state),
|
|
"prefetch_hits": list(runtime_notes.get("prefetch_hits", []) or []),
|
|
"verification": dict(runtime_notes.get("verification", {}) or {}),
|
|
"verification_evidence": dict((runtime_notes.get("verification", {}) or {}).get("evidence", {}) or {}),
|
|
"verification_verdict": str(dict(runtime_notes.get("verification", {}) or {}).get("status_line", "") or ""),
|
|
"compaction_records": list(compaction_boundaries),
|
|
"artifact_manifest": list(artifact_manifest),
|
|
"resume_state": self._build_resume_state(
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=compaction_boundaries,
|
|
active_subagents=active_subagents,
|
|
),
|
|
}
|
|
|
|
def _build_resume_state(
|
|
self,
|
|
*,
|
|
messages: list[dict[str, Any]],
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
compaction_boundaries: list[dict[str, Any]],
|
|
active_subagents: list[dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"message_count": len(messages),
|
|
"task_ledger_items": len(todo_state),
|
|
"prefetch_hits": list(runtime_notes.get("prefetch_hits", []) or []),
|
|
"compaction_records": list(compaction_boundaries),
|
|
"active_subagents": len(active_subagents),
|
|
"verification_evidence": dict((runtime_notes.get("verification", {}) or {}).get("evidence", {}) or {}),
|
|
"verification_status": str(dict(runtime_notes.get("verification", {}) or {}).get("status_line", "") or ""),
|
|
"artifact_count": len(list(runtime_notes.get("artifact_manifest", []) or [])),
|
|
"artifact_types": [
|
|
str(item.get("type", "") or "").strip()
|
|
for item in list(runtime_notes.get("artifact_manifest", []) or [])
|
|
if isinstance(item, dict) and str(item.get("type", "") or "").strip()
|
|
],
|
|
}
|
|
|
|
def _build_verification_prompt(
|
|
self,
|
|
task: Task | None,
|
|
*,
|
|
work_item_name: str = "",
|
|
turn_type: str = "",
|
|
) -> str:
|
|
scope = "the parent agent's work"
|
|
if work_item_name:
|
|
scope = f"the parent agent's company work item `{work_item_name}` ({turn_type or 'execute'})"
|
|
contract = [
|
|
f"Validate {scope}.",
|
|
"You must produce executable verification evidence, not just an opinion.",
|
|
"Required format:",
|
|
"Check: <what you verified>",
|
|
"Command: <exact command you ran>",
|
|
"Observed Output: <what you saw>",
|
|
"Result: PASS | FAIL",
|
|
"Repeat for each check you actually ran.",
|
|
"End with exactly one line: VERDICT: PASS or VERDICT: FAIL or VERDICT: PARTIAL.",
|
|
"If the work is acceptable overall, you may begin the final summary with `VERIFIED:`.",
|
|
"If blocking issues remain, begin the final summary with `ISSUES:`.",
|
|
"Do not omit command/output evidence.",
|
|
]
|
|
if task is not None and str(task.metadata.get("execution_mode", "") or "").strip() == "company_mode":
|
|
contract.append(
|
|
"Focus on regressions, missing evidence, handoff quality, ownership-contract violations, and risky filesystem or shell changes."
|
|
)
|
|
else:
|
|
contract.append(
|
|
"Focus on regressions, missed validation, and risky filesystem or shell changes."
|
|
)
|
|
return " ".join(contract)
|
|
|
|
def _parse_verification_evidence(self, text: str) -> VerificationEvidence:
|
|
raw = str(text or "").strip()
|
|
if not raw:
|
|
return VerificationEvidence(status="missing", raw_output=raw)
|
|
lines = [line.rstrip() for line in raw.splitlines()]
|
|
checks: list[dict[str, Any]] = []
|
|
current: dict[str, str] = {}
|
|
verdict = ""
|
|
summary_lines: list[str] = []
|
|
for raw_line in lines:
|
|
line = raw_line.strip()
|
|
if not line:
|
|
continue
|
|
normalized_line = self._normalize_verification_contract_line(line)
|
|
lowered = normalized_line.lower()
|
|
if lowered.startswith("verdict:"):
|
|
verdict_value = normalized_line.split(":", 1)[1].strip().lower()
|
|
if verdict_value.startswith("pass"):
|
|
verdict = "pass"
|
|
elif verdict_value.startswith("fail"):
|
|
verdict = "fail"
|
|
elif verdict_value.startswith("partial"):
|
|
verdict = "partial"
|
|
continue
|
|
if lowered.startswith("check:"):
|
|
if current:
|
|
checks.append(dict(current))
|
|
current = {}
|
|
current["check"] = normalized_line.split(":", 1)[1].strip()
|
|
continue
|
|
if lowered.startswith("command:"):
|
|
current["command"] = normalized_line.split(":", 1)[1].strip()
|
|
continue
|
|
if lowered.startswith("observed output:"):
|
|
current["observed_output"] = normalized_line.split(":", 1)[1].strip()
|
|
continue
|
|
if lowered.startswith("result:"):
|
|
current["result"] = normalized_line.split(":", 1)[1].strip().upper()
|
|
continue
|
|
if normalized_line.startswith("VERIFIED:") or normalized_line.startswith("ISSUES:"):
|
|
summary_lines.append(normalized_line)
|
|
continue
|
|
if current and "observed_output" in current and "result" not in current:
|
|
current["observed_output"] = f"{current.get('observed_output', '')}\n{line}".strip()
|
|
else:
|
|
summary_lines.append(normalized_line)
|
|
if current:
|
|
checks.append(dict(current))
|
|
status = "provided" if checks and verdict else "missing"
|
|
summary = "\n".join(summary_lines).strip()
|
|
return VerificationEvidence(
|
|
status=status,
|
|
verdict=verdict,
|
|
summary=summary,
|
|
checks=checks,
|
|
raw_output=raw,
|
|
)
|
|
|
|
@staticmethod
|
|
def _verification_unavailable_evidence(*, summary: str, raw_output: str = "") -> VerificationEvidence:
|
|
text = str(summary or "").strip() or "Verification unavailable."
|
|
return VerificationEvidence(
|
|
status="unavailable",
|
|
verdict="partial",
|
|
summary=text,
|
|
checks=[],
|
|
raw_output=str(raw_output or text).strip(),
|
|
)
|
|
|
|
def _build_verification_retry_prompt(
|
|
self,
|
|
task: Task | None,
|
|
*,
|
|
previous_output: str,
|
|
) -> str:
|
|
summary = self._build_verification_prompt(task)
|
|
if task is not None and str(task.metadata.get("execution_mode", "") or "").strip() == "company_mode":
|
|
work_item_name = str(task.title or "").strip()
|
|
turn_type = turn_type_for_task(task, fallback="execute")
|
|
summary = self._build_verification_prompt(
|
|
task,
|
|
work_item_name=work_item_name,
|
|
turn_type=turn_type,
|
|
)
|
|
previous = str(previous_output or "").strip()
|
|
return (
|
|
"Your previous verification response did not satisfy the structured evidence contract. "
|
|
"Rewrite the verification result using the required format below. "
|
|
"If your earlier pass lacked executable checks, rerun the minimum necessary checks now and report them explicitly.\n\n"
|
|
f"{summary}\n\n"
|
|
"Previous output:\n"
|
|
f"{previous or '[empty]'}"
|
|
)
|
|
|
|
def _apply_verification_contract(
|
|
self,
|
|
assistant_text: str,
|
|
*,
|
|
task: Task | None,
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
) -> tuple[str, str]:
|
|
config = self.config.system.native_runtime.verification_contract
|
|
if not config.enabled:
|
|
return assistant_text, ""
|
|
if self._is_task_mode_runtime_task(task):
|
|
verification_state = dict(runtime_notes.get("verification", {}) or {})
|
|
if verification_state.get("completed"):
|
|
verdict = (
|
|
f"Verification: verified by {verification_state.get('profile', 'verify')}."
|
|
if verification_state.get("passed")
|
|
else f"Verification: not verified. {str(verification_state.get('verdict', '')).strip() or 'Issues remain.'}"
|
|
)
|
|
runtime_notes["verification"] = {
|
|
**verification_state,
|
|
"status_line": verdict,
|
|
"advisory": True,
|
|
}
|
|
return assistant_text, verdict
|
|
return assistant_text, ""
|
|
verification_state = dict(runtime_notes.get("verification", {}) or {})
|
|
if verification_state.get("completed"):
|
|
if verification_state.get("passed"):
|
|
verdict = f"Verification: verified by {verification_state.get('profile', 'verify')}."
|
|
else:
|
|
verdict = f"Verification: not verified. {str(verification_state.get('verdict', '')).strip() or 'Blocking issues remain.'}"
|
|
elif self._verification_required(task=task, todo_state=todo_state, runtime_notes=runtime_notes):
|
|
verdict = "Verification: not run because the verifier did not complete successfully."
|
|
elif task is not None and task.metadata.get(self.config.system.native_runtime.verification_policy.skip_metadata_key):
|
|
verdict = "Verification: not run because this task explicitly skipped verification."
|
|
else:
|
|
verdict = "Verification: not required because no code edits or risky runtime actions were detected."
|
|
runtime_notes["verification"] = {
|
|
**verification_state,
|
|
"status_line": verdict,
|
|
}
|
|
if not config.append_status_to_final:
|
|
return assistant_text, verdict
|
|
if not config.require_explicit_status or "verification:" in assistant_text.lower():
|
|
return assistant_text, verdict
|
|
final_text = assistant_text.strip()
|
|
if final_text:
|
|
final_text = f"{final_text}\n\n{verdict}"
|
|
else:
|
|
final_text = verdict
|
|
return final_text, verdict
|
|
|
|
async def _seed_user_turn(
|
|
self,
|
|
task: Task | None,
|
|
user_message: str,
|
|
*,
|
|
runtime_session_id: str,
|
|
conversation_turn_id: str,
|
|
) -> None:
|
|
if not task or not self.memory_manager or not task.session_id:
|
|
return
|
|
if task.metadata.get("_runtime_v2_user_seeded"):
|
|
return
|
|
task.metadata["_runtime_v2_user_seeded"] = True
|
|
canonical_turn_id = str(conversation_turn_id or "").strip()
|
|
metadata = {
|
|
"kind": "runtime_v2_user_turn",
|
|
"runtime_session_id": runtime_session_id,
|
|
}
|
|
if canonical_turn_id:
|
|
metadata.update({
|
|
"conversation_turn_id": canonical_turn_id,
|
|
"canonical_turn_id": canonical_turn_id,
|
|
"turn_id": canonical_turn_id,
|
|
"ui_message_id": f"runtime-v2-user:{canonical_turn_id}",
|
|
})
|
|
message = await self.memory_manager.record_user_turn(
|
|
session_id=task.session_id,
|
|
content=user_message,
|
|
project_id=task.project_id,
|
|
metadata=metadata,
|
|
)
|
|
store = getattr(self.memory_manager, "store", None)
|
|
if store and hasattr(store, "save_runtime_transcript_entry"):
|
|
await store.save_runtime_transcript_entry(
|
|
runtime_session_id=runtime_session_id,
|
|
task_id=task.id,
|
|
session_id=task.session_id,
|
|
message_id=getattr(message, "message_id", "") if message else "",
|
|
role="user",
|
|
entry_type="message",
|
|
content=user_message,
|
|
metadata=metadata,
|
|
)
|
|
|
|
async def _persist_assistant_turn(
|
|
self,
|
|
task: Task | None,
|
|
assistant_text: str,
|
|
tool_calls: list[dict[str, Any]],
|
|
*,
|
|
runtime_session_id: str,
|
|
turn_id: str = "",
|
|
conversation_turn_id: str = "",
|
|
iteration: int | None = None,
|
|
thinking_text: str = "",
|
|
) -> None:
|
|
if not task or not self.memory_manager or not task.session_id:
|
|
return
|
|
message_turn_id = str(turn_id or f"{runtime_session_id}:{iteration or ''}").strip().rstrip(":")
|
|
canonical_turn_id = str(conversation_turn_id or message_turn_id).strip().rstrip(":")
|
|
is_task_mode = self._is_task_mode_runtime_task(task)
|
|
is_company_mode = (not is_task_mode) and self._is_company_mode_runtime_task(task)
|
|
is_intermediate_tool_turn = bool(tool_calls) and is_task_mode
|
|
if is_intermediate_tool_turn:
|
|
source_kind = "runtime_v2_intermediate_assistant"
|
|
elif is_company_mode:
|
|
source_kind = "runtime_v2_company_assistant"
|
|
else:
|
|
source_kind = "runtime_v2_assistant"
|
|
metadata = {
|
|
"kind": source_kind,
|
|
"runtime_session_id": runtime_session_id,
|
|
"source_kind": source_kind,
|
|
}
|
|
if not tool_calls:
|
|
metadata.update(
|
|
result_delivery_identity_payload_for_task(
|
|
task,
|
|
canonical_turn_id=canonical_turn_id,
|
|
)
|
|
)
|
|
metadata.update(work_item_identity_payload_for_task(task))
|
|
if task.session_id:
|
|
metadata["child_session_id"] = str(task.session_id)
|
|
if is_company_mode:
|
|
metadata["execution_mode"] = "company_mode"
|
|
metadata["company_runtime_raw_turn"] = True
|
|
if not tool_calls:
|
|
# Terminal iteration of the company turn — this is the role's
|
|
# final reply. Marked so the UI can show it at summary detail
|
|
# even though the kind is otherwise full-detail-only.
|
|
metadata["company_final_turn"] = True
|
|
if task.assigned_to:
|
|
metadata["role_id"] = str(task.assigned_to)
|
|
else:
|
|
metadata["visible_speaker"] = "OPC"
|
|
if canonical_turn_id:
|
|
metadata["conversation_turn_id"] = canonical_turn_id
|
|
metadata["canonical_turn_id"] = canonical_turn_id
|
|
metadata["turn_id"] = canonical_turn_id
|
|
if message_turn_id and message_turn_id != canonical_turn_id:
|
|
metadata["execution_turn_id"] = message_turn_id
|
|
if is_company_mode:
|
|
# Tool-calling iterations of a company conversation turn share
|
|
# one UI row; the terminal reply gets its own id. The id-keyed
|
|
# backfill merge keeps the first-inserted content for same-kind
|
|
# candidates, so reusing the shared id freezes the row at
|
|
# iteration 1 and swallows the final reply entirely.
|
|
if metadata.get("company_final_turn"):
|
|
metadata["ui_message_id"] = f"runtime-v2-company-assistant-final:{canonical_turn_id}"
|
|
else:
|
|
metadata["ui_message_id"] = f"runtime-v2-company-assistant:{canonical_turn_id}"
|
|
elif is_intermediate_tool_turn:
|
|
metadata["ui_message_id"] = f"runtime-v2-intermediate-assistant:{message_turn_id or canonical_turn_id}"
|
|
else:
|
|
metadata["ui_message_id"] = f"runtime-v2-assistant:{canonical_turn_id}"
|
|
if iteration is not None:
|
|
metadata["iteration"] = iteration
|
|
normalized_thinking = str(thinking_text or "").strip()
|
|
if normalized_thinking and is_task_mode:
|
|
metadata["runtime_thinking"] = normalized_thinking
|
|
message = await self.memory_manager.append_session_message(
|
|
session_id=task.session_id,
|
|
role="assistant",
|
|
text=assistant_text,
|
|
project_id=task.project_id,
|
|
agent_id=task.assigned_to or None,
|
|
task_id=task.id,
|
|
metadata=metadata,
|
|
)
|
|
if not message:
|
|
return
|
|
if normalized_thinking and is_task_mode:
|
|
await self.memory_manager.append_session_part(
|
|
task.session_id,
|
|
message.message_id,
|
|
"thinking",
|
|
{
|
|
"text": normalized_thinking,
|
|
"turn_id": canonical_turn_id,
|
|
"runtime_session_id": runtime_session_id,
|
|
"kind": "runtime_v2_thinking",
|
|
},
|
|
)
|
|
store = getattr(self.memory_manager, "store", None)
|
|
if store and hasattr(store, "save_runtime_transcript_entry"):
|
|
await store.save_runtime_transcript_entry(
|
|
runtime_session_id=runtime_session_id,
|
|
task_id=task.id,
|
|
session_id=task.session_id,
|
|
message_id=message.message_id,
|
|
role="assistant",
|
|
entry_type="message",
|
|
content=assistant_text,
|
|
metadata=metadata,
|
|
)
|
|
for tool_call in tool_calls:
|
|
await self.memory_manager.append_session_part(
|
|
task.session_id,
|
|
message.message_id,
|
|
"tool_call",
|
|
{
|
|
"tool_call_id": tool_call.get("id", ""),
|
|
"tool_name": tool_call.get("function", ""),
|
|
"arguments": tool_call.get("arguments", {}),
|
|
},
|
|
)
|
|
if store and hasattr(store, "save_runtime_tool_call"):
|
|
await store.save_runtime_tool_call(
|
|
runtime_session_id=runtime_session_id,
|
|
task_id=task.id,
|
|
session_id=task.session_id,
|
|
message_id=message.message_id,
|
|
tool_call_id=str(tool_call.get("id", "") or ""),
|
|
tool_name=str(tool_call.get("function", "") or ""),
|
|
arguments=dict(tool_call.get("arguments", {}) or {}),
|
|
metadata={
|
|
"arguments_raw": str(tool_call.get("arguments_raw", "") or ""),
|
|
"arguments_parse_error": str(tool_call.get("arguments_parse_error", "") or ""),
|
|
},
|
|
)
|
|
|
|
async def _persist_tool_result(
|
|
self,
|
|
task: Task | None,
|
|
call: dict[str, Any],
|
|
result: dict[str, Any],
|
|
decision: Any,
|
|
*,
|
|
runtime_session_id: str,
|
|
hook_metadata: dict[str, Any] | None = None,
|
|
) -> None:
|
|
if not task or not self.memory_manager or not task.session_id:
|
|
return
|
|
message = await self.memory_manager.append_session_message(
|
|
session_id=task.session_id,
|
|
role="assistant",
|
|
text=json.dumps(result, ensure_ascii=False, default=str),
|
|
part_type="tool_output",
|
|
project_id=task.project_id,
|
|
agent_id=task.assigned_to or None,
|
|
task_id=task.id,
|
|
metadata={
|
|
"kind": "runtime_v2_tool_output",
|
|
"tool_name": str(call.get("function", "") or ""),
|
|
"runtime_session_id": runtime_session_id,
|
|
"hook_metadata": dict(hook_metadata or {}),
|
|
},
|
|
)
|
|
if not message:
|
|
return
|
|
store = getattr(self.memory_manager, "store", None)
|
|
await self.memory_manager.append_session_part(
|
|
task.session_id,
|
|
message.message_id,
|
|
"tool_result",
|
|
{
|
|
"tool_call_id": str(call.get("id", "") or ""),
|
|
"tool_name": str(call.get("function", "") or ""),
|
|
"result": result.get("result", result),
|
|
"permission_decision": {
|
|
"resolution": getattr(getattr(decision, "resolution", None), "value", ""),
|
|
"scope": getattr(getattr(decision, "scope", None), "value", ""),
|
|
"rationale": str(getattr(decision, "rationale", "") or ""),
|
|
"source": str(getattr(decision, "source", "") or ""),
|
|
},
|
|
},
|
|
)
|
|
if store and hasattr(store, "save_runtime_transcript_entry"):
|
|
await store.save_runtime_transcript_entry(
|
|
runtime_session_id=runtime_session_id,
|
|
task_id=task.id,
|
|
session_id=task.session_id,
|
|
message_id=message.message_id,
|
|
role="assistant",
|
|
entry_type="tool_result",
|
|
content=json.dumps(result, ensure_ascii=False, default=str),
|
|
metadata={"tool_name": str(call.get("function", "") or "")},
|
|
)
|
|
if store and hasattr(store, "save_runtime_tool_result"):
|
|
await store.save_runtime_tool_result(
|
|
runtime_session_id=runtime_session_id,
|
|
task_id=task.id,
|
|
session_id=task.session_id,
|
|
message_id=message.message_id,
|
|
tool_call_id=str(call.get("id", "") or ""),
|
|
tool_name=str(call.get("function", "") or ""),
|
|
payload=dict(result),
|
|
metadata={
|
|
"hook_metadata": dict(hook_metadata or {}),
|
|
"permission_decision": {
|
|
"resolution": getattr(getattr(decision, "resolution", None), "value", ""),
|
|
"scope": getattr(getattr(decision, "scope", None), "value", ""),
|
|
"risk_level": getattr(getattr(decision, "risk_level", None), "value", ""),
|
|
"rationale": str(getattr(decision, "rationale", "") or ""),
|
|
"source": str(getattr(decision, "source", "") or ""),
|
|
}
|
|
},
|
|
)
|
|
|
|
async def _persist_compaction_boundary(
|
|
self,
|
|
task: Task | None,
|
|
boundary_record: dict[str, Any],
|
|
runtime_session_id: str,
|
|
) -> None:
|
|
if not task or not self.memory_manager or not task.session_id:
|
|
return
|
|
message = await self.memory_manager.append_session_message(
|
|
session_id=task.session_id,
|
|
role="assistant",
|
|
text=boundary_record["summary"],
|
|
project_id=task.project_id,
|
|
agent_id=task.assigned_to or None,
|
|
task_id=task.id,
|
|
summary_flag=True,
|
|
metadata={
|
|
"kind": "runtime_v2_compaction_boundary",
|
|
"runtime_session_id": runtime_session_id,
|
|
**boundary_record,
|
|
},
|
|
)
|
|
store = getattr(self.memory_manager, "store", None)
|
|
if store and hasattr(store, "save_runtime_transcript_entry"):
|
|
await store.save_runtime_transcript_entry(
|
|
runtime_session_id=runtime_session_id,
|
|
task_id=task.id,
|
|
session_id=task.session_id,
|
|
message_id=getattr(message, "message_id", "") if message else "",
|
|
role="assistant",
|
|
entry_type="compaction_boundary",
|
|
content=boundary_record["summary"],
|
|
metadata=dict(boundary_record),
|
|
)
|
|
|
|
@staticmethod
|
|
def _is_task_mode_runtime_task(task: Task | None) -> bool:
|
|
if task is None:
|
|
return False
|
|
metadata = dict(getattr(task, "metadata", {}) or {})
|
|
execution_mode = str(metadata.get("execution_mode", "") or "").strip().lower()
|
|
if execution_mode == "company_mode":
|
|
return False
|
|
if execution_mode in {"task_mode", "task", "project_mode", "project"}:
|
|
return True
|
|
mode = str(metadata.get("mode", "") or "").strip().lower()
|
|
task_mode_contract = str(metadata.get("task_mode_contract", "") or "").strip()
|
|
runtime_kind = str(metadata.get("runtime_kind", "") or "").strip()
|
|
projection_id = str(metadata.get("work_item_projection_id", "") or "").strip()
|
|
if projection_id and projection_id != "task_mode_execution":
|
|
return False
|
|
return (
|
|
mode == "task"
|
|
or task_mode_contract == "single_full_capability_main_agent"
|
|
or runtime_kind == "task_mode_agent_turn"
|
|
or projection_id == "task_mode_execution"
|
|
)
|
|
|
|
@staticmethod
|
|
def _is_company_mode_runtime_task(task: Task | None) -> bool:
|
|
if task is None:
|
|
return False
|
|
metadata = dict(getattr(task, "metadata", {}) or {})
|
|
execution_mode = str(metadata.get("execution_mode", "") or "").strip().lower()
|
|
mode = str(metadata.get("mode", "") or "").strip().lower()
|
|
task_mode_contract = str(metadata.get("task_mode_contract", "") or "").strip()
|
|
runtime_kind = str(metadata.get("runtime_kind", "") or "").strip()
|
|
projection_id = str(metadata.get("work_item_projection_id", "") or "").strip()
|
|
if (
|
|
execution_mode in {"task_mode", "task", "project_mode", "project"}
|
|
or task_mode_contract == "single_full_capability_main_agent"
|
|
or runtime_kind == "task_mode_agent_turn"
|
|
or projection_id == "task_mode_execution"
|
|
or (mode == "task" and not projection_id)
|
|
):
|
|
return False
|
|
if execution_mode == "company_mode":
|
|
return True
|
|
if str(metadata.get("execution_model", "") or "").strip() == "multi_team_org":
|
|
return True
|
|
if projection_id:
|
|
return True
|
|
if str(metadata.get("company_profile", "") or "").strip():
|
|
return True
|
|
return bool(metadata.get("work_item_runtime"))
|
|
|
|
@staticmethod
|
|
def _metadata_flag_enabled(value: Any) -> bool:
|
|
if isinstance(value, bool):
|
|
return value
|
|
if isinstance(value, str):
|
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
return False
|
|
|
|
@staticmethod
|
|
def _runtime_event_identity_payload(task: Task | None) -> dict[str, str]:
|
|
if NativeRuntimeV2._is_task_mode_runtime_task(task):
|
|
return {}
|
|
return work_item_identity_payload_for_task(task)
|
|
|
|
@staticmethod
|
|
def _permission_group_key(tool_name: str, arguments: dict[str, Any]) -> str:
|
|
normalized_tool = str(tool_name or "").strip().casefold()
|
|
if normalized_tool != "shell_exec":
|
|
return ""
|
|
command = str(
|
|
arguments.get("command")
|
|
or arguments.get("cmd")
|
|
or ""
|
|
).strip()
|
|
command_family = ""
|
|
if re.match(r"^(?:python|python3)\b", command, re.IGNORECASE):
|
|
command_family = "python"
|
|
elif re.match(r"^node\b", command, re.IGNORECASE):
|
|
command_family = "node"
|
|
if not command_family:
|
|
return ""
|
|
domains = sorted({
|
|
match.group(1).casefold()
|
|
for match in re.finditer(r"https?://([^/\s'\"<>]+)", command)
|
|
})
|
|
domain_key = ",".join(domains) if domains else "no-domain"
|
|
return f"tool:shell_exec/{command_family}:domain:{domain_key}"
|
|
|
|
async def _emit_runtime_event(
|
|
self,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
event_type: str,
|
|
payload: dict[str, Any],
|
|
) -> None:
|
|
task_metadata = dict(getattr(task, "metadata", {}) or {}) if task is not None else {}
|
|
execution_mode = str(task_metadata.get("execution_mode", "") or "").strip()
|
|
if not execution_mode and self._is_task_mode_runtime_task(task):
|
|
execution_mode = "task_mode"
|
|
event_payload = {
|
|
"type": event_type,
|
|
"runtime_session_id": runtime_session_id,
|
|
"task_id": task.id if task else None,
|
|
"session_id": task.session_id if task else None,
|
|
"agent_id": task.assigned_to if task else None,
|
|
"role_id": task.assigned_to if task else None,
|
|
"execution_mode": execution_mode,
|
|
**self._runtime_event_identity_payload(task),
|
|
"timestamp_ms": int(time.time() * 1000),
|
|
**payload,
|
|
}
|
|
store = getattr(self.memory_manager, "store", None)
|
|
if store and hasattr(store, "save_runtime_event"):
|
|
await store.save_runtime_event(runtime_session_id, event_type, event_payload)
|
|
if self.event_bus:
|
|
await self.event_bus.publish(OPCEvent(event_type="runtime_event", payload=event_payload))
|
|
|
|
def _primary_worktree_path(self, active_subagents: list[dict[str, Any]]) -> str:
|
|
for item in active_subagents:
|
|
path = str(item.get("worktree_path", "") or "").strip()
|
|
if path:
|
|
return path
|
|
return ""
|
|
|
|
async def _save_runtime_session(
|
|
self,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
status: str,
|
|
metadata: dict[str, Any],
|
|
) -> None:
|
|
if task is not None:
|
|
runtime_meta = dict(task.metadata.get("runtime_v2", {}) or {})
|
|
runtime_meta.update({
|
|
"runtime_session_id": runtime_session_id,
|
|
"status": status,
|
|
**dict(metadata or {}),
|
|
})
|
|
task.metadata["runtime_v2"] = runtime_meta
|
|
store = getattr(self.memory_manager, "store", None)
|
|
if store and hasattr(store, "save_runtime_session"):
|
|
await store.save_runtime_session(
|
|
runtime_session_id=runtime_session_id,
|
|
task_id=task.id if task else None,
|
|
session_id=task.session_id if task else None,
|
|
project_id=task.project_id if task else "default",
|
|
status=status,
|
|
metadata=metadata,
|
|
)
|
|
|
|
async def _emit_prompt_prefix_state(
|
|
self,
|
|
*,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
messages: list[dict[str, Any]],
|
|
tool_schemas: list[dict[str, Any]] | None,
|
|
base_prefix_len: int,
|
|
) -> None:
|
|
config = self.config.system.native_runtime.prompt_prefix_stability
|
|
if not config.enabled or not config.emit_cache_fingerprint_events:
|
|
return
|
|
prefix_messages = messages[:base_prefix_len]
|
|
if hasattr(self.llm, "build_cache_fingerprint"):
|
|
fingerprint = self.llm.build_cache_fingerprint(messages=prefix_messages, tools=tool_schemas)
|
|
else:
|
|
fingerprint = hashlib.sha256(
|
|
json.dumps(
|
|
{"messages": prefix_messages, "tools": tool_schemas or []},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
default=str,
|
|
).encode("utf-8")
|
|
).hexdigest()
|
|
token_count = self._safe_count_input_tokens(prefix_messages, tool_schemas)
|
|
if task is not None:
|
|
runtime_meta = dict(task.metadata.get("runtime_v2", {}) or {})
|
|
runtime_meta["prompt_prefix_fingerprint"] = fingerprint
|
|
runtime_meta["prompt_prefix_tokens"] = token_count
|
|
task.metadata["runtime_v2"] = runtime_meta
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"prompt_prefix_state",
|
|
{
|
|
"fingerprint": fingerprint,
|
|
"token_count": token_count,
|
|
"base_prefix_len": base_prefix_len,
|
|
"message_count": len(prefix_messages),
|
|
},
|
|
)
|
|
|
|
async def _emit_context_usage(
|
|
self,
|
|
*,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
messages: list[dict[str, Any]],
|
|
tool_schemas: list[dict[str, Any]] | None,
|
|
phase: str,
|
|
) -> dict[str, Any]:
|
|
config = self.config.system.native_runtime.context_usage_reporting
|
|
payload = self._context_usage_payload(messages, tool_schemas)
|
|
if not config.enabled or not config.emit_runtime_events:
|
|
return payload
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"context_usage",
|
|
{
|
|
"phase": phase,
|
|
**payload,
|
|
"message_count": len(messages),
|
|
},
|
|
)
|
|
warn_remaining_pct = int(self.config.system.native_runtime.context_guard.warn_remaining_pct or 0)
|
|
if (
|
|
payload.get("context_window", 0)
|
|
and warn_remaining_pct > 0
|
|
and int(payload.get("context_remaining_pct", 100) or 100) <= warn_remaining_pct
|
|
):
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"context_warning",
|
|
{
|
|
"phase": phase,
|
|
**payload,
|
|
"threshold_pct": warn_remaining_pct,
|
|
},
|
|
)
|
|
return payload
|
|
|
|
def _update_runtime_notes(
|
|
self,
|
|
runtime_notes: dict[str, Any],
|
|
execution_results: list[dict[str, Any]],
|
|
) -> None:
|
|
mutating_tools = set(runtime_notes.get("mutating_tools", []) or [])
|
|
risky_tools = set(runtime_notes.get("observed_risky_tools", []) or [])
|
|
permission_details = list(runtime_notes.get("permission_details", []) or [])
|
|
for item in execution_results:
|
|
call = dict(item.get("tool_call", {}) or {})
|
|
decision = item.get("permission_decision")
|
|
tool_name = str(call.get("function", "") or "")
|
|
if tool_name in {
|
|
"file_write",
|
|
"file_edit",
|
|
"apply_patch",
|
|
"shell_exec",
|
|
"python_exec",
|
|
"git_commit",
|
|
}:
|
|
mutating_tools.add(tool_name)
|
|
if decision is not None:
|
|
risk_level = getattr(getattr(decision, "risk_level", None), "value", str(getattr(decision, "risk_level", "")))
|
|
resolution = getattr(getattr(decision, "resolution", None), "value", str(getattr(decision, "resolution", "")))
|
|
detail = {
|
|
"tool_name": tool_name,
|
|
"resolution": resolution,
|
|
"risk_level": risk_level,
|
|
"source": str(getattr(decision, "source", "") or ""),
|
|
"rationale": str(getattr(decision, "rationale", "") or ""),
|
|
}
|
|
permission_details.append(detail)
|
|
if risk_level in {"high", "critical"} or resolution in {"ask", "deny"}:
|
|
risky_tools.add(tool_name)
|
|
runtime_notes["mutating_tools"] = sorted(mutating_tools)
|
|
runtime_notes["observed_risky_tools"] = sorted(risky_tools)
|
|
runtime_notes["permission_details"] = permission_details[-20:]
|
|
|
|
def _verification_required(
|
|
self,
|
|
*,
|
|
task: Task | None,
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
) -> bool:
|
|
policy = self.config.system.native_runtime.verification_policy
|
|
if not policy.enabled or task is None:
|
|
return False
|
|
if task.metadata.get(policy.skip_metadata_key):
|
|
return False
|
|
verification_state = dict(runtime_notes.get("verification", {}) or {})
|
|
if verification_state.get("completed"):
|
|
return False
|
|
if self._is_task_mode_runtime_task(task):
|
|
return self._metadata_flag_enabled(task.metadata.get("explicit_verification_requested"))
|
|
explicit_requirement = task.metadata.get("work_item_verification_required")
|
|
if isinstance(explicit_requirement, bool):
|
|
return explicit_requirement
|
|
if len(todo_state or []) >= max(1, policy.min_todos_for_verification):
|
|
return True
|
|
if policy.require_on_code_edits and runtime_notes.get("mutating_tools"):
|
|
return True
|
|
if policy.require_on_risky_tools and runtime_notes.get("observed_risky_tools"):
|
|
return True
|
|
return False
|
|
|
|
def _verification_block_would_deadlock(self, task: Task | None) -> bool:
|
|
"""Whether parking ``task`` on AWAITING_HUMAN after a failed
|
|
verification would deadlock the company workflow.
|
|
|
|
A failed verification gate normally parks the turn on AWAITING_HUMAN so
|
|
a human can intervene. That is correct only for user-facing company
|
|
cards (chiefly the final delivery card routed to a human reviewer): they
|
|
surface an approval card in the UI. Non-user-visible turns — worker
|
|
execute, the hidden worker report/handoff card, internal review cards —
|
|
have no UI surface, so blocking on a human is a guaranteed deadlock: the
|
|
hidden card stalls, the manager-review work item never spawns, and the
|
|
parent stays ``waiting_for_children`` with its claim unreleased. For
|
|
those, the turn should complete (DONE) and flow into the normal
|
|
manager-review gate, which is the real quality check for worker output.
|
|
|
|
Conservative by design: returns True only for cards that demonstrably
|
|
cannot surface a human approval card. Anything user-visible keeps the
|
|
existing AWAITING_HUMAN behavior unchanged.
|
|
"""
|
|
if task is None:
|
|
return False
|
|
meta = dict(getattr(task, "metadata", {}) or {})
|
|
if meta.get("user_visible") is False:
|
|
return True
|
|
if meta.get("report_execution_work_item") or meta.get("review_execution_work_item"):
|
|
return True
|
|
if meta.get("hidden_from_company_kanban"):
|
|
return True
|
|
return False
|
|
|
|
async def _run_verification_gate(
|
|
self,
|
|
*,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
subagents: SubagentManager,
|
|
messages: list[dict[str, Any]],
|
|
todo_state: list[dict[str, Any]],
|
|
runtime_notes: dict[str, Any],
|
|
) -> TaskResult | None:
|
|
if not self._verification_required(task=task, todo_state=todo_state, runtime_notes=runtime_notes):
|
|
return None
|
|
policy = self.config.system.native_runtime.verification_policy
|
|
summary = self._build_verification_prompt(task)
|
|
if task is not None and str(task.metadata.get("execution_mode", "") or "").strip() == "company_mode":
|
|
work_item_name = str(task.title or "").strip()
|
|
turn_type = turn_type_for_task(task, fallback="execute")
|
|
summary = self._build_verification_prompt(
|
|
task,
|
|
work_item_name=work_item_name,
|
|
turn_type=turn_type,
|
|
)
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"verification_started",
|
|
{
|
|
"profile": policy.verifier_profile,
|
|
"mutating_tools": list(runtime_notes.get("mutating_tools", []) or []),
|
|
"risky_tools": list(runtime_notes.get("observed_risky_tools", []) or []),
|
|
"todo_count": len(todo_state or []),
|
|
},
|
|
)
|
|
verification_result = await subagents.spawn(
|
|
profile=policy.verifier_profile,
|
|
prompt=summary,
|
|
description="Runtime verification pass",
|
|
name="verifier",
|
|
background=False,
|
|
isolation="worktree",
|
|
fork_context_messages=list(messages),
|
|
fork_system_prompt=str(messages[0].get("content", "")) if messages else "",
|
|
fork_mode=True,
|
|
)
|
|
verdict_text = str(
|
|
verification_result.get("result", "")
|
|
or verification_result.get("error", "")
|
|
or ""
|
|
).strip()
|
|
verification_evidence = self._parse_verification_evidence(verdict_text)
|
|
spawn_success = bool(verification_result.get("success", False))
|
|
repair_attempted = False
|
|
if spawn_success and verdict_text and verification_evidence.status != "provided":
|
|
repair_attempted = True
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"verification_repair_requested",
|
|
{
|
|
"profile": policy.verifier_profile,
|
|
"reason": "missing_structured_evidence",
|
|
},
|
|
)
|
|
repair_result = await subagents.spawn(
|
|
profile=policy.verifier_profile,
|
|
prompt=self._build_verification_retry_prompt(task, previous_output=verdict_text),
|
|
description="Runtime verification evidence repair",
|
|
name="verifier_repair",
|
|
background=False,
|
|
isolation="worktree",
|
|
fork_context_messages=list(messages),
|
|
fork_system_prompt=str(messages[0].get("content", "")) if messages else "",
|
|
fork_mode=True,
|
|
)
|
|
repaired_text = str(
|
|
repair_result.get("result", "")
|
|
or repair_result.get("error", "")
|
|
or ""
|
|
).strip()
|
|
if repaired_text:
|
|
verdict_text = repaired_text
|
|
verification_evidence = self._parse_verification_evidence(verdict_text)
|
|
spawn_success = bool(repair_result.get("success", False))
|
|
if verification_evidence.status != "provided":
|
|
unavailable_reason = verdict_text or "Verifier did not provide structured evidence."
|
|
if not spawn_success:
|
|
unavailable_reason = (
|
|
"Verification unavailable: verifier did not complete successfully. "
|
|
+ unavailable_reason
|
|
).strip()
|
|
elif repair_attempted:
|
|
unavailable_reason = (
|
|
"Verification unavailable: verifier still did not provide structured evidence after repair. "
|
|
+ unavailable_reason
|
|
).strip()
|
|
else:
|
|
unavailable_reason = (
|
|
"Verification unavailable: verifier did not provide structured evidence. "
|
|
+ unavailable_reason
|
|
).strip()
|
|
verification_evidence = self._verification_unavailable_evidence(
|
|
summary=unavailable_reason,
|
|
raw_output=verdict_text,
|
|
)
|
|
verdict_text = unavailable_reason
|
|
passed = (
|
|
spawn_success
|
|
and verdict_text
|
|
and verification_evidence.status == "provided"
|
|
and verification_evidence.verdict == "pass"
|
|
)
|
|
verification_state = {
|
|
"completed": True,
|
|
"passed": passed,
|
|
"profile": policy.verifier_profile,
|
|
"verdict": verdict_text,
|
|
"spawn_success": spawn_success,
|
|
"evidence": verification_evidence.__dict__,
|
|
"repair_attempted": repair_attempted,
|
|
}
|
|
runtime_notes["verification"] = verification_state
|
|
if task is not None:
|
|
runtime_meta = dict(task.metadata.get("runtime_v2", {}) or {})
|
|
runtime_meta["verification"] = verification_state
|
|
task.metadata["runtime_v2"] = runtime_meta
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"verification_completed",
|
|
verification_state,
|
|
)
|
|
if passed:
|
|
return None
|
|
if self._is_task_mode_runtime_task(task):
|
|
return None
|
|
if self._verification_block_would_deadlock(task):
|
|
# The task's company card is not user-visible (worker execute,
|
|
# hidden report/handoff, internal review). Parking it on
|
|
# AWAITING_HUMAN can never surface an approval card, so the whole
|
|
# company run deadlocks: the hidden report turn stalls → the manager
|
|
# review work item never spawns → the parent stays
|
|
# waiting_for_children with its claim unreleased. Complete the turn
|
|
# instead and let the company manager-review gate be the quality
|
|
# check. The failed verdict is already persisted in runtime_v2
|
|
# metadata and emitted as verification_completed for audit.
|
|
logger.warning(
|
|
"Native verification failed on a non-user-visible company turn; "
|
|
"completing instead of parking on AWAITING_HUMAN to avoid a "
|
|
"hidden-card deadlock. task_id=%s work_kind=%s",
|
|
getattr(task, "id", ""),
|
|
(dict(getattr(task, "metadata", {}) or {})).get("work_kind"),
|
|
)
|
|
return None
|
|
active_subagents = subagents.list_agents().get("agents", [])
|
|
failure_reason = verdict_text or "Verification found blocking issues."
|
|
if verification_evidence.status == "unavailable":
|
|
return None
|
|
return TaskResult(
|
|
status=TaskStatus.AWAITING_HUMAN,
|
|
content=failure_reason,
|
|
artifacts={
|
|
"runtime_session_id": runtime_session_id,
|
|
"verification": verification_state,
|
|
"verification_evidence": verification_evidence.__dict__,
|
|
"permission_requests": list(runtime_notes.get("permission_details", []) or []),
|
|
"task_ledger": list(todo_state or []),
|
|
"prefetch_hits": list(runtime_notes.get("prefetch_hits", []) or []),
|
|
"compaction_records": [],
|
|
"active_subagents": active_subagents,
|
|
"artifact_manifest": list(self._compose_runtime_artifact_manifest(
|
|
task=task,
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=[],
|
|
active_subagents=active_subagents,
|
|
)),
|
|
"resume_state": self._build_resume_state(
|
|
messages=messages,
|
|
todo_state=todo_state,
|
|
runtime_notes=runtime_notes,
|
|
compaction_boundaries=[],
|
|
active_subagents=active_subagents,
|
|
),
|
|
},
|
|
)
|
|
|
|
async def _maybe_update_background_session_memory(
|
|
self,
|
|
*,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
messages: list[dict[str, Any]],
|
|
) -> None:
|
|
config = self.config.system.native_runtime.background_session_memory
|
|
if not config.enabled or not task or not self.memory_manager or not task.session_id:
|
|
return
|
|
updater = getattr(self.memory_manager, "update_runtime_session_memory", None)
|
|
if not callable(updater):
|
|
return
|
|
try:
|
|
result = await updater(
|
|
session_id=task.session_id,
|
|
project_id=task.project_id,
|
|
llm=self.llm,
|
|
messages=messages,
|
|
update_interval_messages=config.update_interval_messages,
|
|
max_input_chars=config.max_input_chars,
|
|
)
|
|
except Exception as exc: # pragma: no cover - defensive
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"session_memory_update_failed",
|
|
{"message": str(exc)},
|
|
)
|
|
return
|
|
if result:
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"session_memory_updated",
|
|
dict(result),
|
|
)
|
|
|
|
def _safe_count_input_tokens(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
tool_schemas: list[dict[str, Any]] | None,
|
|
) -> int:
|
|
counter = getattr(self.llm, "count_input_tokens", None)
|
|
if not callable(counter):
|
|
return 0
|
|
try:
|
|
return int(counter(messages, tools=tool_schemas) or 0)
|
|
except TypeError:
|
|
try:
|
|
return int(counter(messages) or 0)
|
|
except Exception:
|
|
return 0
|
|
except Exception:
|
|
return 0
|
|
|
|
def _context_window_limit(self) -> int:
|
|
getter = getattr(self.llm, "get_context_window", None)
|
|
if not callable(getter):
|
|
return 0
|
|
try:
|
|
return int(getter() or 0)
|
|
except TypeError:
|
|
try:
|
|
return int(getter(task_type=None) or 0)
|
|
except Exception:
|
|
return 0
|
|
except Exception:
|
|
return 0
|
|
|
|
def _context_usage_payload(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
tool_schemas: list[dict[str, Any]] | None,
|
|
*,
|
|
observed_tokens: int = 0,
|
|
) -> dict[str, Any]:
|
|
# Anchor on the provider-reported prompt size of the latest request
|
|
# when it exceeds the local estimate: the context only grows within a
|
|
# turn, so max() protects against estimator undercounting.
|
|
token_count = max(
|
|
self._safe_count_input_tokens(messages, tool_schemas),
|
|
int(observed_tokens or 0),
|
|
)
|
|
context_window = self._context_window_limit()
|
|
remaining_tokens = max(0, context_window - token_count) if context_window > 0 else 0
|
|
remaining_pct = int((remaining_tokens / context_window) * 100) if context_window > 0 else 0
|
|
usage_ratio = (token_count / context_window) if context_window > 0 else 0.0
|
|
return {
|
|
"token_count": token_count,
|
|
"context_tokens": token_count,
|
|
"context_window": context_window,
|
|
"context_remaining_tokens": remaining_tokens,
|
|
"context_remaining_pct": remaining_pct,
|
|
"usage_ratio": round(usage_ratio, 4),
|
|
"hard_threshold": float(self.config.system.native_runtime.context_guard.hard_threshold or 0.90),
|
|
}
|
|
|
|
def _should_apply_hard_compaction(
|
|
self,
|
|
messages: list[dict[str, Any]],
|
|
tool_schemas: list[dict[str, Any]] | None,
|
|
*,
|
|
observed_tokens: int = 0,
|
|
) -> bool:
|
|
config = self.config.system.native_runtime.context_guard
|
|
if not config.enabled:
|
|
return False
|
|
payload = self._context_usage_payload(messages, tool_schemas, observed_tokens=observed_tokens)
|
|
if payload["context_window"] <= 0:
|
|
return False
|
|
return float(payload["usage_ratio"]) >= float(config.hard_threshold or 0.90)
|
|
|
|
def _clip_tool_result_for_history(
|
|
self,
|
|
tool_name: str,
|
|
result: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
clipped = json.loads(json.dumps(result, ensure_ascii=False, default=str))
|
|
payload = clipped.get("result")
|
|
config = self.config.system.native_runtime.context_guard
|
|
generic_budget = int(config.tool_output_char_budget or 12_000)
|
|
shell_stdout_budget = int(config.shell_stdout_char_budget or 12_000)
|
|
shell_stderr_budget = int(config.shell_stderr_char_budget or 6_000)
|
|
if not isinstance(payload, dict):
|
|
return clipped
|
|
if tool_name in {"shell_exec", "python_exec"}:
|
|
if payload.get("stdout"):
|
|
clipped_stdout = clip_text(
|
|
str(payload.get("stdout", "") or ""),
|
|
limit=shell_stdout_budget,
|
|
marker="stdout truncated by context_guard",
|
|
)
|
|
payload["stdout"] = clipped_stdout.text
|
|
if clipped_stdout.truncated:
|
|
payload["stdout_truncated"] = True
|
|
payload["stdout_omitted_chars"] = clipped_stdout.omitted_chars
|
|
if payload.get("stderr"):
|
|
clipped_stderr = clip_text(
|
|
str(payload.get("stderr", "") or ""),
|
|
limit=shell_stderr_budget,
|
|
marker="stderr truncated by context_guard",
|
|
)
|
|
payload["stderr"] = clipped_stderr.text
|
|
if clipped_stderr.truncated:
|
|
payload["stderr_truncated"] = True
|
|
payload["stderr_omitted_chars"] = clipped_stderr.omitted_chars
|
|
for key in ("content", "rendered", "summary", "diff_preview"):
|
|
if payload.get(key):
|
|
clipped_value = clip_text(
|
|
str(payload.get(key, "") or ""),
|
|
limit=generic_budget,
|
|
marker="tool output truncated by context_guard",
|
|
)
|
|
payload[key] = clipped_value.text
|
|
if clipped_value.truncated:
|
|
payload[f"{key}_truncated"] = True
|
|
payload[f"{key}_omitted_chars"] = clipped_value.omitted_chars
|
|
clipped["result"] = payload
|
|
return clipped
|
|
|
|
async def _emit_status_snapshot(
|
|
self,
|
|
runtime_session_id: str,
|
|
task: Task | None,
|
|
snapshot: dict[str, Any],
|
|
) -> None:
|
|
await self._emit_runtime_event(runtime_session_id, task, "status_snapshot", dict(snapshot))
|
|
|
|
async def _extract_durable_memory(
|
|
self,
|
|
*,
|
|
task: Task | None,
|
|
user_message: str,
|
|
assistant_text: str,
|
|
runtime_session_id: str,
|
|
) -> dict[str, Any]:
|
|
if not task or not self.memory_manager or not task.session_id:
|
|
return {}
|
|
if not self.config.system.native_runtime.auto_extract_durable_memory:
|
|
return {}
|
|
extractor = getattr(self.memory_manager, "extract_durable_memories", None)
|
|
if not callable(extractor):
|
|
return {}
|
|
try:
|
|
result = await extractor(
|
|
session_id=task.session_id,
|
|
project_id=task.project_id,
|
|
query=user_message,
|
|
assistant_response=assistant_text,
|
|
llm=self.llm,
|
|
min_messages=self.config.system.native_runtime.durable_memory_extract_min_messages,
|
|
max_input_chars=self.config.system.native_runtime.durable_memory_max_input_chars,
|
|
)
|
|
except Exception as exc: # pragma: no cover - defensive
|
|
logger.debug(f"Durable memory extraction failed: {exc}")
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"durable_memory_extraction_failed",
|
|
{"message": str(exc)},
|
|
)
|
|
return {}
|
|
if result:
|
|
await self._emit_runtime_event(
|
|
runtime_session_id,
|
|
task,
|
|
"durable_memory_extracted",
|
|
dict(result),
|
|
)
|
|
return {"durable_memory_extraction": result} if result else {}
|
|
|
|
def _merge_artifacts(self, current: dict[str, Any], incoming: dict[str, Any]) -> dict[str, Any]:
|
|
merged = dict(current)
|
|
for key, value in incoming.items():
|
|
if isinstance(value, list) and isinstance(merged.get(key), list):
|
|
merged[key] = [*merged[key], *value]
|
|
elif isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
merged[key] = {**merged[key], **value}
|
|
else:
|
|
merged[key] = value
|
|
return merged
|
|
|
|
def _format_interrupt_notice(self, inbox_messages: list[dict[str, Any]]) -> str:
|
|
lines = ["New runtime inbox messages arrived while you were working:"]
|
|
for item in inbox_messages[:6]:
|
|
message_id = str(item.get("msg_id", "") or "").strip()
|
|
from_agent = str(item.get("from_agent", "") or item.get("from", "") or "").strip()
|
|
subject = str(item.get("subject", "") or "").strip()
|
|
message_class = str(item.get("message_class", "") or dict(item.get("metadata", {}) or {}).get("message_class", "")).strip()
|
|
protocol_type = str(item.get("protocol_type", "") or dict(item.get("metadata", {}) or {}).get("protocol_type", "")).strip()
|
|
summary = " | ".join(
|
|
[
|
|
part
|
|
for part in [
|
|
f"message_id={message_id}" if message_id else "",
|
|
f"from={from_agent}" if from_agent else "",
|
|
f"subject={subject}" if subject else "",
|
|
f"class={message_class}" if message_class else "",
|
|
f"protocol={protocol_type}" if protocol_type else "",
|
|
]
|
|
if part
|
|
]
|
|
)
|
|
body = str(item.get("body", "") or "").strip()
|
|
lines.append(f"- {summary or str(item)}")
|
|
if body:
|
|
lines.append(f" body={body}")
|
|
return "\n".join(lines)
|
|
|
|
def _render_todos(self, todo_state: list[dict[str, Any]]) -> str:
|
|
if not todo_state:
|
|
return "(no TODO items)"
|
|
status_icons = {
|
|
"pending": "[ ]",
|
|
"in_progress": "[>]",
|
|
"completed": "[x]",
|
|
"done": "[x]",
|
|
}
|
|
return "\n".join(
|
|
f"{status_icons.get(str(item.get('status', 'pending')), '[ ]')} "
|
|
f"{str(item.get('title') or item.get('content') or item.get('id', 'todo'))}"
|
|
for item in todo_state
|
|
)
|
|
|
|
def _normalize_todos(self, todos_payload: Any) -> list[dict[str, Any]]:
|
|
if not isinstance(todos_payload, list):
|
|
return []
|
|
normalized: list[dict[str, Any]] = []
|
|
has_active = False
|
|
for index, raw in enumerate(todos_payload, start=1):
|
|
if not isinstance(raw, dict):
|
|
continue
|
|
status = self._normalize_todo_status(raw.get("status"))
|
|
if status == "in_progress":
|
|
if has_active:
|
|
status = "pending"
|
|
else:
|
|
has_active = True
|
|
title = (
|
|
str(raw.get("title") or "").strip()
|
|
or str(raw.get("content") or "").strip()
|
|
or str(raw.get("active_form") or raw.get("activeForm") or "").strip()
|
|
or f"todo-{index}"
|
|
)
|
|
active_form = str(raw.get("active_form") or raw.get("activeForm") or title).strip() or title
|
|
content = str(raw.get("content") or title).strip() or title
|
|
normalized.append(
|
|
{
|
|
"id": str(raw.get("id") or index),
|
|
"title": title,
|
|
"content": content,
|
|
"active_form": active_form,
|
|
"status": status,
|
|
}
|
|
)
|
|
return normalized
|
|
|
|
@staticmethod
|
|
def _normalize_spawn_mode(value: Any) -> str:
|
|
mode = str(value or "default").strip() or "default"
|
|
legacy_key = mode.replace("_", "").lower()
|
|
return {
|
|
"acceptedits": "accept_edits",
|
|
"bypasspermissions": "bypass_permissions",
|
|
"dontask": "dont_ask",
|
|
}.get(legacy_key, mode)
|
|
|
|
@staticmethod
|
|
def _normalize_todo_status(value: Any) -> str:
|
|
status = str(value or "pending").strip().lower()
|
|
if status in {"done", "completed", "complete", "finished"}:
|
|
return "completed"
|
|
if status in {"in_progress", "in-progress", "active", "current"}:
|
|
return "in_progress"
|
|
return "pending"
|
|
|
|
async def _emit_progress(self, callback: Any, text: str, task: Task | None) -> None:
|
|
try:
|
|
await callback(text, task_id=task.id if task else None)
|
|
except TypeError:
|
|
await callback(text)
|
|
|
|
def _permission_requests_from_results(self, execution_results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
requests: list[dict[str, Any]] = []
|
|
for item in execution_results:
|
|
decision = item.get("permission_decision")
|
|
call = item.get("tool_call", {})
|
|
if decision is None:
|
|
continue
|
|
resolution = getattr(decision, "resolution", None)
|
|
resolution_value = getattr(resolution, "value", str(resolution))
|
|
if resolution_value not in {"ask", "deny"}:
|
|
continue
|
|
requests.append({
|
|
"tool_name": str(call.get("function", "") or ""),
|
|
"resolution": resolution_value,
|
|
"scope": getattr(getattr(decision, "scope", None), "value", str(getattr(decision, "scope", ""))),
|
|
"risk_level": getattr(getattr(decision, "risk_level", None), "value", str(getattr(decision, "risk_level", ""))),
|
|
"rationale": str(getattr(decision, "rationale", "") or ""),
|
|
"source": str(getattr(decision, "source", "") or ""),
|
|
})
|
|
return requests
|