@agentswf/engine 0.1.0-beta.3 → 0.1.0-beta.4
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 +571 -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-output.ts +1 -1
- package/src/control-plane.ts +1 -1
- package/src/emit-command.ts +2 -13
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +13 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +13 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +44 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/here.ts +1 -1
- package/src/holder-command.ts +2 -20
- package/src/inbox-command.ts +2 -14
- package/src/init-command.ts +2 -11
- package/src/init-template/AGENTS.md.template +3 -3
- package/src/init-template/hello/workflow.ts.template +4 -4
- package/src/logs-command.ts +2 -15
- package/src/mailbox.ts +15 -2
- package/src/messaging/limits.ts +1 -1
- package/src/messaging/messaging.ts +22 -22
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +17 -17
- package/src/operation-liveness.ts +1 -1
- package/src/operator-runtime.ts +4 -7
- package/src/result-validation.ts +1 -1
- package/src/run-command.ts +2 -43
- package/src/run-usage.ts +1 -1
- package/src/runs-command.ts +2 -13
- package/src/sandboxes.ts +3 -2
- package/src/simple-input.ts +13 -3
- package/src/stage-ledger.ts +1 -1
- package/src/test-command.ts +2 -11
- package/src/testing.ts +1 -1
- package/src/tick-command.ts +2 -13
- package/src/tools.ts +16 -7
- package/src/workflow-runner.ts +85 -38
- package/src/workflow-testing/coordinator.typecheck.ts +12 -13
- package/src/workflow-testing/host.ts +8 -8
- package/src/workflow-testing/index.ts +1 -2
- package/src/workflow-testing/script.ts +4 -4
- package/src/workflow-testing/workflow-testing.typecheck.ts +2 -2
package/src/testing.ts
CHANGED
|
@@ -104,7 +104,7 @@ export function future(milliseconds = 60_000): { unixMilliseconds: number } {
|
|
|
104
104
|
return { unixMilliseconds: Date.now() + milliseconds };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/** Answers a call the way an agent's `
|
|
107
|
+
/** Answers a call the way an agent's `wfa` does: one framed request over its own socket. */
|
|
108
108
|
export async function submit(
|
|
109
109
|
binding: { endpoint: string; operationId: string },
|
|
110
110
|
value: unknown,
|
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
|
@@ -14,7 +14,14 @@ import { canonical } from "./canonical-json";
|
|
|
14
14
|
import { scheduleAt } from "./deadlines";
|
|
15
15
|
import { messageOf } from "./errors";
|
|
16
16
|
import type { RunEventBody } from "./run-event-log";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
checkGiven,
|
|
19
|
+
QUOTING_RULE,
|
|
20
|
+
readGiven,
|
|
21
|
+
simpleInput,
|
|
22
|
+
takesText,
|
|
23
|
+
usageLine,
|
|
24
|
+
} from "./simple-input";
|
|
18
25
|
|
|
19
26
|
/** How long a handler that ignores its aborted signal is waited for before its call is settled. */
|
|
20
27
|
export const TOOL_SETTLE_MS = 5_000;
|
|
@@ -195,7 +202,7 @@ export class ToolCalls<Scope> {
|
|
|
195
202
|
if (operation) operation.deadline = () => deadline;
|
|
196
203
|
}
|
|
197
204
|
|
|
198
|
-
/** The call this agent has running, which holds back its `
|
|
205
|
+
/** The call this agent has running, which holds back its `wfa result` and `wfa waiting`. */
|
|
199
206
|
running(agent: AgentKey): { tool: string; settledBy?: number } | undefined {
|
|
200
207
|
const running = this.#current.get(agent)?.running;
|
|
201
208
|
return running && { tool: running.tool, ...optional("settledBy", running.settledBy) };
|
|
@@ -204,7 +211,7 @@ export class ToolCalls<Scope> {
|
|
|
204
211
|
// Calls
|
|
205
212
|
|
|
206
213
|
/**
|
|
207
|
-
* `
|
|
214
|
+
* `wfa tool`'s request from `agent`'s socket. `lifetime` sets how long its connection may stay
|
|
208
215
|
* open, from now: undefined for as long as the call takes.
|
|
209
216
|
*/
|
|
210
217
|
async call(
|
|
@@ -519,8 +526,8 @@ function readInput(
|
|
|
519
526
|
|
|
520
527
|
/**
|
|
521
528
|
* 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.
|
|
529
|
+
* and its usage line, or the schema for an input that isn't simple. The quoting rule comes with a
|
|
530
|
+
* free-text flag unless the result block above already said it; none for an agent without tools.
|
|
524
531
|
*/
|
|
525
532
|
export function toolsLesson(
|
|
526
533
|
tools: readonly { name: string; description: string; input?: JsonSchema }[],
|
|
@@ -533,9 +540,11 @@ export function toolsLesson(
|
|
|
533
540
|
return { ...each, simple };
|
|
534
541
|
});
|
|
535
542
|
const flags = taught.some((each) => each.simple && each.simple.length > 0);
|
|
543
|
+
const text = taught.some((each) => each.simple && takesText(each.simple));
|
|
536
544
|
const json = taught.some((each) => !each.simple);
|
|
545
|
+
const value = text ? "'<value>'" : "<value>";
|
|
537
546
|
const forms = [
|
|
538
|
-
flags ? `${launcher} tool <name> --<option>
|
|
547
|
+
flags ? `${launcher} tool <name> --<option> ${value} ...` : `${launcher} tool <name>`,
|
|
539
548
|
...(json ? [`${launcher} tool <name> <<'WF_JSON'`, "<json>", "WF_JSON"] : []),
|
|
540
549
|
];
|
|
541
550
|
const entries = taught.map(({ name, description, input, simple }) => {
|
|
@@ -549,7 +558,7 @@ export function toolsLesson(
|
|
|
549
558
|
"You have tools the workflow runs for you. Call one with:",
|
|
550
559
|
"",
|
|
551
560
|
...forms,
|
|
552
|
-
...(
|
|
561
|
+
...(text && !quotingSaid ? ["", QUOTING_RULE] : []),
|
|
553
562
|
"",
|
|
554
563
|
...entries,
|
|
555
564
|
].join("\n");
|
package/src/workflow-runner.ts
CHANGED
|
@@ -189,7 +189,7 @@ import {
|
|
|
189
189
|
type RunSandboxOptions,
|
|
190
190
|
type SeatedAgent,
|
|
191
191
|
} from "./sandboxes";
|
|
192
|
-
import { QUOTING_RULE, simpleInput, usageLine } from "./simple-input";
|
|
192
|
+
import { QUOTING_RULE, simpleInput, takesText, usageLine } from "./simple-input";
|
|
193
193
|
import { placeSkills, RunSkills, readSkillSources } from "./skills/run-skills";
|
|
194
194
|
import { asSource, branchOf, branchValue, source, takeOf } from "./sources";
|
|
195
195
|
import { StageLedger, type StageProgress } from "./stage-ledger";
|
|
@@ -204,7 +204,7 @@ export type RunWorkflowOptions = {
|
|
|
204
204
|
livenessPolicy?: LivenessPolicy;
|
|
205
205
|
/** A `keepAlive` window that gives no `timeoutMs`; absent, an hour. A test's is short. */
|
|
206
206
|
defaultWindowMs?: number;
|
|
207
|
-
/** The run's time, which its timers and `
|
|
207
|
+
/** The run's time, which its timers and `wf.now()` read; absent, the clock's. A test's is virtual. */
|
|
208
208
|
now?: () => number;
|
|
209
209
|
runRoot: string;
|
|
210
210
|
/** The run this is an attempt of, whose folder the operator claimed under `runRoot`. */
|
|
@@ -253,7 +253,7 @@ export type RunWorkflowOptions = {
|
|
|
253
253
|
skillCache?: string;
|
|
254
254
|
/** The decision models workflows may ask; without them, `decisions.decide` is refused. */
|
|
255
255
|
decisions?: DecisionInstallation;
|
|
256
|
-
/** How `
|
|
256
|
+
/** How `wf.allowance` reads a harness's plan; without it, every harness reads `none`. */
|
|
257
257
|
allowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
258
258
|
/**
|
|
259
259
|
* Where an agent may `--await-reply`: the engine's list by harness and placement unless given. A
|
|
@@ -267,7 +267,7 @@ export type RunWorkflowOptions = {
|
|
|
267
267
|
*/
|
|
268
268
|
coordinators?: readonly CoordinatorPort[];
|
|
269
269
|
/**
|
|
270
|
-
* Sends `
|
|
270
|
+
* Sends `wf.emit`'s event through the one way in; absent, `emitEvent` on this machine. A
|
|
271
271
|
* test records them instead.
|
|
272
272
|
*/
|
|
273
273
|
emit?: Emitter;
|
|
@@ -891,7 +891,7 @@ class WorkflowOwner {
|
|
|
891
891
|
readonly #inFlight = new Set<Promise<unknown>>();
|
|
892
892
|
#closed = false;
|
|
893
893
|
#closing: Promise<unknown[]> | undefined;
|
|
894
|
-
/** The bundled `
|
|
894
|
+
/** The bundled `wfa`'s source, built once, at the first sandboxed agent. */
|
|
895
895
|
#bundle: Promise<string> | undefined;
|
|
896
896
|
/** The calling session, once the workflow has asked for it (ADR 0010). */
|
|
897
897
|
#caller: { key: string; state: Promise<LogicalAgent> } | undefined;
|
|
@@ -957,7 +957,7 @@ class WorkflowOwner {
|
|
|
957
957
|
if (!agent || agent.hasStartedTurn) return false;
|
|
958
958
|
return options.awaitReply(agent.execution, agent.confirmsDelivery);
|
|
959
959
|
},
|
|
960
|
-
launcher: (key) => this.#logical.get(key)?.launcher ?? "
|
|
960
|
+
launcher: (key) => this.#logical.get(key)?.launcher ?? "wfa",
|
|
961
961
|
changed: (key) => {
|
|
962
962
|
this.#messagesChanged();
|
|
963
963
|
this.#logical.get(key)?.messagesChanged();
|
|
@@ -1142,7 +1142,7 @@ class WorkflowOwner {
|
|
|
1142
1142
|
},
|
|
1143
1143
|
log: (message, fields) => options.log(message, fields),
|
|
1144
1144
|
now: () => {
|
|
1145
|
-
this.#notReading("
|
|
1145
|
+
this.#notReading("wf.now()");
|
|
1146
1146
|
return new Date(options.now());
|
|
1147
1147
|
},
|
|
1148
1148
|
sleep: async (name, when) => {
|
|
@@ -1168,10 +1168,10 @@ class WorkflowOwner {
|
|
|
1168
1168
|
timers: () => source({}, { name: "timer:due" }),
|
|
1169
1169
|
at: (read: () => Date | null) => timeRead(read),
|
|
1170
1170
|
after: (duration: Duration) => {
|
|
1171
|
-
this.#notReading("
|
|
1171
|
+
this.#notReading("wf.after()");
|
|
1172
1172
|
if (typeof duration !== "string" || !/^\d+(?:\.\d+)?[dhm]$/.test(duration))
|
|
1173
1173
|
throw new Error(
|
|
1174
|
-
`
|
|
1174
|
+
`wf.after takes days, hours or minutes, "1d", "3h", "1.5h"; not ${String(duration)}`,
|
|
1175
1175
|
);
|
|
1176
1176
|
return new Date(parseWhen(duration, options.now()).due);
|
|
1177
1177
|
},
|
|
@@ -1189,7 +1189,7 @@ class WorkflowOwner {
|
|
|
1189
1189
|
#notReading(what: string): void {
|
|
1190
1190
|
if (this.#reading)
|
|
1191
1191
|
throw new Error(
|
|
1192
|
-
`${what} in
|
|
1192
|
+
`${what} in wf.at's read: a time made at the read moves with every wait and never comes due; keep the time in the run's state where it is set`,
|
|
1193
1193
|
);
|
|
1194
1194
|
}
|
|
1195
1195
|
|
|
@@ -1205,7 +1205,7 @@ class WorkflowOwner {
|
|
|
1205
1205
|
if (value === null) return undefined;
|
|
1206
1206
|
if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
|
|
1207
1207
|
throw new Error(
|
|
1208
|
-
`select's ${branch}:
|
|
1208
|
+
`select's ${branch}: wf.at reads a valid Date or null from the run's state, not ${
|
|
1209
1209
|
value instanceof Date ? "an invalid Date" : (JSON.stringify(value) ?? String(value))
|
|
1210
1210
|
}`,
|
|
1211
1211
|
);
|
|
@@ -2988,6 +2988,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2988
2988
|
.then(
|
|
2989
2989
|
({ outcome }) => outcome,
|
|
2990
2990
|
(error: unknown): TurnOutcome<JsonValue> => ({
|
|
2991
|
+
answered: false,
|
|
2991
2992
|
kind: "failed",
|
|
2992
2993
|
reason: messageOf(error),
|
|
2993
2994
|
retryable: false,
|
|
@@ -3423,7 +3424,12 @@ class LogicalAgent implements AgentRef {
|
|
|
3423
3424
|
const usage = () =>
|
|
3424
3425
|
entry.settle({ settledAt: Math.min(Date.now(), deadline.unixMilliseconds) }, []);
|
|
3425
3426
|
if (Date.now() >= deadline.unixMilliseconds) {
|
|
3426
|
-
return {
|
|
3427
|
+
return {
|
|
3428
|
+
answered: false,
|
|
3429
|
+
kind: "timed-out",
|
|
3430
|
+
reason: "settings deadline exceeded",
|
|
3431
|
+
usage: usage(),
|
|
3432
|
+
};
|
|
3427
3433
|
}
|
|
3428
3434
|
const next: AgentExecution = { ...this.#execution, ...change };
|
|
3429
3435
|
const pane = placementOf(next) === "pane";
|
|
@@ -3451,15 +3457,16 @@ class LogicalAgent implements AgentRef {
|
|
|
3451
3457
|
cancelled !== undefined ? "cancelled" : expired ? "timed-out" : "failed",
|
|
3452
3458
|
);
|
|
3453
3459
|
}
|
|
3454
|
-
if (cancelled !== undefined)
|
|
3460
|
+
if (cancelled !== undefined)
|
|
3461
|
+
return { answered: false, kind: "cancelled", reason, usage: usage() };
|
|
3455
3462
|
return expired
|
|
3456
|
-
? { kind: "timed-out", reason, usage: usage() }
|
|
3457
|
-
: { kind: "failed", reason, retryable: false, usage: usage() };
|
|
3463
|
+
? { answered: false, kind: "timed-out", reason, usage: usage() }
|
|
3464
|
+
: { answered: false, kind: "failed", reason, retryable: false, usage: usage() };
|
|
3458
3465
|
} finally {
|
|
3459
3466
|
removeCanceller?.();
|
|
3460
3467
|
}
|
|
3461
3468
|
this.#execution = next;
|
|
3462
|
-
return { kind: "answered", value: null, usage: usage() };
|
|
3469
|
+
return { answered: true, kind: "answered", value: null, usage: usage() };
|
|
3463
3470
|
}
|
|
3464
3471
|
|
|
3465
3472
|
/**
|
|
@@ -3607,7 +3614,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3607
3614
|
const recoveryPrompt =
|
|
3608
3615
|
nudge?.prompt ??
|
|
3609
3616
|
(waitingSupported
|
|
3610
|
-
? "Report the result now, or use
|
|
3617
|
+
? "Report the result now, or use wfa waiting with a reason if you still need time."
|
|
3611
3618
|
: "You finished without reporting the requested result. Report it now.");
|
|
3612
3619
|
// The first prompt carries only the result command; waiting is rare, so it is offered only
|
|
3613
3620
|
// once a check-in finds the agent idle without an answer.
|
|
@@ -3665,7 +3672,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3665
3672
|
toolsLesson(
|
|
3666
3673
|
this.options.tools.taught(this.key),
|
|
3667
3674
|
this.options.launcher,
|
|
3668
|
-
|
|
3675
|
+
resultSaysQuoting(schema),
|
|
3669
3676
|
),
|
|
3670
3677
|
]
|
|
3671
3678
|
.filter(Boolean)
|
|
@@ -3733,14 +3740,23 @@ class LogicalAgent implements AgentRef {
|
|
|
3733
3740
|
outcome.charges,
|
|
3734
3741
|
);
|
|
3735
3742
|
if (outcome.kind === "answered")
|
|
3736
|
-
return {
|
|
3743
|
+
return {
|
|
3744
|
+
outcome: { answered: true, kind: "answered", value: outcome.value as JsonValue, usage },
|
|
3745
|
+
};
|
|
3737
3746
|
if (outcome.kind === "failed") {
|
|
3738
3747
|
const login = outcome.login ? { login: outcome.login } : {};
|
|
3739
3748
|
return {
|
|
3740
|
-
outcome: {
|
|
3749
|
+
outcome: {
|
|
3750
|
+
answered: false,
|
|
3751
|
+
kind: "failed",
|
|
3752
|
+
reason: outcome.reason,
|
|
3753
|
+
retryable: false,
|
|
3754
|
+
...login,
|
|
3755
|
+
usage,
|
|
3756
|
+
},
|
|
3741
3757
|
};
|
|
3742
3758
|
}
|
|
3743
|
-
return { outcome: { kind: outcome.kind, reason: outcome.reason, usage } };
|
|
3759
|
+
return { outcome: { answered: false, kind: outcome.kind, reason: outcome.reason, usage } };
|
|
3744
3760
|
}
|
|
3745
3761
|
|
|
3746
3762
|
/**
|
|
@@ -4036,10 +4052,10 @@ class LogicalAgent implements AgentRef {
|
|
|
4036
4052
|
// Recorded `answered` with no value: it did what it was woken for, and takes no result.
|
|
4037
4053
|
this.turnEnded(
|
|
4038
4054
|
outcome.kind === "answered"
|
|
4039
|
-
? { kind: "answered", value: null, usage }
|
|
4055
|
+
? { answered: true, kind: "answered", value: null, usage }
|
|
4040
4056
|
: outcome.kind === "failed"
|
|
4041
|
-
? { kind: "failed", reason: outcome.reason, retryable: false, usage }
|
|
4042
|
-
: { kind: outcome.kind, reason: outcome.reason, usage },
|
|
4057
|
+
? { answered: false, kind: "failed", reason: outcome.reason, retryable: false, usage }
|
|
4058
|
+
: { answered: false, kind: outcome.kind, reason: outcome.reason, usage },
|
|
4043
4059
|
"turn",
|
|
4044
4060
|
true,
|
|
4045
4061
|
);
|
|
@@ -4074,17 +4090,23 @@ class LogicalAgent implements AgentRef {
|
|
|
4074
4090
|
later,
|
|
4075
4091
|
);
|
|
4076
4092
|
if (native === "expired") {
|
|
4077
|
-
return {
|
|
4093
|
+
return {
|
|
4094
|
+
answered: false,
|
|
4095
|
+
kind: "timed-out",
|
|
4096
|
+
reason: "compaction deadline exceeded",
|
|
4097
|
+
usage,
|
|
4098
|
+
};
|
|
4078
4099
|
}
|
|
4079
4100
|
if (native.state === "completed") {
|
|
4080
4101
|
return native.summary === undefined
|
|
4081
4102
|
? {
|
|
4103
|
+
answered: false,
|
|
4082
4104
|
kind: "failed",
|
|
4083
4105
|
reason: native.detail ?? "the harness did not confirm a compaction",
|
|
4084
4106
|
retryable: false,
|
|
4085
4107
|
usage,
|
|
4086
4108
|
}
|
|
4087
|
-
: { kind: "answered", value: native.summary, usage };
|
|
4109
|
+
: { answered: true, kind: "answered", value: native.summary, usage };
|
|
4088
4110
|
}
|
|
4089
4111
|
return reconcile<string>(native, usage);
|
|
4090
4112
|
};
|
|
@@ -4156,7 +4178,7 @@ function checkedWhen(when: unknown): string | Date {
|
|
|
4156
4178
|
if (when instanceof Date || typeof when === "string") return when;
|
|
4157
4179
|
if (readOf(when))
|
|
4158
4180
|
throw new Error(
|
|
4159
|
-
"sleep is for a duration or a Date;
|
|
4181
|
+
"sleep is for a duration or a Date; wf.at() is a select's branch, read at each of its waits",
|
|
4160
4182
|
);
|
|
4161
4183
|
throw new Error('sleep is for a duration, "2h", or until a Date');
|
|
4162
4184
|
}
|
|
@@ -4176,18 +4198,18 @@ function branchesOf(what: string, branches: unknown, durations: boolean): [strin
|
|
|
4176
4198
|
if ("key" in branches) throw new Error(`${what}: no branch is named key, which the answer uses`);
|
|
4177
4199
|
for (const [branch, source] of entries)
|
|
4178
4200
|
if (typeof source === "string" && !durations)
|
|
4179
|
-
throw new Error(`${what}'s ${branch} is a duration: a timer in a loop is
|
|
4201
|
+
throw new Error(`${what}'s ${branch} is a duration: a timer in a loop is wf.timer()'s`);
|
|
4180
4202
|
return entries;
|
|
4181
4203
|
}
|
|
4182
4204
|
|
|
4183
4205
|
/** What a time read from state reads, by its source. */
|
|
4184
4206
|
const timeReads = new WeakMap<object, () => Date | null>();
|
|
4185
4207
|
|
|
4186
|
-
/** `
|
|
4208
|
+
/** `wf.at(read)`: a select's branch, due at the time `read` returns. */
|
|
4187
4209
|
function timeRead(read: () => Date | null): TimeRead {
|
|
4188
4210
|
if (typeof read !== "function")
|
|
4189
4211
|
throw new Error(
|
|
4190
|
-
"
|
|
4212
|
+
"wf.at takes a function that reads a Date or null from the run's state, as () => ledger.nextNudge()",
|
|
4191
4213
|
);
|
|
4192
4214
|
const value = Object.freeze({});
|
|
4193
4215
|
timeReads.set(value, read);
|
|
@@ -4205,7 +4227,7 @@ function sourceTake(what: string, branch: string, source: unknown): WaitTake {
|
|
|
4205
4227
|
if (take) return take;
|
|
4206
4228
|
if (readOf(source))
|
|
4207
4229
|
throw new Error(
|
|
4208
|
-
`${what}'s ${branch} is
|
|
4230
|
+
`${what}'s ${branch} is wf.at(), a select's: a race answers once, so it takes a timer, wf.timer(name, date)`,
|
|
4209
4231
|
);
|
|
4210
4232
|
throw new Error(
|
|
4211
4233
|
`${what}'s ${branch} is not a source: a timer, a signal, a participant routed to the code, an agent, or a started turn`,
|
|
@@ -4439,20 +4461,41 @@ function reconcile<T extends JsonValue>(
|
|
|
4439
4461
|
): TurnOutcome<T> {
|
|
4440
4462
|
switch (native.state) {
|
|
4441
4463
|
case "completed":
|
|
4442
|
-
return {
|
|
4464
|
+
return {
|
|
4465
|
+
answered: false,
|
|
4466
|
+
kind: "unanswered",
|
|
4467
|
+
reason: "agent settled without an accepted result",
|
|
4468
|
+
usage,
|
|
4469
|
+
};
|
|
4443
4470
|
case "blocked":
|
|
4444
|
-
return {
|
|
4471
|
+
return {
|
|
4472
|
+
answered: false,
|
|
4473
|
+
kind: "blocked",
|
|
4474
|
+
reason: native.detail ?? "agent is blocked",
|
|
4475
|
+
usage,
|
|
4476
|
+
};
|
|
4445
4477
|
case "timed-out":
|
|
4446
|
-
return {
|
|
4478
|
+
return {
|
|
4479
|
+
answered: false,
|
|
4480
|
+
kind: "timed-out",
|
|
4481
|
+
reason: native.detail ?? "operation timed out",
|
|
4482
|
+
usage,
|
|
4483
|
+
};
|
|
4447
4484
|
case "failed":
|
|
4448
4485
|
return {
|
|
4486
|
+
answered: false,
|
|
4449
4487
|
kind: "failed",
|
|
4450
4488
|
reason: native.detail ?? "harness operation failed",
|
|
4451
4489
|
retryable: false,
|
|
4452
4490
|
usage,
|
|
4453
4491
|
};
|
|
4454
4492
|
case "cancelled":
|
|
4455
|
-
return {
|
|
4493
|
+
return {
|
|
4494
|
+
answered: false,
|
|
4495
|
+
kind: "cancelled",
|
|
4496
|
+
reason: native.detail ?? "operation cancelled",
|
|
4497
|
+
usage,
|
|
4498
|
+
};
|
|
4456
4499
|
}
|
|
4457
4500
|
}
|
|
4458
4501
|
|
|
@@ -4566,6 +4609,11 @@ function resultSchema<T extends JsonValue>(schema: OutputSchema<T> | undefined):
|
|
|
4566
4609
|
return schema ? parseJsonSchema(schema) : { type: "string" };
|
|
4567
4610
|
}
|
|
4568
4611
|
|
|
4612
|
+
function resultSaysQuoting(schema: JsonSchema): boolean {
|
|
4613
|
+
const simple = simpleInput(schema);
|
|
4614
|
+
return simple !== undefined && takesText(simple);
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4569
4617
|
function operationPrompt(
|
|
4570
4618
|
prompt: string,
|
|
4571
4619
|
schema: JsonSchema,
|
|
@@ -4576,7 +4624,7 @@ function operationPrompt(
|
|
|
4576
4624
|
// A quoted heredoc passes the JSON through untouched: a quoted argument broke on shell quoting
|
|
4577
4625
|
// (story 002). Not indented: a copied terminator with leading spaces never closes the heredoc.
|
|
4578
4626
|
// The schema itself, not a rendering of it: without its bounds, first answers were 0/160 valid
|
|
4579
|
-
// (E5). `
|
|
4627
|
+
// (E5). `wfa` says what to do about a rejection, so the prompt does not. A simple schema is taught
|
|
4580
4628
|
// as flags instead, its usage line carrying every bound, which held them as well (story 035, M1).
|
|
4581
4629
|
const simple = simpleInput(schema);
|
|
4582
4630
|
if (simple) {
|
|
@@ -4586,8 +4634,7 @@ function operationPrompt(
|
|
|
4586
4634
|
"When the answer is ready, return it by running:",
|
|
4587
4635
|
"",
|
|
4588
4636
|
`${launcher} result ${callId} ${usageLine(simple)}`,
|
|
4589
|
-
"",
|
|
4590
|
-
QUOTING_RULE,
|
|
4637
|
+
...(takesText(simple) ? ["", QUOTING_RULE] : []),
|
|
4591
4638
|
].join("\n");
|
|
4592
4639
|
}
|
|
4593
4640
|
return [
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Checked by `tsc --noEmit`, never run: story 032's workflow and its test, as an author writes them.
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type WorkflowDefinition,
|
|
2
|
+
import type {
|
|
3
|
+
ParticipantRef,
|
|
4
|
+
WorkflowContext,
|
|
5
|
+
WorkflowDefinition,
|
|
7
6
|
} from "@agentswf/contract/workflow";
|
|
8
7
|
import { answer, type ParticipantScript, testWorkflow } from ".";
|
|
9
8
|
|
|
@@ -11,13 +10,13 @@ declare const runtime: string;
|
|
|
11
10
|
|
|
12
11
|
const investigate: WorkflowDefinition<null, string> = {
|
|
13
12
|
meta: { name: "investigate", description: "watches checkout-api" },
|
|
14
|
-
async run(
|
|
15
|
-
const investigator = await
|
|
16
|
-
const coordinator: ParticipantRef = await
|
|
13
|
+
async run(wf: WorkflowContext) {
|
|
14
|
+
const investigator = await wf.agents.open({ key: "investigator", runtime });
|
|
15
|
+
const coordinator: ParticipantRef = await wf.participants.connect({
|
|
17
16
|
key: "coordinator",
|
|
18
17
|
labels: { audience: "operator" },
|
|
19
18
|
});
|
|
20
|
-
await
|
|
19
|
+
await wf.messages.allow({
|
|
21
20
|
between: [investigator, coordinator],
|
|
22
21
|
purpose: "report findings that hold up; ask when the logs can't settle it",
|
|
23
22
|
timeoutMs: 2 * 60 * 60_000,
|
|
@@ -25,8 +24,8 @@ const investigate: WorkflowDefinition<null, string> = {
|
|
|
25
24
|
const { outcome } = await investigator.run({ prompt: "Check the checkout-api logs." });
|
|
26
25
|
await coordinator.release("investigation done");
|
|
27
26
|
// A run has no deadline unless `awf run --timeout` gives it one.
|
|
28
|
-
const left =
|
|
29
|
-
return
|
|
27
|
+
const left = wf.deadline ? wf.deadline.unixMilliseconds - Date.now() : undefined;
|
|
28
|
+
return outcome.answered ? `${outcome.value} (${left ?? "no"} ms left)` : outcome.kind;
|
|
30
29
|
},
|
|
31
30
|
};
|
|
32
31
|
|
|
@@ -49,7 +48,7 @@ export const sendsFirst: ParticipantScript = async (event) => {
|
|
|
49
48
|
if (sent.code !== "delivered") throw new Error(sent.code);
|
|
50
49
|
};
|
|
51
50
|
|
|
52
|
-
export async function refused(
|
|
51
|
+
export async function refused(wf: WorkflowContext): Promise<void> {
|
|
53
52
|
// @ts-expect-error bindingPath is gone: the socket's place is fixed.
|
|
54
|
-
await
|
|
53
|
+
await wf.participants.connect({ key: "coordinator", bindingPath: "/tmp/x" });
|
|
55
54
|
}
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
type FakeFork,
|
|
38
38
|
type FakeSet,
|
|
39
39
|
} from "@agentswf/harness/testing";
|
|
40
|
-
import { submitControl, submitResult, submitWaiting } from "
|
|
40
|
+
import { submitControl, submitResult, submitWaiting } from "../agent-cli/client";
|
|
41
41
|
import {
|
|
42
42
|
type Message,
|
|
43
43
|
type MessageOutcome,
|
|
@@ -192,7 +192,7 @@ export function createScriptedHost(
|
|
|
192
192
|
return n;
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
-
/** `
|
|
195
|
+
/** `wfa send` or `wfa reply` from a running script; with `awaitReply`, resolves with the reply. */
|
|
196
196
|
const message = async (
|
|
197
197
|
invocation: Invocation,
|
|
198
198
|
command: "send" | "reply",
|
|
@@ -226,7 +226,7 @@ export function createScriptedHost(
|
|
|
226
226
|
suspendIfAwaiting(invocation);
|
|
227
227
|
return { kind: "refused", code: response.code as SendCode | ReplyCode };
|
|
228
228
|
}
|
|
229
|
-
throw new Error(`
|
|
229
|
+
throw new Error(`wfa ${command} was refused (${response.code}): ${response.error}`);
|
|
230
230
|
}
|
|
231
231
|
if (!options.awaitReply) {
|
|
232
232
|
suspendIfAwaiting(invocation);
|
|
@@ -238,7 +238,7 @@ export function createScriptedHost(
|
|
|
238
238
|
});
|
|
239
239
|
};
|
|
240
240
|
|
|
241
|
-
/** `
|
|
241
|
+
/** `wfa tool` from a running script, as JSON or no input; waits as long as the engine takes. */
|
|
242
242
|
const callTool = async (
|
|
243
243
|
invocation: Invocation,
|
|
244
244
|
name: string,
|
|
@@ -278,14 +278,14 @@ export function createScriptedHost(
|
|
|
278
278
|
return { kind: "failed", outcome: response.outcome, error: response.error };
|
|
279
279
|
default:
|
|
280
280
|
if (response.code === "internal-error" || !TOOL_REFUSALS.has(response.code))
|
|
281
|
-
throw new Error(`
|
|
281
|
+
throw new Error(`wfa tool was refused (${response.code}): ${response.error}`);
|
|
282
282
|
return { kind: "refused", code: response.code as ToolCode, error: response.error };
|
|
283
283
|
}
|
|
284
284
|
};
|
|
285
285
|
|
|
286
286
|
/**
|
|
287
287
|
* Ends the native turn when its script has gone still awaiting a reply, as an agent ends its turn
|
|
288
|
-
* after `
|
|
288
|
+
* after `wfa send --await-reply`: nothing of the host's in flight, and no step taken since.
|
|
289
289
|
*/
|
|
290
290
|
const suspendIfAwaiting = (invocation: Invocation) => {
|
|
291
291
|
setTimeout(() => {
|
|
@@ -413,7 +413,7 @@ export function createScriptedHost(
|
|
|
413
413
|
// Submitted is answered, even though the engine then releases the turn.
|
|
414
414
|
let outcome: TurnOutcome = "cancelled";
|
|
415
415
|
try {
|
|
416
|
-
// As an agent's `
|
|
416
|
+
// As an agent's `wfa send` returns before it can run `wfa result`.
|
|
417
417
|
if (invocation) await Promise.allSettled([...invocation.calls]);
|
|
418
418
|
const { endpoint, operationId } = context.binding!;
|
|
419
419
|
const response = await submitResult(endpoint, {
|
|
@@ -782,7 +782,7 @@ type Awaiting = { peer: string; by: Invocation; resolve(outcome: MessageOutcome)
|
|
|
782
782
|
type Invocation = {
|
|
783
783
|
operation: Operation;
|
|
784
784
|
turn: Turn;
|
|
785
|
-
/** Its `
|
|
785
|
+
/** Its `wfa` calls in flight. */
|
|
786
786
|
calls: Set<Promise<unknown>>;
|
|
787
787
|
state: "running" | "suspended" | "settled" | "cancelled";
|
|
788
788
|
/** Where it stopped in its current native turn. */
|
|
@@ -102,7 +102,7 @@ export type TestOptions = {
|
|
|
102
102
|
awaitReply?: "listed" | "anywhere";
|
|
103
103
|
/** Each decision's answers, by its key or a pattern. */
|
|
104
104
|
decisions?: Readonly<Record<string, DecisionScript>>;
|
|
105
|
-
/** What `
|
|
105
|
+
/** What `wf.allowance` reads, one per harness; a harness with none here reads `none`. */
|
|
106
106
|
allowance?: readonly HarnessAllowance[];
|
|
107
107
|
/** Runtime aliases beside the ones `awf run` installs, `claude` and `codex`; the same name replaces one. */
|
|
108
108
|
runtimes?: RuntimeAliases;
|
|
@@ -488,7 +488,6 @@ export async function testWorkflow<Args extends JsonValue, Result extends JsonVa
|
|
|
488
488
|
sandboxes: {
|
|
489
489
|
providers: {
|
|
490
490
|
installed: { srt: sandboxes.provider, docker: sandboxes.provider },
|
|
491
|
-
default: "srt",
|
|
492
491
|
},
|
|
493
492
|
sandboxesDir: directory(),
|
|
494
493
|
machineRoot: directory(),
|
|
@@ -42,16 +42,16 @@ export type Turn = {
|
|
|
42
42
|
*/
|
|
43
43
|
readonly messages: readonly Message[];
|
|
44
44
|
/**
|
|
45
|
-
* `
|
|
45
|
+
* `wfa send`: resolves once admitted, `sent`; or, with `awaitReply`, with the reply, which arrives
|
|
46
46
|
* in a later turn of the same call. Returning a result without awaiting it drops the wait, and it
|
|
47
47
|
* resolves `failed` with reason `dropped`. While a reply is awaited the script awaits only the
|
|
48
48
|
* host's calls: once it is still, its turn ends.
|
|
49
49
|
*/
|
|
50
50
|
send(peer: string, text: string, options?: SendOptions): Promise<MessageOutcome>;
|
|
51
|
-
/** `
|
|
51
|
+
/** `wfa reply`: answers the question `peer` asked. A result is refused while one is owed. */
|
|
52
52
|
reply(peer: string, text: string, options?: SendOptions): Promise<MessageOutcome>;
|
|
53
53
|
/**
|
|
54
|
-
* `
|
|
54
|
+
* `wfa tool`: calls the agent's tool `name` with `input` as its JSON, or none, through the run's
|
|
55
55
|
* real checks and handler; resolves with what the agent would read.
|
|
56
56
|
*/
|
|
57
57
|
tool(name: string, input?: JsonObject): Promise<ToolOutcome>;
|
|
@@ -60,7 +60,7 @@ export type Turn = {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* How a tool call ended, as `
|
|
63
|
+
* How a tool call ended, as `wfa tool` reports it; a failure or a refusal is a value to test. A
|
|
64
64
|
* refusal carries what the agent is told, which for `invalid-input` is what it corrects from.
|
|
65
65
|
*/
|
|
66
66
|
export type ToolOutcome =
|
|
@@ -50,7 +50,7 @@ const withEvents = defineWorkflow({
|
|
|
50
50
|
meta: { name: "w", description: "w" },
|
|
51
51
|
file: import.meta.path,
|
|
52
52
|
events: { ping: { type: "object" } as OutputSchema<{ n: number }> },
|
|
53
|
-
run: async (
|
|
53
|
+
run: async (wf) => (await wf.receive()).length,
|
|
54
54
|
});
|
|
55
55
|
export const tested = () => testWorkflow(withEvents, { id: "k" });
|
|
56
56
|
// @ts-expect-error a workflow with events is tested from { id, event? }, never its args
|
|
@@ -58,7 +58,7 @@ export const byArgs = () => testWorkflow(withEvents, null);
|
|
|
58
58
|
const plain = defineWorkflow({
|
|
59
59
|
meta: { name: "p", description: "p" },
|
|
60
60
|
prepare: ({ argv }) => ({ n: argv.length }),
|
|
61
|
-
run: async (
|
|
61
|
+
run: async (_wf, args) => args.n,
|
|
62
62
|
});
|
|
63
63
|
// @ts-expect-error a workflow without events is tested from its args, not { id }
|
|
64
64
|
export const byOptions = () => testWorkflow(plain, { id: "k" });
|