mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-05 04:00:17 +00:00
perf(session): reuse immutable event snapshots
This commit is contained in:
@@ -67,7 +67,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('coding task: fix a failing test
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// The agent claims success…
|
||||
const summary = finalText([...agent.session.snapshotEvents()]).toLowerCase()
|
||||
const summary = finalText(agent.session.snapshotEvents()).toLowerCase()
|
||||
expect(summary.length).toBeGreaterThan(0)
|
||||
|
||||
// …and the world agrees: the test passes when WE run it, and the test
|
||||
|
||||
@@ -56,7 +56,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('compaction: a long session compa
|
||||
}], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const events = [...agent.session.snapshotEvents()]
|
||||
const events = agent.session.snapshotEvents()
|
||||
|
||||
// A compaction ran: the start…end bracket landed in the real log.
|
||||
const starts = events.filter(e => e.type === 'compaction/start')
|
||||
|
||||
@@ -34,7 +34,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('full loop: real model + real bas
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Run `echo e2e-ok` with the bash tool and tell me its exact output.' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const events = [...agent.session.snapshotEvents()]
|
||||
const events = agent.session.snapshotEvents()
|
||||
const calls = events.filter(event => event.type === 'tool/call')
|
||||
expect(calls.length).toBeGreaterThan(0)
|
||||
expect(calls.some(event => event.data.name === 'bash')).toBe(true)
|
||||
|
||||
@@ -96,7 +96,7 @@ export function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
export function finalText(events: SessionEvent[]): string {
|
||||
export function finalText(events: readonly SessionEvent[]): string {
|
||||
const message = events.findLast(event => event.type === 'assistant/message')
|
||||
if (message?.type !== 'assistant/message') return ''
|
||||
return message.data.message.content
|
||||
|
||||
@@ -366,7 +366,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('PTC mode: real model writes a pr
|
||||
+ 'and return only the joined string.',
|
||||
}], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
const events: SessionEvent[] = [...agent.session.snapshotEvents()]
|
||||
const events: readonly SessionEvent[] = agent.session.snapshotEvents()
|
||||
|
||||
// The wire contract: every request this session made offered EXACTLY ONE
|
||||
// tool — run_code (the logged header snapshots the assembled list).
|
||||
@@ -418,7 +418,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('PTC mode: real model writes a pr
|
||||
}], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, handle.agent)
|
||||
|
||||
const events: SessionEvent[] = [...handle.agent.session.snapshotEvents()]
|
||||
const events: readonly SessionEvent[] = handle.agent.session.snapshotEvents()
|
||||
const dispatch = events.find(event => event.type === 'tool/code-dispatch' && event.data.name === 'read')
|
||||
const outerResult = events.find(event => event.type === 'tool/result')
|
||||
const workspaceContext = await vi.waitFor(() => {
|
||||
|
||||
@@ -63,6 +63,6 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses
|
||||
await waitForIdle(ctx, resumed)
|
||||
|
||||
// The model recalls it — only possible from the resumed history.
|
||||
expect(finalText([...resumed.session.snapshotEvents()])).toContain(SECRET)
|
||||
expect(finalText(resumed.session.snapshotEvents())).toContain(SECRET)
|
||||
}, 180_000)
|
||||
})
|
||||
|
||||
@@ -37,7 +37,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('todo_write: real model records a
|
||||
+ 'Send all three in one todo_write call, then reply with the single word DONE.' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const events = [...agent.session.snapshotEvents()]
|
||||
const events = agent.session.snapshotEvents()
|
||||
|
||||
// The model actually called the tool.
|
||||
const calls = events.filter(event => event.type === 'tool/call')
|
||||
|
||||
@@ -48,7 +48,7 @@ import type {
|
||||
interface SessionReadState {
|
||||
readonly id: SessionId
|
||||
readonly header: SessionHeader
|
||||
readonly events: SessionEvent[]
|
||||
readonly events: readonly SessionEvent[]
|
||||
}
|
||||
|
||||
/** Implements Session business commands delegated by the Session Controller Remote service. */
|
||||
@@ -484,7 +484,7 @@ export class SessionCommandController {
|
||||
private async readSessionState(sessionId: SessionId): Promise<SessionReadState> {
|
||||
const attached = this.ctx.sessions.get(sessionId)
|
||||
if (attached !== undefined) {
|
||||
return { id: attached.id, header: attached.header, events: [...attached.snapshotEvents()] }
|
||||
return { id: attached.id, header: attached.header, events: attached.snapshotEvents() }
|
||||
}
|
||||
const inspected = await inspectApiSession(this.ctx, sessionId)
|
||||
return { id: inspected.meta.id, header: inspected.meta, events: inspected.events }
|
||||
|
||||
@@ -191,10 +191,10 @@ export class SessionController extends TypertRemoteService {
|
||||
inspect(
|
||||
sessionId: SessionId,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
|
||||
): Promise<{ meta: SessionHeader; events: readonly SessionEvent[] }> {
|
||||
const attached = this.ctx.sessions.get(sessionId)
|
||||
if (attached !== undefined) {
|
||||
return Promise.resolve({ meta: attached.header, events: [...attached.snapshotEvents()] })
|
||||
return Promise.resolve({ meta: attached.header, events: attached.snapshotEvents() })
|
||||
}
|
||||
return inspectApiSession(this.ctx, sessionId, signal)
|
||||
}
|
||||
|
||||
@@ -882,7 +882,7 @@ describe('compaction region transaction', () => {
|
||||
expect(head.content[0]?.type === 'text' ? head.content[0].text : '').toContain('<compacted-summary>')
|
||||
expect(head.content.at(-1)).toEqual({ type: 'text', text: '</compacted-summary>' })
|
||||
|
||||
const replay = Session.create(SessionId('replay'), [...session.snapshotEvents()])
|
||||
const replay = Session.create(SessionId('replay'), session.snapshotEvents())
|
||||
expect(replay.deriveMessages()).toEqual(session.deriveMessages())
|
||||
})
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
function overflowHistorySeed(): SessionEvent[] {
|
||||
function overflowHistorySeed(): readonly SessionEvent[] {
|
||||
const session = Session.create(SessionId('overflow-history-seed'))
|
||||
for (let turn = 1; turn <= 2; turn += 1) {
|
||||
const sentinel = turn === 1 ? 'OLD HISTORY SENTINEL' : 'RECENT HISTORY'
|
||||
@@ -215,7 +215,7 @@ function overflowHistorySeed(): SessionEvent[] {
|
||||
session.append('step/end', { turn, step: 1 })
|
||||
session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
||||
}
|
||||
return [...session.snapshotEvents()]
|
||||
return session.snapshotEvents()
|
||||
}
|
||||
|
||||
describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', () => {
|
||||
@@ -250,7 +250,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', ()
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'do tool work' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const events = [...agent.session.snapshotEvents()]
|
||||
const events = agent.session.snapshotEvents()
|
||||
const compactStart = events.find(event => event.type === 'compaction/start')
|
||||
expect(compactStart).toBeDefined()
|
||||
const precedingResult = events.findLast(event =>
|
||||
@@ -282,7 +282,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', ()
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'do a long multi-step task' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const events = [...agent.session.snapshotEvents()]
|
||||
const events = agent.session.snapshotEvents()
|
||||
// A compaction ran: at least one checkpoint landed on the surface.
|
||||
const checkpoints = events.filter(
|
||||
(e): e is SurfaceEvent =>
|
||||
@@ -356,7 +356,7 @@ describe('context-overflow recovery across the real loop and compaction-basic',
|
||||
expect(retry).toContain('RECOVERY CHECKPOINT')
|
||||
expect(retry).not.toContain('OLD HISTORY SENTINEL')
|
||||
|
||||
const events = [...agent.session.snapshotEvents()]
|
||||
const events = agent.session.snapshotEvents()
|
||||
const stepStart = events.find(event =>
|
||||
event.type === 'step/start' && event.data.turn === 3 && event.data.step === 1,
|
||||
)!
|
||||
|
||||
@@ -442,7 +442,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
compactionId: CompactionId('stale-manual-compaction'),
|
||||
turn: null,
|
||||
})
|
||||
const reloaded = Session.create(SessionId('stale-orphan'), [...original.snapshotEvents()])
|
||||
const reloaded = Session.create(SessionId('stale-orphan'), original.snapshotEvents())
|
||||
const boundary = reloaded.snapshotEvents().findLast(event => event.type === 'session/end-seed')
|
||||
const orphan = reloaded.snapshotEvents().find(event => event.type === 'compaction/start')
|
||||
const agent = fakeAgent(reloaded, () => () => undefined)
|
||||
@@ -461,7 +461,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
})
|
||||
original.append('turn/start', { turn: 3 })
|
||||
original.append('turn/end', { turn: 3, reason: { kind: 'interrupted' } })
|
||||
const reloaded = Session.create(SessionId('reloaded-orphan'), [...original.snapshotEvents()])
|
||||
const reloaded = Session.create(SessionId('reloaded-orphan'), original.snapshotEvents())
|
||||
const agent = fakeAgent(reloaded, () => () => undefined)
|
||||
|
||||
await expect(compact.compactNow(agent, SIGNAL)).resolves.not.toBeNull()
|
||||
@@ -716,7 +716,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
const reserve = vi.fn(() => testCase.release)
|
||||
const measure = vi.spyOn(ctx.tokenMeter, 'measure')
|
||||
const agent = fakeAgent(testCase.session, reserve)
|
||||
const before = [...testCase.session.snapshotEvents()]
|
||||
const before = testCase.session.snapshotEvents()
|
||||
const reason = Object.freeze({ kind: 'cancelled', case: testCase.name })
|
||||
const controller = new AbortController()
|
||||
controller.abort(reason)
|
||||
|
||||
@@ -254,7 +254,7 @@ describe('ToolResultPruner session transaction', () => {
|
||||
turn: 2,
|
||||
})
|
||||
service().pruneSession(session)
|
||||
const replay = Session.create(session.id, [...session.snapshotEvents()])
|
||||
const replay = Session.create(session.id, session.snapshotEvents())
|
||||
expect(replay.deriveMessages()).toEqual(session.deriveMessages())
|
||||
expect(replay.surface.replaceGeneration).toBe(session.surface.replaceGeneration)
|
||||
})
|
||||
|
||||
@@ -139,11 +139,11 @@ function visibleInstructionChanges(
|
||||
): Map<string, AgentInstructionChange> {
|
||||
const visibleSeqs = new Set(agent.session.surface.nodes)
|
||||
const visible = new Map<string, AgentInstructionChange>()
|
||||
for (const [seq, event] of agent.session.snapshotEvents().entries()) {
|
||||
for (const event of agent.session.snapshotEvents()) {
|
||||
if (event.type !== 'user/message' || !isWorkspaceContextSource(event.data.source)) continue
|
||||
const changes = workspaceInstructionChanges(event.data.source)
|
||||
for (const change of changes) {
|
||||
if (visibleSeqs.has(seq)) visible.set(change.scope, change)
|
||||
if (visibleSeqs.has(event.seq)) visible.set(change.scope, change)
|
||||
}
|
||||
}
|
||||
for (const message of authorityMessages) {
|
||||
|
||||
@@ -68,7 +68,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
function finalText(events: SessionEvent[]): string {
|
||||
function finalText(events: readonly SessionEvent[]): string {
|
||||
const message = events.findLast(event => event.type === 'assistant/message')
|
||||
if (message?.type !== 'assistant/message') return ''
|
||||
return message.data.message.content
|
||||
@@ -84,7 +84,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('workspace context e2e: real mode
|
||||
live.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Workspace context handshake?' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(live.ctx, live.agent)
|
||||
|
||||
expect(finalText([...live.agent.session.snapshotEvents()])).toContain(PROBE)
|
||||
expect(finalText(live.agent.session.snapshotEvents())).toContain(PROBE)
|
||||
}, 120_000)
|
||||
|
||||
it('loads a nested AGENTS.md after the real read tool touches a descendant file', async () => {
|
||||
@@ -96,7 +96,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('workspace context e2e: real mode
|
||||
live.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Use the read tool to inspect pkg/deep/file.txt. After reading it, answer: nested instruction handshake?' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(live.ctx, live.agent)
|
||||
|
||||
expect(finalText([...live.agent.session.snapshotEvents()])).toContain(NESTED_PROBE)
|
||||
expect(finalText(live.agent.session.snapshotEvents())).toContain(NESTED_PROBE)
|
||||
}, 120_000)
|
||||
|
||||
it('appends changed baseline instructions after a real file-tool touch without rewriting the frozen prefix', async () => {
|
||||
@@ -109,7 +109,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('workspace context e2e: real mode
|
||||
live.agent.followup(createUserMessage({ content: [{ type: 'text', text: 'You must use the read tool to inspect trigger.txt. After reading it, answer: updated workspace context handshake?' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(live.ctx, live.agent)
|
||||
|
||||
const events = [...live.agent.session.snapshotEvents()]
|
||||
const events = live.agent.session.snapshotEvents()
|
||||
const update = events.find(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'agent-instructions'
|
||||
&& event.data.source.baseline !== true)
|
||||
|
||||
@@ -187,7 +187,7 @@ async function mountFileToolsAndWorkspaceContext(ctx: Context, config: workspace
|
||||
return mountWorkspaceContextPlugin(ctx, config)
|
||||
}
|
||||
|
||||
function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
|
||||
function stubAgent(cwd?: string, seed: readonly SessionEvent[] = []): Agent {
|
||||
const id = SessionId('s1')
|
||||
const session = Session.create(id, seed, cwd === undefined ? undefined : { version: SESSION_FORMAT_VERSION, id, createdAt: 0, cwd })
|
||||
return {
|
||||
@@ -1115,9 +1115,9 @@ describe('workspace context request injection', () => {
|
||||
const original = stubAgent(root)
|
||||
await composeBaselinePrefix(ctx, original)
|
||||
|
||||
const firstResume = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const firstResume = stubAgent(root, original.session.snapshotEvents())
|
||||
await composeBaselinePrefix(ctx, firstResume)
|
||||
const secondResume = stubAgent(root, [...firstResume.session.snapshotEvents()])
|
||||
const secondResume = stubAgent(root, firstResume.session.snapshotEvents())
|
||||
await composeBaselinePrefix(ctx, secondResume)
|
||||
|
||||
expect(baselineEvents(firstResume)).toHaveLength(1)
|
||||
@@ -1144,7 +1144,7 @@ describe('workspace context request injection', () => {
|
||||
await composeBaselinePrefix(ctx, original)
|
||||
|
||||
fs.throwOnStat.add(join(root, 'AGENTS.md'))
|
||||
const resumed = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, original.session.snapshotEvents())
|
||||
await composeBaselinePrefix(ctx, resumed)
|
||||
|
||||
expect(baselineEvents(resumed)).toHaveLength(1)
|
||||
@@ -1170,9 +1170,9 @@ describe('workspace context request injection', () => {
|
||||
const original = stubAgent(cwd)
|
||||
await composeBaselinePrefix(ctx, original)
|
||||
|
||||
const firstResume = stubAgent(cwd, [...original.session.snapshotEvents()])
|
||||
const firstResume = stubAgent(cwd, original.session.snapshotEvents())
|
||||
await composeBaselinePrefix(ctx, firstResume)
|
||||
const secondResume = stubAgent(cwd, [...firstResume.session.snapshotEvents()])
|
||||
const secondResume = stubAgent(cwd, firstResume.session.snapshotEvents())
|
||||
await composeBaselinePrefix(ctx, secondResume)
|
||||
|
||||
expect(baselineEvents(secondResume)).toHaveLength(1)
|
||||
@@ -1200,7 +1200,7 @@ describe('workspace context request injection', () => {
|
||||
await composeBaselinePrefix(ctx, original)
|
||||
|
||||
await write(join(cwd, 'AGENTS.md'), 'package rule')
|
||||
const resumed = stubAgent(cwd, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(cwd, original.session.snapshotEvents())
|
||||
await composeBaselinePrefix(ctx, resumed)
|
||||
|
||||
expect(baselineEvents(resumed)).toHaveLength(1)
|
||||
@@ -1237,7 +1237,7 @@ describe('workspace context request injection', () => {
|
||||
maxBytes: 65536,
|
||||
instructionFileCandidates: ['CLAUDE.md', 'AGENTS.md'],
|
||||
})
|
||||
const resumed = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, original.session.snapshotEvents())
|
||||
await composeBaselinePrefix(resumedCtx, resumed)
|
||||
|
||||
const baselines = baselineEvents(resumed)
|
||||
@@ -1256,7 +1256,7 @@ describe('workspace context request injection', () => {
|
||||
: [])
|
||||
expect(new Set(baselineIdentities).size).toBe(2)
|
||||
|
||||
const repeated = stubAgent(root, [...resumed.session.snapshotEvents()])
|
||||
const repeated = stubAgent(root, resumed.session.snapshotEvents())
|
||||
await composeBaselinePrefix(resumedCtx, repeated)
|
||||
expect(baselineEvents(repeated)).toHaveLength(2)
|
||||
} finally {
|
||||
@@ -1290,7 +1290,7 @@ describe('workspace context request injection', () => {
|
||||
maxBytes: 65536,
|
||||
instructionFileCandidates: ['CLAUDE.md'],
|
||||
})
|
||||
const claudeResume = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const claudeResume = stubAgent(root, original.session.snapshotEvents())
|
||||
await composeBaselinePrefix(claudeCtx, claudeResume)
|
||||
const claudeBaseline = baselineEvents(claudeResume).at(-1)
|
||||
expect(claudeBaseline?.type === 'user/message' && claudeBaseline.data.source.kind === 'agent-instructions'
|
||||
@@ -1305,7 +1305,7 @@ describe('workspace context request injection', () => {
|
||||
maxBytes: 65536,
|
||||
instructionFileCandidates: ['AGENTS.md'],
|
||||
})
|
||||
const restored = stubAgent(root, [...claudeResume.session.snapshotEvents()])
|
||||
const restored = stubAgent(root, claudeResume.session.snapshotEvents())
|
||||
await composeBaselinePrefix(restoredCtx, restored)
|
||||
const restoredBaseline = baselineEvents(restored).at(-1)
|
||||
expect(restoredBaseline?.type === 'user/message' && restoredBaseline.data.source.kind === 'agent-instructions'
|
||||
@@ -1340,7 +1340,7 @@ describe('workspace context request injection', () => {
|
||||
maxBytes: 65536,
|
||||
instructionFileCandidates: ['POLICY.md'],
|
||||
})
|
||||
const resumed = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, original.session.snapshotEvents())
|
||||
await composeBaselinePrefix(resumedCtx, resumed)
|
||||
|
||||
const baselines = baselineEvents(resumed)
|
||||
@@ -1354,7 +1354,7 @@ describe('workspace context request injection', () => {
|
||||
{ action: 'remove', scope: sk('.', 'AGENTS.md'), path: 'AGENTS.md' },
|
||||
])
|
||||
|
||||
const repeated = stubAgent(root, [...resumed.session.snapshotEvents()])
|
||||
const repeated = stubAgent(root, resumed.session.snapshotEvents())
|
||||
await composeBaselinePrefix(resumedCtx, repeated)
|
||||
expect(baselineEvents(repeated)).toHaveLength(2)
|
||||
} finally {
|
||||
@@ -1384,7 +1384,7 @@ describe('workspace context request injection', () => {
|
||||
|
||||
await fiber.dispose()
|
||||
await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
|
||||
const resumed = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, original.session.snapshotEvents())
|
||||
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
|
||||
const claimed = resumed.inbox.claim('next-step', 1)
|
||||
const decision = await agentEvents(ctx, resumed).waterfall(
|
||||
@@ -1430,7 +1430,7 @@ describe('workspace context request injection', () => {
|
||||
await write(join(root, 'AGENTS.md'), 'new repo rule')
|
||||
await fiber.dispose()
|
||||
await mountWorkspaceContextPlugin(ctx, { dshHome: home, maxBytes: 65536 })
|
||||
const resumed = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, original.session.snapshotEvents())
|
||||
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
|
||||
const staleClaim = resumed.inbox.claim('next-step', 1)
|
||||
const staleDecision = await agentEvents(ctx, resumed).waterfall(
|
||||
@@ -1483,7 +1483,7 @@ describe('workspace context request injection', () => {
|
||||
await originalCtx.fiber.dispose()
|
||||
if (provideFs) await resumedCtx.plugin(LocalFileSystem, { cwd: '/' })
|
||||
await mountWorkspaceContextPlugin(resumedCtx, { dshHome: home, maxBytes })
|
||||
const resumed = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, original.session.snapshotEvents())
|
||||
agentEvents(resumedCtx, resumed).emit('agent/session-start', { source: 'resume' })
|
||||
const claimed = resumed.inbox.claim('next-step', 1)
|
||||
const decision = await agentEvents(resumedCtx, resumed).waterfall(
|
||||
@@ -1790,7 +1790,7 @@ describe('workspace context request injection', () => {
|
||||
// The first resumed pre-step retains the compatible visible baseline and
|
||||
// appends only the offline file transition needed to reach current state.
|
||||
await write(join(root, 'AGENTS.md'), 'new root rule after offline edit')
|
||||
const resumed = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, original.session.snapshotEvents())
|
||||
|
||||
// Resume announces its lifecycle start before the first step.
|
||||
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
|
||||
@@ -3540,7 +3540,7 @@ describe('dynamic nested workspace context injection', () => {
|
||||
agent,
|
||||
})
|
||||
await appendAdditionalContexts(ctx, agent)
|
||||
const resumed = stubAgent(root, [...agent.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, agent.session.snapshotEvents())
|
||||
|
||||
const afterResume = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
@@ -3574,7 +3574,7 @@ describe('dynamic nested workspace context injection', () => {
|
||||
})
|
||||
await appendAdditionalContexts(ctx, original)
|
||||
await write(join(root, 'pkg/AGENTS.md'), 'new nested rule after resume')
|
||||
const resumed = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, original.session.snapshotEvents())
|
||||
|
||||
await composeBaselinePrefix(ctx, resumed)
|
||||
|
||||
@@ -4608,7 +4608,7 @@ describe('workspace context inbox synchronization', () => {
|
||||
callId: ToolCallId('recover-pending-a'), name: 'read', arguments: { file_path: join('a', 'file.txt') }, agent: original,
|
||||
})
|
||||
await syncWorkspaceContext(ctx, original)
|
||||
const resumed = stubAgent(root, [...original.session.snapshotEvents()])
|
||||
const resumed = stubAgent(root, original.session.snapshotEvents())
|
||||
|
||||
await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
|
||||
@@ -304,7 +304,7 @@ describe('durable step context', () => {
|
||||
original.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
expect(JSON.stringify(original.deriveMessages())).not.toContain('Time sampled while preparing')
|
||||
|
||||
const resumed = Session.create(SessionId('resumed'), [...original.snapshotEvents()])
|
||||
const resumed = Session.create(SessionId('resumed'), original.snapshotEvents())
|
||||
const resumedAgent = sessionAgent(resumed)
|
||||
vi.setSystemTime(BASE + 999)
|
||||
openMessageTurn(resumed, 2)
|
||||
|
||||
@@ -175,7 +175,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
send(agent, 'go')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const events = [...agent.session.snapshotEvents()]
|
||||
const events = agent.session.snapshotEvents()
|
||||
expect(events
|
||||
.filter(event => event.type === 'tool/result'
|
||||
|| (event.type === 'user/message' && event.data.source.kind === 'plugin')
|
||||
@@ -534,7 +534,7 @@ describe('turn numbering continues across seeded sessions', () => {
|
||||
await ctx2.plugin(AgentLoop, { agents: [] })
|
||||
ctx2.llm.registerAdapter(['mock'], second)
|
||||
|
||||
const seeded = ctx2.sessions.create(SessionId('forked'), { seed: [...agent.session.snapshotEvents()] })
|
||||
const seeded = ctx2.sessions.create(SessionId('forked'), { seed: agent.session.snapshotEvents() })
|
||||
const forked = new ReactLoopAgent(
|
||||
ctx2, SessionId('forked-agent'), { provider: 'mock', model: 'mock' }, seeded,
|
||||
)
|
||||
@@ -603,7 +603,7 @@ describe('a finish-error stream chunk ends the turn as error, not completed', ()
|
||||
expect(errors[0]).toBeInstanceOf(LlmError)
|
||||
expect((errors[0] as LlmError).failure).toEqual(failure)
|
||||
|
||||
const events = [...agent.session.snapshotEvents()]
|
||||
const events = agent.session.snapshotEvents()
|
||||
const turnEnd = events.find(event => event.type === 'turn/end')
|
||||
expect(turnEnd).toMatchObject({ data: { reason: { kind: 'error', error: failure } } })
|
||||
// A failed step must not synthesize an assistant message.
|
||||
@@ -625,7 +625,7 @@ describe('a finish-error stream chunk ends the turn as error, not completed', ()
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(reasons).toEqual([{ kind: 'error', error: { message: 'model stream aborted', code: 'ABORTED' } }])
|
||||
expect([...agent.session.snapshotEvents()].some(event => event.type === 'assistant/message')).toBe(false)
|
||||
expect(agent.session.snapshotEvents().some(event => event.type === 'assistant/message')).toBe(false)
|
||||
})
|
||||
|
||||
it('handles a finish error without a code (code key omitted)', async () => {
|
||||
@@ -655,7 +655,7 @@ describe('step boundary publication order', () => {
|
||||
const observed: { turn: number; step: number; lastEventType: string | undefined; sawStepStart: boolean }[] = []
|
||||
ctx.on('session/event', (subject, event) => {
|
||||
if (subject !== agent.session || event.type !== 'step/start') return
|
||||
const events = [...subject.snapshotEvents()]
|
||||
const events = subject.snapshotEvents()
|
||||
const last = events.at(-1)
|
||||
observed.push({
|
||||
turn: event.data.turn,
|
||||
@@ -691,7 +691,7 @@ describe('turn and step boundary recovery', () => {
|
||||
|
||||
/** Count turn/step boundary events for balance assertions. */
|
||||
function boundaryCounts(agent: Agent) {
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
return {
|
||||
turnStart: e.filter(x => x.type === 'turn/start').length,
|
||||
turnEnd: e.filter(x => x.type === 'turn/end').length,
|
||||
@@ -721,7 +721,7 @@ describe('turn and step boundary recovery', () => {
|
||||
send(agent, 'go')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
const c = boundaryCounts(agent)
|
||||
expect(c).toMatchObject({ turnStart: 1, turnEnd: 1, stepStart: 1, stepEnd: 1, errors: 0 })
|
||||
expect(errors).toEqual([])
|
||||
@@ -878,7 +878,7 @@ describe('turn and step boundary recovery', () => {
|
||||
await fiber.dispose() // dispose during the hanging step
|
||||
await driverDone(agent)
|
||||
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
const turnStarts = e.filter(x => x.type === 'turn/start').length
|
||||
const turnEnds = e.filter(x => x.type === 'turn/end').length
|
||||
expect(turnStarts).toBe(1)
|
||||
@@ -911,7 +911,7 @@ describe('turn and step boundary recovery', () => {
|
||||
send(agent, 'go')
|
||||
await agent.whenIdle()
|
||||
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type))
|
||||
.toEqual(['turn/start', 'turn/end'])
|
||||
expect(e.find(x => x.type === 'turn/end')?.data.reason)
|
||||
@@ -940,10 +940,10 @@ describe('turn and step boundary recovery', () => {
|
||||
expect(errors).toEqual([])
|
||||
// Session contains the observer failure per listener, so the committed turn
|
||||
// remains visible to later observers and executes normally.
|
||||
const types = [...agent.session.snapshotEvents()].map(e => e.type)
|
||||
const types = agent.session.snapshotEvents().map(e => e.type)
|
||||
expect(types.filter(t => t === 'turn/start')).toHaveLength(1)
|
||||
expect(types.filter(t => t === 'turn/end')).toHaveLength(1)
|
||||
const lastBoundary = [...agent.session.snapshotEvents()].reverse().find(e => e.type === 'turn/start' || e.type === 'turn/end')
|
||||
const lastBoundary = agent.session.snapshotEvents().findLast(e => e.type === 'turn/start' || e.type === 'turn/end')
|
||||
expect(lastBoundary?.type).toBe('turn/end')
|
||||
expect(agent.session.snapshotEvents().at(-1)?.type).toBe('turn/end')
|
||||
|
||||
@@ -977,7 +977,7 @@ describe('turn and step boundary recovery', () => {
|
||||
.toEqual({ kind: 'completed' })
|
||||
|
||||
// step/end precedes turn/end (ordering contract)
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
const stepEndIdx = e.findIndex(x => x.type === 'step/end')
|
||||
const turnEndIdx = e.findIndex(x => x.type === 'turn/end')
|
||||
expect(stepEndIdx).toBeGreaterThanOrEqual(0)
|
||||
@@ -1011,7 +1011,7 @@ describe('turn and step boundary recovery', () => {
|
||||
send(agent, 'go')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
// Both step/end and turn/end are present — finalization ran to completion.
|
||||
expect(e.some(x => x.type === 'step/end')).toBe(true)
|
||||
expect(e.some(x => x.type === 'turn/end')).toBe(true)
|
||||
@@ -1041,7 +1041,7 @@ describe('turn and step boundary recovery', () => {
|
||||
send(agent, 'go')
|
||||
await waitForIdle(ctx, agent)
|
||||
// turn 1 is balanced despite the throwing turn/end listener.
|
||||
const e1 = [...agent.session.snapshotEvents()]
|
||||
const e1 = agent.session.snapshotEvents()
|
||||
expect(e1.filter(x => x.type === 'turn/start')).toHaveLength(1)
|
||||
expect(e1.filter(x => x.type === 'turn/end')).toHaveLength(1)
|
||||
expect(e1.at(-1)?.type).toBe('turn/end')
|
||||
@@ -1050,7 +1050,7 @@ describe('turn and step boundary recovery', () => {
|
||||
send(agent, 'again')
|
||||
await waitForIdle(ctx, agent)
|
||||
expect(adapter.requests).toHaveLength(2)
|
||||
expect([...agent.session.snapshotEvents()].filter(x => x.type === 'turn/end')).toHaveLength(2)
|
||||
expect(agent.session.snapshotEvents().filter(x => x.type === 'turn/end')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1082,7 +1082,7 @@ describe('tool result call identity', () => {
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// The logged tool/result.callId is the originating call.id.
|
||||
const resultEvent = [...agent.session.snapshotEvents()].find(e => e.type === 'tool/result')
|
||||
const resultEvent = agent.session.snapshotEvents().find(e => e.type === 'tool/result')
|
||||
expect(resultEvent?.type).toBe('tool/result')
|
||||
if (resultEvent?.type === 'tool/result') {
|
||||
expect(resultEvent.data.message.source.callId).toBe(ToolCallId('c1'))
|
||||
@@ -1146,7 +1146,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
|
||||
await driverDone(agent)
|
||||
unlisten()
|
||||
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type))
|
||||
.toEqual(['turn/start', 'turn/end'])
|
||||
expect(e.some(x => x.type === 'step/start')).toBe(false)
|
||||
@@ -1194,7 +1194,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
|
||||
await driverDone(agent)
|
||||
unlisten()
|
||||
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type))
|
||||
.toEqual(['turn/start', 'turn/end'])
|
||||
expect(e.some(x => x.type === 'step/start')).toBe(false)
|
||||
@@ -1244,7 +1244,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
|
||||
await driverDone(agent)
|
||||
|
||||
// The post-listener cancellation check catches disposal before any step or LLM call.
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type))
|
||||
.toEqual(['turn/start', 'turn/end'])
|
||||
expect(e.some(x => x.type === 'step/start')).toBe(false)
|
||||
@@ -1291,7 +1291,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
|
||||
await fiber.dispose()
|
||||
await driverDone(agent)
|
||||
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type))
|
||||
.toEqual(['turn/start', 'turn/end'])
|
||||
expect(e.some(x => x.type === 'step/start')).toBe(false)
|
||||
@@ -1336,7 +1336,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
|
||||
await disposalDone
|
||||
await driverDone(agent)
|
||||
|
||||
const e = [...agent.session.snapshotEvents()]
|
||||
const e = agent.session.snapshotEvents()
|
||||
expect(e.filter(x => x.type === 'turn/start' || x.type === 'turn/end').map(x => x.type))
|
||||
.toEqual(['turn/start', 'turn/end'])
|
||||
expect(e.find(x => x.type === 'turn/end')?.data.reason)
|
||||
|
||||
@@ -56,8 +56,8 @@ function send(agent: Agent, text: string) {
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text }], source: { kind: 'user' } }))
|
||||
}
|
||||
|
||||
function events(agent: Agent): SessionEvent[] {
|
||||
return [...agent.session.snapshotEvents()]
|
||||
function events(agent: Agent): readonly SessionEvent[] {
|
||||
return agent.session.snapshotEvents()
|
||||
}
|
||||
|
||||
describe('agent/pre-step', () => {
|
||||
|
||||
@@ -1526,7 +1526,7 @@ describe('agent loop', () => {
|
||||
send(agent, 'run')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const replayed = ctx.sessions.create(SessionId('replayed'), { seed: [...agent.session.snapshotEvents()] })
|
||||
const replayed = ctx.sessions.create(SessionId('replayed'), { seed: agent.session.snapshotEvents() })
|
||||
expect(replayed.deriveMessages()).toEqual(agent.session.deriveMessages())
|
||||
// event-by-event identity of types over the inherited prefix
|
||||
expect(replayed.snapshotEvents().slice(0, agent.session.seq).map(e => e.type)).toEqual(
|
||||
|
||||
@@ -82,7 +82,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('log-derived request cache hits (
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Thanks. Repeat that value one more time.' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const usages = [...agent.session.snapshotEvents()]
|
||||
const usages = agent.session.snapshotEvents()
|
||||
.filter(e => e.type === 'assistant/message')
|
||||
.map(e => e.data.usage)
|
||||
expect(usages.length).toBeGreaterThanOrEqual(3) // 2 steps in turn 1 + ≥1 in turn 2
|
||||
|
||||
@@ -650,7 +650,7 @@ describe('request stability across the loop', () => {
|
||||
const ctx2 = await harness(adapter2)
|
||||
const handle = await ctx2.agents.create({
|
||||
sessionId: SessionId('gen2-session'),
|
||||
seed: [...agent.session.snapshotEvents()],
|
||||
seed: agent.session.snapshotEvents(),
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
const agent2 = handle.agent
|
||||
|
||||
@@ -650,7 +650,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
|
||||
const a1 = (await ctx1.agents.create({ sessionId: SessionId('sess-resume'), meta: { cwd: '/w' } })).agent
|
||||
a1.followup(createUserMessage({ content: [{ type: 'text', text: 'first question' }], source: { kind: 'user' } }))
|
||||
await waitForIdle(ctx1, a1)
|
||||
const events1 = [...a1.session.snapshotEvents()]
|
||||
const events1 = a1.session.snapshotEvents()
|
||||
const seqs1 = events1.map(e => e.seq)
|
||||
expect(seqs1).toEqual([...seqs1].sort((x, y) => x - y)) // contiguous
|
||||
await ctx1.fiber.dispose()
|
||||
|
||||
@@ -41,8 +41,8 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
function events(agent: Agent): SessionEvent[] {
|
||||
return [...agent.session.snapshotEvents()]
|
||||
function events(agent: Agent): readonly SessionEvent[] {
|
||||
return agent.session.snapshotEvents()
|
||||
}
|
||||
|
||||
/** Build one assistant response containing the supplied tool calls. */
|
||||
|
||||
@@ -16,7 +16,7 @@ function userText(session: Session, text: string): void {
|
||||
|
||||
/** From-scratch oracle: replay the log into a fresh session and derive. */
|
||||
function scratch(session: Session): unknown {
|
||||
return Session.create(SessionId(`${session.id}-scratch-${session.seq}`), [...session.snapshotEvents()]).deriveMessages()
|
||||
return Session.create(SessionId(`${session.id}-scratch-${session.seq}`), session.snapshotEvents()).deriveMessages()
|
||||
}
|
||||
|
||||
describe('derived-message cache', () => {
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('Session properties', () => {
|
||||
it('replay-from-seed reproduces the derivation identically', () => {
|
||||
fc.assert(fc.property(logArb, (events) => {
|
||||
const original = build(events)
|
||||
const replayed = Session.create(SessionId(`replay-${counter++}`), [...original.snapshotEvents()])
|
||||
const replayed = Session.create(SessionId(`replay-${counter++}`), original.snapshotEvents())
|
||||
expect(replayed.deriveMessages()).toEqual(original.deriveMessages())
|
||||
// Every explicit replay grows by exactly one log-only boundary.
|
||||
expect(replayed.snapshotEvents().slice(0, original.seq)).toEqual(original.snapshotEvents())
|
||||
@@ -121,8 +121,8 @@ describe('Session properties', () => {
|
||||
it('replaying a log that already ends in end-seed adds no further marker', () => {
|
||||
fc.assert(fc.property(logArb, (events) => {
|
||||
const original = build(events)
|
||||
const once = Session.create(SessionId(`idem-a-${counter++}`), [...original.snapshotEvents()])
|
||||
const twice = Session.create(SessionId(`idem-b-${counter++}`), [...once.snapshotEvents()])
|
||||
const once = Session.create(SessionId(`idem-a-${counter++}`), original.snapshotEvents())
|
||||
const twice = Session.create(SessionId(`idem-b-${counter++}`), once.snapshotEvents())
|
||||
// Lazy resume makes browsing a pickup, so this must not grow per open.
|
||||
expect(twice.snapshotEvents()).toEqual(once.snapshotEvents())
|
||||
}))
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('Session', () => {
|
||||
}, { surfaceOp: 'append' })
|
||||
original.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
|
||||
const replayed = Session.create(SessionId('s3-replay'), [...original.snapshotEvents()])
|
||||
const replayed = Session.create(SessionId('s3-replay'), original.snapshotEvents())
|
||||
expect(replayed.deriveMessages()).toEqual(original.deriveMessages())
|
||||
// The seed verbatim, plus the end-seed event the constructor appends.
|
||||
expect(replayed.snapshotEvents().slice(0, original.seq)).toEqual(original.snapshotEvents())
|
||||
@@ -1141,7 +1141,7 @@ describe('SessionStore', () => {
|
||||
a.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'q' }], source: { kind: 'user' },
|
||||
}), { surfaceOp: 'append' })
|
||||
const forked = ctx.sessions.create(SessionId('fork'), { seed: [...a.snapshotEvents()] })
|
||||
const forked = ctx.sessions.create(SessionId('fork'), { seed: a.snapshotEvents() })
|
||||
expect(forked.deriveMessages()).toEqual(a.deriveMessages())
|
||||
})
|
||||
|
||||
|
||||
@@ -479,7 +479,7 @@ describe('SurfaceManager', () => {
|
||||
isError: false,
|
||||
}),
|
||||
}, { surfaceOp: 'append' })
|
||||
const replayed = Session.create(SessionId('replay'), [...original.snapshotEvents()])
|
||||
const replayed = Session.create(SessionId('replay'), original.snapshotEvents())
|
||||
expect(replayed.surface.nodes).toEqual([1, 2, 4])
|
||||
expect(replayed.deriveMessages()).toEqual(original.deriveMessages())
|
||||
})
|
||||
|
||||
@@ -43,7 +43,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('fs tools with-key smoke', () =>
|
||||
expect(content).not.toContain('draft')
|
||||
|
||||
// The log records real read/write/edit tool calls (not bash).
|
||||
const calls = [...agent.session.snapshotEvents()].filter(e => e.type === 'tool/call').map(e => e.data.name)
|
||||
const calls = agent.session.snapshotEvents().filter(e => e.type === 'tool/call').map(e => e.data.name)
|
||||
expect(calls).toContain('write')
|
||||
expect(calls).toContain('read')
|
||||
expect(calls).toContain('edit')
|
||||
|
||||
@@ -40,7 +40,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
|
||||
/** Every injected-context user message in the agent's log, flattened to joined text + source for terse assertions. */
|
||||
function reminders(agent: Agent): { text: string; source: unknown }[] {
|
||||
return [...agent.session.snapshotEvents()]
|
||||
return agent.session.snapshotEvents()
|
||||
.filter((e): e is SessionEvent<'user/message'> => e.type === 'user/message' && e.data.source.kind !== 'user')
|
||||
.map(e => ({
|
||||
text: e.data.content.map(block => block.type === 'text' ? block.text : '').join('|'),
|
||||
@@ -340,7 +340,7 @@ describe('fold onto the downstream decision', () => {
|
||||
expect(found[1]!.source).toEqual(guardSource('probe', 2))
|
||||
expect(found[2]).toEqual({ text: 'downstream-ctx', source: { kind: 'plugin', plugin: 'test' } })
|
||||
// The block's feedback reached the tool result unchanged.
|
||||
const results = [...agent.session.snapshotEvents()].filter((e): e is SessionEvent<'tool/result'> => e.type === 'tool/result')
|
||||
const results = agent.session.snapshotEvents().filter((e): e is SessionEvent<'tool/result'> => e.type === 'tool/result')
|
||||
expect(results.every(r => r.data.message.content[0].isError)).toBe(true)
|
||||
expect(results[1]!.data.message.content[0].content).toEqual([{ type: 'text', text: 'nope' }])
|
||||
})
|
||||
@@ -364,7 +364,7 @@ describe('fold onto the downstream decision', () => {
|
||||
const found = reminders(agent)
|
||||
expect(found).toHaveLength(1)
|
||||
expect(found[0]!.text).toContain('repeating the exact same tool call')
|
||||
const results = [...agent.session.snapshotEvents()].filter((e): e is SessionEvent<'tool/result'> => e.type === 'tool/result')
|
||||
const results = agent.session.snapshotEvents().filter((e): e is SessionEvent<'tool/result'> => e.type === 'tool/result')
|
||||
expect(results[1]!.data.message.content[0].content).toEqual([{ type: 'text', text: 'replaced' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('hook/* session events', () => {
|
||||
const session = Session.create(SessionId('s'))
|
||||
appendHookInvoked(session, { turn: 1, point: 'PreToolUse', dialect: 'claude-code', handlerId: 'h1', matcher: 'Bash' })
|
||||
|
||||
const ev = [...session.snapshotEvents()].find(e => e.type === 'hook/invoked')
|
||||
const ev = session.snapshotEvents().find(e => e.type === 'hook/invoked')
|
||||
expect(ev?.type).toBe('hook/invoked')
|
||||
if (ev?.type === 'hook/invoked') {
|
||||
expect(ev.data).toMatchObject({ turn: 1, point: 'PreToolUse', dialect: 'claude-code', handlerId: 'h1', matcher: 'Bash' })
|
||||
@@ -25,7 +25,7 @@ describe('hook/* session events', () => {
|
||||
const session = Session.create(SessionId('s'))
|
||||
appendHookInvoked(session, { turn: 2, point: 'Stop', dialect: 'codex', handlerId: 'h2' })
|
||||
|
||||
const ev = [...session.snapshotEvents()].find(e => e.type === 'hook/invoked')
|
||||
const ev = session.snapshotEvents().find(e => e.type === 'hook/invoked')
|
||||
if (ev?.type === 'hook/invoked') {
|
||||
expect('matcher' in ev.data).toBe(false)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ describe('hook/* session events', () => {
|
||||
turn: 1, point: 'PreToolUse', handlerId: 'h1',
|
||||
stderrSummaryMaxChars: 500, durationMs: 5, output: output({ exitCode: 2, stderr: 'blocked', decision: 'deny' }),
|
||||
})
|
||||
const full = [...session.snapshotEvents()].find(e => e.type === 'hook/result')
|
||||
const full = session.snapshotEvents().find(e => e.type === 'hook/result')
|
||||
if (full?.type === 'hook/result') {
|
||||
expect(full.data).toEqual({ turn: 1, point: 'PreToolUse', handlerId: 'h1', decision: 'deny', exitCode: 2, stderrSummary: 'blocked', durationMs: 5 })
|
||||
}
|
||||
@@ -48,7 +48,7 @@ describe('hook/* session events', () => {
|
||||
turn: 1, point: 'Stop', handlerId: 'h3',
|
||||
stderrSummaryMaxChars: 500, durationMs: 5, output: output({ exitCode: undefined, decision: 'allow' }),
|
||||
})
|
||||
const sparse = [...session2.snapshotEvents()].find(e => e.type === 'hook/result')
|
||||
const sparse = session2.snapshotEvents().find(e => e.type === 'hook/result')
|
||||
if (sparse?.type === 'hook/result') {
|
||||
expect('exitCode' in sparse.data).toBe(false)
|
||||
expect('stderrSummary' in sparse.data).toBe(false)
|
||||
@@ -63,7 +63,7 @@ describe('hook/* session events', () => {
|
||||
// An explicit decision wins over the continue:false fallback.
|
||||
appendHookResult(session, { turn: 1, point: 'Stop', handlerId: 'both', stderrSummaryMaxChars: 500, durationMs: 5, output: output({ continue: false, decision: 'block' }) })
|
||||
|
||||
const decisions = [...session.snapshotEvents()]
|
||||
const decisions = session.snapshotEvents()
|
||||
.filter(e => e.type === 'hook/result')
|
||||
.map(e => e.type === 'hook/result' ? [e.data.handlerId, e.data.decision] : [])
|
||||
expect(decisions).toEqual([['halt', 'stop'], ['noop', 'pass'], ['both', 'block']])
|
||||
@@ -75,7 +75,7 @@ describe('hook/* session events', () => {
|
||||
turn: 1, point: 'PreToolUse', handlerId: 'long',
|
||||
stderrSummaryMaxChars: 500, durationMs: 5, output: output({ exitCode: 2, stderr: ` ${'x'.repeat(600)} ` }),
|
||||
})
|
||||
const ev = [...session.snapshotEvents()].find(e => e.type === 'hook/result')
|
||||
const ev = session.snapshotEvents().find(e => e.type === 'hook/result')
|
||||
if (ev?.type === 'hook/result') {
|
||||
expect(ev.data.stderrSummary).toBe('x'.repeat(500) + '…')
|
||||
}
|
||||
@@ -87,7 +87,7 @@ describe('hook/* session events', () => {
|
||||
turn: 1, point: 'PreToolUse', handlerId: 'edge',
|
||||
stderrSummaryMaxChars: 500, durationMs: 5, output: output({ exitCode: 2, stderr: 'y'.repeat(500) }),
|
||||
})
|
||||
const ev = [...session.snapshotEvents()].find(e => e.type === 'hook/result')
|
||||
const ev = session.snapshotEvents().find(e => e.type === 'hook/result')
|
||||
if (ev?.type === 'hook/result') {
|
||||
expect(ev.data.stderrSummary).toBe('y'.repeat(500))
|
||||
}
|
||||
@@ -98,8 +98,8 @@ describe('hook/* session events', () => {
|
||||
appendHookInvoked(session, { turn: 1, point: 'PreToolUse', dialect: 'claude-code', handlerId: 'pair-1' })
|
||||
appendHookResult(session, { turn: 1, point: 'PreToolUse', handlerId: 'pair-1', stderrSummaryMaxChars: 500, durationMs: 5, output: output({ decision: 'allow' }) })
|
||||
|
||||
const invoked = [...session.snapshotEvents()].find(e => e.type === 'hook/invoked')
|
||||
const result = [...session.snapshotEvents()].find(e => e.type === 'hook/result')
|
||||
const invoked = session.snapshotEvents().find(e => e.type === 'hook/invoked')
|
||||
const result = session.snapshotEvents().find(e => e.type === 'hook/result')
|
||||
expect(invoked?.type === 'hook/invoked' && invoked.data.handlerId).toBe('pair-1')
|
||||
expect(result?.type === 'hook/result' && result.data.handlerId).toBe('pair-1')
|
||||
})
|
||||
|
||||
@@ -72,8 +72,8 @@ function waitForIdle(_ctx: Context, agent: Agent): Promise<void> {
|
||||
return agent.whenIdle()
|
||||
}
|
||||
|
||||
function events(agent: Agent): SessionEvent[] {
|
||||
return [...agent.session.snapshotEvents()]
|
||||
function events(agent: Agent): readonly SessionEvent[] {
|
||||
return agent.session.snapshotEvents()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -54,7 +54,7 @@ async function harness(configPath: string, adapter: MockAdapter, opts: HarnessOp
|
||||
function waitForIdle(_ctx: Context, agent: Agent): Promise<void> {
|
||||
return agent.whenIdle()
|
||||
}
|
||||
function events(agent: Agent): SessionEvent[] { return [...agent.session.snapshotEvents()] }
|
||||
function events(agent: Agent): readonly SessionEvent[] { return agent.session.snapshotEvents() }
|
||||
/** Poll until `predicate` holds or the deadline passes — robust to detached
|
||||
* emit-listener hooks firing on a `.then` (a fixed sleep flakes under load). */
|
||||
async function waitFor(predicate: () => boolean, timeout = 5000, interval = 10): Promise<void> {
|
||||
|
||||
@@ -56,7 +56,7 @@ async function harness(dir: string, adapter: MockAdapter, beforeHooks?: (ctx: Co
|
||||
function waitForIdle(_ctx: Context, agent: Agent): Promise<void> {
|
||||
return agent.whenIdle()
|
||||
}
|
||||
function events(agent: Agent): SessionEvent[] { return [...agent.session.snapshotEvents()] }
|
||||
function events(agent: Agent): readonly SessionEvent[] { return agent.session.snapshotEvents() }
|
||||
|
||||
/** Poll `predicate` until true or the deadline passes (detached hook effects can't be awaited directly). */
|
||||
async function waitFor(predicate: () => boolean, timeout = 5000, interval = 10): Promise<void> {
|
||||
|
||||
@@ -44,7 +44,7 @@ async function harness(configPath: string, adapter: MockAdapter, opts: HarnessOp
|
||||
function waitForIdle(_ctx: Context, agent: Agent): Promise<void> {
|
||||
return agent.whenIdle()
|
||||
}
|
||||
function events(agent: Agent): SessionEvent[] { return [...agent.session.snapshotEvents()] }
|
||||
function events(agent: Agent): readonly SessionEvent[] { return agent.session.snapshotEvents() }
|
||||
/** Poll until `predicate` holds or the deadline passes — robust to detached
|
||||
* emit-listener hooks firing on a `.then` (a fixed sleep flakes under load). */
|
||||
async function waitFor(predicate: () => boolean, timeout = 5000, interval = 10): Promise<void> {
|
||||
|
||||
@@ -288,7 +288,7 @@ describe('SessionTelemetryCoordinator adoption', () => {
|
||||
inject: ['sessions'],
|
||||
apply: (inner: Context) => void new SessionTelemetryCoordinator(inner, backend),
|
||||
})
|
||||
const child = ctx.sessions.prepare(SessionId('seeded'), { seed: [...parent.snapshotEvents()], meta: {} })
|
||||
const child = ctx.sessions.prepare(SessionId('seeded'), { seed: parent.snapshotEvents(), meta: {} })
|
||||
child.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
ctx.sessions.enter(child)
|
||||
ctx.sessions.announce(child)
|
||||
@@ -307,7 +307,7 @@ describe('SessionTelemetryCoordinator adoption', () => {
|
||||
const donor = ctx.sessions.create(SessionId('donor'), { meta: {} })
|
||||
donor.append('turn/start', { turn: 1 })
|
||||
donor.append('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'first' } })
|
||||
const resumed = ctx.sessions.create(SessionId('resumed'), { seed: [...donor.snapshotEvents()], meta: {} })
|
||||
const resumed = ctx.sessions.create(SessionId('resumed'), { seed: donor.snapshotEvents(), meta: {} })
|
||||
await ctx.plugin({
|
||||
name: 'fake-telemetry',
|
||||
inject: ['sessions'],
|
||||
@@ -335,7 +335,7 @@ describe('SessionTelemetryCoordinator adoption', () => {
|
||||
const parent = liveSession(ctx, 'stitch-parent')
|
||||
appendTurn(parent)
|
||||
const child = ctx.sessions.create(SessionId('stitch-child'), {
|
||||
seed: [...parent.snapshotEvents()],
|
||||
seed: parent.snapshotEvents(),
|
||||
meta: { parentSession: SessionId('stitch-parent'), seedLength: 2 },
|
||||
})
|
||||
await ctx.plugin({
|
||||
|
||||
@@ -60,13 +60,13 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
function events(agent: Agent): SessionEvent[] {
|
||||
return [...agent.session.snapshotEvents()]
|
||||
function events(agent: Agent): readonly SessionEvent[] {
|
||||
return agent.session.snapshotEvents()
|
||||
}
|
||||
|
||||
/** Find a session event by type, narrowed; throws when absent. */
|
||||
function findEvent<T extends SessionEvent['type']>(
|
||||
log: SessionEvent[],
|
||||
log: readonly SessionEvent[],
|
||||
type: T,
|
||||
position: 'first' | 'last' = 'first',
|
||||
): Extract<SessionEvent, { type: T }> {
|
||||
|
||||
@@ -67,7 +67,7 @@ function toStopReason(reason: TurnEndReason | undefined): SubagentStopReason {
|
||||
/** Extra inputs the spawn and fork providers supply to the shared driver. */
|
||||
export interface InProcessRunOptions {
|
||||
/** Completed-turn seed for fork, or undefined for a fresh spawn. */
|
||||
readonly seed?: SessionEvent[]
|
||||
readonly seed?: readonly SessionEvent[]
|
||||
}
|
||||
|
||||
/** Error used when cancellation wins before the child publication boundary. */
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('in-process policy inheritance', () => {
|
||||
const { ctx, parent } = await setupWalled(script)
|
||||
const blocked = join(workspace, 'fork-blocked.txt')
|
||||
setSandboxMode(parent.session, 'workspace-write')
|
||||
const seed = [...parent.session.snapshotEvents()]
|
||||
const seed = parent.session.snapshotEvents()
|
||||
setSandboxMode(parent.session, 'read-only')
|
||||
script.push(
|
||||
toolCallResponse('write', 'write', { file_path: blocked, content: 'escaped' }),
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ describe('startInProcessRun', () => {
|
||||
const { ctx, parent } = await setup([textResponse('parent answer'), textResponse('child answer')])
|
||||
parent.followup(createUserMessage({ content: [{ type: 'text', text: 'parent question' }], source: { kind: 'user' } }))
|
||||
await parent.whenIdle()
|
||||
const seed = parent.session.snapshotEvents().slice()
|
||||
const seed = parent.session.snapshotEvents()
|
||||
const run = await startInProcessRun(request(parent), { seed })
|
||||
const result = await run.result
|
||||
expect(text(result.output)).toBe('child answer')
|
||||
|
||||
@@ -38,7 +38,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('spawn backend with-key smoke', (
|
||||
|
||||
// The parent's log records the subagent tool/call + its result (not the
|
||||
// child's internal steps).
|
||||
const events = [...parent.session.snapshotEvents()]
|
||||
const events = parent.session.snapshotEvents()
|
||||
const subagentCalls = events.filter(e => e.type === 'tool/call' && e.data.name === 'subagent')
|
||||
expect(subagentCalls.length).toBeGreaterThan(0)
|
||||
}, 180_000)
|
||||
|
||||
@@ -24,8 +24,8 @@ export function seedDescriptorTurn(
|
||||
childId: SessionId,
|
||||
seed: readonly SessionEvent[] | undefined,
|
||||
descriptor: SubagentDescriptorData,
|
||||
): SessionEvent[] {
|
||||
): readonly SessionEvent[] {
|
||||
const staged = Session.create(childId, seed)
|
||||
staged.append('subagent/descriptor', descriptor)
|
||||
return [...staged.snapshotEvents()]
|
||||
return staged.snapshotEvents()
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('todo snapshot invariants', () => {
|
||||
const session = ctx.sessions.create()
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
const before = [...session.snapshotEvents()]
|
||||
const before = session.snapshotEvents()
|
||||
|
||||
expect(() => session.append('todo/write', { todos: [] })).toThrow(/outside any open turn/)
|
||||
expect(session.snapshotEvents()).toEqual(before)
|
||||
|
||||
Reference in New Issue
Block a user