@betterlu/agentlu 1.7.1 → 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.
@@ -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
- export interface ClientDefaults {
225
- backend?: Backend;
226
- agent?: string;
227
- model?: string;
228
- instructions?: string;
229
- temperature?: number;
230
- tools?: ApiToolInput[];
231
- timeoutMs?: number;
232
- store?: boolean;
233
- reasoning?: ClientReasoningOptions;
234
- text?: TextConfig;
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
- * Retired on client defaults (server ignores it). Pass `max_output_tokens`
237
- * or `maxOutputTokens` on `run` / `runJson` / `stream` instead.
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
- maxOutputTokens?: never;
240
- }
241
- /** High-level use-case declaration for model routing and priority tier. */
242
- export interface UseCaseProfile {
243
- inputModalities?: Array<"text" | "image" | "video">;
244
- outputKind?: "text" | "json_schema";
245
- /** MiniMax priority admission consent (priority costs 1.5x standard). */
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,6 +578,39 @@ 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;
493
615
  /** Attempts for a run: the caller's `maxAttempts` or the shared default. */
494
616
  private runAttempts;
@@ -571,11 +693,7 @@ export declare class UseCaseRunner {
571
693
  private registerFailedAt;
572
694
  /** Cooldown is short and only suppresses *repeated* failures in one burst. */
573
695
  private static readonly REGISTER_COOLDOWN_MS;
574
- constructor(parent: AgentLu, name: string, opts?: {
575
- person?: string | null;
576
- defaults?: ClientDefaults;
577
- use_case_profile?: UseCaseProfile;
578
- });
696
+ constructor(parent: AgentLu, name: string, opts?: ClientRegistrationOptions);
579
697
  /**
580
698
  * Idempotent get-or-create; safe to call on boot or before every run.
581
699
  * A failed registration is remembered for a short cooldown so an app that
@@ -620,6 +738,20 @@ export declare class UseCaseRunner {
620
738
  id: string;
621
739
  kind?: string;
622
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
+ }>>;
623
755
  readonly responses: {
624
756
  retrieve: (id: string) => Promise<ResponseObject>;
625
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", { name, ...opts }, { auth: false, maxAttempts: 2 });
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,6 +412,55 @@ 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
465
  // A failed run is an HTTP 200 with `error.retryable` in the body, which the
293
466
  // transport retry never sees. Retry those here (idempotency keys make it
@@ -810,6 +983,22 @@ export class UseCaseRunner {
810
983
  async upload(data, filename, mime, opts) {
811
984
  return this.withAuthRetry((apiKey) => this.operations.upload(apiKey, data, filename, mime, opts));
812
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
+ }
813
1002
  responses = {
814
1003
  retrieve: (id) => this.withAuthRetry((apiKey) => this.operations.retrieve(apiKey, id)),
815
1004
  cancel: (id) => this.withAuthRetry((apiKey) => this.operations.cancel(apiKey, id)),
@@ -933,6 +1122,10 @@ export function isRetryableError(err) {
933
1122
  return false;
934
1123
  if (err.code === "systemone_unconfigured")
935
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;
936
1129
  if (err.code === "thread_busy" || err.code === "too_many_active_runs")
937
1130
  return true;
938
1131
  // Network faults are retried; a client timeout is not (the deadline was hit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterlu/agentlu",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "Client for the agentlu service API (/v1). Or just use the openai SDK pointed at http://localhost:8912/v1.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",