@agentswf/engine 0.1.0-beta.2 → 0.1.0-beta.3
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/package.json +5 -5
- package/src/allowance-command.ts +29 -7
- package/src/attempt-ending.ts +23 -5
- package/src/attempt-output.ts +48 -18
- package/src/attempt-view.ts +25 -15
- package/src/commands.ts +176 -0
- package/src/control-plane.ts +72 -21
- package/src/coordinator-port.ts +29 -0
- package/src/coordinator-prompt.ts +237 -0
- package/src/coordinator-socket.ts +236 -0
- package/src/deadlines.ts +47 -10
- package/src/decisions/directory.ts +8 -6
- package/src/duration.ts +29 -0
- package/src/emit-command.ts +130 -0
- package/src/emit.ts +246 -0
- package/src/files.ts +11 -1
- package/src/here.ts +8 -2
- package/src/hold-run-lock.fixture.ts +8 -0
- package/src/holder-command.ts +297 -0
- package/src/inbox-command.ts +727 -0
- package/src/init-command.ts +27 -0
- package/src/init-template/AGENTS.md.template +1 -1
- package/src/init-template/hello/workflow.test.ts.template +2 -2
- package/src/init-template/hello/workflow.ts.template +16 -20
- package/src/keep-alive.ts +40 -0
- package/src/logs-command.ts +104 -20
- package/src/machine.ts +12 -1
- package/src/mailbox.ts +977 -0
- package/src/message-operation.ts +26 -3
- package/src/messaging/messaging.ts +458 -65
- package/src/messaging/routes.ts +10 -3
- package/src/messaging/text.ts +8 -5
- package/src/operation-liveness.ts +54 -8
- package/src/operator-cli.ts +290 -119
- package/src/operator-runtime.ts +5 -2
- package/src/progress-view.ts +49 -7
- package/src/projects.ts +57 -0
- package/src/result-slots.ts +43 -19
- package/src/result-validation.ts +18 -27
- package/src/run-command.ts +36 -32
- package/src/run-commands.ts +13 -2
- package/src/run-lock.ts +123 -0
- package/src/run-prepare.ts +94 -22
- package/src/run-state.ts +125 -6
- package/src/run-usage.ts +1 -1
- package/src/run-view.ts +0 -9
- package/src/runs-command.ts +46 -17
- package/src/runs.ts +142 -23
- package/src/sandboxes.ts +8 -2
- package/src/simple-input.ts +258 -0
- package/src/socket-exchange.ts +26 -0
- package/src/sources.ts +70 -0
- package/src/stage-ledger.ts +88 -13
- package/src/stage-plan.ts +23 -0
- package/src/stop-crashed.ts +39 -0
- package/src/test-command.ts +32 -0
- package/src/testing.ts +71 -3
- package/src/tick-command.ts +87 -0
- package/src/tools.ts +636 -0
- package/src/wake.ts +264 -0
- package/src/workflow-loader.ts +32 -22
- package/src/workflow-runner.ts +1210 -77
- package/src/workflow-testing/coordinator.typecheck.ts +55 -0
- package/src/workflow-testing/coordinators.ts +137 -0
- package/src/workflow-testing/host.ts +61 -1
- package/src/workflow-testing/index.ts +488 -33
- package/src/workflow-testing/script.ts +72 -2
- package/src/workflow-testing/workflow-testing.typecheck.ts +19 -2
package/src/progress-view.ts
CHANGED
|
@@ -62,7 +62,7 @@ export function renderProgress(
|
|
|
62
62
|
const shown =
|
|
63
63
|
agents.length <= MAX_LISTED
|
|
64
64
|
? agents
|
|
65
|
-
: agents.filter((a) => isRunning(a) || isFailed(a) || a.stopped);
|
|
65
|
+
: agents.filter((a) => isRunning(a) || isFailed(a) || a.stopped || a.liveUntil);
|
|
66
66
|
lines.push(...shown.slice(0, MAX_LISTED).map(agentLine));
|
|
67
67
|
const hidden = agents.length - Math.min(shown.length, MAX_LISTED);
|
|
68
68
|
if (hidden > 0) lines.push(paint.dim(` … ${hidden} more`));
|
|
@@ -96,7 +96,9 @@ export function renderProgress(
|
|
|
96
96
|
});
|
|
97
97
|
// Agents opened outside any labelled parallel.
|
|
98
98
|
const loose = snapshot.agents.filter((agent) => agent.group === undefined && !inStage(agent));
|
|
99
|
-
if (
|
|
99
|
+
if (
|
|
100
|
+
loose.some((agent) => isRunning(agent) || isFailed(agent) || agent.stopped || agent.liveUntil)
|
|
101
|
+
)
|
|
100
102
|
listAgents(loose);
|
|
101
103
|
return lines;
|
|
102
104
|
}
|
|
@@ -105,7 +107,13 @@ export function renderProgress(
|
|
|
105
107
|
export function progressEvents(
|
|
106
108
|
before: WorkflowRunSnapshot | undefined,
|
|
107
109
|
after: WorkflowRunSnapshot,
|
|
108
|
-
view: {
|
|
110
|
+
view: {
|
|
111
|
+
startedAt: number;
|
|
112
|
+
now: number;
|
|
113
|
+
herdrSession?: string;
|
|
114
|
+
/** Messages another view shows already, which get no line here. */
|
|
115
|
+
shown?: (message: WorkflowRunSnapshot["messages"][number]) => boolean;
|
|
116
|
+
},
|
|
109
117
|
): string[] {
|
|
110
118
|
// Each stamped and ordered by when it happened; at the same instant, what ends goes before what
|
|
111
119
|
// starts, and the inner before the outer as each ends, the outer before the inner as each starts.
|
|
@@ -141,6 +149,8 @@ export function progressEvents(
|
|
|
141
149
|
`■ ${agent.key} stopped: ${oneLine(agent.stopped.reason)}`,
|
|
142
150
|
);
|
|
143
151
|
}
|
|
152
|
+
if (agent.liveUntil !== undefined && agent.liveUntil !== earlier?.liveUntil)
|
|
153
|
+
add(view.now, "agent phase", `… ${agent.key} · ${liveNote(agent.liveUntil)}`);
|
|
144
154
|
if (!turn) continue;
|
|
145
155
|
const fresh = earlier?.turns !== agent.turns;
|
|
146
156
|
if (fresh && (turn.outcome === undefined || agent.stopped))
|
|
@@ -175,6 +185,7 @@ export function progressEvents(
|
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
187
|
for (const message of after.messages.slice(before?.messages.length ?? 0)) {
|
|
188
|
+
if (view.shown?.(message)) continue;
|
|
178
189
|
add(message.at, "message", messageLine(message));
|
|
179
190
|
}
|
|
180
191
|
after.stages.forEach((stage, index) => {
|
|
@@ -340,7 +351,7 @@ function stageAgentLine(
|
|
|
340
351
|
const turn = agent.turn!;
|
|
341
352
|
const [mark, note] =
|
|
342
353
|
turn.outcome === "answered" && !agent.stopped
|
|
343
|
-
? [paint.dim("·"), paint.dim("waiting")]
|
|
354
|
+
? [paint.dim("·"), paint.dim(agent.liveUntil ? liveNote(agent.liveUntil) : "waiting")]
|
|
344
355
|
: agentState(agent, now, paint, herdrSession);
|
|
345
356
|
const took = duration((turn.settledAt ?? now) - turn.startedAt);
|
|
346
357
|
const label = turn.label ?? (turn.kind === "turn" ? undefined : turn.kind);
|
|
@@ -375,7 +386,8 @@ function agentState(
|
|
|
375
386
|
const queued = agent.queued ? `${agent.queued} queued` : "";
|
|
376
387
|
return [paint.busy(spin(now)), [note, queued].filter(Boolean).join(" · ")];
|
|
377
388
|
}
|
|
378
|
-
if (turn.outcome === "answered")
|
|
389
|
+
if (turn.outcome === "answered")
|
|
390
|
+
return [paint.ok("✓"), agent.liveUntil ? paint.dim(liveNote(agent.liveUntil)) : ""];
|
|
379
391
|
return [
|
|
380
392
|
paint.bad("✗"),
|
|
381
393
|
paint.bad(
|
|
@@ -465,6 +477,13 @@ function isFailed(agent: Agent): boolean {
|
|
|
465
477
|
return agent.turn.outcome !== undefined && agent.turn.outcome !== "answered";
|
|
466
478
|
}
|
|
467
479
|
|
|
480
|
+
/** Kept alive: the run stays live, rather than parked, to this agent's window's end. */
|
|
481
|
+
function liveNote(until: number): string {
|
|
482
|
+
const date = new Date(until);
|
|
483
|
+
const two = (n: number) => String(n).padStart(2, "0");
|
|
484
|
+
return `live until ${two(date.getHours())}:${two(date.getMinutes())}`;
|
|
485
|
+
}
|
|
486
|
+
|
|
468
487
|
function spin(now: number): string {
|
|
469
488
|
return SPINNER[Math.floor(now / 100) % SPINNER.length]!;
|
|
470
489
|
}
|
|
@@ -504,6 +523,10 @@ export function watchProgress(
|
|
|
504
523
|
// nothing is said under the closing block.
|
|
505
524
|
let stopped = false;
|
|
506
525
|
let drawn = 0;
|
|
526
|
+
/** While a prompt waits for a line: the block is not drawn, and lines go above the prompt. */
|
|
527
|
+
let above: ((line: string) => void) | undefined;
|
|
528
|
+
/** Messages another view shows already, which get no line here. */
|
|
529
|
+
let shown: (message: WorkflowRunSnapshot["messages"][number]) => boolean = () => false;
|
|
507
530
|
const clear = () => {
|
|
508
531
|
if (terminal && drawn > 0) terminal.write(`\x1b[${drawn}F\x1b[0J`);
|
|
509
532
|
drawn = 0;
|
|
@@ -513,7 +536,14 @@ export function watchProgress(
|
|
|
513
536
|
const snapshot = handle.inspect();
|
|
514
537
|
if (terminal) {
|
|
515
538
|
// A message is an event: a line above the block, as a log line is.
|
|
516
|
-
const sent = snapshot.messages
|
|
539
|
+
const sent = snapshot.messages
|
|
540
|
+
.slice(last?.messages.length ?? 0)
|
|
541
|
+
.filter((message) => !shown(message));
|
|
542
|
+
if (above && !final) {
|
|
543
|
+
for (const message of sent) above(messageLine(message));
|
|
544
|
+
last = snapshot;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
517
547
|
if (sent.length > 0) {
|
|
518
548
|
clear();
|
|
519
549
|
for (const message of sent) stderr(messageLine(message));
|
|
@@ -533,7 +563,7 @@ export function watchProgress(
|
|
|
533
563
|
if (lines.length > 0) terminal.write(`${lines.join("\n")}\n`);
|
|
534
564
|
drawn = lines.length;
|
|
535
565
|
} else {
|
|
536
|
-
const view = { startedAt, now: now(), ...(herdrSession ? { herdrSession } : {}) };
|
|
566
|
+
const view = { startedAt, now: now(), shown, ...(herdrSession ? { herdrSession } : {}) };
|
|
537
567
|
for (const line of progressEvents(last, snapshot, view)) stderr(line);
|
|
538
568
|
}
|
|
539
569
|
last = snapshot;
|
|
@@ -548,10 +578,22 @@ export function watchProgress(
|
|
|
548
578
|
},
|
|
549
579
|
log(text: string) {
|
|
550
580
|
if (stopped) return;
|
|
581
|
+
if (above) return above(text);
|
|
551
582
|
clear();
|
|
552
583
|
stderr(text);
|
|
553
584
|
if (terminal) tick();
|
|
554
585
|
},
|
|
586
|
+
/** Messages the prompt shows itself, with their text: no size line for them here. */
|
|
587
|
+
showsMessages(by: (message: WorkflowRunSnapshot["messages"][number]) => boolean) {
|
|
588
|
+
shown = by;
|
|
589
|
+
},
|
|
590
|
+
/** A prompt opened, writing lines through `under`, or closed with none. */
|
|
591
|
+
hold(under: ((line: string) => void) | undefined) {
|
|
592
|
+
if (stopped || !terminal) return;
|
|
593
|
+
above = under;
|
|
594
|
+
if (under) clear();
|
|
595
|
+
else tick();
|
|
596
|
+
},
|
|
555
597
|
/** The Herdr session pane agents opened in, which a blocked one's line names. */
|
|
556
598
|
placedIn(session: string) {
|
|
557
599
|
if (!stopped) herdrSession = session;
|
package/src/projects.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { realPath } from "./files";
|
|
4
|
+
import { appendLine } from "./jsonl";
|
|
5
|
+
import { withRunLock } from "./run-lock";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The project a workflow with `events` runs in: the nearest directory at or above its file that
|
|
9
|
+
* holds `.awf/`, or else the file's own directory. Every sender names the file, so every sender,
|
|
10
|
+
* wherever it runs, finds the same runs (Q15). `{home}/.awf` is awf's own, not a project's.
|
|
11
|
+
*/
|
|
12
|
+
export async function projectOf(file: string, home: string): Promise<string> {
|
|
13
|
+
// The real path, so a sender naming the file through a link finds the same runs.
|
|
14
|
+
const start = realPath(dirname(resolve(file)));
|
|
15
|
+
const ownHome = realPath(home);
|
|
16
|
+
if (start === ownHome)
|
|
17
|
+
throw new Error(
|
|
18
|
+
`${file} is directly in home, where awf keeps its own .awf: move it into a directory`,
|
|
19
|
+
);
|
|
20
|
+
for (let dir = start; ; dir = dirname(dir)) {
|
|
21
|
+
if (dir === ownHome) return start;
|
|
22
|
+
const found = await stat(join(dir, ".awf")).then(
|
|
23
|
+
(info) => info.isDirectory(),
|
|
24
|
+
() => false,
|
|
25
|
+
);
|
|
26
|
+
if (found) return dir;
|
|
27
|
+
if (dirname(dir) === dir) return start;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Where a project's runs are kept. */
|
|
32
|
+
export function projectRunRoot(project: string): string {
|
|
33
|
+
return join(project, ".awf", "runs");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `~/.awf/projects`: each project awf has started an event run in, one absolute path a line. */
|
|
37
|
+
export function projectsFile(home: string): string {
|
|
38
|
+
return join(home, ".awf", "projects");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The projects awf has started an event run in, each once, in the order first started. */
|
|
42
|
+
export async function listProjects(home: string): Promise<string[]> {
|
|
43
|
+
const text = await Bun.file(projectsFile(home))
|
|
44
|
+
.text()
|
|
45
|
+
.catch(() => "");
|
|
46
|
+
return [...new Set(text.split("\n").filter((line) => line.startsWith("/")))];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Adds `project` to `~/.awf/projects`, once. */
|
|
50
|
+
export async function recordProject(home: string, project: string): Promise<void> {
|
|
51
|
+
const file = projectsFile(home);
|
|
52
|
+
if ((await listProjects(home)).includes(project)) return;
|
|
53
|
+
await withRunLock(`${file}.lock`, async () => {
|
|
54
|
+
if ((await listProjects(home)).includes(project)) return;
|
|
55
|
+
await appendLine(file, project);
|
|
56
|
+
});
|
|
57
|
+
}
|
package/src/result-slots.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { acceptAny, type SemanticCheck } from "@agentswf/contract";
|
|
2
2
|
import type { CandidateSource } from "@agentswf/contract/records";
|
|
3
3
|
import type { JsonSchema } from "@agentswf/contract/schema";
|
|
4
|
-
import type { ResultSubmitCode } from "@agentswf/contract/wire";
|
|
4
|
+
import type { GivenInput, ResultSubmitCode } from "@agentswf/contract/wire";
|
|
5
5
|
import type { AbsoluteDeadline } from "@agentswf/contract/workflow";
|
|
6
6
|
import { scheduleAt } from "./deadlines";
|
|
7
|
-
import {
|
|
7
|
+
import { evaluateGiven } from "./result-validation";
|
|
8
8
|
import { recordCandidate, writeAcceptedExclusive, writeCall } from "./runs";
|
|
9
9
|
|
|
10
10
|
export type ResultRejectionCode = ResultSubmitCode;
|
|
@@ -19,7 +19,7 @@ export type ResultSlotAccepted = {
|
|
|
19
19
|
};
|
|
20
20
|
type Rejected = { kind: "rejected"; code: ResultRejectionCode; error: string };
|
|
21
21
|
export type ResultSubmission = ResultSlotAccepted | Rejected;
|
|
22
|
-
export type WaitingGrant = { kind: "waiting"; waitUntil: number; deadline
|
|
22
|
+
export type WaitingGrant = { kind: "waiting"; waitUntil: number; deadline?: number };
|
|
23
23
|
export type ResultSlotSettlement =
|
|
24
24
|
| ResultSlotAccepted
|
|
25
25
|
| { kind: "closed" }
|
|
@@ -36,7 +36,8 @@ export type ResultSlotSpec = {
|
|
|
36
36
|
question: string;
|
|
37
37
|
schema?: JsonSchema;
|
|
38
38
|
semantic?: SemanticCheck;
|
|
39
|
-
|
|
39
|
+
/** Absent when nothing bounds the operation. */
|
|
40
|
+
deadline?: AbsoluteDeadline;
|
|
40
41
|
allowWaiting?: boolean;
|
|
41
42
|
onEvent?(event: ResultSlotEvent): void;
|
|
42
43
|
};
|
|
@@ -58,9 +59,8 @@ interface ResultSlotPersistence {
|
|
|
58
59
|
type SubmissionInput = {
|
|
59
60
|
operationId: string;
|
|
60
61
|
agentId: string;
|
|
61
|
-
raw: string;
|
|
62
62
|
source: CandidateSource;
|
|
63
|
-
};
|
|
63
|
+
} & GivenInput;
|
|
64
64
|
export interface ResultSlotRegistry {
|
|
65
65
|
open(spec: ResultSlotSpec): Promise<ResultSlotBinding>;
|
|
66
66
|
submit(input: SubmissionInput): Promise<ResultSubmission>;
|
|
@@ -71,6 +71,8 @@ export interface ResultSlotRegistry {
|
|
|
71
71
|
timeoutMs?: number;
|
|
72
72
|
}): Promise<WaitingGrant | Rejected>;
|
|
73
73
|
close(operationId: string): Promise<boolean>;
|
|
74
|
+
/** Its operation's deadline moved, out by a wait that did not count, or to none. */
|
|
75
|
+
extend(operationId: string, deadline: AbsoluteDeadline | undefined): void;
|
|
74
76
|
}
|
|
75
77
|
type Slot = ResultSlotSpec & {
|
|
76
78
|
state: "open" | "committing" | "accepted" | "closed" | "expired" | "failed";
|
|
@@ -107,7 +109,8 @@ export function createResultSlotRegistry(options: ResultSlotRegistryOptions): Re
|
|
|
107
109
|
persistence.recordCandidate(options.runDir, slot.operationId, {
|
|
108
110
|
at: new Date(now()).toISOString(),
|
|
109
111
|
source: input.source,
|
|
110
|
-
|
|
112
|
+
// Flags as given, whether or not they convert, so the record shows what the agent typed.
|
|
113
|
+
raw: input.raw ?? JSON.stringify(input.args ?? []),
|
|
111
114
|
accepted,
|
|
112
115
|
...(error === undefined ? {} : { error }),
|
|
113
116
|
}),
|
|
@@ -160,13 +163,7 @@ export function createResultSlotRegistry(options: ResultSlotRegistryOptions): Re
|
|
|
160
163
|
} else slots.delete(spec.operationId);
|
|
161
164
|
throw error;
|
|
162
165
|
}
|
|
163
|
-
if (slot.state === "open")
|
|
164
|
-
if (now() >= spec.deadline.unixMilliseconds) closeSlot(slot, "expired");
|
|
165
|
-
else
|
|
166
|
-
slot.cancelExpiry = schedule(spec.deadline.unixMilliseconds - now(), () => {
|
|
167
|
-
if (slot.state === "open") closeSlot(slot, "expired");
|
|
168
|
-
});
|
|
169
|
-
}
|
|
166
|
+
if (slot.state === "open") expireAt(slot);
|
|
170
167
|
return { operationId: spec.operationId, settled };
|
|
171
168
|
},
|
|
172
169
|
|
|
@@ -190,7 +187,7 @@ export function createResultSlotRegistry(options: ResultSlotRegistryOptions): Re
|
|
|
190
187
|
)
|
|
191
188
|
return rejected("invalid-request", "waiting timeout must be a positive safe integer");
|
|
192
189
|
const at = now();
|
|
193
|
-
const remaining = slot.deadline
|
|
190
|
+
const remaining = (slot.deadline?.unixMilliseconds ?? Number.POSITIVE_INFINITY) - at;
|
|
194
191
|
if (remaining <= 0) {
|
|
195
192
|
closeSlot(slot, "expired");
|
|
196
193
|
return rejected("expired-operation");
|
|
@@ -204,7 +201,11 @@ export function createResultSlotRegistry(options: ResultSlotRegistryOptions): Re
|
|
|
204
201
|
at,
|
|
205
202
|
revision: ++slot.revision,
|
|
206
203
|
});
|
|
207
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
kind: "waiting",
|
|
206
|
+
waitUntil,
|
|
207
|
+
...(slot.deadline ? { deadline: slot.deadline.unixMilliseconds } : {}),
|
|
208
|
+
};
|
|
208
209
|
},
|
|
209
210
|
|
|
210
211
|
async submit(input) {
|
|
@@ -213,7 +214,7 @@ export function createResultSlotRegistry(options: ResultSlotRegistryOptions): Re
|
|
|
213
214
|
if (input.agentId !== slot.agentId) return rejected("wrong-agent");
|
|
214
215
|
const unavailable = unavailableCode(slot, now());
|
|
215
216
|
if (unavailable) return rejectKnown(slot, input, unavailable);
|
|
216
|
-
const evaluated = await
|
|
217
|
+
const evaluated = await evaluateGiven(slot, input, slot.semantic ?? acceptAny);
|
|
217
218
|
const admittedAt = now();
|
|
218
219
|
const late = unavailableCode(slot, admittedAt);
|
|
219
220
|
if (late) return rejectKnown(slot, input, late);
|
|
@@ -281,16 +282,39 @@ export function createResultSlotRegistry(options: ResultSlotRegistryOptions): Re
|
|
|
281
282
|
async close(operationId) {
|
|
282
283
|
const slot = slots.get(operationId);
|
|
283
284
|
if (!slot || (slot.state !== "open" && slot.state !== "committing")) return false;
|
|
284
|
-
closeSlot(
|
|
285
|
+
closeSlot(
|
|
286
|
+
slot,
|
|
287
|
+
slot.deadline && now() >= slot.deadline.unixMilliseconds ? "expired" : "closed",
|
|
288
|
+
);
|
|
285
289
|
return true;
|
|
286
290
|
},
|
|
291
|
+
|
|
292
|
+
extend(operationId, deadline) {
|
|
293
|
+
const slot = slots.get(operationId);
|
|
294
|
+
if (slot?.state !== "open") return;
|
|
295
|
+
slot.cancelExpiry();
|
|
296
|
+
slot.cancelExpiry = () => {};
|
|
297
|
+
if (deadline) slot.deadline = deadline;
|
|
298
|
+
else delete slot.deadline;
|
|
299
|
+
expireAt(slot);
|
|
300
|
+
},
|
|
287
301
|
};
|
|
302
|
+
|
|
303
|
+
function expireAt(slot: Slot): void {
|
|
304
|
+
const { deadline } = slot;
|
|
305
|
+
if (!deadline) return;
|
|
306
|
+
if (now() >= deadline.unixMilliseconds) closeSlot(slot, "expired");
|
|
307
|
+
else
|
|
308
|
+
slot.cancelExpiry = schedule(deadline.unixMilliseconds - now(), () => {
|
|
309
|
+
if (slot.state === "open") closeSlot(slot, "expired");
|
|
310
|
+
});
|
|
311
|
+
}
|
|
288
312
|
}
|
|
289
313
|
|
|
290
314
|
function unavailableCode(slot: Slot, now: number): ResultRejectionCode | null {
|
|
291
315
|
if (slot.state === "expired") return "expired-operation";
|
|
292
316
|
if (slot.state !== "open") return "closed-operation";
|
|
293
|
-
if (now >= slot.deadline.unixMilliseconds) {
|
|
317
|
+
if (slot.deadline && now >= slot.deadline.unixMilliseconds) {
|
|
294
318
|
closeSlot(slot, "expired");
|
|
295
319
|
return "expired-operation";
|
|
296
320
|
}
|
package/src/result-validation.ts
CHANGED
|
@@ -1,37 +1,32 @@
|
|
|
1
1
|
import { acceptAny, type SemanticCheck } from "@agentswf/contract";
|
|
2
2
|
import type { CallSpec } from "@agentswf/contract/records";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import type { GivenInput } from "@agentswf/contract/wire";
|
|
4
|
+
import { checkGiven, readGiven } from "./simple-input";
|
|
5
5
|
|
|
6
6
|
export type CandidateEvaluation =
|
|
7
7
|
| { kind: "accepted"; value: unknown }
|
|
8
8
|
| { kind: "rejected"; error: string };
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A result as `wf` sent it, validated without repairing or persisting; the result-slot transition
|
|
12
|
+
* owns acceptance.
|
|
13
|
+
*/
|
|
14
|
+
export async function evaluateGiven(
|
|
12
15
|
call: Pick<CallSpec, "question" | "schema">,
|
|
13
|
-
|
|
16
|
+
given: GivenInput,
|
|
14
17
|
semantic: SemanticCheck = acceptAny,
|
|
15
18
|
): Promise<CandidateEvaluation> {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
error: [
|
|
24
|
-
`the value is not valid JSON: ${reason}`,
|
|
25
|
-
`expected: ${expectedShape(call)}`,
|
|
26
|
-
"send one JSON value, quoted as a single shell argument.",
|
|
27
|
-
].join("\n"),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (call.schema) {
|
|
32
|
-
const errors = validate(call.schema, value);
|
|
33
|
-
if (errors.length > 0) return { kind: "rejected", error: formatErrors(errors) };
|
|
19
|
+
const read = readGiven(call.schema, given);
|
|
20
|
+
if (!read.ok) {
|
|
21
|
+
const advice =
|
|
22
|
+
given.raw === undefined
|
|
23
|
+
? ""
|
|
24
|
+
: "\nsend one JSON value in a quoted heredoc, as the request shows.";
|
|
25
|
+
return { kind: "rejected", error: `${read.error}${advice}` };
|
|
34
26
|
}
|
|
27
|
+
const value = read.value;
|
|
28
|
+
const invalid = call.schema ? checkGiven(call.schema, given, value) : undefined;
|
|
29
|
+
if (invalid) return { kind: "rejected", error: invalid };
|
|
35
30
|
|
|
36
31
|
const verdict = await semantic({ question: call.question, value: structuredClone(value) });
|
|
37
32
|
return verdict.kind === "accepted"
|
|
@@ -41,7 +36,3 @@ export async function evaluateResult(
|
|
|
41
36
|
error: `the value does not answer what was asked: ${verdict.reason}`,
|
|
42
37
|
};
|
|
43
38
|
}
|
|
44
|
-
|
|
45
|
-
function expectedShape(call: Pick<CallSpec, "schema">): string {
|
|
46
|
-
return call.schema ? JSON.stringify(call.schema) : "any JSON value";
|
|
47
|
-
}
|
package/src/run-command.ts
CHANGED
|
@@ -6,30 +6,22 @@ import { messageOf } from "./errors";
|
|
|
6
6
|
import { SESSION_CODE } from "./here";
|
|
7
7
|
import { idProblem, runRootOf, stageNameProblem } from "./runs";
|
|
8
8
|
|
|
9
|
-
const DEFAULT_TIMEOUT = "1h";
|
|
10
|
-
|
|
11
9
|
export const usage = [
|
|
12
10
|
"usage: awf run [options] <workflow-file> [options] [-- workflow arguments...]",
|
|
13
|
-
" awf run [options] <workflow-file> --
|
|
14
|
-
"
|
|
15
|
-
" awf test [paths...] [-t <pattern>] [--watch] [--timeout <duration>]",
|
|
16
|
-
" awf allowance [harness...] [--json]",
|
|
17
|
-
" awf runs [<run>] [--json] (awf runs -h)",
|
|
18
|
-
" awf logs [<run>] [--after <cursor>] [--wait <seconds>] [--json] (awf logs -h)",
|
|
19
|
-
" awf --version",
|
|
20
|
-
"run options: --id <id>, --continue <id>, --from-stage <stage>, --values <file>,",
|
|
11
|
+
" awf run [options] <workflow-file> --id <id> --continue",
|
|
12
|
+
"run options: --id <id>, --continue, --from-stage <stage>, --values <file>,",
|
|
21
13
|
" --timeout <duration>, --run-root <directory>, --cwd <directory>, --sandbox <file>,",
|
|
22
14
|
" --json, --no-watch, --here",
|
|
23
15
|
"",
|
|
24
|
-
"Each awf run is an attempt of a run: a new run, with --id's id or a generated one, or
|
|
25
|
-
"--continue names, with the arguments, --cwd and --sandbox it was started with. A continue reuses",
|
|
16
|
+
"Each awf run is an attempt of a run: a new run, with --id's id or a generated one, or, with",
|
|
17
|
+
"--continue, the run --id names, with the arguments, --cwd and --sandbox it was started with. A continue reuses",
|
|
26
18
|
"the stages that succeeded, up to the first with no record or to --from-stage, and runs the rest.",
|
|
27
19
|
"A new run with --from-stage starts there. A stage before it with no record to reuse takes its",
|
|
28
20
|
"value from --values, a JSON object by stage name, checked by its result; one that returns",
|
|
29
21
|
"nothing is passed. Without one the attempt stops, naming the stage and its schema.",
|
|
30
22
|
"A stop exits 3. A run is kept in .awf/runs/<workflow>/<id> under --cwd, unless --run-root names",
|
|
31
23
|
"another folder for .awf/runs.",
|
|
32
|
-
"
|
|
24
|
+
"A run has no deadline unless --timeout gives it one, per attempt.",
|
|
33
25
|
"A workflow that knows how to present its result prints that; --json prints the full result instead.",
|
|
34
26
|
"Either way the full result is kept as output.json in the run's folder, beside report.md",
|
|
35
27
|
"when the workflow writes one. A run that fails or is cancelled once its agents have started keeps",
|
|
@@ -44,6 +36,10 @@ export const usage = [
|
|
|
44
36
|
"--here, run by an agent in a Herdr pane, starts the run in a new tab and has it take that agent's",
|
|
45
37
|
"session over as one of its agents, from its next turn until the run ends. It prints a line for",
|
|
46
38
|
"the agent to end its turn with, which is how the run finds the session.",
|
|
39
|
+
"A run of a workflow with events is named by --id, or by the workflow's id(args). awf run starts it",
|
|
40
|
+
"with its arguments and no event, detached in its project, as awf emit would, and the run waits at",
|
|
41
|
+
"its first receive. A run already there is only woken. A run an event starts has no argv: its args",
|
|
42
|
+
"are what prepare makes of none.",
|
|
47
43
|
"",
|
|
48
44
|
"Examples:",
|
|
49
45
|
" awf run examples/minimum-review/review-loop.ts",
|
|
@@ -65,13 +61,13 @@ export type RunCommand = {
|
|
|
65
61
|
runRootGiven: boolean;
|
|
66
62
|
workflowFile: string;
|
|
67
63
|
workflowArgs: string[];
|
|
68
|
-
/** As typed, for the attempt's record. */
|
|
69
|
-
timeout
|
|
70
|
-
timeoutMilliseconds
|
|
64
|
+
/** As typed, for the attempt's record; absent, the run has no deadline. */
|
|
65
|
+
timeout?: string;
|
|
66
|
+
timeoutMilliseconds?: number;
|
|
71
67
|
runRoot: string;
|
|
72
68
|
/** A new run's id, as `--id` gave it. */
|
|
73
69
|
id?: string;
|
|
74
|
-
/** The run `--continue` adds an attempt to. */
|
|
70
|
+
/** The run `--id` names, which `--continue` adds an attempt to. */
|
|
75
71
|
continueId?: string;
|
|
76
72
|
/** The stage the attempt starts at, reusing those before it. */
|
|
77
73
|
fromStage?: string;
|
|
@@ -88,13 +84,17 @@ export type RunCommand = {
|
|
|
88
84
|
session?: string;
|
|
89
85
|
};
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
/** `awf run`'s options and workflow; `"help"` for a `-h` or `--help` anywhere before `--`. */
|
|
88
|
+
export function parseCommand(argv: readonly string[], cwd: string): RunCommand | "help" {
|
|
92
89
|
if (argv[0] !== "run") throw new Error("expected the run command");
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const end = argv.indexOf("--");
|
|
91
|
+
const own = end < 0 ? argv : argv.slice(0, end);
|
|
92
|
+
if (own.includes("-h") || own.includes("--help")) return "help";
|
|
93
|
+
let timeout: string | undefined;
|
|
94
|
+
let timeoutMilliseconds: number | undefined;
|
|
95
95
|
let runRoot: string | undefined;
|
|
96
|
-
let
|
|
97
|
-
let
|
|
96
|
+
let named: string | undefined;
|
|
97
|
+
let continuing = false;
|
|
98
98
|
let fromStage: string | undefined;
|
|
99
99
|
let values: RunCommand["values"];
|
|
100
100
|
let cwdGiven = false;
|
|
@@ -126,6 +126,10 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand {
|
|
|
126
126
|
here = true;
|
|
127
127
|
continue;
|
|
128
128
|
}
|
|
129
|
+
if (option === "--continue") {
|
|
130
|
+
continuing = true;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
129
133
|
const value = argv[index + 1];
|
|
130
134
|
if (option === "--timeout") {
|
|
131
135
|
if (!value) throw new Error("--timeout needs a duration such as 30m");
|
|
@@ -141,12 +145,11 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand {
|
|
|
141
145
|
if (!statSync(workCwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
142
146
|
throw new Error(`--cwd: not a directory: ${workCwd}`);
|
|
143
147
|
}
|
|
144
|
-
} else if (option === "--id"
|
|
145
|
-
if (!value) throw new Error(
|
|
148
|
+
} else if (option === "--id") {
|
|
149
|
+
if (!value) throw new Error("--id needs a run's id");
|
|
146
150
|
const problem = idProblem(value);
|
|
147
|
-
if (problem) throw new Error(
|
|
148
|
-
|
|
149
|
-
else continueId = value;
|
|
151
|
+
if (problem) throw new Error(`--id: ${problem}`);
|
|
152
|
+
named = value;
|
|
150
153
|
} else if (option === "--from-stage") {
|
|
151
154
|
if (!value) throw new Error("--from-stage needs a stage's name");
|
|
152
155
|
const problem = stageNameProblem(value);
|
|
@@ -171,9 +174,10 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand {
|
|
|
171
174
|
}
|
|
172
175
|
if (!workflowFile) throw new Error("run needs one workflow file");
|
|
173
176
|
if (here && session !== undefined) throw new Error("--here and --session do not go together");
|
|
174
|
-
if (
|
|
175
|
-
throw new Error("--
|
|
176
|
-
|
|
177
|
+
if (continuing && named === undefined)
|
|
178
|
+
throw new Error("--continue goes on with the run --id names");
|
|
179
|
+
const id = continuing ? undefined : named;
|
|
180
|
+
const continueId = continuing ? named : undefined;
|
|
177
181
|
if (values !== undefined && fromStage === undefined) {
|
|
178
182
|
throw new Error("--values goes with --from-stage: it gives the stages before it");
|
|
179
183
|
}
|
|
@@ -185,8 +189,8 @@ export function parseCommand(argv: readonly string[], cwd: string): RunCommand {
|
|
|
185
189
|
runRootGiven: runRoot !== undefined,
|
|
186
190
|
workflowFile,
|
|
187
191
|
workflowArgs,
|
|
188
|
-
timeout,
|
|
189
|
-
timeoutMilliseconds,
|
|
192
|
+
...(timeout === undefined ? {} : { timeout }),
|
|
193
|
+
...(timeoutMilliseconds === undefined ? {} : { timeoutMilliseconds }),
|
|
190
194
|
runRoot: runRoot ?? runRootOf(workCwd),
|
|
191
195
|
...(id === undefined ? {} : { id }),
|
|
192
196
|
...(continueId === undefined ? {} : { continueId }),
|
package/src/run-commands.ts
CHANGED
|
@@ -26,6 +26,11 @@ export function parsePlace(
|
|
|
26
26
|
const rest: string[] = [];
|
|
27
27
|
for (let index = 0; index < argv.length; index += 1) {
|
|
28
28
|
const arg = argv[index]!;
|
|
29
|
+
// What follows is the command's own, a message's text included.
|
|
30
|
+
if (arg === "--") {
|
|
31
|
+
rest.push(...argv.slice(index));
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
29
34
|
if (arg === "--json") json = true;
|
|
30
35
|
else if (arg === "--cwd" || arg === "--run-root") {
|
|
31
36
|
const value = argv[index + 1];
|
|
@@ -106,11 +111,17 @@ export function startedLine(
|
|
|
106
111
|
run: Pick<RunCommand, "cwd" | "cwdGiven" | "runRoot" | "runRootGiven">,
|
|
107
112
|
): string {
|
|
108
113
|
const ref = `${workflow}/${id}`;
|
|
109
|
-
|
|
114
|
+
return `awf: ${ref} started · watch: ${shellLine(["awf", "logs", ref, ...placeFlags(run)])}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The flags that find this run again from another command: the folder or run root given. */
|
|
118
|
+
export function placeFlags(
|
|
119
|
+
run: Pick<RunCommand, "cwd" | "cwdGiven" | "runRoot" | "runRootGiven">,
|
|
120
|
+
): string[] {
|
|
121
|
+
return [
|
|
110
122
|
...(run.cwdGiven ? ["--cwd", run.cwd] : []),
|
|
111
123
|
...(run.runRootGiven ? ["--run-root", run.runRoot] : []),
|
|
112
124
|
];
|
|
113
|
-
return `awf: ${ref} started · watch: ${shellLine(["awf", "logs", ref, ...repeat])}`;
|
|
114
125
|
}
|
|
115
126
|
|
|
116
127
|
/** `awf logs` from `cursor`: with `--wait` unless the page stopped short of the newest. */
|