Merge remote-tracking branch 'origin/master' into codex/project-instruction-files
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.md # docs/event-producer-consumer.md # docs/persistence-catalog.md # docs/rfc/implemented/feature/2026-06-15-code-mode.md # docs/rfc/implemented/feature/2026-06-30-interception-seams.md # docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md # docs/tool-execution-pipeline.md # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-core/tests/agent-core.spec.ts # packages/core/agent-loop/src/agent.ts # packages/core/agent/README.md # packages/core/session/src/index.ts # packages/core/tools/README.md # packages/core/tools/src/index.ts # pnpm-lock.yaml # scripts/gen-doc-graphs.ts # scripts/type-equiv.manifest.json
This commit is contained in:
@@ -168,7 +168,7 @@ describe('agent loop', () => {
|
||||
it('resolves {{cwd}} from the agent session workspace (factory create with meta.cwd)', async () => {
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await harness(adapter, 'Working in {{cwd}}.')
|
||||
const handle = ctx.agents.create({
|
||||
const handle = await ctx.agents.create({
|
||||
agentId: AgentId('a-cwd'),
|
||||
sessionId: SessionId('s-cwd'),
|
||||
meta: { cwd: '/work/space' },
|
||||
@@ -243,6 +243,44 @@ describe('agent loop', () => {
|
||||
expect(adapter.requests[0]!.system).toBe('You are an AI agent powered by the DeepSeek Harness SDK.\n\nYou run on mock.')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['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) => {
|
||||
const adapter = new MockAdapter([
|
||||
toolCallResponse('bad-meta-call', 'bad-meta', {}, 'calling'),
|
||||
textResponse('recovered'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'bad-meta',
|
||||
description: 'returns invalid durable metadata',
|
||||
parameters: {},
|
||||
execute: () => Promise.resolve({ content: [{ type: 'text' as const, text: 'apparent success' }], meta }),
|
||||
}))
|
||||
const agent = ctx.agentLoop.create(AgentId('bad-meta-agent'), { model: 'mock' })
|
||||
|
||||
send(agent, 'use the tool')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const result = agent.session.events.find(event => event.type === 'tool/result')
|
||||
expect(result?.type).toBe('tool/result')
|
||||
if (result?.type === 'tool/result') {
|
||||
expect(result.data.callId).toBe('bad-meta-call')
|
||||
expect(result.data.isError).toBe(true)
|
||||
expect(result.data.meta).toBeUndefined()
|
||||
expect(result.data.content).toEqual([{
|
||||
type: 'text',
|
||||
text: 'Error: tool result must be losslessly JSON-serializable',
|
||||
}])
|
||||
}
|
||||
// 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')
|
||||
})
|
||||
|
||||
it('omits the system field when a system-prompt/assemble veto empties the assembly', async () => {
|
||||
// The documented escape valve: a deployment that must drop the harness
|
||||
// openers short-circuits the assemble waterfall; the request then carries
|
||||
@@ -798,10 +836,10 @@ describe('agent loop', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('stops the turn when a step/end session-event listener failure has recorded an error', async () => {
|
||||
it('contains a step/end observer failure without changing continuation', async () => {
|
||||
const adapter = new MockAdapter([
|
||||
toolCallResponse('c1', 'echo', { text: 'x' }),
|
||||
textResponse('should not run'),
|
||||
textResponse('continued after tool call'),
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
ctx.tools.register(defineTool({
|
||||
@@ -814,9 +852,8 @@ describe('agent loop', () => {
|
||||
}))
|
||||
const agent = ctx.agentLoop.create(AgentId('a1'), { model: 'mock' })
|
||||
let threw = false
|
||||
// A throwing step/end session-event listener is the surviving boundary-listener
|
||||
// failure path (step boundaries have no agent/* mirror): closeStep contains it
|
||||
// and surfaces it as a turn error rather than stranding the turn open.
|
||||
// Post-commit session observers cannot control the loop. The tool call still
|
||||
// drives the second model request, and the turn completes normally.
|
||||
ctx.on('session/event', (_session, event) => {
|
||||
if (event.type === 'step/end' && !threw) { threw = true; throw new Error('bad step/end listener') }
|
||||
})
|
||||
@@ -824,9 +861,9 @@ describe('agent loop', () => {
|
||||
send(agent, 'go')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
expect(adapter.requests).toHaveLength(2)
|
||||
const turnEnd = agent.session.events.findLast(e => e.type === 'turn/end')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind).toBe('error')
|
||||
expect(turnEnd?.type === 'turn/end' && turnEnd.data.reason.kind).toBe('completed')
|
||||
})
|
||||
|
||||
it('chains queued messages into consecutive turns', async () => {
|
||||
|
||||
Reference in New Issue
Block a user