框架: 拆 dispatch_agent_task 为装配/执行/登记

This commit is contained in:
zizi 2026-08-28 06:43:19 +08:00
parent 041af960e7
commit 40bbf554a0
11 changed files with 904 additions and 695 deletions

View File

@ -218,7 +218,7 @@ events(会话)+ reviews(人审)+ revisions(修订 diff)
执行状态(每完成一步在此打标,格式:`[x] YYYY-MM-DD <一句话证据>`;新会话从第一个未勾选项繼续):
- [x] P0.1 删 catalog 双投影 [x] 2026-08-28 git ls-files framework/catalog 为空;generate 写入 .pi/skills 与 .dsh/skills(各 15 个,gitignore);架构测试 12 项绿
- [ ] P0.2 拆 dispatch_agent_task
- [x] P0.2 拆 dispatch_agent_task [x] 2026-08-28 CLI 66 行;runtime 纯度门绿;dispatch 离线测试 37 项绿
- [ ] P0.3 建 data/muse.db(runs/events/reviews/revisions/cards)
- [ ] P0.4 存量迁移 PG → SQLite
- [ ] P1.4 蒸馏闭环(lesson 证据强制 + 人批准 + skill 升格)

1
muse/flow/__init__.py Normal file
View File

@ -0,0 +1 @@
"""写作链装配与编排。"""

680
muse/flow/dispatch.py Normal file
View File

