Merge branch 'codex/canonical-tool-output' into codex/code-mode-typed-results
This commit is contained in:
@@ -30,7 +30,7 @@ This plugin registers **no service** and owns no storage or preview mechanics: p
|
||||
|
||||
## Scope
|
||||
|
||||
The policy sees only the FINAL formatted surface result—not a tool's internal resource or canonical value. If a provider already truncated (e.g. `web-fetch-local.maxBodyChars`), the spill artifact holds the full formatted result the tool returned, not the full original source. Provider/resource caps stay mandatory and separate. `glob`/`grep` own item-level surface spill because their complete acquired values still exist before rendering; bash streams own acquisition-time spill. See the [tool output spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md).
|
||||
The policy sees only the FINAL formatted surface result—not a tool's internal resource or canonical value. If a provider already truncated (e.g. `web-fetch-local.maxBodyChars`), the spill artifact holds the full formatted result the tool returned, not the full original source. Provider/resource caps stay mandatory and separate. `glob`/`grep` own item-level surface spill because their complete acquired values still exist before rendering; bash streams own acquisition-time spill. The generic policy prepends its waterfall listener, then delegates, so ordinary tool-owned asynchronous projections complete before generic byte bounding regardless of plugin load order. See the [tool output spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md).
|
||||
|
||||
## Model Experience
|
||||
|
||||
|
||||
@@ -26,10 +26,11 @@
|
||||
* failure ⇒ log and return the original result. A spill failure must NEVER
|
||||
* turn a successful tool call into an `isError` or hide the inline result.
|
||||
*
|
||||
* It COMPOSES with other post-execute listeners: it delegates via `next()` and
|
||||
* bounds the resulting content projection, so a hook that replaced content
|
||||
* still has its replacement bounded, while value replacements and `block`
|
||||
* decisions pass through unchanged.
|
||||
* It COMPOSES with other post-execute listeners: its prepended listener
|
||||
* delegates via `next()` and bounds the resulting content projection, so
|
||||
* tool-owned asynchronous projection runs before generic bounding, a hook that
|
||||
* replaced content still has its replacement bounded, and value replacements
|
||||
* and `block` decisions pass through unchanged.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-spill-policy
|
||||
*/
|
||||
@@ -176,5 +177,5 @@ export function apply(ctx: Context, config: Config): void {
|
||||
}
|
||||
const replaced: ContentBlock[] = [{ type: 'text', text: replacedText }]
|
||||
return { kind: 'accept', content: replaced, ...decision.additionalContexts ? { additionalContexts: decision.additionalContexts } : {} }
|
||||
})
|
||||
}, { prepend: true })
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
|
||||
import type { ToolExecution, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
|
||||
import type { PostToolDecision, ToolExecution, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
|
||||
import { SpillLocator, SpillStore } from '@deepseek-ai/dsh-spill'
|
||||
import type { SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill'
|
||||
import * as SpillPolicy from '@deepseek-ai/dsh-spill-policy'
|
||||
@@ -61,7 +61,11 @@ function exec(name: string, session = 's1'): ToolExecution {
|
||||
* Build a context with tools + the policy, and optionally a spill backend.
|
||||
* Returns the context and the backend handle (undefined when `withSpill` false).
|
||||
*/
|
||||
async function setup(config: SpillPolicy.Config, withSpill = true): Promise<{ ctx: Context; spill?: StubStore; fiber: Awaited<ReturnType<Context['plugin']>> }> {
|
||||
async function setup(
|
||||
config: SpillPolicy.Config,
|
||||
withSpill = true,
|
||||
beforePolicy?: (ctx: Context) => void,
|
||||
): Promise<{ ctx: Context; spill?: StubStore; fiber: Awaited<ReturnType<Context['plugin']>> }> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
@@ -70,6 +74,7 @@ async function setup(config: SpillPolicy.Config, withSpill = true): Promise<{ ct
|
||||
await ctx.plugin(StubStore)
|
||||
spill = ctx.spillStore as StubStore
|
||||
}
|
||||
beforePolicy?.(ctx)
|
||||
const fiber = await ctx.plugin(SpillPolicy, config)
|
||||
return { ctx, fiber, ...spill ? { spill } : {} }
|
||||
}
|
||||
@@ -272,6 +277,26 @@ describe('best-effort fallback', () => {
|
||||
})
|
||||
|
||||
describe('composition', () => {
|
||||
it('wraps an earlier tool-owned projection before applying the generic cap', async () => {
|
||||
let downstreamDecision: PostToolDecision | undefined
|
||||
const { ctx, spill } = await setup({ maxInlineBytes: 200 }, true, (target) => {
|
||||
target.on('tools/post-execute', async (_exec, _result, next): Promise<PostToolDecision> => {
|
||||
downstreamDecision = await next()
|
||||
return {
|
||||
kind: 'accept',
|
||||
content: [{ type: 'text', text: `first page\n\nFull canonical result stored at /spill/search-results.txt.\n${'z'.repeat(500)}` }],
|
||||
}
|
||||
})
|
||||
})
|
||||
ctx.tools.register(textTool('search', 'initial capped page'))
|
||||
|
||||
const result = await ctx.tools.execute(exec('search'))
|
||||
|
||||
expect(downstreamDecision).toEqual({ kind: 'accept' })
|
||||
expect(spill?.saves[0]?.content).toContain('Full canonical result stored at /spill/search-results.txt.')
|
||||
expect(textOf(result.content)).toContain('Full formatted result stored at')
|
||||
})
|
||||
|
||||
it('bounds content a downstream post-execute listener replaced', async () => {
|
||||
const { ctx, spill } = await setup({ maxInlineBytes: 200 })
|
||||
// A later-registered listener replaces the (small) tool result with a big one;
|
||||
|
||||
Reference in New Issue
Block a user