@hachej/boring-agent 0.4.26 → 0.4.27
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 +41 -0
- package/dist/actions.d.ts +38 -0
- package/dist/actions.js +158 -0
- package/dist/run-ledger.d.ts +98 -0
- package/dist/run-ledger.js +194 -0
- package/dist/run-outbox.d.ts +108 -0
- package/dist/run-outbox.js +115 -0
- package/dist/run-result.d.ts +201 -0
- package/dist/run-result.js +195 -0
- package/dist/runs.d.ts +83 -0
- package/dist/runs.js +83 -0
- package/package.json +24 -3
package/dist/runs.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ResultFailure } from './run-result.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a run is, in every home: the ways it ends, the view an operation reads, the usage it reports, and the delivery of its end
|
|
4
|
+
* to the operation that started it. The home keeps the run (a durable task on a Durable Object, a row in a person's SQLite file);
|
|
5
|
+
* these are the parts that must not differ between homes.
|
|
6
|
+
*/
|
|
7
|
+
/** How a run ends. `revoked`: the host's authority was withdrawn (a delivery or validation refused with 401/403). */
|
|
8
|
+
export declare const RUN_ENDS: readonly ["completed", "invalid_output", "cancelled", "interrupted", "revoked", "error"];
|
|
9
|
+
export type RunEnd = typeof RUN_ENDS[number];
|
|
10
|
+
export type RunStatus = 'queued' | 'running' | RunEnd;
|
|
11
|
+
/** The end code of a run that the host's spend cap refused (`error: "over_budget"`), and of a request it refused. */
|
|
12
|
+
export declare const OVER_BUDGET = "over_budget";
|
|
13
|
+
/** What a run used, summed over its model answers: tokens, cost in USD as pi-ai prices it, and the models that answered. */
|
|
14
|
+
export interface RunUsage {
|
|
15
|
+
readonly input: number;
|
|
16
|
+
readonly output: number;
|
|
17
|
+
readonly cacheRead: number;
|
|
18
|
+
readonly cacheWrite: number;
|
|
19
|
+
readonly cost: number;
|
|
20
|
+
readonly models: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/** A run by its key, as the operation that started it reads it. `usage` is null for a run that never answered. */
|
|
23
|
+
export interface RunView {
|
|
24
|
+
readonly run: string;
|
|
25
|
+
readonly status: RunStatus;
|
|
26
|
+
readonly startedAt?: string;
|
|
27
|
+
readonly output?: unknown;
|
|
28
|
+
readonly usage: RunUsage | null;
|
|
29
|
+
readonly error?: string;
|
|
30
|
+
}
|
|
31
|
+
/** What the delivery operation is called with: the run's end. */
|
|
32
|
+
export interface RunPayload {
|
|
33
|
+
readonly run: string;
|
|
34
|
+
readonly status: RunEnd;
|
|
35
|
+
readonly output?: unknown;
|
|
36
|
+
readonly usage: RunUsage | null;
|
|
37
|
+
readonly error?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Statuses a run can still be in, and ends the view carries an `error` text for. */
|
|
40
|
+
export declare const isEnd: (status: string) => status is RunEnd;
|
|
41
|
+
export declare const endsWithError: (status: string) => boolean;
|
|
42
|
+
/** True when `error` is the spend meter's refusal (`over_budget`, 429), by its `code` or its `extra.code`. */
|
|
43
|
+
export declare function isOverBudget(error: unknown): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* A run's model answers summed from its transcript messages (pi-ai's `usage` on each assistant message); null when none answered.
|
|
46
|
+
* `modelRef` names the model of a message (default: its `model`), for hosts that report `provider:model`; return undefined to skip one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function sumUsage(messages: readonly unknown[], modelRef?: (message: {
|
|
49
|
+
provider?: string;
|
|
50
|
+
model?: string;
|
|
51
|
+
}) => string | undefined): RunUsage | null;
|
|
52
|
+
export interface DeliveryOptions {
|
|
53
|
+
/** The operation that receives the end (`deliver:` of the start). */
|
|
54
|
+
readonly operation: string;
|
|
55
|
+
/** The run's key: the delivery's receipt key is `deliver:<key>`, so the host's receipt makes the delivery once. */
|
|
56
|
+
readonly key: string;
|
|
57
|
+
readonly end: Omit<RunPayload, 'run'>;
|
|
58
|
+
/** Calls the operation as the person whose run it is. Resolve = delivered; throw = refused (the next attempt may pass). */
|
|
59
|
+
readonly invoke: (operation: string, input: unknown, key: string) => Promise<unknown>;
|
|
60
|
+
/** A completed run is checked again just before it is delivered (a `phase: "delivery"` check): its failure becomes the end delivered instead. */
|
|
61
|
+
readonly recheck?: () => Promise<ResultFailure | undefined>;
|
|
62
|
+
/** Told once when the re-check fails, before the delivery, so the home can record the failed end. */
|
|
63
|
+
readonly onFailure?: (failure: ResultFailure) => Promise<void> | void;
|
|
64
|
+
/** Attempts (default 3) and the first wait between two (default 20 ms, doubling). */
|
|
65
|
+
readonly attempts?: number;
|
|
66
|
+
readonly backoffMs?: number;
|
|
67
|
+
}
|
|
68
|
+
export interface DeliveryResult {
|
|
69
|
+
/** False when no attempt was accepted: the run's end stays undelivered and is delivered again later. */
|
|
70
|
+
readonly delivered: boolean;
|
|
71
|
+
/** The end as delivered or last tried: the re-check's failure replaces a completed end. */
|
|
72
|
+
readonly end: Omit<RunPayload, 'run'>;
|
|
73
|
+
readonly failure?: ResultFailure;
|
|
74
|
+
/** The last refusal, when not delivered. */
|
|
75
|
+
readonly error?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Hand a run's end to the operation that started it, once however often it is tried: attempts are bounded, every one carries the same
|
|
79
|
+
* `deliver:<key>` receipt key, and a completed run is first checked again so a result that stopped being valid is delivered as
|
|
80
|
+
* `invalid_output` (or `revoked`), never as a success. A failed delivery is reported, not dropped: the home retries it later.
|
|
81
|
+
*/
|
|
82
|
+
export declare function deliverRunEnd(options: DeliveryOptions): Promise<DeliveryResult>;
|
|
83
|
+
//# sourceMappingURL=runs.d.ts.map
|
package/dist/runs.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run is, in every home: the ways it ends, the view an operation reads, the usage it reports, and the delivery of its end
|
|
3
|
+
* to the operation that started it. The home keeps the run (a durable task on a Durable Object, a row in a person's SQLite file);
|
|
4
|
+
* these are the parts that must not differ between homes.
|
|
5
|
+
*/
|
|
6
|
+
/** How a run ends. `revoked`: the host's authority was withdrawn (a delivery or validation refused with 401/403). */
|
|
7
|
+
export const RUN_ENDS = Object.freeze(['completed', 'invalid_output', 'cancelled', 'interrupted', 'revoked', 'error']);
|
|
8
|
+
/** The end code of a run that the host's spend cap refused (`error: "over_budget"`), and of a request it refused. */
|
|
9
|
+
export const OVER_BUDGET = 'over_budget';
|
|
10
|
+
/** Statuses a run can still be in, and ends the view carries an `error` text for. */
|
|
11
|
+
export const isEnd = (status) => RUN_ENDS.includes(status);
|
|
12
|
+
const WITH_ERROR = new Set(['error', 'interrupted', 'invalid_output', 'revoked']);
|
|
13
|
+
export const endsWithError = (status) => WITH_ERROR.has(status);
|
|
14
|
+
/** True when `error` is the spend meter's refusal (`over_budget`, 429), by its `code` or its `extra.code`. */
|
|
15
|
+
export function isOverBudget(error) {
|
|
16
|
+
const e = error;
|
|
17
|
+
return (e?.code ?? e?.extra?.code) === OVER_BUDGET;
|
|
18
|
+
}
|
|
19
|
+
const count = (v) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : 0);
|
|
20
|
+
/**
|
|
21
|
+
* A run's model answers summed from its transcript messages (pi-ai's `usage` on each assistant message); null when none answered.
|
|
22
|
+
* `modelRef` names the model of a message (default: its `model`), for hosts that report `provider:model`; return undefined to skip one.
|
|
23
|
+
*/
|
|
24
|
+
export function sumUsage(messages, modelRef = (m) => m.model) {
|
|
25
|
+
let answered = false, input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
|
|
26
|
+
const models = [];
|
|
27
|
+
for (const raw of messages) {
|
|
28
|
+
const message = raw;
|
|
29
|
+
if (message?.role !== 'assistant' || !message.usage)
|
|
30
|
+
continue;
|
|
31
|
+
const u = message.usage;
|
|
32
|
+
answered = true;
|
|
33
|
+
input += count(u.input);
|
|
34
|
+
output += count(u.output);
|
|
35
|
+
cacheRead += count(u.cacheRead);
|
|
36
|
+
cacheWrite += count(u.cacheWrite);
|
|
37
|
+
cost += count(u.cost?.total);
|
|
38
|
+
const ref = typeof message.model === 'string' ? modelRef(message) : undefined;
|
|
39
|
+
if (ref && !models.includes(ref))
|
|
40
|
+
models.push(ref);
|
|
41
|
+
}
|
|
42
|
+
return answered ? { input, output, cacheRead, cacheWrite, cost, models } : null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Hand a run's end to the operation that started it, once however often it is tried: attempts are bounded, every one carries the same
|
|
46
|
+
* `deliver:<key>` receipt key, and a completed run is first checked again so a result that stopped being valid is delivered as
|
|
47
|
+
* `invalid_output` (or `revoked`), never as a success. A failed delivery is reported, not dropped: the home retries it later.
|
|
48
|
+
*/
|
|
49
|
+
export async function deliverRunEnd(options) {
|
|
50
|
+
const { operation, key, invoke, recheck, onFailure, attempts = 3, backoffMs = 20 } = options;
|
|
51
|
+
let end = options.end, failure, last;
|
|
52
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
53
|
+
if (attempt)
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, backoffMs * 2 ** (attempt - 1)));
|
|
55
|
+
if (end.status === 'completed' && recheck) {
|
|
56
|
+
let found;
|
|
57
|
+
try {
|
|
58
|
+
found = await recheck();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Only a refusal by authority changes the end; any other throw (an outage) leaves it completed, tried again.
|
|
62
|
+
const status = error?.status;
|
|
63
|
+
last = error?.message ?? error;
|
|
64
|
+
if (status !== 401 && status !== 403)
|
|
65
|
+
continue;
|
|
66
|
+
found = { error: String(last).slice(0, 500), end: 'revoked' };
|
|
67
|
+
}
|
|
68
|
+
if (found) {
|
|
69
|
+
failure = found;
|
|
70
|
+
end = { status: found.end, usage: end.usage, error: found.error };
|
|
71
|
+
await onFailure?.(found);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await invoke(operation, { run: key, ...end }, `deliver:${key}`);
|
|
76
|
+
return { delivered: true, end, ...(failure ? { failure } : {}) };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
last = error?.message ?? error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { delivered: false, end, ...(failure ? { failure } : {}), error: `deliver ${operation}: ${String(last).slice(0, 200)}` };
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.27",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -148,6 +148,26 @@
|
|
|
148
148
|
"./room": {
|
|
149
149
|
"types": "./dist/room.d.ts",
|
|
150
150
|
"import": "./dist/room.js"
|
|
151
|
+
},
|
|
152
|
+
"./run-result": {
|
|
153
|
+
"types": "./dist/run-result.d.ts",
|
|
154
|
+
"import": "./dist/run-result.js"
|
|
155
|
+
},
|
|
156
|
+
"./runs": {
|
|
157
|
+
"types": "./dist/runs.d.ts",
|
|
158
|
+
"import": "./dist/runs.js"
|
|
159
|
+
},
|
|
160
|
+
"./run-ledger": {
|
|
161
|
+
"types": "./dist/run-ledger.d.ts",
|
|
162
|
+
"import": "./dist/run-ledger.js"
|
|
163
|
+
},
|
|
164
|
+
"./run-outbox": {
|
|
165
|
+
"types": "./dist/run-outbox.d.ts",
|
|
166
|
+
"import": "./dist/run-outbox.js"
|
|
167
|
+
},
|
|
168
|
+
"./actions": {
|
|
169
|
+
"types": "./dist/actions.d.ts",
|
|
170
|
+
"import": "./dist/actions.js"
|
|
151
171
|
}
|
|
152
172
|
},
|
|
153
173
|
"dependencies": {
|
|
@@ -160,8 +180,9 @@
|
|
|
160
180
|
"@earendil-works/pi-durable": "~1.2.0",
|
|
161
181
|
"@earendil-works/pi-ai": "~1.2.0",
|
|
162
182
|
"@earendil-works/chord": "~1.2.0",
|
|
163
|
-
"@hachej/boring-files": "^0.4.
|
|
164
|
-
"@hachej/boring-ui-kit": "^0.4.
|
|
183
|
+
"@hachej/boring-files": "^0.4.27",
|
|
184
|
+
"@hachej/boring-ui-kit": "^0.4.27",
|
|
185
|
+
"typebox": "1.3.27",
|
|
165
186
|
"xlsx": "^0.20.3"
|
|
166
187
|
},
|
|
167
188
|
"peerDependenciesMeta": {
|