fix: keep SDK subagent notifications local
This commit is contained in:
@@ -28,7 +28,7 @@ The config-driven path keeps `agents[].id` as a stable configuration label, not
|
|||||||
|
|
||||||
- Agent create/resume and subagent creation carry one identity, and `Session` stores it in one place.
|
- Agent create/resume and subagent creation carry one identity, and `Session` stores it in one place.
|
||||||
- The creation transaction retains final-entry collision, exact-entry detach, rollback, and quiescence coverage without identity-specific lifecycle state.
|
- The creation transaction retains final-entry collision, exact-entry detach, rollback, and quiescence coverage without identity-specific lifecycle state.
|
||||||
- ACP, stdio, hooks, bash ownership, persistence, and lineage use the shared `SessionId` directly. The ACP subagent backend uses the child server's returned session id as its run id; the ACP bridge verifies exact `Agent` ownership from the forward session map; and JSON-RPC caches only disposable-child parent lineage.
|
- ACP, stdio, hooks, bash ownership, persistence, and lineage use the shared `SessionId` directly. The ACP subagent backend uses the child server's returned session id as its run id; the ACP bridge verifies exact `Agent` ownership from the forward session map; and JSON-RPC caches only local disposable-child parent lineage while leaving remote runs outside its local-session notification pair.
|
||||||
- The config-driven resume-or-create policy is explicit and covered across a durable restart.
|
- The config-driven resume-or-create policy is explicit and covered across a durable restart.
|
||||||
- A production listener search kept `agent/created`/`agent/disposed` and their publication semantics.
|
- A production listener search kept `agent/created`/`agent/disposed` and their publication semantics.
|
||||||
- Typecheck, coverage, snapshots, doc-sync, module-graph verification, build, and hygiene pass.
|
- Typecheck, coverage, snapshots, doc-sync, module-graph verification, build, and hygiene pass.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ The **SDK server plugin** (`jsonrpc`): mounting it serves a stdio JSON-RPC serve
|
|||||||
|
|
||||||
## Wiring
|
## Wiring
|
||||||
|
|
||||||
`inject: ['agents']` — the server creates one agent per SDK `sessionId` (get-or-create on `session/prompt`). A 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`. 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 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`.
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
|
|||||||
@@ -104,8 +104,14 @@ export class HarnessSdkServer {
|
|||||||
}))
|
}))
|
||||||
this.disposers.push(ctx.on('subagent/end', (info: SubagentRunEndInfo) => {
|
this.disposers.push(ctx.on('subagent/end', (info: SubagentRunEndInfo) => {
|
||||||
const agent = this.ctx.agents.get(info.id)
|
const agent = this.ctx.agents.get(info.id)
|
||||||
const parentSessionId = this.subagentParents.get(info.id) ?? agent?.session.header.parentSession
|
const cachedParentSessionId = this.subagentParents.get(info.id)
|
||||||
this.subagentParents.delete(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
|
||||||
this.transport.notify('subagent.finished', {
|
this.transport.notify('subagent.finished', {
|
||||||
provider: info.provider,
|
provider: info.provider,
|
||||||
agentId: String(info.id),
|
agentId: String(info.id),
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ describe('HarnessSdkServer', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('falls back to live lineage and treats the shared id as the child session id', async () => {
|
it('falls back to live lineage and ignores runs without a local child session', async () => {
|
||||||
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-subagent-fallback-'))
|
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-subagent-fallback-'))
|
||||||
const ctx = await makeHarness(storageDir)
|
const ctx = await makeHarness(storageDir)
|
||||||
let parentHandle: AgentHandle | undefined
|
let parentHandle: AgentHandle | undefined
|
||||||
@@ -367,16 +367,10 @@ describe('HarnessSdkServer', () => {
|
|||||||
stopReason: 'error',
|
stopReason: 'error',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(transport.notifications).toContainEqual({
|
expect(transport.notifications.some(n =>
|
||||||
method: 'subagent.finished',
|
n.method === 'subagent.finished'
|
||||||
params: {
|
&& n.params?.agentId === 'missing-child-agent',
|
||||||
provider: 'fork',
|
)).toBe(false)
|
||||||
agentId: 'missing-child-agent',
|
|
||||||
childSessionId: 'missing-child-agent',
|
|
||||||
status: 'error',
|
|
||||||
stopReason: 'error',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await server.shutdown()
|
await server.shutdown()
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user