@agentswf/engine 0.1.0-beta.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) 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 +35 -22
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +5 -7
  11. package/src/attempt-output.ts +15 -32
  12. package/src/attempt-view.ts +34 -33
  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 +41 -11
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +41 -33
  25. package/src/deadlines.ts +40 -7
  26. package/src/decisions/directory.ts +51 -40
  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 +43 -27
  32. package/src/emit-command.ts +88 -58
  33. package/src/emit.ts +254 -56
  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 +18 -6
  39. package/src/help/inbox.txt +6 -4
  40. package/src/help/logs.txt +9 -6
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +30 -11
  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 +7 -15
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +56 -133
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +40 -57
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.test.ts.template +3 -3
  55. package/src/init-template/hello/workflow.ts.template +5 -4
  56. package/src/jsonl.ts +10 -6
  57. package/src/keep-alive.ts +15 -12
  58. package/src/logs-command.ts +128 -47
  59. package/src/message-operation.ts +6 -3
  60. package/src/messaging/limits.ts +4 -4
  61. package/src/messaging/messaging.ts +31 -28
  62. package/src/messaging/routes.ts +1 -1
  63. package/src/messaging/text.ts +9 -7
  64. package/src/open-command.ts +770 -0
  65. package/src/operation-events.ts +4 -6
  66. package/src/operation-liveness.ts +19 -4
  67. package/src/operator-cli.ts +224 -94
  68. package/src/operator-runtime.ts +29 -7
  69. package/src/ops.ts +270 -0
  70. package/src/pane-layout.ts +105 -59
  71. package/src/progress-view.ts +10 -10
  72. package/src/project-engine.ts +79 -0
  73. package/src/projects.ts +13 -2
  74. package/src/refusals.ts +119 -0
  75. package/src/result-slots.ts +11 -31
  76. package/src/result-validation.ts +2 -2
  77. package/src/run-command.ts +45 -28
  78. package/src/run-commands.ts +43 -115
  79. package/src/run-event-log.ts +1 -1
  80. package/src/run-lines.ts +159 -0
  81. package/src/run-lock.ts +57 -1
  82. package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
  83. package/src/run-policy/stages.ts +113 -0
  84. package/src/run-policy/waits.ts +452 -0
  85. package/src/run-prepare.ts +205 -43
  86. package/src/run-read.ts +109 -41
  87. package/src/run-state.ts +170 -50
  88. package/src/run-text.ts +54 -0
  89. package/src/run-unreachable.ts +18 -0
  90. package/src/run-usage.ts +18 -12
  91. package/src/run-view.ts +81 -17
  92. package/src/runs-command.ts +83 -63
  93. package/src/runs.ts +310 -212
  94. package/src/sandbox-homes.ts +65 -13
  95. package/src/sandboxes.ts +182 -28
  96. package/src/session-lines.ts +93 -0
  97. package/src/skills/run-skills.ts +8 -1
  98. package/src/sources.ts +28 -29
  99. package/src/spend.ts +333 -0
  100. package/src/stage-ledger.ts +119 -131
  101. package/src/stop-crashed.ts +6 -5
  102. package/src/stopped.ts +4 -4
  103. package/src/test-command.ts +36 -14
  104. package/src/testing/operation-events.ts +3 -23
  105. package/src/testing.ts +73 -26
  106. package/src/tick-command.ts +145 -15
  107. package/src/tools.ts +25 -16
  108. package/src/turn-ends.ts +37 -0
  109. package/src/waits.ts +651 -0
  110. package/src/wake.ts +84 -54
  111. package/src/workflow-loader.ts +54 -4
  112. package/src/workflow-runner.ts +805 -580
  113. package/src/workflow-test-preload.ts +4 -1
  114. package/src/workflow-testing/coordinators.ts +3 -3
  115. package/src/workflow-testing/fake-client.ts +387 -0
  116. package/src/workflow-testing/host.ts +121 -53
  117. package/src/workflow-testing/index.ts +33 -842
  118. package/src/workflow-testing/run-test.ts +1128 -0
  119. package/src/workflow-testing/script.ts +74 -45
  120. package/src/hold-run-lock.fixture.ts +0 -8
  121. package/src/mailbox.ts +0 -990
  122. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  123. package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
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.4",
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.4",
33
- "@agentswf/harness": "0.1.0-beta.4",
34
- "@agentswf/sandbox": "0.1.0-beta.4",
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,6 +5,7 @@ import {
5
5
  validWaitingReason,
6
6
  WIRE_VERSION,
7
7
  } from "@agentswf/contract/wire";
8
+ import { durationMilliseconds, secondsForMilliseconds } from "@agentswf/contract/workflow";
8
9
  import { NotSentError, submitControl } from "./client";
9
10
 
10
11
  /** `wfa` is its launcher's full path, as an agent must type it; `wfa` alone when it is not known. */
@@ -45,9 +46,14 @@ function messageUsage(wfa: string, indent = "usage: "): string[] {
45
46
  ];
46
47
  }
47
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
+ }
48
54
  const STDIN_TIMEOUT_MS = 30_000;
49
55
 
