Skip to content

feat(tool): unify extension registry#43

Merged
omarluq merged 2 commits into
mainfrom
feat/unified-tool-registry
May 23, 2026
Merged

feat(tool): unify extension registry#43
omarluq merged 2 commits into
mainfrom
feat/unified-tool-registry

Conversation

@omarluq
Copy link
Copy Markdown
Owner

@omarluq omarluq commented May 23, 2026

No description provided.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Extension tools now support custom input schemas for more sophisticated tool definitions.
    • Added duplicate tool detection to prevent registration conflicts.
  • Bug Fixes

    • Improved tool execution to properly handle extension tools alongside built-in tools.
  • Tests

    • Added comprehensive test coverage for tool registry behavior and extension tool execution.

Walkthrough

This PR extends the tool system to support extension-provided tools with custom JSON schemas and makes tool definition selection request-aware. It adds a per-request ToolRegistry field, refactors tool schema generation to operate on definition objects, introduces an extension tool adapter layer, and threads the registry through completion handling for Anthropic, OpenAI Chat, and OpenAI Responses.

Changes

Extension Tool Registration & Request-Aware Schemas

Layer / File(s) Summary
Definition Schema Field & Built-in Tools
internal/tool/types.go, internal/tool/bash.go, internal/tool/edit.go, internal/tool/find.go, internal/tool/grep.go, internal/tool/ls.go, internal/tool/read.go, internal/tool/write.go
Definition struct gains a Schema field (map[string]any) to carry custom tool input schemas. All built-in tools explicitly set Schema: nil in their Definition() returns.
Extension Tool Metadata with Input Schemas
internal/extension/types.go, internal/extension/manager.go, internal/extension/manager_test.go
extension.Tool adds InputSchema for tool inputs. luaRegisterTool reads an optional input schema argument via luaOptionalSchema; tests assert InputSchema is preserved.
Registry Duplicate Detection & Extension Adapter
internal/tool/registry.go, internal/tool/registry_test.go, internal/tool/extension.go
Adds exported ErrDuplicateTool, Registry.Register() to reject duplicate names, ExtensionToolRunner interface and ExtensionExecutor adapter, and Registry.RegisterExtensions() for registering extension tools. Tests cover duplicate registration and registry metadata.
Request-Aware Tool Schema Generation
internal/assistant/tool_schema.go
responseTools and openAIChatTools accept *CompletionRequest and prefer registry-specific definitions; toolParameterSchema now accepts *tool.Definition, clones provided schemas, or falls back to built-in schema factories and freeform schemas.
Tool Registry Field & Request Threading
internal/assistant/client.go, internal/assistant/tool_registry.go, internal/assistant/runtime.go
CompletionRequest gains ToolRegistry *tool.Registry (json:"-"). Adds newToolRegistry factory with nil-safe provider handling and wires registry creation into modelResponse and request construction.
Tool Loop Execution Refactored for Registry
internal/assistant/tool_loop.go
executeToolCalls signature now accepts registry *tool.Registry and constructs a registry from cwd only when registry is nil. Tests add newToolRegistryForTest helper.
Anthropic Tool Execution & Schema
internal/assistant/anthropic.go, internal/assistant/anthropic_internal_test.go
executeAnthropicToolCalls passes request.ToolRegistry to executeToolCalls; anthropicTools uses requestToolDefinitions(request) and toolParameterSchema(&definition) for JSON input schemas; removes now-unused tool import; tests updated to initialize ToolRegistry: nil.
OpenAI Chat & Responses Integration
internal/assistant/openai_chat.go, internal/assistant/openai_responses.go
executeOpenAIChatToolCalls and completeResponsesLoop now supply request.ToolRegistry to executeToolCalls. openAIChatPayload and responsesPayload build tools using request-aware helpers (openAIChatTools(request), responseTools(request)).
Tool Loop Test Helpers & Registry Setup
internal/assistant/tool_loop_test.go
Adds newToolRegistryForTest helper to centralize temp-registry creation; updates existing tool-loop test to use the helper.
Extension Tool Registration & Execution Tests
internal/assistant/tool_registry_test.go, internal/assistant/tool_registry_internal_test.go, internal/assistant/tool_schema_internal_test.go, internal/tool/extension_test.go
Adds tests validating extension tool registration into the runtime registry, execution via adapter, duplicate-name rejection, schema visibility/cloning behavior, and fallback freeform schemas. Includes test completion client, Lua source helpers, and schema assertion helpers.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • omarluq/librecode#30: Modifies executeToolCalls signature in multiple callers; related to threading additional context into tool calls.
  • omarluq/librecode#39: Also modifies Anthropic tool handling and tool schema generation; overlaps with request-aware schema changes here.
  • omarluq/librecode#34: Updates Anthropic tool wiring in anthropic.go; touches similar payload-generation code paths.

🐰 Hop-hop, the tools do grow,
Extension schemas in a row,
Registry threads through the flow,
Request-aware, now tools bestow! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive No description was provided by the author, but the PR objectives clearly document the intent and scope of the changes. Consider adding a pull request description that outlines the main objectives and context, even if brief, to help reviewers understand the motivation and scope.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: unifying the extension registry to route both built-in and extension tools through a single registry.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/unified-tool-registry

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented May 23, 2026

