@north-light/sdk 0.0.0-stage → 0.3.414
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/LICENSE +18 -0
- package/README.md +187 -2
- package/dist/client.d.ts +162 -0
- package/dist/client.js +543 -0
- package/dist/error-codes.d.ts +7 -0
- package/dist/error-codes.js +41 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.js +87 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +17 -0
- package/dist/keygen-cli.d.ts +2 -0
- package/dist/keygen-cli.js +6 -0
- package/dist/keygen-command.d.ts +6 -0
- package/dist/keygen-command.js +157 -0
- package/dist/oauth/index.d.ts +217 -0
- package/dist/oauth/index.js +535 -0
- package/dist/oauth/keygen.d.ts +18 -0
- package/dist/oauth/keygen.js +46 -0
- package/dist/oauth/refusal.d.ts +32 -0
- package/dist/oauth/refusal.js +17 -0
- package/dist/resources/activity.d.ts +22 -0
- package/dist/resources/activity.js +43 -0
- package/dist/resources/attachments.d.ts +15 -0
- package/dist/resources/attachments.js +13 -0
- package/dist/resources/bash.d.ts +9 -0
- package/dist/resources/bash.js +15 -0
- package/dist/resources/canvas/history.d.ts +11 -0
- package/dist/resources/canvas/history.js +20 -0
- package/dist/resources/canvas.d.ts +19 -0
- package/dist/resources/canvas.js +42 -0
- package/dist/resources/crons.d.ts +15 -0
- package/dist/resources/crons.js +33 -0
- package/dist/resources/custom-objects.d.ts +20 -0
- package/dist/resources/custom-objects.js +84 -0
- package/dist/resources/files.d.ts +34 -0
- package/dist/resources/files.js +34 -0
- package/dist/resources/follower.d.ts +71 -0
- package/dist/resources/follower.js +202 -0
- package/dist/resources/forward.d.ts +7 -0
- package/dist/resources/forward.js +67 -0
- package/dist/resources/grant-watcher.d.ts +57 -0
- package/dist/resources/grant-watcher.js +100 -0
- package/dist/resources/human/inbox.d.ts +14 -0
- package/dist/resources/human/inbox.js +30 -0
- package/dist/resources/human/requests.d.ts +13 -0
- package/dist/resources/human/requests.js +26 -0
- package/dist/resources/human.d.ts +8 -0
- package/dist/resources/human.js +10 -0
- package/dist/resources/identifiers.d.ts +8 -0
- package/dist/resources/identifiers.js +33 -0
- package/dist/resources/memory.d.ts +69 -0
- package/dist/resources/memory.js +30 -0
- package/dist/resources/models/config.d.ts +8 -0
- package/dist/resources/models/config.js +9 -0
- package/dist/resources/models/credentials.d.ts +10 -0
- package/dist/resources/models/credentials.js +17 -0
- package/dist/resources/models.d.ts +8 -0
- package/dist/resources/models.js +10 -0
- package/dist/resources/node-stream.d.ts +34 -0
- package/dist/resources/node-stream.js +176 -0
- package/dist/resources/nodes/jobs.d.ts +9 -0
- package/dist/resources/nodes/jobs.js +14 -0
- package/dist/resources/nodes/result.d.ts +8 -0
- package/dist/resources/nodes/result.js +11 -0
- package/dist/resources/nodes/worktree.d.ts +9 -0
- package/dist/resources/nodes/worktree.js +14 -0
- package/dist/resources/nodes.d.ts +23 -0
- package/dist/resources/nodes.js +47 -0
- package/dist/resources/providers.d.ts +51 -0
- package/dist/resources/providers.js +15 -0
- package/dist/resources/questions.d.ts +49 -0
- package/dist/resources/questions.js +21 -0
- package/dist/resources/request.d.ts +7 -0
- package/dist/resources/request.js +19 -0
- package/dist/resources/run-notice.d.ts +34 -0
- package/dist/resources/run-notice.js +67 -0
- package/dist/resources/run-reply.d.ts +79 -0
- package/dist/resources/run-reply.js +93 -0
- package/dist/resources/run-stream.d.ts +65 -0
- package/dist/resources/run-stream.js +243 -0
- package/dist/resources/run-watcher.d.ts +98 -0
- package/dist/resources/run-watcher.js +162 -0
- package/dist/resources/runs.d.ts +192 -0
- package/dist/resources/runs.js +208 -0
- package/dist/resources/shares.d.ts +40 -0
- package/dist/resources/shares.js +19 -0
- package/dist/resources/structured-requests.d.ts +14 -0
- package/dist/resources/structured-requests.js +19 -0
- package/dist/resources/uploads.d.ts +27 -0
- package/dist/resources/uploads.js +11 -0
- package/dist/schema.d.ts +7 -0
- package/dist/schema.js +10 -0
- package/dist/stores/postgres.d.ts +32 -0
- package/dist/stores/postgres.js +92 -0
- package/dist/testing/cloud.d.ts +209 -0
- package/dist/testing/cloud.js +880 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/notices.d.ts +70 -0
- package/dist/testing/notices.js +205 -0
- package/dist/testing/run.d.ts +172 -0
- package/dist/testing/run.js +429 -0
- package/dist/testing/users.d.ts +121 -0
- package/dist/testing/users.js +70 -0
- package/dist/testing/wire.d.ts +62 -0
- package/dist/testing/wire.js +148 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +57 -4
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Crouter } from '../client.js';
|
|
2
|
+
import type { RunActivity, RunObject } from './runs.js';
|
|
3
|
+
/** A run the app is still waiting on: whose runtime holds it, and its id. `pending()` returns these from the app's own rows. */
|
|
4
|
+
export interface PendingRun {
|
|
5
|
+
userId: string;
|
|
6
|
+
runId: string;
|
|
7
|
+
}
|
|
8
|
+
/** How a watched run came to rest. `gone`: the runtime answered 404 `not_found` for it (the run was deleted). */
|
|
9
|
+
export type RunEnding = 'settled' | 'idle' | 'waiting_on_user' | 'gone';
|
|
10
|
+
/** One ending `RunWatcher` hands to `onEnded`. `run` is the `runs.get` answer that came to rest, or `null` when `gone`. */
|
|
11
|
+
export type RunEnded = {
|
|
12
|
+
userId: string;
|
|
13
|
+
runId: string;
|
|
14
|
+
ended: Exclude<RunEnding, 'gone'>;
|
|
15
|
+
run: RunObject;
|
|
16
|
+
} | {
|
|
17
|
+
userId: string;
|
|
18
|
+
runId: string;
|
|
19
|
+
ended: 'gone';
|
|
20
|
+
run: null;
|
|
21
|
+
};
|
|
22
|
+
/** One live-progress reading `RunWatcher` hands to `onActivity`: a `running` run's `activity` and the `runs.get` answer it came from. */
|
|
23
|
+
export interface RunActivityEvent {
|
|
24
|
+
userId: string;
|
|
25
|
+
runId: string;
|
|
26
|
+
activity: RunActivity | null;
|
|
27
|
+
run: RunObject;
|
|
28
|
+
}
|
|
29
|
+
export interface RunWatchOptions {
|
|
30
|
+
/** Every run the app still waits on, read from its own rows (the durable record). Called at `start()`, every
|
|
31
|
+
* `sweepMs`, after each delivery, and on `nudge()`. Stop returning a run once `onEnded` has landed it. */
|
|
32
|
+
pending: () => Promise<PendingRun[]> | PendingRun[];
|
|
33
|
+
/** Land one ending. At least once per `(runId, status, latest_sequence)`: make it idempotent (guard the write on
|
|
34
|
+
* the row still waiting on that run). If it throws, the same ending is retried after 5 s, doubling to 5 min. */
|
|
35
|
+
onEnded: (event: RunEnded) => Promise<void> | void;
|
|
36
|
+
/** Told about every failure the watcher recovers from itself: a throwing `pending()` or `onEnded`, a transient
|
|
37
|
+
* runtime error, a missing (`not_connected`) or refused connection. Defaults to ignoring them. */
|
|
38
|
+
onError?: (error: unknown, run: PendingRun | null) => void;
|
|
39
|
+
/** Live progress, best effort: called with each read of a run that is still `running` whose `activity` differs from
|
|
40
|
+
* the last one handed over for that run (compared as JSON: `phase`, plus `tool.name` and `tool.summary`). Never
|
|
41
|
+
* awaited by the watcher: at most one call per run runs at a time, and a newer reading taken meanwhile replaces any
|
|
42
|
+
* still waiting, so the next call gets the newest. A call may finish after `onEnded` for that run: guard the write on
|
|
43
|
+
* the row still waiting on the run. A throw goes to `onError` and is not retried; it never delays or changes
|
|
44
|
+
* `onEnded`. When set, running runs are read with `wait: activityWait` instead of 25 s. */
|
|
45
|
+
onActivity?: (event: RunActivityEvent) => Promise<void> | void;
|
|
46
|
+
/** Seconds each read of a running run waits while `onActivity` is set: an integer 1–25, default 2. A run that comes
|
|
47
|
+
* to rest still answers at once. A followed run is read at least this often; a run waiting for a `concurrency` slot
|
|
48
|
+
* or backing off after a failure is not read. */
|
|
49
|
+
activityWait?: number;
|
|
50
|
+
/** Milliseconds between sweeps of `pending()`. Default 30 000. */
|
|
51
|
+
sweepMs?: number;
|
|
52
|
+
/** Most runs followed with an open long-poll at once. Default 50; the rest wait their turn. */
|
|
53
|
+
concurrency?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Follows an app's background runs until each comes to rest (`settled`, `idle`, `waiting_on_user`, or deleted),
|
|
57
|
+
* and hands each ending to `onEnded` at least once. Build it with `oauth.watchRuns(...)`, then `start()`.
|
|
58
|
+
*
|
|
59
|
+
* Delivery is keyed in memory on `(runId, status, latest_sequence)`: a run still in `pending()` after delivery is
|
|
60
|
+
* re-read once per sweep without `wait` and delivered again only when that key changes (it worked again, or an
|
|
61
|
+
* `idle` run was later canceled to `settled`); a sweep that reads `running` goes back to the long-poll. The app's
|
|
62
|
+
* rows are the durable record, so a restarted watcher re-derives everything from `pending()`.
|
|
63
|
+
*
|
|
64
|
+
* With `onActivity`, each running read whose `activity` (as JSON) differs from the last handed over is reported,
|
|
65
|
+
* and running runs are read with a short `wait` (`activityWait`), since the runtime's long-poll answers early only at rest.
|
|
66
|
+
*/
|
|
67
|
+
export declare class RunWatcher {
|
|
68
|
+
private readonly open;
|
|
69
|
+
private readonly options;
|
|
70
|
+
/** The `wait` of each read of a running run: 25, or `activityWait` while `onActivity` is set. */
|
|
71
|
+
private readonly runningWait;
|
|
72
|
+
/** The last key delivered per runId: `${status}:${latest_sequence}`, or `gone`. */
|
|
73
|
+
private readonly delivered;
|
|
74
|
+
/** The last `activity` handed to `onActivity` per runId, as JSON. Cleared when the run rests. */
|
|
75
|
+
private readonly activitySeen;
|
|
76
|
+
/** Runs with an `onActivity` call in flight, each with the newest reading taken meanwhile (null: none). */
|
|
77
|
+
private readonly activityCalls;
|
|
78
|
+
/** Runs that rested and were delivered: re-read only on sweeps. */
|
|
79
|
+
private readonly resting;
|
|
80
|
+
private readonly follower;
|
|
81
|
+
constructor(open: (userId: string) => Promise<Crouter>, options: RunWatchOptions);
|
|
82
|
+
private get running();
|
|
83
|
+
/** Read `pending()` and begin following; sweeps repeat every `sweepMs` until `stop()`. Resolves after the first sweep. */
|
|
84
|
+
start(): Promise<void>;
|
|
85
|
+
/** Sweep now instead of waiting for `sweepMs`, for example right after the app starts a run it will watch. */
|
|
86
|
+
nudge(): void;
|
|
87
|
+
/** Stop sweeping, abort every open long-poll, and resolve once every loop has ended. No handler starts afterwards;
|
|
88
|
+
* an `onActivity` call already running is not waited for. */
|
|
89
|
+
stop(): Promise<void>;
|
|
90
|
+
private forget;
|
|
91
|
+
/** One loop: read the run until it rests, deliver once per key, then leave it to the sweeps (`park`). */
|
|
92
|
+
private follow;
|
|
93
|
+
/** Hand a running run's activity to `onActivity` once per distinct value, without blocking the follow loop. */
|
|
94
|
+
private progress;
|
|
95
|
+
/** Run `onActivity` for one run, then for the newest reading taken meanwhile, until none waits or the watcher stops. */
|
|
96
|
+
private callActivity;
|
|
97
|
+
private deliver;
|
|
98
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { APIError, CrouterError } from '../errors.js';
|
|
2
|
+
import { Follower, troubleResult } from './follower.js';
|
|
3
|
+
const defaultActivityWait = 2;
|
|
4
|
+
const restingStatuses = new Set(['settled', 'idle', 'waiting_on_user']);
|
|
5
|
+
/**
|
|
6
|
+
* Follows an app's background runs until each comes to rest (`settled`, `idle`, `waiting_on_user`, or deleted),
|
|
7
|
+
* and hands each ending to `onEnded` at least once. Build it with `oauth.watchRuns(...)`, then `start()`.
|
|
8
|
+
*
|
|
9
|
+
* Delivery is keyed in memory on `(runId, status, latest_sequence)`: a run still in `pending()` after delivery is
|
|
10
|
+
* re-read once per sweep without `wait` and delivered again only when that key changes (it worked again, or an
|
|
11
|
+
* `idle` run was later canceled to `settled`); a sweep that reads `running` goes back to the long-poll. The app's
|
|
12
|
+
* rows are the durable record, so a restarted watcher re-derives everything from `pending()`.
|
|
13
|
+
*
|
|
14
|
+
* With `onActivity`, each running read whose `activity` (as JSON) differs from the last handed over is reported,
|
|
15
|
+
* and running runs are read with a short `wait` (`activityWait`), since the runtime's long-poll answers early only at rest.
|
|
16
|
+
*/
|
|
17
|
+
export class RunWatcher {
|
|
18
|
+
open;
|
|
19
|
+
options;
|
|
20
|
+
/** The `wait` of each read of a running run: 25, or `activityWait` while `onActivity` is set. */
|
|
21
|
+
runningWait;
|
|
22
|
+
/** The last key delivered per runId: `${status}:${latest_sequence}`, or `gone`. */
|
|
23
|
+
delivered = new Map();
|
|
24
|
+
/** The last `activity` handed to `onActivity` per runId, as JSON. Cleared when the run rests. */
|
|
25
|
+
activitySeen = new Map();
|
|
26
|
+
/** Runs with an `onActivity` call in flight, each with the newest reading taken meanwhile (null: none). */
|
|
27
|
+
activityCalls = new Map();
|
|
28
|
+
/** Runs that rested and were delivered: re-read only on sweeps. */
|
|
29
|
+
resting = new Set();
|
|
30
|
+
follower;
|
|
31
|
+
constructor(open, options) {
|
|
32
|
+
this.open = open;
|
|
33
|
+
this.options = options;
|
|
34
|
+
const activityWait = options.activityWait ?? defaultActivityWait;
|
|
35
|
+
this.follower = new Follower({
|
|
36
|
+
name: 'RunWatcher',
|
|
37
|
+
items: () => options.pending(),
|
|
38
|
+
key: (run) => run.runId,
|
|
39
|
+
turn: (run, signal) => this.follow(run, signal),
|
|
40
|
+
// A deleted run cannot come back: once `gone` is delivered it is not read again while pending() returns it.
|
|
41
|
+
admit: (runId) => this.delivered.get(runId) !== 'gone',
|
|
42
|
+
onForget: (runId) => this.forget(runId),
|
|
43
|
+
onError: options.onError,
|
|
44
|
+
sweepMs: options.sweepMs ?? 30_000,
|
|
45
|
+
concurrency: options.concurrency ?? 50,
|
|
46
|
+
});
|
|
47
|
+
if (!Number.isInteger(activityWait) || activityWait < 1 || activityWait > 25)
|
|
48
|
+
throw new CrouterError('activityWait must be an integer from 1 to 25');
|
|
49
|
+
this.runningWait = options.onActivity ? activityWait : 25;
|
|
50
|
+
}
|
|
51
|
+
get running() { return this.follower.isRunning; }
|
|
52
|
+
/** Read `pending()` and begin following; sweeps repeat every `sweepMs` until `stop()`. Resolves after the first sweep. */
|
|
53
|
+
start() { return this.follower.start(); }
|
|
54
|
+
/** Sweep now instead of waiting for `sweepMs`, for example right after the app starts a run it will watch. */
|
|
55
|
+
nudge() { this.follower.nudge(); }
|
|
56
|
+
/** Stop sweeping, abort every open long-poll, and resolve once every loop has ended. No handler starts afterwards;
|
|
57
|
+
* an `onActivity` call already running is not waited for. */
|
|
58
|
+
stop() { return this.follower.stop(); }
|
|
59
|
+
forget(runId) {
|
|
60
|
+
this.delivered.delete(runId);
|
|
61
|
+
this.resting.delete(runId);
|
|
62
|
+
this.activitySeen.delete(runId);
|
|
63
|
+
if (this.activityCalls.has(runId))
|
|
64
|
+
this.activityCalls.set(runId, null);
|
|
65
|
+
}
|
|
66
|
+
/** One loop: read the run until it rests, deliver once per key, then leave it to the sweeps (`park`). */
|
|
67
|
+
async follow(run, signal) {
|
|
68
|
+
let client;
|
|
69
|
+
try {
|
|
70
|
+
client = await this.open(run.userId);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return troubleResult(this.follower, error, run);
|
|
74
|
+
}
|
|
75
|
+
// A run delivered before is re-read once without waiting: waitRun answers a resting run at once, so a
|
|
76
|
+
// long-poll would spin. Only a run seen running (or never delivered) long-polls.
|
|
77
|
+
let plain = this.resting.has(run.runId);
|
|
78
|
+
for (;;) {
|
|
79
|
+
if (signal.aborted || !this.running)
|
|
80
|
+
return 'park';
|
|
81
|
+
let current;
|
|
82
|
+
try {
|
|
83
|
+
current = await client.runs.get(run.runId, plain ? { signal } : { signal, wait: this.runningWait });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (signal.aborted || !this.running)
|
|
87
|
+
return 'park';
|
|
88
|
+
if (error instanceof APIError && error.status === 404 && error.code === 'not_found' && error.origin === 'runtime') {
|
|
89
|
+
return this.deliver(run, 'gone', { userId: run.userId, runId: run.runId, ended: 'gone', run: null });
|
|
90
|
+
}
|
|
91
|
+
return troubleResult(this.follower, error, run);
|
|
92
|
+
}
|
|
93
|
+
if (!restingStatuses.has(current.status)) {
|
|
94
|
+
plain = false;
|
|
95
|
+
this.resting.delete(run.runId);
|
|
96
|
+
this.progress(run, current);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// At rest: a later run of the same id reports its first phase again, and no reading still waiting is handed over.
|
|
100
|
+
this.activitySeen.delete(run.runId);
|
|
101
|
+
if (this.activityCalls.has(run.runId))
|
|
102
|
+
this.activityCalls.set(run.runId, null);
|
|
103
|
+
const key = `${current.status}:${current.latest_sequence}`;
|
|
104
|
+
if (this.delivered.get(run.runId) === key) {
|
|
105
|
+
this.resting.add(run.runId);
|
|
106
|
+
return 'park';
|
|
107
|
+
}
|
|
108
|
+
return this.deliver(run, key, { userId: run.userId, runId: run.runId, ended: current.status, run: current });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Hand a running run's activity to `onActivity` once per distinct value, without blocking the follow loop. */
|
|
112
|
+
progress(run, current) {
|
|
113
|
+
if (!this.options.onActivity || !this.running)
|
|
114
|
+
return;
|
|
115
|
+
const activity = current.activity ?? null;
|
|
116
|
+
const key = JSON.stringify(activity);
|
|
117
|
+
if (this.activitySeen.get(run.runId) === key)
|
|
118
|
+
return;
|
|
119
|
+
this.activitySeen.set(run.runId, key);
|
|
120
|
+
const event = { userId: run.userId, runId: run.runId, activity, run: current };
|
|
121
|
+
// One call per run at a time: a reading taken while one runs replaces any still waiting.
|
|
122
|
+
if (this.activityCalls.has(run.runId)) {
|
|
123
|
+
this.activityCalls.set(run.runId, event);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.activityCalls.set(run.runId, null);
|
|
127
|
+
void this.callActivity(run, event);
|
|
128
|
+
}
|
|
129
|
+
/** Run `onActivity` for one run, then for the newest reading taken meanwhile, until none waits or the watcher stops. */
|
|
130
|
+
async callActivity(run, first) {
|
|
131
|
+
let event = first;
|
|
132
|
+
while (event && this.running) {
|
|
133
|
+
try {
|
|
134
|
+
await this.options.onActivity(event);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
this.follower.report(error, run);
|
|
138
|
+
}
|
|
139
|
+
event = this.activityCalls.get(run.runId) ?? null;
|
|
140
|
+
this.activityCalls.set(run.runId, null);
|
|
141
|
+
}
|
|
142
|
+
this.activityCalls.delete(run.runId);
|
|
143
|
+
}
|
|
144
|
+
async deliver(run, key, event) {
|
|
145
|
+
if (!this.running)
|
|
146
|
+
return 'park';
|
|
147
|
+
try {
|
|
148
|
+
await this.options.onEnded(event);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.follower.report(error, run);
|
|
152
|
+
return 'fail';
|
|
153
|
+
}
|
|
154
|
+
this.delivered.set(run.runId, key);
|
|
155
|
+
this.resting.add(run.runId);
|
|
156
|
+
this.follower.succeeded(run.runId);
|
|
157
|
+
// The app has likely landed it; re-read pending() so the set shrinks (and the next run gets a slot) promptly.
|
|
158
|
+
if (this.running)
|
|
159
|
+
void this.follower.sweep();
|
|
160
|
+
return 'park';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { RunStream } from './run-stream.js';
|
|
2
|
+
import { RunReply, type SentRunMessage } from './run-reply.js';
|
|
3
|
+
import type { Questions, Question, QuestionResponses } from './questions.js';
|
|
4
|
+
import type { JsonSchema, OutputSchema, RequestOptions, SchemaOutput } from '../types.js';
|
|
5
|
+
import type { RunActivityDTO, RunOutcomeDTO, RunTraceDTO, TurnErrorDTO } from '@north-light/api';
|
|
6
|
+
export interface RunStartParams {
|
|
7
|
+
prompt: string;
|
|
8
|
+
scopes?: string[];
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
/** Launch every node without network or the app's files mount. Descendants cannot disable it. */
|
|
11
|
+
isolated?: boolean;
|
|
12
|
+
/** Profile id (`profile_id` from `profiles.list()`) the run's agent uses; absent uses the app's default profile. */
|
|
13
|
+
profile?: string;
|
|
14
|
+
/** A JSON Schema, or an object with `toJSONSchema()` (a Zod v4 object), the run's result must satisfy.
|
|
15
|
+
* Sent as a JSON object; the daemon refuses one that is not an object or is over 256 KB serialized
|
|
16
|
+
* (`invalid_request`, `param: output_schema`), which is raised without a resend. The SDK does not
|
|
17
|
+
* validate results against it. */
|
|
18
|
+
output_schema?: JsonSchema | {
|
|
19
|
+
toJSONSchema(): JsonSchema;
|
|
20
|
+
};
|
|
21
|
+
/** A first message, stored with the run as a `start_turn: false` message: it always joins the first turn
|
|
22
|
+
* as context ahead of `prompt`. The result's `message` is what `runs.reply` takes to follow that turn. */
|
|
23
|
+
message?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface RunStartResult {
|
|
26
|
+
run_id: string;
|
|
27
|
+
node_id: string;
|
|
28
|
+
grantee: string;
|
|
29
|
+
grant: string;
|
|
30
|
+
rev: number;
|
|
31
|
+
scopes: string[] | null;
|
|
32
|
+
payer: 'user';
|
|
33
|
+
status: 'running';
|
|
34
|
+
created_at: string;
|
|
35
|
+
metadata: Record<string, unknown>;
|
|
36
|
+
sequence_number: number;
|
|
37
|
+
/** Present when `RunStartParams.message` was sent: pass it to `runs.reply(run.run_id, run.message)`. */
|
|
38
|
+
message?: SentRunMessage;
|
|
39
|
+
}
|
|
40
|
+
export type RunStatus = 'running' | 'waiting_on_user' | 'idle' | 'settled';
|
|
41
|
+
export interface RunObject {
|
|
42
|
+
run_id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
status: RunStatus;
|
|
45
|
+
/** Present once the run settled: `finalized`; `declined` with `details: {reason, code, retryable}`;
|
|
46
|
+
* `canceled`; `deadline_exceeded`; `runtime_error` with `details.crouter_reason`. */
|
|
47
|
+
outcome?: RunOutcomeDTO;
|
|
48
|
+
/** The validated `output_schema` result, exactly as the daemon sent it; present when `outcome.kind` is `result` and one was submitted. */
|
|
49
|
+
result?: unknown;
|
|
50
|
+
last_report?: string;
|
|
51
|
+
/** Present when the root's latest completed turn ended in error; the next turn that completes without one clears it. */
|
|
52
|
+
last_turn_error?: TurnErrorDTO;
|
|
53
|
+
pending_question_ids: string[];
|
|
54
|
+
usage: {
|
|
55
|
+
input_tokens: number;
|
|
56
|
+
output_tokens: number;
|
|
57
|
+
cache_read_tokens: number;
|
|
58
|
+
cache_write_tokens: number;
|
|
59
|
+
cost_usd: number;
|
|
60
|
+
};
|
|
61
|
+
payer: 'user';
|
|
62
|
+
scopes: string[] | null;
|
|
63
|
+
metadata: Record<string, unknown>;
|
|
64
|
+
created_at: string;
|
|
65
|
+
settled_at?: string;
|
|
66
|
+
latest_sequence: number;
|
|
67
|
+
last_activity_at: string;
|
|
68
|
+
/** What the root is doing while the run is `running`, else null: `starting`, `thinking`, `tool` (with `tool.name`
|
|
69
|
+
* and a one-line `tool.summary`), `writing`, `submitting_result` (writing or recording the `output_schema` result), or
|
|
70
|
+
* `between_turns`. Poll `runs.get` for a progress badge instead of
|
|
71
|
+
* reading `runs.trace`. */
|
|
72
|
+
activity: RunActivity | null;
|
|
73
|
+
}
|
|
74
|
+
/** `RunObject.activity` while the run is `running`. */
|
|
75
|
+
export type RunActivity = RunActivityDTO;
|
|
76
|
+
/** A profile an app may start runs with (`profiles.list()`). Pass `profile_id` as `RunStartParams.profile`. */
|
|
77
|
+
export interface AppProfile {
|
|
78
|
+
profile_id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
grantee: string;
|
|
81
|
+
default_kind: string;
|
|
82
|
+
default: boolean;
|
|
83
|
+
}
|
|
84
|
+
export interface RunDeleteResult {
|
|
85
|
+
run_id: string;
|
|
86
|
+
uploads_pending: number;
|
|
87
|
+
}
|
|
88
|
+
/** `runs.trace` in its rendered form; message `content` narrows on `type` and role fields on `role`. */
|
|
89
|
+
export type RunTrace = RunTraceDTO;
|
|
90
|
+
/** A settled `runs.parse` run: the run outcome plus `output_parsed`, typed to the schema and non-null exactly when `kind` is `result`. */
|
|
91
|
+
export type RunParsedOutcome<T> = (Extract<RunOutcomeDTO, {
|
|
92
|
+
kind: 'result';
|
|
93
|
+
}> & {
|
|
94
|
+
output_parsed: T;
|
|
95
|
+
}) | (Exclude<RunOutcomeDTO, {
|
|
96
|
+
kind: 'result';
|
|
97
|
+
}> & {
|
|
98
|
+
output_parsed: null;
|
|
99
|
+
});
|
|
100
|
+
/** `runs.parse` params: `runs.start`'s, with `output_schema` required. */
|
|
101
|
+
export type RunParseParams<TSchema> = Omit<RunStartParams, 'output_schema'> & {
|
|
102
|
+
output_schema: TSchema;
|
|
103
|
+
};
|
|
104
|
+
export interface RunParseOptions extends RequestOptions {
|
|
105
|
+
/** Answers each open question once, by id, with one response per slot. Without it `parse` rejects `waiting_on_user`. */
|
|
106
|
+
onQuestion?: (question: Question) => QuestionResponses | Promise<QuestionResponses>;
|
|
107
|
+
idempotencyKey?: string;
|
|
108
|
+
}
|
|
109
|
+
export type RunRequest = <T>(method: string, path: string, body?: unknown, options?: RequestOptions) => Promise<T>;
|
|
110
|
+
export declare class Runs {
|
|
111
|
+
private readonly request;
|
|
112
|
+
private readonly openEvents;
|
|
113
|
+
private readonly questions?;
|
|
114
|
+
constructor(request: RunRequest, openEvents: (runId: string, after: number, signal: AbortSignal) => Promise<Response>, questions?: Questions | undefined);
|
|
115
|
+
start(params: RunStartParams, options?: RequestOptions & {
|
|
116
|
+
idempotencyKey?: string;
|
|
117
|
+
}): Promise<RunStartResult>;
|
|
118
|
+
get(runId: string, options?: RequestOptions & {
|
|
119
|
+
wait?: number;
|
|
120
|
+
}): Promise<RunObject>;
|
|
121
|
+
list(filters?: {
|
|
122
|
+
status?: RunStatus;
|
|
123
|
+
after?: string;
|
|
124
|
+
limit?: number;
|
|
125
|
+
}, options?: RequestOptions): Promise<{
|
|
126
|
+
runs: RunObject[];
|
|
127
|
+
next: string | null;
|
|
128
|
+
}>;
|
|
129
|
+
/** Every run across pages, most recently active first: `for await (const run of runs.listAll())`. Fetches
|
|
130
|
+
* the next page (`limit` per page) only as you iterate; `break` stops fetching. */
|
|
131
|
+
listAll(filters?: {
|
|
132
|
+
status?: RunStatus;
|
|
133
|
+
limit?: number;
|
|
134
|
+
}, options?: RequestOptions): AsyncGenerator<RunObject, void, undefined>;
|
|
135
|
+
/** Send a message to a run. `start_turn: true` (the default) answers it in its
|
|
136
|
+
* own turn: a running run receives it when its current turn ends, and a
|
|
137
|
+
* message sent before the first turn has started queues behind that turn,
|
|
138
|
+
* because a run's `runs.start` prompt is always its first turn's input.
|
|
139
|
+
* `start_turn: false` stores it without starting a turn; it is delivered as
|
|
140
|
+
* context at the start of the next turn, ahead of that turn's input. For a
|
|
141
|
+
* new run that is the first turn only if it is stored before the run claims
|
|
142
|
+
* its first turn's messages; to guarantee it, pass `message` to `runs.start`.
|
|
143
|
+
*
|
|
144
|
+
* The send carries an `Idempotency-Key`: `idempotencyKey` when given, otherwise a UUID generated for this
|
|
145
|
+
* call and reused on every retry of it. The daemon keeps the key for 24 hours; a repeat with the same key
|
|
146
|
+
* and the same message returns the first send's result without delivering again, and the same key with a
|
|
147
|
+
* different message or `start_turn` throws `ConflictError` `idempotency_conflict`. To make a send safe
|
|
148
|
+
* across your own restarts, derive the key from your own record of the send (a batch or row id). */
|
|
149
|
+
message(runId: string, message: string, options?: RequestOptions & {
|
|
150
|
+
start_turn?: boolean;
|
|
151
|
+
idempotencyKey?: string;
|
|
152
|
+
}): Promise<SentRunMessage>;
|
|
153
|
+
interrupt(runId: string, options?: RequestOptions & {
|
|
154
|
+
children?: 'stop' | 'continue';
|
|
155
|
+
}): Promise<RunObject>;
|
|
156
|
+
cancel(runId: string, options?: RequestOptions & {
|
|
157
|
+
keepSchedules?: boolean;
|
|
158
|
+
}): Promise<RunObject>;
|
|
159
|
+
/** Delete the run, running or not: the runtime stops its turns and sandboxes itself, so there is no need to
|
|
160
|
+
* `cancel` first. Deleting a run that is already gone throws `NotFoundError` (404).
|
|
161
|
+
* `uploads_pending` counts the run's released uploads storage has not yet confirmed deleted; the runtime retries those. */
|
|
162
|
+
delete(runId: string, options?: RequestOptions): Promise<RunDeleteResult>;
|
|
163
|
+
/** Rename the caller's own run. The name is trimmed, must not be empty, and is at most 200 characters. */
|
|
164
|
+
rename(runId: string, name: string, options?: RequestOptions): Promise<RunObject>;
|
|
165
|
+
trace(runId: string, options?: RequestOptions & {
|
|
166
|
+
format?: 'entries';
|
|
167
|
+
}): Promise<RunTrace>;
|
|
168
|
+
events(runId: string, options?: {
|
|
169
|
+
after?: number;
|
|
170
|
+
signal?: AbortSignal;
|
|
171
|
+
onReconnect?: (state: 'reconnecting' | 'reconnected', error?: unknown) => void;
|
|
172
|
+
}): RunStream;
|
|
173
|
+
/** Follow the reply to one message: `sent` is what `runs.message` returned, or `runs.start`'s `message`. Reads the run's events after
|
|
174
|
+
* `sent.sequence_number`, keeps the root's, starts at the turn that delivers the message and ends after that
|
|
175
|
+
* turn's `turn.completed` (or the run's settlement). Several messages delivered in one turn share that turn's reply.
|
|
176
|
+
* An expired cursor throws `stream_gap`. */
|
|
177
|
+
reply(runId: string, sent: SentRunMessage, options?: {
|
|
178
|
+
signal?: AbortSignal;
|
|
179
|
+
}): RunReply;
|
|
180
|
+
stream(params: RunStartParams, options?: RequestOptions & {
|
|
181
|
+
idempotencyKey?: string;
|
|
182
|
+
}): RunStream;
|
|
183
|
+
/** Long-polls `runs.get` until the run is `settled`, `waiting_on_user`, or `idle`, and returns that response
|
|
184
|
+
* unchanged (`result` is not parsed). Aborting `signal` rejects with its reason and leaves the run as it is. */
|
|
185
|
+
wait(runId: string, options?: RequestOptions): Promise<RunObject>;
|
|
186
|
+
/** Start a run with `output_schema` and wait for it to settle; resolves with the outcome and `output_parsed`
|
|
187
|
+
* typed to the schema. `onQuestion` answers each open question once; without it, a question rejects with
|
|
188
|
+
* `APIError` code `waiting_on_user`. A run that stops without settling rejects `run_idle` (untouched:
|
|
189
|
+
* send `runs.message` or `runs.cancel`). Aborting `signal` rejects with its reason; the run is never canceled. */
|
|
190
|
+
parse<TSchema extends OutputSchema>(params: RunParseParams<TSchema>, options?: RunParseOptions): Promise<RunParsedOutcome<SchemaOutput<TSchema>>>;
|
|
191
|
+
private parseRun;
|
|
192
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { APIError } from '../errors.js';
|
|
2
|
+
import { RunStream } from './run-stream.js';
|
|
3
|
+
import { RunReply } from './run-reply.js';
|
|
4
|
+
import { resolveJsonSchema } from '../schema.js';
|
|
5
|
+
import { heldReadOptions } from './request.js';
|
|
6
|
+
export class Runs {
|
|
7
|
+
request;
|
|
8
|
+
openEvents;
|
|
9
|
+
questions;
|
|
10
|
+
constructor(request, openEvents, questions) {
|
|
11
|
+
this.request = request;
|
|
12
|
+
this.openEvents = openEvents;
|
|
13
|
+
this.questions = questions;
|
|
14
|
+
}
|
|
15
|
+
async start(params, options = {}) {
|
|
16
|
+
const { idempotencyKey = crypto.randomUUID(), maxRetries = 2, ...rest } = options;
|
|
17
|
+
const body = params.output_schema === undefined ? params : { ...params, output_schema: resolveJsonSchema(params.output_schema) };
|
|
18
|
+
for (let attempt = 0;; attempt++) {
|
|
19
|
+
try {
|
|
20
|
+
return await this.request('POST', '/v1/runs', body, { ...rest, maxRetries: 0, headers: { ...rest.headers, 'Idempotency-Key': idempotencyKey } });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (attempt >= maxRetries || !(error instanceof APIError) || !retryableSend(error))
|
|
24
|
+
throw error;
|
|
25
|
+
await pause(error.retryAfterS ? error.retryAfterS * 1_000 : 500 * 2 ** attempt, rest.signal);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
get(runId, options = {}) {
|
|
30
|
+
const { wait, ...rest } = options;
|
|
31
|
+
return this.request('GET', `${runPath(runId)}${query({ wait })}`, undefined, heldReadOptions(wait, rest));
|
|
32
|
+
}
|
|
33
|
+
list(filters = {}, options) {
|
|
34
|
+
return this.request('GET', `/v1/runs${query(filters)}`, undefined, options);
|
|
35
|
+
}
|
|
36
|
+
/** Every run across pages, most recently active first: `for await (const run of runs.listAll())`. Fetches
|
|
37
|
+
* the next page (`limit` per page) only as you iterate; `break` stops fetching. */
|
|
38
|
+
async *listAll(filters = {}, options) {
|
|
39
|
+
for (let after;;) {
|
|
40
|
+
const page = await this.list({ ...filters, ...(after === undefined ? {} : { after }) }, options);
|
|
41
|
+
yield* page.runs;
|
|
42
|
+
if (!page.next)
|
|
43
|
+
return;
|
|
44
|
+
after = page.next;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Send a message to a run. `start_turn: true` (the default) answers it in its
|
|
48
|
+
* own turn: a running run receives it when its current turn ends, and a
|
|
49
|
+
* message sent before the first turn has started queues behind that turn,
|
|
50
|
+
* because a run's `runs.start` prompt is always its first turn's input.
|
|
51
|
+
* `start_turn: false` stores it without starting a turn; it is delivered as
|
|
52
|
+
* context at the start of the next turn, ahead of that turn's input. For a
|
|
53
|
+
* new run that is the first turn only if it is stored before the run claims
|
|
54
|
+
* its first turn's messages; to guarantee it, pass `message` to `runs.start`.
|
|
55
|
+
*
|
|
56
|
+
* The send carries an `Idempotency-Key`: `idempotencyKey` when given, otherwise a UUID generated for this
|
|
57
|
+
* call and reused on every retry of it. The daemon keeps the key for 24 hours; a repeat with the same key
|
|
58
|
+
* and the same message returns the first send's result without delivering again, and the same key with a
|
|
59
|
+
* different message or `start_turn` throws `ConflictError` `idempotency_conflict`. To make a send safe
|
|
60
|
+
* across your own restarts, derive the key from your own record of the send (a batch or row id). */
|
|
61
|
+
async message(runId, message, options = {}) {
|
|
62
|
+
const { idempotencyKey = crypto.randomUUID(), maxRetries = 2, start_turn, ...rest } = options;
|
|
63
|
+
const body = { message, ...(start_turn === undefined ? {} : { start_turn }) };
|
|
64
|
+
// One key per logical send, reused on every retry: the daemon answers a repeat with the first send's
|
|
65
|
+
// result, so a retry after a lost response cannot deliver the message twice.
|
|
66
|
+
for (let attempt = 0;; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
return await this.request('POST', `${runPath(runId)}/messages`, body, { ...rest, maxRetries: 0, headers: { ...rest.headers, 'Idempotency-Key': idempotencyKey } });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (attempt >= maxRetries || !(error instanceof APIError) || !retryableSend(error))
|
|
72
|
+
throw error;
|
|
73
|
+
await pause(error.retryAfterS ? error.retryAfterS * 1_000 : 500 * 2 ** attempt, rest.signal);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
interrupt(runId, options = {}) {
|
|
78
|
+
const { children, ...rest } = options;
|
|
79
|
+
return this.request('POST', `${runPath(runId)}/interrupt`, children === undefined ? {} : { children }, rest);
|
|
80
|
+
}
|
|
81
|
+
cancel(runId, options = {}) {
|
|
82
|
+
const { keepSchedules, ...rest } = options;
|
|
83
|
+
return this.request('POST', `${runPath(runId)}/cancel`, keepSchedules === undefined ? {} : { keep_schedules: keepSchedules }, rest);
|
|
84
|
+
}
|
|
85
|
+
/** Delete the run, running or not: the runtime stops its turns and sandboxes itself, so there is no need to
|
|
86
|
+
* `cancel` first. Deleting a run that is already gone throws `NotFoundError` (404).
|
|
87
|
+
* `uploads_pending` counts the run's released uploads storage has not yet confirmed deleted; the runtime retries those. */
|
|
88
|
+
delete(runId, options) {
|
|
89
|
+
return this.request('DELETE', runPath(runId), undefined, options);
|
|
90
|
+
}
|
|
91
|
+
/** Rename the caller's own run. The name is trimmed, must not be empty, and is at most 200 characters. */
|
|
92
|
+
rename(runId, name, options) {
|
|
93
|
+
return this.request('POST', `${runPath(runId)}/rename`, { name }, options);
|
|
94
|
+
}
|
|
95
|
+
trace(runId, options = {}) {
|
|
96
|
+
const { format, ...rest } = options;
|
|
97
|
+
return this.request('GET', `${runPath(runId)}/trace${query({ format })}`, undefined, rest);
|
|
98
|
+
}
|
|
99
|
+
events(runId, options = {}) {
|
|
100
|
+
return new RunStream((after, signal) => this.openEvents(runId, after, signal), options.after, options.signal, options.onReconnect);
|
|
101
|
+
}
|
|
102
|
+
/** Follow the reply to one message: `sent` is what `runs.message` returned, or `runs.start`'s `message`. Reads the run's events after
|
|
103
|
+
* `sent.sequence_number`, keeps the root's, starts at the turn that delivers the message and ends after that
|
|
104
|
+
* turn's `turn.completed` (or the run's settlement). Several messages delivered in one turn share that turn's reply.
|
|
105
|
+
* An expired cursor throws `stream_gap`. */
|
|
106
|
+
reply(runId, sent, options = {}) {
|
|
107
|
+
if (sent.run_id !== runId)
|
|
108
|
+
throw new TypeError('sent belongs to a different run');
|
|
109
|
+
return new RunReply((after) => this.events(runId, { after, signal: options.signal }), sent);
|
|
110
|
+
}
|
|
111
|
+
stream(params, options = {}) {
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
options.signal?.addEventListener('abort', () => controller.abort(), { once: true });
|
|
114
|
+
const started = this.start(params, options);
|
|
115
|
+
return new RunStream(async (after, signal) => {
|
|
116
|
+
const run = await started;
|
|
117
|
+
return this.openEvents(run.run_id, after, signal);
|
|
118
|
+
}, 0, controller.signal);
|
|
119
|
+
}
|
|
120
|
+
/** Long-polls `runs.get` until the run is `settled`, `waiting_on_user`, or `idle`, and returns that response
|
|
121
|
+
* unchanged (`result` is not parsed). Aborting `signal` rejects with its reason and leaves the run as it is. */
|
|
122
|
+
async wait(runId, options = {}) {
|
|
123
|
+
return abortable(options.signal, async () => {
|
|
124
|
+
for (;;) {
|
|
125
|
+
const result = await this.get(runId, { ...options, wait: 25 });
|
|
126
|
+
if (result.status === 'settled' || result.status === 'waiting_on_user' || result.status === 'idle')
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Start a run with `output_schema` and wait for it to settle; resolves with the outcome and `output_parsed`
|
|
132
|
+
* typed to the schema. `onQuestion` answers each open question once; without it, a question rejects with
|
|
133
|
+
* `APIError` code `waiting_on_user`. A run that stops without settling rejects `run_idle` (untouched:
|
|
134
|
+
* send `runs.message` or `runs.cancel`). Aborting `signal` rejects with its reason; the run is never canceled. */
|
|
135
|
+
async parse(params, options = {}) {
|
|
136
|
+
const { onQuestion, idempotencyKey, ...rest } = options;
|
|
137
|
+
return abortable(rest.signal, () => this.parseRun(params, onQuestion, idempotencyKey, rest));
|
|
138
|
+
}
|
|
139
|
+
async parseRun(params, onQuestion, idempotencyKey, rest) {
|
|
140
|
+
const run = await this.start(params, { ...rest, ...(idempotencyKey === undefined ? {} : { idempotencyKey }) });
|
|
141
|
+
const answered = new Set();
|
|
142
|
+
for (;;) {
|
|
143
|
+
const current = await this.wait(run.run_id, rest);
|
|
144
|
+
if (current.status === 'settled' && current.outcome) {
|
|
145
|
+
const outcome = current.outcome;
|
|
146
|
+
if (outcome.kind === 'result')
|
|
147
|
+
return { ...outcome, output_parsed: current.result };
|
|
148
|
+
return { ...outcome, output_parsed: null };
|
|
149
|
+
}
|
|
150
|
+
if (current.status === 'idle') {
|
|
151
|
+
throw new APIError(0, 'run_idle', 'the run stopped without settling; send runs.message or runs.cancel', undefined, undefined, { retryable: false, run_id: run.run_id });
|
|
152
|
+
}
|
|
153
|
+
const open = current.pending_question_ids.filter((id) => !answered.has(id));
|
|
154
|
+
if (!onQuestion) {
|
|
155
|
+
const questionId = open[0] ?? current.pending_question_ids[0];
|
|
156
|
+
throw new APIError(0, 'waiting_on_user', 'the run is waiting on a question and no onQuestion was given', undefined, undefined, { retryable: false, run_id: run.run_id, ...(questionId === undefined ? {} : { question_id: questionId }) });
|
|
157
|
+
}
|
|
158
|
+
if (!this.questions)
|
|
159
|
+
throw new TypeError('runs.parse needs the client questions resource to answer a question');
|
|
160
|
+
for (const id of open) {
|
|
161
|
+
const question = await this.questions.get(id, rest);
|
|
162
|
+
const responses = await onQuestion(question);
|
|
163
|
+
answered.add(id);
|
|
164
|
+
await this.questions.answer(id, responses, rest);
|
|
165
|
+
}
|
|
166
|
+
// Every listed question already answered: the run has not moved on yet, so poll again after a pause.
|
|
167
|
+
if (open.length === 0)
|
|
168
|
+
await pause(500, rest.signal);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** A keyed send is safe to repeat after a refusal the daemon marked retryable or a transport failure. */
|
|
173
|
+
function retryableSend(error) {
|
|
174
|
+
return error.retryable === true || error.code === 'transport_error' || error.code === 'daemon_request_interrupted' || error.code === 'connection_error' || error.code === 'request_timeout';
|
|
175
|
+
}
|
|
176
|
+
function runPath(id) {
|
|
177
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id))
|
|
178
|
+
throw new TypeError('Invalid run id');
|
|
179
|
+
return `/v1/runs/${encodeURIComponent(id)}`;
|
|
180
|
+
}
|
|
181
|
+
function query(fields) {
|
|
182
|
+
const params = new URLSearchParams();
|
|
183
|
+
for (const [key, value] of Object.entries(fields))
|
|
184
|
+
if (value !== undefined)
|
|
185
|
+
params.set(key, String(value));
|
|
186
|
+
return params.size ? `?${params}` : '';
|
|
187
|
+
}
|
|
188
|
+
/** SDK-RUN-27: an aborted helper rejects with the signal's own reason, whatever the in-flight request raised. */
|
|
189
|
+
async function abortable(signal, run) {
|
|
190
|
+
if (signal?.aborted)
|
|
191
|
+
throw signal.reason;
|
|
192
|
+
try {
|
|
193
|
+
return await run();
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (signal?.aborted)
|
|
197
|
+
throw signal.reason;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function pause(ms, signal) {
|
|
202
|
+
if (signal?.aborted)
|
|
203
|
+
throw signal.reason;
|
|
204
|
+
await new Promise((resolve, reject) => {
|
|
205
|
+
const timer = setTimeout(resolve, ms);
|
|
206
|
+
signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason); }, { once: true });
|
|
207
|
+
});
|
|
208
|
+
}
|