@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.
Files changed (49) hide show
  1. package/package.json +5 -6
  2. package/src/agent-cli/cli.ts +571 -0
  3. package/src/agent-cli/client.ts +115 -0
  4. package/src/agent-launcher.ts +15 -25
  5. package/src/allowance-command.ts +3 -15
  6. package/src/attempt-output.ts +1 -1
  7. package/src/control-plane.ts +1 -1
  8. package/src/emit-command.ts +2 -13
  9. package/src/help/allowance.txt +13 -0
  10. package/src/help/emit.txt +13 -0
  11. package/src/help/inbox.txt +12 -0
  12. package/src/help/init.txt +12 -0
  13. package/src/help/logs.txt +13 -0
  14. package/src/help/reply.txt +18 -0
  15. package/src/help/run.txt +44 -0
  16. package/src/help/runs.txt +11 -0
  17. package/src/help/test.txt +9 -0
  18. package/src/help/tick.txt +12 -0
  19. package/src/here.ts +1 -1
  20. package/src/holder-command.ts +2 -20
  21. package/src/inbox-command.ts +2 -14
  22. package/src/init-command.ts +2 -11
  23. package/src/init-template/AGENTS.md.template +3 -3
  24. package/src/init-template/hello/workflow.ts.template +4 -4
  25. package/src/logs-command.ts +2 -15
  26. package/src/mailbox.ts +15 -2
  27. package/src/messaging/limits.ts +1 -1
  28. package/src/messaging/messaging.ts +22 -22
  29. package/src/messaging/routes.ts +1 -1
  30. package/src/messaging/text.ts +17 -17
  31. package/src/operation-liveness.ts +1 -1
  32. package/src/operator-runtime.ts +4 -7
  33. package/src/result-validation.ts +1 -1
  34. package/src/run-command.ts +2 -43
  35. package/src/run-usage.ts +1 -1
  36. package/src/runs-command.ts +2 -13
  37. package/src/sandboxes.ts +3 -2
  38. package/src/simple-input.ts +13 -3
  39. package/src/stage-ledger.ts +1 -1
  40. package/src/test-command.ts +2 -11
  41. package/src/testing.ts +1 -1
  42. package/src/tick-command.ts +2 -13
  43. package/src/tools.ts +16 -7
  44. package/src/workflow-runner.ts +85 -38
  45. package/src/workflow-testing/coordinator.typecheck.ts +12 -13
  46. package/src/workflow-testing/host.ts +8 -8
  47. package/src/workflow-testing/index.ts +1 -2
  48. package/src/workflow-testing/script.ts +4 -4
  49. 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 `wf` does: one framed request over its own socket. */
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,
@@ -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 { checkGiven, QUOTING_RULE, readGiven, simpleInput, usageLine } from "./simple-input";
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 `wf result` and `wf waiting`. */
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
- * `wf tool`'s request from `agent`'s socket. `lifetime` sets how long its connection may stay
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 it
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> '<value>' ...` : `${launcher} tool <name>`,
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
- ...(flags && !quotingSaid ? ["", QUOTING_RULE] : []),
561
+ ...(text && !quotingSaid ? ["", QUOTING_RULE] : []),
553
562
  "",
554
563
  ...entries,
555
564
  ].join("\n");
@@ -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 `workflow.now()` read; absent, the clock's. A test's is virtual. */
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 `workflow.allowance` reads a harness's plan; without it, every harness reads `none`. */
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 `workflow.emit`'s event through the one way in; absent, `emitEvent` on this machine. A
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 `wf`'s source, built once, at the first sandboxed agent. */
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 ?? "wf",
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("workflow.now()");
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("workflow.after()");
1171
+ this.#notReading("wf.after()");
1172
1172
  if (typeof duration !== "string" || !/^\d+(?:\.\d+)?[dhm]$/.test(duration))
1173
1173
  throw new Error(
1174
- `workflow.after takes days, hours or minutes, "1d", "3h", "1.5h"; not ${String(duration)}`,
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 workflow.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`,
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}: workflow.at reads a valid Date or null from the run's state, not ${
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 { kind: "timed-out", reason: "settings deadline exceeded", usage: usage() };
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) return { kind: "cancelled", reason, usage: usage() };
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 wf waiting with a reason if you still need time."
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
- simpleInput(schema) !== undefined,
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 { outcome: { kind: "answered", value: outcome.value as JsonValue, usage } };
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: { kind: "failed", reason: outcome.reason, retryable: false, ...login, usage },
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 { kind: "timed-out", reason: "compaction deadline exceeded", usage };
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; workflow.at() is a select's branch, read at each of its waits",
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 workflow.timer()'s`);
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
- /** `workflow.at(read)`: a select's branch, due at the time `read` returns. */
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
- "workflow.at takes a function that reads a Date or null from the run's state, as () => ledger.nextNudge()",
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 workflow.at(), a select's: a race answers once, so it takes a timer, workflow.timer(name, date)`,
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 { kind: "unanswered", reason: "agent settled without an accepted result", usage };
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 { kind: "blocked", reason: native.detail ?? "agent is blocked", usage };
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 { kind: "timed-out", reason: native.detail ?? "operation timed out", usage };
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 { kind: "cancelled", reason: native.detail ?? "operation cancelled", usage };
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). `wf` says what to do about a rejection, so the prompt does not. A simple schema is taught
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
- isAnswered,
4
- type ParticipantRef,
5
- type WorkflowContext,
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(workflow: WorkflowContext) {
15
- const investigator = await workflow.agents.open({ key: "investigator", runtime });
16
- const coordinator: ParticipantRef = await workflow.participants.connect({
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 workflow.messages.allow({
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 = workflow.deadline ? workflow.deadline.unixMilliseconds - Date.now() : undefined;
29
- return isAnswered(outcome) ? `${outcome.value} (${left ?? "no"} ms left)` : outcome.kind;
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(workflow: WorkflowContext): Promise<void> {
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 workflow.participants.connect({ key: "coordinator", bindingPath: "/tmp/x" });
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 "@agentswf/wf/client";
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
- /** `wf send` or `wf reply` from a running script; with `awaitReply`, resolves with the reply. */
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(`wf ${command} was refused (${response.code}): ${response.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
- /** `wf tool` from a running script, as JSON or no input; waits as long as the engine takes. */
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(`wf tool was refused (${response.code}): ${response.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 `wf send --await-reply`: nothing of the host's in flight, and no step taken since.
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 `wf send` returns before it can run `wf result`.
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 `wf` calls in flight. */
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 `workflow.allowance` reads, one per harness; a harness with none here reads `none`. */
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
- * `wf send`: resolves once admitted, `sent`; or, with `awaitReply`, with the reply, which arrives
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
- /** `wf reply`: answers the question `peer` asked. A result is refused while one is owed. */
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
- * `wf tool`: calls the agent's tool `name` with `input` as its JSON, or none, through the run's
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 `wf tool` reports it; a failure or a refusal is a value to test. A
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 (workflow) => (await workflow.receive()).length,
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 (_workflow, args) => args.n,
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" });