Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring
# Conflicts: # docs/module-graph.i18n.yaml # docs/module-graph.md # docs/module-graph.zh.md # docs/subsystems/tools.i18n.yaml # docs/subsystems/tools.md # docs/subsystems/tools.zh.md # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
This commit is contained in:
@@ -15,6 +15,10 @@
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./lib/types/types.d.ts",
|
||||
"default": "./lib/types/types.js"
|
||||
},
|
||||
"./presentation": {
|
||||
"types": "./lib/types/presentation.d.ts",
|
||||
"default": "./lib/types/presentation.js"
|
||||
|
||||
@@ -14,41 +14,7 @@ import type { JsonValue } from '@deepseek-ai/dsh-session'
|
||||
import { defineTool, parameterSchemaSpecToJsonSchema } from './schema.ts'
|
||||
import { TOOL_REGISTRY_SCHEDULER } from './index.ts'
|
||||
import type { CodeDispatchLog, ToolDefinition, ToolExecutionResult, ToolRegistry, ToolRunContext } from './index.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-session' {
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
* One sub-dispatch STARTING inside a `run_code` program: the parent
|
||||
* `run_code` call id, the deterministic sub-call id (`<parent>:code:<n>`,
|
||||
* numbered in submission order), and the tool `name` with its
|
||||
* JSON-normalized `arguments` — the exact value dispatched, normalized
|
||||
* BEFORE dispatch, so this append can never fail on payload shape.
|
||||
* Appended when the scheduler actually starts the call (not at
|
||||
* submission), so a start means the tool body pipeline was entered; a
|
||||
* call abandoned in the queue logs nothing. Log-only: `deriveMessages()`
|
||||
* ignores it; UIs use it for live per-sub-call running state and pair it
|
||||
* with `tool/code-dispatch` by `subCallId` (timing = the two events'
|
||||
* `time` fields).
|
||||
*/
|
||||
'tool/code-dispatch-start': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown }
|
||||
/**
|
||||
* One bridged sub-dispatch SETTLING: the pairing ids (matching the
|
||||
* `tool/code-dispatch-start` with the same `subCallId`), the tool `name`
|
||||
* with the same JSON-normalized `arguments`, and the sub-call's complete
|
||||
* model-facing outcome in `tool/result`'s own vocabulary
|
||||
* (`content` + `isError`), so UIs render a sub-call through the exact
|
||||
* code path that renders a native call. Every started sub-call settles
|
||||
* with exactly one of these (abort included: the aborted pipeline result
|
||||
* is an `isError` outcome).
|
||||
* Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter
|
||||
* model context; persistence and UIs get every call. Appended inside the
|
||||
* parent `run_code`'s execution (the bridge drains in-flight dispatches
|
||||
* before returning), so its execution-enclosure relation holds by
|
||||
* construction.
|
||||
*/
|
||||
'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; content: ContentBlock[] }
|
||||
}
|
||||
}
|
||||
import type {} from './types.ts'
|
||||
|
||||
/** The model-facing name of the Code Mode tool. */
|
||||
export const RUN_CODE_NAME = 'run_code'
|
||||
@@ -502,6 +468,7 @@ export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridge
|
||||
const subCallId = CallId(`${String(exec.callId)}:code:${n}`)
|
||||
const input = {
|
||||
callId: subCallId,
|
||||
rootCallId: exec.rootCallId,
|
||||
name,
|
||||
arguments: normalized.dispatched,
|
||||
...exec.agent ? { agent: exec.agent } : {},
|
||||
@@ -539,6 +506,7 @@ export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridge
|
||||
content: result.content,
|
||||
})
|
||||
agent.session.append('tool/code-dispatch', {
|
||||
rootCallId: exec.rootCallId,
|
||||
parentCallId: exec.callId,
|
||||
subCallId,
|
||||
name,
|
||||
@@ -563,6 +531,7 @@ export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridge
|
||||
},
|
||||
async start(): Promise<void> {
|
||||
exec.agent?.session.append('tool/code-dispatch-start', {
|
||||
rootCallId: exec.rootCallId,
|
||||
parentCallId: exec.callId,
|
||||
subCallId,
|
||||
name,
|
||||
|
||||
@@ -85,6 +85,7 @@ export {
|
||||
} from './json-schema.ts'
|
||||
|
||||
export type { JsonValue } from '@deepseek-ai/dsh-session'
|
||||
export type { CodeDispatchEventData, CodeDispatchStartEventData } from './types.ts'
|
||||
|
||||
export { CodeRunFailedError, RUN_CODE_NAME } from './code-mode.ts'
|
||||
export { jsonSchemaToTs, renderToolsSdk } from './ts-types.ts'
|
||||
@@ -297,6 +298,11 @@ export type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]:
|
||||
*/
|
||||
export interface ToolExecutionInput {
|
||||
readonly callId: CallId
|
||||
/**
|
||||
* Root model-requested call owning this execution tree. Callers omit it for
|
||||
* a root execution; nested dispatchers propagate the enclosing value.
|
||||
*/
|
||||
readonly rootCallId?: CallId
|
||||
readonly name: string
|
||||
/** Losslessly JSON-serializable parsed arguments (tools validate their own schema). */
|
||||
readonly arguments: unknown
|
||||
@@ -352,6 +358,8 @@ export interface CodeDispatchLog {
|
||||
* observers run.
|
||||
*/
|
||||
export interface ToolExecution extends ToolExecutionInput {
|
||||
/** Root model-requested call, resolved for every root and nested execution. */
|
||||
readonly rootCallId: CallId
|
||||
/** Registry-assigned identity shared with nested calls only as their opaque `parent` token. */
|
||||
readonly token: ToolExecutionToken
|
||||
}
|
||||
@@ -1233,6 +1241,7 @@ export class ToolRegistry extends Service {
|
||||
const deferredContexts: UserMessage[] = []
|
||||
const token = createExecutionToken()
|
||||
const callId = exec.callId
|
||||
const rootCallId = exec.rootCallId ?? callId
|
||||
const name = exec.name
|
||||
const agent = exec.agent
|
||||
const parent = exec.parent
|
||||
@@ -1243,6 +1252,7 @@ export class ToolRegistry extends Service {
|
||||
const base = {
|
||||
token,
|
||||
callId,
|
||||
rootCallId,
|
||||
name,
|
||||
signal,
|
||||
...agent !== undefined ? { agent } : {},
|
||||
|
||||
@@ -33,9 +33,34 @@ function validateResult(
|
||||
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
|
||||
const stages = new WeakMap<object, ToolStage>()
|
||||
const openTurns = new WeakMap<Session, number | null>()
|
||||
const dispatchRoots = new WeakMap<Session, Map<string, string>>()
|
||||
const validateDispatch = (session: Session, event: SessionEvent): void => {
|
||||
if (event.type !== 'tool/code-dispatch-start' && event.type !== 'tool/code-dispatch') return
|
||||
const root = String(event.data.rootCallId)
|
||||
const parent = String(event.data.parentCallId)
|
||||
const child = String(event.data.subCallId)
|
||||
if (root.length === 0 || parent.length === 0 || child.length === 0) {
|
||||
fail(`${event.type} must carry non-empty rootCallId, parentCallId, and subCallId`)
|
||||
return
|
||||
}
|
||||
const roots = dispatchRoots.get(session)
|
||||
const known = roots?.get(child)
|
||||
if (known !== undefined && known !== root) fail(`${event.type} changed rootCallId for subCallId ${child}`)
|
||||
if (parent !== root && roots?.get(parent) !== root) {
|
||||
fail(`${event.type} parentCallId ${parent} does not belong to rootCallId ${root}`)
|
||||
}
|
||||
}
|
||||
const commitDispatch = (session: Session, event: SessionEvent): void => {
|
||||
if (event.type !== 'tool/code-dispatch-start' && event.type !== 'tool/code-dispatch') return
|
||||
const roots = dispatchRoots.get(session) as Map<string, string>
|
||||
roots.set(String(event.data.subCallId), String(event.data.rootCallId))
|
||||
}
|
||||
const seed = (session: Session): number | null => {
|
||||
let openTurn: number | null = null
|
||||
dispatchRoots.set(session, new Map())
|
||||
for (const event of session.events) {
|
||||
validateDispatch(session, event)
|
||||
commitDispatch(session, event)
|
||||
if (event.type === 'turn/start') openTurn = event.data.turn
|
||||
else if (event.type === 'turn/end') openTurn = null
|
||||
else if ((event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch')
|
||||
@@ -51,12 +76,15 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant
|
||||
for (const session of ctx.sessions.list()) seed(session)
|
||||
ctx.on('session/created', (session) => { seed(session) }, { global: true })
|
||||
ctx.on('session/event', (session, event) => {
|
||||
validateDispatch(session, event)
|
||||
commitDispatch(session, event)
|
||||
if (event.type === 'turn/start') openTurns.set(session, event.data.turn)
|
||||
else if (event.type === 'turn/end') openTurns.set(session, null)
|
||||
}, { global: true })
|
||||
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
||||
if (eventName === 'session/event') {
|
||||
const [session, event] = args as [Session, SessionEvent]
|
||||
validateDispatch(session, event)
|
||||
if ((event.type === 'tool/code-dispatch-start' || event.type === 'tool/code-dispatch')
|
||||
&& openTurnFor(session) === null) {
|
||||
fail(`${event.type} appended outside any open turn`)
|
||||
|
||||
58
packages/core/tools/src/types.ts
Normal file
58
packages/core/tools/src/types.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Durable Tool event vocabulary shared with type-only consumers.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-tools/types
|
||||
*/
|
||||
|
||||
import type { CallId } from '@deepseek-ai/dsh-llm/brand'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
||||
|
||||
/** Payload recorded when one nested Code Mode Tool dispatch starts. */
|
||||
export interface CodeDispatchStartEventData {
|
||||
rootCallId: CallId
|
||||
parentCallId: CallId
|
||||
subCallId: CallId
|
||||
name: string
|
||||
arguments: unknown
|
||||
}
|
||||
|
||||
/** Payload recorded when one nested Code Mode Tool dispatch settles. */
|
||||
export interface CodeDispatchEventData extends CodeDispatchStartEventData {
|
||||
isError: boolean
|
||||
content: ContentBlock[]
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-session/types' {
|
||||
interface SessionEventMap {
|
||||
/**
|
||||
* One sub-dispatch STARTING inside a `run_code` program: the parent
|
||||
* `run_code` call id, the deterministic sub-call id (`<parent>:code:<n>`,
|
||||
* numbered in submission order), and the tool `name` with its
|
||||
* JSON-normalized `arguments` — the exact value dispatched, normalized
|
||||
* BEFORE dispatch, so this append can never fail on payload shape.
|
||||
* Appended when the scheduler actually starts the call (not at
|
||||
* submission), so a start means the tool body pipeline was entered; a
|
||||
* call abandoned in the queue logs nothing. Log-only: `deriveMessages()`
|
||||
* ignores it; UIs use it for live per-sub-call running state and pair it
|
||||
* with `tool/code-dispatch` by `subCallId` (timing = the two events'
|
||||
* `time` fields).
|
||||
*/
|
||||
'tool/code-dispatch-start': CodeDispatchStartEventData
|
||||
/**
|
||||
* One bridged sub-dispatch SETTLING: the pairing ids (matching the
|
||||
* `tool/code-dispatch-start` with the same `subCallId`), the tool `name`
|
||||
* with the same JSON-normalized `arguments`, and the sub-call's complete
|
||||
* model-facing outcome in `tool/result`'s own vocabulary
|
||||
* (`content` + `isError`), so UIs render a sub-call through the exact
|
||||
* code path that renders a native call. Every started sub-call settles
|
||||
* with exactly one of these (abort included: the aborted pipeline result
|
||||
* is an `isError` outcome).
|
||||
* Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter
|
||||
* model context; persistence and UIs get every call. Appended inside the
|
||||
* parent `run_code`'s execution (the bridge drains in-flight dispatches
|
||||
* before returning), so its execution-enclosure relation holds by
|
||||
* construction.
|
||||
*/
|
||||
'tool/code-dispatch': CodeDispatchEventData
|
||||
}
|
||||
}
|
||||
@@ -785,11 +785,11 @@ describe('the run_code dispatch bridge', () => {
|
||||
const dispatches = events.filter(event => event.type === 'tool/code-dispatch')
|
||||
expect(dispatches.map(event => event.data)).toEqual([
|
||||
{
|
||||
parentCallId: 'call-1', subCallId: 'call-1:code:1', name: 'echo',
|
||||
rootCallId: 'call-1', parentCallId: 'call-1', subCallId: 'call-1:code:1', name: 'echo',
|
||||
arguments: { value: 'one' }, isError: false, content: [{ type: 'text', text: 'echo:one' }],
|
||||
},
|
||||
{
|
||||
parentCallId: 'call-1', subCallId: 'call-1:code:2', name: 'echo',
|
||||
rootCallId: 'call-1', parentCallId: 'call-1', subCallId: 'call-1:code:2', name: 'echo',
|
||||
arguments: { value: 'two' }, isError: false, content: [{ type: 'text', text: 'echo:two' }],
|
||||
},
|
||||
])
|
||||
@@ -1526,6 +1526,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' })
|
||||
session.append('tool/code-dispatch', {
|
||||
rootCallId: CallId('p1'),
|
||||
parentCallId: CallId('p1'),
|
||||
subCallId: CallId('p1:code:1'),
|
||||
name: 'echo',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { scopeTarget } from '@deepseek-ai/dsh-scope'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore from '@deepseek-ai/dsh-session'
|
||||
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
|
||||
import * as ToolsInvariant from '@deepseek-ai/dsh-tools/invariant'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
@@ -24,6 +24,7 @@ const execution = (overrides: Partial<ToolExecution> = {}): ToolExecution => ({
|
||||
arguments: Object.freeze({ text: 'hi' }),
|
||||
...overrides,
|
||||
signal: overrides.signal ?? testToolSignal,
|
||||
rootCallId: overrides.rootCallId ?? overrides.callId ?? CallId('call-1'),
|
||||
})
|
||||
|
||||
const outcome = (): ToolExecutionResult => Object.freeze({
|
||||
@@ -92,6 +93,7 @@ describe('tool-pipeline invariants', () => {
|
||||
const ctx = await setup()
|
||||
const session = ctx.sessions.create()
|
||||
const data = {
|
||||
rootCallId: CallId('parent'),
|
||||
parentCallId: CallId('parent'),
|
||||
subCallId: CallId('child'),
|
||||
name: 'echo',
|
||||
@@ -103,12 +105,112 @@ describe('tool-pipeline invariants', () => {
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
})
|
||||
|
||||
it('does not commit a rejected dispatch edge into the root index', async () => {
|
||||
const ctx = await setup()
|
||||
const session = ctx.sessions.create()
|
||||
expect(() => session.append('tool/code-dispatch-start', {
|
||||
rootCallId: CallId('rejected-root'),
|
||||
parentCallId: CallId('rejected-root'),
|
||||
subCallId: CallId('reused-child'),
|
||||
name: 'echo',
|
||||
arguments: {},
|
||||
})).toThrow(/outside any open turn/)
|
||||
|
||||
session.append('turn/start', { turn: 1 })
|
||||
expect(() => session.append('tool/code-dispatch-start', {
|
||||
rootCallId: CallId('accepted-root'),
|
||||
parentCallId: CallId('accepted-root'),
|
||||
subCallId: CallId('reused-child'),
|
||||
name: 'echo',
|
||||
arguments: {},
|
||||
})).not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects a nested code dispatch that changes its parent chain root before append', async () => {
|
||||
const ctx = await setup()
|
||||
const session = ctx.sessions.create()
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('tool/code-dispatch-start', {
|
||||
rootCallId: CallId('root'),
|
||||
parentCallId: CallId('root'),
|
||||
subCallId: CallId('child'),
|
||||
name: 'run_code',
|
||||
arguments: {},
|
||||
})
|
||||
session.append('tool/code-dispatch-start', {
|
||||
rootCallId: CallId('root'),
|
||||
parentCallId: CallId('child'),
|
||||
subCallId: CallId('grandchild'),
|
||||
name: 'echo',
|
||||
arguments: {},
|
||||
})
|
||||
|
||||
expect(() => session.append('tool/code-dispatch-start', {
|
||||
rootCallId: CallId('another-root'),
|
||||
parentCallId: CallId('child'),
|
||||
subCallId: CallId('invalid-grandchild'),
|
||||
name: 'echo',
|
||||
arguments: {},
|
||||
})).toThrow(/parentCallId child does not belong to rootCallId another-root/)
|
||||
expect(session.events.some(event => event.type === 'tool/code-dispatch-start'
|
||||
&& String(event.data.subCallId) === 'invalid-grandchild')).toBe(false)
|
||||
})
|
||||
|
||||
it('requires non-empty dispatch identities and keeps one subcall on one root', async () => {
|
||||
const ctx = await setup()
|
||||
const session = ctx.sessions.create()
|
||||
session.append('turn/start', { turn: 1 })
|
||||
expect(() => session.append('tool/code-dispatch-start', {
|
||||
rootCallId: CallId(''),
|
||||
parentCallId: CallId('root'),
|
||||
subCallId: CallId('child'),
|
||||
name: 'echo',
|
||||
arguments: {},
|
||||
})).toThrow(/must carry non-empty rootCallId/)
|
||||
|
||||
session.append('tool/code-dispatch-start', {
|
||||
rootCallId: CallId('root'),
|
||||
parentCallId: CallId('root'),
|
||||
subCallId: CallId('child'),
|
||||
name: 'echo',
|
||||
arguments: {},
|
||||
})
|
||||
expect(() => session.append('tool/code-dispatch-start', {
|
||||
rootCallId: CallId('other-root'),
|
||||
parentCallId: CallId('other-root'),
|
||||
subCallId: CallId('child'),
|
||||
name: 'echo',
|
||||
arguments: {},
|
||||
})).toThrow(/changed rootCallId for subCallId child/)
|
||||
})
|
||||
|
||||
it('indexes dispatch records emitted for a bare session', async () => {
|
||||
const ctx = await setup()
|
||||
const session = Session.create(SessionId('bare-dispatch-session'))
|
||||
session.append('turn/start', { turn: 1 })
|
||||
expect(() => {
|
||||
ctx.emit('session/event', session as never, {
|
||||
type: 'tool/code-dispatch-start',
|
||||
seq: 1,
|
||||
time: 1,
|
||||
data: {
|
||||
rootCallId: CallId('root'),
|
||||
parentCallId: CallId('root'),
|
||||
subCallId: CallId('child'),
|
||||
name: 'echo',
|
||||
arguments: {},
|
||||
},
|
||||
} as never)
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('replays enclosed code-dispatch records on late registration', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const session = ctx.sessions.create()
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('tool/code-dispatch', {
|
||||
rootCallId: CallId('parent'),
|
||||
parentCallId: CallId('parent'),
|
||||
subCallId: CallId('child'),
|
||||
name: 'echo',
|
||||
@@ -125,6 +227,7 @@ describe('tool-pipeline invariants', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
ctx.sessions.create().append('tool/code-dispatch-start', {
|
||||
rootCallId: CallId('parent'),
|
||||
parentCallId: CallId('parent'),
|
||||
subCallId: CallId('child'),
|
||||
name: 'echo',
|
||||
|
||||
Reference in New Issue
Block a user