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

@@ -244,18 +244,18 @@ export function apply(ctx: Context, config: AcpConfig): void {
if (text.trim().length === 0) throw invalidParams('empty prompt')
const stopReason = await new Promise<StopReason>((resolve, reject) => {
// Arm the slot before send() so a listener-driven synchronous turn
// cannot slip past correlation; a synchronous send() failure (an
// agent disposed outside the bridge, e.g. an agent-loop-only reload)
// must free the slot again or the session would reject every later
// prompt as already in flight.
// Arm the slot before followup() so a listener-driven synchronous
// turn cannot slip past correlation; a synchronous followup()
// failure (an agent disposed outside the bridge, e.g. an
// agent-loop-only reload) must free the slot again or the session
// would reject every later prompt as already in flight.
record.inflight = { resolve, reject, turn: undefined }
try {
record.agent.send([{ type: 'text', text }])
record.agent.followup([{ type: 'text', text }])
} catch (error: unknown) {
record.inflight = undefined
// send() throws only Errors (disposed agent / invalid input); the
// String arm is a defensive fallback for a non-Error throw.
// followup() throws only Errors (disposed agent / invalid input);
// the String arm is a defensive fallback for a non-Error throw.
/* v8 ignore next */
const detail = error instanceof Error ? error.message : String(error)
throw internalError(`prompt was not queued: ${detail}`)

View File

@@ -49,7 +49,7 @@ describe('ACP automation output boundary', () => {
sessionId: SessionId('foreign'),
agentOptions: { provider: 'mock', model: 'mock' },
})
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await agent.whenIdle()
expect(harness.updates).toHaveLength(0)
})

View File

@@ -69,7 +69,7 @@ describe('ACP prompt lifecycle', () => {
const sessionId = await newSession(harness)
const agent = harness.ctx.agents.get(SessionId(sessionId))!
let injected = false
harness.ctx.on('agent/queued', (subject) => {
harness.ctx.on('agent/inbox/enqueue', (subject) => {
if (subject === agent && !injected) {
injected = true
agent.inject([{ type: 'text', text: 'context' }], { source: { kind: 'plugin', plugin: 'test' } })
@@ -86,8 +86,8 @@ describe('ACP prompt lifecycle', () => {
const sessionId = await newSession(harness)
const agent = harness.ctx.agents.get(SessionId(sessionId))!
let inserted = false
harness.ctx.on('agent/queued', (subject, _content, info) => {
if (subject !== agent || info.source.kind !== 'user' || inserted) return
harness.ctx.on('agent/inbox/enqueue', (subject, message) => {
if (subject !== agent || message.source.kind !== 'user' || inserted) return
inserted = true
const source = { kind: 'plugin', plugin: 'test' } as const
agent.session.append('turn/start', { turn: 1, trigger: { kind: 'message', source } })