refactor(agent): unify agent-scoped event signatures as payload objects
All agent/* and agent-loop/config-start-failed events take one payload object carrying the agent subject; waterfall/serial payloads require a signal and keep next as the final argument. PreStepContext and RequestFailureContext are unfolded into payloads and retired. goal/changed follows the same shape so agentEvents keeps its listener error containment. ReactLoopAgent builds its scope carrier once in the constructor. Regenerates scope resolvers, tool-cordis api catalog, and docs catalogs; updates all affected listeners, tests, and the core-data-structures docs (en + zh).
This commit is contained in:
@@ -11,9 +11,10 @@ import type {
|
||||
AgentStatus,
|
||||
CancelOptions,
|
||||
InboxTarget,
|
||||
PreStepDecision,
|
||||
RequestErrorAction,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import { Inbox, agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from '@deepseek-ai/dsh-agent'
|
||||
import { Inbox, agentCarrier, assembleContextFor, emitAgentEvent } from '@deepseek-ai/dsh-agent'
|
||||
import type { GenerateOptions, LlmCallConfig, Message, PreparedLlmCall } from '@deepseek-ai/dsh-llm'
|
||||
import {
|
||||
BlockAssembler,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
errorChain,
|
||||
markAgentLoopRequest,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import type { Scope } from '@deepseek-ai/dsh-scope'
|
||||
import type { Scope, Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import { createScope } from '@deepseek-ai/dsh-scope'
|
||||
import type { EpochHeader, RequestContext, Session, SessionId, TurnEndReason, UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { canonicalHeader, headerEquals } from '@deepseek-ai/dsh-session'
|
||||
@@ -68,6 +69,9 @@ export class ReactLoopAgent implements Agent {
|
||||
readonly scope: Scope
|
||||
readonly ctx: Context
|
||||
|
||||
/** Fused scope carrier, built once in the constructor for every dispatch. */
|
||||
readonly carrier: Scoped<Agent>
|
||||
|
||||
/** Whether this loop instance has appended its initial/resume request anchor. */
|
||||
private requestHeaderLogged = false
|
||||
private readonly runtimeContext: RuntimeContextProjection
|
||||
@@ -78,6 +82,7 @@ export class ReactLoopAgent implements Agent {
|
||||
public readonly options: AgentOptions,
|
||||
public readonly session: Session,
|
||||
) {
|
||||
this.carrier = agentCarrier(this)
|
||||
this.inbox = new Inbox(session, {
|
||||
inserted: (message) => { emitAgentEvent(loopCtx, this, 'agent/inbox/inserted', { message }) },
|
||||
discarded: (message) => { emitAgentEvent(loopCtx, this, 'agent/inbox/discarded', { message }) },
|
||||
@@ -100,7 +105,7 @@ export class ReactLoopAgent implements Agent {
|
||||
this.phase = next
|
||||
const status = this.status
|
||||
if (status !== previousStatus) {
|
||||
emitAgentEvent(this.loopCtx, this, 'agent/status', status)
|
||||
emitAgentEvent(this.loopCtx, this, 'agent/status', { status })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +183,7 @@ export class ReactLoopAgent implements Agent {
|
||||
private throwError(error: unknown): never {
|
||||
const turn = this.phase.kind === 'running' ? this.phase.turn : this.phase.lastTurn
|
||||
const step = this.phase.kind === 'running' ? this.phase.step : 0
|
||||
emitAgentEvent(this.loopCtx, this, 'agent/error', turn, step, error)
|
||||
emitAgentEvent(this.loopCtx, this, 'agent/error', { turn, step, error })
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -203,9 +208,9 @@ export class ReactLoopAgent implements Agent {
|
||||
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
|
||||
signal.throwIfAborted()
|
||||
const context = this.runtimeContext.project(renderContextSnapshot(assembly))
|
||||
const decision = await agentEvents(this.loopCtx, this).waterfall(
|
||||
'agent/pre-step', claimed, { ...position, signal },
|
||||
() => Promise.resolve({
|
||||
const decision = await this.loopCtx.waterfall(
|
||||
this.carrier, 'agent/pre-step', { agent: this, messages: claimed, ...position, signal },
|
||||
(): Promise<PreStepDecision> => Promise.resolve<PreStepDecision>({
|
||||
kind: 'enter',
|
||||
messages: context === undefined ? claimed : [...claimed, context],
|
||||
}),
|
||||
@@ -265,7 +270,7 @@ export class ReactLoopAgent implements Agent {
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
if (turnEnds && this.inbox.nextStep.length === 0) {
|
||||
await this.loopCtx.serial(agentCarrier(this), 'agent/turn-stopping', this, turn, signal)
|
||||
await this.loopCtx.serial(this.carrier, 'agent/turn-stopping', { agent: this, turn, signal })
|
||||
signal.throwIfAborted()
|
||||
}
|
||||
if (turnEnds && this.inbox.nextStep.length === 0) break
|
||||
@@ -323,13 +328,15 @@ export class ReactLoopAgent implements Agent {
|
||||
const finish = assembler.finish
|
||||
if (finish.kind === 'error' || finish.kind === 'aborted') {
|
||||
const action = await this.loopCtx.waterfall(
|
||||
agentCarrier(this), 'agent/request-error', this, {
|
||||
this.carrier, 'agent/request-error', {
|
||||
agent: this,
|
||||
turn,
|
||||
step,
|
||||
provider: request.provider,
|
||||
failure: finish.failure,
|
||||
retryPolicy: preparedCall?.retryPolicy,
|
||||
}, signal,
|
||||
signal,
|
||||
},
|
||||
() => Promise.resolve<RequestErrorAction>(undefined),
|
||||
)
|
||||
signal.throwIfAborted()
|
||||
@@ -405,7 +412,7 @@ export class ReactLoopAgent implements Agent {
|
||||
},
|
||||
))
|
||||
const proposedConfig = await this.loopCtx.waterfall(
|
||||
agentCarrier(this), 'agent/request', this, turn, step, signal,
|
||||
this.carrier, 'agent/request', { agent: this, turn, step, signal },
|
||||
() => Promise.resolve(seedConfig),
|
||||
)
|
||||
signal.throwIfAborted()
|
||||
|
||||
@@ -175,11 +175,11 @@ declare module 'cordis' {
|
||||
* Consumers that buffer work for the configured identity use this
|
||||
* transient signal to reject that work instead of waiting forever. Normal
|
||||
* factory teardown suppresses failures from the cancelled startup attempt.
|
||||
* @param sessionId - exact shared agent/session identity that failed startup.
|
||||
* @param error - persistence, setup, or publication failure.
|
||||
* @param payload.sessionId - exact shared agent/session identity that failed startup.
|
||||
* @param payload.error - persistence, setup, or publication failure.
|
||||
* @mode emit
|
||||
*/
|
||||
'agent-loop/config-start-failed'(sessionId: SessionId, error: unknown): void
|
||||
'agent-loop/config-start-failed'(payload: { sessionId: SessionId; error: unknown }): void
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
): void {
|
||||
if (!this.ownership.isActive()) return
|
||||
this.ctx.logger.warn(`agent "${configId}": config-driven ${action} of "${sessionId}" failed: ${errorChain(error)}`)
|
||||
const args: unknown[] = ['agent-loop/config-start-failed', sessionId, error]
|
||||
const args: unknown[] = ['agent-loop/config-start-failed', { sessionId, error }]
|
||||
for (const callback of this.ctx.events.dispatch('emit', args)) {
|
||||
try {
|
||||
const returned: unknown = callback(...args)
|
||||
@@ -400,7 +400,7 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
released.resolve()
|
||||
}
|
||||
}
|
||||
const disposeAgentListener = ownerCtx.on('agent/disposed', checkReleased)
|
||||
const disposeAgentListener = ownerCtx.on('agent/disposed', () => { checkReleased() })
|
||||
const disposeSessionListener = ownerCtx.on('session/disposed', checkReleased)
|
||||
try {
|
||||
checkReleased()
|
||||
@@ -525,7 +525,7 @@ export class AgentLoop extends Service implements AgentFactory {
|
||||
// A synchronous announce/session-start listener may have started
|
||||
// teardown; the machine is already live (delivery works from the
|
||||
// session-start seam), so only the liveness recheck is owed.
|
||||
emitAgentEvent(loopCtx, agent, 'agent/session-start', source)
|
||||
emitAgentEvent(loopCtx, agent, 'agent/session-start', { source })
|
||||
assertLive()
|
||||
return { agent, dispose }
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ async function harness(adapter: LlmAdapter): Promise<Harness> {
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
@@ -164,18 +164,18 @@ describe('AgentLoop initiator scope', () => {
|
||||
if (context.agent === agent) capture(context.signal)
|
||||
return next()
|
||||
})
|
||||
ctx.on('agent/pre-step', async (subject, _message, { signal }, next) => {
|
||||
ctx.on('agent/pre-step', async ({ agent: subject, signal }, next) => {
|
||||
if (subject === agent) {
|
||||
expect(ctx.agents.requireInitiator()).toBe(agent)
|
||||
preStepSignals.push(signal)
|
||||
}
|
||||
return next()
|
||||
})
|
||||
ctx.on('agent/request', async (subject, _turn, _step, signal, next) => {
|
||||
ctx.on('agent/request', async ({ agent: subject, signal }, next) => {
|
||||
if (subject === agent) capture(signal)
|
||||
return next()
|
||||
})
|
||||
ctx.on('agent/turn-stopping', (subject, _turn, signal) => {
|
||||
ctx.on('agent/turn-stopping', ({ agent: subject, signal }) => {
|
||||
if (subject === agent) capture(signal)
|
||||
})
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
|
||||
@@ -60,17 +60,17 @@ describe('Agent', () => {
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (session === agent.session && event.type === 'turn/start') lifecycle.push('turn/start')
|
||||
})
|
||||
ctx.on('agent/inbox/inserted', (subject, event) => {
|
||||
if (subject === agent) inserted.push(event)
|
||||
ctx.on('agent/inbox/inserted', ({ agent: subject, message }) => {
|
||||
if (subject === agent) inserted.push({ message })
|
||||
})
|
||||
ctx.on('agent/inbox/claimed', (subject, event) => {
|
||||
ctx.on('agent/inbox/claimed', ({ agent: subject, message, turn }) => {
|
||||
if (subject === agent) {
|
||||
lifecycle.push('agent/inbox/claimed')
|
||||
claimed.push(event)
|
||||
claimed.push({ message, turn })
|
||||
}
|
||||
})
|
||||
ctx.on('agent/inbox/discarded', (subject, event) => {
|
||||
if (subject === agent) discarded.push(event)
|
||||
ctx.on('agent/inbox/discarded', ({ agent: subject, message }) => {
|
||||
if (subject === agent) discarded.push({ message })
|
||||
})
|
||||
const context = createUserMessage({
|
||||
content: [{ type: 'text', text: 'discard me' }],
|
||||
@@ -114,7 +114,7 @@ describe('Agent', () => {
|
||||
const ctx = await harness(new MockAdapter([textResponse('ok')]))
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
const statuses: string[] = []
|
||||
ctx.on('agent/status', (subject, status) => {
|
||||
ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent) statuses.push(status)
|
||||
})
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('Agent', () => {
|
||||
const ctx = await harness(new MockAdapter([textResponse('ok')]))
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/status', (_subject, status) => {
|
||||
ctx.on('agent/status', ({ status }) => {
|
||||
throw new Error(`bad ${status} listener`)
|
||||
})
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ function send(agent: Agent, text: string) {
|
||||
/** Resolve on the agent's next idle transition (event-based, not status poll). */
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') { dispose(); resolve() }
|
||||
})
|
||||
})
|
||||
@@ -156,7 +156,7 @@ describe('Agent.cancel()', () => {
|
||||
|
||||
const running = Promise.withResolvers<undefined>()
|
||||
let disposalDone: Promise<void> | undefined
|
||||
ctx.on('agent/status', (subject, status) => {
|
||||
ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject !== agent || status !== 'running') return
|
||||
disposalDone = handle.dispose()
|
||||
running.resolve(undefined)
|
||||
@@ -200,7 +200,7 @@ describe('Agent.cancel()', () => {
|
||||
|
||||
const replacementRegistered = Promise.withResolvers<undefined>()
|
||||
let replacementObservation: Promise<{ status: string; requests: number; turns: number }> | undefined
|
||||
ctx.on('agent/status', (subject, status) => {
|
||||
ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject !== agent || status !== 'idle' || replacementObservation !== undefined) return
|
||||
send(agent, 'cancelled replacement')
|
||||
replacementObservation = agent.whenIdle().then(() => ({
|
||||
@@ -239,7 +239,7 @@ describe('Agent.cancel()', () => {
|
||||
|
||||
const replacementRegistered = Promise.withResolvers<undefined>()
|
||||
let replacementIdle: Promise<void> | undefined
|
||||
ctx.on('agent/status', (subject, status) => {
|
||||
ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject !== agent || status !== 'idle' || replacementIdle !== undefined) return
|
||||
send(agent, 'cancelled replacement')
|
||||
agent.cancel({ kind: 'user' })
|
||||
@@ -440,7 +440,7 @@ describe('Agent.cancel()', () => {
|
||||
})
|
||||
|
||||
let cancelled = false
|
||||
ctx.on('agent/turn-stopping', (subject) => {
|
||||
ctx.on('agent/turn-stopping', ({ agent: subject }) => {
|
||||
if (subject === agent && !cancelled) {
|
||||
cancelled = true
|
||||
agent.cancel({ kind: 'user' })
|
||||
@@ -465,7 +465,7 @@ describe('Agent.cancel()', () => {
|
||||
// durable turn-start commit and must drop the reserved work.
|
||||
let streamed = false
|
||||
ctx.on('session/event', (_s, event) => { if (event.type === 'assistant/chunk') streamed = true })
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'running') agent.cancel({ kind: 'user' })
|
||||
})
|
||||
|
||||
@@ -485,7 +485,7 @@ describe('Agent.cancel()', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
let replaced = false
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject !== agent || status !== 'running' || replaced) return
|
||||
replaced = true
|
||||
agent.cancel({ kind: 'user' })
|
||||
@@ -664,7 +664,7 @@ describe('Agent.cancel()', () => {
|
||||
|
||||
switch (stage) {
|
||||
case 'pre-step':
|
||||
ctx.on('agent/pre-step', async (subject, _message, { signal }, next) => {
|
||||
ctx.on('agent/pre-step', async ({ agent: subject, signal }, next) => {
|
||||
if (subject === agent) await blockUntilAbort(signal)
|
||||
return next()
|
||||
})
|
||||
@@ -679,13 +679,13 @@ describe('Agent.cancel()', () => {
|
||||
})
|
||||
break
|
||||
case 'request':
|
||||
ctx.on('agent/request', async (subject, _turn, _step, signal, next) => {
|
||||
ctx.on('agent/request', async ({ agent: subject, signal }, next) => {
|
||||
if (subject === agent) await blockUntilAbort(signal)
|
||||
return next()
|
||||
})
|
||||
break
|
||||
case 'stopping':
|
||||
ctx.on('agent/turn-stopping', async (subject, _turn, signal) => {
|
||||
ctx.on('agent/turn-stopping', async ({ agent: subject, signal }) => {
|
||||
if (subject === agent) await blockUntilAbort(signal)
|
||||
})
|
||||
break
|
||||
|
||||
@@ -19,7 +19,7 @@ afterEach(async () => { for (const d of dirs.splice(0)) await rm(d, { recursive:
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') { dispose(); resolve() }
|
||||
})
|
||||
})
|
||||
@@ -170,7 +170,7 @@ describe('config-driven session id', () => {
|
||||
await cleanupStarted.promise
|
||||
expect(first.status).toBe('idle')
|
||||
const failures: unknown[] = []
|
||||
ctx.on('agent-loop/config-start-failed', (_id, error) => { failures.push(error) })
|
||||
ctx.on('agent-loop/config-start-failed', ({ error }) => { failures.push(error) })
|
||||
const secondLoop = await ctx.plugin(AgentLoop, config)
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
expect(ctx.agents.get(sessionId)).toBe(first)
|
||||
@@ -234,7 +234,7 @@ describe('config-driven session id', () => {
|
||||
const failures: { sessionId: SessionId; error: unknown }[] = []
|
||||
ctx.on('agent-loop/config-start-failed', () => { throw listenerFailure })
|
||||
ctx.on('agent-loop/config-start-failed', () => Promise.reject(asyncListenerFailure) as never)
|
||||
ctx.on('agent-loop/config-start-failed', (sessionId, error) => {
|
||||
ctx.on('agent-loop/config-start-failed', ({ sessionId, error }) => {
|
||||
failures.push({ sessionId, error })
|
||||
})
|
||||
vi.spyOn(ctx.sessionPersistence, 'list').mockRejectedValue(failure)
|
||||
@@ -274,7 +274,7 @@ describe('config-driven session id', () => {
|
||||
// Deliberately violate the normal Error-only rejection rule to exercise the unknown boundary.
|
||||
// oxlint-disable-next-line typescript/prefer-promise-reject-errors
|
||||
ctx.on('agent-loop/config-start-failed', () => Promise.reject(unrenderable) as never)
|
||||
ctx.on('agent-loop/config-start-failed', (_sessionId, error) => { failures.push(error) })
|
||||
ctx.on('agent-loop/config-start-failed', ({ error }) => { failures.push(error) })
|
||||
vi.spyOn(ctx.sessionPersistence, 'list').mockRejectedValue(unrenderable)
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
|
||||
|
||||
@@ -307,7 +307,7 @@ describe('config-driven session id', () => {
|
||||
const released = vi.fn()
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
|
||||
const failures: unknown[] = []
|
||||
ctx.on('agent-loop/config-start-failed', (_sessionId, error) => { failures.push(error) })
|
||||
ctx.on('agent-loop/config-start-failed', ({ error }) => { failures.push(error) })
|
||||
|
||||
const loop = await ctx.plugin(AgentLoop, {
|
||||
agents: [{ id: 'main', sessionId: SessionId('config-exact-dispose'), model: 'mock' }],
|
||||
@@ -479,7 +479,7 @@ describe('startup reporting after factory teardown', () => {
|
||||
gate.promise.catch(() => undefined)
|
||||
vi.spyOn(ctx.sessionPersistence, 'list').mockReturnValue(gate.promise)
|
||||
const failures: unknown[] = []
|
||||
ctx.on('agent-loop/config-start-failed', (_id, error) => { failures.push(error) })
|
||||
ctx.on('agent-loop/config-start-failed', ({ error }) => { failures.push(error) })
|
||||
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined)
|
||||
|
||||
const loop = await ctx.plugin(AgentLoop, {
|
||||
|
||||
@@ -40,7 +40,7 @@ async function harness(adapter: MockAdapter) {
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
@@ -191,7 +191,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
const adapter = new MockAdapter([textResponse('must not run')])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a-empty-batch'), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/pre-step', (subject, _messages, _context, next) => {
|
||||
ctx.on('agent/pre-step', ({ agent: subject }, next) => {
|
||||
if (subject !== agent) return next()
|
||||
return Promise.resolve({ kind: 'enter', messages: [] })
|
||||
})
|
||||
@@ -288,7 +288,7 @@ describe('abort during tool execution ends the turn', () => {
|
||||
|
||||
send(agent, 'leave an unmatched historical call')
|
||||
await waitForIdle(ctx, agent)
|
||||
const disposeInjection = ctx.on('agent/pre-step', async (subject, _messages, { turn }, next) => {
|
||||
const disposeInjection = ctx.on('agent/pre-step', async ({ agent: subject, turn }, next) => {
|
||||
const decision = await next()
|
||||
if (subject === agent && turn === 2 && decision.kind === 'enter') {
|
||||
disposeInjection()
|
||||
@@ -382,7 +382,7 @@ describe('disposal leaves the two-state status contract balanced', () => {
|
||||
|
||||
const statuses: string[] = []
|
||||
const reasons: TurnEndReason[] = []
|
||||
ctx.on('agent/status', (_agent, status) => void statuses.push(status))
|
||||
ctx.on('agent/status', ({ status }) => void statuses.push(status))
|
||||
ctx.on('session/event', (_s, event) => { if (event.type === 'turn/end') reasons.push(event.data.reason) })
|
||||
|
||||
send(agent, 'go')
|
||||
@@ -411,7 +411,7 @@ describe('disposal leaves the two-state status contract balanced', () => {
|
||||
agent = inner.agentLoop.create(SessionId('scoped'), { provider: 'mock', model: 'mock' })
|
||||
}, { inject: ['agentLoop'] }))
|
||||
|
||||
ctx.on('agent/status', (_agent, status) => {
|
||||
ctx.on('agent/status', ({ status }) => {
|
||||
if (status === 'idle') throw new Error('broken status listener')
|
||||
})
|
||||
|
||||
@@ -457,7 +457,7 @@ describe('adapter registration, routing, and accepted-input ownership', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), {}) // no model — router plugin decides
|
||||
|
||||
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async (_payload, next) => {
|
||||
return { ...await next(), provider: 'mock', model: 'mock' }
|
||||
})
|
||||
|
||||
@@ -540,7 +540,7 @@ describe('turn numbering continues across seeded sessions', () => {
|
||||
ctx2.on('session/event', (_s, event) => { if (event.type === 'turn/start') turns.push(event.data.turn) })
|
||||
forked.followup(createUserMessage({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'user' } }))
|
||||
await new Promise<void>((resolve) => {
|
||||
ctx2.on('agent/status', (subject, status) => {
|
||||
ctx2.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === forked && status === 'idle') resolve()
|
||||
})
|
||||
})
|
||||
@@ -586,7 +586,7 @@ describe('a finish-error stream chunk ends the turn as error, not completed', ()
|
||||
|
||||
const reasons: TurnEndReason[] = []
|
||||
const errors: unknown[] = []
|
||||
ctx.on('agent/error', (_agent, turn, step, error) => {
|
||||
ctx.on('agent/error', ({ turn, step, error }) => {
|
||||
expect({ turn, step }).toEqual({ turn: 1, step: 1 })
|
||||
errors.push(error)
|
||||
})
|
||||
@@ -710,7 +710,7 @@ describe('turn and step boundary recovery', () => {
|
||||
if (event.type === 'step/start' && !threw) { threw = true; throw new Error('boom step-start') }
|
||||
})
|
||||
const errors: Error[] = []
|
||||
ctx.on('agent/error', (_a, _t, _s, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errors.push(error)
|
||||
})
|
||||
|
||||
@@ -743,7 +743,7 @@ describe('turn and step boundary recovery', () => {
|
||||
}
|
||||
})
|
||||
const errors: Error[] = []
|
||||
ctx.on('agent/error', (_agent, _turn, _step, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errors.push(error)
|
||||
})
|
||||
|
||||
@@ -800,7 +800,7 @@ describe('turn and step boundary recovery', () => {
|
||||
}
|
||||
})
|
||||
const errors: Error[] = []
|
||||
ctx.on('agent/error', (_agent, _turn, _step, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errors.push(error)
|
||||
})
|
||||
|
||||
@@ -893,14 +893,14 @@ describe('turn and step boundary recovery', () => {
|
||||
}, { inject: ['agentLoop'] }))
|
||||
|
||||
let threw = false
|
||||
ctx.on('agent/pre-step', (_subject, _messages, _context, next) => {
|
||||
ctx.on('agent/pre-step', (_payload, next) => {
|
||||
if (threw) return next()
|
||||
threw = true
|
||||
void fiber.dispose()
|
||||
throw new Error('boom pre-step during disposal')
|
||||
})
|
||||
const errorEmits: Error[] = []
|
||||
ctx.on('agent/error', (_a, _t, _s, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errorEmits.push(error)
|
||||
})
|
||||
|
||||
@@ -926,7 +926,7 @@ describe('turn and step boundary recovery', () => {
|
||||
if (!threw && event.type === 'turn/start') { threw = true; throw new Error('boom turn/start append') }
|
||||
})
|
||||
const errors: Error[] = []
|
||||
ctx.on('agent/error', (_a, _t, _s, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errors.push(error)
|
||||
})
|
||||
|
||||
@@ -959,7 +959,7 @@ describe('turn and step boundary recovery', () => {
|
||||
if (event.type === 'step/end' && !threw) { threw = true; throw new Error('boom step-end') }
|
||||
})
|
||||
const errors: Error[] = []
|
||||
ctx.on('agent/error', (_a, _t, _s, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errors.push(error)
|
||||
})
|
||||
|
||||
@@ -1000,7 +1000,7 @@ describe('turn and step boundary recovery', () => {
|
||||
if (!threw && event.type === 'step/end') { threw = true; throw new Error('boom step/end listener') }
|
||||
})
|
||||
const errors: Error[] = []
|
||||
ctx.on('agent/error', (_a, _t, _s, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errors.push(error)
|
||||
})
|
||||
|
||||
@@ -1215,7 +1215,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
|
||||
await mountInvariants(ctx)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
|
||||
ctx.on('agent/pre-step', async (_subject, _messages, _context, next) => {
|
||||
ctx.on('agent/pre-step', async (_payload, next) => {
|
||||
await blocker
|
||||
return next()
|
||||
})
|
||||
@@ -1261,7 +1261,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
|
||||
await mountInvariants(ctx)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
|
||||
ctx.on('agent/pre-step', async (_subject, _messages, _context, next) => {
|
||||
ctx.on('agent/pre-step', async (_payload, next) => {
|
||||
await blocker
|
||||
return next()
|
||||
})
|
||||
|
||||
@@ -28,7 +28,7 @@ async function harness(adapter: MockAdapter) {
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
@@ -120,7 +120,7 @@ describe('thrown-value propagation', () => {
|
||||
})
|
||||
|
||||
const errors: unknown[] = []
|
||||
ctx.on('agent/error', (_agent, _turn, _step, error) => void errors.push(error))
|
||||
ctx.on('agent/error', ({ error }) => void errors.push(error))
|
||||
|
||||
send(agent, 'fails before turn start')
|
||||
send(agent, 'survives as the next item')
|
||||
@@ -143,7 +143,7 @@ describe('thrown-value propagation', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
let threwOnce = false
|
||||
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async (_payload, next) => {
|
||||
if (!threwOnce) {
|
||||
threwOnce = true
|
||||
throw { code: 500 }
|
||||
@@ -167,7 +167,7 @@ describe('durable error rendering', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
let threwOnce = false
|
||||
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async (_payload, next) => {
|
||||
if (!threwOnce) {
|
||||
threwOnce = true
|
||||
throw new LlmError('server overloaded', 'RATE_LIMIT')
|
||||
@@ -250,7 +250,7 @@ describe('request-error action edges', () => {
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('retry-after-cancel'), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/request-error', async (subject) => {
|
||||
ctx.on('agent/request-error', async ({ agent: subject }) => {
|
||||
subject.cancel({ kind: 'user' })
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
@@ -271,7 +271,7 @@ describe('request-error action edges', () => {
|
||||
])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('retry-raced'), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/request-error', async (subject, _context, signal, next) => {
|
||||
ctx.on('agent/request-error', async ({ agent: subject, signal }, next) => {
|
||||
await next()
|
||||
subject.cancel({ kind: 'user' })
|
||||
expect(signal.aborted).toBe(true)
|
||||
@@ -350,7 +350,7 @@ describe('persistent step-close rejection', () => {
|
||||
if (event.type === 'step/end') throw new Error('step close permanently rejected')
|
||||
})
|
||||
const statuses: string[] = []
|
||||
ctx.on('agent/status', (subject, status) => { if (subject === agent) statuses.push(status) })
|
||||
ctx.on('agent/status', ({ agent: subject, status }) => { if (subject === agent) statuses.push(status) })
|
||||
|
||||
send(agent, 'go')
|
||||
await agent.whenIdle()
|
||||
@@ -406,7 +406,7 @@ describe('turn close failure containment', () => {
|
||||
}
|
||||
})
|
||||
const errors: unknown[] = []
|
||||
ctx.on('agent/error', (_agent, _turn, _step, error) => { errors.push(error) })
|
||||
ctx.on('agent/error', ({ error }) => { errors.push(error) })
|
||||
|
||||
send(agent, 'go')
|
||||
await agent.whenIdle()
|
||||
@@ -484,11 +484,11 @@ describe('driver bookkeeping edges', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('reject-next-step'), { provider: 'mock', model: 'mock' })
|
||||
let proposals = 0
|
||||
ctx.on('agent/pre-step', async (_subject, _messages, _context, next) => {
|
||||
ctx.on('agent/pre-step', async (_payload, next) => {
|
||||
proposals += 1
|
||||
return proposals === 2 ? { kind: 'reject' } : next()
|
||||
})
|
||||
ctx.on('agent/turn-stopping', (subject) => {
|
||||
ctx.on('agent/turn-stopping', ({ agent: subject }) => {
|
||||
subject.inject(createUserMessage({
|
||||
content: [{ type: 'text', text: 'do not enter the next step' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
|
||||
@@ -41,7 +41,7 @@ async function harness(adapter: MockAdapter) {
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
@@ -65,7 +65,7 @@ describe('agent/pre-step', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
const seen: string[] = []
|
||||
ctx.on('agent/pre-step', async (_agent, messages, _signal, next) => {
|
||||
ctx.on('agent/pre-step', async ({ messages }, next) => {
|
||||
seen.push(messages[0]!.content.map(b => (b.type === 'text' ? b.text : '')).join(''))
|
||||
return next()
|
||||
})
|
||||
@@ -92,8 +92,8 @@ describe('agent/pre-step', () => {
|
||||
}))
|
||||
const agent = ctx.agentLoop.create(SessionId('prompt-coordinates'), { provider: 'mock', model: 'mock' })
|
||||
const seen: Array<{ turn: number; step: number; messages: number }> = []
|
||||
ctx.on('agent/pre-step', async (_agent, messages, context, next) => {
|
||||
seen.push({ turn: context.turn, step: context.step, messages: messages.length })
|
||||
ctx.on('agent/pre-step', async ({ messages, turn, step }, next) => {
|
||||
seen.push({ turn, step, messages: messages.length })
|
||||
return next()
|
||||
})
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('agent/pre-step', () => {
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const decision = Promise.withResolvers<PreStepDecision>()
|
||||
const observed: UserMessage[] = []
|
||||
ctx.on('agent/pre-step', async (subject, messages) => {
|
||||
ctx.on('agent/pre-step', async ({ agent: subject, messages }) => {
|
||||
if (subject !== agent) return { kind: 'enter', messages }
|
||||
const message = messages[0]!
|
||||
expect(Object.isFrozen(message)).toBe(true)
|
||||
@@ -161,7 +161,7 @@ describe('agent/pre-step', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
ctx.on('agent/pre-step', async (_agent, messages): Promise<PreStepDecision> =>
|
||||
ctx.on('agent/pre-step', async ({ messages }): Promise<PreStepDecision> =>
|
||||
({
|
||||
kind: 'enter',
|
||||
messages: [{ ...messages[0]!, content: [{ type: 'text', text: 'REWRITTEN' }] }],
|
||||
@@ -182,7 +182,7 @@ describe('agent/pre-step', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
ctx.on('agent/pre-step', async (_agent, messages): Promise<PreStepDecision> =>
|
||||
ctx.on('agent/pre-step', async ({ messages }): Promise<PreStepDecision> =>
|
||||
({
|
||||
kind: 'enter',
|
||||
messages: [...messages, createUserMessage({
|
||||
@@ -211,15 +211,15 @@ describe('agent/pre-step', () => {
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
})
|
||||
ctx.on('agent/turn-stopping', (subject) => {
|
||||
ctx.on('agent/turn-stopping', ({ agent: subject }) => {
|
||||
subject.inject(createUserMessage({
|
||||
content: [{ type: 'text', text: 'pending context' }],
|
||||
source: { kind: 'plugin', plugin: 'test' },
|
||||
}))
|
||||
})
|
||||
ctx.on('agent/pre-step', async (_subject, _messages, context, next) => {
|
||||
ctx.on('agent/pre-step', async ({ step }, next) => {
|
||||
const decision = await next()
|
||||
return context.step === 1 || decision.kind === 'reject'
|
||||
return step === 1 || decision.kind === 'reject'
|
||||
? decision
|
||||
: { kind: 'enter', messages: [] }
|
||||
})
|
||||
@@ -262,7 +262,7 @@ describe('agent/pre-step', () => {
|
||||
const decision = Promise.withResolvers<PreStepDecision>()
|
||||
let claimed: UserMessage[] = []
|
||||
let firstProposal = true
|
||||
ctx.on('agent/pre-step', async (_agent, messages) => {
|
||||
ctx.on('agent/pre-step', async ({ messages }) => {
|
||||
if (!firstProposal) return { kind: 'enter', messages }
|
||||
firstProposal = false
|
||||
claimed = messages
|
||||
@@ -372,14 +372,14 @@ describe('agent/pre-step', () => {
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
})
|
||||
ctx.on('agent/pre-step', async (_agent, messages, _signal, next) => {
|
||||
ctx.on('agent/pre-step', async ({ messages }, next) => {
|
||||
const decision = await next()
|
||||
return messages.some(message =>
|
||||
message.content.some(block => block.type === 'text' && block.text === 'blocked prompt'))
|
||||
? { kind: 'reject' as const }
|
||||
: decision
|
||||
})
|
||||
ctx.on('agent/pre-step', async (subject, messages, _signal, next) => {
|
||||
ctx.on('agent/pre-step', async ({ agent: subject, messages }, next) => {
|
||||
if (messages.some(message =>
|
||||
message.content.some(block => block.type === 'text' && block.text === 'blocked prompt'))) {
|
||||
subject.inject(createUserMessage({
|
||||
@@ -482,7 +482,7 @@ describe('agent/pre-step', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
ctx.on('agent/pre-step', async (_agent, messages, _signal, next): Promise<PreStepDecision> => {
|
||||
ctx.on('agent/pre-step', async ({ messages }, next): Promise<PreStepDecision> => {
|
||||
const text = messages.flatMap(message => message.content)
|
||||
.map(b => (b.type === 'text' ? b.text : '')).join('')
|
||||
return text === 'secret'
|
||||
@@ -519,17 +519,17 @@ describe('agent/pre-step', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
let threw = false
|
||||
ctx.on('agent/pre-step', async (_agent, messages) => {
|
||||
ctx.on('agent/pre-step', async ({ messages }) => {
|
||||
if (!threw) { threw = true; throw new Error('prompt hook broke') }
|
||||
return { kind: 'enter' as const, messages }
|
||||
})
|
||||
const errors: Error[] = []
|
||||
const reasons: TurnEndReason[] = []
|
||||
const statuses: string[] = []
|
||||
ctx.on('agent/error', (_a, _t, _s, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errors.push(error)
|
||||
})
|
||||
ctx.on('agent/status', (subject, status) => { if (subject === agent) statuses.push(status) })
|
||||
ctx.on('agent/status', ({ agent: subject, status }) => { if (subject === agent) statuses.push(status) })
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (session === agent.session && event.type === 'turn/end') reasons.push(event.data.reason)
|
||||
})
|
||||
@@ -559,7 +559,7 @@ describe('agent/session-start', () => {
|
||||
const ctx = await harness(adapter)
|
||||
|
||||
const sources: SessionStartSource[] = []
|
||||
ctx.on('agent/session-start', (_agent, source) => void sources.push(source))
|
||||
ctx.on('agent/session-start', ({ source }) => void sources.push(source))
|
||||
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
// fires synchronously at create, before any turn
|
||||
@@ -576,7 +576,7 @@ describe('agent/session-start', () => {
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await harness(adapter)
|
||||
|
||||
ctx.on('agent/session-start', (agent) => {
|
||||
ctx.on('agent/session-start', ({ agent }) => {
|
||||
agent.inject(createUserMessage({ content: [{ type: 'text', text: 'session preamble' }], source: { kind: 'plugin', plugin: 'test' } }))
|
||||
})
|
||||
|
||||
@@ -724,11 +724,11 @@ describe('worked example: a native hook plugin is just a cordis plugin on the se
|
||||
name: 'native-guard',
|
||||
apply(ctx: Context) {
|
||||
// 1. SessionStart: seed a standing instruction.
|
||||
ctx.on('agent/session-start', (agent, source) => {
|
||||
ctx.on('agent/session-start', ({ agent, source }) => {
|
||||
agent.inject(createUserMessage({ content: [{ type: 'text', text: `policy active (started: ${source})` }], source: { kind: 'plugin', plugin: 'native-guard' } }))
|
||||
})
|
||||
// 2. PreStep: reject a forbidden prompt, annotate the rest.
|
||||
ctx.on('agent/pre-step', async (_agent, messages, _signal, next): Promise<PreStepDecision> => {
|
||||
ctx.on('agent/pre-step', async ({ messages }, next): Promise<PreStepDecision> => {
|
||||
const text = messages.flatMap(message => message.content)
|
||||
.map(b => (b.type === 'text' ? b.text : '')).join('')
|
||||
if (text.includes('rm -rf')) {
|
||||
|
||||
@@ -28,7 +28,7 @@ async function harness(adapter: MockAdapter, persona = '') {
|
||||
/** Wait for the agent's next transition to idle after a waking send. */
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
@@ -216,7 +216,7 @@ describe('agent loop', () => {
|
||||
const adapter = new MockAdapter([textResponse('ok after rescue')])
|
||||
const ctx = await harness(adapter, 'In {{cwd}}.')
|
||||
const errors: Error[] = []
|
||||
ctx.on('agent/error', (_agent, _turn, _step, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errors.push(error)
|
||||
})
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
@@ -263,7 +263,7 @@ describe('agent loop', () => {
|
||||
assembly.variables['model'] = 'mock'
|
||||
return next()
|
||||
})
|
||||
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async (_payload, next) => {
|
||||
const config = await next()
|
||||
return { ...config, provider: 'mock', model: 'mock' }
|
||||
})
|
||||
@@ -553,7 +553,7 @@ describe('agent loop', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('failed-steering'), { provider: 'mock', model: 'mock' })
|
||||
let fail = true
|
||||
ctx.on('agent/pre-step', (subject, _messages, _context, next) => {
|
||||
ctx.on('agent/pre-step', ({ agent: subject }, next) => {
|
||||
if (subject !== agent || !fail) return next()
|
||||
fail = false
|
||||
subject.steer(createUserMessage({ content: [{ type: 'text', text: 'pending steering' }], source: { kind: 'user' } }))
|
||||
@@ -713,7 +713,7 @@ describe('agent loop', () => {
|
||||
|
||||
let steps = 0
|
||||
ctx.on('session/event', (_session, event) => { if (event.type === 'step/end') steps++ })
|
||||
ctx.on('agent/turn-stopping', (subject) => {
|
||||
ctx.on('agent/turn-stopping', ({ agent: subject }) => {
|
||||
if (steps < 3) {
|
||||
subject.steer(createUserMessage({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'plugin', plugin: 'loop-test' } }))
|
||||
}
|
||||
@@ -785,7 +785,7 @@ describe('agent loop', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async (_payload, next) => {
|
||||
const config = await next()
|
||||
// The seed is frozen — config is not a mutable per-call knob; a switch
|
||||
// is proposed by returning a replacement, and the loop logs it.
|
||||
@@ -816,7 +816,7 @@ describe('agent loop', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
const fires: { turn: number; step: number; signal: AbortSignal }[] = []
|
||||
ctx.on('agent/pre-step', (subject, _messages, { turn, step, signal }, next) => {
|
||||
ctx.on('agent/pre-step', ({ agent: subject, turn, step, signal }, next) => {
|
||||
if (subject === agent) fires.push({ turn, step, signal })
|
||||
return next()
|
||||
})
|
||||
@@ -837,7 +837,7 @@ describe('agent loop', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
let boundaryOpen = true
|
||||
ctx.on('agent/pre-step', (subject, _messages, _context, next) => {
|
||||
ctx.on('agent/pre-step', ({ agent: subject }, next) => {
|
||||
if (subject === agent) boundaryOpen = subject.session.events.at(-1)?.type === 'step/start'
|
||||
return next()
|
||||
})
|
||||
@@ -855,13 +855,13 @@ describe('agent loop', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
let throwOnce = true
|
||||
ctx.on('agent/pre-step', (_agent, _messages, _context, next) => {
|
||||
ctx.on('agent/pre-step', (_payload, next) => {
|
||||
if (throwOnce) { throwOnce = false; throw new Error('boom in pre-step') }
|
||||
return next()
|
||||
})
|
||||
|
||||
const errors: Error[] = []
|
||||
ctx.on('agent/error', (_a, _t, _s, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
if (error instanceof Error) errors.push(error)
|
||||
})
|
||||
|
||||
@@ -933,7 +933,7 @@ describe('agent loop', () => {
|
||||
ctx.on('session/event', (_session, event) => { if (event.type === 'step/end') steps++ })
|
||||
// Force exactly one continuation (step 1 → step 2), then defer to default
|
||||
// (step 2 is a plain stop with no tool calls → stops).
|
||||
ctx.on('agent/turn-stopping', (subject) => {
|
||||
ctx.on('agent/turn-stopping', ({ agent: subject }) => {
|
||||
if (steps < 2) {
|
||||
subject.steer(createUserMessage({ content: [{ type: 'text', text: 'continue after truncation' }], source: { kind: 'plugin', plugin: 'max-tokens-test' } }))
|
||||
}
|
||||
@@ -1296,7 +1296,7 @@ describe('agent loop', () => {
|
||||
|
||||
const errors: unknown[] = []
|
||||
const reasons: TurnEndReason[] = []
|
||||
ctx.on('agent/error', (_agent, _turn, _step, error) => {
|
||||
ctx.on('agent/error', ({ error }) => {
|
||||
errors.push(error)
|
||||
})
|
||||
ctx.on('session/event', (_s, event) => { if (event.type === 'turn/end') reasons.push(event.data.reason) })
|
||||
|
||||
@@ -50,7 +50,7 @@ async function harness() {
|
||||
/** Resolve on the agent's next transition to idle (event-based, not polled). */
|
||||
function nextIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
@@ -63,7 +63,7 @@ function nextIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
* the seen list plus a disposer for the listener (per the registry convention). */
|
||||
function recordStatus(ctx: Context, agent: Agent): { seen: string[]; dispose: () => void } {
|
||||
const seen: string[] = []
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent) seen.push(status)
|
||||
})
|
||||
return { seen, dispose }
|
||||
|
||||
@@ -59,7 +59,7 @@ async function loopHarness(): Promise<Context> {
|
||||
|
||||
function waitForIdle(context: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = context.on('agent/status', (subject, status) => {
|
||||
const dispose = context.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
|
||||
@@ -62,12 +62,12 @@ describe('agent/request-error', () => {
|
||||
retryPolicy: ResolvedRetryPolicy | undefined
|
||||
}[] = []
|
||||
const statuses: string[] = []
|
||||
ctx.on('agent/status', (subject, status) => {
|
||||
ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent) statuses.push(status)
|
||||
})
|
||||
ctx.on('agent/request-error', async (subject, context) => {
|
||||
ctx.on('agent/request-error', async ({ agent: subject, turn, step, failure, retryPolicy }) => {
|
||||
expect(subject).toBe(agent)
|
||||
seen.push(context)
|
||||
seen.push({ turn, step, failure, retryPolicy })
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
|
||||
@@ -102,7 +102,7 @@ describe('agent/request-error', () => {
|
||||
const adapter = new MockAdapter([fail('busy', 'RATE_LIMIT'), textResponse('unused')])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('request-error-cancel'), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/request-error', async (subject) => {
|
||||
ctx.on('agent/request-error', async ({ agent: subject }) => {
|
||||
subject.cancel({ kind: 'user' })
|
||||
return { kind: 'retry' }
|
||||
})
|
||||
|
||||
@@ -38,7 +38,7 @@ async function harnessRoutes(
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
@@ -122,7 +122,7 @@ describe('request stability across the loop', () => {
|
||||
const adapter = new MockAdapter([textResponse('one'), textResponse('two')], reasoning)
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('effort'), { provider: 'mock', model: 'mock' })
|
||||
ctx.on('agent/request', async (_agent, turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async ({ turn }, next) => {
|
||||
const config = await next()
|
||||
return turn === 2 ? { ...config, reasoningEffort: ReasoningEffortId('max') } : config
|
||||
})
|
||||
@@ -198,7 +198,7 @@ describe('request stability across the loop', () => {
|
||||
provider: 'deepseek',
|
||||
model: 'deepseek-model',
|
||||
})
|
||||
ctx.on('agent/request', async (_agent, turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async ({ turn }, next) => {
|
||||
const config = await next()
|
||||
return turn === 2
|
||||
? { ...config, provider: 'other', model: 'other-model' }
|
||||
@@ -232,7 +232,7 @@ describe('request stability across the loop', () => {
|
||||
model: 'deepseek-model',
|
||||
maxTokens: 4_096,
|
||||
})
|
||||
ctx.on('agent/request', async (_agent, turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async ({ turn }, next) => {
|
||||
const config = await next()
|
||||
return turn === 2
|
||||
? { ...config, provider: 'other', model: 'other-model' }
|
||||
@@ -460,7 +460,7 @@ describe('request stability across the loop', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
let injected = false
|
||||
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async (_payload, next) => {
|
||||
if (!injected) {
|
||||
injected = true
|
||||
agent.inject(createUserMessage({ content: [{ type: 'text', text: '[late context]' }], source: { kind: 'plugin', plugin: 'test' } }))
|
||||
@@ -539,7 +539,7 @@ describe('request stability across the loop', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => {
|
||||
ctx.on('agent/request', async (_payload, next) => {
|
||||
const config = await next()
|
||||
// next() resolves the SAME frozen seed — in-place shaping after
|
||||
// delegation is unrepresentable, so a "mutate what next() returned"
|
||||
@@ -576,7 +576,7 @@ describe('request stability across the loop', () => {
|
||||
send(agent, 'go')
|
||||
await waitForIdle(ctx, agent)
|
||||
ctx.systemPrompt.section({ name: 'extra', order: 2, text: 'now with guidance' })
|
||||
ctx.on('agent/request', async (_agent, _turn, _step, _signal, next) => ({
|
||||
ctx.on('agent/request', async (_payload, next) => ({
|
||||
...await next(), temperature: 0.5, maxTokens: 99, stop: ['<END>'],
|
||||
}))
|
||||
send(agent, 'again')
|
||||
@@ -658,7 +658,7 @@ describe('request/context capacity records', () => {
|
||||
|
||||
send(agent, 'first')
|
||||
await waitForIdle(ctx, agent)
|
||||
ctx.on('agent/request', (subject, _turn, _step, _signal, next) => subject === agent
|
||||
ctx.on('agent/request', ({ agent: subject }, next) => subject === agent
|
||||
? Promise.resolve({ provider: 'mock', model: 'large' })
|
||||
: next())
|
||||
send(agent, 'second')
|
||||
@@ -686,7 +686,7 @@ describe('request/context capacity records', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('capacity-clear'), { provider: 'mock', model: 'known' })
|
||||
let model = 'known'
|
||||
ctx.on('agent/request', (subject, _turn, _step, _signal, next) => subject === agent
|
||||
ctx.on('agent/request', ({ agent: subject }, next) => subject === agent
|
||||
? Promise.resolve({ provider: 'mock', model })
|
||||
: next())
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ function preparationFromSnapshot(
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') { dispose(); resolve() }
|
||||
})
|
||||
})
|
||||
@@ -260,7 +260,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 sources1: string[] = []
|
||||
ctx1.on('agent/session-start', (_agent, source) => void sources1.push(source))
|
||||
ctx1.on('agent/session-start', ({ source }) => void sources1.push(source))
|
||||
const a1 = (await ctx1.agents.create({ sessionId: SessionId('start-sess') })).agent
|
||||
expect(sources1).toEqual(['startup'])
|
||||
a1.followup(createUserMessage({ content: [{ type: 'text', text: 'q' }], source: { kind: 'user' } }))
|
||||
@@ -279,7 +279,7 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
|
||||
await ctx2.plugin(SessionPersistenceJsonl, { root })
|
||||
ctx2.llm.registerAdapter(['mock'], adapter2)
|
||||
const sources2: string[] = []
|
||||
ctx2.on('agent/session-start', (_agent, source) => void sources2.push(source))
|
||||
ctx2.on('agent/session-start', ({ source }) => void sources2.push(source))
|
||||
await ctx2.agents.resume({ resumeSessionId: SessionId('start-sess') })
|
||||
expect(sources2).toEqual(['resume'])
|
||||
await ctx2.fiber.dispose()
|
||||
@@ -298,11 +298,11 @@ describe('the session-persistence Agent Note: AgentLoop factory create/resume',
|
||||
expect(ctx.agents.get(sessionId)?.session).toBe(session)
|
||||
order.push('session/created')
|
||||
})
|
||||
ctx.on('agent/created', (agent) => {
|
||||
ctx.on('agent/created', ({ agent }) => {
|
||||
expect(agent.status).toBe('idle')
|
||||
order.push('agent/created')
|
||||
})
|
||||
ctx.on('agent/session-start', (agent) => {
|
||||
ctx.on('agent/session-start', ({ agent }) => {
|
||||
expect(() => { agent.cancel({ kind: 'user' }) }).not.toThrow()
|
||||
order.push('agent/session-start')
|
||||
})
|
||||
@@ -882,7 +882,7 @@ describe('configured-start failure edges', () => {
|
||||
configured.llm.registerAdapter(['mock'], new MockAdapter([]))
|
||||
configured.sessionPersistence.prepare = (id, signal) => ctx.sessionPersistence.prepare(id, signal)
|
||||
const configFailures: unknown[] = []
|
||||
configured.on('agent-loop/config-start-failed', (_id, error) => { configFailures.push(error) })
|
||||
configured.on('agent-loop/config-start-failed', ({ error }) => { configFailures.push(error) })
|
||||
const configWarnings: string[] = []
|
||||
const configWarn = configured.logger.warn.bind(configured.logger)
|
||||
configured.logger.warn = ((...args: unknown[]) => {
|
||||
@@ -915,7 +915,7 @@ describe('configured-start failure edges', () => {
|
||||
return gate.promise
|
||||
}
|
||||
const failures: unknown[] = []
|
||||
ctx.on('agent-loop/config-start-failed', (_id, error) => { failures.push(error) })
|
||||
ctx.on('agent-loop/config-start-failed', ({ error }) => { failures.push(error) })
|
||||
|
||||
const configured = new Context()
|
||||
await configured.plugin(LlmService)
|
||||
@@ -926,7 +926,7 @@ describe('configured-start failure edges', () => {
|
||||
await configured.plugin(SessionPersistenceJsonl, { root })
|
||||
configured.llm.registerAdapter(['mock'], new MockAdapter([]))
|
||||
configured.sessionPersistence.prepare = (id, signal) => ctx.sessionPersistence.prepare(id, signal)
|
||||
configured.on('agent-loop/config-start-failed', (_id, error) => { failures.push(error) })
|
||||
configured.on('agent-loop/config-start-failed', ({ error }) => { failures.push(error) })
|
||||
const loop = await configured.plugin(AgentLoop, {
|
||||
agents: [{ id: 'main', resumeSessionId: sessionId, provider: 'mock', model: 'mock' }],
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ async function harness(adapter: MockAdapter = new MockAdapter([textResponse('ok'
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
@@ -199,7 +199,7 @@ describe('agent scope lifecycle', () => {
|
||||
const b = ctx.agentLoop.create(SessionId('b'), { provider: 'mock', model: 'mock' })
|
||||
|
||||
const heard: string[] = []
|
||||
a.ctx.on('agent/status', (subject, status) => void heard.push(`a-sees:${subject.id}:${status}`))
|
||||
a.ctx.on('agent/status', ({ agent: subject, status }) => void heard.push(`a-sees:${subject.id}:${status}`))
|
||||
a.ctx.on('session/event', (_s, event) => {
|
||||
if (event.type === 'user/message') heard.push('a-sees:user-message')
|
||||
})
|
||||
@@ -217,7 +217,7 @@ describe('agent scope lifecycle', () => {
|
||||
it('runs setup in the guaranteed slot: scoped world complete before session-start and the first assembly', async () => {
|
||||
const ctx = await harness()
|
||||
const order: string[] = []
|
||||
ctx.on('agent/session-start', (agent) => {
|
||||
ctx.on('agent/session-start', ({ agent }) => {
|
||||
order.push('session-start')
|
||||
// The scoped section is already registered by the time session-start fires.
|
||||
void ctx.systemPrompt.assemble(assembleContextFor(agent)).then((assembly) => {
|
||||
@@ -673,19 +673,19 @@ describe('agent scope lifecycle', () => {
|
||||
ctx.on('session/created', (session) => {
|
||||
if (session.id === SessionId('agent-created-barrier-s')) lifecycle.push('session-created')
|
||||
})
|
||||
ctx.on('agent/created', (agent) => {
|
||||
ctx.on('agent/created', ({ agent }) => {
|
||||
if (agent.id !== SessionId('agent-created-barrier-s')) return
|
||||
lifecycle.push('agent-created:dispose')
|
||||
disposeCurrentLifecycle(ownerCtx)
|
||||
})
|
||||
ctx.on('agent/created', (agent) => {
|
||||
ctx.on('agent/created', ({ agent }) => {
|
||||
if (agent.id !== SessionId('agent-created-barrier-s')) return
|
||||
expect(ctx.agents.get(agent.id)).toBe(agent)
|
||||
expect(ctx.sessions.get(agent.session.id)).toBe(agent.session)
|
||||
agent.ctx.effect(() => () => { lifecycle.push('scope-disposed') })
|
||||
lifecycle.push('agent-created:observer')
|
||||
})
|
||||
ctx.on('agent/disposed', (agent) => {
|
||||
ctx.on('agent/disposed', ({ agent }) => {
|
||||
if (agent.id === SessionId('agent-created-barrier-s')) lifecycle.push('agent-disposed')
|
||||
})
|
||||
ctx.on('session/disposed', (session) => {
|
||||
@@ -720,8 +720,8 @@ describe('agent scope lifecycle', () => {
|
||||
const starts: string[] = []
|
||||
let ownerCtx!: Context
|
||||
let creating!: ReturnType<typeof ctx.agents.create>
|
||||
ctx.on('agent/session-start', agent => void starts.push(agent.id))
|
||||
ctx.on('agent/created', (agent) => {
|
||||
ctx.on('agent/session-start', ({ agent }) => void starts.push(agent.id))
|
||||
ctx.on('agent/created', ({ agent }) => {
|
||||
if (agent.id === SessionId('listener-dispose-s')) disposeCurrentLifecycle(ownerCtx)
|
||||
})
|
||||
|
||||
@@ -749,15 +749,15 @@ describe('agent scope lifecycle', () => {
|
||||
const statuses: string[] = []
|
||||
let scopeDisposed = false
|
||||
let observerSawLive = false
|
||||
ctx.on('agent/status', (agent, status) => {
|
||||
ctx.on('agent/status', ({ agent, status }) => {
|
||||
if (agent.id === SessionId('session-start-dispose-s')) statuses.push(status)
|
||||
})
|
||||
ctx.on('agent/session-start', (agent) => {
|
||||
ctx.on('agent/session-start', ({ agent }) => {
|
||||
if (agent.id !== SessionId('session-start-dispose-s')) return
|
||||
announced = agent
|
||||
disposeCurrentLifecycle(ownerCtx)
|
||||
})
|
||||
ctx.on('agent/session-start', (agent) => {
|
||||
ctx.on('agent/session-start', ({ agent }) => {
|
||||
if (agent.id !== SessionId('session-start-dispose-s')) return
|
||||
expect(ctx.agents.get(agent.id)).toBe(agent)
|
||||
expect(ctx.sessions.get(agent.session.id)).toBe(agent.session)
|
||||
@@ -840,7 +840,7 @@ describe('agent scope lifecycle', () => {
|
||||
const ctx = await harness()
|
||||
let boom = true
|
||||
const disposed: string[] = []
|
||||
ctx.on('agent/disposed', agent => void disposed.push(agent.id))
|
||||
ctx.on('agent/disposed', ({ agent }) => void disposed.push(agent.id))
|
||||
ctx.on('session/created', () => {
|
||||
if (boom) { boom = false; throw new Error('boom created') }
|
||||
})
|
||||
@@ -861,11 +861,11 @@ describe('agent scope lifecycle', () => {
|
||||
const lifecycle: string[] = []
|
||||
ctx.on('session/created', (session) => { lifecycle.push(`session-created:${session.id}`) })
|
||||
ctx.on('session/disposed', (session) => { lifecycle.push(`session-disposed:${session.id}`) })
|
||||
ctx.on('agent/created', (agent) => {
|
||||
ctx.on('agent/created', ({ agent }) => {
|
||||
lifecycle.push(`agent-created:${agent.id}`)
|
||||
throw new Error('agent observer failed')
|
||||
})
|
||||
ctx.on('agent/disposed', (agent) => { lifecycle.push(`agent-disposed:${agent.id}`) })
|
||||
ctx.on('agent/disposed', ({ agent }) => { lifecycle.push(`agent-disposed:${agent.id}`) })
|
||||
|
||||
await expect(ctx.agents.create({
|
||||
sessionId: SessionId('partial-session'),
|
||||
@@ -911,10 +911,10 @@ describe('agent scope lifecycle', () => {
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
const other = ctx.agentLoop.create(SessionId('a2'), { provider: 'mock', model: 'mock' })
|
||||
const heard: string[] = []
|
||||
agent.ctx.on('agent/error', (subject: Agent, turn: number) => void heard.push(`${subject.id}:${turn}`))
|
||||
agent.ctx.on('agent/error', ({ agent: subject, turn }) => void heard.push(`${subject.id}:${turn}`))
|
||||
|
||||
agentEvents(ctx, other).emit('agent/error', 1, 0, new Error('not for a1'))
|
||||
agentEvents(ctx, agent).emit('agent/error', 2, 0, new Error('for a1'))
|
||||
agentEvents(ctx, other).emit('agent/error', { turn: 1, step: 0, error: new Error('not for a1') })
|
||||
agentEvents(ctx, agent).emit('agent/error', { turn: 2, step: 0, error: new Error('for a1') })
|
||||
expect(heard).toEqual(['a1:2'])
|
||||
})
|
||||
|
||||
@@ -1064,7 +1064,7 @@ describe('agent scope lifecycle', () => {
|
||||
})
|
||||
const agent = handle.agent
|
||||
let reentered = false
|
||||
ctx.on('agent/status', (subject, status) => {
|
||||
ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject !== agent || status !== 'idle' || reentered) return
|
||||
reentered = true
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'reentrant' }], source: { kind: 'user' } }))
|
||||
|
||||
@@ -31,7 +31,7 @@ async function harness(adapter: MockAdapter, maxParallelToolCalls?: number) {
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') { dispose(); resolve() }
|
||||
})
|
||||
})
|
||||
|
||||
@@ -33,7 +33,7 @@ async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['too
|
||||
|
||||
function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const dispose = ctx.on('agent/status', (subject, status) => {
|
||||
const dispose = ctx.on('agent/status', ({ agent: subject, status }) => {
|
||||
if (subject === agent && status === 'idle') {
|
||||
dispose()
|
||||
resolve()
|
||||
|
||||
Reference in New Issue
Block a user