oclaw/runtime/direct_loop.py
oliver dbbe3add6a 重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。

Made-with: Cursor
2026-04-26 08:34:33 +08:00

799 lines
28 KiB
Python

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*</\1>\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...<tool_result_guard_truncated>"
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"]