@agentswf/engine 0.1.0-beta.4 → 0.1.0-beta.5

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 (56) hide show
  1. package/package.json +4 -4
  2. package/src/agent-cli/cli.ts +57 -17
  3. package/src/attempt-ending.ts +2 -4
  4. package/src/attempt-output.ts +2 -1
  5. package/src/attempt-view.ts +1 -1
  6. package/src/control-plane.ts +37 -5
  7. package/src/coordinator-socket.ts +15 -6
  8. package/src/deadlines.ts +69 -2
  9. package/src/decisions/directory.ts +9 -11
  10. package/src/deprecations.ts +177 -0
  11. package/src/duration.ts +50 -20
  12. package/src/emit-command.ts +55 -41
  13. package/src/emit.ts +124 -15
  14. package/src/help/emit.txt +12 -4
  15. package/src/help/inbox.txt +2 -2
  16. package/src/help/logs.txt +5 -4
  17. package/src/help/reply.txt +5 -5
  18. package/src/help/run.txt +21 -7
  19. package/src/herdr-run-session.ts +2 -2
  20. package/src/holder-command.ts +36 -31
  21. package/src/inbox-command.ts +7 -10
  22. package/src/init-template/hello/workflow.test.ts.template +3 -3
  23. package/src/init-template/hello/workflow.ts.template +2 -2
  24. package/src/keep-alive.ts +28 -13
  25. package/src/logs-command.ts +12 -12
  26. package/src/mailbox.ts +2 -20
  27. package/src/messaging/limits.ts +4 -4
  28. package/src/messaging/messaging.ts +26 -21
  29. package/src/messaging/routes.ts +1 -1
  30. package/src/messaging/text.ts +7 -5
  31. package/src/operator-cli.ts +23 -9
  32. package/src/operator-runtime.ts +5 -5
  33. package/src/pane-layout.ts +125 -32
  34. package/src/progress-view.ts +8 -8
  35. package/src/run-command.ts +25 -4
  36. package/src/run-commands.ts +1 -1
  37. package/src/run-prepare.ts +46 -8
  38. package/src/run-usage.ts +1 -1
  39. package/src/run-view.ts +9 -4
  40. package/src/runs-command.ts +12 -10
  41. package/src/runs.ts +44 -8
  42. package/src/sources.ts +42 -6
  43. package/src/stage-ledger.ts +42 -12
  44. package/src/stage-plan.ts +37 -1
  45. package/src/test-command.ts +7 -3
  46. package/src/testing.ts +7 -8
  47. package/src/tools.ts +46 -18
  48. package/src/workflow-loader.ts +5 -2
  49. package/src/workflow-runner.ts +553 -235
  50. package/src/workflow-testing/coordinator.typecheck.ts +7 -7
  51. package/src/workflow-testing/coordinators.ts +2 -2
  52. package/src/workflow-testing/host.ts +107 -38
  53. package/src/workflow-testing/index.ts +45 -836
  54. package/src/workflow-testing/run-test.ts +1213 -0
  55. package/src/workflow-testing/script.ts +102 -41
  56. package/src/workflow-testing/workflow-testing.typecheck.ts +11 -8
package/src/tools.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  type AgentTools,
8
8
  isJsonValue,
9
9
  type JsonValue,
10
+ positiveDurationMilliseconds,
10
11
  TOOL_LIMITS,
11
12
  type ToolSpec,
12
13
  } from "@agentswf/contract/workflow";
@@ -58,6 +59,8 @@ export type ToolHost<Scope> = {
58
59
  /** Runs `fn` in the operation's scope, or in none. */
59
60
  runIn<T>(scope: Scope | undefined, fn: () => T): T;
60
61
  now?(): number;
62
+ /** A tool given its deprecated `timeoutMs`. */
63
+ deprecated?(name: "timeout-ms", call: string): void;
61
64
  };
62
65
 
63
66
  /** An operation as tool calls bind to it. */
@@ -76,7 +79,14 @@ export type ToolAnswer =
76
79
  | { kind: "tool-failed"; outcome: ToolFailure; error: string }
77
80
  | { kind: "refused"; code: ToolCode | "internal-error"; error: string };
78
81
 
79
- type Held = { specs: AgentTools; schemas: Map<string, JsonSchema | undefined> };
82
+ /** A call's budget: milliseconds from its start, or none of its own. */
83
+ type Budget = number | false;
84
+
85
+ type Held = {
86
+ specs: AgentTools;
87
+ schemas: Map<string, JsonSchema | undefined>;
88
+ budgets: Map<string, Budget>;
89
+ };
80
90
 