@ -0,0 +1,680 @@
"""把冻结任务包装配后派发给 Agent 执行器,并闭合 Muse 侧证据。
流程(全部失败关闭):
加载 spec -> 装配任务包 -> 登记 example_run -> 事件账本 run.started ->
runtime 执行(事件流逐条入账)-> 证据原子落库(raw + 逐回合 llm_call)
-> 结构化输出校验 -> run.completed + 运行终态。
业务决策(补证、重写、下一步)不属于本入口。
"""
from __future__ import annotations
import json
import os
import sys
from dataclasses import replace
from pathlib import Path
from typing import Any, Callable, Iterable, Mapping
PROJECT_ROOT = next(
parent
for parent in (Path(__file__).resolve().parent, *Path(__file__).resolve().parents)
if (parent / "AGENTS.md").is_file() and (parent / ".git").exists()
)
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
DISPATCH_SCRIPTS = (
PROJECT_ROOT
/ "muse"
/ "lifecycle"
/ "dispatch"
/ "skills"
/ "dispatch-agent-task"
/ "scripts"
)
EVIDENCE_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "record-run-evidence" / "scripts"
READ_TOOLS_DIR = PROJECT_ROOT / "muse" / "authority" / "tools" / "read"
for _path in (DISPATCH_SCRIPTS, EVIDENCE_DIR, READ_TOOLS_DIR):
if str(_path) not in sys.path:
sys.path.insert(0, str(_path))
from role_task import ( # noqa: E402
OutputInvalidError,
TaskSpecError,
build_task_package,
load_spec,
validate_structured_output,
)
from agent_trace import AgentTraceWriter, persist_agent_evidence # noqa: E402
from persist_raw import _check_no_secrets # noqa: E402
import read_tools
from framework.adapters.pi.normalization import normalize_pi_transcript # noqa: E402
from framework.adapters.pi.runner import ( # noqa: E402
AgentStreamOutcome,
ExecutionPolicy,
FrameworkError,
)
from role_policy import validate_role_execution_policy # noqa: E402
from run_registry import finish_run, new_run_id, start_run # noqa: E402
from runtime.agent_executor import execute_agent # noqa: E402
from runtime.runs import ( # noqa: E402
DEFAULT_RUN_DIR_ROOT,
open_transcript,
prepare_run_dir,
read_transcript_text,
remove_local_raw,
seed_run_inputs,
validate_run_id,
write_private_json,
write_private_text,
)
EXIT_OK = 0
EXIT_SPEC_INVALID = 2
EXIT_FRAMEWORK_FAILED = 3
EXIT_OUTPUT_INVALID = 4
EXIT_EVIDENCE_FAILED = 5
READ_TOOL_EXTENSION = PROJECT_ROOT / "framework" / "adapters" / "pi" / "mcp_bridge.ts"
BUILTIN_TOOL_NAMES = frozenset({"read", "grep", "find", "ls"})
def _usage_int(usage: Mapping[str, Any], *keys: str) -> int:
"""读取第一种存在的 usage 字段;脏值与负值按 0 聚合。"""
for key in keys:
if key not in usage:
continue
try:
return max(0, int(usage.get(key) or 0))
except (TypeError, ValueError, OverflowError):
return 0
return 0
def _usage_totals(outcome: AgentStreamOutcome) -> dict[str, int]:
"""聚合全部模型回合的 token 用量;输入口径包含 cache 读写。"""
totals = {"inputTokens": 0, "outputTokens": 0, "cachedTokens": 0, "reasoningTokens": 0}
for call in outcome.model_calls:
usage = call.usage or {}
cached = _usage_int(usage, "cacheRead", "cache_read_input_tokens", "cached_tokens")
cache_write = _usage_int(usage, "cacheWrite", "cache_creation_input_tokens")
totals["inputTokens"] += _usage_int(usage, "input", "input_tokens", "prompt_tokens") + cached + cache_write
totals["outputTokens"] += _usage_int(usage, "output", "output_tokens", "completion_tokens")
totals["cachedTokens"] += cached
totals["reasoningTokens"] += _usage_int(usage, "reasoning", "reasoning_tokens")
return totals
def _cost_totals(outcome: AgentStreamOutcome) -> tuple[float | None, bool]:
"""已知成本求和;任一回合未知则 total 为 None 并标记不完整。"""
known: list[float] = []
complete = True
for call in outcome.model_calls:
if call.cost_usd is None:
complete = False
else:
known.append(call.cost_usd)
if not complete:
return (sum(known) if known else None), False
return sum(known), True
def _model_call_rows(outcome: AgentStreamOutcome) -> list[dict[str, Any]]:
"""把模型回合映射成 agent_trace.persist_agent_evidence 的输入行。"""
return [
{
"actual_model_id": call.actual_model_id,
"usage": dict(call.usage or {}),
"stop_reason": call.stop_reason,
"cost_usd": call.cost_usd,
"duration_ms": None,
}
for call in outcome.model_calls
]
def run_dispatch(
spec_path: str | Path,
*,
repo_root: str | Path,
policy: ExecutionPolicy,
run_id: str | None = None,
run_dir: str | Path | None = None,
connect_factory: Callable[..., Any] | None = None,
launcher: Callable[..., Iterable] | None = None,
trigger_source: str = "user",
trigger_detail: Mapping[str, Any] | None = None,
session_id: str | None = None,
session_dir: str | Path | None = None,
enable_read_tools: bool = False,
) -> tuple[dict[str, Any], int]:
"""执行一次完整派发;所有可控失败都返回稳定回执与退出码。"""
repo_root_path = Path(repo_root).resolve()
try:
spec = load_spec(spec_path)
package = build_task_package(spec, repo_root_path)
_check_no_secrets(package.system_prompt)
_check_no_secrets(package.user_message)
except TaskSpecError as exc:
return (
{
"status": "failed",
"errorCode": "SPEC_INVALID",
"error": str(exc),
"specPath": str(spec_path),
},
EXIT_SPEC_INVALID,
)
except ValueError as exc:
return (
{
"status": "failed",
"errorCode": "SPEC_INVALID",
"error": f"任务包不可安全执行: {type(exc).__name__}",
"specPath": str(spec_path),
},
EXIT_SPEC_INVALID,
)
disabled_read_tools = sorted(
{
tool
for tool in package.spec.tool_allowlist
if tool in read_tools.TOOL_REGISTRY and not enable_read_tools
}
)
if disabled_read_tools:
return (
{
"status": "failed",
"errorCode": "READ_TOOLS_NOT_ENABLED",
"error": f"任务包含探索工具但未启用工具 server: {disabled_read_tools}",
},
EXIT_SPEC_INVALID,
)
policy_kwargs: dict[str, Any] = {"cwd": policy.cwd or str(repo_root_path)}
if session_id is not None:
policy_kwargs["session_id"] = session_id
policy_kwargs["session_dir"] = str(session_dir) if session_dir is not None else None
if enable_read_tools:
# 工具 server 扩展经环境变量定位只读实现;框架进程继承该环境。
os.environ["MUSE_READ_TOOLS_PYTHON"] = sys.executable
os.environ["MUSE_READ_TOOLS_SCRIPT"] = str(
READ_TOOLS_DIR / "read_tools.py"
)
policy_kwargs["extension_path"] = str(READ_TOOL_EXTENSION)
effective_policy = replace(policy, **policy_kwargs)
try:
validate_role_execution_policy(package, effective_policy)
except ValueError as exc:
return (
{
"status": "failed",
"errorCode": "ROLE_MODEL_POLICY_MISMATCH",
"error": str(exc),
"requestedModelId": effective_policy.requested_model_id,
},
EXIT_SPEC_INVALID,
)
run_id = run_id or new_run_id(
f"agent-{spec.role}", work_id=spec.work_id, target_chapter=spec.target_chapter
)
if not validate_run_id(run_id):
return (
{
"status": "failed",
"errorCode": "RUN_ID_INVALID",
"error": "run_id 只能包含 ASCII 字母、数字、点、下划线和短横线,长度不超过 64",
},
EXIT_SPEC_INVALID,
)
run_dir_path = Path(run_dir) if run_dir is not None else DEFAULT_RUN_DIR_ROOT / run_id
identity = package.as_identity()
def _receipt(**fields: Any) -> dict[str, Any]:
base = {
"runId": run_id,
"role": spec.role,
"framework": effective_policy.framework,
"requestedModelId": effective_policy.requested_model_id,
"outputSchemaId": spec.output_schema_id,
"runDir": str(run_dir_path),
**identity,
}
base.update(fields)
return base
try:
run_dir_path = prepare_run_dir(run_id, run_dir)
seed_run_inputs(
run_dir_path,
spec_text=Path(spec_path).read_text(encoding="utf-8"),
system_prompt=package.system_prompt,
user_message=package.user_message,
)
except (OSError, UnicodeError) as exc:
return (
_receipt(
status="failed",
errorCode="AUDIT_WRITE_FAILED",
error=f"运行审计目录写入失败: {type(exc).__name__}",
),
EXIT_EVIDENCE_FAILED,
)
try:
detail = dict(trigger_detail or {})
except (TypeError, ValueError) as exc:
receipt = _receipt(
status="failed",
errorCode="TRIGGER_DETAIL_INVALID",
error=f"trigger_detail 不是对象: {type(exc).__name__}",
)
write_private_json(run_dir_path / "receipt.json", receipt)
return receipt, EXIT_SPEC_INVALID
detail.setdefault(
"dispatch",
{
"framework": effective_policy.framework,
"requestedModelId": effective_policy.requested_model_id,
"specSha256": package.spec_sha256,
},
)
try:
detail_json = json.dumps(
detail, ensure_ascii=False, sort_keys=True, default=str, allow_nan=False
)
_check_no_secrets(detail_json)
except (TypeError, ValueError) as exc:
receipt = _receipt(
status="failed",
errorCode="TRIGGER_DETAIL_INVALID",
error=f"trigger_detail 不可安全记录: {type(exc).__name__}",
)
write_private_json(run_dir_path / "receipt.json", receipt)
return receipt, EXIT_SPEC_INVALID
try:
run_record = start_run(
connect=connect_factory,
run_id=run_id,
work_id=spec.work_id,
target_chapter=spec.target_chapter,
trigger_source=trigger_source,
trigger_detail=detail,
)
except Exception as exc: # noqa: BLE001 - 注册失败不能启动外部 Agent。
receipt = _receipt(
status="failed",
errorCode="RUN_REGISTRY_START_FAILED",
error=f"运行登记失败: {type(exc).__name__}",
)
write_private_json(run_dir_path / "receipt.json", receipt)
return receipt, EXIT_EVIDENCE_FAILED
if run_record["status"] != "started":
receipt = _receipt(
status="failed",
errorCode="RUN_ID_EXISTS",
error="run_id 已存在,拒绝覆盖既有运行证据",
)
write_private_json(run_dir_path / "receipt.json", receipt)
return receipt, EXIT_EVIDENCE_FAILED
writer = AgentTraceWriter(
run_id=run_id,
framework=effective_policy.framework,
agent_role=spec.role,
connect=connect_factory,
)
def _failed(
error_code: str,
error: str,
exit_code: int,
*,
agent_failed: bool = False,
session_id: str | None = None,
final_message: str | None = None,
evidence: Mapping[str, Any] | None = None,
cause_error_code: str | None = None,
) -> tuple[dict[str, Any], int]:
"""尽力闭合失败终态;留痕本身失败时升级为证据错误,保留原始原因码。"""
failures: list[str] = []
if final_message is not None:
try:
write_private_text(run_dir_path / "final-message.txt", final_message)
except OSError as exc:
failures.append(type(exc).__name__)
if agent_failed:
try:
writer.emit(
"agent.failed",
status="error",
requested_model_id=effective_policy.requested_model_id,
details={"errorCode": error_code},
)
except Exception as exc: # noqa: BLE001 - 继续尝试写 run.failed/终态。
failures.append(type(exc).__name__)
try:
writer.emit(
"run.failed",
status="error",
requested_model_id=effective_policy.requested_model_id,
raw_ref=(evidence or {}).get("transcriptId"),
details={"errorCode": error_code},
)
except Exception as exc: # noqa: BLE001 - 继续尝试闭合 example_run。
failures.append(type(exc).__name__)
try:
finish_run(
run_id,
"failed",
trigger_detail={"errorCode": error_code},
connect=connect_factory,
)
except Exception as exc: # noqa: BLE001 - 回执必须揭示终态未能闭合。
failures.append(type(exc).__name__)
fields: dict[str, Any] = {
"status": "failed",
"errorCode": error_code,
"error": error,
"sessionId": session_id,
}
if evidence is not None:
fields["evidence"] = dict(evidence)
if cause_error_code is not None:
fields["causeErrorCode"] = cause_error_code
if failures:
fields.update(
{
"causeErrorCode": cause_error_code or error_code,
"errorCode": "EVIDENCE_PERSIST_FAILED",
"error": "失败终态留痕未完整写入",
"finalizationErrorTypes": sorted(set(failures)),
}
)
exit_code = EXIT_EVIDENCE_FAILED
receipt = _receipt(**fields)
try:
write_private_json(run_dir_path / "receipt.json", receipt)
except OSError:
receipt["receiptFileWritten"] = False
exit_code = EXIT_EVIDENCE_FAILED
return receipt, exit_code
try:
writer.emit(
"run.started",
status="ok",
requested_model_id=effective_policy.requested_model_id,
details={"specSha256": package.spec_sha256, "toolAllowlist": list(spec.tool_allowlist)},
)
except Exception as exc: # noqa: BLE001 - 未留起始事件时不得启动框架。
return _failed(
"EVIDENCE_PERSIST_FAILED",
f"运行起始事件写入失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
)
transcript_path = run_dir_path / "transcript.jsonl"
def _persist_failure_evidence(
failed_outcome: AgentStreamOutcome | None,
) -> tuple[dict[str, Any] | None, str | None]:
"""失败也尽量把已有转录和模型回合写入同一套 raw 证据。"""
if failed_outcome is None:
return None, None
try:
transcript_text = read_transcript_text(run_dir_path)
except (OSError, UnicodeError):
return None, "EVIDENCE_PERSIST_FAILED"
if not transcript_text.strip():
return None, None
try:
_check_no_secrets(transcript_text)
except ValueError:
remove_local_raw(run_dir_path)
return None, "RAW_SECRET_DETECTED"
try:
evidence_result = persist_agent_evidence(
run_id=run_id,
agent_role=spec.role,
system_prompt=package.system_prompt,
user_message=package.user_message,
final_message=failed_outcome.final_text,
transcript=transcript_text,
model_calls=_model_call_rows(failed_outcome),
requested_model_id=effective_policy.requested_model_id,
connect=connect_factory,
)
return evidence_result, None
except Exception:
return None, "EVIDENCE_PERSIST_FAILED"
try:
with open_transcript(run_dir_path) as transcript_file:
outcome = execute_agent(
package.as_framework_request(
session_mode="continue" if session_id is not None else "fresh",
request_id=run_id,
),
effective_policy,
writer,
timeout_seconds=spec.max_duration_seconds,
raw_sink=transcript_file.write,
launcher=launcher,
)
except FrameworkError as exc:
failed_outcome = exc.outcome
failure_evidence, evidence_error = _persist_failure_evidence(failed_outcome)
if evidence_error is not None:
return _failed(
evidence_error,
"框架失败证据未能安全落库",
EXIT_EVIDENCE_FAILED,
agent_failed=True,
session_id=failed_outcome.session_id if failed_outcome else None,
cause_error_code=exc.error_code,
)
return _failed(
exc.error_code,
str(exc),
EXIT_FRAMEWORK_FAILED,
agent_failed=True,
session_id=failed_outcome.session_id if failed_outcome else None,
final_message=failed_outcome.final_text if failed_outcome else None,
evidence=failure_evidence,
)
except Exception as exc: # noqa: BLE001 - 事件/本地转录失败属于证据失败。
return _failed(
"EVIDENCE_PERSIST_FAILED",
f"框架执行留痕失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
agent_failed=True,
)
try:
transcript_text = read_transcript_text(run_dir_path)
_check_no_secrets(transcript_text)
except ValueError as exc:
remove_local_raw(run_dir_path)
return _failed(
"RAW_SECRET_DETECTED",
"框架转录含疑似凭据,已拒绝留存",
EXIT_EVIDENCE_FAILED,
)
except (OSError, UnicodeError) as exc:
return _failed(
"EVIDENCE_PERSIST_FAILED",
f"框架转录回读失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
)
if not transcript_text.strip():
return _failed(
"TRANSCRIPT_EMPTY",
"框架转录为空",
EXIT_FRAMEWORK_FAILED,
session_id=outcome.session_id,
)
try:
framework_artifact = normalize_pi_transcript(
transcript_path,
run_dir_path / "framework-events.jsonl",
run_id=run_id,
framework_version="pi-json-v1",
)
except (OSError, UnicodeError, ValueError) as exc:
return _failed(
"FRAMEWORK_ARTIFACT_INVALID",
f"通用框架事件工件生成失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
)
try:
evidence = persist_agent_evidence(
run_id=run_id,
agent_role=spec.role,
system_prompt=package.system_prompt,
user_message=package.user_message,
final_message=outcome.final_text,
transcript=transcript_text,
model_calls=_model_call_rows(outcome),
requested_model_id=effective_policy.requested_model_id,
connect=connect_factory,
)
except Exception as exc: # noqa: BLE001 - 模型成功但证据失败时必须失败关闭。
return _failed(
"EVIDENCE_PERSIST_FAILED",
f"框架证据落库失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
final_message=outcome.final_text,
)
try:
structured = validate_structured_output(outcome.final_text or "", spec)
except OutputInvalidError as exc:
return _failed(
"OUTPUT_SCHEMA_INVALID",
str(exc),
EXIT_OUTPUT_INVALID,
session_id=outcome.session_id,
final_message=outcome.final_text,
evidence=evidence,
)
try:
write_private_text(run_dir_path / "final-message.txt", outcome.final_text or "")
write_private_json(run_dir_path / "output.json", structured)
except OSError as exc:
return _failed(
"AUDIT_WRITE_FAILED",
f"运行结果审计文件写入失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
evidence=evidence,
)
# 依赖清单:本次运行实际读取了什么(工具调用序列与参数),链路透视的读侧材料。
dependency_count = len(outcome.tool_calls)
if dependency_count:
dependencies = [
{
"seq": index + 1,
"tool": call.name,
"args": dict(call.args) if call.args else {},
"isError": call.is_error,
}
for index, call in enumerate(outcome.tool_calls)
]
try:
write_private_json(run_dir_path / "dependencies.json", dependencies)
except OSError as exc:
return _failed(
"AUDIT_WRITE_FAILED",
f"依赖清单写入失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
evidence=evidence,
)
usage = _usage_totals(outcome)
total_cost, cost_complete = _cost_totals(outcome)
try:
writer.emit(
"run.completed",
status="ok",
requested_model_id=effective_policy.requested_model_id,
actual_model_id=outcome.model_calls[-1].actual_model_id,
usage={
"input": usage["inputTokens"] - usage["cachedTokens"],
"output": usage["outputTokens"],
"cacheRead": usage["cachedTokens"],
},
cost_usd=total_cost,
raw_ref=evidence.get("transcriptId"),
details={
"sessionId": outcome.session_id,
"turns": outcome.turns,
"toolCalls": len(outcome.tool_calls),
"unknownFrameworkEvents": list(outcome.unknown_event_types),
"costComplete": cost_complete,
"leaseId": evidence.get("leaseId"),
"llmCallIds": evidence.get("llmCallIds"),
},
)
finish_run(run_id, "completed", connect=connect_factory)
except Exception as exc: # noqa: BLE001 - 成功终态与终态事件必须一起可见。
return _failed(
"RUN_FINALIZE_FAILED",
f"成功终态写入失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
evidence=evidence,
)
receipt = _receipt(
status="completed",
sessionId=outcome.session_id,
durationMs=outcome.duration_ms,
turns=outcome.turns,
toolCallCount=len(outcome.tool_calls),
unknownFrameworkEvents=list(outcome.unknown_event_types),
dependencies=(
{"count": dependency_count, "file": "dependencies.json"}
if dependency_count
else None
),
modelCallCount=len(outcome.model_calls),
actualModelIds=[call.actual_model_id for call in outcome.model_calls],
usage=usage,
totalCostUsd=round(total_cost, 6) if total_cost is not None else None,
costComplete=cost_complete,
finalMessageSha256=_sha256_bare(outcome.final_text or ""),
structuredOutputSha256=_sha256_bare(
json.dumps(structured, ensure_ascii=False, sort_keys=True)
),
evidence=evidence,
frameworkArtifact=framework_artifact,
)
try:
write_private_json(run_dir_path / "receipt.json", receipt)
except OSError:
receipt["receiptFileWritten"] = False
return receipt, EXIT_OK
def _sha256_bare(text: str) -> str:
import hashlib
return hashlib.sha256(text.encode("utf-8")).hexdigest()

