@betterlu/agentlu 1.5.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 +47 -3
- package/dist/index.d.ts +134 -20
- package/dist/index.js +295 -49
- 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,12 @@ 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.
|
|
120
164
|
|
|
121
165
|
Decision calls accept three client-side options beyond `state` / `questions` /
|
|
122
166
|
`model`:
|
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";
|
|
@@ -137,7 +155,7 @@ export interface DecisionOptions {
|
|
|
137
155
|
/**
|
|
138
156
|
* Per-attempt client deadline in milliseconds. A timeout aborts the request
|
|
139
157
|
* and is never retried; without it the request is bounded only by the
|
|
140
|
-
* caller's fetch implementation.
|
|
158
|
+
* caller's fetch implementation (or the AgentLu constructor `timeoutMs`).
|
|
141
159
|
*/
|
|
142
160
|
timeoutMs?: number;
|
|
143
161
|
/** Total attempts including the first. Defaults to the shared 5-attempt policy. */
|
|
@@ -158,6 +176,12 @@ export interface DecisionObject {
|
|
|
158
176
|
} | null;
|
|
159
177
|
[k: string]: unknown;
|
|
160
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
|
+
}
|
|
161
185
|
/** Question builders; sugar over the wire shape. */
|
|
162
186
|
export declare const noul: (instructions: string, criteria?: {
|
|
163
187
|
true: string;
|
|
@@ -184,6 +208,12 @@ export interface AgentLuOptions {
|
|
|
184
208
|
baseUrl?: string;
|
|
185
209
|
apiKey?: string;
|
|
186
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;
|
|
187
217
|
}
|
|
188
218
|
export interface ClientDefaults {
|
|
189
219
|
backend?: Backend;
|
|
@@ -196,6 +226,11 @@ export interface ClientDefaults {
|
|
|
196
226
|
store?: boolean;
|
|
197
227
|
reasoning?: ClientReasoningOptions;
|
|
198
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;
|
|
199
234
|
}
|
|
200
235
|
/** High-level use-case declaration for model routing and priority tier. */
|
|
201
236
|
export interface UseCaseProfile {
|
|
@@ -204,18 +239,33 @@ export interface UseCaseProfile {
|
|
|
204
239
|
/** MiniMax priority admission consent (priority costs 1.5x standard). */
|
|
205
240
|
priorityTier?: "never" | "retry" | "first" | "always";
|
|
206
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
|
+
};
|
|
207
255
|
export interface RunOptions {
|
|
208
256
|
backend?: Backend;
|
|
209
257
|
agent?: string;
|
|
210
258
|
model?: string;
|
|
211
259
|
input?: string;
|
|
212
260
|
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
261
|
+
* Attachments previously uploaded via `upload()`. Prepended to the user
|
|
262
|
+
* turn using each file's `kind` (`input_image` / `input_video` / `input_file`).
|
|
263
|
+
*/
|
|
216
264
|
fileIds?: string[];
|
|
217
265
|
/** Optional kinds aligned with `fileIds` (from `upload()`). Defaults to image. */
|
|
218
|
-
fileKinds?:
|
|
266
|
+
fileKinds?: FileKind[];
|
|
267
|
+
/** Upload-and-attach in one call. Combined with `fileIds` when both are set. */
|
|
268
|
+
files?: RunFile[];
|
|
219
269
|
/** Structured input messages (OpenAI shape) - overrides `input`. */
|
|
220
270
|
inputMessages?: Array<{
|
|
221
271
|
role: string;
|
|
@@ -223,18 +273,32 @@ export interface RunOptions {
|
|
|
223
273
|
}>;
|
|
224
274
|
instructions?: string;
|
|
225
275
|
tools?: ApiToolInput[];
|
|
276
|
+
tool_choice?: ApiToolChoice;
|
|
226
277
|
reasoning?: ReasoningOptions;
|
|
227
278
|
text?: TextConfig;
|
|
228
279
|
strict?: boolean;
|
|
229
280
|
conversation?: string;
|
|
281
|
+
previous_response_id?: string;
|
|
230
282
|
store?: boolean;
|
|
231
283
|
person?: string | null;
|
|
232
284
|
owner?: "client" | "person";
|
|
233
285
|
title?: string;
|
|
286
|
+
/** Server-side run budget (wire name). */
|
|
234
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;
|
|
235
297
|
include?: string[];
|
|
236
298
|
temperature?: number;
|
|
237
299
|
max_output_tokens?: number;
|
|
300
|
+
/** Alias for `max_output_tokens` on this run (not a client default). */
|
|
301
|
+
maxOutputTokens?: number;
|
|
238
302
|
/** Caller metadata stored on the response row (console visibility). */
|
|
239
303
|
metadata?: Record<string, string>;
|
|
240
304
|
/** JSON Schema. When set, `data` on the result is the validated object. */
|
|
@@ -242,6 +306,23 @@ export interface RunOptions {
|
|
|
242
306
|
schemaName?: string;
|
|
243
307
|
/** Queue a background run instead of waiting. Returns the queued response. */
|
|
244
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;
|
|
245
326
|
}
|
|
246
327
|
export interface ResponseObject {
|
|
247
328
|
id: string;
|
|
@@ -273,10 +354,12 @@ export interface ResponseObject {
|
|
|
273
354
|
};
|
|
274
355
|
[k: string]: unknown;
|
|
275
356
|
}
|
|
357
|
+
export declare function isTerminalStatus(status: string): boolean;
|
|
276
358
|
export declare class AgentLu {
|
|
277
359
|
readonly baseUrl: string;
|
|
278
360
|
apiKey: string | undefined;
|
|
279
361
|
private readonly fetchImpl;
|
|
362
|
+
private readonly defaultTimeoutMs;
|
|
280
363
|
constructor(opts?: AgentLuOptions);
|
|
281
364
|
private postClient;
|
|
282
365
|
/** Get-or-create a client by name. Caches the returned key on this instance. */
|
|
@@ -295,12 +378,21 @@ export declare class AgentLu {
|
|
|
295
378
|
* The use-case's defaults (system prompt, tools, temperature, ...) live
|
|
296
379
|
* server-side on the client, so `run()` only needs the input. Registering a
|
|
297
380
|
* use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
|
|
298
|
-
|
|
381
|
+
*/
|
|
299
382
|
useCase(name: string, opts?: {
|
|
300
383
|
person?: string | null;
|
|
301
384
|
defaults?: ClientDefaults;
|
|
302
385
|
use_case_profile?: UseCaseProfile;
|
|
303
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>;
|
|
304
396
|
/**
|
|
305
397
|
* Register a use-case client and resolve to its api key. The key is cached
|
|
306
398
|
* on this instance (same semantics as `register`), so a following
|
|
@@ -316,7 +408,7 @@ export declare class AgentLu {
|
|
|
316
408
|
* Upload an image, video, or document for use with `run({ fileIds })`.
|
|
317
409
|
* Returns the `file_id` and stored `kind`.
|
|
318
410
|
*/
|
|
319
|
-
upload(data: Uint8Array | Blob, filename: string, mime?: string): Promise<{
|
|
411
|
+
upload(data: Uint8Array | Blob, filename: string, mime?: string, opts?: UploadOptions): Promise<{
|
|
320
412
|
id: string;
|
|
321
413
|
kind?: string;
|
|
322
414
|
}>;
|
|
@@ -335,9 +427,19 @@ export declare class AgentLu {
|
|
|
335
427
|
data: T;
|
|
336
428
|
response: ResponseObject;
|
|
337
429
|
}>;
|
|
430
|
+
runText(opts: RunOptions): Promise<{
|
|
431
|
+
text: string;
|
|
432
|
+
response: ResponseObject;
|
|
433
|
+
}>;
|
|
434
|
+
stream(opts: RunOptions): AsyncGenerator<Record<string, unknown>>;
|
|
338
435
|
private runForApiKey;
|
|
339
436
|
private runJsonForApiKey;
|
|
437
|
+
private runJsonOnce;
|
|
438
|
+
private runTextForApiKey;
|
|
340
439
|
private createRunResponse;
|
|
440
|
+
private buildRunBody;
|
|
441
|
+
private uploadRunFiles;
|
|
442
|
+
private transportFromRun;
|
|
341
443
|
readonly responses: {
|
|
342
444
|
create: (body: Record<string, unknown>) => Promise<ResponseObject>;
|
|
343
445
|
retrieve: (id: string) => Promise<ResponseObject>;
|
|
@@ -350,8 +452,11 @@ export declare class AgentLu {
|
|
|
350
452
|
data: ResponseObject[];
|
|
351
453
|
}>;
|
|
352
454
|
stream: (body: Record<string, unknown>) => AsyncGenerator<Record<string, unknown>, any, any>;
|
|
455
|
+
wait: (id: string, opts?: WaitOptions) => Promise<ResponseObject>;
|
|
353
456
|
};
|
|
354
|
-
private
|
|
457
|
+
private waitForApiKey;
|
|
458
|
+
private streamRunForApiKey;
|
|
459
|
+
private streamRawForApiKey;
|
|
355
460
|
readonly threads: {
|
|
356
461
|
create: (body?: Record<string, unknown>) => Promise<unknown>;
|
|
357
462
|
retrieve: (id: string) => Promise<unknown>;
|
|
@@ -375,17 +480,14 @@ export declare class AgentLu {
|
|
|
375
480
|
* `timeoutMs` bounds each attempt (timeouts are never retried), `maxAttempts`
|
|
376
481
|
* overrides the shared 5-attempt policy, and `signal` cancels the call.
|
|
377
482
|
*/
|
|
378
|
-
readonly decisions:
|
|
379
|
-
create: (body: DecisionOptions) => Promise<DecisionObject>;
|
|
380
|
-
/** Same call, returning only the answers map. */
|
|
381
|
-
ask: (body: DecisionOptions) => Promise<Record<string, DecisionAnswer>>;
|
|
382
|
-
};
|
|
483
|
+
readonly decisions: DecisionApi;
|
|
383
484
|
agents(): Promise<unknown>;
|
|
384
485
|
models(): Promise<unknown>;
|
|
385
486
|
tools(): Promise<unknown>;
|
|
386
487
|
health(): Promise<unknown>;
|
|
387
488
|
private request;
|
|
388
489
|
private raw;
|
|
490
|
+
private rawOnce;
|
|
389
491
|
}
|
|
390
492
|
/**
|
|
391
493
|
* A runner bound to one registered use-case (one service client + its
|
|
@@ -409,8 +511,10 @@ export declare class UseCaseRunner {
|
|
|
409
511
|
defaults?: ClientDefaults;
|
|
410
512
|
use_case_profile?: UseCaseProfile;
|
|
411
513
|
});
|
|
412
|
-
/** Idempotent get-or-create; safe to call before every run. */
|
|
413
|
-
|
|
514
|
+
/** Idempotent get-or-create; safe to call on boot or before every run. */
|
|
515
|
+
ensure(): Promise<void>;
|
|
516
|
+
private invalidate;
|
|
517
|
+
private withAuthRetry;
|
|
414
518
|
/**
|
|
415
519
|
* Run with this use-case's key. Request fields that are set override the
|
|
416
520
|
* use-case's registered defaults; unset fields inherit them server-side.
|
|
@@ -423,13 +527,23 @@ export declare class UseCaseRunner {
|
|
|
423
527
|
data: T;
|
|
424
528
|
response: ResponseObject;
|
|
425
529
|
}>;
|
|
426
|
-
|
|
530
|
+
runText(opts: RunOptions): Promise<{
|
|
531
|
+
text: string;
|
|
532
|
+
response: ResponseObject;
|
|
533
|
+
}>;
|
|
534
|
+
stream(opts: RunOptions): AsyncGenerator<Record<string, unknown>>;
|
|
427
535
|
/**
|
|
428
536
|
* Upload with this use-case's key, so the file is owned by the same
|
|
429
537
|
* service client that will reference it via `run({ fileIds })`.
|
|
430
538
|
*/
|
|
431
|
-
upload(data: Uint8Array | Blob, filename: string, mime?: string): Promise<{
|
|
539
|
+
upload(data: Uint8Array | Blob, filename: string, mime?: string, opts?: UploadOptions): Promise<{
|
|
432
540
|
id: string;
|
|
433
541
|
kind?: string;
|
|
434
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;
|
|
435
549
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
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
13
|
/** Strip client-only decision options before the body reaches `/v1/decisions`. */
|
|
@@ -17,8 +17,12 @@ function decisionWire(body) {
|
|
|
17
17
|
wire.model = body.model;
|
|
18
18
|
return wire;
|
|
19
19
|
}
|
|
20
|
-
function decisionRequestOptions(body) {
|
|
21
|
-
return {
|
|
20
|
+
function decisionRequestOptions(body, defaultTimeoutMs) {
|
|
21
|
+
return {
|
|
22
|
+
timeoutMs: body.timeoutMs ?? defaultTimeoutMs,
|
|
23
|
+
maxAttempts: body.maxAttempts,
|
|
24
|
+
signal: body.signal,
|
|
25
|
+
};
|
|
22
26
|
}
|
|
23
27
|
/** Question builders; sugar over the wire shape. */
|
|
24
28
|
export const noul = (instructions, criteria) => ({
|
|
@@ -65,6 +69,9 @@ export class AgentLuResponseError extends AgentLuError {
|
|
|
65
69
|
this.responseStatus = responseStatus;
|
|
66
70
|
}
|
|
67
71
|
}
|
|
72
|
+
export function isTerminalStatus(status) {
|
|
73
|
+
return status === "completed" || status === "failed" || status === "incomplete" || status === "cancelled";
|
|
74
|
+
}
|
|
68
75
|
function env(name) {
|
|
69
76
|
return typeof process !== "undefined" ? process.env[name] : undefined;
|
|
70
77
|
}
|
|
@@ -73,16 +80,30 @@ export class AgentLu {
|
|
|
73
80
|
baseUrl;
|
|
74
81
|
apiKey;
|
|
75
82
|
fetchImpl;
|
|
83
|
+
defaultTimeoutMs;
|
|
76
84
|
constructor(opts = {}) {
|
|
77
85
|
this.baseUrl = (opts.baseUrl ?? env("AGENTLU_URL") ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
78
86
|
this.apiKey = opts.apiKey ?? env("AGENTLU_API_KEY");
|
|
79
87
|
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
88
|
+
this.defaultTimeoutMs = opts.timeoutMs;
|
|
80
89
|
scopedUseCaseOperations.set(this, {
|
|
81
90
|
register: (name, registerOpts) => this.registerForUseCase(name, registerOpts),
|
|
82
91
|
run: (apiKey, runOpts) => this.runForApiKey(apiKey, runOpts),
|
|
83
92
|
runJson: (apiKey, runOpts) => this.runJsonForApiKey(apiKey, runOpts),
|
|
84
|
-
|
|
85
|
-
|
|
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,
|
|
86
107
|
});
|
|
87
108
|
}
|
|
88
109
|
postClient(name, opts) {
|
|
@@ -101,10 +122,30 @@ export class AgentLu {
|
|
|
101
122
|
* The use-case's defaults (system prompt, tools, temperature, ...) live
|
|
102
123
|
* server-side on the client, so `run()` only needs the input. Registering a
|
|
103
124
|
* use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
|
|
104
|
-
|
|
125
|
+
*/
|
|
105
126
|
useCase(name, opts) {
|
|
106
127
|
return new UseCaseRunner(this, name, opts);
|
|
107
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
|
+
}
|
|
108
149
|
/**
|
|
109
150
|
* Register a use-case client and resolve to its api key. The key is cached
|
|
110
151
|
* on this instance (same semantics as `register`), so a following
|
|
@@ -127,14 +168,19 @@ export class AgentLu {
|
|
|
127
168
|
* Upload an image, video, or document for use with `run({ fileIds })`.
|
|
128
169
|
* Returns the `file_id` and stored `kind`.
|
|
129
170
|
*/
|
|
130
|
-
async upload(data, filename, mime) {
|
|
131
|
-
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);
|
|
132
173
|
}
|
|
133
|
-
async uploadForApiKey(data, filename, mime, apiKey) {
|
|
174
|
+
async uploadForApiKey(data, filename, mime, apiKey, opts) {
|
|
134
175
|
const form = new FormData();
|
|
135
176
|
const blob = data instanceof Blob ? data : new Blob([new Uint8Array(data)], { type: mime ?? "application/octet-stream" });
|
|
136
177
|
form.append("file", blob, filename);
|
|
137
|
-
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
|
+
});
|
|
138
184
|
return (await res.json());
|
|
139
185
|
}
|
|
140
186
|
async me() {
|
|
@@ -152,6 +198,12 @@ export class AgentLu {
|
|
|
152
198
|
assertJsonRunOptions(opts);
|
|
153
199
|
return this.runJsonForApiKey(this.apiKey, opts);
|
|
154
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
|
+
}
|
|
155
207
|
async runForApiKey(apiKey, opts) {
|
|
156
208
|
const response = await this.createRunResponse(opts, apiKey);
|
|
157
209
|
if (response.status === "failed" && response.error) {
|
|
@@ -160,6 +212,21 @@ export class AgentLu {
|
|
|
160
212
|
return { data: response.output_parsed, response };
|
|
161
213
|
}
|
|
162
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) {
|
|
163
230
|
const response = await this.createRunResponse(opts, apiKey);
|
|
164
231
|
if (response.status !== "completed")
|
|
165
232
|
throw new AgentLuResponseError(response);
|
|
@@ -168,15 +235,32 @@ export class AgentLu {
|
|
|
168
235
|
}
|
|
169
236
|
return { data: response.output_parsed, response };
|
|
170
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
|
+
}
|
|
171
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 ?? [])];
|
|
172
256
|
let input = opts.inputMessages ?? opts.input;
|
|
173
|
-
if (
|
|
257
|
+
if (fileIds.length) {
|
|
174
258
|
input = [
|
|
175
259
|
{
|
|
176
260
|
role: "user",
|
|
177
261
|
content: [
|
|
178
|
-
...
|
|
179
|
-
const kind =
|
|
262
|
+
...fileIds.map((file_id, i) => {
|
|
263
|
+
const kind = fileKinds[i];
|
|
180
264
|
if (kind === "video")
|
|
181
265
|
return { type: "input_video", file_id };
|
|
182
266
|
if (kind === "text" || kind === "document")
|
|
@@ -195,16 +279,18 @@ export class AgentLu {
|
|
|
195
279
|
model: opts.model,
|
|
196
280
|
instructions: opts.instructions,
|
|
197
281
|
tools: opts.tools,
|
|
282
|
+
tool_choice: opts.tool_choice,
|
|
198
283
|
reasoning: opts.reasoning,
|
|
199
284
|
conversation: opts.conversation,
|
|
285
|
+
previous_response_id: opts.previous_response_id,
|
|
200
286
|
store: opts.store,
|
|
201
287
|
person: opts.person,
|
|
202
288
|
owner: opts.owner,
|
|
203
289
|
title: opts.title,
|
|
204
|
-
timeout_ms: opts.timeout_ms,
|
|
290
|
+
timeout_ms: opts.timeout_ms ?? opts.timeoutMs ?? this.defaultTimeoutMs,
|
|
205
291
|
include: opts.include,
|
|
206
292
|
temperature: opts.temperature,
|
|
207
|
-
max_output_tokens: opts.max_output_tokens,
|
|
293
|
+
max_output_tokens: opts.max_output_tokens ?? opts.maxOutputTokens,
|
|
208
294
|
background: opts.background,
|
|
209
295
|
metadata: opts.metadata,
|
|
210
296
|
};
|
|
@@ -221,7 +307,33 @@ export class AgentLu {
|
|
|
221
307
|
},
|
|
222
308
|
};
|
|
223
309
|
}
|
|
224
|
-
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
|
+
};
|
|
225
337
|
}
|
|
226
338
|
responses = {
|
|
227
339
|
create: (body) => this.request("POST", "/responses", body),
|
|
@@ -236,10 +348,42 @@ export class AgentLu {
|
|
|
236
348
|
const qs = q.toString();
|
|
237
349
|
return this.request("GET", `/responses${qs ? `?${qs}` : ""}`);
|
|
238
350
|
},
|
|
239
|
-
stream: (body) => this.
|
|
351
|
+
stream: (body) => this.streamRawForApiKey(body, this.apiKey),
|
|
352
|
+
wait: (id, opts) => this.waitForApiKey(id, this.apiKey, opts),
|
|
240
353
|
};
|
|
241
|
-
async
|
|
242
|
-
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 });
|
|
243
387
|
if (!res.body)
|
|
244
388
|
throw new AgentLuError(0, "internal_error", "empty stream body", true);
|
|
245
389
|
const reader = res.body.getReader();
|
|
@@ -251,13 +395,13 @@ export class AgentLu {
|
|
|
251
395
|
if (done)
|
|
252
396
|
break;
|
|
253
397
|
buf += decoder.decode(value, { stream: true });
|
|
254
|
-
const lines = buf.split(
|
|
398
|
+
const lines = buf.split(/\r?\n/);
|
|
255
399
|
buf = lines.pop() ?? "";
|
|
256
400
|
for (const line of lines) {
|
|
257
401
|
const trimmed = line.trim();
|
|
258
|
-
if (!trimmed.startsWith("data:
|
|
402
|
+
if (!trimmed.startsWith("data:"))
|
|
259
403
|
continue;
|
|
260
|
-
const payload = trimmed.slice(
|
|
404
|
+
const payload = trimmed.slice(5).trim();
|
|
261
405
|
if (payload === "[DONE]")
|
|
262
406
|
return;
|
|
263
407
|
try {
|
|
@@ -270,6 +414,7 @@ export class AgentLu {
|
|
|
270
414
|
}
|
|
271
415
|
}
|
|
272
416
|
finally {
|
|
417
|
+
await reader.cancel().catch(() => { });
|
|
273
418
|
reader.releaseLock();
|
|
274
419
|
}
|
|
275
420
|
}
|
|
@@ -297,9 +442,16 @@ export class AgentLu {
|
|
|
297
442
|
* overrides the shared 5-attempt policy, and `signal` cancels the call.
|
|
298
443
|
*/
|
|
299
444
|
decisions = {
|
|
300
|
-
create: (body) => this.request("POST", "/decisions", decisionWire(body), decisionRequestOptions(body)),
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
},
|
|
303
455
|
};
|
|
304
456
|
async agents() {
|
|
305
457
|
return this.request("GET", "/agents", undefined, { auth: false });
|
|
@@ -314,6 +466,12 @@ export class AgentLu {
|
|
|
314
466
|
return this.request("GET", "/health", undefined, { auth: false });
|
|
315
467
|
}
|
|
316
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) {
|
|
317
475
|
// `409 thread_busy` and `503 too_many_active_runs` are correctness bounds,
|
|
318
476
|
// not rate limits. Retrying with backoff is almost always the right thing,
|
|
319
477
|
// so the SDK does it - honoring Retry-After when the server sends one.
|
|
@@ -327,11 +485,7 @@ export class AgentLu {
|
|
|
327
485
|
let last;
|
|
328
486
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
329
487
|
try {
|
|
330
|
-
|
|
331
|
-
if (res.status === 204)
|
|
332
|
-
return undefined;
|
|
333
|
-
const json = (await res.json());
|
|
334
|
-
return json;
|
|
488
|
+
return await this.rawOnce(method, path, body, opts);
|
|
335
489
|
}
|
|
336
490
|
catch (e) {
|
|
337
491
|
if (e instanceof AgentLuError &&
|
|
@@ -348,7 +502,7 @@ export class AgentLu {
|
|
|
348
502
|
}
|
|
349
503
|
throw last ?? new AgentLuError(503, "too_many_active_runs", "too many retries", true);
|
|
350
504
|
}
|
|
351
|
-
async
|
|
505
|
+
async rawOnce(method, path, body, opts) {
|
|
352
506
|
const headers = {};
|
|
353
507
|
const isForm = typeof FormData !== "undefined" && body instanceof FormData;
|
|
354
508
|
if (body !== undefined && !isForm) {
|
|
@@ -357,13 +511,19 @@ export class AgentLu {
|
|
|
357
511
|
const apiKey = opts?.apiKey ?? this.apiKey;
|
|
358
512
|
if (opts?.auth !== false && apiKey)
|
|
359
513
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
360
|
-
const signal = requestSignal(opts?.signal, opts?.timeoutMs);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
}
|
|
367
527
|
if (!res.ok) {
|
|
368
528
|
let code = `http_${res.status}`;
|
|
369
529
|
let message = `HTTP ${res.status}`;
|
|
@@ -407,7 +567,7 @@ export class UseCaseRunner {
|
|
|
407
567
|
this.registerOpts = opts ?? {};
|
|
408
568
|
this.operations = scopedUseCaseOperations.get(parent);
|
|
409
569
|
}
|
|
410
|
-
/** Idempotent get-or-create; safe to call before every run. */
|
|
570
|
+
/** Idempotent get-or-create; safe to call on boot or before every run. */
|
|
411
571
|
ensure() {
|
|
412
572
|
if (this.apiKey)
|
|
413
573
|
return Promise.resolve();
|
|
@@ -423,37 +583,123 @@ export class UseCaseRunner {
|
|
|
423
583
|
}
|
|
424
584
|
return this.ready;
|
|
425
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
|
+
}
|
|
426
604
|
/**
|
|
427
605
|
* Run with this use-case's key. Request fields that are set override the
|
|
428
606
|
* use-case's registered defaults; unset fields inherit them server-side.
|
|
429
607
|
*/
|
|
430
608
|
async run(opts) {
|
|
431
|
-
await this.
|
|
432
|
-
return (await this.operations.run(this.apiKey, opts));
|
|
609
|
+
return (await this.withAuthRetry((apiKey) => this.operations.run(apiKey, opts)));
|
|
433
610
|
}
|
|
434
611
|
async runJson(opts) {
|
|
435
612
|
assertJsonRunOptions(opts);
|
|
436
|
-
await this.
|
|
437
|
-
|
|
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));
|
|
438
617
|
}
|
|
439
|
-
async stream(
|
|
618
|
+
async *stream(opts) {
|
|
440
619
|
await this.ensure();
|
|
441
|
-
|
|
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
|
+
}
|
|
442
630
|
}
|
|
443
631
|
/**
|
|
444
632
|
* Upload with this use-case's key, so the file is owned by the same
|
|
445
633
|
* service client that will reference it via `run({ fileIds })`.
|
|
446
634
|
*/
|
|
447
|
-
async upload(data, filename, mime) {
|
|
448
|
-
|
|
449
|
-
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));
|
|
450
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
|
+
};
|
|
451
655
|
}
|
|
452
656
|
function assertJsonRunOptions(opts) {
|
|
453
657
|
if (opts.background === true) {
|
|
454
658
|
throw new AgentLuError(400, "background_not_supported", "runJson requires a completed response and cannot use background.", false);
|
|
455
659
|
}
|
|
456
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
|
+
}
|
|
457
703
|
function parseRetryAfter(res) {
|
|
458
704
|
const header = res.headers.get("retry-after");
|
|
459
705
|
if (!header)
|
package/package.json
CHANGED