@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.
- package/package.json +4 -4
- package/src/agent-cli/cli.ts +57 -17
- package/src/attempt-ending.ts +2 -4
- package/src/attempt-output.ts +2 -1
- package/src/attempt-view.ts +1 -1
- package/src/control-plane.ts +37 -5
- package/src/coordinator-socket.ts +15 -6
- package/src/deadlines.ts +69 -2
- package/src/decisions/directory.ts +9 -11
- package/src/deprecations.ts +177 -0
- package/src/duration.ts +50 -20
- package/src/emit-command.ts +55 -41
- package/src/emit.ts +124 -15
- package/src/help/emit.txt +12 -4
- package/src/help/inbox.txt +2 -2
- package/src/help/logs.txt +5 -4
- package/src/help/reply.txt +5 -5
- package/src/help/run.txt +21 -7
- package/src/herdr-run-session.ts +2 -2
- package/src/holder-command.ts +36 -31
- package/src/inbox-command.ts +7 -10
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +2 -2
- package/src/keep-alive.ts +28 -13
- package/src/logs-command.ts +12 -12
- package/src/mailbox.ts +2 -20
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +26 -21
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +7 -5
- package/src/operator-cli.ts +23 -9
- package/src/operator-runtime.ts +5 -5
- package/src/pane-layout.ts +125 -32
- package/src/progress-view.ts +8 -8
- package/src/run-command.ts +25 -4
- package/src/run-commands.ts +1 -1
- package/src/run-prepare.ts +46 -8
- package/src/run-usage.ts +1 -1
- package/src/run-view.ts +9 -4
- package/src/runs-command.ts +12 -10
- package/src/runs.ts +44 -8
- package/src/sources.ts +42 -6
- package/src/stage-ledger.ts +42 -12
- package/src/stage-plan.ts +37 -1
- package/src/test-command.ts +7 -3
- package/src/testing.ts +7 -8
- package/src/tools.ts +46 -18
- package/src/workflow-loader.ts +5 -2
- package/src/workflow-runner.ts +553 -235
- package/src/workflow-testing/coordinator.typecheck.ts +7 -7
- package/src/workflow-testing/coordinators.ts +2 -2
- package/src/workflow-testing/host.ts +107 -38
- package/src/workflow-testing/index.ts +45 -836
- package/src/workflow-testing/run-test.ts +1213 -0
- package/src/workflow-testing/script.ts +102 -41
- 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
|
-
|
|
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
|
|
302
|
+
const budget = tools.budgets.get(request.tool)!;
|
|
293
303
|
const deadline = operation.deadline();
|
|
294
304
|
const startedAt = this.#now();
|
|
295
|
-
const budgetEnd = minimum(
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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 {
|
package/src/workflow-loader.ts
CHANGED
|
@@ -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/
|
|
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 (
|
|
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
|
);
|