@betterlu/agentlu 1.5.0 → 1.7.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 +214 -20
- package/dist/index.js +493 -63
- 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,36 @@
|
|
|
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
|
+
/**
|
|
14
|
+
* Accept an origin (`http://host:8912`) or a `/v1` base and normalize to the
|
|
15
|
+
* `/v1` base the API actually serves, so `new AgentLu({ baseUrl: "http://host:8912" })`
|
|
16
|
+
* works the same as the documented default.
|
|
17
|
+
*/
|
|
18
|
+
export declare function normalizeBaseUrl(raw: string): string;
|
|
13
19
|
export type Backend = "pi";
|
|
14
20
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
15
21
|
export type ApiToolFamily = "web_search" | "fetch_content" | "hublu" | "memory" | "image" | "browser" | "android";
|
|
22
|
+
export type ApiToolChoice = "auto" | "none" | "required";
|
|
16
23
|
/** Closed tool-family request accepted by `/v1/responses`. */
|
|
17
24
|
export interface ApiToolSpec {
|
|
18
25
|
type: ApiToolFamily;
|
|
19
26
|
/** Optional family-specific operation allowlist (HubLu, BrowserLu, or AndroidLu). */
|
|
20
27
|
allowed_tools?: string[];
|
|
21
28
|
}
|
|
22
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Native MCP server for the machine-only `api` agent. Mounted per run;
|
|
31
|
+
* `allowed_tools` pre-scopes the names the server lists.
|
|
32
|
+
*/
|
|
33
|
+
export interface ApiMcpServerSpec {
|
|
34
|
+
type: "mcp";
|
|
35
|
+
url: string;
|
|
36
|
+
allowed_tools?: string[];
|
|
37
|
+
}
|
|
38
|
+
export type ApiToolInput = ApiToolFamily | ApiToolSpec | ApiMcpServerSpec;
|
|
23
39
|
export type BrowserStatus = "active" | "needs_human" | "human_control" | "idle" | "closed" | "interrupted";
|
|
24
40
|
export interface BrowserScreenshot {
|
|
25
41
|
id: string;
|
|
@@ -87,6 +103,14 @@ export type TextFormat = TextFormatText | TextFormatJsonSchema;
|
|
|
87
103
|
export interface TextConfig {
|
|
88
104
|
format?: TextFormat;
|
|
89
105
|
}
|
|
106
|
+
export type FileKind = "image" | "video" | "text" | "document";
|
|
107
|
+
/** Local file to upload and attach on the same `run` / `stream`. */
|
|
108
|
+
export interface RunFile {
|
|
109
|
+
data: Uint8Array | Blob;
|
|
110
|
+
filename: string;
|
|
111
|
+
mime?: string;
|
|
112
|
+
kind?: FileKind;
|
|
113
|
+
}
|
|
90
114
|
/** Yes/no, answered as the probability that the answer is yes. */
|
|
91
115
|
export interface NoulQuestion {
|
|
92
116
|
type: "noul";
|
|
@@ -137,7 +161,7 @@ export interface DecisionOptions {
|
|
|
137
161
|
/**
|
|
138
162
|
* Per-attempt client deadline in milliseconds. A timeout aborts the request
|
|
139
163
|
* and is never retried; without it the request is bounded only by the
|
|
140
|
-
* caller's fetch implementation.
|
|
164
|
+
* caller's fetch implementation (or the AgentLu constructor `timeoutMs`).
|
|
141
165
|
*/
|
|
142
166
|
timeoutMs?: number;
|
|
143
167
|
/** Total attempts including the first. Defaults to the shared 5-attempt policy. */
|
|
@@ -158,6 +182,12 @@ export interface DecisionObject {
|
|
|
158
182
|
} | null;
|
|
159
183
|
[k: string]: unknown;
|
|
160
184
|
}
|
|
185
|
+
export interface DecisionApi {
|
|
186
|
+
create: (body: DecisionOptions) => Promise<DecisionObject>;
|
|
187
|
+
ask: (body: DecisionOptions) => Promise<Record<string, DecisionAnswer>>;
|
|
188
|
+
/** Fail-open: returns `null` when the model is unconfigured or the call fails. */
|
|
189
|
+
askOrNull: (body: DecisionOptions) => Promise<Record<string, DecisionAnswer> | null>;
|
|
190
|
+
}
|
|
161
191
|
/** Question builders; sugar over the wire shape. */
|
|
162
192
|
export declare const noul: (instructions: string, criteria?: {
|
|
163
193
|
true: string;
|
|
@@ -184,6 +214,12 @@ export interface AgentLuOptions {
|
|
|
184
214
|
baseUrl?: string;
|
|
185
215
|
apiKey?: string;
|
|
186
216
|
fetch?: typeof fetch;
|
|
217
|
+
/**
|
|
218
|
+
* Default per-attempt client deadline for `run` / `stream` / `upload` /
|
|
219
|
+
* `decisions` when the call omits `timeoutMs`. Also fills `timeout_ms` on
|
|
220
|
+
* `/v1/responses` when the call omits that field.
|
|
221
|
+
*/
|
|
222
|
+
timeoutMs?: number;
|
|
187
223
|
}
|
|
188
224
|
export interface ClientDefaults {
|
|
189
225
|
backend?: Backend;
|
|
@@ -196,6 +232,11 @@ export interface ClientDefaults {
|
|
|
196
232
|
store?: boolean;
|
|
197
233
|
reasoning?: ClientReasoningOptions;
|
|
198
234
|
text?: TextConfig;
|
|
235
|
+
/**
|
|
236
|
+
* Retired on client defaults (server ignores it). Pass `max_output_tokens`
|
|
237
|
+
* or `maxOutputTokens` on `run` / `runJson` / `stream` instead.
|
|
238
|
+
*/
|
|
239
|
+
maxOutputTokens?: never;
|
|
199
240
|
}
|
|
200
241
|
/** High-level use-case declaration for model routing and priority tier. */
|
|
201
242
|
export interface UseCaseProfile {
|
|
@@ -204,18 +245,33 @@ export interface UseCaseProfile {
|
|
|
204
245
|
/** MiniMax priority admission consent (priority costs 1.5x standard). */
|
|
205
246
|
priorityTier?: "never" | "retry" | "first" | "always";
|
|
206
247
|
}
|
|
248
|
+
export interface UseCaseDeclaration {
|
|
249
|
+
/** Registered client name. Defaults to the catalog key. */
|
|
250
|
+
name?: string;
|
|
251
|
+
person?: string | null;
|
|
252
|
+
defaults?: ClientDefaults;
|
|
253
|
+
use_case_profile?: UseCaseProfile;
|
|
254
|
+
}
|
|
255
|
+
export type UseCaseCatalog<T extends Record<string, UseCaseDeclaration>> = {
|
|
256
|
+
[K in keyof T]: UseCaseRunner;
|
|
257
|
+
} & {
|
|
258
|
+
/** Idempotent get-or-create for every use-case. Safe to call on boot. */
|
|
259
|
+
ensure: () => Promise<void>;
|
|
260
|
+
};
|
|
207
261
|
export interface RunOptions {
|
|
208
262
|
backend?: Backend;
|
|
209
263
|
agent?: string;
|
|
210
264
|
model?: string;
|
|
211
265
|
input?: string;
|
|
212
266
|
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
267
|
+
* Attachments previously uploaded via `upload()`. Prepended to the user
|
|
268
|
+
* turn using each file's `kind` (`input_image` / `input_video` / `input_file`).
|
|
269
|
+
*/
|
|
216
270
|
fileIds?: string[];
|
|
217
271
|
/** Optional kinds aligned with `fileIds` (from `upload()`). Defaults to image. */
|
|
218
|
-
fileKinds?:
|
|
272
|
+
fileKinds?: FileKind[];
|
|
273
|
+
/** Upload-and-attach in one call. Combined with `fileIds` when both are set. */
|
|
274
|
+
files?: RunFile[];
|
|
219
275
|
/** Structured input messages (OpenAI shape) - overrides `input`. */
|
|
220
276
|
inputMessages?: Array<{
|
|
221
277
|
role: string;
|
|
@@ -223,18 +279,32 @@ export interface RunOptions {
|
|
|
223
279
|
}>;
|
|
224
280
|
instructions?: string;
|
|
225
281
|
tools?: ApiToolInput[];
|
|
282
|
+
tool_choice?: ApiToolChoice;
|
|
226
283
|
reasoning?: ReasoningOptions;
|
|
227
284
|
text?: TextConfig;
|
|
228
285
|
strict?: boolean;
|
|
229
286
|
conversation?: string;
|
|
287
|
+
previous_response_id?: string;
|
|
230
288
|
store?: boolean;
|
|
231
289
|
person?: string | null;
|
|
232
290
|
owner?: "client" | "person";
|
|
233
291
|
title?: string;
|
|
292
|
+
/** Server-side run budget (wire name). */
|
|
234
293
|
timeout_ms?: number;
|
|
294
|
+
/**
|
|
295
|
+
* Per-attempt client deadline. When `timeout_ms` is omitted, the same value
|
|
296
|
+
* is also sent as the server-side budget.
|
|
297
|
+
*/
|
|
298
|
+
timeoutMs?: number;
|
|
299
|
+
/** Caller cancellation, combined with `timeoutMs` when both are set. */
|
|
300
|
+
signal?: AbortSignal;
|
|
301
|
+
/** Total HTTP attempts including the first. Defaults to the shared 5-attempt policy. */
|
|
302
|
+
maxAttempts?: number;
|
|
235
303
|
include?: string[];
|
|
236
304
|
temperature?: number;
|
|
237
305
|
max_output_tokens?: number;
|
|
306
|
+
/** Alias for `max_output_tokens` on this run (not a client default). */
|
|
307
|
+
maxOutputTokens?: number;
|
|
238
308
|
/** Caller metadata stored on the response row (console visibility). */
|
|
239
309
|
metadata?: Record<string, string>;
|
|
240
310
|
/** JSON Schema. When set, `data` on the result is the validated object. */
|
|
@@ -242,6 +312,36 @@ export interface RunOptions {
|
|
|
242
312
|
schemaName?: string;
|
|
243
313
|
/** Queue a background run instead of waiting. Returns the queued response. */
|
|
244
314
|
background?: boolean;
|
|
315
|
+
/**
|
|
316
|
+
* Explicit idempotency key. Non-streaming runs always send one (generated
|
|
317
|
+
* when omitted) so SDK-level retries cannot double-run; set it yourself to
|
|
318
|
+
* make two separate `run` calls idempotent.
|
|
319
|
+
*/
|
|
320
|
+
idempotencyKey?: string;
|
|
321
|
+
/**
|
|
322
|
+
* `runJson` only: extra model attempts after `schema_validation_failed`,
|
|
323
|
+
* `missing_output_parsed`, or `incomplete`. Defaults to 1 (no extra tries).
|
|
324
|
+
*/
|
|
325
|
+
attempts?: number;
|
|
326
|
+
/** `run`/`runJson`/`runText` only: throw instead of returning when the run is `incomplete`. */
|
|
327
|
+
throwOnIncomplete?: boolean;
|
|
328
|
+
}
|
|
329
|
+
export interface WaitOptions {
|
|
330
|
+
/** Delay between polls. Defaults to 1000ms. */
|
|
331
|
+
intervalMs?: number;
|
|
332
|
+
/**
|
|
333
|
+
* Overall wait deadline (default 10 minutes). The in-flight poll is aborted
|
|
334
|
+
* when it fires. Pass `0` to wait indefinitely.
|
|
335
|
+
*/
|
|
336
|
+
timeoutMs?: number;
|
|
337
|
+
signal?: AbortSignal;
|
|
338
|
+
}
|
|
339
|
+
/** Default overall deadline for `responses.wait`. */
|
|
340
|
+
export declare const DEFAULT_WAIT_TIMEOUT_MS: number;
|
|
341
|
+
export interface UploadOptions {
|
|
342
|
+
timeoutMs?: number;
|
|
343
|
+
signal?: AbortSignal;
|
|
344
|
+
maxAttempts?: number;
|
|
245
345
|
}
|
|
246
346
|
export interface ResponseObject {
|
|
247
347
|
id: string;
|
|
@@ -273,10 +373,36 @@ export interface ResponseObject {
|
|
|
273
373
|
};
|
|
274
374
|
[k: string]: unknown;
|
|
275
375
|
}
|
|
376
|
+
export declare function isTerminalStatus(status: string): boolean;
|
|
377
|
+
/**
|
|
378
|
+
* One SSE event from `/v1/responses` with `stream: true`. The event names and
|
|
379
|
+
* shapes mirror the OpenAI Responses stream; `delta` is the incremental text
|
|
380
|
+
* for `response.output_text.delta`.
|
|
381
|
+
*/
|
|
382
|
+
export interface StreamEvent {
|
|
383
|
+
type: string;
|
|
384
|
+
sequence_number?: number;
|
|
385
|
+
/** Incremental text on `response.output_text.delta`. */
|
|
386
|
+
delta?: string;
|
|
387
|
+
[k: string]: unknown;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Accumulate a raw stream into typed events and the final text. `events()`
|
|
391
|
+
* yields every event; `finalText()` resolves after the stream ends with the
|
|
392
|
+
* concatenated `response.output_text.delta` payloads.
|
|
393
|
+
*
|
|
394
|
+
* const stream = alu.typedStream({ input: "hi" });
|
|
395
|
+
* for await (const ev of stream) { ... }
|
|
396
|
+
* const text = stream.finalText();
|
|
397
|
+
*/
|
|
398
|
+
export declare function typedStream(source: AsyncIterable<Record<string, unknown>>): AsyncIterable<StreamEvent> & {
|
|
399
|
+
finalText: () => Promise<string>;
|
|
400
|
+
};
|
|
276
401
|
export declare class AgentLu {
|
|
277
402
|
readonly baseUrl: string;
|
|
278
403
|
apiKey: string | undefined;
|
|
279
404
|
private readonly fetchImpl;
|
|
405
|
+
private readonly defaultTimeoutMs;
|
|
280
406
|
constructor(opts?: AgentLuOptions);
|
|
281
407
|
private postClient;
|
|
282
408
|
/** Get-or-create a client by name. Caches the returned key on this instance. */
|
|
@@ -295,12 +421,21 @@ export declare class AgentLu {
|
|
|
295
421
|
* The use-case's defaults (system prompt, tools, temperature, ...) live
|
|
296
422
|
* server-side on the client, so `run()` only needs the input. Registering a
|
|
297
423
|
* use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
|
|
298
|
-
|
|
424
|
+
*/
|
|
299
425
|
useCase(name: string, opts?: {
|
|
300
426
|
person?: string | null;
|
|
301
427
|
defaults?: ClientDefaults;
|
|
302
428
|
use_case_profile?: UseCaseProfile;
|
|
303
429
|
}): UseCaseRunner;
|
|
430
|
+
/**
|
|
431
|
+
* Several named use-case runners that share this client. Call `ensure()` on
|
|
432
|
+
* boot, then `catalog.extract.runJson({ input })`.
|
|
433
|
+
*
|
|
434
|
+
* const ai = alu.useCases({
|
|
435
|
+
* extract: { name: "deallu-extract", defaults: { agent: "api", store: false } },
|
|
436
|
+
* });
|
|
437
|
+
*/
|
|
438
|
+
useCases<T extends Record<string, UseCaseDeclaration>>(specs: T): UseCaseCatalog<T>;
|
|
304
439
|
/**
|
|
305
440
|
* Register a use-case client and resolve to its api key. The key is cached
|
|
306
441
|
* on this instance (same semantics as `register`), so a following
|
|
@@ -316,7 +451,7 @@ export declare class AgentLu {
|
|
|
316
451
|
* Upload an image, video, or document for use with `run({ fileIds })`.
|
|
317
452
|
* Returns the `file_id` and stored `kind`.
|
|
318
453
|
*/
|
|
319
|
-
upload(data: Uint8Array | Blob, filename: string, mime?: string): Promise<{
|
|
454
|
+
upload(data: Uint8Array | Blob, filename: string, mime?: string, opts?: UploadOptions): Promise<{
|
|
320
455
|
id: string;
|
|
321
456
|
kind?: string;
|
|
322
457
|
}>;
|
|
@@ -335,9 +470,33 @@ export declare class AgentLu {
|
|
|
335
470
|
data: T;
|
|
336
471
|
response: ResponseObject;
|
|
337
472
|
}>;
|
|
473
|
+
runText(opts: RunOptions): Promise<{
|
|
474
|
+
text: string;
|
|
475
|
+
response: ResponseObject;
|
|
476
|
+
}>;
|
|
477
|
+
/**
|
|
478
|
+
* `runJson` that returns `null` instead of throwing on any failure. For
|
|
479
|
+
* best-effort enrichment where a missing result is a normal outcome.
|
|
480
|
+
*/
|
|
481
|
+
runJsonOrNull<T = unknown>(opts: RunOptions): Promise<T | null>;
|
|
482
|
+
/** `runText` that returns `null` instead of throwing on any failure. */
|
|
483
|
+
runTextOrNull(opts: RunOptions): Promise<string | null>;
|
|
484
|
+
stream(opts: RunOptions): AsyncGenerator<Record<string, unknown>>;
|
|
485
|
+
/**
|
|
486
|
+
* Typed stream: yields `StreamEvent`s and exposes `finalText()` with the
|
|
487
|
+
* concatenated output text after the stream ends.
|
|
488
|
+
*/
|
|
489
|
+
typedStream(opts: RunOptions): AsyncIterable<StreamEvent> & {
|
|
490
|
+
finalText: () => Promise<string>;
|
|
491
|
+
};
|
|
338
492
|
private runForApiKey;
|
|
339
493
|
private runJsonForApiKey;
|
|
494
|
+
private runJsonOnce;
|
|
495
|
+
private runTextForApiKey;
|
|
340
496
|
private createRunResponse;
|
|
497
|
+
private buildRunBody;
|
|
498
|
+
private uploadRunFiles;
|
|
499
|
+
private transportFromRun;
|
|
341
500
|
readonly responses: {
|
|
342
501
|
create: (body: Record<string, unknown>) => Promise<ResponseObject>;
|
|
343
502
|
retrieve: (id: string) => Promise<ResponseObject>;
|
|
@@ -350,8 +509,11 @@ export declare class AgentLu {
|
|
|
350
509
|
data: ResponseObject[];
|
|
351
510
|
}>;
|
|
352
511
|
stream: (body: Record<string, unknown>) => AsyncGenerator<Record<string, unknown>, any, any>;
|
|
512
|
+
wait: (id: string, opts?: WaitOptions) => Promise<ResponseObject>;
|
|
353
513
|
};
|
|
354
|
-
private
|
|
514
|
+
private waitForApiKey;
|
|
515
|
+
private streamRunForApiKey;
|
|
516
|
+
private streamRawForApiKey;
|
|
355
517
|
readonly threads: {
|
|
356
518
|
create: (body?: Record<string, unknown>) => Promise<unknown>;
|
|
357
519
|
retrieve: (id: string) => Promise<unknown>;
|
|
@@ -375,17 +537,14 @@ export declare class AgentLu {
|
|
|
375
537
|
* `timeoutMs` bounds each attempt (timeouts are never retried), `maxAttempts`
|
|
376
538
|
* overrides the shared 5-attempt policy, and `signal` cancels the call.
|
|
377
539
|
*/
|
|
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
|
-
};
|
|
540
|
+
readonly decisions: DecisionApi;
|
|
383
541
|
agents(): Promise<unknown>;
|
|
384
542
|
models(): Promise<unknown>;
|
|
385
543
|
tools(): Promise<unknown>;
|
|
386
544
|
health(): Promise<unknown>;
|
|
387
545
|
private request;
|
|
388
546
|
private raw;
|
|
547
|
+
private rawOnce;
|
|
389
548
|
}
|
|
390
549
|
/**
|
|
391
550
|
* A runner bound to one registered use-case (one service client + its
|
|
@@ -404,13 +563,28 @@ export declare class UseCaseRunner {
|
|
|
404
563
|
private readonly operations;
|
|
405
564
|
private apiKey;
|
|
406
565
|
private ready;
|
|
566
|
+
/** In-flight/short-lived guard so a burst of runs does not stampede registration. */
|
|
567
|
+
private registerFailures;
|
|
568
|
+
private lastRegisterError;
|
|
569
|
+
private registerFailedAt;
|
|
570
|
+
/** Cooldown is short and only suppresses *repeated* failures in one burst. */
|
|
571
|
+
private static readonly REGISTER_COOLDOWN_MS;
|
|
407
572
|
constructor(parent: AgentLu, name: string, opts?: {
|
|
408
573
|
person?: string | null;
|
|
409
574
|
defaults?: ClientDefaults;
|
|
410
575
|
use_case_profile?: UseCaseProfile;
|
|
411
576
|
});
|
|
412
|
-
/**
|
|
413
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Idempotent get-or-create; safe to call on boot or before every run.
|
|
579
|
+
* A failed registration is remembered for a short cooldown so an app that
|
|
580
|
+
* calls `ensure()` in a loop does not hammer the server; runs always retry
|
|
581
|
+
* registration transparently (pass `force`).
|
|
582
|
+
*/
|
|
583
|
+
ensure(opts?: {
|
|
584
|
+
force?: boolean;
|
|
585
|
+
}): Promise<void>;
|
|
586
|
+
private invalidate;
|
|
587
|
+
private withAuthRetry;
|
|
414
588
|
/**
|
|
415
589
|
* Run with this use-case's key. Request fields that are set override the
|
|
416
590
|
* use-case's registered defaults; unset fields inherit them server-side.
|
|
@@ -423,13 +597,33 @@ export declare class UseCaseRunner {
|
|
|
423
597
|
data: T;
|
|
424
598
|
response: ResponseObject;
|
|
425
599
|
}>;
|
|
426
|
-
|
|
600
|
+
runText(opts: RunOptions): Promise<{
|
|
601
|
+
text: string;
|
|
602
|
+
response: ResponseObject;
|
|
603
|
+
}>;
|
|
604
|
+
/** `runJson` that returns `null` instead of throwing on any failure. */
|
|
605
|
+
runJsonOrNull<T = unknown>(opts: RunOptions): Promise<T | null>;
|
|
606
|
+
/** `runText` that returns `null` instead of throwing on any failure. */
|
|
607
|
+
runTextOrNull(opts: RunOptions): Promise<string | null>;
|
|
608
|
+
stream(opts: RunOptions): AsyncGenerator<Record<string, unknown>>;
|
|
609
|
+
/** Typed stream for this use-case: events + `finalText()`. */
|
|
610
|
+
typedStream(opts: RunOptions): AsyncIterable<StreamEvent> & {
|
|
611
|
+
finalText: () => Promise<string>;
|
|
612
|
+
};
|
|
427
613
|
/**
|
|
428
614
|
* Upload with this use-case's key, so the file is owned by the same
|
|
429
615
|
* service client that will reference it via `run({ fileIds })`.
|
|
430
616
|
*/
|
|
431
|
-
upload(data: Uint8Array | Blob, filename: string, mime?: string): Promise<{
|
|
617
|
+
upload(data: Uint8Array | Blob, filename: string, mime?: string, opts?: UploadOptions): Promise<{
|
|
432
618
|
id: string;
|
|
433
619
|
kind?: string;
|
|
434
620
|
}>;
|
|
621
|
+
readonly responses: {
|
|
622
|
+
retrieve: (id: string) => Promise<ResponseObject>;
|
|
623
|
+
cancel: (id: string) => Promise<ResponseObject>;
|
|
624
|
+
wait: (id: string, opts?: WaitOptions) => Promise<ResponseObject>;
|
|
625
|
+
};
|
|
626
|
+
readonly decisions: DecisionApi;
|
|
435
627
|
}
|
|
628
|
+
/** True when an error should be retried by the shared policy. */
|
|
629
|
+
export declare function isRetryableError(err: unknown): boolean;
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,21 @@
|
|
|
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
|
+
/**
|
|
14
|
+
* Accept an origin (`http://host:8912`) or a `/v1` base and normalize to the
|
|
15
|
+
* `/v1` base the API actually serves, so `new AgentLu({ baseUrl: "http://host:8912" })`
|
|
16
|
+
* works the same as the documented default.
|
|
17
|
+
*/
|
|
18
|
+
export function normalizeBaseUrl(raw) {
|
|
19
|
+
const trimmed = raw.replace(/\/+$/, "");
|
|
20
|
+
if (/\/v1$/.test(trimmed))
|
|
21
|
+
return trimmed;
|
|
22
|
+
return `${trimmed}/v1`;
|
|
23
|
+
}
|
|
13
24
|
/** Strip client-only decision options before the body reaches `/v1/decisions`. */
|
|
14
25
|
function decisionWire(body) {
|
|
15
26
|
const wire = { state: body.state, questions: body.questions };
|
|
@@ -17,8 +28,12 @@ function decisionWire(body) {
|
|
|
17
28
|
wire.model = body.model;
|
|
18
29
|
return wire;
|
|
19
30
|
}
|
|
20
|
-
function decisionRequestOptions(body) {
|
|
21
|
-
return {
|
|
31
|
+
function decisionRequestOptions(body, defaultTimeoutMs) {
|
|
32
|
+
return {
|
|
33
|
+
timeoutMs: body.timeoutMs ?? defaultTimeoutMs,
|
|
34
|
+
maxAttempts: body.maxAttempts,
|
|
35
|
+
signal: body.signal,
|
|
36
|
+
};
|
|
22
37
|
}
|
|
23
38
|
/** Question builders; sugar over the wire shape. */
|
|
24
39
|
export const noul = (instructions, criteria) => ({
|
|
@@ -65,6 +80,49 @@ export class AgentLuResponseError extends AgentLuError {
|
|
|
65
80
|
this.responseStatus = responseStatus;
|
|
66
81
|
}
|
|
67
82
|
}
|
|
83
|
+
/** Default overall deadline for `responses.wait`. */
|
|
84
|
+
export const DEFAULT_WAIT_TIMEOUT_MS = 10 * 60_000;
|
|
85
|
+
export function isTerminalStatus(status) {
|
|
86
|
+
return status === "completed" || status === "failed" || status === "incomplete" || status === "cancelled";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Accumulate a raw stream into typed events and the final text. `events()`
|
|
90
|
+
* yields every event; `finalText()` resolves after the stream ends with the
|
|
91
|
+
* concatenated `response.output_text.delta` payloads.
|
|
92
|
+
*
|
|
93
|
+
* const stream = alu.typedStream({ input: "hi" });
|
|
94
|
+
* for await (const ev of stream) { ... }
|
|
95
|
+
* const text = stream.finalText();
|
|
96
|
+
*/
|
|
97
|
+
export function typedStream(source) {
|
|
98
|
+
let text = "";
|
|
99
|
+
let done = false;
|
|
100
|
+
const it = (async function* () {
|
|
101
|
+
try {
|
|
102
|
+
for await (const raw of source) {
|
|
103
|
+
const ev = raw;
|
|
104
|
+
if (ev.type === "response.output_text.delta" && typeof ev.delta === "string") {
|
|
105
|
+
text += ev.delta;
|
|
106
|
+
}
|
|
107
|
+
yield ev;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
done = true;
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
return Object.assign(it, {
|
|
115
|
+
finalText: async () => {
|
|
116
|
+
// Drain if the caller never iterated to the end.
|
|
117
|
+
if (!done) {
|
|
118
|
+
for await (const _ of it) {
|
|
119
|
+
void _;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return text;
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
68
126
|
function env(name) {
|
|
69
127
|
return typeof process !== "undefined" ? process.env[name] : undefined;
|
|
70
128
|
}
|
|
@@ -73,20 +131,37 @@ export class AgentLu {
|
|
|
73
131
|
baseUrl;
|
|
74
132
|
apiKey;
|
|
75
133
|
fetchImpl;
|
|
134
|
+
defaultTimeoutMs;
|
|
76
135
|
constructor(opts = {}) {
|
|
77
|
-
this.baseUrl = (opts.baseUrl ?? env("AGENTLU_URL") ?? DEFAULT_BASE_URL)
|
|
136
|
+
this.baseUrl = normalizeBaseUrl(opts.baseUrl ?? env("AGENTLU_URL") ?? DEFAULT_BASE_URL);
|
|
78
137
|
this.apiKey = opts.apiKey ?? env("AGENTLU_API_KEY");
|
|
79
138
|
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
139
|
+
this.defaultTimeoutMs = opts.timeoutMs;
|
|
80
140
|
scopedUseCaseOperations.set(this, {
|
|
81
141
|
register: (name, registerOpts) => this.registerForUseCase(name, registerOpts),
|
|
82
142
|
run: (apiKey, runOpts) => this.runForApiKey(apiKey, runOpts),
|
|
83
143
|
runJson: (apiKey, runOpts) => this.runJsonForApiKey(apiKey, runOpts),
|
|
84
|
-
|
|
85
|
-
|
|
144
|
+
runText: (apiKey, runOpts) => this.runTextForApiKey(apiKey, runOpts),
|
|
145
|
+
stream: (apiKey, runOpts) => this.streamRunForApiKey(runOpts, apiKey),
|
|
146
|
+
upload: (apiKey, data, filename, mime, uploadOpts) => this.uploadForApiKey(data, filename, mime, apiKey, uploadOpts),
|
|
147
|
+
wait: (apiKey, id, waitOpts) => this.waitForApiKey(id, apiKey, waitOpts),
|
|
148
|
+
retrieve: (apiKey, id, reqOpts) => this.request("GET", `/responses/${encodeURIComponent(id)}`, undefined, {
|
|
149
|
+
...reqOpts,
|
|
150
|
+
apiKey,
|
|
151
|
+
}),
|
|
152
|
+
cancel: (apiKey, id, reqOpts) => this.request("POST", `/responses/${encodeURIComponent(id)}/cancel`, undefined, {
|
|
153
|
+
...reqOpts,
|
|
154
|
+
apiKey,
|
|
155
|
+
}),
|
|
156
|
+
decideCreate: (apiKey, body) => this.request("POST", "/decisions", decisionWire(body), { ...decisionRequestOptions(body, this.defaultTimeoutMs), apiKey }),
|
|
157
|
+
decideAsk: async (apiKey, body) => (await this.request("POST", "/decisions", decisionWire(body), { ...decisionRequestOptions(body, this.defaultTimeoutMs), apiKey })).answers,
|
|
86
158
|
});
|
|
87
159
|
}
|
|
88
160
|
postClient(name, opts) {
|
|
89
|
-
|
|
161
|
+
// Registration is get-or-create and callers gate it behind `ensure()`'s
|
|
162
|
+
// cooldown; two attempts smooth a blip without stacking long backoffs
|
|
163
|
+
// before the cooldown can do its job.
|
|
164
|
+
return this.request("POST", "/clients", { name, ...opts }, { auth: false, maxAttempts: 2 });
|
|
90
165
|
}
|
|
91
166
|
/** Get-or-create a client by name. Caches the returned key on this instance. */
|
|
92
167
|
async register(name, opts) {
|
|
@@ -101,10 +176,30 @@ export class AgentLu {
|
|
|
101
176
|
* The use-case's defaults (system prompt, tools, temperature, ...) live
|
|
102
177
|
* server-side on the client, so `run()` only needs the input. Registering a
|
|
103
178
|
* use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
|
|
104
|
-
|
|
179
|
+
*/
|
|
105
180
|
useCase(name, opts) {
|
|
106
181
|
return new UseCaseRunner(this, name, opts);
|
|
107
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Several named use-case runners that share this client. Call `ensure()` on
|
|
185
|
+
* boot, then `catalog.extract.runJson({ input })`.
|
|
186
|
+
*
|
|
187
|
+
* const ai = alu.useCases({
|
|
188
|
+
* extract: { name: "deallu-extract", defaults: { agent: "api", store: false } },
|
|
189
|
+
* });
|
|
190
|
+
*/
|
|
191
|
+
useCases(specs) {
|
|
192
|
+
const runners = {};
|
|
193
|
+
for (const key of Object.keys(specs)) {
|
|
194
|
+
const spec = specs[key];
|
|
195
|
+
runners[key] = this.useCase(spec.name ?? String(key), spec);
|
|
196
|
+
}
|
|
197
|
+
const catalog = { ...runners };
|
|
198
|
+
catalog.ensure = async () => {
|
|
199
|
+
await Promise.all(Object.values(runners).map((runner) => runner.ensure()));
|
|
200
|
+
};
|
|
201
|
+
return catalog;
|
|
202
|
+
}
|
|
108
203
|
/**
|
|
109
204
|
* Register a use-case client and resolve to its api key. The key is cached
|
|
110
205
|
* on this instance (same semantics as `register`), so a following
|
|
@@ -127,14 +222,19 @@ export class AgentLu {
|
|
|
127
222
|
* Upload an image, video, or document for use with `run({ fileIds })`.
|
|
128
223
|
* Returns the `file_id` and stored `kind`.
|
|
129
224
|
*/
|
|
130
|
-
async upload(data, filename, mime) {
|
|
131
|
-
return this.uploadForApiKey(data, filename, mime, this.apiKey);
|
|
225
|
+
async upload(data, filename, mime, opts) {
|
|
226
|
+
return this.uploadForApiKey(data, filename, mime, this.apiKey, opts);
|
|
132
227
|
}
|
|
133
|
-
async uploadForApiKey(data, filename, mime, apiKey) {
|
|
228
|
+
async uploadForApiKey(data, filename, mime, apiKey, opts) {
|
|
134
229
|
const form = new FormData();
|
|
135
230
|
const blob = data instanceof Blob ? data : new Blob([new Uint8Array(data)], { type: mime ?? "application/octet-stream" });
|
|
136
231
|
form.append("file", blob, filename);
|
|
137
|
-
const res = await this.raw("POST", "/files", form, {
|
|
232
|
+
const res = await this.raw("POST", "/files", form, {
|
|
233
|
+
apiKey,
|
|
234
|
+
timeoutMs: opts?.timeoutMs,
|
|
235
|
+
signal: opts?.signal,
|
|
236
|
+
maxAttempts: opts?.maxAttempts,
|
|
237
|
+
});
|
|
138
238
|
return (await res.json());
|
|
139
239
|
}
|
|
140
240
|
async me() {
|
|
@@ -152,14 +252,68 @@ export class AgentLu {
|
|
|
152
252
|
assertJsonRunOptions(opts);
|
|
153
253
|
return this.runJsonForApiKey(this.apiKey, opts);
|
|
154
254
|
}
|
|
255
|
+
async runText(opts) {
|
|
256
|
+
return this.runTextForApiKey(this.apiKey, opts);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* `runJson` that returns `null` instead of throwing on any failure. For
|
|
260
|
+
* best-effort enrichment where a missing result is a normal outcome.
|
|
261
|
+
*/
|
|
262
|
+
async runJsonOrNull(opts) {
|
|
263
|
+
try {
|
|
264
|
+
const { data } = await this.runJson(opts);
|
|
265
|
+
return data;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** `runText` that returns `null` instead of throwing on any failure. */
|
|
272
|
+
async runTextOrNull(opts) {
|
|
273
|
+
try {
|
|
274
|
+
const { text } = await this.runText(opts);
|
|
275
|
+
return text;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async *stream(opts) {
|
|
282
|
+
yield* this.streamRunForApiKey(opts, this.apiKey);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Typed stream: yields `StreamEvent`s and exposes `finalText()` with the
|
|
286
|
+
* concatenated output text after the stream ends.
|
|
287
|
+
*/
|
|
288
|
+
typedStream(opts) {
|
|
289
|
+
return typedStream(this.stream(opts));
|
|
290
|
+
}
|
|
155
291
|
async runForApiKey(apiKey, opts) {
|
|
156
292
|
const response = await this.createRunResponse(opts, apiKey);
|
|
157
293
|
if (response.status === "failed" && response.error) {
|
|
158
294
|
throw new AgentLuError(200, response.error.code, response.error.message, response.error.retryable);
|
|
159
295
|
}
|
|
296
|
+
if (opts.throwOnIncomplete && response.status === "incomplete") {
|
|
297
|
+
throw new AgentLuResponseError(response);
|
|
298
|
+
}
|
|
160
299
|
return { data: response.output_parsed, response };
|
|
161
300
|
}
|
|
162
301
|
async runJsonForApiKey(apiKey, opts) {
|
|
302
|
+
const attempts = jsonAttempts(opts);
|
|
303
|
+
let last;
|
|
304
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
305
|
+
try {
|
|
306
|
+
return await this.runJsonOnce(apiKey, opts);
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
last = e;
|
|
310
|
+
if (!isJsonRetryable(e) || attempt >= attempts - 1)
|
|
311
|
+
throw e;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
throw last;
|
|
315
|
+
}
|
|
316
|
+
async runJsonOnce(apiKey, opts) {
|
|
163
317
|
const response = await this.createRunResponse(opts, apiKey);
|
|
164
318
|
if (response.status !== "completed")
|
|
165
319
|
throw new AgentLuResponseError(response);
|
|
@@ -168,15 +322,44 @@ export class AgentLu {
|
|
|
168
322
|
}
|
|
169
323
|
return { data: response.output_parsed, response };
|
|
170
324
|
}
|
|
325
|
+
async runTextForApiKey(apiKey, opts) {
|
|
326
|
+
const response = await this.createRunResponse(opts, apiKey);
|
|
327
|
+
if (response.status !== "completed")
|
|
328
|
+
throw new AgentLuResponseError(response);
|
|
329
|
+
const text = response.output_text ?? "";
|
|
330
|
+
if (!text.trim()) {
|
|
331
|
+
throw new AgentLuError(200, "empty_output", "Completed response did not include output_text.", false);
|
|
332
|
+
}
|
|
333
|
+
return { text, response };
|
|
334
|
+
}
|
|
171
335
|
async createRunResponse(opts, apiKey) {
|
|
336
|
+
const body = await this.buildRunBody(opts, apiKey);
|
|
337
|
+
const transport = this.transportFromRun(opts, apiKey);
|
|
338
|
+
// A non-streaming run is retried on transient faults; an auto key makes
|
|
339
|
+
// those retries safe (the server replays the first outcome instead of
|
|
340
|
+
// starting a second run). `createRunResponse` is only used by the sync
|
|
341
|
+
// paths; streaming has its own method and never sends this header.
|
|
342
|
+
const headers = {};
|
|
343
|
+
if (opts.idempotencyKey) {
|
|
344
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
headers["Idempotency-Key"] = randomIdempotencyKey();
|
|
348
|
+
}
|
|
349
|
+
return this.request("POST", "/responses", body, { ...transport, headers });
|
|
350
|
+
}
|
|
351
|
+
async buildRunBody(opts, apiKey) {
|
|
352
|
+
const uploaded = await this.uploadRunFiles(opts, apiKey);
|
|
353
|
+
const fileIds = [...uploaded.fileIds, ...(opts.fileIds ?? [])];
|
|
354
|
+
const fileKinds = [...uploaded.fileKinds, ...(opts.fileKinds ?? [])];
|
|
172
355
|
let input = opts.inputMessages ?? opts.input;
|
|
173
|
-
if (
|
|
356
|
+
if (fileIds.length) {
|
|
174
357
|
input = [
|
|
175
358
|
{
|
|
176
359
|
role: "user",
|
|
177
360
|
content: [
|
|
178
|
-
...
|
|
179
|
-
const kind =
|
|
361
|
+
...fileIds.map((file_id, i) => {
|
|
362
|
+
const kind = fileKinds[i];
|
|
180
363
|
if (kind === "video")
|
|
181
364
|
return { type: "input_video", file_id };
|
|
182
365
|
if (kind === "text" || kind === "document")
|
|
@@ -195,16 +378,18 @@ export class AgentLu {
|
|
|
195
378
|
model: opts.model,
|
|
196
379
|
instructions: opts.instructions,
|
|
197
380
|
tools: opts.tools,
|
|
381
|
+
tool_choice: opts.tool_choice,
|
|
198
382
|
reasoning: opts.reasoning,
|
|
199
383
|
conversation: opts.conversation,
|
|
384
|
+
previous_response_id: opts.previous_response_id,
|
|
200
385
|
store: opts.store,
|
|
201
386
|
person: opts.person,
|
|
202
387
|
owner: opts.owner,
|
|
203
388
|
title: opts.title,
|
|
204
|
-
timeout_ms: opts.timeout_ms,
|
|
389
|
+
timeout_ms: opts.timeout_ms ?? opts.timeoutMs ?? this.defaultTimeoutMs,
|
|
205
390
|
include: opts.include,
|
|
206
391
|
temperature: opts.temperature,
|
|
207
|
-
max_output_tokens: opts.max_output_tokens,
|
|
392
|
+
max_output_tokens: opts.max_output_tokens ?? opts.maxOutputTokens,
|
|
208
393
|
background: opts.background,
|
|
209
394
|
metadata: opts.metadata,
|
|
210
395
|
};
|
|
@@ -221,7 +406,36 @@ export class AgentLu {
|
|
|
221
406
|
},
|
|
222
407
|
};
|
|
223
408
|
}
|
|
224
|
-
return
|
|
409
|
+
return body;
|
|
410
|
+
}
|
|
411
|
+
async uploadRunFiles(opts, apiKey) {
|
|
412
|
+
const files = opts.files ?? [];
|
|
413
|
+
if (!files.length)
|
|
414
|
+
return { fileIds: [], fileKinds: [] };
|
|
415
|
+
const transport = this.transportFromRun(opts, apiKey);
|
|
416
|
+
const fileIds = [];
|
|
417
|
+
const fileKinds = [];
|
|
418
|
+
for (const file of files) {
|
|
419
|
+
const uploaded = await this.uploadForApiKey(file.data, file.filename, file.mime, apiKey, {
|
|
420
|
+
timeoutMs: transport.timeoutMs,
|
|
421
|
+
signal: transport.signal,
|
|
422
|
+
maxAttempts: transport.maxAttempts,
|
|
423
|
+
});
|
|
424
|
+
fileIds.push(uploaded.id);
|
|
425
|
+
fileKinds.push(file.kind ?? kindFromMime(file.mime, uploaded.kind));
|
|
426
|
+
}
|
|
427
|
+
return { fileIds, fileKinds };
|
|
428
|
+
}
|
|
429
|
+
transportFromRun(opts, apiKey) {
|
|
430
|
+
// The server budget and the client deadline must not race: give the client
|
|
431
|
+
// 5s of slack so the server's own timeout/error envelope arrives first.
|
|
432
|
+
const serverBudgetMs = opts.timeout_ms ?? opts.timeoutMs ?? this.defaultTimeoutMs;
|
|
433
|
+
return {
|
|
434
|
+
apiKey,
|
|
435
|
+
timeoutMs: serverBudgetMs !== undefined ? serverBudgetMs + 5_000 : this.defaultTimeoutMs,
|
|
436
|
+
maxAttempts: opts.maxAttempts,
|
|
437
|
+
signal: opts.signal,
|
|
438
|
+
};
|
|
225
439
|
}
|
|
226
440
|
responses = {
|
|
227
441
|
create: (body) => this.request("POST", "/responses", body),
|
|
@@ -236,10 +450,47 @@ export class AgentLu {
|
|
|
236
450
|
const qs = q.toString();
|
|
237
451
|
return this.request("GET", `/responses${qs ? `?${qs}` : ""}`);
|
|
238
452
|
},
|
|
239
|
-
stream: (body) => this.
|
|
453
|
+
stream: (body) => this.streamRawForApiKey(body, this.apiKey),
|
|
454
|
+
wait: (id, opts) => this.waitForApiKey(id, this.apiKey, opts),
|
|
240
455
|
};
|
|
241
|
-
async
|
|
242
|
-
const
|
|
456
|
+
async waitForApiKey(id, apiKey, opts) {
|
|
457
|
+
const intervalMs = Math.max(opts?.intervalMs ?? 1_000, 50);
|
|
458
|
+
// A background run may sit behind a queue; without a default deadline a
|
|
459
|
+
// caller that forgets timeoutMs polls forever.
|
|
460
|
+
const deadlineMs = opts?.timeoutMs === 0
|
|
461
|
+
? undefined
|
|
462
|
+
: (opts?.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS);
|
|
463
|
+
const started = Date.now();
|
|
464
|
+
for (;;) {
|
|
465
|
+
if (opts?.signal?.aborted) {
|
|
466
|
+
throw opts.signal.reason instanceof Error
|
|
467
|
+
? opts.signal.reason
|
|
468
|
+
: new AgentLuError(0, "aborted", "wait aborted", false);
|
|
469
|
+
}
|
|
470
|
+
const remaining = deadlineMs !== undefined ? Math.max(deadlineMs - (Date.now() - started), 1) : undefined;
|
|
471
|
+
const row = await this.request("GET", `/responses/${encodeURIComponent(id)}`, undefined, {
|
|
472
|
+
apiKey,
|
|
473
|
+
signal: opts?.signal,
|
|
474
|
+
timeoutMs: remaining ?? this.defaultTimeoutMs,
|
|
475
|
+
maxAttempts: 1,
|
|
476
|
+
});
|
|
477
|
+
if (isTerminalStatus(row.status))
|
|
478
|
+
return row;
|
|
479
|
+
if (deadlineMs !== undefined && Date.now() - started + intervalMs >= deadlineMs) {
|
|
480
|
+
throw new AgentLuError(408, "timeout", `Timed out waiting for response ${id}`, true);
|
|
481
|
+
}
|
|
482
|
+
await sleep(intervalMs);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async *streamRunForApiKey(opts, apiKey) {
|
|
486
|
+
if (opts.background === true) {
|
|
487
|
+
throw new AgentLuError(400, "background_not_supported", "stream cannot use background.", false);
|
|
488
|
+
}
|
|
489
|
+
const body = await this.buildRunBody(opts, apiKey);
|
|
490
|
+
yield* this.streamRawForApiKey(body, apiKey, this.transportFromRun(opts, apiKey));
|
|
491
|
+
}
|
|
492
|
+
async *streamRawForApiKey(body, apiKey, transport) {
|
|
493
|
+
const res = await this.raw("POST", "/responses", { ...body, stream: true }, { ...transport, apiKey: transport?.apiKey ?? apiKey });
|
|
243
494
|
if (!res.body)
|
|
244
495
|
throw new AgentLuError(0, "internal_error", "empty stream body", true);
|
|
245
496
|
const reader = res.body.getReader();
|
|
@@ -251,13 +502,13 @@ export class AgentLu {
|
|
|
251
502
|
if (done)
|
|
252
503
|
break;
|
|
253
504
|
buf += decoder.decode(value, { stream: true });
|
|
254
|
-
const lines = buf.split(
|
|
505
|
+
const lines = buf.split(/\r?\n/);
|
|
255
506
|
buf = lines.pop() ?? "";
|
|
256
507
|
for (const line of lines) {
|
|
257
508
|
const trimmed = line.trim();
|
|
258
|
-
if (!trimmed.startsWith("data:
|
|
509
|
+
if (!trimmed.startsWith("data:"))
|
|
259
510
|
continue;
|
|
260
|
-
const payload = trimmed.slice(
|
|
511
|
+
const payload = trimmed.slice(5).trim();
|
|
261
512
|
if (payload === "[DONE]")
|
|
262
513
|
return;
|
|
263
514
|
try {
|
|
@@ -270,6 +521,7 @@ export class AgentLu {
|
|
|
270
521
|
}
|
|
271
522
|
}
|
|
272
523
|
finally {
|
|
524
|
+
await reader.cancel().catch(() => { });
|
|
273
525
|
reader.releaseLock();
|
|
274
526
|
}
|
|
275
527
|
}
|
|
@@ -297,9 +549,16 @@ export class AgentLu {
|
|
|
297
549
|
* overrides the shared 5-attempt policy, and `signal` cancels the call.
|
|
298
550
|
*/
|
|
299
551
|
decisions = {
|
|
300
|
-
create: (body) => this.request("POST", "/decisions", decisionWire(body), decisionRequestOptions(body)),
|
|
301
|
-
|
|
302
|
-
|
|
552
|
+
create: (body) => this.request("POST", "/decisions", decisionWire(body), decisionRequestOptions(body, this.defaultTimeoutMs)),
|
|
553
|
+
ask: async (body) => (await this.request("POST", "/decisions", decisionWire(body), decisionRequestOptions(body, this.defaultTimeoutMs))).answers,
|
|
554
|
+
askOrNull: async (body) => {
|
|
555
|
+
try {
|
|
556
|
+
return await this.decisions.ask(body);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
},
|
|
303
562
|
};
|
|
304
563
|
async agents() {
|
|
305
564
|
return this.request("GET", "/agents", undefined, { auth: false });
|
|
@@ -314,31 +573,29 @@ export class AgentLu {
|
|
|
314
573
|
return this.request("GET", "/health", undefined, { auth: false });
|
|
315
574
|
}
|
|
316
575
|
async request(method, path, body, opts) {
|
|
317
|
-
|
|
318
|
-
|
|
576
|
+
const res = await this.raw(method, path, body, opts);
|
|
577
|
+
if (res.status === 204)
|
|
578
|
+
return undefined;
|
|
579
|
+
return (await res.json());
|
|
580
|
+
}
|
|
581
|
+
async raw(method, path, body, opts) {
|
|
582
|
+
// `409 thread_busy`, `503 too_many_active_runs`, network faults, 502/504
|
|
583
|
+
// and any server-marked retryable error are correctness/transient bounds,
|
|
584
|
+
// not rate limits. Retrying with jittered backoff is almost always right,
|
|
319
585
|
// so the SDK does it - honoring Retry-After when the server sends one.
|
|
320
586
|
// A 503 can also signal a config boundary (no decision model key); those
|
|
321
587
|
// are not retryable and must surface immediately. Callers can cap attempts
|
|
322
588
|
// per request (`maxAttempts`); a timeout/caller abort is never retried.
|
|
323
|
-
const nonRetryable503 = new Set(["systemone_unconfigured"]);
|
|
324
589
|
const maxAttempts = opts?.maxAttempts !== undefined && Number.isFinite(opts.maxAttempts) && opts.maxAttempts >= 1
|
|
325
590
|
? Math.floor(opts.maxAttempts)
|
|
326
591
|
: 5;
|
|
327
592
|
let last;
|
|
328
593
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
329
594
|
try {
|
|
330
|
-
|
|
331
|
-
if (res.status === 204)
|
|
332
|
-
return undefined;
|
|
333
|
-
const json = (await res.json());
|
|
334
|
-
return json;
|
|
595
|
+
return await this.rawOnce(method, path, body, opts);
|
|
335
596
|
}
|
|
336
597
|
catch (e) {
|
|
337
|
-
if (e
|
|
338
|
-
(e.code === "thread_busy" ||
|
|
339
|
-
e.code === "too_many_active_runs" ||
|
|
340
|
-
(e.status === 503 && !nonRetryable503.has(e.code))) &&
|
|
341
|
-
attempt < maxAttempts - 1) {
|
|
598
|
+
if (isRetryableError(e) && attempt < maxAttempts - 1) {
|
|
342
599
|
last = e;
|
|
343
600
|
await sleep(retryDelayMs(e, attempt));
|
|
344
601
|
continue;
|
|
@@ -348,7 +605,7 @@ export class AgentLu {
|
|
|
348
605
|
}
|
|
349
606
|
throw last ?? new AgentLuError(503, "too_many_active_runs", "too many retries", true);
|
|
350
607
|
}
|
|
351
|
-
async
|
|
608
|
+
async rawOnce(method, path, body, opts) {
|
|
352
609
|
const headers = {};
|
|
353
610
|
const isForm = typeof FormData !== "undefined" && body instanceof FormData;
|
|
354
611
|
if (body !== undefined && !isForm) {
|
|
@@ -357,13 +614,21 @@ export class AgentLu {
|
|
|
357
614
|
const apiKey = opts?.apiKey ?? this.apiKey;
|
|
358
615
|
if (opts?.auth !== false && apiKey)
|
|
359
616
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
617
|
+
if (opts?.headers)
|
|
618
|
+
Object.assign(headers, opts.headers);
|
|
619
|
+
const signal = requestSignal(opts?.signal, opts?.timeoutMs ?? this.defaultTimeoutMs);
|
|
620
|
+
let res;
|
|
621
|
+
try {
|
|
622
|
+
res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
623
|
+
method,
|
|
624
|
+
headers,
|
|
625
|
+
...(signal ? { signal } : {}),
|
|
626
|
+
body: body === undefined ? undefined : isForm ? body : JSON.stringify(body),
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
catch (e) {
|
|
630
|
+
throw mapFetchError(e, opts?.signal);
|
|
631
|
+
}
|
|
367
632
|
if (!res.ok) {
|
|
368
633
|
let code = `http_${res.status}`;
|
|
369
634
|
let message = `HTTP ${res.status}`;
|
|
@@ -402,58 +667,191 @@ export class UseCaseRunner {
|
|
|
402
667
|
operations;
|
|
403
668
|
apiKey = null;
|
|
404
669
|
ready = null;
|
|
670
|
+
/** In-flight/short-lived guard so a burst of runs does not stampede registration. */
|
|
671
|
+
registerFailures = 0;
|
|
672
|
+
lastRegisterError = null;
|
|
673
|
+
registerFailedAt = null;
|
|
674
|
+
/** Cooldown is short and only suppresses *repeated* failures in one burst. */
|
|
675
|
+
static REGISTER_COOLDOWN_MS = 1_000;
|
|
405
676
|
constructor(parent, name, opts) {
|
|
406
677
|
this.name = name;
|
|
407
678
|
this.registerOpts = opts ?? {};
|
|
408
679
|
this.operations = scopedUseCaseOperations.get(parent);
|
|
409
680
|
}
|
|
410
|
-
/**
|
|
411
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Idempotent get-or-create; safe to call on boot or before every run.
|
|
683
|
+
* A failed registration is remembered for a short cooldown so an app that
|
|
684
|
+
* calls `ensure()` in a loop does not hammer the server; runs always retry
|
|
685
|
+
* registration transparently (pass `force`).
|
|
686
|
+
*/
|
|
687
|
+
ensure(opts) {
|
|
412
688
|
if (this.apiKey)
|
|
413
689
|
return Promise.resolve();
|
|
414
690
|
if (!this.ready) {
|
|
691
|
+
if (!opts?.force &&
|
|
692
|
+
this.registerFailedAt !== null &&
|
|
693
|
+
Date.now() - this.registerFailedAt < UseCaseRunner.REGISTER_COOLDOWN_MS) {
|
|
694
|
+
return Promise.reject(this.lastRegisterError ??
|
|
695
|
+
new AgentLuError(503, "register_cooldown", "use-case registration is cooling down", true));
|
|
696
|
+
}
|
|
415
697
|
this.ready = this.operations.register(this.name, this.registerOpts)
|
|
416
698
|
.then((key) => {
|
|
417
699
|
this.apiKey = key;
|
|
700
|
+
this.registerFailedAt = null;
|
|
701
|
+
this.registerFailures = 0;
|
|
702
|
+
this.lastRegisterError = null;
|
|
418
703
|
})
|
|
419
704
|
.catch((e) => {
|
|
420
705
|
this.ready = null;
|
|
706
|
+
this.registerFailedAt = Date.now();
|
|
707
|
+
this.registerFailures += 1;
|
|
708
|
+
this.lastRegisterError = e;
|
|
421
709
|
throw e;
|
|
422
710
|
});
|
|
423
711
|
}
|
|
424
712
|
return this.ready;
|
|
425
713
|
}
|
|
714
|
+
invalidate() {
|
|
715
|
+
this.apiKey = null;
|
|
716
|
+
this.ready = null;
|
|
717
|
+
}
|
|
718
|
+
async withAuthRetry(fn) {
|
|
719
|
+
await this.ensure({ force: true });
|
|
720
|
+
try {
|
|
721
|
+
return await fn(this.apiKey);
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
if (e instanceof AgentLuError && e.status === 401) {
|
|
725
|
+
this.invalidate();
|
|
726
|
+
await this.ensure({ force: true });
|
|
727
|
+
return await fn(this.apiKey);
|
|
728
|
+
}
|
|
729
|
+
throw e;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
426
732
|
/**
|
|
427
733
|
* Run with this use-case's key. Request fields that are set override the
|
|
428
734
|
* use-case's registered defaults; unset fields inherit them server-side.
|
|
429
735
|
*/
|
|
430
736
|
async run(opts) {
|
|
431
|
-
await this.
|
|
432
|
-
return (await this.operations.run(this.apiKey, opts));
|
|
737
|
+
return (await this.withAuthRetry((apiKey) => this.operations.run(apiKey, opts)));
|
|
433
738
|
}
|
|
434
739
|
async runJson(opts) {
|
|
435
740
|
assertJsonRunOptions(opts);
|
|
436
|
-
await this.
|
|
437
|
-
|
|
741
|
+
return (await this.withAuthRetry((apiKey) => this.operations.runJson(apiKey, opts)));
|
|
742
|
+
}
|
|
743
|
+
async runText(opts) {
|
|
744
|
+
return this.withAuthRetry((apiKey) => this.operations.runText(apiKey, opts));
|
|
745
|
+
}
|
|
746
|
+
/** `runJson` that returns `null` instead of throwing on any failure. */
|
|
747
|
+
async runJsonOrNull(opts) {
|
|
748
|
+
try {
|
|
749
|
+
const { data } = await this.runJson(opts);
|
|
750
|
+
return data;
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/** `runText` that returns `null` instead of throwing on any failure. */
|
|
757
|
+
async runTextOrNull(opts) {
|
|
758
|
+
try {
|
|
759
|
+
const { text } = await this.runText(opts);
|
|
760
|
+
return text;
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async *stream(opts) {
|
|
767
|
+
await this.ensure({ force: true });
|
|
768
|
+
try {
|
|
769
|
+
yield* this.operations.stream(this.apiKey, opts);
|
|
770
|
+
}
|
|
771
|
+
catch (e) {
|
|
772
|
+
if (!(e instanceof AgentLuError && e.status === 401))
|
|
773
|
+
throw e;
|
|
774
|
+
this.invalidate();
|
|
775
|
+
await this.ensure({ force: true });
|
|
776
|
+
yield* this.operations.stream(this.apiKey, opts);
|
|
777
|
+
}
|
|
438
778
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return this.
|
|
779
|
+
/** Typed stream for this use-case: events + `finalText()`. */
|
|
780
|
+
typedStream(opts) {
|
|
781
|
+
return typedStream(this.stream(opts));
|
|
442
782
|
}
|
|
443
783
|
/**
|
|
444
784
|
* Upload with this use-case's key, so the file is owned by the same
|
|
445
785
|
* service client that will reference it via `run({ fileIds })`.
|
|
446
786
|
*/
|
|
447
|
-
async upload(data, filename, mime) {
|
|
448
|
-
|
|
449
|
-
return this.operations.upload(this.apiKey, data, filename, mime);
|
|
787
|
+
async upload(data, filename, mime, opts) {
|
|
788
|
+
return this.withAuthRetry((apiKey) => this.operations.upload(apiKey, data, filename, mime, opts));
|
|
450
789
|
}
|
|
790
|
+
responses = {
|
|
791
|
+
retrieve: (id) => this.withAuthRetry((apiKey) => this.operations.retrieve(apiKey, id)),
|
|
792
|
+
cancel: (id) => this.withAuthRetry((apiKey) => this.operations.cancel(apiKey, id)),
|
|
793
|
+
wait: (id, opts) => this.withAuthRetry((apiKey) => this.operations.wait(apiKey, id, opts)),
|
|
794
|
+
};
|
|
795
|
+
decisions = {
|
|
796
|
+
create: (body) => this.withAuthRetry((apiKey) => this.operations.decideCreate(apiKey, body)),
|
|
797
|
+
ask: (body) => this.withAuthRetry((apiKey) => this.operations.decideAsk(apiKey, body)),
|
|
798
|
+
askOrNull: async (body) => {
|
|
799
|
+
try {
|
|
800
|
+
return await this.decisions.ask(body);
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
};
|
|
451
807
|
}
|
|
452
808
|
function assertJsonRunOptions(opts) {
|
|
453
809
|
if (opts.background === true) {
|
|
454
810
|
throw new AgentLuError(400, "background_not_supported", "runJson requires a completed response and cannot use background.", false);
|
|
455
811
|
}
|
|
456
812
|
}
|
|
813
|
+
function jsonAttempts(opts) {
|
|
814
|
+
if (opts.attempts !== undefined && Number.isFinite(opts.attempts) && opts.attempts >= 1) {
|
|
815
|
+
return Math.floor(opts.attempts);
|
|
816
|
+
}
|
|
817
|
+
return 1;
|
|
818
|
+
}
|
|
819
|
+
function isJsonRetryable(error) {
|
|
820
|
+
if (error instanceof AgentLuError && error.code === "missing_output_parsed")
|
|
821
|
+
return true;
|
|
822
|
+
if (error instanceof AgentLuResponseError) {
|
|
823
|
+
return error.code === "schema_validation_failed" || error.responseStatus === "incomplete";
|
|
824
|
+
}
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
function kindFromMime(mime, stored) {
|
|
828
|
+
if (stored === "video" || stored === "text" || stored === "document" || stored === "image")
|
|
829
|
+
return stored;
|
|
830
|
+
if (mime?.startsWith("video/"))
|
|
831
|
+
return "video";
|
|
832
|
+
if (mime?.startsWith("text/"))
|
|
833
|
+
return "text";
|
|
834
|
+
if (mime === "application/pdf")
|
|
835
|
+
return "document";
|
|
836
|
+
return "image";
|
|
837
|
+
}
|
|
838
|
+
function mapFetchError(error, callerSignal) {
|
|
839
|
+
if (error instanceof AgentLuError)
|
|
840
|
+
return error;
|
|
841
|
+
if (callerSignal?.aborted) {
|
|
842
|
+
const reason = callerSignal.reason;
|
|
843
|
+
if (reason instanceof AgentLuError)
|
|
844
|
+
return reason;
|
|
845
|
+
const message = reason instanceof Error ? reason.message : "Request aborted";
|
|
846
|
+
return new AgentLuError(0, "aborted", message, false);
|
|
847
|
+
}
|
|
848
|
+
const name = error instanceof Error ? error.name : "";
|
|
849
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
850
|
+
if (name === "TimeoutError" || name === "AbortError" || /timed out/i.test(message)) {
|
|
851
|
+
return new AgentLuError(408, "timeout", message || "Request timed out", true);
|
|
852
|
+
}
|
|
853
|
+
return new AgentLuError(0, "network_error", message || "fetch failed", true);
|
|
854
|
+
}
|
|
457
855
|
function parseRetryAfter(res) {
|
|
458
856
|
const header = res.headers.get("retry-after");
|
|
459
857
|
if (!header)
|
|
@@ -497,11 +895,43 @@ function requestSignal(signal, timeoutMs) {
|
|
|
497
895
|
return signals.length ? mergeSignals(signals) : undefined;
|
|
498
896
|
}
|
|
499
897
|
function retryDelayMs(err, attempt) {
|
|
898
|
+
// Jitter keeps a fleet of clients from retrying in lockstep after a blip.
|
|
899
|
+
const jitter = 0.75 + Math.random() * 0.5;
|
|
500
900
|
if (err.retryAfterMs !== undefined) {
|
|
501
|
-
return Math.min(10_000, Math.max(250, err.retryAfterMs));
|
|
901
|
+
return Math.min(10_000, Math.max(250, err.retryAfterMs * jitter));
|
|
502
902
|
}
|
|
503
|
-
return Math.min(10_000, 500 * 2 ** attempt);
|
|
903
|
+
return Math.min(10_000, 500 * 2 ** attempt * jitter);
|
|
904
|
+
}
|
|
905
|
+
/** Statuses worth a retry even without a server `retryable` flag. */
|
|
906
|
+
const RETRYABLE_STATUSES = new Set([502, 503, 504]);
|
|
907
|
+
/** True when an error should be retried by the shared policy. */
|
|
908
|
+
export function isRetryableError(err) {
|
|
909
|
+
if (!(err instanceof AgentLuError))
|
|
910
|
+
return false;
|
|
911
|
+
if (err.code === "systemone_unconfigured")
|
|
912
|
+
return false;
|
|
913
|
+
if (err.code === "thread_busy" || err.code === "too_many_active_runs")
|
|
914
|
+
return true;
|
|
915
|
+
// Network faults are retried; a client timeout is not (the deadline was hit
|
|
916
|
+
// and a retry would start a second run past the caller's budget).
|
|
917
|
+
if (err.code === "network_error")
|
|
918
|
+
return true;
|
|
919
|
+
if (RETRYABLE_STATUSES.has(err.status))
|
|
920
|
+
return true;
|
|
921
|
+
// The server marks quota exhaustion retryable:false; anything it marks
|
|
922
|
+
// retryable:true is safe to retry even on an otherwise-unremarkable status.
|
|
923
|
+
return err.retryable === true && err.status >= 500;
|
|
504
924
|
}
|
|
505
925
|
function sleep(ms) {
|
|
506
926
|
return new Promise((r) => setTimeout(r, ms));
|
|
507
927
|
}
|
|
928
|
+
/**
|
|
929
|
+
* Unique-enough idempotency key without assuming `crypto.randomUUID` exists
|
|
930
|
+
* (browser SDK consumers on older runtimes).
|
|
931
|
+
*/
|
|
932
|
+
function randomIdempotencyKey() {
|
|
933
|
+
const g = globalThis;
|
|
934
|
+
if (g.crypto?.randomUUID)
|
|
935
|
+
return `sdk_${g.crypto.randomUUID()}`;
|
|
936
|
+
return `sdk_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
937
|
+
}
|
package/package.json
CHANGED