50
- export type CliOutcome = { exitCode: number; stdout: string; stderr: string };
56
+ type CliOutcome = { exitCode: number; stdout: string; stderr: string };
51
57
 
52
58
  type Submit = (
53
59
  endpoint: string,
@@ -74,15 +80,15 @@ export async function runCli(
74
80
  switch (command) {
75
81
  case "waiting":
76
82
  return runWaiting(endpoint, session, args, submit);
83
+ case "send":
84
+ case "reply":
85
+ return runMessage(endpoint, session, command, args, stdin, wfa, submit);
77
86
  case "result":
78
87
  return runResult(endpoint, session, args, stdin, submit);
79
88
  case "tool":
80
89
  return runTool(endpoint, session, args, stdin, submit);
81
90
  case "peers":
82
91
  return runPeers(endpoint, session, args, submit);
83
- case "send":
84
- case "reply":
85
- return runMessage(endpoint, session, command, args, stdin, wfa, submit);
86
92
  default:
87
93
  return usageError(usage(wfa));
88
94
  }
@@ -112,7 +118,7 @@ async function runResult(
112
118
  ...(session ? { session } : {}),
113
119
  });
114
120
  } catch (error) {
115
- const reason = error instanceof Error ? error.message : String(error);
121
+ const reason = reasonOf(error);
116
122
  return { exitCode: 1, stdout: "", stderr: `wfa: result submission failed.\n${reason}` };
117
123
  }
118
124
 
@@ -164,7 +170,7 @@ async function runTool(
164
170
  null,
165
171
  );
