From d247c50c6f99da5b7cfb9e5108bf1e9efa5ec4a4 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Thu, 6 Aug 2026 19:14:13 +0800 Subject: [PATCH] fix(web): return every service a host row injects, and run the browser lane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The browser e2e lane had been failing wholesale since this stack moved the agent plane into presets, and nothing caught it: 34 of 48 files. Two of the causes are product defects, not test breakage. `bashEnv` goes back to the host plane. `apps/cli/src/web.ts` injects it to publish `DSH_WEB_URL`/`DSH_WEB_MODE`, so the earlier note that "nothing outside the agent plane injects bashEnv" was simply wrong — behind a preset's `shell` realm those variables reached no shell at all, and a `dsh web` agent could not find the address of its own interface. This is the same criterion that returned `subagents`: a host row that injects a service resolves before any session exists and has no agent to key by, so the service is host-plane. `tool-bash` consumes the host registry from inside the preset, which works because an agent context chains to the host; only the reverse is invisible. `tool-subagent-report` goes back with it. It is not a tool this agent calls: it registers a continuable SETUP on the host `subagents` singleton, and that list is not scope-aware. One copy per mounted preset meant every child was handed `report` once per live session, so the second registration threw and a cold subagent resume failed with `subagent-not-resumable` — a diagnostic three layers removed from the cause. The lane's own composition facts follow. Skill roots resolve inside a preset now, a subtree include patches cannot reach, so the scaffold pins the roots' documented environment fallback for its whole lifetime rather than for the boot — presets mount per session. Without it the developer's real `~/.dsh/skills` enters replay requests and goldens while CI sees none. The `apps/cli` composition test pins `storage-json` for the same reason: unpinned it wrote, and then read back, the developer's own `~/.dsh/storages/`. Three tests now address through an agent what they used to read off the root context, because that is where the thing lives: the tool catalog, the skill registry, and the token meter. The seeded-history projection baseline asserts the opposite of what it did — a detached session yields a preset-plane projection only from a durable checkpoint written while it was live, and this seed was written straight to persistence and never ran. Goldens re-recorded for the hero's preset chip and the settings nav entry. --- ...-08-03-per-session-agent-presets.i18n.yaml | 4 +- .../agent-presets/cordis/agent.cordis.yml | 34 ++++++------- .../agent-presets/minimal/agent.cordis.yml | 23 ++++----- .../agent-presets/standard/agent.cordis.yml | 34 ++++++------- apps/cli/tests/web-agent-presets.spec.ts | 8 ++- apps/web/tests/scaffold-hermetic.e2e.ts | 26 ++++++++-- apps/web/tests/scaffold.ts | 27 ++++++++++ apps/web/tests/seeded-history.e2e.ts | 35 ++++++++++--- apps/web/tests/shipped-composition.e2e.ts | 27 ++++++++-- .../lifecycle-chrome/hero.expected.md | 4 ++ .../lifecycle-chrome/plan-active.expected.md | 4 ++ .../lifecycle-chrome/reloaded.expected.md | 2 + .../models-settings/configured.expected.md | 3 ++ .../models-settings/empty.expected.md | 3 ++ .../models.expected.md | 3 ++ .../settings-chrome/dialog.expected.md | 7 +++ docs/cordis-catalog/services.md | 50 +++++++++---------- docs/module-graph.md | 7 ++- packages/bundle/web-app/cordis.patch.yml | 5 -- packages/client/README.i18n.yaml | 4 +- packages/client/connection/README.i18n.yaml | 4 +- .../client/ui-agent-preset/README.i18n.yaml | 4 +- packages/client/ui-question/README.i18n.yaml | 4 +- .../cordis/tool-cordis/src/api-catalog.ts | 14 +++--- packages/host/apiproxy/README.i18n.yaml | 4 +- packages/preset/README.i18n.yaml | 4 +- .../preset/agent-presets/README.i18n.yaml | 4 +- .../README.i18n.yaml | 4 +- 28 files changed, 225 insertions(+), 127 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml index 25338cc072..8af5238a2e 100644 --- a/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-03-per-session-agent-presets.md -2026-08-03-per-session-agent-presets.md: 51f1d6790eb763e6ad2f534fb2e1137ecbcba1f2 -2026-08-03-per-session-agent-presets.zh.md: c1881c372880e56adc74a46e20c5ec9bd34b36aa +2026-08-03-per-session-agent-presets.md: b0f0de131b6f5051bb5a17a38636826ebd5771fd +2026-08-03-per-session-agent-presets.zh.md: 9ca1374d1b4b43fda24fea81adf741d474dc0084 diff --git a/apps/cli/config/agent-presets/cordis/agent.cordis.yml b/apps/cli/config/agent-presets/cordis/agent.cordis.yml index a69f80895a..564740e13d 100644 --- a/apps/cli/config/agent-presets/cordis/agent.cordis.yml +++ b/apps/cli/config/agent-presets/cordis/agent.cordis.yml @@ -33,22 +33,15 @@ # ── shell ─────────────────────────────────────────────────────────────────── -# `tool-bash` reads as a tool but provides the `bashEnv` service, so it needs a -# realm like any other provider. The executor behind it (`bash-sandbox`) stays -# in the host composition, where the sandbox policy owns it. -- id: shell - name: cordis:group - group: true - isolate: - bashEnv: true - config: - # The registry and its consumer share the realm: a consumer left outside - # would resolve the host's `bashEnv`, which this plane no longer provides. - - id: bash-env - name: '@deepseek-ai/dsh-bash-env' - - - id: tool-bash - name: '@deepseek-ai/dsh-tool-bash' +# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to +# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is +# the criterion for host-plane ownership — injection resolves before any session +# exists, so there is no agent to key by. Behind a preset realm those variables +# never reached the model's shell at all. `tool-bash` consumes the host registry +# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the +# sandbox policy owns it. +- id: tool-bash + name: '@deepseek-ai/dsh-tool-bash' # ── filesystem ────────────────────────────────────────────────────────────── @@ -166,6 +159,12 @@ # `workflows` is different — nothing outside an agent reads it — so every row # that reaches it shares one entry-local realm here, and a consumer left # outside would resolve a host registry this preset does not populate. +# +# `tool-subagent-report` is host-plane for the same reason as the registry, +# not because a preset may not want it: it registers a CONTINUABLE SETUP on +# that singleton rather than a tool this agent calls, and the setup list is +# not scope-aware — one copy per mounted preset means every child gets +# `report` registered once per live session, which throws on the second. - id: delegation name: cordis:group group: true @@ -192,9 +191,6 @@ toolName: subagent_fork backgroundMode: continuable - - id: tool-subagent-report - name: '@deepseek-ai/dsh-tool-subagent-report' - - id: workflow-workerthread name: '@deepseek-ai/dsh-workflow-workerthread' config: diff --git a/apps/cli/config/agent-presets/minimal/agent.cordis.yml b/apps/cli/config/agent-presets/minimal/agent.cordis.yml index 6302a3cf14..8ca6f0dcdf 100644 --- a/apps/cli/config/agent-presets/minimal/agent.cordis.yml +++ b/apps/cli/config/agent-presets/minimal/agent.cordis.yml @@ -15,20 +15,15 @@ text: >- You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}. -# `tool-bash` provides the `bashEnv` service, so it needs a realm even alone. -- id: shell - name: cordis:group - group: true - isolate: - bashEnv: true - config: - # The registry and its consumer share the realm: a consumer left outside - # would resolve the host's `bashEnv`, which this plane no longer provides. - - id: bash-env - name: '@deepseek-ai/dsh-bash-env' - - - id: tool-bash - name: '@deepseek-ai/dsh-tool-bash' +# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to +# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is +# the criterion for host-plane ownership — injection resolves before any session +# exists, so there is no agent to key by. Behind a preset realm those variables +# never reached the model's shell at all. `tool-bash` consumes the host registry +# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the +# sandbox policy owns it. +- id: tool-bash + name: '@deepseek-ai/dsh-tool-bash' - id: tool-str-replace-editor name: '@deepseek-ai/dsh-tool-str-replace-editor' diff --git a/apps/cli/config/agent-presets/standard/agent.cordis.yml b/apps/cli/config/agent-presets/standard/agent.cordis.yml index e029517f91..875262c69e 100644 --- a/apps/cli/config/agent-presets/standard/agent.cordis.yml +++ b/apps/cli/config/agent-presets/standard/agent.cordis.yml @@ -31,22 +31,15 @@ # ── shell ─────────────────────────────────────────────────────────────────── -# `tool-bash` reads as a tool but provides the `bashEnv` service, so it needs a -# realm like any other provider. The executor behind it (`bash-sandbox`) stays -# in the host composition, where the sandbox policy owns it. -- id: shell - name: cordis:group - group: true - isolate: - bashEnv: true - config: - # The registry and its consumer share the realm: a consumer left outside - # would resolve the host's `bashEnv`, which this plane no longer provides. - - id: bash-env - name: '@deepseek-ai/dsh-bash-env' - - - id: tool-bash - name: '@deepseek-ai/dsh-tool-bash' +# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to +# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is +# the criterion for host-plane ownership — injection resolves before any session +# exists, so there is no agent to key by. Behind a preset realm those variables +# never reached the model's shell at all. `tool-bash` consumes the host registry +# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the +# sandbox policy owns it. +- id: tool-bash + name: '@deepseek-ai/dsh-tool-bash' # ── filesystem ────────────────────────────────────────────────────────────── @@ -181,6 +174,12 @@ # `workflows` is different — nothing outside an agent reads it — so every row # that reaches it shares one entry-local realm here, and a consumer left # outside would resolve a host registry this preset does not populate. +# +# `tool-subagent-report` is host-plane for the same reason as the registry, +# not because a preset may not want it: it registers a CONTINUABLE SETUP on +# that singleton rather than a tool this agent calls, and the setup list is +# not scope-aware — one copy per mounted preset means every child gets +# `report` registered once per live session, which throws on the second. - id: delegation name: cordis:group group: true @@ -207,9 +206,6 @@ toolName: subagent_fork backgroundMode: continuable - - id: tool-subagent-report - name: '@deepseek-ai/dsh-tool-subagent-report' - - id: workflow-workerthread name: '@deepseek-ai/dsh-workflow-workerthread' config: diff --git a/apps/cli/tests/web-agent-presets.spec.ts b/apps/cli/tests/web-agent-presets.spec.ts index 38cb27ca2e..40cb8c2b9d 100644 --- a/apps/cli/tests/web-agent-presets.spec.ts +++ b/apps/cli/tests/web-agent-presets.spec.ts @@ -1,7 +1,7 @@ import { mkdtemp, readFile, stat, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { fileURLToPath } from 'node:url' -import { join } from 'node:path' +import { dirname, join } from 'node:path' import { Context } from 'cordis' import { boot, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot' import { SessionId } from '@deepseek-ai/dsh-session' @@ -22,6 +22,7 @@ const WEB_OVERLAY = join(CONFIG_DIR, 'web.cordis.yml') * agent's capabilities is the real thing, including both shipped presets. */ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promise { + const storageRoot = join(dirname(settingsFile), 'storages') const patches: PatchOptions[] = [ ...loadOverlayPatches('dsh-test', WEB_OVERLAY), // The settings row defaults to `$DSH_HOME/settings.yaml`. Left alone it @@ -30,6 +31,11 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis // outcome. Point it at a temp file for the same reason the roster below // names only the shipped root. { id: 'settings', config: { path: settingsFile, watch: false } }, + // storage-json's root is anchored to the real $DSH_HOME. Unpinned, this + // file writes the developer's own `~/.dsh/storages/` — and then reads it + // back on the next run, so a stored document from any other build decides + // this test's boot. Same reason the settings row above is pinned. + { id: 'storage-json', config: { root: storageRoot } }, // Host rows with side effects outside this process: a bound port, a served // asset tree, a telemetry exporter. `api-gateway` and `directory-picker` // stay ENABLED on purpose — the api-proxy is the host row that injects diff --git a/apps/web/tests/scaffold-hermetic.e2e.ts b/apps/web/tests/scaffold-hermetic.e2e.ts index 6e14eebfa5..8b7c2ccea3 100644 --- a/apps/web/tests/scaffold-hermetic.e2e.ts +++ b/apps/web/tests/scaffold-hermetic.e2e.ts @@ -3,6 +3,8 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { expect, it } from 'vitest' import type {} from '@deepseek-ai/dsh-skill' +import { SessionId } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-agent-presets' import { launchWebScaffold, type WebScaffold } from './scaffold.ts' async function writeSkill(root: string, name: string): Promise { @@ -37,10 +39,26 @@ it('isolates replay skill discovery from every ambient host root', async () => { let scaffold: WebScaffold | undefined try { scaffold = await launchWebScaffold() - const names = (await scaffold.ctx.skills.list({ cwd: scaffold.workspaceCwd })).map(skill => skill.name) - expect(names).not.toContain('ambient-dsh') - expect(names).not.toContain('ambient-agents') - expect(names).not.toContain('ambient-bundled') + const ctx = scaffold.ctx + // The skill registry belongs to one agent's preset, behind an `isolate` + // realm the host cannot resolve by name — so the roots under test are only + // reachable through a composed agent, which is also the only shape that + // ever asks. `serviceFor` is the same addressing the gateway's `skill.list` + // uses for a browser request about a session. + const handle = await ctx.agents.create({ + sessionId: SessionId('hermetic-skills'), + setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined), + }) + try { + const skills = ctx.agentPresets.serviceFor(handle.agent, 'skills') + if (skills === undefined) throw new Error('composed agent mounts no skill registry') + const names = (await skills.list({ cwd: scaffold.workspaceCwd })).map(skill => skill.name) + expect(names).not.toContain('ambient-dsh') + expect(names).not.toContain('ambient-agents') + expect(names).not.toContain('ambient-bundled') + } finally { + await handle.dispose() + } } finally { try { await scaffold?.close() diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 8a3e7a2c79..a4c08dcf17 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -249,6 +249,31 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise [key, process.env[key]]), + ) + let skillRootEnvironmentRestored = false + const restoreSkillRootEnvironment = (): void => { + if (skillRootEnvironmentRestored) return + skillRootEnvironmentRestored = true + for (const [key, value] of Object.entries(originalSkillRootEnvironment)) { + if (value === undefined) Reflect.deleteProperty(process.env, key) + else process.env[key] = value + } + } + Object.assign(process.env, skillRootEnvironment) let persistenceRoot: string try { persistenceRoot = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-sessions-')) @@ -418,6 +443,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise 0) { throw new AggregateError([error, ...cleanupFailures], 'web scaffold setup failed and cleanup was incomplete') } @@ -465,6 +491,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise 0) throw new AggregateError(failures, 'web scaffold teardown failed') }, diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index f9ef1055d1..c91f654440 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -18,6 +18,7 @@ import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm' import { deriveEventMessage, SessionId } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-agent-presets' import type { SessionEvent } from '@deepseek-ai/dsh-session' import type { TokenMeterService } from '@deepseek-ai/dsh-token-meter' import { join } from 'node:path' @@ -170,10 +171,22 @@ describe('web e2e: seeded history renders through cold resume', () => { if (MODE !== 'record') { const raw = await readFile(SEED, 'utf8') expect(fixtureUserPrompts(raw), 'seed fixture must carry exactly the drive prompt').toEqual([PROMPT]) - const meter = scaffold.ctx.get('tokenMeter') - if (meter === undefined) throw new Error('seeded-history requires the composed token meter') - const realized = realizeSeedFixture(scaffold, raw, SEED_ID) - await seedSession(scaffold, withCompaction(realized, meter), SEED_ID) + // The meter belongs to an agent's preset, not to the process — token + // accounting is per session. It is used here as a pure pricing function + // over fixture content, so a throwaway composition is enough to reach one. + const priced = await scaffold.ctx.agents.create({ + sessionId: SessionId('seeded-history-pricing'), + setup: agentCtx => scaffold.ctx.agentPresets.mount(agentCtx).then(() => undefined), + }) + let realizedWithCompaction: string + try { + const meter = scaffold.ctx.agentPresets.serviceFor(priced.agent, 'tokenMeter') + if (meter === undefined) throw new Error('seeded-history requires the composed token meter') + realizedWithCompaction = withCompaction(realizeSeedFixture(scaffold, raw, SEED_ID), meter) + } finally { + await priced.dispose() + } + await seedSession(scaffold, realizedWithCompaction, SEED_ID) } browser = await chromium.launch() page = await newEnglishPage(browser) @@ -220,11 +233,17 @@ describe('web e2e: seeded history renders through cold resume', () => { const projections = body.result.value?.projections expect(projections).toBeDefined() expect(projections?.asOfSeq).toBeGreaterThanOrEqual(0) - // The seed carries a session/title event: the title unit must serve it. + // The seed carries a session/title event: the title unit is host-plane, so + // it folds the detached log and serves the value with nothing composed. expect(typeof projections?.values.title).toBe('string') - // tool-todo is composed but the seed has no todo/write: whole-value null, - // key PRESENT (absence would mean the unit never registered). - expect(projections?.values).toHaveProperty('todos', null) + // `todos` is NOT here, and that is the contract rather than a gap. Its unit + // is registered by `tool-todo` inside an agent's preset, so a detached + // session yields it from exactly one place: a durable checkpoint written + // while the session was live. This seed was written straight to persistence + // and never ran, so it recorded none — and the answer no longer depends on + // whether some UNRELATED session happens to be composed right now, which is + // the whole reason the checkpoint row carries its own view. + expect(projections?.values).not.toHaveProperty('todos') }) it.skipIf(MODE === 'record')('lists the seeded session cold and renders its history from the log', async () => { diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index 782279f074..a0aaf3429e 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -11,6 +11,8 @@ import type {} from '@deepseek-ai/dsh-tools' import type {} from '@deepseek-ai/dsh-sandbox-policy' import type {} from '@deepseek-ai/dsh-user-approval' import type {} from '@deepseek-ai/dsh-permission' +import { SessionId } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-agent-presets' import { launchWebScaffold, type WebScaffold } from './scaffold.ts' /** @@ -63,11 +65,26 @@ afterEach(async () => { it('assembles the shipped Web catalog with the confined access default', async () => { scaffold = await launchWebScaffold() - const names = scaffold.ctx.tools.schemas().map(schema => schema.name).sort() - expect(names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TOOLS) - // The packaged ripgrep binary ships with the dependency, so the pair is a - // fixed roster member on every host. - expect(names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS) + const ctx = scaffold.ctx + // The catalog belongs to an AGENT, not to the process: every model-facing row + // now lives in a preset mounted under one session's scope, so the global + // layer holds nothing and a caller must name the agent to see anything. This + // composes from the deployment default — what a session that names no preset + // gets — which is the shape this test has always been about. + expect(ctx.tools.schemas().map(schema => schema.name)).toEqual([]) + const handle = await ctx.agents.create({ + sessionId: SessionId('shipped-composition'), + setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined), + }) + try { + const names = ctx.tools.schemas(handle.agent).map(schema => schema.name).sort() + expect(names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TOOLS) + // The packaged ripgrep binary ships with the dependency, so the pair is a + // fixed roster member on every host. + expect(names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS) + } finally { + await handle.dispose() + } // `workspace-write` is not "the workspace and nothing else": the shared roots // helper always admits the temp directories too. Pinning it against an // explicit mode keeps the claim independent of this surface's default, and diff --git a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md index 728dc768f8..0a4fe084fb 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md @@ -25,6 +25,10 @@ - img - text: workspace - img +- button "标准模式": + - img + - text: 标准模式 + - img - textbox "Describe what you want to build" - button "Commands": - img diff --git a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md index 6b4d7633e5..a2d17cacc5 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/plan-active.expected.md @@ -25,6 +25,10 @@ - img - text: workspace - img +- button "标准模式": + - img + - text: 标准模式 + - img - textbox "Describe what you want to build" - button "Commands": - img diff --git a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md index 6b6671ec01..8b604394be 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md @@ -1,6 +1,8 @@ - banner: - navigation "Session hierarchy": - button "Reply with the single word" [disabled] + - img + - text: 标准模式 - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/models-settings/configured.expected.md b/apps/web/tests/snapshots/models-settings/configured.expected.md index 210eae3716..0ae48045ab 100644 --- a/apps/web/tests/snapshots/models-settings/configured.expected.md +++ b/apps/web/tests/snapshots/models-settings/configured.expected.md @@ -7,6 +7,9 @@ - button "模型": - img - text: 模型 + - button "Agent 预设": + - img + - text: Agent 预设 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/empty.expected.md b/apps/web/tests/snapshots/models-settings/empty.expected.md index 161b472e57..322b6bce54 100644 --- a/apps/web/tests/snapshots/models-settings/empty.expected.md +++ b/apps/web/tests/snapshots/models-settings/empty.expected.md @@ -7,6 +7,9 @@ - button "模型": - img - text: 模型 + - button "Agent 预设": + - img + - text: Agent 预设 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md index 928a5bd0bd..56a60778c2 100644 --- a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md +++ b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md @@ -7,6 +7,9 @@ - button "模型": - img - text: 模型 + - button "Agent 预设": + - img + - text: Agent 预设 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md index f358ff26f5..aa69a337cb 100644 --- a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md +++ b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md @@ -7,10 +7,17 @@ - button "模型": - img - text: 模型 + - button "Agent 预设": + - img + - text: Agent 预设 - button "打开配置文件" - button "关闭": - img - text: 关闭 + - text: Agent 预设 对此后新建的会话生效。运行中的会话保持它开始时的 preset。 + - button "standard": + - text: standard + - img - text: 权限 选择新会话的默认权限模式 - button "Workspace Write": - text: Workspace Write diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 212a0d1d6a..00f55c0140 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -50,51 +50,51 @@ Source: [`packages/core/agent-loop/src/index.ts:277`](../../packages/core/agent- Registry over the deployment's agent presets. -Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a profile authored while the process runs is visible immediately, and a profile deleted underneath a picker disappears from the next read. +Discovery is unmemoized: `list()` and `resolve()` re-read the roots on every call so a preset authored while the process runs is visible immediately, and a preset deleted underneath a picker disappears from the next read. ```ts cordis-catalog /** - * Every profile the configured roots currently supply. - * @returns the profiles, first-root-wins per id. + * Every preset the configured roots currently supply. + * @returns the presets, first-root-wins per id. */ async list(): Promise /** - * Resolve one profile by id. - * @param id - the profile id, or `undefined` for {@link defaultId}. - * @returns the resolved profile. + * Resolve one preset by id. + * @param id - the preset id, or `undefined` for {@link defaultId}. + * @returns the resolved preset. * @throws when no configured root supplies that id. */ async resolve(id?: string): Promise /** - * Compose one agent from a profile, installing it under that agent alone. + * Compose one agent from a preset, installing it under that agent alone. * * Call from the agent factory's `setup(agentCtx)`; a rejection there rolls - * the agent creation back, so a broken profile never yields a half-composed + * the agent creation back, so a broken preset never yields a half-composed * session. * @param agentCtx - the agent's scope context. - * @param id - the profile id, or `undefined` for {@link defaultId}. - * @returns the profile that was mounted, for the caller to record. - * @throws when the profile is unknown or its composition is unusable. + * @param id - the preset id, or `undefined` for {@link defaultId}. + * @returns the preset that was mounted, for the caller to record. + * @throws when the preset is unknown or its composition is unusable. */ async mount(agentCtx: Context, id?: string): Promise /** - * Read one profile's composition text. - * @param id - the profile id. + * Read one preset's composition text. + * @param id - the preset id. * @returns the composition exactly as stored. * @throws when no configured root supplies that id. */ async read(id: string): Promise /** - * Create or replace a locally authored profile. + * Create or replace a locally authored preset. * * The text is shape-checked before it lands, so a save cannot leave a file no * session could load; it is NOT mounted, so a composition that parses but * names a missing plugin still fails at the next session that selects it. - * @param id - the profile id, which becomes its directory name. + * @param id - the preset id, which becomes its directory name. * @param content - the composition text. * @param metadata - display name and description; clearing both removes the file. * @throws when the id is unusable, the text is not an entry list, or the @@ -103,16 +103,16 @@ async read(id: string): Promise async write(id: string, content: string, metadata: PresetMetadata = {}): Promise /** - * Delete a locally authored profile. - * @param id - the profile id. - * @throws when the profile is unknown or ships with the deployment. + * Delete a locally authored preset. + * @param id - the preset id. + * @throws when the preset is unknown or ships with the deployment. */ async remove(id: string): Promise /** - * One agent's instance of a service its profile mounted. + * One agent's instance of a service its preset mounted. * - * A profile publishes services behind `isolate` realms, which are invisible + * A preset publishes services behind `isolate` realms, which are invisible * outside the group that declares them — including to the host. This is how a * caller holding the agent reads one anyway: a request that is ABOUT a * session but arrives from outside it, which is every browser RPC. @@ -121,8 +121,8 @@ async remove(id: string): Promise * because injection resolves before any session exists and has no agent to * key by; such a service belongs on the host plane instead. * @param agent - the agent whose composition to look inside. - * @param name - the service name as the profile's rows resolve it. - * @returns the agent's instance, or undefined when its profile mounts none. + * @param name - the service name as the preset's rows resolve it. + * @returns the agent's instance, or undefined when its preset mounts none. */ serviceFor(agent: { ctx: Context }, name: K): Context[K] | undefined @@ -138,9 +138,9 @@ serviceFor(agent: { ctx: Context }, name: K): * therefore restores the previous composition rather than leaving the agent * with nothing. * @param agentCtx - the agent's scope context. - * @param id - the profile to compose the agent from instead. - * @returns the profile now installed. - * @throws when the profile is unknown or its composition is unusable; the + * @param id - the preset to compose the agent from instead. + * @returns the preset now installed. + * @throws when the preset is unknown or its composition is unusable; the * previous composition is restored first. */ async recompose(agentCtx: Context, id: string): Promise diff --git a/docs/module-graph.md b/docs/module-graph.md index 4b81fab4cf..10bdd526cc 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -485,6 +485,7 @@ flowchart TD pkg_lsp_local --> pkg_lsp pkg_lsp_local --> pkg_subprocess pkg_lsp_local --> pkg_timeout + pkg_agent_presets --> pkg_atomic_write pkg_agent_presets --> pkg_invariants pkg_agent_presets --> pkg_paths pkg_agent_presets --> pkg_scope @@ -850,7 +851,9 @@ flowchart TD pkg_client_ui_agent_preset --> pkg_client_runtime pkg_client_ui_agent_preset --> pkg_client_ui_conversation pkg_client_ui_agent_preset --> pkg_client_ui_primitives + pkg_client_ui_agent_preset --> pkg_client_ui_settings pkg_client_ui_agent_preset --> pkg_client_ui_slots + pkg_client_ui_agent_preset --> pkg_client_web_react pkg_client_ui_agent_preset --> pkg_invariants pkg_client_ui_command --> pkg_client_connection pkg_client_ui_command --> pkg_client_locale @@ -1191,7 +1194,7 @@ flowchart TD | [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`lsp-local`](../packages/lsp/lsp-local) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | -| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) | +| [`agent-presets`](../packages/preset/agent-presets) | `preset` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`settings`](../packages/settings/settings) | | [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | | [`session-projection`](../packages/session-projection/session-projection) | `session-projection` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | @@ -1264,7 +1267,7 @@ flowchart TD | [`session-title-first-message-llm`](../packages/session-title/session-title-first-message-llm) | `session-title` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session-title/session-title), [`session-title-llm`](../packages/session-title/session-title-llm) | | [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | -| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | | [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) | diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index e462227088..138d009ed9 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -206,8 +206,6 @@ # absent from a surface overlay would silently reappear the day someone reorders # the composition. -- id: bash-env - disabled: true - id: tool-bash disabled: true @@ -282,9 +280,6 @@ - id: tool-subagent-fork disabled: true -- id: tool-subagent-report - disabled: true - - id: workflow-workerthread disabled: true diff --git a/packages/client/README.i18n.yaml b/packages/client/README.i18n.yaml index d06ce83a69..be342071d0 100644 --- a/packages/client/README.i18n.yaml +++ b/packages/client/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/README.md -README.md: a0888b0d02d3d6ed234489d686ae9ad996e11674 -README.zh.md: a6e4e42c12c97802897c6d58efdcc7468d45db8c +README.md: c2400ce36dc0df5494cdf69ec9fe34c71c2b65b9 +README.zh.md: b88c28df776c1a74f9b652282756b614229e0ff5 diff --git a/packages/client/connection/README.i18n.yaml b/packages/client/connection/README.i18n.yaml index 2072e6399e..b2101317a7 100644 --- a/packages/client/connection/README.i18n.yaml +++ b/packages/client/connection/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/connection/README.md -README.md: 526df44ce2a167e6f06bedea6d57e4d703848e89 -README.zh.md: 02bba4aeed7155ebacae2ad66b4325d069f37301 +README.md: 0392d51a929e2b92d9cd4b4728549250437631f1 +README.zh.md: e82b25bf30717abbcd9ec460b5790277d3f9ce13 diff --git a/packages/client/ui-agent-preset/README.i18n.yaml b/packages/client/ui-agent-preset/README.i18n.yaml index a8c3072416..80d6a464cc 100644 --- a/packages/client/ui-agent-preset/README.i18n.yaml +++ b/packages/client/ui-agent-preset/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-agent-preset/README.md -README.md: 3df8e43892ea619155918b28a171d30b6f9d1a3d -README.zh.md: 4725517c6f2ebf5b172bd01023cdef0de6f542ac +README.md: 6261984835a70b6b669545acc8f9bfafc93ab64d +README.zh.md: fb860812b1646f6b1912e4aedf56b00696957352 diff --git a/packages/client/ui-question/README.i18n.yaml b/packages/client/ui-question/README.i18n.yaml index d7a62af6a4..a00cd9bb55 100644 --- a/packages/client/ui-question/README.i18n.yaml +++ b/packages/client/ui-question/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-question/README.md -README.md: 00763e03234468e80dce56d99e6abb719a0d97e3 -README.zh.md: b0dd70264e885c73d7be76add800326e9740ef44 +README.md: d31ceb62c46cb7a720b52d9e2a6c92e98d1c7e42 +README.zh.md: 9f9ad01c3f1f661f60fe11ec072f487cb18c170a diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 06c0e4e123..a0ed4f8c46 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -98,15 +98,15 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: 'async read(id: string): Promise', - jsDoc: '/**\n * Read one profile\'s composition text.\n * @param id - the profile id.\n * @returns the composition exactly as stored.\n * @throws when no configured root supplies that id.\n */', + jsDoc: '/**\n * Read one preset\'s composition text.\n * @param id - the preset id.\n * @returns the composition exactly as stored.\n * @throws when no configured root supplies that id.\n */', }, { signature: 'async write(id: string, content: string, metadata: PresetMetadata = {}): Promise', - jsDoc: '/**\n * Create or replace a locally authored profile.\n *\n * The text is shape-checked before it lands, so a save cannot leave a file no\n * session could load; it is NOT mounted, so a composition that parses but\n * names a missing plugin still fails at the next session that selects it.\n * @param id - the profile id, which becomes its directory name.\n * @param content - the composition text.\n * @param metadata - display name and description; clearing both removes the file.\n * @throws when the id is unusable, the text is not an entry list, or the\n * deployment configures no writable root.\n */', + jsDoc: '/**\n * Create or replace a locally authored preset.\n *\n * The text is shape-checked before it lands, so a save cannot leave a file no\n * session could load; it is NOT mounted, so a composition that parses but\n * names a missing plugin still fails at the next session that selects it.\n * @param id - the preset id, which becomes its directory name.\n * @param content - the composition text.\n * @param metadata - display name and description; clearing both removes the file.\n * @throws when the id is unusable, the text is not an entry list, or the\n * deployment configures no writable root.\n */', }, { signature: 'async remove(id: string): Promise', - jsDoc: '/**\n * Delete a locally authored profile.\n * @param id - the profile id.\n * @throws when the profile is unknown or ships with the deployment.\n */', + jsDoc: '/**\n * Delete a locally authored preset.\n * @param id - the preset id.\n * @throws when the preset is unknown or ships with the deployment.\n */', }, { signature: 'serviceFor(agent: { ctx: Context }, name: K): Context[K] | undefined', @@ -2227,6 +2227,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'PrepareSessionOptions', declaration: 'export type PrepareSessionOptions = (CreateSessionOptions & {\n readonly seedSource?: undefined;\n}) | RestoredSessionOptions;', }, + { + name: 'PresetMetadata', + declaration: 'export interface PresetMetadata {\n readonly name?: string;\n readonly description?: string;\n readonly order?: number;\n}', + }, { name: 'PresetOption', declaration: 'export interface PresetOption {\n value: string;\n name: string;\n description?: string;\n}', @@ -2235,10 +2239,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'PresetSpec', declaration: 'export interface PresetSpec {\n sandbox: SandboxMode;\n approval: ApprovalPolicy;\n name?: string;\n description?: string;\n}', }, - { - name: 'PresetMetadata', - declaration: 'export interface PresetMetadata {\n readonly name?: string;\n readonly description?: string;\n readonly order?: number;\n}', - }, { name: 'PresetTrust', declaration: 'export type PresetTrust = \'system\' | \'user\';', diff --git a/packages/host/apiproxy/README.i18n.yaml b/packages/host/apiproxy/README.i18n.yaml index 8a1863f7a0..4bc8467b10 100644 --- a/packages/host/apiproxy/README.i18n.yaml +++ b/packages/host/apiproxy/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md -README.md: abada8172e0ddc74850e1b3a0ec6b19aee5b8011 -README.zh.md: 833ebf603da94ecd46dec5f8ea8c22b71d164859 +README.md: 2715a3115307017a06ac13f35efd50c239036923 +README.zh.md: 597c1fe2df6ce97385e16c6120e62fdfe0ab090d diff --git a/packages/preset/README.i18n.yaml b/packages/preset/README.i18n.yaml index 2c654c7fa6..613e8a463c 100644 --- a/packages/preset/README.i18n.yaml +++ b/packages/preset/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/preset/README.md -README.md: 997e64c75e36d6a70438a92c223dea93eab5eb32 -README.zh.md: 946b98a7b1366facec37360c9f39ca36cc908872 +README.md: ca874398f06728af60999d11f03b67fb2f4b62e0 +README.zh.md: 2187bc78ed2ad2dffcc989c587992e3aeda3c5ea diff --git a/packages/preset/agent-presets/README.i18n.yaml b/packages/preset/agent-presets/README.i18n.yaml index 2de3b390ca..2df8b66017 100644 --- a/packages/preset/agent-presets/README.i18n.yaml +++ b/packages/preset/agent-presets/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/preset/agent-presets/README.md -README.md: b6f00366ef319637d19cccb513d41dbff1eaa5e1 -README.zh.md: f09fd89c2c9bfb08fafbdb47b80ebaf26ae42b47 +README.md: c61a84730a273052eeab31dc24696a7340621d4f +README.zh.md: 1caacec4f93990865e82c4498e64578eb149a3d6 diff --git a/packages/session-persistence/session-persistence-jsonl/README.i18n.yaml b/packages/session-persistence/session-persistence-jsonl/README.i18n.yaml index 5210bcbdba..0699a58284 100644 --- a/packages/session-persistence/session-persistence-jsonl/README.i18n.yaml +++ b/packages/session-persistence/session-persistence-jsonl/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session-persistence/session-persistence-jsonl/README.md -README.md: 7cd3b36f40e00e37c161ebb30eaae95f86337203 -README.zh.md: a7395c94eb8adcddf30b3b8452c5426d57ae3a4e +README.md: 79bc4cebe6f303a5aa8b73a60dfee55dcb9a90a7 +README.zh.md: 9bd4d34f2983c60d5df2d5bab85913618d2e6d66