@intx/agent 0.1.2 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/README.md +80 -5
- package/dist/agent.d.ts +87 -0
- package/dist/agent.js +638 -0
- package/dist/canonicalize.d.ts +15 -0
- package/dist/canonicalize.js +160 -0
- package/dist/default-director.d.ts +24 -0
- package/dist/default-director.js +45 -0
- package/dist/definition.d.ts +116 -0
- package/dist/definition.js +39 -0
- package/dist/director-registry.d.ts +38 -0
- package/dist/director-registry.js +73 -0
- package/dist/director-types.d.ts +80 -0
- package/dist/director-types.js +13 -0
- package/dist/director.d.ts +56 -0
- package/dist/director.js +92 -0
- package/dist/env-validation.d.ts +59 -0
- package/dist/env-validation.js +180 -0
- package/dist/env.d.ts +160 -0
- package/dist/env.js +53 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +23 -0
- package/dist/internal-fixtures/mail.d.ts +39 -0
- package/dist/internal-fixtures/mail.js +85 -0
- package/dist/internal-fixtures/planner.d.ts +19 -0
- package/dist/internal-fixtures/planner.js +49 -0
- package/dist/lock.d.ts +16 -0
- package/dist/lock.js +47 -0
- package/dist/namespace.d.ts +12 -0
- package/dist/namespace.js +39 -0
- package/dist/send-queue.d.ts +25 -0
- package/dist/send-queue.js +147 -0
- package/dist/source.d.ts +43 -0
- package/dist/source.js +118 -0
- package/dist/stream.d.ts +16 -0
- package/dist/stream.js +115 -0
- package/dist/testing/audit-noop.d.ts +7 -0
- package/dist/testing/audit-noop.js +25 -0
- package/dist/testing/authorize-allow.d.ts +8 -0
- package/dist/testing/authorize-allow.js +19 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +17 -0
- package/dist/tool.d.ts +182 -0
- package/dist/tool.js +215 -0
- package/package.json +25 -7
- package/src/agent.test.ts +0 -46
- package/src/agent.ts +0 -494
- package/src/index.ts +0 -38
- package/src/lock.test.ts +0 -93
- package/src/lock.ts +0 -57
- package/src/send-queue.test.ts +0 -207
- package/src/send-queue.ts +0 -200
- package/src/source.test.ts +0 -171
- package/src/source.ts +0 -93
- package/src/stream.test.ts +0 -167
- package/src/stream.ts +0 -142
- package/src/tool.test.ts +0 -217
- package/src/tool.ts +0 -148
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
package/dist/stream.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Bounded per-consumer fan-out for the agent's reactor event stream.
|
|
2
|
+
//
|
|
3
|
+
// Each call to `agent.stream()` creates a fresh `StreamConsumer`. The
|
|
4
|
+
// agent feeds every reactor event to every consumer; consumers buffer
|
|
5
|
+
// independently. If a consumer falls more than `maxBuffer` events behind
|
|
6
|
+
// it is poisoned with `StreamBackpressureError` and its iterator throws
|
|
7
|
+
// on the next read — the consumer is removed but other consumers keep
|
|
8
|
+
// running.
|
|
9
|
+
//
|
|
10
|
+
// Loud failure matches the defensive-coding rule: silently dropping
|
|
11
|
+
// events would hide consumer bugs, and unbounded buffering would let a
|
|
12
|
+
// stalled consumer balloon the agent's memory. The cap is configurable
|
|
13
|
+
// via `streamBufferMax` on `BaseEnv`.
|
|
14
|
+
export class StreamBackpressureError extends Error {
|
|
15
|
+
maxBuffer;
|
|
16
|
+
constructor(maxBuffer) {
|
|
17
|
+
super(`stream consumer fell more than ${String(maxBuffer)} events behind`);
|
|
18
|
+
this.name = "StreamBackpressureError";
|
|
19
|
+
this.maxBuffer = maxBuffer;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function createStreamConsumer(maxBuffer) {
|
|
23
|
+
if (maxBuffer < 1) {
|
|
24
|
+
throw new Error(`streamBufferMax must be >= 1, got ${String(maxBuffer)}`);
|
|
25
|
+
}
|
|
26
|
+
const buffer = [];
|
|
27
|
+
const waiters = [];
|
|
28
|
+
let overflow;
|
|
29
|
+
let done = false;
|
|
30
|
+
function settleOverflowedWaiters(err) {
|
|
31
|
+
while (waiters.length > 0) {
|
|
32
|
+
const w = waiters.shift();
|
|
33
|
+
if (w === undefined)
|
|
34
|
+
return;
|
|
35
|
+
w.reject(err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function settleDoneWaiters() {
|
|
39
|
+
while (waiters.length > 0) {
|
|
40
|
+
const w = waiters.shift();
|
|
41
|
+
if (w === undefined)
|
|
42
|
+
return;
|
|
43
|
+
w.resolve({ value: undefined, done: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function push(event) {
|
|
47
|
+
if (done || overflow !== undefined)
|
|
48
|
+
return;
|
|
49
|
+
if (waiters.length > 0) {
|
|
50
|
+
const w = waiters.shift();
|
|
51
|
+
if (w === undefined)
|
|
52
|
+
return;
|
|
53
|
+
w.resolve({ value: event, done: false });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (buffer.length >= maxBuffer) {
|
|
57
|
+
overflow = new StreamBackpressureError(maxBuffer);
|
|
58
|
+
settleOverflowedWaiters(overflow);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
buffer.push(event);
|
|
62
|
+
}
|
|
63
|
+
function close() {
|
|
64
|
+
if (done)
|
|
65
|
+
return;
|
|
66
|
+
done = true;
|
|
67
|
+
settleDoneWaiters();
|
|
68
|
+
}
|
|
69
|
+
function nextResult() {
|
|
70
|
+
if (overflow !== undefined) {
|
|
71
|
+
// Drain any buffered events before throwing so the caller sees
|
|
72
|
+
// every event up to the overflow point.
|
|
73
|
+
if (buffer.length > 0) {
|
|
74
|
+
const ev = buffer.shift();
|
|
75
|
+
if (ev !== undefined) {
|
|
76
|
+
return Promise.resolve({ value: ev, done: false });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return Promise.reject(overflow);
|
|
80
|
+
}
|
|
81
|
+
if (buffer.length > 0) {
|
|
82
|
+
const ev = buffer.shift();
|
|
83
|
+
if (ev !== undefined) {
|
|
84
|
+
return Promise.resolve({ value: ev, done: false });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (done) {
|
|
88
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
89
|
+
}
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
waiters.push({ resolve, reject });
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function iterator() {
|
|
95
|
+
const it = {
|
|
96
|
+
next: nextResult,
|
|
97
|
+
async return() {
|
|
98
|
+
close();
|
|
99
|
+
return { value: undefined, done: true };
|
|
100
|
+
},
|
|
101
|
+
[Symbol.asyncIterator]() {
|
|
102
|
+
return it;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
return it;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
push,
|
|
109
|
+
close,
|
|
110
|
+
get closed() {
|
|
111
|
+
return done || overflow !== undefined;
|
|
112
|
+
},
|
|
113
|
+
iterator,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AuditStore } from "@intx/types/runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Construct a no-op AuditStore. Each call returns a fresh object so
|
|
4
|
+
* tests that introspect the store identity (e.g. asserting two agents
|
|
5
|
+
* received different stores) can do so.
|
|
6
|
+
*/
|
|
7
|
+
export declare function noopAuditStore(): AuditStore;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// No-op AuditStore for tests and examples.
|
|
2
|
+
//
|
|
3
|
+
// Returns immediately for every commit; returns empty arrays for every
|
|
4
|
+
// load. Useful when the agent is exercised in tests that do not assert
|
|
5
|
+
// audit content, or in examples whose purpose is the agent surface
|
|
6
|
+
// rather than the audit ledger. Production callers must supply a real
|
|
7
|
+
// audit store.
|
|
8
|
+
/**
|
|
9
|
+
* Construct a no-op AuditStore. Each call returns a fresh object so
|
|
10
|
+
* tests that introspect the store identity (e.g. asserting two agents
|
|
11
|
+
* received different stores) can do so.
|
|
12
|
+
*/
|
|
13
|
+
export function noopAuditStore() {
|
|
14
|
+
return {
|
|
15
|
+
async commitAudit(_records) {
|
|
16
|
+
// No-op.
|
|
17
|
+
},
|
|
18
|
+
async commitErrors(_errors) {
|
|
19
|
+
// No-op.
|
|
20
|
+
},
|
|
21
|
+
async loadAudit(_sessionId) {
|
|
22
|
+
return [];
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AuthorizeFn } from "../env.js";
|
|
2
|
+
/**
|
|
3
|
+
* Construct a permissive AuthorizeFn that allows every call. The
|
|
4
|
+
* returned function ignores its arguments (including the third
|
|
5
|
+
* per-call context parameter) and returns the same shape the
|
|
6
|
+
* production authz extension expects.
|
|
7
|
+
*/
|
|
8
|
+
export declare function permissiveAuthorize(): AuthorizeFn;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Permissive AuthorizeFn for tests and examples.
|
|
2
|
+
//
|
|
3
|
+
// Returns { effect: "allow" } for every call. Useful when the agent is
|
|
4
|
+
// exercised without grants -- the test cares about the agent surface
|
|
5
|
+
// rather than the authz decision. Production callers must supply a real
|
|
6
|
+
// authorize function tied to actual policy.
|
|
7
|
+
/**
|
|
8
|
+
* Construct a permissive AuthorizeFn that allows every call. The
|
|
9
|
+
* returned function ignores its arguments (including the third
|
|
10
|
+
* per-call context parameter) and returns the same shape the
|
|
11
|
+
* production authz extension expects.
|
|
12
|
+
*/
|
|
13
|
+
export function permissiveAuthorize() {
|
|
14
|
+
return async (_resource, _action, _context) => ({
|
|
15
|
+
effect: "allow",
|
|
16
|
+
matchingGrants: [],
|
|
17
|
+
resolvedBy: null,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @intx/agent/testing -- no-op implementations of the env contract's
|
|
2
|
+
// required fields, for tests and examples.
|
|
3
|
+
//
|
|
4
|
+
// The exports here silently permit every authz decision and discard
|
|
5
|
+
// every audit record. They exist so test fixtures, the in-tree
|
|
6
|
+
// examples, and short-lived demos can satisfy `BaseEnv` without
|
|
7
|
+
// bringing in a real audit store or policy engine. Production
|
|
8
|
+
// deployments replace these with a real `AuditStore` (durably
|
|
9
|
+
// recording audit and error events) and a real `authorize` callback
|
|
10
|
+
// (gating tool calls per the deployment's policy). Importing from
|
|
11
|
+
// this subpath in production silently disables auditing and allows
|
|
12
|
+
// every tool call, which is almost never what a production caller
|
|
13
|
+
// actually wants -- treat the subpath the same way you would treat a
|
|
14
|
+
// hard-coded `() => true` permission check elsewhere in the
|
|
15
|
+
// codebase.
|
|
16
|
+
export { noopAuditStore } from "./audit-noop.js";
|
|
17
|
+
export { permissiveAuthorize } from "./authorize-allow.js";
|
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { ToolCall, ToolDefinition, ToolResult, ToolRunner } from "@intx/types/runtime";
|
|
2
|
+
import type { BaseEnv } from "./env.js";
|
|
3
|
+
export type ToolHandler = (call: ToolCall, signal: AbortSignal) => Promise<ToolResult>;
|
|
4
|
+
export type StringToolHandler = (args: Record<string, unknown>, signal: AbortSignal) => Promise<string>;
|
|
5
|
+
export type AgentTool = {
|
|
6
|
+
kind: "full";
|
|
7
|
+
definition: ToolDefinition;
|
|
8
|
+
handler: ToolHandler;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "string";
|
|
11
|
+
definition: ToolDefinition;
|
|
12
|
+
handler: StringToolHandler;
|
|
13
|
+
};
|
|
14
|
+
export declare function tool(args: {
|
|
15
|
+
definition: ToolDefinition;
|
|
16
|
+
handler: ToolHandler;
|
|
17
|
+
}): AgentTool;
|
|
18
|
+
export declare function stringTool(args: {
|
|
19
|
+
definition: ToolDefinition;
|
|
20
|
+
handler: StringToolHandler;
|
|
21
|
+
}): AgentTool;
|
|
22
|
+
/**
|
|
23
|
+
* Adapt a pre-built ToolRunner (e.g. the one returned by
|
|
24
|
+
* `createPosixTools`) into a list of AgentTools that can be passed to
|
|
25
|
+
* `createAgent({ tools })`. Each definition becomes a full-handler
|
|
26
|
+
* AgentTool that delegates to the runner's `run`.
|
|
27
|
+
*
|
|
28
|
+
* Use this when integrating tool packages whose public surface is a
|
|
29
|
+
* single ToolRunner rather than individual handlers.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fromToolRunner(runner: {
|
|
32
|
+
readonly definitions: readonly ToolDefinition[];
|
|
33
|
+
run: ToolRunner["run"];
|
|
34
|
+
}): AgentTool[];
|
|
35
|
+
export declare class DuplicateToolError extends Error {
|
|
36
|
+
readonly toolName: string;
|
|
37
|
+
constructor(toolName: string);
|
|
38
|
+
}
|
|
39
|
+
export type AgentToolRunner = ToolRunner & {
|
|
40
|
+
readonly definitions: readonly ToolDefinition[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* A bundle of tools constructed by an `AnnotatedToolFactory`. Exposes
|
|
44
|
+
* the set of tool definitions the model sees, a single dispatcher
|
|
45
|
+
* (`run`), and an optional disposer the caller invokes after the agent
|
|
46
|
+
* closes.
|
|
47
|
+
*
|
|
48
|
+
* Disposer ownership lives with the caller -- the env is the agent's
|
|
49
|
+
* dependency contract; the caller owns the lifetime of what it puts in
|
|
50
|
+
* env. The agent does not invoke `dispose` itself.
|
|
51
|
+
*/
|
|
52
|
+
export interface ToolBundle {
|
|
53
|
+
readonly definitions: readonly ToolDefinition[];
|
|
54
|
+
run(call: ToolCall, signal: AbortSignal): Promise<ToolResult>;
|
|
55
|
+
dispose?(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Factory function shape -- consumes an env extending `BaseEnv` and
|
|
59
|
+
* produces a `ToolBundle`. The factory is invoked once per agent
|
|
60
|
+
* instantiation.
|
|
61
|
+
*/
|
|
62
|
+
export type ToolFactory<EnvReq extends BaseEnv = BaseEnv> = (env: EnvReq) => ToolBundle;
|
|
63
|
+
/**
|
|
64
|
+
* Runtime metadata attached to a `ToolFactory` by `defineTool`. `id` is
|
|
65
|
+
* package-namespaced; `requires` enumerates env keys the factory touches
|
|
66
|
+
* beyond `BaseEnv`'s six core fields.
|
|
67
|
+
*/
|
|
68
|
+
export interface ToolFactoryMeta {
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly requires: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A tool factory carrying its runtime metadata. `defineTool` is the only
|
|
74
|
+
* sanctioned construction path.
|
|
75
|
+
*
|
|
76
|
+
* The intersection `ToolFactory<EnvReq> & ToolFactoryMeta` is the
|
|
77
|
+
* type-level surface; at runtime the meta fields are attached to the
|
|
78
|
+
* factory function via `Object.assign`.
|
|
79
|
+
*/
|
|
80
|
+
export type AnnotatedToolFactory<EnvReq extends BaseEnv = BaseEnv> = ToolFactory<EnvReq> & ToolFactoryMeta;
|
|
81
|
+
/**
|
|
82
|
+
* Define a tool bundle factory.
|
|
83
|
+
*
|
|
84
|
+
* - `id` must be package-namespaced ("@vendor/pkg/name" or
|
|
85
|
+
* "pkg/name"). Bare ids throw `Error` at definition time.
|
|
86
|
+
* - `requires` enumerates the env keys this factory touches beyond
|
|
87
|
+
* `BaseEnv`'s six core fields. The runtime `validateEnv` checks
|
|
88
|
+
* presence; the factory itself may also fail loud at construction
|
|
89
|
+
* if the env contents are structurally wrong.
|
|
90
|
+
* - `factory(env)` returns a `ToolBundle`. Invoked once per agent
|
|
91
|
+
* instantiation; the bundle's lifetime is tied to that agent.
|
|
92
|
+
*
|
|
93
|
+
* The returned object is the same callable as the supplied `factory`
|
|
94
|
+
* with `id` and a frozen `requires` array attached.
|
|
95
|
+
*/
|
|
96
|
+
export declare function defineTool<EnvReq extends BaseEnv = BaseEnv>(opts: {
|
|
97
|
+
id: string;
|
|
98
|
+
requires?: readonly string[];
|
|
99
|
+
factory: ToolFactory<EnvReq>;
|
|
100
|
+
}): AnnotatedToolFactory<EnvReq>;
|
|
101
|
+
/**
|
|
102
|
+
* A plugin contributes capabilities (extra tools, middleware, anything
|
|
103
|
+
* a host plugin protocol defines) without producing a `ToolBundle`
|
|
104
|
+
* itself. Plugins are first-class entries in an `interchange.tools`
|
|
105
|
+
* module alongside `AnnotatedToolFactory` exports.
|
|
106
|
+
*
|
|
107
|
+
* The shape the factory returns is host-defined: tool packages that
|
|
108
|
+
* accept plugins read `env.plugins` and dispatch by structural shape
|
|
109
|
+
* (or by an explicit kind marker the host agrees on). The agent
|
|
110
|
+
* runtime does not interpret plugin shapes; it only delivers them.
|
|
111
|
+
*
|
|
112
|
+
* The marker is a `Symbol.for`-registered key (PLUGIN_MARKER) so a
|
|
113
|
+
* duck-typed loader can separate plugins from `AnnotatedToolFactory`s
|
|
114
|
+
* without re-running `defineTool`/`definePlugin` against each export.
|
|
115
|
+
* Using a registered symbol (rather than a string key like `_plugin`)
|
|
116
|
+
* prevents third-party objects from accidentally satisfying the
|
|
117
|
+
* marker check by happening to have a property of the same name.
|
|
118
|
+
*/
|
|
119
|
+
export type PluginFactory<EnvReq extends BaseEnv, Result> = (env: EnvReq) => Result;
|
|
120
|
+
/**
|
|
121
|
+
* Registered symbol that tags `AnnotatedPluginFactory` values. Exported
|
|
122
|
+
* so consumers that need to introspect plugin factories directly can
|
|
123
|
+
* read the marker without re-registering the key.
|
|
124
|
+
*/
|
|
125
|
+
export declare const PLUGIN_MARKER: unique symbol;
|
|
126
|
+
export interface AnnotatedPluginMeta {
|
|
127
|
+
readonly id: string;
|
|
128
|
+
readonly requires: readonly string[];
|
|
129
|
+
readonly [PLUGIN_MARKER]: true;
|
|
130
|
+
}
|
|
131
|
+
export type AnnotatedPluginFactory<EnvReq extends BaseEnv = BaseEnv, Result = unknown> = PluginFactory<EnvReq, Result> & AnnotatedPluginMeta;
|
|
132
|
+
/**
|
|
133
|
+
* Constant on every plugin instance returned by `definePlugin`. Hosts
|
|
134
|
+
* that need to distinguish plugin instances from arbitrary objects
|
|
135
|
+
* received via `env.plugins` check this field before duck-typing on
|
|
136
|
+
* shape. The string value is the operative form of the contract — the
|
|
137
|
+
* value-side marker exists because the factory-side symbol marker
|
|
138
|
+
* (PLUGIN_MARKER above) is only visible to code that imported it,
|
|
139
|
+
* while the kind string travels through pure-JSON inspection too.
|
|
140
|
+
*/
|
|
141
|
+
export declare const TOOL_PLUGIN_KIND: "tool-plugin";
|
|
142
|
+
/** Type-level form of the kind marker. */
|
|
143
|
+
export type ToolPluginKind = typeof TOOL_PLUGIN_KIND;
|
|
144
|
+
/**
|
|
145
|
+
* Predicate hosts use to confirm a value off `env.plugins` was minted
|
|
146
|
+
* by `definePlugin` rather than happening to satisfy a shape-based
|
|
147
|
+
* check. Returns true iff the value is an object carrying the
|
|
148
|
+
* literal `kind: "tool-plugin"` marker.
|
|
149
|
+
*/
|
|
150
|
+
export declare function isToolPluginInstance(value: unknown): value is Record<string, unknown> & {
|
|
151
|
+
kind: ToolPluginKind;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Define a plugin factory. The plugin's `Result` is host-defined and
|
|
155
|
+
* surfaces in `env.plugins` for the host-side tool factories that
|
|
156
|
+
* consume it. The returned instance is tagged with
|
|
157
|
+
* `kind: "tool-plugin"` so hosts can identify plugin instances
|
|
158
|
+
* structurally without falling back to duck-typing on the result's
|
|
159
|
+
* own shape.
|
|
160
|
+
*
|
|
161
|
+
* `id` must be package-namespaced — same rule as `defineTool` — so
|
|
162
|
+
* audit provenance threads through plugins as cleanly as through
|
|
163
|
+
* tools.
|
|
164
|
+
*/
|
|
165
|
+
export declare function definePlugin<Result extends object, EnvReq extends BaseEnv = BaseEnv>(opts: {
|
|
166
|
+
id: string;
|
|
167
|
+
requires?: readonly string[];
|
|
168
|
+
factory: PluginFactory<EnvReq, Result>;
|
|
169
|
+
}): AnnotatedPluginFactory<EnvReq, Result & {
|
|
170
|
+
kind: ToolPluginKind;
|
|
171
|
+
}>;
|
|
172
|
+
/** Type predicate distinguishing plugin factories from tool factories. */
|
|
173
|
+
export declare function isAnnotatedPluginFactory(value: unknown): value is AnnotatedPluginFactory;
|
|
174
|
+
/**
|
|
175
|
+
* Build a `ToolRunner` that dispatches by tool name. Throws
|
|
176
|
+
* `DuplicateToolError` at construction if any two tools share a name.
|
|
177
|
+
*
|
|
178
|
+
* At call time, unknown tool names and exceptions from handlers are
|
|
179
|
+
* converted to `ToolResult { isError: true }` so the contract on
|
|
180
|
+
* `ToolRunner.run` ("must not throw") is upheld.
|
|
181
|
+
*/
|
|
182
|
+
export declare function createToolRunner(tools: AgentTool[]): AgentToolRunner;
|
package/dist/tool.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Tool registration and dispatch.
|
|
2
|
+
//
|
|
3
|
+
// Two registration shapes are supported for per-tool authoring:
|
|
4
|
+
//
|
|
5
|
+
// `tool({ definition, handler })` - handler receives the full
|
|
6
|
+
// ToolCall and returns the full
|
|
7
|
+
// ToolResult. Use when the
|
|
8
|
+
// handler needs the callId or
|
|
9
|
+
// wants to set isError/detail/
|
|
10
|
+
// pendingMarker.
|
|
11
|
+
//
|
|
12
|
+
// `stringTool({ definition, handler })` - sugar for the common case of
|
|
13
|
+
// "compute a string from the
|
|
14
|
+
// parsed arguments." The callId
|
|
15
|
+
// is filled in from the
|
|
16
|
+
// surrounding ToolCall, and
|
|
17
|
+
// isError is false unless the
|
|
18
|
+
// handler throws.
|
|
19
|
+
//
|
|
20
|
+
// `createToolRunner(tools)` builds a `ToolRunner` that dispatches by tool
|
|
21
|
+
// name. Per the ToolRunner contract (packages/types/src/runtime.ts), `run`
|
|
22
|
+
// must not throw -- unknown tool names and handler exceptions are surfaced
|
|
23
|
+
// as `ToolResult` with `isError: true` so the model sees them and can
|
|
24
|
+
// recover.
|
|
25
|
+
//
|
|
26
|
+
// `defineTool({ id, requires?, factory })` is the env-DI factory shape.
|
|
27
|
+
// It produces an
|
|
28
|
+
// `AnnotatedToolFactory` whose `factory(env)` returns a `ToolBundle`
|
|
29
|
+
// exposing a set of tool definitions, a dispatcher, and an optional
|
|
30
|
+
// disposer. Bundle-style (rather than per-tool) factory shapes match
|
|
31
|
+
// the existing posix-tools and mail-tools ergonomics; a package that
|
|
32
|
+
// wants per-tool granularity wraps each tool in its own single-
|
|
33
|
+
// definition bundle.
|
|
34
|
+
import { validateNamespacedId } from "./namespace.js";
|
|
35
|
+
export function tool(args) {
|
|
36
|
+
return { kind: "full", definition: args.definition, handler: args.handler };
|
|
37
|
+
}
|
|
38
|
+
export function stringTool(args) {
|
|
39
|
+
return {
|
|
40
|
+
kind: "string",
|
|
41
|
+
definition: args.definition,
|
|
42
|
+
handler: args.handler,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Adapt a pre-built ToolRunner (e.g. the one returned by
|
|
47
|
+
* `createPosixTools`) into a list of AgentTools that can be passed to
|
|
48
|
+
* `createAgent({ tools })`. Each definition becomes a full-handler
|
|
49
|
+
* AgentTool that delegates to the runner's `run`.
|
|
50
|
+
*
|
|
51
|
+
* Use this when integrating tool packages whose public surface is a
|
|
52
|
+
* single ToolRunner rather than individual handlers.
|
|
53
|
+
*/
|
|
54
|
+
export function fromToolRunner(runner) {
|
|
55
|
+
return runner.definitions.map((definition) => ({
|
|
56
|
+
kind: "full",
|
|
57
|
+
definition,
|
|
58
|
+
handler: (call, signal) => runner.run(call, signal),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
export class DuplicateToolError extends Error {
|
|
62
|
+
toolName;
|
|
63
|
+
constructor(toolName) {
|
|
64
|
+
super(`duplicate tool name: ${toolName}`);
|
|
65
|
+
this.name = "DuplicateToolError";
|
|
66
|
+
this.toolName = toolName;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Define a tool bundle factory.
|
|
71
|
+
*
|
|
72
|
+
* - `id` must be package-namespaced ("@vendor/pkg/name" or
|
|
73
|
+
* "pkg/name"). Bare ids throw `Error` at definition time.
|
|
74
|
+
* - `requires` enumerates the env keys this factory touches beyond
|
|
75
|
+
* `BaseEnv`'s six core fields. The runtime `validateEnv` checks
|
|
76
|
+
* presence; the factory itself may also fail loud at construction
|
|
77
|
+
* if the env contents are structurally wrong.
|
|
78
|
+
* - `factory(env)` returns a `ToolBundle`. Invoked once per agent
|
|
79
|
+
* instantiation; the bundle's lifetime is tied to that agent.
|
|
80
|
+
*
|
|
81
|
+
* The returned object is the same callable as the supplied `factory`
|
|
82
|
+
* with `id` and a frozen `requires` array attached.
|
|
83
|
+
*/
|
|
84
|
+
export function defineTool(opts) {
|
|
85
|
+
validateNamespacedId(opts.id);
|
|
86
|
+
const requires = Object.freeze([
|
|
87
|
+
...(opts.requires ?? []),
|
|
88
|
+
]);
|
|
89
|
+
// Wrap the caller's factory rather than mutating it. A caller that
|
|
90
|
+
// shares a factory function across multiple `defineTool` calls
|
|
91
|
+
// (e.g. registering the same constructor under two ids in different
|
|
92
|
+
// bundles) needs each `AnnotatedToolFactory` to be a distinct
|
|
93
|
+
// identity with its own metadata; a direct `Object.assign` on
|
|
94
|
+
// `opts.factory` would let the second call silently overwrite the
|
|
95
|
+
// first's annotations.
|
|
96
|
+
const wrapped = (env) => opts.factory(env);
|
|
97
|
+
return Object.assign(wrapped, {
|
|
98
|
+
id: opts.id,
|
|
99
|
+
requires,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Registered symbol that tags `AnnotatedPluginFactory` values. Exported
|
|
104
|
+
* so consumers that need to introspect plugin factories directly can
|
|
105
|
+
* read the marker without re-registering the key.
|
|
106
|
+
*/
|
|
107
|
+
export const PLUGIN_MARKER = Symbol.for("@intx/agent.plugin");
|
|
108
|
+
/**
|
|
109
|
+
* Constant on every plugin instance returned by `definePlugin`. Hosts
|
|
110
|
+
* that need to distinguish plugin instances from arbitrary objects
|
|
111
|
+
* received via `env.plugins` check this field before duck-typing on
|
|
112
|
+
* shape. The string value is the operative form of the contract — the
|
|
113
|
+
* value-side marker exists because the factory-side symbol marker
|
|
114
|
+
* (PLUGIN_MARKER above) is only visible to code that imported it,
|
|
115
|
+
* while the kind string travels through pure-JSON inspection too.
|
|
116
|
+
*/
|
|
117
|
+
export const TOOL_PLUGIN_KIND = "tool-plugin";
|
|
118
|
+
/**
|
|
119
|
+
* Predicate hosts use to confirm a value off `env.plugins` was minted
|
|
120
|
+
* by `definePlugin` rather than happening to satisfy a shape-based
|
|
121
|
+
* check. Returns true iff the value is an object carrying the
|
|
122
|
+
* literal `kind: "tool-plugin"` marker.
|
|
123
|
+
*/
|
|
124
|
+
export function isToolPluginInstance(value) {
|
|
125
|
+
if (value === null || typeof value !== "object")
|
|
126
|
+
return false;
|
|
127
|
+
if (!("kind" in value))
|
|
128
|
+
return false;
|
|
129
|
+
return value.kind === TOOL_PLUGIN_KIND;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Define a plugin factory. The plugin's `Result` is host-defined and
|
|
133
|
+
* surfaces in `env.plugins` for the host-side tool factories that
|
|
134
|
+
* consume it. The returned instance is tagged with
|
|
135
|
+
* `kind: "tool-plugin"` so hosts can identify plugin instances
|
|
136
|
+
* structurally without falling back to duck-typing on the result's
|
|
137
|
+
* own shape.
|
|
138
|
+
*
|
|
139
|
+
* `id` must be package-namespaced — same rule as `defineTool` — so
|
|
140
|
+
* audit provenance threads through plugins as cleanly as through
|
|
141
|
+
* tools.
|
|
142
|
+
*/
|
|
143
|
+
export function definePlugin(opts) {
|
|
144
|
+
validateNamespacedId(opts.id);
|
|
145
|
+
const requires = Object.freeze([
|
|
146
|
+
...(opts.requires ?? []),
|
|
147
|
+
]);
|
|
148
|
+
const wrapped = (env) => {
|
|
149
|
+
const instance = opts.factory(env);
|
|
150
|
+
// Re-stamping the marker is harmless if the factory chose to set
|
|
151
|
+
// it itself; otherwise we add it. Either way the returned value
|
|
152
|
+
// carries the contract.
|
|
153
|
+
return Object.assign(instance, { kind: TOOL_PLUGIN_KIND });
|
|
154
|
+
};
|
|
155
|
+
return Object.assign(wrapped, {
|
|
156
|
+
id: opts.id,
|
|
157
|
+
requires,
|
|
158
|
+
[PLUGIN_MARKER]: true,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** Type predicate distinguishing plugin factories from tool factories. */
|
|
162
|
+
export function isAnnotatedPluginFactory(value) {
|
|
163
|
+
if (typeof value !== "function")
|
|
164
|
+
return false;
|
|
165
|
+
if (!(PLUGIN_MARKER in value))
|
|
166
|
+
return false;
|
|
167
|
+
// `PLUGIN_MARKER in value` narrows `value` to include the symbol
|
|
168
|
+
// key, so the index access below is type-safe without a cast.
|
|
169
|
+
return value[PLUGIN_MARKER] === true;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build a `ToolRunner` that dispatches by tool name. Throws
|
|
173
|
+
* `DuplicateToolError` at construction if any two tools share a name.
|
|
174
|
+
*
|
|
175
|
+
* At call time, unknown tool names and exceptions from handlers are
|
|
176
|
+
* converted to `ToolResult { isError: true }` so the contract on
|
|
177
|
+
* `ToolRunner.run` ("must not throw") is upheld.
|
|
178
|
+
*/
|
|
179
|
+
export function createToolRunner(tools) {
|
|
180
|
+
const byName = new Map();
|
|
181
|
+
for (const t of tools) {
|
|
182
|
+
if (byName.has(t.definition.name)) {
|
|
183
|
+
throw new DuplicateToolError(t.definition.name);
|
|
184
|
+
}
|
|
185
|
+
byName.set(t.definition.name, t);
|
|
186
|
+
}
|
|
187
|
+
const definitions = tools.map((t) => t.definition);
|
|
188
|
+
return {
|
|
189
|
+
definitions,
|
|
190
|
+
async run(call, signal) {
|
|
191
|
+
const found = byName.get(call.name);
|
|
192
|
+
if (found === undefined) {
|
|
193
|
+
return {
|
|
194
|
+
callId: call.id,
|
|
195
|
+
content: `unknown tool: ${call.name}`,
|
|
196
|
+
isError: true,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
if (found.kind === "full") {
|
|
201
|
+
return await found.handler(call, signal);
|
|
202
|
+
}
|
|
203
|
+
const text = await found.handler(call.arguments, signal);
|
|
204
|
+
return { callId: call.id, content: text };
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
return {
|
|
208
|
+
callId: call.id,
|
|
209
|
+
content: err instanceof Error ? err.message : String(err),
|
|
210
|
+
isError: true,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intx/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"license": "LGPL-2.1-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"intx-src": "./src/index.ts",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./testing": {
|
|
13
|
+
"intx-src": "./src/testing/index.ts",
|
|
14
|
+
"types": "./dist/testing/index.d.ts",
|
|
15
|
+
"default": "./dist/testing/index.js"
|
|
10
16
|
}
|
|
11
17
|
},
|
|
12
18
|
"dependencies": {
|
|
13
|
-
"@intx/inference": "0.
|
|
14
|
-
"@intx/
|
|
15
|
-
"@intx/
|
|
16
|
-
"@intx/types": "0.
|
|
19
|
+
"@intx/inference": "0.2.2",
|
|
20
|
+
"@intx/log": "0.2.2",
|
|
21
|
+
"@intx/mime": "0.2.2",
|
|
22
|
+
"@intx/types": "0.2.2",
|
|
17
23
|
"arktype": "^2.1.29"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@intx/storage-isogit": "0.2.2"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"sideEffects": false,
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
18
36
|
}
|
|
19
37
|
}
|