Skip to content

PR #32631 followup: ToolDefinition = z.infer<>, kill double factory#32683

Merged
dvargasfuertes merged 1 commit into
mainfrom
apollo/tool-definition-followup
May 30, 2026
Merged

PR #32631 followup: ToolDefinition = z.infer<>, kill double factory#32683
dvargasfuertes merged 1 commit into
mainfrom
apollo/tool-definition-followup

Conversation

@vellum-apollo-bot
Copy link
Copy Markdown
Contributor

Follow-up to #32631 — addresses all 4 review comments left on the APPROVED review.

Changes

1. types.tsToolDefinition = z.infer<typeof ToolDefinitionSchema> (review thread)

ToolDefinitionSchema now uses z.custom<...>() for both input_schema and execute:

input_schema: z
  .custom<object>(
    (val) => val !== null && typeof val === "object" && !Array.isArray(val),
    { message: "input_schema must be a plain object" },
  )
  .optional(),

execute: z
  .custom<
    (
      input: Record<string, unknown>,
      context: ToolContext,
    ) => Promise<ToolExecutionResult>
  >()
  .optional(),

Then:

export type ToolDefinition = z.infer<typeof ToolDefinitionSchema>;

Single declaration. No Omit<> overlay. Both fields are typed correctly by the schema itself.

z.custom on execute is a runtime no-op — but it doesn't need a runtime guard because the wire path drops closures (they can't cross IPC) and finalizeTool synthesizes a no-op error closure on arrival.

2. tool-defaults.tsfinalizeTool second arg optional (review thread)

-export function finalizeTool(tool: ToolDefinition, defaultName: string): Tool {
+export function finalizeTool(tool: ToolDefinition, defaultName = ""): Tool {

The "" fallback is fail-loud — registerSkillTools already rejects empty names with a clear error.

3. registries.ts — one-liner (review thread)

-const proxies = tools.map((tool) => finalizeTool(tool, tool.name ?? ""));
+const proxies = tools.map((t) => finalizeTool(t));

Defensive default lives inside finalizeTool now, not at every IPC call site.

4. ask-question-tool.ts — kill the double factory (review thread)

The createAskQuestionTool(prompterFactory = ...) factory existed only to inject a stub QuestionPrompter from tests. That's two layers of indirection for one mock seam. Replaced with a plain literal matching every other tool:

export const askQuestionTool = {
  // ...
  async execute(input, context) {
    const prompter = new QuestionPrompter({ broadcastMessage });
    // ...
  },
} satisfies ToolDefinition;

Test rewrite uses mock.module() + a dynamic await import() of the tool, which collapses both layers into one mock seam at the module boundary.

Verification

  • 55/55 tests pass across: ask-question-tool.test.ts (23), registries.test.ts (16), plugin-api-tool-definition.test.ts (4), plugin-tool-contribution.test.ts (12).
  • Lint clean on all touched files.
  • Spot tsc on changed files: no errors.

Stack

Builds on #32631 (merged). No interaction with any open PRs.

Address all 4 review comments from PR #32631 (post-merge):

1. types.ts — ToolDefinitionSchema now uses z.custom<object>(predicate)
   for input_schema and z.custom<(input, ctx) => Promise<...>>() for
   execute. ToolDefinition collapses to a direct z.infer<typeof
   ToolDefinitionSchema> — single declaration, no Omit overlay, both
   fields typed correctly. z.custom on execute is a runtime no-op (the
   wire path drops closures anyway, finalizeTool synthesizes a no-op
   error closure on arrival).

2. tool-defaults.ts — finalizeTool's defaultName arg is now optional
   with an empty-string default. The empty-string fallback is
   fail-loud because registerSkillTools rejects an empty name with a
   clear error.

3. registries.ts — call site is now a one-liner:
   tools.map((t) => finalizeTool(t)). The defensive '?? ""' moved
   inside finalizeTool itself, so it doesn't repeat at every IPC
   handler. Comment trimmed to match.

4. ask-question-tool.ts — killed the createAskQuestionTool factory and
   the prompterFactory parameter. askQuestionTool is now a plain
   literal matching every other tool: '} satisfies ToolDefinition'.
   Test rewrite uses mock.module() on ../../permissions/question-
   prompter.js + a dynamic import of the tool, replacing
   makeToolWithStub's prompter-factory seam.

Tests: 55/55 across ask-question, registries, plugin-api-tool-
definition, plugin-tool-contribution. Lint clean.
}
return `${prefix} Skipped`;
});
const prompter = new QuestionPrompter({ broadcastMessage });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QuestionPrompter can remove its broadcastMessage constructor args and just import it directly

