@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the agents.wf authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @agentswf/engine
2
+
3
+ The runtime behind `awf`: it runs a workflow, keeps its record in `.awf/runs`, and serves its agents' `wfa` command.
4
+
5
+ This package is internal to [agents.wf](https://agents.wf). It is published so that
6
+ [`agentswf`](https://www.npmjs.com/package/agentswf) can depend on it, and its exports change
7
+ without notice between versions. Install `agentswf` instead:
8
+
9
+ ```sh
10
+ bun add -g agentswf
11
+ ```
12
+
13
+ A workflow imports `agentswf/workflow`, its tests `agentswf/testing`, and a program that drives
14
+ runs `agentswf/client`. The [guide](https://agents.wf/guide/) starts there.
15
+
16
+ MIT licensed. Source: [github.com/romanlv/agentswf](https://github.com/romanlv/agentswf/tree/main/packages/engine).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentswf/engine",
3
- "version": "0.1.0-beta.5",
3
+ "version": "0.1.0",
4
4
  "description": "agents.wf: the engine that runs workflows made of coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://agents.wf",
@@ -13,25 +13,29 @@
13
13
  "engines": {
14
14
  "bun": ">=1.4.0"
15
15
  },
16
- "bin": {
17
- "awf": "./src/operator-cli.ts"
18
- },
19
16
  "exports": {
20
17
  ".": "./src/index.ts",
21
18
  "./testing": "./src/testing.ts",
19
+ "./typebox": "./src/author-typebox.ts",
22
20
  "./workflow-testing": "./src/workflow-testing/index.ts",
23
- "./cli": "./src/operator-cli.ts"
21
+ "./cli": "./src/operator-cli.ts",
22
+ "./hand-over": "./src/hand-over.ts",
23
+ "./client": "./src/client/published.ts"
24
24
  },
25
25
  "files": [
26
26
  "src",
27
27
  "!src/**/*.test.ts",
28
28
  "!src/**/*.eval.ts",
29
- "!src/**/fixtures"
29
+ "!src/**/*.typecheck.ts",
30
+ "!src/**/*.fixture.ts",
31
+ "!src/**/fixtures",
32
+ "README.md",
33
+ "LICENSE"
30
34
  ],
31
35
  "dependencies": {
32
- "@agentswf/contract": "0.1.0-beta.5",
33
- "@agentswf/harness": "0.1.0-beta.5",
34
- "@agentswf/sandbox": "0.1.0-beta.5",
36
+ "@agentswf/contract": "0.1.0",
37
+ "@agentswf/harness": "0.1.0",
38
+ "@agentswf/sandbox": "0.1.0",
35
39
  "typebox": "^1.3.34"
36
40
  },
37
41
  "publishConfig": {
@@ -50,7 +50,7 @@ function describeTotals(accounting: AttemptAccounting): string {
50
50
  took,
51
51
  tokensOf(totals),
52
52
  ...(totals.estimate === undefined ? [] : [`${estimate(totals.estimate)} at ${basis}`]),
53
- ...(totals.charged === undefined ? [] : [`${usd(totals.charged)} charged`]),
53
+ ...(totals.reportedCost === undefined ? [] : [`${usd(totals.reportedCost)} reported`]),
54
54
  accounting.billing,
55
55
  ...(totals.billed < totals.agents ? [`billing known ${totals.billed}/${totals.agents}`] : []),
56
56
  ...usageGap(totals),
@@ -182,7 +182,7 @@ function describeDecisions(decisions: DecisionFigures, basis: string): string {
182
182
  ...(decisions.attempts > decisions.calls ? [plural(decisions.attempts, "request")] : []),
183
183
  decisions.known === 0 ? "no usage known" : plural(tokens, "token", count),
184
184
  ...(decisions.estimate === undefined ? [] : [`${estimate(decisions.estimate)} at ${basis}`]),
185
- ...(decisions.charged === undefined ? [] : [`${usd(decisions.charged)} charged`]),
185
+ ...(decisions.reportedCost === undefined ? [] : [`${usd(decisions.reportedCost)} reported`]),
186
186
  ...decisionGaps(decisions),
187
187
  ].join(" · ");
188
188
  }
@@ -237,7 +237,8 @@ function count(tokens: number): string {
237
237
  return String(tokens);
238
238
  }
239
239
 
240
- function usd(amount: number): string {
240
+ /** `$6.10`, or `<$0.01` for a cost too small to show in cents. */
241
+ export function usd(amount: number): string {
241
242
  return belowCent(amount) ? "<$0.01" : `$${amount.toFixed(2)}`;
242
243
  }
243
244
 
@@ -0,0 +1,128 @@
1
+ import type {
2
+ AttemptAccounting,
3
+ ClaimedAttempt,
4
+ OpRecord,
5
+ RunAccounting,
6
+ SettledDecision,
7
+ SpendRecord,
8
+ } from "@agentswf/contract/records";
9
+ import type { PriceTable } from "./prices";
10
+ import { attemptAccounting, type PricedOperation, sumAttempts, summarizeRun } from "./summary";
11
+
12
+ /** An operation as its folder records it: `op.json`, and its `spend.json` or decision's record. */
13
+ export type RecordedOp = { op: OpRecord; spend?: SpendRecord; decision?: SettledDecision };
14
+
15
+ /** What one attempt's operations recorded, priced now. */
16
+ export type AttemptSpent = {
17
+ attempt: number;
18
+ accounting: RunAccounting;
19
+ /** Its agent operations with no `spend.json` yet: their usage is unknown, not zero. */
20
+ unsettled: number;
21
+ /** Its agent operations settled without their usage: their sessions could not be read. */
22
+ unread: number;
23
+ };
24
+
25
+ /**
26
+ * Each attempt's accounting, summed from its operations' records and priced with `prices`: a run is
27
+ * priced when it is summed, never when it is written. An attempt still running, or one interrupted
28
+ * and not yet settled by the next, has `unsettled` operations. One with no `ended` is timed to the
29
+ * last of its operations to start or settle.
30
+ */
31
+ export function attemptsSpent(
32
+ ops: readonly RecordedOp[],
33
+ attempts: readonly Pick<ClaimedAttempt, "attempt" | "started" | "ended">[],
34
+ prices: PriceTable,
35
+ ): AttemptSpent[] {
36
+ return attempts.map(({ attempt, started, ended }) => {
37
+ const own = ops.filter(({ op }) => op.attempt === attempt);
38
+ const usage = own.flatMap(({ op, spend }) =>
39
+ op.kind === "decision" || !spend ? [] : [settledOf(op, spend)],
40
+ );
41
+ const decisions = own.flatMap(({ decision }) => (decision ? [decision] : []));
42
+ const agentOps = own.filter(({ op }) => op.kind !== "decision");
43
+ const finishedAt =
44
+ ended ??
45
+ [
46
+ started,
47
+ ...own.map(({ op }) => op.at),
48
+ ...usage.flatMap((record) => record.settledAt ?? []),
49
+ ...decisions.map((record) => record.settledAt),
50
+ ].reduce((last, at) => (Date.parse(at) > Date.parse(last) ? at : last));
51
+ return {
52
+ attempt,
53
+ accounting: summarizeRun(usage, prices, { startedAt: started, finishedAt }, decisions),
54
+ unsettled: agentOps.filter(({ spend }) => !spend).length,
55
+ unread: agentOps.filter(({ spend }) => spend && spend.spend === undefined).length,
56
+ };
57
+ });
58
+ }
59
+
60
+ /**
61
+ * The attempts before one, for a run's total: each one's accounting as far as it is known, and how
62
+ * many have an operation whose usage isn't, by whether they ended.
63
+ */
64
+ export type EarlierSpent = { recorded: AttemptAccounting[]; interrupted: number; ended: number };
65
+
66
+ /** The attempts before `current` summed, for a run's total beside `current`'s own. */
67
+ export function earlierSpent(
68
+ spent: readonly AttemptSpent[],
69
+ attempts: readonly Pick<ClaimedAttempt, "attempt" | "ended">[],
70
+ current: number,
71
+ ): EarlierSpent {
72
+ const before = spent.filter(({ attempt }) => attempt < current);
73
+ const unknown = before.filter(costUnknown);
74
+ const interrupted = unknown.filter(
75
+ ({ attempt }) => attempts.find((each) => each.attempt === attempt)?.ended === undefined,
76
+ ).length;
77
+ return {
78
+ recorded: before.map(({ accounting }) => attemptAccounting(accounting)),
79
+ interrupted,
80
+ ended: unknown.length - interrupted,
81
+ };
82
+ }
83
+
84
+ /** The attempts that ran, when `awf run` can't read what they spent: each of unknown cost. */
85
+ export function unreadEarlier(
86
+ attempts: readonly Pick<ClaimedAttempt, "attempt" | "ended">[],
87
+ current: number,
88
+ ): EarlierSpent {
89
+ const before = attempts.filter(({ attempt }) => attempt < current);
90
+ const interrupted = before.filter(({ ended }) => ended === undefined).length;
91
+ return { recorded: [], interrupted, ended: before.length - interrupted };
92
+ }
93
+
94
+ /** A run's estimate at list prices, over its attempts: never what was billed. */
95
+ export function runEstimate(spent: readonly AttemptSpent[]): number | undefined {
96
+ if (spent.length === 0) return undefined;
97
+ return sumAttempts(spent.map(({ accounting }) => attemptAccounting(accounting))).totals.estimate;
98
+ }
99
+
100
+ /** How many of a run's agent operations have a usage not known, so its estimate is a floor. */
101
+ export function unknownOps(spent: readonly AttemptSpent[]): number {
102
+ return spent.reduce((total, each) => total + each.unsettled + each.unread, 0);
103
+ }
104
+
105
+ function costUnknown({ unsettled, unread }: AttemptSpent): boolean {
106
+ return unsettled + unread > 0;
107
+ }
108
+
109
+ function settledOf(op: Extract<OpRecord, { agent: string }>, spend: SpendRecord): PricedOperation {
110
+ return {
111
+ callPath: [],
112
+ agent: op.agent,
113
+ operationId: op.id,
114
+ execution: op.execution,
115
+ sessions: [],
116
+ ...(op.stage === undefined ? {} : { stage: op.stage }),
117
+ ...(spend.deliveredAt === undefined ? {} : { deliveredAt: spend.deliveredAt }),
118
+ ...(spend.settledAt === undefined ? {} : { settledAt: spend.settledAt }),
119
+ billing: spend.billing,
120
+ ...(spend.spend === undefined ? {} : { spend: spend.spend }),
121
+ ...(spend.reportedCost === undefined ? {} : { reportedCost: spend.reportedCost }),
122
+ // A metered turn's cost is printed as it ends, never after: read by the next attempt, its
123
+ // reported cost is unknown.
124
+ ...(spend.settledBy !== undefined && spend.billing === "metered"
125
+ ? { reportedCostUnread: true as const }
126
+ : {}),
127
+ };
128
+ }
@@ -13,6 +13,9 @@ import { addTokens } from "./tokens";
13
13
 
14
14
  const NONE: TokenUsage = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
15
15
 
16
+ /** An operation as priced: `reportedCostUnread` when what its harness reported can't be known. */
17
+ export type PricedOperation = SettledOperation & { reportedCostUnread?: true };
18
+
16
19
  /** What ran between a run's stages, as a row of `byStage`. */
17
20
  export const NO_STAGE = "(no stage)";
18
21
 
@@ -20,10 +23,10 @@ export const NO_STAGE = "(no stage)";
20
23
  * What a run cost and how long it took, from its usage and decision records alone, so a finished
21
24
  * run can be priced again with another table. `entered` are the workflow stages entered, in order:
22
25
  * each is a row timed by its own span, a reused one at zero; without them, a row per call path and
23
- * key prefix, timed by what ran in it.
26
+ * agent or decision key, timed by what ran in it.
24
27
  */
25
28
  export function summarizeRun(
26
- usage: readonly SettledOperation[],
29
+ usage: readonly PricedOperation[],
27
30
  prices: PriceTable,
28
31
  times: { startedAt: string; finishedAt: string },
29
32
  decisions: readonly SettledDecision[],
@@ -38,8 +41,8 @@ export function summarizeRun(
38
41
  entered.length > 0 ||
39
42
  usage.some((record) => record.stage !== undefined) ||
40
43
  decisions.some((record) => record.stage !== undefined);
41
- const stageOf = staged ? workflowStageOf : prefixStageOf;
42
- const decisionStageOf = staged ? workflowStageOf : decisionPrefixStageOf;
44
+ const stageOf = staged ? workflowStageOf : agentStageOf;
45
+ const decisionStageOf = staged ? workflowStageOf : decisionKeyStageOf;
43
46
  const stages = unique([
44
47
  ...entered.map(({ stage }) => stage),
45
48
  ...usage.map(stageOf),
@@ -49,7 +52,7 @@ export function summarizeRun(
49
52
  (billing) => billing !== "unknown",
50
53
  );
51
54
  const figures = (
52
- records: readonly SettledOperation[],
55
+ records: readonly PricedOperation[],
53
56
  asked: readonly SettledDecision[] = [],
54
57
  ): AccountingFigures => {
55
58
  const agents = figuresOf(records, prices);
@@ -61,7 +64,7 @@ export function summarizeRun(
61
64
  startedAt: times.startedAt,
62
65
  finishedAt: times.finishedAt,
63
66
  wallMs: Date.parse(times.finishedAt) - Date.parse(times.startedAt),
64
- grouping: staged ? "stages" : "prefix",
67
+ grouping: staged ? "stages" : "agents",
65
68
  billing: billings.length === 0 ? "unknown" : billings.length === 1 ? billings[0]! : "mixed",
66
69
  totals: figures(usage, decisions),
67
70
  byStage: stages.map((stage) => {
@@ -92,10 +95,10 @@ export function summarizeRun(
92
95
  };
93
96
  }
94
97
 
95
- function figuresOf(records: readonly SettledOperation[], prices: PriceTable): AccountingFigures {
98
+ function figuresOf(records: readonly PricedOperation[], prices: PriceTable): AccountingFigures {
96
99
  let tokens = NONE;
97
100
  let delegated = NONE;
98
- let charged: number | undefined;
101
+ let reportedCost: number | undefined;
99
102
  let agentMs = 0;
100
103
  const agents = new Map<
101
104
  string,
@@ -113,9 +116,11 @@ function figuresOf(records: readonly SettledOperation[], prices: PriceTable): Ac
113
116
  if (record.deliveredAt !== undefined && record.settledAt !== undefined) {
114
117
  agentMs += Date.parse(record.settledAt) - Date.parse(record.deliveredAt);
115
118
  }
116
- // Another currency would need converting first; until then its charge is an unknown one.
117
- if (record.charged?.currency === "USD") charged = (charged ?? 0) + record.charged.amount;
118
- else if (record.charged) agent.billed = false;
119
+ // Another currency would need converting first; until then its reported cost is an unknown one.
120
+ if (record.reportedCost?.currency === "USD")
121
+ reportedCost = (reportedCost ?? 0) + record.reportedCost.amount;
122
+ else if (record.reportedCost) agent.billed = false;
123
+ if (record.reportedCostUnread) agent.billed = false;
119
124
  if (!record.spend) {
120
125
  agent.known = false;
121
126
  agent.priced = false;
@@ -141,7 +146,7 @@ function figuresOf(records: readonly SettledOperation[], prices: PriceTable): Ac
141
146
  tokens,
142
147
  delegated,
143
148
  ...(estimate === undefined ? {} : { estimate }),
144
- ...(charged === undefined ? {} : { charged }),
149
+ ...(reportedCost === undefined ? {} : { reportedCost }),
145
150
  known: counts.filter((agent) => agent.known).length,
146
151
  priced: counts.filter((agent) => agent.priced).length,
147
152
  billed: counts.filter((agent) => agent.billed).length,
@@ -152,14 +157,15 @@ function decisionFigures(records: readonly SettledDecision[], prices: PriceTable
152
157
  let input = 0;
153
158
  let output = 0;
154
159
  let estimate: number | undefined;
155
- let charged: number | undefined;
160
+ let reportedCost: number | undefined;
156
161
  let known = 0;
157
162
  let priced = 0;
158
163
  let attempts = 0;
159
164
  for (const record of records) {
160
165
  attempts += record.attempts;
161
- // Another currency would need converting first; until then its charge is not counted.
162
- if (record.charged?.currency === "USD") charged = (charged ?? 0) + record.charged.amount;
166
+ // Another currency would need converting first; until then its reported cost is not counted.
167
+ if (record.reportedCost?.currency === "USD")
168
+ reportedCost = (reportedCost ?? 0) + record.reportedCost.amount;
163
169
  if (!record.tokens) continue;
164
170
  known += 1;
165
171
  input += record.tokens.input;
@@ -174,7 +180,7 @@ function decisionFigures(records: readonly SettledDecision[], prices: PriceTable
174
180
  attempts,
175
181
  tokens: { input, output },
176
182
  ...(estimate === undefined ? {} : { estimate }),
177
- ...(charged === undefined ? {} : { charged }),
183
+ ...(reportedCost === undefined ? {} : { reportedCost }),
178
184
  known,
179
185
  priced,
180
186
  };
@@ -205,8 +211,8 @@ function decisionModelOf(record: SettledDecision): string {
205
211
  return record.snapshot ?? record.model;
206
212
  }
207
213
 
208
- function decisionPrefixStageOf(record: SettledDecision): string {
209
- return [...record.callPath, prefixOf(record.key)].join("/");
214
+ function decisionKeyStageOf(record: SettledDecision): string {
215
+ return [...record.callPath, record.key].join("/");
210
216
  }
211
217
 
212
218
  function modelFigures(
@@ -249,13 +255,8 @@ function noStageLast(a: string, b: string): number {
249
255
  return Number(a === NO_STAGE) - Number(b === NO_STAGE);
250
256
  }
251
257
 
252
- function prefixStageOf(record: SettledOperation): string {
253
- return [...record.callPath, prefixOf(record.agent)].join("/");
254
- }
255
-
256
- function prefixOf(key: string): string {
257
- const separator = key.indexOf(":");
258
- return separator === -1 ? key : key.slice(0, separator);
258
+ function agentStageOf(record: SettledOperation): string {
259
+ return [...record.callPath, record.agent].join("/");
259
260
  }
260
261
 
261
262
  function spanOf(
@@ -298,15 +299,22 @@ export function attemptAccounting({
298
299
 
299
300
  /**
300
301
  * A run's attempts summed, each estimate as its attempt recorded it, under the last one's basis:
301
- * its total, and each stage's across the attempts that ran it. Once any attempt grouped by stages, what another grouped by prefix, having failed
302
- * before it entered one, ran between stages.
302
+ * its total, and each stage's across the attempts that ran it. Once any attempt grouped by stages,
303
+ * what another grouped by agent, having failed before it entered one, ran between stages. An older
304
+ * attempt's `prefix` rows, by a key's part before `:`, sum with `agents` rows by name: a key
305
+ * without a colon is both.
303
306
  */
304
307
  export function sumAttempts(attempts: readonly AttemptAccounting[]): AttemptAccounting {
305
- const grouping = attempts.some((attempt) => attempt.grouping === "stages") ? "stages" : "prefix";
308
+ const grouping = attempts.some((attempt) => attempt.grouping === "stages")
309
+ ? "stages"
310
+ : attempts.some((attempt) => attempt.grouping === "agents")
311
+ ? "agents"
312
+ : "prefix";
306
313
  const byStage = new Map<string, RunAccounting["byStage"][number]>();
307
314
  for (const attempt of attempts) {
308
315
  for (const row of attempt.byStage) {
309
- const stage = grouping === attempt.grouping ? row.stage : NO_STAGE;
316
+ const stage =
317
+ (grouping === "stages") === (attempt.grouping === "stages") ? row.stage : NO_STAGE;
310
318
  const before = byStage.get(stage);
311
319
  byStage.set(
312
320
  stage,
@@ -351,7 +359,7 @@ function addFigures(left: AccountingFigures, right: AccountingFigures): Accounti
351
359
  tokens: addTokens(left.tokens, right.tokens),
352
360
  delegated: addTokens(left.delegated, right.delegated),
353
361
  ...addOptional("estimate", left, right),
354
- ...addOptional("charged", left, right),
362
+ ...addOptional("reportedCost", left, right),
355
363
  known: left.known + right.known,
356
364
  priced: left.priced + right.priced,
357
365
  billed: left.billed + right.billed,
@@ -368,14 +376,14 @@ function addDecisions(left: DecisionFigures, right: DecisionFigures): DecisionFi
368
376
  output: left.tokens.output + right.tokens.output,
369
377
  },
370
378
  ...addOptional("estimate", left, right),
371
- ...addOptional("charged", left, right),
379
+ ...addOptional("reportedCost", left, right),
372
380
  known: left.known + right.known,
373
381
  priced: left.priced + right.priced,
374
382
  };
375
383
  }
376
384
 
377
385
  /** A sum of what either side knows; absent only when neither does. */
378
- function addOptional<K extends "estimate" | "charged">(
386
+ function addOptional<K extends "estimate" | "reportedCost">(
379
387
  field: K,
380
388
  left: { [P in K]?: number },
381
389
  right: { [P in K]?: number },
@@ -5,7 +5,7 @@ import {
5
5
  validWaitingReason,
6
6
  WIRE_VERSION,
7
7
  } from "@agentswf/contract/wire";
8
- import { durationMilliseconds } from "@agentswf/contract/workflow";
8
+ import { durationMilliseconds, secondsForMilliseconds } from "@agentswf/contract/workflow";
9
9
  import { NotSentError, submitControl } from "./client";
10
10
 
11
11
  /** `wfa` is its launcher's full path, as an agent must type it; `wfa` alone when it is not known. */
@@ -46,9 +46,14 @@ function messageUsage(wfa: string, indent = "usage: "): string[] {
46
46
  ];
47
47
  }
48
48
  const MAX_STDIN_BYTES = 1024 * 1024;
49
+
50
+ /** An error's message, or the thrown value as text: the engine's `messageOf`, which wfa can't import. */
51
+ function reasonOf(error: unknown): string {
52
+ return error instanceof Error ? error.message : String(error);
53
+ }
49
54
  const STDIN_TIMEOUT_MS = 30_000;
50
55
 
51
- export type CliOutcome = { exitCode: number; stdout: string; stderr: string };
56
+ type CliOutcome = { exitCode: number; stdout: string; stderr: string };
52
57
 
53
58
  type Submit = (
54
59
  endpoint: string,
@@ -74,15 +79,10 @@ export async function runCli(
74
79
  }
75
80
  switch (command) {
76
81
  case "waiting":
82
+ return runWaiting(endpoint, session, args, submit);
77
83
  case "send":
78
- case "reply": {
79
- const said: Said = {};
80
- const outcome =
81
- command === "waiting"
82
- ? await runWaiting(endpoint, session, args, submit, said)
83
- : await runMessage(endpoint, session, command, args, stdin, wfa, submit, said);
84
- return noticed(outcome, said.notice);
85
- }
84
+ case "reply":
85
+ return runMessage(endpoint, session, command, args, stdin, wfa, submit);
86
86
  case "result":
87
87
  return runResult(endpoint, session, args, stdin, submit);
88
88
  case "tool":
@@ -94,16 +94,6 @@ export async function runCli(
94
94
  }
95
95
  }
96
96
 
97
- /** What a command accepted in a deprecated form, said beside its outcome. */
98
- type Said = { notice?: string };
99
-
100
- /** Never beside a usage error: the form said was not taken then. */
101
- function noticed(outcome: CliOutcome, notice: string | undefined): CliOutcome {
102
- return notice && outcome.exitCode !== 2
103
- ? { ...outcome, stderr: outcome.stderr ? `${notice}\n${outcome.stderr}` : notice }
104
- : outcome;
105
- }
106
-
107
97
  async function runResult(
108
98
  endpoint: string,
109
99
  session: string | undefined,
@@ -128,7 +118,7 @@ async function runResult(
128
118
  ...(session ? { session } : {}),
129
119
  });
130
120
  } catch (error) {
131
- const reason = error instanceof Error ? error.message : String(error);
121
+ const reason = reasonOf(error);
132
122
  return { exitCode: 1, stdout: "", stderr: `wfa: result submission failed.\n${reason}` };
133
123
  }
134
124
 
@@ -180,7 +170,7 @@ async function runTool(
180
170
  null,
181
171
  );
182
172
  } catch (error) {
183
- const reason = error instanceof Error ? error.message : String(error);
173
+ const reason = reasonOf(error);
184
174
  return error instanceof NotSentError
185
175
  ? {
186
176
  exitCode: 3,
@@ -224,7 +214,6 @@ async function runWaiting(
224
214
  session: string | undefined,
225
215
  args: readonly string[],
226
216
  submit: Submit,
227
- said: Said,
228
217
  ): Promise<CliOutcome> {
229
218
  const [operationId, ...flags] = args;
230
219
  if (!operationId?.trim() || operationId.startsWith("--"))
@@ -244,9 +233,8 @@ async function runWaiting(
244
233
  let timeoutMs: number | undefined;
245
234
  if (duration !== undefined) {
246
235
  const read = durationOf(duration);
247
- if (!read) return usageError(`wfa waiting --timeout ${DURATION_GRAMMAR}`);
248
- timeoutMs = read.milliseconds;
249
- said.notice = read.notice;
236
+ if (typeof read === "string") return usageError(`wfa waiting --timeout ${read}`);
237
+ timeoutMs = read;
250
238
  }
251
239
  let response: ControlResponse;
252
240
  try {
@@ -259,7 +247,7 @@ async function runWaiting(
259
247
  ...(session ? { session } : {}),
260
248
  });
261
249
  } catch (error) {
262
- const detail = error instanceof Error ? error.message : String(error);
250
+ const detail = reasonOf(error);
263
251
  return {
264
252
  exitCode: 1,
265
253
  stdout: "",
@@ -304,7 +292,7 @@ async function runPeers(
304
292
  ...(session ? { session } : {}),
305
293
  });
306
294
  } catch (error) {
307
- const detail = error instanceof Error ? error.message : String(error);
295
+ const detail = reasonOf(error);
308
296
  return { exitCode: 1, stdout: "", stderr: `wfa: peers failed; nothing changed.\n${detail}` };
309
297
  }
310
298
  if (response.kind === "rejected")
@@ -331,14 +319,12 @@ async function runMessage(
331
319
  stdin: string | null,
332
320
  wfa: string,
333
321
  submit: Submit,
334
- said: Said,
335
322
  ): Promise<CliOutcome> {
336
323
  const parsed = messageArgs(args);
337
324
  const wrong = (problem: string) =>
338
325
  usageError(`wfa: ${command}: ${problem}\n\n${messageUsage(wfa).join("\n")}`);
339
326
  if ("error" in parsed) return wrong(parsed.error);
340
327
  const { peer, message, awaitReply, timeoutMs } = parsed;
341
- said.notice = parsed.notice;
342
328
  // A heredoc ends with a newline the agent did not mean as part of the message.
343
329
  const text = message ?? stdin?.replace(/\r?\n$/, "") ?? null;
344
330
  if (text === null || text.trim() === "") return wrong("it needs a message");
@@ -355,7 +341,7 @@ async function runMessage(
355
341
  try {
356
342
  response = await submit(endpoint, request);
357
343
  } catch (error) {
358
- const detail = error instanceof Error ? error.message : String(error);
344
+ const detail = reasonOf(error);
359
345
  const what = command === "send" ? "message" : "reply";
360
346
  // Refused at the connection, nothing reached the engine; after, it may have been admitted.
361
347
  if (error instanceof NotSentError) {
@@ -384,12 +370,9 @@ async function runMessage(
384
370
  */
385
371
  function messageArgs(
386
372
  args: readonly string[],
387
- ):
388
- | { peer: string; message?: string; awaitReply: boolean; timeoutMs?: number; notice?: string }
389
- | { error: string } {
373
+ ): { peer: string; message?: string; awaitReply: boolean; timeoutMs?: number } | { error: string } {
390
374
  let awaitReply = false;
391
375
  let timeoutMs: number | undefined;
392
- let notice: string | undefined;
393
376
  const positional: string[] = [];
394
377
  for (let i = 0; i < args.length; i += 1) {
395
378
  const arg = args[i]!;
@@ -401,8 +384,8 @@ function messageArgs(
401
384
  if (arg === "--await-reply" && !awaitReply) awaitReply = true;
402
385
  else if (arg === "--timeout" && timeoutMs === undefined) {
403
386
  const read = durationOf(args[i + 1] ?? "");
404
- if (!read) return { error: `--timeout ${DURATION_GRAMMAR}` };
405
- ({ milliseconds: timeoutMs, notice } = read);
387
+ if (typeof read === "string") return { error: `--timeout ${read}` };
388
+ timeoutMs = read;
406
389
  i += 1;
407
390
  } else if (positional.length === 1 && arg !== "--await-reply" && arg !== "--timeout") {
408
391
  return { error: `${arg} is not a flag; a message that starts with -- goes in a heredoc` };
@@ -418,32 +401,22 @@ function messageArgs(
418
401
  ...(rest[0] === undefined ? {} : { message: rest[0] }),
419
402
  awaitReply,
420
403
  ...(timeoutMs === undefined ? {} : { timeoutMs }),
421
- ...(notice === undefined ? {} : { notice }),
422
404
  };
423
405
  }
424
406
 
425
407
  const DURATION_GRAMMAR = 'takes a duration, a decimal and s, m, h or d, such as "5m"';
426
- const LEGACY_MS = /^([0-9]+)ms$/;
427
408
 
428
- /**
429
- * The one grammar's, longer than none. A whole number of `ms` is read until 0.1.0, with the line
430
- * `wfa` says beside its outcome.
431
- */
432
- function durationOf(text: string): { milliseconds: number; notice?: string } | undefined {
433
- const legacy = LEGACY_MS.exec(text);
409
+ /** The one grammar's milliseconds, longer than none; else why not, `ms` named in `s`. */
410
+ function durationOf(text: string): number | string {
411
+ const seconds = secondsForMilliseconds(text);
412
+ if (seconds !== undefined) return `${DURATION_GRAMMAR}: give ${seconds}`;
434
413
  let milliseconds: number;
435
414
  try {
436
- milliseconds = legacy ? Number(legacy[1]) : durationMilliseconds(text);
415
+ milliseconds = durationMilliseconds(text);
437
416
  } catch {
438
- return undefined;
417
+ return DURATION_GRAMMAR;
439
418
  }
440
- if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) return undefined;
441
- return legacy
442
- ? {
443
- milliseconds,
444
- notice: `wfa: --timeout ${text} is deprecated; give ${milliseconds / 1000}s: a duration is a decimal and s, m, h or d`,
445
- }
446
- : { milliseconds };
419
+ return milliseconds > 0 ? milliseconds : DURATION_GRAMMAR;
447
420
  }
448
421
 
449
422
  /** What is owed leads, so it comes back after a harness compacts; then the acknowledgement. */
@@ -536,7 +509,7 @@ if (import.meta.main) {
536
509
  Bun.stdin.stream(),
537
510
  );
538
511
  } catch (error) {
539
- console.error(`wfa: ${error instanceof Error ? error.message : String(error)}`);
512
+ console.error(`wfa: ${reasonOf(error)}`);
540
513
  process.exit(2);
541
514
  }
542
515
  const outcome = await runCli(process.argv.slice(2), stdinText);