fix(tools,client): chain-mode resolution and a live preset label
modeFor read only the exact scope's layer, so a `code`-preset session advertised the native catalog: the mode is declared on the preset's STANDING scope, and the agent only parents to it. Nearest scope wins along the chain — the mode decides what the model SEES, which is the class of fact the chain inherits. Caught live (the model politely computed with bash and said run_code was not in its list); the chain test pins it. The client half of the label fix: the create echo and the session-added frame's agentPreset now reach the session list (newest wins in the upsert — every producer of the field reports the CURRENT composition), and a confirmed blank-session switch publishes through the new ISessions.noteAgentPreset, so the header label moves with the composition instead of waiting for a reload.
This commit is contained in:
@@ -835,7 +835,16 @@ export class ToolRegistry extends Service {
|
||||
* @returns the resolved presentation mode.
|
||||
*/
|
||||
private modeFor(scope?: ScopeKey): ToolPresentationMode {
|
||||
return this.layers.peek(scope)?.mode ?? this.defaultMode
|
||||
// Nearest scope wins along the chain: a preset's standing declaration
|
||||
// covers every agent parented under it, and an agent's own (were one ever
|
||||
// declared) would override its preset's. The mode decides what the model
|
||||
// SEES, which is exactly the class of fact the chain inherits.
|
||||
const layers = this.layers.chainLayers(scope)
|
||||
for (let index = layers.length - 1; index >= 0; index -= 1) {
|
||||
const mode = layers[index]?.mode
|
||||
if (mode !== undefined) return mode
|
||||
}
|
||||
return this.defaultMode
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1587,6 +1587,27 @@ describe('per-agent presentation', () => {
|
||||
expect(native.sections.some(section => section.name === 'tools:sdk')).toBe(false)
|
||||
})
|
||||
|
||||
it('inherits a STANDING preset scope\'s mode down the chain, agents beside it unaffected', async () => {
|
||||
const { setScopeParent } = await import('@deepseek-ai/dsh-scope')
|
||||
const { ctx, systemPrompt } = await setup({ mode: 'native' })
|
||||
registerEcho(ctx)
|
||||
// The preset's standing scope declares once; the agent only PARENTS to it
|
||||
// (the per-preset standing-mount shape — no per-agent declaration at all).
|
||||
const standing = await mintAgentScope(ctx, 'preset:code-like')
|
||||
standing.scope.ctx.tools.presentAs('code')
|
||||
const joined = await mintAgentScope(ctx, 'joined-agent')
|
||||
setScopeParent(joined.agent, standing.agent)
|
||||
const loner = await mintAgentScope(ctx, 'loner-agent')
|
||||
|
||||
expect(ctx.tools.get(RUN_CODE_NAME, joined.agent)).toBeDefined()
|
||||
const coded = await systemPrompt.assemble({ scope: joined.agent })
|
||||
expect(coded.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
|
||||
// A sibling that never parented stays native, as does the global view.
|
||||
expect(ctx.tools.get(RUN_CODE_NAME, loner.agent)).toBeUndefined()
|
||||
const native = await systemPrompt.assemble({ scope: loner.agent })
|
||||
expect(native.tools.map(tool => tool.name)).toEqual(['echo'])
|
||||
})
|
||||
|
||||
it('keeps run_code out of a native agent\'s dispatch table', async () => {
|
||||
const { ctx } = await setup({ mode: 'native' })
|
||||
registerEcho(ctx)
|
||||
|
||||
Reference in New Issue
Block a user