@absolutejs/agent-runtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/budget.d.ts +6 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +639 -0
- package/dist/manifest.d.ts +1 -0
- package/dist/manifest.js +20 -0
- package/dist/manifest.json +16 -0
- package/dist/memory.d.ts +2 -0
- package/dist/postgres.d.ts +15 -0
- package/dist/runtime.d.ts +11 -0
- package/dist/types.d.ts +183 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AbsoluteJS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @absolutejs/agent-runtime
|
|
2
|
+
|
|
3
|
+
Durable, provider-neutral orchestration for AI agents. This package owns runs,
|
|
4
|
+
steps, leases, budgets, checkpoints, timers, cancellation, effects, and handoffs.
|
|
5
|
+
It does not choose a model, database driver, queue, or web framework.
|
|
6
|
+
|
|
7
|
+
Every run pins the exact signed discovery descriptor id, version, and digest that
|
|
8
|
+
was selected. A remote agent therefore cannot silently replace its advertised
|
|
9
|
+
capabilities during a workflow or handoff.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const runtime = createAgentRuntime({
|
|
13
|
+
store: createPostgresAgentRuntimeStore({ client: sql }),
|
|
14
|
+
driver: myModelAdapter,
|
|
15
|
+
effects: executionAdapter,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
await runtime.start({
|
|
19
|
+
actor: { tenantId, userId, agentId, delegationId },
|
|
20
|
+
agent: selectedDiscoveryIdentity,
|
|
21
|
+
goal: "Rebook the delayed flight",
|
|
22
|
+
input,
|
|
23
|
+
budget: { actions: 8, costMicros: 50_000, spendMinor: 30_000 },
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Workers claim due runs with `FOR UPDATE SKIP LOCKED`. Every append checks the
|
|
28
|
+
lease, optimistic version, sequence, idempotency key, and budget inside one
|
|
29
|
+
transaction. Effects are recorded as requested before execution and require a
|
|
30
|
+
stable idempotency key; use `@absolutejs/execution` as the executor for crash-safe
|
|
31
|
+
external effects and reconciliation.
|
|
32
|
+
|
|
33
|
+
PostgreSQL is the durable default. Apply `agentRuntimePostgresSchemaSql()` through
|
|
34
|
+
your migrations. Redis is not a run store or work queue.
|
|
35
|
+
|
|
36
|
+
Memory stores are development and test defaults only.
|
package/dist/budget.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentBudget, AgentBudgetUsage } from "./types";
|
|
2
|
+
export declare const zeroUsage: () => AgentBudgetUsage;
|
|
3
|
+
export declare const normalizeUsage: (value?: AgentBudget) => AgentBudgetUsage;
|
|
4
|
+
export declare const addUsage: (left: AgentBudgetUsage, right: AgentBudgetUsage) => AgentBudgetUsage;
|
|
5
|
+
export declare const remainingBudget: (budget: AgentBudget, usage: AgentBudgetUsage) => AgentBudgetUsage;
|
|
6
|
+
export declare const budgetExceeded: (budget: AgentBudget, usage: AgentBudgetUsage, requested: AgentBudgetUsage) => keyof AgentBudget | undefined;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/budget.ts
|
|
3
|
+
var zeroUsage = () => ({
|
|
4
|
+
actions: 0,
|
|
5
|
+
costMicros: 0,
|
|
6
|
+
inputTokens: 0,
|
|
7
|
+
outputTokens: 0,
|
|
8
|
+
spendMinor: 0,
|
|
9
|
+
wallTimeMs: 0
|
|
10
|
+
});
|
|
11
|
+
var normalizeUsage = (value = {}) => {
|
|
12
|
+
const result = { ...zeroUsage(), ...value };
|
|
13
|
+
for (const [name, amount] of Object.entries(result)) {
|
|
14
|
+
if (!Number.isSafeInteger(amount) || amount < 0)
|
|
15
|
+
throw new Error(`Invalid budget usage: ${name}`);
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
var addUsage = (left, right) => ({
|
|
20
|
+
actions: left.actions + right.actions,
|
|
21
|
+
costMicros: left.costMicros + right.costMicros,
|
|
22
|
+
inputTokens: left.inputTokens + right.inputTokens,
|
|
23
|
+
outputTokens: left.outputTokens + right.outputTokens,
|
|
24
|
+
spendMinor: left.spendMinor + right.spendMinor,
|
|
25
|
+
wallTimeMs: left.wallTimeMs + right.wallTimeMs
|
|
26
|
+
});
|
|
27
|
+
var remainingBudget = (budget, usage) => ({
|
|
28
|
+
actions: Math.max(0, (budget.actions ?? Number.MAX_SAFE_INTEGER) - usage.actions),
|
|
29
|
+
costMicros: Math.max(0, (budget.costMicros ?? Number.MAX_SAFE_INTEGER) - usage.costMicros),
|
|
30
|
+
inputTokens: Math.max(0, (budget.inputTokens ?? Number.MAX_SAFE_INTEGER) - usage.inputTokens),
|
|
31
|
+
outputTokens: Math.max(0, (budget.outputTokens ?? Number.MAX_SAFE_INTEGER) - usage.outputTokens),
|
|
32
|
+
spendMinor: Math.max(0, (budget.spendMinor ?? Number.MAX_SAFE_INTEGER) - usage.spendMinor),
|
|
33
|
+
wallTimeMs: Math.max(0, (budget.wallTimeMs ?? Number.MAX_SAFE_INTEGER) - usage.wallTimeMs)
|
|
34
|
+
});
|
|
35
|
+
var budgetExceeded = (budget, usage, requested) => {
|
|
36
|
+
const next = addUsage(usage, requested);
|
|
37
|
+
return Object.keys(next).find((name) => budget[name] !== undefined && next[name] > (budget[name] ?? 0));
|
|
38
|
+
};
|
|
39
|
+
// src/memory.ts
|
|
40
|
+
var clone = (value) => structuredClone(value);
|
|
41
|
+
var createMemoryAgentRuntimeStore = () => {
|
|
42
|
+
const runs = new Map;
|
|
43
|
+
const steps = new Map;
|
|
44
|
+
const idempotency = new Set;
|
|
45
|
+
return {
|
|
46
|
+
createRun: async (run) => {
|
|
47
|
+
if (runs.has(run.id))
|
|
48
|
+
throw new Error("Run already exists");
|
|
49
|
+
runs.set(run.id, clone(run));
|
|
50
|
+
steps.set(run.id, []);
|
|
51
|
+
},
|
|
52
|
+
getRun: async (id) => clone(runs.get(id)),
|
|
53
|
+
listSteps: async (runId) => clone(steps.get(runId) ?? []),
|
|
54
|
+
claimDue: async ({ workerId, now, leaseExpiresAt }) => {
|
|
55
|
+
const timestamp = Date.parse(now);
|
|
56
|
+
const due = [...runs.values()].filter((run) => (run.status === "queued" || run.status === "waiting" && Date.parse(run.wakeAt ?? "") <= timestamp || run.status === "running" && Date.parse(run.leaseExpiresAt ?? "") <= timestamp) && run.cancelRequestedAt === undefined).sort((left, right) => left.createdAt.localeCompare(right.createdAt))[0];
|
|
57
|
+
if (!due)
|
|
58
|
+
return;
|
|
59
|
+
const next = {
|
|
60
|
+
...due,
|
|
61
|
+
status: "running",
|
|
62
|
+
leaseOwner: workerId,
|
|
63
|
+
leaseExpiresAt,
|
|
64
|
+
wakeAt: undefined,
|
|
65
|
+
version: due.version + 1,
|
|
66
|
+
updatedAt: now
|
|
67
|
+
};
|
|
68
|
+
runs.set(next.id, next);
|
|
69
|
+
return clone(next);
|
|
70
|
+
},
|
|
71
|
+
heartbeat: async ({
|
|
72
|
+
runId,
|
|
73
|
+
workerId,
|
|
74
|
+
expectedVersion,
|
|
75
|
+
leaseExpiresAt,
|
|
76
|
+
now
|
|
77
|
+
}) => {
|
|
78
|
+
const run = runs.get(runId);
|
|
79
|
+
if (!run || run.version !== expectedVersion || run.leaseOwner !== workerId || run.status !== "running")
|
|
80
|
+
return;
|
|
81
|
+
const next = {
|
|
82
|
+
...run,
|
|
83
|
+
leaseExpiresAt,
|
|
84
|
+
version: run.version + 1,
|
|
85
|
+
updatedAt: now
|
|
86
|
+
};
|
|
87
|
+
runs.set(runId, next);
|
|
88
|
+
return clone(next);
|
|
89
|
+
},
|
|
90
|
+
appendStep: async ({ runId, workerId, expectedVersion, step, now }) => {
|
|
91
|
+
const run = runs.get(runId);
|
|
92
|
+
if (!run || run.version !== expectedVersion || run.leaseOwner !== workerId || run.status !== "running")
|
|
93
|
+
return;
|
|
94
|
+
const key = step.idempotencyKey ? `${runId}:${step.idempotencyKey}:${step.kind}` : undefined;
|
|
95
|
+
if (key && idempotency.has(key))
|
|
96
|
+
return clone(run);
|
|
97
|
+
const exceeded = budgetExceeded(run.budget, run.usage, step.usage);
|
|
98
|
+
if (exceeded)
|
|
99
|
+
throw new Error(`Agent budget exceeded: ${exceeded}`);
|
|
100
|
+
const list = steps.get(runId) ?? [];
|
|
101
|
+
if (step.sequence !== list.length + 1)
|
|
102
|
+
return;
|
|
103
|
+
list.push(clone(step));
|
|
104
|
+
if (key)
|
|
105
|
+
idempotency.add(key);
|
|
106
|
+
const next = {
|
|
107
|
+
...run,
|
|
108
|
+
usage: addUsage(run.usage, step.usage),
|
|
109
|
+
version: run.version + 1,
|
|
110
|
+
updatedAt: now
|
|
111
|
+
};
|
|
112
|
+
runs.set(runId, next);
|
|
113
|
+
return clone(next);
|
|
114
|
+
},
|
|
115
|
+
transition: async ({
|
|
116
|
+
runId,
|
|
117
|
+
workerId,
|
|
118
|
+
expectedVersion,
|
|
119
|
+
status,
|
|
120
|
+
now,
|
|
121
|
+
output,
|
|
122
|
+
checkpoint,
|
|
123
|
+
wakeAt,
|
|
124
|
+
error
|
|
125
|
+
}) => {
|
|
126
|
+
const run = runs.get(runId);
|
|
127
|
+
if (!run || run.version !== expectedVersion || workerId && run.leaseOwner !== workerId)
|
|
128
|
+
return;
|
|
129
|
+
const next = {
|
|
130
|
+
...run,
|
|
131
|
+
status,
|
|
132
|
+
version: run.version + 1,
|
|
133
|
+
updatedAt: now,
|
|
134
|
+
...output === undefined ? {} : { output },
|
|
135
|
+
...checkpoint === undefined ? {} : { checkpoint },
|
|
136
|
+
...wakeAt === undefined ? {} : { wakeAt },
|
|
137
|
+
...error === undefined ? {} : { error },
|
|
138
|
+
...status === "running" ? {} : { leaseOwner: undefined, leaseExpiresAt: undefined }
|
|
139
|
+
};
|
|
140
|
+
runs.set(runId, next);
|
|
141
|
+
return clone(next);
|
|
142
|
+
},
|
|
143
|
+
requestCancel: async ({ runId, now }) => {
|
|
144
|
+
const run = runs.get(runId);
|
|
145
|
+
if (!run)
|
|
146
|
+
return;
|
|
147
|
+
const terminal = [
|
|
148
|
+
"completed",
|
|
149
|
+
"failed",
|
|
150
|
+
"cancelled",
|
|
151
|
+
"handed_off"
|
|
152
|
+
].includes(run.status);
|
|
153
|
+
if (terminal)
|
|
154
|
+
return clone(run);
|
|
155
|
+
const canCancelImmediately = run.status !== "running";
|
|
156
|
+
const next = {
|
|
157
|
+
...run,
|
|
158
|
+
status: canCancelImmediately ? "cancelled" : run.status,
|
|
159
|
+
cancelRequestedAt: now,
|
|
160
|
+
version: run.version + 1,
|
|
161
|
+
updatedAt: now,
|
|
162
|
+
...canCancelImmediately ? {
|
|
163
|
+
leaseOwner: undefined,
|
|
164
|
+
leaseExpiresAt: undefined,
|
|
165
|
+
wakeAt: undefined
|
|
166
|
+
} : {}
|
|
167
|
+
};
|
|
168
|
+
runs.set(runId, next);
|
|
169
|
+
return clone(next);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
// src/runtime.ts
|
|
174
|
+
var makeId = () => crypto.randomUUID();
|
|
175
|
+
var createAgentRuntime = ({
|
|
176
|
+
store,
|
|
177
|
+
driver,
|
|
178
|
+
effects,
|
|
179
|
+
now = Date.now,
|
|
180
|
+
id = makeId,
|
|
181
|
+
leaseMs = 30000,
|
|
182
|
+
maxStepsPerClaim = 25,
|
|
183
|
+
onEvent
|
|
184
|
+
}) => {
|
|
185
|
+
const emit = async (event) => {
|
|
186
|
+
await onEvent?.(event);
|
|
187
|
+
};
|
|
188
|
+
const transition = async (run, workerId, change) => {
|
|
189
|
+
const updated = await store.transition({
|
|
190
|
+
runId: run.id,
|
|
191
|
+
workerId,
|
|
192
|
+
expectedVersion: run.version,
|
|
193
|
+
now: new Date(now()).toISOString(),
|
|
194
|
+
...change
|
|
195
|
+
});
|
|
196
|
+
if (!updated)
|
|
197
|
+
throw new Error("Agent run lease or version was lost");
|
|
198
|
+
await emit({ type: "run.transitioned", run: updated });
|
|
199
|
+
return updated;
|
|
200
|
+
};
|
|
201
|
+
const append = async (run, workerId, value) => {
|
|
202
|
+
const steps = await store.listSteps(run.id);
|
|
203
|
+
const step = {
|
|
204
|
+
...value,
|
|
205
|
+
id: id(),
|
|
206
|
+
runId: run.id,
|
|
207
|
+
sequence: steps.length + 1,
|
|
208
|
+
createdAt: new Date(now()).toISOString(),
|
|
209
|
+
usage: normalizeUsage(value.usage)
|
|
210
|
+
};
|
|
211
|
+
const updated = await store.appendStep({
|
|
212
|
+
runId: run.id,
|
|
213
|
+
workerId,
|
|
214
|
+
expectedVersion: run.version,
|
|
215
|
+
step,
|
|
216
|
+
now: step.createdAt
|
|
217
|
+
});
|
|
218
|
+
if (!updated)
|
|
219
|
+
throw new Error("Agent run lease, sequence, or version was lost");
|
|
220
|
+
await emit({ type: "step.appended", run: updated, step });
|
|
221
|
+
return { run: updated, step };
|
|
222
|
+
};
|
|
223
|
+
const handle = async (run, workerId, result) => {
|
|
224
|
+
if (result.type === "continue")
|
|
225
|
+
return (await append(run, workerId, {
|
|
226
|
+
kind: result.kind ?? "observation",
|
|
227
|
+
name: result.name,
|
|
228
|
+
input: result.input,
|
|
229
|
+
output: result.output,
|
|
230
|
+
usage: result.usage
|
|
231
|
+
})).run;
|
|
232
|
+
if (result.type === "effect") {
|
|
233
|
+
const requested = await append(run, workerId, {
|
|
234
|
+
kind: "effect.requested",
|
|
235
|
+
name: result.name,
|
|
236
|
+
input: result.input,
|
|
237
|
+
idempotencyKey: result.idempotencyKey,
|
|
238
|
+
usage: { ...result.usage, actions: (result.usage?.actions ?? 0) + 1 }
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
const output = await effects.execute({
|
|
242
|
+
run: requested.run,
|
|
243
|
+
step: requested.step,
|
|
244
|
+
name: result.name,
|
|
245
|
+
payload: result.input,
|
|
246
|
+
idempotencyKey: result.idempotencyKey
|
|
247
|
+
});
|
|
248
|
+
return (await append(requested.run, workerId, {
|
|
249
|
+
kind: "effect.completed",
|
|
250
|
+
name: result.name,
|
|
251
|
+
output,
|
|
252
|
+
idempotencyKey: result.idempotencyKey
|
|
253
|
+
})).run;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const failed2 = await append(requested.run, workerId, {
|
|
256
|
+
kind: "effect.failed",
|
|
257
|
+
name: result.name,
|
|
258
|
+
output: {
|
|
259
|
+
message: error instanceof Error ? error.message : "Effect failed"
|
|
260
|
+
},
|
|
261
|
+
idempotencyKey: result.idempotencyKey
|
|
262
|
+
});
|
|
263
|
+
return transition(failed2.run, workerId, {
|
|
264
|
+
status: "failed",
|
|
265
|
+
error: {
|
|
266
|
+
code: "effect_failed",
|
|
267
|
+
message: error instanceof Error ? error.message : "Effect failed",
|
|
268
|
+
retryable: true
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (result.type === "checkpoint") {
|
|
274
|
+
const checkpointed = await append(run, workerId, {
|
|
275
|
+
kind: "checkpoint",
|
|
276
|
+
output: { reason: result.reason, checkpoint: result.checkpoint }
|
|
277
|
+
});
|
|
278
|
+
return transition(checkpointed.run, workerId, {
|
|
279
|
+
status: "suspended",
|
|
280
|
+
checkpoint: result.checkpoint
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (result.type === "wait") {
|
|
284
|
+
if (!Number.isFinite(Date.parse(result.until)))
|
|
285
|
+
throw new Error("Invalid wait timestamp");
|
|
286
|
+
const waiting = await append(run, workerId, {
|
|
287
|
+
kind: "wait",
|
|
288
|
+
output: { reason: result.reason, until: result.until }
|
|
289
|
+
});
|
|
290
|
+
return transition(waiting.run, workerId, {
|
|
291
|
+
status: "waiting",
|
|
292
|
+
wakeAt: result.until,
|
|
293
|
+
checkpoint: result.checkpoint
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (result.type === "handoff") {
|
|
297
|
+
const handed = await append(run, workerId, {
|
|
298
|
+
kind: "handoff",
|
|
299
|
+
output: {
|
|
300
|
+
target: result.target,
|
|
301
|
+
input: result.input,
|
|
302
|
+
reason: result.reason
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
return transition(handed.run, workerId, {
|
|
306
|
+
status: "handed_off",
|
|
307
|
+
output: {
|
|
308
|
+
target: result.target,
|
|
309
|
+
input: result.input,
|
|
310
|
+
reason: result.reason
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (result.type === "complete") {
|
|
315
|
+
const completed = await append(run, workerId, {
|
|
316
|
+
kind: "completed",
|
|
317
|
+
output: result.output
|
|
318
|
+
});
|
|
319
|
+
return transition(completed.run, workerId, {
|
|
320
|
+
status: "completed",
|
|
321
|
+
output: result.output
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const failed = await append(run, workerId, {
|
|
325
|
+
kind: "failed",
|
|
326
|
+
output: { code: result.code, message: result.message }
|
|
327
|
+
});
|
|
328
|
+
return transition(failed.run, workerId, {
|
|
329
|
+
status: "failed",
|
|
330
|
+
error: {
|
|
331
|
+
code: result.code,
|
|
332
|
+
message: result.message,
|
|
333
|
+
retryable: result.retryable ?? false
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
};
|
|
337
|
+
const resumePendingEffect = async (run, workerId, steps) => {
|
|
338
|
+
const requested = steps.at(-1);
|
|
339
|
+
if (requested?.kind !== "effect.requested" || !requested.name || !requested.idempotencyKey)
|
|
340
|
+
return;
|
|
341
|
+
try {
|
|
342
|
+
const output = await effects.execute({
|
|
343
|
+
run,
|
|
344
|
+
step: requested,
|
|
345
|
+
name: requested.name,
|
|
346
|
+
payload: requested.input,
|
|
347
|
+
idempotencyKey: requested.idempotencyKey
|
|
348
|
+
});
|
|
349
|
+
return (await append(run, workerId, {
|
|
350
|
+
kind: "effect.completed",
|
|
351
|
+
name: requested.name,
|
|
352
|
+
output,
|
|
353
|
+
idempotencyKey: requested.idempotencyKey
|
|
354
|
+
})).run;
|
|
355
|
+
} catch (error) {
|
|
356
|
+
const message = error instanceof Error ? error.message : "Effect failed";
|
|
357
|
+
const failed = await append(run, workerId, {
|
|
358
|
+
kind: "effect.failed",
|
|
359
|
+
name: requested.name,
|
|
360
|
+
output: { message },
|
|
361
|
+
idempotencyKey: requested.idempotencyKey
|
|
362
|
+
});
|
|
363
|
+
return transition(failed.run, workerId, {
|
|
364
|
+
status: "failed",
|
|
365
|
+
error: { code: "effect_failed", message, retryable: true }
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
return {
|
|
370
|
+
start: async (input) => {
|
|
371
|
+
const timestamp = new Date(now()).toISOString();
|
|
372
|
+
const run = {
|
|
373
|
+
id: input.idempotencyKey ?? id(),
|
|
374
|
+
actor: input.actor,
|
|
375
|
+
agent: input.agent,
|
|
376
|
+
goal: input.goal,
|
|
377
|
+
input: input.input,
|
|
378
|
+
...input.parentRunId ? { parentRunId: input.parentRunId } : {},
|
|
379
|
+
status: "queued",
|
|
380
|
+
budget: input.budget ?? {},
|
|
381
|
+
usage: zeroUsage(),
|
|
382
|
+
version: 0,
|
|
383
|
+
createdAt: timestamp,
|
|
384
|
+
updatedAt: timestamp
|
|
385
|
+
};
|
|
386
|
+
await store.createRun(run);
|
|
387
|
+
await emit({ type: "run.created", run });
|
|
388
|
+
return run;
|
|
389
|
+
},
|
|
390
|
+
cancel: (runId) => store.requestCancel({ runId, now: new Date(now()).toISOString() }),
|
|
391
|
+
inspect: async (runId) => {
|
|
392
|
+
const run = await store.getRun(runId);
|
|
393
|
+
return run ? { run, steps: await store.listSteps(runId) } : undefined;
|
|
394
|
+
},
|
|
395
|
+
workOne: async (workerId) => {
|
|
396
|
+
const claimedAt = now();
|
|
397
|
+
let run = await store.claimDue({
|
|
398
|
+
workerId,
|
|
399
|
+
now: new Date(claimedAt).toISOString(),
|
|
400
|
+
leaseExpiresAt: new Date(claimedAt + leaseMs).toISOString()
|
|
401
|
+
});
|
|
402
|
+
if (!run)
|
|
403
|
+
return;
|
|
404
|
+
await emit({ type: "run.claimed", run });
|
|
405
|
+
for (let count = 0;count < maxStepsPerClaim && run.status === "running"; count += 1) {
|
|
406
|
+
const current = await store.getRun(run.id);
|
|
407
|
+
if (!current)
|
|
408
|
+
throw new Error("Claimed run disappeared");
|
|
409
|
+
if (current.cancelRequestedAt) {
|
|
410
|
+
run = await transition(current, workerId, { status: "cancelled" });
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
run = current;
|
|
414
|
+
const steps = await store.listSteps(run.id);
|
|
415
|
+
try {
|
|
416
|
+
const resumed = await resumePendingEffect(run, workerId, steps);
|
|
417
|
+
if (resumed) {
|
|
418
|
+
run = resumed;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
run = await handle(run, workerId, await driver.next({
|
|
422
|
+
run,
|
|
423
|
+
steps,
|
|
424
|
+
remaining: remainingBudget(run.budget, run.usage)
|
|
425
|
+
}));
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error instanceof Error && error.message.startsWith("Agent budget exceeded:")) {
|
|
428
|
+
run = await transition(run, workerId, {
|
|
429
|
+
status: "failed",
|
|
430
|
+
error: {
|
|
431
|
+
code: "budget_exceeded",
|
|
432
|
+
message: error.message,
|
|
433
|
+
retryable: false
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (run.status === "running") {
|
|
442
|
+
run = await transition(run, workerId, { status: "queued" });
|
|
443
|
+
}
|
|
444
|
+
return run;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
};
|
|
448
|
+
// src/postgres.ts
|
|
449
|
+
var safe = (value) => {
|
|
450
|
+
if (!/^[a-z_][a-z0-9_]*$/i.test(value))
|
|
451
|
+
throw new Error("Invalid SQL namespace");
|
|
452
|
+
return value;
|
|
453
|
+
};
|
|
454
|
+
var agentRuntimePostgresSchemaSql = (schema = "agent_runtime") => {
|
|
455
|
+
const ns = safe(schema);
|
|
456
|
+
return `CREATE SCHEMA IF NOT EXISTS ${ns};
|
|
457
|
+
CREATE TABLE IF NOT EXISTS ${ns}.runs (
|
|
458
|
+
id text PRIMARY KEY,
|
|
459
|
+
document jsonb NOT NULL,
|
|
460
|
+
status text NOT NULL,
|
|
461
|
+
version integer NOT NULL,
|
|
462
|
+
wake_at timestamptz,
|
|
463
|
+
lease_owner text,
|
|
464
|
+
lease_expires_at timestamptz,
|
|
465
|
+
cancel_requested_at timestamptz,
|
|
466
|
+
created_at timestamptz NOT NULL,
|
|
467
|
+
updated_at timestamptz NOT NULL
|
|
468
|
+
);
|
|
469
|
+
CREATE INDEX IF NOT EXISTS agent_runs_due_idx ON ${ns}.runs (status, wake_at, lease_expires_at, created_at);
|
|
470
|
+
CREATE INDEX IF NOT EXISTS agent_runs_actor_idx ON ${ns}.runs ((document->'actor'->>'tenantId'), (document->'actor'->>'userId'), created_at DESC);
|
|
471
|
+
CREATE TABLE IF NOT EXISTS ${ns}.steps (
|
|
472
|
+
run_id text NOT NULL REFERENCES ${ns}.runs(id) ON DELETE CASCADE,
|
|
473
|
+
sequence integer NOT NULL,
|
|
474
|
+
id text NOT NULL UNIQUE,
|
|
475
|
+
kind text NOT NULL,
|
|
476
|
+
idempotency_key text,
|
|
477
|
+
document jsonb NOT NULL,
|
|
478
|
+
created_at timestamptz NOT NULL,
|
|
479
|
+
PRIMARY KEY (run_id, sequence)
|
|
480
|
+
);
|
|
481
|
+
CREATE UNIQUE INDEX IF NOT EXISTS agent_steps_idempotency_idx ON ${ns}.steps (run_id, idempotency_key, kind) WHERE idempotency_key IS NOT NULL;`;
|
|
482
|
+
};
|
|
483
|
+
var saved = (run) => [
|
|
484
|
+
JSON.stringify(run),
|
|
485
|
+
run.status,
|
|
486
|
+
run.version,
|
|
487
|
+
run.wakeAt ?? null,
|
|
488
|
+
run.leaseOwner ?? null,
|
|
489
|
+
run.leaseExpiresAt ?? null,
|
|
490
|
+
run.cancelRequestedAt ?? null,
|
|
491
|
+
run.updatedAt,
|
|
492
|
+
run.id
|
|
493
|
+
];
|
|
494
|
+
var createPostgresAgentRuntimeStore = ({
|
|
495
|
+
client,
|
|
496
|
+
schema = "agent_runtime"
|
|
497
|
+
}) => {
|
|
498
|
+
const ns = safe(schema);
|
|
499
|
+
const update = async (tx, run) => {
|
|
500
|
+
const result = await tx.query(`UPDATE ${ns}.runs SET document=$1::jsonb,status=$2,version=$3,wake_at=$4::timestamptz,lease_owner=$5,lease_expires_at=$6::timestamptz,cancel_requested_at=$7::timestamptz,updated_at=$8::timestamptz WHERE id=$9 RETURNING document`, saved(run));
|
|
501
|
+
return result.rows[0]?.document;
|
|
502
|
+
};
|
|
503
|
+
return {
|
|
504
|
+
createRun: async (run) => {
|
|
505
|
+
await client.query(`INSERT INTO ${ns}.runs (id,document,status,version,wake_at,lease_owner,lease_expires_at,cancel_requested_at,created_at,updated_at) VALUES ($1,$2::jsonb,$3,$4,$5,$6,$7,$8,$9::timestamptz,$10::timestamptz)`, [
|
|
506
|
+
run.id,
|
|
507
|
+
JSON.stringify(run),
|
|
508
|
+
run.status,
|
|
509
|
+
run.version,
|
|
510
|
+
null,
|
|
511
|
+
null,
|
|
512
|
+
null,
|
|
513
|
+
null,
|
|
514
|
+
run.createdAt,
|
|
515
|
+
run.updatedAt
|
|
516
|
+
]);
|
|
517
|
+
},
|
|
518
|
+
getRun: async (id) => (await client.query(`SELECT document FROM ${ns}.runs WHERE id=$1`, [id])).rows[0]?.document,
|
|
519
|
+
listSteps: async (runId) => (await client.query(`SELECT document FROM ${ns}.steps WHERE run_id=$1 ORDER BY sequence`, [runId])).rows.map((row) => row.document),
|
|
520
|
+
claimDue: async ({ workerId, now, leaseExpiresAt }) => client.transaction(async (tx) => {
|
|
521
|
+
const result = await tx.query(`SELECT document FROM ${ns}.runs WHERE cancel_requested_at IS NULL AND (status='queued' OR (status='waiting' AND wake_at <= $1::timestamptz) OR (status='running' AND lease_expires_at <= $1::timestamptz)) ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1`, [now]);
|
|
522
|
+
const run = result.rows[0]?.document;
|
|
523
|
+
if (!run)
|
|
524
|
+
return;
|
|
525
|
+
return update(tx, {
|
|
526
|
+
...run,
|
|
527
|
+
status: "running",
|
|
528
|
+
wakeAt: undefined,
|
|
529
|
+
leaseOwner: workerId,
|
|
530
|
+
leaseExpiresAt,
|
|
531
|
+
version: run.version + 1,
|
|
532
|
+
updatedAt: now
|
|
533
|
+
});
|
|
534
|
+
}),
|
|
535
|
+
heartbeat: async ({
|
|
536
|
+
runId,
|
|
537
|
+
workerId,
|
|
538
|
+
expectedVersion,
|
|
539
|
+
leaseExpiresAt,
|
|
540
|
+
now
|
|
541
|
+
}) => client.transaction(async (tx) => {
|
|
542
|
+
const run = (await tx.query(`SELECT document FROM ${ns}.runs WHERE id=$1 AND version=$2 AND lease_owner=$3 AND status='running' FOR UPDATE`, [runId, expectedVersion, workerId])).rows[0]?.document;
|
|
543
|
+
return run ? update(tx, {
|
|
544
|
+
...run,
|
|
545
|
+
leaseExpiresAt,
|
|
546
|
+
version: run.version + 1,
|
|
547
|
+
updatedAt: now
|
|
548
|
+
}) : undefined;
|
|
549
|
+
}),
|
|
550
|
+
appendStep: async ({ runId, workerId, expectedVersion, step, now }) => client.transaction(async (tx) => {
|
|
551
|
+
const run = (await tx.query(`SELECT document FROM ${ns}.runs WHERE id=$1 AND version=$2 AND lease_owner=$3 AND status='running' FOR UPDATE`, [runId, expectedVersion, workerId])).rows[0]?.document;
|
|
552
|
+
if (!run)
|
|
553
|
+
return;
|
|
554
|
+
const keys = Object.keys(step.usage);
|
|
555
|
+
const exceeded = keys.find((key) => run.budget[key] !== undefined && run.usage[key] + step.usage[key] > (run.budget[key] ?? 0));
|
|
556
|
+
if (exceeded)
|
|
557
|
+
throw new Error(`Agent budget exceeded: ${exceeded}`);
|
|
558
|
+
const inserted = await tx.query(`INSERT INTO ${ns}.steps (run_id,sequence,id,kind,idempotency_key,document,created_at) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7::timestamptz) ON CONFLICT DO NOTHING RETURNING id`, [
|
|
559
|
+
runId,
|
|
560
|
+
step.sequence,
|
|
561
|
+
step.id,
|
|
562
|
+
step.kind,
|
|
563
|
+
step.idempotencyKey ?? null,
|
|
564
|
+
JSON.stringify(step),
|
|
565
|
+
step.createdAt
|
|
566
|
+
]);
|
|
567
|
+
if (!inserted.rows.length)
|
|
568
|
+
return;
|
|
569
|
+
const usage = Object.fromEntries(keys.map((key) => [key, run.usage[key] + step.usage[key]]));
|
|
570
|
+
return update(tx, {
|
|
571
|
+
...run,
|
|
572
|
+
usage,
|
|
573
|
+
version: run.version + 1,
|
|
574
|
+
updatedAt: now
|
|
575
|
+
});
|
|
576
|
+
}),
|
|
577
|
+
transition: async ({
|
|
578
|
+
runId,
|
|
579
|
+
workerId,
|
|
580
|
+
expectedVersion,
|
|
581
|
+
status,
|
|
582
|
+
now,
|
|
583
|
+
output,
|
|
584
|
+
checkpoint,
|
|
585
|
+
wakeAt,
|
|
586
|
+
error
|
|
587
|
+
}) => client.transaction(async (tx) => {
|
|
588
|
+
const params = [runId, expectedVersion];
|
|
589
|
+
const owner = workerId ? ` AND lease_owner=$3` : "";
|
|
590
|
+
if (workerId)
|
|
591
|
+
params.push(workerId);
|
|
592
|
+
const run = (await tx.query(`SELECT document FROM ${ns}.runs WHERE id=$1 AND version=$2${owner} FOR UPDATE`, params)).rows[0]?.document;
|
|
593
|
+
if (!run)
|
|
594
|
+
return;
|
|
595
|
+
return update(tx, {
|
|
596
|
+
...run,
|
|
597
|
+
status,
|
|
598
|
+
version: run.version + 1,
|
|
599
|
+
updatedAt: now,
|
|
600
|
+
...output === undefined ? {} : { output },
|
|
601
|
+
...checkpoint === undefined ? {} : { checkpoint },
|
|
602
|
+
...wakeAt === undefined ? {} : { wakeAt },
|
|
603
|
+
...error === undefined ? {} : { error },
|
|
604
|
+
...status === "running" ? {} : { leaseOwner: undefined, leaseExpiresAt: undefined }
|
|
605
|
+
});
|
|
606
|
+
}),
|
|
607
|
+
requestCancel: async ({ runId, now }) => client.transaction(async (tx) => {
|
|
608
|
+
const run = (await tx.query(`SELECT document FROM ${ns}.runs WHERE id=$1 FOR UPDATE`, [runId])).rows[0]?.document;
|
|
609
|
+
if (!run)
|
|
610
|
+
return;
|
|
611
|
+
if (["completed", "failed", "cancelled", "handed_off"].includes(run.status))
|
|
612
|
+
return run;
|
|
613
|
+
const canCancelImmediately = run.status !== "running";
|
|
614
|
+
return update(tx, {
|
|
615
|
+
...run,
|
|
616
|
+
status: canCancelImmediately ? "cancelled" : run.status,
|
|
617
|
+
cancelRequestedAt: now,
|
|
618
|
+
version: run.version + 1,
|
|
619
|
+
updatedAt: now,
|
|
620
|
+
...canCancelImmediately ? {
|
|
621
|
+
leaseOwner: undefined,
|
|
622
|
+
leaseExpiresAt: undefined,
|
|
623
|
+
wakeAt: undefined
|
|
624
|
+
} : {}
|
|
625
|
+
});
|
|
626
|
+
})
|
|
627
|
+
};
|
|
628
|
+
};
|
|
629
|
+
export {
|
|
630
|
+
zeroUsage,
|
|
631
|
+
remainingBudget,
|
|
632
|
+
normalizeUsage,
|
|
633
|
+
createPostgresAgentRuntimeStore,
|
|
634
|
+
createMemoryAgentRuntimeStore,
|
|
635
|
+
createAgentRuntime,
|
|
636
|
+
budgetExceeded,
|
|
637
|
+
agentRuntimePostgresSchemaSql,
|
|
638
|
+
addUsage
|
|
639
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const manifest: import("@absolutejs/manifest").PackageManifest<Record<string, never>, never>;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/manifest.ts
|
|
3
|
+
import { defineManifest } from "@absolutejs/manifest";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
var manifest = defineManifest()({
|
|
6
|
+
contract: 2,
|
|
7
|
+
identity: {
|
|
8
|
+
accent: "#f97316",
|
|
9
|
+
category: "ai",
|
|
10
|
+
description: "Durable provider-neutral AI agent orchestration with leased runs, atomic steps, hard budgets, checkpoints, timers, cancellation, idempotent effects, and discovery-pinned handoffs.",
|
|
11
|
+
docsUrl: "https://github.com/absolutejs/agent-runtime",
|
|
12
|
+
name: "@absolutejs/agent-runtime",
|
|
13
|
+
tagline: "Run agents durably without choosing their model provider."
|
|
14
|
+
},
|
|
15
|
+
settings: Type.Object({}),
|
|
16
|
+
wiring: []
|
|
17
|
+
});
|
|
18
|
+
export {
|
|
19
|
+
manifest
|
|
20
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contract": 2,
|
|
3
|
+
"identity": {
|
|
4
|
+
"accent": "#f97316",
|
|
5
|
+
"category": "ai",
|
|
6
|
+
"description": "Durable provider-neutral AI agent orchestration with leased runs, atomic steps, hard budgets, checkpoints, timers, cancellation, idempotent effects, and discovery-pinned handoffs.",
|
|
7
|
+
"docsUrl": "https://github.com/absolutejs/agent-runtime",
|
|
8
|
+
"name": "@absolutejs/agent-runtime",
|
|
9
|
+
"tagline": "Run agents durably without choosing their model provider."
|
|
10
|
+
},
|
|
11
|
+
"settings": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {}
|
|
14
|
+
},
|
|
15
|
+
"wiring": []
|
|
16
|
+
}
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentRuntimeStore } from "./types";
|
|
2
|
+
export type AgentRuntimeSqlResult<Row> = {
|
|
3
|
+
rows: Row[];
|
|
4
|
+
};
|
|
5
|
+
export type AgentRuntimeSqlTransaction = {
|
|
6
|
+
query<Row = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<AgentRuntimeSqlResult<Row>>;
|
|
7
|
+
};
|
|
8
|
+
export type AgentRuntimeSqlClient = AgentRuntimeSqlTransaction & {
|
|
9
|
+
transaction<Value>(work: (tx: AgentRuntimeSqlTransaction) => Promise<Value>): Promise<Value>;
|
|
10
|
+
};
|
|
11
|
+
export declare const agentRuntimePostgresSchemaSql: (schema?: string) => string;
|
|
12
|
+
export declare const createPostgresAgentRuntimeStore: ({ client, schema, }: {
|
|
13
|
+
client: AgentRuntimeSqlClient;
|
|
14
|
+
schema?: string;
|
|
15
|
+
}) => AgentRuntimeStore;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentDriver, AgentEffectExecutor, AgentRunEvent, AgentRuntime, AgentRuntimeStore } from "./types";
|
|
2
|
+
export declare const createAgentRuntime: ({ store, driver, effects, now, id, leaseMs, maxStepsPerClaim, onEvent, }: {
|
|
3
|
+
store: AgentRuntimeStore;
|
|
4
|
+
driver: AgentDriver;
|
|
5
|
+
effects: AgentEffectExecutor;
|
|
6
|
+
now?: () => number;
|
|
7
|
+
id?: () => string;
|
|
8
|
+
leaseMs?: number;
|
|
9
|
+
maxStepsPerClaim?: number;
|
|
10
|
+
onEvent?: (event: AgentRunEvent) => void | Promise<void>;
|
|
11
|
+
}) => AgentRuntime;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export type AgentRunStatus = "queued" | "running" | "waiting" | "suspended" | "handed_off" | "completed" | "failed" | "cancelled";
|
|
2
|
+
export type AgentIdentityPin = {
|
|
3
|
+
descriptorId: string;
|
|
4
|
+
descriptorVersion: string;
|
|
5
|
+
descriptorDigest: string;
|
|
6
|
+
};
|
|
7
|
+
export type AgentActor = {
|
|
8
|
+
tenantId: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
agentId: string;
|
|
11
|
+
delegationId?: string;
|
|
12
|
+
organizationId?: string;
|
|
13
|
+
};
|
|
14
|
+
export type AgentBudget = {
|
|
15
|
+
actions?: number;
|
|
16
|
+
costMicros?: number;
|
|
17
|
+
inputTokens?: number;
|
|
18
|
+
outputTokens?: number;
|
|
19
|
+
spendMinor?: number;
|
|
20
|
+
wallTimeMs?: number;
|
|
21
|
+
};
|
|
22
|
+
export type AgentBudgetUsage = Required<AgentBudget>;
|
|
23
|
+
export type AgentRun = {
|
|
24
|
+
id: string;
|
|
25
|
+
parentRunId?: string;
|
|
26
|
+
actor: AgentActor;
|
|
27
|
+
agent: AgentIdentityPin;
|
|
28
|
+
goal: string;
|
|
29
|
+
input: unknown;
|
|
30
|
+
output?: unknown;
|
|
31
|
+
status: AgentRunStatus;
|
|
32
|
+
budget: AgentBudget;
|
|
33
|
+
usage: AgentBudgetUsage;
|
|
34
|
+
version: number;
|
|
35
|
+
cancelRequestedAt?: string;
|
|
36
|
+
checkpoint?: unknown;
|
|
37
|
+
wakeAt?: string;
|
|
38
|
+
leaseOwner?: string;
|
|
39
|
+
leaseExpiresAt?: string;
|
|
40
|
+
error?: {
|
|
41
|
+
code: string;
|
|
42
|
+
message: string;
|
|
43
|
+
retryable: boolean;
|
|
44
|
+
};
|
|
45
|
+
createdAt: string;
|
|
46
|
+
updatedAt: string;
|
|
47
|
+
};
|
|
48
|
+
export type AgentStepKind = "observation" | "thought" | "message" | "effect.requested" | "effect.completed" | "effect.failed" | "checkpoint" | "handoff" | "wait" | "completed" | "failed";
|
|
49
|
+
export type AgentStep = {
|
|
50
|
+
id: string;
|
|
51
|
+
runId: string;
|
|
52
|
+
sequence: number;
|
|
53
|
+
kind: AgentStepKind;
|
|
54
|
+
idempotencyKey?: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
input?: unknown;
|
|
57
|
+
output?: unknown;
|
|
58
|
+
usage: AgentBudgetUsage;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
};
|
|
61
|
+
export type AgentRunEvent = {
|
|
62
|
+
type: "run.created";
|
|
63
|
+
run: AgentRun;
|
|
64
|
+
} | {
|
|
65
|
+
type: "run.claimed";
|
|
66
|
+
run: AgentRun;
|
|
67
|
+
} | {
|
|
68
|
+
type: "run.transitioned";
|
|
69
|
+
run: AgentRun;
|
|
70
|
+
} | {
|
|
71
|
+
type: "step.appended";
|
|
72
|
+
run: AgentRun;
|
|
73
|
+
step: AgentStep;
|
|
74
|
+
};
|
|
75
|
+
export type AgentRuntimeStore = {
|
|
76
|
+
createRun(run: AgentRun): Promise<void>;
|
|
77
|
+
getRun(id: string): Promise<AgentRun | undefined>;
|
|
78
|
+
listSteps(runId: string): Promise<readonly AgentStep[]>;
|
|
79
|
+
claimDue(input: {
|
|
80
|
+
workerId: string;
|
|
81
|
+
now: string;
|
|
82
|
+
leaseExpiresAt: string;
|
|
83
|
+
}): Promise<AgentRun | undefined>;
|
|
84
|
+
heartbeat(input: {
|
|
85
|
+
runId: string;
|
|
86
|
+
workerId: string;
|
|
87
|
+
expectedVersion: number;
|
|
88
|
+
leaseExpiresAt: string;
|
|
89
|
+
now: string;
|
|
90
|
+
}): Promise<AgentRun | undefined>;
|
|
91
|
+
appendStep(input: {
|
|
92
|
+
runId: string;
|
|
93
|
+
workerId: string;
|
|
94
|
+
expectedVersion: number;
|
|
95
|
+
step: AgentStep;
|
|
96
|
+
now: string;
|
|
97
|
+
}): Promise<AgentRun | undefined>;
|
|
98
|
+
transition(input: {
|
|
99
|
+
runId: string;
|
|
100
|
+
workerId?: string;
|
|
101
|
+
expectedVersion: number;
|
|
102
|
+
status: AgentRunStatus;
|
|
103
|
+
now: string;
|
|
104
|
+
output?: unknown;
|
|
105
|
+
checkpoint?: unknown;
|
|
106
|
+
wakeAt?: string;
|
|
107
|
+
error?: AgentRun["error"];
|
|
108
|
+
}): Promise<AgentRun | undefined>;
|
|
109
|
+
requestCancel(input: {
|
|
110
|
+
runId: string;
|
|
111
|
+
now: string;
|
|
112
|
+
}): Promise<AgentRun | undefined>;
|
|
113
|
+
};
|
|
114
|
+
export type AgentEffectExecutor = {
|
|
115
|
+
execute(input: {
|
|
116
|
+
run: AgentRun;
|
|
117
|
+
step: AgentStep;
|
|
118
|
+
name: string;
|
|
119
|
+
payload: unknown;
|
|
120
|
+
idempotencyKey: string;
|
|
121
|
+
}): Promise<unknown>;
|
|
122
|
+
};
|
|
123
|
+
export type AgentTransition = {
|
|
124
|
+
type: "continue";
|
|
125
|
+
kind?: "observation" | "thought" | "message";
|
|
126
|
+
name?: string;
|
|
127
|
+
input?: unknown;
|
|
128
|
+
output?: unknown;
|
|
129
|
+
usage?: AgentBudget;
|
|
130
|
+
} | {
|
|
131
|
+
type: "effect";
|
|
132
|
+
name: string;
|
|
133
|
+
input: unknown;
|
|
134
|
+
idempotencyKey: string;
|
|
135
|
+
usage?: AgentBudget;
|
|
136
|
+
} | {
|
|
137
|
+
type: "checkpoint";
|
|
138
|
+
checkpoint: unknown;
|
|
139
|
+
reason?: string;
|
|
140
|
+
} | {
|
|
141
|
+
type: "wait";
|
|
142
|
+
until: string;
|
|
143
|
+
reason: string;
|
|
144
|
+
checkpoint?: unknown;
|
|
145
|
+
} | {
|
|
146
|
+
type: "handoff";
|
|
147
|
+
target: AgentIdentityPin;
|
|
148
|
+
input: unknown;
|
|
149
|
+
reason: string;
|
|
150
|
+
} | {
|
|
151
|
+
type: "complete";
|
|
152
|
+
output: unknown;
|
|
153
|
+
} | {
|
|
154
|
+
type: "fail";
|
|
155
|
+
code: string;
|
|
156
|
+
message: string;
|
|
157
|
+
retryable?: boolean;
|
|
158
|
+
};
|
|
159
|
+
export type AgentDriverContext = {
|
|
160
|
+
run: AgentRun;
|
|
161
|
+
steps: readonly AgentStep[];
|
|
162
|
+
remaining: AgentBudgetUsage;
|
|
163
|
+
};
|
|
164
|
+
export type AgentDriver = {
|
|
165
|
+
next(context: AgentDriverContext): Promise<AgentTransition>;
|
|
166
|
+
};
|
|
167
|
+
export type AgentRuntime = {
|
|
168
|
+
start(input: {
|
|
169
|
+
actor: AgentActor;
|
|
170
|
+
agent: AgentIdentityPin;
|
|
171
|
+
goal: string;
|
|
172
|
+
input: unknown;
|
|
173
|
+
budget?: AgentBudget;
|
|
174
|
+
idempotencyKey?: string;
|
|
175
|
+
parentRunId?: string;
|
|
176
|
+
}): Promise<AgentRun>;
|
|
177
|
+
cancel(runId: string): Promise<AgentRun | undefined>;
|
|
178
|
+
inspect(runId: string): Promise<{
|
|
179
|
+
run: AgentRun;
|
|
180
|
+
steps: readonly AgentStep[];
|
|
181
|
+
} | undefined>;
|
|
182
|
+
workOne(workerId: string): Promise<AgentRun | undefined>;
|
|
183
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@absolutejs/agent-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Durable provider-neutral AI agent runs, steps, checkpoints, budgets, leases, timers, effects, and handoffs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/absolutejs/agent-runtime.git"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"absolutejs": {
|
|
17
|
+
"manifestContract": 2
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./manifest": {
|
|
25
|
+
"types": "./dist/manifest.d.ts",
|
|
26
|
+
"import": "./dist/manifest.js"
|
|
27
|
+
},
|
|
28
|
+
"./manifest.json": "./dist/manifest.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"format": "prettier --write \"./**/*.{ts,json,md,yml}\"",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"build": "rm -rf dist && bun build src/index.ts src/manifest.ts --outdir dist --target=bun --external @absolutejs/manifest --external @sinclair/typebox && tsc -p tsconfig.build.json && absolute-manifest emit",
|
|
40
|
+
"check:package": "bun run typecheck && bun run test && bun run build"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@absolutejs/manifest": "^0.2.0",
|
|
44
|
+
"@sinclair/typebox": "^0.34.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "^1.3.14",
|
|
48
|
+
"prettier": "3.5.3",
|
|
49
|
+
"typescript": "5.8.3"
|
|
50
|
+
}
|
|
51
|
+
}
|