refactor(agent): align delivery method names

This commit is contained in:
Turtle
2026-07-24 15:08:36 +08:00
parent 9848a9542b
commit b02b438667
113 changed files with 462 additions and 405 deletions

View File

@@ -41,7 +41,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string): void {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
/** Adapter that holds both drivers at the same awaited continuation. */

View File

@@ -48,7 +48,7 @@ function waitForStatus(ctx: Context, agent: Agent, expected: Agent['status']): P
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
describe('Agent', () => {
@@ -83,7 +83,7 @@ describe('Agent', () => {
await ctx.fiber.dispose()
})
it('acceptInput exposes the fully resolved delivery path without applying helper defaults', async () => {
it('send exposes the fully resolved delivery path without applying helper defaults', async () => {
const adapter = new MockAdapter([textResponse('accepted')])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
@@ -92,7 +92,7 @@ describe('Agent', () => {
if (subject === agent) enqueued.resolve(message)
})
const id = agent.acceptInput({
const id = agent.send({
content: [{ type: 'text', text: 'advanced input' }],
source: { kind: 'plugin', plugin: 'advanced-caller' },
contexts: [],
@@ -117,7 +117,7 @@ describe('Agent', () => {
await ctx.fiber.dispose()
})
it('send() throws after disposal', async () => {
it('followup() throws after disposal', async () => {
const adapter = new MockAdapter(['hang'])
const ctx = await harness(adapter)
let agent!: Agent
@@ -129,7 +129,7 @@ describe('Agent', () => {
await fiber.dispose()
await driverDone(agent)
expect(() => { agent.send([{ type: 'text', text: 'too late' }]) }).toThrow('disposed')
expect(() => { agent.followup([{ type: 'text', text: 'too late' }]) }).toThrow('disposed')
})
it('disposal discards still-pending inbox items so every id gets a terminal event', async () => {
@@ -490,7 +490,7 @@ describe('Agent', () => {
const { agent } = prepared
prepared.markPublished()
const dispose = prepared.startDriver()
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await new Promise(r => setTimeout(r, 30))
expect(agent.status).toBe('running')

View File

@@ -33,7 +33,7 @@ async function harness(adapter: MockAdapter) {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
/** Resolve on the agent's next idle transition (event-based, not status poll). */
@@ -63,7 +63,7 @@ describe('Agent.cancel()', () => {
ctx.on('agent/cancel-requested', (subject, cause) => {
if (subject !== agent) return
seen.push(`first:${cause.kind}`)
subject.send([{ type: 'text', text: 'queued by cancel observer' }])
subject.followup([{ type: 'text', text: 'queued by cancel observer' }])
throw new Error('observer failed')
})
ctx.on('agent/cancel-requested', (subject, cause) => {

View File

@@ -98,7 +98,7 @@ describe('config-driven session id', () => {
first = ctx.agents.get(SessionId('config-exact-reload'))
}
expect(first).toBeDefined()
first!.send([{ type: 'text', text: 'remember me' }], { source: { kind: 'user' } })
first!.followup([{ type: 'text', text: 'remember me' }], { source: { kind: 'user' } })
await waitForIdle(ctx, first!)
await firstLoop.dispose()
@@ -110,7 +110,7 @@ describe('config-driven session id', () => {
}
expect(second).toBeDefined()
expect(JSON.stringify(second!.session.deriveMessages())).toContain('remember me')
second!.send([{ type: 'text', text: 'continue' }], { source: { kind: 'user' } })
second!.followup([{ type: 'text', text: 'continue' }], { source: { kind: 'user' } })
await waitForIdle(ctx, second!)
await ctx.sessions.flush(second!.session)
const loaded = await ctx.sessionPersistence.load(SessionId('config-exact-reload'))
@@ -335,7 +335,7 @@ describe('config-driven session id', () => {
expect(a1.id).toBe(a1.session.id)
expect(a1.session.id).toMatch(idPattern)
expect(ctx1.agents.get(SessionId('cfg'))).toBeUndefined()
a1.send([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
await waitForIdle(ctx1, a1)
await ctx1.fiber.dispose()
@@ -354,7 +354,7 @@ describe('config-driven session id', () => {
expect(a2.id).toBe(a2.session.id)
expect(a2.session.id).toMatch(idPattern)
expect(a2.session.id).not.toBe(a1.session.id)
a2.send([{ type: 'text', text: 'q2' }], { source: { kind: 'user' } })
a2.followup([{ type: 'text', text: 'q2' }], { source: { kind: 'user' } })
await waitForIdle(ctx2, a2)
await ctx2.fiber.dispose()
})
@@ -375,7 +375,7 @@ describe('config-driven session id', () => {
await ctx1.plugin(SessionPersistenceJsonl, { root })
ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('first')]))
const a1 = (await ctx1.agents.create({ sessionId: SessionId('sticky-1') })).agent
a1.send([{ type: 'text', text: 'remember me' }], { source: { kind: 'user' } })
a1.followup([{ type: 'text', text: 'remember me' }], { source: { kind: 'user' } })
await waitForIdle(ctx1, a1)
await ctx1.fiber.dispose()

View File

@@ -50,7 +50,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
describe('session log records what agent/step-result actually produced', () => {
@@ -818,7 +818,7 @@ describe('adapter registration, routing, and accepted-input ownership', () => {
source: { kind: 'plugin', plugin: 'context-source' },
meta: { version: 1 },
}]
agent.send(content, { source, contexts })
agent.followup(content, { source, contexts })
content[0]!.text = 'caller-mutated-send'
source.plugin = 'caller-mutated-source'
contexts[0]!.content[0] = { type: 'text', text: 'caller-mutated-context' }
@@ -874,7 +874,7 @@ describe('adapter registration, routing, and accepted-input ownership', () => {
notifiedContexts = info.contexts
})
agent.send([{ type: 'text', text: 'start' }])
agent.followup([{ type: 'text', text: 'start' }])
await entered.promise
expect(agent.status).toBe('running')
const content = [{ type: 'text' as const, text: 'accepted-steer' }]
@@ -991,7 +991,7 @@ describe('turn numbering continues across seeded sessions', () => {
const turns: number[] = []
ctx2.on('session/event', (_s, event) => { if (event.type === 'turn/start') turns.push(event.data.turn) })
forked.send([{ type: 'text', text: 'continue' }])
forked.followup([{ type: 'text', text: 'continue' }])
await new Promise<void>((resolve) => {
ctx2.on('agent/status', (subject, status) => {
if (subject === forked && status === 'idle') resolve()

View File

@@ -38,7 +38,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
describe('inbox acceptance', () => {
@@ -50,10 +50,10 @@ describe('inbox acceptance', () => {
ctx.on('agent/inbox/enqueue', () => { queued += 1 })
expect(() => {
agent.send([{ type: 'text', text: 'first', bad: 1n } as never])
agent.followup([{ type: 'text', text: 'first', bad: 1n } as never])
}).toThrow(/losslessly JSON-serializable/)
expect(() => {
agent.send([{ type: 'text', text: 'first' }], { source: { kind: 'plugin', plugin: 'p', bad: 1n } as never })
agent.followup([{ type: 'text', text: 'first' }], { source: { kind: 'plugin', plugin: 'p', bad: 1n } as never })
}).toThrow(/losslessly JSON-serializable/)
expect(queued).toBe(0)
expect(agent.session.events).toHaveLength(0)

View File

@@ -55,7 +55,7 @@ describe('inbox FIFO-conservation invariant', () => {
return { action: 'continue' as const, reason: { content: [{ type: 'text', text: 'keep going' }], source: { kind: 'plugin', plugin: 'loop' } } }
})
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await waitForIdle(ctx, agent)
expect(adapter.requests).toHaveLength(2)
@@ -80,7 +80,7 @@ describe('inbox FIFO-conservation invariant', () => {
return { action: 'continue' as const, reason: { content: [{ type: 'text', text: 'keep going' }], source: { kind: 'plugin', plugin: 'loop' } } }
})
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await waitForIdle(ctx, agent)
expect(warn.mock.calls.flat().some(arg => String(arg).includes('agent/inbox'))).toBe(false)
@@ -110,7 +110,7 @@ describe('inbox FIFO-conservation invariant', () => {
return { action: 'stop' as const }
})
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await waitForIdle(ctx, agent)
expect(discards).toEqual([1]) // the dropped steering item was reported
@@ -142,7 +142,7 @@ describe('inbox FIFO-conservation invariant', () => {
agent.steer([{ type: 'text', text: 'late' }], { source: { kind: 'plugin', plugin: 'late' } })
})
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await waitForIdle(ctx, agent)
// The prompt plus the late steer both enqueued; both are matched (the prompt

View File

@@ -42,7 +42,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
function events(agent: Agent): SessionEvent[] {
@@ -128,7 +128,7 @@ describe('agent/prompt-submit', () => {
? downstream
: { ...downstream, content: [{ type: 'text', text: 'rewritten request' }] }
})
agent.send([{ type: 'text', text: 'original request' }], {
agent.followup([{ type: 'text', text: 'original request' }], {
contexts: [{
content: [{ type: 'text', text: 'untrusted prefix' }],
source: { kind: 'plugin', plugin: 'prefix' },
@@ -203,7 +203,7 @@ describe('agent/prompt-submit', () => {
const reasons: TurnEndReason[] = []
ctx.on('session/event', (_s, event: SessionEvent) => { if (event.type === 'turn/end') reasons.push(event.data.reason) })
agent.send([{ type: 'text', text: 'do something' }], {
agent.followup([{ type: 'text', text: 'do something' }], {
contexts: [{ content: [{ type: 'text', text: 'must be dropped' }], source: { kind: 'plugin', plugin: 'test' } }],
})
await waitForIdle(ctx, agent)

View File

@@ -42,7 +42,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
describe('agent loop', () => {
@@ -539,7 +539,7 @@ describe('agent loop', () => {
}))
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }], { meta: { prompt: 1 } })
agent.followup([{ type: 'text', text: 'go' }], { meta: { prompt: 1 } })
await waitForIdle(ctx, agent)
const user = agent.session.events.find(e => e.type === 'user/message')
@@ -1083,9 +1083,9 @@ describe('agent loop', () => {
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
const idle = waitForIdle(ctx, agent)
agent.send([{ type: 'text', text: 'user message' }])
agent.followup([{ type: 'text', text: 'user message' }])
await Promise.resolve()
agent.send(
agent.followup(
[{ type: 'text', text: 'plugin message' }],
{ source: { kind: 'plugin', plugin: 'test' } },
)

View File

@@ -115,7 +115,7 @@ describe('agent loop scheduling properties', () => {
const { seen: trace } = recordStatus(ctx, agent)
const idle = nextIdle(ctx, agent)
// Send all in one synchronous tick: they queue before the loop wakes.
for (const text of texts) agent.send([{ type: 'text', text }])
for (const text of texts) agent.followup([{ type: 'text', text }])
await idle
// No message lost: every send appears as a user/message, in order.
@@ -142,7 +142,7 @@ describe('agent loop scheduling properties', () => {
const agent = ctx.agentLoop.create(SessionId('a'), { provider: 'mock', model: 'mock' })
for (const text of texts) {
const idle = nextIdle(ctx, agent)
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
await idle
}
// Each send was drained at a separate turn start: N turns, 1..N.
@@ -171,7 +171,7 @@ describe('agent loop scheduling properties', () => {
for (const step of steps) {
const idle = nextIdle(ctx, agent)
lastIdle = idle
agent.send([{ type: 'text', text: step.text }])
agent.followup([{ type: 'text', text: step.text }])
if (step.settle) await idle
}
await lastIdle

View File

@@ -73,10 +73,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('log-derived request cache hits (
const agent = ctx.agentLoop.create(SessionId('cache-e2e'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
// Turn 1: forces a tool call → at least two steps (two model requests).
agent.send([{ type: 'text', text: 'Look up the key "deploy-color" with the lookup tool and tell me the value.' }])
agent.followup([{ type: 'text', text: 'Look up the key "deploy-color" with the lookup tool and tell me the value.' }])
await waitForIdle(ctx, agent)
// Turn 2: a follow-up over the same (longer) prefix.
agent.send([{ type: 'text', text: 'Thanks. Repeat that value one more time.' }])
agent.followup([{ type: 'text', text: 'Thanks. Repeat that value one more time.' }])
await waitForIdle(ctx, agent)
const usages = [...agent.session.events]

View File

@@ -41,7 +41,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent, text: string) {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
}
/** Assert `previous` is a strict value-prefix of `current`. */

View File

@@ -114,7 +114,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
}
function send(agent: Agent): void {
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
}
function contextError(message = 'context too large'): LlmError {

View File

@@ -125,7 +125,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const adapter1 = new MockAdapter([textResponse('a')])
const { ctx: ctx1, root } = await persistentHarness(adapter1)
const a1 = (await ctx1.agents.create({ sessionId: SessionId('nocwd-sess') })).agent
a1.send([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
await waitForIdle(ctx1, a1)
await ctx1.fiber.dispose()
@@ -153,7 +153,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
ctx1.on('agent/session-start', (_agent, source) => void sources1.push(source))
const a1 = (await ctx1.agents.create({ sessionId: SessionId('start-sess') })).agent
expect(sources1).toEqual(['startup'])
a1.send([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
await waitForIdle(ctx1, a1)
await ctx1.fiber.dispose()
@@ -459,7 +459,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const adapter1 = new MockAdapter([textResponse('answer')])
const { ctx: ctx1, root } = await persistentHarness(adapter1)
const a1 = (await ctx1.agents.create({ sessionId: SessionId('inject-sess'), meta: { cwd: '/w' } })).agent
a1.send([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
await waitForIdle(ctx1, a1)
a1.inject([{ type: 'text', text: 'background task 42 finished' }], { source: { kind: 'plugin', plugin: 'tool-bash' } })
// Let inject()'s fire-and-forget flush settle (NO explicit flush/dispose).
@@ -482,7 +482,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const adapter1 = new MockAdapter([textResponse('answer')])
const { ctx: ctx1, root } = await persistentHarness(adapter1)
const a1 = (await ctx1.agents.create({ sessionId: SessionId('inject-sess'), meta: { cwd: '/w' } })).agent
a1.send([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
a1.followup([{ type: 'text', text: 'q' }], { source: { kind: 'user' } })
await waitForIdle(ctx1, a1)
a1.inject([{ type: 'text', text: 'background task 42 finished' }], { source: { kind: 'plugin', plugin: 'tool-bash' } })
await ctx1.sessions.flush(a1.session)
@@ -510,7 +510,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
const adapter1 = new MockAdapter([textResponse('first answer')])
const { ctx: ctx1, root } = await persistentHarness(adapter1)
const a1 = (await ctx1.agents.create({ sessionId: SessionId('sess-resume'), meta: { cwd: '/w' } })).agent
a1.send([{ type: 'text', text: 'first question' }], { source: { kind: 'user' } })
a1.followup([{ type: 'text', text: 'first question' }], { source: { kind: 'user' } })
await waitForIdle(ctx1, a1)
const events1 = [...a1.session.events]
const seqs1 = events1.map(e => e.seq)
@@ -537,7 +537,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
expect(a2.session.deriveMessages()).toEqual(replay.deriveMessages())
// …and a new turn continues numbering (turn 2) with contiguous seqs.
a2.send([{ type: 'text', text: 'second question' }], { source: { kind: 'user' } })
a2.followup([{ type: 'text', text: 'second question' }], { source: { kind: 'user' } })
await waitForIdle(ctx2, a2)
const allSeqs = a2.session.events.map(e => e.seq)
expect(allSeqs).toEqual(allSeqs.map((_, i) => i)) // 0..N contiguous, no duplicates

View File

@@ -203,11 +203,11 @@ describe('agent scope lifecycle', () => {
if (event.type === 'user/message') heard.push('a-sees:user-message')
})
b.send(text('for b'))
b.followup(text('for b'))
await waitForIdle(ctx, b)
expect(heard).toEqual([]) // nothing of b's leaked into a's scope
a.send(text('for a'))
a.followup(text('for a'))
await waitForIdle(ctx, a)
expect(heard).toContain('a-sees:a:running')
expect(heard).toContain('a-sees:user-message')
@@ -934,7 +934,7 @@ describe('agent scope lifecycle', () => {
if (event.type === 'turn/start') { off(); resolve() }
})
})
agent.send(text('work'))
agent.followup(text('work'))
await turnOpen
await owner.dispose()
expect(order).toEqual(['turn-end', 'disposed(listed=false)', 'session-still-stored=true'])

View File

@@ -105,7 +105,7 @@ describe('tool-call scheduler: grouping and barriers', () => {
ctx.tools.register(gated.tool)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 3)
expect(gated.started).toEqual(['1', '2', '3'])
gated.release('1'); gated.release('2'); gated.release('3')
@@ -133,7 +133,7 @@ describe('tool-call scheduler: grouping and barriers', () => {
async execute(args) { order.push(`w-${args.id}`); return [{ type: 'text', text: 'w' }] },
}))
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await waitForIdle(ctx, agent)
expect(order).toEqual(['r-start-A1', 'r-end-A1', 'w-A2', 'r-start-A3', 'r-end-A3'])
@@ -169,7 +169,7 @@ describe('tool-call scheduler: grouping and barriers', () => {
}))
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => replacement.started.length === 1)
await new Promise(r => setTimeout(r, 5))
expect(replacement.started).toEqual(['1'])
@@ -200,7 +200,7 @@ describe('tool-call scheduler: grouping and barriers', () => {
})
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => initial.started.length === 2)
initial.release('1')
await until(() => events(agent).some(event =>
@@ -226,7 +226,7 @@ describe('tool-call scheduler: model-order results despite out-of-order settleme
ctx.tools.register(gated.tool)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 2)
gated.release('2')
await new Promise(r => setTimeout(r, 5))
@@ -248,7 +248,7 @@ describe('tool-call scheduler: model-order results despite out-of-order settleme
const gated = gatedParallelTool('p')
ctx.tools.register(gated.tool)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 2)
gated.release('2'); gated.release('1')
await waitForIdle(ctx, agent)
@@ -294,7 +294,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
ctx.tools.register(gated.tool)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 2)
await new Promise(r => setTimeout(r, 5))
expect(gated.started).toEqual(['1', '2'])
@@ -323,7 +323,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
const gated = gatedParallelTool('p')
ctx.tools.register(gated.tool)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 1)
await new Promise(r => setTimeout(r, 5))
expect(gated.started).toEqual(['1'])
@@ -349,7 +349,7 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () =>
const gated = gatedParallelTool('p')
ctx.tools.register(gated.tool)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 1)
await new Promise(r => setTimeout(r, 5))
expect(gated.started).toEqual(['1'])
@@ -376,7 +376,7 @@ describe('tool-call scheduler: ordered middleware and additional contexts', () =
ctx.on('tools/post-execute', async (exec, _result, next): Promise<PostToolDecision> => { post.push(String(exec.callId)); return next() })
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 3)
gated.release('3'); gated.release('2'); gated.release('1')
await waitForIdle(ctx, agent)
@@ -397,7 +397,7 @@ describe('tool-call scheduler: ordered middleware and additional contexts', () =
({ kind: 'accept', additionalContexts: [{ content: [{ type: 'text', text: `ctx-${exec.callId}` }], source: { kind: 'plugin', plugin: 'p' } }] }))
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 2)
gated.release('2'); gated.release('1')
await waitForIdle(ctx, agent)
@@ -435,7 +435,7 @@ describe('tool-call scheduler: ordered middleware and additional contexts', () =
})
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 1)
gated.release('1')
await waitForIdle(ctx, agent)
@@ -465,7 +465,7 @@ describe('tool-call scheduler: abort handling', () => {
}
})
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await waitForIdle(ctx, agent)
expect(gated.started).toEqual([])
@@ -497,7 +497,7 @@ describe('tool-call scheduler: abort handling', () => {
return next()
})
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await waitForIdle(ctx, agent)
expect(gated.started).toEqual([])
@@ -527,7 +527,7 @@ describe('tool-call scheduler: abort handling', () => {
}))
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 2)
agent.cancel({ kind: 'user' })
gated.release('1')
@@ -578,7 +578,7 @@ describe('tool-call scheduler: abort handling', () => {
}))
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await until(() => gated.started.length === 2)
agent.cancel({ kind: 'user' })
gated.release('1')

View File

@@ -58,7 +58,7 @@ async function runTurn(registrationOrder: string[], toolOrder?: SystemPromptConf
const ctx = await harness(adapter, toolOrder)
for (const name of registrationOrder) registerNamed(ctx, name)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await waitForIdle(ctx, agent)
return { ctx, agent, adapter }
}
@@ -100,7 +100,7 @@ describe('loop-level canonical tool order', () => {
const errors: Error[] = []
ctx.on('agent/error', (_agent, _turn, _step, error) => void errors.push(error))
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'go' }])
agent.followup([{ type: 'text', text: 'go' }])
await waitForIdle(ctx, agent)
expect(adapter.requests).toHaveLength(0)
expect(errors.map(e => e.message)).toEqual(['toolOrder lists unregistered tool "ghost"; known tools: alpha'])

View File

@@ -34,7 +34,7 @@ async function harness(adapter: MockAdapter): Promise<Context> {
}
function send(agent: Agent, text = 'go'): Promise<void> {
agent.send([{ type: 'text', text }])
agent.followup([{ type: 'text', text }])
return agent.whenIdle()
}
@@ -116,7 +116,7 @@ describe('agent/turn-stop', () => {
ctx.on('session/flush', (session) => {
if (session !== agent.session || queued) return
queued = true
agent.send([{ type: 'text', text: 'ordinary queued follow-up' }])
agent.followup([{ type: 'text', text: 'ordinary queued follow-up' }])
})
await send(agent)