@agentswf/engine 0.1.0-beta.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
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,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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 (
|
|
248
|
-
timeoutMs = read
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
405
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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 =
|
|
415
|
+
milliseconds = durationMilliseconds(text);
|
|
437
416
|
} catch {
|
|
438
|
-
return
|
|
417
|
+
return DURATION_GRAMMAR;
|
|
439
418
|
}
|
|
440
|
-
|
|
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: ${
|
|
512
|
+
console.error(`wfa: ${reasonOf(error)}`);
|
|
540
513
|
process.exit(2);
|
|
541
514
|
}
|
|
542
515
|
const outcome = await runCli(process.argv.slice(2), stdinText);
|