refactor(acp): reduce bridge to automation protocol

This commit is contained in:
Tianyi Cui
2026-07-24 01:40:25 +08:00
parent b06bcfd21c
commit e819a586b0
406 changed files with 3880 additions and 21325 deletions
+2 -3
View File
@@ -7,7 +7,7 @@
import { structuredPatch } from 'diff'
import type { FileDiff } from '@deepseek-ai/dsh-tools'
/** Context lines shown on each side of an applied hunk (matches claude-agent-acp). */
/** Context lines shown on each side of an applied hunk. */
export const DIFF_CONTEXT = 3
/**
@@ -15,8 +15,7 @@ export const DIFF_CONTEXT = 3
* contextual-diff hunks. Attached opaquely (as `unknown`) on the tool result and
* persisted with the session log — it must be JSON-serializable (the session
* validates this at `append`), so `presentResult` reproduces the diff card on
* replay. The producing tool owns this shape; the bridge only sees the opaque
* `meta` and the tool narrows it back via {@link diffsFromMeta}.
* replay. The producing tool owns and narrows this opaque shape.
*/
export type FsDiffMeta = { diffs: FileDiff[] }
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* Derive the working directory a filesystem tool resolves relative paths against: the calling
* agent's per-session workspace (`exec.agent.session.header.cwd`), so each ACP session's
* agent's per-session workspace (`exec.agent.session.header.cwd`), so each session's
* `read`/`write`/`edit` act on ITS workspace, not the server's launch dir — mirroring how
* `dsh-tool-bash` defaults a bash `workdir` to the session cwd.
* Non-agent calls return `undefined`, leaving the fallback in the provider rather than reading
+5 -7
View File
@@ -125,9 +125,8 @@ export function applyWriteTool(ctx: Context, sandbox: FsSandboxSurface): void {
after: outcome.after,
}
},
// Pure display: a diff card (an editor renders write as a new-file / full- replace diff).
// `oldText: null` — a call-time presenter has no access to the file's prior content, so
// even an overwrite renders new-file style, matching claude-agent-acp.
// Pure display: a diff card. A call-time presenter has no access to prior
// file content, so `oldText: null` also represents an overwrite here.
presentCall(args): DiffCallView {
return {
card: 'diff',
@@ -136,10 +135,9 @@ export function applyWriteTool(ctx: Context, sandbox: FsSandboxSurface): void {
locations: [{ path: args.file_path }],
}
},
// Result-time display: a `diff` card so the completed `tool_call_update` re-installs the
// diff rather than the model-facing result text (an ACP `tool_call_update.content` REPLACES
// the call's content, so a text result would clobber the pending diff card). Overwrites use
// applied metadata; creates and identical overwrites use the replay-safe args fallback.
// Result-time display repeats the diff because completed views replace the
// pending view. Overwrites use applied metadata; creates and identical
// overwrites use the replay-safe args fallback.
presentResult(args, result: ToolResult): DiffResultView | undefined {
if (result.isError) return undefined
const diffs = diffsFromMeta(result.meta)
+1 -1
View File
@@ -2,7 +2,7 @@
* Unit tests for the result-time contextual-diff computation (`src/diff.ts`):
* the pure before/after → {@link FileDiff}[] hunk builder and the defensive
* `meta` narrowing. These pin the exact hunk reconstruction (context lines,
* multi-hunk replaceAll, pure insertion/deletion, no-op) the ACP bridge renders.
* multi-hunk replaceAll, pure insertion/deletion, no-op) that UIs render.
*/
import { describe, expect, it } from 'vitest'
@@ -284,8 +284,8 @@ describe('bare provider (no dsh-fs-policy)', () => {
})
// Per-session cwd: a relative file_path resolves against the calling session's workspace
// (`exec.agent.session.header.cwd`), not the backend's config.cwd — so an ACP editor's
// per-session dir wins, matching dsh-tool-bash.
// (`exec.agent.session.header.cwd`), not the backend's config.cwd, so the
// caller-selected session workspace wins, matching dsh-tool-bash.
describe('per-session cwd', () => {
let sessionDir: string
beforeEach(async () => {
+5 -6
View File
@@ -425,8 +425,8 @@ describe('edit tool', () => {
})
describe('tool-owned presentation (pure presentCall)', () => {
// presentCall is a pure display function of args (no I/O); it drives the ACP
// card's title/kind and the `locations` an editor follows along to.
// presentCall is a pure display function of args (no I/O); it drives the
// card's title/kind and the `locations` a UI follows along to.
const presentCall = async (name: string, args: unknown) => {
const { ctx } = await setup()
return ctx.tools.get(name)?.presentCall?.(args)
@@ -478,7 +478,7 @@ describe('tool-owned presentation (pure presentCall)', () => {
describe('result-time contextual diff (meta + presentResult)', () => {
// An edit records the applied contextual hunk on `tool/result` meta, and the tool's
// presentResult narrows it back into a `diff` result card the bridge renders.
// presentResult narrows it back into a replayable `diff` result card.
const withContext = 'a\nb\nc\nOLD\nd\ne\nf\n'
it('edit: execute attaches the applied hunk as meta { diffs }', async () => {
@@ -519,9 +519,8 @@ describe('result-time contextual diff (meta + presentResult)', () => {
})
it('write CREATE: an empty applied-diff projection still falls back to the whole-file diff card', async () => {
// A create has no prior content, yet the completed card must be a `diff` — an
// ACP tool_call_update.content REPLACES the call's content, so a non-diff result would
// clobber the pending new-file diff.
// A create has no prior content, yet the completed replacement view must
// remain a diff instead of clobbering the pending new-file diff with text.
const { ctx } = await setup()
const session = { header: {} }
const result = await call(ctx, 'write', { file_path: 'new.txt', content: 'fresh\n' }, { session })