@betterlu/agentlu 1.3.0 → 1.4.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 `run({ schema })`, and auto-retry on `409 thread_busy`.
13
+ This client adds get-or-create registration, typed `runJson`, and auto-retry on `409 thread_busy`.
14
14
 
15
15
  ## Install
16
16
 
@@ -33,4 +33,110 @@ const { data, response } = await alu.run({
33
33
  console.log(response.output_text);
34
34
  ```
35
35
 
36
+ ## JSON results
37
+
38
+ `runJson` returns only a completed response with the server-provided `output_parsed` value. It preserves JSON `null`, `false`, `0`, and empty arrays. A missing parsed value, failed response, incomplete response, cancellation, or queued response throws an SDK error instead of parsing `output_text`. `background: true` is rejected by this helper; use raw `run` when polling is intended.
39
+
40
+ ```ts
41
+ const { data, response } = await alu.runJson<{ answer: string }>({
42
+ input: "Answer briefly",
43
+ schema: { type: "object", properties: { answer: { type: "string" } } },
44
+ schemaName: "Answer",
45
+ strict: true,
46
+ });
47
+ ```
48
+
49
+ An explicit `text.format` is forwarded unchanged. In particular, `text: { format: { type: "text" } }` is not silently changed to JSON by `runJson`.
50
+
51
+ ## Use-case defaults
52
+
53
+ Defaults are resolved by the service for every request:
54
+
55
+ ```text
56
+ request field -> registered client default -> agent/profile default
57
+ ```
58
+
59
+ Register a client once per use-case. The runner sends only request-level overrides, so its registered schema is not resent with every run:
60
+
61
+ ```ts
62
+ const schema = { type: "object", properties: { meal: { type: "string" } } };
63
+ const mealEnricher = alu.useCase("mealu-enrich", {
64
+ defaults: {
65
+ agent: "api",
66
+ store: false,
67
+ tools: [],
68
+ reasoning: { effort: "medium" },
69
+ text: { format: { type: "json_schema", name: "Meal", schema } },
70
+ },
71
+ });
72
+
73
+ const { data } = await mealEnricher.runJson<{ meal: string }>({ input: "two eggs" });
74
+ ```
75
+
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
+
36
78
  Env: `AGENTLU_URL` (default `http://localhost:8912/v1`), `AGENTLU_API_KEY`.
79
+
80
+ ## Decisions (System One / Jev)
81
+
82
+ `decisions` is a different primitive from `run`: Jev returns typed, calibrated
83
+ probabilities - never text - in ~100-500ms. Use it for routing, classification,
84
+ scoring and gating inside your own code.
85
+
86
+ ```ts
87
+ import { AgentLu, noul, choice, score } from "@betterlu/agentlu";
88
+
89
+ const alu = new AgentLu();
90
+ alu.apiKey = key;
91
+
92
+ const { answers } = await alu.decisions.create({
93
+ state: "I was charged twice and I'm losing sales. Please fix this ASAP.",
94
+ questions: {
95
+ urgent: noul("Does this need urgent attention?"),
96
+ team: choice("Which team should handle it?", {
97
+ billing: "charges and payments",
98
+ shipping: "delivery",
99
+ }),
100
+ frustration: score("How frustrated is the customer?", ["Calm", "Frustrated", "Very angry"]),
101
+ },
102
+ });
103
+
104
+ if (answers.urgent.type === "noul" && answers.urgent.noul > 0.8) routeToHuman();
105
+ if (answers.team.type === "choice") return lookupOrder(answers.team.choice);
106
+ ```
107
+
108
+ `answers.<name>` is a discriminated union on `type`:
109
+
110
+ | Question | Answer fields |
111
+ |---|---|
112
+ | `noul` | `noul` - probability that the answer is yes (0..1) |
113
+ | `choice` | `choice`, `confidence`, `probabilities` per option |
114
+ | `score` | `score`, `confidence`, `legend`, `probabilities` per step |
115
+
116
+ `decisions.ask()` is the same call returning only the answers map. Discover the
117
+ model in `GET /v1/models` (`harness: "systemone"`); it is only callable on
118
+ `POST /v1/decisions`, sync, no streaming. A missing key surfaces as HTTP 503
119
+ `systemone_unconfigured` (not retried by the SDK).
120
+
121
+ ## Browser handoff
122
+
123
+ The `browser` family reuses a persistent, headful BrowserLu session. Scope it
124
+ to the operations the caller needs and identify the linked household person:
125
+
126
+ ```ts
127
+ const { response } = await alu.run({
128
+ agent: "api",
129
+ person: "larry",
130
+ tools: [{ type: "browser", allowed_tools: ["browser_open", "browser_snapshot"] }],
131
+ input: "Read the current page and summarize it.",
132
+ });
133
+
134
+ if (response.agentlu?.browser?.status === "needs_human") {
135
+ // Let this request end. The human completes login/MFA in the AgentLu viewer,
136
+ // then send a new request with the same conversation/session context.
137
+ }
138
+ ```
139
+
140
+ Browser response metadata contains the session and page identity plus screenshot
141
+ ids. Screenshot pixels stay in the authenticated viewer and are not copied into
142
+ API response metadata.
package/dist/index.d.ts CHANGED
@@ -6,10 +6,155 @@
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 `run({ schema })`, and auto-retry on
9
+ * get-or-create registration, typed `runJson`, and auto-retry on
10
10
  * `409 thread_busy`.
11
11
  */
12
12
  export declare const DEFAULT_BASE_URL = "http://localhost:8912/v1";
13
+ export type Backend = "pi";
14
+ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
15
+ export type ApiToolFamily = "web_search" | "fetch_content" | "hublu" | "memory" | "image" | "browser" | "android";
16
+ /** Closed tool-family request accepted by `/v1/responses`. */
17
+ export interface ApiToolSpec {
18
+ type: ApiToolFamily;
19
+ /** Optional family-specific operation allowlist (HubLu, BrowserLu, or AndroidLu). */
20
+ allowed_tools?: string[];
21
+ }
22
+ export type ApiToolInput = ApiToolFamily | ApiToolSpec;
23
+ export type BrowserStatus = "active" | "needs_human" | "human_control" | "idle" | "closed" | "interrupted";
24
+ export interface BrowserScreenshot {
25
+ id: string;
26
+ createdAt: string;
27
+ width: number;
28
+ height: number;
29
+ }
30
+ export interface BrowserActivity {
31
+ sessionId: string;
32
+ username: "larry" | "misa";
33
+ ownerId?: string;
34
+ tabId?: string;
35
+ url?: string;
36
+ title?: string;
37
+ status: BrowserStatus;
38
+ action?: string;
39
+ reason?: string;
40
+ observationId?: string;
41
+ screenshots?: BrowserScreenshot[];
42
+ }
43
+ export type AndroidStatus = "active" | "needs_human" | "human_control" | "idle" | "closed" | "interrupted" | "reserved";
44
+ export type AndroidOperationState = "running" | "succeeded" | "failed" | "unknown";
45
+ export interface AndroidOperation {
46
+ operationId: string;
47
+ kind: string;
48
+ state: AndroidOperationState;
49
+ dispatchedAt: string;
50
+ settledAt?: string;
51
+ outcome?: string;
52
+ }
53
+ export interface AndroidActivity {
54
+ sessionId: string;
55
+ username: "larry" | "misa";
56
+ ownerId?: string;
57
+ status: AndroidStatus;
58
+ action?: string;
59
+ reason?: string;
60
+ observationId?: string;
61
+ leaseId?: string;
62
+ readiness?: "stopped" | "booting" | "ready" | "failed";
63
+ control?: "idle" | "agent" | "needs_human" | "human" | "reserved" | "interrupted";
64
+ foregroundPackage?: string;
65
+ pendingOperation?: AndroidOperation;
66
+ lastOperation?: AndroidOperation;
67
+ viewerPath?: string;
68
+ reservationExpiresAt?: string;
69
+ }
70
+ export interface ReasoningOptions {
71
+ effort?: ThinkingLevel;
72
+ }
73
+ export interface ClientReasoningOptions {
74
+ effort: ThinkingLevel;
75
+ }
76
+ export interface TextFormatText {
77
+ type: "text";
78
+ }
79
+ export interface TextFormatJsonSchema {
80
+ type: "json_schema";
81
+ name: string;
82
+ schema: Record<string, unknown>;
83
+ strict?: boolean;
84
+ description?: string;
85
+ }
86
+ export type TextFormat = TextFormatText | TextFormatJsonSchema;
87
+ export interface TextConfig {
88
+ format?: TextFormat;
89
+ }
90
+ /** Yes/no, answered as the probability that the answer is yes. */
91
+ export interface NoulQuestion {
92
+ type: "noul";
93
+ instructions: string;
94
+ /** Optional descriptions of the true/false poles (calibrates the answer). */
95
+ criteria?: {
96
+ true: string;
97
+ false: string;
98
+ };
99
+ }
100
+ export interface ChoiceQuestion {
101
+ type: "choice";
102
+ instructions: string;
103
+ /** Option name -> description. At least two options. */
104
+ criteria: Record<string, string>;
105
+ }
106
+ export interface ScoreQuestion {
107
+ type: "score";
108
+ instructions: string;
109
+ /** Ordered levels, lowest first. At least two. */
110
+ criteria: string[];
111
+ }
112
+ export type DecisionQuestion = NoulQuestion | ChoiceQuestion | ScoreQuestion;
113
+ export interface NoulAnswer {
114
+ type: "noul";
115
+ noul: number;
116
+ }
117
+ export interface ChoiceAnswer {
118
+ type: "choice";
119
+ choice: string;
120
+ confidence: number;
121
+ probabilities: Record<string, number>;
122
+ }
123
+ export interface ScoreAnswer {
124
+ type: "score";
125
+ score: number;
126
+ confidence: number;
127
+ legend?: Record<string, string>;
128
+ probabilities: Record<string, number>;
129
+ }
130
+ export type DecisionAnswer = NoulAnswer | ChoiceAnswer | ScoreAnswer;
131
+ export interface DecisionOptions {
132
+ /** Unstructured input: a text state, or a JSON object (serialized upstream). */
133
+ state: string | Record<string, unknown>;
134
+ questions: Record<string, DecisionQuestion>;
135
+ /** Decision model id (default: the configured `typesafe/jev`). */
136
+ model?: string;
137
+ }
138
+ export interface DecisionObject {
139
+ id: string;
140
+ object: "decision";
141
+ created_at: number;
142
+ model: string;
143
+ answers: Record<string, DecisionAnswer>;
144
+ usage: {
145
+ input_tokens: number;
146
+ output_tokens: number;
147
+ total_tokens: number;
148
+ } | null;
149
+ [k: string]: unknown;
150
+ }
151
+ /** Question builders; sugar over the wire shape. */
152
+ export declare const noul: (instructions: string, criteria?: {
153
+ true: string;
154
+ false: string;
155
+ }) => NoulQuestion;
156
+ export declare const choice: (instructions: string, criteria: Record<string, string>) => ChoiceQuestion;
157
+ export declare const score: (instructions: string, levels: string[]) => ScoreQuestion;
13
158
  export declare class AgentLuError extends Error {
14
159
  readonly status: number;
15
160
  readonly code: string;
@@ -20,24 +165,37 @@ export declare class AgentLuError extends Error {
20
165
  /** Parsed Retry-After header, in milliseconds, when present. */
21
166
  retryAfterMs?: number | undefined);
22
167
  }
168
+ export declare class AgentLuResponseError extends AgentLuError {
169
+ readonly response: ResponseObject;
170
+ readonly responseStatus: string;
171
+ constructor(response: ResponseObject);
172
+ }
23
173
  export interface AgentLuOptions {
24
174
  baseUrl?: string;
25
175
  apiKey?: string;
26
176
  fetch?: typeof fetch;
27
177
  }
28
178
  export interface ClientDefaults {
179
+ backend?: Backend;
29
180
  agent?: string;
30
181
  model?: string;
31
182
  instructions?: string;
32
183
  temperature?: number;
33
- maxOutputTokens?: number;
34
- tools?: Array<string | {
35
- type: string;
36
- allowed_tools?: string[];
37
- }>;
184
+ tools?: ApiToolInput[];
38
185
  timeoutMs?: number;
186
+ store?: boolean;
187
+ reasoning?: ClientReasoningOptions;
188
+ text?: TextConfig;
189
+ }
190
+ /** High-level use-case declaration for model routing and priority tier. */
191
+ export interface UseCaseProfile {
192
+ inputModalities?: Array<"text" | "image" | "video">;
193
+ outputKind?: "text" | "json_schema";
194
+ /** MiniMax priority admission consent (priority costs 1.5x standard). */
195
+ priorityTier?: "never" | "retry" | "first" | "always";
39
196
  }
40
197
  export interface RunOptions {
198
+ backend?: Backend;
41
199
  agent?: string;
42
200
  model?: string;
43
201
  input?: string;
@@ -54,10 +212,10 @@ export interface RunOptions {
54
212
  content: string | Array<Record<string, unknown>>;
55
213
  }>;
56
214
  instructions?: string;
57
- tools?: Array<string | {
58
- type: string;
59
- allowed_tools?: string[];
60
- }>;
215
+ tools?: ApiToolInput[];
216
+ reasoning?: ReasoningOptions;
217
+ text?: TextConfig;
218
+ strict?: boolean;
61
219
  conversation?: string;
62
220
  store?: boolean;
63
221
  person?: string | null;
@@ -78,6 +236,7 @@ export interface RunOptions {
78
236
  export interface ResponseObject {
79
237
  id: string;
80
238
  status: string;
239
+ backend: Backend;
81
240
  output_text: string;
82
241
  output_parsed?: unknown;
83
242
  conversation?: {
@@ -88,8 +247,19 @@ export interface ResponseObject {
88
247
  message: string;
89
248
  retryable: boolean;
90
249
  } | null;
250
+ incomplete_details?: {
251
+ reason: string;
252
+ } | null;
91
253
  agentlu?: {
92
254
  session_url?: string | null;
255
+ /** Model the runtime actually served (requested `model` may have fallen back). */
256
+ effective_model?: string | null;
257
+ /** MiniMax admission tier requested for the winning attempt, when any. */
258
+ service_tier?: "standard" | "priority";
259
+ /** Latest persistent BrowserLu activity; screenshot bytes are never in this object. */
260
+ browser?: BrowserActivity;
261
+ /** Latest persistent AndroidLu activity; screenshot bytes are never in this object. */
262
+ android?: AndroidActivity;
93
263
  };
94
264
  [k: string]: unknown;
95
265
  }
@@ -98,11 +268,13 @@ export declare class AgentLu {
98
268
  apiKey: string | undefined;
99
269
  private readonly fetchImpl;
100
270
  constructor(opts?: AgentLuOptions);
271
+ private postClient;
101
272
  /** Get-or-create a client by name. Caches the returned key on this instance. */
102
273
  register(name: string, opts?: {
103
274
  person?: string | null;
104
275
  instructions?: string | null;
105
276
  defaults?: ClientDefaults;
277
+ use_case_profile?: UseCaseProfile;
106
278
  }): Promise<ResponseObject & {
107
279
  api_key?: string;
108
280
  name: string;
@@ -113,10 +285,11 @@ export declare class AgentLu {
113
285
  * The use-case's defaults (system prompt, tools, temperature, ...) live
114
286
  * server-side on the client, so `run()` only needs the input. Registering a
115
287
  * use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
116
- */
288
+ */
117
289
  useCase(name: string, opts?: {
118
290
  person?: string | null;
119
291
  defaults?: ClientDefaults;
292
+ use_case_profile?: UseCaseProfile;
120
293
  }): UseCaseRunner;
121
294
  /**
122
295
  * Register a use-case client and resolve to its api key. The key is cached
@@ -126,7 +299,9 @@ export declare class AgentLu {
126
299
  useCaseClient(name: string, opts?: {
127
300
  person?: string | null;
128
301
  defaults?: ClientDefaults;
302
+ use_case_profile?: UseCaseProfile;
129
303
  }): Promise<string>;
304
+ private registerForUseCase;
130
305
  /**
131
306
  * Upload an image, video, or document for use with `run({ fileIds })`.
132
307
  * Returns the `file_id` and stored `kind`.
@@ -135,6 +310,7 @@ export declare class AgentLu {
135
310
  id: string;
136
311
  kind?: string;
137
312
  }>;
313
+ private uploadForApiKey;
138
314
  me(): Promise<unknown>;
139
315
  /**
140
316
  * One-shot helper. When `schema` is given, returns the validated object as
@@ -145,6 +321,13 @@ export declare class AgentLu {
145
321
  data?: T;
146
322
  response: ResponseObject;
147
323
  }>;
324
+ runJson<T = unknown>(opts: RunOptions): Promise<{
325
+ data: T;
326
+ response: ResponseObject;
327
+ }>;
328
+ private runForApiKey;
329
+ private runJsonForApiKey;
330
+ private createRunResponse;
148
331
  readonly responses: {
149
332
  create: (body: Record<string, unknown>) => Promise<ResponseObject>;
150
333
  retrieve: (id: string) => Promise<ResponseObject>;
@@ -156,8 +339,9 @@ export declare class AgentLu {
156
339
  object: string;
157
340
  data: ResponseObject[];
158
341
  }>;
159
- stream: (body: Record<string, unknown>) => AsyncGenerator<Record<string, unknown>>;
342
+ stream: (body: Record<string, unknown>) => AsyncGenerator<Record<string, unknown>, any, any>;
160
343
  };
344
+ private streamForApiKey;
161
345
  readonly threads: {
162
346
  create: (body?: Record<string, unknown>) => Promise<unknown>;
163
347
  retrieve: (id: string) => Promise<unknown>;
@@ -165,6 +349,24 @@ export declare class AgentLu {
165
349
  delete: (id: string) => Promise<unknown>;
166
350
  messages: (id: string) => Promise<unknown>;
167
351
  };
352
+ /**
353
+ * Decision model (System One / Jev): typed questions in, calibrated
354
+ * probabilities out, ~100-500ms. Sync only - there is no stream or poll.
355
+ *
356
+ * const { answers } = await alu.decisions.create({
357
+ * state: "I was charged twice and I'm losing sales. Please fix this ASAP.",
358
+ * questions: {
359
+ * urgent: noul("Does this need urgent attention?"),
360
+ * team: choice("Which team should handle it?", { billing: "charges", shipping: "delivery" }),
361
+ * },
362
+ * });
363
+ * if (answers.urgent.noul > 0.8) routeToHuman();
364
+ */
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
+ };
168
370
  agents(): Promise<unknown>;
169
371
  models(): Promise<unknown>;
170
372
  tools(): Promise<unknown>;
@@ -184,14 +386,15 @@ export declare class AgentLu {
184
386
  * const { data } = await enrich.run({ input: "two eggs and toast" });
185
387
  */
186
388
  export declare class UseCaseRunner {
187
- private readonly parent;
188
389
  readonly name: string;
189
390
  private readonly registerOpts;
391
+ private readonly operations;
190
392
  private apiKey;
191
393
  private ready;
192
394
  constructor(parent: AgentLu, name: string, opts?: {
193
395
  person?: string | null;
194
396
  defaults?: ClientDefaults;
397
+ use_case_profile?: UseCaseProfile;
195
398
  });
196
399
  /** Idempotent get-or-create; safe to call before every run. */
197
400
  private ensure;
@@ -203,6 +406,10 @@ export declare class UseCaseRunner {
203
406
  data?: T;
204
407
  response: ResponseObject;
205
408
  }>;
409
+ runJson<T = unknown>(opts: RunOptions): Promise<{
410
+ data: T;
411
+ response: ResponseObject;
412
+ }>;
206
413
  stream(body: Record<string, unknown>): Promise<AsyncGenerator<Record<string, unknown>, any, any>>;
207
414
  /**
208
415
  * Upload with this use-case's key, so the file is owned by the same
package/dist/index.js CHANGED
@@ -6,10 +6,26 @@
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 `run({ schema })`, and auto-retry on
9
+ * get-or-create registration, typed `runJson`, and auto-retry on
10
10
  * `409 thread_busy`.
11
11
  */
12
12
  export const DEFAULT_BASE_URL = "http://localhost:8912/v1";
13
+ /** Question builders; sugar over the wire shape. */
14
+ export const noul = (instructions, criteria) => ({
15
+ type: "noul",
16
+ instructions,
17
+ ...(criteria ? { criteria } : {}),
18
+ });
19
+ export const choice = (instructions, criteria) => ({
20
+ type: "choice",
21
+ instructions,
22
+ criteria,
23
+ });
24
+ export const score = (instructions, levels) => ({
25
+ type: "score",
26
+ instructions,
27
+ criteria: levels,
28
+ });
13
29
  export class AgentLuError extends Error {
14
30
  status;
15
31
  code;
@@ -26,9 +42,23 @@ export class AgentLuError extends Error {
26
42
  this.name = "AgentLuError";
27
43
  }
28
44
  }
45
+ export class AgentLuResponseError extends AgentLuError {
46
+ response;
47
+ responseStatus;
48
+ constructor(response) {
49
+ const responseStatus = response.status;
50
+ const code = response.error?.code ?? responseStatus;
51
+ const message = response.error?.message ?? `Response status "${responseStatus}" is not completed.`;
52
+ super(200, code, message, response.error?.retryable ?? false);
53
+ this.name = "AgentLuResponseError";
54
+ this.response = response;
55
+ this.responseStatus = responseStatus;
56
+ }
57
+ }
29
58
  function env(name) {
30
59
  return typeof process !== "undefined" ? process.env[name] : undefined;
31
60
  }
61
+ const scopedUseCaseOperations = new WeakMap();
32
62
  export class AgentLu {
33
63
  baseUrl;
34
64
  apiKey;
@@ -37,10 +67,20 @@ export class AgentLu {
37
67
  this.baseUrl = (opts.baseUrl ?? env("AGENTLU_URL") ?? DEFAULT_BASE_URL).replace(/\/$/, "");
38
68
  this.apiKey = opts.apiKey ?? env("AGENTLU_API_KEY");
39
69
  this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
70
+ scopedUseCaseOperations.set(this, {
71
+ register: (name, registerOpts) => this.registerForUseCase(name, registerOpts),
72
+ run: (apiKey, runOpts) => this.runForApiKey(apiKey, runOpts),
73
+ 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),
76
+ });
77
+ }
78
+ postClient(name, opts) {
79
+ return this.request("POST", "/clients", { name, ...opts }, { auth: false });
40
80
  }
41
81
  /** Get-or-create a client by name. Caches the returned key on this instance. */
42
82
  async register(name, opts) {
43
- const res = await this.request("POST", "/clients", { name, ...opts }, { auth: false });
83
+ const res = await this.postClient(name, opts);
44
84
  if (res.api_key)
45
85
  this.apiKey = res.api_key;
46
86
  return res;
@@ -51,7 +91,7 @@ export class AgentLu {
51
91
  * The use-case's defaults (system prompt, tools, temperature, ...) live
52
92
  * server-side on the client, so `run()` only needs the input. Registering a
53
93
  * use-case is registering a client - `alu.useCase("mealu-enrich", {...})`.
54
- */
94
+ */
55
95
  useCase(name, opts) {
56
96
  return new UseCaseRunner(this, name, opts);
57
97
  }
@@ -61,21 +101,30 @@ export class AgentLu {
61
101
  * `responses.create` is authorized.
62
102
  */
63
103
  async useCaseClient(name, opts) {
64
- const res = await this.request("POST", "/clients", { name, ...opts }, { auth: false });
104
+ const res = await this.postClient(name, opts);
65
105
  if (!res.api_key)
66
106
  throw new AgentLuError(500, "internal_error", "register returned no key", true);
67
107
  this.apiKey = res.api_key;
68
108
  return res.api_key;
69
109
  }
110
+ async registerForUseCase(name, opts) {
111
+ const res = await this.postClient(name, opts);
112
+ if (!res.api_key)
113
+ throw new AgentLuError(500, "internal_error", "register returned no key", true);
114
+ return res.api_key;
115
+ }
70
116
  /**
71
117
  * Upload an image, video, or document for use with `run({ fileIds })`.
72
118
  * Returns the `file_id` and stored `kind`.
73
119
  */
74
120
  async upload(data, filename, mime) {
121
+ return this.uploadForApiKey(data, filename, mime, this.apiKey);
122
+ }
123
+ async uploadForApiKey(data, filename, mime, apiKey) {
75
124
  const form = new FormData();
76
125
  const blob = data instanceof Blob ? data : new Blob([new Uint8Array(data)], { type: mime ?? "application/octet-stream" });
77
126
  form.append("file", blob, filename);
78
- const res = await this.raw("POST", "/files", form);
127
+ const res = await this.raw("POST", "/files", form, { apiKey });
79
128
  return (await res.json());
80
129
  }
81
130
  async me() {
@@ -87,6 +136,29 @@ export class AgentLu {
87
136
  * could not produce matching JSON.
88
137
  */
89
138
  async run(opts) {
139
+ return this.runForApiKey(this.apiKey, opts);
140
+ }
141
+ async runJson(opts) {
142
+ assertJsonRunOptions(opts);
143
+ return this.runJsonForApiKey(this.apiKey, opts);
144
+ }
145
+ async runForApiKey(apiKey, opts) {
146
+ const response = await this.createRunResponse(opts, apiKey);
147
+ if (response.status === "failed" && response.error) {
148
+ throw new AgentLuError(200, response.error.code, response.error.message, response.error.retryable);
149
+ }
150
+ return { data: response.output_parsed, response };
151
+ }
152
+ async runJsonForApiKey(apiKey, opts) {
153
+ const response = await this.createRunResponse(opts, apiKey);
154
+ if (response.status !== "completed")
155
+ throw new AgentLuResponseError(response);
156
+ if (!Object.prototype.hasOwnProperty.call(response, "output_parsed")) {
157
+ throw new AgentLuError(200, "missing_output_parsed", "Completed response did not include output_parsed.", false);
158
+ }
159
+ return { data: response.output_parsed, response };
160
+ }
161
+ async createRunResponse(opts, apiKey) {
90
162
  let input = opts.inputMessages ?? opts.input;
91
163
  if (opts.fileIds?.length) {
92
164
  input = [
@@ -108,10 +180,12 @@ export class AgentLu {
108
180
  }
109
181
  const body = {
110
182
  input,
183
+ backend: opts.backend,
111
184
  agent: opts.agent,
112
185
  model: opts.model,
113
186
  instructions: opts.instructions,
114
187
  tools: opts.tools,
188
+ reasoning: opts.reasoning,
115
189
  conversation: opts.conversation,
116
190
  store: opts.store,
117
191
  person: opts.person,
@@ -124,21 +198,20 @@ export class AgentLu {
124
198
  background: opts.background,
125
199
  metadata: opts.metadata,
126
200
  };
127
- if (opts.schema) {
201
+ if (opts.text !== undefined) {
202
+ body.text = opts.text;
203
+ }
204
+ else if (opts.schema !== undefined) {
128
205
  body.text = {
129
206
  format: {
130
207
  type: "json_schema",
131
208
  name: opts.schemaName ?? "Result",
132
209
  schema: opts.schema,
133
- strict: true,
210
+ strict: opts.strict ?? true,
134
211
  },
135
212
  };
136
213
  }
137
- const response = await this.responses.create(body);
138
- if (response.status === "failed" && response.error) {
139
- throw new AgentLuError(200, response.error.code, response.error.message, response.error.retryable);
140
- }
141
- return { data: response.output_parsed, response };
214
+ return this.request("POST", "/responses", body, { apiKey });
142
215
  }
143
216
  responses = {
144
217
  create: (body) => this.request("POST", "/responses", body),
@@ -153,42 +226,43 @@ export class AgentLu {
153
226
  const qs = q.toString();
154
227
  return this.request("GET", `/responses${qs ? `?${qs}` : ""}`);
155
228
  },
156
- stream: async function* stream(body) {
157
- const res = await this.raw("POST", "/responses", { ...body, stream: true });
158
- if (!res.body)
159
- throw new AgentLuError(0, "internal_error", "empty stream body", true);
160
- const reader = res.body.getReader();
161
- const decoder = new TextDecoder();
162
- let buf = "";
163
- try {
164
- for (;;) {
165
- const { done, value } = await reader.read();
166
- if (done)
167
- break;
168
- buf += decoder.decode(value, { stream: true });
169
- const lines = buf.split("\n");
170
- buf = lines.pop() ?? "";
171
- for (const line of lines) {
172
- const trimmed = line.trim();
173
- if (!trimmed.startsWith("data: "))
174
- continue;
175
- const payload = trimmed.slice(6);
176
- if (payload === "[DONE]")
177
- return;
178
- try {
179
- yield JSON.parse(payload);
180
- }
181
- catch {
182
- /* ignore malformed chunks */
183
- }
229
+ stream: (body) => this.streamForApiKey(body),
230
+ };
231
+ async *streamForApiKey(body, apiKey) {
232
+ const res = await this.raw("POST", "/responses", { ...body, stream: true }, { apiKey });
233
+ if (!res.body)
234
+ throw new AgentLuError(0, "internal_error", "empty stream body", true);
235
+ const reader = res.body.getReader();
236
+ const decoder = new TextDecoder();
237
+ let buf = "";
238
+ try {
239
+ for (;;) {
240
+ const { done, value } = await reader.read();
241
+ if (done)
242
+ break;
243
+ buf += decoder.decode(value, { stream: true });
244
+ const lines = buf.split("\n");
245
+ buf = lines.pop() ?? "";
246
+ for (const line of lines) {
247
+ const trimmed = line.trim();
248
+ if (!trimmed.startsWith("data: "))
249
+ continue;
250
+ const payload = trimmed.slice(6);
251
+ if (payload === "[DONE]")
252
+ return;
253
+ try {
254
+ yield JSON.parse(payload);
255
+ }
256
+ catch {
257
+ /* ignore malformed chunks */
184
258
  }
185
259
  }
186
260
  }
187
- finally {
188
- reader.releaseLock();
189
- }
190
- }.bind(this),
191
- };
261
+ }
262
+ finally {
263
+ reader.releaseLock();
264
+ }
265
+ }
192
266
  threads = {
193
267
  create: (body = {}) => this.request("POST", "/threads", body),
194
268
  retrieve: (id) => this.request("GET", `/threads/${encodeURIComponent(id)}`),
@@ -196,6 +270,24 @@ export class AgentLu {
196
270
  delete: (id) => this.request("DELETE", `/threads/${encodeURIComponent(id)}`),
197
271
  messages: (id) => this.request("GET", `/threads/${encodeURIComponent(id)}/messages`),
198
272
  };
273
+ /**
274
+ * Decision model (System One / Jev): typed questions in, calibrated
275
+ * probabilities out, ~100-500ms. Sync only - there is no stream or poll.
276
+ *
277
+ * const { answers } = await alu.decisions.create({
278
+ * state: "I was charged twice and I'm losing sales. Please fix this ASAP.",
279
+ * questions: {
280
+ * urgent: noul("Does this need urgent attention?"),
281
+ * team: choice("Which team should handle it?", { billing: "charges", shipping: "delivery" }),
282
+ * },
283
+ * });
284
+ * if (answers.urgent.noul > 0.8) routeToHuman();
285
+ */
286
+ 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,
290
+ };
199
291
  async agents() {
200
292
  return this.request("GET", "/agents", undefined, { auth: false });
201
293
  }
@@ -212,6 +304,9 @@ export class AgentLu {
212
304
  // `409 thread_busy` and `503 too_many_active_runs` are correctness bounds,
213
305
  // not rate limits. Retrying with backoff is almost always the right thing,
214
306
  // so the SDK does it - honoring Retry-After when the server sends one.
307
+ // A 503 can also signal a config boundary (no decision model key); those
308
+ // are not retryable and must surface immediately.
309
+ const nonRetryable503 = new Set(["systemone_unconfigured"]);
215
310
  let last;
216
311
  for (let attempt = 0; attempt < 5; attempt++) {
217
312
  try {
@@ -223,7 +318,9 @@ export class AgentLu {
223
318
  }
224
319
  catch (e) {
225
320
  if (e instanceof AgentLuError &&
226
- (e.code === "thread_busy" || e.code === "too_many_active_runs" || e.status === 503) &&
321
+ (e.code === "thread_busy" ||
322
+ e.code === "too_many_active_runs" ||
323
+ (e.status === 503 && !nonRetryable503.has(e.code))) &&
227
324
  attempt < 4) {
228
325
  last = e;
229
326
  await sleep(retryDelayMs(e, attempt));
@@ -240,8 +337,9 @@ export class AgentLu {
240
337
  if (body !== undefined && !isForm) {
241
338
  headers["Content-Type"] = "application/json";
242
339
  }
243
- if (opts?.auth !== false && this.apiKey)
244
- headers.Authorization = `Bearer ${this.apiKey}`;
340
+ const apiKey = opts?.apiKey ?? this.apiKey;
341
+ if (opts?.auth !== false && apiKey)
342
+ headers.Authorization = `Bearer ${apiKey}`;
245
343
  const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
246
344
  method,
247
345
  headers,
@@ -280,23 +378,22 @@ export class AgentLu {
280
378
  * const { data } = await enrich.run({ input: "two eggs and toast" });
281
379
  */
282
380
  export class UseCaseRunner {
283
- parent;
284
381
  name;
285
382
  registerOpts;
383
+ operations;
286
384
  apiKey = null;
287
385
  ready = null;
288
386
  constructor(parent, name, opts) {
289
- this.parent = parent;
290
387
  this.name = name;
291
388
  this.registerOpts = opts ?? {};
389
+ this.operations = scopedUseCaseOperations.get(parent);
292
390
  }
293
391
  /** Idempotent get-or-create; safe to call before every run. */
294
392
  ensure() {
295
393
  if (this.apiKey)
296
394
  return Promise.resolve();
297
395
  if (!this.ready) {
298
- this.ready = this.parent
299
- .useCaseClient(this.name, this.registerOpts)
396
+ this.ready = this.operations.register(this.name, this.registerOpts)
300
397
  .then((key) => {
301
398
  this.apiKey = key;
302
399
  })
@@ -313,25 +410,16 @@ export class UseCaseRunner {
313
410
  */
314
411
  async run(opts) {
315
412
  await this.ensure();
316
- const parentKey = this.parent.apiKey;
317
- this.parent.apiKey = this.apiKey ?? undefined;
318
- try {
319
- return await this.parent.run(opts);
320
- }
321
- finally {
322
- this.parent.apiKey = parentKey;
323
- }
413
+ return (await this.operations.run(this.apiKey, opts));
414
+ }
415
+ async runJson(opts) {
416
+ assertJsonRunOptions(opts);
417
+ await this.ensure();
418
+ return (await this.operations.runJson(this.apiKey, opts));
324
419
  }
325
420
  async stream(body) {
326
421
  await this.ensure();
327
- const parentKey = this.parent.apiKey;
328
- this.parent.apiKey = this.apiKey ?? undefined;
329
- try {
330
- return await this.parent.responses.stream(body);
331
- }
332
- finally {
333
- this.parent.apiKey = parentKey;
334
- }
422
+ return this.operations.stream(this.apiKey, body);
335
423
  }
336
424
  /**
337
425
  * Upload with this use-case's key, so the file is owned by the same
@@ -339,14 +427,12 @@ export class UseCaseRunner {
339
427
  */
340
428
  async upload(data, filename, mime) {
341
429
  await this.ensure();
342
- const parentKey = this.parent.apiKey;
343
- this.parent.apiKey = this.apiKey ?? undefined;
344
- try {
345
- return await this.parent.upload(data, filename, mime);
346
- }
347
- finally {
348
- this.parent.apiKey = parentKey;
349
- }
430
+ return this.operations.upload(this.apiKey, data, filename, mime);
431
+ }
432
+ }
433
+ function assertJsonRunOptions(opts) {
434
+ if (opts.background === true) {
435
+ throw new AgentLuError(400, "background_not_supported", "runJson requires a completed response and cannot use background.", false);
350
436
  }
351
437
  }
352
438
  function parseRetryAfter(res) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@betterlu/agentlu",
3
- "version": "1.3.0",
3
+ "version": "1.4.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",