Merge pull request #3680 from Tibsfox/fix/restore-tools-after-discuss

fix(gsd): restore full tool set after discuss flow scoping
This commit is contained in:
Jeremy McSpadden 2026-04-07 07:06:38 -05:00 committed by GitHub
commit 218e53addd
2 changed files with 72 additions and 0 deletions

View file

@ -295,8 +295,10 @@ async function dispatchWorkflow(
// "Grammar is too complex" when the combined tool schema is too large.
// Discuss flows only need a small subset of GSD tools — strip the heavy
// planning/execution/completion tools to keep the grammar within limits.
let savedTools: string[] | null = null;
if (unitType?.startsWith("discuss-")) {
const currentTools = pi.getActiveTools();
savedTools = currentTools;
// Keep all non-GSD tools (builtins, other extensions) and only the
// GSD tools on the discuss allowlist.
const scopedTools = currentTools.filter(
@ -322,6 +324,13 @@ async function dispatchWorkflow(
},
{ triggerTurn: true },
);
// Restore full tool set after the message is queued. The LLM turn has
// already captured the scoped set — restoring prevents the narrowed
// tools from leaking into subsequent dispatches (#3628).
if (savedTools) {
pi.setActiveTools(savedTools);
}
}
/**

View file

@ -0,0 +1,63 @@
/**
* Regression test for #3628 restore tool set after discuss flow scoping
*
* The discuss flow narrows the active tool set to avoid "grammar too complex"
* errors. Without restoring after sendMessage, the narrowed tools leaked into
* subsequent dispatches, breaking plan/execute flows.
*
* The fix saves the full tool set before scoping and restores it after
* sendMessage returns.
*/
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const src = readFileSync(
resolve(process.cwd(), 'src', 'resources', 'extensions', 'gsd', 'guided-flow.ts'),
'utf-8',
)
describe('restore tools after discuss flow scoping (#3628)', () => {
it('savedTools is declared before the discuss scoping block', () => {
// savedTools must be declared before the discuss-* check
const savedToolsDecl = src.indexOf('let savedTools')
const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-"))')
assert.ok(savedToolsDecl !== -1, 'savedTools variable must be declared')
assert.ok(discussCheck !== -1, 'discuss-* type check must exist')
assert.ok(
savedToolsDecl < discussCheck,
'savedTools must be declared before the discuss scoping block',
)
})
it('savedTools captures current tools inside the discuss block', () => {
const discussCheck = src.indexOf('if (unitType?.startsWith("discuss-"))')
assert.ok(discussCheck !== -1)
// Look for savedTools assignment within the discuss block
const blockAfter = src.slice(discussCheck, discussCheck + 500)
assert.ok(
blockAfter.includes('savedTools = currentTools'),
'savedTools must be assigned from currentTools inside the discuss block',
)
})
it('savedTools is restored after sendMessage', () => {
// Find the sendMessage call
const sendMsg = src.indexOf('triggerTurn: true')
assert.ok(sendMsg !== -1, 'sendMessage with triggerTurn must exist')
// After sendMessage, savedTools should be restored via setActiveTools
const afterSend = src.slice(sendMsg, sendMsg + 500)
assert.ok(
afterSend.includes('if (savedTools)'),
'savedTools restoration guard must exist after sendMessage',
)
assert.ok(
afterSend.includes('setActiveTools(savedTools)'),
'setActiveTools(savedTools) must be called to restore the full tool set',
)
})
})