81
91
  type Running = {
82
92
  tool: string;
@@ -115,7 +125,7 @@ export class ToolCalls<Scope> {
115
125
 
116
126
  /** An agent opened or forked with these tools; checked against the limits first. */
117
127
  give(agent: AgentKey, tools: AgentTools): void {
118
- this.#held.set(agent, held(agent, tools));
128
+ this.#held.set(agent, held(agent, tools, this.#host.deprecated));
119
129
  }
120
130
 
121
131
  /**
@@ -124,7 +134,7 @@ export class ToolCalls<Scope> {
124
134
  */
125
135
  reopen(agent: AgentKey, tools: AgentTools | undefined): void {
126
136
  if (tools === undefined) return;
127
- const next = held(agent, tools);
137
+ const next = held(agent, tools, this.#host.deprecated);
128
138
  const existing = this.#held.get(agent);
129
139
  if (!existing || !sameTools(existing, next)) {
130
140
  throw new Error(`agent ${agent} is already open with different tools`);
@@ -289,12 +299,11 @@ export class ToolCalls<Scope> {
289
299
  const controller = new AbortController();
290
300
  let cause: ToolFailure | undefined;
291
301
  let handlerEnded = false;
292
- const timeoutMs = spec.timeoutMs === undefined ? TOOL_LIMITS.defaultTimeoutMs : spec.timeoutMs;
302
+ const budget = tools.budgets.get(request.tool)!;
293
303
  const deadline = operation.deadline();
294
304
  const startedAt = this.#now();
295
- const budgetEnd = minimum(timeoutMs === false ? undefined : startedAt + timeoutMs, deadline);
296
- const bound =
297
- deadline !== undefined && budgetEnd === deadline ? ("deadline" as const) : timeoutMs;
305
+ const budgetEnd = minimum(budget === false ? undefined : startedAt + budget, deadline);
306
+ const bound = deadline !== undefined && budgetEnd === deadline ? ("deadline" as const) : budget;
298
307
  const { promise: settled, resolve: settle } = Promise.withResolvers<void>();
299
308
  const { promise: answered, resolve: answer } = Promise.withResolvers<ToolAnswer>();
300
309
  const running: Running = {
@@ -479,7 +488,7 @@ function aborted(
479
488
  tool: string,
480
489
  cause: ToolFailure,
481
490
  /** What bounded it: its own budget, or the turn's deadline. */
482
- bound: number | false | "deadline",
491
+ bound: Budget | "deadline",
483
492
  ): { kind: "tool-failed"; outcome: ToolFailure; error: string } {
484
493
  const what =
485
494
  cause === "cancelled"
@@ -564,7 +573,11 @@ export function toolsLesson(
564
573
  ].join("\n");
565
574
  }
566
575
 
567
- function held(agent: AgentKey, tools: AgentTools): Held {
576
+ function held(
577
+ agent: AgentKey,
578
+ tools: AgentTools,
579
+ deprecated: ToolHost<unknown>["deprecated"],
580
+ ): Held {
568
581
  if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
569
582
  throw new Error(`agent ${agent}'s tools are an object of tools by name`);
570
583
  }
@@ -573,6 +586,7 @@ function held(agent: AgentKey, tools: AgentTools): Held {
573
586
  throw new Error(`agent ${agent} has ${names.length} tools; at most ${TOOL_LIMITS.tools}`);
574
587
  }
575
588
  const schemas = new Map<string, JsonSchema | undefined>();
589
+ const budgets = new Map<string, Budget>();
576
590
  for (const name of names) {
577
591
  const spec = tools[name]!;
578
592
  const where = `agent ${agent}'s tool ${JSON.stringify(name)}`;
@@ -586,14 +600,7 @@ function held(agent: AgentKey, tools: AgentTools): Held {
586
600
  if (Buffer.byteLength(spec.description) > TOOL_LIMITS.descriptionBytes) {
587
601
  throw new Error(`${where}: its description is over ${TOOL_LIMITS.descriptionBytes} bytes`);
588
602
  }
589
- const timeout = spec.timeoutMs;
590
- if (
591
- timeout !== undefined &&
592
- timeout !== false &&
593
- !(Number.isSafeInteger(timeout) && timeout > 0)
594
- ) {
595
- throw new Error(`${where}: timeoutMs is a positive integer, or false for none`);
596
- }
603
+ budgets.set(name, budgetOf(where, spec, deprecated));
597
604
  if (spec.input === undefined) {
598
605
  schemas.set(name, undefined);
599
606
  continue;
@@ -619,7 +626,28 @@ function held(agent: AgentKey, tools: AgentTools): Held {
619
626
  // Its JSON, in the author's order for the lesson, without TypeBox's own keys.
620
627
  schemas.set(name, JSON.parse(JSON.stringify(input)) as JsonSchema);
621
628
  }
622
- return { specs: { ...tools }, schemas };
629
+ return { specs: { ...tools }, schemas, budgets };
630
+ }
631
+
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;
623
651
  }
624
652
 
625
653
  function sameTools(a: Held, b: Held): boolean {
@@ -1,7 +1,7 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import { extname, resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
- import * as workflowSurface from "@agentswf/contract/workflow";
4
+ import * as workflowSurface from "@agentswf/contract/author";
5
5
  import {
6
6
  type EventSchemas,
7
7
  eventWorkflowProblem,
@@ -85,7 +85,10 @@ export async function loadWorkflowFile(file: string, cwd: string): Promise<Loade
85
85
  // As `tsc` would have checked them, for a file it never saw.
86
86
  const eventProblem = eventWorkflowProblem(given);
87
87
  if (eventProblem) throw new Error(eventProblem);
88
- if (realPath(given.file as string) !== realPath(absolute))
88
+ if (given.file === undefined)
89
+ // Found by the path it was loaded from, which is all a sender with that path needs.
90
+ return { file: absolute, workflow: { ...given, file: absolute } as AnyWorkflow };
91
+ if (realPath(given.file) !== realPath(absolute))
89
92
  throw new Error(
90
93
  `file is ${JSON.stringify(given.file)}; a workflow with events gives file: import.meta.path, ${absolute}`,
91
94
  );