@agentswf/engine 0.1.0-beta.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/package.json +13 -9
  4. package/src/accounting/format.ts +4 -3
  5. package/src/accounting/spent.ts +128 -0
  6. package/src/accounting/summary.ts +40 -32
  7. package/src/agent-cli/cli.ts +28 -55
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +4 -4
  11. package/src/attempt-output.ts +13 -31
  12. package/src/attempt-view.ts +33 -32
  13. package/src/author-typebox.ts +13 -0
  14. package/src/client/checks.ts +94 -0
  15. package/src/client/exported.ts +17 -0
  16. package/src/client/index.ts +122 -0
  17. package/src/client/locate.ts +34 -0
  18. package/src/client/published.ts +3 -0
  19. package/src/client/runs.ts +337 -0
  20. package/src/command-line.ts +64 -0
  21. package/src/commands.ts +7 -1
  22. package/src/control-plane.ts +4 -6
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +27 -28
  25. package/src/deadlines.ts +6 -40
  26. package/src/decisions/directory.ts +50 -37
  27. package/src/decisions/fake.ts +1 -1
  28. package/src/decisions/openrouter.ts +4 -2
  29. package/src/decisions/seam.ts +4 -4
  30. package/src/display-path.ts +3 -3
  31. package/src/duration.ts +19 -33
  32. package/src/emit-command.ts +38 -22
  33. package/src/emit.ts +148 -59
  34. package/src/event-ids.ts +31 -0
  35. package/src/files.ts +44 -4
  36. package/src/hand-over.ts +150 -0
  37. package/src/help/allowance.txt +4 -3
  38. package/src/help/emit.txt +11 -7
  39. package/src/help/inbox.txt +5 -3
  40. package/src/help/logs.txt +7 -5
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +13 -8
  44. package/src/help/runs.txt +4 -2
  45. package/src/help/tick.txt +9 -4
  46. package/src/herdr-placement.ts +3 -33
  47. package/src/herdr-run-session.ts +5 -13
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +54 -136
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +42 -56
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.ts.template +3 -2
  55. package/src/jsonl.ts +10 -6
  56. package/src/keep-alive.ts +7 -19
  57. package/src/logs-command.ts +134 -53
  58. package/src/message-operation.ts +6 -3
  59. package/src/messaging/messaging.ts +8 -10
  60. package/src/messaging/text.ts +2 -2
  61. package/src/open-command.ts +770 -0
  62. package/src/operation-events.ts +4 -6
  63. package/src/operation-liveness.ts +19 -4
  64. package/src/operator-cli.ts +206 -90
  65. package/src/operator-runtime.ts +24 -2
  66. package/src/ops.ts +270 -0
  67. package/src/pane-layout.ts +49 -96
  68. package/src/progress-view.ts +2 -2
  69. package/src/project-engine.ts +79 -0
  70. package/src/projects.ts +13 -2
  71. package/src/refusals.ts +119 -0
  72. package/src/result-slots.ts +11 -31
  73. package/src/result-validation.ts +2 -2
  74. package/src/run-command.ts +27 -31
  75. package/src/run-commands.ts +42 -114
  76. package/src/run-event-log.ts +1 -1
  77. package/src/run-lines.ts +159 -0
  78. package/src/run-lock.ts +57 -1
  79. package/src/run-policy/stage-plan.ts +154 -0
  80. package/src/run-policy/stages.ts +113 -0
  81. package/src/run-policy/waits.ts +452 -0
  82. package/src/run-prepare.ts +165 -41
  83. package/src/run-read.ts +109 -41
  84. package/src/run-state.ts +170 -50
  85. package/src/run-text.ts +54 -0
  86. package/src/run-unreachable.ts +18 -0
  87. package/src/run-usage.ts +17 -11
  88. package/src/run-view.ts +73 -14
  89. package/src/runs-command.ts +81 -63
  90. package/src/runs.ts +284 -222
  91. package/src/sandbox-homes.ts +65 -13
  92. package/src/sandboxes.ts +182 -28
  93. package/src/session-lines.ts +93 -0
  94. package/src/skills/run-skills.ts +8 -1
  95. package/src/sources.ts +2 -39
  96. package/src/spend.ts +333 -0
  97. package/src/stage-ledger.ts +111 -153
  98. package/src/stop-crashed.ts +6 -5
  99. package/src/stopped.ts +4 -4
  100. package/src/test-command.ts +37 -19
  101. package/src/testing/operation-events.ts +3 -23
  102. package/src/testing.ts +66 -18
  103. package/src/tick-command.ts +145 -15
  104. package/src/tools.ts +11 -30
  105. package/src/turn-ends.ts +37 -0
  106. package/src/waits.ts +651 -0
  107. package/src/wake.ts +84 -54
  108. package/src/workflow-loader.ts +49 -2
  109. package/src/workflow-runner.ts +525 -618
  110. package/src/workflow-test-preload.ts +4 -1
  111. package/src/workflow-testing/coordinators.ts +1 -1
  112. package/src/workflow-testing/fake-client.ts +387 -0
  113. package/src/workflow-testing/host.ts +28 -29
  114. package/src/workflow-testing/index.ts +9 -27
  115. package/src/workflow-testing/run-test.ts +201 -286
  116. package/src/workflow-testing/script.ts +7 -39
  117. package/src/deprecations.ts +0 -177
  118. package/src/hold-run-lock.fixture.ts +0 -8
  119. package/src/mailbox.ts +0 -972
  120. package/src/stage-plan.ts +0 -186
  121. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  122. package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
