Merge branch 'codex/simp-agent-entry-state' into codex/simp-unify-agent-session-id

# Conflicts:
#	docs/architecture.md
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/coding-agent/tests/code-mode.e2e.ts
#	examples/coding-agent/tests/coding-task.e2e.ts
#	examples/coding-agent/tests/compaction.e2e.ts
#	examples/coding-agent/tests/full-loop.e2e.ts
#	examples/coding-agent/tests/todo-write.e2e.ts
#	examples/cordis-agent/tests/cordis-tools.e2e.ts
#	packages/bash/tool-bash/tests/integration.spec.ts
#	packages/bash/tool-bash/tests/tools.spec.ts
#	packages/compact/compact-basic/tests/compact-loop-repro.spec.ts
#	packages/context/time-context/tests/time-context.spec.ts
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/cordis/tool-cordis/tests/integration.spec.ts
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/agent.ts
#	packages/core/agent-loop/src/index.ts
#	packages/core/agent-loop/tests/agent.spec.ts
#	packages/core/agent-loop/tests/cancel.spec.ts
#	packages/core/agent-loop/tests/config-session-id.spec.ts
#	packages/core/agent-loop/tests/contract-regressions.spec.ts
#	packages/core/agent-loop/tests/coverage-edges.spec.ts
#	packages/core/agent-loop/tests/interception.spec.ts
#	packages/core/agent-loop/tests/loop.spec.ts
#	packages/core/agent-loop/tests/properties.spec.ts
#	packages/core/agent-loop/tests/request-cache.e2e.ts
#	packages/core/agent-loop/tests/request-reconstruction.spec.ts
#	packages/core/agent-loop/tests/resume.spec.ts
#	packages/core/agent-loop/tests/scope-lifecycle.spec.ts
#	packages/core/agent-loop/tests/tool-order.spec.ts
#	packages/core/agent-loop/tests/turn-stop.spec.ts
#	packages/core/agent/src/types.ts
#	packages/examples/agent-spine-demo/README.md
#	packages/examples/agent-spine-demo/tests/agent-core.spec.ts
#	packages/examples/stdio-demo/README.md
#	packages/examples/stdio-demo/src/index.ts
#	packages/examples/stdio-demo/tests/stdio-agent.spec.ts
#	packages/fs/tool-fs/tests/fs-tools.e2e.ts
#	packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts
#	packages/hooks/hooks-claude/tests/bridge.spec.ts
#	packages/hooks/hooks-claude/tests/coverage.spec.ts
#	packages/hooks/hooks-codex/tests/bridge.spec.ts
#	packages/hooks/hooks-codex/tests/coverage.spec.ts
#	packages/subagent/subagent-fork/tests/multi-subagent.spec.ts
#	packages/subagent/subagent-fork/tests/subagent-fork.spec.ts
#	packages/subagent/subagent-inprocess/tests/structured.spec.ts
#	packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts
#	packages/subagent/subagent-spawn/tests/spawn.e2e.ts
#	packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts
#	packages/todo/tool-todo/tests/integration.spec.ts
#	packages/ui/acp/tests/dispose.spec.ts
#	packages/ui/acp/tests/edges.spec.ts
#	packages/workflow/workflow-workerthread/tests/integration.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-18 12:21:15 +08:00
657 changed files with 36658 additions and 7514 deletions
@@ -141,7 +141,7 @@ describe('agent scope lifecycle', () => {
it('wires agent.ctx: tagged with the agent, DX field set, ctx.agent safe elsewhere', async () => {
const ctx = await harness()
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
expect(scopeOf(agent.ctx)).toBe(agent)
expect(agent.ctx.agent).toBe(agent)
// The root accessor default: a plain context answers undefined, not a throw.
@@ -169,7 +169,7 @@ describe('agent scope lifecycle', () => {
it('scoped registrations live in the agent world and die with the agent', async () => {
const ctx = await harness()
const handle = await ctx.agents.create({ sessionId: SessionId('s1'), agentOptions: { model: 'mock' } })
const handle = await ctx.agents.create({ sessionId: SessionId('s1'), agentOptions: { provider: 'mock', model: 'mock' } })
const { agent } = handle
agent.ctx.systemPrompt.section({ name: 'deployment:persona', order: 0, text: 'You run tests.' })
agent.ctx.tools.register({
@@ -194,8 +194,8 @@ describe('agent scope lifecycle', () => {
it('agent.ctx listeners hear only their own agent (scoped dispatch end to end)', async () => {
const ctx = await harness(new MockAdapter([textResponse('one'), textResponse('two')]))
const a = ctx.agentLoop.create(SessionId('a'), { model: 'mock' })
const b = ctx.agentLoop.create(SessionId('b'), { model: 'mock' })
const a = ctx.agentLoop.create(SessionId('a'), { provider: 'mock', model: 'mock' })
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}`))
@@ -226,7 +226,7 @@ describe('agent scope lifecycle', () => {
const handle = await ctx.agents.create({
sessionId: SessionId('child-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup: async (agentCtx) => {
order.push('setup')
await Promise.resolve()
@@ -250,7 +250,7 @@ describe('agent scope lifecycle', () => {
})
ctx.on('agent/created', () => void order.push('agent/created'))
ctx.on('agent/session-start', () => void order.push('agent/session-start'))
const acceptedOptions = { model: 'mock' }
const acceptedOptions = { provider: 'mock', model: 'mock' }
const creating = ctx.agents.create({
sessionId: SessionId('atomic'),
@@ -297,12 +297,12 @@ describe('agent scope lifecycle', () => {
const sessionId = SessionId('concurrent-final-enter')
const first = ctx.agents.create({
sessionId,
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup,
})
const second = ctx.agents.create({
sessionId,
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup,
})
await bothStarted.promise
@@ -330,7 +330,7 @@ describe('agent scope lifecycle', () => {
const setupStarted = Promise.withResolvers<undefined>()
const pending = ctx.agents.create({
sessionId: SessionId('signal-pending-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
signal: pendingController.signal,
setup: async () => {
setupStarted.resolve(undefined)
@@ -346,7 +346,7 @@ describe('agent scope lifecycle', () => {
const liveController = new AbortController()
const live = await ctx.agents.create({
sessionId: SessionId('signal-live-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
signal: liveController.signal,
})
liveController.abort(new Error('too late'))
@@ -368,7 +368,7 @@ describe('agent scope lifecycle', () => {
const owner = await ctx.plugin(Object.assign((inner: Context) => {
creating = inner.agents.create({
sessionId: SessionId('owner-race-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup: async () => {
setupStarted.resolve(undefined)
await gate.promise
@@ -395,7 +395,7 @@ describe('agent scope lifecycle', () => {
const owner2 = await ctx.plugin(Object.assign((inner: Context) => {
creating2 = inner.agents.create({
sessionId: SessionId('owner-race-s-2'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup: async () => {
setupStarted2.resolve(undefined)
await gate2.promise
@@ -421,7 +421,7 @@ describe('agent scope lifecycle', () => {
const creating = ctx.agents.create({
sessionId: SessionId('factory-setup-race-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup: async () => {
setupStarted.resolve(undefined)
await gate.promise
@@ -451,7 +451,7 @@ describe('agent scope lifecycle', () => {
const creating = ctx.agents.create({
sessionId: SessionId('factory-scope-race-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup: () => { setupCalls += 1 },
})
await expect(creating).rejects.toThrow(/agent loop is not active/)
@@ -485,7 +485,7 @@ describe('agent scope lifecycle', () => {
ownerFiber = inner.fiber
creating = inner.agents.create({
sessionId: SessionId('caller-scope-race-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})
}, { inject: ['agents'] }))
@@ -515,7 +515,7 @@ describe('agent scope lifecycle', () => {
void loopFiber.dispose()
})
expect(() => ctx.agentLoop.create(SessionId('config-scope-race'), { model: 'mock' }))
expect(() => ctx.agentLoop.create(SessionId('config-scope-race'), { provider: 'mock', model: 'mock' }))
.toThrow(/agent loop is not active/)
await loopFiber.dispose()
expect(ctx.agents.get(SessionId('config-scope-race'))).toBeUndefined()
@@ -527,9 +527,9 @@ describe('agent scope lifecycle', () => {
const ctx = await harness()
const id = SessionId('config-prepare-failure')
expect(() => ctx.agentLoop.create(id, { model: 'mock' }, { cwd: 'relative' }))
expect(() => ctx.agentLoop.create(id, { provider: 'mock', model: 'mock' }, { cwd: 'relative' }))
.toThrow(/absolute path/)
const replacement = ctx.agentLoop.create(id, { model: 'mock' }, { cwd: '/recovered' })
const replacement = ctx.agentLoop.create(id, { provider: 'mock', model: 'mock' }, { cwd: '/recovered' })
expect(ctx.agents.get(id)).toBe(replacement)
await replacement.whenIdle()
await ctx.fiber.dispose()
@@ -547,7 +547,7 @@ describe('agent scope lifecycle', () => {
await expect(ctx.agents.create({
sessionId: SessionId('factory-scope-throw-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})).rejects.toThrow('scope preparation failed')
await loopFiber.dispose()
expect(ctx.agents.get(SessionId('factory-scope-throw-s'))).toBeUndefined()
@@ -561,8 +561,8 @@ describe('agent scope lifecycle', () => {
const loop = ctx.agentLoop
const sessionId = SessionId('factory-live')
const handle = await ctx.agents.create({
sessionId,
agentOptions: { model: 'mock' },
sessionId: SessionId('factory-live-s'),
agentOptions: { provider: 'mock', model: 'mock' },
})
await loopFiber.dispose()
@@ -585,7 +585,7 @@ describe('agent scope lifecycle', () => {
const owner = await ctx.plugin(Object.assign((inner: Context) => {
creating = inner.agents.create({
sessionId: SessionId('dependency-origin-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup: (agentCtx) => {
agentCtx.tools.register({
name: 'dependency-origin-tool',
@@ -639,7 +639,7 @@ describe('agent scope lifecycle', () => {
ownerCtx = inner
creating = inner.agents.create({
sessionId: SessionId('session-created-barrier-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})
}, { inject: ['agents'] }))
@@ -687,7 +687,7 @@ describe('agent scope lifecycle', () => {
ownerCtx = inner
creating = inner.agents.create({
sessionId: SessionId('agent-created-barrier-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})
}, { inject: ['agents'] }))
@@ -720,7 +720,7 @@ describe('agent scope lifecycle', () => {
ownerCtx = inner
creating = inner.agents.create({
sessionId: SessionId('listener-dispose-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})
}, { inject: ['agents'] }))
@@ -760,7 +760,7 @@ describe('agent scope lifecycle', () => {
ownerCtx = inner
creating = inner.agents.create({
sessionId: SessionId('session-start-dispose-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})
}, { inject: ['agents'] }))
@@ -784,7 +784,7 @@ describe('agent scope lifecycle', () => {
ctx.on('agent/session-start', () => void published.push('agent/session-start'))
await expect(ctx.agents.create({
sessionId: SessionId('bad-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup: async () => {
await Promise.resolve()
throw new Error('boom setup')
@@ -795,7 +795,7 @@ describe('agent scope lifecycle', () => {
expect(published).toEqual([])
expect(ctx.agents.get(SessionId('bad-s'))).toBeUndefined()
expect(ctx.sessions.get(SessionId('bad-s'))).toBeUndefined()
const retry = await ctx.agents.create({ sessionId: SessionId('bad-s'), agentOptions: { model: 'mock' } })
const retry = await ctx.agents.create({ sessionId: SessionId('bad-s'), agentOptions: { provider: 'mock', model: 'mock' } })
await retry.dispose()
})
@@ -813,7 +813,7 @@ describe('agent scope lifecycle', () => {
await expect(ctx.agents.create({
sessionId: SessionId('exotic-seed-session'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
seed,
})).rejects.toThrow(/seed event at index 0 is not losslessly JSON-serializable/)
@@ -822,7 +822,7 @@ describe('agent scope lifecycle', () => {
expect(ctx.sessions.get(SessionId('exotic-seed-session'))).toBeUndefined()
const retry = await ctx.agents.create({
sessionId: SessionId('exotic-seed-session'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})
await retry.dispose()
})
@@ -836,13 +836,13 @@ describe('agent scope lifecycle', () => {
if (boom) { boom = false; throw new Error('boom created') }
})
await expect(ctx.agents.create({
sessionId: SessionId('bad-s'), agentOptions: { model: 'mock' },
sessionId: SessionId('bad-s'), agentOptions: { provider: 'mock', model: 'mock' },
})).rejects.toThrow('boom created')
expect(ctx.agents.get(SessionId('bad-s'))).toBeUndefined()
expect(ctx.sessions.get(SessionId('bad-s'))).toBeUndefined()
expect(disposed).toEqual([]) // inserted but never announced: no impossible disposed edge
// The rollback also disposed the scope fiber: re-creating works cleanly.
const retry = await ctx.agents.create({ sessionId: SessionId('bad-s'), agentOptions: { model: 'mock' } })
const retry = await ctx.agents.create({ sessionId: SessionId('bad-s'), agentOptions: { provider: 'mock', model: 'mock' } })
expect(scopeOf(retry.agent.ctx)).toBe(retry.agent)
await retry.dispose()
})
@@ -860,7 +860,7 @@ describe('agent scope lifecycle', () => {
await expect(ctx.agents.create({
sessionId: SessionId('partial-session'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})).rejects.toThrow('agent observer failed')
expect(lifecycle).toEqual([
@@ -884,7 +884,7 @@ describe('agent scope lifecycle', () => {
}
})
expect(() => ctx.agentLoop.create(SessionId('config-bad'), { model: 'mock' }))
expect(() => ctx.agentLoop.create(SessionId('config-bad'), { provider: 'mock', model: 'mock' }))
.toThrow('config publish failed')
expect(ctx.agents.get(SessionId('config-bad'))).toBeUndefined()
expect(ctx.sessions.list()).toHaveLength(sessionsBefore)
@@ -892,15 +892,15 @@ describe('agent scope lifecycle', () => {
it('registrations through a disposed agent ctx throw INACTIVE_EFFECT', async () => {
const ctx = await harness()
const handle = await ctx.agents.create({ sessionId: SessionId('s1'), agentOptions: { model: 'mock' } })
const handle = await ctx.agents.create({ sessionId: SessionId('s1'), agentOptions: { provider: 'mock', model: 'mock' } })
await handle.dispose()
expect(() => handle.agent.ctx.on('agent/status', () => {})).toThrow(/inactive context/)
})
it('agentEvents fuses carrier and subject for custom drivers', async () => {
const ctx = await harness()
const agent = ctx.agentLoop.create(SessionId('a1'), { model: 'mock' })
const other = ctx.agentLoop.create(SessionId('a2'), { model: 'mock' })
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}`))
@@ -913,7 +913,7 @@ describe('agent scope lifecycle', () => {
const ctx = await harness()
let handle!: Awaited<ReturnType<typeof ctx.agents.create>>
const owner = await ctx.plugin(Object.assign(async (inner: Context) => {
handle = await inner.agents.create({ sessionId: SessionId('o1-s'), agentOptions: { model: 'mock' } })
handle = await inner.agents.create({ sessionId: SessionId('o1-s'), agentOptions: { provider: 'mock', model: 'mock' } })
}, { inject: ['agents'] }))
const { agent } = handle
@@ -945,7 +945,7 @@ describe('agent scope lifecycle', () => {
const ctx = await harness()
let handle!: Awaited<ReturnType<typeof ctx.agents.create>>
const owner = await ctx.plugin(Object.assign(async (inner: Context) => {
handle = await inner.agents.create({ sessionId: SessionId('h1-s'), agentOptions: { model: 'mock' } })
handle = await inner.agents.create({ sessionId: SessionId('h1-s'), agentOptions: { provider: 'mock', model: 'mock' } })
}, { inject: ['agents'] }))
const teardownDone: string[] = []
@@ -967,7 +967,7 @@ describe('agent scope lifecycle', () => {
const sessionId = SessionId('retired-owner-effect')
const handle = await ctx.agents.create({
sessionId,
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})
expect(ctx.fiber.getEffects().map(effect => effect.label)).toContain(`agentLoop.owner(${sessionId})`)
@@ -984,7 +984,7 @@ describe('agent scope lifecycle', () => {
const owner = await ctx.plugin(Object.assign(async (inner: Context) => {
handle = await inner.agents.create({
sessionId: SessionId('manual-first-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup(agentCtx) {
agentCtx.effect(() => async () => {
cleanupStarted.resolve(undefined)
@@ -1018,7 +1018,7 @@ describe('agent scope lifecycle', () => {
})
const first = await ctx.agents.create({
sessionId,
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
setup(agentCtx) {
agentCtx.effect(() => async () => {
cleanupStarted.resolve(undefined)
@@ -1031,7 +1031,7 @@ describe('agent scope lifecycle', () => {
await Promise.all([sessionDisposed.promise, cleanupStarted.promise])
expect(ctx.agents.get(sessionId)).toBeUndefined()
expect(ctx.sessions.get(sessionId)).toBeUndefined()
const replacement = await ctx.agents.create({ sessionId, agentOptions: { model: 'mock' } })
const replacement = await ctx.agents.create({ sessionId, agentOptions: { provider: 'mock', model: 'mock' } })
expect(ctx.agents.get(sessionId)).toBe(replacement.agent)
expect(ctx.sessions.get(sessionId)).toBe(replacement.agent.session)
@@ -1045,7 +1045,7 @@ describe('agent scope lifecycle', () => {
const ctx = await harness()
const handle = await ctx.agents.create({
sessionId: SessionId('idle-flush-s'),
agentOptions: { model: 'mock' },
agentOptions: { provider: 'mock', model: 'mock' },
})
const gate = Promise.withResolvers<undefined>()
let flushStarted = false