@dvargasfuertes dvargasfuertes merged commit 0e73f9f into main May 30, 2026
13 checks passed
@dvargasfuertes dvargasfuertes deleted the apollo/tool-definition-followup branch May 30, 2026 19:59
vellum-apollo-bot Bot pushed a commit that referenced this pull request May 30, 2026
Workspace tools are user-authored files dropped under
`<workspaceDir>/tools/<name>.{ts,js,json}` that authoritatively replace
same-named core tools at registry-load time. The original core tool is
stashed and restored on workspace-tool unregister, so workspace overrides
are non-destructive to the core baseline.

Highlights:

- New `registerWorkspaceTools` / `unregisterWorkspaceTool` /
  `removeCoreToolViaWorkspace` / `restoreStrippedCoreTool` registry
  entry points. Ownership is tracked in the existing `ownersByName` map
  (`kind: "workspace"`, `id: <workspacePath>`) — no extra fields land
  on the `Tool` object, keeping the post-#32631 `Required<ToolDefinition>`
  shape pristine.
- Workspace ownership wins over plugin / skill / MCP: registrations of
  the same name from those surfaces warn-skip (or throw, for skills) so
  the workspace override stays canonical for its lifetime.
- `<name>.removed` sentinel strips a core tool from the live registry
  while keeping it in the stash. Deleting the sentinel restores it.
  Workspace-owned names can't be sentinel-stripped (the user must
  remove the workspace tool file first).
- File watcher (`workspace-tools-watcher.ts`) drives hot reload:
  re-imports on change, unregister on delete, re-strip / re-restore on
  sentinel toggle. Uses `getToolOwner` to detect workspace ownership
  (no `origin` field needed).
- Loader (`workspace-tools/loader.ts`) delegates default-filling to the
  shared `finalizeTool` from `tool-defaults.ts`, layering only two
  workspace-specific overrides on top: `defaultRiskLevel: "high"` (user
  code from disk runs with full privilege) and a workspace-flavored
  default `execute` error message.

Followup items from PR #31949 round-1 review (now incorporated):

- Generalized the `workspace-tools.md` doc example to use
  `ToolDefinition` instead of `PluginTool` (comment 3295343340).
- Default-risk-level table entry shows `"high"` (comment 3295345328).

Followup from PR #32683 review (also incorporated):

- `QuestionPrompter` dropped its `{ broadcastMessage }` constructor arg
  and imports `broadcastMessage` directly from `runtime/assistant-event-hub.js`.
  Updated `ask-question-tool.ts` caller + `question-prompter.test.ts`
  setup (the test now mocks the module export rather than injecting via
  constructor; `mock.module` calls in that file also spread the real
  module exports so other tests in the same `bun test` run still see
  e.g. `assistantEventHub` and `setNestedValue`).

Tests: 88/88 passing across the 4 critical files (registry.test.ts,
workspace-tool-loader.test.ts, question-prompter.test.ts,
ask-question-tool.test.ts).
vellum-apollo-bot Bot added a commit that referenced this pull request May 30, 2026
Workspace tools are user-authored files dropped under
`<workspaceDir>/tools/<name>.{ts,js,json}` that authoritatively replace
same-named core tools at registry-load time. The original core tool is
stashed and restored on workspace-tool unregister, so workspace overrides
are non-destructive to the core baseline.

Highlights:

- New `registerWorkspaceTools` / `unregisterWorkspaceTool` /
  `removeCoreToolViaWorkspace` / `restoreStrippedCoreTool` registry
  entry points. Ownership is tracked in the existing `ownersByName` map
  (`kind: "workspace"`, `id: <workspacePath>`) — no extra fields land
  on the `Tool` object, keeping the post-#32631 `Required<ToolDefinition>`
  shape pristine.
- Workspace ownership wins over plugin / skill / MCP: registrations of
  the same name from those surfaces warn-skip (or throw, for skills) so
  the workspace override stays canonical for its lifetime.
- `<name>.removed` sentinel strips a core tool from the live registry
  while keeping it in the stash. Deleting the sentinel restores it.
  Workspace-owned names can't be sentinel-stripped (the user must
  remove the workspace tool file first).
