refactor: unify agent and session identity

This commit is contained in:
Tianyi Cui
2026-07-14 01:59:21 +08:00
parent 7a33ee94be
commit 709cc7200e
105 changed files with 899 additions and 948 deletions

View File

@@ -37,8 +37,8 @@ import {
type SessionNotification,
type StopReason,
} from '@agentclientprotocol/sdk'
import { AgentId } from '@deepseek-ai/dsh-agent'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SubagentResult, SubagentRun, SubagentStartRequest, SubagentStopReason } from '@deepseek-ai/dsh-subagent'
import { buildChildEnv, disposeChildProcess, spawnFailure } from '@deepseek-ai/dsh-subagent-subprocess'
@@ -190,7 +190,7 @@ function toError(value: unknown): Error {
* @returns the ready run handle for the child subprocess.
*/
export async function startAcpRun(request: SubagentStartRequest, spec: AcpRunSpec): Promise<SubagentRun> {
const id = AgentId(randomUUID())
const id = SessionId(randomUUID())
if (request.signal.aborted) throw new Error('subagent request was aborted before the ACP child started')

View File

@@ -1,10 +1,11 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import * as Invariants from '@deepseek-ai/dsh-invariants'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
@@ -37,7 +38,7 @@ async function setup(script: Script) {
await ctx.plugin(Spawn, { providerName: 'spawn' })
await ctx.plugin(fork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' })
const parent = ctx.agentLoop.create(SessionId('parent'), { model: 'mock' })
return { ctx, parent }
}

View File

@@ -2,10 +2,11 @@ import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import * as Invariants from '@deepseek-ai/dsh-invariants'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
@@ -43,7 +44,7 @@ async function setup(script: Script) {
await ctx.plugin(SubagentService)
await ctx.plugin(fork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' })
const parent = ctx.agentLoop.create(SessionId('parent'), { model: 'mock' })
return { ctx, parent }
}

View File

@@ -9,7 +9,7 @@
import { randomUUID } from 'node:crypto'
import type { Context } from 'cordis'
import { AgentId, type Agent, type AgentOptions } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent'
import { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { assertSubagentMaxDepth } from '@deepseek-ai/dsh-subagent'
@@ -104,7 +104,7 @@ export async function startInProcessRun(
throw new SubagentDepthError(childDepth, request.maxDepth)
}
const childId = AgentId(randomUUID())
const childId = SessionId(randomUUID())
const seedLength = options.seed?.length ?? 0
const parentHeader = parent.session.header
const parentModel = parent.options.model
@@ -127,8 +127,7 @@ export async function startInProcessRun(
const flags = { cancelled: false }
const handle = await parent.ctx.agents.create({
agentId: childId,
sessionId: SessionId(randomUUID()),
sessionId: childId,
meta: {
...parentHeader.cwd !== undefined ? { cwd: parentHeader.cwd } : {},
parentSession: parentHeader.id,

View File

@@ -1,10 +1,11 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import LlmService, { CallId, type ContentBlock, type GenerateOptions } from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { ContinuationDecision } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import * as Invariants from '@deepseek-ai/dsh-invariants'
@@ -68,7 +69,7 @@ async function setup(script: Script, options: SetupOptions = {}) {
start: (request: SubagentStartRequest) => startInProcessRun(request, {}),
})
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' })
const parent = ctx.agentLoop.create(SessionId('parent'), { model: 'mock' })
return { ctx, parent, adapter, disposeProvider }
}
@@ -333,7 +334,7 @@ describe('in-process structured output', () => {
await expect(ctx.subagents.start('spawn', structuredRequest(parent, {
outputSchema: { type: 'object', oneOf: [] } as unknown as StructuredOutputSchema,
}))).rejects.toThrow(/unsupported output schema/)
expect(ctx.agents.get(AgentId('parent'))).toBeDefined()
expect(ctx.agents.get(SessionId('parent'))).toBeDefined()
})
it('a schema carrying non-JSON values fails as OutputSchemaError at the validation boundary', async () => {

View File

@@ -1,10 +1,11 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import * as Invariants from '@deepseek-ai/dsh-invariants'
import SubagentService from '@deepseek-ai/dsh-subagent'
@@ -24,7 +25,7 @@ async function setup(script: Script) {
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' })
const parent = ctx.agentLoop.create(SessionId('parent'), { model: 'mock' })
return { ctx, parent }
}

View File

@@ -3,8 +3,8 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import type { Context } from 'cordis'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { spawnHarness, waitForIdle } from './harness.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
/**
* With-key smoke for the in-process spawn backend: a REAL parent agent delegates
@@ -29,7 +29,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('spawn backend with-key smoke', (
it('a parent delegates to a child that writes a file on disk', async () => {
workdir = await mkdtemp(join(tmpdir(), 'dsh-subagent-spawn-e2e-'))
ctx = await spawnHarness(workdir)
const parent = ctx.agentLoop.create(AgentId('e2e-parent'), { model: 'deepseek-v4-flash' })
const parent = ctx.agentLoop.create(SessionId('e2e-parent'), { model: 'deepseek-v4-flash' })
parent.send([{ type: 'text', text:
'Use the subagent tool to delegate this exact task: "Use the bash tool to write the text '

View File

@@ -5,7 +5,8 @@ import LlmService from '@deepseek-ai/dsh-llm'
import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import * as Invariants from '@deepseek-ai/dsh-invariants'
@@ -36,7 +37,7 @@ async function setup(script: Script) {
await ctx.plugin(SubagentService)
await ctx.plugin(spawn, { providerName: 'spawn' })
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' })
const parent = ctx.agentLoop.create(SessionId('parent'), { model: 'mock' })
return { ctx, parent, adapter }
}
@@ -237,7 +238,6 @@ describe('dsh-subagent-spawn', () => {
const { ctx } = await setup([textResponse('x')])
// A parent WITH a cwd (config agents have none, so create one explicitly).
const parentHandle = await ctx.agents.create({
agentId: AgentId('cwd-parent'),
sessionId: SessionId('cwd-parent-session'),
meta: { cwd: '/tmp/parent-workspace' },
agentOptions: { model: 'mock' },
@@ -254,7 +254,6 @@ describe('dsh-subagent-spawn', () => {
const { ctx } = await setup([textResponse('explicit model child')])
// A parent with NO model (its own turns would need one supplied per-request).
const parentHandle = await ctx.agents.create({
agentId: AgentId('modelless-parent'),
sessionId: SessionId('modelless-parent-session'),
agentOptions: {},
})
@@ -318,7 +317,7 @@ describe('dsh-subagent-spawn', () => {
await ctx.plugin(SubagentService)
const fiber = await ctx.plugin(spawn, { providerName: 'spawn' })
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' })
const parent = ctx.agentLoop.create(SessionId('parent'), { model: 'mock' })
const controller = new AbortController()
const run = await start(ctx, 'spawn', {
prompt: [{ type: 'text', text: 'q' }],
@@ -349,7 +348,7 @@ describe('dsh-subagent-spawn', () => {
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
const fiber = await ctx.plugin(spawn, { providerName: 'spawn' })
const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' })
const parent = ctx.agentLoop.create(SessionId('parent'), { model: 'mock' })
const parentEffects = parent.ctx.fiber.getEffects().length
const published: string[] = []
ctx.on('session/created', () => void published.push('session/created'))
@@ -442,7 +441,6 @@ describe('dsh-subagent-spawn', () => {
const { ctx } = await setup([])
// A handle-owned parent we can dispose (config agents dispose with the loop fiber).
const parentHandle = await ctx.agents.create({
agentId: AgentId('doomed-parent'),
sessionId: SessionId('doomed-s'),
agentOptions: { model: 'mock' },
})
@@ -465,7 +463,6 @@ describe('dsh-subagent-spawn', () => {
it('parent disposal during the child setup transaction prevents every publication notification', async () => {
const { ctx } = await setup([])
const parentHandle = await ctx.agents.create({
agentId: AgentId('setup-race-parent'),
sessionId: SessionId('setup-race-parent-session'),
agentOptions: { model: 'mock' },
})

View File

@@ -18,7 +18,8 @@ import type { Scoped } from '@deepseek-ai/dsh-scope'
import { assertSupportedOutputSchema } from '@deepseek-ai/dsh-tools'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { Agent, AgentId } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type {
SubagentCapabilities,
SubagentProvider,
@@ -96,7 +97,7 @@ export interface SubagentRunInfo {
/** The provider that established the run. */
readonly provider: string
/** The child agent's id. */
readonly id: AgentId
readonly id: SessionId
}
/** Observe-only outcome detail for a settled subagent run. */
@@ -104,7 +105,7 @@ export interface SubagentRunEndInfo {
/** The provider that ran it. */
readonly provider: string
/** The child agent's id. */
readonly id: AgentId
readonly id: SessionId
/** The terminal stop reason. */
readonly stopReason: SubagentResult['stopReason']
/** The child's final assistant output, absent on infrastructure rejection. */

View File

@@ -6,8 +6,9 @@
* @module @deepseek-ai/dsh-subagent/types
*/
import type { Agent, AgentId, AgentOptions } from '@deepseek-ai/dsh-agent'
import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { StructuredOutputSchema, ToolRestriction } from '@deepseek-ai/dsh-tools'
/**
@@ -147,7 +148,7 @@ export interface SubagentResult {
*/
export interface SubagentRun {
/** The child agent's id (local in-process runs are already published in `ctx.agents`; remote transports need not publish locally). */
readonly id: AgentId
readonly id: SessionId
/**
* Resolves with the child's terminal {@link SubagentResult} when the run
* settles. Does NOT reject on a child-level failure — a model/transport

View File

@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import { type Agent } from '@deepseek-ai/dsh-agent'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import { carrierKeyOf } from '@deepseek-ai/dsh-scope'
import SubagentService, {
@@ -12,9 +13,10 @@ import SubagentService, {
type SubagentRun,
type SubagentStartRequest,
} from '@deepseek-ai/dsh-subagent'
import { SessionId } from '@deepseek-ai/dsh-session'
function fakeParent(id = 'parent-1'): Agent {
return { id: AgentId(id) } as unknown as Agent
return { id: SessionId(id) } as unknown as Agent
}
const ALL_CAPS: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }
@@ -45,7 +47,7 @@ class StubProvider implements SubagentProvider {
async start(request: SubagentStartRequest): Promise<SubagentRun> {
this.startCount += 1
return {
id: AgentId(`child:${this.name}:${request.parent.id}`),
id: SessionId(`child:${this.name}:${request.parent.id}`),
result: Promise.resolve(this.outcome),
async dispose() {},
}
@@ -141,7 +143,7 @@ describe('SubagentService', () => {
const starting = subagents.start('deferred', baseRequest({ parent }))
await Promise.resolve()
expect(events).toEqual([])
ready.resolve({ id: AgentId('child'), result: result.promise, async dispose() {} })
ready.resolve({ id: SessionId('child'), result: result.promise, async dispose() {} })
const run = await starting
expect(events).toEqual(['start'])
result.resolve({ output: [{ type: 'text', text: 'answer' }], stopReason: 'completed' })
@@ -190,7 +192,7 @@ describe('SubagentService', () => {
capabilities: NO_CAPS,
inheritsParentContext: false,
async start() {
return { id: AgentId('infra-child'), result: failure.promise, async dispose() {} }
return { id: SessionId('infra-child'), result: failure.promise, async dispose() {} }
},
})
const failedRun = await subagents.start('infra', baseRequest())

View File

@@ -4,10 +4,12 @@ import Loader from '@cordisjs/plugin-loader'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import { type Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as mock from '@deepseek-ai/dsh-subagent-mock'
import * as tool from '../src/index.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
/**
* Drives the REAL plugin body: mounts `dsh-tool-subagent` on a real
@@ -20,7 +22,7 @@ import * as tool from '../src/index.ts'
/** A minimal parent Agent — the tool reads `agent.id` for `parent`. */
function fakeAgent(id = 'parent-1'): Agent {
return { id: AgentId(id) } as unknown as Agent
return { id: SessionId(id) } as unknown as Agent
}
async function setup(toolConfig: tool.Config, mockConfig: Partial<mock.Config> = {}) {
@@ -114,7 +116,7 @@ describe('dsh-tool-subagent', () => {
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
inheritsParentContext: false,
start: async () => ({
id: AgentId('weird-child'),
id: SessionId('weird-child'),
result: Promise.resolve({ output: [{ type: 'text', text: 'partial' }], stopReason: 'frobnicated' as never }),
dispose: async () => {},
}),
@@ -141,7 +143,7 @@ describe('dsh-tool-subagent', () => {
start: async (request) => {
seen = request
return {
id: AgentId('capture-child'),
id: SessionId('capture-child'),
result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
dispose: async () => {},
}
@@ -170,7 +172,7 @@ describe('dsh-tool-subagent', () => {
start: async (request) => {
seen = request
return {
id: AgentId('bare-child'),
id: SessionId('bare-child'),
result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
dispose: async () => {},
}
@@ -297,7 +299,7 @@ describe('dsh-tool-subagent', () => {
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
inheritsParentContext: false,
start: async () => ({
id: AgentId('spy-child'),
id: SessionId('spy-child'),
result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
dispose: async () => void disposed(),
}),
@@ -319,7 +321,7 @@ describe('dsh-tool-subagent', () => {
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
inheritsParentContext: false,
start: async () => ({
id: AgentId('spy-child'),
id: SessionId('spy-child'),
result: Promise.resolve({ output: [], stopReason: 'error' as const }),
dispose: async () => void disposed(),
}),
@@ -350,7 +352,7 @@ describe('dsh-tool-subagent', () => {
resolveResult({ output: [], stopReason: 'aborted' })
}, { once: true })
return {
id: AgentId('spy-child'),
id: SessionId('spy-child'),
result,
dispose: async () => {},
}
@@ -442,7 +444,7 @@ describe('dsh-tool-subagent', () => {
start: async (request) => {
seen = request
return {
id: AgentId('capture2-child'),
id: SessionId('capture2-child'),
result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
dispose: async () => {},
}
@@ -499,7 +501,7 @@ describe('dsh-tool-subagent', () => {
start: async (request) => {
seen = request
return {
id: AgentId('capture3-child'),
id: SessionId('capture3-child'),
result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
dispose: async () => {},
}
@@ -528,7 +530,7 @@ describe('dsh-tool-subagent', () => {
start: async (request) => {
seen = request
return {
id: AgentId('capture4-child'),
id: SessionId('capture4-child'),
result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }),
dispose: async () => {},
}