Merge remote-tracking branch 'origin/worktree/ci-native-windows-20260808' into worktree/ci-native-windows-coverage-20260808

This commit is contained in:
Tianyi Cui
2026-08-08 21:28:09 +08:00
25 changed files with 239 additions and 133 deletions

View File

@@ -11,28 +11,44 @@ import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { SessionId } from '@deepseek-ai/dsh-session'
import { assertNever } from '@deepseek-ai/dsh-llm'
import type { SubagentDescendantListEntry, SubagentListEntry } from '@deepseek-ai/dsh-subagent'
export const name = 'tool-subagent-list-agents'
export const inject = ['tools', 'subagents', 'agents']
type ListAgentsScope = 'children' | 'descendants'
interface ListAgentsRequest {
readonly scope?: ListAgentsScope
}
interface ListAgentsSpec {
readonly scope: ListAgentsScope
}
type ListAgentsEntry =
| {
readonly kind: 'child'
readonly id: string
readonly id: SessionId
readonly label: string
readonly status: 'running' | 'idle' | 'complete'
readonly parent?: string
readonly parent?: SessionId
readonly depth?: number
}
| {
readonly kind: 'diagnostic'
readonly id: string
readonly id: SessionId
readonly reason: 'corrupt' | 'unsupported' | 'unavailable'
readonly parent?: string
readonly parent?: SessionId
readonly depth?: number
}
/** Resolve the optional model request into an internal required-scope spec. */
function resolveListAgentsRequest(request: ListAgentsRequest): ListAgentsSpec {
return { scope: request.scope ?? 'children' }
}
/**
* Refine one candidate's status through the live Agent registry: `running`
* for an active driver, `idle` for a resident Agent between turns (possibly
@@ -50,7 +66,7 @@ function project(
entry: SubagentListEntry,
position?: Pick<SubagentDescendantListEntry, 'parentId' | 'depth'>,
): ListAgentsEntry | undefined {
const at = position === undefined ? {} : { parent: position.parentId as string, depth: position.depth }
const at = position === undefined ? {} : { parent: position.parentId, depth: position.depth }
if (entry.kind === 'diagnostic') {
return { kind: 'diagnostic', id: entry.id, reason: entry.reason, ...at }
}
@@ -121,22 +137,25 @@ export function apply(ctx: Context): void {
],
},
},
render: (args, entries) => [{
type: 'text',
text: entries.length === 0
? '(no subagents)'
: entries.map((entry) => {
// A descendants row always carries its position; children rows
// never render it. String() spans the schema-optional shape
// without a dead fallback branch.
const at = args.scope === 'descendants'
? ` parent=${String(entry.parent)} depth=${String(entry.depth)}`
: ''
return entry.kind === 'child'
? `${entry.id} [${entry.status}]${at}${entry.label}`
: `${entry.id} [diagnostic: ${entry.reason}]${at}`
}).join('\n'),
}],
render: (args, entries) => {
const request = resolveListAgentsRequest(args)
return [{
type: 'text',
text: entries.length === 0
? '(no subagents)'
: entries.map((entry) => {
// A descendants row always carries its position; children rows
// never render it. String() spans the schema-optional shape
// without a dead fallback branch.
const at = request.scope === 'descendants'
? ` parent=${String(entry.parent)} depth=${String(entry.depth)}`
: ''
return entry.kind === 'child'
? `${entry.id} [${entry.status}]${at}${entry.label}`
: `${entry.id} [diagnostic: ${entry.reason}]${at}`
}).join('\n'),
}]
},
},
async execute(args, exec) {
const parent = exec.agent
@@ -144,18 +163,26 @@ export function apply(ctx: Context): void {
// Non-agent callers have no session whose children could be listed.
throw new Error('list_agents requires a calling agent (exec.agent was undefined)')
}
const request = resolveListAgentsRequest(args)
// The registry drains started tool bodies, so the scan must observe the
// call's signal rather than finish a slow catalog after cancellation.
if (args.scope === 'descendants') {
const entries = await ctx.subagents.listDescendants(parent.id, exec.signal)
return entries
.map(entry => project(ctx.agents, entry, entry))
.filter(entry => entry !== undefined)
switch (request.scope) {
case 'children': {
const entries = await ctx.subagents.listChildren(parent.id, exec.signal)
return entries
.map(entry => project(ctx.agents, entry))
.filter(entry => entry !== undefined)
}
case 'descendants': {
const entries = await ctx.subagents.listDescendants(parent.id, exec.signal)
return entries
.map(entry => project(ctx.agents, entry, entry))
.filter(entry => entry !== undefined)
}
/* v8 ignore next 2 -- the resolver normalizes the schema-validated closed scope before dispatch. */
default:
return assertNever(request.scope, 'list_agents scope')
}
const entries = await ctx.subagents.listChildren(parent.id, exec.signal)
return entries
.map(entry => project(ctx.agents, entry))
.filter(entry => entry !== undefined)
},
}))
}

View File

@@ -185,7 +185,7 @@ describe('dsh-tool-subagent-control/list-agents', () => {
)
})
it('forwards the tool cancellation signal to child enumeration', async () => {
it('resolves omitted scope to children and forwards the tool cancellation signal', async () => {
const { ctx, parent } = await setup([])
const signal = new AbortController().signal
const listChildren = vi.spyOn(ctx.subagents, 'listChildren').mockResolvedValue([])
@@ -326,7 +326,7 @@ describe('dsh-tool-subagent-control/list-agents', () => {
)
})
it('forwards the tool cancellation signal to descendant enumeration', async () => {
it('preserves explicit descendants scope and forwards the tool cancellation signal', async () => {
const { ctx, parent } = await setup([])
const signal = new AbortController().signal
const listDescendants = vi.spyOn(ctx.subagents, 'listDescendants').mockResolvedValue([])