- 入库历史遗漏源码/测试:router_system 9 模块(agent/executors/inference/knowledge/ memory/planner/skills/trace)、tests 11 个测试文件、config/knowledge 领域知识 - 入库根目录方案文档(v2/v3/可行性×2)、references 文献(arxiv 14-18/cnki_open/ 参考文献清单)、research 论文素材(routerarena/paper/中文文献 PDF) - 前端构建产物刷新(新 hash);webapp 误写文档删除 - gitignore 增补:deepseek-harness、research/_refs、.mimosa/.zcode、网关日志/pid、 临时调试脚本、tests/e2e/node_modules、AI代理功能开发/prefix - 基线确认:318 passed
546 lines
24 KiB
Python
546 lines
24 KiB
Python
"""交流文本(Workspace)—— 端云协同编程智能体系统的核心协议(零依赖)。
|
||
|
||
大模型(Architect)与小模型(Worker)互不共享内部状态,只通过这份
|
||
schema 约束的结构化 JSON 共享工作区交接(类比前后端通过 API 契约协作)。
|
||
|
||
本模块实现(对齐《实现方案_v2》第 4 节):
|
||
- WORKSPACE_SCHEMA:draft-07 风格 schema 常量(文档/校验依据)
|
||
- validate():结构 + 字段长度校验(写入前必过,D2/D9)
|
||
- 锚点寻址:a://<file>#L<start>-<end>(引用工件片段,替代全文复制)
|
||
- 双渲染函数:render_for_architect(≤1200 token)、render_for_worker(≤8K token)
|
||
- rollup():已完成步骤折叠为 archive 摘要行;超限压缩(只减不删,4.5)
|
||
- 状态机:draft -> in_progress -> reviewing -> done / escalated / failed
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import copy
|
||
import json
|
||
import re
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
VERSION = "1.0"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 字段长度上限(同时是 rollup 依据,4.2)
|
||
# ---------------------------------------------------------------------------
|
||
LIMITS = {
|
||
"goal": 500, # brief.goal 字数
|
||
"task": 300, # brief.plan[].task 字数
|
||
"summary": 200, # progress[].summary 字数
|
||
"issue_text": 300, # issues[].observed/expected/tried/ask 字数
|
||
"reply": 600, # decisions[].reply 字数
|
||
"archive": 160, # archive[] 每条字数
|
||
"constraints": 8, # brief.constraints 上限条数
|
||
"plan_steps": 5, # brief.plan 上限步数
|
||
"acceptance": 20, # brief.acceptance 上限条数
|
||
"query_truncate": 200, # render_for_architect 中 query 截断
|
||
}
|
||
|
||
# 允许的领域标签(4.2 brief.tags;仅用于安全标记与验证接地,不做路由 D3)
|
||
ALLOWED_TAGS = {"code", "math", "legal", "medical", "finance",
|
||
"life", "education", "general", "safety", "science"}
|
||
|
||
STATUS_FLOW = {
|
||
"draft": {"in_progress"},
|
||
"in_progress": {"reviewing", "escalated", "failed", "in_progress"},
|
||
"reviewing": {"done", "in_progress", "failed"},
|
||
"escalated": {"reviewing", "done", "failed"},
|
||
"done": set(),
|
||
"failed": set(),
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# JSON Schema(draft-07 风格,draft-07 依赖内嵌;供校验与文档参考)
|
||
# ---------------------------------------------------------------------------
|
||
WORKSPACE_SCHEMA: Dict[str, Any] = {
|
||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||
"title": "Communication Workspace",
|
||
"type": "object",
|
||
"required": ["version", "request_id", "query", "meta"],
|
||
"additionalProperties": False,
|
||
"properties": {
|
||
"version": {"const": VERSION},
|
||
"request_id": {"type": "string", "minLength": 1},
|
||
"query": {"type": "string", "minLength": 1},
|
||
"meta": {
|
||
"type": "object",
|
||
"required": ["status", "round", "budget"],
|
||
"properties": {
|
||
"status": {"enum": ["draft", "in_progress", "reviewing", "escalated", "done", "failed"]},
|
||
"round": {"type": "integer", "minimum": 0},
|
||
"budget": {
|
||
"type": "object",
|
||
"required": ["api_input_tokens", "api_output_tokens", "api_token_cap", "rounds_cap"],
|
||
"properties": {
|
||
"api_input_tokens": {"type": "integer", "minimum": 0},
|
||
"api_output_tokens": {"type": "integer", "minimum": 0},
|
||
"api_token_cap": {"type": "integer", "minimum": 1},
|
||
"rounds_cap": {"type": "integer", "minimum": 1},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
"brief": {
|
||
"type": "object",
|
||
"required": ["goal", "constraints", "tags", "acceptance", "plan"],
|
||
"properties": {
|
||
"goal": {"type": "string"},
|
||
"constraints": {"type": "array", "items": {"type": "string"}},
|
||
"tags": {"type": "array", "items": {"type": "string"}},
|
||
"acceptance": {"type": "array", "items": {"type": "object"}},
|
||
"plan": {"type": "array", "items": {"type": "object"}},
|
||
},
|
||
},
|
||
"progress": {"type": "array", "items": {"type": "object"}},
|
||
"issues": {"type": "array", "items": {"type": "object"}},
|
||
"decisions": {"type": "array", "items": {"type": "object"}},
|
||
"archive": {"type": "array", "items": {"type": "string"}},
|
||
},
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 工具
|
||
# ---------------------------------------------------------------------------
|
||
_TOKEN_PER_CHAR_ZH = 1 / 1.6 # 中文约 1.6 字/token
|
||
_TOKEN_PER_CHAR_EN = 1 / 4.0 # 英文约 4 字/token
|
||
|
||
|
||
def estimate_tokens(text: str) -> int:
|
||
"""粗略 token 估算(中英混合,用于渲染预算校验)。"""
|
||
if not text:
|
||
return 0
|
||
zh = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff")
|
||
en = len(text) - zh
|
||
return max(1, int(zh * _TOKEN_PER_CHAR_ZH + en * _TOKEN_PER_CHAR_EN))
|
||
|
||
|
||
def build_anchor(filename: str, start: int = 1, end: Optional[int] = None) -> str:
|
||
"""构造锚点:a://<file>#L<start>-<end>;end 缺省仅 L<start>。"""
|
||
if end is None:
|
||
return f"a://{filename}#L{start}"
|
||
return f"a://{filename}#L{start}-{end}"
|
||
|
||
|
||
_ANCHOR_RE = re.compile(r"^a://(?P<file>[^#]+?)(?:#L(?P<start>\d+)(?:-(?P<end>\d+))?)?$")
|
||
|
||
|
||
def parse_anchor(anchor: str) -> Optional[Dict[str, Any]]:
|
||
"""解析锚点为 {file, start, end};非法返回 None。"""
|
||
m = _ANCHOR_RE.match(anchor)
|
||
if not m:
|
||
return None
|
||
start = int(m.group("start")) if m.group("start") else 1
|
||
end = int(m.group("end")) if m.group("end") else start
|
||
return {"file": m.group("file"), "start": start, "end": end}
|
||
|
||
|
||
def _clip(text: str, limit: int) -> str:
|
||
"""按字数截断(中文按字符)。"""
|
||
if len(text) <= limit:
|
||
return text
|
||
return text[:limit] + "…"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 校验
|
||
# ---------------------------------------------------------------------------
|
||
def validate(ws: Dict[str, Any]) -> List[str]:
|
||
"""校验 workspace 结构 + 字段长度。返回错误列表(空 = 合法)。"""
|
||
errors: List[str] = []
|
||
if not isinstance(ws, dict):
|
||
return ["workspace 必须是 object"]
|
||
if ws.get("version") != VERSION:
|
||
errors.append(f"version 必须是 {VERSION}")
|
||
if not isinstance(ws.get("request_id"), str) or not ws["request_id"]:
|
||
errors.append("request_id 必须是非空字符串")
|
||
if not isinstance(ws.get("query"), str) or not ws["query"]:
|
||
errors.append("query 必须是非空字符串")
|
||
|
||
meta = ws.get("meta")
|
||
if not isinstance(meta, dict):
|
||
errors.append("meta 必须是 object")
|
||
else:
|
||
if meta.get("status") not in STATUS_FLOW:
|
||
errors.append(f"meta.status 非法: {meta.get('status')}")
|
||
budget = meta.get("budget")
|
||
if not isinstance(budget, dict):
|
||
errors.append("meta.budget 必须是 object")
|
||
else:
|
||
for k in ("api_input_tokens", "api_output_tokens", "api_token_cap", "rounds_cap"):
|
||
if not isinstance(budget.get(k), int) or budget.get(k) < 0:
|
||
errors.append(f"meta.budget.{k} 必须是非负整数")
|
||
|
||
brief = ws.get("brief")
|
||
if brief is not None:
|
||
if not isinstance(brief, dict):
|
||
errors.append("brief 必须是 object")
|
||
else:
|
||
if not isinstance(brief.get("goal"), str):
|
||
errors.append("brief.goal 必须是字符串")
|
||
elif len(brief["goal"]) > LIMITS["goal"]:
|
||
errors.append(f"brief.goal 超长(>{LIMITS['goal']}字)")
|
||
if not isinstance(brief.get("constraints"), list):
|
||
errors.append("brief.constraints 必须是数组")
|
||
elif len(brief["constraints"]) > LIMITS["constraints"]:
|
||
errors.append(f"brief.constraints 超过 {LIMITS['constraints']} 条")
|
||
if not isinstance(brief.get("tags"), list):
|
||
errors.append("brief.tags 必须是数组")
|
||
for t in brief.get("tags", []) or []:
|
||
if t not in ALLOWED_TAGS:
|
||
errors.append(f"brief.tags 含非法标签: {t}")
|
||
acc = brief.get("acceptance")
|
||
if not isinstance(acc, list) or len(acc) > LIMITS["acceptance"]:
|
||
errors.append(f"brief.acceptance 需为 ≤{LIMITS['acceptance']} 的数组")
|
||
plan = brief.get("plan")
|
||
if not isinstance(plan, list) or len(plan) > LIMITS["plan_steps"]:
|
||
errors.append(f"brief.plan 需为 ≤{LIMITS['plan_steps']} 步的数组")
|
||
else:
|
||
ids = [p.get("id") for p in plan if isinstance(p, dict)]
|
||
if len(set(ids)) != len(ids):
|
||
errors.append("brief.plan 存在重复 step id")
|
||
for p in plan:
|
||
if not isinstance(p, dict):
|
||
errors.append("brief.plan 元素必须是 object")
|
||
continue
|
||
if not isinstance(p.get("task"), str):
|
||
errors.append(f"brief.plan[{p.get('id')}].task 必须是字符串")
|
||
elif len(p["task"]) > LIMITS["task"]:
|
||
errors.append(f"brief.plan[{p.get('id')}].task 超长(>{LIMITS['task']}字)")
|
||
|
||
for i, entry in enumerate(ws.get("progress", []) or []):
|
||
if not isinstance(entry, dict):
|
||
errors.append(f"progress[{i}] 必须是 object"); continue
|
||
if entry.get("status") not in ("done", "failed", "blocked"):
|
||
errors.append(f"progress[{i}].status 非法")
|
||
if not isinstance(entry.get("summary"), str) or len(entry["summary"]) > LIMITS["summary"]:
|
||
errors.append(f"progress[{i}].summary 非法或超长")
|
||
|
||
for i, entry in enumerate(ws.get("issues", []) or []):
|
||
if not isinstance(entry, dict):
|
||
errors.append(f"issues[{i}] 必须是 object"); continue
|
||
for k in ("observed", "expected", "tried", "ask"):
|
||
if isinstance(entry.get(k), str) and len(entry[k]) > LIMITS["issue_text"]:
|
||
errors.append(f"issues[{i}].{k} 超长(>{LIMITS['issue_text']}字)")
|
||
|
||
for i, entry in enumerate(ws.get("decisions", []) or []):
|
||
if not isinstance(entry, dict):
|
||
errors.append(f"decisions[{i}] 必须是 object"); continue
|
||
if isinstance(entry.get("reply"), str) and len(entry["reply"]) > LIMITS["reply"]:
|
||
errors.append(f"decisions[{i}].reply 超长(>{LIMITS['reply']}字)")
|
||
|
||
for i, line in enumerate(ws.get("archive", []) or []):
|
||
if not isinstance(line, str) or len(line) > LIMITS["archive"]:
|
||
errors.append(f"archive[{i}] 非法或超长")
|
||
return errors
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Workspace
|
||
# ---------------------------------------------------------------------------
|
||
class Workspace:
|
||
"""交流文本对象:持有状态、执行写入前校验、渲染、rollup、持久化。"""
|
||
|
||
def __init__(self, data: Dict[str, Any]):
|
||
errors = validate(data)
|
||
if errors:
|
||
raise ValueError("workspace 校验失败: " + "; ".join(errors[:5]))
|
||
self._data = data
|
||
self._brief_locked = False
|
||
|
||
# ---------- 构造 ----------
|
||
@classmethod
|
||
def new(cls, request_id: str, query: str,
|
||
api_token_cap: int = 8000, rounds_cap: int = 6) -> "Workspace":
|
||
data = {
|
||
"version": VERSION,
|
||
"request_id": request_id,
|
||
"query": query,
|
||
"meta": {
|
||
"status": "draft",
|
||
"round": 0,
|
||
"budget": {
|
||
"api_input_tokens": 0,
|
||
"api_output_tokens": 0,
|
||
"api_token_cap": api_token_cap,
|
||
"rounds_cap": rounds_cap,
|
||
},
|
||
},
|
||
"brief": None,
|
||
"progress": [],
|
||
"issues": [],
|
||
"decisions": [],
|
||
"archive": [],
|
||
}
|
||
return cls(data)
|
||
|
||
# ---------- 访问 ----------
|
||
@property
|
||
def request_id(self) -> str:
|
||
return self._data["request_id"]
|
||
|
||
@property
|
||
def status(self) -> str:
|
||
return self._data["meta"]["status"]
|
||
|
||
@property
|
||
def data(self) -> Dict[str, Any]:
|
||
return copy.deepcopy(self._data)
|
||
|
||
def get(self, key: str, default: Any = None) -> Any:
|
||
return self._data.get(key, default)
|
||
|
||
def __getitem__(self, key: str) -> Any:
|
||
return self._data[key]
|
||
|
||
def meta(self) -> Dict[str, Any]:
|
||
return self._data["meta"]
|
||
|
||
def budget(self) -> Dict[str, int]:
|
||
return self._data["meta"]["budget"]
|
||
|
||
# ---------- 写入(均先校验) ----------
|
||
def _commit(self, data: Dict[str, Any]) -> None:
|
||
errors = validate(data)
|
||
if errors:
|
||
raise ValueError("写入校验失败: " + "; ".join(errors[:5]))
|
||
self._data = data
|
||
|
||
def transition(self, new_status: str) -> None:
|
||
cur = self.status
|
||
if new_status == cur:
|
||
return
|
||
if new_status not in STATUS_FLOW.get(cur, set()):
|
||
raise ValueError(f"非法状态迁移: {cur} -> {new_status}")
|
||
self._data["meta"]["status"] = new_status
|
||
|
||
def apply_brief(self, brief: Dict[str, Any]) -> None:
|
||
"""写入 brief(写一次后锁定,D2:brief 恒定位于文档前部,prefix cache 友好)。"""
|
||
if self._data.get("brief") is not None or self._brief_locked:
|
||
raise ValueError("brief 已写入,不可重复")
|
||
new = copy.deepcopy(self._data)
|
||
new["brief"] = brief
|
||
new["meta"]["status"] = "in_progress"
|
||
self._commit(new)
|
||
self._brief_locked = True
|
||
|
||
def add_progress(self, step: str, status: str, summary: str,
|
||
artifact: Optional[str] = None) -> None:
|
||
new = copy.deepcopy(self._data)
|
||
entry: Dict[str, Any] = {"step": step, "status": status, "summary": summary}
|
||
if artifact:
|
||
entry["artifact"] = artifact
|
||
new["progress"].append(entry)
|
||
self._commit(new)
|
||
|
||
def add_issue(self, step: str, anchor: str, observed: str, expected: str,
|
||
tried: str, ask: str) -> str:
|
||
new = copy.deepcopy(self._data)
|
||
iid = f"i{len(new['issues']) + 1}"
|
||
entry = {
|
||
"id": iid, "step": step, "anchor": anchor,
|
||
"observed": observed, "expected": expected,
|
||
"tried": tried, "ask": ask,
|
||
}
|
||
new["issues"].append(entry)
|
||
self._commit(new)
|
||
return iid
|
||
|
||
def add_decision(self, ref: str, reply: str,
|
||
patch_plan: Optional[List[Dict[str, str]]] = None) -> None:
|
||
new = copy.deepcopy(self._data)
|
||
new["decisions"].append({
|
||
"ref": ref, "reply": reply,
|
||
"patch_plan": patch_plan or [],
|
||
})
|
||
self._commit(new)
|
||
|
||
def revise_plan(self, updates: Dict[str, str]) -> None:
|
||
"""按 decision.patch_plan 修订既有 step 的 task(不改结构/顺序)。"""
|
||
if self._data.get("brief") is None:
|
||
raise ValueError("brief 尚未写入,无法修订 plan")
|
||
new = copy.deepcopy(self._data)
|
||
for pid, task in updates.items():
|
||
for p in new["brief"]["plan"]:
|
||
if p["id"] == pid:
|
||
p["task"] = task
|
||
break
|
||
self._commit(new)
|
||
|
||
def mark_round(self) -> None:
|
||
self._data["meta"]["round"] += 1
|
||
|
||
def add_budget(self, input_tokens: int = 0, output_tokens: int = 0) -> None:
|
||
b = self._data["meta"]["budget"]
|
||
b["api_input_tokens"] += int(input_tokens)
|
||
b["api_output_tokens"] += int(output_tokens)
|
||
self._commit(self._data)
|
||
|
||
def exhausted(self) -> bool:
|
||
"""预算熔断判定:API token 或回合任一触顶(D6)。"""
|
||
b = self._data["meta"]["budget"]
|
||
used = b["api_input_tokens"] + b["api_output_tokens"]
|
||
if b["api_token_cap"] and used >= b["api_token_cap"]:
|
||
return True
|
||
if b["rounds_cap"] and self._data["meta"]["round"] >= b["rounds_cap"]:
|
||
return True
|
||
return False
|
||
|
||
# ---------- rollup(4.5) ----------
|
||
def rollup(self) -> int:
|
||
"""把 done 的 progress 折叠为 archive 摘要行,并清理解析完成的问题。
|
||
|
||
只减不删 archive 历史;progress 中 done 的条目折叠后移除(保留 failed/blocked)。
|
||
返回本次折叠的条目数。
|
||
"""
|
||
folded = 0
|
||
new_progress: List[Dict[str, Any]] = []
|
||
for entry in self._data.get("progress", []):
|
||
if entry.get("status") == "done" and entry.get("step"):
|
||
line = _clip(f"{entry['step']}: {entry.get('summary', '')}", LIMITS["archive"])
|
||
if line not in self._data["archive"]:
|
||
self._data["archive"].append(line)
|
||
folded += 1
|
||
else:
|
||
new_progress.append(entry)
|
||
resolved = {d.get("ref") for d in self._data.get("decisions", [])}
|
||
kept_issues: List[Dict[str, Any]] = []
|
||
for iss in self._data.get("issues", []):
|
||
if iss.get("id") in resolved:
|
||
line = _clip(f"{iss['id']}: {iss.get('expected', '')[:60]}", LIMITS["archive"])
|
||
if line not in self._data["archive"]:
|
||
self._data["archive"].append(line)
|
||
else:
|
||
kept_issues.append(iss)
|
||
self._data["issues"] = kept_issues
|
||
self._data["progress"] = new_progress
|
||
return folded
|
||
|
||
# ---------- 渲染 ----------
|
||
def render_for_architect(self) -> str:
|
||
"""渲染 Architect 输入(D7):meta+query(截断)+全部 issues+最近3条 decisions
|
||
+最近回合 progress 摘要。目标 ≤1200 token。"""
|
||
d = self._data
|
||
parts: List[str] = []
|
||
m = d["meta"]
|
||
parts.append("== meta ==")
|
||
parts.append(f"status={m['status']} round={m['round']} "
|
||
f"budget={json.dumps(m['budget'], ensure_ascii=False)}")
|
||
parts.append("== query ==")
|
||
parts.append(_clip(d["query"], LIMITS["query_truncate"]))
|
||
if d.get("brief"):
|
||
b = d["brief"]
|
||
parts.append("== brief(锁定) ==")
|
||
parts.append(f"goal: {_clip(b['goal'], 120)}")
|
||
parts.append(f"plan: {[p['id'] for p in b.get('plan', [])]}")
|
||
parts.append(f"acceptance: {[a.get('id') for a in b.get('acceptance', [])]}")
|
||
parts.append("== issues ==")
|
||
for iss in d.get("issues", []):
|
||
parts.append(f"{iss['id']} step={iss.get('step')} anchor={iss.get('anchor')} "
|
||
f"ask={_clip(iss.get('ask', ''), 80)}")
|
||
parts.append("== 最近 3 条 decisions ==")
|
||
for dec in d.get("decisions", [])[-3:]:
|
||
parts.append(f"ref={dec.get('ref')} reply={_clip(dec.get('reply', ''), 80)}")
|
||
parts.append("== progress 摘要 ==")
|
||
for p in d.get("progress", [])[-5:]:
|
||
parts.append(f"{p.get('step')} [{p.get('status')}] {_clip(p.get('summary', ''), 40)}")
|
||
parts.append("== archive ==")
|
||
for line in d.get("archive", [])[-8:]:
|
||
parts.append(line)
|
||
# token 预算:超限先截断最旧 archive(已只保留 3 条 decisions)
|
||
out = "\n".join(parts)
|
||
while estimate_tokens(out) > 1200 and len(d.get("archive", [])) > 4:
|
||
d = copy.deepcopy(d)
|
||
d["archive"] = d["archive"][4:]
|
||
out = "\n".join(_rerender(self, d))
|
||
return out
|
||
|
||
def render_for_worker(self, step_id: str,
|
||
artifact_text: Optional[str] = None) -> str:
|
||
"""渲染 Worker 输入(4.4):brief 全文+该 step 定义+依赖 step 的 archive 摘要行
|
||
+该 step 现有工件全文+验收标准。目标 ≤8K token。"""
|
||
d = self._data
|
||
b = d.get("brief")
|
||
parts: List[str] = []
|
||
if b:
|
||
parts.append("== 任务目标 (goal) ==")
|
||
parts.append(b["goal"])
|
||
parts.append("== 约束 (constraints) ==")
|
||
parts.extend(f"- {c}" for c in b.get("constraints", []))
|
||
parts.append("== 全部步骤 (plan) ==")
|
||
for p in b.get("plan", []):
|
||
mark = " <-- 当前步" if p.get("id") == step_id else ""
|
||
parts.append(f"{p['id']}: {p.get('task', '')}{mark}")
|
||
parts.append(f" done_criteria: {p.get('done_criteria', '')}")
|
||
parts.append("== 依赖步摘要 (archive) ==")
|
||
for line in d.get("archive", [])[-6:]:
|
||
parts.append(line)
|
||
if artifact_text:
|
||
parts.append(f"== 当前步已有工件({step_id}) ==")
|
||
parts.append(artifact_text)
|
||
parts.append("== 验收标准 ==")
|
||
if b:
|
||
for a in b.get("acceptance", []):
|
||
parts.append(f"- {a.get('id')}: {a.get('check', '')} "
|
||
f"(machine_checkable={a.get('machine_checkable', False)})")
|
||
parts.append("== 要求 ==")
|
||
parts.append("请实现当前步,并用可执行验证/事实对照/结构检查自验证;"
|
||
"通过则写 progress(done),失败自修 ≤2 次,仍失败则写 issue。")
|
||
return "\n".join(parts)
|
||
|
||
# ---------- 持久化 ----------
|
||
def save(self, path: Path) -> None:
|
||
path = Path(path)
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
json.dump(self._data, f, ensure_ascii=False, indent=2)
|
||
|
||
@classmethod
|
||
def load(cls, path: Path) -> "Workspace":
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
return cls(data)
|
||
|
||
def prefix_signature(self) -> str:
|
||
"""返回稳定前缀的签名(T10 prefix cache)。
|
||
|
||
交流文本的"恒定位于前部"的部分(version + request_id + query + meta + brief)
|
||
应不随 progress/issues/decisions 追加而变化,从而使 llama-server 的
|
||
--cache-reuse 能命中该前缀、降低 prefill 开销。用紧凑 JSON 的哈希度量稳定性。
|
||
"""
|
||
stable = {
|
||
"version": self._data.get("version"),
|
||
"request_id": self._data.get("request_id"),
|
||
"query": self._data.get("query"),
|
||
"brief": self._data.get("brief"),
|
||
}
|
||
import hashlib
|
||
s = json.dumps(stable, ensure_ascii=False, sort_keys=True)
|
||
return hashlib.sha256(s.encode("utf-8")).hexdigest()[:16]
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
return self.data
|
||
|
||
|
||
def _rerender(ws: "Workspace", d: Dict[str, Any]) -> List[str]:
|
||
"""用裁剪后的数据重建 Architect 渲染(供超限压缩内部用)。"""
|
||
parts: List[str] = []
|
||
m = d["meta"]
|
||
parts.append("== meta ==")
|
||
parts.append(f"status={m['status']} round={m['round']}")
|
||
parts.append("== query ==")
|
||
parts.append(_clip(d["query"], LIMITS["query_truncate"]))
|
||
parts.append("== issues ==")
|
||
for iss in d.get("issues", []):
|
||
parts.append(f"{iss['id']} step={iss.get('step')} ask={_clip(iss.get('ask', ''), 80)}")
|
||
parts.append("== decisions(最近3) ==")
|
||
for dec in d.get("decisions", [])[-3:]:
|
||
parts.append(f"ref={dec.get('ref')} reply={_clip(dec.get('reply', ''), 80)}")
|
||
parts.append("== progress ==")
|
||
for p in d.get("progress", [])[-5:]:
|
||
parts.append(f"{p.get('step')} [{p.get('status')}] {_clip(p.get('summary', ''), 40)}")
|
||
parts.append("== archive ==")
|
||
for line in d.get("archive", [])[-6:]:
|
||
parts.append(line)
|
||
return parts
|