@agentswf/engine 0.1.0-beta.5 → 0.1.0

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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/package.json +13 -9
  4. package/src/accounting/format.ts +4 -3
  5. package/src/accounting/spent.ts +128 -0
  6. package/src/accounting/summary.ts +40 -32
  7. package/src/agent-cli/cli.ts +28 -55
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +4 -4
  11. package/src/attempt-output.ts +13 -31
  12. package/src/attempt-view.ts +33 -32
  13. package/src/author-typebox.ts +13 -0
  14. package/src/client/checks.ts +94 -0
  15. package/src/client/exported.ts +17 -0
  16. package/src/client/index.ts +122 -0
  17. package/src/client/locate.ts +34 -0
  18. package/src/client/published.ts +3 -0
  19. package/src/client/runs.ts +337 -0
  20. package/src/command-line.ts +64 -0
  21. package/src/commands.ts +7 -1
  22. package/src/control-plane.ts +4 -6
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +27 -28
  25. package/src/deadlines.ts +6 -40
  26. package/src/decisions/directory.ts +50 -37
  27. package/src/decisions/fake.ts +1 -1
  28. package/src/decisions/openrouter.ts +4 -2
  29. package/src/decisions/seam.ts +4 -4
  30. package/src/display-path.ts +3 -3
  31. package/src/duration.ts +19 -33
  32. package/src/emit-command.ts +38 -22
  33. package/src/emit.ts +148 -59
  34. package/src/event-ids.ts +31 -0
  35. package/src/files.ts +44 -4
  36. package/src/hand-over.ts +150 -0
  37. package/src/help/allowance.txt +4 -3
  38. package/src/help/emit.txt +11 -7
  39. package/src/help/inbox.txt +5 -3
  40. package/src/help/logs.txt +7 -5
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +13 -8
  44. package/src/help/runs.txt +4 -2
  45. package/src/help/tick.txt +9 -4
  46. package/src/herdr-placement.ts +3 -33
  47. package/src/herdr-run-session.ts +5 -13
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +54 -136
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +42 -56
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.ts.template +3 -2
  55. package/src/jsonl.ts +10 -6
  56. package/src/keep-alive.ts +7 -19
  57. package/src/logs-command.ts +134 -53
  58. package/src/message-operation.ts +6 -3
  59. package/src/messaging/messaging.ts +8 -10
  60. package/src/messaging/text.ts +2 -2
  61. package/src/open-command.ts +770 -0
  62. package/src/operation-events.ts +4 -6
  63. package/src/operation-liveness.ts +19 -4
  64. package/src/operator-cli.ts +206 -90
  65. package/src/operator-runtime.ts +24 -2
  66. package/src/ops.ts +270 -0
  67. package/src/pane-layout.ts +49 -96
  68. package/src/progress-view.ts +2 -2
  69. package/src/project-engine.ts +79 -0
  70. package/src/projects.ts +13 -2
  71. package/src/refusals.ts +119 -0
  72. package/src/result-slots.ts +11 -31
  73. package/src/result-validation.ts +2 -2
  74. package/src/run-command.ts +27 -31
  75. package/src/run-commands.ts +42 -114
  76. package/src/run-event-log.ts +1 -1
  77. package/src/run-lines.ts +159 -0
  78. package/src/run-lock.ts +57 -1
  79. package/src/run-policy/stage-plan.ts +154 -0
  80. package/src/run-policy/stages.ts +113 -0
  81. package/src/run-policy/waits.ts +452 -0
  82. package/src/run-prepare.ts +165 -41
  83. package/src/run-read.ts +109 -41
  84. package/src/run-state.ts +170 -50
  85. package/src/run-text.ts +54 -0
  86. package/src/run-unreachable.ts +18 -0
  87. package/src/run-usage.ts +17 -11
  88. package/src/run-view.ts +73 -14
  89. package/src/runs-command.ts +81 -63
  90. package/src/runs.ts +284 -222
  91. package/src/sandbox-homes.ts +65 -13
  92. package/src/sandboxes.ts +182 -28
  93. package/src/session-lines.ts +93 -0
  94. package/src/skills/run-skills.ts +8 -1
  95. package/src/sources.ts +2 -39
  96. package/src/spend.ts +333 -0
  97. package/src/stage-ledger.ts +111 -153
  98. package/src/stop-crashed.ts +6 -5
  99. package/src/stopped.ts +4 -4
  100. package/src/test-command.ts +37 -19
  101. package/src/testing/operation-events.ts +3 -23
  102. package/src/testing.ts +66 -18
  103. package/src/tick-command.ts +145 -15
  104. package/src/tools.ts +11 -30
  105. package/src/turn-ends.ts +37 -0
  106. package/src/waits.ts +651 -0
  107. package/src/wake.ts +84 -54
  108. package/src/workflow-loader.ts +49 -2
  109. package/src/workflow-runner.ts +525 -618
  110. package/src/workflow-test-preload.ts +4 -1
  111. package/src/workflow-testing/coordinators.ts +1 -1
  112. package/src/workflow-testing/fake-client.ts +387 -0
  113. package/src/workflow-testing/host.ts +28 -29
  114. package/src/workflow-testing/index.ts +9 -27
  115. package/src/workflow-testing/run-test.ts +201 -286
  116. package/src/workflow-testing/script.ts +7 -39
  117. package/src/deprecations.ts +0 -177
  118. package/src/hold-run-lock.fixture.ts +0 -8
  119. package/src/mailbox.ts +0 -972
  120. package/src/stage-plan.ts +0 -186
  121. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  122. package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
