@betterlu/agentlu 1.0.0 → 1.1.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/dist/index.d.ts CHANGED
@@ -14,13 +14,29 @@ export declare class AgentLuError extends Error {
14
14
  readonly status: number;
15
15
  readonly code: string;
16
16
  readonly retryable: boolean;
17
- constructor(status: number, code: string, message: string, retryable: boolean);
17
+ /** Parsed Retry-After header, in milliseconds, when present. */
18
+ readonly retryAfterMs?: number | undefined;
19
+ constructor(status: number, code: string, message: string, retryable: boolean,
20
+ /** Parsed Retry-After header, in milliseconds, when present. */
21
+ retryAfterMs?: number | undefined);
18
22
  }
19
23
  export interface AgentLuOptions {
20
24
  baseUrl?: string;
21
25
  apiKey?: string;
22
26
  fetch?: typeof fetch;
23
27
  }
28
+ export interface ClientDefaults {
29
+ agent?: string;
30
+ model?: string;
31
+ instructions?: string;
32
+ temperature?: number;
33
+ maxOutputTokens?: number;
34
+ tools?: Array<string | {
35
+ type: string;
36
+ allowed_tools?: string[];
37
+ }>;
38
+ timeoutMs?: number;
39
+ }
24
40
  export interface RunOptions {
25
41
  agent?: string;
26
42
  model?: string;
@@ -33,11 +49,17 @@ export interface RunOptions {
33
49
  conversation?: string;
34
50
  store?: boolean;
35
51
  person?: string | null;
52
+ owner?: "client" | "person";
53
+ title?: string;
36
54
  timeout_ms?: number;
37
55
  include?: string[];
56
+ temperature?: number;
57
+ max_output_tokens?: number;
38
58
  /** JSON Schema. When set, `data` on the result is the validated object. */
39
59
  schema?: Record<string, unknown>;
40
60
  schemaName?: string;
61
+ /** Queue a background run instead of waiting. Returns the queued response. */
62
+ background?: boolean;
41
63
  }
42
64
  export interface ResponseObject {
43
65
  id: string;
@@ -66,10 +88,27 @@ export declare class AgentLu {
66
88
  register(name: string, opts?: {
67
89
  person?: string | null;
68
90
  instructions?: string | null;
91
+ defaults?: ClientDefaults;
69
92
  }): Promise<ResponseObject & {
70
93
  api_key?: string;
71
94
  name: string;
72
95
  }>;
96
+ /**
97
+ * A runner bound to one registered use-case client.
98
+ *
99
+ * The use-case's defaults (system prompt, tools, temperature, ...) live
100
+ * server-side on the client, so `run()` only needs the input. Registering a
101
+ * use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
102
+ */
103
+ useCase(name: string, opts?: {
104
+ person?: string | null;
105
+ defaults?: ClientDefaults;
106
+ }): UseCaseRunner;
107
+ /** Register a use-case client and resolve to its api key. */
108
+ useCaseClient(name: string, opts?: {
109
+ person?: string | null;
110
+ defaults?: ClientDefaults;
111
+ }): Promise<string>;
73
112
  me(): Promise<unknown>;
74
113
  /**
75
114
  * One-shot helper. When `schema` is given, returns the validated object as
@@ -107,3 +146,36 @@ export declare class AgentLu {
107
146
  private request;
108
147
  private raw;
109
148
  }
149
+ /**
150
+ * A runner bound to one registered use-case (one service client + its
151
+ * server-side defaults). Holds its own key so multiple use-cases can share
152
+ * one parent AgentLu without clobbering each other.
153
+ *
154
+ * const alu = new AgentLu();
155
+ * const enrich = alu.useCase("mealu-enrich", {
156
+ * defaults: { instructions: "Parse meals...", temperature: 0.2 },
157
+ * });
158
+ * const { data } = await enrich.run({ input: "two eggs and toast" });
159
+ */
160
+ export declare class UseCaseRunner {
161
+ private readonly parent;
162
+ readonly name: string;
163
+ private readonly registerOpts;
164
+ private apiKey;
165
+ private ready;
166
+ constructor(parent: AgentLu, name: string, opts?: {
167
+ person?: string | null;
168
+ defaults?: ClientDefaults;
169
+ });
170
+ /** Idempotent get-or-create; safe to call before every run. */
171
+ private ensure;
172
+ /**
173
+ * Run with this use-case's key. Request fields that are set override the
174
+ * use-case's registered defaults; unset fields inherit them server-side.
175
+ */
176
+ run<T = unknown>(opts: RunOptions): Promise<{
177
+ data?: T;
178
+ response: ResponseObject;
179
+ }>;
180
+ stream(body: Record<string, unknown>): Promise<AsyncGenerator<Record<string, unknown>, any, any>>;
181
+ }
package/dist/index.js CHANGED
@@ -14,11 +14,15 @@ export class AgentLuError extends Error {
14
14
  status;
15
15
  code;
16
16
  retryable;
17
- constructor(status, code, message, retryable) {
17
+ retryAfterMs;
18
+ constructor(status, code, message, retryable,
19
+ /** Parsed Retry-After header, in milliseconds, when present. */
20
+ retryAfterMs) {
18
21
  super(message);
19
22
  this.status = status;
20
23
  this.code = code;
21
24
  this.retryable = retryable;
25
+ this.retryAfterMs = retryAfterMs;
22
26
  this.name = "AgentLuError";
23
27
  }
24
28
  }
@@ -41,6 +45,23 @@ export class AgentLu {
41
45
  this.apiKey = res.api_key;
42
46
  return res;
43
47
  }
48
+ /**
49
+ * A runner bound to one registered use-case client.
50
+ *
51
+ * The use-case's defaults (system prompt, tools, temperature, ...) live
52
+ * server-side on the client, so `run()` only needs the input. Registering a
53
+ * use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
54
+ */
55
+ useCase(name, opts) {
56
+ return new UseCaseRunner(this, name, opts);
57
+ }
58
+ /** Register a use-case client and resolve to its api key. */
59
+ async useCaseClient(name, opts) {
60
+ const res = await this.request("POST", "/clients", { name, ...opts }, { auth: false });
61
+ if (!res.api_key)
62
+ throw new AgentLuError(500, "internal_error", "register returned no key", true);
63
+ return res.api_key;
64
+ }
44
65
  async me() {
45
66
  return this.request("GET", "/me");
46
67
  }
@@ -59,8 +80,13 @@ export class AgentLu {
59
80
  conversation: opts.conversation,
60
81
  store: opts.store,
61
82
  person: opts.person,
83
+ owner: opts.owner,
84
+ title: opts.title,
62
85
  timeout_ms: opts.timeout_ms,
63
86
  include: opts.include,
87
+ temperature: opts.temperature,
88
+ max_output_tokens: opts.max_output_tokens,
89
+ background: opts.background,
64
90
  };
65
91
  if (opts.schema) {
66
92
  body.text = {
@@ -147,10 +173,11 @@ export class AgentLu {
147
173
  return this.request("GET", "/health", undefined, { auth: false });
148
174
  }
149
175
  async request(method, path, body, opts) {
150
- // `409 thread_busy` is a correctness bound, not a rate limit. Retrying with
151
- // a short backoff is almost always the right thing, so the SDK does it.
176
+ // `409 thread_busy` and `503 too_many_active_runs` are correctness bounds,
177
+ // not rate limits. Retrying with backoff is almost always the right thing,
178
+ // so the SDK does it - honoring Retry-After when the server sends one.
152
179
  let last;
153
- for (let attempt = 0; attempt < 4; attempt++) {
180
+ for (let attempt = 0; attempt < 5; attempt++) {
154
181
  try {
155
182
  const res = await this.raw(method, path, body, opts);
156
183
  if (res.status === 204)
@@ -159,15 +186,17 @@ export class AgentLu {
159
186
  return json;
160
187
  }
161
188
  catch (e) {
162
- if (e instanceof AgentLuError && e.code === "thread_busy" && attempt < 3) {
189
+ if (e instanceof AgentLuError &&
190
+ (e.code === "thread_busy" || e.code === "too_many_active_runs" || e.status === 503) &&
191
+ attempt < 4) {
163
192
  last = e;
164
- await sleep(500 * 2 ** attempt);
193
+ await sleep(retryDelayMs(e, attempt));
165
194
  continue;
166
195
  }
167
196
  throw e;
168
197
  }
169
198
  }
170
- throw last ?? new AgentLuError(409, "thread_busy", "thread busy", true);
199
+ throw last ?? new AgentLuError(503, "too_many_active_runs", "too many retries", true);
171
200
  }
172
201
  async raw(method, path, body, opts) {
173
202
  const headers = {};
@@ -196,11 +225,90 @@ export class AgentLu {
196
225
  catch {
197
226
  /* keep defaults */
198
227
  }
199
- throw new AgentLuError(res.status, code, message, retryable);
228
+ throw new AgentLuError(res.status, code, message, retryable, parseRetryAfter(res));
200
229
  }
201
230
  return res;
202
231
  }
203
232
  }
233
+ /**
234
+ * A runner bound to one registered use-case (one service client + its
235
+ * server-side defaults). Holds its own key so multiple use-cases can share
236
+ * one parent AgentLu without clobbering each other.
237
+ *
238
+ * const alu = new AgentLu();
239
+ * const enrich = alu.useCase("mealu-enrich", {
240
+ * defaults: { instructions: "Parse meals...", temperature: 0.2 },
241
+ * });
242
+ * const { data } = await enrich.run({ input: "two eggs and toast" });
243
+ */
244
+ export class UseCaseRunner {
245
+ parent;
246
+ name;
247
+ registerOpts;
248
+ apiKey = null;
249
+ ready = null;
250
+ constructor(parent, name, opts) {
251
+ this.parent = parent;
252
+ this.name = name;
253
+ this.registerOpts = opts ?? {};
254
+ }
255
+ /** Idempotent get-or-create; safe to call before every run. */
256
+ ensure() {
257
+ if (this.apiKey)
258
+ return Promise.resolve();
259
+ if (!this.ready) {
260
+ this.ready = this.parent
261
+ .useCaseClient(this.name, this.registerOpts)
262
+ .then((key) => {
263
+ this.apiKey = key;
264
+ })
265
+ .catch((e) => {
266
+ this.ready = null;
267
+ throw e;
268
+ });
269
+ }
270
+ return this.ready;
271
+ }
272
+ /**
273
+ * Run with this use-case's key. Request fields that are set override the
274
+ * use-case's registered defaults; unset fields inherit them server-side.
275
+ */
276
+ async run(opts) {
277
+ await this.ensure();
278
+ const parentKey = this.parent.apiKey;
279
+ this.parent.apiKey = this.apiKey ?? undefined;
280
+ try {
281
+ return await this.parent.run(opts);
282
+ }
283
+ finally {
284
+ this.parent.apiKey = parentKey;
285
+ }
286
+ }
287
+ async stream(body) {
288
+ await this.ensure();
289
+ const parentKey = this.parent.apiKey;
290
+ this.parent.apiKey = this.apiKey ?? undefined;
291
+ try {
292
+ return await this.parent.responses.stream(body);
293
+ }
294
+ finally {
295
+ this.parent.apiKey = parentKey;
296
+ }
297
+ }
298
+ }
299
+ function parseRetryAfter(res) {
300
+ const header = res.headers.get("retry-after");
301
+ if (!header)
302
+ return undefined;
303
+ const seconds = Number(header);
304
+ return Number.isFinite(seconds) ? seconds * 1000 : undefined;
305
+ }
306
+ function retryDelayMs(err, attempt) {
307
+ if (err.retryAfterMs !== undefined) {
308
+ return Math.min(10_000, Math.max(250, err.retryAfterMs));
309
+ }
310
+ return Math.min(10_000, 500 * 2 ** attempt);
311
+ }
204
312
  function sleep(ms) {
205
313
  return new Promise((r) => setTimeout(r, ms));
206
314
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterlu/agentlu",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",