Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
3326 lines
125 KiB
Python
3326 lines
125 KiB
Python
"""Jarvis agent graph orchestration."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
import logging
|
||
import re
|
||
from uuid import uuid4
|
||
from typing import Any, Literal, cast
|
||
|
||
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage, ToolMessage
|
||
from langgraph.graph import END, StateGraph
|
||
|
||
from app.agents.isolation import (
|
||
WorktreeIsolationError,
|
||
prepare_session_isolation,
|
||
prepare_worktree_isolation,
|
||
select_isolation_strategy,
|
||
)
|
||
from app.agents.prompts import (
|
||
ANALYST_SYSTEM_PROMPT,
|
||
COORDINATOR_SYSTEM_PROMPT,
|
||
EXECUTOR_SYSTEM_PROMPT,
|
||
JSON_ACTION_FALLBACK_PROMPT,
|
||
LIBRARIAN_SYSTEM_PROMPT,
|
||
MASTER_SYSTEM_PROMPT,
|
||
SCHEDULE_PLANNER_SYSTEM_PROMPT,
|
||
)
|
||
from app.agents.orchestration.result_merge import merge_task_results
|
||
from app.agents.orchestration.scheduler import build_subtask_specs, ensure_child_links
|
||
from app.agents.orchestration.subagent_runtime import subtask_spec_to_agent_task
|
||
from app.agents.registry import load_builtin_registry_indexes
|
||
from app.agents.runtime_metrics import (
|
||
coerce_cost_thresholds,
|
||
estimate_token_cost,
|
||
extract_token_usage,
|
||
is_cost_budget_warning,
|
||
)
|
||
from app.agents.schemas.event import AgentEvent
|
||
from app.agents.schemas.message import AgentMessage
|
||
from app.agents.schemas.orchestration import (
|
||
ExecutionDecision,
|
||
MergeReport,
|
||
RuntimeRequestContext,
|
||
TaskGraph,
|
||
VerificationReport,
|
||
render_runtime_request_context_summary,
|
||
)
|
||
from app.agents.schemas.task import (
|
||
AgentTask,
|
||
CollaborationBudget,
|
||
InterruptRecord,
|
||
RecoveryRecord,
|
||
TaskResult,
|
||
)
|
||
from app.agents.skill_registry import build_skill_context
|
||
from app.agents.skills.retriever import build_shortlisted_skill_context
|
||
from app.agents.state import AgentRole, AgentState
|
||
from app.agents.tools import SUB_COMMANDER_TOOLSETS
|
||
from app.agents.tools.time_reasoning import normalize_tool_time_arguments
|
||
from app.agents.verifier import (
|
||
apply_verification_verdict,
|
||
normalize_task_result,
|
||
verify_task_result,
|
||
)
|
||
from app.services.llm_service import (
|
||
create_llm_from_config,
|
||
default_provider_capabilities,
|
||
get_llm,
|
||
resolve_provider_capabilities,
|
||
)
|
||
|
||
logger = logging.getLogger("jarvis.agent")
|
||
|
||
SUB_COMMANDER_PROMPTS = {
|
||
"schedule_analysis": "你负责做日程判断与聚焦分析,只给判断和依据,不直接落库。",
|
||
"schedule_planning": "你负责把当前目标转成明确安排;当用户要求创建提醒/任务/目标时,直接调用工具执行。",
|
||
"executor_tasks": "你负责执行任务、提醒、目标、待办相关操作,需要时直接调用工具。",
|
||
"executor_forum": "你只处理论坛与指令帖相关操作。",
|
||
"librarian_retrieval": "你负责知识检索与外部搜索,基于证据回答问题。",
|
||
"librarian_graph": "你负责知识图谱上下文与结构化沉淀。",
|
||
"analyst_progress": "你负责进度研判,汇总当前推进情况。",
|
||
"analyst_insights": "你负责趋势、风险和机会判断,必要时调用检索工具。",
|
||
}
|
||
|
||
ROLE_SKILL_CONTEXT = {
|
||
AgentRole.SCHEDULE_PLANNER: "schedule_planner",
|
||
AgentRole.EXECUTOR: "executor",
|
||
AgentRole.LIBRARIAN: "librarian",
|
||
AgentRole.ANALYST: "analyst",
|
||
AgentRole.CODE_COMMANDER: None,
|
||
}
|
||
|
||
from app.agents.prompts import CODE_COMMANDER_SYSTEM_PROMPT
|
||
|
||
ROLE_SYSTEM_PROMPTS = {
|
||
AgentRole.SCHEDULE_PLANNER: SCHEDULE_PLANNER_SYSTEM_PROMPT,
|
||
AgentRole.EXECUTOR: EXECUTOR_SYSTEM_PROMPT,
|
||
AgentRole.LIBRARIAN: LIBRARIAN_SYSTEM_PROMPT,
|
||
AgentRole.ANALYST: ANALYST_SYSTEM_PROMPT,
|
||
AgentRole.CODE_COMMANDER: CODE_COMMANDER_SYSTEM_PROMPT,
|
||
}
|
||
|
||
SCHEDULE_KEYWORDS = (
|
||
"提醒",
|
||
"日程",
|
||
"安排",
|
||
"计划",
|
||
"排期",
|
||
"会议",
|
||
"开会",
|
||
"明天",
|
||
"今天",
|
||
"后天",
|
||
"下周",
|
||
"本周",
|
||
"周",
|
||
"星期",
|
||
"交付",
|
||
"节点",
|
||
"deadline",
|
||
)
|
||
ACCOUNTING_INTENT_KEYWORDS = (
|
||
"记账",
|
||
"账单",
|
||
"花了多少钱",
|
||
"用了多少钱",
|
||
"支出",
|
||
"消费",
|
||
"花销",
|
||
"开销",
|
||
)
|
||
KNOWLEDGE_KEYWORDS = (
|
||
"知识",
|
||
"搜索",
|
||
"检索",
|
||
"资料",
|
||
"文档",
|
||
"联网",
|
||
"上网",
|
||
"查询",
|
||
"查一下",
|
||
"最新",
|
||
)
|
||
GENERAL_QA_PATTERNS = (
|
||
"介绍一下",
|
||
"介绍下",
|
||
"什么是",
|
||
"是谁",
|
||
"在哪里",
|
||
"为什么",
|
||
"怎么理解",
|
||
"聊聊",
|
||
)
|
||
ANALYSIS_KEYWORDS = ("分析", "报告", "统计", "趋势", "风险", "洞察", "总结")
|
||
EXECUTION_KEYWORDS = ("创建", "更新", "修改", "执行", "发帖", "论坛", "帖子", "完成", "处理")
|
||
CODE_COMMANDER_KEYWORDS = (
|
||
"写代码",
|
||
"代码",
|
||
"代码助手",
|
||
"demo",
|
||
"示例项目",
|
||
"贪食蛇",
|
||
"小游戏",
|
||
"帮我写一个",
|
||
"opencode",
|
||
"codex",
|
||
"claude code",
|
||
"gemini code",
|
||
)
|
||
SCHEDULE_ANALYSIS_KEYWORDS = ("聚焦", "判断", "分析", "优先级", "取舍", "最近对话", "该做什么")
|
||
SCHEDULE_PLANNING_KEYWORDS = (
|
||
"安排",
|
||
"计划",
|
||
"排期",
|
||
"提醒",
|
||
"创建",
|
||
"新增",
|
||
"会议",
|
||
"交付",
|
||
"节点",
|
||
)
|
||
IDENTITY_PATTERNS = ("你是谁", "你是誰")
|
||
CAPABILITY_PATTERNS = ("你能做什么", "你可以做什么", "你会做什么")
|
||
SHORT_CONFIRMATION_PATTERNS = ("创建", "好的创建", "确认创建", "就创建", "那就创建")
|
||
SCHEDULE_CONFIRMATION_HINTS = (
|
||
"创建这条提醒",
|
||
"现在创建这条提醒",
|
||
"现在创建提醒",
|
||
"是否需要我现在创建",
|
||
)
|
||
SCHEDULE_CONFIRMATION_QUESTION_MARKERS = ("是否", "要不要", "吗", "?", "?")
|
||
COLLABORATION_STEP_MARKERS = (
|
||
"然后",
|
||
"再",
|
||
"并且",
|
||
"同时",
|
||
"顺便",
|
||
"最后",
|
||
"分别",
|
||
"拆成",
|
||
"协作",
|
||
"整合",
|
||
)
|
||
COLLABORATION_ROLE_ORDER = {
|
||
AgentRole.LIBRARIAN: 0,
|
||
AgentRole.ANALYST: 1,
|
||
AgentRole.SCHEDULE_PLANNER: 2,
|
||
AgentRole.EXECUTOR: 3,
|
||
}
|
||
|
||
|
||
def _role_value(role: AgentRole | str | None) -> str:
|
||
if isinstance(role, AgentRole):
|
||
return role.value
|
||
return str(role or "")
|
||
|
||
|
||
def _normalize_current_agent(value: AgentRole | str | None) -> str:
|
||
role_value = _role_value(value)
|
||
return role_value or AgentRole.MASTER.value
|
||
|
||
|
||
def _normalize_active_agents(values: list[AgentRole | str] | None) -> list[AgentRole]:
|
||
normalized: list[AgentRole] = []
|
||
for value in values or [AgentRole.MASTER]:
|
||
role_value = _role_value(value)
|
||
try:
|
||
role = AgentRole(role_value)
|
||
except ValueError:
|
||
continue
|
||
if role not in normalized:
|
||
normalized.append(role)
|
||
return normalized or [AgentRole.MASTER]
|
||
|
||
|
||
def _normalize_user_text(text: str) -> str:
|
||
normalized = (text or "").strip().lower()
|
||
normalized = re.sub(r"[,。!?;:,.!?;:\s]+", "", normalized)
|
||
return normalized
|
||
|
||
|
||
def _stringify_message_content(content: Any) -> str:
|
||
if content is None:
|
||
return ""
|
||
if isinstance(content, str):
|
||
return content
|
||
if isinstance(content, list):
|
||
parts: list[str] = []
|
||
for item in content:
|
||
if isinstance(item, str):
|
||
parts.append(item)
|
||
continue
|
||
if isinstance(item, dict):
|
||
text = item.get("text")
|
||
if text:
|
||
parts.append(str(text))
|
||
continue
|
||
nested = item.get("content")
|
||
if nested:
|
||
parts.append(_stringify_message_content(nested))
|
||
continue
|
||
parts.append(str(item))
|
||
return "".join(parts)
|
||
if isinstance(content, dict):
|
||
text = content.get("text")
|
||
if text:
|
||
return str(text)
|
||
nested = content.get("content")
|
||
if nested:
|
||
return _stringify_message_content(nested)
|
||
return json.dumps(content, ensure_ascii=False)
|
||
return str(content)
|
||
|
||
|
||
def _get_state_int(state: AgentState, key: str) -> int:
|
||
value = state.get(key)
|
||
return value if isinstance(value, int) else 0
|
||
|
||
|
||
def _clear_isolation_state(state: AgentState) -> None:
|
||
state["isolation_mode"] = "none"
|
||
state["isolation_id"] = None
|
||
state["isolation_workspace_path"] = None
|
||
state["isolation_parent_conversation_id"] = None
|
||
state["isolation_metadata"] = {}
|
||
|
||
|
||
def _apply_isolation_payload(state: AgentState, payload: dict[str, Any]) -> None:
|
||
state["isolation_mode"] = str(payload.get("mode") or "none")
|
||
state["isolation_id"] = str(payload.get("isolation_id") or "") or None
|
||
state["isolation_workspace_path"] = str(payload.get("workspace_path") or "") or None
|
||
state["isolation_parent_conversation_id"] = (
|
||
str(payload.get("parent_conversation_id") or "") or None
|
||
)
|
||
state["isolation_metadata"] = dict(payload.get("metadata") or {})
|
||
|
||
|
||
def _prepare_isolation_context(
|
||
state: AgentState,
|
||
*,
|
||
role: AgentRole,
|
||
sub_commander: str,
|
||
user_query: str,
|
||
toolset: list[Any],
|
||
) -> None:
|
||
tool_names = [tool.name for tool in toolset]
|
||
decision = select_isolation_strategy(
|
||
user_query=user_query,
|
||
tool_names=tool_names,
|
||
role_value=role.value,
|
||
execution_mode=str(state.get("execution_mode") or "direct"),
|
||
)
|
||
if decision.mode == "none":
|
||
_clear_isolation_state(state)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.isolation.selected",
|
||
payload={"mode": "none", "reason": decision.reason, "tool_names": tool_names},
|
||
)
|
||
return
|
||
|
||
if decision.mode == "session":
|
||
isolation_payload = prepare_session_isolation(
|
||
state=state,
|
||
decision=decision,
|
||
role_value=role.value,
|
||
sub_commander=sub_commander,
|
||
)
|
||
_apply_isolation_payload(state, isolation_payload)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.isolation.selected",
|
||
payload=isolation_payload,
|
||
)
|
||
return
|
||
|
||
try:
|
||
isolation_payload = prepare_worktree_isolation(
|
||
state=state,
|
||
decision=decision,
|
||
role_value=role.value,
|
||
sub_commander=sub_commander,
|
||
)
|
||
except WorktreeIsolationError as exc:
|
||
isolation_payload = prepare_session_isolation(
|
||
state=state,
|
||
decision=decision,
|
||
role_value=role.value,
|
||
sub_commander=sub_commander,
|
||
)
|
||
isolation_payload["metadata"] = {
|
||
**dict(isolation_payload.get("metadata") or {}),
|
||
"fallback_reason": str(exc),
|
||
"fallback_from": "worktree",
|
||
}
|
||
_append_event_trace(
|
||
state,
|
||
"agent.isolation.fallback",
|
||
payload={
|
||
"requested_mode": "worktree",
|
||
"fallback_mode": "session",
|
||
"reason": str(exc),
|
||
"tool_names": tool_names,
|
||
},
|
||
severity="warning",
|
||
)
|
||
|
||
_apply_isolation_payload(state, isolation_payload)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.isolation.selected",
|
||
payload=isolation_payload,
|
||
)
|
||
|
||
|
||
def _record_response_usage(state: AgentState, response: Any) -> None:
|
||
input_tokens, output_tokens = extract_token_usage(response)
|
||
if not input_tokens and not output_tokens:
|
||
return
|
||
|
||
current_input_tokens = int(state.get("input_tokens") or 0)
|
||
current_output_tokens = int(state.get("output_tokens") or 0)
|
||
total_input_tokens = current_input_tokens + input_tokens
|
||
total_output_tokens = current_output_tokens + output_tokens
|
||
state["input_tokens"] = total_input_tokens
|
||
state["output_tokens"] = total_output_tokens
|
||
state["estimated_cost"] = estimate_token_cost(total_input_tokens, total_output_tokens)
|
||
|
||
thresholds = coerce_cost_thresholds(state.get("cost_thresholds"))
|
||
state["cost_thresholds"] = thresholds
|
||
budget_warning = is_cost_budget_warning(
|
||
total_input_tokens,
|
||
total_output_tokens,
|
||
state.get("estimated_cost"),
|
||
thresholds,
|
||
)
|
||
previous_budget_warning = bool(state.get("budget_warning") or False)
|
||
state["budget_warning"] = budget_warning
|
||
|
||
agent_id = str(state.get("agent_id") or state.get("current_agent") or AgentRole.MASTER.value)
|
||
cost_by_agent = {
|
||
key: dict(value) for key, value in dict(state.get("cost_by_agent") or {}).items()
|
||
}
|
||
agent_totals = dict(cost_by_agent.get(agent_id) or {})
|
||
agent_input_tokens = int(agent_totals.get("input_tokens") or 0) + input_tokens
|
||
agent_output_tokens = int(agent_totals.get("output_tokens") or 0) + output_tokens
|
||
agent_estimated_cost = estimate_token_cost(agent_input_tokens, agent_output_tokens)
|
||
cost_by_agent[agent_id] = {
|
||
"agent_id": agent_id,
|
||
"input_tokens": agent_input_tokens,
|
||
"output_tokens": agent_output_tokens,
|
||
"total_tokens": agent_input_tokens + agent_output_tokens,
|
||
"estimated_cost": agent_estimated_cost,
|
||
"budget_warning": is_cost_budget_warning(
|
||
agent_input_tokens,
|
||
agent_output_tokens,
|
||
agent_estimated_cost,
|
||
thresholds,
|
||
),
|
||
}
|
||
state["cost_by_agent"] = cost_by_agent
|
||
|
||
_append_event_trace(
|
||
state,
|
||
"agent.cost.updated",
|
||
payload={
|
||
"agent_id": agent_id,
|
||
"input_tokens_delta": input_tokens,
|
||
"output_tokens_delta": output_tokens,
|
||
"input_tokens": total_input_tokens,
|
||
"output_tokens": total_output_tokens,
|
||
"estimated_cost": state.get("estimated_cost"),
|
||
"budget_warning": budget_warning,
|
||
},
|
||
)
|
||
if budget_warning and not previous_budget_warning:
|
||
_append_event_trace(
|
||
state,
|
||
"agent.cost.warning",
|
||
payload={
|
||
"thresholds": thresholds,
|
||
"input_tokens": total_input_tokens,
|
||
"output_tokens": total_output_tokens,
|
||
"estimated_cost": state.get("estimated_cost"),
|
||
},
|
||
severity="warning",
|
||
)
|
||
|
||
|
||
def _role_values() -> set[str]:
|
||
return {role.value for role in AgentRole}
|
||
|
||
|
||
def _summary_state_key(
|
||
target: str,
|
||
) -> Literal["schedule_context_summary", "knowledge_context", "analysis_report"]:
|
||
if target not in {"schedule_context_summary", "knowledge_context", "analysis_report"}:
|
||
raise ValueError(f"unsupported summary target: {target}")
|
||
return cast(Literal["schedule_context_summary", "knowledge_context", "analysis_report"], target)
|
||
|
||
|
||
def _ensure_message_thread(state: AgentState) -> str:
|
||
thread_id = str(state.get("thread_id") or "").strip()
|
||
if thread_id:
|
||
return thread_id
|
||
thread_id = f"thread-{uuid4()}"
|
||
state["thread_id"] = thread_id
|
||
return thread_id
|
||
|
||
|
||
def _bump_message_sequence(state: AgentState) -> tuple[str, int]:
|
||
sequence = int(state.get("message_sequence") or 0) + 1
|
||
state["message_sequence"] = sequence
|
||
message_id = f"msg-{sequence}"
|
||
state["last_message_id"] = message_id
|
||
return message_id, sequence
|
||
|
||
|
||
def _task_payload(task: AgentTask | dict[str, Any] | None) -> dict[str, Any]:
|
||
if isinstance(task, AgentTask):
|
||
return task.model_dump(mode="json")
|
||
return dict(task or {})
|
||
|
||
|
||
def _budget_snapshot_from_state(
|
||
state: AgentState,
|
||
*,
|
||
mode: Literal["direct", "collaboration"],
|
||
remaining_parallel_tasks: int | None = None,
|
||
metadata: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
metadata_payload = {
|
||
"max_spawn_depth": 1,
|
||
"max_child_agents": remaining_parallel_tasks,
|
||
"max_messages_per_thread": 24,
|
||
"max_messages_per_turn": 8,
|
||
"max_parallel_collaborators": remaining_parallel_tasks,
|
||
"recovery_attempt_limit": _get_state_int(state, "max_retries") or 1,
|
||
"current_depth": _get_state_int(state, "collaboration_depth"),
|
||
**(metadata or {}),
|
||
}
|
||
budget = CollaborationBudget(
|
||
mode=mode,
|
||
max_parallel_tasks=remaining_parallel_tasks,
|
||
remaining_parallel_tasks=remaining_parallel_tasks,
|
||
max_tool_calls=_get_state_int(state, "max_tool_rounds") or None,
|
||
remaining_tool_calls=max(
|
||
_get_state_int(state, "max_tool_rounds") - _get_state_int(state, "tool_round_count"), 0
|
||
)
|
||
if _get_state_int(state, "max_tool_rounds")
|
||
else None,
|
||
max_iterations=_get_state_int(state, "max_iterations") or None,
|
||
remaining_iterations=max(
|
||
_get_state_int(state, "max_iterations") - _get_state_int(state, "iteration_count"), 0
|
||
)
|
||
if _get_state_int(state, "max_iterations")
|
||
else None,
|
||
metadata=metadata_payload,
|
||
)
|
||
return budget.model_dump(mode="json")
|
||
|
||
|
||
def _store_budget_snapshot(state: AgentState, budget_snapshot: dict[str, Any]) -> None:
|
||
state["budget_state"] = budget_snapshot
|
||
state["collaboration_budget_history"] = [
|
||
*(state.get("collaboration_budget_history") or []),
|
||
budget_snapshot,
|
||
]
|
||
|
||
|
||
def _budget_metadata(state: AgentState) -> dict[str, Any]:
|
||
budget_state = state.get("budget_state") or {}
|
||
if isinstance(budget_state, CollaborationBudget):
|
||
return dict(budget_state.metadata or {})
|
||
if isinstance(budget_state, dict):
|
||
return dict(budget_state.get("metadata") or {})
|
||
return {}
|
||
|
||
|
||
def _budget_limit(state: AgentState, key: str, default: int) -> int:
|
||
value = _budget_metadata(state).get(key)
|
||
return value if isinstance(value, int) and value > 0 else default
|
||
|
||
|
||
def _append_message_trace(
|
||
state: AgentState,
|
||
*,
|
||
from_agent_id: str,
|
||
to_agent_id: str,
|
||
message_type: str,
|
||
content_summary: str,
|
||
task_id: str | None = None,
|
||
reply_to_message_id: str | None = None,
|
||
payload: dict[str, Any] | None = None,
|
||
message_id: str | None = None,
|
||
message_index: int | None = None,
|
||
) -> dict[str, Any]:
|
||
thread_id = _ensure_message_thread(state)
|
||
if message_id is None or message_index is None:
|
||
message_id, message_index = _bump_message_sequence(state)
|
||
message = AgentMessage(
|
||
message_id=message_id,
|
||
thread_id=thread_id,
|
||
from_agent_id=from_agent_id,
|
||
to_agent_id=to_agent_id,
|
||
task_id=task_id,
|
||
reply_to_message_id=reply_to_message_id,
|
||
message_type=cast(Any, message_type),
|
||
content_summary=content_summary[:400],
|
||
payload=payload or {},
|
||
)
|
||
serialized = message.model_dump(mode="json")
|
||
state["message_trace"] = [*(state.get("message_trace") or []), serialized]
|
||
return serialized
|
||
|
||
|
||
def _create_child_agent(
|
||
state: AgentState,
|
||
*,
|
||
role: AgentRole,
|
||
task: AgentTask,
|
||
) -> str | None:
|
||
parent_agent_id = str(state.get("agent_id") or AgentRole.MASTER.value)
|
||
if not _spawn_permission_for_role(state, role):
|
||
_append_event_trace(
|
||
state,
|
||
"agent.spawn.blocked",
|
||
payload={"reason": "role_policy_blocked", "target_role": role.value},
|
||
severity="warning",
|
||
task_id=task.task_id,
|
||
parent_task_id=task.parent_task_id,
|
||
child_task_id=task.task_id,
|
||
)
|
||
return None
|
||
current_depth = _get_state_int(state, "collaboration_depth")
|
||
if current_depth >= _budget_limit(state, "max_spawn_depth", 1):
|
||
_append_event_trace(
|
||
state,
|
||
"agent.spawn.blocked",
|
||
payload={
|
||
"reason": "max_spawn_depth_exceeded",
|
||
"target_role": role.value,
|
||
"depth": current_depth,
|
||
},
|
||
severity="warning",
|
||
task_id=task.task_id,
|
||
parent_task_id=task.parent_task_id,
|
||
child_task_id=task.task_id,
|
||
)
|
||
return None
|
||
spawned_agent_ids = list(state.get("spawned_agent_ids") or [])
|
||
if len(spawned_agent_ids) >= _budget_limit(state, "max_child_agents", 4):
|
||
_append_event_trace(
|
||
state,
|
||
"agent.spawn.blocked",
|
||
payload={"reason": "max_child_agents_exceeded", "target_role": role.value},
|
||
severity="warning",
|
||
task_id=task.task_id,
|
||
parent_task_id=task.parent_task_id,
|
||
child_task_id=task.task_id,
|
||
)
|
||
return None
|
||
child_agent_id = f"{role.value}-{uuid4().hex[:8]}"
|
||
state["spawned_agent_ids"] = [*spawned_agent_ids, child_agent_id]
|
||
_append_event_trace(
|
||
state,
|
||
"agent.created",
|
||
payload={
|
||
"parent_agent_id": parent_agent_id,
|
||
"child_agent_id": child_agent_id,
|
||
"target_role": role.value,
|
||
},
|
||
task_id=task.task_id,
|
||
parent_task_id=task.parent_task_id,
|
||
child_task_id=task.task_id,
|
||
)
|
||
return child_agent_id
|
||
|
||
|
||
def _collaboration_task_from_state(state: AgentState) -> dict[str, Any]:
|
||
return dict((state.get("turn_context") or {}).get("collaboration_task") or {})
|
||
|
||
|
||
def _record_interrupt(
|
||
state: AgentState,
|
||
*,
|
||
reason: str,
|
||
task: AgentTask | dict[str, Any] | None = None,
|
||
payload: dict[str, Any] | None = None,
|
||
) -> InterruptRecord:
|
||
task_payload = (
|
||
_task_payload(task) if task is not None else _collaboration_task_from_state(state)
|
||
)
|
||
interrupt = InterruptRecord(
|
||
interrupt_id=f"interrupt-{uuid4()}",
|
||
reason=reason,
|
||
requested_by=_role_value(state.get("current_agent")),
|
||
payload=payload or {},
|
||
)
|
||
interrupt_payload = interrupt.model_dump(mode="json")
|
||
state["interrupted_tasks"] = [*(state.get("interrupted_tasks") or []), interrupt_payload]
|
||
state["recovery_points"] = [
|
||
*(state.get("recovery_points") or []),
|
||
{
|
||
"interrupt_id": interrupt.interrupt_id,
|
||
"task_id": task_payload.get("task_id"),
|
||
"thread_id": state.get("thread_id"),
|
||
"message_id": state.get("last_message_id"),
|
||
},
|
||
]
|
||
_append_event_trace(
|
||
state,
|
||
"agent.interrupt.requested",
|
||
payload={"reason": reason, **(payload or {})},
|
||
severity="warning",
|
||
task_id=task_payload.get("task_id"),
|
||
parent_task_id=task_payload.get("parent_task_id"),
|
||
child_task_id=(task_payload.get("child_task_ids") or [None])[0],
|
||
interrupt_id=interrupt.interrupt_id,
|
||
)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.task.interrupted",
|
||
payload={"reason": reason, **(payload or {})},
|
||
severity="warning",
|
||
task_id=task_payload.get("task_id"),
|
||
parent_task_id=task_payload.get("parent_task_id"),
|
||
child_task_id=(task_payload.get("child_task_ids") or [None])[0],
|
||
interrupt_id=interrupt.interrupt_id,
|
||
)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.interrupt.completed",
|
||
payload={"reason": reason, **(payload or {})},
|
||
severity="warning",
|
||
task_id=task_payload.get("task_id"),
|
||
parent_task_id=task_payload.get("parent_task_id"),
|
||
child_task_id=(task_payload.get("child_task_ids") or [None])[0],
|
||
interrupt_id=interrupt.interrupt_id,
|
||
)
|
||
return interrupt
|
||
|
||
|
||
def _record_recovery(
|
||
state: AgentState,
|
||
*,
|
||
interrupt: InterruptRecord,
|
||
strategy: str,
|
||
task: AgentTask | dict[str, Any] | None = None,
|
||
payload: dict[str, Any] | None = None,
|
||
) -> RecoveryRecord:
|
||
task_payload = (
|
||
_task_payload(task) if task is not None else _collaboration_task_from_state(state)
|
||
)
|
||
recovery = RecoveryRecord(
|
||
recovery_id=f"recovery-{uuid4()}",
|
||
source_interrupt_id=interrupt.interrupt_id,
|
||
strategy=strategy,
|
||
resumed_from_task_id=task_payload.get("task_id"),
|
||
resumed_from_thread_id=str(state.get("thread_id") or "") or None,
|
||
payload=payload or {},
|
||
)
|
||
recovery_payload = recovery.model_dump(mode="json")
|
||
state["recovery_trace"] = [*(state.get("recovery_trace") or []), recovery_payload]
|
||
_append_event_trace(
|
||
state,
|
||
"agent.recovery.started",
|
||
payload={"strategy": strategy, **(payload or {})},
|
||
task_id=task_payload.get("task_id"),
|
||
parent_task_id=task_payload.get("parent_task_id"),
|
||
child_task_id=(task_payload.get("child_task_ids") or [None])[0],
|
||
recovery_id=recovery.recovery_id,
|
||
interrupt_id=interrupt.interrupt_id,
|
||
)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.task.recovered",
|
||
payload={"strategy": strategy, **(payload or {})},
|
||
task_id=task_payload.get("task_id"),
|
||
parent_task_id=task_payload.get("parent_task_id"),
|
||
child_task_id=(task_payload.get("child_task_ids") or [None])[0],
|
||
recovery_id=recovery.recovery_id,
|
||
interrupt_id=interrupt.interrupt_id,
|
||
)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.recovery.completed",
|
||
payload={"strategy": strategy, **(payload or {})},
|
||
task_id=task_payload.get("task_id"),
|
||
parent_task_id=task_payload.get("parent_task_id"),
|
||
child_task_id=(task_payload.get("child_task_ids") or [None])[0],
|
||
recovery_id=recovery.recovery_id,
|
||
interrupt_id=interrupt.interrupt_id,
|
||
)
|
||
return recovery
|
||
|
||
|
||
def _spawn_permission_for_role(state: AgentState, role: AgentRole) -> bool:
|
||
indexes = load_builtin_registry_indexes()
|
||
current_role_value = _normalize_current_agent(state.get("current_agent"))
|
||
current_manifest = indexes.agent_by_role_value.get(current_role_value)
|
||
if current_manifest is None or not current_manifest.can_spawn_children:
|
||
return False
|
||
allowed_roles = indexes.spawnable_role_values_by_agent_id.get(current_manifest.agent_id, ())
|
||
return role.value in allowed_roles
|
||
|
||
|
||
def _get_llm_for_state(state: AgentState):
|
||
user_llm_config = state.get("user_llm_config")
|
||
llm = create_llm_from_config(user_llm_config) if user_llm_config else get_llm()
|
||
capabilities = getattr(llm, "_jarvis_provider_capabilities", None)
|
||
if capabilities is None:
|
||
capabilities = (
|
||
resolve_provider_capabilities(user_llm_config)
|
||
if user_llm_config
|
||
else default_provider_capabilities()
|
||
)
|
||
state["provider_capabilities"] = {
|
||
"provider": capabilities.provider,
|
||
"supports_native_tools": capabilities.supports_native_tools,
|
||
"preferred_tool_strategy": capabilities.preferred_tool_strategy,
|
||
}
|
||
return llm
|
||
|
||
|
||
def _resolve_capabilities(state: AgentState, llm) -> Any:
|
||
capabilities = getattr(llm, "_jarvis_provider_capabilities", None)
|
||
if capabilities is None:
|
||
config = state.get("user_llm_config")
|
||
capabilities = (
|
||
resolve_provider_capabilities(config) if config else default_provider_capabilities()
|
||
)
|
||
state["provider_capabilities"] = {
|
||
"provider": capabilities.provider,
|
||
"supports_native_tools": capabilities.supports_native_tools,
|
||
"preferred_tool_strategy": capabilities.preferred_tool_strategy,
|
||
}
|
||
return capabilities
|
||
|
||
|
||
def _filter_user_messages(messages: list[BaseMessage]) -> list[BaseMessage]:
|
||
return [message for message in messages if getattr(message, "type", "") in {"human", "user"}]
|
||
|
||
|
||
def _coalesce_system_messages(messages: list[BaseMessage]) -> list[BaseMessage]:
|
||
system_parts: list[str] = []
|
||
non_system_messages: list[BaseMessage] = []
|
||
|
||
for message in messages:
|
||
if getattr(message, "type", "") == "system":
|
||
text = _stringify_message_content(getattr(message, "content", ""))
|
||
if text:
|
||
system_parts.append(text)
|
||
continue
|
||
non_system_messages.append(message)
|
||
|
||
if not system_parts:
|
||
return non_system_messages
|
||
|
||
return [SystemMessage(content="\n\n".join(system_parts)), *non_system_messages]
|
||
|
||
|
||
def _is_simple_greeting(text: str) -> bool:
|
||
return _normalize_user_text(text) in {
|
||
"你好",
|
||
"您好",
|
||
"早",
|
||
"早上好",
|
||
"在吗",
|
||
"嗨",
|
||
"hi",
|
||
"hello",
|
||
}
|
||
|
||
|
||
def _is_identity_question(text: str) -> bool:
|
||
normalized = _normalize_user_text(text)
|
||
return any(normalized.startswith(pattern) for pattern in IDENTITY_PATTERNS)
|
||
|
||
|
||
def _is_capability_question(text: str) -> bool:
|
||
normalized = _normalize_user_text(text)
|
||
return any(normalized.startswith(pattern) for pattern in CAPABILITY_PATTERNS)
|
||
|
||
|
||
def _is_short_confirmation(text: str) -> bool:
|
||
normalized = _normalize_user_text(text)
|
||
return normalized in SHORT_CONFIRMATION_PATTERNS
|
||
|
||
|
||
def _tool_result_indicates_failure(result: Any) -> bool:
|
||
text = _stringify_message_content(result)
|
||
return "失败" in text
|
||
|
||
|
||
def _latest_assistant_message_content(messages: list[BaseMessage]) -> str:
|
||
previous_assistant_message = next(
|
||
(message for message in reversed(messages[:-1]) if getattr(message, "type", "") == "ai"),
|
||
None,
|
||
)
|
||
if previous_assistant_message is None:
|
||
return ""
|
||
return _stringify_message_content(getattr(previous_assistant_message, "content", ""))
|
||
|
||
|
||
def _previous_turn_proposed_schedule_creation(messages: list[BaseMessage]) -> bool:
|
||
if len(messages) < 2:
|
||
return False
|
||
content = _latest_assistant_message_content(messages)
|
||
has_schedule_confirmation_hint = any(hint in content for hint in SCHEDULE_CONFIRMATION_HINTS)
|
||
has_question_marker = any(
|
||
marker in content for marker in SCHEDULE_CONFIRMATION_QUESTION_MARKERS
|
||
)
|
||
return has_schedule_confirmation_hint and has_question_marker
|
||
|
||
|
||
def _previous_turn_completed_reminder_creation(state: AgentState) -> bool:
|
||
created_entities = state.get("created_entities") or []
|
||
if not created_entities:
|
||
return False
|
||
latest_entity = created_entities[-1]
|
||
if latest_entity.get("type") != "reminder":
|
||
return False
|
||
tool_calls = state.get("tool_calls") or []
|
||
if not tool_calls:
|
||
return False
|
||
latest_tool_call = tool_calls[-1]
|
||
if latest_tool_call.get("name") != "create_reminder":
|
||
return False
|
||
messages = state.get("messages") or []
|
||
if len(messages) < 2:
|
||
return False
|
||
if getattr(messages[-1], "type", "") not in {"human", "user"}:
|
||
return False
|
||
previous_message = messages[-2]
|
||
if getattr(previous_message, "type", "") != "ai":
|
||
return False
|
||
previous_assistant_content = _stringify_message_content(
|
||
getattr(previous_message, "content", "")
|
||
)
|
||
completion_markers = ("已创建提醒", "提醒已经创建好了", "帮你设好了这条提醒", "创建成功")
|
||
return any(marker in previous_assistant_content for marker in completion_markers)
|
||
|
||
|
||
def _latest_non_confirmation_user_request(messages: list[BaseMessage]) -> str | None:
|
||
user_messages = [
|
||
_stringify_message_content(getattr(message, "content", "")).strip()
|
||
for message in messages
|
||
if getattr(message, "type", "") in {"human", "user"}
|
||
]
|
||
for content in reversed(user_messages[:-1]):
|
||
if content and not _is_short_confirmation(content):
|
||
return content
|
||
return None
|
||
|
||
|
||
def _expand_schedule_confirmation_query(user_query: str, messages: list[BaseMessage]) -> str:
|
||
previous_request = _latest_non_confirmation_user_request(messages)
|
||
if not previous_request:
|
||
return user_query
|
||
return f"用户确认继续创建上一条提醒安排:{previous_request}"
|
||
|
||
|
||
def _is_schedule_creation_confirmation_response(response_text: str) -> bool:
|
||
content = _stringify_message_content(response_text)
|
||
has_schedule_confirmation_hint = any(hint in content for hint in SCHEDULE_CONFIRMATION_HINTS)
|
||
has_question_marker = any(
|
||
marker in content for marker in SCHEDULE_CONFIRMATION_QUESTION_MARKERS
|
||
)
|
||
return has_schedule_confirmation_hint and has_question_marker
|
||
|
||
|
||
def _write_schedule_creation_continuity(state: AgentState, user_query: str) -> None:
|
||
summary = user_query.strip()
|
||
if not summary:
|
||
return
|
||
state["pending_action"] = {
|
||
"type": "schedule_creation",
|
||
"summary": summary,
|
||
"status": "pending",
|
||
}
|
||
state["routing_decision"] = {
|
||
"target_agent": AgentRole.SCHEDULE_PLANNER.value,
|
||
"reason": "continue_pending_action",
|
||
}
|
||
state["continuity_state"] = {"status": "fresh"}
|
||
|
||
|
||
def _clear_structured_continuity(state: AgentState) -> None:
|
||
state["pending_action"] = None
|
||
state["routing_decision"] = None
|
||
state["continuity_state"] = None
|
||
|
||
|
||
def _should_clear_schedule_creation_continuity(
|
||
state: AgentState, created_entities: list[dict[str, Any]]
|
||
) -> bool:
|
||
if not _has_active_structured_continuation(state):
|
||
return False
|
||
pending_action = state.get("pending_action") or {}
|
||
if pending_action.get("type") != "schedule_creation":
|
||
return False
|
||
return any(entity.get("type") == "reminder" for entity in created_entities)
|
||
|
||
|
||
def _classify_request_signals(user_query: str) -> dict[str, Any]:
|
||
text = (user_query or "").strip().lower()
|
||
has_accounting_signal = any(keyword in text for keyword in ACCOUNTING_INTENT_KEYWORDS)
|
||
has_schedule_signal = not has_accounting_signal and (
|
||
bool(re.search(r"\d{1,2}月\d{1,2}日", text))
|
||
or any(keyword in text for keyword in SCHEDULE_KEYWORDS)
|
||
)
|
||
has_analysis_signal = any(keyword in text for keyword in ANALYSIS_KEYWORDS)
|
||
has_knowledge_signal = any(keyword in text for keyword in KNOWLEDGE_KEYWORDS)
|
||
has_execution_signal = has_accounting_signal or any(
|
||
keyword in text for keyword in EXECUTION_KEYWORDS
|
||
)
|
||
|
||
roles: list[AgentRole] = []
|
||
if has_knowledge_signal:
|
||
roles.append(AgentRole.LIBRARIAN)
|
||
if has_analysis_signal:
|
||
roles.append(AgentRole.ANALYST)
|
||
if has_schedule_signal:
|
||
roles.append(AgentRole.SCHEDULE_PLANNER)
|
||
if has_execution_signal:
|
||
roles.append(AgentRole.EXECUTOR)
|
||
|
||
return {
|
||
"text": text,
|
||
"has_accounting_signal": has_accounting_signal,
|
||
"has_schedule_signal": has_schedule_signal,
|
||
"has_analysis_signal": has_analysis_signal,
|
||
"has_knowledge_signal": has_knowledge_signal,
|
||
"has_execution_signal": has_execution_signal,
|
||
"roles": roles,
|
||
}
|
||
|
||
|
||
def _select_request_mode(
|
||
user_query: str,
|
||
) -> tuple[Literal["direct", "collaboration"], dict[str, Any]]:
|
||
signals = _classify_request_signals(user_query)
|
||
roles: list[AgentRole] = signals["roles"]
|
||
text = signals["text"]
|
||
has_multi_step_signal = any(marker in text for marker in COLLABORATION_STEP_MARKERS)
|
||
is_explicit_collaboration_request = "协作" in text or ("拆" in text and "任务" in text)
|
||
is_long_request = len((user_query or "").strip()) >= 24
|
||
|
||
should_collaborate = len(roles) >= 3 or (
|
||
len(roles) >= 2
|
||
and (has_multi_step_signal or is_explicit_collaboration_request or is_long_request)
|
||
)
|
||
selected_mode: Literal["direct", "collaboration"] = (
|
||
"collaboration" if should_collaborate else "direct"
|
||
)
|
||
metadata = {
|
||
"mode": selected_mode,
|
||
"reason": "multi_role_request" if should_collaborate else "single_role_or_simple_request",
|
||
"roles": [role.value for role in roles],
|
||
"multi_step": has_multi_step_signal,
|
||
}
|
||
return selected_mode, metadata
|
||
|
||
|
||
def _route_agent_from_user_query(user_query: str) -> AgentRole:
|
||
signals = _classify_request_signals(user_query)
|
||
text = signals["text"]
|
||
|
||
# Code Commander routing - check first as it has specific keywords
|
||
if any(keyword in text for keyword in CODE_COMMANDER_KEYWORDS):
|
||
return AgentRole.CODE_COMMANDER
|
||
|
||
if signals["has_accounting_signal"]:
|
||
return AgentRole.EXECUTOR
|
||
|
||
if signals["has_schedule_signal"]:
|
||
return AgentRole.SCHEDULE_PLANNER
|
||
|
||
if signals["has_analysis_signal"]:
|
||
return AgentRole.ANALYST
|
||
|
||
if signals["has_knowledge_signal"]:
|
||
return AgentRole.LIBRARIAN
|
||
|
||
if any(pattern in text for pattern in GENERAL_QA_PATTERNS):
|
||
return AgentRole.MASTER
|
||
|
||
if signals["has_execution_signal"]:
|
||
return AgentRole.EXECUTOR
|
||
return AgentRole.MASTER
|
||
|
||
|
||
def _choose_sub_commander(role: AgentRole, user_query: str) -> str:
|
||
text = (user_query or "").strip().lower()
|
||
|
||
if role == AgentRole.SCHEDULE_PLANNER:
|
||
if re.search(r"\d{1,2}月\d{1,2}日", text) or any(
|
||
keyword in text for keyword in SCHEDULE_PLANNING_KEYWORDS
|
||
):
|
||
return "schedule_planning"
|
||
return "schedule_analysis"
|
||
if role == AgentRole.EXECUTOR:
|
||
if any(keyword in text for keyword in ("论坛", "帖子", "发帖", "指令")):
|
||
return "executor_forum"
|
||
return "executor_tasks"
|
||
if role == AgentRole.LIBRARIAN:
|
||
if any(keyword in text for keyword in ("图谱", "关系", "沉淀", "graph")):
|
||
return "librarian_graph"
|
||
return "librarian_retrieval"
|
||
if role == AgentRole.ANALYST:
|
||
if any(keyword in text for keyword in ("趋势", "风险", "洞察", "建议", "机会")):
|
||
return "analyst_insights"
|
||
return "analyst_progress"
|
||
raise ValueError(f"unsupported role: {role}")
|
||
|
||
|
||
def _is_missing_knowledge_result(tool_result: str | None) -> bool:
|
||
text = (tool_result or "").strip()
|
||
if not text:
|
||
return True
|
||
markers = (
|
||
"未找到相关知识",
|
||
"知识库可能为空",
|
||
"未找到相关网页结果",
|
||
"暂无相关记录",
|
||
"没有找到",
|
||
)
|
||
return any(marker in text for marker in markers)
|
||
|
||
|
||
def _extract_json_object(content: str) -> str | None:
|
||
text = (content or "").strip()
|
||
if not text:
|
||
return None
|
||
|
||
fenced_match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL | re.IGNORECASE)
|
||
if fenced_match:
|
||
return fenced_match.group(1)
|
||
|
||
decoder = json.JSONDecoder()
|
||
for index, char in enumerate(text):
|
||
if char != "{":
|
||
continue
|
||
try:
|
||
_, end = decoder.raw_decode(text[index:])
|
||
return text[index : index + end]
|
||
except json.JSONDecodeError:
|
||
continue
|
||
return None
|
||
|
||
|
||
def _parse_json_action(content: str, allowed_tools: list[str]) -> dict[str, Any] | None:
|
||
json_text = _extract_json_object(_stringify_message_content(content))
|
||
if not json_text:
|
||
return None
|
||
|
||
try:
|
||
payload = json.loads(json_text)
|
||
except json.JSONDecodeError:
|
||
return None
|
||
|
||
if not isinstance(payload, dict):
|
||
return None
|
||
|
||
mode = payload.get("mode")
|
||
if mode == "tool_call":
|
||
tool_calls: list[dict[str, Any]] = []
|
||
raw_tool_calls = payload.get("tool_calls", [])
|
||
if not isinstance(raw_tool_calls, list):
|
||
return None
|
||
for item in raw_tool_calls:
|
||
if not isinstance(item, dict):
|
||
return None
|
||
name = item.get("name")
|
||
if name not in allowed_tools:
|
||
return None
|
||
args = item.get("arguments")
|
||
if args is None:
|
||
args = item.get("parameters")
|
||
if not isinstance(args, dict):
|
||
return None
|
||
tool_calls.append(
|
||
{
|
||
"name": name,
|
||
"args": args,
|
||
"reason": item.get("reason"),
|
||
}
|
||
)
|
||
return {"mode": mode, "tool_calls": tool_calls}
|
||
|
||
if mode == "final" and isinstance(payload.get("final_response"), str):
|
||
return {"mode": mode, "final_response": payload["final_response"]}
|
||
|
||
if mode == "clarification" and isinstance(payload.get("clarification_question"), str):
|
||
return {"mode": mode, "clarification_question": payload["clarification_question"]}
|
||
|
||
return None
|
||
|
||
|
||
def _looks_like_internal_tool_markup(content: str) -> bool:
|
||
text = (content or "").strip()
|
||
if not text:
|
||
return False
|
||
|
||
lowered = text.lower()
|
||
xml_markers = (
|
||
"<minimax:tool_call",
|
||
"</minimax:tool_call>",
|
||
"<invoke name=",
|
||
"</invoke>",
|
||
"<parameter name=",
|
||
"</parameter>",
|
||
)
|
||
if any(marker in lowered for marker in xml_markers):
|
||
return True
|
||
|
||
return "分发说明" in text and ("<invoke name=" in lowered or "tool_call" in lowered)
|
||
|
||
|
||
def _clean_tool_result_for_user(tool_result: str | None) -> str:
|
||
text = (tool_result or "").strip()
|
||
if not text:
|
||
return ""
|
||
|
||
cleaned_lines = [
|
||
re.sub(r"^\[[^\]]+\]\s*", "", line).strip()
|
||
for line in text.splitlines()
|
||
if line.strip()
|
||
]
|
||
return "\n".join(line for line in cleaned_lines if line).strip()
|
||
|
||
|
||
def _build_internal_markup_fallback_response(
|
||
state: AgentState,
|
||
*,
|
||
sub_commander: str,
|
||
) -> str | None:
|
||
tool_result = state.get("last_tool_result")
|
||
cleaned_tool_result = _clean_tool_result_for_user(tool_result)
|
||
if not cleaned_tool_result or _tool_result_indicates_failure(cleaned_tool_result):
|
||
return None
|
||
|
||
if sub_commander == "librarian_retrieval":
|
||
if _is_missing_knowledge_result(tool_result):
|
||
return "这次检索没有拿到有效证据。我先不展示内部调度过程;如果您愿意,我可以直接基于常识回答,或改为联网搜索后再整理。"
|
||
return f"我已经完成检索,直接给您可用信息:\n\n{cleaned_tool_result}"
|
||
|
||
return cleaned_tool_result
|
||
|
||
|
||
def _has_active_structured_continuation(state: AgentState) -> bool:
|
||
pending_action = state.get("pending_action") or {}
|
||
routing_decision = state.get("routing_decision") or {}
|
||
continuity_state = state.get("continuity_state") or {}
|
||
|
||
if continuity_state.get("status") != "fresh":
|
||
return False
|
||
if pending_action.get("status") != "pending":
|
||
return False
|
||
if routing_decision.get("reason") != "continue_pending_action":
|
||
return False
|
||
target_agent = routing_decision.get("target_agent")
|
||
return target_agent in _role_values()
|
||
|
||
|
||
def _route_from_structured_continuity(state: AgentState, user_query: str) -> AgentRole | None:
|
||
if not _is_short_confirmation(user_query):
|
||
return None
|
||
if not _has_active_structured_continuation(state):
|
||
return None
|
||
|
||
target_agent = (state.get("routing_decision") or {}).get("target_agent")
|
||
|
||
try:
|
||
return AgentRole(str(target_agent))
|
||
except ValueError:
|
||
return None
|
||
|
||
|
||
def _build_structured_continuity_summary(state: AgentState) -> str | None:
|
||
pending_action = state.get("pending_action")
|
||
routing_decision = state.get("routing_decision")
|
||
|
||
if not pending_action or not routing_decision:
|
||
return None
|
||
if not _has_active_structured_continuation(state):
|
||
return None
|
||
|
||
action_type = str(pending_action.get("type") or "unknown")
|
||
action_summary = str(pending_action.get("summary") or "")
|
||
routing_reason = str(routing_decision.get("reason") or "")
|
||
target_agent = str(routing_decision.get("target_agent") or "")
|
||
|
||
lines = [
|
||
"structured_continuity:",
|
||
f"- pending_action.type: {action_type}",
|
||
]
|
||
if action_summary:
|
||
lines.append(f"- pending_action.summary: {action_summary}")
|
||
if target_agent:
|
||
lines.append(f"- routing_decision.target_agent: {target_agent}")
|
||
if routing_reason:
|
||
lines.append(f"- routing_decision.reason: {routing_reason}")
|
||
lines.append("- instruction: continue_pending_action")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _build_retrospective_context_summary(state: AgentState) -> str | None:
|
||
retrospectives = list(state.get("recalled_retrospectives") or [])
|
||
if not retrospectives:
|
||
return None
|
||
|
||
lines = ["【相关历史复盘】"]
|
||
for item in retrospectives[:2]:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
request_summary = str(item.get("request_summary") or item.get("task_type") or "").strip()
|
||
execution_mode = str(item.get("execution_mode") or "").strip()
|
||
success_score = float(item.get("success_score") or 0.0)
|
||
reusable_patterns = list(item.get("reusable_patterns") or [])
|
||
avoid_patterns = list(item.get("avoid_patterns") or [])
|
||
|
||
summary_parts = [request_summary[:80] or execution_mode or "历史任务"]
|
||
if execution_mode:
|
||
summary_parts.append(f"mode={execution_mode}")
|
||
summary_parts.append(f"score={success_score:.2f}")
|
||
if reusable_patterns:
|
||
summary_parts.append(f"可复用={','.join(reusable_patterns[:2])}")
|
||
elif avoid_patterns:
|
||
summary_parts.append(f"避坑={','.join(avoid_patterns[:2])}")
|
||
|
||
lines.append(f"- {';'.join(summary_parts)}")
|
||
|
||
return "\n".join(lines) if len(lines) > 1 else None
|
||
|
||
|
||
def _estimate_request_complexity(user_query: str, selected_roles: list[str]) -> float:
|
||
text = (user_query or "").strip()
|
||
base = min(len(text) / 120.0, 1.0)
|
||
role_boost = min(len(selected_roles) * 0.2, 0.6)
|
||
return round(min(base + role_boost, 1.0), 2)
|
||
|
||
|
||
def _record_execution_decision(
|
||
state: AgentState,
|
||
*,
|
||
user_query: str,
|
||
mode: Literal["direct", "collaboration", "parallel"],
|
||
reason: str,
|
||
selected_roles: list[str] | None = None,
|
||
parallel_worthiness_score: float | None = None,
|
||
) -> None:
|
||
runtime_request_context = state.get("runtime_request_context") or {}
|
||
request_id = str(runtime_request_context.get("request_id") or state.get("conversation_id") or "")
|
||
roles = list(selected_roles or [])
|
||
decision = ExecutionDecision(
|
||
request_id=request_id or f"request-{uuid4().hex[:8]}",
|
||
mode=mode,
|
||
reason=reason,
|
||
complexity_score=_estimate_request_complexity(user_query, roles),
|
||
parallel_worthiness_score=parallel_worthiness_score,
|
||
selected_roles=roles,
|
||
)
|
||
state["execution_decision"] = decision.model_dump(mode="json")
|
||
_append_event_trace(
|
||
state,
|
||
"agent.execution.decided",
|
||
payload=state["execution_decision"],
|
||
)
|
||
|
||
|
||
def _build_system_messages(
|
||
state: AgentState, system_prompt: str, role: AgentRole, sub_commander: str
|
||
) -> list[BaseMessage]:
|
||
messages: list[BaseMessage] = [SystemMessage(content=system_prompt)]
|
||
|
||
current_datetime_context = state.get("current_datetime_context")
|
||
if current_datetime_context:
|
||
messages.append(SystemMessage(content=current_datetime_context))
|
||
|
||
runtime_request_context = state.get("runtime_request_context")
|
||
if isinstance(runtime_request_context, dict) and runtime_request_context:
|
||
try:
|
||
runtime_context_model = RuntimeRequestContext.model_validate(runtime_request_context)
|
||
except Exception:
|
||
runtime_context_model = None
|
||
if runtime_context_model is not None:
|
||
messages.append(
|
||
SystemMessage(
|
||
content=render_runtime_request_context_summary(runtime_context_model)
|
||
)
|
||
)
|
||
|
||
continuity_summary = _build_structured_continuity_summary(state)
|
||
if continuity_summary:
|
||
messages.append(SystemMessage(content=continuity_summary))
|
||
|
||
clarification_summary = _build_clarification_summary(state)
|
||
if clarification_summary:
|
||
messages.append(SystemMessage(content=clarification_summary))
|
||
|
||
collaboration_summary = _build_collaboration_context_summary(state)
|
||
if collaboration_summary:
|
||
messages.append(SystemMessage(content=collaboration_summary))
|
||
|
||
retrospective_summary = _build_retrospective_context_summary(state)
|
||
if retrospective_summary:
|
||
messages.append(SystemMessage(content=retrospective_summary))
|
||
|
||
role_context_map = {
|
||
AgentRole.SCHEDULE_PLANNER: state.get("schedule_context_summary"),
|
||
AgentRole.LIBRARIAN: state.get("knowledge_context"),
|
||
AgentRole.ANALYST: state.get("analysis_report"),
|
||
}
|
||
role_context = role_context_map.get(role)
|
||
if role_context:
|
||
messages.append(SystemMessage(content=f"角色上下文:\n{role_context}"))
|
||
|
||
role_skill_key = ROLE_SKILL_CONTEXT.get(role)
|
||
if role_skill_key:
|
||
shortlisted_context = build_shortlisted_skill_context(
|
||
state.get("skill_shortlist"),
|
||
agent_type=role_skill_key,
|
||
)
|
||
skill_context = shortlisted_context or build_skill_context(role_skill_key)
|
||
if skill_context:
|
||
messages.append(SystemMessage(content=skill_context))
|
||
|
||
messages.append(SystemMessage(content=f"本次应由子指挥官 `{sub_commander}` 接手。"))
|
||
messages.append(SystemMessage(content=SUB_COMMANDER_PROMPTS[sub_commander]))
|
||
return messages
|
||
|
||
|
||
def _assign_agent_for_task_role(role_value: str | None) -> AgentRole:
|
||
try:
|
||
return AgentRole(str(role_value or AgentRole.MASTER.value))
|
||
except ValueError:
|
||
return AgentRole.MASTER
|
||
|
||
|
||
def _build_task_title(role: AgentRole) -> str:
|
||
titles = {
|
||
AgentRole.LIBRARIAN: "补齐事实与证据",
|
||
AgentRole.ANALYST: "给出分析与判断",
|
||
AgentRole.SCHEDULE_PLANNER: "形成安排与计划",
|
||
AgentRole.EXECUTOR: "执行必要动作",
|
||
}
|
||
return titles.get(role, "处理协作任务")
|
||
|
||
|
||
def _build_task_goal(role: AgentRole, user_query: str) -> str:
|
||
goals = {
|
||
AgentRole.LIBRARIAN: f"围绕用户请求检索并整理能直接支撑结论的证据:{user_query}",
|
||
AgentRole.ANALYST: f"基于已有上下文与证据,对用户请求给出结论、风险和建议:{user_query}",
|
||
AgentRole.SCHEDULE_PLANNER: f"把用户请求收束为明确安排、节奏或日程建议:{user_query}",
|
||
AgentRole.EXECUTOR: f"基于用户请求和前序结论执行必要动作,并回收结果:{user_query}",
|
||
}
|
||
return goals.get(role, user_query)
|
||
|
||
|
||
def _build_expected_evidence(role: AgentRole) -> list[dict[str, Any]]:
|
||
evidence_map = {
|
||
AgentRole.LIBRARIAN: [
|
||
{"type": "evidence", "detail": "retrieval findings or source-backed context"}
|
||
],
|
||
AgentRole.ANALYST: [{"type": "analysis", "detail": "judgment with supporting rationale"}],
|
||
AgentRole.SCHEDULE_PLANNER: [
|
||
{"type": "plan", "detail": "explicit schedule or next-step proposal"}
|
||
],
|
||
AgentRole.EXECUTOR: [
|
||
{"type": "execution", "detail": "tool output or execution confirmation"}
|
||
],
|
||
}
|
||
return evidence_map.get(role, [{"type": "summary", "detail": "task evidence"}])
|
||
|
||
|
||
def _build_collaboration_tasks(user_query: str) -> list[AgentTask]:
|
||
_, metadata = _select_request_mode(user_query)
|
||
raw_roles = metadata.get("roles") or []
|
||
roles = sorted(
|
||
(_assign_agent_for_task_role(role_value) for role_value in raw_roles),
|
||
key=lambda role: COLLABORATION_ROLE_ORDER.get(role, 99),
|
||
)
|
||
unique_roles: list[AgentRole] = []
|
||
for role in roles:
|
||
if role not in unique_roles and role != AgentRole.MASTER:
|
||
unique_roles.append(role)
|
||
|
||
parent_task_id = f"task-root-{uuid4().hex[:8]}"
|
||
task_ids = [
|
||
f"task-{index}-{uuid4().hex[:8]}" for index, _ in enumerate(unique_roles[:4], start=1)
|
||
]
|
||
tasks: list[AgentTask] = []
|
||
for index, role in enumerate(unique_roles[:4]):
|
||
tasks.append(
|
||
AgentTask(
|
||
task_id=task_ids[index],
|
||
title=_build_task_title(role),
|
||
owner_agent_id=role.value,
|
||
role=role.value,
|
||
goal=_build_task_goal(role, user_query),
|
||
parent_task_id=parent_task_id,
|
||
child_task_ids=[task_ids[index + 1]] if index + 1 < len(task_ids) else [],
|
||
expected_evidence=_build_expected_evidence(role),
|
||
)
|
||
)
|
||
return tasks
|
||
|
||
|
||
def _build_collaboration_plan_from_task_graph(
|
||
state: AgentState,
|
||
user_query: str,
|
||
) -> tuple[list[AgentTask], list[dict[str, Any]]]:
|
||
raw_task_graph = state.get("task_graph")
|
||
if not isinstance(raw_task_graph, dict) or not raw_task_graph.get("nodes"):
|
||
return _build_collaboration_tasks(user_query), []
|
||
|
||
task_graph = TaskGraph.model_validate(raw_task_graph)
|
||
specs = [
|
||
spec
|
||
for spec in build_subtask_specs(task_graph, query_text=user_query)
|
||
if spec.role != "master"
|
||
]
|
||
child_links = ensure_child_links(specs)
|
||
tasks: list[AgentTask] = []
|
||
for spec in specs:
|
||
task = subtask_spec_to_agent_task(spec)
|
||
task.child_task_ids = child_links.get(spec.subtask_id, [])
|
||
tasks.append(task)
|
||
return tasks, [spec.model_dump(mode="json") for spec in specs]
|
||
|
||
|
||
def _build_collaboration_context_summary(state: AgentState) -> str | None:
|
||
if state.get("execution_mode") != "collaboration":
|
||
return None
|
||
|
||
lines = [COORDINATOR_SYSTEM_PROMPT]
|
||
current_task = (state.get("turn_context") or {}).get("collaboration_task") or {}
|
||
if current_task:
|
||
lines.extend(
|
||
[
|
||
"current_collaboration_task:",
|
||
f"- task_id: {current_task.get('task_id')}",
|
||
f"- title: {current_task.get('title')}",
|
||
f"- role: {current_task.get('role')}",
|
||
f"- goal: {current_task.get('goal')}",
|
||
]
|
||
)
|
||
|
||
task_results = state.get("task_results") or []
|
||
if task_results:
|
||
lines.append("completed_collaboration_results:")
|
||
for item in task_results[-4:]:
|
||
normalized = (
|
||
item.model_dump(mode="json") if isinstance(item, TaskResult) else dict(item)
|
||
)
|
||
lines.append(
|
||
f"- {normalized.get('task_id')} | {normalized.get('status')} | {normalized.get('summary') or ''}"
|
||
)
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _maybe_reset_turn_budgets(state: AgentState) -> None:
|
||
messages = state.get("messages") or []
|
||
if not messages:
|
||
state["routing_hops"] = 0
|
||
state["terminated_due_to_loop_guard"] = False
|
||
state["iteration_count"] = 0
|
||
state["tool_round_count"] = 0
|
||
state["retry_count"] = 0
|
||
state["stop_reason"] = None
|
||
state["clarification_needed"] = False
|
||
state["clarification_question"] = None
|
||
state["final_response"] = None
|
||
return
|
||
|
||
last_message_type = getattr(messages[-1], "type", "")
|
||
has_prior_assistant_turn = any(
|
||
getattr(message, "type", "") == "ai" for message in messages[:-1]
|
||
)
|
||
if last_message_type in {"human", "user"} and has_prior_assistant_turn:
|
||
state["routing_hops"] = 0
|
||
state["terminated_due_to_loop_guard"] = False
|
||
state["iteration_count"] = 0
|
||
state["tool_round_count"] = 0
|
||
state["retry_count"] = 0
|
||
state["stop_reason"] = None
|
||
state["clarification_needed"] = False
|
||
state["clarification_question"] = None
|
||
state["final_response"] = None
|
||
|
||
|
||
def _conversation_history_messages(state: AgentState) -> list[BaseMessage]:
|
||
history = list(state.get("messages", []))
|
||
return [message for message in history if getattr(message, "type", "") != "system"]
|
||
|
||
|
||
def _append_event_trace(
|
||
state: AgentState,
|
||
event_type: str,
|
||
*,
|
||
payload: dict[str, Any] | None = None,
|
||
severity: str = "info",
|
||
task_id: str | None = None,
|
||
parent_task_id: str | None = None,
|
||
child_task_id: str | None = None,
|
||
interrupt_id: str | None = None,
|
||
recovery_id: str | None = None,
|
||
message_id: str | None = None,
|
||
) -> None:
|
||
thread_id = _ensure_message_thread(state)
|
||
event = AgentEvent(
|
||
event_id=f"evt-{uuid4()}",
|
||
event_type=cast(Any, event_type),
|
||
conversation_id=str(state.get("conversation_id") or "") or None,
|
||
agent_id=_role_value(state.get("current_agent")),
|
||
sub_commander_id=state.get("current_sub_commander"),
|
||
task_id=task_id,
|
||
parent_task_id=parent_task_id,
|
||
child_task_id=child_task_id,
|
||
thread_id=thread_id,
|
||
message_id=message_id or (str(state.get("last_message_id") or "") or None),
|
||
interrupt_id=interrupt_id,
|
||
recovery_id=recovery_id,
|
||
payload=payload or {},
|
||
severity=cast(Any, severity),
|
||
)
|
||
state["event_trace"] = [
|
||
*(state.get("event_trace") or []),
|
||
event.model_dump(mode="json"),
|
||
]
|
||
|
||
|
||
def _set_phase(
|
||
state: AgentState, phase: str, *, reason: str, payload: dict[str, Any] | None = None
|
||
) -> None:
|
||
if state.get("current_phase") == phase:
|
||
return
|
||
state["current_phase"] = phase
|
||
state["phase_history"] = [
|
||
*(state.get("phase_history") or []),
|
||
{
|
||
"phase": phase,
|
||
"reason": reason,
|
||
**({"payload": payload} if payload else {}),
|
||
},
|
||
]
|
||
_append_event_trace(
|
||
state,
|
||
"agent.phase.changed",
|
||
payload={"phase": phase, "reason": reason, **(payload or {})},
|
||
)
|
||
|
||
|
||
def _record_checkpoint(
|
||
state: AgentState, checkpoint: str, *, reason: str, payload: dict[str, Any] | None = None
|
||
) -> None:
|
||
state["current_checkpoint"] = checkpoint
|
||
state["checkpoint_history"] = [
|
||
*(state.get("checkpoint_history") or []),
|
||
{
|
||
"checkpoint": checkpoint,
|
||
"phase": state.get("current_phase"),
|
||
"reason": reason,
|
||
**({"payload": payload} if payload else {}),
|
||
},
|
||
]
|
||
_append_event_trace(
|
||
state,
|
||
"agent.checkpoint.recorded",
|
||
payload={
|
||
"checkpoint": checkpoint,
|
||
"phase": state.get("current_phase"),
|
||
"reason": reason,
|
||
**(payload or {}),
|
||
},
|
||
)
|
||
|
||
|
||
def _capability_manifest_for_tool(tool_name: str):
|
||
indexes = load_builtin_registry_indexes()
|
||
capability_id = indexes.capability_id_by_tool_name.get(tool_name)
|
||
if capability_id is None:
|
||
return None
|
||
return indexes.capability_by_id.get(capability_id)
|
||
|
||
|
||
def _build_verifier_hints(state: AgentState, tool_name: str, result: Any) -> dict[str, Any]:
|
||
capability = _capability_manifest_for_tool(tool_name)
|
||
permission_class = getattr(capability, "permission_class", None)
|
||
side_effect_scope = getattr(capability, "side_effect_scope", None)
|
||
return {
|
||
"tool_name": tool_name,
|
||
"permission_class": getattr(permission_class, "value", None),
|
||
"side_effect_scope": getattr(side_effect_scope, "value", None),
|
||
"requires_confirmation": bool(getattr(capability, "requires_confirmation", False)),
|
||
"supports_retry": bool(getattr(capability, "supports_retry", False)),
|
||
"safe_for_parallel_use": bool(getattr(capability, "safe_for_parallel_use", False)),
|
||
"result_preview": _stringify_message_content(result)[:200],
|
||
}
|
||
|
||
|
||
def _update_task_result_summary(state: AgentState, tool_summaries: list[dict[str, Any]]) -> None:
|
||
if not tool_summaries:
|
||
return
|
||
|
||
previous_summary = state.get("task_result_summary") or {}
|
||
previous_tools = previous_summary.get("tools") or []
|
||
merged_tools = [*previous_tools, *tool_summaries]
|
||
summary = {
|
||
"tool_count": len(merged_tools),
|
||
"tools": merged_tools,
|
||
"created_count": sum(int(item.get("created_count") or 0) for item in merged_tools),
|
||
"created_entity_types": [
|
||
entity_type
|
||
for item in merged_tools
|
||
for entity_type in item.get("created_entity_types") or []
|
||
if entity_type
|
||
],
|
||
"stop_reason": state.get("stop_reason"),
|
||
}
|
||
state["task_result_summary"] = summary
|
||
state["action_results"] = [*(state.get("action_results") or []), summary]
|
||
|
||
|
||
def _record_sub_commander(
|
||
state: AgentState, role: AgentRole, sub_commander: str, user_query: str
|
||
) -> None:
|
||
thread_id = _ensure_message_thread(state)
|
||
message_id, message_index = _bump_message_sequence(state)
|
||
current_agent_id = str(state.get("agent_id") or AgentRole.MASTER.value)
|
||
state["current_agent"] = role.value
|
||
state["current_sub_commander"] = sub_commander
|
||
state["active_agents"] = _normalize_active_agents(state.get("active_agents"))
|
||
if role not in state["active_agents"]:
|
||
state["active_agents"] = [*state["active_agents"], role]
|
||
state["active_sub_commanders"] = [*(state.get("active_sub_commanders") or []), sub_commander]
|
||
state["agent_trace"] = [*(state.get("agent_trace") or []), role.value]
|
||
trace_entry = {
|
||
"agent": _role_value(role),
|
||
"agent_id": current_agent_id,
|
||
"sub_commander": sub_commander,
|
||
"query": user_query,
|
||
"thread_id": thread_id,
|
||
"message_id": message_id,
|
||
"message_index": message_index,
|
||
}
|
||
state["sub_commander_trace"] = [*(state.get("sub_commander_trace") or []), trace_entry]
|
||
state["retrieval_trace"] = [*(state.get("retrieval_trace") or []), trace_entry]
|
||
_append_message_trace(
|
||
state,
|
||
from_agent_id=str(state.get("parent_agent_id") or AgentRole.MASTER.value),
|
||
to_agent_id=current_agent_id,
|
||
message_type="task_request",
|
||
content_summary=user_query,
|
||
task_id=((state.get("turn_context") or {}).get("collaboration_task") or {}).get("task_id"),
|
||
payload={"sub_commander": sub_commander},
|
||
message_id=message_id,
|
||
message_index=message_index,
|
||
)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.message.received",
|
||
payload={"sub_commander": sub_commander, "summary": user_query[:200]},
|
||
task_id=((state.get("turn_context") or {}).get("collaboration_task") or {}).get("task_id"),
|
||
message_id=message_id,
|
||
)
|
||
|
||
|
||
def _stop_sub_commander_due_to_budget(state: AgentState, reason: str) -> None:
|
||
state["stop_reason"] = reason
|
||
state["final_response"] = (
|
||
"这次需要处理的步骤有点多,我先停在这里。您可以把目标再明确一点,或让我先只完成其中一步。"
|
||
)
|
||
|
||
|
||
def _guard_sub_commander_budget(
|
||
state: AgentState, counter_key: str, max_key: str, reason: str
|
||
) -> bool:
|
||
max_value = _get_state_int(state, max_key)
|
||
current_value = _get_state_int(state, counter_key)
|
||
if max_value > 0 and current_value >= max_value:
|
||
_stop_sub_commander_due_to_budget(state, reason)
|
||
return False
|
||
return True
|
||
|
||
|
||
def _classify_created_entity(tool_name: str) -> dict[str, str] | None:
|
||
mapping = {
|
||
"create_reminder": "reminder",
|
||
"create_goal": "goal",
|
||
"create_todo": "todo",
|
||
"create_schedule_task": "task",
|
||
"create_task": "task",
|
||
"create_forum_post": "forum_post",
|
||
}
|
||
entity_type = mapping.get(tool_name)
|
||
if not entity_type:
|
||
return None
|
||
return {"type": entity_type, "tool": tool_name}
|
||
|
||
|
||
def _build_reminder_time_clarification(args: dict[str, Any]) -> dict[str, Any] | None:
|
||
if not args.get("date") or args.get("reminder_at"):
|
||
return None
|
||
title = str(args.get("title") or args.get("content") or "这件事").strip() or "这件事"
|
||
return {
|
||
"question": f"要把“{title}”提醒在几点?如果您不想特地指定,我也可以默认按当天早上 9 点给您设置。",
|
||
"missing_fields": ["reminder_at"],
|
||
"partial_args": args,
|
||
}
|
||
|
||
|
||
def _has_active_clarification_context(state: AgentState) -> bool:
|
||
clarification_context = state.get("clarification_context") or {}
|
||
if not clarification_context:
|
||
return False
|
||
if not clarification_context.get("question"):
|
||
return False
|
||
owning_agent = clarification_context.get("owning_agent")
|
||
return isinstance(owning_agent, str) and owning_agent in _role_values()
|
||
|
||
|
||
def _clear_clarification_context(state: AgentState) -> None:
|
||
state["clarification_context"] = None
|
||
|
||
|
||
def _write_clarification_context(
|
||
state: AgentState,
|
||
*,
|
||
role: AgentRole,
|
||
sub_commander: str,
|
||
tool_name: str,
|
||
question: str,
|
||
partial_args: dict[str, Any] | None = None,
|
||
missing_fields: list[str] | None = None,
|
||
) -> None:
|
||
state["clarification_context"] = {
|
||
"owning_agent": role.value,
|
||
"owning_sub_commander": sub_commander,
|
||
"target_action": tool_name,
|
||
"question": question,
|
||
"partial_args": dict(partial_args or {}),
|
||
"missing_fields": list(missing_fields or []),
|
||
"status": "pending",
|
||
}
|
||
pending_action = state.get("pending_action") or {}
|
||
if pending_action.get("status") == "pending":
|
||
state["continuity_state"] = {"status": "fresh", "mode": "resume_after_clarification"}
|
||
|
||
|
||
def _route_from_clarification_context(state: AgentState, user_query: str) -> AgentRole | None:
|
||
if not _has_active_clarification_context(state):
|
||
return None
|
||
if _route_agent_from_user_query(user_query) != AgentRole.MASTER:
|
||
return None
|
||
owning_agent = str((state.get("clarification_context") or {}).get("owning_agent") or "")
|
||
try:
|
||
return AgentRole(owning_agent)
|
||
except ValueError:
|
||
return None
|
||
|
||
|
||
def _build_clarification_summary(state: AgentState) -> str | None:
|
||
clarification_context = state.get("clarification_context") or {}
|
||
if not _has_active_clarification_context(state):
|
||
return None
|
||
lines = [
|
||
"clarification_context:",
|
||
f"- owning_agent: {clarification_context.get('owning_agent')}",
|
||
f"- owning_sub_commander: {clarification_context.get('owning_sub_commander')}",
|
||
f"- target_action: {clarification_context.get('target_action')}",
|
||
f"- question: {clarification_context.get('question')}",
|
||
]
|
||
missing_fields = clarification_context.get("missing_fields") or []
|
||
partial_args = clarification_context.get("partial_args") or {}
|
||
if missing_fields:
|
||
lines.append(f"- missing_fields: {', '.join(str(field) for field in missing_fields)}")
|
||
if partial_args:
|
||
lines.append(f"- partial_args: {json.dumps(partial_args, ensure_ascii=False)}")
|
||
lines.append(
|
||
"- instruction: merge the user's latest answer into the missing fields and continue the same action"
|
||
)
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _build_resumed_clarification_query(state: AgentState, user_query: str) -> str:
|
||
clarification_context = state.get("clarification_context") or {}
|
||
partial_args = clarification_context.get("partial_args") or {}
|
||
target_action = clarification_context.get("target_action") or ""
|
||
if not partial_args or target_action != "create_reminder":
|
||
return user_query
|
||
|
||
parts: list[str] = []
|
||
title = partial_args.get("title") or partial_args.get("content")
|
||
if title:
|
||
parts.append(f"title={title}")
|
||
date = partial_args.get("date")
|
||
if date:
|
||
parts.append(f"date={date}")
|
||
parts.append(f"reminder_at={user_query}")
|
||
return f"继续完成提醒创建,请合并参数:{';'.join(parts)}"
|
||
|
||
|
||
def _prepare_tool_calls_for_execution(
|
||
tool_calls: list[dict[str, Any]],
|
||
state: AgentState,
|
||
) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
||
prepared_calls: list[dict[str, Any]] = []
|
||
for call in tool_calls:
|
||
tool_name, args = _canonicalize_tool_call(call["name"], dict(call.get("args") or {}))
|
||
normalized_args = normalize_tool_time_arguments(
|
||
tool_name,
|
||
args,
|
||
state.get("current_datetime_context"),
|
||
)
|
||
clarification = None
|
||
if tool_name == "create_reminder":
|
||
clarification = _build_reminder_time_clarification(normalized_args)
|
||
if clarification:
|
||
return [], {
|
||
"tool_name": tool_name,
|
||
"question": clarification["question"],
|
||
"partial_args": clarification.get("partial_args") or normalized_args,
|
||
"missing_fields": clarification.get("missing_fields") or [],
|
||
}
|
||
prepared_calls.append(
|
||
{
|
||
"id": call.get("id"),
|
||
"name": tool_name,
|
||
"args": normalized_args,
|
||
"reason": call.get("reason"),
|
||
}
|
||
)
|
||
return prepared_calls, None
|
||
|
||
|
||
def _canonicalize_tool_call(tool_name: str, args: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
||
normalized_name = tool_name
|
||
normalized_args = dict(args)
|
||
|
||
if normalized_name == "create_reminder":
|
||
if normalized_args.get("description") and not (
|
||
normalized_args.get("title") or normalized_args.get("content")
|
||
):
|
||
normalized_args["content"] = normalized_args["description"]
|
||
if normalized_args.get("reminder_content") and not (
|
||
normalized_args.get("title") or normalized_args.get("content")
|
||
):
|
||
normalized_args["content"] = normalized_args["reminder_content"]
|
||
if normalized_args.get("reminder_time") and not (
|
||
normalized_args.get("reminder_at")
|
||
or normalized_args.get("datetime")
|
||
or normalized_args.get("at")
|
||
or normalized_args.get("remind_at")
|
||
or normalized_args.get("time")
|
||
):
|
||
normalized_args["time"] = normalized_args["reminder_time"]
|
||
|
||
if normalized_name in {"create_schedule_task", "create_task"}:
|
||
if normalized_args.get("task") and not (
|
||
normalized_args.get("title") or normalized_args.get("content")
|
||
):
|
||
normalized_args["title"] = normalized_args["task"]
|
||
if normalized_args.get("due_datetime") and not (
|
||
normalized_args.get("due_date") or normalized_args.get("date")
|
||
):
|
||
normalized_args["due_date"] = normalized_args["due_datetime"]
|
||
if normalized_args.get("due_time") and not normalized_args.get("due_date"):
|
||
normalized_args["due_date"] = normalized_args["due_time"]
|
||
|
||
if normalized_name == "create_todo":
|
||
if normalized_args.get("task") and not (
|
||
normalized_args.get("title") or normalized_args.get("content")
|
||
):
|
||
normalized_args["title"] = normalized_args["task"]
|
||
if normalized_args.get("date") and not normalized_args.get("todo_date"):
|
||
normalized_args["todo_date"] = normalized_args["date"]
|
||
if normalized_args.get("due_date") and not normalized_args.get("todo_date"):
|
||
normalized_args["todo_date"] = normalized_args["due_date"]
|
||
if normalized_args.get("due_datetime") and not normalized_args.get("todo_date"):
|
||
normalized_args["todo_date"] = normalized_args["due_datetime"]
|
||
if any(
|
||
normalized_args.get(key)
|
||
for key in ("due_datetime", "start_time", "end_time", "due_time")
|
||
):
|
||
normalized_name = "create_schedule_task"
|
||
if normalized_args.get("due_time") and not normalized_args.get("due_date"):
|
||
normalized_args["due_date"] = normalized_args["due_time"]
|
||
elif normalized_args.get("todo_date") and not normalized_args.get("due_date"):
|
||
normalized_args["due_date"] = normalized_args["todo_date"]
|
||
|
||
if normalized_name == "create_goal":
|
||
if normalized_args.get("task") and not (
|
||
normalized_args.get("title") or normalized_args.get("content")
|
||
):
|
||
normalized_args["title"] = normalized_args["task"]
|
||
if normalized_args.get("date") and not normalized_args.get("goal_date"):
|
||
normalized_args["goal_date"] = normalized_args["date"]
|
||
if normalized_args.get("target_date") and not normalized_args.get("goal_date"):
|
||
normalized_args["goal_date"] = normalized_args["target_date"]
|
||
|
||
return normalized_name, normalized_args
|
||
|
||
|
||
async def _invoke_llm(llm, messages: list[BaseMessage], tools: list[Any] | None = None):
|
||
messages = _coalesce_system_messages(messages)
|
||
if tools:
|
||
llm = llm.bind_tools(tools)
|
||
return await llm.ainvoke(messages)
|
||
|
||
|
||
async def _execute_tool_calls(
|
||
tool_calls: list[dict[str, Any]],
|
||
toolset: list[Any],
|
||
state: AgentState,
|
||
) -> tuple[list[dict[str, Any]], str, list[dict[str, str]], list[ToolMessage]]:
|
||
tool_map = {tool.name: tool for tool in toolset}
|
||
normalized_calls: list[dict[str, Any]] = []
|
||
result_lines: list[str] = []
|
||
created_entities: list[dict[str, str]] = []
|
||
tool_messages: list[ToolMessage] = []
|
||
verifier_hints_by_tool: list[dict[str, Any]] = []
|
||
tool_summaries: list[dict[str, Any]] = []
|
||
|
||
for call in tool_calls:
|
||
tool_name = call["name"]
|
||
normalized_args = dict(call.get("args") or {})
|
||
tool = tool_map.get(tool_name)
|
||
if tool is None:
|
||
raise ValueError(f"Tool not found: {tool_name}")
|
||
|
||
_append_event_trace(
|
||
state,
|
||
"agent.tool.start",
|
||
payload={"tool_name": tool_name, "args": normalized_args},
|
||
task_id=str(call.get("id") or "") or None,
|
||
)
|
||
|
||
try:
|
||
if hasattr(tool, "ainvoke"):
|
||
result = await tool.ainvoke(normalized_args)
|
||
else:
|
||
result = await asyncio.to_thread(tool.invoke, normalized_args)
|
||
except Exception as exc:
|
||
logger.exception("Tool execution failed: %s args=%s", tool_name, normalized_args)
|
||
result = f"工具执行失败: {exc}"
|
||
_append_event_trace(
|
||
state,
|
||
"agent.error",
|
||
payload={"tool_name": tool_name, "args": normalized_args, "error": str(exc)},
|
||
severity="error",
|
||
task_id=str(call.get("id") or "") or None,
|
||
)
|
||
|
||
normalized_call = {
|
||
"id": call.get("id"),
|
||
"name": tool_name,
|
||
"args": normalized_args,
|
||
"reason": call.get("reason"),
|
||
}
|
||
normalized_calls.append(normalized_call)
|
||
result_lines.append(f"[{tool_name}] {result}")
|
||
verifier_hints = _build_verifier_hints(state, tool_name, result)
|
||
verifier_hints_by_tool.append(verifier_hints)
|
||
tool_outcome = {
|
||
"tool_name": tool_name,
|
||
"args": normalized_args,
|
||
"result_preview": _stringify_message_content(result)[:200],
|
||
"verifier_hints": verifier_hints,
|
||
"isolation": {
|
||
"mode": state.get("isolation_mode"),
|
||
"workspace_path": state.get("isolation_workspace_path"),
|
||
},
|
||
}
|
||
state["tool_outcomes"] = [*(state.get("tool_outcomes") or []), tool_outcome]
|
||
_append_event_trace(
|
||
state,
|
||
"agent.tool.result",
|
||
payload={
|
||
"tool_name": tool_name,
|
||
"args": normalized_args,
|
||
"result_preview": _stringify_message_content(result)[:200],
|
||
"verification": verifier_hints,
|
||
},
|
||
severity="error" if _tool_result_indicates_failure(result) else "info",
|
||
task_id=str(call.get("id") or "") or None,
|
||
)
|
||
tool_messages.append(
|
||
ToolMessage(
|
||
content=_stringify_message_content(result),
|
||
tool_call_id=str(call.get("id") or tool_name),
|
||
name=tool_name,
|
||
)
|
||
)
|
||
entity = _classify_created_entity(tool_name)
|
||
call_created_entities: list[dict[str, str]] = []
|
||
if entity and not _tool_result_indicates_failure(result):
|
||
created_entities.append(entity)
|
||
call_created_entities.append(entity)
|
||
tool_summaries.append(
|
||
{
|
||
"tool_name": tool_name,
|
||
"result_preview": _stringify_message_content(result)[:200],
|
||
"created_entity_types": [
|
||
entity.get("type") for entity in call_created_entities if entity.get("type")
|
||
],
|
||
"created_count": len(call_created_entities),
|
||
}
|
||
)
|
||
|
||
state["verifier_hints"] = {"tools": verifier_hints_by_tool}
|
||
_update_task_result_summary(state, tool_summaries)
|
||
return normalized_calls, "\n".join(result_lines), created_entities, tool_messages
|
||
|
||
|
||
async def _run_sub_commander(
|
||
state: AgentState,
|
||
role: AgentRole,
|
||
manager_prompt: str,
|
||
user_query: str,
|
||
*,
|
||
use_tools: bool,
|
||
summary_target: str | None = None,
|
||
):
|
||
state["clarification_needed"] = False
|
||
state["clarification_question"] = None
|
||
state["stop_reason"] = None
|
||
|
||
llm = _get_llm_for_state(state)
|
||
capabilities = _resolve_capabilities(state, llm)
|
||
if _has_active_clarification_context(state) and role.value == str(
|
||
(state.get("clarification_context") or {}).get("owning_agent") or ""
|
||
):
|
||
user_query = _build_resumed_clarification_query(state, user_query)
|
||
sub_commander = _choose_sub_commander(role, user_query)
|
||
_record_sub_commander(state, role, sub_commander, user_query)
|
||
|
||
toolset = SUB_COMMANDER_TOOLSETS.get(sub_commander, []) if use_tools else []
|
||
_prepare_isolation_context(
|
||
state,
|
||
role=role,
|
||
sub_commander=sub_commander,
|
||
user_query=user_query,
|
||
toolset=toolset,
|
||
)
|
||
if (
|
||
role == AgentRole.EXECUTOR
|
||
and _is_short_confirmation(user_query)
|
||
and _previous_turn_completed_reminder_creation(state)
|
||
):
|
||
state["tool_calls"] = []
|
||
state["last_tool_result"] = None
|
||
state["final_response"] = (
|
||
"上一条提醒已经创建好了。若您现在要新建别的内容,请直接告诉我要创建什么。"
|
||
)
|
||
history_messages = list(state.get("messages", []))
|
||
history_messages.append(AIMessage(content=state["final_response"]))
|
||
state["messages"] = history_messages
|
||
state["should_respond"] = True
|
||
return state
|
||
base_messages = _build_system_messages(state, manager_prompt, role, sub_commander)
|
||
conversation_history = _conversation_history_messages(state)
|
||
if conversation_history and getattr(conversation_history[-1], "type", "") in {"human", "user"}:
|
||
conversation_history = conversation_history[:-1]
|
||
user_message = HumanMessage(content=f"用户请求: {user_query}")
|
||
working_messages = [*base_messages, *conversation_history, user_message]
|
||
|
||
state["tool_calls"] = []
|
||
state["last_tool_result"] = None
|
||
state["tool_strategy_used"] = None
|
||
state["fallback_parse_error"] = None
|
||
start_tool_index = len(state.get("tool_outcomes") or [])
|
||
|
||
if not _guard_sub_commander_budget(
|
||
state, "tool_round_count", "max_tool_rounds", "max_tool_rounds_exceeded"
|
||
):
|
||
pass
|
||
elif not _guard_sub_commander_budget(
|
||
state, "retry_count", "max_retries", "max_retries_exceeded"
|
||
):
|
||
pass
|
||
elif not toolset:
|
||
if _guard_sub_commander_budget(
|
||
state, "iteration_count", "max_iterations", "max_iterations_exceeded"
|
||
):
|
||
state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
|
||
response = await _invoke_llm(llm, working_messages)
|
||
_record_response_usage(state, response)
|
||
state["final_response"] = _stringify_message_content(response.content)
|
||
elif capabilities.supports_native_tools:
|
||
state["tool_strategy_used"] = "native"
|
||
bound_llm = llm.bind_tools(toolset)
|
||
while state.get("final_response") is None and not state.get("clarification_needed"):
|
||
if not _guard_sub_commander_budget(
|
||
state, "iteration_count", "max_iterations", "max_iterations_exceeded"
|
||
):
|
||
break
|
||
state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
|
||
response = await _invoke_llm(bound_llm, working_messages)
|
||
_record_response_usage(state, response)
|
||
tool_calls = getattr(response, "tool_calls", None) or []
|
||
if tool_calls:
|
||
if not _guard_sub_commander_budget(
|
||
state, "tool_round_count", "max_tool_rounds", "max_tool_rounds_exceeded"
|
||
):
|
||
break
|
||
prepared_calls, clarification = _prepare_tool_calls_for_execution(tool_calls, state)
|
||
if clarification:
|
||
state["clarification_needed"] = True
|
||
state["clarification_question"] = clarification["question"]
|
||
_write_clarification_context(
|
||
state,
|
||
role=role,
|
||
sub_commander=sub_commander,
|
||
tool_name=clarification["tool_name"],
|
||
question=clarification["question"],
|
||
partial_args=clarification.get("partial_args"),
|
||
missing_fields=clarification.get("missing_fields"),
|
||
)
|
||
state["stop_reason"] = "clarification_needed"
|
||
state["final_response"] = clarification["question"]
|
||
break
|
||
state["tool_round_count"] = int(state.get("tool_round_count") or 0) + 1
|
||
assistant_tool_message = AIMessage(
|
||
content=_stringify_message_content(getattr(response, "content", "")),
|
||
tool_calls=tool_calls,
|
||
)
|
||
(
|
||
normalized_calls,
|
||
tool_result,
|
||
created_entities,
|
||
tool_messages,
|
||
) = await _execute_tool_calls(
|
||
prepared_calls,
|
||
toolset,
|
||
state,
|
||
)
|
||
state["tool_calls"] = normalized_calls
|
||
state["last_tool_result"] = tool_result
|
||
state["created_entities"] = [
|
||
*(state.get("created_entities") or []),
|
||
*created_entities,
|
||
]
|
||
if created_entities:
|
||
_clear_clarification_context(state)
|
||
if (
|
||
role == AgentRole.SCHEDULE_PLANNER
|
||
and _should_clear_schedule_creation_continuity(state, created_entities)
|
||
):
|
||
_clear_structured_continuity(state)
|
||
working_messages = [*working_messages, assistant_tool_message, *tool_messages]
|
||
if sub_commander == "librarian_retrieval" and _is_missing_knowledge_result(
|
||
tool_result
|
||
):
|
||
working_messages.append(
|
||
SystemMessage(
|
||
content="如果检索工具没有找到证据,可以直接基于你的常识给出清晰回答,不要机械地说不知道。"
|
||
)
|
||
)
|
||
continue
|
||
state["final_response"] = _stringify_message_content(response.content)
|
||
else:
|
||
state["tool_strategy_used"] = "json_fallback"
|
||
allowed_tools = [tool.name for tool in toolset]
|
||
while state.get("final_response") is None and not state.get("clarification_needed"):
|
||
if not _guard_sub_commander_budget(
|
||
state, "iteration_count", "max_iterations", "max_iterations_exceeded"
|
||
):
|
||
break
|
||
state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
|
||
parsed = None
|
||
retry_instruction: BaseMessage | None = None
|
||
while parsed is None:
|
||
response = await _invoke_llm(
|
||
llm,
|
||
[
|
||
*working_messages,
|
||
SystemMessage(content=JSON_ACTION_FALLBACK_PROMPT),
|
||
SystemMessage(content=f"本次可用工具列表: {', '.join(allowed_tools)}"),
|
||
*([retry_instruction] if retry_instruction else []),
|
||
],
|
||
)
|
||
_record_response_usage(state, response)
|
||
response_text = _stringify_message_content(response.content)
|
||
response_text_stripped = response_text.strip()
|
||
parsed = _parse_json_action(response_text, allowed_tools)
|
||
if parsed is None and response_text_stripped and _looks_like_internal_tool_markup(
|
||
response_text_stripped
|
||
):
|
||
if int(state.get("retry_count") or 0) >= int(state.get("max_retries") or 0):
|
||
state["fallback_parse_error"] = "internal_tool_markup"
|
||
state["final_response"] = _build_internal_markup_fallback_response(
|
||
state,
|
||
sub_commander=sub_commander,
|
||
) or (
|
||
"这次内部调度没有正确收束成最终答复。我先不展示内部调用过程;您重试一次,我会直接用自然语言回答。"
|
||
)
|
||
break
|
||
if not _guard_sub_commander_budget(
|
||
state, "iteration_count", "max_iterations", "max_iterations_exceeded"
|
||
):
|
||
parsed = None
|
||
break
|
||
state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
|
||
state["retry_count"] = int(state.get("retry_count") or 0) + 1
|
||
retry_instruction = SystemMessage(
|
||
content=(
|
||
"上一轮输出了内部调度或工具调用标记,这是协议错误。"
|
||
"不要再输出分发说明、XML 标签、<invoke>、<parameter>、JSON 或 tool_call。"
|
||
"请直接面向用户给出最终自然语言答复;如果已有工具结果,就基于结果整理;"
|
||
"如果工具没有找到证据,可以基于常识直接回答。"
|
||
)
|
||
)
|
||
continue
|
||
if parsed is None and response_text.strip() and state.get("tool_round_count"):
|
||
state["fallback_parse_error"] = None
|
||
state["final_response"] = response_text.strip()
|
||
break
|
||
if parsed is not None:
|
||
state["fallback_parse_error"] = None
|
||
break
|
||
if not _guard_sub_commander_budget(
|
||
state, "iteration_count", "max_iterations", "max_iterations_exceeded"
|
||
):
|
||
parsed = None
|
||
break
|
||
if int(state.get("retry_count") or 0) >= int(state.get("max_retries") or 0):
|
||
state["fallback_parse_error"] = "invalid_json_action"
|
||
state["final_response"] = (
|
||
"这次内部动作解析没整理好,不过您的意思我接住了。您再说一遍要我执行的内容,我只回结果,不展示内部调用细节。"
|
||
)
|
||
break
|
||
state["iteration_count"] = int(state.get("iteration_count") or 0) + 1
|
||
state["retry_count"] = int(state.get("retry_count") or 0) + 1
|
||
retry_instruction = SystemMessage(
|
||
content="上一次输出不是有效 JSON。请严格只返回合法 JSON,不要加解释。"
|
||
)
|
||
if state.get("final_response") is not None:
|
||
break
|
||
if parsed is None:
|
||
break
|
||
if parsed["mode"] == "final":
|
||
state["final_response"] = parsed["final_response"]
|
||
break
|
||
if parsed["mode"] == "clarification":
|
||
state["clarification_needed"] = True
|
||
state["clarification_question"] = parsed["clarification_question"]
|
||
_write_clarification_context(
|
||
state,
|
||
role=role,
|
||
sub_commander=sub_commander,
|
||
tool_name="clarification",
|
||
question=parsed["clarification_question"],
|
||
)
|
||
state["stop_reason"] = "clarification_needed"
|
||
state["final_response"] = parsed["clarification_question"]
|
||
break
|
||
if not _guard_sub_commander_budget(
|
||
state, "tool_round_count", "max_tool_rounds", "max_tool_rounds_exceeded"
|
||
):
|
||
break
|
||
prepared_calls, clarification = _prepare_tool_calls_for_execution(
|
||
parsed["tool_calls"], state
|
||
)
|
||
if clarification:
|
||
state["clarification_needed"] = True
|
||
state["clarification_question"] = clarification["question"]
|
||
_write_clarification_context(
|
||
state,
|
||
role=role,
|
||
sub_commander=sub_commander,
|
||
tool_name=clarification["tool_name"],
|
||
question=clarification["question"],
|
||
partial_args=clarification.get("partial_args"),
|
||
missing_fields=clarification.get("missing_fields"),
|
||
)
|
||
state["stop_reason"] = "clarification_needed"
|
||
state["final_response"] = clarification["question"]
|
||
break
|
||
state["tool_round_count"] = int(state.get("tool_round_count") or 0) + 1
|
||
(
|
||
normalized_calls,
|
||
tool_result,
|
||
created_entities,
|
||
tool_messages,
|
||
) = await _execute_tool_calls(
|
||
prepared_calls,
|
||
toolset,
|
||
state,
|
||
)
|
||
state["tool_calls"] = normalized_calls
|
||
state["last_tool_result"] = tool_result
|
||
state["created_entities"] = [*(state.get("created_entities") or []), *created_entities]
|
||
if role == AgentRole.SCHEDULE_PLANNER and _should_clear_schedule_creation_continuity(
|
||
state, created_entities
|
||
):
|
||
_clear_structured_continuity(state)
|
||
working_messages = [*working_messages, *tool_messages]
|
||
if sub_commander == "librarian_retrieval" and _is_missing_knowledge_result(tool_result):
|
||
working_messages.append(
|
||
SystemMessage(
|
||
content="如果检索工具没有找到证据,可以直接基于你的常识给出清晰回答,不要机械地说不知道。"
|
||
)
|
||
)
|
||
|
||
if summary_target:
|
||
state[_summary_state_key(summary_target)] = state.get("final_response")
|
||
|
||
task_result_summary = state.get("task_result_summary")
|
||
tool_outcomes = list(state.get("tool_outcomes") or [])[start_tool_index:]
|
||
has_tool_failure = any(
|
||
_tool_result_indicates_failure(outcome.get("result_preview")) for outcome in tool_outcomes
|
||
)
|
||
verifier_input = {
|
||
"summary": state.get("final_response") or (task_result_summary or {}).get("tools"),
|
||
"evidence": tool_outcomes,
|
||
"success": bool(tool_outcomes or state.get("final_response")) and not has_tool_failure,
|
||
}
|
||
_append_event_trace(
|
||
state,
|
||
"agent.verify.started",
|
||
payload={
|
||
"summary_present": bool(verifier_input["summary"]),
|
||
"evidence_count": len(verifier_input["evidence"]),
|
||
},
|
||
)
|
||
verdict = verify_task_result(
|
||
summary=state.get("final_response"),
|
||
evidence=tool_outcomes,
|
||
result=verifier_input,
|
||
)
|
||
updated_state = apply_verification_verdict(state, verdict)
|
||
state.update(updated_state)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.verify.completed",
|
||
payload={
|
||
"status": verdict.status,
|
||
"summary": verdict.summary,
|
||
"evidence_count": len(verdict.evidence),
|
||
},
|
||
severity="error" if verdict.status == "failed" else "info",
|
||
)
|
||
|
||
final_response_text = state.get("final_response")
|
||
if not state.get("clarification_needed") and final_response_text:
|
||
_clear_clarification_context(state)
|
||
if (
|
||
role == AgentRole.SCHEDULE_PLANNER
|
||
and isinstance(final_response_text, str)
|
||
and _is_schedule_creation_confirmation_response(final_response_text)
|
||
):
|
||
_write_schedule_creation_continuity(state, user_query)
|
||
|
||
history_messages = list(state.get("messages", []))
|
||
final_response = state.get("final_response")
|
||
if isinstance(final_response, str):
|
||
history_messages.append(AIMessage(content=final_response))
|
||
|
||
state["messages"] = history_messages
|
||
state["should_respond"] = True
|
||
return state
|
||
|
||
|
||
def _derive_task_status(state: AgentState) -> Literal["completed", "failed", "blocked"]:
|
||
if state.get("clarification_needed") or state.get("stop_reason") == "clarification_needed":
|
||
return "blocked"
|
||
if state.get("verification_status") == "failed":
|
||
return "failed"
|
||
if state.get("stop_reason") and state.get("stop_reason") != "clarification_needed":
|
||
return "blocked"
|
||
if state.get("final_response"):
|
||
return "completed"
|
||
return "failed"
|
||
|
||
|
||
def _build_task_evidence(state: AgentState, start_index: int) -> list[dict[str, Any]]:
|
||
tool_outcomes = [dict(item) for item in list(state.get("tool_outcomes") or [])[start_index:]]
|
||
if tool_outcomes:
|
||
evidence = tool_outcomes
|
||
else:
|
||
evidence = []
|
||
|
||
if state.get("isolation_mode") and state.get("isolation_mode") != "none":
|
||
evidence.append(
|
||
{
|
||
"type": "isolation",
|
||
"mode": state.get("isolation_mode"),
|
||
"workspace_path": state.get("isolation_workspace_path"),
|
||
"metadata": dict(state.get("isolation_metadata") or {}),
|
||
}
|
||
)
|
||
|
||
if state.get("input_tokens") or state.get("output_tokens"):
|
||
evidence.append(
|
||
{
|
||
"type": "cost",
|
||
"input_tokens": int(state.get("input_tokens") or 0),
|
||
"output_tokens": int(state.get("output_tokens") or 0),
|
||
"estimated_cost": state.get("estimated_cost"),
|
||
"budget_warning": bool(state.get("budget_warning") or False),
|
||
}
|
||
)
|
||
|
||
if state.get("verification_status") or state.get("verification_summary"):
|
||
evidence.append(
|
||
{
|
||
"type": "verification",
|
||
"status": state.get("verification_status"),
|
||
"summary": state.get("verification_summary"),
|
||
}
|
||
)
|
||
if not evidence and state.get("final_response"):
|
||
evidence.append(
|
||
{
|
||
"type": "response_summary",
|
||
"content": _stringify_message_content(state.get("final_response"))[:400],
|
||
}
|
||
)
|
||
return evidence
|
||
|
||
|
||
def _collect_task_result(task: AgentTask, state: AgentState, start_tool_index: int) -> TaskResult:
|
||
status = _derive_task_status(state)
|
||
next_action = state.get("clarification_question")
|
||
if not next_action and status != "completed" and state.get("stop_reason"):
|
||
next_action = f"resolve_{state['stop_reason']}"
|
||
return TaskResult(
|
||
task_id=task.task_id,
|
||
status=status,
|
||
summary=_stringify_message_content(
|
||
state.get("final_response") or state.get("verification_summary")
|
||
),
|
||
evidence=_build_task_evidence(state, start_tool_index),
|
||
owner_agent_id=task.owner_agent_id,
|
||
parent_task_id=task.parent_task_id,
|
||
child_task_ids=list(task.child_task_ids),
|
||
thread_id=str(state.get("thread_id") or "") or None,
|
||
message_id=str(state.get("last_message_id") or "") or None,
|
||
message_index=int(state.get("message_sequence") or 0) or None,
|
||
interrupt_records=list(state.get("interrupted_tasks") or []),
|
||
recovery_records=list(state.get("recovery_trace") or []),
|
||
budget_snapshot=state.get("budget_state"),
|
||
next_action=next_action,
|
||
output_data={
|
||
"role": task.role,
|
||
"sub_commander": state.get("current_sub_commander"),
|
||
"verification_status": state.get("verification_status"),
|
||
"isolation_mode": state.get("isolation_mode"),
|
||
"isolation_workspace_path": state.get("isolation_workspace_path"),
|
||
"estimated_cost": state.get("estimated_cost"),
|
||
"budget_warning": bool(state.get("budget_warning") or False),
|
||
},
|
||
)
|
||
|
||
|
||
def _apply_task_result_to_state(
|
||
state: AgentState, task: AgentTask, task_result: TaskResult
|
||
) -> None:
|
||
normalized_result = normalize_task_result(task_result, default_task_id=task.task_id)
|
||
serialized_result = normalized_result.model_dump(mode="json")
|
||
state["task_results"] = [*(state.get("task_results") or []), serialized_result]
|
||
|
||
updated_tasks: list[dict[str, Any]] = []
|
||
completed_entry: dict[str, Any] | None = None
|
||
pending_tasks: list[dict[str, Any]] = []
|
||
for existing_task in state.get("active_tasks") or []:
|
||
normalized_task = (
|
||
existing_task.model_dump(mode="json")
|
||
if isinstance(existing_task, AgentTask)
|
||
else dict(existing_task)
|
||
)
|
||
if normalized_task.get("task_id") == task.task_id:
|
||
normalized_task["status"] = normalized_result.status
|
||
normalized_task["evidence"] = list(normalized_result.evidence)
|
||
normalized_task["result_summary"] = normalized_result.summary
|
||
if normalized_result.next_action:
|
||
normalized_task["next_action"] = normalized_result.next_action
|
||
completed_entry = {
|
||
"task_id": task.task_id,
|
||
"role": task.role,
|
||
"owner_agent_id": task.owner_agent_id,
|
||
"status": normalized_result.status,
|
||
"summary": normalized_result.summary,
|
||
"next_action": normalized_result.next_action,
|
||
}
|
||
updated_tasks.append(normalized_task)
|
||
if normalized_task.get("status") != "completed":
|
||
pending_tasks.append(normalized_task)
|
||
state["active_tasks"] = updated_tasks
|
||
state["pending_tasks"] = pending_tasks
|
||
|
||
if completed_entry is not None:
|
||
state["completed_tasks"] = [*(state.get("completed_tasks") or []), completed_entry]
|
||
|
||
|
||
def _build_collaboration_final_response(task_results: list[TaskResult | dict[str, Any]]) -> str:
|
||
normalized_results = [normalize_task_result(item) for item in task_results]
|
||
lines = [f"已按协作模式回收 {len(normalized_results)} 个子任务结果:"]
|
||
final_completed_summary: str | None = None
|
||
for index, result in enumerate(normalized_results, start=1):
|
||
owner = result.owner_agent_id or "unknown"
|
||
summary = result.summary or "未返回摘要。"
|
||
lines.append(f"{index}. [{owner}] ({result.status}) {summary}")
|
||
if result.status == "completed" and result.summary:
|
||
final_completed_summary = result.summary
|
||
if final_completed_summary:
|
||
lines.append(f"汇总结论:{final_completed_summary}")
|
||
elif normalized_results:
|
||
blocked_result = next(
|
||
(item for item in reversed(normalized_results) if item.next_action), None
|
||
)
|
||
if blocked_result and blocked_result.next_action:
|
||
lines.append(f"下一步:{blocked_result.next_action}")
|
||
else:
|
||
lines.append("汇总结论:协作任务已执行,但最终摘要为空。")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _build_serial_fallback_response(
|
||
user_query: str,
|
||
task_results: list[TaskResult | dict[str, Any]],
|
||
merge_report: MergeReport | dict[str, Any] | None,
|
||
) -> str:
|
||
normalized_results = [normalize_task_result(item) for item in task_results]
|
||
completed = [item for item in normalized_results if item.status == "completed"]
|
||
merge_payload = (
|
||
merge_report.model_dump(mode="json")
|
||
if isinstance(merge_report, MergeReport)
|
||
else dict(merge_report or {})
|
||
)
|
||
lines = [
|
||
"并行/协作结果出现冲突或失败,我已切回保守收敛路径。",
|
||
f"原始请求:{user_query}",
|
||
]
|
||
if completed:
|
||
lines.append("当前仍可确认的结果:")
|
||
for item in completed[:3]:
|
||
lines.append(f"- [{item.owner_agent_id or 'unknown'}] {item.summary or '已完成'}")
|
||
if merge_payload.get("conflict_flags"):
|
||
lines.append("冲突/回退原因:")
|
||
for flag in list(merge_payload.get("conflict_flags") or [])[:3]:
|
||
lines.append(f"- {flag}")
|
||
if not completed:
|
||
lines.append("目前没有足够稳定的子任务结果,建议改走 direct 或更小范围的 collaboration。")
|
||
return "\n".join(lines)
|
||
|
||
|
||
def _verify_collaboration_results(
|
||
state: AgentState,
|
||
tasks: list[AgentTask],
|
||
task_results: list[TaskResult | dict[str, Any]] | None = None,
|
||
) -> None:
|
||
expected_task_ids = {task.task_id for task in tasks}
|
||
normalized_results = [
|
||
normalize_task_result(item)
|
||
for item in (
|
||
task_results if task_results is not None else (state.get("task_results") or [])
|
||
)
|
||
if normalize_task_result(item).task_id in expected_task_ids
|
||
]
|
||
result_by_task_id = {item.task_id: item for item in normalized_results}
|
||
missing_task_ids = [task.task_id for task in tasks if task.task_id not in result_by_task_id]
|
||
failed_or_blocked = [
|
||
item.task_id for item in normalized_results if item.status in {"failed", "blocked"}
|
||
]
|
||
missing_evidence = [item.task_id for item in normalized_results if not item.evidence]
|
||
|
||
verification_evidence = [
|
||
{
|
||
"task_id": item.task_id,
|
||
"owner_agent_id": item.owner_agent_id,
|
||
"status": item.status,
|
||
"evidence_count": len(item.evidence or []),
|
||
}
|
||
for item in normalized_results
|
||
]
|
||
merge_report = merge_task_results([item.model_dump(mode="json") for item in normalized_results])
|
||
state["merge_report"] = merge_report.model_dump(mode="json")
|
||
_append_event_trace(
|
||
state,
|
||
"agent.merge.completed",
|
||
payload=state["merge_report"],
|
||
)
|
||
|
||
if missing_task_ids:
|
||
summary = f"协作结果不完整,缺少任务结果: {', '.join(missing_task_ids)}"
|
||
verdict = verify_task_result(
|
||
status="failed", summary=summary, evidence=verification_evidence
|
||
)
|
||
elif failed_or_blocked:
|
||
summary = f"协作结果未闭环,存在失败或阻塞任务: {', '.join(failed_or_blocked)}"
|
||
verdict = verify_task_result(
|
||
status="failed", summary=summary, evidence=verification_evidence
|
||
)
|
||
elif missing_evidence:
|
||
summary = f"协作结果证据不足,缺少 evidence 的任务: {', '.join(missing_evidence)}"
|
||
verdict = verify_task_result(
|
||
status="failed", summary=summary, evidence=verification_evidence
|
||
)
|
||
elif merge_report.status == "conflicted":
|
||
verdict = verify_task_result(
|
||
status="failed",
|
||
summary=merge_report.summary,
|
||
evidence=[
|
||
*verification_evidence,
|
||
{"type": "merge_conflict", "conflict_flags": merge_report.conflict_flags},
|
||
],
|
||
)
|
||
elif merge_report.status == "fallback":
|
||
verdict = verify_task_result(
|
||
status="failed",
|
||
summary=merge_report.summary,
|
||
evidence=[
|
||
*verification_evidence,
|
||
{"type": "merge_fallback", "conflict_flags": merge_report.conflict_flags},
|
||
],
|
||
)
|
||
else:
|
||
summary = (
|
||
merge_report.summary
|
||
or f"协作模式已完成 {len(normalized_results)}/{len(tasks)} 个子任务,并为每个子任务回收了结果与 evidence。"
|
||
)
|
||
verdict = verify_task_result(
|
||
status="passed", summary=summary, evidence=verification_evidence
|
||
)
|
||
|
||
updated_state = apply_verification_verdict(state, verdict)
|
||
state.update(updated_state)
|
||
state["verification_report"] = VerificationReport(
|
||
status=state.get("verification_status") or "skipped",
|
||
summary=state.get("verification_summary"),
|
||
evidence=list(state.get("verification_evidence") or []),
|
||
).model_dump(mode="json")
|
||
_append_event_trace(
|
||
state,
|
||
"agent.verify.completed",
|
||
payload=state["verification_report"],
|
||
)
|
||
|
||
|
||
async def _run_collaboration_flow(state: AgentState, user_query: str) -> AgentState:
|
||
_set_phase(state, "phase_2_controlled_collaboration", reason="collaboration_flow_started")
|
||
_record_checkpoint(state, "collaboration.tasks_planning", reason="collaboration_flow_started")
|
||
tasks, scheduled_subtasks = _build_collaboration_plan_from_task_graph(state, user_query)
|
||
if len(tasks) < 2:
|
||
state["execution_mode"] = "direct"
|
||
state["routing_decision"] = {"mode": "direct", "reason": "collaboration_plan_fell_back"}
|
||
_record_checkpoint(
|
||
state,
|
||
"collaboration.fallback_to_direct",
|
||
reason="insufficient_tasks",
|
||
payload={"task_count": len(tasks)},
|
||
)
|
||
_set_phase(
|
||
state,
|
||
"phase_1_routing",
|
||
reason="collaboration_flow_abandoned",
|
||
payload={"task_count": len(tasks)},
|
||
)
|
||
_record_checkpoint(
|
||
state,
|
||
"routing.direct_resumed",
|
||
reason="collaboration_flow_abandoned",
|
||
payload={"task_count": len(tasks)},
|
||
)
|
||
return state
|
||
|
||
base_history = list(state.get("messages", []))
|
||
root_agent_id = str(
|
||
state.get("root_agent_id") or state.get("agent_id") or AgentRole.MASTER.value
|
||
)
|
||
coordinator_agent_id = str(state.get("agent_id") or AgentRole.MASTER.value)
|
||
state["execution_mode"] = "collaboration"
|
||
state["routing_decision"] = {
|
||
"mode": "collaboration",
|
||
"reason": "multi_role_request",
|
||
"task_count": len(tasks),
|
||
"roles": [task.role for task in tasks],
|
||
}
|
||
budget_snapshot = _budget_snapshot_from_state(
|
||
state,
|
||
mode="collaboration",
|
||
remaining_parallel_tasks=len(tasks),
|
||
metadata={"task_count": len(tasks)},
|
||
)
|
||
_store_budget_snapshot(state, budget_snapshot)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.collaboration.budget.updated",
|
||
payload=budget_snapshot,
|
||
)
|
||
state["scheduled_subtasks"] = scheduled_subtasks
|
||
state["active_tasks"] = [task.model_dump(mode="json") for task in tasks]
|
||
_record_checkpoint(
|
||
state, "collaboration.tasks_ready", reason="tasks_built", payload={"task_count": len(tasks)}
|
||
)
|
||
parent_task_id = (
|
||
next((task.parent_task_id for task in tasks if task.parent_task_id), None) or "root"
|
||
)
|
||
state["task_hierarchy"] = {parent_task_id: [task.task_id for task in tasks]}
|
||
state["task_results"] = []
|
||
state["next_step"] = None
|
||
|
||
_set_phase(state, "phase_3_dynamic_collaboration", reason="collaboration_workers_dispatch")
|
||
for task in tasks:
|
||
scheduled_subtask = next(
|
||
(item for item in scheduled_subtasks if item.get("subtask_id") == task.task_id),
|
||
None,
|
||
)
|
||
if scheduled_subtask is not None:
|
||
_append_event_trace(
|
||
state,
|
||
"agent.subtask.started",
|
||
payload={
|
||
"subtask_id": scheduled_subtask.get("subtask_id"),
|
||
"role": scheduled_subtask.get("role"),
|
||
"dependencies": scheduled_subtask.get("dependencies") or [],
|
||
},
|
||
task_id=task.task_id,
|
||
)
|
||
_record_checkpoint(
|
||
state,
|
||
"collaboration.task_dispatch",
|
||
reason="dispatch_task",
|
||
payload={"task_id": task.task_id, "role": task.role},
|
||
)
|
||
state["current_agent"] = AgentRole.MASTER.value
|
||
state["agent_id"] = coordinator_agent_id
|
||
state["parent_agent_id"] = None
|
||
state["root_agent_id"] = root_agent_id
|
||
state["collaboration_depth"] = 0
|
||
assigned_role = _assign_agent_for_task_role(task.role)
|
||
child_agent_id = _create_child_agent(state, role=assigned_role, task=task)
|
||
if child_agent_id is None:
|
||
interrupt = _record_interrupt(
|
||
state,
|
||
reason="spawn_blocked",
|
||
task=task,
|
||
payload={"target_role": assigned_role.value},
|
||
)
|
||
_record_recovery(
|
||
state,
|
||
interrupt=interrupt,
|
||
strategy="fallback_to_direct_role_execution",
|
||
task=task,
|
||
payload={"target_role": assigned_role.value},
|
||
)
|
||
child_agent_id = f"blocked-{assigned_role.value}"
|
||
state["messages"] = list(base_history)
|
||
state["current_agent"] = assigned_role.value
|
||
state["agent_id"] = child_agent_id
|
||
state["parent_agent_id"] = coordinator_agent_id
|
||
state["root_agent_id"] = root_agent_id
|
||
state["collaboration_depth"] = 1
|
||
task.thread_id = str(state.get("thread_id") or "") or None
|
||
task.message_id = str(state.get("last_message_id") or "") or None
|
||
task.message_index = int(state.get("message_sequence") or 0) or None
|
||
task.collaboration_budget = state.get("budget_state")
|
||
state["turn_context"] = {"collaboration_task": task.model_dump(mode="json")}
|
||
state["final_response"] = None
|
||
state["verification_status"] = None
|
||
state["verification_summary"] = None
|
||
state["verification_evidence"] = []
|
||
state["clarification_needed"] = False
|
||
state["clarification_question"] = None
|
||
state["stop_reason"] = None
|
||
start_tool_index = len(state.get("tool_outcomes") or [])
|
||
|
||
await _run_sub_commander(
|
||
state,
|
||
assigned_role,
|
||
ROLE_SYSTEM_PROMPTS[assigned_role],
|
||
task.goal or user_query,
|
||
use_tools=True,
|
||
summary_target=None,
|
||
)
|
||
|
||
task_result = _collect_task_result(task, state, start_tool_index)
|
||
_record_checkpoint(
|
||
state,
|
||
"collaboration.task_result_collected",
|
||
reason="task_finished",
|
||
payload={"task_id": task.task_id, "status": task_result.status},
|
||
)
|
||
_append_message_trace(
|
||
state,
|
||
from_agent_id=child_agent_id,
|
||
to_agent_id=coordinator_agent_id,
|
||
message_type="task_update",
|
||
content_summary=task_result.summary or task.title,
|
||
task_id=task.task_id,
|
||
reply_to_message_id=task.message_id,
|
||
payload={"status": task_result.status, "owner_agent_id": task_result.owner_agent_id},
|
||
)
|
||
_append_event_trace(
|
||
state,
|
||
"agent.message.sent",
|
||
payload={"status": task_result.status, "summary": (task_result.summary or "")[:200]},
|
||
task_id=task.task_id,
|
||
parent_task_id=task.parent_task_id,
|
||
child_task_id=(task.child_task_ids or [None])[0],
|
||
message_id=str(state.get("last_message_id") or "") or None,
|
||
)
|
||
if scheduled_subtask is not None:
|
||
_append_event_trace(
|
||
state,
|
||
"agent.subtask.completed",
|
||
payload={
|
||
"subtask_id": scheduled_subtask.get("subtask_id"),
|
||
"status": task_result.status,
|
||
"summary": task_result.summary,
|
||
},
|
||
task_id=task.task_id,
|
||
)
|
||
_apply_task_result_to_state(state, task, task_result)
|
||
|
||
if task_result.status != "completed":
|
||
break
|
||
|
||
state["turn_context"] = None
|
||
state["current_agent"] = AgentRole.MASTER.value
|
||
state["agent_id"] = coordinator_agent_id
|
||
state["parent_agent_id"] = None
|
||
state["root_agent_id"] = root_agent_id
|
||
state["collaboration_depth"] = 0
|
||
state["final_response"] = _build_collaboration_final_response(state.get("task_results") or [])
|
||
_set_phase(
|
||
state, "phase_4_visibility_and_verification", reason="collaboration_verification_started"
|
||
)
|
||
_record_checkpoint(state, "collaboration.verification_started", reason="before_verify")
|
||
_append_event_trace(
|
||
state,
|
||
"agent.verify.started",
|
||
payload={
|
||
"summary_present": bool(state["final_response"]),
|
||
"evidence_count": len(state.get("task_results") or []),
|
||
},
|
||
)
|
||
_verify_collaboration_results(state, tasks, state.get("task_results") or [])
|
||
_append_event_trace(
|
||
state,
|
||
"agent.verify.completed",
|
||
payload={
|
||
"status": state.get("verification_status"),
|
||
"summary": state.get("verification_summary"),
|
||
"evidence_count": len(state.get("verification_evidence") or []),
|
||
},
|
||
severity="error" if state.get("verification_status") == "failed" else "info",
|
||
)
|
||
merge_report = state.get("merge_report") or {}
|
||
if state.get("verification_status") == "failed" and merge_report.get("fallback_used"):
|
||
state["final_response"] = _build_serial_fallback_response(
|
||
user_query,
|
||
state.get("task_results") or [],
|
||
merge_report,
|
||
)
|
||
state["routing_decision"] = {
|
||
"mode": "direct",
|
||
"reason": "fallback_to_serial_recovery",
|
||
}
|
||
_append_event_trace(
|
||
state,
|
||
"agent.rollback.triggered",
|
||
payload={
|
||
"layer": "collaboration_runtime",
|
||
"reason": "merge_fallback_used",
|
||
},
|
||
severity="warning",
|
||
)
|
||
_record_checkpoint(
|
||
state,
|
||
"collaboration.completed",
|
||
reason="collaboration_flow_finished",
|
||
payload={"verification_status": state.get("verification_status")},
|
||
)
|
||
state["messages"] = [*base_history, AIMessage(content=state["final_response"])]
|
||
state["should_respond"] = True
|
||
return state
|
||
|
||
|
||
def _can_delegate_within_hop_budget(state: AgentState) -> bool:
|
||
return _get_state_int(state, "routing_hops") < _get_state_int(state, "max_routing_hops")
|
||
|
||
|
||
def _stop_due_to_loop_guard(state: AgentState) -> AgentState:
|
||
state["terminated_due_to_loop_guard"] = True
|
||
state["final_response"] = (
|
||
"这次需要处理的步骤有点多,我先停在这里。您可以把目标再明确一点,或让我先只完成其中一步。"
|
||
)
|
||
state["messages"] = [*state.get("messages", []), AIMessage(content=state["final_response"])]
|
||
return state
|
||
|
||
|
||
async def master_node(state: AgentState) -> AgentState:
|
||
_maybe_reset_turn_budgets(state)
|
||
_set_phase(state, "phase_1_routing", reason="master_node_entered")
|
||
_record_checkpoint(state, "routing.master_entered", reason="master_node_entered")
|
||
user_messages = _filter_user_messages(state["messages"])
|
||
user_query = (
|
||
_stringify_message_content(user_messages[-1].content).strip() if user_messages else ""
|
||
)
|
||
|
||
state["current_agent"] = state.get("current_agent") or AgentRole.MASTER
|
||
state["active_agents"] = _normalize_active_agents(state.get("active_agents"))
|
||
|
||
if _is_simple_greeting(user_query):
|
||
state["final_response"] = "您好。我在。\n\n您把问题给我,我先帮您收束重点,再往下推。"
|
||
state["messages"] = [*state.get("messages", []), AIMessage(content=state["final_response"])]
|
||
return state
|
||
|
||
if _is_identity_question(user_query):
|
||
state["final_response"] = (
|
||
"我是 Jarvis。\n\n比起做一个泛泛的助手,我更像您的判断型协作伙伴:"
|
||
"帮您看清问题、压缩路径、把事情往前推进。"
|
||
)
|
||
state["messages"] = [*state.get("messages", []), AIMessage(content=state["final_response"])]
|
||
return state
|
||
|
||
if _is_capability_question(user_query):
|
||
state["final_response"] = (
|
||
"主要做三件事。\n"
|
||
"- 帮您判断:看问题本质、梳理取舍、给出方向\n"
|
||
"- 帮您收束:把复杂内容理顺,把重点拎出来\n"
|
||
"- 帮您推进:拆任务、定步骤、把下一步变清楚\n\n"
|
||
"如果您现在有具体目标,我可以直接进入处理。"
|
||
)
|
||
state["messages"] = [*state.get("messages", []), AIMessage(content=state["final_response"])]
|
||
return state
|
||
|
||
state["current_agent"] = _normalize_current_agent(state.get("current_agent"))
|
||
parallel_worthiness = state.get("parallel_worthiness")
|
||
if not isinstance(parallel_worthiness, dict):
|
||
runtime_request_context = state.get("runtime_request_context") or {}
|
||
if isinstance(runtime_request_context, dict):
|
||
candidate_parallel = runtime_request_context.get("parallel_worthiness")
|
||
if isinstance(candidate_parallel, dict):
|
||
parallel_worthiness = candidate_parallel
|
||
if isinstance(parallel_worthiness, dict) and parallel_worthiness:
|
||
_append_event_trace(
|
||
state,
|
||
"agent.parallel.assessed",
|
||
payload=parallel_worthiness,
|
||
)
|
||
skill_shortlist = list(state.get("skill_shortlist") or [])
|
||
if skill_shortlist:
|
||
_append_event_trace(
|
||
state,
|
||
"agent.skill.shortlisted",
|
||
payload={
|
||
"count": len(skill_shortlist),
|
||
"skills": [str(item.get("skill_name") or "") for item in skill_shortlist[:4]],
|
||
},
|
||
)
|
||
task_graph = state.get("task_graph")
|
||
if isinstance(task_graph, dict) and task_graph.get("nodes"):
|
||
_append_event_trace(
|
||
state,
|
||
"agent.task_graph.built",
|
||
payload={
|
||
"graph_id": task_graph.get("graph_id"),
|
||
"node_count": len(task_graph.get("nodes") or []),
|
||
"entry_node_ids": task_graph.get("entry_node_ids") or [],
|
||
"max_parallelism": task_graph.get("max_parallelism"),
|
||
},
|
||
)
|
||
elif (
|
||
isinstance(parallel_worthiness, dict)
|
||
and parallel_worthiness.get("preferred_mode") in {"collaboration", "parallel"}
|
||
and not (state.get("feature_flags") or {}).get("ENABLE_PARALLEL_TASK_GRAPH", True)
|
||
):
|
||
_append_event_trace(
|
||
state,
|
||
"agent.rollback.triggered",
|
||
payload={
|
||
"layer": "parallel_task_graph",
|
||
"reason": "feature_flag_disabled",
|
||
},
|
||
)
|
||
|
||
structured_continuity_route = _route_from_structured_continuity(state, user_query)
|
||
clarification_route = _route_from_clarification_context(state, user_query)
|
||
if structured_continuity_route is not None:
|
||
state["execution_mode"] = "direct"
|
||
routed_agent = structured_continuity_route
|
||
_record_execution_decision(
|
||
state,
|
||
user_query=user_query,
|
||
mode="direct",
|
||
reason="continue_pending_action",
|
||
selected_roles=[routed_agent.value],
|
||
)
|
||
elif clarification_route is not None:
|
||
state["execution_mode"] = "direct"
|
||
routed_agent = clarification_route
|
||
_record_execution_decision(
|
||
state,
|
||
user_query=user_query,
|
||
mode="direct",
|
||
reason="clarification_follow_up",
|
||
selected_roles=[routed_agent.value],
|
||
)
|
||
elif _is_short_confirmation(user_query) and _previous_turn_proposed_schedule_creation(
|
||
state.get("messages", [])
|
||
):
|
||
state["execution_mode"] = "direct"
|
||
routed_agent = AgentRole.SCHEDULE_PLANNER
|
||
_record_execution_decision(
|
||
state,
|
||
user_query=user_query,
|
||
mode="direct",
|
||
reason="schedule_confirmation_follow_up",
|
||
selected_roles=[routed_agent.value],
|
||
)
|
||
else:
|
||
request_mode, routing_metadata = _select_request_mode(user_query)
|
||
state["routing_decision"] = routing_metadata
|
||
_record_execution_decision(
|
||
state,
|
||
user_query=user_query,
|
||
mode=request_mode,
|
||
reason=str(routing_metadata.get("reason") or request_mode),
|
||
selected_roles=list(routing_metadata.get("roles") or []),
|
||
)
|
||
if request_mode == "collaboration":
|
||
collaboration_state = await _run_collaboration_flow(state, user_query)
|
||
if collaboration_state.get(
|
||
"execution_mode"
|
||
) == "collaboration" or collaboration_state.get("final_response"):
|
||
return collaboration_state
|
||
state["execution_mode"] = "direct"
|
||
routed_agent = _route_agent_from_user_query(user_query)
|
||
if routed_agent != AgentRole.MASTER:
|
||
if not _can_delegate_within_hop_budget(state):
|
||
return _stop_due_to_loop_guard(state)
|
||
state["routing_hops"] = int(state.get("routing_hops") or 0) + 1
|
||
state["current_agent"] = routed_agent.value
|
||
state["next_step"] = routed_agent.value
|
||
if routed_agent not in state["active_agents"]:
|
||
state["active_agents"] = [*state["active_agents"], routed_agent]
|
||
state["agent_trace"] = [
|
||
*(state.get("agent_trace") or [AgentRole.MASTER.value]),
|
||
routed_agent.value,
|
||
]
|
||
return state
|
||
|
||
llm = _get_llm_for_state(state)
|
||
response = await _invoke_llm(
|
||
llm, [SystemMessage(content=MASTER_SYSTEM_PROMPT), *state["messages"]]
|
||
)
|
||
_record_response_usage(state, response)
|
||
content = _stringify_message_content(response.content).strip()
|
||
|
||
routed_agent = _route_agent_from_user_query(content)
|
||
if routed_agent != AgentRole.MASTER and len(content) <= 64:
|
||
if not _can_delegate_within_hop_budget(state):
|
||
return _stop_due_to_loop_guard(state)
|
||
state["routing_hops"] = int(state.get("routing_hops") or 0) + 1
|
||
state["current_agent"] = routed_agent.value
|
||
state["next_step"] = routed_agent.value
|
||
if routed_agent not in state["active_agents"]:
|
||
state["active_agents"] = [*state["active_agents"], routed_agent]
|
||
state["agent_trace"] = [
|
||
*(state.get("agent_trace") or [AgentRole.MASTER.value]),
|
||
routed_agent.value,
|
||
]
|
||
return state
|
||
|
||
state["final_response"] = content
|
||
state["messages"] = [*state.get("messages", []), AIMessage(content=content)]
|
||
return state
|
||
|
||
|
||
async def planner_node(state: AgentState) -> AgentState:
|
||
state["next_step"] = None
|
||
user_messages = _filter_user_messages(state["messages"])
|
||
user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""
|
||
if _has_active_clarification_context(state):
|
||
user_query = _build_resumed_clarification_query(state, user_query)
|
||
elif _is_short_confirmation(user_query) and _previous_turn_proposed_schedule_creation(
|
||
state.get("messages", [])
|
||
):
|
||
user_query = _expand_schedule_confirmation_query(user_query, state.get("messages", []))
|
||
return await _run_sub_commander(
|
||
state,
|
||
AgentRole.SCHEDULE_PLANNER,
|
||
ROLE_SYSTEM_PROMPTS[AgentRole.SCHEDULE_PLANNER],
|
||
user_query,
|
||
use_tools=True,
|
||
summary_target="schedule_context_summary",
|
||
)
|
||
|
||
|
||
async def executor_node(state: AgentState) -> AgentState:
|
||
user_messages = _filter_user_messages(state["messages"])
|
||
user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""
|
||
return await _run_sub_commander(
|
||
state,
|
||
AgentRole.EXECUTOR,
|
||
ROLE_SYSTEM_PROMPTS[AgentRole.EXECUTOR],
|
||
user_query,
|
||
use_tools=True,
|
||
)
|
||
|
||
|
||
async def librarian_node(state: AgentState) -> AgentState:
|
||
user_messages = _filter_user_messages(state["messages"])
|
||
user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""
|
||
return await _run_sub_commander(
|
||
state,
|
||
AgentRole.LIBRARIAN,
|
||
ROLE_SYSTEM_PROMPTS[AgentRole.LIBRARIAN],
|
||
user_query,
|
||
use_tools=True,
|
||
summary_target="knowledge_context",
|
||
)
|
||
|
||
|
||
async def analyst_node(state: AgentState) -> AgentState:
|
||
user_messages = _filter_user_messages(state["messages"])
|
||
user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""
|
||
return await _run_sub_commander(
|
||
state,
|
||
AgentRole.ANALYST,
|
||
ROLE_SYSTEM_PROMPTS[AgentRole.ANALYST],
|
||
user_query,
|
||
use_tools=True,
|
||
summary_target="analysis_report",
|
||
)
|
||
|
||
|
||
async def code_commander_node(state: AgentState) -> AgentState:
|
||
"""
|
||
Code Commander node - handles code execution tasks
|
||
Routes to DirectExecutor (LOW risk) or SandboxExecutor (HIGH risk)
|
||
"""
|
||
from app.agents.prompts import CODE_COMMANDER_SYSTEM_PROMPT
|
||
from app.agents.tools.ai_adapter import get_adapter
|
||
from app.agents.tools.direct_executor import DirectExecutor
|
||
from app.agents.tools.sandbox_executor import SandboxExecutor
|
||
from app.agents.tools.security_classifier import RiskLevel, SecurityClassifier
|
||
|
||
user_messages = _filter_user_messages(state["messages"])
|
||
user_query = _stringify_message_content(user_messages[-1].content) if user_messages else ""
|
||
|
||
# Get AI provider from state (default: claude)
|
||
ai_provider = str(state.get("code_ai_provider") or "claude")
|
||
|
||
# Classify risk level
|
||
classifier = SecurityClassifier()
|
||
risk_level = classifier.classify(user_query)
|
||
|
||
# Select executor based on risk
|
||
adapter = get_adapter(ai_provider)
|
||
if risk_level == RiskLevel.LOW:
|
||
executor = DirectExecutor(adapter)
|
||
result_parts = []
|
||
async for line in executor.execute(user_query):
|
||
result_parts.append(line)
|
||
result_output = "".join(result_parts)
|
||
else:
|
||
sandbox_exec = SandboxExecutor(adapter)
|
||
result_parts = []
|
||
async for line in sandbox_exec.execute(user_query):
|
||
result_parts.append(line)
|
||
result_output = "".join(result_parts)
|
||
|
||
state["final_response"] = result_output
|
||
state["code_execution_result"] = {
|
||
"ai_provider": ai_provider,
|
||
"risk_level": risk_level.value,
|
||
"output": result_output,
|
||
}
|
||
state["messages"] = [*state.get("messages", []), AIMessage(content=result_output)]
|
||
state["should_respond"] = True
|
||
return state
|
||
|
||
|
||
def route_agent(state: AgentState) -> str:
|
||
if state.get("final_response"):
|
||
return END
|
||
next_step = _role_value(state.get("next_step"))
|
||
if next_step in _role_values():
|
||
return next_step
|
||
return _role_value(state.get("current_agent") or AgentRole.MASTER)
|
||
|
||
|
||
def _compile_graph(graph: StateGraph, callbacks: list | None = None):
|
||
if callbacks:
|
||
try:
|
||
return graph.compile(callbacks=callbacks)
|
||
except TypeError as exc:
|
||
if "callbacks" not in str(exc):
|
||
raise
|
||
return graph.compile()
|
||
|
||
|
||
def create_agent_graph(callbacks: list | None = None):
|
||
graph = StateGraph(AgentState)
|
||
|
||
graph.add_node(AgentRole.MASTER.value, master_node)
|
||
graph.add_node(AgentRole.SCHEDULE_PLANNER.value, planner_node)
|
||
graph.add_node(AgentRole.EXECUTOR.value, executor_node)
|
||
graph.add_node(AgentRole.LIBRARIAN.value, librarian_node)
|
||
graph.add_node(AgentRole.ANALYST.value, analyst_node)
|
||
graph.add_node(AgentRole.CODE_COMMANDER.value, code_commander_node)
|
||
|
||
graph.set_entry_point(AgentRole.MASTER.value)
|
||
graph.add_conditional_edges(
|
||
AgentRole.MASTER.value,
|
||
route_agent,
|
||
{
|
||
AgentRole.SCHEDULE_PLANNER.value: AgentRole.SCHEDULE_PLANNER.value,
|
||
AgentRole.EXECUTOR.value: AgentRole.EXECUTOR.value,
|
||
AgentRole.LIBRARIAN.value: AgentRole.LIBRARIAN.value,
|
||
AgentRole.ANALYST.value: AgentRole.ANALYST.value,
|
||
AgentRole.CODE_COMMANDER.value: AgentRole.CODE_COMMANDER.value,
|
||
END: END,
|
||
},
|
||
)
|
||
|
||
for role in (
|
||
AgentRole.SCHEDULE_PLANNER,
|
||
AgentRole.EXECUTOR,
|
||
AgentRole.LIBRARIAN,
|
||
AgentRole.ANALYST,
|
||
AgentRole.CODE_COMMANDER,
|
||
):
|
||
graph.add_edge(role.value, END)
|
||
|
||
return _compile_graph(graph, callbacks=callbacks)
|
||
|
||
|
||
_agent_graph = None
|
||
|
||
|
||
def get_agent_graph(callbacks: list | None = None):
|
||
global _agent_graph
|
||
if _agent_graph is None:
|
||
from app.config_tracing import get_langsmith_callbacks
|
||
|
||
langsmith_callbacks = get_langsmith_callbacks()
|
||
all_callbacks = (callbacks or []) + langsmith_callbacks
|
||
_agent_graph = create_agent_graph(callbacks=all_callbacks or None)
|
||
return _agent_graph
|
||
|
||
|
||
__all__ = [
|
||
"_build_collaboration_tasks",
|
||
"_build_verifier_hints",
|
||
"_choose_sub_commander",
|
||
"_parse_json_action",
|
||
"_route_agent_from_user_query",
|
||
"_select_request_mode",
|
||
"_run_collaboration_flow",
|
||
"_run_sub_commander",
|
||
"create_agent_graph",
|
||
"get_agent_graph",
|
||
"master_node",
|
||
]
|