Merge latest master into codex/llm-error-recovery-rfc
# Conflicts: # docs/architecture.md # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/event-producer-consumer.md # docs/module-graph.md # docs/persistence-catalog.md # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-loop/src/loop.ts # packages/core/agent/src/types.ts # packages/examples/agent-spine-demo/README.md # packages/examples/stdio-demo/README.md # packages/examples/stdio-demo/src/index.ts # packages/llm/llm-deepseek/README.md # packages/llm/llm-deepseek/src/adapter.ts # packages/llm/llm-deepseek/tests/adapter.spec.ts # packages/llm/llm/README.md # packages/llm/llm/src/error.ts # packages/llm/llm/tests/service.spec.ts # packages/sandbox/sandbox-policy/tsconfig.json # packages/ui/stdio/README.md # packages/ui/stdio/src/index.ts # packages/ui/stdio/tests/stdio.spec.ts # packages/ui/tui/src/index.ts # pnpm-lock.yaml # website/zh-CN/api/harness/events.md # website/zh-CN/api/harness/llm.md
This commit is contained in:
@@ -132,6 +132,22 @@ describe('streamSessionEventUpdate', () => {
|
||||
expect((failed[0] as { status: string }).status).toBe('failed')
|
||||
})
|
||||
|
||||
it('emits no execution update for a tool-result surface replacement', () => {
|
||||
const replacement = {
|
||||
...evt('tool/result', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
callId: CallId('c1'),
|
||||
content: [{ type: 'text', text: '[... tool result middle pruned ...]' }],
|
||||
isError: false,
|
||||
}),
|
||||
seq: 2,
|
||||
surfaceOp: { op: 'replace', start: 1, end: 1 },
|
||||
sourceEventSeqs: [1],
|
||||
} as SessionEvent
|
||||
expect(updatesFor(replacement)).toEqual([])
|
||||
})
|
||||
|
||||
it('drops non-text tool-result content (text-only)', () => {
|
||||
const update = updatesFor(evt('tool/result', {
|
||||
turn: 1, step: 1, callId: CallId('c1'),
|
||||
@@ -477,6 +493,16 @@ describe('terminal-card mapping (capability-gated)', () => {
|
||||
|
||||
const callEvent = evt('tool/call', { turn: 1, step: 1, callId: CallId('c1'), name: 'bash', arguments: JSON.stringify({ command: 'echo hi', description: 'Greet' }) })
|
||||
const resultEvent = evt('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'hi\n' }], isError: false })
|
||||
const prunedResultEvent = {
|
||||
...resultEvent,
|
||||
seq: 2,
|
||||
data: {
|
||||
...resultEvent.data,
|
||||
content: [{ type: 'text', text: '[... tool result middle pruned ...]' }],
|
||||
},
|
||||
surfaceOp: { op: 'replace', start: 1, end: 1 },
|
||||
sourceEventSeqs: [1],
|
||||
} as SessionEvent
|
||||
|
||||
function termUpdates(tool: ToolDefinition, enabled: boolean, cwd: string | undefined, ...events: SessionEvent[]): SessionNotification['update'][] {
|
||||
const presenter = new ToolPresenter(registryOf(tool))
|
||||
@@ -504,6 +530,27 @@ describe('terminal-card mapping (capability-gated)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('live/replay translation preserves the original terminal completion across a pruning rewrite', () => {
|
||||
const updates = termUpdates(
|
||||
termTool({ card: 'terminal' }, { output: 'hi\n', exitCode: 0 }),
|
||||
true,
|
||||
'/work/proj',
|
||||
callEvent,
|
||||
resultEvent,
|
||||
prunedResultEvent,
|
||||
)
|
||||
expect(updates).toHaveLength(2)
|
||||
expect(updates[1]).toEqual({
|
||||
sessionUpdate: 'tool_call_update',
|
||||
toolCallId: 'c1',
|
||||
status: 'completed',
|
||||
_meta: {
|
||||
terminal_output: { terminal_id: 'c1', data: 'hi\n' },
|
||||
terminal_exit: { terminal_id: 'c1', exit_code: 0 },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('capability ON: an ABSOLUTE tool cwd wins; a RELATIVE one resolves against the session cwd', () => {
|
||||
const [absCall] = termUpdates(termTool({ card: 'terminal', cwd: '/explicit/abs' }, { output: 'x' }), true, '/work/proj', callEvent)
|
||||
expect((absCall as unknown as { _meta: { terminal_info: { cwd: string } } })._meta.terminal_info.cwd).toBe('/explicit/abs')
|
||||
@@ -660,17 +707,38 @@ describe('result-time diff card (REAL fs edit tool → tool_call_update diff blo
|
||||
// call-time snippet, then the tool/result carries the tool's computed applied-hunk `meta`,
|
||||
// which presentResult narrows into a `diff` result card the bridge forwards as `{ type:
|
||||
// 'diff' }` content blocks. The real tool is required because its result metadata is the contract.
|
||||
it('forwards the applied-hunk meta onto the wire as tool_call_update diff content', async () => {
|
||||
it('live/replay translation keeps the applied diff when a pruning rewrite follows', async () => {
|
||||
const ctx = await fsCtx()
|
||||
const presenter = new ToolPresenter(ctx.tools)
|
||||
const args = JSON.stringify({ file_path: 'src/b.ts', old_string: 'OLD', new_string: 'NEW' })
|
||||
// The applied hunk the tool would compute and persist on the result meta.
|
||||
const meta = { diffs: [{ path: 'src/b.ts', oldText: 'a\nOLD\nb', newText: 'a\nNEW\nb' }] }
|
||||
const [, resultUpdate] = updatesWith(
|
||||
const originalResult = evt('tool/result', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
callId: CallId('e1'),
|
||||
content: [{ type: 'text', text: 'ok' }],
|
||||
isError: false,
|
||||
meta,
|
||||
})
|
||||
const replacement = {
|
||||
...originalResult,
|
||||
seq: 3,
|
||||
data: {
|
||||
...originalResult.data,
|
||||
content: [{ type: 'text', text: '[... tool result middle pruned ...]' }],
|
||||
},
|
||||
surfaceOp: { op: 'replace', start: 2, end: 2 },
|
||||
sourceEventSeqs: [2],
|
||||
} as SessionEvent
|
||||
const updates = updatesWith(
|
||||
presenter,
|
||||
evt('tool/call', { turn: 1, step: 1, callId: CallId('e1'), name: 'edit', arguments: args }),
|
||||
evt('tool/result', { turn: 1, step: 1, callId: CallId('e1'), content: [{ type: 'text', text: 'ok' }], isError: false, meta }),
|
||||
originalResult,
|
||||
replacement,
|
||||
)
|
||||
expect(updates).toHaveLength(2)
|
||||
const resultUpdate = updates[1]
|
||||
expect(resultUpdate).toEqual({
|
||||
sessionUpdate: 'tool_call_update',
|
||||
toolCallId: 'e1',
|
||||
|
||||
Reference in New Issue
Block a user