@betterlu/agentlu 1.4.0 → 1.5.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 +18 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +59 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,6 +118,24 @@ model in `GET /v1/models` (`harness: "systemone"`); it is only callable on
|
|
|
118
118
|
`POST /v1/decisions`, sync, no streaming. A missing key surfaces as HTTP 503
|
|
119
119
|
`systemone_unconfigured` (not retried by the SDK).
|
|
120
120
|
|
|
121
|
+
Decision calls accept three client-side options beyond `state` / `questions` /
|
|
122
|
+
`model`:
|
|
123
|
+
|
|
124
|
+
| Option | Effect |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `timeoutMs` | Per-attempt deadline. A timeout aborts the request and is never retried. |
|
|
127
|
+
| `maxAttempts` | Total attempts including the first; defaults to the shared 5-attempt policy. |
|
|
128
|
+
| `signal` | Caller cancellation, combined with `timeoutMs` when both are set. |
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const answers = await alu.decisions.ask({
|
|
132
|
+
state: latestMessage,
|
|
133
|
+
questions: { rename: noul("Is this a name change request?") },
|
|
134
|
+
timeoutMs: 5_000,
|
|
135
|
+
maxAttempts: 2,
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
121
139
|
## Browser handoff
|
|
122
140
|
|
|
123
141
|
The `browser` family reuses a persistent, headful BrowserLu session. Scope it
|
package/dist/index.d.ts
CHANGED
|
@@ -134,6 +134,16 @@ export interface DecisionOptions {
|
|
|
134
134
|
questions: Record<string, DecisionQuestion>;
|
|
135
135
|
/** Decision model id (default: the configured `typesafe/jev`). */
|
|
136
136
|
model?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Per-attempt client deadline in milliseconds. A timeout aborts the request
|
|
139
|
+
* and is never retried; without it the request is bounded only by the
|
|
140
|
+
* caller's fetch implementation.
|
|
141
|
+
*/
|
|
142
|
+
timeoutMs?: number;
|
|
143
|
+
/** Total attempts including the first. Defaults to the shared 5-attempt policy. */
|
|
144
|
+
maxAttempts?: number;
|
|
145
|
+
/** Caller cancellation, combined with `timeoutMs` when both are set. */
|
|
146
|
+
signal?: AbortSignal;
|
|
137
147
|
}
|
|
138
148
|
export interface DecisionObject {
|
|
139
149
|
id: string;
|
|
@@ -361,6 +371,9 @@ export declare class AgentLu {
|
|
|
361
371
|
* },
|
|
362
372
|
* });
|
|
363
373
|
* if (answers.urgent.noul > 0.8) routeToHuman();
|
|
374
|
+
*
|
|
375
|
+
* `timeoutMs` bounds each attempt (timeouts are never retried), `maxAttempts`
|
|
376
|
+
* overrides the shared 5-attempt policy, and `signal` cancels the call.
|
|
364
377
|
*/
|
|
365
378
|
readonly decisions: {
|
|
366
379
|
create: (body: DecisionOptions) => Promise<DecisionObject>;
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
* `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) {
|
|
21
|
+
return { timeoutMs: body.timeoutMs, maxAttempts: body.maxAttempts, signal: body.signal };
|
|
22
|
+
}
|
|
13
23
|
/** Question builders; sugar over the wire shape. */
|
|
14
24
|
export const noul = (instructions, criteria) => ({
|
|
15
25
|
type: "noul",
|
|
@@ -282,11 +292,14 @@ export class AgentLu {
|
|
|
282
292
|
* },
|
|
283
293
|
* });
|
|
284
294
|
* if (answers.urgent.noul > 0.8) routeToHuman();
|
|
295
|
+
*
|
|
296
|
+
* `timeoutMs` bounds each attempt (timeouts are never retried), `maxAttempts`
|
|
297
|
+
* overrides the shared 5-attempt policy, and `signal` cancels the call.
|
|
285
298
|
*/
|
|
286
299
|
decisions = {
|
|
287
|
-
create: (body) => this.request("POST", "/decisions", body),
|
|
300
|
+
create: (body) => this.request("POST", "/decisions", decisionWire(body), decisionRequestOptions(body)),
|
|
288
301
|
/** Same call, returning only the answers map. */
|
|
289
|
-
ask: async (body) => (await this.request("POST", "/decisions", body)).answers,
|
|
302
|
+
ask: async (body) => (await this.request("POST", "/decisions", decisionWire(body), decisionRequestOptions(body))).answers,
|
|
290
303
|
};
|
|
291
304
|
async agents() {
|
|
292
305
|
return this.request("GET", "/agents", undefined, { auth: false });
|
|
@@ -305,10 +318,14 @@ export class AgentLu {
|
|
|
305
318
|
// not rate limits. Retrying with backoff is almost always the right thing,
|
|
306
319
|
// so the SDK does it - honoring Retry-After when the server sends one.
|
|
307
320
|
// A 503 can also signal a config boundary (no decision model key); those
|
|
308
|
-
// are not retryable and must surface immediately.
|
|
321
|
+
// are not retryable and must surface immediately. Callers can cap attempts
|
|
322
|
+
// per request (`maxAttempts`); a timeout/caller abort is never retried.
|
|
309
323
|
const nonRetryable503 = new Set(["systemone_unconfigured"]);
|
|
324
|
+
const maxAttempts = opts?.maxAttempts !== undefined && Number.isFinite(opts.maxAttempts) && opts.maxAttempts >= 1
|
|
325
|
+
? Math.floor(opts.maxAttempts)
|
|
326
|
+
: 5;
|
|
310
327
|
let last;
|
|
311
|
-
for (let attempt = 0; attempt <
|
|
328
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
312
329
|
try {
|
|
313
330
|
const res = await this.raw(method, path, body, opts);
|
|
314
331
|
if (res.status === 204)
|
|
@@ -321,7 +338,7 @@ export class AgentLu {
|
|
|
321
338
|
(e.code === "thread_busy" ||
|
|
322
339
|
e.code === "too_many_active_runs" ||
|
|
323
340
|
(e.status === 503 && !nonRetryable503.has(e.code))) &&
|
|
324
|
-
attempt <
|
|
341
|
+
attempt < maxAttempts - 1) {
|
|
325
342
|
last = e;
|
|
326
343
|
await sleep(retryDelayMs(e, attempt));
|
|
327
344
|
continue;
|
|
@@ -340,9 +357,11 @@ export class AgentLu {
|
|
|
340
357
|
const apiKey = opts?.apiKey ?? this.apiKey;
|
|
341
358
|
if (opts?.auth !== false && apiKey)
|
|
342
359
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
360
|
+
const signal = requestSignal(opts?.signal, opts?.timeoutMs);
|
|
343
361
|
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
344
362
|
method,
|
|
345
363
|
headers,
|
|
364
|
+
...(signal ? { signal } : {}),
|
|
346
365
|
body: body === undefined ? undefined : isForm ? body : JSON.stringify(body),
|
|
347
366
|
});
|
|
348
367
|
if (!res.ok) {
|
|
@@ -442,6 +461,41 @@ function parseRetryAfter(res) {
|
|
|
442
461
|
const seconds = Number(header);
|
|
443
462
|
return Number.isFinite(seconds) ? seconds * 1000 : undefined;
|
|
444
463
|
}
|
|
464
|
+
/** AbortSignal.timeout with a bare fallback (Node 18, older browsers). */
|
|
465
|
+
function timeoutSignal(timeoutMs) {
|
|
466
|
+
if (typeof AbortSignal.timeout === "function")
|
|
467
|
+
return AbortSignal.timeout(timeoutMs);
|
|
468
|
+
const controller = new AbortController();
|
|
469
|
+
const timer = setTimeout(() => controller.abort(new Error(`Request timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
470
|
+
timer.unref?.();
|
|
471
|
+
return controller.signal;
|
|
472
|
+
}
|
|
473
|
+
/** AbortSignal.any with a bare fallback (Node 18, older browsers). */
|
|
474
|
+
function mergeSignals(signals) {
|
|
475
|
+
if (signals.length === 1)
|
|
476
|
+
return signals[0];
|
|
477
|
+
if (typeof AbortSignal.any === "function")
|
|
478
|
+
return AbortSignal.any(signals);
|
|
479
|
+
const controller = new AbortController();
|
|
480
|
+
for (const signal of signals) {
|
|
481
|
+
if (signal.aborted) {
|
|
482
|
+
controller.abort(signal.reason);
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
486
|
+
}
|
|
487
|
+
return controller.signal;
|
|
488
|
+
}
|
|
489
|
+
/** Combined deadline/cancellation signal; undefined when neither applies. */
|
|
490
|
+
function requestSignal(signal, timeoutMs) {
|
|
491
|
+
const signals = [];
|
|
492
|
+
if (signal)
|
|
493
|
+
signals.push(signal);
|
|
494
|
+
if (timeoutMs !== undefined && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
495
|
+
signals.push(timeoutSignal(timeoutMs));
|
|
496
|
+
}
|
|
497
|
+
return signals.length ? mergeSignals(signals) : undefined;
|
|
498
|
+
}
|
|
445
499
|
function retryDelayMs(err, attempt) {
|
|
446
500
|
if (err.retryAfterMs !== undefined) {
|
|
447
501
|
return Math.min(10_000, Math.max(250, err.retryAfterMs));
|
package/package.json
CHANGED