Merge branch 'codex/simp-unify-agent-session-id' into codex/simp-ui-identity-residue

# Conflicts:
#	AGENTS.md
#	docs/config-catalog.md
#	docs/event-producer-consumer.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/cancel/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-turn/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/config-options/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/error-finish/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-approved/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-rejected/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/fs-edit/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/fs-policy-reject/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/fs-read-window/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/fs-read/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/fs-terminal-card/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/fs-write-overwrite/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/fs-write/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/handshake/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-posttool-block/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-posttool-context/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-block/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-stop-continue/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-posttool-block/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-posttool-context/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-pretool-block/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-block/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-stop-continue/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/multi-turn/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/permission-switching/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/repeat-tool-guard/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/subagent-fork/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/subagent-mixed/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/subagent-multi/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/subagent-spawn/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/todo-plan/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/tool-call-turn/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/workflow-run/stdout.golden.jsonl
#	examples/acp-agent/tests/snapshots/workspace-edit/stdout.golden.jsonl
#	packages/ui/acp/README.md
#	packages/ui/acp/src/index.ts
#	packages/ui/jsonrpc/README.md
#	packages/ui/jsonrpc/src/server.ts
This commit is contained in:
Tianyi Cui
2026-07-18 12:25:33 +08:00
657 changed files with 36684 additions and 7561 deletions

View File

@@ -4,7 +4,7 @@ The **SDK server plugin** (`jsonrpc`): mounting it serves a stdio JSON-RPC serve
## Wiring
`inject: ['agents']` — the server creates one agent per SDK `sessionId` (get-or-create on `session/prompt`). A local subagent's shared agent/session id supplies `subagent.finished.childSessionId` directly; the server recognizes a live child through either exact delegating-parent runtime ownership or matching durable `parentSession` lineage, then counts local starts by provider/id and that parent carrier because a continuation may reuse one child and the child may be disposed before a later `subagent/end`. The paired event carrier preserves parent correlation even when reused ids settle out of order. Runs from remote providers are not reported even when their parent-scoped run id collides with an unrelated local agent. The LLM seam is read opportunistically via `ctx.get('llm')` (not injected): when `initialize.model` has no registered adapter, the plugin mounts `dsh-llm-deepseek` for it (credentials from `$DEEPSEEK_API_KEY` / `$DEEPSEEK_BASE_URL`) — a config-registered adapter for the model wins. Everything else — persistence, the tool stacks, the adapter set — comes from the surrounding `cordis.yml`.
`inject: ['agents']`. The server gets or creates one agent per `sessionId` from the `initialize.provider`/`initialize.model` pair. It classifies subagent completions through live parent ownership or durable parent lineage and retains parent-scoped provider/id counts after child disposal. A registered owner for the provider route wins; an unowned `deepseek` route mounts `dsh-llm-deepseek` using `$DEEPSEEK_API_KEY` and `$DEEPSEEK_BASE_URL`, while any other unowned provider fails initialization. Persistence, tools, and other adapters come from the surrounding `cordis.yml`.
## Config

View File

