@agentswf/engine 0.1.0-beta.5 → 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 +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- 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 +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- 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 +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- 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 +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- 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 +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- 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 +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- 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 +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- 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 +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- 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 +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
4
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
type DecisionArtifact,
|
|
5
|
+
type Money,
|
|
6
|
+
OP_FILE_VERSIONS,
|
|
7
|
+
type SettledDecision,
|
|
8
|
+
} from "@agentswf/contract/records";
|
|
5
9
|
import {
|
|
6
10
|
type AbsoluteDeadline,
|
|
7
11
|
type Answers,
|
|
@@ -16,6 +20,8 @@ import {
|
|
|
16
20
|
import { canonical } from "../canonical-json";
|
|
17
21
|
import { abortableSleep, assertDeadline, boundedDeadline, scheduleAt } from "../deadlines";
|
|
18
22
|
import { messageOf } from "../errors";
|
|
23
|
+
import { writeJson } from "../files";
|
|
24
|
+
import { decisionArtifact, type OpIds } from "../ops";
|
|
19
25
|
import type { RunEventBody } from "../run-event-log";
|
|
20
26
|
import {
|
|
21
27
|
type DecisionInstallation,
|
|
@@ -35,8 +41,6 @@ export type DecisionScope = {
|
|
|
35
41
|
add?(cancel: (reason: string) => Promise<unknown>): () => void;
|
|
36
42
|
/** The workflow stage it is asked in. */
|
|
37
43
|
stage?: string;
|
|
38
|
-
/** The spec gave a deprecated form of `timeout`. */
|
|
39
|
-
deprecated?(name: "timeout-ms" | "deadline-option", call: string): void;
|
|
40
44
|
};
|
|
41
45
|
|
|
42
46
|
type PreparedCall = {
|
|
@@ -66,6 +70,8 @@ export class RunDecisions {
|
|
|
66
70
|
private readonly options: {
|
|
67
71
|
installation?: DecisionInstallation;
|
|
68
72
|
runDir: string;
|
|
73
|
+
/** Numbers each asking, `{key}.{n}`, and keeps the keys apart from agents'. */
|
|
74
|
+
ops: OpIds;
|
|
69
75
|
sleep?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
|
|
70
76
|
/** Writes the run's event log. */
|
|
71
77
|
event?(body: RunEventBody): void;
|
|
@@ -129,24 +135,12 @@ export class RunDecisions {
|
|
|
129
135
|
? installed.providers[resolved.provider]
|
|
130
136
|
: undefined;
|
|
131
137
|
if (!provider) throw new Error(`decision provider "${resolved.provider}" is not installed`);
|
|
132
|
-
const deadline = boundedDeadline(
|
|
133
|
-
{ timeout: spec.timeout, deadline: spec.deadline },
|
|
134
|
-
scope.deadline,
|
|
135
|
-
(name, call) => scope.deprecated?.(name, call),
|
|
136
|
-
"on decide",
|
|
137
|
-
);
|
|
138
|
+
const deadline = boundedDeadline(spec.timeout, scope.deadline);
|
|
138
139
|
assertDeadline(deadline);
|
|
140
|
+
// Last: a decision refused for anything else holds no key.
|
|
141
|
+
this.options.ops.claim(spec.key, "decision");
|
|
139
142
|
const slot = this.#slots.length;
|
|
140
143
|
this.#slots.push(undefined);
|
|
141
|
-
this.options.event?.({
|
|
142
|
-
kind: "decision-asked",
|
|
143
|
-
decision: slot + 1,
|
|
144
|
-
...(scope.stage === undefined ? {} : { stage: scope.stage }),
|
|
145
|
-
key: spec.key,
|
|
146
|
-
alias: spec.model,
|
|
147
|
-
provider: resolved.provider,
|
|
148
|
-
model: resolved.model,
|
|
149
|
-
});
|
|
150
144
|
return { spec, ids, resolved, provider, deadline, slot };
|
|
151
145
|
}
|
|
152
146
|
|
|
@@ -155,6 +149,12 @@ export class RunDecisions {
|
|
|
155
149
|
scope: DecisionScope,
|
|
156
150
|
): Promise<{ answers: Record<string, unknown>; record: DecisionRecord }> {
|
|
157
151
|
const { spec, ids, resolved, provider, deadline, slot } = call;
|
|
152
|
+
const { runDir, ops } = this.options;
|
|
153
|
+
// Its folder and op.json are written as its id is drawn, before any line names it.
|
|
154
|
+
const { id: operation, lost: opLost } = ops.draw(spec.key, {
|
|
155
|
+
kind: "decision",
|
|
156
|
+
...(scope.stage === undefined ? {} : { stage: scope.stage }),
|
|
157
|
+
});
|
|
158
158
|
const startedAt = new Date().toISOString();
|
|
159
159
|
const controller = new AbortController();
|
|
160
160
|
let ending: Ending | undefined;
|
|
@@ -168,17 +168,27 @@ export class RunDecisions {
|
|
|
168
168
|
: () => undefined;
|
|
169
169
|
this.#cancellers.add(cancel);
|
|
170
170
|
const removeFromScope = scope.add?.(async (reason) => cancel(reason));
|
|
171
|
+
this.options.event?.({
|
|
172
|
+
kind: "decision-asked",
|
|
173
|
+
decision: slot + 1,
|
|
174
|
+
operation,
|
|
175
|
+
...(scope.stage === undefined ? {} : { stage: scope.stage }),
|
|
176
|
+
key: spec.key,
|
|
177
|
+
alias: spec.model,
|
|
178
|
+
provider: resolved.provider,
|
|
179
|
+
model: resolved.model,
|
|
180
|
+
});
|
|
171
181
|
|
|
172
182
|
let attempts = 0;
|
|
173
183
|
let tokens: { input: number; output: number } | undefined;
|
|
174
|
-
let
|
|
184
|
+
let reportedCosts: Money[] = [];
|
|
175
185
|
let requestId: string | undefined;
|
|
176
186
|
let response: ProviderResponse | undefined;
|
|
177
187
|
let answers: Record<string, unknown> | undefined;
|
|
178
188
|
let error: string | undefined;
|
|
179
189
|
const count = (spent: {
|
|
180
190
|
tokens?: ProviderResponse["tokens"];
|
|
181
|
-
|
|
191
|
+
reportedCost?: Money;
|
|
182
192
|
requestId?: string;
|
|
183
193
|
}) => {
|
|
184
194
|
if (spent.tokens) {
|
|
@@ -187,7 +197,7 @@ export class RunDecisions {
|
|
|
187
197
|
output: (tokens?.output ?? 0) + spent.tokens.output,
|
|
188
198
|
};
|
|
189
199
|
}
|
|
190
|
-
if (spent.
|
|
200
|
+
if (spent.reportedCost) reportedCosts = [...reportedCosts, spent.reportedCost];
|
|
191
201
|
if (spent.requestId) requestId = spent.requestId;
|
|
192
202
|
};
|
|
193
203
|
const request = {
|
|
@@ -230,7 +240,7 @@ export class RunDecisions {
|
|
|
230
240
|
this.#cancellers.delete(cancel);
|
|
231
241
|
}
|
|
232
242
|
|
|
233
|
-
const artifact =
|
|
243
|
+
const artifact = decisionArtifact(operation);
|
|
234
244
|
const outcome: DecisionRecord["outcome"] = ending
|
|
235
245
|
? ending.outcome
|
|
236
246
|
: answers
|
|
@@ -261,19 +271,21 @@ export class RunDecisions {
|
|
|
261
271
|
...(requestId ? { requestId } : {}),
|
|
262
272
|
artifact,
|
|
263
273
|
};
|
|
264
|
-
const total = sumMoney(
|
|
265
|
-
let settled: SettledDecision = { ...record, ...(total ? {
|
|
274
|
+
const total = sumMoney(reportedCosts);
|
|
275
|
+
let settled: SettledDecision = { ...record, ...(total ? { reportedCost: total } : {}) };
|
|
266
276
|
const written: DecisionArtifact = {
|
|
277
|
+
version: OP_FILE_VERSIONS.decision,
|
|
267
278
|
record: settled,
|
|
268
279
|
request: { state: spec.state, questions: spec.questions },
|
|
269
280
|
...(answers ? { answers: answers as JsonObject } : {}),
|
|
270
281
|
};
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
282
|
+
const artifactLost = await writeJson(join(runDir, artifact), written).then(
|
|
283
|
+
() => undefined,
|
|
284
|
+
(thrown: unknown) => `artifact not written: ${messageOf(thrown)}`,
|
|
285
|
+
);
|
|
286
|
+
if (opLost !== undefined || artifactLost !== undefined) {
|
|
287
|
+
const lost = [opLost, artifactLost].filter((each) => each !== undefined).join("; ");
|
|
288
|
+
// Without its records an answer cannot be fitted again, so it does not count as one; any
|
|
277
289
|
// other outcome stands.
|
|
278
290
|
settled =
|
|
279
291
|
settled.outcome === "answered"
|
|
@@ -284,11 +296,12 @@ export class RunDecisions {
|
|
|
284
296
|
this.options.event?.({
|
|
285
297
|
kind: "decision-ended",
|
|
286
298
|
decision: slot + 1,
|
|
299
|
+
operation,
|
|
287
300
|
...(settled.stage === undefined ? {} : { stage: settled.stage }),
|
|
288
301
|
outcome: settled.outcome,
|
|
289
302
|
...(settled.error === undefined ? {} : { error: settled.error }),
|
|
290
303
|
});
|
|
291
|
-
const {
|
|
304
|
+
const { reportedCost: _reportedCost, ...returned } = settled;
|
|
292
305
|
|
|
293
306
|
if (returned.outcome === "answered") return { answers: answers!, record: returned };
|
|
294
307
|
if (returned.outcome === "timed-out") throw new DeadlineExceededError(deadline!);
|
|
@@ -406,11 +419,11 @@ function isProbability(value: unknown): value is number {
|
|
|
406
419
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
407
420
|
}
|
|
408
421
|
|
|
409
|
-
/**
|
|
410
|
-
function sumMoney(
|
|
411
|
-
const currency =
|
|
412
|
-
if (!currency ||
|
|
413
|
-
return { amount:
|
|
422
|
+
/** Costs in more than one currency cannot be added without converting, so their sum is unknown. */
|
|
423
|
+
function sumMoney(costs: readonly Money[]): Money | undefined {
|
|
424
|
+
const currency = costs[0]?.currency;
|
|
425
|
+
if (!currency || costs.some((cost) => cost.currency !== currency)) return undefined;
|
|
426
|
+
return { amount: costs.reduce((sum, cost) => sum + cost.amount, 0), currency };
|
|
414
427
|
}
|
|
415
428
|
|
|
416
429
|
function untilAborted<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
package/src/decisions/fake.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function confidentResponse(request: ProviderRequest): ProviderResponse {
|
|
|
36
36
|
snapshot: `${request.model}-20260926`,
|
|
37
37
|
answers,
|
|
38
38
|
tokens: { input: 1_000, output: 10 },
|
|
39
|
-
|
|
39
|
+
reportedCost: { amount: 0.000042, currency: "USD" },
|
|
40
40
|
requestId: `request-${Object.keys(request.questions).join("-")}`,
|
|
41
41
|
};
|
|
42
42
|
}
|
|
@@ -130,7 +130,7 @@ function levelsOf(question: Question, probabilities: Record<string, unknown>): n
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/** What a body says was spent, as far as it says: tokens only when both counts are reported. */
|
|
133
|
-
function spendOf(body: unknown): Pick<ProviderResponse, "tokens" | "
|
|
133
|
+
function spendOf(body: unknown): Pick<ProviderResponse, "tokens" | "reportedCost" | "requestId"> {
|
|
134
134
|
if (!isRecord(body)) return {};
|
|
135
135
|
const usage = isRecord(body.usage) ? body.usage : {};
|
|
136
136
|
const counted = typeof usage.input_tokens === "number" && typeof usage.output_tokens === "number";
|
|
@@ -138,7 +138,9 @@ function spendOf(body: unknown): Pick<ProviderResponse, "tokens" | "charged" | "
|
|
|
138
138
|
...(counted
|
|
139
139
|
? { tokens: { input: usage.input_tokens as number, output: usage.output_tokens as number } }
|
|
140
140
|
: {}),
|
|
141
|
-
...(typeof usage.cost === "number"
|
|
141
|
+
...(typeof usage.cost === "number"
|
|
142
|
+
? { reportedCost: { amount: usage.cost, currency: "USD" } }
|
|
143
|
+
: {}),
|
|
142
144
|
...(typeof body.id === "string" ? { requestId: body.id } : {}),
|
|
143
145
|
};
|
|
144
146
|
}
|
package/src/decisions/seam.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type ProviderResponse = {
|
|
|
24
24
|
answers: Record<string, ProviderAnswer>;
|
|
25
25
|
/** Absent when the provider did not report them: unknown, not zero. */
|
|
26
26
|
tokens?: { input: number; output: number };
|
|
27
|
-
|
|
27
|
+
reportedCost?: Money;
|
|
28
28
|
requestId?: string;
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -40,7 +40,7 @@ export interface DecisionProvider {
|
|
|
40
40
|
export class DecisionProviderError extends Error {
|
|
41
41
|
readonly retryable: boolean;
|
|
42
42
|
readonly tokens?: { input: number; output: number };
|
|
43
|
-
readonly
|
|
43
|
+
readonly reportedCost?: Money;
|
|
44
44
|
readonly requestId?: string;
|
|
45
45
|
|
|
46
46
|
constructor(
|
|
@@ -48,7 +48,7 @@ export class DecisionProviderError extends Error {
|
|
|
48
48
|
options: {
|
|
49
49
|
retryable: boolean;
|
|
50
50
|
tokens?: { input: number; output: number };
|
|
51
|
-
|
|
51
|
+
reportedCost?: Money;
|
|
52
52
|
requestId?: string;
|
|
53
53
|
},
|
|
54
54
|
) {
|
|
@@ -56,7 +56,7 @@ export class DecisionProviderError extends Error {
|
|
|
56
56
|
this.name = "DecisionProviderError";
|
|
57
57
|
this.retryable = options.retryable;
|
|
58
58
|
if (options.tokens) this.tokens = options.tokens;
|
|
59
|
-
if (options.
|
|
59
|
+
if (options.reportedCost) this.reportedCost = options.reportedCost;
|
|
60
60
|
if (options.requestId) this.requestId = options.requestId;
|
|
61
61
|
}
|
|
62
62
|
}
|
package/src/display-path.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { relative, resolve } from "node:path";
|
|
2
|
+
import { isInside } from "./files";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* `path` as typed from `cwd`: relative under it, absolute elsewhere, so it works from anywhere; with
|
|
@@ -7,8 +8,7 @@ import { isAbsolute, relative, resolve } from "node:path";
|
|
|
7
8
|
*/
|
|
8
9
|
export function pathFrom(cwd: string, path: string, options: { home?: string } = {}): string {
|
|
9
10
|
const absolute = resolve(cwd, path);
|
|
10
|
-
|
|
11
|
-
if (inside && !inside.startsWith("..") && !isAbsolute(inside)) return inside;
|
|
11
|
+
if (isInside(cwd, absolute)) return relative(cwd, absolute);
|
|
12
12
|
const { home } = options;
|
|
13
13
|
return home !== undefined && absolute.startsWith(`${home}/`)
|
|
14
14
|
? `~${absolute.slice(home.length)}`
|
package/src/duration.ts
CHANGED
|
@@ -1,55 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
durationMilliseconds,
|
|
3
|
+
positiveDurationMilliseconds,
|
|
4
|
+
secondsForMilliseconds,
|
|
5
|
+
} from "@agentswf/contract/workflow";
|
|
3
6
|
import { messageOf } from "./errors";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* A flag's duration in milliseconds, longer than none: the one grammar, `"90s"`, `"1.5h"`, `"1d"`.
|
|
7
|
-
* A
|
|
10
|
+
* A number of `ms`, which the CLI took before 0.1.0-beta.6, is refused with its replacement.
|
|
8
11
|
*/
|
|
9
|
-
export function parseDuration(
|
|
10
|
-
value
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (!ms) return positiveDurationMilliseconds(value, flag);
|
|
16
|
-
const milliseconds = Number(ms[1]);
|
|
17
|
-
if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0)
|
|
18
|
-
throw new Error(`${flag} is a positive duration: not ${value}`);
|
|
19
|
-
deprecated(deprecationLine("duration-ms", value));
|
|
20
|
-
return milliseconds;
|
|
12
|
+
export function parseDuration(value: string, flag: string): number {
|
|
13
|
+
const seconds = secondsForMilliseconds(value);
|
|
14
|
+
if (seconds === undefined) return positiveDurationMilliseconds(value, flag);
|
|
15
|
+
throw new Error(
|
|
16
|
+
`${flag} is a duration, a decimal and s, m, h or d: give ${seconds}, not ${value}`,
|
|
17
|
+
);
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
/**
|
|
24
21
|
* `--wait`, in seconds from none to `maxSeconds`: a duration, `"90s"`. A bare number of seconds,
|
|
25
|
-
* which it took before
|
|
22
|
+
* which it took before 0.1.0-beta.6, is refused with its replacement.
|
|
26
23
|
*/
|
|
27
|
-
export function parseWait(
|
|
28
|
-
value
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
const bare = /^\d+(\.\d+)?$/.test(value);
|
|
24
|
+
export function parseWait(value: string, maxSeconds: number): number {
|
|
25
|
+
if (/^\d+(\.\d+)?$/.test(value))
|
|
26
|
+
throw new Error(
|
|
27
|
+
`--wait takes a duration up to ${maxSeconds}s: give --wait ${Math.min(Number(value), maxSeconds)}s`,
|
|
28
|
+
);
|
|
33
29
|
let seconds: number;
|
|
34
30
|
try {
|
|
35
|
-
seconds =
|
|
31
|
+
seconds = durationMilliseconds(value) / 1000;
|
|
36
32
|
} catch (error) {
|
|
37
33
|
throw new Error(`--wait takes a duration up to ${maxSeconds}s: ${messageOf(error)}`);
|
|
38
34
|
}
|
|
39
35
|
if (seconds > maxSeconds) throw new Error(`--wait takes a duration up to ${maxSeconds}s`);
|
|
40
|
-
if (bare) deprecated(deprecationLine("wait-seconds", value));
|
|
41
36
|
return seconds;
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
/** `45s`, `6m40s`, `1h02m`. */
|
|
45
|
-
export function formatDuration(milliseconds: number): string {
|
|
46
|
-
const seconds = Math.max(0, Math.round(milliseconds / 1000));
|
|
47
|
-
if (seconds < 60) return `${seconds}s`;
|
|
48
|
-
const minutes = Math.floor(seconds / 60);
|
|
49
|
-
if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
50
|
-
return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, "0")}m`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
39
|
/**
|
|
54
40
|
* When a timer given `when` is due, epoch ms, from `now`, and `when` as its record keeps it: a
|
|
55
41
|
* `Duration` as given, or a `Date` as an ISO time. A workflow's grammar, apart from the CLI's
|
package/src/emit-command.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import type { EmitResultJson } from "@agentswf/contract/records";
|
|
2
2
|
import { isEventWorkflow, type JsonValue } from "@agentswf/contract/workflow";
|
|
3
|
+
import { type Out, parsePlace, type ReadPlace } from "./command-line";
|
|
3
4
|
import { EmitError, emitEvent, emitToRun } from "./emit";
|
|
4
5
|
import { messageOf } from "./errors";
|
|
5
6
|
import help from "./help/emit.txt" with { type: "text" };
|
|
6
|
-
import {
|
|
7
|
+
import { asksJson, printRefusal, readFailureCode } from "./refusals";
|
|
7
8
|
import { findRunByRef } from "./run-state";
|
|
8
|
-
import type { Launch } from "./wake";
|
|
9
|
+
import type { Launch, Woken } from "./wake";
|
|
9
10
|
import { loadWorkflowFile } from "./workflow-loader";
|
|
10
11
|
|
|
12
|
+
/** `awf emit --json`'s `"v"`, which its refusals carry too. */
|
|
13
|
+
export const EMIT_V = 1 satisfies EmitResultJson["v"];
|
|
14
|
+
|
|
11
15
|
export const emitUsage = help.trimEnd();
|
|
12
16
|
|
|
13
|
-
type Event = { name: string; data: JsonValue; id?: string };
|
|
17
|
+
type Event = { name: string; data: JsonValue; id?: string; source?: string };
|
|
14
18
|
|
|
15
19
|
/** `--target {file} --id {id}`, which may start the run; or a run that is there, by its ref. */
|
|
16
20
|
type Parsed = Event & { json: boolean } & (
|
|
@@ -28,21 +32,19 @@ export async function emitCommand(
|
|
|
28
32
|
launch?: Launch;
|
|
29
33
|
},
|
|
30
34
|
): Promise<number> {
|
|
31
|
-
if (argv.includes("-h") || argv.includes("--help")) {
|
|
32
|
-
out.stdout(emitUsage);
|
|
33
|
-
return 0;
|
|
34
|
-
}
|
|
35
35
|
let parsed: Parsed;
|
|
36
36
|
try {
|
|
37
37
|
parsed = parse(argv, shellCwd);
|
|
38
38
|
} catch (error) {
|
|
39
|
-
out
|
|
39
|
+
printRefusal(out, { json: asksJson(argv), v: EMIT_V }, error, "usage", emitUsage);
|
|
40
40
|
return 2;
|
|
41
41
|
}
|
|
42
|
+
const json = { json: parsed.json, v: EMIT_V };
|
|
42
43
|
const event = {
|
|
43
44
|
name: parsed.name,
|
|
44
45
|
data: parsed.data,
|
|
45
46
|
...(parsed.id === undefined ? {} : { id: parsed.id }),
|
|
47
|
+
...(parsed.source === undefined ? {} : { source: parsed.source }),
|
|
46
48
|
};
|
|
47
49
|
let result: EmitResultJson;
|
|
48
50
|
try {
|
|
@@ -50,9 +52,9 @@ export async function emitCommand(
|
|
|
50
52
|
const status = await findRunByRef(parsed.ref, parsed.place, context.home);
|
|
51
53
|
result = await emitToRun(status, event, context);
|
|
52
54
|
} else {
|
|
53
|
-
const loaded = await loadWorkflowFile(parsed.target, shellCwd);
|
|
55
|
+
const loaded = await loadWorkflowFile(parsed.target, shellCwd, context.home);
|
|
54
56
|
if (!isEventWorkflow(loaded.workflow)) {
|
|
55
|
-
out
|
|
57
|
+
printRefusal(out, json, `${loaded.file} declares no events; awf run starts it`, "event");
|
|
56
58
|
return 2;
|
|
57
59
|
}
|
|
58
60
|
result = await emitEvent(
|
|
@@ -62,24 +64,29 @@ export async function emitCommand(
|
|
|
62
64
|
);
|
|
63
65
|
}
|
|
64
66
|
} catch (error) {
|
|
65
|
-
out
|
|
67
|
+
printRefusal(out, json, error, readFailureCode(error));
|
|
66
68
|
return error instanceof EmitError ? 2 : 1;
|
|
67
69
|
}
|
|
68
70
|
return printEmit(result, parsed.json, out);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
/** Prints an emit's result,
|
|
72
|
-
|
|
73
|
-
out.stdout(json ? JSON.stringify(result) :
|
|
74
|
-
|
|
75
|
-
out.stderr(`awf: the run's attempt did not start: ${result.error}`);
|
|
76
|
-
return 1;
|
|
73
|
+
/** Prints an emit's result, `accepted dd-case/kombo: started`: 1 when the run's attempt didn't start. */
|
|
74
|
+
function printEmit(result: EmitResultJson, json: boolean, out: Out): number {
|
|
75
|
+
out.stdout(json ? JSON.stringify(result) : `${result.outcome} ${result.ref}${didOf(result)}`);
|
|
76
|
+
return launchExit(result, out);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/**
|
|
80
|
-
function
|
|
81
|
-
const did =
|
|
82
|
-
return
|
|
79
|
+
/** What waking a run did, `: started`, or why it did nothing; empty when neither is said. */
|
|
80
|
+
export function didOf(woken: Pick<Woken, "did" | "why">): string {
|
|
81
|
+
const did = woken.did === "none" ? woken.why : woken.did;
|
|
82
|
+
return did === undefined ? "" : `: ${did}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 0; or 1, said on stderr, when the run's attempt did not start. */
|
|
86
|
+
export function launchExit(woken: Pick<Woken, "why" | "error">, out: Out): number {
|
|
87
|
+
if (woken.why !== "launch-failed") return 0;
|
|
88
|
+
out.stderr(`awf: the run's attempt did not start: ${woken.error}`);
|
|
89
|
+
return 1;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
function parse(argv: readonly string[], shellCwd: string): Parsed {
|
|
@@ -87,6 +94,7 @@ function parse(argv: readonly string[], shellCwd: string): Parsed {
|
|
|
87
94
|
let target: string | undefined;
|
|
88
95
|
let run: string | undefined;
|
|
89
96
|
let id: string | undefined;
|
|
97
|
+
let source: string | undefined;
|
|
90
98
|
const words: string[] = [];
|
|
91
99
|
for (let i = 0; i < rest.length; i++) {
|
|
92
100
|
const word = rest[i]!;
|
|
@@ -98,6 +106,7 @@ function parse(argv: readonly string[], shellCwd: string): Parsed {
|
|
|
98
106
|
if (word === "--target") target = value();
|
|
99
107
|
else if (word === "--id") run = value();
|
|
100
108
|
else if (word === "--event-id") id = value();
|
|
109
|
+
else if (word === "--source") source = value();
|
|
101
110
|
else if (word.startsWith("--")) throw new Error(`unknown option ${word}`);
|
|
102
111
|
else words.push(word);
|
|
103
112
|
}
|
|
@@ -129,5 +138,12 @@ function parse(argv: readonly string[], shellCwd: string): Parsed {
|
|
|
129
138
|
throw new Error(`the event's data is not JSON: ${messageOf(error)}`);
|
|
130
139
|
}
|
|
131
140
|
}
|
|
132
|
-
return {
|
|
141
|
+
return {
|
|
142
|
+
...form,
|
|
143
|
+
name,
|
|
144
|
+
data,
|
|
145
|
+
...(id === undefined ? {} : { id }),
|
|
146
|
+
...(source === undefined ? {} : { source }),
|
|
147
|
+
json,
|
|
148
|
+
};
|
|
133
149
|
}
|