@betterlu/agentlu 1.4.0 → 1.6.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/README.md +65 -3
- package/dist/index.d.ts +146 -19
- package/dist/index.js +348 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ const client = new OpenAI({ baseURL: "http://localhost:8912/v1", apiKey });
|
|
|
10
10
|
const r = await client.responses.create({ model: "fast", input: "hi" });
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
This client adds get-or-create registration, typed `runJson`, and auto-retry on `409 thread_busy`.
|
|
13
|
+
This client adds get-or-create registration, typed `runJson` / `runText`, use-case catalogs, background `wait`, client-side `timeoutMs`, and auto-retry on `409 thread_busy`.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -75,6 +75,48 @@ const { data } = await mealEnricher.runJson<{ meal: string }>({ input: "two eggs
|
|
|
75
75
|
|
|
76
76
|
Set a field on `run` or `runJson` to override its registered default. `strict: false` still requires parsed JSON but does not require that value to match the schema.
|
|
77
77
|
|
|
78
|
+
Do **not** put `maxOutputTokens` on client defaults — the server ignores it. Pass `max_output_tokens` (or `maxOutputTokens`) on the run.
|
|
79
|
+
|
|
80
|
+
Several use-cases sharing one client:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
const ai = alu.useCases({
|
|
84
|
+
extract: {
|
|
85
|
+
name: "myapp-extract",
|
|
86
|
+
defaults: { agent: "api", store: false, timeoutMs: 45_000, instructions: "Parse into JSON." },
|
|
87
|
+
},
|
|
88
|
+
tag: { defaults: { agent: "api", store: false } },
|
|
89
|
+
});
|
|
90
|
+
await ai.ensure(); // boot
|
|
91
|
+
const { data } = await ai.extract.runJson<{ items: string[] }>({ input: "two milk" });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Plain text (throws unless `status === "completed"` with non-empty `output_text`):
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const { text } = await ai.tag.runText({ input: "summarize this" });
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`timeoutMs` aborts the HTTP attempt and, when `timeout_ms` is omitted, is also sent as the server-side run budget. Constructor `new AgentLu({ timeoutMs: 30_000 })` is the default for calls that omit it.
|
|
101
|
+
|
|
102
|
+
Upload-and-run:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
await extract.runJson({
|
|
106
|
+
input: "what's in this photo",
|
|
107
|
+
files: [{ data: bytes, filename: "shelf.jpg", mime: "image/jpeg" }],
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Background poll:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const queued = await extract.run({ input, schema, background: true, store: false });
|
|
115
|
+
const done = await alu.responses.wait(queued.response.id, { intervalMs: 1000, timeoutMs: 120_000 });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`runJson({ attempts: 2 })` re-asks once after `schema_validation_failed` / `incomplete` / `missing_output_parsed`. It never scrapes `output_text`.
|
|
119
|
+
|
|
78
120
|
Env: `AGENTLU_URL` (default `http://localhost:8912/v1`), `AGENTLU_API_KEY`.
|
|
79
121
|
|
|
80
122
|
## Decisions (System One / Jev)
|
|
@@ -113,10 +155,30 @@ if (answers.team.type === "choice") return lookupOrder(answers.team.choice);
|
|
|
113
155
|
| `choice` | `choice`, `confidence`, `probabilities` per option |
|
|
114
156
|
| `score` | `score`, `confidence`, `legend`, `probabilities` per step |
|
|
115
157
|
|
|
116
|
-
`decisions.ask()` is the same call returning only the answers map.
|
|
158
|
+
`decisions.ask()` is the same call returning only the answers map.
|
|
159
|
+
`decisions.askOrNull()` is fail-open (`null` on unconfigured/error). Discover the
|
|
117
160
|
model in `GET /v1/models` (`harness: "systemone"`); it is only callable on
|
|
118
161
|
`POST /v1/decisions`, sync, no streaming. A missing key surfaces as HTTP 503
|
|
119
|
-
`systemone_unconfigured` (not retried by the SDK).
|
|
162
|
+
`systemone_unconfigured` (not retried by the SDK). Use-case runners expose the
|
|
163
|
+
same `decisions` helpers on their own key.
|
|
164
|
+
|
|
165
|
+
Decision calls accept three client-side options beyond `state` / `questions` /
|
|
166
|
+
`model`:
|
|
167
|
+
|
|
168
|
+
| Option | Effect |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `timeoutMs` | Per-attempt deadline. A timeout aborts the request and is never retried. |
|
|
171
|
+
| `maxAttempts` | Total attempts including the first; defaults to the shared 5-attempt policy. |
|
|
172
|
+
| `signal` | Caller cancellation, combined with `timeoutMs` when both are set. |
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const answers = await alu.decisions.ask({
|
|
176
|
+
state: latestMessage,
|
|
177
|
+
questions: { rename: noul("Is this a name change request?") },
|
|
178
|
+
timeoutMs: 5_000,
|
|
179
|
+
maxAttempts: 2,
|
|
180
|
+
});
|
|
181
|
+
```
|
|
120
182
|
|
|
121
183
|
## Browser handoff
|
|
122
184
|
|
package/dist/index.d.ts
CHANGED
|
@@ -6,20 +6,30 @@
|
|
|
6
6
|
* new OpenAI({ baseURL: "http://localhost:8912/v1", apiKey })
|
|
7
7
|
*
|
|
8
8
|
* This client exists for the ergonomic bits OpenAI's SDK does not know about:
|
|
9
|
-
* get-or-create registration, typed `runJson`,
|
|
10
|
-
* `409 thread_busy`.
|
|
9
|
+
* get-or-create registration, typed `runJson` / `runText`, use-case catalogs,
|
|
10
|
+
* background `wait`, and auto-retry on `409 thread_busy`.
|
|
11
11
|
*/
|
|
12
12
|
export declare const DEFAULT_BASE_URL = "http://localhost:8912/v1";
|
|
13
13
|
export type Backend = "pi";
|
|
14
14
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
15
15
|
export type ApiToolFamily = "web_search" | "fetch_content" | "hublu" | "memory" | "image" | "browser" | "android";
|
|
16
|
+
export type ApiToolChoice = "auto" | "none" | "required";
|
|
16
17
|
/** Closed tool-family request accepted by `/v1/responses`. */
|
|
17
18
|
export interface ApiToolSpec {
|
|
18
19
|
type: ApiToolFamily;
|
|
19
20
|
/** Optional family-specific operation allowlist (HubLu, BrowserLu, or AndroidLu). */
|
|
20
21
|
allowed_tools?: string[];
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Native MCP server for the machine-only `api` agent. Mounted per run;
|
|
25
|
+
* `allowed_tools` pre-scopes the names the server lists.
|
|
26
|
+
*/
|
|
27
|
+
export interface ApiMcpServerSpec {
|
|
28
|
+
type: "mcp";
|
|
29
|
+
url: string;
|
|
30
|
+
allowed_tools?: string[];
|
|
31
|
+
}
|
|
32
|
+
export type ApiToolInput = ApiToolFamily | ApiToolSpec | ApiMcpServerSpec;
|
|
23
33
|
export type BrowserStatus = "active" | "needs_human" | "human_control" | "idle" | "closed" | "interrupted";
|
|
24
34
|
export interface BrowserScreenshot {
|
|
25
35
|
id: string;
|
|
@@ -87,6 +97,14 @@ export type TextFormat = TextFormatText | TextFormatJsonSchema;
|
|
|
87
97
|
export interface TextConfig {
|
|
88
98
|
format?: TextFormat;
|
|
89
99
|
}
|
|
100
|
+
export type FileKind = "image" | "video" | "text" | "document";
|
|
101
|
+
/** Local file to upload and attach on the same `run` / `stream`. */
|
|
102
|
+
export interface RunFile {
|
|
103
|
+
data: Uint8Array | Blob;
|
|
104
|
+
filename: string;
|
|
105
|
+
mime?: string;
|
|
106
|
+
kind?: FileKind;
|
|
107
|
+
}
|
|
90
108
|
/** Yes/no, answered as the probability that the answer is yes. */
|
|
91
109
|
export interface NoulQuestion {
|
|
92
110
|
type: "noul";
|
|
@@ -134,6 +152,16 @@ export interface DecisionOptions {
|
|
|
134
152
|
questions: Record<string, DecisionQuestion>;
|
|
135
153
|
/** Decision model id (default: the configured `typesafe/jev`). */
|
|
136
154
|
model?: string;
|
|
155
|
+
/**
|
|
156
|
+
* Per-attempt client deadline in milliseconds. A timeout aborts the request
|
|
157
|
+
* and is never retried; without it the request is bounded only by the
|
|
158
|
+
* caller's fetch implementation (or the AgentLu constructor `timeoutMs`).
|
|
159
|
+
*/
|
|
160
|
+
timeoutMs?: number;
|
|
161
|
+
/** Total attempts including the first. Defaults to the shared 5-attempt policy. */
|
|
162
|
+
maxAttempts?: number;
|
|
163
|
+
/** Caller cancellation, combined with `timeoutMs` when both are set. */
|
|
164
|
+
signal?: AbortSignal;
|
|
137
165
|
}
|
|
138
166
|
export interface DecisionObject {
|
|
139
167
|
id: string;
|
|
@@ -148,6 +176,12 @@ export interface DecisionObject {
|
|
|
148
176
|
} | null;
|
|
149
177
|
[k: string]: unknown;
|
|
150
178
|
}
|
|
179
|
+
export interface DecisionApi {
|
|
180
|
+
create: (body: DecisionOptions) => Promise<DecisionObject>;
|
|
181
|
+
ask: (body: DecisionOptions) => Promise<Record<string, DecisionAnswer>>;
|
|
182
|
+
/** Fail-open: returns `null` when the model is unconfigured or the call fails. */
|
|
183
|
+
askOrNull: (body: DecisionOptions) => Promise<Record<string, DecisionAnswer> | null>;
|
|
184
|
+
}
|
|
151
185
|
/** Question builders; sugar over the wire shape. */
|
|
152
186
|
export declare const noul: (instructions: string, criteria?: {
|
|
153
187
|
true: string;
|
|
@@ -174,6 +208,12 @@ export interface AgentLuOptions {
|
|
|
174
208
|
baseUrl?: string;
|
|
175
209
|
apiKey?: string;
|
|
176
210
|
fetch?: typeof fetch;
|
|
211
|
+
/**
|
|
212
|
+
* Default per-attempt client deadline for `run` / `stream` / `upload` /
|
|
213
|
+
* `decisions` when the call omits `timeoutMs`. Also fills `timeout_ms` on
|
|
214
|
+
* `/v1/responses` when the call omits that field.
|
|
215
|
+
*/
|
|
216
|
+
timeoutMs?: number;
|
|
177
217
|
}
|
|
178
218
|
export interface ClientDefaults {
|
|
179
219
|
backend?: Backend;
|
|
@@ -186,6 +226,11 @@ export interface ClientDefaults {
|
|
|
186
226
|
store?: boolean;
|
|
187
227
|
reasoning?: ClientReasoningOptions;
|
|
188
228
|
text?: TextConfig;
|
|
229
|
+
/**
|
|
230
|
+
* Retired on client defaults (server ignores it). Pass `max_output_tokens`
|
|
231
|
+
* or `maxOutputTokens` on `run` / `runJson` / `stream` instead.
|
|
232
|
+
*/
|
|
233
|
+
maxOutputTokens?: never;
|
|
189
234
|
}
|
|
190
235
|
/** High-level use-case declaration for model routing and priority tier. */
|
|
191
236
|
export interface UseCaseProfile {
|
|
@@ -194,18 +239,33 @@ export interface UseCaseProfile {
|
|
|
194
239
|
/** MiniMax priority admission consent (priority costs 1.5x standard). */
|
|
195
240
|
priorityTier?: "never" | "retry" | "first" | "always";
|
|
196
241
|
}
|
|
242
|
+
export interface UseCaseDeclaration {
|
|
243
|
+
/** Registered client name. Defaults to the catalog key. */
|
|
244
|
+
name?: string;
|
|
245
|
+
person?: string | null;
|
|
246
|
+
defaults?: ClientDefaults;
|
|
247
|
+
use_case_profile?: UseCaseProfile;
|
|
248
|
+
}
|
|
249
|
+
export type UseCaseCatalog<T extends Record<string, UseCaseDeclaration>> = {
|
|
250
|
+
[K in keyof T]: UseCaseRunner;
|
|
251
|
+
} & {
|
|
252
|
+
/** Idempotent get-or-create for every use-case. Safe to call on boot. */
|
|
253
|
+
ensure: () => Promise<void>;
|
|
254
|
+
};
|
|
197
255
|
export interface RunOptions {
|
|
198
256
|
backend?: Backend;
|
|
199
257
|
agent?: string;
|
|
200
258
|
model?: string;
|
|
201
259
|
input?: string;
|
|
202
260
|
/**
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
261
|
+
* Attachments previously uploaded via `upload()`. Prepended to the user
|
|
262
|
+
* turn using each file's `kind` (`input_image` / `input_video` / `input_file`).
|
|
263
|
+
*/
|
|
206
264
|
fileIds?: string[];
|
|
207
265
|
/** Optional kinds aligned with `fileIds` (from `upload()`). Defaults to image. */
|
|
208
|
-
fileKinds?:
|
|
266
|
+
fileKinds?: FileKind[];
|
|
267
|
+
/** Upload-and-attach in one call. Combined with `fileIds` when both are set. */
|
|
268
|
+
files?: RunFile[];
|
|
209
269
|
/** Structured input messages (OpenAI shape) - overrides `input`. */
|
|
210
270
|
inputMessages?: Array<{
|
|
211
271
|
role: string;
|
|
@@ -213,18 +273,32 @@ export interface RunOptions {
|
|
|
213
273
|
}>;
|
|
214
274
|
instructions?: string;
|
|
215
275
|
tools?: ApiToolInput[];
|
|
276
|
+
tool_choice?: ApiToolChoice;
|
|
216
277
|
reasoning?: ReasoningOptions;
|
|
217
278
|
text?: TextConfig;
|
|
218
279
|
strict?: boolean;
|
|
219
280
|
conversation?: string;
|
|
281
|
+
previous_response_id?: string;
|
|
220
282
|
store?: boolean;
|
|
221
283
|
person?: string | null;
|
|
222
284
|
owner?: "client" | "person";
|
|
223
285
|
title?: string;
|
|
286
|
+
/** Server-side run budget (wire name). */
|
|
224
287
|
timeout_ms?: number;
|
|
288
|
+
/**
|
|
289
|
+
* Per-attempt client deadline. When `timeout_ms` is omitted, the same value
|
|
290
|
+
* is also sent as the server-side budget.
|
|
291
|
+
*/
|
|
292
|
+
timeoutMs?: number;
|
|
293
|
+
/** Caller cancellation, combined with `timeoutMs` when both are set. */
|
|
294
|
+
signal?: AbortSignal;
|
|
295
|
+
/** Total HTTP attempts including the first. Defaults to the shared 5-attempt policy. */
|
|
296
|
+
maxAttempts?: number;
|
|
225
297
|
include?: string[];
|
|
226
298
|
temperature?: number;
|
|
227
299
|
max_output_tokens?: number;
|
|
300
|
+
/** Alias for `max_output_tokens` on this run (not a client default). */
|
|
301
|
+
maxOutputTokens?: number;
|
|
228
302
|
/** Caller metadata stored on the response row (console visibility). */
|
|
229
303
|
metadata?: Record<string, string>;
|
|
230
304
|
/** JSON Schema. When set, `data` on the result is the validated object. */
|
|
@@ -232,6 +306,23 @@ export interface RunOptions {
|
|
|
232
306
|
schemaName?: string;
|
|
233
307
|
/** Queue a background run instead of waiting. Returns the queued response. */
|
|
234
308
|
background?: boolean;
|
|
309
|
+
/**
|
|
310
|
+
* `runJson` only: extra model attempts after `schema_validation_failed`,
|
|
311
|
+
* `missing_output_parsed`, or `incomplete`. Defaults to 1 (no extra tries).
|
|
312
|
+
*/
|
|
313
|
+
attempts?: number;
|
|
314
|
+
}
|
|
315
|
+
export interface WaitOptions {
|
|
316
|
+
/** Delay between polls. Defaults to 1000ms. */
|
|
317
|
+
intervalMs?: number;
|
|
318
|
+
/** Overall wait deadline. The in-flight poll is aborted when it fires. */
|
|
319
|
+
timeoutMs?: number;
|
|
320
|
+
signal?: AbortSignal;
|
|
321
|
+
}
|
|
322
|
+
export interface UploadOptions {
|
|
323
|
+
timeoutMs?: number;
|
|
324
|
+
signal?: AbortSignal;
|
|
325
|
+
maxAttempts?: number;
|
|
235
326
|
}
|
|
236
327
|
export interface ResponseObject {
|
|
237
328
|
id: string;
|
|
@@ -263,10 +354,12 @@ export interface ResponseObject {
|
|
|
263
354
|
};
|
|
264
355
|
[k: string]: unknown;
|
|
265
356
|
}
|
|
357
|
+
export declare function isTerminalStatus(status: string): boolean;
|
|
266
358
|
export declare class AgentLu {
|
|
267
359
|
readonly baseUrl: string;
|
|
268
360
|
apiKey: string | undefined;
|
|
269
361
|
private readonly fetchImpl;
|
|
362
|
+
private readonly defaultTimeoutMs;
|
|
270
363
|
constructor(opts?: AgentLuOptions);
|
|
271
364
|
private postClient;
|
|
272
365
|
/** Get-or-create a client by name. Caches the returned key on this instance. */
|
|
@@ -285,12 +378,21 @@ export declare class AgentLu {
|
|
|
285
378
|
* The use-case's defaults (system prompt, tools, temperature, ...) live
|
|
286
379
|
* server-side on the client, so `run()` only needs the input. Registering a
|
|
287
380
|
* use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
|
|
288
|
-
|
|
381
|
+
*/
|
|
289
382
|
useCase(name: string, opts?: {
|
|
290
383
|
person?: string | null;
|
|
291
384
|
defaults?: ClientDefaults;
|
|
292
385
|
use_case_profile?: UseCaseProfile;
|
|
293
386
|
}): UseCaseRunner;
|
|
387
|
+
/**
|
|
388
|
+
* Several named use-case runners that share this client. Call `ensure()` on
|
|
389
|
+
* boot, then `catalog.extract.runJson({ input })`.
|
|
390
|
+
*
|
|
391
|
+
* const ai = alu.useCases({
|
|
392
|
+
* extract: { name: "deallu-extract", defaults: { agent: "api", store: false } },
|
|
393
|
+
* });
|
|
394
|
+
*/
|
|
395
|
+
useCases<T extends Record<string, UseCaseDeclaration>>(specs: T): UseCaseCatalog<T>;
|
|
294
396
|
/**
|
|
295
397
|
* Register a use-case client and resolve to its api key. The key is cached
|
|
296
398
|
* on this instance (same semantics as `register`), so a following
|
|
@@ -306,7 +408,7 @@ export declare class AgentLu {
|
|
|
306
408
|
* Upload an image, video, or document for use with `run({ fileIds })`.
|
|
307
409
|
* Returns the `file_id` and stored `kind`.
|
|
308
410
|
*/
|
|
309
|
-
upload(data: Uint8Array | Blob, filename: string, mime?: string): Promise<{
|
|
411
|
+
upload(data: Uint8Array | Blob, filename: string, mime?: string, opts?: UploadOptions): Promise<{
|
|
310
412
|
id: string;
|
|
311
413
|
kind?: string;
|
|
312
414
|
}>;
|
|
@@ -325,9 +427,19 @@ export declare class AgentLu {
|
|
|
325
427
|
data: T;
|
|
326
428
|
response: ResponseObject;
|
|
327
429
|
}>;
|
|
430
|
+
runText(opts: RunOptions): Promise<{
|
|
431
|
+
text: string;
|
|
432
|
+
response: ResponseObject;
|
|
433
|
+
}>;
|
|
434
|
+
stream(opts: RunOptions): AsyncGenerator<Record<string, unknown>>;
|
|
328
435
|
private runForApiKey;
|
|
329
436
|
private runJsonForApiKey;
|
|
437
|
+
private runJsonOnce;
|
|
438
|
+
private runTextForApiKey;
|
|
330
439
|
private createRunResponse;
|
|
440
|
+
private buildRunBody;
|
|
441
|
+
private uploadRunFiles;
|
|
442
|
+
private transportFromRun;
|
|
331
443
|
readonly responses: {
|
|
332
444
|
create: (body: Record<string, unknown>) => Promise<ResponseObject>;
|
|
333
445
|
retrieve: (id: string) => Promise<ResponseObject>;
|
|
@@ -340,8 +452,11 @@ export declare class AgentLu {
|
|
|
340
452
|
data: ResponseObject[];
|
|
341
453
|
}>;
|
|
342
454
|
stream: (body: Record<string, unknown>) => AsyncGenerator<Record<string, unknown>, any, any>;
|
|
455
|
+
wait: (id: string, opts?: WaitOptions) => Promise<ResponseObject>;
|
|
343
456
|
};
|
|
344
|
-
private
|
|
457
|
+
private waitForApiKey;
|
|
458
|
+
private streamRunForApiKey;
|
|
459
|
+
private streamRawForApiKey;
|
|
345
460
|
readonly threads: {
|
|
346
461
|
create: (body?: Record<string, unknown>) => Promise<unknown>;
|
|
347
462
|
retrieve: (id: string) => Promise<unknown>;
|
|
@@ -361,18 +476,18 @@ export declare class AgentLu {
|
|
|
361
476
|
* },
|
|
362
477
|
* });
|
|
363
478
|
* if (answers.urgent.noul > 0.8) routeToHuman();
|
|
479
|
+
*
|
|
480
|
+
* `timeoutMs` bounds each attempt (timeouts are never retried), `maxAttempts`
|
|
481
|
+
* overrides the shared 5-attempt policy, and `signal` cancels the call.
|
|
364
482
|
*/
|
|
365
|
-
readonly decisions:
|
|
366
|
-
create: (body: DecisionOptions) => Promise<DecisionObject>;
|
|
367
|
-
/** Same call, returning only the answers map. */
|
|
368
|
-
ask: (body: DecisionOptions) => Promise<Record<string, DecisionAnswer>>;
|
|
369
|
-
};
|
|
483
|
+
readonly decisions: DecisionApi;
|
|
370
484
|
agents(): Promise<unknown>;
|
|
371
485
|
models(): Promise<unknown>;
|
|
372
486
|
tools(): Promise<unknown>;
|
|
373
487
|
health(): Promise<unknown>;
|
|
374
488
|
private request;
|
|
375
489
|
private raw;
|
|
490
|
+
private rawOnce;
|
|
376
491
|
}
|
|
377
492
|
/**
|
|
378
493
|
* A runner bound to one registered use-case (one service client + its
|
|
@@ -396,8 +511,10 @@ export declare class UseCaseRunner {
|
|
|
396
511
|
defaults?: ClientDefaults;
|
|
397
512
|
use_case_profile?: UseCaseProfile;
|
|
398
513
|
});
|
|
399
|
-
/** Idempotent get-or-create; safe to call before every run. */
|
|
400
|
-
|
|
514
|
+
/** Idempotent get-or-create; safe to call on boot or before every run. */
|
|
515
|
+
ensure(): Promise<void>;
|
|
516
|
+
private invalidate;
|
|
517
|
+
private withAuthRetry;
|
|
401
518
|
/**
|
|
402
519
|
* Run with this use-case's key. Request fields that are set override the
|
|
403
520
|
* use-case's registered defaults; unset fields inherit them server-side.
|
|
@@ -410,13 +527,23 @@ export declare class UseCaseRunner {
|
|
|
410
527
|
data: T;
|
|
411
528
|
response: ResponseObject;
|
|
412
529
|
}>;
|
|
413
|
-
|
|
530
|
+
runText(opts: RunOptions): Promise<{
|
|
531
|
+
text: string;
|
|
532
|
+
response: ResponseObject;
|
|
533
|
+
}>;
|
|
534
|
+
stream(opts: RunOptions): AsyncGenerator<Record<string, unknown>>;
|
|
414
535
|
/**
|
|
415
536
|
* Upload with this use-case's key, so the file is owned by the same
|
|
416
537
|
* service client that will reference it via `run({ fileIds })`.
|
|
417
538
|
*/
|
|
418
|
-
upload(data: Uint8Array | Blob, filename: string, mime?: string): Promise<{
|
|
539
|
+
upload(data: Uint8Array | Blob, filename: string, mime?: string, opts?: UploadOptions): Promise<{
|
|
419
540
|
id: string;
|
|
420
541
|
kind?: string;
|
|
421
542
|
}>;
|
|
543
|
+
readonly responses: {
|
|
544
|
+
retrieve: (id: string) => Promise<ResponseObject>;
|
|
545
|
+
cancel: (id: string) => Promise<ResponseObject>;
|
|
546
|
+
wait: (id: string, opts?: WaitOptions) => Promise<ResponseObject>;
|
|
547
|
+
};
|
|
548
|
+
readonly decisions: DecisionApi;
|
|
422
549
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,24 @@
|
|
|
6
6
|
* new OpenAI({ baseURL: "http://localhost:8912/v1", apiKey })
|
|
7
7
|
*
|
|
8
8
|
* This client exists for the ergonomic bits OpenAI's SDK does not know about:
|
|
9
|
-
* get-or-create registration, typed `runJson`,
|
|
10
|
-
* `409 thread_busy`.
|
|
9
|
+
* get-or-create registration, typed `runJson` / `runText`, use-case catalogs,
|
|
10
|
+
* background `wait`, and auto-retry on `409 thread_busy`.
|
|
11
11
|
*/
|
|
12
12
|
export const DEFAULT_BASE_URL = "http://localhost:8912/v1";
|
|
13
|
+
/** Strip client-only decision options before the body reaches `/v1/decisions`. */
|
|
14
|
+
function decisionWire(body) {
|
|
15
|
+
const wire = { state: body.state, questions: body.questions };
|
|
16
|
+
if (body.model !== undefined)
|
|
17
|
+
wire.model = body.model;
|
|
18
|
+
return wire;
|
|
19
|
+
}
|
|
20
|
+
function decisionRequestOptions(body, defaultTimeoutMs) {
|
|
21
|
+
return {
|
|
22
|
+
timeoutMs: body.timeoutMs ?? defaultTimeoutMs,
|
|
23
|
+
maxAttempts: body.maxAttempts,
|
|
24
|
+
signal: body.signal,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
13
27
|
/** Question builders; sugar over the wire shape. */
|
|
14
28
|
export const noul = (instructions, criteria) => ({
|
|
15
29
|
type: "noul",
|
|
@@ -55,6 +69,9 @@ export class AgentLuResponseError extends AgentLuError {
|
|
|
55
69
|
this.responseStatus = responseStatus;
|
|
56
70
|
}
|
|
57
71
|
}
|
|
72
|
+
export function isTerminalStatus(status) {
|
|
73
|
+
return status === "completed" || status === "failed" || status === "incomplete" || status === "cancelled";
|
|
74
|
+
}
|
|
58
75
|
function env(name) {
|
|
59
76
|
return typeof process !== "undefined" ? process.env[name] : undefined;
|
|
60
77
|
}
|
|
@@ -63,16 +80,30 @@ export class AgentLu {
|
|
|
63
80
|
baseUrl;
|
|
64
81
|
apiKey;
|
|
65
82
|
fetchImpl;
|
|
83
|
+
defaultTimeoutMs;
|
|
66
84
|
constructor(opts = {}) {
|
|
67
85
|
this.baseUrl = (opts.baseUrl ?? env("AGENTLU_URL") ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
68
86
|
this.apiKey = opts.apiKey ?? env("AGENTLU_API_KEY");
|
|
69
87
|
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
88
|
+
this.defaultTimeoutMs = opts.timeoutMs;
|
|
70
89
|
scopedUseCaseOperations.set(this, {
|
|
71
90
|
register: (name, registerOpts) => this.registerForUseCase(name, registerOpts),
|
|
72
91
|
run: (apiKey, runOpts) => this.runForApiKey(apiKey, runOpts),
|
|
73
92
|
runJson: (apiKey, runOpts) => this.runJsonForApiKey(apiKey, runOpts),
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
runText: (apiKey, runOpts) => this.runTextForApiKey(apiKey, runOpts),
|
|
94
|
+
stream: (apiKey, runOpts) => this.streamRunForApiKey(runOpts, apiKey),
|
|
95
|
+
upload: (apiKey, data, filename, mime, uploadOpts) => this.uploadForApiKey(data, filename, mime, apiKey, uploadOpts),
|
|
96
|
+
wait: (apiKey, id, waitOpts) => this.waitForApiKey(id, apiKey, waitOpts),
|
|
97
|
+
retrieve: (apiKey, id, reqOpts) => this.request("GET", `/responses/${encodeURIComponent(id)}`, undefined, {
|
|
98
|
+
...reqOpts,
|
|
99
|
+
apiKey,
|
|
100
|
+
}),
|
|
101
|
+
cancel: (apiKey, id, reqOpts) => this.request("POST", `/responses/${encodeURIComponent(id)}/cancel`, undefined, {
|
|
102
|
+
...reqOpts,
|
|
103
|
+
apiKey,
|
|
104
|
+
}),
|
|
105
|
+
decideCreate: (apiKey, body) => this.request("POST", "/decisions", decisionWire(body), { ...decisionRequestOptions(body, this.defaultTimeoutMs), apiKey }),
|
|
106
|
+
decideAsk: async (apiKey, body) => (await this.request("POST", "/decisions", decisionWire(body), { ...decisionRequestOptions(body, this.defaultTimeoutMs), apiKey })).answers,
|
|
76
107
|
});
|
|
77
108
|
}
|
|
78
109
|
postClient(name, opts) {
|
|
@@ -91,10 +122,30 @@ export class AgentLu {
|
|
|
91
122
|
* The use-case's defaults (system prompt, tools, temperature, ...) live
|
|
92
123
|
* server-side on the client, so `run()` only needs the input. Registering a
|
|
93
124
|
* use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
|
|
94
|
-
|
|
125
|
+
*/
|
|
95
126
|
useCase(name, opts) {
|
|
96
127
|
return new UseCaseRunner(this, name, opts);
|
|
97
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Several named use-case runners that share this client. Call `ensure()` on
|
|
131
|
+
* boot, then `catalog.extract.runJson({ input })`.
|
|
132
|
+
*
|
|
133
|
+
* const ai = alu.useCases({
|
|
134
|
+
* extract: { name: "deallu-extract", defaults: { agent: "api", store: false } },
|
|
135
|
+
* });
|
|
136
|
+
*/
|
|
137
|
+
useCases(specs) {
|
|
138
|
+
const runners = {};
|
|
139
|
+
for (const key of Object.keys(specs)) {
|
|
140
|
+
const spec = specs[key];
|
|
141
|
+
runners[key] = this.useCase(spec.name ?? String(key), spec);
|
|
142
|
+
}
|
|
143
|
+
const catalog = { ...runners };
|
|
144
|
+
catalog.ensure = async () => {
|
|
145
|
+
await Promise.all(Object.values(runners).map((runner) => runner.ensure()));
|
|
146
|
+
};
|
|
147
|
+
return catalog;
|
|
148
|
+
}
|
|
98
149
|
/**
|
|
99
150
|
* Register a use-case client and resolve to its api key. The key is cached
|
|
100
151
|
* on this instance (same semantics as `register`), so a following
|
|
@@ -117,14 +168,19 @@ export class AgentLu {
|
|
|
117
168
|
* Upload an image, video, or document for use with `run({ fileIds })`.
|
|
118
169
|
* Returns the `file_id` and stored `kind`.
|
|
119
170
|
*/
|
|
120
|
-
async upload(data, filename, mime) {
|
|
121
|
-
return this.uploadForApiKey(data, filename, mime, this.apiKey);
|
|
171
|
+
async upload(data, filename, mime, opts) {
|
|
172
|
+
return this.uploadForApiKey(data, filename, mime, this.apiKey, opts);
|
|
122
173
|
}
|
|
123
|
-
async uploadForApiKey(data, filename, mime, apiKey) {
|
|
174
|
+
async uploadForApiKey(data, filename, mime, apiKey, opts) {
|
|
124
175
|
const form = new FormData();
|
|
125
176
|
const blob = data instanceof Blob ? data : new Blob([new Uint8Array(data)], { type: mime ?? "application/octet-stream" });
|
|
126
177
|
form.append("file", blob, filename);
|
|
127
|
-
const res = await this.raw("POST", "/files", form, {
|
|
178
|
+
const res = await this.raw("POST", "/files", form, {
|
|
179
|
+
apiKey,
|
|
180
|
+
timeoutMs: opts?.timeoutMs,
|
|
181
|
+
signal: opts?.signal,
|
|
182
|
+
maxAttempts: opts?.maxAttempts,
|
|
183
|
+
});
|
|
128
184
|
return (await res.json());
|
|
129
185
|
}
|
|
130
186
|
async me() {
|
|
@@ -142,6 +198,12 @@ export class AgentLu {
|
|
|
142
198
|
assertJsonRunOptions(opts);
|
|
143
199
|
return this.runJsonForApiKey(this.apiKey, opts);
|
|
144
200
|
}
|
|
201
|
+
async runText(opts) {
|
|
202
|
+
return this.runTextForApiKey(this.apiKey, opts);
|
|
203
|
+
}
|
|
204
|
+
async *stream(opts) {
|
|
205
|
+
yield* this.streamRunForApiKey(opts, this.apiKey);
|
|
206
|
+
}
|
|
145
207
|
async runForApiKey(apiKey, opts) {
|
|
146
208
|
const response = await this.createRunResponse(opts, apiKey);
|
|
147
209
|
if (response.status === "failed" && response.error) {
|
|
@@ -150,6 +212,21 @@ export class AgentLu {
|
|
|
150
212
|
return { data: response.output_parsed, response };
|
|
151
213
|
}
|
|
152
214
|
async runJsonForApiKey(apiKey, opts) {
|
|
215
|
+
const attempts = jsonAttempts(opts);
|
|
216
|
+
let last;
|
|
217
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
218
|
+
try {
|
|
219
|
+
return await this.runJsonOnce(apiKey, opts);
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
last = e;
|
|
223
|
+
if (!isJsonRetryable(e) || attempt >= attempts - 1)
|
|
224
|
+
throw e;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
throw last;
|
|
228
|
+
}
|
|
229
|
+
async runJsonOnce(apiKey, opts) {
|
|
153
230
|
const response = await this.createRunResponse(opts, apiKey);
|
|
154
231
|
if (response.status !== "completed")
|
|
155
232
|
throw new AgentLuResponseError(response);
|
|
@@ -158,15 +235,32 @@ export class AgentLu {
|
|
|
158
235
|
}
|
|
159
236
|
return { data: response.output_parsed, response };
|
|
160
237
|
}
|
|
238
|
+
async runTextForApiKey(apiKey, opts) {
|
|
239
|
+
const response = await this.createRunResponse(opts, apiKey);
|
|
240
|
+
if (response.status !== "completed")
|
|
241
|
+
throw new AgentLuResponseError(response);
|
|
242
|
+
const text = response.output_text ?? "";
|
|
243
|
+
if (!text.trim()) {
|
|
244
|
+
throw new AgentLuError(200, "empty_output", "Completed response did not include output_text.", false);
|
|
245
|
+
}
|
|
246
|
+
return { text, response };
|
|
247
|
+
}
|
|
161
248
|
async createRunResponse(opts, apiKey) {
|
|
249
|
+
const body = await this.buildRunBody(opts, apiKey);
|
|
250
|
+
return this.request("POST", "/responses", body, this.transportFromRun(opts, apiKey));
|
|
251
|
+
}
|
|
252
|
+
async buildRunBody(opts, apiKey) {
|
|
253
|
+
const uploaded = await this.uploadRunFiles(opts, apiKey);
|
|
254
|
+
const fileIds = [...uploaded.fileIds, ...(opts.fileIds ?? [])];
|
|
255
|
+
const fileKinds = [...uploaded.fileKinds, ...(opts.fileKinds ?? [])];
|
|
162
256
|
let input = opts.inputMessages ?? opts.input;
|
|
163
|
-
if (
|
|
257
|
+
if (fileIds.length) {
|
|
164
258
|
input = [
|
|
165
259
|
{
|
|
166
260
|
role: "user",
|
|
167
261
|
content: [
|
|
168
|
-
...
|
|
169
|
-
const kind =
|
|
262
|
+
...fileIds.map((file_id, i) => {
|
|
263
|
+
const kind = fileKinds[i];
|
|
170
264
|
if (kind === "video")
|
|
171
265
|
return { type: "input_video", file_id };
|
|
172
266
|
if (kind === "text" || kind === "document")
|
|
@@ -185,16 +279,18 @@ export class AgentLu {
|
|
|
185
279
|
model: opts.model,
|
|
186
280
|
instructions: opts.instructions,
|
|
187
281
|
tools: opts.tools,
|
|
282
|
+
tool_choice: opts.tool_choice,
|
|
188
283
|
reasoning: opts.reasoning,
|
|
189
284
|
conversation: opts.conversation,
|
|
285
|
+
previous_response_id: opts.previous_response_id,
|
|
190
286
|
store: opts.store,
|
|
191
287
|
person: opts.person,
|
|
192
288
|
owner: opts.owner,
|
|
193
289
|
title: opts.title,
|
|
194
|
-
timeout_ms: opts.timeout_ms,
|
|
290
|
+
timeout_ms: opts.timeout_ms ?? opts.timeoutMs ?? this.defaultTimeoutMs,
|
|
195
291
|
include: opts.include,
|
|
196
292
|
temperature: opts.temperature,
|
|
197
|
-
max_output_tokens: opts.max_output_tokens,
|
|
293
|
+
max_output_tokens: opts.max_output_tokens ?? opts.maxOutputTokens,
|
|
198
294
|
background: opts.background,
|
|
199
295
|
metadata: opts.metadata,
|
|
200
296
|
};
|
|
@@ -211,7 +307,33 @@ export class AgentLu {
|
|
|
211
307
|
},
|
|
212
308
|
};
|
|
213
309
|
}
|
|
214
|
-
return
|
|
310
|
+
return body;
|
|
311
|
+
}
|
|
312
|
+
async uploadRunFiles(opts, apiKey) {
|
|
313
|
+
const files = opts.files ?? [];
|
|
314
|
+
if (!files.length)
|
|
315
|
+
return { fileIds: [], fileKinds: [] };
|
|
316
|
+
const transport = this.transportFromRun(opts, apiKey);
|
|
317
|
+
const fileIds = [];
|
|
318
|
+
const fileKinds = [];
|
|
319
|
+
for (const file of files) {
|
|
320
|
+
const uploaded = await this.uploadForApiKey(file.data, file.filename, file.mime, apiKey, {
|
|
321
|
+
timeoutMs: transport.timeoutMs,
|
|
322
|
+
signal: transport.signal,
|
|
323
|
+
maxAttempts: transport.maxAttempts,
|
|
324
|
+
});
|
|
325
|
+
fileIds.push(uploaded.id);
|
|
326
|
+
fileKinds.push(file.kind ?? kindFromMime(file.mime, uploaded.kind));
|
|
327
|
+
}
|
|
328
|
+
return { fileIds, fileKinds };
|
|
329
|
+
}
|
|
330
|
+
transportFromRun(opts, apiKey) {
|
|
331
|
+
return {
|
|
332
|
+
apiKey,
|
|
333
|
+
timeoutMs: opts.timeoutMs ?? this.defaultTimeoutMs,
|
|
334
|
+
maxAttempts: opts.maxAttempts,
|
|
335
|
+
signal: opts.signal,
|
|
336
|
+
};
|
|
215
337
|
}
|
|
216
338
|
responses = {
|
|
217
339
|
create: (body) => this.request("POST", "/responses", body),
|
|
@@ -226,10 +348,42 @@ export class AgentLu {
|
|
|
226
348
|
const qs = q.toString();
|
|
227
349
|
return this.request("GET", `/responses${qs ? `?${qs}` : ""}`);
|
|
228
350
|
},
|
|
229
|
-
stream: (body) => this.
|
|
351
|
+
stream: (body) => this.streamRawForApiKey(body, this.apiKey),
|
|
352
|
+
wait: (id, opts) => this.waitForApiKey(id, this.apiKey, opts),
|
|
230
353
|
};
|
|
231
|
-
async
|
|
232
|
-
const
|
|
354
|
+
async waitForApiKey(id, apiKey, opts) {
|
|
355
|
+
const intervalMs = Math.max(opts?.intervalMs ?? 1_000, 50);
|
|
356
|
+
const started = Date.now();
|
|
357
|
+
for (;;) {
|
|
358
|
+
if (opts?.signal?.aborted) {
|
|
359
|
+
throw opts.signal.reason instanceof Error
|
|
360
|
+
? opts.signal.reason
|
|
361
|
+
: new AgentLuError(0, "aborted", "wait aborted", false);
|
|
362
|
+
}
|
|
363
|
+
const remaining = opts?.timeoutMs !== undefined ? Math.max(opts.timeoutMs - (Date.now() - started), 1) : undefined;
|
|
364
|
+
const row = await this.request("GET", `/responses/${encodeURIComponent(id)}`, undefined, {
|
|
365
|
+
apiKey,
|
|
366
|
+
signal: opts?.signal,
|
|
367
|
+
timeoutMs: remaining ?? this.defaultTimeoutMs,
|
|
368
|
+
maxAttempts: 1,
|
|
369
|
+
});
|
|
370
|
+
if (isTerminalStatus(row.status))
|
|
371
|
+
return row;
|
|
372
|
+
if (opts?.timeoutMs !== undefined && Date.now() - started + intervalMs >= opts.timeoutMs) {
|
|
373
|
+
throw new AgentLuError(408, "timeout", `Timed out waiting for response ${id}`, true);
|
|
374
|
+
}
|
|
375
|
+
await sleep(intervalMs);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async *streamRunForApiKey(opts, apiKey) {
|
|
379
|
+
if (opts.background === true) {
|
|
380
|
+
throw new AgentLuError(400, "background_not_supported", "stream cannot use background.", false);
|
|
381
|
+
}
|
|
382
|
+
const body = await this.buildRunBody(opts, apiKey);
|
|
383
|
+
yield* this.streamRawForApiKey(body, apiKey, this.transportFromRun(opts, apiKey));
|
|
384
|
+
}
|
|
385
|
+
async *streamRawForApiKey(body, apiKey, transport) {
|
|
386
|
+
const res = await this.raw("POST", "/responses", { ...body, stream: true }, { ...transport, apiKey: transport?.apiKey ?? apiKey });
|
|
233
387
|
if (!res.body)
|
|
234
388
|
throw new AgentLuError(0, "internal_error", "empty stream body", true);
|
|
235
389
|
const reader = res.body.getReader();
|
|
@@ -241,13 +395,13 @@ export class AgentLu {
|
|
|
241
395
|
if (done)
|
|
242
396
|
break;
|
|
243
397
|
buf += decoder.decode(value, { stream: true });
|
|
244
|
-
const lines = buf.split(
|
|
398
|
+
const lines = buf.split(/\r?\n/);
|
|
245
399
|
buf = lines.pop() ?? "";
|
|
246
400
|
for (const line of lines) {
|
|
247
401
|
const trimmed = line.trim();
|
|
248
|
-
if (!trimmed.startsWith("data:
|
|
402
|
+
if (!trimmed.startsWith("data:"))
|
|
249
403
|
continue;
|
|
250
|
-
const payload = trimmed.slice(
|
|
404
|
+
const payload = trimmed.slice(5).trim();
|
|
251
405
|
if (payload === "[DONE]")
|
|
252
406
|
return;
|
|
253
407
|
try {
|
|
@@ -260,6 +414,7 @@ export class AgentLu {
|
|
|
260
414
|
}
|
|
261
415
|
}
|
|
262
416
|
finally {
|
|
417
|
+
await reader.cancel().catch(() => { });
|
|
263
418
|
reader.releaseLock();
|
|
264
419
|
}
|
|
265
420
|
}
|
|
@@ -282,11 +437,21 @@ export class AgentLu {
|
|
|
282
437
|
* },
|
|
283
438
|
* });
|
|
284
439
|
* if (answers.urgent.noul > 0.8) routeToHuman();
|
|
440
|
+
*
|
|
441
|
+
* `timeoutMs` bounds each attempt (timeouts are never retried), `maxAttempts`
|
|
442
|
+
* overrides the shared 5-attempt policy, and `signal` cancels the call.
|
|
285
443
|
*/
|
|
286
444
|
decisions = {
|
|
287
|
-
create: (body) => this.request("POST", "/decisions", body),
|
|
288
|
-
|
|
289
|
-
|
|
445
|
+
create: (body) => this.request("POST", "/decisions", decisionWire(body), decisionRequestOptions(body, this.defaultTimeoutMs)),
|
|
446
|
+
ask: async (body) => (await this.request("POST", "/decisions", decisionWire(body), decisionRequestOptions(body, this.defaultTimeoutMs))).answers,
|
|
447
|
+
askOrNull: async (body) => {
|
|
448
|
+
try {
|
|
449
|
+
return await this.decisions.ask(body);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
},
|
|
290
455
|
};
|
|
291
456
|
async agents() {
|
|
292
457
|
return this.request("GET", "/agents", undefined, { auth: false });
|
|
@@ -301,27 +466,33 @@ export class AgentLu {
|
|
|
301
466
|
return this.request("GET", "/health", undefined, { auth: false });
|
|
302
467
|
}
|
|
303
468
|
async request(method, path, body, opts) {
|
|
469
|
+
const res = await this.raw(method, path, body, opts);
|
|
470
|
+
if (res.status === 204)
|
|
471
|
+
return undefined;
|
|
472
|
+
return (await res.json());
|
|
473
|
+
}
|
|
474
|
+
async raw(method, path, body, opts) {
|
|
304
475
|
// `409 thread_busy` and `503 too_many_active_runs` are correctness bounds,
|
|
305
476
|
// not rate limits. Retrying with backoff is almost always the right thing,
|
|
306
477
|
// so the SDK does it - honoring Retry-After when the server sends one.
|
|
307
478
|
// A 503 can also signal a config boundary (no decision model key); those
|
|
308
|
-
// are not retryable and must surface immediately.
|
|
479
|
+
// are not retryable and must surface immediately. Callers can cap attempts
|
|
480
|
+
// per request (`maxAttempts`); a timeout/caller abort is never retried.
|
|
309
481
|
const nonRetryable503 = new Set(["systemone_unconfigured"]);
|
|
482
|
+
const maxAttempts = opts?.maxAttempts !== undefined && Number.isFinite(opts.maxAttempts) && opts.maxAttempts >= 1
|
|
483
|
+
? Math.floor(opts.maxAttempts)
|
|
484
|
+
: 5;
|
|
310
485
|
let last;
|
|
311
|
-
for (let attempt = 0; attempt <
|
|
486
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
312
487
|
try {
|
|
313
|
-
|
|
314
|
-
if (res.status === 204)
|
|
315
|
-
return undefined;
|
|
316
|
-
const json = (await res.json());
|
|
317
|
-
return json;
|
|
488
|
+
return await this.rawOnce(method, path, body, opts);
|
|
318
489
|
}
|
|
319
490
|
catch (e) {
|
|
320
491
|
if (e instanceof AgentLuError &&
|
|
321
492
|
(e.code === "thread_busy" ||
|
|
322
493
|
e.code === "too_many_active_runs" ||
|
|
323
494
|
(e.status === 503 && !nonRetryable503.has(e.code))) &&
|
|
324
|
-
attempt <
|
|
495
|
+
attempt < maxAttempts - 1) {
|
|
325
496
|
last = e;
|
|
326
497
|
await sleep(retryDelayMs(e, attempt));
|
|
327
498
|
continue;
|
|
@@ -331,7 +502,7 @@ export class AgentLu {
|
|
|
331
502
|
}
|
|
332
503
|
throw last ?? new AgentLuError(503, "too_many_active_runs", "too many retries", true);
|
|
333
504
|
}
|
|
334
|
-
async
|
|
505
|
+
async rawOnce(method, path, body, opts) {
|
|
335
506
|
const headers = {};
|
|
336
507
|
const isForm = typeof FormData !== "undefined" && body instanceof FormData;
|
|
337
508
|
if (body !== undefined && !isForm) {
|
|
@@ -340,11 +511,19 @@ export class AgentLu {
|
|
|
340
511
|
const apiKey = opts?.apiKey ?? this.apiKey;
|
|
341
512
|
if (opts?.auth !== false && apiKey)
|
|
342
513
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
514
|
+
const signal = requestSignal(opts?.signal, opts?.timeoutMs ?? this.defaultTimeoutMs);
|
|
515
|
+
let res;
|
|
516
|
+
try {
|
|
517
|
+
res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
518
|
+
method,
|
|
519
|
+
headers,
|
|
520
|
+
...(signal ? { signal } : {}),
|
|
521
|
+
body: body === undefined ? undefined : isForm ? body : JSON.stringify(body),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch (e) {
|
|
525
|
+
throw mapFetchError(e, opts?.signal);
|
|
526
|
+
}
|
|
348
527
|
if (!res.ok) {
|
|
349
528
|
let code = `http_${res.status}`;
|
|
350
529
|
let message = `HTTP ${res.status}`;
|
|
@@ -388,7 +567,7 @@ export class UseCaseRunner {
|
|
|
388
567
|
this.registerOpts = opts ?? {};
|
|
389
568
|
this.operations = scopedUseCaseOperations.get(parent);
|
|
390
569
|
}
|
|
391
|
-
/** Idempotent get-or-create; safe to call before every run. */
|
|
570
|
+
/** Idempotent get-or-create; safe to call on boot or before every run. */
|
|
392
571
|
ensure() {
|
|
393
572
|
if (this.apiKey)
|
|
394
573
|
return Promise.resolve();
|
|
@@ -404,37 +583,123 @@ export class UseCaseRunner {
|
|
|
404
583
|
}
|
|
405
584
|
return this.ready;
|
|
406
585
|
}
|
|
586
|
+
invalidate() {
|
|
587
|
+
this.apiKey = null;
|
|
588
|
+
this.ready = null;
|
|
589
|
+
}
|
|
590
|
+
async withAuthRetry(fn) {
|
|
591
|
+
await this.ensure();
|
|
592
|
+
try {
|
|
593
|
+
return await fn(this.apiKey);
|
|
594
|
+
}
|
|
595
|
+
catch (e) {
|
|
596
|
+
if (e instanceof AgentLuError && e.status === 401) {
|
|
597
|
+
this.invalidate();
|
|
598
|
+
await this.ensure();
|
|
599
|
+
return await fn(this.apiKey);
|
|
600
|
+
}
|
|
601
|
+
throw e;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
407
604
|
/**
|
|
408
605
|
* Run with this use-case's key. Request fields that are set override the
|
|
409
606
|
* use-case's registered defaults; unset fields inherit them server-side.
|
|
410
607
|
*/
|
|
411
608
|
async run(opts) {
|
|
412
|
-
await this.
|
|
413
|
-
return (await this.operations.run(this.apiKey, opts));
|
|
609
|
+
return (await this.withAuthRetry((apiKey) => this.operations.run(apiKey, opts)));
|
|
414
610
|
}
|
|
415
611
|
async runJson(opts) {
|
|
416
612
|
assertJsonRunOptions(opts);
|
|
417
|
-
await this.
|
|
418
|
-
|
|
613
|
+
return (await this.withAuthRetry((apiKey) => this.operations.runJson(apiKey, opts)));
|
|
614
|
+
}
|
|
615
|
+
async runText(opts) {
|
|
616
|
+
return this.withAuthRetry((apiKey) => this.operations.runText(apiKey, opts));
|
|
419
617
|
}
|
|
420
|
-
async stream(
|
|
618
|
+
async *stream(opts) {
|
|
421
619
|
await this.ensure();
|
|
422
|
-
|
|
620
|
+
try {
|
|
621
|
+
yield* this.operations.stream(this.apiKey, opts);
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
if (!(e instanceof AgentLuError && e.status === 401))
|
|
625
|
+
throw e;
|
|
626
|
+
this.invalidate();
|
|
627
|
+
await this.ensure();
|
|
628
|
+
yield* this.operations.stream(this.apiKey, opts);
|
|
629
|
+
}
|
|
423
630
|
}
|
|
424
631
|
/**
|
|
425
632
|
* Upload with this use-case's key, so the file is owned by the same
|
|
426
633
|
* service client that will reference it via `run({ fileIds })`.
|
|
427
634
|
*/
|
|
428
|
-
async upload(data, filename, mime) {
|
|
429
|
-
|
|
430
|
-
return this.operations.upload(this.apiKey, data, filename, mime);
|
|
635
|
+
async upload(data, filename, mime, opts) {
|
|
636
|
+
return this.withAuthRetry((apiKey) => this.operations.upload(apiKey, data, filename, mime, opts));
|
|
431
637
|
}
|
|
638
|
+
responses = {
|
|
639
|
+
retrieve: (id) => this.withAuthRetry((apiKey) => this.operations.retrieve(apiKey, id)),
|
|
640
|
+
cancel: (id) => this.withAuthRetry((apiKey) => this.operations.cancel(apiKey, id)),
|
|
641
|
+
wait: (id, opts) => this.withAuthRetry((apiKey) => this.operations.wait(apiKey, id, opts)),
|
|
642
|
+
};
|
|
643
|
+
decisions = {
|
|
644
|
+
create: (body) => this.withAuthRetry((apiKey) => this.operations.decideCreate(apiKey, body)),
|
|
645
|
+
ask: (body) => this.withAuthRetry((apiKey) => this.operations.decideAsk(apiKey, body)),
|
|
646
|
+
askOrNull: async (body) => {
|
|
647
|
+
try {
|
|
648
|
+
return await this.decisions.ask(body);
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
};
|
|
432
655
|
}
|
|
433
656
|
function assertJsonRunOptions(opts) {
|
|
434
657
|
if (opts.background === true) {
|
|
435
658
|
throw new AgentLuError(400, "background_not_supported", "runJson requires a completed response and cannot use background.", false);
|
|
436
659
|
}
|
|
437
660
|
}
|
|
661
|
+
function jsonAttempts(opts) {
|
|
662
|
+
if (opts.attempts !== undefined && Number.isFinite(opts.attempts) && opts.attempts >= 1) {
|
|
663
|
+
return Math.floor(opts.attempts);
|
|
664
|
+
}
|
|
665
|
+
return 1;
|
|
666
|
+
}
|
|
667
|
+
function isJsonRetryable(error) {
|
|
668
|
+
if (error instanceof AgentLuError && error.code === "missing_output_parsed")
|
|
669
|
+
return true;
|
|
670
|
+
if (error instanceof AgentLuResponseError) {
|
|
671
|
+
return error.code === "schema_validation_failed" || error.responseStatus === "incomplete";
|
|
672
|
+
}
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
function kindFromMime(mime, stored) {
|
|
676
|
+
if (stored === "video" || stored === "text" || stored === "document" || stored === "image")
|
|
677
|
+
return stored;
|
|
678
|
+
if (mime?.startsWith("video/"))
|
|
679
|
+
return "video";
|
|
680
|
+
if (mime?.startsWith("text/"))
|
|
681
|
+
return "text";
|
|
682
|
+
if (mime === "application/pdf")
|
|
683
|
+
return "document";
|
|
684
|
+
return "image";
|
|
685
|
+
}
|
|
686
|
+
function mapFetchError(error, callerSignal) {
|
|
687
|
+
if (error instanceof AgentLuError)
|
|
688
|
+
return error;
|
|
689
|
+
if (callerSignal?.aborted) {
|
|
690
|
+
const reason = callerSignal.reason;
|
|
691
|
+
if (reason instanceof AgentLuError)
|
|
692
|
+
return reason;
|
|
693
|
+
const message = reason instanceof Error ? reason.message : "Request aborted";
|
|
694
|
+
return new AgentLuError(0, "aborted", message, false);
|
|
695
|
+
}
|
|
696
|
+
const name = error instanceof Error ? error.name : "";
|
|
697
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
698
|
+
if (name === "TimeoutError" || name === "AbortError" || /timed out/i.test(message)) {
|
|
699
|
+
return new AgentLuError(408, "timeout", message || "Request timed out", true);
|
|
700
|
+
}
|
|
701
|
+
return new AgentLuError(0, "network_error", message || "fetch failed", true);
|
|
702
|
+
}
|
|
438
703
|
function parseRetryAfter(res) {
|
|
439
704
|
const header = res.headers.get("retry-after");
|
|
440
705
|
if (!header)
|
|
@@ -442,6 +707,41 @@ function parseRetryAfter(res) {
|
|
|
442
707
|
const seconds = Number(header);
|
|
443
708
|
return Number.isFinite(seconds) ? seconds * 1000 : undefined;
|
|
444
709
|
}
|
|
710
|
+
/** AbortSignal.timeout with a bare fallback (Node 18, older browsers). */
|
|
711
|
+
function timeoutSignal(timeoutMs) {
|
|
712
|
+
if (typeof AbortSignal.timeout === "function")
|
|
713
|
+
return AbortSignal.timeout(timeoutMs);
|
|
714
|
+
const controller = new AbortController();
|
|
715
|
+
const timer = setTimeout(() => controller.abort(new Error(`Request timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
716
|
+
timer.unref?.();
|
|
717
|
+
return controller.signal;
|
|
718
|
+
}
|
|
719
|
+
/** AbortSignal.any with a bare fallback (Node 18, older browsers). */
|
|
720
|
+
function mergeSignals(signals) {
|
|
721
|
+
if (signals.length === 1)
|
|
722
|
+
return signals[0];
|
|
723
|
+
if (typeof AbortSignal.any === "function")
|
|
724
|
+
return AbortSignal.any(signals);
|
|
725
|
+
const controller = new AbortController();
|
|
726
|
+
for (const signal of signals) {
|
|
727
|
+
if (signal.aborted) {
|
|
728
|
+
controller.abort(signal.reason);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
732
|
+
}
|
|
733
|
+
return controller.signal;
|
|
734
|
+
}
|
|
735
|
+
/** Combined deadline/cancellation signal; undefined when neither applies. */
|
|
736
|
+
function requestSignal(signal, timeoutMs) {
|
|
737
|
+
const signals = [];
|
|
738
|
+
if (signal)
|
|
739
|
+
signals.push(signal);
|
|
740
|
+
if (timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
741
|
+
signals.push(timeoutSignal(timeoutMs));
|
|
742
|
+
}
|
|
743
|
+
return signals.length ? mergeSignals(signals) : undefined;
|
|
744
|
+
}
|
|
445
745
|
function retryDelayMs(err, attempt) {
|
|
446
746
|
if (err.retryAfterMs !== undefined) {
|
|
447
747
|
return Math.min(10_000, Math.max(250, err.retryAfterMs));
|
package/package.json
CHANGED