fix: address PR #1802 review round
- listChildren reads the session store via strict ctx.get (property proxy is caller-scoped), orders candidates branchlessly, narrows the cold-read return type, and pins the cost model and store/registry composition gaps with tests; per-file coverage restored - acp-agent and headless-agent compositions mount session-projection; a keyless snapshot pins the descriptor-less diagnostic row - api-proxy cold spec pins header-origin ownership and the legacy descriptor-only opt-out - design note ships as implemented with its English pairing; companion notes and core-data-structures pages synced
This commit is contained in:
@@ -80,6 +80,12 @@
|
||||
maxTokens: 8192
|
||||
compactionRetries: 1
|
||||
|
||||
# Projection registry: subagent catalog identity (mode/label) folds through
|
||||
# its registered units; the catalog surfaces (`list_agents`, subagent listing)
|
||||
# fail loud without the capability.
|
||||
- id: session-projection
|
||||
name: '@deepseek-ai/dsh-session-projection'
|
||||
|
||||
# Expose fresh-child `spawn` and completed-prefix `fork` through separate tool
|
||||
# names so multi-child scenarios exercise both transports. These leaves follow
|
||||
# the app because it provides `ctx.agents` and `ctx.tools`.
|
||||
|
||||
@@ -71,6 +71,11 @@
|
||||
maxTokens: 8192
|
||||
compactionRetries: 1
|
||||
|
||||
# Projection registry: durable subagent identity (mode/label) folds through
|
||||
# its registered units; subagent catalog reads fail loud without the capability.
|
||||
- id: session-projection
|
||||
name: '@deepseek-ai/dsh-session-projection'
|
||||
|
||||
# Expose fresh-child `spawn` and completed-prefix `fork` through independent
|
||||
# in-process backends.
|
||||
- id: subagent
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Keyless real-Loader composition for the descriptor-less cold-child
|
||||
# diagnostic snapshot. The seeded parent owns one session-backed child whose
|
||||
# log carries `origin: 'subagent'` but no descriptor event, so the projection
|
||||
# fold produces no identity and `list_agents` must surface the child as a
|
||||
# `[diagnostic: corrupt]` row instead of silently dropping it.
|
||||
|
||||
- id: persistence
|
||||
name: '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
config:
|
||||
root: './.sessions'
|
||||
compression: none
|
||||
|
||||
# file/override both default to their DSH_SNAPSHOT_* env vars.
|
||||
- id: replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
|
||||
# This scenario probes the subagent catalog only, so the bash/filesystem
|
||||
# stacks are absent; the bundle must opt out of the tools that would wait
|
||||
# forever for executors this tree never mounts.
|
||||
- id: agent
|
||||
name: '@deepseek-ai/dsh-agent-spine-demo'
|
||||
config:
|
||||
agents: []
|
||||
workspaceContext: false
|
||||
skills:
|
||||
enabled: false
|
||||
toolBash: false
|
||||
toolTasks: false
|
||||
goals: false
|
||||
|
||||
# Projection registry: the cold child's identity fold runs through it; the
|
||||
# catalog read fails loud when the capability is absent.
|
||||
- id: session-projection
|
||||
name: '@deepseek-ai/dsh-session-projection'
|
||||
|
||||
- id: subagent
|
||||
name: '@deepseek-ai/dsh-subagent'
|
||||
|
||||
- id: tool-subagent-list-agents
|
||||
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
||||
|
||||
# Await the persisted resume before the headless driver inspects root agents.
|
||||
- id: resumed-agent
|
||||
name: './tests/fixtures/subagent-diagnostic-agent.ts'
|
||||
26
examples/headless-agent/tests/fixtures/subagent-diagnostic-agent.ts
vendored
Normal file
26
examples/headless-agent/tests/fixtures/subagent-diagnostic-agent.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Loader fixture that resumes the seeded diagnostic-scenario parent before
|
||||
* CLI dispatch, so `list_agents` runs against its pre-seeded cold child.
|
||||
* @module subagent-diagnostic-agent
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** Fixture plugin name. */
|
||||
export const name = 'subagent-diagnostic-agent'
|
||||
/** Services that must exist before the fixture resumes its agent. */
|
||||
export const inject = ['agents', 'agentLoop', 'sessionPersistence']
|
||||
|
||||
/**
|
||||
* Resume the seeded session and bind its exact handle to this fixture's lifetime.
|
||||
* @param ctx - settled agent and persistence services from the Loader tree.
|
||||
* @returns after the resumed agent is published.
|
||||
*/
|
||||
export async function apply(ctx: Context): Promise<void> {
|
||||
const handle = await ctx.agents.resume({
|
||||
resumeSessionId: 'subagent-diagnostic-parent' as SessionId,
|
||||
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
ctx.effect(() => () => handle.dispose(), 'subagent-diagnostic-agent.handle')
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":0,"cwd":"{{cwd}}","delegationDepth":0}
|
||||
{"type":"turn/start","seq":0,"time":0,"data":{"turn":1}}
|
||||
{"type":"user/message","seq":1,"time":0,"data":{"content":[{"type":"text","text":"Start a background task."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"turn/end","seq":2,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
{"type":"session/end-seed","seq":3,"time":0,"data":{}}
|
||||
{"type":"agent/inbox/spliced","seq":4,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}
|
||||
{"type":"turn/start","seq":5,"time":0,"data":{"turn":2}}
|
||||
{"type":"agent/inbox/spliced","seq":6,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","seq":7,"time":0,"data":{"turn":2,"step":1}}
|
||||
{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":9,"time":0,"data":{"title":"Start a background task.","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
|
||||
{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"tool-call-delta","index":0,"id":"list-once","name":"list_agents","argumentsDelta":"{}"}}}
|
||||
{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}}}}
|
||||
{"type":"assistant/chunk","seq":15,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":16,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":17,"time":0,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[12,13,14,15,16],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":18,"time":0,"data":{"turn":2,"step":1,"callId":"list-once","name":"list_agents","arguments":"{}"}}
|
||||
{"type":"tool/result","seq":19,"time":0,"data":{"turn":2,"step":1,"message":{"source":{"kind":"tool","callId":"list-once"},"content":[{"type":"tool-result","toolCallId":"list-once","content":[{"type":"text","text":"{{sessionId}} [diagnostic: corrupt]"}],"isError":false}],"role":"user","id":"{{sessionId}}"}},"sourceEventSeqs":[18],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":20,"time":0,"data":{"turn":2,"step":1}}
|
||||
{"type":"step/start","seq":21,"time":0,"data":{"turn":2,"step":2}}
|
||||
{"type":"assistant/chunk","seq":22,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":23,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"text-delta","index":0,"text":"The stored subagent is unreadable. PARENT_DONE"}}}
|
||||
{"type":"assistant/chunk","seq":24,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The stored subagent is unreadable. PARENT_DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":25,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}}}
|
||||
{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":27,"time":0,"data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The stored subagent is unreadable. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5}},"sourceEventSeqs":[22,23,24,25,26],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":28,"time":0,"data":{"turn":2,"step":2}}
|
||||
{"type":"turn/end","seq":29,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}
|
||||
@@ -0,0 +1 @@
|
||||
[{"kind": "chunks", "chunks": [{"type": "block-start", "index": 0, "blockType": "tool-call"}, {"type": "tool-call-delta", "index": 0, "id": "list-once", "name": "list_agents", "argumentsDelta": "{}"}, {"type": "block-end", "index": 0, "block": {"type": "tool-call", "id": "list-once", "name": "list_agents", "arguments": "{}"}}, {"type": "usage", "usage": {"inputTokens": 10, "outputTokens": 5}}, {"type": "finish", "reason": {"kind": "tool-calls"}}]}, {"kind": "chunks", "chunks": [{"type": "block-start", "index": 0, "blockType": "text"}, {"type": "text-delta", "index": 0, "text": "The stored subagent is unreadable. PARENT_DONE"}, {"type": "block-end", "index": 0, "block": {"type": "text", "text": "The stored subagent is unreadable. PARENT_DONE"}}, {"type": "usage", "usage": {"inputTokens": 10, "outputTokens": 5}}, {"type": "finish", "reason": {"kind": "stop"}}]}]
|
||||
119
examples/headless-agent/tests/subagent-diagnostic.snapshot.ts
Normal file
119
examples/headless-agent/tests/subagent-diagnostic.snapshot.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Assembled-app regression: a persisted `origin: 'subagent'` child whose log
|
||||
* carries no descriptor event is surfaced by `list_agents` as a
|
||||
* `[diagnostic: corrupt]` row instead of being silently dropped.
|
||||
*/
|
||||
|
||||
import { readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Context } from 'cordis'
|
||||
import { normalizeSessionLog, scrubRequestHeaders, type NormalizeContext } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const fixtureDir = fileURLToPath(new URL('./subagent-diagnostic-snapshots/descriptorless-child', import.meta.url))
|
||||
const replayOverride = join(fixtureDir, 'replay.override.json')
|
||||
const parentExpected = join(fixtureDir, 'parent.expected.jsonl')
|
||||
const configPath = fileURLToPath(new URL('../subagent-diagnostic.cordis.snapshot.yml', import.meta.url))
|
||||
const binScript = fileURLToPath(new URL('../../../packages/examples/cli-demo/src/bin.ts', import.meta.url))
|
||||
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||
const parentId = SessionId('subagent-diagnostic-parent')
|
||||
const childId = SessionId('subagent-diagnostic-child')
|
||||
const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
|
||||
const task = 'Call list_agents once and report what it shows.'
|
||||
|
||||
/**
|
||||
* Seed a completed parent turn plus one cold child that durably classifies
|
||||
* as a subagent (`origin`) but never appended its descriptor event — the
|
||||
* publication-window death the diagnostic row exists for.
|
||||
*/
|
||||
async function seedDescriptorlessChild(root: string, cwd: string): Promise<void> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(SessionPersistenceJsonl, { root, compression: 'none' })
|
||||
const parentMeta: SessionHeader = {
|
||||
version: SESSION_FORMAT_VERSION,
|
||||
id: parentId,
|
||||
createdAt: 1,
|
||||
cwd,
|
||||
delegationDepth: 0,
|
||||
}
|
||||
const parentEvents: SessionEvent[] = [
|
||||
{ type: 'turn/start', seq: 0, time: 10, data: { turn: 1 } },
|
||||
{ type: 'user/message', seq: 1, time: 11, data: createUserMessage({ content: [{ type: 'text', text: 'Start a background task.' }], source: { kind: 'user' } }), surfaceOp: 'append' },
|
||||
{ type: 'turn/end', seq: 2, time: 12, data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
]
|
||||
const childMeta: SessionHeader = {
|
||||
version: SESSION_FORMAT_VERSION,
|
||||
id: childId,
|
||||
createdAt: 2,
|
||||
cwd,
|
||||
parentSession: parentId,
|
||||
origin: 'subagent',
|
||||
delegationDepth: 1,
|
||||
}
|
||||
const childEvents: SessionEvent[] = [
|
||||
{ type: 'turn/start', seq: 0, time: 20, data: { turn: 1 } },
|
||||
{ type: 'turn/end', seq: 1, time: 21, data: { turn: 1, reason: { kind: 'interrupted' } } },
|
||||
]
|
||||
try {
|
||||
await ctx.sessionPersistence.create(parentMeta)
|
||||
await ctx.sessionPersistence.append(parentId, parentEvents)
|
||||
await ctx.sessionPersistence.create(childMeta)
|
||||
await ctx.sessionPersistence.append(childId, childEvents)
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
describe('descriptor-less cold child diagnostic snapshot', () => {
|
||||
it('surfaces the unreadable child as a corrupt diagnostic through the assembled headless app', async () => {
|
||||
let cwd = ''
|
||||
const result = await runLoaderSmoke({
|
||||
label: 'subagent diagnostic headless stream-json snapshot',
|
||||
tempDirPrefix: 'dsh-subagent-diag-',
|
||||
binScript,
|
||||
configPath,
|
||||
binArgs: ['--config', configPath, '--output-format', 'stream-json', task],
|
||||
tsconfigPath,
|
||||
env: {
|
||||
DSH_SNAPSHOT_FILE: replayOverride,
|
||||
DSH_SNAPSHOT_OVERRIDE: replayOverride,
|
||||
},
|
||||
prepare: async (runCwd) => {
|
||||
cwd = runCwd
|
||||
await seedDescriptorlessChild(join(runCwd, '.sessions'), runCwd)
|
||||
},
|
||||
inspect: async (runCwd) => {
|
||||
const sessionsDir = join(runCwd, '.sessions')
|
||||
const files = (await readdir(sessionsDir, { recursive: true })).filter(file => file.endsWith('.jsonl'))
|
||||
const logs = await Promise.all(files.map(async file => readFile(join(sessionsDir, file), 'utf8')))
|
||||
const parent = logs.find(content => content.includes('"subagent-diagnostic-parent"'))
|
||||
if (parent === undefined) throw new Error('missing persisted parent log')
|
||||
|
||||
// THE model-visible fact: the descriptor-less child is reported, not
|
||||
// silently dropped, and its reason is the corrupt classification.
|
||||
expect(parent).toContain(`${childId} [diagnostic: corrupt]`)
|
||||
|
||||
const context: NormalizeContext = { sessionIds: [parentId, childId], cwd }
|
||||
const normalizedParent = scrubRequestHeaders(normalizeSessionLog(parent, context))
|
||||
if (refreshing) {
|
||||
await writeFile(parentExpected, normalizedParent)
|
||||
}
|
||||
expect(normalizedParent).toBe(await readFile(parentExpected, 'utf8'))
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.stderr).toBe('')
|
||||
const records = result.stdout.trimEnd().split('\n').map(line => JSON.parse(line) as Record<string, unknown>)
|
||||
expect(records.at(-1)).toMatchObject({
|
||||
type: 'result',
|
||||
sessionId: parentId,
|
||||
output: 'The stored subagent is unreadable. PARENT_DONE',
|
||||
})
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
})
|
||||
@@ -54,6 +54,7 @@
|
||||
"@deepseek-ai/dsh-session": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-projection": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-query": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-query-sqlite": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-reference": "workspace:*",
|
||||
|
||||
Reference in New Issue
Block a user