Merge remote-tracking branch 'origin/master' into session-query-tool

# Conflicts:
#	docs/core-data-structures/persistence.i18n.yaml
#	packages/session-persistence/session-persistence-jsonl/src/index.ts
This commit is contained in:
Hypatia May
2026-07-25 15:43:45 +08:00
136 changed files with 1080 additions and 1024 deletions

View File

@@ -188,15 +188,12 @@ describe('Agent', () => {
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
// Simulate an OPEN turn in the log while the agent is idle (status is not a
// reliable open-turn signal). inject must append into that open turn, NOT
// wrap a new one.
// Status is idle while the log has an open turn; enclosure must follow the log.
agent.session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
agent.inject([{ type: 'text', text: 'mid' }], { source: { kind: 'plugin', plugin: 'p' } })
expect(agent.session.events.filter(e => e.type === 'turn/start')).toHaveLength(1)
expect(agent.session.events.at(-1)!.type).toBe('user/message')
// Close the turn; now inject must wrap its own one-shot injection turn.
agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
agent.inject([{ type: 'text', text: 'after' }], { source: { kind: 'plugin', plugin: 'p' } })
const starts = agent.session.events.filter(e => e.type === 'turn/start')
@@ -342,11 +339,9 @@ describe('Agent', () => {
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
// steer while idle delegates to send
agent.steer([{ type: 'text', text: 'steer idle' }], { source: { kind: 'plugin', plugin: 'test' } })
await waitForIdle(ctx, agent)
// The message was recorded as a user-level message (send path)
expect(agent.session.events.some(e => e.type === 'user/message')).toBe(true)
expect(adapter.requests).toHaveLength(1)
})
@@ -369,12 +364,10 @@ describe('Agent', () => {
prepared.markPublished()
const dispose = prepared.startDriver()
// First dispose
const firstDisposal = dispose()
expect(agent.status).toBe('disposed')
await firstDisposal
// Second dispose — idempotent, no throw
await expect(dispose()).resolves.toBeUndefined()
expect(agent.status).toBe('disposed')
})

View File

@@ -142,8 +142,6 @@ describe('Agent.cancel()', () => {
agent.queue([{ type: 'text', text: 'quiet' }])
const idle = agent.whenIdle()
// Cancel reaches quiescence with no status transition and no waking send;
// whenIdle must still resolve (previously it hung until the next send).
agent.cancel({ kind: 'user' })
await idle
expect(agent.session.events.some(e => e.type === 'turn/start')).toBe(false)

View File

@@ -1096,7 +1096,6 @@ describe('step boundary publication order', () => {
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('a-step-order'), { provider: 'mock', model: 'mock' })
// Append commits before observers run.
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
@@ -1704,7 +1703,6 @@ describe('disposal and cancellation during pre-step assembly', () => {
send(agent, 'go')
await new Promise(r => setTimeout(r, 50))
// Start disposal, then release the block, then await disposal.
const disposalDone = fiber.dispose()
releasePreStep()
await disposalDone

View File

@@ -283,20 +283,17 @@ describe('SurfaceManager', () => {
it('empty surface yields empty nodes', () => {
const s = new Session(SessionId('empty'))
// Only turn boundaries, no surface nodes.
s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
s.append('step/start', { turn: 1, step: 1 })
s.append('step/end', { turn: 1, step: 1 })
s.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
expect(s.surface.nodes.length).toBe(0)
// deriveMessages returns empty array
expect(s.deriveMessages()).toEqual([])
})
it('picks up new events incrementally (delta processing)', () => {
const s = surfaceSession()
expect(s.surface.nodes.length).toBe(2)
// Append another surface node
s.append('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false }, { surfaceOp: 'append' })
expect(s.surface.nodes.length).toBe(3)
expect(s.surface.nodes[2]!).toBe(4) // seq 4: after turn/end at seq 3
@@ -306,7 +303,6 @@ describe('SurfaceManager', () => {
const original = surfaceSession()
original.append('tool/result', { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'ok' }], isError: false }, { surfaceOp: 'append' })
const replayed = new Session(SessionId('replay'), [...original.events])
// Surface rebuilds from the seeded log's markers.
expect(replayed.surface.nodes).toEqual([1, 2, 4])
expect(replayed.deriveMessages()).toEqual(original.deriveMessages())
})

View File

@@ -1893,7 +1893,6 @@ describe('ToolRegistry', () => {
const ctx = await setup()
ctx.tools.register(echoTool)
// Register a second tool and call its returned disposer directly
const dispose = ctx.tools.register({ ...echoTool, name: 'disposable' })
expect(ctx.tools.schemas().map(t => t.name)).toEqual(['echo', 'disposable'])
@@ -2055,9 +2054,7 @@ describe('defineTool / schema DSL', () => {
parameters: { a: { type: 'string' as const, required: true as const }, b: { type: 'number' as const } },
output: { schema: { type: 'string' }, render: () => [] },
async execute(args) {
// Verify types at runtime via typeof
expect(typeof args.a).toBe('string')
// args.b should be undefined when not provided
void args
return args.a
},