@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/src/source.ts DELETED
@@ -1,93 +0,0 @@
1
- // Inference source registry.
2
- //
3
- // The agent accepts an array of pre-configured inference sources and a
4
- // `defaultSource` id at construction. The source whose `id` matches
5
- // `defaultSource` becomes the active source — the same object reference
6
- // is what the reactor's assembly holds and reads lazily at each
7
- // inference call.
8
- //
9
- // `setSource` mutates that shared object in place so the next inference
10
- // call observes the new credentials, model, and bound defaults. In-flight
11
- // calls keep using the values they read at start-of-call (the reactor
12
- // does not refetch mid-stream); the swap is therefore safe with respect
13
- // to torn state.
14
-
15
- import { type } from "arktype";
16
-
17
- import {
18
- InferenceSource as InferenceSourceValidator,
19
- applyInferenceSourceFields,
20
- type InferenceSource,
21
- } from "@intx/types/runtime";
22
-
23
- export class InvalidInferenceSourceError extends Error {
24
- constructor(message: string) {
25
- super(message);
26
- this.name = "InvalidInferenceSourceError";
27
- }
28
- }
29
-
30
- export class SourceNotFoundError extends Error {
31
- readonly id: string;
32
-
33
- constructor(id: string) {
34
- super(`no source in sources[] has id ${id}`);
35
- this.name = "SourceNotFoundError";
36
- this.id = id;
37
- }
38
- }
39
-
40
- export type SourceRegistry = {
41
- /**
42
- * The mutable active source. The same object reference is held by the
43
- * reactor; mutating it through `setSource` is what swaps the source for
44
- * subsequent inference calls.
45
- */
46
- readonly active: InferenceSource;
47
- /** Replace the active source's fields in place. */
48
- setSource(source: InferenceSource): void;
49
- };
50
-
51
- export function createSourceRegistry(opts: {
52
- sources: InferenceSource[];
53
- defaultSource: string;
54
- }): SourceRegistry {
55
- if (opts.sources.length === 0) {
56
- throw new InvalidInferenceSourceError("sources[] must be non-empty");
57
- }
58
-
59
- const validated: InferenceSource[] = [];
60
- const seenIds = new Set<string>();
61
- for (const [i, raw] of opts.sources.entries()) {
62
- const parsed = InferenceSourceValidator(raw);
63
- if (parsed instanceof type.errors) {
64
- throw new InvalidInferenceSourceError(
65
- `sources[${String(i)}]: ${parsed.summary}`,
66
- );
67
- }
68
- if (seenIds.has(parsed.id)) {
69
- throw new InvalidInferenceSourceError(
70
- `sources[${String(i)}]: duplicate id ${parsed.id}`,
71
- );
72
- }
73
- seenIds.add(parsed.id);
74
- validated.push(parsed);
75
- }
76
-
77
- const initial = validated.find((s) => s.id === opts.defaultSource);
78
- if (initial === undefined) {
79
- throw new SourceNotFoundError(opts.defaultSource);
80
- }
81
-
82
- const active: InferenceSource = { ...initial };
83
-
84
- function setSource(source: InferenceSource): void {
85
- const parsed = InferenceSourceValidator(source);
86
- if (parsed instanceof type.errors) {
87
- throw new InvalidInferenceSourceError(parsed.summary);
88
- }
89
- applyInferenceSourceFields(active, parsed);
90
- }
91
-
92
- return { active, setSource };
93
- }
@@ -1,167 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
-
3
- import type { ReactorEmittedEvent } from "@intx/inference";
4
-
5
- import { createStreamConsumer, StreamBackpressureError } from "./stream";
6
-
7
- /**
8
- * Build a minimal ReactorEmittedEvent suitable for fan-out testing. The
9
- * event's structural details do not matter — the stream consumer treats
10
- * events opaquely — so we use `reactor.done`, which has an empty `data`.
11
- */
12
- function makeEvent(seq: number): ReactorEmittedEvent {
13
- return { type: "reactor.done", seq, data: {} };
14
- }
15
-
16
- async function collect(
17
- it: AsyncIterableIterator<ReactorEmittedEvent>,
18
- n: number,
19
- ): Promise<ReactorEmittedEvent[]> {
20
- const out: ReactorEmittedEvent[] = [];
21
- for (let i = 0; i < n; i++) {
22
- const r = await it.next();
23
- if (r.done === true) break;
24
- out.push(r.value);
25
- }
26
- return out;
27
- }
28
-
29
- describe("createStreamConsumer", () => {
30
- test("delivers buffered events to a later iterator read", async () => {
31
- const c = createStreamConsumer(8);
32
- const it = c.iterator();
33
- c.push(makeEvent(1));
34
- c.push(makeEvent(2));
35
- const got = await collect(it, 2);
36
- expect(got.map((e) => e.seq)).toEqual([1, 2]);
37
- });
38
-
39
- test("delivers events directly to a waiting iterator", async () => {
40
- const c = createStreamConsumer(8);
41
- const it = c.iterator();
42
- const pending = it.next();
43
- c.push(makeEvent(42));
44
- const r = await pending;
45
- expect(r.done).toBe(false);
46
- if (r.done !== true) expect(r.value.seq).toBe(42);
47
- });
48
-
49
- test("close terminates pending and subsequent reads with done", async () => {
50
- const c = createStreamConsumer(8);
51
- const it = c.iterator();
52
- const pending = it.next();
53
- c.close();
54
- const r1 = await pending;
55
- expect(r1.done).toBe(true);
56
- const r2 = await it.next();
57
- expect(r2.done).toBe(true);
58
- });
59
-
60
- test("close after buffered events still drains them before done", async () => {
61
- const c = createStreamConsumer(8);
62
- const it = c.iterator();
63
- c.push(makeEvent(1));
64
- c.push(makeEvent(2));
65
- c.close();
66
- const r1 = await it.next();
67
- expect(r1.done).toBe(false);
68
- const r2 = await it.next();
69
- expect(r2.done).toBe(false);
70
- const r3 = await it.next();
71
- expect(r3.done).toBe(true);
72
- });
73
-
74
- test("overflow throws StreamBackpressureError on next read", async () => {
75
- const c = createStreamConsumer(3);
76
- const it = c.iterator();
77
- c.push(makeEvent(1));
78
- c.push(makeEvent(2));
79
- c.push(makeEvent(3));
80
- c.push(makeEvent(4));
81
-
82
- // Buffered events drain first.
83
- const r1 = await it.next();
84
- expect(r1.done).toBe(false);
85
- const r2 = await it.next();
86
- expect(r2.done).toBe(false);
87
- const r3 = await it.next();
88
- expect(r3.done).toBe(false);
89
-
90
- // Next read sees the overflow.
91
- await expect(it.next()).rejects.toBeInstanceOf(StreamBackpressureError);
92
- });
93
-
94
- test("overflow rejects a pending waiter", async () => {
95
- const c = createStreamConsumer(2);
96
- const it = c.iterator();
97
- const pending = it.next();
98
- // Direct delivery to the waiter does NOT increase the buffer.
99
- c.push(makeEvent(1));
100
- const r1 = await pending;
101
- expect(r1.done).toBe(false);
102
-
103
- // Now buffer 2 events (capacity), then a 3rd while another waiter is
104
- // pending — wait, an immediate waiter would consume the 3rd directly.
105
- // Instead saturate the buffer first.
106
- c.push(makeEvent(2));
107
- c.push(makeEvent(3));
108
- // Saturated. A pending waiter at this point will be served from the
109
- // buffer; the overflow only fires on a push that has no waiter and a
110
- // full buffer.
111
- c.push(makeEvent(4));
112
-
113
- // Drain.
114
- const r2 = await it.next();
115
- expect(r2.done).toBe(false);
116
- const r3 = await it.next();
117
- expect(r3.done).toBe(false);
118
-
119
- await expect(it.next()).rejects.toBeInstanceOf(StreamBackpressureError);
120
- });
121
-
122
- test("multiple consumers buffer independently", async () => {
123
- const a = createStreamConsumer(8);
124
- const b = createStreamConsumer(8);
125
- const ai = a.iterator();
126
- const bi = b.iterator();
127
-
128
- a.push(makeEvent(1));
129
- b.push(makeEvent(1));
130
- a.push(makeEvent(2));
131
- b.push(makeEvent(2));
132
-
133
- const aGot = await collect(ai, 2);
134
- const bGot = await collect(bi, 2);
135
- expect(aGot.map((e) => e.seq)).toEqual([1, 2]);
136
- expect(bGot.map((e) => e.seq)).toEqual([1, 2]);
137
- });
138
-
139
- test("iterator.return() closes the consumer", async () => {
140
- const c = createStreamConsumer(8);
141
- const it = c.iterator();
142
- expect(c.closed).toBe(false);
143
- await it.return?.();
144
- expect(c.closed).toBe(true);
145
- const r = await it.next();
146
- expect(r.done).toBe(true);
147
- });
148
-
149
- test("push after close is ignored", async () => {
150
- const c = createStreamConsumer(8);
151
- const it = c.iterator();
152
- c.close();
153
- c.push(makeEvent(1));
154
- const r = await it.next();
155
- expect(r.done).toBe(true);
156
- });
157
-
158
- test("Symbol.asyncIterator returns the iterator itself", async () => {
159
- const c = createStreamConsumer(8);
160
- const it = c.iterator();
161
- expect(it[Symbol.asyncIterator]()).toBe(it);
162
- });
163
-
164
- test("rejects maxBuffer < 1", () => {
165
- expect(() => createStreamConsumer(0)).toThrow();
166
- });
167
- });
package/src/stream.ts DELETED
@@ -1,142 +0,0 @@
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 `AgentConfig`.
14
-
15
- import type { ReactorEmittedEvent } from "@intx/inference";
16
-
17
- export class StreamBackpressureError extends Error {
18
- readonly maxBuffer: number;
19
-
20
- constructor(maxBuffer: number) {
21
- super(`stream consumer fell more than ${String(maxBuffer)} events behind`);
22
- this.name = "StreamBackpressureError";
23
- this.maxBuffer = maxBuffer;
24
- }
25
- }
26
-
27
- type Waiter = {
28
- resolve: (value: IteratorResult<ReactorEmittedEvent>) => void;
29
- reject: (reason: unknown) => void;
30
- };
31
-
32
- export type StreamConsumer = {
33
- /** Deliver an event to this consumer's buffer. */
34
- push(event: ReactorEmittedEvent): void;
35
- /** Cleanly terminate the iterator with `done: true`. */
36
- close(): void;
37
- /** True once close() or an overflow has poisoned the consumer. */
38
- readonly closed: boolean;
39
- /** Iterator handed back to the caller of `stream()`. */
40
- iterator(): AsyncIterableIterator<ReactorEmittedEvent>;
41
- };
42
-
43
- export function createStreamConsumer(maxBuffer: number): StreamConsumer {
44
- if (maxBuffer < 1) {
45
- throw new Error(`streamBufferMax must be >= 1, got ${String(maxBuffer)}`);
46
- }
47
-
48
- const buffer: ReactorEmittedEvent[] = [];
49
- const waiters: Waiter[] = [];
50
- let overflow: StreamBackpressureError | undefined;
51
- let done = false;
52
-
53
- function settleOverflowedWaiters(err: StreamBackpressureError): void {
54
- while (waiters.length > 0) {
55
- const w = waiters.shift();
56
- if (w === undefined) return;
57
- w.reject(err);
58
- }
59
- }
60
-
61
- function settleDoneWaiters(): void {
62
- while (waiters.length > 0) {
63
- const w = waiters.shift();
64
- if (w === undefined) return;
65
- w.resolve({ value: undefined, done: true });
66
- }
67
- }
68
-
69
- function push(event: ReactorEmittedEvent): void {
70
- if (done || overflow !== undefined) return;
71
-
72
- if (waiters.length > 0) {
73
- const w = waiters.shift();
74
- if (w === undefined) return;
75
- w.resolve({ value: event, done: false });
76
- return;
77
- }
78
-
79
- if (buffer.length >= maxBuffer) {
80
- overflow = new StreamBackpressureError(maxBuffer);
81
- settleOverflowedWaiters(overflow);
82
- return;
83
- }
84
-
85
- buffer.push(event);
86
- }
87
-
88
- function close(): void {
89
- if (done) return;
90
- done = true;
91
- settleDoneWaiters();
92
- }
93
-
94
- function nextResult(): Promise<IteratorResult<ReactorEmittedEvent>> {
95
- if (overflow !== undefined) {
96
- // Drain any buffered events before throwing so the caller sees
97
- // every event up to the overflow point.
98
- if (buffer.length > 0) {
99
- const ev = buffer.shift();
100
- if (ev !== undefined) {
101
- return Promise.resolve({ value: ev, done: false });
102
- }
103
- }
104
- return Promise.reject(overflow);
105
- }
106
- if (buffer.length > 0) {
107
- const ev = buffer.shift();
108
- if (ev !== undefined) {
109
- return Promise.resolve({ value: ev, done: false });
110
- }
111
- }
112
- if (done) {
113
- return Promise.resolve({ value: undefined, done: true });
114
- }
115
- return new Promise((resolve, reject) => {
116
- waiters.push({ resolve, reject });
117
- });
118
- }
119
-
120
- function iterator(): AsyncIterableIterator<ReactorEmittedEvent> {
121
- const it: AsyncIterableIterator<ReactorEmittedEvent> = {
122
- next: nextResult,
123
- async return() {
124
- close();
125
- return { value: undefined, done: true };
126
- },
127
- [Symbol.asyncIterator]() {
128
- return it;
129
- },
130
- };
131
- return it;
132
- }
133
-
134
- return {
135
- push,
136
- close,
137
- get closed() {
138
- return done || overflow !== undefined;
139
- },
140
- iterator,
141
- };
142
- }
package/src/tool.test.ts DELETED
@@ -1,217 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
-
3
- import type { ToolDefinition } from "@intx/types/runtime";
4
-
5
- import {
6
- createToolRunner,
7
- DuplicateToolError,
8
- fromToolRunner,
9
- stringTool,
10
- tool,
11
- } from "./tool";
12
-
13
- const DEF_A: ToolDefinition = {
14
- name: "a",
15
- description: "tool a",
16
- inputSchema: { type: "object" },
17
- };
18
-
19
- const DEF_B: ToolDefinition = {
20
- name: "b",
21
- description: "tool b",
22
- inputSchema: { type: "object" },
23
- };
24
-
25
- describe("createToolRunner", () => {
26
- test("dispatches to a full handler by tool name", async () => {
27
- const runner = createToolRunner([
28
- tool({
29
- definition: DEF_A,
30
- handler: async (call) => ({
31
- callId: call.id,
32
- content: `got ${String(call.arguments.x)}`,
33
- }),
34
- }),
35
- ]);
36
-
37
- const result = await runner.run(
38
- { id: "c1", name: "a", arguments: { x: 5 } },
39
- new AbortController().signal,
40
- );
41
-
42
- expect(result).toEqual({ callId: "c1", content: "got 5" });
43
- });
44
-
45
- test("lifts the string handler return into a ToolResult", async () => {
46
- const runner = createToolRunner([
47
- stringTool({
48
- definition: DEF_A,
49
- handler: async (args) => `hello ${String(args.name)}`,
50
- }),
51
- ]);
52
-
53
- const result = await runner.run(
54
- { id: "c2", name: "a", arguments: { name: "world" } },
55
- new AbortController().signal,
56
- );
57
-
58
- expect(result).toEqual({ callId: "c2", content: "hello world" });
59
- });
60
-
61
- test("returns an error ToolResult for an unknown tool name", async () => {
62
- const runner = createToolRunner([
63
- tool({
64
- definition: DEF_A,
65
- handler: async (call) => ({ callId: call.id, content: "" }),
66
- }),
67
- ]);
68
-
69
- const result = await runner.run(
70
- { id: "c3", name: "nope", arguments: {} },
71
- new AbortController().signal,
72
- );
73
-
74
- expect(result.isError).toBe(true);
75
- expect(result.callId).toBe("c3");
76
- expect(result.content).toBe("unknown tool: nope");
77
- });
78
-
79
- test("wraps a thrown error from a full handler into an error ToolResult", async () => {
80
- const runner = createToolRunner([
81
- tool({
82
- definition: DEF_A,
83
- handler: async () => {
84
- throw new Error("boom");
85
- },
86
- }),
87
- ]);
88
-
89
- const result = await runner.run(
90
- { id: "c4", name: "a", arguments: {} },
91
- new AbortController().signal,
92
- );
93
-
94
- expect(result).toEqual({ callId: "c4", content: "boom", isError: true });
95
- });
96
-
97
- test("wraps a thrown error from a string handler into an error ToolResult", async () => {
98
- const runner = createToolRunner([
99
- stringTool({
100
- definition: DEF_A,
101
- handler: async () => {
102
- throw new Error("bad input");
103
- },
104
- }),
105
- ]);
106
-
107
- const result = await runner.run(
108
- { id: "c5", name: "a", arguments: {} },
109
- new AbortController().signal,
110
- );
111
-
112
- expect(result).toEqual({
113
- callId: "c5",
114
- content: "bad input",
115
- isError: true,
116
- });
117
- });
118
-
119
- test("exposes definitions in registration order", () => {
120
- const runner = createToolRunner([
121
- tool({
122
- definition: DEF_A,
123
- handler: async (call) => ({ callId: call.id, content: "" }),
124
- }),
125
- stringTool({ definition: DEF_B, handler: async () => "x" }),
126
- ]);
127
-
128
- expect(runner.definitions.map((d) => d.name)).toEqual(["a", "b"]);
129
- });
130
-
131
- test("throws DuplicateToolError at construction on duplicate names", () => {
132
- expect(() =>
133
- createToolRunner([
134
- tool({
135
- definition: DEF_A,
136
- handler: async (call) => ({ callId: call.id, content: "" }),
137
- }),
138
- stringTool({ definition: DEF_A, handler: async () => "x" }),
139
- ]),
140
- ).toThrow(DuplicateToolError);
141
- });
142
-
143
- test("propagates the AbortSignal to the handler", async () => {
144
- let received: AbortSignal | undefined;
145
- const runner = createToolRunner([
146
- tool({
147
- definition: DEF_A,
148
- handler: async (call, signal) => {
149
- received = signal;
150
- return { callId: call.id, content: "ok" };
151
- },
152
- }),
153
- ]);
154
-
155
- const ctl = new AbortController();
156
- await runner.run({ id: "c6", name: "a", arguments: {} }, ctl.signal);
157
-
158
- expect(received).toBe(ctl.signal);
159
- });
160
-
161
- test("fromToolRunner wraps each definition as a full-handler AgentTool", async () => {
162
- const calls: string[] = [];
163
- const stubRunner = {
164
- definitions: [DEF_A, DEF_B] as const,
165
- run: async (call: {
166
- id: string;
167
- name: string;
168
- arguments: Record<string, unknown>;
169
- }) => {
170
- calls.push(call.name);
171
- return Promise.resolve({
172
- callId: call.id,
173
- content: `ran ${call.name}`,
174
- });
175
- },
176
- };
177
-
178
- const tools = fromToolRunner(stubRunner);
179
- const runner = createToolRunner(tools);
180
-
181
- expect(runner.definitions.map((d) => d.name)).toEqual(["a", "b"]);
182
-
183
- const ra = await runner.run(
184
- { id: "c8", name: "a", arguments: {} },
185
- new AbortController().signal,
186
- );
187
- expect(ra).toEqual({ callId: "c8", content: "ran a" });
188
-
189
- const rb = await runner.run(
190
- { id: "c9", name: "b", arguments: {} },
191
- new AbortController().signal,
192
- );
193
- expect(rb).toEqual({ callId: "c9", content: "ran b" });
194
-
195
- expect(calls).toEqual(["a", "b"]);
196
- });
197
-
198
- test("string handler receives the parsed arguments object", async () => {
199
- let received: Record<string, unknown> | undefined;
200
- const runner = createToolRunner([
201
- stringTool({
202
- definition: DEF_A,
203
- handler: async (args) => {
204
- received = args;
205
- return "ok";
206
- },
207
- }),
208
- ]);
209
-
210
- await runner.run(
211
- { id: "c7", name: "a", arguments: { foo: 1, bar: "x" } },
212
- new AbortController().signal,
213
- );
214
-
215
- expect(received).toEqual({ foo: 1, bar: "x" });
216
- });
217
- });