@@ -1,19 +1,12 @@
1
1
  import { open } from "node:fs/promises";
2
- import { join } from "node:path";
3
2
  import {
4
- OPERATION_LIVENESS_KINDS,
5
3
  OPERATION_LIVENESS_RECORD_VERSION,
6
4
  type OperationLivenessRecord,
7
5
  } from "@agentswf/contract/records";
6
+ import { validEvent } from "../operation-events";
7
+ import { liveFile } from "../ops";
8
8
 
9
- type OperationEvent = Pick<
10
- OperationLivenessRecord,
11
- "kind" | "at" | "sequence" | "reason" | "until" | "peer"
12
- >;
13
9
  const MAX_BYTES = 1024 * 1024;
14
- function eventFile(runDir: string, operationId: string): string {
15
- return join(runDir, "calls", operationId, "liveness.jsonl");
16
- }
17
10
 
18
11
  export async function readOperationEvents(
19
12
  runDir: string,
@@ -25,7 +18,7 @@ export async function readOperationEvents(
25
18
  let text: string;
26
19
  let clipped = false;
27
20
  try {
28
- const file = await open(eventFile(runDir, operationId), "r");
21
+ const file = await open(liveFile(runDir, operationId), "r");
29
22
  try {
30
23
  const buffer = Buffer.alloc(MAX_BYTES + 1);
31
24
  let size = 0;
@@ -63,19 +56,6 @@ export async function readOperationEvents(
63
56
  return { records, incomplete };
64
57
  }
65
58
 
66
- function validEvent(value: OperationEvent): boolean {
67
- return (
68
- typeof value.kind === "string" &&
69
- (OPERATION_LIVENESS_KINDS as readonly string[]).includes(value.kind) &&
70
- Number.isSafeInteger(value.at) &&
71
- value.at >= 0 &&
72
- Number.isSafeInteger(value.sequence) &&
73
- value.sequence >= 0 &&
74
- (value.reason === undefined || typeof value.reason === "string") &&
75
- (value.until === undefined || (Number.isSafeInteger(value.until) && value.until >= 0)) &&
76
- (value.peer === undefined || (typeof value.peer === "string" && value.peer.trim() !== ""))
77
- );
78
- }
79
59
  function isRecord(value: unknown, operationId: string): value is OperationLivenessRecord {
80
60
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
81
61
  const item = value as OperationLivenessRecord;
package/src/testing.ts CHANGED
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:
2
2
  import { createConnection } from "node:net";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import type { AttemptRecord } from "@agentswf/contract/records";
5
+ import type { AttemptRecord, StageLine } from "@agentswf/contract/records";
6
6
  import { type ResultSubmitResponse, WIRE_VERSION } from "@agentswf/contract/wire";
7
7
  import {
8
8
  defineWorkflow,
@@ -14,9 +14,12 @@ import { createSingleSessionHostFactory } from "@agentswf/harness";
14
14
  import type { AgentSessionAdapter } from "@agentswf/harness/adapter";
15
15
  import { createFakeAdapter } from "@agentswf/harness/testing";
16
16
  import Type from "typebox";
17
+ import type { DecisionInstallation } from "./decisions/seam";
17
18
  import { writeJson } from "./files";
18
19
  import { OPERATOR_ALIASES } from "./operator-aliases";
19
- import { createRun, openRun, type Run, readAttempts } from "./runs";
20
+ import { listOps } from "./ops";
21
+ import { claimRunLines, readRunLines } from "./run-lines";
22
+ import { createRun, openRun, type Run, readAttemptFiles } from "./runs";
20
23
  import type { RunnableWorkflow } from "./workflow-runner";
21
24
  import { type RunWorkflowOptions, runWorkflow, startWorkflow } from "./workflow-runner";
22
25
 
@@ -61,13 +64,28 @@ export async function until(check: () => Promise<boolean>, ms = 20_000): Promise
61
64
  }
62
65
  }
63
66
 
67
+ /**
68
+ * Attempt `attempt`'s writer on the run's `run.jsonl`, claimed now, and its listing of `ops/`, as
69
+ * `awf run` claims them.
70
+ */
71
+ export async function claimLines(runDir: string, attempt: number) {
72
+ const ops = await listOps(runDir);
73
+ return { lines: await claimRunLines(runDir, attempt, new Date().toISOString()), ops };
74
+ }
75
+
76
+ /** Each stage's latest line in the run's `run.jsonl`, by name, in the order first written. */
77
+ export async function lastLines(runDir: string): Promise<Map<string, StageLine>> {
78
+ const { stages } = (await readRunLines(runDir)).view;
79
+ return new Map(Object.entries(stages).map(([name, { last }]) => [name, last]));
80
+ }
81
+
64
82
  /** Writes the run's next attempt record, as `awf run` claims one, with `fields` over defaults. */
65
83
  export async function writeAttempt(run: Run, fields: Partial<AttemptRecord>): Promise<void> {
66
- const n = (await readAttempts(run)).length + 1;
84
+ const n = (await readAttemptFiles(run)).length + 1;
67
85
  mkdirSync(join(run.dir, "attempts"), { recursive: true });
68
86
  await writeJson(join(run.dir, "attempts", `${n}.json`), {
69
- version: 1,
70
87
  attempt: n,
88
+ kind: "attempt",
71
89
  file: "/flows/case.ts",
72
90
  flags: {},
73
91
  started: new Date().toISOString(),
@@ -164,7 +182,8 @@ export function answering(): AgentSessionAdapter {
164
182
 
165
183
  /**
166
184
  * One attempt of run r1 of `workflow` under `runRoot`, created by the first, its agents answered by
167
- * `answering()`. Its folder is `{runRoot}/{workflow}/r1`.
185
+ * `answering()`. Its folder is `{runRoot}/{workflow}/r1`. It claims `run.jsonl` as a claim does, and
186
+ * writes its attempt's file once it ends, as a later attempt reads its version there.
168
187
  */
169
188
  export async function runAttempt<Result extends JsonValue>(
170
189
  workflow: RunnableWorkflow<null, Result>,
@@ -174,10 +193,12 @@ export async function runAttempt<Result extends JsonValue>(
174
193
  fromStage?: string;
175
194
  values?: Record<string, JsonValue>;
176
195
  adapter?: AgentSessionAdapter;
196
+ decisions?: DecisionInstallation;
177
197
  signal?: AbortSignal;
178
198
  },
179
199
  ) {
180
200
  const { runRoot, attempt = 1, fromStage, values, adapter = answering(), signal } = options;
201
+ const { decisions } = options;
181
202
  const name = workflow.meta.name;
182
203
  const run = existsSync(join(runRoot, name, "r1"))
183
204
  ? await openRun(runRoot, name, "r1")
@@ -188,19 +209,46 @@ export async function runAttempt<Result extends JsonValue>(
188
209
  cwd: process.cwd(),
189
210
  sandbox: null,
190
211
  });
191
- return runWorkflow(workflow, null, {
192
- runRoot,
193
- run: {
194
- dir: run.dir,
195
- id: run.record.id,
212
+ const started = new Date().toISOString();
213
+ const ops = await listOps(run.dir);
214
+ const lines = await claimRunLines(run.dir, attempt, started);
215
+ const ended = (outcome: AttemptRecord["outcome"]) =>
216
+ writeJson(join(run.dir, "attempts", `${attempt}.json`), {
196
217
  attempt,
197
- ...(fromStage === undefined ? {} : { fromStage }),
198
- ...(values === undefined ? {} : { values: new Map(Object.entries(values)) }),
199
- },
200
- runtime: { aliases: OPERATOR_ALIASES, host: createSingleSessionHostFactory(adapter) },
201
- deadline: future(),
202
- ...(signal === undefined ? {} : { signal }),
203
- });
218
+ kind: "attempt",
219
+ file: name,
220
+ ...(workflow.meta.version === undefined ? {} : { workflowVersion: workflow.meta.version }),
221
+ flags: fromStage === undefined ? {} : { fromStage },
222
+ pid: process.pid,
223
+ processStart: started,
224
+ started,
225
+ ended: new Date().toISOString(),
226
+ outcome,
227
+ } satisfies AttemptRecord);
228
+ mkdirSync(join(run.dir, "attempts"), { recursive: true });
229
+ try {
230
+ const result = await runWorkflow(workflow, null, {
231
+ runRoot,
232
+ run: {
233
+ dir: run.dir,
234
+ id: run.record.id,
235
+
236
+ lines,
237
+ ops,
238
+ ...(fromStage === undefined ? {} : { fromStage }),
239
+ ...(values === undefined ? {} : { values: new Map(Object.entries(values)) }),
240
+ },
241
+ runtime: { aliases: OPERATOR_ALIASES, host: createSingleSessionHostFactory(adapter) },
242
+ deadline: future(),
243
+ ...(decisions === undefined ? {} : { decisions }),
244
+ ...(signal === undefined ? {} : { signal }),
245
+ });
246
+ await ended("completed");
247
+ return result;
248
+ } catch (error) {
249
+ await ended("failed");
250
+ throw error;
251
+ }
204
252
  }
205
253
 
206
254
  /** `startWorkflow` as the first attempt of a new run of the workflow under `options.runRoot`. */
@@ -217,7 +265,7 @@ export async function startNew<Args extends JsonValue, Result extends JsonValue>
217
265
  });
218
266
  return startWorkflow(definition, args, {
219
267
  ...options,
220
- run: { dir: run.dir, id: run.record.id, attempt: 1 },
268
+ run: { dir: run.dir, id: run.record.id, ...(await claimLines(run.dir, 1)) },
221
269
  });
222
270
  }
223
271
 
@@ -1,14 +1,22 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { basename, dirname, join, resolve } from "node:path";
1
3
  import type { TickResultJson } from "@agentswf/contract/records";
4
+ import { type Out, parsePlace } from "./command-line";
2
5
  import { messageOf } from "./errors";
3
6
  import { exists } from "./files";
7
+ import { ENGINE_VARIABLE, type InstalledEngine, type ProjectEngine } from "./hand-over";
4
8
  import help from "./help/tick.txt" with { type: "text" };
9
+ import { ENGINE_VERSION, EngineRefused, engineOf } from "./project-engine";
5
10
  import { listProjects, projectRunRoot } from "./projects";
6
- import type { Out } from "./run-commands";
7
- import { parsePlace } from "./run-commands";
11
+ import { asksJson, printRefusal } from "./refusals";
8
12
  import { runLockFile, withRunLock } from "./run-lock";
9
- import { listRuns } from "./run-state";
13
+ import { listRunEntries, listRuns } from "./run-state";
14
+ import { RunUnreadable, readAttempts } from "./runs";
10
15
  import { type Launch, wake } from "./wake";
11
16
 
17
+ /** `awf tick --json`'s `"v"`, which its refusals carry too. */
18
+ export const TICK_V = 1 satisfies TickResultJson["v"];
19
+
12
20
  export const tickUsage = help.trimEnd();
13
21
 
14
22
  export async function tickCommand(
@@ -22,10 +30,6 @@ export async function tickCommand(
22
30
  now?: () => number;
23
31
  },
24
32
  ): Promise<number> {
25
- if (argv.includes("-h") || argv.includes("--help")) {
26
- out.stdout(tickUsage);
27
- return 0;
28
- }
29
33
  let parsed: ReturnType<typeof parsePlace>;
30
34
  try {
31
35
  parsed = parsePlace(argv, shellCwd);
@@ -34,24 +38,72 @@ export async function tickCommand(
34
38
  if (parsed.rest.includes("--all") && parsed.place.repeat.length > 0)
35
39
  throw new Error("--all wakes every project's runs; it takes no --cwd or --run-root");
36
40
  } catch (error) {
37
- out.stderr(`awf: ${messageOf(error)}\n\n${tickUsage}`);
41
+ printRefusal(out, { json: asksJson(argv), v: TICK_V }, error, "usage", tickUsage);
38
42
  return 2;
39
43
  }
40
44
  const { place, json } = parsed;
41
45
  const woke: TickResultJson["woke"] = [];
46
+ const unreadable: string[] = [];
42
47
  let failed = false;
43
48
  const roots: string[] = [];
44
- if (!parsed.rest.includes("--all")) roots.push(place.runRoot);
45
- else
49
+ /** Each project's runs on its own engine (ADR 0015): this one, or its own in a child. */
50
+ const onItsEngine = async (project: string, root: string) => {
51
+ let engine: ProjectEngine;
52
+ try {
53
+ engine = engineOf(project, await runFiles(root));
54
+ } catch (error) {
55
+ if (!(error instanceof EngineRefused)) throw error;
56
+ failed = true;
57
+ out.stderr(`awf: ${error.message}; its runs are not woken`);
58
+ return;
59
+ }
60
+ if (engine.kind === "declared") {
61
+ failed = true;
62
+ out.stderr(
63
+ `awf: ${project} declares agentswf in ${engine.manifest} and has none installed, so its runs are not woken: run bun install there`,
64
+ );
65
+ } else if (engine.kind === "installed" && engine.version !== ENGINE_VERSION) {
66
+ // This is the engine the project's agentswf runs, and they disagree: another child would find
67
+ // the same, without end.
68
+ if (isOwnEngine(engine)) {
69
+ failed = true;
70
+ out.stderr(
71
+ `awf: ${project}'s agentswf is ${engine.version} and its engine ${ENGINE_VERSION}, so its runs are not woken: run bun install there`,
72
+ );
73
+ return;
74
+ }
75
+ const ticked = await tickElsewhere(project, root, engine, context.environment, out);
76
+ woke.push(...ticked.woke);
77
+ if (!ticked.ok) failed = true;
78
+ } else roots.push(root);
79
+ };
80
+ if (!parsed.rest.includes("--all")) {
81
+ // A project's own run root is its engine's to tick; any other, the operator's, this one's.
82
+ const root = resolve(place.runRoot);
83
+ if (basename(root) === "runs" && basename(dirname(root)) === ".awf")
84
+ await onItsEngine(dirname(dirname(root)), root);
85
+ else roots.push(root);
86
+ } else
46
87
  for (const project of await listProjects(context.home)) {
47
- if (await exists(project)) roots.push(projectRunRoot(project));
48
- else {
88
+ if (!(await exists(project))) {
49
89
  failed = true;
50
90
  out.stderr(`awf: ${project}, in ~/.awf/projects, is gone; its runs are not woken`);
91
+ continue;
51
92
  }
93
+ // A project a plain run registered (ADR 0016) has nothing for a tick to wake; one on another
94
+ // engine is that engine's to judge, whose records this one may not read.
95
+ const root = projectRunRoot(project);
96
+ if (!(await listRuns(root)).some((run) => run.record.events) && onThisEngine(project))
97
+ continue;
98
+ await onItsEngine(project, root);
52
99
  }
53
100
  for (const root of roots) {
54
- for (const run of await listRuns(root)) {
101
+ for (const run of await listRunEntries(root)) {
102
+ if (run instanceof RunUnreadable) {
103
+ // One with events is one a tick would have woken: it is named, not an error.
104
+ if (run.events) unreadable.push(run.ref);
105
+ continue;
106
+ }
55
107
  if (!run.record.events) continue;
56
108
  const ref = `${run.record.workflow}/${run.record.id}`;
57
109
  try {
@@ -70,7 +122,85 @@ export async function tickCommand(
70
122
  }
71
123
  }
72
124
  }
73
- if (json) out.stdout(JSON.stringify({ v: 1, woke } satisfies TickResultJson));
74
- else for (const each of woke) out.stdout(`${each.did} ${each.ref}: ${each.why}`);
125
+ if (unreadable.length > 0 && !json) {
126
+ const runs = unreadable.length === 1 ? "1 run" : `${unreadable.length} runs`;
127
+ out.stderr(
128
+ `awf: ${runs} with events can't be read and ${unreadable.length === 1 ? "is" : "are"} not woken; awf runs says why`,
129
+ );
130
+ }
131
+ if (json) {
132
+ const result: TickResultJson = {
133
+ v: TICK_V,
134
+ woke,
135
+ ...(unreadable.length ? { unreadable } : {}),
136
+ };
137
+ out.stdout(JSON.stringify(result));
138
+ } else for (const each of woke) out.stdout(`${each.did} ${each.ref}: ${each.why}`);
75
139
  return failed ? 1 : 0;
76
140
  }
141
+
142
+ /** The files the event runs under `root` last ran, by which a project with no engine finds one. */
143
+ async function runFiles(root: string): Promise<string[]> {
144
+ const files: string[] = [];
145
+ for (const run of await listRuns(root)) {
146
+ if (!run.record.events) continue;
147
+ const file = (await readAttempts(run).catch(() => [])).at(-1)?.file ?? run.record.file;
148
+ if (file !== undefined) files.push(file);
149
+ }
150
+ return files;
151
+ }
152
+
153
+ /**
154
+ * `awf tick` of one project's runs on its own engine, as the hand-over runs it: in the project,
155
+ * with `AWF_ENGINE` naming it, so it never hands over by its directory.
156
+ */
157
+ async function tickElsewhere(
158
+ project: string,
159
+ root: string,
160
+ engine: InstalledEngine,
161
+ environment: Readonly<Record<string, string | undefined>>,
162
+ out: Out,
163
+ ): Promise<{ ok: boolean; woke: TickResultJson["woke"] }> {
164
+ const child = Bun.spawn({
165
+ cmd: [process.execPath, "--no-env-file", engine.bin, "tick", "--run-root", root, "--json"],
166
+ cwd: project,
167
+ env: { ...environment, [ENGINE_VARIABLE]: engine.dir },
168
+ stdio: ["ignore", "pipe", "pipe"],
169
+ });
170
+ const [stdout, stderr, code] = await Promise.all([
171
+ new Response(child.stdout).text(),
172
+ new Response(child.stderr).text(),
173
+ child.exited,
174
+ ]);
175
+ for (const line of stderr.trimEnd().split("\n")) if (line) out.stderr(line);
176
+ try {
177
+ const result = JSON.parse(stdout) as Partial<TickResultJson>;
178
+ if (!Array.isArray(result.woke)) throw new Error("no woke");
179
+ return { ok: code === 0, woke: result.woke };
180
+ } catch {
181
+ out.stderr(
182
+ `awf: ${project}'s agentswf ${engine.version} did not say what it woke (exit ${code})`,
183
+ );
184
+ return { ok: false, woke: [] };
185
+ }
186
+ }
187
+
188
+ /** Whether `engine`'s agentswf runs on this very engine package. */
189
+ function isOwnEngine(engine: InstalledEngine): boolean {
190
+ try {
191
+ const theirs = Bun.resolveSync("@agentswf/engine/package.json", engine.dir);
192
+ return realpathSync(theirs) === realpathSync(join(import.meta.dir, "..", "package.json"));
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ /** Whether a project's runs would run on this engine, or none is installed there to run them. */
199
+ function onThisEngine(project: string): boolean {
200
+ try {
201
+ const engine = engineOf(project);
202
+ return engine.kind !== "installed" || engine.version === ENGINE_VERSION;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
package/src/tools.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  } from "./simple-input";
26
26
 
27
27
  /** How long a handler that ignores its aborted signal is waited for before its call is settled. */
28
- export const TOOL_SETTLE_MS = 5_000;
28
+ const TOOL_SETTLE_MS = 5_000;
29
29
 
30
30
  /**
31
31
  * The agents whose handlers the current code runs in, innermost last: one of them can't be given
@@ -59,8 +59,6 @@ export type ToolHost<Scope> = {
59
59
  /** Runs `fn` in the operation's scope, or in none. */
60
60
  runIn<T>(scope: Scope | undefined, fn: () => T): T;
61
61
  now?(): number;
62
- /** A tool given its deprecated `timeoutMs`. */
63
- deprecated?(name: "timeout-ms", call: string): void;
64
62
  };
65
63
 
66
64
  /** An operation as tool calls bind to it. */
@@ -125,7 +123,7 @@ export class ToolCalls<Scope> {
125
123
 
126
124
  /** An agent opened or forked with these tools; checked against the limits first. */
127
125
  give(agent: AgentKey, tools: AgentTools): void {
128
- this.#held.set(agent, held(agent, tools, this.#host.deprecated));
126
+ this.#held.set(agent, held(agent, tools));
129
127
  }
130
128
 
131
129
  /**
@@ -134,7 +132,7 @@ export class ToolCalls<Scope> {
134
132
  */
135
133
  reopen(agent: AgentKey, tools: AgentTools | undefined): void {
136
134
  if (tools === undefined) return;
137
- const next = held(agent, tools, this.#host.deprecated);
135
+ const next = held(agent, tools);
138
136
  const existing = this.#held.get(agent);
139
137
  if (!existing || !sameTools(existing, next)) {
140
138
  throw new Error(`agent ${agent} is already open with different tools`);
@@ -573,11 +571,7 @@ export function toolsLesson(
573
571
  ].join("\n");
574
572
  }
575
573
 
576
- function held(
577
- agent: AgentKey,
578
- tools: AgentTools,
579
- deprecated: ToolHost<unknown>["deprecated"],
580
- ): Held {
574
+ function held(agent: AgentKey, tools: AgentTools): Held {
581
575
  if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
582
576
  throw new Error(`agent ${agent}'s tools are an object of tools by name`);
583
577
  }
@@ -600,7 +594,7 @@ function held(
600
594
  if (Buffer.byteLength(spec.description) > TOOL_LIMITS.descriptionBytes) {
601
595
  throw new Error(`${where}: its description is over ${TOOL_LIMITS.descriptionBytes} bytes`);
602
596
  }
603
- budgets.set(name, budgetOf(where, spec, deprecated));
597
+ budgets.set(name, budgetOf(where, spec));
604
598
  if (spec.input === undefined) {
605
599
  schemas.set(name, undefined);
606
600
  continue;
@@ -629,25 +623,12 @@ function held(
629
623
  return { specs: { ...tools }, schemas, budgets };
630
624
  }
631
625
 
632
- /** A tool's `timeout`, or its deprecated `timeoutMs`, checked; absent, the default. */
633
- function budgetOf(
634
- where: string,
635
- spec: ToolSpec,
636
- deprecated: ToolHost<unknown>["deprecated"],
637
- ): Budget {
638
- const { timeout, timeoutMs } = spec;
639
- if (timeout != null) {
640
- if (timeoutMs != null)
641
- throw new Error(`${where}: give timeout alone: timeoutMs is its deprecated form`);
642
- if (timeout === false) return false;
643
- return positiveDurationMilliseconds(timeout, `${where}: its timeout`);
644
- }
645
- if (timeoutMs == null) return TOOL_LIMITS.defaultTimeoutMs;
646
- if (timeoutMs !== false && !(Number.isSafeInteger(timeoutMs) && timeoutMs > 0)) {
647
- throw new Error(`${where}: timeoutMs is a positive integer, or false for none`);
648
- }
649
- deprecated?.("timeout-ms", "on a tool");
650
- return timeoutMs;
626
+ /** A tool's `timeout`, checked; absent, the default. */
627
+ function budgetOf(where: string, spec: ToolSpec): Budget {
628
+ const { timeout } = spec;
629
+ if (timeout == null) return TOOL_LIMITS.defaultTimeoutMs;
630
+ if (timeout === false) return false;
631
+ return positiveDurationMilliseconds(timeout, `${where}: its timeout`);
651
632
  }
652
633
 
653
634
  function sameTools(a: Held, b: Held): boolean {
@@ -0,0 +1,37 @@
1
+ import type { RunLine } from "@agentswf/contract/records";
2
+ import type { AgentExecution, OperationRecord } from "@agentswf/contract/workflow";
3
+ import { endIdOf } from "./event-ids";
4
+ import type { TurnStartedLine } from "./run-policy/waits";
5
+
6
+ /** The usage of a turn no delivery settled: one refused, cancelled while queued, or cut off by a crash. */
7
+ export function unrunUsage(
8
+ agent: string,
9
+ operationId: string,
10
+ execution: AgentExecution,
11
+ ): OperationRecord {
12
+ return { callPath: [], agent, operationId, execution, sessions: [] };
13
+ }
14
+
15
+ /** The failed `turn-ended` attempt `attempt`'s claim writes for `turn`, which its attempt left running. */
16
+ export function crashedEnd(turn: TurnStartedLine, attempt: number, at: string): RunLine {
17
+ return {
18
+ kind: "turn-ended",
19
+ attempt,
20
+ at,
21
+ id: endIdOf(turn.agent, turn.turn, turn.attempt),
22
+ agent: turn.agent,
23
+ turn: turn.turn,
24
+ began: turn.attempt,
25
+ end: {
26
+ turn: turn.turn,
27
+ meta: turn.meta,
28
+ outcome: {
29
+ answered: false,
30
+ kind: "failed",
31
+ reason: `attempt ${turn.attempt} ended before this turn did; its work, if any, is in the agent's session`,
32
+ retryable: true,
33
+ usage: unrunUsage(turn.agent, turn.turn, turn.execution),
34
+ },
35
+ },
36
+ };
37
+ }