fixes
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
from threading import RLock
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import AppConfig, load_app_config
|
||||
from app.core.context_builder import ContextBuilder
|
||||
from app.core.command_analyzer import CommandAnalyzer
|
||||
from app.core.contracts import UserTask
|
||||
from app.core.execution_engine import ExecutionEngine
|
||||
from app.core.execution_scheduler import ExecutionScheduler
|
||||
@@ -13,6 +15,7 @@ from app.core.async_router import AsyncRouter
|
||||
from app.events.event_bus import EventBus
|
||||
from app.events.event_store import SQLiteEventStore
|
||||
from app.memory import MemoryInterface, MemoryStore, VectorIndex
|
||||
from app.memory.recall import MemoryRecallService
|
||||
from app.memory.write_policy import MemoryWritePolicy
|
||||
from app.models import (
|
||||
CoderAdapter,
|
||||
@@ -64,6 +67,8 @@ class RuntimeController:
|
||||
self._model_cache: dict[tuple[object, ...], tuple[object, RLock]] = {}
|
||||
self._memory_interface: MemoryInterface | None = None
|
||||
self._memory_policy: MemoryWritePolicy | None = None
|
||||
self._background_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="ducklm-task")
|
||||
self._background_tasks: dict[str, Future[dict[str, object]]] = {}
|
||||
self.tool_registry = None
|
||||
self.tool_sandbox = None
|
||||
|
||||
@@ -75,6 +80,8 @@ class RuntimeController:
|
||||
self.tool_sandbox = ToolSandbox(
|
||||
allowed_root=self.base_dir,
|
||||
timeout_ms=runtime_config.step_timeout_ms,
|
||||
command_timeout_ms=runtime_config.shell_command_timeout_ms,
|
||||
idle_timeout_ms=runtime_config.shell_idle_timeout_ms,
|
||||
)
|
||||
|
||||
self.tool_registry = self._create_tool_registry()
|
||||
@@ -121,6 +128,7 @@ class RuntimeController:
|
||||
self.permission_service = PermissionService(
|
||||
config=self._load_permissions_config(),
|
||||
)
|
||||
self.command_analyzer = CommandAnalyzer(self.permission_service)
|
||||
|
||||
self.execution_engine = ExecutionEngine(
|
||||
event_bus=self.event_bus,
|
||||
@@ -134,6 +142,8 @@ class RuntimeController:
|
||||
memory_interface=self._memory_interface,
|
||||
prompts=self._prompts,
|
||||
recovery_limit=runtime_config.tool_retry_limit,
|
||||
critic_retry_limit=runtime_config.critic_retry_limit,
|
||||
command_analyzer=self.command_analyzer,
|
||||
)
|
||||
|
||||
self.runtime_loop = RuntimeLoop(
|
||||
@@ -194,35 +204,35 @@ class RuntimeController:
|
||||
if thinker_config.get("path"):
|
||||
llm, lock = self._get_or_create_llm("thinker", thinker_config)
|
||||
self._thinker = OrchestratorAdapter(llm, system_prompt=self._prompts.get("thinker"), lock=lock)
|
||||
print(f"Thinker loaded: {self._thinker} (model: {thinker_config.get("path")})")
|
||||
print(f"Thinker loaded: {self._thinker} (model: {thinker_config.get('path')})")
|
||||
|
||||
print("Loading json_compiler model...")
|
||||
compiler_config = self.config.models.json_compiler or {}
|
||||
if compiler_config.get("path"):
|
||||
llm, lock = self._get_or_create_llm("json_compiler", compiler_config)
|
||||
self._json_compiler = OrchestratorAdapter(llm, system_prompt=self._prompts.get("json_compiler"), lock=lock)
|
||||
print(f"JSON Compiler loaded: {self._json_compiler} (model: {compiler_config.get("path")})")
|
||||
print(f"JSON Compiler loaded: {self._json_compiler} (model: {compiler_config.get('path')})")
|
||||
|
||||
print("Loading coder model...")
|
||||
coder_config = self.config.models.coder or {}
|
||||
if coder_config.get("path"):
|
||||
llm, lock = self._get_or_create_llm("coder", coder_config)
|
||||
self._coder = CoderAdapter(llm, system_prompt=self._prompts.get("coder"), lock=lock)
|
||||
print(f"Coder loaded: {self._coder} (model: {coder_config.get("path")})")
|
||||
print(f"Coder loaded: {self._coder} (model: {coder_config.get('path')})")
|
||||
|
||||
print("Loading critic model...")
|
||||
critic_config = self.config.models.critic or {}
|
||||
if critic_config.get("path"):
|
||||
llm, lock = self._get_or_create_llm("critic", critic_config)
|
||||
self._critic = CriticAdapter(llm, system_prompt=self._prompts.get("critic"), lock=lock)
|
||||
print(f"Critic loaded: {self._critic} (model: {critic_config.get("path")})")
|
||||
print(f"Critic loaded: {self._critic} (model: {critic_config.get('path')})")
|
||||
|
||||
print("Loading sys_util model...")
|
||||
sys_util_config = self.config.models.sys_util or {}
|
||||
if sys_util_config.get("path"):
|
||||
llm, lock = self._get_or_create_llm("sys_util", sys_util_config)
|
||||
self._sys_util = OrchestratorAdapter(llm, system_prompt=self._prompts.get("sys_util"), lock=lock)
|
||||
print(f"Sys_util loaded: {self._sys_util} (model: {sys_util_config.get("path")})")
|
||||
print(f"Sys_util loaded: {self._sys_util} (model: {sys_util_config.get('path')})")
|
||||
|
||||
print("All models loaded successfully")
|
||||
|
||||
@@ -241,6 +251,28 @@ class RuntimeController:
|
||||
if async_coder:
|
||||
self.execution_engine.set_coder(async_coder)
|
||||
|
||||
# Create MemoryRecallService using the configured model (default: sys_util)
|
||||
# Reuses already-loaded async adapter — no duplicate model loading
|
||||
recall_model_name = self.config.runtime.recall_model
|
||||
recall_async_model = {
|
||||
"sys_util": async_sys_util,
|
||||
"thinker": async_thinker,
|
||||
"json_compiler": async_compiler,
|
||||
"critic": async_critic,
|
||||
"coder": async_coder,
|
||||
}.get(recall_model_name, async_sys_util)
|
||||
|
||||
self._recall_service = MemoryRecallService(
|
||||
memory_interface=self._memory_interface,
|
||||
recall_model=recall_async_model,
|
||||
)
|
||||
self.runtime_loop.set_recall_service(self._recall_service)
|
||||
print(f"MemoryRecallService initialized with model: {recall_model_name}")
|
||||
|
||||
# Set memory policy in runtime loop
|
||||
self.runtime_loop.set_memory_policy(self._memory_policy)
|
||||
print(f"MemoryWritePolicy set: {self._memory_policy is not None}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to load models at startup: {e}")
|
||||
raise RuntimeError(f"Model loading failed: {e}") from e
|
||||
@@ -375,21 +407,76 @@ class RuntimeController:
|
||||
def handle_task(self, task: UserTask) -> dict[str, object]:
|
||||
return self.runtime_loop.run_task(task)
|
||||
|
||||
def submit_task(self, task: UserTask) -> dict[str, object]:
|
||||
self._background_tasks[task.task_id] = self._background_executor.submit(
|
||||
self.handle_task,
|
||||
task,
|
||||
)
|
||||
return {"task_id": task.task_id, "status": "accepted"}
|
||||
|
||||
def resolve_permission(self, task_id: str, decision: str) -> dict[str, object]:
|
||||
return self.runtime_loop.resolve_permission(
|
||||
task_id=task_id, decision=decision
|
||||
)
|
||||
|
||||
def submit_permission_resolution(self, task_id: str, decision: str) -> dict[str, object]:
|
||||
if not self.task_state_store.get_task(task_id):
|
||||
return {"task_id": task_id, "status": "failed", "result": {"error": "Unknown task_id"}}
|
||||
self._background_tasks[task_id] = self._background_executor.submit(
|
||||
self.resolve_permission,
|
||||
task_id,
|
||||
decision,
|
||||
)
|
||||
return {"task_id": task_id, "status": "accepted"}
|
||||
|
||||
def resolve_secret(self, task_id: str, secret: str) -> dict[str, object]:
|
||||
return self.runtime_loop.resolve_secret(
|
||||
task_id=task_id, secret=secret
|
||||
)
|
||||
|
||||
def submit_secret_resolution(self, task_id: str, secret: str) -> dict[str, object]:
|
||||
if not self.task_state_store.get_task(task_id):
|
||||
return {"task_id": task_id, "status": "failed", "result": {"error": "Unknown task_id"}}
|
||||
self._background_tasks[task_id] = self._background_executor.submit(
|
||||
self.resolve_secret,
|
||||
task_id,
|
||||
secret,
|
||||
)
|
||||
return {"task_id": task_id, "status": "accepted"}
|
||||
|
||||
def resolve_password(self, task_id: str, password: str) -> dict[str, object]:
|
||||
return self.runtime_loop.resolve_password(
|
||||
task_id=task_id, password=password
|
||||
)
|
||||
|
||||
def resolve_review(self, task_id: str, decision: str, correction: str | None = None) -> dict[str, object]:
|
||||
return self.runtime_loop.resolve_review(
|
||||
task_id=task_id,
|
||||
decision=decision,
|
||||
correction=correction,
|
||||
)
|
||||
|
||||
def submit_review_resolution(self, task_id: str, decision: str, correction: str | None = None) -> dict[str, object]:
|
||||
if not self.task_state_store.get_task(task_id):
|
||||
return {"task_id": task_id, "status": "failed", "result": {"error": "Unknown task_id"}}
|
||||
self._background_tasks[task_id] = self._background_executor.submit(
|
||||
self.resolve_review,
|
||||
task_id,
|
||||
decision,
|
||||
correction,
|
||||
)
|
||||
return {"task_id": task_id, "status": "accepted"}
|
||||
|
||||
def submit_password_resolution(self, task_id: str, password: str) -> dict[str, object]:
|
||||
if not self.task_state_store.get_task(task_id):
|
||||
return {"task_id": task_id, "status": "failed", "result": {"error": "Unknown task_id"}}
|
||||
self._background_tasks[task_id] = self._background_executor.submit(
|
||||
self.resolve_password,
|
||||
task_id,
|
||||
password,
|
||||
)
|
||||
return {"task_id": task_id, "status": "accepted"}
|
||||
|
||||
def handle_critic_feedback(
|
||||
self,
|
||||
feedback: str,
|
||||
|
||||
+223
-39
@@ -3,16 +3,46 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
|
||||
from app.core.context_builder import ContextBuilder
|
||||
from app.core.contracts import ExecutionDirective, PermissionDecision, PermissionRequest, RuntimeEvent, SecretRequest, TaskCheckpoint, UserTask
|
||||
from app.core.contracts import CriticScore, ExecutionDirective, PermissionDecision, PermissionRequest, RuntimeEvent, SecretRequest, TaskCheckpoint, UserTask
|
||||
from app.core.execution_engine import ExecutionEngine
|
||||
from app.core.async_router import AsyncRouter
|
||||
from app.events.event_bus import EventBus
|
||||
from app.events.event_types import CHECKPOINT_SAVED, CONTEXT_BUILT, TASK_AWAITING_INPUT, TASK_AWAITING_PERMISSION, TASK_COMPLETED, TASK_FAILED, TASK_RECEIVED
|
||||
from app.events.event_types import CHECKPOINT_SAVED, CONTEXT_BUILT, MEMORY_RECALL_USED, MEMORY_WRITE_DECIDED, REVIEW_RESOLVED, TASK_AWAITING_INPUT, TASK_AWAITING_PERMISSION, TASK_AWAITING_REVIEW, TASK_COMPLETED, TASK_FAILED, TASK_RECEIVED
|
||||
from app.core.permission_service import PermissionService
|
||||
from app.memory.recall import MemoryRecallService
|
||||
from app.memory.write_policy import MemoryWritePolicy
|
||||
from app.state.checkpoint_store import SQLiteCheckpointStore
|
||||
from app.state.task_state_store import SQLiteTaskStateStore
|
||||
|
||||
|
||||
def _build_response_directive(execution_result: dict) -> dict | None:
|
||||
"""Build a response_directive from step_results or direct output for the client."""
|
||||
result = execution_result.get("result", {})
|
||||
|
||||
# Case 1: step_results from plan execution
|
||||
step_results = result.get("step_results")
|
||||
if step_results:
|
||||
response_parts = []
|
||||
for step in step_results:
|
||||
result_data = step.get("result", {})
|
||||
tool_result = result_data.get("result", result_data)
|
||||
if tool_result.get("ok") and tool_result.get("output"):
|
||||
response_parts.append(str(tool_result["output"]))
|
||||
if response_parts:
|
||||
response_text = "\n\n".join(response_parts)
|
||||
return ExecutionDirective(
|
||||
type="respond", payload={"text": response_text}
|
||||
).model_dump(mode="json")
|
||||
|
||||
# Case 2: direct tool output (e.g. from resolve_secret -> execute_tool)
|
||||
if result.get("ok") and result.get("output"):
|
||||
return ExecutionDirective(
|
||||
type="respond", payload={"text": str(result["output"])}
|
||||
).model_dump(mode="json")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class RuntimeLoop:
|
||||
"""Central control loop skeleton coordinating task state and events."""
|
||||
|
||||
@@ -26,6 +56,8 @@ class RuntimeLoop:
|
||||
execution_engine: ExecutionEngine,
|
||||
permission_service: PermissionService,
|
||||
memory_interface=None,
|
||||
recall_service: MemoryRecallService | None = None,
|
||||
memory_policy: MemoryWritePolicy | None = None,
|
||||
) -> None:
|
||||
self._event_bus = event_bus
|
||||
self._task_state_store = task_state_store
|
||||
@@ -35,6 +67,14 @@ class RuntimeLoop:
|
||||
self._execution_engine = execution_engine
|
||||
self._permission_service = permission_service
|
||||
self._memory_interface = memory_interface
|
||||
self._recall_service = recall_service
|
||||
self._memory_policy = memory_policy
|
||||
|
||||
def set_recall_service(self, recall_service: MemoryRecallService) -> None:
|
||||
self._recall_service = recall_service
|
||||
|
||||
def set_memory_policy(self, policy: MemoryWritePolicy | None) -> None:
|
||||
self._memory_policy = policy
|
||||
|
||||
def run_task(self, task: UserTask) -> dict[str, object]:
|
||||
# Check input for hard-stop commands BEFORE processing
|
||||
@@ -82,6 +122,23 @@ class RuntimeLoop:
|
||||
context = self._context_builder.build(task=task, checkpoint=checkpoint)
|
||||
self._publish(task, CONTEXT_BUILT, {"keys": sorted(context.keys())})
|
||||
|
||||
# Active memory recall: system decides if it needs to search memory
|
||||
recall_result = asyncio.run(self._run_recall(task))
|
||||
if recall_result["should_recall"]:
|
||||
context["memory_recall"] = {
|
||||
"query": recall_result["query"],
|
||||
"summary": recall_result["summary"],
|
||||
"entries": [
|
||||
{"text": e.text, "kind": e.kind, "weight": e.weight}
|
||||
for e in recall_result["results"]
|
||||
],
|
||||
}
|
||||
self._publish(task, MEMORY_RECALL_USED, {
|
||||
"query": recall_result["query"],
|
||||
"results_count": len(recall_result["results"]),
|
||||
"reason": recall_result["reason"],
|
||||
})
|
||||
|
||||
directive = asyncio.run(
|
||||
self._router.decide(state=state, context=context, task_id=task.task_id, session_id=task.session_id)
|
||||
)
|
||||
@@ -104,15 +161,21 @@ class RuntimeLoop:
|
||||
"reason": "Permission denied - требуется sudo пароль",
|
||||
"attempts": 0,
|
||||
}
|
||||
elif execution_result["status"] == "awaiting_review":
|
||||
state_patch["pending_permission_request"] = None
|
||||
state_patch["pending_secret_request"] = None
|
||||
state_patch["resolved_permission_decision"] = None
|
||||
state_patch["pending_review"] = execution_result["result"]["review"]
|
||||
else:
|
||||
state_patch["pending_permission_request"] = None
|
||||
state_patch["pending_secret_request"] = None
|
||||
state_patch["resolved_permission_decision"] = None
|
||||
state_patch["pending_review"] = None
|
||||
self._task_state_store.update_task(task.task_id, state_patch)
|
||||
final_status = str(execution_result["status"])
|
||||
|
||||
# For awaiting states - do NOT mark task as completed, keep it in pending state
|
||||
if final_status in ("awaiting_permission", "awaiting_input", "awaiting_password"):
|
||||
if final_status in ("awaiting_permission", "awaiting_input", "awaiting_password", "awaiting_review"):
|
||||
# Task stays in pending state, don't update to completed
|
||||
pass
|
||||
else:
|
||||
@@ -125,9 +188,9 @@ class RuntimeLoop:
|
||||
)
|
||||
self._checkpoint_store.save(final_checkpoint)
|
||||
|
||||
# Generate response after plan execution
|
||||
# Generate response for user
|
||||
# Case 1: step_results from plan execution
|
||||
if final_status == "completed" and execution_result.get("result", {}).get("step_results"):
|
||||
# Format tool results into response
|
||||
step_results = execution_result["result"]["step_results"]
|
||||
response_parts = []
|
||||
for step in step_results:
|
||||
@@ -135,16 +198,21 @@ class RuntimeLoop:
|
||||
tool_result = result_data.get("result", result_data)
|
||||
if tool_result.get("ok") and tool_result.get("output"):
|
||||
response_parts.append(tool_result["output"])
|
||||
|
||||
if response_parts:
|
||||
# Create respond directive
|
||||
response_text = "\n\n".join(response_parts)
|
||||
respond_directive = ExecutionDirective(
|
||||
type="respond",
|
||||
payload={"text": response_text},
|
||||
)
|
||||
# Add to execution result
|
||||
execution_result["response_directive"] = respond_directive.model_dump(mode="json")
|
||||
execution_result["response_directive"] = ExecutionDirective(
|
||||
type="respond", payload={"text": response_text}
|
||||
).model_dump(mode="json")
|
||||
|
||||
# Case 2: respond directive from orchestrator (direct response, no steps)
|
||||
if final_status == "completed" and not execution_result.get("response_directive"):
|
||||
# Use the original directive from router.decide()
|
||||
if hasattr(directive, "type") and directive.type == "respond":
|
||||
if directive.payload.get("text"):
|
||||
execution_result["response_directive"] = directive.model_dump(mode="json")
|
||||
elif isinstance(directive, dict) and directive.get("type") == "respond":
|
||||
if directive.get("payload", {}).get("text"):
|
||||
execution_result["response_directive"] = directive
|
||||
|
||||
# Map status to terminal event type
|
||||
if final_status == "completed":
|
||||
@@ -155,6 +223,8 @@ class RuntimeLoop:
|
||||
terminal_event_type = TASK_AWAITING_PERMISSION
|
||||
elif final_status == "awaiting_input":
|
||||
terminal_event_type = TASK_AWAITING_INPUT
|
||||
elif final_status == "awaiting_review":
|
||||
terminal_event_type = TASK_AWAITING_REVIEW
|
||||
elif final_status == "awaiting_password":
|
||||
terminal_event_type = TASK_AWAITING_PERMISSION
|
||||
else:
|
||||
@@ -175,7 +245,10 @@ class RuntimeLoop:
|
||||
"task_id": task.task_id,
|
||||
"status": final_status,
|
||||
"directive": directive.model_dump(mode="json"),
|
||||
"result": execution_result["result"],
|
||||
"result": {
|
||||
**execution_result["result"],
|
||||
"response_directive": execution_result.get("response_directive"),
|
||||
},
|
||||
"events": [event.model_dump(mode="json") for event in self._event_bus.list_for_task(task.task_id)],
|
||||
}
|
||||
|
||||
@@ -254,6 +327,9 @@ class RuntimeLoop:
|
||||
"pending_secret_request": execution_result["result"].get("secret_request")
|
||||
if final_status == "awaiting_input"
|
||||
else None,
|
||||
"pending_review": execution_result["result"].get("review")
|
||||
if final_status == "awaiting_review"
|
||||
else None,
|
||||
"resolved_permission_decision": resolved,
|
||||
},
|
||||
)
|
||||
@@ -266,6 +342,8 @@ class RuntimeLoop:
|
||||
terminal_event_type = TASK_AWAITING_INPUT
|
||||
elif final_status == "awaiting_permission":
|
||||
terminal_event_type = TASK_AWAITING_PERMISSION
|
||||
elif final_status == "awaiting_review":
|
||||
terminal_event_type = TASK_AWAITING_REVIEW
|
||||
else:
|
||||
terminal_event_type = TASK_FAILED
|
||||
self._publish(
|
||||
@@ -283,7 +361,10 @@ class RuntimeLoop:
|
||||
return {
|
||||
"task_id": task.task_id,
|
||||
"status": final_status,
|
||||
"result": execution_result["result"],
|
||||
"result": {
|
||||
**execution_result["result"],
|
||||
"response_directive": _build_response_directive(execution_result),
|
||||
},
|
||||
"events": [event.model_dump(mode="json") for event in self._event_bus.list_for_task(task.task_id)],
|
||||
}
|
||||
|
||||
@@ -314,12 +395,15 @@ class RuntimeLoop:
|
||||
secret_override=secret,
|
||||
)
|
||||
final_status = str(execution_result["status"])
|
||||
pending_review = execution_result["result"].get("review") if final_status == "awaiting_review" else None
|
||||
pending_secret = execution_result["result"].get("secret_request") if final_status == "awaiting_input" else None
|
||||
self._task_state_store.update_task(
|
||||
task.task_id,
|
||||
{
|
||||
"status": final_status,
|
||||
"pending_secret_request": None,
|
||||
"resolved_permission_decision": None,
|
||||
"pending_secret_request": pending_secret,
|
||||
"resolved_permission_decision": resolved_permission_payload if final_status == "awaiting_input" else None,
|
||||
"pending_review": pending_review,
|
||||
},
|
||||
)
|
||||
checkpoint = TaskCheckpoint(task_id=task.task_id, status=final_status)
|
||||
@@ -331,6 +415,8 @@ class RuntimeLoop:
|
||||
terminal_event_type = TASK_AWAITING_INPUT
|
||||
elif final_status == "awaiting_permission":
|
||||
terminal_event_type = TASK_AWAITING_PERMISSION
|
||||
elif final_status == "awaiting_review":
|
||||
terminal_event_type = TASK_AWAITING_REVIEW
|
||||
else:
|
||||
terminal_event_type = TASK_FAILED
|
||||
self._publish(
|
||||
@@ -344,10 +430,55 @@ class RuntimeLoop:
|
||||
return {
|
||||
"task_id": task.task_id,
|
||||
"status": final_status,
|
||||
"result": execution_result["result"],
|
||||
"result": {
|
||||
**execution_result["result"],
|
||||
"response_directive": _build_response_directive(execution_result),
|
||||
},
|
||||
"events": [event.model_dump(mode="json") for event in self._event_bus.list_for_task(task.task_id)],
|
||||
}
|
||||
|
||||
def resolve_review(self, task_id: str, decision: str, correction: str | None = None) -> dict[str, object]:
|
||||
state = self._task_state_store.get_task(task_id)
|
||||
if not state:
|
||||
return {"task_id": task_id, "status": "failed", "result": {"error": "Unknown task_id"}}
|
||||
pending_review = state.get("pending_review")
|
||||
if not pending_review:
|
||||
return {"task_id": task_id, "status": "failed", "result": {"error": "No pending review"}}
|
||||
|
||||
task = UserTask(
|
||||
task_id=task_id,
|
||||
session_id=state["session_id"],
|
||||
input=state["task_input"],
|
||||
context={
|
||||
**state.get("task_context", {}),
|
||||
"previous_action_review": {
|
||||
"decision": decision,
|
||||
"correction": correction,
|
||||
"review": pending_review,
|
||||
},
|
||||
},
|
||||
)
|
||||
self._publish(task, REVIEW_RESOLVED, {
|
||||
"decision": decision,
|
||||
"correction": correction,
|
||||
"review": pending_review,
|
||||
})
|
||||
if self._memory_interface:
|
||||
try:
|
||||
self._memory_interface.insert(
|
||||
text=f"User reviewed model action as {decision}. Correction: {correction or ''}. Review: {pending_review}",
|
||||
kind="critique",
|
||||
source="user",
|
||||
task_id=task_id,
|
||||
session_id=state["session_id"],
|
||||
weight=0.9 if decision == "wrong_action" else 0.5,
|
||||
metadata={"decision": decision, "review": pending_review},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
self._task_state_store.update_task(task_id, {"pending_review": None, "status": "replanning"})
|
||||
return self.run_task(task)
|
||||
|
||||
def resolve_password(self, task_id: str, password: str) -> dict[str, object]:
|
||||
state = self._task_state_store.get_task(task_id)
|
||||
if not state:
|
||||
@@ -445,7 +576,10 @@ class RuntimeLoop:
|
||||
return {
|
||||
"task_id": task.task_id,
|
||||
"status": final_status,
|
||||
"result": execution_result["result"],
|
||||
"result": {
|
||||
**execution_result["result"],
|
||||
"response_directive": _build_response_directive(execution_result),
|
||||
},
|
||||
"events": [event.model_dump(mode="json") for event in self._event_bus.list_for_task(task.task_id)],
|
||||
}
|
||||
|
||||
@@ -459,22 +593,61 @@ class RuntimeLoop:
|
||||
)
|
||||
self._event_bus.publish(event)
|
||||
|
||||
async def _run_recall(self, task: UserTask) -> dict:
|
||||
"""Run active memory recall before orchestration."""
|
||||
if not self._recall_service:
|
||||
return {"should_recall": False, "reason": "no_recall_service", "query": "", "results": [], "summary": ""}
|
||||
try:
|
||||
return await self._recall_service.recall(task_input=task.input)
|
||||
except Exception as e:
|
||||
return {"should_recall": False, "reason": f"recall_error: {e}", "query": "", "results": [], "summary": ""}
|
||||
|
||||
def _save_to_memory(self, task: UserTask, execution_result: dict, status: str) -> None:
|
||||
"""Save task input and result to memory for session context."""
|
||||
"""Save task input and result to memory for session context, using MemoryWritePolicy."""
|
||||
if not self._memory_interface:
|
||||
return
|
||||
|
||||
try:
|
||||
# Build a synthetic critic_score for policy based on task status
|
||||
# For summary/tool_result without real critic, we derive from execution outcome
|
||||
if status == "completed":
|
||||
synthetic_score = CriticScore(
|
||||
correctness=0.9, usefulness=0.8, safety=0.95,
|
||||
memory_store=True, weight=0.85, explanation="Task completed successfully"
|
||||
)
|
||||
elif status == "failed":
|
||||
synthetic_score = CriticScore(
|
||||
correctness=0.2, usefulness=0.3, safety=0.7,
|
||||
memory_store=True, weight=0.5, explanation="Task failed — store for learning"
|
||||
)
|
||||
else:
|
||||
synthetic_score = CriticScore(
|
||||
correctness=0.5, usefulness=0.5, safety=0.8,
|
||||
memory_store=False, weight=0.3, explanation=f"Status: {status}"
|
||||
)
|
||||
|
||||
# Save task input as summary
|
||||
self._memory_interface.insert(
|
||||
text=f"User request: {task.input}",
|
||||
kind="summary",
|
||||
source="user",
|
||||
task_id=task.task_id,
|
||||
session_id=task.session_id,
|
||||
weight=0.8,
|
||||
metadata={"status": status},
|
||||
)
|
||||
decision = "store"
|
||||
if self._memory_policy:
|
||||
decision = self._memory_policy.decide(
|
||||
critic_score=synthetic_score,
|
||||
memory_type="summary",
|
||||
session_id=task.session_id,
|
||||
)
|
||||
if decision in ("store", "store_with_weight"):
|
||||
weight = synthetic_score.weight if decision == "store_with_weight" else 0.8
|
||||
self._memory_interface.insert(
|
||||
text=f"User request: {task.input}",
|
||||
kind="summary",
|
||||
source="user",
|
||||
task_id=task.task_id,
|
||||
session_id=task.session_id,
|
||||
weight=weight,
|
||||
metadata={"status": status, "policy_decision": decision},
|
||||
)
|
||||
self._publish(task, MEMORY_WRITE_DECIDED, {
|
||||
"kind": "summary", "decision": decision, "text_preview": task.input[:80]
|
||||
})
|
||||
|
||||
# Save execution result
|
||||
result_text = ""
|
||||
@@ -489,16 +662,27 @@ class RuntimeLoop:
|
||||
result_text = f" | Error: {execution_result.get('result', {}).get('error', 'Unknown')}"
|
||||
|
||||
if result_text:
|
||||
self._memory_interface.insert(
|
||||
text=f"Result: {status}{result_text}",
|
||||
kind="tool_result",
|
||||
source="system",
|
||||
task_id=task.task_id,
|
||||
session_id=task.session_id,
|
||||
weight=0.7,
|
||||
metadata={"status": status},
|
||||
)
|
||||
decision = "store"
|
||||
if self._memory_policy:
|
||||
decision = self._memory_policy.decide(
|
||||
critic_score=synthetic_score,
|
||||
memory_type="tool_result",
|
||||
session_id=task.session_id,
|
||||
)
|
||||
if decision in ("store", "store_with_weight"):
|
||||
weight = synthetic_score.weight if decision == "store_with_weight" else 0.7
|
||||
self._memory_interface.insert(
|
||||
text=f"Result: {status}{result_text}",
|
||||
kind="tool_result",
|
||||
source="system",
|
||||
task_id=task.task_id,
|
||||
session_id=task.session_id,
|
||||
weight=weight,
|
||||
metadata={"status": status, "policy_decision": decision},
|
||||
)
|
||||
self._publish(task, MEMORY_WRITE_DECIDED, {
|
||||
"kind": "tool_result", "decision": decision, "text_preview": result_text[:80]
|
||||
})
|
||||
except Exception as e:
|
||||
# Log but don't fail the task
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"Failed to save to memory: {e}")
|
||||
|
||||
Reference in New Issue
Block a user