Merge origin/master into codex/provider-retry-policy
This commit is contained in:
@@ -29,7 +29,7 @@ The `initialize` handshake reports a fixed server identity (`agentInfo: { name:
|
||||
| `session/new` | `ctx.agents.create({ sessionId, meta:{cwd} })` | creates a new session/agent; N concurrent sessions are allowed, keyed by id; advertises the effective command snapshot; `cwd` must be absolute (it becomes the session's workspace — see Per-session cwd); non-empty `additionalDirectories` and `mcpServers` rejected |
|
||||
| `session/load` | `ctx.agents.resume(...)` | reserves the id, verifies the persisted cwd, resumes, replays user, assistant, tool, and title events, and re-advertises commands |
|
||||
| `session/list` | `ctx.sessionQuery` | returns live-preferred newest-first sessions with absolute cwd and optional folded title; supports exact normalized cwd filtering, returns no cursor, and rejects supplied cursors |
|
||||
| `session/prompt` | `ctx.commands.execute()` or `agent.send()` | a flattened prompt beginning with `/` stays in the direct command plane; ordinary prompts support ACP `text` and `resource_link`; `dsh-session:` links and inline mentions are snapshotted through optional `ctx.sessionReferences` before enqueue; unsupported content, unavailable reference capability, failed snapshots, and empty prompts are rejected; one request is in flight per session and settles on the owning turn's end, with an error turn rejecting the RPC |
|
||||
| `session/prompt` | `ctx.commands.execute()` or `agent.followup()` | a flattened prompt beginning with `/` stays in the direct command plane; ordinary prompts support ACP `text` and `resource_link`; `dsh-session:` links and inline mentions are snapshotted through optional `ctx.sessionReferences` before enqueue; unsupported content, unavailable reference capability, failed snapshots, and empty prompts are rejected; one request is in flight per session and settles on the owning turn's end, with an error turn rejecting the RPC |
|
||||
| `session/cancel` | command `AbortSignal` or `agent.cancel()` | aborts the exact direct command, or applies the queue-aware agent cancel and settles its prompt `cancelled`; one session never cancels another |
|
||||
| `session/update` | `session/event` | streams user replay, assistant text/reasoning, retry/failure attempt markers, tool render intents, and `session_info_update` title revisions |
|
||||
| `elicitation/create` | `ctx.userInteraction.ask()` | maps `ask_user_question` questions to ACP form elicitations; option descriptions are shown in enum titles, `multi_select` uses ACP array enums, optionless requests use a required `custom` field, and a non-empty custom answer overrides any selected choice |
|
||||
|
||||
@@ -23,7 +23,7 @@ The bridge implements the **core prompt-turn loop** for N concurrent sessions: i
|
||||
| `session/load` | S | ✅ | ✅ | ✅ | Maps to `agents.resume` + full event-log replay; validates persisted `cwd` before constructing the agent. |
|
||||
| `session/resume` | S | ❌ | ✅ | ✅ | Reconnect WITHOUT replay; gated by `sessionCapabilities.resume`. Not advertised. |
|
||||
| `session/close` | S | ❌ | ✅ | ✅ | No `session/close` handler — the SDK dispatch returns `method_not_found`. The bridge tears sessions down on client disconnect / Cordis disposal (cross-cutting, see [§8](#8-cross-cutting)), but that is not the on-demand per-session method. |
|
||||
| `session/prompt` | S | ✅ | ✅ | ✅ | A flattened prompt beginning with `/` dispatches through `ctx.commands` without a model request; ordinary input maps to `agent.send`. One request is in flight per session. |
|
||||
| `session/prompt` | S | ✅ | ✅ | ✅ | A flattened prompt beginning with `/` dispatches through `ctx.commands` without a model request; ordinary input maps to `agent.followup`. One request is in flight per session. |
|
||||
| `session/cancel` | S | ✅ | ✅ | ✅ | Aborts the exact direct command, or applies queue-aware `agent.cancel` and settles its prompt `cancelled`, scoped to one session. |
|
||||
| `session/set_mode` | S | ✅ | ✅ | ✅ | Composed opportunistically: with `@deepseek-ai/dsh-plan-mode` mounted, `session/new`/`session/load` advertise the fixed `default` / `plan` projection and `session/set_mode` records the boolean pending intent (optimistic `current_mode_update`; logged `plan/mode` lands at the turn boundary). Without the plugin: no `modes` advertised, `set_mode` rejected (see [§6 Modes](#6-session-modes--config-options--models)). |
|
||||
| `session/set_config_option` | S | ✅ | ✅ | ✅ | A provider/model select is present for a complete registered target; one `permission` select is added when `ctx.permission` is composed. Every response carries the complete refreshed state. |
|
||||
|
||||
@@ -1056,7 +1056,7 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
}
|
||||
const { text } = referencedPrompt
|
||||
let preparedContent: ContentBlock[] = [{ type: 'text', text }]
|
||||
let preparedContexts: NonNullable<Parameters<Agent['send']>[1]>['contexts'] = []
|
||||
let preparedContexts: NonNullable<Parameters<Agent['followup']>[1]>['contexts'] = []
|
||||
if (referencedPrompt.references.length > 0) {
|
||||
const sessionReferences = ctx.get('sessionReferences')
|
||||
if (sessionReferences === undefined) {
|
||||
@@ -1081,14 +1081,14 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
}
|
||||
assertOpen()
|
||||
}
|
||||
// Install the in-flight slot BEFORE send() (send does not synchronously
|
||||
// Install the in-flight slot BEFORE followup() (followup does not synchronously
|
||||
// flip status to running; the session/event listener records the turn
|
||||
// number and settle/rejects it). Capture the log length now as the
|
||||
// A turn that ends in error rejects this promise (the codec never
|
||||
// produces an error stop reason).
|
||||
const stopReason = await new Promise<StopReason>((resolve, reject) => {
|
||||
rec.inflight = { resolve, reject, turn: undefined }
|
||||
rec.agent.send(preparedContent, { contexts: preparedContexts })
|
||||
rec.agent.followup(preparedContent, { contexts: preparedContexts })
|
||||
})
|
||||
return { stopReason }
|
||||
},
|
||||
@@ -1333,8 +1333,8 @@ function validateMcpServers(params: { mcpServers?: unknown[] }): void {
|
||||
* generic fallback (title = tool name, raw args as input) when no registry is
|
||||
* available (e.g. pure translator tests).
|
||||
*
|
||||
* Other event types (turn/step boundaries, context/message, …) produce
|
||||
* no client update.
|
||||
* Other event types (turn/step boundaries, injected-context user messages, …)
|
||||
* produce no client update.
|
||||
* @param sessionId - the ACP session id stamped on every emitted notification.
|
||||
* @param event - the harness session event to translate.
|
||||
* @param notify - sink for each produced `session/update` notification; called
|
||||
@@ -1375,6 +1375,9 @@ export function streamSessionEventUpdate(
|
||||
}
|
||||
case 'user/message': {
|
||||
if (!includeUserMessages) return
|
||||
// Only a direct human prompt replays as a user message; injected context
|
||||
// (plugin/goal source) is not the user's turn and produces no update.
|
||||
if (event.data.source.kind !== 'user') return
|
||||
// Replay the user's prompt so a loaded session shows both sides of each
|
||||
// turn. Live prompt turns suppress this path to avoid duplicating what
|
||||
// the client just sent.
|
||||
@@ -1421,7 +1424,7 @@ export function streamSessionEventUpdate(
|
||||
notify({ sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } } })
|
||||
return
|
||||
}
|
||||
// non-error turn/step boundaries, context/message, steering,
|
||||
// non-error turn/step boundaries, injected-context user messages, steering,
|
||||
// assistant/message — no direct ACP client update.
|
||||
default:
|
||||
return
|
||||
|
||||
@@ -383,7 +383,7 @@ describe('acp bridge', () => {
|
||||
},
|
||||
}],
|
||||
})
|
||||
expect(target.events.some(event => event.type === 'context/message')).toBe(false)
|
||||
expect(target.events.some(event => event.type === 'user/message' && event.data.source.kind !== 'user')).toBe(false)
|
||||
const request = JSON.stringify(harness.adapter.requests[0]?.messages)
|
||||
expect(request).toContain('untrusted, read-only snapshot')
|
||||
expect(request).toContain('source background')
|
||||
|
||||
@@ -264,7 +264,7 @@ describe('acp bridge — disposal & HMR safety', () => {
|
||||
const handle = await harness.ctx.agents.create({
|
||||
sessionId: SessionId('guard-a'), agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
handle.agent.send([{ type: 'text', text: 'go' }])
|
||||
handle.agent.followup([{ type: 'text', text: 'go' }])
|
||||
await handle.agent.whenIdle()
|
||||
expect(harness.ctx.sessions.get(SessionId('guard-a'))).toBeDefined()
|
||||
|
||||
@@ -288,7 +288,7 @@ describe('acp bridge — disposal & HMR safety', () => {
|
||||
// Drive a turn that hangs in the model stream, so the loop is mid-turn when
|
||||
// disposed — its exit runs a final session/flush we can gate to hold the
|
||||
// teardown observably in-flight.
|
||||
handle.agent.send([{ type: 'text', text: 'go' }])
|
||||
handle.agent.followup([{ type: 'text', text: 'go' }])
|
||||
await new Promise(r => setTimeout(r, 30))
|
||||
expect(handle.agent.status).toBe('running')
|
||||
let releaseFlush!: () => void
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('acp bridge — demux & config edges', () => {
|
||||
const before = harness.updates.length
|
||||
|
||||
const { agent: foreign } = await harness.ctx.agents.create({ sessionId: SessionId('foreign-session'), agentOptions: { provider: 'mock', model: 'mock' } })
|
||||
foreign.send([{ type: 'text', text: 'hi' }])
|
||||
foreign.followup([{ type: 'text', text: 'hi' }])
|
||||
await foreign.whenIdle()
|
||||
await new Promise(r => setTimeout(r, 10))
|
||||
|
||||
|
||||
@@ -285,10 +285,10 @@ describe('acp bridge — turn outcomes', () => {
|
||||
const sessionId = await newSession(harness)
|
||||
const agent = harness.ctx.agents.get(SessionId(sessionId))!
|
||||
// On the queued prompt, synchronously inject a one-shot context turn (idle
|
||||
// inject writes turn/start{injection} → context/message → turn/end). Fire
|
||||
// inject writes turn/start{injection} → user/message → turn/end). Fire
|
||||
// once so it lands between install and the prompt turn.
|
||||
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: 'ctx note' }], { source: { kind: 'plugin', plugin: 'test' } })
|
||||
|
||||
Reference in New Issue
Block a user