Skip to content
Open
Show file tree
Hide file tree
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 Apr 20, 2026
1514491
refactor(agent-tool-set): rename InferUIToolSet to InferToolSet
mattapperson Apr 20, 2026
b4e76ad
fix(agent, agent-tool-set): handle ServerToolBase in activeTools filt…
mattapperson Apr 21, 2026
65b63c3
fix(agent-tool-set): preserve server tools and thread TShared generic
mattapperson Apr 21, 2026
ad99f89
chore(agent-tool-set): prepare initial package release
LukasParke Jul 22, 2026
d8fcf88
feat(agent): preserve tool names in typed events
LukasParke Jul 22, 2026
8bbddee
feat(agent-tool-set): typed partition state machine and situation sna…
LukasParke Jul 22, 2026
d605156
fix(agent-tool-set): tighten typed snapshot integration
LukasParke Jul 22, 2026
a3b886b
fix(agent-tool-set): skip nonexistent e2e suite
LukasParke Jul 23, 2026
529255d
docs(agent-tool-set): clarify active tool coupling
LukasParke Jul 23, 2026
2c595c4
fix(agent): don't collapse tool event unions for generic readonly Tool[]
LukasParke Aug 6, 2026
1da5ccc
style(agent): format wide event type tests
LukasParke Aug 6, 2026
617f67f
fix(agent): omit tools key instead of sending empty array when active…
LukasParke Aug 6, 2026
0c7235e
docs(changeset): add agent tool set API example
LukasParke Aug 6, 2026
5e73382
fix(agent): keep legacy ServerToolBase/toolName shapes source-compatible
LukasParke Aug 6, 2026
ada9ebc
fix(agent): strip tool-set metadata from model requests
LukasParke Aug 6, 2026
4a6a4a2
fix(agent-tool-set): include conditional ids in disabled type
LukasParke Aug 6, 2026
7c3fabd
fix(agent-tool-set): make statusByTool exhaustive for __proto__ IDs
LukasParke Aug 7, 2026
3b0c2d5
fix(agent-tool-set): sound FilterToolsByIds fallback for dynamic tool…
LukasParke Aug 7, 2026
b180de4
fix(agent): include runtime error payload in correlated tool.result t…
LukasParke Aug 7, 2026
f1739e9
fix(agent-tool-set): widen ServerToolIdOf to string for erased custom…
LukasParke Aug 7, 2026
10cf412
fix(agent-tool-set): sound partition/situation types for mutable Tool…
LukasParke Aug 7, 2026
c8f5405
fix(agent): preserve async events in correlated streams
LukasParke Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/agent-tool-set.md
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.
Comment thread
LukasParke marked this conversation as resolved.

```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,
});
```
213 changes: 213 additions & 0 deletions packages/agent-tool-set/README.md
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.
25 changes: 25 additions & 0 deletions packages/agent-tool-set/THIRD_PARTY_NOTICES.md
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.
55 changes: 55 additions & 0 deletions packages/agent-tool-set/package.json
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"
}
}
39 changes: 39 additions & 0 deletions packages/agent-tool-set/src/index.ts
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';
Loading
Loading