refactor(acp): trim unreachable bridge surface (branding knobs, kind-sniffing fallback)

Two pieces of dsh-acp surface were unreachable from any shipped config:

- AcpConfig.agentName/agentVersion: the app package hands the bridge only
  { model, systemPrompt }, so no leaf cordis.yml could set them; they were
  settable only by direct-mounting the bridge (a unit test). Hardcode
  agentInfo at the initialize site and delete the fields, their schema
  defaults, the ?? fallbacks, and the TODO(double-default) whose subject
  vanishes. The handshake wire value is unchanged (all snapshot initialize
  lines byte-identical).

- The toolKindFor name heuristic special-cased bash*/read*/write/edit*
  names in the generic-fallback path, violating the bridge's own design
  rule ("the bridge never special-cases tool names"). Every first-party
  tool ships its kind via presentCall; the fallback now renders the
  neutral kind 'other'. The fallback is reachable when a presentCall
  throws OR when model args fail the tool schema (defineTool's presentCall
  wrapper returns undefined on violations) — the latter shows up in one
  committed golden (hook-codex-posttool-block: three bash calls missing
  the required description), whose kind cells flip execute->other. That
  3-line golden refresh is the whole transcript delta.

The empty-arguments branch of parseToolArguments lost its only exercise
with the deleted heuristic test; it is live behavior (JSON.parse('')
throws, so the guard is what renders a zero-arg call as rawInput {}), so
it gets a dedicated pin instead of deletion.

RFC moved to docs/rfc/implemented/simplification/ and amended to shipped
reality: fallback reachability includes schema-invalid args, and the
golden churn is exactly the three kind cells (the original zero-churn
claim held only for the branding half).
This commit is contained in:
Tianyi Cui
2026-07-04 15:46:11 +08:00
parent 226a8b5e4c
commit 4036300353
10 changed files with 62 additions and 80 deletions
+4 -5
View File
@@ -33,7 +33,7 @@ describe('acp bridge', () => {
expect(res.protocolVersion).toBe(PROTOCOL_VERSION)
expect(res.agentCapabilities?.loadSession).toBe(true)
expect(res.agentCapabilities?.promptCapabilities).toMatchObject({ image: false, audio: false })
expect(res.agentInfo?.name).toBe('deepseek-harness-acp')
expect(res.agentInfo).toEqual({ name: 'deepseek-harness-acp', version: '0.0.1' })
})
it('session/new creates a session and a full prompt turn streams text then settles end_turn', async () => {
@@ -148,14 +148,13 @@ describe('acp bridge', () => {
await expect(harness.client.authenticate({ methodId: 'whatever' })).resolves.toBeDefined()
})
it('honors agentName/agentVersion/systemPrompt config', async () => {
it('honors systemPrompt config', async () => {
harness = await makeBridgeHarness({
storageDir,
script: [textResponse('ok')],
config: { agentName: 'custom-agent', agentVersion: '9.9.9', systemPrompt: 'be terse' },
config: { systemPrompt: 'be terse' },
})
const res = await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
expect(res.agentInfo).toMatchObject({ name: 'custom-agent', version: '9.9.9' })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
// Create + prompt so the systemPrompt config flows through agentOptions and
// reaches the model request.
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
+13 -11
View File
@@ -50,32 +50,34 @@ describe('streamSessionEventUpdate', () => {
.toEqual([])
})
it('maps tool/call to an in_progress tool_call with inferred kind and parsed rawInput (generic fallback, no presenter)', () => {
it('maps tool/call to an in_progress tool_call with kind other and parsed rawInput (generic fallback, no presenter)', () => {
const updates = updatesFor(evt('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'bash', arguments: '{"command":"ls"}' }))
expect(updates).toEqual([{
sessionUpdate: 'tool_call',
toolCallId: 'c1',
title: 'bash',
kind: 'execute',
// The fallback never sniffs a kind from the tool name — even a name a
// first-party tool uses (`bash`) renders `other`; kinds are tool-owned
// via presentCall.
kind: 'other',
status: 'in_progress',
rawInput: { command: 'ls' },
}])
})
it('infers tool kinds: read*/write*/edit*/other', () => {
const kind = (name: string): unknown =>
updatesFor(evt('tool/call', { turn: 1, step: 1, callId: CallId('c'), name, arguments: '' }))[0]
expect((kind('read_file') as { kind: string }).kind).toBe('read')
expect((kind('write') as { kind: string }).kind).toBe('edit')
expect((kind('edit_file') as { kind: string }).kind).toBe('edit')
expect((kind('frobnicate') as { kind: string }).kind).toBe('other')
})
it('falls back to the raw argument string when tool arguments are not JSON', () => {
const update = updatesFor(evt('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'bash', arguments: 'not json' }))[0]
expect((update as { rawInput: unknown }).rawInput).toBe('not json')
})
it('parses EMPTY tool arguments to an empty-object rawInput (a zero-arg call, not the raw-string fallback)', () => {
// `JSON.parse('')` throws, so without the empty-string guard a zero-arg
// call would render `rawInput: ''` via the non-JSON fallback; the guard
// normalizes it to `{}`.
const update = updatesFor(evt('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'noop', arguments: '' }))[0]
expect((update as { rawInput: unknown }).rawInput).toEqual({})
})
it('maps tool/result to completed/failed tool_call_update with text content', () => {
const ok = updatesFor(evt('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'out' }], isError: false }))
expect(ok).toEqual([{
+4 -1
View File
@@ -66,7 +66,10 @@ describe('acp bridge — turn outcomes', () => {
const toolCalls = harness.updates.filter(u => u.sessionUpdate === 'tool_call')
const toolUpdates = harness.updates.filter(u => u.sessionUpdate === 'tool_call_update')
expect(toolCalls).toHaveLength(1)
expect(toolCalls[0]).toMatchObject({ toolCallId: 'c1', title: 'bash', kind: 'execute', status: 'in_progress' })
// The inline stand-in declares no presentCall, so the generic fallback
// renders kind `other` (kinds are tool-owned; the bridge never sniffs the
// name — the REAL dsh-tool-bash test below covers the execute card).
expect(toolCalls[0]).toMatchObject({ toolCallId: 'c1', title: 'bash', kind: 'other', status: 'in_progress' })
expect(toolUpdates).toHaveLength(1)
expect(toolUpdates[0]).toMatchObject({ toolCallId: 'c1', status: 'completed' })