@agentswf/engine 0.0.0-stage → 0.1.0-beta.1

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 (82) hide show
  1. package/package.json +39 -4
  2. package/src/accounting/format.ts +252 -0
  3. package/src/accounting/plans.ts +38 -0
  4. package/src/accounting/prices.ts +98 -0
  5. package/src/accounting/summary.ts +385 -0
  6. package/src/accounting/tokens.ts +34 -0
  7. package/src/agent-launcher.ts +127 -0
  8. package/src/allowance-command.ts +105 -0
  9. package/src/attempt-ending.ts +198 -0
  10. package/src/attempt-output.ts +132 -0
  11. package/src/attempt-view.ts +362 -0
  12. package/src/canonical-json.ts +12 -0
  13. package/src/control-plane.ts +424 -0
  14. package/src/deadlines.ts +106 -0
  15. package/src/decisions/directory.ts +432 -0
  16. package/src/decisions/fake.ts +60 -0
  17. package/src/decisions/openrouter.ts +161 -0
  18. package/src/decisions/seam.ts +70 -0
  19. package/src/display-path.ts +16 -0
  20. package/src/duration.ts +12 -0
  21. package/src/errors.ts +4 -0
  22. package/src/files.ts +78 -0
  23. package/src/herdr-placement.ts +182 -0
  24. package/src/herdr-run-session.ts +530 -0
  25. package/src/herdr-workspace-marks.ts +194 -0
  26. package/src/here.ts +289 -0
  27. package/src/index.ts +5 -0
  28. package/src/init-command.ts +240 -0
  29. package/src/init-template/AGENTS.md.template +16 -0
  30. package/src/init-template/hello/workflow.test.ts.template +17 -0
  31. package/src/init-template/hello/workflow.ts.template +26 -0
  32. package/src/init-template/tsconfig.json.template +11 -0
  33. package/src/jsonl.ts +74 -0
  34. package/src/logs-command.ts +605 -0
  35. package/src/machine.ts +27 -0
  36. package/src/message-operation.ts +245 -0
  37. package/src/messaging/limits.ts +35 -0
  38. package/src/messaging/messaging.ts +1303 -0
  39. package/src/messaging/routes.ts +79 -0
  40. package/src/messaging/text.ts +251 -0
  41. package/src/operation-events.ts +182 -0
  42. package/src/operation-liveness.ts +674 -0
  43. package/src/operator-aliases.ts +10 -0
  44. package/src/operator-cli.ts +600 -0
  45. package/src/operator-runtime.ts +503 -0
  46. package/src/pane-layout.ts +125 -0
  47. package/src/progress-view.ts +568 -0
  48. package/src/result-slots.ts +325 -0
  49. package/src/result-validation.ts +47 -0
  50. package/src/run-command.ts +242 -0
  51. package/src/run-commands.ts +215 -0
  52. package/src/run-event-log.ts +72 -0
  53. package/src/run-prepare.ts +178 -0
  54. package/src/run-progress.ts +137 -0
  55. package/src/run-read.ts +425 -0
  56. package/src/run-state.ts +120 -0
  57. package/src/run-usage.ts +393 -0
  58. package/src/run-view.ts +225 -0
  59. package/src/runs-command.ts +356 -0
  60. package/src/runs.ts +645 -0
  61. package/src/sandbox-homes.ts +211 -0
  62. package/src/sandboxes.ts +521 -0
  63. package/src/skills/fetch.ts +305 -0
  64. package/src/skills/run-skills.ts +181 -0
  65. package/src/skills/sources.ts +87 -0
  66. package/src/skills/tree.ts +60 -0
  67. package/src/stage-ledger.ts +535 -0
  68. package/src/stage-plan.ts +127 -0
  69. package/src/stage-schema.ts +88 -0
  70. package/src/stopped.ts +36 -0
  71. package/src/test-command.ts +110 -0
  72. package/src/testing/operation-events.ts +89 -0
  73. package/src/testing.ts +164 -0
  74. package/src/workflow-loader.ts +113 -0
  75. package/src/workflow-runner.ts +3666 -0
  76. package/src/workflow-test-preload.ts +13 -0
  77. package/src/workflow-testing/decisions.ts +129 -0
  78. package/src/workflow-testing/host.ts +811 -0
  79. package/src/workflow-testing/index.ts +420 -0
  80. package/src/workflow-testing/script.ts +447 -0
  81. package/src/workflow-testing/workflow-testing.typecheck.ts +47 -0
  82. package/README.md +0 -3
