-
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
Open
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
d248c2c
feat(agent-tool-set): port ai-tool-set to @openrouter/agent-tool-set
mattapperson 1514491
refactor(agent-tool-set): rename InferUIToolSet to InferToolSet
mattapperson b4e76ad
fix(agent, agent-tool-set): handle ServerToolBase in activeTools filt…
mattapperson 65b63c3
fix(agent-tool-set): preserve server tools and thread TShared generic
mattapperson ad99f89
chore(agent-tool-set): prepare initial package release
LukasParke d8fcf88
feat(agent): preserve tool names in typed events
LukasParke 8bbddee
feat(agent-tool-set): typed partition state machine and situation sna…
LukasParke d605156
fix(agent-tool-set): tighten typed snapshot integration
LukasParke a3b886b
fix(agent-tool-set): skip nonexistent e2e suite
LukasParke 529255d
docs(agent-tool-set): clarify active tool coupling
LukasParke 2c595c4
fix(agent): don't collapse tool event unions for generic readonly Tool[]
LukasParke 1da5ccc
style(agent): format wide event type tests
LukasParke 617f67f
fix(agent): omit tools key instead of sending empty array when active…
LukasParke 0c7235e
docs(changeset): add agent tool set API example
LukasParke 5e73382
fix(agent): keep legacy ServerToolBase/toolName shapes source-compatible
LukasParke ada9ebc
fix(agent): strip tool-set metadata from model requests
LukasParke 4a6a4a2
fix(agent-tool-set): include conditional ids in disabled type
LukasParke 7c3fabd
fix(agent-tool-set): make statusByTool exhaustive for __proto__ IDs
LukasParke 3b0c2d5
fix(agent-tool-set): sound FilterToolsByIds fallback for dynamic tool…
LukasParke b180de4
fix(agent): include runtime error payload in correlated tool.result t…
LukasParke f1739e9
fix(agent-tool-set): widen ServerToolIdOf to string for erased custom…
LukasParke 10cf412
fix(agent-tool-set): sound partition/situation types for mutable Tool…
LukasParke c8f5405
fix(agent): preserve async events in correlated streams
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,34 @@ | ||
| --- | ||
| "@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'; | ||
|
|
||
| const listOrders = tool({ | ||
| name: 'list_orders', | ||
| inputSchema: z.object({}), | ||
| execute: async () => ({ 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 | Exact snapshot | | ||
| | Named static situation (`enabled`/`disabled` only) | Exact filtered tool tuple | Exact snapshot | | ||
| | `activateWhen` / `deactivateWhen` / situation `conditional` | Upper bound (`enabled ∪ conditional`) | Exact snapshot after predicates | | ||
| | Mutable `ToolSet` | Partition types may widen | Exact snapshot | | ||
|
|
||
| The type system cannot execute predicates. Conditional IDs therefore expand the compile-time upper bound of active tools; after `resolve`, the returned arrays and `statusByTool` are always exhaustive and exact. | ||
|
|
||
| ## 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: /* active tools, construction order, concrete types */; | ||
| 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", | ||
| "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,39 @@ | ||
| 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, | ||
| 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.