Merge remote-tracking branch 'origin/master' into worktree/acp-automation-protocol

# Conflicts:
#	.agents/notes/implemented/architecture/2026-06-14-session-persistence.md
#	.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md
#	.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
#	.agents/notes/implemented/feature/2026-06-14-acp-multi-session.md
#	.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.zh.md
#	.agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
#	docs/architecture.i18n.yaml
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/cookbook/extension-cookbook.md
#	docs/cookbook/extension-cookbook.zh.md
#	docs/core-data-structures/approval.md
#	docs/core-data-structures/user-interaction.md
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	docs/testing.md
#	docs/tool-catalog.md
#	examples/acp-agent/tests/fixtures/live-mode-switching-2026-07-07.session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/permission-switching/session.jsonl
#	packages/goal/tool-goal/README.md
#	packages/ui/acp/README.md
#	packages/ui/acp/acp-feature-support.md
#	packages/ui/acp/src/index.ts
#	packages/ui/acp/tests/bridge.spec.ts
#	packages/ui/acp/tests/dispose.spec.ts
#	packages/ui/acp/tests/edges.spec.ts
#	packages/ui/acp/tests/turns.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-24 23:43:10 +08:00
829 changed files with 23607 additions and 1815 deletions

View File

@@ -6,7 +6,7 @@ Logged, per-agent plan collaboration state with deployment-owned guidance, direc
`plan/mode` (`{ active: boolean }`) is a log-only, whole-value-replace `SessionEventMap` member. `foldPlanMode(events)` returns the last logged value or `false`, so resume, fork, and compaction recover plan state directly from the session log. UIs observe committed flips through `session/event`.
`ctx.planMode.set(agent, active)` records a pending selection and flushes it inside the next turn boundary. `get(agent)` returns `{ active, pending? }`, separating the logged state shaping the current step from a user's optimistic selection. Prompt submission, ordinary continuation, and request-recovery retry are all covered; a changed user selection contributes one `context/message` notice when the last logged request header described the other state.
`ctx.planMode.set(agent, active)` records a pending selection and flushes it inside the next turn boundary. `get(agent)` returns `{ active, pending? }`, separating the logged state shaping the current step from a user's optimistic selection. Prompt submission, ordinary continuation, and request-recovery retry are all covered; a changed user selection contributes one plugin-sourced `user/message` notice when the last logged request header described the other state.
## Model and human surfaces

View File

@@ -357,7 +357,7 @@ export class PlanModeService extends Service {
const text = target
? 'The user switched this session to plan mode.'
: 'The user switched this session back to the default mode.'
session.append('context/message', {
session.append('user/message', {
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: 'plan-mode' },
}, { surfaceOp: 'append' })

View File

@@ -75,7 +75,7 @@ describe('plan mode through the agent loop', () => {
// prompt-submit, BEFORE the first assembly.
ctx.planMode.set(agent, true)
agent.send([{ type: 'text', text: 'explore the repo' }])
agent.followup([{ type: 'text', text: 'explore the repo' }])
await waitForIdle(ctx, agent)
const log = agent.session.events
@@ -92,7 +92,7 @@ describe('plan mode through the agent loop', () => {
const result = findEvent(log, 'tool/result')
expect(result.data.isError).toBe(false)
expect(foldPlanMode(log)).toBe(true)
expect(log.some(event => event.type === 'context/message')).toBe(false)
expect(log.some(event => event.type === 'user/message' && event.data.source.kind === 'plugin')).toBe(false)
})
it('a user flip between turns lands at the boundary: one notice and a changed header with stable tool schemas', async () => {
@@ -103,21 +103,21 @@ describe('plan mode through the agent loop', () => {
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('it-plan-flip'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'hello' }])
agent.followup([{ type: 'text', text: 'hello' }])
await waitForIdle(ctx, agent)
expect(foldPlanMode(agent.session.events)).toBe(false)
const first = findEvent(agent.session.events, 'request/header')
expect(first.data.header.tools?.map(tool => tool.name)).toEqual(['exit_plan_mode', 'read', 'write'])
ctx.planMode.set(agent, true)
agent.send([{ type: 'text', text: 'now plan' }])
agent.followup([{ type: 'text', text: 'now plan' }])
await waitForIdle(ctx, agent)
const log = agent.session.events
expect(foldPlanMode(log)).toBe(true)
const notices = log.filter(event => event.type === 'context/message')
const notices = log.filter(event => event.type === 'user/message' && event.data.source.kind === 'plugin')
expect(notices).toHaveLength(1)
expect(findEvent(log, 'context/message').data.content).toEqual([
expect(notices[0]?.type === 'user/message' && notices[0].data.content).toEqual([
{ type: 'text', text: 'The user switched this session to plan mode.' },
])
// The changed request is logged as a complete snapshot.
@@ -146,7 +146,7 @@ describe('plan mode through the agent loop', () => {
})
const idle = waitForIdle(ctx, agent)
agent.send([{ type: 'text', text: 'plan after the transient failure' }])
agent.followup([{ type: 'text', text: 'plan after the transient failure' }])
await recoveryEntered.promise
ctx.planMode.set(agent, true)
releaseRecovery.resolve(true)
@@ -163,7 +163,8 @@ describe('plan mode through the agent loop', () => {
expect(firstEnd?.seq).toBeLessThan(planMode.seq)
expect(planMode.seq).toBeLessThan(retryStart?.seq ?? 0)
expect(findEvent(log, 'request/header', 'last').data.header.system).toContain(PLAN_CONFIG.section)
expect(findEvent(log, 'context/message').data.content).toEqual([
const notice = log.find(event => event.type === 'user/message' && event.data.source.kind === 'plugin')
expect(notice?.type === 'user/message' && notice.data.content).toEqual([
{ type: 'text', text: 'The user switched this session to plan mode.' },
])
})

View File

@@ -95,7 +95,7 @@ function header(session: Session): void {
function noticeTexts(session: Session): string[] {
return session.events
.filter(event => event.type === 'context/message')
.filter(event => event.type === 'user/message' && event.data.source.kind === 'plugin')
.map(event => (event.data as { content: { type: string; text?: string }[] }).content.map(block => block.text ?? '').join(''))
}