fix(subagent): preserve continuable behavior after rebase
This commit is contained in:
@@ -25,6 +25,9 @@
|
||||
- id: plan-mode
|
||||
disabled: true
|
||||
|
||||
- id: tool-subagent-control
|
||||
disabled: true
|
||||
|
||||
- id: tool-subagent
|
||||
disabled: true
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ const EXPECTED_TUI_TOOLS = [
|
||||
'get_goal',
|
||||
'ralph',
|
||||
'read',
|
||||
'send_message',
|
||||
'skill',
|
||||
'str_replace_editor',
|
||||
'subagent',
|
||||
|
||||
@@ -30,6 +30,7 @@ const EXPECTED_TOOLS = [
|
||||
'get_goal',
|
||||
'ralph',
|
||||
'read',
|
||||
'send_message',
|
||||
'skill',
|
||||
'str_replace_editor',
|
||||
'subagent',
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/core-data-structures/core.md
|
||||
core.md: 70c63d8a84963f56468b5fcdacb580a798de2e10
|
||||
core.zh.md: 280529b7de6c3e8546b71791367710c588ad9282
|
||||
core.md: 795256a2b30e44771baf8bcb7c1d541134692a02
|
||||
core.zh.md: 5cebff049aae53df9f0494da1fadc8dfa5d9ad09
|
||||
@@ -568,6 +568,8 @@ interface CancelOptions {
|
||||
}
|
||||
```
|
||||
|
||||
`SteeringReceipt.outcome` 始终会解析。`admitted` 标识其不可变请求历史包含该确切消息的轮次与步骤;`rejected` 表示生命周期或终止策略先丢弃了该消息。同步输入校验仍会从 `steer()` 抛出异常。
|
||||
|
||||
```ts type-equiv
|
||||
/** Stable runtime cause accepted by {@link Agent.cancel}. */
|
||||
type AgentCancelCause =
|
||||
@@ -669,16 +671,18 @@ interface Agent {
|
||||
followup(message: UserMessage): void
|
||||
|
||||
/**
|
||||
* Submit steering during prompt admission or an open turn — the
|
||||
* `next-step`/wakeup preset of {@link send}. It stages for the next steering
|
||||
* checkpoint before a request or stop decision. If the activity fails before
|
||||
* that boundary, the remainder stays staged without waking the agent; retry
|
||||
* or a later prompt takes it. Outside that window steering falls back to a
|
||||
* woken follow-up turn, while cancellation or disposal may discard pending
|
||||
* steering.
|
||||
* Submit steering with a message-owned admission receipt — the
|
||||
* `next-step`/wakeup preset of {@link send}. During prompt admission or an
|
||||
* open turn, the message waits in the steering FIFO until a committed step
|
||||
* snapshots it; outside that window it enters the ordinary queued FIFO. The
|
||||
* receipt resolves `admitted` only after the message joins that step's
|
||||
* immutable request history, or `rejected` when terminal policy,
|
||||
* cancellation, or disposal discards it first. A non-terminal turn close may
|
||||
* leave it staged for a later admitted prompt without settling the receipt.
|
||||
* @param message - identified steering content and its producer provenance.
|
||||
* @returns the receipt for this exact message's eventual admission outcome.
|
||||
*/
|
||||
steer(message: UserMessage): void
|
||||
steer(message: UserMessage): SteeringReceipt
|
||||
|
||||
/**
|
||||
* Append model-facing context without running the model — the
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
config:
|
||||
provider: spawn
|
||||
toolName: subagent
|
||||
backgroundMode: continuable
|
||||
maxDepth: 2
|
||||
# Re-pin the recorded model: cordis.yml ships deepseek-v4-pro, but this
|
||||
# scenario's corpus was captured on flash. A config patch replaces the
|
||||
|
||||
@@ -10,4 +10,5 @@
|
||||
config:
|
||||
provider: spawn
|
||||
toolName: subagent
|
||||
backgroundMode: continuable
|
||||
maxDepth: 2
|
||||
@@ -2,8 +2,8 @@
|
||||
{"type":"turn/start","seq":0,"time":1785014475014,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785014475015,"data":{"content":[{"type":"text","text":"Using ONE run_code program, call tools.read on nested/task.txt. After the program finishes, answer the workspace handshake question using the newly discovered instructions: What is the Code Mode workspace handshake?"}],"source":{"kind":"user"},"role":"user","id":"a5066d26-ed57-4f98-8672-b34e883e1299"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785014475022,"data":{"title":"Using ONE run_code program, call","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"user/message","seq":3,"time":1785122256262,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"}]},"role":"user","id":"c9aaa351-f7e6-40ef-955a-c5b8ee07667f"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":4,"time":1785464674590,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"68f653ef-7b05-4a60-a517-6dda5d3f4be4"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":3,"time":1785122256262,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"c9aaa351-f7e6-40ef-955a-c5b8ee07667f"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":4,"time":1785464674590,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nWorkspace snapshot root instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2119a7072358cc727f8d9c4cb7388e905b075fe6"}]},"role":"user","id":"68f653ef-7b05-4a60-a517-6dda5d3f4be4"},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":5,"time":1785464674590,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":6,"time":1785464674590,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":7,"time":1785487644564,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
{"type":"turn/start","seq":0,"time":1783654655602,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783654655603,"data":{"content":[{"type":"text","text":"Load the snapshot-skill skill with the skill tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"2243ae1a-2d65-4f9c-a972-d360b8cc08aa"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1783654655603,"data":{"title":"Load the snapshot-skill skill with","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"user/message","seq":3,"time":1784903324926,"data":{"content":[{"type":"text","text":"<system-reminder>\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n<available_skills>\n- `model-only-skill`: Prove user-disabled skills remain available to the model.\n- `snapshot-skill`: Exercise project skill discovery and loading in snapshot tests.\n</available_skills>\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\n</system-reminder>"}],"source":{"kind":"plugin","plugin":"dsh-tool-skill"},"role":"user","id":"021a7fcb-3d54-4ed9-8c2c-ca7565599fd8"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":4,"time":1785464638477,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"22b51be2-4727-4990-96cd-7017c137152e"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":3,"time":1784903324926,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"021a7fcb-3d54-4ed9-8c2c-ca7565599fd8"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":4,"time":1785464638477,"data":{"content":[{"type":"text","text":"<system-reminder>\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n<available_skills>\n- `model-only-skill`: Prove user-disabled skills remain available to the model.\n- `snapshot-skill`: Exercise project skill discovery and loading in snapshot tests.\n</available_skills>\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\n</system-reminder>"}],"source":{"kind":"plugin","plugin":"dsh-tool-skill"},"role":"user","id":"22b51be2-4727-4990-96cd-7017c137152e"},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":5,"time":1785464638477,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":6,"time":1785464638478,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":7,"time":1785487588943,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
{"type":"turn/start","seq":0,"time":1783778297065,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
|
||||
{"type":"user/message","seq":1,"time":1783778297066,"data":{"content":[{"type":"text","text":"Read nested/task.txt, then read scope</system-reminder>/task.txt with the read tool, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"7cb62d32-ef8e-4d45-9b5e-d2a1fbdbabbd"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1783778297066,"data":{"title":"Read nested/task.txt, then read scope</s","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"user/message","seq":3,"time":1784903339799,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"}]},"role":"user","id":"c95810d8-2b1e-42b9-9d81-82269ddb0035"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":4,"time":1785464650864,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"6dd61dad-f320-4dda-a481-63ee420df9af"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":3,"time":1784903339799,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"c95810d8-2b1e-42b9-9d81-82269ddb0035"},"surfaceOp":"append"}
|
||||
{"type":"user/message","seq":4,"time":1785464650864,"data":{"content":[{"type":"text","text":"<system-reminder>\nThe following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nRoot snapshot instruction.\n\n</system-reminder>"}],"source":{"kind":"workspace-instructions","baseline":true,"changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"2e18766c26603608f321508caae00ea8f4434d59"}]},"role":"user","id":"6dd61dad-f320-4dda-a481-63ee420df9af"},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":5,"time":1785464650864,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":6,"time":1785464650864,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":7,"time":1785487608778,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
|
||||
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
File diff suppressed because one or more lines are too long
@@ -71,6 +71,7 @@ function createSteeringDelivery(): SteeringDelivery {
|
||||
return {
|
||||
receipt: { outcome: promise },
|
||||
settle(outcome): void {
|
||||
/* v8 ignore next -- each ownership transfer removes the delivery before another settlement path can reach it. */
|
||||
if (settled) return
|
||||
settled = true
|
||||
resolve(outcome)
|
||||
@@ -544,6 +545,7 @@ export class ReactLoopAgent implements Agent {
|
||||
this.drainOutbox(turn)
|
||||
break steps
|
||||
}
|
||||
/* v8 ignore next -- step() folded the same steering predicate into continueTurn immediately before returning. */
|
||||
if (outcome.continueTurn || this.outbox.some(item => item.steering)) continue
|
||||
break
|
||||
case 'request-failed': {
|
||||
|
||||
@@ -470,7 +470,10 @@ describe('agent loop', () => {
|
||||
parameters: {},
|
||||
async execute() {
|
||||
// steer while the turn is running (during tool execution)
|
||||
agent.steer(createUserMessage({ content: [{ type: 'text', text: 'change of plans' }], source: { kind: 'user' } }))
|
||||
agent.send(
|
||||
createUserMessage({ content: [{ type: 'text', text: 'change of plans' }], source: { kind: 'user' } }),
|
||||
{ target: 'next-step', wakeup: true },
|
||||
)
|
||||
return [{ type: 'text', text: 'tool done' }]
|
||||
},
|
||||
}))
|
||||
@@ -544,6 +547,120 @@ describe('agent loop', () => {
|
||||
expect(JSON.stringify(adapter.requests[0]?.messages)).toContain('pending steering')
|
||||
})
|
||||
|
||||
it('rejects failed steering commits while preserving later context', async () => {
|
||||
const adapter = new MockAdapter([textResponse('recovered')])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('failed-steering-commit'), { provider: 'mock', model: 'mock' })
|
||||
let receipt: ReturnType<Agent['steer']> | undefined
|
||||
ctx.on('agent/step', (subject) => {
|
||||
if (subject !== agent || receipt !== undefined) return
|
||||
receipt = subject.steer(createUserMessage({
|
||||
content: [{ type: 'text', text: 'rejected steering' }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
subject.inject(createUserMessage({
|
||||
content: [{ type: 'text', text: 'preserved context' }],
|
||||
source: { kind: 'plugin', plugin: 'loop-test' },
|
||||
}))
|
||||
})
|
||||
let rejected = false
|
||||
ctx.on('internal/dispatch', (_mode, name, args) => {
|
||||
if (name !== 'session/event') return
|
||||
const event = args[1] as { type: string }
|
||||
if (event.type === 'steering/message' && !rejected) {
|
||||
rejected = true
|
||||
throw new Error('reject steering commit')
|
||||
}
|
||||
})
|
||||
|
||||
send(agent, 'first prompt')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
if (receipt === undefined) throw new Error('agent/step did not submit steering')
|
||||
expect(await receipt.outcome).toEqual({ status: 'rejected' })
|
||||
expect(agent.session.events.some(event => event.type === 'steering/message')).toBe(false)
|
||||
|
||||
send(agent, 'recover')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
const request = JSON.stringify(adapter.requests[0]?.messages)
|
||||
expect(request).toContain('preserved context')
|
||||
expect(request).not.toContain('rejected steering')
|
||||
})
|
||||
|
||||
it('rejects committed steering when the step boundary fails', async () => {
|
||||
const adapter = new MockAdapter([])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('failed-step-boundary'), { provider: 'mock', model: 'mock' })
|
||||
let receipt: ReturnType<Agent['steer']> | undefined
|
||||
ctx.on('agent/step', (subject) => {
|
||||
if (subject !== agent || receipt !== undefined) return
|
||||
receipt = subject.steer(createUserMessage({
|
||||
content: [{ type: 'text', text: 'committed steering' }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
})
|
||||
ctx.on('internal/dispatch', (_mode, name, args) => {
|
||||
if (name !== 'session/event') return
|
||||
const event = args[1] as { type: string }
|
||||
if (event.type === 'step/start') throw new Error('reject step boundary')
|
||||
})
|
||||
|
||||
send(agent, 'prompt')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
if (receipt === undefined) throw new Error('agent/step did not submit steering')
|
||||
expect(await receipt.outcome).toEqual({ status: 'rejected' })
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(agent.session.events.some(event => event.type === 'steering/message')).toBe(true)
|
||||
expect(agent.session.events.some(event => event.type === 'step/start')).toBe(false)
|
||||
})
|
||||
|
||||
it('retries context and steering after a context commit fails', async () => {
|
||||
const adapter = new MockAdapter([textResponse('recovered')])
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('failed-context-commit'), { provider: 'mock', model: 'mock' })
|
||||
let receipt: ReturnType<Agent['steer']> | undefined
|
||||
ctx.on('agent/step', (subject) => {
|
||||
if (subject !== agent || receipt !== undefined) return
|
||||
subject.inject(createUserMessage({
|
||||
content: [{ type: 'text', text: 'preserved context' }],
|
||||
source: { kind: 'plugin', plugin: 'loop-test' },
|
||||
}))
|
||||
receipt = subject.steer(createUserMessage({
|
||||
content: [{ type: 'text', text: 'preserved steering' }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
})
|
||||
let rejected = false
|
||||
ctx.on('internal/dispatch', (_mode, name, args) => {
|
||||
if (name !== 'session/event') return
|
||||
const event = args[1] as { type: string; data?: { source?: { kind: string } } }
|
||||
if (event.type === 'user/message' && event.data?.source?.kind === 'plugin' && !rejected) {
|
||||
rejected = true
|
||||
throw new Error('reject context commit')
|
||||
}
|
||||
})
|
||||
|
||||
send(agent, 'first prompt')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
expect(adapter.requests).toHaveLength(0)
|
||||
expect(agent.session.events.some(event => event.type === 'steering/message')).toBe(false)
|
||||
|
||||
send(agent, 'recover')
|
||||
await waitForIdle(ctx, agent)
|
||||
|
||||
if (receipt === undefined) throw new Error('agent/step did not submit steering')
|
||||
expect(await receipt.outcome).toEqual({ status: 'admitted', turn: 2, step: 1 })
|
||||
expect(adapter.requests).toHaveLength(1)
|
||||
const request = JSON.stringify(adapter.requests[0]?.messages)
|
||||
expect(request).toContain('preserved context')
|
||||
expect(request).toContain('preserved steering')
|
||||
})
|
||||
|
||||
it('inject() while idle appends context without opening a turn', async () => {
|
||||
const adapter = new MockAdapter([textResponse('ok')])
|
||||
const ctx = await harness(adapter)
|
||||
@@ -722,12 +839,22 @@ describe('agent loop', () => {
|
||||
const ctx = await harness(adapter)
|
||||
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
|
||||
let receipt: ReturnType<Agent['steer']> | undefined
|
||||
let contextInjected = false
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (session !== agent.session || event.type !== 'step/end' || contextInjected) return
|
||||
contextInjected = true
|
||||
agent.inject(createUserMessage({
|
||||
content: [{ type: 'text', text: 'final context' }],
|
||||
source: { kind: 'plugin', plugin: 'finalize' },
|
||||
}))
|
||||
})
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
name: 'finalize',
|
||||
description: '',
|
||||
parameters: {},
|
||||
async execute(_args, exec) {
|
||||
// Steering lands while the concluding tool is still executing.
|
||||
// Steering lands while the concluding tool is still executing; the
|
||||
// step/end listener adds ordinary context after the normal result drain.
|
||||
receipt = agent.steer(createUserMessage({ content: [{ type: 'text', text: 'late steering' }], source: { kind: 'user' } }))
|
||||
exec.concludeTurn()
|
||||
return [{ type: 'text', text: 'final' }]
|
||||
@@ -744,6 +871,9 @@ describe('agent loop', () => {
|
||||
if (receipt === undefined) throw new Error('concluding tool did not submit steering')
|
||||
expect(await receipt.outcome).toEqual({ status: 'rejected' })
|
||||
expect(events).not.toContain('steering/message')
|
||||
expect(agent.session.events.some(event => event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.content.some(block => block.type === 'text' && block.text === 'final context'))).toBe(true)
|
||||
|
||||
send(agent, 'follow up')
|
||||
await waitForIdle(ctx, agent)
|
||||
@@ -752,6 +882,7 @@ describe('agent loop', () => {
|
||||
.flatMap(message => message.content)
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
expect(texts).toContain('final context')
|
||||
expect(texts).not.toContain('late steering')
|
||||
})
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/core/agent/README.md
|
||||
README.md: 8799bc3664b2137b386b752f905e1414fb770cb9
|
||||
README.zh.md: 851c174ba80bebbab8ee1255cb04be4bcec7eabd
|
||||
README.md: 8a6028352127c4638c0b5e0e3ee85964d1d7d734
|
||||
README.zh.md: ffa71ea987ab355ff2f30b6164376199cd5d0170
|
||||
@@ -9,7 +9,7 @@ import type { GoalView } from '@deepseek-ai/dsh-goal'
|
||||
import { createUserMessage, LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import type { TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import * as goalSession from '../src/index.ts'
|
||||
|
||||
declare module '@deepseek-ai/dsh-session' {
|
||||
@@ -787,35 +787,6 @@ describe('same-session goal driving', () => {
|
||||
expect(test.adapter.requests).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('yields to a round whose turn/end never committed instead of misreading it as settled', async () => {
|
||||
const test = await harness([textResponse('round ran')])
|
||||
// A persistent pre-commit turn/end rejection reaches idle with the
|
||||
// attempt's turn open and no terminal reason. The driver must yield
|
||||
// instead of clearing the reservation or scheduling another round.
|
||||
let roundTurn: number | undefined
|
||||
test.ctx.on('internal/dispatch', (_mode, name, args) => {
|
||||
if (name !== 'session/event') return
|
||||
const event = args[1] as SessionEvent
|
||||
if (event.type === 'turn/start' && event.data.trigger.kind === 'message'
|
||||
&& event.data.trigger.source.kind === 'goal') {
|
||||
roundTurn = event.data.turn
|
||||
}
|
||||
if (event.type === 'turn/end' && event.data.turn === roundTurn) {
|
||||
throw new Error('turn close permanently rejected')
|
||||
}
|
||||
})
|
||||
test.ctx.goals.create(test.agent, { objective: 'survive a lost turn end' })
|
||||
await waitForRequests(test.adapter, 1)
|
||||
await test.agent.whenIdle()
|
||||
await new Promise((resolve) => { setImmediate(resolve) })
|
||||
|
||||
expect(test.adapter.requests).toHaveLength(1)
|
||||
expect(test.ctx.goals.get(test.agent)).toMatchObject({
|
||||
phase: 'active',
|
||||
activation: 'armed',
|
||||
})
|
||||
})
|
||||
|
||||
it('cancels an accepted queued round and awaits its driver task during teardown', async () => {
|
||||
const test = await harness([])
|
||||
let unloading: Promise<void> | undefined
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-fork/README.md
|
||||
README.md: b448dc309bff07c744443530a648c7c30e4d20d9
|
||||
README.zh.md: 3e14206d5637fded9edb4e173608c55e3341f8fc
|
||||
README.md: 55475aee7841e91960de79887dfe9bf37afdf9da
|
||||
README.zh.md: 3eec8cb51a47243a1f06416a3f8f99ae8df8e734
|
||||
@@ -57,5 +57,4 @@ fork 会把保留的已完成历史复制到独立的子 agent 请求中;随
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **运行不公开 `sendMessage`/`resume`**:进程内运行不具备这些可选运行时能力。
|
||||
- **初始内容是一次性快照**:子 agent 只能看到 fork 时父 agent 已完成的轮次,看不到父 agent 此后记录的任何内容;不会实时共享上下文。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-spawn/README.md
|
||||
README.md: 868f829edbcfe2eb4d66ccd0ff9988924c70298b
|
||||
README.zh.md: 99cfdf0e633345d1152c59cbe5ce7a029eb6ec9d
|
||||
README.md: 811f19e6e68362bd14e75d0a9059ee61fda3f015
|
||||
README.zh.md: 2b189f77c4ff63ca026f472187a55c68def18ea1
|
||||
@@ -52,5 +52,4 @@ spawn 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona:
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **运行不公开 `sendMessage`/`resume`**:进程内运行不具备这些可选运行时能力。
|
||||
- **全新表示不含父 agent transcript(文本记录)**:子 agent 会继承 cwd、谱系、模型及显式配置的 persona/工具限制,但不继承父 agent 的任何对话;需要已完成轮次上下文时,请使用 fork 提供方。
|
||||
Reference in New Issue
Block a user