Merge remote-tracking branch 'origin/master' into codex/sandbox-policy-context

# Conflicts:
#	examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/acp-agent/tests/snapshots/bash-spill/session.jsonl
#	examples/acp-agent/tests/snapshots/bash-tool-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl
#	examples/acp-agent/tests/snapshots/cancel/session.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl
#	examples/acp-agent/tests/snapshots/error-finish/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-edit/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-read/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-write/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-invalid-matcher/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-invalid-matcher/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl
#	examples/acp-agent/tests/snapshots/lsp-definition/session.jsonl
#	examples/acp-agent/tests/snapshots/multi-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/packed-chunks/session.jsonl
#	examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl
#	examples/acp-agent/tests/snapshots/pty-tools/session.jsonl
#	examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl
#	examples/acp-agent/tests/snapshots/session-query-spill/session.jsonl
#	examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl
#	examples/acp-agent/tests/snapshots/session-title-after-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl
#	examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl
#	examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl
#	examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/todo-write/session.jsonl
#	examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/web-fetch/session.jsonl
#	examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl
#	examples/acp-agent/tests/snapshots/workflow-run/session.jsonl
#	examples/acp-agent/tests/snapshots/workspace-context/session.jsonl
#	examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl
#	examples/headless-agent/tests/snapshots/pty-tools/session.jsonl
#	examples/headless-agent/tests/snapshots/pty-tools/stream-json.expected.jsonl
#	examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl
#	examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl
#	examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl
#	examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl
#	packages/core/agent-loop/src/agent.ts
This commit is contained in:
NI0317
2026-07-31 11:02:45 +08:00
718 changed files with 19344 additions and 4215 deletions

View File

@@ -67,6 +67,7 @@ export class MockAdapter extends LlmAdapter {
constructor(
private script: (StreamChunk[] | ((options: GenerateOptions) => StreamChunk[]) | 'hang')[],
private readonly reasoning?: LlmModelReasoningInfo,
private readonly defaultMaxTokens?: number,
) {
super()
}
@@ -80,6 +81,7 @@ export class MockAdapter extends LlmAdapter {
id: model,
name: model,
...this.reasoning === undefined ? {} : { reasoning: this.reasoning },
...this.defaultMaxTokens === undefined ? {} : { defaultMaxTokens: this.defaultMaxTokens },
})
}

View File

@@ -71,7 +71,7 @@ function waitForIdle(context: Context, agent: Agent): Promise<void> {
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('log-derived request cache hits (real API)', () => {
it('every request after the first hits the provider prefix cache', async () => {
ctx = await loopHarness()
const agent = ctx.agentLoop.create(SessionId('cache-e2e'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
const agent = ctx.agentLoop.create(SessionId('cache-e2e'), { provider: 'deepseek-official', model: 'deepseek-v4-flash' })
// Turn 1: forces a tool call → at least two steps (two model requests).
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Look up the key "deploy-color" with the lookup tool and tell me the value.' }], source: { kind: 'user' } }))

View File

@@ -18,6 +18,13 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
async function harness(adapter: MockAdapter, persona = 'stable base') {
return harnessRoutes([['mock', adapter]], persona)
}
async function harnessRoutes(
adapters: readonly (readonly [provider: string, adapter: MockAdapter])[],
persona = 'stable base',
) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
@@ -25,7 +32,7 @@ async function harness(adapter: MockAdapter, persona = 'stable base') {
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
for (const [provider, adapter] of adapters) ctx.llm.registerAdapter([provider], adapter)
return ctx
}
@@ -134,6 +141,10 @@ describe('request stability across the loop', () => {
ReasoningEffortId('high'),
ReasoningEffortId('max'),
])
expect(headers.map(event => event.data.header.adapterDefaults)).toEqual([
{ reasoningEffort: true },
undefined,
])
expect(headers.map(event => event.data.reason)).toEqual(['initial', 'change'])
for (const [model, effort] of [
@@ -158,6 +169,88 @@ describe('request stability across the loop', () => {
}
})
it('logs an adapter-owned maxTokens default before dispatch', async () => {
const adapter = new MockAdapter([textResponse('bounded')], undefined, 256_000)
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('adapter-max-tokens'), {
provider: 'mock',
model: 'mock',
})
send(agent, 'use the adapter output limit')
await waitForIdle(ctx, agent)
expect(adapter.requests[0]?.maxTokens).toBe(256_000)
const header = agent.session.events.find(event => event.type === 'request/header')
expect(header?.type === 'request/header' && header.data.header.config.maxTokens).toBe(256_000)
expect(header?.type === 'request/header' && header.data.header.adapterDefaults)
.toEqual({ maxTokens: true })
})
it('rematerializes the selected adapter maxTokens default after a provider switch', async () => {
const deepseek = new MockAdapter([textResponse('deepseek')], undefined, 256_000)
const other = new MockAdapter([textResponse('other')], undefined, 8_192)
const ctx = await harnessRoutes([
['deepseek', deepseek],
['other', other],
])
const agent = ctx.agentLoop.create(SessionId('adapter-max-tokens-switch'), {
provider: 'deepseek',
model: 'deepseek-model',
})
ctx.on('agent/request', async (_agent, turn, _step, _signal, next) => {
const config = await next()
return turn === 2
? { ...config, provider: 'other', model: 'other-model' }
: config
})
send(agent, 'first')
await waitForIdle(ctx, agent)
send(agent, 'second')
await waitForIdle(ctx, agent)
expect(deepseek.requests[0]?.maxTokens).toBe(256_000)
expect(other.requests[0]?.maxTokens).toBe(8_192)
const headers = agent.session.events.filter(event => event.type === 'request/header')
expect(headers.map(event => event.data.header.config.maxTokens)).toEqual([256_000, 8_192])
expect(headers.map(event => event.data.header.adapterDefaults)).toEqual([
{ maxTokens: true },
{ maxTokens: true },
])
})
it('preserves an explicit agent maxTokens cap across a provider switch', async () => {
const deepseek = new MockAdapter([textResponse('deepseek')], undefined, 256_000)
const other = new MockAdapter([textResponse('other')], undefined, 8_192)
const ctx = await harnessRoutes([
['deepseek', deepseek],
['other', other],
])
const agent = ctx.agentLoop.create(SessionId('explicit-max-tokens-switch'), {
provider: 'deepseek',
model: 'deepseek-model',
maxTokens: 4_096,
})
ctx.on('agent/request', async (_agent, turn, _step, _signal, next) => {
const config = await next()
return turn === 2
? { ...config, provider: 'other', model: 'other-model' }
: config
})
send(agent, 'first')
await waitForIdle(ctx, agent)
send(agent, 'second')
await waitForIdle(ctx, agent)
expect(deepseek.requests[0]?.maxTokens).toBe(4_096)
expect(other.requests[0]?.maxTokens).toBe(4_096)
const headers = agent.session.events.filter(event => event.type === 'request/header')
expect(headers.map(event => event.data.header.config.maxTokens)).toEqual([4_096, 4_096])
expect(headers.map(event => event.data.header.adapterDefaults)).toEqual([undefined, undefined])
})
it('keeps exact-model resolution, request logging, and dispatch on one adapter registration', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)