- File watcher (`workspace-tools-watcher.ts`) drives hot reload:
  re-imports on change, unregister on delete, re-strip / re-restore on
  sentinel toggle. Uses `getToolOwner` to detect workspace ownership
  (no `origin` field needed).
- Loader (`workspace-tools/loader.ts`) delegates default-filling to the
  shared `finalizeTool` from `tool-defaults.ts`, layering only two
  workspace-specific overrides on top: `defaultRiskLevel: "high"` (user
  code from disk runs with full privilege) and a workspace-flavored
  default `execute` error message.

Followup items from PR #31949 round-1 review (now incorporated):

- Generalized the `workspace-tools.md` doc example to use
  `ToolDefinition` instead of `PluginTool` (comment 3295343340).
- Default-risk-level table entry shows `"high"` (comment 3295345328).

Followup from PR #32683 review (also incorporated):

- `QuestionPrompter` dropped its `{ broadcastMessage }` constructor arg
  and imports `broadcastMessage` directly from `runtime/assistant-event-hub.js`.
  Updated `ask-question-tool.ts` caller + `question-prompter.test.ts`
  setup (the test now mocks the module export rather than injecting via
  constructor; `mock.module` calls in that file also spread the real
  module exports so other tests in the same `bun test` run still see
  e.g. `assistantEventHub` and `setNestedValue`).

Tests: 88/88 passing across the 4 critical files (registry.test.ts,
workspace-tool-loader.test.ts, question-prompter.test.ts,
ask-question-tool.test.ts).
devin-ai-integration Bot pushed a commit that referenced this pull request May 31, 2026
Workspace tools are user-authored files dropped under
`<workspaceDir>/tools/<name>.{ts,js,json}` that authoritatively replace
same-named core tools at registry-load time. The original core tool is
stashed and restored on workspace-tool unregister, so workspace overrides
are non-destructive to the core baseline.

Highlights:

- New `registerWorkspaceTools` / `unregisterWorkspaceTool` /
  `removeCoreToolViaWorkspace` / `restoreStrippedCoreTool` registry
  entry points. Ownership is tracked in the existing `ownersByName` map
  (`kind: "workspace"`, `id: <workspacePath>`) — no extra fields land
  on the `Tool` object, keeping the post-#32631 `Required<ToolDefinition>`
  shape pristine.
- Workspace ownership wins over plugin / skill / MCP: registrations of
  the same name from those surfaces warn-skip (or throw, for skills) so
  the workspace override stays canonical for its lifetime.
- `<name>.removed` sentinel strips a core tool from the live registry
  while keeping it in the stash. Deleting the sentinel restores it.
  Workspace-owned names can't be sentinel-stripped (the user must
  remove the workspace tool file first).
- File watcher (`workspace-tools-watcher.ts`) drives hot reload:
  re-imports on change, unregister on delete, re-strip / re-restore on
  sentinel toggle. Uses `getToolOwner` to detect workspace ownership
  (no `origin` field needed).
- Loader (`workspace-tools/loader.ts`) delegates default-filling to the
  shared `finalizeTool` from `tool-defaults.ts`, layering only two
  workspace-specific overrides on top: `defaultRiskLevel: "high"` (user
  code from disk runs with full privilege) and a workspace-flavored
  default `execute` error message.

Followup items from PR #31949 round-1 review (now incorporated):

- Generalized the `workspace-tools.md` doc example to use
  `ToolDefinition` instead of `PluginTool` (comment 3295343340).
- Default-risk-level table entry shows `"high"` (comment 3295345328).

Followup from PR #32683 review (also incorporated):

- `QuestionPrompter` dropped its `{ broadcastMessage }` constructor arg
  and imports `broadcastMessage` directly from `runtime/assistant-event-hub.js`.
  Updated `ask-question-tool.ts` caller + `question-prompter.test.ts`
  setup (the test now mocks the module export rather than injecting via
  constructor; `mock.module` calls in that file also spread the real
  module exports so other tests in the same `bun test` run still see
  e.g. `assistantEventHub` and `setNestedValue`).

Tests: 88/88 passing across the 4 critical files (registry.test.ts,
workspace-tool-loader.test.ts, question-prompter.test.ts,
ask-question-tool.test.ts).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant