Skip to content

bug(core): the workflow keyword bridge sentence names tools CodeModeOnly hides and refuses #12425

Description

@yiliang114

What happened

#12323 adds a rule to the bundled-reference route resolver: under ToolMode.CodeModeOnly, registration is not reachability, because CodeModeOnly hides both bridge tools (packages/core/src/tools/code-mode.ts:28HIDDEN_TOOLS = new Set([ToolNames.TOOL_SEARCH, 'tool_call']), surfaced as 'hidden' by getToolExposure), so a bridge pointer would name tools the session cannot call. The guard sits at packages/core/src/skills/bundled-reference.ts:152:

config.getToolMode?.() !== ToolMode.CodeModeOnly &&
isToolDeferredBehindToolSearch(config, ToolNames.SKILL)

The sibling export eighty lines below, isToolHiddenBehindToolSearch (:234-240), keeps the pure registration + visibility check with no tool-mode arm — and the consumer that turns it into a bridge instruction can still emit one. packages/cli/src/ui/utils/workflow-keyword.ts:146 calls it for ToolNames.WORKFLOW, reached from AppContainer.tsx:3187 on every user message containing "workflow", gated only on ui.disableWorkflowKeywordTrigger and shell mode. When it returns true and both bridge tools are in getAllToolNames(), the hook sets bridgeWorkflowTool = true and the steering notice carries toolSearchBridgeSentence(ToolDisplayNames.WORKFLOW) — telling the model to review the schema with tool_search and invoke it with tool_call, instead of pointing at tools.workflow(...) inside exec, the only real route there.

With tools.codeModeOnly: true plus a tools.eager allowlist omitting workflow, every part of that path holds:

  • registerLazyTool marks Workflow permission-deferred regardless of mode (packages/core/src/config/config.ts:11089, registerPermissionDeferredFactory);
  • both bridge tools are registered unconditionally in the standard branch (config.ts:11370-11377, under the // --- Core tools (always registered) --- comment);
  • so isToolDeferredBehindToolSearch returns true, the consumer's "both halves registered" test passes, and each triggered turn injects a reminder towards tools that getToolExposure marks hidden, getCodeModeFunctionDeclarations never declares and isCodeModeToolCallAllowed refuses.

The predicate's body is byte-identical to the merge-base version — the blind spot is carried over rather than introduced — but the guard landing beside it in the same file is what makes the contradiction shipped and adjacent. The route path's own new comment names exactly this outcome.

Prevalence is unmeasured: the review that filed this used a stub Config, not an end-to-end session, and found no shipping preset combining codeModeOnly with an eager allowlist that omits workflow. What ships is a documented contradiction between two neighbours in one file, not an observed field failure.

What I expected

One rule, applied to both exports. A tool the session cannot call should not be named in a bridge instruction, whichever function is asked.

Suggested fix

Give the shared predicate the same mode check, which fixes both arms at once — at the top of isToolDeferredBehindToolSearch:

if (config.getToolMode?.() === ToolMode.CodeModeOnly) return false;

or guard at the workflow-keyword.ts:146 call site if the predicate's other callers should keep the registration-only answer.

Two constraints:

  • config.ts:11370-11377 registers TOOL_CALL and TOOL_SEARCH unconditionally, so a fix cannot infer CodeModeOnly from the bridge tools being absent from getAllToolNames().
  • It must not change the CodeModeOnly route away from 'skill'. That is safe by construction at perf(core): move the Agent tool's prompt-writing guidance into a bundled skill #12323's head — the route resolver short-circuits before calling the predicate — but it is pinned by workflow-authoring-skill.test.ts's points straight at the skill when CodeModeOnly hides the bridge, so a fix that moved the guard into the resolver instead of the predicate would show up there.

Acceptance test

Must go red when the mode check is removed:

  • a workflow-keyword.test.ts case with getToolMode() → ToolMode.CodeModeOnly and workflow permission-deferred, asserting the returned prefix does not contain toolSearchBridgeSentence(ToolDisplayNames.WORKFLOW); and/or
  • an it.each row ['deferred under CodeModeOnly', { deferred: [ToolNames.WORKFLOW], toolMode: ToolMode.CodeModeOnly }, false] — the existing stub already accepts toolMode.

Filed from R2-3 in #12323's round-2 review, which was explicitly left out of that PR as its own change: #12323 (comment)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    category/coreCore engine and logicpriority/P3Low - Minor, cosmetic, nice-to-fix issuesreview/self-reportedThe linked issue was opened by the PR author (self-reported)scope/clitype/bugSomething isn't working as expected

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions