@flow-engineer/flow 0.3.1 → 0.3.3
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/README.md +9 -2
- package/coding-agents/skill.md +2 -0
- package/coding-agents/test-skill.md +7 -2
- package/dist/testing/src/account.d.ts +1 -1
- package/dist/testing/src/account.js +61 -14
- package/dist/testing/src/cli.js +19 -3
- package/dist/testing/src/client.d.ts +2 -0
- package/dist/testing/src/client.js +3 -0
- package/dist/testing/src/flow-login.d.ts +4 -0
- package/dist/testing/src/flow-login.js +5 -2
- package/dist/testing/src/print-run.js +6 -1
- package/dist/testing/src/protocol.d.ts +6 -0
- package/dist/testing/src/report.d.ts +46 -0
- package/dist/testing/src/report.js +42 -0
- package/dist/testing/src/show.d.ts +9 -0
- package/docs/agents.md +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -408,13 +408,18 @@ Each run's line carries its id (`FAIL latte-correction #1 (whatsapp) trn_…: 6
|
|
|
408
408
|
"runs": [
|
|
409
409
|
{ "runId": "trn_…", "scenario": "latte-correction", "channel": "whatsapp", "run": 1, "verdict": "failed",
|
|
410
410
|
"error": null, "ended": "goal_met", "turns": 6, "watch": "https://…",
|
|
411
|
-
"failedChecks": [{ "kind": "judge", "text": "The agent asks Sam to confirm before placing the order.", "reason": "…" }]
|
|
412
|
-
|
|
411
|
+
"failedChecks": [{ "kind": "judge", "text": "The agent asks Sam to confirm before placing the order.", "reason": "…" }],
|
|
412
|
+
"invalid": null }
|
|
413
|
+
],
|
|
414
|
+
"invalid": [],
|
|
415
|
+
"sessionInvalid": null
|
|
413
416
|
}
|
|
414
417
|
```
|
|
415
418
|
|
|
416
419
|
`outcome` is `passed` (exit 0), `failed` or `invalid` (1), `errored` or `transport` (2), `refused` (3) or `interrupted` (130). A run's `verdict` is `errored` whenever the cloud names an `error` for it (such as `judge_failed`, `simulator_failed` or `spend_cap`), whatever it `ended` as: the cloud could not play or judge it, so it says nothing about the agent. Its line reads `ERROR` instead of `PASS` or `FAIL`, and the run's `error` is that code (else `null`). `error` is `{ "message", "status", "type", "retryAfterSeconds" }` when the session did not finish (`type` is the cloud's code, such as `spend_cap`; the others are `null` when it did not say).
|
|
417
420
|
|
|
421
|
+
A scenario the cloud cannot play (a key it does not know, a value it cannot read) does not stop the others: its runs are `errored` with `error` `scenario_invalid` and `invalid` `{ "path", "reason" }` (the reason starts `unsupported:` when the cloud does not know a key), its line reads `INVALID` with the file and what is wrong, and the top-level `invalid` lists each such scenario once as `{ "scenario", "path", "reason" }`. When the cloud ends the whole session `scenario_invalid` (a tools or checks file it cannot read, or an older cloud), `sessionInvalid` is `{ "reason", "olderCloud", "sent" }`: what the cloud said after the code (else `null`), whether that suggests a cloud older than this client, and the scenarios sent as `{ "scenario", "path" }`; the same lines are printed.
|
|
422
|
+
|
|
418
423
|
`--only latte-correction,order-fails` runs only those scenarios (file names without `.yaml`); every tool file still goes. Once the session has opened, every exit (0, 1, 2, 3 or 130) writes `.flow/last-session.json` in the agent's folder and prints its path; `flow new` git-ignores `.flow/`. It holds what each run ended as and the judge's reasons, never the messages (a reason can quote one, which is why the file stays on your machine; the suite is synthetic by `--synthetic`):
|
|
419
424
|
|
|
420
425
|
```json
|
|
@@ -429,6 +434,8 @@ Each run's line carries its id (`FAIL latte-correction #1 (whatsapp) trn_…: 6
|
|
|
429
434
|
}
|
|
430
435
|
```
|
|
431
436
|
|
|
437
|
+
A run of a scenario the cloud could not play also has `invalid` (`{ "path", "reason" }`), and a session the cloud ended `scenario_invalid` has `sessionInvalid`, as in `--json`.
|
|
438
|
+
|
|
432
439
|
`ended` is `finished`, `stopped` (the session broke off or the cloud stopped it) or `interrupted`. `agent` is the cloud's id for the agent (a hosted cloud names it), which `flow show` reads. `only` is the scenarios the session was limited to, `null` for the whole suite. `verdict` is `passed`, `failed`, `errored` (the cloud could not play or judge the run) or `not_played` (the session ended first; its `runId` is `null`).
|
|
433
440
|
|
|
434
441
|
`flow show <run-id>` prints one run's record from the cloud, read with `FLOW_FRAMEWORK_CLOUD_KEY`: the verdict, every check with the judge's reason, then the conversation in time order with each tool call the simulator played (its input, output and what it changed). `flow show last` picks the first run in `.flow/last-session.json` that did not pass (its first run when all passed) and names the others. `--json` prints the record exactly as the cloud answered it. It exits 0 when it printed; 1 for a wrong flag, no key or last session, no such run (404) or one past the 30 days runs are kept (410); 2 when the cloud did not answer or answered 5xx; 3 when it refused the key (401, 403) or is busy (429).
|
package/coding-agents/skill.md
CHANGED
|
@@ -352,6 +352,8 @@ run again. `--channel telegram|whatsapp|imessage` picks the channel; a channel f
|
|
|
352
352
|
open yet exits 3 with its `next`. When only observe's part of setup fails (traces and the test cloud),
|
|
353
353
|
`link` still prints the link and exits 0, with one warning saying what is missing and the server's
|
|
354
354
|
fix (`warnings` in `--json`): pass it on to the owner. `login`, `setup` and `link` also run on their own.
|
|
355
|
+
When the owner names a team's observe workspace, pass `--workspace <name|acc_id>` to `setup` or `link`
|
|
356
|
+
once: its `acc_` id is kept in `.env` (`FLOW_FRAMEWORK_WORKSPACE`) for later runs, and `status` shows it.
|
|
355
357
|
|
|
356
358
|
In `src/main.ts`, put `fm.telegram()` from `@flow-engineer/flow/flow-messaging` in the agent's
|
|
357
359
|
channels and call `fm.start(agent, { cursorFile: ".flow/flow-messaging-cursor" })`: with no webhook
|
|
@@ -183,13 +183,18 @@ connects out to the cloud, which plays the person and the tools and judges.
|
|
|
183
183
|
`runId`, `scenario`, `channel`, `run`, `verdict`, `error` (the cloud's code when it could not play
|
|
184
184
|
or judge the run, such as `judge_failed`; the run is then `errored` and its line reads `ERROR`,
|
|
185
185
|
whatever it ended as), `ended`, `turns`, `watch` and `failedChecks` (`kind`, `text` and the judge's
|
|
186
|
-
`reason`)
|
|
186
|
+
`reason`), and `invalid` (`{ path, reason }` for a scenario the cloud could not play, its error
|
|
187
|
+
`scenario_invalid` and its line `INVALID`; else `null`); top-level `invalid` lists those scenarios
|
|
188
|
+
once each (`scenario`, `path`, `reason`: fix the file; `unsupported: ...` means the cloud does not
|
|
189
|
+
know that key), and `sessionInvalid` (`reason`, `olderCloud`, `sent`) is set when the cloud ended
|
|
190
|
+
the whole session `scenario_invalid`. Each session that opened also leaves
|
|
187
191
|
`.flow/last-session.json` in the agent's folder (its path is printed on every exit): `sessionId`,
|
|
188
192
|
`agent`, `dashboard`, `suiteHash`, `endedAt`, `ended` (`finished`, `stopped` or `interrupted`),
|
|
189
193
|
`only` (the scenarios it was limited to, else `null`) and per run `scenario` (file name without
|
|
190
194
|
`.yaml`), `channel`, `run`, `runId`, `verdict` (`passed`, `failed`, `errored`: the cloud could not
|
|
191
195
|
play or judge it, such as a failed tool simulator or person model, which is not a failure of your
|
|
192
|
-
agent, or `not_played`: the session ended first)
|
|
196
|
+
agent, or `not_played`: the session ended first), `failedChecks` (with reasons) and `invalid`
|
|
197
|
+
when the cloud could not play the scenario, plus `sessionInvalid` as in `--json`.
|
|
193
198
|
Read these instead of parsing the printed lines.
|
|
194
199
|
Exit status: 0 all passed; 1 a run failed, or nothing was sent (a wrong flag, a test file that does
|
|
195
200
|
not parse, no key); 2 the connection or the cloud failed, or runs errored and none failed; 3 the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare const ACCOUNT_COMMANDS: Set<string>;
|
|
2
|
-
export declare const ACCOUNT_LINES = " flow login [--wait | --no-wait] [--timeout <s>] [--force] [--no-browser] [--json]\n Sign in to flow.engineer: opens the browser, prints the link and code,\n and waits up to --timeout (default 120) for approval; --wait goes on\n waiting for the same code, --no-wait returns at once\n flow logout [--json] End this machine's sign-in and delete its token\n flow whoami [--json] Show the account you are signed in as\n flow setup [--replace] [--rotate <keys> [--now]] [--display-name <name>] [--ci] [--json]\n Write this agent's keys (its Flow key FLOW_KEY, which also pays for the\n model on the account's trial credit when there is no provider key, test\n cloud, traces, Flow Messaging) and each open channel's sender into\n ./.env (in an agent's folder: package.json); --rotate key,cloud,traces,\n messaging or all makes those keys again (the old FLOW_KEY works 24 h\n more; --now ends it at once)\n flow link [--channel telegram|whatsapp|imessage] [--display-name <name>] [--timeout <s>] [--no-browser] [--json]\n Print the link that opens a chat with this agent from a phone; signs in\n and runs setup first when this folder is not set up yet. --display-name\n (here and on setup) renames the agent: package.json's displayName and\n its prompt, agent/AGENTS.md; the phone link then says \"Hi <name>\"\n flow status [--json] What this folder has and lacks: FLOW_KEY and the model key (never their\n values) with the trial credit left, the\n sign-in, each channel's sender in .env, and, signed in, which channels\n are open now and their free-trial counts";
|
|
2
|
+
export declare const ACCOUNT_LINES = " flow login [--wait | --no-wait] [--timeout <s>] [--force] [--no-browser] [--json]\n Sign in to flow.engineer: opens the browser, prints the link and code,\n and waits up to --timeout (default 120) for approval; --wait goes on\n waiting for the same code, --no-wait returns at once\n flow logout [--json] End this machine's sign-in and delete its token\n flow whoami [--json] Show the account you are signed in as\n flow setup [--replace] [--rotate <keys> [--now]] [--workspace <name|acc_id>] [--display-name <name>] [--ci] [--json]\n Write this agent's keys (its Flow key FLOW_KEY, which also pays for the\n model on the account's trial credit when there is no provider key, test\n cloud, traces, Flow Messaging) and each open channel's sender into\n ./.env (in an agent's folder: package.json); --rotate key,cloud,traces,\n messaging or all makes those keys again (the old FLOW_KEY works 24 h\n more; --now ends it at once). --workspace (here and on link) puts the\n agent in that observe workspace of yours, kept in .env as\n FLOW_FRAMEWORK_WORKSPACE; a new one makes the cloud and traces keys again\n flow link [--channel telegram|whatsapp|imessage] [--workspace <name|acc_id>] [--display-name <name>] [--timeout <s>] [--no-browser] [--json]\n Print the link that opens a chat with this agent from a phone; signs in\n and runs setup first when this folder is not set up yet. --display-name\n (here and on setup) renames the agent: package.json's displayName and\n its prompt, agent/AGENTS.md; the phone link then says \"Hi <name>\"\n flow status [--json] What this folder has and lacks: FLOW_KEY and the model key (never their\n values) with the trial credit left, the workspace, the\n sign-in, each channel's sender in .env, and, signed in, which channels\n are open now and their free-trial counts";
|
|
3
3
|
/** Runs one account command; resolves to the exit status. */
|
|
4
4
|
export declare function accountCli(command: string, args: string[]): Promise<number>;
|
|
@@ -30,20 +30,22 @@ export const ACCOUNT_LINES = ` flow login [--wait | --no-wait] [--timeout <s>]
|
|
|
30
30
|
waiting for the same code, --no-wait returns at once
|
|
31
31
|
flow logout [--json] End this machine's sign-in and delete its token
|
|
32
32
|
flow whoami [--json] Show the account you are signed in as
|
|
33
|
-
flow setup [--replace] [--rotate <keys> [--now]] [--display-name <name>] [--ci] [--json]
|
|
33
|
+
flow setup [--replace] [--rotate <keys> [--now]] [--workspace <name|acc_id>] [--display-name <name>] [--ci] [--json]
|
|
34
34
|
Write this agent's keys (its Flow key FLOW_KEY, which also pays for the
|
|
35
35
|
model on the account's trial credit when there is no provider key, test
|
|
36
36
|
cloud, traces, Flow Messaging) and each open channel's sender into
|
|
37
37
|
./.env (in an agent's folder: package.json); --rotate key,cloud,traces,
|
|
38
38
|
messaging or all makes those keys again (the old FLOW_KEY works 24 h
|
|
39
|
-
more; --now ends it at once)
|
|
40
|
-
|
|
39
|
+
more; --now ends it at once). --workspace (here and on link) puts the
|
|
40
|
+
agent in that observe workspace of yours, kept in .env as
|
|
41
|
+
FLOW_FRAMEWORK_WORKSPACE; a new one makes the cloud and traces keys again
|
|
42
|
+
flow link [--channel telegram|whatsapp|imessage] [--workspace <name|acc_id>] [--display-name <name>] [--timeout <s>] [--no-browser] [--json]
|
|
41
43
|
Print the link that opens a chat with this agent from a phone; signs in
|
|
42
44
|
and runs setup first when this folder is not set up yet. --display-name
|
|
43
45
|
(here and on setup) renames the agent: package.json's displayName and
|
|
44
46
|
its prompt, agent/AGENTS.md; the phone link then says "Hi <name>"
|
|
45
47
|
flow status [--json] What this folder has and lacks: FLOW_KEY and the model key (never their
|
|
46
|
-
values) with the trial credit left, the
|
|
48
|
+
values) with the trial credit left, the workspace, the
|
|
47
49
|
sign-in, each channel's sender in .env, and, signed in, which channels
|
|
48
50
|
are open now and their free-trial counts`;
|
|
49
51
|
const USAGE = `Usage:\n${ACCOUNT_LINES}
|
|
@@ -100,7 +102,7 @@ export async function accountCli(command, args) {
|
|
|
100
102
|
: {}),
|
|
101
103
|
...(command === "setup" ? { replace: { type: "boolean", default: false }, rotate: { type: "string" }, now: { type: "boolean", default: false }, ci: { type: "boolean", default: false } } : {}),
|
|
102
104
|
...(command === "link" ? { channel: { type: "string", default: "telegram" } } : {}),
|
|
103
|
-
...(command === "link" || command === "setup" ? { "display-name": { type: "string" } } : {}),
|
|
105
|
+
...(command === "link" || command === "setup" ? { "display-name": { type: "string" }, workspace: { type: "string" } } : {}),
|
|
104
106
|
},
|
|
105
107
|
}));
|
|
106
108
|
}
|
|
@@ -131,6 +133,12 @@ export async function accountCli(command, args) {
|
|
|
131
133
|
console.error(`flow setup: --now goes with --rotate key (or all): it ends the old FLOW_KEY at once.\n\n${USAGE}`);
|
|
132
134
|
return EXIT.usage;
|
|
133
135
|
}
|
|
136
|
+
const workspace = typeof values.workspace === "string" ? values.workspace.trim() : undefined;
|
|
137
|
+
// flow.engineer takes a workspace's acc_ id or its name: 1 to 100 characters, no control characters.
|
|
138
|
+
if (workspace !== undefined && (!workspace || workspace.length > 100 || /[\u0000-\u001f\u007f]/.test(workspace))) {
|
|
139
|
+
console.error(`flow ${command}: --workspace takes a workspace's name or its acc_ id, 1 to 100 characters.\n\n${USAGE}`);
|
|
140
|
+
return EXIT.usage;
|
|
141
|
+
}
|
|
134
142
|
const json = values.json === true;
|
|
135
143
|
// Straight to the file descriptors, never buffered: a coding agent's cut-off command keeps every line.
|
|
136
144
|
const out = (report) => writeAll(1, `${JSON.stringify(report)}\n`);
|
|
@@ -162,9 +170,9 @@ export async function accountCli(command, args) {
|
|
|
162
170
|
else if (command === "status")
|
|
163
171
|
await status(json, out, say);
|
|
164
172
|
else if (command === "setup")
|
|
165
|
-
await setupEnv({ replace: values.replace === true, rotate, now: values.now === true, ci: values.ci === true }, json, out, say);
|
|
173
|
+
await setupEnv({ replace: values.replace === true, rotate, now: values.now === true, ci: values.ci === true, workspace }, json, out, say);
|
|
166
174
|
else
|
|
167
|
-
await link({ channel, noBrowser: values["no-browser"] === true, timeoutSeconds, renamed }, json, out, say);
|
|
175
|
+
await link({ channel, noBrowser: values["no-browser"] === true, timeoutSeconds, renamed, workspace }, json, out, say);
|
|
168
176
|
return EXIT.ok;
|
|
169
177
|
}
|
|
170
178
|
catch (error) {
|
|
@@ -499,6 +507,19 @@ const OPEN_HINT = {
|
|
|
499
507
|
whatsapp: "Open it and send the message it fills in.",
|
|
500
508
|
imessage: "Open it on an iPhone or a Mac and send the message it fills in.",
|
|
501
509
|
};
|
|
510
|
+
/** The observe workspace this folder's agent was put in by --workspace (its acc_ id); unset: the personal one. */
|
|
511
|
+
const WORKSPACE_ENV = "FLOW_FRAMEWORK_WORKSPACE";
|
|
512
|
+
/** The answer's workspace_id, under the name setupEnv gives it only when a workspace was asked for. */
|
|
513
|
+
const WORKSPACE_FIELD = "workspace_kept";
|
|
514
|
+
/** --workspace names another workspace than the one .env's cloud and traces keys are in (none named yet: the personal one). */
|
|
515
|
+
function movesWorkspace(workspace, value) {
|
|
516
|
+
if (workspace === undefined)
|
|
517
|
+
return false;
|
|
518
|
+
const stored = value(WORKSPACE_ENV);
|
|
519
|
+
if (stored)
|
|
520
|
+
return workspace.toLowerCase() !== stored.toLowerCase();
|
|
521
|
+
return Boolean(value(SETUP_ENV.cloud_key) || value(SETUP_ENV.traces_key));
|
|
522
|
+
}
|
|
502
523
|
async function setupEnv(options, json, out, say, forLink) {
|
|
503
524
|
if (options.ci) {
|
|
504
525
|
const steps = [
|
|
@@ -527,6 +548,14 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
527
548
|
const name = named.replace(/^@[^/]*\//, "").replace(/[^A-Za-z0-9 ._-]/g, "-").slice(0, 64) || "my-agent";
|
|
528
549
|
const all = options.rotate.includes("all");
|
|
529
550
|
const rotated = new Set(all ? Object.values(ROTATE).filter(Boolean) : options.rotate.map((k) => ROTATE[k]));
|
|
551
|
+
// The workspace asked for: --workspace, else the one an earlier --workspace kept in .env.
|
|
552
|
+
const workspace = options.workspace ?? (before.get(WORKSPACE_ENV) || undefined);
|
|
553
|
+
// Another workspace: the cloud and traces keys in .env are the old one's, so they are made again
|
|
554
|
+
// there (Messaging's key and FLOW_KEY are not bound to a workspace).
|
|
555
|
+
const moving = movesWorkspace(options.workspace, (n) => before.get(n));
|
|
556
|
+
if (moving)
|
|
557
|
+
for (const field of ["cloud_key", "traces_key"])
|
|
558
|
+
rotated.add(field);
|
|
530
559
|
const rotatedEnv = new Set([...rotated].map((field) => (field === "flow_key" ? FLOW_KEY : SETUP_ENV[field])));
|
|
531
560
|
// An empty value (`cp .env.example .env`) is missing: the server mints it. A model key counts in
|
|
532
561
|
// the shell too (npm start reads both): FLOW_KEY is made only when it is not named as had, and
|
|
@@ -542,19 +571,22 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
542
571
|
// FLOW_FRAMEWORK_TRY_URL is one channel's page: link's channel when it is open, else the server's (Telegram's).
|
|
543
572
|
const withTryUrl = (fields) => {
|
|
544
573
|
const code = fields.join_code;
|
|
574
|
+
// The workspace the answer put the agent in, kept only when one was asked for.
|
|
575
|
+
if (workspace !== undefined && typeof fields.workspace_id === "string" && fields.workspace_id)
|
|
576
|
+
fields = { ...fields, [WORKSPACE_FIELD]: fields.workspace_id };
|
|
545
577
|
if (!forLink || typeof fields[`${forLink.channel}_sender`] !== "string" || typeof code !== "string" || !code)
|
|
546
578
|
return fields;
|
|
547
579
|
const pages = fields.try_urls;
|
|
548
580
|
const page = typeof pages?.[forLink.channel] === "string" ? pages[forLink.channel] : tryLink(url, code, forLink.channel);
|
|
549
581
|
return { ...fields, try_url: page };
|
|
550
582
|
};
|
|
551
|
-
const chosenPage = (n) => forLink !== undefined && n === SETUP_ENV.try_url;
|
|
583
|
+
const chosenPage = (n) => (forLink !== undefined && n === SETUP_ENV.try_url) || n === WORKSPACE_ENV;
|
|
552
584
|
let answer;
|
|
553
585
|
// For link: the server's refusal when Messaging's part came (a join code) and only observe's failed.
|
|
554
586
|
let observeFailed;
|
|
555
587
|
try {
|
|
556
588
|
// Every channel: each comes back with its shared sender only while it is open.
|
|
557
|
-
answer = await setup(url, token, { name, hostname: hostname(), channels: [...CHANNELS], have, ...(rotate ? { rotate } : {}), ...(rotateNow ? { rotate_now: true } : {}) });
|
|
589
|
+
answer = await setup(url, token, { name, hostname: hostname(), channels: [...CHANNELS], have, ...(workspace !== undefined ? { workspace } : {}), ...(rotate ? { rotate } : {}), ...(rotateNow ? { rotate_now: true } : {}) });
|
|
558
590
|
}
|
|
559
591
|
catch (error) {
|
|
560
592
|
// Texting needs only Messaging's part, so link goes on without observe's (traces and the test
|
|
@@ -579,6 +611,12 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
579
611
|
const had = new Map(before);
|
|
580
612
|
// --replace takes every new value but a model key: only --rotate key replaces FLOW_KEY.
|
|
581
613
|
const written = writeEnv(file, lines, before, envEntries(withTryUrl(fields)), (n) => rotatedEnv.has(n) || (options.replace && !MODEL_KEY_NAMES.includes(n)) || chosenPage(n));
|
|
614
|
+
// An older flow.engineer ignores the field and sets the agent up in the personal workspace.
|
|
615
|
+
if (workspace !== undefined && !observeFailed && (typeof answer.workspace_id !== "string" || !answer.workspace_id)) {
|
|
616
|
+
const stop = new Stop("workspace_unsupported", "flow.engineer does not support --workspace yet; the agent was set up in your personal workspace.", undefined, EXIT.refused);
|
|
617
|
+
stop.warnings = warnings;
|
|
618
|
+
throw stop;
|
|
619
|
+
}
|
|
582
620
|
if (typeof answer.join_code !== "string" || !answer.join_code) {
|
|
583
621
|
const kept = written.filter((w) => w.action === "added" || w.action === "replaced").map((w) => w.name);
|
|
584
622
|
const stop = new Stop("no_join_code", `flow.engineer gave no join code${kept.length ? ` (the keys it made are in .env: ${kept.join(", ")})` : ""}, so there is no phone link yet.`, answer.next?.[0] ?? NEXT.setup, EXIT.refused);
|
|
@@ -586,7 +624,8 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
586
624
|
throw stop;
|
|
587
625
|
}
|
|
588
626
|
if (observeFailed) {
|
|
589
|
-
|
|
627
|
+
// Moving workspaces, the old workspace's keys still in .env do not count.
|
|
628
|
+
const missing = [SETUP_ENV.cloud_key, SETUP_ENV.traces_key].filter((n) => !before.get(n) || (moving && before.get(n) === had.get(n)));
|
|
590
629
|
const why = observeFailed.why.replace(/\.$/, "");
|
|
591
630
|
warnings.push(`Traces and tests won't work${missing.length ? ` until .env has ${missing.join(" and ")}` : ""}: ${why}.${observeFailed.fix ? ` Then: ${observeFailed.fix}` : ""}`);
|
|
592
631
|
}
|
|
@@ -621,6 +660,7 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
621
660
|
whatsapp_link: answer.whatsapp_link ?? null, imessage_link: answer.imessage_link ?? null, try_urls: links,
|
|
622
661
|
direct_links: Object.fromEntries(Object.entries(direct).map(([c, d]) => [c, d.link])), join_message: Object.values(direct)[0]?.message ?? null,
|
|
623
662
|
trial, model_key: model.report,
|
|
663
|
+
workspace: typeof answer.workspace_id === "string" && answer.workspace_id ? { id: answer.workspace_id, name: answer.workspace_name ?? null } : null,
|
|
624
664
|
// FLOW_KEY's state, never its value: made in this call, with its id, mode and scopes.
|
|
625
665
|
flow_key: { set: Boolean(before.get(FLOW_KEY) || process.env[FLOW_KEY]?.trim()), minted: flowKey?.minted === true,
|
|
626
666
|
...(flowKey?.minted ? { kid: flowKey.kid ?? null, mode: flowKey.mode ?? null, scopes: flowKey.scopes ?? [], previous_expires_at: flowKey.previous_expires_at ?? null } : {}) },
|
|
@@ -633,6 +673,8 @@ async function setupEnv(options, json, out, say, forLink) {
|
|
|
633
673
|
say(` ${w.name}=${w.value} ${w.action}${w.action === "kept" ? (w.name === FLOW_KEY ? " (--rotate key makes a new one)" : " (yours differs; --replace takes the new one)") : ""}`);
|
|
634
674
|
if (previous)
|
|
635
675
|
say(previous);
|
|
676
|
+
if (answer.workspace_id)
|
|
677
|
+
say(`Workspace: ${answer.workspace_name ? `${answer.workspace_name} (${answer.workspace_id})` : answer.workspace_id}`);
|
|
636
678
|
if (answer.dashboard_url)
|
|
637
679
|
say(`Its dashboard: ${answer.dashboard_url}`);
|
|
638
680
|
// Said by link instead, once, with its link.
|
|
@@ -669,6 +711,7 @@ function objectField(fields, name) {
|
|
|
669
711
|
*/
|
|
670
712
|
function envEntries(fields) {
|
|
671
713
|
const entries = Object.entries(SETUP_ENV).map(([field, envName]) => [envName, fields[field]]);
|
|
714
|
+
entries.push([WORKSPACE_ENV, fields[WORKSPACE_FIELD]]);
|
|
672
715
|
const key = objectField(fields, "flow_key");
|
|
673
716
|
if ((key?.env ?? FLOW_KEY) === FLOW_KEY && typeof key?.value === "string")
|
|
674
717
|
entries.push([FLOW_KEY, key.value]);
|
|
@@ -745,7 +788,8 @@ async function status(json, out, say) {
|
|
|
745
788
|
const named = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf8")).name ?? basename(process.cwd());
|
|
746
789
|
const name = named.replace(/^@[^/]*\//, "").replace(/[^A-Za-z0-9 ._-]/g, "-").slice(0, 64) || "my-agent";
|
|
747
790
|
const have = [...Object.values(SETUP_ENV).filter((n) => n !== SETUP_ENV.try_url), ...MODEL_KEY_NAMES];
|
|
748
|
-
|
|
791
|
+
const workspace = value(WORKSPACE_ENV);
|
|
792
|
+
server = await setup(url, current.token, { name, hostname: hostname(), channels: [...CHANNELS], have, ...(workspace ? { workspace } : {}) });
|
|
749
793
|
}
|
|
750
794
|
}
|
|
751
795
|
catch {
|
|
@@ -764,11 +808,13 @@ async function status(json, out, say) {
|
|
|
764
808
|
...(model.report.set && !model.next ? ["npm start"] : []),
|
|
765
809
|
...(!joinCode || !value(SETUP_ENV.messaging_key) ? [NEXT.link] : CHANNELS.filter((c) => channels[c].open && !channels[c].sender).map((c) => `${NEXT.link} --channel ${c}`)),
|
|
766
810
|
];
|
|
811
|
+
const workspace = value(WORKSPACE_ENV) || "personal";
|
|
767
812
|
if (json)
|
|
768
|
-
out({ model_key: model.report, flow_key: { set: Boolean(value(FLOW_KEY)) }, signed_in: account ? { email: account.email, account: account.account } : false, join_code: joinCode, messaging_key: Boolean(value(SETUP_ENV.messaging_key)), channels, next });
|
|
813
|
+
out({ model_key: model.report, flow_key: { set: Boolean(value(FLOW_KEY)) }, workspace, signed_in: account ? { email: account.email, account: account.account } : false, join_code: joinCode, messaging_key: Boolean(value(SETUP_ENV.messaging_key)), channels, next });
|
|
769
814
|
if (model.report.env && model.report.env !== FLOW_KEY)
|
|
770
815
|
say(value(FLOW_KEY) ? `${FLOW_KEY} is set.` : `${FLOW_KEY}: not set yet (${RUN} setup writes it).`);
|
|
771
816
|
say(model.line);
|
|
817
|
+
say(`Workspace: ${workspace}`);
|
|
772
818
|
say(account ? `Signed in as ${account.email}.` : current ? "Not signed in here (or flow.engineer could not be reached)." : "Not signed in.");
|
|
773
819
|
for (const c of CHANNELS) {
|
|
774
820
|
const { sender, open, trial } = channels[c];
|
|
@@ -828,7 +874,8 @@ async function link(options, json, out, say) {
|
|
|
828
874
|
let trial;
|
|
829
875
|
let had;
|
|
830
876
|
let warnings = [];
|
|
831
|
-
|
|
877
|
+
const newWorkspace = options.workspace !== undefined && (movesWorkspace(options.workspace, value) || !value(WORKSPACE_ENV));
|
|
878
|
+
if (newWorkspace || !value(SETUP_ENV.join_code) || !value(SETUP_ENV.messaging_key) || (needsSender && !value(SENDER_ENV[options.channel]))) {
|
|
832
879
|
agentFolder(process.cwd());
|
|
833
880
|
if (!(await signedInAccount(loginUrl()))) {
|
|
834
881
|
say("Not signed in to flow.engineer yet: signing in first.");
|
|
@@ -836,7 +883,7 @@ async function link(options, json, out, say) {
|
|
|
836
883
|
}
|
|
837
884
|
say("Setting up this agent's keys in .env.");
|
|
838
885
|
had = value;
|
|
839
|
-
({ answer: { model_credit: credit, trial }, warnings } = await setupEnv({ replace: false, rotate: [], ci: false }, json, out, say, { channel: options.channel }));
|
|
886
|
+
({ answer: { model_credit: credit, trial }, warnings } = await setupEnv({ replace: false, rotate: [], ci: false, workspace: options.workspace }, json, out, say, { channel: options.channel }));
|
|
840
887
|
value = dotenvValue();
|
|
841
888
|
}
|
|
842
889
|
const joinCode = value(SETUP_ENV.join_code);
|
package/dist/testing/src/cli.js
CHANGED
|
@@ -44,7 +44,7 @@ import { CHAT_LINE, chatCli } from "./chat.js";
|
|
|
44
44
|
import { httpAgent } from "./http-agent.js";
|
|
45
45
|
import { createInterface } from "node:readline/promises";
|
|
46
46
|
import { displayNameOf, folderName, initHere, newStandalone, packageRoot } from "./new.js";
|
|
47
|
-
import { EXIT, finishedOutcome, loadEnv, networkCode, reportError, runVerdict, stoppedOutcome } from "./report.js";
|
|
47
|
+
import { EXIT, finishedOutcome, invalidLines, invalidScenarios, loadEnv, networkCode, reportError, runVerdict, sessionInvalid, sessionInvalidLines, stoppedOutcome } from "./report.js";
|
|
48
48
|
import { LAST_SESSION, lastSessionIn, SHOW_LINE, showCli } from "./show.js";
|
|
49
49
|
import { loadSuite, parseScenario, parseToolFile, suiteHash } from "./suite.js";
|
|
50
50
|
const NEW_LINE = ` flow new <name> [--display-name <name>]
|
|
@@ -105,7 +105,7 @@ Exit status:
|
|
|
105
105
|
Results: once the session has opened, every exit writes .flow/last-session.json in the agent's
|
|
106
106
|
folder: the session id, the cloud's agent id, dashboard link, suite hash and time, and per run its
|
|
107
107
|
scenario, channel, number, run id, verdict (passed, failed, errored or not_played) and failed checks
|
|
108
|
-
with the judge's reasons. Never the messages. The folder is git-ignored and the suite synthetic.
|
|
108
|
+
with the judge's reasons (and, for a scenario the cloud could not play, its file and what is wrong). Never the messages. The folder is git-ignored and the suite synthetic.
|
|
109
109
|
|
|
110
110
|
Environment (also read from the agent folder's .env, or --dotenv; variables already set win;
|
|
111
111
|
the CHINWAG_ names from before the rename still work):
|
|
@@ -251,6 +251,8 @@ let sessionId = "";
|
|
|
251
251
|
let dashboard = "";
|
|
252
252
|
let agentId = "";
|
|
253
253
|
const seen = [];
|
|
254
|
+
/** Set when the cloud ended the whole session scenario_invalid. */
|
|
255
|
+
let endedInvalid = null;
|
|
254
256
|
if (command !== undefined && (command !== "test" || rest.length))
|
|
255
257
|
invalid(`flow: unknown command "${positionals.join(" ")}".\n\n${USAGE}`);
|
|
256
258
|
const envProblem = loadEnv(values.dotenv);
|
|
@@ -381,6 +383,10 @@ catch (error) {
|
|
|
381
383
|
end("transport", { received: error.runs, error: reportError(error, reason) });
|
|
382
384
|
}
|
|
383
385
|
console.error(reason);
|
|
386
|
+
endedInvalid = sessionInvalid(error, files.filter((f) => f.path.startsWith("scenarios/")).map((f) => ({ scenario: f.path.slice("scenarios/".length, -".yaml".length), path: f.path })));
|
|
387
|
+
if (endedInvalid)
|
|
388
|
+
for (const line of sessionInvalidLines(endedInvalid))
|
|
389
|
+
console.error(line);
|
|
384
390
|
// The cloud stopped a session that had opened: what finished is kept, the rest is not_played.
|
|
385
391
|
saveSession("stopped", seen, dashboard, console.error);
|
|
386
392
|
end(stoppedOutcome(error, sessionId !== ""), { received: seen, error: reportError(error, reason) });
|
|
@@ -402,6 +408,8 @@ for (const channel of [...new Set(summary.runs.map((r) => r.channel))]) {
|
|
|
402
408
|
const runs = summary.runs.filter((r) => r.channel === channel);
|
|
403
409
|
say(` ${channel}: ${runs.filter((r) => runVerdict(r) === "passed").length} of ${runs.length} passed`);
|
|
404
410
|
}
|
|
411
|
+
for (const line of invalidLines(invalidScenarios(mergedRuns(summary.runs).map((m) => ({ scenario: m.scenario, invalid: m.summary?.invalid })))))
|
|
412
|
+
say(line);
|
|
405
413
|
if (summary.lost)
|
|
406
414
|
say(`The cloud lost session ${summary.lost.sessionId} part way; a second session played its unfinished runs. The first: ${summary.lost.dashboard}`);
|
|
407
415
|
say(`Cost: ${usd(total((r) => r.agentUsage.costUsd))} agent (${tokens(total((r) => r.agentUsage.input))} in, ${tokens(total((r) => r.agentUsage.output))} out) + ${usd(total((r) => r.testUsage.costUsd))} test models.\n${summary.dashboard}`);
|
|
@@ -432,8 +440,11 @@ function mergedRuns(received) {
|
|
|
432
440
|
fileOf.set(scenario.id, id);
|
|
433
441
|
for (let run = 1; run <= perScenario; run++) {
|
|
434
442
|
for (const channel of scenario.channels) {
|
|
435
|
-
|
|
443
|
+
// A scenario the cloud could not read is named by its file, not its id.
|
|
444
|
+
const r = byRun.get(key(scenario.id, channel, run)) ?? byRun.get(key(id, channel, run));
|
|
436
445
|
byRun.delete(key(scenario.id, channel, run));
|
|
446
|
+
if (r)
|
|
447
|
+
byRun.delete(key(r.scenario, channel, run));
|
|
437
448
|
out.push(r ? merged(id, r) : { scenario: id, channel, run, verdict: "not_played" });
|
|
438
449
|
}
|
|
439
450
|
}
|
|
@@ -466,7 +477,9 @@ function saveSession(ended, received, link, print) {
|
|
|
466
477
|
runId: m.summary?.runId ?? null,
|
|
467
478
|
verdict: m.verdict,
|
|
468
479
|
failedChecks: failedChecks(m.summary),
|
|
480
|
+
...(m.summary?.invalid ? { invalid: m.summary.invalid } : {}),
|
|
469
481
|
})),
|
|
482
|
+
...(endedInvalid ? { sessionInvalid: endedInvalid } : {}),
|
|
470
483
|
};
|
|
471
484
|
const path = join(dir, LAST_SESSION);
|
|
472
485
|
try {
|
|
@@ -496,6 +509,7 @@ function end(outcome, options = {}) {
|
|
|
496
509
|
turns: m.summary?.turns ?? null,
|
|
497
510
|
watch: m.summary?.watch || null,
|
|
498
511
|
failedChecks: failedChecks(m.summary),
|
|
512
|
+
invalid: m.summary?.invalid ?? null,
|
|
499
513
|
})) : [];
|
|
500
514
|
const count = (verdict) => runs.filter((r) => r.verdict === verdict).length;
|
|
501
515
|
const cost = (pick) => received.reduce((n, r) => n + pick(r), 0);
|
|
@@ -511,6 +525,8 @@ function end(outcome, options = {}) {
|
|
|
511
525
|
counts: { runs: runs.length, passed: count("passed"), failed: count("failed"), errored: count("errored"), notPlayed: count("not_played") },
|
|
512
526
|
costUsd: { agent: cost((r) => r.agentUsage.costUsd), test: cost((r) => r.testUsage.costUsd) },
|
|
513
527
|
runs,
|
|
528
|
+
invalid: invalidScenarios(runs),
|
|
529
|
+
sessionInvalid: endedInvalid,
|
|
514
530
|
};
|
|
515
531
|
writeAll(`${JSON.stringify(report)}\n`);
|
|
516
532
|
}
|
|
@@ -49,6 +49,8 @@ export interface ConnectOptions {
|
|
|
49
49
|
* code (`capacity: ...` gives capacity), when it starts with one. */
|
|
50
50
|
export declare class CloudEnded extends Error {
|
|
51
51
|
readonly code?: string;
|
|
52
|
+
/** The cloud's error as it sent it. */
|
|
53
|
+
readonly error: string;
|
|
52
54
|
constructor(error: string);
|
|
53
55
|
}
|
|
54
56
|
/** The tunnel, the stream, or the connection failed after the session opened. `runs` are the ones already received. The CLI prints them and exits 2. */
|
|
@@ -15,9 +15,12 @@ import { businessRuns } from "./business.js";
|
|
|
15
15
|
* code (`capacity: ...` gives capacity), when it starts with one. */
|
|
16
16
|
export class CloudEnded extends Error {
|
|
17
17
|
code;
|
|
18
|
+
/** The cloud's error as it sent it. */
|
|
19
|
+
error;
|
|
18
20
|
constructor(error) {
|
|
19
21
|
super(`the cloud stopped the session: ${error}`);
|
|
20
22
|
this.name = "CloudEnded";
|
|
23
|
+
this.error = error;
|
|
21
24
|
this.code = /^([a-z_]+):/.exec(error)?.[1];
|
|
22
25
|
}
|
|
23
26
|
}
|
|
@@ -71,6 +71,9 @@ export interface FlowKey {
|
|
|
71
71
|
}
|
|
72
72
|
export interface SetupAnswer {
|
|
73
73
|
agent_id: string;
|
|
74
|
+
/** The observe workspace (acc_...) the agent and its cloud and traces keys are in; absent from an older server. */
|
|
75
|
+
workspace_id?: string;
|
|
76
|
+
workspace_name?: string;
|
|
74
77
|
/** Only the keys minted in this call come back (those the request did not say it has). */
|
|
75
78
|
cloud_key?: string;
|
|
76
79
|
traces_key?: string;
|
|
@@ -148,6 +151,7 @@ export declare function setup(url: string, token: string, request: {
|
|
|
148
151
|
hostname: string;
|
|
149
152
|
channels: string[];
|
|
150
153
|
have: string[];
|
|
154
|
+
workspace?: string;
|
|
151
155
|
rotate?: true | string[];
|
|
152
156
|
rotate_now?: true;
|
|
153
157
|
}): Promise<SetupAnswer>;
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// authorization_pending, slow_down, expired_token, access_denied, invalid_grant
|
|
7
7
|
// POST /cli/revoke (bearer) {} ends this token
|
|
8
8
|
// GET /cli/whoami (bearer) -> {account, email, token}
|
|
9
|
-
// POST /cli/setup (bearer) {name, hostname, channels, have, rotate?, rotate_now?}
|
|
10
|
-
// -> {agent_id, cloud_key?, traces_key?, messaging_key?, flow_key?, model_credit?, join_code, account, app_id,
|
|
9
|
+
// POST /cli/setup (bearer) {name, hostname, channels, have, workspace?, rotate?, rotate_now?}
|
|
10
|
+
// -> {agent_id, workspace_id?, workspace_name?, cloud_key?, traces_key?, messaging_key?, flow_key?, model_credit?, join_code, account, app_id,
|
|
11
11
|
// dashboard_url, try_url, <channel>_sender?, <channel>_link? (telegram, whatsapp, imessage: each only while
|
|
12
12
|
// that shared sender is open), try_urls?, trial?, minted, next}; a 502 carries every field that worked too;
|
|
13
13
|
// flow_key is the agent's one Flow key, {env: "FLOW_KEY", value: "flow_test_...", kid, mode, scopes, agent_id,
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
// the reason in warnings; every folder's setup makes its own key, the credit is the account's;
|
|
19
19
|
// rotate is true (every key) or the answer fields to make again, such as ["flow_key"]; rotate_now: true
|
|
20
20
|
// ends the old FLOW_KEY at once instead of 24 h later (previous_expires_at);
|
|
21
|
+
// workspace is an observe workspace's acc_ id or name (case-insensitive), 1 to 100 characters; absent: the
|
|
22
|
+
// personal one; workspace_id and workspace_name are the workspace the agent and its cloud and traces keys are in
|
|
23
|
+
// (whenever observe answered; an older server sends neither); 403 not_a_member, 409 workspace_ambiguous (pass the id);
|
|
21
24
|
// 429 slow_down with Retry-After; 403 for a trial limit (sandbox_allowance_used, ...), its next prose
|
|
22
25
|
// Any answer may carry warnings: up to 20 strings to show the owner as they are.
|
|
23
26
|
// GET <model URL>/credit (bearer FLOW_KEY) -> {limit_usd, spent_usd, left_usd, default_model, models};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runVerdict } from "./report.js";
|
|
1
|
+
import { runVerdict, unsupported } from "./report.js";
|
|
2
2
|
const usd = (n) => `$${n.toFixed(4)}`;
|
|
3
3
|
/** Prints each run once. A run already shown as it finished is not printed again with the summary. */
|
|
4
4
|
export function runPrinter(log = console.log) {
|
|
@@ -8,6 +8,11 @@ export function runPrinter(log = console.log) {
|
|
|
8
8
|
return;
|
|
9
9
|
printed.add(r.runId);
|
|
10
10
|
const verdict = runVerdict(r);
|
|
11
|
+
// A scenario the cloud could not play: its file and what is wrong in it, not a run of the agent.
|
|
12
|
+
if (r.invalid) {
|
|
13
|
+
log(`INVALID ${r.scenario} #${r.run} (${r.channel}) ${r.runId}: the cloud could not play this scenario: ${r.invalid.path}: ${r.invalid.reason}${unsupported(r.invalid.reason)}`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
11
16
|
const gaveUp = verdict === "passed" && r.ended === "gave_up" ? " (passed, the person gave up)" : "";
|
|
12
17
|
const retried = r.retried ? "; tried once more" : "";
|
|
13
18
|
// An errored run: the cloud could not play or judge it, so its checks say nothing about the agent.
|
|
@@ -246,6 +246,12 @@ export interface RunSummary {
|
|
|
246
246
|
/** The cloud's code when it could not play or judge the run (judge_failed, spend_cap, simulator_failed, ...).
|
|
247
247
|
* The run then says nothing about the agent, whatever `passed` and `ended` say. */
|
|
248
248
|
error?: string;
|
|
249
|
+
/** With error scenario_invalid: the scenario file the cloud could not play, and what is wrong in it
|
|
250
|
+
* (`unsupported: ...` when the cloud does not know a key). */
|
|
251
|
+
invalid?: {
|
|
252
|
+
path: string;
|
|
253
|
+
reason: string;
|
|
254
|
+
};
|
|
249
255
|
}
|
|
250
256
|
export interface SessionSummary {
|
|
251
257
|
sessionId: string;
|
|
@@ -26,6 +26,27 @@ export interface ReportRun {
|
|
|
26
26
|
text: string;
|
|
27
27
|
reason: string;
|
|
28
28
|
}>;
|
|
29
|
+
/** The scenario file the cloud could not play and what is wrong in it (error scenario_invalid); else null. */
|
|
30
|
+
invalid: {
|
|
31
|
+
path: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
} | null;
|
|
34
|
+
}
|
|
35
|
+
/** A scenario the cloud could not play, once per scenario. */
|
|
36
|
+
export interface InvalidScenario {
|
|
37
|
+
scenario: string;
|
|
38
|
+
path: string;
|
|
39
|
+
reason: string;
|
|
40
|
+
}
|
|
41
|
+
/** The cloud ended the whole session scenario_invalid: what it said after the code (null when nothing),
|
|
42
|
+
* whether that looks like a cloud older than this client, and the scenario files this session sent. */
|
|
43
|
+
export interface SessionInvalid {
|
|
44
|
+
reason: string | null;
|
|
45
|
+
olderCloud: boolean;
|
|
46
|
+
sent: Array<{
|
|
47
|
+
scenario: string;
|
|
48
|
+
path: string;
|
|
49
|
+
}>;
|
|
29
50
|
}
|
|
30
51
|
/** `flow test --json`: one document on stdout, whatever the exit. */
|
|
31
52
|
export interface JsonReport {
|
|
@@ -50,6 +71,10 @@ export interface JsonReport {
|
|
|
50
71
|
test: number;
|
|
51
72
|
};
|
|
52
73
|
runs: ReportRun[];
|
|
74
|
+
/** Each scenario the cloud could not play; the other scenarios played. */
|
|
75
|
+
invalid: InvalidScenario[];
|
|
76
|
+
/** Set when the cloud ended the whole session scenario_invalid; else null. */
|
|
77
|
+
sessionInvalid: SessionInvalid | null;
|
|
53
78
|
}
|
|
54
79
|
/** A run's verdict: errored when the cloud names an error for it or ended it run_failed, since the cloud
|
|
55
80
|
* could not play or judge it and that says nothing about the agent (a judge_failed run can end goal_met). */
|
|
@@ -65,6 +90,27 @@ export declare function finishedOutcome(runs: Array<{
|
|
|
65
90
|
}>): Outcome;
|
|
66
91
|
/** What stopped a session, or the open, as an outcome. `opened`: the session had opened. */
|
|
67
92
|
export declare function stoppedOutcome(error: unknown, opened: boolean): Outcome;
|
|
93
|
+
/** The scenarios the cloud could not play, once each, from the runs it reported (a run per channel and --runs). */
|
|
94
|
+
export declare function invalidScenarios(runs: Array<{
|
|
95
|
+
scenario: string;
|
|
96
|
+
invalid?: {
|
|
97
|
+
path: string;
|
|
98
|
+
reason: string;
|
|
99
|
+
} | null;
|
|
100
|
+
}>): InvalidScenario[];
|
|
101
|
+
/** The lines that name each scenario the cloud could not play. */
|
|
102
|
+
export declare function invalidLines(invalid: InvalidScenario[]): string[];
|
|
103
|
+
/** A reason naming a key the cloud does not know: the client may be newer than the cloud. */
|
|
104
|
+
export declare function unsupported(reason: string): string;
|
|
105
|
+
/** A session the cloud ended scenario_invalid; null for any other stop. A tools/ or checks/ file with
|
|
106
|
+
* its reason is the suite's own problem; anything else (no reason, or a scenario, which a current
|
|
107
|
+
* cloud reports as that scenario's run) suggests a cloud older than this client. */
|
|
108
|
+
export declare function sessionInvalid(error: unknown, sent: Array<{
|
|
109
|
+
scenario: string;
|
|
110
|
+
path: string;
|
|
111
|
+
}>): SessionInvalid | null;
|
|
112
|
+
/** The lines that say why the session ended scenario_invalid. */
|
|
113
|
+
export declare function sessionInvalidLines(info: SessionInvalid): string[];
|
|
68
114
|
/** The error as the JSON report says it: the cloud's status, code and Retry-After when it answered. */
|
|
69
115
|
export declare function reportError(error: unknown, message: string): ReportError;
|
|
70
116
|
/** The connection's error code, from the error or what caused it. */
|
|
@@ -38,6 +38,48 @@ export function stoppedOutcome(error, opened) {
|
|
|
38
38
|
// Something on this machine: the agent would not build before the open, or broke the session after it.
|
|
39
39
|
return opened ? "failed" : "invalid";
|
|
40
40
|
}
|
|
41
|
+
/** The scenarios the cloud could not play, once each, from the runs it reported (a run per channel and --runs). */
|
|
42
|
+
export function invalidScenarios(runs) {
|
|
43
|
+
const out = new Map();
|
|
44
|
+
for (const r of runs)
|
|
45
|
+
if (r.invalid)
|
|
46
|
+
out.set(`${r.scenario}\n${r.invalid.path}`, { scenario: r.scenario, path: r.invalid.path, reason: r.invalid.reason });
|
|
47
|
+
return [...out.values()];
|
|
48
|
+
}
|
|
49
|
+
/** The lines that name each scenario the cloud could not play. */
|
|
50
|
+
export function invalidLines(invalid) {
|
|
51
|
+
if (!invalid.length)
|
|
52
|
+
return [];
|
|
53
|
+
const lines = [`${invalid.length} ${invalid.length === 1 ? "scenario" : "scenarios"} could not be played (fix ${invalid.length === 1 ? "it" : "them"} and run again):`];
|
|
54
|
+
for (const i of invalid)
|
|
55
|
+
lines.push(` ${i.scenario}: ${i.path}: ${i.reason}${unsupported(i.reason)}`);
|
|
56
|
+
return lines;
|
|
57
|
+
}
|
|
58
|
+
/** A reason naming a key the cloud does not know: the client may be newer than the cloud. */
|
|
59
|
+
export function unsupported(reason) {
|
|
60
|
+
return reason.startsWith("unsupported: ") ? " (the test cloud does not know this key; it may be older than this client)" : "";
|
|
61
|
+
}
|
|
62
|
+
/** A session the cloud ended scenario_invalid; null for any other stop. A tools/ or checks/ file with
|
|
63
|
+
* its reason is the suite's own problem; anything else (no reason, or a scenario, which a current
|
|
64
|
+
* cloud reports as that scenario's run) suggests a cloud older than this client. */
|
|
65
|
+
export function sessionInvalid(error, sent) {
|
|
66
|
+
if (!(error instanceof CloudEnded))
|
|
67
|
+
return null;
|
|
68
|
+
const match = /^scenario_invalid(?::\s*(.*))?$/s.exec(error.error.trim());
|
|
69
|
+
if (!match)
|
|
70
|
+
return null;
|
|
71
|
+
const reason = match[1]?.trim() || null;
|
|
72
|
+
const suiteFile = reason !== null && /^(?:\S*\/)?(?:tools|checks)\/[^:]+:\s*\S/.test(reason);
|
|
73
|
+
return { reason, olderCloud: !suiteFile, sent };
|
|
74
|
+
}
|
|
75
|
+
/** The lines that say why the session ended scenario_invalid. */
|
|
76
|
+
export function sessionInvalidLines(info) {
|
|
77
|
+
const lines = [`The test cloud could not play this suite${info.reason ? `: ${info.reason}` : " and gave no reason."}`];
|
|
78
|
+
if (info.olderCloud)
|
|
79
|
+
lines.push("the test cloud may be older than this client");
|
|
80
|
+
lines.push(`Scenarios sent: ${info.sent.map((s) => `${s.scenario} (${s.path})`).join(", ") || "none"}`);
|
|
81
|
+
return lines;
|
|
82
|
+
}
|
|
41
83
|
/** The error as the JSON report says it: the cloud's status, code and Retry-After when it answered. */
|
|
42
84
|
export function reportError(error, message) {
|
|
43
85
|
const refusal = postErrorOf(error);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SessionInvalid } from "./report.ts";
|
|
1
2
|
/** Where every session's results go, in the agent's folder; `flow new` git-ignores .flow/. */
|
|
2
3
|
export declare const LAST_SESSION: string;
|
|
3
4
|
/** The last session's file in dir: LAST_SESSION, else, before a run has written it, the one from before the rename (.chinwag/). */
|
|
@@ -29,7 +30,15 @@ export interface LastSession {
|
|
|
29
30
|
text: string;
|
|
30
31
|
reason?: string;
|
|
31
32
|
}>;
|
|
33
|
+
/** The scenario file the cloud could not play and what is wrong in it (error scenario_invalid). */
|
|
34
|
+
invalid?: {
|
|
35
|
+
path: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
};
|
|
32
38
|
}>;
|
|
39
|
+
/** The cloud ended the whole session scenario_invalid: its reason, whether the cloud looks older
|
|
40
|
+
* than the client, and the scenario files sent. */
|
|
41
|
+
sessionInvalid?: SessionInvalid;
|
|
33
42
|
}
|
|
34
43
|
export declare const SHOW_LINE = " flow show <run-id|last> [--json]\n Print one run's record from the test cloud: its checks with the\n judge's reasons, the transcript and the tool calls (flow show --help)";
|
|
35
44
|
/** `flow show`: returns the exit status. */
|
package/docs/agents.md
CHANGED
|
@@ -63,9 +63,12 @@ before you hand it back; run them yourself, and do not offer them to the owner a
|
|
|
63
63
|
and `init`, `setup`, `link` and `status` say so too, with the way to get one first in `next`.
|
|
64
64
|
Each folder's `setup` makes its own `FLOW_KEY` (the credit is the account's);
|
|
65
65
|
`setup --rotate key` replaces a leaked one (the old one works 24 h more; `--now` ends it at once).
|
|
66
|
+
- `setup --workspace <name|acc_id>` (and `link --workspace`) puts the agent, its cloud and traces keys in
|
|
67
|
+
that observe workspace of yours and keeps its `acc_` id in `.env` as `FLOW_FRAMEWORK_WORKSPACE`, which later
|
|
68
|
+
runs send; another workspace makes those two keys again (403 `not_a_member`, 409 `workspace_ambiguous`: pass the id).
|
|
66
69
|
- `status` reports `FLOW_KEY` and the model key (never their values; `flow_key.set` in `--json`; with
|
|
67
70
|
`FLOW_KEY` the model's key, the credit left from Flow, `model_key.flow_credit`; a key Flow does not
|
|
68
|
-
know gets `setup --rotate key`), the sign-in, each channel's sender in `.env`
|
|
71
|
+
know gets `setup --rotate key`), the workspace (`FLOW_FRAMEWORK_WORKSPACE`, else `personal`), the sign-in, each channel's sender in `.env`
|
|
69
72
|
and, signed in, which channels are open now with their trial counts. It writes nothing.
|
|
70
73
|
- `chat` builds the agent from `src/agent.ts` and writes as its first test person. Each reply is
|
|
71
74
|
printed (`agent: ...`) once the turn is over; `--json` gives one JSON line per thing it sends.
|