@agentswf/engine 0.1.0-beta.3 → 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 +5 -6
- package/src/agent-cli/cli.ts +611 -0
- package/src/agent-cli/client.ts +115 -0
- package/src/agent-launcher.ts +15 -25
- package/src/allowance-command.ts +3 -15
- package/src/attempt-ending.ts +2 -4
- package/src/attempt-output.ts +3 -2
- package/src/attempt-view.ts +1 -1
- package/src/control-plane.ts +38 -6
- 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 +57 -54
- package/src/emit.ts +124 -15
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +21 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +14 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +58 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/herdr-run-session.ts +2 -2
- package/src/here.ts +1 -1
- package/src/holder-command.ts +38 -51
- package/src/inbox-command.ts +9 -24
- package/src/init-command.ts +2 -11
- package/src/init-template/AGENTS.md.template +3 -3
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +6 -6
- package/src/keep-alive.ts +28 -13
- package/src/logs-command.ts +14 -27
- package/src/mailbox.ts +16 -21
- package/src/messaging/limits.ts +5 -5
- package/src/messaging/messaging.ts +48 -43
- package/src/messaging/routes.ts +2 -2
- package/src/messaging/text.ts +24 -22
- package/src/operation-liveness.ts +1 -1
- package/src/operator-cli.ts +23 -9
- package/src/operator-runtime.ts +9 -12
- package/src/pane-layout.ts +125 -32
- package/src/progress-view.ts +8 -8
- package/src/result-validation.ts +1 -1
- package/src/run-command.ts +27 -47
- package/src/run-commands.ts +1 -1
- package/src/run-prepare.ts +46 -8
- package/src/run-usage.ts +2 -2
- package/src/run-view.ts +9 -4
- package/src/runs-command.ts +14 -23
- package/src/runs.ts +44 -8
- package/src/sandboxes.ts +3 -2
- package/src/simple-input.ts +13 -3
- package/src/sources.ts +42 -6
- package/src/stage-ledger.ts +43 -13
- package/src/stage-plan.ts +37 -1
- package/src/test-command.ts +9 -14
- package/src/testing.ts +8 -9
- package/src/tick-command.ts +2 -13
- package/src/tools.ts +62 -25
- package/src/workflow-loader.ts +5 -2
- package/src/workflow-runner.ts +636 -271
- package/src/workflow-testing/coordinator.typecheck.ts +12 -13
- package/src/workflow-testing/coordinators.ts +2 -2
- package/src/workflow-testing/host.ts +115 -46
- package/src/workflow-testing/index.ts +45 -837
- package/src/workflow-testing/run-test.ts +1213 -0
- package/src/workflow-testing/script.ts +106 -45
- package/src/workflow-testing/workflow-testing.typecheck.ts +13 -10
package/src/stage-plan.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AttemptRecord,
|
|
3
|
+
AttemptStage,
|
|
4
|
+
StageNeed,
|
|
5
|
+
StageRecord,
|
|
6
|
+
} from "@agentswf/contract/records";
|
|
2
7
|
import { type JsonSchema, type SchemaError, validate } from "@agentswf/contract/schema";
|
|
3
8
|
import type { JsonValue } from "@agentswf/contract/workflow";
|
|
4
9
|
|
|
@@ -148,3 +153,34 @@ export function planStageByContent(
|
|
|
148
153
|
? { kind: "reuse", record, value: record.value }
|
|
149
154
|
: { kind: "stop", reason: misfit };
|
|
150
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A `--from-stage {stage}` that attempt `attempt` redid, in a run of a workflow with `events`. In
|
|
159
|
+
* that attempt and every later one, a stage first entered after `stage` whose record an earlier
|
|
160
|
+
* attempt than `attempt` wrote runs again, its record replaced: it was made against the stage
|
|
161
|
+
* before the redo. A record never entered after the stage keeps its reuse.
|
|
162
|
+
*/
|
|
163
|
+
export type StageRedo = { stage: string; attempt: number };
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The last `--from-stage` an earlier attempt of a run with `events` redid: the latest attempt with
|
|
167
|
+
* one that ran its stage. Its attempt record says so in `stages`, which a stop for a stage never
|
|
168
|
+
* reached, a refusal and a crash leave without it; a crash after the stage's new record was written
|
|
169
|
+
* leaves that record, the attempt's own. A redo whose stage failed and ran again on a wake is still
|
|
170
|
+
* the redo. Only attempt and stage records are read, which a continue may read.
|
|
171
|
+
*/
|
|
172
|
+
export function lastRedo(
|
|
173
|
+
attempts: readonly (Pick<AttemptRecord, "attempt" | "flags"> & {
|
|
174
|
+
stages?: readonly Pick<AttemptStage, "stage" | "source">[];
|
|
175
|
+
})[],
|
|
176
|
+
records: ReadonlyMap<string, StageRecord>,
|
|
177
|
+
before: number,
|
|
178
|
+
): StageRedo | undefined {
|
|
179
|
+
for (const { attempt, flags, stages } of [...attempts].sort((a, b) => b.attempt - a.attempt)) {
|
|
180
|
+
const stage = flags.fromStage;
|
|
181
|
+
if (attempt >= before || stage === undefined) continue;
|
|
182
|
+
const ran = stages?.some((each) => each.stage === stage && each.source === "ran") ?? false;
|
|
183
|
+
if (ran || records.get(stage)?.attempt === attempt) return { stage, attempt };
|
|
184
|
+
}
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
package/src/test-command.ts
CHANGED
|
@@ -4,21 +4,12 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { parseDuration } from "./duration";
|
|
6
6
|
import { messageOf } from "./errors";
|
|
7
|
+
import help from "./help/test.txt" with { type: "text" };
|
|
7
8
|
import type { Out } from "./run-commands";
|
|
8
9
|
|
|
9
10
|
const PRELOAD = fileURLToPath(new URL("./workflow-test-preload.ts", import.meta.url));
|
|
10
11
|
|
|
11
|
-
export const testUsage =
|
|
12
|
-
"usage: awf test [paths...] [-t <pattern>] [--watch] [--timeout <duration>]",
|
|
13
|
-
"",
|
|
14
|
-
"Runs a workflow's tests (*.test.ts and *.spec.ts files) under the current directory, or under",
|
|
15
|
-
"each file or directory given, with agentswf/workflow, agentswf/testing and typebox served as",
|
|
16
|
-
"awf run serves them: nothing is installed. -t runs the tests whose names match the pattern;",
|
|
17
|
-
"--watch reruns them on a change; --timeout bounds each test, 5s by default.",
|
|
18
|
-
"",
|
|
19
|
-
"Exits 0 when every test passed, 1 when one failed or none was found, 2 on a usage error. What",
|
|
20
|
-
"it prints is for reading, not parsing.",
|
|
21
|
-
].join("\n");
|
|
12
|
+
export const testUsage = help.trimEnd();
|
|
22
13
|
|
|
23
14
|
export type TestCommand = { paths: string[]; bunArgs: string[] };
|
|
24
15
|
|
|
@@ -33,7 +24,7 @@ export async function testCommand(
|
|
|
33
24
|
): Promise<number> {
|
|
34
25
|
let tests: TestCommand | "help";
|
|
35
26
|
try {
|
|
36
|
-
tests = parseTestCommand(argv, cwd);
|
|
27
|
+
tests = parseTestCommand(argv, cwd, (line) => io.stderr(line));
|
|
37
28
|
} catch (error) {
|
|
38
29
|
io.stderr(`awf: ${messageOf(error)}\n\n${testUsage}`);
|
|
39
30
|
return 2;
|
|
@@ -52,7 +43,11 @@ export async function testCommand(
|
|
|
52
43
|
* Only these flags pass through, so the published command is awf's, not the whole of `bun test`'s,
|
|
53
44
|
* and a later runner can take its place. A path must exist; Bun would take it as a filter.
|
|
54
45
|
*/
|
|
55
|
-
export function parseTestCommand(
|
|
46
|
+
export function parseTestCommand(
|
|
47
|
+
argv: readonly string[],
|
|
48
|
+
cwd: string,
|
|
49
|
+
deprecated: (line: string) => void = () => {},
|
|
50
|
+
): TestCommand | "help" {
|
|
56
51
|
const paths: string[] = [];
|
|
57
52
|
const bunArgs: string[] = [];
|
|
58
53
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -67,7 +62,7 @@ export function parseTestCommand(argv: readonly string[], cwd: string): TestComm
|
|
|
67
62
|
index += 1;
|
|
68
63
|
} else if (option === "--timeout") {
|
|
69
64
|
if (value === undefined) throw new Error("--timeout needs a duration such as 10s");
|
|
70
|
-
bunArgs.push("--timeout", String(parseDuration(value)));
|
|
65
|
+
bunArgs.push("--timeout", String(parseDuration(value, "--timeout", deprecated)));
|
|
71
66
|
index += 1;
|
|
72
67
|
} else if (option.startsWith("-")) {
|
|
73
68
|
throw new Error(`unknown option: ${option}`);
|
package/src/testing.ts
CHANGED
|
@@ -6,11 +6,9 @@ import type { AttemptRecord } from "@agentswf/contract/records";
|
|
|
6
6
|
import { type ResultSubmitResponse, WIRE_VERSION } from "@agentswf/contract/wire";
|
|
7
7
|
import {
|
|
8
8
|
defineWorkflow,
|
|
9
|
-
type EventWorkflowContext,
|
|
10
9
|
type JsonValue,
|
|
11
10
|
type OutputSchema,
|
|
12
11
|
type WorkflowContext,
|
|
13
|
-
type WorkflowDefinition,
|
|
14
12
|
} from "@agentswf/contract/workflow";
|
|
15
13
|
import { createSingleSessionHostFactory } from "@agentswf/harness";
|
|
16
14
|
import type { AgentSessionAdapter } from "@agentswf/harness/adapter";
|
|
@@ -19,6 +17,7 @@ import Type from "typebox";
|
|
|
19
17
|
import { writeJson } from "./files";
|
|
20
18
|
import { OPERATOR_ALIASES } from "./operator-aliases";
|
|
21
19
|
import { createRun, openRun, type Run, readAttempts } from "./runs";
|
|
20
|
+
import type { RunnableWorkflow } from "./workflow-runner";
|
|
22
21
|
import { type RunWorkflowOptions, runWorkflow, startWorkflow } from "./workflow-runner";
|
|
23
22
|
|
|
24
23
|
export function createTempRunDirs(): {
|
|
@@ -81,11 +80,11 @@ const Changes: OutputSchema<{ case: string }> = {
|
|
|
81
80
|
properties: { case: { type: "string" } },
|
|
82
81
|
required: ["case"],
|
|
83
82
|
};
|
|
84
|
-
const caseEvents = { "dd.changes": Changes };
|
|
83
|
+
export const caseEvents = { "dd.changes": Changes };
|
|
85
84
|
|
|
86
85
|
/** A workflow with one event, `dd.changes`, keyed by its case, whose body is `run`. */
|
|
87
86
|
export const caseRun = (
|
|
88
|
-
run: (workflow:
|
|
87
|
+
run: (workflow: WorkflowContext<typeof caseEvents>) => Promise<JsonValue>,
|
|
89
88
|
) =>
|
|
90
89
|
defineWorkflow({
|
|
91
90
|
meta: { name: "dd-case", description: "A case." },
|
|
@@ -104,7 +103,7 @@ export function future(milliseconds = 60_000): { unixMilliseconds: number } {
|
|
|
104
103
|
return { unixMilliseconds: Date.now() + milliseconds };
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
/** Answers a call the way an agent's `
|
|
106
|
+
/** Answers a call the way an agent's `wfa` does: one framed request over its own socket. */
|
|
108
107
|
export async function submit(
|
|
109
108
|
binding: { endpoint: string; operationId: string },
|
|
110
109
|
value: unknown,
|
|
@@ -140,7 +139,7 @@ export const DOC = Type.Object({ path: Type.String() }, { additionalProperties:
|
|
|
140
139
|
export function workflowOf<Result extends JsonValue>(
|
|
141
140
|
run: (workflow: WorkflowContext) => Promise<Result>,
|
|
142
141
|
meta: { name?: string; version?: string } = {},
|
|
143
|
-
):
|
|
142
|
+
): RunnableWorkflow<null, Result> {
|
|
144
143
|
const { name = "staged", version } = meta;
|
|
145
144
|
return {
|
|
146
145
|
meta: { name, description: name, ...(version === undefined ? {} : { version }) },
|
|
@@ -168,7 +167,7 @@ export function answering(): AgentSessionAdapter {
|
|
|
168
167
|
* `answering()`. Its folder is `{runRoot}/{workflow}/r1`.
|
|
169
168
|
*/
|
|
170
169
|
export async function runAttempt<Result extends JsonValue>(
|
|
171
|
-
workflow:
|
|
170
|
+
workflow: RunnableWorkflow<null, Result>,
|
|
172
171
|
options: {
|
|
173
172
|
runRoot: string;
|
|
174
173
|
attempt?: number;
|
|
@@ -206,7 +205,7 @@ export async function runAttempt<Result extends JsonValue>(
|
|
|
206
205
|
|
|
207
206
|
/** `startWorkflow` as the first attempt of a new run of the workflow under `options.runRoot`. */
|
|
208
207
|
export async function startNew<Args extends JsonValue, Result extends JsonValue>(
|
|
209
|
-
definition:
|
|
208
|
+
definition: RunnableWorkflow<Args, Result>,
|
|
210
209
|
args: Args,
|
|
211
210
|
options: Omit<RunWorkflowOptions, "run">,
|
|
212
211
|
) {
|
|
@@ -224,7 +223,7 @@ export async function startNew<Args extends JsonValue, Result extends JsonValue>
|
|
|
224
223
|
|
|
225
224
|
/** `runWorkflow` as the first attempt of a new run of the workflow under `options.runRoot`. */
|
|
226
225
|
export async function runNew<Args extends JsonValue, Result extends JsonValue>(
|
|
227
|
-
definition:
|
|
226
|
+
definition: RunnableWorkflow<Args, Result>,
|
|
228
227
|
args: Args,
|
|
229
228
|
options: Omit<RunWorkflowOptions, "run">,
|
|
230
229
|
) {
|
package/src/tick-command.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TickResultJson } from "@agentswf/contract/records";
|
|
2
2
|
import { messageOf } from "./errors";
|
|
3
3
|
import { exists } from "./files";
|
|
4
|
+
import help from "./help/tick.txt" with { type: "text" };
|
|
4
5
|
import { listProjects, projectRunRoot } from "./projects";
|
|
5
6
|
import type { Out } from "./run-commands";
|
|
6
7
|
import { parsePlace } from "./run-commands";
|
|
@@ -8,19 +9,7 @@ import { runLockFile, withRunLock } from "./run-lock";
|
|
|
8
9
|
import { listRuns } from "./run-state";
|
|
9
10
|
import { type Launch, wake } from "./wake";
|
|
10
11
|
|
|
11
|
-
export const tickUsage =
|
|
12
|
-
"usage: awf tick [--cwd <directory>] [--run-root <directory>] [--json]",
|
|
13
|
-
" awf tick --all [--json]",
|
|
14
|
-
"",
|
|
15
|
-
"Wakes every run of a workflow with events under --cwd's .awf/runs, or --run-root, the runs awf",
|
|
16
|
-
"runs lists there, as an event would: continues a parked run whose wait's timer is due, and a",
|
|
17
|
-
"crashed or failed one until it has failed three times in a row. Runs of other workflows are left",
|
|
18
|
-
"as they are. --all wakes them in every project awf has started one in, as ~/.awf/projects lists",
|
|
19
|
-
"them. Run it on any schedule; a run is continued once however many wakers come at once.",
|
|
20
|
-
"",
|
|
21
|
-
"Prints a line for each run it woke, and nothing when it woke none. --json prints one document.",
|
|
22
|
-
"Exits 0, or 1 when a project is gone or a run could not be woken; the others are still woken.",
|
|
23
|
-
].join("\n");
|
|
12
|
+
export const tickUsage = help.trimEnd();
|
|
24
13
|
|
|
25
14
|
export async function tickCommand(
|
|
26
15
|
argv: readonly string[],
|
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";
|
|
@@ -14,7 +15,14 @@ import { canonical } from "./canonical-json";
|
|
|
14
15
|
import { scheduleAt } from "./deadlines";
|
|
15
16
|
import { messageOf } from "./errors";
|
|
16
17
|
import type { RunEventBody } from "./run-event-log";
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
checkGiven,
|
|
20
|
+
QUOTING_RULE,
|
|
21
|
+
readGiven,
|
|
22
|
+
simpleInput,
|
|
23
|
+
takesText,
|
|
24
|
+
usageLine,
|
|
25
|
+
} from "./simple-input";
|
|
18
26
|
|
|
19
27
|
/** How long a handler that ignores its aborted signal is waited for before its call is settled. */
|
|
20
28
|
export const TOOL_SETTLE_MS = 5_000;
|
|
@@ -51,6 +59,8 @@ export type ToolHost<Scope> = {
|
|
|
51
59
|
/** Runs `fn` in the operation's scope, or in none. */
|
|
52
60
|
runIn<T>(scope: Scope | undefined, fn: () => T): T;
|
|
53
61
|
now?(): number;
|
|
62
|
+
/** A tool given its deprecated `timeoutMs`. */
|
|
63
|
+
deprecated?(name: "timeout-ms", call: string): void;
|
|
54
64
|
};
|
|
55
65
|
|
|
56
66
|
/** An operation as tool calls bind to it. */
|
|
@@ -69,7 +79,14 @@ export type ToolAnswer =
|
|
|
69
79
|
| { kind: "tool-failed"; outcome: ToolFailure; error: string }
|
|
70
80
|
| { kind: "refused"; code: ToolCode | "internal-error"; error: string };
|
|
71
81
|
|
|
72
|
-
|
|
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
|
+
};
|
|
73
90
|
|
|
74
91
|
type Running = {
|
|
75
92
|
tool: string;
|
|
@@ -108,7 +125,7 @@ export class ToolCalls<Scope> {
|
|
|
108
125
|
|
|
109
126
|
/** An agent opened or forked with these tools; checked against the limits first. */
|
|
110
127
|
give(agent: AgentKey, tools: AgentTools): void {
|
|
111
|
-
this.#held.set(agent, held(agent, tools));
|
|
128
|
+
this.#held.set(agent, held(agent, tools, this.#host.deprecated));
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
/**
|
|
@@ -117,7 +134,7 @@ export class ToolCalls<Scope> {
|
|
|
117
134
|
*/
|
|
118
135
|
reopen(agent: AgentKey, tools: AgentTools | undefined): void {
|
|
119
136
|
if (tools === undefined) return;
|
|
120
|
-
const next = held(agent, tools);
|
|
137
|
+
const next = held(agent, tools, this.#host.deprecated);
|
|
121
138
|
const existing = this.#held.get(agent);
|
|
122
139
|
if (!existing || !sameTools(existing, next)) {
|
|
123
140
|
throw new Error(`agent ${agent} is already open with different tools`);
|
|
@@ -195,7 +212,7 @@ export class ToolCalls<Scope> {
|
|
|
195
212
|
if (operation) operation.deadline = () => deadline;
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
/** The call this agent has running, which holds back its `
|
|
215
|
+
/** The call this agent has running, which holds back its `wfa result` and `wfa waiting`. */
|
|
199
216
|
running(agent: AgentKey): { tool: string; settledBy?: number } | undefined {
|
|
200
217
|
const running = this.#current.get(agent)?.running;
|
|
201
218
|
return running && { tool: running.tool, ...optional("settledBy", running.settledBy) };
|
|
@@ -204,7 +221,7 @@ export class ToolCalls<Scope> {
|
|
|
204
221
|
// Calls
|
|
205
222
|
|
|
206
223
|
/**
|
|
207
|
-
* `
|
|
224
|
+
* `wfa tool`'s request from `agent`'s socket. `lifetime` sets how long its connection may stay
|
|
208
225
|
* open, from now: undefined for as long as the call takes.
|
|
209
226
|
*/
|
|
210
227
|
async call(
|
|
@@ -282,12 +299,11 @@ export class ToolCalls<Scope> {
|
|
|
282
299
|
const controller = new AbortController();
|
|
283
300
|
let cause: ToolFailure | undefined;
|
|
284
301
|
let handlerEnded = false;
|
|
285
|
-
const
|
|
302
|
+
const budget = tools.budgets.get(request.tool)!;
|
|
286
303
|
const deadline = operation.deadline();
|
|
287
304
|
const startedAt = this.#now();
|
|
288
|
-
const budgetEnd = minimum(
|
|
289
|
-
const bound =
|
|
290
|
-
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;
|
|
291
307
|
const { promise: settled, resolve: settle } = Promise.withResolvers<void>();
|
|
292
308
|
const { promise: answered, resolve: answer } = Promise.withResolvers<ToolAnswer>();
|
|
293
309
|
const running: Running = {
|
|
@@ -472,7 +488,7 @@ function aborted(
|
|
|
472
488
|
tool: string,
|
|
473
489
|
cause: ToolFailure,
|
|
474
490
|
/** What bounded it: its own budget, or the turn's deadline. */
|
|
475
|
-
bound:
|
|
491
|
+
bound: Budget | "deadline",
|
|
476
492
|
): { kind: "tool-failed"; outcome: ToolFailure; error: string } {
|
|
477
493
|
const what =
|
|
478
494
|
cause === "cancelled"
|
|
@@ -519,8 +535,8 @@ function readInput(
|
|
|
519
535
|
|
|
520
536
|
/**
|
|
521
537
|
* The tools block of an operation's prompt: how to call one, then each tool's name and description
|
|
522
|
-
* and its usage line, or the schema for an input that isn't simple. The quoting rule comes with
|
|
523
|
-
* unless the result block above already said it; none for an agent without tools.
|
|
538
|
+
* and its usage line, or the schema for an input that isn't simple. The quoting rule comes with a
|
|
539
|
+
* free-text flag unless the result block above already said it; none for an agent without tools.
|
|
524
540
|
*/
|
|
525
541
|
export function toolsLesson(
|
|
526
542
|
tools: readonly { name: string; description: string; input?: JsonSchema }[],
|
|
@@ -533,9 +549,11 @@ export function toolsLesson(
|
|
|
533
549
|
return { ...each, simple };
|
|
534
550
|
});
|
|
535
551
|
const flags = taught.some((each) => each.simple && each.simple.length > 0);
|
|
552
|
+
const text = taught.some((each) => each.simple && takesText(each.simple));
|
|
536
553
|
const json = taught.some((each) => !each.simple);
|
|
554
|
+
const value = text ? "'<value>'" : "<value>";
|
|
537
555
|
const forms = [
|
|
538
|
-
flags ? `${launcher} tool <name> --<option>
|
|
556
|
+
flags ? `${launcher} tool <name> --<option> ${value} ...` : `${launcher} tool <name>`,
|
|
539
557
|
...(json ? [`${launcher} tool <name> <<'WF_JSON'`, "<json>", "WF_JSON"] : []),
|
|
540
558
|
];
|
|
541
559
|
const entries = taught.map(({ name, description, input, simple }) => {
|
|
@@ -549,13 +567,17 @@ export function toolsLesson(
|
|
|
549
567
|
"You have tools the workflow runs for you. Call one with:",
|
|
550
568
|
"",
|
|
551
569
|
...forms,
|
|
552
|
-
...(
|
|
570
|
+
...(text && !quotingSaid ? ["", QUOTING_RULE] : []),
|
|
553
571
|
"",
|
|
554
572
|
...entries,
|
|
555
573
|
].join("\n");
|
|
556
574
|
}
|
|
557
575
|
|
|
558
|
-
function held(
|
|
576
|
+
function held(
|
|
577
|
+
agent: AgentKey,
|
|
578
|
+
tools: AgentTools,
|
|
579
|
+
deprecated: ToolHost<unknown>["deprecated"],
|
|
580
|
+
): Held {
|
|
559
581
|
if (typeof tools !== "object" || tools === null || Array.isArray(tools)) {
|
|
560
582
|
throw new Error(`agent ${agent}'s tools are an object of tools by name`);
|
|
561
583
|
}
|
|
@@ -564,6 +586,7 @@ function held(agent: AgentKey, tools: AgentTools): Held {
|
|
|
564
586
|
throw new Error(`agent ${agent} has ${names.length} tools; at most ${TOOL_LIMITS.tools}`);
|
|
565
587
|
}
|
|
566
588
|
const schemas = new Map<string, JsonSchema | undefined>();
|
|
589
|
+
const budgets = new Map<string, Budget>();
|
|
567
590
|
for (const name of names) {
|
|
568
591
|
const spec = tools[name]!;
|
|
569
592
|
const where = `agent ${agent}'s tool ${JSON.stringify(name)}`;
|
|
@@ -577,14 +600,7 @@ function held(agent: AgentKey, tools: AgentTools): Held {
|
|
|
577
600
|
if (Buffer.byteLength(spec.description) > TOOL_LIMITS.descriptionBytes) {
|
|
578
601
|
throw new Error(`${where}: its description is over ${TOOL_LIMITS.descriptionBytes} bytes`);
|
|
579
602
|
}
|
|
580
|
-
|
|
581
|
-
if (
|
|
582
|
-
timeout !== undefined &&
|
|
583
|
-
timeout !== false &&
|
|
584
|
-
!(Number.isSafeInteger(timeout) && timeout > 0)
|
|
585
|
-
) {
|
|
586
|
-
throw new Error(`${where}: timeoutMs is a positive integer, or false for none`);
|
|
587
|
-
}
|
|
603
|
+
budgets.set(name, budgetOf(where, spec, deprecated));
|
|
588
604
|
if (spec.input === undefined) {
|
|
589
605
|
schemas.set(name, undefined);
|
|
590
606
|
continue;
|
|
@@ -610,7 +626,28 @@ function held(agent: AgentKey, tools: AgentTools): Held {
|
|
|
610
626
|
// Its JSON, in the author's order for the lesson, without TypeBox's own keys.
|
|
611
627
|
schemas.set(name, JSON.parse(JSON.stringify(input)) as JsonSchema);
|
|
612
628
|
}
|
|
613
|
-
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;
|
|
614
651
|
}
|
|
615
652
|
|
|
616
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
|
);
|