@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 +180 -9
- package/dist/adapters/forge.js +9 -0
- package/dist/adapters/forge.js.map +1 -0
- package/dist/adapters/harness.js +12 -1
- package/dist/adapters/harness.js.map +1 -1
- package/dist/adapters/inference.js +14 -0
- package/dist/adapters/inference.js.map +1 -0
- package/dist/adapters/program.js +21 -0
- package/dist/adapters/program.js.map +1 -0
- package/dist/adapters/sandbox.js +3 -15
- package/dist/adapters/sandbox.js.map +1 -1
- package/dist/commands/runs.js +23 -5
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/serve.js +60 -37
- package/dist/commands/serve.js.map +1 -1
- package/dist/domain/definition.js +30 -4
- package/dist/domain/definition.js.map +1 -1
- package/dist/domain/run.js +70 -13
- package/dist/domain/run.js.map +1 -1
- package/dist/workflows/activities.js +65 -13
- package/dist/workflows/activities.js.map +1 -1
- package/dist/workflows/run.js +25 -4
- package/dist/workflows/run.js.map +1 -1
- package/dist/workflows.bundle.js +34 -5
- package/package.json +4 -4
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),
|
|
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`,
|
|
159
|
-
`succeeded`, `failed`, `rejected` (by its gate) or `cancelled`; it has its harness `sessionId` once
|
|
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"}
|
package/dist/adapters/harness.js
CHANGED
|
@@ -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":"AAAA,kHAAkH;AAClH,mHAAmH;AACnH,
|
|
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"}
|
package/dist/adapters/sandbox.js
CHANGED
|
@@ -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
|
-
|
|
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")))
|
|
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":"AAAA,mHAAmH;AACnH,wHAAwH;AACxH,OAAO,EAAE,
|
|
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"}
|
package/dist/commands/runs.js
CHANGED
|
@@ -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,
|
|
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
|
|
61
|
-
return handle(runId).query(
|
|
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":"AAAA
|
|
1
|
+
{"version":3,"file":"runs.js","sourceRoot":"","sources":["../../src/commands/runs.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/serve.js
CHANGED
|
@@ -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.
|
|
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,
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
connection: await
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|