View File

@ -20,12 +20,15 @@ disable-model-invocation: true
| 模块 | 职责 |
|---|---|
| `muse/lifecycle/dispatch/skills/dispatch-agent-task/scripts/role_task.py` | Muse 侧任务包装配:spec、角色合同、业务 Schema 和模型策略前置校验。 |
| `muse/flow/dispatch.py` | 装配与编排:任务包、角色模型策略、证据账本、结构化输出校验;调用 runtime 执行。 |
| `runtime/agent_executor.py` | 多轮 Agent 调用;不加载 Muse 合同。 |
| `runtime/runs.py` | 不可变 run 目录读写;不加载 Muse 合同。 |
| `framework/primitives/execution.py` | 框架通用执行请求、事件和结果对象;不加载 Muse 合同。 |
| `framework/adapters/pi/runner.py` | Pi 框架适配器:只消费 `FrameworkExecutionRequest`,构造 argv、消费 JSON 事件流并执行超时保护;当前生产入口。 |
| `framework/adapters/dsh/` | DSH headless 对照适配器:只开放无工具 fresh 任务,读取 flush 后 session JSONL;未接入本 Skill 的生产派发。 |
| `muse/lifecycle/dispatch/skills/dispatch-agent-task/scripts/read_tools.py` | 探索工具 server 只读实现:五个登记工具(细纲/文风/范式绑定/章节正文/实体检索),只读连接、未知工具拒绝、结果有界;`--list` 输出登记表。 |
| `framework/adapters/pi/mcp_bridge.ts` | 工具 server 的框架扩展:装载时从 `read_tools.py --list` 动态注册工具,每次执行转给只读实现;不经派发器注入环境变量时不注册任何工具。 |
| `muse/lifecycle/dispatch/skills/dispatch-agent-task/scripts/dispatch_agent_task.py` | CLI:运行登记 -> 事件入账 -> 框架执行 -> 校验 -> 证据落库 -> 依赖清单 -> 回执。 |
| `muse/lifecycle/dispatch/skills/dispatch-agent-task/scripts/dispatch_agent_task.py` | CLI 薄入口:解析参数后调用 `muse.flow.dispatch.run_dispatch`。 |
## 输入与输出

