Merge branch 'master' into worktree/provider-routed-llm-adapters
This commit is contained in:
@@ -65,10 +65,6 @@ export interface PromptSection {
|
||||
export interface AssembledSection {
|
||||
/** The contributing section's unique name. */
|
||||
name: string
|
||||
// TODO(assembled-section-order): drop this output field; registry order has
|
||||
// already sorted the array, and no production renderer/listener reads it.
|
||||
/** The contributing section's order (sections arrive sorted ascending). */
|
||||
order: number
|
||||
/** The resolved (but not yet interpolated) section text. */
|
||||
text: string
|
||||
}
|
||||
@@ -403,12 +399,11 @@ export class SystemPrompt extends Service {
|
||||
}
|
||||
const assembly: PromptAssembly = {
|
||||
sections: [...sectionByName.values()]
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.map(section => ({
|
||||
name: section.name,
|
||||
order: section.order,
|
||||
text: typeof section.text === 'function' ? section.text(context) : section.text,
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order),
|
||||
})),
|
||||
tools: orderTools(collected, this.toolOrder, knownNames),
|
||||
variables,
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('scoped assemble dispatch', () => {
|
||||
scope.ctx.on('system-prompt/assemble', async (_assembly: PromptAssembly, context, next: () => Promise<PromptAssembly>) => {
|
||||
shaped.push(context.scope)
|
||||
const result = await next()
|
||||
result.sections.push({ name: 'listener:extra', order: 999, text: 'listener text' })
|
||||
result.sections.push({ name: 'listener:extra', text: 'listener text' })
|
||||
return result
|
||||
})
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ describe('SystemPrompt', () => {
|
||||
await ctx.plugin(SystemPrompt, { persona: 'You are DeepSeek Harness SDK.' })
|
||||
|
||||
const assembly = await ctx.systemPrompt.assemble()
|
||||
expect(assembly.sections.map(s => [s.name, s.order])).toEqual([
|
||||
['harness:identity', -100],
|
||||
['deployment:persona', 0],
|
||||
expect(assembly.sections.map(s => s.name)).toEqual([
|
||||
'harness:identity',
|
||||
'deployment:persona',
|
||||
])
|
||||
expect(renderPrompt(assembly)).toBe(`${IDENTITY}\n\nYou are DeepSeek Harness SDK.`)
|
||||
// The names are reserved by the plugin — one owner per section.
|
||||
@@ -183,7 +183,7 @@ describe('SystemPrompt', () => {
|
||||
const contexts: AssembleContext[] = []
|
||||
ctx.on('system-prompt/assemble', async (assembly: PromptAssembly, context, next) => {
|
||||
contexts.push(context)
|
||||
assembly.sections.push({ name: 'from-a', order: 100, text: 'a' })
|
||||
assembly.sections.push({ name: 'from-a', text: 'a' })
|
||||
return next()
|
||||
})
|
||||
// Listener B (registered later, runs after A) sees A's contribution.
|
||||
@@ -235,8 +235,8 @@ describe('SystemPrompt', () => {
|
||||
it('filters out empty section text from renderPrompt', () => {
|
||||
const result = renderPrompt({
|
||||
sections: [
|
||||
{ name: 'empty', order: 0, text: '' },
|
||||
{ name: 'real', order: 1, text: 'content' },
|
||||
{ name: 'empty', text: '' },
|
||||
{ name: 'real', text: 'content' },
|
||||
],
|
||||
tools: [],
|
||||
variables: {},
|
||||
@@ -356,13 +356,13 @@ describe('SystemPrompt', () => {
|
||||
})
|
||||
|
||||
it('names "(none)" when no variables are registered at all', () => {
|
||||
expect(() => renderPrompt({ sections: [{ name: 's', order: 0, text: '{{x}}' }], tools: [], variables: {} }))
|
||||
expect(() => renderPrompt({ sections: [{ name: 's', text: '{{x}}' }], tools: [], variables: {} }))
|
||||
.toThrow('unknown prompt variable "{{x}}" in section "s"; registered variables: (none)')
|
||||
})
|
||||
|
||||
it('throws when a referenced variable has no value for this assembly', () => {
|
||||
expect(() => renderPrompt({
|
||||
sections: [{ name: 'persona', order: 0, text: 'in {{cwd}}' }],
|
||||
sections: [{ name: 'persona', text: 'in {{cwd}}' }],
|
||||
tools: [],
|
||||
variables: { cwd: undefined },
|
||||
})).toThrow('prompt variable "{{cwd}}" has no value for this assembly (section "persona")')
|
||||
@@ -370,7 +370,7 @@ describe('SystemPrompt', () => {
|
||||
|
||||
it('throws on a malformed complete reference, e.g. inner spaces', () => {
|
||||
expect(() => renderPrompt({
|
||||
sections: [{ name: 's', order: 0, text: 'on {{ model }}' }],
|
||||
sections: [{ name: 's', text: 'on {{ model }}' }],
|
||||
tools: [],
|
||||
variables: { model: 'm' },
|
||||
})).toThrow('malformed prompt variable reference "{{ model }}" in section "s"')
|
||||
@@ -378,7 +378,7 @@ describe('SystemPrompt', () => {
|
||||
|
||||
it('leaves a lone {{ verbatim only when NO }} follows anywhere after it', () => {
|
||||
const text = renderPrompt({
|
||||
sections: [{ name: 's', order: 0, text: 'shell ${X:-{{fallback} stays' }],
|
||||
sections: [{ name: 's', text: 'shell ${X:-{{fallback} stays' }],
|
||||
tools: [],
|
||||
variables: {},
|
||||
})
|
||||
@@ -390,7 +390,7 @@ describe('SystemPrompt', () => {
|
||||
{ text: 'x {{a{b}} y {{model}}', label: 'nested brace inside a would-be group' },
|
||||
])('throws on a mangled reference with a }} still following ($label)', ({ text }) => {
|
||||
expect(() => renderPrompt({
|
||||
sections: [{ name: 's', order: 0, text }],
|
||||
sections: [{ name: 's', text }],
|
||||
tools: [],
|
||||
variables: { model: 'm' },
|
||||
})).toThrow('malformed prompt variable reference at')
|
||||
@@ -400,7 +400,7 @@ describe('SystemPrompt', () => {
|
||||
// `in` would find Object.prototype.constructor and splice function
|
||||
// source into the prompt; Object.hasOwn must reject it instead.
|
||||
expect(() => renderPrompt({
|
||||
sections: [{ name: 's', order: 0, text: 'on {{constructor}}' }],
|
||||
sections: [{ name: 's', text: 'on {{constructor}}' }],
|
||||
tools: [],
|
||||
variables: { model: 'm' },
|
||||
})).toThrow('unknown prompt variable "{{constructor}}"')
|
||||
@@ -416,7 +416,7 @@ describe('SystemPrompt', () => {
|
||||
|
||||
it('never re-scans substituted values (a value containing {{sneaky}} stays literal)', () => {
|
||||
const text = renderPrompt({
|
||||
sections: [{ name: 's', order: 0, text: 'v = {{model}}!' }],
|
||||
sections: [{ name: 's', text: 'v = {{model}}!' }],
|
||||
tools: [],
|
||||
variables: { model: 'literal {{sneaky}} inside' },
|
||||
})
|
||||
|
||||
@@ -108,14 +108,13 @@ function renderValue(value: unknown): string {
|
||||
/** The run_code result's `meta` payload (JSON-serializable; `presentResult` narrows it back). */
|
||||
interface RunCodeMeta {
|
||||
logs: CodeRunResult['logs']
|
||||
dispatches: number
|
||||
}
|
||||
|
||||
/** Soft-narrow a result `meta` back to {@link RunCodeMeta} (replay may carry older shapes; presentation must not throw). */
|
||||
function asRunCodeMeta(meta: unknown): RunCodeMeta | undefined {
|
||||
if (typeof meta !== 'object' || meta === null) return undefined
|
||||
const m = meta as Record<string, unknown>
|
||||
if (!Array.isArray(m.logs) || typeof m.dispatches !== 'number') return undefined
|
||||
if (!Array.isArray(m.logs) || !m.logs.every(log => typeof log === 'string')) return undefined
|
||||
return m as unknown as RunCodeMeta
|
||||
}
|
||||
|
||||
@@ -251,12 +250,12 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.map(entry => entry.text).join('\n')}` : ''
|
||||
const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join('\n')}` : ''
|
||||
throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}`)
|
||||
}
|
||||
const rendered = renderValue(result.value)
|
||||
const parts = [result.logs.map(entry => entry.text).join('\n'), rendered].filter(part => part.length > 0)
|
||||
const meta: RunCodeMeta = { logs: result.logs, dispatches }
|
||||
const parts = [result.logs.join('\n'), rendered].filter(part => part.length > 0)
|
||||
const meta: RunCodeMeta = { logs: result.logs }
|
||||
return {
|
||||
content: [{ type: 'text', text: parts.length > 0 ? parts.join('\n') : '(run_code completed with no output)' }],
|
||||
meta,
|
||||
@@ -278,7 +277,7 @@ export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () =>
|
||||
presentResult: (_args, result) => {
|
||||
const meta = asRunCodeMeta(result.meta)
|
||||
if (!meta) return undefined
|
||||
const output = meta.logs.map(entry => entry.text).join('\n')
|
||||
const output = meta.logs.join('\n')
|
||||
return {
|
||||
card: 'generic',
|
||||
...output.length > 0 ? { content: [{ type: 'text' as const, text: output }] } : {},
|
||||
|
||||
@@ -328,7 +328,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
const tools = request.bindings[0]!.functions
|
||||
const first = await tools.echo!({ value: 'one' })
|
||||
const second = await tools.echo!({ value: 'two' })
|
||||
return { logs: [{ source: 'console', level: 'log', text: `saw ${String(first)}` }], value: second }
|
||||
return { logs: [`saw ${String(first)}`], value: second }
|
||||
}
|
||||
const result = await runCode(ctx, 'const …: string = …', { agent })
|
||||
expect(result.isError).toBe(false)
|
||||
@@ -339,7 +339,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
{ parentCallId: 'call-1', subCallId: 'call-1:code:1', name: 'echo', arguments: { value: 'one' }, isError: false, resultSummary: 'echo:one' },
|
||||
{ parentCallId: 'call-1', subCallId: 'call-1:code:2', name: 'echo', arguments: { value: 'two' }, isError: false, resultSummary: 'echo:two' },
|
||||
])
|
||||
expect(result.meta).toEqual({ logs: [{ source: 'console', level: 'log', text: 'saw echo:one' }], dispatches: 2 })
|
||||
expect(result.meta).toEqual({ logs: ['saw echo:one'] })
|
||||
})
|
||||
|
||||
it('exposes only an opaque parent token to nested result observers', async () => {
|
||||
@@ -503,7 +503,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
it('converts a failed run into a structured isError result carrying kind, message, and captured logs', async () => {
|
||||
const { ctx, runtime } = await setup({ mode: 'code' })
|
||||
runtime.behavior = () => Promise.resolve({
|
||||
logs: [{ source: 'console', level: 'log', text: 'got this far' }],
|
||||
logs: ['got this far'],
|
||||
error: { kind: 'timeout', message: 'compute budget exhausted (300ms busy)' },
|
||||
})
|
||||
const result = await runCode(ctx, 'program')
|
||||
@@ -627,7 +627,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
const view = tool.presentResult?.({ code: 'return 1' }, {
|
||||
content: [{ type: 'text', text: 'model-facing' }],
|
||||
isError: false,
|
||||
meta: { logs: [{ source: 'console', level: 'log', text: 'printed' }], dispatches: 1 },
|
||||
meta: { logs: ['printed'] },
|
||||
})
|
||||
// The result omits the title — an update replaces only provided fields,
|
||||
// so the pending card's program title persists through completion.
|
||||
@@ -636,9 +636,10 @@ describe('the run_code dispatch bridge', () => {
|
||||
content: [{ type: 'text', text: 'printed' }],
|
||||
})
|
||||
// No captured output → no content either; everything pending persists.
|
||||
expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { logs: [], dispatches: 2 } }))
|
||||
expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { logs: [] } }))
|
||||
.toEqual({ card: 'generic' })
|
||||
// Replay with an unrecognizable meta falls back to the generic rendering.
|
||||
expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { logs: [{ text: 'legacy' }], dispatches: 1 } })).toBeUndefined()
|
||||
expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false, meta: { other: true } })).toBeUndefined()
|
||||
expect(tool.presentResult?.({ code: 'x' }, { content: [], isError: false })).toBeUndefined()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user