@intx/agent 0.1.2 → 0.3.0
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 +116 -0
- package/dist/agent.js +682 -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 +139 -0
- package/dist/definition.js +40 -0
- package/dist/director-registry.d.ts +47 -0
- package/dist/director-registry.js +87 -0
- package/dist/director-types.d.ts +80 -0
- package/dist/director-types.js +13 -0
- package/dist/director.d.ts +70 -0
- package/dist/director.js +131 -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 +86 -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 +238 -0
- package/dist/tool.js +244 -0
- package/package.json +26 -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/src/send-queue.test.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { createSendQueue, SendQueueFullError } from "./send-queue";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Tests use `string` as the result type so the queue's generic R parameter
|
|
7
|
-
* holds a real value (it cannot be `void` under
|
|
8
|
-
* `@typescript-eslint/no-invalid-void-type`). The actual values are
|
|
9
|
-
* arbitrary; tests assert ordering and lifecycle, not result content.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
describe("createSendQueue", () => {
|
|
13
|
-
test("starts the first enqueued item immediately", () => {
|
|
14
|
-
const started: string[] = [];
|
|
15
|
-
const q = createSendQueue<string, string>({
|
|
16
|
-
maxDepth: 4,
|
|
17
|
-
start: (item) => {
|
|
18
|
-
started.push(item);
|
|
19
|
-
},
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
void q.enqueue("a");
|
|
23
|
-
expect(started).toEqual(["a"]);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("queues subsequent items and starts each on resolveActive", async () => {
|
|
27
|
-
const started: string[] = [];
|
|
28
|
-
const q = createSendQueue<string, string>({
|
|
29
|
-
maxDepth: 4,
|
|
30
|
-
start: (item) => {
|
|
31
|
-
started.push(item);
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const p1 = q.enqueue("a");
|
|
36
|
-
const p2 = q.enqueue("b");
|
|
37
|
-
expect(started).toEqual(["a"]);
|
|
38
|
-
|
|
39
|
-
q.resolveActive("r1");
|
|
40
|
-
expect(await p1).toBe("r1");
|
|
41
|
-
expect(started).toEqual(["a", "b"]);
|
|
42
|
-
|
|
43
|
-
q.resolveActive("r2");
|
|
44
|
-
expect(await p2).toBe("r2");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("throws SendQueueFullError synchronously at capacity", () => {
|
|
48
|
-
const started: number[] = [];
|
|
49
|
-
const q = createSendQueue<number, string>({
|
|
50
|
-
maxDepth: 2,
|
|
51
|
-
start: (item) => {
|
|
52
|
-
started.push(item);
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
void q.enqueue(1);
|
|
57
|
-
void q.enqueue(2);
|
|
58
|
-
expect(() => q.enqueue(3)).toThrow(SendQueueFullError);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("rejects pre-aborted signal without enqueuing", async () => {
|
|
62
|
-
const ctl = new AbortController();
|
|
63
|
-
ctl.abort();
|
|
64
|
-
|
|
65
|
-
const started: number[] = [];
|
|
66
|
-
const q = createSendQueue<number, string>({
|
|
67
|
-
maxDepth: 4,
|
|
68
|
-
start: (item) => {
|
|
69
|
-
started.push(item);
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
await expect(q.enqueue(1, ctl.signal)).rejects.toBeDefined();
|
|
74
|
-
expect(started).toEqual([]);
|
|
75
|
-
expect(q.depth).toBe(0);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("removes a queued item whose signal fires before processing", async () => {
|
|
79
|
-
const started: number[] = [];
|
|
80
|
-
const ctl = new AbortController();
|
|
81
|
-
const q = createSendQueue<number, string>({
|
|
82
|
-
maxDepth: 4,
|
|
83
|
-
start: (item) => {
|
|
84
|
-
started.push(item);
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const p1 = q.enqueue(1);
|
|
89
|
-
const p2 = q.enqueue(2, ctl.signal);
|
|
90
|
-
const p3 = q.enqueue(3);
|
|
91
|
-
|
|
92
|
-
expect(started).toEqual([1]);
|
|
93
|
-
ctl.abort();
|
|
94
|
-
await expect(p2).rejects.toBeDefined();
|
|
95
|
-
|
|
96
|
-
q.resolveActive("r1");
|
|
97
|
-
expect(await p1).toBe("r1");
|
|
98
|
-
expect(started).toEqual([1, 3]);
|
|
99
|
-
|
|
100
|
-
q.resolveActive("r3");
|
|
101
|
-
expect(await p3).toBe("r3");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("settles caller on in-flight abort but waits for consumer to advance", async () => {
|
|
105
|
-
const started: number[] = [];
|
|
106
|
-
const ctl = new AbortController();
|
|
107
|
-
const q = createSendQueue<number, string>({
|
|
108
|
-
maxDepth: 4,
|
|
109
|
-
start: (item) => {
|
|
110
|
-
started.push(item);
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const p1 = q.enqueue(1, ctl.signal);
|
|
115
|
-
const p2 = q.enqueue(2);
|
|
116
|
-
expect(started).toEqual([1]);
|
|
117
|
-
|
|
118
|
-
ctl.abort();
|
|
119
|
-
await expect(p1).rejects.toBeDefined();
|
|
120
|
-
// Active slot is still held until the consumer reports the cycle done.
|
|
121
|
-
expect(started).toEqual([1]);
|
|
122
|
-
|
|
123
|
-
q.resolveActive("late-r1");
|
|
124
|
-
expect(started).toEqual([1, 2]);
|
|
125
|
-
|
|
126
|
-
q.resolveActive("r2");
|
|
127
|
-
expect(await p2).toBe("r2");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("late resolveActive after abort is a no-op for the caller", async () => {
|
|
131
|
-
const ctl = new AbortController();
|
|
132
|
-
const started: number[] = [];
|
|
133
|
-
const q = createSendQueue<number, string>({
|
|
134
|
-
maxDepth: 4,
|
|
135
|
-
start: (item) => {
|
|
136
|
-
started.push(item);
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const p = q.enqueue(1, ctl.signal);
|
|
141
|
-
ctl.abort();
|
|
142
|
-
await expect(p).rejects.toBeDefined();
|
|
143
|
-
|
|
144
|
-
q.resolveActive("late");
|
|
145
|
-
expect(q.depth).toBe(0);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("drain rejects active and pending jobs with the given reason", async () => {
|
|
149
|
-
const started: number[] = [];
|
|
150
|
-
const q = createSendQueue<number, string>({
|
|
151
|
-
maxDepth: 4,
|
|
152
|
-
start: (item) => {
|
|
153
|
-
started.push(item);
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const p1 = q.enqueue(1);
|
|
158
|
-
const p2 = q.enqueue(2);
|
|
159
|
-
const reason = new Error("closed");
|
|
160
|
-
|
|
161
|
-
q.drain(reason);
|
|
162
|
-
await expect(p1).rejects.toBe(reason);
|
|
163
|
-
await expect(p2).rejects.toBe(reason);
|
|
164
|
-
expect(q.depth).toBe(0);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("depth reflects active + pending", () => {
|
|
168
|
-
const started: number[] = [];
|
|
169
|
-
const q = createSendQueue<number, string>({
|
|
170
|
-
maxDepth: 4,
|
|
171
|
-
start: (item) => {
|
|
172
|
-
started.push(item);
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
expect(q.depth).toBe(0);
|
|
177
|
-
void q.enqueue(1);
|
|
178
|
-
expect(q.depth).toBe(1);
|
|
179
|
-
void q.enqueue(2);
|
|
180
|
-
expect(q.depth).toBe(2);
|
|
181
|
-
|
|
182
|
-
q.resolveActive("r1");
|
|
183
|
-
expect(q.depth).toBe(1);
|
|
184
|
-
q.resolveActive("r2");
|
|
185
|
-
expect(q.depth).toBe(0);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("capacity check counts the abandoned active slot", async () => {
|
|
189
|
-
const ctl = new AbortController();
|
|
190
|
-
const started: number[] = [];
|
|
191
|
-
const q = createSendQueue<number, string>({
|
|
192
|
-
maxDepth: 2,
|
|
193
|
-
start: (item) => {
|
|
194
|
-
started.push(item);
|
|
195
|
-
},
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
const p1 = q.enqueue(1, ctl.signal);
|
|
199
|
-
void q.enqueue(2);
|
|
200
|
-
ctl.abort();
|
|
201
|
-
await expect(p1).rejects.toBeDefined();
|
|
202
|
-
|
|
203
|
-
// Active slot still occupied (consumer has not advanced). Adding a
|
|
204
|
-
// third would exceed maxDepth=2.
|
|
205
|
-
expect(() => q.enqueue(3)).toThrow(SendQueueFullError);
|
|
206
|
-
});
|
|
207
|
-
});
|
package/src/send-queue.ts
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
// FIFO queue for serializing send() calls against a single reactor.
|
|
2
|
-
//
|
|
3
|
-
// The agent processes one reactor cycle at a time, so concurrent send()
|
|
4
|
-
// callers are queued. Each queued item carries the caller's resolve/reject
|
|
5
|
-
// hooks and an optional AbortSignal:
|
|
6
|
-
//
|
|
7
|
-
// - If the signal is already aborted when enqueue() is called the queue
|
|
8
|
-
// rejects synchronously without enqueueing.
|
|
9
|
-
// - If the signal fires while the item is still queued the item is
|
|
10
|
-
// removed and rejected.
|
|
11
|
-
// - If the signal fires while the item is active the caller-facing
|
|
12
|
-
// promise rejects immediately, but the reactor cycle continues in the
|
|
13
|
-
// background. The queue does not start the next item until the consumer
|
|
14
|
-
// reports the cycle done via resolveActive/rejectActive. This keeps the
|
|
15
|
-
// queue ordered against actual reactor cycles — two send() promises
|
|
16
|
-
// cannot interleave at the reactor level.
|
|
17
|
-
//
|
|
18
|
-
// Queue depth (active + pending) is bounded by `maxDepth`; exceeding it
|
|
19
|
-
// throws `SendQueueFullError` synchronously from enqueue() so a buggy
|
|
20
|
-
// caller flooding sends fails loud instead of silently buffering.
|
|
21
|
-
|
|
22
|
-
export class SendQueueFullError extends Error {
|
|
23
|
-
readonly maxDepth: number;
|
|
24
|
-
|
|
25
|
-
constructor(maxDepth: number) {
|
|
26
|
-
super(`send queue is full (max depth ${String(maxDepth)})`);
|
|
27
|
-
this.name = "SendQueueFullError";
|
|
28
|
-
this.maxDepth = maxDepth;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type Job<T, R> = {
|
|
33
|
-
item: T;
|
|
34
|
-
signal?: AbortSignal;
|
|
35
|
-
abortHandler?: () => void;
|
|
36
|
-
resolve: (value: R) => void;
|
|
37
|
-
reject: (reason: unknown) => void;
|
|
38
|
-
/**
|
|
39
|
-
* True once the caller-facing promise has been settled (resolve or
|
|
40
|
-
* reject). Subsequent settles are no-ops. The active slot may remain
|
|
41
|
-
* occupied after a settle when the caller aborted mid-cycle — the queue
|
|
42
|
-
* waits for the consumer's resolveActive/rejectActive before pumping the
|
|
43
|
-
* next item.
|
|
44
|
-
*/
|
|
45
|
-
settled: boolean;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export type SendQueueOptions<T> = {
|
|
49
|
-
maxDepth: number;
|
|
50
|
-
/**
|
|
51
|
-
* Called when a job moves from pending to active. The consumer drives
|
|
52
|
-
* the underlying work and must eventually call `resolveActive` or
|
|
53
|
-
* `rejectActive` exactly once.
|
|
54
|
-
*/
|
|
55
|
-
start: (item: T) => void;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export type SendQueue<T, R> = {
|
|
59
|
-
enqueue(item: T, signal?: AbortSignal): Promise<R>;
|
|
60
|
-
/** Mark the active job complete with success and pump the next. */
|
|
61
|
-
resolveActive(value: R): void;
|
|
62
|
-
/** Mark the active job complete with failure and pump the next. */
|
|
63
|
-
rejectActive(reason: unknown): void;
|
|
64
|
-
/** Reject the active job (if any) and every pending job with `reason`. */
|
|
65
|
-
drain(reason: unknown): void;
|
|
66
|
-
/** Current pending count (queued + active). */
|
|
67
|
-
readonly depth: number;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
function abortReason(signal: AbortSignal): unknown {
|
|
71
|
-
return signal.reason ?? new DOMException("aborted", "AbortError");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function createSendQueue<T, R>(
|
|
75
|
-
opts: SendQueueOptions<T>,
|
|
76
|
-
): SendQueue<T, R> {
|
|
77
|
-
const pending: Job<T, R>[] = [];
|
|
78
|
-
let active: Job<T, R> | null = null;
|
|
79
|
-
|
|
80
|
-
function settle(
|
|
81
|
-
job: Job<T, R>,
|
|
82
|
-
kind: "resolve" | "reject",
|
|
83
|
-
value: unknown,
|
|
84
|
-
): void {
|
|
85
|
-
if (job.settled) return;
|
|
86
|
-
job.settled = true;
|
|
87
|
-
if (job.abortHandler !== undefined && job.signal !== undefined) {
|
|
88
|
-
job.signal.removeEventListener("abort", job.abortHandler);
|
|
89
|
-
}
|
|
90
|
-
if (kind === "resolve") {
|
|
91
|
-
// The queue's value type is checked at enqueue / resolveActive; the
|
|
92
|
-
// generic narrowing here is safe by construction.
|
|
93
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- generic resolve value
|
|
94
|
-
job.resolve(value as R);
|
|
95
|
-
} else {
|
|
96
|
-
job.reject(value);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function pump(): void {
|
|
101
|
-
while (active === null && pending.length > 0) {
|
|
102
|
-
const next = pending.shift();
|
|
103
|
-
if (next === undefined) return;
|
|
104
|
-
if (next.signal?.aborted === true) {
|
|
105
|
-
settle(next, "reject", abortReason(next.signal));
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
active = next;
|
|
109
|
-
opts.start(next.item);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function enqueue(item: T, signal?: AbortSignal): Promise<R> {
|
|
115
|
-
if (signal?.aborted === true) {
|
|
116
|
-
return Promise.reject(abortReason(signal));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const depth = pending.length + (active !== null ? 1 : 0);
|
|
120
|
-
if (depth >= opts.maxDepth) {
|
|
121
|
-
throw new SendQueueFullError(opts.maxDepth);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
let resolve!: (value: R) => void;
|
|
125
|
-
let reject!: (reason: unknown) => void;
|
|
126
|
-
const promise = new Promise<R>((res, rej) => {
|
|
127
|
-
resolve = res;
|
|
128
|
-
reject = rej;
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const job: Job<T, R> = {
|
|
132
|
-
item,
|
|
133
|
-
...(signal !== undefined ? { signal } : {}),
|
|
134
|
-
resolve,
|
|
135
|
-
reject,
|
|
136
|
-
settled: false,
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
if (signal !== undefined) {
|
|
140
|
-
const handler = (): void => {
|
|
141
|
-
const reason = abortReason(signal);
|
|
142
|
-
if (active === job) {
|
|
143
|
-
// In flight: settle the caller now; the consumer will eventually
|
|
144
|
-
// call resolveActive/rejectActive which becomes a no-op and
|
|
145
|
-
// advances the queue.
|
|
146
|
-
settle(job, "reject", reason);
|
|
147
|
-
} else {
|
|
148
|
-
const idx = pending.indexOf(job);
|
|
149
|
-
if (idx >= 0) pending.splice(idx, 1);
|
|
150
|
-
settle(job, "reject", reason);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
signal.addEventListener("abort", handler, { once: true });
|
|
154
|
-
job.abortHandler = handler;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
pending.push(job);
|
|
158
|
-
pump();
|
|
159
|
-
return promise;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function resolveActive(value: R): void {
|
|
163
|
-
if (active === null) return;
|
|
164
|
-
const job = active;
|
|
165
|
-
active = null;
|
|
166
|
-
settle(job, "resolve", value);
|
|
167
|
-
pump();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function rejectActive(reason: unknown): void {
|
|
171
|
-
if (active === null) return;
|
|
172
|
-
const job = active;
|
|
173
|
-
active = null;
|
|
174
|
-
settle(job, "reject", reason);
|
|
175
|
-
pump();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function drain(reason: unknown): void {
|
|
179
|
-
const drained: Job<T, R>[] = [];
|
|
180
|
-
if (active !== null) {
|
|
181
|
-
drained.push(active);
|
|
182
|
-
active = null;
|
|
183
|
-
}
|
|
184
|
-
drained.push(...pending);
|
|
185
|
-
pending.length = 0;
|
|
186
|
-
for (const job of drained) {
|
|
187
|
-
settle(job, "reject", reason);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
enqueue,
|
|
193
|
-
resolveActive,
|
|
194
|
-
rejectActive,
|
|
195
|
-
drain,
|
|
196
|
-
get depth() {
|
|
197
|
-
return pending.length + (active !== null ? 1 : 0);
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|
package/src/source.test.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import type { InferenceSource } from "@intx/types/runtime";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
createSourceRegistry,
|
|
7
|
-
InvalidInferenceSourceError,
|
|
8
|
-
SourceNotFoundError,
|
|
9
|
-
} from "./source";
|
|
10
|
-
|
|
11
|
-
const S_ANTHROPIC: InferenceSource = {
|
|
12
|
-
id: "anthropic:claude-3-5-sonnet",
|
|
13
|
-
provider: "anthropic",
|
|
14
|
-
baseURL: "https://api.anthropic.com",
|
|
15
|
-
apiKey: "sk-anthropic-1",
|
|
16
|
-
model: "claude-3-5-sonnet",
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const S_OPENAI: InferenceSource = {
|
|
20
|
-
id: "openai:gpt-4o",
|
|
21
|
-
provider: "openai",
|
|
22
|
-
baseURL: "https://api.openai.com",
|
|
23
|
-
apiKey: "sk-openai-1",
|
|
24
|
-
model: "gpt-4o",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Test helper: produce an intentionally-invalid `InferenceSource` value so
|
|
29
|
-
* we can verify that runtime validation rejects it. The whole point is to
|
|
30
|
-
* exercise the arktype check, which requires bypassing the static type.
|
|
31
|
-
*/
|
|
32
|
-
function invalidSource(value: unknown): InferenceSource {
|
|
33
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- intentional invalid input for arktype validation test
|
|
34
|
-
return value as InferenceSource;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("createSourceRegistry", () => {
|
|
38
|
-
test("selects the source whose id matches defaultSource", () => {
|
|
39
|
-
const reg = createSourceRegistry({
|
|
40
|
-
sources: [S_ANTHROPIC, S_OPENAI],
|
|
41
|
-
defaultSource: "openai:gpt-4o",
|
|
42
|
-
});
|
|
43
|
-
expect(reg.active.provider).toBe("openai");
|
|
44
|
-
expect(reg.active.model).toBe("gpt-4o");
|
|
45
|
-
expect(reg.active.apiKey).toBe("sk-openai-1");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("rejects an empty sources[] array", () => {
|
|
49
|
-
expect(() =>
|
|
50
|
-
createSourceRegistry({ sources: [], defaultSource: "anything" }),
|
|
51
|
-
).toThrow(InvalidInferenceSourceError);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("rejects sources that fail InferenceSource arktype validation", () => {
|
|
55
|
-
expect(() =>
|
|
56
|
-
createSourceRegistry({
|
|
57
|
-
sources: [invalidSource({ id: "x", provider: "x", apiKey: "k" })],
|
|
58
|
-
defaultSource: "anything",
|
|
59
|
-
}),
|
|
60
|
-
).toThrow(InvalidInferenceSourceError);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("rejects a source entry missing model", () => {
|
|
64
|
-
const noModel = invalidSource({
|
|
65
|
-
id: "anthropic:none",
|
|
66
|
-
provider: "anthropic",
|
|
67
|
-
baseURL: "u",
|
|
68
|
-
apiKey: "k",
|
|
69
|
-
});
|
|
70
|
-
expect(() =>
|
|
71
|
-
createSourceRegistry({
|
|
72
|
-
sources: [noModel],
|
|
73
|
-
defaultSource: "anthropic:none",
|
|
74
|
-
}),
|
|
75
|
-
).toThrow(InvalidInferenceSourceError);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("rejects sources[] with duplicate ids", () => {
|
|
79
|
-
expect(() =>
|
|
80
|
-
createSourceRegistry({
|
|
81
|
-
sources: [S_ANTHROPIC, { ...S_ANTHROPIC, apiKey: "sk-other" }],
|
|
82
|
-
defaultSource: S_ANTHROPIC.id,
|
|
83
|
-
}),
|
|
84
|
-
).toThrow(InvalidInferenceSourceError);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("throws SourceNotFoundError when defaultSource matches no source", () => {
|
|
88
|
-
expect(() =>
|
|
89
|
-
createSourceRegistry({
|
|
90
|
-
sources: [S_ANTHROPIC],
|
|
91
|
-
defaultSource: "openai:gpt-4o",
|
|
92
|
-
}),
|
|
93
|
-
).toThrow(SourceNotFoundError);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("active is a mutable holder; setSource mutates fields in place", () => {
|
|
97
|
-
const reg = createSourceRegistry({
|
|
98
|
-
sources: [S_ANTHROPIC],
|
|
99
|
-
defaultSource: "anthropic:claude-3-5-sonnet",
|
|
100
|
-
});
|
|
101
|
-
const reference = reg.active;
|
|
102
|
-
|
|
103
|
-
reg.setSource({
|
|
104
|
-
id: "anthropic:claude-3-5-haiku",
|
|
105
|
-
provider: "anthropic",
|
|
106
|
-
baseURL: "https://proxy.example.com",
|
|
107
|
-
apiKey: "sk-new",
|
|
108
|
-
model: "claude-3-5-haiku",
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
expect(reg.active).toBe(reference);
|
|
112
|
-
expect(reg.active.id).toBe("anthropic:claude-3-5-haiku");
|
|
113
|
-
expect(reg.active.provider).toBe("anthropic");
|
|
114
|
-
expect(reg.active.baseURL).toBe("https://proxy.example.com");
|
|
115
|
-
expect(reg.active.apiKey).toBe("sk-new");
|
|
116
|
-
expect(reg.active.model).toBe("claude-3-5-haiku");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("setSource overwrites defaults and capabilities, including deletion", () => {
|
|
120
|
-
const reg = createSourceRegistry({
|
|
121
|
-
sources: [
|
|
122
|
-
{
|
|
123
|
-
...S_ANTHROPIC,
|
|
124
|
-
defaults: { maxTokens: 1024 },
|
|
125
|
-
capabilities: ["text"],
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
defaultSource: S_ANTHROPIC.id,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
reg.setSource({
|
|
132
|
-
...S_ANTHROPIC,
|
|
133
|
-
defaults: { maxTokens: 4096 },
|
|
134
|
-
capabilities: ["text", "vision"],
|
|
135
|
-
});
|
|
136
|
-
expect(reg.active.defaults).toEqual({ maxTokens: 4096 });
|
|
137
|
-
expect(reg.active.capabilities).toEqual(["text", "vision"]);
|
|
138
|
-
|
|
139
|
-
reg.setSource(S_ANTHROPIC);
|
|
140
|
-
expect(reg.active.defaults).toBeUndefined();
|
|
141
|
-
expect(reg.active.capabilities).toBeUndefined();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("setSource throws InvalidInferenceSourceError on invalid input", () => {
|
|
145
|
-
const reg = createSourceRegistry({
|
|
146
|
-
sources: [S_ANTHROPIC],
|
|
147
|
-
defaultSource: S_ANTHROPIC.id,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
expect(() => reg.setSource(invalidSource({ provider: "x" }))).toThrow(
|
|
151
|
-
InvalidInferenceSourceError,
|
|
152
|
-
);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("does not mutate the caller's sources[] entries", () => {
|
|
156
|
-
const inputs: InferenceSource[] = [{ ...S_ANTHROPIC }];
|
|
157
|
-
const reg = createSourceRegistry({
|
|
158
|
-
sources: inputs,
|
|
159
|
-
defaultSource: S_ANTHROPIC.id,
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
reg.setSource({
|
|
163
|
-
...S_ANTHROPIC,
|
|
164
|
-
baseURL: "https://other.example.com",
|
|
165
|
-
apiKey: "sk-other",
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
expect(inputs[0]?.apiKey).toBe("sk-anthropic-1");
|
|
169
|
-
expect(inputs[0]?.baseURL).toBe("https://api.anthropic.com");
|
|
170
|
-
});
|
|
171
|
-
});
|
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
|
-
}
|