Merge remote-tracking branch 'origin/master' into worktree/pr628-merge-20260727
# Conflicts: # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md # .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md # docs/architecture.i18n.yaml # docs/architecture.md # docs/architecture.zh.md # docs/cordis-catalog/events.md # docs/core-data-structures/llm-streaming.i18n.yaml # docs/core-data-structures/llm-streaming.md # docs/core-data-structures/llm-streaming.zh.md # docs/event-producer-consumer.md # examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl # packages/compact/compact-basic/src/index.ts # packages/compact/compact-basic/tests/compact-basic.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts # packages/core/agent-loop/README.i18n.yaml # packages/core/agent-loop/README.md # packages/core/agent-loop/README.zh.md # packages/core/agent-loop/src/loop.ts # packages/core/agent-loop/tests/request-recovery.spec.ts # packages/core/agent/src/types.ts # packages/core/scope/tests/invariant.spec.ts # packages/llm/llm-retry/README.i18n.yaml # packages/llm/llm-retry/README.md # packages/llm/llm-retry/README.zh.md # packages/llm/llm-retry/src/index.ts # packages/llm/llm-retry/src/invariant.ts # packages/llm/llm-retry/tests/invariant.spec.ts # packages/llm/llm-retry/tests/retry.spec.ts # packages/plan/plan-mode/src/index.ts # packages/plan/plan-mode/tests/integration.spec.ts # packages/plan/plan-mode/tests/plan-mode.spec.ts
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
# Replay counterpart to goal.cordis.yml; only the live model is replaced.
|
||||
# Replay counterpart to goal.cordis.yml. It includes cordis.yml directly because
|
||||
# a config patch cannot target an entry behind a nested include, then restates
|
||||
# the goal overlay while replacing the live model with keyless replay.
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
path: ./goal.cordis.yml
|
||||
path: ./cordis.yml
|
||||
patches:
|
||||
- id: llm-deepseek
|
||||
name: '@deepseek-ai/dsh-llm-deepseek'
|
||||
disabled: true
|
||||
- insert:
|
||||
- id: goal
|
||||
name: '@deepseek-ai/dsh-goal'
|
||||
- id: tool-goal
|
||||
name: '@deepseek-ai/dsh-tool-goal'
|
||||
- id: llm-replay
|
||||
name: '@deepseek-ai/dsh-llm-replay'
|
||||
|
||||
@@ -314,12 +314,12 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('Code Mode: real model writes a p
|
||||
ctx = await codeModeHarness(workdir)
|
||||
const agent = ctx.agentLoop.create(SessionId('e2e-code-mode'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.followup([{
|
||||
agent.followup({ content: [{
|
||||
type: 'text',
|
||||
text: 'Using one run_code program: run `echo alpha-7` with the bash tool, run `echo beta-9` with the bash tool, '
|
||||
+ 'then write both outputs joined by a plus sign into combined.txt (bash heredoc or redirect), '
|
||||
+ 'and return only the joined string.',
|
||||
}])
|
||||
}], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
const events: SessionEvent[] = [...agent.session.events]
|
||||
|
||||
@@ -366,21 +366,17 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('Code Mode: real model writes a p
|
||||
agentOptions: { provider: 'deepseek', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
|
||||
handle.agent.followup([{
|
||||
handle.agent.followup({ content: [{
|
||||
type: 'text',
|
||||
text: 'Use one run_code program to call tools.read on pkg/deep/task.txt. After it finishes, answer: Code Mode workspace handshake?',
|
||||
}])
|
||||
}], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, handle.agent)
|
||||
|
||||
const events: SessionEvent[] = [...handle.agent.session.events]
|
||||
const dispatch = events.find(event => event.type === 'tool/code-dispatch' && event.data.name === 'read')
|
||||
const outerResult = events.find(event => event.type === 'tool/result')
|
||||
const workspaceContext = events.find(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& typeof event.data.meta === 'object'
|
||||
&& event.data.meta !== null
|
||||
&& !Array.isArray(event.data.meta)
|
||||
&& event.data.meta.kind === 'workspace-instructions')
|
||||
&& event.data.source.kind === 'workspace-instructions')
|
||||
expect(dispatch).toBeDefined()
|
||||
expect(outerResult).toBeDefined()
|
||||
expect(workspaceContext).toBeDefined()
|
||||
|
||||
@@ -56,12 +56,12 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('coding task: fix a failing test
|
||||
ctx = await codingHarness(workdir, { persona: SYSTEM_PROMPT })
|
||||
const agent = ctx.agentLoop.create(SessionId('e2e-task'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.followup([{
|
||||
agent.followup({ content: [{
|
||||
type: 'text',
|
||||
text: 'In the current directory, `node add.test.js` fails because add.js has a bug. '
|
||||
+ 'Fix add.js so the test passes, run `node add.test.js` to verify, and report the result. '
|
||||
+ 'Do not modify add.test.js.',
|
||||
}])
|
||||
}], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
// The agent claims success…
|
||||
|
||||
@@ -46,12 +46,12 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('compaction: a long session compa
|
||||
})
|
||||
const agent = ctx.agentLoop.create(SessionId('e2e-compaction'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.followup([{
|
||||
agent.followup({ content: [{
|
||||
type: 'text',
|
||||
text: 'Read file1.txt, file2.txt, file3.txt, and file4.txt one at a '
|
||||
+ 'time using cat (a separate bash command for each). After reading all four, tell me how '
|
||||
+ 'many files you read and the number mentioned in file1.txt.',
|
||||
}])
|
||||
}], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const events = [...agent.session.events]
|
||||
|
||||
@@ -59,7 +59,7 @@ export const inject = ['llm']
|
||||
/** Register the keyless `cli-mock` adapter. */
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.llm.registerAdapter(['cli-mock'], new CliMockAdapter())
|
||||
ctx.on('agent/request', async (_agent, _turn, step, _config, _signal, next) => {
|
||||
ctx.on('agent/request', async (_agent, _turn, step, _signal, next) => {
|
||||
const config = await next()
|
||||
return step === 2 ? { ...config, reasoningEffort: OFF } : config
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ export const name = 'seed-goal'
|
||||
export const inject = ['goals']
|
||||
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.on('agent/pre-step', (agent) => {
|
||||
ctx.on('agent/step', (agent) => {
|
||||
if (ctx.goals.get(agent) !== undefined) return
|
||||
ctx.goals.create(agent, {
|
||||
objective: 'Prove the composed goal survives in the session log',
|
||||
|
||||
@@ -30,7 +30,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('full loop: real model + real bas
|
||||
ctx = await codingHarness(workdir, { persona: SYSTEM_PROMPT })
|
||||
const agent = ctx.agentLoop.create(SessionId('e2e-loop'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.followup([{ type: 'text', text: 'Run `echo e2e-ok` with the bash tool and tell me its exact output.' }])
|
||||
agent.followup({ content: [{ type: 'text', text: 'Run `echo e2e-ok` with the bash tool and tell me its exact output.' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const events = [...agent.session.events]
|
||||
|
||||
@@ -314,14 +314,16 @@ describe('headless stream-json snapshots', () => {
|
||||
const goalChanges = records.filter((record) => {
|
||||
if (record.type !== 'user/message') return false
|
||||
const data = record.data as JsonObject | undefined
|
||||
const meta = data?.meta as JsonObject | undefined
|
||||
return meta?.kind === 'goal/change'
|
||||
const source = data?.source as JsonObject | undefined
|
||||
const change = source?.change as JsonObject | undefined
|
||||
return source?.kind === 'goal' && change?.kind === 'goal/change'
|
||||
})
|
||||
expect(goalChanges).toHaveLength(1)
|
||||
const data = goalChanges[0]?.data as JsonObject | undefined
|
||||
const meta = data?.meta as JsonObject | undefined
|
||||
const goal = meta?.goal as JsonObject | undefined
|
||||
expect(meta?.operation).toBe('create')
|
||||
const source = data?.source as JsonObject | undefined
|
||||
const change = source?.change as JsonObject | undefined
|
||||
const goal = change?.goal as JsonObject | undefined
|
||||
expect(change?.operation).toBe('create')
|
||||
expect(goal).toMatchObject({
|
||||
objective: 'Finish the headless goal-tool snapshot proof',
|
||||
phase: 'active',
|
||||
|
||||
@@ -41,7 +41,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses
|
||||
sessionId: SESSION_ID,
|
||||
agentOptions: { provider: 'deepseek', model: 'deepseek-v4-flash' },
|
||||
})).agent
|
||||
first.followup([{ type: 'text', text: `Remember this code for later: ${SECRET}. Just acknowledge it.` }])
|
||||
first.followup({ content: [{ type: 'text', text: `Remember this code for later: ${SECRET}. Just acknowledge it.` }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, first)
|
||||
await ctx.fiber.dispose()
|
||||
ctx = undefined
|
||||
@@ -58,7 +58,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('resume: continue a persisted ses
|
||||
// The prior user turn is in the rehydrated log before the model is asked.
|
||||
expect(JSON.stringify(resumed.session.deriveMessages())).toContain(SECRET)
|
||||
|
||||
resumed.followup([{ type: 'text', text: 'What was the code I asked you to remember? Reply with just the code.' }])
|
||||
resumed.followup({ content: [{ type: 'text', text: 'What was the code I asked you to remember? Reply with just the code.' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, resumed)
|
||||
|
||||
// The model recalls it — only possible from the resumed history.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -21,7 +21,7 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":20,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"tool-call","id":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the headless goal-tool snapshot proof\",\"max_goal_rounds\":7}"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":20,"outputTokens":8}},"sourceEventSeqs":[15,16,17,18,19],"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":21,"time":0,"data":{"turn":1,"step":2,"callId":"call_goal_create","name":"create_goal","arguments":"{\"objective\":\"Finish the headless goal-tool snapshot proof\",\"max_goal_rounds\":7}"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":22,"time":0,"data":{"turn":1,"step":2,"callId":"call_goal_create","content":[{"type":"text","text":"{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the headless goal-tool snapshot proof\",\"phase\":\"active\",\"roundsStarted\":0,\"maxGoalRounds\":7},\"activation\":\"armed\"}"}],"isError":false},"sourceEventSeqs":[21],"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":23,"time":0,"data":{"content":[{"type":"text","text":"<goal_state>{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the headless goal-tool snapshot proof\",\"phase\":\"active\",\"maxGoalRounds\":7},\"roundsStarted\":0,\"createdAt\":0,\"updatedAt\":0}</goal_state>"}],"source":{"kind":"goal","goalId":"goal-{{sessionId}}","revision":1,"round":0},"meta":{"kind":"goal/change","version":1,"operation":"create","goal":{"id":"goal-{{sessionId}}","revision":1,"objective":"Finish the headless goal-tool snapshot proof","phase":"active","maxGoalRounds":7},"roundsStarted":0,"createdAt":0,"updatedAt":0}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":23,"time":0,"data":{"content":[{"type":"text","text":"<goal_state>{\"goal\":{\"id\":\"goal-{{sessionId}}\",\"revision\":1,\"objective\":\"Finish the headless goal-tool snapshot proof\",\"phase\":\"active\",\"maxGoalRounds\":7},\"roundsStarted\":0,\"createdAt\":0,\"updatedAt\":0}</goal_state>"}],"source":{"kind":"goal","goalId":"goal-{{sessionId}}","revision":1,"round":0,"change":{"kind":"goal/change","version":1,"operation":"create","goal":{"id":"goal-{{sessionId}}","revision":1,"objective":"Finish the headless goal-tool snapshot proof","phase":"active","maxGoalRounds":7},"roundsStarted":0,"createdAt":0,"updatedAt":0}}},"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":24,"time":0,"data":{"turn":1,"step":2}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":25,"time":0,"data":{"turn":1,"step":3}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":26,"time":0,"data":{"turn":1,"step":3,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}}
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":4,"time":0,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":5,"time":0,"data":{"turn":1,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"llm/retry","seq":6,"time":0,"data":{"turn":1,"step":1,"provider":"deepseek","mode":"normal","policyKey":"[\"normal\",1,[\"RATE_LIMIT\"],1,1,0]","retry":1,"maxRetries":1,"delayMs":1,"failure":{"message":"snapshot transient failure","code":"RATE_LIMIT","status":429}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":7,"time":0,"data":{"turn":1,"step":2}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":8,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":9,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"RETRY_OK"}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RETRY_OK"}}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":4,"outputTokens":2}}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":2,"content":[{"type":"text","text":"RETRY_OK"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":4,"outputTokens":2}},"sourceEventSeqs":[8,9,10,11,12],"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":14,"time":0,"data":{"turn":1,"step":2}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":15,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}}
|
||||
{"type":"result","success":true,"sessionId":"{{sessionId}}","turn":1,"result":"RETRY_OK","reason":{"kind":"completed"},"usage":{"inputTokens":4,"outputTokens":2}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":7,"time":0,"data":{"turn":1,"reason":{"kind":"error","step":1,"failure":{"message":"snapshot transient failure","code":"RATE_LIMIT","status":429}}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":8,"time":0,"data":{"turn":2,"trigger":{"kind":"retry"}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":9,"time":0,"data":{"turn":2,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":10,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":11,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"text-delta","index":0,"text":"RETRY_OK"}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":12,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"RETRY_OK"}}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":13,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":4,"outputTokens":2}}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/chunk","seq":14,"time":0,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":15,"time":0,"data":{"turn":2,"step":1,"content":[{"type":"text","text":"RETRY_OK"}],"provenance":{"provider":"deepseek","model":"deepseek-v4-flash"},"usage":{"inputTokens":4,"outputTokens":2}},"sourceEventSeqs":[10,11,12,13,14],"surfaceOp":"append"}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":16,"time":0,"data":{"turn":2,"step":1}}}
|
||||
{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":17,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}}
|
||||
{"type":"result","success":true,"sessionId":"{{sessionId}}","turn":2,"result":"RETRY_OK","reason":{"kind":"completed"},"usage":{"inputTokens":4,"outputTokens":2}}
|
||||
|
||||
@@ -28,10 +28,10 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('todo_write: real model records a
|
||||
ctx = await codingHarness(workdir, { persona: TODO_SYSTEM_PROMPT })
|
||||
const agent = ctx.agentLoop.create(SessionId('e2e-todo'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
||||
|
||||
agent.followup([{ type: 'text', text:
|
||||
agent.followup({ content: [{ type: 'text', text:
|
||||
'Use the todo_write tool to record a plan of exactly two steps: first '
|
||||
+ '"inspect the failing test" (in_progress), then "apply the fix" (pending). '
|
||||
+ 'Send both in one todo_write call, then reply with the single word DONE.' }])
|
||||
+ 'Send both in one todo_write call, then reply with the single word DONE.' }], source: { kind: 'user' } })
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
const events = [...agent.session.events]
|
||||
|
||||
Reference in New Issue
Block a user