@agentswf/engine 0.1.0-beta.0 → 0.1.0-beta.2
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 -5
- package/src/init-command.ts +240 -0
- package/src/init-template/AGENTS.md.template +16 -0
- package/src/init-template/hello/workflow.test.ts.template +17 -0
- package/src/init-template/hello/workflow.ts.template +26 -0
- package/src/init-template/tsconfig.json.template +11 -0
- package/src/operator-cli.ts +31 -0
- package/src/run-command.ts +1 -0
- package/src/sandboxes.ts +5 -0
- package/src/workflow-runner.ts +9 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentswf/engine",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "agents.wf: the engine that runs workflows made of coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/romanlv/agentswf",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"!src/**/fixtures"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@agentswf/wf": "0.1.0-beta.
|
|
33
|
-
"@agentswf/contract": "0.1.0-beta.
|
|
34
|
-
"@agentswf/harness": "0.1.0-beta.
|
|
35
|
-
"@agentswf/sandbox": "0.1.0-beta.
|
|
32
|
+
"@agentswf/wf": "0.1.0-beta.2",
|
|
33
|
+
"@agentswf/contract": "0.1.0-beta.2",
|
|
34
|
+
"@agentswf/harness": "0.1.0-beta.2",
|
|
35
|
+
"@agentswf/sandbox": "0.1.0-beta.2",
|
|
36
36
|
"typebox": "^1.3.34"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import manifest from "../package.json" with { type: "json" };
|
|
4
|
+
|
|
5
|
+
export const initUsage = [
|
|
6
|
+
"usage: awf init [dir] [--no-install]",
|
|
7
|
+
"",
|
|
8
|
+
"Sets up the current directory, or dir, for writing workflows: hello/workflow.ts, a workflow",
|
|
9
|
+
"with one agent, and hello/workflow.test.ts, its test; package.json, tsconfig.json, .gitignore",
|
|
10
|
+
"and AGENTS.md. Then runs bun install, unless --no-install, for editor types: awf runs a",
|
|
11
|
+
"workflow against its own copies either way.",
|
|
12
|
+
"",
|
|
13
|
+
"Never overwrites. A file that exists is skipped, except that package.json gains the",
|
|
14
|
+
"devDependencies and scripts it lacks and .gitignore the lines it lacks.",
|
|
15
|
+
].join("\n");
|
|
16
|
+
|
|
17
|
+
export type InitCommand = { dir: string; install: boolean };
|
|
18
|
+
|
|
19
|
+
export function parseInitCommand(argv: readonly string[], cwd: string): InitCommand | "help" {
|
|
20
|
+
let dir: string | undefined;
|
|
21
|
+
let install = true;
|
|
22
|
+
for (const option of argv) {
|
|
23
|
+
if (option === "-h" || option === "--help") return "help";
|
|
24
|
+
if (option === "--no-install") install = false;
|
|
25
|
+
else if (option.startsWith("-")) throw new Error(`unknown option: ${option}`);
|
|
26
|
+
else if (dir !== undefined) throw new Error(`one directory only, not ${dir} and ${option}`);
|
|
27
|
+
else dir = option;
|
|
28
|
+
}
|
|
29
|
+
const path = resolve(cwd, dir ?? ".");
|
|
30
|
+
if (existsSync(path) && !statSync(path).isDirectory()) {
|
|
31
|
+
throw new Error(`not a directory: ${dir}`);
|
|
32
|
+
}
|
|
33
|
+
return { dir: path, install };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TEMPLATES = join(import.meta.dir, "init-template");
|
|
37
|
+
|
|
38
|
+
/** Copied as they are, each only where nothing is yet. */
|
|
39
|
+
const COPIED = ["hello/workflow.ts", "hello/workflow.test.ts", "tsconfig.json", "AGENTS.md"];
|
|
40
|
+
|
|
41
|
+
const SCRIPTS = { test: "awf test", check: "tsc --noEmit" };
|
|
42
|
+
|
|
43
|
+
// The versions this repository builds with; the root's catalog holds the same.
|
|
44
|
+
export const DEV_DEPENDENCIES = {
|
|
45
|
+
"@types/bun": "^1.3.4",
|
|
46
|
+
// agentswf is released at the engine's version; it can't be imported here, it imports the engine.
|
|
47
|
+
agentswf: `^${manifest.version}`,
|
|
48
|
+
typebox: manifest.dependencies.typebox,
|
|
49
|
+
typescript: "^5.9.3",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const IGNORED = ["node_modules/", ".awf/"];
|
|
53
|
+
|
|
54
|
+
type Change = { path: string; change: "created" | "updated" | "skipped"; detail?: string };
|
|
55
|
+
|
|
56
|
+
/** A target init may write: absent, a file it reads or skips, or a symlink it never follows. */
|
|
57
|
+
type Target = "missing" | "file" | "symlink";
|
|
58
|
+
|
|
59
|
+
function targetOf(dir: string, path: string): Target {
|
|
60
|
+
const stat = lstatSync(join(dir, path), { throwIfNoEntry: false });
|
|
61
|
+
if (stat === undefined) return "missing";
|
|
62
|
+
if (stat.isSymbolicLink()) return "symlink";
|
|
63
|
+
if (!stat.isFile()) throw new Error(`${path} exists and is not a file`);
|
|
64
|
+
return "file";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Writes what `awf init` writes into `dir`, and says what it did to each file. Everything that
|
|
69
|
+
* would stop it, such as a package.json that isn't a JSON object, is found before it writes.
|
|
70
|
+
*/
|
|
71
|
+
export function initFolder(dir: string): Change[] {
|
|
72
|
+
for (const parent of new Set(COPIED.map(dirname))) {
|
|
73
|
+
const stat = lstatSync(join(dir, parent), { throwIfNoEntry: false });
|
|
74
|
+
if (stat !== undefined && !stat.isDirectory()) {
|
|
75
|
+
throw new Error(`${parent} exists and is not a directory`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const targets = new Map(
|
|
79
|
+
["package.json", ".gitignore", ...COPIED].map((path) => [path, targetOf(dir, path)]),
|
|
80
|
+
);
|
|
81
|
+
const pkg = targets.get("package.json") === "file" ? readPackageJson(dir) : undefined;
|
|
82
|
+
|
|
83
|
+
const changes: Change[] = [packageJson(dir, targets.get("package.json")!, pkg)];
|
|
84
|
+
for (const path of COPIED) {
|
|
85
|
+
const target = targets.get(path)!;
|
|
86
|
+
if (target !== "missing") {
|
|
87
|
+
changes.push({ path, change: "skipped", detail: skippedBecause(target) });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
mkdirSync(join(dir, dirname(path)), { recursive: true });
|
|
91
|
+
const template = readFileSync(join(TEMPLATES, `${path}.template`));
|
|
92
|
+
writeFileSync(join(dir, path), template, { flag: "wx" });
|
|
93
|
+
changes.push({ path, change: "created" });
|
|
94
|
+
}
|
|
95
|
+
changes.push(gitignore(dir, targets.get(".gitignore")!));
|
|
96
|
+
return changes;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const skippedBecause = (target: Target) => (target === "symlink" ? "a symlink" : "exists");
|
|
100
|
+
|
|
101
|
+
function readPackageJson(dir: string): { text: string; value: Record<string, unknown> } {
|
|
102
|
+
const text = readFileSync(join(dir, "package.json"), "utf8");
|
|
103
|
+
let value: unknown;
|
|
104
|
+
try {
|
|
105
|
+
value = JSON.parse(text);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`package.json is not JSON: ${(error as Error).message}`);
|
|
108
|
+
}
|
|
109
|
+
if (!isRecord(value)) throw new Error("package.json is not a JSON object");
|
|
110
|
+
return { text, value };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
114
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
115
|
+
|
|
116
|
+
function packageJson(
|
|
117
|
+
dir: string,
|
|
118
|
+
target: Target,
|
|
119
|
+
pkg: { text: string; value: Record<string, unknown> } | undefined,
|
|
120
|
+
): Change {
|
|
121
|
+
const path = join(dir, "package.json");
|
|
122
|
+
if (target === "symlink") return { path: "package.json", change: "skipped", detail: "a symlink" };
|
|
123
|
+
if (pkg === undefined) {
|
|
124
|
+
const created = {
|
|
125
|
+
name: packageName(dir),
|
|
126
|
+
private: true,
|
|
127
|
+
type: "module",
|
|
128
|
+
scripts: SCRIPTS,
|
|
129
|
+
devDependencies: DEV_DEPENDENCIES,
|
|
130
|
+
};
|
|
131
|
+
writeFileSync(path, `${JSON.stringify(created, null, 2)}\n`, { flag: "wx" });
|
|
132
|
+
return { path: "package.json", change: "created" };
|
|
133
|
+
}
|
|
134
|
+
const { text, value: existing } = pkg;
|
|
135
|
+
const record = (key: string): Record<string, unknown> => {
|
|
136
|
+
const value = existing[key];
|
|
137
|
+
return isRecord(value) ? value : {};
|
|
138
|
+
};
|
|
139
|
+
const scripts = record("scripts");
|
|
140
|
+
const declared = {
|
|
141
|
+
...record("dependencies"),
|
|
142
|
+
...record("devDependencies"),
|
|
143
|
+
...record("peerDependencies"),
|
|
144
|
+
...record("optionalDependencies"),
|
|
145
|
+
};
|
|
146
|
+
const addedScripts = Object.entries(SCRIPTS).filter(([name]) => !(name in scripts));
|
|
147
|
+
const addedDependencies = Object.entries(DEV_DEPENDENCIES).filter(
|
|
148
|
+
([name]) => !(name in declared),
|
|
149
|
+
);
|
|
150
|
+
if (addedScripts.length === 0 && addedDependencies.length === 0) {
|
|
151
|
+
return { path: "package.json", change: "skipped", detail: "has what init adds" };
|
|
152
|
+
}
|
|
153
|
+
const updated = {
|
|
154
|
+
...existing,
|
|
155
|
+
scripts: { ...scripts, ...Object.fromEntries(addedScripts) },
|
|
156
|
+
devDependencies: { ...record("devDependencies"), ...Object.fromEntries(addedDependencies) },
|
|
157
|
+
};
|
|
158
|
+
const json = `${JSON.stringify(updated, null, indentOf(text))}\n`;
|
|
159
|
+
writeFileSync(path, text.includes("\r\n") ? json.replaceAll("\n", "\r\n") : json);
|
|
160
|
+
const added = [
|
|
161
|
+
...addedDependencies.map(([name]) => name),
|
|
162
|
+
...addedScripts.map(([name]) => `the ${name} script`),
|
|
163
|
+
];
|
|
164
|
+
return { path: "package.json", change: "updated", detail: `added ${added.join(", ")}` };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function gitignore(dir: string, target: Target): Change {
|
|
168
|
+
const path = join(dir, ".gitignore");
|
|
169
|
+
if (target === "symlink") return { path: ".gitignore", change: "skipped", detail: "a symlink" };
|
|
170
|
+
if (target === "missing") {
|
|
171
|
+
writeFileSync(path, `${IGNORED.join("\n")}\n`, { flag: "wx" });
|
|
172
|
+
return { path: ".gitignore", change: "created" };
|
|
173
|
+
}
|
|
174
|
+
const text = readFileSync(path, "utf8");
|
|
175
|
+
const lines = new Set(text.split("\n").map((line) => line.trim().replace(/^\/|\/$/g, "")));
|
|
176
|
+
const missing = IGNORED.filter((line) => !lines.has(line.replace(/\/$/, "")));
|
|
177
|
+
if (missing.length === 0) return { path: ".gitignore", change: "skipped", detail: "has both" };
|
|
178
|
+
const newline = text.includes("\r\n") ? "\r\n" : "\n";
|
|
179
|
+
const separator = text === "" || text.endsWith("\n") ? "" : newline;
|
|
180
|
+
writeFileSync(path, `${text}${separator}${missing.join(newline)}${newline}`);
|
|
181
|
+
return { path: ".gitignore", change: "updated", detail: `added ${missing.join(", ")}` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** A valid npm name from the folder's, which is all a private package's name has to be. */
|
|
185
|
+
function packageName(dir: string): string {
|
|
186
|
+
const name = basename(dir)
|
|
187
|
+
.toLowerCase()
|
|
188
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
189
|
+
.slice(0, 214)
|
|
190
|
+
.replace(/^[._-]+|-+$/g, "");
|
|
191
|
+
return name || "workflows";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function indentOf(text: string): string | number {
|
|
195
|
+
return /^\{\s*\r?\n([ \t]+)"/.exec(text)?.[1] ?? 2;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** As a shell reads it: bare when that's safe, otherwise single-quoted. */
|
|
199
|
+
function shellWord(word: string): string {
|
|
200
|
+
return /^[\w./-]+$/.test(word) ? word : `'${word.replaceAll("'", "'\\''")}'`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** `awf init`: writes the folder, installs unless told not to, and says what to run next. */
|
|
204
|
+
export async function runInit(
|
|
205
|
+
command: InitCommand,
|
|
206
|
+
cwd: string,
|
|
207
|
+
output: { stdout(text: string): void; stderr(text: string): void },
|
|
208
|
+
install: (dir: string) => Promise<number> = bunInstall,
|
|
209
|
+
): Promise<number> {
|
|
210
|
+
mkdirSync(command.dir, { recursive: true });
|
|
211
|
+
const changes = initFolder(command.dir);
|
|
212
|
+
const installed = command.install ? (await install(command.dir)) === 0 : undefined;
|
|
213
|
+
const width = Math.max(...changes.map(({ change }) => change.length));
|
|
214
|
+
const lines = changes.map(
|
|
215
|
+
({ path, change, detail }) =>
|
|
216
|
+
`${change.padEnd(width)} ${path}${detail === undefined ? "" : ` (${detail})`}`,
|
|
217
|
+
);
|
|
218
|
+
const into = relative(cwd, command.dir);
|
|
219
|
+
const next = [
|
|
220
|
+
...(into === "" ? [] : [`cd ${shellWord(into)}`]),
|
|
221
|
+
...(installed ? [] : ["bun install"]),
|
|
222
|
+
"awf test # scripted agents: free, no agent starts",
|
|
223
|
+
"awf run hello/workflow.ts # one real codex agent",
|
|
224
|
+
];
|
|
225
|
+
output.stdout([...lines, "", "next:", ...next.map((line) => ` ${line}`)].join("\n"));
|
|
226
|
+
if (installed === false) {
|
|
227
|
+
output.stderr("awf: bun install failed; the files are written, run it again there");
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function bunInstall(dir: string): Promise<number> {
|
|
234
|
+
const child = Bun.spawn([process.execPath, "install"], {
|
|
235
|
+
cwd: dir,
|
|
236
|
+
stdout: "inherit",
|
|
237
|
+
stderr: "inherit",
|
|
238
|
+
});
|
|
239
|
+
return child.exited;
|
|
240
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Workflows
|
|
2
|
+
|
|
3
|
+
This folder holds [agents.wf](https://github.com/romanlv/agentswf) workflows: TypeScript files
|
|
4
|
+
that run coding agents, started with `awf`.
|
|
5
|
+
|
|
6
|
+
- A workflow is a folder with a `workflow.ts`, which default-exports
|
|
7
|
+
`defineExecutableWorkflow(…)` from `agentswf/workflow`, and a `workflow.test.ts` beside it.
|
|
8
|
+
`hello/` is the smallest one; copy it to start another.
|
|
9
|
+
- The API, with every option and outcome:
|
|
10
|
+
https://github.com/romanlv/agentswf/blob/main/docs/workflow-api.md
|
|
11
|
+
- `awf test` runs the tests with each agent replaced by scripted answers: no agent starts, and it
|
|
12
|
+
costs nothing. After a change, run it and `bun run check`, which typechecks.
|
|
13
|
+
- `awf run hello/workflow.ts` starts real agents, on the operator's accounts. Don't run a workflow
|
|
14
|
+
unless asked.
|
|
15
|
+
- Each run is kept in `.awf/runs/{workflow}/{id}`. `awf runs` lists them; `awf logs {run}` shows
|
|
16
|
+
what one did.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { answer, reply, testWorkflow } from "agentswf/testing";
|
|
3
|
+
import hello, { ANSWER } from "./workflow";
|
|
4
|
+
|
|
5
|
+
test("returns the agent's answer", async () => {
|
|
6
|
+
const run = await testWorkflow(hello, {}, {
|
|
7
|
+
agents: { hello: answer(ANSWER, { answer: 391 }) },
|
|
8
|
+
});
|
|
9
|
+
expect(run.value).toEqual({ answer: 391 });
|
|
10
|
+
expect(run.turnsOf("hello")[0]?.prompt).toContain("17 multiplied by 23");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("an agent that never answers is nudged once, then the result says why", async () => {
|
|
14
|
+
const run = await testWorkflow(hello, {}, { agents: { hello: reply.silent() } });
|
|
15
|
+
expect(run.value).toHaveProperty("unanswered");
|
|
16
|
+
expect(run.turnsOf("hello").map((turn) => turn.nudge)).toEqual([false, true]);
|
|
17
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineExecutableWorkflow, isAnswered } from "agentswf/workflow";
|
|
2
|
+
import Type from "typebox";
|
|
3
|
+
|
|
4
|
+
export const ANSWER = Type.Object({ answer: Type.Integer() }, { additionalProperties: false });
|
|
5
|
+
|
|
6
|
+
export default defineExecutableWorkflow({
|
|
7
|
+
definition: {
|
|
8
|
+
meta: { name: "hello", description: "Ask one agent a question with a known answer." },
|
|
9
|
+
async run(workflow) {
|
|
10
|
+
// A headless agent needs neither Herdr nor a sandbox. Swap in any harness and model your
|
|
11
|
+
// account has: "claude" in a pane, the default placement, runs on a subscription.
|
|
12
|
+
const agent = await workflow.agents.open({
|
|
13
|
+
key: "hello",
|
|
14
|
+
runtime: { harness: "codex", model: "gpt-6-luna", placement: "headless" },
|
|
15
|
+
});
|
|
16
|
+
const { outcome } = await agent.run<Type.Static<typeof ANSWER>>({
|
|
17
|
+
prompt: "What is 17 multiplied by 23? Work it out yourself; do not run any code.",
|
|
18
|
+
schema: ANSWER,
|
|
19
|
+
timeoutMs: 3 * 60_000,
|
|
20
|
+
});
|
|
21
|
+
return isAnswered(outcome) ? outcome.value : { unanswered: outcome.reason };
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
// Turns the command line into args: `awf run hello/workflow.ts -- ...`.
|
|
25
|
+
prepare: () => ({}),
|
|
26
|
+
});
|
package/src/operator-cli.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { type Kept, keepRecords } from "./attempt-output";
|
|
|
27
27
|
import { continueCommand, printEnding, toldOf } from "./attempt-view";
|
|
28
28
|
import { messageOf } from "./errors";
|
|
29
29
|
import { type Caller, showOwnTab, startHere, takeCaller } from "./here";
|
|
30
|
+
import { type InitCommand, initUsage, parseInitCommand, runInit } from "./init-command";
|
|
30
31
|
import { logsCommand } from "./logs-command";
|
|
31
32
|
import { machinePaths, sandboxesDirOf } from "./machine";
|
|
32
33
|
import {
|
|
@@ -79,6 +80,8 @@ export type OperatorEnvironment = {
|
|
|
79
80
|
callerSearchMs?: number;
|
|
80
81
|
signal?: AbortSignal;
|
|
81
82
|
now?: () => number;
|
|
83
|
+
/** What `awf init` runs to install, in the folder it set up. */
|
|
84
|
+
install?: (dir: string) => Promise<number>;
|
|
82
85
|
/** How `awf allowance` reads a harness's plan. */
|
|
83
86
|
readAllowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
84
87
|
/**
|
|
@@ -126,6 +129,7 @@ export async function runOperatorCli(
|
|
|
126
129
|
return 0;
|
|
127
130
|
}
|
|
128
131
|
if (argv[0] === "test") return testCommand(argv.slice(1), environment, stdout, stderr);
|
|
132
|
+
if (argv[0] === "init") return initCommand(argv.slice(1), environment, stdout, stderr);
|
|
129
133
|
if (argv[0] === "logs") {
|
|
130
134
|
return logsCommand(argv.slice(1), environment.cwd ?? process.cwd(), { stdout, stderr });
|
|
131
135
|
}
|
|
@@ -453,6 +457,33 @@ async function allowanceCommand(
|
|
|
453
457
|
return 0;
|
|
454
458
|
}
|
|
455
459
|
|
|
460
|
+
/** `awf init`: a folder set up for writing workflows. */
|
|
461
|
+
async function initCommand(
|
|
462
|
+
argv: readonly string[],
|
|
463
|
+
environment: OperatorEnvironment,
|
|
464
|
+
stdout: (text: string) => void,
|
|
465
|
+
stderr: (text: string) => void,
|
|
466
|
+
): Promise<number> {
|
|
467
|
+
const cwd = environment.cwd ?? process.cwd();
|
|
468
|
+
let command: InitCommand | "help";
|
|
469
|
+
try {
|
|
470
|
+
command = parseInitCommand(argv, cwd);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
stderr(`awf: ${messageOf(error)}\n\n${initUsage}`);
|
|
473
|
+
return 2;
|
|
474
|
+
}
|
|
475
|
+
if (command === "help") {
|
|
476
|
+
stdout(initUsage);
|
|
477
|
+
return 0;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
return await runInit(command, cwd, { stdout, stderr }, environment.install);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
stderr(`awf: ${messageOf(error)}`);
|
|
483
|
+
return 1;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
456
487
|
/** `awf test`: the workflow tests under the given paths, with Bun's test runner. */
|
|
457
488
|
async function testCommand(
|
|
458
489
|
argv: readonly string[],
|
package/src/run-command.ts
CHANGED
|
@@ -11,6 +11,7 @@ const DEFAULT_TIMEOUT = "1h";
|
|
|
11
11
|
export const usage = [
|
|
12
12
|
"usage: awf run [options] <workflow-file> [options] [-- workflow arguments...]",
|
|
13
13
|
" awf run [options] <workflow-file> --continue <id>",
|
|
14
|
+
" awf init [dir] [--no-install]",
|
|
14
15
|
" awf test [paths...] [-t <pattern>] [--watch] [--timeout <duration>]",
|
|
15
16
|
" awf allowance [harness...] [--json]",
|
|
16
17
|
" awf runs [<run>] [--json] (awf runs -h)",
|
package/src/sandboxes.ts
CHANGED
|
@@ -75,6 +75,8 @@ export type Seat = {
|
|
|
75
75
|
|
|
76
76
|
export type SeatedAgent = {
|
|
77
77
|
occupant: Occupant;
|
|
78
|
+
/** Its sandbox keeps the harness's model-side web tools. */
|
|
79
|
+
webSearch?: true;
|
|
78
80
|
/** Hands a refreshed credential back to the operator; after each operation and at release. */
|
|
79
81
|
writeBack(): Promise<void>;
|
|
80
82
|
/** Writes back, then releases the occupant; once, however often it is called. */
|
|
@@ -299,6 +301,8 @@ export class RunSandboxes {
|
|
|
299
301
|
read: [...resolved.read],
|
|
300
302
|
write: [...resolved.write],
|
|
301
303
|
network: [...resolved.network],
|
|
304
|
+
...(resolved.sockets.length > 0 ? { sockets: [...resolved.sockets] } : {}),
|
|
305
|
+
...(resolved.webSearch ? { webSearch: true } : {}),
|
|
302
306
|
[provider]: resolved.environment,
|
|
303
307
|
},
|
|
304
308
|
directory,
|
|
@@ -441,6 +445,7 @@ export class RunSandboxes {
|
|
|
441
445
|
let released: Promise<void> | undefined;
|
|
442
446
|
const seated: SeatedAgent = {
|
|
443
447
|
occupant: this.#logged(sandbox, occupant, who),
|
|
448
|
+
...(sandbox.resolved.webSearch ? { webSearch: true as const } : {}),
|
|
444
449
|
writeBack: () => seeded.writeBack(),
|
|
445
450
|
release: () => {
|
|
446
451
|
released ??= seeded
|
package/src/workflow-runner.ts
CHANGED
|
@@ -79,7 +79,6 @@ import type {
|
|
|
79
79
|
SessionCopy,
|
|
80
80
|
SessionSettings,
|
|
81
81
|
} from "@agentswf/harness/adapter";
|
|
82
|
-
import type { Occupant } from "@agentswf/sandbox";
|
|
83
82
|
import { PUBLISHED_PRICES } from "./accounting/prices";
|
|
84
83
|
import { summarizeRun } from "./accounting/summary";
|
|
85
84
|
import { buildAgentBundle, installAgentLauncher, installSandboxedDoor } from "./agent-launcher";
|
|
@@ -687,10 +686,10 @@ type AgentOpening = {
|
|
|
687
686
|
ledger: AgentLedger;
|
|
688
687
|
/** Its skills' sources, resolved when it opens; absent when it has the operator's. */
|
|
689
688
|
skills?: SkillSource[];
|
|
690
|
-
/** The host's activation for the agent, working in `cwd`, inside
|
|
689
|
+
/** The host's activation for the agent, working in `cwd`, inside its seat's occupant if sandboxed. */
|
|
691
690
|
activation(
|
|
692
691
|
cwd: string,
|
|
693
|
-
|
|
692
|
+
seated?: Pick<SeatedAgent, "occupant" | "webSearch">,
|
|
694
693
|
skills?: AgentSkills,
|
|
695
694
|
/** Its harness home: a sandbox's, or its own on the host. */
|
|
696
695
|
home?: string,
|
|
@@ -1227,14 +1226,15 @@ class WorkflowOwner {
|
|
|
1227
1226
|
sessions,
|
|
1228
1227
|
ledger: this.options.ledger.agent(accounted),
|
|
1229
1228
|
...(identity.skills ? { skills: identity.skills } : {}),
|
|
1230
|
-
activation: (cwd,
|
|
1229
|
+
activation: (cwd, seated, skills, home) => ({
|
|
1231
1230
|
key: spec.key,
|
|
1232
1231
|
deadline: effectiveDeadline,
|
|
1233
1232
|
cwd,
|
|
1234
1233
|
execution,
|
|
1235
1234
|
...(spec.instructions === undefined ? {} : { instructions: spec.instructions }),
|
|
1236
1235
|
...(spec.labels === undefined ? {} : { labels: spec.labels }),
|
|
1237
|
-
...(
|
|
1236
|
+
...(seated ? { occupant: seated.occupant } : {}),
|
|
1237
|
+
...(seated?.webSearch ? { webSearch: true } : {}),
|
|
1238
1238
|
...(skills ? { skills } : {}),
|
|
1239
1239
|
...(home ? { home } : {}),
|
|
1240
1240
|
...paneOptions(identity),
|
|
@@ -1624,7 +1624,7 @@ class WorkflowOwner {
|
|
|
1624
1624
|
let session: HarnessSession;
|
|
1625
1625
|
try {
|
|
1626
1626
|
session = await this.options.host.openAgent(
|
|
1627
|
-
activation(seat.cwd, seated
|
|
1627
|
+
activation(seat.cwd, seated, seat.skills, seat.home),
|
|
1628
1628
|
);
|
|
1629
1629
|
} catch (error) {
|
|
1630
1630
|
await seated.release().catch(() => undefined);
|
|
@@ -1827,14 +1827,15 @@ class WorkflowOwner {
|
|
|
1827
1827
|
ledger: this.options.ledger.agent(accounted),
|
|
1828
1828
|
...(sources ? { skills: sources } : {}),
|
|
1829
1829
|
...(sandboxed && copy ? { carried: { parent: parentKey, forked, copy } } : {}),
|
|
1830
|
-
activation: (cwd,
|
|
1830
|
+
activation: (cwd, seated, given, home) => ({
|
|
1831
1831
|
key: spec.key,
|
|
1832
1832
|
deadline,
|
|
1833
1833
|
cwd,
|
|
1834
1834
|
execution,
|
|
1835
1835
|
...(forkSpec.instructions === undefined ? {} : { instructions: forkSpec.instructions }),
|
|
1836
1836
|
...(forkSpec.labels === undefined ? {} : { labels: forkSpec.labels }),
|
|
1837
|
-
...(
|
|
1837
|
+
...(seated ? { occupant: seated.occupant } : {}),
|
|
1838
|
+
...(seated?.webSearch ? { webSearch: true } : {}),
|
|
1838
1839
|
...(given ? { skills: given } : {}),
|
|
1839
1840
|
...(home ? { home } : {}),
|
|
1840
1841
|
...(continues ? { continues } : {}),
|