-
Notifications
You must be signed in to change notification settings - Fork 12
feat(agent-tool-set) #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
ad1c356
feat(agent-tool-set): port ai-tool-set to @openrouter/agent-tool-set
mattapperson b489fcd
refactor(agent-tool-set): rename InferUIToolSet to InferToolSet
mattapperson 77ddb38
fix(agent, agent-tool-set): handle ServerToolBase in activeTools filt…
mattapperson 01ed155
fix(agent-tool-set): preserve server tools and thread TShared generic
mattapperson 53b85be
chore(agent-tool-set): prepare initial package release
LukasParke 21b8078
feat(agent): preserve tool names in typed events
LukasParke 0af78f9
feat(agent-tool-set): typed partition state machine and situation sna…
LukasParke dd97fc6
fix(agent-tool-set): tighten typed snapshot integration
LukasParke 7bcdf25
fix(agent-tool-set): skip nonexistent e2e suite
LukasParke 78f823f
docs(agent-tool-set): clarify active tool coupling
LukasParke 294a7f7
fix(agent): don't collapse tool event unions for generic readonly Tool[]
LukasParke 6b23d45
style(agent): format wide event type tests
LukasParke 6081b25
fix(agent): omit tools key instead of sending empty array when active…
LukasParke d7b9686
docs(changeset): add agent tool set API example
LukasParke cf03c09
fix(agent): keep legacy ServerToolBase/toolName shapes source-compatible
LukasParke a322f2d
fix(agent): strip tool-set metadata from model requests
LukasParke 9b7dde5
fix(agent-tool-set): include conditional ids in disabled type
LukasParke f4be2a5
fix(agent-tool-set): make statusByTool exhaustive for __proto__ IDs
LukasParke 8e9555c
fix(agent-tool-set): sound FilterToolsByIds fallback for dynamic tool…
LukasParke 2b54a37
fix(agent): include runtime error payload in correlated tool.result t…
LukasParke 0f0efba
fix(agent-tool-set): widen ServerToolIdOf to string for erased custom…
LukasParke 9d17e04
fix(agent-tool-set): sound partition/situation types for mutable Tool…
LukasParke c275cc5
fix(agent): preserve async events in correlated streams
LukasParke b45e982
fix(agent): align active tool request handling
LukasParke 6a50aa1
fix(agent): preserve unified tool names
LukasParke b310bf6
fix(agent): cover built-in task events and situation rules
LukasParke f903a08
fix(agent): scope tool-set request metadata
LukasParke bbda34f
fix(toolkits): address server and shared tool identities
LukasParke e96ac0f
fix(agent): clarify shared tool name inference
LukasParke 6c6b2ee
fix(tool-set): make conditional snapshots type-safe
LukasParke 578dd9e
fix(tool-set): preserve statically enabled resolved tools
LukasParke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| --- | ||
| "@openrouter/agent-tool-set": minor | ||
| "@openrouter/agent": minor | ||
| --- | ||
|
|
||
| Add `@openrouter/agent-tool-set` (port of ai-tool-set v1.0.0, MIT © Chris Cook): declarative activate / deactivate / activateWhen / deactivateWhen for tools with state- and context-aware predicates. Integrates with a new `activeTools?: readonly string[]` option on `callModel` that filters which tools are sent to the model for a given call. | ||
|
|
||
| ```ts | ||
| import { callModel, OpenRouter, serverTool, tool } from '@openrouter/agent'; | ||
| import { createToolSet } from '@openrouter/agent-tool-set'; | ||
| import { z } from 'zod/v4'; | ||
|
|
||
| type AppContext = { accountId: string }; | ||
|
|
||
| // Curried form preserves the literal name for correlated tool event types. | ||
| const listOrders = tool<AppContext>()({ | ||
| name: 'list_orders', | ||
| inputSchema: z.object({}), | ||
| execute: async (_params, ctx) => ({ accountId: ctx?.shared.accountId, orders: [] }), | ||
| }); | ||
| // override the default `server:${type}` id | ||
| const search = serverTool({ type: 'web_search_2025_08_26' }, { id: 'public_search' }); | ||
|
|
||
| const toolSet = createToolSet({ tools: [listOrders, search] as const }).deactivate( | ||
| 'list_orders', | ||
| ); | ||
|
|
||
| const client = new OpenRouter({ apiKey: process.env['OPENROUTER_API_KEY'] }); | ||
| const resolved = toolSet.resolve(); | ||
|
|
||
| // resolved.callModel is `{ tools, activeTools }` — spread it straight in | ||
| const result = callModel(client, { | ||
| model: 'openai/gpt-4o-mini', | ||
| input: 'Search for OpenRouter pricing.', | ||
| ...resolved.callModel, | ||
| }); | ||
| ``` | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| # @openrouter/agent-tool-set | ||
|
|
||
| Declarative, state-aware activation and deactivation for tools used with `@openrouter/agent`. | ||
|
|
||
| Port of [`ai-tool-set`](https://github.com/zirkelc/ai-tool-set) (MIT © Chris Cook), adapted for this SDK's ordered `Tool[]` / `callModel` model. See [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md). | ||
|
|
||
| ## What it adds | ||
|
|
||
| - **Stable tool-set IDs** for every addressable tool: | ||
| - client tools → `function.name` | ||
| - server tools → `server:${config.type}` by default (overridable via `serverTool(config, { id })`) | ||
| - A **typed three-way partition** of those IDs: definitely enabled, definitely disabled, conditional. | ||
| - **Exhaustive runtime snapshots** from `resolve()` / `resolveSituation()` — every ID appears in `statusByTool`. | ||
| - **Named declarative situations** with compile-time exact tool tuples when the situation is fully static. | ||
| - Integration with `callModel`'s `activeTools` option via the snapshot's spread-safe `.callModel` input. | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| pnpm add @openrouter/agent-tool-set | ||
| ``` | ||
|
|
||
| ## Usage | ||
|
|
||
| ```ts | ||
| import { OpenRouter, tool, serverTool, callModel } from '@openrouter/agent'; | ||
| import { | ||
| createToolSet, | ||
| type InferEnabledIds, | ||
| type InferDisabledIds, | ||
| type InferConditionalIds, | ||
| type InferAllIds, | ||
| } from '@openrouter/agent-tool-set'; | ||
| import { z } from 'zod/v4'; | ||
|
|
||
| type AppContext = { | ||
| isAuthenticated: boolean; | ||
| isAdmin: boolean; | ||
| }; | ||
|
|
||
| const listOrders = tool({ | ||
| name: 'list_orders', | ||
| inputSchema: z.object({}), | ||
| execute: async () => ({ orders: [] }), | ||
| }); | ||
|
|
||
| const cancelOrder = tool({ | ||
| name: 'cancel_order', | ||
| inputSchema: z.object({ id: z.string() }), | ||
| execute: async () => ({ ok: true }), | ||
| }); | ||
|
|
||
| const login = tool({ | ||
| name: 'login', | ||
| inputSchema: z.object({}), | ||
| execute: async () => ({ token: '…' }), | ||
| }); | ||
|
|
||
| const webSearch = serverTool({ type: 'web_search_2025_08_26' }); | ||
| // id defaults to 'server:web_search_2025_08_26' | ||
|
|
||
| const allTools = [listOrders, cancelOrder, login, webSearch] as const; | ||
|
|
||
| const toolSet = createToolSet<typeof allTools, AppContext>({ tools: allTools }) | ||
| .deactivate('cancel_order') | ||
| .activateWhen('list_orders', ({ context }) => context?.isAuthenticated === true) | ||
| .defineSituations({ | ||
| guest: { | ||
| enabled: ['login', 'server:web_search_2025_08_26'], | ||
| disabled: ['list_orders', 'cancel_order'], | ||
| }, | ||
| authenticated: { | ||
| enabled: ['list_orders', 'server:web_search_2025_08_26'], | ||
| disabled: ['login'], | ||
| conditional: { | ||
| cancel_order: ({ context }) => context?.isAdmin === true, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| // Compile-time partition of the *base* set (before a situation overlay): | ||
| type All = InferAllIds<typeof toolSet>; | ||
| // 'list_orders' | 'cancel_order' | 'login' | 'server:web_search_2025_08_26' | ||
| type Enabled = InferEnabledIds<typeof toolSet>; // excludes cancel_order + list_orders (conditional) | ||
| type Disabled = InferDisabledIds<typeof toolSet>; // 'cancel_order' | ||
| type Conditional = InferConditionalIds<typeof toolSet>; // 'list_orders' | ||
|
|
||
| const client = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY }); | ||
|
|
||
| // Named static situation → exact tool tuple at compile time | ||
| const guest = toolSet.resolveSituation('guest'); | ||
| // guest.tools is exactly [login, webSearch] | ||
| // guest.enabled / guest.disabled / guest.statusByTool are exhaustive | ||
|
|
||
| const authenticated = toolSet.resolveSituation('authenticated', { | ||
| context: { isAuthenticated: true, isAdmin: false }, | ||
| }); | ||
|
|
||
| const result = callModel(client, { | ||
| model: 'openai/gpt-4o-mini', | ||
| input: 'List my orders.', | ||
| ...authenticated.callModel, | ||
| }); | ||
| ``` | ||
|
|
||
| ## Identity | ||
|
|
||
| | Kind | Tool-set ID | | ||
| | --- | --- | | ||
| | Client `tool({ name: 'x' })` | `'x'` | | ||
| | `serverTool({ type: 'web_search_2025_08_26' })` | `'server:web_search_2025_08_26'` | | ||
| | `serverTool(config, { id: 'server:public_search' })` | `'server:public_search'` | | ||
|
|
||
| Duplicate IDs throw at `createToolSet` construction. Activation methods accept only known IDs. | ||
|
|
||
| ## Compile-time vs runtime exactness | ||
|
|
||
| | Resolution style | Developer-time knowledge | Runtime knowledge | | ||
| | --- | --- | --- | | ||
| | Static `activate` / `deactivate` | Exact partition and filtered `tools` tuple | Exact snapshot | | ||
| | Named static situation (`enabled`/`disabled` only) | Exact partition and filtered `tools` tuple | Exact snapshot | | ||
| | `activateWhen` / `deactivateWhen` / situation `conditional` | `tools` is a readonly array of possible active members; length and positions are not exact | Exact snapshot after predicates | | ||
| | Mutable `ToolSet` | Widened partition; `tools` is a readonly array of possible active members | Exact snapshot | | ||
|
|
||
| The type system cannot execute predicates. If any IDs are conditional, `snapshot.tools` and `snapshot.callModel.tools` are arrays whose member union is limited to the active upper bound, but their length and positions remain unknown. Static-only partitions retain exact filtered tuples. At runtime, all snapshot arrays and `statusByTool` reflect the resolved predicates exactly. | ||
|
|
||
| ## API | ||
|
|
||
| ### `createToolSet<T, TShared?>({ tools, mutable? })` | ||
|
|
||
| Build a set from an ordered tool array. Optional `TShared` types the `context` argument on predicates. Defaults to immutable. | ||
|
|
||
| ### `.tools` | ||
|
|
||
| Concrete tools tuple in construction order (client + server), regardless of activation. | ||
|
|
||
| ### `.activate(id | id[])` / `.deactivate(id | id[])` | ||
|
|
||
| Static flip (last-call-wins). Accepts client names **and** server IDs. Updates the compile-time partition. | ||
|
|
||
| ### `.activateWhen(id, predicate)` / `.activateWhen({ [id]: predicate })` | ||
|
|
||
| Conditional activation — defaults inactive, becomes active when predicate returns `true`. Moves the ID into the conditional partition. | ||
|
|
||
| ### `.deactivateWhen(id, predicate)` / `.deactivateWhen({ [id]: predicate })` | ||
|
|
||
| Conditional deactivation — defaults active, becomes inactive when predicate returns `true`. Also moves the ID into the conditional partition. | ||
|
|
||
| Predicate input: `{ state?: ConversationState; context?: TShared }`. | ||
|
|
||
| ### `.defineSituations({ [name]: config })` | ||
|
|
||
| Declarative named situations. Each config may include: | ||
|
|
||
| - `enabled?: readonly Id[]` — statically on | ||
| - `disabled?: readonly Id[]` — statically off | ||
| - `conditional?: { [id]: predicate | { mode?, predicate } }` — runtime rules | ||
|
|
||
| Situation overlays the base partition for every ID it mentions; unmentioned IDs keep the base state. Unknown, duplicate, or conflicting IDs within one situation throw. | ||
|
|
||
| ### `.resolve(input?)` → snapshot | ||
|
|
||
| ```ts | ||
| { | ||
| tools: /* exact tuple when static; possible-member array when conditional */; | ||
| activeTools: /* active *client* names for callModel */; | ||
| callModel: { tools, activeTools }; // safe to spread into callModel() | ||
| enabled: /* every active ID (client + server) */; | ||
| disabled: /* every inactive ID */; | ||
| statusByTool: { | ||
| [id]: { | ||
| enabled: boolean; | ||
| reason: 'default' | 'activate' | 'deactivate' | 'activateWhen' | 'deactivateWhen' | 'situation'; | ||
| directive?: 'activate' | 'deactivate' | 'activateWhen' | 'deactivateWhen'; | ||
| predicate?: boolean; // true when a runtime predicate decided the result | ||
| }; | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| ### `.resolveSituation(name, input?)` → snapshot | ||
|
|
||
| Same shape as `resolve`, with the named situation overlay applied first. | ||
|
|
||
| ### `.inferTools(input?)` | ||
|
|
||
| Back-compat alias for `resolve`. Prefer `resolve` in new code. | ||
|
|
||
| ### `.clone({ mutable? })` | ||
|
|
||
| Copy state, optionally flipping mode. | ||
|
|
||
| ### Inference utilities | ||
|
|
||
| ```ts | ||
| type All = InferAllIds<typeof toolSet>; | ||
| type Enabled = InferEnabledIds<typeof toolSet>; | ||
| type Disabled = InferDisabledIds<typeof toolSet>; | ||
| type Conditional = InferConditionalIds<typeof toolSet>; | ||
| ``` | ||
|
|
||
| ### `InferToolSet<TTools>` | ||
|
|
||
| Alias of the agent's `CorrelatedToolEventUnion<TTools>` — name-correlated preliminary/result stream events based on a tools tuple. | ||
|
|
||
| ## Notes | ||
|
|
||
| - Immutable by default (every mutator returns a new `ToolSet` with refined partition types). | ||
| - `mutable: true` mutates in place. Partition type parameters may widen for soundness; runtime state is still exact. | ||
| - Last-call-wins: each directive on a given ID replaces any prior one for that ID. | ||
| - Server tools participate fully in activation once they have an ID. When active they appear in `tools` (and `enabled` / `statusByTool`) but **not** in `activeTools`, which remains the client-name list expected by `callModel`. | ||
| - Keep a snapshot's `tools` and `activeTools` together by spreading `.callModel`; `callModel` cannot verify `activeTools` against an unrelated tools array. | ||
| - `callModel` ignores names in `activeTools` that are not present in `tools`. Tool-set snapshots avoid stale names by deriving both arrays from the same set. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Third-Party Notices | ||
|
|
||
| `@openrouter/agent-tool-set` is adapted from [`ai-tool-set` v1.0.0](https://github.com/zirkelc/ai-tool-set/tree/v1.0.0), which is licensed under the MIT License: | ||
|
|
||
| > MIT License | ||
| > | ||
| > Copyright (c) 2024 Chris | ||
| > | ||
| > Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| > of this software and associated documentation files (the "Software"), to deal | ||
| > in the Software without restriction, including without limitation the rights | ||
| > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| > copies of the Software, and to permit persons to whom the Software is | ||
| > furnished to do so, subject to the following conditions: | ||
| > | ||
| > The above copyright notice and this permission notice shall be included in all | ||
| > copies or substantial portions of the Software. | ||
| > | ||
| > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| > SOFTWARE. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| { | ||
| "name": "@openrouter/agent-tool-set", | ||
| "version": "0.0.0", | ||
| "author": "OpenRouter", | ||
| "description": "Declarative activation/deactivation for @openrouter/agent tools. Port of ai-tool-set (MIT © Chris Cook) adapted for callModel + tool().", | ||
| "keywords": [ | ||
| "openrouter", | ||
| "agent", | ||
| "tools", | ||
| "toolset", | ||
| "typescript", | ||
| "ai" | ||
| ], | ||
| "license": "Apache-2.0", | ||
| "type": "module", | ||
| "main": "./esm/index.js", | ||
| "exports": { | ||
| ".": { | ||
| "types": "./esm/index.d.ts", | ||
| "default": "./esm/index.js" | ||
| }, | ||
| "./package.json": "./package.json" | ||
| }, | ||
| "sideEffects": false, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/OpenRouterTeam/typescript-agent.git", | ||
| "directory": "packages/agent-tool-set" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "provenance": true | ||
| }, | ||
| "files": [ | ||
| "esm", | ||
| "package.json", | ||
| "README.md", | ||
| "THIRD_PARTY_NOTICES.md" | ||
| ], | ||
| "scripts": { | ||
| "lint": "biome check src tests", | ||
| "lint:fix": "biome check --write src tests", | ||
| "build": "tsc", | ||
| "test": "vitest --run --project unit", | ||
| "test:watch": "vitest --watch --project unit", | ||
| "typecheck": "tsc --noEmit -p tsconfig.typecheck.json", | ||
| "compile": "tsc" | ||
| }, | ||
| "dependencies": { | ||
| "@openrouter/agent": "workspace:*" | ||
| }, | ||
| "peerDependencies": { | ||
| "zod": "^4.0.0" | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| export { createToolSet, ToolSet } from './tool-set.js'; | ||
| export type { | ||
| ActivatePartition, | ||
| ActivationInput, | ||
| ActivationPredicate, | ||
| ApplySituationPartition, | ||
| ClientToolName, | ||
| ClientToolNamesOfTuple, | ||
| ConditionalPartition, | ||
| DeactivatePartition, | ||
| EmptyPartition, | ||
| EmptySituations, | ||
| FilterToolsByIds, | ||
| InferAllIds, | ||
| InferConditionalIds, | ||
| InferDisabledIds, | ||
| InferEnabledIds, | ||
| InferSituationEntry, | ||
| InferSituationMap, | ||
| InferToolSet, | ||
| InitialPartition, | ||
| Partition, | ||
| ResolvedToolSnapshot, | ||
| ResolvedTools, | ||
| ServerToolIdOf, | ||
| ServerToolIdsOfTuple, | ||
| SituationConditionalRule, | ||
| SituationConfig, | ||
| SituationMap, | ||
| SituationNames, | ||
| StatusByToolMap, | ||
| StatusReason, | ||
| ToolById, | ||
| ToolIdOf, | ||
| ToolIdsOfTuple, | ||
| ToolSetLike, | ||
| ToolStatusEntry, | ||
| WidenedPartition, | ||
| WidenedSituationMap, | ||
| } from './types.js'; |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.