@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.
Files changed (60) hide show
  1. package/LICENSE +176 -0
  2. package/README.md +80 -5
  3. package/dist/agent.d.ts +87 -0
  4. package/dist/agent.js +638 -0
  5. package/dist/canonicalize.d.ts +15 -0
  6. package/dist/canonicalize.js +160 -0
  7. package/dist/default-director.d.ts +24 -0
  8. package/dist/default-director.js +45 -0
  9. package/dist/definition.d.ts +116 -0
  10. package/dist/definition.js +39 -0
  11. package/dist/director-registry.d.ts +38 -0
  12. package/dist/director-registry.js +73 -0
  13. package/dist/director-types.d.ts +80 -0
  14. package/dist/director-types.js +13 -0
  15. package/dist/director.d.ts +56 -0
  16. package/dist/director.js +92 -0
  17. package/dist/env-validation.d.ts +59 -0
  18. package/dist/env-validation.js +180 -0
  19. package/dist/env.d.ts +160 -0
  20. package/dist/env.js +53 -0
  21. package/dist/index.d.ts +16 -0
  22. package/dist/index.js +23 -0
  23. package/dist/internal-fixtures/mail.d.ts +39 -0
  24. package/dist/internal-fixtures/mail.js +85 -0
  25. package/dist/internal-fixtures/planner.d.ts +19 -0
  26. package/dist/internal-fixtures/planner.js +49 -0
  27. package/dist/lock.d.ts +16 -0
  28. package/dist/lock.js +47 -0
  29. package/dist/namespace.d.ts +12 -0
  30. package/dist/namespace.js +39 -0
  31. package/dist/send-queue.d.ts +25 -0
  32. package/dist/send-queue.js +147 -0
  33. package/dist/source.d.ts +43 -0
  34. package/dist/source.js +118 -0
  35. package/dist/stream.d.ts +16 -0
  36. package/dist/stream.js +115 -0
  37. package/dist/testing/audit-noop.d.ts +7 -0
  38. package/dist/testing/audit-noop.js +25 -0
  39. package/dist/testing/authorize-allow.d.ts +8 -0
  40. package/dist/testing/authorize-allow.js +19 -0
  41. package/dist/testing/index.d.ts +2 -0
  42. package/dist/testing/index.js +17 -0
  43. package/dist/tool.d.ts +182 -0
  44. package/dist/tool.js +215 -0
  45. package/package.json +25 -7
  46. package/src/agent.test.ts +0 -46
  47. package/src/agent.ts +0 -494
  48. package/src/index.ts +0 -38
  49. package/src/lock.test.ts +0 -93
  50. package/src/lock.ts +0 -57
  51. package/src/send-queue.test.ts +0 -207
  52. package/src/send-queue.ts +0 -200
  53. package/src/source.test.ts +0 -171
  54. package/src/source.ts +0 -93
  55. package/src/stream.test.ts +0 -167
  56. package/src/stream.ts +0 -142
  57. package/src/tool.test.ts +0 -217
  58. package/src/tool.ts +0 -148
  59. package/tsconfig.json +0 -4
  60. 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,2 @@
1
+ export { noopAuditStore } from "./audit-noop.js";
2
+ export { permissiveAuthorize } from "./authorize-allow.js";
@@ -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.1.2",
3
+ "version": "0.2.2",
4
4
  "license": "LGPL-2.1-only",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "types": "./src/index.ts",
9
- "default": "./src/index.ts"
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.0.0",
14
- "@intx/mime": "0.0.0",
15
- "@intx/storage-isogit": "0.0.0",
16
- "@intx/types": "0.0.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
  }