View File

@ -1,708 +1,31 @@
#!/usr/bin/env python3
"""dispatch-agent-task CLI:把冻结任务包派发给 Agent 框架子代理并自动留痕。
"""dispatch-agent-task CLI:装配见 muse.flow.dispatch,执行见 runtime。"""
流程(全部失败关闭):
加载 spec -> 装配任务包 -> 登记 example_run -> 事件账本 run.started ->
框架适配器执行(事件流逐条入账)-> 证据原子落库(raw + 逐回合 llm_call)
-> 结构化输出校验 -> run.completed + 运行终态 -> 打印回执。
业务决策(补证、重写、下一步)不属于本入口:那是框架里模型的事。
用法:
.venv/bin/python dispatch_agent_task.py --spec task.json \
[--provider P] [--model M] [--thinking low] [--run-id ID]
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from dataclasses import replace
from pathlib import Path
from typing import Any, Callable, Iterable, Mapping
SCRIPT_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = next(
parent
for parent in (SCRIPT_DIR, *SCRIPT_DIR.parents)
for parent in (Path(__file__).resolve().parent, *Path(__file__).resolve().parents)
if (parent / "AGENTS.md").is_file() and (parent / ".git").exists()
)
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
EVIDENCE_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "record-run-evidence" / "scripts"
READ_TOOLS_DIR = PROJECT_ROOT / "muse" / "authority" / "tools" / "read"
for _path in (EVIDENCE_DIR, READ_TOOLS_DIR):
if str(_path) not in sys.path:
sys.path.insert(0, str(_path))
from role_task import ( # noqa: E402
AgentTaskSpec,
OutputInvalidError,
TaskSpecError,
build_task_package,
load_spec,
validate_structured_output,
)
from agent_trace import AgentTraceWriter, persist_agent_evidence # noqa: E402
from persist_raw import _check_no_secrets # noqa: E402
import read_tools
from framework.adapters.pi.normalization import normalize_pi_transcript # noqa: E402
from framework.adapters.pi.runner import ( # noqa: E402
DEFAULT_PI_BIN,
AgentStreamOutcome,
ExecutionPolicy,
FrameworkError,
PiAgentRunner,
)
from role_policy import validate_role_execution_policy # noqa: E402
from run_registry import finish_run, new_run_id, start_run # noqa: E402
EXIT_OK = 0
EXIT_SPEC_INVALID = 2
EXIT_FRAMEWORK_FAILED = 3
EXIT_OUTPUT_INVALID = 4
EXIT_EVIDENCE_FAILED = 5
DEFAULT_RUN_DIR_ROOT = Path("/tmp/muse-agent-runs")
_RUN_ID_PATTERN = re.compile(r"^[A-Za-z0-9_.-]{1,64}$")
def _usage_int(usage: Mapping[str, Any], *keys: str) -> int:
"""读取第一种存在的 usage 字段;脏值与负值按 0 聚合。"""
for key in keys:
if key not in usage:
continue
try:
return max(0, int(usage.get(key) or 0))
except (TypeError, ValueError, OverflowError):
return 0
return 0
def _usage_totals(outcome: AgentStreamOutcome) -> dict[str, int]:
"""聚合全部模型回合的 token 用量;输入口径包含 cache 读写。"""
totals = {"inputTokens": 0, "outputTokens": 0, "cachedTokens": 0, "reasoningTokens": 0}
for call in outcome.model_calls:
usage = call.usage or {}
cached = _usage_int(usage, "cacheRead", "cache_read_input_tokens", "cached_tokens")
cache_write = _usage_int(usage, "cacheWrite", "cache_creation_input_tokens")
totals["inputTokens"] += _usage_int(usage, "input", "input_tokens", "prompt_tokens") + cached + cache_write
totals["outputTokens"] += _usage_int(usage, "output", "output_tokens", "completion_tokens")
totals["cachedTokens"] += cached
totals["reasoningTokens"] += _usage_int(usage, "reasoning", "reasoning_tokens")
return totals
def _cost_totals(outcome: AgentStreamOutcome) -> tuple[float | None, bool]:
"""已知成本求和;任一回合未知则 total 为 None 并标记不完整。"""
known: list[float] = []
complete = True
for call in outcome.model_calls:
if call.cost_usd is None:
complete = False
else:
known.append(call.cost_usd)
if not complete:
return (sum(known) if known else None), False
return sum(known), True
def _model_call_rows(outcome: AgentStreamOutcome) -> list[dict[str, Any]]:
"""把模型回合映射成 agent_trace.persist_agent_evidence 的输入行。"""
return [
{
"actual_model_id": call.actual_model_id,
"usage": dict(call.usage or {}),
"stop_reason": call.stop_reason,
"cost_usd": call.cost_usd,
"duration_ms": None,
}
for call in outcome.model_calls
]
def _write_private_text(path: Path, text: str) -> None:
"""创建仅当前用户可读写的运行审计文件。"""
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(text)
path.chmod(0o600)
def _write_private_json(path: Path, value: Mapping[str, Any]) -> None:
_write_private_text(path, json.dumps(value, ensure_ascii=False, indent=2))
# 工具 server 扩展路径与内建只读文件工具(探索白名单的两类合法成员)。
READ_TOOL_EXTENSION = PROJECT_ROOT / "framework" / "adapters" / "pi" / "mcp_bridge.ts"
BUILTIN_TOOL_NAMES = frozenset({"read", "grep", "find", "ls"})
def run_dispatch(
spec_path: str | Path,
*,
repo_root: str | Path,
policy: ExecutionPolicy,
run_id: str | None = None,
run_dir: str | Path | None = None,
connect_factory: Callable[..., Any] | None = None,
launcher: Callable[..., Iterable] | None = None,
trigger_source: str = "user",
trigger_detail: Mapping[str, Any] | None = None,
session_id: str | None = None,
session_dir: str | Path | None = None,
enable_read_tools: bool = False,
) -> tuple[dict[str, Any], int]:
"""执行一次完整派发;所有可控失败都返回稳定回执与退出码。"""
repo_root_path = Path(repo_root).resolve()
try:
spec = load_spec(spec_path)
package = build_task_package(spec, repo_root_path)
_check_no_secrets(package.system_prompt)
_check_no_secrets(package.user_message)
except TaskSpecError as exc:
return (
{
"status": "failed",
"errorCode": "SPEC_INVALID",
"error": str(exc),
"specPath": str(spec_path),
},
EXIT_SPEC_INVALID,
)
except ValueError as exc:
return (
{
"status": "failed",
"errorCode": "SPEC_INVALID",
"error": f"任务包不可安全执行: {type(exc).__name__}",
"specPath": str(spec_path),
},
EXIT_SPEC_INVALID,
)
disabled_read_tools = sorted(
{
tool
for tool in package.spec.tool_allowlist
if tool in read_tools.TOOL_REGISTRY and not enable_read_tools
}
)
if disabled_read_tools:
return (
{
"status": "failed",
"errorCode": "READ_TOOLS_NOT_ENABLED",
"error": f"任务包含探索工具但未启用工具 server: {disabled_read_tools}",
},
EXIT_SPEC_INVALID,
)
policy_kwargs: dict[str, Any] = {"cwd": policy.cwd or str(repo_root_path)}
if session_id is not None:
policy_kwargs["session_id"] = session_id
policy_kwargs["session_dir"] = str(session_dir) if session_dir is not None else None
if enable_read_tools:
# 工具 server 扩展经环境变量定位只读实现;框架进程继承该环境。
os.environ["MUSE_READ_TOOLS_PYTHON"] = sys.executable
os.environ["MUSE_READ_TOOLS_SCRIPT"] = str(
READ_TOOLS_DIR / "read_tools.py"
)
policy_kwargs["extension_path"] = str(READ_TOOL_EXTENSION)
effective_policy = replace(policy, **policy_kwargs)
try:
validate_role_execution_policy(package, effective_policy)
except ValueError as exc:
return (
{
"status": "failed",
"errorCode": "ROLE_MODEL_POLICY_MISMATCH",
"error": str(exc),
"requestedModelId": effective_policy.requested_model_id,
},
EXIT_SPEC_INVALID,
)
run_id = run_id or new_run_id(
f"agent-{spec.role}", work_id=spec.work_id, target_chapter=spec.target_chapter
)
if not isinstance(run_id, str) or _RUN_ID_PATTERN.fullmatch(run_id) is None:
return (
{
"status": "failed",
"errorCode": "RUN_ID_INVALID",
"error": "run_id 只能包含 ASCII 字母、数字、点、下划线和短横线,长度不超过 64",
},
EXIT_SPEC_INVALID,
)
run_dir_path = Path(run_dir) if run_dir is not None else DEFAULT_RUN_DIR_ROOT / run_id
identity = package.as_identity()
def _receipt(**fields: Any) -> dict[str, Any]:
base = {
"runId": run_id,
"role": spec.role,
"framework": effective_policy.framework,
"requestedModelId": effective_policy.requested_model_id,
"outputSchemaId": spec.output_schema_id,
"runDir": str(run_dir_path),
**identity,
}
base.update(fields)
return base
try:
if run_dir is None:
DEFAULT_RUN_DIR_ROOT.mkdir(parents=True, mode=0o700, exist_ok=True)
DEFAULT_RUN_DIR_ROOT.chmod(0o700)
run_dir_path.mkdir(parents=True, mode=0o700, exist_ok=False)
run_dir_path.chmod(0o700)
_write_private_text(
run_dir_path / "task-spec.json", Path(spec_path).read_text(encoding="utf-8")
)
_write_private_text(run_dir_path / "system-prompt.txt", package.system_prompt)
_write_private_text(run_dir_path / "user-message.txt", package.user_message)
except (OSError, UnicodeError) as exc:
return (
_receipt(
status="failed",
errorCode="AUDIT_WRITE_FAILED",
error=f"运行审计目录写入失败: {type(exc).__name__}",
),
from framework.adapters.pi.runner import DEFAULT_PI_BIN, ExecutionPolicy # noqa: E402
from muse.flow.dispatch import ( # noqa: E402
DEFAULT_RUN_DIR_ROOT,
EXIT_EVIDENCE_FAILED,
)
try:
detail = dict(trigger_detail or {})
except (TypeError, ValueError) as exc:
receipt = _receipt(
status="failed",
errorCode="TRIGGER_DETAIL_INVALID",
error=f"trigger_detail 不是对象: {type(exc).__name__}",
)
_write_private_json(run_dir_path / "receipt.json", receipt)
return receipt, EXIT_SPEC_INVALID
detail.setdefault(
"dispatch",
{
"framework": effective_policy.framework,
"requestedModelId": effective_policy.requested_model_id,
"specSha256": package.spec_sha256,
},
)
try:
detail_json = json.dumps(
detail, ensure_ascii=False, sort_keys=True, default=str, allow_nan=False
)
_check_no_secrets(detail_json)
except (TypeError, ValueError) as exc:
receipt = _receipt(
status="failed",
errorCode="TRIGGER_DETAIL_INVALID",
error=f"trigger_detail 不可安全记录: {type(exc).__name__}",
)
_write_private_json(run_dir_path / "receipt.json", receipt)
return receipt, EXIT_SPEC_INVALID
try:
run_record = start_run(
connect=connect_factory,
run_id=run_id,
work_id=spec.work_id,
target_chapter=spec.target_chapter,
trigger_source=trigger_source,
trigger_detail=detail,
)
except Exception as exc: # noqa: BLE001 - 注册失败不能启动外部 Agent。
receipt = _receipt(
status="failed",
errorCode="RUN_REGISTRY_START_FAILED",
error=f"运行登记失败: {type(exc).__name__}",
)
_write_private_json(run_dir_path / "receipt.json", receipt)
return receipt, EXIT_EVIDENCE_FAILED
if run_record["status"] != "started":
receipt = _receipt(
status="failed",
errorCode="RUN_ID_EXISTS",
error="run_id 已存在,拒绝覆盖既有运行证据",
)
_write_private_json(run_dir_path / "receipt.json", receipt)
return receipt, EXIT_EVIDENCE_FAILED
writer = AgentTraceWriter(
run_id=run_id,
framework=effective_policy.framework,
agent_role=spec.role,
connect=connect_factory,
)
runner = PiAgentRunner(launcher=launcher)
def _failed(
error_code: str,
error: str,
exit_code: int,
*,
agent_failed: bool = False,
session_id: str | None = None,
final_message: str | None = None,
evidence: Mapping[str, Any] | None = None,
cause_error_code: str | None = None,
) -> tuple[dict[str, Any], int]:
"""尽力闭合失败终态;留痕本身失败时升级为证据错误,保留原始原因码。"""
failures: list[str] = []
if final_message is not None:
try:
_write_private_text(run_dir_path / "final-message.txt", final_message)
except OSError as exc:
failures.append(type(exc).__name__)
if agent_failed:
try:
writer.emit(
"agent.failed",
status="error",
requested_model_id=effective_policy.requested_model_id,
details={"errorCode": error_code},
)
except Exception as exc: # noqa: BLE001 - 继续尝试写 run.failed/终态。
failures.append(type(exc).__name__)
try:
writer.emit(
"run.failed",
status="error",
requested_model_id=effective_policy.requested_model_id,
raw_ref=(evidence or {}).get("transcriptId"),
details={"errorCode": error_code},
)
except Exception as exc: # noqa: BLE001 - 继续尝试闭合 example_run。
failures.append(type(exc).__name__)
try:
finish_run(
run_id,
"failed",
trigger_detail={"errorCode": error_code},
connect=connect_factory,
)
except Exception as exc: # noqa: BLE001 - 回执必须揭示终态未能闭合。
failures.append(type(exc).__name__)
fields: dict[str, Any] = {
"status": "failed",
"errorCode": error_code,
"error": error,
"sessionId": session_id,
}
if evidence is not None:
fields["evidence"] = dict(evidence)
if cause_error_code is not None:
fields["causeErrorCode"] = cause_error_code
if failures:
fields.update(
{
"causeErrorCode": cause_error_code or error_code,
"errorCode": "EVIDENCE_PERSIST_FAILED",
"error": "失败终态留痕未完整写入",
"finalizationErrorTypes": sorted(set(failures)),
}
)
exit_code = EXIT_EVIDENCE_FAILED
receipt = _receipt(**fields)
try:
_write_private_json(run_dir_path / "receipt.json", receipt)
except OSError:
receipt["receiptFileWritten"] = False
exit_code = EXIT_EVIDENCE_FAILED
return receipt, exit_code
try:
writer.emit(
"run.started",
status="ok",
requested_model_id=effective_policy.requested_model_id,
details={"specSha256": package.spec_sha256, "toolAllowlist": list(spec.tool_allowlist)},
)
except Exception as exc: # noqa: BLE001 - 未留起始事件时不得启动框架。
return _failed(
"EVIDENCE_PERSIST_FAILED",
f"运行起始事件写入失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
)
transcript_path = run_dir_path / "transcript.jsonl"
def _remove_local_raw() -> None:
for path in (transcript_path, run_dir_path / "final-message.txt"):
try:
path.unlink(missing_ok=True)
except OSError:
pass
def _persist_failure_evidence(
failed_outcome: AgentStreamOutcome | None,
) -> tuple[dict[str, Any] | None, str | None]:
"""失败也尽量把已有转录和模型回合写入同一套 raw 证据。"""
if failed_outcome is None:
return None, None
try:
transcript_text = transcript_path.read_text(encoding="utf-8")
except (OSError, UnicodeError):
return None, "EVIDENCE_PERSIST_FAILED"
if not transcript_text.strip():
return None, None
try:
_check_no_secrets(transcript_text)
except ValueError:
_remove_local_raw()
return None, "RAW_SECRET_DETECTED"
try:
evidence_result = persist_agent_evidence(
run_id=run_id,
agent_role=spec.role,
system_prompt=package.system_prompt,
user_message=package.user_message,
final_message=failed_outcome.final_text,
transcript=transcript_text,
model_calls=_model_call_rows(failed_outcome),
requested_model_id=effective_policy.requested_model_id,
connect=connect_factory,
)
return evidence_result, None
except Exception:
return None, "EVIDENCE_PERSIST_FAILED"
try:
transcript_fd = os.open(
transcript_path,
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
0o600,
)
with os.fdopen(transcript_fd, "wb") as transcript_file:
outcome = runner.run(
package.as_framework_request(
session_mode="continue" if session_id is not None else "fresh",
request_id=run_id,
),
effective_policy,
writer,
timeout_seconds=spec.max_duration_seconds,
raw_sink=transcript_file.write,
)
transcript_path.chmod(0o600)
except FrameworkError as exc:
failed_outcome = exc.outcome
failure_evidence, evidence_error = _persist_failure_evidence(failed_outcome)
if evidence_error is not None:
return _failed(
evidence_error,
"框架失败证据未能安全落库",
EXIT_EVIDENCE_FAILED,
agent_failed=True,
session_id=failed_outcome.session_id if failed_outcome else None,
cause_error_code=exc.error_code,
)
return _failed(
exc.error_code,
str(exc),
EXIT_FRAMEWORK_FAILED,
agent_failed=True,
session_id=failed_outcome.session_id if failed_outcome else None,
final_message=failed_outcome.final_text if failed_outcome else None,
evidence=failure_evidence,
)
except Exception as exc: # noqa: BLE001 - 事件/本地转录失败属于证据失败。
return _failed(
"EVIDENCE_PERSIST_FAILED",
f"框架执行留痕失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
agent_failed=True,
)
try:
transcript_text = transcript_path.read_text(encoding="utf-8")
_check_no_secrets(transcript_text)
except ValueError as exc:
_remove_local_raw()
return _failed(
"RAW_SECRET_DETECTED",
"框架转录含疑似凭据,已拒绝留存",
EXIT_EVIDENCE_FAILED,
)
except (OSError, UnicodeError) as exc:
return _failed(
"EVIDENCE_PERSIST_FAILED",
f"框架转录回读失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
)
if not transcript_text.strip():
return _failed(
"TRANSCRIPT_EMPTY",
"框架转录为空",
EXIT_FRAMEWORK_FAILED,
session_id=outcome.session_id,
)
try:
framework_artifact = normalize_pi_transcript(
transcript_path,
run_dir_path / "framework-events.jsonl",
run_id=run_id,
framework_version="pi-json-v1",
)
except (OSError, UnicodeError, ValueError) as exc:
return _failed(
"FRAMEWORK_ARTIFACT_INVALID",
f"通用框架事件工件生成失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
)
try:
evidence = persist_agent_evidence(
run_id=run_id,
agent_role=spec.role,
system_prompt=package.system_prompt,
user_message=package.user_message,
final_message=outcome.final_text,
transcript=transcript_text,
model_calls=_model_call_rows(outcome),
requested_model_id=effective_policy.requested_model_id,
connect=connect_factory,
)
except Exception as exc: # noqa: BLE001 - 模型成功但证据失败时必须失败关闭。
return _failed(
"EVIDENCE_PERSIST_FAILED",
f"框架证据落库失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
final_message=outcome.final_text,
)
try:
structured = validate_structured_output(outcome.final_text or "", spec)
except OutputInvalidError as exc:
return _failed(
"OUTPUT_SCHEMA_INVALID",
str(exc),
EXIT_OK,
EXIT_OUTPUT_INVALID,
session_id=outcome.session_id,
final_message=outcome.final_text,
evidence=evidence,
)
try:
_write_private_text(run_dir_path / "final-message.txt", outcome.final_text or "")
_write_private_json(run_dir_path / "output.json", structured)
except OSError as exc:
return _failed(
"AUDIT_WRITE_FAILED",
f"运行结果审计文件写入失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
evidence=evidence,
)
# 依赖清单:本次运行实际读取了什么(工具调用序列与参数),链路透视的读侧材料。
dependency_count = len(outcome.tool_calls)
if dependency_count:
dependencies = [
{
"seq": index + 1,
"tool": call.name,
"args": dict(call.args) if call.args else {},
"isError": call.is_error,
}
for index, call in enumerate(outcome.tool_calls)
]
try:
_write_private_json(run_dir_path / "dependencies.json", dependencies)
except OSError as exc:
return _failed(
"AUDIT_WRITE_FAILED",
f"依赖清单写入失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
evidence=evidence,
)
usage = _usage_totals(outcome)
total_cost, cost_complete = _cost_totals(outcome)
try:
writer.emit(
"run.completed",
status="ok",
requested_model_id=effective_policy.requested_model_id,
actual_model_id=outcome.model_calls[-1].actual_model_id,
usage={
"input": usage["inputTokens"] - usage["cachedTokens"],
"output": usage["outputTokens"],
"cacheRead": usage["cachedTokens"],
},
cost_usd=total_cost,
raw_ref=evidence.get("transcriptId"),
details={
"sessionId": outcome.session_id,
"turns": outcome.turns,
"toolCalls": len(outcome.tool_calls),
"unknownFrameworkEvents": list(outcome.unknown_event_types),
"costComplete": cost_complete,
"leaseId": evidence.get("leaseId"),
"llmCallIds": evidence.get("llmCallIds"),
},
)
finish_run(run_id, "completed", connect=connect_factory)
except Exception as exc: # noqa: BLE001 - 成功终态与终态事件必须一起可见。
return _failed(
"RUN_FINALIZE_FAILED",
f"成功终态写入失败: {type(exc).__name__}",
EXIT_EVIDENCE_FAILED,
session_id=outcome.session_id,
evidence=evidence,
)
receipt = _receipt(
status="completed",
sessionId=outcome.session_id,
durationMs=outcome.duration_ms,
turns=outcome.turns,
toolCallCount=len(outcome.tool_calls),
unknownFrameworkEvents=list(outcome.unknown_event_types),
dependencies=(
{"count": dependency_count, "file": "dependencies.json"}
if dependency_count
else None
),
modelCallCount=len(outcome.model_calls),
actualModelIds=[call.actual_model_id for call in outcome.model_calls],
usage=usage,
totalCostUsd=round(total_cost, 6) if total_cost is not None else None,
costComplete=cost_complete,
finalMessageSha256=_sha256_bare(outcome.final_text or ""),
structuredOutputSha256=_sha256_bare(
json.dumps(structured, ensure_ascii=False, sort_keys=True)
),
evidence=evidence,
frameworkArtifact=framework_artifact,
)
try:
_write_private_json(run_dir_path / "receipt.json", receipt)
except OSError:
receipt["receiptFileWritten"] = False
return receipt, EXIT_OK
def _sha256_bare(text: str) -> str:
import hashlib
return hashlib.sha256(text.encode("utf-8")).hexdigest()
EXIT_SPEC_INVALID,
run_dispatch,
)
def main(argv: list[str] | None = None) -> int:

View File

@ -2002,18 +2002,35 @@
"skill_behavior_eval": false,
"classification_confidence": "high",
"classification_basis": "验证迁移后的只读看板、候选决策和经验确认入口从独立仓根加载业务脚本,并覆盖写通道页面地标与键盘可达滚动区。"
},
{
"path": "tests/architecture/test_runtime_purity.py",
"scope": "domain",
"owner_skill_or_domain": "architecture",
"kind": "tool_contract",
"evidence_level": "static_structure",
"requires": [
"offline",
"filesystem"
],
"side_effects": [
"none"
],
"skill_behavior_eval": false,
"classification_confidence": "high",
"classification_basis": "扫描 runtime 目录,阻断对 Muse 业务模块的反向 import,并要求 dispatch_agent_task.py 保持薄 CLI。"
}
],
"summary": {
"entry_count": 122,
"entry_count": 123,
"by_scope": {
"other": 1,
"runtime_skill": 103,
"harness": 3,
"domain": 15
"domain": 16
},
"by_kind": {
"tool_contract": 42,
"tool_contract": 43,
"skill_behavior_eval": 1,
"harness_self_test": 3,
"domain_eval": 4,
@ -2026,8 +2043,8 @@
"by_evidence_level": {
"deterministic_offline": 101,
"real_dependency_integration": 11,
"static_structure": 10
"static_structure": 11
},
"total": 122
"total": 123
}
}

1
runtime/__init__.py Normal file
View File

@ -0,0 +1 @@
"""执行底座:协议、执行器与 run 目录。不认识 Muse 业务对象。"""

33
runtime/agent_executor.py Normal file
View File

@ -0,0 +1,33 @@
"""多轮 Agent 调用。不认识 Muse 业务对象。"""
from __future__ import annotations
from typing import Callable, Iterable
from framework.adapters.pi.runner import (
AgentStreamOutcome,
ExecutionPolicy,
PiAgentRunner,
TraceSink,
)
from framework.primitives.execution import FrameworkExecutionRequest
def execute_agent(
request: FrameworkExecutionRequest,
policy: ExecutionPolicy,
sink: TraceSink,
*,
timeout_seconds: float,
raw_sink: Callable[[bytes], None] | None = None,
launcher: Callable[..., Iterable[bytes]] | None = None,
) -> AgentStreamOutcome:
"""把通用执行请求交给当前生产宿主适配器。"""
return PiAgentRunner(launcher=launcher).run(
request,
policy,
sink,
timeout_seconds=timeout_seconds,
raw_sink=raw_sink,
)

81
runtime/runs.py Normal file
View File

@ -0,0 +1,81 @@
"""不可变 run 目录读写。不认识 Muse 业务对象。"""
from __future__ import annotations
import json
import os
import re
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from pathlib import Path
from typing import Any, BinaryIO
DEFAULT_RUN_DIR_ROOT = Path("/tmp/muse-agent-runs")
_RUN_ID_PATTERN = re.compile(r"^[A-Za-z0-9_.-]{1,64}$")
class RunDirectoryError(OSError):
"""run 目录创建或写入失败。"""
def validate_run_id(run_id: str) -> bool:
return isinstance(run_id, str) and _RUN_ID_PATTERN.fullmatch(run_id) is not None
def prepare_run_dir(run_id: str, run_dir: str | Path | None = None) -> Path:
"""创建仅当前用户可访问的 run 目录;默认根目录可复用,本次目录不可覆盖。"""
run_dir_path = Path(run_dir) if run_dir is not None else DEFAULT_RUN_DIR_ROOT / run_id
if run_dir is None:
DEFAULT_RUN_DIR_ROOT.mkdir(parents=True, mode=0o700, exist_ok=True)
DEFAULT_RUN_DIR_ROOT.chmod(0o700)
run_dir_path.mkdir(parents=True, mode=0o700, exist_ok=False)
run_dir_path.chmod(0o700)
return run_dir_path
def write_private_text(path: Path, text: str) -> None:
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as handle:
handle.write(text)
path.chmod(0o600)
def write_private_json(path: Path, value: Mapping[str, Any]) -> None:
write_private_text(path, json.dumps(value, ensure_ascii=False, indent=2))
def seed_run_inputs(
run_dir: Path,
*,
spec_text: str,
system_prompt: str,
user_message: str,
) -> None:
write_private_text(run_dir / "task-spec.json", spec_text)
write_private_text(run_dir / "system-prompt.txt", system_prompt)
write_private_text(run_dir / "user-message.txt", user_message)
@contextmanager
def open_transcript(run_dir: Path) -> Iterator[BinaryIO]:
path = run_dir / "transcript.jsonl"
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
handle = os.fdopen(fd, "wb")
try:
yield handle
path.chmod(0o600)
finally:
handle.close()
def read_transcript_text(run_dir: Path) -> str:
return (run_dir / "transcript.jsonl").read_text(encoding="utf-8")
def remove_local_raw(run_dir: Path) -> None:
for name in ("transcript.jsonl", "final-message.txt"):
try:
(run_dir / name).unlink(missing_ok=True)
except OSError:
pass

View File

@ -27,6 +27,7 @@ DASHBOARD_ROOTS = (pathlib.Path("muse") / "authority" / "studio" / "read",)
ACTIVE_RUNTIME_RELATIVE_ROOTS = (
pathlib.Path(".agent") / "skills",
pathlib.Path("framework"),
pathlib.Path("runtime"),
pathlib.Path("muse"),
pathlib.Path("muse") / "lifecycle" / "quality" / "harness",
pathlib.Path("muse") / "platform" / "llm",

View File

@ -0,0 +1,69 @@
"""执行底座纯度门:runtime 不得反向依赖 Muse 业务实现。"""
from __future__ import annotations
import pathlib
import re
import unittest
ROOT = next(
parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
if (parent / "AGENTS.md").is_file() and (parent / ".git").exists()
)
FORBIDDEN_IMPORTS = (
"agent_trace",
"muse_role",
"muse_role_contract",
"muse_db",
"psycopg",
"fixed-opus",
"role_task",
"role_policy",
"run_registry",
"persist_raw",
"read_tools",
)
DISPATCH_CLI = (
ROOT
/ "muse"
/ "lifecycle"
/ "dispatch"
/ "skills"
/ "dispatch-agent-task"
/ "scripts"
/ "dispatch_agent_task.py"
)
class RuntimePurityTest(unittest.TestCase):
def test_runtime_does_not_import_muse_business(self) -> None:
offenders: list[str] = []
import_pattern = re.compile(
r"(?:from|import)\s+(?:" + "|".join(map(re.escape, FORBIDDEN_IMPORTS)) + r")\b"
)
muse_pattern = re.compile(r"(?:from|import)\s+muse(?:\.|\s)")
runtime_root = ROOT / "runtime"
self.assertTrue(runtime_root.is_dir(), "runtime/ 必须存在")
for path in runtime_root.rglob("*"):
if not path.is_file() or "__pycache__" in path.parts:
continue
if path.suffix not in {".py", ".ts", ".md"}:
continue
text = path.read_text(encoding="utf-8")
if import_pattern.search(text) or muse_pattern.search(text):
offenders.append(path.relative_to(ROOT).as_posix())
self.assertEqual(offenders, [], "runtime 反向依赖 Muse 业务:\n" + "\n".join(offenders))
def test_dispatch_agent_task_is_thin_cli(self) -> None:
lines = DISPATCH_CLI.read_text(encoding="utf-8").splitlines()
self.assertLess(
len(lines),
100,
f"dispatch_agent_task.py 必须 < 100 行,实际 {len(lines)}",
)
if __name__ == "__main__":
unittest.main()