Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ca2a86b77 | ||
|
|
b2ef004313 | ||
|
|
6fe40b0342 | ||
|
|
0cf03446c4 | ||
|
|
cbd3d3e3d0 | ||
|
|
5cae3a6e30 | ||
|
|
b240077300 | ||
|
|
18f617d4e8 | ||
|
|
1ada9955d5 | ||
|
|
a160b03c7b | ||
|
|
98cfc6408d | ||
|
|
dde4f359a8 | ||
|
|
35560aa5a3 | ||
|
|
1a5ef6b75c | ||
|
|
fcfe8149bd | ||
|
|
00f182611c | ||
|
|
b4743949e5 | ||
|
|
c722c16626 | ||
|
|
c55ebbcb96 | ||
|
|
5773e74086 | ||
|
|
d6f62af0ed | ||
|
|
beafd7537a | ||
|
|
0f94e4b062 | ||
|
|
aced665ba6 | ||
|
|
9150e6f90b | ||
|
|
6e385743e4 | ||
|
|
dbe1f544d7 | ||
|
|
124309b985 | ||
|
|
06f2723161 | ||
|
|
8e4f4e0382 | ||
|
|
ba289e7b17 | ||
|
|
89f3906c39 | ||
|
|
bbebd153f4 | ||
|
|
a563d31b1e | ||
|
|
39632c2e7a | ||
|
|
37b96fd94f | ||
|
|
7a98a7e7fe | ||
|
|
360a8a8946 | ||
|
|
452daf3ffa | ||
|
|
626f8e1062 | ||
|
|
a253a40b8e | ||
|
|
b8913dca6a | ||
|
|
1b7e2c0f47 | ||
|
|
8a3ff92518 | ||
|
|
f4798ac852 | ||
|
|
cf2615def6 | ||
|
|
b12f9e29e6 | ||
|
|
4eae71864c | ||
|
|
0b5c7747a3 | ||
|
|
bd632236c2 | ||
|
|
a7db6a4d0f | ||
|
|
5dc190ee63 | ||
|
|
b7e5baf15c | ||
|
|
cf7df54874 | ||
|
|
88e481afcd | ||
|
|
7c66075ee9 | ||
|
|
3f7067fb2d | ||
|
|
f08a4a90c6 | ||
|
|
4d1123e45c | ||
|
|
f93f473a0e | ||
|
|
a8e81e078d | ||
|
|
9b8cf79c96 | ||
|
|
493a846f07 |
@@ -0,0 +1,150 @@
|
||||
name: governance
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
governance:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
steps:
|
||||
- name: Checkout
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# 只物化两个治理 gitlink(Guidelines 与 deps/development-manager),非递归;
|
||||
# 其它 gitlink 不物化、不取回。凭据只以 URL 限定的 extraheader 存在于本进程树内。
|
||||
python3 - <<'PY_CHECKOUT'
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
# 服务端基址取运行器注入的 GITHUB_SERVER_URL(平台控制、候选 PR 不可影响)。
|
||||
SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/")
|
||||
parsed_server = urlsplit(SERVER)
|
||||
if parsed_server.scheme not in {"http", "https"} or not parsed_server.netloc:
|
||||
sys.exit("Checkout server must be the platform-injected http(s) URL")
|
||||
SSH = "ssh://git@gitea.naitangbaobao.cn:52222/weichen/"
|
||||
# 审查过的白名单,独立于候选 .gitmodules:仅列出两个治理 gitlink。
|
||||
REPOSITORIES = {
|
||||
"deps/development-guidelines": "Development-Guidelines",
|
||||
"deps/development-manager": "Development-Manager",
|
||||
}
|
||||
TARGET = "weichen/Agent-System-Core"
|
||||
root_token = os.environ.pop("TOKEN", "")
|
||||
component_token = os.environ.pop("SUBMODULE_TOKEN", "")
|
||||
if not root_token or not component_token:
|
||||
sys.exit("Checkout requires TOKEN and SUBMODULE_TOKEN")
|
||||
if os.environ.get("GITHUB_REPOSITORY") != TARGET:
|
||||
sys.exit("Checkout repository is outside the reviewed allowlist")
|
||||
sha = os.environ.get("GITHUB_SHA", "")
|
||||
if not re.fullmatch(r"[0-9a-fA-F]{40}", sha):
|
||||
sys.exit("Checkout requires a fixed commit SHA")
|
||||
|
||||
# 丢弃继承的 Git 追踪/配置/SSH 钩子;凭据只存在于单个 Git 进程树的精确 URL 头中。
|
||||
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
|
||||
clean_env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null",
|
||||
GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0")
|
||||
config = {
|
||||
"core.hooksPath": "/dev/null", "core.fsmonitor": "false",
|
||||
"init.templateDir": "", "credential.helper": "",
|
||||
"fetch.recurseSubmodules": "false", "submodule.recurse": "false",
|
||||
"http.followRedirects": "false", "protocol.allow": "never",
|
||||
"protocol." + urlsplit(SERVER).scheme + ".allow": "always",
|
||||
}
|
||||
|
||||
|
||||
def git(repo, *args, extra=None):
|
||||
values = config | (extra or {})
|
||||
env = clean_env | {"GIT_CONFIG_COUNT": str(len(values))}
|
||||
for index, (key, value) in enumerate(values.items()):
|
||||
env[f"GIT_CONFIG_KEY_{index}"] = key
|
||||
env[f"GIT_CONFIG_VALUE_{index}"] = value
|
||||
return subprocess.run(["git", "-C", str(repo), *args], env=env,
|
||||
check=True, stdout=subprocess.PIPE).stdout
|
||||
|
||||
|
||||
def materialize_governance(root):
|
||||
# -r 列出任意深度的 gitlink,但不进入 submodule 内部;其它 gitlink 只记录、从不取回。
|
||||
links = {}
|
||||
for entry in git(root, "ls-tree", "-rz", "HEAD").split(b"\0"):
|
||||
if not entry:
|
||||
continue
|
||||
metadata, path = entry.split(b"\t", 1)
|
||||
mode, kind, commit = metadata.decode().split()
|
||||
if mode == "160000":
|
||||
links[path.decode()] = commit
|
||||
for name in REPOSITORIES:
|
||||
if name not in links:
|
||||
raise ValueError("Governance gitlink is missing from the fixed commit")
|
||||
raw = git(root, "config", "--no-includes", "--blob", "HEAD:.gitmodules", "--null", "--list")
|
||||
entries = {}
|
||||
for item in raw.split(b"\0"):
|
||||
if not item:
|
||||
continue
|
||||
key, value = item.decode().split("\n", 1)
|
||||
match = re.fullmatch(r"submodule\.(.+)\.(path|url)", key)
|
||||
if not match:
|
||||
continue
|
||||
name, field = match.groups()
|
||||
if name not in REPOSITORIES:
|
||||
continue
|
||||
if field in entries.setdefault(name, {}):
|
||||
raise ValueError("Duplicate candidate .gitmodules setting")
|
||||
entries[name][field] = value
|
||||
# 先整体校验两个治理条目,再进行任何带凭据的取回。
|
||||
for name, repository in REPOSITORIES.items():
|
||||
fields = entries.get(name, {})
|
||||
if fields.get("path") != name:
|
||||
raise ValueError("Governance submodule path is outside the reviewed allowlist")
|
||||
trusted_url = f"{SERVER}/weichen/{repository}.git"
|
||||
if fields.get("url") not in {SSH + repository + ".git", trusted_url}:
|
||||
raise ValueError("Governance submodule URL is outside the reviewed allowlist")
|
||||
for name, repository in REPOSITORIES.items():
|
||||
url = f"{SERVER}/weichen/{repository}.git"
|
||||
# 仅持久化审查过的公开 URL,使 Git 在进程级配置消失后仍识别该 submodule。
|
||||
git(root, "config", f"submodule.{name}.url", url)
|
||||
git(root, "submodule", "update", "--init", "--checkout", "--", name,
|
||||
extra={f"submodule.{name}.url": url,
|
||||
f"submodule.{name}.update": "checkout",
|
||||
f"http.{url}.extraheader": "AUTHORIZATION: token " + component_token})
|
||||
child = root / name
|
||||
if git(child, "rev-parse", "HEAD").decode().strip() != links[name]:
|
||||
raise ValueError("Materialized governance gitlink does not match the fixed SHA")
|
||||
|
||||
|
||||
try:
|
||||
root = Path.cwd()
|
||||
# 必须是干净的临时工作区;不复用既有 Git 状态。
|
||||
if (root / ".git").exists():
|
||||
raise ValueError("Checkout requires a fresh workspace")
|
||||
git(root, "init", "-q")
|
||||
url = f"{SERVER}/{TARGET}.git"
|
||||
git(root, "remote", "add", "origin", url)
|
||||
git(root, "fetch", "--depth=1", "origin", sha,
|
||||
extra={f"http.{url}.extraheader": "AUTHORIZATION: token " + root_token})
|
||||
git(root, "checkout", "-q", "--detach", "FETCH_HEAD")
|
||||
materialize_governance(root)
|
||||
except (ValueError, OSError, subprocess.CalledProcessError) as error:
|
||||
sys.exit(f"Checkout refused: {error}")
|
||||
print("Checkout complete; credentials were not persisted in Git configuration")
|
||||
PY_CHECKOUT
|
||||
unset TOKEN SUBMODULE_TOKEN
|
||||
- name: Install Go 1.27.1
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL -o "$RUNNER_TEMP/go.tgz" https://mirrors.aliyun.com/golang/go1.27.1.linux-amd64.tar.gz
|
||||
tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz"
|
||||
ln -sfn /usr/local/go/bin/go /usr/local/bin/go
|
||||
ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt
|
||||
- name: Validate agent execution configuration
|
||||
run: scripts/agent-exec validate --repo .
|
||||
- name: Governance check
|
||||
run: scripts/dm governance check --repo .
|
||||
@@ -0,0 +1,6 @@
|
||||
[submodule "deps/development-guidelines"]
|
||||
path = deps/development-guidelines
|
||||
url = ssh://git@gitea.naitangbaobao.cn:52222/weichen/Development-Guidelines.git
|
||||
[submodule "deps/development-manager"]
|
||||
path = deps/development-manager
|
||||
url = ssh://git@gitea.naitangbaobao.cn:52222/weichen/Development-Manager.git
|
||||
@@ -0,0 +1,53 @@
|
||||
# Agent-System-Core 采纳 development-governance-v2 治理套件 任务卡
|
||||
|
||||
任务主记录:[Agent-System-Core PR #35](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Core/pulls/35)(本文件随 PR 描述维护)。
|
||||
状态:已完成(闭合随本 PR 合入;合入以平台记录为准);owner:weichen。
|
||||
|
||||
## 基线与授权
|
||||
|
||||
- 权威仓:weichen/Agent-System-Core;root_target:main;immediate_target:main
|
||||
- branch_kind:initiative;initiative_id:governance-adoption-Agent-System-Core
|
||||
- R0(initial_main_sha):35560aa5a3fdc9d8f92bb4b2e9eece5254b94c41;工作区:/home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption(worktree);分支:feature/governance-adoption
|
||||
- 父任务信息(仅子任务填写):无
|
||||
- accepting_requirements:false(单仓采纳 initiative,范围固定为下列十步,不接收新需求)
|
||||
- 需求分流:verdict=NEW;selected_initiative=无;候选=无;理由=development-governance-v2 Phase 6 每个正式仓库独立 initiative;triage 收据=无(本仓采纳前尚无 triage 入口)
|
||||
- 授权与交付终点:owner 已授权 development-governance-v2 全部实施(Agent-System-Dev 主记录 docs/superpowers/plans/2026-10-16-development-governance-v2-master.md);终点为本 PR 合入 main 且采纳状态 active。分支保护与 tag 由用户执行。
|
||||
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md;外部 Agent 一单元一新进程/会话/工作区)
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
目标(Phase 6 十步):固定 Guidelines SHA(deps/development-guidelines @ 2688fd2be6a52c0bf110e7e6366b6f291ab5e4eb);增加 AGENT-EXECUTION.md;增加共享执行器薄入口(deps/development-manager @ 42e675fec8e0069646319fcd298e61d03d8cbfd5 + scripts/dm、scripts/agent-exec、scripts/task);四条路由(implementation/repair/test/review);任务卡模板与分支元数据;CI 中启用 schema 校验(.gitea/workflows/governance.yml:agent-exec validate + governance check);一次正例运行;负例(缺配置、坏收据、越界路径、未知副作用);独立 Review + CI;采纳状态 active。
|
||||
|
||||
非目标:不改业务代码与既有测试;不改分支保护(由用户执行);不发布 tag。
|
||||
|
||||
引导说明:AGENT-EXECUTION.md 由 host 以固定版本治理套件(dm-server governance init @ 42e675fec8e0069646319fcd298e61d03d8cbfd5)生成并作为引导配置提交(执行器运行前必须存在有效配置;沿用 DM 采纳先例);其余文件由实施单元生成。
|
||||
|
||||
## Agent 实施收据
|
||||
|
||||
| 单元 ID | 预期范围 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| governance-adoption-Agent-System-Core-r1 | 13 个允许路径(治理文件 + 两个 gitlink) | dde4f359 | implementation | cursor→antigravity→pi | run 9a7ec135(dm-server 42e675f,attempts=1) | cursor / not-observable | success | 98cfc6408d49(apply 快进) | 否 | /tmp/gov-receipt-Agent-System-Core-r1.json |
|
||||
| governance-adoption-Agent-System-Core-repin-r1 | 9 个允许路径(治理文件 + 两个 gitlink) | a160b03c | implementation | cursor→antigravity→pi | run e64b5afe(dm-server pinned,attempts=1) | cursor / not-observable | success | 1ada9955d57a(apply 快进) | 否 | /tmp/gov-receipt-Agent-System-Core-repin-r1.json |
|
||||
|
||||
## 同步记录
|
||||
|
||||
| 节点 | sync_base_sha | tested_head_sha | checked_at | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| 启动 | 35560aa5a3fdc9d8f92bb4b2e9eece5254b94c41 | 35560aa5a3fdc9d8f92bb4b2e9eece5254b94c41 | 2026-10-17T05:51:28Z | 一致 |
|
||||
|
||||
## 结果与验证(完成后填写)
|
||||
|
||||
- 结果:治理套件已采纳——Guidelines 与 Development-Manager gitlink 固定、AGENT-EXECUTION.md 四条路由、共享执行器薄入口、pre-commit admit、governance CI;采纳状态 active(随合入生效)。
|
||||
- 验证:见下表(命令输出由 /tmp/gov-neg/ev.sh 写入 .task/evidence/governance-adoption/,SELF 除非另注)。
|
||||
- 影响与剩余项:不改业务代码;本地启用钩子需由人执行 `git config core.hooksPath scripts/hooks`;将 governance 检查设为分支保护必需项属用户双钥匙事项(命令见 Agent-System-Dev 治理主记录)。
|
||||
|
||||
| 时间(UTC) | 命令 | 被测 HEAD | exit | 来源 | 摘要/位置 |
|
||||
|---|---|---|---|---|---|
|
||||
| 20261017T061900Z | `scripts/dm governance check --repo .` | 98cfc6408d49 | 0 | SELF | ok fixtures;[输出](../evidence/governance-adoption/00-20261017T061900Z.log) sha256:ae4439e39cad7e8d |
|
||||
| 20261017T061900Z | `scripts/agent-exec validate --repo .` | 98cfc6408d49 | 0 | SELF | validate: ok;[输出](../evidence/governance-adoption/01-20261017T061900Z.log) sha256:89ea92d2c3d1f9e4 |
|
||||
| 20261017T061900Z | `python3 /tmp/gov-neg/adopt_cases.py .` | 98cfc6408d49 | 0 | SELF | [verdict] ALL PASS cases=5;[输出](../evidence/governance-adoption/02-20261017T061900Z.log) sha256:64f13535108476f9 |
|
||||
| 20261017T071536Z | `scripts/dm governance check --repo .` | 1ada9955d57a | 0 | SELF | ok fixtures;[输出](../evidence/governance-adoption/03-20261017T071536Z.log) sha256:f46b6a39dd69f693 |
|
||||
| 20261017T071536Z | `scripts/agent-exec validate --repo .` | 1ada9955d57a | 0 | SELF | validate: ok;[输出](../evidence/governance-adoption/04-20261017T071536Z.log) sha256:4a0c76e79c70e3bb |
|
||||
| 20261017T071536Z | `python3 /tmp/gov-neg/adopt_cases.py .` | 1ada9955d57a | 0 | SELF | [verdict] ALL PASS cases=5;[输出](../evidence/governance-adoption/05-20261017T071536Z.log) sha256:3eb026a392f8336b |
|
||||
| — | 候选 HEAD CI | 18f617d4e8cd | 0 | GATE | success governance / governance (pull_request)=success(/weichen/Agent-System-Core/actions/runs/3535/jobs/5611) |
|
||||
| — | 独立 Review | 18f617d4e8cd | 0 | REPLAY | cursor-agent-stable --mode ask 全新只读会话(fresh_session=true,session id not-observable;codex/pi 因 Token Plan 429 不可用)VERDICT: APPROVE,无阻塞项 |
|
||||
@@ -0,0 +1,19 @@
|
||||
command: scripts/dm governance check --repo .
|
||||
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
|
||||
head: 98cfc6408d49dd88d957bb2dd13ca37bebe9facf
|
||||
exit: 0
|
||||
utc: 20261017T061900Z
|
||||
---
|
||||
ok gitlink deps/development-guidelines
|
||||
ok gitlink deps/development-manager
|
||||
ok agent-execution
|
||||
ok file AGENTS.md
|
||||
ok file PROJECT.md
|
||||
ok file policy/guidelines-adoption.md
|
||||
ok file .task/templates/task-card.md
|
||||
ok executable scripts/agent-exec
|
||||
ok executable scripts/hooks/pre-commit
|
||||
ok executable scripts/dm
|
||||
ok executable scripts/task
|
||||
ok adoption status
|
||||
ok fixtures
|
||||
@@ -0,0 +1,7 @@
|
||||
command: scripts/agent-exec validate --repo .
|
||||
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
|
||||
head: 98cfc6408d49dd88d957bb2dd13ca37bebe9facf
|
||||
exit: 0
|
||||
utc: 20261017T061900Z
|
||||
---
|
||||
validate: ok
|
||||
@@ -0,0 +1,13 @@
|
||||
command: python3 /tmp/gov-neg/adopt_cases.py .
|
||||
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
|
||||
head: 98cfc6408d49dd88d957bb2dd13ca37bebe9facf
|
||||
exit: 0
|
||||
utc: 20261017T061900Z
|
||||
---
|
||||
[object] Agent-System-Core-wt-feature-governance-adoption HEAD=98cfc6408d49dd88d957bb2dd13ca37bebe9facf
|
||||
PASS positive-run-apply: run exit=0 terminal=success apply exit=0 head=b38a8a0da1df
|
||||
PASS missing-config: run exit=2 terminal=invalid-config validate exit=2
|
||||
PASS bad-receipt: apply exit=4 head_unchanged=True msg=apply: no local integrity record: changed paths do not match the transport diff
|
||||
PASS out-of-scope-path: run exit=5 terminal=scope-expansion transport=False
|
||||
PASS unknown-side-effect: run exit=4 terminal=failed-unknown transport=False
|
||||
[verdict] ALL PASS cases=5
|
||||
@@ -0,0 +1,19 @@
|
||||
command: scripts/dm governance check --repo .
|
||||
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
|
||||
head: 1ada9955d57af1dcfb750db2f01a1bcc40fd3ac4
|
||||
exit: 0
|
||||
utc: 20261017T071536Z
|
||||
---
|
||||
ok gitlink deps/development-guidelines
|
||||
ok gitlink deps/development-manager
|
||||
ok agent-execution
|
||||
ok file AGENTS.md
|
||||
ok file PROJECT.md
|
||||
ok file policy/guidelines-adoption.md
|
||||
ok file .task/templates/task-card.md
|
||||
ok executable scripts/agent-exec
|
||||
ok executable scripts/hooks/pre-commit
|
||||
ok executable scripts/dm
|
||||
ok executable scripts/task
|
||||
ok adoption status
|
||||
ok fixtures
|
||||
@@ -0,0 +1,7 @@
|
||||
command: scripts/agent-exec validate --repo .
|
||||
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
|
||||
head: 1ada9955d57af1dcfb750db2f01a1bcc40fd3ac4
|
||||
exit: 0
|
||||
utc: 20261017T071536Z
|
||||
---
|
||||
validate: ok
|
||||
@@ -0,0 +1,13 @@
|
||||
command: python3 /tmp/gov-neg/adopt_cases.py .
|
||||
cwd: /home/weichen/projects/Agent-System-Core-wt-feature-governance-adoption
|
||||
head: 1ada9955d57af1dcfb750db2f01a1bcc40fd3ac4
|
||||
exit: 0
|
||||
utc: 20261017T071536Z
|
||||
---
|
||||
[object] Agent-System-Core-wt-feature-governance-adoption HEAD=1ada9955d57af1dcfb750db2f01a1bcc40fd3ac4
|
||||
PASS positive-run-apply: run exit=0 terminal=success apply exit=0 head=f6303ea75059
|
||||
PASS missing-config: run exit=2 terminal=invalid-config validate exit=2
|
||||
PASS bad-receipt: apply exit=4 head_unchanged=True msg=apply: no local integrity record: changed paths do not match the transport diff
|
||||
PASS out-of-scope-path: run exit=5 terminal=scope-expansion transport=False
|
||||
PASS unknown-side-effect: run exit=4 terminal=failed-unknown transport=False
|
||||
[verdict] ALL PASS cases=5
|
||||
@@ -0,0 +1,66 @@
|
||||
# <任务标题> 任务卡
|
||||
|
||||
任务主记录:<Issue/PR 链接,或注明"本文件随 PR 描述维护">。
|
||||
状态:<进行中 / 待审核 / 已合入 / 已放弃>;owner:<填写>。
|
||||
|
||||
## 基线与授权
|
||||
|
||||
- 权威仓:<仓 URL 或逻辑 ID>;root_target:main;immediate_target:<main 或 feature/initiative>
|
||||
- branch_kind:<initiative | child | standalone>;initiative_id:<稳定 ID,普通任务为空>
|
||||
- R0(initial_main_sha):<完整 SHA>;工作区:<路径>(worktree / clone);分支:<feature|fix|hotfix/...>
|
||||
- 父任务信息(仅子任务填写):parent_branch=<父分支>;parent_head_sha=<SHA>;parent_pr=<父 PR>
|
||||
- accepting_requirements:<true | false>(仅 initiative 可为 true;冻结后为 false)
|
||||
- 需求分流:verdict=<JOIN | NEW>;selected_initiative=<分支或无>;候选=<分支@HEAD 列表或无>;理由=<不适配/适配依据>;triage 收据=<源码外路径与摘要>
|
||||
- 授权与交付终点:<本次允许的动作与终点;未授权事项写明>
|
||||
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md;外部 Agent 一单元一新进程/会话/工作区)
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
<预期结果、受影响入口/模块/契约;明确不做什么。>
|
||||
|
||||
## Agent 实施收据
|
||||
|
||||
按根目录 AGENT-EXECUTION.md 路由;外部 Agent 一单元一新进程/会话/隔离工作区。实施、修复与测试必须通过登记的候选执行;未配置或候选安全耗尽停在 unavailable/blocked,禁止 host 隐式代写。
|
||||
|
||||
| 单元 ID | 预期范围 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 待定义 | — | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — |
|
||||
|
||||
## 复用材料(task-start 自动检索结果摘录)
|
||||
|
||||
- 相关近期提交:<git log -- <paths> 命中;无则写无>。
|
||||
- 相关经验条目:<检索命中;无则写无>。
|
||||
|
||||
## 同步记录
|
||||
|
||||
| 节点 | sync_base_sha | tested_head_sha | checked_at | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| 启动 | <R0> | <HEAD> | <日期时间> | <一致/已同步> |
|
||||
| 验收前 | <填写> | <填写> | <填写> | <填写> |
|
||||
|
||||
字段没有发生的写"无",不省略。本卡与 PR 描述同源:开 PR 时将"目标与非目标、结果与验证、影响与剩余项"复制进 PR 描述,不另写第二份。
|
||||
|
||||
## 交付调度(有多个就绪任务时)
|
||||
|
||||
- 调度记录 / entry_id / 当前 attempt_id:<平台或调度服务链接及标识;未入队写无>。
|
||||
- 入队源 HEAD / owner / 依赖:<完整 SHA、身份、依赖或无>。
|
||||
- 固定候选引用:<调度尝试中的 base/head、组件与配置;未固定写无>。
|
||||
- 窗口、最终 CI、Review 与真实合入结果:<引用调度记录及原始证据,不复制全队列>。
|
||||
|
||||
定义见 [合入调度与 CI 分配](../standards/merge-queue.md)。本段在候选固定前整理;固定后先使用源码之外的记录保存运行结果,再按项目闭合流程回填。
|
||||
|
||||
## 结果与验证(完成后填写)
|
||||
|
||||
- 结果:<交付了什么;未发生写无>。
|
||||
- 验证:<结论性摘要:PASS/FAIL/SKIP/NOT_RUN 逐项;逐条证据见下表,来源标注与写入规则见 [按影响选择验证与证据](../standards/verification.md)>。
|
||||
- 影响与剩余项:<影响面、遗留风险、后续任务>。
|
||||
|
||||
| 时间(UTC) | 命令 | 被测 HEAD | exit | 来源 | 摘要/位置 |
|
||||
|---|---|---|---|---|---|
|
||||
| <时间> | <命令;由项目 evidence 工具写入,无工具时附命令+对象 SHA+输出 digest 并标 SELF> | <HEAD> | <exit> | GATE/REPLAY/SELF | <摘要/位置> |
|
||||
|
||||
## 回收(task-finish)
|
||||
|
||||
- 合入核对:<分支已包含于目标分支的核对输出>。
|
||||
- 未跟踪/ignored 文件处置:<归档/可再生/存疑 逐项;无则写无>。
|
||||
- 回收动作:<worktree remove、分支删除、prune;放弃路径注明原因>。
|
||||
@@ -0,0 +1,67 @@
|
||||
# Project Agent Execution Configuration
|
||||
# 项目工程 Agent 执行路由配置
|
||||
|
||||
适用范围:定义本项目中由 AI / Agent 主导的代码实施、修复、测试与审阅所调用的 Agent CLI 路由。
|
||||
解析器识别正文中唯一一个 ```agent-execution 代码块。正文外的说明文字可根据项目需要调整。
|
||||
|
||||
通用硬规则见 Development-Guidelines 的 [Agent 执行与协作契约](deps/development-guidelines/standards/agent-execution.md)。
|
||||
本配置为项目本地路由权威,声明具体的候选 profile、尝试次数上限、超时、集成入口与豁免路径;
|
||||
代码块须通过 `deps/development-guidelines/templates/schemas/agent-execution.schema.json` 校验后才能登记为 active。
|
||||
`fallback_to_host` 只能为 `never`;旧值 `all-unavailable` 仅供迁移工具读取,不能通过准入。
|
||||
下例的 CLI 名称、命令与路径均为占位,按项目实际执行器替换。
|
||||
|
||||
```agent-execution
|
||||
schema: agent-execution/v1
|
||||
|
||||
execution:
|
||||
fresh_process_per_unit: true
|
||||
permission: run-everything
|
||||
fallback_to_host: never
|
||||
max_parallel: 3
|
||||
timeout_seconds: 7200
|
||||
idle_timeout_seconds: 600
|
||||
summary_bytes: 16384
|
||||
raw_output_bytes: 10485760
|
||||
max_attempts_per_profile: 3
|
||||
emergency_profile: codex
|
||||
|
||||
roles:
|
||||
implementation: [cursor, antigravity, pi]
|
||||
repair: [cursor, antigravity, pi]
|
||||
test: [pi, antigravity, cursor]
|
||||
review: [pi, antigravity, cursor, codex]
|
||||
|
||||
profiles:
|
||||
cursor:
|
||||
adapter: cursor
|
||||
executable: "cursor-agent-stable"
|
||||
model: auto
|
||||
enabled: true
|
||||
|
||||
antigravity:
|
||||
adapter: antigravity
|
||||
executable: agy
|
||||
model: auto
|
||||
enabled: true
|
||||
|
||||
pi:
|
||||
adapter: pi
|
||||
executable: pi
|
||||
model: auto
|
||||
enabled: true
|
||||
|
||||
codex:
|
||||
adapter: codex
|
||||
executable: codex
|
||||
model: auto
|
||||
enabled: true
|
||||
|
||||
integration:
|
||||
exempt_paths: [".task/**", "docs/superpowers/specs/**", "docs/superpowers/plans/**"]
|
||||
transport:
|
||||
command: "scripts/agent-exec apply"
|
||||
admission:
|
||||
command: "scripts/agent-exec admit"
|
||||
cleanup:
|
||||
command: "scripts/agent-exec cleanup"
|
||||
```
|
||||
@@ -23,9 +23,9 @@
|
||||
历史文档与旧链接里的 `git-panel.u1680832.nyat.app:30319` 已失效(TLS 握手失败),
|
||||
见到一律按过期处理,不要再试。当前权威通道:
|
||||
|
||||
- **Web / API(HTTPS)**:`https://git-home.u1680832.nyat.app:30319`(Gitea 1.27.3,
|
||||
- **Web / API(HTTPS)**:`https://gitea.naitangbaobao.cn`(Gitea 1.27.3,
|
||||
API 前缀 `/api/v1`;偶发 broken pipe / 超时,重试即可)。
|
||||
- **Git over SSH**:`ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/<Repo>.git`
|
||||
- **Git over SSH**:`ssh://git@gitea.naitangbaobao.cn:52222/weichen/<Repo>.git`
|
||||
(各仓 origin 与 gitlink 均为此格式)。
|
||||
- **API token**:`~/.secrets/gitea`,两行键值 `URL_PREFIX=…` 与
|
||||
`ACCESS_TOKEN=<40 位 hex>`;取值 `grep ACCESS_TOKEN ~/.secrets/gitea | cut -d= -f2-`,
|
||||
@@ -33,4 +33,22 @@
|
||||
(baseline 硬边界 2:只记录来源、字段与可用状态)。
|
||||
- **SSH key**:`~/.ssh/home_gitea`(ED25519,公钥指纹
|
||||
`SHA256:GYXaOXIMeloQ1fKSfdJ/ndgPQzs/Ix7Hk62ekfkfQ20`,Gitea 侧密钥名 GMKtec);
|
||||
`~/.ssh/config` 已有 `git-ssh.u1680832.nyat.app` 主机条目,克隆/推送直接可用。
|
||||
`~/.ssh/config` 已有 `gitea.naitangbaobao.cn` 主机条目(端口 52222),克隆/推送直接可用。
|
||||
|
||||
## Agent 执行与新项目准入(强制)
|
||||
|
||||
1. 缺 Guidelines 固定版本(`deps/development-guidelines` gitlink 未就绪或 HEAD 不符)时,Agent 只能只读分析,不得实施。
|
||||
2. 缺有效 `AGENT-EXECUTION.md`(`scripts/dm governance check` 未通过)时禁止 AI 实施。
|
||||
3. 缺共享执行器(`scripts/agent-exec` 或 `deps/development-manager` 不可用)时,状态为 `blocked/unavailable`,不得改由 host 替代。
|
||||
4. 不允许 host 代写实施单元;实施、修复与测试必须经 `AGENT-EXECUTION.md` 登记的候选执行。
|
||||
5. 人类直接开发按其自身授权与项目规则进行,不受本条款约束。
|
||||
6. 分支保护与规范 tag 由用户负责设置与维护,Agent 不得代为设置。
|
||||
|
||||
AI 实施单元只能通过 `scripts/agent-exec run` 运行(配置与路由见 AGENT-EXECUTION.md),并由 `scripts/agent-exec apply` 集成。
|
||||
|
||||
入口:
|
||||
|
||||
- 需求分流:`scripts/task triage discover|decide|verify`
|
||||
- 任务启动:`scripts/task start …`
|
||||
- PR 检查:`scripts/task pr-check …`
|
||||
- Agent 执行:`scripts/agent-exec run|apply|admit|validate`
|
||||
|
||||
+12
-2
@@ -2,5 +2,15 @@
|
||||
|
||||
- 用途:见 README。定位与边界见设计文档(Agent-System-Documents)。
|
||||
- 技术栈:Go(静态链接,CGO_ENABLED=0)
|
||||
- 构建/验证:见仓内 Makefile 或 Dev 仓顶层 Makefile(待 P1 起补充并保持本节同步)。
|
||||
- 状态:P0 骨架;当前阶段见 Dev 仓任务记录。
|
||||
|
||||
## 验证
|
||||
|
||||
在本仓根目录执行 `go test ./...` 验证 Go 契约与不变量;纯文档修改执行
|
||||
`git diff --check` 并核对受影响链接、命令和来源。本仓没有 Makefile。
|
||||
|
||||
治理门禁(与 `.gitea/workflows/governance.yml` 相同):在本仓根目录依次执行
|
||||
`scripts/agent-exec validate --repo .` 与 `scripts/dm governance check --repo .`,两者均须通过。
|
||||
|
||||
修改 `ts/types.ts` 后在 Dev 仓运行 `make contracts` 同步消费者,再以 `make test`
|
||||
验证固定组件组合;单独运行 Core 测试不能代替提供方与消费者的兼容验证。
|
||||
本仓提供公共契约,不是独立部署服务;当前能力以源码和对应验证记录为准。
|
||||
|
||||
+47
-13
@@ -12,19 +12,53 @@ import "time"
|
||||
type ActivityCard struct {
|
||||
ID ID `json:"id"`
|
||||
// ProjectID/ConversationID 定位归属;ConversationID 为空表示项目级卡片(远期跨会话收件箱)。
|
||||
ProjectID ID `json:"projectId"`
|
||||
ConversationID ID `json:"conversationId,omitempty"`
|
||||
SourceRunID ID `json:"sourceRunId,omitempty"` // 溯源:产生卡片的 run
|
||||
AnchorMessageID ID `json:"anchorMessageId,omitempty"` // 互定位:出生消息(↙ 定位到对话)
|
||||
Kind CardKind `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Status CardStatus `json:"status"`
|
||||
Tone string `json:"tone,omitempty"` // ok | warn | danger | info
|
||||
Body []UINode `json:"body,omitempty"`
|
||||
Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息
|
||||
Pinned bool `json:"pinned,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ProjectID ID `json:"projectId"`
|
||||
ConversationID ID `json:"conversationId,omitempty"`
|
||||
SourceRunID ID `json:"sourceRunId,omitempty"` // 溯源:产生卡片的 run
|
||||
AnchorMessageID ID `json:"anchorMessageId,omitempty"` // 互定位:出生消息(↙ 定位到对话)
|
||||
Kind CardKind `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Status CardStatus `json:"status"`
|
||||
Tone string `json:"tone,omitempty"` // ok | warn | danger | info
|
||||
Task *TaskSnapshot `json:"task,omitempty"`
|
||||
Body []UINode `json:"body,omitempty"`
|
||||
Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息
|
||||
Pinned bool `json:"pinned,omitempty"`
|
||||
Work *WorkCard `json:"work,omitempty"` // optional background-work snapshot; never a foreground approval
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// WorkCard keeps the complete saved result and an optional, non-blocking next step
|
||||
// on the original task. Reading and answering do not change the result version.
|
||||
type WorkCard struct {
|
||||
Result string `json:"result,omitempty"`
|
||||
ResultVersion int `json:"resultVersion"`
|
||||
ReadVersion int `json:"readVersion"`
|
||||
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
||||
Decision *WorkDecision `json:"decision,omitempty"`
|
||||
}
|
||||
|
||||
type WorkDecision struct {
|
||||
RunID ID `json:"runId,omitempty"` // reserved child identity; reconciles acceptance after restart without rerunning
|
||||
Mode string `json:"mode"` // single | multiple
|
||||
State string `json:"state"` // open | deferred | dispatching | submitted
|
||||
Options []WorkOption `json:"options,omitempty"`
|
||||
SelectedIDs []string `json:"selectedIds,omitempty"`
|
||||
FreeText string `json:"freeText,omitempty"`
|
||||
RespondedAt *time.Time `json:"respondedAt,omitempty"`
|
||||
}
|
||||
|
||||
type WorkOption struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// WorkCardAction is version-fenced. Presentation-only opens never mark a result read.
|
||||
type WorkCardAction struct {
|
||||
ResultVersion int `json:"resultVersion"`
|
||||
SelectedIDs []string `json:"selectedIds,omitempty"`
|
||||
FreeText string `json:"freeText,omitempty"`
|
||||
}
|
||||
|
||||
type CardKind string
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWorkCardSurvivesPersistedSnapshot(t *testing.T) {
|
||||
completed := time.Date(2026, 10, 15, 6, 0, 0, 0, time.UTC)
|
||||
card := ActivityCard{ID: "card_work", Status: CardDone, Work: &WorkCard{
|
||||
Result: strings.Repeat("完整结果\n", 200) + "最后一项依据",
|
||||
ResultVersion: 2, ReadVersion: 1, CompletedAt: &completed,
|
||||
Decision: &WorkDecision{Mode: "multiple", State: "submitted", Options: []WorkOption{{ID: "summary", Label: "生成摘要"}},
|
||||
SelectedIDs: []string{"summary"}, FreeText: "请突出关键限制", RespondedAt: &completed},
|
||||
}}
|
||||
data, err := json.Marshal(card)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var restored ActivityCard
|
||||
if err := json.Unmarshal(data, &restored); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if restored.Work == nil || restored.Work.Result != card.Work.Result || restored.Work.ReadVersion != 1 || restored.Work.ResultVersion != 2 {
|
||||
t.Fatalf("complete result or unread version lost: %+v", restored.Work)
|
||||
}
|
||||
if d := restored.Work.Decision; d.State != "submitted" || d.SelectedIDs[0] != "summary" || d.FreeText != "请突出关键限制" || !d.RespondedAt.Equal(completed) {
|
||||
t.Fatalf("decision history lost: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyCardOmitsOptionalWork(t *testing.T) {
|
||||
var legacy ActivityCard
|
||||
if err := json.Unmarshal([]byte(`{"id":"old","status":"resolved","body":[{"type":"markdown","markdown":"保留历史"}]}`), &legacy); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := json.Marshal(legacy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if legacy.Work != nil || strings.Contains(string(data), `"work"`) || legacy.Body[0].Markdown != "保留历史" {
|
||||
t.Fatalf("legacy payload changed: %s", data)
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,14 @@ func sampleRun() Run {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenPromptModule(t *testing.T) {
|
||||
golden(t, "prompt_module_subagent", PromptModule{
|
||||
ID: "agent.sub.explore", Layer: LayerAgent, Version: 1,
|
||||
Title: "探索子代理", Body: "只读检索。",
|
||||
Tools: []string{"knowledge_search", "web_search"}, Model: "lightweight", MaxTurns: 8,
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoldenDomain(t *testing.T) {
|
||||
golden(t, "message", sampleMessage())
|
||||
golden(t, "run", sampleRun())
|
||||
@@ -199,6 +207,26 @@ func TestGoldenIngest(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoldenWorkflowDef(t *testing.T) {
|
||||
golden(t, "workflow_def", WorkflowDef{
|
||||
ID: "demo.brief", Title: "简报", Description: "按主题写一份简报",
|
||||
IntentID: "demo.brief", MaxRetries: 6,
|
||||
Steps: []WorkflowStep{
|
||||
{ID: "length", Title: "篇幅", Kind: "hitl", Hitl: &HITLCardTemplate{
|
||||
Question: "篇幅?", Kind: HITLKindChoice, AllowFreeInput: false,
|
||||
Choices: []HITLChoice{{ID: "short", Label: "短", Description: "一页", Impact: "更快"}},
|
||||
}},
|
||||
{ID: "draft", Title: "起草", Kind: "prompt", Agent: "research",
|
||||
Prompt: "写一份关于 {{slots.topic}} 的简报。用户说:{{input}}。上一步:{{steps.length.output}}",
|
||||
Critic: &StepCritic{Criteria: "必须点明主题", MaxRetries: 1}},
|
||||
{ID: "lookup", Title: "查时间", Kind: "tool", Tool: "time_now", Args: json.RawMessage(`{"tz":"UTC"}`)},
|
||||
{ID: "save", Title: "存稿", Kind: "artifact", Artifact: "brief.md", Prompt: "{{steps.draft.output}}"},
|
||||
{ID: "note", Title: "通知", Kind: "card", Card: &ActivityCardTemplate{Kind: CardKindResult, Title: "简报已写好"}},
|
||||
{ID: "run", Title: "脚本", Kind: "script", Script: "scripts/demo.py"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoldenPlugin(t *testing.T) {
|
||||
golden(t, "plugin_manifest", PluginManifest{
|
||||
ID: "example", Name: "示例插件", Version: "0.1.0", APIVersion: PluginAPIVersion,
|
||||
|
||||
@@ -13,18 +13,19 @@ type ParsedDocument struct {
|
||||
}
|
||||
|
||||
type DocMeta struct {
|
||||
Filename string `json:"filename"`
|
||||
MIME string `json:"mime,omitempty"`
|
||||
Parser string `json:"parser"` // builtin:md / builtin:docx / mineru / agent
|
||||
Confidence float64 `json:"confidence,omitempty"` // 0–1;交叉验证差异块拉低
|
||||
CrossCheck *CrossCheck `json:"crossCheck,omitempty"`
|
||||
ParsedAt time.Time `json:"parsedAt"`
|
||||
Origin *DocumentOrigin `json:"origin,omitempty"`
|
||||
Filename string `json:"filename"`
|
||||
MIME string `json:"mime,omitempty"`
|
||||
Parser string `json:"parser"` // builtin:md / builtin:docx / mineru / agent
|
||||
Confidence float64 `json:"confidence,omitempty"` // 0–1;交叉验证差异块拉低
|
||||
CrossCheck *CrossCheck `json:"crossCheck,omitempty"`
|
||||
ParsedAt time.Time `json:"parsedAt"`
|
||||
}
|
||||
|
||||
type CrossCheck struct {
|
||||
With string `json:"with"` // 交叉验证的另一解析器
|
||||
DiffBlocks int `json:"diffBlocks"`
|
||||
Verdict string `json:"verdict"` // agree | differ
|
||||
With string `json:"with"` // 交叉验证的另一解析器
|
||||
DiffBlocks int `json:"diffBlocks"`
|
||||
Verdict string `json:"verdict"` // agree | differ
|
||||
}
|
||||
|
||||
type DocBlockType string
|
||||
@@ -39,13 +40,13 @@ const (
|
||||
)
|
||||
|
||||
type DocBlock struct {
|
||||
Type DocBlockType `json:"type"`
|
||||
Level int `json:"level,omitempty"` // heading
|
||||
Text string `json:"text,omitempty"` // para/heading/list 项拼接/code
|
||||
Items []string `json:"items,omitempty"` // list
|
||||
Rows [][]string `json:"rows,omitempty"` // table(首行为表头)
|
||||
Ref string `json:"ref,omitempty"` // image-ref 的对象键
|
||||
Confidence float64 `json:"confidence,omitempty"`
|
||||
Type DocBlockType `json:"type"`
|
||||
Level int `json:"level,omitempty"` // heading
|
||||
Text string `json:"text,omitempty"` // para/heading/list 项拼接/code
|
||||
Items []string `json:"items,omitempty"` // list
|
||||
Rows [][]string `json:"rows,omitempty"` // table(首行为表头)
|
||||
Ref string `json:"ref,omitempty"` // image-ref 的对象键
|
||||
Confidence float64 `json:"confidence,omitempty"`
|
||||
}
|
||||
|
||||
// PlainText 把文档压成纯文本(附件注入与全文索引用);表格按行拼接。
|
||||
@@ -88,60 +89,60 @@ func (d ParsedDocument) PlainText() string {
|
||||
|
||||
// ParserDesc 注册表条目(来源标记:builtin/plugin/...;一切可禁用)。
|
||||
type ParserDesc struct {
|
||||
ID string `json:"id"`
|
||||
Formats []string `json:"formats"` // 支持的扩展名
|
||||
ID string `json:"id"`
|
||||
Formats []string `json:"formats"` // 支持的扩展名
|
||||
Source SourceKind `json:"source"`
|
||||
Note string `json:"note,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
// ---- 检索(4.12)----
|
||||
|
||||
// KnowledgeDoc 是检索语料的一条文档(来自 ingest 或插件知识库,带来源标记)。
|
||||
type KnowledgeDoc struct {
|
||||
ID ID `json:"id"`
|
||||
Owner ID `json:"owner,omitempty"` // 空 = 共享(插件级)知识
|
||||
ProjectID ID `json:"projectId,omitempty"` // 空 = 插件级知识库
|
||||
Source string `json:"source"` // upload | artifact | plugin:<id>
|
||||
Title string `json:"title"`
|
||||
Ref string `json:"ref,omitempty"` // 工件/文件引用
|
||||
Chunk string `json:"chunk"`
|
||||
ID ID `json:"id"`
|
||||
Owner ID `json:"owner,omitempty"` // 空 = 共享(插件级)知识
|
||||
ProjectID ID `json:"projectId,omitempty"` // 空 = 插件级知识库
|
||||
Source string `json:"source"` // upload | artifact | plugin:<id>
|
||||
Title string `json:"title"`
|
||||
Ref string `json:"ref,omitempty"` // 工件/文件引用
|
||||
Chunk string `json:"chunk"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type RetrievalHit struct {
|
||||
Doc KnowledgeDoc `json:"doc"`
|
||||
Score float64 `json:"score"`
|
||||
Kind string `json:"kind"` // fulltext | vector | plugin
|
||||
Doc KnowledgeDoc `json:"doc"`
|
||||
Score float64 `json:"score"`
|
||||
Kind string `json:"kind"` // fulltext | vector | plugin
|
||||
}
|
||||
|
||||
type RetrievalQuery struct {
|
||||
ProjectID ID `json:"projectId,omitempty"`
|
||||
Text string `json:"text"`
|
||||
TopK int `json:"topK,omitempty"`
|
||||
ProjectID ID `json:"projectId,omitempty"`
|
||||
Text string `json:"text"`
|
||||
TopK int `json:"topK,omitempty"`
|
||||
Sources []string `json:"sources,omitempty"` // 过滤来源
|
||||
}
|
||||
|
||||
// ---- 记忆(4.13 契约;实现远期)----
|
||||
|
||||
type MemoryEntry struct {
|
||||
ID string `json:"id"`
|
||||
Scope string `json:"scope"` // user | project
|
||||
ProjectID ID `json:"projectId,omitempty"`
|
||||
Type string `json:"type"` // fact | preference | decision | episode | procedure
|
||||
Content string `json:"content"`
|
||||
Source string `json:"source"` // run:<id> | manual
|
||||
Confidence float64 `json:"confidence,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastAccessed time.Time `json:"lastAccessed"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Scope string `json:"scope"` // user | project
|
||||
ProjectID ID `json:"projectId,omitempty"`
|
||||
Type string `json:"type"` // fact | preference | decision | episode | procedure
|
||||
Content string `json:"content"`
|
||||
Source string `json:"source"` // run:<id> | manual
|
||||
Confidence float64 `json:"confidence,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastAccessed time.Time `json:"lastAccessed"`
|
||||
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||
}
|
||||
|
||||
// ---- 上传文件 ----
|
||||
|
||||
type UploadInfo struct {
|
||||
ArtifactID ID `json:"artifactId"`
|
||||
Filename string `json:"filename"`
|
||||
MIME string `json:"mime,omitempty"`
|
||||
Size int64 `json:"size"`
|
||||
ArtifactID ID `json:"artifactId"`
|
||||
Filename string `json:"filename"`
|
||||
MIME string `json:"mime,omitempty"`
|
||||
Size int64 `json:"size"`
|
||||
Parsed *ParsedDocument `json:"parsed,omitempty"`
|
||||
}
|
||||
|
||||
+1
Submodule deps/development-guidelines added at 2688fd2be6
+1
Submodule deps/development-manager added at 24e8a5c8ba
@@ -8,13 +8,13 @@ import (
|
||||
// 事件信封:后端 → 前端的唯一增量通道(SSE),持久化后转发,Seq 在会话内单调。
|
||||
// 消费方对未知 Type 静默忽略(原则 6 前向兼容)。
|
||||
// 项目流(D50):项目频道事件以伪会话键 "project:<projectId>" 复用同一机制
|
||||
//(独立 seq、持久化、断线重放),信封以 ProjectID 标识归属(会话流事件为空)。
|
||||
// (独立 seq、持久化、断线重放),信封以 ProjectID 标识归属(会话流事件为空)。
|
||||
type StreamEvent struct {
|
||||
Seq int64 `json:"seq"`
|
||||
RunID ID `json:"runId,omitempty"`
|
||||
ConversationID ID `json:"conversationId"`
|
||||
ProjectID ID `json:"projectID,omitempty"` // 项目流事件的项目归属(D50;只增不删)
|
||||
TS string `json:"ts"` // RFC3339
|
||||
TS string `json:"ts"` // RFC3339
|
||||
V int `json:"v"`
|
||||
Type StreamEventType `json:"type"`
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
@@ -38,9 +38,12 @@ func ProjectFromChannel(key ID) (ID, bool) {
|
||||
const (
|
||||
EvMessageDelta StreamEventType = "message.delta"
|
||||
EvThinkingDelta StreamEventType = "thinking.delta"
|
||||
EvToolCallDelta StreamEventType = "tool.call.delta" // 参数增量:工具开始执行前即可上屏
|
||||
EvToolCall StreamEventType = "tool.call"
|
||||
EvToolOutput StreamEventType = "tool.output" // 执行中的 stdout/stderr 增量
|
||||
EvToolResult StreamEventType = "tool.result"
|
||||
EvRunPhase StreamEventType = "run.phase"
|
||||
EvRunCheck StreamEventType = "run.check" // 终轮自动检查,过程区一行
|
||||
EvHITLRequest StreamEventType = "hitl.request"
|
||||
EvHITLResolved StreamEventType = "hitl.resolved"
|
||||
EvPlanUpdate StreamEventType = "plan.update"
|
||||
@@ -58,6 +61,9 @@ const (
|
||||
EvCardUpdated StreamEventType = "card.updated"
|
||||
EvCardStatus StreamEventType = "card.status"
|
||||
EvCardResolved StreamEventType = "card.resolved"
|
||||
// EvSurfaceUpdated:Generative UI Surface 快照保存后的投影事件(提案 U2;
|
||||
// 载荷 core.SurfaceSnapshot,按会话流发布,前端消费在 U4)。
|
||||
EvSurfaceUpdated StreamEventType = "surface.updated"
|
||||
)
|
||||
|
||||
// ProtocolVersion 是事件信封与 payload 词汇的当前版本。
|
||||
@@ -82,17 +88,40 @@ type ToolCallPayload struct {
|
||||
Arguments string `json:"arguments,omitempty"`
|
||||
}
|
||||
|
||||
// ToolCallDeltaPayload 是模型正在编写的工具参数增量。Index 标识同轮第几次调用
|
||||
// (与供应商 tool_calls 下标一致);Name/CallID 往往只在首个分片出现。
|
||||
type ToolCallDeltaPayload struct {
|
||||
Index int `json:"index"`
|
||||
CallID string `json:"callId,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Delta string `json:"delta"`
|
||||
}
|
||||
|
||||
// ToolOutputPayload 是工具执行过程中的输出增量。Stream 为 stdout 或 stderr。
|
||||
type ToolOutputPayload struct {
|
||||
CallID string `json:"callId"`
|
||||
Stream string `json:"stream"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type ToolResultPayload struct {
|
||||
CallID string `json:"callId"`
|
||||
Status string `json:"status"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Truncated bool `json:"truncated,omitempty"`
|
||||
CallID string `json:"callId"`
|
||||
Status string `json:"status"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Truncated bool `json:"truncated,omitempty"`
|
||||
DurationMs float64 `json:"durationMs,omitempty"`
|
||||
}
|
||||
|
||||
type RunPhasePayload struct {
|
||||
Phase RunPhase `json:"phase"`
|
||||
}
|
||||
|
||||
// RunCheckPayload 与 CheckPart 同形,让运行中和重新加载后的检查行一致。
|
||||
type RunCheckPayload struct {
|
||||
Passed bool `json:"passed"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type HITLResolvedPayload struct {
|
||||
CardID ID `json:"cardId"`
|
||||
Answer string `json:"answer,omitempty"`
|
||||
@@ -115,15 +144,15 @@ type AgentSpawnPayload struct {
|
||||
}
|
||||
|
||||
type AgentProgressPayload struct {
|
||||
AgentRun ID `json:"agentRun"`
|
||||
Note string `json:"note,omitempty"`
|
||||
AgentRun ID `json:"agentRun"`
|
||||
Note string `json:"note,omitempty"`
|
||||
Phase RunPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
type AgentResultPayload struct {
|
||||
AgentRun ID `json:"agentRun"`
|
||||
AgentRun ID `json:"agentRun"`
|
||||
Status RunStatus `json:"status"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
// Text 为子代理完整文本产出(squad 步骤交付物用;截断摘要见 Summary)。
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
@@ -138,11 +167,20 @@ type RunCompletedPayload struct {
|
||||
Context *ContextObservation `json:"context,omitempty"`
|
||||
}
|
||||
|
||||
// ContextSection 是组装结果里一段的估算。EstTokens 用 UTF-8 字节数 / 3.35,
|
||||
// 再按上一请求的真实用量校准。
|
||||
type ContextSection struct {
|
||||
Name string `json:"name"`
|
||||
EstTokens int `json:"estTokens"`
|
||||
}
|
||||
|
||||
// ContextObservation 记录一次模型输入的实际构成与预算裁剪(ctxasm 产出)。
|
||||
type ContextObservation struct {
|
||||
Items []string `json:"items"` // 如 system(画像) / history(12) / attachments(1) / knowledge(3)
|
||||
Truncations []string `json:"truncations,omitempty"` // 裁剪记录(历史超窗/工具结果超限/附件截断)
|
||||
EstChars int `json:"estChars"` // 估算输入字符量
|
||||
Items []string `json:"items"` // 如 system(画像) / history(12) / attachments(1) / knowledge(3)
|
||||
Truncations []string `json:"truncations,omitempty"` // 裁剪记录(历史超窗/工具结果超限/附件截断)
|
||||
EstChars int `json:"estChars"` // 估算输入量(token;字段名保持兼容)
|
||||
Sections []ContextSection `json:"sections,omitempty"`
|
||||
PrefixHash string `json:"prefixHash,omitempty"` // 系统前缀哈希,前缀不变时保持稳定
|
||||
}
|
||||
|
||||
type RunFailedPayload struct {
|
||||
|
||||
@@ -23,6 +23,8 @@ type HITLCard struct {
|
||||
// Blocked:阻断态(门禁不通过等)——确认入口禁用并给出出路;阻断本身进审计。
|
||||
Blocked bool `json:"blocked,omitempty"`
|
||||
BlockReason string `json:"blockReason,omitempty"`
|
||||
// Detail 是确认卡上的参数预览或其他补充说明。
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
type HITLKind string
|
||||
|
||||
@@ -17,26 +17,30 @@ type ID = string
|
||||
// Owner 是租户标识(多租户主线,设计 D5/D16):空串 = 单租户默认(嵌入式档/
|
||||
// 未启用认证时的隐式 owner)。API 层强制 owner 过滤,跨租户访问一律 NotFound。
|
||||
type Project struct {
|
||||
ID ID `json:"id"`
|
||||
Owner ID `json:"owner,omitempty"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID ID `json:"id"`
|
||||
Owner ID `json:"owner,omitempty"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Settings map[string]any `json:"settings,omitempty"` // 项目层设置覆盖(4.9 覆盖链)
|
||||
}
|
||||
|
||||
type Conversation struct {
|
||||
ID ID `json:"id"`
|
||||
ProjectID ID `json:"projectId"`
|
||||
Owner ID `json:"owner,omitempty"` // 冗余自项目,查询过滤用
|
||||
Title string `json:"title"`
|
||||
ProfileID ID `json:"profileId,omitempty"` // 空为默认画像
|
||||
Pinned bool `json:"pinned,omitempty"` // 侧栏置顶(方案 A P6/P8:本端组织偏好,服务端权威)
|
||||
Archived bool `json:"archived,omitempty"` // 归档:列表默认隐藏,数据保留
|
||||
AllowCrossRead bool `json:"allowCrossRead,omitempty"` // 跨会话出站:本会话读取同项目其他会话记录(需项目开关 crossSession.read)
|
||||
CrossPrivate bool `json:"crossPrivate,omitempty"` // 跨会话入站:私有会话,不被他读、不出现在他方列表
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID ID `json:"id"`
|
||||
ProjectID ID `json:"projectId"`
|
||||
Owner ID `json:"owner,omitempty"` // 冗余自项目,查询过滤用
|
||||
Title string `json:"title"`
|
||||
ProfileID ID `json:"profileId,omitempty"` // 空为默认画像
|
||||
Pinned bool `json:"pinned,omitempty"` // 侧栏置顶(方案 A P6/P8:本端组织偏好,服务端权威)
|
||||
Archived bool `json:"archived,omitempty"` // 归档:列表默认隐藏,数据保留
|
||||
AllowCrossRead bool `json:"allowCrossRead,omitempty"` // 跨会话出站:本会话读取同项目其他会话记录(需项目开关 crossSession.read)
|
||||
CrossPrivate bool `json:"crossPrivate,omitempty"` // 跨会话入站:私有会话,不被他读、不出现在他方列表
|
||||
// PermissionMode 是会话权限:空或 default 沿用画像;plan 只读加计划工具;auto_edit 放行工作区写文件。
|
||||
PermissionMode string `json:"permissionMode,omitempty"`
|
||||
// SessionNote 是本会话要求,组装时附在上下文末尾,不进入系统前缀。
|
||||
SessionNote string `json:"sessionNote,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// ---- 消息与部件 ----
|
||||
@@ -61,16 +65,22 @@ const (
|
||||
PartHITL PartType = "hitl"
|
||||
PartPlan PartType = "plan"
|
||||
PartError PartType = "error"
|
||||
PartCard PartType = "card" // 插件自定义卡片(Data 为 UI schema)
|
||||
PartCard PartType = "card" // 插件自定义卡片(Data 为 UI schema)
|
||||
PartCheck PartType = "check" // 终轮自动检查,过程区一行,不是给用户的回复
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
ID ID `json:"id"`
|
||||
ConversationID ID `json:"conversationId"`
|
||||
RunID ID `json:"runId,omitempty"`
|
||||
Role Role `json:"role"`
|
||||
Parts []Part `json:"parts"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ID ID `json:"id"`
|
||||
ConversationID ID `json:"conversationId"`
|
||||
RunID ID `json:"runId,omitempty"`
|
||||
Role Role `json:"role"`
|
||||
Parts []Part `json:"parts"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
// Excluded 为真时界面仍显示,但组装上下文时整条(及同一 run 的成组消息)不送入模型。
|
||||
Excluded bool `json:"excluded,omitempty"`
|
||||
// VariantGroup 把同一轮的候选回复绑在一起。只把未排除的那条送入模型。
|
||||
VariantGroup string `json:"variantGroup,omitempty"`
|
||||
VariantIndex int `json:"variantIndex,omitempty"`
|
||||
}
|
||||
|
||||
// Part 是判别联合:Type 决定哪些字段有意义;未用字段为零值。
|
||||
@@ -84,8 +94,9 @@ type Part struct {
|
||||
Artifact *ArtifactRef `json:"artifact,omitempty"`
|
||||
Hitl *HITLSummary `json:"hitl,omitempty"`
|
||||
Plan *PlanSnapshot `json:"plan,omitempty"`
|
||||
Card *CardPart `json:"card,omitempty"`
|
||||
Card *CardPart `json:"card,omitempty"`
|
||||
Err *ErrorPart `json:"error,omitempty"`
|
||||
Check *CheckPart `json:"check,omitempty"`
|
||||
|
||||
// 过程标记:true 的部件在 run 终态后由前端折叠为摘要行(4.2)。
|
||||
Process bool `json:"process,omitempty"`
|
||||
@@ -96,17 +107,17 @@ type Part struct {
|
||||
}
|
||||
|
||||
type ToolCallPart struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments,omitempty"` // 原始 JSON
|
||||
Duration float64 `json:"durationMs,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments,omitempty"` // 原始 JSON
|
||||
Duration float64 `json:"durationMs,omitempty"`
|
||||
}
|
||||
|
||||
type ToolResultPart struct {
|
||||
CallID string `json:"callId"`
|
||||
Status string `json:"status"` // ok | error
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Truncated bool `json:"truncated,omitempty"` // 超长截断并转工件引用(4.10)
|
||||
CallID string `json:"callId"`
|
||||
Status string `json:"status"` // ok | error
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Truncated bool `json:"truncated,omitempty"` // 超长截断并转工件引用(4.10)
|
||||
}
|
||||
|
||||
type ArtifactRef struct {
|
||||
@@ -127,6 +138,12 @@ type ErrorPart struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// CheckPart 是终轮自动检查的结果。过程区显示一行,不进入给用户的回复。
|
||||
type CheckPart struct {
|
||||
Passed bool `json:"passed"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// CardPart.Data 是 UI schema 节点(4.6 有限词汇)。
|
||||
// CardID 非空 = 收件箱 ActivityCard 的引用部件(如 submit_card 产卡:卡片为产物
|
||||
// 权威载体,消息只存引用+锚点渲染);空 = 插件内嵌卡片部件(自包含 Data)。
|
||||
@@ -149,39 +166,40 @@ const (
|
||||
type RunStatus string
|
||||
|
||||
const (
|
||||
RunPending RunStatus = "pending"
|
||||
RunRunning RunStatus = "running"
|
||||
RunWaitingHITL RunStatus = "waiting_hitl"
|
||||
RunCompleted RunStatus = "completed"
|
||||
RunFailed RunStatus = "failed"
|
||||
RunCancelled RunStatus = "cancelled"
|
||||
RunInterrupted RunStatus = "interrupted"
|
||||
RunPending RunStatus = "pending"
|
||||
RunRunning RunStatus = "running"
|
||||
RunWaitingHITL RunStatus = "waiting_hitl"
|
||||
RunCompleted RunStatus = "completed"
|
||||
RunFailed RunStatus = "failed"
|
||||
RunCancelled RunStatus = "cancelled"
|
||||
RunInterrupted RunStatus = "interrupted"
|
||||
)
|
||||
|
||||
type RunPhase string
|
||||
|
||||
const (
|
||||
PhaseThinking RunPhase = "thinking"
|
||||
PhaseActing RunPhase = "acting"
|
||||
PhaseThinking RunPhase = "thinking"
|
||||
PhaseActing RunPhase = "acting"
|
||||
PhaseResponding RunPhase = "responding"
|
||||
PhaseDone RunPhase = "done"
|
||||
PhaseFinalizing RunPhase = "finalizing" // 工作循环已结束,正在整理给用户的最终回复
|
||||
PhaseDone RunPhase = "done"
|
||||
)
|
||||
|
||||
type Run struct {
|
||||
ID ID `json:"id"`
|
||||
ParentRunID ID `json:"parentRunId,omitempty"`
|
||||
ConversationID ID `json:"conversationId"`
|
||||
ProjectID ID `json:"projectId"`
|
||||
Owner ID `json:"owner,omitempty"` // 冗余自会话
|
||||
Kind RunKind `json:"kind"`
|
||||
ProfileID ID `json:"profileId,omitempty"`
|
||||
Status RunStatus `json:"status"`
|
||||
Phase RunPhase `json:"phase,omitempty"`
|
||||
AgentName string `json:"agentName,omitempty"` // subagent 显示名
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
EndedAt *time.Time `json:"endedAt,omitempty"`
|
||||
Usage *Usage `json:"usage,omitempty"`
|
||||
Error *ErrorPart `json:"error,omitempty"`
|
||||
ID ID `json:"id"`
|
||||
ParentRunID ID `json:"parentRunId,omitempty"`
|
||||
ConversationID ID `json:"conversationId"`
|
||||
ProjectID ID `json:"projectId"`
|
||||
Owner ID `json:"owner,omitempty"` // 冗余自会话
|
||||
Kind RunKind `json:"kind"`
|
||||
ProfileID ID `json:"profileId,omitempty"`
|
||||
Status RunStatus `json:"status"`
|
||||
Phase RunPhase `json:"phase,omitempty"`
|
||||
AgentName string `json:"agentName,omitempty"` // subagent 显示名
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
EndedAt *time.Time `json:"endedAt,omitempty"`
|
||||
Usage *Usage `json:"usage,omitempty"`
|
||||
Error *ErrorPart `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type Usage struct {
|
||||
@@ -190,6 +208,14 @@ type Usage struct {
|
||||
TotalTokens int `json:"totalTokens"`
|
||||
DurationMs float64 `json:"durationMs"`
|
||||
ToolCalls int `json:"toolCalls"`
|
||||
// 以下四项是 N3 的计费拆分(additive;旧记录为零值省略):
|
||||
// Last* 是最近一次请求的用量(上一请求),Cache* 是缓存命中累计。
|
||||
LastPromptTokens int `json:"lastPromptTokens,omitempty"`
|
||||
LastCompletionTokens int `json:"lastCompletionTokens,omitempty"`
|
||||
CacheReadTokens int `json:"cacheReadTokens,omitempty"`
|
||||
CacheWriteTokens int `json:"cacheWriteTokens,omitempty"`
|
||||
// Model 是实际给出这段用量的模型。空表示当时没有记录。
|
||||
Model string `json:"model,omitempty"`
|
||||
}
|
||||
|
||||
// ---- 工件 ----
|
||||
@@ -210,20 +236,20 @@ type Artifact struct {
|
||||
// ---- 计划与待办(右栏 Plan / Todos)----
|
||||
|
||||
type PlanSnapshot struct {
|
||||
PlanID ID `json:"planId"`
|
||||
Title string `json:"title"`
|
||||
Steps []PlanStep `json:"steps"`
|
||||
PlanID ID `json:"planId"`
|
||||
Title string `json:"title"`
|
||||
Steps []PlanStep `json:"steps"`
|
||||
}
|
||||
|
||||
type PlanStep struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Status string `json:"status"` // pending | in_progress | done | skipped
|
||||
AgentRun ID `json:"agentRun,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Status string `json:"status"` // pending | in_progress | done | skipped
|
||||
AgentRun ID `json:"agentRun,omitempty"`
|
||||
}
|
||||
|
||||
type TodoList struct {
|
||||
ConversationID ID `json:"conversationId"`
|
||||
ConversationID ID `json:"conversationId"`
|
||||
Items []TodoItem `json:"items"`
|
||||
}
|
||||
|
||||
|
||||
@@ -52,24 +52,35 @@ type PluginPanel struct {
|
||||
}
|
||||
|
||||
// WorkflowDef:工作流即数据(4.6)。Step 可产 HITL 卡片、调工具、产出工件。
|
||||
// 模板占位符:{{input}}、{{slots.<key>}}、{{steps.<id>.output}}。
|
||||
type WorkflowDef struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
IntentID string `json:"intentId,omitempty"` // 绑定意图(命中即启动)
|
||||
MaxRetries int `json:"maxRetries,omitempty"` // 整条工作流的 critic 重试上限
|
||||
Steps []WorkflowStep `json:"steps"`
|
||||
}
|
||||
|
||||
// StepCritic:单步验收。无法解析的判定按不通过处理。
|
||||
type StepCritic struct {
|
||||
Criteria string `json:"criteria"`
|
||||
MaxRetries int `json:"maxRetries,omitempty"` // 本步重试上限;0 表示缺省 1 次
|
||||
}
|
||||
|
||||
type WorkflowStep struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Kind string `json:"kind"` // prompt | tool | hitl | artifact | card
|
||||
Kind string `json:"kind"` // prompt | tool | hitl | artifact | card | script
|
||||
Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板
|
||||
Agent string `json:"agent,omitempty"` // prompt: 子代理 profile,缺省 general
|
||||
Tool string `json:"tool,omitempty"` // tool: 工具名
|
||||
Args json.RawMessage `json:"args,omitempty"` // tool: 参数模板
|
||||
Hitl *HITLCardTemplate `json:"hitl,omitempty"` // hitl: 卡片模板
|
||||
Artifact string `json:"artifact,omitempty"` // artifact: 产出工件名模板
|
||||
Card *ActivityCardTemplate `json:"card,omitempty"` // card: 产收件箱任务卡
|
||||
Script string `json:"script,omitempty"` // script: 插件目录内的相对路径
|
||||
Critic *StepCritic `json:"critic,omitempty"`
|
||||
}
|
||||
|
||||
// ActivityCardTemplate:工作流步骤产卡模板(kind=card)——步骤执行时生成
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# 通用规范采纳记录
|
||||
|
||||
- 项目/负责人:Agent-System-Core / weichen
|
||||
- 状态:active
|
||||
- 规范位置:`deps/development-guidelines`
|
||||
- 规范性文档目录:`policy/`(本仓现有 `policy/README.md`;项目规则与制度放此处)
|
||||
- 过程文档位置:仓内固定目录 `.task/`(任务卡与任务主记录;任务主记录同时随 PR 描述维护,豁免理由见「项目差异」)
|
||||
- 版本权威:本仓 gitlink `deps/development-guidelines` @ `2688fd2be6a5`(完整 SHA `2688fd2be6a52c0bf110e7e6366b6f291ab5e4eb`);可读标签:无
|
||||
- 批准/生效依据:feature/governance-adoption PR(Agent-System-Core)
|
||||
- 替代范围和迁移说明:无
|
||||
|
||||
| 规范/工作流 | 适用条件与范围 | 采纳或不适用理由 | 执行方式/证据 |
|
||||
|---|---|---|---|
|
||||
| core/baseline.md | 日常工作 | 采纳:所有任务的硬边界、默认做法与完成声明基线 | manual |
|
||||
| workflows/delivery.md、debugging.md、handoff.md | 交付、排障与交接 | 采纳:本仓以 PR 交付,Bug 排障与会话交接走统一流程 | manual |
|
||||
| standards/collaboration.md | 并行共同起点、两级分支、节点同步与陈旧候选拒绝 | 采纳:PR 合入前须同步最新目标分支并附证据(见 AGENTS.md) | manual;暂无真实平台负例,未登记 enforced |
|
||||
| standards/merge-queue.md | 多个就绪任务的最终同步、CI、Review 与合入调度 | 不适用:本仓由单一 owner 串行合入,当前无多个就绪任务竞争交付窗口;出现时另行登记 | 不适用 |
|
||||
| standards/agent-execution.md | 工程 Agent CLI 执行路由、会话隔离与受控集成 | 采纳:允许 AI 协助代码实施;配置见 AGENT-EXECUTION.md | AGENT-EXECUTION.md + scripts/agent-exec(validate / admit);证据:governance check 与正负例(.task/feature-governance-adoption.md) |
|
||||
| standards/verification.md | 验证与结论 | 采纳:完成声明须附实际命令输出 | 命令见 PROJECT.md「验证」节(`go test ./...`、`git diff --check`、`scripts/agent-exec validate --repo .`、`scripts/dm governance check --repo .`);CI:`.gitea/workflows/governance.yml` 运行治理门禁 |
|
||||
| standards/module-contracts.md、standards/multi-repo.md | 公共契约(Go 类型、黄金样本、TS 镜像)的边界,以及依赖仓以 gitlink 固定版本 | 采纳:本仓是公共契约库,治理依赖以 gitlink 固定 | manual;gitlink 与 `.gitmodules` 固定,CI 物化白名单见 `.gitea/workflows/governance.yml`;契约一致性由 `conformance_test.go` 与 `testdata/golden` 覆盖 |
|
||||
| standards/security.md | CI 凭据、submodule 取回与执行工具边界 | 采纳:CI 仅以限定 URL 的凭据取回两个治理 gitlink,凭据不落盘 | manual;`.gitea/workflows/governance.yml` 白名单与凭据约束 |
|
||||
| workflows/design-verification.md、standards/testability.md | 契约增量的设计与验证、纯函数的可独立测试性 | 采纳:契约变更按增量设计验收,并以黄金样本与一致性测试验证 | manual;`conformance_test.go`、`testdata/golden` |
|
||||
| standards/failure-containment.md、data-reliability.md、observability.md、configuration.md、release-operations.md、automation.md | 运行时服务、数据库、部署发布、定时自动化与故障控制 | 不适用:本仓只含契约与纯函数,不含 I/O、数据库或运行服务,不独立部署(见 PROJECT.md) | 不适用 |
|
||||
|
||||
## 项目差异
|
||||
|
||||
| 原规则/章节 | 项目采用方式 | 原因与风险 | 负责人/复查条件 |
|
||||
|---|---|---|---|
|
||||
| 过程文档默认放独立过程仓 | 任务卡与任务主记录放仓内 `.task/`,AGENT-EXECUTION.md 的 `exempt_paths` 已豁免 `.task/**` | 单一 owner 的小仓库,过程记录体量小,独立过程仓成本不划算;风险是过程记录与代码同仓,禁止写入凭据或 token | weichen;协作人增加或过程记录需跨仓共享时复查 |
|
||||
|
||||
## 本次升级或首次接入验证
|
||||
|
||||
- 入口:`AGENTS.md` 保留原内容并追加「Agent 执行与新项目准入(强制)」;`PROJECT.md`「验证」节补充 `scripts/agent-exec validate --repo .` 与 `scripts/dm governance check --repo .`;`AGENT-EXECUTION.md` 原样保留(`governance init --adopt` 跳过已存在文件)。
|
||||
- 版本获取:`deps/development-guidelines` @ `2688fd2be6a5`,`deps/development-manager` @ `bd3a7de57a3a`;`.gitmodules` 使用规范 SSH 地址,不含凭证或本机路径。
|
||||
- 占位符:模板占位符已全部替换;`policy/guidelines-adoption.md` 与追加的准入段中不再有模板尖括号文字。
|
||||
- 冲突检查:仓内原无其他采纳记录(无 `policy/adoption.md`),原无旧版 Agent 执行入口(无 `scripts/agent_exec.py`),原无 `.gitea/workflows`,治理 workflow 为新增,无需修改既有 CI。
|
||||
- 正负例与 Review/CI 证据:见 `.task/feature-governance-adoption.md`。
|
||||
+125
-1
@@ -1,6 +1,9 @@
|
||||
package core
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 智能体画像(4.7):核心定制机制,全部数据化、可导入导出。
|
||||
type AgentProfile struct {
|
||||
@@ -107,6 +110,16 @@ const (
|
||||
EffectRestart SettingEffect = "restart" // 需重启
|
||||
)
|
||||
|
||||
// SettingScope 声明设置项的写入归属(提案 2026-10-13-rdm-settings-members-duties-perms-increment P2)。
|
||||
// 空串 = 默认:user 与 project 层可写(向后兼容既有 schema)。
|
||||
type SettingScope string
|
||||
|
||||
const (
|
||||
// SettingScopeSystem 受管项:仅系统管理面(admin)可写;user/project 写入被拒,
|
||||
// 生效链忽略这两层的存量覆盖值。
|
||||
SettingScopeSystem SettingScope = "system"
|
||||
)
|
||||
|
||||
// SettingDesc 描述一个设置项;设置页按此渲染并标注生效时机(schema 驱动)。
|
||||
type SettingDesc struct {
|
||||
Key string `json:"key"`
|
||||
@@ -118,6 +131,117 @@ type SettingDesc struct {
|
||||
Options []string `json:"options,omitempty"` // enum
|
||||
Effect SettingEffect `json:"effect"`
|
||||
Secret bool `json:"secret,omitempty"`
|
||||
Scope SettingScope `json:"scope,omitempty"` // 空 = user/project 可写;system = 受管
|
||||
}
|
||||
|
||||
// ManagedPaths 返回 schema 中 scope=system 的键路径(点路径形式)。
|
||||
func ManagedPaths(descs []SettingDesc) []string {
|
||||
out := make([]string, 0, len(descs))
|
||||
for _, d := range descs {
|
||||
if d.Scope == SettingScopeSystem {
|
||||
out = append(out, d.Key)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FlattenSettingLeafPaths 列出 values 的全部叶子点路径。子树为标量或数组时该子树
|
||||
// 根即叶子。用于受管触碰检测:深合并语义下叶子决定最终覆盖面。
|
||||
func FlattenSettingLeafPaths(values map[string]any) []string {
|
||||
var out []string
|
||||
var walk func(prefix string, m map[string]any)
|
||||
walk = func(prefix string, m map[string]any) {
|
||||
for k, v := range m {
|
||||
path := k
|
||||
if prefix != "" {
|
||||
path = prefix + "." + k
|
||||
}
|
||||
if vm, ok := v.(map[string]any); ok && len(vm) > 0 {
|
||||
walk(path, vm)
|
||||
continue
|
||||
}
|
||||
out = append(out, path)
|
||||
}
|
||||
}
|
||||
walk("", values)
|
||||
return out
|
||||
}
|
||||
|
||||
// pathTouchesManaged 判断 path 是否触碰 managed:等于受管路径、位于受管子树内
|
||||
// (受管路径是 path 的前缀),或整体覆盖受管子树(path 是受管路径的前缀——
|
||||
// 标量覆盖整棵受管 map 同样破坏受管值)。
|
||||
func pathTouchesManaged(path string, managed []string) bool {
|
||||
for _, mp := range managed {
|
||||
if path == mp ||
|
||||
strings.HasPrefix(path, mp+".") ||
|
||||
strings.HasPrefix(mp, path+".") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TouchesManagedSettings 返回 values 中触碰受管路径的叶子;空切片 = 无触碰。
|
||||
// 调用方须传入已展开点路径的嵌套 map(normalize 后)。
|
||||
func TouchesManagedSettings(values map[string]any, managed []string) []string {
|
||||
var hits []string
|
||||
for _, p := range FlattenSettingLeafPaths(values) {
|
||||
if pathTouchesManaged(p, managed) {
|
||||
hits = append(hits, p)
|
||||
}
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
// StripManagedSettings 返回删除受管路径后的浅拷贝(不改入参)。受管路径的祖先
|
||||
// 若因此只剩空 map 一并删除。入参须为嵌套形式(点路径已展开)。
|
||||
func StripManagedSettings(values map[string]any, managed []string) map[string]any {
|
||||
if len(values) == 0 || len(managed) == 0 {
|
||||
return values
|
||||
}
|
||||
// 叶子命中或祖先整体命中都删;逐叶子重建最简单可靠。
|
||||
leaves := FlattenSettingLeafPaths(values)
|
||||
keep := map[string]any{}
|
||||
for _, leaf := range leaves {
|
||||
if pathTouchesManaged(leaf, managed) {
|
||||
continue
|
||||
}
|
||||
setPath(keep, leaf, lookupAny(values, leaf))
|
||||
}
|
||||
return keep
|
||||
}
|
||||
|
||||
func setPath(m map[string]any, path string, val any) {
|
||||
parts := strings.Split(path, ".")
|
||||
cur := m
|
||||
for i, p := range parts {
|
||||
if i == len(parts)-1 {
|
||||
cur[p] = val
|
||||
return
|
||||
}
|
||||
next, ok := cur[p].(map[string]any)
|
||||
if !ok {
|
||||
next = map[string]any{}
|
||||
cur[p] = next
|
||||
}
|
||||
cur = next
|
||||
}
|
||||
}
|
||||
|
||||
func lookupAny(m map[string]any, path string) any {
|
||||
parts := strings.Split(path, ".")
|
||||
var cur any = m
|
||||
for _, p := range parts {
|
||||
cm, ok := cur.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
cur, ok = cm[p]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
// EffectiveSettings 是覆盖链(system → user → project → conversation)深合并结果。
|
||||
|
||||
@@ -26,6 +26,10 @@ type PromptModule struct {
|
||||
BasedOnVersion int `json:"basedOnVersion,omitempty"` // 用户副本基于的内置版本
|
||||
Drifted bool `json:"drifted,omitempty"` // 用户副本落后于当前内置版本(提示用,不阻断)
|
||||
Disabled bool `json:"disabled,omitempty"` // 用户停用
|
||||
// 子代理 profile。只对 agent.sub.* 有意义;空字段表示沿用内置。
|
||||
Tools []string `json:"tools,omitempty"`
|
||||
Model string `json:"model,omitempty"` // lightweight | chat
|
||||
MaxTurns int `json:"maxTurns,omitempty"`
|
||||
}
|
||||
|
||||
// PromptTrigger 触发条件;模块命中 = 任一 trigger 命中(再过 RequiresAttachments)。
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# 共享 Agent 执行器薄入口:转调 scripts/dm 的 agent-exec 子命令(validate / run / apply / admit / cleanup)。
|
||||
set -euo pipefail
|
||||
exec "$(dirname "$0")/dm" agent-exec "$@"
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# 共享 dm-server 薄入口:读取 deps/development-manager 的 gitlink 钉住提交,
|
||||
# 按该提交构建并缓存 dm-server,再 exec 其子命令(agent-exec、task、governance)。执行语义与退出码以该二进制为准。
|
||||
set -euo pipefail
|
||||
|
||||
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
|
||||
dm="$root/deps/development-manager"
|
||||
|
||||
die() { echo "dm: $*" >&2; exit 2; }
|
||||
|
||||
# git 钩子会导出仓库本地变量(GIT_DIR、GIT_INDEX_FILE 等),它们会让 `git -C "$dm"` 解析到外层仓库。
|
||||
# 所有针对 DM 检出的 git 与 go 调用都在清除这些变量后执行;读取外层 gitlink 与最终 exec 仍保留继承环境。
|
||||
dm_local_env="GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_COMMON_DIR GIT_GRAFT_FILE GIT_NAMESPACE GIT_CONFIG GIT_CONFIG_PARAMETERS GIT_CONFIG_COUNT GIT_SHALLOW_FILE GIT_IMPLICIT_WORK_TREE $(git rev-parse --local-env-vars 2>/dev/null || true)"
|
||||
dm_clean_env() { for v in $dm_local_env; do unset "$v"; done; }
|
||||
dmgit() ( dm_clean_env; git -C "$dm" "$@" )
|
||||
|
||||
# 1. 钉住版本只来自索引中的 gitlink(mode 160000),不读取工作区文件。
|
||||
entry=$(git -C "$root" ls-files -s -- deps/development-manager 2>/dev/null || true)
|
||||
[ -n "$entry" ] || die "索引中缺少 deps/development-manager 的 gitlink,无法确定钉住的 DM 版本"
|
||||
read -r mode pin _stage _path <<<"$entry"
|
||||
[ "$mode" = 160000 ] || die "deps/development-manager 不是 gitlink(mode=$mode)"
|
||||
[[ "$pin" =~ ^[0-9a-f]{40}$ ]] || die "gitlink 提交格式无效:$pin"
|
||||
|
||||
# 2. 检出必须存在且确为 DM 独立仓库(避免 git -C 误解析到外层仓库),HEAD 必须等于钉住提交。
|
||||
[ -e "$dm/.git" ] || die "deps/development-manager 未检出;请按钉住提交 $pin 检出该 gitlink"
|
||||
top=$(dmgit rev-parse --show-toplevel 2>/dev/null || true)
|
||||
[ -n "$top" ] && [ "$top" = "$(cd "$dm" && pwd -P)" ] \
|
||||
|| die "deps/development-manager 不是独立检出;请按钉住提交 $pin 检出该 gitlink"
|
||||
head=$(dmgit rev-parse HEAD 2>/dev/null || true)
|
||||
[ "$head" = "$pin" ] \
|
||||
|| die "deps/development-manager HEAD(${head:-未知})与钉住提交 $pin 不一致;请按钉住提交检出"
|
||||
|
||||
# 3. 检出工作树必须干净(忽略嵌套 submodule 状态)。
|
||||
dirty=$(dmgit status --porcelain --ignore-submodules=all) \
|
||||
|| die "无法读取 deps/development-manager 的工作树状态"
|
||||
[ -z "$dirty" ] || die "deps/development-manager 存在未提交改动,拒绝运行(请先清理该检出)"
|
||||
|
||||
# 4. 二进制按钉住提交缓存;缺失时在缓存目录内临时构建,完成后原子改名,避免留下半成品。
|
||||
cache="${DM_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/development-manager/dm-server}"
|
||||
bin="$cache/dm-server-$pin"
|
||||
if [ ! -x "$bin" ]; then
|
||||
command -v go >/dev/null 2>&1 || die "缺少 go,无法构建 dm-server($pin)"
|
||||
mkdir -p "$cache"
|
||||
cache=$(cd "$cache" && pwd -P)
|
||||
bin="$cache/dm-server-$pin"
|
||||
work=$(mktemp -d "$cache/build-$pin.XXXXXX")
|
||||
if ! (dm_clean_env; cd "$dm" && GOWORK=off go build -trimpath -o "$work/dm-server" ./cmd/dm-server); then
|
||||
rm -rf "$work"
|
||||
die "构建 dm-server 失败($pin)"
|
||||
fi
|
||||
mv -f "$work/dm-server" "$bin"
|
||||
rm -rf "$work"
|
||||
fi
|
||||
|
||||
exec "$bin" "$@"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# 本地检查(pre-commit):集成分支直提拒绝 + 分支命名 + 本地完整性核对。权威准入在 PR(baseline.md:26)。
|
||||
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||
case "$branch" in
|
||||
main|master)
|
||||
echo "pre-commit: 拒绝——集成分支 ${branch} 不得直接提交,走 feature 分支 + PR(collaboration.md)" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
echo "$branch" | grep -qE '^(feature|fix|hotfix|release)/[A-Za-z0-9._-]+$' || {
|
||||
echo "pre-commit: 拒绝——分支名须为 feature|fix|hotfix|release/<slug>,当前:${branch}" >&2
|
||||
exit 1
|
||||
}
|
||||
root=$(git rev-parse --show-toplevel)
|
||||
# admit 只是本地完整性/篡改证据(SELF):同一 OS 用户可读取本地 HMAC 密钥并绕过本钩子,
|
||||
# 它不是 GATE,也不是可信身份。缺失或无效的本地记录仍拒绝;交付信任来自精确 HEAD 的 CI 或独立 REPLAY。
|
||||
"$root/scripts/agent-exec" admit --repo "$root"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# 通用任务入口:转调 scripts/dm 的 task 子命令(triage / start / pr-check)。
|
||||
set -euo pipefail
|
||||
exec "$(dirname "$0")/dm" task "$@"
|
||||
@@ -0,0 +1,85 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManagedPathsFiltersSystemScope(t *testing.T) {
|
||||
descs := []SettingDesc{
|
||||
{Key: "models.chat.model", Scope: SettingScopeSystem},
|
||||
{Key: "ui.theme"},
|
||||
{Key: "tools.tavily.apiKeys", Scope: SettingScopeSystem},
|
||||
}
|
||||
got := ManagedPaths(descs)
|
||||
sort.Strings(got)
|
||||
want := []string{"models.chat.model", "tools.tavily.apiKeys"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("ManagedPaths = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTouchesManagedSettings(t *testing.T) {
|
||||
managed := []string{"models.chat.model", "tools.tavily.apiKeys", "telemetry.otlpEndpoint"}
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
want []string
|
||||
}{
|
||||
{"exact leaf", map[string]any{"models": map[string]any{"chat": map[string]any{"model": "x"}}}, []string{"models.chat.model"}},
|
||||
{"sibling untouched", map[string]any{"models": map[string]any{"chat": map[string]any{"baseUrl": "x"}}}, nil},
|
||||
{"ancestor scalar overwrite", map[string]any{"models": "oops"}, []string{"models"}},
|
||||
{"subtree inside managed", map[string]any{"telemetry": map[string]any{"otlpEndpoint": "http://x"}}, []string{"telemetry.otlpEndpoint"}},
|
||||
{"other group untouched", map[string]any{"ui": map[string]any{"theme": "light"}}, nil},
|
||||
{"empty map", map[string]any{}, nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := TouchesManagedSettings(tc.in, managed)
|
||||
sort.Strings(got)
|
||||
if tc.want == nil && len(got) != 0 {
|
||||
t.Fatalf("TouchesManagedSettings = %v, want none", got)
|
||||
}
|
||||
if tc.want != nil && !reflect.DeepEqual(got, tc.want) {
|
||||
t.Fatalf("TouchesManagedSettings = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripManagedSettings(t *testing.T) {
|
||||
managed := []string{"models.chat.model", "models.chat.apiKey"}
|
||||
in := map[string]any{
|
||||
"models": map[string]any{
|
||||
"chat": map[string]any{"model": "x", "baseUrl": "https://b"},
|
||||
"lightweight": map[string]any{"model": "y"},
|
||||
},
|
||||
"ui": map[string]any{"theme": "dark"},
|
||||
}
|
||||
got := StripManagedSettings(in, managed)
|
||||
want := map[string]any{
|
||||
"models": map[string]any{
|
||||
"chat": map[string]any{"baseUrl": "https://b"},
|
||||
"lightweight": map[string]any{"model": "y"},
|
||||
},
|
||||
"ui": map[string]any{"theme": "dark"},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("StripManagedSettings = %v, want %v", got, want)
|
||||
}
|
||||
if in["models"].(map[string]any)["chat"].(map[string]any)["model"] != "x" {
|
||||
t.Fatal("StripManagedSettings 不应修改入参")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripManagedSettingsWholeSubtree(t *testing.T) {
|
||||
managed := []string{"models.chat.model", "models.chat.baseUrl", "models.chat.apiKey", "models.chat.fallbacks"}
|
||||
in := map[string]any{
|
||||
"models": map[string]any{"chat": map[string]any{"model": "x", "baseUrl": "b", "apiKey": "k"}},
|
||||
}
|
||||
got := StripManagedSettings(in, managed)
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("整棵受管子树应被剥除(含空父),got %v", got)
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package core
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Surface 契约(Generative UI 提案 U2;依据 r2「U1 结果与 r2 修订」):
|
||||
// 有版本的呈现产物信封 + 页面规格校验。信封与版本字段由服务端分配/校验,
|
||||
// 模型只产出 SurfaceSpec;节点级稳定 ID 与来源标注由 UINode 的可选字段承载
|
||||
// (ui.go),旧卡片/表单路径零影响。
|
||||
|
||||
const (
|
||||
// SurfaceSchemaVersion 是 Surface 信封/规格的结构版本。
|
||||
SurfaceSchemaVersion = 1
|
||||
// SurfaceCatalogVersion 是当前认可的组件目录版本(现有 16 类 UINode 词表)。
|
||||
SurfaceCatalogVersion = "ui-v1"
|
||||
// SurfaceMaxNodes / SurfaceMaxDepth / SurfaceMaxBytes 是 U1 试验采纳的资源
|
||||
// 预算(提案 KTD5):超限整页拒绝,不静默裁剪。
|
||||
SurfaceMaxNodes = 200
|
||||
SurfaceMaxDepth = 8
|
||||
SurfaceMaxBytes = 256 * 1024
|
||||
)
|
||||
|
||||
// SurfaceCompat 兼容标识(KTD3):由宿主与已安装组件协商记录,不采信自报值。
|
||||
type SurfaceCompat struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
CatalogVersion string `json:"catalogVersion"`
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
}
|
||||
|
||||
// SurfaceIdentity 身份信封:全部由服务端分配或从当前请求绑定;提交方不能自报。
|
||||
type SurfaceIdentity struct {
|
||||
SurfaceID ID `json:"surfaceId"`
|
||||
Owner string `json:"owner"`
|
||||
ProjectID ID `json:"projectId,omitempty"`
|
||||
ConversationID ID `json:"conversationId,omitempty"`
|
||||
SourceRunID ID `json:"sourceRunId,omitempty"`
|
||||
PluginID string `json:"pluginId"`
|
||||
}
|
||||
|
||||
// SurfaceSpec 是模型可产出的页面规格(KTD3「页面」层)。
|
||||
type SurfaceSpec struct {
|
||||
Title string `json:"title"`
|
||||
Nodes []UINode `json:"nodes"`
|
||||
}
|
||||
|
||||
// SurfaceSnapshot 是已保存的呈现产物:信封+兼容+版本+页面+回放摘要。
|
||||
// Revision 单调递增;更新必须携带 BaseRevision,冲突由服务端拒绝(不自动覆盖)。
|
||||
type SurfaceSnapshot struct {
|
||||
Identity SurfaceIdentity `json:"identity"`
|
||||
Compat SurfaceCompat `json:"compat"`
|
||||
Revision int `json:"revision"`
|
||||
BaseRevision int `json:"baseRevision,omitempty"`
|
||||
ContextVersion string `json:"contextVersion,omitempty"`
|
||||
ActivationEpoch int64 `json:"activationEpoch"`
|
||||
Spec SurfaceSpec `json:"spec"`
|
||||
SemanticText string `json:"semanticText,omitempty"`
|
||||
}
|
||||
|
||||
// SurfaceMeta 是列表投影(不携带整页规格)。
|
||||
type SurfaceMeta struct {
|
||||
SurfaceID ID `json:"surfaceId"`
|
||||
Title string `json:"title"`
|
||||
PluginID string `json:"pluginId"`
|
||||
Revision int `json:"revision"`
|
||||
ConversationID ID `json:"conversationId,omitempty"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// ValidateSurfaceSpec 按目录纪律校验页面规格,返回违规清单(空即合法)。
|
||||
// knownSources 为 nil 表示本层不校验来源授权(U3 数据绑定层职责),仅做格式
|
||||
// 检查;非空时引用/doc-ref 未知来源即为违规。未知类型、非法 props、重复 ID、
|
||||
// 超限均拒绝(U1 试验校验器的 Go 版本:目录=提示=校验三同源)。
|
||||
func ValidateSurfaceSpec(spec SurfaceSpec, knownSources map[string]bool) []string {
|
||||
var errs []string
|
||||
seen := map[string]bool{}
|
||||
nodes := 0
|
||||
|
||||
var walk func(n UINode, path string, depth int)
|
||||
walk = func(n UINode, path string, depth int) {
|
||||
nodes++
|
||||
if nodes > SurfaceMaxNodes {
|
||||
errs = append(errs, fmt.Sprintf("%s: 节点数超过 %d", path, SurfaceMaxNodes))
|
||||
return
|
||||
}
|
||||
if depth > SurfaceMaxDepth {
|
||||
errs = append(errs, fmt.Sprintf("%s: 嵌套深度超过 %d", path, SurfaceMaxDepth))
|
||||
return
|
||||
}
|
||||
if n.ID != "" {
|
||||
if seen[n.ID] {
|
||||
errs = append(errs, fmt.Sprintf("%s: 重复节点 id %q", path, n.ID))
|
||||
}
|
||||
seen[n.ID] = true
|
||||
}
|
||||
for _, s := range n.Source {
|
||||
if s == "" {
|
||||
errs = append(errs, path+": source 含空标识")
|
||||
continue
|
||||
}
|
||||
if knownSources != nil && !knownSources[s] {
|
||||
errs = append(errs, fmt.Sprintf("%s: 引用未知来源 %q", path, s))
|
||||
}
|
||||
}
|
||||
switch n.Type {
|
||||
case UISection:
|
||||
for i, c := range n.Children {
|
||||
walk(c, fmt.Sprintf("%s/child[%d]", path, i), depth+1)
|
||||
}
|
||||
case UIList:
|
||||
if n.Items == nil {
|
||||
errs = append(errs, path+": list 需要 items")
|
||||
}
|
||||
case UIKV:
|
||||
if n.KV == nil {
|
||||
errs = append(errs, path+": kv 需要键值对数组")
|
||||
}
|
||||
case UIForm:
|
||||
if len(n.Fields) == 0 {
|
||||
errs = append(errs, path+": form.fields 需非空")
|
||||
}
|
||||
for _, f := range n.Fields {
|
||||
switch f.Type {
|
||||
case "text", "textarea", "number", "boolean", "select":
|
||||
default:
|
||||
errs = append(errs, fmt.Sprintf("%s: 字段 %q 非法 type %q", path, f.Key, f.Type))
|
||||
}
|
||||
if f.Type == "select" && len(f.Options) == 0 {
|
||||
errs = append(errs, fmt.Sprintf("%s: select 字段 %q 缺 options", path, f.Key))
|
||||
}
|
||||
}
|
||||
case UIMarkdown:
|
||||
if n.Markdown == "" {
|
||||
errs = append(errs, path+": markdown 需要正文")
|
||||
}
|
||||
case UIDocRef:
|
||||
if n.DocID == "" {
|
||||
errs = append(errs, path+": doc-ref 需要 docId")
|
||||
} else if knownSources != nil && !knownSources[string(n.DocID)] {
|
||||
errs = append(errs, fmt.Sprintf("%s: doc-ref 引用未知工件 %q", path, n.DocID))
|
||||
}
|
||||
case UIActions:
|
||||
if len(n.Actions) == 0 {
|
||||
errs = append(errs, path+": actions 需非空")
|
||||
}
|
||||
for _, a := range n.Actions {
|
||||
if a.ID == "" || a.Label == "" {
|
||||
errs = append(errs, fmt.Sprintf("%s: action 缺 id/label", path))
|
||||
}
|
||||
switch a.Style {
|
||||
case "", "primary", "default", "danger":
|
||||
default:
|
||||
errs = append(errs, fmt.Sprintf("%s: action %q 非法 style %q", path, a.ID, a.Style))
|
||||
}
|
||||
}
|
||||
case UIChartLite:
|
||||
if n.Chart == nil || (n.Chart.Kind != "bar" && n.Chart.Kind != "line") ||
|
||||
len(n.Chart.Labels) != len(n.Chart.Series) {
|
||||
errs = append(errs, path+": chart 需要 kind∈{bar,line} 且 labels/series 等长")
|
||||
}
|
||||
case UIAvatar:
|
||||
if n.Avatar == nil || n.Avatar.Name == "" {
|
||||
errs = append(errs, path+": avatar 需要 name")
|
||||
}
|
||||
case UIProgress:
|
||||
if n.Progress == nil || n.Progress.Value < 0 || n.Progress.Value > 1 {
|
||||
errs = append(errs, path+": progress.value 需在 0~1")
|
||||
}
|
||||
case UICompare:
|
||||
if n.Compare == nil {
|
||||
errs = append(errs, path+": compare 需要 left/right")
|
||||
} else {
|
||||
for side, sd := range map[string]UICompareSide{"left": n.Compare.Left, "right": n.Compare.Right} {
|
||||
for i, c := range sd.Nodes {
|
||||
walk(c, fmt.Sprintf("%s/%s[%d]", path, side, i), depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
case UITree:
|
||||
if n.Tree == nil || len(n.Tree.Nodes) == 0 {
|
||||
errs = append(errs, path+": tree.nodes 需非空")
|
||||
}
|
||||
case UITable:
|
||||
if n.Table == nil || len(n.Table.Columns) == 0 {
|
||||
errs = append(errs, path+": table.columns 需非空")
|
||||
} else {
|
||||
for i, r := range n.Table.Rows {
|
||||
if len(r.Cells) != len(n.Table.Columns) {
|
||||
errs = append(errs, fmt.Sprintf("%s: 行[%d] cells 与 columns 不等长", path, i))
|
||||
}
|
||||
}
|
||||
}
|
||||
case UISteps:
|
||||
if n.Steps == nil || len(n.Steps.Steps) == 0 || n.Steps.Current < 0 || n.Steps.Current >= len(n.Steps.Steps) {
|
||||
errs = append(errs, path+": steps 需非空且 current 在范围内")
|
||||
}
|
||||
case UITabs:
|
||||
if n.Tabs == nil || len(n.Tabs.Tabs) == 0 {
|
||||
errs = append(errs, path+": tabs.tabs 需非空")
|
||||
} else {
|
||||
for i, tb := range n.Tabs.Tabs {
|
||||
for j, c := range tb.Nodes {
|
||||
walk(c, fmt.Sprintf("%s/tab[%d][%d]", path, i, j), depth+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
case UITimeline:
|
||||
if n.Timeline == nil || len(n.Timeline.Items) == 0 {
|
||||
errs = append(errs, path+": timeline.items 需非空")
|
||||
}
|
||||
default:
|
||||
errs = append(errs, fmt.Sprintf("%s: 未知节点类型 %q", path, n.Type))
|
||||
}
|
||||
}
|
||||
|
||||
if spec.Title == "" {
|
||||
errs = append(errs, "$: title 必填")
|
||||
}
|
||||
if len(spec.Nodes) == 0 {
|
||||
errs = append(errs, "$: nodes 需非空")
|
||||
}
|
||||
for i, n := range spec.Nodes {
|
||||
walk(n, fmt.Sprintf("$[%d]", i), 1)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func validSpec() SurfaceSpec {
|
||||
return SurfaceSpec{
|
||||
Title: "三家候选工具选型对比",
|
||||
Nodes: []UINode{
|
||||
{Type: UISection, ID: "n1", Title: "一、概览", Children: []UINode{
|
||||
{Type: UIMarkdown, ID: "n1a", Source: []string{"doc:eval/rc-01/vendor1"}, Markdown: "本期评估三家候选。"},
|
||||
}},
|
||||
{Type: UITable, ID: "n2", Source: []string{"doc:eval/rc-01/vendor1", "doc:eval/rc-01/vendor2"}, Table: &UITableSpec{
|
||||
Columns: []UITableColumn{{Key: "k", Label: "维度"}, {Key: "v", Label: "值"}},
|
||||
Rows: []UITableRow{{ID: "r1", Cells: [][2]string{{"k", "价格"}, {"v", "¥8/人/天"}}}},
|
||||
}},
|
||||
{Type: UISteps, ID: "n3", Steps: &UIStepsSpec{Current: 1, Steps: []UIStep{
|
||||
{Label: "需求冻结", Status: "done"}, {Label: "契约定义", Status: "active"},
|
||||
}}},
|
||||
{Type: UIForm, ID: "n4", Fields: []UIField{
|
||||
{Key: "years", Label: "归档年限", Type: "select", Options: []string{"3", "5"}, Required: true},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSurfaceSpecValid(t *testing.T) {
|
||||
if errs := ValidateSurfaceSpec(validSpec(), nil); len(errs) != 0 {
|
||||
t.Fatalf("合法规格不应有违规: %v", errs)
|
||||
}
|
||||
// JSON 往返后仍合法(信封序列化面)。
|
||||
b, err := json.Marshal(validSpec())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var back SurfaceSpec
|
||||
if err := json.Unmarshal(b, &back); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if errs := ValidateSurfaceSpec(back, nil); len(errs) != 0 {
|
||||
t.Fatalf("JSON 往返后不应有违规: %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSurfaceSpecViolations(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mut func(*SurfaceSpec)
|
||||
want string
|
||||
}{
|
||||
{"未知类型", func(s *SurfaceSpec) { s.Nodes[0].Type = " hologram" }, "未知节点类型"},
|
||||
{"重复节点 id", func(s *SurfaceSpec) { s.Nodes[1].ID = "n1" }, "重复节点 id"},
|
||||
{"表格行列不齐", func(s *SurfaceSpec) {
|
||||
s.Nodes[1].Table.Rows = []UITableRow{{Cells: [][2]string{{"k", "只有一列"}}}}
|
||||
}, "不等长"},
|
||||
{"select 缺 options", func(s *SurfaceSpec) {
|
||||
s.Nodes[3].Fields[0].Options = nil
|
||||
}, "缺 options"},
|
||||
{"steps 越界", func(s *SurfaceSpec) { s.Nodes[2].Steps.Current = 5 }, "current 在范围内"},
|
||||
{"空 title", func(s *SurfaceSpec) { s.Title = "" }, "title 必填"},
|
||||
{"空 nodes", func(s *SurfaceSpec) { s.Nodes = nil }, "nodes 需非空"},
|
||||
{"markdown 空", func(s *SurfaceSpec) {
|
||||
s.Nodes[0].Children[0].Markdown = ""
|
||||
}, "markdown 需要正文"},
|
||||
{"progress 越界", func(s *SurfaceSpec) {
|
||||
s.Nodes = append(s.Nodes, UINode{Type: UIProgress, Progress: &UIProgressSpec{Value: 1.5}})
|
||||
}, "progress.value"},
|
||||
{"未知来源", func(s *SurfaceSpec) {}, ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
s := validSpec()
|
||||
c.mut(&s)
|
||||
errs := ValidateSurfaceSpec(s, nil)
|
||||
if c.want == "" {
|
||||
continue
|
||||
}
|
||||
found := false
|
||||
for _, e := range errs {
|
||||
if strings.Contains(e, c.want) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("%s: 期望包含 %q,得到 %v", c.name, c.want, errs)
|
||||
}
|
||||
}
|
||||
// knownSources 非空时:未知来源拒绝、已知来源放行。
|
||||
s := validSpec()
|
||||
errs := ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true, "doc:eval/rc-01/vendor2": true})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("已知来源全引用不应违规: %v", errs)
|
||||
}
|
||||
s.Nodes[0].Children[0].Source = append(s.Nodes[0].Children[0].Source, "doc:eval/rc-99/ghost")
|
||||
errs = ValidateSurfaceSpec(s, map[string]bool{"doc:eval/rc-01/vendor1": true})
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("引用未知来源应拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateSurfaceSpecLimits(t *testing.T) {
|
||||
deep := UINode{Type: UISection, ID: "d0"}
|
||||
cur := &deep
|
||||
for i := 1; i <= SurfaceMaxDepth+1; i++ {
|
||||
cur.Children = []UINode{{Type: UISection, ID: "d" + string(rune('0'+i%10))}}
|
||||
cur = &cur.Children[0]
|
||||
}
|
||||
if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: []UINode{deep}}, nil); len(errs) == 0 {
|
||||
t.Fatal("超过嵌套深度应拒绝")
|
||||
}
|
||||
many := make([]UINode, SurfaceMaxNodes+1)
|
||||
for i := range many {
|
||||
many[i] = UINode{Type: UIMarkdown, Markdown: "x"}
|
||||
}
|
||||
if errs := ValidateSurfaceSpec(SurfaceSpec{Title: "t", Nodes: many}, nil); len(errs) == 0 {
|
||||
t.Fatal("超过节点数应拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUINodeBackwardCompat(t *testing.T) {
|
||||
// 旧卡片路径的 UINode JSON(无 id/source)解出后字段为零值,不受影响。
|
||||
var n UINode
|
||||
if err := json.Unmarshal([]byte(`{"type":"list","items":["a"]}`), &n); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n.ID != "" || n.Source != nil || n.Type != UIList || len(n.Items) != 1 {
|
||||
t.Fatalf("旧负载解引用异常: %+v", n)
|
||||
}
|
||||
// 新字段双向序列化。
|
||||
b, _ := json.Marshal(UINode{Type: UIMarkdown, ID: "m1", Source: []string{"s1"}, Markdown: "x"})
|
||||
if !strings.Contains(string(b), `"id":"m1"`) || !strings.Contains(string(b), `"source":["s1"]`) {
|
||||
t.Fatalf("新字段序列化缺失: %s", b)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DocumentOrigin struct {
|
||||
ArtifactID ID `json:"artifactId"`
|
||||
ContentHash string `json:"contentHash"`
|
||||
ParserRevision string `json:"parserRevision"`
|
||||
}
|
||||
type TaskPhase struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
ObservedAt time.Time `json:"observedAt,omitempty"`
|
||||
}
|
||||
type TaskSourceRef struct {
|
||||
ID string `json:"id"`
|
||||
ArtifactID string `json:"artifactId"`
|
||||
ContentHash string `json:"contentHash"`
|
||||
ParserRevision string `json:"parserRevision"`
|
||||
BlockIndex int `json:"blockIndex"`
|
||||
Label string `json:"label"`
|
||||
Quote string `json:"quote,omitempty"`
|
||||
Availability string `json:"availability,omitempty"`
|
||||
}
|
||||
type TaskResultRef struct {
|
||||
ArtifactID ID `json:"artifactId,omitempty"`
|
||||
SurfaceID ID `json:"surfaceId,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
ContentHash string `json:"contentHash,omitempty"`
|
||||
Availability string `json:"availability"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
type TaskFinding struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Impact string `json:"impact,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
Status string `json:"status"`
|
||||
Revision int64 `json:"revision"`
|
||||
Sources []TaskSourceRef `json:"sources"`
|
||||
Assumptions string `json:"assumptions,omitempty"`
|
||||
Corrects string `json:"corrects,omitempty"`
|
||||
}
|
||||
type TaskSnapshot struct {
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
TaskID ID `json:"taskId"`
|
||||
AttemptID ID `json:"attemptId"`
|
||||
CardID ID `json:"cardId"`
|
||||
ProjectID ID `json:"projectId"`
|
||||
ConversationID ID `json:"conversationId"`
|
||||
SourceMessageID ID `json:"sourceMessageId,omitempty"`
|
||||
ParentRunID ID `json:"parentRunId,omitempty"`
|
||||
RunID ID `json:"runId,omitempty"`
|
||||
OwnerInstanceID string `json:"ownerInstanceId"`
|
||||
ExecutionEpoch int64 `json:"executionEpoch,omitempty"`
|
||||
Revision int64 `json:"revision"`
|
||||
SourceSeq int64 `json:"sourceSeq"`
|
||||
StreamScope string `json:"streamScope"`
|
||||
InputRevision string `json:"inputRevision,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Goal string `json:"goal,omitempty"`
|
||||
ExecutionState string `json:"executionState"`
|
||||
OutcomeCompleteness string `json:"outcomeCompleteness"`
|
||||
DeliveryState string `json:"deliveryState"`
|
||||
Phase TaskPhase `json:"phase"`
|
||||
Focus string `json:"focus,omitempty"`
|
||||
Coverage string `json:"coverage,omitempty"`
|
||||
Remaining string `json:"remaining,omitempty"`
|
||||
NextStep string `json:"nextStep,omitempty"`
|
||||
Conclusion string `json:"conclusion,omitempty"`
|
||||
Findings []TaskFinding `json:"findings,omitempty"`
|
||||
Limitations []string `json:"limitations,omitempty"`
|
||||
Inputs []TaskSourceRef `json:"inputs,omitempty"`
|
||||
Actions []UIAction `json:"actions,omitempty"`
|
||||
ResultRefs []TaskResultRef `json:"resultRefs,omitempty"`
|
||||
LastActivityAt time.Time `json:"lastActivityAt,omitempty"`
|
||||
LastMeaningfulUpdateAt time.Time `json:"lastMeaningfulUpdateAt,omitempty"`
|
||||
CheckpointKeys map[string]string `json:"checkpointKeys,omitempty"`
|
||||
CheckpointCount int `json:"checkpointCount,omitempty"`
|
||||
CheckpointSeq int64 `json:"checkpointSeq,omitempty"`
|
||||
}
|
||||
type TaskSourceQuote struct {
|
||||
SourceID string `json:"sourceId"`
|
||||
BlockIndex int `json:"blockIndex"`
|
||||
Quote string `json:"quote"`
|
||||
}
|
||||
type WorkCheckpointCandidate struct {
|
||||
CheckpointID string `json:"checkpointId"`
|
||||
PhaseID string `json:"phaseId"`
|
||||
Focus string `json:"focus,omitempty"`
|
||||
Remaining string `json:"remaining,omitempty"`
|
||||
NextStep string `json:"nextStep,omitempty"`
|
||||
Findings []WorkFindingCandidate `json:"findings"`
|
||||
}
|
||||
type WorkFindingCandidate struct {
|
||||
FindingKey string `json:"findingKey"`
|
||||
Text string `json:"text"`
|
||||
Impact string `json:"impact,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
Assumptions string `json:"assumptions,omitempty"`
|
||||
Corrects string `json:"corrects,omitempty"`
|
||||
Sources []TaskSourceQuote `json:"sources"`
|
||||
Evidence *WorkFindingEvidence `json:"evidence,omitempty"`
|
||||
}
|
||||
|
||||
// WorkFindingEvidence is a model-supplied candidate, not a persisted proof.
|
||||
// Backend checks the quoted source text and computes any resulting statement.
|
||||
type WorkEvidencePremise struct {
|
||||
Role string `json:"role"`
|
||||
SourceIndex int `json:"sourceIndex"`
|
||||
Value *int `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type WorkFindingEvidence struct {
|
||||
Mode string `json:"mode"`
|
||||
Premises []WorkEvidencePremise `json:"premises"`
|
||||
Result *int `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
func validateWorkFindingEvidence(e *WorkFindingEvidence, sourceCount int) error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
var required []string
|
||||
switch e.Mode {
|
||||
case "direct_relation":
|
||||
required = []string{"direct_relation"}
|
||||
if e.Result != nil {
|
||||
return fmt.Errorf("checkpoint.evidence: direct relation cannot have result")
|
||||
}
|
||||
case "difference":
|
||||
required = []string{"total", "phase"}
|
||||
if e.Result == nil || *e.Result < 0 || *e.Result > 1000000 {
|
||||
return fmt.Errorf("checkpoint.evidence: difference requires bounded result")
|
||||
}
|
||||
case "unresolved_relation":
|
||||
required = []string{"total", "phase", "dependent"}
|
||||
if e.Result != nil {
|
||||
return fmt.Errorf("checkpoint.evidence: unresolved relation cannot have result")
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("checkpoint.evidence: unknown mode")
|
||||
}
|
||||
if len(e.Premises) != len(required) {
|
||||
return fmt.Errorf("checkpoint.evidence: wrong premise count")
|
||||
}
|
||||
roles := make(map[string]bool, len(required))
|
||||
for _, premise := range e.Premises {
|
||||
if roles[premise.Role] || premise.SourceIndex < 0 || premise.SourceIndex >= sourceCount {
|
||||
return fmt.Errorf("checkpoint.evidence: duplicate role or invalid source index")
|
||||
}
|
||||
roles[premise.Role] = true
|
||||
if e.Mode == "direct_relation" {
|
||||
if premise.Value != nil {
|
||||
return fmt.Errorf("checkpoint.evidence: direct relation cannot have value")
|
||||
}
|
||||
} else if premise.Value == nil || *premise.Value < 0 || *premise.Value > 1000000 {
|
||||
return fmt.Errorf("checkpoint.evidence: count premise requires bounded value")
|
||||
}
|
||||
}
|
||||
for _, role := range required {
|
||||
if !roles[role] {
|
||||
return fmt.Errorf("checkpoint.evidence: missing premise role")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate enforces wire limits; owner/source authorization belongs to Backend.
|
||||
func (s TaskSnapshot) Validate() error {
|
||||
if s.SchemaVersion != 1 {
|
||||
return fmt.Errorf("task.schema: unsupported")
|
||||
}
|
||||
if s.TaskID == "" || s.AttemptID == "" || s.CardID == "" || s.ProjectID == "" || s.ConversationID == "" || s.OwnerInstanceID == "" {
|
||||
return fmt.Errorf("task.scope: missing identity")
|
||||
}
|
||||
if s.StreamScope != "conversation:"+s.ConversationID {
|
||||
return fmt.Errorf("task.scope: inconsistent stream")
|
||||
}
|
||||
if s.Revision < 1 || s.Revision > 9007199254740991 || s.SourceSeq < 0 || s.SourceSeq > 9007199254740991 || s.ExecutionEpoch < 0 {
|
||||
return fmt.Errorf("task.revision: unsafe cursor")
|
||||
}
|
||||
switch s.ExecutionState {
|
||||
case "queued", "running", "awaiting_user", "completed", "failed", "interrupted":
|
||||
default:
|
||||
return fmt.Errorf("task.state: unknown")
|
||||
}
|
||||
if (s.ExecutionState == "running" || s.ExecutionState == "awaiting_user") && s.RunID == "" {
|
||||
return fmt.Errorf("task.run: active task requires run")
|
||||
}
|
||||
switch s.OutcomeCompleteness {
|
||||
case "none", "partial", "complete":
|
||||
default:
|
||||
return fmt.Errorf("task.outcome: unknown")
|
||||
}
|
||||
switch s.DeliveryState {
|
||||
case "pending", "available", "reported":
|
||||
default:
|
||||
return fmt.Errorf("task.delivery: unknown")
|
||||
}
|
||||
active := 0
|
||||
for _, f := range s.Findings {
|
||||
if f.ID == "" || f.Revision < 1 {
|
||||
return fmt.Errorf("task.finding: missing identity")
|
||||
}
|
||||
switch f.Kind {
|
||||
case "material_record", "analysis", "provisional", "needs_user", "verified":
|
||||
default:
|
||||
return fmt.Errorf("task.finding.kind: unknown")
|
||||
}
|
||||
switch f.Status {
|
||||
case "active":
|
||||
active++
|
||||
case "corrected", "withdrawn", "superseded":
|
||||
default:
|
||||
return fmt.Errorf("task.finding.status: unknown")
|
||||
}
|
||||
}
|
||||
if active > 12 {
|
||||
return fmt.Errorf("task.findings: too many active")
|
||||
}
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(b) > 64*1024 {
|
||||
return fmt.Errorf("task.size: exceeds 64KiB")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s TaskSnapshot) Terminal() bool {
|
||||
return s.ExecutionState == "completed" || s.ExecutionState == "failed" || s.ExecutionState == "interrupted"
|
||||
}
|
||||
func ValidateWorkCheckpoint(c WorkCheckpointCandidate) error {
|
||||
if strings.TrimSpace(c.CheckpointID) == "" || len(c.CheckpointID) > 128 {
|
||||
return fmt.Errorf("checkpoint.id: required or too long")
|
||||
}
|
||||
switch c.PhaseID {
|
||||
case "reading", "crosscheck", "summarizing", "draft-ready":
|
||||
default:
|
||||
return fmt.Errorf("checkpoint.phase: unknown")
|
||||
}
|
||||
if len(c.Findings) == 0 || len(c.Findings) > 6 {
|
||||
return fmt.Errorf("checkpoint.findings: require 1..6")
|
||||
}
|
||||
keys := map[string]bool{}
|
||||
for _, f := range c.Findings {
|
||||
switch f.Kind {
|
||||
case "material_record", "analysis", "provisional", "needs_user":
|
||||
default:
|
||||
return fmt.Errorf("checkpoint.kind: model cannot grant verified status")
|
||||
}
|
||||
if f.FindingKey == "" || keys[f.FindingKey] || strings.TrimSpace(f.Text) == "" {
|
||||
return fmt.Errorf("checkpoint.finding: empty or duplicate key")
|
||||
}
|
||||
keys[f.FindingKey] = true
|
||||
if len(f.Sources) == 0 {
|
||||
return fmt.Errorf("checkpoint.sources: evidence required")
|
||||
}
|
||||
for _, src := range f.Sources {
|
||||
if src.SourceID == "" || src.BlockIndex < 0 || strings.TrimSpace(src.Quote) == "" {
|
||||
return fmt.Errorf("checkpoint.source: invalid")
|
||||
}
|
||||
}
|
||||
if err := validateWorkFindingEvidence(f.Evidence, len(f.Sources)); err != nil {
|
||||
return err
|
||||
}
|
||||
if (f.Kind == "analysis" || f.Kind == "provisional") && strings.TrimSpace(f.Assumptions) == "" {
|
||||
return fmt.Errorf("checkpoint.assumptions: required")
|
||||
}
|
||||
}
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(b) > 16*1024 {
|
||||
return fmt.Errorf("checkpoint.size: exceeds 16KiB")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s TaskSnapshot) SummaryBody() []UINode {
|
||||
labels := map[string]string{"queued": "排队中", "running": "进行中", "awaiting_user": "需要你处理", "completed": "已完成", "failed": "执行失败", "interrupted": "已中断"}
|
||||
var lines []string
|
||||
for _, f := range s.Findings {
|
||||
if f.Status == "active" {
|
||||
line := f.Text
|
||||
if f.Impact != "" {
|
||||
line += ";" + f.Impact
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
note := strings.Join(lines, "\n")
|
||||
if note == "" {
|
||||
note = TaskSummaryNoFindingsYet
|
||||
if s.ExecutionState == "queued" {
|
||||
note = "尚未开始处理,等待执行资源"
|
||||
}
|
||||
if s.Terminal() {
|
||||
note = s.Conclusion
|
||||
if note == "" {
|
||||
note = "没有可用阶段成果"
|
||||
}
|
||||
}
|
||||
}
|
||||
v := TaskSummaryView{Focus: s.Focus, FindingsNote: note, Remaining: s.Remaining, NextStep: s.NextStep, OutcomeLine: s.Conclusion}
|
||||
var body []UINode
|
||||
if s.Terminal() {
|
||||
body = v.FinishedBody(labels[s.ExecutionState], nil)
|
||||
} else {
|
||||
body = v.RunningBody(labels[s.ExecutionState], nil)
|
||||
}
|
||||
if s.Coverage != "" {
|
||||
body = append(body, UINode{Type: UIKV, KV: [][2]string{{"提供范围", s.Coverage}}})
|
||||
}
|
||||
if len(s.Limitations) > 0 {
|
||||
body = append(body, UINode{Type: UIList, Title: "范围与限制", Items: s.Limitations})
|
||||
}
|
||||
for _, r := range s.ResultRefs {
|
||||
if r.ArtifactID != "" && r.Availability == "available" {
|
||||
body = append(body, UINode{Type: UIDocRef, DocID: r.ArtifactID, Title: r.Name})
|
||||
}
|
||||
}
|
||||
return body
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Loss of the optional task object makes refresh discard stage findings.
|
||||
func TestTaskSnapshotCardRoundTrip(t *testing.T) {
|
||||
raw := []byte(`{"id":"card1","projectId":"p1","conversationId":"c1","kind":"progress","status":"active","task":{"schemaVersion":1,"taskId":"t1","attemptId":"a1","cardId":"card1","revision":4,"executionState":"running","focus":"核对材料","findings":[{"id":"f1","text":"日期不同","kind":"material_record","status":"active","revision":1}]}}`)
|
||||
var card ActivityCard
|
||||
if err := json.Unmarshal(raw, &card); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
encoded, err := json.Marshal(card)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var restored map[string]json.RawMessage
|
||||
if err := json.Unmarshal(encoded, &restored); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := restored["task"]; !ok {
|
||||
t.Fatal("task findings were lost in card persistence")
|
||||
}
|
||||
var task map[string]json.RawMessage
|
||||
if err := json.Unmarshal(restored["task"], &task); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(task["revision"]) != "4" || string(task["focus"]) != `"核对材料"` {
|
||||
t.Fatal("revision/focus did not survive")
|
||||
}
|
||||
}
|
||||
|
||||
func validTaskSnapshot() TaskSnapshot {
|
||||
return TaskSnapshot{SchemaVersion: 1, TaskID: "t1", AttemptID: "a1", CardID: "card1", ProjectID: "p1", ConversationID: "c1", OwnerInstanceID: "i1", Revision: 1, ExecutionState: "queued", OutcomeCompleteness: "none", DeliveryState: "pending", StreamScope: "conversation:c1", Title: "整理材料"}
|
||||
}
|
||||
func TestTaskSnapshotRejectsUnsafeAndUnboundState(t *testing.T) {
|
||||
for _, mutate := range []func(*TaskSnapshot){
|
||||
func(s *TaskSnapshot) { s.TaskID = "" }, func(s *TaskSnapshot) { s.AttemptID = "" },
|
||||
func(s *TaskSnapshot) { s.Revision = 9007199254740992 }, func(s *TaskSnapshot) { s.SourceSeq = -1 },
|
||||
func(s *TaskSnapshot) { s.ExecutionState = "imagined" }, func(s *TaskSnapshot) { s.StreamScope = "conversation:other" },
|
||||
func(s *TaskSnapshot) { s.ExecutionState = "running" },
|
||||
} {
|
||||
s := validTaskSnapshot()
|
||||
mutate(&s)
|
||||
if s.Validate() == nil {
|
||||
t.Errorf("invalid snapshot accepted: %+v", s)
|
||||
}
|
||||
}
|
||||
if err := validTaskSnapshot().Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
func TestWorkCheckpointRejectsVerifiedAndEmptyEvidence(t *testing.T) {
|
||||
c := WorkCheckpointCandidate{CheckpointID: "cp1", PhaseID: "crosscheck", Findings: []WorkFindingCandidate{{FindingKey: "date", Text: "日期不同", Kind: "verified", Sources: []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}}}}}
|
||||
if err := ValidateWorkCheckpoint(c); err == nil || !strings.Contains(err.Error(), "kind") {
|
||||
t.Fatal("model granted verified status", err)
|
||||
}
|
||||
c.Findings[0].Kind = "material_record"
|
||||
c.Findings[0].Sources = nil
|
||||
if ValidateWorkCheckpoint(c) == nil {
|
||||
t.Fatal("source-free fact accepted")
|
||||
}
|
||||
c.Findings[0].Sources = []TaskSourceQuote{{SourceID: "src1", Quote: "六月"}}
|
||||
if err := ValidateWorkCheckpoint(c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Findings[0].Text = strings.Repeat("x", 17000)
|
||||
if ValidateWorkCheckpoint(c) == nil {
|
||||
t.Fatal("unbounded candidate accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkCheckpointEvidenceModesAndBounds(t *testing.T) {
|
||||
base := func() WorkCheckpointCandidate {
|
||||
return WorkCheckpointCandidate{CheckpointID: "cp-evidence", PhaseID: "crosscheck", Findings: []WorkFindingCandidate{{
|
||||
FindingKey: "phase", Text: "待核对", Kind: "needs_user",
|
||||
Sources: []TaskSourceQuote{{SourceID: "total", Quote: "总12项"}, {SourceID: "phase", Quote: "一期8项"}, {SourceID: "dependent", Quote: "其中3项依赖接口"}},
|
||||
}}}
|
||||
}
|
||||
n := func(v int) *int { return &v }
|
||||
total := WorkEvidencePremise{Role: "total", SourceIndex: 0, Value: n(12)}
|
||||
phase := WorkEvidencePremise{Role: "phase", SourceIndex: 1, Value: n(8)}
|
||||
dependent := WorkEvidencePremise{Role: "dependent", SourceIndex: 2, Value: n(3)}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
evidence WorkFindingEvidence
|
||||
valid bool
|
||||
}{
|
||||
{"direct", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0}}}, true},
|
||||
{"difference", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}, Result: n(4)}, true},
|
||||
{"unresolved", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase, dependent}}, true},
|
||||
{"unknown mode", WorkFindingEvidence{Mode: "prediction", Premises: []WorkEvidencePremise{total, phase}}, false},
|
||||
{"missing result", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}}, false},
|
||||
{"extra result", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase, dependent}, Result: n(3)}, false},
|
||||
{"duplicate role", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, total}, Result: n(4)}, false},
|
||||
{"missing premise", WorkFindingEvidence{Mode: "unresolved_relation", Premises: []WorkEvidencePremise{total, phase}}, false},
|
||||
{"negative index", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: -1}}}, false},
|
||||
{"foreign index", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 3}}}, false},
|
||||
{"negative value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(-1)}, phase}, Result: n(4)}, false},
|
||||
{"large value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0, Value: n(1000001)}, phase}, Result: n(4)}, false},
|
||||
{"direct value", WorkFindingEvidence{Mode: "direct_relation", Premises: []WorkEvidencePremise{{Role: "direct_relation", SourceIndex: 0, Value: n(1)}}}, false},
|
||||
{"missing value", WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{{Role: "total", SourceIndex: 0}, phase}, Result: n(4)}, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := base()
|
||||
c.Findings[0].Evidence = &tc.evidence
|
||||
err := ValidateWorkCheckpoint(c)
|
||||
if (err == nil) != tc.valid {
|
||||
t.Fatalf("valid=%v err=%v", tc.valid, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
c := base()
|
||||
c.Findings[0].Evidence = &WorkFindingEvidence{Mode: "difference", Premises: []WorkEvidencePremise{total, phase}, Result: n(4)}
|
||||
c.Findings[0].Text = strings.Repeat("字", 5500)
|
||||
if err := ValidateWorkCheckpoint(c); err == nil || !strings.Contains(err.Error(), "16KiB") {
|
||||
t.Fatal("evidence bypassed checkpoint size cap", err)
|
||||
}
|
||||
}
|
||||
func TestTaskSnapshotBodyPreservesLimitsAndHonestStates(t *testing.T) {
|
||||
s := validTaskSnapshot()
|
||||
s.Remaining = "图片内容未纳入"
|
||||
s.ExecutionState = "failed"
|
||||
s.Conclusion = "处理失败,已有发现可查看"
|
||||
s.Findings = []TaskFinding{{ID: "f1", Text: "金额缺失", Kind: "material_record", Status: "active", Revision: 1}}
|
||||
raw, _ := json.Marshal(s.SummaryBody())
|
||||
for _, want := range []string{"执行失败", "图片内容未纳入", "金额缺失", "处理失败"} {
|
||||
if !strings.Contains(string(raw), want) {
|
||||
t.Errorf("lost critical summary %s", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskSnapshotUnstartedAndFinishedDoNotClaimReading(t *testing.T) {
|
||||
for _, state := range []string{"queued", "completed", "interrupted"} {
|
||||
s := validTaskSnapshot()
|
||||
s.ExecutionState = state
|
||||
s.Conclusion = "处理已结束"
|
||||
b, _ := json.Marshal(s.SummaryBody())
|
||||
if strings.Contains(string(b), "正在读取") {
|
||||
t.Errorf("%s falsely claims active reading", state)
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package core
|
||||
|
||||
// TaskSummaryView 是 U1 写入 ActivityCard.Body 的权威语义草图。
|
||||
// 完整 TaskSnapshot(findings[]、revision、streamScope)留待 U2;本类型只冻结
|
||||
// 运行中/终态的精炼骨架字段与 Body KV 键名,供 Backend 生成、旧客户端回退阅读。
|
||||
type TaskSummaryView struct {
|
||||
Title string // 业务动宾短标题(通常已在 ActivityCard.Title)
|
||||
Focus string // 正在做什么,≤2 行
|
||||
FindingsNote string // U1:无成果时诚实占位;结构化 findings 属 U3
|
||||
Remaining string
|
||||
NextStep string
|
||||
OutcomeLine string // 完成后的结论一行;运行中可空
|
||||
}
|
||||
|
||||
// Body KV 键名(U1 冻结;用户默认卡身可见,勿改成内部材料键)。
|
||||
const (
|
||||
TaskSummaryKVStatus = "状态"
|
||||
TaskSummaryKVFocus = "正在做"
|
||||
TaskSummaryKVFindings = "当前发现"
|
||||
TaskSummaryKVRemaining = "剩余"
|
||||
TaskSummaryKVNextStep = "下一步"
|
||||
)
|
||||
|
||||
// 状态行用户文案(与执行态映射;排队/进行中/完成/失败)。
|
||||
const (
|
||||
TaskSummaryStatusQueued = "排队中"
|
||||
TaskSummaryStatusRunning = "进行中"
|
||||
TaskSummaryStatusDone = "已完成"
|
||||
TaskSummaryStatusFailed = "执行失败"
|
||||
)
|
||||
|
||||
// 无阶段成果时的诚实占位(R03-a);不得编造业务发现。
|
||||
const TaskSummaryNoFindingsYet = "尚无阶段结论,正在读取材料"
|
||||
|
||||
// RunningBody 生成运行中(含排队)进度卡 Body:Markdown 占位 + 固定骨架 KV。
|
||||
// extraKV(材料键等)保留在同一 UIKV 节点,供系统去重,不充当用户进度正文。
|
||||
func (v TaskSummaryView) RunningBody(statusLabel string, extraKV [][2]string) []UINode {
|
||||
focus := stringsOr(v.Focus, "处理已登记工作")
|
||||
findings := stringsOr(v.FindingsNote, TaskSummaryNoFindingsYet)
|
||||
remaining := stringsOr(v.Remaining, "待任务进展后更新")
|
||||
next := stringsOr(v.NextStep, "继续处理已登记工作")
|
||||
if statusLabel == "" {
|
||||
statusLabel = TaskSummaryStatusRunning
|
||||
}
|
||||
kv := append(append([][2]string(nil), extraKV...),
|
||||
[2]string{TaskSummaryKVStatus, statusLabel},
|
||||
[2]string{TaskSummaryKVFocus, focus},
|
||||
[2]string{TaskSummaryKVFindings, findings},
|
||||
[2]string{TaskSummaryKVRemaining, remaining},
|
||||
[2]string{TaskSummaryKVNextStep, next},
|
||||
)
|
||||
return []UINode{
|
||||
{Type: UIMarkdown, Markdown: findings},
|
||||
{Type: UIKV, KV: kv},
|
||||
}
|
||||
}
|
||||
|
||||
// FinishedBody 生成终态卡 Body:结论 Markdown + 骨架 KV(状态为已完成或执行失败)。
|
||||
func (v TaskSummaryView) FinishedBody(statusLabel string, extraKV [][2]string) []UINode {
|
||||
outcome := stringsOr(v.OutcomeLine, "(无结果摘要)")
|
||||
findings := stringsOr(v.FindingsNote, outcome)
|
||||
if statusLabel == "" {
|
||||
statusLabel = TaskSummaryStatusDone
|
||||
}
|
||||
kv := append(append([][2]string(nil), extraKV...),
|
||||
[2]string{TaskSummaryKVStatus, statusLabel},
|
||||
[2]string{TaskSummaryKVFocus, stringsOr(v.Focus, "")},
|
||||
[2]string{TaskSummaryKVFindings, findings},
|
||||
[2]string{TaskSummaryKVRemaining, stringsOr(v.Remaining, "")},
|
||||
[2]string{TaskSummaryKVNextStep, stringsOr(v.NextStep, "")},
|
||||
)
|
||||
// 去掉空值骨架行,避免完成卡出现空白「正在做」。
|
||||
compact := make([][2]string, 0, len(kv))
|
||||
for _, pair := range kv {
|
||||
if pair[1] == "" && (pair[0] == TaskSummaryKVFocus || pair[0] == TaskSummaryKVRemaining || pair[0] == TaskSummaryKVNextStep) {
|
||||
continue
|
||||
}
|
||||
compact = append(compact, pair)
|
||||
}
|
||||
return []UINode{
|
||||
{Type: UIMarkdown, Markdown: outcome},
|
||||
{Type: UIKV, KV: compact},
|
||||
}
|
||||
}
|
||||
|
||||
// CardDisplaysFailed 判断卡是否应按失败展示:含 CardFailed,以及旧客户端
|
||||
// done + danger 兼容映射(U1 出口白名单)。
|
||||
func CardDisplaysFailed(status CardStatus, tone string) bool {
|
||||
if status == CardFailed {
|
||||
return true
|
||||
}
|
||||
return status == CardDone && tone == "danger"
|
||||
}
|
||||
|
||||
func stringsOr(s, fallback string) string {
|
||||
if s == "" {
|
||||
return fallback
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTaskSummaryRunningBodyOmitsRawTaskAndUsesHonestPlaceholder(t *testing.T) {
|
||||
v := TaskSummaryView{Focus: "核对需求与会议决定"}
|
||||
body := v.RunningBody(TaskSummaryStatusQueued, [][2]string{{"材料键", "mk_x"}})
|
||||
if len(body) != 2 || body[0].Type != UIMarkdown || body[1].Type != UIKV {
|
||||
t.Fatalf("body shape = %+v", body)
|
||||
}
|
||||
if body[0].Markdown != TaskSummaryNoFindingsYet {
|
||||
t.Fatalf("markdown = %q, want honest placeholder", body[0].Markdown)
|
||||
}
|
||||
rawTask := "逐页解析上传材料并对照验收条件" + strings.Repeat("x", 200)
|
||||
if strings.Contains(body[0].Markdown, rawTask) {
|
||||
t.Fatal("running body must not embed raw task text")
|
||||
}
|
||||
got := map[string]string{}
|
||||
for _, kv := range body[1].KV {
|
||||
got[kv[0]] = kv[1]
|
||||
}
|
||||
for _, key := range []string{TaskSummaryKVStatus, TaskSummaryKVFocus, TaskSummaryKVFindings, TaskSummaryKVRemaining, TaskSummaryKVNextStep, "材料键"} {
|
||||
if _, ok := got[key]; !ok {
|
||||
t.Fatalf("missing kv %q in %v", key, got)
|
||||
}
|
||||
}
|
||||
if got[TaskSummaryKVStatus] != TaskSummaryStatusQueued {
|
||||
t.Fatalf("status = %q", got[TaskSummaryKVStatus])
|
||||
}
|
||||
if got[TaskSummaryKVFindings] != TaskSummaryNoFindingsYet {
|
||||
t.Fatalf("findings = %q", got[TaskSummaryKVFindings])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskSummaryFinishedBodyFailureLabel(t *testing.T) {
|
||||
v := TaskSummaryView{OutcomeLine: "执行失败:boom"}
|
||||
body := v.FinishedBody(TaskSummaryStatusFailed, nil)
|
||||
if body[0].Markdown != "执行失败:boom" {
|
||||
t.Fatalf("outcome = %q", body[0].Markdown)
|
||||
}
|
||||
found := false
|
||||
for _, kv := range body[1].KV {
|
||||
if kv[0] == TaskSummaryKVStatus {
|
||||
found = true
|
||||
if kv[1] != TaskSummaryStatusFailed {
|
||||
t.Fatalf("status = %q, want %q", kv[1], TaskSummaryStatusFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("missing status kv")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskSummaryViewJSONRoundTrip(t *testing.T) {
|
||||
in := TaskSummaryView{
|
||||
Title: "核对三份材料", Focus: "核对需求", FindingsNote: TaskSummaryNoFindingsYet,
|
||||
Remaining: "评审意见", NextStep: "整理冲突", OutcomeLine: "",
|
||||
}
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out TaskSummaryView
|
||||
if err := json.Unmarshal(b, &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out != in {
|
||||
t.Fatalf("round-trip = %+v, want %+v", out, in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardDisplaysFailed(t *testing.T) {
|
||||
if !CardDisplaysFailed(CardFailed, "danger") {
|
||||
t.Fatal("CardFailed should display failed")
|
||||
}
|
||||
if !CardDisplaysFailed(CardDone, "danger") {
|
||||
t.Fatal("legacy done+danger should display failed")
|
||||
}
|
||||
if CardDisplaysFailed(CardDone, "ok") {
|
||||
t.Fatal("done+ok must not display failed")
|
||||
}
|
||||
if CardDisplaysFailed(CardActive, "info") {
|
||||
t.Fatal("active must not display failed")
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"id": "agent.sub.explore",
|
||||
"layer": "agent",
|
||||
"version": 1,
|
||||
"title": "探索子代理",
|
||||
"description": "",
|
||||
"body": "只读检索。",
|
||||
"tools": [
|
||||
"knowledge_search",
|
||||
"web_search"
|
||||
],
|
||||
"model": "lightweight",
|
||||
"maxTurns": 8
|
||||
}
|
||||
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"id": "demo.brief",
|
||||
"title": "简报",
|
||||
"description": "按主题写一份简报",
|
||||
"intentId": "demo.brief",
|
||||
"maxRetries": 6,
|
||||
"steps": [
|
||||
{
|
||||
"id": "length",
|
||||
"title": "篇幅",
|
||||
"kind": "hitl",
|
||||
"hitl": {
|
||||
"question": "篇幅?",
|
||||
"kind": "choice",
|
||||
"choices": [
|
||||
{
|
||||
"id": "short",
|
||||
"label": "短",
|
||||
"description": "一页",
|
||||
"impact": "更快"
|
||||
}
|
||||
],
|
||||
"allowFreeInput": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "draft",
|
||||
"title": "起草",
|
||||
"kind": "prompt",
|
||||
"prompt": "写一份关于 {{slots.topic}} 的简报。用户说:{{input}}。上一步:{{steps.length.output}}",
|
||||
"agent": "research",
|
||||
"critic": {
|
||||
"criteria": "必须点明主题",
|
||||
"maxRetries": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "lookup",
|
||||
"title": "查时间",
|
||||
"kind": "tool",
|
||||
"tool": "time_now",
|
||||
"args": {
|
||||
"tz": "UTC"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "save",
|
||||
"title": "存稿",
|
||||
"kind": "artifact",
|
||||
"prompt": "{{steps.draft.output}}",
|
||||
"artifact": "brief.md"
|
||||
},
|
||||
{
|
||||
"id": "note",
|
||||
"title": "通知",
|
||||
"kind": "card",
|
||||
"card": {
|
||||
"kind": "result",
|
||||
"title": "简报已写好"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "run",
|
||||
"title": "脚本",
|
||||
"kind": "script",
|
||||
"script": "scripts/demo.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,16 +23,20 @@ type ToolDesc struct {
|
||||
// Hidden 预留能力形态:默认不对模型与 /api/tools 列表可见,须运行时开关
|
||||
// (如 runtime.fsTools)或管理动作开启后可用(提案 2026-09-30-file-preview-and-reserved-capabilities)。
|
||||
Hidden bool `json:"hidden,omitempty"`
|
||||
// ReadOnly、Edits、Risk 是工具注册时的权限声明。Risk 取 low、medium、high。
|
||||
ReadOnly bool `json:"readOnly,omitempty"`
|
||||
Edits bool `json:"edits,omitempty"`
|
||||
Risk string `json:"risk,omitempty"`
|
||||
}
|
||||
|
||||
// HTTPToolSpec:无代码自定义工具(4.8)。模板变量 {{param.x}} 从调用参数注入。
|
||||
type HTTPToolSpec struct {
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
BodyTemplate string `json:"bodyTemplate,omitempty"` // JSON 模板
|
||||
AuthRef string `json:"authRef,omitempty"` // 设置中的密钥引用(settings key)
|
||||
TimeoutMs int `json:"timeoutMs,omitempty"`
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers,omitempty"`
|
||||
BodyTemplate string `json:"bodyTemplate,omitempty"` // JSON 模板
|
||||
AuthRef string `json:"authRef,omitempty"` // 设置中的密钥引用(settings key)
|
||||
TimeoutMs int `json:"timeoutMs,omitempty"`
|
||||
// ResultPath 从响应 JSON 中裁剪(jq 点路径);空取整个响应。
|
||||
ResultPath string `json:"resultPath,omitempty"`
|
||||
}
|
||||
@@ -44,9 +48,9 @@ type ToolCallRequest struct {
|
||||
}
|
||||
|
||||
type ToolCallResult struct {
|
||||
CallID string `json:"callId"`
|
||||
Status string `json:"status"` // ok | error
|
||||
Output string `json:"output,omitempty"`
|
||||
Truncated bool `json:"truncated,omitempty"`
|
||||
Artifact *Artifact `json:"artifact,omitempty"` // 超长转工件
|
||||
CallID string `json:"callId"`
|
||||
Status string `json:"status"` // ok | error
|
||||
Output string `json:"output,omitempty"`
|
||||
Truncated bool `json:"truncated,omitempty"`
|
||||
Artifact *Artifact `json:"artifact,omitempty"` // 超长转工件
|
||||
}
|
||||
|
||||
+153
-7
@@ -15,6 +15,8 @@ export interface Conversation {
|
||||
id: ID; projectId: ID; owner?: ID; title: string; profileId?: ID;
|
||||
pinned?: boolean; archived?: boolean;
|
||||
allowCrossRead?: boolean; crossPrivate?: boolean;
|
||||
permissionMode?: 'default' | 'plan' | 'auto_edit' | '';
|
||||
sessionNote?: string;
|
||||
createdAt: string; updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -22,7 +24,7 @@ export type Role = 'user' | 'assistant' | 'system';
|
||||
|
||||
export type PartType =
|
||||
| 'text' | 'thinking' | 'tool_call' | 'tool_result' | 'artifact_ref'
|
||||
| 'hitl' | 'plan' | 'error' | 'card';
|
||||
| 'hitl' | 'plan' | 'error' | 'card' | 'check';
|
||||
|
||||
export interface Part {
|
||||
type: PartType;
|
||||
@@ -34,6 +36,7 @@ export interface Part {
|
||||
plan?: PlanSnapshot;
|
||||
card?: { kind: string; data: UINode[]; cardId?: ID }; // cardId 非空 = 收件箱卡片引用部件(submit_card)
|
||||
error?: { code: string; message: string };
|
||||
check?: { passed: boolean; reason?: string }; // 终轮自动检查,过程区一行
|
||||
process?: boolean; // run 终态后折叠为摘要行
|
||||
durationMs?: number; // thinking 部件的思考时长(毫秒);工具耗时在 toolCall.durationMs
|
||||
}
|
||||
@@ -41,17 +44,27 @@ export interface Part {
|
||||
export interface Message {
|
||||
id: ID; conversationId: ID; runId?: ID; role: Role;
|
||||
parts: Part[]; createdAt: string;
|
||||
excluded?: boolean;
|
||||
variantGroup?: string;
|
||||
variantIndex?: number;
|
||||
}
|
||||
|
||||
export type RunKind = 'conversation' | 'subagent' | 'tool';
|
||||
export type RunStatus =
|
||||
| 'pending' | 'running' | 'waiting_hitl' | 'completed'
|
||||
| 'failed' | 'cancelled' | 'interrupted';
|
||||
export type RunPhase = 'thinking' | 'acting' | 'responding' | 'done';
|
||||
export type RunPhase = 'thinking' | 'acting' | 'responding' | 'finalizing' | 'done';
|
||||
|
||||
export interface Usage {
|
||||
promptTokens: number; completionTokens: number; totalTokens: number;
|
||||
durationMs: number; toolCalls: number;
|
||||
model?: string;
|
||||
// N3 计费拆分(additive;旧记录缺省为 0):
|
||||
// Last* 是最近一次请求的用量,Cache* 是缓存命中累计。
|
||||
lastPromptTokens?: number;
|
||||
lastCompletionTokens?: number;
|
||||
cacheReadTokens?: number;
|
||||
cacheWriteTokens?: number;
|
||||
}
|
||||
|
||||
export interface Run {
|
||||
@@ -79,12 +92,14 @@ export interface TodoList {
|
||||
// ---- 流式事件 ----
|
||||
|
||||
export type StreamEventType =
|
||||
| 'message.delta' | 'thinking.delta' | 'tool.call' | 'tool.result'
|
||||
| 'run.phase' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update'
|
||||
| 'message.delta' | 'thinking.delta'
|
||||
| 'tool.call.delta' | 'tool.call' | 'tool.output' | 'tool.result'
|
||||
| 'run.phase' | 'run.check' | 'hitl.request' | 'hitl.resolved' | 'plan.update' | 'todo.update'
|
||||
| 'agent.spawn' | 'agent.progress' | 'agent.result'
|
||||
| 'artifact.created' | 'artifact.updated'
|
||||
| 'run.completed' | 'run.failed' | 'error'
|
||||
| 'card.created' | 'card.updated' | 'card.status' | 'card.resolved';
|
||||
| 'card.created' | 'card.updated' | 'card.status' | 'card.resolved'
|
||||
| 'surface.updated';
|
||||
|
||||
export interface StreamEvent<P = unknown> {
|
||||
seq: number; runId?: ID; conversationId: ID; ts: string; v: number;
|
||||
@@ -93,6 +108,9 @@ export interface StreamEvent<P = unknown> {
|
||||
|
||||
// payload 形状(按 type 判别;未知字段忽略)
|
||||
export interface MessageDeltaPayload { index: number; text: string }
|
||||
export interface ToolCallDeltaPayload { index: number; callId?: string; name?: string; delta: string }
|
||||
export interface ToolOutputPayload { callId: string; stream: 'stdout' | 'stderr'; text: string }
|
||||
export interface ToolResultPayload { callId: string; status: string; summary?: string; truncated?: boolean; durationMs?: number }
|
||||
export interface AgentSpawnPayload { agentRun: ID; name: string; task: string }
|
||||
export interface AgentProgressPayload { agentRun: ID; note?: string; phase?: RunPhase }
|
||||
export interface AgentResultPayload { agentRun: ID; status: RunStatus; summary?: string }
|
||||
@@ -102,11 +120,19 @@ export interface RunCompletedPayload {
|
||||
context?: ContextObservation
|
||||
}
|
||||
|
||||
interface ContextSection {
|
||||
name: string
|
||||
estTokens: number
|
||||
}
|
||||
|
||||
export interface ContextObservation {
|
||||
items: string[]
|
||||
truncations?: string[]
|
||||
estChars: number
|
||||
sections?: ContextSection[]
|
||||
prefixHash?: string
|
||||
}
|
||||
export interface RunCheckPayload { passed: boolean; reason?: string }
|
||||
export interface RunFailedPayload { code: string; message: string }
|
||||
|
||||
// ---- 收件箱卡片 ----
|
||||
@@ -123,8 +149,23 @@ export interface ActivityCard {
|
||||
id: ID; projectId: ID; conversationId?: ID;
|
||||
sourceRunId?: ID; anchorMessageId?: ID;
|
||||
kind: CardKind; title: string; status: CardStatus; tone?: string;
|
||||
task?: TaskSnapshot;
|
||||
body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[];
|
||||
pinned?: boolean; createdAt: string; updatedAt: string;
|
||||
pinned?: boolean; createdAt: string; updatedAt: string; work?: WorkCard;
|
||||
}
|
||||
|
||||
export interface WorkCard {
|
||||
result?: string; resultVersion: number; readVersion: number; completedAt?: string;
|
||||
decision?: WorkDecision;
|
||||
}
|
||||
export interface WorkDecision {
|
||||
runId?: ID;
|
||||
mode: 'single' | 'multiple'; state: 'open' | 'deferred' | 'dispatching' | 'submitted';
|
||||
options?: { id: string; label: string }[];
|
||||
selectedIds?: string[]; freeText?: string; respondedAt?: string;
|
||||
}
|
||||
export interface WorkCardAction {
|
||||
resultVersion: number; selectedIds?: string[]; freeText?: string;
|
||||
}
|
||||
|
||||
export interface CardPayload { card: ActivityCard }
|
||||
@@ -144,6 +185,7 @@ export interface HITLCard {
|
||||
choices?: HITLChoice[]; allowFreeInput: boolean; freeInputHint?: string;
|
||||
timeoutSec?: number | null; form?: UINode[]; createdAt: string; resolved: boolean;
|
||||
anchorMessageId?: ID; blocked?: boolean; blockReason?: string;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface HITLResponse {
|
||||
@@ -158,6 +200,9 @@ export type UINodeType =
|
||||
|
||||
export interface UINode {
|
||||
type: UINodeType;
|
||||
/** Surface 层节点级标注(Generative UI U2):整页唯一稳定 id 与事实来源标识,旧路径不填。 */
|
||||
id?: string;
|
||||
source?: string[];
|
||||
title?: string;
|
||||
items?: string[];
|
||||
kv?: [string, string][];
|
||||
@@ -209,6 +254,8 @@ export interface PromptModule {
|
||||
body: string; outputContract?: string;
|
||||
source?: 'builtin' | 'plugin' | 'user'; owner?: string;
|
||||
basedOnVersion?: number; drifted?: boolean; disabled?: boolean;
|
||||
/** 子代理 profile。只对 agent.sub.* 有意义;空字段表示沿用内置。 */
|
||||
tools?: string[]; model?: string; maxTurns?: number;
|
||||
}
|
||||
|
||||
export interface PromptBlock { moduleId: string; version: number; layer: string; body: string; }
|
||||
@@ -237,6 +284,8 @@ export interface SettingDesc {
|
||||
default?: unknown; options?: string[];
|
||||
effect: 'hot' | 'new-run' | 'restart';
|
||||
secret?: boolean;
|
||||
/** P2 受管:system = 系统受管项,仅系统管理面可写(用户/项目层拒绝)。 */
|
||||
scope?: 'system';
|
||||
}
|
||||
|
||||
// ---- 插件 ----
|
||||
@@ -259,6 +308,36 @@ export interface PluginPanel {
|
||||
id: string; title: string; icon?: string; body: UINode[]; refreshTool?: string;
|
||||
}
|
||||
|
||||
/** 模板占位符:{{input}}、{{slots.<key>}}、{{steps.<id>.output}}。 */
|
||||
export interface WorkflowDef {
|
||||
id: string; title: string; description?: string; intentId?: string;
|
||||
/** 整条工作流的 critic 重试上限。 */
|
||||
maxRetries?: number;
|
||||
steps: WorkflowStep[];
|
||||
}
|
||||
|
||||
export interface StepCritic {
|
||||
criteria: string;
|
||||
/** 本步重试上限;缺省 1 次。 */
|
||||
maxRetries?: number;
|
||||
}
|
||||
|
||||
export interface WorkflowStep {
|
||||
id: string; title: string;
|
||||
kind: 'prompt' | 'tool' | 'hitl' | 'artifact' | 'card' | 'script';
|
||||
prompt?: string;
|
||||
/** prompt 步骤的子代理 profile,缺省 general。 */
|
||||
agent?: string;
|
||||
tool?: string;
|
||||
args?: unknown;
|
||||
hitl?: { question: string; kind: HITLKind; choices?: HITLChoice[]; allowFreeInput: boolean; freeInputHint?: string };
|
||||
artifact?: string;
|
||||
card?: { kind: CardKind; title: string; tone?: string; body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[] };
|
||||
/** script 步骤在插件目录内的相对路径。 */
|
||||
script?: string;
|
||||
critic?: StepCritic;
|
||||
}
|
||||
|
||||
export interface PluginWorkspace {
|
||||
id: string; title: string; icon?: string; shortcut?: string;
|
||||
badgeTool?: string; body: UINode[]; refreshTool?: string;
|
||||
@@ -272,7 +351,7 @@ export interface PluginPageDecl {
|
||||
|
||||
export interface ParsedDocument {
|
||||
meta: {
|
||||
filename: string; mime?: string; parser: string; confidence?: number;
|
||||
filename: string; mime?: string; parser: string; confidence?: number; origin?: DocumentOrigin;
|
||||
crossCheck?: { with: string; diffBlocks: number; verdict: 'agree' | 'differ' };
|
||||
parsedAt: string;
|
||||
};
|
||||
@@ -293,3 +372,70 @@ export interface UploadInfo {
|
||||
artifactId: ID; filename: string; mime?: string; size: number;
|
||||
parsed?: ParsedDocument;
|
||||
}
|
||||
|
||||
// ---- Generative UI Surface(提案 U2/U4;与 Go surface.go 同步)----
|
||||
|
||||
export interface SurfaceCompat {
|
||||
schemaVersion: number;
|
||||
catalogVersion: string;
|
||||
pluginVersion?: string;
|
||||
}
|
||||
|
||||
export interface SurfaceIdentity {
|
||||
surfaceId: ID;
|
||||
owner: string;
|
||||
projectId?: ID;
|
||||
conversationId?: ID;
|
||||
sourceRunId?: ID;
|
||||
pluginId: string;
|
||||
}
|
||||
|
||||
export interface SurfaceSpec {
|
||||
title: string;
|
||||
nodes: UINode[];
|
||||
}
|
||||
|
||||
export interface SurfaceSnapshot {
|
||||
identity: SurfaceIdentity;
|
||||
compat: SurfaceCompat;
|
||||
revision: number;
|
||||
baseRevision?: number;
|
||||
contextVersion?: string;
|
||||
activationEpoch: number;
|
||||
spec: SurfaceSpec;
|
||||
semanticText?: string;
|
||||
}
|
||||
|
||||
export interface SurfaceMeta {
|
||||
surfaceId: ID;
|
||||
title: string;
|
||||
pluginId: string;
|
||||
revision: number;
|
||||
conversationId?: ID;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// Persistent work projection; host-owned identities and revisions.
|
||||
export interface DocumentOrigin { artifactId: ID; contentHash: string; parserRevision: string }
|
||||
export interface TaskPhase { id: string; label: string; observedAt?: string }
|
||||
export interface TaskSourceRef { id: string; artifactId: ID; contentHash: string; parserRevision: string; blockIndex: number; label: string; quote?: string; availability?: string }
|
||||
export interface TaskResultRef { artifactId?: ID; surfaceId?: ID; version?: string; contentHash?: string; availability: string; name?: string }
|
||||
export interface TaskFinding { id: string; text: string; impact?: string; kind: string; status: string; revision: number; sources: TaskSourceRef[]; assumptions?: string; corrects?: string }
|
||||
export interface TaskSnapshot {
|
||||
inputs?: TaskSourceRef[]
|
||||
limitations?: string[]
|
||||
schemaVersion: number; taskId: ID; attemptId: ID; cardId: ID; projectId: ID; conversationId: ID;
|
||||
sourceMessageId?: ID; parentRunId?: ID; runId?: ID; ownerInstanceId: string; executionEpoch?: number;
|
||||
revision: number; sourceSeq: number; streamScope: string; inputRevision?: string; title: string; goal?: string;
|
||||
executionState: 'queued'|'running'|'awaiting_user'|'completed'|'failed'|'interrupted'; outcomeCompleteness: 'none'|'partial'|'complete'; deliveryState: 'pending'|'available'|'reported'; phase: TaskPhase;
|
||||
focus?: string; coverage?: string; remaining?: string; nextStep?: string; conclusion?: string;
|
||||
findings?: TaskFinding[]; actions?: UIAction[]; resultRefs?: TaskResultRef[]; lastActivityAt?: string; lastMeaningfulUpdateAt?: string;
|
||||
checkpointKeys?: Record<string,string>; checkpointCount?: number; checkpointSeq?: number;
|
||||
}
|
||||
export interface TaskSourceQuote { sourceId: string; blockIndex: number; quote: string }
|
||||
interface WorkEvidencePremise { role: "total" | "phase" | "dependent" | "direct_relation"; sourceIndex: number; value?: number }
|
||||
interface WorkFindingEvidence { mode: "direct_relation" | "difference" | "unresolved_relation"; premises: WorkEvidencePremise[]; result?: number }
|
||||
export interface WorkFindingCandidate { findingKey: string; text: string; impact?: string; kind: string; assumptions?: string; corrects?: string; sources: TaskSourceQuote[]; evidence?: WorkFindingEvidence }
|
||||
export interface WorkCheckpointCandidate { checkpointId: string; phaseId: string; focus?: string; remaining?: string; nextStep?: string; findings: WorkFindingCandidate[] }
|
||||
|
||||
export interface UIAction { id: string; label: string; style?: string; command?: string }
|
||||
|
||||
@@ -28,7 +28,12 @@ const (
|
||||
)
|
||||
|
||||
type UINode struct {
|
||||
Type UINodeType `json:"type"`
|
||||
Type UINodeType `json:"type"`
|
||||
// ID / Source 是 Generative UI Surface 层的节点级标注(提案 U2):ID 整页
|
||||
// 唯一且稳定(交互与修订对位);Source 为支撑该节点事实内容的来源标识,
|
||||
// 授权校验在数据绑定层(U3)。均为可选:旧卡片/表单路径不填不受影响。
|
||||
ID string `json:"id,omitempty"`
|
||||
Source []string `json:"source,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Items []string `json:"items,omitempty"` // list
|
||||
KV [][2]string `json:"kv,omitempty"` // kv
|
||||
|
||||
Reference in New Issue
Block a user