@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.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
package/src/help/emit.txt
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
usage: awf emit --target <file> --id <id> <event> [<json>] [--event-id <id>] [--json]
|
|
2
|
-
awf emit <run> <key>:reply '{"text": …, "author": …}' [--event-id <id>] [--cwd <directory>] [--run-root <directory>] [--json]
|
|
1
|
+
usage: awf emit --target <file> --id <id> <event> [<json>] [--event-id <id>] [--source <name>] [--json]
|
|
2
|
+
awf emit <run> <key>:reply '{"text": …, "author": …}' [--event-id <id>] [--source <name>] [--cwd <directory>] [--run-root <directory>] [--json]
|
|
3
3
|
|
|
4
4
|
Sends an event to the run of a workflow with events that --id names. That run starts if it has
|
|
5
5
|
none and the event is one that starts one, and is woken if it has one: told while it runs,
|
|
6
6
|
continued when it is parked or crashed. The run lives in the project of <file>, the nearest
|
|
7
7
|
directory above it holding .awf/, whoever sends. --event-id names the occurrence: the same id
|
|
8
|
-
again is a duplicate.
|
|
8
|
+
again is a duplicate. --source names the integration that sent it, such as github, which the
|
|
9
|
+
event a wait returns carries.
|
|
9
10
|
|
|
10
11
|
The second form sends a participant's message to the code of a run that is there, <workflow>/<id>
|
|
11
12
|
or an <id> only one workflow has, here or in a project awf knows: <key>:reply, which the workflow
|
|
@@ -15,7 +16,10 @@ arrive before the run connects the participant: it waits for the run's next wait
|
|
|
15
16
|
takes only the workflow's own events.
|
|
16
17
|
|
|
17
18
|
Prints what became of the event, and what was done to the run: accepted, duplicate or refused.
|
|
18
|
-
--json prints one document
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
--json prints one document: the result, or a refusal whose code says why it failed: usage,
|
|
20
|
+
no-run, ambiguous, id, file, event, prepare, project, no-engine, other-engine, unreachable or
|
|
21
|
+
internal-error. Exits 0 for any of the three; 1 when <file> could not be loaded, or the run
|
|
22
|
+
isn't found or can't be read, and nothing is kept, or when the run's attempt could not be
|
|
23
|
+
started (the event is kept, and the next event or awf tick tries again); 2 on a usage error, for
|
|
24
|
+
a workflow with no events, or when the event is wrong for its target: its name, its data, or its
|
|
25
|
+
id.
|
package/src/help/inbox.txt
CHANGED
|
@@ -4,9 +4,11 @@ usage: awf inbox [<run>] [--open] [--workflow <name>] [--as <key>] [--after <cur
|
|
|
4
4
|
What agents said to a participant outside the run, and what it said back: routes, messages,
|
|
5
5
|
answers, questions that closed unanswered, and each attempt's end, oldest first. Without a run,
|
|
6
6
|
every run here that connected one; --workflow narrows that. A run is <workflow>/<id>, or an <id>
|
|
7
|
-
only one workflow has. --after reads on from a cursor the last answer gave; --wait
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
only one workflow has. --after reads on from a cursor the last answer gave; --wait waits up to
|
|
8
|
+
that long, at most 5m, when nothing is new. --open lists the questions still waiting for an
|
|
9
|
+
answer, a snapshot. --json prints one item per line, and when it fails a refusal last whose code
|
|
10
|
+
says why: usage, no-run, ambiguous, id, cursor, no-participant, unreachable, no-engine,
|
|
11
|
+
other-engine or internal-error.
|
|
10
12
|
|
|
11
13
|
Exits 0 on a read that worked, 1 when there is no such run, it has no participant, or the
|
|
12
14
|
cursor is another's, 2 on a usage error, 3 when the run's files could not be read.
|
package/src/help/logs.txt
CHANGED
|
@@ -6,9 +6,11 @@ What a run did and is doing: its events and what its agents did, read from their
|
|
|
6
6
|
files, merged by time. With no run, the newest here. Without --after, the last 40 events; with
|
|
7
7
|
it, what came after, oldest first. Every answer ends with the command that gets only what is
|
|
8
8
|
new. --wait: with nothing to show yet, wait for the first event, at most that long, up to 5m,
|
|
9
|
-
such as 90s
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
such as 90s; --follow prints each event as it comes until the run ends. @<handle> names one
|
|
10
|
+
event, and --full prints its text, with common credential shapes masked; --json carries it
|
|
11
|
+
always.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
With --json, a failure prints a refusal whose code says why: usage, no-run, ambiguous, id,
|
|
14
|
+
cursor, unreachable, no-engine, other-engine or internal-error. Exits 0 on any read that worked,
|
|
15
|
+
1 when there is no such run or the cursor or handle does not fit it, 2 on a usage error, 3 when
|
|
16
|
+
the run's files could not be read.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
usage: awf open <run> <agent> [--no-fork] [--session <id>] [-p [<text>]]
|
|
2
|
+
[--cwd <directory>] [--run-root <directory>] [--json]
|
|
3
|
+
|
|
4
|
+
A run is <workflow>/<id>, or an <id> only one workflow has, here or in a project awf knows.
|
|
5
|
+
Without an agent, it prints the command that opens each of the run's agents.
|
|
6
|
+
Opens a copy of an agent's latest session in its harness's own TUI, where the agent worked: its
|
|
7
|
+
working directory, its home and skills, its sandbox, on the model and effort it ran on, with the
|
|
8
|
+
harness's own permission prompts. The copy is made without asking the model, and is yours: the
|
|
9
|
+
run and the agent's session are unchanged, and awf keeps no record of it.
|
|
10
|
+
|
|
11
|
+
--no-fork opens the session itself: what you say stays with the agent, awf logs shows it, and the
|
|
12
|
+
next attempt resumes it. It is refused while the run is live; while it is open, no attempt starts
|
|
13
|
+
and an event waits, and once the harness exits a run with events is woken for it.
|
|
14
|
+
--session opens another session in the agent's place: a copy an earlier awf open made, as it is;
|
|
15
|
+
or one of the agent's earlier sessions, as its latest would be.
|
|
16
|
+
-p asks one question headless and prints the answer; the text is its argument, or stdin without
|
|
17
|
+
one. It runs as the agent's turns ran, without prompts, in its sandbox when it had one; on
|
|
18
|
+
claude, billed per token headless, it is refused unless the agent ran metered.
|
|
19
|
+
As the harness exits, awf prints the command that opens the same session again.
|
|
20
|
+
|
|
21
|
+
With --json, -p prints {"v": 1, "session", "forkedFrom"?, "reply", "again"}, or a refusal whose
|
|
22
|
+
code says why: usage, no-run, ambiguous, id, no-session, live, opened, unreachable, no-engine,
|
|
23
|
+
other-engine or internal-error. no-session is an agent with no session or operation to open;
|
|
24
|
+
live, --no-fork, or a sandboxed agent, on a running run, or a --no-fork whose session changed as
|
|
25
|
+
it held the run; opened, a run another awf open --no-fork holds, or a sandbox another awf open
|
|
26
|
+
has open; internal-error, a copy or a -p question that failed. A TUI has no JSON: --json without
|
|
27
|
+
-p is a usage error. Exits with the TUI's exit code, or 0 once -p printed its answer; 1 when
|
|
28
|
+
refused or failed; 2 on a usage error, or a -p or a TUI the agent's harness can't give; 3 when
|
|
29
|
+
the run or the agent's place could not be read, or awf itself failed.
|
package/src/help/reply.txt
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
usage: awf reply <run> <agent> [--to <id>] [--as <participant>] [<text>]
|
|
2
2
|
awf send <run> <agent> [--as <participant>] [<text>]
|
|
3
|
-
awf send <run> --as <participant> --event-id <id> [--author <name>] [<text>]
|
|
4
3
|
each takes [--cwd <directory>] [--run-root <directory>] [--json]
|
|
5
4
|
|
|
6
5
|
A run is <workflow>/<id>, or an <id> only one workflow has, here or in a project awf knows.
|
|
@@ -8,11 +7,10 @@ Answers a question an agent asked a participant outside the run, or messages an
|
|
|
8
7
|
over a route the workflow allowed. The text is the argument, or stdin without one. --to names
|
|
9
8
|
the question, as awf inbox prints it; it may be left out when the agent has one open. --as
|
|
10
9
|
names the participant when the workflow connects more than one. Neither waits for an answer:
|
|
11
|
-
it comes back in awf inbox.
|
|
12
|
-
'{"text":…,"author":…}' --event-id <id
|
|
13
|
-
message on a participant to the run's code, the id the message's own, and prints as awf emit
|
|
14
|
-
does; it exits 0 for whatever the run did with it (accepted, duplicate, or refused by a run that
|
|
15
|
-
returned); 1 when the run or participant isn't found, or its attempt didn't start.
|
|
10
|
+
it comes back in awf inbox. A message on a participant to the run's code is sent with awf emit
|
|
11
|
+
<run> <participant>:reply '{"text":…,"author":…}' --event-id <id>.
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
With --json, each prints {"v": 3, "id": …} when delivered, or a refusal whose code says why: the
|
|
14
|
+
participant's, or usage, no-run, ambiguous, id, unreachable, no-engine, other-engine or
|
|
15
|
+
internal-error. Exits 0 when delivered, 1 when refused (the code says why), 2 on a usage error,
|
|
16
|
+
3 when the run could not be reached.
|
package/src/help/run.txt
CHANGED
|
@@ -13,14 +13,20 @@ A new run with --from-stage starts there. A stage before it with no record to re
|
|
|
13
13
|
value from --values, a JSON object by stage name, checked by its result; one that returns
|
|
14
14
|
nothing is passed. Without one the attempt stops, naming the stage and its schema.
|
|
15
15
|
A run is kept in .awf/runs/<workflow>/<id> under --cwd, unless --run-root names another folder
|
|
16
|
-
for .awf/runs.
|
|
16
|
+
for .awf/runs. A workflow that gives file: import.meta.path keeps its runs in its file's project
|
|
17
|
+
unless --run-root names another folder: the nearest directory at or above the file holding .awf/,
|
|
18
|
+
else the file's own, so --continue finds a run a program started there from anywhere.
|
|
17
19
|
A run has no deadline unless --timeout gives it one, per attempt: a duration, a decimal and s, m,
|
|
18
20
|
h or d, such as 20m or 1d.
|
|
19
21
|
A workflow that knows how to present its result prints that; --json prints the full result instead.
|
|
20
|
-
Either way the full result is kept as
|
|
22
|
+
Either way the full result is kept as out.json in the run's folder, beside report.md
|
|
21
23
|
when the workflow writes one. A run that fails or is cancelled once its agents have started keeps
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
out.json too, with what it spent and why it ended; --json prints it. When no run comes of it,
|
|
25
|
+
--json prints a refusal whose code says why: usage, file, prepare, no-id, id, no-run,
|
|
26
|
+
other-args, not-started, unreachable, project, no-engine, other-engine or internal-error;
|
|
27
|
+
--here takes no --json, nor does opening a run of a workflow with events, which prints no
|
|
28
|
+
document. A second Ctrl-C stops awf at once, without it; a continue then reads
|
|
29
|
+
what its agents spent.
|
|
24
30
|
--cwd sets the directory the workflow and its agents work in; it defaults to the current one.
|
|
25
31
|
--sandbox puts every agent of the run in one sandbox, working in --cwd; the file is a JSON spec
|
|
26
32
|
such as {"read": ["/data/request.md"], "srt": {}}, naming srt or docker, whose relative paths are
|
|
@@ -31,10 +37,9 @@ panes; --no-watch leaves it out, and awf still prints the command that shows the
|
|
|
31
37
|
default); origin, the one awf run was typed in; or any other word, the workspace with that label,
|
|
32
38
|
found or made. --keep-panes says whether an agent's pane stays once it is done: never (the
|
|
33
39
|
default), on-failure, when its last turn wasn't answered, or always. A kept pane's harness is left
|
|
34
|
-
running, and yours. Both are kept with the run for every attempt, a woken one too
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
none, and no other.
|
|
40
|
+
running, and yours. Both are kept with the run for every attempt, a woken one too. A run an event
|
|
41
|
+
started has neither, and takes the defaults. A continue may give either again with the run's
|
|
42
|
+
value, or its default where the run had none, and no other.
|
|
38
43
|
--here, run by an agent in a Herdr pane, starts the run in a new tab and has it take that agent's
|
|
39
44
|
session over as one of its agents, from its next turn until the run ends. It prints a line for
|
|
40
45
|
the agent to end its turn with, which is how the run finds the session.
|
package/src/help/runs.txt
CHANGED
|
@@ -7,5 +7,7 @@ number. A run is <workflow>/<id>, or an <id> only one
|
|
|
7
7
|
workflow has. Runs are listed under --cwd's .awf/runs, or --run-root, as awf run keeps them; a
|
|
8
8
|
run named is found there first, then in each project awf has started a run with events in.
|
|
9
9
|
|
|
10
|
-
--json prints one document, the interface for scripts
|
|
11
|
-
|
|
10
|
+
--json prints one document, the interface for scripts: when it fails, a refusal whose code says
|
|
11
|
+
why: usage, no-run, ambiguous, id, unreachable, no-engine, other-engine or internal-error. Exits
|
|
12
|
+
0 on any read that worked, 1 when there is no such run, 2 on a usage error, 3 when the run's
|
|
13
|
+
files could not be read.
|
package/src/help/tick.txt
CHANGED
|
@@ -5,8 +5,13 @@ Wakes every run of a workflow with events under --cwd's .awf/runs, or --run-root
|
|
|
5
5
|
runs lists there, as an event would: continues a parked run whose wait's timer is due, and a
|
|
6
6
|
crashed or failed one until it has failed three times in a row. Runs of other workflows are left
|
|
7
7
|
as they are. --all wakes them in every project awf has started one in, as ~/.awf/projects lists
|
|
8
|
-
them.
|
|
8
|
+
them. A project's runs are woken by its own agentswf: one at another version than this awf runs
|
|
9
|
+
its own awf tick for them. Run it on any schedule; a run is continued once however many wakers
|
|
10
|
+
come at once.
|
|
9
11
|
|
|
10
|
-
Prints a line for each run it woke, and nothing when it woke none
|
|
11
|
-
|
|
12
|
-
usage
|
|
12
|
+
Prints a line for each run it woke, and nothing when it woke none, and one line on stderr
|
|
13
|
+
counting runs with events it can't read, which it doesn't wake; --json prints one document,
|
|
14
|
+
those runs under unreadable, on exit 1 too, or a refusal whose code says why it failed: usage or
|
|
15
|
+
internal-error. Exits 0; 1 when a project is gone, declares agentswf without installing it, or
|
|
16
|
+
its own agentswf failed, or a run could not be woken, the others still woken; 2 on a usage
|
|
17
|
+
error.
|
package/src/herdr-placement.ts
CHANGED
|
@@ -2,46 +2,16 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { mkdir, rename, rmdir, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { HERDR_VERSION, type RunProcess } from "@agentswf/harness";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { isCode } from "./files";
|
|
6
|
+
import { parseSessions } from "./herdr-run-session";
|
|
7
7
|
import { machinePaths } from "./machine";
|
|
8
8
|
|
|
9
|
-
/** Where a layout may open a session awf does not otherwise use: one named `awf-…` it starts. */
|
|
10
|
-
const OWN_SESSION = /^awf(-|$)/;
|
|
11
9
|
const LOCK_POLL_MS = 100;
|
|
12
10
|
/** Held inside a session's queue: a short wait, then made anyway. */
|
|
13
11
|
const LOCK_WAIT_MS = 5_000;
|
|
14
12
|
/** A lock older than this was left by a run that died holding it. */
|
|
15
13
|
const LOCK_STALE_MS = 30_000;
|
|
16
14
|
|
|
17
|
-
/**
|
|
18
|
-
* Whether a session a layout names can be used: undefined when it can, else why not. One named
|
|
19
|
-
* `awf-…` is started, swept and restarted as the run session is; any other must be running. Either
|
|
20
|
-
* must answer, on the Herdr the adapter drives.
|
|
21
|
-
*/
|
|
22
|
-
export async function namedSession(
|
|
23
|
-
name: string,
|
|
24
|
-
deps: RunSessionDeps,
|
|
25
|
-
): Promise<string | undefined> {
|
|
26
|
-
if (OWN_SESSION.test(name)) {
|
|
27
|
-
try {
|
|
28
|
-
await ensureRunSession(name, deps);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
return messageOf(error);
|
|
31
|
-
}
|
|
32
|
-
} else {
|
|
33
|
-
const listed = await deps.run({
|
|
34
|
-
argv: ["herdr", "session", "list", "--json"],
|
|
35
|
-
timeoutMs: 10_000,
|
|
36
|
-
});
|
|
37
|
-
const found = parseSessions(listed.stdout).find((session) => session.name === name);
|
|
38
|
-
if (!found?.running) {
|
|
39
|
-
return "it is not running, and awf starts only sessions named awf-…";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return wrongVersion(name, deps.run);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
15
|
/** Why the session's server is not the Herdr the adapter drives; undefined when it is. */
|
|
46
16
|
async function wrongVersion(session: string, run: RunProcess): Promise<string | undefined> {
|
|
47
17
|
const status = await run({
|
|
@@ -154,7 +124,7 @@ export function workspaceLock(home: string) {
|
|
|
154
124
|
await mkdir(lock);
|
|
155
125
|
return () => rmdir(lock).catch(() => undefined);
|
|
156
126
|
} catch (error) {
|
|
157
|
-
if ((error
|
|
127
|
+
if (!isCode(error, "EEXIST")) throw error;
|
|
158
128
|
}
|
|
159
129
|
const made = await stat(lock).then(
|
|
160
130
|
(found) => found.mtimeMs,
|
package/src/herdr-run-session.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { link, mkdir, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import type { RunProcess } from "@agentswf/harness";
|
|
6
5
|
import { messageOf } from "./errors";
|
|
6
|
+
import { linkNewText } from "./files";
|
|
7
7
|
import {
|
|
8
8
|
liveness,
|
|
9
9
|
type MarkedPane,
|
|
@@ -53,7 +53,7 @@ const READY_POLL_MS = 200;
|
|
|
53
53
|
const READY_WITHIN_MS = 10_000;
|
|
54
54
|
|
|
55
55
|
/** Starts a server that outlives this process; settles once it has started, or could not. */
|
|
56
|
-
|
|
56
|
+
type StartServer = (
|
|
57
57
|
argv: readonly string[],
|
|
58
58
|
options: { cwd: string; env: Record<string, string> },
|
|
59
59
|
) => Promise<void>;
|
|
@@ -87,7 +87,7 @@ export type RunSession = {
|
|
|
87
87
|
kept?: KeptPane[];
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
type KeptPane = { run: string; session: string; paneId: string };
|
|
91
91
|
|
|
92
92
|
/** `AWF_HERDR_SESSION`, else `awf`; refused when Herdr could not use it as a session name. */
|
|
93
93
|
export function runSessionName(environment: Readonly<Record<string, string | undefined>>): string {
|
|
@@ -431,15 +431,7 @@ export function serverEnvironment(
|
|
|
431
431
|
async function quietConfig(home: string): Promise<string> {
|
|
432
432
|
const file = join(machinePaths(home).herdr, "config.toml");
|
|
433
433
|
await mkdir(dirname(file), { recursive: true });
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
await writeFile(temporary, QUIET_CONFIG, { flag: "wx" });
|
|
437
|
-
await link(temporary, file);
|
|
438
|
-
} catch (error) {
|
|
439
|
-
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
440
|
-
} finally {
|
|
441
|
-
await unlink(temporary).catch(() => undefined);
|
|
442
|
-
}
|
|
434
|
+
await linkNewText(file, QUIET_CONFIG);
|
|
443
435
|
return file;
|
|
444
436
|
}
|
|
445
437
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { mkdir, readdir, readFile,
|
|
2
|
+
import { mkdir, readdir, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { isCode, writeJson } from "./files";
|
|
4
5
|
import { machinePaths } from "./machine";
|
|
5
6
|
import { type ProcessProbe, processStart, sameProcess } from "./runs";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
const WORKSPACE_MARK_VERSION = 2;
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* A run's claim on its workspace in a shared Herdr session: the run's process, and when that
|
|
@@ -73,7 +74,7 @@ export async function markWorkspace(
|
|
|
73
74
|
};
|
|
74
75
|
try {
|
|
75
76
|
await mkdir(dir, { recursive: true });
|
|
76
|
-
await
|
|
77
|
+
await writeJson(file, mark);
|
|
77
78
|
} catch {
|
|
78
79
|
return unmarked;
|
|
79
80
|
}
|
|
@@ -85,7 +86,7 @@ export async function markWorkspace(
|
|
|
85
86
|
const write = (next: WorkspaceMark) => {
|
|
86
87
|
if (released) return writing;
|
|
87
88
|
current = next;
|
|
88
|
-
writing = writing.then(() =>
|
|
89
|
+
writing = writing.then(() => writeJson(file, current).catch(() => undefined));
|
|
89
90
|
return writing;
|
|
90
91
|
};
|
|
91
92
|
return {
|
|
@@ -110,17 +111,6 @@ export async function markWorkspace(
|
|
|
110
111
|
};
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
/** Renamed into place, so a reader never sees half a mark and skips a live run. */
|
|
114
|
-
async function writeWhole(file: string, mark: WorkspaceMark): Promise<void> {
|
|
115
|
-
const temporary = `${file}.${randomBytes(4).toString("hex")}.tmp`;
|
|
116
|
-
try {
|
|
117
|
-
await writeFile(temporary, JSON.stringify(mark));
|
|
118
|
-
await rename(temporary, file);
|
|
119
|
-
} finally {
|
|
120
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
114
|
export async function readMarks(
|
|
125
115
|
home: string,
|
|
126
116
|
session: string,
|
|
@@ -163,7 +153,7 @@ export async function readAllMarks(
|
|
|
163
153
|
|
|
164
154
|
/** Rewrites a mark read from `file`, as one sweep leaves it. */
|
|
165
155
|
export function rewriteMark(file: string, mark: WorkspaceMark): Promise<void> {
|
|
166
|
-
return
|
|
156
|
+
return writeJson(file, mark).catch(() => undefined);
|
|
167
157
|
}
|
|
168
158
|
|
|
169
159
|
export function removeMark(file: string): Promise<void> {
|
|
@@ -189,6 +179,6 @@ function processExists(pid: number): boolean {
|
|
|
189
179
|
process.kill(pid, 0);
|
|
190
180
|
return true;
|
|
191
181
|
} catch (error) {
|
|
192
|
-
return (error
|
|
182
|
+
return !isCode(error, "ESRCH");
|
|
193
183
|
}
|
|
194
184
|
}
|