Merge branch 'codex/simp-ui-identity-residue' into codex/simp-hide-concrete-agent-loop

This commit is contained in:
Tianyi Cui
2026-07-14 07:30:06 +08:00
15 changed files with 141 additions and 66 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 caches only parent lineage because the child may be disposed before `subagent/end`. Runs from remote providers are not reported through this local-session notification pair because they create no local `session/created`/`subagent.started` edge. 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 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 caches runtime-local identity plus optional parent lineage because the child may be disposed before `subagent/end`, and the provider contract does not require lineage. Runs from remote providers are not reported because they create no 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`.
## Config

View File

@@ -58,6 +58,11 @@ interface SessionRecord {
activePrompt: boolean
}
/** Runtime-local agent identity plus optional durable fork lineage. */
interface LocalAgentRecord {
parentSessionId?: SessionId
}
/**
* The SDK server over a booted harness context. Constructing it subscribes to
* the context's `session/event`, `session/created`, `agent/created`, and
@@ -71,7 +76,7 @@ export class HarnessSdkServer {
private llmFiber: { dispose(): Promise<void> } | undefined
private readonly sessions = new Map<string, SessionRecord>()
private readonly sessionCreations = new Map<string, Promise<SessionRecord>>()
private readonly subagentParents = new Map<SessionId, SessionId>()
private readonly localAgents = new Map<SessionId, LocalAgentRecord>()
private readonly disposers: (() => void)[] = []
private shutdownTask: Promise<Record<string, never>> | undefined
private shuttingDown = false
@@ -95,23 +100,24 @@ export class HarnessSdkServer {
childSessionId: String(session.id),
})
}))
// Cache parent lineage on creation: by the time `subagent/end` fires the
// child agent may already be disposed and gone from the registry. The child
// session id needs no cache because it is the shared agent/session id.
// Cache runtime-local identity and optional lineage on creation: by the
// time `subagent/end` fires the child agent may already be disposed and
// gone from the registry. Parent lineage is not required by the provider
// contract, so an empty record remains a load-bearing locality marker.
this.disposers.push(ctx.on('agent/created', (agent) => {
const parentSessionId = agent.session.header.parentSession
if (parentSessionId !== undefined) this.subagentParents.set(agent.id, parentSessionId)
this.localAgents.set(agent.id, parentSessionId === undefined ? {} : { parentSessionId })
}))
this.disposers.push(ctx.on('subagent/end', (info: SubagentRunEndInfo) => {
const agent = this.ctx.agents.get(info.id)
const cachedParentSessionId = this.subagentParents.get(info.id)
this.subagentParents.delete(info.id)
// This protocol reports LOCAL child sessions, paired with the
// session/created-driven subagent.started notification above. A remote
// provider may use a real remote SessionId for its run, but that session
// does not exist in this harness and therefore has no paired start event.
if (cachedParentSessionId === undefined && agent === undefined) return
const parentSessionId = cachedParentSessionId ?? agent?.session.header.parentSession
const cachedLocalAgent = this.localAgents.get(info.id)
this.localAgents.delete(info.id)
// This protocol reports LOCAL child sessions. A lineage-bearing child
// has the session/created-driven start notification above; a parentless
// local provider still gets its terminal notification. A remote provider
// has neither a cached creation nor a live local agent and is ignored.
if (cachedLocalAgent === undefined && agent === undefined) return
const parentSessionId = cachedLocalAgent?.parentSessionId ?? agent?.session.header.parentSession
this.transport.notify('subagent.finished', {
provider: info.provider,
agentId: String(info.id),
@@ -189,7 +195,7 @@ export class HarnessSdkServer {
this.sessionCreations.clear()
const records = [...this.sessions.values()]
this.sessions.clear()
this.subagentParents.clear()
this.localAgents.clear()
const failures: unknown[] = []
while (this.disposers.length > 0) {
try {

View File

@@ -272,15 +272,26 @@ describe('HarnessSdkServer', () => {
meta: { cwd: storageDir, parentSession: SessionId('main') },
agentOptions: { model: 'deepseek' },
})
const parentlessHandle = await ctx.agents.create({
sessionId: SessionId('parentless-child-session'),
meta: { cwd: storageDir },
agentOptions: { model: 'deepseek' },
})
// The backend may dispose the child before publishing its run outcome;
// only the cached parent lineage should be needed at this point.
// cached locality must survive with or without optional parent lineage.
await handle.dispose()
await parentlessHandle.dispose()
await settleSubagent(ctx, parentHandle.agent, {
provider: 'spawn',
id: SessionId('child-session'),
stopReason: 'completed',
lastAssistantMessage: [{ type: 'text', text: 'child done' }],
})
await settleSubagent(ctx, parentHandle.agent, {
provider: 'spawn',
id: SessionId('parentless-child-session'),
stopReason: 'error',
})
expect(transport.notifications).toContainEqual({
method: 'subagent.finished',
@@ -294,6 +305,16 @@ describe('HarnessSdkServer', () => {
lastAssistantMessage: [{ type: 'text', text: 'child done' }],
},
})
expect(transport.notifications).toContainEqual({
method: 'subagent.finished',
params: {
provider: 'spawn',
agentId: 'parentless-child-session',
childSessionId: 'parentless-child-session',
status: 'error',
stopReason: 'error',
},
})
await parentHandle.dispose()
await server.shutdown()