Merge remote-tracking branch 'origin/master' into feat/send-unify
# Conflicts: # docs/persistence-catalog.md # examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl # packages/context/time-context/tests/time-context.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts
This commit is contained in:
@@ -6,7 +6,7 @@ import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
@@ -189,7 +189,7 @@ describe('AgentLoop initiator scope', () => {
|
||||
ctx.on('agent/turn-stop', (subject, _turn, signal) => {
|
||||
if (subject === agent) capture(signal)
|
||||
})
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'observe',
|
||||
description: 'observe explicit turn state',
|
||||
parameters: {},
|
||||
@@ -232,7 +232,7 @@ describe('AgentLoop initiator scope', () => {
|
||||
let parentWhileChildDriverActive: Agent | undefined
|
||||
let child: Agent | undefined
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'spawn-child',
|
||||
description: 'create one child agent',
|
||||
parameters: {},
|
||||
@@ -244,7 +244,7 @@ describe('AgentLoop initiator scope', () => {
|
||||
setup: (agentCtx) => {
|
||||
parentDuringSetup = ctx.agents.requireInitiator()
|
||||
explicitChild = agentCtx.agent
|
||||
agentCtx.tools.register(defineTool({
|
||||
agentCtx.tools.register(defineContentToolFixture({
|
||||
name: 'observe-child',
|
||||
description: 'observe child execution identity',
|
||||
parameters: {},
|
||||
@@ -292,7 +292,7 @@ describe('AgentLoop initiator scope', () => {
|
||||
let directAmbient: Agent | undefined
|
||||
let captured: Agent | undefined
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'agentless-probe',
|
||||
description: 'observe an agentless call',
|
||||
parameters: {},
|
||||
@@ -302,7 +302,7 @@ describe('AgentLoop initiator scope', () => {
|
||||
return [{ type: 'text', text: 'ok' }]
|
||||
},
|
||||
}))
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'capability-request',
|
||||
description: 'call the test capability transport',
|
||||
parameters: { path: { type: 'string' } },
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Context } from 'cordis'
|
||||
import LlmService, { type Message } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
|
||||
@@ -384,7 +384,7 @@ describe('Agent.cancel()', () => {
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
let executions = 0
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'danger',
|
||||
description: 'must not run after cancellation',
|
||||
parameters: {},
|
||||
@@ -971,7 +971,7 @@ describe('Agent.cancel()', () => {
|
||||
})
|
||||
break
|
||||
case 'tool':
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'blocked',
|
||||
description: 'wait for cancellation',
|
||||
parameters: {},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Context } from 'cordis'
|
||||
import LlmService, { CallId, ContentBlock, MessageSource, ProviderRequestId, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionEvent, SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool, TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH, type PostToolDecision } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture, TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH, type PostToolDecision } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent, type ContinuationDecision, type HookContext } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from '@deepseek-ai/dsh-agent-loop'
|
||||
import { prepareReactLoopAgent } from '../src/agent.ts'
|
||||
@@ -60,7 +60,7 @@ describe('session log records what agent/step-result actually produced', () => {
|
||||
const adapter = new MockAdapter([original, textResponse('done')])
|
||||
const ctx = await harness(adapter)
|
||||
const executed: string[] = []
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'injected-tool',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -229,7 +229,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const executed: string[] = []
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'aborter',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -250,7 +250,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
source: { kind: 'plugin', plugin: 'abort-test' },
|
||||
}],
|
||||
}))
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'second',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -334,7 +334,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
const adapter = new MockAdapter([toolCallResponse('c1', 'aborter', {})])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a-abort-injection'), { provider: 'mock', model: 'mock' })
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'aborter',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -381,7 +381,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
] satisfies StreamChunk[]])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a-later-abort-context'), { provider: 'mock', model: 'mock' })
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'first',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -389,7 +389,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
return [{ type: 'text', text: 'first done' }]
|
||||
},
|
||||
}))
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'aborter',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -431,7 +431,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
|
||||
agent = inner.agentLoop.create(SessionId('a-dispose-injection'), { provider: 'mock', model: 'mock' })
|
||||
}, { inject: ['agentLoop'] }))
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'waiter',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -483,7 +483,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a-historical-tool-pair'), { provider: 'mock', model: 'mock' })
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'aborter',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -492,7 +492,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
return [{ type: 'text', text: 'done' }]
|
||||
},
|
||||
}))
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'second',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -771,7 +771,7 @@ describe('adapter registration, routing, and accepted-input ownership', () => {
|
||||
const adapter = new MockAdapter([toolCallResponse('c1', 'noop', {}), textResponse('done')])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'noop',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -854,7 +854,7 @@ describe('adapter registration, routing, and accepted-input ownership', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('owned-steer'), { provider: 'mock', model: 'mock' })
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'gate',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -1501,7 +1501,7 @@ describe('tool result call identity', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: 'echo',
|
||||
parameters: { x: { type: 'number' } },
|
||||
|
||||
@@ -4,7 +4,7 @@ import LlmService, { CallId, LlmError, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -77,7 +77,7 @@ describe('tool JSON parse', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: 'echo tool',
|
||||
parameters: { input: { type: 'string' } },
|
||||
@@ -110,7 +110,7 @@ describe('tool JSON parse', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'noarg',
|
||||
description: 'no-arg tool',
|
||||
parameters: {},
|
||||
@@ -259,7 +259,7 @@ describe('structured tool error propagation (the runtime-validation Agent Note,
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'boom',
|
||||
description: 'always fails',
|
||||
parameters: {},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Context } from 'cordis'
|
||||
import LlmService, { CallId, type Message } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent, type ContinuationDecision, type PromptDecision, type SessionStartSource } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -398,7 +398,7 @@ describe('agent/session-prefix', () => {
|
||||
textResponse('again'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo', description: 'echo', parameters: { text: { type: 'string' } },
|
||||
async execute(args) { return [{ type: 'text', text: String(args.text) }] },
|
||||
}))
|
||||
@@ -520,7 +520,7 @@ describe('agent/session-prefix', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo', description: 'echo', parameters: { text: { type: 'string' } },
|
||||
async execute(args) { return [{ type: 'text', text: String(args.text) }] },
|
||||
}))
|
||||
@@ -573,7 +573,7 @@ describe('agent/turn-continuation (ContinuationDecision)', () => {
|
||||
it('a stop decision ends the turn even when the step had tool calls', async () => {
|
||||
const adapter = new MockAdapter([toolCallResponse('c1', 'echo', { text: 'hi' })])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo', description: 'echo', parameters: { text: { type: 'string' } },
|
||||
async execute(args) { return [{ type: 'text', text: String(args.text) }] },
|
||||
}))
|
||||
@@ -603,7 +603,7 @@ describe('tool additionalContexts buffering across a step', () => {
|
||||
]
|
||||
const adapter = new MockAdapter([twoCalls, textResponse('done')])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo', description: 'echo', parameters: { text: { type: 'string' } },
|
||||
async execute(args) { return [{ type: 'text', text: String(args.text) }] },
|
||||
}))
|
||||
@@ -644,7 +644,7 @@ describe('tool additionalContexts buffering across a step', () => {
|
||||
it('appends multiple contexts deferred by one composite tool after its outer result', async () => {
|
||||
const adapter = new MockAdapter([toolCallResponse('c1', 'composite', {}), textResponse('done')])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'composite', description: 'composite', parameters: {},
|
||||
async execute(_args, exec) {
|
||||
exec.deferContext({ content: [{ type: 'text', text: 'nested-a' }], source: { kind: 'plugin', plugin: 'a' }, meta: { order: 1 } })
|
||||
@@ -675,7 +675,7 @@ describe('tools/pre-execute gate (native-plugin permission pattern, end-to-end t
|
||||
const adapter = new MockAdapter([toolCallResponse('c1', 'danger', {}), textResponse('ok')])
|
||||
const ctx = await harness(adapter)
|
||||
let ran = false
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'danger', description: 'danger', parameters: {},
|
||||
async execute() { ran = true; return [{ type: 'text', text: 'should not run' }] },
|
||||
}))
|
||||
@@ -737,7 +737,7 @@ describe('worked example: a native hook plugin is just a cordis plugin on the se
|
||||
const adapter = new MockAdapter([toolCallResponse('c1', 'echo', { text: 'hi' }), textResponse('done')])
|
||||
const ctx = await harness(adapter)
|
||||
await ctx.plugin(NativeGuard)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo', description: 'echo', parameters: { text: { type: 'string' } },
|
||||
async execute(args) { return [{ type: 'text', text: String(args.text) }] },
|
||||
}))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import LlmService, { CallId, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import SessionStore, { SessionId, TurnEndReason, type JsonValue } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -89,7 +89,7 @@ describe('agent loop', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: 'echo back',
|
||||
parameters: { text: { type: 'string' } },
|
||||
@@ -118,22 +118,27 @@ describe('agent loop', () => {
|
||||
const types = agent.session.events.map(e => e.type)
|
||||
expect(types).toContain('tool/call')
|
||||
expect(types).toContain('tool/result')
|
||||
const durableResult = agent.session.events.find(event => event.type === 'tool/result')
|
||||
expect(durableResult?.type === 'tool/result' && 'value' in durableResult.data).toBe(false)
|
||||
})
|
||||
|
||||
it('threads a tool-attached meta (execute object return) onto the tool/result event', async () => {
|
||||
it('persists presentation metadata projected from the canonical value', async () => {
|
||||
const adapter = new MockAdapter([
|
||||
toolCallResponse('c1', 'writer', { path: 'a.txt' }, 'writing'),
|
||||
textResponse('done'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
// A tool that returns the { content, meta } object form: the loop must
|
||||
// persist `meta` on the tool/result event so a UI reproduces the card on replay.
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'writer',
|
||||
description: 'writes a file',
|
||||
parameters: { path: { type: 'string' } },
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: () => [{ type: 'text', text: 'ok' }],
|
||||
presentationMeta: (_args, value) => ({ diffs: [{ path: value, oldText: null, newText: 'x' }] }),
|
||||
},
|
||||
async execute() {
|
||||
return { content: [{ type: 'text', text: 'ok' }], meta: { diffs: [{ path: 'a.txt', oldText: null, newText: 'x' }] } }
|
||||
return 'a.txt'
|
||||
},
|
||||
}))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
@@ -152,7 +157,7 @@ describe('agent loop', () => {
|
||||
// projecting this agent's configured model, so the model knows its own name.
|
||||
const ctx = await harness(adapter, 'You are a test agent on {{model}}.')
|
||||
ctx.systemPrompt.section({ name: 'tool:noop', order: 100, text: 'Use the noop tool wisely.' })
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'noop',
|
||||
description: 'does nothing',
|
||||
parameters: {},
|
||||
@@ -248,7 +253,7 @@ describe('agent loop', () => {
|
||||
['BigInt', { n: 1n }],
|
||||
['Map', new Map([['key', 'value']])],
|
||||
['class instance', new (class ResultMeta { x = 1 })()],
|
||||
])('normalizes non-JSON tool meta (%s) before the durable result commit', async (_kind, meta) => {
|
||||
])('rejects non-JSON presentation metadata (%s) before the durable result commit', async (_kind, meta) => {
|
||||
const adapter = new MockAdapter([
|
||||
toolCallResponse('bad-meta-call', 'bad-meta', {}, 'calling'),
|
||||
textResponse('recovered'),
|
||||
@@ -258,7 +263,12 @@ describe('agent loop', () => {
|
||||
name: 'bad-meta',
|
||||
description: 'returns invalid durable metadata',
|
||||
parameters: {},
|
||||
execute: () => Promise.resolve({ content: [{ type: 'text' as const, text: 'apparent success' }], meta }),
|
||||
output: {
|
||||
schema: { type: 'string' },
|
||||
render: (_args, value) => [{ type: 'text', text: value }],
|
||||
presentationMeta: () => meta as unknown as JsonValue,
|
||||
},
|
||||
execute: () => Promise.resolve('apparent success'),
|
||||
}))
|
||||
const agent = ctx.agentLoop.create(SessionId('bad-meta-agent'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
@@ -271,15 +281,16 @@ describe('agent loop', () => {
|
||||
expect(result.data.callId).toBe('bad-meta-call')
|
||||
expect(result.data.isError).toBe(true)
|
||||
expect(result.data.meta).toBeUndefined()
|
||||
expect(result.data.error).toEqual({ name: 'ToolOutputError', code: 'INVALID_TOOL_OUTPUT' })
|
||||
expect(result.data.content).toEqual([{
|
||||
type: 'text',
|
||||
text: 'Error: tool result must be losslessly JSON-serializable',
|
||||
text: 'Error: tool "bad-meta" returned invalid output: output.presentationMeta returned non-lossless JSON',
|
||||
}])
|
||||
}
|
||||
// The normalized failure was durably logged and fed back to the model; the
|
||||
// turn continued normally instead of failing after an apparent success.
|
||||
expect(adapter.requests).toHaveLength(2)
|
||||
expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('losslessly JSON-serializable')
|
||||
expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('output.presentationMeta returned non-lossless JSON')
|
||||
})
|
||||
|
||||
it('omits the system field when a system-prompt/assemble veto empties the assembly', async () => {
|
||||
@@ -326,7 +337,7 @@ describe('agent loop', () => {
|
||||
const ctx = await harness(adapter)
|
||||
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'slow',
|
||||
description: '',
|
||||
parameters: {},
|
||||
@@ -432,7 +443,7 @@ describe('agent loop', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
let visibleDuringTool = false
|
||||
const meta = { kind: 'deferred-test', version: 1 }
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'noticer',
|
||||
description: 'injects a notice',
|
||||
parameters: {},
|
||||
@@ -494,7 +505,7 @@ describe('agent loop', () => {
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('invalid-context'), { provider: 'mock', model: 'mock' })
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'invalid-injector',
|
||||
description: 'attempts an invalid context injection',
|
||||
parameters: {},
|
||||
@@ -541,7 +552,7 @@ describe('agent loop', () => {
|
||||
it('agent/turn-continuation can veto continuation despite tool calls (budget-guard pattern)', async () => {
|
||||
const adapter = new MockAdapter([toolCallResponse('c1', 'echo', { text: 'x' })])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: '',
|
||||
parameters: { text: { type: 'string' } },
|
||||
@@ -589,7 +600,7 @@ describe('agent loop', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo', description: 'echo', parameters: {},
|
||||
async execute() { return [{ type: 'text', text: 'echoed' }] },
|
||||
}))
|
||||
@@ -781,7 +792,7 @@ describe('agent loop', () => {
|
||||
]])
|
||||
const ctx = await harness(adapter)
|
||||
let executions = 0
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: '',
|
||||
parameters: { text: { type: 'string' } },
|
||||
@@ -821,7 +832,7 @@ describe('agent loop', () => {
|
||||
{ type: 'finish', reason: { kind: 'max-tokens' } },
|
||||
]])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: '',
|
||||
parameters: { text: { type: 'string' } },
|
||||
@@ -908,7 +919,7 @@ describe('agent loop', () => {
|
||||
textResponse('continued after tool call'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: '',
|
||||
parameters: { text: { type: 'string' } },
|
||||
@@ -1240,7 +1251,7 @@ describe('agent loop', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: '',
|
||||
parameters: { text: { type: 'string' } },
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Context } from 'cordis'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -45,7 +45,7 @@ async function loopHarness(): Promise<Context> {
|
||||
await created.plugin(AgentRegistry)
|
||||
await created.plugin(AgentLoop, { agents: [] })
|
||||
await created.plugin(LlmDeepSeek)
|
||||
created.tools.register(defineTool({
|
||||
created.tools.register(defineContentToolFixture({
|
||||
name: 'lookup',
|
||||
description: 'Look up the stored value for a key.',
|
||||
parameters: { key: { type: 'string', description: 'The key to look up.' } },
|
||||
|
||||
@@ -11,7 +11,7 @@ import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -53,7 +53,7 @@ function expectPrefixExtension(previous: GenerateOptions, current: GenerateOptio
|
||||
}
|
||||
|
||||
function registerEcho(ctx: Context) {
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: 'echo back',
|
||||
parameters: { text: { type: 'string' } },
|
||||
|
||||
@@ -11,7 +11,7 @@ import LlmService, {
|
||||
import type { GenerateOptions, LlmFailure, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import type { PostToolDecision } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
@@ -133,7 +133,7 @@ describe('agent post-step and request-error lifecycle', () => {
|
||||
]
|
||||
const adapter = new FailureScriptAdapter([twoCalls, textResponse('done')])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'work',
|
||||
description: 'do work',
|
||||
parameters: {},
|
||||
@@ -225,7 +225,7 @@ describe('agent post-step and request-error lifecycle', () => {
|
||||
textResponse('must not continue'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'work',
|
||||
description: 'do work',
|
||||
parameters: {},
|
||||
@@ -538,7 +538,7 @@ describe('agent post-step and request-error lifecycle', () => {
|
||||
contextError('later overflow'),
|
||||
])
|
||||
const resetCtx = await harness(reset)
|
||||
resetCtx.tools.register(defineTool({
|
||||
resetCtx.tools.register(defineContentToolFixture({
|
||||
name: 'work',
|
||||
description: 'continue',
|
||||
parameters: {},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Context, symbols, type EffectMeta, type Fiber } from 'cordis'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
@@ -172,10 +172,10 @@ describe('agent scope lifecycle', () => {
|
||||
const handle = await ctx.agents.create({ sessionId: SessionId('s1'), agentOptions: { provider: 'mock', model: 'mock' } })
|
||||
const { agent } = handle
|
||||
agent.ctx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: 'You run tests.' })
|
||||
agent.ctx.tools.register({
|
||||
agent.ctx.tools.register(defineContentToolFixture({
|
||||
name: 'mine', description: 'scoped', parameters: {},
|
||||
execute: () => Promise.resolve(text('ran')),
|
||||
})
|
||||
}))
|
||||
|
||||
const scopedAssembly = await ctx.systemPrompt.assemble(assembleContextFor(agent))
|
||||
expect(scopedAssembly.sections.find(s => s.name === 'deployment:persona')?.text).toBe('You run tests.')
|
||||
@@ -587,12 +587,12 @@ describe('agent scope lifecycle', () => {
|
||||
sessionId: SessionId('dependency-origin-s'),
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
setup: (agentCtx) => {
|
||||
agentCtx.tools.register({
|
||||
agentCtx.tools.register(defineContentToolFixture({
|
||||
name: 'dependency-origin-tool',
|
||||
description: 'proves AgentLoop dependency origin',
|
||||
parameters: {},
|
||||
execute: () => Promise.resolve(text('ok')),
|
||||
})
|
||||
}))
|
||||
agentCtx.systemPrompt.section({
|
||||
name: 'dependency-origin-section',
|
||||
order: 1,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { CallId, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import ToolRegistry, { defineTool, TOOL_ABORTED_BEFORE_DISPATCH, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture, TOOL_ABORTED_BEFORE_DISPATCH, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { MockAdapter, textResponse } from './mock-adapter.ts'
|
||||
@@ -61,7 +61,7 @@ function multiCall(calls: { id: string; name: string; args: object }[]): StreamC
|
||||
function gatedTool(name: string, parallel: boolean) {
|
||||
const gates = new Map<string, () => void>()
|
||||
const started: string[] = []
|
||||
const tool = defineTool({
|
||||
const tool = defineContentToolFixture({
|
||||
name,
|
||||
description: `gated ${name}`,
|
||||
parameters: { id: { type: 'string', required: true } },
|
||||
@@ -123,12 +123,12 @@ describe('tool-call scheduler: grouping and barriers', () => {
|
||||
textResponse('done'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'r', description: 'read', parameters: { id: { type: 'string', required: true } },
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args) { order.push(`r-start-${args.id}`); order.push(`r-end-${args.id}`); return [{ type: 'text', text: 'r' }] },
|
||||
}))
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'w', description: 'write', parameters: { id: { type: 'string', required: true } },
|
||||
async execute(args) { order.push(`w-${args.id}`); return [{ type: 'text', text: 'w' }] },
|
||||
}))
|
||||
@@ -150,14 +150,14 @@ describe('tool-call scheduler: grouping and barriers', () => {
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
const replacement = gatedExclusiveTool('x')
|
||||
const disposeSafe = ctx.tools.register(defineTool({
|
||||
const disposeSafe = ctx.tools.register(defineContentToolFixture({
|
||||
name: 'x',
|
||||
description: 'initially safe',
|
||||
parameters: { id: { type: 'string', required: true } },
|
||||
isConcurrencySafe: () => true,
|
||||
async execute(args) { return [{ type: 'text', text: `old-${args.id}` }] },
|
||||
}))
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'replace',
|
||||
description: 'replace x',
|
||||
parameters: { id: { type: 'string', required: true } },
|
||||
@@ -539,10 +539,14 @@ describe('tool-call scheduler: abort handling', () => {
|
||||
.toEqual([CallId('c1'), CallId('c2'), CallId('c3'), CallId('c4')])
|
||||
expect(events(agent).filter(e => e.type === 'tool/result').map(e => e.data.callId))
|
||||
.toEqual([CallId('c1'), CallId('c2'), CallId('c3'), CallId('c4')])
|
||||
expect(events(agent).filter(e => e.type === 'tool/result').slice(-2).map(e => e.data))
|
||||
expect(events(agent).filter(e => e.type === 'tool/result').slice(-2).map(e => ({
|
||||
callId: e.data.callId,
|
||||
isError: e.data.isError,
|
||||
errorInfo: e.data.error,
|
||||
})))
|
||||
.toEqual([
|
||||
expect.objectContaining({ callId: CallId('c3'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }),
|
||||
expect.objectContaining({ callId: CallId('c4'), isError: true, error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } }),
|
||||
{ callId: CallId('c3'), isError: true, errorInfo: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } },
|
||||
{ callId: CallId('c4'), isError: true, errorInfo: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH } },
|
||||
])
|
||||
const settled = events(agent).filter(e => e.type === 'tool/result'
|
||||
|| (e.type === 'user/message' && e.data.source.kind === 'plugin'))
|
||||
@@ -566,7 +570,7 @@ describe('tool-call scheduler: abort handling', () => {
|
||||
const gated = gatedParallelTool('p')
|
||||
const exclusive: string[] = []
|
||||
ctx.tools.register(gated.tool)
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'x',
|
||||
description: 'exclusive',
|
||||
parameters: { id: { type: 'string', required: true } },
|
||||
|
||||
@@ -12,7 +12,7 @@ import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt, { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -42,7 +42,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
}
|
||||
|
||||
function registerNamed(ctx: Context, name: string) {
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name,
|
||||
description: `the ${name} tool`,
|
||||
parameters: {},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Context } from 'cordis'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId, type TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { type Agent, type ContinuationStop } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -39,7 +39,7 @@ function send(agent: Agent, text = 'go'): Promise<void> {
|
||||
}
|
||||
|
||||
function registerEcho(ctx: Context): void {
|
||||
ctx.tools.register(defineTool({
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'echo',
|
||||
description: 'echo',
|
||||
parameters: { text: { type: 'string' } },
|
||||
|
||||
Reference in New Issue
Block a user