Merge refreshed rfc/pty into feature/persistent-pty-sessions
# Conflicts: # docs/architecture.md # docs/capability-seams.md # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/module-graph.md # docs/tool-catalog.md # examples/acp-agent/tests/acp.snapshot.ts # examples/headless-agent/tests/headless.snapshot.ts # examples/package.json # packages/README.md # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/tools/tests/gen-tool-catalog.spec.ts # pnpm-lock.yaml # pnpm-workspace.yaml # scripts/gen-tool-catalog.ts # scripts/type-equiv.manifest.json # website/.vitepress/config/api-sidebar.json
This commit is contained in:
@@ -6,12 +6,14 @@ import type { Scope } from '@deepseek-ai/dsh-scope'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
||||
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
|
||||
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { Config, PostToolDecision, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEventMap } from '@deepseek-ai/dsh-session'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
/**
|
||||
* Code Mode unit tier (per the Agent Note's plan): provider contribution per mode,
|
||||
* misconfiguration rejections, the run_code dispatch bridge (serialization,
|
||||
@@ -95,6 +97,7 @@ function fakeAgent(options: { cwd?: string } = { cwd: '/workspace' }): { agent:
|
||||
/** Dispatch run_code through the registry pipeline, as the loop would. */
|
||||
async function runCode(ctx: Context, code: string, extras: { agent?: Agent; signal?: AbortSignal } = {}): Promise<ToolExecutionResult> {
|
||||
return ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('call-1'),
|
||||
name: RUN_CODE_NAME,
|
||||
arguments: { code },
|
||||
@@ -357,8 +360,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
const previous = exec.signal
|
||||
exec.signal = new AbortController().signal
|
||||
const result = await next()
|
||||
if (previous === undefined) delete exec.signal
|
||||
else exec.signal = previous
|
||||
exec.signal = previous
|
||||
return result
|
||||
})
|
||||
ctx.on('tools/result', (exec) => {
|
||||
@@ -574,7 +576,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
seen.push(args.id)
|
||||
await new Promise<void>((resolve) => {
|
||||
const timer = setTimeout(resolve, 500)
|
||||
exec.signal?.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true })
|
||||
exec.signal.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true })
|
||||
})
|
||||
return [{ type: 'text' as const, text: args.id }]
|
||||
},
|
||||
@@ -610,7 +612,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
started()
|
||||
await new Promise<void>((resolve) => {
|
||||
const timer = setTimeout(resolve, 500)
|
||||
exec.signal?.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true })
|
||||
exec.signal.addEventListener('abort', () => { sawAbort = true; clearTimeout(timer); resolve() }, { once: true })
|
||||
})
|
||||
return [{ type: 'text' as const, text: args.id }]
|
||||
},
|
||||
@@ -838,7 +840,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
expect((result.content[0] as { text: string }).text).toBe('{ n: 42 }')
|
||||
})
|
||||
|
||||
it('reports a pre-aborted outer signal as the run failure without dispatching anything', async () => {
|
||||
it('short-circuits a pre-aborted outer signal before the code runtime', async () => {
|
||||
const { ctx, runtime } = await setup({ mode: 'code' })
|
||||
const calls = registerEcho(ctx)
|
||||
runtime.behavior = (request) => {
|
||||
@@ -850,11 +852,16 @@ describe('the run_code dispatch bridge', () => {
|
||||
controller.abort('too-late')
|
||||
const result = await runCode(ctx, 'program', { signal: controller.signal })
|
||||
expect(result.isError).toBe(true)
|
||||
expect((result.content[0] as { text: string }).text).toContain('code run failed (abort)')
|
||||
expect(result).toEqual({
|
||||
content: [{ type: 'text', text: 'Error: tool call aborted before dispatch' }],
|
||||
isError: true,
|
||||
error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH },
|
||||
})
|
||||
expect(runtime.lastRequest).toBeUndefined()
|
||||
expect(calls).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects a binding invoked after the run is over without dispatching it', async () => {
|
||||
it('reports cancellation after rejecting a late binding without dispatching it', async () => {
|
||||
const { ctx, runtime } = await setup({ mode: 'code' })
|
||||
const calls = registerEcho(ctx)
|
||||
const controller = new AbortController()
|
||||
@@ -865,8 +872,9 @@ describe('the run_code dispatch bridge', () => {
|
||||
return { logs: [], value: message }
|
||||
}
|
||||
const result = await runCode(ctx, 'program', { signal: controller.signal })
|
||||
expect(result.isError).toBe(false)
|
||||
expect((result.content[0] as { text: string }).text).toContain('not dispatched')
|
||||
expect(result.isError).toBe(true)
|
||||
expect(result.error).toEqual({ name: 'AbortError', code: 'ABORTED' })
|
||||
expect((result.content[0] as { text: string }).text).toBe('Error: tool call aborted')
|
||||
expect(calls).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import ToolRegistry, {
|
||||
type ToolExecutionMode,
|
||||
} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
async function setup() {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
@@ -19,7 +21,7 @@ async function setup() {
|
||||
}
|
||||
|
||||
function exec(name: string, args: unknown): ToolExecutionInput {
|
||||
return { callId: CallId('c1'), name, arguments: args }
|
||||
return { signal: testToolSignal, callId: CallId('c1'), name, arguments: args }
|
||||
}
|
||||
|
||||
describe('ToolRegistry.executionMode', () => {
|
||||
|
||||
100
packages/core/tools/tests/execution-signal-types.spec.ts
Normal file
100
packages/core/tools/tests/execution-signal-types.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, expectTypeOf, it } from 'vitest'
|
||||
import type { Context } from 'cordis'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type {
|
||||
ToolDispatchExecution,
|
||||
ToolExecution,
|
||||
ToolExecutionInput,
|
||||
ToolRunContext,
|
||||
} from '@deepseek-ai/dsh-tools'
|
||||
|
||||
function inputAndExecutionContracts(
|
||||
input: ToolExecutionInput,
|
||||
execution: ToolExecution,
|
||||
run: ToolRunContext,
|
||||
): void {
|
||||
// @ts-expect-error -- every typed invocation must supply a caller-owned signal.
|
||||
const missingSignal: ToolExecutionInput = { callId: CallId('missing'), name: 'probe', arguments: {} }
|
||||
void missingSignal
|
||||
|
||||
// @ts-expect-error -- caller input is readonly after construction.
|
||||
input.signal = new AbortController().signal
|
||||
// @ts-expect-error -- required readonly properties cannot be deleted.
|
||||
delete input.signal
|
||||
// @ts-expect-error -- required signals cannot become undefined.
|
||||
input.signal = undefined
|
||||
|
||||
// @ts-expect-error -- pipeline observers receive a readonly execution view.
|
||||
execution.signal = new AbortController().signal
|
||||
// @ts-expect-error -- pipeline observers cannot remove the required signal.
|
||||
delete execution.signal
|
||||
// @ts-expect-error -- tool bodies receive a readonly run context.
|
||||
run.signal = new AbortController().signal
|
||||
// @ts-expect-error -- tool bodies cannot remove the required signal.
|
||||
delete run.signal
|
||||
// @ts-expect-error -- tool bodies cannot replace the required signal with undefined.
|
||||
run.signal = undefined
|
||||
}
|
||||
void inputAndExecutionContracts
|
||||
|
||||
function observerContracts(ctx: Context): void {
|
||||
ctx.on('tools/pre-execute', (exec, next) => {
|
||||
// @ts-expect-error -- pre-policy sees a readonly signal.
|
||||
exec.signal = new AbortController().signal
|
||||
// @ts-expect-error -- pre-policy cannot remove the required signal.
|
||||
delete exec.signal
|
||||
// @ts-expect-error -- pre-policy cannot replace the required signal with undefined.
|
||||
exec.signal = undefined
|
||||
return next()
|
||||
})
|
||||
ctx.on('tools/post-execute', (exec, _result, next) => {
|
||||
// @ts-expect-error -- post-policy sees a readonly signal.
|
||||
exec.signal = new AbortController().signal
|
||||
// @ts-expect-error -- post-policy sees a readonly signal.
|
||||
delete exec.signal
|
||||
// @ts-expect-error -- post-policy cannot replace the required signal with undefined.
|
||||
exec.signal = undefined
|
||||
return next()
|
||||
})
|
||||
ctx.on('tools/result', (exec) => {
|
||||
// @ts-expect-error -- result observers see a readonly signal.
|
||||
exec.signal = new AbortController().signal
|
||||
// @ts-expect-error -- result observers cannot remove the required signal.
|
||||
delete exec.signal
|
||||
// @ts-expect-error -- result observers see a readonly signal.
|
||||
exec.signal = undefined
|
||||
})
|
||||
ctx.on('tools/execute', (exec, next) => {
|
||||
exec.signal = new AbortController().signal
|
||||
// @ts-expect-error -- around-dispatch may replace but not remove the signal.
|
||||
delete exec.signal
|
||||
// @ts-expect-error -- around-dispatch cannot replace the required signal with undefined.
|
||||
exec.signal = undefined
|
||||
return next()
|
||||
})
|
||||
}
|
||||
void observerContracts
|
||||
|
||||
const inferredTool = defineTool({
|
||||
name: 'signal-inference',
|
||||
description: 'Pins contextual signal inference.',
|
||||
parameters: {},
|
||||
async execute(_args, exec) {
|
||||
expectTypeOf(exec.signal).toEqualTypeOf<AbortSignal>()
|
||||
// @ts-expect-error -- defineTool contextually exposes a readonly signal.
|
||||
exec.signal = new AbortController().signal
|
||||
return []
|
||||
},
|
||||
})
|
||||
void inferredTool
|
||||
|
||||
describe('tool execution signal types', () => {
|
||||
it('requires an exact AbortSignal at every readonly tool view', () => {
|
||||
expectTypeOf<ToolExecutionInput['signal']>().toEqualTypeOf<AbortSignal>()
|
||||
expectTypeOf<ToolExecution['signal']>().toEqualTypeOf<AbortSignal>()
|
||||
expectTypeOf<ToolRunContext['signal']>().toEqualTypeOf<AbortSignal>()
|
||||
expectTypeOf<ToolDispatchExecution['signal']>().toEqualTypeOf<AbortSignal>()
|
||||
expectTypeOf<typeof inferredTool.execute>().toBeFunction()
|
||||
})
|
||||
})
|
||||
@@ -23,7 +23,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
|
||||
it('boots every shipped tool package and harvests its model-facing schemas', async () => {
|
||||
const catalog = await collectToolCatalog()
|
||||
const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
|
||||
expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'edit', 'glob', 'grep', 'read', 'run_code', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'web_fetch', 'web_search', 'workflow', 'write'])
|
||||
expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'lsp', 'ralph', 'read', 'run_code', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
|
||||
// Every tool carries a JSON-Schema `parameters` object (what the model sees).
|
||||
for (const entry of catalog) {
|
||||
for (const schema of entry.schemas) {
|
||||
|
||||
87
packages/core/tools/tests/invariant.spec.ts
Normal file
87
packages/core/tools/tests/invariant.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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 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'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
async function setup(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(InvariantService)
|
||||
await ctx.plugin(ToolsInvariant)
|
||||
return ctx
|
||||
}
|
||||
|
||||
const execution = (overrides: Partial<ToolExecution> = {}): ToolExecution => ({
|
||||
token: Symbol('tool') as ToolExecutionToken,
|
||||
callId: CallId('call-1'),
|
||||
name: 'echo',
|
||||
arguments: Object.freeze({ text: 'hi' }),
|
||||
...overrides,
|
||||
signal: overrides.signal ?? testToolSignal,
|
||||
})
|
||||
|
||||
const outcome = (): ToolExecutionResult => Object.freeze({
|
||||
content: Object.freeze([{ type: 'text' as const, text: 'ok' }]) as never,
|
||||
isError: false,
|
||||
})
|
||||
|
||||
function emitResult(ctx: Context, exec: ToolExecution, result: ToolExecutionResult): void {
|
||||
ctx.emit(scopeTarget(ctx as never, undefined), 'tools/result', exec, result)
|
||||
}
|
||||
|
||||
async function stage(ctx: Context, name: 'tools/pre-execute' | 'tools/execute', exec: ToolExecution): Promise<void> {
|
||||
if (name === 'tools/pre-execute') {
|
||||
await ctx.waterfall(ctx as never, name, exec, () => Promise.resolve({ kind: 'allow' as const }))
|
||||
} else {
|
||||
await ctx.waterfall(ctx as never, name, exec, () => Promise.resolve(outcome()))
|
||||
}
|
||||
}
|
||||
|
||||
describe('tool-pipeline invariants', () => {
|
||||
it('accepts dispatch and denial stage orders with frozen results', async () => {
|
||||
const ctx = await setup()
|
||||
const dispatched = execution()
|
||||
await stage(ctx, 'tools/pre-execute', dispatched)
|
||||
await stage(ctx, 'tools/execute', dispatched)
|
||||
await ctx.waterfall(ctx as never, 'tools/post-execute', dispatched, outcome(), () => Promise.resolve({ kind: 'accept' as const }))
|
||||
Object.freeze(dispatched)
|
||||
emitResult(ctx, dispatched, outcome())
|
||||
|
||||
const denied = execution({ callId: CallId('call-2') })
|
||||
await stage(ctx, 'tools/pre-execute', denied)
|
||||
await ctx.waterfall(ctx as never, 'tools/post-execute', denied, outcome(), () => Promise.resolve({ kind: 'accept' as const }))
|
||||
Object.freeze(denied)
|
||||
emitResult(ctx, denied, outcome())
|
||||
ctx.emit('tools/change')
|
||||
})
|
||||
|
||||
it('rejects repeated and out-of-order pipeline stages', async () => {
|
||||
const ctx = await setup()
|
||||
const exec = execution()
|
||||
await stage(ctx, 'tools/pre-execute', exec)
|
||||
await expect(stage(ctx, 'tools/pre-execute', exec)).rejects.toThrow(/repeated/)
|
||||
|
||||
const noPre = execution({ callId: CallId('call-2') })
|
||||
await expect(stage(ctx, 'tools/execute', noPre)).rejects.toThrow(/must follow tools\/pre-execute/)
|
||||
expect(() => ctx.waterfall(
|
||||
ctx as never, 'tools/post-execute', noPre, outcome(),
|
||||
() => Promise.resolve({ kind: 'accept' as const }),
|
||||
)).toThrow(/must follow tools\/pre-execute or tools\/execute/)
|
||||
})
|
||||
|
||||
it('rejects mutable or anonymous final snapshots', async () => {
|
||||
const ctx = await setup()
|
||||
expect(() => { emitResult(ctx, execution(), outcome()) }).toThrow(/execution must be frozen/)
|
||||
|
||||
const exec = Object.freeze(execution())
|
||||
expect(() => { emitResult(ctx, exec, { content: [], isError: false }) })
|
||||
.toThrow(/outcome and content must be frozen/)
|
||||
|
||||
const anonymous = Object.freeze(execution({ name: '' }))
|
||||
expect(() => { emitResult(ctx, anonymous, outcome()) }).toThrow(/non-empty name and callId/)
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,8 @@ import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
/** Mount the registry (with its systemPrompt dependency) on a fresh context. */
|
||||
async function mount(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
@@ -43,6 +45,7 @@ function tool(name: string, reply = `ran:${name}`): ToolDefinition {
|
||||
|
||||
async function run(ctx: Context, name: string, agent?: Agent): Promise<string> {
|
||||
const result = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('c1'),
|
||||
name,
|
||||
arguments: {},
|
||||
@@ -263,6 +266,49 @@ describe('scoped execution dispatch', () => {
|
||||
expect(bodyCalls).toBe(0)
|
||||
})
|
||||
|
||||
it('live-iterates a guard registered by an earlier guard', async () => {
|
||||
const ctx = await mount()
|
||||
const calls: string[] = []
|
||||
let added = false
|
||||
ctx.tools.register(tool('t'))
|
||||
ctx.tools.guard(() => {
|
||||
calls.push('first')
|
||||
if (!added) {
|
||||
added = true
|
||||
ctx.tools.guard(() => {
|
||||
calls.push('late')
|
||||
return 'late denial'
|
||||
})
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
expect(await run(ctx, 't')).toBe('Error: late denial')
|
||||
expect(calls).toEqual(['first', 'late'])
|
||||
})
|
||||
|
||||
it('defers a scoped guard that replaces the last guard in its generation', async () => {
|
||||
const ctx = await mount()
|
||||
const { scope, key } = await mintAgentScope(ctx, 'a')
|
||||
const calls: string[] = []
|
||||
ctx.tools.register(tool('t'))
|
||||
scope.ctx.tools.register(tool('scope_sibling'))
|
||||
const lift = scope.ctx.tools.guard(() => {
|
||||
calls.push('first')
|
||||
lift()
|
||||
scope.ctx.tools.guard(() => {
|
||||
calls.push('replacement')
|
||||
return 'replacement denial'
|
||||
})
|
||||
return undefined
|
||||
})
|
||||
|
||||
expect(await run(ctx, 't', key)).toBe('ran:t')
|
||||
expect(calls).toEqual(['first'])
|
||||
expect(await run(ctx, 't', key)).toBe('Error: replacement denial')
|
||||
expect(calls).toEqual(['first', 'replacement'])
|
||||
})
|
||||
|
||||
it('shares one token and materialized argument value across the pipeline', async () => {
|
||||
const ctx = await mount()
|
||||
const { scope, key } = await mintAgentScope(ctx, 'a')
|
||||
@@ -305,6 +351,7 @@ describe('scoped execution dispatch', () => {
|
||||
expect(await run(ctx, 'danger', key)).toBe('Error: danger denied')
|
||||
const callerArguments = { source: true }
|
||||
const safeResult = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('safe-call'),
|
||||
name: 'safe',
|
||||
arguments: callerArguments,
|
||||
@@ -348,7 +395,7 @@ describe('scoped execution dispatch', () => {
|
||||
if (exec.name === 'parent') parent = exec.token
|
||||
return next()
|
||||
})
|
||||
await ctx.tools.execute({ callId: CallId('parent'), name: 'parent', arguments: {} })
|
||||
await ctx.tools.execute({ signal: testToolSignal, callId: CallId('parent'), name: 'parent', arguments: {} })
|
||||
stopCapture()
|
||||
policyCalls = 0
|
||||
const signal = new AbortController().signal
|
||||
@@ -372,6 +419,7 @@ describe('scoped execution dispatch', () => {
|
||||
signal,
|
||||
})
|
||||
const subjectlessResult = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('non-cloneable-subjectless'),
|
||||
name: 't',
|
||||
arguments: { invalid: () => undefined },
|
||||
@@ -414,6 +462,7 @@ describe('scoped execution dispatch', () => {
|
||||
callId: CallId('stateful-parent'),
|
||||
name: 't',
|
||||
arguments: {},
|
||||
signal: testToolSignal,
|
||||
get parent(): ToolExecutionToken | undefined {
|
||||
parentReads += 1
|
||||
return parentReads === 1 ? undefined : forged
|
||||
@@ -438,7 +487,7 @@ describe('scoped execution dispatch', () => {
|
||||
if (exec.name === 'parent') parent = exec.token
|
||||
return next()
|
||||
})
|
||||
await ctx.tools.execute({ callId: CallId('parent'), name: 'parent', arguments: {} })
|
||||
await ctx.tools.execute({ signal: testToolSignal, callId: CallId('parent'), name: 'parent', arguments: {} })
|
||||
stopCapture()
|
||||
const acceptedSignal = new AbortController().signal
|
||||
const driftSignal = new AbortController().signal
|
||||
@@ -485,6 +534,7 @@ describe('scoped execution dispatch', () => {
|
||||
const input = {
|
||||
callId: CallId('throwing-arguments'),
|
||||
name: 't',
|
||||
signal: testToolSignal,
|
||||
get arguments(): unknown {
|
||||
argumentReads += 1
|
||||
throw new Error('getter exploded')
|
||||
@@ -525,6 +575,7 @@ describe('scoped execution dispatch', () => {
|
||||
})
|
||||
|
||||
const result = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('bad-arguments'), name: 't', arguments: argumentsValue,
|
||||
})
|
||||
|
||||
@@ -545,6 +596,7 @@ describe('scoped execution dispatch', () => {
|
||||
})
|
||||
|
||||
const result = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('unstable-arguments'), name: 't', arguments: argumentsValue,
|
||||
})
|
||||
|
||||
@@ -585,7 +637,7 @@ describe('scoped execution dispatch', () => {
|
||||
ctx.on('tools/result', () => Promise.reject(new Error('async observer failure')) as never)
|
||||
ctx.on('tools/result', (_exec, result) => { seen.push(result.isError) })
|
||||
|
||||
const result = await ctx.tools.execute({ callId: CallId('final'), name: 't', arguments: {}, agent: key })
|
||||
const result = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('final'), name: 't', arguments: {}, agent: key })
|
||||
await Promise.resolve()
|
||||
expect(result).toMatchObject({ isError: true, content: [{ type: 'text', text: 'outer failure' }] })
|
||||
expect(seen).toEqual([true, true])
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user