from __future__ import annotations import json import os import re import time import uuid import copy import threading from dataclasses import dataclass from types import SimpleNamespace from typing import Any, Callable, Optional from oclaw.runtime.chat.agent_messages import build_llm_messages from oclaw.runtime.chat.tool_runtime import ToolExecutionConfig from oclaw.runtime.chat.turn_types import TurnRunOutcome from oclaw.runtime.skill_executor import SkillExecutionContext, SkillExecutor from oclaw.runtime.skills import build_skill_manifest from oclaw.platform.llm.chat_models import ChatModel from oclaw.runtime.system_prompt import build_oclaw_executor_system_prompt from oclaw.runtime.types import OclawMemoryContext from oclaw.runtime.orchestration.trace import new_span_id from oclaw.runtime.tools.base import ToolRegistry from oclaw.runtime.hooks_runtime import trigger_hook_event _OCLAW_TOOL_RESULT_HARD_CAP_CHARS = 24_000 _DIRECT_LOOP_OC_STAGE: dict[str, str] = { "tool_wire_filter": "wire_filter", "tool_result_context_guard": "tool_context_guard", } _THINK_BLOCK_RE = re.compile(r"<(think|redacted_thinking)>\s*(.*?)\s*\s*", flags=re.IGNORECASE | re.DOTALL) _TOOL_WIRE_CACHE_LOCK = threading.Lock() _TOOL_WIRE_CACHE: dict[str, tuple[float, list[dict[str, Any]]]] = {} _TOOL_WIRE_CACHE_TTL_SEC = 300.0 _TOOL_WIRE_FROZEN_SIGNATURE: str | None = None _TOOL_WIRE_LAST_WARM_TS_MS: int = 0 _TOOL_WIRE_LAST_WARM_ROLES: tuple[str, ...] = () _TOOL_WIRE_LAST_WARM_COUNT: int = 0 def _tool_wire_freeze_enabled(store: Any) -> bool: raw = "" try: raw = str(store.get_setting("AIA_TOOL_WIRE_FROZEN_ON_STARTUP") or "").strip().lower() except Exception: raw = "" if not raw: raw = str(os.getenv("AIA_TOOL_WIRE_FROZEN_ON_STARTUP") or "").strip().lower() if not raw: return True return raw in {"1", "true", "yes", "on"} def _tool_wire_settings_signature(store: Any) -> tuple[bool, str]: runtime_enabled = True try: raw_flag = str(store.get_setting("AIA_SKILL_RUNTIME_ENABLED") or "").strip().lower() if raw_flag: runtime_enabled = raw_flag in {"1", "true", "yes", "on"} except Exception: runtime_enabled = True sig = "|".join( [ f"rt={int(bool(runtime_enabled))}", f"mcp={str(store.get_setting('AIA_ENABLE_MCP_TOOLS') or '')}", f"plugin={str(store.get_setting('AIA_ENABLE_PLUGIN_TOOLS') or '')}", f"skill_rt={str(store.get_setting('AIA_SKILL_RUNTIME_ENABLED') or '')}", f"skill_disabled={str(store.get_setting('AIA_SKILL_DISABLED_NAMES') or '')}", f"bind_en={str(store.get_setting('AIA_SKILL_ROLE_BINDING_ENABLED') or '')}", f"bind_inherit={str(store.get_setting('AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT') or '')}", ] ) return runtime_enabled, sig def _tool_wire_cache_key( *, store: Any, base_url: str, wire_policy_role: str | None, runtime_enabled: bool, settings_sig: str | None = None, ) -> str: _, sig = _tool_wire_settings_signature(store) effective_sig = str(settings_sig or sig) return ( f"base={base_url}|role={str(wire_policy_role or '').strip().lower()}|" f"rt={int(bool(runtime_enabled))}|{effective_sig}" ) def warm_tool_wire_cache( *, store: Any, tools: ToolRegistry, base_url: str, roles: list[str] | tuple[str, ...], ) -> dict[str, int]: global _TOOL_WIRE_FROZEN_SIGNATURE, _TOOL_WIRE_LAST_WARM_TS_MS, _TOOL_WIRE_LAST_WARM_ROLES, _TOOL_WIRE_LAST_WARM_COUNT freeze_enabled = _tool_wire_freeze_enabled(store) runtime_enabled, sig = _tool_wire_settings_signature(store) warmed = 0 for role in roles or []: _ = _prepare_llm_tools( store=store, tools=tools, base_url=base_url, session_id="startup-prewarm", trace_id=None, parent_span_id=None, run_id="startup-prewarm", attempt_no=0, lang="", wire_policy_role=str(role or "").strip().lower() or None, ) warmed += 1 with _TOOL_WIRE_CACHE_LOCK: _TOOL_WIRE_FROZEN_SIGNATURE = f"rt={int(bool(runtime_enabled))}|{sig}" if freeze_enabled else None _TOOL_WIRE_LAST_WARM_TS_MS = int(time.time() * 1000) _TOOL_WIRE_LAST_WARM_ROLES = tuple(str(x or "").strip().lower() for x in roles or []) _TOOL_WIRE_LAST_WARM_COUNT = int(warmed) return {"roles_warmed": int(warmed), "frozen": int(bool(freeze_enabled))} def tool_wire_freeze_status(*, store: Any | None = None) -> dict[str, Any]: enabled = True if store is not None: enabled = _tool_wire_freeze_enabled(store) with _TOOL_WIRE_CACHE_LOCK: return { "enabled": bool(enabled), "frozen": bool(isinstance(_TOOL_WIRE_FROZEN_SIGNATURE, str) and _TOOL_WIRE_FROZEN_SIGNATURE.strip()), "frozen_signature": str(_TOOL_WIRE_FROZEN_SIGNATURE or ""), "last_warm_ts_ms": int(_TOOL_WIRE_LAST_WARM_TS_MS), "last_warm_roles": list(_TOOL_WIRE_LAST_WARM_ROLES), "last_warm_count": int(_TOOL_WIRE_LAST_WARM_COUNT), "cache_entries": int(len(_TOOL_WIRE_CACHE)), } def _emit_direct_loop_trace( *, store: Any, session_id: str, trace_id: str | None, parent_span_id: str | None, event_type: str, payload: dict[str, Any], run_id: str | None, attempt_no: int | None, lang: str, ) -> None: if not trace_id: return merged: dict[str, Any] = dict(payload or {}) merged.setdefault("pipeline", "oclaw_direct_loop") merged.setdefault("trace_id", str(trace_id)) merged.setdefault("lang", str(lang or "")) merged["oc_stage"] = _DIRECT_LOOP_OC_STAGE.get(event_type, event_type) rid = str(run_id or "").strip() if rid: merged.setdefault("run_id", rid) if attempt_no is not None: merged.setdefault("attempt_no", int(attempt_no)) try: store.add_trace_event( session_id=session_id, trace_id=str(trace_id), span_id=new_span_id(), parent_span_id=parent_span_id, event_type=event_type, payload=merged, ) except Exception: pass @dataclass(frozen=True) class _LoopStepResult: assistant_text: str llm_tool_calls: list[Any] assistant_msg_id: int def _json_dumps_safe(obj: Any) -> str: try: return json.dumps(obj, ensure_ascii=False, default=str) except Exception: return json.dumps({"ok": False, "error": "not_json_serializable"}, ensure_ascii=False) def _split_reasoning_and_body(text: str, *, explicit_reasoning: str | None = None) -> tuple[list[str], str]: explicit = str(explicit_reasoning or "").strip() raw = str(text or "") if not raw: return ([explicit] if explicit else []), "" if explicit: body = _THINK_BLOCK_RE.sub("", raw).strip() return [explicit], body chunks: list[str] = [] for m in _THINK_BLOCK_RE.finditer(raw): t = str(m.group(2) or "").strip() if t: chunks.append(t) body = _THINK_BLOCK_RE.sub("", raw).strip() return chunks, body def _guard_tool_results_for_llm_context( *, store: Any, session_id: str, store_messages: list[Any], trace_id: str | None, parent_span_id: str | None, hard_cap_chars: int, run_id: str | None = None, attempt_no: int | None = None, lang: str = "", ) -> list[Any]: """Hard-guard overlarge `role=tool` message contents before sending to model. This does NOT rewrite DB history (tool_log / chat_message). It only guards the in-flight LLM context to prevent provider context overflow spirals. """ cap = max(4096, min(int(hard_cap_chars or _OCLAW_TOOL_RESULT_HARD_CAP_CHARS), 500_000)) out: list[Any] = [] for m in store_messages or []: role = str(getattr(m, "role", "") or "") if role != "tool": out.append(m) continue raw = str(getattr(m, "content", "") or "") if len(raw) <= cap: out.append(m) continue # Best-effort parse tool JSON for a minimal summary. ok = None error_code = "" error = "" try: obj = json.loads(raw) if isinstance(obj, dict): ok = obj.get("ok") error_code = str(obj.get("error_code") or "").strip() error = str(obj.get("error") or "").strip() except Exception: obj = None preview = raw[: max(1, min(4000, cap - 400))] + "\n..." guarded_obj = { "ok": bool(ok) if ok is not None else None, "error_code": error_code, "error": error, "_tool_result_guarded": True, "original_chars": len(raw), "guard_cap_chars": cap, "preview": preview, "hint": ( "Tool output was too large for safe context replay; it was truncated for the model context. " "Use narrower queries (e.g., smaller glob/max_results) or adjust AIA_TOOL_LLM_MESSAGE_MAX_CHARS. / " "工具输出过大,已在发给模型的上下文中强制截断;请缩小范围或配置 AIA_TOOL_LLM_MESSAGE_MAX_CHARS。" ), } guarded = _json_dumps_safe(guarded_obj) out.append( SimpleNamespace( id=getattr(m, "id", 0), session_id=getattr(m, "session_id", session_id), role="tool", content=guarded, tool_calls=getattr(m, "tool_calls", None), timestamp=getattr(m, "timestamp", ""), attachments=getattr(m, "attachments", None), ) ) if trace_id: _emit_direct_loop_trace( store=store, session_id=session_id, trace_id=trace_id, parent_span_id=parent_span_id, event_type="tool_result_context_guard", payload={ "message_id": int(getattr(m, "id", 0) or 0), "original_chars": int(len(raw)), "guarded_chars": int(len(guarded)), "guard_cap_chars": int(cap), }, run_id=run_id, attempt_no=attempt_no, lang=lang, ) return out def _check_stop(should_stop: Optional[Callable[[], bool]]) -> None: if should_stop and should_stop(): raise RuntimeError("generation interrupted by user") def _build_model_context( *, store: Any, session_id: str, max_messages: int, system_prompt: str, model: ChatModel, lang: str, memory_context: OclawMemoryContext | None, trace_id: str | None, parent_span_id: str | None, tools: ToolRegistry | None = None, base_url: str = "", run_id: str | None = None, attempt_no: int | None = None, workspace_dir: str | None = None, skill_binding_role: str | None = None, user_text: str = "", prompt_build_context: dict[str, Any] | None = None, ) -> list[dict[str, Any]]: rows = store.get_messages(session_id=session_id, limit=int(max_messages)) rows = _guard_tool_results_for_llm_context( store=store, session_id=session_id, store_messages=rows, trace_id=trace_id, parent_span_id=parent_span_id, hard_cap_chars=_OCLAW_TOOL_RESULT_HARD_CAP_CHARS, run_id=run_id, attempt_no=attempt_no, lang=lang, ) final_system = build_oclaw_executor_system_prompt( store=store, tools=tools, base_url=str(base_url or ""), base_system=str(system_prompt or ""), memory_context=memory_context, lang=lang, workspace_dir=workspace_dir, skill_binding_role=skill_binding_role, ) # Hook integration: wiki-auto-inject can prepend retrieval snippets # before prompt build when query/topic hints indicate supplemental lookup. try: pb_ctx = prompt_build_context if isinstance(prompt_build_context, dict) else {} user_text_final = str(user_text or "").strip() wiki_query = str(pb_ctx.get("wiki_query") or "").strip() hook_ctx = { "userText": (wiki_query or user_text_final), "prepend_system_context": "", "need_wiki_inject": pb_ctx.get("need_wiki_inject"), "memory_mode": str(pb_ctx.get("memory_mode") or ""), "wiki_query": wiki_query, } hook_out = trigger_hook_event( event_type="llm", action="before_prompt_build", session_key=str(session_id or "system"), context=hook_ctx, ) prepend = str((hook_out or {}).get("prepend_system_context") or "").strip() if prepend: final_system = f"{prepend}\n\n{final_system}".strip() except Exception: pass trunc_raw = str(store.get_setting("AIA_TOOL_CONTEXT_TRUNCATE_ENABLED") or "").strip().lower() tool_context_truncate_enabled = trunc_raw not in ("0", "false", "no", "off") return build_llm_messages( store_messages=rows, system_prompt=final_system, model=model, lang=lang, tool_context_truncate_enabled=tool_context_truncate_enabled, ) def _prepare_llm_tools( *, store: Any, tools: ToolRegistry, base_url: str, session_id: str, trace_id: str | None, parent_span_id: str | None, run_id: str | None = None, attempt_no: int | None = None, lang: str = "", wire_policy_role: str | None = None, ) -> list[dict[str, Any]]: global _TOOL_WIRE_FROZEN_SIGNATURE now = time.time() runtime_enabled, sig = _tool_wire_settings_signature(store) freeze_enabled = _tool_wire_freeze_enabled(store) frozen_sig = _TOOL_WIRE_FROZEN_SIGNATURE if freeze_enabled else None if isinstance(frozen_sig, str) and frozen_sig.strip(): # Startup-prewarmed frozen mode: execution path reuses precomputed tool wiring # and does not perform per-turn policy revalidation. sig = frozen_sig try: rt_head = str(frozen_sig).split("|", 1)[0].strip().lower() runtime_enabled = rt_head == "rt=1" except Exception: pass cache_key = _tool_wire_cache_key( store=store, base_url=base_url, wire_policy_role=wire_policy_role, runtime_enabled=runtime_enabled, settings_sig=sig, ) with _TOOL_WIRE_CACHE_LOCK: cached = _TOOL_WIRE_CACHE.get(cache_key) if cached and ( (isinstance(frozen_sig, str) and frozen_sig.strip()) or (now - float(cached[0])) <= _TOOL_WIRE_CACHE_TTL_SEC ): return copy.deepcopy(cached[1]) if runtime_enabled: skill_specs, _ = build_skill_manifest(registry=tools, store=store, base_url=base_url) raw_llm_tools = [s.as_openai_tool() for s in skill_specs] else: raw_llm_tools = tools.as_openai_tools() from oclaw.runtime.tools.exposure_plan import build_llm_tools_plan plan = build_llm_tools_plan( store=store, role=str(wire_policy_role or "").strip().lower() or "generalist", base_url=base_url or None, max_json_bytes=None, include_mcp=False, preview_internal=False, raw_openai_tools_override=raw_llm_tools, ) llm_tools = plan.tools_wired if trace_id: try: import os raw_names = { str(((t.get("function") or {}) if isinstance(t, dict) else {}).get("name") or "") for t in (raw_llm_tools or []) if isinstance(t, dict) } raw_names.discard("") hidden = list(plan.removed_names) hidden_mcp = list(plan.removed_mcp_names) _emit_direct_loop_trace( store=store, session_id=session_id, trace_id=trace_id, parent_span_id=parent_span_id, event_type="tool_wire_filter", payload={ "runner": "oclaw_direct", "base_url": base_url, "wire_policy_role": str(wire_policy_role or ""), "tools_before": len(raw_names), "tools_after": int(len(_tool_names_for_trace(llm_tools))), "hidden_total": int(len(hidden)), "hidden_mcp_total": int(len(hidden_mcp)), "hidden_mcp_preview": list(hidden_mcp)[:20], "role_mode": str(plan.role_mode or ""), "wire_policy_effective": bool(plan.wire_policy_effective), "max_json_bytes": plan.max_json_bytes, "changed_total": int(len(plan.changed_names)), }, run_id=run_id, attempt_no=attempt_no, lang=lang, ) # Optional richer snapshot for debugging (may be large). trace_plan_enabled = False try: raw = str(store.get_setting("AIA_TRACE_TOOL_EXPOSURE_PLAN") or "").strip() if raw: trace_plan_enabled = raw.lower() in {"1", "true", "yes", "on"} else: trace_plan_enabled = str(os.getenv("AIA_TRACE_TOOL_EXPOSURE_PLAN") or "").strip().lower() in { "1", "true", "yes", "on", } except Exception: trace_plan_enabled = str(os.getenv("AIA_TRACE_TOOL_EXPOSURE_PLAN") or "").strip().lower() in { "1", "true", "yes", "on", } if trace_plan_enabled: _emit_direct_loop_trace( store=store, session_id=session_id, trace_id=trace_id, parent_span_id=parent_span_id, event_type="tool_exposure_plan", payload={ "runner": "oclaw_direct", "base_url": base_url, "wire_policy_role": str(wire_policy_role or ""), "role_mode": str(plan.role_mode or ""), "wire_policy_effective": bool(plan.wire_policy_effective), "max_json_bytes": plan.max_json_bytes, "raw_names": sorted(list(raw_names))[:300], "wired_names": sorted(list(set(_tool_names_for_trace(llm_tools))))[:300], "removed_names": list(plan.removed_names)[:300], "removed_mcp_names": list(plan.removed_mcp_names)[:300], "added_names": list(plan.added_names)[:300], "changed_names": list(plan.changed_names)[:300], }, run_id=run_id, attempt_no=attempt_no, lang=lang, ) except Exception: pass with _TOOL_WIRE_CACHE_LOCK: _TOOL_WIRE_CACHE[cache_key] = (now, copy.deepcopy(llm_tools)) if len(_TOOL_WIRE_CACHE) > 256: oldest_key = sorted(_TOOL_WIRE_CACHE.items(), key=lambda kv: kv[1][0])[0][0] _TOOL_WIRE_CACHE.pop(oldest_key, None) return llm_tools def _tool_names_for_trace(tools: list[dict[str, Any]]) -> list[str]: out: list[str] = [] for t in tools or []: if not isinstance(t, dict): continue fn = t.get("function") if not isinstance(fn, dict): continue nm = str(fn.get("name") or "").strip() if nm: out.append(nm) return out def _persist_assistant_step( *, store: Any, session_id: str, turn_uuid: str, assistant_text: str, reasoning_text: str, llm_tool_calls: list[Any], ) -> _LoopStepResult: stored_tool_calls = [] for tc in llm_tool_calls: stored_tool_calls.append( { "id": str(getattr(tc, "id", "") or ""), "name": str(getattr(tc, "name", "") or ""), "arguments": dict(getattr(tc, "arguments", {}) or {}), "thought_signature": getattr(tc, "thought_signature", None), } ) reasoning_chunks, assistant_body = _split_reasoning_and_body( assistant_text, explicit_reasoning=reasoning_text, ) for idx, chunk in enumerate(reasoning_chunks): store.add_message( session_id=session_id, role="assistant", content=chunk, turn_uuid=turn_uuid, event_type="reasoning", event_payload={"chunk_index": int(idx), "chunk_count": len(reasoning_chunks)}, ) assistant_row = store.add_message( session_id=session_id, role="assistant", content=assistant_body, tool_calls=stored_tool_calls or None, turn_uuid=turn_uuid, event_type="tool_call" if stored_tool_calls else "assistant_text", ) return _LoopStepResult( assistant_text=assistant_body, llm_tool_calls=llm_tool_calls, assistant_msg_id=int(getattr(assistant_row, "id", 0) or 0), ) def _execute_tool_step( *, skill_exec: SkillExecutor, store: Any, tools: ToolRegistry, session_id: str, lang: str, user_text: str, trace_id: str | None, parent_span_id: str | None, workspace_dir: str | None, workspace_owner_session_id: str | None, path_policy_tenant_id: str | None, path_policy_user_id: str | None, assistant_msg_id: int, llm_tool_calls: list[Any], on_tool_ui: Optional[Callable[[str, dict[str, Any]], None]], should_stop: Optional[Callable[[], bool]], signature_budget: int, run_id: str | None = None, attempt_no: int | None = None, turn_uuid: str | None = None, ) -> tuple[int, dict[str, tuple[dict[str, Any], int]]]: t0 = time.perf_counter() _tool_messages, results_by_id = skill_exec.execute_skill_uses( ctx=SkillExecutionContext( store=store, tools=tools, session_id=session_id, lang=lang, user_text=user_text, specialist="oclaw", trace_id=trace_id, parent_span_id=parent_span_id, workspace_dir=workspace_dir, workspace_owner_session_id=workspace_owner_session_id, path_policy_tenant_id=path_policy_tenant_id, path_policy_user_id=path_policy_user_id, run_id=run_id, attempt_no=attempt_no, turn_uuid=turn_uuid, ), assistant_msg_id=assistant_msg_id, skill_uses=llm_tool_calls, on_tool_ui=None, on_skill_ui=on_tool_ui, should_stop=should_stop, signature_budget=signature_budget, ) return int((time.perf_counter() - t0) * 1000), results_by_id def run_oclaw_direct_loop( *, store: Any, session_id: str, lang: str, system_prompt: str, model: ChatModel, tools: ToolRegistry, user_text: str, attachments: list[dict[str, Any]] | None = None, trace_id: str | None = None, parent_span_id: str | None = None, run_id: str | None = None, attempt_no: int | None = None, max_messages: int = 80, max_tool_rounds: int = 8, max_tool_workers: int = 8, on_token: Optional[Callable[[str], None]] = None, on_progress: Optional[Callable[[str], None]] = None, on_tool_ui: Optional[Callable[[str, dict[str, Any]], None]] = None, should_stop: Optional[Callable[[], bool]] = None, workspace_owner_session_id: str | None = None, path_policy_tenant_id: str | None = None, path_policy_user_id: str | None = None, workspace_dir: str | None = None, memory_context: OclawMemoryContext | None = None, persist_user_message: bool = True, tool_signature_budget: int = 2, skill_binding_role: str | None = None, wire_policy_role: str | None = None, prompt_build_context: dict[str, Any] | None = None, ) -> TurnRunOutcome: """A minimal oclaw-style loop: model -> tool_uses -> execute -> tool_results -> continue.""" _check_stop(should_stop) turn_uuid = str(uuid.uuid4()) if persist_user_message: store.add_message( session_id=session_id, role="user", content=str(user_text or ""), attachments=attachments, turn_uuid=turn_uuid, event_type="user_text", ) skill_exec = SkillExecutor(config=ToolExecutionConfig(max_workers=max(1, min(int(max_tool_workers or 8), 32)))) tool_traces: list[dict[str, Any]] = [] final_text = "" base_url = str(getattr(model, "base_url", "") or "") for round_idx in range(max(1, int(max_tool_rounds or 1))): _check_stop(should_stop) if on_progress: on_progress(f"oclaw: think ({round_idx + 1})…") msgs = _build_model_context( store=store, session_id=session_id, max_messages=max_messages, system_prompt=system_prompt, model=model, lang=lang, memory_context=memory_context, trace_id=trace_id, parent_span_id=parent_span_id, tools=tools, base_url=base_url, run_id=run_id, attempt_no=attempt_no, workspace_dir=workspace_dir, skill_binding_role=skill_binding_role, user_text=str(user_text or ""), prompt_build_context=prompt_build_context, ) llm_tools = _prepare_llm_tools( store=store, tools=tools, base_url=base_url, session_id=session_id, trace_id=trace_id, parent_span_id=parent_span_id, run_id=run_id, attempt_no=attempt_no, lang=lang, wire_policy_role=wire_policy_role, ) resp = model.chat(msgs, llm_tools, on_token=on_token) assistant_text = str(getattr(resp, "content", "") or "") reasoning_text = str(getattr(resp, "reasoning_content", "") or "") llm_tool_calls = list(getattr(resp, "tool_calls", []) or []) step = _persist_assistant_step( store=store, session_id=session_id, turn_uuid=turn_uuid, assistant_text=assistant_text, reasoning_text=reasoning_text, llm_tool_calls=llm_tool_calls, ) final_text = step.assistant_text if not step.llm_tool_calls: break elapsed_ms, results_by_id = _execute_tool_step( skill_exec=skill_exec, store=store, tools=tools, session_id=session_id, lang=lang, user_text=str(user_text or ""), trace_id=trace_id, parent_span_id=parent_span_id, workspace_dir=workspace_dir, workspace_owner_session_id=workspace_owner_session_id, path_policy_tenant_id=path_policy_tenant_id, path_policy_user_id=path_policy_user_id, assistant_msg_id=step.assistant_msg_id, llm_tool_calls=step.llm_tool_calls, on_tool_ui=on_tool_ui, should_stop=should_stop, signature_budget=tool_signature_budget, run_id=run_id, attempt_no=attempt_no, turn_uuid=turn_uuid, ) for tc in step.llm_tool_calls: result, dur = results_by_id.get(str(getattr(tc, "id", "") or ""), ({}, 0)) tool_traces.append( { "name": str(getattr(tc, "name", "") or ""), "tool_call_id": str(getattr(tc, "id", "") or ""), "ok": bool((result or {}).get("ok")) if isinstance(result, dict) else None, "duration_ms": int(dur), "round": int(round_idx + 1), } ) if on_progress: on_progress(f"oclaw: tools done ({elapsed_ms}ms)") return TurnRunOutcome( final_text=str(final_text or ""), tool_traces=tuple(tool_traces), handoff_note="", turn_uuid=turn_uuid, ) def run_direct_loop(**kwargs: Any) -> TurnRunOutcome: return run_oclaw_direct_loop(**kwargs) __all__ = ["run_oclaw_direct_loop", "run_direct_loop", "warm_tool_wire_cache", "tool_wire_freeze_status"]