@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.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/package.json +13 -9
  4. package/src/accounting/format.ts +4 -3
  5. package/src/accounting/spent.ts +128 -0
  6. package/src/accounting/summary.ts +40 -32
  7. package/src/agent-cli/cli.ts +28 -55
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +4 -4
  11. package/src/attempt-output.ts +13 -31
  12. package/src/attempt-view.ts +33 -32
  13. package/src/author-typebox.ts +13 -0
  14. package/src/client/checks.ts +94 -0
  15. package/src/client/exported.ts +17 -0
  16. package/src/client/index.ts +122 -0
  17. package/src/client/locate.ts +34 -0
  18. package/src/client/published.ts +3 -0
  19. package/src/client/runs.ts +337 -0
  20. package/src/command-line.ts +64 -0
  21. package/src/commands.ts +7 -1
  22. package/src/control-plane.ts +4 -6
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +27 -28
  25. package/src/deadlines.ts +6 -40
  26. package/src/decisions/directory.ts +50 -37
  27. package/src/decisions/fake.ts +1 -1
  28. package/src/decisions/openrouter.ts +4 -2
  29. package/src/decisions/seam.ts +4 -4
  30. package/src/display-path.ts +3 -3
  31. package/src/duration.ts +19 -33
  32. package/src/emit-command.ts +38 -22
  33. package/src/emit.ts +148 -59
  34. package/src/event-ids.ts +31 -0
  35. package/src/files.ts +44 -4
  36. package/src/hand-over.ts +150 -0
  37. package/src/help/allowance.txt +4 -3
  38. package/src/help/emit.txt +11 -7
  39. package/src/help/inbox.txt +5 -3
  40. package/src/help/logs.txt +7 -5
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +13 -8
  44. package/src/help/runs.txt +4 -2
  45. package/src/help/tick.txt +9 -4
  46. package/src/herdr-placement.ts +3 -33
  47. package/src/herdr-run-session.ts +5 -13
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +54 -136
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +42 -56
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.ts.template +3 -2
  55. package/src/jsonl.ts +10 -6
  56. package/src/keep-alive.ts +7 -19
  57. package/src/logs-command.ts +134 -53
  58. package/src/message-operation.ts +6 -3
  59. package/src/messaging/messaging.ts +8 -10
  60. package/src/messaging/text.ts +2 -2
  61. package/src/open-command.ts +770 -0
  62. package/src/operation-events.ts +4 -6
  63. package/src/operation-liveness.ts +19 -4
  64. package/src/operator-cli.ts +206 -90
  65. package/src/operator-runtime.ts +24 -2
  66. package/src/ops.ts +270 -0
  67. package/src/pane-layout.ts +49 -96
  68. package/src/progress-view.ts +2 -2
  69. package/src/project-engine.ts +79 -0
  70. package/src/projects.ts +13 -2
  71. package/src/refusals.ts +119 -0
  72. package/src/result-slots.ts +11 -31
  73. package/src/result-validation.ts +2 -2
  74. package/src/run-command.ts +27 -31
  75. package/src/run-commands.ts +42 -114
  76. package/src/run-event-log.ts +1 -1
  77. package/src/run-lines.ts +159 -0
  78. package/src/run-lock.ts +57 -1
  79. package/src/run-policy/stage-plan.ts +154 -0
  80. package/src/run-policy/stages.ts +113 -0
  81. package/src/run-policy/waits.ts +452 -0
  82. package/src/run-prepare.ts +165 -41
  83. package/src/run-read.ts +109 -41
  84. package/src/run-state.ts +170 -50
  85. package/src/run-text.ts +54 -0
  86. package/src/run-unreachable.ts +18 -0
  87. package/src/run-usage.ts +17 -11
  88. package/src/run-view.ts +73 -14
  89. package/src/runs-command.ts +81 -63
  90. package/src/runs.ts +284 -222
  91. package/src/sandbox-homes.ts +65 -13
  92. package/src/sandboxes.ts +182 -28
  93. package/src/session-lines.ts +93 -0
  94. package/src/skills/run-skills.ts +8 -1
  95. package/src/sources.ts +2 -39
  96. package/src/spend.ts +333 -0
  97. package/src/stage-ledger.ts +111 -153
  98. package/src/stop-crashed.ts +6 -5
  99. package/src/stopped.ts +4 -4
  100. package/src/test-command.ts +37 -19
  101. package/src/testing/operation-events.ts +3 -23
  102. package/src/testing.ts +66 -18
  103. package/src/tick-command.ts +145 -15
  104. package/src/tools.ts +11 -30
  105. package/src/turn-ends.ts +37 -0
  106. package/src/waits.ts +651 -0
  107. package/src/wake.ts +84 -54
  108. package/src/workflow-loader.ts +49 -2
  109. package/src/workflow-runner.ts +525 -618
  110. package/src/workflow-test-preload.ts +4 -1
  111. package/src/workflow-testing/coordinators.ts +1 -1
  112. package/src/workflow-testing/fake-client.ts +387 -0
  113. package/src/workflow-testing/host.ts +28 -29
  114. package/src/workflow-testing/index.ts +9 -27
  115. package/src/workflow-testing/run-test.ts +201 -286
  116. package/src/workflow-testing/script.ts +7 -39
  117. package/src/deprecations.ts +0 -177
  118. package/src/hold-run-lock.fixture.ts +0 -8
  119. package/src/mailbox.ts +0 -972
  120. package/src/stage-plan.ts +0 -186
  121. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  122. 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 type { DecisionArtifact, Money, SettledDecision } from "@agentswf/contract/records";
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 charged: Money[] = [];
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
- charged?: Money;
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.charged) charged = [...charged, spent.charged];
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 = join("decisions", `${slot + 1}.json`);
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(charged);
265
- let settled: SettledDecision = { ...record, ...(total ? { charged: 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
- try {
272
- await mkdir(join(this.options.runDir, "decisions"), { recursive: true });
273
- await writeFile(join(this.options.runDir, artifact), `${JSON.stringify(written, null, 2)}\n`);
274
- } catch (thrown) {
275
- const lost = `artifact not written: ${messageOf(thrown)}`;
276
- // Without its artifact an answer cannot be fitted again, so it does not count as one; any
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 { charged: _charged, ...returned } = settled;
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
- /** Charges in more than one currency cannot be added without converting, so their sum is unknown. */
410
- function sumMoney(charges: readonly Money[]): Money | undefined {
411
- const currency = charges[0]?.currency;
412
- if (!currency || charges.some((charge) => charge.currency !== currency)) return undefined;
413
- return { amount: charges.reduce((sum, charge) => sum + charge.amount, 0), currency };
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> {
@@ -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
- charged: { amount: 0.000042, currency: "USD" },
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" | "charged" | "requestId"> {
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" ? { charged: { amount: usage.cost, currency: "USD" } } : {}),
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
  }
@@ -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
- charged?: Money;
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 charged?: Money;
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
- charged?: Money;
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.charged) this.charged = options.charged;
59
+ if (options.reportedCost) this.reportedCost = options.reportedCost;
60
60
  if (options.requestId) this.requestId = options.requestId;
61
61
  }
62
62
  }
@@ -1,4 +1,5 @@
1
- import { isAbsolute, relative, resolve } from "node:path";
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
- const inside = relative(cwd, absolute);
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 { durationMilliseconds, positiveDurationMilliseconds } from "@agentswf/contract/workflow";
2
- import { deprecationLine } from "./deprecations";
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 whole number of `ms`, which the CLI took before, is read until 0.1.0 and `deprecated` told.
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: string,
11
- flag: string,
12
- deprecated: (line: string) => void,
13
- ): number {
14
- const ms = /^(\d+)ms$/.exec(value);
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, is read until 0.1.0 and `deprecated` told.
22
+ * which it took before 0.1.0-beta.6, is refused with its replacement.
26
23
  */
27
- export function parseWait(
28
- value: string,
29
- maxSeconds: number,
30
- deprecated: (line: string) => void,
31
- ): number {
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 = bare ? Number(value) : durationMilliseconds(value) / 1000;
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
@@ -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 { type Out, parsePlace, type ReadPlace } from "./run-commands";
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.stderr(`awf: ${messageOf(error)}\n\n${emitUsage}`);
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.stderr(`awf: ${loaded.file} declares no events; awf run starts it`);
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.stderr(`awf: ${messageOf(error)}`);
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, as `awf emit` and `awf send` do: 1 when the run's attempt didn't start. */
72
- export function printEmit(result: EmitResultJson, json: boolean, out: Out): number {
73
- out.stdout(json ? JSON.stringify(result) : emitLine(result));
74
- if (result.why !== "launch-failed") return 0;
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
- /** `accepted dd-case/kombo: started`. */
80
- function emitLine(result: EmitResultJson): string {
81
- const did = result.did === "none" ? result.why : result.did;
82
- return `${result.outcome} ${result.ref}${did === undefined ? "" : `: ${did}`}`;
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 { ...form, name, data, ...(id === undefined ? {} : { id }), json };
141
+ return {
142
+ ...form,
143
+ name,
144
+ data,
145
+ ...(id === undefined ? {} : { id }),
146
+ ...(source === undefined ? {} : { source }),
147
+ json,
148
+ };
133
149
  }