@@ -0,0 +1,194 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { machinePaths } from "./machine";
5
+ import { type ProcessProbe, processStart, sameProcess } from "./runs";
6
+
7
+ export const WORKSPACE_MARK_VERSION = 2;
8
+
9
+ /**
10
+ * A run's claim on its workspace in a shared Herdr session: the run's process, and when that
11
+ * started, which tells a reused pid apart. Written before the workspace exists, so a session in
12
+ * use is never taken for empty, and given the workspace's id once it does: only that id is ever
13
+ * closed by it, as a label is unique only within its project's runs.
14
+ */
15
+ export type WorkspaceMark = {
16
+ /** 1 names only the workspace; an older awf reads only 1, and leaves a newer run's alone. */
17
+ version: 1 | typeof WORKSPACE_MARK_VERSION;
18
+ label: string;
19
+ pid: number;
20
+ processStart: string;
21
+ workspaceId?: string;
22
+ /**
23
+ * Every pane the run made and has not closed, by the session it is in, its own workspace's
24
+ * included: a pane closes by id and terminal id both, as Herdr's pane ids repeat.
25
+ */
26
+ panes?: Record<string, MarkedPane[]>;
27
+ /** The run ended, leaving the panes it kept: this mark outlives it until they are gone. */
28
+ ended?: true;
29
+ };
30
+
31
+ /** As the Herdr host reports a pane it made (`MadePane`); kept apart, as this is the record. */
32
+ export type MarkedPane = { paneId: string; terminalId?: string; workspaceId: string; kept?: true };
33
+
34
+ export type Liveness = "live" | "dead" | "unknown";
35
+
36
+ export type HeldMark = {
37
+ /** The workspace this run created. */
38
+ bind(workspaceId: string): Promise<void>;
39
+ /** Every pane the run has made in `session` and not closed. */
40
+ panes(session: string, panes: readonly MarkedPane[]): Promise<void>;
41
+ /** At the run's end: removed, or, where it kept panes, rewritten to name only those. */
42
+ release(): Promise<void>;
43
+ };
44
+
45
+ export function marksDir(home: string, session: string): string {
46
+ return join(machinePaths(home).herdr, "sessions", session, "workspaces");
47
+ }
48
+
49
+ /**
50
+ * Marks the workspace labelled `label` as this process's, a file of its own. Best effort: a run
51
+ * whose mark can't be written still runs, and its workspace reads as nobody's, which is kept.
52
+ */
53
+ export async function markWorkspace(
54
+ home: string,
55
+ session: string,
56
+ label: string,
57
+ probe: ProcessProbe = processStart,
58
+ ): Promise<HeldMark> {
59
+ const unmarked: HeldMark = {
60
+ bind: async () => undefined,
61
+ panes: async () => undefined,
62
+ release: async () => undefined,
63
+ };
64
+ const started = probe(process.pid);
65
+ if (started === undefined) return unmarked;
66
+ const dir = marksDir(home, session);
67
+ const file = join(dir, `${process.pid}-${randomBytes(4).toString("hex")}.json`);
68
+ const mark: WorkspaceMark = {
69
+ version: WORKSPACE_MARK_VERSION,
70
+ label,
71
+ pid: process.pid,
72
+ processStart: started,
73
+ };
74
+ try {
75
+ await mkdir(dir, { recursive: true });
76
+ await writeWhole(file, mark);
77
+ } catch {
78
+ return unmarked;
79
+ }
80
+ let current = mark;
81
+ // One write at a time, each of the mark as it is then: a later one never lands before an earlier.
82
+ let writing = Promise.resolve();
83
+ /** Once released, nothing writes it again: a late write would bring a removed mark back. */
84
+ let released = false;
85
+ const write = (next: WorkspaceMark) => {
86
+ if (released) return writing;
87
+ current = next;
88
+ writing = writing.then(() => writeWhole(file, current).catch(() => undefined));
89
+ return writing;
90
+ };
91
+ return {
92
+ bind: (workspaceId) => write({ ...current, workspaceId }),
93
+ panes: (session, panes) =>
94
+ write({ ...current, panes: { ...current.panes, [session]: [...panes] } }),
95
+ release: async () => {
96
+ if (released) return writing;
97
+ const kept = Object.fromEntries(
98
+ Object.entries(current.panes ?? {})
99
+ .map(([session, panes]) => [session, panes.filter((pane) => pane.kept)] as const)
100
+ .filter(([, panes]) => panes.length > 0),
101
+ );
102
+ if (Object.keys(kept).length === 0) {
103
+ released = true;
104
+ await writing;
105
+ return removeMark(file);
106
+ }
107
+ await write({ ...current, panes: kept, ended: true });
108
+ released = true;
109
+ },
110
+ };
111
+ }
112
+
113
+ /** Renamed into place, so a reader never sees half a mark and skips a live run. */
114
+ async function writeWhole(file: string, mark: WorkspaceMark): Promise<void> {
115
+ const temporary = `${file}.${randomBytes(4).toString("hex")}.tmp`;
116
+ try {
117
+ await writeFile(temporary, JSON.stringify(mark));
118
+ await rename(temporary, file);
119
+ } finally {
120
+ await rm(temporary, { force: true }).catch(() => undefined);
121
+ }
122
+ }
123
+
124
+ export async function readMarks(
125
+ home: string,
126
+ session: string,
127
+ ): Promise<(WorkspaceMark & { file: string })[]> {
128
+ const dir = marksDir(home, session);
129
+ const files = await readdir(dir).catch(() => [] as string[]);
130
+ const marks: (WorkspaceMark & { file: string })[] = [];
131
+ for (const name of files.filter((file) => file.endsWith(".json"))) {
132
+ const file = join(dir, name);
133
+ try {
134
+ const mark = JSON.parse(await readFile(file, "utf8"));
135
+ if (
136
+ (mark?.version === 1 || mark?.version === WORKSPACE_MARK_VERSION) &&
137
+ typeof mark.label === "string" &&
138
+ typeof mark.pid === "number" &&
139
+ typeof mark.processStart === "string" &&
140
+ (mark.workspaceId === undefined || typeof mark.workspaceId === "string")
141
+ ) {
142
+ marks.push({ ...mark, file });
143
+ }
144
+ } catch {}
145
+ }
146
+ return marks;
147
+ }
148
+
149
+ /** Every session's marks, by the session whose directory holds them. */
150
+ export async function readAllMarks(
151
+ home: string,
152
+ ): Promise<(WorkspaceMark & { file: string; session: string })[]> {
153
+ const sessions = await readdir(join(machinePaths(home).herdr, "sessions")).catch(
154
+ () => [] as string[],
155
+ );
156
+ const all = await Promise.all(
157
+ sessions.map(async (session) =>
158
+ (await readMarks(home, session)).map((mark) => ({ ...mark, session })),
159
+ ),
160
+ );
161
+ return all.flat();
162
+ }
163
+
164
+ /** Rewrites a mark read from `file`, as one sweep leaves it. */
165
+ export function rewriteMark(file: string, mark: WorkspaceMark): Promise<void> {
166
+ return writeWhole(file, mark).catch(() => undefined);
167
+ }
168
+
169
+ export function removeMark(file: string): Promise<void> {
170
+ return rm(file, { force: true }).catch(() => undefined);
171
+ }
172
+
173
+ /**
174
+ * Dead only when its process is gone, or another holds its pid. One `ps` can't see, as from a
175
+ * sandbox, is unknown: closing a live run's workspace loses its work.
176
+ */
177
+ export function liveness(
178
+ mark: Pick<WorkspaceMark, "pid" | "processStart">,
179
+ probe: ProcessProbe = processStart,
180
+ exists: (pid: number) => boolean = processExists,
181
+ ): Liveness {
182
+ if (!exists(mark.pid)) return "dead";
183
+ if (probe(mark.pid) === undefined) return "unknown";
184
+ return sameProcess(mark.pid, mark.processStart, probe) ? "live" : "dead";
185
+ }
186
+
187
+ function processExists(pid: number): boolean {
188
+ try {
189
+ process.kill(pid, 0);
190
+ return true;
191
+ } catch (error) {
192
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
193
+ }
194
+ }
package/src/here.ts ADDED
@@ -0,0 +1,289 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { rmSync } from "node:fs";
3
+ import { mkdir, readFile, rm } from "node:fs/promises";
4
+ import { basename, join } from "node:path";
5
+ import {
6
+ type CallerPane,
7
+ focusTab,
8
+ HARNESSES,
9
+ herdrReachable,
10
+ runProcess,
11
+ searchCaller,
12
+ startInNewTab,
13
+ } from "@agentswf/harness";
14
+ import { messageOf } from "./errors";
15
+ import { linkNew } from "./files";
16
+ import { runSessionName } from "./herdr-run-session";
17
+ import type { OperatorEnvironment } from "./operator-cli";
18
+ import { callerSession, herdrConfig } from "./operator-runtime";
19
+ import type { RunCommand } from "./run-command";
20
+ import { startedLine } from "./run-commands";
21
+ import { loadAndPrepare } from "./run-prepare";
22
+ import { generateId, processStart, sameProcess } from "./runs";
23
+
24
+ /** What `--here` and `--session` reach Herdr and the calling session through. */
25
+ type HereEnvironment = Pick<
26
+ OperatorEnvironment,
27
+ "herdr" | "environment" | "self" | "callerSearchMs" | "signal" | "now"
28
+ >;
29
+
30
+ export type Caller = { pane: CallerPane; session: string };
31
+
32
+ export const SESSION_CODE = /^awf-here-[0-9a-f]{8}$/;
33
+ const CALLER_SEARCH_MS = 120_000;
34
+
35
+ /**
36
+ * `awf run --here`, in an agent's shell: checks the workflow loads and its run can start, and that
37
+ * the session can be driven, then has Herdr start the run in a new tab, outside this shell and any
38
+ * sandbox it is in, and prints the code the agent ends its turn with (ADR 0010). Nothing is started
39
+ * when a check fails.
40
+ */
41
+ export async function startHere(
42
+ argv: readonly string[],
43
+ command: RunCommand,
44
+ environment: HereEnvironment,
45
+ stdout: (text: string) => void,
46
+ stderr: (text: string) => void,
47
+ notice: (text: string) => void,
48
+ ): Promise<number> {
49
+ // In this shell, before any tab opens: a workflow that will not load or a run that will not
50
+ // start is said here, where the agent reads it, and not in a tab nobody is looking at.
51
+ const ready = await loadAndPrepare(command, (environment.now ?? Date.now)());
52
+ if ("refused" in ready) {
53
+ stderr(`awf: ${ready.refused}`);
54
+ return 2;
55
+ }
56
+ for (const line of ready.prepared.continued?.warnings ?? []) notice(line);
57
+ const env = environment.environment ?? process.env;
58
+ const run = environment.herdr ?? runProcess;
59
+ const refuse = (why: string, instead: string) => {
60
+ stderr(`awf: --here: ${why}. ${instead}`);
61
+ return 1;
62
+ };
63
+ const workspace = env.HERDR_WORKSPACE_ID;
64
+ if (env.HERDR_ENV !== "1" || !workspace) {
65
+ return refuse(
66
+ "this session is not in a Herdr pane, so a run cannot drive it",
67
+ "Start the agent in a Herdr pane, or run the workflow from a shell with awf run and no --here.",
68
+ );
69
+ }
70
+ try {
71
+ // Said in this shell: the run's own tab would find it only after taking the session over.
72
+ runSessionName(env);
73
+ } catch (error) {
74
+ stderr(`awf: --here: ${messageOf(error)}`);
75
+ return 2;
76
+ }
77
+ let session: string;
78
+ try {
79
+ session = await callerSession(run, env);
80
+ } catch (error) {
81
+ return refuse(`Herdr did not answer: ${messageOf(error)}`, sandboxFix(env));
82
+ }
83
+ const unreachable = await herdrReachable(herdrConfig(session), run);
84
+ if (unreachable) {
85
+ return refuse(`this session cannot reach Herdr: ${unreachable.trim()}`, sandboxFix(env));
86
+ }
87
+ const code = `awf-here-${randomBytes(4).toString("hex")}`;
88
+ // Chosen here, so this shell, which the agent reads, can name the run its tab starts.
89
+ const id = command.continueId ?? ready.prepared.id ?? generateId();
90
+ const end = argv.indexOf("--");
91
+ const options = (end === -1 ? argv.slice(1) : argv.slice(1, end)).filter(
92
+ (arg) => arg !== "--here",
93
+ );
94
+ const rest = end === -1 ? [] : argv.slice(end);
95
+ const self = environment.self ?? [process.execPath, "--no-env-file", process.argv[1]!];
96
+ const started = await startInNewTab(
97
+ herdrConfig(session),
98
+ {
99
+ // Under codex this can be another pane's workspace (E8); the run's tab still works from it.
100
+ workspace,
101
+ cwd: command.shellCwd,
102
+ label: tabLabel(ready.loaded.executable.definition.meta.name, id),
103
+ argv: [
104
+ ...self,
105
+ "run",
106
+ "--session",
107
+ code,
108
+ ...((command.continueId ?? ready.prepared.id) ? [] : ["--id", id]),
109
+ ...options,
110
+ ...rest,
111
+ ],
112
+ // The tab takes the Herdr server's environment, not this shell's: the operator's choice of
113
+ // where the run's agents open is passed on, and the session's own id, which only this shell
114
+ // has, for a fork of it.
115
+ env: {
116
+ ...(env.AWF_HERDR_SESSION ? { AWF_HERDR_SESSION: env.AWF_HERDR_SESSION } : {}),
117
+ ...callerSessions(env),
118
+ },
119
+ },
120
+ run,
121
+ );
122
+ if (!started.ok) {
123
+ return refuse(
124
+ `Herdr did not open the run's tab: ${started.error}`,
125
+ "Run the workflow from a shell with awf run instead.",
126
+ );
127
+ }
128
+ notice(startedLine(ready.loaded.executable.definition.meta.name, id, command));
129
+ stdout(
130
+ [
131
+ `awf: ${basename(ready.loaded.file)} is starting in Herdr tab ${started.tabId}. Once this turn ends it takes this session over: each of its steps arrives here as a prompt, and a last message hands the session back.`,
132
+ "",
133
+ "End your turn now by replying with only this line, exactly:",
134
+ code,
135
+ ].join("\n"),
136
+ );
137
+ return 0;
138
+ }
139
+
140
+ /**
141
+ * `awf run --session`: the session showing `code`, marked as driven by this process until the
142
+ * release, under `callers`. The reason when it can't be had, said in this run's own tab too.
143
+ */
144
+ export async function takeCaller(
145
+ code: string,
146
+ callers: string,
147
+ environment: HereEnvironment,
148
+ ): Promise<{ caller: Caller; release: () => void } | string> {
149
+ const found = await findCaller(code, environment);
150
+ let refused: string;
151
+ if (found.kind === "found") {
152
+ const release = await claimCaller(callers, found.caller.pane.paneId);
153
+ if (typeof release !== "string") return { caller: found.caller, release };
154
+ refused = release;
155
+ } else {
156
+ refused = found.reason;
157
+ }
158
+ // The session that started this waits on a run that will not come, and this tab opened
159
+ // unfocused: it is the one place that says why.
160
+ await showOwnTab(environment);
161
+ return refused;
162
+ }
163
+
164
+ /** Carries the calling session's ids, by harness, from `--here`'s shell to the run's tab. */
165
+ const CALLER_SESSIONS = "AWF_CALLER_SESSIONS";
166
+
167
+ /**
168
+ * Each harness's session variable this shell has, which a harness sets in its own tool calls'
169
+ * shells: the session's native id, by harness, since which harness the pane runs is found later.
170
+ */
171
+ function callerSessions(env: Readonly<Record<string, string | undefined>>): Record<string, string> {
172
+ const sessions = Object.fromEntries(
173
+ Object.entries(HARNESSES).flatMap(([harness, spec]) => {
174
+ const id = spec.sessionEnv ? env[spec.sessionEnv]?.trim() : undefined;
175
+ return id ? [[harness, id]] : [];
176
+ }),
177
+ );
178
+ return Object.keys(sessions).length ? { [CALLER_SESSIONS]: JSON.stringify(sessions) } : {};
179
+ }
180
+
181
+ /** The calling session's id `--here` passed on for `harness`, where it had one. */
182
+ function passedSession(
183
+ env: Readonly<Record<string, string | undefined>>,
184
+ harness: string,
185
+ ): string | undefined {
186
+ try {
187
+ const id = JSON.parse(env[CALLER_SESSIONS] ?? "{}")?.[harness];
188
+ return typeof id === "string" && id ? id : undefined;
189
+ } catch {
190
+ return undefined;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * What lets a sandboxed session reach Herdr's socket, which `--here` and every `wf` call need: its
196
+ * harness's own advice, for the harness whose session variable this shell has.
197
+ */
198
+ function sandboxFix(env: Readonly<Record<string, string | undefined>>): string {
199
+ const harness = Object.values(HARNESSES).find(
200
+ (spec) => spec.sessionEnv && env[spec.sessionEnv] && spec.localSockets,
201
+ );
202
+ return harness?.localSockets
203
+ ? `${harness.localSockets}.`
204
+ : "If this session runs in a sandbox, let it reach Herdr's socket and local sockets, or run the workflow from a shell with awf run.";
205
+ }
206
+
207
+ /**
208
+ * Marks `paneId` as driven by this process until the returned release, under `~/.awf`, which every
209
+ * run on this machine shares whatever its run root: a second run started from a driven session is
210
+ * refused, as ADR 0010 allows one at a time. A mark whose process is gone, or is another process
211
+ * with its pid, is taken over. The reason when refused.
212
+ */
213
+ async function claimCaller(marks: string, paneId: string): Promise<(() => void) | string> {
214
+ const mark = join(marks, `${paneId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
215
+ const started = processStart(process.pid);
216
+ if (started === undefined) return `ps gave no start time for this process (${process.pid})`;
217
+ const holder: CallerMark = { pid: process.pid, processStart: started };
218
+ try {
219
+ await mkdir(marks, { recursive: true });
220
+ for (let attempt = 0; attempt < 2; attempt += 1) {
221
+ if (await linkNew(mark, holder)) return () => rmSync(mark, { force: true });
222
+ const other = await readMark(mark);
223
+ if (other && sameProcess(other.pid, other.processStart)) {
224
+ return `another run (process ${other.pid}) is already driving the session in ${paneId}; one run drives a session at a time`;
225
+ }
226
+ await rm(mark, { force: true });
227
+ }
228
+ return `could not mark the session in ${paneId} as driven`;
229
+ } catch (error) {
230
+ return messageOf(error);
231
+ }
232
+ }
233
+
234
+ /** The run driving a session: its process, and when that started, which tells a reused pid apart. */
235
+ type CallerMark = { pid: number; processStart: string };
236
+
237
+ /** A mark as written, or undefined for one gone or unreadable, which nothing holds. */
238
+ async function readMark(file: string): Promise<CallerMark | undefined> {
239
+ try {
240
+ const mark = JSON.parse(await readFile(file, "utf8"));
241
+ return typeof mark?.pid === "number" && typeof mark.processStart === "string"
242
+ ? mark
243
+ : undefined;
244
+ } catch {
245
+ return undefined;
246
+ }
247
+ }
248
+
249
+ /** Brings the tab this process runs in forward, where Herdr says which one that is. */
250
+ export async function showOwnTab(environment: HereEnvironment): Promise<void> {
251
+ const env = environment.environment ?? process.env;
252
+ if (!env.HERDR_TAB_ID) return;
253
+ const session = await callerSession(environment.herdr ?? runProcess, env).catch(() => undefined);
254
+ if (session) await focusTab(herdrConfig(session), env.HERDR_TAB_ID, environment.herdr);
255
+ }
256
+
257
+ /** The pane showing `code`, in the Herdr session this run's tab is in, for `--session`. */
258
+ async function findCaller(
259
+ code: string,
260
+ environment: HereEnvironment,
261
+ ): Promise<{ kind: "found"; caller: Caller } | { kind: "refused"; reason: string }> {
262
+ const run = environment.herdr ?? runProcess;
263
+ let session: string;
264
+ try {
265
+ session = await callerSession(run, environment.environment ?? process.env);
266
+ } catch (error) {
267
+ return { kind: "refused", reason: messageOf(error) };
268
+ }
269
+ const found = await searchCaller(
270
+ herdrConfig(session),
271
+ code,
272
+ {
273
+ by: (environment.now ?? Date.now)() + (environment.callerSearchMs ?? CALLER_SEARCH_MS),
274
+ ...(environment.signal ? { signal: environment.signal } : {}),
275
+ },
276
+ run,
277
+ );
278
+ if (found.kind !== "found") return found;
279
+ const id = passedSession(environment.environment ?? process.env, found.pane.harness);
280
+ return {
281
+ kind: "found",
282
+ caller: { pane: { ...found.pane, ...(id ? { session: id } : {}) }, session },
283
+ };
284
+ }
285
+
286
+ /** The run's tab, named as its Herdr workspace is, but for the attempt, which isn't claimed yet. */
287
+ function tabLabel(workflow: string, id: string | undefined): string {
288
+ return `awf ${workflow}${id === undefined ? "" : ` ${id}`}`;
289
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // A finished run is re-priced from its `output.json` with another table.
2
+ export { describeAccounting } from "./accounting/format";
3
+ export { type ModelRate, type PriceTable, PUBLISHED_PRICES } from "./accounting/prices";
4
+ export { summarizeRun } from "./accounting/summary";
5
+ export * from "./workflow-runner";