Compare commits

...
Author SHA1 Message Date
weichen 6fe40b0342 Merge pull request 'Add bounded evidence to work checkpoint candidates' (#31) from feature/fast-slow-u2-u3 into main
governance / governance (push) Successful in 16s
2026-10-19 09:20:24 +00:00
weichen 0cf03446c4 Merge the fast-slow U2/U3 branch history after syncing with main
governance / governance (pull_request) Successful in 16s
2026-10-19 02:59:18 +00:00
agent-exec-transport cbd3d3e3d0 agent-exec transport 68889ca8d4b0eec4cfe89d0cc80b6516/5ca3b7798211 2026-10-19 02:59:03 +00:00
weichen 5cae3a6e30 Merge pull request 'Adopt the shared governance kit (development-governance-v2)' (#35) from feature/governance-adoption into main
governance / governance (push) Successful in 17s
2026-10-17 08:31:36 +00:00
weichen b240077300 Close and archive the governance adoption card
governance / governance (pull_request) Successful in 16s
Why: the adoption PR is reviewed and its candidate HEAD passed CI; the card closure is folded into the PR so no separate close PR is needed.
Evidence: CI GATE on 18f617d4e8cd63d172a5ab9519271831beb546db; independent Review REPLAY (cursor-agent-stable --mode ask 全新只读会话(fresh_session=true,session id not-observable;codex/pi 因 Token Plan 429 不可用)VERDICT: APPROVE,无阻塞项).
Impact: task card only (moved to .task/archive/).
Refs: https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Core/pulls/35
2026-10-17 08:25:12 +00:00
weichen 18f617d4e8 Record the adoption unit run and positive/negative evidence
governance / governance (pull_request) Successful in 23s
Why: Phase 6 requires one positive executor run and the negative cases (missing config, bad receipt, out-of-scope path, unknown side effect) before adoption becomes active.
Evidence: .task/evidence/governance-adoption/ (SELF): governance check, agent-exec validate and adoption cases; this commit passed the generated pre-commit hook (admit).
Impact: task card and evidence logs only.
Refs: .task/feature-governance-adoption.md
2026-10-17 07:15:37 +00:00
agent-exec-transport 1ada9955d5 agent-exec transport e64b5afee9fabb0925523057eb540b68/8399a44af60d 2026-10-17 06:55:22 +00:00
weichenandCursor a160b03c7b Record the adoption unit run and positive/negative evidence
Why: Phase 6 requires one positive executor run and the negative cases (missing config, bad receipt, out-of-scope path, unknown side effect) before adoption becomes active.
Evidence: .task/evidence/governance-adoption/ (SELF): governance check all ok; agent-exec validate ok; adoption cases ALL PASS. The generated pre-commit hook refused this commit because scripts/dm resolves the DM checkout through the hook's GIT_DIR; fixed in Development-Manager before this repository re-pins.
Impact: task card and evidence logs only.
Refs: .task/feature-governance-adoption.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 06:19:48 +00:00
agent-exec-transport 98cfc6408d agent-exec transport 9a7ec13541404ada96c0f8bfc8d77058/a09c2ed0bb4b 2026-10-17 05:55:46 +00:00
weichen dde4f359a8 Bootstrap AGENT-EXECUTION.md and the governance adoption card
Why: development-governance-v2 Phase 6 adopts the shared governance kit in every formal repository; the executor needs a valid AGENT-EXECUTION.md before any implementation unit can run.
Evidence: generated by dm-server governance init at Development-Manager 42e675fec8e0069646319fcd298e61d03d8cbfd5; dm-server agent-exec validate passes.
Impact: configuration and task card only; no code or CI change yet.
Refs: Agent-System-Dev docs/superpowers/plans/2026-10-16-development-governance-v2-master.md
2026-10-17 05:51:28 +00:00
weichen 35560aa5a3 Merge pull request 'Keep complete background results and optional decisions on work cards' (#34) from feature/workbench-flow-completion into main 2026-10-15 08:18:43 +00:00
weichen 1a5ef6b75c Anchor work decisions to a reserved follow-up run identity
Why: Reconcile interrupted dispatch without spending another model request.

Evidence: Core package tests pass locally; integration exercises acceptance and recovery.

Impact: Add one optional persisted run identifier.

Refs: feature/workbench-flow-completion
2026-10-15 06:53:28 +00:00
weichen fcfe8149bd Verify durable work results and legacy card compatibility
Why: Retain full results and exact answers across persistence and reload.

Evidence: go test ./... passed locally, including full-tail and legacy JSON coverage.

Impact: Add contract roundtrip tests and a guarded dispatching state.

Refs: feature/workbench-flow-completion
2026-10-15 06:49:33 +00:00
weichen 00f182611c Define durable nonblocking work card snapshots
Why: Keep background results and optional decisions outside the foreground conversation lock.

Evidence: Core package tests and combination checks are recorded in the Dev task.

Impact: Add an optional backward-compatible work card payload without a database migration.

Refs: feature/workbench-flow-completion
2026-10-15 06:46:23 +00:00
weichen b4743949e5 Keep U3 TypeScript evidence helpers local
Why: Generated mirrors gained unused named exports and tripped the frontend knip ratchet.

Evidence: Exact Dev combination failed at 53 unused exports over 52 baseline; Core Go tests passed after narrowing the TS export surface.

Impact: Preserve WorkFindingCandidate.evidence shape while avoiding unused exported helper names.

Refs: Dev task feature/fast-slow-u2-u3-plan; failed combo receipt 275bebe2
2026-10-13 16:48:01 +00:00
weichen c722c16626 Validate optional evidence in work checkpoint candidates
Why: U3 needs bounded, source-indexed premises before the host can judge numeric and membership claims.

Evidence: TestWorkCheckpointEvidenceModesAndBounds failed on missing types, then go test ./... -count=1 and git diff --check passed.

Impact: Extend only the candidate wire contract; persisted task snapshots and display kinds stay unchanged.

Refs: Dev task feature/fast-slow-u2-u3-plan; r0.3 evidence implementation plan Task 1
2026-10-13 16:03:33 +00:00
weichen c55ebbcb96 Merge remote-tracking branch 'origin/main' into feature/fast-slow-u2-u3 2026-10-13 10:19:06 +00:00
weichen 5773e74086 Merge pull request 'Add managed scope to authoritative SettingDesc TS mirror' (#32) from fix/settingdesc-managed-scope into main 2026-10-13 06:54:12 +00:00
weichen d6f62af0ed Add managed scope to authoritative SettingDesc TS mirror
Why: Dev 组合构建经 make contracts 从 Core ts/types.ts 重新生成 frontend/ui 的 contracts.ts——受管 scope 字段此前只改了 frontend 镜像,构建时被权威源覆盖丢失(TS2339)。
Evidence: go test ./... 全绿;生成链路验证随 Dev 组合 CI。
Impact: 权威 TS 类型加可选字段;Go 契约(SettingDesc.Scope)此前已随 Core #30 合入,两侧对齐。
2026-10-13 06:53:42 +00:00
weichen beafd7537a Merge scoped settings contracts from current main
Why: Preserve upstream settings authority and current component admission.

Evidence: Exact authoritative main object fetched before merge; composition replay follows.

Impact: Retain Task contracts alongside scoped settings.

Refs: Agent-System-Dev U2/U3 delivery
2026-10-13 05:28:02 +00:00
weichen 0f94e4b062 Merge pull request 'Add system-managed setting scope contract to SettingDesc' (#30) from feature/rdm-increment-settings-duties into main 2026-10-13 04:19:40 +00:00
weichen aced665ba6 Merge origin/main into feature/rdm-increment-settings-duties 2026-10-13 04:19:24 +00:00
weichen 9150e6f90b Add system-managed setting scope contract to SettingDesc
Why: P2 受管设置机制需要 schema 声明写入归属——system 受管项仅管理员可写,user/project 层拒绝写入且生效链忽略存量覆盖。
Evidence: go test ./... 全绿(ManagedPaths/Touches/Strip 含祖先标量覆盖与整棵子树剥离用例)。
Impact: 新增 SettingScope 常量、SettingDesc.Scope 字段与三个纯函数;空 Scope 保持既有行为,向后兼容。
Refs: modules/documents/docs/proposals/2026-10-13-rdm-settings-members-duties-perms-increment.md P2
2026-10-13 03:40:25 +00:00
weichen 6e385743e4 Retain coverage limits in legacy task summaries
Why: Older clients must see the same coverage limits as structured task cards.

Evidence: Core suite and composition checks follow this projection update.

Impact: Projects optional limitations into existing list nodes.

Refs: fast-slow-u2-u3 Task 6
2026-10-13 02:08:00 +00:00
weichen dbe1f544d7 Keep material coverage limits separate from outcome claims
Why: A completed run can still lack readable or fully provided source material.

Evidence: Composition validation follows the backend and UI consumers.

Impact: Adds optional host-owned limitations without changing candidate authority.

Refs: fast-slow-u2-u3 Tasks 4 through 6
2026-10-13 01:59:40 +00:00
weichen 124309b985 Retain trusted input bindings for task result reuse
Why: Completed tasks require current source authorization and content validation.

Evidence: Optional fields preserve existing contracts; composition verification follows.

Impact: Adds host-owned input source references with no new model authority.

Refs: fast-slow-u2-u3 Tasks 2 and 4
2026-10-13 01:35:53 +00:00
weichen 06f2723161 Add persistent work snapshots and checkpoint contracts
Why: preserve scoped work findings and truthful activity across reloads.

Evidence: task JSON loss and validation/state regressions RED to GREEN; complete Core suite.

Impact: optional card/document wire fields and bounded candidate validation; old Body remains supported.

Refs: fast-slow U2/U3 Task1, R03 R05 R09 R17
2026-10-13 01:04:56 +00:00
weichen 8e4f4e0382 Merge pull request 'Add U1 task summary body contract helpers' (#29) from feature/fast-slow-display-u1 into main
Reviewed-on: https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Core/pulls/29
2026-10-12 12:16:33 +00:00
weichenandCursor ba289e7b17 Add U1 task summary body contract helpers
Why: Freeze L1 skeleton KV keys and honest no-findings placeholder
so Backend can stop stuffing raw task text into progress cards.
Evidence: go test ./... -count=1
Impact: New TaskSummaryView helpers; findings[] still deferred to U2/U3.
Refs: fast-slow-channel-display-u1-plan W1

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 07:41:05 +00:00
weichen 89f3906c39 Merge pull request 'Add surface.updated to the StreamEventType union in the TS authority' (#28) from feature/generative-ui-u4-eventtype into main 2026-10-11 11:37:16 +00:00
weichen bbebd153f4 Merge remote-tracking branch 'origin/main' into feature/generative-ui-u4-eventtype 2026-10-11 11:37:11 +00:00
weichen a563d31b1e Add surface.updated to the StreamEventType union in the TS authority
Why: U4 frontend store switches on the event type; the TS union mirrors
the Go EvSurfaceUpdated constant (Core #25) and must include it or the
frontend typecheck rejects the case.

Evidence: go test unchanged (TS-only); frontend tsc in the Dev
workspace after mirror sync.

Impact: Type-only additive.

Refs: Core PR #25; Dev task feature/generative-ui-u4
2026-10-11 11:36:33 +00:00
weichen 39632c2e7a Merge pull request 'Add the Surface snapshot types to the TS contract authority' (#27) from feature/generative-ui-u4-ts into main 2026-10-11 11:32:55 +00:00
weichen 37b96fd94f Add the Surface snapshot types to the TS contract authority
Why: U4 needs the frontend to render saved surfaces; the Go contract
(Core #25) gains its TypeScript mirror in the single authority so make
contracts regenerates the frontend and UI copies.

Evidence: Mirror regeneration and combined make test in the Dev
workspace after re-pinning.

Impact: Type-only additive change.

Refs: Core PR #25; Dev task feature/generative-ui-u4
2026-10-11 11:32:43 +00:00
weichen 7a98a7e7fe Merge pull request 'Add the optional node id/source fields to the TS contract authority' (#26) from feature/generative-ui-u2-ts-nodeids into main 2026-10-11 08:44:13 +00:00
weichen 360a8a8946 Add the optional node id/source fields to the TS contract authority
Why: PR #25 added the fields to the Go UINode; ts/types.ts is the
single authority that make contracts mirrors into the frontend and UI
repos, so the fields must originate here or the sync reverts manual
mirror edits (CI build caught exactly that).

Evidence: Regenerated mirrors and combined make test run in the Dev
workspace after re-pinning.

Impact: Type-only additive change.

Refs: Core PR #25; Dev task feature/generative-ui-u2
2026-10-11 08:44:00 +00:00
weichen 452daf3ffa Merge pull request 'Add the Surface contract types, node ids, and spec validation' (#25) from feature/generative-ui-u2-surface into main 2026-10-11 08:15:26 +00:00
weichen 626f8e1062 Add the Surface contract types, node ids, and spec validation
Why: The Generative UI proposal U2 needs a versioned, host-validated
envelope for agent-authored pages before any backend surface exists:
identity/compat/version envelopes, stable per-node ids with source
annotations, catalog-driven spec validation, and the surface.updated
projection event.

Evidence: go vet + go test in this repo (table tests cover a realistic
valid spec from the U1 trial shape, violations, limits, and old-payload
backward compatibility).

Impact: Additive only - UINode gains optional id/source (omitempty),
old card/form paths unaffected; no existing symbol changes.

Refs: Documents proposal 2026-10-10-generative-ui-plugin.md (r2, U2/U1
sections); Dev task feature/generative-ui-u2
2026-10-11 08:15:10 +00:00
weichen a253a40b8e Merge pull request 'Add the usage split fields to the TypeScript contract authority (N3 WP6)' (#24) from feature/n3-usage-fields-ts into main
Add the usage split fields to the TypeScript contract authority (N3 WP6)
2026-10-07 17:41:26 +00:00
weichen b8913dca6a Add the usage split fields to the TypeScript contract authority (N3 WP6)
Why: frontend and ui contracts.ts are generated from this file by
make contracts; editing the generated copies directly gets reverted by
the next codegen run (the Dev CI build step proves it regenerates).
Evidence: GOWORK=off go test green; the shape matches the Go Usage
fields added earlier.
Impact: additive to the contract mirror.
Refs: Agent-System-Core#2, Agent-System-Frontend#48
2026-10-07 17:41:21 +00:00
weichen 1b7e2c0f47 Merge pull request 'Add last-request and cache fields to Usage (N3 WP6)' (#23) from feature/n3-usage-fields into main
Add last-request and cache fields to Usage (N3 WP6)
2026-10-07 17:04:22 +00:00
weichen 8a3ff92518 Add last-request and cache fields to Usage (N3 WP6)
Why: the loop now emits Last* and cache token usage; the product run
record needs the same fields for display and accounting (additive JSON,
old records deserialize as zero values).
Evidence: GOWORK=off go build + go test green.
Impact: additive only.
Refs: Agent-Native#35, N3 plan WP6
2026-10-07 16:34:27 +00:00
weichen f4798ac852 Merge pull request 'Document executable Core verification commands' (#22) from feature/governance-docs into main 2026-10-06 03:09:54 +00:00
weichen cf2615def6 Document executable Core verification commands
Why: project facts pointed to a nonexistent Makefile and retained obsolete skeleton status.

Evidence: SELF go test ./... and git diff --check completed in this component worktree.

Impact: documentation only; does not advance the Dev Core gitlink or change public contracts.

Refs: Agent-System-Dev feature/governance-reliability audit remediation
2026-10-05 12:29:42 +00:00
weichen b12f9e29e6 Extend messages and context observation for variants
Merge pull request 'Extend messages and context observation for variants' (#21) from feature/sillytavern-review into main
2026-10-04 19:44:55 +00:00
weichen 4eae71864c Merge remote-tracking branch 'origin/main' into feature/sillytavern-review 2026-10-04 19:42:53 +00:00
weichenandCursor 0b5c7747a3 Keep the context section type inside the contract file
Why: A separately exported ContextSection is unused inside the UI package and pushes the knip ratchet over its baseline.
Evidence: UI knip reported 56 unused exports against baseline 55. The section shape stays on ContextObservation.sections.
Impact: Generated frontend and UI contracts drop the extra export. JSON field names are unchanged.
Refs: .task/feature-sillytavern-review.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 18:36:08 +00:00
weichen bd632236c2 Extend messages and context observation for variants and estimates
Why: Labeled secrets, excluded turns, and candidate replies need fields on the shared message contract, and context snapshots need a section breakdown plus a stable prefix hash.
Evidence: go test ./... in agent-system-core. New fields use omitempty, so existing golden JSON stays the same.
Impact: Clients that ignore unknown fields keep working. Assemblers can now mark a message excluded or as one candidate in a group.
Refs: docs/records/2026-10-04-sillytavern-review.md
2026-10-04 18:18:57 +00:00
weichen a7db6a4d0f Merge pull request 'Record the model that served a run's usage' (#20) from feature/durable-rewind into main 2026-10-03 17:40:15 +00:00
weichenandCursor 5dc190ee63 Record the model that served a run's usage
Why: a fallback chain can answer with a different model than the one that was requested, and usage has to name the model that actually served.
Evidence: go test .
Impact: the field is omitted when empty, so existing usage JSON stays the same.
Refs: feature/durable-rewind
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 17:28:40 +00:00
weichen b7e5baf15c Merge pull request 'Add session permission mode and confirmation detail' (#19) from feature/permission-hooks into main 2026-10-03 17:06:48 +00:00
weichenandCursor cf7df54874 Add session permission mode and confirmation detail
Why: The product needs a session mode and a place to show tool arguments on a confirmation card without inventing a second card type.
Evidence: GOWORK=off go test . passed. New fields use omitempty, so existing golden JSON stays the same.
Impact: Empty permission mode still means the profile policy. Clients that ignore unknown fields keep working.
Refs: feature/permission-hooks
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 17:02:28 +00:00
weichen 88e481afcd Merge pull request 'Add a finalizing phase and a check part' (#18) from feature/final-reply-compose into main 2026-10-02 14:15:05 +00:00
weichenandCursor 7c66075ee9 Add a finalizing phase and a check part
Why: The final reply is composed after the work loop, and the automatic end check needs its own part and event so the timeline can show it without mixing it into the user-facing text.
Evidence: go test ./...
Impact: New RunPhase finalizing, PartType check, and run.check. Existing payloads are unchanged.
Refs: feature/final-reply-compose
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-02 13:32:32 +00:00
weichen 3f7067fb2d Merge pull request 'Add streaming events for in-flight tool calls' (#17) from feature/stream-display-overhaul into main 2026-10-02 05:04:38 +00:00
weichenandCursor f08a4a90c6 Add streaming events for in-flight tool calls
Why: The client only learned about a tool call once execution started, so long argument generation looked like a frozen cursor.
Evidence: go test ./...
Impact: New tool.call.delta and tool.output events; tool.result may include durationMs. Unknown event types stay ignored.
Refs: feature/stream-display-overhaul
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-02 02:11:42 +00:00
weichen 4d1123e45c Merge pull request 'Add script steps and critic fields to workflow definitions' (#16) from feature/agent-harness-p4 into main
Merge pull request #16
2026-10-02 01:03:55 +00:00
weichenandCursor f93f473a0e Add script steps and critic fields to workflow definitions
Why: The workflow executor needs a shared contract for subagent profiles, plugin scripts, and per-step plus whole-run retry caps.
Evidence: go test ./... passed, including the new workflow_def golden.
Impact: New fields are omitempty, so existing workflow JSON still decodes. Kind script is additive.
Refs: feature/agent-harness-p4
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 15:55:52 +00:00
weichen a8e81e078d Merge pull request 'Add optional tools, model, and maxTurns to prompt modules' (#15) from feature/agent-harness-p3b into main 2026-10-01 15:51:54 +00:00
weichenandCursor 9b8cf79c96 Add optional tools, model, and maxTurns to prompt modules
Why: User overrides of a subagent profile need a place on the shared contract, and empty fields must stay omitted so older modules keep their meaning.
Evidence: go test ./... passed, including the new prompt_module_subagent golden.
Impact: Existing modules serialize the same. Frontend and UI contracts still need a copy of the type.
Refs: feature/agent-harness-p3b
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 15:21:47 +00:00
weichen 493a846f07 Accept plugin manifest apiVersion 2.0-experimental
Why: Downstream repositories need a versioned public manifest for skills, MCP, knowledge, parsers, agents, channels, and pages without forking the base.

Evidence: go test ./... passed after omitzero on empty scope and backend so the golden plugin_manifest stays stable.

Impact: apiVersion 1.0 remains accepted. New provides fields are optional. Merge without squash so Backend can pin this commit.

Refs: Agent-System-Dev feature/base-extension-roadmap
2026-10-01 10:47:10 +00:00
weichen 447ea5c5ae Merge remote-tracking branch 'origin/main' into feature/base-extension-roadmap 2026-10-01 10:46:55 +00:00
weichenandCursor fdc0c1b933 Accept plugin manifest apiVersion 2.0-experimental
Why: Downstream repositories need a versioned public manifest for skills, MCP, knowledge, parsers, agents, channels, and pages without forking the base.
Evidence: go test ./... passed after omitzero on empty scope and backend so the golden plugin_manifest stays stable.
Impact: apiVersion 1.0 remains accepted. New provides fields are optional. Scope and backend are omitted when zero.
Refs: docs/proposals/2026-10-01-plugin-contract-v2.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 10:34:29 +00:00
weichen b80407a9a6 Merge pull request 'Add thinking duration to message parts' (#13) from feature/chat-timeline-preview into main
Merged by the timeline task after the owner asked to land the open pull requests.
2026-10-01 07:38:46 +00:00
weichenandCursor d4375300cd Add thinking duration to message parts
Why: The chat timeline needs a persisted thinking duration, and tool duration already lives on the tool call, so the thinking part had nowhere to put it.
Evidence: go test ./... passes; the message golden now includes durationMs on the thinking part.
Impact: Additive JSON field with omitempty. Existing clients ignore unknown fields.
Refs: feature/chat-timeline-preview
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 06:03:19 +00:00
weichen 1b04a34bbc Merge pull request 'Extract project channel key helpers (CF3 review fix)' (#12) from feature/project-inbox-r1 into main 2026-09-30 19:10:08 +00:00
weichen c8266fef42 Merge main into feature/project-inbox-r1 (sync before merge per branch protection) 2026-09-30 19:10:04 +00:00
weichen ed579bf29c Extract project channel key helpers (CF3 review fix)
Why: project channel convention (pseudo-key project:<projectId>) was
duplicated across runmgr/server/store; centralize so replay envelopes
match live stamping.
Evidence: go test green.
Impact: no behavior change; enables store replay stamping.
2026-09-30 19:07:06 +00:00
weichen f8b77c0d02 Merge pull request 'Add ProjectID to stream event envelope (CF3 project channel)' (#11) from feature/project-inbox-r1 into main 2026-09-30 18:56:52 +00:00
weichen 0b3392acea Add ProjectID to stream event envelope for project channel (CF3)
Why: D50 approved — the project inbox needs its own SSE stream reusing the
conversation event mechanism via the pseudo-key project:<projectId>; the
envelope stamps project ownership (additive field).
Evidence: conformance golden event_project_stream; go test green.
Impact: additive only (omitempty); consumers ignore unknown fields.
Refs: documents proposal 2026-09-30-card-stream-artifact-unify-project-inbox.md (D50).
2026-09-30 18:56:47 +00:00
weichen 90b57477bb Merge pull request 'Add Hidden to ToolDesc for reserved tool capabilities' (#10) from feature/file-preview-caps into main 2026-09-30 17:20:20 +00:00
weichen 6bdc4f2ada Add Hidden to ToolDesc for reserved tool capabilities
Why: the fs tool family (proposal 2026-09-30-file-preview-and-reserved-
capabilities) is implemented but must stay invisible to models and tool
listings until a runtime gate opens it.

Evidence: new bool field with json omitempty; no golden samples carry it;
go test ./... green.

Impact: registries can mark gated tools; servers and runners filter on
it (backend PR in the same task).

Refs: task feature/file-preview-caps.
2026-09-30 17:13:15 +00:00
weichen cafd9a54e5 Merge pull request 'Add CardID to CardPart for inbox-reference card parts' (#8) from feature/cardflow-r1 into main 2026-09-30 17:10:52 +00:00
weichen a64fb5a168 Add CardID to CardPart for inbox-reference card parts
Why: 卡流机制(CF1)需要消息部件正式引用收件箱 ActivityCard——submit_card 产卡时卡片为产物权威载体,消息只存引用+锚点渲染(原仅能从 data 字符串里正则抠 card:<id>)。
Evidence: go test ./... 通过;golden message.json 增 card 引用部件锁定 cardId 线格式(-update 重生成后逐字节一致)。
Impact: CardPart 增可选 cardId(omitempty,旧数据零影响);ts/types.ts 同步,镜像经 make contracts 再生成。
Refs: Dev 任务卡 feature/cardflow-r1;提案 cardflow-and-process-split(编制中)。
2026-09-30 16:10:20 +00:00
weichen c9aff06ed5 Merge pull request 'Record Gitea access facts in AGENTS.md' (#7) from feature/gitea-access-facts into main 2026-09-30 16:08:25 +00:00
weichen e6706450e4 Record Gitea access facts in AGENTS.md
Why: agents rediscovered Gitea hosts and credential locations from
scratch each session, and the retired git-panel host kept failing with
TLS broken-pipe errors before the right host was recalled.

Evidence: curl git-home...:30319/api/v1/version with token from
~/.secrets/gitea -> {"version":"1.27.3"}; ssh -p 31411 git@git-ssh...
(BatchMode) -> "Hi there, weichen!" with key ~/.ssh/home_gitea (ED25519,
SHA256:GYXaOXIMeloQ1fKSfdJ/ndgPQzs/Ix7Hk62ekfkfQ20). Same section already
merged in Agent-System-Dev (PR #73).

Impact: AGENTS.md only; records hosts and credential locations, no
secret values (baseline hard boundary 2).

Refs: Agent-System-Dev PR #73
2026-09-30 16:06:15 +00:00
weichen fd441794b1 Merge pull request 'Add cross-session flags to the Conversation contract' (#6) from feature/cross-session-reading into main 2026-09-30 15:00:44 +00:00
weichen 284e8f365f Add cross-session flags to the Conversation contract
Why: cross-session transcript reading needs per-conversation outbound
(allowCrossRead, default true) and inbound (crossPrivate, default false)
flags alongside the project-level gate; see the approved proposal.

Evidence: go test ./... passes with golden conversation.json regenerated
to exercise both wire fields.

Impact: additive contract fields (omitempty), wire-compatible for
existing clients that ignore unknown fields.

Refs: Agent-System-Documents proposals/2026-09-30-cross-session-reading.md
(Dev task feature/cross-session-reading).
2026-09-30 15:00:22 +00:00
weichen 10a84345b7 Merge pull request 'Add the avatar vocabulary to UINode (UI capability platform)' (#5) from feature/ui-capability-platform into main 2026-09-30 14:22:27 +00:00
47 changed files with 2672 additions and 206 deletions
+150
View File
@@ -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@git-ssh.u1680832.nyat.app:31411/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 .
+6
View File
@@ -0,0 +1,6 @@
[submodule "deps/development-guidelines"]
path = deps/development-guidelines
url = ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Development-Guidelines.git
[submodule "deps/development-manager"]
path = deps/development-manager
url = ssh://git@git-ssh.u1680832.nyat.app:31411/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
+66
View File
@@ -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;放弃路径注明原因>。
+67
View File
@@ -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"
```
+35
View File
@@ -17,3 +17,38 @@
- 模块边界、公开契约与隔离档位见 `policy/modules/`(如有)与 PROJECT.md。
- 跨仓 gitlink 变更须经组合验证后在 Dev 仓推进;受保护分支经 PR 合入。
- 验证命令见 PROJECT.md「验证」节;完成声明须附实际命令输出证据。
## Gitea 与 Git 访问事实(2026-09-30 实测)
历史文档与旧链接里的 `git-panel.u1680832.nyat.app:30319` 已失效(TLS 握手失败),
见到一律按过期处理,不要再试。当前权威通道:
- **Web / API(HTTPS)**:`https://git-home.u1680832.nyat.app:30319`(Gitea 1.27.3,
API 前缀 `/api/v1`;偶发 broken pipe / 超时,重试即可)。
- **Git over SSH**:`ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/<Repo>.git`
(各仓 origin 与 gitlink 均为此格式)。
- **API token**:`~/.secrets/gitea`,两行键值 `URL_PREFIX=…` 与
`ACCESS_TOKEN=<40 位 hex>`;取值 `grep ACCESS_TOKEN ~/.secrets/gitea | cut -d= -f2-`,
用作 `Authorization: token <值>` 请求头。**token 值不入仓、不回显、不写进产出物**
(baseline 硬边界 2:只记录来源、字段与可用状态)。
- **SSH key**:`~/.ssh/home_gitea`(ED25519,公钥指纹
`SHA256:GYXaOXIMeloQ1fKSfdJ/ndgPQzs/Ix7Hk62ekfkfQ20`,Gitea 侧密钥名 GMKtec);
`~/.ssh/config` 已有 `git-ssh.u1680832.nyat.app` 主机条目,克隆/推送直接可用。
## 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
View File
@@ -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
View File
@@ -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
+46
View File
@@ -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)
}
}
+38 -3
View File
@@ -42,11 +42,12 @@ func sampleMessage() Message {
ID: "msg_abc123def456", ConversationID: "cnv_abc123def456", RunID: "run_abc123def456",
Role: RoleAssistant,
Parts: []Part{
{Type: PartThinking, Text: "考虑先检索…", Process: true},
{Type: PartThinking, Text: "考虑先检索…", Process: true, DurationMs: 1500},
{Type: PartToolCall, Process: true, ToolCall: &ToolCallPart{ID: "call_1", Name: "history_search", Arguments: `{"q":"部署"}`, Duration: 12.5}},
{Type: PartToolResult, Process: true, ToolResult: &ToolResultPart{CallID: "call_1", Status: "ok", Summary: "3 条命中"}},
{Type: PartText, Text: "根据历史记录……"},
{Type: PartArtifactRef, Artifact: &ArtifactRef{ArtifactID: "art_abc123def456", Name: "报告.md", Kind: "document"}},
{Type: PartCard, Card: &CardPart{Kind: "result", Data: []UINode{{Type: "markdown", Markdown: "全文见卡片"}}, CardID: "card_abc123def456"}},
},
}
}
@@ -59,11 +60,19 @@ 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())
golden(t, "project", Project{ID: "prj_abc123def456", Name: "示例项目"})
golden(t, "conversation", Conversation{ID: "cnv_abc123def456", ProjectID: "prj_abc123def456", Title: "新会话"})
golden(t, "conversation", Conversation{ID: "cnv_abc123def456", ProjectID: "prj_abc123def456", Title: "新会话", AllowCrossRead: true, CrossPrivate: true})
golden(t, "artifact", Artifact{ID: "art_abc123def456", ProjectID: "prj_abc123def456", Name: "报告.md", Kind: "document", MIME: "text/markdown", Size: 128})
}
@@ -124,6 +133,12 @@ func TestGoldenCardEvents(t *testing.T) {
Payload: json.RawMessage(`{"cardId":"card_abc123def456","actionId":"escalate"}`)})
}
func TestGoldenProjectStreamEvent(t *testing.T) {
// 项目流(D50):伪会话键 "project:<projectId>" 复用会话流机制;信封带 ProjectID。
golden(t, "event_project_stream", StreamEvent{Seq: 47, ConversationID: "project:prj_abc123def456", ProjectID: "prj_abc123def456", TS: "2026-09-30T11:00:00Z", V: 1, Type: EvCardCreated,
Payload: json.RawMessage(`{"card":{"id":"card_def456abc789","projectId":"prj_abc123def456","kind":"result","title":"项目级卡","status":"done","createdAt":"2026-09-30T11:00:00Z","updatedAt":"2026-09-30T11:00:00Z"}}`)})
}
func TestGoldenUIVocabularies(t *testing.T) {
golden(t, "ui_avatar", []UINode{
{Type: UIAvatar, Avatar: &UIAvatarSpec{Name: "weichen", Sub: "OWNER"}},
@@ -176,7 +191,7 @@ func TestGoldenProfileAndTools(t *testing.T) {
golden(t, "tool_http", ToolDesc{
Name: "http_translate", Description: "调用翻译 API", Source: SourceUser,
Parameters: json.RawMessage(`{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}`),
HTTP: &HTTPToolSpec{Method: "POST", URL: "https://api.example.com/translate", BodyTemplate: `{"q":"{{param.text}}"}`, TimeoutMs: 10000, ResultPath: "data.0"},
HTTP: &HTTPToolSpec{Method: "POST", URL: "https://api.example.com/translate", BodyTemplate: `{"q":"{{param.text}}"}`, TimeoutMs: 10000, ResultPath: "data.0"},
})
}
@@ -192,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,
+47 -46
View File
@@ -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"`
}
Vendored Submodule
+1
+70 -13
View File
@@ -1,14 +1,20 @@
package core
import "encoding/json"
import (
"encoding/json"
"strings"
)
// 事件信封:后端 → 前端的唯一增量通道(SSE),持久化后转发,Seq 在会话内单调。
// 消费方对未知 Type 静默忽略(原则 6 前向兼容)。
// 项目流(D50):项目频道事件以伪会话键 "project:<projectId>" 复用同一机制
// (独立 seq、持久化、断线重放),信封以 ProjectID 标识归属(会话流事件为空)。
type StreamEvent struct {
Seq int64 `json:"seq"`
RunID ID `json:"runId,omitempty"`
ConversationID ID `json:"conversationId"`
TS string `json:"ts"` // RFC3339
ProjectID ID `json:"projectID,omitempty"` // 项目流事件的项目归属(D50;只增不删)
TS string `json:"ts"` // RFC3339
V int `json:"v"`
Type StreamEventType `json:"type"`
Payload json.RawMessage `json:"payload,omitempty"`
@@ -16,12 +22,28 @@ type StreamEvent struct {
type StreamEventType string
// 项目流频道约定(D50):项目频道以伪会话键 "project:<projectId>" 复用会话
// 事件的持久化/seq/重放机制;信封 ProjectID 由该键派生(发布与重放同源)。
const projectChannelPrefix = "project:"
// ProjectChannelKey 返回项目流的事件频道键。
func ProjectChannelKey(projectID ID) ID { return ID(projectChannelPrefix + string(projectID)) }
// ProjectFromChannel 反解项目流频道键;非项目流频道返回 false。
func ProjectFromChannel(key ID) (ID, bool) {
pid, ok := strings.CutPrefix(string(key), projectChannelPrefix)
return ID(pid), ok && pid != ""
}
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"
@@ -39,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 词汇的当前版本。
@@ -63,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"`
@@ -96,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"`
}
@@ -119,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 {
+2
View File
@@ -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
+99 -64
View File
@@ -17,24 +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"` // 归档:列表默认隐藏,数据保留
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"`
}
// ---- 消息与部件 ----
@@ -59,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 决定哪些字段有意义;未用字段为零值。
@@ -82,25 +94,30 @@ 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"`
// DurationMs 是 thinking 部件的思考时长(毫秒,从首个推理增量到最后一个)。
// 工具耗时在 ToolCallPart.Duration,不复用本字段。
DurationMs float64 `json:"durationMs,omitempty"`
}
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 {
@@ -121,10 +138,19 @@ 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)。
type CardPart struct {
Kind string `json:"kind"`
Data []UINode `json:"data"`
Kind string `json:"kind"`
Data []UINode `json:"data"`
CardID ID `json:"cardId,omitempty"`
}
// ---- Run(一切运行皆 Run)----
@@ -140,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 {
@@ -181,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"`
}
// ---- 工件 ----
@@ -201,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"`
}
+63 -43
View File
@@ -8,59 +8,79 @@ import "encoding/json"
const PluginAPIVersion = "1.0"
type PluginManifest struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
APIVersion string `json:"apiVersion"`
Requires PluginRequires `json:"requires,omitempty"`
Provides PluginProvides `json:"provides"`
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
APIVersion string `json:"apiVersion"`
Requires PluginRequires `json:"requires,omitempty"`
Provides PluginProvides `json:"provides"`
Scope PluginScope `json:"scope,omitzero"`
Backend PluginBackend `json:"backend,omitzero"`
}
type PluginRequires struct {
Network []string `json:"network,omitempty"` // 允许访问的域名(展示用,v1 自装自审)
Network []string `json:"network,omitempty"` // 出站域名白名单;v2 空名单拒绝出站,v1 空名单保持放行
}
type PluginProvides struct {
Tools []string `json:"tools,omitempty"` // tools/ 目录下的资产名
Workflows []string `json:"workflows,omitempty"`
Intents []string `json:"intents,omitempty"`
Prompts []string `json:"prompts,omitempty"`
Panels []string `json:"panels,omitempty"`
Cards []string `json:"cards,omitempty"`
Settings []string `json:"settings,omitempty"`
Commands []string `json:"commands,omitempty"` // commands/ 资产名(spec 权威在 Agent-Command 库)
Tools []string `json:"tools,omitempty"` // tools/ 目录下的资产名
Workflows []string `json:"workflows,omitempty"`
Intents []string `json:"intents,omitempty"`
Prompts []string `json:"prompts,omitempty"`
Panels []string `json:"panels,omitempty"`
Cards []string `json:"cards,omitempty"`
Settings []string `json:"settings,omitempty"`
Commands []string `json:"commands,omitempty"` // commands/ 资产名(spec 权威在 Agent-Command 库)
Workspaces []string `json:"workspaces,omitempty"` // workspaces/ 资产名(工作区弹层)
Skills []string `json:"skills,omitempty"`
MCPServers []string `json:"mcpServers,omitempty"`
Knowledge []string `json:"knowledge,omitempty"`
Parsers []string `json:"parsers,omitempty"`
Agents []string `json:"agents,omitempty"`
Channels []string `json:"channels,omitempty"`
Pages []string `json:"pages,omitempty"`
}
// PluginPanel:右栏面板声明(UI schema 静态骨架 + 可选数据提供者)。
type PluginPanel struct {
ID string `json:"id"`
Title string `json:"title"`
Icon string `json:"icon,omitempty"`
ID string `json:"id"`
Title string `json:"title"`
Icon string `json:"icon,omitempty"`
Body []UINode `json:"body"`
// Tool 绑定:面板挂载时调用该工具刷新 Body 中的动态区(v1 简化为整体刷新)。
RefreshTool string `json:"refreshTool,omitempty"`
}
// 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"` // 绑定意图(命中即启动)
Steps []WorkflowStep `json:"steps"`
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
Prompt string `json:"prompt,omitempty"` // prompt: 发给模型的指令模板
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: 产收件箱任务卡
ID string `json:"id"`
Title string `json:"title"`
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)——步骤执行时生成
@@ -76,20 +96,20 @@ type ActivityCardTemplate struct {
// PluginWorkspace:工作区弹层声明(深读与治理视图;Esc/遮罩关闭回对话现场,
// 不承载任何唯一步骤——每个操作必须另有命令路径,提案 D31)。
type PluginWorkspace struct {
ID string `json:"id"`
Title string `json:"title"`
Icon string `json:"icon,omitempty"`
Shortcut string `json:"shortcut,omitempty"` // "1"–"9"(⌘N / Ctrl+N 召出)
BadgeTool string `json:"badgeTool,omitempty"` // 角标提供者工具:返回 {count,tone}
Body []UINode `json:"body"`
ID string `json:"id"`
Title string `json:"title"`
Icon string `json:"icon,omitempty"`
Shortcut string `json:"shortcut,omitempty"` // "1"–"9"(⌘N / Ctrl+N 召出)
BadgeTool string `json:"badgeTool,omitempty"` // 角标提供者工具:返回 {count,tone}
Body []UINode `json:"body"`
// RefreshTool:挂载时调用刷新 Body 动态区(与 Panel 同机制,v1 整体刷新)。
RefreshTool string `json:"refreshTool,omitempty"`
}
type HITLCardTemplate struct {
Question string `json:"question"`
Kind HITLKind `json:"kind"`
Choices []HITLChoice `json:"choices,omitempty"`
AllowFreeInput bool `json:"allowFreeInput"`
FreeInputHint string `json:"freeInputHint,omitempty"`
Question string `json:"question"`
Kind HITLKind `json:"kind"`
Choices []HITLChoice `json:"choices,omitempty"`
AllowFreeInput bool `json:"allowFreeInput"`
FreeInputHint string `json:"freeInputHint,omitempty"`
}
+100
View File
@@ -0,0 +1,100 @@
package core
// PluginAPIVersionExperimental 是插件契约 v2 的实验窗口。
// 1.0 清单继续有效;v2 只增加可选字段,不收紧 1.0 已有资产。
const PluginAPIVersionExperimental = "2.0-experimental"
// PluginAPIAccepted 报告宿主是否接受该 apiVersion。空值由宿主填成 PluginAPIVersion。
func PluginAPIAccepted(v string) bool {
switch v {
case PluginAPIVersion, PluginAPIVersionExperimental:
return true
default:
return false
}
}
// PluginScope 限制插件对哪些租户/项目可见。两者都空 = 全部可见。
type PluginScope struct {
Tenants []string `json:"tenants,omitempty"`
Projects []string `json:"projects,omitempty"`
}
// Allows 判断 owner/project 是否落在作用域内。
func (s PluginScope) Allows(owner, project string) bool {
if len(s.Tenants) > 0 && !scopeContains(s.Tenants, owner) {
return false
}
if len(s.Projects) > 0 && !scopeContains(s.Projects, project) {
return false
}
return true
}
func scopeContains(xs []string, v string) bool {
for _, x := range xs {
if x == v {
return true
}
}
return false
}
// PluginBackend 声明进程内或 sidecar 代码档。SchemaVersion 是插件自有数据的迁移版本。
type PluginBackend struct {
Mode string `json:"mode,omitempty"` // inproc | sidecar
Command string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
SchemaVersion int `json:"schemaVersion,omitempty"`
}
// PluginSkill 是 skills/<name>.md 的装载结果(SKILL.md 正文)。
type PluginSkill struct {
Name string `json:"name"`
Body string `json:"body"`
}
// PluginMCPServer 是 mcpServers/<name>.json,字段与 agent-native MCP ServerConfig 对齐。
type PluginMCPServer struct {
Name string `json:"name"`
Command string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
URL string `json:"url,omitempty"`
}
// PluginKnowledgeSet 是知识数据集声明(数据在插件侧,检索引擎在基座)。
type PluginKnowledgeSet struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
}
// PluginParserDecl 是解析器声明。实现可以是内置名或 sidecar 工具名。
type PluginParserDecl struct {
ID string `json:"id"`
Title string `json:"title"`
MIME string `json:"mime,omitempty"`
Tool string `json:"tool,omitempty"`
}
// PluginAgentDecl 是画像/子代理声明。
type PluginAgentDecl struct {
ID string `json:"id"`
Title string `json:"title"`
Instructions string `json:"instructions,omitempty"`
Tools []string `json:"tools,omitempty"`
}
// PluginChannelDecl 是渠道适配器声明(飞书等由下游实现,基座只登记入口)。
type PluginChannelDecl struct {
ID string `json:"id"`
Kind string `json:"kind"`
Title string `json:"title,omitempty"`
}
// PluginPageDecl 是前端 ESM 页面。Module 相对插件目录或绝对 URL。
type PluginPageDecl struct {
ID string `json:"id"`
Title string `json:"title"`
Module string `json:"module"`
}
+26
View File
@@ -0,0 +1,26 @@
package core
import "testing"
func TestPluginAPIAccepted(t *testing.T) {
if !PluginAPIAccepted(PluginAPIVersion) || !PluginAPIAccepted(PluginAPIVersionExperimental) {
t.Fatal("current and experimental windows must be accepted")
}
if PluginAPIAccepted("0.9") || PluginAPIAccepted("") {
t.Fatal("unknown or empty apiVersion must be rejected")
}
}
func TestPluginScopeAllows(t *testing.T) {
open := PluginScope{}
if !open.Allows("a", "p") {
t.Fatal("empty scope is visible to everyone")
}
scoped := PluginScope{Tenants: []string{"acme"}, Projects: []string{"p1"}}
if !scoped.Allows("acme", "p1") {
t.Fatal("matching tenant and project should pass")
}
if scoped.Allows("other", "p1") || scoped.Allows("acme", "p2") {
t.Fatal("scope must reject mismatches")
}
}
+37
View File
@@ -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
View File
@@ -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)深合并结果。
+4
View File
@@ -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)。
+4
View File
@@ -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
View File
@@ -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" "$@"
+16
View File
@@ -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
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# 通用任务入口:转调 scripts/dm 的 task 子命令(triage / start / pr-check)。
set -euo pipefail
exec "$(dirname "$0")/dm" task "$@"
+85
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
+330
View File
@@ -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
}
+147
View File
@@ -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
View File
@@ -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
}
+90
View File
@@ -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")
}
}
+2
View File
@@ -2,6 +2,8 @@
"id": "cnv_abc123def456",
"projectId": "prj_abc123def456",
"title": "新会话",
"allowCrossRead": true,
"crossPrivate": true,
"createdAt": "0001-01-01T00:00:00Z",
"updatedAt": "0001-01-01T00:00:00Z"
}
+19
View File
@@ -0,0 +1,19 @@
{
"seq": 47,
"conversationId": "project:prj_abc123def456",
"projectID": "prj_abc123def456",
"ts": "2026-09-30T11:00:00Z",
"v": 1,
"type": "card.created",
"payload": {
"card": {
"id": "card_def456abc789",
"projectId": "prj_abc123def456",
"kind": "result",
"title": "项目级卡",
"status": "done",
"createdAt": "2026-09-30T11:00:00Z",
"updatedAt": "2026-09-30T11:00:00Z"
}
}
}
+15 -1
View File
@@ -7,7 +7,8 @@
{
"type": "thinking",
"text": "考虑先检索…",
"process": true
"process": true,
"durationMs": 1500
},
{
"type": "tool_call",
@@ -39,6 +40,19 @@
"name": "报告.md",
"kind": "document"
}
},
{
"type": "card",
"card": {
"kind": "result",
"data": [
{
"type": "markdown",
"markdown": "全文见卡片"
}
],
"cardId": "card_abc123def456"
}
}
],
"createdAt": "0001-01-01T00:00:00Z"
+14
View File
@@ -0,0 +1,14 @@
{
"id": "agent.sub.explore",
"layer": "agent",
"version": 1,
"title": "探索子代理",
"description": "",
"body": "只读检索。",
"tools": [
"knowledge_search",
"web_search"
],
"model": "lightweight",
"maxTurns": 8
}
+69
View File
@@ -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"
}
]
}
+18 -11
View File
@@ -20,16 +20,23 @@ type ToolDesc struct {
Source SourceKind `json:"source"`
PluginID string `json:"pluginId,omitempty"`
HTTP *HTTPToolSpec `json:"http,omitempty"` // 声明式 HTTP 工具(无代码)
// 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"`
}
@@ -41,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"` // 超长转工件
}
+164 -8
View File
@@ -14,6 +14,9 @@ export interface Project {
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;
}
@@ -21,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;
@@ -31,25 +34,37 @@ export interface Part {
artifact?: { artifactId: ID; name: string; kind?: string };
hitl?: { cardId: ID; question: string; resolved: boolean; answer?: string };
plan?: PlanSnapshot;
card?: { kind: string; data: UINode[] };
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
}
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 {
@@ -77,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;
@@ -91,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 }
@@ -100,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 }
// ---- 收件箱卡片 ----
@@ -121,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 }
@@ -142,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 {
@@ -156,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][];
@@ -207,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; }
@@ -235,6 +284,8 @@ export interface SettingDesc {
default?: unknown; options?: string[];
effect: 'hot' | 'new-run' | 'restart';
secret?: boolean;
/** P2 受管:system = 系统受管项,仅系统管理面可写(用户/项目层拒绝)。 */
scope?: 'system';
}
// ---- 插件 ----
@@ -246,23 +297,61 @@ export interface PluginManifest {
tools?: string[]; workflows?: string[]; intents?: string[];
prompts?: string[]; panels?: string[]; cards?: string[]; settings?: string[];
commands?: string[]; workspaces?: string[];
skills?: string[]; mcpServers?: string[]; knowledge?: string[];
parsers?: string[]; agents?: string[]; channels?: string[]; pages?: string[];
};
scope?: { tenants?: string[]; projects?: string[] };
backend?: { mode?: 'inproc' | 'sidecar'; command?: string; args?: string[]; schemaVersion?: number };
}
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;
}
export interface PluginPageDecl {
id: string; title: string; module: string;
}
// ---- 摄取 / 检索 ----
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;
};
@@ -283,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 }
+6 -1
View File
@@ -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