@agentswf/engine 0.1.0-beta.4 → 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 +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +35 -22
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +5 -7
- package/src/attempt-output.ts +15 -32
- package/src/attempt-view.ts +34 -33
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +41 -11
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +41 -33
- package/src/deadlines.ts +40 -7
- package/src/decisions/directory.ts +51 -40
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +43 -27
- package/src/emit-command.ts +88 -58
- package/src/emit.ts +254 -56
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +18 -6
- package/src/help/inbox.txt +6 -4
- package/src/help/logs.txt +9 -6
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +30 -11
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +7 -15
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +56 -133
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +40 -57
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +5 -4
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +15 -12
- package/src/logs-command.ts +128 -47
- package/src/message-operation.ts +6 -3
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +31 -28
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +9 -7
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +224 -94
- package/src/operator-runtime.ts +29 -7
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +105 -59
- package/src/progress-view.ts +10 -10
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +45 -28
- package/src/run-commands.ts +43 -115
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +205 -43
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +18 -12
- package/src/run-view.ts +81 -17
- package/src/runs-command.ts +83 -63
- package/src/runs.ts +310 -212
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +28 -29
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +119 -131
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +36 -14
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +73 -26
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +25 -16
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +54 -4
- package/src/workflow-runner.ts +805 -580
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +3 -3
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +121 -53
- package/src/workflow-testing/index.ts +33 -842
- package/src/workflow-testing/run-test.ts +1128 -0
- package/src/workflow-testing/script.ts +74 -45
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -990
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
ToolFailure,
|
|
7
7
|
} from "@agentswf/contract/wire";
|
|
8
8
|
import type {
|
|
9
|
+
Duration,
|
|
9
10
|
Effort,
|
|
10
11
|
JsonObject,
|
|
11
12
|
JsonValue,
|
|
@@ -78,7 +79,11 @@ export type Message = {
|
|
|
78
79
|
answers?: true;
|
|
79
80
|
};
|
|
80
81
|
|
|
81
|
-
export type SendOptions = {
|
|
82
|
+
export type SendOptions = {
|
|
83
|
+
awaitReply?: boolean;
|
|
84
|
+
/** How long an awaited reply is waited for, as `wfa send --timeout` gives it. */
|
|
85
|
+
timeout?: Duration;
|
|
86
|
+
};
|
|
82
87
|
|
|
83
88
|
/** How a send or reply ended; a refusal is a value to test, not a throw. */
|
|
84
89
|
export type MessageOutcome =
|
|
@@ -89,7 +94,8 @@ export type MessageOutcome =
|
|
|
89
94
|
|
|
90
95
|
/**
|
|
91
96
|
* A message that woke the agent, and what its script may do in that turn: `reply` and `send` as
|
|
92
|
-
* the turn's own
|
|
97
|
+
* the turn's own. Returning nothing ends the turn without a reply, and one owed is reminded once,
|
|
98
|
+
* then fails; returning `noAnswer.failed(reason)` fails it as a failed harness does.
|
|
93
99
|
*/
|
|
94
100
|
export type ReceivedMessage = Message & {
|
|
95
101
|
/** The turn it woke: whether it is a reminder (`nudge`), and every message shown in it. */
|
|
@@ -97,26 +103,24 @@ export type ReceivedMessage = Message & {
|
|
|
97
103
|
reply(text: string, options?: SendOptions): Promise<MessageOutcome>;
|
|
98
104
|
send(peer: string, text: string, options?: SendOptions): Promise<MessageOutcome>;
|
|
99
105
|
tool(name: string, input?: JsonObject): Promise<ToolOutcome>;
|
|
100
|
-
/** Ends the turn without a reply; one owed is reminded once, then fails. */
|
|
101
|
-
silent(): Reply;
|
|
102
|
-
/** The harness failed: the message operation fails, and the agent stops. */
|
|
103
|
-
fail(reason?: string): Reply;
|
|
104
106
|
};
|
|
105
107
|
|
|
106
108
|
/**
|
|
107
109
|
* A message operation's script: called with the message that woke the agent, and again with the
|
|
108
110
|
* next one when held messages continue the operation, or with the same one, `turn.nudge` set, when
|
|
109
|
-
* it is reminded of a reply it owes.
|
|
111
|
+
* it is reminded of a reply it owes. A `noAnswer` it returns ends the turn as that says; nothing,
|
|
112
|
+
* or what a `reply` or `send` resolved to, ends it without a reply. A throw fails the test.
|
|
110
113
|
*/
|
|
111
114
|
export type MessageScript = (
|
|
112
115
|
message: ReceivedMessage,
|
|
113
|
-
|
|
116
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: an async script that returns nothing is Promise<void>.
|
|
117
|
+
) => void | NoAnswer | MessageOutcome | Promise<void | NoAnswer | MessageOutcome>;
|
|
114
118
|
|
|
115
119
|
/**
|
|
116
|
-
* How a participant's script's `reply` or `send` ended, as `awf reply --json`
|
|
117
|
-
* `id`
|
|
120
|
+
* How a participant's script's `reply` or `send` ended, as `awf reply --json` prints it without the
|
|
121
|
+
* `v`: delivered, `id` a reply's question or a send's new message, or refused with its `code`.
|
|
118
122
|
*/
|
|
119
|
-
export type ParticipantOutcome = {
|
|
123
|
+
export type ParticipantOutcome = { id: string } | { code: ParticipantCode };
|
|
120
124
|
|
|
121
125
|
/**
|
|
122
126
|
* A message an agent sent a participant, and what the participant's script may do about it, as
|
|
@@ -157,7 +161,7 @@ export type ParticipantScript = (event: ParticipantRoute | ParticipantMessage) =
|
|
|
157
161
|
|
|
158
162
|
const ENTRY: unique symbol = Symbol("agentswf.script-entry");
|
|
159
163
|
|
|
160
|
-
type Respond = (turn: Turn) => JsonValue |
|
|
164
|
+
type Respond = (turn: Turn) => JsonValue | NoAnswer | Promise<JsonValue | NoAnswer>;
|
|
161
165
|
|
|
162
166
|
/** An answer to a turn that asks for its schema, or for text. Made by `answer`. */
|
|
163
167
|
export type Answer = {
|
|
@@ -172,11 +176,13 @@ type Ending =
|
|
|
172
176
|
| { kind: "hang" }
|
|
173
177
|
| { kind: "interrupted" };
|
|
174
178
|
|
|
175
|
-
/** A turn ended without an answer. Made by `
|
|
176
|
-
export type
|
|
179
|
+
/** A turn ended without an answer. Made by `noAnswer`. */
|
|
180
|
+
export type NoAnswer = {
|
|
181
|
+
readonly [ENTRY]: { kind: "no-answer"; ending: Ending };
|
|
182
|
+
};
|
|
177
183
|
|
|
178
184
|
/** Every turn the agent is asked, alike; or a list, one entry per turn in order. */
|
|
179
|
-
export type Script = Answer |
|
|
185
|
+
export type Script = Answer | NoAnswer | readonly (Answer | NoAnswer)[];
|
|
180
186
|
|
|
181
187
|
/** What a schema's answer is: the carrier the author surface types with, else TypeBox's `Static`. */
|
|
182
188
|
export type AnswerOf<S> = S extends { readonly "~output"?: infer O }
|
|
@@ -186,11 +192,11 @@ export type AnswerOf<S> = S extends { readonly "~output"?: infer O }
|
|
|
186
192
|
: StaticOf<S>;
|
|
187
193
|
type StaticOf<S> = S extends Type.TSchema ? Type.Static<S> : JsonValue;
|
|
188
194
|
|
|
189
|
-
type Answering<A> = A | ((turn: Turn) => A |
|
|
195
|
+
type Answering<A> = A | ((turn: Turn) => A | NoAnswer | Promise<A | NoAnswer>);
|
|
190
196
|
|
|
191
197
|
/**
|
|
192
198
|
* An answer to a turn that asks for `schema`: a value of its type, or a function of the turn that
|
|
193
|
-
* returns one or a `
|
|
199
|
+
* returns one or a `noAnswer`. A function that returns no answer is asked again for the nudge, with
|
|
194
200
|
* `turn.nudge` set.
|
|
195
201
|
*/
|
|
196
202
|
export function answer<const S extends OutputSchema>(
|
|
@@ -206,46 +212,69 @@ export function answer(...args: [unknown] | [unknown, unknown]): Answer {
|
|
|
206
212
|
return { [ENTRY]: { kind: "answer", schema: schema as OutputSchema | undefined, respond } };
|
|
207
213
|
}
|
|
208
214
|
|
|
209
|
-
const ending = (value: Ending):
|
|
215
|
+
const ending = (value: Ending): NoAnswer => ({ [ENTRY]: { kind: "no-answer", ending: value } });
|
|
210
216
|
|
|
211
|
-
/** Ends a turn without an answer
|
|
212
|
-
export const
|
|
213
|
-
/** Reports a cooperative wait; later check-ins invoke the same answer function again. */
|
|
214
|
-
waiting: (reason: string, timeoutMs?: number): Reply =>
|
|
215
|
-
ending({ kind: "waiting", reason, ...(timeoutMs === undefined ? {} : { timeoutMs }) }),
|
|
217
|
+
/** Ends a turn without an answer, so it takes no schema. */
|
|
218
|
+
export const noAnswer = {
|
|
216
219
|
/** Ends without a result; recovery follows the placement's production support. */
|
|
217
|
-
silent: ():
|
|
220
|
+
silent: (): NoAnswer => ending({ kind: "silent" }),
|
|
218
221
|
/** The agent is stuck, as on a permission prompt. */
|
|
219
|
-
blocked: (reason = "waiting on a permission prompt"):
|
|
222
|
+
blocked: (reason = "waiting on a permission prompt"): NoAnswer =>
|
|
220
223
|
ending({ kind: "blocked", reason }),
|
|
221
224
|
/** The harness failed. */
|
|
222
|
-
failed: (reason = "harness crashed"):
|
|
225
|
+
failed: (reason = "harness crashed"): NoAnswer => ending({ kind: "failed", reason }),
|
|
223
226
|
/**
|
|
224
227
|
* The agent's harness cannot sign in, as with an expired login: the turn fails with `login` set,
|
|
225
228
|
* naming the agent's harness, `provider` where it logs in to several, and what to run.
|
|
226
229
|
*/
|
|
227
|
-
needsLogin: (provider?: string):
|
|
230
|
+
needsLogin: (provider?: string): NoAnswer =>
|
|
228
231
|
ending({ kind: "needs-login", ...(provider === undefined ? {} : { provider }) }),
|
|
229
232
|
/** The harness reported the turn timed out; it ends at once. */
|
|
230
|
-
timedOut: (reason = "turn timed out"):
|
|
233
|
+
timedOut: (reason = "turn timed out"): NoAnswer => ending({ kind: "timed-out", reason }),
|
|
231
234
|
/** Holds the turn until the engine cancels it. */
|
|
232
|
-
hang: ():
|
|
235
|
+
hang: (): NoAnswer => ending({ kind: "hang" }),
|
|
233
236
|
/**
|
|
234
237
|
* The operator stopped the turn in their pane, as only the calling session's can be: it settles
|
|
235
238
|
* `cancelled`, is never nudged, and the session takes the next turn.
|
|
236
239
|
*/
|
|
237
|
-
interrupted: ():
|
|
240
|
+
interrupted: (): NoAnswer => ending({ kind: "interrupted" }),
|
|
238
241
|
};
|
|
239
242
|
|
|
240
243
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
244
|
+
* Reports a cooperative wait, `wfa waiting`; later check-ins invoke the same answer function again.
|
|
245
|
+
* Only the engine's own tests use it: whether an agent may wait is its harness's, not a workflow's.
|
|
246
|
+
*/
|
|
247
|
+
export const waiting = (reason: string, timeoutMs?: number): NoAnswer =>
|
|
248
|
+
ending({ kind: "waiting", reason, ...(timeoutMs === undefined ? {} : { timeoutMs }) });
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* How a turn ended, as `run.turns` keeps it: the engine's word, but for `waiting`, a check-in, and
|
|
252
|
+
* `awaiting-reply`, a script awaiting a reply, which a later turn of the same call resumes it with.
|
|
243
253
|
*/
|
|
244
|
-
export type
|
|
254
|
+
export type TestTurnOutcome =
|
|
255
|
+
| "answered"
|
|
256
|
+
| "unanswered"
|
|
257
|
+
| "blocked"
|
|
258
|
+
| "failed"
|
|
259
|
+
| "timed-out"
|
|
260
|
+
| "cancelled"
|
|
261
|
+
| "waiting"
|
|
262
|
+
| "awaiting-reply";
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* A turn's outcome until the engine's record replaces it: what a turn before its operation's last,
|
|
266
|
+
* a nudged one, keeps.
|
|
267
|
+
*/
|
|
268
|
+
export function outcomeOf(kind: Exclude<Ending["kind"], "waiting">): TestTurnOutcome {
|
|
269
|
+
if (kind === "silent") return "unanswered";
|
|
270
|
+
if (kind === "needs-login") return "failed";
|
|
271
|
+
if (kind === "hang" || kind === "interrupted") return "cancelled";
|
|
272
|
+
return kind;
|
|
273
|
+
}
|
|
245
274
|
|
|
246
275
|
export type Step =
|
|
247
276
|
| { kind: "answer"; value: JsonValue }
|
|
248
|
-
| { kind: "
|
|
277
|
+
| { kind: "no-answer"; ending: Ending }
|
|
249
278
|
/** The script cannot meet this turn: the test's mistake, not the workflow's. */
|
|
250
279
|
| { kind: "script-error"; message: string };
|
|
251
280
|
|
|
@@ -333,7 +362,7 @@ class Lists<E> {
|
|
|
333
362
|
type Of = { noun: string; option: string; whose: string; expected: string };
|
|
334
363
|
|
|
335
364
|
/** Each agent's turn scripts, or its compactions'. */
|
|
336
|
-
export class Scripts extends Lists<Answer |
|
|
365
|
+
export class Scripts extends Lists<Answer | NoAnswer> {
|
|
337
366
|
readonly #noun: string;
|
|
338
367
|
|
|
339
368
|
constructor(
|
|
@@ -345,7 +374,7 @@ export class Scripts extends Lists<Answer | Reply> {
|
|
|
345
374
|
{
|
|
346
375
|
...of,
|
|
347
376
|
whose: of.noun === "turn" ? "script" : `${of.noun} script`,
|
|
348
|
-
expected: "answer(…),
|
|
377
|
+
expected: "answer(…), noAnswer.…()",
|
|
349
378
|
},
|
|
350
379
|
isEntry,
|
|
351
380
|
);
|
|
@@ -357,14 +386,14 @@ export class Scripts extends Lists<Answer | Reply> {
|
|
|
357
386
|
const found = this.entry(turn.agent, index);
|
|
358
387
|
if ("why" in found) return this.error(turn, found.why);
|
|
359
388
|
const meant = found.entry[ENTRY];
|
|
360
|
-
if (meant.kind === "
|
|
389
|
+
if (meant.kind === "no-answer") return { ...meant };
|
|
361
390
|
if (!sameSchema(meant.schema, turn.schema)) {
|
|
362
391
|
return this.error(
|
|
363
392
|
turn,
|
|
364
393
|
`it asks for ${describeSchema(turn.schema)}; its script answers ${describeSchema(meant.schema)}`,
|
|
365
394
|
);
|
|
366
395
|
}
|
|
367
|
-
let out: JsonValue |
|
|
396
|
+
let out: JsonValue | NoAnswer | undefined;
|
|
368
397
|
try {
|
|
369
398
|
out = await meant.respond(turn);
|
|
370
399
|
} catch (error) {
|
|
@@ -373,7 +402,7 @@ export class Scripts extends Lists<Answer | Reply> {
|
|
|
373
402
|
if (out === undefined) return this.error(turn, "its script returned nothing");
|
|
374
403
|
if (isEntry(out)) {
|
|
375
404
|
const given = out[ENTRY];
|
|
376
|
-
if (given.kind === "
|
|
405
|
+
if (given.kind === "no-answer") return { ...given };
|
|
377
406
|
return this.error(turn, "its script returned answer(…); a function returns the value itself");
|
|
378
407
|
}
|
|
379
408
|
return { kind: "answer", value: out };
|
|
@@ -403,8 +432,8 @@ export class MessageScripts extends Lists<MessageScript> {
|
|
|
403
432
|
}
|
|
404
433
|
|
|
405
434
|
/**
|
|
406
|
-
* Runs the script for the agent's `index`th message operation with `message
|
|
407
|
-
* ends the turn
|
|
435
|
+
* Runs the script for the agent's `index`th message operation with `message`: what it returns
|
|
436
|
+
* ends the turn silent unless it is a `noAnswer`.
|
|
408
437
|
*/
|
|
409
438
|
async step(message: ReceivedMessage, index: number): Promise<Step> {
|
|
410
439
|
const { turn } = message;
|
|
@@ -427,13 +456,13 @@ export class MessageScripts extends Lists<MessageScript> {
|
|
|
427
456
|
}
|
|
428
457
|
if (isEntry(out)) {
|
|
429
458
|
const given = out[ENTRY];
|
|
430
|
-
if (given.kind === "
|
|
459
|
+
if (given.kind === "no-answer") return { ...given };
|
|
431
460
|
return {
|
|
432
461
|
kind: "script-error",
|
|
433
462
|
message: `${where}: a message script ends a turn, it answers no call`,
|
|
434
463
|
};
|
|
435
464
|
}
|
|
436
|
-
return { kind: "
|
|
465
|
+
return { kind: "no-answer", ending: { kind: "silent" } };
|
|
437
466
|
}
|
|
438
467
|
}
|
|
439
468
|
|
|
@@ -459,7 +488,7 @@ export function findByKey<T>(
|
|
|
459
488
|
};
|
|
460
489
|
}
|
|
461
490
|
|
|
462
|
-
function isEntry(value: unknown): value is Answer |
|
|
491
|
+
function isEntry(value: unknown): value is Answer | NoAnswer {
|
|
463
492
|
return typeof value === "object" && value !== null && ENTRY in value;
|
|
464
493
|
}
|
|
465
494
|
|
|
@@ -489,7 +518,7 @@ function globMatches(pattern: string, key: string): boolean {
|
|
|
489
518
|
return new RegExp(`^${escaped.join(".*")}$`).test(key);
|
|
490
519
|
}
|
|
491
520
|
|
|
492
|
-
|
|
521
|
+
function firstLine(text: string): string {
|
|
493
522
|
const line = text.split("\n", 1)[0]!;
|
|
494
523
|
return line.length > 100 ? `${line.slice(0, 100)}…` : line;
|
|
495
524
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Holds a run's lock from another process for `emit.test.ts`: `bun hold-run-lock.fixture.ts {file} {ms}`.
|
|
2
|
-
import { withRunLock } from "./run-lock";
|
|
3
|
-
|
|
4
|
-
const [file, ms] = process.argv.slice(2);
|
|
5
|
-
await withRunLock(file!, async () => {
|
|
6
|
-
console.log("held");
|
|
7
|
-
await Bun.sleep(Number(ms));
|
|
8
|
-
});
|