166
172
  } catch (error) {
167
- const reason = error instanceof Error ? error.message : String(error);
173
+ const reason = reasonOf(error);
168
174
  return error instanceof NotSentError
169
175
  ? {
170
176
  exitCode: 3,
@@ -226,9 +232,9 @@ async function runWaiting(
226
232
  const duration = values.get("--timeout");
227
233
  let timeoutMs: number | undefined;
228
234
  if (duration !== undefined) {
229
- timeoutMs = durationOf(duration);
230
- if (timeoutMs === undefined)
231
- return usageError("wfa waiting --timeout must be a positive integer with ms, s, m or h");
235
+ const read = durationOf(duration);
236
+ if (typeof read === "string") return usageError(`wfa waiting --timeout ${read}`);
237
+ timeoutMs = read;
232
238
  }
233
239
  let response: ControlResponse;
234
240
  try {
@@ -241,7 +247,7 @@ async function runWaiting(
241
247
  ...(session ? { session } : {}),
242
248
  });
243
249
  } catch (error) {
244
- const detail = error instanceof Error ? error.message : String(error);
250
+ const detail = reasonOf(error);
245
251
  return {
246
252
  exitCode: 1,
247
253
  stdout: "",
@@ -286,7 +292,7 @@ async function runPeers(
286
292
  ...(session ? { session } : {}),
287
293
  });
288
294
  } catch (error) {
289
- const detail = error instanceof Error ? error.message : String(error);
295
+ const detail = reasonOf(error);
290
296
  return { exitCode: 1, stdout: "", stderr: `wfa: peers failed; nothing changed.\n${detail}` };
291
297
  }
292
298
  if (response.kind === "rejected")
@@ -335,7 +341,7 @@ async function runMessage(
335
341
  try {
336
342
  response = await submit(endpoint, request);
337
343
  } catch (error) {
338
- const detail = error instanceof Error ? error.message : String(error);
344
+ const detail = reasonOf(error);
339
345
  const what = command === "send" ? "message" : "reply";
340
346
  // Refused at the connection, nothing reached the engine; after, it may have been admitted.
341
347
  if (error instanceof NotSentError) {
@@ -377,9 +383,9 @@ function messageArgs(
377
383
  if (positional.length > 1) return { error: "flags go before the message" };
378
384
  if (arg === "--await-reply" && !awaitReply) awaitReply = true;
379
385
  else if (arg === "--timeout" && timeoutMs === undefined) {
380
- timeoutMs = durationOf(args[i + 1] ?? "");
381
- if (timeoutMs === undefined)
382
- return { error: "--timeout takes a positive integer with ms, s, m or h, such as 5m" };
386
+ const read = durationOf(args[i + 1] ?? "");
387
+ if (typeof read === "string") return { error: `--timeout ${read}` };
388
+ timeoutMs = read;
383
389
  i += 1;
384
390
  } else if (positional.length === 1 && arg !== "--await-reply" && arg !== "--timeout") {
385
391
  return { error: `${arg} is not a flag; a message that starts with -- goes in a heredoc` };
@@ -398,12 +404,19 @@ function messageArgs(
398
404
  };
399
405
  }
400
406
 
401
- function durationOf(text: string): number | undefined {
402
- const match = /^([0-9]+)(ms|s|m|h)$/.exec(text);
403
- if (!match) return undefined;
404
- const factor = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 }[match[2]!]!;
405
- const value = Number(match[1]) * factor;
406
- return Number.isSafeInteger(value) && value > 0 ? value : undefined;
407
+ const DURATION_GRAMMAR = 'takes a duration, a decimal and s, m, h or d, such as "5m"';
408
+
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}`;
413
+ let milliseconds: number;
414
+ try {
415
+ milliseconds = durationMilliseconds(text);
416
+ } catch {
417
+ return DURATION_GRAMMAR;
418
+ }
419
+ return milliseconds > 0 ? milliseconds : DURATION_GRAMMAR;
407
420
  }
408
421
 
409
422
  /** What is owed leads, so it comes back after a harness compacts; then the acknowledgement. */
@@ -496,7 +509,7 @@ if (import.meta.main) {
496
509
  Bun.stdin.stream(),
497
510
  );
498
511
  } catch (error) {
499
- console.error(`wfa: ${error instanceof Error ? error.message : String(error)}`);
512
+ console.error(`wfa: ${reasonOf(error)}`);
500
513
  process.exit(2);
501
514
  }
502
515
  const outcome = await runCli(process.argv.slice(2), stdinText);
@@ -1,37 +1,11 @@
1
1
  import { createConnection, type Socket } from "node:net";
2
- import {
3
- type ControlRequest,
4
- decodeResponseFor,
5
- type ResponseFor,
6
- type ResultSubmitRequest,
7
- type ResultSubmitResponse,
8
- type WaitingRequest,
9
- type WaitingResponse,
10
- } from "@agentswf/contract/wire";
2
+ import { type ControlRequest, decodeResponseFor, type ResponseFor } from "@agentswf/contract/wire";
11
3
 
12
4
  const MAX_RESPONSE_BYTES = 256 * 1024;
13
5
 
14
6
  /** The connection never opened: nothing was written, so nothing reached the engine. */
15
7
  export class NotSentError extends Error {}
16
8
 
17
- export function submitResult(
18
- endpoint: string,
19
- request: ResultSubmitRequest,
20
- timeoutSeconds = 30,
21
- connect: (endpoint: string) => Socket = createConnection,
22
- ): Promise<ResultSubmitResponse> {
23
- return submitControl(endpoint, request, timeoutSeconds, connect);
24
- }
25
-
26
- export function submitWaiting(
27
- endpoint: string,
28
- request: WaitingRequest,
29
- timeoutSeconds = 30,
30
- connect: (endpoint: string) => Socket = createConnection,
31
- ): Promise<WaitingResponse> {
32
- return submitControl(endpoint, request, timeoutSeconds, connect);
33
- }
34
-
35
9
  /** Sends one request and resolves with a response that answers its command. */
36
10
  export function submitControl<R extends ControlRequest>(
37
11
  endpoint: string,
@@ -5,19 +5,21 @@ import type {
5
5
  } from "@agentswf/contract/records";
6
6
  import { HARNESS_NAMES, type Harness } from "@agentswf/harness";
7
7
  import { planPrice } from "./accounting/plans";
8
- import { messageOf } from "./errors";
8
+ import type { Out } from "./command-line";
9
9
  import help from "./help/allowance.txt" with { type: "text" };
10
- import type { Out } from "./run-commands";
10
+ import { asksJson, printRefusal } from "./refusals";
11
+
12
+ /** `awf allowance --json`'s `"v"`, which its refusals carry too. */
13
+ export const ALLOWANCE_V = 1 satisfies AllowanceReport["v"];
11
14
 
12
15
  export const allowanceUsage = help.trimEnd();
13
16
 
14
- export type AllowanceCommand = { harnesses: Harness[]; json: boolean } | "help";
17
+ type AllowanceCommand = { harnesses: Harness[]; json: boolean };
15
18
 
16
19
  export function parseAllowanceCommand(argv: readonly string[]): AllowanceCommand {
17
20
  const harnesses: Harness[] = [];
18
21
  let json = false;
19
22
  for (const arg of argv) {
20
- if (arg === "-h" || arg === "--help") return "help";
21
23
  if (arg === "--json") json = true;
22
24
  else if ((HARNESS_NAMES as readonly string[]).includes(arg)) {
23
25
  if (!harnesses.includes(arg as Harness)) harnesses.push(arg as Harness);
@@ -35,13 +37,9 @@ export async function allowanceCommand(
35
37
  try {
36
38
  command = parseAllowanceCommand(argv);
37
39
  } catch (error) {
38
- io.stderr(`awf: ${messageOf(error)}\n\n${allowanceUsage}`);
40
+ printRefusal(io, { json: asksJson(argv), v: ALLOWANCE_V }, error, "usage", allowanceUsage);
39
41
  return 2;
40
42
  }
41
- if (command === "help") {
42
- io.stdout(allowanceUsage);
43
- return 0;
44
- }
45
43
  const report = await readReport(command.harnesses, io.read, io.now());
46
44
  io.stdout(command.json ? JSON.stringify(report, null, 2) : describeReport(report));
47
45
  return 0;
@@ -54,7 +52,7 @@ export async function readReport(
54
52
  now: number,
55
53
  ): Promise<AllowanceReport> {
56
54
  return {
57
- v: 1,
55
+ v: ALLOWANCE_V,
58
56
  readAt: new Date(now).toISOString(),
59
57
  harnesses: await Promise.all(harnesses.map(read)),
60
58
  };