fix: complete immutable message migration
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -3056,6 +3056,8 @@ describe('dynamic nested workspace context injection', () => {
|
|||||||
expect(blocksText(workspaceContextOf(result)?.content)).toContain('nested package rule')
|
expect(blocksText(workspaceContextOf(result)?.content)).toContain('nested package rule')
|
||||||
expect(blocksText(workspaceContextOf(result)?.content)).not.toContain('downstream context')
|
expect(blocksText(workspaceContextOf(result)?.content)).not.toContain('downstream context')
|
||||||
expect(result.additionalContexts?.[1]).toEqual({
|
expect(result.additionalContexts?.[1]).toEqual({
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
content: [{ type: 'text', text: 'downstream context' }],
|
content: [{ type: 'text', text: 'downstream context' }],
|
||||||
source: { kind: 'plugin', plugin: 'downstream' },
|
source: { kind: 'plugin', plugin: 'downstream' },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -338,8 +338,10 @@ describe('Agent.cancel()', () => {
|
|||||||
const result = agent.session.events.find(event => event.type === 'tool/result')
|
const result = agent.session.events.find(event => event.type === 'tool/result')
|
||||||
expect(call?.type === 'tool/call' ? call.data.callId : undefined).toBe('c1')
|
expect(call?.type === 'tool/call' ? call.data.callId : undefined).toBe('c1')
|
||||||
expect(result?.type === 'tool/result' ? result.data : undefined).toMatchObject({
|
expect(result?.type === 'tool/result' ? result.data : undefined).toMatchObject({
|
||||||
callId: 'c1',
|
message: {
|
||||||
isError: true,
|
source: { kind: 'tool', callId: 'c1' },
|
||||||
|
content: [{ type: 'tool-result', toolCallId: 'c1', isError: true }],
|
||||||
|
},
|
||||||
error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH },
|
error: { name: 'AbortError', code: TOOL_ABORTED_BEFORE_DISPATCH },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -744,9 +744,24 @@ describe('agent loop', () => {
|
|||||||
expect(steps).toBe(2)
|
expect(steps).toBe(2)
|
||||||
expect(adapter.requests).toHaveLength(2)
|
expect(adapter.requests).toHaveLength(2)
|
||||||
expect(adapter.requests[1]!.messages).toEqual([
|
expect(adapter.requests[1]!.messages).toEqual([
|
||||||
{ role: 'user', content: [{ type: 'text', text: 'go' }] },
|
{
|
||||||
{ role: 'assistant', content: [{ type: 'text', text: 'first half' }], provenance: { provider: 'mock', model: 'mock' } },
|
id: expect.any(String) as unknown,
|
||||||
{ role: 'user', content: [{ type: 'text', text: 'continue after truncation' }] },
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'go' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'assistant',
|
||||||
|
content: [{ type: 'text', text: 'first half' }],
|
||||||
|
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'continue after truncation' }],
|
||||||
|
source: { kind: 'plugin', plugin: 'max-tokens-test' },
|
||||||
|
},
|
||||||
])
|
])
|
||||||
expect(reasons).toEqual([{ kind: 'max-tokens' }])
|
expect(reasons).toEqual([{ kind: 'max-tokens' }])
|
||||||
})
|
})
|
||||||
@@ -799,13 +814,26 @@ describe('agent loop', () => {
|
|||||||
|
|
||||||
expect(executions).toBe(0)
|
expect(executions).toBe(0)
|
||||||
expect(agent.session.events.some(e => e.type === 'tool/call')).toBe(false)
|
expect(agent.session.events.some(e => e.type === 'tool/call')).toBe(false)
|
||||||
expect(agent.session.deriveMessages()).toEqual([{ role: 'user', content: [{ type: 'text', text: 'go' }] }])
|
expect(agent.session.deriveMessages()).toEqual([{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'go' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
}])
|
||||||
expect(reasons).toEqual([{ kind: 'max-tokens' }])
|
expect(reasons).toEqual([{ kind: 'max-tokens' }])
|
||||||
// Empty content still needs an assistant/message to carry usage; derivation
|
// Empty content still needs an assistant/message to carry usage; derivation
|
||||||
// skips that host so it does not create a spurious assistant turn.
|
// skips that host so it does not create a spurious assistant turn.
|
||||||
const assistantMessage = agent.session.events.find(e => e.type === 'assistant/message')
|
const assistantMessage = agent.session.events.find(e => e.type === 'assistant/message')
|
||||||
expect(assistantMessage?.type === 'assistant/message' && assistantMessage.data).toEqual({
|
expect(assistantMessage?.type === 'assistant/message' && assistantMessage.data).toEqual({
|
||||||
turn: 1, step: 1, content: [], provenance: { provider: 'mock', model: 'mock' }, usage: { inputTokens: 10, outputTokens: 5 },
|
turn: 1,
|
||||||
|
step: 1,
|
||||||
|
message: {
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'assistant',
|
||||||
|
content: [],
|
||||||
|
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||||
|
},
|
||||||
|
usage: { inputTokens: 10, outputTokens: 5 },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -839,11 +867,20 @@ describe('agent loop', () => {
|
|||||||
expect(assistant.type === 'assistant/message' && assistant.data).toEqual({
|
expect(assistant.type === 'assistant/message' && assistant.data).toEqual({
|
||||||
turn: 1,
|
turn: 1,
|
||||||
step: 1,
|
step: 1,
|
||||||
content: [],
|
message: {
|
||||||
provenance: { provider: 'mock', model: 'mock' },
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'assistant',
|
||||||
|
content: [],
|
||||||
|
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(assistant.sourceEventSeqs?.length).toBeGreaterThan(0)
|
expect(assistant.sourceEventSeqs?.length).toBeGreaterThan(0)
|
||||||
expect(agent.session.deriveMessages()).toEqual([{ role: 'user', content: [{ type: 'text', text: 'go' }] }])
|
expect(agent.session.deriveMessages()).toEqual([{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'go' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('appends an empty completion anchor for a normal stop with no usage', async () => {
|
it('appends an empty completion anchor for a normal stop with no usage', async () => {
|
||||||
@@ -864,11 +901,20 @@ describe('agent loop', () => {
|
|||||||
expect(assistant.type === 'assistant/message' && assistant.data).toEqual({
|
expect(assistant.type === 'assistant/message' && assistant.data).toEqual({
|
||||||
turn: 1,
|
turn: 1,
|
||||||
step: 1,
|
step: 1,
|
||||||
content: [],
|
message: {
|
||||||
provenance: { provider: 'mock', model: 'mock' },
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'assistant',
|
||||||
|
content: [],
|
||||||
|
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(assistant.sourceEventSeqs?.length).toBe(1)
|
expect(assistant.sourceEventSeqs?.length).toBe(1)
|
||||||
expect(agent.session.deriveMessages()).toEqual([{ role: 'user', content: [{ type: 'text', text: 'go' }] }])
|
expect(agent.session.deriveMessages()).toEqual([{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'go' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps safe max-tokens assistant content while dropping truncated tool calls', async () => {
|
it('keeps safe max-tokens assistant content while dropping truncated tool calls', async () => {
|
||||||
@@ -889,8 +935,18 @@ describe('agent loop', () => {
|
|||||||
|
|
||||||
expect(agent.session.events.some(e => e.type === 'tool/call')).toBe(false)
|
expect(agent.session.events.some(e => e.type === 'tool/call')).toBe(false)
|
||||||
expect(agent.session.deriveMessages()).toEqual([
|
expect(agent.session.deriveMessages()).toEqual([
|
||||||
{ role: 'user', content: [{ type: 'text', text: 'go' }] },
|
{
|
||||||
{ role: 'assistant', content: [{ type: 'text', text: 'partial text' }], provenance: { provider: 'mock', model: 'mock' } },
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'go' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'assistant',
|
||||||
|
content: [{ type: 'text', text: 'partial text' }],
|
||||||
|
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||||
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,12 @@ describe('SessionStore.fork', () => {
|
|||||||
|
|
||||||
expect(child.events).toEqual(source.events.slice(0, firstBoundary + 1))
|
expect(child.events).toEqual(source.events.slice(0, firstBoundary + 1))
|
||||||
expect(child.header.seedLength).toBe(firstBoundary + 1)
|
expect(child.header.seedLength).toBe(firstBoundary + 1)
|
||||||
expect(child.deriveMessages()).toEqual([{ role: 'user', content: [{ type: 'text', text: 'first' }] }])
|
expect(child.deriveMessages()).toEqual([{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'first' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('accepts every turn/end reason as an explicit fork boundary', async () => {
|
it('accepts every turn/end reason as an explicit fork boundary', async () => {
|
||||||
|
|||||||
@@ -998,6 +998,8 @@ describe('the run_code dispatch bridge', () => {
|
|||||||
|
|
||||||
expect(result.isError).toBe(true)
|
expect(result.isError).toBe(true)
|
||||||
expect(result.additionalContexts).toEqual([{
|
expect(result.additionalContexts).toEqual([{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
content: [{ type: 'text', text: 'nested context' }],
|
content: [{ type: 'text', text: 'nested context' }],
|
||||||
source: { kind: 'plugin', plugin: 'test' },
|
source: { kind: 'plugin', plugin: 'test' },
|
||||||
}])
|
}])
|
||||||
|
|||||||
@@ -385,7 +385,12 @@ describe('ToolRegistry', () => {
|
|||||||
value: { text: 'policy value' },
|
value: { text: 'policy value' },
|
||||||
content: [{ type: 'text', text: 'render:policy value' }],
|
content: [{ type: 'text', text: 'render:policy value' }],
|
||||||
meta: { projected: 'policy value' },
|
meta: { projected: 'policy value' },
|
||||||
additionalContexts: [{ content: [{ type: 'text', text: 'value context' }], source: { kind: 'plugin', plugin: 'test' } }],
|
additionalContexts: [{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'value context' }],
|
||||||
|
source: { kind: 'plugin', plugin: 'test' },
|
||||||
|
}],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -518,7 +523,12 @@ describe('ToolRegistry', () => {
|
|||||||
error: { message: 'wrapped failure' },
|
error: { message: 'wrapped failure' },
|
||||||
content: [{ type: 'text', text: 'wrapper content' }],
|
content: [{ type: 'text', text: 'wrapper content' }],
|
||||||
meta: { wrapped: true },
|
meta: { wrapped: true },
|
||||||
additionalContexts: [{ content: [{ type: 'text', text: 'wrapper context' }], source: { kind: 'plugin', plugin: 'test' } }],
|
additionalContexts: [{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'wrapper context' }],
|
||||||
|
source: { kind: 'plugin', plugin: 'test' },
|
||||||
|
}],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1820,6 +1830,8 @@ describe('ToolRegistry', () => {
|
|||||||
callId: CallId('around-context'), name: 'echo', arguments: {},
|
callId: CallId('around-context'), name: 'echo', arguments: {},
|
||||||
})
|
})
|
||||||
expect(result.additionalContexts).toEqual([{
|
expect(result.additionalContexts).toEqual([{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
content: [{ type: 'text', text: 'from around dispatch' }],
|
content: [{ type: 'text', text: 'from around dispatch' }],
|
||||||
source: { kind: 'plugin', plugin: 'test' },
|
source: { kind: 'plugin', plugin: 'test' },
|
||||||
}])
|
}])
|
||||||
|
|||||||
@@ -367,7 +367,12 @@ describe('dsh-agent-spine-demo bundle', () => {
|
|||||||
handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }))
|
handle.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' } }))
|
||||||
await waitForIdle(ctx, handle.agent)
|
await waitForIdle(ctx, handle.agent)
|
||||||
|
|
||||||
expect(adapter.requests[0]?.messages).toEqual([{ role: 'user', content: [{ type: 'text', text: 'hi' }] }])
|
expect(adapter.requests[0]?.messages).toEqual([{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'hi' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
}])
|
||||||
await handle.dispose()
|
await handle.dispose()
|
||||||
await ctx.fiber.dispose()
|
await ctx.fiber.dispose()
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ describe('GoalService creation and replay', () => {
|
|||||||
if (change === undefined) throw new Error('expected decoded goal change')
|
if (change === undefined) throw new Error('expected decoded goal change')
|
||||||
expect(change).toMatchObject({ operation: 'create', goal: { id: goal.id } })
|
expect(change).toMatchObject({ operation: 'create', goal: { id: goal.id } })
|
||||||
expect(context.data.content).toEqual(renderGoalChange(change))
|
expect(context.data.content).toEqual(renderGoalChange(change))
|
||||||
expect(session.deriveMessages()).toEqual([{ role: 'user', content: context.data.content }])
|
expect(session.deriveMessages()).toEqual([context.data])
|
||||||
expect(foldGoal(session.events)).toMatchObject({ goal: { id: goal.id }, roundsStarted: 0 })
|
expect(foldGoal(session.events)).toMatchObject({ goal: { id: goal.id }, roundsStarted: 0 })
|
||||||
vi.useRealTimers()
|
vi.useRealTimers()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -265,8 +265,8 @@ describe('session/queued frames', () => {
|
|||||||
|
|
||||||
const liveFrames = (await liveCollected).filter(f => f.type === 'session/queued')
|
const liveFrames = (await liveCollected).filter(f => f.type === 'session/queued')
|
||||||
expect(liveFrames).toEqual([
|
expect(liveFrames).toEqual([
|
||||||
{ type: 'session/queued', sessionId: agent.id, content: queued.content, source: { kind: 'user' }, steering: false },
|
{ type: 'session/queued', sessionId: agent.id, message: queued, steering: false },
|
||||||
{ type: 'session/queued', sessionId: agent.id, content: steering.content, source: { kind: 'user' }, steering: true },
|
{ type: 'session/queued', sessionId: agent.id, message: steering, steering: true },
|
||||||
])
|
])
|
||||||
|
|
||||||
// A fresh mux connection replays the still-pending entries as its baseline.
|
// A fresh mux connection replays the still-pending entries as its baseline.
|
||||||
@@ -308,6 +308,6 @@ describe('session/queued frames', () => {
|
|||||||
api.events.mux({ rpcId: RpcId('t-mux-swept'), payload: {} }, abort.signal), 2, abort)
|
api.events.mux({ rpcId: RpcId('t-mux-swept'), payload: {} }, abort.signal), 2, abort)
|
||||||
const remaining = frames.filter(f => f.type === 'session/queued')
|
const remaining = frames.filter(f => f.type === 'session/queued')
|
||||||
expect(remaining).toHaveLength(1)
|
expect(remaining).toHaveLength(1)
|
||||||
expect(remaining[0]).toMatchObject({ content: survivor.content })
|
expect(remaining[0]).toMatchObject({ message: survivor })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|||||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||||
import { CallId, createMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
import { CallId, createMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||||
import type { Session, SessionId } from '@deepseek-ai/dsh-session'
|
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
||||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||||
import type { MuxFrame, RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api'
|
import type { MuxFrame, RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||||
@@ -111,7 +111,12 @@ describe('mux live view computation', () => {
|
|||||||
const events = frames.filter(f => f.type === 'session/event')
|
const events = frames.filter(f => f.type === 'session/event')
|
||||||
const byCall = new Map(events
|
const byCall = new Map(events
|
||||||
.filter(f => f.event.type === 'tool/call' || f.event.type === 'tool/result')
|
.filter(f => f.event.type === 'tool/call' || f.event.type === 'tool/result')
|
||||||
.map(f => [`${f.event.type}:${(f.event.data as { callId: string }).callId}`, f]))
|
.map(f => [
|
||||||
|
`${f.event.type}:${f.event.type === 'tool/call'
|
||||||
|
? f.event.data.callId
|
||||||
|
: (f.event.data as SessionEvent<'tool/result'>['data']).message.source.callId}`,
|
||||||
|
f,
|
||||||
|
]))
|
||||||
|
|
||||||
expect(byCall.get('tool/call:c-gen')?.view).toEqual({ for: 'call', view: { card: 'generic', title: 'gen call' } })
|
expect(byCall.get('tool/call:c-gen')?.view).toEqual({ for: 'call', view: { card: 'generic', title: 'gen call' } })
|
||||||
expect(byCall.get('tool/call:c-term')?.view).toEqual({ for: 'call', view: { card: 'terminal', title: 'echo hi' } })
|
expect(byCall.get('tool/call:c-term')?.view).toEqual({ for: 'call', view: { card: 'terminal', title: 'echo hi' } })
|
||||||
@@ -189,7 +194,12 @@ describe('mux live view computation', () => {
|
|||||||
const entries = response.result.value.events
|
const entries = response.result.value.events
|
||||||
const byKey = new Map(entries
|
const byKey = new Map(entries
|
||||||
.filter(entry => entry.event.type === 'tool/call' || entry.event.type === 'tool/result')
|
.filter(entry => entry.event.type === 'tool/call' || entry.event.type === 'tool/result')
|
||||||
.map(entry => [`${entry.event.type}:${(entry.event.data as { callId: string }).callId}`, entry]))
|
.map(entry => [
|
||||||
|
`${entry.event.type}:${entry.event.type === 'tool/call'
|
||||||
|
? entry.event.data.callId
|
||||||
|
: (entry.event.data as SessionEvent<'tool/result'>['data']).message.source.callId}`,
|
||||||
|
entry,
|
||||||
|
]))
|
||||||
expect(byKey.get('tool/call:h-term')?.view).toEqual({ for: 'call', view: { card: 'terminal', title: 'ls' } })
|
expect(byKey.get('tool/call:h-term')?.view).toEqual({ for: 'call', view: { card: 'terminal', title: 'ls' } })
|
||||||
expect(byKey.get('tool/result:h-term')?.view).toEqual({ for: 'result', view: { card: 'terminal', output: 'done' } })
|
expect(byKey.get('tool/result:h-term')?.view).toEqual({ for: 'result', view: { card: 'terminal', output: 'done' } })
|
||||||
expect('view' in (byKey.get('tool/result:h-orphan') ?? {})).toBe(false)
|
expect('view' in (byKey.get('tool/result:h-orphan') ?? {})).toBe(false)
|
||||||
|
|||||||
@@ -319,8 +319,8 @@ describe('events frame schemas', () => {
|
|||||||
{ type: 'approval/resolved', sessionId: 's', approvalId: 'a', outcome: 'allowed-once' },
|
{ type: 'approval/resolved', sessionId: 's', approvalId: 'a', outcome: 'allowed-once' },
|
||||||
{ type: 'question/requested', sessionId: 's', questions: [{ id: 'q', question: 'Q?', options: [{ label: 'L' }], multiSelect: true }] },
|
{ type: 'question/requested', sessionId: 's', questions: [{ id: 'q', question: 'Q?', options: [{ label: 'L' }], multiSelect: true }] },
|
||||||
{ type: 'question/resolved', sessionId: 's', questionRpcId: 'r', outcome: 'answered' },
|
{ type: 'question/resolved', sessionId: 's', questionRpcId: 'r', outcome: 'answered' },
|
||||||
{ type: 'session/queued', sessionId: 's', content: [{ type: 'text', text: 'queued prompt' }], source: { kind: 'user', rpcId: 'r9' }, steering: false },
|
{ type: 'session/queued', sessionId: 's', message: { id: 'm1', role: 'user', content: [{ type: 'text', text: 'queued prompt' }], source: { kind: 'user', rpcId: 'r9' } }, steering: false },
|
||||||
{ type: 'session/queued', sessionId: 's', content: [{ type: 'text', text: 'steer' }], source: { kind: 'user' }, steering: true },
|
{ type: 'session/queued', sessionId: 's', message: { id: 'm2', role: 'user', content: [{ type: 'text', text: 'steer' }], source: { kind: 'user' } }, steering: true },
|
||||||
{ type: 'stream/error', error: { code: 'internal', message: 'm', details: {} } },
|
{ type: 'stream/error', error: { code: 'internal', message: 'm', details: {} } },
|
||||||
]
|
]
|
||||||
for (const frame of frames) expect(muxFrameSchema.parse(frame)).toMatchObject({ type: frame.type })
|
for (const frame of frames) expect(muxFrameSchema.parse(frame)).toMatchObject({ type: frame.type })
|
||||||
@@ -340,9 +340,9 @@ describe('events frame schemas', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('rejects a queued frame missing its members', () => {
|
it('rejects a queued frame missing its members', () => {
|
||||||
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', content: 'x', source: { kind: 'user' } })).toThrow()
|
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', message: 'x', steering: false })).toThrow()
|
||||||
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', content: [], source: { kind: 'user' } })).toThrow()
|
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', message: { id: 'm', role: 'user', content: [], source: { kind: 'user' } } })).toThrow()
|
||||||
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', content: [], source: {}, steering: false })).toThrow()
|
expect(() => muxFrameSchema.parse({ type: 'session/queued', sessionId: 's', message: { id: 'm', role: 'user', content: [], source: {} }, steering: false })).toThrow()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('accepts every host frame branch', () => {
|
it('accepts every host frame branch', () => {
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ describe('toPiContext', () => {
|
|||||||
it.each([
|
it.each([
|
||||||
['provider', { ...validReplay, provider: 'openai' }],
|
['provider', { ...validReplay, provider: 'openai' }],
|
||||||
['model', { ...validReplay, model: 'deepseek-v4-pro' }],
|
['model', { ...validReplay, model: 'deepseek-v4-pro' }],
|
||||||
])('rejects replay metadata whose %s differs from assistant provenance', (field, replayState) => {
|
])('rejects replay metadata whose %s differs from assistant source', (field, replayState) => {
|
||||||
try {
|
try {
|
||||||
toPiContext({
|
toPiContext({
|
||||||
provider: 'deepseek',
|
provider: 'deepseek',
|
||||||
@@ -382,7 +382,7 @@ describe('toPiContext', () => {
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
expect(error).toBeInstanceOf(LlmError)
|
expect(error).toBeInstanceOf(LlmError)
|
||||||
expect((error as LlmError).code).toBe('INVALID_REPLAY_STATE')
|
expect((error as LlmError).code).toBe('INVALID_REPLAY_STATE')
|
||||||
expect((error as Error).message).toContain(`${field} does not match assistant provenance`)
|
expect((error as Error).message).toContain(`${field} does not match assistant source`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -216,9 +216,10 @@ describe('provider-routed retry policy', () => {
|
|||||||
expect(agent.session.events.filter(item => item.type === 'step/start').map(item => item.data))
|
expect(agent.session.events.filter(item => item.type === 'step/start').map(item => item.data))
|
||||||
.toEqual([{ turn: 1, step: 1 }, { turn: 2, step: 1 }])
|
.toEqual([{ turn: 1, step: 1 }, { turn: 2, step: 1 }])
|
||||||
expect(agent.session.deriveMessages().at(-1)).toEqual({
|
expect(agent.session.deriveMessages().at(-1)).toEqual({
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: [{ type: 'text', text: 'done' }],
|
content: [{ type: 'text', text: 'done' }],
|
||||||
provenance: { provider: 'mock', model: 'mock' },
|
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -296,7 +297,7 @@ describe('provider-routed retry policy', () => {
|
|||||||
expect(agent.session.deriveMessages().at(-1)).toMatchObject({
|
expect(agent.session.deriveMessages().at(-1)).toMatchObject({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: [{ type: 'text', text: 'recovered' }],
|
content: [{ type: 'text', text: 'recovered' }],
|
||||||
provenance: { provider: 'mock', model: 'mock' },
|
source: { kind: 'model', provider: 'mock', model: 'mock' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -128,8 +128,8 @@ export function createAssistantMessage(
|
|||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: input.content,
|
content: input.content,
|
||||||
source: {
|
source: {
|
||||||
...input.source,
|
|
||||||
kind: 'model',
|
kind: 'model',
|
||||||
|
...input.source,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -527,6 +527,8 @@ describe('/plan', () => {
|
|||||||
})
|
})
|
||||||
expect(ctx.planMode.get(messageAgent)).toEqual({ active: false, pending: true })
|
expect(ctx.planMode.get(messageAgent)).toEqual({ active: false, pending: true })
|
||||||
expect(messageSteer).toHaveBeenCalledExactlyOnceWith({
|
expect(messageSteer).toHaveBeenCalledExactlyOnceWith({
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
content: [{ type: 'text', text: 'draft the migration' }],
|
content: [{ type: 'text', text: 'draft the migration' }],
|
||||||
source: { kind: 'user' },
|
source: { kind: 'user' },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createUserMessage, createMessage } from '@deepseek-ai/dsh-llm'
|
import { MessageId, createUserMessage, createMessage } from '@deepseek-ai/dsh-llm'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { Context } from 'cordis'
|
import { Context } from 'cordis'
|
||||||
import { appendFile, mkdtemp, mkdir, rm, readFile, writeFile, readdir, stat, symlink } from 'node:fs/promises'
|
import { appendFile, mkdtemp, mkdir, rm, readFile, writeFile, readdir, stat, symlink } from 'node:fs/promises'
|
||||||
@@ -1291,9 +1291,18 @@ describe('SessionPersistenceJsonl: edge cases', () => {
|
|||||||
it('rejects non-JSON event data: BigInt, function, circular, Map, undefined property', async () => {
|
it('rejects non-JSON event data: BigInt, function, circular, Map, undefined property', async () => {
|
||||||
const m = meta('serial')
|
const m = meta('serial')
|
||||||
await ctx.sessionPersistence.create(m)
|
await ctx.sessionPersistence.create(m)
|
||||||
const bad = (extra: unknown) => [{ type: 'user/message', seq: 0, time: 1, data: createUserMessage({
|
const bad = (extra: unknown) => [{
|
||||||
content: [{ type: 'text', text: 'x' }], source: { kind: 'user' }, extra,
|
type: 'user/message',
|
||||||
}) }] as unknown as SessionEvent[]
|
seq: 0,
|
||||||
|
time: 1,
|
||||||
|
data: {
|
||||||
|
id: MessageId('invalid-json'),
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'x' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
extra,
|
||||||
|
},
|
||||||
|
}] as unknown as SessionEvent[]
|
||||||
await expect(ctx.sessionPersistence.append(m.id, bad(1n))).rejects.toThrow(/non-JSON-serializable/)
|
await expect(ctx.sessionPersistence.append(m.id, bad(1n))).rejects.toThrow(/non-JSON-serializable/)
|
||||||
await expect(ctx.sessionPersistence.append(m.id, bad(() => 0))).rejects.toThrow(/non-JSON-serializable/)
|
await expect(ctx.sessionPersistence.append(m.id, bad(() => 0))).rejects.toThrow(/non-JSON-serializable/)
|
||||||
await expect(ctx.sessionPersistence.append(m.id, bad(Symbol('s')))).rejects.toThrow(/non-JSON-serializable/)
|
await expect(ctx.sessionPersistence.append(m.id, bad(Symbol('s')))).rejects.toThrow(/non-JSON-serializable/)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { SESSION_FORMAT_VERSION, Session, SessionId, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '@deepseek-ai/dsh-session'
|
import { SESSION_FORMAT_VERSION, Session, SessionId, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '@deepseek-ai/dsh-session'
|
||||||
import type { SessionEvent, SessionHeader, SurfaceEventType, SurfaceIntent } from '@deepseek-ai/dsh-session'
|
import type { SessionEvent, SessionHeader, SurfaceEventType, SurfaceIntent } from '@deepseek-ai/dsh-session'
|
||||||
import { createUserMessage, CallId , createMessage } from '@deepseek-ai/dsh-llm'
|
import { CallId, MessageId, createMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
|
||||||
import type { SessionPersistence } from '../src/index.ts'
|
import type { SessionPersistence } from '../src/index.ts'
|
||||||
|
|
||||||
/** A backend under test plus its teardown. */
|
/** A backend under test plus its teardown. */
|
||||||
@@ -34,13 +34,16 @@ export function meta(id: string, cwd?: string): SessionHeader {
|
|||||||
export function oneTurnLog(): SessionEvent[] {
|
export function oneTurnLog(): SessionEvent[] {
|
||||||
return [
|
return [
|
||||||
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
|
{ type: 'turn/start', seq: 0, time: 1, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
|
||||||
{ type: 'user/message', seq: 1, time: 2, data: createUserMessage({
|
{ type: 'user/message', seq: 1, time: 2, data: freezeMessage({
|
||||||
|
id: MessageId('one-turn-user'),
|
||||||
|
role: 'user',
|
||||||
content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
|
content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
|
||||||
}), surfaceOp: 'append' },
|
}), surfaceOp: 'append' },
|
||||||
{ type: 'step/start', seq: 2, time: 3, data: { turn: 1, step: 1 } },
|
{ type: 'step/start', seq: 2, time: 3, data: { turn: 1, step: 1 } },
|
||||||
{ type: 'assistant/message', seq: 3, time: 4, data: {
|
{ type: 'assistant/message', seq: 3, time: 4, data: {
|
||||||
turn: 1, step: 1,
|
turn: 1, step: 1,
|
||||||
message: createMessage({
|
message: freezeMessage({
|
||||||
|
id: MessageId('one-turn-assistant'),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: [{ type: 'text', text: 'hello' }],
|
content: [{ type: 'text', text: 'hello' }],
|
||||||
source: {
|
source: {
|
||||||
@@ -201,7 +204,11 @@ export function runPersistenceContract(name: string, make: () => Promise<Contrac
|
|||||||
])
|
])
|
||||||
const synthetic = loaded.events.find(e => e.type === 'tool/result')
|
const synthetic = loaded.events.find(e => e.type === 'tool/result')
|
||||||
expect(synthetic?.type === 'tool/result' && synthetic.data).toMatchObject({
|
expect(synthetic?.type === 'tool/result' && synthetic.data).toMatchObject({
|
||||||
callId: CallId('call-x'), isError: true, error: { code: TOOL_NOT_STARTED },
|
message: {
|
||||||
|
source: { kind: 'tool', callId: CallId('call-x') },
|
||||||
|
content: [{ type: 'tool-result', toolCallId: CallId('call-x'), isError: true }],
|
||||||
|
},
|
||||||
|
error: { code: TOOL_NOT_STARTED },
|
||||||
})
|
})
|
||||||
// The synthetic result carries the SAME callId as the orphaned tool-call,
|
// The synthetic result carries the SAME callId as the orphaned tool-call,
|
||||||
// so deriveMessages() pairs them — no provider-invalid dangling call.
|
// so deriveMessages() pairs them — no provider-invalid dangling call.
|
||||||
@@ -365,9 +372,18 @@ export function runPersistenceContract(name: string, make: () => Promise<Contrac
|
|||||||
const mi = meta(`s5-${i}`)
|
const mi = meta(`s5-${i}`)
|
||||||
await persistence.create(mi)
|
await persistence.create(mi)
|
||||||
const events = [
|
const events = [
|
||||||
{ type: 'user/message', seq: 0, time: 1, data: createUserMessage({
|
{
|
||||||
content: [{ type: 'text', text: 'x' }], source: { kind: 'user' }, extra: bad,
|
type: 'user/message',
|
||||||
}) },
|
seq: 0,
|
||||||
|
time: 1,
|
||||||
|
data: {
|
||||||
|
id: MessageId(`invalid-json-${i}`),
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'x' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
extra: bad,
|
||||||
|
},
|
||||||
|
},
|
||||||
] as unknown as SessionEvent[]
|
] as unknown as SessionEvent[]
|
||||||
await expect(persistence.append(mi.id, events)).rejects.toThrow(/losslessly JSON-serializable/)
|
await expect(persistence.append(mi.id, events)).rejects.toThrow(/losslessly JSON-serializable/)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
materializeSessionResultFilters,
|
materializeSessionResultFilters,
|
||||||
} from './filters.ts'
|
} from './filters.ts'
|
||||||
import * as tracing from './tracing.ts'
|
import * as tracing from './tracing.ts'
|
||||||
|
import { snapshotEvent } from './snapshot.ts'
|
||||||
|
|
||||||
export type * from './types.ts'
|
export type * from './types.ts'
|
||||||
export { SessionSearchCursor } from './cursor.ts'
|
export { SessionSearchCursor } from './cursor.ts'
|
||||||
@@ -146,7 +147,7 @@ export abstract class SessionQueryService extends Service {
|
|||||||
new Session(sessionId, loaded.events, loaded.header)
|
new Session(sessionId, loaded.events, loaded.header)
|
||||||
return {
|
return {
|
||||||
session: structuredClone(loaded.header),
|
session: structuredClone(loaded.header),
|
||||||
events: loaded.events.map(event => structuredClone(event)),
|
events: loaded.events.map(snapshotEvent),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,10 +331,13 @@ export abstract class SessionQueryService extends Service {
|
|||||||
}
|
}
|
||||||
const startSeq = Math.max(0, seq - before)
|
const startSeq = Math.max(0, seq - before)
|
||||||
const endSeq = Math.min(loaded.events.length - 1, seq + after)
|
const endSeq = Math.min(loaded.events.length - 1, seq + after)
|
||||||
|
const targetSnapshot = snapshotEvent(target)
|
||||||
|
const events = loaded.events.slice(startSeq, endSeq + 1)
|
||||||
|
.map(event => event === target ? targetSnapshot : snapshotEvent(event))
|
||||||
return {
|
return {
|
||||||
session: loaded.header,
|
session: structuredClone(loaded.header),
|
||||||
target,
|
target: targetSnapshot,
|
||||||
events: loaded.events.slice(startSeq, endSeq + 1),
|
events,
|
||||||
startSeq,
|
startSeq,
|
||||||
endSeq,
|
endSeq,
|
||||||
}
|
}
|
||||||
|
|||||||
27
packages/session-query/session-query/src/snapshot.ts
Normal file
27
packages/session-query/session-query/src/snapshot.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/** Detached session-query snapshots that preserve message immutability. */
|
||||||
|
|
||||||
|
import { deepFreeze } from '@deepseek-ai/dsh-llm'
|
||||||
|
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone one event while retaining the invariant that every identified message is frozen.
|
||||||
|
* @param event - source event from one corpus observation.
|
||||||
|
* @returns a detached event whose message value, if any, is deeply frozen.
|
||||||
|
*/
|
||||||
|
export function snapshotEvent<T extends SessionEvent>(event: T): T {
|
||||||
|
const snapshot = structuredClone(event)
|
||||||
|
switch (snapshot.type) {
|
||||||
|
case 'user/message':
|
||||||
|
deepFreeze(snapshot.data)
|
||||||
|
break
|
||||||
|
case 'assistant/message':
|
||||||
|
case 'tool/result':
|
||||||
|
case 'steering/message':
|
||||||
|
deepFreeze(snapshot.data.message)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// SessionEventMap is merge-extensible; plugin-owned log-only events carry no core message.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return snapshot
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
SessionLineageTrace,
|
SessionLineageTrace,
|
||||||
SessionRecord,
|
SessionRecord,
|
||||||
} from './types.ts'
|
} from './types.ts'
|
||||||
|
import { snapshotEvent } from './snapshot.ts'
|
||||||
|
|
||||||
interface EventLogAnalysis {
|
interface EventLogAnalysis {
|
||||||
records: SessionEventRecord[]
|
records: SessionEventRecord[]
|
||||||
@@ -51,7 +52,7 @@ export function currentSurfaceEvents(
|
|||||||
'SESSION_QUERY_INVALID_SURFACE',
|
'SESSION_QUERY_INVALID_SURFACE',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return structuredClone(event)
|
return snapshotEvent(event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,9 @@ describe('dsh-tool-skill', () => {
|
|||||||
|
|
||||||
expect(prefix).toEqual([
|
expect(prefix).toEqual([
|
||||||
{
|
{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
|
source: { kind: 'plugin', plugin: 'dsh-tool-skill' },
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: [
|
text: [
|
||||||
@@ -167,7 +169,12 @@ describe('dsh-tool-skill', () => {
|
|||||||
].join('\n'),
|
].join('\n'),
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
{ role: 'user', content: [{ type: 'text', text: 'later contribution' }] },
|
{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
|
content: [{ type: 'text', text: 'later contribution' }],
|
||||||
|
source: { kind: 'plugin', plugin: 'later-contribution' },
|
||||||
|
},
|
||||||
])
|
])
|
||||||
const rendered = JSON.stringify(prefix[0])
|
const rendered = JSON.stringify(prefix[0])
|
||||||
expect(rendered).not.toContain('whenToUse')
|
expect(rendered).not.toContain('whenToUse')
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ describe('in-process structured output', () => {
|
|||||||
const child = ctx.agents.get(run.id)!
|
const child = ctx.agents.get(run.id)!
|
||||||
const results = child.session.events.filter(e => e.type === 'tool/result')
|
const results = child.session.events.filter(e => e.type === 'tool/result')
|
||||||
expect(results.length).toBe(2)
|
expect(results.length).toBe(2)
|
||||||
expect((results[0]!.data as { isError?: boolean }).isError).toBe(true)
|
expect(results[0]!.data.message.content[0].isError).toBe(true)
|
||||||
await run.dispose()
|
await run.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -439,9 +439,12 @@ export function unknownToolCallIds(rawLog: string): string[] {
|
|||||||
if (record.type !== 'tool/result') return []
|
if (record.type !== 'tool/result') return []
|
||||||
const data = record.data
|
const data = record.data
|
||||||
if (data === null || typeof data !== 'object') return []
|
if (data === null || typeof data !== 'object') return []
|
||||||
const { source, error } = data as { source?: unknown; error?: unknown }
|
const { message, error } = data as { message?: unknown; error?: unknown }
|
||||||
if (error === null || typeof error !== 'object') return []
|
if (error === null || typeof error !== 'object') return []
|
||||||
if ((error as { code?: unknown }).code !== 'UNKNOWN_TOOL') return []
|
if ((error as { code?: unknown }).code !== 'UNKNOWN_TOOL') return []
|
||||||
|
const source = typeof message === 'object' && message !== null
|
||||||
|
? (message as { source?: unknown }).source
|
||||||
|
: undefined
|
||||||
const callId = typeof source === 'object' && source !== null
|
const callId = typeof source === 'object' && source !== null
|
||||||
? (source as { callId?: unknown }).callId
|
? (source as { callId?: unknown }).callId
|
||||||
: undefined
|
: undefined
|
||||||
|
|||||||
@@ -432,8 +432,8 @@ describe('tool-schema snapshots', () => {
|
|||||||
describe('unknownToolCallIds', () => {
|
describe('unknownToolCallIds', () => {
|
||||||
it('returns structured UNKNOWN_TOOL call ids and ignores other results', () => {
|
it('returns structured UNKNOWN_TOOL call ids and ignores other results', () => {
|
||||||
const log = [
|
const log = [
|
||||||
'{"type":"tool/result","data":{"callId":"missing","error":{"code":"UNKNOWN_TOOL"}}}',
|
'{"type":"tool/result","data":{"message":{"source":{"kind":"tool","callId":"missing"}},"error":{"code":"UNKNOWN_TOOL"}}}',
|
||||||
'{"type":"tool/result","data":{"callId":"failed","error":{"code":"EXECUTION_FAILED"}}}',
|
'{"type":"tool/result","data":{"message":{"source":{"kind":"tool","callId":"failed"}},"error":{"code":"EXECUTION_FAILED"}}}',
|
||||||
'{"type":"tool/result","data":null}',
|
'{"type":"tool/result","data":null}',
|
||||||
'{"type":"tool/result","data":"invalid"}',
|
'{"type":"tool/result","data":"invalid"}',
|
||||||
'{"type":"tool/result","data":{"error":null}}',
|
'{"type":"tool/result","data":{"error":null}}',
|
||||||
@@ -446,7 +446,7 @@ describe('unknownToolCallIds', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns no failures for ordinary tool results', () => {
|
it('returns no failures for ordinary tool results', () => {
|
||||||
expect(unknownToolCallIds('{"type":"tool/result","data":{"callId":"ok"}}\n')).toEqual([])
|
expect(unknownToolCallIds('{"type":"tool/result","data":{"message":{"source":{"kind":"tool","callId":"ok"}}}}\n')).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -457,6 +457,8 @@ describe('completion notices', () => {
|
|||||||
await tick()
|
await tick()
|
||||||
expect(inject).toHaveBeenCalledTimes(1)
|
expect(inject).toHaveBeenCalledTimes(1)
|
||||||
expect(inject).toHaveBeenCalledWith({
|
expect(inject).toHaveBeenCalledWith({
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
content: [{ type: 'text', text: 'background task bash-1 (bash: pnpm test) finished [status: completed, exit code: 0]. Read its output with task_output.' }],
|
content: [{ type: 'text', text: 'background task bash-1 (bash: pnpm test) finished [status: completed, exit code: 0]. Read its output with task_output.' }],
|
||||||
source: { kind: 'plugin', plugin: 'tool-tasks' },
|
source: { kind: 'plugin', plugin: 'tool-tasks' },
|
||||||
})
|
})
|
||||||
@@ -479,6 +481,8 @@ describe('completion notices', () => {
|
|||||||
expect(inject).toHaveBeenNthCalledWith(
|
expect(inject).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
|
id: expect.any(String) as unknown,
|
||||||
|
role: 'user',
|
||||||
content: [{ type: 'text', text: 'background task subagent-1\n[notice truncated]\nDone; task_output.' }],
|
content: [{ type: 'text', text: 'background task subagent-1\n[notice truncated]\nDone; task_output.' }],
|
||||||
source: { kind: 'plugin', plugin: 'tool-tasks' },
|
source: { kind: 'plugin', plugin: 'tool-tasks' },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2365,7 +2365,10 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
|||||||
type: 'user/message',
|
type: 'user/message',
|
||||||
seq: 0,
|
seq: 0,
|
||||||
time: 1,
|
time: 1,
|
||||||
data: { content: [{ type: 'text', text: 'source background' }], source: { kind: 'user' } },
|
data: createUserMessage({
|
||||||
|
content: [{ type: 'text', text: 'source background' }],
|
||||||
|
source: { kind: 'user' },
|
||||||
|
}),
|
||||||
surfaceOp: 'append',
|
surfaceOp: 'append',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user