@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.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +35 -22
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +5 -7
- package/src/attempt-output.ts +15 -32
- package/src/attempt-view.ts +34 -33
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +41 -11
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +41 -33
- package/src/deadlines.ts +40 -7
- package/src/decisions/directory.ts +51 -40
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +43 -27
- package/src/emit-command.ts +88 -58
- package/src/emit.ts +254 -56
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +18 -6
- package/src/help/inbox.txt +6 -4
- package/src/help/logs.txt +9 -6
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +30 -11
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +7 -15
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +56 -133
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +40 -57
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +5 -4
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +15 -12
- package/src/logs-command.ts +128 -47
- package/src/message-operation.ts +6 -3
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +31 -28
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +9 -7
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +224 -94
- package/src/operator-runtime.ts +29 -7
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +105 -59
- package/src/progress-view.ts +10 -10
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +45 -28
- package/src/run-commands.ts +43 -115
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +205 -43
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +18 -12
- package/src/run-view.ts +81 -17
- package/src/runs-command.ts +83 -63
- package/src/runs.ts +310 -212
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +28 -29
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +119 -131
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +36 -14
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +73 -26
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +25 -16
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +54 -4
- package/src/workflow-runner.ts +805 -580
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +3 -3
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +121 -53
- package/src/workflow-testing/index.ts +33 -842
- package/src/workflow-testing/run-test.ts +1128 -0
- package/src/workflow-testing/script.ts +74 -45
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -990
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- 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
|
|
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
|
|
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
|
|
33
|
-
"@agentswf/harness": "0.1.0
|
|
34
|
-
"@agentswf/sandbox": "0.1.0
|
|
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": {
|
package/src/accounting/format.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
26
|
+
* agent or decision key, timed by what ran in it.
|
|
24
27
|
*/
|
|
25
28
|
export function summarizeRun(
|
|
26
|
-
usage: readonly
|
|
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 :
|
|
42
|
-
const decisionStageOf = staged ? workflowStageOf :
|
|
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
|
|
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" : "
|
|
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
|
|
98
|
+
function figuresOf(records: readonly PricedOperation[], prices: PriceTable): AccountingFigures {
|
|
96
99
|
let tokens = NONE;
|
|
97
100
|
let delegated = NONE;
|
|
98
|
-
let
|
|
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
|
|
117
|
-
if (record.
|
|
118
|
-
|
|
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
|
-
...(
|
|
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
|
|
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
|
|
162
|
-
if (record.
|
|
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
|
-
...(
|
|
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
|
|
209
|
-
return [...record.callPath,
|
|
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
|
|
253
|
-
return [...record.callPath,
|
|
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,
|
|
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")
|
|
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 =
|
|
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("
|
|
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("
|
|
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" | "
|
|
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 },
|
package/src/agent-cli/cli.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
381
|
-
if (
|
|
382
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const
|
|
406
|
-
|
|
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: ${
|
|
512
|
+
console.error(`wfa: ${reasonOf(error)}`);
|
|
500
513
|
process.exit(2);
|
|
501
514
|
}
|
|
502
515
|
const outcome = await runCli(process.argv.slice(2), stdinText);
|
package/src/agent-cli/client.ts
CHANGED
|
@@ -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,
|
package/src/allowance-command.ts
CHANGED
|
@@ -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 {
|
|
8
|
+
import type { Out } from "./command-line";
|
|
9
9
|
import help from "./help/allowance.txt" with { type: "text" };
|
|
10
|
-
import
|
|
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
|
-
|
|
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
|
|
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:
|
|
55
|
+
v: ALLOWANCE_V,
|
|
58
56
|
readAt: new Date(now).toISOString(),
|
|
59
57
|
harnesses: await Promise.all(harnesses.map(read)),
|
|
60
58
|
};
|