Skip to content

fix(agent): infer tool context types from contextSchema#65

Merged
LukasParke merged 3 commits into
mainfrom
fix/context-schema-inference
Jul 17, 2026
Merged

fix(agent): infer tool context types from contextSchema#65
LukasParke merged 3 commits into
mainfrom
fix/context-schema-inference

Conversation

@LukasParke

@LukasParke LukasParke commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Problem

contextSchema's Zod type never reached execute's context or callModel's context map: ctx typed as ToolExecuteContext<Name, Record<string, unknown>>, and InferToolContext saw only the erased $ZodObject<$ZodShape> → map slots resolved to never → every consumer needed ctx.local as X and context: map as any. Typed tool context is a headline feature; the types didn't help. (agent-monorepo pr-review FRICTION #2 — 2 casts; wikillm — 1.)

Fix (types-only)

  • tool() overloads carry a TCtx generic so the concrete schema type survives to the returned tool's function.contextSchema
  • ctx.local is now zodInfer<TCtx> inside execute; ToolContextMap slots resolve to the real per-tool shape (and reject wrong shapes)
  • Tools without a contextSchema keep Record<string, never> exactly as before

Type-mechanics worth reviewing

Preserving concrete types exposed strict-function-types contravariance against the wide Tool union (previously masked by full erasure). Restored assignability with the standard patterns:

  • contextSchema is readonly (covariant TCtx)
  • execute/onToolCalled/onResponseReceived use method syntax; ToModelOutputFunction/NextTurnParamsFunctions/ToolApprovalCheck use the bivarianceHack pattern — params positions check bivariantly, which erased types implicitly got before
  • One sanctioned construction-time write cast for the readonly field in tool() (runtime unchanged)

Typecheck coverage note

pnpm typecheck now includes the new type test file. Discovered while wiring: the existing .test-d.ts files were never actually typechecked (vitest typecheck config gap), and several fail tsc against the current @openrouter/sdk (pre-existing drift, e.g. OutputServerToolItem renamed). Only the new file is included, with a tsconfig note; fixing the others is out of scope here.

Tests

tests/unit/context-schema-inference.test-d.ts — ctx.local inference, map accept/reject (incl. missing-key + per-slot in mixed arrays), no-schema unchanged, manual tools, wide-union assignability. Verified the gate goes red on a deliberate type error. Suite: 30 files / 315 tests, build + biome green.

Changeset: patch. Plan: agent-monorepo planning/sdk-fix-plan.md PR-3.


Open in Devin Review

tool() now preserves the concrete contextSchema type through its
overloads: execute's ctx.local is typed from the schema, and
callModel's context map slots accept/reject the real per-tool shape.
No more 'ctx.local as X' or 'context: map as any' in consumers.

Type-mechanics notes:
- BaseToolFunction.contextSchema is readonly so TCtx stays covariant;
  concrete tools remain assignable to the wide Tool union.
- execute/onToolCalled/onResponseReceived use method syntax, and
  ToModelOutputFunction/NextTurnParamsFunctions/ToolApprovalCheck use
  the bivarianceHack pattern: params positions check bivariantly, which
  the fully-erased types previously got implicitly.
- InferToolContext distinguishes the wide default schema type from
  concrete schemas, so no-context tools keep Record<string, never>.
- pnpm typecheck now includes the new type test (other .test-d.ts files
  have pre-existing SDK-version drift; excluded with a note).

Types-only: zero runtime behavior change (one sanctioned construction-
time write cast for the readonly field).

315 tests green; type gates verified red/green.

@devin-ai-integration devin-ai-integration Bot left a comment

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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@perry-the-pr-reviewer perry-the-pr-reviewer 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.

Perry's Review

Verdict: 💬 Comments / questions
Risk: 🟢 Low

Types-only change that makes contextSchema inference work end-to-end. CI is green across all checks (lint, typecheck, unit, e2e). The bivariance hack pattern is the standard TypeScript technique for restoring assignability after preserving concrete generic types, and the type test file pins all the important consumer scenarios.

Findings

Suggestion: WithConcreteContextSchema is a no-op intersection

WithConcreteContextSchema<TCtx> always resolves to {}, so the & WithConcreteContextSchema<TCtx> intersections on ToolWithExecute, ToolWithGenerator, ManualTool, and HITLTool add no constraint. The concrete schema type is already preserved through BaseToolFunction<TInput, TCtx>.readonly contextSchema. The type is marked @deprecated, but the 4 intersection sites still reference it — they could be removed to reduce surface area. Low priority; not blocking.

What's good

  • The TCtx generic threading through tool() overloads, config types, and return types is clean. The concrete schema type survives the overload boundary as intended.
  • ContextFromSchema (for ctx.local in execute) vs. InferToolContext (for ToolContextMap slots) correctly diverge on the wide-default case: execute context gets Record<string, unknown> (read/write any key), map gets Record<string, never> (no context values accepted). This matches the semantic difference between "the tool's runtime context" and "what callers must provide."
  • Method-syntax / bivarianceHack pattern is applied consistently across all parametric function types that need it (execute, onToolCalled, onResponseReceived, ToModelOutputFunction, NextTurnParamsFunctions, ToolApprovalCheck).
  • The tsconfig.typecheck.json correctly extends the base config and adds only the new test file, with an honest note about the pre-existing drift in other type-level test files.
  • Type test coverage is excellent: inference acceptance, wrong-shape rejection, missing-key rejection, no-schema unchanged, mixed arrays, manual tools, and wide-union assignability.

Comment thread packages/agent/src/lib/tool-types.ts Outdated
* `InferToolContext` distinguishes the wide default from concrete schemas.
* Kept as an alias for source compatibility.
*/
export type WithConcreteContextSchema<_TCtx extends $ZodObject<$ZodShape>> =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Suggestion: WithConcreteContextSchema<TCtx> always resolves to {}, so the & WithConcreteContextSchema<TCtx> intersections on ToolWithExecute (line 389), ToolWithGenerator (line 405), ManualTool (line 418), and HITLTool (line 433) are no-ops. The concrete TCtx is already preserved through BaseToolFunction<TInput, TCtx>.readonly contextSchema.

Consider removing the type and the 4 intersection sites to reduce dead surface area. Low priority — not blocking.

▶ Prompt for agents

In packages/agent/src/lib/tool-types.ts, WithConcreteContextSchema<TCtx> (line 72) always resolves to {}. Remove the type definition and the 4 `& WithConcreteContextSchema<TCtx>` intersections on ToolWithExecute, ToolWithGenerator, ManualTool, and HITLTool. Run `pnpm --filter @openrouter/agent typecheck` and `pnpm --filter @openrouter/agent test` to verify the types still resolve correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 6a53336 — removed WithConcreteContextSchema and the 4 intersection sites (plus the stale comment reference in tool.ts). Also merged main (hooks system #7). Typecheck, lint, build, and unit suite (505) green.

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 1 new potential issue.

Open in Devin Review

"test:e2e": "vitest --run --project e2e",
"test:watch": "vitest --watch --project unit",
"typecheck": "tsc --noEmit",
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json",

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.

🔍 typecheck now covers only one test-d file; others still undrifted

The typecheck script now points at tsconfig.typecheck.json (packages/agent/package.json:119), which includes src/**/*.ts plus only tests/unit/context-schema-inference.test-d.ts. Previously tsc --noEmit used tsconfig.json which includes only src, so tests were never typechecked at all — the new config strictly increases coverage for the one file. However, the other .test-d.ts files remain excluded from tsc and (per the PR description) fail against the current @openrouter/sdk. This is acknowledged as out of scope, but reviewers should track that these type tests are still not gated, so future type regressions in them will go unnoticed.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@LukasParke
LukasParke merged commit 09a041e into main Jul 17, 2026
6 checks passed
@LukasParke
LukasParke deleted the fix/context-schema-inference branch July 17, 2026 15: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.

1 participant