@betterlu/agentlu 1.4.0 → 1.6.0

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