@@ -27,6 +27,8 @@ import type { JsonRpcTransportPeer } from './transport.ts'
export interface InitializeParams {
/** Working directory recorded on every SDK-created session's header. */
cwd: string
/** Provider route every SDK-created agent runs on. */
provider: string
/** Model name every SDK-created agent runs on (see {@link HarnessSdkServer.initialize} for adapter fallback). */
model: string
}
@@ -85,6 +87,7 @@ function isLocalChild(ctx: Context, id: SessionId, parent: Agent): boolean {
*/
export class HarnessSdkServer {
private cwd = process.cwd()
private provider = 'deepseek'
private model = 'deepseek'
private llmFiber: { dispose(): Promise<void> } | undefined
private readonly sessions = new Map<string, SessionRecord>()
@@ -156,18 +159,18 @@ export class HarnessSdkServer {
}
/**
* Handle `initialize`: record the SDK deployment facts (cwd, model) and, when
* no registered adapter serves `params.model`, mount the DeepSeek adapter for
* it (credentials from `$DEEPSEEK_API_KEY`/`$DEEPSEEK_BASE_URL`) — a config
* that already registered an adapter for the model wins.
* Record cwd and provider/model, mounting the DeepSeek adapter only when the
* `deepseek` provider route has no configured owner.
* @param params - the SDK handshake parameters.
* @returns the server identity for the handshake.
*/
async initialize(params: InitializeParams): Promise<InitializeResult> {
this.cwd = resolve(params.cwd)
this.provider = params.provider
this.model = params.model
if (!this.llmFiber && !this.hasAdapterFor(this.model)) {
this.llmFiber = await this.ctx.plugin(LlmDeepSeek, { models: [this.model] })
if (!this.hasAdapterFor(this.provider)) {
if (this.provider !== 'deepseek') throw new Error(`no adapter registered for provider "${this.provider}"`)
this.llmFiber = await this.ctx.plugin(LlmDeepSeek, {})
}
return { serverInfo: { name: 'deepseek-harness-sdk-runtime', version: '0.0.1' } }
}
@@ -281,7 +284,7 @@ export class HarnessSdkServer {
const handle = await this.ctx.agents.create({
sessionId: SessionId(sessionId),
meta: { cwd: this.cwd },
agentOptions: { model: this.model },
agentOptions: { provider: this.provider, model: this.model },
})
const rec: SessionRecord = { handle, lastTurnEnd: undefined, activePrompt: false }
this.sessions.set(sessionId, rec)
@@ -293,7 +296,7 @@ export class HarnessSdkServer {
return reason.kind === 'completed' ? 'ok' : 'error'
}
private hasAdapterFor(model: string): boolean {
return this.ctx.get('llm')?.models().includes(model) ?? false
private hasAdapterFor(provider: string): boolean {
return this.ctx.get('llm')?.listProviders().some(entry => entry.id === provider) ?? false
}
}

View File

@@ -59,7 +59,7 @@ async function mountPlugin(
options: { writeDelayMs?: number; failFlush?: boolean } = {},
): Promise<ApplyHarness> {
const ctx = new Context()
await ctx.plugin(agentCore)
await ctx.plugin(agentCore, { workspaceContext: false })
await ctx.plugin(SessionPersistenceJsonl, { root: storageDir })
await new Promise(resolve => setTimeout(resolve, 50))
@@ -153,7 +153,7 @@ describe('dsh-jsonrpc plugin apply', () => {
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
const harness = await mountPlugin(storageDir)
try {
harness.send({ jsonrpc: '2.0', id: 'init-1', method: 'initialize', params: { cwd: storageDir, model: 'apply-model' } })
harness.send({ jsonrpc: '2.0', id: 'init-1', method: 'initialize', params: { cwd: storageDir, provider: 'deepseek', model: 'apply-model' } })
const response = await harness.waitForFrame(frame => frame.id === 'init-1', 'initialize response')
expect(response).toEqual({
@@ -175,7 +175,7 @@ describe('dsh-jsonrpc plugin apply', () => {
vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url)
const harness = await mountPlugin(storageDir)
try {
harness.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { cwd: storageDir, model: 'dsagent-model' } })
harness.send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { cwd: storageDir, provider: 'deepseek', model: 'dsagent-model' } })
await harness.waitForFrame(frame => frame.id === 1, 'initialize response')
harness.send({
@@ -236,7 +236,7 @@ describe('dsh-jsonrpc plugin apply', () => {
expect(harness.exits()).toEqual([0])
const before = harness.frames().length
harness.send({ jsonrpc: '2.0', id: 'after-exit', method: 'initialize', params: { cwd: storageDir, model: 'x' } })
harness.send({ jsonrpc: '2.0', id: 'after-exit', method: 'initialize', params: { cwd: storageDir, provider: 'deepseek', model: 'x' } })
await settle()
expect(harness.frames().length).toBe(before)
} finally {
@@ -257,7 +257,7 @@ describe('dsh-jsonrpc plugin apply', () => {
expect(harness.outputErrors.map(error => error.message)).toEqual(['flush callback failed'])
const before = harness.frames().length
harness.send({ jsonrpc: '2.0', id: 'after-flush-failure', method: 'initialize', params: { cwd: storageDir, model: 'x' } })
harness.send({ jsonrpc: '2.0', id: 'after-flush-failure', method: 'initialize', params: { cwd: storageDir, provider: 'deepseek', model: 'x' } })
await settle()
expect(harness.frames().length).toBe(before)
} finally {
@@ -281,7 +281,7 @@ describe('dsh-jsonrpc plugin apply', () => {
await harness.fiber.dispose()
const before = harness.frames().length
harness.send({ jsonrpc: '2.0', id: 'probe-2', method: 'initialize', params: { cwd: storageDir, model: 'x' } })
harness.send({ jsonrpc: '2.0', id: 'probe-2', method: 'initialize', params: { cwd: storageDir, provider: 'deepseek', model: 'x' } })
await settle()
expect(harness.frames().length).toBe(before)
expect(harness.exits()).toEqual([])

View File

@@ -59,7 +59,7 @@ async function mockCompletionServer(): Promise<{ url: string; requests: unknown[
async function makeHarness(storageDir: string) {
const ctx = new Context()
await ctx.plugin(agentCore)
await ctx.plugin(agentCore, { workspaceContext: false })
await ctx.plugin(SubagentService)
await ctx.plugin(SessionPersistenceJsonl, { root: storageDir })
await new Promise(resolve => setTimeout(resolve, 50))
@@ -118,6 +118,7 @@ describe('HarnessSdkServer', () => {
const init = await server.handleRequest('initialize', {
cwd: storageDir,
provider: 'deepseek',
model: 'dsagent-model',
}) as { serverInfo: { name: string } }
expect(init.serverInfo.name).toBe('deepseek-harness-sdk-runtime')
@@ -148,7 +149,7 @@ describe('HarnessSdkServer', () => {
const orphanHandle = await ctx.agents.create({
sessionId: SessionId('orphan-session'),
meta: { cwd: storageDir },
agentOptions: { model: 'dsagent-model' },
agentOptions: { provider: 'deepseek', model: 'dsagent-model' },
})
orphanHandle.agent.send([{ type: 'text', text: 'outside the sdk session map' }])
await orphanHandle.agent.whenIdle()
@@ -251,7 +252,7 @@ describe('HarnessSdkServer', () => {
try {
const server = new HarnessSdkServer(ctx, new FakeTransport())
await server.initialize({ cwd: storageDir, model: 'plain-model' })
await server.initialize({ cwd: storageDir, provider: 'deepseek', model: 'plain-model' })
await server.prompt({
sessionId: 'plain',
contentBlocks: [{ type: 'text', text: 'hello' }],
@@ -275,14 +276,14 @@ describe('HarnessSdkServer', () => {
const parentHandle = await ctx.agents.create({
sessionId: SessionId('main'),
meta: { cwd: storageDir },
agentOptions: { model: 'deepseek' },
agentOptions: { provider: 'deepseek', model: 'deepseek' },
})
// A custom in-process provider may own its child at the provider/root
// scope while preserving durable parent lineage.
const handle = await ctx.agents.create({
sessionId: SessionId('child-session'),
meta: { cwd: storageDir, parentSession: SessionId('main') },
agentOptions: { model: 'deepseek' },
agentOptions: { provider: 'deepseek', model: 'deepseek' },
})
expect(ctx.agents.roots()).toContain(handle.agent)
const parentlessHandle = await parentHandle.agent.ctx.agents.create({
@@ -521,17 +522,17 @@ describe('HarnessSdkServer', () => {
parentHandle = await ctx.agents.create({
sessionId: SessionId('fallback-parent'),
meta: { cwd: storageDir },
agentOptions: { model: 'deepseek' },
agentOptions: { provider: 'deepseek', model: 'deepseek' },
})
handle = await parentHandle.agent.ctx.agents.create({
sessionId: SessionId('fallback-child-session'),
meta: { cwd: storageDir, parentSession: SessionId('fallback-parent') },
agentOptions: { model: 'deepseek' },
agentOptions: { provider: 'deepseek', model: 'deepseek' },
})
failedHandle = await parentHandle.agent.ctx.agents.create({
sessionId: SessionId('failed-child-session'),
meta: { cwd: storageDir },
agentOptions: { model: 'deepseek' },
agentOptions: { provider: 'deepseek', model: 'deepseek' },
})
const missedStartResult = Promise.withResolvers<SubagentResult>()
const disposeMissedStartProvider = ctx.subagents.registerProvider({
@@ -616,20 +617,20 @@ describe('HarnessSdkServer', () => {
}
})
it('does not re-register an LLM adapter that already exists', async () => {
it('does not re-register an LLM adapter whose provider already has an owner', async () => {
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-existing-llm-'))
const ctx = await makeHarness(storageDir)
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
await ctx.plugin(LlmDeepSeek, { models: ['preinstalled-model'] })
await ctx.plugin(LlmDeepSeek)
try {
const server = new HarnessSdkServer(ctx, new FakeTransport())
const inspect = server as unknown as { hasAdapterFor(model: string): boolean }
const inspect = server as unknown as { hasAdapterFor(provider: string): boolean }
expect(inspect.hasAdapterFor('preinstalled-model')).toBe(true)
expect(inspect.hasAdapterFor('missing-model')).toBe(false)
await server.initialize({ cwd: storageDir, model: 'preinstalled-model' })
expect(inspect.hasAdapterFor('deepseek')).toBe(true)
expect(inspect.hasAdapterFor('missing-provider')).toBe(false)
await server.initialize({ cwd: storageDir, provider: 'deepseek', model: 'preinstalled-model' })
expect(ctx.get('llm')?.models().filter(model => model === 'preinstalled-model')).toEqual(['preinstalled-model'])
expect(ctx.get('llm')?.listProviders().filter(provider => provider.id === 'deepseek')).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
await server.shutdown()
} finally {
await ctx.fiber.dispose()
@@ -637,17 +638,18 @@ describe('HarnessSdkServer', () => {
}
})
it('registers a missing model when an LLM service already exists', async () => {
it('rejects a missing non-DeepSeek provider when an LLM service already exists', async () => {
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-new-llm-'))
const ctx = await makeHarness(storageDir)
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
await ctx.plugin(LlmDeepSeek, { models: ['other-model'] })
await ctx.plugin(LlmDeepSeek)
try {
const server = new HarnessSdkServer(ctx, new FakeTransport())
await server.initialize({ cwd: storageDir, model: 'new-model' })
await expect(server.initialize({ cwd: storageDir, provider: 'private', model: 'new-model' }))
.rejects.toThrow('no adapter registered for provider "private"')
expect(ctx.get('llm')?.models()).toEqual(expect.arrayContaining(['other-model', 'new-model']))
expect(ctx.get('llm')?.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
await server.shutdown()
} finally {
await ctx.fiber.dispose()
@@ -748,15 +750,15 @@ describe('HarnessSdkServer', () => {
const ctx = {
on: vi.fn(() => () => undefined),
agents: { create, get: () => undefined },
get: () => ({ models: () => ['model'] }),
get: () => ({ listProviders: () => [{ id: 'mock', name: 'Mock' }] }),
} as unknown as Context
const server = new HarnessSdkServer(ctx, new FakeTransport()) as unknown as {
initialize(params: { cwd: string; model: string }): Promise<unknown>
initialize(params: { cwd: string; provider: string; model: string }): Promise<unknown>
getOrCreateSession(sessionId: string): Promise<unknown>
shutdown(): Promise<Record<string, never>>
}
await server.initialize({ cwd: '.', model: 'model' })
await server.initialize({ cwd: '.', provider: 'mock', model: 'model' })
await server.getOrCreateSession('relative')
expect(create).toHaveBeenCalledWith(expect.objectContaining({ meta: { cwd: process.cwd() } }))