Merge remote-tracking branch 'origin/master' into codex/fix-compact-agents-reinjection

# Conflicts:
#	docs/architecture.i18n.yaml
#	examples/acp-agent/tests/snapshots/workspace-context/session.jsonl
This commit is contained in:
fz
2026-08-06 20:13:04 +08:00
635 changed files with 18029 additions and 3509 deletions
@@ -57,7 +57,7 @@ async function harness(): Promise<{ ctx: Context; agent: Agent }> {
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()
@@ -209,8 +209,7 @@ async function workspaceContextOf(agent: Agent): Promise<UserMessage> {
async function syncWorkspaceContext(ctx: Context, agent: Agent): Promise<void> {
await agentEvents(ctx, agent).waterfall(
'agent/pre-step', [],
{ turn: 1, step: 1, signal: testToolSignal },
'agent/pre-step', { messages: [], turn: 1, step: 1, signal: testToolSignal },
async () => ({ kind: 'enter' as const, messages: [] }),
)
}
@@ -245,15 +244,13 @@ async function composeBaselinePrefix(ctx: Context, agent: Agent): Promise<Messag
const signal = AbortSignal.timeout(1000)
await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
[],
{ turn: 1, step: 1, signal },
{ messages: [], turn: 1, step: 1, signal },
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
)
const claimed = agent.inbox.claim('next-step', 1)
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
claimed,
{ turn: 1, step: 2, signal },
{ messages: claimed, turn: 1, step: 2, signal },
() => Promise.resolve({ kind: 'enter' as const, messages: claimed }),
)
const entered = decision.kind === 'enter' ? decision.messages : []
@@ -916,6 +913,7 @@ describe('workspace context request injection', () => {
role: 'user',
source: {
kind: 'workspace-instructions',
form: 'instructions',
baseline: true,
changes: [{ action: 'set', scope: sk('.', 'AGENTS.md'), path: 'AGENTS.md' }],
},
@@ -968,8 +966,7 @@ describe('workspace context request injection', () => {
const original = stubAgent(root)
await agentEvents(ctx, original).waterfall(
'agent/pre-step',
[],
{ turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
{ messages: [], turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
)
const inserted = original.inbox.nextStep[0]
@@ -978,12 +975,11 @@ describe('workspace context request injection', () => {
await fiber.dispose()
await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
const resumed = stubAgent(root, [...original.session.events])
agentEvents(ctx, resumed).emit('agent/session-start', 'resume')
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
const claimed = resumed.inbox.claim('next-step', 1)
const decision = await agentEvents(ctx, resumed).waterfall(
'agent/pre-step',
claimed,
{ turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
{ messages: claimed, turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
() => Promise.resolve({ kind: 'enter' as const, messages: claimed }),
)
if (decision.kind !== 'enter') throw new Error('recovered baseline was rejected')
@@ -1015,8 +1011,7 @@ describe('workspace context request injection', () => {
const original = stubAgent(root)
await agentEvents(ctx, original).waterfall(
'agent/pre-step',
[],
{ turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
{ messages: [], turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
)
const stale = original.inbox.nextStep[0]
@@ -1026,12 +1021,11 @@ describe('workspace context request injection', () => {
await fiber.dispose()
await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
const resumed = stubAgent(root, [...original.session.events])
agentEvents(ctx, resumed).emit('agent/session-start', 'resume')
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
const staleClaim = resumed.inbox.claim('next-step', 1)
const staleDecision = await agentEvents(ctx, resumed).waterfall(
'agent/pre-step',
staleClaim,
{ turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
{ messages: staleClaim, turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
() => Promise.resolve({ kind: 'enter' as const, messages: staleClaim }),
)
@@ -1070,8 +1064,7 @@ describe('workspace context request injection', () => {
const original = stubAgent(root)
await agentEvents(originalCtx, original).waterfall(
'agent/pre-step',
[],
{ turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
{ messages: [], turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
)
const stale = original.inbox.nextStep[0]
@@ -1081,12 +1074,11 @@ describe('workspace context request injection', () => {
if (provideFs) await resumedCtx.plugin(LocalFileSystem, { cwd: '/' })
await resumedCtx.plugin(workspaceContext, { dshHome: home, maxBytes })
const resumed = stubAgent(root, [...original.session.events])
agentEvents(resumedCtx, resumed).emit('agent/session-start', 'resume')
agentEvents(resumedCtx, resumed).emit('agent/session-start', { source: 'resume' })
const claimed = resumed.inbox.claim('next-step', 1)
const decision = await agentEvents(resumedCtx, resumed).waterfall(
'agent/pre-step',
claimed,
{ turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
{ messages: claimed, turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
() => Promise.resolve({ kind: 'enter' as const, messages: claimed }),
)
@@ -1113,6 +1105,7 @@ describe('workspace context request injection', () => {
content: [{ type: 'text', text: 'stale nested instructions' }],
source: {
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md'), digest: 'stale' }],
},
}), {
@@ -1146,6 +1139,7 @@ describe('workspace context request injection', () => {
content: [{ type: 'text', text: 'stale nested instructions' }],
source: {
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md'), digest: 'stale' }],
},
}), {
@@ -1188,8 +1182,7 @@ describe('workspace context request injection', () => {
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
[prompt],
{ turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
{ messages: [prompt], turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
() => Promise.resolve(downstream),
)
@@ -1246,8 +1239,7 @@ describe('workspace context request injection', () => {
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
[],
{ turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
{ messages: [], turn: 1, step: 1, signal: AbortSignal.timeout(1000) },
() => Promise.resolve(downstream),
)
@@ -1358,8 +1350,7 @@ describe('workspace context request injection', () => {
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step',
[prompt],
{ turn: 2, step: 1, signal: AbortSignal.timeout(1000) },
{ messages: [prompt], turn: 2, step: 1, signal: AbortSignal.timeout(1000) },
() => Promise.resolve({ kind: 'enter' as const, messages: [prompt] }),
)
@@ -1397,7 +1388,7 @@ describe('workspace context request injection', () => {
const resumed = stubAgent(root, [...original.session.events])
// Resume announces its lifecycle start before the first step.
agentEvents(ctx, resumed).emit('agent/session-start', 'resume')
agentEvents(ctx, resumed).emit('agent/session-start', { source: 'resume' })
await composeBaselinePrefix(ctx, resumed)
const baselines = baselineEvents(resumed)
@@ -1445,7 +1436,7 @@ describe('workspace context request injection', () => {
await write(join(root, 'AGENTS.md'), 'repo rule')
const ctx = new Context()
await mountWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
ctx.on('agent/pre-step', async (_agent, _messages, _context, next) => {
ctx.on('agent/pre-step', async (_payload, next) => {
const decision = await next()
if (decision.kind === 'reject') return decision
return {
@@ -1719,8 +1710,7 @@ describe('workspace context request injection', () => {
const reason = new Error('cancel prefix')
const pending = agentEvents(ctx, stubAgent(root)).waterfall(
'agent/pre-step',
[],
{ turn: 1, step: 1, signal: controller.signal },
{ messages: [], turn: 1, step: 1, signal: controller.signal },
() => Promise.resolve({ kind: 'enter' as const, messages: [] }),
)
@@ -2262,7 +2252,7 @@ describe('dynamic nested workspace context injection', () => {
expect(result.isError).toBe(false)
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ kind: 'workspace-instructions' })
const queuedSource = ((await syncedWorkspaceContext(ctx, agent))).source
expect(queuedSource).toMatchObject({ kind: 'workspace-instructions' })
expect(queuedSource).toMatchObject({ kind: 'workspace-instructions', form: 'instructions' })
expect(queuedSource.kind === 'workspace-instructions' && queuedSource.changes.some(change =>
change.action === 'set'
&& change.scope === sk('pkg', 'AGENTS.md')
@@ -2605,6 +2595,7 @@ describe('dynamic nested workspace context injection', () => {
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'replace', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(((await syncedWorkspaceContext(ctx, agent))).content)).toBe([
@@ -2714,7 +2705,7 @@ describe('dynamic nested workspace context injection', () => {
if (previous === undefined) throw new Error('missing AGENTS.md baseline state')
const authoritative = createUserMessage({
content: [{ type: 'text', text: 'nested rule' }],
source: { kind: 'workspace-instructions', changes: [previous] },
source: { kind: 'workspace-instructions', form: 'instructions', changes: [previous] },
})
if (authority === 'visible') {
agent.session.append('user/message', authoritative, { surfaceOp: 'append' })
@@ -2740,6 +2731,7 @@ describe('dynamic nested workspace context injection', () => {
content: [{ type: 'text', text: 'pending baseline duplicate' }],
source: {
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'set', scope: sk('.', 'AGENTS.md'), path: 'AGENTS.md' }],
},
})],
@@ -2783,7 +2775,7 @@ describe('dynamic nested workspace context injection', () => {
if (previous === undefined) throw new Error('missing AGENTS.md baseline state')
const authoritative = createUserMessage({
content: [{ type: 'text', text: 'repo rule' }],
source: { kind: 'workspace-instructions', changes: [previous] },
source: { kind: 'workspace-instructions', form: 'instructions', changes: [previous] },
})
agent.session.append('user/message', authoritative, { surfaceOp: 'append' })
const resolved = resolveConfig({ dshHome: home, maxBytes: 65536, localInstructionFileCandidates: [] })
@@ -2907,6 +2899,7 @@ describe('dynamic nested workspace context injection', () => {
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'remove', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(((await syncedWorkspaceContext(ctx, agent))).content)).toBe([
@@ -3309,6 +3302,7 @@ describe('dynamic nested workspace context injection', () => {
],
source: {
kind: 'workspace-instructions',
form: 'instructions',
changes: [
null,
{ action: 'unknown', scope: 'pkg', path: join('pkg', 'AGENTS.md') },
@@ -3462,6 +3456,7 @@ describe('dynamic nested workspace context injection', () => {
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ kind: 'workspace-instructions' })
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(((await syncedWorkspaceContext(ctx, agent))).content)).toContain('nested package rule')
@@ -3904,8 +3899,7 @@ describe('workspace context inbox synchronization', () => {
controller.abort(new Error('abort pre-step reconciliation'))
await expect(agentEvents(ctx, agent).waterfall(
'agent/pre-step', [],
{ turn: 1, step: 1, signal: controller.signal },
'agent/pre-step', { messages: [], turn: 1, step: 1, signal: controller.signal },
async () => ({ kind: 'enter' as const, messages: [] }),
)).rejects.toThrow('abort pre-step reconciliation')
@@ -4015,8 +4009,7 @@ describe('workspace context inbox synchronization', () => {
const downstream = { kind: 'enter' as const, messages: claimed }
const decision = await agentEvents(ctx, agent).waterfall(
'agent/pre-step', claimed,
{ turn: 1, step: 1, signal: testToolSignal },
'agent/pre-step', { messages: claimed, turn: 1, step: 1, signal: testToolSignal },
async () => downstream,
)