From 11ef9b3751ea06f4444c7022efc46c09d1ce7da5 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 17:08:21 +0800 Subject: [PATCH 01/21] docs(agent): specify long-horizon orchestration --- .autopilot/PROGRESS.md | 23 ++++++++ .../acceptance/accept-01-execution-graph.sh | 5 ++ .../acceptance/accept-02-workspace-lease.sh | 5 ++ .../accept-03-completion-arbiter.sh | 5 ++ .../acceptance/accept-04-product-surface.sh | 11 ++++ .autopilot/config.sh | 9 +++ .autopilot/no-touch.txt | 5 ++ .autopilot/sprint-prompt.md | 4 ++ .autopilot/state | 6 ++ .autopilot/task.md | 47 +++++++++++++++ .../.openspec.yaml | 2 + .../unify-long-horizon-execution/design.md | 52 +++++++++++++++++ .../unify-long-horizon-execution/proposal.md | 30 ++++++++++ .../specs/orchestration/spec.md | 58 +++++++++++++++++++ .../unify-long-horizon-execution/tasks.md | 34 +++++++++++ 15 files changed, 296 insertions(+) create mode 100644 .autopilot/PROGRESS.md create mode 100755 .autopilot/acceptance/accept-01-execution-graph.sh create mode 100755 .autopilot/acceptance/accept-02-workspace-lease.sh create mode 100755 .autopilot/acceptance/accept-03-completion-arbiter.sh create mode 100755 .autopilot/acceptance/accept-04-product-surface.sh create mode 100644 .autopilot/config.sh create mode 100644 .autopilot/no-touch.txt create mode 100644 .autopilot/sprint-prompt.md create mode 100644 .autopilot/state create mode 100644 .autopilot/task.md create mode 100644 openspec/changes/unify-long-horizon-execution/.openspec.yaml create mode 100644 openspec/changes/unify-long-horizon-execution/design.md create mode 100644 openspec/changes/unify-long-horizon-execution/proposal.md create mode 100644 openspec/changes/unify-long-horizon-execution/specs/orchestration/spec.md create mode 100644 openspec/changes/unify-long-horizon-execution/tasks.md diff --git a/.autopilot/PROGRESS.md b/.autopilot/PROGRESS.md new file mode 100644 index 00000000..9befb600 --- /dev/null +++ b/.autopilot/PROGRESS.md @@ -0,0 +1,23 @@ +# PROGRESS + +## 当前 + +- 下一项:T1 Execution Graph contracts and stores +- 最近绿 tag:尚未建立 +- 备注:基线为 origin/main 5ad2b202;当前由主会话执行,未启动外部 relay。 + +## 棒日志 + +| 棒 | 时间 | 认领项 | 门禁 | 证据 | 备注 | +|---|---|---|---|---|---| + +## 仲裁队列 + +| 编号 | 类型 | 状态 | 摘要 | +|---|---|---|---| + +## 打回记录 + +| 时间 | 打回内容 | 蒸馏成的验收器 | +|---|---|---| + diff --git a/.autopilot/acceptance/accept-01-execution-graph.sh b/.autopilot/acceptance/accept-01-execution-graph.sh new file mode 100755 index 00000000..75200b41 --- /dev/null +++ b/.autopilot/acceptance/accept-01-execution-graph.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail +npm run build -w @rdk-moss/agent +npm run test:filter -w @rdk-moss/agent -- --filter execution-graph + diff --git a/.autopilot/acceptance/accept-02-workspace-lease.sh b/.autopilot/acceptance/accept-02-workspace-lease.sh new file mode 100755 index 00000000..5c658f0e --- /dev/null +++ b/.autopilot/acceptance/accept-02-workspace-lease.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail +npm run build -w @rdk-moss/agent +npm run test:filter -w @rdk-moss/agent -- --filter workspace-lease + diff --git a/.autopilot/acceptance/accept-03-completion-arbiter.sh b/.autopilot/acceptance/accept-03-completion-arbiter.sh new file mode 100755 index 00000000..71e37307 --- /dev/null +++ b/.autopilot/acceptance/accept-03-completion-arbiter.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail +npm run build -w @rdk-moss/agent +npm run test:filter -w @rdk-moss/agent -- --filter completion-arbiter + diff --git a/.autopilot/acceptance/accept-04-product-surface.sh b/.autopilot/acceptance/accept-04-product-surface.sh new file mode 100755 index 00000000..649fc0c3 --- /dev/null +++ b/.autopilot/acceptance/accept-04-product-surface.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail +npm run build -w @rdk-moss/agent +node --input-type=module - <<'NODE' +const orchestration = await import('@rdk-moss/agent/orchestration'); +for (const name of ['InMemoryExecutionStore', 'JsonlExecutionStore', 'ExecutionGraphScheduler', 'CompletionArbiter']) { + if (!(name in orchestration)) throw new Error(`missing orchestration export: ${name}`); +} +NODE +npm run test:filter -w @rdk-moss/agent -- --filter task-command --filter plugin-agent-role + diff --git a/.autopilot/config.sh b/.autopilot/config.sh new file mode 100644 index 00000000..5d78c150 --- /dev/null +++ b/.autopilot/config.sh @@ -0,0 +1,9 @@ +BUILD_CMD="npm run build -w @rdk-moss/agent" +TEST_CMD="npm run test:filter -w @rdk-moss/agent -- --filter orchestration" +FULL_CMD="npm run verify" +FULL_EVERY=2 +CONTRACT_PATHS="openspec/changes/unify-long-horizon-execution/" +RUNNER_CMD="codex exec --full-auto" +PROVIDER_PROBES=() +REQUIRED_ENV=() + diff --git a/.autopilot/no-touch.txt b/.autopilot/no-touch.txt new file mode 100644 index 00000000..2389dd7b --- /dev/null +++ b/.autopilot/no-touch.txt @@ -0,0 +1,5 @@ +packages/moss/src/ +packages/create-moss-app/ +:(glob)**/.env +:(glob)**/*.lock + diff --git a/.autopilot/sprint-prompt.md b/.autopilot/sprint-prompt.md new file mode 100644 index 00000000..13cf2c28 --- /dev/null +++ b/.autopilot/sprint-prompt.md @@ -0,0 +1,4 @@ +你是 Moss 长程执行图实施任务的一棒。先读 qiaolong-autopilot Phase 3、`.autopilot/task.md`、 +`.autopilot/PROGRESS.md`、`.autopilot/state` 和最近五个提交;恰好认领一项,以测试先红后绿推进, +保存真实命令证据,门禁绿后提交。禁止削弱 OpenSpec、工具安全、审批与插件原子生命周期。 + diff --git a/.autopilot/state b/.autopilot/state new file mode 100644 index 00000000..01168999 --- /dev/null +++ b/.autopilot/state @@ -0,0 +1,6 @@ +STATUS=RUNNING +SPRINT=0 +CONSEC_RED=0 +SAME_ERR_COUNT=0 +LAST_ERROR_SIG= + diff --git a/.autopilot/task.md b/.autopilot/task.md new file mode 100644 index 00000000..07c551f1 --- /dev/null +++ b/.autopilot/task.md @@ -0,0 +1,47 @@ +# Autopilot 合同:Moss 长程执行图与专家团 + +## 1. 目标与 Done(机器可判定) + +- 问题本质:把分散、不可恢复、不可合并的 agent 工作转换为单一持久执行图与证据闭环。 +- 自治级别:L3(合并和推送是扳机,但用户已在任务中显式授权)。 +- Done = 下列验收器全绿 + `npm run verify` 全绿 + CI 全绿: + - [ ] `acceptance/accept-01-execution-graph.sh` + - [ ] `acceptance/accept-02-workspace-lease.sh` + - [ ] `acceptance/accept-03-completion-arbiter.sh` + - [ ] `acceptance/accept-04-product-surface.sh` + +## 2. Non-goals + +- 不实现分布式高可用、递归专家树、共享目录并行直写或外部工作流引擎。 +- 不削弱现有工具安全、审批、插件原子生命周期与错误边界。 + +## 3. 改动范围与禁区 + +- 允许改:`packages/moss-agent/`、相关 docs、OpenSpec、API reports、benchmarks、changelog。 +- 禁改清单:见 `no-touch.txt`。 +- 契约冻结:本 OpenSpec 在实现期间只做纠错,不为让测试变绿而弱化验收。 + +## 4. Checklist + +- [ ] T1 Execution Graph contracts and stores +- [ ] T2 DAG scheduler and recovery policy +- [ ] T3 Workspace Lease and guarded patch merge +- [ ] T4 Expert routing, synthesis, and completion arbiter +- [ ] T5 MossAgent, plugin, CLI/Web/TUI/ACP integration and migration +- [ ] T6 public API, docs, benchmarks, full verification, merge, push, CI + +## 5. 风险与不确定 + +- Windows 无法在本机真跑;以路径单测和 GitHub Windows CI 作为平台证据。 +- 真实模型场景依赖 provider 凭据;无凭据时必须明确 SKIP,不能用 fixture 冒充。 + +## 6. 扳机操作 + +- 合并到 `main` 与 push;用户已明确授权,仅在全量门全绿后执行。 +- 不执行 npm publish、删除用户数据或生产部署。 + +## 7. 预算与阈值 + +- 最大阶段:6;每个阶段 focused tests 后提交可恢复绿点。 +- 同一错误连续三次或连续两阶段全量门红即熔断并报告。 + diff --git a/openspec/changes/unify-long-horizon-execution/.openspec.yaml b/openspec/changes/unify-long-horizon-execution/.openspec.yaml new file mode 100644 index 00000000..2029f010 --- /dev/null +++ b/openspec/changes/unify-long-horizon-execution/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven + diff --git a/openspec/changes/unify-long-horizon-execution/design.md b/openspec/changes/unify-long-horizon-execution/design.md new file mode 100644 index 00000000..5d6421e7 --- /dev/null +++ b/openspec/changes/unify-long-horizon-execution/design.md @@ -0,0 +1,52 @@ +# Design + +## Authority and storage + +`ExecutionGraph` is an instance-owned event-sourced aggregate. `ExecutionStore` owns persistence and +compare-and-set revision checks. `JsonlExecutionStore` writes one graph directory under +`.moss/runtime/executions//`, repairs only an invalid JSONL tail, snapshots every 100 events, +and uses a renewable owner lease to prevent two local hosts from advancing the same graph. + +All recovery is fail-closed. Restarted graphs enter `paused_recovered`. Read-only nodes without side +effects may be made ready by an explicit policy; interrupted external mutations become blocked. + +## Scheduling + +The scheduler derives readiness from dependencies rather than step number. It admits at most the +configured concurrency, rejects overlapping normalized write paths, preserves successful siblings, +and pauses on explicit token, cost, or wall-time budget exhaustion. Three consecutive occurrences of +the same failure fingerprint block a node. + +## Workspace leases + +Git workspaces use detached worktrees seeded from a captured base commit, followed by the parent's +staged/unstaged changes and non-ignored untracked files. Non-Git and unborn repositories use a filtered +copy snapshot. Secrets, `.env`, `.git`, `.moss`, and build caches are excluded. A worker returns a patch +and evidence; parent files are hash-checked before the patch passes existing safety and approval seams. +Unmerged leases survive restart and are removed only after merge, rejection, or cancellation. + +## Expert roles + +Legacy `SubagentExpertDefinition` remains the read-only advisor interface. `AgentRoleDefinition` adds +advisor, implementer, and verifier roles with capability, tool, budget, workspace, and structured-result +contracts. Plugins may register roles atomically, but isolated-write implementers require explicit host +authorization. Workers cannot delegate further. + +## Completion + +`CompletionArbiter` combines dependency state, pending work, lease/merge state, acceptance predicates, +fresh verification, and requirement coverage. Deterministic failures dominate semantic model judgment. +Strict mode returns `blocked/needs_evidence`; a verified verdict always cites evidence identifiers. + +## Migration + +Preview mode shadow-writes the graph while legacy projections remain authoritative. Authority mode +writes only through the graph and exposes legacy adapters. Importers preserve original files and write a +migration marker only after a successful append. + +## Do not touch + +- Tool side-effect metadata, approvals, stale-read protection, and error boundaries remain authoritative. +- Plugin registration/disposal remains atomic and instance-owned. +- Read-only expert tool filtering remains fail-closed. + diff --git a/openspec/changes/unify-long-horizon-execution/proposal.md b/openspec/changes/unify-long-horizon-execution/proposal.md new file mode 100644 index 00000000..337ab28d --- /dev/null +++ b/openspec/changes/unify-long-horizon-execution/proposal.md @@ -0,0 +1,30 @@ +# Change: unify long-horizon execution and expert work + +## Why + +Moss currently records Goal, TaskFrame, Plan, Loop, Web task runs, and background work through +separate state owners. That is sufficient for bounded single-process work, but it cannot safely resume +one authoritative task after restart or let implementation experts return isolated changes with +machine-checkable evidence. + +## User outcome + +- One graph identity and ordered event stream describes a long-running task in CLI, TUI, Web, and ACP. +- A restarted local host recovers unfinished work as paused without replaying uncertain mutations. +- Independent expert assignments run concurrently when dependencies and declared write paths permit. +- Implementation assignments write only in retained workspace leases and return patches for guarded merge. +- Completion and verification cite concrete evidence; missing evidence pauses instead of accepting model text. + +## Non-goals + +- Distributed or multi-user scheduling, remote high availability, or an external workflow engine. +- Recursive worker delegation or direct concurrent writes into the parent workspace. +- Automatic replay of interrupted external mutations. +- A plugin marketplace or sandbox claim for trusted third-party JavaScript. + +## Compatibility + +`TaskRunLedger`, Goal, TaskFrame, Plan, Loop, and `MossAsyncTaskRegistry` remain readable during one +release cycle. New runtime writes are projected from the execution graph, with legacy files imported +non-destructively and marked after successful migration. + diff --git a/openspec/changes/unify-long-horizon-execution/specs/orchestration/spec.md b/openspec/changes/unify-long-horizon-execution/specs/orchestration/spec.md new file mode 100644 index 00000000..3dc065c5 --- /dev/null +++ b/openspec/changes/unify-long-horizon-execution/specs/orchestration/spec.md @@ -0,0 +1,58 @@ +# Orchestration specification + +## ADDED Requirements + +### Requirement: Durable authoritative execution graph + +The runtime SHALL persist every task transition as an ordered graph event guarded by an expected +revision, and SHALL recover an unfinished graph as paused after restart. + +#### Scenario: competing local owners + +- **WHEN** two runtime instances attempt to advance the same graph +- **THEN** only the holder of the unexpired owner lease may append execution transitions + +#### Scenario: damaged final write + +- **WHEN** a JSONL file ends with an incomplete event +- **THEN** the valid prefix is preserved, the invalid tail is quarantined, and no completed node is replayed + +### Requirement: Dependency and write-safe scheduling + +The runtime SHALL schedule only nodes whose dependencies are satisfied and whose normalized write paths +do not overlap another active implementation node. + +#### Scenario: failed sibling + +- **WHEN** one independent node fails +- **THEN** its dependents are blocked while successful sibling evidence remains available + +### Requirement: Isolated implementation + +An implementation worker SHALL write only in a retained workspace lease and SHALL return a patch whose +parent targets are unchanged from their captured baseline before merge. + +#### Scenario: parent edit races with merge + +- **WHEN** a target parent file changes after lease creation +- **THEN** merge stops with `merge_conflict` and does not overwrite the parent file + +### Requirement: Evidence-bound completion + +The runtime SHALL refuse verified completion when required evidence, merged changes, fresh verification, +or requirement coverage is missing. + +#### Scenario: model claims success without proof + +- **WHEN** model output says the task is complete but required evidence is absent +- **THEN** strict arbitration returns `blocked` with reason `needs_evidence` + +### Requirement: Atomic plugin roles + +The plugin host SHALL register and dispose agent roles atomically, and SHALL require explicit host +authorization before accepting an implementer with isolated-write permission. + +#### Scenario: unauthorized implementation role + +- **WHEN** a plugin contributes an isolated-write implementer without host authorization +- **THEN** plugin activation fails atomically and no staged role remains registered diff --git a/openspec/changes/unify-long-horizon-execution/tasks.md b/openspec/changes/unify-long-horizon-execution/tasks.md new file mode 100644 index 00000000..7db1c896 --- /dev/null +++ b/openspec/changes/unify-long-horizon-execution/tasks.md @@ -0,0 +1,34 @@ +## 1. Execution graph + +- [ ] 1.1 Define beta graph, node, event, evidence, policy, and store contracts. +- [ ] 1.2 Implement in-memory CAS, transition validation, leases, recovery, and projection. +- [ ] 1.3 Implement JSONL durability, locking, fsync, snapshots, corrupt-tail repair, and migration markers. +- [ ] 1.4 Project `TaskRunLedger` and remove module-level Plan state. + +## 2. Scheduling and isolation + +- [ ] 2.1 Implement dependency-driven readiness, concurrency, budgets, retries, and failure propagation. +- [ ] 2.2 Implement Git worktree and copy-snapshot workspace lease adapters. +- [ ] 2.3 Produce patches, digests, verification evidence, guarded merge, and retained recovery. + +## 3. Expert team and completion + +- [ ] 3.1 Add capability-based role routing and structured assignment results. +- [ ] 3.2 Add coverage/conflict synthesis and independent verifier scheduling. +- [ ] 3.3 Add deterministic-first completion arbitration with evidence-bound verdicts. +- [ ] 3.4 Add atomic plugin role contribution with isolated-write authorization. + +## 4. Product integration and migration + +- [ ] 4.1 Add graph-backed MossAgent configuration and async task compatibility. +- [ ] 4.2 Add `/tasks` and `/task inspect|resume|retry|stop` commands. +- [ ] 4.3 Expose the same graph identity, sequence, evidence, and verdict to Web, TUI, and ACP. +- [ ] 4.4 Import legacy TaskRun, Goal, TaskFrame, Plan, and Loop files without deleting originals. + +## 5. Evidence and delivery + +- [ ] 5.1 Cover state, restart, DAG, workspace, expert, plugin, and completion negative cases. +- [ ] 5.2 Run real CLI/Web paths and record 10/10 long-task plus 10/10 concurrency evidence. +- [ ] 5.3 Update API reports, Architecture, extension/user docs, and changelog. +- [ ] 5.4 Pass focused tests, `npm run check`, CLI smoke, `npm run verify`, and three-platform CI. + From 79b157e0a25de2867309f2d5f04573f08b63018a Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 17:17:10 +0800 Subject: [PATCH 02/21] feat(agent): add durable execution graph store --- .autopilot/PROGRESS.md | 16 +- .autopilot/config.sh | 3 +- .autopilot/sprint-prompt.md | 1 - .autopilot/state | 3 +- .autopilot/task.md | 3 +- .../.openspec.yaml | 1 - .../unify-long-horizon-execution/design.md | 1 - .../unify-long-horizon-execution/proposal.md | 1 - .../unify-long-horizon-execution/tasks.md | 7 +- packages/moss-agent/package.json | 5 + packages/moss-agent/src/cli/exit-codes.ts | 4 + packages/moss-agent/src/errors.ts | 8 + packages/moss-agent/src/index.ts | 31 ++ .../src/orchestration/execution-projector.ts | 285 ++++++++++++++++ .../src/orchestration/execution-recovery.ts | 60 ++++ .../src/orchestration/execution-types.ts | 223 +++++++++++++ .../in-memory-execution-store.ts | 131 ++++++++ .../moss-agent/src/orchestration/index.ts | 28 ++ .../orchestration/jsonl-execution-store.ts | 307 ++++++++++++++++++ .../moss-agent/test/execution-graph.spec.mjs | 178 ++++++++++ scripts/config/api-entrypoints.json | 5 + 21 files changed, 1279 insertions(+), 22 deletions(-) create mode 100644 packages/moss-agent/src/orchestration/execution-projector.ts create mode 100644 packages/moss-agent/src/orchestration/execution-recovery.ts create mode 100644 packages/moss-agent/src/orchestration/execution-types.ts create mode 100644 packages/moss-agent/src/orchestration/in-memory-execution-store.ts create mode 100644 packages/moss-agent/src/orchestration/index.ts create mode 100644 packages/moss-agent/src/orchestration/jsonl-execution-store.ts create mode 100644 packages/moss-agent/test/execution-graph.spec.mjs diff --git a/.autopilot/PROGRESS.md b/.autopilot/PROGRESS.md index 9befb600..375596cc 100644 --- a/.autopilot/PROGRESS.md +++ b/.autopilot/PROGRESS.md @@ -2,22 +2,22 @@ ## 当前 -- 下一项:T1 Execution Graph contracts and stores -- 最近绿 tag:尚未建立 -- 备注:基线为 origin/main 5ad2b202;当前由主会话执行,未启动外部 relay。 +- 下一项:T2 DAG scheduler and recovery policy +- 最近绿 tag:autopilot/unify-long-horizon-green-000 +- 备注:执行图内存/JSONL adapter、CAS、owner lease、尾损恢复与 fail-closed restart 已转绿。 ## 棒日志 -| 棒 | 时间 | 认领项 | 门禁 | 证据 | 备注 | -|---|---|---|---|---|---| +| 棒 | 时间 | 认领项 | 门禁 | 证据 | 备注 | +| --- | ---------- | --------------------------------------- | ----- | ------------------------------- | ----------------- | +| 001 | 2026-08-18 | T1 Execution Graph contracts and stores | GREEN | `test/execution-graph.spec.mjs` | 4/4 focused cases | ## 仲裁队列 | 编号 | 类型 | 状态 | 摘要 | -|---|---|---|---| +| ---- | ---- | ---- | ---- | ## 打回记录 | 时间 | 打回内容 | 蒸馏成的验收器 | -|---|---|---| - +| ---- | -------- | -------------- | diff --git a/.autopilot/config.sh b/.autopilot/config.sh index 5d78c150..c083dcb9 100644 --- a/.autopilot/config.sh +++ b/.autopilot/config.sh @@ -1,9 +1,8 @@ BUILD_CMD="npm run build -w @rdk-moss/agent" -TEST_CMD="npm run test:filter -w @rdk-moss/agent -- --filter orchestration" +TEST_CMD="npm run test:filter -w @rdk-moss/agent -- --filter execution-graph" FULL_CMD="npm run verify" FULL_EVERY=2 CONTRACT_PATHS="openspec/changes/unify-long-horizon-execution/" RUNNER_CMD="codex exec --full-auto" PROVIDER_PROBES=() REQUIRED_ENV=() - diff --git a/.autopilot/sprint-prompt.md b/.autopilot/sprint-prompt.md index 13cf2c28..fca09fe4 100644 --- a/.autopilot/sprint-prompt.md +++ b/.autopilot/sprint-prompt.md @@ -1,4 +1,3 @@ 你是 Moss 长程执行图实施任务的一棒。先读 qiaolong-autopilot Phase 3、`.autopilot/task.md`、 `.autopilot/PROGRESS.md`、`.autopilot/state` 和最近五个提交;恰好认领一项,以测试先红后绿推进, 保存真实命令证据,门禁绿后提交。禁止削弱 OpenSpec、工具安全、审批与插件原子生命周期。 - diff --git a/.autopilot/state b/.autopilot/state index 01168999..1c37d962 100644 --- a/.autopilot/state +++ b/.autopilot/state @@ -1,6 +1,5 @@ STATUS=RUNNING -SPRINT=0 +SPRINT=1 CONSEC_RED=0 SAME_ERR_COUNT=0 LAST_ERROR_SIG= - diff --git a/.autopilot/task.md b/.autopilot/task.md index 07c551f1..3d79d7e1 100644 --- a/.autopilot/task.md +++ b/.autopilot/task.md @@ -23,7 +23,7 @@ ## 4. Checklist -- [ ] T1 Execution Graph contracts and stores +- [x] T1 Execution Graph contracts and stores - [ ] T2 DAG scheduler and recovery policy - [ ] T3 Workspace Lease and guarded patch merge - [ ] T4 Expert routing, synthesis, and completion arbiter @@ -44,4 +44,3 @@ - 最大阶段:6;每个阶段 focused tests 后提交可恢复绿点。 - 同一错误连续三次或连续两阶段全量门红即熔断并报告。 - diff --git a/openspec/changes/unify-long-horizon-execution/.openspec.yaml b/openspec/changes/unify-long-horizon-execution/.openspec.yaml index 2029f010..b4bbeb94 100644 --- a/openspec/changes/unify-long-horizon-execution/.openspec.yaml +++ b/openspec/changes/unify-long-horizon-execution/.openspec.yaml @@ -1,2 +1 @@ schema: spec-driven - diff --git a/openspec/changes/unify-long-horizon-execution/design.md b/openspec/changes/unify-long-horizon-execution/design.md index 5d6421e7..f30cd98f 100644 --- a/openspec/changes/unify-long-horizon-execution/design.md +++ b/openspec/changes/unify-long-horizon-execution/design.md @@ -49,4 +49,3 @@ migration marker only after a successful append. - Tool side-effect metadata, approvals, stale-read protection, and error boundaries remain authoritative. - Plugin registration/disposal remains atomic and instance-owned. - Read-only expert tool filtering remains fail-closed. - diff --git a/openspec/changes/unify-long-horizon-execution/proposal.md b/openspec/changes/unify-long-horizon-execution/proposal.md index 337ab28d..dbfea990 100644 --- a/openspec/changes/unify-long-horizon-execution/proposal.md +++ b/openspec/changes/unify-long-horizon-execution/proposal.md @@ -27,4 +27,3 @@ machine-checkable evidence. `TaskRunLedger`, Goal, TaskFrame, Plan, Loop, and `MossAsyncTaskRegistry` remain readable during one release cycle. New runtime writes are projected from the execution graph, with legacy files imported non-destructively and marked after successful migration. - diff --git a/openspec/changes/unify-long-horizon-execution/tasks.md b/openspec/changes/unify-long-horizon-execution/tasks.md index 7db1c896..551eee42 100644 --- a/openspec/changes/unify-long-horizon-execution/tasks.md +++ b/openspec/changes/unify-long-horizon-execution/tasks.md @@ -1,8 +1,8 @@ ## 1. Execution graph -- [ ] 1.1 Define beta graph, node, event, evidence, policy, and store contracts. -- [ ] 1.2 Implement in-memory CAS, transition validation, leases, recovery, and projection. -- [ ] 1.3 Implement JSONL durability, locking, fsync, snapshots, corrupt-tail repair, and migration markers. +- [x] 1.1 Define beta graph, node, event, evidence, policy, and store contracts. +- [x] 1.2 Implement in-memory CAS, transition validation, leases, recovery, and projection. +- [x] 1.3 Implement JSONL durability, locking, fsync, snapshots, corrupt-tail repair, and migration markers. - [ ] 1.4 Project `TaskRunLedger` and remove module-level Plan state. ## 2. Scheduling and isolation @@ -31,4 +31,3 @@ - [ ] 5.2 Run real CLI/Web paths and record 10/10 long-task plus 10/10 concurrency evidence. - [ ] 5.3 Update API reports, Architecture, extension/user docs, and changelog. - [ ] 5.4 Pass focused tests, `npm run check`, CLI smoke, `npm run verify`, and three-platform CI. - diff --git a/packages/moss-agent/package.json b/packages/moss-agent/package.json index 826d6d65..8096893d 100644 --- a/packages/moss-agent/package.json +++ b/packages/moss-agent/package.json @@ -131,6 +131,11 @@ "import": "./dist/runtime/index.js", "default": "./dist/runtime/index.js" }, + "./orchestration": { + "types": "./dist/orchestration/index.d.ts", + "import": "./dist/orchestration/index.js", + "default": "./dist/orchestration/index.js" + }, "./web": { "types": "./dist/web-ui/index.d.ts", "import": "./dist/web-ui/index.js", diff --git a/packages/moss-agent/src/cli/exit-codes.ts b/packages/moss-agent/src/cli/exit-codes.ts index 47825ff1..22f7cb45 100644 --- a/packages/moss-agent/src/cli/exit-codes.ts +++ b/packages/moss-agent/src/cli/exit-codes.ts @@ -36,6 +36,10 @@ const ERROR_CODE_TO_EXIT: Record = { [ErrorCode.DEVICE_SSH_FAILED]: ExitCode.DEVICE_SSH, [ErrorCode.USER_ABORTED]: ExitCode.USER_ABORTED, [ErrorCode.AGENT_DISPOSED]: ExitCode.GENERIC, + [ErrorCode.EXECUTION_REVISION_CONFLICT]: ExitCode.GENERIC, + [ErrorCode.EXECUTION_LEASE_HELD]: ExitCode.GENERIC, + [ErrorCode.EXECUTION_STATE_INVALID]: ExitCode.USAGE, + [ErrorCode.EXECUTION_STORE_FAILED]: ExitCode.INTERNAL, [ErrorCode.CONFIG_IO_FAILED]: ExitCode.CONFIG, [ErrorCode.INTERNAL_INVARIANT_VIOLATED]: ExitCode.INTERNAL, [ErrorCode.UNKNOWN]: ExitCode.GENERIC, diff --git a/packages/moss-agent/src/errors.ts b/packages/moss-agent/src/errors.ts index 9e930db0..b5eb0f92 100644 --- a/packages/moss-agent/src/errors.ts +++ b/packages/moss-agent/src/errors.ts @@ -37,6 +37,14 @@ export enum ErrorCode { USER_ABORTED = 'USER_ABORTED', AGENT_DISPOSED = 'AGENT_DISPOSED', + EXECUTION_REVISION_CONFLICT = 'EXECUTION_REVISION_CONFLICT', + + EXECUTION_LEASE_HELD = 'EXECUTION_LEASE_HELD', + + EXECUTION_STATE_INVALID = 'EXECUTION_STATE_INVALID', + + EXECUTION_STORE_FAILED = 'EXECUTION_STORE_FAILED', + CONFIG_IO_FAILED = 'CONFIG_IO_FAILED', INTERNAL_INVARIANT_VIOLATED = 'INTERNAL_INVARIANT_VIOLATED', diff --git a/packages/moss-agent/src/index.ts b/packages/moss-agent/src/index.ts index 93b21662..fc0f55dc 100644 --- a/packages/moss-agent/src/index.ts +++ b/packages/moss-agent/src/index.ts @@ -189,6 +189,37 @@ export type { TaskRunStatus, TaskRunVerification, } from './core/index.js'; +export { + DEFAULT_ORCHESTRATION_POLICY, + InMemoryExecutionStore, + JsonlExecutionStore, + projectExecutionGraph, + recoverExecutionGraph, +} from './orchestration/index.js'; +export type { + AcquireExecutionLeaseInput, + AppendExecutionEventInput, + CreateExecutionGraphInput, + ExecutionBudget, + ExecutionEvent, + ExecutionEventType, + ExecutionEvidence, + ExecutionEvidenceKind, + ExecutionGraphSnapshot, + ExecutionGraphStatus, + ExecutionNode, + ExecutionNodeDefinition, + ExecutionNodeKind, + ExecutionNodeStatus, + ExecutionOwnerLease, + ExecutionRecovery, + ExecutionStore, + ExecutionVerification, + InMemoryExecutionStoreOptions, + JsonlExecutionStoreOptions, + OrchestrationPolicy, + RecoverExecutionGraphOptions, +} from './orchestration/index.js'; export type { JsonlSessionStoreConfig } from './core/index.js'; export type { SessionStore, SessionMeta } from './core/index.js'; diff --git a/packages/moss-agent/src/orchestration/execution-projector.ts b/packages/moss-agent/src/orchestration/execution-projector.ts new file mode 100644 index 00000000..34875a61 --- /dev/null +++ b/packages/moss-agent/src/orchestration/execution-projector.ts @@ -0,0 +1,285 @@ +import { ErrorCode, MossError } from '../errors.js'; +import type { + ExecutionBudget, + ExecutionEvent, + ExecutionEvidence, + ExecutionEventType, + ExecutionGraphSnapshot, + ExecutionNode, + ExecutionNodeDefinition, + ExecutionNodeStatus, + ExecutionRecovery, + ExecutionVerification, + OrchestrationPolicy, +} from './execution-types.js'; + +export const DEFAULT_ORCHESTRATION_POLICY: OrchestrationPolicy = { + maxConcurrency: 4, + maxAttemptsPerNode: 3, + strictCompletion: true, + autoResumeReadonly: false, +}; + +const NODE_TRANSITIONS: Readonly> = { + 'node.ready': ['pending', 'interrupted', 'failed'], + 'node.leased': ['ready'], + 'node.started': ['ready', 'leased'], + 'node.succeeded': ['running'], + 'node.failed': ['running'], + 'node.interrupted': ['ready', 'leased', 'running'], + 'node.retry_requested': ['failed', 'interrupted'], + 'node.blocked': ['pending', 'ready', 'leased', 'running', 'failed', 'interrupted'], + 'node.skipped': ['pending', 'ready', 'blocked'], + 'node.merge_conflict': ['running', 'succeeded'], + 'node.cancelled': ['pending', 'ready', 'leased', 'running', 'blocked', 'interrupted'], +}; + +function invalid(message: string): MossError { + return new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message }); +} + +function requiredString(value: unknown, field: string): string { + if (typeof value !== 'string' || !value.trim()) + throw invalid(`${field} must be a non-empty string`); + return value.trim(); +} + +function positiveInteger(value: unknown, fallback: number): number { + return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback; +} + +function normalizePolicy(raw: unknown): OrchestrationPolicy { + const value = raw && typeof raw === 'object' ? (raw as Record) : {}; + return { + maxConcurrency: Math.min(8, positiveInteger(value.maxConcurrency, 4)), + maxAttemptsPerNode: positiveInteger(value.maxAttemptsPerNode, 3), + strictCompletion: value.strictCompletion !== false, + autoResumeReadonly: value.autoResumeReadonly === true, + }; +} + +function stringArray(value: unknown): string[] { + return Array.isArray(value) + ? value.filter((item): item is string => typeof item === 'string').map((item) => item.trim()) + : []; +} + +function normalizeNode(definition: ExecutionNodeDefinition): ExecutionNode { + return { + id: requiredString(definition.id, 'node.id'), + kind: definition.kind, + title: requiredString(definition.title, 'node.title'), + dependencies: [...definition.dependencies], + ...(definition.roleId ? { roleId: definition.roleId } : {}), + ...(definition.requiredCapabilities + ? { requiredCapabilities: [...definition.requiredCapabilities] } + : {}), + ...(definition.writePaths ? { writePaths: [...definition.writePaths] } : {}), + ...(definition.acceptanceCriteria + ? { acceptanceCriteria: [...definition.acceptanceCriteria] } + : {}), + ...(definition.budget ? { budget: { ...definition.budget } } : {}), + status: 'pending', + attempts: 0, + consecutiveSameFailures: 0, + evidenceIds: [], + }; +} + +function createdSnapshot(event: ExecutionEvent): ExecutionGraphSnapshot { + const rawNodes = Array.isArray(event.data.nodes) + ? (event.data.nodes as ExecutionNodeDefinition[]) + : []; + const nodes: Record = {}; + for (const definition of rawNodes) { + const node = normalizeNode(definition); + if (nodes[node.id]) throw invalid(`duplicate execution node "${node.id}"`); + nodes[node.id] = node; + } + for (const node of Object.values(nodes)) { + for (const dependency of node.dependencies) { + if (!nodes[dependency]) + throw invalid(`node "${node.id}" depends on unknown node "${dependency}"`); + if (dependency === node.id) throw invalid(`node "${node.id}" cannot depend on itself`); + } + } + return { + id: event.graphId, + ...(typeof event.data.sessionId === 'string' ? { sessionId: event.data.sessionId } : {}), + goal: requiredString(event.data.goal, 'goal'), + status: 'paused', + revision: event.seq, + createdAt: event.time, + updatedAt: event.time, + policy: normalizePolicy(event.data.policy), + budget: + event.data.budget && typeof event.data.budget === 'object' + ? { ...(event.data.budget as ExecutionBudget) } + : {}, + nodes, + evidence: [], + }; +} + +function requireNode(snapshot: ExecutionGraphSnapshot, event: ExecutionEvent): ExecutionNode { + if (!event.nodeId) throw invalid(`${event.type} requires nodeId`); + const node = snapshot.nodes[event.nodeId]; + if (!node) throw invalid(`unknown execution node "${event.nodeId}"`); + return node; +} + +function nodeStatusFor(event: ExecutionEvent): ExecutionNodeStatus | undefined { + const suffix = event.type.startsWith('node.') ? event.type.slice(5) : ''; + if (suffix === 'retry_requested') return 'ready'; + if ( + suffix === 'ready' || + suffix === 'leased' || + suffix === 'started' || + suffix === 'succeeded' || + suffix === 'failed' || + suffix === 'interrupted' || + suffix === 'blocked' || + suffix === 'skipped' || + suffix === 'merge_conflict' || + suffix === 'cancelled' + ) { + return suffix === 'started' ? 'running' : suffix; + } + return undefined; +} + +function applyNodeEvent( + snapshot: ExecutionGraphSnapshot, + event: ExecutionEvent +): ExecutionGraphSnapshot { + const current = requireNode(snapshot, event); + const allowed = NODE_TRANSITIONS[event.type]; + const target = nodeStatusFor(event); + if (!allowed || !target) return snapshot; + if (!allowed.includes(current.status)) { + throw invalid(`node "${current.id}" cannot transition from ${current.status} to ${target}`); + } + const fingerprint = + typeof event.data.failureFingerprint === 'string' ? event.data.failureFingerprint : undefined; + const sameFailure = + event.type === 'node.failed' && fingerprint && fingerprint === current.failureFingerprint + ? current.consecutiveSameFailures + 1 + : event.type === 'node.failed' + ? 1 + : current.consecutiveSameFailures; + const attempts = event.type === 'node.started' ? current.attempts + 1 : current.attempts; + const next: ExecutionNode = { + ...current, + status: target, + attempts, + consecutiveSameFailures: sameFailure, + ...(fingerprint ? { failureFingerprint: fingerprint } : {}), + ...(typeof event.data.error === 'string' ? { error: event.data.error } : {}), + ...(typeof event.data.workspaceLeaseId === 'string' + ? { workspaceLeaseId: event.data.workspaceLeaseId } + : {}), + ...(event.type === 'node.started' ? { startedAt: event.time } : {}), + ...(event.type === 'node.succeeded' || event.type === 'node.skipped' + ? { completedAt: event.time } + : {}), + }; + return { ...snapshot, nodes: { ...snapshot.nodes, [current.id]: next } }; +} + +function parseEvidence(event: ExecutionEvent): ExecutionEvidence { + const raw = event.data.evidence; + if (!raw || typeof raw !== 'object') throw invalid('evidence.recorded requires evidence'); + const evidence = raw as ExecutionEvidence; + return { + ...evidence, + id: requiredString(evidence.id, 'evidence.id'), + summary: requiredString(evidence.summary, 'evidence.summary'), + createdAt: Number.isFinite(evidence.createdAt) ? evidence.createdAt : event.time, + }; +} + +/** Project and validate an ordered event stream. @beta */ +export function projectExecutionGraph(events: readonly ExecutionEvent[]): ExecutionGraphSnapshot { + const first = events[0]; + if (!first || first.type !== 'graph.created' || first.seq !== 1) { + throw invalid('execution graph must start with graph.created at sequence 1'); + } + let snapshot = createdSnapshot(first); + for (const event of events.slice(1)) { + if (event.graphId !== snapshot.id || event.seq !== snapshot.revision + 1) { + throw invalid(`invalid execution event sequence ${event.seq}`); + } + if (event.type === 'node.added') { + const raw = event.data.node; + if (!raw || typeof raw !== 'object') throw invalid('node.added requires node'); + const node = normalizeNode(raw as ExecutionNodeDefinition); + if (snapshot.nodes[node.id]) throw invalid(`duplicate execution node "${node.id}"`); + snapshot = { ...snapshot, nodes: { ...snapshot.nodes, [node.id]: node } }; + } else if (event.type.startsWith('node.') && event.type !== 'node.progressed') { + snapshot = applyNodeEvent(snapshot, event); + } else if (event.type === 'evidence.recorded') { + const evidence = parseEvidence(event); + if (!snapshot.evidence.some((item) => item.id === evidence.id)) { + const nodes = { ...snapshot.nodes }; + if (evidence.nodeId && nodes[evidence.nodeId]) { + nodes[evidence.nodeId] = { + ...nodes[evidence.nodeId], + evidenceIds: [...nodes[evidence.nodeId].evidenceIds, evidence.id], + }; + } + snapshot = { ...snapshot, nodes, evidence: [...snapshot.evidence, evidence] }; + } + } else if (event.type === 'budget.updated') { + snapshot = { ...snapshot, budget: { ...snapshot.budget, ...event.data } }; + } else if (event.type === 'verification.recorded') { + const raw = event.data.verification; + if (!raw || typeof raw !== 'object') + throw invalid('verification.recorded requires verification'); + snapshot = { ...snapshot, verification: raw as ExecutionVerification }; + } else if (event.type === 'graph.recovered') { + snapshot = { + ...snapshot, + status: 'paused_recovered', + recovery: event.data.recovery as unknown as ExecutionRecovery, + }; + } else { + const graphStatuses: Partial> = { + 'graph.ready': 'ready', + 'graph.resumed': 'running', + 'graph.paused': 'paused', + 'graph.blocked': 'blocked', + 'graph.completed': 'completed', + 'graph.failed': 'failed', + 'graph.cancelled': 'cancelled', + }; + const graphStatus = graphStatuses[event.type]; + if (graphStatus) snapshot = { ...snapshot, status: graphStatus }; + } + snapshot = { ...snapshot, revision: event.seq, updatedAt: event.time }; + } + return snapshot; +} + +export function createGraphCreatedEvent( + input: import('./execution-types.js').CreateExecutionGraphInput, + id: string +): ExecutionEvent { + return { + id, + graphId: input.id, + seq: 1, + type: 'graph.created', + time: input.now ?? Date.now(), + data: { + goal: input.goal, + ...(input.sessionId ? { sessionId: input.sessionId } : {}), + nodes: input.nodes ?? [], + policy: { ...DEFAULT_ORCHESTRATION_POLICY, ...input.policy }, + budget: input.budget ?? {}, + }, + }; +} + +export function eventStringArray(event: ExecutionEvent, field: string): readonly string[] { + return stringArray(event.data[field]); +} diff --git a/packages/moss-agent/src/orchestration/execution-recovery.ts b/packages/moss-agent/src/orchestration/execution-recovery.ts new file mode 100644 index 00000000..305cf9a4 --- /dev/null +++ b/packages/moss-agent/src/orchestration/execution-recovery.ts @@ -0,0 +1,60 @@ +import type { ExecutionGraphSnapshot, ExecutionStore } from './execution-types.js'; + +/** Fail-closed restart recovery options. @beta */ +export interface RecoverExecutionGraphOptions { + readonly now?: number; +} + +/** + * Interrupt active nodes and move an unfinished graph to `paused_recovered`. + * External mutations are never replayed by this operation. + * @beta + */ +export function recoverExecutionGraph( + store: ExecutionStore, + graphId: string, + options: RecoverExecutionGraphOptions = {} +): ExecutionGraphSnapshot { + let graph = store.load(graphId); + if (!graph) throw new Error(`unknown execution graph "${graphId}"`); + if (graph.status === 'completed' || graph.status === 'failed' || graph.status === 'cancelled') { + return graph; + } + const now = options.now ?? Date.now(); + const interruptedNodeIds: string[] = []; + const blockedMutationNodeIds: string[] = []; + for (const node of Object.values(graph.nodes)) { + if (node.status !== 'ready' && node.status !== 'leased' && node.status !== 'running') continue; + graph = store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.interrupted', + nodeId: node.id, + time: now, + data: { reason: 'host_restarted' }, + }); + interruptedNodeIds.push(node.id); + if (node.kind !== 'analysis' && node.kind !== 'verification') { + graph = store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.blocked', + nodeId: node.id, + time: now, + data: { error: 'Interrupted mutation requires explicit inspection before retry.' }, + }); + blockedMutationNodeIds.push(node.id); + } + } + return store.append(graphId, { + expectedRevision: graph.revision, + type: 'graph.recovered', + time: now, + data: { + recovery: { + recoveredAt: now, + requiresUserResume: true, + interruptedNodeIds, + blockedMutationNodeIds, + }, + }, + }); +} diff --git a/packages/moss-agent/src/orchestration/execution-types.ts b/packages/moss-agent/src/orchestration/execution-types.ts new file mode 100644 index 00000000..49503e08 --- /dev/null +++ b/packages/moss-agent/src/orchestration/execution-types.ts @@ -0,0 +1,223 @@ +/** Lifecycle of an authoritative long-running execution graph. @beta */ +export type ExecutionGraphStatus = + | 'paused' + | 'ready' + | 'running' + | 'paused_recovered' + | 'blocked' + | 'completed' + | 'failed' + | 'cancelled'; + +/** Kind of work represented by an execution node. @beta */ +export type ExecutionNodeKind = + | 'analysis' + | 'implementation' + | 'verification' + | 'merge' + | 'approval' + | 'manual'; + +/** Lifecycle of one execution node. @beta */ +export type ExecutionNodeStatus = + | 'pending' + | 'ready' + | 'leased' + | 'running' + | 'succeeded' + | 'failed' + | 'blocked' + | 'skipped' + | 'interrupted' + | 'merge_conflict' + | 'cancelled'; + +/** Explicit resource limits. Omitted values are unlimited and never hidden. @beta */ +export interface ExecutionBudget { + readonly maxTokens?: number; + readonly maxCostUsd?: number; + readonly maxWallTimeMs?: number; + readonly usedTokens?: number; + readonly usedCostUsd?: number; + readonly usedWallTimeMs?: number; +} + +/** Scheduling and completion policy for one graph. @beta */ +export interface OrchestrationPolicy { + readonly maxConcurrency: number; + readonly maxAttemptsPerNode: number; + readonly strictCompletion: boolean; + readonly autoResumeReadonly?: boolean; +} + +/** Stable definition used to create one graph node. @beta */ +export interface ExecutionNodeDefinition { + readonly id: string; + readonly kind: ExecutionNodeKind; + readonly title: string; + readonly dependencies: readonly string[]; + readonly roleId?: string; + readonly requiredCapabilities?: readonly string[]; + readonly writePaths?: readonly string[]; + readonly acceptanceCriteria?: readonly string[]; + readonly budget?: ExecutionBudget; +} + +/** Current projection of one graph node. @beta */ +export interface ExecutionNode extends ExecutionNodeDefinition { + readonly status: ExecutionNodeStatus; + readonly attempts: number; + readonly failureFingerprint?: string; + readonly consecutiveSameFailures: number; + readonly evidenceIds: readonly string[]; + readonly workspaceLeaseId?: string; + readonly startedAt?: number; + readonly completedAt?: number; + readonly error?: string; +} + +/** Evidence category retained without secret-bearing request or result bodies. @beta */ +export type ExecutionEvidenceKind = + | 'tool_result' + | 'command_exit' + | 'artifact_digest' + | 'patch' + | 'expert_claim' + | 'approval' + | 'verification' + | 'probe_receipt' + | 'citation'; + +/** One immutable evidence reference. @beta */ +export interface ExecutionEvidence { + readonly id: string; + readonly kind: ExecutionEvidenceKind; + readonly nodeId?: string; + readonly summary: string; + readonly createdAt: number; + readonly metadata?: Readonly>; + readonly artifactRef?: string; + readonly digest?: string; +} + +/** Evidence-bound graph verification verdict. @beta */ +export interface ExecutionVerification { + readonly verdict: 'verified' | 'rejected' | 'needs_evidence'; + readonly evidenceIds: readonly string[]; + readonly reasons: readonly string[]; + readonly verifiedAt: number; +} + +/** Recovery facts surfaced after a host restart. @beta */ +export interface ExecutionRecovery { + readonly recoveredAt: number; + readonly requiresUserResume: boolean; + readonly interruptedNodeIds: readonly string[]; + readonly blockedMutationNodeIds: readonly string[]; +} + +/** Current event-sourced task projection shared by all product surfaces. @beta */ +export interface ExecutionGraphSnapshot { + readonly id: string; + readonly sessionId?: string; + readonly goal: string; + readonly status: ExecutionGraphStatus; + readonly revision: number; + readonly createdAt: number; + readonly updatedAt: number; + readonly policy: OrchestrationPolicy; + readonly budget: ExecutionBudget; + readonly nodes: Readonly>; + readonly evidence: readonly ExecutionEvidence[]; + readonly verification?: ExecutionVerification; + readonly recovery?: ExecutionRecovery; +} + +/** Ordered event kinds understood by the execution graph projector. @beta */ +export type ExecutionEventType = + | 'graph.created' + | 'graph.ready' + | 'graph.resumed' + | 'graph.paused' + | 'graph.recovered' + | 'graph.blocked' + | 'graph.completed' + | 'graph.failed' + | 'graph.cancelled' + | 'plan.revised' + | 'node.added' + | 'node.ready' + | 'node.leased' + | 'node.started' + | 'node.progressed' + | 'node.succeeded' + | 'node.failed' + | 'node.interrupted' + | 'node.retry_requested' + | 'node.blocked' + | 'node.skipped' + | 'node.merge_conflict' + | 'node.cancelled' + | 'evidence.recorded' + | 'budget.updated' + | 'steering.recorded' + | 'verification.recorded'; + +/** One immutable graph fact with a monotonic graph-local sequence. @beta */ +export interface ExecutionEvent { + readonly id: string; + readonly graphId: string; + readonly seq: number; + readonly type: ExecutionEventType; + readonly time: number; + readonly nodeId?: string; + readonly data: Readonly>; +} + +/** Input for creating an execution graph. @beta */ +export interface CreateExecutionGraphInput { + readonly id: string; + readonly sessionId?: string; + readonly goal: string; + readonly nodes?: readonly ExecutionNodeDefinition[]; + readonly policy?: Partial; + readonly budget?: ExecutionBudget; + readonly now?: number; +} + +/** Compare-and-set append request. @beta */ +export interface AppendExecutionEventInput { + readonly id?: string; + readonly expectedRevision: number; + readonly type: Exclude; + readonly time?: number; + readonly nodeId?: string; + readonly data?: Readonly>; +} + +/** Renewable local ownership lease for one graph. @beta */ +export interface ExecutionOwnerLease { + readonly graphId: string; + readonly ownerId: string; + readonly token: string; + readonly acquiredAt: number; + readonly expiresAt: number; +} + +/** Owner lease acquisition options. @beta */ +export interface AcquireExecutionLeaseInput { + readonly ownerId: string; + readonly ttlMs?: number; +} + +/** Authoritative execution persistence seam. @beta */ +export interface ExecutionStore { + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + load(graphId: string): ExecutionGraphSnapshot | undefined; + list(): readonly ExecutionGraphSnapshot[]; + events(graphId: string, after?: number): readonly ExecutionEvent[]; + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; +} diff --git a/packages/moss-agent/src/orchestration/in-memory-execution-store.ts b/packages/moss-agent/src/orchestration/in-memory-execution-store.ts new file mode 100644 index 00000000..1aba23ec --- /dev/null +++ b/packages/moss-agent/src/orchestration/in-memory-execution-store.ts @@ -0,0 +1,131 @@ +import { randomUUID } from 'node:crypto'; + +import { ErrorCode, MossError } from '../errors.js'; +import { createGraphCreatedEvent, projectExecutionGraph } from './execution-projector.js'; +import type { + AcquireExecutionLeaseInput, + AppendExecutionEventInput, + CreateExecutionGraphInput, + ExecutionEvent, + ExecutionGraphSnapshot, + ExecutionOwnerLease, + ExecutionStore, +} from './execution-types.js'; + +/** Options for the instance-owned in-memory execution store. @beta */ +export interface InMemoryExecutionStoreOptions { + readonly now?: () => number; +} + +/** Deterministic in-memory adapter implementing the complete execution-store contract. @beta */ +export class InMemoryExecutionStore implements ExecutionStore { + private readonly graphs = new Map(); + private readonly eventGraphIds = new Map(); + private readonly leases = new Map(); + private readonly now: () => number; + + constructor(options: InMemoryExecutionStoreOptions = {}) { + this.now = options.now ?? Date.now; + } + + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot { + const existing = this.load(input.id); + if (existing) return existing; + const event = createGraphCreatedEvent(input, `exe_${randomUUID()}`); + this.graphs.set(input.id, [event]); + this.eventGraphIds.set(event.id, input.id); + return projectExecutionGraph([event]); + } + + load(graphId: string): ExecutionGraphSnapshot | undefined { + const events = this.graphs.get(graphId); + return events ? projectExecutionGraph(events) : undefined; + } + + list(): readonly ExecutionGraphSnapshot[] { + return [...this.graphs.values()] + .map(projectExecutionGraph) + .sort((left, right) => right.updatedAt - left.updatedAt); + } + + events(graphId: string, after = 0): readonly ExecutionEvent[] { + return (this.graphs.get(graphId) ?? []) + .filter((event) => event.seq > after) + .map((event) => ({ ...event, data: { ...event.data } })); + } + + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot { + const events = this.graphs.get(graphId); + if (!events) throw this.invalid(`unknown execution graph "${graphId}"`); + const id = input.id ?? `exe_${randomUUID()}`; + const existingGraphId = this.eventGraphIds.get(id); + if (existingGraphId === graphId) return projectExecutionGraph(events); + if (existingGraphId) throw this.invalid(`execution event "${id}" belongs to another graph`); + if (events.length !== input.expectedRevision) { + throw new MossError({ + code: ErrorCode.EXECUTION_REVISION_CONFLICT, + message: `execution graph "${graphId}" revision is ${events.length}, expected ${input.expectedRevision}`, + recoverable: true, + }); + } + const event: ExecutionEvent = { + id, + graphId, + seq: events.length + 1, + type: input.type, + time: input.time ?? this.now(), + ...(input.nodeId ? { nodeId: input.nodeId } : {}), + data: input.data ?? {}, + }; + const projected = projectExecutionGraph([...events, event]); + events.push(event); + this.eventGraphIds.set(event.id, graphId); + return projected; + } + + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease { + if (!this.graphs.has(graphId)) throw this.invalid(`unknown execution graph "${graphId}"`); + const now = this.now(); + const current = this.leases.get(graphId); + if (current && current.expiresAt > now && current.ownerId !== input.ownerId) { + throw this.leaseHeld(graphId, current.ownerId); + } + const lease: ExecutionOwnerLease = { + graphId, + ownerId: input.ownerId, + token: randomUUID(), + acquiredAt: now, + expiresAt: now + (input.ttlMs ?? 30_000), + }; + this.leases.set(graphId, lease); + return lease; + } + + renewLease(lease: ExecutionOwnerLease, ttlMs = 30_000): ExecutionOwnerLease { + const current = this.leases.get(lease.graphId); + if (!current || current.token !== lease.token) + throw this.invalid('execution lease token is stale'); + const renewed = { ...current, expiresAt: this.now() + ttlMs }; + this.leases.set(lease.graphId, renewed); + return renewed; + } + + releaseLease(graphId: string, lease: ExecutionOwnerLease): void { + const current = this.leases.get(graphId); + if (!current || current.token !== lease.token) + throw this.invalid('execution lease token is stale'); + this.leases.delete(graphId); + } + + private invalid(message: string): MossError { + return new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message }); + } + + private leaseHeld(graphId: string, ownerId: string): MossError { + return new MossError({ + code: ErrorCode.EXECUTION_LEASE_HELD, + message: `execution graph "${graphId}" is leased by "${ownerId}"`, + recoverable: true, + }); + } +} diff --git a/packages/moss-agent/src/orchestration/index.ts b/packages/moss-agent/src/orchestration/index.ts new file mode 100644 index 00000000..1162f836 --- /dev/null +++ b/packages/moss-agent/src/orchestration/index.ts @@ -0,0 +1,28 @@ +export { DEFAULT_ORCHESTRATION_POLICY, projectExecutionGraph } from './execution-projector.js'; +export { InMemoryExecutionStore } from './in-memory-execution-store.js'; +export type { InMemoryExecutionStoreOptions } from './in-memory-execution-store.js'; +export { JsonlExecutionStore } from './jsonl-execution-store.js'; +export type { JsonlExecutionStoreOptions } from './jsonl-execution-store.js'; +export { recoverExecutionGraph } from './execution-recovery.js'; +export type { RecoverExecutionGraphOptions } from './execution-recovery.js'; +export type { + AcquireExecutionLeaseInput, + AppendExecutionEventInput, + CreateExecutionGraphInput, + ExecutionBudget, + ExecutionEvent, + ExecutionEventType, + ExecutionEvidence, + ExecutionEvidenceKind, + ExecutionGraphSnapshot, + ExecutionGraphStatus, + ExecutionNode, + ExecutionNodeDefinition, + ExecutionNodeKind, + ExecutionNodeStatus, + ExecutionOwnerLease, + ExecutionRecovery, + ExecutionStore, + ExecutionVerification, + OrchestrationPolicy, +} from './execution-types.js'; diff --git a/packages/moss-agent/src/orchestration/jsonl-execution-store.ts b/packages/moss-agent/src/orchestration/jsonl-execution-store.ts new file mode 100644 index 00000000..6c5115ac --- /dev/null +++ b/packages/moss-agent/src/orchestration/jsonl-execution-store.ts @@ -0,0 +1,307 @@ +import { randomUUID } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +import lockfile from 'proper-lockfile'; + +import { ErrorCode, MossError, wrapAsMoss } from '../errors.js'; +import { createGraphCreatedEvent, projectExecutionGraph } from './execution-projector.js'; +import type { + AcquireExecutionLeaseInput, + AppendExecutionEventInput, + CreateExecutionGraphInput, + ExecutionEvent, + ExecutionGraphSnapshot, + ExecutionOwnerLease, + ExecutionStore, +} from './execution-types.js'; + +/** JSONL execution-store configuration. @beta */ +export interface JsonlExecutionStoreOptions { + readonly rootDir: string; + readonly snapshotEvery?: number; + readonly now?: () => number; +} + +interface PersistedSnapshot { + readonly revision: number; + readonly graph: ExecutionGraphSnapshot; +} + +/** Durable local JSONL adapter with cross-process CAS and renewable graph ownership. @beta */ +export class JsonlExecutionStore implements ExecutionStore { + private readonly rootDir: string; + private readonly snapshotEvery: number; + private readonly now: () => number; + + constructor(options: JsonlExecutionStoreOptions) { + this.rootDir = path.resolve(options.rootDir); + this.snapshotEvery = Math.max(1, Math.floor(options.snapshotEvery ?? 100)); + this.now = options.now ?? Date.now; + fs.mkdirSync(this.rootDir, { recursive: true, mode: 0o700 }); + } + + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot { + const graphDir = this.graphDir(input.id); + fs.mkdirSync(graphDir, { recursive: true, mode: 0o700 }); + const eventsFile = this.ensureEventsFile(input.id); + return this.withFileLock(eventsFile, () => { + const events = this.readEvents(input.id, true); + if (events.length > 0) return projectExecutionGraph(events); + const event = createGraphCreatedEvent(input, `exe_${randomUUID()}`); + const snapshot = projectExecutionGraph([event]); + this.appendLine(eventsFile, event); + this.maybeWriteSnapshot(input.id, snapshot); + return snapshot; + }); + } + + load(graphId: string): ExecutionGraphSnapshot | undefined { + const eventsFile = this.eventsFile(graphId); + if (!fs.existsSync(eventsFile)) return undefined; + return this.withFileLock(eventsFile, () => { + const events = this.readEvents(graphId, true); + return events.length > 0 ? projectExecutionGraph(events) : undefined; + }); + } + + list(): readonly ExecutionGraphSnapshot[] { + let entries: fs.Dirent[] = []; + try { + entries = fs.readdirSync(this.rootDir, { withFileTypes: true }); + } catch (error) { + throw this.storeFailure('Failed to list execution graphs', error); + } + return entries + .filter((entry) => entry.isDirectory()) + .map((entry) => this.load(entry.name)) + .filter((snapshot): snapshot is ExecutionGraphSnapshot => snapshot !== undefined) + .sort((left, right) => right.updatedAt - left.updatedAt); + } + + events(graphId: string, after = 0): readonly ExecutionEvent[] { + const eventsFile = this.eventsFile(graphId); + if (!fs.existsSync(eventsFile)) return []; + return this.withFileLock(eventsFile, () => + this.readEvents(graphId, true) + .filter((event) => event.seq > after) + .map((event) => ({ ...event, data: { ...event.data } })) + ); + } + + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot { + const eventsFile = this.eventsFile(graphId); + if (!fs.existsSync(eventsFile)) throw this.invalid(`unknown execution graph "${graphId}"`); + return this.withFileLock(eventsFile, () => { + const events = this.readEvents(graphId, true); + const id = input.id ?? `exe_${randomUUID()}`; + const duplicate = events.find((event) => event.id === id); + if (duplicate) return projectExecutionGraph(events); + if (events.length !== input.expectedRevision) { + throw new MossError({ + code: ErrorCode.EXECUTION_REVISION_CONFLICT, + message: `execution graph "${graphId}" revision is ${events.length}, expected ${input.expectedRevision}`, + recoverable: true, + }); + } + const event: ExecutionEvent = { + id, + graphId, + seq: events.length + 1, + type: input.type, + time: input.time ?? this.now(), + ...(input.nodeId ? { nodeId: input.nodeId } : {}), + data: input.data ?? {}, + }; + const snapshot = projectExecutionGraph([...events, event]); + this.appendLine(eventsFile, event); + this.maybeWriteSnapshot(graphId, snapshot); + return snapshot; + }); + } + + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease { + const eventsFile = this.eventsFile(graphId); + if (!fs.existsSync(eventsFile)) throw this.invalid(`unknown execution graph "${graphId}"`); + return this.withFileLock(eventsFile, () => { + const now = this.now(); + const current = this.readLease(graphId); + if (current && current.expiresAt > now && current.ownerId !== input.ownerId) { + throw new MossError({ + code: ErrorCode.EXECUTION_LEASE_HELD, + message: `execution graph "${graphId}" is leased by "${current.ownerId}"`, + recoverable: true, + }); + } + const lease: ExecutionOwnerLease = { + graphId, + ownerId: input.ownerId, + token: randomUUID(), + acquiredAt: now, + expiresAt: now + (input.ttlMs ?? 30_000), + }; + this.writeJsonAtomic(this.leaseFile(graphId), lease); + return lease; + }); + } + + renewLease(lease: ExecutionOwnerLease, ttlMs = 30_000): ExecutionOwnerLease { + const eventsFile = this.eventsFile(lease.graphId); + return this.withFileLock(eventsFile, () => { + const current = this.readLease(lease.graphId); + if (!current || current.token !== lease.token) + throw this.invalid('execution lease token is stale'); + const renewed = { ...current, expiresAt: this.now() + ttlMs }; + this.writeJsonAtomic(this.leaseFile(lease.graphId), renewed); + return renewed; + }); + } + + releaseLease(graphId: string, lease: ExecutionOwnerLease): void { + const eventsFile = this.eventsFile(graphId); + this.withFileLock(eventsFile, () => { + const current = this.readLease(graphId); + if (!current || current.token !== lease.token) + throw this.invalid('execution lease token is stale'); + fs.unlinkSync(this.leaseFile(graphId)); + }); + } + + private graphDir(graphId: string): string { + if (!/^[a-zA-Z0-9._-]+$/.test(graphId)) + throw this.invalid('graph id contains unsafe characters'); + return path.join(this.rootDir, graphId); + } + + private eventsFile(graphId: string): string { + return path.join(this.graphDir(graphId), 'events.jsonl'); + } + + private leaseFile(graphId: string): string { + return path.join(this.graphDir(graphId), 'owner-lease.json'); + } + + private ensureEventsFile(graphId: string): string { + const file = this.eventsFile(graphId); + fs.closeSync(fs.openSync(file, 'a', 0o600)); + return file; + } + + private readEvents(graphId: string, repairTail: boolean): ExecutionEvent[] { + const file = this.eventsFile(graphId); + let body: string; + try { + body = fs.readFileSync(file, 'utf8'); + } catch (error) { + throw this.storeFailure(`Failed to read execution graph "${graphId}"`, error); + } + const events: ExecutionEvent[] = []; + let validBytes = 0; + for (const segment of body.match(/.*(?:\n|$)/g) ?? []) { + if (!segment) continue; + const line = segment.endsWith('\n') ? segment.slice(0, -1) : segment; + if (!line.trim()) { + validBytes += Buffer.byteLength(segment); + continue; + } + try { + const event = JSON.parse(line) as ExecutionEvent; + if ( + typeof event.id !== 'string' || + event.graphId !== graphId || + event.seq !== events.length + 1 || + typeof event.type !== 'string' || + typeof event.time !== 'number' + ) { + break; + } + events.push(event); + validBytes += Buffer.byteLength(segment); + } catch { + break; + } + } + if (repairTail && validBytes < Buffer.byteLength(body)) { + const corrupt = Buffer.from(body).subarray(validBytes); + const quarantine = path.join( + this.graphDir(graphId), + `events.corrupt.${this.now()}.${randomUUID().slice(0, 8)}.jsonl` + ); + fs.writeFileSync(quarantine, corrupt, { mode: 0o600 }); + const descriptor = fs.openSync(file, 'r+'); + try { + fs.ftruncateSync(descriptor, validBytes); + fs.fsyncSync(descriptor); + } finally { + fs.closeSync(descriptor); + } + } + return events; + } + + private appendLine(file: string, event: ExecutionEvent): void { + const descriptor = fs.openSync(file, 'a', 0o600); + try { + fs.writeSync(descriptor, `${JSON.stringify(event)}\n`); + fs.fsyncSync(descriptor); + } catch (error) { + throw this.storeFailure('Failed to append execution event', error); + } finally { + fs.closeSync(descriptor); + } + } + + private maybeWriteSnapshot(graphId: string, graph: ExecutionGraphSnapshot): void { + if (graph.revision % this.snapshotEvery !== 0) return; + const snapshot: PersistedSnapshot = { revision: graph.revision, graph }; + this.writeJsonAtomic(path.join(this.graphDir(graphId), 'snapshot.json'), snapshot); + } + + private writeJsonAtomic(file: string, value: unknown): void { + const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`; + const descriptor = fs.openSync(temporary, 'wx', 0o600); + try { + fs.writeSync(descriptor, `${JSON.stringify(value)}\n`); + fs.fsyncSync(descriptor); + } finally { + fs.closeSync(descriptor); + } + fs.renameSync(temporary, file); + const directory = fs.openSync(path.dirname(file), 'r'); + try { + fs.fsyncSync(directory); + } finally { + fs.closeSync(directory); + } + } + + private readLease(graphId: string): ExecutionOwnerLease | undefined { + try { + return JSON.parse(fs.readFileSync(this.leaseFile(graphId), 'utf8')) as ExecutionOwnerLease; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; + throw this.storeFailure('Failed to read execution owner lease', error); + } + } + + private withFileLock(file: string, operation: () => T): T { + let release: (() => void) | undefined; + try { + release = lockfile.lockSync(file, { stale: 30_000, realpath: false }); + return operation(); + } catch (error) { + if (error instanceof MossError) throw error; + throw this.storeFailure('Failed to lock execution graph', error); + } finally { + release?.(); + } + } + + private invalid(message: string): MossError { + return new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message }); + } + + private storeFailure(message: string, cause: unknown): MossError { + return wrapAsMoss(cause, ErrorCode.EXECUTION_STORE_FAILED, { message, recoverable: true }); + } +} diff --git a/packages/moss-agent/test/execution-graph.spec.mjs b/packages/moss-agent/test/execution-graph.spec.mjs new file mode 100644 index 00000000..323c0b24 --- /dev/null +++ b/packages/moss-agent/test/execution-graph.spec.mjs @@ -0,0 +1,178 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { + InMemoryExecutionStore, + JsonlExecutionStore, + recoverExecutionGraph, +} from '../dist/orchestration/index.js'; + +function graphInput(id = 'graph-1') { + return { + id, + goal: 'Implement and verify a durable task', + sessionId: 'session-1', + nodes: [ + { + id: 'analyse', + kind: 'analysis', + title: 'Analyse requirements', + dependencies: [], + requiredCapabilities: ['architecture'], + acceptanceCriteria: ['claims cite evidence'], + }, + { + id: 'implement', + kind: 'implementation', + title: 'Implement change', + dependencies: ['analyse'], + requiredCapabilities: ['code'], + writePaths: ['src/'], + acceptanceCriteria: ['patch produced'], + }, + ], + policy: { maxConcurrency: 4, maxAttemptsPerNode: 3, strictCompletion: true }, + budget: { maxTokens: 20_000, maxCostUsd: 5, maxWallTimeMs: 3_600_000 }, + now: 10, + }; +} + +test('in-memory execution graph enforces revision CAS and legal node transitions', () => { + const store = new InMemoryExecutionStore(); + const created = store.create(graphInput()); + assert.equal(created.revision, 1); + assert.equal(created.status, 'paused'); + assert.equal(created.nodes.analyse.status, 'pending'); + + const ready = store.append('graph-1', { + id: 'event-ready', + expectedRevision: 1, + type: 'node.ready', + nodeId: 'analyse', + time: 20, + }); + assert.equal(ready.revision, 2); + assert.equal(ready.nodes.analyse.status, 'ready'); + + assert.throws( + () => + store.append('graph-1', { + expectedRevision: 1, + type: 'node.started', + nodeId: 'analyse', + }), + (error) => error?.code === 'EXECUTION_REVISION_CONFLICT' + ); + assert.throws( + () => + store.append('graph-1', { + expectedRevision: 2, + type: 'node.succeeded', + nodeId: 'implement', + }), + /cannot transition from pending to succeeded/ + ); + + const duplicate = store.append('graph-1', { + id: 'event-ready', + expectedRevision: 1, + type: 'node.ready', + nodeId: 'analyse', + }); + assert.equal(duplicate.revision, 2); +}); + +test('owner lease excludes a second local runtime and expires deterministically', () => { + let now = 100; + const store = new InMemoryExecutionStore({ now: () => now }); + store.create(graphInput()); + const first = store.acquireLease('graph-1', { ownerId: 'cli', ttlMs: 30 }); + assert.equal(first.ownerId, 'cli'); + assert.throws( + () => store.acquireLease('graph-1', { ownerId: 'web', ttlMs: 30 }), + (error) => error?.code === 'EXECUTION_LEASE_HELD' + ); + now = 131; + const second = store.acquireLease('graph-1', { ownerId: 'web', ttlMs: 30 }); + assert.equal(second.ownerId, 'web'); + assert.throws(() => store.releaseLease('graph-1', first), /lease token/); + store.releaseLease('graph-1', second); +}); + +test('JSONL store persists CAS across instances, snapshots, and repairs only a corrupt tail', () => { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-execution-graph-')); + try { + const first = new JsonlExecutionStore({ rootDir, snapshotEvery: 2 }); + first.create(graphInput()); + first.append('graph-1', { + expectedRevision: 1, + type: 'node.ready', + nodeId: 'analyse', + time: 20, + }); + + const second = new JsonlExecutionStore({ rootDir, snapshotEvery: 2 }); + assert.equal(second.load('graph-1')?.revision, 2); + assert.throws( + () => + second.append('graph-1', { + expectedRevision: 1, + type: 'node.started', + nodeId: 'analyse', + }), + (error) => error?.code === 'EXECUTION_REVISION_CONFLICT' + ); + second.append('graph-1', { + expectedRevision: 2, + type: 'node.started', + nodeId: 'analyse', + time: 30, + }); + + const graphDir = path.join(rootDir, 'graph-1'); + assert.ok(fs.existsSync(path.join(graphDir, 'snapshot.json'))); + fs.appendFileSync(path.join(graphDir, 'events.jsonl'), '{truncated'); + + const recovered = new JsonlExecutionStore({ rootDir, snapshotEvery: 2 }); + assert.equal(recovered.load('graph-1')?.revision, 3); + assert.equal(recovered.load('graph-1')?.nodes.analyse.status, 'running'); + assert.ok( + fs.readdirSync(graphDir).some((name) => name.startsWith('events.corrupt.')), + 'invalid tail should be quarantined for inspection' + ); + } finally { + fs.rmSync(rootDir, { recursive: true, force: true }); + } +}); + +test('restart recovery is fail-closed for active work', () => { + const store = new InMemoryExecutionStore(); + let graph = store.create(graphInput()); + graph = store.append(graph.id, { + expectedRevision: graph.revision, + type: 'graph.resumed', + time: 20, + }); + graph = store.append(graph.id, { + expectedRevision: graph.revision, + type: 'node.ready', + nodeId: 'analyse', + time: 30, + }); + graph = store.append(graph.id, { + expectedRevision: graph.revision, + type: 'node.started', + nodeId: 'analyse', + time: 40, + }); + + const recovered = recoverExecutionGraph(store, graph.id, { now: 50 }); + assert.equal(recovered.status, 'paused_recovered'); + assert.equal(recovered.nodes.analyse.status, 'interrupted'); + assert.equal(recovered.recovery?.requiresUserResume, true); + assert.equal(recovered.recovery?.interruptedNodeIds[0], 'analyse'); +}); diff --git a/scripts/config/api-entrypoints.json b/scripts/config/api-entrypoints.json index 88e5d923..7047f3ac 100644 --- a/scripts/config/api-entrypoints.json +++ b/scripts/config/api-entrypoints.json @@ -173,6 +173,11 @@ "report": "runtime", "stability": "public" }, + "./orchestration": { + "types": "./dist/orchestration/index.d.ts", + "report": "orchestration", + "stability": "beta" + }, "./web": { "types": "./dist/web-ui/index.d.ts", "report": "web", From 0b36b4de8ce14bd807497976c0d1f86de96feaf6 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 17:29:39 +0800 Subject: [PATCH 03/21] feat(agent): schedule isolated implementation work --- .autopilot/PROGRESS.md | 14 +- .autopilot/config.sh | 2 +- .autopilot/state | 2 +- .autopilot/task.md | 4 +- .../unify-long-horizon-execution/tasks.md | 6 +- packages/moss-agent/src/index.ts | 18 ++ .../src/orchestration/copy-workspace-lease.ts | 58 ++++ .../execution-graph-scheduler.ts | 304 ++++++++++++++++++ .../src/orchestration/execution-projector.ts | 17 +- .../src/orchestration/execution-types.ts | 21 ++ .../git-worktree-workspace-lease.ts | 96 ++++++ .../moss-agent/src/orchestration/index.ts | 22 ++ .../orchestration/workspace-lease-adapter.ts | 289 +++++++++++++++++ .../orchestration/workspace-lease-files.ts | 125 +++++++ .../orchestration/workspace-lease-types.ts | 75 +++++ .../test/execution-graph-scheduler.spec.mjs | 152 +++++++++ .../moss-agent/test/workspace-lease.spec.mjs | 209 ++++++++++++ 17 files changed, 1400 insertions(+), 14 deletions(-) create mode 100644 packages/moss-agent/src/orchestration/copy-workspace-lease.ts create mode 100644 packages/moss-agent/src/orchestration/execution-graph-scheduler.ts create mode 100644 packages/moss-agent/src/orchestration/git-worktree-workspace-lease.ts create mode 100644 packages/moss-agent/src/orchestration/workspace-lease-adapter.ts create mode 100644 packages/moss-agent/src/orchestration/workspace-lease-files.ts create mode 100644 packages/moss-agent/src/orchestration/workspace-lease-types.ts create mode 100644 packages/moss-agent/test/execution-graph-scheduler.spec.mjs create mode 100644 packages/moss-agent/test/workspace-lease.spec.mjs diff --git a/.autopilot/PROGRESS.md b/.autopilot/PROGRESS.md index 375596cc..8f10e3fe 100644 --- a/.autopilot/PROGRESS.md +++ b/.autopilot/PROGRESS.md @@ -2,15 +2,17 @@ ## 当前 -- 下一项:T2 DAG scheduler and recovery policy -- 最近绿 tag:autopilot/unify-long-horizon-green-000 -- 备注:执行图内存/JSONL adapter、CAS、owner lease、尾损恢复与 fail-closed restart 已转绿。 +- 下一项:T4 Expert routing, synthesis, and completion arbiter +- 最近绿 tag:autopilot/unify-long-horizon-green-001 +- 备注:DAG 并发、预算/熔断和 Git/copy workspace lease 已通过真实文件与进程测试。 ## 棒日志 -| 棒 | 时间 | 认领项 | 门禁 | 证据 | 备注 | -| --- | ---------- | --------------------------------------- | ----- | ------------------------------- | ----------------- | -| 001 | 2026-08-18 | T1 Execution Graph contracts and stores | GREEN | `test/execution-graph.spec.mjs` | 4/4 focused cases | +| 棒 | 时间 | 认领项 | 门禁 | 证据 | 备注 | +| --- | ---------- | ------------------------------------------ | ----- | ----------------------------------------- | ----------------- | +| 001 | 2026-08-18 | T1 Execution Graph contracts and stores | GREEN | `test/execution-graph.spec.mjs` | 4/4 focused cases | +| 002 | 2026-08-18 | T2 DAG scheduler and recovery policy | GREEN | `test/execution-graph-scheduler.spec.mjs` | 5/5 focused cases | +| 003 | 2026-08-18 | T3 Workspace Lease and guarded patch merge | GREEN | `test/workspace-lease.spec.mjs` | 5/5 focused cases | ## 仲裁队列 diff --git a/.autopilot/config.sh b/.autopilot/config.sh index c083dcb9..fe84dcc6 100644 --- a/.autopilot/config.sh +++ b/.autopilot/config.sh @@ -1,5 +1,5 @@ BUILD_CMD="npm run build -w @rdk-moss/agent" -TEST_CMD="npm run test:filter -w @rdk-moss/agent -- --filter execution-graph" +TEST_CMD="npm run test:filter -w @rdk-moss/agent -- --filter execution-graph --filter workspace-lease" FULL_CMD="npm run verify" FULL_EVERY=2 CONTRACT_PATHS="openspec/changes/unify-long-horizon-execution/" diff --git a/.autopilot/state b/.autopilot/state index 1c37d962..abcdfdea 100644 --- a/.autopilot/state +++ b/.autopilot/state @@ -1,5 +1,5 @@ STATUS=RUNNING -SPRINT=1 +SPRINT=3 CONSEC_RED=0 SAME_ERR_COUNT=0 LAST_ERROR_SIG= diff --git a/.autopilot/task.md b/.autopilot/task.md index 3d79d7e1..519e5c02 100644 --- a/.autopilot/task.md +++ b/.autopilot/task.md @@ -24,8 +24,8 @@ ## 4. Checklist - [x] T1 Execution Graph contracts and stores -- [ ] T2 DAG scheduler and recovery policy -- [ ] T3 Workspace Lease and guarded patch merge +- [x] T2 DAG scheduler and recovery policy +- [x] T3 Workspace Lease and guarded patch merge - [ ] T4 Expert routing, synthesis, and completion arbiter - [ ] T5 MossAgent, plugin, CLI/Web/TUI/ACP integration and migration - [ ] T6 public API, docs, benchmarks, full verification, merge, push, CI diff --git a/openspec/changes/unify-long-horizon-execution/tasks.md b/openspec/changes/unify-long-horizon-execution/tasks.md index 551eee42..95b13ed1 100644 --- a/openspec/changes/unify-long-horizon-execution/tasks.md +++ b/openspec/changes/unify-long-horizon-execution/tasks.md @@ -7,9 +7,9 @@ ## 2. Scheduling and isolation -- [ ] 2.1 Implement dependency-driven readiness, concurrency, budgets, retries, and failure propagation. -- [ ] 2.2 Implement Git worktree and copy-snapshot workspace lease adapters. -- [ ] 2.3 Produce patches, digests, verification evidence, guarded merge, and retained recovery. +- [x] 2.1 Implement dependency-driven readiness, concurrency, budgets, retries, and failure propagation. +- [x] 2.2 Implement Git worktree and copy-snapshot workspace lease adapters. +- [x] 2.3 Produce patches, digests, verification evidence, guarded merge, and retained recovery. ## 3. Expert team and completion diff --git a/packages/moss-agent/src/index.ts b/packages/moss-agent/src/index.ts index fc0f55dc..13e602c2 100644 --- a/packages/moss-agent/src/index.ts +++ b/packages/moss-agent/src/index.ts @@ -191,15 +191,21 @@ export type { } from './core/index.js'; export { DEFAULT_ORCHESTRATION_POLICY, + CopyWorkspaceLeaseAdapter, + ExecutionGraphScheduler, + GitWorktreeWorkspaceLeaseAdapter, InMemoryExecutionStore, JsonlExecutionStore, projectExecutionGraph, recoverExecutionGraph, + normalizeWritePath, + writePathsOverlap, } from './orchestration/index.js'; export type { AcquireExecutionLeaseInput, AppendExecutionEventInput, CreateExecutionGraphInput, + CreateWorkspaceLeaseInput, ExecutionBudget, ExecutionEvent, ExecutionEventType, @@ -209,16 +215,28 @@ export type { ExecutionGraphStatus, ExecutionNode, ExecutionNodeDefinition, + ExecutionNodeExecutor, ExecutionNodeKind, + ExecutionNodeRunResult, ExecutionNodeStatus, ExecutionOwnerLease, ExecutionRecovery, + ExecutionScheduleResult, ExecutionStore, ExecutionVerification, InMemoryExecutionStoreOptions, JsonlExecutionStoreOptions, OrchestrationPolicy, RecoverExecutionGraphOptions, + WorkspaceLease, + WorkspaceLeaseAdapter, + WorkspaceLeaseAdapterOptions, + WorkspaceLeaseKind, + WorkspaceLeaseReleaseReason, + WorkspaceLeaseStatus, + WorkspaceMergeResult, + WorkspaceMergeAuthorizationRequest, + WorkspacePatch, } from './orchestration/index.js'; export type { JsonlSessionStoreConfig } from './core/index.js'; export type { SessionStore, SessionMeta } from './core/index.js'; diff --git a/packages/moss-agent/src/orchestration/copy-workspace-lease.ts b/packages/moss-agent/src/orchestration/copy-workspace-lease.ts new file mode 100644 index 00000000..8cf5f4b3 --- /dev/null +++ b/packages/moss-agent/src/orchestration/copy-workspace-lease.ts @@ -0,0 +1,58 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { ErrorCode, MossError } from '../errors.js'; +import { BaseWorkspaceLeaseAdapter } from './workspace-lease-adapter.js'; +import { copyWorkspaceEntry } from './workspace-lease-files.js'; +import type { + CreateWorkspaceLeaseInput, + WorkspaceLease, + WorkspaceLeaseAdapterOptions, +} from './workspace-lease-types.js'; + +/** Non-Git and unborn-repository fallback backed by a filtered copy snapshot. @beta */ +export class CopyWorkspaceLeaseAdapter extends BaseWorkspaceLeaseAdapter { + constructor(options: WorkspaceLeaseAdapterOptions) { + super('copy-snapshot', options); + } + + async create(input: CreateWorkspaceLeaseInput): Promise { + if (this.load(input.id)) throw this.invalid(`workspace lease already exists: ${input.id}`); + const parentWorkspace = path.resolve(input.parentWorkspace); + const leaseDir = this.leaseDirectory(input.id); + const workspacePath = path.join(leaseDir, 'workspace'); + fs.mkdirSync(workspacePath, { recursive: true, mode: 0o700 }); + try { + for (const entry of fs.readdirSync(parentWorkspace)) { + copyWorkspaceEntry(parentWorkspace, workspacePath, entry); + } + await this.git(workspacePath, ['init']); + await this.git(workspacePath, ['add', '-A']); + await this.git( + workspacePath, + ['commit', '--allow-empty', '-m', 'moss workspace lease baseline'], + undefined, + this.commitEnvironment() + ); + const base = await this.git(workspacePath, ['rev-parse', 'HEAD']); + const lease = this.createManifest( + { ...input, parentWorkspace }, + workspacePath, + base.stdout.trim() + ); + this.writeManifest(lease); + return lease; + } catch (error) { + fs.rmSync(leaseDir, { recursive: true, force: true }); + throw error; + } + } + + protected removeWorkspace(_lease: WorkspaceLease): Promise { + return Promise.resolve(); + } + + private invalid(message: string): MossError { + return new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message }); + } +} diff --git a/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts b/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts new file mode 100644 index 00000000..a66e7e01 --- /dev/null +++ b/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts @@ -0,0 +1,304 @@ +import path from 'node:path'; + +import { ErrorCode, MossError, errorMessage } from '../errors.js'; +import type { + ExecutionEvidence, + ExecutionGraphSnapshot, + ExecutionNode, + ExecutionNodeExecutor, + ExecutionScheduleResult, + ExecutionStore, +} from './execution-types.js'; + +const DEPENDENCY_FAILURES = new Set([ + 'failed', + 'blocked', + 'cancelled', + 'merge_conflict', + 'interrupted', +]); + +/** Normalize one declared write path without resolving it outside the workspace. @beta */ +export function normalizeWritePath(value: string): string { + const portable = value.trim().replaceAll('\\', '/'); + if (!portable || portable.startsWith('/') || /^[a-zA-Z]:\//.test(portable)) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `write path "${value}" must be workspace-relative`, + }); + } + const normalized = path.posix.normalize(portable).replace(/^\.\//, '').replace(/\/$/, ''); + if (normalized === '..' || normalized.startsWith('../')) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `write path "${value}" must be workspace-relative`, + }); + } + return normalized || '.'; +} + +/** Return whether two declared write-path sets overlap by exact or ancestor ownership. @beta */ +export function writePathsOverlap( + leftPaths: readonly string[], + rightPaths: readonly string[] +): boolean { + const left = leftPaths.map(normalizeWritePath); + const right = rightPaths.map(normalizeWritePath); + return left.some((a) => + right.some( + (b) => a === '.' || b === '.' || a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`) + ) + ); +} + +function budgetReason(graph: ExecutionGraphSnapshot): string | undefined { + const budget = graph.budget; + if (budget.maxTokens !== undefined && (budget.usedTokens ?? 0) >= budget.maxTokens) { + return 'token budget exhausted'; + } + if (budget.maxCostUsd !== undefined && (budget.usedCostUsd ?? 0) >= budget.maxCostUsd) { + return 'cost budget exhausted'; + } + if (budget.maxWallTimeMs !== undefined && (budget.usedWallTimeMs ?? 0) >= budget.maxWallTimeMs) { + return 'wall-time budget exhausted'; + } + return undefined; +} + +/** Instance-owned dependency scheduler for one or more execution graphs. @beta */ +export class ExecutionGraphScheduler { + constructor(private readonly store: ExecutionStore) {} + + reconcile(graphId: string): ExecutionGraphSnapshot { + let graph = this.requiredGraph(graphId); + const exhausted = budgetReason(graph); + if (exhausted) { + if (graph.status !== 'paused') { + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'graph.paused', + data: { reason: exhausted, budget: graph.budget }, + }); + } else { + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'graph.paused', + data: { reason: exhausted, budget: graph.budget }, + }); + } + return graph; + } + + let changed = true; + while (changed) { + changed = false; + for (const node of Object.values(graph.nodes)) { + const dependencies = node.dependencies.map((id) => graph.nodes[id]); + const failedDependencies = dependencies + .filter((dependency) => DEPENDENCY_FAILURES.has(dependency.status)) + .map((dependency) => dependency.id); + const allSatisfied = dependencies.every( + (dependency) => dependency.status === 'succeeded' || dependency.status === 'skipped' + ); + + if ( + failedDependencies.length > 0 && + (node.status === 'pending' || node.status === 'ready') + ) { + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.blocked', + nodeId: node.id, + data: { + error: `Blocked by dependencies: ${failedDependencies.join(', ')}`, + blockedByDependencies: failedDependencies, + }, + }); + changed = true; + break; + } + if ( + allSatisfied && + (node.status === 'pending' || + (node.status === 'blocked' && (node.blockedByDependencies?.length ?? 0) > 0)) + ) { + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.ready', + nodeId: node.id, + }); + changed = true; + break; + } + } + } + return graph; + } + + selectRunnable(graph: ExecutionGraphSnapshot): readonly ExecutionNode[] { + if (budgetReason(graph)) return []; + if (graph.status === 'blocked' || graph.status === 'completed' || graph.status === 'failed') { + return []; + } + const active = Object.values(graph.nodes).filter( + (node) => node.status === 'leased' || node.status === 'running' + ); + const selected: ExecutionNode[] = []; + const available = Math.max(0, graph.policy.maxConcurrency - active.length); + for (const node of Object.values(graph.nodes)) { + if (node.status !== 'ready' || selected.length >= available) continue; + const writePaths = node.writePaths ?? []; + const conflicts = [...active, ...selected].some((other) => { + const otherPaths = other.writePaths ?? []; + return ( + writePaths.length > 0 && + otherPaths.length > 0 && + writePathsOverlap(writePaths, otherPaths) + ); + }); + if (!conflicts) selected.push(node); + } + return selected; + } + + startNode(graphId: string, nodeId: string): ExecutionGraphSnapshot { + let graph = this.reconcile(graphId); + let node = this.requiredNode(graph, nodeId); + if (node.status === 'failed' || node.status === 'interrupted') { + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.retry_requested', + nodeId, + }); + node = this.requiredNode(graph, nodeId); + } + if (node.status !== 'ready' && node.status !== 'leased') { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `node "${nodeId}" is not runnable from ${node.status}`, + }); + } + if (graph.status !== 'running') { + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'graph.resumed', + }); + } + return this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.started', + nodeId, + }); + } + + succeedNode( + graphId: string, + nodeId: string, + evidence: readonly ExecutionEvidence[] = [] + ): ExecutionGraphSnapshot { + let graph = this.requiredGraph(graphId); + for (const item of evidence) { + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'evidence.recorded', + nodeId, + data: { evidence: { ...item, nodeId: item.nodeId ?? nodeId } }, + }); + } + return this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.succeeded', + nodeId, + }); + } + + failNode( + graphId: string, + nodeId: string, + failure: { readonly error: string; readonly failureFingerprint?: string } + ): ExecutionGraphSnapshot { + let graph = this.requiredGraph(graphId); + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.failed', + nodeId, + data: { + error: failure.error, + ...(failure.failureFingerprint ? { failureFingerprint: failure.failureFingerprint } : {}), + }, + }); + const node = this.requiredNode(graph, nodeId); + if (node.attempts >= graph.policy.maxAttemptsPerNode || node.consecutiveSameFailures >= 3) { + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.blocked', + nodeId, + data: { error: failure.error, reason: 'retry_limit_reached' }, + }); + } + return graph; + } + + async runAvailable( + graphId: string, + executor: ExecutionNodeExecutor + ): Promise { + let graph = this.reconcile(graphId); + const candidates = this.selectRunnable(graph); + const started: ExecutionNode[] = []; + for (const candidate of candidates) { + graph = this.startNode(graphId, candidate.id); + started.push(this.requiredNode(graph, candidate.id)); + } + const settled = await Promise.all( + started.map(async (node) => { + try { + return { node, result: await executor(node, graph) }; + } catch (error) { + return { + node, + result: { + success: false, + error: errorMessage(error), + failureFingerprint: `executor:${error instanceof Error ? error.name : 'unknown'}`, + }, + }; + } + }) + ); + for (const item of settled) { + graph = item.result.success + ? this.succeedNode(graphId, item.node.id, item.result.evidence) + : this.failNode(graphId, item.node.id, { + error: item.result.error ?? 'Node execution failed', + ...(item.result.failureFingerprint + ? { failureFingerprint: item.result.failureFingerprint } + : {}), + }); + } + graph = this.reconcile(graphId); + return { graph, startedNodeIds: started.map((node) => node.id) }; + } + + private requiredGraph(graphId: string): ExecutionGraphSnapshot { + const graph = this.store.load(graphId); + if (!graph) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `unknown execution graph "${graphId}"`, + }); + } + return graph; + } + + private requiredNode(graph: ExecutionGraphSnapshot, nodeId: string): ExecutionNode { + const node = graph.nodes[nodeId]; + if (!node) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `unknown execution node "${nodeId}"`, + }); + } + return node; + } +} diff --git a/packages/moss-agent/src/orchestration/execution-projector.ts b/packages/moss-agent/src/orchestration/execution-projector.ts index 34875a61..0e669d88 100644 --- a/packages/moss-agent/src/orchestration/execution-projector.ts +++ b/packages/moss-agent/src/orchestration/execution-projector.ts @@ -21,7 +21,7 @@ export const DEFAULT_ORCHESTRATION_POLICY: OrchestrationPolicy = { }; const NODE_TRANSITIONS: Readonly> = { - 'node.ready': ['pending', 'interrupted', 'failed'], + 'node.ready': ['pending', 'interrupted', 'failed', 'blocked'], 'node.leased': ['ready'], 'node.started': ['ready', 'leased'], 'node.succeeded': ['running'], @@ -103,6 +103,17 @@ function createdSnapshot(event: ExecutionEvent): ExecutionGraphSnapshot { if (dependency === node.id) throw invalid(`node "${node.id}" cannot depend on itself`); } } + const visiting = new Set(); + const visited = new Set(); + const visit = (nodeId: string): void => { + if (visiting.has(nodeId)) throw invalid(`dependency cycle includes node "${nodeId}"`); + if (visited.has(nodeId)) return; + visiting.add(nodeId); + for (const dependency of nodes[nodeId].dependencies) visit(dependency); + visiting.delete(nodeId); + visited.add(nodeId); + }; + for (const nodeId of Object.keys(nodes)) visit(nodeId); return { id: event.graphId, ...(typeof event.data.sessionId === 'string' ? { sessionId: event.data.sessionId } : {}), @@ -178,6 +189,10 @@ function applyNodeEvent( ...(typeof event.data.workspaceLeaseId === 'string' ? { workspaceLeaseId: event.data.workspaceLeaseId } : {}), + ...(event.type === 'node.blocked' + ? { blockedByDependencies: stringArray(event.data.blockedByDependencies) } + : {}), + ...(event.type === 'node.ready' ? { blockedByDependencies: undefined, error: undefined } : {}), ...(event.type === 'node.started' ? { startedAt: event.time } : {}), ...(event.type === 'node.succeeded' || event.type === 'node.skipped' ? { completedAt: event.time } diff --git a/packages/moss-agent/src/orchestration/execution-types.ts b/packages/moss-agent/src/orchestration/execution-types.ts index 49503e08..1d232eab 100644 --- a/packages/moss-agent/src/orchestration/execution-types.ts +++ b/packages/moss-agent/src/orchestration/execution-types.ts @@ -71,6 +71,7 @@ export interface ExecutionNode extends ExecutionNodeDefinition { readonly consecutiveSameFailures: number; readonly evidenceIds: readonly string[]; readonly workspaceLeaseId?: string; + readonly blockedByDependencies?: readonly string[]; readonly startedAt?: number; readonly completedAt?: number; readonly error?: string; @@ -221,3 +222,23 @@ export interface ExecutionStore { renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; releaseLease(graphId: string, lease: ExecutionOwnerLease): void; } + +/** Result returned by one scheduler-managed node execution. @beta */ +export interface ExecutionNodeRunResult { + readonly success: boolean; + readonly evidence?: readonly ExecutionEvidence[]; + readonly error?: string; + readonly failureFingerprint?: string; +} + +/** Executor callback used by {@link ExecutionGraphScheduler}. @beta */ +export type ExecutionNodeExecutor = ( + node: ExecutionNode, + graph: ExecutionGraphSnapshot +) => Promise; + +/** Outcome of one scheduler admission cycle. @beta */ +export interface ExecutionScheduleResult { + readonly graph: ExecutionGraphSnapshot; + readonly startedNodeIds: readonly string[]; +} diff --git a/packages/moss-agent/src/orchestration/git-worktree-workspace-lease.ts b/packages/moss-agent/src/orchestration/git-worktree-workspace-lease.ts new file mode 100644 index 00000000..062453b4 --- /dev/null +++ b/packages/moss-agent/src/orchestration/git-worktree-workspace-lease.ts @@ -0,0 +1,96 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { ErrorCode, MossError } from '../errors.js'; +import { BaseWorkspaceLeaseAdapter } from './workspace-lease-adapter.js'; +import { copyWorkspaceEntry, isExcludedWorkspacePath } from './workspace-lease-files.js'; +import type { + CreateWorkspaceLeaseInput, + WorkspaceLease, + WorkspaceLeaseAdapterOptions, +} from './workspace-lease-types.js'; + +/** Git worktree adapter seeded with tracked, staged, unstaged, and safe untracked content. @beta */ +export class GitWorktreeWorkspaceLeaseAdapter extends BaseWorkspaceLeaseAdapter { + constructor(options: WorkspaceLeaseAdapterOptions) { + super('git-worktree', options); + } + + async create(input: CreateWorkspaceLeaseInput): Promise { + if (this.load(input.id)) throw this.invalid(`workspace lease already exists: ${input.id}`); + const parentWorkspace = path.resolve(input.parentWorkspace); + const inside = await this.git(parentWorkspace, ['rev-parse', '--is-inside-work-tree']); + if (inside.stdout.trim() !== 'true') + throw this.invalid('parent workspace is not a Git worktree'); + const head = await this.git(parentWorkspace, ['rev-parse', '--verify', 'HEAD']); + const leaseDir = this.leaseDirectory(input.id); + const workspacePath = path.join(leaseDir, 'workspace'); + fs.mkdirSync(leaseDir, { recursive: true, mode: 0o700 }); + try { + await this.git(parentWorkspace, [ + 'worktree', + 'add', + '--detach', + workspacePath, + head.stdout.trim(), + ]); + const tracked = await this.git(parentWorkspace, ['ls-files', '-z']); + const untracked = await this.git(parentWorkspace, [ + 'ls-files', + '--others', + '--exclude-standard', + '-z', + ]); + for (const relative of [...tracked.stdout.split('\0'), ...untracked.stdout.split('\0')]) { + if (!relative || isExcludedWorkspacePath(relative)) continue; + const source = path.join(parentWorkspace, relative); + try { + fs.lstatSync(source); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue; + throw error; + } + copyWorkspaceEntry(parentWorkspace, workspacePath, relative); + } + this.removeExcludedRoots(workspacePath); + await this.git(workspacePath, ['add', '-A']); + await this.git( + workspacePath, + ['commit', '--allow-empty', '-m', 'moss workspace lease baseline'], + undefined, + this.commitEnvironment() + ); + const base = await this.git(workspacePath, ['rev-parse', 'HEAD']); + const lease = this.createManifest( + { ...input, parentWorkspace }, + workspacePath, + base.stdout.trim() + ); + this.writeManifest(lease); + return lease; + } catch (error) { + try { + await this.git(parentWorkspace, ['worktree', 'remove', '--force', workspacePath]); + } catch {} + fs.rmSync(leaseDir, { recursive: true, force: true }); + throw error; + } + } + + protected async removeWorkspace(lease: WorkspaceLease): Promise { + if (!fs.existsSync(lease.workspacePath)) return; + await this.git(lease.parentWorkspace, ['worktree', 'remove', '--force', lease.workspacePath]); + } + + private removeExcludedRoots(workspacePath: string): void { + for (const entry of fs.readdirSync(workspacePath)) { + if (entry === '.git') continue; + if (!isExcludedWorkspacePath(entry)) continue; + fs.rmSync(path.join(workspacePath, entry), { recursive: true, force: true }); + } + } + + private invalid(message: string): MossError { + return new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message }); + } +} diff --git a/packages/moss-agent/src/orchestration/index.ts b/packages/moss-agent/src/orchestration/index.ts index 1162f836..c684a111 100644 --- a/packages/moss-agent/src/orchestration/index.ts +++ b/packages/moss-agent/src/orchestration/index.ts @@ -5,6 +5,13 @@ export { JsonlExecutionStore } from './jsonl-execution-store.js'; export type { JsonlExecutionStoreOptions } from './jsonl-execution-store.js'; export { recoverExecutionGraph } from './execution-recovery.js'; export type { RecoverExecutionGraphOptions } from './execution-recovery.js'; +export { + ExecutionGraphScheduler, + normalizeWritePath, + writePathsOverlap, +} from './execution-graph-scheduler.js'; +export { GitWorktreeWorkspaceLeaseAdapter } from './git-worktree-workspace-lease.js'; +export { CopyWorkspaceLeaseAdapter } from './copy-workspace-lease.js'; export type { AcquireExecutionLeaseInput, AppendExecutionEventInput, @@ -19,10 +26,25 @@ export type { ExecutionNode, ExecutionNodeDefinition, ExecutionNodeKind, + ExecutionNodeExecutor, + ExecutionNodeRunResult, ExecutionNodeStatus, ExecutionOwnerLease, ExecutionRecovery, + ExecutionScheduleResult, ExecutionStore, ExecutionVerification, OrchestrationPolicy, } from './execution-types.js'; +export type { + CreateWorkspaceLeaseInput, + WorkspaceLease, + WorkspaceLeaseAdapter, + WorkspaceLeaseAdapterOptions, + WorkspaceLeaseKind, + WorkspaceLeaseReleaseReason, + WorkspaceLeaseStatus, + WorkspaceMergeResult, + WorkspaceMergeAuthorizationRequest, + WorkspacePatch, +} from './workspace-lease-types.js'; diff --git a/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts b/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts new file mode 100644 index 00000000..cdb95f71 --- /dev/null +++ b/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts @@ -0,0 +1,289 @@ +import { createHash, randomUUID } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { ErrorCode, MossError, wrapAsMoss } from '../errors.js'; +import { runProcess } from '../utils/run-process.js'; +import { normalizeWritePath, writePathsOverlap } from './execution-graph-scheduler.js'; +import { assertLeasePath, captureBaselineHashes, hashFile } from './workspace-lease-files.js'; +import { isExcludedWorkspacePath } from './workspace-lease-files.js'; +import type { + CreateWorkspaceLeaseInput, + WorkspaceLease, + WorkspaceLeaseAdapter, + WorkspaceLeaseAdapterOptions, + WorkspaceLeaseKind, + WorkspaceLeaseReleaseReason, + WorkspaceMergeResult, + WorkspacePatch, +} from './workspace-lease-types.js'; + +const PROCESS_TIMEOUT_MS = 120_000; + +function processEnvironment(): Record { + return Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined) + ); +} + +function parseNullSeparated(value: string): string[] { + return value.split('\0').filter(Boolean).map(normalizeWritePath).sort(); +} + +export abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { + protected readonly rootDir: string; + protected readonly now: () => number; + private readonly authorizeMerge: WorkspaceLeaseAdapterOptions['authorizeMerge']; + + protected constructor( + private readonly kind: WorkspaceLeaseKind, + options: WorkspaceLeaseAdapterOptions + ) { + this.rootDir = path.resolve(options.rootDir); + this.now = options.now ?? Date.now; + this.authorizeMerge = options.authorizeMerge; + fs.mkdirSync(this.rootDir, { recursive: true, mode: 0o700 }); + } + + abstract create(input: CreateWorkspaceLeaseInput): Promise; + + load(leaseId: string): WorkspaceLease | undefined { + try { + return JSON.parse(fs.readFileSync(this.manifestFile(leaseId), 'utf8')) as WorkspaceLease; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined; + throw this.failure(`Failed to load workspace lease "${leaseId}"`, error); + } + } + + list(): readonly WorkspaceLease[] { + return fs + .readdirSync(this.rootDir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => this.load(entry.name)) + .filter((lease): lease is WorkspaceLease => lease !== undefined) + .sort((left, right) => right.updatedAt - left.updatedAt); + } + + async createPatch(lease: WorkspaceLease): Promise { + this.assertCompatible(lease); + const loaded = this.requiredLease(lease.id); + await this.git(loaded.workspacePath, ['add', '-A']); + const names = await this.git(loaded.workspacePath, [ + 'diff', + '--cached', + '--name-only', + '-z', + loaded.baseRef, + '--', + '.', + ]); + const changedPaths = parseNullSeparated(names.stdout); + const excluded = changedPaths.filter(isExcludedWorkspacePath); + if (excluded.length > 0) { + throw new MossError({ + code: ErrorCode.TOOL_NOT_ALLOWED, + message: `workspace changes include excluded secret or runtime paths: ${excluded.join(', ')}`, + }); + } + const outside = changedPaths.filter( + (changed) => !loaded.writePaths.some((declared) => writePathsOverlap([changed], [declared])) + ); + if (outside.length > 0) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `workspace changes outside declared write paths: ${outside.join(', ')}`, + }); + } + const diff = await this.git(loaded.workspacePath, [ + 'diff', + '--cached', + '--binary', + '--full-index', + loaded.baseRef, + '--', + '.', + ]); + return { + id: `patch_${randomUUID()}`, + leaseId: loaded.id, + patch: diff.stdout, + digest: `sha256:${createHash('sha256').update(diff.stdout).digest('hex')}`, + changedPaths, + createdAt: this.now(), + }; + } + + async merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise { + this.assertCompatible(lease); + const loaded = this.requiredLease(lease.id); + if (patch.leaseId !== loaded.id) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `patch "${patch.id}" belongs to another workspace lease`, + }); + } + const digest = `sha256:${createHash('sha256').update(patch.patch).digest('hex')}`; + if (digest !== patch.digest) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `patch "${patch.id}" digest does not match its contents`, + }); + } + const conflictingPaths = patch.changedPaths.filter((relative) => { + const current = hashFile(path.join(loaded.parentWorkspace, ...relative.split('/'))); + return current !== (loaded.baselineHashes[relative] ?? null); + }); + if (conflictingPaths.length > 0) { + return { status: 'merge_conflict', patchId: patch.id, conflictingPaths }; + } + if (this.authorizeMerge) { + try { + await this.authorizeMerge({ + lease: loaded, + patchId: patch.id, + digest: patch.digest, + changedPaths: patch.changedPaths, + }); + } catch (error) { + throw wrapAsMoss(error, ErrorCode.TOOL_NOT_ALLOWED, { + message: `Workspace patch authorization failed: ${error instanceof Error ? error.message : String(error)}`, + cause: error, + }); + } + } + if (patch.patch) { + await this.git( + loaded.parentWorkspace, + ['apply', '--check', '--whitespace=nowarn', '-'], + patch.patch + ); + await this.git(loaded.parentWorkspace, ['apply', '--whitespace=nowarn', '-'], patch.patch); + } + await this.release(loaded.id, 'merged'); + return { status: 'merged', patchId: patch.id, conflictingPaths: [] }; + } + + async release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise { + const lease = this.load(leaseId); + if (!lease) return; + this.assertCompatible(lease); + const terminal: WorkspaceLease = { + ...lease, + status: reason, + updatedAt: this.now(), + }; + this.writeManifest(terminal); + await this.removeWorkspace(terminal); + fs.rmSync(assertLeasePath(this.rootDir, leaseId), { recursive: true, force: true }); + } + + protected abstract removeWorkspace(lease: WorkspaceLease): Promise; + + protected createManifest( + input: CreateWorkspaceLeaseInput, + workspacePath: string, + baseRef: string + ): WorkspaceLease { + const now = this.now(); + const writePaths = input.writePaths.map(normalizeWritePath); + if (writePaths.length === 0) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: 'implementation workspace lease requires at least one declared write path', + }); + } + return { + id: input.id, + graphId: input.graphId, + nodeId: input.nodeId, + kind: this.kind, + status: 'active', + parentWorkspace: path.resolve(input.parentWorkspace), + workspacePath, + writePaths, + baseRef, + baselineHashes: captureBaselineHashes(input.parentWorkspace, writePaths), + createdAt: now, + updatedAt: now, + }; + } + + protected writeManifest(lease: WorkspaceLease): void { + const file = this.manifestFile(lease.id); + const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`; + fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 }); + fs.writeFileSync(temporary, `${JSON.stringify(lease, null, 2)}\n`, { mode: 0o600 }); + fs.renameSync(temporary, file); + } + + protected leaseDirectory(leaseId: string): string { + return assertLeasePath(this.rootDir, leaseId); + } + + protected async git( + cwd: string, + args: readonly string[], + stdin?: string, + extraEnv: Readonly> = {} + ): Promise<{ stdout: string; stderr: string }> { + try { + return await runProcess('git', { + args: [...args], + cwd, + timeout: PROCESS_TIMEOUT_MS, + maxBuffer: 50 * 1024 * 1024, + ...(stdin !== undefined ? { stdin } : {}), + env: { ...processEnvironment(), ...extraEnv }, + }); + } catch (error) { + throw this.failure(`Git command failed: git ${args.join(' ')}`, error); + } + } + + protected commitEnvironment(): Record { + return { + GIT_AUTHOR_NAME: 'Moss Workspace Lease', + GIT_AUTHOR_EMAIL: 'moss-workspace@example.invalid', + GIT_COMMITTER_NAME: 'Moss Workspace Lease', + GIT_COMMITTER_EMAIL: 'moss-workspace@example.invalid', + }; + } + + private manifestFile(leaseId: string): string { + return path.join(assertLeasePath(this.rootDir, leaseId), 'lease.json'); + } + + private requiredLease(leaseId: string): WorkspaceLease { + const lease = this.load(leaseId); + if (!lease) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `unknown workspace lease "${leaseId}"`, + }); + } + if (lease.status !== 'active') { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `workspace lease "${leaseId}" is ${lease.status}`, + }); + } + return lease; + } + + private assertCompatible(lease: WorkspaceLease): void { + if (lease.kind !== this.kind) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `workspace lease "${lease.id}" requires ${lease.kind} adapter`, + }); + } + } + + private failure(message: string, cause: unknown): MossError { + return wrapAsMoss(cause, ErrorCode.EXECUTION_STORE_FAILED, { + message, + recoverable: true, + }); + } +} diff --git a/packages/moss-agent/src/orchestration/workspace-lease-files.ts b/packages/moss-agent/src/orchestration/workspace-lease-files.ts new file mode 100644 index 00000000..97373968 --- /dev/null +++ b/packages/moss-agent/src/orchestration/workspace-lease-files.ts @@ -0,0 +1,125 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { ErrorCode, MossError } from '../errors.js'; +import { normalizeWritePath } from './execution-graph-scheduler.js'; + +const EXCLUDED_DIRECTORY_NAMES = new Set([ + '.git', + '.moss', + 'node_modules', + '.cache', + '.next', + 'dist', + 'build', + 'coverage', +]); + +export function isExcludedWorkspacePath(relativePath: string): boolean { + const normalized = relativePath.replaceAll('\\', '/').replace(/^\.\//, ''); + const segments = normalized.split('/').filter(Boolean); + if (segments.some((segment) => EXCLUDED_DIRECTORY_NAMES.has(segment))) return true; + const basename = segments.at(-1) ?? ''; + return basename === '.env' || basename.startsWith('.env.'); +} + +export function hashFile(file: string): string | null { + try { + const stats = fs.lstatSync(file); + if (stats.isSymbolicLink()) { + return `symlink:${createHash('sha256').update(fs.readlinkSync(file)).digest('hex')}`; + } + if (!stats.isFile()) return null; + return `sha256:${createHash('sha256').update(fs.readFileSync(file)).digest('hex')}`; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw error; + } +} + +export function listFiles(root: string, relativeRoot = ''): string[] { + const absolute = path.join(root, relativeRoot); + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(absolute, { withFileTypes: true }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; + const stats = fs.lstatSync(absolute); + return stats.isFile() || stats.isSymbolicLink() ? [normalizeWritePath(relativeRoot)] : []; + } + const files: string[] = []; + for (const entry of entries) { + const relative = relativeRoot ? `${relativeRoot}/${entry.name}` : entry.name; + if (isExcludedWorkspacePath(relative)) continue; + if (entry.isDirectory()) files.push(...listFiles(root, relative)); + else if (entry.isFile() || entry.isSymbolicLink()) files.push(normalizeWritePath(relative)); + } + return files.sort(); +} + +export function captureBaselineHashes( + parentWorkspace: string, + writePaths: readonly string[] +): Readonly> { + const hashes: Record = {}; + for (const declared of writePaths.map(normalizeWritePath)) { + const absolute = path.join(parentWorkspace, ...declared.split('/')); + const direct = hashFile(absolute); + if (direct !== null) { + hashes[declared] = direct; + continue; + } + for (const relative of listFiles(parentWorkspace, declared)) { + hashes[relative] = hashFile(path.join(parentWorkspace, ...relative.split('/'))); + } + } + return hashes; +} + +export function copyWorkspaceEntry(sourceRoot: string, targetRoot: string, relative: string): void { + if (isExcludedWorkspacePath(relative)) return; + const normalized = normalizeWritePath(relative); + const source = path.join(sourceRoot, ...normalized.split('/')); + const target = path.join(targetRoot, ...normalized.split('/')); + const stats = fs.lstatSync(source); + fs.mkdirSync(path.dirname(target), { recursive: true }); + if (stats.isSymbolicLink()) { + const link = fs.readlinkSync(source); + const resolved = path.resolve(path.dirname(source), link); + const parent = `${path.resolve(sourceRoot)}${path.sep}`; + if (!resolved.startsWith(parent)) return; + try { + fs.unlinkSync(target); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + fs.symlinkSync(link, target, process.platform === 'win32' ? 'junction' : undefined); + return; + } + if (stats.isDirectory()) { + fs.mkdirSync(target, { recursive: true }); + for (const entry of fs.readdirSync(source)) { + copyWorkspaceEntry(sourceRoot, targetRoot, `${normalized}/${entry}`); + } + return; + } + if (stats.isFile()) fs.copyFileSync(source, target); +} + +export function assertLeasePath(rootDir: string, leaseId: string): string { + if (!/^[a-zA-Z0-9._-]+$/.test(leaseId)) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: 'workspace lease id contains unsafe characters', + }); + } + const leaseDir = path.resolve(rootDir, leaseId); + if (!leaseDir.startsWith(`${path.resolve(rootDir)}${path.sep}`)) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: 'workspace lease escaped its storage root', + }); + } + return leaseDir; +} diff --git a/packages/moss-agent/src/orchestration/workspace-lease-types.ts b/packages/moss-agent/src/orchestration/workspace-lease-types.ts new file mode 100644 index 00000000..176d363b --- /dev/null +++ b/packages/moss-agent/src/orchestration/workspace-lease-types.ts @@ -0,0 +1,75 @@ +/** Isolation strategy used for one implementation assignment. @beta */ +export type WorkspaceLeaseKind = 'git-worktree' | 'copy-snapshot'; + +/** Lifecycle retained on disk for an isolated workspace. @beta */ +export type WorkspaceLeaseStatus = 'active' | 'merged' | 'rejected' | 'cancelled'; + +/** Input for creating an implementation workspace lease. @beta */ +export interface CreateWorkspaceLeaseInput { + readonly id: string; + readonly graphId: string; + readonly nodeId: string; + readonly parentWorkspace: string; + readonly writePaths: readonly string[]; +} + +/** Durable manifest for one isolated implementation workspace. @beta */ +export interface WorkspaceLease { + readonly id: string; + readonly graphId: string; + readonly nodeId: string; + readonly kind: WorkspaceLeaseKind; + readonly status: WorkspaceLeaseStatus; + readonly parentWorkspace: string; + readonly workspacePath: string; + readonly writePaths: readonly string[]; + readonly baseRef: string; + readonly baselineHashes: Readonly>; + readonly createdAt: number; + readonly updatedAt: number; +} + +/** Patch and artifact digest returned by an isolated worker. @beta */ +export interface WorkspacePatch { + readonly id: string; + readonly leaseId: string; + readonly patch: string; + readonly digest: string; + readonly changedPaths: readonly string[]; + readonly createdAt: number; +} + +/** Guarded parent merge result. @beta */ +export interface WorkspaceMergeResult { + readonly status: 'merged' | 'merge_conflict'; + readonly patchId: string; + readonly conflictingPaths: readonly string[]; +} + +/** Explicit terminal reasons that permit lease cleanup. @beta */ +export type WorkspaceLeaseReleaseReason = 'merged' | 'rejected' | 'cancelled'; + +/** Isolation seam used by implementation roles. @beta */ +export interface WorkspaceLeaseAdapter { + create(input: CreateWorkspaceLeaseInput): Promise; + load(leaseId: string): WorkspaceLease | undefined; + list(): readonly WorkspaceLease[]; + createPatch(lease: WorkspaceLease): Promise; + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; +} + +/** Shared workspace adapter options. @beta */ +export interface WorkspaceLeaseAdapterOptions { + readonly rootDir: string; + readonly now?: () => number; + readonly authorizeMerge?: (request: WorkspaceMergeAuthorizationRequest) => void | Promise; +} + +/** Host approval request emitted after hash checks and before parent mutation. @beta */ +export interface WorkspaceMergeAuthorizationRequest { + readonly lease: WorkspaceLease; + readonly patchId: string; + readonly digest: string; + readonly changedPaths: readonly string[]; +} diff --git a/packages/moss-agent/test/execution-graph-scheduler.spec.mjs b/packages/moss-agent/test/execution-graph-scheduler.spec.mjs new file mode 100644 index 00000000..60d86ced --- /dev/null +++ b/packages/moss-agent/test/execution-graph-scheduler.spec.mjs @@ -0,0 +1,152 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + ExecutionGraphScheduler, + InMemoryExecutionStore, + writePathsOverlap, +} from '../dist/orchestration/index.js'; + +function createStore(nodes, options = {}) { + const store = new InMemoryExecutionStore(); + store.create({ + id: 'graph', + goal: 'Run a real dependency graph', + nodes, + policy: { + maxConcurrency: options.maxConcurrency ?? 4, + maxAttemptsPerNode: options.maxAttemptsPerNode ?? 3, + strictCompletion: true, + }, + budget: options.budget, + now: 1, + }); + return store; +} + +test('scheduler runs independent nodes concurrently and preserves successful siblings', async () => { + const store = createStore([ + { id: 'a', kind: 'analysis', title: 'A', dependencies: [] }, + { id: 'b', kind: 'analysis', title: 'B', dependencies: [] }, + { id: 'c', kind: 'verification', title: 'C', dependencies: ['a'] }, + { id: 'd', kind: 'verification', title: 'D', dependencies: ['b'] }, + ]); + const scheduler = new ExecutionGraphScheduler(store); + let active = 0; + let peak = 0; + + const result = await scheduler.runAvailable('graph', async (node) => { + active += 1; + peak = Math.max(peak, active); + await new Promise((resolve) => setTimeout(resolve, 15)); + active -= 1; + if (node.id === 'b') { + return { success: false, error: 'boom', failureFingerprint: 'boom:stable' }; + } + return { + success: true, + evidence: [ + { + id: `evidence-${node.id}`, + kind: 'expert_claim', + summary: `${node.id} completed`, + createdAt: 10, + }, + ], + }; + }); + + assert.equal(peak, 2, 'independent nodes should overlap in wall-clock execution'); + assert.deepEqual(result.startedNodeIds.sort(), ['a', 'b']); + assert.equal(result.graph.nodes.a.status, 'succeeded'); + assert.equal(result.graph.nodes.b.status, 'failed'); + assert.equal(result.graph.nodes.c.status, 'ready'); + assert.equal(result.graph.nodes.d.status, 'blocked'); + assert.deepEqual(result.graph.nodes.a.evidenceIds, ['evidence-a']); +}); + +test('scheduler excludes overlapping write paths while admitting unrelated work', () => { + const store = createStore([ + { + id: 'root', + kind: 'implementation', + title: 'Root source', + dependencies: [], + writePaths: ['src'], + }, + { + id: 'nested', + kind: 'implementation', + title: 'Nested source', + dependencies: [], + writePaths: ['src\\feature'], + }, + { + id: 'docs', + kind: 'implementation', + title: 'Docs', + dependencies: [], + writePaths: ['docs/'], + }, + ]); + const scheduler = new ExecutionGraphScheduler(store); + const graph = scheduler.reconcile('graph'); + assert.deepEqual( + scheduler.selectRunnable(graph).map((node) => node.id), + ['root', 'docs'] + ); + assert.equal(writePathsOverlap(['src'], ['src/feature/file.ts']), true); + assert.equal(writePathsOverlap(['src2'], ['src/feature']), false); + assert.throws(() => writePathsOverlap(['../escape'], ['docs']), /workspace-relative/); +}); + +test('three identical failure fingerprints block retry and propagate to dependents', () => { + const store = createStore( + [ + { id: 'work', kind: 'analysis', title: 'Work', dependencies: [] }, + { id: 'verify', kind: 'verification', title: 'Verify', dependencies: ['work'] }, + ], + { maxAttemptsPerNode: 5 } + ); + const scheduler = new ExecutionGraphScheduler(store); + for (let attempt = 0; attempt < 3; attempt += 1) { + scheduler.reconcile('graph'); + scheduler.startNode('graph', 'work'); + scheduler.failNode('graph', 'work', { + error: 'same failure', + failureFingerprint: 'stable-failure', + }); + } + const graph = scheduler.reconcile('graph'); + assert.equal(graph.nodes.work.status, 'blocked'); + assert.equal(graph.nodes.work.consecutiveSameFailures, 3); + assert.equal(graph.nodes.verify.status, 'blocked'); +}); + +test('explicit budget exhaustion pauses rather than completes the graph', () => { + const store = createStore([{ id: 'work', kind: 'analysis', title: 'Work', dependencies: [] }], { + budget: { maxTokens: 100, usedTokens: 100 }, + }); + const scheduler = new ExecutionGraphScheduler(store); + const graph = scheduler.reconcile('graph'); + assert.equal(graph.status, 'paused'); + assert.deepEqual(scheduler.selectRunnable(graph), []); + assert.match(String(store.events('graph').at(-1)?.data.reason), /token budget exhausted/); +}); + +test('graph creation rejects dependency cycles before any work can run', () => { + const store = new InMemoryExecutionStore(); + assert.throws( + () => + store.create({ + id: 'cycle', + goal: 'Never run a cycle', + nodes: [ + { id: 'a', kind: 'analysis', title: 'A', dependencies: ['b'] }, + { id: 'b', kind: 'analysis', title: 'B', dependencies: ['a'] }, + ], + }), + /dependency cycle/ + ); +}); diff --git a/packages/moss-agent/test/workspace-lease.spec.mjs b/packages/moss-agent/test/workspace-lease.spec.mjs new file mode 100644 index 00000000..0f5eac3a --- /dev/null +++ b/packages/moss-agent/test/workspace-lease.spec.mjs @@ -0,0 +1,209 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import test from 'node:test'; + +import { + CopyWorkspaceLeaseAdapter, + GitWorktreeWorkspaceLeaseAdapter, +} from '../dist/orchestration/index.js'; + +function git(cwd, ...args) { + const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr); + return result.stdout.trim(); +} + +function write(file, contents) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, contents); +} + +function createDirtyRepository(root) { + fs.mkdirSync(root, { recursive: true }); + git(root, 'init'); + git(root, 'config', 'user.name', 'Moss Test'); + git(root, 'config', 'user.email', 'moss-test@example.invalid'); + write(path.join(root, '.gitignore'), 'secret.env\nnode_modules/\n'); + write(path.join(root, 'src', 'app.txt'), 'committed\n'); + git(root, 'add', '.'); + git(root, 'commit', '-m', 'base'); + write(path.join(root, 'src', 'app.txt'), 'parent dirty\n'); + write(path.join(root, 'src', 'staged.txt'), 'staged\n'); + git(root, 'add', 'src/staged.txt'); + write(path.join(root, 'src', 'untracked.txt'), 'untracked\n'); + write(path.join(root, 'secret.env'), 'ignored-secret\n'); + write(path.join(root, '.env'), 'never-copy\n'); +} + +test('git worktree lease snapshots dirty tracked, staged, and untracked files without secrets', async () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-workspace-lease-')); + const repository = path.join(temp, 'repo'); + const leases = path.join(temp, 'leases'); + createDirtyRepository(repository); + const adapter = new GitWorktreeWorkspaceLeaseAdapter({ rootDir: leases }); + try { + const lease = await adapter.create({ + id: 'lease-git', + graphId: 'graph', + nodeId: 'implement', + parentWorkspace: repository, + writePaths: ['src/'], + }); + assert.equal( + fs.readFileSync(path.join(lease.workspacePath, 'src/app.txt'), 'utf8'), + 'parent dirty\n' + ); + assert.equal( + fs.readFileSync(path.join(lease.workspacePath, 'src/staged.txt'), 'utf8'), + 'staged\n' + ); + assert.equal( + fs.readFileSync(path.join(lease.workspacePath, 'src/untracked.txt'), 'utf8'), + 'untracked\n' + ); + assert.equal(fs.existsSync(path.join(lease.workspacePath, 'secret.env')), false); + assert.equal(fs.existsSync(path.join(lease.workspacePath, '.env')), false); + + write(path.join(lease.workspacePath, 'src/app.txt'), 'worker change\n'); + const patch = await adapter.createPatch(lease); + assert.deepEqual(patch.changedPaths, ['src/app.txt']); + assert.match(patch.patch, /worker change/); + assert.match(patch.digest, /^sha256:/); + + const restored = new GitWorktreeWorkspaceLeaseAdapter({ rootDir: leases }).load('lease-git'); + assert.equal(restored?.baseRef, lease.baseRef); + assert.equal(fs.existsSync(restored.workspacePath), true); + } finally { + await adapter.release('lease-git', 'cancelled'); + fs.rmSync(temp, { recursive: true, force: true }); + } +}); + +test('guarded merge refuses a parent race and never overwrites the edited file', async () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-workspace-conflict-')); + const repository = path.join(temp, 'repo'); + const adapter = new GitWorktreeWorkspaceLeaseAdapter({ rootDir: path.join(temp, 'leases') }); + createDirtyRepository(repository); + try { + const lease = await adapter.create({ + id: 'lease-conflict', + graphId: 'graph', + nodeId: 'implement', + parentWorkspace: repository, + writePaths: ['src/app.txt'], + }); + write(path.join(lease.workspacePath, 'src/app.txt'), 'worker change\n'); + const patch = await adapter.createPatch(lease); + write(path.join(repository, 'src/app.txt'), 'user edit after lease\n'); + + const merged = await adapter.merge(lease, patch); + assert.equal(merged.status, 'merge_conflict'); + assert.deepEqual(merged.conflictingPaths, ['src/app.txt']); + assert.equal( + fs.readFileSync(path.join(repository, 'src/app.txt'), 'utf8'), + 'user edit after lease\n' + ); + assert.equal(fs.existsSync(lease.workspacePath), true, 'conflicted lease must be retained'); + } finally { + await adapter.release('lease-conflict', 'rejected'); + fs.rmSync(temp, { recursive: true, force: true }); + } +}); + +test('copy snapshot adapter merges declared changes and rejects undeclared writes', async () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-workspace-copy-')); + const parent = path.join(temp, 'plain'); + const adapter = new CopyWorkspaceLeaseAdapter({ rootDir: path.join(temp, 'leases') }); + fs.mkdirSync(parent, { recursive: true }); + write(path.join(parent, 'docs', 'input.txt'), 'input\n'); + write(path.join(parent, '.env'), 'never-copy\n'); + try { + const lease = await adapter.create({ + id: 'lease-copy', + graphId: 'graph', + nodeId: 'docs', + parentWorkspace: parent, + writePaths: ['docs'], + }); + assert.equal(fs.existsSync(path.join(lease.workspacePath, '.env')), false); + write(path.join(lease.workspacePath, 'docs', 'output.txt'), 'generated\n'); + const patch = await adapter.createPatch(lease); + assert.deepEqual(patch.changedPaths, ['docs/output.txt']); + const merged = await adapter.merge(lease, patch); + assert.equal(merged.status, 'merged'); + assert.equal(fs.readFileSync(path.join(parent, 'docs/output.txt'), 'utf8'), 'generated\n'); + + const unsafe = await adapter.create({ + id: 'lease-unsafe', + graphId: 'graph', + nodeId: 'docs', + parentWorkspace: parent, + writePaths: ['docs'], + }); + write(path.join(unsafe.workspacePath, 'outside.txt'), 'not declared\n'); + await assert.rejects(() => adapter.createPatch(unsafe), /outside declared write paths/); + await adapter.release('lease-unsafe', 'rejected'); + } finally { + await adapter.release('lease-copy', 'merged'); + fs.rmSync(temp, { recursive: true, force: true }); + } +}); + +test('workspace patches reject secret paths even under a root write lease', async () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-workspace-secret-')); + const parent = path.join(temp, 'plain'); + const adapter = new CopyWorkspaceLeaseAdapter({ rootDir: path.join(temp, 'leases') }); + fs.mkdirSync(parent, { recursive: true }); + write(path.join(parent, 'safe.txt'), 'safe\n'); + try { + const lease = await adapter.create({ + id: 'lease-secret', + graphId: 'graph', + nodeId: 'work', + parentWorkspace: parent, + writePaths: ['.'], + }); + write(path.join(lease.workspacePath, '.env'), 'secret\n'); + await assert.rejects(() => adapter.createPatch(lease), /excluded secret or runtime paths/); + } finally { + await adapter.release('lease-secret', 'rejected'); + fs.rmSync(temp, { recursive: true, force: true }); + } +}); + +test('host merge authorization runs before the parent workspace is mutated', async () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-workspace-approval-')); + const parent = path.join(temp, 'plain'); + let authorizationCalls = 0; + const adapter = new CopyWorkspaceLeaseAdapter({ + rootDir: path.join(temp, 'leases'), + authorizeMerge(request) { + authorizationCalls += 1; + assert.deepEqual(request.changedPaths, ['safe.txt']); + throw new Error('approval denied'); + }, + }); + fs.mkdirSync(parent, { recursive: true }); + write(path.join(parent, 'safe.txt'), 'before\n'); + try { + const lease = await adapter.create({ + id: 'lease-denied', + graphId: 'graph', + nodeId: 'work', + parentWorkspace: parent, + writePaths: ['safe.txt'], + }); + write(path.join(lease.workspacePath, 'safe.txt'), 'after\n'); + const patch = await adapter.createPatch(lease); + await assert.rejects(() => adapter.merge(lease, patch), /approval denied/); + assert.equal(authorizationCalls, 1); + assert.equal(fs.readFileSync(path.join(parent, 'safe.txt'), 'utf8'), 'before\n'); + } finally { + await adapter.release('lease-denied', 'rejected'); + fs.rmSync(temp, { recursive: true, force: true }); + } +}); From d9800f8b52fa23ad0ac7bb4130835f6ceaf76a58 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 17:40:48 +0800 Subject: [PATCH 04/21] feat(agent): route expert roles with evidence gates --- .../core/agent/agent-plugin-composition.ts | 12 + .../src/core/agent/moss-agent-types.ts | 19 ++ .../moss-agent/src/core/agent/moss-agent.ts | 23 ++ .../core/plugins/plugin-host-validation.ts | 9 + .../src/core/plugins/plugin-host.ts | 15 ++ packages/moss-agent/src/index.ts | 19 ++ .../orchestration/agent-result-synthesis.ts | 78 +++++++ .../src/orchestration/agent-role-registry.ts | 88 +++++++ .../src/orchestration/agent-role-types.ts | 89 ++++++++ .../src/orchestration/assignment-router.ts | 45 ++++ .../src/orchestration/completion-arbiter.ts | 214 ++++++++++++++++++ .../moss-agent/src/orchestration/index.ts | 23 ++ .../src/plugins/isolated-plugin-runtime.ts | 3 + .../src/plugins/plugin-setup-worker.ts | 6 + .../test/agent-role-routing.spec.mjs | 154 +++++++++++++ .../test/completion-arbiter.spec.mjs | 184 +++++++++++++++ .../test/plugin-agent-role.spec.mjs | 91 ++++++++ 17 files changed, 1072 insertions(+) create mode 100644 packages/moss-agent/src/orchestration/agent-result-synthesis.ts create mode 100644 packages/moss-agent/src/orchestration/agent-role-registry.ts create mode 100644 packages/moss-agent/src/orchestration/agent-role-types.ts create mode 100644 packages/moss-agent/src/orchestration/assignment-router.ts create mode 100644 packages/moss-agent/src/orchestration/completion-arbiter.ts create mode 100644 packages/moss-agent/test/agent-role-routing.spec.mjs create mode 100644 packages/moss-agent/test/completion-arbiter.spec.mjs create mode 100644 packages/moss-agent/test/plugin-agent-role.spec.mjs diff --git a/packages/moss-agent/src/core/agent/agent-plugin-composition.ts b/packages/moss-agent/src/core/agent/agent-plugin-composition.ts index 32785fad..c78261de 100644 --- a/packages/moss-agent/src/core/agent/agent-plugin-composition.ts +++ b/packages/moss-agent/src/core/agent/agent-plugin-composition.ts @@ -8,9 +8,11 @@ import { } from '../subagent/expert-registry.js'; import type { ToolRegistry } from '../tools/tool-registry.js'; import type { MossAgentConfig } from './moss-agent-types.js'; +import { AgentRoleRegistry } from '../../orchestration/agent-role-registry.js'; export interface AgentPluginComposition { readonly expertRegistry: SubagentExpertRegistry; + readonly agentRoleRegistry: AgentRoleRegistry; readonly pluginHost: MossPluginHost; readonly packPromptLayers: readonly string[]; readonly packHostRequirements: readonly string[]; @@ -24,6 +26,10 @@ export function createAgentPluginComposition( try { const contributions = collectCapabilityPacks(config.capabilityPacks ?? []); const expertResolution = resolveSubagentExpertRegistry(config, contributions); + const agentRoleRegistry = + config.agentRoleRegistry ?? + new AgentRoleRegistry({ allowIsolatedWrite: config.allowPluginIsolatedWrite }); + const roleDisposers = (config.agentRoles ?? []).map((role) => agentRoleRegistry.register(role)); const skillRegistry: SkillRegistry | undefined = config.skillRegistry; const pluginHost = createMossPluginHost({ hasTool: (name) => tools.has(name), @@ -35,14 +41,20 @@ export function createAgentPluginComposition( }, hasExpert: (id) => expertResolution.registry.get(id) !== undefined, registerExpert: (expert) => expertResolution.registry.register(expert), + hasAgentRole: (id) => agentRoleRegistry.get(id) !== undefined, + registerAgentRole: (role) => agentRoleRegistry.register(role), }); pluginHost.own(expertResolution.disposePackExperts, 'capability-pack experts'); + for (const [index, dispose] of roleDisposers.entries()) { + pluginHost.own(dispose, `configured agent role:${config.agentRoles?.[index]?.id ?? index}`); + } for (const group of contributions.toolGroups) { tools.registerGroup(group); pluginHost.own(() => tools.removeGroup(group.id), `capability-pack tools:${group.id}`); } return { expertRegistry: expertResolution.registry, + agentRoleRegistry, pluginHost, packPromptLayers: contributions.promptLayers, packHostRequirements: contributions.requiredHostCapabilities, diff --git a/packages/moss-agent/src/core/agent/moss-agent-types.ts b/packages/moss-agent/src/core/agent/moss-agent-types.ts index 640838c4..f6336960 100644 --- a/packages/moss-agent/src/core/agent/moss-agent-types.ts +++ b/packages/moss-agent/src/core/agent/moss-agent-types.ts @@ -25,6 +25,13 @@ import type { ApprovedPreflightAssignment, ApprovedPreflightProgress, } from '../subagent/approved-preflight-subagents.js'; +import type { + AgentRoleDefinition, + AgentRoleRegistry, + ExecutionStore, + OrchestrationPolicy, + WorkspaceLeaseAdapter, +} from '../../orchestration/index.js'; export interface ProviderConfig { llmProvider: LLMProvider; @@ -115,6 +122,18 @@ export interface MossAgentConfig sessionStore: SessionStore; workspaceDir?: string; + /** Authoritative execution graph store. Defaults to an instance-local in-memory adapter. @beta */ + executionStore?: ExecutionStore; + /** Visible scheduling policy applied to graphs created by this agent. @beta */ + orchestrationPolicy?: Partial; + /** Host-owned isolated implementation workspace seam. @beta */ + workspaceLeaseAdapter?: WorkspaceLeaseAdapter; + /** Instance-local trusted agent roles. @beta */ + agentRoles?: readonly AgentRoleDefinition[]; + /** Optional plugin-populated role registry. @beta */ + agentRoleRegistry?: AgentRoleRegistry; + /** Explicit host authorization for isolated-write plugin roles. @beta */ + allowPluginIsolatedWrite?: boolean; maxAgentTurns?: number; /** Persist per-call usage records for host-level cost and token reporting. */ recordLlmUsage?: boolean; diff --git a/packages/moss-agent/src/core/agent/moss-agent.ts b/packages/moss-agent/src/core/agent/moss-agent.ts index dfbd13e8..74774cb7 100644 --- a/packages/moss-agent/src/core/agent/moss-agent.ts +++ b/packages/moss-agent/src/core/agent/moss-agent.ts @@ -124,6 +124,14 @@ import { initializeEpoch, reconcileEpoch, type ContextSources } from '../session import { loadContextEpoch, saveContextEpoch } from '../session/context-epoch-store.js'; import { sanitizeSecrets } from '../../safety/secret-sanitizer.js'; import { MossError, ErrorCode, errorMessage, isMossError } from '../../errors.js'; +import { InMemoryExecutionStore } from '../../orchestration/in-memory-execution-store.js'; +import { DEFAULT_ORCHESTRATION_POLICY } from '../../orchestration/execution-projector.js'; +import type { + AgentRoleRegistry, + ExecutionStore, + OrchestrationPolicy, + WorkspaceLeaseAdapter, +} from '../../orchestration/index.js'; import type { MossAgentConfig as SharedMossAgentConfig, ChatOptions as SharedChatOptions, @@ -180,6 +188,14 @@ export class MossAgent { readonly commandQueues: CommandQueueRegistry; readonly spawnRegistry: SpawnProfileRegistry; readonly asyncTasks: MossAsyncTaskRegistry; + /** Authoritative graph store shared by product projections. @beta */ + readonly executionStore: ExecutionStore; + /** Effective visible scheduling policy. @beta */ + readonly orchestrationPolicy: OrchestrationPolicy; + /** Isolated implementation workspace seam when configured by the host. @beta */ + readonly workspaceLeaseAdapter?: WorkspaceLeaseAdapter; + /** Instance-local registry used by built-in and plugin agent roles. @beta */ + readonly agentRoles: AgentRoleRegistry; /** @beta */ readonly plugins: MossPluginHost; private readonly knowledge: KnowledgeRegistry; @@ -215,11 +231,18 @@ export class MossAgent { this.expertRegistry = config.subagentExpertRegistry ?? new SubagentExpertRegistry(config.subagentExperts); this.asyncTasks = config.asyncTaskRegistry ?? createInMemoryMossAsyncTaskRegistry(); + this.executionStore = config.executionStore ?? new InMemoryExecutionStore(); + this.orchestrationPolicy = { + ...DEFAULT_ORCHESTRATION_POLICY, + ...config.orchestrationPolicy, + }; + this.workspaceLeaseAdapter = config.workspaceLeaseAdapter; this.toolHooks = new ToolHookRegistry(); this.toolHooks.registerPost(createSecretSanitizerHook(sanitizeSecrets)); setTraceRedactor(sanitizeSecrets); const composition = createAgentPluginComposition(config, this.tools); this.expertRegistry = composition.expertRegistry; + this.agentRoles = composition.agentRoleRegistry; this.plugins = composition.pluginHost; this.packPromptLayers = composition.packPromptLayers; this.packHostRequirements = composition.packHostRequirements; diff --git a/packages/moss-agent/src/core/plugins/plugin-host-validation.ts b/packages/moss-agent/src/core/plugins/plugin-host-validation.ts index 6f82b161..b79c40be 100644 --- a/packages/moss-agent/src/core/plugins/plugin-host-validation.ts +++ b/packages/moss-agent/src/core/plugins/plugin-host-validation.ts @@ -9,6 +9,7 @@ export interface PluginContributionRegistryView { readonly hasTool: (id: string) => boolean; readonly hasSkill: (id: string) => boolean; readonly hasExpert: (id: string) => boolean; + readonly hasAgentRole: (id: string) => boolean; readonly hasCommand: (id: string) => boolean; readonly hasProvider: (id: string) => boolean; readonly hasMcpPreset: (id: string) => boolean; @@ -21,6 +22,7 @@ export function createStagedPlugin(): StagedPlugin { tools: [], skills: [], experts: [], + agentRoles: [], commands: [], providers: [], mcpPresets: [], @@ -64,6 +66,7 @@ export function validateStagedContributions( duplicate(staged.tools.map(({ name }) => name)) ?? duplicate(staged.skills.map((skill) => skill.stableId ?? skill.name)) ?? duplicate(staged.experts.map(({ id }) => id)) ?? + duplicate(staged.agentRoles.map(({ id }) => id)) ?? duplicate(staged.commands.map(({ id }) => id)) ?? duplicate(staged.providers.map(({ id }) => id)) ?? duplicate(staged.mcpPresets.map(({ id }) => id)) ?? @@ -84,6 +87,12 @@ export function validateStagedContributions( if (registry.hasExpert(expert.id)) throw new Error(`plugin expert already registered: ${expert.id}`); } + for (const role of staged.agentRoles) { + validateContributionId(role.id, 'agent role'); + if (registry.hasAgentRole(role.id)) { + throw new Error(`plugin agent role already registered: ${role.id}`); + } + } for (const command of staged.commands) { validateContributionId(command.id, 'command'); if (!command.title.trim() || typeof command.expand !== 'function') { diff --git a/packages/moss-agent/src/core/plugins/plugin-host.ts b/packages/moss-agent/src/core/plugins/plugin-host.ts index 3789ac53..6a45e749 100644 --- a/packages/moss-agent/src/core/plugins/plugin-host.ts +++ b/packages/moss-agent/src/core/plugins/plugin-host.ts @@ -1,5 +1,6 @@ import { ErrorCode, MossError, wrapAsMoss } from '../../errors.js'; import type { SkillMeta } from '../../skills/types.js'; +import type { AgentRoleDefinition } from '../../orchestration/agent-role-types.js'; import { CordisEffectScope } from '../../vendor/cordis/effect-scope.js'; import type { LLMProvider } from '../llm/llm-provider.js'; import type { SubagentExpertDefinition } from '../subagent/expert-registry.js'; @@ -27,6 +28,7 @@ export interface MossPluginSnapshot { readonly tools: readonly string[]; readonly skills: readonly string[]; readonly experts: readonly string[]; + readonly agentRoles: readonly string[]; readonly commands: readonly string[]; readonly providers: readonly string[]; readonly mcpPresets: readonly string[]; @@ -115,6 +117,7 @@ export interface MossPluginContext { registerTool(tool: Tool): void; registerSkill(skill: SkillMeta): void; registerExpert(expert: SubagentExpertDefinition): void; + registerAgentRole(role: AgentRoleDefinition): void; registerCommand(command: MossPluginCommand): void; registerProvider(provider: MossPluginProvider): void; registerMcpPreset(preset: MossPluginMcpPreset): void; @@ -174,6 +177,8 @@ export interface MossPluginHostAdapters { registerSkill(skill: SkillMeta, owner: string): MossPluginDisposer; hasExpert(id: string): boolean; registerExpert(expert: SubagentExpertDefinition): MossPluginDisposer; + hasAgentRole?: (id: string) => boolean; + registerAgentRole?: (role: AgentRoleDefinition) => MossPluginDisposer; } /** Contribution batch staged before an atomic plugin activation. @internal */ @@ -181,6 +186,7 @@ export interface StagedPlugin { readonly tools: Tool[]; readonly skills: SkillMeta[]; readonly experts: SubagentExpertDefinition[]; + readonly agentRoles: AgentRoleDefinition[]; readonly commands: MossPluginCommand[]; readonly providers: MossPluginProvider[]; readonly mcpPresets: MossPluginMcpPreset[]; @@ -265,6 +271,7 @@ class MossPluginHostImpl implements MossPluginHost { hasTool: this.adapters.hasTool, hasSkill: this.adapters.hasSkill, hasExpert: this.adapters.hasExpert, + hasAgentRole: (id) => this.adapters.hasAgentRole?.(id) ?? false, hasCommand: (id) => this.commands.has(id), hasProvider: (id) => this.providers.has(id), hasMcpPreset: (id) => this.mcpPresets.has(id), @@ -418,6 +425,7 @@ class MossPluginHostImpl implements MossPluginHost { record.staged.skills.map((skill) => skill.stableId ?? skill.name).sort() ), experts: Object.freeze(record.staged.experts.map(({ id }) => id).sort()), + agentRoles: Object.freeze(record.staged.agentRoles.map(({ id }) => id).sort()), commands: Object.freeze(record.staged.commands.map(({ id }) => id).sort()), providers: Object.freeze(record.staged.providers.map(({ id }) => id).sort()), mcpPresets: Object.freeze(record.staged.mcpPresets.map(({ id }) => id).sort()), @@ -534,6 +542,7 @@ class MossPluginHostImpl implements MossPluginHost { registerTool: (tool) => staged.tools.push(tool), registerSkill: (skill) => staged.skills.push(skill), registerExpert: (expert) => staged.experts.push(expert), + registerAgentRole: (role) => staged.agentRoles.push(role), registerCommand: (command) => staged.commands.push(command), registerProvider: (provider) => staged.providers.push(provider), registerMcpPreset: (preset) => staged.mcpPresets.push(preset), @@ -560,6 +569,12 @@ class MossPluginHostImpl implements MossPluginHost { for (const expert of record.staged.experts) { await this.addOwned(record, this.adapters.registerExpert(expert), `expert:${expert.id}`); } + for (const role of record.staged.agentRoles) { + if (!this.adapters.registerAgentRole) { + throw new Error(`plugin agent role registration is unavailable: ${role.id}`); + } + await this.addOwned(record, this.adapters.registerAgentRole(role), `agent-role:${role.id}`); + } for (const command of record.staged.commands) { await this.addOwned( record, diff --git a/packages/moss-agent/src/index.ts b/packages/moss-agent/src/index.ts index 13e602c2..332b1aca 100644 --- a/packages/moss-agent/src/index.ts +++ b/packages/moss-agent/src/index.ts @@ -192,6 +192,9 @@ export type { export { DEFAULT_ORCHESTRATION_POLICY, CopyWorkspaceLeaseAdapter, + AgentRoleRegistry, + AssignmentRouter, + CompletionArbiter, ExecutionGraphScheduler, GitWorktreeWorkspaceLeaseAdapter, InMemoryExecutionStore, @@ -199,11 +202,24 @@ export { projectExecutionGraph, recoverExecutionGraph, normalizeWritePath, + synthesizeAgentResults, writePathsOverlap, } from './orchestration/index.js'; export type { AcquireExecutionLeaseInput, + AgentCapability, + AgentClaim, + AgentResult, + AgentResultConflict, + AgentRoleDefinition, + AgentRoleKind, + AgentRoleRegistryOptions, + AgentSynthesisResult, AppendExecutionEventInput, + AssignmentSpec, + CompletionArbiterInput, + CompletionDecision, + CompletionTaskKind, CreateExecutionGraphInput, CreateWorkspaceLeaseInput, ExecutionBudget, @@ -228,6 +244,9 @@ export type { JsonlExecutionStoreOptions, OrchestrationPolicy, RecoverExecutionGraphOptions, + RoutedAssignment, + SemanticCompletionJudge, + SemanticCompletionResult, WorkspaceLease, WorkspaceLeaseAdapter, WorkspaceLeaseAdapterOptions, diff --git a/packages/moss-agent/src/orchestration/agent-result-synthesis.ts b/packages/moss-agent/src/orchestration/agent-result-synthesis.ts new file mode 100644 index 00000000..6d073287 --- /dev/null +++ b/packages/moss-agent/src/orchestration/agent-result-synthesis.ts @@ -0,0 +1,78 @@ +import type { + AgentClaim, + AgentResult, + AgentResultConflict, + AgentSynthesisResult, + AssignmentSpec, +} from './agent-role-types.js'; + +function conflictSeverity(claims: readonly AgentClaim[]): 'low' | 'medium' | 'high' { + if (claims.some((claim) => claim.severity === 'high')) return 'high'; + if (claims.some((claim) => claim.severity === 'medium')) return 'medium'; + return 'low'; +} + +/** Synthesize structured results without converting partial work into success. @beta */ +export function synthesizeAgentResults(input: { + readonly assignments: readonly AssignmentSpec[]; + readonly results: readonly AgentResult[]; +}): AgentSynthesisResult { + const resultsByAssignment = new Map(input.results.map((result) => [result.assignmentId, result])); + const acceptedEvidenceIds = [...new Set(input.results.flatMap((result) => result.evidenceRefs))]; + const missingCriteria = new Set(); + for (const assignment of input.assignments) { + const result = resultsByAssignment.get(assignment.id); + if (!result || result.status === 'FAIL') { + for (const criterion of assignment.acceptanceCriteria) missingCriteria.add(criterion); + continue; + } + if (result.status === 'PARTIAL') { + for (const criterion of result.unmetCriteria) missingCriteria.add(criterion); + } + } + + const claimsBySubject = new Map(); + for (const claim of input.results.flatMap((result) => result.claims)) { + const claims = claimsBySubject.get(claim.subject) ?? []; + claims.push(claim); + claimsBySubject.set(claim.subject, claims); + } + const conflicts: AgentResultConflict[] = []; + const verifierAssignments: AssignmentSpec[] = []; + for (const [subject, claims] of claimsBySubject) { + if (new Set(claims.map((claim) => claim.conclusion)).size < 2) continue; + const severity = conflictSeverity(claims); + const evidenceIds = [...new Set(claims.flatMap((claim) => claim.evidenceRefs))]; + conflicts.push({ + subject, + severity, + claimIds: claims.map((claim) => claim.id), + evidenceIds, + }); + if (severity === 'high') { + const first = input.assignments[0]; + verifierAssignments.push({ + id: `verify-conflict-${subject}`, + graphId: first?.graphId ?? 'unknown', + nodeId: `verify-conflict-${subject}`, + goal: `Independently resolve conflicting claims about ${subject}`, + requiredRoleKind: 'verifier', + requiredCapabilities: [], + inputEvidenceIds: evidenceIds, + dependencies: [], + writePaths: [], + acceptanceCriteria: [`resolve-conflict:${subject}`], + }); + } + } + const criteria = [...new Set(input.assignments.flatMap((item) => item.acceptanceCriteria))]; + const missing = [...missingCriteria].sort(); + const coverage = criteria.length === 0 ? 1 : (criteria.length - missing.length) / criteria.length; + return { + coverage, + acceptedEvidenceIds, + missingCriteria: missing, + conflicts, + verifierAssignments, + }; +} diff --git a/packages/moss-agent/src/orchestration/agent-role-registry.ts b/packages/moss-agent/src/orchestration/agent-role-registry.ts new file mode 100644 index 00000000..f37fa3aa --- /dev/null +++ b/packages/moss-agent/src/orchestration/agent-role-registry.ts @@ -0,0 +1,88 @@ +import { ErrorCode, MossError } from '../errors.js'; +import type { AgentRoleDefinition } from './agent-role-types.js'; + +/** Role-registry authorization options owned by one MossAgent instance. @beta */ +export interface AgentRoleRegistryOptions { + readonly allowIsolatedWrite?: boolean; +} + +function required(value: string, field: string): string { + const normalized = value.trim(); + if (!normalized) { + throw new MossError({ + code: ErrorCode.USER_INPUT_INVALID, + message: `${field} must not be empty`, + }); + } + return normalized; +} + +function snapshotRole(definition: AgentRoleDefinition): Readonly { + return Object.freeze({ + ...definition, + id: required(definition.id, 'agent role id'), + displayName: required(definition.displayName, 'agent role displayName'), + instructions: required(definition.instructions, 'agent role instructions'), + capabilities: Object.freeze([...new Set(definition.capabilities)]), + ...(definition.allowedTools + ? { allowedTools: Object.freeze([...new Set(definition.allowedTools)]) } + : {}), + ...(definition.budget ? { budget: Object.freeze({ ...definition.budget }) } : {}), + }); +} + +/** Instance-owned, atomically disposable registry for trusted agent roles. @beta */ +export class AgentRoleRegistry { + private readonly roles = new Map>(); + private readonly allowIsolatedWrite: boolean; + + constructor(options: AgentRoleRegistryOptions = {}) { + this.allowIsolatedWrite = options.allowIsolatedWrite === true; + } + + register(definition: AgentRoleDefinition): () => void { + const role = snapshotRole(definition); + if (this.roles.has(role.id)) { + throw new MossError({ + code: ErrorCode.USER_INPUT_INVALID, + message: `agent role already registered: ${role.id}`, + }); + } + if (role.kind === 'implementer' && role.workspaceMode !== 'isolated-write') { + throw new MossError({ + code: ErrorCode.USER_INPUT_INVALID, + message: `implementer role "${role.id}" must use isolated-write`, + }); + } + if (role.kind === 'advisor' && role.workspaceMode !== 'shared-readonly') { + throw new MossError({ + code: ErrorCode.USER_INPUT_INVALID, + message: `advisor role "${role.id}" must use shared-readonly`, + }); + } + if (role.workspaceMode === 'isolated-write' && !this.allowIsolatedWrite) { + throw new MossError({ + code: ErrorCode.TOOL_NOT_ALLOWED, + message: `isolated-write role authorization is required for "${role.id}"`, + }); + } + this.roles.set(role.id, role); + return () => { + if (this.roles.get(role.id) === role) this.roles.delete(role.id); + }; + } + + get(id: string): Readonly | undefined { + return this.roles.get(id); + } + + list(): readonly Readonly[] { + return [...this.roles.values()]; + } +} + +export function cloneAgentRoleSnapshot( + definition: AgentRoleDefinition +): Readonly { + return snapshotRole(definition); +} diff --git a/packages/moss-agent/src/orchestration/agent-role-types.ts b/packages/moss-agent/src/orchestration/agent-role-types.ts new file mode 100644 index 00000000..d7552d9d --- /dev/null +++ b/packages/moss-agent/src/orchestration/agent-role-types.ts @@ -0,0 +1,89 @@ +import type { ExecutionBudget } from './execution-types.js'; + +/** Root-orchestrated role category. Workers cannot delegate recursively. @beta */ +export type AgentRoleKind = 'advisor' | 'implementer' | 'verifier'; + +/** Built-in capability vocabulary; plugins may add namespaced capability strings. @beta */ +export type AgentCapability = + | 'architecture' + | 'code' + | 'test' + | 'security' + | 'performance' + | 'documentation' + | 'device' + | 'research' + | (string & {}); + +/** Host-trusted role definition contributed by Moss or a plugin. @beta */ +export interface AgentRoleDefinition { + readonly id: string; + readonly displayName: string; + readonly kind: AgentRoleKind; + readonly capabilities: readonly AgentCapability[]; + readonly instructions: string; + readonly workspaceMode: 'shared-readonly' | 'isolated-write'; + readonly outputContract: 'structured-v1'; + readonly allowedTools?: readonly string[]; + readonly model?: string; + readonly budget?: ExecutionBudget; +} + +/** One bounded unit assigned by the root orchestrator. @beta */ +export interface AssignmentSpec { + readonly id: string; + readonly graphId: string; + readonly nodeId: string; + readonly goal: string; + readonly requiredRoleKind: AgentRoleKind; + readonly requiredCapabilities: readonly AgentCapability[]; + readonly inputEvidenceIds: readonly string[]; + readonly dependencies: readonly string[]; + readonly writePaths: readonly string[]; + readonly acceptanceCriteria: readonly string[]; + readonly budget?: ExecutionBudget; +} + +/** Immutable role snapshot bound to an assignment before plugin unload is possible. @beta */ +export interface RoutedAssignment { + readonly assignment: AssignmentSpec; + readonly role: Readonly; +} + +/** Structured claim emitted by an expert role. @beta */ +export interface AgentClaim { + readonly id: string; + readonly subject: string; + readonly conclusion: string; + readonly severity: 'low' | 'medium' | 'high'; + readonly evidenceRefs: readonly string[]; +} + +/** Structured assignment result. Non-empty prose alone is not success. @beta */ +export interface AgentResult { + readonly assignmentId: string; + readonly roleId: string; + readonly status: 'PASS' | 'FAIL' | 'PARTIAL'; + readonly claims: readonly AgentClaim[]; + readonly evidenceRefs: readonly string[]; + readonly patchRef?: string; + readonly unmetCriteria: readonly string[]; + readonly runId?: string; +} + +/** Contradictory claims requiring disclosure or independent verification. @beta */ +export interface AgentResultConflict { + readonly subject: string; + readonly severity: 'low' | 'medium' | 'high'; + readonly claimIds: readonly string[]; + readonly evidenceIds: readonly string[]; +} + +/** Requirement-coverage synthesis over structured expert results. @beta */ +export interface AgentSynthesisResult { + readonly coverage: number; + readonly acceptedEvidenceIds: readonly string[]; + readonly missingCriteria: readonly string[]; + readonly conflicts: readonly AgentResultConflict[]; + readonly verifierAssignments: readonly AssignmentSpec[]; +} diff --git a/packages/moss-agent/src/orchestration/assignment-router.ts b/packages/moss-agent/src/orchestration/assignment-router.ts new file mode 100644 index 00000000..ef3aaa54 --- /dev/null +++ b/packages/moss-agent/src/orchestration/assignment-router.ts @@ -0,0 +1,45 @@ +import { ErrorCode, MossError } from '../errors.js'; +import { cloneAgentRoleSnapshot, type AgentRoleRegistry } from './agent-role-registry.js'; +import type { AgentRoleDefinition, AssignmentSpec, RoutedAssignment } from './agent-role-types.js'; + +function satisfies(role: AgentRoleDefinition, assignment: AssignmentSpec): boolean { + return ( + role.kind === assignment.requiredRoleKind && + assignment.requiredCapabilities.every((capability) => role.capabilities.includes(capability)) && + (assignment.writePaths.length === 0 || role.workspaceMode === 'isolated-write') + ); +} + +/** Capability- and permission-constrained assignment router. @beta */ +export class AssignmentRouter { + constructor(private readonly registry: AgentRoleRegistry) {} + + route(assignment: AssignmentSpec, preferredRoleId?: string): RoutedAssignment { + if (preferredRoleId) { + const preferred = this.registry.get(preferredRoleId); + if (!preferred || !satisfies(preferred, assignment)) { + throw new MossError({ + code: ErrorCode.TOOL_NOT_ALLOWED, + message: `preferred role "${preferredRoleId}" does not satisfy assignment "${assignment.id}"`, + }); + } + return { assignment, role: cloneAgentRoleSnapshot(preferred) }; + } + const candidates = this.registry + .list() + .filter((role) => satisfies(role, assignment)) + .sort((left, right) => { + const leftExtra = left.capabilities.length - assignment.requiredCapabilities.length; + const rightExtra = right.capabilities.length - assignment.requiredCapabilities.length; + return leftExtra - rightExtra || left.id.localeCompare(right.id); + }); + const role = candidates[0]; + if (!role) { + throw new MossError({ + code: ErrorCode.TOOL_NOT_ALLOWED, + message: `no authorized agent role satisfies assignment "${assignment.id}"`, + }); + } + return { assignment, role: cloneAgentRoleSnapshot(role) }; + } +} diff --git a/packages/moss-agent/src/orchestration/completion-arbiter.ts b/packages/moss-agent/src/orchestration/completion-arbiter.ts new file mode 100644 index 00000000..dc454283 --- /dev/null +++ b/packages/moss-agent/src/orchestration/completion-arbiter.ts @@ -0,0 +1,214 @@ +import type { + ExecutionEvidence, + ExecutionGraphSnapshot, + ExecutionStore, + ExecutionVerification, +} from './execution-types.js'; + +/** Task-specific evidence policy selected by the trusted host. @beta */ +export type CompletionTaskKind = 'coding' | 'research' | 'device' | 'analysis'; + +/** Semantic coverage result; it cannot override deterministic failures. @beta */ +export interface SemanticCompletionResult { + readonly covered: boolean; + readonly reasons: readonly string[]; +} + +/** Optional model callback invoked only after deterministic conditions pass. @beta */ +export type SemanticCompletionJudge = ( + graph: ExecutionGraphSnapshot +) => Promise; + +/** Inputs that are not derivable from the graph event stream yet. @beta */ +export interface CompletionArbiterInput { + readonly taskKind: CompletionTaskKind; + readonly activeBackgroundTaskIds?: readonly string[]; + readonly activeWorkspaceLeaseIds?: readonly string[]; + readonly semanticJudge?: SemanticCompletionJudge; +} + +/** Unified completion outcome consumed by CLI, Web, TUI, and ACP. @beta */ +export interface CompletionDecision { + readonly status: 'in_progress' | 'blocked' | 'completed' | 'failed'; + readonly verdict: 'pending' | 'needs_evidence' | 'verified' | 'rejected'; + readonly reasons: readonly string[]; + readonly evidenceIds: readonly string[]; +} + +function criterionEvidence(graph: ExecutionGraphSnapshot): Set { + return new Set( + graph.evidence + .map((evidence) => evidence.metadata?.criterion) + .filter((criterion): criterion is string => typeof criterion === 'string') + ); +} + +function evidenceOfKind( + graph: ExecutionGraphSnapshot, + kind: ExecutionEvidence['kind'] +): ExecutionEvidence[] { + return graph.evidence.filter((evidence) => evidence.kind === kind); +} + +function deterministicDecision( + graph: ExecutionGraphSnapshot, + input: CompletionArbiterInput +): CompletionDecision { + const reasons: string[] = []; + if (input.activeBackgroundTaskIds?.length) { + reasons.push(`Background tasks are still active: ${input.activeBackgroundTaskIds.join(', ')}`); + } + if (input.activeWorkspaceLeaseIds?.length) { + reasons.push(`Workspace leases are still active: ${input.activeWorkspaceLeaseIds.join(', ')}`); + } + if (reasons.length > 0) { + return { status: 'in_progress', verdict: 'pending', reasons, evidenceIds: [] }; + } + + const nodes = Object.values(graph.nodes); + const rejectedNodes = nodes.filter((node) => + ['blocked', 'merge_conflict', 'cancelled'].includes(node.status) + ); + if (rejectedNodes.length > 0) { + return { + status: 'failed', + verdict: 'rejected', + reasons: [`Terminal node failures: ${rejectedNodes.map((node) => node.id).join(', ')}`], + evidenceIds: rejectedNodes.flatMap((node) => node.evidenceIds), + }; + } + const unfinished = nodes.filter( + (node) => node.status !== 'succeeded' && node.status !== 'skipped' + ); + if (unfinished.length > 0) { + return { + status: 'in_progress', + verdict: 'pending', + reasons: [`Unfinished nodes: ${unfinished.map((node) => node.id).join(', ')}`], + evidenceIds: [], + }; + } + + const requiredCriteria = [...new Set(nodes.flatMap((node) => node.acceptanceCriteria ?? []))]; + const coveredCriteria = criterionEvidence(graph); + const missingCriteria = requiredCriteria.filter((criterion) => !coveredCriteria.has(criterion)); + if (missingCriteria.length > 0) { + reasons.push(`Missing acceptance evidence: ${missingCriteria.join(', ')}`); + } + + const evidenceIds = new Set(); + if (input.taskKind === 'coding') { + const patches = evidenceOfKind(graph, 'patch').filter( + (evidence) => evidence.metadata?.merged === true + ); + if (patches.length === 0) reasons.push('Coding completion requires merged patch evidence.'); + for (const patch of patches) evidenceIds.add(patch.id); + const newestPatchAt = Math.max(...patches.map((patch) => patch.createdAt), 0); + const verificationNodes = nodes.filter((node) => node.kind === 'verification'); + if ( + verificationNodes.length === 0 || + verificationNodes.some((node) => node.status !== 'succeeded') + ) { + reasons.push('Coding completion requires an independent successful verifier node.'); + } + const fresh = evidenceOfKind(graph, 'verification').filter( + (evidence) => + evidence.metadata?.fresh === true && + evidence.metadata?.exitCode === 0 && + evidence.createdAt > newestPatchAt + ); + if (fresh.length === 0) { + reasons.push('Coding completion requires fresh verification after the latest patch.'); + } + for (const evidence of fresh) evidenceIds.add(evidence.id); + } else if (input.taskKind === 'research') { + const sources = graph.evidence.filter( + (evidence) => evidence.kind === 'citation' || evidence.kind === 'tool_result' + ); + if (sources.length === 0) + reasons.push('Research completion requires citation or tool evidence.'); + for (const evidence of sources) evidenceIds.add(evidence.id); + } else if (input.taskKind === 'device') { + const probes = evidenceOfKind(graph, 'probe_receipt').filter( + (evidence) => evidence.metadata?.real === true + ); + if (probes.length === 0) reasons.push('Device completion requires a real probe receipt.'); + for (const evidence of probes) evidenceIds.add(evidence.id); + } else { + for (const evidence of graph.evidence) evidenceIds.add(evidence.id); + } + + if (reasons.length > 0) { + return { + status: graph.policy.strictCompletion ? 'blocked' : 'in_progress', + verdict: 'needs_evidence', + reasons, + evidenceIds: [...evidenceIds], + }; + } + return { + status: 'completed', + verdict: 'verified', + reasons: [], + evidenceIds: [...evidenceIds], + }; +} + +/** Deterministic-first completion authority for one execution store. @beta */ +export class CompletionArbiter { + constructor(private readonly store: ExecutionStore) {} + + async decide(graphId: string, input: CompletionArbiterInput): Promise { + let graph = this.store.load(graphId); + if (!graph) throw new Error(`unknown execution graph "${graphId}"`); + let decision = deterministicDecision(graph, input); + if (decision.verdict === 'verified' && input.semanticJudge) { + const semantic = await input.semanticJudge(graph); + if (!semantic.covered) { + decision = { + status: graph.policy.strictCompletion ? 'blocked' : 'in_progress', + verdict: 'needs_evidence', + reasons: [...semantic.reasons], + evidenceIds: decision.evidenceIds, + }; + } + } + if (decision.verdict === 'pending') return decision; + const verifiedAt = Date.now(); + const verification: ExecutionVerification = { + verdict: decision.verdict, + evidenceIds: decision.evidenceIds, + reasons: decision.reasons, + verifiedAt, + }; + graph = this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'verification.recorded', + time: verifiedAt, + data: { verification }, + }); + if (decision.status === 'completed') { + this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'graph.completed', + time: verifiedAt, + data: { evidenceIds: decision.evidenceIds }, + }); + } else if (decision.status === 'blocked') { + this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'graph.blocked', + time: verifiedAt, + data: { reason: 'needs_evidence', reasons: decision.reasons }, + }); + } else if (decision.status === 'failed') { + this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'graph.failed', + time: verifiedAt, + data: { reasons: decision.reasons }, + }); + } + return decision; + } +} diff --git a/packages/moss-agent/src/orchestration/index.ts b/packages/moss-agent/src/orchestration/index.ts index c684a111..172120fd 100644 --- a/packages/moss-agent/src/orchestration/index.ts +++ b/packages/moss-agent/src/orchestration/index.ts @@ -12,6 +12,18 @@ export { } from './execution-graph-scheduler.js'; export { GitWorktreeWorkspaceLeaseAdapter } from './git-worktree-workspace-lease.js'; export { CopyWorkspaceLeaseAdapter } from './copy-workspace-lease.js'; +export { AgentRoleRegistry, cloneAgentRoleSnapshot } from './agent-role-registry.js'; +export type { AgentRoleRegistryOptions } from './agent-role-registry.js'; +export { AssignmentRouter } from './assignment-router.js'; +export { synthesizeAgentResults } from './agent-result-synthesis.js'; +export { CompletionArbiter } from './completion-arbiter.js'; +export type { + CompletionArbiterInput, + CompletionDecision, + CompletionTaskKind, + SemanticCompletionJudge, + SemanticCompletionResult, +} from './completion-arbiter.js'; export type { AcquireExecutionLeaseInput, AppendExecutionEventInput, @@ -48,3 +60,14 @@ export type { WorkspaceMergeAuthorizationRequest, WorkspacePatch, } from './workspace-lease-types.js'; +export type { + AgentCapability, + AgentClaim, + AgentResult, + AgentResultConflict, + AgentRoleDefinition, + AgentRoleKind, + AgentSynthesisResult, + AssignmentSpec, + RoutedAssignment, +} from './agent-role-types.js'; diff --git a/packages/moss-agent/src/plugins/isolated-plugin-runtime.ts b/packages/moss-agent/src/plugins/isolated-plugin-runtime.ts index c21b37f0..3a18d54e 100644 --- a/packages/moss-agent/src/plugins/isolated-plugin-runtime.ts +++ b/packages/moss-agent/src/plugins/isolated-plugin-runtime.ts @@ -14,6 +14,7 @@ import type { } from '../core/llm/llm-provider.js'; import type { SkillMeta } from '../skills/types.js'; import type { SubagentExpertDefinition } from '../core/subagent/expert-registry.js'; +import type { AgentRoleDefinition } from '../orchestration/agent-role-types.js'; import type { StructuredToolResult, ToolContext, ToolMetadata } from '../core/tools/tool-types.js'; interface IsolatedContributions { @@ -30,6 +31,7 @@ interface IsolatedContributions { }>; readonly skills: readonly SkillMeta[]; readonly experts: readonly SubagentExpertDefinition[]; + readonly agentRoles: readonly AgentRoleDefinition[]; readonly commands: ReadonlyArray<{ readonly id: string; readonly title: string; @@ -329,6 +331,7 @@ function registerRemoteContributions( } for (const skill of runtime.contributions.skills) context.registerSkill(skill); for (const expert of runtime.contributions.experts) context.registerExpert(expert); + for (const role of runtime.contributions.agentRoles) context.registerAgentRole(role); for (const command of runtime.contributions.commands) { context.registerCommand({ ...command, diff --git a/packages/moss-agent/src/plugins/plugin-setup-worker.ts b/packages/moss-agent/src/plugins/plugin-setup-worker.ts index 56534a2a..33ff7d9d 100644 --- a/packages/moss-agent/src/plugins/plugin-setup-worker.ts +++ b/packages/moss-agent/src/plugins/plugin-setup-worker.ts @@ -11,6 +11,7 @@ import type { LLMProvider, LLMRequestOptions, LLMStreamEvent } from '../core/llm import type { Tool, ToolContext } from '../core/tools/tool-types.js'; import type { SkillMeta } from '../skills/types.js'; import type { SubagentExpertDefinition } from '../core/subagent/expert-registry.js'; +import type { AgentRoleDefinition } from '../orchestration/agent-role-types.js'; interface PluginSetupWorkerData { readonly moduleUrl: string; @@ -34,6 +35,7 @@ const providers = new Map(); const mcpPresets: MossPluginMcpPreset[] = []; const skills: SkillMeta[] = []; const experts: SubagentExpertDefinition[] = []; +const agentRoles: AgentRoleDefinition[] = []; const promptLayers: string[] = []; const webContributions: MossWebContribution[] = []; const effects: Array<() => MossPluginDisposer | Promise> = []; @@ -87,6 +89,9 @@ async function setup(): Promise { registerExpert(expert) { experts.push(expert); }, + registerAgentRole(role) { + agentRoles.push(role); + }, registerCommand(command) { commands.set(command.id, command); }, @@ -114,6 +119,7 @@ async function setup(): Promise { tools: [...tools.values()].map(cloneableTool), skills, experts, + agentRoles, commands: [...commands.values()].map(({ id, title, description }) => ({ id, title, diff --git a/packages/moss-agent/test/agent-role-routing.spec.mjs b/packages/moss-agent/test/agent-role-routing.spec.mjs new file mode 100644 index 00000000..1fcf8ee5 --- /dev/null +++ b/packages/moss-agent/test/agent-role-routing.spec.mjs @@ -0,0 +1,154 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + AgentRoleRegistry, + AssignmentRouter, + synthesizeAgentResults, +} from '../dist/orchestration/index.js'; + +const architect = { + id: 'architect', + displayName: 'Architect', + kind: 'advisor', + capabilities: ['architecture', 'security'], + instructions: 'Review boundaries and cite evidence.', + workspaceMode: 'shared-readonly', + outputContract: 'structured-v1', +}; + +const implementer = { + id: 'implementer', + displayName: 'Implementer', + kind: 'implementer', + capabilities: ['code', 'test'], + instructions: 'Return a patch and verification evidence.', + workspaceMode: 'isolated-write', + outputContract: 'structured-v1', +}; + +function assignment(overrides = {}) { + return { + id: 'assignment-1', + graphId: 'graph', + nodeId: 'node', + goal: 'Implement safely', + requiredRoleKind: 'implementer', + requiredCapabilities: ['code', 'test'], + inputEvidenceIds: [], + dependencies: [], + writePaths: ['src'], + acceptanceCriteria: ['tests-green'], + ...overrides, + }; +} + +test('implementer roles require explicit isolated-write host authorization', () => { + const denied = new AgentRoleRegistry(); + assert.throws(() => denied.register(implementer), /isolated-write role authorization/); + assert.equal(denied.list().length, 0); + + const allowed = new AgentRoleRegistry({ allowIsolatedWrite: true }); + const dispose = allowed.register(implementer); + assert.equal(allowed.get('implementer')?.kind, 'implementer'); + dispose(); + assert.equal(allowed.get('implementer'), undefined); +}); + +test('assignment router matches every capability and never expands a preferred role', () => { + const registry = new AgentRoleRegistry({ allowIsolatedWrite: true }); + registry.register(architect); + registry.register(implementer); + const router = new AssignmentRouter(registry); + + const routed = router.route(assignment()); + assert.equal(routed.role.id, 'implementer'); + assert.deepEqual(routed.role.capabilities, ['code', 'test']); + assert.throws(() => router.route(assignment(), 'architect'), /does not satisfy assignment/); + assert.throws( + () => router.route(assignment({ requiredCapabilities: ['code', 'performance'] })), + /no authorized agent role/ + ); +}); + +test('routed assignments retain an immutable role snapshot across plugin unload', () => { + const registry = new AgentRoleRegistry({ allowIsolatedWrite: true }); + const dispose = registry.register(implementer); + const routed = new AssignmentRouter(registry).route(assignment()); + dispose(); + assert.equal(registry.get('implementer'), undefined); + assert.equal(routed.role.id, 'implementer'); + assert.deepEqual(routed.role.capabilities, ['code', 'test']); + assert.equal(Object.isFrozen(routed.role), true); +}); + +test('synthesis keeps partial evidence, exposes gaps, and schedules high-severity conflict verification', () => { + const result = synthesizeAgentResults({ + assignments: [ + assignment({ + id: 'security-a', + requiredRoleKind: 'advisor', + acceptanceCriteria: ['threats'], + }), + assignment({ + id: 'security-b', + requiredRoleKind: 'advisor', + acceptanceCriteria: ['threats'], + }), + assignment({ id: 'tests', acceptanceCriteria: ['tests-green'] }), + ], + results: [ + { + assignmentId: 'security-a', + roleId: 'architect-a', + status: 'PASS', + claims: [ + { + id: 'claim-a', + subject: 'auth-boundary', + conclusion: 'safe', + severity: 'high', + evidenceRefs: ['evidence-a'], + }, + ], + evidenceRefs: ['evidence-a'], + unmetCriteria: [], + }, + { + assignmentId: 'security-b', + roleId: 'architect-b', + status: 'PASS', + claims: [ + { + id: 'claim-b', + subject: 'auth-boundary', + conclusion: 'unsafe', + severity: 'high', + evidenceRefs: ['evidence-b'], + }, + ], + evidenceRefs: ['evidence-b'], + unmetCriteria: [], + }, + { + assignmentId: 'tests', + roleId: 'implementer', + status: 'PARTIAL', + claims: [], + evidenceRefs: ['partial-log'], + unmetCriteria: ['tests-green'], + }, + ], + }); + + assert.deepEqual(result.acceptedEvidenceIds.sort(), ['evidence-a', 'evidence-b', 'partial-log']); + assert.deepEqual(result.missingCriteria, ['tests-green']); + assert.equal(result.conflicts.length, 1); + assert.equal(result.verifierAssignments.length, 1); + assert.deepEqual(result.verifierAssignments[0].inputEvidenceIds.sort(), [ + 'evidence-a', + 'evidence-b', + ]); + assert.ok(result.coverage < 1); +}); diff --git a/packages/moss-agent/test/completion-arbiter.spec.mjs b/packages/moss-agent/test/completion-arbiter.spec.mjs new file mode 100644 index 00000000..63eb1484 --- /dev/null +++ b/packages/moss-agent/test/completion-arbiter.spec.mjs @@ -0,0 +1,184 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + CompletionArbiter, + ExecutionGraphScheduler, + InMemoryExecutionStore, +} from '../dist/orchestration/index.js'; + +function createCodingGraph() { + const store = new InMemoryExecutionStore(); + store.create({ + id: 'coding', + goal: 'Change code safely', + nodes: [ + { + id: 'implement', + kind: 'implementation', + title: 'Implement', + dependencies: [], + writePaths: ['src'], + acceptanceCriteria: ['patch-merged'], + }, + { + id: 'verify', + kind: 'verification', + title: 'Verify', + dependencies: ['implement'], + acceptanceCriteria: ['tests-green'], + }, + ], + policy: { strictCompletion: true }, + now: 1, + }); + return store; +} + +function append(store, graphId, input) { + const graph = store.load(graphId); + return store.append(graphId, { expectedRevision: graph.revision, ...input }); +} + +test('model success text cannot override missing deterministic coding evidence', async () => { + const store = createCodingGraph(); + const scheduler = new ExecutionGraphScheduler(store); + scheduler.reconcile('coding'); + scheduler.startNode('coding', 'implement'); + scheduler.succeedNode('coding', 'implement'); + scheduler.reconcile('coding'); + scheduler.startNode('coding', 'verify'); + scheduler.succeedNode('coding', 'verify'); + let semanticCalls = 0; + + const decision = await new CompletionArbiter(store).decide('coding', { + taskKind: 'coding', + semanticJudge: async () => { + semanticCalls += 1; + return { covered: true, reasons: [] }; + }, + }); + + assert.equal(decision.verdict, 'needs_evidence'); + assert.equal(decision.status, 'blocked'); + assert.equal(semanticCalls, 0); + assert.match(decision.reasons.join('\n'), /merged patch evidence/); + assert.equal(store.load('coding').status, 'blocked'); +}); + +test('coding completion requires merged patch followed by fresh green verification', async () => { + const store = createCodingGraph(); + const scheduler = new ExecutionGraphScheduler(store); + scheduler.reconcile('coding'); + scheduler.startNode('coding', 'implement'); + scheduler.succeedNode('coding', 'implement', [ + { + id: 'patch', + kind: 'patch', + summary: 'Patch merged', + createdAt: 10, + metadata: { merged: true, criterion: 'patch-merged' }, + }, + ]); + scheduler.reconcile('coding'); + scheduler.startNode('coding', 'verify'); + scheduler.succeedNode('coding', 'verify', [ + { + id: 'verify-green', + kind: 'verification', + summary: 'Tests passed after merge', + createdAt: 20, + metadata: { fresh: true, exitCode: 0, criterion: 'tests-green' }, + }, + ]); + + const decision = await new CompletionArbiter(store).decide('coding', { + taskKind: 'coding', + semanticJudge: async () => ({ covered: true, reasons: [] }), + }); + assert.equal(decision.verdict, 'verified'); + assert.equal(decision.status, 'completed'); + assert.deepEqual(decision.evidenceIds.sort(), ['patch', 'verify-green']); + assert.equal(store.load('coding').verification.verdict, 'verified'); + assert.equal(store.load('coding').status, 'completed'); +}); + +test('verification older than the merged patch is not fresh evidence', async () => { + const store = createCodingGraph(); + for (const [nodeId, evidence] of [ + [ + 'implement', + { + id: 'patch-newer', + kind: 'patch', + summary: 'Patch merged later', + createdAt: 30, + metadata: { merged: true, criterion: 'patch-merged' }, + }, + ], + [ + 'verify', + { + id: 'verify-old', + kind: 'verification', + summary: 'Old green run', + createdAt: 20, + metadata: { fresh: true, exitCode: 0, criterion: 'tests-green' }, + }, + ], + ]) { + append(store, 'coding', { type: 'node.ready', nodeId }); + append(store, 'coding', { type: 'node.started', nodeId }); + append(store, 'coding', { + type: 'evidence.recorded', + nodeId, + data: { evidence: { ...evidence, nodeId } }, + }); + append(store, 'coding', { type: 'node.succeeded', nodeId }); + } + const decision = await new CompletionArbiter(store).decide('coding', { taskKind: 'coding' }); + assert.equal(decision.verdict, 'needs_evidence'); + assert.match(decision.reasons.join('\n'), /fresh verification after the latest patch/); +}); + +test('active background work and workspace leases keep a graph in progress', async () => { + const store = createCodingGraph(); + const decision = await new CompletionArbiter(store).decide('coding', { + taskKind: 'coding', + activeBackgroundTaskIds: ['background-1'], + activeWorkspaceLeaseIds: ['lease-1'], + }); + assert.equal(decision.status, 'in_progress'); + assert.equal(store.load('coding').status, 'paused'); + assert.match(decision.reasons.join('\n'), /background-1/); + assert.match(decision.reasons.join('\n'), /lease-1/); +}); + +test('research and device tasks require citations and real probe receipts', async () => { + for (const [graphId, taskKind, evidenceKind] of [ + ['research', 'research', 'citation'], + ['device', 'device', 'probe_receipt'], + ]) { + const store = new InMemoryExecutionStore(); + store.create({ + id: graphId, + goal: `Complete ${taskKind}`, + nodes: [{ id: 'work', kind: 'analysis', title: 'Work', dependencies: [] }], + }); + const scheduler = new ExecutionGraphScheduler(store); + scheduler.reconcile(graphId); + scheduler.startNode(graphId, 'work'); + scheduler.succeedNode(graphId, 'work', [ + { + id: `${graphId}-evidence`, + kind: evidenceKind, + summary: `${taskKind} evidence`, + createdAt: 10, + metadata: taskKind === 'device' ? { real: true } : {}, + }, + ]); + const decision = await new CompletionArbiter(store).decide(graphId, { taskKind }); + assert.equal(decision.verdict, 'verified'); + } +}); diff --git a/packages/moss-agent/test/plugin-agent-role.spec.mjs b/packages/moss-agent/test/plugin-agent-role.spec.mjs new file mode 100644 index 00000000..830c5bc3 --- /dev/null +++ b/packages/moss-agent/test/plugin-agent-role.spec.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { createMossPluginHost } from '../dist/core/plugins/plugin-host.js'; +import { AgentRoleRegistry, AssignmentRouter } from '../dist/orchestration/index.js'; + +function adapters(registry) { + return { + hasTool: () => false, + registerTool: () => () => {}, + hasSkill: () => false, + registerSkill: () => () => {}, + hasExpert: () => false, + registerExpert: () => () => {}, + hasAgentRole: (id) => registry.get(id) !== undefined, + registerAgentRole: (role) => registry.register(role), + }; +} + +const advisor = { + id: 'plugin-advisor', + displayName: 'Plugin advisor', + kind: 'advisor', + capabilities: ['architecture'], + instructions: 'Cite architecture evidence.', + workspaceMode: 'shared-readonly', + outputContract: 'structured-v1', +}; + +const implementer = { + id: 'plugin-implementer', + displayName: 'Plugin implementer', + kind: 'implementer', + capabilities: ['code'], + instructions: 'Return a patch.', + workspaceMode: 'isolated-write', + outputContract: 'structured-v1', +}; + +test('plugin agent roles install and unload atomically while routed snapshots survive', async () => { + const registry = new AgentRoleRegistry({ allowIsolatedWrite: true }); + const host = createMossPluginHost(adapters(registry)); + try { + await host.install({ + id: 'roles/example', + setup(context) { + context.registerAgentRole(advisor); + }, + }); + assert.deepEqual(host.inspect().plugins[0].agentRoles, ['plugin-advisor']); + const routed = new AssignmentRouter(registry).route({ + id: 'review', + graphId: 'graph', + nodeId: 'review', + goal: 'Review architecture', + requiredRoleKind: 'advisor', + requiredCapabilities: ['architecture'], + inputEvidenceIds: [], + dependencies: [], + writePaths: [], + acceptanceCriteria: ['reviewed'], + }); + await host.unload('roles/example'); + assert.equal(registry.get('plugin-advisor'), undefined); + assert.equal(routed.role.id, 'plugin-advisor'); + } finally { + await host.close(); + } +}); + +test('unauthorized isolated-write role rolls back every staged plugin contribution', async () => { + const registry = new AgentRoleRegistry(); + const host = createMossPluginHost(adapters(registry)); + try { + await assert.rejects( + host.install({ + id: 'roles/denied', + setup(context) { + context.registerAgentRole(advisor); + context.registerAgentRole(implementer); + }, + }), + /isolated-write role authorization/ + ); + assert.deepEqual(registry.list(), []); + assert.deepEqual(host.inspect().plugins, []); + } finally { + await host.close(); + } +}); From 76055931c05672e28eb63b43373598a4e6acb094 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 17:49:07 +0800 Subject: [PATCH 05/21] feat(agent): isolate implementation subagents --- .../moss-agent/src/core/agent/moss-agent.ts | 16 +- .../core/subagent/subagent-orchestrator.ts | 7 + .../src/core/subagent/subagent-runner.ts | 101 ++++++++--- .../moss-agent/src/core/tools/tool-types.ts | 13 +- packages/moss-agent/src/index.ts | 1 + .../orchestration/adaptive-workspace-lease.ts | 77 ++++++++ .../moss-agent/src/orchestration/index.ts | 1 + .../orchestration/workspace-lease-adapter.ts | 8 +- .../orchestration/workspace-lease-types.ts | 2 + .../moss-agent/src/tools/create-subagent.ts | 35 ++++ .../subagent-forced-finalization.spec.mjs | 28 +++ .../test/subagent-tools-lifecycle.spec.mjs | 5 +- .../test/subagent-workspace-lease.spec.mjs | 167 ++++++++++++++++++ .../moss-agent/test/workspace-lease.spec.mjs | 41 +++++ 14 files changed, 469 insertions(+), 33 deletions(-) create mode 100644 packages/moss-agent/src/orchestration/adaptive-workspace-lease.ts create mode 100644 packages/moss-agent/test/subagent-workspace-lease.spec.mjs diff --git a/packages/moss-agent/src/core/agent/moss-agent.ts b/packages/moss-agent/src/core/agent/moss-agent.ts index 74774cb7..c65c3544 100644 --- a/packages/moss-agent/src/core/agent/moss-agent.ts +++ b/packages/moss-agent/src/core/agent/moss-agent.ts @@ -125,6 +125,7 @@ import { loadContextEpoch, saveContextEpoch } from '../session/context-epoch-sto import { sanitizeSecrets } from '../../safety/secret-sanitizer.js'; import { MossError, ErrorCode, errorMessage, isMossError } from '../../errors.js'; import { InMemoryExecutionStore } from '../../orchestration/in-memory-execution-store.js'; +import { AdaptiveWorkspaceLeaseAdapter } from '../../orchestration/adaptive-workspace-lease.js'; import { DEFAULT_ORCHESTRATION_POLICY } from '../../orchestration/execution-projector.js'; import type { AgentRoleRegistry, @@ -193,7 +194,7 @@ export class MossAgent { /** Effective visible scheduling policy. @beta */ readonly orchestrationPolicy: OrchestrationPolicy; /** Isolated implementation workspace seam when configured by the host. @beta */ - readonly workspaceLeaseAdapter?: WorkspaceLeaseAdapter; + readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; /** Instance-local registry used by built-in and plugin agent roles. @beta */ readonly agentRoles: AgentRoleRegistry; /** @beta */ @@ -236,7 +237,12 @@ export class MossAgent { ...DEFAULT_ORCHESTRATION_POLICY, ...config.orchestrationPolicy, }; - this.workspaceLeaseAdapter = config.workspaceLeaseAdapter; + const workspaceDir = path.resolve(config.workspaceDir ?? process.cwd()); + this.workspaceLeaseAdapter = + config.workspaceLeaseAdapter ?? + new AdaptiveWorkspaceLeaseAdapter({ + rootDir: path.join(getMossWorkspacePaths(workspaceDir).runtimeDir, 'workspaces'), + }); this.toolHooks = new ToolHookRegistry(); this.toolHooks.registerPost(createSecretSanitizerHook(sanitizeSecrets)); setTraceRedactor(sanitizeSecrets); @@ -1183,6 +1189,7 @@ export class MossAgent { toolHooks: this.toolHooks, spawnRegistry: this.spawnRegistry, workspaceDir, + workspaceLeaseAdapter: this.workspaceLeaseAdapter, systemPromptParts, // Child agents inherit host coding gates (verify/todo/false-complete) so // fan_out_subagents / create_subagent cannot skip completion honesty. @@ -1231,6 +1238,7 @@ export class MossAgent { parentRunId: runId, scope: (params.scope ?? 'full') as SpawnToolScope, task: params.task, + ...(params.writePaths ? { writePaths: params.writePaths } : {}), ...(params.allowedTools !== undefined ? { allowedTools: params.allowedTools } : {}), model: params.model, ...(overrideContextTokens !== undefined @@ -1255,6 +1263,10 @@ export class MossAgent { ...(result.toolResults !== undefined ? { toolResults: result.toolResults } : {}), ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}), ...(result.error ? { error: result.error } : {}), + ...(result.workspaceLeaseId ? { workspaceLeaseId: result.workspaceLeaseId } : {}), + ...(result.patchRef ? { patchRef: result.patchRef } : {}), + ...(result.patchDigest ? { patchDigest: result.patchDigest } : {}), + ...(result.changedPaths ? { changedPaths: result.changedPaths } : {}), }; } finally { clearTimeout(timeout); diff --git a/packages/moss-agent/src/core/subagent/subagent-orchestrator.ts b/packages/moss-agent/src/core/subagent/subagent-orchestrator.ts index 0ab6ecca..7a8041ed 100644 --- a/packages/moss-agent/src/core/subagent/subagent-orchestrator.ts +++ b/packages/moss-agent/src/core/subagent/subagent-orchestrator.ts @@ -13,6 +13,9 @@ export interface SubAgentConfig { task: string; + /** Parent-relative paths that an implementation worker may modify. */ + writePaths?: readonly string[]; + /** Optional exact host allowlist, always intersected with the selected scope. */ allowedTools?: readonly string[]; @@ -60,6 +63,10 @@ export interface SubAgentResult { durationMs: number; success: boolean; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; } export interface FanOutResult { diff --git a/packages/moss-agent/src/core/subagent/subagent-runner.ts b/packages/moss-agent/src/core/subagent/subagent-runner.ts index 9e176720..2fda5839 100644 --- a/packages/moss-agent/src/core/subagent/subagent-runner.ts +++ b/packages/moss-agent/src/core/subagent/subagent-runner.ts @@ -1,4 +1,4 @@ -import fs from 'node:fs/promises'; +import { createHash } from 'node:crypto'; import path from 'node:path'; import type { Tool } from '../tools/tool-types.js'; import type { SubagentRunProgress } from '../tools/tool-types.js'; @@ -11,8 +11,8 @@ import { resolveSpawnToolSet, buildSubagentPromptAddon } from './spawn-profile.j import type { SpawnProfileRegistry, SpawnToolScope } from './spawn-profile.js'; import { runAgentLoop } from '../loop/agent-loop.js'; import { getRootLogger } from '../../logger.js'; -import { getMossWorkspacePaths } from '../../utils/workspace-paths.js'; import { errorMessage } from '../../errors.js'; +import type { WorkspaceLease, WorkspaceLeaseAdapter } from '../../orchestration/index.js'; const log = getRootLogger().child('subagent-runner'); @@ -57,31 +57,36 @@ function subtaskSummaryNeedsContractRepair(task: string, summary: string): boole async function prepareWorkspaceDir( parentWorkspaceDir: string, scope: SpawnToolScope, - runId: string -): Promise<{ workspaceDir: string; isolated: boolean }> { + runId: string, + parentRunId: string, + writePaths: readonly string[] | undefined, + adapter: WorkspaceLeaseAdapter | undefined +): Promise<{ workspaceDir: string; isolated: boolean; lease?: WorkspaceLease }> { const workspaceDir = path.resolve(parentWorkspaceDir); if (!scopeNeedsIsolation(scope)) { return { workspaceDir, isolated: false }; } - const isolatedDir = path.join(getMossWorkspacePaths(workspaceDir).runtimeDir, 'subagent', runId); - await fs.mkdir(isolatedDir, { recursive: true }); - log.info('created isolated workspace for child agent', { + if (!adapter) { + throw new Error(`scope ${scope} requires a workspace lease adapter`); + } + if (scope === 'full' && (!writePaths || writePaths.length === 0)) { + throw new Error('full sub-agent requires at least one declared write path'); + } + const leaseId = `subagent_${createHash('sha256').update(runId).digest('hex').slice(0, 24)}`; + const lease = await adapter.create({ + id: leaseId, + graphId: parentRunId, + nodeId: runId, + parentWorkspace: workspaceDir, + writePaths: scope === 'verify' ? ['.'] : (writePaths ?? []), + }); + log.info('created workspace lease for child agent', { runId, scope, - workspaceDir: isolatedDir, + leaseId, + workspaceDir: lease.workspacePath, }); - return { workspaceDir: isolatedDir, isolated: true }; -} - -async function cleanupIsolatedWorkspace(workspaceDir: string): Promise { - try { - await fs.rm(workspaceDir, { recursive: true, force: true }); - } catch (err) { - log.warn('failed to clean up isolated workspace', { - workspaceDir, - error: errorMessage(err), - }); - } + return { workspaceDir: lease.workspacePath, isolated: true, lease }; } export interface SubAgentRunnerDeps { @@ -113,6 +118,8 @@ export interface SubAgentRunnerDeps { workspaceDir?: string; + workspaceLeaseAdapter?: WorkspaceLeaseAdapter; + /** * Optional host completion gate (e.g. CLI coding gates). When set, child * agent loops inherit the same end-of-turn honesty checks as the parent @@ -221,11 +228,33 @@ export function createSubAgentRunner(deps: SubAgentRunnerDeps): SubAgentRunner { }); }; - const { workspaceDir, isolated } = await prepareWorkspaceDir( - deps.workspaceDir ?? process.cwd(), - config.scope, - childRunId - ); + let workspaceDir: string; + let isolated = false; + let workspaceLease: WorkspaceLease | undefined; + try { + const prepared = await prepareWorkspaceDir( + deps.workspaceDir ?? process.cwd(), + config.scope, + childRunId, + config.parentRunId, + config.writePaths, + deps.workspaceLeaseAdapter + ); + workspaceDir = prepared.workspaceDir; + isolated = prepared.isolated; + workspaceLease = prepared.lease; + } catch (err) { + const message = errorMessage(err); + return { + runId: childRunId, + summary: `Sub-agent failed: ${message}`, + toolResults: 0, + turns: 0, + durationMs: Date.now() - startedAt, + success: false, + error: message, + }; + } // Reserve the tail of every bounded child run for tool-free synthesis. // Without this, a diligent verifier can consume the entire wall-clock // budget on tool follow-ups and be aborted while streaming an otherwise @@ -451,6 +480,13 @@ export function createSubAgentRunner(deps: SubAgentRunnerDeps): SubAgentRunner { summaryPreview: finalSummary.slice(0, 240), }); + const workspacePatch = + config.scope === 'full' && workspaceLease + ? await deps.workspaceLeaseAdapter?.createPatch(workspaceLease) + : undefined; + if (config.scope === 'verify' && workspaceLease) { + await deps.workspaceLeaseAdapter?.release(workspaceLease.id, 'rejected'); + } return { runId: childRunId, summary: finalSummary, @@ -458,6 +494,14 @@ export function createSubAgentRunner(deps: SubAgentRunnerDeps): SubAgentRunner { turns: turnCount, durationMs: Date.now() - startedAt, success: true, + ...(workspaceLease ? { workspaceLeaseId: workspaceLease.id } : {}), + ...(workspacePatch + ? { + patchRef: workspacePatch.artifactRef, + patchDigest: workspacePatch.digest, + changedPaths: workspacePatch.changedPaths, + } + : {}), }; } catch (err) { const errorMsg = errorMessage(err); @@ -486,9 +530,10 @@ export function createSubAgentRunner(deps: SubAgentRunnerDeps): SubAgentRunner { } finally { clearTimeout(workPhaseTimer); signal.removeEventListener('abort', abortWorkPhase); - if (isolated) { - await cleanupIsolatedWorkspace(workspaceDir); - } + // Implementation leases and failed verification leases are deliberately + // retained for recovery. Successful verification releases its disposable + // snapshot above; the parent orchestrator owns implementation merge/cleanup. + void isolated; } }; } diff --git a/packages/moss-agent/src/core/tools/tool-types.ts b/packages/moss-agent/src/core/tools/tool-types.ts index f9d5ff97..4a24b97c 100644 --- a/packages/moss-agent/src/core/tools/tool-types.ts +++ b/packages/moss-agent/src/core/tools/tool-types.ts @@ -44,6 +44,8 @@ export interface ToolContext { askUserQuestion?: (question: string, abortSignal?: AbortSignal) => Promise; spawnSubagent?: (params: { task: string; + /** Parent-relative paths a full-scope worker may modify. */ + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -61,7 +63,12 @@ export interface ToolContext { /** Host-trusted expert instructions appended to the inherited system prompt. */ expertPrompt?: string; mode?: 'single' | 'fan-out' | 'pipeline'; - tasks?: Array<{ task: string; scope?: string; allowedTools?: readonly string[] }>; + tasks?: Array<{ + task: string; + scope?: string; + writePaths?: readonly string[]; + allowedTools?: readonly string[]; + }>; abortSignal?: AbortSignal; onProgress?: (progress: SubagentRunProgress) => void; }) => Promise<{ @@ -73,6 +80,10 @@ export interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; /** Resolve a model-requested expert id through the host-trusted per-agent registry. */ resolveSubagentExpert?: (id: string) => diff --git a/packages/moss-agent/src/index.ts b/packages/moss-agent/src/index.ts index 332b1aca..2f30e2a1 100644 --- a/packages/moss-agent/src/index.ts +++ b/packages/moss-agent/src/index.ts @@ -191,6 +191,7 @@ export type { } from './core/index.js'; export { DEFAULT_ORCHESTRATION_POLICY, + AdaptiveWorkspaceLeaseAdapter, CopyWorkspaceLeaseAdapter, AgentRoleRegistry, AssignmentRouter, diff --git a/packages/moss-agent/src/orchestration/adaptive-workspace-lease.ts b/packages/moss-agent/src/orchestration/adaptive-workspace-lease.ts new file mode 100644 index 00000000..73890943 --- /dev/null +++ b/packages/moss-agent/src/orchestration/adaptive-workspace-lease.ts @@ -0,0 +1,77 @@ +import { ErrorCode, MossError } from '../errors.js'; +import { runProcess } from '../utils/run-process.js'; +import { CopyWorkspaceLeaseAdapter } from './copy-workspace-lease.js'; +import { GitWorktreeWorkspaceLeaseAdapter } from './git-worktree-workspace-lease.js'; +import type { + CreateWorkspaceLeaseInput, + WorkspaceLease, + WorkspaceLeaseAdapter, + WorkspaceLeaseAdapterOptions, + WorkspaceLeaseReleaseReason, + WorkspaceMergeResult, + WorkspacePatch, +} from './workspace-lease-types.js'; + +/** Selects Git worktrees for repositories with a HEAD and copy snapshots otherwise. @beta */ +export class AdaptiveWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { + private readonly git: GitWorktreeWorkspaceLeaseAdapter; + private readonly copy: CopyWorkspaceLeaseAdapter; + + constructor(options: WorkspaceLeaseAdapterOptions) { + this.git = new GitWorktreeWorkspaceLeaseAdapter({ + ...options, + rootDir: `${options.rootDir}/git`, + }); + this.copy = new CopyWorkspaceLeaseAdapter({ + ...options, + rootDir: `${options.rootDir}/copy`, + }); + } + + async create(input: CreateWorkspaceLeaseInput): Promise { + if (this.load(input.id)) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `workspace lease already exists: ${input.id}`, + }); + } + let hasGitHead = false; + try { + await runProcess('git', { + args: ['rev-parse', '--verify', 'HEAD'], + cwd: input.parentWorkspace, + timeout: 10_000, + maxBuffer: 1024 * 1024, + }); + hasGitHead = true; + } catch {} + return hasGitHead ? this.git.create(input) : this.copy.create(input); + } + + load(leaseId: string): WorkspaceLease | undefined { + return this.git.load(leaseId) ?? this.copy.load(leaseId); + } + + list(): readonly WorkspaceLease[] { + return [...this.git.list(), ...this.copy.list()].sort( + (left, right) => right.updatedAt - left.updatedAt + ); + } + + createPatch(lease: WorkspaceLease): Promise { + return this.adapterFor(lease).createPatch(lease); + } + + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise { + return this.adapterFor(lease).merge(lease, patch); + } + + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise { + const lease = this.load(leaseId); + return lease ? this.adapterFor(lease).release(leaseId, reason) : Promise.resolve(); + } + + private adapterFor(lease: WorkspaceLease): WorkspaceLeaseAdapter { + return lease.kind === 'git-worktree' ? this.git : this.copy; + } +} diff --git a/packages/moss-agent/src/orchestration/index.ts b/packages/moss-agent/src/orchestration/index.ts index 172120fd..13cf2d99 100644 --- a/packages/moss-agent/src/orchestration/index.ts +++ b/packages/moss-agent/src/orchestration/index.ts @@ -12,6 +12,7 @@ export { } from './execution-graph-scheduler.js'; export { GitWorktreeWorkspaceLeaseAdapter } from './git-worktree-workspace-lease.js'; export { CopyWorkspaceLeaseAdapter } from './copy-workspace-lease.js'; +export { AdaptiveWorkspaceLeaseAdapter } from './adaptive-workspace-lease.js'; export { AgentRoleRegistry, cloneAgentRoleSnapshot } from './agent-role-registry.js'; export type { AgentRoleRegistryOptions } from './agent-role-registry.js'; export { AssignmentRouter } from './assignment-router.js'; diff --git a/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts b/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts index cdb95f71..1dd6a8c2 100644 --- a/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts +++ b/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts @@ -104,10 +104,16 @@ export abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter '--', '.', ]); + const patchId = `patch_${randomUUID()}`; + const artifactDirectory = path.join(this.leaseDirectory(loaded.id), 'artifacts'); + const artifactRef = path.join(artifactDirectory, `${patchId}.patch`); + fs.mkdirSync(artifactDirectory, { recursive: true, mode: 0o700 }); + fs.writeFileSync(artifactRef, diff.stdout, { mode: 0o600 }); return { - id: `patch_${randomUUID()}`, + id: patchId, leaseId: loaded.id, patch: diff.stdout, + artifactRef, digest: `sha256:${createHash('sha256').update(diff.stdout).digest('hex')}`, changedPaths, createdAt: this.now(), diff --git a/packages/moss-agent/src/orchestration/workspace-lease-types.ts b/packages/moss-agent/src/orchestration/workspace-lease-types.ts index 176d363b..c7acbba2 100644 --- a/packages/moss-agent/src/orchestration/workspace-lease-types.ts +++ b/packages/moss-agent/src/orchestration/workspace-lease-types.ts @@ -34,6 +34,8 @@ export interface WorkspacePatch { readonly id: string; readonly leaseId: string; readonly patch: string; + /** Durable local artifact containing the exact patch bytes. */ + readonly artifactRef: string; readonly digest: string; readonly changedPaths: readonly string[]; readonly createdAt: number; diff --git a/packages/moss-agent/src/tools/create-subagent.ts b/packages/moss-agent/src/tools/create-subagent.ts index a3401c4d..86bb59da 100644 --- a/packages/moss-agent/src/tools/create-subagent.ts +++ b/packages/moss-agent/src/tools/create-subagent.ts @@ -7,6 +7,7 @@ import type { SubagentRunProgress, Tool, ToolContext } from '../core/tools/tool- interface CreateSubagentInput { task: string; + writePaths?: string[]; expert?: string; scope?: 'read-only' | 'device-read' | 'full' | 'explore' | 'plan' | 'verify'; maxTurns?: number; @@ -120,6 +121,12 @@ export const createSubagentTool: Tool = { type: 'string', description: 'Task description for the sub-agent', }, + writePaths: { + type: 'array', + items: { type: 'string' }, + description: + 'Required for full scope: parent-relative files or directories this worker may modify.', + }, expert: { type: 'string', description: @@ -177,6 +184,9 @@ export const createSubagentTool: Tool = { expert?.maxTurns ?? input.maxTurns ?? defaultMaxTurnsForScope(selectedScope); const selectedTimeoutMs = resolveSubagentTimeoutMs(expert?.timeoutMs ?? input.timeoutMs); const selectedModel = expert?.model ?? input.model; + if (selectedScope === 'full' && (!input.writePaths || input.writePaths.length === 0)) { + return 'Error: full-scope sub-agents require at least one declared writePaths entry.'; + } if (input.background) { if (!ctx.asyncTaskRegistry) { @@ -215,6 +225,7 @@ export const createSubagentTool: Tool = { timeoutMs, payload: { task: input.task, + ...(input.writePaths ? { writePaths: input.writePaths } : {}), scope, maxTurns, timeoutMs, @@ -223,6 +234,7 @@ export const createSubagentTool: Tool = { async (_request: MossAsyncTaskStartRequest, signal: AbortSignal) => { const result = await ctx.spawnSubagent?.({ task: input.task, + ...(input.writePaths ? { writePaths: input.writePaths } : {}), scope, maxTurns, timeoutMs, @@ -253,6 +265,10 @@ export const createSubagentTool: Tool = { ...(result.toolResults !== undefined ? { toolResults: result.toolResults } : {}), ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}), ...(result.error ? { error: result.error } : {}), + ...(result.workspaceLeaseId ? { workspaceLeaseId: result.workspaceLeaseId } : {}), + ...(result.patchRef ? { patchRef: result.patchRef } : {}), + ...(result.patchDigest ? { patchDigest: result.patchDigest } : {}), + ...(result.changedPaths ? { changedPaths: result.changedPaths } : {}), ...(ok ? {} : { normalizedFailure: true }), }, }; @@ -270,6 +286,7 @@ export const createSubagentTool: Tool = { const maxTurns = selectedMaxTurns; const result = await ctx.spawnSubagent({ task: input.task, + ...(input.writePaths ? { writePaths: input.writePaths } : {}), scope, maxTurns, timeoutMs: selectedTimeoutMs, @@ -289,6 +306,9 @@ export const createSubagentTool: Tool = { `turns: ${result.turns ?? 0}`, `toolCalls: ${result.toolResults ?? 0}`, `elapsed: ${result.durationMs ?? 0} ms`, + ...(result.patchRef ? [`patch: ${result.patchRef}`] : []), + ...(result.patchDigest ? [`digest: ${result.patchDigest}`] : []), + ...(result.workspaceLeaseId ? [`lease: ${result.workspaceLeaseId}`] : []), ].join(' | '); // Prefix FAILED with Error: so isStringToolFailureResult / is_error and // failure-driven completion gates treat the child as a real failure. @@ -301,6 +321,7 @@ export const createSubagentTool: Tool = { interface FanOutTaskInput { task: string; + writePaths?: string[]; expert?: string; scope?: 'read-only' | 'device-read' | 'full' | 'explore' | 'plan' | 'verify'; label?: string; @@ -432,6 +453,11 @@ export const fanOutSubagentsTool: Tool = { type: 'object', properties: { task: { type: 'string', description: 'Task / system prompt for this sub-agent' }, + writePaths: { + type: 'array', + items: { type: 'string' }, + description: 'Required for full scope: parent-relative allowed write paths.', + }, expert: { type: 'string', description: 'Optional host-registered read-only expert id for this angle.', @@ -502,11 +528,19 @@ export const fanOutSubagentsTool: Tool = { (task, index) => experts[index]?.scope ?? inferFanOutScopeWithExploreDefault(task.task, task.scope) ); + const missingWritePathsIndex = tasks.findIndex( + (task, index) => + resolvedScopes[index] === 'full' && (!task.writePaths || task.writePaths.length === 0) + ); + if (missingWritePathsIndex >= 0) { + return `Error: full-scope fan-out task ${missingWritePathsIndex + 1} requires declared writePaths.`; + } const settled = await Promise.allSettled( tasks.map((t, i) => ctx.spawnSubagent!({ task: t.task, + ...(t.writePaths ? { writePaths: t.writePaths } : {}), scope: resolvedScopes[i], maxTurns: experts[i]?.maxTurns ?? maxTurns, timeoutMs: resolveSubagentTimeoutMs(experts[i]?.timeoutMs ?? timeoutMs), @@ -516,6 +550,7 @@ export const fanOutSubagentsTool: Tool = { tasks: tasks.map((item, taskIndex) => ({ task: item.task, scope: resolvedScopes[taskIndex], + ...(item.writePaths ? { writePaths: item.writePaths } : {}), })), abortSignal: ctx.abortSignal, ...(experts[i]?.model || t.model ? { model: experts[i]?.model ?? t.model } : {}), diff --git a/packages/moss-agent/test/subagent-forced-finalization.spec.mjs b/packages/moss-agent/test/subagent-forced-finalization.spec.mjs index 98c7eafb..b35fc7a6 100644 --- a/packages/moss-agent/test/subagent-forced-finalization.spec.mjs +++ b/packages/moss-agent/test/subagent-forced-finalization.spec.mjs @@ -22,6 +22,32 @@ const modelDef = { maxTokens: 1024, }; +const testWorkspaceLeaseAdapter = { + async create(input) { + return { + ...input, + kind: 'copy-snapshot', + status: 'active', + workspacePath: input.parentWorkspace, + baseRef: 'test', + baselineHashes: {}, + createdAt: 1, + updatedAt: 1, + }; + }, + load() {}, + list() { + return []; + }, + async createPatch() { + throw new Error('not used by verification'); + }, + async merge() { + throw new Error('not used by verification'); + }, + async release() {}, +}; + function responseStream(text) { return { async *[Symbol.asyncIterator]() { @@ -69,6 +95,7 @@ function responseStream(text) { reasoning: 'high', spawnRegistry: createSpawnProfileRegistryFromDefaults(), workspaceDir: process.cwd(), + workspaceLeaseAdapter: testWorkspaceLeaseAdapter, }); const result = await runner( @@ -121,6 +148,7 @@ function responseStream(text) { contextTokens: 32_000, spawnRegistry: createSpawnProfileRegistryFromDefaults(), workspaceDir: process.cwd(), + workspaceLeaseAdapter: testWorkspaceLeaseAdapter, }); const result = await runner( diff --git a/packages/moss-agent/test/subagent-tools-lifecycle.spec.mjs b/packages/moss-agent/test/subagent-tools-lifecycle.spec.mjs index 0c84e661..3d39dc2a 100644 --- a/packages/moss-agent/test/subagent-tools-lifecycle.spec.mjs +++ b/packages/moss-agent/test/subagent-tools-lifecycle.spec.mjs @@ -84,7 +84,10 @@ test('background sub-agent stop aborts the child signal', async () => { ); }), }; - const started = await createSubagentTool.execute({ task: 'wait forever', background: true }, ctx); + const started = await createSubagentTool.execute( + { task: 'wait forever', scope: 'explore', background: true }, + ctx + ); const taskId = taskIdFrom(started); assert.ok(taskId, started); const stopped = await subagentStopTool.execute({ taskId }, ctx); diff --git a/packages/moss-agent/test/subagent-workspace-lease.spec.mjs b/packages/moss-agent/test/subagent-workspace-lease.spec.mjs new file mode 100644 index 00000000..0ad50492 --- /dev/null +++ b/packages/moss-agent/test/subagent-workspace-lease.spec.mjs @@ -0,0 +1,167 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { createSubAgentRunner } from '../dist/core/subagent/subagent-runner.js'; + +const modelDef = { + id: 'test-model', + name: 'test-model', + api: 'openai-completions', + provider: 'test', + baseUrl: '', + reasoning: false, + input: ['text'], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 32_000, + maxTokens: 1024, +}; + +function completedStream() { + return { + async *[Symbol.asyncIterator]() { + yield { type: 'text_end', content: 'Implemented and verified.' }; + yield { + type: 'done', + message: { + role: 'assistant', + content: [{ type: 'text', text: 'Implemented and verified.' }], + stopReason: 'end_turn', + }, + }; + }, + async result() { + return { + role: 'assistant', + content: [{ type: 'text', text: 'Implemented and verified.' }], + stopReason: 'end_turn', + usage: { input: 1, output: 4, cacheRead: 0, cacheWrite: 0 }, + }; + }, + push() {}, + end() {}, + }; +} + +function runnerOptions(workspaceDir, workspaceLeaseAdapter) { + return { + parentTools: [], + streamFn: completedStream, + modelDef, + systemPrompt: 'test', + maxOutputTokens: 512, + contextTokens: 32_000, + workspaceDir, + ...(workspaceLeaseAdapter ? { workspaceLeaseAdapter } : {}), + }; +} + +test('full subagents fail closed without a lease adapter or declared write paths', async () => { + const signal = new AbortController().signal; + const noAdapter = await createSubAgentRunner(runnerOptions(process.cwd()))( + { + runId: 'parent/sub-no-adapter', + parentRunId: 'parent', + scope: 'full', + task: 'implement', + writePaths: ['src'], + }, + signal + ); + assert.equal(noAdapter.success, false); + assert.match(noAdapter.error, /workspace lease adapter/i); + + const adapter = { + create() { + throw new Error('must not create'); + }, + }; + const noPaths = await createSubAgentRunner(runnerOptions(process.cwd(), adapter))( + { + runId: 'parent/sub-no-paths', + parentRunId: 'parent', + scope: 'full', + task: 'implement', + }, + signal + ); + assert.equal(noPaths.success, false); + assert.match(noPaths.error, /declared write path/i); +}); + +test('full subagents execute in a retained lease and return a durable patch reference', async () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-subagent-lease-')); + const parent = path.join(temp, 'parent'); + const child = path.join(temp, 'lease', 'workspace'); + fs.mkdirSync(path.join(parent, 'src'), { recursive: true }); + fs.writeFileSync(path.join(parent, 'src', 'input.txt'), 'parent snapshot\n'); + fs.mkdirSync(path.join(child, 'src'), { recursive: true }); + fs.copyFileSync(path.join(parent, 'src', 'input.txt'), path.join(child, 'src', 'input.txt')); + const calls = []; + const adapter = { + async create(input) { + calls.push(['create', input]); + return { + ...input, + kind: 'copy-snapshot', + status: 'active', + workspacePath: child, + baseRef: 'base', + baselineHashes: {}, + createdAt: 1, + updatedAt: 1, + }; + }, + load() {}, + list() { + return []; + }, + async createPatch(lease) { + calls.push(['patch', lease.workspacePath]); + const artifactRef = path.join(temp, 'lease', 'patch.diff'); + fs.writeFileSync(artifactRef, 'diff --git a/src/input.txt b/src/input.txt\n'); + return { + id: 'patch-1', + leaseId: lease.id, + patch: fs.readFileSync(artifactRef, 'utf8'), + artifactRef, + digest: 'sha256:test', + changedPaths: ['src/input.txt'], + createdAt: 2, + }; + }, + async merge() { + throw new Error('parent orchestrator owns merge'); + }, + async release() { + throw new Error('unmerged implementation lease must be retained'); + }, + }; + + try { + const result = await createSubAgentRunner(runnerOptions(parent, adapter))( + { + runId: 'parent/sub-implementation', + parentRunId: 'parent', + scope: 'full', + task: 'implement within src', + writePaths: ['src'], + maxTurns: 1, + }, + new AbortController().signal + ); + assert.equal(result.success, true); + assert.equal(result.workspaceLeaseId, calls[0][1].id); + assert.deepEqual(calls[0][1].writePaths, ['src']); + assert.equal(result.patchRef, path.join(temp, 'lease', 'patch.diff')); + assert.equal(result.patchDigest, 'sha256:test'); + assert.deepEqual(result.changedPaths, ['src/input.txt']); + assert.equal(calls[1][1], child); + assert.equal(fs.existsSync(child), true, 'unmerged lease must survive the child run'); + } finally { + fs.rmSync(temp, { recursive: true, force: true }); + } +}); diff --git a/packages/moss-agent/test/workspace-lease.spec.mjs b/packages/moss-agent/test/workspace-lease.spec.mjs index 0f5eac3a..9318e35d 100644 --- a/packages/moss-agent/test/workspace-lease.spec.mjs +++ b/packages/moss-agent/test/workspace-lease.spec.mjs @@ -7,6 +7,7 @@ import { spawnSync } from 'node:child_process'; import test from 'node:test'; import { + AdaptiveWorkspaceLeaseAdapter, CopyWorkspaceLeaseAdapter, GitWorktreeWorkspaceLeaseAdapter, } from '../dist/orchestration/index.js'; @@ -73,6 +74,7 @@ test('git worktree lease snapshots dirty tracked, staged, and untracked files wi assert.deepEqual(patch.changedPaths, ['src/app.txt']); assert.match(patch.patch, /worker change/); assert.match(patch.digest, /^sha256:/); + assert.equal(fs.readFileSync(patch.artifactRef, 'utf8'), patch.patch); const restored = new GitWorktreeWorkspaceLeaseAdapter({ rootDir: leases }).load('lease-git'); assert.equal(restored?.baseRef, lease.baseRef); @@ -83,6 +85,45 @@ test('git worktree lease snapshots dirty tracked, staged, and untracked files wi } }); +test('adaptive leases select git worktrees with HEAD and copy snapshots otherwise', async () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-workspace-adaptive-')); + const repository = path.join(temp, 'repo'); + const plain = path.join(temp, 'plain'); + createDirtyRepository(repository); + fs.mkdirSync(plain, { recursive: true }); + write(path.join(plain, 'input.txt'), 'plain\n'); + const adapter = new AdaptiveWorkspaceLeaseAdapter({ rootDir: path.join(temp, 'leases') }); + try { + const gitLease = await adapter.create({ + id: 'adaptive-git', + graphId: 'graph', + nodeId: 'git', + parentWorkspace: repository, + writePaths: ['src'], + }); + const copyLease = await adapter.create({ + id: 'adaptive-copy', + graphId: 'graph', + nodeId: 'copy', + parentWorkspace: plain, + writePaths: ['input.txt'], + }); + assert.equal(gitLease.kind, 'git-worktree'); + assert.equal(copyLease.kind, 'copy-snapshot'); + assert.deepEqual( + adapter + .list() + .map((lease) => lease.id) + .sort(), + ['adaptive-copy', 'adaptive-git'] + ); + } finally { + await adapter.release('adaptive-git', 'cancelled'); + await adapter.release('adaptive-copy', 'cancelled'); + fs.rmSync(temp, { recursive: true, force: true }); + } +}); + test('guarded merge refuses a parent race and never overwrites the edited file', async () => { const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-workspace-conflict-')); const repository = path.join(temp, 'repo'); From 4cdb456e389fed9fe54689f5328dd64591f599de Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 17:53:58 +0800 Subject: [PATCH 06/21] refactor(agent): own durable orchestration state --- .../src/core/agent/moss-agent-types.ts | 5 +- .../moss-agent/src/core/agent/moss-agent.ts | 20 ++++-- packages/moss-agent/src/index.ts | 1 + packages/moss-agent/src/plan-execute/index.ts | 1 + .../src/plan-execute/plan-controller-store.ts | 70 ++++++++++++++----- .../moss-agent/src/plan-execute/plan-tools.ts | 23 +++--- .../moss-agent/src/runtime/shared-runtime.ts | 27 +++++-- packages/moss-agent/src/tools/builtin.ts | 23 +++++- .../moss-agent-durable-orchestration.spec.mjs | 54 ++++++++++++++ .../plan-controller-store-isolation.spec.mjs | 21 ++++++ 10 files changed, 202 insertions(+), 43 deletions(-) create mode 100644 packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs create mode 100644 packages/moss-agent/test/plan-controller-store-isolation.spec.mjs diff --git a/packages/moss-agent/src/core/agent/moss-agent-types.ts b/packages/moss-agent/src/core/agent/moss-agent-types.ts index f6336960..f64b665d 100644 --- a/packages/moss-agent/src/core/agent/moss-agent-types.ts +++ b/packages/moss-agent/src/core/agent/moss-agent-types.ts @@ -32,6 +32,7 @@ import type { OrchestrationPolicy, WorkspaceLeaseAdapter, } from '../../orchestration/index.js'; +import type { PlanControllerStore } from '../../plan-execute/plan-controller-store.js'; export interface ProviderConfig { llmProvider: LLMProvider; @@ -122,12 +123,14 @@ export interface MossAgentConfig sessionStore: SessionStore; workspaceDir?: string; - /** Authoritative execution graph store. Defaults to an instance-local in-memory adapter. @beta */ + /** Authoritative graph store. Defaults to durable workspace JSONL. @beta */ executionStore?: ExecutionStore; /** Visible scheduling policy applied to graphs created by this agent. @beta */ orchestrationPolicy?: Partial; /** Host-owned isolated implementation workspace seam. @beta */ workspaceLeaseAdapter?: WorkspaceLeaseAdapter; + /** Instance-owned projection used by legacy Plan tools during graph migration. @beta */ + planControllerStore?: PlanControllerStore; /** Instance-local trusted agent roles. @beta */ agentRoles?: readonly AgentRoleDefinition[]; /** Optional plugin-populated role registry. @beta */ diff --git a/packages/moss-agent/src/core/agent/moss-agent.ts b/packages/moss-agent/src/core/agent/moss-agent.ts index c65c3544..1819db58 100644 --- a/packages/moss-agent/src/core/agent/moss-agent.ts +++ b/packages/moss-agent/src/core/agent/moss-agent.ts @@ -75,7 +75,7 @@ import { clearPendingStructuredValidation, } from '../../structured-output/structured-output-tool.js'; import { evaluatePlanCompletionGate } from '../../plan-execute/plan-completion-gate.js'; -import { getActivePlanForSession } from '../../plan-execute/plan-controller-store.js'; +import { PlanControllerStore } from '../../plan-execute/plan-controller-store.js'; import { createSpawnProfileRegistryFromDefaults, type SpawnProfileRegistry, @@ -124,7 +124,7 @@ import { initializeEpoch, reconcileEpoch, type ContextSources } from '../session import { loadContextEpoch, saveContextEpoch } from '../session/context-epoch-store.js'; import { sanitizeSecrets } from '../../safety/secret-sanitizer.js'; import { MossError, ErrorCode, errorMessage, isMossError } from '../../errors.js'; -import { InMemoryExecutionStore } from '../../orchestration/in-memory-execution-store.js'; +import { JsonlExecutionStore } from '../../orchestration/jsonl-execution-store.js'; import { AdaptiveWorkspaceLeaseAdapter } from '../../orchestration/adaptive-workspace-lease.js'; import { DEFAULT_ORCHESTRATION_POLICY } from '../../orchestration/execution-projector.js'; import type { @@ -197,6 +197,8 @@ export class MossAgent { readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; /** Instance-local registry used by built-in and plugin agent roles. @beta */ readonly agentRoles: AgentRoleRegistry; + /** Instance-local compatibility projection for legacy Plan tools. @beta */ + readonly planControllerStore: PlanControllerStore; /** @beta */ readonly plugins: MossPluginHost; private readonly knowledge: KnowledgeRegistry; @@ -232,17 +234,22 @@ export class MossAgent { this.expertRegistry = config.subagentExpertRegistry ?? new SubagentExpertRegistry(config.subagentExperts); this.asyncTasks = config.asyncTaskRegistry ?? createInMemoryMossAsyncTaskRegistry(); - this.executionStore = config.executionStore ?? new InMemoryExecutionStore(); + const workspaceDir = path.resolve(config.workspaceDir ?? process.cwd()); + this.executionStore = + config.executionStore ?? + new JsonlExecutionStore({ + rootDir: path.join(getMossWorkspacePaths(workspaceDir).runtimeDir, 'executions'), + }); this.orchestrationPolicy = { ...DEFAULT_ORCHESTRATION_POLICY, ...config.orchestrationPolicy, }; - const workspaceDir = path.resolve(config.workspaceDir ?? process.cwd()); this.workspaceLeaseAdapter = config.workspaceLeaseAdapter ?? new AdaptiveWorkspaceLeaseAdapter({ rootDir: path.join(getMossWorkspacePaths(workspaceDir).runtimeDir, 'workspaces'), }); + this.planControllerStore = config.planControllerStore ?? new PlanControllerStore(); this.toolHooks = new ToolHookRegistry(); this.toolHooks.registerPost(createSecretSanitizerHook(sanitizeSecrets)); setTraceRedactor(sanitizeSecrets); @@ -1565,7 +1572,10 @@ export class MossAgent { if (request.sessionKey && this.config.hostProvidesPlanStore !== false) { const planGate = evaluatePlanCompletionGate( { sessionKey: request.sessionKey, stopReason: request.stopReason }, - { getActivePlanForSession } + { + getActivePlanForSession: (key) => + this.planControllerStore.getActivePlanForSession(key), + } ); if (!planGate.ok) return planGate; } else if ( diff --git a/packages/moss-agent/src/index.ts b/packages/moss-agent/src/index.ts index 2f30e2a1..de8748d1 100644 --- a/packages/moss-agent/src/index.ts +++ b/packages/moss-agent/src/index.ts @@ -604,6 +604,7 @@ export type { export { PlanExecuteController, + PlanControllerStore, createPlanTool, planTool, createPlanStepTool, diff --git a/packages/moss-agent/src/plan-execute/index.ts b/packages/moss-agent/src/plan-execute/index.ts index 75f6ec3a..dda82cab 100644 --- a/packages/moss-agent/src/plan-execute/index.ts +++ b/packages/moss-agent/src/plan-execute/index.ts @@ -20,6 +20,7 @@ export { } from './plan-tools.js'; export { + PlanControllerStore, getPlanController, getSharedPlanController, setActivePlanId, diff --git a/packages/moss-agent/src/plan-execute/plan-controller-store.ts b/packages/moss-agent/src/plan-execute/plan-controller-store.ts index a2ec892a..8d513b25 100644 --- a/packages/moss-agent/src/plan-execute/plan-controller-store.ts +++ b/packages/moss-agent/src/plan-execute/plan-controller-store.ts @@ -3,40 +3,72 @@ import type { Plan } from './plan-execute-controller.js'; const DEFAULT_CONFIG = { maxReplans: 3, requireApproval: true, autoApproveSimple: true }; -const sessionControllers = new Map(); -let sharedController: PlanExecuteController | null = null; -const activePlanIdBySession = new Map(); +/** Instance-owned plan compatibility projection. @beta */ +export class PlanControllerStore { + private readonly sessionControllers = new Map(); + private sharedController: PlanExecuteController | null = null; + private readonly activePlanIdBySession = new Map(); -export function getPlanController(sessionKey: string): PlanExecuteController { - let c = sessionControllers.get(sessionKey); - if (!c) { - c = new PlanExecuteController({ ...DEFAULT_CONFIG }); - sessionControllers.set(sessionKey, c); + getPlanController(sessionKey: string): PlanExecuteController { + let controller = this.sessionControllers.get(sessionKey); + if (!controller) { + controller = new PlanExecuteController({ ...DEFAULT_CONFIG }); + this.sessionControllers.set(sessionKey, controller); + } + return controller; + } + + getSharedPlanController(): PlanExecuteController { + if (!this.sharedController) { + this.sharedController = new PlanExecuteController({ ...DEFAULT_CONFIG }); + } + return this.sharedController; + } + + setActivePlanId(sessionKey: string, planId: string): void { + this.activePlanIdBySession.set(sessionKey, planId); + } + + getActivePlanId(sessionKey: string): string | undefined { + return this.activePlanIdBySession.get(sessionKey); } - return c; + + getActivePlanForSession(sessionKey: string): Plan | null { + const id = this.activePlanIdBySession.get(sessionKey); + if (!id) return null; + return this.getPlanController(sessionKey).getPlan(id); + } + + reset(): void { + this.sessionControllers.clear(); + this.activePlanIdBySession.clear(); + this.sharedController = null; + } +} + +// One-release compatibility projection for hosts importing the legacy functions. +const compatibilityStore = new PlanControllerStore(); + +export function getPlanController(sessionKey: string): PlanExecuteController { + return compatibilityStore.getPlanController(sessionKey); } export function getSharedPlanController(): PlanExecuteController { - if (!sharedController) sharedController = new PlanExecuteController({ ...DEFAULT_CONFIG }); - return sharedController; + return compatibilityStore.getSharedPlanController(); } export function setActivePlanId(sessionKey: string, planId: string): void { - activePlanIdBySession.set(sessionKey, planId); + compatibilityStore.setActivePlanId(sessionKey, planId); } export function getActivePlanId(sessionKey: string): string | undefined { - return activePlanIdBySession.get(sessionKey); + return compatibilityStore.getActivePlanId(sessionKey); } export function getActivePlanForSession(sessionKey: string): Plan | null { - const id = activePlanIdBySession.get(sessionKey); - if (!id) return null; - return getPlanController(sessionKey).getPlan(id); + return compatibilityStore.getActivePlanForSession(sessionKey); } export function resetPlanControllerStoreForTests(): void { - sessionControllers.clear(); - activePlanIdBySession.clear(); - sharedController = null; + compatibilityStore.reset(); } diff --git a/packages/moss-agent/src/plan-execute/plan-tools.ts b/packages/moss-agent/src/plan-execute/plan-tools.ts index d35aa28c..90318967 100644 --- a/packages/moss-agent/src/plan-execute/plan-tools.ts +++ b/packages/moss-agent/src/plan-execute/plan-tools.ts @@ -5,6 +5,7 @@ import type { AcceptSpec } from '../acceptance/types.js'; import { validateAcceptSpecs } from '../acceptance/accept-spec-validator.js'; import { ContractRegistry } from '../acceptance/contract-registry.js'; import { SkillRegistry } from '../skills/registry.js'; +import type { PlanControllerStore } from './plan-controller-store.js'; import { getPlanController, getSharedPlanController, @@ -218,7 +219,7 @@ export const activePlanProvider: ActivePlanProvider = { get: getActivePlanForSession, }; -export function createPlanTool(): Tool { +export function createPlanTool(store?: PlanControllerStore): Tool { return { name: 'plan', description: @@ -331,8 +332,8 @@ export function createPlanTool(): Tool { async execute(input, ctx) { try { const controller = ctx.sessionKey - ? getPlanController(ctx.sessionKey) - : getSharedPlanController(); + ? (store?.getPlanController(ctx.sessionKey) ?? getPlanController(ctx.sessionKey)) + : (store?.getSharedPlanController() ?? getSharedPlanController()); switch (input.action) { case 'create': { @@ -451,7 +452,10 @@ export function createPlanTool(): Tool { } const ok = controller.approvePlan(input.planId); if (!ok) return `Error: could not approve plan ${input.planId}.`; - if (ctx.sessionKey) setActivePlanId(ctx.sessionKey, input.planId); + if (ctx.sessionKey) { + if (store) store.setActivePlanId(ctx.sessionKey, input.planId); + else setActivePlanId(ctx.sessionKey, input.planId); + } // Claude ExitPlanMode parity (light): approving a plan is the user's // go-ahead to leave read-only planning and begin execution. If the // session is still in interactionMode=plan, drop to default so @@ -477,7 +481,10 @@ export function createPlanTool(): Tool { return `Error: machine acceptance is incomplete:\n${acceptanceIssues.map((e) => `- ${e}`).join('\n')}`; const ok = controller.startExecution(input.planId); if (!ok) return `Error: could not start plan ${input.planId}. Ensure it is approved.`; - if (ctx.sessionKey) setActivePlanId(ctx.sessionKey, input.planId); + if (ctx.sessionKey) { + if (store) store.setActivePlanId(ctx.sessionKey, input.planId); + else setActivePlanId(ctx.sessionKey, input.planId); + } const leftPlanMode = leavePlanModeForExecution(); const plan = controller.getPlan(input.planId); @@ -552,7 +559,7 @@ export function createPlanTool(): Tool { }; } -export function createPlanStepTool(): Tool { +export function createPlanStepTool(store?: PlanControllerStore): Tool { return { name: 'plan_step', description: @@ -590,8 +597,8 @@ export function createPlanStepTool(): Tool { async execute(input, ctx) { try { const controller = ctx.sessionKey - ? getPlanController(ctx.sessionKey) - : getSharedPlanController(); + ? (store?.getPlanController(ctx.sessionKey) ?? getPlanController(ctx.sessionKey)) + : (store?.getSharedPlanController() ?? getSharedPlanController()); switch (input.action) { case 'complete': { diff --git a/packages/moss-agent/src/runtime/shared-runtime.ts b/packages/moss-agent/src/runtime/shared-runtime.ts index 65dbea1c..53c952bd 100644 --- a/packages/moss-agent/src/runtime/shared-runtime.ts +++ b/packages/moss-agent/src/runtime/shared-runtime.ts @@ -26,7 +26,8 @@ import { createOpinionSink } from '../acceptance/promotion-opinion-sink.js'; import { createObjectiveVerifierHook } from '../core/tools/objective-verifier-hook.js'; import { wrapWithTerminalArbitration } from '../core/tools/terminal-arbitration-gate.js'; import { wrapWithPromotionObservation } from '../core/tools/promotion-completion-gate.js'; -import { getActivePlanForSession } from '../plan-execute/plan-controller-store.js'; +import { PlanControllerStore } from '../plan-execute/plan-controller-store.js'; +import { createPlanStepTool, createPlanTool } from '../plan-execute/plan-tools.js'; import { builtinTools } from '../tools/builtin.js'; import { createLoadSkillTool } from '../tools/skill-tools.js'; import type { DeviceReadonlyExecutor } from '../core/tools/device-readonly-executor.js'; @@ -155,14 +156,21 @@ const DESKTOP_SAFE_TOOLS = new Set([ 'plan_step', ]); -function selectTools(profile: MossRuntimeToolProfile, skillRegistry: SkillRegistry): Tool[] { +function selectTools( + profile: MossRuntimeToolProfile, + skillRegistry: SkillRegistry, + planControllerStore: MossAgent['planControllerStore'] +): Tool[] { const selected = profile === 'full' ? [...builtinTools] : builtinTools.filter((tool) => DESKTOP_SAFE_TOOLS.has(tool.name)); - return selected.map((tool) => - tool.name === 'load_skill' ? createLoadSkillTool(skillRegistry) : tool - ); + return selected.map((tool) => { + if (tool.name === 'load_skill') return createLoadSkillTool(skillRegistry); + if (tool.name === 'plan') return createPlanTool(planControllerStore); + if (tool.name === 'plan_step') return createPlanStepTool(planControllerStore); + return tool; + }); } function readSkillBody(skill: SkillMeta): string | undefined { @@ -193,7 +201,10 @@ export async function createMossRuntime(options: CreateMossRuntimeOptions): Prom const selfEvolutionEnabled = options.enableSelfEvolution !== false; const terminalVerdictLog = new TerminalVerdictLog({ baseDir: services.memoryDir }); - const planProvider = { get: getActivePlanForSession }; + const planControllerStore = new PlanControllerStore(); + const planProvider = { + get: (sessionKey: string) => planControllerStore.getActivePlanForSession(sessionKey), + }; const deviceExecutor = options.deviceExecutor ?? { current: null }; const promotionCoordinator = new PromotionCoordinator({ candidateSource: createTerminalCandidateSource({ terminalVerdictLog }), @@ -229,10 +240,12 @@ export async function createMossRuntime(options: CreateMossRuntimeOptions): Prom skillPipeline: services.skillPipeline, skillRegistry: services.skillRegistry, memoryContextProvider: () => services.memoryManager.buildDigest(), + planControllerStore, ...(completionGate ? { completionGate } : {}), }); - for (const tool of selectTools(toolProfile, services.skillRegistry)) agent.tools.register(tool); + for (const tool of selectTools(toolProfile, services.skillRegistry, agent.planControllerStore)) + agent.tools.register(tool); for (const tool of options.extraTools ?? []) agent.tools.replace(tool, 'host:extra-tools'); try { for (const plugin of options.plugins ?? []) await agent.plugins.install(plugin); diff --git a/packages/moss-agent/src/tools/builtin.ts b/packages/moss-agent/src/tools/builtin.ts index 362c9e79..d6f71aa0 100644 --- a/packages/moss-agent/src/tools/builtin.ts +++ b/packages/moss-agent/src/tools/builtin.ts @@ -23,7 +23,13 @@ import { webBrowserAgentTool } from '../web-browser/browser-agent-tool.js'; import { structuredOutputTool } from '../structured-output/structured-output-tool.js'; import { evalTool } from '../eval/eval-tool.js'; import { harnessTools } from './harness-tools.js'; -import { planTool, planStepTool } from '../plan-execute/plan-tools.js'; +import { + createPlanStepTool, + createPlanTool, + planStepTool, + planTool, +} from '../plan-execute/plan-tools.js'; +import type { PlanControllerStore } from '../plan-execute/plan-controller-store.js'; import { atomicWriteFile } from '../utils/atomic-write.js'; import { getMossWorkspacePaths } from '../utils/workspace-paths.js'; import { @@ -506,12 +512,23 @@ export const builtinTools: Tool[] = [ ...harnessTools, ]; -export function registerBuiltinTools(agent: { tools: { register: (tool: Tool) => void } }): void { +export function registerBuiltinTools(agent: { + tools: { register: (tool: Tool) => void }; + planControllerStore?: PlanControllerStore; +}): void { const skillRegistry = (agent as { config?: { skillRegistry?: SkillRegistry } }).config ?.skillRegistry; for (const tool of builtinTools) { + const instanceTool = + tool.name === 'plan' && agent.planControllerStore + ? createPlanTool(agent.planControllerStore) + : tool.name === 'plan_step' && agent.planControllerStore + ? createPlanStepTool(agent.planControllerStore) + : tool; agent.tools.register( - tool.name === 'load_skill' && skillRegistry ? createLoadSkillTool(skillRegistry) : tool + instanceTool.name === 'load_skill' && skillRegistry + ? createLoadSkillTool(skillRegistry) + : instanceTool ); } } diff --git a/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs b/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs new file mode 100644 index 00000000..8b37378d --- /dev/null +++ b/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { MossAgent } from '../dist/core/agent/moss-agent.js'; +import { InMemorySessionStore } from '../dist/core/session/session.js'; +import { JsonlExecutionStore } from '../dist/orchestration/index.js'; + +const provider = { + id: 'unused', + displayName: 'unused', + capabilities: { streaming: false }, + async complete() { + return { content: [{ type: 'text', text: 'unused' }], stopReason: 'end_turn' }; + }, +}; + +function createAgent(workspaceDir) { + return new MossAgent({ + llmProvider: provider, + sessionStore: new InMemorySessionStore(), + workspaceDir, + enableSteering: false, + }); +} + +test('MossAgent defaults to durable execution state shared across restarts', async () => { + const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-agent-durable-')); + const first = createAgent(workspaceDir); + try { + assert.ok(first.executionStore instanceof JsonlExecutionStore); + first.executionStore.create({ + id: 'restartable', + goal: 'survive restart', + sessionId: 'session', + nodes: [], + policy: { maxConcurrency: 4, maxAttemptsPerNode: 3, strictCompletion: true }, + }); + await first.close(); + + const second = createAgent(workspaceDir); + try { + assert.equal(second.executionStore.load('restartable').goal, 'survive restart'); + assert.equal(second.planControllerStore.getActivePlanForSession('session'), null); + } finally { + await second.close(); + } + } finally { + fs.rmSync(workspaceDir, { recursive: true, force: true }); + } +}); diff --git a/packages/moss-agent/test/plan-controller-store-isolation.spec.mjs b/packages/moss-agent/test/plan-controller-store-isolation.spec.mjs new file mode 100644 index 00000000..6b5625c6 --- /dev/null +++ b/packages/moss-agent/test/plan-controller-store-isolation.spec.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { PlanControllerStore } from '../dist/plan-execute/index.js'; + +test('plan controller state is isolated per agent-owned store', () => { + const first = new PlanControllerStore(); + const second = new PlanControllerStore(); + const plan = first + .getPlanController('shared-session') + .createPlan('first agent goal', [{ step: 1, description: 'only first sees this' }]); + first.setActivePlanId('shared-session', plan.id); + + assert.equal(first.getActivePlanForSession('shared-session')?.id, plan.id); + assert.equal(second.getActivePlanForSession('shared-session'), null); + assert.notEqual( + first.getPlanController('shared-session'), + second.getPlanController('shared-session') + ); +}); From b3fb98ad6ec93a6b4fa1281f2cf194c6d6af3e19 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 17:57:12 +0800 Subject: [PATCH 07/21] feat(agent): add durable task controls --- .../src/cli/interactive-commands.ts | 8 +++ packages/moss-agent/src/cli/repl.ts | 7 +++ packages/moss-agent/src/cli/task-command.ts | 45 ++++++++++++++ packages/moss-agent/src/cli/tui.ts | 5 ++ .../moss-agent/src/core/agent/moss-agent.ts | 4 ++ packages/moss-agent/src/index.ts | 1 + .../execution-task-controller.ts | 58 +++++++++++++++++++ .../moss-agent/src/orchestration/index.ts | 1 + .../test/execution-task-controller.spec.mjs | 35 +++++++++++ .../moss-agent/test/task-command.spec.mjs | 17 ++++++ 10 files changed, 181 insertions(+) create mode 100644 packages/moss-agent/src/cli/task-command.ts create mode 100644 packages/moss-agent/src/orchestration/execution-task-controller.ts create mode 100644 packages/moss-agent/test/execution-task-controller.spec.mjs create mode 100644 packages/moss-agent/test/task-command.spec.mjs diff --git a/packages/moss-agent/src/cli/interactive-commands.ts b/packages/moss-agent/src/cli/interactive-commands.ts index ae156b37..f05f4e5b 100644 --- a/packages/moss-agent/src/cli/interactive-commands.ts +++ b/packages/moss-agent/src/cli/interactive-commands.ts @@ -34,6 +34,14 @@ export const INTERACTIVE_COMMAND_SECTIONS: readonly InteractiveCommandSection[] command: '/goal ', description: 'set a goal and keep working toward it until you mark it done', }, + { + command: '/tasks', + description: 'list durable execution graphs and their recovery state', + }, + { + command: '/task ', + description: 'inspect or control one durable execution graph', + }, { command: '/compact', description: 'compress older conversation history into a summary' }, { command: '/compact [instructions]', diff --git a/packages/moss-agent/src/cli/repl.ts b/packages/moss-agent/src/cli/repl.ts index 24e16e1d..026ad5ad 100644 --- a/packages/moss-agent/src/cli/repl.ts +++ b/packages/moss-agent/src/cli/repl.ts @@ -6,6 +6,7 @@ import type { SkillLearner } from '../core/memory/skill-learner.js'; import { handleGoalCommand } from '../goal.js'; import { setCliApprovalAsker } from './approval.js'; import { handleCompactCommand } from './compact-command.js'; +import { handleTaskCommand } from './task-command.js'; import { resolveLoopMaxIterations } from './loop-tui-events.js'; import { formatCommunityAuthLoginError, formatCommunityAuthStatus } from './community-auth.js'; import { runRegistryCommand, unknownSlashCommandLines } from './commands/registry.js'; @@ -409,6 +410,12 @@ export async function runInteractive( continue; } + if (msg === '/tasks' || msg === '/task' || msg.startsWith('/task ')) { + console.error(handleTaskCommand(agent, msg)); + rl.prompt(); + continue; + } + if (msg === '/goal' || msg.startsWith('/goal ')) { const result = await handleGoalCommand({ agent, diff --git a/packages/moss-agent/src/cli/task-command.ts b/packages/moss-agent/src/cli/task-command.ts new file mode 100644 index 00000000..dc1e3a39 --- /dev/null +++ b/packages/moss-agent/src/cli/task-command.ts @@ -0,0 +1,45 @@ +import type { MossAgent } from '../core/agent/moss-agent.js'; +import type { ExecutionGraphSnapshot } from '../orchestration/index.js'; +import { errorMessage } from '../errors.js'; + +function summary(graph: ExecutionGraphSnapshot): string { + const nodes = Object.values(graph.nodes); + const done = nodes.filter((node) => ['succeeded', 'skipped'].includes(node.status)).length; + return `${graph.id} ${graph.status} rev=${graph.revision} nodes=${done}/${nodes.length} ${graph.goal}`; +} + +function details(graph: ExecutionGraphSnapshot): string { + const nodeLines = Object.values(graph.nodes).map( + (node) => + ` ${node.id} ${node.status} kind=${node.kind} attempts=${node.attempts}` + + (node.error ? ` error=${node.error}` : '') + ); + return [summary(graph), ...nodeLines, `evidence=${graph.evidence.length}`].join('\n'); +} + +/** Execute /tasks and /task control commands against the shared graph controller. @internal */ +export function handleTaskCommand(agent: Pick, input: string): string { + try { + const parts = input.trim().split(/\s+/); + if (parts[0] === '/tasks') { + const tasks = agent.tasks.list(); + return tasks.length ? tasks.map(summary).join('\n') : 'No durable tasks.'; + } + const action = parts[1]; + const graphId = parts[2]; + if (!action || !graphId) { + return 'Usage: /task [node-id]'; + } + if (action === 'inspect') return details(agent.tasks.inspect(graphId)); + if (action === 'resume') return `Resumed ${summary(agent.tasks.resume(graphId))}`; + if (action === 'stop') return `Stopped ${summary(agent.tasks.stop(graphId))}`; + if (action === 'retry') { + const nodeId = parts[3]; + if (!nodeId) return 'Usage: /task retry '; + return `Retry requested: ${details(agent.tasks.retry(graphId, nodeId))}`; + } + return 'Usage: /task [node-id]'; + } catch (error) { + return `Error: ${errorMessage(error)}`; + } +} diff --git a/packages/moss-agent/src/cli/tui.ts b/packages/moss-agent/src/cli/tui.ts index db5bfc88..05592a24 100644 --- a/packages/moss-agent/src/cli/tui.ts +++ b/packages/moss-agent/src/cli/tui.ts @@ -62,6 +62,7 @@ import { } from './attachments.js'; import { prepareClipboardAttachment } from './clipboard-image.js'; import { handleCompactCommand } from './compact-command.js'; +import { handleTaskCommand } from './task-command.js'; import { inputPlaceholder, useCommandInput, visibleInput } from './command-input.js'; import { extractLatestTodosFromMessages } from './coding-completion-gate.js'; import { formatCommunityAuthLoginError, formatCommunityAuthStatus } from './community-auth.js'; @@ -4140,6 +4141,10 @@ export function MossTui({ addTranscript('system', 'Usage: /auth '); return true; } + if (message === '/tasks' || message === '/task' || message.startsWith('/task ')) { + addTranscript('system', handleTaskCommand(agent, message)); + return true; + } if (message === '/goal' || message.startsWith('/goal ')) { const result = await handleGoalCommand({ agent, diff --git a/packages/moss-agent/src/core/agent/moss-agent.ts b/packages/moss-agent/src/core/agent/moss-agent.ts index 1819db58..6ed5e3ee 100644 --- a/packages/moss-agent/src/core/agent/moss-agent.ts +++ b/packages/moss-agent/src/core/agent/moss-agent.ts @@ -126,6 +126,7 @@ import { sanitizeSecrets } from '../../safety/secret-sanitizer.js'; import { MossError, ErrorCode, errorMessage, isMossError } from '../../errors.js'; import { JsonlExecutionStore } from '../../orchestration/jsonl-execution-store.js'; import { AdaptiveWorkspaceLeaseAdapter } from '../../orchestration/adaptive-workspace-lease.js'; +import { ExecutionTaskController } from '../../orchestration/execution-task-controller.js'; import { DEFAULT_ORCHESTRATION_POLICY } from '../../orchestration/execution-projector.js'; import type { AgentRoleRegistry, @@ -191,6 +192,8 @@ export class MossAgent { readonly asyncTasks: MossAsyncTaskRegistry; /** Authoritative graph store shared by product projections. @beta */ readonly executionStore: ExecutionStore; + /** Shared task control projection for every host surface. @beta */ + readonly tasks: ExecutionTaskController; /** Effective visible scheduling policy. @beta */ readonly orchestrationPolicy: OrchestrationPolicy; /** Isolated implementation workspace seam when configured by the host. @beta */ @@ -240,6 +243,7 @@ export class MossAgent { new JsonlExecutionStore({ rootDir: path.join(getMossWorkspacePaths(workspaceDir).runtimeDir, 'executions'), }); + this.tasks = new ExecutionTaskController(this.executionStore); this.orchestrationPolicy = { ...DEFAULT_ORCHESTRATION_POLICY, ...config.orchestrationPolicy, diff --git a/packages/moss-agent/src/index.ts b/packages/moss-agent/src/index.ts index de8748d1..2d9559fa 100644 --- a/packages/moss-agent/src/index.ts +++ b/packages/moss-agent/src/index.ts @@ -197,6 +197,7 @@ export { AssignmentRouter, CompletionArbiter, ExecutionGraphScheduler, + ExecutionTaskController, GitWorktreeWorkspaceLeaseAdapter, InMemoryExecutionStore, JsonlExecutionStore, diff --git a/packages/moss-agent/src/orchestration/execution-task-controller.ts b/packages/moss-agent/src/orchestration/execution-task-controller.ts new file mode 100644 index 00000000..400ad194 --- /dev/null +++ b/packages/moss-agent/src/orchestration/execution-task-controller.ts @@ -0,0 +1,58 @@ +import { ErrorCode, MossError } from '../errors.js'; +import type { ExecutionGraphSnapshot, ExecutionStore } from './execution-types.js'; + +const TERMINAL = new Set(['completed', 'failed', 'cancelled']); + +/** Product-neutral control plane shared by CLI, Web, TUI, and ACP. @beta */ +export class ExecutionTaskController { + constructor(private readonly store: ExecutionStore) {} + + list(): readonly ExecutionGraphSnapshot[] { + return this.store.list(); + } + + inspect(graphId: string): ExecutionGraphSnapshot { + return this.required(graphId); + } + + resume(graphId: string): ExecutionGraphSnapshot { + const graph = this.required(graphId); + if (TERMINAL.has(graph.status)) this.invalid(`task "${graphId}" is ${graph.status}`); + return this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'graph.resumed', + data: { recoveryAcknowledged: graph.status === 'paused_recovered' }, + }); + } + + retry(graphId: string, nodeId: string): ExecutionGraphSnapshot { + const graph = this.required(graphId); + if (!graph.nodes[nodeId]) this.invalid(`unknown task node "${nodeId}"`); + return this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'node.retry_requested', + nodeId, + data: { requestedBy: 'user' }, + }); + } + + stop(graphId: string): ExecutionGraphSnapshot { + const graph = this.required(graphId); + if (TERMINAL.has(graph.status)) return graph; + return this.store.append(graphId, { + expectedRevision: graph.revision, + type: 'graph.cancelled', + data: { requestedBy: 'user' }, + }); + } + + private required(graphId: string): ExecutionGraphSnapshot { + const graph = this.store.load(graphId); + if (!graph) this.invalid(`unknown task "${graphId}"`); + return graph; + } + + private invalid(message: string): never { + throw new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message }); + } +} diff --git a/packages/moss-agent/src/orchestration/index.ts b/packages/moss-agent/src/orchestration/index.ts index 13cf2d99..2555d867 100644 --- a/packages/moss-agent/src/orchestration/index.ts +++ b/packages/moss-agent/src/orchestration/index.ts @@ -4,6 +4,7 @@ export type { InMemoryExecutionStoreOptions } from './in-memory-execution-store. export { JsonlExecutionStore } from './jsonl-execution-store.js'; export type { JsonlExecutionStoreOptions } from './jsonl-execution-store.js'; export { recoverExecutionGraph } from './execution-recovery.js'; +export { ExecutionTaskController } from './execution-task-controller.js'; export type { RecoverExecutionGraphOptions } from './execution-recovery.js'; export { ExecutionGraphScheduler, diff --git a/packages/moss-agent/test/execution-task-controller.spec.mjs b/packages/moss-agent/test/execution-task-controller.spec.mjs new file mode 100644 index 00000000..ce2baa2f --- /dev/null +++ b/packages/moss-agent/test/execution-task-controller.spec.mjs @@ -0,0 +1,35 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { ExecutionTaskController, InMemoryExecutionStore } from '../dist/orchestration/index.js'; + +test('task controller exposes consistent list, inspect, resume, retry, and stop actions', () => { + const store = new InMemoryExecutionStore(); + store.create({ + id: 'task-1', + goal: 'recover me', + nodes: [{ id: 'analysis', kind: 'analysis', title: 'analyse', dependencies: [] }], + }); + const tasks = new ExecutionTaskController(store); + assert.deepEqual( + tasks.list().map((task) => task.id), + ['task-1'] + ); + assert.equal(tasks.inspect('task-1').revision, 1); + assert.equal(tasks.resume('task-1').status, 'running'); + + let graph = store.load('task-1'); + for (const type of ['node.ready', 'node.started', 'node.failed']) { + graph = store.append('task-1', { + expectedRevision: graph.revision, + type, + nodeId: 'analysis', + ...(type === 'node.failed' ? { data: { error: 'transient' } } : {}), + }); + } + assert.equal(tasks.retry('task-1', 'analysis').nodes.analysis.status, 'ready'); + assert.equal(tasks.stop('task-1').status, 'cancelled'); + assert.equal(tasks.stop('task-1').revision, tasks.inspect('task-1').revision); + assert.throws(() => tasks.resume('task-1'), /is cancelled/); +}); diff --git a/packages/moss-agent/test/task-command.spec.mjs b/packages/moss-agent/test/task-command.spec.mjs new file mode 100644 index 00000000..958ecdbe --- /dev/null +++ b/packages/moss-agent/test/task-command.spec.mjs @@ -0,0 +1,17 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { handleTaskCommand } from '../dist/cli/task-command.js'; +import { ExecutionTaskController, InMemoryExecutionStore } from '../dist/orchestration/index.js'; + +test('task commands project the same graph ids, revisions, and recovery actions', () => { + const store = new InMemoryExecutionStore(); + store.create({ id: 'cli-task', goal: 'visible task', nodes: [] }); + const agent = { tasks: new ExecutionTaskController(store) }; + assert.match(handleTaskCommand(agent, '/tasks'), /cli-task\s+paused\s+rev=1/); + assert.match(handleTaskCommand(agent, '/task inspect cli-task'), /visible task/); + assert.match(handleTaskCommand(agent, '/task resume cli-task'), /running\s+rev=2/); + assert.match(handleTaskCommand(agent, '/task stop cli-task'), /cancelled\s+rev=3/); + assert.match(handleTaskCommand(agent, '/task inspect missing'), /^Error: unknown task/); +}); From 6d2aaf7035ac0983819babfdec211a741a232e34 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 18:05:38 +0800 Subject: [PATCH 08/21] feat(agent): expose execution graphs in web workbench --- .../src/web-ui/client/api-client.ts | 8 +++ .../src/web-ui/client/details-panel.tsx | 68 ++++++++++++++++++- .../src/web-ui/client/workbench-types.ts | 26 +++++++ .../src/web-ui/web-control-plane-router.ts | 20 +++++- .../src/web-ui/web-runtime-service.ts | 15 ++++ .../test/web-control-plane.spec.mjs | 12 ++++ 6 files changed, 146 insertions(+), 3 deletions(-) diff --git a/packages/moss-agent/src/web-ui/client/api-client.ts b/packages/moss-agent/src/web-ui/client/api-client.ts index 415e8e18..ec2b2f4b 100644 --- a/packages/moss-agent/src/web-ui/client/api-client.ts +++ b/packages/moss-agent/src/web-ui/client/api-client.ts @@ -1,6 +1,7 @@ import type { BootstrapResponse, GoalSnapshot, + ExecutionGraphSnapshot, Interaction, JobSnapshot, MentionInventory, @@ -151,6 +152,13 @@ export const api = { command: (id: string, command: string) => jsonRequest(`/api/sessions/${sid(id)}/commands`, json('POST', { command })), jobs: () => jsonRequest<{ jobs: JobSnapshot[] }>('/api/jobs'), + tasks: () => jsonRequest<{ tasks: ExecutionGraphSnapshot[] }>('/api/tasks'), + task: (id: string) => jsonRequest<{ task: ExecutionGraphSnapshot }>(`/api/tasks/${sid(id)}`), + controlTask: (id: string, action: 'resume' | 'retry' | 'stop', nodeId?: string) => + jsonRequest<{ task: ExecutionGraphSnapshot }>( + `/api/tasks/${sid(id)}/${action}`, + json('POST', nodeId ? { nodeId } : {}) + ), stopJob: (id: string) => jsonRequest(`/api/jobs/${sid(id)}/stop`, json('POST')), workflows: () => jsonRequest<{ workflows: WorkflowSnapshot[] }>('/api/workflows'), runWorkflow: (id: string, sessionId: string, args = '') => diff --git a/packages/moss-agent/src/web-ui/client/details-panel.tsx b/packages/moss-agent/src/web-ui/client/details-panel.tsx index 9b9663fd..0996c534 100644 --- a/packages/moss-agent/src/web-ui/client/details-panel.tsx +++ b/packages/moss-agent/src/web-ui/client/details-panel.tsx @@ -5,6 +5,7 @@ import { PluginSlot } from './plugin-slot.js'; import type { BootstrapResponse, GoalSnapshot, + ExecutionGraphSnapshot, JobSnapshot, RunSnapshot, TimelineItem, @@ -48,17 +49,25 @@ export const DetailsPanel = ({ const [goal, setGoal] = useState({}); const [todos, setTodos] = useState([]); const [jobs, setJobs] = useState([]); + const [tasks, setTasks] = useState([]); const [workflows, setWorkflows] = useState([]); const [trajectory, setTrajectory] = useState(); const [verdict, setVerdict] = useState(); useEffect(() => { if (!sessionId) return; - void Promise.all([api.goal(sessionId), api.todos(sessionId), api.jobs(), api.workflows()]) - .then(([nextGoal, nextTodos, nextJobs, nextWorkflows]) => { + void Promise.all([ + api.goal(sessionId), + api.todos(sessionId), + api.jobs(), + api.workflows(), + api.tasks(), + ]) + .then(([nextGoal, nextTodos, nextJobs, nextWorkflows, nextTasks]) => { setGoal(nextGoal.goal ?? {}); setTodos(safeList(nextTodos, 'todos')); setJobs(safeList(nextJobs, 'jobs')); setWorkflows(safeList(nextWorkflows, 'workflows')); + setTasks(safeList(nextTasks, 'tasks')); }) .catch(() => {}); }, [sessionId]); @@ -173,6 +182,61 @@ export const DetailsPanel = ({ )} {tab === 'plan' && ( <> +
+
Execution Graph
+ {tasks.filter((task) => !task.sessionId || task.sessionId === sessionId) + .length ? ( + tasks + .filter((task) => !task.sessionId || task.sessionId === sessionId) + .map((task) => ( +
+ {task.goal} + + {task.status} · revision {task.revision} ·{' '} + { + Object.values(task.nodes).filter( + (node) => node.status === 'succeeded' + ).length + } + /{Object.keys(task.nodes).length} nodes + + {task.status === 'paused' || task.status === 'paused_recovered' ? ( + + ) : null} + {['ready', 'running', 'blocked'].includes(task.status) ? ( + + ) : null} + {JSON.stringify(task, null, 2)} +
+ )) + ) : ( +

No durable execution graph for this session.

+ )} +
Queue / Steering

diff --git a/packages/moss-agent/src/web-ui/client/workbench-types.ts b/packages/moss-agent/src/web-ui/client/workbench-types.ts index 5b125003..d149e57b 100644 --- a/packages/moss-agent/src/web-ui/client/workbench-types.ts +++ b/packages/moss-agent/src/web-ui/client/workbench-types.ts @@ -159,6 +159,32 @@ export interface JobSnapshot { status?: string; [key: string]: unknown; } +export interface ExecutionGraphSnapshot { + id: string; + sessionId?: string; + goal: string; + status: string; + revision: number; + budget: Record; + nodes: Record< + string, + { + id: string; + title: string; + kind: string; + status: string; + roleId?: string; + dependencies: string[]; + attempts: number; + workspaceLeaseId?: string; + evidenceIds: string[]; + error?: string; + } + >; + evidence: Array<{ id: string; kind: string; summary: string; artifactRef?: string }>; + verification?: { verdict: string; evidenceIds: string[]; reasons: string[] }; + recovery?: { requiresUserResume: boolean; interruptedNodeIds: string[] }; +} export interface WorkflowSnapshot { id: string; title?: string; diff --git a/packages/moss-agent/src/web-ui/web-control-plane-router.ts b/packages/moss-agent/src/web-ui/web-control-plane-router.ts index c8e1d7a8..3f3988b1 100644 --- a/packages/moss-agent/src/web-ui/web-control-plane-router.ts +++ b/packages/moss-agent/src/web-ui/web-control-plane-router.ts @@ -41,6 +41,13 @@ export async function handleMossWebControlPlaneRequest( if (request.method === 'GET' && url.pathname === '/api/jobs') { return send(200, { jobs: runtime.jobs() }); } + if (request.method === 'GET' && url.pathname === '/api/tasks') { + return send(200, { tasks: runtime.tasks() }); + } + const taskMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)$/); + if (request.method === 'GET' && taskMatch) { + return send(200, { task: runtime.task(decodeURIComponent(taskMatch[1])) }); + } if (request.method === 'GET' && url.pathname === '/api/workflows') { return send(200, { workflows: runtime.workflows() }); } @@ -79,6 +86,7 @@ export async function handleMossWebControlPlaneRequest( const interactionMatch = url.pathname.match(/^\/api\/interactions\/([^/]+)\/(resolve|cancel)$/); const jobStopMatch = url.pathname.match(/^\/api\/jobs\/([^/]+)\/stop$/); const workflowRunMatch = url.pathname.match(/^\/api\/workflows\/([^/]+)\/run$/); + const taskActionMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(resume|retry|stop)$/); const settingsValidationMatch = url.pathname.match(/^\/api\/settings\/([^/]+)\/validate$/); const credentialMatch = url.pathname.match(/^\/api\/settings\/credentials\/([^/]+)$/); const steerMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/steer$/); @@ -92,7 +100,8 @@ export async function handleMossWebControlPlaneRequest( settingsValidationMatch || inboxMatch || steerMatch || - commandMatch + commandMatch || + taskActionMatch )) || (request.method === 'PUT' && Boolean( @@ -143,6 +152,15 @@ export async function handleMossWebControlPlaneRequest( ? send(200, { taskId, stopped: true }) : send(404, { error: 'job not found' }); } + if (request.method === 'POST' && taskActionMatch) { + const body = await readJson(request); + const task = runtime.controlTask( + decodeURIComponent(taskActionMatch[1]), + taskActionMatch[2] as 'resume' | 'retry' | 'stop', + typeof body.nodeId === 'string' ? body.nodeId : undefined + ); + return send(200, { task }); + } if (request.method === 'POST' && workflowRunMatch) { const body = await readJson(request); const sessionId = typeof body.sessionId === 'string' ? body.sessionId : ''; diff --git a/packages/moss-agent/src/web-ui/web-runtime-service.ts b/packages/moss-agent/src/web-ui/web-runtime-service.ts index d5829614..babd0108 100644 --- a/packages/moss-agent/src/web-ui/web-runtime-service.ts +++ b/packages/moss-agent/src/web-ui/web-runtime-service.ts @@ -94,6 +94,21 @@ export class MossWebRuntimeService { return this.agent.asyncTasks.list(); } + tasks() { + return this.agent.tasks.list(); + } + + task(graphId: string) { + return this.agent.tasks.inspect(graphId); + } + + controlTask(graphId: string, action: 'resume' | 'retry' | 'stop', nodeId?: string) { + if (action === 'resume') return this.agent.tasks.resume(graphId); + if (action === 'stop') return this.agent.tasks.stop(graphId); + if (!nodeId) this.invalid('nodeId is required for retry'); + return this.agent.tasks.retry(graphId, nodeId); + } + stopJob(taskId: string): boolean { return this.agent.asyncTasks.stop(taskId, 'user_cancelled'); } diff --git a/packages/moss-agent/test/web-control-plane.spec.mjs b/packages/moss-agent/test/web-control-plane.spec.mjs index 0065de6b..b845cbbc 100644 --- a/packages/moss-agent/test/web-control-plane.spec.mjs +++ b/packages/moss-agent/test/web-control-plane.spec.mjs @@ -33,6 +33,7 @@ function createAgent(workspaceDir) { test('Web control plane resolves interactions and drives runtime and settings owners', async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'moss-web-control-plane-')); const agent = createAgent(tempDir); + agent.executionStore.create({ id: 'web-task', goal: 'Visible in every surface', nodes: [] }); const broker = new MossWebInteractionBroker(); const web = await startMossWebServer(agent, { port: 0, @@ -81,6 +82,17 @@ test('Web control plane resolves interactions and drives runtime and settings ow }).then((response) => response.json()); assert.equal(mode.mode, 'plan'); + const tasks = await fetch(`${web.url}/api/tasks`).then((response) => response.json()); + assert.equal(tasks.tasks[0].id, 'web-task'); + assert.equal(tasks.tasks[0].revision, 1); + const resumedTask = await fetch(`${web.url}/api/tasks/web-task/resume`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: '{}', + }).then((response) => response.json()); + assert.equal(resumedTask.task.status, 'running'); + assert.equal(resumedTask.task.revision, 2); + const goal = await fetch(`${web.url}/api/sessions/${session.sessionId}/goal`, { method: 'PUT', headers: { 'content-type': 'application/json' }, From d9da6dd526b0533166240671b6c1823dc23879c0 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 18:12:25 +0800 Subject: [PATCH 09/21] feat(agent): project plans and task runs into execution graphs --- .../moss-agent/src/core/agent/moss-agent.ts | 4 + .../src/core/task-run/task-run-ledger.ts | 73 ++++++++++- .../plan-execute/plan-execute-controller.ts | 68 ++++++---- .../src/plan-execute/plan-execution-graph.ts | 122 ++++++++++++++++++ .../moss-agent/src/plan-execute/plan-tools.ts | 31 ++++- .../moss-agent/src/runtime/shared-runtime.ts | 15 ++- packages/moss-agent/src/tools/builtin.ts | 5 +- .../src/web-ui/web-runtime-service.ts | 13 ++ packages/moss-agent/src/web-ui/web-server.ts | 3 +- .../test/plan-dag-scheduling.spec.mjs | 46 +++++++ .../test/plan-execution-graph.spec.mjs | 47 +++++++ .../task-run-execution-projection.spec.mjs | 26 ++++ 12 files changed, 419 insertions(+), 34 deletions(-) create mode 100644 packages/moss-agent/src/plan-execute/plan-execution-graph.ts create mode 100644 packages/moss-agent/test/plan-dag-scheduling.spec.mjs create mode 100644 packages/moss-agent/test/plan-execution-graph.spec.mjs create mode 100644 packages/moss-agent/test/task-run-execution-projection.spec.mjs diff --git a/packages/moss-agent/src/core/agent/moss-agent.ts b/packages/moss-agent/src/core/agent/moss-agent.ts index 6ed5e3ee..dc1c3353 100644 --- a/packages/moss-agent/src/core/agent/moss-agent.ts +++ b/packages/moss-agent/src/core/agent/moss-agent.ts @@ -127,6 +127,7 @@ import { MossError, ErrorCode, errorMessage, isMossError } from '../../errors.js import { JsonlExecutionStore } from '../../orchestration/jsonl-execution-store.js'; import { AdaptiveWorkspaceLeaseAdapter } from '../../orchestration/adaptive-workspace-lease.js'; import { ExecutionTaskController } from '../../orchestration/execution-task-controller.js'; +import { CompletionArbiter } from '../../orchestration/completion-arbiter.js'; import { DEFAULT_ORCHESTRATION_POLICY } from '../../orchestration/execution-projector.js'; import type { AgentRoleRegistry, @@ -194,6 +195,8 @@ export class MossAgent { readonly executionStore: ExecutionStore; /** Shared task control projection for every host surface. @beta */ readonly tasks: ExecutionTaskController; + /** Deterministic-first completion authority for this agent's graphs. @beta */ + readonly completionArbiter: CompletionArbiter; /** Effective visible scheduling policy. @beta */ readonly orchestrationPolicy: OrchestrationPolicy; /** Isolated implementation workspace seam when configured by the host. @beta */ @@ -244,6 +247,7 @@ export class MossAgent { rootDir: path.join(getMossWorkspacePaths(workspaceDir).runtimeDir, 'executions'), }); this.tasks = new ExecutionTaskController(this.executionStore); + this.completionArbiter = new CompletionArbiter(this.executionStore); this.orchestrationPolicy = { ...DEFAULT_ORCHESTRATION_POLICY, ...config.orchestrationPolicy, diff --git a/packages/moss-agent/src/core/task-run/task-run-ledger.ts b/packages/moss-agent/src/core/task-run/task-run-ledger.ts index 55d74525..16b4f44a 100644 --- a/packages/moss-agent/src/core/task-run/task-run-ledger.ts +++ b/packages/moss-agent/src/core/task-run/task-run-ledger.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { randomUUID } from 'node:crypto'; import { ErrorCode, MossError } from '../../errors.js'; +import type { ExecutionStore } from '../../orchestration/index.js'; import type { AppendTaskRunEventInput, CreateTaskRunInput, @@ -46,7 +47,10 @@ export class TaskRunLedger { private readonly runs = new Map(); private readonly eventIds = new Map(); - constructor(private readonly filePath?: string) { + constructor( + private readonly filePath?: string, + private readonly executionStore?: ExecutionStore + ) { if (filePath) this.load(filePath); } @@ -64,6 +68,15 @@ export class TaskRunLedger { this.runs.set(input.id, [event]); this.eventIds.set(event.id, input.id); this.persist(event); + if (this.executionStore && !this.executionStore.load(input.id)) { + this.executionStore.create({ + id: input.id, + sessionId: input.sessionId, + goal: input.title?.trim() || 'New task', + nodes: [], + now: event.time, + }); + } return project([event]); } @@ -107,6 +120,7 @@ export class TaskRunLedger { events.push(event); this.eventIds.set(id, runId); this.persist(event); + this.mirrorExecutionEvent(event); return project(events); } @@ -145,6 +159,63 @@ export class TaskRunLedger { fs.appendFileSync(this.filePath, `${JSON.stringify(event)}\n`, { mode: 0o600 }); } + private mirrorExecutionEvent(event: TaskRunEvent): void { + const store = this.executionStore; + let graph = store?.load(event.runId); + if (!store || !graph) return; + const append = ( + type: Parameters[1]['type'], + data: Readonly> = {} + ): void => { + graph = store.append(event.runId, { + id: `task-run-${event.id}-${type}`, + expectedRevision: graph!.revision, + type, + time: event.time, + data, + }); + }; + if (event.type === 'run.started') append('graph.resumed'); + else if (event.type === 'run.failed') append('graph.failed', { source: 'task-run-v1' }); + else if (event.type === 'run.cancelled') append('graph.cancelled', { source: 'task-run-v1' }); + else if (event.type === 'run.interrupted') { + append('graph.recovered', { + recovery: { + recoveredAt: event.time, + requiresUserResume: true, + interruptedNodeIds: [], + blockedMutationNodeIds: [], + }, + }); + } else if (event.type === 'tool.succeeded' || event.type === 'tool.failed') { + const toolName = typeof event.data.toolName === 'string' ? event.data.toolName : 'tool'; + append('evidence.recorded', { + evidence: { + id: `task-run-evidence-${event.id}`, + kind: 'tool_result', + summary: `${toolName} ${event.type === 'tool.succeeded' ? 'succeeded' : 'failed'}`, + createdAt: event.time, + metadata: { success: event.type === 'tool.succeeded', source: 'task-run-v1' }, + }, + }); + } else if (event.type === 'run.verified' || event.type === 'run.rejected') { + const verdict = event.type === 'run.verified' ? 'verified' : 'rejected'; + append('verification.recorded', { + verification: { + verdict, + evidenceIds: graph.evidence.map((evidence) => evidence.id), + reasons: verdict === 'verified' ? [] : ['legacy verifier rejected the run'], + verifiedAt: event.time, + }, + }); + append(verdict === 'verified' ? 'graph.completed' : 'graph.failed', { + source: 'task-run-v1-verdict', + }); + } + // run.completed intentionally does not complete the graph: a completion + // claim remains pending until a verifier or CompletionArbiter binds evidence. + } + private load(filePath: string): void { let body = ''; try { diff --git a/packages/moss-agent/src/plan-execute/plan-execute-controller.ts b/packages/moss-agent/src/plan-execute/plan-execute-controller.ts index 1851f919..d0c1d3ee 100644 --- a/packages/moss-agent/src/plan-execute/plan-execute-controller.ts +++ b/packages/moss-agent/src/plan-execute/plan-execute-controller.ts @@ -19,6 +19,9 @@ export interface PlanStep { expectedOutput?: string; + /** Parent-relative write lease declared for an implementation node. */ + writePaths?: string[]; + /** * 该步骤引用的验收契约 skill 名(D10 解 A:有 plan 时按 step 查契约)。 * 如 ['rdk-device'] → hook 查 contractRegistry.findBySkill('rdk-device') @@ -129,6 +132,7 @@ export class PlanExecuteController { steps: steps.map((s, i) => ({ ...s, step: i + 1, + dependsOn: s.dependsOn ?? (i === 0 ? [] : [i]), status: 'pending' as StepStatus, })), rationale, @@ -221,13 +225,8 @@ export class PlanExecuteController { if (!plan || plan.status !== 'approved') return false; plan.status = 'executing'; - plan.currentStep = 1; plan.updatedAt = new Date().toISOString(); - - if (plan.steps.length > 0) { - plan.steps[0].status = 'in_progress'; - plan.steps[0].startedAt = new Date().toISOString(); - } + this.advanceReadySteps(plan); this.activePlanId = planId; return true; @@ -250,15 +249,7 @@ export class PlanExecuteController { step.actualTools = actualTools; step.completedAt = new Date().toISOString(); - const nextStep = plan.steps.find((s) => s.step === stepNumber + 1); - if (nextStep) { - nextStep.status = 'in_progress'; - nextStep.startedAt = new Date().toISOString(); - plan.currentStep = nextStep.step; - } else { - plan.status = 'completed'; - plan.currentStep = undefined; - } + this.advanceReadySteps(plan); plan.updatedAt = new Date().toISOString(); return true; @@ -274,6 +265,7 @@ export class PlanExecuteController { step.status = 'failed'; step.error = error; step.completedAt = new Date().toISOString(); + this.advanceReadySteps(plan); plan.updatedAt = new Date().toISOString(); return true; @@ -290,20 +282,48 @@ export class PlanExecuteController { step.actualOutput = `Skipped: ${reason}`; step.completedAt = new Date().toISOString(); - const nextStep = plan.steps.find((s) => s.step === stepNumber + 1); - if (nextStep) { - nextStep.status = 'in_progress'; - nextStep.startedAt = new Date().toISOString(); - plan.currentStep = nextStep.step; - } else { - plan.status = 'completed'; - plan.currentStep = undefined; - } + this.advanceReadySteps(plan); plan.updatedAt = new Date().toISOString(); return true; } + private advanceReadySteps(plan: Plan): void { + const terminalSuccess = new Set(['completed', 'skipped']); + const terminalFailure = new Set(['failed', 'blocked']); + let changed = true; + while (changed) { + changed = false; + for (const step of plan.steps) { + if (step.status !== 'pending') continue; + const dependencies = (step.dependsOn ?? []).map((number) => + plan.steps.find((candidate) => candidate.step === number) + ); + if ( + dependencies.some((dependency) => dependency && terminalFailure.has(dependency.status)) + ) { + step.status = 'blocked'; + step.error = 'blocked by failed dependency'; + changed = true; + continue; + } + if ( + dependencies.every((dependency) => dependency && terminalSuccess.has(dependency.status)) + ) { + step.status = 'in_progress'; + step.startedAt = new Date().toISOString(); + changed = true; + } + } + } + const active = plan.steps.filter((step) => step.status === 'in_progress'); + plan.currentStep = active.length ? Math.min(...active.map((step) => step.step)) : undefined; + if (active.length > 0 || plan.steps.some((step) => step.status === 'pending')) return; + plan.status = plan.steps.some((step) => terminalFailure.has(step.status)) + ? 'failed' + : 'completed'; + } + requestReplan( planId: string, _reason: string, diff --git a/packages/moss-agent/src/plan-execute/plan-execution-graph.ts b/packages/moss-agent/src/plan-execute/plan-execution-graph.ts new file mode 100644 index 00000000..668da15f --- /dev/null +++ b/packages/moss-agent/src/plan-execute/plan-execution-graph.ts @@ -0,0 +1,122 @@ +import type { ExecutionNodeKind, ExecutionStore } from '../orchestration/index.js'; +import type { Plan, PlanStep } from './plan-execute-controller.js'; + +function nodeId(step: number): string { + return `step-${step}`; +} + +function kind(step: PlanStep): ExecutionNodeKind { + if (step.expectedTools?.some((tool) => /verify|test|diagnostic/i.test(tool))) { + return 'verification'; + } + if ( + step.writePaths?.length || + step.expectedTools?.some((tool) => /write|edit|patch|move|exec/i.test(tool)) + ) { + return 'implementation'; + } + return 'analysis'; +} + +/** Create the authoritative graph projection for a compatibility Plan. @internal */ +export function createExecutionGraphForPlan( + store: ExecutionStore, + plan: Plan, + sessionId?: string +): void { + store.create({ + id: plan.id, + goal: plan.goal, + ...(sessionId ? { sessionId } : {}), + nodes: plan.steps.map((step) => ({ + id: nodeId(step.step), + kind: kind(step), + title: step.description, + dependencies: (step.dependsOn ?? []).map(nodeId), + ...(step.writePaths?.length ? { writePaths: step.writePaths } : {}), + ...(step.expectedOutput ? { acceptanceCriteria: [step.expectedOutput] } : {}), + })), + }); +} + +/** Mirror legacy Plan mutations into graph events while compatibility is supported. @internal */ +export function syncExecutionGraphFromPlan(store: ExecutionStore, plan: Plan): void { + let graph = store.load(plan.id); + if (!graph) return; + if (plan.status === 'executing' && graph.status !== 'running') { + graph = store.append(plan.id, { + expectedRevision: graph.revision, + type: 'graph.resumed', + }); + } + for (const step of plan.steps) { + const id = nodeId(step.step); + let node = graph.nodes[id]; + if ( + step.status === 'in_progress' && + ['pending', 'interrupted', 'failed', 'blocked'].includes(node.status) + ) { + graph = store.append(plan.id, { + expectedRevision: graph.revision, + type: 'node.ready', + nodeId: id, + }); + graph = store.append(plan.id, { + expectedRevision: graph.revision, + type: 'node.started', + nodeId: id, + }); + node = graph.nodes[id]; + } + if (step.status === 'completed' && node.status === 'running') { + if (step.actualOutput) { + graph = store.append(plan.id, { + expectedRevision: graph.revision, + type: 'evidence.recorded', + nodeId: id, + data: { + evidence: { + id: `plan-output-${plan.id}-${step.step}-${graph.revision + 1}`, + kind: 'expert_claim', + nodeId: id, + summary: step.actualOutput, + createdAt: Date.now(), + }, + }, + }); + } + graph = store.append(plan.id, { + expectedRevision: graph.revision, + type: 'node.succeeded', + nodeId: id, + }); + } else if (step.status === 'failed' && node.status === 'running') { + graph = store.append(plan.id, { + expectedRevision: graph.revision, + type: 'node.failed', + nodeId: id, + data: { error: step.error ?? 'plan step failed' }, + }); + } else if (step.status === 'skipped' && ['pending', 'ready', 'blocked'].includes(node.status)) { + graph = store.append(plan.id, { + expectedRevision: graph.revision, + type: 'node.skipped', + nodeId: id, + }); + } else if (step.status === 'blocked' && !['blocked', 'cancelled'].includes(node.status)) { + graph = store.append(plan.id, { + expectedRevision: graph.revision, + type: 'node.blocked', + nodeId: id, + data: { blockedByDependencies: step.dependsOn?.map(nodeId) ?? [] }, + }); + } + } + if (plan.status === 'completed' && graph.status !== 'completed') { + store.append(plan.id, { expectedRevision: graph.revision, type: 'graph.completed' }); + } else if (plan.status === 'failed' && graph.status !== 'failed') { + store.append(plan.id, { expectedRevision: graph.revision, type: 'graph.failed' }); + } else if (plan.status === 'cancelled' && graph.status !== 'cancelled') { + store.append(plan.id, { expectedRevision: graph.revision, type: 'graph.cancelled' }); + } +} diff --git a/packages/moss-agent/src/plan-execute/plan-tools.ts b/packages/moss-agent/src/plan-execute/plan-tools.ts index 90318967..512ed36f 100644 --- a/packages/moss-agent/src/plan-execute/plan-tools.ts +++ b/packages/moss-agent/src/plan-execute/plan-tools.ts @@ -6,6 +6,8 @@ import { validateAcceptSpecs } from '../acceptance/accept-spec-validator.js'; import { ContractRegistry } from '../acceptance/contract-registry.js'; import { SkillRegistry } from '../skills/registry.js'; import type { PlanControllerStore } from './plan-controller-store.js'; +import type { ExecutionStore } from '../orchestration/index.js'; +import { createExecutionGraphForPlan, syncExecutionGraphFromPlan } from './plan-execution-graph.js'; import { getPlanController, getSharedPlanController, @@ -40,6 +42,7 @@ export interface PlanToolInput { expectedAccept?: string[]; dependsOn?: number[]; estimatedTimeSec?: number; + writePaths?: string[]; }>; rationale?: string; @@ -219,7 +222,10 @@ export const activePlanProvider: ActivePlanProvider = { get: getActivePlanForSession, }; -export function createPlanTool(store?: PlanControllerStore): Tool { +export function createPlanTool( + store?: PlanControllerStore, + executionStore?: ExecutionStore +): Tool { return { name: 'plan', description: @@ -277,6 +283,11 @@ export function createPlanTool(store?: PlanControllerStore): Tool description: 'Step numbers this step depends on.', }, estimatedTimeSec: { type: 'number', description: 'Estimated time in seconds.' }, + writePaths: { + type: 'array', + items: { type: 'string' }, + description: 'Parent-relative paths this implementation step may modify.', + }, }, required: ['description'], }, @@ -357,6 +368,7 @@ export function createPlanTool(store?: PlanControllerStore): Tool expectedAccept: s.expectedAccept, dependsOn: s.dependsOn, estimatedTimeSec: s.estimatedTimeSec, + writePaths: s.writePaths, })); const plan = controller.createPlan(input.goal, planSteps, input.rationale); @@ -364,6 +376,7 @@ export function createPlanTool(store?: PlanControllerStore): Tool if (input.preconditions) plan.preconditions = input.preconditions; if (input.successCriteria) plan.successCriteria = input.successCriteria; if (input.terminalAccept) plan.terminalAccept = input.terminalAccept; + if (executionStore) createExecutionGraphForPlan(executionStore, plan, ctx.sessionKey); const warnings = machineAcceptanceIssues(plan); @@ -485,6 +498,9 @@ export function createPlanTool(store?: PlanControllerStore): Tool if (store) store.setActivePlanId(ctx.sessionKey, input.planId); else setActivePlanId(ctx.sessionKey, input.planId); } + const startedPlan = controller.getPlan(input.planId); + if (executionStore && startedPlan) + syncExecutionGraphFromPlan(executionStore, startedPlan); const leftPlanMode = leavePlanModeForExecution(); const plan = controller.getPlan(input.planId); @@ -499,6 +515,9 @@ export function createPlanTool(store?: PlanControllerStore): Tool case 'cancel': { if (!input.planId) return 'Error: planId is required to cancel.'; const ok = controller.cancelPlan(input.planId); + const cancelledPlan = controller.getPlan(input.planId); + if (ok && executionStore && cancelledPlan) + syncExecutionGraphFromPlan(executionStore, cancelledPlan); return ok ? `Plan ${input.planId} cancelled.` : `Error: could not cancel plan ${input.planId}.`; @@ -559,7 +578,10 @@ export function createPlanTool(store?: PlanControllerStore): Tool }; } -export function createPlanStepTool(store?: PlanControllerStore): Tool { +export function createPlanStepTool( + store?: PlanControllerStore, + executionStore?: ExecutionStore +): Tool { return { name: 'plan_step', description: @@ -613,6 +635,7 @@ export function createPlanStepTool(store?: PlanControllerStore): Tool DESKTOP_SAFE_TOOLS.has(tool.name)); return selected.map((tool) => { if (tool.name === 'load_skill') return createLoadSkillTool(skillRegistry); - if (tool.name === 'plan') return createPlanTool(planControllerStore); - if (tool.name === 'plan_step') return createPlanStepTool(planControllerStore); + if (tool.name === 'plan') return createPlanTool(planControllerStore, agentExecutionStore); + if (tool.name === 'plan_step') + return createPlanStepTool(planControllerStore, agentExecutionStore); return tool; }); } @@ -244,7 +246,12 @@ export async function createMossRuntime(options: CreateMossRuntimeOptions): Prom ...(completionGate ? { completionGate } : {}), }); - for (const tool of selectTools(toolProfile, services.skillRegistry, agent.planControllerStore)) + for (const tool of selectTools( + toolProfile, + services.skillRegistry, + agent.planControllerStore, + agent.executionStore + )) agent.tools.register(tool); for (const tool of options.extraTools ?? []) agent.tools.replace(tool, 'host:extra-tools'); try { diff --git a/packages/moss-agent/src/tools/builtin.ts b/packages/moss-agent/src/tools/builtin.ts index d6f71aa0..68d28bc7 100644 --- a/packages/moss-agent/src/tools/builtin.ts +++ b/packages/moss-agent/src/tools/builtin.ts @@ -515,15 +515,16 @@ export const builtinTools: Tool[] = [ export function registerBuiltinTools(agent: { tools: { register: (tool: Tool) => void }; planControllerStore?: PlanControllerStore; + executionStore?: import('../orchestration/index.js').ExecutionStore; }): void { const skillRegistry = (agent as { config?: { skillRegistry?: SkillRegistry } }).config ?.skillRegistry; for (const tool of builtinTools) { const instanceTool = tool.name === 'plan' && agent.planControllerStore - ? createPlanTool(agent.planControllerStore) + ? createPlanTool(agent.planControllerStore, agent.executionStore) : tool.name === 'plan_step' && agent.planControllerStore - ? createPlanStepTool(agent.planControllerStore) + ? createPlanStepTool(agent.planControllerStore, agent.executionStore) : tool; agent.tools.register( instanceTool.name === 'load_skill' && skillRegistry diff --git a/packages/moss-agent/src/web-ui/web-runtime-service.ts b/packages/moss-agent/src/web-ui/web-runtime-service.ts index babd0108..2b87338e 100644 --- a/packages/moss-agent/src/web-ui/web-runtime-service.ts +++ b/packages/moss-agent/src/web-ui/web-runtime-service.ts @@ -142,6 +142,19 @@ export class MossWebRuntimeService { } completionVerdict(runId: string) { + const graph = this.agent.executionStore.load(runId); + if (graph) { + return { + runId, + status: graph.status, + verdict: graph.verification?.verdict ?? 'pending', + evidenceIds: graph.verification?.evidenceIds ?? [], + reasons: graph.verification?.reasons ?? [], + evidenceCount: graph.evidence.length, + complete: graph.status === 'completed' && graph.verification?.verdict === 'verified', + revision: graph.revision, + }; + } const run = this.taskRuns.get(runId); if (!run) this.invalid(`run "${runId}" was not found`); return { diff --git a/packages/moss-agent/src/web-ui/web-server.ts b/packages/moss-agent/src/web-ui/web-server.ts index b36acc35..9f114476 100644 --- a/packages/moss-agent/src/web-ui/web-server.ts +++ b/packages/moss-agent/src/web-ui/web-server.ts @@ -285,7 +285,8 @@ export async function startMossWebServer( const taskRuns = options.taskRunLedger ?? new TaskRunLedger( - options.taskRunFile ?? (runtimeDir ? path.join(runtimeDir, 'task-runs.jsonl') : undefined) + options.taskRunFile ?? (runtimeDir ? path.join(runtimeDir, 'task-runs.jsonl') : undefined), + agent.executionStore ); const eventJournal = new MossWebEventJournal( options.eventJournalFile ?? (runtimeDir ? path.join(runtimeDir, 'web-events.jsonl') : undefined) diff --git a/packages/moss-agent/test/plan-dag-scheduling.spec.mjs b/packages/moss-agent/test/plan-dag-scheduling.spec.mjs new file mode 100644 index 00000000..8816dbda --- /dev/null +++ b/packages/moss-agent/test/plan-dag-scheduling.spec.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { PlanExecuteController } from '../dist/plan-execute/index.js'; + +test('plan controller activates dependency-ready siblings and preserves them across failure', () => { + const controller = new PlanExecuteController({ requireApproval: false }); + const plan = controller.createPlan('DAG', [ + { step: 1, description: 'analyse A', dependsOn: [] }, + { step: 2, description: 'analyse B', dependsOn: [] }, + { step: 3, description: 'merge', dependsOn: [1, 2] }, + { step: 4, description: 'independent verification', dependsOn: [2] }, + ]); + assert.equal(controller.approvePlan(plan.id), true); + assert.equal(controller.startExecution(plan.id), true); + assert.deepEqual( + controller + .getPlan(plan.id) + .steps.filter((step) => step.status === 'in_progress') + .map((step) => step.step), + [1, 2] + ); + + assert.equal(controller.completeStep(plan.id, 2, 'B done'), true); + assert.equal(controller.getPlan(plan.id).steps[3].status, 'in_progress'); + assert.equal(controller.failStep(plan.id, 1, 'A failed'), true); + assert.equal(controller.getPlan(plan.id).steps[2].status, 'blocked'); + assert.equal( + controller.getPlan(plan.id).steps[3].status, + 'in_progress', + 'successful sibling remains runnable' + ); +}); + +test('omitted dependencies retain ordered-plan compatibility', () => { + const controller = new PlanExecuteController(); + const plan = controller.createPlan('ordered', [ + { step: 1, description: 'first' }, + { step: 2, description: 'second' }, + ]); + assert.deepEqual( + plan.steps.map((step) => step.dependsOn), + [[], [1]] + ); +}); diff --git a/packages/moss-agent/test/plan-execution-graph.spec.mjs b/packages/moss-agent/test/plan-execution-graph.spec.mjs new file mode 100644 index 00000000..431f72ca --- /dev/null +++ b/packages/moss-agent/test/plan-execution-graph.spec.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { InMemoryExecutionStore } from '../dist/orchestration/index.js'; +import { + createPlanStepTool, + createPlanTool, + PlanControllerStore, +} from '../dist/plan-execute/index.js'; + +test('legacy Plan tools shadow every mutation into the authoritative dependency graph', async () => { + const plans = new PlanControllerStore(); + const executions = new InMemoryExecutionStore(); + const planTool = createPlanTool(plans, executions); + const stepTool = createPlanStepTool(plans, executions); + const ctx = { sessionKey: 'session', workspaceDir: process.cwd() }; + const created = await planTool.execute( + { + action: 'create', + goal: 'parallel plan', + steps: [ + { description: 'A', dependsOn: [] }, + { description: 'B', dependsOn: [] }, + { description: 'merge', dependsOn: [1, 2], writePaths: ['src'] }, + ], + }, + ctx + ); + const planId = /Plan created: (\S+)/.exec(created)?.[1]; + assert.ok(planId); + assert.deepEqual(executions.load(planId).nodes['step-3'].dependencies, ['step-1', 'step-2']); + assert.deepEqual(executions.load(planId).nodes['step-3'].writePaths, ['src']); + + assert.match(await planTool.execute({ action: 'approve', planId }, ctx), /approved/); + assert.match(await planTool.execute({ action: 'start', planId }, ctx), /started/); + assert.equal(executions.load(planId).status, 'running'); + assert.equal(executions.load(planId).nodes['step-1'].status, 'running'); + assert.equal(executions.load(planId).nodes['step-2'].status, 'running'); + + await stepTool.execute( + { action: 'complete', planId, stepNumber: 1, actualOutput: 'A evidence' }, + ctx + ); + assert.equal(executions.load(planId).nodes['step-1'].status, 'succeeded'); + assert.match(executions.load(planId).evidence[0].summary, /A evidence/); +}); diff --git a/packages/moss-agent/test/task-run-execution-projection.spec.mjs b/packages/moss-agent/test/task-run-execution-projection.spec.mjs new file mode 100644 index 00000000..f6b29372 --- /dev/null +++ b/packages/moss-agent/test/task-run-execution-projection.spec.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { TaskRunLedger } from '../dist/core/task-run/index.js'; +import { InMemoryExecutionStore } from '../dist/orchestration/index.js'; + +test('TaskRun v1 shadow-writes evidence but cannot false-complete an execution graph', () => { + const store = new InMemoryExecutionStore(); + const ledger = new TaskRunLedger(undefined, store); + ledger.create({ id: 'legacy', sessionId: 'session', title: 'Legacy run', time: 1 }); + ledger.append('legacy', { type: 'run.started', time: 2 }); + ledger.append('legacy', { + type: 'tool.succeeded', + time: 3, + data: { toolName: 'exec', secretOutput: 'must not copy' }, + }); + ledger.append('legacy', { type: 'run.completed', time: 4 }); + assert.equal(store.load('legacy').status, 'running'); + assert.equal(store.load('legacy').evidence[0].summary, 'exec succeeded'); + assert.equal(JSON.stringify(store.load('legacy')).includes('must not copy'), false); + + ledger.append('legacy', { type: 'run.verified', time: 5 }); + assert.equal(store.load('legacy').status, 'completed'); + assert.equal(store.load('legacy').verification.verdict, 'verified'); +}); From 6fe62bf1048ff0d72f30b461f05e72c2cc58d791 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 18:14:37 +0800 Subject: [PATCH 10/21] feat(agent): persist background task lifecycle --- .../moss-agent/src/core/agent/moss-agent.ts | 6 +- .../execution-async-task-registry.ts | 167 ++++++++++++++++++ .../execution-async-task-registry.spec.mjs | 44 +++++ 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 packages/moss-agent/src/orchestration/execution-async-task-registry.ts create mode 100644 packages/moss-agent/test/execution-async-task-registry.spec.mjs diff --git a/packages/moss-agent/src/core/agent/moss-agent.ts b/packages/moss-agent/src/core/agent/moss-agent.ts index dc1c3353..59303d4f 100644 --- a/packages/moss-agent/src/core/agent/moss-agent.ts +++ b/packages/moss-agent/src/core/agent/moss-agent.ts @@ -128,6 +128,7 @@ import { JsonlExecutionStore } from '../../orchestration/jsonl-execution-store.j import { AdaptiveWorkspaceLeaseAdapter } from '../../orchestration/adaptive-workspace-lease.js'; import { ExecutionTaskController } from '../../orchestration/execution-task-controller.js'; import { CompletionArbiter } from '../../orchestration/completion-arbiter.js'; +import { ExecutionBackedAsyncTaskRegistry } from '../../orchestration/execution-async-task-registry.js'; import { DEFAULT_ORCHESTRATION_POLICY } from '../../orchestration/execution-projector.js'; import type { AgentRoleRegistry, @@ -239,13 +240,16 @@ export class MossAgent { this.spawnRegistry = createSpawnProfileRegistryFromDefaults(); this.expertRegistry = config.subagentExpertRegistry ?? new SubagentExpertRegistry(config.subagentExperts); - this.asyncTasks = config.asyncTaskRegistry ?? createInMemoryMossAsyncTaskRegistry(); const workspaceDir = path.resolve(config.workspaceDir ?? process.cwd()); this.executionStore = config.executionStore ?? new JsonlExecutionStore({ rootDir: path.join(getMossWorkspacePaths(workspaceDir).runtimeDir, 'executions'), }); + this.asyncTasks = new ExecutionBackedAsyncTaskRegistry( + config.asyncTaskRegistry ?? createInMemoryMossAsyncTaskRegistry(), + this.executionStore + ); this.tasks = new ExecutionTaskController(this.executionStore); this.completionArbiter = new CompletionArbiter(this.executionStore); this.orchestrationPolicy = { diff --git a/packages/moss-agent/src/orchestration/execution-async-task-registry.ts b/packages/moss-agent/src/orchestration/execution-async-task-registry.ts new file mode 100644 index 00000000..a878d1bc --- /dev/null +++ b/packages/moss-agent/src/orchestration/execution-async-task-registry.ts @@ -0,0 +1,167 @@ +import type { + MossAsyncTaskCompletion, + MossAsyncTaskHandle, + MossAsyncTaskRegistry, + MossAsyncTaskResult, + MossAsyncTaskRunner, + MossAsyncTaskSnapshot, + MossAsyncTaskStartRequest, + MossAsyncTaskStatus, + MossAsyncTaskStopReason, + MossAsyncTaskUpdate, +} from '@rdk-moss/core/contracts/async-task'; +import type { ExecutionGraphSnapshot, ExecutionStore } from './execution-types.js'; + +const NODE_ID = 'background-task'; + +function graphId(taskId: string): string { + return `async_${Buffer.from(taskId, 'utf8').toString('base64url')}`; +} + +function append( + store: ExecutionStore, + graph: ExecutionGraphSnapshot, + type: Parameters[1]['type'], + data: Readonly> = {} +): ExecutionGraphSnapshot { + return store.append(graph.id, { + expectedRevision: graph.revision, + type, + nodeId: type.startsWith('node.') ? NODE_ID : undefined, + data, + }); +} + +/** Async-task compatibility adapter backed by durable graph facts. @internal */ +export class ExecutionBackedAsyncTaskRegistry implements MossAsyncTaskRegistry { + constructor( + private readonly delegate: MossAsyncTaskRegistry, + private readonly store: ExecutionStore + ) {} + + start( + request: MossAsyncTaskStartRequest, + runner: MossAsyncTaskRunner, + options?: { parentSignal?: AbortSignal } + ): MossAsyncTaskHandle { + const id = graphId(request.taskId); + let graph = this.store.load(id); + if (!graph) { + graph = this.store.create({ + id, + goal: request.label?.trim() || `${request.kind} background task`, + nodes: [ + { + id: NODE_ID, + kind: request.kind === 'subagent' ? 'analysis' : 'manual', + title: request.label?.trim() || request.taskId, + dependencies: [], + }, + ], + }); + graph = append(this.store, graph, 'graph.resumed'); + graph = append(this.store, graph, 'node.ready'); + append(this.store, graph, 'node.started'); + } + const wrapped: MossAsyncTaskRunner = async (input, signal) => { + try { + const result = await runner(input, signal); + this.finish(request.taskId, result); + return result; + } catch (error) { + this.fail(request.taskId, error); + throw error; + } + }; + return this.delegate.start(request, wrapped, options); + } + + update( + taskId: string, + patch: MossAsyncTaskUpdate + ): MossAsyncTaskSnapshot | undefined { + const snapshot = this.delegate.update(taskId, patch); + const graph = this.store.load(graphId(taskId)); + if (graph && graph.nodes[NODE_ID]?.status === 'running') { + append(this.store, graph, 'node.progressed', { + phase: patch.progress?.phase ?? null, + currentTurn: patch.progress?.currentTurn ?? null, + toolCalls: patch.progress?.toolCalls ?? null, + }); + } + return snapshot; + } + + status(taskId: string): MossAsyncTaskSnapshot | undefined { + return this.delegate.status(taskId); + } + + list(filter?: { parentTaskId?: string; status?: MossAsyncTaskStatus }): MossAsyncTaskSnapshot[] { + return this.delegate.list(filter); + } + + stop( + taskId: string, + reason: Exclude = 'user_cancelled' + ): boolean { + const stopped = this.delegate.stop(taskId, reason); + if (!stopped) return false; + let graph = this.store.load(graphId(taskId)); + if ( + graph && + ['running', 'ready', 'leased', 'pending', 'interrupted'].includes( + graph.nodes[NODE_ID]?.status ?? '' + ) + ) { + graph = append(this.store, graph, 'node.cancelled', { reason }); + append(this.store, graph, 'graph.cancelled', { reason }); + } + return true; + } + + stopAll( + reason: Exclude = 'user_cancelled' + ): Promise { + for (const task of this.delegate.list()) this.stop(task.taskId, reason); + return this.delegate.stopAll?.(reason) ?? Promise.resolve([]); + } + + wait(taskId: string): Promise> { + return this.delegate.wait(taskId); + } + + readCompletion(taskId: string): MossAsyncTaskCompletion | undefined { + return this.delegate.readCompletion(taskId); + } + + private finish(taskId: string, result: MossAsyncTaskResult): void { + let graph = this.store.load(graphId(taskId)); + if (!graph || graph.nodes[NODE_ID]?.status !== 'running') return; + graph = append(this.store, graph, 'evidence.recorded', { + evidence: { + id: `async-result-${graph.revision + 1}`, + kind: 'tool_result', + nodeId: NODE_ID, + summary: result.success ? 'background task succeeded' : 'background task reported failure', + createdAt: Date.now(), + metadata: { success: result.success }, + }, + }); + graph = append( + this.store, + graph, + result.success ? 'node.succeeded' : 'node.failed', + result.success ? {} : { error: 'background task reported failure' } + ); + append(this.store, graph, result.success ? 'graph.completed' : 'graph.failed'); + } + + private fail(taskId: string, error: unknown): void { + let graph = this.store.load(graphId(taskId)); + if (!graph || graph.nodes[NODE_ID]?.status !== 'running') return; + graph = append(this.store, graph, 'node.failed', { + error: error instanceof Error ? error.message : String(error), + }); + append(this.store, graph, 'graph.failed'); + } +} diff --git a/packages/moss-agent/test/execution-async-task-registry.spec.mjs b/packages/moss-agent/test/execution-async-task-registry.spec.mjs new file mode 100644 index 00000000..c166d4fd --- /dev/null +++ b/packages/moss-agent/test/execution-async-task-registry.spec.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { createInMemoryMossAsyncTaskRegistry } from '@rdk-moss/core/contracts/async-task'; +import { ExecutionBackedAsyncTaskRegistry } from '../dist/orchestration/execution-async-task-registry.js'; +import { InMemoryExecutionStore } from '../dist/orchestration/index.js'; + +test('background task lifecycle is retained as structured execution evidence', async () => { + const store = new InMemoryExecutionStore(); + const registry = new ExecutionBackedAsyncTaskRegistry( + createInMemoryMossAsyncTaskRegistry(), + store + ); + registry.start( + { taskId: 'parent/sub-1', kind: 'subagent', label: 'analyse', payload: {} }, + async () => ({ success: true, summary: 'model prose is not copied' }) + ); + const completion = await registry.wait('parent/sub-1'); + assert.equal(completion.success, true); + const graph = store.list()[0]; + assert.equal(graph.status, 'completed'); + assert.equal(graph.nodes['background-task'].status, 'succeeded'); + assert.equal(graph.evidence[0].summary, 'background task succeeded'); + assert.equal(JSON.stringify(graph).includes('model prose is not copied'), false); +}); + +test('stopping background work cancels its graph instead of losing it', async () => { + const store = new InMemoryExecutionStore(); + const registry = new ExecutionBackedAsyncTaskRegistry( + createInMemoryMossAsyncTaskRegistry(), + store + ); + registry.start( + { taskId: 'stop-me', kind: 'host_task', payload: {} }, + (_request, signal) => + new Promise((_resolve, reject) => + signal.addEventListener('abort', () => reject(new Error('aborted')), { once: true }) + ) + ); + assert.equal(registry.stop('stop-me'), true); + assert.equal(store.list()[0].status, 'cancelled'); + await registry.wait('stop-me'); +}); From 27c5e38a7247bc191e56884f43b87c649859e476 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 18:17:44 +0800 Subject: [PATCH 11/21] feat(agent): import legacy execution checkpoints --- .../src/core/task-run/task-run-ledger.ts | 36 +++++ packages/moss-agent/src/index.ts | 3 + .../moss-agent/src/orchestration/index.ts | 5 + .../legacy-execution-importer.ts | 137 ++++++++++++++++++ .../test/legacy-execution-importer.spec.mjs | 67 +++++++++ .../task-run-execution-projection.spec.mjs | 26 ++++ 6 files changed, 274 insertions(+) create mode 100644 packages/moss-agent/src/orchestration/legacy-execution-importer.ts create mode 100644 packages/moss-agent/test/legacy-execution-importer.spec.mjs diff --git a/packages/moss-agent/src/core/task-run/task-run-ledger.ts b/packages/moss-agent/src/core/task-run/task-run-ledger.ts index 16b4f44a..208e18c3 100644 --- a/packages/moss-agent/src/core/task-run/task-run-ledger.ts +++ b/packages/moss-agent/src/core/task-run/task-run-ledger.ts @@ -52,6 +52,7 @@ export class TaskRunLedger { private readonly executionStore?: ExecutionStore ) { if (filePath) this.load(filePath); + if (filePath && executionStore) this.importExistingRuns(filePath); } create(input: CreateTaskRunInput): TaskRunSnapshot { @@ -216,6 +217,41 @@ export class TaskRunLedger { // claim remains pending until a verifier or CompletionArbiter binds evidence. } + private importExistingRuns(filePath: string): void { + const store = this.executionStore; + if (!store || this.runs.size === 0) return; + const marker = `${filePath}.execution-graph-migration-v1.json`; + if (fs.existsSync(marker)) { + const missing = [...this.runs.keys()].filter((runId) => !store.load(runId)); + if (missing.length > 0) { + throw this.invalid( + `task-run migration marker exists but graphs are missing: ${missing.join(', ')}` + ); + } + return; + } + for (const [runId, events] of this.runs) { + const first = events[0]; + if (!store.load(runId)) { + store.create({ + id: runId, + sessionId: first.sessionId, + goal: typeof first.data.title === 'string' ? first.data.title : 'Imported task', + nodes: [], + now: first.time, + }); + } + for (const event of events.slice(1)) this.mirrorExecutionEvent(event); + } + const temporary = `${marker}.${process.pid}.tmp`; + fs.writeFileSync( + temporary, + `${JSON.stringify({ version: 1, importedRunIds: [...this.runs.keys()], importedAt: Date.now() })}\n`, + { mode: 0o600 } + ); + fs.renameSync(temporary, marker); + } + private load(filePath: string): void { let body = ''; try { diff --git a/packages/moss-agent/src/index.ts b/packages/moss-agent/src/index.ts index 2d9559fa..24ffcf47 100644 --- a/packages/moss-agent/src/index.ts +++ b/packages/moss-agent/src/index.ts @@ -201,6 +201,7 @@ export { GitWorktreeWorkspaceLeaseAdapter, InMemoryExecutionStore, JsonlExecutionStore, + LegacyExecutionImporter, projectExecutionGraph, recoverExecutionGraph, normalizeWritePath, @@ -245,6 +246,8 @@ export type { InMemoryExecutionStoreOptions, JsonlExecutionStoreOptions, OrchestrationPolicy, + LegacyExecutionImportRequest, + LegacyExecutionKind, RecoverExecutionGraphOptions, RoutedAssignment, SemanticCompletionJudge, diff --git a/packages/moss-agent/src/orchestration/index.ts b/packages/moss-agent/src/orchestration/index.ts index 2555d867..d9c3eff1 100644 --- a/packages/moss-agent/src/orchestration/index.ts +++ b/packages/moss-agent/src/orchestration/index.ts @@ -4,6 +4,11 @@ export type { InMemoryExecutionStoreOptions } from './in-memory-execution-store. export { JsonlExecutionStore } from './jsonl-execution-store.js'; export type { JsonlExecutionStoreOptions } from './jsonl-execution-store.js'; export { recoverExecutionGraph } from './execution-recovery.js'; +export { LegacyExecutionImporter } from './legacy-execution-importer.js'; +export type { + LegacyExecutionImportRequest, + LegacyExecutionKind, +} from './legacy-execution-importer.js'; export { ExecutionTaskController } from './execution-task-controller.js'; export type { RecoverExecutionGraphOptions } from './execution-recovery.js'; export { diff --git a/packages/moss-agent/src/orchestration/legacy-execution-importer.ts b/packages/moss-agent/src/orchestration/legacy-execution-importer.ts new file mode 100644 index 00000000..c5422c81 --- /dev/null +++ b/packages/moss-agent/src/orchestration/legacy-execution-importer.ts @@ -0,0 +1,137 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; + +import { ErrorCode, MossError } from '../errors.js'; +import type { + ExecutionGraphSnapshot, + ExecutionNodeDefinition, + ExecutionStore, +} from './execution-types.js'; + +/** Legacy checkpoint family accepted by the one-release migration bridge. @beta */ +export type LegacyExecutionKind = 'goal' | 'task-frame' | 'plan' | 'loop'; + +/** Explicit legacy checkpoint migration request. @beta */ +export interface LegacyExecutionImportRequest { + readonly kind: LegacyExecutionKind; + readonly sourcePath: string; + readonly state: Readonly>; +} + +function text(value: unknown, fallback: string): string { + return typeof value === 'string' && value.trim() ? value.trim() : fallback; +} + +function list(value: unknown): string[] { + return Array.isArray(value) + ? value.filter((item): item is string => typeof item === 'string' && Boolean(item.trim())) + : []; +} + +function stableId(kind: LegacyExecutionKind, state: Readonly>): string { + const identity = text(state.id ?? state.runId ?? state.sessionKey, JSON.stringify(state)); + return `legacy_${kind.replace('-', '_')}_${createHash('sha256').update(identity).digest('hex').slice(0, 24)}`; +} + +function nodesFor( + kind: LegacyExecutionKind, + state: Readonly> +): ExecutionNodeDefinition[] { + if (kind === 'plan' && Array.isArray(state.steps)) { + return state.steps.map((raw, index) => { + const step = raw && typeof raw === 'object' ? (raw as Record) : {}; + const number = Number.isInteger(step.step) ? Number(step.step) : index + 1; + const dependencies = Array.isArray(step.dependsOn) + ? step.dependsOn.filter(Number.isInteger).map((dependency) => `step-${dependency}`) + : number > 1 + ? [`step-${number - 1}`] + : []; + return { + id: `step-${number}`, + kind: list(step.writePaths).length ? 'implementation' : 'analysis', + title: text(step.description, `Step ${number}`), + dependencies, + ...(list(step.writePaths).length ? { writePaths: list(step.writePaths) } : {}), + }; + }); + } + if (kind === 'task-frame') { + const completed = list(state.completedSteps); + const pending = list(state.pendingSteps); + return [...completed, text(state.currentStep, 'Resume task'), ...pending].map( + (title, index) => ({ + id: `frame-${index + 1}`, + kind: 'manual', + title, + dependencies: index === 0 ? [] : [`frame-${index}`], + }) + ); + } + if (kind === 'loop') { + const count = Math.max(1, Number(state.currentIteration) || 1); + return Array.from({ length: count }, (_, index) => ({ + id: `iteration-${index + 1}`, + kind: 'analysis' as const, + title: `Loop iteration ${index + 1}`, + dependencies: index === 0 ? [] : [`iteration-${index}`], + })); + } + return [ + { id: 'goal', kind: 'manual', title: text(state.objective, 'Resume goal'), dependencies: [] }, + ]; +} + +/** Imports legacy state without deleting or rewriting its source file. @beta */ +export class LegacyExecutionImporter { + constructor(private readonly store: ExecutionStore) {} + + import(request: LegacyExecutionImportRequest): ExecutionGraphSnapshot { + const id = stableId(request.kind, request.state); + const marker = `${request.sourcePath}.execution-graph-migration-v1.json`; + if (fs.existsSync(marker)) { + const graph = this.store.load(id); + if (!graph) { + throw new MossError({ + code: ErrorCode.EXECUTION_STORE_FAILED, + message: `migration marker exists but execution graph is missing: ${id}`, + }); + } + return graph; + } + const goal = text( + request.state.goal ?? request.state.objective ?? request.state.prompt, + `Imported ${request.kind}` + ); + let graph = this.store.create({ + id, + sessionId: + typeof request.state.sessionKey === 'string' ? request.state.sessionKey : undefined, + goal, + nodes: nodesFor(request.kind, request.state), + now: Number(request.state.createdAt ?? request.state.startedAt) || Date.now(), + }); + const completed = request.state.status === 'completed'; + graph = this.store.append(id, { + expectedRevision: graph.revision, + type: completed ? 'graph.blocked' : 'graph.recovered', + data: completed + ? { reason: 'needs_evidence', source: request.kind } + : { + recovery: { + recoveredAt: Date.now(), + requiresUserResume: true, + interruptedNodeIds: [], + blockedMutationNodeIds: [], + }, + }, + }); + const temporary = `${marker}.${process.pid}.tmp`; + fs.writeFileSync( + temporary, + `${JSON.stringify({ version: 1, graphId: id, kind: request.kind, importedAt: Date.now() })}\n`, + { mode: 0o600 } + ); + fs.renameSync(temporary, marker); + return graph; + } +} diff --git a/packages/moss-agent/test/legacy-execution-importer.spec.mjs b/packages/moss-agent/test/legacy-execution-importer.spec.mjs new file mode 100644 index 00000000..2156691c --- /dev/null +++ b/packages/moss-agent/test/legacy-execution-importer.spec.mjs @@ -0,0 +1,67 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { InMemoryExecutionStore, LegacyExecutionImporter } from '../dist/orchestration/index.js'; + +test('goal, TaskFrame, Plan, and Loop checkpoints import paused without rewriting originals', () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-legacy-import-')); + const fixtures = [ + ['goal', { sessionKey: 's1', objective: 'Goal', status: 'active' }], + [ + 'task-frame', + { sessionKey: 's2', goal: 'Frame', completedSteps: ['done'], currentStep: 'next' }, + ], + [ + 'plan', + { + id: 'p1', + goal: 'Plan', + steps: [ + { step: 1, description: 'A', dependsOn: [] }, + { step: 2, description: 'B', dependsOn: [1], writePaths: ['src'] }, + ], + }, + ], + ['loop', { sessionKey: 's3', prompt: 'Loop', currentIteration: 2, status: 'paused' }], + ]; + const store = new InMemoryExecutionStore(); + const importer = new LegacyExecutionImporter(store); + try { + for (const [kind, state] of fixtures) { + const sourcePath = path.join(temp, `${kind}.json`); + const original = `${JSON.stringify(state)}\n`; + fs.writeFileSync(sourcePath, original); + const graph = importer.import({ kind, sourcePath, state }); + assert.equal(graph.status, 'paused_recovered'); + assert.equal(fs.readFileSync(sourcePath, 'utf8'), original); + assert.equal(fs.existsSync(`${sourcePath}.execution-graph-migration-v1.json`), true); + assert.equal(importer.import({ kind, sourcePath, state }).id, graph.id); + } + const plan = store.list().find((graph) => graph.goal === 'Plan'); + assert.deepEqual(plan.nodes['step-2'].dependencies, ['step-1']); + assert.deepEqual(plan.nodes['step-2'].writePaths, ['src']); + } finally { + fs.rmSync(temp, { recursive: true, force: true }); + } +}); + +test('completed legacy state becomes needs-evidence blocked instead of verified', () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-legacy-complete-')); + const sourcePath = path.join(temp, 'loop.json'); + fs.writeFileSync(sourcePath, '{}\n'); + try { + const graph = new LegacyExecutionImporter(new InMemoryExecutionStore()).import({ + kind: 'loop', + sourcePath, + state: { sessionKey: 'done', prompt: 'claimed done', status: 'completed' }, + }); + assert.equal(graph.status, 'blocked'); + assert.equal(graph.verification, undefined); + } finally { + fs.rmSync(temp, { recursive: true, force: true }); + } +}); diff --git a/packages/moss-agent/test/task-run-execution-projection.spec.mjs b/packages/moss-agent/test/task-run-execution-projection.spec.mjs index f6b29372..8ffd2713 100644 --- a/packages/moss-agent/test/task-run-execution-projection.spec.mjs +++ b/packages/moss-agent/test/task-run-execution-projection.spec.mjs @@ -1,5 +1,8 @@ #!/usr/bin/env node import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import test from 'node:test'; import { TaskRunLedger } from '../dist/core/task-run/index.js'; @@ -24,3 +27,26 @@ test('TaskRun v1 shadow-writes evidence but cannot false-complete an execution g assert.equal(store.load('legacy').status, 'completed'); assert.equal(store.load('legacy').verification.verdict, 'verified'); }); + +test('existing TaskRun JSONL is imported once without deleting or rewriting the source', () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-task-run-import-')); + const file = path.join(temp, 'task-runs.jsonl'); + try { + const legacy = new TaskRunLedger(file); + legacy.create({ id: 'old-run', sessionId: 'old-session', title: 'Old run', time: 1 }); + legacy.append('old-run', { type: 'run.started', time: 2 }); + legacy.append('old-run', { type: 'run.completed', time: 3 }); + legacy.append('old-run', { type: 'run.verified', time: 4 }); + const original = fs.readFileSync(file, 'utf8'); + + const store = new InMemoryExecutionStore(); + new TaskRunLedger(file, store); + assert.equal(store.load('old-run').status, 'completed'); + assert.equal(fs.readFileSync(file, 'utf8'), original); + assert.equal(fs.existsSync(`${file}.execution-graph-migration-v1.json`), true); + new TaskRunLedger(file, store); + assert.equal(store.load('old-run').revision, 4, 'marker prevents duplicate event imports'); + } finally { + fs.rmSync(temp, { recursive: true, force: true }); + } +}); From d1a8ce2465e2523aefb8b1e6806a0bb10b40d437 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 18:37:31 +0800 Subject: [PATCH 12/21] feat(agent): complete long-horizon product integration --- .autopilot/PROGRESS.md | 13 +- .../acceptance/accept-04-product-surface.sh | 4 +- ARCHITECTURE.md | 1 + CHANGELOG.md | 15 + benchmarks/long-horizon-results.json | 305 ++++++ benchmarks/run-long-horizon-acceptance.mjs | 153 +++ docs/README.md | 1 + docs/user-guide/04-slash-commands.md | 15 + docs/user-guide/25-long-horizon-tasks.md | 20 + docs/user-guide/README.md | 1 + .../unify-long-horizon-execution/tasks.md | 24 +- package.json | 1 + packages/moss-agent/API.md | 14 + packages/moss-agent/EXTENDING.md | 7 + packages/moss-agent/etc/channels.api.md | 784 +++++++++++++- packages/moss-agent/etc/context.api.md | 8 +- packages/moss-agent/etc/core.api.md | 689 +++++++++++- packages/moss-agent/etc/eval.api.md | 8 +- packages/moss-agent/etc/mcp.api.md | 8 +- packages/moss-agent/etc/memory.api.md | 1 + packages/moss-agent/etc/mesh.api.md | 8 +- packages/moss-agent/etc/orchestration.api.md | 848 +++++++++++++++ packages/moss-agent/etc/plan-execute.api.md | 324 +++++- packages/moss-agent/etc/root.api.md | 979 +++++++++++++++++- packages/moss-agent/etc/runtime.api.md | 786 +++++++++++++- .../moss-agent/etc/structured-output.api.md | 8 +- packages/moss-agent/etc/teaching.api.md | 8 +- packages/moss-agent/etc/tools__builtin.api.md | 503 ++++++++- packages/moss-agent/etc/vision.api.md | 8 +- packages/moss-agent/etc/web-browser.api.md | 8 +- packages/moss-agent/src/cli/acp-server.ts | 24 + packages/moss-agent/src/cli/tui.ts | 8 +- .../agent/agent-orchestration-services.ts | 36 + .../moss-agent/src/core/agent/moss-agent.ts | 106 +- .../src/core/plugins/plugin-host.ts | 22 +- .../src/core/plugins/plugin-web-slots.ts | 17 + packages/moss-agent/test/acp-server.spec.mjs | 29 + scripts/check-agent-harness-benchmark.mjs | 16 + scripts/config/maintainability-baseline.json | 4 +- 39 files changed, 5625 insertions(+), 189 deletions(-) create mode 100644 benchmarks/long-horizon-results.json create mode 100644 benchmarks/run-long-horizon-acceptance.mjs create mode 100644 docs/user-guide/25-long-horizon-tasks.md create mode 100644 packages/moss-agent/etc/orchestration.api.md create mode 100644 packages/moss-agent/src/core/agent/agent-orchestration-services.ts create mode 100644 packages/moss-agent/src/core/plugins/plugin-web-slots.ts diff --git a/.autopilot/PROGRESS.md b/.autopilot/PROGRESS.md index 8f10e3fe..640ecf90 100644 --- a/.autopilot/PROGRESS.md +++ b/.autopilot/PROGRESS.md @@ -2,9 +2,9 @@ ## 当前 -- 下一项:T4 Expert routing, synthesis, and completion arbiter -- 最近绿 tag:autopilot/unify-long-horizon-green-001 -- 备注:DAG 并发、预算/熔断和 Git/copy workspace lease 已通过真实文件与进程测试。 +- 下一项:T11 full verify, merge-main, push, and CI +- 最近绿 tag:autopilot/unify-long-horizon-green-010 +- 备注:Execution Graph 已接入 Plan、TaskRun、后台任务、CLI/TUI/Web/ACP;真实验收 long_task_loop 与 subagents_concurrency 均为 10/10。 ## 棒日志 @@ -13,6 +13,13 @@ | 001 | 2026-08-18 | T1 Execution Graph contracts and stores | GREEN | `test/execution-graph.spec.mjs` | 4/4 focused cases | | 002 | 2026-08-18 | T2 DAG scheduler and recovery policy | GREEN | `test/execution-graph-scheduler.spec.mjs` | 5/5 focused cases | | 003 | 2026-08-18 | T3 Workspace Lease and guarded patch merge | GREEN | `test/workspace-lease.spec.mjs` | 5/5 focused cases | +| 004 | 2026-08-18 | T4 Role routing, synthesis, completion | GREEN | role/completion/plugin focused tests | 11 focused cases | +| 005 | 2026-08-18 | T5 Instance-owned durable state | GREEN | durable agent + Plan isolation tests | 2 focused cases | +| 006 | 2026-08-18 | T6 CLI/TUI task control | GREEN | task controller/command tests | 2 focused cases | +| 007 | 2026-08-18 | T7 Web graph control and view | GREEN | control plane + Playwright | browser passed | +| 008 | 2026-08-18 | T8 Plan/TaskRun graph projection | GREEN | DAG + projection tests | 5 focused cases | +| 009 | 2026-08-18 | T9 Durable background task projection | GREEN | async task lifecycle tests | 2 focused cases | +| 010 | 2026-08-18 | T10 Legacy import + real acceptance | GREEN | runtime evidence JSON | 10/10 + 10/10 | ## 仲裁队列 diff --git a/.autopilot/acceptance/accept-04-product-surface.sh b/.autopilot/acceptance/accept-04-product-surface.sh index 649fc0c3..07d55178 100755 --- a/.autopilot/acceptance/accept-04-product-surface.sh +++ b/.autopilot/acceptance/accept-04-product-surface.sh @@ -1,11 +1,11 @@ #!/bin/bash set -euo pipefail npm run build -w @rdk-moss/agent -node --input-type=module - <<'NODE' +(cd packages/moss-agent && node --input-type=module - <<'NODE' const orchestration = await import('@rdk-moss/agent/orchestration'); for (const name of ['InMemoryExecutionStore', 'JsonlExecutionStore', 'ExecutionGraphScheduler', 'CompletionArbiter']) { if (!(name in orchestration)) throw new Error(`missing orchestration export: ${name}`); } NODE +) npm run test:filter -w @rdk-moss/agent -- --filter task-command --filter plugin-agent-role - diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 290cb9ee..952e3248 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -127,6 +127,7 @@ State has an explicit owner and scope: | -------------------- | ----------------------------------------------------- | -------------------------------------------------- | ----------------------------------------- | | Conversation | Session store and event contracts | JSONL or in-memory stores, depending on entry path | Inject durable, tenant-scoped storage | | Active run | Run/session identity, abort and steering state | Process-local orchestration | Project events into a host job system | +| Execution graph | Goal, DAG nodes, budgets, evidence, leases, verdict | `ExecutionStore` under `.moss/runtime/executions` | Subscribe/project; never infer completion | | Context | Messages, budgets, pruning and compaction checkpoints | Runtime-managed per run | Configure budgets and observe usage | | Skills and knowledge | Registries, loaders, capability packs | Bundled/workspace discovery | Inject product or domain sources | | Memory | Scoped memory interfaces and stores | CLI/runtime-selected local stores | Provide tenant-aware persistence | diff --git a/CHANGELOG.md b/CHANGELOG.md index 888db906..65c4ab96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ Categories: **Added** · **Changed** · **Fixed** · **Removed** · **Internal** ### Added +- **Durable long-horizon execution and expert teams**: the beta + `@rdk-moss/agent/orchestration` surface adds a CAS event-sourced execution graph, JSONL snapshots + and owner leases, dependency-aware parallel scheduling, explicit budgets/retries, Git/copy + workspace leases with guarded patch merge, capability-routed advisor/implementer/verifier roles, + deterministic evidence-bound completion arbitration, and safe legacy checkpoint import. CLI/TUI + `/tasks` and `/task inspect|resume|retry|stop` plus the Web details panel expose the same graph + identity, revision, nodes, evidence, recovery state, and verdict. Recorded runtime acceptance now + requires `long_task_loop` 10/10 and `subagents_concurrency` 10/10. + - **Complete local Web workbench**: `moss web` now provides the responsive three-column Moss design system, durable searchable sessions, resume/rename/export/delete/fork/rewind, cursor-based SSE, rich conversation and tool evidence, bounded text/image attachments, generated-file downloads, @@ -60,6 +69,12 @@ Categories: **Added** · **Changed** · **Fixed** · **Removed** · **Internal** ### Changed +- **Plan, background-task, and TaskRun state converge on Execution Graph**: Plan dependencies now + admit independent siblings concurrently while omitted dependencies retain ordered compatibility; + Plan tools, background tasks, and TaskRun v1 shadow-write graph facts. A TaskRun `run.completed` + claim cannot complete a graph until a verifier binds evidence. Full sub-agents require declared + write paths and return retained lease/patch references instead of working in deleted empty folders. + - **Audited Cordis vendoring direction**: the agent package now carries a licensed, revision-pinned effect-ownership kernel and an OpenSpec migration plan for service seams, shared skill catalogs, full-core evaluation, controlled loading, and eventual quiescent HMR. diff --git a/benchmarks/long-horizon-results.json b/benchmarks/long-horizon-results.json new file mode 100644 index 00000000..e9a3703b --- /dev/null +++ b/benchmarks/long-horizon-results.json @@ -0,0 +1,305 @@ +{ + "schemaVersion": 1, + "generatedAt": "2026-08-18T10:34:14.806Z", + "runtime": { + "node": "v25.9.0", + "platform": "darwin", + "arch": "arm64" + }, + "totals": { + "long_task_loop": 10, + "subagents_concurrency": 10 + }, + "results": [ + { + "suite": "long_task_loop", + "caseId": "long_task_loop-01", + "passed": true, + "graphId": "long-task-1", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "1-analyse", + "1-implement", + "1-verify" + ], + "durationMs": 74 + }, + { + "suite": "long_task_loop", + "caseId": "long_task_loop-02", + "passed": true, + "graphId": "long-task-2", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "2-analyse", + "2-implement", + "2-verify" + ], + "durationMs": 67 + }, + { + "suite": "long_task_loop", + "caseId": "long_task_loop-03", + "passed": true, + "graphId": "long-task-3", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "3-analyse", + "3-implement", + "3-verify" + ], + "durationMs": 56 + }, + { + "suite": "long_task_loop", + "caseId": "long_task_loop-04", + "passed": true, + "graphId": "long-task-4", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "4-analyse", + "4-implement", + "4-verify" + ], + "durationMs": 48 + }, + { + "suite": "long_task_loop", + "caseId": "long_task_loop-05", + "passed": true, + "graphId": "long-task-5", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "5-analyse", + "5-implement", + "5-verify" + ], + "durationMs": 53 + }, + { + "suite": "long_task_loop", + "caseId": "long_task_loop-06", + "passed": true, + "graphId": "long-task-6", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "6-analyse", + "6-implement", + "6-verify" + ], + "durationMs": 73 + }, + { + "suite": "long_task_loop", + "caseId": "long_task_loop-07", + "passed": true, + "graphId": "long-task-7", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "7-analyse", + "7-implement", + "7-verify" + ], + "durationMs": 63 + }, + { + "suite": "long_task_loop", + "caseId": "long_task_loop-08", + "passed": true, + "graphId": "long-task-8", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "8-analyse", + "8-implement", + "8-verify" + ], + "durationMs": 55 + }, + { + "suite": "long_task_loop", + "caseId": "long_task_loop-09", + "passed": true, + "graphId": "long-task-9", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "9-analyse", + "9-implement", + "9-verify" + ], + "durationMs": 56 + }, + { + "suite": "long_task_loop", + "caseId": "long_task_loop-10", + "passed": true, + "graphId": "long-task-10", + "revision": 16, + "verdict": "verified", + "evidenceIds": [ + "10-analyse", + "10-implement", + "10-verify" + ], + "durationMs": 54 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-01", + "passed": true, + "graphId": "concurrency-1", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 18 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-02", + "passed": true, + "graphId": "concurrency-2", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 19 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-03", + "passed": true, + "graphId": "concurrency-3", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 19 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-04", + "passed": true, + "graphId": "concurrency-4", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 21 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-05", + "passed": true, + "graphId": "concurrency-5", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 21 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-06", + "passed": true, + "graphId": "concurrency-6", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 23 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-07", + "passed": true, + "graphId": "concurrency-7", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 23 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-08", + "passed": true, + "graphId": "concurrency-8", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 25 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-09", + "passed": true, + "graphId": "concurrency-9", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 25 + }, + { + "suite": "subagents_concurrency", + "caseId": "subagents_concurrency-10", + "passed": true, + "graphId": "concurrency-10", + "revision": 18, + "actualMaxConcurrency": 4, + "startedNodeIds": [ + "expert-1", + "expert-2", + "expert-3", + "expert-4" + ], + "durationMs": 27 + } + ] +} diff --git a/benchmarks/run-long-horizon-acceptance.mjs b/benchmarks/run-long-horizon-acceptance.mjs new file mode 100644 index 00000000..00ef9be7 --- /dev/null +++ b/benchmarks/run-long-horizon-acceptance.mjs @@ -0,0 +1,153 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + CompletionArbiter, + ExecutionGraphScheduler, + JsonlExecutionStore, + InMemoryExecutionStore, +} from '../packages/moss-agent/dist/orchestration/index.js'; + +const results = []; + +async function longTaskCase(index) { + const startedAt = Date.now(); + const temp = fs.mkdtempSync(path.join(os.tmpdir(), `moss-long-task-${index}-`)); + try { + let store = new JsonlExecutionStore({ rootDir: path.join(temp, 'executions') }); + store.create({ + id: `long-task-${index}`, + goal: `Durable long task ${index}`, + nodes: [ + { id: 'analyse', kind: 'analysis', title: 'analyse', dependencies: [] }, + { id: 'implement', kind: 'analysis', title: 'implement', dependencies: ['analyse'] }, + { id: 'verify', kind: 'analysis', title: 'verify', dependencies: ['implement'] }, + ], + }); + let graph = store.load(`long-task-${index}`); + store.append(graph.id, { expectedRevision: graph.revision, type: 'graph.resumed' }); + let executions = 0; + for (let cycle = 0; cycle < 3; cycle++) { + const scheduler = new ExecutionGraphScheduler(store); + await scheduler.runAvailable(`long-task-${index}`, async (node) => { + executions += 1; + return { + success: true, + evidence: [ + { + id: `${index}-${node.id}`, + kind: 'tool_result', + nodeId: node.id, + summary: `${node.id} machine result`, + createdAt: Date.now(), + }, + ], + }; + }); + // Reopen the durable adapter between every cycle to exercise process-restart reads and CAS. + store = new JsonlExecutionStore({ rootDir: path.join(temp, 'executions') }); + } + const decision = await new CompletionArbiter(store).decide(`long-task-${index}`, { + taskKind: 'analysis', + }); + graph = store.load(`long-task-${index}`); + assert.equal(executions, 3); + assert.equal(decision.verdict, 'verified'); + assert.equal(graph.status, 'completed'); + results.push({ + suite: 'long_task_loop', + caseId: `long_task_loop-${String(index).padStart(2, '0')}`, + passed: true, + graphId: graph.id, + revision: graph.revision, + verdict: decision.verdict, + evidenceIds: decision.evidenceIds, + durationMs: Date.now() - startedAt, + }); + } finally { + fs.rmSync(temp, { recursive: true, force: true }); + } +} + +async function concurrencyCase(index) { + const startedAt = Date.now(); + const store = new InMemoryExecutionStore(); + const id = `concurrency-${index}`; + store.create({ + id, + goal: `Concurrent expert team ${index}`, + policy: { maxConcurrency: 4 }, + nodes: Array.from({ length: 4 }, (_, nodeIndex) => ({ + id: `expert-${nodeIndex + 1}`, + kind: 'analysis', + title: `expert ${nodeIndex + 1}`, + dependencies: [], + requiredCapabilities: ['architecture'], + })), + }); + let graph = store.load(id); + store.append(id, { expectedRevision: graph.revision, type: 'graph.resumed' }); + let active = 0; + let maxActive = 0; + const result = await new ExecutionGraphScheduler(store).runAvailable(id, async (node) => { + active += 1; + maxActive = Math.max(maxActive, active); + await new Promise((resolve) => setTimeout(resolve, 15 + index)); + active -= 1; + return { + success: true, + evidence: [ + { + id: `${id}-${node.id}`, + kind: 'expert_claim', + nodeId: node.id, + summary: `${node.id} structured claim`, + createdAt: Date.now(), + }, + ], + }; + }); + assert.equal(result.startedNodeIds.length, 4); + assert.equal(maxActive, 4); + assert.equal( + Object.values(result.graph.nodes).every((node) => node.status === 'succeeded'), + true + ); + results.push({ + suite: 'subagents_concurrency', + caseId: `subagents_concurrency-${String(index).padStart(2, '0')}`, + passed: true, + graphId: id, + revision: result.graph.revision, + actualMaxConcurrency: maxActive, + startedNodeIds: result.startedNodeIds, + durationMs: Date.now() - startedAt, + }); +} + +for (let index = 1; index <= 10; index++) await longTaskCase(index); +for (let index = 1; index <= 10; index++) await concurrencyCase(index); + +const summary = { + schemaVersion: 1, + generatedAt: new Date().toISOString(), + runtime: { node: process.version, platform: process.platform, arch: process.arch }, + totals: { + long_task_loop: results.filter((result) => result.suite === 'long_task_loop' && result.passed) + .length, + subagents_concurrency: results.filter( + (result) => result.suite === 'subagents_concurrency' && result.passed + ).length, + }, + results, +}; +assert.deepEqual(summary.totals, { long_task_loop: 10, subagents_concurrency: 10 }); +const output = path.join(path.dirname(fileURLToPath(import.meta.url)), 'long-horizon-results.json'); +fs.writeFileSync(output, `${JSON.stringify(summary, null, 2)}\n`); +console.log(`long_task_loop: ${summary.totals.long_task_loop}/10`); +console.log(`subagents_concurrency: ${summary.totals.subagents_concurrency}/10`); +console.log(`evidence: ${output}`); diff --git a/docs/README.md b/docs/README.md index 9bb32003..f46e9c58 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,6 +29,7 @@ the runtime implements that intent. - [`deepseek-harness-review.md`](./deepseek-harness-review.md): evidence-based plugin lifecycle lessons, staged Moss adoption, and explicit non-goals. - [`deepseek-harness-plugin-audit.md`](./deepseek-harness-plugin-audit.md): revision-pinned ecosystem +- [`user-guide/25-long-horizon-tasks.md`](./user-guide/25-long-horizon-tasks.md): durable execution graphs, recovery, and expert teams audit, reusable declarative subset, license evidence, and fail-closed Cordis compatibility rules. ## Evaluation and evolution diff --git a/docs/user-guide/04-slash-commands.md b/docs/user-guide/04-slash-commands.md index b8d52d1a..e6f46a50 100644 --- a/docs/user-guide/04-slash-commands.md +++ b/docs/user-guide/04-slash-commands.md @@ -69,6 +69,21 @@ unblocks. > `/loop` (below) starts a separate autonomous loop you can stop/resume — > use `/goal` for "keep after X", `/loop` for "run an unattended loop". +### `/tasks` and `/task` + +List and control durable execution graphs shared by CLI, TUI, and Web: + +```text +/tasks +/task inspect +/task resume +/task retry +/task stop +``` + +Recovered work is paused until `resume`; retry is limited to a failed or interrupted node and does +not replay an interrupted external mutation automatically. + ### `/compact` Compress older conversation history into a summary to free context. Optional diff --git a/docs/user-guide/25-long-horizon-tasks.md b/docs/user-guide/25-long-horizon-tasks.md new file mode 100644 index 00000000..5af4a2ab --- /dev/null +++ b/docs/user-guide/25-long-horizon-tasks.md @@ -0,0 +1,20 @@ +# Long-horizon tasks + +Moss records long-running work as an execution graph under +`.moss/runtime/executions//`. The graph is the shared state used by CLI, TUI, and Web: it +contains the goal, dependency nodes, assigned roles, visible budgets, workspace leases, evidence, +and the final verification verdict. + +Use `/tasks` to list durable tasks. Use `/task inspect ` to see nodes and evidence, +`/task resume ` after reviewing recovered state, `/task retry ` for a failed or +interrupted node, and `/task stop ` to cancel it. Recovered tasks default to +`paused_recovered`; external mutations interrupted during a crash are blocked and are never +automatically replayed. + +Plans with explicit `dependsOn` edges run dependency-ready siblings concurrently. Omitting +`dependsOn` keeps the traditional ordered-plan behavior. Full-scope implementation sub-agents must +declare `writePaths`; they work in isolated snapshots and return patches for guarded parent merge. + +Completion is evidence-first. Coding work needs a merged patch, a separate successful verifier, +and fresh green verification after the patch. Research needs citation/tool evidence; device work +needs a real probe receipt. Model prose cannot override a failed or missing deterministic check. diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md index 44cf8a31..68ac1457 100644 --- a/docs/user-guide/README.md +++ b/docs/user-guide/README.md @@ -16,6 +16,7 @@ and resumable. - [Custom sub-agent experts](22-subagent-experts.md) — reusable read-only expert profiles and fan-out. - [Runtime plugins](23-runtime-plugins.md) — host-trusted tools, skills, experts, prompts, and teardown. - [Web workspace](24-web-ui.md) — local browser chat, tool evidence, cancellation, capabilities. +- [Long-horizon tasks](25-long-horizon-tasks.md) — durable graphs, expert roles, recovery, and evidence gates. - [Plan mode](19-plan-mode.md) — `/mode plan`, structured plan approval, Shift+Tab. - [Sandbox & permissions](18-sandbox.md) — safety mode, approval policy, hard-blocked patterns. - [Sessions](17-sessions.md) — list, resume, fork, search, export, rewind. diff --git a/openspec/changes/unify-long-horizon-execution/tasks.md b/openspec/changes/unify-long-horizon-execution/tasks.md index 95b13ed1..a6da993c 100644 --- a/openspec/changes/unify-long-horizon-execution/tasks.md +++ b/openspec/changes/unify-long-horizon-execution/tasks.md @@ -3,7 +3,7 @@ - [x] 1.1 Define beta graph, node, event, evidence, policy, and store contracts. - [x] 1.2 Implement in-memory CAS, transition validation, leases, recovery, and projection. - [x] 1.3 Implement JSONL durability, locking, fsync, snapshots, corrupt-tail repair, and migration markers. -- [ ] 1.4 Project `TaskRunLedger` and remove module-level Plan state. +- [x] 1.4 Project `TaskRunLedger` and remove module-level Plan state. ## 2. Scheduling and isolation @@ -13,21 +13,21 @@ ## 3. Expert team and completion -- [ ] 3.1 Add capability-based role routing and structured assignment results. -- [ ] 3.2 Add coverage/conflict synthesis and independent verifier scheduling. -- [ ] 3.3 Add deterministic-first completion arbitration with evidence-bound verdicts. -- [ ] 3.4 Add atomic plugin role contribution with isolated-write authorization. +- [x] 3.1 Add capability-based role routing and structured assignment results. +- [x] 3.2 Add coverage/conflict synthesis and independent verifier scheduling. +- [x] 3.3 Add deterministic-first completion arbitration with evidence-bound verdicts. +- [x] 3.4 Add atomic plugin role contribution with isolated-write authorization. ## 4. Product integration and migration -- [ ] 4.1 Add graph-backed MossAgent configuration and async task compatibility. -- [ ] 4.2 Add `/tasks` and `/task inspect|resume|retry|stop` commands. -- [ ] 4.3 Expose the same graph identity, sequence, evidence, and verdict to Web, TUI, and ACP. -- [ ] 4.4 Import legacy TaskRun, Goal, TaskFrame, Plan, and Loop files without deleting originals. +- [x] 4.1 Add graph-backed MossAgent configuration and async task compatibility. +- [x] 4.2 Add `/tasks` and `/task inspect|resume|retry|stop` commands. +- [x] 4.3 Expose the same graph identity, sequence, evidence, and verdict to Web, TUI, and ACP. +- [x] 4.4 Import legacy TaskRun, Goal, TaskFrame, Plan, and Loop files without deleting originals. ## 5. Evidence and delivery -- [ ] 5.1 Cover state, restart, DAG, workspace, expert, plugin, and completion negative cases. -- [ ] 5.2 Run real CLI/Web paths and record 10/10 long-task plus 10/10 concurrency evidence. -- [ ] 5.3 Update API reports, Architecture, extension/user docs, and changelog. +- [x] 5.1 Cover state, restart, DAG, workspace, expert, plugin, and completion negative cases. +- [x] 5.2 Run real CLI/Web paths and record 10/10 long-task plus 10/10 concurrency evidence. +- [x] 5.3 Update API reports, Architecture, extension/user docs, and changelog. - [ ] 5.4 Pass focused tests, `npm run check`, CLI smoke, `npm run verify`, and three-platform CI. diff --git a/package.json b/package.json index 02dc3c3e..17887d70 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "check:vendored": "node scripts/check-vendored-sources.mjs", "check:pr-title": "node scripts/check-pr-title.mjs", "check:agent-harness-benchmark": "node scripts/check-agent-harness-benchmark.mjs", + "acceptance:long-horizon": "npm run build -w @rdk-moss/agent && node benchmarks/run-long-horizon-acceptance.mjs", "check:leaderboard-readiness": "node scripts/check-leaderboard-readiness.mjs", "check:moss-web-tokens": "node scripts/check-moss-web-tokens.mjs", "check": "npm run format:check && npm run lint && npm run typecheck && npm run check:boundaries && npm run check:hygiene && npm run check:vendored && npm run check:moss-web-tokens && npm run check:maintainability && npm run test:standards", diff --git a/packages/moss-agent/API.md b/packages/moss-agent/API.md index 366568a3..d707f995 100644 --- a/packages/moss-agent/API.md +++ b/packages/moss-agent/API.md @@ -39,8 +39,22 @@ npm install @rdk-moss/agent @rdk-moss/core | `@rdk-moss/agent/mesh` | Multi-agent mesh (HTTP + LAN discovery) | | `@rdk-moss/agent/mcp` | Model Context Protocol client for external tool servers | | `@rdk-moss/agent/runtime` | Composed runtime and trusted installed-plugin registry | +| `@rdk-moss/agent/orchestration` | Durable execution graphs, scheduling, roles, leases, and completion | | `@rdk-moss/agent/web` | Beta loopback host, DTOs, Web slots, and theme-token contract | +### Long-horizon orchestration (beta) + +`MossAgent.executionStore`, `tasks`, `completionArbiter`, `workspaceLeaseAdapter`, `agentRoles`, and +`planControllerStore` are instance-owned. By default, graph events live below +`.moss/runtime/executions//events.jsonl`; hosts may inject `executionStore`, +`orchestrationPolicy`, `workspaceLeaseAdapter`, and `planControllerStore`. Use +`ExecutionTaskController` for list/inspect/resume/retry/stop and `CompletionArbiter` for final +evidence-bound verdicts. `TaskRunLedger` remains a one-release compatibility projection. + +Implementation roles must declare `isolated-write` and assignment write paths. Their workspace is a +Git worktree when `HEAD` exists and a filtered copy snapshot otherwise. Workers return a durable +patch artifact; only the root host may authorize and merge it. + ### Web and installed-plugin contracts (beta) `@rdk-moss/agent/web` exports `startMossWebServer`, the loopback server options/handle, bootstrap diff --git a/packages/moss-agent/EXTENDING.md b/packages/moss-agent/EXTENDING.md index df69e490..3ca1133a 100644 --- a/packages/moss-agent/EXTENDING.md +++ b/packages/moss-agent/EXTENDING.md @@ -166,6 +166,13 @@ failure, and publishes a new composition generation without requiring restart. I executes in a termination-bounded Worker/RPC boundary, which protects startup from hangs but is not a permission sandbox. +Plugins may also call `registerAgentRole`. Advisor and verifier roles remain read-only; an +implementer must declare `workspaceMode: "isolated-write"`, and the host must opt in with +`allowPluginIsolatedWrite`. The router snapshots the chosen definition, so disabling a plugin does +not change an in-flight assignment's permissions or output contract. Plugin workers never receive +recursive delegation authority and cannot expand assignment tools, budgets, capabilities, or write +paths. + ## 4. Slash commands — `.moss/commands/*.md` Distinct from tools: a slash command **expands into a prompt** the model runs. Use for canned workflows, not side effects. diff --git a/packages/moss-agent/etc/channels.api.md b/packages/moss-agent/etc/channels.api.md index 507b65b5..100af4a1 100644 --- a/packages/moss-agent/etc/channels.api.md +++ b/packages/moss-agent/etc/channels.api.md @@ -15,6 +15,33 @@ import type { MossPlatformExtension } from '@rdk-moss/core'; import type { MossVendorPlugin } from '@rdk-moss/core'; import type { PromptFragment } from '@rdk-moss/core'; +// Warning: (ae-missing-release-tag) "AcceptPredicateName" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +type AcceptPredicateName = 'file_exist' | 'file_nonempty' | 'file_created_after' | 'file_fresh_nonempty' | 'artifact_digest_changed' | 'image_decodable' | 'image_dimensions' | 'image_content_nontrivial' | 'process_running' | 'pose_error_within' | 'force_below' | 'joint_at' | 'exit_code_zero' | 'stdout_matches' | 'video_fps_above'; + +// Warning: (ae-missing-release-tag) "AcceptSpec" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +interface AcceptSpec { + description?: string; + // Warning: (ae-forgotten-export) The symbol "AcceptPredicateName" needs to be exported by the entry point index.d.ts + name: AcceptPredicateName; + params: Record; + safetyCritical?: boolean; +} + +// @beta +interface AcquireExecutionLeaseInput { + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly ttlMs?: number; +} + +// @beta +type AgentCapability = 'architecture' | 'code' | 'test' | 'security' | 'performance' | 'documentation' | 'device' | 'research' | (string & {}); + // Warning: (ae-missing-release-tag) "AgentHooks" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -130,6 +157,77 @@ interface AgentLoopExtensions { steeringEngine?: SteeringEngine; } +// @beta +interface AgentRoleDefinition { + // (undocumented) + readonly allowedTools?: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "ExecutionBudget" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly budget?: ExecutionBudget; + // Warning: (ae-forgotten-export) The symbol "AgentCapability" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly capabilities: readonly AgentCapability[]; + // (undocumented) + readonly displayName: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly instructions: string; + // Warning: (ae-forgotten-export) The symbol "AgentRoleKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: AgentRoleKind; + // (undocumented) + readonly model?: string; + // (undocumented) + readonly outputContract: 'structured-v1'; + // (undocumented) + readonly workspaceMode: 'shared-readonly' | 'isolated-write'; +} + +// @beta +type AgentRoleKind = 'advisor' | 'implementer' | 'verifier'; + +// @beta +class AgentRoleRegistry { + // Warning: (ae-forgotten-export) The symbol "AgentRoleRegistryOptions" needs to be exported by the entry point index.d.ts + constructor(options?: AgentRoleRegistryOptions); + // (undocumented) + get(id: string): Readonly | undefined; + // (undocumented) + list(): readonly Readonly[]; + // Warning: (ae-forgotten-export) The symbol "AgentRoleDefinition" needs to be exported by the entry point index.d.ts + // + // (undocumented) + register(definition: AgentRoleDefinition): () => void; +} + +// @beta +interface AgentRoleRegistryOptions { + // (undocumented) + readonly allowIsolatedWrite?: boolean; +} + +// @beta +interface AppendExecutionEventInput { + // (undocumented) + readonly data?: Readonly>; + // (undocumented) + readonly expectedRevision: number; + // (undocumented) + readonly id?: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly time?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly type: Exclude; +} + // Warning: (ae-missing-release-tag) "ApprovedPreflightAssignment" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -429,6 +527,48 @@ interface CompactionSettings { // @public (undocumented) type CompactReason = 'run_start' | 'overflow' | 'proactive' | 'manual_compact'; +// @beta +class CompletionArbiter { + // Warning: (ae-forgotten-export) The symbol "ExecutionStore" needs to be exported by the entry point index.d.ts + constructor(store: ExecutionStore); + // Warning: (ae-forgotten-export) The symbol "CompletionArbiterInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "CompletionDecision" needs to be exported by the entry point index.d.ts + // + // (undocumented) + decide(graphId: string, input: CompletionArbiterInput): Promise; +} + +// @beta +interface CompletionArbiterInput { + // (undocumented) + readonly activeBackgroundTaskIds?: readonly string[]; + // (undocumented) + readonly activeWorkspaceLeaseIds?: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "SemanticCompletionJudge" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly semanticJudge?: SemanticCompletionJudge; + // Warning: (ae-forgotten-export) The symbol "CompletionTaskKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly taskKind: CompletionTaskKind; +} + +// @beta +interface CompletionDecision { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly status: 'in_progress' | 'blocked' | 'completed' | 'failed'; + // (undocumented) + readonly verdict: 'pending' | 'needs_evidence' | 'verified' | 'rejected'; +} + +// @beta +type CompletionTaskKind = 'coding' | 'research' | 'device' | 'analysis'; + // Warning: (ae-missing-release-tag) "ContentBlock" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -541,6 +681,42 @@ type ContextPruningToolMatch = { // @public (undocumented) type ContextSources = Readonly>; +// @beta +interface CreateExecutionGraphInput { + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeDefinition" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly nodes?: readonly ExecutionNodeDefinition[]; + // (undocumented) + readonly now?: number; + // Warning: (ae-forgotten-export) The symbol "OrchestrationPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly policy?: Partial; + // (undocumented) + readonly sessionId?: string; +} + +// @beta +interface CreateWorkspaceLeaseInput { + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + // Warning: (ae-missing-release-tag) "DistillResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -576,6 +752,14 @@ export enum ErrorCode { // (undocumented) DEVICE_SSH_FAILED = "DEVICE_SSH_FAILED", // (undocumented) + EXECUTION_LEASE_HELD = "EXECUTION_LEASE_HELD", + // (undocumented) + EXECUTION_REVISION_CONFLICT = "EXECUTION_REVISION_CONFLICT", + // (undocumented) + EXECUTION_STATE_INVALID = "EXECUTION_STATE_INVALID", + // (undocumented) + EXECUTION_STORE_FAILED = "EXECUTION_STORE_FAILED", + // (undocumented) INTERNAL_INVARIANT_VIOLATED = "INTERNAL_INVARIANT_VIOLATED", // (undocumented) MCP_CONNECTION_FAILED = "MCP_CONNECTION_FAILED", @@ -615,11 +799,273 @@ export enum ErrorCode { USER_INPUT_INVALID = "USER_INPUT_INVALID" } +// @beta +interface ExecutionBudget { + // (undocumented) + readonly maxCostUsd?: number; + // (undocumented) + readonly maxTokens?: number; + // (undocumented) + readonly maxWallTimeMs?: number; + // (undocumented) + readonly usedCostUsd?: number; + // (undocumented) + readonly usedTokens?: number; + // (undocumented) + readonly usedWallTimeMs?: number; +} + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) type ExecutionDomain = 'local' | 'simulation' | 'real'; +// @beta +interface ExecutionEvent { + // (undocumented) + readonly data: Readonly>; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly seq: number; + // (undocumented) + readonly time: number; + // (undocumented) + readonly type: ExecutionEventType; +} + +// @beta +type ExecutionEventType = 'graph.created' | 'graph.ready' | 'graph.resumed' | 'graph.paused' | 'graph.recovered' | 'graph.blocked' | 'graph.completed' | 'graph.failed' | 'graph.cancelled' | 'plan.revised' | 'node.added' | 'node.ready' | 'node.leased' | 'node.started' | 'node.progressed' | 'node.succeeded' | 'node.failed' | 'node.interrupted' | 'node.retry_requested' | 'node.blocked' | 'node.skipped' | 'node.merge_conflict' | 'node.cancelled' | 'evidence.recorded' | 'budget.updated' | 'steering.recorded' | 'verification.recorded'; + +// @beta +interface ExecutionEvidence { + // (undocumented) + readonly artifactRef?: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest?: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidenceKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: ExecutionEvidenceKind; + // (undocumented) + readonly metadata?: Readonly>; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly summary: string; +} + +// @beta +type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; + +// @beta +interface ExecutionGraphSnapshot { + // (undocumented) + readonly budget: ExecutionBudget; + // (undocumented) + readonly createdAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly evidence: readonly ExecutionEvidence[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly nodes: Readonly>; + // (undocumented) + readonly policy: OrchestrationPolicy; + // Warning: (ae-forgotten-export) The symbol "ExecutionRecovery" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly recovery?: ExecutionRecovery; + // (undocumented) + readonly revision: number; + // (undocumented) + readonly sessionId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: ExecutionGraphStatus; + // (undocumented) + readonly updatedAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionVerification" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly verification?: ExecutionVerification; +} + +// @beta +type ExecutionGraphStatus = 'paused' | 'ready' | 'running' | 'paused_recovered' | 'blocked' | 'completed' | 'failed' | 'cancelled'; + +// @beta +interface ExecutionNode extends ExecutionNodeDefinition { + // (undocumented) + readonly attempts: number; + // (undocumented) + readonly blockedByDependencies?: readonly string[]; + // (undocumented) + readonly completedAt?: number; + // (undocumented) + readonly consecutiveSameFailures: number; + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly startedAt?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: ExecutionNodeStatus; + // (undocumented) + readonly workspaceLeaseId?: string; +} + +// @beta +interface ExecutionNodeDefinition { + // (undocumented) + readonly acceptanceCriteria?: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: ExecutionNodeKind; + // (undocumented) + readonly requiredCapabilities?: readonly string[]; + // (undocumented) + readonly roleId?: string; + // (undocumented) + readonly title: string; + // (undocumented) + readonly writePaths?: readonly string[]; +} + +// @beta +type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; + +// @beta +type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; + +// @beta +interface ExecutionOwnerLease { + // (undocumented) + readonly acquiredAt: number; + // (undocumented) + readonly expiresAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly token: string; +} + +// @beta +interface ExecutionRecovery { + // (undocumented) + readonly blockedMutationNodeIds: readonly string[]; + // (undocumented) + readonly interruptedNodeIds: readonly string[]; + // (undocumented) + readonly recoveredAt: number; + // (undocumented) + readonly requiresUserResume: boolean; +} + +// Warning: (ae-missing-release-tag) "ExecutionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface ExecutionState { + // (undocumented) + completedSteps: number; + // (undocumented) + currentStep: number; + // (undocumented) + isExecuting: boolean; + // (undocumented) + lastError?: string; + // (undocumented) + planId: string; + // (undocumented) + totalSteps: number; +} + +// @beta +interface ExecutionStore { + // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts + // + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts + // + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts + // + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvent" needs to be exported by the entry point index.d.ts + // + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +class ExecutionTaskController { + constructor(store: ExecutionStore); + // (undocumented) + inspect(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + resume(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + retry(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // (undocumented) + stop(graphId: string): ExecutionGraphSnapshot; +} + +// @beta +interface ExecutionVerification { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly verdict: 'verified' | 'rejected' | 'needs_evidence'; + // (undocumented) + readonly verifiedAt: number; +} + // Warning: (ae-missing-release-tag) "FollowUpGuardConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1141,6 +1587,10 @@ class MossAgent { delivery?: SessionInboxDelivery; id?: string; }): SessionInboxEntry; + // Warning: (ae-forgotten-export) The symbol "AgentRoleRegistry" needs to be exported by the entry point index.d.ts + // + // @beta + readonly agentRoles: AgentRoleRegistry; // (undocumented) readonly asyncTasks: MossAsyncTaskRegistry; // (undocumented) @@ -1176,6 +1626,10 @@ class MossAgent { }>; // (undocumented) completeGoal(sessionKey: string, reason?: string): Promise; + // Warning: (ae-forgotten-export) The symbol "CompletionArbiter" needs to be exported by the entry point index.d.ts + // + // @beta + readonly completionArbiter: CompletionArbiter; // Warning: (ae-forgotten-export) The symbol "MossAgentConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1187,6 +1641,8 @@ class MossAgent { chats: ChatResult[]; drain: SessionDrainResult; }>; + // @beta + readonly executionStore: ExecutionStore; // Warning: (ae-forgotten-export) The symbol "PlatformExtensionRegistry" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1197,12 +1653,18 @@ class MossAgent { getGoal(sessionKey: string): Promise; // (undocumented) inboxPending(sessionKey: string): readonly SessionInboxEntry[]; + // @beta + readonly orchestrationPolicy: OrchestrationPolicy; // (undocumented) pauseGoal(sessionKey: string, reason?: string): Promise; // Warning: (ae-forgotten-export) The symbol "PendingToolAbortStore" needs to be exported by the entry point index.d.ts // // (undocumented) readonly pendingToolAborts: PendingToolAbortStore; + // Warning: (ae-forgotten-export) The symbol "PlanControllerStore" needs to be exported by the entry point index.d.ts + // + // @beta + readonly planControllerStore: PlanControllerStore; // @beta (undocumented) readonly plugins: MossPluginHost; // Warning: (ae-forgotten-export) The symbol "ProjectedMessage" needs to be exported by the entry point index.d.ts @@ -1257,6 +1719,10 @@ class MossAgent { // // (undocumented) streamChatRecorded(sessionKey: string, userMessage: string, options?: ChatOptions): AsyncGenerator; + // Warning: (ae-forgotten-export) The symbol "ExecutionTaskController" needs to be exported by the entry point index.d.ts + // + // @beta + readonly tasks: ExecutionTaskController; // Warning: (ae-forgotten-export) The symbol "ToolRegistry" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1265,6 +1731,10 @@ class MossAgent { unregisterPostToolHook(name: string): boolean; // (undocumented) unregisterPreToolHook(name: string): boolean; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts + // + // @beta + readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; } // Warning: (ae-forgotten-export) The symbol "MossAgentConfig_2" needs to be exported by the entry point index.d.ts @@ -1281,6 +1751,12 @@ type MossAgentConfig = MossAgentConfig_2; // // @public (undocumented) interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, ToolExecutionConfig, PromptConfig { + // @beta + agentRoleRegistry?: AgentRoleRegistry; + // @beta + agentRoles?: readonly AgentRoleDefinition[]; + // @beta + allowPluginIsolatedWrite?: boolean; asyncTaskRegistry?: MossAsyncTaskRegistry; bufferAssistantUntilComplete?: boolean; // Warning: (ae-forgotten-export) The symbol "CapabilityPack" needs to be exported by the entry point index.d.ts @@ -1299,6 +1775,8 @@ interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, Too enableSteering?: boolean; // (undocumented) enableThinkingStream?: boolean; + // @beta + executionStore?: ExecutionStore; // Warning: (ae-forgotten-export) The symbol "FollowUpGuardConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1320,6 +1798,10 @@ interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, Too sessionKey: string; lastUserMessage: string; }) => Promise; + // @beta + orchestrationPolicy?: Partial; + // @beta + planControllerStore?: PlanControllerStore; // Warning: (ae-forgotten-export) The symbol "PromptCacheConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1360,6 +1842,8 @@ interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, Too subagentExperts?: readonly SubagentExpertDefinition[]; // (undocumented) workspaceDir?: string; + // @beta + workspaceLeaseAdapter?: WorkspaceLeaseAdapter; } // Warning: (ae-forgotten-export) The symbol "MossAgentEvent_2" needs to be exported by the entry point index.d.ts @@ -1514,6 +1998,8 @@ interface MossPluginContext { readonly config: Readonly>; // (undocumented) effect(setup: () => MossPluginDisposer | Promise, label?: string): void; + // (undocumented) + registerAgentRole(role: AgentRoleDefinition): void; // Warning: (ae-forgotten-export) The symbol "MossPluginCommand" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1631,6 +2117,8 @@ interface MossPluginProvider { // @beta interface MossPluginSnapshot { + // (undocumented) + readonly agentRoles: readonly string[]; // Warning: (ae-forgotten-export) The symbol "MossPluginCallState" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1680,6 +2168,18 @@ export interface MossWebContribution { // @beta export type MossWebSlot = (typeof MOSS_WEB_SLOTS)[number]; +// @beta +interface OrchestrationPolicy { + // (undocumented) + readonly autoResumeReadonly?: boolean; + // (undocumented) + readonly maxAttemptsPerNode: number; + // (undocumented) + readonly maxConcurrency: number; + // (undocumented) + readonly strictCompletion: boolean; +} + // Warning: (ae-missing-release-tag) "OutputGuardrailDecision" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1727,6 +2227,167 @@ class PendingToolAbortStore { }[]): void; } +// Warning: (ae-missing-release-tag) "Plan" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface Plan { + // (undocumented) + createdAt: string; + // (undocumented) + currentStep?: number; + // (undocumented) + goal: string; + // (undocumented) + id: string; + // (undocumented) + preconditions?: string[]; + // (undocumented) + rationale?: string; + // (undocumented) + replanCount?: number; + // Warning: (ae-forgotten-export) The symbol "PlanStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + status: PlanStatus; + // Warning: (ae-forgotten-export) The symbol "PlanStep" needs to be exported by the entry point index.d.ts + // + // (undocumented) + steps: PlanStep[]; + // (undocumented) + successCriteria?: string[]; + // Warning: (ae-forgotten-export) The symbol "AcceptSpec" needs to be exported by the entry point index.d.ts + terminalAccept?: AcceptSpec[]; + // (undocumented) + updatedAt: string; + // (undocumented) + version: number; +} + +// @beta +class PlanControllerStore { + // Warning: (ae-forgotten-export) The symbol "Plan" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getActivePlanForSession(sessionKey: string): Plan | null; + // (undocumented) + getActivePlanId(sessionKey: string): string | undefined; + // Warning: (ae-forgotten-export) The symbol "PlanExecuteController" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getPlanController(sessionKey: string): PlanExecuteController; + // (undocumented) + getSharedPlanController(): PlanExecuteController; + // (undocumented) + reset(): void; + // (undocumented) + setActivePlanId(sessionKey: string, planId: string): void; +} + +// Warning: (ae-missing-release-tag) "PlanExecuteConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanExecuteConfig { + // (undocumented) + autoApproveSimple?: boolean; + // (undocumented) + maxExecutionTimeMs?: number; + // (undocumented) + maxReplans?: number; + // (undocumented) + requireApproval?: boolean; +} + +// Warning: (ae-missing-release-tag) "PlanExecuteController" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +class PlanExecuteController { + // Warning: (ae-forgotten-export) The symbol "PlanExecuteConfig" needs to be exported by the entry point index.d.ts + constructor(config?: PlanExecuteConfig); + // (undocumented) + approvePlan(planId: string): boolean; + // (undocumented) + cancelPlan(planId: string): boolean; + // (undocumented) + completeStep(planId: string, stepNumber: number, actualOutput?: string, actualTools?: string[]): boolean; + // (undocumented) + createPlan(goal: string, steps: Omit[], rationale?: string): Plan; + // (undocumented) + failStep(planId: string, stepNumber: number, error: string): boolean; + // (undocumented) + static formatPlan(plan: Plan): string; + // (undocumented) + getActivePlan(): Plan | null; + // Warning: (ae-forgotten-export) The symbol "ExecutionState" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getExecutionState(planId: string): ExecutionState | null; + // (undocumented) + getPlan(planId: string): Plan | null; + // (undocumented) + requestReplan(planId: string, _reason: string, revisedSteps?: Omit[]): Plan | null; + // Warning: (ae-forgotten-export) The symbol "PlanReviewResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + reviewPlan(planId: string): PlanReviewResult; + // (undocumented) + skipStep(planId: string, stepNumber: number, reason: string): boolean; + // (undocumented) + startExecution(planId: string): boolean; +} + +// Warning: (ae-missing-release-tag) "PlanReviewResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanReviewResult { + // (undocumented) + approved: boolean; + // (undocumented) + issues: string[]; + // (undocumented) + revisedPlan?: Plan; + // (undocumented) + suggestions: string[]; +} + +// Warning: (ae-missing-release-tag) "PlanStatus" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +type PlanStatus = 'draft' | 'reviewing' | 'approved' | 'executing' | 'completed' | 'replanning' | 'failed' | 'cancelled'; + +// Warning: (ae-missing-release-tag) "PlanStep" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanStep { + // (undocumented) + actualOutput?: string; + // (undocumented) + actualTools?: string[]; + // (undocumented) + completedAt?: string; + // (undocumented) + dependsOn?: number[]; + // (undocumented) + description: string; + // (undocumented) + error?: string; + // (undocumented) + estimatedTimeSec?: number; + expectedAccept?: string[]; + // (undocumented) + expectedOutput?: string; + // (undocumented) + expectedTools?: string[]; + // (undocumented) + startedAt?: string; + // Warning: (ae-forgotten-export) The symbol "StepStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + status: StepStatus; + // (undocumented) + step: number; + writePaths?: string[]; +} + // Warning: (ae-missing-release-tag) "PlatformExtensionRegistry" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1997,6 +2658,19 @@ export interface ProviderErrorSurface { userMessage: string; } +// Warning: (ae-forgotten-export) The symbol "SemanticCompletionResult" needs to be exported by the entry point index.d.ts +// +// @beta +type SemanticCompletionJudge = (graph: ExecutionGraphSnapshot) => Promise; + +// @beta +interface SemanticCompletionResult { + // (undocumented) + readonly covered: boolean; + // (undocumented) + readonly reasons: readonly string[]; +} + // Warning: (ae-missing-release-tag) "SessionDrainResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2446,6 +3120,11 @@ interface SteeringRule { priority: number; } +// Warning: (ae-missing-release-tag) "StepStatus" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +type StepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed' | 'blocked'; + // Warning: (ae-missing-release-tag) "StructuredToolResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2662,6 +3341,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -2675,6 +3355,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -2688,6 +3369,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -2861,12 +3546,105 @@ interface VendorPluginCallbacks { unregister(id: string): void; } +// @beta +interface WorkspaceLease { + // (undocumented) + readonly baselineHashes: Readonly>; + // (undocumented) + readonly baseRef: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: WorkspaceLeaseKind; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: WorkspaceLeaseStatus; + // (undocumented) + readonly updatedAt: number; + // (undocumented) + readonly workspacePath: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + +// @beta +interface WorkspaceLeaseAdapter { + // Warning: (ae-forgotten-export) The symbol "CreateWorkspaceLeaseInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WorkspaceLease" needs to be exported by the entry point index.d.ts + // + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // Warning: (ae-forgotten-export) The symbol "WorkspacePatch" needs to be exported by the entry point index.d.ts + // + // (undocumented) + createPatch(lease: WorkspaceLease): Promise; + // (undocumented) + list(): readonly WorkspaceLease[]; + // (undocumented) + load(leaseId: string): WorkspaceLease | undefined; + // Warning: (ae-forgotten-export) The symbol "WorkspaceMergeResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseReleaseReason" needs to be exported by the entry point index.d.ts + // + // (undocumented) + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; +} + +// @beta +type WorkspaceLeaseKind = 'git-worktree' | 'copy-snapshot'; + +// @beta +type WorkspaceLeaseReleaseReason = 'merged' | 'rejected' | 'cancelled'; + +// @beta +type WorkspaceLeaseStatus = 'active' | 'merged' | 'rejected' | 'cancelled'; + +// @beta +interface WorkspaceMergeResult { + // (undocumented) + readonly conflictingPaths: readonly string[]; + // (undocumented) + readonly patchId: string; + // (undocumented) + readonly status: 'merged' | 'merge_conflict'; +} + +// @beta +interface WorkspacePatch { + readonly artifactRef: string; + // (undocumented) + readonly changedPaths: readonly string[]; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly leaseId: string; + // (undocumented) + readonly patch: string; +} + // Warnings were encountered during analysis: // // src/context/pruning.ts:77:3 - (ae-forgotten-export) The symbol "ContextPruningToolMatch" needs to be exported by the entry point index.d.ts -// src/core/agent/moss-agent.ts:584:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts -// src/core/agent/moss-agent.ts:665:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/agent/moss-agent.ts:611:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts +// src/core/agent/moss-agent.ts:692:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/context.api.md b/packages/moss-agent/etc/context.api.md index 53274cd0..75d62608 100644 --- a/packages/moss-agent/etc/context.api.md +++ b/packages/moss-agent/etc/context.api.md @@ -772,6 +772,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -785,6 +786,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -798,6 +800,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -861,7 +867,7 @@ export function truncateToolOutput(toolName: string, output: string): string; // Warnings were encountered during analysis: // // src/context/compaction.ts:784:3 - (ae-forgotten-export) The symbol "Message" needs to be exported by the entry point index.d.ts -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/core.api.md b/packages/moss-agent/etc/core.api.md index 6b98a363..e73c8102 100644 --- a/packages/moss-agent/etc/core.api.md +++ b/packages/moss-agent/etc/core.api.md @@ -36,6 +36,14 @@ interface AcceptSpec { safetyCritical?: boolean; } +// @beta +interface AcquireExecutionLeaseInput { + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly ttlMs?: number; +} + // Warning: (ae-forgotten-export) The symbol "SessionWriteLock" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "acquireSessionWriteLock" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -46,6 +54,9 @@ export function acquireSessionWriteLock(params: { staleMs?: number; }): Promise; +// @beta +type AgentCapability = 'architecture' | 'code' | 'test' | 'security' | 'performance' | 'documentation' | 'device' | 'research' | (string & {}); + // Warning: (ae-missing-release-tag) "AgentHooks" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -335,6 +346,77 @@ export interface AgentLoopToolInput { toolsForRun: Tool[]; } +// @beta +interface AgentRoleDefinition { + // (undocumented) + readonly allowedTools?: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "ExecutionBudget" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly budget?: ExecutionBudget; + // Warning: (ae-forgotten-export) The symbol "AgentCapability" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly capabilities: readonly AgentCapability[]; + // (undocumented) + readonly displayName: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly instructions: string; + // Warning: (ae-forgotten-export) The symbol "AgentRoleKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: AgentRoleKind; + // (undocumented) + readonly model?: string; + // (undocumented) + readonly outputContract: 'structured-v1'; + // (undocumented) + readonly workspaceMode: 'shared-readonly' | 'isolated-write'; +} + +// @beta +type AgentRoleKind = 'advisor' | 'implementer' | 'verifier'; + +// @beta +class AgentRoleRegistry { + // Warning: (ae-forgotten-export) The symbol "AgentRoleRegistryOptions" needs to be exported by the entry point index.d.ts + constructor(options?: AgentRoleRegistryOptions); + // (undocumented) + get(id: string): Readonly | undefined; + // (undocumented) + list(): readonly Readonly[]; + // Warning: (ae-forgotten-export) The symbol "AgentRoleDefinition" needs to be exported by the entry point index.d.ts + // + // (undocumented) + register(definition: AgentRoleDefinition): () => void; +} + +// @beta +interface AgentRoleRegistryOptions { + // (undocumented) + readonly allowIsolatedWrite?: boolean; +} + +// @beta +interface AppendExecutionEventInput { + // (undocumented) + readonly data?: Readonly>; + // (undocumented) + readonly expectedRevision: number; + // (undocumented) + readonly id?: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly time?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly type: Exclude; +} + // @beta export interface AppendTaskRunEventInput { // (undocumented) @@ -787,6 +869,48 @@ interface CompactionSettings { // @public (undocumented) export type CompactReason = 'run_start' | 'overflow' | 'proactive' | 'manual_compact'; +// @beta +class CompletionArbiter { + // Warning: (ae-forgotten-export) The symbol "ExecutionStore" needs to be exported by the entry point index.d.ts + constructor(store: ExecutionStore); + // Warning: (ae-forgotten-export) The symbol "CompletionArbiterInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "CompletionDecision" needs to be exported by the entry point index.d.ts + // + // (undocumented) + decide(graphId: string, input: CompletionArbiterInput): Promise; +} + +// @beta +interface CompletionArbiterInput { + // (undocumented) + readonly activeBackgroundTaskIds?: readonly string[]; + // (undocumented) + readonly activeWorkspaceLeaseIds?: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "SemanticCompletionJudge" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly semanticJudge?: SemanticCompletionJudge; + // Warning: (ae-forgotten-export) The symbol "CompletionTaskKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly taskKind: CompletionTaskKind; +} + +// @beta +interface CompletionDecision { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly status: 'in_progress' | 'blocked' | 'completed' | 'failed'; + // (undocumented) + readonly verdict: 'pending' | 'needs_evidence' | 'verified' | 'rejected'; +} + +// @beta +type CompletionTaskKind = 'coding' | 'research' | 'device' | 'analysis'; + // Warning: (ae-missing-release-tag) "Confidence" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1039,6 +1163,28 @@ export function createCompactionSummaryMessage(summary: string, timestamp?: stri // @public (undocumented) export function createExecLikeFailureHintHook(isExecLike?: (toolName: string) => boolean): PostToolUseFailureHook; +// @beta +interface CreateExecutionGraphInput { + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeDefinition" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly nodes?: readonly ExecutionNodeDefinition[]; + // (undocumented) + readonly now?: number; + // Warning: (ae-forgotten-export) The symbol "OrchestrationPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly policy?: Partial; + // (undocumented) + readonly sessionId?: string; +} + // Warning: (ae-missing-release-tag) "createGoalCheckpointMessage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1149,6 +1295,20 @@ export interface CreateTaskRunInput { // @public (undocumented) export function createTimingHook(onTiming: (toolName: string, durationMs: number, isError: boolean) => void): PostToolUseHook; +// @beta +interface CreateWorkspaceLeaseInput { + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + // Warning: (ae-missing-release-tag) "CURRENT_SESSION_VERSION" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1229,6 +1389,14 @@ export enum ErrorCode { // (undocumented) DEVICE_SSH_FAILED = "DEVICE_SSH_FAILED", // (undocumented) + EXECUTION_LEASE_HELD = "EXECUTION_LEASE_HELD", + // (undocumented) + EXECUTION_REVISION_CONFLICT = "EXECUTION_REVISION_CONFLICT", + // (undocumented) + EXECUTION_STATE_INVALID = "EXECUTION_STATE_INVALID", + // (undocumented) + EXECUTION_STORE_FAILED = "EXECUTION_STORE_FAILED", + // (undocumented) INTERNAL_INVARIANT_VIOLATED = "INTERNAL_INVARIANT_VIOLATED", // (undocumented) MCP_CONNECTION_FAILED = "MCP_CONNECTION_FAILED", @@ -1300,11 +1468,273 @@ interface EvidenceTrustBoundary { // @public (undocumented) export function executeGoalCommand(agent: GoalCommandAgent, sessionKey: string, parsedCommand: ParsedGoalCommand, options?: GoalCommandOptions): Promise; +// @beta +interface ExecutionBudget { + // (undocumented) + readonly maxCostUsd?: number; + // (undocumented) + readonly maxTokens?: number; + // (undocumented) + readonly maxWallTimeMs?: number; + // (undocumented) + readonly usedCostUsd?: number; + // (undocumented) + readonly usedTokens?: number; + // (undocumented) + readonly usedWallTimeMs?: number; +} + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) type ExecutionDomain = 'local' | 'simulation' | 'real'; +// @beta +interface ExecutionEvent { + // (undocumented) + readonly data: Readonly>; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly seq: number; + // (undocumented) + readonly time: number; + // (undocumented) + readonly type: ExecutionEventType; +} + +// @beta +type ExecutionEventType = 'graph.created' | 'graph.ready' | 'graph.resumed' | 'graph.paused' | 'graph.recovered' | 'graph.blocked' | 'graph.completed' | 'graph.failed' | 'graph.cancelled' | 'plan.revised' | 'node.added' | 'node.ready' | 'node.leased' | 'node.started' | 'node.progressed' | 'node.succeeded' | 'node.failed' | 'node.interrupted' | 'node.retry_requested' | 'node.blocked' | 'node.skipped' | 'node.merge_conflict' | 'node.cancelled' | 'evidence.recorded' | 'budget.updated' | 'steering.recorded' | 'verification.recorded'; + +// @beta +interface ExecutionEvidence { + // (undocumented) + readonly artifactRef?: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest?: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidenceKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: ExecutionEvidenceKind; + // (undocumented) + readonly metadata?: Readonly>; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly summary: string; +} + +// @beta +type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; + +// @beta +interface ExecutionGraphSnapshot { + // (undocumented) + readonly budget: ExecutionBudget; + // (undocumented) + readonly createdAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly evidence: readonly ExecutionEvidence[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly nodes: Readonly>; + // (undocumented) + readonly policy: OrchestrationPolicy; + // Warning: (ae-forgotten-export) The symbol "ExecutionRecovery" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly recovery?: ExecutionRecovery; + // (undocumented) + readonly revision: number; + // (undocumented) + readonly sessionId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: ExecutionGraphStatus; + // (undocumented) + readonly updatedAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionVerification" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly verification?: ExecutionVerification; +} + +// @beta +type ExecutionGraphStatus = 'paused' | 'ready' | 'running' | 'paused_recovered' | 'blocked' | 'completed' | 'failed' | 'cancelled'; + +// @beta +interface ExecutionNode extends ExecutionNodeDefinition { + // (undocumented) + readonly attempts: number; + // (undocumented) + readonly blockedByDependencies?: readonly string[]; + // (undocumented) + readonly completedAt?: number; + // (undocumented) + readonly consecutiveSameFailures: number; + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly startedAt?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: ExecutionNodeStatus; + // (undocumented) + readonly workspaceLeaseId?: string; +} + +// @beta +interface ExecutionNodeDefinition { + // (undocumented) + readonly acceptanceCriteria?: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: ExecutionNodeKind; + // (undocumented) + readonly requiredCapabilities?: readonly string[]; + // (undocumented) + readonly roleId?: string; + // (undocumented) + readonly title: string; + // (undocumented) + readonly writePaths?: readonly string[]; +} + +// @beta +type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; + +// @beta +type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; + +// @beta +interface ExecutionOwnerLease { + // (undocumented) + readonly acquiredAt: number; + // (undocumented) + readonly expiresAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly token: string; +} + +// @beta +interface ExecutionRecovery { + // (undocumented) + readonly blockedMutationNodeIds: readonly string[]; + // (undocumented) + readonly interruptedNodeIds: readonly string[]; + // (undocumented) + readonly recoveredAt: number; + // (undocumented) + readonly requiresUserResume: boolean; +} + +// Warning: (ae-missing-release-tag) "ExecutionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface ExecutionState { + // (undocumented) + completedSteps: number; + // (undocumented) + currentStep: number; + // (undocumented) + isExecuting: boolean; + // (undocumented) + lastError?: string; + // (undocumented) + planId: string; + // (undocumented) + totalSteps: number; +} + +// @beta +interface ExecutionStore { + // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts + // + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts + // + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts + // + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvent" needs to be exported by the entry point index.d.ts + // + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +class ExecutionTaskController { + constructor(store: ExecutionStore); + // (undocumented) + inspect(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + resume(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + retry(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // (undocumented) + stop(graphId: string): ExecutionGraphSnapshot; +} + +// @beta +interface ExecutionVerification { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly verdict: 'verified' | 'rejected' | 'needs_evidence'; + // (undocumented) + readonly verifiedAt: number; +} + // Warning: (ae-forgotten-export) The symbol "EvidenceTrustBoundary" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExperienceEntry" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2391,6 +2821,10 @@ export class MossAgent { delivery?: SessionInboxDelivery; id?: string; }): SessionInboxEntry; + // Warning: (ae-forgotten-export) The symbol "AgentRoleRegistry" needs to be exported by the entry point index.d.ts + // + // @beta + readonly agentRoles: AgentRoleRegistry; // (undocumented) readonly asyncTasks: MossAsyncTaskRegistry; // (undocumented) @@ -2421,6 +2855,10 @@ export class MossAgent { }>; // (undocumented) completeGoal(sessionKey: string, reason?: string): Promise; + // Warning: (ae-forgotten-export) The symbol "CompletionArbiter" needs to be exported by the entry point index.d.ts + // + // @beta + readonly completionArbiter: CompletionArbiter; // (undocumented) readonly config: MossAgentConfig; // (undocumented) @@ -2430,6 +2868,8 @@ export class MossAgent { chats: ChatResult[]; drain: SessionDrainResult; }>; + // @beta + readonly executionStore: ExecutionStore; // Warning: (ae-forgotten-export) The symbol "PlatformExtensionRegistry" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2440,10 +2880,16 @@ export class MossAgent { getGoal(sessionKey: string): Promise; // (undocumented) inboxPending(sessionKey: string): readonly SessionInboxEntry[]; + // @beta + readonly orchestrationPolicy: OrchestrationPolicy; // (undocumented) pauseGoal(sessionKey: string, reason?: string): Promise; // (undocumented) readonly pendingToolAborts: PendingToolAbortStore; + // Warning: (ae-forgotten-export) The symbol "PlanControllerStore" needs to be exported by the entry point index.d.ts + // + // @beta + readonly planControllerStore: PlanControllerStore; // @beta (undocumented) readonly plugins: MossPluginHost; // Warning: (ae-forgotten-export) The symbol "ProjectedMessage" needs to be exported by the entry point index.d.ts @@ -2488,12 +2934,20 @@ export class MossAgent { streamChat(sessionKey: string, userMessage: string, options?: ChatOptions): AsyncGenerator; // (undocumented) streamChatRecorded(sessionKey: string, userMessage: string, options?: ChatOptions): AsyncGenerator; + // Warning: (ae-forgotten-export) The symbol "ExecutionTaskController" needs to be exported by the entry point index.d.ts + // + // @beta + readonly tasks: ExecutionTaskController; // (undocumented) readonly tools: ToolRegistry; // (undocumented) unregisterPostToolHook(name: string): boolean; // (undocumented) unregisterPreToolHook(name: string): boolean; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts + // + // @beta + readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; } // Warning: (ae-missing-release-tag) "MossAgentConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2509,6 +2963,12 @@ export type MossAgentConfig = MossAgentConfig_2; // // @public (undocumented) interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, ToolExecutionConfig, PromptConfig { + // @beta + agentRoleRegistry?: AgentRoleRegistry; + // @beta + agentRoles?: readonly AgentRoleDefinition[]; + // @beta + allowPluginIsolatedWrite?: boolean; asyncTaskRegistry?: MossAsyncTaskRegistry; bufferAssistantUntilComplete?: boolean; // (undocumented) @@ -2523,6 +2983,8 @@ interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, Too enableSteering?: boolean; // (undocumented) enableThinkingStream?: boolean; + // @beta + executionStore?: ExecutionStore; // (undocumented) followUpGuardConfig?: Partial; // (undocumented) @@ -2539,6 +3001,10 @@ interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, Too sessionKey: string; lastUserMessage: string; }) => Promise; + // @beta + orchestrationPolicy?: Partial; + // @beta + planControllerStore?: PlanControllerStore; // Warning: (ae-forgotten-export) The symbol "PromptCacheConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2571,6 +3037,8 @@ interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, Too subagentExperts?: readonly SubagentExpertDefinition[]; // (undocumented) workspaceDir?: string; + // @beta + workspaceLeaseAdapter?: WorkspaceLeaseAdapter; } // Warning: (ae-forgotten-export) The symbol "MossAgentEvent_2" needs to be exported by the entry point index.d.ts @@ -2748,6 +3216,8 @@ export interface MossPluginContext { // (undocumented) effect(setup: () => MossPluginDisposer | Promise, label?: string): void; // (undocumented) + registerAgentRole(role: AgentRoleDefinition): void; + // (undocumented) registerCommand(command: MossPluginCommand): void; // (undocumented) registerExpert(expert: SubagentExpertDefinition): void; @@ -2845,6 +3315,8 @@ export interface MossPluginProvider { // @beta export interface MossPluginSnapshot { + // (undocumented) + readonly agentRoles: readonly string[]; // (undocumented) readonly callState: MossPluginCallState; // (undocumented) @@ -2937,6 +3409,18 @@ export interface ObjectiveVerifierDeps { }; } +// @beta +interface OrchestrationPolicy { + // (undocumented) + readonly autoResumeReadonly?: boolean; + // (undocumented) + readonly maxAttemptsPerNode: number; + // (undocumented) + readonly maxConcurrency: number; + // (undocumented) + readonly strictCompletion: boolean; +} + // Warning: (ae-missing-release-tag) "OutputGuardrailDecision" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -3059,6 +3543,90 @@ interface Plan { // @public (undocumented) export function planContextBudgetActions(input: ContextBudgetPlannerInput): ContextBudgetPlan; +// @beta +class PlanControllerStore { + // (undocumented) + getActivePlanForSession(sessionKey: string): Plan | null; + // (undocumented) + getActivePlanId(sessionKey: string): string | undefined; + // Warning: (ae-forgotten-export) The symbol "PlanExecuteController" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getPlanController(sessionKey: string): PlanExecuteController; + // (undocumented) + getSharedPlanController(): PlanExecuteController; + // (undocumented) + reset(): void; + // (undocumented) + setActivePlanId(sessionKey: string, planId: string): void; +} + +// Warning: (ae-missing-release-tag) "PlanExecuteConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanExecuteConfig { + // (undocumented) + autoApproveSimple?: boolean; + // (undocumented) + maxExecutionTimeMs?: number; + // (undocumented) + maxReplans?: number; + // (undocumented) + requireApproval?: boolean; +} + +// Warning: (ae-missing-release-tag) "PlanExecuteController" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +class PlanExecuteController { + // Warning: (ae-forgotten-export) The symbol "PlanExecuteConfig" needs to be exported by the entry point index.d.ts + constructor(config?: PlanExecuteConfig); + // (undocumented) + approvePlan(planId: string): boolean; + // (undocumented) + cancelPlan(planId: string): boolean; + // (undocumented) + completeStep(planId: string, stepNumber: number, actualOutput?: string, actualTools?: string[]): boolean; + // (undocumented) + createPlan(goal: string, steps: Omit[], rationale?: string): Plan; + // (undocumented) + failStep(planId: string, stepNumber: number, error: string): boolean; + // (undocumented) + static formatPlan(plan: Plan): string; + // (undocumented) + getActivePlan(): Plan | null; + // Warning: (ae-forgotten-export) The symbol "ExecutionState" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getExecutionState(planId: string): ExecutionState | null; + // (undocumented) + getPlan(planId: string): Plan | null; + // (undocumented) + requestReplan(planId: string, _reason: string, revisedSteps?: Omit[]): Plan | null; + // Warning: (ae-forgotten-export) The symbol "PlanReviewResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + reviewPlan(planId: string): PlanReviewResult; + // (undocumented) + skipStep(planId: string, stepNumber: number, reason: string): boolean; + // (undocumented) + startExecution(planId: string): boolean; +} + +// Warning: (ae-missing-release-tag) "PlanReviewResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanReviewResult { + // (undocumented) + approved: boolean; + // (undocumented) + issues: string[]; + // (undocumented) + revisedPlan?: Plan; + // (undocumented) + suggestions: string[]; +} + // Warning: (ae-missing-release-tag) "PlanStatus" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -3095,6 +3663,7 @@ interface PlanStep { status: StepStatus; // (undocumented) step: number; + writePaths?: string[]; } // Warning: (ae-missing-release-tag) "PlatformExtensionRegistry" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -3611,6 +4180,19 @@ export interface RunMetrics { // @public (undocumented) export function runPreToolHookChain(toolName: string, input: Record, sessionKey: string): Promise; +// Warning: (ae-forgotten-export) The symbol "SemanticCompletionResult" needs to be exported by the entry point index.d.ts +// +// @beta +type SemanticCompletionJudge = (graph: ExecutionGraphSnapshot) => Promise; + +// @beta +interface SemanticCompletionResult { + // (undocumented) + readonly covered: boolean; + // (undocumented) + readonly reasons: readonly string[]; +} + // Warning: (ae-missing-release-tag) "SessionDrainResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -4469,7 +5051,7 @@ export type TaskRunEventType = 'run.created' | 'run.started' | 'tool.started' | // @beta export class TaskRunLedger { - constructor(filePath?: string | undefined); + constructor(filePath?: string | undefined, executionStore?: ExecutionStore | undefined); // (undocumented) append(runId: string, input: AppendTaskRunEventInput): TaskRunSnapshot; // (undocumented) @@ -4706,6 +5288,7 @@ export interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -4719,6 +5302,7 @@ export interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -4732,6 +5316,10 @@ export interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -5061,6 +5649,72 @@ type VerdictLevel = 'L1' | 'L2' | 'L3'; // @public type VerdictSource = 'exit_code' | 'file_exist' | 'geometric' | 'sensor' | 'model_judge'; +// @beta +interface WorkspaceLease { + // (undocumented) + readonly baselineHashes: Readonly>; + // (undocumented) + readonly baseRef: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: WorkspaceLeaseKind; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: WorkspaceLeaseStatus; + // (undocumented) + readonly updatedAt: number; + // (undocumented) + readonly workspacePath: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + +// @beta +interface WorkspaceLeaseAdapter { + // Warning: (ae-forgotten-export) The symbol "CreateWorkspaceLeaseInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WorkspaceLease" needs to be exported by the entry point index.d.ts + // + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // Warning: (ae-forgotten-export) The symbol "WorkspacePatch" needs to be exported by the entry point index.d.ts + // + // (undocumented) + createPatch(lease: WorkspaceLease): Promise; + // (undocumented) + list(): readonly WorkspaceLease[]; + // (undocumented) + load(leaseId: string): WorkspaceLease | undefined; + // Warning: (ae-forgotten-export) The symbol "WorkspaceMergeResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseReleaseReason" needs to be exported by the entry point index.d.ts + // + // (undocumented) + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; +} + +// @beta +type WorkspaceLeaseKind = 'git-worktree' | 'copy-snapshot'; + +// @beta +type WorkspaceLeaseReleaseReason = 'merged' | 'rejected' | 'cancelled'; + +// @beta +type WorkspaceLeaseStatus = 'active' | 'merged' | 'rejected' | 'cancelled'; + // Warning: (ae-missing-release-tag) "WorkspaceMemory" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -5098,6 +5752,33 @@ export interface WorkspaceMemoryContext { agentRulesSources?: string[]; } +// @beta +interface WorkspaceMergeResult { + // (undocumented) + readonly conflictingPaths: readonly string[]; + // (undocumented) + readonly patchId: string; + // (undocumented) + readonly status: 'merged' | 'merge_conflict'; +} + +// @beta +interface WorkspacePatch { + readonly artifactRef: string; + // (undocumented) + readonly changedPaths: readonly string[]; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly leaseId: string; + // (undocumented) + readonly patch: string; +} + // Warning: (ae-missing-release-tag) "wrapToolWithAbortSignal" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -5106,14 +5787,14 @@ export function wrapToolWithAbortSignal(tool: Tool, runSignal: AbortSignal // Warnings were encountered during analysis: // // src/context/pruning.ts:77:3 - (ae-forgotten-export) The symbol "ContextPruningToolMatch" needs to be exported by the entry point index.d.ts -// src/core/agent/moss-agent.ts:584:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts -// src/core/agent/moss-agent.ts:665:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts +// src/core/agent/moss-agent.ts:611:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts +// src/core/agent/moss-agent.ts:692:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts // src/core/llm/summarization-strategy.ts:37:7 - (ae-forgotten-export) The symbol "PruneResult" needs to be exported by the entry point index.d.ts // src/core/llm/summarization-strategy.ts:79:3 - (ae-forgotten-export) The symbol "SummarizeFn" needs to be exported by the entry point index.d.ts // src/core/loop/agent-loop-types.ts:148:5 - (ae-forgotten-export) The symbol "AgentLoopLlmUsage" needs to be exported by the entry point index.d.ts // src/core/tools/objective-verifier-hook.ts:65:22 - (ae-forgotten-export) The symbol "DeviceReadonlyExecutor" needs to be exported by the entry point index.d.ts // src/core/tools/objective-verifier-hook.ts:77:20 - (ae-forgotten-export) The symbol "Plan" needs to be exported by the entry point index.d.ts -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // src/provider/pi-ai-types.ts:133:50 - (ae-forgotten-export) The symbol "AssistantMessage" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/eval.api.md b/packages/moss-agent/etc/eval.api.md index 94bdfd0d..8985a958 100644 --- a/packages/moss-agent/etc/eval.api.md +++ b/packages/moss-agent/etc/eval.api.md @@ -600,6 +600,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -613,6 +614,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -626,6 +628,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -678,7 +684,7 @@ export const toolUsageMetric: MetricFn; // Warnings were encountered during analysis: // -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/mcp.api.md b/packages/moss-agent/etc/mcp.api.md index 0aa3f209..5f0955cf 100644 --- a/packages/moss-agent/etc/mcp.api.md +++ b/packages/moss-agent/etc/mcp.api.md @@ -247,6 +247,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -260,6 +261,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -273,6 +275,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -320,7 +326,7 @@ type ToolSideEffectClass = 'readonly' | 'local_write' | 'device_mutation' | 'cre // Warnings were encountered during analysis: // -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/memory.api.md b/packages/moss-agent/etc/memory.api.md index 85d7f430..4b763241 100644 --- a/packages/moss-agent/etc/memory.api.md +++ b/packages/moss-agent/etc/memory.api.md @@ -1072,6 +1072,7 @@ interface PlanStep { status: StepStatus; // (undocumented) step: number; + writePaths?: string[]; } // Warning: (ae-missing-release-tag) "PreparedPatchExperiment" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/packages/moss-agent/etc/mesh.api.md b/packages/moss-agent/etc/mesh.api.md index 7016fc52..b4e986d5 100644 --- a/packages/moss-agent/etc/mesh.api.md +++ b/packages/moss-agent/etc/mesh.api.md @@ -529,6 +529,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -542,6 +543,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -555,6 +557,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -602,7 +608,7 @@ type ToolSideEffectClass = 'readonly' | 'local_write' | 'device_mutation' | 'cre // Warnings were encountered during analysis: // -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // src/mesh/agent-mesh.ts:304:40 - (ae-forgotten-export) The symbol "HostAddressResolver" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/orchestration.api.md b/packages/moss-agent/etc/orchestration.api.md new file mode 100644 index 00000000..74a049d0 --- /dev/null +++ b/packages/moss-agent/etc/orchestration.api.md @@ -0,0 +1,848 @@ +## API Report File for "@rdk-moss/agent" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @beta +export interface AcquireExecutionLeaseInput { + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly ttlMs?: number; +} + +// @beta +export class AdaptiveWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { + constructor(options: WorkspaceLeaseAdapterOptions); + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // (undocumented) + createPatch(lease: WorkspaceLease): Promise; + // (undocumented) + list(): readonly WorkspaceLease[]; + // (undocumented) + load(leaseId: string): WorkspaceLease | undefined; + // (undocumented) + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // (undocumented) + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; +} + +// @beta +export type AgentCapability = 'architecture' | 'code' | 'test' | 'security' | 'performance' | 'documentation' | 'device' | 'research' | (string & {}); + +// @beta +export interface AgentClaim { + // (undocumented) + readonly conclusion: string; + // (undocumented) + readonly evidenceRefs: readonly string[]; + // (undocumented) + readonly id: string; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + +// @beta +export interface AgentResult { + // (undocumented) + readonly assignmentId: string; + // (undocumented) + readonly claims: readonly AgentClaim[]; + // (undocumented) + readonly evidenceRefs: readonly string[]; + // (undocumented) + readonly patchRef?: string; + // (undocumented) + readonly roleId: string; + // (undocumented) + readonly runId?: string; + // (undocumented) + readonly status: 'PASS' | 'FAIL' | 'PARTIAL'; + // (undocumented) + readonly unmetCriteria: readonly string[]; +} + +// @beta +export interface AgentResultConflict { + // (undocumented) + readonly claimIds: readonly string[]; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + +// @beta +export interface AgentRoleDefinition { + // (undocumented) + readonly allowedTools?: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly capabilities: readonly AgentCapability[]; + // (undocumented) + readonly displayName: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly instructions: string; + // (undocumented) + readonly kind: AgentRoleKind; + // (undocumented) + readonly model?: string; + // (undocumented) + readonly outputContract: 'structured-v1'; + // (undocumented) + readonly workspaceMode: 'shared-readonly' | 'isolated-write'; +} + +// @beta +export type AgentRoleKind = 'advisor' | 'implementer' | 'verifier'; + +// @beta +export class AgentRoleRegistry { + constructor(options?: AgentRoleRegistryOptions); + // (undocumented) + get(id: string): Readonly | undefined; + // (undocumented) + list(): readonly Readonly[]; + // (undocumented) + register(definition: AgentRoleDefinition): () => void; +} + +// @beta +export interface AgentRoleRegistryOptions { + // (undocumented) + readonly allowIsolatedWrite?: boolean; +} + +// @beta +export interface AgentSynthesisResult { + // (undocumented) + readonly acceptedEvidenceIds: readonly string[]; + // (undocumented) + readonly conflicts: readonly AgentResultConflict[]; + // (undocumented) + readonly coverage: number; + // (undocumented) + readonly missingCriteria: readonly string[]; + // (undocumented) + readonly verifierAssignments: readonly AssignmentSpec[]; +} + +// @beta +export interface AppendExecutionEventInput { + // (undocumented) + readonly data?: Readonly>; + // (undocumented) + readonly expectedRevision: number; + // (undocumented) + readonly id?: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly time?: number; + // (undocumented) + readonly type: Exclude; +} + +// @beta +export class AssignmentRouter { + constructor(registry: AgentRoleRegistry); + // (undocumented) + route(assignment: AssignmentSpec, preferredRoleId?: string): RoutedAssignment; +} + +// @beta +export interface AssignmentSpec { + // (undocumented) + readonly acceptanceCriteria: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly inputEvidenceIds: readonly string[]; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly requiredCapabilities: readonly AgentCapability[]; + // (undocumented) + readonly requiredRoleKind: AgentRoleKind; + // (undocumented) + readonly writePaths: readonly string[]; +} + +// Warning: (ae-incompatible-release-tags) The symbol "BaseWorkspaceLeaseAdapter" is marked as @public, but its signature references "WorkspaceLeaseAdapter" which is marked as @beta +// Warning: (ae-missing-release-tag) "BaseWorkspaceLeaseAdapter" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { + // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "WorkspaceLeaseKind" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "WorkspaceLeaseAdapterOptions" which is marked as @beta + protected constructor(kind: WorkspaceLeaseKind, options: WorkspaceLeaseAdapterOptions); + // (undocumented) + protected commitEnvironment(): Record; + // Warning: (ae-incompatible-release-tags) The symbol "create" is marked as @public, but its signature references "CreateWorkspaceLeaseInput" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "create" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + abstract create(input: CreateWorkspaceLeaseInput): Promise; + // Warning: (ae-incompatible-release-tags) The symbol "createManifest" is marked as @public, but its signature references "CreateWorkspaceLeaseInput" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "createManifest" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + protected createManifest(input: CreateWorkspaceLeaseInput, workspacePath: string, baseRef: string): WorkspaceLease; + // Warning: (ae-incompatible-release-tags) The symbol "createPatch" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "createPatch" is marked as @public, but its signature references "WorkspacePatch" which is marked as @beta + // + // (undocumented) + createPatch(lease: WorkspaceLease): Promise; + // (undocumented) + protected git(cwd: string, args: readonly string[], stdin?: string, extraEnv?: Readonly>): Promise<{ + stdout: string; + stderr: string; + }>; + // (undocumented) + protected leaseDirectory(leaseId: string): string; + // Warning: (ae-incompatible-release-tags) The symbol "list" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + list(): readonly WorkspaceLease[]; + // Warning: (ae-incompatible-release-tags) The symbol "load" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + load(leaseId: string): WorkspaceLease | undefined; + // Warning: (ae-incompatible-release-tags) The symbol "merge" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "merge" is marked as @public, but its signature references "WorkspacePatch" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "merge" is marked as @public, but its signature references "WorkspaceMergeResult" which is marked as @beta + // + // (undocumented) + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // (undocumented) + protected readonly now: () => number; + // Warning: (ae-incompatible-release-tags) The symbol "release" is marked as @public, but its signature references "WorkspaceLeaseReleaseReason" which is marked as @beta + // + // (undocumented) + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; + // Warning: (ae-incompatible-release-tags) The symbol "removeWorkspace" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + protected abstract removeWorkspace(lease: WorkspaceLease): Promise; + // (undocumented) + protected readonly rootDir: string; + // Warning: (ae-incompatible-release-tags) The symbol "writeManifest" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + protected writeManifest(lease: WorkspaceLease): void; +} + +// Warning: (ae-incompatible-release-tags) The symbol "cloneAgentRoleSnapshot" is marked as @public, but its signature references "AgentRoleDefinition" which is marked as @beta +// Warning: (ae-missing-release-tag) "cloneAgentRoleSnapshot" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function cloneAgentRoleSnapshot(definition: AgentRoleDefinition): Readonly; + +// @beta +export class CompletionArbiter { + constructor(store: ExecutionStore); + // (undocumented) + decide(graphId: string, input: CompletionArbiterInput): Promise; +} + +// @beta +export interface CompletionArbiterInput { + // (undocumented) + readonly activeBackgroundTaskIds?: readonly string[]; + // (undocumented) + readonly activeWorkspaceLeaseIds?: readonly string[]; + // (undocumented) + readonly semanticJudge?: SemanticCompletionJudge; + // (undocumented) + readonly taskKind: CompletionTaskKind; +} + +// @beta +export interface CompletionDecision { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly status: 'in_progress' | 'blocked' | 'completed' | 'failed'; + // (undocumented) + readonly verdict: 'pending' | 'needs_evidence' | 'verified' | 'rejected'; +} + +// @beta +export type CompletionTaskKind = 'coding' | 'research' | 'device' | 'analysis'; + +// Warning: (ae-forgotten-export) The symbol "BaseWorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts +// +// @beta +export class CopyWorkspaceLeaseAdapter extends BaseWorkspaceLeaseAdapter { + constructor(options: WorkspaceLeaseAdapterOptions); + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // (undocumented) + protected removeWorkspace(_lease: WorkspaceLease): Promise; +} + +// @beta +export interface CreateExecutionGraphInput { + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodes?: readonly ExecutionNodeDefinition[]; + // (undocumented) + readonly now?: number; + // (undocumented) + readonly policy?: Partial; + // (undocumented) + readonly sessionId?: string; +} + +// @beta +export interface CreateWorkspaceLeaseInput { + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + +// Warning: (ae-incompatible-release-tags) The symbol "DEFAULT_ORCHESTRATION_POLICY" is marked as @public, but its signature references "OrchestrationPolicy" which is marked as @beta +// Warning: (ae-missing-release-tag) "DEFAULT_ORCHESTRATION_POLICY" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const DEFAULT_ORCHESTRATION_POLICY: OrchestrationPolicy; + +// @beta +export interface ExecutionBudget { + // (undocumented) + readonly maxCostUsd?: number; + // (undocumented) + readonly maxTokens?: number; + // (undocumented) + readonly maxWallTimeMs?: number; + // (undocumented) + readonly usedCostUsd?: number; + // (undocumented) + readonly usedTokens?: number; + // (undocumented) + readonly usedWallTimeMs?: number; +} + +// @beta +export interface ExecutionEvent { + // (undocumented) + readonly data: Readonly>; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly seq: number; + // (undocumented) + readonly time: number; + // (undocumented) + readonly type: ExecutionEventType; +} + +// @beta +export type ExecutionEventType = 'graph.created' | 'graph.ready' | 'graph.resumed' | 'graph.paused' | 'graph.recovered' | 'graph.blocked' | 'graph.completed' | 'graph.failed' | 'graph.cancelled' | 'plan.revised' | 'node.added' | 'node.ready' | 'node.leased' | 'node.started' | 'node.progressed' | 'node.succeeded' | 'node.failed' | 'node.interrupted' | 'node.retry_requested' | 'node.blocked' | 'node.skipped' | 'node.merge_conflict' | 'node.cancelled' | 'evidence.recorded' | 'budget.updated' | 'steering.recorded' | 'verification.recorded'; + +// @beta +export interface ExecutionEvidence { + // (undocumented) + readonly artifactRef?: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest?: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly kind: ExecutionEvidenceKind; + // (undocumented) + readonly metadata?: Readonly>; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly summary: string; +} + +// @beta +export type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; + +// @beta +export class ExecutionGraphScheduler { + constructor(store: ExecutionStore); + // (undocumented) + failNode(graphId: string, nodeId: string, failure: { + readonly error: string; + readonly failureFingerprint?: string; + }): ExecutionGraphSnapshot; + // (undocumented) + reconcile(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + runAvailable(graphId: string, executor: ExecutionNodeExecutor): Promise; + // (undocumented) + selectRunnable(graph: ExecutionGraphSnapshot): readonly ExecutionNode[]; + // (undocumented) + startNode(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // (undocumented) + succeedNode(graphId: string, nodeId: string, evidence?: readonly ExecutionEvidence[]): ExecutionGraphSnapshot; +} + +// @beta +export interface ExecutionGraphSnapshot { + // (undocumented) + readonly budget: ExecutionBudget; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly evidence: readonly ExecutionEvidence[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodes: Readonly>; + // (undocumented) + readonly policy: OrchestrationPolicy; + // (undocumented) + readonly recovery?: ExecutionRecovery; + // (undocumented) + readonly revision: number; + // (undocumented) + readonly sessionId?: string; + // (undocumented) + readonly status: ExecutionGraphStatus; + // (undocumented) + readonly updatedAt: number; + // (undocumented) + readonly verification?: ExecutionVerification; +} + +// @beta +export type ExecutionGraphStatus = 'paused' | 'ready' | 'running' | 'paused_recovered' | 'blocked' | 'completed' | 'failed' | 'cancelled'; + +// @beta +export interface ExecutionNode extends ExecutionNodeDefinition { + // (undocumented) + readonly attempts: number; + // (undocumented) + readonly blockedByDependencies?: readonly string[]; + // (undocumented) + readonly completedAt?: number; + // (undocumented) + readonly consecutiveSameFailures: number; + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly startedAt?: number; + // (undocumented) + readonly status: ExecutionNodeStatus; + // (undocumented) + readonly workspaceLeaseId?: string; +} + +// @beta +export interface ExecutionNodeDefinition { + // (undocumented) + readonly acceptanceCriteria?: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly id: string; + // (undocumented) + readonly kind: ExecutionNodeKind; + // (undocumented) + readonly requiredCapabilities?: readonly string[]; + // (undocumented) + readonly roleId?: string; + // (undocumented) + readonly title: string; + // (undocumented) + readonly writePaths?: readonly string[]; +} + +// @beta +export type ExecutionNodeExecutor = (node: ExecutionNode, graph: ExecutionGraphSnapshot) => Promise; + +// @beta +export type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; + +// @beta +export interface ExecutionNodeRunResult { + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidence?: readonly ExecutionEvidence[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly success: boolean; +} + +// @beta +export type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; + +// @beta +export interface ExecutionOwnerLease { + // (undocumented) + readonly acquiredAt: number; + // (undocumented) + readonly expiresAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly token: string; +} + +// @beta +export interface ExecutionRecovery { + // (undocumented) + readonly blockedMutationNodeIds: readonly string[]; + // (undocumented) + readonly interruptedNodeIds: readonly string[]; + // (undocumented) + readonly recoveredAt: number; + // (undocumented) + readonly requiresUserResume: boolean; +} + +// @beta +export interface ExecutionScheduleResult { + // (undocumented) + readonly graph: ExecutionGraphSnapshot; + // (undocumented) + readonly startedNodeIds: readonly string[]; +} + +// @beta +export interface ExecutionStore { + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +export class ExecutionTaskController { + constructor(store: ExecutionStore); + // (undocumented) + inspect(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + resume(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + retry(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // (undocumented) + stop(graphId: string): ExecutionGraphSnapshot; +} + +// @beta +export interface ExecutionVerification { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly verdict: 'verified' | 'rejected' | 'needs_evidence'; + // (undocumented) + readonly verifiedAt: number; +} + +// @beta +export class GitWorktreeWorkspaceLeaseAdapter extends BaseWorkspaceLeaseAdapter { + constructor(options: WorkspaceLeaseAdapterOptions); + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // (undocumented) + protected removeWorkspace(lease: WorkspaceLease): Promise; +} + +// @beta +export class InMemoryExecutionStore implements ExecutionStore { + constructor(options?: InMemoryExecutionStoreOptions); + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +export interface InMemoryExecutionStoreOptions { + // (undocumented) + readonly now?: () => number; +} + +// @beta +export class JsonlExecutionStore implements ExecutionStore { + constructor(options: JsonlExecutionStoreOptions); + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +export interface JsonlExecutionStoreOptions { + // (undocumented) + readonly now?: () => number; + // (undocumented) + readonly rootDir: string; + // (undocumented) + readonly snapshotEvery?: number; +} + +// @beta +export class LegacyExecutionImporter { + constructor(store: ExecutionStore); + // (undocumented) + import(request: LegacyExecutionImportRequest): ExecutionGraphSnapshot; +} + +// @beta +export interface LegacyExecutionImportRequest { + // (undocumented) + readonly kind: LegacyExecutionKind; + // (undocumented) + readonly sourcePath: string; + // (undocumented) + readonly state: Readonly>; +} + +// @beta +export type LegacyExecutionKind = 'goal' | 'task-frame' | 'plan' | 'loop'; + +// @beta +export function normalizeWritePath(value: string): string; + +// @beta +export interface OrchestrationPolicy { + // (undocumented) + readonly autoResumeReadonly?: boolean; + // (undocumented) + readonly maxAttemptsPerNode: number; + // (undocumented) + readonly maxConcurrency: number; + // (undocumented) + readonly strictCompletion: boolean; +} + +// @beta +export function projectExecutionGraph(events: readonly ExecutionEvent[]): ExecutionGraphSnapshot; + +// @beta +export function recoverExecutionGraph(store: ExecutionStore, graphId: string, options?: RecoverExecutionGraphOptions): ExecutionGraphSnapshot; + +// @beta +export interface RecoverExecutionGraphOptions { + // (undocumented) + readonly now?: number; +} + +// @beta +export interface RoutedAssignment { + // (undocumented) + readonly assignment: AssignmentSpec; + // (undocumented) + readonly role: Readonly; +} + +// @beta +export type SemanticCompletionJudge = (graph: ExecutionGraphSnapshot) => Promise; + +// @beta +export interface SemanticCompletionResult { + // (undocumented) + readonly covered: boolean; + // (undocumented) + readonly reasons: readonly string[]; +} + +// @beta +export function synthesizeAgentResults(input: { + readonly assignments: readonly AssignmentSpec[]; + readonly results: readonly AgentResult[]; +}): AgentSynthesisResult; + +// @beta +export interface WorkspaceLease { + // (undocumented) + readonly baselineHashes: Readonly>; + // (undocumented) + readonly baseRef: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly kind: WorkspaceLeaseKind; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // (undocumented) + readonly status: WorkspaceLeaseStatus; + // (undocumented) + readonly updatedAt: number; + // (undocumented) + readonly workspacePath: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + +// @beta +export interface WorkspaceLeaseAdapter { + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // (undocumented) + createPatch(lease: WorkspaceLease): Promise; + // (undocumented) + list(): readonly WorkspaceLease[]; + // (undocumented) + load(leaseId: string): WorkspaceLease | undefined; + // (undocumented) + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // (undocumented) + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; +} + +// @beta +export interface WorkspaceLeaseAdapterOptions { + // (undocumented) + readonly authorizeMerge?: (request: WorkspaceMergeAuthorizationRequest) => void | Promise; + // (undocumented) + readonly now?: () => number; + // (undocumented) + readonly rootDir: string; +} + +// @beta +export type WorkspaceLeaseKind = 'git-worktree' | 'copy-snapshot'; + +// @beta +export type WorkspaceLeaseReleaseReason = 'merged' | 'rejected' | 'cancelled'; + +// @beta +export type WorkspaceLeaseStatus = 'active' | 'merged' | 'rejected' | 'cancelled'; + +// @beta +export interface WorkspaceMergeAuthorizationRequest { + // (undocumented) + readonly changedPaths: readonly string[]; + // (undocumented) + readonly digest: string; + // (undocumented) + readonly lease: WorkspaceLease; + // (undocumented) + readonly patchId: string; +} + +// @beta +export interface WorkspaceMergeResult { + // (undocumented) + readonly conflictingPaths: readonly string[]; + // (undocumented) + readonly patchId: string; + // (undocumented) + readonly status: 'merged' | 'merge_conflict'; +} + +// @beta +export interface WorkspacePatch { + readonly artifactRef: string; + // (undocumented) + readonly changedPaths: readonly string[]; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly leaseId: string; + // (undocumented) + readonly patch: string; +} + +// @beta +export function writePathsOverlap(leftPaths: readonly string[], rightPaths: readonly string[]): boolean; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/moss-agent/etc/plan-execute.api.md b/packages/moss-agent/etc/plan-execute.api.md index 5e3d597b..d5af16d1 100644 --- a/packages/moss-agent/etc/plan-execute.api.md +++ b/packages/moss-agent/etc/plan-execute.api.md @@ -22,21 +22,74 @@ interface AcceptSpec { safetyCritical?: boolean; } +// @beta +interface AcquireExecutionLeaseInput { + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly ttlMs?: number; +} + +// @beta +interface AppendExecutionEventInput { + // (undocumented) + readonly data?: Readonly>; + // (undocumented) + readonly expectedRevision: number; + // (undocumented) + readonly id?: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly time?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly type: Exclude; +} + // Warning: (ae-missing-release-tag) "buildPlanExecuteSystemPrompt" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function buildPlanExecuteSystemPrompt(options?: PlanExecutePromptOptions): string; +// @beta +interface CreateExecutionGraphInput { + // Warning: (ae-forgotten-export) The symbol "ExecutionBudget" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeDefinition" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly nodes?: readonly ExecutionNodeDefinition[]; + // (undocumented) + readonly now?: number; + // Warning: (ae-forgotten-export) The symbol "OrchestrationPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly policy?: Partial; + // (undocumented) + readonly sessionId?: string; +} + +// Warning: (ae-forgotten-export) The symbol "ExecutionStore" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Tool" needs to be exported by the entry point index.d.ts +// Warning: (ae-incompatible-release-tags) The symbol "createPlanStepTool" is marked as @public, but its signature references "PlanControllerStore" which is marked as @beta // Warning: (ae-missing-release-tag) "createPlanStepTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function createPlanStepTool(): Tool; +export function createPlanStepTool(store?: PlanControllerStore, executionStore?: ExecutionStore): Tool; +// Warning: (ae-incompatible-release-tags) The symbol "createPlanTool" is marked as @public, but its signature references "PlanControllerStore" which is marked as @beta // Warning: (ae-missing-release-tag) "createPlanTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function createPlanTool(): Tool; +export function createPlanTool(store?: PlanControllerStore, executionStore?: ExecutionStore): Tool; // Warning: (ae-missing-release-tag) "criticEnabled" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -83,11 +136,198 @@ export type CritiqueResult = { // @public (undocumented) export function evaluatePlanCompletionGate(request: PlanCompletionGateRequest, deps: PlanCompletionGateDeps): PlanCompletionGateResult; +// @beta +interface ExecutionBudget { + // (undocumented) + readonly maxCostUsd?: number; + // (undocumented) + readonly maxTokens?: number; + // (undocumented) + readonly maxWallTimeMs?: number; + // (undocumented) + readonly usedCostUsd?: number; + // (undocumented) + readonly usedTokens?: number; + // (undocumented) + readonly usedWallTimeMs?: number; +} + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) type ExecutionDomain = 'local' | 'simulation' | 'real'; +// @beta +interface ExecutionEvent { + // (undocumented) + readonly data: Readonly>; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly seq: number; + // (undocumented) + readonly time: number; + // (undocumented) + readonly type: ExecutionEventType; +} + +// @beta +type ExecutionEventType = 'graph.created' | 'graph.ready' | 'graph.resumed' | 'graph.paused' | 'graph.recovered' | 'graph.blocked' | 'graph.completed' | 'graph.failed' | 'graph.cancelled' | 'plan.revised' | 'node.added' | 'node.ready' | 'node.leased' | 'node.started' | 'node.progressed' | 'node.succeeded' | 'node.failed' | 'node.interrupted' | 'node.retry_requested' | 'node.blocked' | 'node.skipped' | 'node.merge_conflict' | 'node.cancelled' | 'evidence.recorded' | 'budget.updated' | 'steering.recorded' | 'verification.recorded'; + +// @beta +interface ExecutionEvidence { + // (undocumented) + readonly artifactRef?: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest?: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidenceKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: ExecutionEvidenceKind; + // (undocumented) + readonly metadata?: Readonly>; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly summary: string; +} + +// @beta +type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; + +// @beta +interface ExecutionGraphSnapshot { + // (undocumented) + readonly budget: ExecutionBudget; + // (undocumented) + readonly createdAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly evidence: readonly ExecutionEvidence[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly nodes: Readonly>; + // (undocumented) + readonly policy: OrchestrationPolicy; + // Warning: (ae-forgotten-export) The symbol "ExecutionRecovery" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly recovery?: ExecutionRecovery; + // (undocumented) + readonly revision: number; + // (undocumented) + readonly sessionId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: ExecutionGraphStatus; + // (undocumented) + readonly updatedAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionVerification" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly verification?: ExecutionVerification; +} + +// @beta +type ExecutionGraphStatus = 'paused' | 'ready' | 'running' | 'paused_recovered' | 'blocked' | 'completed' | 'failed' | 'cancelled'; + +// @beta +interface ExecutionNode extends ExecutionNodeDefinition { + // (undocumented) + readonly attempts: number; + // (undocumented) + readonly blockedByDependencies?: readonly string[]; + // (undocumented) + readonly completedAt?: number; + // (undocumented) + readonly consecutiveSameFailures: number; + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly startedAt?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: ExecutionNodeStatus; + // (undocumented) + readonly workspaceLeaseId?: string; +} + +// @beta +interface ExecutionNodeDefinition { + // (undocumented) + readonly acceptanceCriteria?: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: ExecutionNodeKind; + // (undocumented) + readonly requiredCapabilities?: readonly string[]; + // (undocumented) + readonly roleId?: string; + // (undocumented) + readonly title: string; + // (undocumented) + readonly writePaths?: readonly string[]; +} + +// @beta +type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; + +// @beta +type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; + +// @beta +interface ExecutionOwnerLease { + // (undocumented) + readonly acquiredAt: number; + // (undocumented) + readonly expiresAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly token: string; +} + +// @beta +interface ExecutionRecovery { + // (undocumented) + readonly blockedMutationNodeIds: readonly string[]; + // (undocumented) + readonly interruptedNodeIds: readonly string[]; + // (undocumented) + readonly recoveredAt: number; + // (undocumented) + readonly requiresUserResume: boolean; +} + // Warning: (ae-missing-release-tag) "ExecutionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -106,6 +346,48 @@ export interface ExecutionState { totalSteps: number; } +// @beta +interface ExecutionStore { + // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts + // + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts + // + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts + // + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvent" needs to be exported by the entry point index.d.ts + // + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +interface ExecutionVerification { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly verdict: 'verified' | 'rejected' | 'needs_evidence'; + // (undocumented) + readonly verifiedAt: number; +} + // Warning: (ae-missing-release-tag) "formatCritiqueForModel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -131,6 +413,18 @@ export function getPlanController(sessionKey: string): PlanExecuteController; // @public (undocumented) export function getSharedPlanController(): PlanExecuteController; +// @beta +interface OrchestrationPolicy { + // (undocumented) + readonly autoResumeReadonly?: boolean; + // (undocumented) + readonly maxAttemptsPerNode: number; + // (undocumented) + readonly maxConcurrency: number; + // (undocumented) + readonly strictCompletion: boolean; +} + // Warning: (ae-missing-release-tag) "Plan" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -198,6 +492,22 @@ export type PlanCompletionGateResult = { retryLimit: number; }; +// @beta +export class PlanControllerStore { + // (undocumented) + getActivePlanForSession(sessionKey: string): Plan | null; + // (undocumented) + getActivePlanId(sessionKey: string): string | undefined; + // (undocumented) + getPlanController(sessionKey: string): PlanExecuteController; + // (undocumented) + getSharedPlanController(): PlanExecuteController; + // (undocumented) + reset(): void; + // (undocumented) + setActivePlanId(sessionKey: string, planId: string): void; +} + // Warning: (ae-missing-release-tag) "PlanExecuteConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -306,6 +616,7 @@ export interface PlanStep { status: StepStatus; // (undocumented) step: number; + writePaths?: string[]; } // Warning: (ae-missing-release-tag) "planStepTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -360,6 +671,7 @@ export interface PlanToolInput { expectedAccept?: string[]; dependsOn?: number[]; estimatedTimeSec?: number; + writePaths?: string[]; }>; // (undocumented) successCriteria?: string[]; @@ -537,6 +849,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -550,6 +863,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -563,6 +877,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -610,7 +928,7 @@ type ToolSideEffectClass = 'readonly' | 'local_write' | 'device_mutation' | 'cre // Warnings were encountered during analysis: // -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/root.api.md b/packages/moss-agent/etc/root.api.md index cdd98f22..5530395e 100644 --- a/packages/moss-agent/etc/root.api.md +++ b/packages/moss-agent/etc/root.api.md @@ -35,6 +35,48 @@ interface AcceptSpec { safetyCritical?: boolean; } +// @beta +export interface AcquireExecutionLeaseInput { + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly ttlMs?: number; +} + +// @beta +export class AdaptiveWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { + constructor(options: WorkspaceLeaseAdapterOptions); + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // (undocumented) + createPatch(lease: WorkspaceLease): Promise; + // (undocumented) + list(): readonly WorkspaceLease[]; + // (undocumented) + load(leaseId: string): WorkspaceLease | undefined; + // (undocumented) + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // (undocumented) + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; +} + +// @beta +export type AgentCapability = 'architecture' | 'code' | 'test' | 'security' | 'performance' | 'documentation' | 'device' | 'research' | (string & {}); + +// @beta +export interface AgentClaim { + // (undocumented) + readonly conclusion: string; + // (undocumented) + readonly evidenceRefs: readonly string[]; + // (undocumented) + readonly id: string; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + // Warning: (ae-missing-release-tag) "AgentHooks" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -192,6 +234,82 @@ export class AgentMesh { stop(): Promise; } +// @beta +export interface AgentResult { + // (undocumented) + readonly assignmentId: string; + // (undocumented) + readonly claims: readonly AgentClaim[]; + // (undocumented) + readonly evidenceRefs: readonly string[]; + // (undocumented) + readonly patchRef?: string; + // (undocumented) + readonly roleId: string; + // (undocumented) + readonly runId?: string; + // (undocumented) + readonly status: 'PASS' | 'FAIL' | 'PARTIAL'; + // (undocumented) + readonly unmetCriteria: readonly string[]; +} + +// @beta +export interface AgentResultConflict { + // (undocumented) + readonly claimIds: readonly string[]; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + +// @beta +export interface AgentRoleDefinition { + // (undocumented) + readonly allowedTools?: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly capabilities: readonly AgentCapability[]; + // (undocumented) + readonly displayName: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly instructions: string; + // (undocumented) + readonly kind: AgentRoleKind; + // (undocumented) + readonly model?: string; + // (undocumented) + readonly outputContract: 'structured-v1'; + // (undocumented) + readonly workspaceMode: 'shared-readonly' | 'isolated-write'; +} + +// @beta +export type AgentRoleKind = 'advisor' | 'implementer' | 'verifier'; + +// @beta +export class AgentRoleRegistry { + constructor(options?: AgentRoleRegistryOptions); + // (undocumented) + get(id: string): Readonly | undefined; + // (undocumented) + list(): readonly Readonly[]; + // (undocumented) + register(definition: AgentRoleDefinition): () => void; +} + +// @beta +export interface AgentRoleRegistryOptions { + // (undocumented) + readonly allowIsolatedWrite?: boolean; +} + // Warning: (ae-missing-release-tag) "AgentRuntimeConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -207,6 +325,20 @@ interface AgentRuntimeConfig { maxTurns?: number; } +// @beta +export interface AgentSynthesisResult { + // (undocumented) + readonly acceptedEvidenceIds: readonly string[]; + // (undocumented) + readonly conflicts: readonly AgentResultConflict[]; + // (undocumented) + readonly coverage: number; + // (undocumented) + readonly missingCriteria: readonly string[]; + // (undocumented) + readonly verifierAssignments: readonly AssignmentSpec[]; +} + // Warning: (ae-missing-release-tag) "AnthropicLLMProvider" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -238,6 +370,22 @@ export interface AnthropicLLMProviderConfig { defaultModel?: string; } +// @beta +export interface AppendExecutionEventInput { + // (undocumented) + readonly data?: Readonly>; + // (undocumented) + readonly expectedRevision: number; + // (undocumented) + readonly id?: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly time?: number; + // (undocumented) + readonly type: Exclude; +} + // @beta export interface AppendTaskRunEventInput { // (undocumented) @@ -361,6 +509,39 @@ interface ApprovedPreflightStopDecision { // @public (undocumented) type ApprovedPreflightStopResult = 'cancel_requested' | 'queued' | 'already_cancelled' | 'already_terminal' | 'not_found'; +// @beta +export class AssignmentRouter { + constructor(registry: AgentRoleRegistry); + // (undocumented) + route(assignment: AssignmentSpec, preferredRoleId?: string): RoutedAssignment; +} + +// @beta +export interface AssignmentSpec { + // (undocumented) + readonly acceptanceCriteria: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly inputEvidenceIds: readonly string[]; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly requiredCapabilities: readonly AgentCapability[]; + // (undocumented) + readonly requiredRoleKind: AgentRoleKind; + // (undocumented) + readonly writePaths: readonly string[]; +} + // Warning: (ae-missing-release-tag) "assignPatchExperimentVariant" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -531,6 +712,70 @@ export interface BackgroundProcSnapshot { // @public (undocumented) type BackgroundStatus = 'running' | 'exited' | 'killed' | 'error'; +// Warning: (ae-incompatible-release-tags) The symbol "BaseWorkspaceLeaseAdapter" is marked as @public, but its signature references "WorkspaceLeaseAdapter" which is marked as @beta +// Warning: (ae-missing-release-tag) "BaseWorkspaceLeaseAdapter" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { + // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "WorkspaceLeaseKind" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "__constructor" is marked as @public, but its signature references "WorkspaceLeaseAdapterOptions" which is marked as @beta + protected constructor(kind: WorkspaceLeaseKind, options: WorkspaceLeaseAdapterOptions); + // (undocumented) + protected commitEnvironment(): Record; + // Warning: (ae-incompatible-release-tags) The symbol "create" is marked as @public, but its signature references "CreateWorkspaceLeaseInput" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "create" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + abstract create(input: CreateWorkspaceLeaseInput): Promise; + // Warning: (ae-incompatible-release-tags) The symbol "createManifest" is marked as @public, but its signature references "CreateWorkspaceLeaseInput" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "createManifest" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + protected createManifest(input: CreateWorkspaceLeaseInput, workspacePath: string, baseRef: string): WorkspaceLease; + // Warning: (ae-incompatible-release-tags) The symbol "createPatch" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "createPatch" is marked as @public, but its signature references "WorkspacePatch" which is marked as @beta + // + // (undocumented) + createPatch(lease: WorkspaceLease): Promise; + // (undocumented) + protected git(cwd: string, args: readonly string[], stdin?: string, extraEnv?: Readonly>): Promise<{ + stdout: string; + stderr: string; + }>; + // (undocumented) + protected leaseDirectory(leaseId: string): string; + // Warning: (ae-incompatible-release-tags) The symbol "list" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + list(): readonly WorkspaceLease[]; + // Warning: (ae-incompatible-release-tags) The symbol "load" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + load(leaseId: string): WorkspaceLease | undefined; + // Warning: (ae-incompatible-release-tags) The symbol "merge" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "merge" is marked as @public, but its signature references "WorkspacePatch" which is marked as @beta + // Warning: (ae-incompatible-release-tags) The symbol "merge" is marked as @public, but its signature references "WorkspaceMergeResult" which is marked as @beta + // + // (undocumented) + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // (undocumented) + protected readonly now: () => number; + // Warning: (ae-incompatible-release-tags) The symbol "release" is marked as @public, but its signature references "WorkspaceLeaseReleaseReason" which is marked as @beta + // + // (undocumented) + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; + // Warning: (ae-incompatible-release-tags) The symbol "removeWorkspace" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + protected abstract removeWorkspace(lease: WorkspaceLease): Promise; + // (undocumented) + protected readonly rootDir: string; + // Warning: (ae-incompatible-release-tags) The symbol "writeManifest" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta + // + // (undocumented) + protected writeManifest(lease: WorkspaceLease): void; +} + // @beta export function bingSearch(query: string, opts: WebSearchBackendOptions): Promise; @@ -1326,6 +1571,40 @@ export function compileRecoveryRecipe(input: { previous?: RecoveryRecipe; }): RecoveryRecipe | null; +// @beta +export class CompletionArbiter { + constructor(store: ExecutionStore); + // (undocumented) + decide(graphId: string, input: CompletionArbiterInput): Promise; +} + +// @beta +export interface CompletionArbiterInput { + // (undocumented) + readonly activeBackgroundTaskIds?: readonly string[]; + // (undocumented) + readonly activeWorkspaceLeaseIds?: readonly string[]; + // (undocumented) + readonly semanticJudge?: SemanticCompletionJudge; + // (undocumented) + readonly taskKind: CompletionTaskKind; +} + +// @beta +export interface CompletionDecision { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly status: 'in_progress' | 'blocked' | 'completed' | 'failed'; + // (undocumented) + readonly verdict: 'pending' | 'needs_evidence' | 'verified' | 'rejected'; +} + +// @beta +export type CompletionTaskKind = 'coding' | 'research' | 'device' | 'analysis'; + // Warning: (ae-missing-release-tag) "ComposedSkillContext" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1680,6 +1959,17 @@ export function convertMessagesToPi(messages: Message[], modelInfo: { reasoning?: unknown; }): Message_2[]; +// Warning: (ae-forgotten-export) The symbol "BaseWorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts +// +// @beta +export class CopyWorkspaceLeaseAdapter extends BaseWorkspaceLeaseAdapter { + constructor(options: WorkspaceLeaseAdapterOptions); + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // (undocumented) + protected removeWorkspace(_lease: WorkspaceLease): Promise; +} + // Warning: (ae-missing-release-tag) "createBraveSearch" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -1712,6 +2002,24 @@ export function createDefaultVisionRegistry(): VisionRegistry; // @public (undocumented) export function createEvalTool(): Tool; +// @beta +export interface CreateExecutionGraphInput { + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodes?: readonly ExecutionNodeDefinition[]; + // (undocumented) + readonly now?: number; + // (undocumented) + readonly policy?: Partial; + // (undocumented) + readonly sessionId?: string; +} + // Warning: (ae-missing-release-tag) "createInlineThinkingRouter" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1780,15 +2088,19 @@ export function createPatchExperimentTaskSignature(input: { plan?: Plan | null; }): string; +// Warning: (ae-incompatible-release-tags) The symbol "createPlanStepTool" is marked as @public, but its signature references "PlanControllerStore" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "createPlanStepTool" is marked as @public, but its signature references "ExecutionStore" which is marked as @beta // Warning: (ae-missing-release-tag) "createPlanStepTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function createPlanStepTool(): Tool; +export function createPlanStepTool(store?: PlanControllerStore, executionStore?: ExecutionStore): Tool; +// Warning: (ae-incompatible-release-tags) The symbol "createPlanTool" is marked as @public, but its signature references "PlanControllerStore" which is marked as @beta +// Warning: (ae-incompatible-release-tags) The symbol "createPlanTool" is marked as @public, but its signature references "ExecutionStore" which is marked as @beta // Warning: (ae-missing-release-tag) "createPlanTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function createPlanTool(): Tool; +export function createPlanTool(store?: PlanControllerStore, executionStore?: ExecutionStore): Tool; // Warning: (ae-forgotten-export) The symbol "DeviceSshConfig" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "createRos1Tools" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1860,6 +2172,20 @@ export function createWebSearchTool(opts?: WebSearchOptions): Tool<{ blocked_domains?: string[]; }>; +// @beta +export interface CreateWorkspaceLeaseInput { + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + // Warning: (ae-missing-release-tag) "CustomModelConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1885,6 +2211,12 @@ type CustomModelConfigParseResult = { message: string; }; +// Warning: (ae-incompatible-release-tags) The symbol "DEFAULT_ORCHESTRATION_POLICY" is marked as @public, but its signature references "OrchestrationPolicy" which is marked as @beta +// Warning: (ae-missing-release-tag) "DEFAULT_ORCHESTRATION_POLICY" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const DEFAULT_ORCHESTRATION_POLICY: OrchestrationPolicy; + // Warning: (ae-missing-release-tag) "DEFAULT_PATCH_EXPERIMENT_THRESHOLDS" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2137,6 +2469,14 @@ export enum ErrorCode { // (undocumented) DEVICE_SSH_FAILED = "DEVICE_SSH_FAILED", // (undocumented) + EXECUTION_LEASE_HELD = "EXECUTION_LEASE_HELD", + // (undocumented) + EXECUTION_REVISION_CONFLICT = "EXECUTION_REVISION_CONFLICT", + // (undocumented) + EXECUTION_STATE_INVALID = "EXECUTION_STATE_INVALID", + // (undocumented) + EXECUTION_STORE_FAILED = "EXECUTION_STORE_FAILED", + // (undocumented) INTERNAL_INVARIANT_VIOLATED = "INTERNAL_INVARIANT_VIOLATED", // (undocumented) MCP_CONNECTION_FAILED = "MCP_CONNECTION_FAILED", @@ -2408,45 +2748,259 @@ interface EvidenceTrustBoundary { // Warning: (ae-forgotten-export) The symbol "ExecutionDomain" needs to be exported by the entry point index.d.ts // // (undocumented) - executionDomain?: ExecutionDomain; + executionDomain?: ExecutionDomain; + // (undocumented) + realEvidenceEligible?: boolean; +} + +// Warning: (ae-missing-release-tag) "exactMatchMetric" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const exactMatchMetric: MetricFn; + +// Warning: (ae-missing-release-tag) "execBackgroundTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const execBackgroundTool: Tool; + +// Warning: (ae-missing-release-tag) "execLogsTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const execLogsTool: Tool; + +// Warning: (ae-missing-release-tag) "execStopTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const execStopTool: Tool; + +// Warning: (ae-missing-release-tag) "execTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const execTool: Tool; + +// Warning: (ae-missing-release-tag) "executeGoalCommand" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function executeGoalCommand(agent: GoalCommandAgent, sessionKey: string, parsedCommand: ParsedGoalCommand, options?: GoalCommandOptions): Promise; + +// @beta +export interface ExecutionBudget { + // (undocumented) + readonly maxCostUsd?: number; + // (undocumented) + readonly maxTokens?: number; + // (undocumented) + readonly maxWallTimeMs?: number; + // (undocumented) + readonly usedCostUsd?: number; + // (undocumented) + readonly usedTokens?: number; + // (undocumented) + readonly usedWallTimeMs?: number; +} + +// Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +type ExecutionDomain = 'local' | 'simulation' | 'real'; + +// @beta +export interface ExecutionEvent { + // (undocumented) + readonly data: Readonly>; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly seq: number; + // (undocumented) + readonly time: number; + // (undocumented) + readonly type: ExecutionEventType; +} + +// @beta +export type ExecutionEventType = 'graph.created' | 'graph.ready' | 'graph.resumed' | 'graph.paused' | 'graph.recovered' | 'graph.blocked' | 'graph.completed' | 'graph.failed' | 'graph.cancelled' | 'plan.revised' | 'node.added' | 'node.ready' | 'node.leased' | 'node.started' | 'node.progressed' | 'node.succeeded' | 'node.failed' | 'node.interrupted' | 'node.retry_requested' | 'node.blocked' | 'node.skipped' | 'node.merge_conflict' | 'node.cancelled' | 'evidence.recorded' | 'budget.updated' | 'steering.recorded' | 'verification.recorded'; + +// @beta +export interface ExecutionEvidence { + // (undocumented) + readonly artifactRef?: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest?: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly kind: ExecutionEvidenceKind; + // (undocumented) + readonly metadata?: Readonly>; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly summary: string; +} + +// @beta +export type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; + +// @beta +export class ExecutionGraphScheduler { + constructor(store: ExecutionStore); + // (undocumented) + failNode(graphId: string, nodeId: string, failure: { + readonly error: string; + readonly failureFingerprint?: string; + }): ExecutionGraphSnapshot; + // (undocumented) + reconcile(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + runAvailable(graphId: string, executor: ExecutionNodeExecutor): Promise; + // (undocumented) + selectRunnable(graph: ExecutionGraphSnapshot): readonly ExecutionNode[]; + // (undocumented) + startNode(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // (undocumented) + succeedNode(graphId: string, nodeId: string, evidence?: readonly ExecutionEvidence[]): ExecutionGraphSnapshot; +} + +// @beta +export interface ExecutionGraphSnapshot { + // (undocumented) + readonly budget: ExecutionBudget; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly evidence: readonly ExecutionEvidence[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodes: Readonly>; + // (undocumented) + readonly policy: OrchestrationPolicy; + // (undocumented) + readonly recovery?: ExecutionRecovery; + // (undocumented) + readonly revision: number; + // (undocumented) + readonly sessionId?: string; + // (undocumented) + readonly status: ExecutionGraphStatus; + // (undocumented) + readonly updatedAt: number; + // (undocumented) + readonly verification?: ExecutionVerification; +} + +// @beta +export type ExecutionGraphStatus = 'paused' | 'ready' | 'running' | 'paused_recovered' | 'blocked' | 'completed' | 'failed' | 'cancelled'; + +// @beta +export interface ExecutionNode extends ExecutionNodeDefinition { + // (undocumented) + readonly attempts: number; + // (undocumented) + readonly blockedByDependencies?: readonly string[]; + // (undocumented) + readonly completedAt?: number; + // (undocumented) + readonly consecutiveSameFailures: number; + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly startedAt?: number; + // (undocumented) + readonly status: ExecutionNodeStatus; + // (undocumented) + readonly workspaceLeaseId?: string; +} + +// @beta +export interface ExecutionNodeDefinition { + // (undocumented) + readonly acceptanceCriteria?: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly id: string; + // (undocumented) + readonly kind: ExecutionNodeKind; + // (undocumented) + readonly requiredCapabilities?: readonly string[]; // (undocumented) - realEvidenceEligible?: boolean; + readonly roleId?: string; + // (undocumented) + readonly title: string; + // (undocumented) + readonly writePaths?: readonly string[]; } -// Warning: (ae-missing-release-tag) "exactMatchMetric" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const exactMatchMetric: MetricFn; +// @beta +export type ExecutionNodeExecutor = (node: ExecutionNode, graph: ExecutionGraphSnapshot) => Promise; -// Warning: (ae-missing-release-tag) "execBackgroundTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const execBackgroundTool: Tool; +// @beta +export type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; -// Warning: (ae-missing-release-tag) "execLogsTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const execLogsTool: Tool; +// @beta +export interface ExecutionNodeRunResult { + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidence?: readonly ExecutionEvidence[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly success: boolean; +} -// Warning: (ae-missing-release-tag) "execStopTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const execStopTool: Tool; +// @beta +export type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; -// Warning: (ae-missing-release-tag) "execTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const execTool: Tool; +// @beta +export interface ExecutionOwnerLease { + // (undocumented) + readonly acquiredAt: number; + // (undocumented) + readonly expiresAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly token: string; +} -// Warning: (ae-missing-release-tag) "executeGoalCommand" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export function executeGoalCommand(agent: GoalCommandAgent, sessionKey: string, parsedCommand: ParsedGoalCommand, options?: GoalCommandOptions): Promise; +// @beta +export interface ExecutionRecovery { + // (undocumented) + readonly blockedMutationNodeIds: readonly string[]; + // (undocumented) + readonly interruptedNodeIds: readonly string[]; + // (undocumented) + readonly recoveredAt: number; + // (undocumented) + readonly requiresUserResume: boolean; +} -// Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -type ExecutionDomain = 'local' | 'simulation' | 'real'; +// @beta +export interface ExecutionScheduleResult { + // (undocumented) + readonly graph: ExecutionGraphSnapshot; + // (undocumented) + readonly startedNodeIds: readonly string[]; +} // Warning: (ae-missing-release-tag) "ExecutionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2466,6 +3020,53 @@ export interface ExecutionState { totalSteps: number; } +// @beta +export interface ExecutionStore { + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +export class ExecutionTaskController { + constructor(store: ExecutionStore); + // (undocumented) + inspect(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + resume(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + retry(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // (undocumented) + stop(graphId: string): ExecutionGraphSnapshot; +} + +// @beta +export interface ExecutionVerification { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly verdict: 'verified' | 'rejected' | 'needs_evidence'; + // (undocumented) + readonly verifiedAt: number; +} + // Warning: (ae-missing-release-tag) "ExperienceEntry" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2717,6 +3318,15 @@ export function getRootLogger(): Logger; // @public (undocumented) export function getTracer(): Tracer; +// @beta +export class GitWorktreeWorkspaceLeaseAdapter extends BaseWorkspaceLeaseAdapter { + constructor(options: WorkspaceLeaseAdapterOptions); + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // (undocumented) + protected removeWorkspace(lease: WorkspaceLease): Promise; +} + // Warning: (ae-missing-release-tag) "GoalCommandAction" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2946,6 +3556,33 @@ export type InlineThinkingRouter = { }; }; +// @beta +export class InMemoryExecutionStore implements ExecutionStore { + constructor(options?: InMemoryExecutionStoreOptions); + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +export interface InMemoryExecutionStoreOptions { + // (undocumented) + readonly now?: () => number; +} + // Warning: (ae-missing-release-tag) "InMemorySessionStore" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -3079,6 +3716,37 @@ export function isTimeoutError(message?: string): boolean; // @public (undocumented) export function isTransientError(message?: string): boolean; +// @beta +export class JsonlExecutionStore implements ExecutionStore { + constructor(options: JsonlExecutionStoreOptions); + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +export interface JsonlExecutionStoreOptions { + // (undocumented) + readonly now?: () => number; + // (undocumented) + readonly rootDir: string; + // (undocumented) + readonly snapshotEvery?: number; +} + // Warning: (ae-missing-release-tag) "JsonlSessionStore" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -3321,6 +3989,26 @@ type LearningFailureClass = 'execution_failure' | 'acceptance_failure' | 'contra // @public (undocumented) type LearningOutcome = 'failed' | 'recovered' | 'passed' | 'unknown'; +// @beta +export class LegacyExecutionImporter { + constructor(store: ExecutionStore); + // (undocumented) + import(request: LegacyExecutionImportRequest): ExecutionGraphSnapshot; +} + +// @beta +export interface LegacyExecutionImportRequest { + // (undocumented) + readonly kind: LegacyExecutionKind; + // (undocumented) + readonly sourcePath: string; + // (undocumented) + readonly state: Readonly>; +} + +// @beta +export type LegacyExecutionKind = 'goal' | 'task-frame' | 'plan' | 'loop'; + // Warning: (ae-missing-release-tag) "listAppliedPlatformExtensionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -4294,6 +4982,8 @@ export class MossAgent { delivery?: SessionInboxDelivery; id?: string; }): SessionInboxEntry; + // @beta + readonly agentRoles: AgentRoleRegistry; // (undocumented) readonly asyncTasks: MossAsyncTaskRegistry; // (undocumented) @@ -4324,6 +5014,8 @@ export class MossAgent { }>; // (undocumented) completeGoal(sessionKey: string, reason?: string): Promise; + // @beta + readonly completionArbiter: CompletionArbiter; // (undocumented) readonly config: MossAgentConfig; // (undocumented) @@ -4333,6 +5025,8 @@ export class MossAgent { chats: ChatResult[]; drain: SessionDrainResult; }>; + // @beta + readonly executionStore: ExecutionStore; // (undocumented) readonly extensions: PlatformExtensionRegistry; // (undocumented) @@ -4341,10 +5035,14 @@ export class MossAgent { getGoal(sessionKey: string): Promise; // (undocumented) inboxPending(sessionKey: string): readonly SessionInboxEntry[]; + // @beta + readonly orchestrationPolicy: OrchestrationPolicy; // (undocumented) pauseGoal(sessionKey: string, reason?: string): Promise; // (undocumented) readonly pendingToolAborts: PendingToolAbortStore; + // @beta + readonly planControllerStore: PlanControllerStore; // @beta (undocumented) readonly plugins: MossPluginHost; // Warning: (ae-forgotten-export) The symbol "ProjectedMessage" needs to be exported by the entry point index.d.ts @@ -4389,12 +5087,16 @@ export class MossAgent { streamChat(sessionKey: string, userMessage: string, options?: ChatOptions): AsyncGenerator; // (undocumented) streamChatRecorded(sessionKey: string, userMessage: string, options?: ChatOptions): AsyncGenerator; + // @beta + readonly tasks: ExecutionTaskController; // (undocumented) readonly tools: ToolRegistry; // (undocumented) unregisterPostToolHook(name: string): boolean; // (undocumented) unregisterPreToolHook(name: string): boolean; + // @beta + readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; } // Warning: (ae-missing-release-tag) "MossAgentConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -4410,6 +5112,12 @@ export type MossAgentConfig = MossAgentConfig_2; // // @public (undocumented) interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, ToolExecutionConfig, PromptConfig { + // @beta + agentRoleRegistry?: AgentRoleRegistry; + // @beta + agentRoles?: readonly AgentRoleDefinition[]; + // @beta + allowPluginIsolatedWrite?: boolean; asyncTaskRegistry?: MossAsyncTaskRegistry; bufferAssistantUntilComplete?: boolean; // (undocumented) @@ -4426,6 +5134,8 @@ interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, Too enableSteering?: boolean; // (undocumented) enableThinkingStream?: boolean; + // @beta + executionStore?: ExecutionStore; // Warning: (ae-forgotten-export) The symbol "FollowUpGuardConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -4443,6 +5153,10 @@ interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, Too sessionKey: string; lastUserMessage: string; }) => Promise; + // @beta + orchestrationPolicy?: Partial; + // @beta + planControllerStore?: PlanControllerStore; // Warning: (ae-forgotten-export) The symbol "PromptCacheConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -4475,6 +5189,8 @@ interface MossAgentConfig_2 extends ProviderConfig, ContextManagementConfig, Too subagentExperts?: readonly SubagentExpertDefinition[]; // (undocumented) workspaceDir?: string; + // @beta + workspaceLeaseAdapter?: WorkspaceLeaseAdapter; } // Warning: (ae-forgotten-export) The symbol "MossAgentEvent_2" needs to be exported by the entry point index.d.ts @@ -4812,6 +5528,8 @@ export interface MossPluginContext { // (undocumented) effect(setup: () => MossPluginDisposer | Promise, label?: string): void; // (undocumented) + registerAgentRole(role: AgentRoleDefinition): void; + // (undocumented) registerCommand(command: MossPluginCommand): void; // (undocumented) registerExpert(expert: SubagentExpertDefinition): void; @@ -4909,6 +5627,8 @@ export interface MossPluginProvider { // @beta export interface MossPluginSnapshot { + // (undocumented) + readonly agentRoles: readonly string[]; // (undocumented) readonly callState: MossPluginCallState; // (undocumented) @@ -5006,6 +5726,9 @@ export function normalizeProvider(value: string | undefined): CliProviderPreset; // @public (undocumented) export function normalizeTeachingDepth(raw: unknown): TeachingDepth; +// @beta +export function normalizeWritePath(value: string): string; + // Warning: (ae-missing-release-tag) "ObservationAggregator" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -5059,6 +5782,18 @@ export interface OpenAILLMProviderConfig { defaultModel?: string; } +// @beta +export interface OrchestrationPolicy { + // (undocumented) + readonly autoResumeReadonly?: boolean; + // (undocumented) + readonly maxAttemptsPerNode: number; + // (undocumented) + readonly maxConcurrency: number; + // (undocumented) + readonly strictCompletion: boolean; +} + // Warning: (ae-missing-release-tag) "OutputGuardrailDecision" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -5591,6 +6326,22 @@ export interface Plan { version: number; } +// @beta +export class PlanControllerStore { + // (undocumented) + getActivePlanForSession(sessionKey: string): Plan | null; + // (undocumented) + getActivePlanId(sessionKey: string): string | undefined; + // (undocumented) + getPlanController(sessionKey: string): PlanExecuteController; + // (undocumented) + getSharedPlanController(): PlanExecuteController; + // (undocumented) + reset(): void; + // (undocumented) + setActivePlanId(sessionKey: string, planId: string): void; +} + // Warning: (ae-missing-release-tag) "PlanExecuteConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -5708,6 +6459,7 @@ export interface PlanStep { status: StepStatus; // (undocumented) step: number; + writePaths?: string[]; } // Warning: (ae-missing-release-tag) "planStepTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -5762,6 +6514,7 @@ export interface PlanToolInput { expectedAccept?: string[]; dependsOn?: number[]; estimatedTimeSec?: number; + writePaths?: string[]; }>; // (undocumented) successCriteria?: string[]; @@ -5964,6 +6717,9 @@ interface ProjectedToolCall { // @public (undocumented) type ProjectedToolStatus = 'called' | 'succeeded' | 'failed'; +// @beta +export function projectExecutionGraph(events: readonly ExecutionEvent[]): ExecutionGraphSnapshot; + // Warning: (ae-missing-release-tag) "PromoteResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -6185,6 +6941,15 @@ export function readUsageLog(options?: { logPath?: string; }): Promise; +// @beta +export function recoverExecutionGraph(store: ExecutionStore, graphId: string, options?: RecoverExecutionGraphOptions): ExecutionGraphSnapshot; + +// @beta +export interface RecoverExecutionGraphOptions { + // (undocumented) + readonly now?: number; +} + // Warning: (ae-missing-release-tag) "RecoveryRecipe" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -6315,6 +7080,8 @@ export function registerBuiltinTools(agent: { tools: { register: (tool: Tool) => void; }; + planControllerStore?: PlanControllerStore; + executionStore?: ExecutionStore; }): void; // Warning: (ae-missing-release-tag) "registerKnowledgeModule" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -6577,6 +7344,14 @@ export interface RetryOptions { shouldRetry?: (err: unknown, attempt: number) => boolean; } +// @beta +export interface RoutedAssignment { + // (undocumented) + readonly assignment: AssignmentSpec; + // (undocumented) + readonly role: Readonly; +} + // Warning: (ae-missing-release-tag) "runMossCommunityAuthLogin" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -6706,6 +7481,17 @@ interface SelectMemoryForContextParams { workspaceTopN?: number; } +// @beta +export type SemanticCompletionJudge = (graph: ExecutionGraphSnapshot) => Promise; + +// @beta +export interface SemanticCompletionResult { + // (undocumented) + readonly covered: boolean; + // (undocumented) + readonly reasons: readonly string[]; +} + // Warning: (ae-missing-release-tag) "SessionDrainResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -7499,6 +8285,12 @@ export function summarizeUsage(records: LLMUsageRecord[], periodStart?: string, // @public (undocumented) export function syncPlatformExtensionsAtStartup(factories: Array<() => MossPlatformExtension>): void; +// @beta +export function synthesizeAgentResults(input: { + readonly assignments: readonly AssignmentSpec[]; + readonly results: readonly AgentResult[]; +}): AgentSynthesisResult; + // Warning: (ae-missing-release-tag) "TailToolSnipConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -7534,7 +8326,7 @@ export type TaskRunEventType = 'run.created' | 'run.started' | 'tool.started' | // @beta export class TaskRunLedger { - constructor(filePath?: string | undefined); + constructor(filePath?: string | undefined, executionStore?: ExecutionStore | undefined); // (undocumented) append(runId: string, input: AppendTaskRunEventInput): TaskRunSnapshot; // (undocumented) @@ -7938,6 +8730,7 @@ export interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -7951,6 +8744,7 @@ export interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -7964,6 +8758,10 @@ export interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -8757,6 +9555,108 @@ export const webSearchTool: Tool; // @public export function withSpan(name: string, attributes: Record | undefined, fn: (span: Span) => Promise): Promise; +// @beta +export interface WorkspaceLease { + // (undocumented) + readonly baselineHashes: Readonly>; + // (undocumented) + readonly baseRef: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly kind: WorkspaceLeaseKind; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // (undocumented) + readonly status: WorkspaceLeaseStatus; + // (undocumented) + readonly updatedAt: number; + // (undocumented) + readonly workspacePath: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + +// @beta +export interface WorkspaceLeaseAdapter { + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // (undocumented) + createPatch(lease: WorkspaceLease): Promise; + // (undocumented) + list(): readonly WorkspaceLease[]; + // (undocumented) + load(leaseId: string): WorkspaceLease | undefined; + // (undocumented) + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // (undocumented) + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; +} + +// @beta +export interface WorkspaceLeaseAdapterOptions { + // (undocumented) + readonly authorizeMerge?: (request: WorkspaceMergeAuthorizationRequest) => void | Promise; + // (undocumented) + readonly now?: () => number; + // (undocumented) + readonly rootDir: string; +} + +// @beta +export type WorkspaceLeaseKind = 'git-worktree' | 'copy-snapshot'; + +// @beta +export type WorkspaceLeaseReleaseReason = 'merged' | 'rejected' | 'cancelled'; + +// @beta +export type WorkspaceLeaseStatus = 'active' | 'merged' | 'rejected' | 'cancelled'; + +// @beta +export interface WorkspaceMergeAuthorizationRequest { + // (undocumented) + readonly changedPaths: readonly string[]; + // (undocumented) + readonly digest: string; + // (undocumented) + readonly lease: WorkspaceLease; + // (undocumented) + readonly patchId: string; +} + +// @beta +export interface WorkspaceMergeResult { + // (undocumented) + readonly conflictingPaths: readonly string[]; + // (undocumented) + readonly patchId: string; + // (undocumented) + readonly status: 'merged' | 'merge_conflict'; +} + +// @beta +export interface WorkspacePatch { + readonly artifactRef: string; + // (undocumented) + readonly changedPaths: readonly string[]; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly leaseId: string; + // (undocumented) + readonly patch: string; +} + // Warning: (ae-missing-release-tag) "wrapAsMoss" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -8774,20 +9674,25 @@ export const writeFileTool: Tool; // @public (undocumented) export function writeMossCommunityAuthSession(session: MossCommunityAuthSession, configDir?: string): string; +// @beta +export function writePathsOverlap(leftPaths: readonly string[], rightPaths: readonly string[]): boolean; + // Warnings were encountered during analysis: // // src/cli/community-auth.ts:694:5 - (ae-forgotten-export) The symbol "FetchImpl" needs to be exported by the entry point index.d.ts // src/cli/model-catalog.ts:62:17 - (ae-forgotten-export) The symbol "CustomModelConfig" needs to be exported by the entry point index.d.ts // src/context/pruning.ts:77:3 - (ae-forgotten-export) The symbol "ContextPruningToolMatch" needs to be exported by the entry point index.d.ts -// src/core/agent/moss-agent.ts:584:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts -// src/core/agent/moss-agent.ts:665:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/agent/moss-agent.ts:611:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts +// src/core/agent/moss-agent.ts:692:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // src/memory/recovery-recipe-log.ts:162:3 - (ae-forgotten-export) The symbol "LearningEvent" needs to be exported by the entry point index.d.ts // src/memory/recovery-recipe-log.ts:163:3 - (ae-forgotten-export) The symbol "ExperienceEntry" needs to be exported by the entry point index.d.ts // src/memory/trusted-patch-coordinator.ts:53:7 - (ae-forgotten-export) The symbol "LearningEventLog" needs to be exported by the entry point index.d.ts // src/memory/trusted-skill-experiment-coordinator.ts:253:7 - (ae-forgotten-export) The symbol "TerminalVerdictLog" needs to be exported by the entry point index.d.ts // src/mesh/agent-mesh.ts:304:40 - (ae-forgotten-export) The symbol "HostAddressResolver_2" needs to be exported by the entry point index.d.ts -// src/runtime/shared-runtime.ts:111:22 - (ae-forgotten-export) The symbol "DeviceReadonlyExecutor" needs to be exported by the entry point index.d.ts +// src/runtime/shared-runtime.ts:112:22 - (ae-forgotten-export) The symbol "DeviceReadonlyExecutor" needs to be exported by the entry point index.d.ts +// src/tools/builtin.ts:517:3 - (ae-incompatible-release-tags) The symbol "planControllerStore" is marked as @public, but its signature references "PlanControllerStore" which is marked as @beta +// src/tools/builtin.ts:518:3 - (ae-incompatible-release-tags) The symbol "executionStore" is marked as @public, but its signature references "ExecutionStore" which is marked as @beta // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/runtime.api.md b/packages/moss-agent/etc/runtime.api.md index a5eedfe8..7212718c 100644 --- a/packages/moss-agent/etc/runtime.api.md +++ b/packages/moss-agent/etc/runtime.api.md @@ -15,6 +15,33 @@ import type { MossPlatformExtension } from '@rdk-moss/core'; import type { MossVendorPlugin } from '@rdk-moss/core'; import type { PromptFragment } from '@rdk-moss/core'; +// Warning: (ae-missing-release-tag) "AcceptPredicateName" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +type AcceptPredicateName = 'file_exist' | 'file_nonempty' | 'file_created_after' | 'file_fresh_nonempty' | 'artifact_digest_changed' | 'image_decodable' | 'image_dimensions' | 'image_content_nontrivial' | 'process_running' | 'pose_error_within' | 'force_below' | 'joint_at' | 'exit_code_zero' | 'stdout_matches' | 'video_fps_above'; + +// Warning: (ae-missing-release-tag) "AcceptSpec" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +interface AcceptSpec { + description?: string; + // Warning: (ae-forgotten-export) The symbol "AcceptPredicateName" needs to be exported by the entry point index.d.ts + name: AcceptPredicateName; + params: Record; + safetyCritical?: boolean; +} + +// @beta +interface AcquireExecutionLeaseInput { + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly ttlMs?: number; +} + +// @beta +type AgentCapability = 'architecture' | 'code' | 'test' | 'security' | 'performance' | 'documentation' | 'device' | 'research' | (string & {}); + // Warning: (ae-missing-release-tag) "AgentHooks" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -130,6 +157,77 @@ interface AgentLoopExtensions { steeringEngine?: SteeringEngine; } +// @beta +interface AgentRoleDefinition { + // (undocumented) + readonly allowedTools?: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "ExecutionBudget" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly budget?: ExecutionBudget; + // Warning: (ae-forgotten-export) The symbol "AgentCapability" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly capabilities: readonly AgentCapability[]; + // (undocumented) + readonly displayName: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly instructions: string; + // Warning: (ae-forgotten-export) The symbol "AgentRoleKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: AgentRoleKind; + // (undocumented) + readonly model?: string; + // (undocumented) + readonly outputContract: 'structured-v1'; + // (undocumented) + readonly workspaceMode: 'shared-readonly' | 'isolated-write'; +} + +// @beta +type AgentRoleKind = 'advisor' | 'implementer' | 'verifier'; + +// @beta +class AgentRoleRegistry { + // Warning: (ae-forgotten-export) The symbol "AgentRoleRegistryOptions" needs to be exported by the entry point index.d.ts + constructor(options?: AgentRoleRegistryOptions); + // (undocumented) + get(id: string): Readonly | undefined; + // (undocumented) + list(): readonly Readonly[]; + // Warning: (ae-forgotten-export) The symbol "AgentRoleDefinition" needs to be exported by the entry point index.d.ts + // + // (undocumented) + register(definition: AgentRoleDefinition): () => void; +} + +// @beta +interface AgentRoleRegistryOptions { + // (undocumented) + readonly allowIsolatedWrite?: boolean; +} + +// @beta +interface AppendExecutionEventInput { + // (undocumented) + readonly data?: Readonly>; + // (undocumented) + readonly expectedRevision: number; + // (undocumented) + readonly id?: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly time?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly type: Exclude; +} + // Warning: (ae-missing-release-tag) "ApprovedPreflightAssignment" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -374,6 +472,48 @@ interface CompactionSettings { // @public (undocumented) type CompactReason = 'run_start' | 'overflow' | 'proactive' | 'manual_compact'; +// @beta +class CompletionArbiter { + // Warning: (ae-forgotten-export) The symbol "ExecutionStore" needs to be exported by the entry point index.d.ts + constructor(store: ExecutionStore); + // Warning: (ae-forgotten-export) The symbol "CompletionArbiterInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "CompletionDecision" needs to be exported by the entry point index.d.ts + // + // (undocumented) + decide(graphId: string, input: CompletionArbiterInput): Promise; +} + +// @beta +interface CompletionArbiterInput { + // (undocumented) + readonly activeBackgroundTaskIds?: readonly string[]; + // (undocumented) + readonly activeWorkspaceLeaseIds?: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "SemanticCompletionJudge" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly semanticJudge?: SemanticCompletionJudge; + // Warning: (ae-forgotten-export) The symbol "CompletionTaskKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly taskKind: CompletionTaskKind; +} + +// @beta +interface CompletionDecision { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly status: 'in_progress' | 'blocked' | 'completed' | 'failed'; + // (undocumented) + readonly verdict: 'pending' | 'needs_evidence' | 'verified' | 'rejected'; +} + +// @beta +type CompletionTaskKind = 'coding' | 'research' | 'device' | 'analysis'; + // Warning: (ae-missing-release-tag) "ComposedSkillContext" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -503,6 +643,28 @@ type ContextPruningToolMatch = { // @public (undocumented) type ContextSources = Readonly>; +// @beta +interface CreateExecutionGraphInput { + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeDefinition" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly nodes?: readonly ExecutionNodeDefinition[]; + // (undocumented) + readonly now?: number; + // Warning: (ae-forgotten-export) The symbol "OrchestrationPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly policy?: Partial; + // (undocumented) + readonly sessionId?: string; +} + // Warning: (ae-missing-release-tag) "createMossCoreServices" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -546,6 +708,20 @@ export interface CreateMossRuntimeOptions { workspaceDir: string; } +// @beta +interface CreateWorkspaceLeaseInput { + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + // Warning: (ae-missing-release-tag) "DeviceReadonlyExecutor" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -607,6 +783,14 @@ export enum ErrorCode { // (undocumented) DEVICE_SSH_FAILED = "DEVICE_SSH_FAILED", // (undocumented) + EXECUTION_LEASE_HELD = "EXECUTION_LEASE_HELD", + // (undocumented) + EXECUTION_REVISION_CONFLICT = "EXECUTION_REVISION_CONFLICT", + // (undocumented) + EXECUTION_STATE_INVALID = "EXECUTION_STATE_INVALID", + // (undocumented) + EXECUTION_STORE_FAILED = "EXECUTION_STORE_FAILED", + // (undocumented) INTERNAL_INVARIANT_VIOLATED = "INTERNAL_INVARIANT_VIOLATED", // (undocumented) MCP_CONNECTION_FAILED = "MCP_CONNECTION_FAILED", @@ -658,11 +842,273 @@ interface EvidenceTrustBoundary { realEvidenceEligible?: boolean; } +// @beta +interface ExecutionBudget { + // (undocumented) + readonly maxCostUsd?: number; + // (undocumented) + readonly maxTokens?: number; + // (undocumented) + readonly maxWallTimeMs?: number; + // (undocumented) + readonly usedCostUsd?: number; + // (undocumented) + readonly usedTokens?: number; + // (undocumented) + readonly usedWallTimeMs?: number; +} + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) type ExecutionDomain = 'local' | 'simulation' | 'real'; +// @beta +interface ExecutionEvent { + // (undocumented) + readonly data: Readonly>; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly seq: number; + // (undocumented) + readonly time: number; + // (undocumented) + readonly type: ExecutionEventType; +} + +// @beta +type ExecutionEventType = 'graph.created' | 'graph.ready' | 'graph.resumed' | 'graph.paused' | 'graph.recovered' | 'graph.blocked' | 'graph.completed' | 'graph.failed' | 'graph.cancelled' | 'plan.revised' | 'node.added' | 'node.ready' | 'node.leased' | 'node.started' | 'node.progressed' | 'node.succeeded' | 'node.failed' | 'node.interrupted' | 'node.retry_requested' | 'node.blocked' | 'node.skipped' | 'node.merge_conflict' | 'node.cancelled' | 'evidence.recorded' | 'budget.updated' | 'steering.recorded' | 'verification.recorded'; + +// @beta +interface ExecutionEvidence { + // (undocumented) + readonly artifactRef?: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest?: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidenceKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: ExecutionEvidenceKind; + // (undocumented) + readonly metadata?: Readonly>; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly summary: string; +} + +// @beta +type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; + +// @beta +interface ExecutionGraphSnapshot { + // (undocumented) + readonly budget: ExecutionBudget; + // (undocumented) + readonly createdAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly evidence: readonly ExecutionEvidence[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly nodes: Readonly>; + // (undocumented) + readonly policy: OrchestrationPolicy; + // Warning: (ae-forgotten-export) The symbol "ExecutionRecovery" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly recovery?: ExecutionRecovery; + // (undocumented) + readonly revision: number; + // (undocumented) + readonly sessionId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: ExecutionGraphStatus; + // (undocumented) + readonly updatedAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionVerification" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly verification?: ExecutionVerification; +} + +// @beta +type ExecutionGraphStatus = 'paused' | 'ready' | 'running' | 'paused_recovered' | 'blocked' | 'completed' | 'failed' | 'cancelled'; + +// @beta +interface ExecutionNode extends ExecutionNodeDefinition { + // (undocumented) + readonly attempts: number; + // (undocumented) + readonly blockedByDependencies?: readonly string[]; + // (undocumented) + readonly completedAt?: number; + // (undocumented) + readonly consecutiveSameFailures: number; + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly startedAt?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: ExecutionNodeStatus; + // (undocumented) + readonly workspaceLeaseId?: string; +} + +// @beta +interface ExecutionNodeDefinition { + // (undocumented) + readonly acceptanceCriteria?: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: ExecutionNodeKind; + // (undocumented) + readonly requiredCapabilities?: readonly string[]; + // (undocumented) + readonly roleId?: string; + // (undocumented) + readonly title: string; + // (undocumented) + readonly writePaths?: readonly string[]; +} + +// @beta +type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; + +// @beta +type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; + +// @beta +interface ExecutionOwnerLease { + // (undocumented) + readonly acquiredAt: number; + // (undocumented) + readonly expiresAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly token: string; +} + +// @beta +interface ExecutionRecovery { + // (undocumented) + readonly blockedMutationNodeIds: readonly string[]; + // (undocumented) + readonly interruptedNodeIds: readonly string[]; + // (undocumented) + readonly recoveredAt: number; + // (undocumented) + readonly requiresUserResume: boolean; +} + +// Warning: (ae-missing-release-tag) "ExecutionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface ExecutionState { + // (undocumented) + completedSteps: number; + // (undocumented) + currentStep: number; + // (undocumented) + isExecuting: boolean; + // (undocumented) + lastError?: string; + // (undocumented) + planId: string; + // (undocumented) + totalSteps: number; +} + +// @beta +interface ExecutionStore { + // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts + // + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts + // + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts + // + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvent" needs to be exported by the entry point index.d.ts + // + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +class ExecutionTaskController { + constructor(store: ExecutionStore); + // (undocumented) + inspect(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + resume(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + retry(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // (undocumented) + stop(graphId: string): ExecutionGraphSnapshot; +} + +// @beta +interface ExecutionVerification { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly verdict: 'verified' | 'rejected' | 'needs_evidence'; + // (undocumented) + readonly verifiedAt: number; +} + // Warning: (ae-forgotten-export) The symbol "EvidenceTrustBoundary" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ExperienceEntry" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1321,6 +1767,10 @@ class MossAgent { delivery?: SessionInboxDelivery; id?: string; }): SessionInboxEntry; + // Warning: (ae-forgotten-export) The symbol "AgentRoleRegistry" needs to be exported by the entry point index.d.ts + // + // @beta + readonly agentRoles: AgentRoleRegistry; // (undocumented) readonly asyncTasks: MossAsyncTaskRegistry; // (undocumented) @@ -1356,6 +1806,10 @@ class MossAgent { }>; // (undocumented) completeGoal(sessionKey: string, reason?: string): Promise; + // Warning: (ae-forgotten-export) The symbol "CompletionArbiter" needs to be exported by the entry point index.d.ts + // + // @beta + readonly completionArbiter: CompletionArbiter; // Warning: (ae-forgotten-export) The symbol "MossAgentConfig_2" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1367,6 +1821,8 @@ class MossAgent { chats: ChatResult[]; drain: SessionDrainResult; }>; + // @beta + readonly executionStore: ExecutionStore; // Warning: (ae-forgotten-export) The symbol "PlatformExtensionRegistry" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1377,12 +1833,18 @@ class MossAgent { getGoal(sessionKey: string): Promise; // (undocumented) inboxPending(sessionKey: string): readonly SessionInboxEntry[]; + // @beta + readonly orchestrationPolicy: OrchestrationPolicy; // (undocumented) pauseGoal(sessionKey: string, reason?: string): Promise; // Warning: (ae-forgotten-export) The symbol "PendingToolAbortStore" needs to be exported by the entry point index.d.ts // // (undocumented) readonly pendingToolAborts: PendingToolAbortStore; + // Warning: (ae-forgotten-export) The symbol "PlanControllerStore" needs to be exported by the entry point index.d.ts + // + // @beta + readonly planControllerStore: PlanControllerStore; // @beta (undocumented) readonly plugins: MossPluginHost; // Warning: (ae-forgotten-export) The symbol "ProjectedMessage" needs to be exported by the entry point index.d.ts @@ -1437,6 +1899,10 @@ class MossAgent { // // (undocumented) streamChatRecorded(sessionKey: string, userMessage: string, options?: ChatOptions): AsyncGenerator; + // Warning: (ae-forgotten-export) The symbol "ExecutionTaskController" needs to be exported by the entry point index.d.ts + // + // @beta + readonly tasks: ExecutionTaskController; // Warning: (ae-forgotten-export) The symbol "ToolRegistry" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1445,6 +1911,10 @@ class MossAgent { unregisterPostToolHook(name: string): boolean; // (undocumented) unregisterPreToolHook(name: string): boolean; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts + // + // @beta + readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; } // Warning: (ae-forgotten-export) The symbol "ProviderConfig" needs to be exported by the entry point index.d.ts @@ -1455,6 +1925,12 @@ class MossAgent { // // @public (undocumented) interface MossAgentConfig extends ProviderConfig, ContextManagementConfig, ToolExecutionConfig, PromptConfig { + // @beta + agentRoleRegistry?: AgentRoleRegistry; + // @beta + agentRoles?: readonly AgentRoleDefinition[]; + // @beta + allowPluginIsolatedWrite?: boolean; asyncTaskRegistry?: MossAsyncTaskRegistry; bufferAssistantUntilComplete?: boolean; // Warning: (ae-forgotten-export) The symbol "CapabilityPack" needs to be exported by the entry point index.d.ts @@ -1473,6 +1949,8 @@ interface MossAgentConfig extends ProviderConfig, ContextManagementConfig, ToolE enableSteering?: boolean; // (undocumented) enableThinkingStream?: boolean; + // @beta + executionStore?: ExecutionStore; // Warning: (ae-forgotten-export) The symbol "FollowUpGuardConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1494,6 +1972,10 @@ interface MossAgentConfig extends ProviderConfig, ContextManagementConfig, ToolE sessionKey: string; lastUserMessage: string; }) => Promise; + // @beta + orchestrationPolicy?: Partial; + // @beta + planControllerStore?: PlanControllerStore; // Warning: (ae-forgotten-export) The symbol "PromptCacheConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1534,6 +2016,8 @@ interface MossAgentConfig extends ProviderConfig, ContextManagementConfig, ToolE subagentExperts?: readonly SubagentExpertDefinition[]; // (undocumented) workspaceDir?: string; + // @beta + workspaceLeaseAdapter?: WorkspaceLeaseAdapter; } // Warning: (ae-missing-release-tag) "MossAgentConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1777,6 +2261,8 @@ export interface MossPluginContext { // (undocumented) effect(setup: () => MossPluginDisposer | Promise, label?: string): void; // (undocumented) + registerAgentRole(role: AgentRoleDefinition): void; + // (undocumented) registerCommand(command: MossPluginCommand): void; // (undocumented) registerExpert(expert: SubagentExpertDefinition): void; @@ -1974,6 +2460,8 @@ export interface MossPluginRuntimeManifest { // @beta export interface MossPluginSnapshot { + // (undocumented) + readonly agentRoles: readonly string[]; // (undocumented) readonly callState: MossPluginCallState; // (undocumented) @@ -2076,6 +2564,18 @@ interface ObservationAggregatorOptions { scopeRef?: string; } +// @beta +interface OrchestrationPolicy { + // (undocumented) + readonly autoResumeReadonly?: boolean; + // (undocumented) + readonly maxAttemptsPerNode: number; + // (undocumented) + readonly maxConcurrency: number; + // (undocumented) + readonly strictCompletion: boolean; +} + // Warning: (ae-missing-release-tag) "OutputGuardrailDecision" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2123,6 +2623,114 @@ class PendingToolAbortStore { }[]): void; } +// Warning: (ae-missing-release-tag) "Plan" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface Plan { + // (undocumented) + createdAt: string; + // (undocumented) + currentStep?: number; + // (undocumented) + goal: string; + // (undocumented) + id: string; + // (undocumented) + preconditions?: string[]; + // (undocumented) + rationale?: string; + // (undocumented) + replanCount?: number; + // Warning: (ae-forgotten-export) The symbol "PlanStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + status: PlanStatus; + // Warning: (ae-forgotten-export) The symbol "PlanStep" needs to be exported by the entry point index.d.ts + // + // (undocumented) + steps: PlanStep[]; + // (undocumented) + successCriteria?: string[]; + // Warning: (ae-forgotten-export) The symbol "AcceptSpec" needs to be exported by the entry point index.d.ts + terminalAccept?: AcceptSpec[]; + // (undocumented) + updatedAt: string; + // (undocumented) + version: number; +} + +// @beta +class PlanControllerStore { + // Warning: (ae-forgotten-export) The symbol "Plan" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getActivePlanForSession(sessionKey: string): Plan | null; + // (undocumented) + getActivePlanId(sessionKey: string): string | undefined; + // Warning: (ae-forgotten-export) The symbol "PlanExecuteController" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getPlanController(sessionKey: string): PlanExecuteController; + // (undocumented) + getSharedPlanController(): PlanExecuteController; + // (undocumented) + reset(): void; + // (undocumented) + setActivePlanId(sessionKey: string, planId: string): void; +} + +// Warning: (ae-missing-release-tag) "PlanExecuteConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanExecuteConfig { + // (undocumented) + autoApproveSimple?: boolean; + // (undocumented) + maxExecutionTimeMs?: number; + // (undocumented) + maxReplans?: number; + // (undocumented) + requireApproval?: boolean; +} + +// Warning: (ae-missing-release-tag) "PlanExecuteController" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +class PlanExecuteController { + // Warning: (ae-forgotten-export) The symbol "PlanExecuteConfig" needs to be exported by the entry point index.d.ts + constructor(config?: PlanExecuteConfig); + // (undocumented) + approvePlan(planId: string): boolean; + // (undocumented) + cancelPlan(planId: string): boolean; + // (undocumented) + completeStep(planId: string, stepNumber: number, actualOutput?: string, actualTools?: string[]): boolean; + // (undocumented) + createPlan(goal: string, steps: Omit[], rationale?: string): Plan; + // (undocumented) + failStep(planId: string, stepNumber: number, error: string): boolean; + // (undocumented) + static formatPlan(plan: Plan): string; + // (undocumented) + getActivePlan(): Plan | null; + // Warning: (ae-forgotten-export) The symbol "ExecutionState" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getExecutionState(planId: string): ExecutionState | null; + // (undocumented) + getPlan(planId: string): Plan | null; + // (undocumented) + requestReplan(planId: string, _reason: string, revisedSteps?: Omit[]): Plan | null; + // Warning: (ae-forgotten-export) The symbol "PlanReviewResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + reviewPlan(planId: string): PlanReviewResult; + // (undocumented) + skipStep(planId: string, stepNumber: number, reason: string): boolean; + // (undocumented) + startExecution(planId: string): boolean; +} + // Warning: (ae-missing-release-tag) "PlannedSkill" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2137,6 +2745,59 @@ interface PlannedSkill { stableId: string; } +// Warning: (ae-missing-release-tag) "PlanReviewResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanReviewResult { + // (undocumented) + approved: boolean; + // (undocumented) + issues: string[]; + // (undocumented) + revisedPlan?: Plan; + // (undocumented) + suggestions: string[]; +} + +// Warning: (ae-missing-release-tag) "PlanStatus" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +type PlanStatus = 'draft' | 'reviewing' | 'approved' | 'executing' | 'completed' | 'replanning' | 'failed' | 'cancelled'; + +// Warning: (ae-missing-release-tag) "PlanStep" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanStep { + // (undocumented) + actualOutput?: string; + // (undocumented) + actualTools?: string[]; + // (undocumented) + completedAt?: string; + // (undocumented) + dependsOn?: number[]; + // (undocumented) + description: string; + // (undocumented) + error?: string; + // (undocumented) + estimatedTimeSec?: number; + expectedAccept?: string[]; + // (undocumented) + expectedOutput?: string; + // (undocumented) + expectedTools?: string[]; + // (undocumented) + startedAt?: string; + // Warning: (ae-forgotten-export) The symbol "StepStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + status: StepStatus; + // (undocumented) + step: number; + writePaths?: string[]; +} + // Warning: (ae-missing-release-tag) "PlatformExtensionRegistry" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2423,6 +3084,19 @@ interface ReadonlyExecResult { stdout: string; } +// Warning: (ae-forgotten-export) The symbol "SemanticCompletionResult" needs to be exported by the entry point index.d.ts +// +// @beta +type SemanticCompletionJudge = (graph: ExecutionGraphSnapshot) => Promise; + +// @beta +interface SemanticCompletionResult { + // (undocumented) + readonly covered: boolean; + // (undocumented) + readonly reasons: readonly string[]; +} + // Warning: (ae-missing-release-tag) "SessionDrainResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2977,6 +3651,11 @@ interface SteeringRule { priority: number; } +// Warning: (ae-missing-release-tag) "StepStatus" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +type StepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed' | 'blocked'; + // Warning: (ae-missing-release-tag) "StructuredToolResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -3192,6 +3871,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -3205,6 +3885,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -3218,6 +3899,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -3401,13 +4086,106 @@ type VerdictLevel = 'L1' | 'L2' | 'L3'; // @public type VerdictSource = 'exit_code' | 'file_exist' | 'geometric' | 'sensor' | 'model_judge'; +// @beta +interface WorkspaceLease { + // (undocumented) + readonly baselineHashes: Readonly>; + // (undocumented) + readonly baseRef: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseKind" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly kind: WorkspaceLeaseKind; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly parentWorkspace: string; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly status: WorkspaceLeaseStatus; + // (undocumented) + readonly updatedAt: number; + // (undocumented) + readonly workspacePath: string; + // (undocumented) + readonly writePaths: readonly string[]; +} + +// @beta +interface WorkspaceLeaseAdapter { + // Warning: (ae-forgotten-export) The symbol "CreateWorkspaceLeaseInput" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WorkspaceLease" needs to be exported by the entry point index.d.ts + // + // (undocumented) + create(input: CreateWorkspaceLeaseInput): Promise; + // Warning: (ae-forgotten-export) The symbol "WorkspacePatch" needs to be exported by the entry point index.d.ts + // + // (undocumented) + createPatch(lease: WorkspaceLease): Promise; + // (undocumented) + list(): readonly WorkspaceLease[]; + // (undocumented) + load(leaseId: string): WorkspaceLease | undefined; + // Warning: (ae-forgotten-export) The symbol "WorkspaceMergeResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseReleaseReason" needs to be exported by the entry point index.d.ts + // + // (undocumented) + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; +} + +// @beta +type WorkspaceLeaseKind = 'git-worktree' | 'copy-snapshot'; + +// @beta +type WorkspaceLeaseReleaseReason = 'merged' | 'rejected' | 'cancelled'; + +// @beta +type WorkspaceLeaseStatus = 'active' | 'merged' | 'rejected' | 'cancelled'; + +// @beta +interface WorkspaceMergeResult { + // (undocumented) + readonly conflictingPaths: readonly string[]; + // (undocumented) + readonly patchId: string; + // (undocumented) + readonly status: 'merged' | 'merge_conflict'; +} + +// @beta +interface WorkspacePatch { + readonly artifactRef: string; + // (undocumented) + readonly changedPaths: readonly string[]; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly leaseId: string; + // (undocumented) + readonly patch: string; +} + // Warnings were encountered during analysis: // // src/context/pruning.ts:77:3 - (ae-forgotten-export) The symbol "ContextPruningToolMatch" needs to be exported by the entry point index.d.ts -// src/core/agent/moss-agent.ts:584:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts -// src/core/agent/moss-agent.ts:665:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts -// src/runtime/shared-runtime.ts:111:22 - (ae-forgotten-export) The symbol "DeviceReadonlyExecutor" needs to be exported by the entry point index.d.ts +// src/core/agent/moss-agent.ts:611:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts +// src/core/agent/moss-agent.ts:692:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/runtime/shared-runtime.ts:112:22 - (ae-forgotten-export) The symbol "DeviceReadonlyExecutor" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/structured-output.api.md b/packages/moss-agent/etc/structured-output.api.md index c6a45c1e..27972093 100644 --- a/packages/moss-agent/etc/structured-output.api.md +++ b/packages/moss-agent/etc/structured-output.api.md @@ -360,6 +360,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -373,6 +374,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -386,6 +388,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -443,7 +449,7 @@ export function validateJsonSchemaDefinition(schema: JsonSchema): SchemaDefiniti // Warnings were encountered during analysis: // -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/teaching.api.md b/packages/moss-agent/etc/teaching.api.md index 77d84cf3..3c531ff5 100644 --- a/packages/moss-agent/etc/teaching.api.md +++ b/packages/moss-agent/etc/teaching.api.md @@ -504,6 +504,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -517,6 +518,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -530,6 +532,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -615,7 +621,7 @@ type ToolSideEffectClass = 'readonly' | 'local_write' | 'device_mutation' | 'cre // Warnings were encountered during analysis: // // src/core/llm/llm-provider.ts:19:7 - (ae-forgotten-export) The symbol "ToolContentBlock" needs to be exported by the entry point index.d.ts -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // src/teaching/teaching-layer.ts:162:3 - (ae-forgotten-export) The symbol "ToolApprovalRequest" needs to be exported by the entry point index.d.ts // src/teaching/teaching-layer.ts:162:3 - (ae-forgotten-export) The symbol "ToolApprovalDecision" needs to be exported by the entry point index.d.ts // src/teaching/teaching-layer.ts:163:3 - (ae-forgotten-export) The symbol "ToolCall" needs to be exported by the entry point index.d.ts diff --git a/packages/moss-agent/etc/tools__builtin.api.md b/packages/moss-agent/etc/tools__builtin.api.md index 52b52512..25ca2fe8 100644 --- a/packages/moss-agent/etc/tools__builtin.api.md +++ b/packages/moss-agent/etc/tools__builtin.api.md @@ -6,6 +6,48 @@ import type { MossAsyncTaskRegistry } from '@rdk-moss/core/contracts/async-task'; +// Warning: (ae-missing-release-tag) "AcceptPredicateName" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +type AcceptPredicateName = 'file_exist' | 'file_nonempty' | 'file_created_after' | 'file_fresh_nonempty' | 'artifact_digest_changed' | 'image_decodable' | 'image_dimensions' | 'image_content_nontrivial' | 'process_running' | 'pose_error_within' | 'force_below' | 'joint_at' | 'exit_code_zero' | 'stdout_matches' | 'video_fps_above'; + +// Warning: (ae-missing-release-tag) "AcceptSpec" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +interface AcceptSpec { + description?: string; + // Warning: (ae-forgotten-export) The symbol "AcceptPredicateName" needs to be exported by the entry point builtin.d.ts + name: AcceptPredicateName; + params: Record; + safetyCritical?: boolean; +} + +// @beta +interface AcquireExecutionLeaseInput { + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly ttlMs?: number; +} + +// @beta +interface AppendExecutionEventInput { + // (undocumented) + readonly data?: Readonly>; + // (undocumented) + readonly expectedRevision: number; + // (undocumented) + readonly id?: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly time?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly type: Exclude; +} + // Warning: (ae-forgotten-export) The symbol "Tool" needs to be exported by the entry point builtin.d.ts // Warning: (ae-missing-release-tag) "applyPatchTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -32,6 +74,30 @@ export const builtinTools: Tool[]; // @public (undocumented) export const bundledBochaKey: string | undefined; +// @beta +interface CreateExecutionGraphInput { + // Warning: (ae-forgotten-export) The symbol "ExecutionBudget" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeDefinition" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly nodes?: readonly ExecutionNodeDefinition[]; + // (undocumented) + readonly now?: number; + // Warning: (ae-forgotten-export) The symbol "OrchestrationPolicy" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly policy?: Partial; + // (undocumented) + readonly sessionId?: string; +} + // Warning: (ae-missing-release-tag) "editFileTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -42,11 +108,258 @@ export const editFileTool: Tool; // @public (undocumented) export const execTool: Tool; +// @beta +interface ExecutionBudget { + // (undocumented) + readonly maxCostUsd?: number; + // (undocumented) + readonly maxTokens?: number; + // (undocumented) + readonly maxWallTimeMs?: number; + // (undocumented) + readonly usedCostUsd?: number; + // (undocumented) + readonly usedTokens?: number; + // (undocumented) + readonly usedWallTimeMs?: number; +} + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) type ExecutionDomain = 'local' | 'simulation' | 'real'; +// @beta +interface ExecutionEvent { + // (undocumented) + readonly data: Readonly>; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly seq: number; + // (undocumented) + readonly time: number; + // (undocumented) + readonly type: ExecutionEventType; +} + +// @beta +type ExecutionEventType = 'graph.created' | 'graph.ready' | 'graph.resumed' | 'graph.paused' | 'graph.recovered' | 'graph.blocked' | 'graph.completed' | 'graph.failed' | 'graph.cancelled' | 'plan.revised' | 'node.added' | 'node.ready' | 'node.leased' | 'node.started' | 'node.progressed' | 'node.succeeded' | 'node.failed' | 'node.interrupted' | 'node.retry_requested' | 'node.blocked' | 'node.skipped' | 'node.merge_conflict' | 'node.cancelled' | 'evidence.recorded' | 'budget.updated' | 'steering.recorded' | 'verification.recorded'; + +// @beta +interface ExecutionEvidence { + // (undocumented) + readonly artifactRef?: string; + // (undocumented) + readonly createdAt: number; + // (undocumented) + readonly digest?: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidenceKind" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly kind: ExecutionEvidenceKind; + // (undocumented) + readonly metadata?: Readonly>; + // (undocumented) + readonly nodeId?: string; + // (undocumented) + readonly summary: string; +} + +// @beta +type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; + +// @beta +interface ExecutionGraphSnapshot { + // (undocumented) + readonly budget: ExecutionBudget; + // (undocumented) + readonly createdAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly evidence: readonly ExecutionEvidence[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly nodes: Readonly>; + // (undocumented) + readonly policy: OrchestrationPolicy; + // Warning: (ae-forgotten-export) The symbol "ExecutionRecovery" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly recovery?: ExecutionRecovery; + // (undocumented) + readonly revision: number; + // (undocumented) + readonly sessionId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphStatus" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly status: ExecutionGraphStatus; + // (undocumented) + readonly updatedAt: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionVerification" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly verification?: ExecutionVerification; +} + +// @beta +type ExecutionGraphStatus = 'paused' | 'ready' | 'running' | 'paused_recovered' | 'blocked' | 'completed' | 'failed' | 'cancelled'; + +// @beta +interface ExecutionNode extends ExecutionNodeDefinition { + // (undocumented) + readonly attempts: number; + // (undocumented) + readonly blockedByDependencies?: readonly string[]; + // (undocumented) + readonly completedAt?: number; + // (undocumented) + readonly consecutiveSameFailures: number; + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly startedAt?: number; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeStatus" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly status: ExecutionNodeStatus; + // (undocumented) + readonly workspaceLeaseId?: string; +} + +// @beta +interface ExecutionNodeDefinition { + // (undocumented) + readonly acceptanceCriteria?: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly id: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionNodeKind" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + readonly kind: ExecutionNodeKind; + // (undocumented) + readonly requiredCapabilities?: readonly string[]; + // (undocumented) + readonly roleId?: string; + // (undocumented) + readonly title: string; + // (undocumented) + readonly writePaths?: readonly string[]; +} + +// @beta +type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; + +// @beta +type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; + +// @beta +interface ExecutionOwnerLease { + // (undocumented) + readonly acquiredAt: number; + // (undocumented) + readonly expiresAt: number; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly ownerId: string; + // (undocumented) + readonly token: string; +} + +// @beta +interface ExecutionRecovery { + // (undocumented) + readonly blockedMutationNodeIds: readonly string[]; + // (undocumented) + readonly interruptedNodeIds: readonly string[]; + // (undocumented) + readonly recoveredAt: number; + // (undocumented) + readonly requiresUserResume: boolean; +} + +// Warning: (ae-missing-release-tag) "ExecutionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface ExecutionState { + // (undocumented) + completedSteps: number; + // (undocumented) + currentStep: number; + // (undocumented) + isExecuting: boolean; + // (undocumented) + lastError?: string; + // (undocumented) + planId: string; + // (undocumented) + totalSteps: number; +} + +// @beta +interface ExecutionStore { + // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point builtin.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; + // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point builtin.d.ts + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvent" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + events(graphId: string, after?: number): readonly ExecutionEvent[]; + // (undocumented) + list(): readonly ExecutionGraphSnapshot[]; + // (undocumented) + load(graphId: string): ExecutionGraphSnapshot | undefined; + // (undocumented) + releaseLease(graphId: string, lease: ExecutionOwnerLease): void; + // (undocumented) + renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; +} + +// @beta +interface ExecutionVerification { + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly reasons: readonly string[]; + // (undocumented) + readonly verdict: 'verified' | 'rejected' | 'needs_evidence'; + // (undocumented) + readonly verifiedAt: number; +} + // Warning: (ae-missing-release-tag) "extractVerificationFailurePreview" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -100,6 +413,179 @@ export function normalizeSkillSlug(input: unknown): string | null; // @public (undocumented) export function oneLine(input: unknown, fallback?: string): string; +// @beta +interface OrchestrationPolicy { + // (undocumented) + readonly autoResumeReadonly?: boolean; + // (undocumented) + readonly maxAttemptsPerNode: number; + // (undocumented) + readonly maxConcurrency: number; + // (undocumented) + readonly strictCompletion: boolean; +} + +// Warning: (ae-missing-release-tag) "Plan" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface Plan { + // (undocumented) + createdAt: string; + // (undocumented) + currentStep?: number; + // (undocumented) + goal: string; + // (undocumented) + id: string; + // (undocumented) + preconditions?: string[]; + // (undocumented) + rationale?: string; + // (undocumented) + replanCount?: number; + // Warning: (ae-forgotten-export) The symbol "PlanStatus" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + status: PlanStatus; + // Warning: (ae-forgotten-export) The symbol "PlanStep" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + steps: PlanStep[]; + // (undocumented) + successCriteria?: string[]; + // Warning: (ae-forgotten-export) The symbol "AcceptSpec" needs to be exported by the entry point builtin.d.ts + terminalAccept?: AcceptSpec[]; + // (undocumented) + updatedAt: string; + // (undocumented) + version: number; +} + +// @beta +class PlanControllerStore { + // Warning: (ae-forgotten-export) The symbol "Plan" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + getActivePlanForSession(sessionKey: string): Plan | null; + // (undocumented) + getActivePlanId(sessionKey: string): string | undefined; + // Warning: (ae-forgotten-export) The symbol "PlanExecuteController" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + getPlanController(sessionKey: string): PlanExecuteController; + // (undocumented) + getSharedPlanController(): PlanExecuteController; + // (undocumented) + reset(): void; + // (undocumented) + setActivePlanId(sessionKey: string, planId: string): void; +} + +// Warning: (ae-missing-release-tag) "PlanExecuteConfig" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanExecuteConfig { + // (undocumented) + autoApproveSimple?: boolean; + // (undocumented) + maxExecutionTimeMs?: number; + // (undocumented) + maxReplans?: number; + // (undocumented) + requireApproval?: boolean; +} + +// Warning: (ae-missing-release-tag) "PlanExecuteController" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +class PlanExecuteController { + // Warning: (ae-forgotten-export) The symbol "PlanExecuteConfig" needs to be exported by the entry point builtin.d.ts + constructor(config?: PlanExecuteConfig); + // (undocumented) + approvePlan(planId: string): boolean; + // (undocumented) + cancelPlan(planId: string): boolean; + // (undocumented) + completeStep(planId: string, stepNumber: number, actualOutput?: string, actualTools?: string[]): boolean; + // (undocumented) + createPlan(goal: string, steps: Omit[], rationale?: string): Plan; + // (undocumented) + failStep(planId: string, stepNumber: number, error: string): boolean; + // (undocumented) + static formatPlan(plan: Plan): string; + // (undocumented) + getActivePlan(): Plan | null; + // Warning: (ae-forgotten-export) The symbol "ExecutionState" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + getExecutionState(planId: string): ExecutionState | null; + // (undocumented) + getPlan(planId: string): Plan | null; + // (undocumented) + requestReplan(planId: string, _reason: string, revisedSteps?: Omit[]): Plan | null; + // Warning: (ae-forgotten-export) The symbol "PlanReviewResult" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + reviewPlan(planId: string): PlanReviewResult; + // (undocumented) + skipStep(planId: string, stepNumber: number, reason: string): boolean; + // (undocumented) + startExecution(planId: string): boolean; +} + +// Warning: (ae-missing-release-tag) "PlanReviewResult" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanReviewResult { + // (undocumented) + approved: boolean; + // (undocumented) + issues: string[]; + // (undocumented) + revisedPlan?: Plan; + // (undocumented) + suggestions: string[]; +} + +// Warning: (ae-missing-release-tag) "PlanStatus" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +type PlanStatus = 'draft' | 'reviewing' | 'approved' | 'executing' | 'completed' | 'replanning' | 'failed' | 'cancelled'; + +// Warning: (ae-missing-release-tag) "PlanStep" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface PlanStep { + // (undocumented) + actualOutput?: string; + // (undocumented) + actualTools?: string[]; + // (undocumented) + completedAt?: string; + // (undocumented) + dependsOn?: number[]; + // (undocumented) + description: string; + // (undocumented) + error?: string; + // (undocumented) + estimatedTimeSec?: number; + expectedAccept?: string[]; + // (undocumented) + expectedOutput?: string; + // (undocumented) + expectedTools?: string[]; + // (undocumented) + startedAt?: string; + // Warning: (ae-forgotten-export) The symbol "StepStatus" needs to be exported by the entry point builtin.d.ts + // + // (undocumented) + status: StepStatus; + // (undocumented) + step: number; + writePaths?: string[]; +} + // Warning: (ae-missing-release-tag) "readFileTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -112,6 +598,8 @@ export function registerBuiltinTools(agent: { tools: { register: (tool: Tool) => void; }; + planControllerStore?: PlanControllerStore; + executionStore?: ExecutionStore; }): void; // Warning: (ae-missing-release-tag) "runTestsTool" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -139,6 +627,11 @@ export const skillhubInstallTool: Tool; // @public (undocumented) export const skillhubSearchTool: Tool; +// Warning: (ae-missing-release-tag) "StepStatus" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +type StepStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed' | 'blocked'; + // Warning: (ae-missing-release-tag) "stringList" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -287,6 +780,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -300,6 +794,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -313,6 +808,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -396,7 +895,9 @@ export const writeFileTool: Tool; // Warnings were encountered during analysis: // -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point builtin.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point builtin.d.ts +// src/tools/builtin.ts:517:3 - (ae-forgotten-export) The symbol "PlanControllerStore" needs to be exported by the entry point builtin.d.ts +// src/tools/builtin.ts:518:3 - (ae-forgotten-export) The symbol "ExecutionStore" needs to be exported by the entry point builtin.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/vision.api.md b/packages/moss-agent/etc/vision.api.md index abfe6caf..16d69ab5 100644 --- a/packages/moss-agent/etc/vision.api.md +++ b/packages/moss-agent/etc/vision.api.md @@ -159,6 +159,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -172,6 +173,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -185,6 +187,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -321,7 +327,7 @@ export interface VisionToolOptions { // Warnings were encountered during analysis: // -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/web-browser.api.md b/packages/moss-agent/etc/web-browser.api.md index a49dcb4e..f530c218 100644 --- a/packages/moss-agent/etc/web-browser.api.md +++ b/packages/moss-agent/etc/web-browser.api.md @@ -203,6 +203,7 @@ interface ToolContext { // (undocumented) spawnSubagent?: (params: { task: string; + writePaths?: readonly string[]; label?: string; cleanup?: 'keep' | 'delete'; scope?: string; @@ -216,6 +217,7 @@ interface ToolContext { tasks?: Array<{ task: string; scope?: string; + writePaths?: readonly string[]; allowedTools?: readonly string[]; }>; abortSignal?: AbortSignal; @@ -229,6 +231,10 @@ interface ToolContext { toolResults?: number; durationMs?: number; error?: string; + workspaceLeaseId?: string; + patchRef?: string; + patchDigest?: string; + changedPaths?: readonly string[]; }>; // (undocumented) toolCallId?: string; @@ -414,7 +420,7 @@ export interface WebBrowserTask { // Warnings were encountered during analysis: // -// src/core/tools/tool-types.ts:66:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts +// src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/src/cli/acp-server.ts b/packages/moss-agent/src/cli/acp-server.ts index e350abe8..34584507 100644 --- a/packages/moss-agent/src/cli/acp-server.ts +++ b/packages/moss-agent/src/cli/acp-server.ts @@ -15,6 +15,7 @@ * for text/thought, session/toolCall for tool calls), * return final result * - session/cancel → abort the active prompt + * - task/list|inspect|resume|retry|stop → durable execution graph control * * stderr is reserved for logs; stdout carries only NDJSON protocol messages. * @public @@ -159,6 +160,21 @@ async function dispatch( return handleSessionPrompt(agent, active, notify, req.params); case 'session/cancel': return handleSessionCancel(active, req.params); + case 'task/list': + return { tasks: agent.tasks.list() }; + case 'task/inspect': + return agent.tasks.inspect(requiredTaskId(req.params)); + case 'task/resume': + return agent.tasks.resume(requiredTaskId(req.params)); + case 'task/retry': { + const taskId = requiredTaskId(req.params); + const nodeId = String(req.params?.nodeId ?? '').trim(); + if (!nodeId) + throw { code: INVALID_PARAMS.code, message: 'task/retry requires nodeId', acpError: true }; + return agent.tasks.retry(taskId, nodeId); + } + case 'task/stop': + return agent.tasks.stop(requiredTaskId(req.params)); default: throw { code: METHOD_NOT_FOUND.code, @@ -178,10 +194,18 @@ function handleInitialize() { sessionCancel: true, toolStream: true, thoughtStream: true, + taskControl: true, }, }; } +function requiredTaskId(params: Record | null | undefined): string { + const taskId = String(params?.taskId ?? '').trim(); + if (!taskId) + throw { code: INVALID_PARAMS.code, message: 'task method requires taskId', acpError: true }; + return taskId; +} + function handleSessionNew(agent: MossAgent) { // A new session is created implicitly by writing to its key; reserve one // so session/prompt can append. Use the same key shape as the CLI. diff --git a/packages/moss-agent/src/cli/tui.ts b/packages/moss-agent/src/cli/tui.ts index 05592a24..499b7c8b 100644 --- a/packages/moss-agent/src/cli/tui.ts +++ b/packages/moss-agent/src/cli/tui.ts @@ -4161,9 +4161,6 @@ export function MossTui({ activateGoalActivity(result.goal); scheduleGoalContinuationRef.current(); } else if (!result.error && result.action === 'set' && result.vague) { - // Goal was too vague to commit. Instead of leaving the user with a static - // rejection message, trigger a quick LLM turn: ask the model to help the - // user clarify the goal into a concrete, actionable objective. const zh = isZhLocale(); const clarifyPrompt = zh ? `用户尝试设置 goal:"${result.objective ?? ''}",但目标还不够具体,无法自主执行。请帮用户明确这个目标:提问 2-3 个针对性问题,问清楚(1)期望的具体完成状态是什么、(2)涉及哪些文件/模块/范围、(3)有什么约束。回答要简短,让用户直接回答问题,然后用 /goal set <明确的目标> 重新设置。` @@ -6112,9 +6109,7 @@ export async function runInkInteractive( try { const mode = await detectTerminalBackgroundMode({ timeoutMs: 250 }); if (mode) applyTerminalThemeMode(mode); - } catch { - // Probe failure is non-fatal — keep the env-resolved default palette. - } + } catch {} } const instance = render( React.createElement(MossTui, { @@ -6136,5 +6131,4 @@ export async function runInkInteractive( await instance.waitUntilExit(); } -// Re-exported for legacy callers that imported transcriptColor. export { transcriptColor }; diff --git a/packages/moss-agent/src/core/agent/agent-orchestration-services.ts b/packages/moss-agent/src/core/agent/agent-orchestration-services.ts new file mode 100644 index 00000000..6a0acc5e --- /dev/null +++ b/packages/moss-agent/src/core/agent/agent-orchestration-services.ts @@ -0,0 +1,36 @@ +import path from 'node:path'; +import { createInMemoryMossAsyncTaskRegistry } from '@rdk-moss/core/contracts/async-task'; +import { PlanControllerStore } from '../../plan-execute/plan-controller-store.js'; +import { AdaptiveWorkspaceLeaseAdapter } from '../../orchestration/adaptive-workspace-lease.js'; +import { CompletionArbiter } from '../../orchestration/completion-arbiter.js'; +import { ExecutionBackedAsyncTaskRegistry } from '../../orchestration/execution-async-task-registry.js'; +import { DEFAULT_ORCHESTRATION_POLICY } from '../../orchestration/execution-projector.js'; +import { ExecutionTaskController } from '../../orchestration/execution-task-controller.js'; +import { JsonlExecutionStore } from '../../orchestration/jsonl-execution-store.js'; +import { getMossWorkspacePaths } from '../../utils/workspace-paths.js'; +import type { MossAgentConfig } from './moss-agent-types.js'; + +export function createAgentOrchestrationServices(config: MossAgentConfig, workspaceDir: string) { + const runtimeDir = getMossWorkspacePaths(workspaceDir).runtimeDir; + const executionStore = + config.executionStore ?? + new JsonlExecutionStore({ rootDir: path.join(runtimeDir, 'executions') }); + + return { + executionStore, + asyncTasks: new ExecutionBackedAsyncTaskRegistry( + config.asyncTaskRegistry ?? createInMemoryMossAsyncTaskRegistry(), + executionStore + ), + tasks: new ExecutionTaskController(executionStore), + completionArbiter: new CompletionArbiter(executionStore), + orchestrationPolicy: { + ...DEFAULT_ORCHESTRATION_POLICY, + ...config.orchestrationPolicy, + }, + workspaceLeaseAdapter: + config.workspaceLeaseAdapter ?? + new AdaptiveWorkspaceLeaseAdapter({ rootDir: path.join(runtimeDir, 'workspaces') }), + planControllerStore: config.planControllerStore ?? new PlanControllerStore(), + }; +} diff --git a/packages/moss-agent/src/core/agent/moss-agent.ts b/packages/moss-agent/src/core/agent/moss-agent.ts index 59303d4f..e0ce09a1 100644 --- a/packages/moss-agent/src/core/agent/moss-agent.ts +++ b/packages/moss-agent/src/core/agent/moss-agent.ts @@ -16,10 +16,7 @@ import { DEFAULT_MODEL, } from '@rdk-moss/core'; import type { KnowledgeModule } from '@rdk-moss/core'; -import { - createInMemoryMossAsyncTaskRegistry, - type MossAsyncTaskRegistry, -} from '@rdk-moss/core/contracts/async-task'; +import type { MossAsyncTaskRegistry } from '@rdk-moss/core/contracts/async-task'; import { compactHistoryIfNeeded, type SummarizeFn } from '../../context/compaction.js'; import { createRemoteCompactProviderFromEnv } from '../../context/remote-compaction.js'; import { setTraceRedactor, startSpan, sessionAttributes } from '../../observability/tracing.js'; @@ -75,7 +72,7 @@ import { clearPendingStructuredValidation, } from '../../structured-output/structured-output-tool.js'; import { evaluatePlanCompletionGate } from '../../plan-execute/plan-completion-gate.js'; -import { PlanControllerStore } from '../../plan-execute/plan-controller-store.js'; +import type { PlanControllerStore } from '../../plan-execute/plan-controller-store.js'; import { createSpawnProfileRegistryFromDefaults, type SpawnProfileRegistry, @@ -124,18 +121,15 @@ import { initializeEpoch, reconcileEpoch, type ContextSources } from '../session import { loadContextEpoch, saveContextEpoch } from '../session/context-epoch-store.js'; import { sanitizeSecrets } from '../../safety/secret-sanitizer.js'; import { MossError, ErrorCode, errorMessage, isMossError } from '../../errors.js'; -import { JsonlExecutionStore } from '../../orchestration/jsonl-execution-store.js'; -import { AdaptiveWorkspaceLeaseAdapter } from '../../orchestration/adaptive-workspace-lease.js'; -import { ExecutionTaskController } from '../../orchestration/execution-task-controller.js'; -import { CompletionArbiter } from '../../orchestration/completion-arbiter.js'; -import { ExecutionBackedAsyncTaskRegistry } from '../../orchestration/execution-async-task-registry.js'; -import { DEFAULT_ORCHESTRATION_POLICY } from '../../orchestration/execution-projector.js'; import type { AgentRoleRegistry, + CompletionArbiter, ExecutionStore, + ExecutionTaskController, OrchestrationPolicy, WorkspaceLeaseAdapter, } from '../../orchestration/index.js'; +import { createAgentOrchestrationServices } from './agent-orchestration-services.js'; import type { MossAgentConfig as SharedMossAgentConfig, ChatOptions as SharedChatOptions, @@ -241,27 +235,14 @@ export class MossAgent { this.expertRegistry = config.subagentExpertRegistry ?? new SubagentExpertRegistry(config.subagentExperts); const workspaceDir = path.resolve(config.workspaceDir ?? process.cwd()); - this.executionStore = - config.executionStore ?? - new JsonlExecutionStore({ - rootDir: path.join(getMossWorkspacePaths(workspaceDir).runtimeDir, 'executions'), - }); - this.asyncTasks = new ExecutionBackedAsyncTaskRegistry( - config.asyncTaskRegistry ?? createInMemoryMossAsyncTaskRegistry(), - this.executionStore - ); - this.tasks = new ExecutionTaskController(this.executionStore); - this.completionArbiter = new CompletionArbiter(this.executionStore); - this.orchestrationPolicy = { - ...DEFAULT_ORCHESTRATION_POLICY, - ...config.orchestrationPolicy, - }; - this.workspaceLeaseAdapter = - config.workspaceLeaseAdapter ?? - new AdaptiveWorkspaceLeaseAdapter({ - rootDir: path.join(getMossWorkspacePaths(workspaceDir).runtimeDir, 'workspaces'), - }); - this.planControllerStore = config.planControllerStore ?? new PlanControllerStore(); + const orchestration = createAgentOrchestrationServices(config, workspaceDir); + this.executionStore = orchestration.executionStore; + this.asyncTasks = orchestration.asyncTasks; + this.tasks = orchestration.tasks; + this.completionArbiter = orchestration.completionArbiter; + this.orchestrationPolicy = orchestration.orchestrationPolicy; + this.workspaceLeaseAdapter = orchestration.workspaceLeaseAdapter; + this.planControllerStore = orchestration.planControllerStore; this.toolHooks = new ToolHookRegistry(); this.toolHooks.registerPost(createSecretSanitizerHook(sanitizeSecrets)); setTraceRedactor(sanitizeSecrets); @@ -365,9 +346,7 @@ export class MossAgent { parts.push(buildRoboticsEngineeringPrompt()); } - // The compact behavior contract is the default — it carries only the - // safety-critical lines the model cannot infer on its own. Hosts that - // want the full long-form prose pass `includeAgentBehaviorPrompt: 'full'`. + // Hosts can opt into the long-form behavior prompt with `includeAgentBehaviorPrompt: 'full'`. if (this.config.includeAgentBehaviorPrompt !== false) { parts.push( this.config.includeAgentBehaviorPrompt === 'full' @@ -910,26 +889,15 @@ export class MossAgent { : new Error('Compaction cancelled.'); } const store = this.config.sessionStore; - // Use the configured context window. Fall back to a conservative 32k - // default — NOT 200k, which would be dangerously optimistic for small - // models. The real value should have been probed and set in - // resolvedConfig before this path is reached (cli-main startup probe). + // The startup probe normally supplies this; 32k is the safe fallback for small models. const contextTokens = this.config.contextTokens ?? 32_000; const maxOutputTokens = this.config.maxTokens ?? 4096; const effectiveContextTokens = getEffectiveContextWindowTokens(contextTokens, maxOutputTokens); - // SessionStore stores messages as a generic Message[] (session-jsonl format). - // InternalMessage and LLMMessage are structurally compatible (both have role + content) - // but TS cannot verify the relationship. These casts bridge the store boundary. const loaded = (await store.loadMessages(sessionKey)) as unknown as InternalMessage[]; if (loaded.length === 0) { return { compacted: false, summaryChars: 0, droppedMessages: 0, tokensAfter: 0 }; } - // Extract the goal checkpoint BEFORE compaction so it isn't pruned away, - // then re-attach it after. Without this, /compact on a session with an - // active goal could drop the goal checkpoint from the persisted history; - // a later resume would lose the goal state from the LLM context (the - // in-memory goal cache would keep the agent running toward it, but the - // persisted state would be silently inconsistent). + // Preserve the goal checkpoint across compaction so persisted and in-memory state agree. const goalSplit = splitGoalCheckpointMessages(loaded as unknown as LLMMessage[]); const goalCheckpoint = goalSplit.goal ? createGoalCheckpointMessage(goalSplit.goal) : undefined; const sessionMessages = toSessionMessages(goalSplit.messages as unknown as InternalMessage[]); @@ -968,23 +936,17 @@ export class MossAgent { tokensAfter: Math.max(0, Math.round(estimateMessagesTokens(sessionMessages))), }; } - // Keep the goal checkpoint immediately after the summary and before the - // retained recent conversation. The checkpoint is a `role: user` message; - // appending it at the very end made it newer than the user's latest - // constraint and could silently override that instruction after compaction. + // Place the checkpoint before recent messages so it cannot override newer constraints. const nextWithGoal = goalCheckpoint ? [result.summaryMessage, goalCheckpoint, ...result.pruneResult.messages] : [result.summaryMessage, ...result.pruneResult.messages]; - // InternalMessage[] -> LLMMessage[] for store persistence (see line 764). await store.replaceMessages(sessionKey, nextWithGoal as unknown as LLMMessage[]); return { compacted: true, summary: result.summary, summaryChars: result.summary.length, droppedMessages: result.pruneResult.droppedMessages.length, - // The store boundary accepts LLMMessage-shaped checkpoints alongside - // session messages. Normalize that exact persisted array before counting - // so the user-visible number includes the re-attached goal checkpoint. + // Count the exact persisted array, including the restored checkpoint. tokensAfter: Math.max( 0, Math.round( @@ -994,21 +956,7 @@ export class MossAgent { }; } - /** - * Rewind the conversation (LLM context) to the first `toMessageCount` - * messages, discarding everything after — the grok-style conversation - * rewind that complements /rewind's file restore. The agent's next turn - * loads the truncated store, so it continues from before the rewound turn. - * - * The goal checkpoint is preserved if it falls within the kept slice; if - * the rewind drops it (the user rewound past goal creation), the persisted - * goal is removed. (In-memory goal-cache desync in that edge case is - * resolved on the next goal read, which reloads from the store.) - * - * The visual transcript (session event log) is NOT truncated — like - * /compact, the TUI keeps the history as a record and shows a "rewound" - * system line. - */ + /** Rewind persisted LLM context while retaining the visual transcript. */ async rewindConversation( sessionKey: string, toMessageCount: number @@ -1565,22 +1513,10 @@ export class MossAgent { Boolean(peekPendingStructuredValidation(sessionKey, runId)) || this.config.bufferAssistantUntilComplete === true, completionGate: async (request) => { - // Host-side structured-output enforcement: if generate_structured was - // called (non-validateOnly) during this run, the schema is in the - // pending registry. After the LLM produces its final text (which should - // contain JSON), validate it here. If invalid, reject with the enforcer's - // retry feedback so the agent loop injects a correction message and - // retries — automatic, no reliance on the LLM self-validating. + // Validate pending structured output here and feed failures back into the retry loop. const pending = peekPendingStructuredValidation(sessionKey, request.runId); if (!pending) { - // No structured validation pending — delegate to the user-provided - // completion gate (if any). - // Plan-completion-gate: only evaluate when the host provides a plan - // store. The Studio host sets hostProvidesPlanStore=false because its plans - // are display-only constraints and it never calls setActivePlanId — - // getActivePlanForSession always returns null there, so the gate - // would fail-open anyway. Making the skip explicit avoids unnecessary - // module-level Map lookups and is debuggable via config.debug. + // Evaluate plan completion only for hosts that own an executable plan store. if (request.sessionKey && this.config.hostProvidesPlanStore !== false) { const planGate = evaluatePlanCompletionGate( { sessionKey: request.sessionKey, stopReason: request.stopReason }, diff --git a/packages/moss-agent/src/core/plugins/plugin-host.ts b/packages/moss-agent/src/core/plugins/plugin-host.ts index 6a45e749..2ed1317a 100644 --- a/packages/moss-agent/src/core/plugins/plugin-host.ts +++ b/packages/moss-agent/src/core/plugins/plugin-host.ts @@ -10,6 +10,10 @@ import { validatePluginId, validateStagedContributions, } from './plugin-host-validation.js'; +import { MOSS_WEB_SLOTS, type MossWebSlot } from './plugin-web-slots.js'; + +export { MOSS_WEB_SLOTS } from './plugin-web-slots.js'; +export type { MossWebSlot } from './plugin-web-slots.js'; /** Lifecycle state exposed by the deterministic plugin inspector. @beta */ export type MossPluginState = 'loading' | 'active' | 'unloading' | 'failed' | 'disposed'; @@ -65,24 +69,6 @@ export interface MossPluginMcpPreset { }; } -/** Stable browser extension slots available to trusted runtime plugins. @beta */ -export const MOSS_WEB_SLOTS = [ - 'navigation.primary', - 'navigation.session', - 'navigation.footer', - 'conversation.header', - 'conversation.message', - 'conversation.composer', - 'conversation.details', - 'tool.inline', - 'tool.details', - 'settings.section', - 'settings.plugin', -] as const; - -/** Stable browser extension slot available to trusted runtime plugins. @beta */ -export type MossWebSlot = (typeof MOSS_WEB_SLOTS)[number]; - /** Declarative browser contribution owned by one trusted runtime plugin. @beta */ export interface MossWebContribution { readonly id: string; diff --git a/packages/moss-agent/src/core/plugins/plugin-web-slots.ts b/packages/moss-agent/src/core/plugins/plugin-web-slots.ts new file mode 100644 index 00000000..ee9d18b0 --- /dev/null +++ b/packages/moss-agent/src/core/plugins/plugin-web-slots.ts @@ -0,0 +1,17 @@ +/** Stable browser extension slots available to trusted runtime plugins. @beta */ +export const MOSS_WEB_SLOTS = [ + 'navigation.primary', + 'navigation.session', + 'navigation.footer', + 'conversation.header', + 'conversation.message', + 'conversation.composer', + 'conversation.details', + 'tool.inline', + 'tool.details', + 'settings.section', + 'settings.plugin', +] as const; + +/** Stable browser extension slot available to trusted runtime plugins. @beta */ +export type MossWebSlot = (typeof MOSS_WEB_SLOTS)[number]; diff --git a/packages/moss-agent/test/acp-server.spec.mjs b/packages/moss-agent/test/acp-server.spec.mjs index ab76897f..0cf1d0f6 100644 --- a/packages/moss-agent/test/acp-server.spec.mjs +++ b/packages/moss-agent/test/acp-server.spec.mjs @@ -35,8 +35,16 @@ function fakeSessionStore() { /** Minimal fake agent: config.sessionStore + streamChat yielding fixed events. */ function fakeAgent(eventsForPrompt) { + const graph = { id: 'task-1', revision: 1, status: 'paused' }; return { config: { sessionStore: fakeSessionStore() }, + tasks: { + list: () => [graph], + inspect: () => graph, + resume: () => ({ ...graph, revision: 2, status: 'running' }), + retry: (_taskId, nodeId) => ({ ...graph, retriedNodeId: nodeId }), + stop: () => ({ ...graph, revision: 2, status: 'cancelled' }), + }, async *streamChat(sessionId, prompt, opts) { for (const e of eventsForPrompt(prompt)) { if (opts?.abortSignal?.aborted) break; @@ -83,6 +91,27 @@ test('session/new returns a sessionId', async () => { assert.ok(out[0].result.sessionId.startsWith('cli-'), 'sessionId is a cli- key'); }); +test('ACP exposes the same durable task identity and control actions', async () => { + const out = await runServer( + fakeAgent(() => []), + [ + { jsonrpc: '2.0', id: 1, method: 'task/list' }, + { jsonrpc: '2.0', id: 2, method: 'task/inspect', params: { taskId: 'task-1' } }, + { jsonrpc: '2.0', id: 3, method: 'task/resume', params: { taskId: 'task-1' } }, + { + jsonrpc: '2.0', + id: 4, + method: 'task/retry', + params: { taskId: 'task-1', nodeId: 'node-1' }, + }, + ] + ); + assert.equal(out.find((message) => message.id === 1).result.tasks[0].id, 'task-1'); + assert.equal(out.find((message) => message.id === 2).result.revision, 1); + assert.equal(out.find((message) => message.id === 3).result.status, 'running'); + assert.equal(out.find((message) => message.id === 4).result.retriedNodeId, 'node-1'); +}); + test('session/prompt streams text + tool notifications and returns the final text', async () => { const events = (prompt) => [ { type: 'text_delta', delta: 'Hello ' }, diff --git a/scripts/check-agent-harness-benchmark.mjs b/scripts/check-agent-harness-benchmark.mjs index 004335f9..e1a4265b 100644 --- a/scripts/check-agent-harness-benchmark.mjs +++ b/scripts/check-agent-harness-benchmark.mjs @@ -3,6 +3,7 @@ import benchmark from '../benchmarks/agent-harness-real-world.mjs'; import sourceIndex from '../benchmarks/agent-harness-source-index.mjs'; import results from '../benchmarks/agent-harness-results.json' with { type: 'json' }; import efficiency from '../benchmarks/agent-efficiency-results.json' with { type: 'json' }; +import longHorizon from '../benchmarks/long-horizon-results.json' with { type: 'json' }; assert.equal(benchmark.cases.length, 200, 'benchmark must contain exactly 200 cases'); assert.equal( @@ -70,3 +71,18 @@ for (const run of efficiency.runs) { console.log( `Efficiency runs: ${efficiency.runs.length} across ${new Set(efficiency.runs.map((run) => run.taskId)).size} task(s).` ); + +assert.deepEqual(longHorizon.totals, { long_task_loop: 10, subagents_concurrency: 10 }); +assert.equal(new Set(longHorizon.results.map((result) => result.caseId)).size, 20); +for (const result of longHorizon.results) { + assert.equal(result.passed, true, `${result.caseId}: real acceptance run failed`); + assert.ok(result.revision > 1, `${result.caseId}: missing graph event evidence`); + if (result.suite === 'long_task_loop') { + assert.equal(result.verdict, 'verified', `${result.caseId}: completion was not verified`); + assert.ok(result.evidenceIds.length >= 3, `${result.caseId}: missing execution evidence`); + } else { + assert.equal(result.actualMaxConcurrency, 4, `${result.caseId}: did not run four roles`); + assert.equal(result.startedNodeIds.length, 4, `${result.caseId}: missing role starts`); + } +} +console.log('Long-horizon runtime evidence: long_task_loop=10/10, subagents_concurrency=10/10.'); diff --git a/scripts/config/maintainability-baseline.json b/scripts/config/maintainability-baseline.json index 39fd0ecd..2bad8b18 100644 --- a/scripts/config/maintainability-baseline.json +++ b/scripts/config/maintainability-baseline.json @@ -31,7 +31,7 @@ "reason": "Legacy hotspot recorded at initial maintainability-ratchet adoption." }, "packages/moss-agent/src/cli/tui.ts": { - "maxLines": 6135, + "maxLines": 6134, "reason": "Legacy hotspot recorded at initial maintainability-ratchet adoption." }, "packages/moss-agent/src/cli/tui-utils.ts": { @@ -47,7 +47,7 @@ "reason": "Legacy hotspot recorded at initial maintainability-ratchet adoption." }, "packages/moss-agent/src/core/agent/moss-agent.ts": { - "maxLines": 2016, + "maxLines": 2009, "reason": "Legacy hotspot recorded at initial maintainability-ratchet adoption." }, "packages/moss-agent/src/core/loop/agent-loop.ts": { From 80ff6bbd00ce09435f3e662ee24ce495959e723d Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 20:09:37 +0800 Subject: [PATCH 13/21] feat(agent): enforce durable long-horizon execution --- CHANGELOG.md | 11 +- benchmarks/long-horizon-results.json | 88 ++--- benchmarks/run-long-horizon-acceptance.mjs | 83 +++-- docs/user-guide/25-long-horizon-tasks.md | 8 + package.json | 4 +- packages/moss-agent/API.md | 13 + packages/moss-agent/etc/channels.api.md | 270 +++++++++++++- packages/moss-agent/etc/context.api.md | 6 + packages/moss-agent/etc/core.api.md | 270 +++++++++++++- packages/moss-agent/etc/eval.api.md | 6 + packages/moss-agent/etc/mcp.api.md | 6 + packages/moss-agent/etc/mesh.api.md | 6 + packages/moss-agent/etc/orchestration.api.md | 60 ++- packages/moss-agent/etc/plan-execute.api.md | 20 +- packages/moss-agent/etc/root.api.md | 119 +++++- packages/moss-agent/etc/runtime.api.md | 270 +++++++++++++- .../moss-agent/etc/structured-output.api.md | 6 + packages/moss-agent/etc/teaching.api.md | 6 + packages/moss-agent/etc/tools__builtin.api.md | 24 +- packages/moss-agent/etc/vision.api.md | 6 + packages/moss-agent/etc/web-browser.api.md | 6 + .../src/core/agent/agent-execution-runtime.ts | 347 ++++++++++++++++++ .../agent/agent-legacy-execution-import.ts | 26 ++ .../src/core/agent/agent-loop-run-state.ts | 23 ++ .../agent/agent-orchestration-services.ts | 42 ++- packages/moss-agent/src/core/agent/index.ts | 1 + .../moss-agent/src/core/agent/moss-agent.ts | 157 ++++---- packages/moss-agent/src/core/index.ts | 8 +- .../core/subagent/subagent-orchestrator.ts | 1 + .../src/core/subagent/subagent-runner.ts | 1 + .../src/core/task-run/task-run-ledger.ts | 16 +- .../moss-agent/src/core/tools/tool-types.ts | 5 + packages/moss-agent/src/index.ts | 13 +- .../orchestration/adaptive-workspace-lease.ts | 13 + .../src/orchestration/agent-role-registry.ts | 7 +- .../src/orchestration/agent-role-types.ts | 22 +- .../src/orchestration/completion-arbiter.ts | 21 +- .../completion-authority-internal.ts | 7 + .../execution-async-task-registry.ts | 11 +- .../execution-graph-scheduler.ts | 269 ++++++++++---- .../src/orchestration/execution-projector.ts | 78 +++- .../src/orchestration/execution-recovery.ts | 11 +- .../src/orchestration/execution-types.ts | 21 +- .../in-memory-execution-store.ts | 55 ++- .../moss-agent/src/orchestration/index.ts | 5 + .../orchestration/jsonl-execution-store.ts | 67 +++- .../legacy-execution-importer.ts | 18 +- .../orchestration/workspace-lease-adapter.ts | 102 ++++- .../orchestration/workspace-lease-types.ts | 3 + .../src/plan-execute/plan-execution-graph.ts | 12 +- packages/moss-agent/src/tools/builtin.ts | 2 + .../moss-agent/src/tools/create-subagent.ts | 2 + .../src/tools/merge-subagent-patch.ts | 34 ++ .../src/web-ui/client/details-panel.tsx | 22 ++ .../test/completion-arbiter.spec.mjs | 104 +++++- .../test/execution-graph-scheduler.spec.mjs | 133 ++++++- .../moss-agent/test/execution-graph.spec.mjs | 88 +++++ .../test/legacy-execution-importer.spec.mjs | 23 ++ .../test/merge-subagent-patch.spec.mjs | 34 ++ .../moss-agent-durable-orchestration.spec.mjs | 255 ++++++++++++- .../test/plan-execution-graph.spec.mjs | 17 + .../task-run-execution-projection.spec.mjs | 7 +- .../moss-agent/test/web-ui-browser.spec.mjs | 1 + .../moss-agent/test/workspace-lease.spec.mjs | 33 ++ scripts/config/maintainability-baseline.json | 2 +- 65 files changed, 3023 insertions(+), 384 deletions(-) create mode 100644 packages/moss-agent/src/core/agent/agent-execution-runtime.ts create mode 100644 packages/moss-agent/src/core/agent/agent-legacy-execution-import.ts create mode 100644 packages/moss-agent/src/core/agent/agent-loop-run-state.ts create mode 100644 packages/moss-agent/src/orchestration/completion-authority-internal.ts create mode 100644 packages/moss-agent/src/tools/merge-subagent-patch.ts create mode 100644 packages/moss-agent/test/merge-subagent-patch.spec.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c4ab96..90b53d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Categories: **Added** · **Changed** · **Fixed** · **Removed** · **Internal** ### Fixed +- **Repeated legacy TaskFrame recovery**: migration markers now resolve their persisted graph ID, + so a later turn with a new run ID reuses the original execution graph instead of failing the Web + conversation before approval or tool execution. - **Cross-platform plugin process lifecycle**: active Worker RPC calls now keep their worker alive until every pending promise settles, and exact-version plugin installation runs npm's JavaScript CLI directly on Windows instead of failing through the `npm.cmd` shim. @@ -23,7 +26,13 @@ Categories: **Added** · **Changed** · **Fixed** · **Removed** · **Internal** deterministic evidence-bound completion arbitration, and safe legacy checkpoint import. CLI/TUI `/tasks` and `/task inspect|resume|retry|stop` plus the Web details panel expose the same graph identity, revision, nodes, evidence, recovery state, and verdict. Recorded runtime acceptance now - requires `long_task_loop` 10/10 and `subagents_concurrency` 10/10. + requires `long_task_loop` 10/10 and `subagents_concurrency` 10/10. Scheduler owner leases now fence + every event for the complete execution wave and abort workers on stop or lease loss; capability + routing and structured synthesis run through `MossAgent`; routed coding work provisions an + isolated lease, requires explicit host merge approval and a distinct zero-exit verifier receipt; + direct store callers cannot forge arbiter terminal events. Isolated patches merge only through + adapter-verified approval paths. The acceptance gate now exercises the packaged CLI and real + loopback Web task API while emitting reproducible tracked evidence. - **Complete local Web workbench**: `moss web` now provides the responsive three-column Moss design system, durable searchable sessions, resume/rename/export/delete/fork/rewind, cursor-based SSE, diff --git a/benchmarks/long-horizon-results.json b/benchmarks/long-horizon-results.json index e9a3703b..3afb057c 100644 --- a/benchmarks/long-horizon-results.json +++ b/benchmarks/long-horizon-results.json @@ -1,10 +1,8 @@ { "schemaVersion": 1, - "generatedAt": "2026-08-18T10:34:14.806Z", - "runtime": { - "node": "v25.9.0", - "platform": "darwin", - "arch": "arm64" + "productPaths": { + "packagedCliSmoke": true, + "webTaskInspection": "10/10" }, "totals": { "long_task_loop": 10, @@ -16,140 +14,130 @@ "caseId": "long_task_loop-01", "passed": true, "graphId": "long-task-1", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "1-analyse", "1-implement", "1-verify" - ], - "durationMs": 74 + ] }, { "suite": "long_task_loop", "caseId": "long_task_loop-02", "passed": true, "graphId": "long-task-2", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "2-analyse", "2-implement", "2-verify" - ], - "durationMs": 67 + ] }, { "suite": "long_task_loop", "caseId": "long_task_loop-03", "passed": true, "graphId": "long-task-3", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "3-analyse", "3-implement", "3-verify" - ], - "durationMs": 56 + ] }, { "suite": "long_task_loop", "caseId": "long_task_loop-04", "passed": true, "graphId": "long-task-4", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "4-analyse", "4-implement", "4-verify" - ], - "durationMs": 48 + ] }, { "suite": "long_task_loop", "caseId": "long_task_loop-05", "passed": true, "graphId": "long-task-5", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "5-analyse", "5-implement", "5-verify" - ], - "durationMs": 53 + ] }, { "suite": "long_task_loop", "caseId": "long_task_loop-06", "passed": true, "graphId": "long-task-6", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "6-analyse", "6-implement", "6-verify" - ], - "durationMs": 73 + ] }, { "suite": "long_task_loop", "caseId": "long_task_loop-07", "passed": true, "graphId": "long-task-7", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "7-analyse", "7-implement", "7-verify" - ], - "durationMs": 63 + ] }, { "suite": "long_task_loop", "caseId": "long_task_loop-08", "passed": true, "graphId": "long-task-8", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "8-analyse", "8-implement", "8-verify" - ], - "durationMs": 55 + ] }, { "suite": "long_task_loop", "caseId": "long_task_loop-09", "passed": true, "graphId": "long-task-9", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "9-analyse", "9-implement", "9-verify" - ], - "durationMs": 56 + ] }, { "suite": "long_task_loop", "caseId": "long_task_loop-10", "passed": true, "graphId": "long-task-10", - "revision": 16, + "revision": 20, "verdict": "verified", "evidenceIds": [ "10-analyse", "10-implement", "10-verify" - ], - "durationMs": 54 + ] }, { "suite": "subagents_concurrency", @@ -163,8 +151,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 18 + ] }, { "suite": "subagents_concurrency", @@ -178,8 +165,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 19 + ] }, { "suite": "subagents_concurrency", @@ -193,8 +179,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 19 + ] }, { "suite": "subagents_concurrency", @@ -208,8 +193,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 21 + ] }, { "suite": "subagents_concurrency", @@ -223,8 +207,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 21 + ] }, { "suite": "subagents_concurrency", @@ -238,8 +221,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 23 + ] }, { "suite": "subagents_concurrency", @@ -253,8 +235,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 23 + ] }, { "suite": "subagents_concurrency", @@ -268,8 +249,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 25 + ] }, { "suite": "subagents_concurrency", @@ -283,8 +263,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 25 + ] }, { "suite": "subagents_concurrency", @@ -298,8 +277,7 @@ "expert-2", "expert-3", "expert-4" - ], - "durationMs": 27 + ] } ] } diff --git a/benchmarks/run-long-horizon-acceptance.mjs b/benchmarks/run-long-horizon-acceptance.mjs index 00ef9be7..653927af 100644 --- a/benchmarks/run-long-horizon-acceptance.mjs +++ b/benchmarks/run-long-horizon-acceptance.mjs @@ -3,23 +3,31 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; -import { - CompletionArbiter, - ExecutionGraphScheduler, - JsonlExecutionStore, - InMemoryExecutionStore, -} from '../packages/moss-agent/dist/orchestration/index.js'; +import { MossAgent } from '../packages/moss-agent/dist/index.js'; +import { startMossWebServer } from '../packages/moss-agent/dist/web-ui/web-server.js'; const results = []; +function sessionStore() { + return { + loadMessages: async () => [], + appendMessage: async () => {}, + replaceMessages: async () => {}, + }; +} + +function createAgent(workspaceDir) { + return new MossAgent({ workspaceDir, sessionStore: sessionStore() }); +} + async function longTaskCase(index) { - const startedAt = Date.now(); const temp = fs.mkdtempSync(path.join(os.tmpdir(), `moss-long-task-${index}-`)); try { - let store = new JsonlExecutionStore({ rootDir: path.join(temp, 'executions') }); - store.create({ + let agent = createAgent(temp); + agent.executionStore.create({ id: `long-task-${index}`, goal: `Durable long task ${index}`, nodes: [ @@ -28,12 +36,11 @@ async function longTaskCase(index) { { id: 'verify', kind: 'analysis', title: 'verify', dependencies: ['implement'] }, ], }); - let graph = store.load(`long-task-${index}`); - store.append(graph.id, { expectedRevision: graph.revision, type: 'graph.resumed' }); + let graph = agent.tasks.resume(`long-task-${index}`); let executions = 0; + let decision; for (let cycle = 0; cycle < 3; cycle++) { - const scheduler = new ExecutionGraphScheduler(store); - await scheduler.runAvailable(`long-task-${index}`, async (node) => { + const outcome = await agent.runExecutionGraph(`long-task-${index}`, async (node) => { executions += 1; return { success: true, @@ -48,13 +55,25 @@ async function longTaskCase(index) { ], }; }); - // Reopen the durable adapter between every cycle to exercise process-restart reads and CAS. - store = new JsonlExecutionStore({ rootDir: path.join(temp, 'executions') }); + decision = outcome.completion ?? decision; + await agent.close(); + if (cycle < 2) { + agent = createAgent(temp); + graph = agent.tasks.inspect(`long-task-${index}`); + assert.equal(graph.status, 'paused_recovered'); + agent.tasks.resume(graph.id); + } } - const decision = await new CompletionArbiter(store).decide(`long-task-${index}`, { - taskKind: 'analysis', - }); - graph = store.load(`long-task-${index}`); + const webAgent = createAgent(temp); + const web = await startMossWebServer(webAgent, { port: 0 }); + const response = await fetch( + `${web.url}/api/tasks/${encodeURIComponent(`long-task-${index}`)}` + ); + assert.equal(response.status, 200); + const body = await response.json(); + graph = body.task; + await web.close(); + await webAgent.close(); assert.equal(executions, 3); assert.equal(decision.verdict, 'verified'); assert.equal(graph.status, 'completed'); @@ -66,7 +85,6 @@ async function longTaskCase(index) { revision: graph.revision, verdict: decision.verdict, evidenceIds: decision.evidenceIds, - durationMs: Date.now() - startedAt, }); } finally { fs.rmSync(temp, { recursive: true, force: true }); @@ -74,10 +92,10 @@ async function longTaskCase(index) { } async function concurrencyCase(index) { - const startedAt = Date.now(); - const store = new InMemoryExecutionStore(); + const temp = fs.mkdtempSync(path.join(os.tmpdir(), `moss-concurrency-${index}-`)); + const agent = createAgent(temp); const id = `concurrency-${index}`; - store.create({ + agent.executionStore.create({ id, goal: `Concurrent expert team ${index}`, policy: { maxConcurrency: 4 }, @@ -89,11 +107,10 @@ async function concurrencyCase(index) { requiredCapabilities: ['architecture'], })), }); - let graph = store.load(id); - store.append(id, { expectedRevision: graph.revision, type: 'graph.resumed' }); + agent.tasks.resume(id); let active = 0; let maxActive = 0; - const result = await new ExecutionGraphScheduler(store).runAvailable(id, async (node) => { + const { schedule: result } = await agent.runExecutionGraph(id, async (node) => { active += 1; maxActive = Math.max(maxActive, active); await new Promise((resolve) => setTimeout(resolve, 15 + index)); @@ -125,17 +142,25 @@ async function concurrencyCase(index) { revision: result.graph.revision, actualMaxConcurrency: maxActive, startedNodeIds: result.startedNodeIds, - durationMs: Date.now() - startedAt, }); + await agent.close(); + fs.rmSync(temp, { recursive: true, force: true }); } +const cli = spawnSync(process.execPath, ['scripts/smoke-moss-cli.mjs'], { + cwd: path.dirname(path.dirname(fileURLToPath(import.meta.url))), + encoding: 'utf8', + timeout: 180_000, +}); +assert.equal(cli.status, 0, cli.stderr || cli.stdout); +assert.match(cli.stdout, /\[smoke:moss-cli\] PASS/); + for (let index = 1; index <= 10; index++) await longTaskCase(index); for (let index = 1; index <= 10; index++) await concurrencyCase(index); const summary = { schemaVersion: 1, - generatedAt: new Date().toISOString(), - runtime: { node: process.version, platform: process.platform, arch: process.arch }, + productPaths: { packagedCliSmoke: true, webTaskInspection: '10/10' }, totals: { long_task_loop: results.filter((result) => result.suite === 'long_task_loop' && result.passed) .length, diff --git a/docs/user-guide/25-long-horizon-tasks.md b/docs/user-guide/25-long-horizon-tasks.md index 5af4a2ab..ce2ec412 100644 --- a/docs/user-guide/25-long-horizon-tasks.md +++ b/docs/user-guide/25-long-horizon-tasks.md @@ -11,10 +11,18 @@ interrupted node, and `/task stop ` to cancel it. Recovered tasks default to `paused_recovered`; external mutations interrupted during a crash are blocked and are never automatically replayed. +One graph owner lease fences each scheduling wave. A second CLI/Web process cannot execute the same +task while that lease is live. Stop requests remain available to the user while a worker is active; +they abort the worker context and the cancelled graph cannot be revived by late success output. + Plans with explicit `dependsOn` edges run dependency-ready siblings concurrently. Omitting `dependsOn` keeps the traditional ordered-plan behavior. Full-scope implementation sub-agents must declare `writePaths`; they work in isolated snapshots and return patches for guarded parent merge. +Use the approval prompt for `merge_subagent_patch` to apply such a patch. Moss reloads the retained +artifact by ID and rejects undeclared paths, secret paths, stale parent files, or altered patch bytes. Completion is evidence-first. Coding work needs a merged patch, a separate successful verifier, and fresh green verification after the patch. Research needs citation/tool evidence; device work needs a real probe receipt. Model prose cannot override a failed or missing deterministic check. +The same rule applies to embedding hosts: direct `ExecutionStore.append()` calls cannot issue a +verification verdict or complete a graph; those terminal events are reserved for the arbiter. diff --git a/package.json b/package.json index 17887d70..81fde3b5 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "check:vendored": "node scripts/check-vendored-sources.mjs", "check:pr-title": "node scripts/check-pr-title.mjs", "check:agent-harness-benchmark": "node scripts/check-agent-harness-benchmark.mjs", - "acceptance:long-horizon": "npm run build -w @rdk-moss/agent && node benchmarks/run-long-horizon-acceptance.mjs", + "acceptance:long-horizon": "node benchmarks/run-long-horizon-acceptance.mjs", "check:leaderboard-readiness": "node scripts/check-leaderboard-readiness.mjs", "check:moss-web-tokens": "node scripts/check-moss-web-tokens.mjs", "check": "npm run format:check && npm run lint && npm run typecheck && npm run check:boundaries && npm run check:hygiene && npm run check:vendored && npm run check:moss-web-tokens && npm run check:maintainability && npm run test:standards", @@ -39,7 +39,7 @@ "demo:cloud-local": "npm run build -w @rdk-moss/agent && node scripts/demo-cloud-local-recovery.mjs", "demo:multi-model-review": "npm run build -w @rdk-moss/agent && node scripts/demo-multi-model-review.mjs", "release:rdk-moss": "node scripts/release-rdk-moss.mjs", - "verify": "npm run check && npm run check:agent-harness-benchmark && npm run api:check && npm run docs && npm run test", + "verify": "npm run check && npm run acceptance:long-horizon && npm run check:agent-harness-benchmark && npm run api:check && npm run docs && npm run test", "prepare": "node scripts/install-hooks.mjs" }, "devDependencies": { diff --git a/packages/moss-agent/API.md b/packages/moss-agent/API.md index d707f995..8e4094ab 100644 --- a/packages/moss-agent/API.md +++ b/packages/moss-agent/API.md @@ -51,9 +51,22 @@ npm install @rdk-moss/agent @rdk-moss/core `ExecutionTaskController` for list/inspect/resume/retry/stop and `CompletionArbiter` for final evidence-bound verdicts. `TaskRunLedger` remains a one-release compatibility projection. +Hosts execute dependency-ready work through `MossAgent.runExecutionGraph()`. The scheduler holds a +fenced owner lease for the whole wave, renews it while work is active, and aborts executor contexts +when a user stops the graph or ownership is lost. `runRoutedExecutionGraph()` additionally binds +each node to an authorized immutable advisor/implementer/verifier role and synthesizes structured +coverage and conflicts before completion arbitration. Routed implementation work receives a +root-created workspace lease and requires an explicit host merge-approval callback; routed verifier +work must return a zero-exit structured command receipt from a distinct run. + Implementation roles must declare `isolated-write` and assignment write paths. Their workspace is a Git worktree when `HEAD` exists and a filtered copy snapshot otherwise. Workers return a durable patch artifact; only the root host may authorize and merge it. +The approval-gated `merge_subagent_patch` tool accepts only the opaque lease and patch IDs. The +workspace adapter reloads its own metadata, recomputes the artifact digest, revalidates write paths +and the parent baseline, and never trusts caller-supplied changed paths. +`verification.recorded` and `graph.completed` appends are capability-bound to `CompletionArbiter`; +public store callers cannot manufacture a verified terminal state. ### Web and installed-plugin contracts (beta) diff --git a/packages/moss-agent/etc/channels.api.md b/packages/moss-agent/etc/channels.api.md index 100af4a1..d19fc428 100644 --- a/packages/moss-agent/etc/channels.api.md +++ b/packages/moss-agent/etc/channels.api.md @@ -42,6 +42,20 @@ interface AcquireExecutionLeaseInput { // @beta type AgentCapability = 'architecture' | 'code' | 'test' | 'security' | 'performance' | 'documentation' | 'device' | 'research' | (string & {}); +// @beta +interface AgentClaim { + // (undocumented) + readonly conclusion: string; + // (undocumented) + readonly evidenceRefs: readonly string[]; + // (undocumented) + readonly id: string; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + // Warning: (ae-missing-release-tag) "AgentHooks" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -157,6 +171,41 @@ interface AgentLoopExtensions { steeringEngine?: SteeringEngine; } +// @beta +interface AgentResult { + // (undocumented) + readonly assignmentId: string; + // Warning: (ae-forgotten-export) The symbol "AgentClaim" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly claims: readonly AgentClaim[]; + // (undocumented) + readonly evidenceRefs: readonly string[]; + readonly patchRef?: string; + // (undocumented) + readonly roleId: string; + // (undocumented) + readonly runId?: string; + // (undocumented) + readonly status: 'PASS' | 'FAIL' | 'PARTIAL'; + // (undocumented) + readonly unmetCriteria: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "AgentVerificationReceipt" needs to be exported by the entry point index.d.ts + readonly verification?: AgentVerificationReceipt; +} + +// @beta +interface AgentResultConflict { + // (undocumented) + readonly claimIds: readonly string[]; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + // @beta interface AgentRoleDefinition { // (undocumented) @@ -210,6 +259,36 @@ interface AgentRoleRegistryOptions { readonly allowIsolatedWrite?: boolean; } +// @beta +interface AgentSynthesisResult { + // (undocumented) + readonly acceptedEvidenceIds: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "AgentResultConflict" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly conflicts: readonly AgentResultConflict[]; + // (undocumented) + readonly coverage: number; + // (undocumented) + readonly missingCriteria: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "AssignmentSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly verifierAssignments: readonly AssignmentSpec[]; +} + +// @beta +interface AgentVerificationReceipt { + // (undocumented) + readonly artifactDigest?: string; + // (undocumented) + readonly command: string; + // (undocumented) + readonly exitCode: number; + // (undocumented) + readonly summary: string; +} + // @beta interface AppendExecutionEventInput { // (undocumented) @@ -220,6 +299,8 @@ interface AppendExecutionEventInput { readonly id?: string; // (undocumented) readonly nodeId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts + readonly ownerLease?: ExecutionOwnerLease; // (undocumented) readonly time?: number; // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point index.d.ts @@ -304,6 +385,32 @@ interface ApprovedPreflightStopDecision { // @public (undocumented) type ApprovedPreflightStopResult = 'cancel_requested' | 'queued' | 'already_cancelled' | 'already_terminal' | 'not_found'; +// @beta +interface AssignmentSpec { + // (undocumented) + readonly acceptanceCriteria: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly inputEvidenceIds: readonly string[]; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly requiredCapabilities: readonly AgentCapability[]; + // (undocumented) + readonly requiredRoleKind: AgentRoleKind; + // (undocumented) + readonly writePaths: readonly string[]; +} + // Warning: (ae-forgotten-export) The symbol "MossAgent" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "bridgeAgentToChannel" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -681,6 +788,24 @@ type ContextPruningToolMatch = { // @public (undocumented) type ContextSources = Readonly>; +// Warning: (ae-missing-release-tag) "createAgentExecutionMethods" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function createAgentExecutionMethods(input: { + scheduler: ExecutionGraphScheduler; + arbiter: CompletionArbiter; + workspaceLeases: WorkspaceLeaseAdapter; + roles: AgentRoleRegistry; + store: ExecutionStore; + workspaceDir: string; +}): { + runExecutionGraph: (graphId: string, executor: ExecutionNodeExecutor, taskKind?: CompletionTaskKind) => Promise<{ + schedule: ExecutionScheduleResult; + completion?: CompletionDecision; + }>; + runRoutedExecutionGraph: (graphId: string, executor: RoutedAgentExecutor, taskKind?: CompletionTaskKind, authorizeMerge?: (lease: WorkspaceLease, patchId: string) => void | Promise) => Promise; +}; + // @beta interface CreateExecutionGraphInput { // (undocumented) @@ -815,6 +940,12 @@ interface ExecutionBudget { readonly usedWallTimeMs?: number; } +// Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts +// +// @beta +type ExecutionCompletionAppender = (graphId: string, input: AppendExecutionEventInput) => ExecutionGraphSnapshot; + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -866,22 +997,53 @@ interface ExecutionEvidence { // @beta type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; +// @beta +class ExecutionGraphScheduler { + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSchedulerOptions" needs to be exported by the entry point index.d.ts + constructor(store: ExecutionStore, options?: ExecutionGraphSchedulerOptions); + // (undocumented) + failNode(graphId: string, nodeId: string, failure: { + readonly error: string; + readonly failureFingerprint?: string; + }): ExecutionGraphSnapshot; + // (undocumented) + reconcile(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + runAvailable(graphId: string, executor: ExecutionNodeExecutor): Promise; + // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts + // + // (undocumented) + selectRunnable(graph: ExecutionGraphSnapshot): readonly ExecutionNode[]; + // (undocumented) + startNode(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts + // + // (undocumented) + succeedNode(graphId: string, nodeId: string, evidence?: readonly ExecutionEvidence[]): ExecutionGraphSnapshot; +} + +// @beta +interface ExecutionGraphSchedulerOptions { + // (undocumented) + readonly leaseRenewIntervalMs?: number; + // (undocumented) + readonly leaseTtlMs?: number; + // (undocumented) + readonly ownerId?: string; +} + // @beta interface ExecutionGraphSnapshot { // (undocumented) readonly budget: ExecutionBudget; // (undocumented) readonly createdAt: number; - // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts - // // (undocumented) readonly evidence: readonly ExecutionEvidence[]; // (undocumented) readonly goal: string; // (undocumented) readonly id: string; - // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts - // // (undocumented) readonly nodes: Readonly>; // (undocumented) @@ -959,9 +1121,32 @@ interface ExecutionNodeDefinition { readonly writePaths?: readonly string[]; } +// @beta +interface ExecutionNodeExecutionContext { + readonly signal: AbortSignal; +} + +// Warning: (ae-forgotten-export) The symbol "ExecutionNodeExecutionContext" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionNodeRunResult" needs to be exported by the entry point index.d.ts +// +// @beta +type ExecutionNodeExecutor = (node: ExecutionNode, graph: ExecutionGraphSnapshot, context: ExecutionNodeExecutionContext) => Promise; + // @beta type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; +// @beta +interface ExecutionNodeRunResult { + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidence?: readonly ExecutionEvidence[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly success: boolean; +} + // @beta type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; @@ -991,6 +1176,14 @@ interface ExecutionRecovery { readonly requiresUserResume: boolean; } +// @beta +interface ExecutionScheduleResult { + // (undocumented) + readonly graph: ExecutionGraphSnapshot; + // (undocumented) + readonly startedNodeIds: readonly string[]; +} + // Warning: (ae-missing-release-tag) "ExecutionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1012,16 +1205,14 @@ interface ExecutionState { // @beta interface ExecutionStore { // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts // // (undocumented) acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; - // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts - // // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionCompletionAppender" needs to be exported by the entry point index.d.ts + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts // // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; @@ -1587,8 +1778,6 @@ class MossAgent { delivery?: SessionInboxDelivery; id?: string; }): SessionInboxEntry; - // Warning: (ae-forgotten-export) The symbol "AgentRoleRegistry" needs to be exported by the entry point index.d.ts - // // @beta readonly agentRoles: AgentRoleRegistry; // (undocumented) @@ -1626,8 +1815,6 @@ class MossAgent { }>; // (undocumented) completeGoal(sessionKey: string, reason?: string): Promise; - // Warning: (ae-forgotten-export) The symbol "CompletionArbiter" needs to be exported by the entry point index.d.ts - // // @beta readonly completionArbiter: CompletionArbiter; // Warning: (ae-forgotten-export) The symbol "MossAgentConfig" needs to be exported by the entry point index.d.ts @@ -1642,6 +1829,8 @@ class MossAgent { drain: SessionDrainResult; }>; // @beta + readonly executionScheduler: ExecutionGraphScheduler; + // @beta readonly executionStore: ExecutionStore; // Warning: (ae-forgotten-export) The symbol "PlatformExtensionRegistry" needs to be exported by the entry point index.d.ts // @@ -1695,6 +1884,12 @@ class MossAgent { rewindConversation(sessionKey: string, toMessageCount: number): Promise<{ truncated: number; }>; + // Warning: (ae-forgotten-export) The symbol "createAgentExecutionMethods" needs to be exported by the entry point index.d.ts + // + // @beta + readonly runExecutionGraph: ReturnType['runExecutionGraph']; + // @beta + readonly runRoutedExecutionGraph: ReturnType['runRoutedExecutionGraph']; // Warning: (ae-forgotten-export) The symbol "SessionEvent" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1731,8 +1926,6 @@ class MossAgent { unregisterPostToolHook(name: string): boolean; // (undocumented) unregisterPreToolHook(name: string): boolean; - // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts - // // @beta readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; } @@ -2658,6 +2851,33 @@ export interface ProviderErrorSurface { userMessage: string; } +// @beta +interface RoutedAgentExecutionOutcome { + // (undocumented) + readonly completion?: CompletionDecision; + // (undocumented) + readonly schedule: ExecutionScheduleResult; + // Warning: (ae-forgotten-export) The symbol "AgentSynthesisResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly synthesis: AgentSynthesisResult; +} + +// Warning: (ae-forgotten-export) The symbol "RoutedAssignment" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "AgentResult" needs to be exported by the entry point index.d.ts +// +// @beta +type RoutedAgentExecutor = (routed: RoutedAssignment, context: ExecutionNodeExecutionContext) => Promise; + +// @beta +interface RoutedAssignment { + // (undocumented) + readonly assignment: AssignmentSpec; + // (undocumented) + readonly role: Readonly; + readonly workspaceLease?: WorkspaceLease; +} + // Warning: (ae-forgotten-export) The symbol "SemanticCompletionResult" needs to be exported by the entry point index.d.ts // // @beta @@ -3320,6 +3540,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -3370,6 +3595,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; @@ -3581,7 +3807,6 @@ interface WorkspaceLease { // @beta interface WorkspaceLeaseAdapter { // Warning: (ae-forgotten-export) The symbol "CreateWorkspaceLeaseInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "WorkspaceLease" needs to be exported by the entry point index.d.ts // // (undocumented) create(input: CreateWorkspaceLeaseInput): Promise; @@ -3597,6 +3822,8 @@ interface WorkspaceLeaseAdapter { // // (undocumented) merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // (undocumented) + mergeStored(leaseId: string, patchId: string): Promise; // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseReleaseReason" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -3614,9 +3841,13 @@ type WorkspaceLeaseStatus = 'active' | 'merged' | 'rejected' | 'cancelled'; // @beta interface WorkspaceMergeResult { + // (undocumented) + readonly changedPaths?: readonly string[]; // (undocumented) readonly conflictingPaths: readonly string[]; // (undocumented) + readonly digest?: string; + // (undocumented) readonly patchId: string; // (undocumented) readonly status: 'merged' | 'merge_conflict'; @@ -3642,6 +3873,15 @@ interface WorkspacePatch { // Warnings were encountered during analysis: // // src/context/pruning.ts:77:3 - (ae-forgotten-export) The symbol "ContextPruningToolMatch" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:31:3 - (ae-forgotten-export) The symbol "ExecutionGraphScheduler" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:32:3 - (ae-forgotten-export) The symbol "CompletionArbiter" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:33:3 - (ae-forgotten-export) The symbol "WorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:34:3 - (ae-forgotten-export) The symbol "AgentRoleRegistry" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:38:5 - (ae-forgotten-export) The symbol "ExecutionNodeExecutor" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:42:35 - (ae-forgotten-export) The symbol "ExecutionScheduleResult" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-forgotten-export) The symbol "RoutedAgentExecutor" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-forgotten-export) The symbol "WorkspaceLease" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-forgotten-export) The symbol "RoutedAgentExecutionOutcome" needs to be exported by the entry point index.d.ts // src/core/agent/moss-agent.ts:611:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts // src/core/agent/moss-agent.ts:692:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts // src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts diff --git a/packages/moss-agent/etc/context.api.md b/packages/moss-agent/etc/context.api.md index 75d62608..77dc8da5 100644 --- a/packages/moss-agent/etc/context.api.md +++ b/packages/moss-agent/etc/context.api.md @@ -751,6 +751,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -801,6 +806,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/etc/core.api.md b/packages/moss-agent/etc/core.api.md index e73c8102..67c6ffbe 100644 --- a/packages/moss-agent/etc/core.api.md +++ b/packages/moss-agent/etc/core.api.md @@ -57,6 +57,20 @@ export function acquireSessionWriteLock(params: { // @beta type AgentCapability = 'architecture' | 'code' | 'test' | 'security' | 'performance' | 'documentation' | 'device' | 'research' | (string & {}); +// @beta +interface AgentClaim { + // (undocumented) + readonly conclusion: string; + // (undocumented) + readonly evidenceRefs: readonly string[]; + // (undocumented) + readonly id: string; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + // Warning: (ae-missing-release-tag) "AgentHooks" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -346,6 +360,41 @@ export interface AgentLoopToolInput { toolsForRun: Tool[]; } +// @beta +interface AgentResult { + // (undocumented) + readonly assignmentId: string; + // Warning: (ae-forgotten-export) The symbol "AgentClaim" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly claims: readonly AgentClaim[]; + // (undocumented) + readonly evidenceRefs: readonly string[]; + readonly patchRef?: string; + // (undocumented) + readonly roleId: string; + // (undocumented) + readonly runId?: string; + // (undocumented) + readonly status: 'PASS' | 'FAIL' | 'PARTIAL'; + // (undocumented) + readonly unmetCriteria: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "AgentVerificationReceipt" needs to be exported by the entry point index.d.ts + readonly verification?: AgentVerificationReceipt; +} + +// @beta +interface AgentResultConflict { + // (undocumented) + readonly claimIds: readonly string[]; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + // @beta interface AgentRoleDefinition { // (undocumented) @@ -399,6 +448,36 @@ interface AgentRoleRegistryOptions { readonly allowIsolatedWrite?: boolean; } +// @beta +interface AgentSynthesisResult { + // (undocumented) + readonly acceptedEvidenceIds: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "AgentResultConflict" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly conflicts: readonly AgentResultConflict[]; + // (undocumented) + readonly coverage: number; + // (undocumented) + readonly missingCriteria: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "AssignmentSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly verifierAssignments: readonly AssignmentSpec[]; +} + +// @beta +interface AgentVerificationReceipt { + // (undocumented) + readonly artifactDigest?: string; + // (undocumented) + readonly command: string; + // (undocumented) + readonly exitCode: number; + // (undocumented) + readonly summary: string; +} + // @beta interface AppendExecutionEventInput { // (undocumented) @@ -409,6 +488,8 @@ interface AppendExecutionEventInput { readonly id?: string; // (undocumented) readonly nodeId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts + readonly ownerLease?: ExecutionOwnerLease; // (undocumented) readonly time?: number; // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point index.d.ts @@ -505,6 +586,32 @@ interface ApprovedPreflightStopDecision { // @public (undocumented) type ApprovedPreflightStopResult = 'cancel_requested' | 'queued' | 'already_cancelled' | 'already_terminal' | 'not_found'; +// @beta +interface AssignmentSpec { + // (undocumented) + readonly acceptanceCriteria: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly inputEvidenceIds: readonly string[]; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly requiredCapabilities: readonly AgentCapability[]; + // (undocumented) + readonly requiredRoleKind: AgentRoleKind; + // (undocumented) + readonly writePaths: readonly string[]; +} + // Warning: (ae-missing-release-tag) "AssistantMessage" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1145,6 +1252,24 @@ export function convertMessagesToPi(messages: Message[], modelInfo: { reasoning?: unknown; }): Message_2[]; +// Warning: (ae-missing-release-tag) "createAgentExecutionMethods" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function createAgentExecutionMethods(input: { + scheduler: ExecutionGraphScheduler; + arbiter: CompletionArbiter; + workspaceLeases: WorkspaceLeaseAdapter; + roles: AgentRoleRegistry; + store: ExecutionStore; + workspaceDir: string; +}): { + runExecutionGraph: (graphId: string, executor: ExecutionNodeExecutor, taskKind?: CompletionTaskKind) => Promise<{ + schedule: ExecutionScheduleResult; + completion?: CompletionDecision; + }>; + runRoutedExecutionGraph: (graphId: string, executor: RoutedAgentExecutor, taskKind?: CompletionTaskKind, authorizeMerge?: (lease: WorkspaceLease, patchId: string) => void | Promise) => Promise; +}; + // Warning: (ae-missing-release-tag) "createClientLlmSummarizationStrategy" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1484,6 +1609,12 @@ interface ExecutionBudget { readonly usedWallTimeMs?: number; } +// Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts +// +// @beta +type ExecutionCompletionAppender = (graphId: string, input: AppendExecutionEventInput) => ExecutionGraphSnapshot; + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1535,22 +1666,53 @@ interface ExecutionEvidence { // @beta type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; +// @beta +class ExecutionGraphScheduler { + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSchedulerOptions" needs to be exported by the entry point index.d.ts + constructor(store: ExecutionStore, options?: ExecutionGraphSchedulerOptions); + // (undocumented) + failNode(graphId: string, nodeId: string, failure: { + readonly error: string; + readonly failureFingerprint?: string; + }): ExecutionGraphSnapshot; + // (undocumented) + reconcile(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + runAvailable(graphId: string, executor: ExecutionNodeExecutor): Promise; + // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts + // + // (undocumented) + selectRunnable(graph: ExecutionGraphSnapshot): readonly ExecutionNode[]; + // (undocumented) + startNode(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts + // + // (undocumented) + succeedNode(graphId: string, nodeId: string, evidence?: readonly ExecutionEvidence[]): ExecutionGraphSnapshot; +} + +// @beta +interface ExecutionGraphSchedulerOptions { + // (undocumented) + readonly leaseRenewIntervalMs?: number; + // (undocumented) + readonly leaseTtlMs?: number; + // (undocumented) + readonly ownerId?: string; +} + // @beta interface ExecutionGraphSnapshot { // (undocumented) readonly budget: ExecutionBudget; // (undocumented) readonly createdAt: number; - // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts - // // (undocumented) readonly evidence: readonly ExecutionEvidence[]; // (undocumented) readonly goal: string; // (undocumented) readonly id: string; - // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts - // // (undocumented) readonly nodes: Readonly>; // (undocumented) @@ -1628,9 +1790,32 @@ interface ExecutionNodeDefinition { readonly writePaths?: readonly string[]; } +// @beta +interface ExecutionNodeExecutionContext { + readonly signal: AbortSignal; +} + +// Warning: (ae-forgotten-export) The symbol "ExecutionNodeExecutionContext" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionNodeRunResult" needs to be exported by the entry point index.d.ts +// +// @beta +type ExecutionNodeExecutor = (node: ExecutionNode, graph: ExecutionGraphSnapshot, context: ExecutionNodeExecutionContext) => Promise; + // @beta type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; +// @beta +interface ExecutionNodeRunResult { + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidence?: readonly ExecutionEvidence[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly success: boolean; +} + // @beta type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; @@ -1660,6 +1845,14 @@ interface ExecutionRecovery { readonly requiresUserResume: boolean; } +// @beta +interface ExecutionScheduleResult { + // (undocumented) + readonly graph: ExecutionGraphSnapshot; + // (undocumented) + readonly startedNodeIds: readonly string[]; +} + // Warning: (ae-missing-release-tag) "ExecutionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1681,16 +1874,14 @@ interface ExecutionState { // @beta interface ExecutionStore { // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts // // (undocumented) acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; - // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts - // // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionCompletionAppender" needs to be exported by the entry point index.d.ts + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts // // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; @@ -2821,8 +3012,6 @@ export class MossAgent { delivery?: SessionInboxDelivery; id?: string; }): SessionInboxEntry; - // Warning: (ae-forgotten-export) The symbol "AgentRoleRegistry" needs to be exported by the entry point index.d.ts - // // @beta readonly agentRoles: AgentRoleRegistry; // (undocumented) @@ -2855,8 +3044,6 @@ export class MossAgent { }>; // (undocumented) completeGoal(sessionKey: string, reason?: string): Promise; - // Warning: (ae-forgotten-export) The symbol "CompletionArbiter" needs to be exported by the entry point index.d.ts - // // @beta readonly completionArbiter: CompletionArbiter; // (undocumented) @@ -2869,6 +3056,8 @@ export class MossAgent { drain: SessionDrainResult; }>; // @beta + readonly executionScheduler: ExecutionGraphScheduler; + // @beta readonly executionStore: ExecutionStore; // Warning: (ae-forgotten-export) The symbol "PlatformExtensionRegistry" needs to be exported by the entry point index.d.ts // @@ -2916,6 +3105,12 @@ export class MossAgent { rewindConversation(sessionKey: string, toMessageCount: number): Promise<{ truncated: number; }>; + // Warning: (ae-forgotten-export) The symbol "createAgentExecutionMethods" needs to be exported by the entry point index.d.ts + // + // @beta + readonly runExecutionGraph: ReturnType['runExecutionGraph']; + // @beta + readonly runRoutedExecutionGraph: ReturnType['runRoutedExecutionGraph']; // Warning: (ae-forgotten-export) The symbol "SessionEvent" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2944,8 +3139,6 @@ export class MossAgent { unregisterPostToolHook(name: string): boolean; // (undocumented) unregisterPreToolHook(name: string): boolean; - // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts - // // @beta readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; } @@ -4102,6 +4295,33 @@ export function resolveSpawnToolSet(scope: SpawnToolScope | undefined, registry? // @public (undocumented) export function retryDelayForLlmError(classification: LlmErrorClassification, attempt?: number): number | undefined; +// @beta +export interface RoutedAgentExecutionOutcome { + // (undocumented) + readonly completion?: CompletionDecision; + // (undocumented) + readonly schedule: ExecutionScheduleResult; + // Warning: (ae-forgotten-export) The symbol "AgentSynthesisResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly synthesis: AgentSynthesisResult; +} + +// Warning: (ae-forgotten-export) The symbol "RoutedAssignment" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "AgentResult" needs to be exported by the entry point index.d.ts +// +// @beta +type RoutedAgentExecutor = (routed: RoutedAssignment, context: ExecutionNodeExecutionContext) => Promise; + +// @beta +interface RoutedAssignment { + // (undocumented) + readonly assignment: AssignmentSpec; + // (undocumented) + readonly role: Readonly; + readonly workspaceLease?: WorkspaceLease; +} + // Warning: (ae-missing-release-tag) "runAgentLoop" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -5267,6 +5487,11 @@ export interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -5317,6 +5542,7 @@ export interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; @@ -5684,7 +5910,6 @@ interface WorkspaceLease { // @beta interface WorkspaceLeaseAdapter { // Warning: (ae-forgotten-export) The symbol "CreateWorkspaceLeaseInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "WorkspaceLease" needs to be exported by the entry point index.d.ts // // (undocumented) create(input: CreateWorkspaceLeaseInput): Promise; @@ -5700,6 +5925,8 @@ interface WorkspaceLeaseAdapter { // // (undocumented) merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // (undocumented) + mergeStored(leaseId: string, patchId: string): Promise; // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseReleaseReason" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -5754,9 +5981,13 @@ export interface WorkspaceMemoryContext { // @beta interface WorkspaceMergeResult { + // (undocumented) + readonly changedPaths?: readonly string[]; // (undocumented) readonly conflictingPaths: readonly string[]; // (undocumented) + readonly digest?: string; + // (undocumented) readonly patchId: string; // (undocumented) readonly status: 'merged' | 'merge_conflict'; @@ -5787,6 +6018,15 @@ export function wrapToolWithAbortSignal(tool: Tool, runSignal: AbortSignal // Warnings were encountered during analysis: // // src/context/pruning.ts:77:3 - (ae-forgotten-export) The symbol "ContextPruningToolMatch" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:31:3 - (ae-forgotten-export) The symbol "ExecutionGraphScheduler" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:32:3 - (ae-forgotten-export) The symbol "CompletionArbiter" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:33:3 - (ae-forgotten-export) The symbol "WorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:34:3 - (ae-forgotten-export) The symbol "AgentRoleRegistry" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:38:5 - (ae-forgotten-export) The symbol "ExecutionNodeExecutor" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:42:35 - (ae-forgotten-export) The symbol "ExecutionScheduleResult" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-forgotten-export) The symbol "RoutedAgentExecutor" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-forgotten-export) The symbol "WorkspaceLease" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-incompatible-release-tags) The symbol "runRoutedExecutionGraph" is marked as @public, but its signature references "RoutedAgentExecutionOutcome" which is marked as @beta // src/core/agent/moss-agent.ts:611:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts // src/core/agent/moss-agent.ts:692:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts // src/core/llm/summarization-strategy.ts:37:7 - (ae-forgotten-export) The symbol "PruneResult" needs to be exported by the entry point index.d.ts diff --git a/packages/moss-agent/etc/eval.api.md b/packages/moss-agent/etc/eval.api.md index 8985a958..0846b414 100644 --- a/packages/moss-agent/etc/eval.api.md +++ b/packages/moss-agent/etc/eval.api.md @@ -579,6 +579,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -629,6 +634,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/etc/mcp.api.md b/packages/moss-agent/etc/mcp.api.md index 5f0955cf..a0da4380 100644 --- a/packages/moss-agent/etc/mcp.api.md +++ b/packages/moss-agent/etc/mcp.api.md @@ -226,6 +226,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -276,6 +281,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/etc/mesh.api.md b/packages/moss-agent/etc/mesh.api.md index b4e986d5..c2de4bd4 100644 --- a/packages/moss-agent/etc/mesh.api.md +++ b/packages/moss-agent/etc/mesh.api.md @@ -508,6 +508,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -558,6 +563,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/etc/orchestration.api.md b/packages/moss-agent/etc/orchestration.api.md index 74a049d0..17c274ff 100644 --- a/packages/moss-agent/etc/orchestration.api.md +++ b/packages/moss-agent/etc/orchestration.api.md @@ -26,6 +26,8 @@ export class AdaptiveWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { // (undocumented) merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; // (undocumented) + mergeStored(leaseId: string, patchId: string): Promise; + // (undocumented) release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; } @@ -54,7 +56,6 @@ export interface AgentResult { readonly claims: readonly AgentClaim[]; // (undocumented) readonly evidenceRefs: readonly string[]; - // (undocumented) readonly patchRef?: string; // (undocumented) readonly roleId: string; @@ -64,6 +65,7 @@ export interface AgentResult { readonly status: 'PASS' | 'FAIL' | 'PARTIAL'; // (undocumented) readonly unmetCriteria: readonly string[]; + readonly verification?: AgentVerificationReceipt; } // @beta @@ -136,6 +138,18 @@ export interface AgentSynthesisResult { readonly verifierAssignments: readonly AssignmentSpec[]; } +// @beta +export interface AgentVerificationReceipt { + // (undocumented) + readonly artifactDigest?: string; + // (undocumented) + readonly command: string; + // (undocumented) + readonly exitCode: number; + // (undocumented) + readonly summary: string; +} + // @beta export interface AppendExecutionEventInput { // (undocumented) @@ -146,6 +160,7 @@ export interface AppendExecutionEventInput { readonly id?: string; // (undocumented) readonly nodeId?: string; + readonly ownerLease?: ExecutionOwnerLease; // (undocumented) readonly time?: number; // (undocumented) @@ -231,6 +246,10 @@ abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { // // (undocumented) merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // Warning: (ae-incompatible-release-tags) The symbol "mergeStored" is marked as @public, but its signature references "WorkspaceMergeResult" which is marked as @beta + // + // (undocumented) + mergeStored(leaseId: string, patchId: string): Promise; // (undocumented) protected readonly now: () => number; // Warning: (ae-incompatible-release-tags) The symbol "release" is marked as @public, but its signature references "WorkspaceLeaseReleaseReason" which is marked as @beta @@ -354,6 +373,9 @@ export interface ExecutionBudget { readonly usedWallTimeMs?: number; } +// @beta +export type ExecutionCompletionAppender = (graphId: string, input: AppendExecutionEventInput) => ExecutionGraphSnapshot; + // @beta export interface ExecutionEvent { // (undocumented) @@ -400,7 +422,7 @@ export type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_d // @beta export class ExecutionGraphScheduler { - constructor(store: ExecutionStore); + constructor(store: ExecutionStore, options?: ExecutionGraphSchedulerOptions); // (undocumented) failNode(graphId: string, nodeId: string, failure: { readonly error: string; @@ -418,6 +440,16 @@ export class ExecutionGraphScheduler { succeedNode(graphId: string, nodeId: string, evidence?: readonly ExecutionEvidence[]): ExecutionGraphSnapshot; } +// @beta +export interface ExecutionGraphSchedulerOptions { + // (undocumented) + readonly leaseRenewIntervalMs?: number; + // (undocumented) + readonly leaseTtlMs?: number; + // (undocumented) + readonly ownerId?: string; +} + // @beta export interface ExecutionGraphSnapshot { // (undocumented) @@ -498,7 +530,12 @@ export interface ExecutionNodeDefinition { } // @beta -export type ExecutionNodeExecutor = (node: ExecutionNode, graph: ExecutionGraphSnapshot) => Promise; +export interface ExecutionNodeExecutionContext { + readonly signal: AbortSignal; +} + +// @beta +export type ExecutionNodeExecutor = (node: ExecutionNode, graph: ExecutionGraphSnapshot, context: ExecutionNodeExecutionContext) => Promise; // @beta export type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; @@ -558,6 +595,7 @@ export interface ExecutionStore { acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; // (undocumented) @@ -616,6 +654,8 @@ export class InMemoryExecutionStore implements ExecutionStore { // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; // (undocumented) + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; + // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; // (undocumented) events(graphId: string, after?: number): readonly ExecutionEvent[]; @@ -643,6 +683,8 @@ export class JsonlExecutionStore implements ExecutionStore { // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; // (undocumented) + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; + // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; // (undocumented) events(graphId: string, after?: number): readonly ExecutionEvent[]; @@ -711,14 +753,20 @@ export function recoverExecutionGraph(store: ExecutionStore, graphId: string, op export interface RecoverExecutionGraphOptions { // (undocumented) readonly now?: number; + // (undocumented) + readonly ownerLease?: ExecutionOwnerLease; } +// @beta +export type RoutedAgentExecutor = (routed: RoutedAssignment, context: ExecutionNodeExecutionContext) => Promise; + // @beta export interface RoutedAssignment { // (undocumented) readonly assignment: AssignmentSpec; // (undocumented) readonly role: Readonly; + readonly workspaceLease?: WorkspaceLease; } // @beta @@ -779,6 +827,8 @@ export interface WorkspaceLeaseAdapter { // (undocumented) merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; // (undocumented) + mergeStored(leaseId: string, patchId: string): Promise; + // (undocumented) release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; } @@ -815,9 +865,13 @@ export interface WorkspaceMergeAuthorizationRequest { // @beta export interface WorkspaceMergeResult { + // (undocumented) + readonly changedPaths?: readonly string[]; // (undocumented) readonly conflictingPaths: readonly string[]; // (undocumented) + readonly digest?: string; + // (undocumented) readonly patchId: string; // (undocumented) readonly status: 'merged' | 'merge_conflict'; diff --git a/packages/moss-agent/etc/plan-execute.api.md b/packages/moss-agent/etc/plan-execute.api.md index d5af16d1..1db95c25 100644 --- a/packages/moss-agent/etc/plan-execute.api.md +++ b/packages/moss-agent/etc/plan-execute.api.md @@ -40,6 +40,8 @@ interface AppendExecutionEventInput { readonly id?: string; // (undocumented) readonly nodeId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts + readonly ownerLease?: ExecutionOwnerLease; // (undocumented) readonly time?: number; // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point index.d.ts @@ -152,6 +154,12 @@ interface ExecutionBudget { readonly usedWallTimeMs?: number; } +// Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts +// +// @beta +type ExecutionCompletionAppender = (graphId: string, input: AppendExecutionEventInput) => ExecutionGraphSnapshot; + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -349,16 +357,14 @@ export interface ExecutionState { // @beta interface ExecutionStore { // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts // // (undocumented) acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; - // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts - // // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionCompletionAppender" needs to be exported by the entry point index.d.ts + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts // // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; @@ -828,6 +834,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -878,6 +889,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/etc/root.api.md b/packages/moss-agent/etc/root.api.md index 5530395e..48b5c775 100644 --- a/packages/moss-agent/etc/root.api.md +++ b/packages/moss-agent/etc/root.api.md @@ -57,6 +57,8 @@ export class AdaptiveWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { // (undocumented) merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; // (undocumented) + mergeStored(leaseId: string, patchId: string): Promise; + // (undocumented) release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; } @@ -242,7 +244,6 @@ export interface AgentResult { readonly claims: readonly AgentClaim[]; // (undocumented) readonly evidenceRefs: readonly string[]; - // (undocumented) readonly patchRef?: string; // (undocumented) readonly roleId: string; @@ -252,6 +253,7 @@ export interface AgentResult { readonly status: 'PASS' | 'FAIL' | 'PARTIAL'; // (undocumented) readonly unmetCriteria: readonly string[]; + readonly verification?: AgentVerificationReceipt; } // @beta @@ -339,6 +341,18 @@ export interface AgentSynthesisResult { readonly verifierAssignments: readonly AssignmentSpec[]; } +// @beta +export interface AgentVerificationReceipt { + // (undocumented) + readonly artifactDigest?: string; + // (undocumented) + readonly command: string; + // (undocumented) + readonly exitCode: number; + // (undocumented) + readonly summary: string; +} + // Warning: (ae-missing-release-tag) "AnthropicLLMProvider" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -380,6 +394,7 @@ export interface AppendExecutionEventInput { readonly id?: string; // (undocumented) readonly nodeId?: string; + readonly ownerLease?: ExecutionOwnerLease; // (undocumented) readonly time?: number; // (undocumented) @@ -758,6 +773,10 @@ abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { // // (undocumented) merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // Warning: (ae-incompatible-release-tags) The symbol "mergeStored" is marked as @public, but its signature references "WorkspaceMergeResult" which is marked as @beta + // + // (undocumented) + mergeStored(leaseId: string, patchId: string): Promise; // (undocumented) protected readonly now: () => number; // Warning: (ae-incompatible-release-tags) The symbol "release" is marked as @public, but its signature references "WorkspaceLeaseReleaseReason" which is marked as @beta @@ -1970,6 +1989,24 @@ export class CopyWorkspaceLeaseAdapter extends BaseWorkspaceLeaseAdapter { protected removeWorkspace(_lease: WorkspaceLease): Promise; } +// Warning: (ae-missing-release-tag) "createAgentExecutionMethods" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function createAgentExecutionMethods(input: { + scheduler: ExecutionGraphScheduler; + arbiter: CompletionArbiter; + workspaceLeases: WorkspaceLeaseAdapter; + roles: AgentRoleRegistry; + store: ExecutionStore; + workspaceDir: string; +}): { + runExecutionGraph: (graphId: string, executor: ExecutionNodeExecutor, taskKind?: CompletionTaskKind) => Promise<{ + schedule: ExecutionScheduleResult; + completion?: CompletionDecision; + }>; + runRoutedExecutionGraph: (graphId: string, executor: RoutedAgentExecutor, taskKind?: CompletionTaskKind, authorizeMerge?: (lease: WorkspaceLease, patchId: string) => void | Promise) => Promise; +}; + // Warning: (ae-missing-release-tag) "createBraveSearch" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -2799,6 +2836,9 @@ export interface ExecutionBudget { readonly usedWallTimeMs?: number; } +// @beta +export type ExecutionCompletionAppender = (graphId: string, input: AppendExecutionEventInput) => ExecutionGraphSnapshot; + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2850,7 +2890,7 @@ export type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_d // @beta export class ExecutionGraphScheduler { - constructor(store: ExecutionStore); + constructor(store: ExecutionStore, options?: ExecutionGraphSchedulerOptions); // (undocumented) failNode(graphId: string, nodeId: string, failure: { readonly error: string; @@ -2868,6 +2908,16 @@ export class ExecutionGraphScheduler { succeedNode(graphId: string, nodeId: string, evidence?: readonly ExecutionEvidence[]): ExecutionGraphSnapshot; } +// @beta +export interface ExecutionGraphSchedulerOptions { + // (undocumented) + readonly leaseRenewIntervalMs?: number; + // (undocumented) + readonly leaseTtlMs?: number; + // (undocumented) + readonly ownerId?: string; +} + // @beta export interface ExecutionGraphSnapshot { // (undocumented) @@ -2948,7 +2998,12 @@ export interface ExecutionNodeDefinition { } // @beta -export type ExecutionNodeExecutor = (node: ExecutionNode, graph: ExecutionGraphSnapshot) => Promise; +export interface ExecutionNodeExecutionContext { + readonly signal: AbortSignal; +} + +// @beta +export type ExecutionNodeExecutor = (node: ExecutionNode, graph: ExecutionGraphSnapshot, context: ExecutionNodeExecutionContext) => Promise; // @beta export type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; @@ -3026,6 +3081,7 @@ export interface ExecutionStore { acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; // (undocumented) @@ -3564,6 +3620,8 @@ export class InMemoryExecutionStore implements ExecutionStore { // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; // (undocumented) + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; + // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; // (undocumented) events(graphId: string, after?: number): readonly ExecutionEvent[]; @@ -3724,6 +3782,8 @@ export class JsonlExecutionStore implements ExecutionStore { // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; // (undocumented) + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; + // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; // (undocumented) events(graphId: string, after?: number): readonly ExecutionEvent[]; @@ -5026,6 +5086,8 @@ export class MossAgent { drain: SessionDrainResult; }>; // @beta + readonly executionScheduler: ExecutionGraphScheduler; + // @beta readonly executionStore: ExecutionStore; // (undocumented) readonly extensions: PlatformExtensionRegistry; @@ -5069,6 +5131,12 @@ export class MossAgent { rewindConversation(sessionKey: string, toMessageCount: number): Promise<{ truncated: number; }>; + // Warning: (ae-forgotten-export) The symbol "createAgentExecutionMethods" needs to be exported by the entry point index.d.ts + // + // @beta + readonly runExecutionGraph: ReturnType['runExecutionGraph']; + // @beta + readonly runRoutedExecutionGraph: ReturnType['runRoutedExecutionGraph']; // Warning: (ae-forgotten-export) The symbol "SessionEvent" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -6948,6 +7016,8 @@ export function recoverExecutionGraph(store: ExecutionStore, graphId: string, op export interface RecoverExecutionGraphOptions { // (undocumented) readonly now?: number; + // (undocumented) + readonly ownerLease?: ExecutionOwnerLease; } // Warning: (ae-missing-release-tag) "RecoveryRecipe" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -7344,12 +7414,26 @@ export interface RetryOptions { shouldRetry?: (err: unknown, attempt: number) => boolean; } +// @beta +export interface RoutedAgentExecutionOutcome { + // (undocumented) + readonly completion?: CompletionDecision; + // (undocumented) + readonly schedule: ExecutionScheduleResult; + // (undocumented) + readonly synthesis: AgentSynthesisResult; +} + +// @beta +export type RoutedAgentExecutor = (routed: RoutedAssignment, context: ExecutionNodeExecutionContext) => Promise; + // @beta export interface RoutedAssignment { // (undocumented) readonly assignment: AssignmentSpec; // (undocumented) readonly role: Readonly; + readonly workspaceLease?: WorkspaceLease; } // Warning: (ae-missing-release-tag) "runMossCommunityAuthLogin" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -8709,6 +8793,11 @@ export interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -8759,6 +8848,7 @@ export interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; @@ -9596,6 +9686,8 @@ export interface WorkspaceLeaseAdapter { // (undocumented) merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; // (undocumented) + mergeStored(leaseId: string, patchId: string): Promise; + // (undocumented) release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; } @@ -9632,9 +9724,13 @@ export interface WorkspaceMergeAuthorizationRequest { // @beta export interface WorkspaceMergeResult { + // (undocumented) + readonly changedPaths?: readonly string[]; // (undocumented) readonly conflictingPaths: readonly string[]; // (undocumented) + readonly digest?: string; + // (undocumented) readonly patchId: string; // (undocumented) readonly status: 'merged' | 'merge_conflict'; @@ -9682,6 +9778,19 @@ export function writePathsOverlap(leftPaths: readonly string[], rightPaths: read // src/cli/community-auth.ts:694:5 - (ae-forgotten-export) The symbol "FetchImpl" needs to be exported by the entry point index.d.ts // src/cli/model-catalog.ts:62:17 - (ae-forgotten-export) The symbol "CustomModelConfig" needs to be exported by the entry point index.d.ts // src/context/pruning.ts:77:3 - (ae-forgotten-export) The symbol "ContextPruningToolMatch" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:31:3 - (ae-incompatible-release-tags) The symbol "scheduler" is marked as @public, but its signature references "ExecutionGraphScheduler" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:32:3 - (ae-incompatible-release-tags) The symbol "arbiter" is marked as @public, but its signature references "CompletionArbiter" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:33:3 - (ae-incompatible-release-tags) The symbol "workspaceLeases" is marked as @public, but its signature references "WorkspaceLeaseAdapter" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:34:3 - (ae-incompatible-release-tags) The symbol "roles" is marked as @public, but its signature references "AgentRoleRegistry" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:35:3 - (ae-incompatible-release-tags) The symbol "store" is marked as @public, but its signature references "ExecutionStore" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:38:5 - (ae-incompatible-release-tags) The symbol "runExecutionGraph" is marked as @public, but its signature references "ExecutionNodeExecutor" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:38:5 - (ae-incompatible-release-tags) The symbol "runExecutionGraph" is marked as @public, but its signature references "CompletionTaskKind" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:42:35 - (ae-incompatible-release-tags) The symbol "schedule" is marked as @public, but its signature references "ExecutionScheduleResult" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-incompatible-release-tags) The symbol "runRoutedExecutionGraph" is marked as @public, but its signature references "RoutedAgentExecutor" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-incompatible-release-tags) The symbol "runRoutedExecutionGraph" is marked as @public, but its signature references "CompletionTaskKind" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-incompatible-release-tags) The symbol "runRoutedExecutionGraph" is marked as @public, but its signature references "WorkspaceLease" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-incompatible-release-tags) The symbol "runRoutedExecutionGraph" is marked as @public, but its signature references "RoutedAgentExecutionOutcome" which is marked as @beta +// src/core/agent/agent-execution-runtime.ts:81:15 - (ae-incompatible-release-tags) The symbol "completion" is marked as @public, but its signature references "CompletionDecision" which is marked as @beta // src/core/agent/moss-agent.ts:611:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts // src/core/agent/moss-agent.ts:692:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts // src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts @@ -9691,8 +9800,8 @@ export function writePathsOverlap(leftPaths: readonly string[], rightPaths: read // src/memory/trusted-skill-experiment-coordinator.ts:253:7 - (ae-forgotten-export) The symbol "TerminalVerdictLog" needs to be exported by the entry point index.d.ts // src/mesh/agent-mesh.ts:304:40 - (ae-forgotten-export) The symbol "HostAddressResolver_2" needs to be exported by the entry point index.d.ts // src/runtime/shared-runtime.ts:112:22 - (ae-forgotten-export) The symbol "DeviceReadonlyExecutor" needs to be exported by the entry point index.d.ts -// src/tools/builtin.ts:517:3 - (ae-incompatible-release-tags) The symbol "planControllerStore" is marked as @public, but its signature references "PlanControllerStore" which is marked as @beta -// src/tools/builtin.ts:518:3 - (ae-incompatible-release-tags) The symbol "executionStore" is marked as @public, but its signature references "ExecutionStore" which is marked as @beta +// src/tools/builtin.ts:519:3 - (ae-incompatible-release-tags) The symbol "planControllerStore" is marked as @public, but its signature references "PlanControllerStore" which is marked as @beta +// src/tools/builtin.ts:520:3 - (ae-incompatible-release-tags) The symbol "executionStore" is marked as @public, but its signature references "ExecutionStore" which is marked as @beta // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/runtime.api.md b/packages/moss-agent/etc/runtime.api.md index 7212718c..22ca4a71 100644 --- a/packages/moss-agent/etc/runtime.api.md +++ b/packages/moss-agent/etc/runtime.api.md @@ -42,6 +42,20 @@ interface AcquireExecutionLeaseInput { // @beta type AgentCapability = 'architecture' | 'code' | 'test' | 'security' | 'performance' | 'documentation' | 'device' | 'research' | (string & {}); +// @beta +interface AgentClaim { + // (undocumented) + readonly conclusion: string; + // (undocumented) + readonly evidenceRefs: readonly string[]; + // (undocumented) + readonly id: string; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + // Warning: (ae-missing-release-tag) "AgentHooks" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -157,6 +171,41 @@ interface AgentLoopExtensions { steeringEngine?: SteeringEngine; } +// @beta +interface AgentResult { + // (undocumented) + readonly assignmentId: string; + // Warning: (ae-forgotten-export) The symbol "AgentClaim" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly claims: readonly AgentClaim[]; + // (undocumented) + readonly evidenceRefs: readonly string[]; + readonly patchRef?: string; + // (undocumented) + readonly roleId: string; + // (undocumented) + readonly runId?: string; + // (undocumented) + readonly status: 'PASS' | 'FAIL' | 'PARTIAL'; + // (undocumented) + readonly unmetCriteria: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "AgentVerificationReceipt" needs to be exported by the entry point index.d.ts + readonly verification?: AgentVerificationReceipt; +} + +// @beta +interface AgentResultConflict { + // (undocumented) + readonly claimIds: readonly string[]; + // (undocumented) + readonly evidenceIds: readonly string[]; + // (undocumented) + readonly severity: 'low' | 'medium' | 'high'; + // (undocumented) + readonly subject: string; +} + // @beta interface AgentRoleDefinition { // (undocumented) @@ -210,6 +259,36 @@ interface AgentRoleRegistryOptions { readonly allowIsolatedWrite?: boolean; } +// @beta +interface AgentSynthesisResult { + // (undocumented) + readonly acceptedEvidenceIds: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "AgentResultConflict" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly conflicts: readonly AgentResultConflict[]; + // (undocumented) + readonly coverage: number; + // (undocumented) + readonly missingCriteria: readonly string[]; + // Warning: (ae-forgotten-export) The symbol "AssignmentSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly verifierAssignments: readonly AssignmentSpec[]; +} + +// @beta +interface AgentVerificationReceipt { + // (undocumented) + readonly artifactDigest?: string; + // (undocumented) + readonly command: string; + // (undocumented) + readonly exitCode: number; + // (undocumented) + readonly summary: string; +} + // @beta interface AppendExecutionEventInput { // (undocumented) @@ -220,6 +299,8 @@ interface AppendExecutionEventInput { readonly id?: string; // (undocumented) readonly nodeId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts + readonly ownerLease?: ExecutionOwnerLease; // (undocumented) readonly time?: number; // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point index.d.ts @@ -304,6 +385,32 @@ interface ApprovedPreflightStopDecision { // @public (undocumented) type ApprovedPreflightStopResult = 'cancel_requested' | 'queued' | 'already_cancelled' | 'already_terminal' | 'not_found'; +// @beta +interface AssignmentSpec { + // (undocumented) + readonly acceptanceCriteria: readonly string[]; + // (undocumented) + readonly budget?: ExecutionBudget; + // (undocumented) + readonly dependencies: readonly string[]; + // (undocumented) + readonly goal: string; + // (undocumented) + readonly graphId: string; + // (undocumented) + readonly id: string; + // (undocumented) + readonly inputEvidenceIds: readonly string[]; + // (undocumented) + readonly nodeId: string; + // (undocumented) + readonly requiredCapabilities: readonly AgentCapability[]; + // (undocumented) + readonly requiredRoleKind: AgentRoleKind; + // (undocumented) + readonly writePaths: readonly string[]; +} + // @public interface CapabilityPack { // Warning: (ae-forgotten-export) The symbol "Tool" needs to be exported by the entry point index.d.ts @@ -643,6 +750,24 @@ type ContextPruningToolMatch = { // @public (undocumented) type ContextSources = Readonly>; +// Warning: (ae-missing-release-tag) "createAgentExecutionMethods" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +function createAgentExecutionMethods(input: { + scheduler: ExecutionGraphScheduler; + arbiter: CompletionArbiter; + workspaceLeases: WorkspaceLeaseAdapter; + roles: AgentRoleRegistry; + store: ExecutionStore; + workspaceDir: string; +}): { + runExecutionGraph: (graphId: string, executor: ExecutionNodeExecutor, taskKind?: CompletionTaskKind) => Promise<{ + schedule: ExecutionScheduleResult; + completion?: CompletionDecision; + }>; + runRoutedExecutionGraph: (graphId: string, executor: RoutedAgentExecutor, taskKind?: CompletionTaskKind, authorizeMerge?: (lease: WorkspaceLease, patchId: string) => void | Promise) => Promise; +}; + // @beta interface CreateExecutionGraphInput { // (undocumented) @@ -858,6 +983,12 @@ interface ExecutionBudget { readonly usedWallTimeMs?: number; } +// Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts +// +// @beta +type ExecutionCompletionAppender = (graphId: string, input: AppendExecutionEventInput) => ExecutionGraphSnapshot; + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -909,22 +1040,53 @@ interface ExecutionEvidence { // @beta type ExecutionEvidenceKind = 'tool_result' | 'command_exit' | 'artifact_digest' | 'patch' | 'expert_claim' | 'approval' | 'verification' | 'probe_receipt' | 'citation'; +// @beta +class ExecutionGraphScheduler { + // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSchedulerOptions" needs to be exported by the entry point index.d.ts + constructor(store: ExecutionStore, options?: ExecutionGraphSchedulerOptions); + // (undocumented) + failNode(graphId: string, nodeId: string, failure: { + readonly error: string; + readonly failureFingerprint?: string; + }): ExecutionGraphSnapshot; + // (undocumented) + reconcile(graphId: string): ExecutionGraphSnapshot; + // (undocumented) + runAvailable(graphId: string, executor: ExecutionNodeExecutor): Promise; + // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts + // + // (undocumented) + selectRunnable(graph: ExecutionGraphSnapshot): readonly ExecutionNode[]; + // (undocumented) + startNode(graphId: string, nodeId: string): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts + // + // (undocumented) + succeedNode(graphId: string, nodeId: string, evidence?: readonly ExecutionEvidence[]): ExecutionGraphSnapshot; +} + +// @beta +interface ExecutionGraphSchedulerOptions { + // (undocumented) + readonly leaseRenewIntervalMs?: number; + // (undocumented) + readonly leaseTtlMs?: number; + // (undocumented) + readonly ownerId?: string; +} + // @beta interface ExecutionGraphSnapshot { // (undocumented) readonly budget: ExecutionBudget; // (undocumented) readonly createdAt: number; - // Warning: (ae-forgotten-export) The symbol "ExecutionEvidence" needs to be exported by the entry point index.d.ts - // // (undocumented) readonly evidence: readonly ExecutionEvidence[]; // (undocumented) readonly goal: string; // (undocumented) readonly id: string; - // Warning: (ae-forgotten-export) The symbol "ExecutionNode" needs to be exported by the entry point index.d.ts - // // (undocumented) readonly nodes: Readonly>; // (undocumented) @@ -1002,9 +1164,32 @@ interface ExecutionNodeDefinition { readonly writePaths?: readonly string[]; } +// @beta +interface ExecutionNodeExecutionContext { + readonly signal: AbortSignal; +} + +// Warning: (ae-forgotten-export) The symbol "ExecutionNodeExecutionContext" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionNodeRunResult" needs to be exported by the entry point index.d.ts +// +// @beta +type ExecutionNodeExecutor = (node: ExecutionNode, graph: ExecutionGraphSnapshot, context: ExecutionNodeExecutionContext) => Promise; + // @beta type ExecutionNodeKind = 'analysis' | 'implementation' | 'verification' | 'merge' | 'approval' | 'manual'; +// @beta +interface ExecutionNodeRunResult { + // (undocumented) + readonly error?: string; + // (undocumented) + readonly evidence?: readonly ExecutionEvidence[]; + // (undocumented) + readonly failureFingerprint?: string; + // (undocumented) + readonly success: boolean; +} + // @beta type ExecutionNodeStatus = 'pending' | 'ready' | 'leased' | 'running' | 'succeeded' | 'failed' | 'blocked' | 'skipped' | 'interrupted' | 'merge_conflict' | 'cancelled'; @@ -1034,6 +1219,14 @@ interface ExecutionRecovery { readonly requiresUserResume: boolean; } +// @beta +interface ExecutionScheduleResult { + // (undocumented) + readonly graph: ExecutionGraphSnapshot; + // (undocumented) + readonly startedNodeIds: readonly string[]; +} + // Warning: (ae-missing-release-tag) "ExecutionState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1055,16 +1248,14 @@ interface ExecutionState { // @beta interface ExecutionStore { // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point index.d.ts // // (undocumented) acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; - // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point index.d.ts - // // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionCompletionAppender" needs to be exported by the entry point index.d.ts + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point index.d.ts // // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; @@ -1767,8 +1958,6 @@ class MossAgent { delivery?: SessionInboxDelivery; id?: string; }): SessionInboxEntry; - // Warning: (ae-forgotten-export) The symbol "AgentRoleRegistry" needs to be exported by the entry point index.d.ts - // // @beta readonly agentRoles: AgentRoleRegistry; // (undocumented) @@ -1806,8 +1995,6 @@ class MossAgent { }>; // (undocumented) completeGoal(sessionKey: string, reason?: string): Promise; - // Warning: (ae-forgotten-export) The symbol "CompletionArbiter" needs to be exported by the entry point index.d.ts - // // @beta readonly completionArbiter: CompletionArbiter; // Warning: (ae-forgotten-export) The symbol "MossAgentConfig_2" needs to be exported by the entry point index.d.ts @@ -1822,6 +2009,8 @@ class MossAgent { drain: SessionDrainResult; }>; // @beta + readonly executionScheduler: ExecutionGraphScheduler; + // @beta readonly executionStore: ExecutionStore; // Warning: (ae-forgotten-export) The symbol "PlatformExtensionRegistry" needs to be exported by the entry point index.d.ts // @@ -1875,6 +2064,12 @@ class MossAgent { rewindConversation(sessionKey: string, toMessageCount: number): Promise<{ truncated: number; }>; + // Warning: (ae-forgotten-export) The symbol "createAgentExecutionMethods" needs to be exported by the entry point index.d.ts + // + // @beta + readonly runExecutionGraph: ReturnType['runExecutionGraph']; + // @beta + readonly runRoutedExecutionGraph: ReturnType['runRoutedExecutionGraph']; // Warning: (ae-forgotten-export) The symbol "SessionEvent" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1911,8 +2106,6 @@ class MossAgent { unregisterPostToolHook(name: string): boolean; // (undocumented) unregisterPreToolHook(name: string): boolean; - // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts - // // @beta readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; } @@ -3084,6 +3277,33 @@ interface ReadonlyExecResult { stdout: string; } +// @beta +interface RoutedAgentExecutionOutcome { + // (undocumented) + readonly completion?: CompletionDecision; + // (undocumented) + readonly schedule: ExecutionScheduleResult; + // Warning: (ae-forgotten-export) The symbol "AgentSynthesisResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly synthesis: AgentSynthesisResult; +} + +// Warning: (ae-forgotten-export) The symbol "RoutedAssignment" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "AgentResult" needs to be exported by the entry point index.d.ts +// +// @beta +type RoutedAgentExecutor = (routed: RoutedAssignment, context: ExecutionNodeExecutionContext) => Promise; + +// @beta +interface RoutedAssignment { + // (undocumented) + readonly assignment: AssignmentSpec; + // (undocumented) + readonly role: Readonly; + readonly workspaceLease?: WorkspaceLease; +} + // Warning: (ae-forgotten-export) The symbol "SemanticCompletionResult" needs to be exported by the entry point index.d.ts // // @beta @@ -3850,6 +4070,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -3900,6 +4125,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; @@ -4121,7 +4347,6 @@ interface WorkspaceLease { // @beta interface WorkspaceLeaseAdapter { // Warning: (ae-forgotten-export) The symbol "CreateWorkspaceLeaseInput" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "WorkspaceLease" needs to be exported by the entry point index.d.ts // // (undocumented) create(input: CreateWorkspaceLeaseInput): Promise; @@ -4137,6 +4362,8 @@ interface WorkspaceLeaseAdapter { // // (undocumented) merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + // (undocumented) + mergeStored(leaseId: string, patchId: string): Promise; // Warning: (ae-forgotten-export) The symbol "WorkspaceLeaseReleaseReason" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -4154,9 +4381,13 @@ type WorkspaceLeaseStatus = 'active' | 'merged' | 'rejected' | 'cancelled'; // @beta interface WorkspaceMergeResult { + // (undocumented) + readonly changedPaths?: readonly string[]; // (undocumented) readonly conflictingPaths: readonly string[]; // (undocumented) + readonly digest?: string; + // (undocumented) readonly patchId: string; // (undocumented) readonly status: 'merged' | 'merge_conflict'; @@ -4182,6 +4413,15 @@ interface WorkspacePatch { // Warnings were encountered during analysis: // // src/context/pruning.ts:77:3 - (ae-forgotten-export) The symbol "ContextPruningToolMatch" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:31:3 - (ae-forgotten-export) The symbol "ExecutionGraphScheduler" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:32:3 - (ae-forgotten-export) The symbol "CompletionArbiter" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:33:3 - (ae-forgotten-export) The symbol "WorkspaceLeaseAdapter" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:34:3 - (ae-forgotten-export) The symbol "AgentRoleRegistry" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:38:5 - (ae-forgotten-export) The symbol "ExecutionNodeExecutor" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:42:35 - (ae-forgotten-export) The symbol "ExecutionScheduleResult" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-forgotten-export) The symbol "RoutedAgentExecutor" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-forgotten-export) The symbol "WorkspaceLease" needs to be exported by the entry point index.d.ts +// src/core/agent/agent-execution-runtime.ts:80:81 - (ae-forgotten-export) The symbol "RoutedAgentExecutionOutcome" needs to be exported by the entry point index.d.ts // src/core/agent/moss-agent.ts:611:17 - (ae-forgotten-export) The symbol "SessionInboxDelivery" needs to be exported by the entry point index.d.ts // src/core/agent/moss-agent.ts:692:37 - (ae-forgotten-export) The symbol "SessionDrainResult" needs to be exported by the entry point index.d.ts // src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point index.d.ts diff --git a/packages/moss-agent/etc/structured-output.api.md b/packages/moss-agent/etc/structured-output.api.md index 27972093..c21b0f6b 100644 --- a/packages/moss-agent/etc/structured-output.api.md +++ b/packages/moss-agent/etc/structured-output.api.md @@ -339,6 +339,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -389,6 +394,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/etc/teaching.api.md b/packages/moss-agent/etc/teaching.api.md index 3c531ff5..10c6d9c0 100644 --- a/packages/moss-agent/etc/teaching.api.md +++ b/packages/moss-agent/etc/teaching.api.md @@ -483,6 +483,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -533,6 +538,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/etc/tools__builtin.api.md b/packages/moss-agent/etc/tools__builtin.api.md index 25ca2fe8..ba6c1af5 100644 --- a/packages/moss-agent/etc/tools__builtin.api.md +++ b/packages/moss-agent/etc/tools__builtin.api.md @@ -40,6 +40,8 @@ interface AppendExecutionEventInput { readonly id?: string; // (undocumented) readonly nodeId?: string; + // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point builtin.d.ts + readonly ownerLease?: ExecutionOwnerLease; // (undocumented) readonly time?: number; // Warning: (ae-forgotten-export) The symbol "ExecutionEventType" needs to be exported by the entry point builtin.d.ts @@ -124,6 +126,12 @@ interface ExecutionBudget { readonly usedWallTimeMs?: number; } +// Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point builtin.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point builtin.d.ts +// +// @beta +type ExecutionCompletionAppender = (graphId: string, input: AppendExecutionEventInput) => ExecutionGraphSnapshot; + // Warning: (ae-missing-release-tag) "ExecutionDomain" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -321,16 +329,14 @@ interface ExecutionState { // @beta interface ExecutionStore { // Warning: (ae-forgotten-export) The symbol "AcquireExecutionLeaseInput" needs to be exported by the entry point builtin.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionOwnerLease" needs to be exported by the entry point builtin.d.ts // // (undocumented) acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; - // Warning: (ae-forgotten-export) The symbol "AppendExecutionEventInput" needs to be exported by the entry point builtin.d.ts - // // (undocumented) append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + // Warning: (ae-forgotten-export) The symbol "ExecutionCompletionAppender" needs to be exported by the entry point builtin.d.ts + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; // Warning: (ae-forgotten-export) The symbol "CreateExecutionGraphInput" needs to be exported by the entry point builtin.d.ts - // Warning: (ae-forgotten-export) The symbol "ExecutionGraphSnapshot" needs to be exported by the entry point builtin.d.ts // // (undocumented) create(input: CreateExecutionGraphInput): ExecutionGraphSnapshot; @@ -759,6 +765,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -809,6 +820,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; @@ -896,8 +908,8 @@ export const writeFileTool: Tool; // Warnings were encountered during analysis: // // src/core/tools/tool-types.ts:73:5 - (ae-forgotten-export) The symbol "SubagentRunProgress" needs to be exported by the entry point builtin.d.ts -// src/tools/builtin.ts:517:3 - (ae-forgotten-export) The symbol "PlanControllerStore" needs to be exported by the entry point builtin.d.ts -// src/tools/builtin.ts:518:3 - (ae-forgotten-export) The symbol "ExecutionStore" needs to be exported by the entry point builtin.d.ts +// src/tools/builtin.ts:519:3 - (ae-forgotten-export) The symbol "PlanControllerStore" needs to be exported by the entry point builtin.d.ts +// src/tools/builtin.ts:520:3 - (ae-forgotten-export) The symbol "ExecutionStore" needs to be exported by the entry point builtin.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/moss-agent/etc/vision.api.md b/packages/moss-agent/etc/vision.api.md index 16d69ab5..b367ebbc 100644 --- a/packages/moss-agent/etc/vision.api.md +++ b/packages/moss-agent/etc/vision.api.md @@ -138,6 +138,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -188,6 +193,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/etc/web-browser.api.md b/packages/moss-agent/etc/web-browser.api.md index f530c218..fa68c902 100644 --- a/packages/moss-agent/etc/web-browser.api.md +++ b/packages/moss-agent/etc/web-browser.api.md @@ -182,6 +182,11 @@ interface ToolContext { extraAllowedRoots?: string[]; // (undocumented) maxSpawnDepth?: number; + // (undocumented) + mergeWorkspacePatch?: (leaseId: string, patchId: string) => Promise<{ + status: 'merged' | 'merge_conflict'; + conflictingPaths: readonly string[]; + }>; onToolOutput?: (text: string) => void; realEvidenceEligible?: boolean; resolveSubagentExpert?: (id: string) => { @@ -232,6 +237,7 @@ interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/src/core/agent/agent-execution-runtime.ts b/packages/moss-agent/src/core/agent/agent-execution-runtime.ts new file mode 100644 index 00000000..fbbb3e48 --- /dev/null +++ b/packages/moss-agent/src/core/agent/agent-execution-runtime.ts @@ -0,0 +1,347 @@ +import { randomUUID } from 'node:crypto'; +import { ErrorCode, MossError } from '../../errors.js'; +import { AssignmentRouter } from '../../orchestration/assignment-router.js'; +import { synthesizeAgentResults } from '../../orchestration/agent-result-synthesis.js'; +import type { + AgentResult, + AgentRoleRegistry, + AgentSynthesisResult, + AssignmentSpec, + CompletionArbiter, + CompletionDecision, + CompletionTaskKind, + ExecutionGraphScheduler, + ExecutionEvidence, + ExecutionNodeExecutor, + ExecutionScheduleResult, + ExecutionStore, + WorkspaceLeaseAdapter, + WorkspaceLease, + RoutedAgentExecutor, +} from '../../orchestration/index.js'; + +/** Product result for one routed execution wave and its coverage synthesis. @beta */ +export interface RoutedAgentExecutionOutcome { + readonly schedule: ExecutionScheduleResult; + readonly synthesis: AgentSynthesisResult; + readonly completion?: CompletionDecision; +} + +export function createAgentExecutionMethods(input: { + scheduler: ExecutionGraphScheduler; + arbiter: CompletionArbiter; + workspaceLeases: WorkspaceLeaseAdapter; + roles: AgentRoleRegistry; + store: ExecutionStore; + workspaceDir: string; +}) { + return { + runExecutionGraph: ( + graphId: string, + executor: ExecutionNodeExecutor, + taskKind: CompletionTaskKind = 'analysis' + ) => + runAgentExecutionGraph({ + graphId, + executor, + taskKind, + scheduler: input.scheduler, + arbiter: input.arbiter, + workspaceLeases: input.workspaceLeases, + }), + runRoutedExecutionGraph: ( + graphId: string, + executor: RoutedAgentExecutor, + taskKind: CompletionTaskKind = 'analysis', + authorizeMerge?: (lease: WorkspaceLease, patchId: string) => void | Promise + ) => + runRoutedAgentExecutionGraph({ + graphId, + executor, + taskKind, + scheduler: input.scheduler, + arbiter: input.arbiter, + workspaceLeases: input.workspaceLeases, + roles: input.roles, + store: input.store, + workspaceDir: input.workspaceDir, + ...(authorizeMerge ? { authorizeMerge } : {}), + }), + }; +} + +export async function runAgentExecutionGraph(input: { + graphId: string; + executor: ExecutionNodeExecutor; + taskKind: CompletionTaskKind; + scheduler: ExecutionGraphScheduler; + arbiter: CompletionArbiter; + workspaceLeases: WorkspaceLeaseAdapter; +}): Promise<{ schedule: ExecutionScheduleResult; completion?: CompletionDecision }> { + const schedule = await input.scheduler.runAvailable(input.graphId, input.executor); + if ( + !Object.values(schedule.graph.nodes).every( + (node) => node.status === 'succeeded' || node.status === 'skipped' + ) + ) { + return { schedule }; + } + const activeWorkspaceLeaseIds = input.workspaceLeases + .list() + .filter((lease) => lease.graphId === input.graphId && lease.status === 'active') + .map((lease) => lease.id); + const completion = await input.arbiter.decide(input.graphId, { + taskKind: input.taskKind, + activeWorkspaceLeaseIds, + }); + return { schedule, completion }; +} + +function roleKind(kind: ExecutionScheduleResult['graph']['nodes'][string]['kind']) { + if (kind === 'implementation' || kind === 'merge') return 'implementer' as const; + if (kind === 'verification') return 'verifier' as const; + return 'advisor' as const; +} + +/** Route one graph wave through authorized role snapshots and synthesize structured gaps. */ +export async function runRoutedAgentExecutionGraph(input: { + graphId: string; + executor: RoutedAgentExecutor; + taskKind: CompletionTaskKind; + scheduler: ExecutionGraphScheduler; + arbiter: CompletionArbiter; + workspaceLeases: WorkspaceLeaseAdapter; + roles: AgentRoleRegistry; + store: ExecutionStore; + workspaceDir: string; + authorizeMerge?: (lease: WorkspaceLease, patchId: string) => void | Promise; +}): Promise { + const router = new AssignmentRouter(input.roles); + const assignments: AssignmentSpec[] = []; + const results: AgentResult[] = []; + let schedule = await input.scheduler.runAvailable(input.graphId, async (node, graph, context) => { + const assignment: AssignmentSpec = { + id: `${graph.id}:${node.id}:${node.attempts + 1}`, + graphId: graph.id, + nodeId: node.id, + goal: node.title, + requiredRoleKind: roleKind(node.kind), + requiredCapabilities: node.requiredCapabilities ?? [], + inputEvidenceIds: node.dependencies.flatMap( + (dependency) => graph.nodes[dependency]?.evidenceIds ?? [] + ), + dependencies: node.dependencies, + writePaths: node.writePaths ?? [], + acceptanceCriteria: node.acceptanceCriteria ?? [], + ...(node.budget ? { budget: node.budget } : {}), + }; + if (assignment.requiredRoleKind === 'implementer' && assignment.writePaths.length === 0) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `implementation assignment "${assignment.id}" must declare write paths`, + }); + } + const workspaceLease = + assignment.requiredRoleKind === 'implementer' + ? await input.workspaceLeases.create({ + id: `assignment_${randomUUID()}`, + graphId: graph.id, + nodeId: node.id, + parentWorkspace: input.workspaceDir, + writePaths: assignment.writePaths, + }) + : undefined; + const selected = router.route(assignment); + const routed = { ...selected, ...(workspaceLease ? { workspaceLease } : {}) }; + assignments.push(assignment); + const result = await input.executor(routed, context); + const knownEvidence = new Set([ + ...graph.evidence.map(({ id }) => id), + ...result.claims.map(({ id }) => id), + ]); + let invalid = + result.assignmentId !== assignment.id || + result.roleId !== routed.role.id || + result.evidenceRefs.some((id) => !knownEvidence.has(id)); + const evidence: ExecutionEvidence[] = result.claims.map((claim) => ({ + id: claim.id, + kind: 'expert_claim' as const, + nodeId: node.id, + summary: `${claim.subject}: ${claim.conclusion}`, + createdAt: Date.now(), + metadata: { + roleId: routed.role.id, + roleKind: routed.role.kind, + runId: result.runId ?? assignment.id, + severity: claim.severity, + evidenceRefs: claim.evidenceRefs.join(','), + }, + })); + if (routed.role.kind === 'implementer' && result.status === 'PASS') { + if (!workspaceLease || !result.patchRef || !input.authorizeMerge) { + invalid = true; + } else { + await input.authorizeMerge(workspaceLease, result.patchRef); + const merge = await input.workspaceLeases.mergeStored(workspaceLease.id, result.patchRef); + if (merge.status !== 'merged') { + invalid = true; + } else { + evidence.push({ + id: `merged-${result.patchRef}`, + kind: 'patch', + nodeId: node.id, + summary: `Merged routed implementation patch ${result.patchRef}`, + createdAt: Date.now(), + metadata: { + merged: true, + patchId: result.patchRef, + runId: result.runId ?? assignment.id, + }, + }); + } + } + } + if (routed.role.kind === 'verifier' && result.status === 'PASS') { + if (!result.verification || result.verification.exitCode !== 0) { + invalid = true; + } else { + evidence.push({ + id: `verification-${assignment.id}`, + kind: 'verification', + nodeId: node.id, + summary: result.verification.summary, + createdAt: Date.now(), + ...(result.verification.artifactDigest + ? { digest: result.verification.artifactDigest } + : {}), + metadata: { + command: result.verification.command, + exitCode: result.verification.exitCode, + fresh: true, + independent: true, + roleKind: 'verifier', + roleId: routed.role.id, + runId: result.runId ?? assignment.id, + }, + }); + } + } + for (const [index, criterion] of assignment.acceptanceCriteria.entries()) { + if (result.unmetCriteria.includes(criterion)) continue; + evidence.push({ + id: `criterion-${assignment.id}-${index}`, + kind: 'expert_claim', + nodeId: node.id, + summary: `Acceptance criterion satisfied: ${criterion}`, + createdAt: Date.now(), + metadata: { criterion, roleId: routed.role.id, runId: result.runId ?? assignment.id }, + }); + } + results.push(invalid ? { ...result, status: 'FAIL' } : result); + const structuredOutput = + routed.role.kind === 'implementer' + ? evidence.some(({ kind }) => kind === 'patch') + : routed.role.kind === 'verifier' + ? evidence.some(({ kind }) => kind === 'verification') + : result.claims.length > 0 || result.evidenceRefs.length > 0; + const success = + !invalid && result.status === 'PASS' && result.unmetCriteria.length === 0 && structuredOutput; + return { + success, + evidence, + ...(!success + ? { + error: invalid + ? 'Agent result, patch merge, or verifier receipt violated its routed contract' + : `Agent result was ${result.status} with unmet criteria`, + failureFingerprint: `agent-result:${invalid ? 'invalid' : result.status}`, + } + : {}), + }; + }); + const synthesis = synthesizeAgentResults({ assignments, results }); + let graph = input.store.load(input.graphId) ?? schedule.graph; + for (const verifier of synthesis.verifierAssignments) { + if (graph.nodes[verifier.nodeId]) continue; + graph = input.store.append(graph.id, { + expectedRevision: graph.revision, + type: 'node.added', + data: { + node: { + id: verifier.nodeId, + kind: 'verification', + title: verifier.goal, + dependencies: verifier.dependencies, + requiredCapabilities: verifier.requiredCapabilities, + acceptanceCriteria: verifier.acceptanceCriteria, + }, + }, + }); + } + schedule = { ...schedule, graph }; + if ( + synthesis.missingCriteria.length > 0 || + !Object.values(graph.nodes).every( + (node) => node.status === 'succeeded' || node.status === 'skipped' + ) + ) { + return { schedule, synthesis }; + } + const activeWorkspaceLeaseIds = input.workspaceLeases + .list() + .filter((lease) => lease.graphId === input.graphId && lease.status === 'active') + .map((lease) => lease.id); + const completion = await input.arbiter.decide(input.graphId, { + taskKind: input.taskKind, + activeWorkspaceLeaseIds, + }); + return { + schedule: { ...schedule, graph: input.store.load(input.graphId) ?? graph }, + synthesis, + completion, + }; +} + +export function createWorkspacePatchMerger( + workspaceLeases: WorkspaceLeaseAdapter, + executionStore: ExecutionStore +) { + return async (leaseId: string, patchId: string) => { + const lease = workspaceLeases.load(leaseId); + if (!lease) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `unknown workspace lease "${leaseId}"`, + }); + } + const result = await workspaceLeases.mergeStored(leaseId, patchId); + const graph = executionStore.load(lease.graphId); + if (result.status === 'merged' && graph?.nodes[lease.nodeId]) { + const node = graph.nodes[lease.nodeId]; + executionStore.append(graph.id, { + expectedRevision: graph.revision, + type: 'evidence.recorded', + nodeId: lease.nodeId, + data: { + evidence: { + id: `merged-${patchId}`, + kind: 'patch', + nodeId: lease.nodeId, + summary: `Merged isolated workspace patch ${patchId}`, + createdAt: Date.now(), + digest: result.digest, + metadata: { + merged: true, + patchId, + runId: lease.nodeId, + ...(node.acceptanceCriteria?.includes('patch-merged') + ? { criterion: 'patch-merged' } + : {}), + }, + }, + }, + }); + } + return result; + }; +} diff --git a/packages/moss-agent/src/core/agent/agent-legacy-execution-import.ts b/packages/moss-agent/src/core/agent/agent-legacy-execution-import.ts new file mode 100644 index 00000000..52183438 --- /dev/null +++ b/packages/moss-agent/src/core/agent/agent-legacy-execution-import.ts @@ -0,0 +1,26 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import type { LegacyExecutionImporter, LegacyExecutionKind } from '../../orchestration/index.js'; +import { getMossWorkspacePaths } from '../../utils/workspace-paths.js'; + +export function importLegacySessionCheckpoint(input: { + importer: LegacyExecutionImporter; + workspaceDir?: string; + sessionKey: string; + kind: Extract; + state?: Readonly>; +}): void { + if (!input.state) return; + const root = path.join( + getMossWorkspacePaths(input.workspaceDir ?? process.cwd()).runtimeDir, + 'runtime', + 'legacy-checkpoints' + ); + fs.mkdirSync(root, { recursive: true, mode: 0o700 }); + input.importer.import({ + kind: input.kind, + sourcePath: path.join(root, `${encodeURIComponent(input.sessionKey)}.${input.kind}.checkpoint`), + state: input.state, + }); +} diff --git a/packages/moss-agent/src/core/agent/agent-loop-run-state.ts b/packages/moss-agent/src/core/agent/agent-loop-run-state.ts new file mode 100644 index 00000000..aacc11d8 --- /dev/null +++ b/packages/moss-agent/src/core/agent/agent-loop-run-state.ts @@ -0,0 +1,23 @@ +import type { TaskFrame } from '../goal/task-frame.js'; +import type { AgentLoopParams } from '../loop/agent-loop-types.js'; +import type { ToolCall } from '../tools/tool-types.js'; +import type { AgentHooks } from './agent-hooks.js'; +import type { createMossAgentLoopEventAdapter } from './moss-agent-loop-adapter.js'; + +export interface AgentLoopRunState { + taskFrame: TaskFrame; + activeToolCalls: Map; + lastAgentFatalError: string | undefined; + completedToolCalls: number; +} + +export interface AgentLoopRun { + params: AgentLoopParams; + state: AgentLoopRunState; + hooks: AgentHooks | undefined; + maxTurns: number; + abortSignal: AbortSignal; + adapter: ReturnType; + sessionKey: string; + userMessage: string; +} diff --git a/packages/moss-agent/src/core/agent/agent-orchestration-services.ts b/packages/moss-agent/src/core/agent/agent-orchestration-services.ts index 6a0acc5e..0ab91479 100644 --- a/packages/moss-agent/src/core/agent/agent-orchestration-services.ts +++ b/packages/moss-agent/src/core/agent/agent-orchestration-services.ts @@ -1,3 +1,4 @@ +import fs from 'node:fs'; import path from 'node:path'; import { createInMemoryMossAsyncTaskRegistry } from '@rdk-moss/core/contracts/async-task'; import { PlanControllerStore } from '../../plan-execute/plan-controller-store.js'; @@ -7,30 +8,67 @@ import { ExecutionBackedAsyncTaskRegistry } from '../../orchestration/execution- import { DEFAULT_ORCHESTRATION_POLICY } from '../../orchestration/execution-projector.js'; import { ExecutionTaskController } from '../../orchestration/execution-task-controller.js'; import { JsonlExecutionStore } from '../../orchestration/jsonl-execution-store.js'; +import { ExecutionGraphScheduler } from '../../orchestration/execution-graph-scheduler.js'; +import { LegacyExecutionImporter } from '../../orchestration/legacy-execution-importer.js'; +import { recoverExecutionGraph } from '../../orchestration/execution-recovery.js'; +import { ErrorCode, isMossError } from '../../errors.js'; import { getMossWorkspacePaths } from '../../utils/workspace-paths.js'; import type { MossAgentConfig } from './moss-agent-types.js'; export function createAgentOrchestrationServices(config: MossAgentConfig, workspaceDir: string) { const runtimeDir = getMossWorkspacePaths(workspaceDir).runtimeDir; + const executionRuntimeDir = path.join(runtimeDir, 'runtime'); const executionStore = config.executionStore ?? - new JsonlExecutionStore({ rootDir: path.join(runtimeDir, 'executions') }); + new JsonlExecutionStore({ rootDir: path.join(executionRuntimeDir, 'executions') }); + const legacyImporter = new LegacyExecutionImporter(executionStore); + const loopStatePath = path.join(runtimeDir, 'loop-state.json'); + if ( + fs.existsSync(loopStatePath) && + !fs.existsSync(`${loopStatePath}.execution-graph-migration-v1.json`) + ) { + legacyImporter.import({ + kind: 'loop', + sourcePath: loopStatePath, + state: JSON.parse(fs.readFileSync(loopStatePath, 'utf8')) as Readonly< + Record + >, + }); + } + for (const graph of executionStore.list()) { + if (graph.status !== 'running' && graph.status !== 'ready') continue; + try { + const ownerLease = executionStore.acquireLease(graph.id, { + ownerId: `startup-recovery:${process.pid}`, + ttlMs: 30_000, + }); + try { + recoverExecutionGraph(executionStore, graph.id, { ownerLease }); + } finally { + executionStore.releaseLease(graph.id, ownerLease); + } + } catch (error) { + if (!isMossError(error) || error.code !== ErrorCode.EXECUTION_LEASE_HELD) throw error; + } + } return { executionStore, + legacyImporter, asyncTasks: new ExecutionBackedAsyncTaskRegistry( config.asyncTaskRegistry ?? createInMemoryMossAsyncTaskRegistry(), executionStore ), tasks: new ExecutionTaskController(executionStore), completionArbiter: new CompletionArbiter(executionStore), + scheduler: new ExecutionGraphScheduler(executionStore), orchestrationPolicy: { ...DEFAULT_ORCHESTRATION_POLICY, ...config.orchestrationPolicy, }, workspaceLeaseAdapter: config.workspaceLeaseAdapter ?? - new AdaptiveWorkspaceLeaseAdapter({ rootDir: path.join(runtimeDir, 'workspaces') }), + new AdaptiveWorkspaceLeaseAdapter({ rootDir: path.join(executionRuntimeDir, 'workspaces') }), planControllerStore: config.planControllerStore ?? new PlanControllerStore(), }; } diff --git a/packages/moss-agent/src/core/agent/index.ts b/packages/moss-agent/src/core/agent/index.ts index fc7ab041..2e37b8d4 100644 --- a/packages/moss-agent/src/core/agent/index.ts +++ b/packages/moss-agent/src/core/agent/index.ts @@ -12,6 +12,7 @@ export { CommandQueueRegistry } from './command-queue.js'; export type { EnqueueOpts } from './command-queue.js'; export { MossAgent } from './moss-agent.js'; export type { MossAgentConfig, ChatOptions, ChatResult, MossAgentEvent } from './moss-agent.js'; +export type { RoutedAgentExecutionOutcome } from './agent-execution-runtime.js'; export { createMossAgentLoopEventAdapter, createModelDefFromMossConfig, diff --git a/packages/moss-agent/src/core/agent/moss-agent.ts b/packages/moss-agent/src/core/agent/moss-agent.ts index e0ce09a1..85274d6f 100644 --- a/packages/moss-agent/src/core/agent/moss-agent.ts +++ b/packages/moss-agent/src/core/agent/moss-agent.ts @@ -6,7 +6,7 @@ import { getRootLogger } from '../../logger.js'; const log = getRootLogger().child('agent'); import { ToolRegistry } from '../tools/tool-registry.js'; import { filterToolsForRun } from '../tools/tool-filter.js'; -import type { AgentHooks } from './agent-hooks.js'; +import type { AgentLoopRun } from './agent-loop-run-state.js'; import { KnowledgeRegistry, drainPendingGlobalModules } from '../../knowledge/registry.js'; import { buildAgentBehaviorPrompt, @@ -124,12 +124,19 @@ import { MossError, ErrorCode, errorMessage, isMossError } from '../../errors.js import type { AgentRoleRegistry, CompletionArbiter, + ExecutionGraphScheduler, ExecutionStore, ExecutionTaskController, OrchestrationPolicy, + LegacyExecutionImporter, WorkspaceLeaseAdapter, } from '../../orchestration/index.js'; import { createAgentOrchestrationServices } from './agent-orchestration-services.js'; +import { importLegacySessionCheckpoint } from './agent-legacy-execution-import.js'; +import { + createAgentExecutionMethods, + createWorkspacePatchMerger, +} from './agent-execution-runtime.js'; import type { MossAgentConfig as SharedMossAgentConfig, ChatOptions as SharedChatOptions, @@ -138,11 +145,7 @@ import type { InternalMessage as SharedInternalMessage, InternalContentBlock as SharedInternalContentBlock, } from './moss-agent-types.js'; -import { - toSessionMessages as sharedToSessionMessages, - fromSessionMessages as sharedFromSessionMessages, - toLLMMessages as sharedToLLMMessages, -} from './moss-agent-types.js'; +import { toSessionMessages, fromSessionMessages, toLLMMessages } from './moss-agent-types.js'; export type MossAgentConfig = SharedMossAgentConfig; export type ChatOptions = SharedChatOptions; @@ -150,34 +153,12 @@ export type ChatResult = SharedChatResult; export type MossAgentEvent = SharedMossAgentEvent; export type InternalMessage = SharedInternalMessage; export type InternalContentBlock = SharedInternalContentBlock; -const toSessionMessages = sharedToSessionMessages; -const fromSessionMessages = sharedFromSessionMessages; -const toLLMMessages = sharedToLLMMessages; - import { buildUserMessageContent, formatAgentError, createPreAbortedRunError, createInputGuardrailDeniedError, } from './moss-agent-helpers.js'; -interface AgentLoopRunState { - taskFrame: TaskFrame; - activeToolCalls: Map; - lastAgentFatalError: string | undefined; - completedToolCalls: number; -} - -interface AgentLoopRun { - params: AgentLoopParams; - state: AgentLoopRunState; - hooks: AgentHooks | undefined; - maxTurns: number; - abortSignal: AbortSignal; - adapter: ReturnType; - sessionKey: string; - - userMessage: string; -} export class MossAgent { readonly pendingToolAborts = new PendingToolAbortStore(); readonly tools: ToolRegistry; @@ -186,20 +167,21 @@ export class MossAgent { readonly commandQueues: CommandQueueRegistry; readonly spawnRegistry: SpawnProfileRegistry; readonly asyncTasks: MossAsyncTaskRegistry; - /** Authoritative graph store shared by product projections. @beta */ - readonly executionStore: ExecutionStore; - /** Shared task control projection for every host surface. @beta */ - readonly tasks: ExecutionTaskController; - /** Deterministic-first completion authority for this agent's graphs. @beta */ - readonly completionArbiter: CompletionArbiter; - /** Effective visible scheduling policy. @beta */ - readonly orchestrationPolicy: OrchestrationPolicy; - /** Isolated implementation workspace seam when configured by the host. @beta */ - readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; - /** Instance-local registry used by built-in and plugin agent roles. @beta */ - readonly agentRoles: AgentRoleRegistry; - /** Instance-local compatibility projection for legacy Plan tools. @beta */ - readonly planControllerStore: PlanControllerStore; + /** Authoritative product graph store. @beta */ readonly executionStore: ExecutionStore; + /** Shared product task controls. @beta */ readonly tasks: ExecutionTaskController; + /** Deterministic completion authority. @beta */ readonly completionArbiter: CompletionArbiter; + /** Lease-fenced DAG scheduler. @beta */ readonly executionScheduler: ExecutionGraphScheduler; + /** Effective scheduling policy. @beta */ readonly orchestrationPolicy: OrchestrationPolicy; + /** Isolated implementation workspaces. @beta */ readonly workspaceLeaseAdapter: WorkspaceLeaseAdapter; + /** Built-in and plugin role registry. @beta */ readonly agentRoles: AgentRoleRegistry; + /** Legacy Plan compatibility projection. @beta */ readonly planControllerStore: PlanControllerStore; + /** Run one dependency-ready graph wave, then arbitrate completion. @beta */ + readonly runExecutionGraph: ReturnType['runExecutionGraph']; + /** Route a graph wave through authorized role snapshots. @beta */ + readonly runRoutedExecutionGraph: ReturnType< + typeof createAgentExecutionMethods + >['runRoutedExecutionGraph']; + private readonly legacyExecutionImporter: LegacyExecutionImporter; /** @beta */ readonly plugins: MossPluginHost; private readonly knowledge: KnowledgeRegistry; @@ -237,9 +219,11 @@ export class MossAgent { const workspaceDir = path.resolve(config.workspaceDir ?? process.cwd()); const orchestration = createAgentOrchestrationServices(config, workspaceDir); this.executionStore = orchestration.executionStore; + this.legacyExecutionImporter = orchestration.legacyImporter; this.asyncTasks = orchestration.asyncTasks; this.tasks = orchestration.tasks; this.completionArbiter = orchestration.completionArbiter; + this.executionScheduler = orchestration.scheduler; this.orchestrationPolicy = orchestration.orchestrationPolicy; this.workspaceLeaseAdapter = orchestration.workspaceLeaseAdapter; this.planControllerStore = orchestration.planControllerStore; @@ -249,6 +233,16 @@ export class MossAgent { const composition = createAgentPluginComposition(config, this.tools); this.expertRegistry = composition.expertRegistry; this.agentRoles = composition.agentRoleRegistry; + const executionMethods = createAgentExecutionMethods({ + scheduler: this.executionScheduler, + arbiter: this.completionArbiter, + workspaceLeases: this.workspaceLeaseAdapter, + roles: this.agentRoles, + store: this.executionStore, + workspaceDir, + }); + this.runExecutionGraph = executionMethods.runExecutionGraph; + this.runRoutedExecutionGraph = executionMethods.runRoutedExecutionGraph; this.plugins = composition.pluginHost; this.packPromptLayers = composition.packPromptLayers; this.packHostRequirements = composition.packHostRequirements; @@ -262,6 +256,7 @@ export class MossAgent { this.extensions.setKnowledgeRegistry(this.knowledge); drainPendingGlobalModules(this.knowledge); } + /** Bind a host-owned question channel to this agent instance. @beta */ setUserQuestionAsker(asker: NonNullable): () => void { this.userQuestionAsker = asker; @@ -308,10 +303,7 @@ export class MossAgent { }); } - /** - * Stop exactly one host-approved preflight child. This never aborts the - * parent or sibling assignments; pre-start requests become tombstones. - */ + /** Stop one preflight child without aborting its parent or siblings. */ stopApprovedPreflightAssignment( runId: string, assignmentId: string @@ -339,7 +331,7 @@ export class MossAgent { } if (this.config.domainPrompt === false) { - // domainPrompt explicitly disabled: add no domain prompt + // Explicitly disabled: add no domain prompt. } else if (typeof this.config.domainPrompt === 'function') { parts.push(this.config.domainPrompt()); } else { @@ -463,7 +455,15 @@ export class MossAgent { messages: LLMMessage[]; }> { const latest = await this.config.sessionStore.loadMessages(sessionKey); - return splitGoalCheckpointMessages(latest); + const split = splitGoalCheckpointMessages(latest); + importLegacySessionCheckpoint({ + importer: this.legacyExecutionImporter, + ...(this.config.workspaceDir ? { workspaceDir: this.config.workspaceDir } : {}), + sessionKey, + kind: 'goal', + ...(split.goal ? { state: { ...split.goal } } : {}), + }); + return split; } private async saveGoalState( @@ -1023,17 +1023,26 @@ export class MossAgent { } } - // SessionStore.Message[] -> InternalMessage[] (structurally compatible, see line 764). const loadedMessages = (await abortable( store.loadMessages(sessionKey), abortSignal )) as unknown as InternalMessage[]; - // InternalMessage[] -> LLMMessage[] for goal checkpoint splitting (see line 764). const goalLoad = splitGoalCheckpointMessages(loadedMessages as unknown as LLMMessage[]); const taskFrameLoad = splitTaskFrameCheckpointMessages( - // LLMMessage[] -> InternalMessage[] for session message conversion (see line 764). toSessionMessages(goalLoad.messages as unknown as InternalMessage[]) ); + for (const [kind, state] of [ + ['goal', goalLoad.goal], + ['task-frame', taskFrameLoad.frame], + ] as const) { + importLegacySessionCheckpoint({ + importer: this.legacyExecutionImporter, + ...(this.config.workspaceDir ? { workspaceDir: this.config.workspaceDir } : {}), + sessionKey, + kind, + ...(state ? { state: { ...state } } : {}), + }); + } const continuationIntent = detectContinuationIntent(activeUserMessage); const taskFrame = createOrUpdateTaskFrame({ previous: taskFrameLoad.frame, @@ -1049,7 +1058,6 @@ export class MossAgent { }; messages.push(userMsg); - // InternalMessage -> LLMMessage for store append (see line 764). await store.appendMessage(sessionKey, userMsg as unknown as LLMMessage); const workingContext = buildTaskFrameContext(taskFrame, continuationIntent); @@ -1102,6 +1110,10 @@ export class MossAgent { ...(options?.toolInputLimits ? { toolInputLimits: options.toolInputLimits } : {}), ...(options?.toolInputOverrides ? { toolInputOverrides: options.toolInputOverrides } : {}), asyncTaskRegistry: this.asyncTasks, + mergeWorkspacePatch: createWorkspacePatchMerger( + this.workspaceLeaseAdapter, + this.executionStore + ), }; const adapter = createMossAgentLoopEventAdapter({ @@ -1183,16 +1195,12 @@ export class MossAgent { spawnedCount++; const childRunId = `${runId}/sub-${crypto.randomUUID().slice(0, 8)}`; const timeoutMs = params.timeoutMs ?? 120_000; - // Resolve an overridden model's context window through the host when available. let overrideContextTokens: number | undefined; if (params.model && this.config.resolveModelContextTokens) { try { overrideContextTokens = await this.config.resolveModelContextTokens(params.model); - } catch { - // Fall back to the parent's context window. - } + } catch {} } - // Foreground children enforce timeout and parent abort without the fan-out orchestrator. const parentSignal = params.abortSignal ?? abortSignal; const controller = new AbortController(); const onParentAbort = () => controller.abort(); @@ -1383,7 +1391,6 @@ export class MossAgent { contextTokens, steeringEngine: this.steeringEngine ?? undefined, appendMessage: async (key, msg) => { - // InternalMessage -> LLMMessage for store append (see line 764). await store.appendMessage(key, msg as unknown as LLMMessage); }, replaceMessages: async (key, nextMessages) => { @@ -1506,9 +1513,7 @@ export class MossAgent { ...(options?.platform ? { platform: options.platform } : {}), }) : undefined, - // Stream live text unless structured-output validation is pending for - // this run (gate may rewrite/discard the answer). Host coding gates that - // only inject a follow-up correction do not need buffering. + // Buffer only when a gate may rewrite or discard the answer. shouldBufferAssistantOutput: () => Boolean(peekPendingStructuredValidation(sessionKey, runId)) || this.config.bufferAssistantUntilComplete === true, @@ -1555,9 +1560,7 @@ export class MossAgent { retryLimit: pending.maxRetries - 1, }; } - // Retry: bump the attempt counter, re-register, and reject with the - // enforcer's retry feedback so the agent loop injects it as a - // correction message and re-prompts. + // Re-register validation and feed its correction into the retry loop. bumpStructuredValidationAttempt(sessionKey, request.runId); return { ok: false, @@ -1566,9 +1569,7 @@ export class MossAgent { retryLimit: pending.maxRetries - 1, }; }, - // Per-instance run-epoch store: multiple MossAgent instances embedded in - // the same host must NOT stomp each other's live streams for the same - // sessionKey. Each instance carries its own Map (created in constructor). + // Per-instance epochs isolate same-session streams across embedded agents. runEpochStore: this.runEpochStore, pendingToolAborts: this.pendingToolAborts, }; @@ -1636,8 +1637,6 @@ export class MossAgent { ] : [...cleanMessages, ...(goalCheckpoint ? [goalCheckpoint] : []), checkpoint]; - // Cast InternalMessage[] to the run params currentMessages type (structurally - // compatible, see line 764 for the full explanation). const nextSessionMessages = nextMessages as unknown as typeof run.params.currentMessages; run.params.currentMessages.splice(0, run.params.currentMessages.length, ...nextSessionMessages); await run.params.replaceMessages?.(run.sessionKey, run.params.currentMessages); @@ -1812,8 +1811,6 @@ export class MossAgent { try { sessionMessages = (await this.config.sessionStore.loadMessages( run.sessionKey - // SessionStore.Message[] -> LLMMessage[] for skill learning extraction - // (structurally compatible, see line 764 for the full explanation). )) as unknown as LLMMessage[]; } catch (err) { log.warn('failed to load session messages (non-critical)', { @@ -1913,12 +1910,7 @@ export class MossAgent { ): AsyncGenerator { const model = String(this.config.model); const sessionStart = Date.now(); - // Session root span covers every CLI entry (oneshot / piped / TUI all - // funnel through streamChat). turn → llm/tool child spans nest under it - // via the active context. When the host passes a runId (an embedding - // host's own run identifier), record it as the span's runId so host-side - // trace collection can correlate spans to its own run records; otherwise - // fall back to the sessionKey as before. + // One session span parents all turn, model, and tool spans for host correlation. const hostRunId = typeof (options as { runId?: unknown } | undefined)?.runId === 'string' ? ((options as { runId?: string }).runId as string) @@ -1932,13 +1924,7 @@ export class MossAgent { this.noteRunStarted(sessionKey, run.params.runId); let done: Extract | undefined; - // Run the agent loop inside the session span's context so child spans - // (moss.agent.turn, moss.llm.request, moss.tool.invoke) nest under - // moss.session — share its traceId with the session span as parent. - // runInSpanContextGen keeps the span's context active across the - // generator's yields/awaits (AsyncLocalStorage propagation). The mini - // stream is created INSIDE the generator so runAgentLoop's background - // async task inherits the session context at startup. + // Create the stream inside the span so async descendants inherit its trace context. try { for await (const event of sessionSpan.runInSpanContextGen( async function* (self: MossAgent) { @@ -1954,8 +1940,7 @@ export class MossAgent { yield event; } } finally { - // Session metrics on every exit path (success or failure). - // end_turn → ok; explicit error stopReason → error; otherwise incomplete. + // Record session metrics on every exit path. const outcome = sessionResult ? sessionResult.stopReason === 'end_turn' ? 'ok' diff --git a/packages/moss-agent/src/core/index.ts b/packages/moss-agent/src/core/index.ts index 64c43a3c..ac61c315 100644 --- a/packages/moss-agent/src/core/index.ts +++ b/packages/moss-agent/src/core/index.ts @@ -15,7 +15,13 @@ export { export { CommandQueueRegistry } from './agent/index.js'; export type { EnqueueOpts } from './agent/index.js'; export { MossAgent } from './agent/index.js'; -export type { MossAgentConfig, ChatOptions, ChatResult, MossAgentEvent } from './agent/index.js'; +export type { + MossAgentConfig, + ChatOptions, + ChatResult, + MossAgentEvent, + RoutedAgentExecutionOutcome, +} from './agent/index.js'; export { createMossAgentLoopEventAdapter, createModelDefFromMossConfig } from './agent/index.js'; export type { MossAgentLoopEventAdapter, MossAgentLoopEventAdapterOptions } from './agent/index.js'; diff --git a/packages/moss-agent/src/core/subagent/subagent-orchestrator.ts b/packages/moss-agent/src/core/subagent/subagent-orchestrator.ts index 7a8041ed..639a418e 100644 --- a/packages/moss-agent/src/core/subagent/subagent-orchestrator.ts +++ b/packages/moss-agent/src/core/subagent/subagent-orchestrator.ts @@ -64,6 +64,7 @@ export interface SubAgentResult { success: boolean; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; diff --git a/packages/moss-agent/src/core/subagent/subagent-runner.ts b/packages/moss-agent/src/core/subagent/subagent-runner.ts index 2fda5839..82a10229 100644 --- a/packages/moss-agent/src/core/subagent/subagent-runner.ts +++ b/packages/moss-agent/src/core/subagent/subagent-runner.ts @@ -497,6 +497,7 @@ export function createSubAgentRunner(deps: SubAgentRunnerDeps): SubAgentRunner { ...(workspaceLease ? { workspaceLeaseId: workspaceLease.id } : {}), ...(workspacePatch ? { + patchId: workspacePatch.id, patchRef: workspacePatch.artifactRef, patchDigest: workspacePatch.digest, changedPaths: workspacePatch.changedPaths, diff --git a/packages/moss-agent/src/core/task-run/task-run-ledger.ts b/packages/moss-agent/src/core/task-run/task-run-ledger.ts index 208e18c3..af5206a9 100644 --- a/packages/moss-agent/src/core/task-run/task-run-ledger.ts +++ b/packages/moss-agent/src/core/task-run/task-run-ledger.ts @@ -201,16 +201,18 @@ export class TaskRunLedger { }); } else if (event.type === 'run.verified' || event.type === 'run.rejected') { const verdict = event.type === 'run.verified' ? 'verified' : 'rejected'; - append('verification.recorded', { - verification: { - verdict, - evidenceIds: graph.evidence.map((evidence) => evidence.id), - reasons: verdict === 'verified' ? [] : ['legacy verifier rejected the run'], - verifiedAt: event.time, + append('evidence.recorded', { + evidence: { + id: `task-run-verdict-${event.id}`, + kind: 'expert_claim', + summary: `Legacy TaskRun reported ${verdict}`, + createdAt: event.time, + metadata: { source: 'task-run-v1', legacyVerdict: verdict }, }, }); - append(verdict === 'verified' ? 'graph.completed' : 'graph.failed', { + append(verdict === 'verified' ? 'graph.blocked' : 'graph.failed', { source: 'task-run-v1-verdict', + ...(verdict === 'verified' ? { reason: 'needs_evidence' } : {}), }); } // run.completed intentionally does not complete the graph: a completion diff --git a/packages/moss-agent/src/core/tools/tool-types.ts b/packages/moss-agent/src/core/tools/tool-types.ts index 4a24b97c..50376a9a 100644 --- a/packages/moss-agent/src/core/tools/tool-types.ts +++ b/packages/moss-agent/src/core/tools/tool-types.ts @@ -81,10 +81,15 @@ export interface ToolContext { durationMs?: number; error?: string; workspaceLeaseId?: string; + patchId?: string; patchRef?: string; patchDigest?: string; changedPaths?: readonly string[]; }>; + mergeWorkspacePatch?: ( + leaseId: string, + patchId: string + ) => Promise<{ status: 'merged' | 'merge_conflict'; conflictingPaths: readonly string[] }>; /** Resolve a model-requested expert id through the host-trusted per-agent registry. */ resolveSubagentExpert?: (id: string) => | { diff --git a/packages/moss-agent/src/index.ts b/packages/moss-agent/src/index.ts index 24ffcf47..3c8b0a40 100644 --- a/packages/moss-agent/src/index.ts +++ b/packages/moss-agent/src/index.ts @@ -65,7 +65,13 @@ export { export type { RuntimeCapabilitiesPromptOptions, RuntimeCapabilityTool } from './context/index.js'; export { MossAgent } from './core/index.js'; -export type { MossAgentConfig, ChatOptions, ChatResult, MossAgentEvent } from './core/index.js'; +export type { + MossAgentConfig, + ChatOptions, + ChatResult, + MossAgentEvent, + RoutedAgentExecutionOutcome, +} from './core/index.js'; // Soul / identity — embeddable (moved from cli/ to core/agent/ for clean embedding) export { resolveSoulIdentity, resolveSoul } from './core/agent/soul.js'; export { @@ -218,6 +224,7 @@ export type { AgentRoleKind, AgentRoleRegistryOptions, AgentSynthesisResult, + AgentVerificationReceipt, AppendExecutionEventInput, AssignmentSpec, CompletionArbiterInput, @@ -226,15 +233,18 @@ export type { CreateExecutionGraphInput, CreateWorkspaceLeaseInput, ExecutionBudget, + ExecutionCompletionAppender, ExecutionEvent, ExecutionEventType, ExecutionEvidence, ExecutionEvidenceKind, ExecutionGraphSnapshot, ExecutionGraphStatus, + ExecutionGraphSchedulerOptions, ExecutionNode, ExecutionNodeDefinition, ExecutionNodeExecutor, + ExecutionNodeExecutionContext, ExecutionNodeKind, ExecutionNodeRunResult, ExecutionNodeStatus, @@ -250,6 +260,7 @@ export type { LegacyExecutionKind, RecoverExecutionGraphOptions, RoutedAssignment, + RoutedAgentExecutor, SemanticCompletionJudge, SemanticCompletionResult, WorkspaceLease, diff --git a/packages/moss-agent/src/orchestration/adaptive-workspace-lease.ts b/packages/moss-agent/src/orchestration/adaptive-workspace-lease.ts index 73890943..a505ee5d 100644 --- a/packages/moss-agent/src/orchestration/adaptive-workspace-lease.ts +++ b/packages/moss-agent/src/orchestration/adaptive-workspace-lease.ts @@ -66,6 +66,19 @@ export class AdaptiveWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter { return this.adapterFor(lease).merge(lease, patch); } + mergeStored(leaseId: string, patchId: string): Promise { + const lease = this.load(leaseId); + if (!lease) { + return Promise.reject( + new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `unknown workspace lease "${leaseId}"`, + }) + ); + } + return this.adapterFor(lease).mergeStored(leaseId, patchId); + } + release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise { const lease = this.load(leaseId); return lease ? this.adapterFor(lease).release(leaseId, reason) : Promise.resolve(); diff --git a/packages/moss-agent/src/orchestration/agent-role-registry.ts b/packages/moss-agent/src/orchestration/agent-role-registry.ts index f37fa3aa..cfbd931b 100644 --- a/packages/moss-agent/src/orchestration/agent-role-registry.ts +++ b/packages/moss-agent/src/orchestration/agent-role-registry.ts @@ -54,10 +54,13 @@ export class AgentRoleRegistry { message: `implementer role "${role.id}" must use isolated-write`, }); } - if (role.kind === 'advisor' && role.workspaceMode !== 'shared-readonly') { + if ( + (role.kind === 'advisor' || role.kind === 'verifier') && + role.workspaceMode !== 'shared-readonly' + ) { throw new MossError({ code: ErrorCode.USER_INPUT_INVALID, - message: `advisor role "${role.id}" must use shared-readonly`, + message: `${role.kind} role "${role.id}" must use shared-readonly`, }); } if (role.workspaceMode === 'isolated-write' && !this.allowIsolatedWrite) { diff --git a/packages/moss-agent/src/orchestration/agent-role-types.ts b/packages/moss-agent/src/orchestration/agent-role-types.ts index d7552d9d..7c789b6d 100644 --- a/packages/moss-agent/src/orchestration/agent-role-types.ts +++ b/packages/moss-agent/src/orchestration/agent-role-types.ts @@ -1,4 +1,5 @@ -import type { ExecutionBudget } from './execution-types.js'; +import type { ExecutionBudget, ExecutionNodeExecutionContext } from './execution-types.js'; +import type { WorkspaceLease } from './workspace-lease-types.js'; /** Root-orchestrated role category. Workers cannot delegate recursively. @beta */ export type AgentRoleKind = 'advisor' | 'implementer' | 'verifier'; @@ -48,6 +49,8 @@ export interface AssignmentSpec { export interface RoutedAssignment { readonly assignment: AssignmentSpec; readonly role: Readonly; + /** Isolated workspace provisioned by the root for an implementation assignment. */ + readonly workspaceLease?: WorkspaceLease; } /** Structured claim emitted by an expert role. @beta */ @@ -66,11 +69,28 @@ export interface AgentResult { readonly status: 'PASS' | 'FAIL' | 'PARTIAL'; readonly claims: readonly AgentClaim[]; readonly evidenceRefs: readonly string[]; + /** Adapter-owned patch ID produced from the routed workspace lease. */ readonly patchRef?: string; + /** Machine receipt returned only by a trusted verifier executor. */ + readonly verification?: AgentVerificationReceipt; readonly unmetCriteria: readonly string[]; readonly runId?: string; } +/** Fresh command receipt produced by a trusted verifier executor. @beta */ +export interface AgentVerificationReceipt { + readonly command: string; + readonly exitCode: number; + readonly summary: string; + readonly artifactDigest?: string; +} + +/** Host callback for one capability-routed, non-recursive role assignment. @beta */ +export type RoutedAgentExecutor = ( + routed: RoutedAssignment, + context: ExecutionNodeExecutionContext +) => Promise; + /** Contradictory claims requiring disclosure or independent verification. @beta */ export interface AgentResultConflict { readonly subject: string; diff --git a/packages/moss-agent/src/orchestration/completion-arbiter.ts b/packages/moss-agent/src/orchestration/completion-arbiter.ts index dc454283..22eab0c7 100644 --- a/packages/moss-agent/src/orchestration/completion-arbiter.ts +++ b/packages/moss-agent/src/orchestration/completion-arbiter.ts @@ -1,9 +1,11 @@ import type { ExecutionEvidence, + ExecutionCompletionAppender, ExecutionGraphSnapshot, ExecutionStore, ExecutionVerification, } from './execution-types.js'; +import { executionCompletionAuthority } from './completion-authority-internal.js'; /** Task-specific evidence policy selected by the trusted host. @beta */ export type CompletionTaskKind = 'coding' | 'research' | 'device' | 'analysis'; @@ -104,6 +106,11 @@ function deterministicDecision( if (patches.length === 0) reasons.push('Coding completion requires merged patch evidence.'); for (const patch of patches) evidenceIds.add(patch.id); const newestPatchAt = Math.max(...patches.map((patch) => patch.createdAt), 0); + const patchRunIds = new Set( + patches + .map((patch) => patch.metadata?.runId) + .filter((runId): runId is string => typeof runId === 'string') + ); const verificationNodes = nodes.filter((node) => node.kind === 'verification'); if ( verificationNodes.length === 0 || @@ -115,6 +122,10 @@ function deterministicDecision( (evidence) => evidence.metadata?.fresh === true && evidence.metadata?.exitCode === 0 && + evidence.metadata?.roleKind === 'verifier' && + evidence.metadata?.independent === true && + typeof evidence.metadata?.runId === 'string' && + !patchRunIds.has(evidence.metadata.runId) && evidence.createdAt > newestPatchAt ); if (fresh.length === 0) { @@ -156,7 +167,11 @@ function deterministicDecision( /** Deterministic-first completion authority for one execution store. @beta */ export class CompletionArbiter { - constructor(private readonly store: ExecutionStore) {} + private readonly appendCompletion: ExecutionCompletionAppender; + + constructor(private readonly store: ExecutionStore) { + this.appendCompletion = store.bindCompletionAuthority(executionCompletionAuthority, this); + } async decide(graphId: string, input: CompletionArbiterInput): Promise { let graph = this.store.load(graphId); @@ -181,14 +196,14 @@ export class CompletionArbiter { reasons: decision.reasons, verifiedAt, }; - graph = this.store.append(graphId, { + graph = this.appendCompletion(graphId, { expectedRevision: graph.revision, type: 'verification.recorded', time: verifiedAt, data: { verification }, }); if (decision.status === 'completed') { - this.store.append(graphId, { + this.appendCompletion(graphId, { expectedRevision: graph.revision, type: 'graph.completed', time: verifiedAt, diff --git a/packages/moss-agent/src/orchestration/completion-authority-internal.ts b/packages/moss-agent/src/orchestration/completion-authority-internal.ts new file mode 100644 index 00000000..92134c06 --- /dev/null +++ b/packages/moss-agent/src/orchestration/completion-authority-internal.ts @@ -0,0 +1,7 @@ +/** + * Package-private capability used to authorize terminal execution events. + * + * This value is intentionally absent from every package export. Stores may expose + * a forwarding seam for composition, but callers cannot mint the capability. + */ +export const executionCompletionAuthority = Object.freeze({}); diff --git a/packages/moss-agent/src/orchestration/execution-async-task-registry.ts b/packages/moss-agent/src/orchestration/execution-async-task-registry.ts index a878d1bc..fd874e08 100644 --- a/packages/moss-agent/src/orchestration/execution-async-task-registry.ts +++ b/packages/moss-agent/src/orchestration/execution-async-task-registry.ts @@ -11,6 +11,7 @@ import type { MossAsyncTaskUpdate, } from '@rdk-moss/core/contracts/async-task'; import type { ExecutionGraphSnapshot, ExecutionStore } from './execution-types.js'; +import { CompletionArbiter } from './completion-arbiter.js'; const NODE_ID = 'background-task'; @@ -66,7 +67,7 @@ export class ExecutionBackedAsyncTaskRegistry implements MossAsyncTaskRegistry { const wrapped: MossAsyncTaskRunner = async (input, signal) => { try { const result = await runner(input, signal); - this.finish(request.taskId, result); + await this.finish(request.taskId, result); return result; } catch (error) { this.fail(request.taskId, error); @@ -134,7 +135,7 @@ export class ExecutionBackedAsyncTaskRegistry implements MossAsyncTaskRegistry { return this.delegate.readCompletion(taskId); } - private finish(taskId: string, result: MossAsyncTaskResult): void { + private async finish(taskId: string, result: MossAsyncTaskResult): Promise { let graph = this.store.load(graphId(taskId)); if (!graph || graph.nodes[NODE_ID]?.status !== 'running') return; graph = append(this.store, graph, 'evidence.recorded', { @@ -153,7 +154,11 @@ export class ExecutionBackedAsyncTaskRegistry implements MossAsyncTaskRegistry { result.success ? 'node.succeeded' : 'node.failed', result.success ? {} : { error: 'background task reported failure' } ); - append(this.store, graph, result.success ? 'graph.completed' : 'graph.failed'); + if (result.success) { + await new CompletionArbiter(this.store).decide(graph.id, { taskKind: 'analysis' }); + } else { + append(this.store, graph, 'graph.failed'); + } } private fail(taskId: string, error: unknown): void { diff --git a/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts b/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts index a66e7e01..a6f4278c 100644 --- a/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts +++ b/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto'; import path from 'node:path'; import { ErrorCode, MossError, errorMessage } from '../errors.js'; @@ -6,10 +7,18 @@ import type { ExecutionGraphSnapshot, ExecutionNode, ExecutionNodeExecutor, + ExecutionOwnerLease, ExecutionScheduleResult, ExecutionStore, } from './execution-types.js'; +/** Local owner-lease timings and identity for one scheduler instance. @beta */ +export interface ExecutionGraphSchedulerOptions { + readonly ownerId?: string; + readonly leaseTtlMs?: number; + readonly leaseRenewIntervalMs?: number; +} + const DEPENDENCY_FAILURES = new Set([ 'failed', 'blocked', @@ -67,25 +76,36 @@ function budgetReason(graph: ExecutionGraphSnapshot): string | undefined { /** Instance-owned dependency scheduler for one or more execution graphs. @beta */ export class ExecutionGraphScheduler { - constructor(private readonly store: ExecutionStore) {} + private readonly ownerId: string; + private readonly leaseTtlMs: number; + private readonly leaseRenewIntervalMs: number; + + constructor( + private readonly store: ExecutionStore, + options: ExecutionGraphSchedulerOptions = {} + ) { + this.ownerId = options.ownerId ?? `scheduler:${process.pid}:${randomUUID()}`; + this.leaseTtlMs = Math.max(1, options.leaseTtlMs ?? 30_000); + this.leaseRenewIntervalMs = Math.max( + 1, + Math.min(options.leaseRenewIntervalMs ?? 10_000, this.leaseTtlMs - 1 || 1) + ); + } reconcile(graphId: string): ExecutionGraphSnapshot { + return this.withLease(graphId, (lease) => this.reconcileOwned(graphId, lease)); + } + + private reconcileOwned(graphId: string, lease: ExecutionOwnerLease): ExecutionGraphSnapshot { let graph = this.requiredGraph(graphId); + if (graph.status !== 'running') return graph; const exhausted = budgetReason(graph); if (exhausted) { - if (graph.status !== 'paused') { - graph = this.store.append(graphId, { - expectedRevision: graph.revision, - type: 'graph.paused', - data: { reason: exhausted, budget: graph.budget }, - }); - } else { - graph = this.store.append(graphId, { - expectedRevision: graph.revision, - type: 'graph.paused', - data: { reason: exhausted, budget: graph.budget }, - }); - } + graph = this.append(graphId, lease, { + expectedRevision: graph.revision, + type: 'graph.paused', + data: { reason: exhausted, budget: graph.budget }, + }); return graph; } @@ -105,7 +125,7 @@ export class ExecutionGraphScheduler { failedDependencies.length > 0 && (node.status === 'pending' || node.status === 'ready') ) { - graph = this.store.append(graphId, { + graph = this.append(graphId, lease, { expectedRevision: graph.revision, type: 'node.blocked', nodeId: node.id, @@ -122,7 +142,7 @@ export class ExecutionGraphScheduler { (node.status === 'pending' || (node.status === 'blocked' && (node.blockedByDependencies?.length ?? 0) > 0)) ) { - graph = this.store.append(graphId, { + graph = this.append(graphId, lease, { expectedRevision: graph.revision, type: 'node.ready', nodeId: node.id, @@ -137,9 +157,7 @@ export class ExecutionGraphScheduler { selectRunnable(graph: ExecutionGraphSnapshot): readonly ExecutionNode[] { if (budgetReason(graph)) return []; - if (graph.status === 'blocked' || graph.status === 'completed' || graph.status === 'failed') { - return []; - } + if (graph.status !== 'running') return []; const active = Object.values(graph.nodes).filter( (node) => node.status === 'leased' || node.status === 'running' ); @@ -162,10 +180,18 @@ export class ExecutionGraphScheduler { } startNode(graphId: string, nodeId: string): ExecutionGraphSnapshot { - let graph = this.reconcile(graphId); + return this.withLease(graphId, (lease) => this.startNodeOwned(graphId, nodeId, lease)); + } + + private startNodeOwned( + graphId: string, + nodeId: string, + lease: ExecutionOwnerLease + ): ExecutionGraphSnapshot { + let graph = this.reconcileOwned(graphId, lease); let node = this.requiredNode(graph, nodeId); if (node.status === 'failed' || node.status === 'interrupted') { - graph = this.store.append(graphId, { + graph = this.append(graphId, lease, { expectedRevision: graph.revision, type: 'node.retry_requested', nodeId, @@ -179,12 +205,12 @@ export class ExecutionGraphScheduler { }); } if (graph.status !== 'running') { - graph = this.store.append(graphId, { - expectedRevision: graph.revision, - type: 'graph.resumed', + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `execution graph "${graphId}" is not running (${graph.status})`, }); } - return this.store.append(graphId, { + return this.append(graphId, lease, { expectedRevision: graph.revision, type: 'node.started', nodeId, @@ -196,16 +222,19 @@ export class ExecutionGraphScheduler { nodeId: string, evidence: readonly ExecutionEvidence[] = [] ): ExecutionGraphSnapshot { - let graph = this.requiredGraph(graphId); - for (const item of evidence) { - graph = this.store.append(graphId, { - expectedRevision: graph.revision, - type: 'evidence.recorded', - nodeId, - data: { evidence: { ...item, nodeId: item.nodeId ?? nodeId } }, - }); - } - return this.store.append(graphId, { + return this.withLease(graphId, (lease) => + this.succeedNodeOwned(graphId, nodeId, evidence, lease) + ); + } + + private succeedNodeOwned( + graphId: string, + nodeId: string, + evidence: readonly ExecutionEvidence[], + lease: ExecutionOwnerLease + ): ExecutionGraphSnapshot { + const graph = this.recordEvidenceOwned(graphId, nodeId, evidence, lease); + return this.append(graphId, lease, { expectedRevision: graph.revision, type: 'node.succeeded', nodeId, @@ -216,9 +245,18 @@ export class ExecutionGraphScheduler { graphId: string, nodeId: string, failure: { readonly error: string; readonly failureFingerprint?: string } + ): ExecutionGraphSnapshot { + return this.withLease(graphId, (lease) => this.failNodeOwned(graphId, nodeId, failure, lease)); + } + + private failNodeOwned( + graphId: string, + nodeId: string, + failure: { readonly error: string; readonly failureFingerprint?: string }, + lease: ExecutionOwnerLease ): ExecutionGraphSnapshot { let graph = this.requiredGraph(graphId); - graph = this.store.append(graphId, { + graph = this.append(graphId, lease, { expectedRevision: graph.revision, type: 'node.failed', nodeId, @@ -229,7 +267,7 @@ export class ExecutionGraphScheduler { }); const node = this.requiredNode(graph, nodeId); if (node.attempts >= graph.policy.maxAttemptsPerNode || node.consecutiveSameFailures >= 3) { - graph = this.store.append(graphId, { + graph = this.append(graphId, lease, { expectedRevision: graph.revision, type: 'node.blocked', nodeId, @@ -243,41 +281,134 @@ export class ExecutionGraphScheduler { graphId: string, executor: ExecutionNodeExecutor ): Promise { - let graph = this.reconcile(graphId); - const candidates = this.selectRunnable(graph); - const started: ExecutionNode[] = []; - for (const candidate of candidates) { - graph = this.startNode(graphId, candidate.id); - started.push(this.requiredNode(graph, candidate.id)); - } - const settled = await Promise.all( - started.map(async (node) => { - try { - return { node, result: await executor(node, graph) }; - } catch (error) { - return { - node, - result: { - success: false, - error: errorMessage(error), - failureFingerprint: `executor:${error instanceof Error ? error.name : 'unknown'}`, + let lease = this.acquireLease(graphId); + let renewalFailure: unknown; + let operationFailure: unknown; + const executionAbort = new AbortController(); + const renewal = setInterval(() => { + if (renewalFailure) return; + try { + lease = this.store.renewLease(lease, this.leaseTtlMs); + const current = this.requiredGraph(graphId); + if (current.status !== 'running') executionAbort.abort(`graph is ${current.status}`); + } catch (error) { + renewalFailure = error; + executionAbort.abort(error); + } + }, this.leaseRenewIntervalMs); + renewal.unref(); + try { + let graph = this.reconcileOwned(graphId, lease); + const candidates = this.selectRunnable(graph); + const started: ExecutionNode[] = []; + for (const candidate of candidates) { + graph = this.startNodeOwned(graphId, candidate.id, lease); + started.push(this.requiredNode(graph, candidate.id)); + } + const settled = await Promise.all( + started.map(async (node) => { + try { + return { node, result: await executor(node, graph, { signal: executionAbort.signal }) }; + } catch (error) { + return { + node, + result: { + success: false, + error: errorMessage(error), + failureFingerprint: `executor:${error instanceof Error ? error.name : 'unknown'}`, + }, + }; + } + }) + ); + if (renewalFailure) throw renewalFailure; + const current = this.requiredGraph(graphId); + if (current.status !== 'running') { + return { graph: current, startedNodeIds: started.map((node) => node.id) }; + } + for (const item of settled) { + if (item.result.success) { + graph = this.succeedNodeOwned(graphId, item.node.id, item.result.evidence ?? [], lease); + } else { + graph = this.recordEvidenceOwned( + graphId, + item.node.id, + item.result.evidence ?? [], + lease + ); + graph = this.failNodeOwned( + graphId, + item.node.id, + { + error: item.result.error ?? 'Node execution failed', + ...(item.result.failureFingerprint + ? { failureFingerprint: item.result.failureFingerprint } + : {}), }, - }; + lease + ); } - }) - ); - for (const item of settled) { - graph = item.result.success - ? this.succeedNode(graphId, item.node.id, item.result.evidence) - : this.failNode(graphId, item.node.id, { - error: item.result.error ?? 'Node execution failed', - ...(item.result.failureFingerprint - ? { failureFingerprint: item.result.failureFingerprint } - : {}), - }); + } + graph = this.reconcileOwned(graphId, lease); + return { graph, startedNodeIds: started.map((node) => node.id) }; + } catch (error) { + operationFailure = error; + throw error; + } finally { + clearInterval(renewal); + try { + this.store.releaseLease(graphId, lease); + } catch (error) { + if (!operationFailure) throw error; + } + } + } + + private append( + graphId: string, + lease: ExecutionOwnerLease, + input: Omit[1], 'ownerLease'> + ): ExecutionGraphSnapshot { + return this.store.append(graphId, { ...input, ownerLease: lease }); + } + + private recordEvidenceOwned( + graphId: string, + nodeId: string, + evidence: readonly ExecutionEvidence[], + lease: ExecutionOwnerLease + ): ExecutionGraphSnapshot { + let graph = this.requiredGraph(graphId); + for (const item of evidence) { + graph = this.append(graphId, lease, { + expectedRevision: graph.revision, + type: 'evidence.recorded', + nodeId, + data: { evidence: { ...item, nodeId: item.nodeId ?? nodeId } }, + }); + } + return graph; + } + + private acquireLease(graphId: string): ExecutionOwnerLease { + return this.store.acquireLease(graphId, { ownerId: this.ownerId, ttlMs: this.leaseTtlMs }); + } + + private withLease(graphId: string, operation: (lease: ExecutionOwnerLease) => T): T { + const lease = this.acquireLease(graphId); + let operationFailure: unknown; + try { + return operation(lease); + } catch (error) { + operationFailure = error; + throw error; + } finally { + try { + this.store.releaseLease(graphId, lease); + } catch (error) { + if (!operationFailure) throw error; + } } - graph = this.reconcile(graphId); - return { graph, startedNodeIds: started.map((node) => node.id) }; } private requiredGraph(graphId: string): ExecutionGraphSnapshot { diff --git a/packages/moss-agent/src/orchestration/execution-projector.ts b/packages/moss-agent/src/orchestration/execution-projector.ts index 0e669d88..5f230579 100644 --- a/packages/moss-agent/src/orchestration/execution-projector.ts +++ b/packages/moss-agent/src/orchestration/execution-projector.ts @@ -34,6 +34,19 @@ const NODE_TRANSITIONS: Readonly> 'node.cancelled': ['pending', 'ready', 'leased', 'running', 'blocked', 'interrupted'], }; +const GRAPH_TRANSITIONS: Readonly< + Partial> +> = { + 'graph.ready': ['paused'], + 'graph.resumed': ['paused', 'paused_recovered', 'ready', 'blocked'], + 'graph.paused': ['ready', 'running', 'blocked'], + 'graph.recovered': ['paused', 'ready', 'running', 'blocked'], + 'graph.blocked': ['paused', 'ready', 'running', 'paused_recovered'], + 'graph.completed': ['running', 'blocked'], + 'graph.failed': ['running', 'blocked'], + 'graph.cancelled': ['paused', 'ready', 'running', 'paused_recovered', 'blocked'], +}; + function invalid(message: string): MossError { return new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message }); } @@ -86,20 +99,12 @@ function normalizeNode(definition: ExecutionNodeDefinition): ExecutionNode { }; } -function createdSnapshot(event: ExecutionEvent): ExecutionGraphSnapshot { - const rawNodes = Array.isArray(event.data.nodes) - ? (event.data.nodes as ExecutionNodeDefinition[]) - : []; - const nodes: Record = {}; - for (const definition of rawNodes) { - const node = normalizeNode(definition); - if (nodes[node.id]) throw invalid(`duplicate execution node "${node.id}"`); - nodes[node.id] = node; - } +function assertValidNodeGraph(nodes: Readonly>): void { for (const node of Object.values(nodes)) { for (const dependency of node.dependencies) { - if (!nodes[dependency]) + if (!nodes[dependency]) { throw invalid(`node "${node.id}" depends on unknown node "${dependency}"`); + } if (dependency === node.id) throw invalid(`node "${node.id}" cannot depend on itself`); } } @@ -114,6 +119,19 @@ function createdSnapshot(event: ExecutionEvent): ExecutionGraphSnapshot { visited.add(nodeId); }; for (const nodeId of Object.keys(nodes)) visit(nodeId); +} + +function createdSnapshot(event: ExecutionEvent): ExecutionGraphSnapshot { + const rawNodes = Array.isArray(event.data.nodes) + ? (event.data.nodes as ExecutionNodeDefinition[]) + : []; + const nodes: Record = {}; + for (const definition of rawNodes) { + const node = normalizeNode(definition); + if (nodes[node.id]) throw invalid(`duplicate execution node "${node.id}"`); + nodes[node.id] = node; + } + assertValidNodeGraph(nodes); return { id: event.graphId, ...(typeof event.data.sessionId === 'string' ? { sessionId: event.data.sessionId } : {}), @@ -220,16 +238,23 @@ export function projectExecutionGraph(events: readonly ExecutionEvent[]): Execut throw invalid('execution graph must start with graph.created at sequence 1'); } let snapshot = createdSnapshot(first); + const strictEventValidation = Number(first.data.schemaVersion ?? 1) >= 2; for (const event of events.slice(1)) { if (event.graphId !== snapshot.id || event.seq !== snapshot.revision + 1) { throw invalid(`invalid execution event sequence ${event.seq}`); } + const graphTransition = GRAPH_TRANSITIONS[event.type]; + if (strictEventValidation && graphTransition && !graphTransition.includes(snapshot.status)) { + throw invalid(`graph cannot transition from ${snapshot.status} via ${event.type}`); + } if (event.type === 'node.added') { const raw = event.data.node; if (!raw || typeof raw !== 'object') throw invalid('node.added requires node'); const node = normalizeNode(raw as ExecutionNodeDefinition); if (snapshot.nodes[node.id]) throw invalid(`duplicate execution node "${node.id}"`); - snapshot = { ...snapshot, nodes: { ...snapshot.nodes, [node.id]: node } }; + const nodes = { ...snapshot.nodes, [node.id]: node }; + if (strictEventValidation) assertValidNodeGraph(nodes); + snapshot = { ...snapshot, nodes }; } else if (event.type.startsWith('node.') && event.type !== 'node.progressed') { snapshot = applyNodeEvent(snapshot, event); } else if (event.type === 'evidence.recorded') { @@ -268,7 +293,33 @@ export function projectExecutionGraph(events: readonly ExecutionEvent[]): Execut 'graph.cancelled': 'cancelled', }; const graphStatus = graphStatuses[event.type]; - if (graphStatus) snapshot = { ...snapshot, status: graphStatus }; + if (graphStatus) { + if (strictEventValidation && event.type === 'graph.completed') { + const unfinished = Object.values(snapshot.nodes).filter( + (node) => node.status !== 'succeeded' && node.status !== 'skipped' + ); + if (unfinished.length > 0) { + throw invalid( + `graph cannot complete with unfinished nodes: ${unfinished.map(({ id }) => id).join(', ')}` + ); + } + if (snapshot.policy.strictCompletion && snapshot.verification?.verdict !== 'verified') { + throw invalid('strict graph completion requires a verified evidence verdict'); + } + } + const nodes = + event.type === 'graph.cancelled' + ? Object.fromEntries( + Object.entries(snapshot.nodes).map(([id, node]) => [ + id, + node.status === 'leased' || node.status === 'running' + ? { ...node, status: 'cancelled' as const } + : node, + ]) + ) + : snapshot.nodes; + snapshot = { ...snapshot, status: graphStatus, nodes }; + } } snapshot = { ...snapshot, revision: event.seq, updatedAt: event.time }; } @@ -291,6 +342,7 @@ export function createGraphCreatedEvent( nodes: input.nodes ?? [], policy: { ...DEFAULT_ORCHESTRATION_POLICY, ...input.policy }, budget: input.budget ?? {}, + schemaVersion: 2, }, }; } diff --git a/packages/moss-agent/src/orchestration/execution-recovery.ts b/packages/moss-agent/src/orchestration/execution-recovery.ts index 305cf9a4..fd8ae0d0 100644 --- a/packages/moss-agent/src/orchestration/execution-recovery.ts +++ b/packages/moss-agent/src/orchestration/execution-recovery.ts @@ -1,8 +1,10 @@ import type { ExecutionGraphSnapshot, ExecutionStore } from './execution-types.js'; +import type { ExecutionOwnerLease } from './execution-types.js'; /** Fail-closed restart recovery options. @beta */ export interface RecoverExecutionGraphOptions { readonly now?: number; + readonly ownerLease?: ExecutionOwnerLease; } /** @@ -17,20 +19,23 @@ export function recoverExecutionGraph( ): ExecutionGraphSnapshot { let graph = store.load(graphId); if (!graph) throw new Error(`unknown execution graph "${graphId}"`); - if (graph.status === 'completed' || graph.status === 'failed' || graph.status === 'cancelled') { + if (graph.status !== 'running' && graph.status !== 'ready') { return graph; } const now = options.now ?? Date.now(); const interruptedNodeIds: string[] = []; const blockedMutationNodeIds: string[] = []; for (const node of Object.values(graph.nodes)) { - if (node.status !== 'ready' && node.status !== 'leased' && node.status !== 'running') continue; + // Ready work has not started and is safe to keep queued across a restart. Only + // nodes which may have been executing need interruption classification. + if (node.status !== 'leased' && node.status !== 'running') continue; graph = store.append(graphId, { expectedRevision: graph.revision, type: 'node.interrupted', nodeId: node.id, time: now, data: { reason: 'host_restarted' }, + ownerLease: options.ownerLease, }); interruptedNodeIds.push(node.id); if (node.kind !== 'analysis' && node.kind !== 'verification') { @@ -40,6 +45,7 @@ export function recoverExecutionGraph( nodeId: node.id, time: now, data: { error: 'Interrupted mutation requires explicit inspection before retry.' }, + ownerLease: options.ownerLease, }); blockedMutationNodeIds.push(node.id); } @@ -56,5 +62,6 @@ export function recoverExecutionGraph( blockedMutationNodeIds, }, }, + ownerLease: options.ownerLease, }); } diff --git a/packages/moss-agent/src/orchestration/execution-types.ts b/packages/moss-agent/src/orchestration/execution-types.ts index 1d232eab..3cc47d80 100644 --- a/packages/moss-agent/src/orchestration/execution-types.ts +++ b/packages/moss-agent/src/orchestration/execution-types.ts @@ -194,6 +194,8 @@ export interface AppendExecutionEventInput { readonly time?: number; readonly nodeId?: string; readonly data?: Readonly>; + /** Current graph-owner lease required while a live owner holds the graph. */ + readonly ownerLease?: ExecutionOwnerLease; } /** Renewable local ownership lease for one graph. @beta */ @@ -218,11 +220,19 @@ export interface ExecutionStore { list(): readonly ExecutionGraphSnapshot[]; events(graphId: string, after?: number): readonly ExecutionEvent[]; append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot; + /** Bind the package-authorized arbiter and return its terminal append capability. */ + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender; acquireLease(graphId: string, input: AcquireExecutionLeaseInput): ExecutionOwnerLease; renewLease(lease: ExecutionOwnerLease, ttlMs?: number): ExecutionOwnerLease; releaseLease(graphId: string, lease: ExecutionOwnerLease): void; } +/** Instance-bound terminal append capability held by CompletionArbiter. @beta */ +export type ExecutionCompletionAppender = ( + graphId: string, + input: AppendExecutionEventInput +) => ExecutionGraphSnapshot; + /** Result returned by one scheduler-managed node execution. @beta */ export interface ExecutionNodeRunResult { readonly success: boolean; @@ -231,10 +241,17 @@ export interface ExecutionNodeRunResult { readonly failureFingerprint?: string; } -/** Executor callback used by {@link ExecutionGraphScheduler}. @beta */ +/** Cancellation and ownership context for one scheduler-managed node execution. @beta */ +export interface ExecutionNodeExecutionContext { + /** Aborted when the graph is stopped/paused or its owner lease can no longer be renewed. */ + readonly signal: AbortSignal; +} + +/** Executor callback used by the execution graph scheduler. @beta */ export type ExecutionNodeExecutor = ( node: ExecutionNode, - graph: ExecutionGraphSnapshot + graph: ExecutionGraphSnapshot, + context: ExecutionNodeExecutionContext ) => Promise; /** Outcome of one scheduler admission cycle. @beta */ diff --git a/packages/moss-agent/src/orchestration/in-memory-execution-store.ts b/packages/moss-agent/src/orchestration/in-memory-execution-store.ts index 1aba23ec..dd15f5fa 100644 --- a/packages/moss-agent/src/orchestration/in-memory-execution-store.ts +++ b/packages/moss-agent/src/orchestration/in-memory-execution-store.ts @@ -1,12 +1,14 @@ import { randomUUID } from 'node:crypto'; import { ErrorCode, MossError } from '../errors.js'; +import { executionCompletionAuthority } from './completion-authority-internal.js'; import { createGraphCreatedEvent, projectExecutionGraph } from './execution-projector.js'; import type { AcquireExecutionLeaseInput, AppendExecutionEventInput, CreateExecutionGraphInput, ExecutionEvent, + ExecutionCompletionAppender, ExecutionGraphSnapshot, ExecutionOwnerLease, ExecutionStore, @@ -22,6 +24,7 @@ export class InMemoryExecutionStore implements ExecutionStore { private readonly graphs = new Map(); private readonly eventGraphIds = new Map(); private readonly leases = new Map(); + private readonly completionAuthorities = new WeakSet(); private readonly now: () => number; constructor(options: InMemoryExecutionStoreOptions = {}) { @@ -55,8 +58,27 @@ export class InMemoryExecutionStore implements ExecutionStore { } append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot { + this.assertPublicAppend(input); + return this.appendEvent(graphId, input); + } + + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender { + if (authority !== executionCompletionAuthority) { + throw this.invalid('execution completion authority is invalid'); + } + this.completionAuthorities.add(owner); + return (graphId, input) => { + if (!this.completionAuthorities.has(owner)) { + throw this.invalid('completion authority is no longer bound to this store'); + } + return this.appendEvent(graphId, input); + }; + } + + private appendEvent(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot { const events = this.graphs.get(graphId); if (!events) throw this.invalid(`unknown execution graph "${graphId}"`); + this.assertAppendLease(graphId, input); const id = input.id ?? `exe_${randomUUID()}`; const existingGraphId = this.eventGraphIds.get(id); if (existingGraphId === graphId) return projectExecutionGraph(events); @@ -87,8 +109,11 @@ export class InMemoryExecutionStore implements ExecutionStore { if (!this.graphs.has(graphId)) throw this.invalid(`unknown execution graph "${graphId}"`); const now = this.now(); const current = this.leases.get(graphId); - if (current && current.expiresAt > now && current.ownerId !== input.ownerId) { - throw this.leaseHeld(graphId, current.ownerId); + if (current && current.expiresAt > now) { + if (current.ownerId !== input.ownerId) throw this.leaseHeld(graphId, current.ownerId); + const renewed = { ...current, expiresAt: now + (input.ttlMs ?? 30_000) }; + this.leases.set(graphId, renewed); + return renewed; } const lease: ExecutionOwnerLease = { graphId, @@ -103,7 +128,7 @@ export class InMemoryExecutionStore implements ExecutionStore { renewLease(lease: ExecutionOwnerLease, ttlMs = 30_000): ExecutionOwnerLease { const current = this.leases.get(lease.graphId); - if (!current || current.token !== lease.token) + if (!current || current.token !== lease.token || current.expiresAt <= this.now()) throw this.invalid('execution lease token is stale'); const renewed = { ...current, expiresAt: this.now() + ttlMs }; this.leases.set(lease.graphId, renewed); @@ -121,6 +146,30 @@ export class InMemoryExecutionStore implements ExecutionStore { return new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message }); } + private assertPublicAppend(input: AppendExecutionEventInput): void { + if (input.type === 'verification.recorded' || input.type === 'graph.completed') { + throw this.invalid(`${input.type} may only be appended by CompletionArbiter`); + } + } + + private assertAppendLease(graphId: string, input: AppendExecutionEventInput): void { + const current = this.leases.get(graphId); + const active = current && current.expiresAt > this.now() ? current : undefined; + if ( + active && + !input.ownerLease && + ['graph.cancelled', 'graph.paused', 'steering.recorded'].includes(input.type) + ) { + return; + } + if (active && input.ownerLease?.token !== active.token) { + throw this.leaseHeld(graphId, active.ownerId); + } + if (input.ownerLease && (!active || input.ownerLease.token !== active.token)) { + throw this.leaseHeld(graphId, active?.ownerId ?? 'another owner'); + } + } + private leaseHeld(graphId: string, ownerId: string): MossError { return new MossError({ code: ErrorCode.EXECUTION_LEASE_HELD, diff --git a/packages/moss-agent/src/orchestration/index.ts b/packages/moss-agent/src/orchestration/index.ts index d9c3eff1..bc139fb7 100644 --- a/packages/moss-agent/src/orchestration/index.ts +++ b/packages/moss-agent/src/orchestration/index.ts @@ -16,6 +16,7 @@ export { normalizeWritePath, writePathsOverlap, } from './execution-graph-scheduler.js'; +export type { ExecutionGraphSchedulerOptions } from './execution-graph-scheduler.js'; export { GitWorktreeWorkspaceLeaseAdapter } from './git-worktree-workspace-lease.js'; export { CopyWorkspaceLeaseAdapter } from './copy-workspace-lease.js'; export { AdaptiveWorkspaceLeaseAdapter } from './adaptive-workspace-lease.js'; @@ -36,6 +37,7 @@ export type { AppendExecutionEventInput, CreateExecutionGraphInput, ExecutionBudget, + ExecutionCompletionAppender, ExecutionEvent, ExecutionEventType, ExecutionEvidence, @@ -46,6 +48,7 @@ export type { ExecutionNodeDefinition, ExecutionNodeKind, ExecutionNodeExecutor, + ExecutionNodeExecutionContext, ExecutionNodeRunResult, ExecutionNodeStatus, ExecutionOwnerLease, @@ -75,6 +78,8 @@ export type { AgentRoleDefinition, AgentRoleKind, AgentSynthesisResult, + AgentVerificationReceipt, AssignmentSpec, RoutedAssignment, + RoutedAgentExecutor, } from './agent-role-types.js'; diff --git a/packages/moss-agent/src/orchestration/jsonl-execution-store.ts b/packages/moss-agent/src/orchestration/jsonl-execution-store.ts index 6c5115ac..932a4e1e 100644 --- a/packages/moss-agent/src/orchestration/jsonl-execution-store.ts +++ b/packages/moss-agent/src/orchestration/jsonl-execution-store.ts @@ -5,12 +5,14 @@ import path from 'node:path'; import lockfile from 'proper-lockfile'; import { ErrorCode, MossError, wrapAsMoss } from '../errors.js'; +import { executionCompletionAuthority } from './completion-authority-internal.js'; import { createGraphCreatedEvent, projectExecutionGraph } from './execution-projector.js'; import type { AcquireExecutionLeaseInput, AppendExecutionEventInput, CreateExecutionGraphInput, ExecutionEvent, + ExecutionCompletionAppender, ExecutionGraphSnapshot, ExecutionOwnerLease, ExecutionStore, @@ -33,6 +35,7 @@ export class JsonlExecutionStore implements ExecutionStore { private readonly rootDir: string; private readonly snapshotEvery: number; private readonly now: () => number; + private readonly completionAuthorities = new WeakSet(); constructor(options: JsonlExecutionStoreOptions) { this.rootDir = path.resolve(options.rootDir); @@ -90,10 +93,29 @@ export class JsonlExecutionStore implements ExecutionStore { } append(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot { + this.assertPublicAppend(input); + return this.appendEvent(graphId, input); + } + + bindCompletionAuthority(authority: object, owner: object): ExecutionCompletionAppender { + if (authority !== executionCompletionAuthority) { + throw this.invalid('execution completion authority is invalid'); + } + this.completionAuthorities.add(owner); + return (graphId, input) => { + if (!this.completionAuthorities.has(owner)) { + throw this.invalid('completion authority is no longer bound to this store'); + } + return this.appendEvent(graphId, input); + }; + } + + private appendEvent(graphId: string, input: AppendExecutionEventInput): ExecutionGraphSnapshot { const eventsFile = this.eventsFile(graphId); if (!fs.existsSync(eventsFile)) throw this.invalid(`unknown execution graph "${graphId}"`); return this.withFileLock(eventsFile, () => { const events = this.readEvents(graphId, true); + this.assertAppendLease(graphId, input); const id = input.id ?? `exe_${randomUUID()}`; const duplicate = events.find((event) => event.id === id); if (duplicate) return projectExecutionGraph(events); @@ -126,12 +148,11 @@ export class JsonlExecutionStore implements ExecutionStore { return this.withFileLock(eventsFile, () => { const now = this.now(); const current = this.readLease(graphId); - if (current && current.expiresAt > now && current.ownerId !== input.ownerId) { - throw new MossError({ - code: ErrorCode.EXECUTION_LEASE_HELD, - message: `execution graph "${graphId}" is leased by "${current.ownerId}"`, - recoverable: true, - }); + if (current && current.expiresAt > now) { + if (current.ownerId !== input.ownerId) throw this.leaseHeld(graphId, current.ownerId); + const renewed = { ...current, expiresAt: now + (input.ttlMs ?? 30_000) }; + this.writeJsonAtomic(this.leaseFile(graphId), renewed); + return renewed; } const lease: ExecutionOwnerLease = { graphId, @@ -149,7 +170,7 @@ export class JsonlExecutionStore implements ExecutionStore { const eventsFile = this.eventsFile(lease.graphId); return this.withFileLock(eventsFile, () => { const current = this.readLease(lease.graphId); - if (!current || current.token !== lease.token) + if (!current || current.token !== lease.token || current.expiresAt <= this.now()) throw this.invalid('execution lease token is stale'); const renewed = { ...current, expiresAt: this.now() + ttlMs }; this.writeJsonAtomic(this.leaseFile(lease.graphId), renewed); @@ -301,6 +322,38 @@ export class JsonlExecutionStore implements ExecutionStore { return new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message }); } + private assertPublicAppend(input: AppendExecutionEventInput): void { + if (input.type === 'verification.recorded' || input.type === 'graph.completed') { + throw this.invalid(`${input.type} may only be appended by CompletionArbiter`); + } + } + + private assertAppendLease(graphId: string, input: AppendExecutionEventInput): void { + const current = this.readLease(graphId); + const active = current && current.expiresAt > this.now() ? current : undefined; + if ( + active && + !input.ownerLease && + ['graph.cancelled', 'graph.paused', 'steering.recorded'].includes(input.type) + ) { + return; + } + if (active && input.ownerLease?.token !== active.token) { + throw this.leaseHeld(graphId, active.ownerId); + } + if (input.ownerLease && (!active || input.ownerLease.token !== active.token)) { + throw this.leaseHeld(graphId, active?.ownerId ?? 'another owner'); + } + } + + private leaseHeld(graphId: string, ownerId: string): MossError { + return new MossError({ + code: ErrorCode.EXECUTION_LEASE_HELD, + message: `execution graph "${graphId}" is leased by "${ownerId}"`, + recoverable: true, + }); + } + private storeFailure(message: string, cause: unknown): MossError { return wrapAsMoss(cause, ErrorCode.EXECUTION_STORE_FAILED, { message, recoverable: true }); } diff --git a/packages/moss-agent/src/orchestration/legacy-execution-importer.ts b/packages/moss-agent/src/orchestration/legacy-execution-importer.ts index c5422c81..4087d41c 100644 --- a/packages/moss-agent/src/orchestration/legacy-execution-importer.ts +++ b/packages/moss-agent/src/orchestration/legacy-execution-importer.ts @@ -89,11 +89,25 @@ export class LegacyExecutionImporter { const id = stableId(request.kind, request.state); const marker = `${request.sourcePath}.execution-graph-migration-v1.json`; if (fs.existsSync(marker)) { - const graph = this.store.load(id); + let markerGraphId: string; + try { + const persisted = JSON.parse(fs.readFileSync(marker, 'utf8')) as Record; + if (persisted.version !== 1 || typeof persisted.graphId !== 'string') { + throw new Error('unsupported or incomplete migration marker'); + } + markerGraphId = persisted.graphId; + } catch (cause) { + throw new MossError({ + code: ErrorCode.EXECUTION_STORE_FAILED, + message: `invalid execution migration marker: ${marker}`, + cause, + }); + } + const graph = this.store.load(markerGraphId); if (!graph) { throw new MossError({ code: ErrorCode.EXECUTION_STORE_FAILED, - message: `migration marker exists but execution graph is missing: ${id}`, + message: `migration marker exists but execution graph is missing: ${markerGraphId}`, }); } return graph; diff --git a/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts b/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts index 1dd6a8c2..ec7bd8ef 100644 --- a/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts +++ b/packages/moss-agent/src/orchestration/workspace-lease-adapter.ts @@ -109,7 +109,7 @@ export abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter const artifactRef = path.join(artifactDirectory, `${patchId}.patch`); fs.mkdirSync(artifactDirectory, { recursive: true, mode: 0o700 }); fs.writeFileSync(artifactRef, diff.stdout, { mode: 0o600 }); - return { + const patch: WorkspacePatch = { id: patchId, leaseId: loaded.id, patch: diff.stdout, @@ -118,38 +118,50 @@ export abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter changedPaths, createdAt: this.now(), }; + this.writeJsonAtomic(path.join(artifactDirectory, `${patchId}.json`), patch); + return patch; } async merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise { this.assertCompatible(lease); const loaded = this.requiredLease(lease.id); - if (patch.leaseId !== loaded.id) { + const trusted = this.requiredPatch(loaded, patch); + if (trusted.leaseId !== loaded.id) { throw new MossError({ code: ErrorCode.EXECUTION_STATE_INVALID, message: `patch "${patch.id}" belongs to another workspace lease`, }); } - const digest = `sha256:${createHash('sha256').update(patch.patch).digest('hex')}`; - if (digest !== patch.digest) { + const excluded = trusted.changedPaths.filter(isExcludedWorkspacePath); + const outside = trusted.changedPaths.filter( + (changed) => !loaded.writePaths.some((declared) => writePathsOverlap([changed], [declared])) + ); + if (excluded.length > 0 || outside.length > 0) { throw new MossError({ - code: ErrorCode.EXECUTION_STATE_INVALID, - message: `patch "${patch.id}" digest does not match its contents`, + code: ErrorCode.TOOL_NOT_ALLOWED, + message: `stored patch violates workspace write policy: ${[...excluded, ...outside].join(', ')}`, }); } - const conflictingPaths = patch.changedPaths.filter((relative) => { + const conflictingPaths = trusted.changedPaths.filter((relative) => { const current = hashFile(path.join(loaded.parentWorkspace, ...relative.split('/'))); return current !== (loaded.baselineHashes[relative] ?? null); }); if (conflictingPaths.length > 0) { - return { status: 'merge_conflict', patchId: patch.id, conflictingPaths }; + return { + status: 'merge_conflict', + patchId: patch.id, + conflictingPaths, + digest: trusted.digest, + changedPaths: trusted.changedPaths, + }; } if (this.authorizeMerge) { try { await this.authorizeMerge({ lease: loaded, - patchId: patch.id, - digest: patch.digest, - changedPaths: patch.changedPaths, + patchId: trusted.id, + digest: trusted.digest, + changedPaths: trusted.changedPaths, }); } catch (error) { throw wrapAsMoss(error, ErrorCode.TOOL_NOT_ALLOWED, { @@ -158,16 +170,27 @@ export abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter }); } } - if (patch.patch) { + if (trusted.patch) { await this.git( loaded.parentWorkspace, ['apply', '--check', '--whitespace=nowarn', '-'], - patch.patch + trusted.patch ); - await this.git(loaded.parentWorkspace, ['apply', '--whitespace=nowarn', '-'], patch.patch); + await this.git(loaded.parentWorkspace, ['apply', '--whitespace=nowarn', '-'], trusted.patch); } await this.release(loaded.id, 'merged'); - return { status: 'merged', patchId: patch.id, conflictingPaths: [] }; + return { + status: 'merged', + patchId: patch.id, + conflictingPaths: [], + digest: trusted.digest, + changedPaths: trusted.changedPaths, + }; + } + + async mergeStored(leaseId: string, patchId: string): Promise { + const lease = this.requiredLease(leaseId); + return this.merge(lease, this.loadStoredPatch(lease, patchId)); } async release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise { @@ -260,6 +283,55 @@ export abstract class BaseWorkspaceLeaseAdapter implements WorkspaceLeaseAdapter return path.join(assertLeasePath(this.rootDir, leaseId), 'lease.json'); } + private requiredPatch(lease: WorkspaceLease, supplied: WorkspacePatch): WorkspacePatch { + const persisted = this.loadStoredPatch(lease, supplied.id); + const exactMetadata = + supplied.leaseId === persisted.leaseId && + supplied.artifactRef === persisted.artifactRef && + supplied.digest === persisted.digest && + supplied.patch === persisted.patch && + JSON.stringify(supplied.changedPaths) === JSON.stringify(persisted.changedPaths); + if (!exactMetadata) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `patch "${supplied.id}" does not match its stored artifact`, + }); + } + return persisted; + } + + private loadStoredPatch(lease: WorkspaceLease, patchId: string): WorkspacePatch { + const artifactDirectory = path.join(this.leaseDirectory(lease.id), 'artifacts'); + const expectedArtifact = path.join(artifactDirectory, `${patchId}.patch`); + const metadataFile = path.join(artifactDirectory, `${patchId}.json`); + let persisted: WorkspacePatch; + let patchBody: string; + try { + persisted = JSON.parse(fs.readFileSync(metadataFile, 'utf8')) as WorkspacePatch; + patchBody = fs.readFileSync(expectedArtifact, 'utf8'); + } catch (error) { + throw this.failure(`Failed to load stored workspace patch "${patchId}"`, error); + } + const digest = `sha256:${createHash('sha256').update(patchBody).digest('hex')}`; + const exactMetadata = + persisted.id === patchId && + persisted.leaseId === lease.id && + persisted.artifactRef === expectedArtifact; + if (!exactMetadata || digest !== persisted.digest) { + throw new MossError({ + code: ErrorCode.EXECUTION_STATE_INVALID, + message: `patch "${patchId}" does not match its stored artifact`, + }); + } + return { ...persisted, patch: patchBody }; + } + + private writeJsonAtomic(file: string, value: unknown): void { + const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`; + fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 }); + fs.renameSync(temporary, file); + } + private requiredLease(leaseId: string): WorkspaceLease { const lease = this.load(leaseId); if (!lease) { diff --git a/packages/moss-agent/src/orchestration/workspace-lease-types.ts b/packages/moss-agent/src/orchestration/workspace-lease-types.ts index c7acbba2..67e22660 100644 --- a/packages/moss-agent/src/orchestration/workspace-lease-types.ts +++ b/packages/moss-agent/src/orchestration/workspace-lease-types.ts @@ -46,6 +46,8 @@ export interface WorkspaceMergeResult { readonly status: 'merged' | 'merge_conflict'; readonly patchId: string; readonly conflictingPaths: readonly string[]; + readonly digest?: string; + readonly changedPaths?: readonly string[]; } /** Explicit terminal reasons that permit lease cleanup. @beta */ @@ -58,6 +60,7 @@ export interface WorkspaceLeaseAdapter { list(): readonly WorkspaceLease[]; createPatch(lease: WorkspaceLease): Promise; merge(lease: WorkspaceLease, patch: WorkspacePatch): Promise; + mergeStored(leaseId: string, patchId: string): Promise; release(leaseId: string, reason: WorkspaceLeaseReleaseReason): Promise; } diff --git a/packages/moss-agent/src/plan-execute/plan-execution-graph.ts b/packages/moss-agent/src/plan-execute/plan-execution-graph.ts index 668da15f..9d622614 100644 --- a/packages/moss-agent/src/plan-execute/plan-execution-graph.ts +++ b/packages/moss-agent/src/plan-execute/plan-execution-graph.ts @@ -42,6 +42,10 @@ export function createExecutionGraphForPlan( /** Mirror legacy Plan mutations into graph events while compatibility is supported. @internal */ export function syncExecutionGraphFromPlan(store: ExecutionStore, plan: Plan): void { let graph = store.load(plan.id); + if (!graph) { + createExecutionGraphForPlan(store, plan); + graph = store.load(plan.id); + } if (!graph) return; if (plan.status === 'executing' && graph.status !== 'running') { graph = store.append(plan.id, { @@ -112,8 +116,12 @@ export function syncExecutionGraphFromPlan(store: ExecutionStore, plan: Plan): v }); } } - if (plan.status === 'completed' && graph.status !== 'completed') { - store.append(plan.id, { expectedRevision: graph.revision, type: 'graph.completed' }); + if (plan.status === 'completed' && !['completed', 'blocked'].includes(graph.status)) { + store.append(plan.id, { + expectedRevision: graph.revision, + type: 'graph.blocked', + data: { reason: 'needs_evidence', source: 'legacy_plan_projection' }, + }); } else if (plan.status === 'failed' && graph.status !== 'failed') { store.append(plan.id, { expectedRevision: graph.revision, type: 'graph.failed' }); } else if (plan.status === 'cancelled' && graph.status !== 'cancelled') { diff --git a/packages/moss-agent/src/tools/builtin.ts b/packages/moss-agent/src/tools/builtin.ts index 68d28bc7..a0f65f38 100644 --- a/packages/moss-agent/src/tools/builtin.ts +++ b/packages/moss-agent/src/tools/builtin.ts @@ -11,6 +11,7 @@ import { subagentStatusTool, subagentStopTool, } from './create-subagent.js'; +import { mergeSubagentPatchTool } from './merge-subagent-patch.js'; import { createWebFetchTool } from './web-fetch.js'; import { createWebSearchTool } from './web-search.js'; import { createBrowserTools } from './browser-tools.js'; @@ -497,6 +498,7 @@ export const builtinTools: Tool[] = [ applyPatchTool, codeDiagnosticsTool, createSubagentTool, + mergeSubagentPatchTool, fanOutSubagentsTool, subagentStatusTool, subagentStopTool, diff --git a/packages/moss-agent/src/tools/create-subagent.ts b/packages/moss-agent/src/tools/create-subagent.ts index 86bb59da..61b41aa1 100644 --- a/packages/moss-agent/src/tools/create-subagent.ts +++ b/packages/moss-agent/src/tools/create-subagent.ts @@ -266,6 +266,7 @@ export const createSubagentTool: Tool = { ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}), ...(result.error ? { error: result.error } : {}), ...(result.workspaceLeaseId ? { workspaceLeaseId: result.workspaceLeaseId } : {}), + ...(result.patchId ? { patchId: result.patchId } : {}), ...(result.patchRef ? { patchRef: result.patchRef } : {}), ...(result.patchDigest ? { patchDigest: result.patchDigest } : {}), ...(result.changedPaths ? { changedPaths: result.changedPaths } : {}), @@ -307,6 +308,7 @@ export const createSubagentTool: Tool = { `toolCalls: ${result.toolResults ?? 0}`, `elapsed: ${result.durationMs ?? 0} ms`, ...(result.patchRef ? [`patch: ${result.patchRef}`] : []), + ...(result.patchId ? [`patchId: ${result.patchId}`] : []), ...(result.patchDigest ? [`digest: ${result.patchDigest}`] : []), ...(result.workspaceLeaseId ? [`lease: ${result.workspaceLeaseId}`] : []), ].join(' | '); diff --git a/packages/moss-agent/src/tools/merge-subagent-patch.ts b/packages/moss-agent/src/tools/merge-subagent-patch.ts new file mode 100644 index 00000000..a966e4e3 --- /dev/null +++ b/packages/moss-agent/src/tools/merge-subagent-patch.ts @@ -0,0 +1,34 @@ +import type { Tool } from '../core/tools/tool-types.js'; + +interface MergeSubagentPatchInput { + leaseId: string; + patchId: string; +} + +/** Approval-gated parent merge for an adapter-owned isolated-worker artifact. @beta */ +export const mergeSubagentPatchTool: Tool = { + name: 'merge_subagent_patch', + description: + 'Merge a completed full-scope sub-agent patch into the parent workspace. Use the lease and patchId returned by create_subagent. The stored artifact, declared write paths, parent baseline, and host approval are revalidated before mutation.', + metadata: { + sideEffectClass: 'local_write', + planMode: 'allow', + requiresApproval: true, + }, + inputSchema: { + type: 'object', + properties: { + leaseId: { type: 'string', description: 'Workspace lease returned by the sub-agent.' }, + patchId: { type: 'string', description: 'Stored patch ID returned by the sub-agent.' }, + }, + required: ['leaseId', 'patchId'], + }, + async execute(input, ctx) { + if (!ctx.mergeWorkspacePatch) return 'Error: workspace patch merging is unavailable.'; + const result = await ctx.mergeWorkspacePatch(input.leaseId, input.patchId); + if (result.status === 'merge_conflict') { + return `Error: sub-agent patch has parent conflicts: ${result.conflictingPaths.join(', ')}`; + } + return `Merged sub-agent patch ${input.patchId} from lease ${input.leaseId}.`; + }, +}; diff --git a/packages/moss-agent/src/web-ui/client/details-panel.tsx b/packages/moss-agent/src/web-ui/client/details-panel.tsx index 0996c534..19187b20 100644 --- a/packages/moss-agent/src/web-ui/client/details-panel.tsx +++ b/packages/moss-agent/src/web-ui/client/details-panel.tsx @@ -230,6 +230,28 @@ export const DetailsPanel = ({ Stop ) : null} + {Object.values(task.nodes) + .filter((node) => + ['failed', 'interrupted', 'blocked', 'merge_conflict'].includes( + node.status + ) + ) + .map((node) => ( + + ))} {JSON.stringify(task, null, 2)} )) diff --git a/packages/moss-agent/test/completion-arbiter.spec.mjs b/packages/moss-agent/test/completion-arbiter.spec.mjs index 63eb1484..c059d05e 100644 --- a/packages/moss-agent/test/completion-arbiter.spec.mjs +++ b/packages/moss-agent/test/completion-arbiter.spec.mjs @@ -10,7 +10,7 @@ import { function createCodingGraph() { const store = new InMemoryExecutionStore(); - store.create({ + const graph = store.create({ id: 'coding', goal: 'Change code safely', nodes: [ @@ -33,6 +33,7 @@ function createCodingGraph() { policy: { strictCompletion: true }, now: 1, }); + store.append('coding', { expectedRevision: graph.revision, type: 'graph.resumed' }); return store; } @@ -41,6 +42,80 @@ function append(store, graphId, input) { return store.append(graphId, { expectedRevision: graph.revision, ...input }); } +test('public store append cannot forge verification or completion authority', () => { + const store = createCodingGraph(); + const graph = store.load('coding'); + assert.throws(() => store.bindCompletionAuthority({}, {}), /completion authority is invalid/); + assert.throws( + () => + store.append(graph.id, { + expectedRevision: graph.revision, + type: 'verification.recorded', + data: { + verification: { + verdict: 'verified', + evidenceIds: [], + reasons: [], + verifiedAt: Date.now(), + }, + }, + }), + /CompletionArbiter/ + ); + assert.throws( + () => + store.append(graph.id, { + expectedRevision: graph.revision, + type: 'graph.completed', + }), + /CompletionArbiter/ + ); +}); + +test('completion authority is instance-owned and survives a cloning store wrapper', async () => { + class CloningStore { + constructor(delegate) { + this.delegate = delegate; + } + create(input) { + return this.delegate.create(input); + } + load(id) { + return this.delegate.load(id); + } + list() { + return this.delegate.list(); + } + events(id, after) { + return this.delegate.events(id, after); + } + append(id, input) { + return this.delegate.append(id, { ...input, data: { ...input.data } }); + } + bindCompletionAuthority(authority, owner) { + const append = this.delegate.bindCompletionAuthority(authority, owner); + return (id, input) => append(id, { ...input, data: { ...input.data } }); + } + acquireLease(id, input) { + return this.delegate.acquireLease(id, input); + } + renewLease(lease, ttlMs) { + return this.delegate.renewLease(lease, ttlMs); + } + releaseLease(id, lease) { + this.delegate.releaseLease(id, lease); + } + } + for (const id of ['first', 'second']) { + const wrapped = new CloningStore(new InMemoryExecutionStore()); + let graph = wrapped.create({ id, goal: id, nodes: [] }); + wrapped.append(id, { expectedRevision: graph.revision, type: 'graph.resumed' }); + const decision = await new CompletionArbiter(wrapped).decide(id, { taskKind: 'analysis' }); + assert.equal(decision.verdict, 'verified'); + assert.equal(wrapped.load(id).status, 'completed'); + } +}); + test('model success text cannot override missing deterministic coding evidence', async () => { const store = createCodingGraph(); const scheduler = new ExecutionGraphScheduler(store); @@ -78,7 +153,7 @@ test('coding completion requires merged patch followed by fresh green verificati kind: 'patch', summary: 'Patch merged', createdAt: 10, - metadata: { merged: true, criterion: 'patch-merged' }, + metadata: { merged: true, criterion: 'patch-merged', runId: 'implement-run' }, }, ]); scheduler.reconcile('coding'); @@ -89,7 +164,14 @@ test('coding completion requires merged patch followed by fresh green verificati kind: 'verification', summary: 'Tests passed after merge', createdAt: 20, - metadata: { fresh: true, exitCode: 0, criterion: 'tests-green' }, + metadata: { + fresh: true, + exitCode: 0, + criterion: 'tests-green', + roleKind: 'verifier', + independent: true, + runId: 'verify-run', + }, }, ]); @@ -114,7 +196,7 @@ test('verification older than the merged patch is not fresh evidence', async () kind: 'patch', summary: 'Patch merged later', createdAt: 30, - metadata: { merged: true, criterion: 'patch-merged' }, + metadata: { merged: true, criterion: 'patch-merged', runId: 'implement-run' }, }, ], [ @@ -124,7 +206,14 @@ test('verification older than the merged patch is not fresh evidence', async () kind: 'verification', summary: 'Old green run', createdAt: 20, - metadata: { fresh: true, exitCode: 0, criterion: 'tests-green' }, + metadata: { + fresh: true, + exitCode: 0, + criterion: 'tests-green', + roleKind: 'verifier', + independent: true, + runId: 'verify-run', + }, }, ], ]) { @@ -150,7 +239,7 @@ test('active background work and workspace leases keep a graph in progress', asy activeWorkspaceLeaseIds: ['lease-1'], }); assert.equal(decision.status, 'in_progress'); - assert.equal(store.load('coding').status, 'paused'); + assert.equal(store.load('coding').status, 'running'); assert.match(decision.reasons.join('\n'), /background-1/); assert.match(decision.reasons.join('\n'), /lease-1/); }); @@ -161,11 +250,12 @@ test('research and device tasks require citations and real probe receipts', asyn ['device', 'device', 'probe_receipt'], ]) { const store = new InMemoryExecutionStore(); - store.create({ + const graph = store.create({ id: graphId, goal: `Complete ${taskKind}`, nodes: [{ id: 'work', kind: 'analysis', title: 'Work', dependencies: [] }], }); + store.append(graphId, { expectedRevision: graph.revision, type: 'graph.resumed' }); const scheduler = new ExecutionGraphScheduler(store); scheduler.reconcile(graphId); scheduler.startNode(graphId, 'work'); diff --git a/packages/moss-agent/test/execution-graph-scheduler.spec.mjs b/packages/moss-agent/test/execution-graph-scheduler.spec.mjs index 60d86ced..bc82ac2c 100644 --- a/packages/moss-agent/test/execution-graph-scheduler.spec.mjs +++ b/packages/moss-agent/test/execution-graph-scheduler.spec.mjs @@ -10,7 +10,7 @@ import { function createStore(nodes, options = {}) { const store = new InMemoryExecutionStore(); - store.create({ + const graph = store.create({ id: 'graph', goal: 'Run a real dependency graph', nodes, @@ -22,6 +22,7 @@ function createStore(nodes, options = {}) { budget: options.budget, now: 1, }); + store.append('graph', { expectedRevision: graph.revision, type: 'graph.resumed' }); return store; } @@ -66,6 +67,136 @@ test('scheduler runs independent nodes concurrently and preserves successful sib assert.deepEqual(result.graph.nodes.a.evidenceIds, ['evidence-a']); }); +test('scheduler holds and renews one fenced owner lease for the complete execution cycle', async () => { + let clock = 0; + const store = new InMemoryExecutionStore({ now: () => clock }); + let graph = store.create({ + id: 'graph', + goal: 'renew deterministically', + nodes: [{ id: 'work', kind: 'analysis', title: 'Work', dependencies: [] }], + }); + store.append('graph', { expectedRevision: graph.revision, type: 'graph.resumed' }); + const first = new ExecutionGraphScheduler(store, { + ownerId: 'scheduler-1', + leaseTtlMs: 30, + leaseRenewIntervalMs: 5, + }); + const second = new ExecutionGraphScheduler(store, { ownerId: 'scheduler-2' }); + let releaseWork; + let markStarted; + const started = new Promise((resolve) => { + markStarted = resolve; + }); + const firstRun = first.runAvailable('graph', async () => { + markStarted(); + await new Promise((resolve) => { + releaseWork = resolve; + }); + return { success: true }; + }); + await started; + clock = 20; + await new Promise((resolve) => setTimeout(resolve, 12)); + clock = 35; + await assert.rejects( + () => second.runAvailable('graph', async () => ({ success: true })), + (error) => error?.code === 'EXECUTION_LEASE_HELD' + ); + releaseWork(); + const result = await firstRun; + assert.equal(result.graph.nodes.work.status, 'succeeded'); +}); + +test('a user stop aborts in-flight executor context and cannot be overwritten by completion', async () => { + const store = createStore([{ id: 'work', kind: 'analysis', title: 'Work', dependencies: [] }]); + const scheduler = new ExecutionGraphScheduler(store, { + ownerId: 'scheduler', + leaseTtlMs: 30, + leaseRenewIntervalMs: 5, + }); + let observedAbort = false; + let markStarted; + const started = new Promise((resolve) => { + markStarted = resolve; + }); + const running = scheduler.runAvailable('graph', async (_node, _graph, context) => { + markStarted(); + await new Promise((resolve) => { + context.signal.addEventListener( + 'abort', + () => { + observedAbort = true; + resolve(); + }, + { once: true } + ); + }); + return { success: true }; + }); + await started; + const current = store.load('graph'); + store.append('graph', { + expectedRevision: current.revision, + type: 'graph.cancelled', + data: { requestedBy: 'user' }, + }); + const result = await running; + assert.equal(observedAbort, true); + assert.equal(result.graph.status, 'cancelled'); + assert.equal(result.graph.nodes.work.status, 'cancelled'); + assert.equal( + store.events('graph').some((event) => event.type === 'node.succeeded'), + false + ); +}); + +test('paused, recovered, and cancelled graphs cannot be executed or revived by the scheduler', async () => { + for (const status of ['paused', 'paused_recovered', 'cancelled']) { + const store = new InMemoryExecutionStore(); + let graph = store.create({ + id: `graph-${status}`, + goal: status, + nodes: [{ id: 'work', kind: 'analysis', title: 'Work', dependencies: [] }], + }); + if (status === 'paused_recovered') { + graph = store.append(graph.id, { + expectedRevision: graph.revision, + type: 'graph.recovered', + data: { + recovery: { + recoveredAt: 2, + requiresUserResume: true, + interruptedNodeIds: [], + blockedMutationNodeIds: [], + }, + }, + }); + } else if (status === 'cancelled') { + graph = store.append(graph.id, { + expectedRevision: graph.revision, + type: 'graph.cancelled', + }); + } + let calls = 0; + const result = await new ExecutionGraphScheduler(store).runAvailable(graph.id, async () => { + calls += 1; + return { success: true }; + }); + assert.equal(calls, 0, `${status} graph executed work`); + assert.equal(result.graph.status, status); + if (status === 'cancelled') { + assert.throws( + () => + store.append(graph.id, { + expectedRevision: result.graph.revision, + type: 'graph.resumed', + }), + /cannot transition from cancelled/ + ); + } + } +}); + test('scheduler excludes overlapping write paths while admitting unrelated work', () => { const store = createStore([ { diff --git a/packages/moss-agent/test/execution-graph.spec.mjs b/packages/moss-agent/test/execution-graph.spec.mjs index 323c0b24..622a88a7 100644 --- a/packages/moss-agent/test/execution-graph.spec.mjs +++ b/packages/moss-agent/test/execution-graph.spec.mjs @@ -92,6 +92,22 @@ test('owner lease excludes a second local runtime and expires deterministically' store.create(graphInput()); const first = store.acquireLease('graph-1', { ownerId: 'cli', ttlMs: 30 }); assert.equal(first.ownerId, 'cli'); + assert.throws( + () => + store.append('graph-1', { + expectedRevision: 1, + type: 'node.ready', + nodeId: 'analyse', + }), + (error) => error?.code === 'EXECUTION_LEASE_HELD' + ); + const owned = store.append('graph-1', { + expectedRevision: 1, + type: 'node.ready', + nodeId: 'analyse', + ownerLease: first, + }); + assert.equal(owned.revision, 2); assert.throws( () => store.acquireLease('graph-1', { ownerId: 'web', ttlMs: 30 }), (error) => error?.code === 'EXECUTION_LEASE_HELD' @@ -99,10 +115,49 @@ test('owner lease excludes a second local runtime and expires deterministically' now = 131; const second = store.acquireLease('graph-1', { ownerId: 'web', ttlMs: 30 }); assert.equal(second.ownerId, 'web'); + assert.throws( + () => + store.append('graph-1', { + expectedRevision: 2, + type: 'node.started', + nodeId: 'analyse', + ownerLease: first, + }), + (error) => error?.code === 'EXECUTION_LEASE_HELD' + ); assert.throws(() => store.releaseLease('graph-1', first), /lease token/); store.releaseLease('graph-1', second); }); +test('JSONL append validates the current fencing token across store instances', () => { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-execution-fence-')); + try { + const first = new JsonlExecutionStore({ rootDir }); + const second = new JsonlExecutionStore({ rootDir }); + first.create(graphInput()); + const lease = first.acquireLease('graph-1', { ownerId: 'scheduler' }); + assert.throws( + () => + second.append('graph-1', { + expectedRevision: 1, + type: 'node.ready', + nodeId: 'analyse', + }), + (error) => error?.code === 'EXECUTION_LEASE_HELD' + ); + const graph = first.append('graph-1', { + expectedRevision: 1, + type: 'node.ready', + nodeId: 'analyse', + ownerLease: lease, + }); + assert.equal(graph.revision, 2); + first.releaseLease('graph-1', lease); + } finally { + fs.rmSync(rootDir, { recursive: true, force: true }); + } +}); + test('JSONL store persists CAS across instances, snapshots, and repairs only a corrupt tail', () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-execution-graph-')); try { @@ -176,3 +231,36 @@ test('restart recovery is fail-closed for active work', () => { assert.equal(recovered.recovery?.requiresUserResume, true); assert.equal(recovered.recovery?.interruptedNodeIds[0], 'analyse'); }); + +test('graph completion and dynamic nodes are rejected when invariants are not satisfied', () => { + const store = new InMemoryExecutionStore(); + let graph = store.create(graphInput('invariants')); + graph = store.append(graph.id, { + expectedRevision: graph.revision, + type: 'graph.resumed', + }); + assert.throws( + () => + store.append(graph.id, { + expectedRevision: graph.revision, + type: 'graph.completed', + }), + /CompletionArbiter/ + ); + assert.throws( + () => + store.append(graph.id, { + expectedRevision: graph.revision, + type: 'node.added', + data: { + node: { + id: 'invalid', + kind: 'analysis', + title: 'Invalid', + dependencies: ['missing'], + }, + }, + }), + /unknown node/ + ); +}); diff --git a/packages/moss-agent/test/legacy-execution-importer.spec.mjs b/packages/moss-agent/test/legacy-execution-importer.spec.mjs index 2156691c..f86c9311 100644 --- a/packages/moss-agent/test/legacy-execution-importer.spec.mjs +++ b/packages/moss-agent/test/legacy-execution-importer.spec.mjs @@ -65,3 +65,26 @@ test('completed legacy state becomes needs-evidence blocked instead of verified' fs.rmSync(temp, { recursive: true, force: true }); } }); + +test('a TaskFrame marker resolves its original graph when the next run id changes', () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-legacy-frame-repeat-')); + const sourcePath = path.join(temp, 'task-frame.json'); + const store = new InMemoryExecutionStore(); + const importer = new LegacyExecutionImporter(store); + try { + const first = importer.import({ + kind: 'task-frame', + sourcePath, + state: { sessionKey: 'same-session', runId: 'first-run', currentStep: 'one' }, + }); + const repeated = importer.import({ + kind: 'task-frame', + sourcePath, + state: { sessionKey: 'same-session', runId: 'second-run', currentStep: 'two' }, + }); + assert.equal(repeated.id, first.id); + assert.equal(store.list().length, 1); + } finally { + fs.rmSync(temp, { recursive: true, force: true }); + } +}); diff --git a/packages/moss-agent/test/merge-subagent-patch.spec.mjs b/packages/moss-agent/test/merge-subagent-patch.spec.mjs new file mode 100644 index 00000000..d4f27dfc --- /dev/null +++ b/packages/moss-agent/test/merge-subagent-patch.spec.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { builtinTools } from '../dist/tools/builtin.js'; + +const tool = builtinTools.find(({ name }) => name === 'merge_subagent_patch'); + +test('merge_subagent_patch is approval-gated and delegates only opaque stored artifact IDs', async () => { + assert.ok(tool); + assert.equal(tool.metadata.sideEffectClass, 'local_write'); + assert.equal(tool.metadata.requiresApproval, true); + const calls = []; + const output = await tool.execute( + { leaseId: 'lease-1', patchId: 'patch-1' }, + { + workspaceDir: '/workspace', + mergeWorkspacePatch: async (leaseId, patchId) => { + calls.push({ leaseId, patchId }); + return { status: 'merged', conflictingPaths: [], changedPaths: ['src/a.ts'] }; + }, + } + ); + assert.deepEqual(calls, [{ leaseId: 'lease-1', patchId: 'patch-1' }]); + assert.match(output, /Merged sub-agent patch patch-1/); +}); + +test('merge_subagent_patch fails closed when the host does not expose merge authority', async () => { + const output = await tool.execute( + { leaseId: 'lease-1', patchId: 'patch-1' }, + { workspaceDir: '/workspace' } + ); + assert.match(output, /unavailable/); +}); diff --git a/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs b/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs index 8b37378d..77080c31 100644 --- a/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs +++ b/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs @@ -8,6 +8,8 @@ import test from 'node:test'; import { MossAgent } from '../dist/core/agent/moss-agent.js'; import { InMemorySessionStore } from '../dist/core/session/session.js'; import { JsonlExecutionStore } from '../dist/orchestration/index.js'; +import { createGoalCheckpointMessage, createGoalState } from '../dist/core/goal/goal-state.js'; +import { createTaskFrameCheckpointMessage } from '../dist/core/goal/task-frame.js'; const provider = { id: 'unused', @@ -18,12 +20,13 @@ const provider = { }, }; -function createAgent(workspaceDir) { +function createAgent(workspaceDir, config = {}) { return new MossAgent({ llmProvider: provider, sessionStore: new InMemorySessionStore(), workspaceDir, enableSteering: false, + ...config, }); } @@ -52,3 +55,253 @@ test('MossAgent defaults to durable execution state shared across restarts', asy fs.rmSync(workspaceDir, { recursive: true, force: true }); } }); + +test('MossAgent routes graph nodes through authorized role snapshots and structured synthesis', async () => { + const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-agent-routed-')); + const agent = createAgent(workspaceDir, { + agentRoles: [ + { + id: 'architecture-advisor', + displayName: 'Architecture advisor', + kind: 'advisor', + capabilities: ['architecture'], + instructions: 'Return structured evidence.', + workspaceMode: 'shared-readonly', + outputContract: 'structured-v1', + }, + ], + }); + try { + let graph = agent.executionStore.create({ + id: 'routed', + goal: 'route by capability', + nodes: [ + { + id: 'analyse', + kind: 'analysis', + title: 'Analyse architecture', + dependencies: [], + requiredCapabilities: ['architecture'], + }, + ], + }); + agent.tasks.resume(graph.id); + let routedRole; + const outcome = await agent.runRoutedExecutionGraph(graph.id, async (routed) => { + routedRole = routed.role; + return { + assignmentId: routed.assignment.id, + roleId: routed.role.id, + status: 'PASS', + claims: [ + { + id: 'architecture-claim', + subject: 'execution authority', + conclusion: 'the graph owns execution state', + severity: 'low', + evidenceRefs: [], + }, + ], + evidenceRefs: ['architecture-claim'], + unmetCriteria: [], + runId: 'independent-advisor-run', + }; + }); + assert.equal(routedRole.id, 'architecture-advisor'); + assert.equal(Object.isFrozen(routedRole), true); + assert.equal(outcome.synthesis.coverage, 1); + assert.equal(outcome.completion.verdict, 'verified'); + assert.equal(agent.tasks.inspect(graph.id).status, 'completed'); + } finally { + await agent.close(); + fs.rmSync(workspaceDir, { recursive: true, force: true }); + } +}); + +test('routed coding work merges an approved isolated patch and requires a fresh verifier receipt', async () => { + const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-agent-routed-coding-')); + fs.writeFileSync(path.join(workspaceDir, 'target.txt'), 'before\n'); + const agent = createAgent(workspaceDir, { + allowPluginIsolatedWrite: true, + agentRoles: [ + { + id: 'implementer', + displayName: 'Implementer', + kind: 'implementer', + capabilities: ['code'], + instructions: 'Write only in the lease.', + workspaceMode: 'isolated-write', + outputContract: 'structured-v1', + }, + { + id: 'verifier', + displayName: 'Verifier', + kind: 'verifier', + capabilities: ['test'], + instructions: 'Verify independently.', + workspaceMode: 'shared-readonly', + outputContract: 'structured-v1', + }, + ], + }); + try { + const graph = agent.executionStore.create({ + id: 'routed-coding', + goal: 'change and verify', + nodes: [ + { + id: 'implement', + kind: 'implementation', + title: 'Implement', + dependencies: [], + requiredCapabilities: ['code'], + writePaths: ['target.txt'], + acceptanceCriteria: ['patch-merged'], + }, + { + id: 'verify', + kind: 'verification', + title: 'Verify', + dependencies: ['implement'], + requiredCapabilities: ['test'], + acceptanceCriteria: ['tests-green'], + }, + ], + }); + agent.tasks.resume(graph.id); + const executor = async (routed) => { + if (routed.role.kind === 'implementer') { + fs.writeFileSync(path.join(routed.workspaceLease.workspacePath, 'target.txt'), 'after\n'); + const patch = await agent.workspaceLeaseAdapter.createPatch(routed.workspaceLease); + return { + assignmentId: routed.assignment.id, + roleId: routed.role.id, + status: 'PASS', + claims: [], + evidenceRefs: [], + patchRef: patch.id, + unmetCriteria: [], + runId: 'implementation-run', + }; + } + return { + assignmentId: routed.assignment.id, + roleId: routed.role.id, + status: 'PASS', + claims: [], + evidenceRefs: [], + verification: { command: 'test', exitCode: 0, summary: 'focused test passed' }, + unmetCriteria: [], + runId: 'verifier-run', + }; + }; + const authorizeMerge = async () => {}; + const first = await agent.runRoutedExecutionGraph(graph.id, executor, 'coding', authorizeMerge); + assert.equal(first.completion, undefined); + const second = await agent.runRoutedExecutionGraph( + graph.id, + executor, + 'coding', + authorizeMerge + ); + assert.equal(second.completion.verdict, 'verified'); + assert.equal(fs.readFileSync(path.join(workspaceDir, 'target.txt'), 'utf8'), 'after\n'); + } finally { + await agent.close(); + fs.rmSync(workspaceDir, { recursive: true, force: true }); + } +}); + +test('MossAgent restart pauses active graphs while keeping unstarted ready work resumable', async () => { + const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-agent-recovery-')); + const first = createAgent(workspaceDir); + try { + let graph = first.executionStore.create({ + id: 'recoverable', + goal: 'resume safely', + nodes: [{ id: 'work', kind: 'analysis', title: 'Work', dependencies: [] }], + }); + first.tasks.resume(graph.id); + graph = first.executionScheduler.reconcile(graph.id); + assert.equal(graph.nodes.work.status, 'ready'); + await first.close(); + + const second = createAgent(workspaceDir); + try { + graph = second.tasks.inspect('recoverable'); + assert.equal(graph.status, 'paused_recovered'); + assert.equal(graph.nodes.work.status, 'ready'); + second.tasks.resume(graph.id); + let executions = 0; + const outcome = await second.runExecutionGraph(graph.id, async () => { + executions += 1; + return { + success: true, + evidence: [ + { + id: 'recovered-work', + kind: 'tool_result', + summary: 'fresh recovered execution', + createdAt: Date.now(), + }, + ], + }; + }); + assert.equal(executions, 1); + assert.equal(outcome.completion.verdict, 'verified'); + assert.equal(second.tasks.inspect(graph.id).status, 'completed'); + } finally { + await second.close(); + } + } finally { + fs.rmSync(workspaceDir, { recursive: true, force: true }); + } +}); + +test('first checkpoint read imports legacy Goal and TaskFrame state without removing messages', async () => { + const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-agent-checkpoint-import-')); + const sessionStore = new InMemorySessionStore(); + const sessionKey = 'legacy-session'; + const goal = createGoalState({ sessionKey, objective: 'Finish legacy work' }); + const frame = { + schemaVersion: 1, + sessionKey, + runId: 'legacy-run', + goal: 'Finish legacy work', + constraints: [], + currentStep: 'Inspect state', + completedSteps: [], + pendingSteps: ['Apply fix'], + artifacts: [], + importantPaths: [], + toolFindings: [], + nextAction: 'Continue', + status: 'paused_resumable', + source: 'error', + updatedAt: 1, + }; + const messages = [createGoalCheckpointMessage(goal), createTaskFrameCheckpointMessage(frame)]; + await sessionStore.replaceMessages(sessionKey, messages); + const agent = createAgent(workspaceDir, { sessionStore }); + try { + assert.equal((await agent.getGoal(sessionKey)).objective, goal.objective); + await agent.chat(sessionKey, 'continue'); + const imported = agent.tasks.list().filter(({ id }) => id.startsWith('legacy_')); + assert.equal( + imported.some(({ id }) => id.startsWith('legacy_goal_')), + true + ); + assert.equal( + imported.some(({ id }) => id.startsWith('legacy_task_frame_')), + true + ); + assert.equal((await sessionStore.loadMessages(sessionKey)).length >= messages.length, true); + const markers = fs + .readdirSync(path.join(workspaceDir, '.moss', 'runtime', 'legacy-checkpoints')) + .filter((name) => name.endsWith('.execution-graph-migration-v1.json')); + assert.equal(markers.length, 2); + } finally { + await agent.close(); + fs.rmSync(workspaceDir, { recursive: true, force: true }); + } +}); diff --git a/packages/moss-agent/test/plan-execution-graph.spec.mjs b/packages/moss-agent/test/plan-execution-graph.spec.mjs index 431f72ca..a9be2fcd 100644 --- a/packages/moss-agent/test/plan-execution-graph.spec.mjs +++ b/packages/moss-agent/test/plan-execution-graph.spec.mjs @@ -45,3 +45,20 @@ test('legacy Plan tools shadow every mutation into the authoritative dependency assert.equal(executions.load(planId).nodes['step-1'].status, 'succeeded'); assert.match(executions.load(planId).evidence[0].summary, /A evidence/); }); + +test('first mutation imports a Plan created before execution graphs were enabled', async () => { + const plans = new PlanControllerStore(); + const legacyTool = createPlanTool(plans); + const ctx = { sessionKey: 'legacy-session', workspaceDir: process.cwd() }; + const created = await legacyTool.execute( + { action: 'create', goal: 'legacy plan', steps: [{ description: 'old step' }] }, + ctx + ); + const planId = /Plan created: (\S+)/.exec(created)?.[1]; + await legacyTool.execute({ action: 'approve', planId }, ctx); + const executions = new InMemoryExecutionStore(); + const graphAwareTool = createPlanTool(plans, executions); + await graphAwareTool.execute({ action: 'start', planId }, ctx); + assert.equal(executions.load(planId).status, 'running'); + assert.equal(executions.load(planId).nodes['step-1'].status, 'running'); +}); diff --git a/packages/moss-agent/test/task-run-execution-projection.spec.mjs b/packages/moss-agent/test/task-run-execution-projection.spec.mjs index 8ffd2713..edae14c5 100644 --- a/packages/moss-agent/test/task-run-execution-projection.spec.mjs +++ b/packages/moss-agent/test/task-run-execution-projection.spec.mjs @@ -24,8 +24,9 @@ test('TaskRun v1 shadow-writes evidence but cannot false-complete an execution g assert.equal(JSON.stringify(store.load('legacy')).includes('must not copy'), false); ledger.append('legacy', { type: 'run.verified', time: 5 }); - assert.equal(store.load('legacy').status, 'completed'); - assert.equal(store.load('legacy').verification.verdict, 'verified'); + assert.equal(store.load('legacy').status, 'blocked'); + assert.equal(store.load('legacy').verification, undefined); + assert.match(store.load('legacy').evidence.at(-1).summary, /reported verified/); }); test('existing TaskRun JSONL is imported once without deleting or rewriting the source', () => { @@ -41,7 +42,7 @@ test('existing TaskRun JSONL is imported once without deleting or rewriting the const store = new InMemoryExecutionStore(); new TaskRunLedger(file, store); - assert.equal(store.load('old-run').status, 'completed'); + assert.equal(store.load('old-run').status, 'blocked'); assert.equal(fs.readFileSync(file, 'utf8'), original); assert.equal(fs.existsSync(`${file}.execution-graph-migration-v1.json`), true); new TaskRunLedger(file, store); diff --git a/packages/moss-agent/test/web-ui-browser.spec.mjs b/packages/moss-agent/test/web-ui-browser.spec.mjs index 03336ebc..40b1aea2 100644 --- a/packages/moss-agent/test/web-ui-browser.spec.mjs +++ b/packages/moss-agent/test/web-ui-browser.spec.mjs @@ -437,6 +437,7 @@ test( ); await page.click('.tool-row.tool-complete'); await page.waitForFunction(() => document.body.textContent.includes('BROWSER_TOOL_OK')); + await page.waitForFunction(() => document.body.textContent.includes('BROWSER_TOOL_COMPLETE')); await assertVisual('desktop-tool', await page.screenshot({ type: 'png' })); await page.type('textarea', 'Require browser approval.'); await page.keyboard.press('Enter'); diff --git a/packages/moss-agent/test/workspace-lease.spec.mjs b/packages/moss-agent/test/workspace-lease.spec.mjs index 9318e35d..0188060b 100644 --- a/packages/moss-agent/test/workspace-lease.spec.mjs +++ b/packages/moss-agent/test/workspace-lease.spec.mjs @@ -1,5 +1,6 @@ #!/usr/bin/env node import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -248,3 +249,35 @@ test('host merge authorization runs before the parent workspace is mutated', asy fs.rmSync(temp, { recursive: true, force: true }); } }); + +test('merge rejects forged changed paths and patch bytes instead of trusting caller metadata', async () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'moss-workspace-forged-')); + const parent = path.join(temp, 'plain'); + const adapter = new CopyWorkspaceLeaseAdapter({ rootDir: path.join(temp, 'leases') }); + fs.mkdirSync(parent, { recursive: true }); + write(path.join(parent, 'safe.txt'), 'before\n'); + try { + const lease = await adapter.create({ + id: 'lease-forged', + graphId: 'graph', + nodeId: 'work', + parentWorkspace: parent, + writePaths: ['safe.txt'], + }); + write(path.join(lease.workspacePath, 'safe.txt'), 'after\n'); + const patch = await adapter.createPatch(lease); + const forgedBody = patch.patch.replaceAll('safe.txt', 'outside.txt'); + const forged = { + ...patch, + patch: forgedBody, + digest: `sha256:${createHash('sha256').update(forgedBody).digest('hex')}`, + changedPaths: [], + }; + await assert.rejects(() => adapter.merge(lease, forged), /stored artifact/); + assert.equal(fs.existsSync(path.join(parent, 'outside.txt')), false); + assert.equal(fs.readFileSync(path.join(parent, 'safe.txt'), 'utf8'), 'before\n'); + } finally { + await adapter.release('lease-forged', 'rejected'); + fs.rmSync(temp, { recursive: true, force: true }); + } +}); diff --git a/scripts/config/maintainability-baseline.json b/scripts/config/maintainability-baseline.json index 2bad8b18..79b4d315 100644 --- a/scripts/config/maintainability-baseline.json +++ b/scripts/config/maintainability-baseline.json @@ -47,7 +47,7 @@ "reason": "Legacy hotspot recorded at initial maintainability-ratchet adoption." }, "packages/moss-agent/src/core/agent/moss-agent.ts": { - "maxLines": 2009, + "maxLines": 1994, "reason": "Legacy hotspot recorded at initial maintainability-ratchet adoption." }, "packages/moss-agent/src/core/loop/agent-loop.ts": { From 24b1f9be738accf9ac107c0a6d5b8d04b88fddab Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 20:17:24 +0800 Subject: [PATCH 14/21] fix(ci): build agent before long-horizon acceptance --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90b53d64..59f20aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Categories: **Added** · **Changed** · **Fixed** · **Removed** · **Internal** ### Fixed +- **Clean-checkout long-horizon acceptance**: the acceptance command now builds the agent before + importing its distribution entrypoints, including on macOS, Ubuntu, and Windows CI. - **Repeated legacy TaskFrame recovery**: migration markers now resolve their persisted graph ID, so a later turn with a new run ID reuses the original execution graph instead of failing the Web conversation before approval or tool execution. diff --git a/package.json b/package.json index 81fde3b5..723b284c 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "check:vendored": "node scripts/check-vendored-sources.mjs", "check:pr-title": "node scripts/check-pr-title.mjs", "check:agent-harness-benchmark": "node scripts/check-agent-harness-benchmark.mjs", - "acceptance:long-horizon": "node benchmarks/run-long-horizon-acceptance.mjs", + "acceptance:long-horizon": "npm run build -w @rdk-moss/agent && node benchmarks/run-long-horizon-acceptance.mjs", "check:leaderboard-readiness": "node scripts/check-leaderboard-readiness.mjs", "check:moss-web-tokens": "node scripts/check-moss-web-tokens.mjs", "check": "npm run format:check && npm run lint && npm run typecheck && npm run check:boundaries && npm run check:hygiene && npm run check:vendored && npm run check:moss-web-tokens && npm run check:maintainability && npm run test:standards", From a0ba3bc96c5ee2bc4a4f34361f1dfd3ee9ac7651 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 20:29:41 +0800 Subject: [PATCH 15/21] fix(agent): keep execution cancellation observable --- benchmarks/run-long-horizon-acceptance.mjs | 4 +++- .../moss-agent/src/orchestration/execution-graph-scheduler.ts | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/benchmarks/run-long-horizon-acceptance.mjs b/benchmarks/run-long-horizon-acceptance.mjs index 653927af..4df7ee11 100644 --- a/benchmarks/run-long-horizon-acceptance.mjs +++ b/benchmarks/run-long-horizon-acceptance.mjs @@ -6,6 +6,8 @@ import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; +const PACKAGED_CLI_SMOKE_TIMEOUT_MS = 10 * 60_000; + import { MossAgent } from '../packages/moss-agent/dist/index.js'; import { startMossWebServer } from '../packages/moss-agent/dist/web-ui/web-server.js'; @@ -150,7 +152,7 @@ async function concurrencyCase(index) { const cli = spawnSync(process.execPath, ['scripts/smoke-moss-cli.mjs'], { cwd: path.dirname(path.dirname(fileURLToPath(import.meta.url))), encoding: 'utf8', - timeout: 180_000, + timeout: PACKAGED_CLI_SMOKE_TIMEOUT_MS, }); assert.equal(cli.status, 0, cli.stderr || cli.stdout); assert.match(cli.stdout, /\[smoke:moss-cli\] PASS/); diff --git a/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts b/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts index a6f4278c..6a45e035 100644 --- a/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts +++ b/packages/moss-agent/src/orchestration/execution-graph-scheduler.ts @@ -296,7 +296,6 @@ export class ExecutionGraphScheduler { executionAbort.abort(error); } }, this.leaseRenewIntervalMs); - renewal.unref(); try { let graph = this.reconcileOwned(graphId, lease); const candidates = this.selectRunnable(graph); From 749a6f192bce4ff771a9a9f8c682a00c48092dd8 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 20:39:49 +0800 Subject: [PATCH 16/21] fix(agent): support durable graph writes on Windows --- CHANGELOG.md | 3 ++ .../orchestration/jsonl-execution-store.ts | 8 +-- .../src/orchestration/sync-directory-entry.ts | 20 ++++++++ .../test/sync-directory-entry.spec.mjs | 49 +++++++++++++++++++ 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 packages/moss-agent/src/orchestration/sync-directory-entry.ts create mode 100644 packages/moss-agent/test/sync-directory-entry.spec.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f20aa1..ee0cb404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Categories: **Added** · **Changed** · **Fixed** · **Removed** · **Internal** ### Fixed +- **Windows execution-store durability**: atomic lease and snapshot publication now retains file + fsync while avoiding unsupported Windows directory-handle fsync, so durable long-horizon runs can + acquire and renew graph ownership on Windows. - **Clean-checkout long-horizon acceptance**: the acceptance command now builds the agent before importing its distribution entrypoints, including on macOS, Ubuntu, and Windows CI. - **Repeated legacy TaskFrame recovery**: migration markers now resolve their persisted graph ID, diff --git a/packages/moss-agent/src/orchestration/jsonl-execution-store.ts b/packages/moss-agent/src/orchestration/jsonl-execution-store.ts index 932a4e1e..6f62b742 100644 --- a/packages/moss-agent/src/orchestration/jsonl-execution-store.ts +++ b/packages/moss-agent/src/orchestration/jsonl-execution-store.ts @@ -7,6 +7,7 @@ import lockfile from 'proper-lockfile'; import { ErrorCode, MossError, wrapAsMoss } from '../errors.js'; import { executionCompletionAuthority } from './completion-authority-internal.js'; import { createGraphCreatedEvent, projectExecutionGraph } from './execution-projector.js'; +import { syncDirectoryEntryIfSupported } from './sync-directory-entry.js'; import type { AcquireExecutionLeaseInput, AppendExecutionEventInput, @@ -288,12 +289,7 @@ export class JsonlExecutionStore implements ExecutionStore { fs.closeSync(descriptor); } fs.renameSync(temporary, file); - const directory = fs.openSync(path.dirname(file), 'r'); - try { - fs.fsyncSync(directory); - } finally { - fs.closeSync(directory); - } + syncDirectoryEntryIfSupported(path.dirname(file)); } private readLease(graphId: string): ExecutionOwnerLease | undefined { diff --git a/packages/moss-agent/src/orchestration/sync-directory-entry.ts b/packages/moss-agent/src/orchestration/sync-directory-entry.ts new file mode 100644 index 00000000..551740ab --- /dev/null +++ b/packages/moss-agent/src/orchestration/sync-directory-entry.ts @@ -0,0 +1,20 @@ +import fs from 'node:fs'; + +type DirectorySyncOperations = Pick; + +/** Persist a directory entry where the host platform supports directory fsync. @internal */ +export function syncDirectoryEntryIfSupported( + directoryPath: string, + operations: DirectorySyncOperations = fs, + platform: NodeJS.Platform = process.platform +): void { + // Windows does not provide portable directory-handle fsync semantics. The + // file itself is fsynced before rename; this mirrors the session-store seam. + if (platform === 'win32') return; + const directory = operations.openSync(directoryPath, 'r'); + try { + operations.fsyncSync(directory); + } finally { + operations.closeSync(directory); + } +} diff --git a/packages/moss-agent/test/sync-directory-entry.spec.mjs b/packages/moss-agent/test/sync-directory-entry.spec.mjs new file mode 100644 index 00000000..fe95e2d5 --- /dev/null +++ b/packages/moss-agent/test/sync-directory-entry.spec.mjs @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { syncDirectoryEntryIfSupported } from '../dist/orchestration/sync-directory-entry.js'; + +test('Windows skips unsupported directory fsync without weakening file fsync', () => { + let called = false; + syncDirectoryEntryIfSupported( + 'C:\\runtime\\graph', + { + openSync: () => { + called = true; + throw Object.assign(new Error('unsupported directory handle'), { code: 'EPERM' }); + }, + fsyncSync: () => { + called = true; + }, + closeSync: () => { + called = true; + }, + }, + 'win32' + ); + assert.equal(called, false); +}); + +test('POSIX directory fsync remains mandatory and closes its descriptor', () => { + const calls = []; + assert.throws( + () => + syncDirectoryEntryIfSupported( + '/runtime/graph', + { + openSync: () => { + calls.push('open'); + return 17; + }, + fsyncSync: () => { + calls.push('fsync'); + throw new Error('durability failure'); + }, + closeSync: () => calls.push('close'), + }, + 'linux' + ), + /durability failure/ + ); + assert.deepEqual(calls, ['open', 'fsync', 'close']); +}); From d65c773a4c9a11effc0dd49971a8ec17021878bd Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 20:50:23 +0800 Subject: [PATCH 17/21] test(web): await narrow layout transition --- packages/moss-agent/test/web-ui-browser.spec.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/moss-agent/test/web-ui-browser.spec.mjs b/packages/moss-agent/test/web-ui-browser.spec.mjs index 40b1aea2..d8b4b79b 100644 --- a/packages/moss-agent/test/web-ui-browser.spec.mjs +++ b/packages/moss-agent/test/web-ui-browser.spec.mjs @@ -467,7 +467,10 @@ test( (expected) => Math.round( globalThis.document.querySelector('.moss-sidebar-column').getBoundingClientRect().width - ) === expected, + ) === expected && + Math.round( + globalThis.document.querySelector('.moss-details-column').getBoundingClientRect().width + ) === 0, resizedSidebar ); assert.equal( From dd7441c5b290c6f3f1689b518c46e7a48c5b81ee Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 21:03:51 +0800 Subject: [PATCH 18/21] test(agent): normalize Windows patch line endings --- .../test/moss-agent-durable-orchestration.spec.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs b/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs index 77080c31..83fc7118 100644 --- a/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs +++ b/packages/moss-agent/test/moss-agent-durable-orchestration.spec.mjs @@ -205,7 +205,10 @@ test('routed coding work merges an approved isolated patch and requires a fresh authorizeMerge ); assert.equal(second.completion.verdict, 'verified'); - assert.equal(fs.readFileSync(path.join(workspaceDir, 'target.txt'), 'utf8'), 'after\n'); + assert.equal( + fs.readFileSync(path.join(workspaceDir, 'target.txt'), 'utf8').replaceAll('\r\n', '\n'), + 'after\n' + ); } finally { await agent.close(); fs.rmSync(workspaceDir, { recursive: true, force: true }); From ca6a869246e90182f87023b056eba04cc596948f Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 21:18:03 +0800 Subject: [PATCH 19/21] test(agent): normalize copy patch line endings --- packages/moss-agent/test/workspace-lease.spec.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/moss-agent/test/workspace-lease.spec.mjs b/packages/moss-agent/test/workspace-lease.spec.mjs index 0188060b..e8afe630 100644 --- a/packages/moss-agent/test/workspace-lease.spec.mjs +++ b/packages/moss-agent/test/workspace-lease.spec.mjs @@ -177,7 +177,10 @@ test('copy snapshot adapter merges declared changes and rejects undeclared write assert.deepEqual(patch.changedPaths, ['docs/output.txt']); const merged = await adapter.merge(lease, patch); assert.equal(merged.status, 'merged'); - assert.equal(fs.readFileSync(path.join(parent, 'docs/output.txt'), 'utf8'), 'generated\n'); + assert.equal( + fs.readFileSync(path.join(parent, 'docs/output.txt'), 'utf8').replaceAll('\r\n', '\n'), + 'generated\n' + ); const unsafe = await adapter.create({ id: 'lease-unsafe', From 7337a42dd582eae30d1e481c3bd53172b5ea2dff Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 21:33:42 +0800 Subject: [PATCH 20/21] fix(ci): accommodate Windows package smoke --- benchmarks/run-long-horizon-acceptance.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/run-long-horizon-acceptance.mjs b/benchmarks/run-long-horizon-acceptance.mjs index 4df7ee11..6c743d00 100644 --- a/benchmarks/run-long-horizon-acceptance.mjs +++ b/benchmarks/run-long-horizon-acceptance.mjs @@ -6,7 +6,7 @@ import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; -const PACKAGED_CLI_SMOKE_TIMEOUT_MS = 10 * 60_000; +const PACKAGED_CLI_SMOKE_TIMEOUT_MS = 20 * 60_000; import { MossAgent } from '../packages/moss-agent/dist/index.js'; import { startMossWebServer } from '../packages/moss-agent/dist/web-ui/web-server.js'; From 012eff594ae8a385f0dc462e70eab845a6c2b541 Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Tue, 18 Aug 2026 21:48:24 +0800 Subject: [PATCH 21/21] docs(agent): close long-horizon delivery checklist --- openspec/changes/unify-long-horizon-execution/tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openspec/changes/unify-long-horizon-execution/tasks.md b/openspec/changes/unify-long-horizon-execution/tasks.md index a6da993c..35d5736f 100644 --- a/openspec/changes/unify-long-horizon-execution/tasks.md +++ b/openspec/changes/unify-long-horizon-execution/tasks.md @@ -30,4 +30,4 @@ - [x] 5.1 Cover state, restart, DAG, workspace, expert, plugin, and completion negative cases. - [x] 5.2 Run real CLI/Web paths and record 10/10 long-task plus 10/10 concurrency evidence. - [x] 5.3 Update API reports, Architecture, extension/user docs, and changelog. -- [ ] 5.4 Pass focused tests, `npm run check`, CLI smoke, `npm run verify`, and three-platform CI. +- [x] 5.4 Pass focused tests, `npm run check`, CLI smoke, `npm run verify`, and three-platform CI.