Skip to content

feat(extension): harden provider and tool runtime seams#52

Merged
omarluq merged 2 commits into
mainfrom
feat/provider-tool-hardening
May 28, 2026
Merged

feat(extension): harden provider and tool runtime seams#52
omarluq merged 2 commits into
mainfrom
feat/provider-tool-hardening

Conversation

@omarluq

@omarluq omarluq commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • add lifecycle diagnostics for provider and tool hooks
  • dispatch tool_error through lifecycle handlers
  • document provider/tool seam hardening behavior

Validation

  • mise exec -- task ci

Note: docs/refactoring/* remain local and are intentionally excluded.

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Lifecycle diagnostic events now emitted for provider requests and tool execution, improving observability of extension hook execution and performance.
  • Documentation

    • Extension API and roadmap updated with current lifecycle event specs, runtime hook behavior, and concrete Lua handler examples for argument normalization and result redaction.
  • Bug Fixes

    • Corrected handling of empty headers in provider request mutations.
  • Tests

    • Added and expanded tests to validate lifecycle diagnostics and hook error/error-reporting behavior.

Walkthrough

This PR adds structured lifecycle diagnostics and refactors tool/provider lifecycle dispatch to use a shared dispatch path that emits diagnostics; it includes diagnostic helpers, updates to extension docs and roadmap, accepts empty-header provider mutations, and adds tests verifying diagnostics for success, noop, and error cases.

Changes

Tool and Provider Lifecycle Diagnostics

Layer / File(s) Summary
Documentation of tool lifecycle hooks
docs/extension-api.md, docs/extension-roadmap.md
"Agent lifecycle seams" now describe current runtime contracts; lifecycle events table includes tool_call, tool_result, tool_error; Lua examples show tool_call argument mutation and tool_result output redaction; Phase 6.1 updated to detail the implemented hook contract.
Lifecycle diagnostics infrastructure
internal/assistant/lifecycle_diagnostics.go
New emitLifecycleDiagnostics builds/emits diagnostic payloads (hook count, duration ms, optional errors) and provides helpers for provider/tool diagnostics and change detection between before/after maps.
Tool lifecycle dispatch with diagnostics
internal/assistant/lifecycle.go
dispatchToolCallLifecycle, dispatchToolResultLifecycle, and dispatchToolErrorLifecycle route through shared runtime.dispatchLifecycle, apply returned mutations, and emit lifecycle diagnostics.
Provider hook wiring and tests support
internal/assistant/provider_hooks.go, internal/assistant/provider_hooks_internal_test.go
dispatchProviderRequestHook assembles output and emits provider diagnostics; test runtime initialization updated to install an events bus and constants added for tests.
Provider hook diagnostics tests
internal/assistant/provider_diagnostics_internal_test.go
Tests added/extended to assert diagnostics for mutating hooks, noop handlers, and handler errors (including lifecycleErrorsKey).
Test infrastructure and constants
internal/assistant/runtime_events_test.go, internal/assistant/tool_diagnostics_test.go
Shared test constants (testToolPath, testToolCallID, testToolResult) introduced; helpers collectAssistantDiagnostics and collectRuntimeDiagnosticPayloads capture diagnostic envelopes from the runtime event bus.
Tool lifecycle diagnostics tests
internal/assistant/tool_diagnostics_test.go, internal/assistant/tool_lifecycle_test.go
New and updated tests verify diagnostic payloads when handlers mutate/err and verify hook_count behavior when handlers are absent.
Test constant usage in existing tests
internal/assistant/runtime_events_test.go, internal/assistant/runtime_lifecycle_test.go, internal/assistant/tool_lifecycle_test.go
Existing tests updated to use new shared constants for tool payload fields.
Extension validation for empty headers
internal/extension/lifecycle.go
providerRequestMutationFromLua now accepts (and returns) an empty headers map as a valid mutation result.

Possibly Related PRs

  • omarluq/librecode#48: Modifies the same tool lifecycle dispatch flow in internal/assistant/lifecycle.go.
  • omarluq/librecode#45: Touches provider lifecycle wiring and providerRequestMutationFromLua parsing/handling.
  • omarluq/librecode#30: Related changes to assistant runtime lifecycle dispatch and lifecycle emission paths.

"A rabbit hops through runtime logs at night,
Counting hooks and whispers, timing each light,
Tool calls rewrite, results gently pared,
Diagnostics bloom where handlers dared.
Hooray — the runtime listens soft and bright!" 🐇✨

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(extension): harden provider and tool runtime seams' clearly summarizes the main change: hardening the provider and tool runtime interfaces through lifecycle diagnostics and error handling improvements.
Description check ✅ Passed The description is directly related to the changeset, accurately summarizing the three main additions: lifecycle diagnostics for hooks, tool_error dispatch through lifecycle handlers, and documentation updates for the seam hardening behavior.
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/provider-tool-hardening

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

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.25843% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.83%. Comparing base (3ca4845) to head (577c1b9).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
internal/assistant/lifecycle.go 76.47% 2 Missing and 2 partials ⚠️
internal/assistant/lifecycle_diagnostics.go 96.72% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #52      +/-   ##
==========================================
+ Coverage   61.65%   61.83%   +0.17%     
==========================================
  Files         173      174       +1     
  Lines       17186    17250      +64     
==========================================
+ Hits        10596    10666      +70     
+ Misses       5535     5531       -4     
+ Partials     1055     1053       -2     
Flag Coverage Δ
unittests 61.83% <93.25%> (+0.17%) ⬆️

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/assistant/provider_hooks.go (1)

84-89: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve provider diagnostics on dispatch failures.

On Line 84-89, returning before diagnostics means before_provider_request_diagnostic is never emitted when handlers fail, so hook_errors/timing/hook_count are dropped in the exact path where diagnostics are most needed.

Suggested fix
 result, err := runtime.dispatchLifecycle(ctx, extension.LifecycleBeforeProviderRequest, payload)
-if err != nil {
-	return providerHookOutput{}, oops.In("assistant").
-		Code("before_provider_request_dispatch_failed").
-		Wrapf(err, "dispatch before_provider_request lifecycle")
-}
+// Build a best-effort output for diagnostics even when dispatch fails.
+output := providerHookOutput{
+	Payload: providerPayloadFromLifecycle(result.Payload, input.Payload),
+	Headers: mergeProviderHeaders(input.Headers, result.ProviderRequest.Headers),
+}
+runtime.emitLifecycleDiagnostics(
+	ctx,
+	extension.LifecycleBeforeProviderRequest,
+	&result,
+	providerHookDiagnostics(input, output),
+)
+if err != nil {
+	return providerHookOutput{}, oops.In("assistant").
+		Code("before_provider_request_dispatch_failed").
+		Wrapf(err, "dispatch before_provider_request lifecycle")
+}
 ...
-output := providerHookOutput{
-	Payload: providerPayloadFromLifecycle(result.Payload, input.Payload),
-	Headers: mergeProviderHeaders(input.Headers, result.ProviderRequest.Headers),
-}
-runtime.emitLifecycleDiagnostics(...)
 return output, nil

Also applies to: 100-107

🤖 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/provider_hooks.go` around lines 84 - 89, The dispatch
error path for runtime.dispatchLifecycle (when called with
extension.LifecycleBeforeProviderRequest) currently returns early and drops the
diagnostics; update the error handling around dispatchLifecycle so that when an
error occurs you still emit the before_provider_request_diagnostic (including
hook_errors, timing, and hook_count) before returning the providerHookOutput{}
error (i.e., capture diagnostics from the failed handler invocation and call the
same diagnostic emission path used on success), and apply the same change to the
analogous after-provider dispatch block (the second dispatchLifecycle call
around lines 100-107) so both before and after provider request failures
preserve and emit diagnostics.
🤖 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/lifecycle.go`:
- Around line 237-240: When runtime.dispatchLifecycle(ctx,
extension.LifecycleToolCall, payload) returns both a non-nil result and an err,
ensure you call emitLifecycleDiagnostics with that result before returning the
error so hook_errors get sent; update the three call sites around
dispatchLifecycle (the lines using result, err :=
runtime.dispatchLifecycle(...)) to check for err and, if result != nil, call
emitLifecycleDiagnostics(ctx, result) (or the existing emitLifecycleDiagnostics
helper) and only then return the err. Also ensure the same pattern is applied to
the other two locations noted (the blocks at the other dispatchLifecycle calls)
so diagnostics are always emitted on failure paths.

---

Outside diff comments:
In `@internal/assistant/provider_hooks.go`:
- Around line 84-89: The dispatch error path for runtime.dispatchLifecycle (when
called with extension.LifecycleBeforeProviderRequest) currently returns early
and drops the diagnostics; update the error handling around dispatchLifecycle so
that when an error occurs you still emit the before_provider_request_diagnostic
(including hook_errors, timing, and hook_count) before returning the
providerHookOutput{} error (i.e., capture diagnostics from the failed handler
invocation and call the same diagnostic emission path used on success), and
apply the same change to the analogous after-provider dispatch block (the second
dispatchLifecycle call around lines 100-107) so both before and after provider
request failures preserve and emit diagnostics.
🪄 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: ac1b81f9-135b-45f0-b20b-8a85acf77378

📥 Commits

Reviewing files that changed from the base of the PR and between 96a5271 and 394185e.

📒 Files selected for processing (12)
  • docs/extension-api.md
  • docs/extension-roadmap.md
  • internal/assistant/lifecycle.go
  • internal/assistant/lifecycle_diagnostics.go
  • internal/assistant/provider_diagnostics_internal_test.go
  • internal/assistant/provider_hooks.go
  • internal/assistant/provider_hooks_internal_test.go
  • internal/assistant/runtime_events_test.go
  • internal/assistant/runtime_lifecycle_test.go
  • internal/assistant/tool_diagnostics_test.go
  • internal/assistant/tool_lifecycle_test.go
  • internal/extension/lifecycle.go

Comment thread internal/assistant/lifecycle.go
@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
internal/assistant/provider_diagnostics_internal_test.go (1)

15-114: ⚡ Quick win

Refactor the provider diagnostics tests into a table-driven suite.
internal/assistant/provider_diagnostics_internal_test.go has three standalone tests with largely repeated runtime setup + diagnostics subscription + dispatch; they differ mainly by the Lua handler and expected assertions. Consolidate into one table-driven test with subtests for success, noop handler, and handler error (reusing the existing collectAssistantDiagnostics helper).

♻️ Suggested refactor shape
func TestRuntime_ProviderRequestHookEmitsDiagnostics(t *testing.T) {
  t.Parallel()

  tests := []struct{
    name string
    lua string
    input providerHookInput
    assertFn func(t *testing.T, events *[]map[string]any, result *providerHookResult, err error)
    expectErr bool
  }{
    // success / noop / handler error
  }

  for _, tt := range tests {
    tt := tt
    t.Run(tt.name, func(t *testing.T) {
      runtime := newProviderHookTestRuntime(t, tt.lua)
      events := collectAssistantDiagnostics(t, runtime.EventBus(), string(extension.LifecycleBeforeProviderRequest)+"_diagnostic")

      result, err := runtime.dispatchProviderRequestHook(context.Background(), tt.input)
      if tt.expectErr { require.Error(t, err) } else { require.NoError(t, err) }

      tt.assertFn(t, events, result, err)
    })
  }
}
🤖 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/provider_diagnostics_internal_test.go` around lines 15 -
114, Consolidate the three standalone tests into a single table-driven test
(keep the existing TestRuntime_ProviderRequestHookEmitsDiagnostics name) by
creating a tests slice describing name, lua (handler), input
(providerHookInput), expectErr bool and assertFn
func(t,*[]map[string]any,*providerHookResult,error); for each case create
runtime via newProviderHookTestRuntime, subscribe with
collectAssistantDiagnostics(runtime.EventBus(),
string(extension.LifecycleBeforeProviderRequest)+"_diagnostic"), call
runtime.dispatchProviderRequestHook and t.Run the subtest (remember tt := tt to
capture loop variable), then check err with expectErr and invoke tt.assertFn to
perform the existing per-case assertions (success, noop, handler error) so you
reuse dispatchProviderRequestHook, collectAssistantDiagnostics and the original
assertions in subtests.
internal/assistant/tool_lifecycle_test.go (1)

205-208: ⚡ Quick win

Assert diagnostic values, not just key presence.

This currently passes even if hook_errors is present but empty/malformed. Please also assert hook_count == 1 and hook_errors is non-empty for both diagnostics.

Suggested assertion hardening
 	require.Len(t, *toolCallDiagnostics, 1)
 	require.Contains(t, (*toolCallDiagnostics)[0], "hook_errors")
+	assert.Equal(t, 1, (*toolCallDiagnostics)[0]["hook_count"])
+	require.NotEmpty(t, (*toolCallDiagnostics)[0]["hook_errors"])
 	require.Len(t, *toolResultDiagnostics, 1)
 	require.Contains(t, (*toolResultDiagnostics)[0], "hook_errors")
+	assert.Equal(t, 1, (*toolResultDiagnostics)[0]["hook_count"])
+	require.NotEmpty(t, (*toolResultDiagnostics)[0]["hook_errors"])
🤖 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_lifecycle_test.go` around lines 205 - 208, The tests
only assert the presence of the "hook_errors" key; update the assertions on
toolCallDiagnostics and toolResultDiagnostics to validate the diagnostic values
as well: after confirming length and key presence for the slices referenced by
toolCallDiagnostics and toolResultDiagnostics, parse or assert the
JSON/structure contains "hook_count == 1" and that "hook_errors" is non-empty
(e.g., len > 0 or non-empty string/array) for both diagnostics entries; use the
same require.* helpers already imported to fail the test if hook_count is not 1
or hook_errors is empty for the entries in toolCallDiagnostics and
toolResultDiagnostics.
🤖 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/provider_diagnostics_internal_test.go`:
- Around line 15-114: Consolidate the three standalone tests into a single
table-driven test (keep the existing
TestRuntime_ProviderRequestHookEmitsDiagnostics name) by creating a tests slice
describing name, lua (handler), input (providerHookInput), expectErr bool and
assertFn func(t,*[]map[string]any,*providerHookResult,error); for each case
create runtime via newProviderHookTestRuntime, subscribe with
collectAssistantDiagnostics(runtime.EventBus(),
string(extension.LifecycleBeforeProviderRequest)+"_diagnostic"), call
runtime.dispatchProviderRequestHook and t.Run the subtest (remember tt := tt to
capture loop variable), then check err with expectErr and invoke tt.assertFn to
perform the existing per-case assertions (success, noop, handler error) so you
reuse dispatchProviderRequestHook, collectAssistantDiagnostics and the original
assertions in subtests.

In `@internal/assistant/tool_lifecycle_test.go`:
- Around line 205-208: The tests only assert the presence of the "hook_errors"
key; update the assertions on toolCallDiagnostics and toolResultDiagnostics to
validate the diagnostic values as well: after confirming length and key presence
for the slices referenced by toolCallDiagnostics and toolResultDiagnostics,
parse or assert the JSON/structure contains "hook_count == 1" and that
"hook_errors" is non-empty (e.g., len > 0 or non-empty string/array) for both
diagnostics entries; use the same require.* helpers already imported to fail the
test if hook_count is not 1 or hook_errors is empty for the entries in
toolCallDiagnostics and toolResultDiagnostics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 351780f8-a228-4520-a463-3b39d9a68c08

📥 Commits

Reviewing files that changed from the base of the PR and between 394185e and 577c1b9.

📒 Files selected for processing (5)
  • internal/assistant/lifecycle.go
  • internal/assistant/provider_diagnostics_internal_test.go
  • internal/assistant/provider_hooks.go
  • internal/assistant/provider_hooks_internal_test.go
  • internal/assistant/tool_lifecycle_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/assistant/provider_hooks_internal_test.go
  • internal/assistant/lifecycle.go

@omarluq
omarluq merged commit 595102d into main May 28, 2026
13 checks passed
@omarluq
omarluq deleted the feat/provider-tool-hardening branch May 28, 2026 21:55
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