@dex-ai/sdk 0.1.30
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/README.md +308 -0
- package/dist/agent.d.ts +181 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +41 -0
- package/dist/agent.js.map +1 -0
- package/dist/context.d.ts +68 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +8 -0
- package/dist/context.js.map +1 -0
- package/dist/create-agent.d.ts +7 -0
- package/dist/create-agent.d.ts.map +1 -0
- package/dist/create-agent.js +205 -0
- package/dist/create-agent.js.map +1 -0
- package/dist/extension.d.ts +162 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +20 -0
- package/dist/extension.js.map +1 -0
- package/dist/generate.d.ts +10 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +839 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/message.d.ts +89 -0
- package/dist/message.d.ts.map +1 -0
- package/dist/message.js +17 -0
- package/dist/message.js.map +1 -0
- package/dist/messages.d.ts +98 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +339 -0
- package/dist/messages.js.map +1 -0
- package/dist/model.d.ts +39 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +11 -0
- package/dist/model.js.map +1 -0
- package/dist/provider.d.ts +157 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +39 -0
- package/dist/provider.js.map +1 -0
- package/dist/resolve-schema.d.ts +44 -0
- package/dist/resolve-schema.d.ts.map +1 -0
- package/dist/resolve-schema.js +367 -0
- package/dist/resolve-schema.js.map +1 -0
- package/dist/schema.d.ts +80 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +90 -0
- package/dist/schema.js.map +1 -0
- package/dist/tool-dispatch.d.ts +24 -0
- package/dist/tool-dispatch.d.ts.map +1 -0
- package/dist/tool-dispatch.js +120 -0
- package/dist/tool-dispatch.js.map +1 -0
- package/dist/tool-result-cache.d.ts +43 -0
- package/dist/tool-result-cache.d.ts.map +1 -0
- package/dist/tool-result-cache.js +118 -0
- package/dist/tool-result-cache.js.map +1 -0
- package/dist/tool.d.ts +96 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +29 -0
- package/dist/tool.js.map +1 -0
- package/dist/util.d.ts +26 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +104 -0
- package/dist/util.js.map +1 -0
- package/package.json +41 -0
- package/src/agent.ts +235 -0
- package/src/context.ts +82 -0
- package/src/create-agent.ts +237 -0
- package/src/extension.ts +244 -0
- package/src/generate.ts +943 -0
- package/src/index.ts +113 -0
- package/src/message.ts +114 -0
- package/src/messages.test.ts +299 -0
- package/src/messages.ts +423 -0
- package/src/model.ts +43 -0
- package/src/provider.ts +187 -0
- package/src/resolve-schema.test.ts +351 -0
- package/src/resolve-schema.ts +426 -0
- package/src/schema.ts +131 -0
- package/src/tool-dispatch.ts +166 -0
- package/src/tool-result-cache.test.ts +182 -0
- package/src/tool-result-cache.ts +164 -0
- package/src/tool.ts +110 -0
- package/src/util.ts +110 -0
package/src/messages.ts
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages — a guarded, ordered collection of conversation messages.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the internal message array with controlled mutation methods and
|
|
5
|
+
* validates structural invariants on every write. Direct array mutation
|
|
6
|
+
* (via index assignment, push, splice, etc.) is trapped by a Proxy and
|
|
7
|
+
* throws at runtime — extensions MUST use the provided mutation API.
|
|
8
|
+
*
|
|
9
|
+
* Invariants enforced:
|
|
10
|
+
* 1. System messages appear only at the front (contiguous prefix).
|
|
11
|
+
* 2. First non-system message has role "user".
|
|
12
|
+
* 3. Role alternation: assistant follows user, tool follows assistant.
|
|
13
|
+
* 4. Every tool-call must have a matching tool-result before the next user turn.
|
|
14
|
+
* 5. No orphaned tool-result without a preceding tool-call.
|
|
15
|
+
* 6. Every message has an `id`.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: Trailing tool-calls (at the end of the sequence) are allowed — the
|
|
18
|
+
* generate loop commits the assistant message before dispatching tools.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Content, Message, Role, ToolCallContent } from "./message";
|
|
22
|
+
|
|
23
|
+
/* ------------------------------------------------------------------ */
|
|
24
|
+
/* Validation */
|
|
25
|
+
/* ------------------------------------------------------------------ */
|
|
26
|
+
|
|
27
|
+
export interface ValidationError {
|
|
28
|
+
readonly index: number;
|
|
29
|
+
readonly message: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate the full message sequence. Returns an empty array if valid.
|
|
34
|
+
*/
|
|
35
|
+
export function validateMessages(
|
|
36
|
+
messages: ReadonlyArray<Message>,
|
|
37
|
+
): ValidationError[] {
|
|
38
|
+
const errors: ValidationError[] = [];
|
|
39
|
+
|
|
40
|
+
if (messages.length === 0) return errors;
|
|
41
|
+
|
|
42
|
+
// 1. System messages only at front.
|
|
43
|
+
let systemEnd = 0;
|
|
44
|
+
for (let i = 0; i < messages.length; i++) {
|
|
45
|
+
if (messages[i]!.role === "system") {
|
|
46
|
+
if (i !== systemEnd) {
|
|
47
|
+
errors.push({
|
|
48
|
+
index: i,
|
|
49
|
+
message: `System message at index ${i} after non-system message at index ${systemEnd}`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
systemEnd = i + 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 2. First non-system message must be user role.
|
|
57
|
+
if (systemEnd < messages.length) {
|
|
58
|
+
const first = messages[systemEnd]!;
|
|
59
|
+
if (first.role !== "user") {
|
|
60
|
+
errors.push({
|
|
61
|
+
index: systemEnd,
|
|
62
|
+
message: `First non-system message must be "user", got "${first.role}"`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Role alternation + tool-call/tool-result pairing.
|
|
68
|
+
let pendingToolCalls = new Map<string, number>(); // toolCallId → assistant msg index
|
|
69
|
+
|
|
70
|
+
for (let i = systemEnd; i < messages.length; i++) {
|
|
71
|
+
const msg = messages[i]!;
|
|
72
|
+
const prev = i > systemEnd ? messages[i - 1]! : null;
|
|
73
|
+
|
|
74
|
+
switch (msg.role) {
|
|
75
|
+
case "user": {
|
|
76
|
+
// user can follow: nothing (first), assistant, or tool
|
|
77
|
+
if (prev && prev.role !== "assistant" && prev.role !== "tool") {
|
|
78
|
+
errors.push({
|
|
79
|
+
index: i,
|
|
80
|
+
message: `"user" message follows "${prev.role}" (expected after "assistant" or "tool")`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// If we still have pending tool calls from a previous assistant, that's an error
|
|
84
|
+
if (pendingToolCalls.size > 0) {
|
|
85
|
+
for (const [id, idx] of pendingToolCalls) {
|
|
86
|
+
errors.push({
|
|
87
|
+
index: idx,
|
|
88
|
+
message: `Orphaned tool-call "${id}" — no matching tool-result before next user turn`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
pendingToolCalls = new Map();
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "assistant": {
|
|
96
|
+
// assistant follows user or tool
|
|
97
|
+
if (prev && prev.role !== "user" && prev.role !== "tool") {
|
|
98
|
+
errors.push({
|
|
99
|
+
index: i,
|
|
100
|
+
message: `"assistant" message follows "${prev.role}" (expected after "user" or "tool")`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Collect tool calls
|
|
104
|
+
for (const c of msg.content) {
|
|
105
|
+
if (c.type === "tool-call") {
|
|
106
|
+
pendingToolCalls.set(
|
|
107
|
+
(c as ToolCallContent).toolCallId,
|
|
108
|
+
i,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "tool": {
|
|
115
|
+
// tool follows assistant or tool
|
|
116
|
+
if (prev && prev.role !== "assistant" && prev.role !== "tool") {
|
|
117
|
+
errors.push({
|
|
118
|
+
index: i,
|
|
119
|
+
message: `"tool" message follows "${prev.role}" (expected after "assistant" or "tool")`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Match tool-results to pending tool-calls
|
|
123
|
+
for (const c of msg.content) {
|
|
124
|
+
if (c.type === "tool-result") {
|
|
125
|
+
const id = c.toolCallId;
|
|
126
|
+
if (!pendingToolCalls.has(id)) {
|
|
127
|
+
errors.push({
|
|
128
|
+
index: i,
|
|
129
|
+
message: `Orphaned tool-result "${id}" — no matching tool-call`,
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
pendingToolCalls.delete(id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// NOTE: We intentionally do NOT flag pending tool-calls at the end of the
|
|
142
|
+
// sequence. The generate loop commits the assistant message (with tool-calls)
|
|
143
|
+
// before dispatching tools and committing their results. This intermediate
|
|
144
|
+
// state is valid — the tool-results will arrive on subsequent appends.
|
|
145
|
+
// The real invariant violation (tool-calls that never get results) is caught
|
|
146
|
+
// at line 81-89 when a "user" message arrives while tool-calls are still
|
|
147
|
+
// pending.
|
|
148
|
+
|
|
149
|
+
return errors;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ------------------------------------------------------------------ */
|
|
153
|
+
/* Messages class */
|
|
154
|
+
/* ------------------------------------------------------------------ */
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Mutation mode controls when validation runs:
|
|
158
|
+
* - "strict": validate after every mutation, throw on failure
|
|
159
|
+
* - "batch": defer validation until `commit()` is called
|
|
160
|
+
*/
|
|
161
|
+
type MutationMode = "strict" | "batch";
|
|
162
|
+
|
|
163
|
+
const TRAPPED_MUTATORS = new Set([
|
|
164
|
+
"push",
|
|
165
|
+
"pop",
|
|
166
|
+
"shift",
|
|
167
|
+
"unshift",
|
|
168
|
+
"splice",
|
|
169
|
+
"sort",
|
|
170
|
+
"reverse",
|
|
171
|
+
"fill",
|
|
172
|
+
"copyWithin",
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
export class Messages {
|
|
176
|
+
private readonly _internal: Message[];
|
|
177
|
+
private readonly _proxy: ReadonlyArray<Message>;
|
|
178
|
+
private _batchDepth = 0;
|
|
179
|
+
|
|
180
|
+
constructor(initial?: ReadonlyArray<Message>) {
|
|
181
|
+
this._internal = initial ? [...initial] : [];
|
|
182
|
+
|
|
183
|
+
// Create a Proxy that traps all mutating operations
|
|
184
|
+
this._proxy = new Proxy(this._internal, {
|
|
185
|
+
set(_target, prop, _value) {
|
|
186
|
+
// Allow length to be read but not set externally
|
|
187
|
+
if (prop === "length") {
|
|
188
|
+
throw new Error(
|
|
189
|
+
"[Messages] Direct array mutation is not allowed. " +
|
|
190
|
+
"Use messages.splice() or messages.append() instead.",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
// Block numeric index assignment
|
|
194
|
+
if (typeof prop === "string" && /^\d+$/.test(prop)) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
"[Messages] Direct index assignment is not allowed. " +
|
|
197
|
+
"Use messages.replace() instead.",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
},
|
|
202
|
+
get(target, prop, receiver) {
|
|
203
|
+
// Trap mutating array methods
|
|
204
|
+
if (typeof prop === "string" && TRAPPED_MUTATORS.has(prop)) {
|
|
205
|
+
return () => {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`[Messages] Direct .${prop}() is not allowed. ` +
|
|
208
|
+
"Use the Messages API (append, splice, replaceRange) instead.",
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return Reflect.get(target, prop, receiver);
|
|
213
|
+
},
|
|
214
|
+
deleteProperty(_target, prop) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`[Messages] Cannot delete index ${String(prop)}. ` +
|
|
217
|
+
"Use messages.splice() instead.",
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
}) as ReadonlyArray<Message>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ---------------------------------------------------------------- */
|
|
224
|
+
/* Read access */
|
|
225
|
+
/* ---------------------------------------------------------------- */
|
|
226
|
+
|
|
227
|
+
/** The proxied read-only view. Safe to expose on AgentContext. */
|
|
228
|
+
get array(): ReadonlyArray<Message> {
|
|
229
|
+
return this._proxy;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
get length(): number {
|
|
233
|
+
return this._internal.length;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
at(index: number): Message | undefined {
|
|
237
|
+
return this._internal.at(index);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ---------------------------------------------------------------- */
|
|
241
|
+
/* Controlled mutations */
|
|
242
|
+
/* ---------------------------------------------------------------- */
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Append one or more messages. Assigns IDs if missing.
|
|
246
|
+
* Validates invariants after append (unless in a batch).
|
|
247
|
+
*/
|
|
248
|
+
append(message: Message | ReadonlyArray<Message>): void {
|
|
249
|
+
const msgs = Array.isArray(message) ? message : [message];
|
|
250
|
+
const prepared: Message[] = [];
|
|
251
|
+
for (const m of msgs) {
|
|
252
|
+
const withId: Message =
|
|
253
|
+
m.id !== undefined ? m : { ...m, id: crypto.randomUUID() };
|
|
254
|
+
prepared.push(withId);
|
|
255
|
+
}
|
|
256
|
+
this._internal.push(...prepared);
|
|
257
|
+
this._validateIfStrict();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Replace a range of messages. Like Array.splice but validates afterward.
|
|
262
|
+
* Returns the removed messages.
|
|
263
|
+
*/
|
|
264
|
+
splice(
|
|
265
|
+
start: number,
|
|
266
|
+
deleteCount: number,
|
|
267
|
+
...items: Message[]
|
|
268
|
+
): Message[] {
|
|
269
|
+
// Assign IDs to inserted items
|
|
270
|
+
const prepared = items.map((m) =>
|
|
271
|
+
m.id !== undefined ? m : { ...m, id: crypto.randomUUID() },
|
|
272
|
+
);
|
|
273
|
+
const removed = this._internal.splice(start, deleteCount, ...prepared);
|
|
274
|
+
this._validateIfStrict();
|
|
275
|
+
return removed;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Replace a contiguous range with new messages.
|
|
280
|
+
* Convenience wrapper over splice.
|
|
281
|
+
*/
|
|
282
|
+
replaceRange(
|
|
283
|
+
start: number,
|
|
284
|
+
end: number,
|
|
285
|
+
replacement: ReadonlyArray<Message>,
|
|
286
|
+
): Message[] {
|
|
287
|
+
const deleteCount = end - start;
|
|
288
|
+
return this.splice(start, deleteCount, ...(replacement as Message[]));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Replace a single message at an index.
|
|
293
|
+
*/
|
|
294
|
+
replace(index: number, message: Message): void {
|
|
295
|
+
if (index < 0 || index >= this._internal.length) {
|
|
296
|
+
throw new RangeError(
|
|
297
|
+
`[Messages] Index ${index} out of bounds (length=${this._internal.length})`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
const withId: Message =
|
|
301
|
+
message.id !== undefined ? message : { ...message, id: crypto.randomUUID() };
|
|
302
|
+
this._internal[index] = withId;
|
|
303
|
+
this._validateIfStrict();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Set the length of the internal array (truncation).
|
|
308
|
+
* Used for reordering during agent creation.
|
|
309
|
+
*/
|
|
310
|
+
truncate(length: number): void {
|
|
311
|
+
if (length < 0) {
|
|
312
|
+
throw new RangeError(`[Messages] Cannot truncate to negative length`);
|
|
313
|
+
}
|
|
314
|
+
this._internal.length = length;
|
|
315
|
+
this._validateIfStrict();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Direct push without validation — used only by the internal
|
|
320
|
+
* reorder step during agent creation where system messages are
|
|
321
|
+
* moved to front. Caller MUST call validate() afterward.
|
|
322
|
+
*/
|
|
323
|
+
_unsafePush(...messages: Message[]): void {
|
|
324
|
+
this._internal.push(...messages);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Set length without validation — used only during agent creation reorder.
|
|
329
|
+
*/
|
|
330
|
+
_unsafeSetLength(length: number): void {
|
|
331
|
+
this._internal.length = length;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* ---------------------------------------------------------------- */
|
|
335
|
+
/* Batch mode */
|
|
336
|
+
/* ---------------------------------------------------------------- */
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Begin a batch — validation is deferred until `commit()`.
|
|
340
|
+
* Batches can nest; validation runs when the outermost batch commits.
|
|
341
|
+
*/
|
|
342
|
+
batch(): void {
|
|
343
|
+
this._batchDepth++;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* End a batch and validate. Throws if invariants are violated.
|
|
348
|
+
* On failure the messages are left as-is (caller must fix or rollback).
|
|
349
|
+
*/
|
|
350
|
+
commit(): void {
|
|
351
|
+
if (this._batchDepth <= 0) {
|
|
352
|
+
throw new Error("[Messages] commit() called without matching batch()");
|
|
353
|
+
}
|
|
354
|
+
this._batchDepth--;
|
|
355
|
+
if (this._batchDepth === 0) {
|
|
356
|
+
this._validate();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Perform a batch operation with automatic commit.
|
|
362
|
+
* If the function throws, the batch is still ended (but not validated).
|
|
363
|
+
*/
|
|
364
|
+
transaction(fn: () => void): void {
|
|
365
|
+
this.batch();
|
|
366
|
+
try {
|
|
367
|
+
fn();
|
|
368
|
+
this.commit();
|
|
369
|
+
} catch (err) {
|
|
370
|
+
this._batchDepth = Math.max(0, this._batchDepth - 1);
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* ---------------------------------------------------------------- */
|
|
376
|
+
/* Validation */
|
|
377
|
+
/* ---------------------------------------------------------------- */
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Explicitly validate the current state. Returns errors or throws.
|
|
381
|
+
*/
|
|
382
|
+
validate(): ValidationError[] {
|
|
383
|
+
return validateMessages(this._internal);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Validate and throw if invalid.
|
|
388
|
+
*/
|
|
389
|
+
assertValid(): void {
|
|
390
|
+
const errors = this.validate();
|
|
391
|
+
if (errors.length > 0) {
|
|
392
|
+
const details = errors
|
|
393
|
+
.slice(0, 5)
|
|
394
|
+
.map((e) => ` [${e.index}] ${e.message}`)
|
|
395
|
+
.join("\n");
|
|
396
|
+
const suffix =
|
|
397
|
+
errors.length > 5 ? `\n ... and ${errors.length - 5} more` : "";
|
|
398
|
+
throw new Error(
|
|
399
|
+
`[Messages] Invalid message sequence (${errors.length} error(s)):\n${details}${suffix}`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private _validateIfStrict(): void {
|
|
405
|
+
if (this._batchDepth > 0) return;
|
|
406
|
+
this._validate();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private _validate(): void {
|
|
410
|
+
const errors = validateMessages(this._internal);
|
|
411
|
+
if (errors.length > 0) {
|
|
412
|
+
const details = errors
|
|
413
|
+
.slice(0, 5)
|
|
414
|
+
.map((e) => ` [${e.index}] ${e.message}`)
|
|
415
|
+
.join("\n");
|
|
416
|
+
const suffix =
|
|
417
|
+
errors.length > 5 ? `\n ... and ${errors.length - 5} more` : "";
|
|
418
|
+
throw new Error(
|
|
419
|
+
`[Messages] Invalid message sequence (${errors.length} error(s)):\n${details}${suffix}`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model — a callable model instance registered by a provider extension.
|
|
3
|
+
*
|
|
4
|
+
* The extension pre-binds connection config (baseUrl, apiKey, protocol logic)
|
|
5
|
+
* into the `stream` method. The loop just calls `model.stream(req)`.
|
|
6
|
+
*
|
|
7
|
+
* Provider packages own the wire translation (SDK shapes ↔ API-specific JSON).
|
|
8
|
+
* The SDK only defines the interface — it never imports protocol-specific code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { StreamPart } from "./provider";
|
|
12
|
+
import type { ModelRequest } from "./provider";
|
|
13
|
+
|
|
14
|
+
export type ThinkingLevel = "off" | "min" | "low" | "med" | "high" | "max";
|
|
15
|
+
|
|
16
|
+
export interface Model {
|
|
17
|
+
/** Model identifier within the provider extension, e.g. 'gpt-4.1'. */
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** Display name for UIs. */
|
|
20
|
+
readonly name?: string;
|
|
21
|
+
/** Maximum context window in tokens. */
|
|
22
|
+
readonly contextWindow?: number;
|
|
23
|
+
/** Maximum output tokens per response. */
|
|
24
|
+
readonly maxTokens?: number;
|
|
25
|
+
/** Whether this model supports extended thinking / chain-of-thought. */
|
|
26
|
+
readonly reasoning?: boolean;
|
|
27
|
+
/** Available thinking levels for this model. Empty or undefined means no thinking support. */
|
|
28
|
+
readonly thinkingLevels?: ReadonlyArray<ThinkingLevel>;
|
|
29
|
+
/** Supported input modalities. */
|
|
30
|
+
readonly input?: ReadonlyArray<"text" | "image">;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Stream a completion. The provider extension pre-binds auth, baseUrl, and
|
|
34
|
+
* protocol translation into this method. The loop calls it directly.
|
|
35
|
+
*/
|
|
36
|
+
stream(req: ModelRequest): AsyncIterable<StreamPart>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional: verify the model is reachable. Throws on failure.
|
|
40
|
+
* Provider extensions can implement this by hitting a lightweight endpoint.
|
|
41
|
+
*/
|
|
42
|
+
ping?(): Promise<void>;
|
|
43
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider — the model boundary.
|
|
3
|
+
*
|
|
4
|
+
* Typed, streaming-first (ai-sdk lean). StreamPart is what the PROVIDER
|
|
5
|
+
* emits — provider-only, no synthetic parts. Every provider-level span
|
|
6
|
+
* has a start/stop pair so tracers can build spans from the stream:
|
|
7
|
+
*
|
|
8
|
+
* response-start ── begins a provider call
|
|
9
|
+
* message-start ── begins the assistant message
|
|
10
|
+
* text-delta
|
|
11
|
+
* reasoning-delta
|
|
12
|
+
* tool-call-delta
|
|
13
|
+
* tool-call
|
|
14
|
+
* message-stop
|
|
15
|
+
* finish ── terminal accounting for this response
|
|
16
|
+
* response-stop ── ends a provider call
|
|
17
|
+
*
|
|
18
|
+
* raw-chunk ── opaque wire payload (optional)
|
|
19
|
+
* error, abort ── control
|
|
20
|
+
*
|
|
21
|
+
* Loop-level lifecycle (iteration boundaries, tool-execute spans) is NOT
|
|
22
|
+
* part of StreamPart. That flows through extension hooks only.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { Message } from "./message";
|
|
26
|
+
import type { AnyTool } from "./tool";
|
|
27
|
+
import type { ThinkingLevel } from "./model";
|
|
28
|
+
|
|
29
|
+
export type FinishReason =
|
|
30
|
+
| "stop"
|
|
31
|
+
| "length"
|
|
32
|
+
| "tool-calls"
|
|
33
|
+
| "content-filter"
|
|
34
|
+
| "error"
|
|
35
|
+
| "abort";
|
|
36
|
+
|
|
37
|
+
export interface Usage {
|
|
38
|
+
readonly inputTokens: number;
|
|
39
|
+
readonly outputTokens: number;
|
|
40
|
+
readonly totalTokens?: number;
|
|
41
|
+
readonly cachedInputTokens?: number;
|
|
42
|
+
readonly cacheCreationInputTokens?: number;
|
|
43
|
+
readonly reasoningTokens?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ToolChoice =
|
|
47
|
+
| "auto"
|
|
48
|
+
| "required"
|
|
49
|
+
| "none"
|
|
50
|
+
| { readonly toolName: string };
|
|
51
|
+
|
|
52
|
+
export interface ModelRequest {
|
|
53
|
+
readonly messages: ReadonlyArray<Message>;
|
|
54
|
+
readonly tools?: ReadonlyArray<AnyTool>;
|
|
55
|
+
readonly toolChoice?: ToolChoice;
|
|
56
|
+
readonly temperature?: number;
|
|
57
|
+
readonly topP?: number;
|
|
58
|
+
readonly maxTokens?: number;
|
|
59
|
+
readonly stopSequences?: ReadonlyArray<string>;
|
|
60
|
+
readonly seed?: number;
|
|
61
|
+
readonly signal?: AbortSignal;
|
|
62
|
+
readonly providerOptions?: Readonly<Record<string, unknown>>;
|
|
63
|
+
/**
|
|
64
|
+
* Enable extended thinking / chain-of-thought reasoning.
|
|
65
|
+
* Pass a ThinkingLevel or `{ budgetTokens: N }` for explicit budget.
|
|
66
|
+
* The provider maps levels to token budgets.
|
|
67
|
+
*/
|
|
68
|
+
readonly thinking?: ThinkingLevel | { readonly budgetTokens: number };
|
|
69
|
+
/**
|
|
70
|
+
* Cache breakpoint hints for providers that support explicit prompt caching.
|
|
71
|
+
* Each entry is an index into `messages` whose last content block should be
|
|
72
|
+
* marked as a cache breakpoint. Providers that don't support explicit caching
|
|
73
|
+
* (e.g. OpenAI, which auto-caches by prefix) ignore this field.
|
|
74
|
+
*/
|
|
75
|
+
readonly cacheBreakpoints?: ReadonlyArray<number>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ResponseMeta {
|
|
79
|
+
readonly id?: string;
|
|
80
|
+
readonly providerName: string;
|
|
81
|
+
readonly modelId: string;
|
|
82
|
+
readonly startedAt: number;
|
|
83
|
+
readonly endedAt?: number;
|
|
84
|
+
readonly providerMetadata?: Readonly<Record<string, unknown>>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* -------------------------- StreamPart (provider-emitted) ---------- */
|
|
88
|
+
|
|
89
|
+
export type ResponseStartPart = {
|
|
90
|
+
readonly type: "response-start";
|
|
91
|
+
readonly meta: ResponseMeta;
|
|
92
|
+
};
|
|
93
|
+
export type ResponseStopPart = {
|
|
94
|
+
readonly type: "response-stop";
|
|
95
|
+
readonly meta: ResponseMeta;
|
|
96
|
+
readonly usage: Usage;
|
|
97
|
+
readonly finishReason: FinishReason;
|
|
98
|
+
};
|
|
99
|
+
export type MessageStartPart = {
|
|
100
|
+
readonly type: "message-start";
|
|
101
|
+
readonly role: "assistant";
|
|
102
|
+
};
|
|
103
|
+
export type MessageStopPart = {
|
|
104
|
+
readonly type: "message-stop";
|
|
105
|
+
readonly message: Message;
|
|
106
|
+
};
|
|
107
|
+
export type TextDeltaPart = {
|
|
108
|
+
readonly type: "text-delta";
|
|
109
|
+
readonly delta: string;
|
|
110
|
+
};
|
|
111
|
+
export type ReasoningDeltaPart = {
|
|
112
|
+
readonly type: "reasoning-delta";
|
|
113
|
+
readonly delta: string;
|
|
114
|
+
};
|
|
115
|
+
export type ToolCallDeltaPart = {
|
|
116
|
+
readonly type: "tool-call-delta";
|
|
117
|
+
readonly toolCallId: string;
|
|
118
|
+
readonly toolName: string;
|
|
119
|
+
readonly inputDelta: string;
|
|
120
|
+
};
|
|
121
|
+
export type ToolCallPart = {
|
|
122
|
+
readonly type: "tool-call";
|
|
123
|
+
readonly toolCallId: string;
|
|
124
|
+
readonly toolName: string;
|
|
125
|
+
readonly input: unknown;
|
|
126
|
+
};
|
|
127
|
+
export type FinishPart = {
|
|
128
|
+
readonly type: "finish";
|
|
129
|
+
readonly reason: FinishReason;
|
|
130
|
+
readonly usage: Usage;
|
|
131
|
+
};
|
|
132
|
+
export type RawChunkPart = {
|
|
133
|
+
readonly type: "raw-chunk";
|
|
134
|
+
readonly providerName: string;
|
|
135
|
+
readonly data: unknown;
|
|
136
|
+
};
|
|
137
|
+
export type ErrorPart = {
|
|
138
|
+
readonly type: "error";
|
|
139
|
+
readonly error: unknown;
|
|
140
|
+
readonly recoverable?: boolean;
|
|
141
|
+
};
|
|
142
|
+
export type AbortPart = { readonly type: "abort"; readonly reason?: unknown };
|
|
143
|
+
|
|
144
|
+
export type StreamPart =
|
|
145
|
+
| ResponseStartPart
|
|
146
|
+
| ResponseStopPart
|
|
147
|
+
| MessageStartPart
|
|
148
|
+
| MessageStopPart
|
|
149
|
+
| TextDeltaPart
|
|
150
|
+
| ReasoningDeltaPart
|
|
151
|
+
| ToolCallDeltaPart
|
|
152
|
+
| ToolCallPart
|
|
153
|
+
| FinishPart
|
|
154
|
+
| RawChunkPart
|
|
155
|
+
| ErrorPart
|
|
156
|
+
| AbortPart;
|
|
157
|
+
|
|
158
|
+
export interface ModelResponse {
|
|
159
|
+
readonly message: Message;
|
|
160
|
+
readonly usage: Usage;
|
|
161
|
+
readonly finishReason: FinishReason;
|
|
162
|
+
readonly meta: ResponseMeta;
|
|
163
|
+
readonly raw?: unknown;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface Provider {
|
|
167
|
+
readonly name: string;
|
|
168
|
+
readonly modelId: string;
|
|
169
|
+
generate(req: ModelRequest): Promise<ModelResponse>;
|
|
170
|
+
stream(req: ModelRequest): AsyncIterable<StreamPart>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Provider namespace — identity-typed declaration helper.
|
|
175
|
+
*
|
|
176
|
+
* Use `Provider.define({...})` when implementing a custom provider to
|
|
177
|
+
* get full contextual typing. Runtime behavior: returns the argument
|
|
178
|
+
* unchanged. Note that concrete provider factories (e.g. `openai()`
|
|
179
|
+
* from `@dex-ai/openai`) are separate; this helper is only useful
|
|
180
|
+
* when authoring your own provider.
|
|
181
|
+
*/
|
|
182
|
+
// eslint-disable-next-line @typescript-eslint/no-redeclare
|
|
183
|
+
export const Provider = {
|
|
184
|
+
define<P extends Provider>(provider: P): P {
|
|
185
|
+
return provider;
|
|
186
|
+
},
|
|
187
|
+
};
|