package/src/stage-plan.ts DELETED
@@ -1,186 +0,0 @@
1
- import type {
2
- AttemptRecord,
3
- AttemptStage,
4
- StageNeed,
5
- StageRecord,
6
- } from "@agentswf/contract/records";
7
- import { type JsonSchema, type SchemaError, validate } from "@agentswf/contract/schema";
8
- import type { JsonValue } from "@agentswf/contract/workflow";
9
-
10
- /**
11
- * What a continue does with a stage it enters: until its start point, each is reused from its
12
- * record, provided, or stops the attempt; from the start point on, every stage runs. The start
13
- * point is `--from-stage`, or else the first stage entered with no succeeded record. Before a
14
- * `--from-stage`, a stage whose record can't be reused takes its value from `--values`, one that
15
- * returns nothing is passed, and one with nothing recorded needs a value. Pure: the records, the values, the result and the version come
16
- * in, a decision goes out.
17
- */
18
- type StagePlanDecision =
19
- | { kind: "reuse"; record: StageRecord; value: JsonValue | undefined }
20
- /** A value given, checked by the stage's result; undefined for a stage that returns nothing. */
21
- | { kind: "provide"; value: JsonValue | undefined }
22
- /** `start` on the stage that is the start point, when the records it outdates are moved. */
23
- | { kind: "run"; start: boolean }
24
- | { kind: "stop"; reason: string }
25
- /** Nothing recorded, and no value given that fits: `--values` lets the attempt go on. */
26
- | { kind: "need"; reason: string; need: StageNeed };
27
-
28
- export type StagePlanState = {
29
- records: ReadonlyMap<string, StageRecord>;
30
- fromStage?: string;
31
- /** `--values`: each stage's value, to start at `fromStage`. */
32
- values?: ReadonlyMap<string, JsonValue>;
33
- /** The workflow's `meta.version` now. */
34
- workflowVersion?: string;
35
- /** Whether this attempt has reached its start point. */
36
- started: boolean;
37
- /** The stages this attempt has entered so far. */
38
- entered: readonly string[];
39
- };
40
-
41
- /**
42
- * Decides a stage entered, given its `result` as JSON Schema, absent for a stage that returns
43
- * nothing. A stop's reason says why; the command that goes on names the stage to redo.
44
- */
45
- export function planStage(
46
- state: StagePlanState,
47
- stage: string,
48
- result: JsonSchema | undefined,
49
- ): StagePlanDecision {
50
- if (state.started) return { kind: "run", start: false };
51
- if (stage === state.fromStage) return { kind: "run", start: true };
52
- const record = state.records.get(stage);
53
- const succeeded = record?.outcome === "succeeded" ? record : undefined;
54
- const stale = succeeded && unreusable(state, succeeded, result);
55
- if (succeeded && stale === undefined) {
56
- return { kind: "reuse", record: succeeded, value: succeeded.value };
57
- }
58
- if (state.fromStage === undefined) {
59
- return stale === undefined ? { kind: "run", start: true } : { kind: "stop", reason: stale };
60
- }
61
- // A record there that failed or went stale is redone from its stage, as on any continue, unless
62
- // a value given replaces it.
63
- const redo = record && {
64
- kind: "stop" as const,
65
- reason: stale ?? `${stage} did not succeed in attempt ${record.attempt}`,
66
- };
67
- if (result === undefined) return redo ?? { kind: "provide", value: undefined };
68
- const need = { stage, schema: result };
69
- const given = state.values?.get(stage);
70
- if (given !== undefined) {
71
- const errors = validate(result, given);
72
- if (errors.length === 0) return { kind: "provide", value: given };
73
- const misfit = problems(`${stage}'s value in --values does not fit its result:`, errors);
74
- return redo ? { kind: "stop", reason: misfit } : { kind: "need", reason: misfit, need };
75
- }
76
- return (
77
- redo ?? {
78
- kind: "need",
79
- reason: `nothing recorded for ${stage}${unreached(state, stage)}`,
80
- need,
81
- }
82
- );
83
- }
84
-
85
- /**
86
- * Why a succeeded record can't be reused, as a stop says it, or undefined when it can: another
87
- * major version recorded it, or its value no longer fits the stage's `result`.
88
- */
89
- function unreusable(
90
- state: StagePlanState,
91
- record: StageRecord,
92
- result: JsonSchema | undefined,
93
- ): string | undefined {
94
- const { stage } = record;
95
- const recorded = majorOf(record.workflowVersion);
96
- const now = majorOf(state.workflowVersion);
97
- if (recorded !== undefined && now !== undefined && recorded !== now) {
98
- return `${stage} was recorded by ${record.workflowVersion}; this is ${state.workflowVersion}`;
99
- }
100
- return misfitOf(record, result);
101
- }
102
-
103
- /** Why a succeeded record's value no longer fits the stage's `result`; undefined when it does. */
104
- function misfitOf(record: StageRecord, result: JsonSchema | undefined): string | undefined {
105
- const { stage } = record;
106
- if (result === undefined) {
107
- return record.value === undefined
108
- ? undefined
109
- : `${stage}'s record holds a value, and the stage no longer has a result`;
110
- }
111
- if (record.value === undefined) {
112
- return `${stage}'s record holds no value, and the stage's result now expects one`;
113
- }
114
- const errors = validate(result, record.value);
115
- return errors.length === 0
116
- ? undefined
117
- : problems(`${stage}'s record no longer fits its result schema:`, errors);
118
- }
119
-
120
- /** A value's problems under what they are problems of, a line each. */
121
- function problems(heading: string, errors: readonly SchemaError[]): string {
122
- return [heading, ...errors.map((error) => ` ${error.path}: ${error.message}`)].join("\n");
123
- }
124
-
125
- /** Recorded stages this attempt hasn't reached, which a rename in the code would leave behind. */
126
- function unreached(state: StagePlanState, stage: string): string {
127
- const others = [...state.records.keys()].filter(
128
- (name) => name !== stage && name !== state.fromStage && !state.entered.includes(name),
129
- );
130
- return others.length > 0 ? ` (recorded and not reached: ${others.join(", ")})` : "";
131
- }
132
-
133
- /** What must match for a record to be reused: the major, or `0.{minor}`, as semver has it. */
134
- function majorOf(version: string | undefined): string | undefined {
135
- if (version === undefined) return undefined;
136
- const [major, minor] = version.split(".");
137
- return major === "0" ? `0.${minor}` : major;
138
- }
139
-
140
- /**
141
- * In a run of a workflow with `events`, a stage runs once ever: a succeeded record is reused
142
- * wherever and however often its name is entered, whatever version of the code recorded it, as
143
- * long as its value still fits the stage's `result`. One that doesn't fit stops the attempt; a
144
- * failed record, or none, runs.
145
- */
146
- export function planStageByContent(
147
- record: StageRecord | undefined,
148
- result: JsonSchema | undefined,
149
- ): Extract<StagePlanDecision, { kind: "reuse" | "stop" }> | { kind: "run" } {
150
- if (record?.outcome !== "succeeded") return { kind: "run" };
151
- const misfit = misfitOf(record, result);
152
- return misfit === undefined
153
- ? { kind: "reuse", record, value: record.value }
154
- : { kind: "stop", reason: misfit };
155
- }
156
-
157
- /**
158
- * A `--from-stage {stage}` that attempt `attempt` redid, in a run of a workflow with `events`. In
159
- * that attempt and every later one, a stage first entered after `stage` whose record an earlier
160
- * attempt than `attempt` wrote runs again, its record replaced: it was made against the stage
161
- * before the redo. A record never entered after the stage keeps its reuse.
162
- */
163
- export type StageRedo = { stage: string; attempt: number };
164
-
165
- /**
166
- * The last `--from-stage` an earlier attempt of a run with `events` redid: the latest attempt with
167
- * one that ran its stage. Its attempt record says so in `stages`, which a stop for a stage never
168
- * reached, a refusal and a crash leave without it; a crash after the stage's new record was written
169
- * leaves that record, the attempt's own. A redo whose stage failed and ran again on a wake is still
170
- * the redo. Only attempt and stage records are read, which a continue may read.
171
- */
172
- export function lastRedo(
173
- attempts: readonly (Pick<AttemptRecord, "attempt" | "flags"> & {
174
- stages?: readonly Pick<AttemptStage, "stage" | "source">[];
175
- })[],
176
- records: ReadonlyMap<string, StageRecord>,
177
- before: number,
178
- ): StageRedo | undefined {
179
- for (const { attempt, flags, stages } of [...attempts].sort((a, b) => b.attempt - a.attempt)) {
180
- const stage = flags.fromStage;
181
- if (attempt >= before || stage === undefined) continue;
182
- const ran = stages?.some((each) => each.stage === stage && each.source === "ran") ?? false;
183
- if (ran || records.get(stage)?.attempt === attempt) return { stage, attempt };
184
- }
185
- return undefined;
186
- }
@@ -1,54 +0,0 @@
1
- // Checked by `tsc --noEmit`, never run: story 032's workflow and its test, as an author writes them.
2
- import {
3
- defineWorkflow,
4
- type ParticipantRef,
5
- type WorkflowContext,
6
- } from "@agentswf/contract/workflow";
7
- import { answer, type ParticipantScript, testWorkflow } from ".";
8
-
9
- declare const runtime: string;
10
-
11
- const investigate = defineWorkflow<null, string>({
12
- meta: { name: "investigate", description: "watches checkout-api" },
13
- async run(wf: WorkflowContext) {
14
- const investigator = await wf.agents.open({ key: "investigator", runtime });
15
- const coordinator: ParticipantRef = await wf.participants.connect({
16
- key: "coordinator",
17
- labels: { audience: "operator" },
18
- });
19
- await wf.messages.allow({
20
- between: [investigator, coordinator],
21
- purpose: "report findings that hold up; ask when the logs can't settle it",
22
- timeoutMs: 2 * 60 * 60_000,
23
- });
24
- const { outcome } = await investigator.run({ prompt: "Check the checkout-api logs." });
25
- await coordinator.release("investigation done");
26
- // A run has no deadline unless `awf run --timeout` gives it one.
27
- const left = wf.deadline ? wf.deadline.unixMilliseconds - Date.now() : undefined;
28
- return outcome.answered ? `${outcome.value} (${left ?? "no"} ms left)` : outcome.kind;
29
- },
30
- });
31
-
32
- export async function story(): Promise<string> {
33
- const run = await testWorkflow(investigate, {
34
- agents: { investigator: [answer("…")] },
35
- participants: {
36
- coordinator: (event) =>
37
- event.kind === "message" && event.asks
38
- ? event.reply("Keep two, and link them.")
39
- : undefined,
40
- },
41
- });
42
- return run.value;
43
- }
44
-
45
- export const sendsFirst: ParticipantScript = async (event) => {
46
- if (event.kind !== "route") return;
47
- const sent = await event.send("Also check the 09:12 deploy.");
48
- if (sent.code !== "delivered") throw new Error(sent.code);
49
- };
50
-
51
- export async function refused(wf: WorkflowContext): Promise<void> {
52
- // @ts-expect-error bindingPath is gone: the socket's place is fixed.
53
- await wf.participants.connect({ key: "coordinator", bindingPath: "/tmp/x" });
54
- }
@@ -1,67 +0,0 @@
1
- // Checked by `tsc --noEmit`, never run: each wrong answer is refused where it is written.
2
- import { defineWorkflow, type OutputSchema } from "@agentswf/contract/workflow";
3
- import Type from "typebox";
4
- import { answer, noAnswer, type Script, testWorkflow } from ".";
5
-
6
- const VERDICT = Type.Union([
7
- Type.Object({ kind: Type.Literal("ready"), summary: Type.String() }),
8
- Type.Object({ kind: Type.Literal("changes-requested"), feedback: Type.Array(Type.String()) }),
9
- ]);
10
- /** A schema from another library, typed by the author surface's carrier. */
11
- const CARRIED = { type: "object" } as OutputSchema<{ count: number }>;
12
-
13
- export const valid: Script[] = [
14
- answer(VERDICT, { kind: "ready", summary: "ok" }),
15
- answer(VERDICT, (turn) =>
16
- turn.n === 1
17
- ? { kind: "changes-requested", feedback: ["more"] }
18
- : { kind: "ready", summary: "ok" },
19
- ),
20
- answer(VERDICT, async (turn) =>
21
- turn.nudge ? noAnswer.failed() : { kind: "ready", summary: "" },
22
- ),
23
- answer(CARRIED, { count: 1 }),
24
- answer(CARRIED, () => ({ count: 2 })),
25
- answer("text"),
26
- answer((turn) => `turn ${turn.n}`),
27
- noAnswer.silent(),
28
- [answer(VERDICT, { kind: "ready", summary: "ok" }), noAnswer.hang(), answer("done")],
29
- ];
30
-
31
- export const invalid: Script[] = [
32
- // @ts-expect-error a missing field
33
- answer(VERDICT, { kind: "ready" }),
34
- // @ts-expect-error a kind the union doesn't have
35
- answer(VERDICT, { kind: "approved", summary: "ok" }),
36
- // @ts-expect-error an extra field
37
- answer(VERDICT, { kind: "ready", summary: "ok", extra: 1 }),
38
- // @ts-expect-error a function's literal must be one of the union's
39
- answer(VERDICT, () => ({ kind: "approved", summary: "ok" })),
40
- // @ts-expect-error the carrier's type
41
- answer(CARRIED, { count: "one" }),
42
- // @ts-expect-error no answer is a script entry of its own, not an answer's value
43
- answer(VERDICT, noAnswer.silent()),
44
- // @ts-expect-error text answers a turn with no schema
45
- answer(42),
46
- ];
47
-
48
- // @ts-expect-error raw JSON where a script expects answer(…)
49
- export const raw: Script = { kind: "ready", summary: "ok" };
50
-
51
- const withEvents = defineWorkflow({
52
- meta: { name: "w", description: "w" },
53
- file: import.meta.path,
54
- events: { ping: { type: "object" } as OutputSchema<{ n: number }> },
55
- run: async (wf) => (await wf.receive()).length,
56
- });
57
- export const tested = () => testWorkflow(withEvents, { id: "k" });
58
- // @ts-expect-error a workflow with events takes its args among its options
59
- export const byArgs = () => testWorkflow(withEvents, null);
60
- const plain = defineWorkflow({
61
- meta: { name: "p", description: "p" },
62
- prepare: ({ argv }) => ({ n: argv.length }),
63
- run: async (_wf, args) => args.n,
64
- });
65
- // @ts-expect-error a workflow that takes args is given them
66
- export const byOptions = () => testWorkflow(plain, { id: "k" });
67
- export const argued = () => testWorkflow(plain, { id: "k", args: { n: 1 } });