@betterlu/agentlu 1.7.0 → 1.8.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 +120 -2
- package/dist/generated.d.ts +878 -0
- package/dist/generated.js +11 -0
- package/dist/index.d.ts +183 -49
- package/dist/index.js +235 -19
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED by scripts/gen-contract.ts - do not edit.
|
|
3
|
+
* Source of truth: src/shared/api-schema.ts (+ src/server/api/schema.ts).
|
|
4
|
+
* Regenerate: `npm run gen:contract`; CI drift check: `npm run check:contract`.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Wire types for the agentlu `/v1` service API, generated from the zod
|
|
8
|
+
* contract. They describe the bytes on the wire (snake_case); the SDK's own
|
|
9
|
+
* types are the ergonomic view over them.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,24 @@
|
|
|
8
8
|
* This client exists for the ergonomic bits OpenAI's SDK does not know about:
|
|
9
9
|
* get-or-create registration, typed `runJson` / `runText`, use-case catalogs,
|
|
10
10
|
* background `wait`, and auto-retry on `409 thread_busy`.
|
|
11
|
+
*
|
|
12
|
+
* Two tiers, two namespaces (1.8):
|
|
13
|
+
*
|
|
14
|
+
* - `alu.llm.text/json/stream(...)` - the direct tier: one profile-free
|
|
15
|
+
* provider call, no tools, no session.
|
|
16
|
+
* - `alu.agent.run/stream/background(...)` - the agent tier: a profile,
|
|
17
|
+
* tool families, threads and background runs.
|
|
18
|
+
* - `alu.run/runJson/runText/stream` stay as aliases of the agent tier.
|
|
19
|
+
*
|
|
20
|
+
* Errors carry a coarse `kind` (`unavailable | timeout | rate_limited |
|
|
21
|
+
* invalid_request | schema | incomplete | internal`) and `isUnavailable(err)`
|
|
22
|
+
* is the fail-open predicate for "the service could not answer right now".
|
|
23
|
+
*
|
|
24
|
+
* Wire types generated from the server's zod contract are re-exported here as
|
|
25
|
+
* `Wire*` types (see `generated.ts`, regenerate with `npm run gen:contract`).
|
|
11
26
|
*/
|
|
27
|
+
import type { WireAppAgentDeclaration, WireClientDefaults, WireUseCaseProfile } from "./generated.js";
|
|
28
|
+
export type { WireAppAgentDeclaration, WireAgentInfo, WireChatCompletion, WireChatCompletionRequest, WireClient, WireClientDefaults, WireCreateDecision, WireCreateResponse, WireCreateThreadRequest, WireDecision, WireError, WireFile, WireModelInfo, WireRegisterClientRequest, WireResponse, WireStreamEvent, WireThread, WireThreadMessage, WireToolInfo, WireUseCaseProfile, } from "./generated.js";
|
|
12
29
|
export declare const DEFAULT_BASE_URL = "http://localhost:8912/v1";
|
|
13
30
|
/**
|
|
14
31
|
* Accept an origin (`http://host:8912`) or a `/v1` base and normalize to the
|
|
@@ -86,9 +103,6 @@ export interface AndroidActivity {
|
|
|
86
103
|
export interface ReasoningOptions {
|
|
87
104
|
effort?: ThinkingLevel;
|
|
88
105
|
}
|
|
89
|
-
export interface ClientReasoningOptions {
|
|
90
|
-
effort: ThinkingLevel;
|
|
91
|
-
}
|
|
92
106
|
export interface TextFormatText {
|
|
93
107
|
type: "text";
|
|
94
108
|
}
|
|
@@ -195,6 +209,21 @@ export declare const noul: (instructions: string, criteria?: {
|
|
|
195
209
|
}) => NoulQuestion;
|
|
196
210
|
export declare const choice: (instructions: string, criteria: Record<string, string>) => ChoiceQuestion;
|
|
197
211
|
export declare const score: (instructions: string, levels: string[]) => ScoreQuestion;
|
|
212
|
+
/**
|
|
213
|
+
* Coarse, stable classification of an SDK error, for retry/degrade decisions:
|
|
214
|
+
*
|
|
215
|
+
* - `unavailable` the service could not answer (offline, 5xx, saturated)
|
|
216
|
+
* - `timeout` the run or the request hit its deadline
|
|
217
|
+
* - `rate_limited` HTTP 429, or a retryable 409 (`thread_busy`) - back off
|
|
218
|
+
* and retry; the SDK already does, with jitter
|
|
219
|
+
* - `invalid_request` the caller must fix the request (4xx)
|
|
220
|
+
* - `schema` structured output failed or never validated
|
|
221
|
+
* - `incomplete` the run finished without a complete answer
|
|
222
|
+
* - `internal` anything else, including a cancelled run and the
|
|
223
|
+
* `systemone_unconfigured` config boundary (never
|
|
224
|
+
* retried, never treated as an outage)
|
|
225
|
+
*/
|
|
226
|
+
export type AgentLuErrorKind = "unavailable" | "timeout" | "rate_limited" | "invalid_request" | "schema" | "incomplete" | "internal";
|
|
198
227
|
export declare class AgentLuError extends Error {
|
|
199
228
|
readonly status: number;
|
|
200
229
|
readonly code: string;
|
|
@@ -204,7 +233,18 @@ export declare class AgentLuError extends Error {
|
|
|
204
233
|
constructor(status: number, code: string, message: string, retryable: boolean,
|
|
205
234
|
/** Parsed Retry-After header, in milliseconds, when present. */
|
|
206
235
|
retryAfterMs?: number | undefined);
|
|
236
|
+
/** Coarse classification; see `AgentLuErrorKind` and `isUnavailable`. */
|
|
237
|
+
get kind(): AgentLuErrorKind;
|
|
207
238
|
}
|
|
239
|
+
/** Classify an error into an `AgentLuErrorKind` (never throws). */
|
|
240
|
+
export declare function classifyAgentLuError(err: AgentLuError): AgentLuErrorKind;
|
|
241
|
+
/**
|
|
242
|
+
* Fail-open predicate: true when the service could not answer right now -
|
|
243
|
+
* offline/network, an upstream 5xx, saturation, or a deadline. An app that
|
|
244
|
+
* degrades gracefully (skip an enrichment, fall back to a local heuristic)
|
|
245
|
+
* treats this as "no answer available", not as a bug in the request.
|
|
246
|
+
*/
|
|
247
|
+
export declare function isUnavailable(err: unknown): boolean;
|
|
208
248
|
export declare class AgentLuResponseError extends AgentLuError {
|
|
209
249
|
readonly response: ResponseObject;
|
|
210
250
|
readonly responseStatus: string;
|
|
@@ -221,36 +261,51 @@ export interface AgentLuOptions {
|
|
|
221
261
|
*/
|
|
222
262
|
timeoutMs?: number;
|
|
223
263
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Registration defaults (`defaults` on `POST /v1/clients`), derived from the
|
|
266
|
+
* server's generated wire contract so the two cannot drift: the server honors
|
|
267
|
+
* `maxOutputTokens` and `attempts` (the latter bounds model-chain fallbacks and
|
|
268
|
+
* the background queue's retry budget). A `maxOutputTokens` stored before the
|
|
269
|
+
* tier contract stays inert until the app re-declares its defaults or the
|
|
270
|
+
* console edits them - see the service API docs.
|
|
271
|
+
*/
|
|
272
|
+
export type ClientDefaults = WireClientDefaults;
|
|
273
|
+
/**
|
|
274
|
+
* High-level use-case declaration for model routing and priority tier.
|
|
275
|
+
* Generated from the server's zod contract, so it carries `priority`
|
|
276
|
+
* (`instant | standard | deep`) - the knob that opts a client into the tier
|
|
277
|
+
* defaults (timeout, output cap, attempts, reasoning).
|
|
278
|
+
*/
|
|
279
|
+
export type UseCaseProfile = WireUseCaseProfile;
|
|
280
|
+
/** Latency/thoroughness tier declared by a client. */
|
|
281
|
+
export type UseCasePriority = "instant" | "standard" | "deep";
|
|
282
|
+
/**
|
|
283
|
+
* Registration options shared by `register`, `useCase` and `useCaseClient`.
|
|
284
|
+
* Registration is get-or-create, so an app can declare its client(s) on boot.
|
|
285
|
+
*/
|
|
286
|
+
export interface ClientRegistrationOptions {
|
|
287
|
+
person?: string | null;
|
|
288
|
+
instructions?: string | null;
|
|
289
|
+
defaults?: ClientDefaults;
|
|
290
|
+
use_case_profile?: UseCaseProfile;
|
|
291
|
+
/** Use-case tier; sugar for `use_case_profile.priority`. */
|
|
292
|
+
priority?: UseCasePriority;
|
|
235
293
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
294
|
+
* App this client belongs to (console `/stats` grouping and the namespace of
|
|
295
|
+
* app-owned agents). Defaults server-side to the part of `name` before the
|
|
296
|
+
* first "-" (`mealu-extract` -> `mealu`).
|
|
238
297
|
*/
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
priorityTier?: "never" | "retry" | "first" | "always";
|
|
298
|
+
app?: string;
|
|
299
|
+
/**
|
|
300
|
+
* App-owned agents this client declares, callable as `agent: "<app>/<name>"`
|
|
301
|
+
* by every client of the same app. Sending the array replaces the agents this
|
|
302
|
+
* client previously declared (`[]` clears them).
|
|
303
|
+
*/
|
|
304
|
+
agents?: WireAppAgentDeclaration[];
|
|
247
305
|
}
|
|
248
|
-
export interface UseCaseDeclaration {
|
|
306
|
+
export interface UseCaseDeclaration extends ClientRegistrationOptions {
|
|
249
307
|
/** Registered client name. Defaults to the catalog key. */
|
|
250
308
|
name?: string;
|
|
251
|
-
person?: string | null;
|
|
252
|
-
defaults?: ClientDefaults;
|
|
253
|
-
use_case_profile?: UseCaseProfile;
|
|
254
309
|
}
|
|
255
310
|
export type UseCaseCatalog<T extends Record<string, UseCaseDeclaration>> = {
|
|
256
311
|
[K in keyof T]: UseCaseRunner;
|
|
@@ -343,6 +398,53 @@ export interface UploadOptions {
|
|
|
343
398
|
signal?: AbortSignal;
|
|
344
399
|
maxAttempts?: number;
|
|
345
400
|
}
|
|
401
|
+
export interface UploadManyOptions extends UploadOptions {
|
|
402
|
+
/** Parallel uploads. Default 3. */
|
|
403
|
+
concurrency?: number;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Direct-tier request (`llm.text` / `llm.json` / `llm.stream`): one
|
|
407
|
+
* profile-free provider call, no tools, no session. `instructions` is the
|
|
408
|
+
* whole system prompt.
|
|
409
|
+
*/
|
|
410
|
+
export interface LlmRequest {
|
|
411
|
+
input: string;
|
|
412
|
+
instructions?: string;
|
|
413
|
+
model?: string;
|
|
414
|
+
temperature?: number;
|
|
415
|
+
timeoutMs?: number;
|
|
416
|
+
maxOutputTokens?: number;
|
|
417
|
+
metadata?: Record<string, string>;
|
|
418
|
+
signal?: AbortSignal;
|
|
419
|
+
maxAttempts?: number;
|
|
420
|
+
idempotencyKey?: string;
|
|
421
|
+
}
|
|
422
|
+
export interface LlmJsonRequest extends LlmRequest {
|
|
423
|
+
/** Format name sent to the provider (default "Result"). */
|
|
424
|
+
schemaName?: string;
|
|
425
|
+
}
|
|
426
|
+
export interface LlmApi {
|
|
427
|
+
text: (request: string | LlmRequest) => Promise<{
|
|
428
|
+
text: string;
|
|
429
|
+
response: ResponseObject;
|
|
430
|
+
}>;
|
|
431
|
+
json: <T = unknown>(schema: Record<string, unknown>, request: string | LlmJsonRequest) => Promise<{
|
|
432
|
+
data: T;
|
|
433
|
+
response: ResponseObject;
|
|
434
|
+
}>;
|
|
435
|
+
stream: (request: string | LlmRequest) => AsyncIterable<StreamEvent> & {
|
|
436
|
+
finalText: () => Promise<string>;
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/** Agent-tier entry points (`alu.agent.*`). */
|
|
440
|
+
export interface AgentTierApi {
|
|
441
|
+
run: (opts: RunOptions) => Promise<{
|
|
442
|
+
data?: unknown;
|
|
443
|
+
response: ResponseObject;
|
|
444
|
+
}>;
|
|
445
|
+
stream: (opts: RunOptions) => AsyncGenerator<Record<string, unknown>>;
|
|
446
|
+
background: (opts: RunOptions, wait?: WaitOptions) => Promise<ResponseObject>;
|
|
447
|
+
}
|
|
346
448
|
export interface ResponseObject {
|
|
347
449
|
id: string;
|
|
348
450
|
status: string;
|
|
@@ -406,12 +508,7 @@ export declare class AgentLu {
|
|
|
406
508
|
constructor(opts?: AgentLuOptions);
|
|
407
509
|
private postClient;
|
|
408
510
|
/** Get-or-create a client by name. Caches the returned key on this instance. */
|
|
409
|
-
register(name: string, opts?: {
|
|
410
|
-
person?: string | null;
|
|
411
|
-
instructions?: string | null;
|
|
412
|
-
defaults?: ClientDefaults;
|
|
413
|
-
use_case_profile?: UseCaseProfile;
|
|
414
|
-
}): Promise<ResponseObject & {
|
|
511
|
+
register(name: string, opts?: ClientRegistrationOptions): Promise<ResponseObject & {
|
|
415
512
|
api_key?: string;
|
|
416
513
|
name: string;
|
|
417
514
|
}>;
|
|
@@ -422,11 +519,7 @@ export declare class AgentLu {
|
|
|
422
519
|
* server-side on the client, so `run()` only needs the input. Registering a
|
|
423
520
|
* use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
|
|
424
521
|
*/
|
|
425
|
-
useCase(name: string, opts?:
|
|
426
|
-
person?: string | null;
|
|
427
|
-
defaults?: ClientDefaults;
|
|
428
|
-
use_case_profile?: UseCaseProfile;
|
|
429
|
-
}): UseCaseRunner;
|
|
522
|
+
useCase(name: string, opts?: ClientRegistrationOptions): UseCaseRunner;
|
|
430
523
|
/**
|
|
431
524
|
* Several named use-case runners that share this client. Call `ensure()` on
|
|
432
525
|
* boot, then `catalog.extract.runJson({ input })`.
|
|
@@ -441,11 +534,7 @@ export declare class AgentLu {
|
|
|
441
534
|
* on this instance (same semantics as `register`), so a following
|
|
442
535
|
* `responses.create` is authorized.
|
|
443
536
|
*/
|
|
444
|
-
useCaseClient(name: string, opts?:
|
|
445
|
-
person?: string | null;
|
|
446
|
-
defaults?: ClientDefaults;
|
|
447
|
-
use_case_profile?: UseCaseProfile;
|
|
448
|
-
}): Promise<string>;
|
|
537
|
+
useCaseClient(name: string, opts?: ClientRegistrationOptions): Promise<string>;
|
|
449
538
|
private registerForUseCase;
|
|
450
539
|
/**
|
|
451
540
|
* Upload an image, video, or document for use with `run({ fileIds })`.
|
|
@@ -489,7 +578,42 @@ export declare class AgentLu {
|
|
|
489
578
|
typedStream(opts: RunOptions): AsyncIterable<StreamEvent> & {
|
|
490
579
|
finalText: () => Promise<string>;
|
|
491
580
|
};
|
|
581
|
+
/**
|
|
582
|
+
* Direct tier (`agent: "direct"`): one profile-free provider call - no
|
|
583
|
+
* profile, no tools, no session. Use it for extraction, classification and
|
|
584
|
+
* short answers; use `agent.*` when the run needs tools or a thread.
|
|
585
|
+
*
|
|
586
|
+
* const { text } = await alu.llm.text("Name three colours.");
|
|
587
|
+
* const { data } = await alu.llm.json(schema, { input, schemaName: "Items" });
|
|
588
|
+
*/
|
|
589
|
+
readonly llm: LlmApi;
|
|
590
|
+
/**
|
|
591
|
+
* Agent tier: a profile, tool families, threads and background runs. The
|
|
592
|
+
* default agent is `api` (no persona - `instructions` are the system prompt);
|
|
593
|
+
* pin `fast`/`research`/... or an app-owned `<app>/<name>` id when needed.
|
|
594
|
+
*/
|
|
595
|
+
readonly agent: AgentTierApi;
|
|
596
|
+
/** Build a direct-tier run body from a short-form `llm.*` request. */
|
|
597
|
+
private llmOptions;
|
|
598
|
+
/**
|
|
599
|
+
* Enqueue a background run (`background: true`) and poll it to a terminal
|
|
600
|
+
* status with `responses.wait`. Resolves with the terminal Response row -
|
|
601
|
+
* check `status` (and `output_parsed`); a failed run is not an exception
|
|
602
|
+
* here, but `wait` still times out after `wait.timeoutMs` (default 10min).
|
|
603
|
+
*/
|
|
604
|
+
runBackground(opts: RunOptions, wait?: WaitOptions): Promise<ResponseObject>;
|
|
605
|
+
/**
|
|
606
|
+
* Upload several files with bounded concurrency (default 3), preserving the
|
|
607
|
+
* input order. Returns the uploaded ids for `run({ fileIds })` - pair with
|
|
608
|
+
* `fileKinds` when the files are not images.
|
|
609
|
+
*/
|
|
610
|
+
uploadMany(files: RunFile[], opts?: UploadManyOptions): Promise<Array<{
|
|
611
|
+
id: string;
|
|
612
|
+
kind?: string;
|
|
613
|
+
}>>;
|
|
492
614
|
private runForApiKey;
|
|
615
|
+
/** Attempts for a run: the caller's `maxAttempts` or the shared default. */
|
|
616
|
+
private runAttempts;
|
|
493
617
|
private runJsonForApiKey;
|
|
494
618
|
private runJsonOnce;
|
|
495
619
|
private runTextForApiKey;
|
|
@@ -569,11 +693,7 @@ export declare class UseCaseRunner {
|
|
|
569
693
|
private registerFailedAt;
|
|
570
694
|
/** Cooldown is short and only suppresses *repeated* failures in one burst. */
|
|
571
695
|
private static readonly REGISTER_COOLDOWN_MS;
|
|
572
|
-
constructor(parent: AgentLu, name: string, opts?:
|
|
573
|
-
person?: string | null;
|
|
574
|
-
defaults?: ClientDefaults;
|
|
575
|
-
use_case_profile?: UseCaseProfile;
|
|
576
|
-
});
|
|
696
|
+
constructor(parent: AgentLu, name: string, opts?: ClientRegistrationOptions);
|
|
577
697
|
/**
|
|
578
698
|
* Idempotent get-or-create; safe to call on boot or before every run.
|
|
579
699
|
* A failed registration is remembered for a short cooldown so an app that
|
|
@@ -618,6 +738,20 @@ export declare class UseCaseRunner {
|
|
|
618
738
|
id: string;
|
|
619
739
|
kind?: string;
|
|
620
740
|
}>;
|
|
741
|
+
/**
|
|
742
|
+
* Background run with this use-case's key: enqueue, then poll to a terminal
|
|
743
|
+
* status (`responses.wait`). Resolves with the terminal Response row.
|
|
744
|
+
*/
|
|
745
|
+
runBackground(opts: RunOptions, wait?: WaitOptions): Promise<ResponseObject>;
|
|
746
|
+
/**
|
|
747
|
+
* Upload several files with bounded concurrency (default 3), preserving the
|
|
748
|
+
* input order. Files are owned by this use-case's client, so the same
|
|
749
|
+
* `run({ fileIds })` can reference them.
|
|
750
|
+
*/
|
|
751
|
+
uploadMany(files: RunFile[], opts?: UploadManyOptions): Promise<Array<{
|
|
752
|
+
id: string;
|
|
753
|
+
kind?: string;
|
|
754
|
+
}>>;
|
|
621
755
|
readonly responses: {
|
|
622
756
|
retrieve: (id: string) => Promise<ResponseObject>;
|
|
623
757
|
cancel: (id: string) => Promise<ResponseObject>;
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @betterlu/agentlu - client for the agentlu service API.
|
|
3
|
-
*
|
|
4
|
-
* You can also skip this package entirely:
|
|
5
|
-
*
|
|
6
|
-
* new OpenAI({ baseURL: "http://localhost:8912/v1", apiKey })
|
|
7
|
-
*
|
|
8
|
-
* This client exists for the ergonomic bits OpenAI's SDK does not know about:
|
|
9
|
-
* get-or-create registration, typed `runJson` / `runText`, use-case catalogs,
|
|
10
|
-
* background `wait`, and auto-retry on `409 thread_busy`.
|
|
11
|
-
*/
|
|
12
1
|
export const DEFAULT_BASE_URL = "http://localhost:8912/v1";
|
|
2
|
+
/** Wire agent id for the direct tier (not a chat mode). */
|
|
3
|
+
const DIRECT_AGENT_ID = "direct";
|
|
4
|
+
/** Default agent for the agent tier: the profile-free machine agent. */
|
|
5
|
+
const AGENT_TIER_AGENT = "api";
|
|
13
6
|
/**
|
|
14
7
|
* Accept an origin (`http://host:8912`) or a `/v1` base and normalize to the
|
|
15
8
|
* `/v1` base the API actually serves, so `new AgentLu({ baseUrl: "http://host:8912" })`
|
|
@@ -66,6 +59,88 @@ export class AgentLuError extends Error {
|
|
|
66
59
|
this.retryAfterMs = retryAfterMs;
|
|
67
60
|
this.name = "AgentLuError";
|
|
68
61
|
}
|
|
62
|
+
/** Coarse classification; see `AgentLuErrorKind` and `isUnavailable`. */
|
|
63
|
+
get kind() {
|
|
64
|
+
return classifyAgentLuError(this);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const SCHEMA_ERROR_CODES = new Set(["schema_validation_failed", "missing_output_parsed"]);
|
|
68
|
+
const INCOMPLETE_ERROR_CODES = new Set(["incomplete", "empty_output"]);
|
|
69
|
+
const UNAVAILABLE_ERROR_CODES = new Set([
|
|
70
|
+
"network_error",
|
|
71
|
+
"upstream_error",
|
|
72
|
+
"empty_completion",
|
|
73
|
+
"too_many_active_runs",
|
|
74
|
+
"systemone_unavailable",
|
|
75
|
+
"harness_unavailable",
|
|
76
|
+
"quota_exhausted",
|
|
77
|
+
"provider_unavailable",
|
|
78
|
+
]);
|
|
79
|
+
const INVALID_REQUEST_ERROR_CODES = new Set([
|
|
80
|
+
"invalid_request",
|
|
81
|
+
"unsupported_tool",
|
|
82
|
+
"unsupported_backend_field",
|
|
83
|
+
"unknown_agent",
|
|
84
|
+
"unknown_model",
|
|
85
|
+
"missing_api_key",
|
|
86
|
+
"invalid_api_key",
|
|
87
|
+
"model_no_vision",
|
|
88
|
+
"model_no_video",
|
|
89
|
+
"reserved_username",
|
|
90
|
+
"person_threads_not_allowed",
|
|
91
|
+
"app_agent_not_allowed",
|
|
92
|
+
"idempotency_conflict",
|
|
93
|
+
"not_found",
|
|
94
|
+
"background_not_supported",
|
|
95
|
+
]);
|
|
96
|
+
const INVALID_REQUEST_STATUSES = new Set([400, 401, 403, 404, 405, 409, 413, 415, 422]);
|
|
97
|
+
/** Classify an error into an `AgentLuErrorKind` (never throws). */
|
|
98
|
+
export function classifyAgentLuError(err) {
|
|
99
|
+
if (SCHEMA_ERROR_CODES.has(err.code))
|
|
100
|
+
return "schema";
|
|
101
|
+
if (INCOMPLETE_ERROR_CODES.has(err.code))
|
|
102
|
+
return "incomplete";
|
|
103
|
+
if (err instanceof AgentLuResponseError && err.responseStatus === "incomplete")
|
|
104
|
+
return "incomplete";
|
|
105
|
+
// A cancelled run is an outcome, not a request the caller must fix and not a
|
|
106
|
+
// service outage: HTTP 499 and a 200 body with status "cancelled" classify
|
|
107
|
+
// the same way.
|
|
108
|
+
if (err.code === "cancelled" || err.status === 499)
|
|
109
|
+
return "internal";
|
|
110
|
+
if (err instanceof AgentLuResponseError && err.responseStatus === "cancelled")
|
|
111
|
+
return "internal";
|
|
112
|
+
if (err.code === "timeout")
|
|
113
|
+
return "timeout";
|
|
114
|
+
if (err.status === 429 || err.code === "rate_limited")
|
|
115
|
+
return "rate_limited";
|
|
116
|
+
// A retryable 409 (`thread_busy`: another turn is in flight) is saturation:
|
|
117
|
+
// back off and retry, never "the caller must fix the request".
|
|
118
|
+
if (err.code === "thread_busy" || (err.status === 409 && err.retryable))
|
|
119
|
+
return "rate_limited";
|
|
120
|
+
// A config boundary wearing a 503 (`systemone_unconfigured`) must surface:
|
|
121
|
+
// it is not transient and is never retried.
|
|
122
|
+
if (err.code === "systemone_unconfigured")
|
|
123
|
+
return "internal";
|
|
124
|
+
if (err.status === 0 || err.status === 502 || err.status === 503 || err.status === 504)
|
|
125
|
+
return "unavailable";
|
|
126
|
+
if (UNAVAILABLE_ERROR_CODES.has(err.code))
|
|
127
|
+
return "unavailable";
|
|
128
|
+
if (INVALID_REQUEST_STATUSES.has(err.status) || INVALID_REQUEST_ERROR_CODES.has(err.code)) {
|
|
129
|
+
return "invalid_request";
|
|
130
|
+
}
|
|
131
|
+
return "internal";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Fail-open predicate: true when the service could not answer right now -
|
|
135
|
+
* offline/network, an upstream 5xx, saturation, or a deadline. An app that
|
|
136
|
+
* degrades gracefully (skip an enrichment, fall back to a local heuristic)
|
|
137
|
+
* treats this as "no answer available", not as a bug in the request.
|
|
138
|
+
*/
|
|
139
|
+
export function isUnavailable(err) {
|
|
140
|
+
if (!(err instanceof AgentLuError))
|
|
141
|
+
return false;
|
|
142
|
+
const kind = err.kind;
|
|
143
|
+
return kind === "unavailable" || kind === "timeout" || kind === "rate_limited";
|
|
69
144
|
}
|
|
70
145
|
export class AgentLuResponseError extends AgentLuError {
|
|
71
146
|
response;
|
|
@@ -82,6 +157,39 @@ export class AgentLuResponseError extends AgentLuError {
|
|
|
82
157
|
}
|
|
83
158
|
/** Default overall deadline for `responses.wait`. */
|
|
84
159
|
export const DEFAULT_WAIT_TIMEOUT_MS = 10 * 60_000;
|
|
160
|
+
/** The agent tier's default agent: the profile-free machine agent. */
|
|
161
|
+
function withAgentTierDefault(opts) {
|
|
162
|
+
return opts.agent === undefined ? { ...opts, agent: AGENT_TIER_AGENT } : opts;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Worker count for `uploadMany`: the caller's concurrency when it is a real
|
|
166
|
+
* number >= 1. A `NaN` (plausible as `Number(process.env.X)`) or a value below
|
|
167
|
+
* 1 would otherwise start no workers and silently upload nothing.
|
|
168
|
+
*/
|
|
169
|
+
function uploadConcurrency(value) {
|
|
170
|
+
const n = Number(value ?? 3);
|
|
171
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 3;
|
|
172
|
+
}
|
|
173
|
+
/** Bounded-concurrency uploads that preserve the input order. */
|
|
174
|
+
async function uploadManyWith(files, opts, upload) {
|
|
175
|
+
const concurrency = uploadConcurrency(opts?.concurrency);
|
|
176
|
+
const results = new Array(files.length);
|
|
177
|
+
let next = 0;
|
|
178
|
+
const workers = Array.from({ length: Math.min(concurrency, files.length) }, async () => {
|
|
179
|
+
for (;;) {
|
|
180
|
+
const index = next++;
|
|
181
|
+
if (index >= files.length)
|
|
182
|
+
return;
|
|
183
|
+
const file = files[index];
|
|
184
|
+
results[index] = await upload(file);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
const settled = await Promise.allSettled(workers);
|
|
188
|
+
const failed = settled.find((result) => result.status === "rejected");
|
|
189
|
+
if (failed && failed.status === "rejected")
|
|
190
|
+
throw failed.reason;
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
85
193
|
export function isTerminalStatus(status) {
|
|
86
194
|
return status === "completed" || status === "failed" || status === "incomplete" || status === "cancelled";
|
|
87
195
|
}
|
|
@@ -158,10 +266,26 @@ export class AgentLu {
|
|
|
158
266
|
});
|
|
159
267
|
}
|
|
160
268
|
postClient(name, opts) {
|
|
269
|
+
// `priority` is sugar for the profile field, and `agents` rides the same
|
|
270
|
+
// registration, so the declared body is built explicitly (no stray keys).
|
|
271
|
+
const profile = opts?.priority !== undefined ? { ...opts.use_case_profile, priority: opts.priority } : opts?.use_case_profile;
|
|
272
|
+
const body = { name };
|
|
273
|
+
if (opts?.person !== undefined)
|
|
274
|
+
body.person = opts.person;
|
|
275
|
+
if (opts?.instructions !== undefined)
|
|
276
|
+
body.instructions = opts.instructions;
|
|
277
|
+
if (opts?.defaults !== undefined)
|
|
278
|
+
body.defaults = opts.defaults;
|
|
279
|
+
if (opts?.app !== undefined)
|
|
280
|
+
body.app = opts.app;
|
|
281
|
+
if (opts?.agents !== undefined)
|
|
282
|
+
body.agents = opts.agents;
|
|
283
|
+
if (profile !== undefined)
|
|
284
|
+
body.use_case_profile = profile;
|
|
161
285
|
// Registration is get-or-create and callers gate it behind `ensure()`'s
|
|
162
286
|
// cooldown; two attempts smooth a blip without stacking long backoffs
|
|
163
287
|
// before the cooldown can do its job.
|
|
164
|
-
return this.request("POST", "/clients",
|
|
288
|
+
return this.request("POST", "/clients", body, { auth: false, maxAttempts: 2 });
|
|
165
289
|
}
|
|
166
290
|
/** Get-or-create a client by name. Caches the returned key on this instance. */
|
|
167
291
|
async register(name, opts) {
|
|
@@ -288,15 +412,87 @@ export class AgentLu {
|
|
|
288
412
|
typedStream(opts) {
|
|
289
413
|
return typedStream(this.stream(opts));
|
|
290
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Direct tier (`agent: "direct"`): one profile-free provider call - no
|
|
417
|
+
* profile, no tools, no session. Use it for extraction, classification and
|
|
418
|
+
* short answers; use `agent.*` when the run needs tools or a thread.
|
|
419
|
+
*
|
|
420
|
+
* const { text } = await alu.llm.text("Name three colours.");
|
|
421
|
+
* const { data } = await alu.llm.json(schema, { input, schemaName: "Items" });
|
|
422
|
+
*/
|
|
423
|
+
llm = {
|
|
424
|
+
text: (request) => this.runText(this.llmOptions(request)),
|
|
425
|
+
json: (schema, request) => {
|
|
426
|
+
const schemaName = typeof request === "string" ? undefined : request.schemaName;
|
|
427
|
+
return this.runJson({ ...this.llmOptions(request), schema, schemaName: schemaName ?? "Result" });
|
|
428
|
+
},
|
|
429
|
+
stream: (request) => this.typedStream(this.llmOptions(request)),
|
|
430
|
+
};
|
|
431
|
+
/**
|
|
432
|
+
* Agent tier: a profile, tool families, threads and background runs. The
|
|
433
|
+
* default agent is `api` (no persona - `instructions` are the system prompt);
|
|
434
|
+
* pin `fast`/`research`/... or an app-owned `<app>/<name>` id when needed.
|
|
435
|
+
*/
|
|
436
|
+
agent = {
|
|
437
|
+
run: (opts) => this.run(withAgentTierDefault(opts)),
|
|
438
|
+
stream: (opts) => this.stream(withAgentTierDefault(opts)),
|
|
439
|
+
background: (opts, wait) => this.runBackground(withAgentTierDefault(opts), wait),
|
|
440
|
+
};
|
|
441
|
+
/** Build a direct-tier run body from a short-form `llm.*` request. */
|
|
442
|
+
llmOptions(request) {
|
|
443
|
+
const opts = typeof request === "string" ? { input: request } : request;
|
|
444
|
+
return { ...opts, agent: DIRECT_AGENT_ID, store: false, tools: [] };
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Enqueue a background run (`background: true`) and poll it to a terminal
|
|
448
|
+
* status with `responses.wait`. Resolves with the terminal Response row -
|
|
449
|
+
* check `status` (and `output_parsed`); a failed run is not an exception
|
|
450
|
+
* here, but `wait` still times out after `wait.timeoutMs` (default 10min).
|
|
451
|
+
*/
|
|
452
|
+
async runBackground(opts, wait) {
|
|
453
|
+
const queued = await this.run({ ...opts, background: true });
|
|
454
|
+
return this.responses.wait(queued.response.id, wait);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Upload several files with bounded concurrency (default 3), preserving the
|
|
458
|
+
* input order. Returns the uploaded ids for `run({ fileIds })` - pair with
|
|
459
|
+
* `fileKinds` when the files are not images.
|
|
460
|
+
*/
|
|
461
|
+
async uploadMany(files, opts) {
|
|
462
|
+
return uploadManyWith(files, opts, (file) => this.upload(file.data, file.filename, file.mime, opts));
|
|
463
|
+
}
|
|
291
464
|
async runForApiKey(apiKey, opts) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
465
|
+
// A failed run is an HTTP 200 with `error.retryable` in the body, which the
|
|
466
|
+
// transport retry never sees. Retry those here (idempotency keys make it
|
|
467
|
+
// safe), honoring `maxAttempts` like the transport does. The key is minted
|
|
468
|
+
// once per logical run so every attempt replays the same server-side run.
|
|
469
|
+
const maxAttempts = this.runAttempts(opts);
|
|
470
|
+
const idempotencyKey = opts.idempotencyKey ?? randomIdempotencyKey();
|
|
471
|
+
let last;
|
|
472
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
473
|
+
const response = await this.createRunResponse({ ...opts, idempotencyKey }, apiKey);
|
|
474
|
+
if (response.status === "failed" && response.error) {
|
|
475
|
+
const err = new AgentLuError(200, response.error.code, response.error.message, response.error.retryable);
|
|
476
|
+
// A retryable failed run is a transient provider fault; retry it.
|
|
477
|
+
if (response.error.retryable && attempt < maxAttempts - 1) {
|
|
478
|
+
last = err;
|
|
479
|
+
await sleep(retryDelayMs(err, attempt));
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
throw err;
|
|
483
|
+
}
|
|
484
|
+
if (opts.throwOnIncomplete && response.status === "incomplete") {
|
|
485
|
+
throw new AgentLuResponseError(response);
|
|
486
|
+
}
|
|
487
|
+
return { data: response.output_parsed, response };
|
|
298
488
|
}
|
|
299
|
-
|
|
489
|
+
throw last ?? new AgentLuError(200, "upstream_error", "run failed after retries", false);
|
|
490
|
+
}
|
|
491
|
+
/** Attempts for a run: the caller's `maxAttempts` or the shared default. */
|
|
492
|
+
runAttempts(opts) {
|
|
493
|
+
return opts.maxAttempts !== undefined && Number.isFinite(opts.maxAttempts) && opts.maxAttempts >= 1
|
|
494
|
+
? Math.floor(opts.maxAttempts)
|
|
495
|
+
: 5;
|
|
300
496
|
}
|
|
301
497
|
async runJsonForApiKey(apiKey, opts) {
|
|
302
498
|
const attempts = jsonAttempts(opts);
|
|
@@ -787,6 +983,22 @@ export class UseCaseRunner {
|
|
|
787
983
|
async upload(data, filename, mime, opts) {
|
|
788
984
|
return this.withAuthRetry((apiKey) => this.operations.upload(apiKey, data, filename, mime, opts));
|
|
789
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Background run with this use-case's key: enqueue, then poll to a terminal
|
|
988
|
+
* status (`responses.wait`). Resolves with the terminal Response row.
|
|
989
|
+
*/
|
|
990
|
+
async runBackground(opts, wait) {
|
|
991
|
+
const queued = await this.run({ ...opts, background: true });
|
|
992
|
+
return this.responses.wait(queued.response.id, wait);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Upload several files with bounded concurrency (default 3), preserving the
|
|
996
|
+
* input order. Files are owned by this use-case's client, so the same
|
|
997
|
+
* `run({ fileIds })` can reference them.
|
|
998
|
+
*/
|
|
999
|
+
async uploadMany(files, opts) {
|
|
1000
|
+
return uploadManyWith(files, opts, (file) => this.upload(file.data, file.filename, file.mime, opts));
|
|
1001
|
+
}
|
|
790
1002
|
responses = {
|
|
791
1003
|
retrieve: (id) => this.withAuthRetry((apiKey) => this.operations.retrieve(apiKey, id)),
|
|
792
1004
|
cancel: (id) => this.withAuthRetry((apiKey) => this.operations.cancel(apiKey, id)),
|
|
@@ -910,6 +1122,10 @@ export function isRetryableError(err) {
|
|
|
910
1122
|
return false;
|
|
911
1123
|
if (err.code === "systemone_unconfigured")
|
|
912
1124
|
return false;
|
|
1125
|
+
// Deterministic exhaustion: the provider keeps refusing until quota is
|
|
1126
|
+
// topped up, however 502-ish the status looks.
|
|
1127
|
+
if (err.code === "quota_exhausted")
|
|
1128
|
+
return false;
|
|
913
1129
|
if (err.code === "thread_busy" || err.code === "too_many_active_runs")
|
|
914
1130
|
return true;
|
|
915
1131
|
// Network faults are retried; a client timeout is not (the deadline was hit
|
package/package.json
CHANGED