@opensoftware/foreman 0.1.0 → 0.3.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/README.md CHANGED
@@ -7,8 +7,9 @@ covers: [modules/foreman/src/**, modules/foreman/scripts/**]
7
7
  Run the factory's workflow for each work item: the stages of `factory.yaml` one after the other, each with its agent
8
8
  through the harness port, with human gates, steering and cancel, on Temporal. A run survives a restart of the
9
9
  foreman, and Temporal's UI shows its history. Decisions: [ADR 0067](../../docs/decisions/0067-foreman.md), on
10
- [ADR 0004](../../docs/decisions/0004-temporal.md), and [ADR 0068](../../docs/decisions/0068-foreman-sandboxes.md) for
11
- sandboxes.
10
+ [ADR 0004](../../docs/decisions/0004-temporal.md), [ADR 0068](../../docs/decisions/0068-foreman-sandboxes.md) for
11
+ sandboxes, [ADR 0069](../../docs/decisions/0069-foreman-checks.md) for checks, and
12
+ [ADR 0070](../../docs/decisions/0070-foreman-gateway-and-take-over.md) for the inference gateway and take over.
12
13
 
13
14
  ## Quick start
14
15
 
@@ -136,6 +137,144 @@ stage then goes like this:
136
137
  `npx -y @opensoftware/sandbox-<name>`. A factory names at most one sandbox adapter. An agent without `sandbox` runs on
137
138
  the foreman's machine in the run's folder. A sandboxed agent reaches no MCP server yet.
138
139
 
140
+ ## Checks
141
+
142
+ A stage can wait for CI: give it `checks`, the names of the checks that must succeed on the run's pull request before
143
+ it starts ([ADR 0069](../../docs/decisions/0069-foreman-checks.md)). This factory merges the pull request only when
144
+ its check `verify` succeeded:
145
+
146
+ ```yaml checks/factory.yaml
147
+ version: 1
148
+ factory: hello
149
+ providers:
150
+ harness:
151
+ acp: "^0"
152
+ forge:
153
+ github: "^0"
154
+ agents:
155
+ plan:
156
+ harness: acp
157
+ model: routes/default
158
+ prompt: ./agents/plan.md
159
+ implement:
160
+ harness: acp
161
+ model: routes/default
162
+ prompt: ./agents/implement-pr.md
163
+ ship:
164
+ harness: acp
165
+ model: routes/default
166
+ prompt: ./agents/ship.md
167
+ workflow:
168
+ - stage: plan
169
+ - stage: implement
170
+ - stage: ship
171
+ checks: [verify]
172
+ ```
173
+
174
+ ```markdown agents/implement-pr.md
175
+ Implement plan.md on a new branch, push it and open a pull request. End with the pull request's link.
176
+ ```
177
+
178
+ ```markdown agents/ship.md
179
+ Merge the pull request.
180
+ ```
181
+
182
+ Serve it as in the quick start, with the forge adapter's token: `export FOREMAN_FORGE_TOKEN=…`. Then:
183
+
184
+ 1. The run's pull request is the last pull request link that an agent of an earlier stage wrote, in its messages or
185
+ in its tool calls' output: `https://<host>/<owner>/<name>/pull/<n>` (GitHub) or `…/pulls/<n>` (Forgejo). The run
186
+ shows it as `pullRequest`, every later stage's prompt ends with `Pull request: <link>`, and `run.completed@v1` lists
187
+ it in `pullRequests`.
188
+ 2. Before the stage `ship` starts, and before its gate if it has one, the foreman runs the
189
+ [forge adapter](../../ports/forge/README.md)'s `pr <owner>/<name> <n>` and reads the named checks. The stage lists
190
+ them under `checks`, each with `name` and `state`.
191
+ 3. Every check `success`: the stage starts. A check `failure` or `error`: the stage is `failed` without running its
192
+ agent, with the `reason` `the check verify ended with failure`, and the run is `failed`. A check `pending`, or one
193
+ the pull request does not have yet: the run and the stage are `blocked`, and the foreman reads the checks again
194
+ every `FOREMAN_CHECK_POLL_MS`. A cancel ends the wait.
195
+ 4. Without a pull request link, the stage is `failed` with the reason `no earlier stage linked a pull request`.
196
+
197
+ The forge adapter is the one the factory names under `providers.forge`; a factory with `checks` names exactly one.
198
+ `FOREMAN_FORGES` maps an adapter's name to its command line; an adapter it does not name runs
199
+ `npx -y @opensoftware/forge-<name>`. It gets `FORGE_URL` and `FORGE_TOKEN` from `FOREMAN_FORGE_URL` and
200
+ `FOREMAN_FORGE_TOKEN`.
201
+
202
+ ## Inference gateway
203
+
204
+ Give the factory's agents one model endpoint with a budget per run: name an
205
+ [inference adapter](../../ports/inference/README.md) under `providers.inference` and its config under `inference`.
206
+ This factory runs the [LiteLLM gateway](../../adapters/inference/litellm/README.md) on Docker and lets a run spend at
207
+ most 5 USD:
208
+
209
+ ```yaml gateway/factory.yaml
210
+ version: 1
211
+ factory: hello
212
+ providers:
213
+ harness:
214
+ acp: "^0"
215
+ inference:
216
+ litellm: "^0"
217
+ inference:
218
+ config: ./inference/litellm.yaml
219
+ budget: 5
220
+ agents:
221
+ plan:
222
+ harness: acp
223
+ model: routes/default
224
+ prompt: ./agents/plan.md
225
+ implement:
226
+ harness: acp
227
+ model: routes/default
228
+ prompt: ./agents/implement.md
229
+ workflow:
230
+ - stage: plan
231
+ - stage: implement
232
+ ```
233
+
234
+ ```yaml inference/litellm.yaml
235
+ model_list:
236
+ - model_name: routes/default
237
+ litellm_params:
238
+ model: anthropic/claude-sonnet-5-5
239
+ api_key: os.environ/OSF_SECRET_ANTHROPIC_API_KEY
240
+ ```
241
+
242
+ Serve it as in the quick start, with `export OSF_SECRET_ANTHROPIC_API_KEY=…` for the adapter. Then:
243
+
244
+ 1. `foreman serve` runs the adapter's `start` with the config, a path relative to `factory.yaml`, before it takes work,
245
+ and `stop` when it stops. A `start` that fails stops the server with exit code 1 and the adapter's message.
246
+ 2. Before a run's first stage, the foreman asks the adapter for the run's key: `key <gateway> <run ID> --budget 5`. The
247
+ gateway refuses the run's calls once it spent its budget. The key stays in the foreman's memory, not in Temporal.
248
+ 3. Each stage's harness adapter gets the gateway in the variables that the harness adapters read: `OPENAI_BASE_URL`
249
+ (the URL `start` printed) and `ANTHROPIC_BASE_URL` (the same without `/v1`), `OPENAI_API_KEY` and
250
+ `ANTHROPIC_API_KEY` (the run's key), and `OPENAI_MODEL` and `ANTHROPIC_MODEL` (the agent's `model`, a routing
251
+ alias of the config). A sandboxed stage gets them in its sandbox's environment.
252
+
253
+ `FOREMAN_INFERENCES` maps an adapter's name to its command line; an adapter it does not name runs
254
+ `npx -y @opensoftware/inference-<name>`. A factory names at most one inference adapter. Without one, the foreman sets
255
+ none of these variables, and each harness adapter calls the model API it is configured for.
256
+
257
+ ## Take over
258
+
259
+ A person can take a running stage over from its agent and hand the run back later
260
+ ([ADR 0070](../../docs/decisions/0070-foreman-gateway-and-take-over.md)):
261
+
262
+ ```sh
263
+ curl -H "Authorization: Bearer dev-token" -H "content-type: application/json" \
264
+ --data '{"type": "take-over"}' http://localhost:8080/v1/runs/run_…/signals
265
+ ```
266
+
267
+ 1. The running stage's agent ends its turn within `FOREMAN_POLL_MS`. A sandboxed stage copies its files out and
268
+ destroys its sandbox, as after every turn.
269
+ 2. The stage is `blocked` with `takenOverAt`, and the run is `blocked` with `folder`: the run's folder on the foreman's
270
+ machine, which holds the stage's files. The stage's `sessionId`, the run's `pullRequest` and the work item are the
271
+ rest of the context.
272
+ 3. The person works in that folder, then posts `{"type": "hand-back"}`. The stage is `succeeded`, and the next stage
273
+ starts from the files in the folder, or the run is `complete` after the last stage.
274
+
275
+ A take-over needs a running stage, and a hand-back a stage that a person took over; otherwise the answer is 409. A
276
+ cancel ends a run that a person took over.
277
+
139
278
  ## Commands
140
279
 
141
280
  | Command | What it does |
@@ -147,17 +286,18 @@ the foreman's machine in the run's folder. A sandboxed agent reaches no MCP serv
147
286
  | Route | What it does |
148
287
  | --- | --- |
149
288
  | `POST /v1/runs` | Start the run of a work item (the [work item](../../contracts/work-item/work-item.schema.json) format). 201 with the new run, 200 with the run the same work item started before, 400 with a problem that names the field. |
150
- | `GET /v1/runs/{runId}` | The run: `state`, `factory`, `factoryVersion`, `startedAt`, `endedAt` and its `stages`. 404 for a run the server does not know. |
151
- | `POST /v1/runs/{runId}/signals` | `{ "type": "steer", "message": "…" }` sends the message to the running stage's agent; `{ "type": "cancel" }` cancels the run. 202 with the run, 409 when the run ended. |
289
+ | `GET /v1/runs/{runId}` | The run: `state`, `factory`, `factoryVersion`, `pullRequest`, `folder`, `startedAt`, `endedAt` and its `stages`. 404 for a run the server does not know. |
290
+ | `POST /v1/runs/{runId}/signals` | `{ "type": "steer", "message": "…" }` sends the message to the running stage's agent; `{ "type": "cancel" }` cancels the run; `{ "type": "take-over" }` and `{ "type": "hand-back" }` hand the running stage to a person and back ([Take over](#take-over)). 202 with the run, 409 when the run ended or the signal does not fit its state. |
152
291
 
153
292
  The OpenAPI of the routes is in [`contracts/openapi/openapi.json`](../../contracts/openapi/openapi.json); the
154
293
  TypeScript type `paths` of `@opensoftware/contracts` types a client. Every route needs `Authorization: Bearer
155
294
  <FOREMAN_API_TOKEN>`, else 401. Errors are `application/problem+json` with `title`, `status` and `detail`.
156
295
 
157
296
  A run's ID is `run_` and 20 hex digits of the SHA-256 of the work item's ID, so a work item has one run. A run is
158
- `running`, `blocked` (at a gate), `complete`, `failed` or `cancelled`. A stage is `pending`, `blocked`, `running`,
159
- `succeeded`, `failed`, `rejected` (by its gate) or `cancelled`; it has its harness `sessionId` once it started and its
160
- `gateId` once its gate was asked.
297
+ `running`, `blocked` (at a gate, waiting for checks, or taken over by a person), `complete`, `failed` or `cancelled`. A stage is `pending`,
298
+ `blocked`, `running`, `succeeded`, `failed`, `rejected` (by its gate) or `cancelled`; it has its harness `sessionId` once
299
+ it started, its `gateId` once its gate was asked, its `checks` once they were read, its `takenOverAt` once a person
300
+ took it over, and a `reason` when it failed for a reason the foreman knows.
161
301
 
162
302
  ## How a run goes
163
303
 
@@ -172,14 +312,18 @@ A run's ID is `run_` and 20 hex digits of the SHA-256 of the work item's ID, so
172
312
  the agent's prompt file, then the work item's ID, title and description. It allows the tool calls the adapter asks
173
313
  about; limit them with the adapter's `--allow`. A turn that ends with `end_turn` succeeds the stage; any other end
174
314
  fails or cancels it, ends the run, and leaves the later stages `pending`. The adapter gets
175
- `OTEL_RESOURCE_ATTRIBUTES` with `osf.run.id`, `osf.work_item.id`, `osf.stage` and `osf.factory.version`.
315
+ `OTEL_RESOURCE_ATTRIBUTES` with `osf.run.id`, `osf.work_item.id`, `osf.stage` and `osf.factory.version`, and the
316
+ factory's inference gateway with the run's key and the agent's model ([Inference gateway](#inference-gateway)).
317
+ - **Checks.** Before a stage with `checks` starts, the foreman waits until those checks succeeded on the run's pull
318
+ request; a failing one fails the run ([Checks](#checks)).
176
319
  - **Gates.** Before a stage with a `gate` starts, the foreman asks gatekeeper at `FOREMAN_GATEKEEPER_URL`
177
320
  ([gatekeeper](../gatekeeper/README.md#server)): the work item's ID, the stage, the summary `<title>: start the
178
321
  stage <stage>`, the origin's link, and the gate's `policy` and `approvers` in `change`. The run is `blocked` until a
179
322
  `gate.decided@v1` for that gate arrives on NATS. `approved` starts the stage; `rejected` or `expired` fails the run.
180
323
  - **Signals.** A steering message reaches the running stage's agent within `FOREMAN_POLL_MS` as a prompt in its
181
324
  session, which the harness port treats as steering. A cancel ends the agent's turn, or the wait at a gate, and the
182
- run is `cancelled`.
325
+ run is `cancelled`. A person takes a running stage over and hands the run back with the signals `take-over` and
326
+ `hand-back` ([Take over](#take-over)).
183
327
  - **Restarts.** The runs live in Temporal's history. A foreman that stops and starts again goes on with them; a stage
184
328
  that ran when it stopped runs again, up to 3 times.
185
329
 
@@ -217,6 +361,11 @@ the minor version. The one-time setup is the same as for contracts
217
361
  | `FOREMAN_TASK_QUEUE` | string | `"foreman"` | serve: the Temporal task queue of the runs' workflows and activities. |
218
362
  | `FOREMAN_HARNESSES` | string | `"{}"` | serve: a JSON object from harness name to the command line of its adapter. A harness it does not name runs npx -y @opensoftware/harness-<name> run. |
219
363
  | `FOREMAN_SANDBOXES` | string | `"{}"` | serve: a JSON object from sandbox adapter name to the command line of the adapter. An adapter it does not name runs npx -y @opensoftware/sandbox-<name>. The factory names its adapter under providers.sandbox, docker by default. |
364
+ | `FOREMAN_FORGES` | string | `"{}"` | serve: a JSON object from forge adapter name to the command line of the adapter. An adapter it does not name runs npx -y @opensoftware/forge-<name>. The factory names its adapter under providers.forge. |
365
+ | `FOREMAN_INFERENCES` | string | `"{}"` | serve: a JSON object from inference adapter name to the command line of the adapter. An adapter it does not name runs npx -y @opensoftware/inference-<name>. The factory names its adapter under providers.inference. |
366
+ | `FOREMAN_FORGE_URL` | string | | serve: the forge adapter's FORGE_URL, for a factory whose stages have checks. Default: the adapter's own default. |
367
+ | `FOREMAN_FORGE_TOKEN` | string | | serve: the forge adapter's FORGE_TOKEN: a token that may read the pull requests and their checks. Never written to a file. |
368
+ | `FOREMAN_CHECK_POLL_MS` | string | `"30000"` | serve: how long a stage that waits for pending checks waits before it reads them again, in milliseconds. |
220
369
  | `FOREMAN_POLL_MS` | string | `"1000"` | serve: how often a running stage looks for steering messages and a cancel, in milliseconds. |
221
370
  | `FOREMAN_GATEKEEPER_URL` | string | | serve: the URL of gatekeeper serve, which a stage with a gate asks for a decision. Required for a factory with gates. |
222
371
  | `FOREMAN_GATEKEEPER_TOKEN` | string | | serve: gatekeeper's API token, GATEKEEPER_API_TOKEN. |
@@ -269,3 +418,25 @@ the minor version. The one-time setup is the same as for contracts
269
418
  | SCN-FORE-039 | implemented | The sandbox adapter is the one the factory names under "providers.sandbox" |
270
419
  | SCN-FORE-040 | implemented | A factory that names two sandbox adapters stops the server at the start |
271
420
  | SCN-FORE-041 | implemented | A stage whose agent declares no sandbox runs on the foreman's machine |
421
+ | SCN-FORE-042 | implemented | A failing check stops the stage that waits for it |
422
+ | SCN-FORE-043 | implemented | A stage starts when its checks succeeded |
423
+ | SCN-FORE-044 | implemented | A pending check holds the stage until it succeeds |
424
+ | SCN-FORE-045 | implemented | A check the pull request does not have yet holds the stage |
425
+ | SCN-FORE-046 | implemented | A stage with checks fails when no earlier stage linked a pull request |
426
+ | SCN-FORE-047 | implemented | A run shows its pull request and publishes it when it ends |
427
+ | SCN-FORE-048 | implemented | A cancel ends a run that waits for a check |
428
+ | SCN-FORE-049 | implemented | A factory with checks but without a forge adapter stops the server at the start |
429
+ | SCN-FORE-050 | implemented | A stage's agent calls the factory's gateway with the run's key |
430
+ | SCN-FORE-051 | implemented | Each stage's agent calls the gateway with its own model |
431
+ | SCN-FORE-052 | implemented | The foreman starts one gateway from the factory's inference config |
432
+ | SCN-FORE-053 | implemented | Each run gets its own key with the factory's budget |
433
+ | SCN-FORE-054 | implemented | A sandboxed stage gets the gateway in its sandbox's environment |
434
+ | SCN-FORE-055 | implemented | The foreman stops the gateway when it stops serving |
435
+ | SCN-FORE-056 | implemented | A gateway that does not start stops the server at the start |
436
+ | SCN-FORE-057 | implemented | A factory with an inference adapter but without its config stops the server at the start |
437
+ | SCN-FORE-058 | implemented | A person takes over the running stage |
438
+ | SCN-FORE-059 | implemented | The next stage starts from the files of the person who handed the run back |
439
+ | SCN-FORE-060 | implemented | A take-over destroys the stage's sandbox and keeps its files |
440
+ | SCN-FORE-061 | implemented | A take-over of a run without a running stage is refused |
441
+ | SCN-FORE-062 | implemented | A hand-back of a run nobody took over is refused |
442
+ | SCN-FORE-063 | implemented | A cancel ends a run a person took over |
@@ -0,0 +1,9 @@
1
+ // The run's pull request through the forge port (ADR 0057, ADR 0069). The adapter is a program; the foreman runs its
2
+ // `pr` command and reads the checks of the pull request's head commit.
3
+ import { adapterRun } from "./program.js";
4
+ /** The checks of the pull request `<repo>#<number>`; a failure names the adapter's error. */
5
+ export async function pullRequestChecks(adapter, env, repo, number) {
6
+ const printed = await adapterRun("forge", adapter, env)("pr", repo, number);
7
+ return JSON.parse(printed).checks ?? [];
8
+ }
9
+ //# sourceMappingURL=forge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forge.js","sourceRoot":"","sources":["../../src/adapters/forge.ts"],"names":[],"mappings":"AAAA,qHAAqH;AACrH,uEAAuE;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAQ1C,6FAA6F;AAC7F,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAiB,EACjB,GAAuC,EACvC,IAAY,EACZ,MAAc;IAEd,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5E,OAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAiC,CAAC,MAAM,IAAI,EAAE,CAAC;AAC3E,CAAC"}
@@ -1,6 +1,7 @@
1
1
  // A stage's agent through the harness port (ADR 0050, ADR 0067): the adapter runs as a program that speaks ACP on
2
2
  // stdio, and the foreman is its client. One session per stage, in the run's folder, with one prompt; a prompt sent
3
- // while the turn runs is a steering message. In a sandbox, the program is the sandbox adapter's exec (ADR 0068).
3
+ // while the turn runs is a steering message. In a sandbox, the program is the sandbox adapter's exec (ADR 0068). The
4
+ // foreman keeps what the agent wrote, to find the run's pull request in it (ADR 0069).
4
5
  import { spawn } from "node:child_process";
5
6
  import { Readable, Writable } from "node:stream";
6
7
  import { client, ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
@@ -12,7 +13,16 @@ export async function startTurn(options) {
12
13
  child.on("exit", () => resolve("exited"));
13
14
  child.on("error", () => resolve("exited"));
14
15
  });
16
+ let transcript = "";
15
17
  const connection = client({ name: "opensoftware-foreman" })
18
+ .onNotification("session/update", ({ params }) => {
19
+ const { update } = params;
20
+ // Message chunks join, so a link split across chunks stays whole.
21
+ if (update.sessionUpdate === "agent_message_chunk" && update.content.type === "text")
22
+ transcript += update.content.text;
23
+ else if (update.sessionUpdate.startsWith("tool_call"))
24
+ transcript += `\n${JSON.stringify(update)}\n`;
25
+ })
16
26
  // The adapter refuses tool kinds outside its --allow list itself; the foreman allows the rest (ADR 0067).
17
27
  .onRequest("session/request_permission", ({ params }) => {
18
28
  const allow = params.options.find((option) => option.kind.startsWith("allow"));
@@ -39,6 +49,7 @@ export async function startTurn(options) {
39
49
  const turn = send(options.prompt).then(({ stopReason }) => stopReason, () => "exited");
40
50
  return {
41
51
  sessionId,
52
+ transcript: () => transcript,
42
53
  done: Promise.race([turn, exited]),
43
54
  steer: (text) => void send(text).catch(() => undefined),
44
55
  cancel: () => void agent.notify("session/cancel", { sessionId }).catch(() => undefined),
@@ -1 +1 @@
1
- {"version":3,"file":"harness.js","sourceRoot":"","sources":["../../src/adapters/harness.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"harness.js","sourceRoot":"","sources":["../../src/adapters/harness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ // The factory's inference gateway through the inference port (ADR 0056, ADR 0070). The adapter is a program; the
2
+ // foreman starts one gateway while it serves, asks it for a key per run, and stops it at the end.
3
+ import { adapterRun, lastLine } from "./program.js";
4
+ /** Starts a gateway from the adapter's config file. */
5
+ export async function startGateway(adapter, config, env) {
6
+ const run = adapterRun("inference", adapter, env);
7
+ const { id, url } = JSON.parse(lastLine(await run("start", config)));
8
+ return {
9
+ url,
10
+ key: async (runId, budget) => lastLine(await run("key", id, runId, ...(budget === undefined ? [] : ["--budget", String(budget)]))),
11
+ stop: async () => void (await run("stop", id)),
12
+ };
13
+ }
14
+ //# sourceMappingURL=inference.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inference.js","sourceRoot":"","sources":["../../src/adapters/inference.ts"],"names":[],"mappings":"AAAA,iHAAiH;AACjH,kGAAkG;AAClG,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAUpD,uDAAuD;AACvD,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAiB,EACjB,MAAc,EACd,GAAuC;IAEvC,MAAM,GAAG,GAAG,UAAU,CAAC,WAAW,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAClD,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAgC,CAAC;IACpG,OAAO;QACL,GAAG;QACH,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAC3B,QAAQ,CAAC,MAAM,GAAG,CAAC,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACtG,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;KAC/C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,21 @@
1
+ // An adapter of a port that is a program (ADR 0054, ADR 0056): the foreman runs one of its commands and reads what it
2
+ // printed.
3
+ import { execFile } from "node:child_process";
4
+ /**
5
+ * Runs commands of the `port`'s adapter and resolves with what it printed, trimmed; a failure names the port, the command
6
+ * and what the adapter wrote to standard error.
7
+ */
8
+ export function adapterRun(port, adapter, env) {
9
+ const [file, ...args] = adapter;
10
+ return (...command) => new Promise((resolve, reject) => {
11
+ execFile(file, [...args, ...command], { env }, (error, stdout, stderr) => {
12
+ if (error)
13
+ reject(new Error(`the ${port} adapter's ${command[0]} failed: ${stderr.trim() || error.message}`));
14
+ else
15
+ resolve(stdout.trim());
16
+ });
17
+ });
18
+ }
19
+ /** The last line an adapter printed. */
20
+ export const lastLine = (printed) => printed.split("\n").pop();
21
+ //# sourceMappingURL=program.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"program.js","sourceRoot":"","sources":["../../src/adapters/program.ts"],"names":[],"mappings":"AAAA,sHAAsH;AACtH,WAAW;AACX,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAiB,EAAE,GAAuC;IACjG,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,OAAgC,CAAC;IACzD,OAAO,CAAC,GAAG,OAAiB,EAAE,EAAE,CAC9B,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACtC,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACvE,IAAI,KAAK;gBAAE,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,IAAI,cAAc,OAAO,CAAC,CAAC,CAAC,YAAY,MAAM,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;;gBACzG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACP,CAAC;AAED,wCAAwC;AACxC,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAU,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAY,CAAC"}
@@ -1,32 +1,20 @@
1
1
  // A stage's sandbox through the sandbox port (ADR 0054, ADR 0068). The adapter is a program; the foreman creates a
2
2
  // sandbox from the spec, copies the run's folder in, runs the harness with exec, copies the folder out and destroys it.
3
- import { execFile } from "node:child_process";
4
3
  import { cpSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
4
  import { tmpdir } from "node:os";
6
5
  import { join, posix } from "node:path";
7
- /** Runs one command of the adapter; a failure names the command and what the adapter wrote to standard error. */
8
- function adapterRun(adapter, env) {
9
- const [file, ...args] = adapter;
10
- return (...command) => new Promise((resolve, reject) => {
11
- execFile(file, [...args, ...command], { env }, (error, stdout, stderr) => {
12
- if (error)
13
- reject(new Error(`the sandbox adapter's ${command[0]} failed: ${stderr.trim() || error.message}`));
14
- else
15
- resolve(stdout.trim());
16
- });
17
- });
18
- }
6
+ import { adapterRun, lastLine } from "./program.js";
19
7
  /**
20
8
  * A new sandbox from the spec for the files of `local`, the run's folder, which it holds at `<working folder>/<name>`
21
9
  * after `copyIn`. The caller destroys it.
22
10
  */
23
11
  export async function createSandbox(adapter, spec, local, name, env) {
24
- const run = adapterRun(adapter, env);
12
+ const run = adapterRun("sandbox", adapter, env);
25
13
  const temporary = mkdtempSync(join(tmpdir(), "foreman-sandbox-"));
26
14
  let id;
27
15
  try {
28
16
  writeFileSync(join(temporary, "spec.json"), JSON.stringify(spec));
29
- id = (await run("create", join(temporary, "spec.json"))).split("\n").pop();
17
+ id = lastLine(await run("create", join(temporary, "spec.json")));
30
18
  }
31
19
  finally {
32
20
  rmSync(temporary, { recursive: true, force: true });
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../src/adapters/sandbox.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../../src/adapters/sandbox.ts"],"names":[],"mappings":"AAAA,mHAAmH;AACnH,wHAAwH;AACxH,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAYpD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAiB,EACjB,IAAiB,EACjB,KAAa,EACb,IAAY,EACZ,GAAuC;IAEvC,MAAM,GAAG,GAAG,UAAU,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAClE,IAAI,EAAU,CAAC;IACf,IAAI,CAAC;QACH,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;QAClE,EAAE,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IACnE,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,OAAO;QACL,KAAK,CAAC,MAAM;YACV,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;YACnD,sGAAsG;YACtG,MAAM,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;YAC1C,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;QAC7D,KAAK,CAAC,OAAO;YACX,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,sBAAsB,CAAC,CAAC,CAAC;YAChE,IAAI,CAAC;gBACH,MAAM,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;gBACpD,MAAM,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBAChD,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACtD,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;QACD,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;KACrD,CAAC;AACJ,CAAC"}
@@ -1,11 +1,18 @@
1
- // The runs of the foreman as Temporal workflows (ADR 0067): start one per work item, read its view, signal it.
1
+ // The runs of the foreman as Temporal workflows (ADR 0067): start one per work item, read its view, signal it: steer,
2
+ // cancel, take over and hand back (ADR 0070).
2
3
  import { UsageError } from "@opensoftware/cli";
3
4
  import { validate } from "@opensoftware/contracts";
4
5
  import { WorkflowExecutionAlreadyStartedError, WorkflowIdReusePolicy, WorkflowNotFoundError, } from "@temporalio/client";
5
- import { firstView, hasEnded, runIdOf, signalOf } from "../domain/run.js";
6
- import { factoryRun, gateSignal, steeringQuery, steerSignal, viewQuery } from "../workflows/run.js";
6
+ import { firstView, hasEnded, runIdOf, signalOf, } from "../domain/run.js";
7
+ import { controlQuery, factoryRun, gateSignal, handBackSignal, steerSignal, takeOverSignal, viewQuery, } from "../workflows/run.js";
7
8
  export class Conflict extends Error {
9
+ title;
10
+ constructor(message, title = "The run ended") {
11
+ super(message);
12
+ this.title = title;
13
+ }
8
14
  }
15
+ const NOT_NOW = "The signal does not fit the run's state";
9
16
  function checked(format, document, what) {
10
17
  const problems = validate(format, document);
11
18
  if (problems.length)
@@ -52,13 +59,24 @@ export function runs(options) {
52
59
  if (signal.type === "steer") {
53
60
  await handle(runId).signal(steerSignal, signal.message);
54
61
  }
62
+ else if (signal.type === "take-over") {
63
+ const index = current.stages.findIndex((stage) => stage.state === "running");
64
+ if (index < 0)
65
+ throw new Conflict(`no stage of ${runId} is running`, NOT_NOW);
66
+ await handle(runId).signal(takeOverSignal, index);
67
+ }
68
+ else if (signal.type === "hand-back") {
69
+ if (!current.stages.some((stage) => stage.state === "blocked" && stage.takenOverAt))
70
+ throw new Conflict(`nobody took over ${runId}`, NOT_NOW);
71
+ await handle(runId).signal(handBackSignal);
72
+ }
55
73
  else {
56
74
  await handle(runId).cancel();
57
75
  }
58
76
  return current;
59
77
  },
60
- async steering(runId) {
61
- return handle(runId).query(steeringQuery);
78
+ async control(runId) {
79
+ return handle(runId).query(controlQuery);
62
80
  },
63
81
  async decided(workItemId, decision) {
64
82
  await handle(runIdOf(workItemId)).signal(gateSignal, decision);
@@ -1 +1 @@
1
- {"version":3,"file":"runs.js","sourceRoot":"","sources":["../../src/commands/runs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runs.js","sourceRoot":"","sources":["../../src/commands/runs.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  // foreman serve: the run API on PORT and a Temporal worker that runs the factory's workflow (ADR 0067). With
2
- // FOREMAN_NATS_URL it also starts runs for work.received@v1 and passes gate.decided@v1 to the waiting runs. It runs
3
- // until SIGTERM or SIGINT.
2
+ // FOREMAN_NATS_URL it also starts runs for work.received@v1 and passes gate.decided@v1 to the waiting runs. A factory
3
+ // with an inference adapter gets its gateway while the server runs (ADR 0070). It runs until SIGTERM or SIGINT.
4
4
  import { timingSafeEqual } from "node:crypto";
5
- import { resolve } from "node:path";
5
+ import { dirname, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { UsageError } from "@opensoftware/cli";
8
8
  import { Client, Connection } from "@temporalio/client";
@@ -10,6 +10,7 @@ import { NativeConnection, Worker } from "@temporalio/worker";
10
10
  import Fastify, {} from "fastify";
11
11
  import { readFactory } from "../adapters/factory.js";
12
12
  import { gatekeeper } from "../adapters/gatekeeper.js";
13
+ import { startGateway } from "../adapters/inference.js";
13
14
  import { openBus, SUBJECTS } from "../adapters/nats.js";
14
15
  import { isoNow } from "../domain/time.js";
15
16
  import { activities } from "../workflows/activities.js";
@@ -27,7 +28,7 @@ function api(work, token) {
27
28
  if (error instanceof UsageError)
28
29
  return problem(reply, 400, "Bad request", error.message);
29
30
  if (error instanceof Conflict)
30
- return problem(reply, 409, "The run ended", error.message);
31
+ return problem(reply, 409, error.title, error.message);
31
32
  return problem(reply, error.statusCode ?? 500, "The request failed", error.message);
32
33
  });
33
34
  app.addHook("onRequest", async (request, reply) => {
@@ -64,7 +65,7 @@ export async function serve(env) {
64
65
  if (!token)
65
66
  throw new UsageError("set FOREMAN_API_TOKEN: the token that callers of the API send");
66
67
  const factoryPath = resolve(env.FOREMAN_FACTORY || "factory.yaml");
67
- await readFactory(factoryPath);
68
+ const { definition } = await readFactory(factoryPath);
68
69
  const now = () => isoNow(env.OSF_NOW);
69
70
  const warn = (text) => process.stderr.write(`foreman: ${text}\n`);
70
71
  const address = env.FOREMAN_TEMPORAL_ADDRESS || "localhost:7233";
@@ -73,38 +74,60 @@ export async function serve(env) {
73
74
  const taskQueue = env.FOREMAN_TASK_QUEUE || "foreman";
74
75
  const pollMs = Number(env.FOREMAN_POLL_MS || 1000);
75
76
  const bus = env.FOREMAN_NATS_URL ? await openBus(env.FOREMAN_NATS_URL, warn) : undefined;
76
- const client = new Client({ connection: await Connection.connect({ address }), namespace });
77
- const work = runs({ client, taskQueue, factory: () => readFactory(factoryPath), now });
78
- const worker = await Worker.create({
79
- connection: await NativeConnection.connect({ address }),
80
- namespace,
81
- taskQueue,
82
- workflowBundle: { codePath: BUNDLE },
83
- activities: activities({
84
- now,
85
- bus,
86
- gatekeeper: gatekeeper(env.FOREMAN_GATEKEEPER_URL, env.FOREMAN_GATEKEEPER_TOKEN),
87
- harnesses: JSON.parse(env.FOREMAN_HARNESSES || "{}"),
88
- sandboxes: JSON.parse(env.FOREMAN_SANDBOXES || "{}"),
89
- data: resolve(env.OSF_DATA || ".foreman"),
90
- pollMs,
91
- steering: (runId) => work.steering(runId),
92
- env,
93
- }),
94
- maxHeartbeatThrottleInterval: pollMs,
95
- defaultHeartbeatThrottleInterval: pollMs,
96
- });
97
- const running = worker.run();
98
- if (bus)
99
- listen(bus, work);
100
- const app = api(work, token);
101
- const port = Number(env.PORT || 8080);
102
- await app.listen({ port, host: "0.0.0.0" });
103
- process.stdout.write(`foreman serves the run API on port ${port}, task queue ${taskQueue} at ${address}\n`);
104
- // Temporal's runtime stops the worker on SIGTERM and SIGINT; the server stops with it.
105
- await running;
106
- await app.close();
107
- await bus?.close();
77
+ const gateway = await gatewayOf(definition.inference, dirname(factoryPath), env);
78
+ // A gateway stops with the server, also when the server cannot start.
79
+ try {
80
+ const client = new Client({ connection: await Connection.connect({ address }), namespace });
81
+ const work = runs({ client, taskQueue, factory: () => readFactory(factoryPath), now });
82
+ const worker = await Worker.create({
83
+ connection: await NativeConnection.connect({ address }),
84
+ namespace,
85
+ taskQueue,
86
+ workflowBundle: { codePath: BUNDLE },
87
+ activities: activities({
88
+ now,
89
+ bus,
90
+ gatekeeper: gatekeeper(env.FOREMAN_GATEKEEPER_URL, env.FOREMAN_GATEKEEPER_TOKEN),
91
+ harnesses: JSON.parse(env.FOREMAN_HARNESSES || "{}"),
92
+ sandboxes: JSON.parse(env.FOREMAN_SANDBOXES || "{}"),
93
+ forges: JSON.parse(env.FOREMAN_FORGES || "{}"),
94
+ forgeEnv: {
95
+ ...(env.FOREMAN_FORGE_URL ? { FORGE_URL: env.FOREMAN_FORGE_URL } : {}),
96
+ ...(env.FOREMAN_FORGE_TOKEN ? { FORGE_TOKEN: env.FOREMAN_FORGE_TOKEN } : {}),
97
+ },
98
+ checkPollMs: Number(env.FOREMAN_CHECK_POLL_MS || 30_000),
99
+ data: resolve(env.OSF_DATA || ".foreman"),
100
+ pollMs,
101
+ control: (runId) => work.control(runId),
102
+ ...(gateway ? { gateway } : {}),
103
+ env,
104
+ }),
105
+ maxHeartbeatThrottleInterval: pollMs,
106
+ defaultHeartbeatThrottleInterval: pollMs,
107
+ });
108
+ const running = worker.run();
109
+ if (bus)
110
+ listen(bus, work);
111
+ const app = api(work, token);
112
+ const port = Number(env.PORT || 8080);
113
+ await app.listen({ port, host: "0.0.0.0" });
114
+ process.stdout.write(`foreman serves the run API on port ${port}, task queue ${taskQueue} at ${address}\n`);
115
+ // Temporal's runtime stops the worker on SIGTERM and SIGINT; the server stops with it.
116
+ await running;
117
+ await app.close();
118
+ await bus?.close();
119
+ }
120
+ finally {
121
+ await gateway?.stop();
122
+ }
108
123
  return { lines: [] };
109
124
  }
125
+ /** Starts the gateway of the factory's inference adapter from its config, a path relative to the factory's folder. */
126
+ async function gatewayOf(inference, folder, env) {
127
+ if (!inference)
128
+ return undefined;
129
+ const lines = JSON.parse(env.FOREMAN_INFERENCES || "{}");
130
+ const adapter = lines[inference.adapter] ?? `npx -y @opensoftware/inference-${inference.adapter}`;
131
+ return startGateway(adapter.split(" "), resolve(folder, inference.config), env);
132
+ }
110
133
  //# sourceMappingURL=serve.js.map