Codecov Report

❌ Patch coverage is 83.03571% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.35%. Comparing base (ec4b5d5) to head (a907e17).

Files with missing lines Patch % Lines
internal/assistant/tool_schema.go 80.76% 4 Missing and 1 partial ⚠️
internal/tool/extension.go 82.14% 5 Missing ⚠️
internal/assistant/tool_registry.go 75.00% 2 Missing and 2 partials ⚠️
internal/assistant/openai_responses.go 0.00% 2 Missing ⚠️
internal/tool/registry.go 80.00% 2 Missing ⚠️
internal/extension/manager.go 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #43      +/-   ##
==========================================
+ Coverage   60.16%   60.35%   +0.18%     
==========================================
  Files         168      170       +2     
  Lines       16665    16746      +81     
==========================================
+ Hits        10027    10107      +80     
+ Misses       5628     5627       -1     
- Partials     1010     1012       +2     
Flag Coverage Δ
unittests 60.35% <83.03%> (+0.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/extension/manager_test.go (1)

567-571: ⚡ Quick win

Add a regression case for non-empty input_schema registration.

This assertion only covers the default empty-map path. Please add a test case where lc.register_tool passes a schema table and assert it is preserved in extension.Tool.InputSchema.

As per coding guidelines, "Prefer table-driven tests for core behavior and regression tests for terminal rendering bugs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/extension/manager_test.go` around lines 567 - 571, Add a regression
test that exercises lc.register_tool with a non-empty schema and asserts the
registered tool preserves it: create a second subtest (or table entry) that
calls lc.register_tool passing a non-empty map (e.g., {"foo":"string"}), then
inspect the resulting tools slice (same variable used in the file, e.g.,
tools[0] or the returned tool) and assert extension.Tool.InputSchema equals the
original map; update the test around the existing InputSchema empty-map
assertion to include this new case so the registration path that sets
extension.Tool.InputSchema is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/assistant/tool_loop.go`:
- Around line 32-42: The nil-registry fallback in executeToolCalls currently
calls tool.NewRegistry("") which loses the request working-directory; change
executeToolCalls signature to accept a cwd string (e.g., add cwd string
parameter) and when registry == nil construct registry = tool.NewRegistry(cwd)
so relative-tool execution preserves the request CWD; update every call site
that invokes executeToolCalls to supply the request.CWD value (or an empty
string if not present) so callers propagate the correct working directory into
tool.NewRegistry.

In `@internal/assistant/tool_registry.go`:
- Around line 17-21: newToolRegistry currently only checks provider == nil but
can receive a typed-nil (non-nil interface) which causes provider.Tools() to
panic; before calling provider.Tools() or registry.RegisterExtensions(provider,
...), detect and treat a typed-nil as nil (for example by using
reflect.ValueOf(provider).IsValid() and reflect.ValueOf(provider).IsNil()) and
return the empty registry like the provider == nil branch; ensure the guard is
placed just before the provider.Tools() call so registry.RegisterExtensions and
provider.Tools() are never invoked on a typed-nil provider.

In `@internal/tool/extension.go`:
- Around line 42-46: The Execute method of ExtensionExecutor returns errors from
executor.runner.ExecuteTool directly; import "github.com/samber/oops" into
internal/tool/extension.go and wrap the returned error using oops with domain
"tool" and code "execute_extension_tool" (include the extension tool name, e.g.
executor.definition.Name) before returning; keep the existing emptyToolResult()
return on error but replace err with the wrapped oops error so callers receive
oops-wrapped context for Execute, referencing ExtensionExecutor.Execute,
executor.runner.ExecuteTool and emptyToolResult().

---

Nitpick comments:
In `@internal/extension/manager_test.go`:
- Around line 567-571: Add a regression test that exercises lc.register_tool
with a non-empty schema and asserts the registered tool preserves it: create a
second subtest (or table entry) that calls lc.register_tool passing a non-empty
map (e.g., {"foo":"string"}), then inspect the resulting tools slice (same
variable used in the file, e.g., tools[0] or the returned tool) and assert
extension.Tool.InputSchema equals the original map; update the test around the
existing InputSchema empty-map assertion to include this new case so the
registration path that sets extension.Tool.InputSchema is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2808fd6b-2296-4914-92bc-b5080647f58a

📥 Commits

Reviewing files that changed from the base of the PR and between ec4b5d5 and cb8ec94.

📒 Files selected for processing (25)
  • internal/assistant/anthropic.go
  • internal/assistant/anthropic_internal_test.go
  • internal/assistant/client.go
  • internal/assistant/openai_chat.go
  • internal/assistant/openai_responses.go
  • internal/assistant/runtime.go
  • internal/assistant/tool_loop.go
  • internal/assistant/tool_loop_test.go
  • internal/assistant/tool_registry.go
  • internal/assistant/tool_registry_test.go
  • internal/assistant/tool_schema.go
  • internal/extension/manager.go
  • internal/extension/manager_test.go
  • internal/extension/types.go
  • internal/tool/bash.go
  • internal/tool/edit.go
  • internal/tool/extension.go
  • internal/tool/find.go
  • internal/tool/grep.go
  • internal/tool/ls.go
  • internal/tool/read.go
  • internal/tool/registry.go
  • internal/tool/registry_test.go
  • internal/tool/types.go
  • internal/tool/write.go

Comment thread internal/assistant/tool_loop.go
Comment thread internal/assistant/tool_registry.go Outdated
Comment thread internal/tool/extension.go
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
internal/tool/extension_test.go (1)

57-64: ⚡ Quick win

Assert result.Details is nil in the error path.

The failing branch returns early after text/error checks, so detail-clearing behavior is not currently verified.

Diff suggestion
 			if testCase.expectedErr != "" {
 				require.Error(t, err)
 				assert.Contains(t, err.Error(), testCase.expectedErr)
 				oopsErr, ok := oops.AsOops(err)
 				require.True(t, ok)
 				assert.Equal(t, testCase.expectedCode, oopsErr.Code())
 				assert.Empty(t, result.Text())
+				assert.Nil(t, result.Details)
 				return
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tool/extension_test.go` around lines 57 - 64, The test's error path
currently verifies error text/code and that result.Text() is empty but doesn't
assert that result.Details is cleared; update the error branch in the test where
testCase.expectedErr != "" (the block checking require.Error, assert.Contains,
oops.AsOops, assert.Equal, assert.Empty) to also assert that result.Details() is
nil (or empty) before the early return so the detail-clearing behavior is
verified.
internal/assistant/tool_schema_internal_test.go (1)

59-64: ⚡ Quick win

Strengthen clone verification with a nested-mutation assertion.

Current checks only prove top-level map cloning; nested properties aliasing could still regress undetected.

Diff suggestion
 		assertion: func(t *testing.T, schema map[string]any) {
 			t.Helper()
 			assert.Equal(t, customSchema, schema)
 			schema["mutated"] = true
 			assert.NotContains(t, customSchema, "mutated")
+
+			schemaProps, ok := schema[jsonPropertiesKey].(map[string]any)
+			require.True(t, ok)
+			schemaProps["message"] = map[string]any{jsonTypeKey: "number"}
+
+			origProps, ok := customSchema[jsonPropertiesKey].(map[string]any)
+			require.True(t, ok)
+			assert.Equal(t, map[string]any{jsonTypeKey: "string"}, origProps["message"])
 		},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/assistant/tool_schema_internal_test.go` around lines 59 - 64, The
test currently mutates only the top-level map to prove cloning; add a
nested-mutation assertion to ensure deep cloning of the "properties" map: inside
the assertion func (the anonymous func using t.Helper(), customSchema and
schema) assert that schema["properties"] is a map[string]any (or
map[string]interface{}) then mutate a field within that nested map (e.g., set
nested["foo"] = "changed") and assert that the same nested key in
customSchema["properties"] was not changed; this ensures functions that produce
the schema perform a deep copy rather than only a shallow/top-level clone.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/assistant/tool_schema_internal_test.go`:
- Around line 59-64: The test currently mutates only the top-level map to prove
cloning; add a nested-mutation assertion to ensure deep cloning of the
"properties" map: inside the assertion func (the anonymous func using
t.Helper(), customSchema and schema) assert that schema["properties"] is a
map[string]any (or map[string]interface{}) then mutate a field within that
nested map (e.g., set nested["foo"] = "changed") and assert that the same nested
key in customSchema["properties"] was not changed; this ensures functions that
produce the schema perform a deep copy rather than only a shallow/top-level
clone.

In `@internal/tool/extension_test.go`:
- Around line 57-64: The test's error path currently verifies error text/code
and that result.Text() is empty but doesn't assert that result.Details is
cleared; update the error branch in the test where testCase.expectedErr != ""
(the block checking require.Error, assert.Contains, oops.AsOops, assert.Equal,
assert.Empty) to also assert that result.Details() is nil (or empty) before the
early return so the detail-clearing behavior is verified.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1e9ceabc-f7b3-4b7f-9e00-e7be33a5e1a1

📥 Commits

Reviewing files that changed from the base of the PR and between cb8ec94 and a907e17.

📒 Files selected for processing (12)
  • internal/assistant/anthropic.go
  • internal/assistant/openai_chat.go
  • internal/assistant/openai_responses.go
  • internal/assistant/tool_loop.go
  • internal/assistant/tool_loop_test.go
  • internal/assistant/tool_registry.go
  • internal/assistant/tool_registry_internal_test.go
  • internal/assistant/tool_schema_internal_test.go
  • internal/extension/manager_test.go
  • internal/tool/extension.go
  • internal/tool/extension_test.go
  • internal/tool/registry_test.go

@omarluq omarluq merged commit bde61ca into main May 23, 2026
13 checks passed
@omarluq omarluq deleted the feat/unified-tool-registry branch May 23, 2026 04:40
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.

2 participants