feat: OpenAI and Anthropic tool-format adapters with middleware (#55, #50, #40)#69
Open
dgenio wants to merge 1 commit into
Open
feat: OpenAI and Anthropic tool-format adapters with middleware (#55, #50, #40)#69dgenio wants to merge 1 commit into
dgenio wants to merge 1 commit into
Conversation
…50, #40) Adds `agent_kernel.adapters` with two drop-in middleware classes that translate Capability objects into vendor tool schemas, route tool calls through the full kernel pipeline (grant → invoke → firewall → trace), and return vendor-shaped tool-result objects. Both share a `BaseToolMiddleware` that owns hook registration, error-as-result conversion, and the canonical Frame → JSON payload shape. OpenAIMiddleware emits Responses-API tools by default (also supports Chat Completions via `format=chat_completions`), with dotted capability IDs mapped to `namespace__function` form and OpenAI `strict` mode opt-in via `Capability.tool_hints`. AnthropicMiddleware emits Anthropic Messages tools with optional `cache_control` (per-capability or middleware default) and preserves dotted capability IDs. Both auto-detect Chat/Responses shape on input regardless of configured output format. Capability gains three optional fields: `parameters_model` (pydantic model used for JSON-Schema generation and input validation), `parameters_schema` (raw JSON Schema escape hatch), and `tool_hints` (ToolHints — vendor flags). All default to None, preserving backward compat. Kernel gains a small `list_capabilities()` accessor. Adds `pydantic>=2` as a runtime dep (justified by the new adapters; only used inside the adapters package). No `openai` / `anthropic` SDK dependency — every adapter function is a pure dict transform. PolicyDenied, CapabilityNotFound, DriverError, argument-validation failures, and hook abort signals all surface as tool-result errors rather than raised exceptions so the LLM can react. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces a new agent_kernel.adapters package providing OpenAI and Anthropic “tool-format” adapters plus middleware that routes vendor tool calls through the kernel’s full pipeline (grant → invoke → firewall → trace), with schema generation/validation support via Pydantic.
Changes:
- Added OpenAI + Anthropic adapter modules and a shared
BaseToolMiddleware(hooks, dispatch, vendor-shape formatting, schema helpers). - Extended
Capabilitywith optionalparameters_model,parameters_schema, andtool_hints(ToolHints) to drive tool schemas and optional strict/cache settings. - Added
Kernel.list_capabilities()and updated docs/tests/changelog and runtime deps (pydantic>=2).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_adapters.py | New test suite covering schema conversion, middleware flow, hooks, aborts, and error-as-result behavior. |
| src/agent_kernel/models.py | Adds ToolHints and new optional Capability fields for adapter schema/validation/hints. |
| src/agent_kernel/kernel.py | Adds Kernel.list_capabilities() to enumerate registered capabilities. |
| src/agent_kernel/adapters/_base.py | New shared middleware base, hook/event types, schema helpers, payload helpers, namespace helpers. |
| src/agent_kernel/adapters/openai.py | New OpenAI tool schema conversion + middleware supporting Responses + Chat Completions formats. |
| src/agent_kernel/adapters/anthropic.py | New Anthropic tool schema conversion + middleware with optional cache_control. |
| src/agent_kernel/adapters/init.py | Public exports for adapter layer. |
| src/agent_kernel/init.py | Re-exports middlewares and ToolHints at top level. |
| pyproject.toml | Adds runtime dependency on pydantic>=2. |
| docs/integrations.md | Adds “LLM tool-format adapters” documentation and usage examples. |
| docs/architecture.md | Documents adapters as an architecture component. |
| AGENTS.md | Updates minimal dependency list to include pydantic. |
| CHANGELOG.md | Adds [Unreleased] entries describing the new adapter feature set and dependency change. |
Comments suppressed due to low confidence (2)
src/agent_kernel/adapters/openai.py:197
- Same as above:
_parse_argumentsraisesValueErrorfor invalid argument types/JSON. For consistency with the repo’s error-contract rule inAGENTS.md, map these parse failures to a customAgentKernelErrorsubclass so callers can reliably catch agent-kernel errors (and so exception types are part of the contract).
if not isinstance(raw, str):
raise ValueError(
f"OpenAI tool_call 'arguments' must be a JSON string or dict, got {type(raw).__name__}."
)
src/agent_kernel/adapters/anthropic.py:128
- Same issue here: raising
ValueErrorfor non-dictinputviolates the repo’s “no bare ValueError to callers” rule. If you add a custom adapter parse/validation exception, use it consistently for all adapter-facing shape errors.
if raw_input is None:
raw_input = {}
if not isinstance(raw_input, dict):
raise ValueError(
f"Anthropic tool_use 'input' must be an object (got {type(raw_input).__name__})."
)
Comment on lines
+185
to
+196
| ``billing.list_invoices`` → ``billing__list_invoices``. The ``__`` separator | ||
| is reserved so the inverse mapping is unambiguous even when individual | ||
| segments contain underscores. | ||
| """ | ||
| return capability_id.replace(".", _NAMESPACE_SEP) | ||
|
|
||
|
|
||
| def restore_namespace(safe_name: str) -> str: | ||
| """Inverse of :func:`make_namespace_safe_name`. | ||
|
|
||
| ``billing__list_invoices`` → ``billing.list_invoices``. | ||
| """ |
Comment on lines
+175
to
+179
| if not isinstance(name, str) or not name: | ||
| raise ValueError( | ||
| "OpenAI tool_call is missing a function name. Expected either " | ||
| "'function.name' (Chat Completions) or 'name' (Responses API)." | ||
| ) |
Comment on lines
+273
to
+286
| Args: | ||
| tool_calls: Either ``response.output`` items from the Responses API | ||
| (filtered or unfiltered — non-function items are passed | ||
| through unchanged) or ``message.tool_calls`` items from the | ||
| Chat Completions API. Input shape is auto-detected per call. | ||
| justification: Justification applied to every call in the batch. | ||
| Individual calls may override by including | ||
| ``"_justification": "..."`` in their arguments. | ||
|
|
||
| Returns: | ||
| One vendor-shaped result envelope per *processed* tool call, in | ||
| input order. Non-tool-call items in the input are skipped so the | ||
| caller can stitch results back into the conversation as-is. | ||
| """ |
Comment on lines
+117
to
+121
| name = tool_use_block.get("name") | ||
| if not isinstance(name, str) or not name: | ||
| raise ValueError( | ||
| "Anthropic tool_use block is missing a 'name' field or it is not a string." | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Adds
agent_kernel.adapterswith two drop-in middleware classes that translateCapabilityobjects into vendor tool schemas, route tool calls through the full kernel pipeline (grant → invoke → firewall → trace), and return vendor-shaped tool-result objects.src/agent_kernel/adapters/__init__.pysrc/agent_kernel/adapters/_base.pyBaseToolMiddleware(hook registration + dispatch, request/grant/invoke flow, error-as-result),ToolCallEvent/ToolResultEventdataclasses, schema generation (build_input_schema,normalize_for_openai_strict,validate_input), namespace helpers, canonicalframe_to_payload/error_to_payload.src/agent_kernel/adapters/openai.pyOpenAIMiddleware,capabilities_to_tools,tool_call_to_request,format_result. Supports both Responses API and Chat Completions; auto-detects input shape; dotted IDs ↔namespace__function.src/agent_kernel/adapters/anthropic.pyAnthropicMiddleware,capabilities_to_toolswith per-capability and middleware-defaultcache_control,tool_use_to_request,format_result.src/agent_kernel/models.pyToolHintsdataclass and three optional fields onCapability:parameters_model(pydantic),parameters_schema(raw JSON Schema escape hatch),tool_hints(vendor flags). All default toNone.src/agent_kernel/kernel.pyKernel.list_capabilities()accessor (used by adapters; generally useful).src/agent_kernel/__init__.pyOpenAIMiddleware,AnthropicMiddleware,ToolHints.tests/test_adapters.pypyproject.tomlpydantic>=2runtime dependency.docs/integrations.mddocs/architecture.mdAGENTS.mdhttpx+pydantic.CHANGELOG.md[Unreleased]entries underAddedandChanged.Why
Closes #55, #50, #40. Developers using OpenAI / Anthropic APIs previously had to hand-translate between
Capabilityobjects and each vendor's tool schema and stitch the call/result loop throughgrant_capability/invokemanually. The new middleware eliminates that boilerplate while preserving every kernel invariant — every call still goes through grant + token + firewall + trace, and per-call tokens prevent cross-principal reuse (I-06).PolicyDenied,CapabilityNotFound,DriverError, argument-validation failures, and hook abort signals all surface as tool-result errors so the surrounding agent loop never crashes.Design decisions
Capability.parameters_model(pydantic) is the canonical input-schema source;parameters_schema(raw dict) is the escape hatch.allowed_fieldsis left alone — it remains an output redaction control consumed by the firewall and is deliberately not used to advertise input shape (which it was never designed to describe).format="chat_completions") on output. Default output is Responses API since issue OpenAI tool-format adapter & middleware #55 namesfunction_call_output.__(double underscore) so capability IDs with underscores in segments round-trip unambiguously. Anthropic preserves dotted IDs.ToolHints(strict=True). The adapter walks the pydantic-emitted schema and forces every object'srequired+additionalProperties: false. Falls back to non-strict with a warning if normalisation raises.justification(for WRITE/DESTRUCTIVE policy), or setaborted=True. Post-hooks observe (or replace) the resultingFrame. Hook exceptions during pre-invoke become tool errors; exceptions during post-invoke are logged but never crash the batch.handle_tool_calls(..., justification="")batch parameter, (2) per-call override viaargs["_justification"], (3) hook-injected viaevent.justification. The simplest path works for READ tools; WRITE tools can supply justification via any of the three.How verified
(Local pytest baseline before this PR: 287 tests; after: 364, +77. New tests live entirely in
tests/test_adapters.py. Total project coverage improved from 95% → 96%.)Scope notes (Mode B)
pydantic>=2. Justified by adapters usingmodel_json_schema()+model_validate(). UpdatedAGENTS.mdso future readers see the new dep set ishttpx + pydantic.Capabilitymodel extensions: three new fields, all defaultNone. No existing fixture or test required updates. Backward-compatible._base.py438 lines,openai.py351,anthropic.py268. The first two exceed AGENTS.md's 300-line guideline. The existing repo has three modules in the same boat (policy.py520,policy_dsl.py503,kernel.py466 — see [policy/kernel] Tech debt: decompose policy_dsl.py and broaden dry-run driver test coverage #68). I chose not to split because the helpers and middleware are tightly coupled (every middleware method calls into the helpers); splitting introduces import gymnastics without clarifying semantic boundaries. Happy to factor_base.pyinto_base.py+_helpers.pyif you'd prefer.Capability.parameters_modelstyle change to require pydantic everywhere; SDK-typed return values (theopenai/anthropicpackages would only buy IDE autocomplete that consumers can get themselves).Risks
model_json_schema()may emit Draft 2020-12 features OpenAI strict mode doesn't accept. Thenormalize_for_openai_strictwalker handles the two common issues (additionalProperties,required). For schemas with$ref/$defs, OpenAI strict accepts those — we leave them alone. If a user supplies a schema feature that breaks normalisation, the adapter falls back to non-strict with a warning rather than raising.kernel.invokeraises aDriverError(after the kernel's own driver fallback), the call becomes a tool error. This matches the rest of the codebase — drivers handle retry internally where appropriate.intercept_*is called from one thread while a batch is dispatching from another, ordering is unspecified. The expected pattern is per-principal middleware instances, hooks registered at setup time — matching the issue body'sOpenAIMiddleware(kernel, principal)shape.AI agent instruction files reviewed
AGENTS.md— updated dep set; no convention changes.docs/agent-context/invariants.md— no change needed; adapters consumeFramepost-firewall and route throughkernel.invoke, so I-01, I-02, I-06 remain enforced by existing code paths.docs/agent-context/review-checklist.md,lessons-learned.md,workflows.md— no change needed..github/copilot-instructions.md,.claude/CLAUDE.md— no change needed.Checklist
make cipasses (fmt → lint → mypy strict → pytest → examples)capability,principal,grant,Framethroughout (seedocs/integrations.md)Capabilityfields default toNone; no existing test required updatesCHANGELOG.mdupdated under[Unreleased]docs/architecture.md,docs/integrations.md,AGENTS.md) in the same PR🤖 Generated with Claude Code