@agentswf/engine 0.1.0-beta.4 → 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 +35 -22
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +5 -7
- package/src/attempt-output.ts +15 -32
- package/src/attempt-view.ts +34 -33
- 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 +41 -11
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +41 -33
- package/src/deadlines.ts +40 -7
- package/src/decisions/directory.ts +51 -40
- 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 +43 -27
- package/src/emit-command.ts +88 -58
- package/src/emit.ts +254 -56
- 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 +18 -6
- package/src/help/inbox.txt +6 -4
- package/src/help/logs.txt +9 -6
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +30 -11
- 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 +7 -15
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +56 -133
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +40 -57
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +5 -4
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +15 -12
- package/src/logs-command.ts +128 -47
- package/src/message-operation.ts +6 -3
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +31 -28
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +9 -7
- 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 +224 -94
- package/src/operator-runtime.ts +29 -7
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +105 -59
- package/src/progress-view.ts +10 -10
- 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 +45 -28
- package/src/run-commands.ts +43 -115
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +205 -43
- 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 +18 -12
- package/src/run-view.ts +81 -17
- package/src/runs-command.ts +83 -63
- package/src/runs.ts +310 -212
- 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 +28 -29
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +119 -131
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +36 -14
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +73 -26
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +25 -16
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +54 -4
- package/src/workflow-runner.ts +805 -580
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +3 -3
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +121 -53
- package/src/workflow-testing/index.ts +33 -842
- package/src/workflow-testing/run-test.ts +1128 -0
- package/src/workflow-testing/script.ts +74 -45
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -990
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
package/src/workflow-runner.ts
CHANGED
|
@@ -10,9 +10,12 @@ import {
|
|
|
10
10
|
RESERVED_PARTICIPANT_KEYS,
|
|
11
11
|
} from "@agentswf/contract/participant";
|
|
12
12
|
import type {
|
|
13
|
+
AgentOp,
|
|
13
14
|
AgentPaneRecord,
|
|
14
15
|
AgentSkillsRecord,
|
|
15
16
|
RunAccounting,
|
|
17
|
+
RunPanes,
|
|
18
|
+
RunView,
|
|
16
19
|
SandboxRecord,
|
|
17
20
|
SettledDecision,
|
|
18
21
|
SettledOperation,
|
|
@@ -31,7 +34,8 @@ import {
|
|
|
31
34
|
} from "@agentswf/contract/schema";
|
|
32
35
|
import {
|
|
33
36
|
type AbsoluteDeadline,
|
|
34
|
-
type
|
|
37
|
+
type AgentEnded,
|
|
38
|
+
type AgentEndOf,
|
|
35
39
|
type AgentExecution,
|
|
36
40
|
type AgentForkSpec,
|
|
37
41
|
type AgentKey,
|
|
@@ -40,42 +44,49 @@ import {
|
|
|
40
44
|
type AgentRunStructuredSpec,
|
|
41
45
|
type AgentRunTextSpec,
|
|
42
46
|
type AgentStartSpec,
|
|
47
|
+
awfEventOf,
|
|
43
48
|
type Branches,
|
|
44
|
-
type
|
|
49
|
+
type CallerForkSpec,
|
|
45
50
|
type CompactionId,
|
|
46
51
|
type CompactSpec,
|
|
47
52
|
DeadlineExceededError,
|
|
48
53
|
type Duration,
|
|
54
|
+
durationMilliseconds,
|
|
49
55
|
type EmitOutcome,
|
|
50
|
-
type
|
|
51
|
-
type
|
|
56
|
+
type EventLike,
|
|
57
|
+
type ForkRuntime,
|
|
52
58
|
type HarnessAllowance,
|
|
59
|
+
type HarnessKind,
|
|
53
60
|
isEventWorkflow,
|
|
54
61
|
isJsonValue,
|
|
55
62
|
type JsonObject,
|
|
56
63
|
type JsonValue,
|
|
57
64
|
type KeepPane,
|
|
58
|
-
type Matched,
|
|
59
65
|
type MessageAccess,
|
|
66
|
+
type NativeSessionRef,
|
|
60
67
|
type Next,
|
|
61
68
|
nameProblem,
|
|
62
69
|
type OperationRecord,
|
|
63
70
|
type OutputSchema,
|
|
64
71
|
type PaneLayout,
|
|
72
|
+
type PaneSpec,
|
|
73
|
+
type ParticipantDirectory,
|
|
65
74
|
type ParticipantRef,
|
|
66
75
|
type ParticipantSpec,
|
|
67
76
|
type PlacementChoice,
|
|
68
77
|
placementOf,
|
|
78
|
+
positiveDurationMilliseconds,
|
|
69
79
|
type RaceBranches,
|
|
70
|
-
type ReceiveOptions,
|
|
71
80
|
type RunResult,
|
|
72
81
|
type RuntimeSelection,
|
|
73
|
-
type SelectOptions,
|
|
74
82
|
type SettingsSpec,
|
|
75
83
|
type SkillSource,
|
|
84
|
+
type Source,
|
|
76
85
|
type StageOptions,
|
|
77
86
|
type StageSummary,
|
|
78
87
|
type StartedTurn,
|
|
88
|
+
type Taken,
|
|
89
|
+
type Timeout,
|
|
79
90
|
type TimeRead,
|
|
80
91
|
type Timer,
|
|
81
92
|
type TurnId,
|
|
@@ -83,8 +94,8 @@ import {
|
|
|
83
94
|
type WaitOptions,
|
|
84
95
|
type When,
|
|
85
96
|
type WorkflowContext,
|
|
86
|
-
type WorkflowDefinition,
|
|
87
97
|
type WorkflowEvent,
|
|
98
|
+
type WorkflowMeta,
|
|
88
99
|
} from "@agentswf/contract/workflow";
|
|
89
100
|
import {
|
|
90
101
|
type AgentSkills,
|
|
@@ -92,7 +103,8 @@ import {
|
|
|
92
103
|
findHarness,
|
|
93
104
|
HARNESS_NAMES,
|
|
94
105
|
type Harness,
|
|
95
|
-
|
|
106
|
+
type HarnessHomes,
|
|
107
|
+
OPERATOR_HOMES,
|
|
96
108
|
settingsRefusal,
|
|
97
109
|
skillsLayout,
|
|
98
110
|
} from "@agentswf/harness";
|
|
@@ -121,7 +133,7 @@ import {
|
|
|
121
133
|
import type { CoordinatorPort } from "./coordinator-port";
|
|
122
134
|
import {
|
|
123
135
|
assertDeadline,
|
|
124
|
-
|
|
136
|
+
boundedDeadline,
|
|
125
137
|
deadlineWithin,
|
|
126
138
|
earlierDeadline,
|
|
127
139
|
harnessDeadline,
|
|
@@ -142,15 +154,6 @@ import {
|
|
|
142
154
|
inTimeoutHandler,
|
|
143
155
|
type KeepAlive,
|
|
144
156
|
} from "./keep-alive";
|
|
145
|
-
import {
|
|
146
|
-
AT_DUE,
|
|
147
|
-
type RestoredTurn,
|
|
148
|
-
RunMailbox,
|
|
149
|
-
RunParked,
|
|
150
|
-
recoverTurns,
|
|
151
|
-
type TurnEnd,
|
|
152
|
-
unrunUsage,
|
|
153
|
-
} from "./mailbox";
|
|
154
157
|
import { superviseMessageOperation } from "./message-operation";
|
|
155
158
|
import { canAwaitReply } from "./messaging/limits";
|
|
156
159
|
import type { HolderRequest } from "./messaging/messaging";
|
|
@@ -165,9 +168,19 @@ import {
|
|
|
165
168
|
type SupervisedOutcome,
|
|
166
169
|
superviseOperation,
|
|
167
170
|
} from "./operation-liveness";
|
|
168
|
-
import {
|
|
171
|
+
import { type EarlierOps, OpIds, writeScreen } from "./ops";
|
|
172
|
+
import {
|
|
173
|
+
besideOf,
|
|
174
|
+
checkPaneOptions,
|
|
175
|
+
keepsPane,
|
|
176
|
+
type PaneAsked,
|
|
177
|
+
paneAsked,
|
|
178
|
+
storedPane,
|
|
179
|
+
} from "./pane-layout";
|
|
169
180
|
import { createResultSlotRegistry, type ResultSlotRegistry } from "./result-slots";
|
|
170
181
|
import { RunEventLog } from "./run-event-log";
|
|
182
|
+
import { type RunLines, readRunLines } from "./run-lines";
|
|
183
|
+
import { branchOf, matches, type RestoredTurn } from "./run-policy/waits";
|
|
171
184
|
import {
|
|
172
185
|
type AgentProgress,
|
|
173
186
|
type GroupProgress,
|
|
@@ -181,7 +194,7 @@ import {
|
|
|
181
194
|
type OperationTags,
|
|
182
195
|
type RunLedger,
|
|
183
196
|
} from "./run-usage";
|
|
184
|
-
import {
|
|
197
|
+
import { endLogLine, readAttempts, workDir } from "./runs";
|
|
185
198
|
import { type CredentialLocks, placeCarried, seedHome } from "./sandbox-homes";
|
|
186
199
|
import {
|
|
187
200
|
RUN_SANDBOX_ONLY,
|
|
@@ -189,45 +202,62 @@ import {
|
|
|
189
202
|
type RunSandboxOptions,
|
|
190
203
|
type SeatedAgent,
|
|
191
204
|
} from "./sandboxes";
|
|
205
|
+
import { SessionLines, type SessionPlace } from "./session-lines";
|
|
192
206
|
import { QUOTING_RULE, simpleInput, takesText, usageLine } from "./simple-input";
|
|
193
207
|
import { placeSkills, RunSkills, readSkillSources } from "./skills/run-skills";
|
|
194
|
-
import { asSource,
|
|
208
|
+
import { asSource, branchValue, isSignal, presented, signalOf, source, takeOf } from "./sources";
|
|
209
|
+
import { keepSpend, settleEarlier } from "./spend";
|
|
195
210
|
import { StageLedger, type StageProgress } from "./stage-ledger";
|
|
196
211
|
import { FromStageUnreached } from "./stopped";
|
|
197
212
|
import { inToolHandler, outsideTools, refusedInTool, ToolCalls, toolsLesson } from "./tools";
|
|
213
|
+
import { unrunUsage } from "./turn-ends";
|
|
214
|
+
import { RunParked, RunWaits, type TurnEnd } from "./waits";
|
|
198
215
|
import type { EventWorkflow } from "./workflow-loader";
|
|
199
216
|
|
|
200
217
|
export { WorkflowCancelledError } from "./deadlines";
|
|
201
218
|
|
|
219
|
+
/** What the runner needs of a workflow: its meta and its run. */
|
|
220
|
+
export type RunnableWorkflow<Args extends JsonValue, Result extends JsonValue> = {
|
|
221
|
+
readonly meta: WorkflowMeta;
|
|
222
|
+
run(context: WorkflowContext, args: Args): Promise<Result>;
|
|
223
|
+
};
|
|
224
|
+
|
|
202
225
|
export type RunWorkflowOptions = {
|
|
203
226
|
/** Internal timing policy; workflow authors use the operation deadline. */
|
|
204
227
|
livenessPolicy?: LivenessPolicy;
|
|
205
|
-
/** A `keepAlive`
|
|
228
|
+
/** A `keepAlive` that gives no `window`; absent, an hour. A test's is short. */
|
|
206
229
|
defaultWindowMs?: number;
|
|
207
230
|
/** The run's time, which its timers and `wf.now()` read; absent, the clock's. A test's is virtual. */
|
|
208
231
|
now?: () => number;
|
|
232
|
+
/** Where the control plane's socket directory is made; `/tmp` unless a test watches its own. */
|
|
233
|
+
socketRoot?: string;
|
|
209
234
|
runRoot: string;
|
|
210
235
|
/** The run this is an attempt of, whose folder the operator claimed under `runRoot`. */
|
|
211
236
|
run: {
|
|
212
237
|
dir: string;
|
|
213
238
|
id: string;
|
|
214
|
-
attempt
|
|
239
|
+
/** The attempt's writer on `run.jsonl`, which its claim made; its `attempt` is this attempt. */
|
|
240
|
+
lines: RunLines;
|
|
241
|
+
/** What the claim's listing of `ops/` found: each key's last `n`, numbered on from. */
|
|
242
|
+
ops: EarlierOps;
|
|
215
243
|
/** How the attempt is named to a person: its Herdr workspace's label. */
|
|
216
244
|
label?: string;
|
|
217
245
|
/** `--from-stage`: the stage this attempt starts at, reusing those before it. */
|
|
218
246
|
fromStage?: string;
|
|
219
247
|
/** `--values`: the values of stages before `fromStage` that have no record to reuse. */
|
|
220
248
|
values?: ReadonlyMap<string, JsonValue>;
|
|
249
|
+
/** `--workspace` and `--keep-panes`, as the run's record keeps them for every attempt. */
|
|
250
|
+
panes?: RunPanes;
|
|
221
251
|
/** A run of a workflow with `events`. */
|
|
222
252
|
events?: true;
|
|
223
|
-
/** With `events`: runs `work` holding the run's lock, as every
|
|
253
|
+
/** With `events`: runs `work` holding the run's lock, as every sender appends to `in.jsonl`. */
|
|
224
254
|
locked?<T>(work: () => Promise<T>): Promise<T>;
|
|
225
255
|
/**
|
|
226
256
|
* With `events`: false when the attempt before this one has no ending, its process gone with its
|
|
227
257
|
* agents perhaps still running: its sessions aren't reopened until they are stopped (Q17).
|
|
228
258
|
*/
|
|
229
259
|
resumeSessions?: boolean;
|
|
230
|
-
/** With `events`: calls `nudge` whenever awf tells the run
|
|
260
|
+
/** With `events`: calls `nudge` whenever awf tells the run it has a new event. */
|
|
231
261
|
listenNudges?(nudge: () => void): void;
|
|
232
262
|
/**
|
|
233
263
|
* Writes the attempt's `parked` ending, which the run's wait calls holding the run's lock: an
|
|
@@ -244,7 +274,7 @@ export type RunWorkflowOptions = {
|
|
|
244
274
|
deadline?: AbsoluteDeadline;
|
|
245
275
|
cwd?: string;
|
|
246
276
|
onLog?: (message: string, fields?: JsonObject) => void;
|
|
247
|
-
/** The operator hands the session back once
|
|
277
|
+
/** The operator hands the session back once close() has released it. */
|
|
248
278
|
onCallerStopped?: (reason: string) => Promise<void>;
|
|
249
279
|
signal?: AbortSignal;
|
|
250
280
|
/** The sandbox providers agents may run in; without them, a sandboxed agent is refused. */
|
|
@@ -304,7 +334,7 @@ export type SettledRun = {
|
|
|
304
334
|
sandboxes?: SandboxRecord[];
|
|
305
335
|
/** Each agent's skills; absent when no agent opened. */
|
|
306
336
|
skills?: AgentSkillsRecord[];
|
|
307
|
-
/** Where each
|
|
337
|
+
/** Where each TUI agent's pane went; absent when the run placed none. */
|
|
308
338
|
panes?: AgentPaneRecord[];
|
|
309
339
|
/** Every decision the run asked, in the order asked; absent when it asked none. */
|
|
310
340
|
decisions?: SettledDecision[];
|
|
@@ -392,7 +422,7 @@ type AgentEntry = {
|
|
|
392
422
|
* `undefined` when the channel never opened; the agent state carries the reason.
|
|
393
423
|
*/
|
|
394
424
|
channel: Promise<ResultChannel | undefined>;
|
|
395
|
-
/** Why `
|
|
425
|
+
/** Why `close()` ended it; its key is not opened again. */
|
|
396
426
|
stopped?: string;
|
|
397
427
|
stopping?: Promise<boolean>;
|
|
398
428
|
stopComplete?: boolean;
|
|
@@ -416,12 +446,12 @@ type AgentIdentity = {
|
|
|
416
446
|
/** Its skills' sources, checked; absent when it has the operator's. */
|
|
417
447
|
skills?: SkillSource[];
|
|
418
448
|
instructions?: string;
|
|
419
|
-
labels?: AgentOpenSpec["labels"];
|
|
420
449
|
/** The agent it was forked from, and the spec it was forked with. */
|
|
421
|
-
forkedFrom?: { parent: AgentKey; spec:
|
|
422
|
-
/** As written; a reopen compares
|
|
423
|
-
|
|
424
|
-
|
|
450
|
+
forkedFrom?: { parent: AgentKey; spec: CallerForkSpec };
|
|
451
|
+
/** As written; a reopen compares it so, never where the pane ended up. */
|
|
452
|
+
pane?: PaneSpec;
|
|
453
|
+
/** What its host is asked for its pane: it, with the operator's flags applied. */
|
|
454
|
+
asked?: PaneAsked;
|
|
425
455
|
};
|
|
426
456
|
|
|
427
457
|
const CLEANUP_GRACE_MILLISECONDS = 5_000;
|
|
@@ -434,7 +464,7 @@ const FORK_TIMEOUT_MS = 60_000;
|
|
|
434
464
|
const scopes = new AsyncLocalStorage<ExecutionScope>();
|
|
435
465
|
|
|
436
466
|
export async function runWorkflow<Args extends JsonValue, Result extends JsonValue>(
|
|
437
|
-
definition:
|
|
467
|
+
definition: RunnableWorkflow<Args, Result>,
|
|
438
468
|
args: Args,
|
|
439
469
|
options: RunWorkflowOptions,
|
|
440
470
|
): Promise<WorkflowRunResult<Result>> {
|
|
@@ -447,25 +477,50 @@ export async function runWorkflow<Args extends JsonValue, Result extends JsonVal
|
|
|
447
477
|
* handle's `result`'s.
|
|
448
478
|
*/
|
|
449
479
|
export async function startWorkflow<Args extends JsonValue, Result extends JsonValue>(
|
|
450
|
-
definition:
|
|
480
|
+
definition: RunnableWorkflow<Args, Result>,
|
|
451
481
|
args: Args,
|
|
452
482
|
options: RunWorkflowOptions,
|
|
453
483
|
): Promise<WorkflowRunHandle<Result>> {
|
|
454
|
-
|
|
484
|
+
// Cancellation wins when the deadline has passed too, as runUntilStopped has it.
|
|
455
485
|
if (options.signal?.aborted) throw new WorkflowCancelledError(options.signal.reason);
|
|
486
|
+
assertDeadline(options.deadline);
|
|
456
487
|
// Before anything opens, so a refusal leaves nothing to close.
|
|
457
488
|
const withEvents = isEventWorkflow(definition);
|
|
458
489
|
const locked = options.run.locked ?? (<T>(work: () => Promise<T>) => work());
|
|
459
|
-
if (withEvents &&
|
|
460
|
-
throw new Error(
|
|
461
|
-
|
|
462
|
-
|
|
490
|
+
if (withEvents && options.run.values)
|
|
491
|
+
throw new Error(
|
|
492
|
+
"a run of a workflow with events takes no --values: before --from-stage, a stage with no record runs",
|
|
493
|
+
);
|
|
494
|
+
const { dir: runDir, id: runId } = options.run;
|
|
495
|
+
const { attempt } = options.run.lines;
|
|
463
496
|
const cwd = options.cwd ?? process.cwd();
|
|
464
|
-
// Before anything opens: a
|
|
465
|
-
|
|
466
|
-
|
|
497
|
+
// Before anything opens: a line this awf can't read refuses the attempt.
|
|
498
|
+
const { lines: written, view: recorded } = await readRunLines(runDir);
|
|
499
|
+
// The version each earlier attempt ran, which a line it wrote is reused under.
|
|
500
|
+
const earlier = await readAttempts({ dir: runDir });
|
|
501
|
+
// While no agent of this attempt can resume a session an earlier one's spend is still in.
|
|
502
|
+
await settleEarlier(runDir, {
|
|
503
|
+
attempt,
|
|
504
|
+
attempts: earlier,
|
|
505
|
+
lines: written,
|
|
506
|
+
accounting: options.runtime.host.accounting,
|
|
507
|
+
now: Date.now(),
|
|
508
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
509
|
+
}).then(
|
|
510
|
+
({ failed }) => {
|
|
511
|
+
for (const each of failed) options.onLog?.(`awf: spend.json not written for ${each}`);
|
|
512
|
+
},
|
|
513
|
+
(error: unknown) =>
|
|
514
|
+
options.onLog?.(`awf: earlier attempts' spend was not settled: ${messageOf(error)}`),
|
|
515
|
+
);
|
|
516
|
+
const startedAt = Date.now();
|
|
517
|
+
const versions = new Map(
|
|
518
|
+
earlier.flatMap(({ attempt: n, workflowVersion }) =>
|
|
519
|
+
workflowVersion === undefined ? [] : [[n, workflowVersion] as const],
|
|
520
|
+
),
|
|
521
|
+
);
|
|
467
522
|
// The log is observation: one that cannot be written says so as it is, and fails nothing.
|
|
468
|
-
await
|
|
523
|
+
await endLogLine(runDir).catch(() => undefined);
|
|
469
524
|
const slots = createResultSlotRegistry({
|
|
470
525
|
runDir,
|
|
471
526
|
...(options.livenessPolicy
|
|
@@ -475,7 +530,10 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
475
530
|
}
|
|
476
531
|
: {}),
|
|
477
532
|
});
|
|
478
|
-
const control = await startResultControlPlane({
|
|
533
|
+
const control = await startResultControlPlane({
|
|
534
|
+
slots,
|
|
535
|
+
...(options.socketRoot ? { socketRoot: options.socketRoot } : {}),
|
|
536
|
+
});
|
|
479
537
|
let host: AgentRunHost;
|
|
480
538
|
const openingHost = Promise.resolve().then(() =>
|
|
481
539
|
options.runtime.host.openRun({
|
|
@@ -512,8 +570,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
512
570
|
});
|
|
513
571
|
};
|
|
514
572
|
const warn = say("warning");
|
|
515
|
-
|
|
516
|
-
const turns: TurnRecord[] = [];
|
|
573
|
+
const sessionLines = new SessionLines({ lines: options.run.lines, failed: (text) => warn(text) });
|
|
517
574
|
const ledger = createRunLedger({ accounting: options.runtime.host.accounting, startedAt });
|
|
518
575
|
const recordTurn: RecordTurn = (outcome, kind) => {
|
|
519
576
|
const turn: TurnRecord = {
|
|
@@ -524,18 +581,16 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
524
581
|
callPath: outcome.usage.callPath,
|
|
525
582
|
...pick(outcome.usage, TURN_OPERATION_FIELDS),
|
|
526
583
|
};
|
|
527
|
-
turns.push(turn);
|
|
528
584
|
events.turn(turn);
|
|
529
585
|
};
|
|
530
586
|
const stages = new StageLedger({
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
587
|
+
lines: options.run.lines,
|
|
588
|
+
run: recorded,
|
|
589
|
+
versions,
|
|
534
590
|
...(options.run.fromStage === undefined ? {} : { fromStage: options.run.fromStage }),
|
|
535
591
|
...(options.run.values === undefined ? {} : { values: options.run.values }),
|
|
536
592
|
...(definition.meta.version === undefined ? {} : { workflowVersion: definition.meta.version }),
|
|
537
593
|
...(withEvents ? { byContent: true } : {}),
|
|
538
|
-
turns: () => turns,
|
|
539
594
|
event: (body, at) => events.event(body, at),
|
|
540
595
|
});
|
|
541
596
|
const progress = new RunProgress();
|
|
@@ -549,6 +604,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
549
604
|
cwd,
|
|
550
605
|
deadline: options.deadline,
|
|
551
606
|
log: (message) => warn(message),
|
|
607
|
+
opened: (record) => events.event({ kind: "sandbox-opened", ...record }),
|
|
552
608
|
});
|
|
553
609
|
if (sandboxes.hasRunSandbox) {
|
|
554
610
|
try {
|
|
@@ -563,9 +619,11 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
563
619
|
throw new Error(`the run's sandbox did not open: ${messageOf(error)}`);
|
|
564
620
|
}
|
|
565
621
|
}
|
|
622
|
+
const ops = new OpIds(runDir, options.run.lines.attempt, options.run.ops);
|
|
566
623
|
const decisions = new RunDecisions({
|
|
567
624
|
...(options.decisions ? { installation: options.decisions } : {}),
|
|
568
625
|
runDir,
|
|
626
|
+
ops,
|
|
569
627
|
event: (body) => events.event(body),
|
|
570
628
|
});
|
|
571
629
|
const skills = new RunSkills({
|
|
@@ -576,28 +634,33 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
576
634
|
const fatal = Promise.withResolvers<never>();
|
|
577
635
|
void fatal.promise.catch(() => undefined);
|
|
578
636
|
const now = options.now ?? Date.now;
|
|
579
|
-
//
|
|
580
|
-
// runs again (Q6); the turns whose ends no wait took are their agents' turns again.
|
|
581
|
-
const restoredTurns = withEvents ? await recoverTurns(runDir, attempt, locked, now) : undefined;
|
|
582
|
-
// An event run's agents reopen on the sessions its earlier attempts' turns ran on.
|
|
637
|
+
// An event run's agents reopen on the sessions its earlier attempts recorded.
|
|
583
638
|
const keptSessions =
|
|
584
639
|
withEvents && options.run.resumeSessions !== false
|
|
585
|
-
? keptSessionsOf(
|
|
640
|
+
? keptSessionsOf(recorded.sessions)
|
|
586
641
|
: undefined;
|
|
587
|
-
// Every run's timers
|
|
642
|
+
// Every run's timers are its own lines; a run of a workflow with events takes its events with them.
|
|
588
643
|
// A plain run without `park` waits for its timers in its process.
|
|
589
|
-
const
|
|
644
|
+
const { park } = options.run;
|
|
645
|
+
const waits = new RunWaits({
|
|
590
646
|
runDir,
|
|
591
|
-
|
|
647
|
+
lines: options.run.lines,
|
|
592
648
|
locked,
|
|
593
649
|
now,
|
|
594
|
-
...(
|
|
650
|
+
...(park
|
|
595
651
|
? {
|
|
596
652
|
parking: {
|
|
597
|
-
idle: (
|
|
653
|
+
idle: () => owner.idle(),
|
|
598
654
|
timeout: (signal) => owner.timeout(signal),
|
|
599
655
|
liveUntil: () => owner.liveUntil(),
|
|
600
|
-
park:
|
|
656
|
+
park: async (wait) => {
|
|
657
|
+
// No wake carries `--from-stage`: parked short of it, the redo would be lost unsaid.
|
|
658
|
+
const unreached = stages.fromStageUnreached;
|
|
659
|
+
if (unreached === undefined) return park(wait);
|
|
660
|
+
const stop = new FromStageUnreached(unreached, stages.recorded);
|
|
661
|
+
fatal.reject(stop);
|
|
662
|
+
throw stop;
|
|
663
|
+
},
|
|
601
664
|
...(options.run.live ? { live: options.run.live } : {}),
|
|
602
665
|
...(options.run.skip ? { skip: options.run.skip } : {}),
|
|
603
666
|
},
|
|
@@ -605,22 +668,28 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
605
668
|
: {}),
|
|
606
669
|
});
|
|
607
670
|
await waits.open();
|
|
608
|
-
const
|
|
609
|
-
if (
|
|
610
|
-
//
|
|
671
|
+
const eventWaits = withEvents ? waits : undefined;
|
|
672
|
+
if (eventWaits) options.run.listenNudges?.(() => eventWaits.nudge());
|
|
673
|
+
// A turn an earlier attempt started whose end no wait took is its agent's turn again (the claim
|
|
674
|
+
// ended any a crash left running).
|
|
675
|
+
const restoredTurns = eventWaits?.restored;
|
|
676
|
+
// The waits' parking asks the owner, made after it: by then it is.
|
|
611
677
|
const owner: WorkflowOwner = new WorkflowOwner({
|
|
612
|
-
...(
|
|
678
|
+
...(eventWaits ? { eventWaits } : {}),
|
|
613
679
|
waits,
|
|
614
680
|
...(keptSessions ? { keptSessions } : {}),
|
|
681
|
+
sessionLines,
|
|
615
682
|
...(restoredTurns ? { restoredTurns } : {}),
|
|
616
683
|
failRun: (error) => fatal.reject(error),
|
|
617
684
|
decisions,
|
|
685
|
+
ops,
|
|
618
686
|
...(options.allowance ? { allowance: options.allowance } : {}),
|
|
619
687
|
sandboxes,
|
|
620
688
|
skills,
|
|
621
689
|
runDir,
|
|
622
690
|
locks,
|
|
623
691
|
environment,
|
|
692
|
+
...(options.sandboxes?.homes ? { homes: options.sandboxes.homes } : {}),
|
|
624
693
|
progress,
|
|
625
694
|
recordTurn,
|
|
626
695
|
stages,
|
|
@@ -644,6 +713,8 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
644
713
|
coordinators: options.coordinators ?? [],
|
|
645
714
|
emit: options.emit ?? machineEmitter,
|
|
646
715
|
workflow: definition.meta.name,
|
|
716
|
+
declared: new Set(withEvents ? Object.keys(definition.events) : []),
|
|
717
|
+
...(options.run.panes ? { panes: options.run.panes } : {}),
|
|
647
718
|
});
|
|
648
719
|
const stopped = new AbortController();
|
|
649
720
|
// Aborted by a stop that comes after the body ended; the one that ended it does not cut short the
|
|
@@ -683,7 +754,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
683
754
|
failed = true;
|
|
684
755
|
failure = error;
|
|
685
756
|
}
|
|
686
|
-
if (
|
|
757
|
+
if (eventWaits && !eventWaits.parked)
|
|
687
758
|
await owner.endStartedTurns(failed ? "the workflow failed" : "the workflow returned");
|
|
688
759
|
// Once `parked` is written, the attempt is parked, whatever the code did after.
|
|
689
760
|
if (waits.parked) {
|
|
@@ -731,12 +802,21 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
731
802
|
|
|
732
803
|
// Before the wait for session files, which is bookkeeping rather than the run's own time.
|
|
733
804
|
const finishedAt = new Date().toISOString();
|
|
805
|
+
// A session that moved as its agent closed is recorded before the attempt ends.
|
|
806
|
+
owner.recordSessions();
|
|
807
|
+
await sessionLines.flushed();
|
|
734
808
|
await stages
|
|
735
809
|
.close()
|
|
736
|
-
.catch((error) => warn(`awf: a stage
|
|
810
|
+
.catch((error) => warn(`awf: a stage's line was not written: ${messageOf(error)}`));
|
|
737
811
|
await events.close();
|
|
738
812
|
const usage = await ledger.settle(reading.signal);
|
|
739
813
|
signal.removeEventListener("abort", cutReading);
|
|
814
|
+
// What a stop or the read's deadline left unread is the next attempt's to read again.
|
|
815
|
+
const reader = options.runtime.host.accounting;
|
|
816
|
+
const failedSpend = await keepSpend(runDir, attempt, usage, {
|
|
817
|
+
reads: (execution) => reader !== undefined && (reader.canRead?.(execution) ?? true),
|
|
818
|
+
}).catch((error: unknown) => [messageOf(error)]);
|
|
819
|
+
for (const each of failedSpend) options.onLog?.(`awf: spend.json not written for ${each}`);
|
|
740
820
|
const times = { startedAt: new Date(startedAt).toISOString(), finishedAt };
|
|
741
821
|
const opened = sandboxes.records();
|
|
742
822
|
const given = skills.records();
|
|
@@ -878,6 +958,8 @@ class WorkflowOwner {
|
|
|
878
958
|
readonly #cleanupFailures: Error[] = [];
|
|
879
959
|
readonly context: WorkflowContext;
|
|
880
960
|
readonly #agents = new Map<string, AgentEntry>();
|
|
961
|
+
/** The skills each agent's harness was given, as placed: what its session lines record. */
|
|
962
|
+
readonly #givenSkills = new Map<string, AgentSkills>();
|
|
881
963
|
/** Each agent once it has opened, by key: what messaging asks about, and what a ref must be. */
|
|
882
964
|
readonly #logical = new Map<string, LogicalAgent>();
|
|
883
965
|
readonly #messaging: Messaging;
|
|
@@ -910,7 +992,9 @@ class WorkflowOwner {
|
|
|
910
992
|
defaultWindowMs?: number;
|
|
911
993
|
locks: CredentialLocks;
|
|
912
994
|
environment: Readonly<Record<string, string | undefined>>;
|
|
995
|
+
homes?: HarnessHomes;
|
|
913
996
|
decisions: RunDecisions;
|
|
997
|
+
ops: OpIds;
|
|
914
998
|
allowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
915
999
|
stages: StageLedger;
|
|
916
1000
|
runId: string;
|
|
@@ -932,19 +1016,25 @@ class WorkflowOwner {
|
|
|
932
1016
|
onCallerStopped?: RunWorkflowOptions["onCallerStopped"];
|
|
933
1017
|
awaitReply: NonNullable<RunWorkflowOptions["awaitReply"]>;
|
|
934
1018
|
coordinators: readonly CoordinatorPort[];
|
|
935
|
-
/** A run of a workflow with `events`: its
|
|
936
|
-
|
|
1019
|
+
/** A run of a workflow with `events`: its waits, which take its events. */
|
|
1020
|
+
eventWaits?: RunWaits;
|
|
937
1021
|
/** The run's time. */
|
|
938
1022
|
now: () => number;
|
|
939
|
-
/** Its timers
|
|
940
|
-
waits:
|
|
941
|
-
/** With `
|
|
1023
|
+
/** Its timers and waits, every run's. */
|
|
1024
|
+
waits: RunWaits;
|
|
1025
|
+
/** With `eventWaits`: each agent's latest session in earlier attempts, which it reopens on. */
|
|
942
1026
|
keptSessions?: ReadonlyMap<string, NativeFork>;
|
|
943
|
-
/**
|
|
1027
|
+
/** Where each session an agent is seen with is recorded, as it is. */
|
|
1028
|
+
sessionLines: SessionLines;
|
|
1029
|
+
/** With `eventWaits`: each agent's turn an earlier attempt started whose end no wait took. */
|
|
944
1030
|
restoredTurns?: ReadonlyMap<string, RestoredTurn>;
|
|
945
1031
|
emit: Emitter;
|
|
946
1032
|
/** The workflow's name, which with the run's id is the ref its events are `from`. */
|
|
947
1033
|
workflow: string;
|
|
1034
|
+
/** The names its `events` declares, which `signal` takes. */
|
|
1035
|
+
declared: ReadonlySet<string>;
|
|
1036
|
+
/** The operator's choices for every TUI agent's pane on the host. */
|
|
1037
|
+
panes?: RunPanes;
|
|
948
1038
|
},
|
|
949
1039
|
) {
|
|
950
1040
|
this.#restored = new Map(options.restoredTurns ?? []);
|
|
@@ -964,6 +1054,7 @@ class WorkflowOwner {
|
|
|
964
1054
|
},
|
|
965
1055
|
schedule: (at, action) => scheduleAt({ unixMilliseconds: at }, action),
|
|
966
1056
|
now: Date.now,
|
|
1057
|
+
question: (n) => `${options.attempt}.${n}`,
|
|
967
1058
|
record: async (event) => {
|
|
968
1059
|
this.#messagesChanged();
|
|
969
1060
|
if (event.kind === "message-sent") {
|
|
@@ -1004,7 +1095,10 @@ class WorkflowOwner {
|
|
|
1004
1095
|
runId: options.runId,
|
|
1005
1096
|
attempt: options.attempt,
|
|
1006
1097
|
cwd: options.cwd,
|
|
1007
|
-
|
|
1098
|
+
// A new `Date` each time, so one a workflow changes isn't the run's.
|
|
1099
|
+
get deadline() {
|
|
1100
|
+
return options.deadline && new Date(options.deadline.unixMilliseconds);
|
|
1101
|
+
},
|
|
1008
1102
|
agents: {
|
|
1009
1103
|
open: (spec) => {
|
|
1010
1104
|
try {
|
|
@@ -1013,24 +1107,6 @@ class WorkflowOwner {
|
|
|
1013
1107
|
return Promise.reject(error);
|
|
1014
1108
|
}
|
|
1015
1109
|
},
|
|
1016
|
-
attach: (key, runtime) => {
|
|
1017
|
-
if (this.#caller?.key !== key) return unavailable("agents.attach");
|
|
1018
|
-
const stopped = this.#agents.get(key)?.stopped;
|
|
1019
|
-
if (stopped !== undefined) return Promise.reject(stoppedError(key, stopped));
|
|
1020
|
-
if (runtime !== undefined) {
|
|
1021
|
-
return Promise.reject(
|
|
1022
|
-
new Error(`agent ${key} is the calling session, whose runtime is the operator's`),
|
|
1023
|
-
);
|
|
1024
|
-
}
|
|
1025
|
-
return this.#caller.state;
|
|
1026
|
-
},
|
|
1027
|
-
stop: (key, reason) => {
|
|
1028
|
-
try {
|
|
1029
|
-
return this.stopAgent(key, reason);
|
|
1030
|
-
} catch (error) {
|
|
1031
|
-
return Promise.reject(error);
|
|
1032
|
-
}
|
|
1033
|
-
},
|
|
1034
1110
|
caller: (spec) => {
|
|
1035
1111
|
try {
|
|
1036
1112
|
return this.caller(spec.key);
|
|
@@ -1083,17 +1159,10 @@ class WorkflowOwner {
|
|
|
1083
1159
|
},
|
|
1084
1160
|
},
|
|
1085
1161
|
participants: {
|
|
1086
|
-
connect: async (spec) =>
|
|
1087
|
-
|
|
1088
|
-
(this.#messaging.connected().includes(key) ? this.#outsideRefs.get(key) : undefined) ??
|
|
1089
|
-
(this.#logical.get(key) as AgentRef | undefined) ??
|
|
1090
|
-
null,
|
|
1162
|
+
connect: (async (spec: ParticipantSpec) =>
|
|
1163
|
+
this.connectParticipant(spec)) as ParticipantDirectory["connect"],
|
|
1091
1164
|
},
|
|
1092
1165
|
messages: { allow: async (access) => this.allowRoute(access) },
|
|
1093
|
-
steps: {
|
|
1094
|
-
run: () => unavailable("steps.run"),
|
|
1095
|
-
sleep: () => unavailable("steps.sleep"),
|
|
1096
|
-
},
|
|
1097
1166
|
emit: async (target, run, name, data, emitOptions) => {
|
|
1098
1167
|
if (this.#closed || this.#cleanupFailures.length)
|
|
1099
1168
|
throw new Error("workflow context is closed");
|
|
@@ -1115,9 +1184,12 @@ class WorkflowOwner {
|
|
|
1115
1184
|
},
|
|
1116
1185
|
parallel: (items, operation, parallelOptions) => {
|
|
1117
1186
|
const inherited = scopes.getStore()?.deadline ?? options.deadline;
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1187
|
+
let deadline: AbsoluteDeadline | undefined;
|
|
1188
|
+
try {
|
|
1189
|
+
deadline = boundedDeadline(parallelOptions?.timeout, inherited);
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
return Promise.reject(error);
|
|
1192
|
+
}
|
|
1121
1193
|
const label = parallelOptions?.label;
|
|
1122
1194
|
return runParallel(items, operation, deadline, parallelOptions?.concurrency, () =>
|
|
1123
1195
|
label === undefined ? undefined : options.progress.group(label, items.length),
|
|
@@ -1129,7 +1201,6 @@ class WorkflowOwner {
|
|
|
1129
1201
|
},
|
|
1130
1202
|
stage: ((name: string, ...rest: unknown[]) =>
|
|
1131
1203
|
this.runStage(name, rest)) as WorkflowContext["stage"],
|
|
1132
|
-
call: () => unavailable("call"),
|
|
1133
1204
|
usage: () => options.ledger.records(),
|
|
1134
1205
|
allowance: async (harness) => {
|
|
1135
1206
|
if (this.#closed || this.#cleanupFailures.length)
|
|
@@ -1153,33 +1224,29 @@ class WorkflowOwner {
|
|
|
1153
1224
|
undefined,
|
|
1154
1225
|
);
|
|
1155
1226
|
},
|
|
1227
|
+
// Every run waits on its own lines; one without events never parks, so it waits here.
|
|
1228
|
+
receive: (spec?: WaitOptions) => this.receive(options.waits, spec),
|
|
1229
|
+
race: ((name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
|
|
1230
|
+
this.race(options.waits, name, branches, raceOptions)) as WorkflowContext["race"],
|
|
1231
|
+
select: ((branches: Branches, selectOptions?: WaitOptions) =>
|
|
1232
|
+
this.select(options.waits, branches, selectOptions)) as WorkflowContext["select"],
|
|
1233
|
+
timer: (name: string, when: When) => this.timer(options.waits, name, when),
|
|
1234
|
+
at: (read: () => Date | null) => timeRead(read),
|
|
1235
|
+
after: (duration: Duration) => {
|
|
1236
|
+
this.#notReading("wf.after()");
|
|
1237
|
+
let milliseconds: number;
|
|
1238
|
+
try {
|
|
1239
|
+
milliseconds = durationMilliseconds(duration);
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
throw new Error(`wf.after takes a duration: ${messageOf(error)}`);
|
|
1242
|
+
}
|
|
1243
|
+
return new Date(options.now() + milliseconds);
|
|
1244
|
+
},
|
|
1245
|
+
signal: ((name: string) => signalSource(name, options.declared)) as WorkflowContext["signal"],
|
|
1246
|
+
...(options.eventWaits?.started
|
|
1247
|
+
? { started: options.eventWaits.started as WorkflowContext["started"] }
|
|
1248
|
+
: {}),
|
|
1156
1249
|
};
|
|
1157
|
-
const { mailbox } = options;
|
|
1158
|
-
Object.assign(
|
|
1159
|
-
this.context,
|
|
1160
|
-
mailbox
|
|
1161
|
-
? {
|
|
1162
|
-
receive: (spec?: ReceiveOptions) => this.receive(mailbox, spec),
|
|
1163
|
-
race: (name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
|
|
1164
|
-
this.race(mailbox, name, branches, raceOptions),
|
|
1165
|
-
select: (branches: Branches, selectOptions?: SelectOptions) =>
|
|
1166
|
-
this.select(mailbox, branches, selectOptions),
|
|
1167
|
-
timer: (name: string, when: When) => this.timer(mailbox, name, when),
|
|
1168
|
-
timers: () => source({}, { name: "timer:due" }),
|
|
1169
|
-
at: (read: () => Date | null) => timeRead(read),
|
|
1170
|
-
after: (duration: Duration) => {
|
|
1171
|
-
this.#notReading("wf.after()");
|
|
1172
|
-
if (typeof duration !== "string" || !/^\d+(?:\.\d+)?[dhm]$/.test(duration))
|
|
1173
|
-
throw new Error(
|
|
1174
|
-
`wf.after takes days, hours or minutes, "1d", "3h", "1.5h"; not ${String(duration)}`,
|
|
1175
|
-
);
|
|
1176
|
-
return new Date(parseWhen(duration, options.now()).due);
|
|
1177
|
-
},
|
|
1178
|
-
signal: (name: string) => signalSource(name),
|
|
1179
|
-
...(mailbox.started ? { started: mailbox.started } : {}),
|
|
1180
|
-
}
|
|
1181
|
-
: { receive: () => Promise.reject(new Error("receive needs a workflow with events")) },
|
|
1182
|
-
);
|
|
1183
1250
|
}
|
|
1184
1251
|
|
|
1185
1252
|
/** Set while a select reads a time from state. */
|
|
@@ -1243,7 +1310,8 @@ class WorkflowOwner {
|
|
|
1243
1310
|
if (this.#restored.get(event.from)?.turn === turn) this.#restored.delete(event.from);
|
|
1244
1311
|
this.#logical.get(event.from)?.endTaken(turn);
|
|
1245
1312
|
}
|
|
1246
|
-
|
|
1313
|
+
// `in.jsonl` and the turns' ends keep awf's record names; the code is handed the names it declares.
|
|
1314
|
+
return taken.map((event) => presented(event, this.options.declared));
|
|
1247
1315
|
} finally {
|
|
1248
1316
|
removeFromScope?.();
|
|
1249
1317
|
this.#waiting = false;
|
|
@@ -1251,7 +1319,7 @@ class WorkflowOwner {
|
|
|
1251
1319
|
}
|
|
1252
1320
|
|
|
1253
1321
|
/** A timer, by name: the recorded one, or set now. A race's own are named `{race}@{branch}`. */
|
|
1254
|
-
private timer(
|
|
1322
|
+
private timer(waits: RunWaits, name: string, when: When, own = false): Timer {
|
|
1255
1323
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1256
1324
|
if (typeof name !== "string") throw new Error("a timer's name is a string");
|
|
1257
1325
|
if (!own) {
|
|
@@ -1261,7 +1329,7 @@ class WorkflowOwner {
|
|
|
1261
1329
|
if (problem) throw new Error(`timer ${name}: its name is ${problem}`);
|
|
1262
1330
|
}
|
|
1263
1331
|
const given = parseWhen(when, this.options.now());
|
|
1264
|
-
const { timer, asked } =
|
|
1332
|
+
const { timer, asked } = waits.setTimer(name, given.due, given.when);
|
|
1265
1333
|
if (timer.cancelled)
|
|
1266
1334
|
this.options.onLog?.(
|
|
1267
1335
|
`awf: timer ${name} was cancelled; a name stays cancelled, so it won't fire`,
|
|
@@ -1270,12 +1338,12 @@ class WorkflowOwner {
|
|
|
1270
1338
|
this.options.onLog?.(
|
|
1271
1339
|
`awf: timer ${name} is due ${timer.due}, as first set (${timer.when}); ${asked} is ignored`,
|
|
1272
1340
|
);
|
|
1273
|
-
return timerSource(
|
|
1341
|
+
return timerSource(waits, name, timer.due);
|
|
1274
1342
|
}
|
|
1275
1343
|
|
|
1276
1344
|
/** Waits for the first of `branches`, recorded under `name`. */
|
|
1277
1345
|
private async race(
|
|
1278
|
-
|
|
1346
|
+
waits: RunWaits,
|
|
1279
1347
|
name: string,
|
|
1280
1348
|
branches: RaceBranches,
|
|
1281
1349
|
raceOptions: WaitOptions | undefined,
|
|
@@ -1291,8 +1359,7 @@ class WorkflowOwner {
|
|
|
1291
1359
|
throw new Error(
|
|
1292
1360
|
`race ${name} has answered in this attempt; a wait in a loop is named per round, as approval-\${round}`,
|
|
1293
1361
|
);
|
|
1294
|
-
|
|
1295
|
-
const label = checkedLabel(raceOptions?.label);
|
|
1362
|
+
const label = labelOf(raceOptions?.label);
|
|
1296
1363
|
const given = branchesOf(`race ${name}`, branches, true);
|
|
1297
1364
|
// A sleep's time may be a Date, its own timer as a duration is.
|
|
1298
1365
|
const own = (source: unknown) => typeof source === "string" || source instanceof Date;
|
|
@@ -1302,36 +1369,43 @@ class WorkflowOwner {
|
|
|
1302
1369
|
own(source) ? [] : [[branch, sourceTake(`race ${name}`, branch, source)] as const],
|
|
1303
1370
|
),
|
|
1304
1371
|
);
|
|
1372
|
+
this.#checkSources(`race ${name}`, takes.entries());
|
|
1373
|
+
// Only once its label and branches are checked: a refused race leaves its name to a retry.
|
|
1374
|
+
this.#raced.add(name);
|
|
1305
1375
|
const owned = new Map<string, string>();
|
|
1306
1376
|
const pairs: (readonly [string, WaitTake])[] = given.map(([branch, source]) => {
|
|
1307
1377
|
if (!own(source)) return [branch, takes.get(branch) as WaitTake] as const;
|
|
1308
|
-
const timer = this.timer(
|
|
1378
|
+
const timer = this.timer(waits, `${name}@${branch}`, source as When, true);
|
|
1309
1379
|
owned.set(branch, timer.name);
|
|
1310
1380
|
return [branch, { name: "timer:due", from: timer.name }] as const;
|
|
1311
1381
|
});
|
|
1312
|
-
let
|
|
1382
|
+
let branch: string | undefined;
|
|
1383
|
+
let event: WorkflowEvent<string, JsonValue>;
|
|
1313
1384
|
try {
|
|
1314
|
-
await this.#holdingWait(scope, async (signal) => {
|
|
1315
|
-
won = await
|
|
1385
|
+
[event] = (await this.#holdingWait(scope, async (signal) => {
|
|
1386
|
+
const won = await waits.race(name, pairs, owned, label, signal);
|
|
1387
|
+
branch = won.branch;
|
|
1316
1388
|
return [won.event];
|
|
1317
|
-
});
|
|
1389
|
+
})) as [WorkflowEvent<string, JsonValue>];
|
|
1318
1390
|
} catch (error) {
|
|
1319
|
-
// Cut short, not parked: its own timers would fire into a later wait. A park's rerun races
|
|
1320
|
-
|
|
1321
|
-
|
|
1391
|
+
// Cut short, not parked: its own timers would fire into a later wait. A park's rerun races
|
|
1392
|
+
// again, and so does a continue's after a park refused short of its --from-stage.
|
|
1393
|
+
if (!(error instanceof RunParked || error instanceof FromStageUnreached))
|
|
1394
|
+
await Promise.all([...owned.values()].map((timer) => waits.cancelTimer(timer))).catch(
|
|
1322
1395
|
() => undefined,
|
|
1323
1396
|
);
|
|
1324
1397
|
throw error;
|
|
1325
1398
|
}
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1399
|
+
const key = branch as string;
|
|
1400
|
+
const whole = isSignal(given.find(([each]) => each === key)?.[1]);
|
|
1401
|
+
return { key, [key]: branchValue(event, whole) } as Next<RaceBranches>;
|
|
1328
1402
|
}
|
|
1329
1403
|
|
|
1330
1404
|
/** A loop over the events `branches` take, one at a time. */
|
|
1331
1405
|
private select(
|
|
1332
|
-
|
|
1406
|
+
waits: RunWaits,
|
|
1333
1407
|
branches: Branches,
|
|
1334
|
-
selectOptions:
|
|
1408
|
+
selectOptions: WaitOptions | undefined,
|
|
1335
1409
|
): AsyncIterable<Next<Branches>> {
|
|
1336
1410
|
const given = branchesOf("select", branches, false);
|
|
1337
1411
|
const reads = new Map(
|
|
@@ -1344,9 +1418,12 @@ class WorkflowOwner {
|
|
|
1344
1418
|
([branch, source]) =>
|
|
1345
1419
|
[
|
|
1346
1420
|
branch,
|
|
1347
|
-
reads.has(branch)
|
|
1421
|
+
reads.has(branch)
|
|
1422
|
+
? { name: "at:due", from: branch }
|
|
1423
|
+
: sourceTake("select", branch, source),
|
|
1348
1424
|
] as const,
|
|
1349
1425
|
);
|
|
1426
|
+
this.#checkSources("select", pairs);
|
|
1350
1427
|
// Read at each pass of the wait: a time follows the state as it moves.
|
|
1351
1428
|
const takes = (): WaitTake[] =>
|
|
1352
1429
|
pairs.map(([branch, take]) => {
|
|
@@ -1354,20 +1431,17 @@ class WorkflowOwner {
|
|
|
1354
1431
|
const due = read && this.#readTime(branch, read);
|
|
1355
1432
|
return due === undefined ? take : { ...take, due };
|
|
1356
1433
|
});
|
|
1357
|
-
const labelOf = () => {
|
|
1358
|
-
const given = selectOptions?.label;
|
|
1359
|
-
return checkedLabel(typeof given === "function" ? given() : given);
|
|
1360
|
-
};
|
|
1361
1434
|
const next = async (): Promise<Next<Branches>> => {
|
|
1362
1435
|
const scope = this.#beginWait("select");
|
|
1363
1436
|
if (scope?.stage !== undefined)
|
|
1364
1437
|
throw new Error(`select is in stage ${scope.stage}: a loop's wait is not a stage's`);
|
|
1365
1438
|
const [event] = await this.#holdingWait(scope, async (signal) => [
|
|
1366
|
-
await
|
|
1439
|
+
await waits.select(takes, labelOf(selectOptions?.label), signal),
|
|
1367
1440
|
]);
|
|
1368
1441
|
const taken = event as WorkflowEvent<string, JsonValue>;
|
|
1369
1442
|
const branch = branchOf(pairs, taken) as string;
|
|
1370
|
-
|
|
1443
|
+
const whole = isSignal(given.find(([each]) => each === branch)?.[1]);
|
|
1444
|
+
return { key: branch, [branch]: branchValue(taken, whole) } as Next<Branches>;
|
|
1371
1445
|
};
|
|
1372
1446
|
return {
|
|
1373
1447
|
[Symbol.asyncIterator]: () => ({
|
|
@@ -1377,17 +1451,38 @@ class WorkflowOwner {
|
|
|
1377
1451
|
};
|
|
1378
1452
|
}
|
|
1379
1453
|
|
|
1454
|
+
/**
|
|
1455
|
+
* A wait's branches as a run can take them: a run without events, which hears no agent's end or
|
|
1456
|
+
* reply to the code, refuses a branch waiting on one, which would never come.
|
|
1457
|
+
*/
|
|
1458
|
+
#checkSources(what: string, takes: Iterable<readonly [string, WaitTake]>): void {
|
|
1459
|
+
const given = [...takes];
|
|
1460
|
+
if (!this.options.eventWaits) {
|
|
1461
|
+
const unheard = given.find(([, take]) => HEARD_WITH_EVENTS.has(take.name));
|
|
1462
|
+
if (unheard)
|
|
1463
|
+
throw new Error(
|
|
1464
|
+
`${what}'s ${unheard[0]} waits on ${unheard[1].name === "agent:ended" ? "an agent's end" : "a participant's reply to the code"}, which a run without events never hears; declare events in the workflow`,
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1380
1469
|
#waiting = false;
|
|
1381
1470
|
/** The races and sleeps this attempt has run, each by its name once. */
|
|
1382
1471
|
readonly #raced = new Set<string>();
|
|
1383
1472
|
/** Each agent's turn an earlier attempt started whose end no wait has taken yet. */
|
|
1384
1473
|
readonly #restored: Map<string, RestoredTurn>;
|
|
1385
1474
|
|
|
1386
|
-
/** How an agent of an event run records its started turns in
|
|
1387
|
-
#turnsOf(key: string,
|
|
1475
|
+
/** How an agent of an event run records its started turns in `run.jsonl`. */
|
|
1476
|
+
#turnsOf(key: string, waits: RunWaits): LogicalAgentTurns {
|
|
1388
1477
|
return {
|
|
1389
|
-
begin: (turn, meta, execution) =>
|
|
1390
|
-
end: (turn, meta, outcome) =>
|
|
1478
|
+
begin: (turn, meta, execution) => waits.beginTurn(key, turn, meta, execution),
|
|
1479
|
+
end: (turn, meta, outcome) => waits.endTurn(key, { turn, meta, outcome }),
|
|
1480
|
+
checkDeclared: () => {
|
|
1481
|
+
if (!this.options.declared.has(`${key}:ended`))
|
|
1482
|
+
throw new Error(
|
|
1483
|
+
`agent ${key}: start()'s end arrives as ${key}:ended, which the workflow declares in events, "${key}:ended": AgentEnded`,
|
|
1484
|
+
);
|
|
1485
|
+
},
|
|
1391
1486
|
// Read when the agent opens: a wait before then may have taken the end.
|
|
1392
1487
|
restored: () => this.#restored.get(key),
|
|
1393
1488
|
};
|
|
@@ -1411,10 +1506,9 @@ class WorkflowOwner {
|
|
|
1411
1506
|
* Nothing of the run is working but its wait: no agent's turn, message or compaction, and no
|
|
1412
1507
|
* decision, sandbox or agent opening in flight.
|
|
1413
1508
|
*/
|
|
1414
|
-
idle(
|
|
1415
|
-
const open = this.options.stages.openStage;
|
|
1509
|
+
idle(): boolean {
|
|
1416
1510
|
return (
|
|
1417
|
-
|
|
1511
|
+
this.options.stages.openStage === undefined &&
|
|
1418
1512
|
this.#inFlight.size === 0 &&
|
|
1419
1513
|
[...this.#logical.values()].every((agent) => !agent.busy)
|
|
1420
1514
|
);
|
|
@@ -1442,7 +1536,7 @@ class WorkflowOwner {
|
|
|
1442
1536
|
if (!onTimeout) continue;
|
|
1443
1537
|
try {
|
|
1444
1538
|
await runUntilStopped(
|
|
1445
|
-
async () => asTimeoutHandler(() => onTimeout(agent
|
|
1539
|
+
async () => asTimeoutHandler(() => onTimeout(agent)),
|
|
1446
1540
|
signal,
|
|
1447
1541
|
undefined,
|
|
1448
1542
|
);
|
|
@@ -1454,34 +1548,17 @@ class WorkflowOwner {
|
|
|
1454
1548
|
}
|
|
1455
1549
|
return false;
|
|
1456
1550
|
}
|
|
1457
|
-
/**
|
|
1458
|
-
readonly #stageBatches = new Map<string, WorkflowEvent<string, JsonValue>[]>();
|
|
1459
|
-
/** Stages around a wait this attempt entered: each is entered once, as a loop over one spins. */
|
|
1460
|
-
readonly #waitedStages = new Set<string>();
|
|
1461
|
-
|
|
1462
|
-
/** A wait: one at a time, and in a stage, the stage's batch. */
|
|
1551
|
+
/** A wait, outside any stage: a wait a rerun gets the same answer from is a `race`. */
|
|
1463
1552
|
private async receive(
|
|
1464
|
-
|
|
1465
|
-
spec:
|
|
1553
|
+
waits: RunWaits,
|
|
1554
|
+
spec: WaitOptions | undefined,
|
|
1466
1555
|
): Promise<WorkflowEvent<string, JsonValue>[]> {
|
|
1467
1556
|
const scope = this.#beginWait("receive()");
|
|
1468
|
-
if (scope?.stage !== undefined
|
|
1469
|
-
throw new Error(
|
|
1470
|
-
|
|
1471
|
-
const taken = await this.#holdingWait(scope, (signal) =>
|
|
1472
|
-
mailbox.receive(spec, scope?.stage, signal),
|
|
1557
|
+
if (scope?.stage !== undefined)
|
|
1558
|
+
throw new Error(
|
|
1559
|
+
`receive() is refused in stage ${scope.stage}: wait outside it with wf.race(name, …), which is recorded under its name`,
|
|
1473
1560
|
);
|
|
1474
|
-
|
|
1475
|
-
this.#stageBatches.set(scope.stage, taken);
|
|
1476
|
-
this.#waitedStages.add(scope.stage);
|
|
1477
|
-
}
|
|
1478
|
-
return taken;
|
|
1479
|
-
} catch (error) {
|
|
1480
|
-
// Parked in a stage: left unended now, before anything closes it, for the next attempt.
|
|
1481
|
-
if (error instanceof RunParked && scope?.stage !== undefined)
|
|
1482
|
-
this.options.stages.abandon(scope.stage);
|
|
1483
|
-
throw error;
|
|
1484
|
-
}
|
|
1561
|
+
return this.#holdingWait(scope, (signal) => waits.receive(receiveLabel(spec), signal));
|
|
1485
1562
|
}
|
|
1486
1563
|
|
|
1487
1564
|
/** An agent as messaging sees it, from its entry and its operations. */
|
|
@@ -1512,47 +1589,38 @@ class WorkflowOwner {
|
|
|
1512
1589
|
!isJsonValue(labels))
|
|
1513
1590
|
)
|
|
1514
1591
|
throw new Error("a participant's labels are a JSON object");
|
|
1515
|
-
|
|
1516
|
-
if (to !== undefined && to !== "code")
|
|
1517
|
-
throw new Error(`a participant's messages go to agents, or to: "code"; not ${String(to)}`);
|
|
1518
|
-
if (to === "code" && !this.options.mailbox)
|
|
1592
|
+
if ((spec as { to?: unknown }).to !== undefined)
|
|
1519
1593
|
throw new Error(
|
|
1520
|
-
|
|
1594
|
+
`participant ${key} is connected without to: its messages reach the code when the workflow declares "${key}:reply": ParticipantReply in events`,
|
|
1521
1595
|
);
|
|
1522
1596
|
this.#messaging.connect(
|
|
1523
1597
|
key,
|
|
1524
1598
|
labels === undefined ? undefined : structuredClone(labels),
|
|
1525
|
-
|
|
1599
|
+
this.options.declared.has(`${key}:reply`) ? "code" : undefined,
|
|
1526
1600
|
);
|
|
1527
1601
|
const ref: ParticipantRef = {
|
|
1528
1602
|
...participantRef(key),
|
|
1603
|
+
post: async (text: string) => {
|
|
1604
|
+
if (typeof text !== "string" || text.trim() === "")
|
|
1605
|
+
throw new Error("post takes the text to write");
|
|
1606
|
+
await this.#messaging.post(key, text);
|
|
1607
|
+
},
|
|
1529
1608
|
release: async (reason) => {
|
|
1530
1609
|
if (reason !== undefined && typeof reason !== "string")
|
|
1531
1610
|
throw new Error("a release's reason is a string");
|
|
1532
1611
|
return this.#messaging.release(key, reason);
|
|
1533
1612
|
},
|
|
1534
1613
|
};
|
|
1535
|
-
if (to === "code") {
|
|
1536
|
-
const routed = source(
|
|
1537
|
-
{
|
|
1538
|
-
...ref,
|
|
1539
|
-
post: async (text: string) => {
|
|
1540
|
-
if (typeof text !== "string" || text.trim() === "")
|
|
1541
|
-
throw new Error("post takes the text to write");
|
|
1542
|
-
await this.#messaging.post(key, text);
|
|
1543
|
-
},
|
|
1544
|
-
},
|
|
1545
|
-
{ name: "participant:reply", from: key },
|
|
1546
|
-
) as unknown as CodeParticipantRef;
|
|
1547
|
-
this.#outsideRefs.set(key, routed);
|
|
1548
|
-
return routed;
|
|
1549
|
-
}
|
|
1550
1614
|
this.#outsideRefs.set(key, ref);
|
|
1551
1615
|
return ref;
|
|
1552
1616
|
}
|
|
1553
1617
|
|
|
1554
1618
|
/** A key an agent may not take: a reserved one, or an outside participant's. */
|
|
1555
1619
|
#assertFreeKey(key: string, who: string): void {
|
|
1620
|
+
if (typeof key === "string" && key.includes(":"))
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`${key} has a colon, kept for awf's own event names, {key}:ended and {key}:reply; ${who} takes another key, such as ${key.replaceAll(":", "-")}`,
|
|
1623
|
+
);
|
|
1556
1624
|
if ((RESERVED_PARTICIPANT_KEYS as readonly string[]).includes(key.toLowerCase()))
|
|
1557
1625
|
throw new Error(
|
|
1558
1626
|
`${key} is reserved: an agent could read a message from it as authority; ${who} takes another key`,
|
|
@@ -1600,10 +1668,14 @@ class WorkflowOwner {
|
|
|
1600
1668
|
throw new Error("a route between agents names two of them");
|
|
1601
1669
|
ends = { between: [end(shape.between[0]), end(shape.between[1])] };
|
|
1602
1670
|
} else ends = { from: end(shape.from), to: end(shape.to) };
|
|
1671
|
+
const timeoutMs =
|
|
1672
|
+
access.timeout == null
|
|
1673
|
+
? undefined
|
|
1674
|
+
: positiveDurationMilliseconds(access.timeout, "a route's timeout");
|
|
1603
1675
|
this.#messaging.grant(ends, {
|
|
1604
1676
|
purpose: access.purpose,
|
|
1605
1677
|
maxMessages: access.maxMessages,
|
|
1606
|
-
timeoutMs
|
|
1678
|
+
timeoutMs,
|
|
1607
1679
|
scope: scope?.messages ?? this.#messaging.attempt,
|
|
1608
1680
|
});
|
|
1609
1681
|
}
|
|
@@ -1694,7 +1766,7 @@ class WorkflowOwner {
|
|
|
1694
1766
|
: "cancelled",
|
|
1695
1767
|
reason: "workflow closing",
|
|
1696
1768
|
})
|
|
1697
|
-
.then(() => state.
|
|
1769
|
+
.then(() => state.terminate("workflow complete")),
|
|
1698
1770
|
() => undefined,
|
|
1699
1771
|
),
|
|
1700
1772
|
);
|
|
@@ -1777,19 +1849,7 @@ class WorkflowOwner {
|
|
|
1777
1849
|
});
|
|
1778
1850
|
if (entered.kind === "reuse") {
|
|
1779
1851
|
entered.release();
|
|
1780
|
-
|
|
1781
|
-
const batch =
|
|
1782
|
-
entered.value === undefined && !options
|
|
1783
|
-
? await this.options.mailbox?.batchOf(name)
|
|
1784
|
-
: undefined;
|
|
1785
|
-
if (batch) {
|
|
1786
|
-
if (this.#waitedStages.has(name))
|
|
1787
|
-
throw new Error(
|
|
1788
|
-
`stage ${name} around a wait was entered again in this attempt, and would return the same batch forever; name it by what it waits for`,
|
|
1789
|
-
);
|
|
1790
|
-
this.#waitedStages.add(name);
|
|
1791
|
-
}
|
|
1792
|
-
return (batch as JsonValue | undefined) ?? entered.value;
|
|
1852
|
+
return entered.value;
|
|
1793
1853
|
}
|
|
1794
1854
|
const open = entered.stage;
|
|
1795
1855
|
const messages = this.#messaging.scope(name);
|
|
@@ -1807,22 +1867,9 @@ class WorkflowOwner {
|
|
|
1807
1867
|
this.#messaging.finishing(messages);
|
|
1808
1868
|
scope.seal();
|
|
1809
1869
|
await this.drainMessages(messages, () => scope.settleOwned());
|
|
1810
|
-
// A timer it set is written before its
|
|
1870
|
+
// A timer it set is written before its line, which a rerun reuses without its work.
|
|
1811
1871
|
await this.options.waits.flush();
|
|
1812
1872
|
if (this.options.stages.stopped?.stage === name) throw this.options.stages.caught();
|
|
1813
|
-
const batch = this.#stageBatches.get(name);
|
|
1814
|
-
if (batch && !options) {
|
|
1815
|
-
const same =
|
|
1816
|
-
Array.isArray(returned) &&
|
|
1817
|
-
returned.length === batch.length &&
|
|
1818
|
-
returned.every((event, i) => (event as { id?: unknown })?.id === batch[i]?.id);
|
|
1819
|
-
if (!same)
|
|
1820
|
-
throw new Error(
|
|
1821
|
-
`stage ${name} returned other events than its wait took; its work is the wait alone`,
|
|
1822
|
-
);
|
|
1823
|
-
await open.succeed(undefined);
|
|
1824
|
-
return batch as JsonValue;
|
|
1825
|
-
}
|
|
1826
1873
|
const value = stageValue(name, options, returned);
|
|
1827
1874
|
await open.succeed(value, summaryOf(name, options, value, this.options.onLog));
|
|
1828
1875
|
return value;
|
|
@@ -1842,7 +1889,6 @@ class WorkflowOwner {
|
|
|
1842
1889
|
}
|
|
1843
1890
|
throw error;
|
|
1844
1891
|
} finally {
|
|
1845
|
-
this.#stageBatches.delete(name);
|
|
1846
1892
|
this.#messaging.closeScope(messages);
|
|
1847
1893
|
this.#stageScopes.delete(messages);
|
|
1848
1894
|
removeFromParent?.();
|
|
@@ -1856,13 +1902,14 @@ class WorkflowOwner {
|
|
|
1856
1902
|
if (this.#caller?.key === spec.key) {
|
|
1857
1903
|
throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
|
|
1858
1904
|
}
|
|
1905
|
+
this.options.ops.claim(spec.key, "agent");
|
|
1859
1906
|
const inRunSandbox = this.options.sandboxes.hasRunSandbox;
|
|
1860
1907
|
if (inRunSandbox && spec.sandbox !== undefined)
|
|
1861
1908
|
throw new Error(`agent ${spec.key}: ${RUN_SANDBOX_ONLY}`);
|
|
1862
1909
|
const existing = this.#agents.get(spec.key);
|
|
1863
1910
|
if (existing?.stopped !== undefined) throw stoppedError(spec.key, existing.stopped);
|
|
1864
|
-
const given =
|
|
1865
|
-
if (given !== undefined && !this.options.
|
|
1911
|
+
const given = spec.keepAlive;
|
|
1912
|
+
if (given !== undefined && !this.options.eventWaits)
|
|
1866
1913
|
throw new Error(
|
|
1867
1914
|
`agent ${spec.key}: keepAlive is for a workflow with events, whose runs park; this one's never do`,
|
|
1868
1915
|
);
|
|
@@ -1870,26 +1917,25 @@ class WorkflowOwner {
|
|
|
1870
1917
|
const execution = existing
|
|
1871
1918
|
? constrainExistingExecution(spec.runtime, existing.identity.execution)
|
|
1872
1919
|
: withKnownEffort(resolveExecution(spec.runtime, this.options.runtime));
|
|
1873
|
-
|
|
1920
|
+
const paneless = {
|
|
1874
1921
|
headless: placementOf(execution) === "headless",
|
|
1875
1922
|
sandboxed:
|
|
1876
1923
|
spec.sandbox !== undefined || existing?.identity.sandbox !== undefined || inRunSandbox,
|
|
1877
|
-
}
|
|
1924
|
+
};
|
|
1925
|
+
checkPaneOptions(spec.key, spec, paneless);
|
|
1926
|
+
const pane = spec.pane === undefined ? undefined : storedPane(spec.pane);
|
|
1878
1927
|
const identity: AgentIdentity = {
|
|
1879
1928
|
execution,
|
|
1880
1929
|
cwd: spec.cwd ?? this.options.cwd,
|
|
1881
1930
|
...(spec.instructions === undefined ? {} : { instructions: spec.instructions }),
|
|
1882
|
-
...(spec.labels === undefined ? {} : { labels: structuredClone(spec.labels) }),
|
|
1883
1931
|
...(spec.sandbox === undefined ? {} : { sandbox: spec.sandbox }),
|
|
1884
1932
|
...(spec.skills === undefined ? {} : { skills: readSkillSources(spec.skills) }),
|
|
1885
|
-
...(
|
|
1886
|
-
|
|
1933
|
+
...(pane === undefined ? {} : { pane }),
|
|
1934
|
+
asked: paneAsked(pane, this.options.panes, !paneless.headless && !paneless.sandboxed),
|
|
1887
1935
|
};
|
|
1888
1936
|
const scope = scopes.getStore();
|
|
1889
1937
|
const inheritedDeadline = scope?.deadline ?? this.options.deadline;
|
|
1890
|
-
const effectiveDeadline = spec.
|
|
1891
|
-
? earlierDeadline(spec.deadline, inheritedDeadline)
|
|
1892
|
-
: inheritedDeadline;
|
|
1938
|
+
const effectiveDeadline = boundedDeadline(spec.timeout, inheritedDeadline);
|
|
1893
1939
|
assertDeadline(effectiveDeadline);
|
|
1894
1940
|
if (existing) {
|
|
1895
1941
|
assertCompatibleAgent(spec.key, existing.identity, identity, spec);
|
|
@@ -1944,15 +1990,14 @@ class WorkflowOwner {
|
|
|
1944
1990
|
// A kept session already holds the instructions; they are not sent again.
|
|
1945
1991
|
...(kept ? { continues: kept } : {}),
|
|
1946
1992
|
...(spec.instructions === undefined || kept ? {} : { instructions: spec.instructions }),
|
|
1947
|
-
...(this.options.
|
|
1948
|
-
...(spec.labels === undefined ? {} : { labels: spec.labels }),
|
|
1993
|
+
...(this.options.eventWaits ? { paneAtFirstTurn: true as const } : {}),
|
|
1949
1994
|
...(seated ? { occupant: seated.occupant } : {}),
|
|
1950
1995
|
...(seated?.webSearch ? { webSearch: true } : {}),
|
|
1951
1996
|
...(skills ? { skills } : {}),
|
|
1952
1997
|
...(home ? { home } : {}),
|
|
1953
|
-
...
|
|
1998
|
+
...identity.asked,
|
|
1954
1999
|
}),
|
|
1955
|
-
layoutFallback: this.besideFallback(identity.layout, effectiveDeadline)(),
|
|
2000
|
+
layoutFallback: this.besideFallback(identity.asked?.layout, effectiveDeadline)(),
|
|
1956
2001
|
};
|
|
1957
2002
|
let skills: Promise<PlacedSkills | undefined> = Promise.resolve(undefined);
|
|
1958
2003
|
let opened: { state: Promise<LogicalAgent>; channel: Promise<ResultChannel> };
|
|
@@ -1985,11 +2030,11 @@ class WorkflowOwner {
|
|
|
1985
2030
|
|
|
1986
2031
|
/**
|
|
1987
2032
|
* Ends an agent the workflow has no more use for, as an operation that fails ends it: what it
|
|
1988
|
-
* is running is cancelled, and its pane closed or, where `
|
|
2033
|
+
* is running is cancelled, and its pane closed or, where `--keep-panes` says, kept and released.
|
|
1989
2034
|
*/
|
|
1990
|
-
private stopAgent(key: string, reason
|
|
2035
|
+
private stopAgent(key: string, reason: string | undefined, what: string): Promise<boolean> {
|
|
1991
2036
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1992
|
-
const inTool = refusedInTool(key,
|
|
2037
|
+
const inTool = refusedInTool(key, what);
|
|
1993
2038
|
if (inTool) throw inTool;
|
|
1994
2039
|
const scope = scopes.getStore();
|
|
1995
2040
|
scope?.assertAccepting();
|
|
@@ -2014,7 +2059,7 @@ class WorkflowOwner {
|
|
|
2014
2059
|
}
|
|
2015
2060
|
this.options.progress.agentStopped(key, entry.stopped!);
|
|
2016
2061
|
try {
|
|
2017
|
-
await state.
|
|
2062
|
+
await state.terminate(entry.stopped);
|
|
2018
2063
|
} finally {
|
|
2019
2064
|
await entry.channel.then((channel) => channel?.close());
|
|
2020
2065
|
}
|
|
@@ -2044,13 +2089,14 @@ class WorkflowOwner {
|
|
|
2044
2089
|
/**
|
|
2045
2090
|
* The session the run was started from, under the key the workflow names: `null` when there is
|
|
2046
2091
|
* none. It is found, not opened: its harness, directory and skills are the operator's, and it
|
|
2047
|
-
* answers through a launcher by path as any
|
|
2092
|
+
* answers through a launcher by path as any TUI agent does (ADR 0010).
|
|
2048
2093
|
*/
|
|
2049
2094
|
private caller(key: string): Promise<AgentRef | null> {
|
|
2050
2095
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
2051
2096
|
const scope = scopes.getStore();
|
|
2052
2097
|
scope?.assertAccepting();
|
|
2053
2098
|
this.#assertFreeKey(key, "an agent");
|
|
2099
|
+
this.options.ops.claim(key, "agent");
|
|
2054
2100
|
const found = this.options.runtime.host.caller;
|
|
2055
2101
|
if (!found) return Promise.resolve(null);
|
|
2056
2102
|
if (this.#caller) {
|
|
@@ -2078,7 +2124,9 @@ class WorkflowOwner {
|
|
|
2078
2124
|
};
|
|
2079
2125
|
const ledger = this.options.ledger.agent(accounted);
|
|
2080
2126
|
this.options.skills.record(key, "operator");
|
|
2081
|
-
const opened = this.options.control.openChannel(key, (id) =>
|
|
2127
|
+
const opened = this.options.control.openChannel(key, (id) =>
|
|
2128
|
+
this.launcherReported(key, execution.harness, sessions, id),
|
|
2129
|
+
);
|
|
2082
2130
|
const state = opened.then(async (channel) => {
|
|
2083
2131
|
try {
|
|
2084
2132
|
const launcher = await installAgentLauncher(
|
|
@@ -2121,7 +2169,7 @@ class WorkflowOwner {
|
|
|
2121
2169
|
* it is that agent's own fork, taken in its queue; otherwise it is taken now, from a parent the
|
|
2122
2170
|
* run never opened. `null`, and the reason logged, where there is none to fork.
|
|
2123
2171
|
*/
|
|
2124
|
-
private forkCaller(spec:
|
|
2172
|
+
private forkCaller(spec: CallerForkSpec): Promise<AgentRef | null> {
|
|
2125
2173
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
2126
2174
|
const scope = scopes.getStore();
|
|
2127
2175
|
scope?.assertAccepting();
|
|
@@ -2150,11 +2198,10 @@ class WorkflowOwner {
|
|
|
2150
2198
|
channel: Promise.resolve(undefined),
|
|
2151
2199
|
};
|
|
2152
2200
|
parent.state.catch(() => undefined);
|
|
2153
|
-
const deadline = scope?.deadline ?? this.options.deadline;
|
|
2154
2201
|
return this.forkAgent(
|
|
2155
2202
|
CALLING,
|
|
2156
2203
|
spec,
|
|
2157
|
-
(into) =>
|
|
2204
|
+
(deadline, into) =>
|
|
2158
2205
|
calling
|
|
2159
2206
|
.fork(deadlineWithin(FORK_TIMEOUT_MS, deadline), into, this.#closingRun.signal)
|
|
2160
2207
|
.then((native) => ({ native, after: null })),
|
|
@@ -2175,7 +2222,10 @@ class WorkflowOwner {
|
|
|
2175
2222
|
): { state: Promise<LogicalAgent>; channel: Promise<ResultChannel> } {
|
|
2176
2223
|
const opened = placed.then((skills) => {
|
|
2177
2224
|
if (skills?.given.ownHome) accounted.home = skills.given.ownHome;
|
|
2178
|
-
|
|
2225
|
+
if (skills) this.#givenSkills.set(spec.key, skills.given);
|
|
2226
|
+
return this.options.control.openChannel(spec.key, (id) =>
|
|
2227
|
+
this.launcherReported(spec.key, execution.harness, sessions, id),
|
|
2228
|
+
);
|
|
2179
2229
|
});
|
|
2180
2230
|
const reachable = opened.then(async (channel) => ({
|
|
2181
2231
|
channel,
|
|
@@ -2253,7 +2303,7 @@ class WorkflowOwner {
|
|
|
2253
2303
|
const given = skillsLayout(
|
|
2254
2304
|
execution.harness,
|
|
2255
2305
|
resolved.map((skill) => skill.name),
|
|
2256
|
-
{ bundle: join(runDir, "agents", id), cwd: realCwd },
|
|
2306
|
+
{ bundle: join(workDir(runDir), "agents", id), cwd: realCwd },
|
|
2257
2307
|
);
|
|
2258
2308
|
this.options.skills.record(key, resolved, given.ownHome);
|
|
2259
2309
|
const { ownHome } = given;
|
|
@@ -2264,8 +2314,12 @@ class WorkflowOwner {
|
|
|
2264
2314
|
await mkdir(dirname(ownHome), { recursive: true });
|
|
2265
2315
|
const seeded = await seedHome(
|
|
2266
2316
|
ownHome,
|
|
2267
|
-
join(runDir, "staging", id),
|
|
2268
|
-
|
|
2317
|
+
join(workDir(runDir), "staging", id),
|
|
2318
|
+
(this.options.homes ?? OPERATOR_HOMES).host(
|
|
2319
|
+
execution.harness,
|
|
2320
|
+
ownHome,
|
|
2321
|
+
this.options.environment,
|
|
2322
|
+
),
|
|
2269
2323
|
realCwd,
|
|
2270
2324
|
this.options.locks,
|
|
2271
2325
|
async (staged) => {
|
|
@@ -2314,10 +2368,11 @@ class WorkflowOwner {
|
|
|
2314
2368
|
// What the agent really ran in, which is where accounting finds its sessions.
|
|
2315
2369
|
accounted.cwd = seat.cwd;
|
|
2316
2370
|
accounted.home = seat.home;
|
|
2371
|
+
if (seat.skills) this.#givenSkills.set(spec.key, seat.skills);
|
|
2317
2372
|
let channel: ResultChannel | undefined;
|
|
2318
2373
|
try {
|
|
2319
2374
|
channel = await this.options.control.openChannel(spec.key, (id) =>
|
|
2320
|
-
|
|
2375
|
+
this.launcherReported(spec.key, execution.harness, sessions, id),
|
|
2321
2376
|
);
|
|
2322
2377
|
const door = await installSandboxedDoor(
|
|
2323
2378
|
dirname(channel.endpoint),
|
|
@@ -2381,6 +2436,7 @@ class WorkflowOwner {
|
|
|
2381
2436
|
execution,
|
|
2382
2437
|
session,
|
|
2383
2438
|
slots: this.options.slots,
|
|
2439
|
+
ops: this.options.ops,
|
|
2384
2440
|
endpoint,
|
|
2385
2441
|
launcher,
|
|
2386
2442
|
deadline: this.options.deadline,
|
|
@@ -2388,6 +2444,11 @@ class WorkflowOwner {
|
|
|
2388
2444
|
progress: this.options.progress,
|
|
2389
2445
|
recordTurn: this.options.recordTurn,
|
|
2390
2446
|
events: this.options.events,
|
|
2447
|
+
sessionsKnown: (sessions, operation) =>
|
|
2448
|
+
this.options.sessionLines.known(key, sessions, {
|
|
2449
|
+
...(operation === undefined ? {} : { operation }),
|
|
2450
|
+
...this.placeOf(key),
|
|
2451
|
+
}),
|
|
2391
2452
|
track: (promise) => this.track(promise),
|
|
2392
2453
|
isRunClosing: () => this.#closed || this.#cleanupFailures.length > 0,
|
|
2393
2454
|
checkOperation: () => this.options.stages.checkOperation(),
|
|
@@ -2397,9 +2458,16 @@ class WorkflowOwner {
|
|
|
2397
2458
|
this.options.failRun(error);
|
|
2398
2459
|
},
|
|
2399
2460
|
livenessPolicy: this.options.livenessPolicy,
|
|
2400
|
-
...(this.options.
|
|
2461
|
+
...(this.options.eventWaits ? { turns: this.#turnsOf(key, this.options.eventWaits) } : {}),
|
|
2401
2462
|
fork: (spec, take, settings) => this.forkAgent(key, spec, take, settings),
|
|
2402
|
-
|
|
2463
|
+
close: (reason) => {
|
|
2464
|
+
try {
|
|
2465
|
+
return this.stopAgent(key, reason, "close()");
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
return Promise.reject(error);
|
|
2468
|
+
}
|
|
2469
|
+
},
|
|
2470
|
+
keepPane: () => this.#agents.get(key)?.identity.asked?.keepPane,
|
|
2403
2471
|
keepAlive: () => this.#agents.get(key)?.keepAlive,
|
|
2404
2472
|
defaultWindowMs: this.options.defaultWindowMs ?? DEFAULT_WINDOW_MS,
|
|
2405
2473
|
now: this.options.now,
|
|
@@ -2431,8 +2499,11 @@ class WorkflowOwner {
|
|
|
2431
2499
|
private forkAgent(
|
|
2432
2500
|
parentKey: string,
|
|
2433
2501
|
spec: AgentForkSpec,
|
|
2434
|
-
/**
|
|
2435
|
-
|
|
2502
|
+
/**
|
|
2503
|
+
* Queues the native fork, or its copy `into` another home, within the fork's `deadline`; or why
|
|
2504
|
+
* its host cannot fork.
|
|
2505
|
+
*/
|
|
2506
|
+
take: TakeFork | string,
|
|
2436
2507
|
/** The parent's settings once every `set` queued before the fork has run. */
|
|
2437
2508
|
parentSettings: AgentExecution,
|
|
2438
2509
|
/** A parent the run never opened: the session it was started from. */
|
|
@@ -2446,11 +2517,13 @@ class WorkflowOwner {
|
|
|
2446
2517
|
throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
|
|
2447
2518
|
}
|
|
2448
2519
|
this.#assertFreeKey(spec.key, "an agent");
|
|
2520
|
+
this.options.ops.claim(spec.key, "agent");
|
|
2449
2521
|
// Out before the clone, which throws on a handler, and out of the spec a repeat is compared by.
|
|
2450
|
-
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2453
|
-
|
|
2522
|
+
// A bound is the call's, not the fork's: a repeat with another is the same fork.
|
|
2523
|
+
const { tools, timeout, ...given } = spec;
|
|
2524
|
+
const forkSpec = structuredClone(forkSpecOf(given));
|
|
2525
|
+
if (forkSpec.pane !== undefined) forkSpec.pane = storedPane(forkSpec.pane);
|
|
2526
|
+
const deadline = boundedDeadline(timeout, scope?.deadline ?? this.options.deadline);
|
|
2454
2527
|
assertDeadline(deadline);
|
|
2455
2528
|
// A fork of the calling session is one whichever way it was asked for.
|
|
2456
2529
|
const from = unopened || this.#caller?.key === parentKey ? CALLING : parentKey;
|
|
@@ -2478,27 +2551,24 @@ class WorkflowOwner {
|
|
|
2478
2551
|
// child's home: anything added to the parent's would be read as the parent's.
|
|
2479
2552
|
const copy =
|
|
2480
2553
|
sandboxed || opening.accounted.home !== undefined
|
|
2481
|
-
? join(this.options.runDir, "forks", randomUUID())
|
|
2554
|
+
? join(workDir(this.options.runDir), "forks", randomUUID())
|
|
2482
2555
|
: undefined;
|
|
2483
|
-
const execution = withKnownEffort(forkExecution(parentSettings, forkSpec));
|
|
2556
|
+
const execution = withKnownEffort(forkExecution(parentSettings, forkSpec.runtime));
|
|
2484
2557
|
// A fork of the calling session is an agent the run opened, on the model the session's files
|
|
2485
2558
|
// show, which the fork reads: `""` until then (ADR 0010).
|
|
2486
2559
|
const ofCaller = execution.caller === true;
|
|
2487
2560
|
delete execution.caller;
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
sandboxed,
|
|
2491
|
-
});
|
|
2561
|
+
const headless = placementOf(execution) === "headless";
|
|
2562
|
+
checkPaneOptions(spec.key, forkSpec, { headless, sandboxed });
|
|
2492
2563
|
const identity: AgentIdentity = {
|
|
2493
2564
|
execution,
|
|
2494
2565
|
cwd: parent.identity.cwd,
|
|
2495
2566
|
...(forkSpec.instructions === undefined ? {} : { instructions: forkSpec.instructions }),
|
|
2496
|
-
...(forkSpec.labels === undefined ? {} : { labels: forkSpec.labels }),
|
|
2497
2567
|
...(parent.identity.skills ? { skills: parent.identity.skills } : {}),
|
|
2498
2568
|
...(parent.identity.sandbox === undefined ? {} : { sandbox: parent.identity.sandbox }),
|
|
2499
2569
|
forkedFrom: { parent: from, spec: forkSpec },
|
|
2500
|
-
...(forkSpec.
|
|
2501
|
-
|
|
2570
|
+
...(forkSpec.pane === undefined ? {} : { pane: forkSpec.pane }),
|
|
2571
|
+
asked: paneAsked(forkSpec.pane, this.options.panes, !headless && !sandboxed),
|
|
2502
2572
|
};
|
|
2503
2573
|
|
|
2504
2574
|
this.options.progress.agentOpened(spec.key, scope?.group);
|
|
@@ -2512,11 +2582,11 @@ class WorkflowOwner {
|
|
|
2512
2582
|
sessions: () => reportedSessions(sessions),
|
|
2513
2583
|
forkedAt: () => forkedAt,
|
|
2514
2584
|
};
|
|
2515
|
-
const besideAtFork = this.besideFallback(identity.layout, deadline);
|
|
2585
|
+
const besideAtFork = this.besideFallback(identity.asked?.layout, deadline);
|
|
2516
2586
|
let continues: NativeFork | undefined;
|
|
2517
2587
|
// The fork is asked for now, in the parent's queue; the child opens once it is made. One that
|
|
2518
2588
|
// was not made leaves nothing behind: its key may be forked again, after the parent's turn.
|
|
2519
|
-
const forked = take(copy === undefined ? undefined : { directory: copy })
|
|
2589
|
+
const forked = take(deadline, copy === undefined ? undefined : { directory: copy })
|
|
2520
2590
|
.then(({ native: fork, after }) => {
|
|
2521
2591
|
if (ofCaller) {
|
|
2522
2592
|
if (!fork.model) throw new Error(`agent ${parentKey}'s fork named no model to run on`);
|
|
@@ -2563,13 +2633,12 @@ class WorkflowOwner {
|
|
|
2563
2633
|
cwd,
|
|
2564
2634
|
execution,
|
|
2565
2635
|
...(forkSpec.instructions === undefined ? {} : { instructions: forkSpec.instructions }),
|
|
2566
|
-
...(forkSpec.labels === undefined ? {} : { labels: forkSpec.labels }),
|
|
2567
2636
|
...(seated ? { occupant: seated.occupant } : {}),
|
|
2568
2637
|
...(seated?.webSearch ? { webSearch: true } : {}),
|
|
2569
2638
|
...(given ? { skills: given } : {}),
|
|
2570
2639
|
...(home ? { home } : {}),
|
|
2571
2640
|
...(continues ? { continues } : {}),
|
|
2572
|
-
...
|
|
2641
|
+
...identity.asked,
|
|
2573
2642
|
}),
|
|
2574
2643
|
// The target as it stands when `fork` is called, waited for once the fork is made.
|
|
2575
2644
|
layoutFallback: forked.then(besideAtFork, () => undefined),
|
|
@@ -2627,6 +2696,10 @@ class WorkflowOwner {
|
|
|
2627
2696
|
): void {
|
|
2628
2697
|
state.then(
|
|
2629
2698
|
() => {
|
|
2699
|
+
const entry = this.#agents.get(accounted.key);
|
|
2700
|
+
const skills = this.options.skills.of(accounted.key);
|
|
2701
|
+
const pane = entry && paneOf(entry);
|
|
2702
|
+
const seat = this.options.sandboxes.admitted(accounted.key);
|
|
2630
2703
|
this.options.events.event({
|
|
2631
2704
|
kind: "agent-opened",
|
|
2632
2705
|
agent: accounted.key,
|
|
@@ -2634,17 +2707,26 @@ class WorkflowOwner {
|
|
|
2634
2707
|
harness: accounted.execution.harness,
|
|
2635
2708
|
cwd: accounted.cwd,
|
|
2636
2709
|
...(accounted.home === undefined ? {} : { home: accounted.home }),
|
|
2710
|
+
...(seat ? { sandbox: seat.sandbox, domains: seat.domains } : {}),
|
|
2637
2711
|
...(as && "caller" in as ? { caller: true } : {}),
|
|
2712
|
+
...(skills
|
|
2713
|
+
? {
|
|
2714
|
+
skills: {
|
|
2715
|
+
skills: skills.skills,
|
|
2716
|
+
...(skills.home === undefined ? {} : { home: skills.home }),
|
|
2717
|
+
},
|
|
2718
|
+
}
|
|
2719
|
+
: {}),
|
|
2720
|
+
...(pane ? { pane } : {}),
|
|
2638
2721
|
});
|
|
2639
2722
|
// A fork's session is made before its first operation names it.
|
|
2640
2723
|
if (!as || !("fork" in as)) return;
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
}
|
|
2724
|
+
const { harness } = accounted.execution;
|
|
2725
|
+
this.options.sessionLines.known(
|
|
2726
|
+
accounted.key,
|
|
2727
|
+
accounted.sessions().map((id) => ({ harness, id })),
|
|
2728
|
+
this.placeOf(accounted.key),
|
|
2729
|
+
);
|
|
2648
2730
|
},
|
|
2649
2731
|
() => undefined,
|
|
2650
2732
|
);
|
|
@@ -2690,24 +2772,60 @@ class WorkflowOwner {
|
|
|
2690
2772
|
);
|
|
2691
2773
|
}
|
|
2692
2774
|
|
|
2693
|
-
/** Where each
|
|
2775
|
+
/** Where each TUI agent's pane went, as its host reported it, in the order opened. */
|
|
2694
2776
|
panes(): AgentPaneRecord[] {
|
|
2695
2777
|
return [...this.#agents].flatMap(([agent, entry]) => {
|
|
2696
|
-
const
|
|
2697
|
-
|
|
2698
|
-
const { layout, keepPane } = entry.identity;
|
|
2699
|
-
return [
|
|
2700
|
-
{
|
|
2701
|
-
callPath: [],
|
|
2702
|
-
agent,
|
|
2703
|
-
...(layout === undefined ? {} : { layout }),
|
|
2704
|
-
...(keepPane === undefined ? {} : { keepPane }),
|
|
2705
|
-
placed,
|
|
2706
|
-
},
|
|
2707
|
-
];
|
|
2778
|
+
const pane = paneOf(entry);
|
|
2779
|
+
return pane ? [{ callPath: [], agent, ...pane }] : [];
|
|
2708
2780
|
});
|
|
2709
2781
|
}
|
|
2710
2782
|
|
|
2783
|
+
/**
|
|
2784
|
+
* Where an agent's sessions live, once it is known: its own harness home, the sandbox that home is
|
|
2785
|
+
* in, and the skills its harness was given, which `awf open` reopens (story 042).
|
|
2786
|
+
*/
|
|
2787
|
+
private placeOf(key: string): SessionPlace {
|
|
2788
|
+
const home = this.#agents.get(key)?.opening?.accounted.home;
|
|
2789
|
+
const sandbox = this.options.sandboxes.sessionSandbox(key);
|
|
2790
|
+
const skills = this.#givenSkills.get(key);
|
|
2791
|
+
return {
|
|
2792
|
+
...(home === undefined ? {} : { home }),
|
|
2793
|
+
...(sandbox ? { sandbox } : {}),
|
|
2794
|
+
...(skills ? { skills: { names: [...skills.names], directory: skills.directory } } : {}),
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
/** A session an agent's `wfa` named: recorded at once, less one a fork or parent holds. */
|
|
2799
|
+
private launcherReported(
|
|
2800
|
+
key: string,
|
|
2801
|
+
harness: HarnessKind,
|
|
2802
|
+
sessions: AgentSessions,
|
|
2803
|
+
id: string,
|
|
2804
|
+
): void {
|
|
2805
|
+
sessions.launcher.add(id);
|
|
2806
|
+
this.options.sessionLines.known(
|
|
2807
|
+
key,
|
|
2808
|
+
reportedSessions(sessions).map((session) => ({ harness, id: session })),
|
|
2809
|
+
this.placeOf(key),
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/**
|
|
2814
|
+
* Every agent's sessions as its close left them: one that moved in a compaction or fork at close,
|
|
2815
|
+
* or that its harness saw after its last operation, is what the next continue resumes.
|
|
2816
|
+
*/
|
|
2817
|
+
recordSessions(): void {
|
|
2818
|
+
for (const [key, entry] of this.#agents) {
|
|
2819
|
+
if (!entry.opening) continue;
|
|
2820
|
+
const { harness } = entry.opening.accounted.execution;
|
|
2821
|
+
this.options.sessionLines.known(
|
|
2822
|
+
key,
|
|
2823
|
+
reportedSessions(entry.opening.sessions).map((id) => ({ harness, id })),
|
|
2824
|
+
this.placeOf(key),
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2711
2829
|
/** A reopened agent names the sandbox it runs in, or none: a different one is a conflict. */
|
|
2712
2830
|
private async assertSameSandbox(
|
|
2713
2831
|
key: string,
|
|
@@ -2726,21 +2844,22 @@ class WorkflowOwner {
|
|
|
2726
2844
|
}
|
|
2727
2845
|
}
|
|
2728
2846
|
|
|
2847
|
+
/** An agent's pane as its workflow wrote it and as its host placed it; undefined for no pane. */
|
|
2848
|
+
function paneOf(entry: AgentEntry): Omit<AgentPaneRecord, "callPath" | "agent"> | undefined {
|
|
2849
|
+
const placed = entry.opening?.sessions.harness?.pane?.();
|
|
2850
|
+
if (!placed) return undefined;
|
|
2851
|
+
const { pane } = entry.identity;
|
|
2852
|
+
return { ...(pane === undefined ? {} : { pane }), placed };
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
// What a wait takes of it is type-only, and `asSource` marks it at run time.
|
|
2856
|
+
// biome-ignore lint/correctness/noUnusedVariables: merged into the class below.
|
|
2857
|
+
interface LogicalAgent extends Source<Taken<AgentEnded>> {}
|
|
2858
|
+
|
|
2859
|
+
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: the interface adds a type-only member.
|
|
2729
2860
|
class LogicalAgent implements AgentRef {
|
|
2730
2861
|
readonly [PARTICIPANT] = true as const;
|
|
2731
2862
|
readonly #operationStops = new Set<(stop: OperationStop) => Promise<void>>();
|
|
2732
|
-
readonly #operations = new Map<
|
|
2733
|
-
string,
|
|
2734
|
-
{
|
|
2735
|
-
spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>;
|
|
2736
|
-
result: Promise<RunResult<JsonValue>>;
|
|
2737
|
-
}
|
|
2738
|
-
>();
|
|
2739
|
-
readonly #compactions = new Map<
|
|
2740
|
-
string,
|
|
2741
|
-
{ spec: CompactSpec; result: Promise<TurnOutcome<string>> }
|
|
2742
|
-
>();
|
|
2743
|
-
readonly #sets = new Map<string, { spec: SettingsSpec; result: Promise<TurnOutcome<null>> }>();
|
|
2744
2863
|
/** Its settings now; see `AgentRef.execution`. */
|
|
2745
2864
|
#execution: AgentExecution;
|
|
2746
2865
|
/** What each `set` queued and not yet settled changes, in queue order. */
|
|
@@ -2781,6 +2900,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2781
2900
|
execution: AgentExecution;
|
|
2782
2901
|
session: HarnessSession;
|
|
2783
2902
|
slots: ResultSlotRegistry;
|
|
2903
|
+
/** Numbers its operations, `{key}.{n}`. */
|
|
2904
|
+
ops: OpIds;
|
|
2784
2905
|
endpoint: string;
|
|
2785
2906
|
/** The path this agent is told to run; see `installAgentLauncher`. */
|
|
2786
2907
|
launcher: string;
|
|
@@ -2789,6 +2910,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2789
2910
|
progress: RunProgress;
|
|
2790
2911
|
recordTurn: RecordTurn;
|
|
2791
2912
|
events: RunEventLog;
|
|
2913
|
+
/** Records the sessions it is seen with, in `operation` when one is named. */
|
|
2914
|
+
sessionsKnown(sessions: readonly NativeSessionRef[], operation?: string): void;
|
|
2792
2915
|
track<T>(promise: Promise<T>): Promise<T>;
|
|
2793
2916
|
isRunClosing(): boolean;
|
|
2794
2917
|
/** Throws when no turn may start: the attempt is looking on for values it needs. */
|
|
@@ -2804,14 +2927,16 @@ class LogicalAgent implements AgentRef {
|
|
|
2804
2927
|
*/
|
|
2805
2928
|
fork(
|
|
2806
2929
|
spec: AgentForkSpec,
|
|
2807
|
-
take:
|
|
2930
|
+
take: TakeFork | string,
|
|
2808
2931
|
settings: AgentExecution,
|
|
2809
2932
|
): Promise<AgentRef>;
|
|
2810
|
-
/**
|
|
2933
|
+
/** The workflow ends it: the run tears it down and its key is not opened again. */
|
|
2934
|
+
close(reason: string | undefined): Promise<boolean>;
|
|
2935
|
+
/** As the operator gave it; decides, at close, whether its pane stays. */
|
|
2811
2936
|
keepPane(): KeepPane | undefined;
|
|
2812
2937
|
/** As the workflow's last open of it gave it. */
|
|
2813
2938
|
keepAlive(): KeepAlive | undefined;
|
|
2814
|
-
/** Its window when `keepAlive` gives no `
|
|
2939
|
+
/** Its window when `keepAlive` gives no `window`. */
|
|
2815
2940
|
defaultWindowMs: number;
|
|
2816
2941
|
/** The run's time, which its window is measured on. */
|
|
2817
2942
|
now: () => number;
|
|
@@ -2825,7 +2950,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2825
2950
|
const restored = options.turns?.restored();
|
|
2826
2951
|
if (restored)
|
|
2827
2952
|
this.#started = {
|
|
2828
|
-
ref: turnRef(
|
|
2953
|
+
ref: turnRef(restored.turn, async () => false),
|
|
2829
2954
|
meta: restored.meta,
|
|
2830
2955
|
ended: true,
|
|
2831
2956
|
done: Promise.resolve(),
|
|
@@ -2865,7 +2990,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2865
2990
|
liveUntil(now = this.options.now()): number | undefined {
|
|
2866
2991
|
const keepAlive = this.options.keepAlive();
|
|
2867
2992
|
if (this.#closed || !keepAlive || this.#lastEnd === undefined) return undefined;
|
|
2868
|
-
const until = this.#lastEnd + (keepAlive.
|
|
2993
|
+
const until = this.#lastEnd + (keepAlive.windowMs ?? this.options.defaultWindowMs);
|
|
2869
2994
|
return until > now ? until : undefined;
|
|
2870
2995
|
}
|
|
2871
2996
|
|
|
@@ -2928,8 +3053,12 @@ class LogicalAgent implements AgentRef {
|
|
|
2928
3053
|
|
|
2929
3054
|
async start(spec: AgentStartSpec): Promise<StartedTurn> {
|
|
2930
3055
|
const { turns } = this.options;
|
|
2931
|
-
if (!turns)
|
|
3056
|
+
if (!turns)
|
|
3057
|
+
throw new Error(
|
|
3058
|
+
`agent ${this.key}: start() is for a workflow with events, at whose waits its end arrives; run() waits for the end here`,
|
|
3059
|
+
);
|
|
2932
3060
|
if (this.#closed) throw new Error("logical agent is closed");
|
|
3061
|
+
turns.checkDeclared();
|
|
2933
3062
|
const inTool = refusedInTool(this.key, "start()");
|
|
2934
3063
|
if (inTool) throw inTool;
|
|
2935
3064
|
const scope = scopes.getStore();
|
|
@@ -2940,12 +3069,12 @@ class LogicalAgent implements AgentRef {
|
|
|
2940
3069
|
const refused = this.#refusedWhileStarted("start");
|
|
2941
3070
|
if (refused) throw refused;
|
|
2942
3071
|
const { meta = null, ...run } = spec;
|
|
2943
|
-
const id =
|
|
3072
|
+
const id = randomUUID();
|
|
2944
3073
|
if (!isJsonValue(meta)) throw new Error("a turn's meta is a JSON value");
|
|
2945
3074
|
const ended = Promise.withResolvers<void>();
|
|
2946
3075
|
let written: TurnOutcome<JsonValue>["kind"] | undefined;
|
|
2947
3076
|
const started = {
|
|
2948
|
-
ref: turnRef(
|
|
3077
|
+
ref: turnRef(id, async (reason?: string) => {
|
|
2949
3078
|
if (started.ended) return false;
|
|
2950
3079
|
// Its turn waits for the handler, which would wait for the turn's end.
|
|
2951
3080
|
const inTool = refusedInTool(this.key, "cancel()");
|
|
@@ -2976,13 +3105,13 @@ class LogicalAgent implements AgentRef {
|
|
|
2976
3105
|
this.#launching = true;
|
|
2977
3106
|
let launched: Promise<RunResult<JsonValue>>;
|
|
2978
3107
|
try {
|
|
2979
|
-
launched = outsideTools(() => this
|
|
3108
|
+
launched = outsideTools(() => this.#runTurn(run, id));
|
|
2980
3109
|
} catch (error) {
|
|
2981
3110
|
launched = Promise.reject(error);
|
|
2982
3111
|
} finally {
|
|
2983
3112
|
this.#launching = false;
|
|
2984
3113
|
}
|
|
2985
|
-
// Tracked until the end is
|
|
3114
|
+
// Tracked until the end is written, so the run doesn't park with a turn's end on its way.
|
|
2986
3115
|
this.options.track(
|
|
2987
3116
|
launched
|
|
2988
3117
|
.then(
|
|
@@ -3023,11 +3152,12 @@ class LogicalAgent implements AgentRef {
|
|
|
3023
3152
|
);
|
|
3024
3153
|
}
|
|
3025
3154
|
|
|
3026
|
-
is<
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3155
|
+
is<
|
|
3156
|
+
Meta extends JsonValue = JsonValue,
|
|
3157
|
+
T extends JsonValue = JsonValue,
|
|
3158
|
+
E extends EventLike = EventLike,
|
|
3159
|
+
>(event: E): event is E & AgentEndOf<Meta, T> {
|
|
3160
|
+
return matches(event, { name: "agent:ended", from: this.key });
|
|
3031
3161
|
}
|
|
3032
3162
|
|
|
3033
3163
|
run(spec: AgentRunTextSpec): Promise<RunResult<string>>;
|
|
@@ -3035,51 +3165,24 @@ class LogicalAgent implements AgentRef {
|
|
|
3035
3165
|
run<T extends JsonValue>(
|
|
3036
3166
|
spec: AgentRunTextSpec | AgentRunStructuredSpec<T>,
|
|
3037
3167
|
): Promise<RunResult<string | T>> {
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3168
|
+
return this.#runTurn(spec, randomUUID()) as Promise<RunResult<string | T>>;
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
/** A turn under `id`: `start()`'s, which its end is recorded by, or a new one. */
|
|
3172
|
+
#runTurn(
|
|
3173
|
+
spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
|
|
3174
|
+
id: TurnId,
|
|
3175
|
+
): Promise<RunResult<JsonValue>> {
|
|
3176
|
+
const admitted = this.#admit<RunResult<JsonValue>, AbsoluteDeadline | undefined>(
|
|
3177
|
+
"run",
|
|
3178
|
+
{ checkOperation: true },
|
|
3179
|
+
(scope) => this.operationDeadline(spec, scope),
|
|
3180
|
+
);
|
|
3181
|
+
if ("rejected" in admitted) return admitted.rejected;
|
|
3182
|
+
const { scope, prepared: deadline } = admitted;
|
|
3048
3183
|
const opensWindow = !inTimeoutHandler();
|
|
3049
|
-
const
|
|
3050
|
-
const
|
|
3051
|
-
| AgentRunTextSpec
|
|
3052
|
-
| AgentRunStructuredSpec<JsonValue>;
|
|
3053
|
-
const existing = this.#operations.get(id);
|
|
3054
|
-
if (existing) {
|
|
3055
|
-
if (!isDeepStrictEqual(existing.spec, completeSpec)) {
|
|
3056
|
-
const rejected = Promise.reject<RunResult<string | T>>(
|
|
3057
|
-
new Error(`turn id ${id} was reused with a different specification`),
|
|
3058
|
-
);
|
|
3059
|
-
scope?.track(rejected);
|
|
3060
|
-
return rejected;
|
|
3061
|
-
}
|
|
3062
|
-
scope?.track(existing.result);
|
|
3063
|
-
return existing.result as Promise<RunResult<string | T>>;
|
|
3064
|
-
}
|
|
3065
|
-
let deadline: AbsoluteDeadline | undefined;
|
|
3066
|
-
try {
|
|
3067
|
-
deadline = this.operationDeadline(spec, scope);
|
|
3068
|
-
const recoveryDeadline = spec.nudge ? spec.nudge.deadline : undefined;
|
|
3069
|
-
if (recoveryDeadline) {
|
|
3070
|
-
assertDeadlineValue(recoveryDeadline);
|
|
3071
|
-
deadline = earlierDeadline(deadline, recoveryDeadline);
|
|
3072
|
-
}
|
|
3073
|
-
} catch (error) {
|
|
3074
|
-
const rejected = Promise.reject<RunResult<string | T>>(error);
|
|
3075
|
-
scope?.track(rejected);
|
|
3076
|
-
return rejected;
|
|
3077
|
-
}
|
|
3078
|
-
const result = this.queue(async () => {
|
|
3079
|
-
if (this.#closed || this.options.isRunClosing()) {
|
|
3080
|
-
throw new Error("logical agent is closed");
|
|
3081
|
-
}
|
|
3082
|
-
scope?.assertActive();
|
|
3184
|
+
const completeSpec: TurnSpec = structuredClone({ ...spec, id });
|
|
3185
|
+
const result = this.#queueActive(scope, async () => {
|
|
3083
3186
|
const cancelled = this.#cancelledTurns.get(id);
|
|
3084
3187
|
if (cancelled !== undefined) {
|
|
3085
3188
|
this.#cancelledTurns.delete(id);
|
|
@@ -3093,7 +3196,19 @@ class LogicalAgent implements AgentRef {
|
|
|
3093
3196
|
}
|
|
3094
3197
|
const tags = tagsOf(scope, completeSpec.label);
|
|
3095
3198
|
// Drawn here, so a turn that throws before it is delivered is still written as failed.
|
|
3096
|
-
|
|
3199
|
+
let schema: JsonSchema | undefined;
|
|
3200
|
+
try {
|
|
3201
|
+
schema = resultSchema(completeSpec.schema);
|
|
3202
|
+
} catch {
|
|
3203
|
+
// Thrown again as the turn runs, under its id, so it is written as failed.
|
|
3204
|
+
}
|
|
3205
|
+
const { id: drawn, lost } = this.options.ops.draw(this.key, {
|
|
3206
|
+
kind: "turn",
|
|
3207
|
+
...this.opWhere(tags),
|
|
3208
|
+
question: completeSpec.prompt,
|
|
3209
|
+
...(schema === undefined ? {} : { schema }),
|
|
3210
|
+
});
|
|
3211
|
+
const operation = this.operationLog(drawn, "turn", tags, lost);
|
|
3097
3212
|
this.options.progress.turnStarted(this.key, tags);
|
|
3098
3213
|
try {
|
|
3099
3214
|
const settled = await this.executeOperation(completeSpec, scope, deadline, operation);
|
|
@@ -3107,54 +3222,25 @@ class LogicalAgent implements AgentRef {
|
|
|
3107
3222
|
await this.options.afterOperation?.();
|
|
3108
3223
|
}
|
|
3109
3224
|
});
|
|
3110
|
-
|
|
3111
|
-
scope?.track(tracked);
|
|
3112
|
-
this.#operations.set(id, { spec: completeSpec, result: tracked });
|
|
3113
|
-
return tracked as Promise<RunResult<string | T>>;
|
|
3225
|
+
return this.#tracked(scope, result);
|
|
3114
3226
|
}
|
|
3115
3227
|
|
|
3116
3228
|
compact(spec: CompactSpec): Promise<TurnOutcome<string>> {
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
}
|
|
3127
|
-
const id = spec.id ?? randomUUID();
|
|
3128
|
-
const completeSpec = structuredClone({ ...spec, id });
|
|
3129
|
-
const existing = this.#compactions.get(id);
|
|
3130
|
-
if (existing) {
|
|
3131
|
-
if (!isDeepStrictEqual(existing.spec, completeSpec)) {
|
|
3132
|
-
const rejected = Promise.reject<TurnOutcome<string>>(
|
|
3133
|
-
new Error(`compaction id ${id} was reused with a different specification`),
|
|
3134
|
-
);
|
|
3135
|
-
scope?.track(rejected);
|
|
3136
|
-
return rejected;
|
|
3137
|
-
}
|
|
3138
|
-
scope?.track(existing.result);
|
|
3139
|
-
return existing.result;
|
|
3140
|
-
}
|
|
3141
|
-
let deadline: AbsoluteDeadline;
|
|
3142
|
-
try {
|
|
3143
|
-
deadline = harnessDeadline(this.operationDeadline(spec, scope));
|
|
3144
|
-
} catch (error) {
|
|
3145
|
-
const rejected = Promise.reject<TurnOutcome<string>>(error);
|
|
3146
|
-
scope?.track(rejected);
|
|
3147
|
-
return rejected;
|
|
3148
|
-
}
|
|
3149
|
-
const result = this.queue(async () => {
|
|
3150
|
-
if (this.#closed || this.options.isRunClosing()) {
|
|
3151
|
-
throw new Error("logical agent is closed");
|
|
3152
|
-
}
|
|
3153
|
-
scope?.assertActive();
|
|
3229
|
+
const admitted = this.#admit<TurnOutcome<string>, AbsoluteDeadline>(
|
|
3230
|
+
"compact",
|
|
3231
|
+
{ checkOperation: true },
|
|
3232
|
+
(scope) => harnessDeadline(this.operationDeadline(spec, scope)),
|
|
3233
|
+
);
|
|
3234
|
+
if ("rejected" in admitted) return admitted.rejected;
|
|
3235
|
+
const { scope, prepared: deadline } = admitted;
|
|
3236
|
+
const id = randomUUID();
|
|
3237
|
+
const result = this.#queueActive(scope, async () => {
|
|
3154
3238
|
const tags = tagsOf(scope);
|
|
3155
|
-
const
|
|
3239
|
+
const drawn = this.options.ops.draw(this.key, { kind: "compact", ...this.opWhere(tags) });
|
|
3240
|
+
const operation = this.operationLog(drawn.id, "compact", tags);
|
|
3156
3241
|
this.options.progress.turnStarted(this.key, tags, "compact");
|
|
3157
3242
|
try {
|
|
3243
|
+
if (drawn.lost !== undefined) throw new Error(drawn.lost);
|
|
3158
3244
|
const outcome = await this.executeCompaction(id, spec.prompt, deadline, scope, operation);
|
|
3159
3245
|
this.turnEnded(outcome, "compact");
|
|
3160
3246
|
return outcome;
|
|
@@ -3166,47 +3252,23 @@ class LogicalAgent implements AgentRef {
|
|
|
3166
3252
|
await this.options.afterOperation?.();
|
|
3167
3253
|
}
|
|
3168
3254
|
});
|
|
3169
|
-
|
|
3170
|
-
scope?.track(tracked);
|
|
3171
|
-
this.#compactions.set(id, { spec: completeSpec, result: tracked });
|
|
3172
|
-
return tracked;
|
|
3255
|
+
return this.#tracked(scope, result);
|
|
3173
3256
|
}
|
|
3174
3257
|
|
|
3175
3258
|
/** Not a turn: progress counts none for it, and it asks the model nothing. */
|
|
3176
3259
|
set(spec: SettingsSpec): Promise<TurnOutcome<null>> {
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
scope
|
|
3183
|
-
}
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
if (existing) {
|
|
3190
|
-
if (!isDeepStrictEqual(existing.spec, completeSpec)) {
|
|
3191
|
-
const rejected = Promise.reject<TurnOutcome<null>>(
|
|
3192
|
-
new Error(`settings id ${id} was reused with a different specification`),
|
|
3193
|
-
);
|
|
3194
|
-
scope?.track(rejected);
|
|
3195
|
-
return rejected;
|
|
3196
|
-
}
|
|
3197
|
-
scope?.track(existing.result);
|
|
3198
|
-
return existing.result;
|
|
3199
|
-
}
|
|
3200
|
-
let change: SettingsChange;
|
|
3201
|
-
let deadline: AbsoluteDeadline;
|
|
3202
|
-
try {
|
|
3203
|
-
change = this.settingsChange(spec);
|
|
3204
|
-
deadline = harnessDeadline(this.operationDeadline(spec, scope));
|
|
3205
|
-
} catch (error) {
|
|
3206
|
-
const rejected = Promise.reject<TurnOutcome<null>>(error);
|
|
3207
|
-
scope?.track(rejected);
|
|
3208
|
-
return rejected;
|
|
3209
|
-
}
|
|
3260
|
+
const admitted = this.#admit<
|
|
3261
|
+
TurnOutcome<null>,
|
|
3262
|
+
{ change: SettingsChange; deadline: AbsoluteDeadline }
|
|
3263
|
+
>("set", { checkOperation: false }, (scope) => ({
|
|
3264
|
+
change: this.settingsChange(spec),
|
|
3265
|
+
deadline: harnessDeadline(this.operationDeadline(spec, scope)),
|
|
3266
|
+
}));
|
|
3267
|
+
if ("rejected" in admitted) return admitted.rejected;
|
|
3268
|
+
const {
|
|
3269
|
+
scope,
|
|
3270
|
+
prepared: { change, deadline },
|
|
3271
|
+
} = admitted;
|
|
3210
3272
|
this.#pendingSets.push(change);
|
|
3211
3273
|
const result = this.queue(async () => {
|
|
3212
3274
|
try {
|
|
@@ -3221,12 +3283,68 @@ class LogicalAgent implements AgentRef {
|
|
|
3221
3283
|
this.#pendingSets.splice(this.#pendingSets.indexOf(change), 1);
|
|
3222
3284
|
}
|
|
3223
3285
|
});
|
|
3286
|
+
return this.#tracked(scope, result);
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
/**
|
|
3290
|
+
* The checks before an operation is queued, in order: the agent open, no started turn in its
|
|
3291
|
+
* way, the scope accepting and, with `checkOperation`, the stage; then `prepare`, whose refusal
|
|
3292
|
+
* the scope tracks. A refusal is a rejection, never a throw.
|
|
3293
|
+
*/
|
|
3294
|
+
#admit<T, P>(
|
|
3295
|
+
what: string,
|
|
3296
|
+
options: { checkOperation: boolean },
|
|
3297
|
+
prepare: (scope: ExecutionScope | undefined) => P,
|
|
3298
|
+
): { scope: ExecutionScope | undefined; prepared: P } | { rejected: Promise<T> } {
|
|
3299
|
+
if (this.#closed) return { rejected: Promise.reject(new Error("logical agent is closed")) };
|
|
3300
|
+
const refused = this.#refusedWhileStarted(what);
|
|
3301
|
+
if (refused) return { rejected: Promise.reject(refused) };
|
|
3302
|
+
const scope = scopes.getStore();
|
|
3303
|
+
try {
|
|
3304
|
+
scope?.assertAccepting();
|
|
3305
|
+
if (options.checkOperation) this.options.checkOperation();
|
|
3306
|
+
} catch (error) {
|
|
3307
|
+
return { rejected: Promise.reject(error) };
|
|
3308
|
+
}
|
|
3309
|
+
try {
|
|
3310
|
+
return { scope, prepared: prepare(scope) };
|
|
3311
|
+
} catch (error) {
|
|
3312
|
+
const rejected = Promise.reject<T>(error);
|
|
3313
|
+
scope?.track(rejected);
|
|
3314
|
+
return { rejected };
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
/** `work` once the queue reaches it, the agent and run still open and `scope` still active. */
|
|
3319
|
+
#queueActive<T>(scope: ExecutionScope | undefined, work: () => Promise<T>): Promise<T> {
|
|
3320
|
+
return this.queue(async () => {
|
|
3321
|
+
if (this.#closed || this.options.isRunClosing()) {
|
|
3322
|
+
throw new Error("logical agent is closed");
|
|
3323
|
+
}
|
|
3324
|
+
scope?.assertActive();
|
|
3325
|
+
return work();
|
|
3326
|
+
});
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
#tracked<T>(scope: ExecutionScope | undefined, result: Promise<T>): Promise<T> {
|
|
3224
3330
|
const tracked = this.options.track(result);
|
|
3225
3331
|
scope?.track(tracked);
|
|
3226
|
-
this.#sets.set(id, { spec: completeSpec, result: tracked });
|
|
3227
3332
|
return tracked;
|
|
3228
3333
|
}
|
|
3229
3334
|
|
|
3335
|
+
/**
|
|
3336
|
+
* Whose an operation is, the settings it runs at and where, as its `op.json` says: what its spend
|
|
3337
|
+
* is read with, by the next attempt when this one never settles it.
|
|
3338
|
+
*/
|
|
3339
|
+
private opWhere(tags: { stage?: string }): AgentOp & { stage?: string } {
|
|
3340
|
+
return {
|
|
3341
|
+
agent: this.key,
|
|
3342
|
+
execution: this.#execution,
|
|
3343
|
+
cwd: this.options.ledger.cwd(),
|
|
3344
|
+
...(tags.stage === undefined ? {} : { stage: tags.stage }),
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3230
3348
|
/**
|
|
3231
3349
|
* An operation's start and failure in the run's event log, under the id its record keeps. Its
|
|
3232
3350
|
* settling is the turn line `recordTurn` writes.
|
|
@@ -3235,10 +3353,12 @@ class LogicalAgent implements AgentRef {
|
|
|
3235
3353
|
operationId: string,
|
|
3236
3354
|
of: TurnRecord["kind"],
|
|
3237
3355
|
tags: OperationTags,
|
|
3356
|
+
unrecorded?: string,
|
|
3238
3357
|
): OperationLog {
|
|
3239
3358
|
let deliveredAt: number | undefined;
|
|
3240
3359
|
/** Named by its host before delivery, for its start to carry. */
|
|
3241
3360
|
const named: string[] = [];
|
|
3361
|
+
// Its sessions as of now, recorded before the line that names it, as a turn's are.
|
|
3242
3362
|
const fields = () => {
|
|
3243
3363
|
const { callPath, sessions, ...record } = this.options.ledger.unsettled(
|
|
3244
3364
|
operationId,
|
|
@@ -3248,16 +3368,16 @@ class LogicalAgent implements AgentRef {
|
|
|
3248
3368
|
);
|
|
3249
3369
|
const harness = this.#execution.harness;
|
|
3250
3370
|
const fresh = named.filter((id) => !sessions.some((known) => known.id === id));
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
};
|
|
3371
|
+
this.options.sessionsKnown(
|
|
3372
|
+
[...sessions, ...fresh.map((id) => ({ harness, id }))],
|
|
3373
|
+
operationId,
|
|
3374
|
+
);
|
|
3375
|
+
return { of, callPath, ...record };
|
|
3257
3376
|
};
|
|
3258
3377
|
return {
|
|
3259
3378
|
operationId,
|
|
3260
3379
|
tags,
|
|
3380
|
+
...(unrecorded === undefined ? {} : { unrecorded }),
|
|
3261
3381
|
delivered: (at: number) => {
|
|
3262
3382
|
deliveredAt = at;
|
|
3263
3383
|
this.#hasRun = true;
|
|
@@ -3269,11 +3389,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3269
3389
|
named.push(id);
|
|
3270
3390
|
return;
|
|
3271
3391
|
}
|
|
3272
|
-
this.options.
|
|
3273
|
-
kind: "session-known",
|
|
3274
|
-
session: { harness: this.#execution.harness, id },
|
|
3275
|
-
operationId,
|
|
3276
|
-
});
|
|
3392
|
+
this.options.sessionsKnown([{ harness: this.#execution.harness, id }], operationId);
|
|
3277
3393
|
},
|
|
3278
3394
|
failed: (error: unknown) =>
|
|
3279
3395
|
this.options.events.event({
|
|
@@ -3284,7 +3400,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3284
3400
|
};
|
|
3285
3401
|
}
|
|
3286
3402
|
|
|
3287
|
-
/** A turn or compaction that settled, as the view shows it and `
|
|
3403
|
+
/** A turn or compaction that settled, as the view shows it and `log.jsonl` keeps it. */
|
|
3288
3404
|
private turnEnded(
|
|
3289
3405
|
outcome: TurnOutcome<JsonValue>,
|
|
3290
3406
|
kind: TurnRecord["kind"],
|
|
@@ -3298,6 +3414,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3298
3414
|
this.#last = outcome.kind;
|
|
3299
3415
|
const reason = "reason" in outcome ? outcome.reason : undefined;
|
|
3300
3416
|
this.options.progress.turnSettled(this.key, outcome.kind, reason);
|
|
3417
|
+
this.options.sessionsKnown(outcome.usage.sessions, outcome.usage.operationId);
|
|
3301
3418
|
this.options.recordTurn(outcome, kind);
|
|
3302
3419
|
}
|
|
3303
3420
|
|
|
@@ -3306,7 +3423,6 @@ class LogicalAgent implements AgentRef {
|
|
|
3306
3423
|
const refused = this.#refusedWhileStarted("fork");
|
|
3307
3424
|
if (refused) return Promise.reject(refused);
|
|
3308
3425
|
const scope = scopes.getStore();
|
|
3309
|
-
const deadline = this.deadlineCeiling(scope);
|
|
3310
3426
|
const { session, execution } = this.options;
|
|
3311
3427
|
const settings = this.queuedExecution();
|
|
3312
3428
|
const fork = session.fork?.bind(session);
|
|
@@ -3325,12 +3441,8 @@ class LogicalAgent implements AgentRef {
|
|
|
3325
3441
|
}
|
|
3326
3442
|
// The harness bounds its fork by this deadline and stops it there, so the session is idle again
|
|
3327
3443
|
// when the fork rejects.
|
|
3328
|
-
const take = (into
|
|
3329
|
-
this
|
|
3330
|
-
if (this.#closed || this.options.isRunClosing()) {
|
|
3331
|
-
throw new Error("logical agent is closed");
|
|
3332
|
-
}
|
|
3333
|
-
scope?.assertActive();
|
|
3444
|
+
const take: TakeFork = (deadline, into) =>
|
|
3445
|
+
this.#queueActive(scope, async () => {
|
|
3334
3446
|
// The fork was opened at the settings it would copy; a `set` before it that did not take
|
|
3335
3447
|
// would leave it recording settings it never ran at.
|
|
3336
3448
|
if (!isDeepStrictEqual(sessionSettings(this.#execution), sessionSettings(settings))) {
|
|
@@ -3348,19 +3460,12 @@ class LogicalAgent implements AgentRef {
|
|
|
3348
3460
|
}
|
|
3349
3461
|
}
|
|
3350
3462
|
|
|
3351
|
-
/**
|
|
3463
|
+
/** Its `timeout`, never later than the scope's or the run's; else the earlier of those. */
|
|
3352
3464
|
private operationDeadline(
|
|
3353
|
-
spec: {
|
|
3465
|
+
spec: { timeout?: Timeout },
|
|
3354
3466
|
scope: ExecutionScope | undefined,
|
|
3355
3467
|
): AbsoluteDeadline | undefined {
|
|
3356
|
-
|
|
3357
|
-
throw new Error("an operation cannot specify both deadline and timeoutMs");
|
|
3358
|
-
}
|
|
3359
|
-
const ceiling = this.deadlineCeiling(scope);
|
|
3360
|
-
if (spec.timeoutMs !== undefined) return deadlineWithin(spec.timeoutMs, ceiling);
|
|
3361
|
-
if (!spec.deadline) return ceiling;
|
|
3362
|
-
assertDeadlineValue(spec.deadline);
|
|
3363
|
-
return earlierDeadline(spec.deadline, ceiling);
|
|
3468
|
+
return boundedDeadline(spec.timeout, this.deadlineCeiling(scope));
|
|
3364
3469
|
}
|
|
3365
3470
|
|
|
3366
3471
|
/** The deadlines around an operation: its scope's and the run's, if either has one. */
|
|
@@ -3381,9 +3486,19 @@ class LogicalAgent implements AgentRef {
|
|
|
3381
3486
|
`agent ${this.key} is the calling session, whose model and effort are the operator's`,
|
|
3382
3487
|
);
|
|
3383
3488
|
}
|
|
3489
|
+
const removed = Object.keys(spec).filter(
|
|
3490
|
+
(field) =>
|
|
3491
|
+
REMOVED_SETTINGS_FIELDS.has(field) && (spec as Record<string, unknown>)[field] != null,
|
|
3492
|
+
);
|
|
3493
|
+
if (removed.length > 0) {
|
|
3494
|
+
const bound = removed.some((field) => field !== "id") ? ": bound it with timeout" : "";
|
|
3495
|
+
throw new Error(`set takes no ${removed.join(", ")}${bound}`);
|
|
3496
|
+
}
|
|
3384
3497
|
// A workflow is untyped JavaScript at run time: a harness or placement asked for must not be
|
|
3385
3498
|
// dropped in silence.
|
|
3386
|
-
const others = Object.keys(spec).filter(
|
|
3499
|
+
const others = Object.keys(spec).filter(
|
|
3500
|
+
(field) => !SETTINGS_FIELDS.has(field) && !REMOVED_SETTINGS_FIELDS.has(field),
|
|
3501
|
+
);
|
|
3387
3502
|
if (others.length > 0) {
|
|
3388
3503
|
throw new Error(
|
|
3389
3504
|
`set changes an agent's model and effort, not its ${others.join(", ")}: another harness or placement is another agent`,
|
|
@@ -3432,7 +3547,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3432
3547
|
};
|
|
3433
3548
|
}
|
|
3434
3549
|
const next: AgentExecution = { ...this.#execution, ...change };
|
|
3435
|
-
const pane = placementOf(next) === "
|
|
3550
|
+
const pane = placementOf(next) === "tui";
|
|
3436
3551
|
let cancelled: string | undefined;
|
|
3437
3552
|
// A pane's switch waits on its screen; a scope cancelled meanwhile closes the agent, as a
|
|
3438
3553
|
// half-made switch leaves it at settings nobody knows. A headless one takes no time.
|
|
@@ -3440,7 +3555,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3440
3555
|
? scope?.add(async (stop) => {
|
|
3441
3556
|
cancelled = stop.reason;
|
|
3442
3557
|
this.#last = "cancelled";
|
|
3443
|
-
await this.
|
|
3558
|
+
await this.terminate(stop.reason);
|
|
3444
3559
|
})
|
|
3445
3560
|
: undefined;
|
|
3446
3561
|
try {
|
|
@@ -3477,10 +3592,15 @@ class LogicalAgent implements AgentRef {
|
|
|
3477
3592
|
if (this.options.execution.caller) return;
|
|
3478
3593
|
// Before its outcome is recorded: whether its pane stays turns on how this one ended.
|
|
3479
3594
|
this.#last = ended;
|
|
3480
|
-
void this.
|
|
3595
|
+
void this.terminate(reason).catch(() => undefined);
|
|
3481
3596
|
}
|
|
3482
3597
|
|
|
3483
|
-
close(reason?: string): Promise<
|
|
3598
|
+
close(reason?: string): Promise<boolean> {
|
|
3599
|
+
return this.options.close(reason);
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
/** What `close()`, a failed operation and the run's end each come to. */
|
|
3603
|
+
terminate(reason?: string): Promise<void> {
|
|
3484
3604
|
const first = !this.#closed;
|
|
3485
3605
|
this.#closed = true;
|
|
3486
3606
|
if (first) {
|
|
@@ -3513,13 +3633,17 @@ class LogicalAgent implements AgentRef {
|
|
|
3513
3633
|
// Once what it was running has ended, so its last operation's line comes first.
|
|
3514
3634
|
if (first) {
|
|
3515
3635
|
const { key, execution } = this.options;
|
|
3516
|
-
const stoppedLine = () =>
|
|
3636
|
+
const stoppedLine = () => {
|
|
3637
|
+
const placed = this.options.session.pane?.();
|
|
3517
3638
|
this.options.events.event({
|
|
3518
3639
|
kind: "agent-stopped",
|
|
3519
3640
|
agent: key,
|
|
3520
3641
|
callPath: [],
|
|
3521
3642
|
harness: execution.harness,
|
|
3643
|
+
...(placed?.kept ? { kept: true } : {}),
|
|
3644
|
+
...(placed?.notKept === undefined ? {} : { notKept: placed.notKept }),
|
|
3522
3645
|
});
|
|
3646
|
+
};
|
|
3523
3647
|
closed.then(stoppedLine, stoppedLine);
|
|
3524
3648
|
}
|
|
3525
3649
|
return closed;
|
|
@@ -3557,22 +3681,21 @@ class LogicalAgent implements AgentRef {
|
|
|
3557
3681
|
}
|
|
3558
3682
|
|
|
3559
3683
|
private async executeOperation(
|
|
3560
|
-
spec:
|
|
3684
|
+
spec: TurnSpec,
|
|
3561
3685
|
scope: ExecutionScope | undefined,
|
|
3562
3686
|
operationDeadline: AbsoluteDeadline | undefined,
|
|
3563
3687
|
operation: OperationLog,
|
|
3564
3688
|
): Promise<RunResult<JsonValue>> {
|
|
3565
3689
|
const nudge = spec.nudge === false ? undefined : (spec.nudge ?? this.defaultNudge());
|
|
3566
3690
|
scope?.assertActive();
|
|
3567
|
-
if (nudge?.deadline) assertDeadlineValue(nudge.deadline);
|
|
3568
3691
|
return this.executeWaitingOperation(spec, scope, operationDeadline, operation, nudge);
|
|
3569
3692
|
}
|
|
3570
3693
|
|
|
3571
3694
|
private async executeWaitingOperation(
|
|
3572
|
-
spec:
|
|
3695
|
+
spec: TurnSpec,
|
|
3573
3696
|
scope: ExecutionScope | undefined,
|
|
3574
3697
|
initial: AbsoluteDeadline | undefined,
|
|
3575
|
-
{ operationId, tags, delivered, session }: OperationLog,
|
|
3698
|
+
{ operationId, tags, delivered, session, unrecorded }: OperationLog,
|
|
3576
3699
|
nudge: Exclude<AgentRunTextSpec["nudge"], false>,
|
|
3577
3700
|
): Promise<RunResult<JsonValue>> {
|
|
3578
3701
|
const schema = resultSchema(spec.schema);
|
|
@@ -3634,7 +3757,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3634
3757
|
].join("\n")
|
|
3635
3758
|
: "");
|
|
3636
3759
|
const events = createOperationEvents(this.options.runDir, operationId);
|
|
3637
|
-
const messages = this.operationMessages(operationId, `${spec.id
|
|
3760
|
+
const messages = this.operationMessages(operationId, `${spec.id}:message`, {
|
|
3638
3761
|
deadline: () => harnessDeadline(deadline),
|
|
3639
3762
|
authored,
|
|
3640
3763
|
session,
|
|
@@ -3645,6 +3768,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3645
3768
|
event: events.record,
|
|
3646
3769
|
operationId,
|
|
3647
3770
|
agentId: this.key,
|
|
3771
|
+
...(unrecorded === undefined ? {} : { unrecorded }),
|
|
3648
3772
|
question: spec.prompt,
|
|
3649
3773
|
schema,
|
|
3650
3774
|
...(deadline ? { deadline } : {}),
|
|
@@ -3666,7 +3790,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3666
3790
|
this.options.session
|
|
3667
3791
|
.start(
|
|
3668
3792
|
{
|
|
3669
|
-
id: spec.id
|
|
3793
|
+
id: spec.id,
|
|
3670
3794
|
prompt: [
|
|
3671
3795
|
operationPrompt(prompt, schema, this.options.launcher, operationId),
|
|
3672
3796
|
toolsLesson(
|
|
@@ -3686,7 +3810,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3686
3810
|
.then((turn) => this.confirmed(operationId, joined?.numbers ?? [], turn)),
|
|
3687
3811
|
successor: (turn, sequence, deliverySignal) =>
|
|
3688
3812
|
turn.nudge({
|
|
3689
|
-
id: `${spec.id
|
|
3813
|
+
id: `${spec.id}:check-in:${sequence}`,
|
|
3690
3814
|
deadline: harnessDeadline(deadline),
|
|
3691
3815
|
deliverySignal,
|
|
3692
3816
|
prompt: checkInPrompt(recoveryPrompt),
|
|
@@ -3700,24 +3824,25 @@ class LogicalAgent implements AgentRef {
|
|
|
3700
3824
|
return settle(request);
|
|
3701
3825
|
};
|
|
3702
3826
|
this.#operationStops.add(stop);
|
|
3703
|
-
this.#stopsByTurn.set(spec.id
|
|
3827
|
+
this.#stopsByTurn.set(spec.id, stop);
|
|
3704
3828
|
const remove = scope?.add(stop);
|
|
3705
3829
|
return () => {
|
|
3706
3830
|
this.#operationStops.delete(stop);
|
|
3707
|
-
if (this.#stopsByTurn.get(spec.id
|
|
3831
|
+
if (this.#stopsByTurn.get(spec.id) === stop) this.#stopsByTurn.delete(spec.id);
|
|
3708
3832
|
remove?.();
|
|
3709
3833
|
};
|
|
3710
3834
|
},
|
|
3711
3835
|
phase: (phase, reason, until) =>
|
|
3712
3836
|
this.options.progress.turnPhase(this.key, phase, reason, until),
|
|
3713
3837
|
delivered,
|
|
3838
|
+
keepScreen: (turn) => keepScreen(this.options.runDir, operationId, turn),
|
|
3714
3839
|
})
|
|
3715
3840
|
.catch(
|
|
3716
3841
|
(error: unknown): SupervisedOutcome => ({
|
|
3717
3842
|
kind: "failed",
|
|
3718
3843
|
reason: messageOf(error),
|
|
3719
3844
|
cleanupUnresolved: true,
|
|
3720
|
-
|
|
3845
|
+
reportedCosts: [],
|
|
3721
3846
|
settledAt: Date.now(),
|
|
3722
3847
|
}),
|
|
3723
3848
|
)
|
|
@@ -3737,7 +3862,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3737
3862
|
...(outcome.deliveredAt === undefined ? {} : { deliveredAt: outcome.deliveredAt }),
|
|
3738
3863
|
settledAt: outcome.settledAt,
|
|
3739
3864
|
},
|
|
3740
|
-
outcome.
|
|
3865
|
+
outcome.reportedCosts,
|
|
3741
3866
|
);
|
|
3742
3867
|
if (outcome.kind === "answered")
|
|
3743
3868
|
return {
|
|
@@ -3883,7 +4008,10 @@ class LogicalAgent implements AgentRef {
|
|
|
3883
4008
|
stage: ExecutionScope | undefined,
|
|
3884
4009
|
): Promise<void> {
|
|
3885
4010
|
const { messaging } = this.options;
|
|
3886
|
-
const operationId =
|
|
4011
|
+
const { id: operationId, lost } = this.options.ops.draw(this.key, {
|
|
4012
|
+
kind: "message",
|
|
4013
|
+
...this.opWhere(stage?.stage === undefined ? {} : { stage: stage.stage }),
|
|
4014
|
+
});
|
|
3887
4015
|
const ceiling = this.deadlineCeiling(stage)?.unixMilliseconds;
|
|
3888
4016
|
let deadline = Math.min(Date.now() + messaging.limits.operationMs, ceiling ?? Infinity);
|
|
3889
4017
|
messaging.opened({
|
|
@@ -3906,7 +4034,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3906
4034
|
let end: SupervisedOutcome["kind"] | undefined;
|
|
3907
4035
|
try {
|
|
3908
4036
|
const opening = messaging.take(operationId, "opens");
|
|
3909
|
-
// Withdrawn before it could be shown: nothing to wake it for.
|
|
4037
|
+
// Withdrawn before it could be shown: nothing to wake it for. Its folder stays, its number used.
|
|
3910
4038
|
if (!opening) return;
|
|
3911
4039
|
if (opening.deadline !== undefined) deadline = opening.deadline;
|
|
3912
4040
|
const cause = { message: { from } };
|
|
@@ -3917,6 +4045,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3917
4045
|
const operation = this.operationLog(operationId, "turn", tags);
|
|
3918
4046
|
this.options.progress.turnStarted(this.key, tags);
|
|
3919
4047
|
try {
|
|
4048
|
+
if (lost !== undefined) throw new Error(lost);
|
|
3920
4049
|
const outcome = await this.superviseMessage(operationId, deadline, opening, {
|
|
3921
4050
|
cause,
|
|
3922
4051
|
tags,
|
|
@@ -4031,6 +4160,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4031
4160
|
phase: (phase, reason, until) =>
|
|
4032
4161
|
this.options.progress.turnPhase(this.key, phase, reason, until),
|
|
4033
4162
|
delivered: operation.delivered,
|
|
4163
|
+
keepScreen: (turn) => keepScreen(this.options.runDir, operationId, turn),
|
|
4034
4164
|
...(this.options.livenessPolicy ? { policy: this.options.livenessPolicy } : {}),
|
|
4035
4165
|
});
|
|
4036
4166
|
} finally {
|
|
@@ -4047,7 +4177,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4047
4177
|
...(outcome.deliveredAt === undefined ? {} : { deliveredAt: outcome.deliveredAt }),
|
|
4048
4178
|
settledAt: outcome.settledAt,
|
|
4049
4179
|
},
|
|
4050
|
-
outcome.
|
|
4180
|
+
outcome.reportedCosts,
|
|
4051
4181
|
);
|
|
4052
4182
|
// Recorded `answered` with no value: it did what it was woken for, and takes no result.
|
|
4053
4183
|
this.turnEnded(
|
|
@@ -4078,7 +4208,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4078
4208
|
const times: { deliveredAt?: number } = {};
|
|
4079
4209
|
const settle = (
|
|
4080
4210
|
native: HarnessTurnOutcome | "expired",
|
|
4081
|
-
/** A compaction left to end on its own, whose
|
|
4211
|
+
/** A compaction left to end on its own, whose reported costs come when it does. */
|
|
4082
4212
|
later?: Promise<HarnessTurnOutcome>,
|
|
4083
4213
|
): TurnOutcome<string> => {
|
|
4084
4214
|
const usage = entry.settle(
|
|
@@ -4086,7 +4216,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4086
4216
|
...times,
|
|
4087
4217
|
settledAt: Math.min(Date.now(), deadline.unixMilliseconds),
|
|
4088
4218
|
},
|
|
4089
|
-
native === "expired" ? [] : native.
|
|
4219
|
+
native === "expired" ? [] : native.reportedCostsUsd,
|
|
4090
4220
|
later,
|
|
4091
4221
|
);
|
|
4092
4222
|
if (native === "expired") {
|
|
@@ -4134,7 +4264,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4134
4264
|
state: "failed",
|
|
4135
4265
|
detail: messageOf(error),
|
|
4136
4266
|
resultEvidence: { kind: "unavailable" },
|
|
4137
|
-
|
|
4267
|
+
reportedCostsUsd: [],
|
|
4138
4268
|
});
|
|
4139
4269
|
}
|
|
4140
4270
|
times.deliveredAt = Date.now();
|
|
@@ -4148,8 +4278,14 @@ class LogicalAgent implements AgentRef {
|
|
|
4148
4278
|
cancelTimer = scheduleAt(deadline, () => resolve("expired"));
|
|
4149
4279
|
}),
|
|
4150
4280
|
]).finally(() => cancelTimer?.());
|
|
4151
|
-
if (native === "expired")
|
|
4152
|
-
|
|
4281
|
+
if (native === "expired") {
|
|
4282
|
+
await keepScreen(this.options.runDir, operationId, turn);
|
|
4283
|
+
return settle(native, (await this.leaveFinishing(turn)).later);
|
|
4284
|
+
}
|
|
4285
|
+
const outcome = settle(native);
|
|
4286
|
+
if (!outcome.answered && outcome.kind !== "cancelled")
|
|
4287
|
+
await keepScreen(this.options.runDir, operationId, turn);
|
|
4288
|
+
return outcome;
|
|
4153
4289
|
} finally {
|
|
4154
4290
|
removeCanceller?.();
|
|
4155
4291
|
}
|
|
@@ -4183,10 +4319,27 @@ function checkedWhen(when: unknown): string | Date {
|
|
|
4183
4319
|
throw new Error('sleep is for a duration, "2h", or until a Date');
|
|
4184
4320
|
}
|
|
4185
4321
|
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4322
|
+
/** Receive's options as given, checked: what a typed caller can't get wrong, a plain one can. */
|
|
4323
|
+
function receiveLabel(options: unknown): string | undefined {
|
|
4324
|
+
if (options === undefined) return undefined;
|
|
4325
|
+
if (typeof options !== "object" || options === null) throw new Error("receive takes { label? }");
|
|
4326
|
+
// `take: "all"` was the default, so only "oldest" asked for what receive no longer does.
|
|
4327
|
+
const { take } = options as { take?: unknown };
|
|
4328
|
+
if (take !== undefined && take !== "all")
|
|
4329
|
+
throw new Error(
|
|
4330
|
+
"receive takes every event not yet acknowledged; wf.select takes one at a time",
|
|
4331
|
+
);
|
|
4332
|
+
if ("until" in options)
|
|
4333
|
+
throw new Error("receive has no until: a timer, wf.timer(name, when), wakes it");
|
|
4334
|
+
return labelOf((options as WaitOptions).label);
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
/** A wait's label, a function's asked now. */
|
|
4338
|
+
function labelOf(label: unknown): string | undefined {
|
|
4339
|
+
const given = typeof label === "function" ? label() : label;
|
|
4340
|
+
if (given !== undefined && typeof given !== "string")
|
|
4341
|
+
throw new Error("a wait's label is a string, or a function that returns one");
|
|
4342
|
+
return given;
|
|
4190
4343
|
}
|
|
4191
4344
|
|
|
4192
4345
|
/** A wait's branches, by name, checked: a race may give a duration where a select may not. */
|
|
@@ -4229,36 +4382,54 @@ function sourceTake(what: string, branch: string, source: unknown): WaitTake {
|
|
|
4229
4382
|
throw new Error(
|
|
4230
4383
|
`${what}'s ${branch} is wf.at(), a select's: a race answers once, so it takes a timer, wf.timer(name, date)`,
|
|
4231
4384
|
);
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4385
|
+
if (typeof source === "object" && source !== null && startedTurns.has(source))
|
|
4386
|
+
throw new Error(
|
|
4387
|
+
`${what}'s ${branch} is a started turn: name its agent, { ${branch}: agent }, which has at most one`,
|
|
4388
|
+
);
|
|
4389
|
+
throw new Error(`${what}'s ${branch} is not a source: a timer, a signal, or an agent`);
|
|
4235
4390
|
}
|
|
4236
4391
|
|
|
4392
|
+
/** The sources only a run of a workflow with events hears. */
|
|
4393
|
+
const HEARD_WITH_EVENTS: ReadonlySet<string> = new Set(["agent:ended", "participant:reply"]);
|
|
4394
|
+
|
|
4237
4395
|
/** One of the workflow's own events, by name, as a source. */
|
|
4238
|
-
function signalSource(name: string): unknown {
|
|
4239
|
-
if (typeof name !== "string"
|
|
4240
|
-
throw new Error(
|
|
4241
|
-
|
|
4396
|
+
function signalSource(name: string, declared: ReadonlySet<string>): unknown {
|
|
4397
|
+
if (typeof name !== "string")
|
|
4398
|
+
throw new Error(
|
|
4399
|
+
`a signal is one of the workflow's declared events, by name; not ${String(name)}`,
|
|
4400
|
+
);
|
|
4401
|
+
if (!declared.has(name))
|
|
4402
|
+
throw new Error(
|
|
4403
|
+
declared.size === 0
|
|
4404
|
+
? `signal ${name}: the workflow declares no events`
|
|
4405
|
+
: `signal ${name}: the workflow declares no such event; its events are ${[...declared].join(", ")}`,
|
|
4406
|
+
);
|
|
4407
|
+
const awf = awfEventOf(name);
|
|
4408
|
+
if (awf?.kind === "ended")
|
|
4409
|
+
throw new Error(
|
|
4410
|
+
`signal ${name}: an agent's end is waited on by naming the agent, { done: ${awf.key} }, not a signal`,
|
|
4411
|
+
);
|
|
4412
|
+
// `in.jsonl` keeps a reply under its record name, from the participant's key.
|
|
4413
|
+
const take = awf ? { name: "participant:reply", from: awf.key } : { name };
|
|
4414
|
+
return signalOf({ name }, take);
|
|
4242
4415
|
}
|
|
4243
4416
|
|
|
4244
4417
|
/** Timer `name`'s source. */
|
|
4245
|
-
function timerSource(
|
|
4418
|
+
function timerSource(waits: RunWaits, name: string, due: string): Timer {
|
|
4246
4419
|
return source(
|
|
4247
|
-
{ name, due, cancel: () =>
|
|
4420
|
+
{ name, due, cancel: () => waits.cancelTimer(name) },
|
|
4248
4421
|
{ name: "timer:due", from: name },
|
|
4249
4422
|
) as unknown as Timer;
|
|
4250
4423
|
}
|
|
4251
4424
|
|
|
4252
|
-
/**
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
{ name: "agent:ended", from: agent, turn: id },
|
|
4261
|
-
) as unknown as StartedTurn;
|
|
4425
|
+
/** The turns `start()` handed out, which a wait refuses by name. */
|
|
4426
|
+
const startedTurns = new WeakSet<object>();
|
|
4427
|
+
|
|
4428
|
+
/** A started turn, as the code holds it: its id and its cancel. A wait names its agent. */
|
|
4429
|
+
function turnRef(id: string, cancel: (reason?: string) => Promise<boolean>): StartedTurn {
|
|
4430
|
+
const turn = Object.freeze({ id, cancel });
|
|
4431
|
+
startedTurns.add(turn);
|
|
4432
|
+
return turn;
|
|
4262
4433
|
}
|
|
4263
4434
|
|
|
4264
4435
|
class ExecutionScope {
|
|
@@ -4435,6 +4606,17 @@ async function executeParallel<Item, Result>(
|
|
|
4435
4606
|
}
|
|
4436
4607
|
}
|
|
4437
4608
|
|
|
4609
|
+
/** How long reading a pane for an operation's evidence may hold up its release. */
|
|
4610
|
+
const SCREEN_READ_MS = 5_000;
|
|
4611
|
+
|
|
4612
|
+
/** Keeps what `turn`'s pane shows, the evidence of an operation that ended without an answer. */
|
|
4613
|
+
async function keepScreen(runDir: string, operationId: string, turn: HarnessTurn): Promise<void> {
|
|
4614
|
+
if (!turn.screen) return;
|
|
4615
|
+
const deadline = { unixMilliseconds: Date.now() + SCREEN_READ_MS };
|
|
4616
|
+
const screen = await waitForDeadline(turn.screen(), deadline).catch(() => undefined);
|
|
4617
|
+
if (screen?.trim()) await writeScreen(runDir, operationId, screen).catch(() => undefined);
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4438
4620
|
/**
|
|
4439
4621
|
* Compaction keeps its existing bounded release behavior. Structured operations use the
|
|
4440
4622
|
* supervisor, which requires confirmed native completion before returning an answer.
|
|
@@ -4521,16 +4703,21 @@ type Warn = (message: string, fields?: JsonObject, stage?: string) => void;
|
|
|
4521
4703
|
type OperationLog = {
|
|
4522
4704
|
operationId: string;
|
|
4523
4705
|
tags: OperationTags;
|
|
4706
|
+
/** Why its `op.json` wasn't written as it was drawn: a turn fails as its result slot would. */
|
|
4707
|
+
unrecorded?: string;
|
|
4524
4708
|
delivered(at: number): void;
|
|
4525
4709
|
/** A session its host named while it ran: on its start if that is still to come. */
|
|
4526
4710
|
session(id: string): void;
|
|
4527
4711
|
failed(error: unknown): void;
|
|
4528
4712
|
};
|
|
4529
4713
|
|
|
4714
|
+
/** A turn as the agent runs it: its spec, under the id it is recorded by. */
|
|
4715
|
+
type TurnSpec = (AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>) & { id: TurnId };
|
|
4530
4716
|
/** A native fork, and the parent's operation it was copied after: null when it ran none. */
|
|
4531
4717
|
type TakenFork = { native: NativeFork; after: string | null };
|
|
4718
|
+
type TakeFork = (deadline: AbsoluteDeadline | undefined, into?: SessionCopy) => Promise<TakenFork>;
|
|
4532
4719
|
|
|
4533
|
-
/** A settled turn or compaction, kept in the run's `
|
|
4720
|
+
/** A settled turn or compaction, kept in the run's `log.jsonl`. */
|
|
4534
4721
|
type RecordTurn = (outcome: TurnOutcome<JsonValue>, kind: TurnRecord["kind"]) => void;
|
|
4535
4722
|
|
|
4536
4723
|
function pick<T extends object, K extends keyof T>(value: T, keys: readonly K[]): Pick<T, K> {
|
|
@@ -4693,46 +4880,99 @@ function withKnownEffort(execution: AgentExecution): AgentExecution {
|
|
|
4693
4880
|
/** What `set` changes. */
|
|
4694
4881
|
type SettingsChange = Partial<Pick<AgentExecution, "model" | "effort">>;
|
|
4695
4882
|
|
|
4696
|
-
const SETTINGS_FIELDS = new Set(["
|
|
4883
|
+
const SETTINGS_FIELDS = new Set(["model", "effort", "timeout"]);
|
|
4884
|
+
|
|
4885
|
+
/** Removed in 0.1.0-beta.6; refused, as the switch would run otherwise than written. */
|
|
4886
|
+
const REMOVED_SETTINGS_FIELDS = new Set(["id", "deadline", "timeoutMs"]);
|
|
4697
4887
|
|
|
4698
4888
|
function sessionSettings({ model, effort }: AgentExecution): SessionSettings {
|
|
4699
4889
|
return { model, ...(effort === undefined ? {} : { effort }) };
|
|
4700
4890
|
}
|
|
4701
4891
|
|
|
4702
4892
|
/**
|
|
4703
|
-
* A
|
|
4893
|
+
* A TUI is the default and is left unsaid, and `metered` is kept only where it means something,
|
|
4704
4894
|
* so the same agent always has one identity.
|
|
4705
4895
|
*/
|
|
4706
4896
|
function storedPlacement(choice: PlacementChoice): PlacementChoice {
|
|
4707
|
-
|
|
4897
|
+
knownPlacement(choice);
|
|
4898
|
+
return placementOf(choice) === "headless"
|
|
4899
|
+
? { placement: "headless", ...(choice.metered === true ? { metered: true as const } : {}) }
|
|
4900
|
+
: {};
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
/** A workflow is untyped JavaScript at run time; a misspelt placement must not become a TUI. */
|
|
4904
|
+
function knownPlacement(choice: PlacementChoice): void {
|
|
4708
4905
|
if (
|
|
4709
4906
|
choice.placement !== undefined &&
|
|
4710
|
-
choice.placement !== "
|
|
4907
|
+
choice.placement !== "tui" &&
|
|
4711
4908
|
choice.placement !== "headless"
|
|
4712
4909
|
) {
|
|
4910
|
+
const renamed = (choice.placement as string) === "pane" ? ` ("pane" is now "tui")` : "";
|
|
4713
4911
|
throw new Error(
|
|
4714
|
-
`unknown placement ${JSON.stringify(choice.placement)}; expected
|
|
4912
|
+
`unknown placement ${JSON.stringify(choice.placement)}; expected tui or headless${renamed}`,
|
|
4715
4913
|
);
|
|
4716
4914
|
}
|
|
4717
|
-
return placementOf(choice) === "headless"
|
|
4718
|
-
? { placement: "headless", ...(choice.metered === true ? { metered: true as const } : {}) }
|
|
4719
|
-
: {};
|
|
4720
4915
|
}
|
|
4721
4916
|
|
|
4722
4917
|
/**
|
|
4723
4918
|
* The parent's harness, model and effort, in the placement and at the effort the fork names, else
|
|
4724
4919
|
* its parent's.
|
|
4725
4920
|
*/
|
|
4726
|
-
function forkExecution(parent: AgentExecution,
|
|
4921
|
+
function forkExecution(parent: AgentExecution, runtime: ForkRuntime = {}): AgentExecution {
|
|
4727
4922
|
const { placement: _placement, metered: _metered, ...target } = parent;
|
|
4728
|
-
const choice =
|
|
4923
|
+
const choice =
|
|
4924
|
+
runtime.placement === undefined && runtime.metered === undefined ? parent : runtime;
|
|
4729
4925
|
return {
|
|
4730
4926
|
...target,
|
|
4731
|
-
...(
|
|
4927
|
+
...(runtime.effort === undefined ? {} : { effort: runtime.effort }),
|
|
4732
4928
|
...storedPlacement(choice),
|
|
4733
4929
|
};
|
|
4734
4930
|
}
|
|
4735
4931
|
|
|
4932
|
+
const FORK_RUNTIME_FIELDS = ["placement", "metered", "effort"] as const;
|
|
4933
|
+
|
|
4934
|
+
/** A fork's spec, its `runtime` checked: what a typed caller can't get wrong, a plain one can. */
|
|
4935
|
+
function forkSpecOf(spec: CallerForkSpec): CallerForkSpec {
|
|
4936
|
+
const { runtime, ...rest } = spec;
|
|
4937
|
+
// Removed in 0.1.0-beta.6; refused, as the fork would run otherwise than written.
|
|
4938
|
+
const flat = FORK_RUNTIME_FIELDS.filter(
|
|
4939
|
+
(field) => (rest as Record<string, unknown>)[field] !== undefined,
|
|
4940
|
+
);
|
|
4941
|
+
if (flat.length > 0)
|
|
4942
|
+
throw new Error(
|
|
4943
|
+
`agent ${spec.key}: a fork's ${flat.join(", ")} go in its runtime: fork({ key, runtime: { ${flat.join(", ")} } })`,
|
|
4944
|
+
);
|
|
4945
|
+
if (runtime === undefined) return rest;
|
|
4946
|
+
if (typeof runtime !== "object" || runtime === null || Array.isArray(runtime)) {
|
|
4947
|
+
throw new Error(
|
|
4948
|
+
`agent ${spec.key}: a fork's runtime is { placement?, metered?, effort? }, not ${JSON.stringify(runtime)}`,
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
// A workflow is untyped JavaScript at run time; a harness or model asked for must not be dropped.
|
|
4952
|
+
const others = Object.keys(runtime).filter(
|
|
4953
|
+
(field) =>
|
|
4954
|
+
!(FORK_RUNTIME_FIELDS as readonly string[]).includes(field) &&
|
|
4955
|
+
(runtime as Record<string, unknown>)[field] !== undefined,
|
|
4956
|
+
);
|
|
4957
|
+
if (others.length > 0) {
|
|
4958
|
+
throw new Error(
|
|
4959
|
+
`agent ${spec.key}: a fork continues its parent's session on its harness and model; its runtime takes placement, metered and effort, not ${others.join(", ")}`,
|
|
4960
|
+
);
|
|
4961
|
+
}
|
|
4962
|
+
const given = definedFields({
|
|
4963
|
+
placement: runtime.placement,
|
|
4964
|
+
metered: runtime.metered,
|
|
4965
|
+
effort: runtime.effort,
|
|
4966
|
+
});
|
|
4967
|
+
return Object.keys(given).length === 0 ? rest : { ...rest, runtime: given };
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
function definedFields<T extends object>(fields: T): Partial<T> {
|
|
4971
|
+
return Object.fromEntries(
|
|
4972
|
+
Object.entries(fields).filter(([, value]) => value !== undefined),
|
|
4973
|
+
) as Partial<T>;
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4736
4976
|
function constrainExistingExecution(
|
|
4737
4977
|
selection: RuntimeSelection,
|
|
4738
4978
|
existing: AgentExecution,
|
|
@@ -4757,6 +4997,7 @@ function constrainExistingExecution(
|
|
|
4757
4997
|
if (selection.effort !== undefined && selection.effort !== existing.effort) {
|
|
4758
4998
|
throw new Error("existing agent was opened at a different effort");
|
|
4759
4999
|
}
|
|
5000
|
+
knownPlacement(selection);
|
|
4760
5001
|
const placed = placementOf(existing);
|
|
4761
5002
|
if (
|
|
4762
5003
|
(selection.placement !== undefined && placementOf(selection) !== placed) ||
|
|
@@ -4792,23 +5033,13 @@ function assertCompatibleAgent(
|
|
|
4792
5033
|
if (spec.skills !== undefined && !isDeepStrictEqual(existing.skills, requested.skills)) {
|
|
4793
5034
|
conflict(key, "skills");
|
|
4794
5035
|
}
|
|
4795
|
-
for (const field of ["instructions", "
|
|
5036
|
+
for (const field of ["instructions", "pane"] as const) {
|
|
4796
5037
|
if (spec[field] !== undefined && !isDeepStrictEqual(existing[field], requested[field])) {
|
|
4797
5038
|
conflict(key, field);
|
|
4798
5039
|
}
|
|
4799
5040
|
}
|
|
4800
5041
|
}
|
|
4801
5042
|
|
|
4802
|
-
function paneOptions({
|
|
4803
|
-
layout,
|
|
4804
|
-
keepPane,
|
|
4805
|
-
}: AgentIdentity): Pick<HarnessActivation, "layout" | "keepPane"> {
|
|
4806
|
-
return {
|
|
4807
|
-
...(layout === undefined ? {} : { layout }),
|
|
4808
|
-
...(keepPane === undefined ? {} : { keepPane }),
|
|
4809
|
-
};
|
|
4810
|
-
}
|
|
4811
|
-
|
|
4812
5043
|
function conflict(key: string, field: string): never {
|
|
4813
5044
|
throw new Error(`agent ${key} is already open with different ${field}`);
|
|
4814
5045
|
}
|
|
@@ -4817,22 +5048,14 @@ function stoppedError(key: string, reason: string): Error {
|
|
|
4817
5048
|
return new Error(`agent ${key} was stopped (${reason}); its key is not opened again`);
|
|
4818
5049
|
}
|
|
4819
5050
|
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
export function keptSessionsOf(turns: readonly TurnRecord[]): Map<string, NativeFork> {
|
|
4829
|
-
const kept = new Map<string, NativeFork>();
|
|
4830
|
-
for (const turn of turns) {
|
|
4831
|
-
if ((turn.callPath?.length ?? 0) > 0) continue;
|
|
4832
|
-
const session = turn.sessions.at(-1);
|
|
4833
|
-
if (session) kept.set(turn.agent, { harness: session.harness, sessionRef: session.id });
|
|
4834
|
-
}
|
|
4835
|
-
return kept;
|
|
5051
|
+
/** Each agent's latest `session` line: what an event run's continue reopens it on. */
|
|
5052
|
+
export function keptSessionsOf(sessions: RunView["sessions"]): Map<string, NativeFork> {
|
|
5053
|
+
return new Map(
|
|
5054
|
+
Object.values(sessions.latest).map((line) => [
|
|
5055
|
+
line.agent,
|
|
5056
|
+
{ harness: line.harness, sessionRef: line.session },
|
|
5057
|
+
]),
|
|
5058
|
+
);
|
|
4836
5059
|
}
|
|
4837
5060
|
|
|
4838
5061
|
/**
|
|
@@ -4843,4 +5066,6 @@ type LogicalAgentTurns = {
|
|
|
4843
5066
|
begin(turn: TurnId, meta: JsonValue, execution: AgentExecution): Promise<void>;
|
|
4844
5067
|
end(turn: TurnId, meta: JsonValue, outcome: TurnOutcome<JsonValue>): Promise<void>;
|
|
4845
5068
|
restored(): RestoredTurn | undefined;
|
|
5069
|
+
/** Throws unless the workflow declares this agent's `{key}:ended`. */
|
|
5070
|
+
checkDeclared(): void;
|
|
4846
5071
|
};
|