@agentswf/engine 0.1.0-beta.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
package/src/workflow-runner.ts
CHANGED
|
@@ -10,10 +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,
|
|
16
17
|
RunPanes,
|
|
18
|
+
RunView,
|
|
17
19
|
SandboxRecord,
|
|
18
20
|
SettledDecision,
|
|
19
21
|
SettledOperation,
|
|
@@ -45,10 +47,8 @@ import {
|
|
|
45
47
|
awfEventOf,
|
|
46
48
|
type Branches,
|
|
47
49
|
type CallerForkSpec,
|
|
48
|
-
type CodeParticipantRef,
|
|
49
50
|
type CompactionId,
|
|
50
51
|
type CompactSpec,
|
|
51
|
-
DEPRECATED_DEFINITION,
|
|
52
52
|
DeadlineExceededError,
|
|
53
53
|
type Duration,
|
|
54
54
|
durationMilliseconds,
|
|
@@ -56,12 +56,14 @@ import {
|
|
|
56
56
|
type EventLike,
|
|
57
57
|
type ForkRuntime,
|
|
58
58
|
type HarnessAllowance,
|
|
59
|
+
type HarnessKind,
|
|
59
60
|
isEventWorkflow,
|
|
60
61
|
isJsonValue,
|
|
61
62
|
type JsonObject,
|
|
62
63
|
type JsonValue,
|
|
63
64
|
type KeepPane,
|
|
64
65
|
type MessageAccess,
|
|
66
|
+
type NativeSessionRef,
|
|
65
67
|
type Next,
|
|
66
68
|
nameProblem,
|
|
67
69
|
type OperationRecord,
|
|
@@ -75,7 +77,6 @@ import {
|
|
|
75
77
|
placementOf,
|
|
76
78
|
positiveDurationMilliseconds,
|
|
77
79
|
type RaceBranches,
|
|
78
|
-
REPORT_DEPRECATED,
|
|
79
80
|
type RunResult,
|
|
80
81
|
type RuntimeSelection,
|
|
81
82
|
type SettingsSpec,
|
|
@@ -85,6 +86,7 @@ import {
|
|
|
85
86
|
type StageSummary,
|
|
86
87
|
type StartedTurn,
|
|
87
88
|
type Taken,
|
|
89
|
+
type Timeout,
|
|
88
90
|
type TimeRead,
|
|
89
91
|
type Timer,
|
|
90
92
|
type TurnId,
|
|
@@ -101,7 +103,8 @@ import {
|
|
|
101
103
|
findHarness,
|
|
102
104
|
HARNESS_NAMES,
|
|
103
105
|
type Harness,
|
|
104
|
-
|
|
106
|
+
type HarnessHomes,
|
|
107
|
+
OPERATOR_HOMES,
|
|
105
108
|
settingsRefusal,
|
|
106
109
|
skillsLayout,
|
|
107
110
|
} from "@agentswf/harness";
|
|
@@ -130,11 +133,9 @@ import {
|
|
|
130
133
|
import type { CoordinatorPort } from "./coordinator-port";
|
|
131
134
|
import {
|
|
132
135
|
assertDeadline,
|
|
133
|
-
assertDeadlineValue,
|
|
134
136
|
boundedDeadline,
|
|
135
137
|
deadlineWithin,
|
|
136
138
|
earlierDeadline,
|
|
137
|
-
type GivenBound,
|
|
138
139
|
harnessDeadline,
|
|
139
140
|
runUntilStopped,
|
|
140
141
|
scheduleAt,
|
|
@@ -143,7 +144,6 @@ import {
|
|
|
143
144
|
} from "./deadlines";
|
|
144
145
|
import { RunDecisions } from "./decisions/directory";
|
|
145
146
|
import type { DecisionInstallation } from "./decisions/seam";
|
|
146
|
-
import { type Deprecated, RunDeprecations } from "./deprecations";
|
|
147
147
|
import { parseWhen } from "./duration";
|
|
148
148
|
import { emitEvent } from "./emit";
|
|
149
149
|
import { messageOf } from "./errors";
|
|
@@ -154,15 +154,6 @@ import {
|
|
|
154
154
|
inTimeoutHandler,
|
|
155
155
|
type KeepAlive,
|
|
156
156
|
} from "./keep-alive";
|
|
157
|
-
import {
|
|
158
|
-
AT_DUE,
|
|
159
|
-
type RestoredTurn,
|
|
160
|
-
RunMailbox,
|
|
161
|
-
RunParked,
|
|
162
|
-
recoverTurns,
|
|
163
|
-
type TurnEnd,
|
|
164
|
-
unrunUsage,
|
|
165
|
-
} from "./mailbox";
|
|
166
157
|
import { superviseMessageOperation } from "./message-operation";
|
|
167
158
|
import { canAwaitReply } from "./messaging/limits";
|
|
168
159
|
import type { HolderRequest } from "./messaging/messaging";
|
|
@@ -177,16 +168,19 @@ import {
|
|
|
177
168
|
type SupervisedOutcome,
|
|
178
169
|
superviseOperation,
|
|
179
170
|
} from "./operation-liveness";
|
|
171
|
+
import { type EarlierOps, OpIds, writeScreen } from "./ops";
|
|
180
172
|
import {
|
|
181
173
|
besideOf,
|
|
182
174
|
checkPaneOptions,
|
|
183
175
|
keepsPane,
|
|
184
176
|
type PaneAsked,
|
|
185
177
|
paneAsked,
|
|
186
|
-
|
|
178
|
+
storedPane,
|
|
187
179
|
} from "./pane-layout";
|
|
188
180
|
import { createResultSlotRegistry, type ResultSlotRegistry } from "./result-slots";
|
|
189
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";
|
|
190
184
|
import {
|
|
191
185
|
type AgentProgress,
|
|
192
186
|
type GroupProgress,
|
|
@@ -200,7 +194,7 @@ import {
|
|
|
200
194
|
type OperationTags,
|
|
201
195
|
type RunLedger,
|
|
202
196
|
} from "./run-usage";
|
|
203
|
-
import {
|
|
197
|
+
import { endLogLine, readAttempts, workDir } from "./runs";
|
|
204
198
|
import { type CredentialLocks, placeCarried, seedHome } from "./sandbox-homes";
|
|
205
199
|
import {
|
|
206
200
|
RUN_SANDBOX_ONLY,
|
|
@@ -208,23 +202,16 @@ import {
|
|
|
208
202
|
type RunSandboxOptions,
|
|
209
203
|
type SeatedAgent,
|
|
210
204
|
} from "./sandboxes";
|
|
205
|
+
import { SessionLines, type SessionPlace } from "./session-lines";
|
|
211
206
|
import { QUOTING_RULE, simpleInput, takesText, usageLine } from "./simple-input";
|
|
212
207
|
import { placeSkills, RunSkills, readSkillSources } from "./skills/run-skills";
|
|
213
|
-
import {
|
|
214
|
-
|
|
215
|
-
branchOf,
|
|
216
|
-
branchValue,
|
|
217
|
-
isSignal,
|
|
218
|
-
matches,
|
|
219
|
-
presented,
|
|
220
|
-
signalOf,
|
|
221
|
-
source,
|
|
222
|
-
takeOf,
|
|
223
|
-
} from "./sources";
|
|
208
|
+
import { asSource, branchValue, isSignal, presented, signalOf, source, takeOf } from "./sources";
|
|
209
|
+
import { keepSpend, settleEarlier } from "./spend";
|
|
224
210
|
import { StageLedger, type StageProgress } from "./stage-ledger";
|
|
225
|
-
import { lastRedo } from "./stage-plan";
|
|
226
211
|
import { FromStageUnreached } from "./stopped";
|
|
227
212
|
import { inToolHandler, outsideTools, refusedInTool, ToolCalls, toolsLesson } from "./tools";
|
|
213
|
+
import { unrunUsage } from "./turn-ends";
|
|
214
|
+
import { RunParked, RunWaits, type TurnEnd } from "./waits";
|
|
228
215
|
import type { EventWorkflow } from "./workflow-loader";
|
|
229
216
|
|
|
230
217
|
export { WorkflowCancelledError } from "./deadlines";
|
|
@@ -242,12 +229,17 @@ export type RunWorkflowOptions = {
|
|
|
242
229
|
defaultWindowMs?: number;
|
|
243
230
|
/** The run's time, which its timers and `wf.now()` read; absent, the clock's. A test's is virtual. */
|
|
244
231
|
now?: () => number;
|
|
232
|
+
/** Where the control plane's socket directory is made; `/tmp` unless a test watches its own. */
|
|
233
|
+
socketRoot?: string;
|
|
245
234
|
runRoot: string;
|
|
246
235
|
/** The run this is an attempt of, whose folder the operator claimed under `runRoot`. */
|
|
247
236
|
run: {
|
|
248
237
|
dir: string;
|
|
249
238
|
id: string;
|
|
250
|
-
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;
|
|
251
243
|
/** How the attempt is named to a person: its Herdr workspace's label. */
|
|
252
244
|
label?: string;
|
|
253
245
|
/** `--from-stage`: the stage this attempt starts at, reusing those before it. */
|
|
@@ -258,14 +250,14 @@ export type RunWorkflowOptions = {
|
|
|
258
250
|
panes?: RunPanes;
|
|
259
251
|
/** A run of a workflow with `events`. */
|
|
260
252
|
events?: true;
|
|
261
|
-
/** 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`. */
|
|
262
254
|
locked?<T>(work: () => Promise<T>): Promise<T>;
|
|
263
255
|
/**
|
|
264
256
|
* With `events`: false when the attempt before this one has no ending, its process gone with its
|
|
265
257
|
* agents perhaps still running: its sessions aren't reopened until they are stopped (Q17).
|
|
266
258
|
*/
|
|
267
259
|
resumeSessions?: boolean;
|
|
268
|
-
/** With `events`: calls `nudge` whenever awf tells the run
|
|
260
|
+
/** With `events`: calls `nudge` whenever awf tells the run it has a new event. */
|
|
269
261
|
listenNudges?(nudge: () => void): void;
|
|
270
262
|
/**
|
|
271
263
|
* Writes the attempt's `parked` ending, which the run's wait calls holding the run's lock: an
|
|
@@ -454,14 +446,11 @@ type AgentIdentity = {
|
|
|
454
446
|
/** Its skills' sources, checked; absent when it has the operator's. */
|
|
455
447
|
skills?: SkillSource[];
|
|
456
448
|
instructions?: string;
|
|
457
|
-
labels?: AgentOpenSpec["labels"];
|
|
458
449
|
/** The agent it was forked from, and the spec it was forked with. */
|
|
459
450
|
forkedFrom?: { parent: AgentKey; spec: CallerForkSpec };
|
|
460
|
-
/** As written; a reopen compares
|
|
451
|
+
/** As written; a reopen compares it so, never where the pane ended up. */
|
|
461
452
|
pane?: PaneSpec;
|
|
462
|
-
|
|
463
|
-
keepPane?: KeepPane;
|
|
464
|
-
/** What its host is asked for its pane: these, with the operator's flags applied. */
|
|
453
|
+
/** What its host is asked for its pane: it, with the operator's flags applied. */
|
|
465
454
|
asked?: PaneAsked;
|
|
466
455
|
};
|
|
467
456
|
|
|
@@ -492,8 +481,9 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
492
481
|
args: Args,
|
|
493
482
|
options: RunWorkflowOptions,
|
|
494
483
|
): Promise<WorkflowRunHandle<Result>> {
|
|
495
|
-
|
|
484
|
+
// Cancellation wins when the deadline has passed too, as runUntilStopped has it.
|
|
496
485
|
if (options.signal?.aborted) throw new WorkflowCancelledError(options.signal.reason);
|
|
486
|
+
assertDeadline(options.deadline);
|
|
497
487
|
// Before anything opens, so a refusal leaves nothing to close.
|
|
498
488
|
const withEvents = isEventWorkflow(definition);
|
|
499
489
|
const locked = options.run.locked ?? (<T>(work: () => Promise<T>) => work());
|
|
@@ -501,18 +491,36 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
501
491
|
throw new Error(
|
|
502
492
|
"a run of a workflow with events takes no --values: before --from-stage, a stage with no record runs",
|
|
503
493
|
);
|
|
504
|
-
const { dir: runDir, id: runId
|
|
505
|
-
const
|
|
494
|
+
const { dir: runDir, id: runId } = options.run;
|
|
495
|
+
const { attempt } = options.run.lines;
|
|
506
496
|
const cwd = options.cwd ?? process.cwd();
|
|
507
|
-
// Before anything opens: a
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
+
);
|
|
514
522
|
// The log is observation: one that cannot be written says so as it is, and fails nothing.
|
|
515
|
-
await
|
|
523
|
+
await endLogLine(runDir).catch(() => undefined);
|
|
516
524
|
const slots = createResultSlotRegistry({
|
|
517
525
|
runDir,
|
|
518
526
|
...(options.livenessPolicy
|
|
@@ -522,7 +530,10 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
522
530
|
}
|
|
523
531
|
: {}),
|
|
524
532
|
});
|
|
525
|
-
const control = await startResultControlPlane({
|
|
533
|
+
const control = await startResultControlPlane({
|
|
534
|
+
slots,
|
|
535
|
+
...(options.socketRoot ? { socketRoot: options.socketRoot } : {}),
|
|
536
|
+
});
|
|
526
537
|
let host: AgentRunHost;
|
|
527
538
|
const openingHost = Promise.resolve().then(() =>
|
|
528
539
|
options.runtime.host.openRun({
|
|
@@ -559,12 +570,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
559
570
|
});
|
|
560
571
|
};
|
|
561
572
|
const warn = say("warning");
|
|
562
|
-
const
|
|
563
|
-
if (definition.meta.whenToUse !== undefined) deprecations.used("whenToUse");
|
|
564
|
-
if ((definition as { [DEPRECATED_DEFINITION]?: unknown })[DEPRECATED_DEFINITION])
|
|
565
|
-
deprecations.used("defineExecutableWorkflow");
|
|
566
|
-
// Each turn as it settles, so an attempt that dies keeps the sessions and times of its turns.
|
|
567
|
-
const turns: TurnRecord[] = [];
|
|
573
|
+
const sessionLines = new SessionLines({ lines: options.run.lines, failed: (text) => warn(text) });
|
|
568
574
|
const ledger = createRunLedger({ accounting: options.runtime.host.accounting, startedAt });
|
|
569
575
|
const recordTurn: RecordTurn = (outcome, kind) => {
|
|
570
576
|
const turn: TurnRecord = {
|
|
@@ -575,19 +581,16 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
575
581
|
callPath: outcome.usage.callPath,
|
|
576
582
|
...pick(outcome.usage, TURN_OPERATION_FIELDS),
|
|
577
583
|
};
|
|
578
|
-
turns.push(turn);
|
|
579
584
|
events.turn(turn);
|
|
580
585
|
};
|
|
581
586
|
const stages = new StageLedger({
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
587
|
+
lines: options.run.lines,
|
|
588
|
+
run: recorded,
|
|
589
|
+
versions,
|
|
585
590
|
...(options.run.fromStage === undefined ? {} : { fromStage: options.run.fromStage }),
|
|
586
591
|
...(options.run.values === undefined ? {} : { values: options.run.values }),
|
|
587
592
|
...(definition.meta.version === undefined ? {} : { workflowVersion: definition.meta.version }),
|
|
588
593
|
...(withEvents ? { byContent: true } : {}),
|
|
589
|
-
...(redo === undefined ? {} : { redo }),
|
|
590
|
-
turns: () => turns,
|
|
591
594
|
event: (body, at) => events.event(body, at),
|
|
592
595
|
});
|
|
593
596
|
const progress = new RunProgress();
|
|
@@ -601,6 +604,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
601
604
|
cwd,
|
|
602
605
|
deadline: options.deadline,
|
|
603
606
|
log: (message) => warn(message),
|
|
607
|
+
opened: (record) => events.event({ kind: "sandbox-opened", ...record }),
|
|
604
608
|
});
|
|
605
609
|
if (sandboxes.hasRunSandbox) {
|
|
606
610
|
try {
|
|
@@ -615,9 +619,11 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
615
619
|
throw new Error(`the run's sandbox did not open: ${messageOf(error)}`);
|
|
616
620
|
}
|
|
617
621
|
}
|
|
622
|
+
const ops = new OpIds(runDir, options.run.lines.attempt, options.run.ops);
|
|
618
623
|
const decisions = new RunDecisions({
|
|
619
624
|
...(options.decisions ? { installation: options.decisions } : {}),
|
|
620
625
|
runDir,
|
|
626
|
+
ops,
|
|
621
627
|
event: (body) => events.event(body),
|
|
622
628
|
});
|
|
623
629
|
const skills = new RunSkills({
|
|
@@ -628,26 +634,23 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
628
634
|
const fatal = Promise.withResolvers<never>();
|
|
629
635
|
void fatal.promise.catch(() => undefined);
|
|
630
636
|
const now = options.now ?? Date.now;
|
|
631
|
-
//
|
|
632
|
-
// runs again (Q6); the turns whose ends no wait took are their agents' turns again.
|
|
633
|
-
const restoredTurns = withEvents ? await recoverTurns(runDir, attempt, locked, now) : undefined;
|
|
634
|
-
// 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.
|
|
635
638
|
const keptSessions =
|
|
636
639
|
withEvents && options.run.resumeSessions !== false
|
|
637
|
-
? keptSessionsOf(
|
|
640
|
+
? keptSessionsOf(recorded.sessions)
|
|
638
641
|
: undefined;
|
|
639
|
-
// 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.
|
|
640
643
|
// A plain run without `park` waits for its timers in its process.
|
|
641
644
|
const { park } = options.run;
|
|
642
|
-
const waits = new
|
|
645
|
+
const waits = new RunWaits({
|
|
643
646
|
runDir,
|
|
644
|
-
|
|
647
|
+
lines: options.run.lines,
|
|
645
648
|
locked,
|
|
646
649
|
now,
|
|
647
650
|
...(park
|
|
648
651
|
? {
|
|
649
652
|
parking: {
|
|
650
|
-
idle: (
|
|
653
|
+
idle: () => owner.idle(),
|
|
651
654
|
timeout: (signal) => owner.timeout(signal),
|
|
652
655
|
liveUntil: () => owner.liveUntil(),
|
|
653
656
|
park: async (wait) => {
|
|
@@ -665,22 +668,28 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
665
668
|
: {}),
|
|
666
669
|
});
|
|
667
670
|
await waits.open();
|
|
668
|
-
const
|
|
669
|
-
if (
|
|
670
|
-
//
|
|
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.
|
|
671
677
|
const owner: WorkflowOwner = new WorkflowOwner({
|
|
672
|
-
...(
|
|
678
|
+
...(eventWaits ? { eventWaits } : {}),
|
|
673
679
|
waits,
|
|
674
680
|
...(keptSessions ? { keptSessions } : {}),
|
|
681
|
+
sessionLines,
|
|
675
682
|
...(restoredTurns ? { restoredTurns } : {}),
|
|
676
683
|
failRun: (error) => fatal.reject(error),
|
|
677
684
|
decisions,
|
|
685
|
+
ops,
|
|
678
686
|
...(options.allowance ? { allowance: options.allowance } : {}),
|
|
679
687
|
sandboxes,
|
|
680
688
|
skills,
|
|
681
689
|
runDir,
|
|
682
690
|
locks,
|
|
683
691
|
environment,
|
|
692
|
+
...(options.sandboxes?.homes ? { homes: options.sandboxes.homes } : {}),
|
|
684
693
|
progress,
|
|
685
694
|
recordTurn,
|
|
686
695
|
stages,
|
|
@@ -696,7 +705,6 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
696
705
|
ledger,
|
|
697
706
|
onLog: warn,
|
|
698
707
|
log: say("log"),
|
|
699
|
-
deprecations,
|
|
700
708
|
events,
|
|
701
709
|
onCallerStopped: options.onCallerStopped,
|
|
702
710
|
livenessPolicy: options.livenessPolicy,
|
|
@@ -746,7 +754,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
746
754
|
failed = true;
|
|
747
755
|
failure = error;
|
|
748
756
|
}
|
|
749
|
-
if (
|
|
757
|
+
if (eventWaits && !eventWaits.parked)
|
|
750
758
|
await owner.endStartedTurns(failed ? "the workflow failed" : "the workflow returned");
|
|
751
759
|
// Once `parked` is written, the attempt is parked, whatever the code did after.
|
|
752
760
|
if (waits.parked) {
|
|
@@ -762,7 +770,6 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
762
770
|
failure = ending;
|
|
763
771
|
}
|
|
764
772
|
bodyEnded = true;
|
|
765
|
-
deprecations.settle();
|
|
766
773
|
// What the body left running enters no stage; one left open is failed once its turns settle.
|
|
767
774
|
stages.seal(failed ? messageOf(failure) : "the workflow returned before the stage ended");
|
|
768
775
|
const cutReading = () => reading.abort();
|
|
@@ -795,12 +802,21 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
795
802
|
|
|
796
803
|
// Before the wait for session files, which is bookkeeping rather than the run's own time.
|
|
797
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();
|
|
798
808
|
await stages
|
|
799
809
|
.close()
|
|
800
|
-
.catch((error) => warn(`awf: a stage
|
|
810
|
+
.catch((error) => warn(`awf: a stage's line was not written: ${messageOf(error)}`));
|
|
801
811
|
await events.close();
|
|
802
812
|
const usage = await ledger.settle(reading.signal);
|
|
803
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}`);
|
|
804
820
|
const times = { startedAt: new Date(startedAt).toISOString(), finishedAt };
|
|
805
821
|
const opened = sandboxes.records();
|
|
806
822
|
const given = skills.records();
|
|
@@ -942,6 +958,8 @@ class WorkflowOwner {
|
|
|
942
958
|
readonly #cleanupFailures: Error[] = [];
|
|
943
959
|
readonly context: WorkflowContext;
|
|
944
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>();
|
|
945
963
|
/** Each agent once it has opened, by key: what messaging asks about, and what a ref must be. */
|
|
946
964
|
readonly #logical = new Map<string, LogicalAgent>();
|
|
947
965
|
readonly #messaging: Messaging;
|
|
@@ -974,7 +992,9 @@ class WorkflowOwner {
|
|
|
974
992
|
defaultWindowMs?: number;
|
|
975
993
|
locks: CredentialLocks;
|
|
976
994
|
environment: Readonly<Record<string, string | undefined>>;
|
|
995
|
+
homes?: HarnessHomes;
|
|
977
996
|
decisions: RunDecisions;
|
|
997
|
+
ops: OpIds;
|
|
978
998
|
allowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
979
999
|
stages: StageLedger;
|
|
980
1000
|
runId: string;
|
|
@@ -992,20 +1012,21 @@ class WorkflowOwner {
|
|
|
992
1012
|
onLog?: Warn;
|
|
993
1013
|
/** The workflow's own log lines. */
|
|
994
1014
|
log(message: string, fields?: JsonObject): void;
|
|
995
|
-
deprecations: RunDeprecations;
|
|
996
1015
|
events: RunEventLog;
|
|
997
1016
|
onCallerStopped?: RunWorkflowOptions["onCallerStopped"];
|
|
998
1017
|
awaitReply: NonNullable<RunWorkflowOptions["awaitReply"]>;
|
|
999
1018
|
coordinators: readonly CoordinatorPort[];
|
|
1000
|
-
/** A run of a workflow with `events`: its
|
|
1001
|
-
|
|
1019
|
+
/** A run of a workflow with `events`: its waits, which take its events. */
|
|
1020
|
+
eventWaits?: RunWaits;
|
|
1002
1021
|
/** The run's time. */
|
|
1003
1022
|
now: () => number;
|
|
1004
|
-
/** Its timers
|
|
1005
|
-
waits:
|
|
1006
|
-
/** 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. */
|
|
1007
1026
|
keptSessions?: ReadonlyMap<string, NativeFork>;
|
|
1008
|
-
/**
|
|
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. */
|
|
1009
1030
|
restoredTurns?: ReadonlyMap<string, RestoredTurn>;
|
|
1010
1031
|
emit: Emitter;
|
|
1011
1032
|
/** The workflow's name, which with the run's id is the ref its events are `from`. */
|
|
@@ -1033,6 +1054,7 @@ class WorkflowOwner {
|
|
|
1033
1054
|
},
|
|
1034
1055
|
schedule: (at, action) => scheduleAt({ unixMilliseconds: at }, action),
|
|
1035
1056
|
now: Date.now,
|
|
1057
|
+
question: (n) => `${options.attempt}.${n}`,
|
|
1036
1058
|
record: async (event) => {
|
|
1037
1059
|
this.#messagesChanged();
|
|
1038
1060
|
if (event.kind === "message-sent") {
|
|
@@ -1061,7 +1083,6 @@ class WorkflowOwner {
|
|
|
1061
1083
|
this.#tools = new ToolCalls<ExecutionScope>({
|
|
1062
1084
|
record: (event) => options.events.event(event),
|
|
1063
1085
|
runIn: (scope, fn) => (scope ? scopes.run(scope, fn) : scopes.exit(fn)),
|
|
1064
|
-
deprecated: (name, call) => options.deprecations.used(name, call),
|
|
1065
1086
|
});
|
|
1066
1087
|
options.control.attachTools(this.#tools);
|
|
1067
1088
|
const holder = {
|
|
@@ -1074,8 +1095,9 @@ class WorkflowOwner {
|
|
|
1074
1095
|
runId: options.runId,
|
|
1075
1096
|
attempt: options.attempt,
|
|
1076
1097
|
cwd: options.cwd,
|
|
1098
|
+
// A new `Date` each time, so one a workflow changes isn't the run's.
|
|
1077
1099
|
get deadline() {
|
|
1078
|
-
return
|
|
1100
|
+
return options.deadline && new Date(options.deadline.unixMilliseconds);
|
|
1079
1101
|
},
|
|
1080
1102
|
agents: {
|
|
1081
1103
|
open: (spec) => {
|
|
@@ -1085,14 +1107,6 @@ class WorkflowOwner {
|
|
|
1085
1107
|
return Promise.reject(error);
|
|
1086
1108
|
}
|
|
1087
1109
|
},
|
|
1088
|
-
stop: (key, reason) => {
|
|
1089
|
-
options.deprecations.used("agents-stop");
|
|
1090
|
-
try {
|
|
1091
|
-
return this.stopAgent(key, reason, "agents.stop");
|
|
1092
|
-
} catch (error) {
|
|
1093
|
-
return Promise.reject(error);
|
|
1094
|
-
}
|
|
1095
|
-
},
|
|
1096
1110
|
caller: (spec) => {
|
|
1097
1111
|
try {
|
|
1098
1112
|
return this.caller(spec.key);
|
|
@@ -1136,7 +1150,6 @@ class WorkflowOwner {
|
|
|
1136
1150
|
const decided = this.track(
|
|
1137
1151
|
options.decisions.decide(spec, {
|
|
1138
1152
|
deadline: scope?.deadline ?? options.deadline,
|
|
1139
|
-
deprecated: (name, call) => options.deprecations.used(name, call),
|
|
1140
1153
|
...(scope ? { add: (cancel) => scope.add((stop) => cancel(stop.reason)) } : {}),
|
|
1141
1154
|
...(scope?.stage === undefined ? {} : { stage: scope.stage }),
|
|
1142
1155
|
}),
|
|
@@ -1173,12 +1186,7 @@ class WorkflowOwner {
|
|
|
1173
1186
|
const inherited = scopes.getStore()?.deadline ?? options.deadline;
|
|
1174
1187
|
let deadline: AbsoluteDeadline | undefined;
|
|
1175
1188
|
try {
|
|
1176
|
-
deadline = boundedDeadline(
|
|
1177
|
-
{ timeout: parallelOptions?.timeout, deadline: parallelOptions?.deadline },
|
|
1178
|
-
inherited,
|
|
1179
|
-
(name, call) => options.deprecations.used(name, call),
|
|
1180
|
-
"on parallel",
|
|
1181
|
-
);
|
|
1189
|
+
deadline = boundedDeadline(parallelOptions?.timeout, inherited);
|
|
1182
1190
|
} catch (error) {
|
|
1183
1191
|
return Promise.reject(error);
|
|
1184
1192
|
}
|
|
@@ -1216,7 +1224,7 @@ class WorkflowOwner {
|
|
|
1216
1224
|
undefined,
|
|
1217
1225
|
);
|
|
1218
1226
|
},
|
|
1219
|
-
// Every run waits on its own
|
|
1227
|
+
// Every run waits on its own lines; one without events never parks, so it waits here.
|
|
1220
1228
|
receive: (spec?: WaitOptions) => this.receive(options.waits, spec),
|
|
1221
1229
|
race: ((name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
|
|
1222
1230
|
this.race(options.waits, name, branches, raceOptions)) as WorkflowContext["race"],
|
|
@@ -1235,8 +1243,8 @@ class WorkflowOwner {
|
|
|
1235
1243
|
return new Date(options.now() + milliseconds);
|
|
1236
1244
|
},
|
|
1237
1245
|
signal: ((name: string) => signalSource(name, options.declared)) as WorkflowContext["signal"],
|
|
1238
|
-
...(options.
|
|
1239
|
-
? { started: options.
|
|
1246
|
+
...(options.eventWaits?.started
|
|
1247
|
+
? { started: options.eventWaits.started as WorkflowContext["started"] }
|
|
1240
1248
|
: {}),
|
|
1241
1249
|
};
|
|
1242
1250
|
}
|
|
@@ -1298,12 +1306,11 @@ class WorkflowOwner {
|
|
|
1298
1306
|
// A turn's end taken: its agent takes other work again.
|
|
1299
1307
|
for (const event of taken) {
|
|
1300
1308
|
if (event.name !== "agent:ended") continue;
|
|
1301
|
-
this.#reporting((event.data as { outcome?: unknown }).outcome);
|
|
1302
1309
|
const { turn } = event.data as unknown as TurnEnd;
|
|
1303
1310
|
if (this.#restored.get(event.from)?.turn === turn) this.#restored.delete(event.from);
|
|
1304
1311
|
this.#logical.get(event.from)?.endTaken(turn);
|
|
1305
1312
|
}
|
|
1306
|
-
//
|
|
1313
|
+
// `in.jsonl` and the turns' ends keep awf's record names; the code is handed the names it declares.
|
|
1307
1314
|
return taken.map((event) => presented(event, this.options.declared));
|
|
1308
1315
|
} finally {
|
|
1309
1316
|
removeFromScope?.();
|
|
@@ -1312,7 +1319,7 @@ class WorkflowOwner {
|
|
|
1312
1319
|
}
|
|
1313
1320
|
|
|
1314
1321
|
/** A timer, by name: the recorded one, or set now. A race's own are named `{race}@{branch}`. */
|
|
1315
|
-
private timer(
|
|
1322
|
+
private timer(waits: RunWaits, name: string, when: When, own = false): Timer {
|
|
1316
1323
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1317
1324
|
if (typeof name !== "string") throw new Error("a timer's name is a string");
|
|
1318
1325
|
if (!own) {
|
|
@@ -1322,7 +1329,7 @@ class WorkflowOwner {
|
|
|
1322
1329
|
if (problem) throw new Error(`timer ${name}: its name is ${problem}`);
|
|
1323
1330
|
}
|
|
1324
1331
|
const given = parseWhen(when, this.options.now());
|
|
1325
|
-
const { timer, asked } =
|
|
1332
|
+
const { timer, asked } = waits.setTimer(name, given.due, given.when);
|
|
1326
1333
|
if (timer.cancelled)
|
|
1327
1334
|
this.options.onLog?.(
|
|
1328
1335
|
`awf: timer ${name} was cancelled; a name stays cancelled, so it won't fire`,
|
|
@@ -1331,12 +1338,12 @@ class WorkflowOwner {
|
|
|
1331
1338
|
this.options.onLog?.(
|
|
1332
1339
|
`awf: timer ${name} is due ${timer.due}, as first set (${timer.when}); ${asked} is ignored`,
|
|
1333
1340
|
);
|
|
1334
|
-
return timerSource(
|
|
1341
|
+
return timerSource(waits, name, timer.due);
|
|
1335
1342
|
}
|
|
1336
1343
|
|
|
1337
1344
|
/** Waits for the first of `branches`, recorded under `name`. */
|
|
1338
1345
|
private async race(
|
|
1339
|
-
|
|
1346
|
+
waits: RunWaits,
|
|
1340
1347
|
name: string,
|
|
1341
1348
|
branches: RaceBranches,
|
|
1342
1349
|
raceOptions: WaitOptions | undefined,
|
|
@@ -1368,7 +1375,7 @@ class WorkflowOwner {
|
|
|
1368
1375
|
const owned = new Map<string, string>();
|
|
1369
1376
|
const pairs: (readonly [string, WaitTake])[] = given.map(([branch, source]) => {
|
|
1370
1377
|
if (!own(source)) return [branch, takes.get(branch) as WaitTake] as const;
|
|
1371
|
-
const timer = this.timer(
|
|
1378
|
+
const timer = this.timer(waits, `${name}@${branch}`, source as When, true);
|
|
1372
1379
|
owned.set(branch, timer.name);
|
|
1373
1380
|
return [branch, { name: "timer:due", from: timer.name }] as const;
|
|
1374
1381
|
});
|
|
@@ -1376,7 +1383,7 @@ class WorkflowOwner {
|
|
|
1376
1383
|
let event: WorkflowEvent<string, JsonValue>;
|
|
1377
1384
|
try {
|
|
1378
1385
|
[event] = (await this.#holdingWait(scope, async (signal) => {
|
|
1379
|
-
const won = await
|
|
1386
|
+
const won = await waits.race(name, pairs, owned, label, signal);
|
|
1380
1387
|
branch = won.branch;
|
|
1381
1388
|
return [won.event];
|
|
1382
1389
|
})) as [WorkflowEvent<string, JsonValue>];
|
|
@@ -1384,7 +1391,7 @@ class WorkflowOwner {
|
|
|
1384
1391
|
// Cut short, not parked: its own timers would fire into a later wait. A park's rerun races
|
|
1385
1392
|
// again, and so does a continue's after a park refused short of its --from-stage.
|
|
1386
1393
|
if (!(error instanceof RunParked || error instanceof FromStageUnreached))
|
|
1387
|
-
await Promise.all([...owned.values()].map((timer) =>
|
|
1394
|
+
await Promise.all([...owned.values()].map((timer) => waits.cancelTimer(timer))).catch(
|
|
1388
1395
|
() => undefined,
|
|
1389
1396
|
);
|
|
1390
1397
|
throw error;
|
|
@@ -1396,7 +1403,7 @@ class WorkflowOwner {
|
|
|
1396
1403
|
|
|
1397
1404
|
/** A loop over the events `branches` take, one at a time. */
|
|
1398
1405
|
private select(
|
|
1399
|
-
|
|
1406
|
+
waits: RunWaits,
|
|
1400
1407
|
branches: Branches,
|
|
1401
1408
|
selectOptions: WaitOptions | undefined,
|
|
1402
1409
|
): AsyncIterable<Next<Branches>> {
|
|
@@ -1411,7 +1418,9 @@ class WorkflowOwner {
|
|
|
1411
1418
|
([branch, source]) =>
|
|
1412
1419
|
[
|
|
1413
1420
|
branch,
|
|
1414
|
-
reads.has(branch)
|
|
1421
|
+
reads.has(branch)
|
|
1422
|
+
? { name: "at:due", from: branch }
|
|
1423
|
+
: sourceTake("select", branch, source),
|
|
1415
1424
|
] as const,
|
|
1416
1425
|
);
|
|
1417
1426
|
this.#checkSources("select", pairs);
|
|
@@ -1427,7 +1436,7 @@ class WorkflowOwner {
|
|
|
1427
1436
|
if (scope?.stage !== undefined)
|
|
1428
1437
|
throw new Error(`select is in stage ${scope.stage}: a loop's wait is not a stage's`);
|
|
1429
1438
|
const [event] = await this.#holdingWait(scope, async (signal) => [
|
|
1430
|
-
await
|
|
1439
|
+
await waits.select(takes, labelOf(selectOptions?.label), signal),
|
|
1431
1440
|
]);
|
|
1432
1441
|
const taken = event as WorkflowEvent<string, JsonValue>;
|
|
1433
1442
|
const branch = branchOf(pairs, taken) as string;
|
|
@@ -1442,58 +1451,37 @@ class WorkflowOwner {
|
|
|
1442
1451
|
};
|
|
1443
1452
|
}
|
|
1444
1453
|
|
|
1445
|
-
/** A started turn among a wait's branches, which naming its agent replaces. */
|
|
1446
1454
|
/**
|
|
1447
|
-
* A wait's branches as a run can take them: a
|
|
1448
|
-
*
|
|
1449
|
-
* one, which would never come.
|
|
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.
|
|
1450
1457
|
*/
|
|
1451
1458
|
#checkSources(what: string, takes: Iterable<readonly [string, WaitTake]>): void {
|
|
1452
1459
|
const given = [...takes];
|
|
1453
|
-
if (!this.options.
|
|
1460
|
+
if (!this.options.eventWaits) {
|
|
1454
1461
|
const unheard = given.find(([, take]) => HEARD_WITH_EVENTS.has(take.name));
|
|
1455
1462
|
if (unheard)
|
|
1456
1463
|
throw new Error(
|
|
1457
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`,
|
|
1458
1465
|
);
|
|
1459
1466
|
}
|
|
1460
|
-
if (given.some(([, take]) => take.turn !== undefined))
|
|
1461
|
-
this.options.deprecations.used("turn-source");
|
|
1462
1467
|
}
|
|
1463
1468
|
|
|
1464
|
-
/** `outcome`, which a deprecated name the contract defines reports its use on. */
|
|
1465
|
-
// Best effort: an outcome the workflow copies or freezes, or a stage reuses, reports nothing.
|
|
1466
|
-
#reporting = <T>(outcome: T): T => {
|
|
1467
|
-
if (typeof outcome === "object" && outcome !== null && !Object.isFrozen(outcome))
|
|
1468
|
-
Object.defineProperty(outcome, REPORT_DEPRECATED, {
|
|
1469
|
-
value: this.options.deprecations.reported,
|
|
1470
|
-
configurable: true,
|
|
1471
|
-
});
|
|
1472
|
-
return outcome;
|
|
1473
|
-
};
|
|
1474
|
-
|
|
1475
1469
|
#waiting = false;
|
|
1476
1470
|
/** The races and sleeps this attempt has run, each by its name once. */
|
|
1477
1471
|
readonly #raced = new Set<string>();
|
|
1478
1472
|
/** Each agent's turn an earlier attempt started whose end no wait has taken yet. */
|
|
1479
1473
|
readonly #restored: Map<string, RestoredTurn>;
|
|
1480
1474
|
|
|
1481
|
-
/** How an agent of an event run records its started turns in
|
|
1482
|
-
#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 {
|
|
1483
1477
|
return {
|
|
1484
|
-
begin: (turn, meta, execution) =>
|
|
1485
|
-
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 }),
|
|
1486
1480
|
checkDeclared: () => {
|
|
1487
|
-
|
|
1488
|
-
if (declared.has(`${key}:ended`)) return;
|
|
1489
|
-
// A workflow that declares an end has moved to declaring them; one written before can't have.
|
|
1490
|
-
if ([...declared].some((name) => awfEventOf(name)?.kind === "ended"))
|
|
1481
|
+
if (!this.options.declared.has(`${key}:ended`))
|
|
1491
1482
|
throw new Error(
|
|
1492
|
-
key
|
|
1493
|
-
? `agent ${key}: start()'s end arrives as {key}:ended, which a key with a colon can't name: rename it ${key.replaceAll(":", "-")} and declare "${key.replaceAll(":", "-")}:ended": AgentEnded in events`
|
|
1494
|
-
: `agent ${key}: start()'s end arrives as ${key}:ended, which the workflow declares in events, "${key}:ended": AgentEnded, as it does its other agents' ends`,
|
|
1483
|
+
`agent ${key}: start()'s end arrives as ${key}:ended, which the workflow declares in events, "${key}:ended": AgentEnded`,
|
|
1495
1484
|
);
|
|
1496
|
-
deprecations.used("undeclared-ended", key);
|
|
1497
1485
|
},
|
|
1498
1486
|
// Read when the agent opens: a wait before then may have taken the end.
|
|
1499
1487
|
restored: () => this.#restored.get(key),
|
|
@@ -1518,10 +1506,9 @@ class WorkflowOwner {
|
|
|
1518
1506
|
* Nothing of the run is working but its wait: no agent's turn, message or compaction, and no
|
|
1519
1507
|
* decision, sandbox or agent opening in flight.
|
|
1520
1508
|
*/
|
|
1521
|
-
idle(
|
|
1522
|
-
const open = this.options.stages.openStage;
|
|
1509
|
+
idle(): boolean {
|
|
1523
1510
|
return (
|
|
1524
|
-
|
|
1511
|
+
this.options.stages.openStage === undefined &&
|
|
1525
1512
|
this.#inFlight.size === 0 &&
|
|
1526
1513
|
[...this.#logical.values()].every((agent) => !agent.busy)
|
|
1527
1514
|
);
|
|
@@ -1561,36 +1548,17 @@ class WorkflowOwner {
|
|
|
1561
1548
|
}
|
|
1562
1549
|
return false;
|
|
1563
1550
|
}
|
|
1564
|
-
/**
|
|
1565
|
-
readonly #stageBatches = new Map<string, WorkflowEvent<string, JsonValue>[]>();
|
|
1566
|
-
/** Stages around a wait this attempt entered: each is entered once, as a loop over one spins. */
|
|
1567
|
-
readonly #waitedStages = new Set<string>();
|
|
1568
|
-
|
|
1569
|
-
/** 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`. */
|
|
1570
1552
|
private async receive(
|
|
1571
|
-
|
|
1553
|
+
waits: RunWaits,
|
|
1572
1554
|
spec: WaitOptions | undefined,
|
|
1573
1555
|
): Promise<WorkflowEvent<string, JsonValue>[]> {
|
|
1574
1556
|
const scope = this.#beginWait("receive()");
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
throw new Error(`stage ${scope.stage} has waited once; a stage holds one wait`);
|
|
1579
|
-
try {
|
|
1580
|
-
const taken = await this.#holdingWait(scope, (signal) =>
|
|
1581
|
-
mailbox.receive(label, 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`,
|
|
1582
1560
|
);
|
|
1583
|
-
|
|
1584
|
-
this.#stageBatches.set(scope.stage, taken);
|
|
1585
|
-
this.#waitedStages.add(scope.stage);
|
|
1586
|
-
}
|
|
1587
|
-
return taken;
|
|
1588
|
-
} catch (error) {
|
|
1589
|
-
// Parked in a stage: left unended now, before anything closes it, for the next attempt.
|
|
1590
|
-
if (error instanceof RunParked && scope?.stage !== undefined)
|
|
1591
|
-
this.options.stages.abandon(scope.stage);
|
|
1592
|
-
throw error;
|
|
1593
|
-
}
|
|
1561
|
+
return this.#holdingWait(scope, (signal) => waits.receive(receiveLabel(spec), signal));
|
|
1594
1562
|
}
|
|
1595
1563
|
|
|
1596
1564
|
/** An agent as messaging sees it, from its entry and its operations. */
|
|
@@ -1621,27 +1589,14 @@ class WorkflowOwner {
|
|
|
1621
1589
|
!isJsonValue(labels))
|
|
1622
1590
|
)
|
|
1623
1591
|
throw new Error("a participant's labels are a JSON object");
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
const replies = declared.has(`${key}:reply`);
|
|
1629
|
-
if (to === "code") {
|
|
1630
|
-
if (!this.options.mailbox)
|
|
1631
|
-
throw new Error(
|
|
1632
|
-
"a participant routed to the code needs a workflow with events, whose waits take its replies",
|
|
1633
|
-
);
|
|
1634
|
-
// A workflow that declares a reply has moved to declaring them; one written before can't have.
|
|
1635
|
-
if (!replies && [...declared].some((name) => awfEventOf(name)?.kind === "reply"))
|
|
1636
|
-
throw new Error(
|
|
1637
|
-
`participant ${key}: its messages reach the code when "${key}:reply": ParticipantReply is declared in events, as this workflow's other replies are; to: "code" is deprecated`,
|
|
1638
|
-
);
|
|
1639
|
-
deprecations.used("to-code", key);
|
|
1640
|
-
}
|
|
1592
|
+
if ((spec as { to?: unknown }).to !== undefined)
|
|
1593
|
+
throw new Error(
|
|
1594
|
+
`participant ${key} is connected without to: its messages reach the code when the workflow declares "${key}:reply": ParticipantReply in events`,
|
|
1595
|
+
);
|
|
1641
1596
|
this.#messaging.connect(
|
|
1642
1597
|
key,
|
|
1643
1598
|
labels === undefined ? undefined : structuredClone(labels),
|
|
1644
|
-
|
|
1599
|
+
this.options.declared.has(`${key}:reply`) ? "code" : undefined,
|
|
1645
1600
|
);
|
|
1646
1601
|
const ref: ParticipantRef = {
|
|
1647
1602
|
...participantRef(key),
|
|
@@ -1656,21 +1611,6 @@ class WorkflowOwner {
|
|
|
1656
1611
|
return this.#messaging.release(key, reason);
|
|
1657
1612
|
},
|
|
1658
1613
|
};
|
|
1659
|
-
if (to === "code") {
|
|
1660
|
-
const take = { name: "participant:reply", from: key };
|
|
1661
|
-
const routed = asSource(
|
|
1662
|
-
Object.freeze({
|
|
1663
|
-
...ref,
|
|
1664
|
-
is: (event: { name: string; from: string }) => {
|
|
1665
|
-
deprecations.used("participant-is");
|
|
1666
|
-
return matches(event, take);
|
|
1667
|
-
},
|
|
1668
|
-
}),
|
|
1669
|
-
take,
|
|
1670
|
-
) as unknown as CodeParticipantRef;
|
|
1671
|
-
this.#outsideRefs.set(key, routed);
|
|
1672
|
-
return routed;
|
|
1673
|
-
}
|
|
1674
1614
|
this.#outsideRefs.set(key, ref);
|
|
1675
1615
|
return ref;
|
|
1676
1616
|
}
|
|
@@ -1678,7 +1618,9 @@ class WorkflowOwner {
|
|
|
1678
1618
|
/** A key an agent may not take: a reserved one, or an outside participant's. */
|
|
1679
1619
|
#assertFreeKey(key: string, who: string): void {
|
|
1680
1620
|
if (typeof key === "string" && key.includes(":"))
|
|
1681
|
-
|
|
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
|
+
);
|
|
1682
1624
|
if ((RESERVED_PARTICIPANT_KEYS as readonly string[]).includes(key.toLowerCase()))
|
|
1683
1625
|
throw new Error(
|
|
1684
1626
|
`${key} is reserved: an agent could read a message from it as authority; ${who} takes another key`,
|
|
@@ -1726,11 +1668,9 @@ class WorkflowOwner {
|
|
|
1726
1668
|
throw new Error("a route between agents names two of them");
|
|
1727
1669
|
ends = { between: [end(shape.between[0]), end(shape.between[1])] };
|
|
1728
1670
|
} else ends = { from: end(shape.from), to: end(shape.to) };
|
|
1729
|
-
if (access.timeout != null && access.timeoutMs != null)
|
|
1730
|
-
throw new Error("a route's timeout is given alone: timeoutMs is its deprecated form");
|
|
1731
1671
|
const timeoutMs =
|
|
1732
1672
|
access.timeout == null
|
|
1733
|
-
?
|
|
1673
|
+
? undefined
|
|
1734
1674
|
: positiveDurationMilliseconds(access.timeout, "a route's timeout");
|
|
1735
1675
|
this.#messaging.grant(ends, {
|
|
1736
1676
|
purpose: access.purpose,
|
|
@@ -1738,8 +1678,6 @@ class WorkflowOwner {
|
|
|
1738
1678
|
timeoutMs,
|
|
1739
1679
|
scope: scope?.messages ?? this.#messaging.attempt,
|
|
1740
1680
|
});
|
|
1741
|
-
if (access.timeout == null && access.timeoutMs != null)
|
|
1742
|
-
this.options.deprecations.used("timeout-ms", "on a route");
|
|
1743
1681
|
}
|
|
1744
1682
|
|
|
1745
1683
|
/** Who owns a message operation a route in `scope` opens: its stage, or the attempt. */
|
|
@@ -1911,22 +1849,7 @@ class WorkflowOwner {
|
|
|
1911
1849
|
});
|
|
1912
1850
|
if (entered.kind === "reuse") {
|
|
1913
1851
|
entered.release();
|
|
1914
|
-
|
|
1915
|
-
const batch =
|
|
1916
|
-
entered.value === undefined && !options
|
|
1917
|
-
? (await this.options.mailbox?.batchOf(name))?.map((event) =>
|
|
1918
|
-
presented(event, this.options.declared),
|
|
1919
|
-
)
|
|
1920
|
-
: undefined;
|
|
1921
|
-
if (batch) {
|
|
1922
|
-
this.options.deprecations.used("stage-receive");
|
|
1923
|
-
if (this.#waitedStages.has(name))
|
|
1924
|
-
throw new Error(
|
|
1925
|
-
`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`,
|
|
1926
|
-
);
|
|
1927
|
-
this.#waitedStages.add(name);
|
|
1928
|
-
}
|
|
1929
|
-
return (batch as JsonValue | undefined) ?? entered.value;
|
|
1852
|
+
return entered.value;
|
|
1930
1853
|
}
|
|
1931
1854
|
const open = entered.stage;
|
|
1932
1855
|
const messages = this.#messaging.scope(name);
|
|
@@ -1944,22 +1867,9 @@ class WorkflowOwner {
|
|
|
1944
1867
|
this.#messaging.finishing(messages);
|
|
1945
1868
|
scope.seal();
|
|
1946
1869
|
await this.drainMessages(messages, () => scope.settleOwned());
|
|
1947
|
-
// 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.
|
|
1948
1871
|
await this.options.waits.flush();
|
|
1949
1872
|
if (this.options.stages.stopped?.stage === name) throw this.options.stages.caught();
|
|
1950
|
-
const batch = this.#stageBatches.get(name);
|
|
1951
|
-
if (batch && !options) {
|
|
1952
|
-
const same =
|
|
1953
|
-
Array.isArray(returned) &&
|
|
1954
|
-
returned.length === batch.length &&
|
|
1955
|
-
returned.every((event, i) => (event as { id?: unknown })?.id === batch[i]?.id);
|
|
1956
|
-
if (!same)
|
|
1957
|
-
throw new Error(
|
|
1958
|
-
`stage ${name} returned other events than its wait took; its work is the wait alone`,
|
|
1959
|
-
);
|
|
1960
|
-
await open.succeed(undefined);
|
|
1961
|
-
return batch as JsonValue;
|
|
1962
|
-
}
|
|
1963
1873
|
const value = stageValue(name, options, returned);
|
|
1964
1874
|
await open.succeed(value, summaryOf(name, options, value, this.options.onLog));
|
|
1965
1875
|
return value;
|
|
@@ -1979,52 +1889,31 @@ class WorkflowOwner {
|
|
|
1979
1889
|
}
|
|
1980
1890
|
throw error;
|
|
1981
1891
|
} finally {
|
|
1982
|
-
this.#stageBatches.delete(name);
|
|
1983
1892
|
this.#messaging.closeScope(messages);
|
|
1984
1893
|
this.#stageScopes.delete(messages);
|
|
1985
1894
|
removeFromParent?.();
|
|
1986
1895
|
}
|
|
1987
1896
|
}
|
|
1988
1897
|
|
|
1989
|
-
/** An agent's pane options as written, which a reopen is compared by; a deprecated one logged. */
|
|
1990
|
-
private writtenPane(spec: {
|
|
1991
|
-
pane?: PaneSpec;
|
|
1992
|
-
layout?: PaneLayout;
|
|
1993
|
-
keepPane?: KeepPane;
|
|
1994
|
-
}): Pick<AgentIdentity, "pane" | "layout" | "keepPane"> {
|
|
1995
|
-
if (spec.layout !== undefined) this.options.deprecations.used("pane-layout");
|
|
1996
|
-
if (spec.keepPane !== undefined) this.options.deprecations.used("keep-pane");
|
|
1997
|
-
return {
|
|
1998
|
-
...(spec.pane === undefined ? {} : { pane: storedLayout(spec.pane) }),
|
|
1999
|
-
...(spec.layout === undefined ? {} : { layout: storedLayout(spec.layout) }),
|
|
2000
|
-
...(spec.keepPane === undefined ? {} : { keepPane: spec.keepPane }),
|
|
2001
|
-
};
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
1898
|
private openAgent(spec: AgentOpenSpec): Promise<AgentRef> {
|
|
2005
1899
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
2006
|
-
if (spec.labels !== undefined) this.options.deprecations.used("labels");
|
|
2007
1900
|
scopes.getStore()?.assertAccepting();
|
|
2008
1901
|
this.#assertFreeKey(spec.key, "an agent");
|
|
2009
1902
|
if (this.#caller?.key === spec.key) {
|
|
2010
1903
|
throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
|
|
2011
1904
|
}
|
|
1905
|
+
this.options.ops.claim(spec.key, "agent");
|
|
2012
1906
|
const inRunSandbox = this.options.sandboxes.hasRunSandbox;
|
|
2013
1907
|
if (inRunSandbox && spec.sandbox !== undefined)
|
|
2014
1908
|
throw new Error(`agent ${spec.key}: ${RUN_SANDBOX_ONLY}`);
|
|
2015
1909
|
const existing = this.#agents.get(spec.key);
|
|
2016
1910
|
if (existing?.stopped !== undefined) throw stoppedError(spec.key, existing.stopped);
|
|
2017
1911
|
const given = spec.keepAlive;
|
|
2018
|
-
if (given !== undefined && !this.options.
|
|
1912
|
+
if (given !== undefined && !this.options.eventWaits)
|
|
2019
1913
|
throw new Error(
|
|
2020
1914
|
`agent ${spec.key}: keepAlive is for a workflow with events, whose runs park; this one's never do`,
|
|
2021
1915
|
);
|
|
2022
|
-
const keepAlive =
|
|
2023
|
-
given === undefined
|
|
2024
|
-
? undefined
|
|
2025
|
-
: checkedKeepAlive(spec.key, given, () =>
|
|
2026
|
-
this.options.deprecations.used("keep-alive-timeout-ms"),
|
|
2027
|
-
);
|
|
1916
|
+
const keepAlive = given === undefined ? undefined : checkedKeepAlive(spec.key, given);
|
|
2028
1917
|
const execution = existing
|
|
2029
1918
|
? constrainExistingExecution(spec.runtime, existing.identity.execution)
|
|
2030
1919
|
: withKnownEffort(resolveExecution(spec.runtime, this.options.runtime));
|
|
@@ -2034,25 +1923,19 @@ class WorkflowOwner {
|
|
|
2034
1923
|
spec.sandbox !== undefined || existing?.identity.sandbox !== undefined || inRunSandbox,
|
|
2035
1924
|
};
|
|
2036
1925
|
checkPaneOptions(spec.key, spec, paneless);
|
|
2037
|
-
const
|
|
1926
|
+
const pane = spec.pane === undefined ? undefined : storedPane(spec.pane);
|
|
2038
1927
|
const identity: AgentIdentity = {
|
|
2039
1928
|
execution,
|
|
2040
1929
|
cwd: spec.cwd ?? this.options.cwd,
|
|
2041
1930
|
...(spec.instructions === undefined ? {} : { instructions: spec.instructions }),
|
|
2042
|
-
...(spec.labels === undefined ? {} : { labels: structuredClone(spec.labels) }),
|
|
2043
1931
|
...(spec.sandbox === undefined ? {} : { sandbox: spec.sandbox }),
|
|
2044
1932
|
...(spec.skills === undefined ? {} : { skills: readSkillSources(spec.skills) }),
|
|
2045
|
-
...
|
|
2046
|
-
asked: paneAsked(
|
|
1933
|
+
...(pane === undefined ? {} : { pane }),
|
|
1934
|
+
asked: paneAsked(pane, this.options.panes, !paneless.headless && !paneless.sandboxed),
|
|
2047
1935
|
};
|
|
2048
1936
|
const scope = scopes.getStore();
|
|
2049
1937
|
const inheritedDeadline = scope?.deadline ?? this.options.deadline;
|
|
2050
|
-
const effectiveDeadline = boundedDeadline(
|
|
2051
|
-
{ timeout: spec.timeout, deadline: spec.deadline },
|
|
2052
|
-
inheritedDeadline,
|
|
2053
|
-
(name, call) => this.options.deprecations.used(name, call),
|
|
2054
|
-
"on open",
|
|
2055
|
-
);
|
|
1938
|
+
const effectiveDeadline = boundedDeadline(spec.timeout, inheritedDeadline);
|
|
2056
1939
|
assertDeadline(effectiveDeadline);
|
|
2057
1940
|
if (existing) {
|
|
2058
1941
|
assertCompatibleAgent(spec.key, existing.identity, identity, spec);
|
|
@@ -2107,8 +1990,7 @@ class WorkflowOwner {
|
|
|
2107
1990
|
// A kept session already holds the instructions; they are not sent again.
|
|
2108
1991
|
...(kept ? { continues: kept } : {}),
|
|
2109
1992
|
...(spec.instructions === undefined || kept ? {} : { instructions: spec.instructions }),
|
|
2110
|
-
...(this.options.
|
|
2111
|
-
...(spec.labels === undefined ? {} : { labels: spec.labels }),
|
|
1993
|
+
...(this.options.eventWaits ? { paneAtFirstTurn: true as const } : {}),
|
|
2112
1994
|
...(seated ? { occupant: seated.occupant } : {}),
|
|
2113
1995
|
...(seated?.webSearch ? { webSearch: true } : {}),
|
|
2114
1996
|
...(skills ? { skills } : {}),
|
|
@@ -2148,7 +2030,7 @@ class WorkflowOwner {
|
|
|
2148
2030
|
|
|
2149
2031
|
/**
|
|
2150
2032
|
* Ends an agent the workflow has no more use for, as an operation that fails ends it: what it
|
|
2151
|
-
* 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.
|
|
2152
2034
|
*/
|
|
2153
2035
|
private stopAgent(key: string, reason: string | undefined, what: string): Promise<boolean> {
|
|
2154
2036
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
@@ -2214,6 +2096,7 @@ class WorkflowOwner {
|
|
|
2214
2096
|
const scope = scopes.getStore();
|
|
2215
2097
|
scope?.assertAccepting();
|
|
2216
2098
|
this.#assertFreeKey(key, "an agent");
|
|
2099
|
+
this.options.ops.claim(key, "agent");
|
|
2217
2100
|
const found = this.options.runtime.host.caller;
|
|
2218
2101
|
if (!found) return Promise.resolve(null);
|
|
2219
2102
|
if (this.#caller) {
|
|
@@ -2241,7 +2124,9 @@ class WorkflowOwner {
|
|
|
2241
2124
|
};
|
|
2242
2125
|
const ledger = this.options.ledger.agent(accounted);
|
|
2243
2126
|
this.options.skills.record(key, "operator");
|
|
2244
|
-
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
|
+
);
|
|
2245
2130
|
const state = opened.then(async (channel) => {
|
|
2246
2131
|
try {
|
|
2247
2132
|
const launcher = await installAgentLauncher(
|
|
@@ -2337,7 +2222,10 @@ class WorkflowOwner {
|
|
|
2337
2222
|
): { state: Promise<LogicalAgent>; channel: Promise<ResultChannel> } {
|
|
2338
2223
|
const opened = placed.then((skills) => {
|
|
2339
2224
|
if (skills?.given.ownHome) accounted.home = skills.given.ownHome;
|
|
2340
|
-
|
|
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
|
+
);
|
|
2341
2229
|
});
|
|
2342
2230
|
const reachable = opened.then(async (channel) => ({
|
|
2343
2231
|
channel,
|
|
@@ -2415,7 +2303,7 @@ class WorkflowOwner {
|
|
|
2415
2303
|
const given = skillsLayout(
|
|
2416
2304
|
execution.harness,
|
|
2417
2305
|
resolved.map((skill) => skill.name),
|
|
2418
|
-
{ bundle: join(runDir, "agents", id), cwd: realCwd },
|
|
2306
|
+
{ bundle: join(workDir(runDir), "agents", id), cwd: realCwd },
|
|
2419
2307
|
);
|
|
2420
2308
|
this.options.skills.record(key, resolved, given.ownHome);
|
|
2421
2309
|
const { ownHome } = given;
|
|
@@ -2426,8 +2314,12 @@ class WorkflowOwner {
|
|
|
2426
2314
|
await mkdir(dirname(ownHome), { recursive: true });
|
|
2427
2315
|
const seeded = await seedHome(
|
|
2428
2316
|
ownHome,
|
|
2429
|
-
join(runDir, "staging", id),
|
|
2430
|
-
|
|
2317
|
+
join(workDir(runDir), "staging", id),
|
|
2318
|
+
(this.options.homes ?? OPERATOR_HOMES).host(
|
|
2319
|
+
execution.harness,
|
|
2320
|
+
ownHome,
|
|
2321
|
+
this.options.environment,
|
|
2322
|
+
),
|
|
2431
2323
|
realCwd,
|
|
2432
2324
|
this.options.locks,
|
|
2433
2325
|
async (staged) => {
|
|
@@ -2476,10 +2368,11 @@ class WorkflowOwner {
|
|
|
2476
2368
|
// What the agent really ran in, which is where accounting finds its sessions.
|
|
2477
2369
|
accounted.cwd = seat.cwd;
|
|
2478
2370
|
accounted.home = seat.home;
|
|
2371
|
+
if (seat.skills) this.#givenSkills.set(spec.key, seat.skills);
|
|
2479
2372
|
let channel: ResultChannel | undefined;
|
|
2480
2373
|
try {
|
|
2481
2374
|
channel = await this.options.control.openChannel(spec.key, (id) =>
|
|
2482
|
-
|
|
2375
|
+
this.launcherReported(spec.key, execution.harness, sessions, id),
|
|
2483
2376
|
);
|
|
2484
2377
|
const door = await installSandboxedDoor(
|
|
2485
2378
|
dirname(channel.endpoint),
|
|
@@ -2543,6 +2436,7 @@ class WorkflowOwner {
|
|
|
2543
2436
|
execution,
|
|
2544
2437
|
session,
|
|
2545
2438
|
slots: this.options.slots,
|
|
2439
|
+
ops: this.options.ops,
|
|
2546
2440
|
endpoint,
|
|
2547
2441
|
launcher,
|
|
2548
2442
|
deadline: this.options.deadline,
|
|
@@ -2550,6 +2444,11 @@ class WorkflowOwner {
|
|
|
2550
2444
|
progress: this.options.progress,
|
|
2551
2445
|
recordTurn: this.options.recordTurn,
|
|
2552
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
|
+
}),
|
|
2553
2452
|
track: (promise) => this.track(promise),
|
|
2554
2453
|
isRunClosing: () => this.#closed || this.#cleanupFailures.length > 0,
|
|
2555
2454
|
checkOperation: () => this.options.stages.checkOperation(),
|
|
@@ -2559,7 +2458,7 @@ class WorkflowOwner {
|
|
|
2559
2458
|
this.options.failRun(error);
|
|
2560
2459
|
},
|
|
2561
2460
|
livenessPolicy: this.options.livenessPolicy,
|
|
2562
|
-
...(this.options.
|
|
2461
|
+
...(this.options.eventWaits ? { turns: this.#turnsOf(key, this.options.eventWaits) } : {}),
|
|
2563
2462
|
fork: (spec, take, settings) => this.forkAgent(key, spec, take, settings),
|
|
2564
2463
|
close: (reason) => {
|
|
2565
2464
|
try {
|
|
@@ -2568,8 +2467,6 @@ class WorkflowOwner {
|
|
|
2568
2467
|
return Promise.reject(error);
|
|
2569
2468
|
}
|
|
2570
2469
|
},
|
|
2571
|
-
deprecated: (name, subject) => this.options.deprecations.used(name, subject),
|
|
2572
|
-
reporting: (outcome) => this.#reporting(outcome),
|
|
2573
2470
|
keepPane: () => this.#agents.get(key)?.identity.asked?.keepPane,
|
|
2574
2471
|
keepAlive: () => this.#agents.get(key)?.keepAlive,
|
|
2575
2472
|
defaultWindowMs: this.options.defaultWindowMs ?? DEFAULT_WINDOW_MS,
|
|
@@ -2613,7 +2510,6 @@ class WorkflowOwner {
|
|
|
2613
2510
|
unopened?: AgentEntry,
|
|
2614
2511
|
): Promise<AgentRef> {
|
|
2615
2512
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
2616
|
-
if (spec.labels !== undefined) this.options.deprecations.used("labels");
|
|
2617
2513
|
const scope = scopes.getStore();
|
|
2618
2514
|
scope?.assertAccepting();
|
|
2619
2515
|
this.options.stages.checkOperation();
|
|
@@ -2621,20 +2517,13 @@ class WorkflowOwner {
|
|
|
2621
2517
|
throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
|
|
2622
2518
|
}
|
|
2623
2519
|
this.#assertFreeKey(spec.key, "an agent");
|
|
2520
|
+
this.options.ops.claim(spec.key, "agent");
|
|
2624
2521
|
// Out before the clone, which throws on a handler, and out of the spec a repeat is compared by.
|
|
2625
2522
|
// A bound is the call's, not the fork's: a repeat with another is the same fork.
|
|
2626
2523
|
const { tools, timeout, ...given } = spec;
|
|
2627
|
-
const forkSpec = structuredClone(
|
|
2628
|
-
|
|
2629
|
-
);
|
|
2630
|
-
if (forkSpec.layout !== undefined) forkSpec.layout = storedLayout(forkSpec.layout);
|
|
2631
|
-
if (forkSpec.pane !== undefined) forkSpec.pane = storedLayout(forkSpec.pane);
|
|
2632
|
-
const deadline = boundedDeadline(
|
|
2633
|
-
{ timeout },
|
|
2634
|
-
scope?.deadline ?? this.options.deadline,
|
|
2635
|
-
(name, call) => this.options.deprecations.used(name, call),
|
|
2636
|
-
"on fork",
|
|
2637
|
-
);
|
|
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);
|
|
2638
2527
|
assertDeadline(deadline);
|
|
2639
2528
|
// A fork of the calling session is one whichever way it was asked for.
|
|
2640
2529
|
const from = unopened || this.#caller?.key === parentKey ? CALLING : parentKey;
|
|
@@ -2662,7 +2551,7 @@ class WorkflowOwner {
|
|
|
2662
2551
|
// child's home: anything added to the parent's would be read as the parent's.
|
|
2663
2552
|
const copy =
|
|
2664
2553
|
sandboxed || opening.accounted.home !== undefined
|
|
2665
|
-
? join(this.options.runDir, "forks", randomUUID())
|
|
2554
|
+
? join(workDir(this.options.runDir), "forks", randomUUID())
|
|
2666
2555
|
: undefined;
|
|
2667
2556
|
const execution = withKnownEffort(forkExecution(parentSettings, forkSpec.runtime));
|
|
2668
2557
|
// A fork of the calling session is an agent the run opened, on the model the session's files
|
|
@@ -2671,17 +2560,15 @@ class WorkflowOwner {
|
|
|
2671
2560
|
delete execution.caller;
|
|
2672
2561
|
const headless = placementOf(execution) === "headless";
|
|
2673
2562
|
checkPaneOptions(spec.key, forkSpec, { headless, sandboxed });
|
|
2674
|
-
const written = this.writtenPane(forkSpec);
|
|
2675
2563
|
const identity: AgentIdentity = {
|
|
2676
2564
|
execution,
|
|
2677
2565
|
cwd: parent.identity.cwd,
|
|
2678
2566
|
...(forkSpec.instructions === undefined ? {} : { instructions: forkSpec.instructions }),
|
|
2679
|
-
...(forkSpec.labels === undefined ? {} : { labels: forkSpec.labels }),
|
|
2680
2567
|
...(parent.identity.skills ? { skills: parent.identity.skills } : {}),
|
|
2681
2568
|
...(parent.identity.sandbox === undefined ? {} : { sandbox: parent.identity.sandbox }),
|
|
2682
2569
|
forkedFrom: { parent: from, spec: forkSpec },
|
|
2683
|
-
...
|
|
2684
|
-
asked: paneAsked(
|
|
2570
|
+
...(forkSpec.pane === undefined ? {} : { pane: forkSpec.pane }),
|
|
2571
|
+
asked: paneAsked(forkSpec.pane, this.options.panes, !headless && !sandboxed),
|
|
2685
2572
|
};
|
|
2686
2573
|
|
|
2687
2574
|
this.options.progress.agentOpened(spec.key, scope?.group);
|
|
@@ -2746,7 +2633,6 @@ class WorkflowOwner {
|
|
|
2746
2633
|
cwd,
|
|
2747
2634
|
execution,
|
|
2748
2635
|
...(forkSpec.instructions === undefined ? {} : { instructions: forkSpec.instructions }),
|
|
2749
|
-
...(forkSpec.labels === undefined ? {} : { labels: forkSpec.labels }),
|
|
2750
2636
|
...(seated ? { occupant: seated.occupant } : {}),
|
|
2751
2637
|
...(seated?.webSearch ? { webSearch: true } : {}),
|
|
2752
2638
|
...(given ? { skills: given } : {}),
|
|
@@ -2810,6 +2696,10 @@ class WorkflowOwner {
|
|
|
2810
2696
|
): void {
|
|
2811
2697
|
state.then(
|
|
2812
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);
|
|
2813
2703
|
this.options.events.event({
|
|
2814
2704
|
kind: "agent-opened",
|
|
2815
2705
|
agent: accounted.key,
|
|
@@ -2817,17 +2707,26 @@ class WorkflowOwner {
|
|
|
2817
2707
|
harness: accounted.execution.harness,
|
|
2818
2708
|
cwd: accounted.cwd,
|
|
2819
2709
|
...(accounted.home === undefined ? {} : { home: accounted.home }),
|
|
2710
|
+
...(seat ? { sandbox: seat.sandbox, domains: seat.domains } : {}),
|
|
2820
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 } : {}),
|
|
2821
2721
|
});
|
|
2822
2722
|
// A fork's session is made before its first operation names it.
|
|
2823
2723
|
if (!as || !("fork" in as)) return;
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
}
|
|
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
|
+
);
|
|
2831
2730
|
},
|
|
2832
2731
|
() => undefined,
|
|
2833
2732
|
);
|
|
@@ -2876,22 +2775,57 @@ class WorkflowOwner {
|
|
|
2876
2775
|
/** Where each TUI agent's pane went, as its host reported it, in the order opened. */
|
|
2877
2776
|
panes(): AgentPaneRecord[] {
|
|
2878
2777
|
return [...this.#agents].flatMap(([agent, entry]) => {
|
|
2879
|
-
const
|
|
2880
|
-
|
|
2881
|
-
const { pane, layout, keepPane } = entry.identity;
|
|
2882
|
-
return [
|
|
2883
|
-
{
|
|
2884
|
-
callPath: [],
|
|
2885
|
-
agent,
|
|
2886
|
-
...(pane === undefined ? {} : { pane }),
|
|
2887
|
-
...(layout === undefined ? {} : { layout }),
|
|
2888
|
-
...(keepPane === undefined ? {} : { keepPane }),
|
|
2889
|
-
placed,
|
|
2890
|
-
},
|
|
2891
|
-
];
|
|
2778
|
+
const pane = paneOf(entry);
|
|
2779
|
+
return pane ? [{ callPath: [], agent, ...pane }] : [];
|
|
2892
2780
|
});
|
|
2893
2781
|
}
|
|
2894
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
|
+
|
|
2895
2829
|
/** A reopened agent names the sandbox it runs in, or none: a different one is a conflict. */
|
|
2896
2830
|
private async assertSameSandbox(
|
|
2897
2831
|
key: string,
|
|
@@ -2910,6 +2844,14 @@ class WorkflowOwner {
|
|
|
2910
2844
|
}
|
|
2911
2845
|
}
|
|
2912
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
|
+
|
|
2913
2855
|
// What a wait takes of it is type-only, and `asSource` marks it at run time.
|
|
2914
2856
|
// biome-ignore lint/correctness/noUnusedVariables: merged into the class below.
|
|
2915
2857
|
interface LogicalAgent extends Source<Taken<AgentEnded>> {}
|
|
@@ -2918,18 +2860,6 @@ interface LogicalAgent extends Source<Taken<AgentEnded>> {}
|
|
|
2918
2860
|
class LogicalAgent implements AgentRef {
|
|
2919
2861
|
readonly [PARTICIPANT] = true as const;
|
|
2920
2862
|
readonly #operationStops = new Set<(stop: OperationStop) => Promise<void>>();
|
|
2921
|
-
readonly #operations = new Map<
|
|
2922
|
-
string,
|
|
2923
|
-
{
|
|
2924
|
-
spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>;
|
|
2925
|
-
result: Promise<RunResult<JsonValue>>;
|
|
2926
|
-
}
|
|
2927
|
-
>();
|
|
2928
|
-
readonly #compactions = new Map<
|
|
2929
|
-
string,
|
|
2930
|
-
{ spec: CompactSpec; result: Promise<TurnOutcome<string>> }
|
|
2931
|
-
>();
|
|
2932
|
-
readonly #sets = new Map<string, { spec: SettingsSpec; result: Promise<TurnOutcome<null>> }>();
|
|
2933
2863
|
/** Its settings now; see `AgentRef.execution`. */
|
|
2934
2864
|
#execution: AgentExecution;
|
|
2935
2865
|
/** What each `set` queued and not yet settled changes, in queue order. */
|
|
@@ -2970,6 +2900,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2970
2900
|
execution: AgentExecution;
|
|
2971
2901
|
session: HarnessSession;
|
|
2972
2902
|
slots: ResultSlotRegistry;
|
|
2903
|
+
/** Numbers its operations, `{key}.{n}`. */
|
|
2904
|
+
ops: OpIds;
|
|
2973
2905
|
endpoint: string;
|
|
2974
2906
|
/** The path this agent is told to run; see `installAgentLauncher`. */
|
|
2975
2907
|
launcher: string;
|
|
@@ -2978,6 +2910,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2978
2910
|
progress: RunProgress;
|
|
2979
2911
|
recordTurn: RecordTurn;
|
|
2980
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;
|
|
2981
2915
|
track<T>(promise: Promise<T>): Promise<T>;
|
|
2982
2916
|
isRunClosing(): boolean;
|
|
2983
2917
|
/** Throws when no turn may start: the attempt is looking on for values it needs. */
|
|
@@ -2998,10 +2932,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2998
2932
|
): Promise<AgentRef>;
|
|
2999
2933
|
/** The workflow ends it: the run tears it down and its key is not opened again. */
|
|
3000
2934
|
close(reason: string | undefined): Promise<boolean>;
|
|
3001
|
-
|
|
3002
|
-
/** An outcome as the workflow is handed it, which a deprecated name it is given reports on. */
|
|
3003
|
-
reporting<T>(outcome: T): T;
|
|
3004
|
-
/** As the workflow wrote it; decides, at close, whether its pane stays. */
|
|
2935
|
+
/** As the operator gave it; decides, at close, whether its pane stays. */
|
|
3005
2936
|
keepPane(): KeepPane | undefined;
|
|
3006
2937
|
/** As the workflow's last open of it gave it. */
|
|
3007
2938
|
keepAlive(): KeepAlive | undefined;
|
|
@@ -3019,7 +2950,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3019
2950
|
const restored = options.turns?.restored();
|
|
3020
2951
|
if (restored)
|
|
3021
2952
|
this.#started = {
|
|
3022
|
-
ref: turnRef(
|
|
2953
|
+
ref: turnRef(restored.turn, async () => false),
|
|
3023
2954
|
meta: restored.meta,
|
|
3024
2955
|
ended: true,
|
|
3025
2956
|
done: Promise.resolve(),
|
|
@@ -3138,30 +3069,24 @@ class LogicalAgent implements AgentRef {
|
|
|
3138
3069
|
const refused = this.#refusedWhileStarted("start");
|
|
3139
3070
|
if (refused) throw refused;
|
|
3140
3071
|
const { meta = null, ...run } = spec;
|
|
3141
|
-
|
|
3142
|
-
const id = run.id ?? randomUUID();
|
|
3072
|
+
const id = randomUUID();
|
|
3143
3073
|
if (!isJsonValue(meta)) throw new Error("a turn's meta is a JSON value");
|
|
3144
3074
|
const ended = Promise.withResolvers<void>();
|
|
3145
3075
|
let written: TurnOutcome<JsonValue>["kind"] | undefined;
|
|
3146
3076
|
const started = {
|
|
3147
|
-
ref: turnRef(
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
await started.done;
|
|
3161
|
-
return written === "cancelled";
|
|
3162
|
-
},
|
|
3163
|
-
this.options.deprecated,
|
|
3164
|
-
),
|
|
3077
|
+
ref: turnRef(id, async (reason?: string) => {
|
|
3078
|
+
if (started.ended) return false;
|
|
3079
|
+
// Its turn waits for the handler, which would wait for the turn's end.
|
|
3080
|
+
const inTool = refusedInTool(this.key, "cancel()");
|
|
3081
|
+
if (inTool) throw inTool;
|
|
3082
|
+
const stop = { kind: "cancelled" as const, reason: reason ?? "the turn was cancelled" };
|
|
3083
|
+
// Its own operation only: one queued behind a message settles cancelled when it is reached.
|
|
3084
|
+
const own = this.#stopsByTurn.get(id);
|
|
3085
|
+
if (own) await own(stop);
|
|
3086
|
+
else this.#cancelledTurns.set(id, stop.reason);
|
|
3087
|
+
await started.done;
|
|
3088
|
+
return written === "cancelled";
|
|
3089
|
+
}),
|
|
3165
3090
|
meta,
|
|
3166
3091
|
ended: false,
|
|
3167
3092
|
done: ended.promise,
|
|
@@ -3180,13 +3105,13 @@ class LogicalAgent implements AgentRef {
|
|
|
3180
3105
|
this.#launching = true;
|
|
3181
3106
|
let launched: Promise<RunResult<JsonValue>>;
|
|
3182
3107
|
try {
|
|
3183
|
-
launched = outsideTools(() => this
|
|
3108
|
+
launched = outsideTools(() => this.#runTurn(run, id));
|
|
3184
3109
|
} catch (error) {
|
|
3185
3110
|
launched = Promise.reject(error);
|
|
3186
3111
|
} finally {
|
|
3187
3112
|
this.#launching = false;
|
|
3188
3113
|
}
|
|
3189
|
-
// 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.
|
|
3190
3115
|
this.options.track(
|
|
3191
3116
|
launched
|
|
3192
3117
|
.then(
|
|
@@ -3231,7 +3156,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3231
3156
|
Meta extends JsonValue = JsonValue,
|
|
3232
3157
|
T extends JsonValue = JsonValue,
|
|
3233
3158
|
E extends EventLike = EventLike,
|
|
3234
|
-
>(event: E): event is E & AgentEndOf<
|
|
3159
|
+
>(event: E): event is E & AgentEndOf<Meta, T> {
|
|
3235
3160
|
return matches(event, { name: "agent:ended", from: this.key });
|
|
3236
3161
|
}
|
|
3237
3162
|
|
|
@@ -3240,54 +3165,24 @@ class LogicalAgent implements AgentRef {
|
|
|
3240
3165
|
run<T extends JsonValue>(
|
|
3241
3166
|
spec: AgentRunTextSpec | AgentRunStructuredSpec<T>,
|
|
3242
3167
|
): Promise<RunResult<string | T>> {
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
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;
|
|
3253
3183
|
const opensWindow = !inTimeoutHandler();
|
|
3254
|
-
|
|
3255
|
-
const
|
|
3256
|
-
const completeSpec = structuredClone({ ...spec, id }) as
|
|
3257
|
-
| AgentRunTextSpec
|
|
3258
|
-
| AgentRunStructuredSpec<JsonValue>;
|
|
3259
|
-
const existing = this.#operations.get(id);
|
|
3260
|
-
if (existing) {
|
|
3261
|
-
if (!isDeepStrictEqual(existing.spec, completeSpec)) {
|
|
3262
|
-
const rejected = Promise.reject<RunResult<string | T>>(
|
|
3263
|
-
new Error(`turn id ${id} was reused with a different specification`),
|
|
3264
|
-
);
|
|
3265
|
-
scope?.track(rejected);
|
|
3266
|
-
return rejected;
|
|
3267
|
-
}
|
|
3268
|
-
scope?.track(existing.result);
|
|
3269
|
-
return existing.result as Promise<RunResult<string | T>>;
|
|
3270
|
-
}
|
|
3271
|
-
let deadline: AbsoluteDeadline | undefined;
|
|
3272
|
-
try {
|
|
3273
|
-
deadline = this.operationDeadline(spec, scope, this.#launching ? "on start" : "on run");
|
|
3274
|
-
const recoveryDeadline = spec.nudge ? spec.nudge.deadline : undefined;
|
|
3275
|
-
if (recoveryDeadline) {
|
|
3276
|
-
this.options.deprecated("nudge-deadline");
|
|
3277
|
-
const own = { unixMilliseconds: recoveryDeadline.unixMilliseconds };
|
|
3278
|
-
assertDeadlineValue(own);
|
|
3279
|
-
deadline = earlierDeadline(deadline, own);
|
|
3280
|
-
}
|
|
3281
|
-
} catch (error) {
|
|
3282
|
-
const rejected = Promise.reject<RunResult<string | T>>(error);
|
|
3283
|
-
scope?.track(rejected);
|
|
3284
|
-
return rejected;
|
|
3285
|
-
}
|
|
3286
|
-
const result = this.queue(async () => {
|
|
3287
|
-
if (this.#closed || this.options.isRunClosing()) {
|
|
3288
|
-
throw new Error("logical agent is closed");
|
|
3289
|
-
}
|
|
3290
|
-
scope?.assertActive();
|
|
3184
|
+
const completeSpec: TurnSpec = structuredClone({ ...spec, id });
|
|
3185
|
+
const result = this.#queueActive(scope, async () => {
|
|
3291
3186
|
const cancelled = this.#cancelledTurns.get(id);
|
|
3292
3187
|
if (cancelled !== undefined) {
|
|
3293
3188
|
this.#cancelledTurns.delete(id);
|
|
@@ -3301,7 +3196,19 @@ class LogicalAgent implements AgentRef {
|
|
|
3301
3196
|
}
|
|
3302
3197
|
const tags = tagsOf(scope, completeSpec.label);
|
|
3303
3198
|
// Drawn here, so a turn that throws before it is delivered is still written as failed.
|
|
3304
|
-
|
|
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);
|
|
3305
3212
|
this.options.progress.turnStarted(this.key, tags);
|
|
3306
3213
|
try {
|
|
3307
3214
|
const settled = await this.executeOperation(completeSpec, scope, deadline, operation);
|
|
@@ -3315,60 +3222,25 @@ class LogicalAgent implements AgentRef {
|
|
|
3315
3222
|
await this.options.afterOperation?.();
|
|
3316
3223
|
}
|
|
3317
3224
|
});
|
|
3318
|
-
|
|
3319
|
-
result.then((settled) => {
|
|
3320
|
-
this.options.reporting(settled.outcome);
|
|
3321
|
-
return settled;
|
|
3322
|
-
}),
|
|
3323
|
-
);
|
|
3324
|
-
scope?.track(tracked);
|
|
3325
|
-
this.#operations.set(id, { spec: completeSpec, result: tracked });
|
|
3326
|
-
return tracked as Promise<RunResult<string | T>>;
|
|
3225
|
+
return this.#tracked(scope, result);
|
|
3327
3226
|
}
|
|
3328
3227
|
|
|
3329
3228
|
compact(spec: CompactSpec): Promise<TurnOutcome<string>> {
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
}
|
|
3340
|
-
if (spec.id !== undefined) this.options.deprecated("call-id");
|
|
3341
|
-
const id = spec.id ?? randomUUID();
|
|
3342
|
-
const completeSpec = structuredClone({ ...spec, id });
|
|
3343
|
-
const existing = this.#compactions.get(id);
|
|
3344
|
-
if (existing) {
|
|
3345
|
-
if (!isDeepStrictEqual(existing.spec, completeSpec)) {
|
|
3346
|
-
const rejected = Promise.reject<TurnOutcome<string>>(
|
|
3347
|
-
new Error(`compaction id ${id} was reused with a different specification`),
|
|
3348
|
-
);
|
|
3349
|
-
scope?.track(rejected);
|
|
3350
|
-
return rejected;
|
|
3351
|
-
}
|
|
3352
|
-
scope?.track(existing.result);
|
|
3353
|
-
return existing.result;
|
|
3354
|
-
}
|
|
3355
|
-
let deadline: AbsoluteDeadline;
|
|
3356
|
-
try {
|
|
3357
|
-
deadline = harnessDeadline(this.operationDeadline(spec, scope, "on compact"));
|
|
3358
|
-
} catch (error) {
|
|
3359
|
-
const rejected = Promise.reject<TurnOutcome<string>>(error);
|
|
3360
|
-
scope?.track(rejected);
|
|
3361
|
-
return rejected;
|
|
3362
|
-
}
|
|
3363
|
-
const result = this.queue(async () => {
|
|
3364
|
-
if (this.#closed || this.options.isRunClosing()) {
|
|
3365
|
-
throw new Error("logical agent is closed");
|
|
3366
|
-
}
|
|
3367
|
-
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 () => {
|
|
3368
3238
|
const tags = tagsOf(scope);
|
|
3369
|
-
const
|
|
3239
|
+
const drawn = this.options.ops.draw(this.key, { kind: "compact", ...this.opWhere(tags) });
|
|
3240
|
+
const operation = this.operationLog(drawn.id, "compact", tags);
|
|
3370
3241
|
this.options.progress.turnStarted(this.key, tags, "compact");
|
|
3371
3242
|
try {
|
|
3243
|
+
if (drawn.lost !== undefined) throw new Error(drawn.lost);
|
|
3372
3244
|
const outcome = await this.executeCompaction(id, spec.prompt, deadline, scope, operation);
|
|
3373
3245
|
this.turnEnded(outcome, "compact");
|
|
3374
3246
|
return outcome;
|
|
@@ -3380,48 +3252,23 @@ class LogicalAgent implements AgentRef {
|
|
|
3380
3252
|
await this.options.afterOperation?.();
|
|
3381
3253
|
}
|
|
3382
3254
|
});
|
|
3383
|
-
|
|
3384
|
-
scope?.track(tracked);
|
|
3385
|
-
this.#compactions.set(id, { spec: completeSpec, result: tracked });
|
|
3386
|
-
return tracked;
|
|
3255
|
+
return this.#tracked(scope, result);
|
|
3387
3256
|
}
|
|
3388
3257
|
|
|
3389
3258
|
/** Not a turn: progress counts none for it, and it asks the model nothing. */
|
|
3390
3259
|
set(spec: SettingsSpec): Promise<TurnOutcome<null>> {
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
scope
|
|
3397
|
-
}
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
const existing = this.#sets.get(id);
|
|
3404
|
-
if (existing) {
|
|
3405
|
-
if (!isDeepStrictEqual(existing.spec, completeSpec)) {
|
|
3406
|
-
const rejected = Promise.reject<TurnOutcome<null>>(
|
|
3407
|
-
new Error(`settings id ${id} was reused with a different specification`),
|
|
3408
|
-
);
|
|
3409
|
-
scope?.track(rejected);
|
|
3410
|
-
return rejected;
|
|
3411
|
-
}
|
|
3412
|
-
scope?.track(existing.result);
|
|
3413
|
-
return existing.result;
|
|
3414
|
-
}
|
|
3415
|
-
let change: SettingsChange;
|
|
3416
|
-
let deadline: AbsoluteDeadline;
|
|
3417
|
-
try {
|
|
3418
|
-
change = this.settingsChange(spec);
|
|
3419
|
-
deadline = harnessDeadline(this.operationDeadline(spec, scope, "on set"));
|
|
3420
|
-
} catch (error) {
|
|
3421
|
-
const rejected = Promise.reject<TurnOutcome<null>>(error);
|
|
3422
|
-
scope?.track(rejected);
|
|
3423
|
-
return rejected;
|
|
3424
|
-
}
|
|
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;
|
|
3425
3272
|
this.#pendingSets.push(change);
|
|
3426
3273
|
const result = this.queue(async () => {
|
|
3427
3274
|
try {
|
|
@@ -3436,12 +3283,68 @@ class LogicalAgent implements AgentRef {
|
|
|
3436
3283
|
this.#pendingSets.splice(this.#pendingSets.indexOf(change), 1);
|
|
3437
3284
|
}
|
|
3438
3285
|
});
|
|
3439
|
-
|
|
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> {
|
|
3330
|
+
const tracked = this.options.track(result);
|
|
3440
3331
|
scope?.track(tracked);
|
|
3441
|
-
this.#sets.set(id, { spec: completeSpec, result: tracked });
|
|
3442
3332
|
return tracked;
|
|
3443
3333
|
}
|
|
3444
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
|
+
|
|
3445
3348
|
/**
|
|
3446
3349
|
* An operation's start and failure in the run's event log, under the id its record keeps. Its
|
|
3447
3350
|
* settling is the turn line `recordTurn` writes.
|
|
@@ -3450,10 +3353,12 @@ class LogicalAgent implements AgentRef {
|
|
|
3450
3353
|
operationId: string,
|
|
3451
3354
|
of: TurnRecord["kind"],
|
|
3452
3355
|
tags: OperationTags,
|
|
3356
|
+
unrecorded?: string,
|
|
3453
3357
|
): OperationLog {
|
|
3454
3358
|
let deliveredAt: number | undefined;
|
|
3455
3359
|
/** Named by its host before delivery, for its start to carry. */
|
|
3456
3360
|
const named: string[] = [];
|
|
3361
|
+
// Its sessions as of now, recorded before the line that names it, as a turn's are.
|
|
3457
3362
|
const fields = () => {
|
|
3458
3363
|
const { callPath, sessions, ...record } = this.options.ledger.unsettled(
|
|
3459
3364
|
operationId,
|
|
@@ -3463,16 +3368,16 @@ class LogicalAgent implements AgentRef {
|
|
|
3463
3368
|
);
|
|
3464
3369
|
const harness = this.#execution.harness;
|
|
3465
3370
|
const fresh = named.filter((id) => !sessions.some((known) => known.id === id));
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
};
|
|
3371
|
+
this.options.sessionsKnown(
|
|
3372
|
+
[...sessions, ...fresh.map((id) => ({ harness, id }))],
|
|
3373
|
+
operationId,
|
|
3374
|
+
);
|
|
3375
|
+
return { of, callPath, ...record };
|
|
3472
3376
|
};
|
|
3473
3377
|
return {
|
|
3474
3378
|
operationId,
|
|
3475
3379
|
tags,
|
|
3380
|
+
...(unrecorded === undefined ? {} : { unrecorded }),
|
|
3476
3381
|
delivered: (at: number) => {
|
|
3477
3382
|
deliveredAt = at;
|
|
3478
3383
|
this.#hasRun = true;
|
|
@@ -3484,11 +3389,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3484
3389
|
named.push(id);
|
|
3485
3390
|
return;
|
|
3486
3391
|
}
|
|
3487
|
-
this.options.
|
|
3488
|
-
kind: "session-known",
|
|
3489
|
-
session: { harness: this.#execution.harness, id },
|
|
3490
|
-
operationId,
|
|
3491
|
-
});
|
|
3392
|
+
this.options.sessionsKnown([{ harness: this.#execution.harness, id }], operationId);
|
|
3492
3393
|
},
|
|
3493
3394
|
failed: (error: unknown) =>
|
|
3494
3395
|
this.options.events.event({
|
|
@@ -3499,7 +3400,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3499
3400
|
};
|
|
3500
3401
|
}
|
|
3501
3402
|
|
|
3502
|
-
/** 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. */
|
|
3503
3404
|
private turnEnded(
|
|
3504
3405
|
outcome: TurnOutcome<JsonValue>,
|
|
3505
3406
|
kind: TurnRecord["kind"],
|
|
@@ -3513,6 +3414,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3513
3414
|
this.#last = outcome.kind;
|
|
3514
3415
|
const reason = "reason" in outcome ? outcome.reason : undefined;
|
|
3515
3416
|
this.options.progress.turnSettled(this.key, outcome.kind, reason);
|
|
3417
|
+
this.options.sessionsKnown(outcome.usage.sessions, outcome.usage.operationId);
|
|
3516
3418
|
this.options.recordTurn(outcome, kind);
|
|
3517
3419
|
}
|
|
3518
3420
|
|
|
@@ -3540,11 +3442,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3540
3442
|
// The harness bounds its fork by this deadline and stops it there, so the session is idle again
|
|
3541
3443
|
// when the fork rejects.
|
|
3542
3444
|
const take: TakeFork = (deadline, into) =>
|
|
3543
|
-
this
|
|
3544
|
-
if (this.#closed || this.options.isRunClosing()) {
|
|
3545
|
-
throw new Error("logical agent is closed");
|
|
3546
|
-
}
|
|
3547
|
-
scope?.assertActive();
|
|
3445
|
+
this.#queueActive(scope, async () => {
|
|
3548
3446
|
// The fork was opened at the settings it would copy; a `set` before it that did not take
|
|
3549
3447
|
// would leave it recording settings it never ran at.
|
|
3550
3448
|
if (!isDeepStrictEqual(sessionSettings(this.#execution), sessionSettings(settings))) {
|
|
@@ -3564,17 +3462,10 @@ class LogicalAgent implements AgentRef {
|
|
|
3564
3462
|
|
|
3565
3463
|
/** Its `timeout`, never later than the scope's or the run's; else the earlier of those. */
|
|
3566
3464
|
private operationDeadline(
|
|
3567
|
-
spec:
|
|
3465
|
+
spec: { timeout?: Timeout },
|
|
3568
3466
|
scope: ExecutionScope | undefined,
|
|
3569
|
-
call: string,
|
|
3570
3467
|
): AbsoluteDeadline | undefined {
|
|
3571
|
-
|
|
3572
|
-
return boundedDeadline(
|
|
3573
|
-
{ timeout, deadline, timeoutMs },
|
|
3574
|
-
this.deadlineCeiling(scope),
|
|
3575
|
-
this.options.deprecated,
|
|
3576
|
-
call,
|
|
3577
|
-
);
|
|
3468
|
+
return boundedDeadline(spec.timeout, this.deadlineCeiling(scope));
|
|
3578
3469
|
}
|
|
3579
3470
|
|
|
3580
3471
|
/** The deadlines around an operation: its scope's and the run's, if either has one. */
|
|
@@ -3595,9 +3486,19 @@ class LogicalAgent implements AgentRef {
|
|
|
3595
3486
|
`agent ${this.key} is the calling session, whose model and effort are the operator's`,
|
|
3596
3487
|
);
|
|
3597
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
|
+
}
|
|
3598
3497
|
// A workflow is untyped JavaScript at run time: a harness or placement asked for must not be
|
|
3599
3498
|
// dropped in silence.
|
|
3600
|
-
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
|
+
);
|
|
3601
3502
|
if (others.length > 0) {
|
|
3602
3503
|
throw new Error(
|
|
3603
3504
|
`set changes an agent's model and effort, not its ${others.join(", ")}: another harness or placement is another agent`,
|
|
@@ -3732,13 +3633,17 @@ class LogicalAgent implements AgentRef {
|
|
|
3732
3633
|
// Once what it was running has ended, so its last operation's line comes first.
|
|
3733
3634
|
if (first) {
|
|
3734
3635
|
const { key, execution } = this.options;
|
|
3735
|
-
const stoppedLine = () =>
|
|
3636
|
+
const stoppedLine = () => {
|
|
3637
|
+
const placed = this.options.session.pane?.();
|
|
3736
3638
|
this.options.events.event({
|
|
3737
3639
|
kind: "agent-stopped",
|
|
3738
3640
|
agent: key,
|
|
3739
3641
|
callPath: [],
|
|
3740
3642
|
harness: execution.harness,
|
|
3643
|
+
...(placed?.kept ? { kept: true } : {}),
|
|
3644
|
+
...(placed?.notKept === undefined ? {} : { notKept: placed.notKept }),
|
|
3741
3645
|
});
|
|
3646
|
+
};
|
|
3742
3647
|
closed.then(stoppedLine, stoppedLine);
|
|
3743
3648
|
}
|
|
3744
3649
|
return closed;
|
|
@@ -3776,7 +3681,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3776
3681
|
}
|
|
3777
3682
|
|
|
3778
3683
|
private async executeOperation(
|
|
3779
|
-
spec:
|
|
3684
|
+
spec: TurnSpec,
|
|
3780
3685
|
scope: ExecutionScope | undefined,
|
|
3781
3686
|
operationDeadline: AbsoluteDeadline | undefined,
|
|
3782
3687
|
operation: OperationLog,
|
|
@@ -3787,10 +3692,10 @@ class LogicalAgent implements AgentRef {
|
|
|
3787
3692
|
}
|
|
3788
3693
|
|
|
3789
3694
|
private async executeWaitingOperation(
|
|
3790
|
-
spec:
|
|
3695
|
+
spec: TurnSpec,
|
|
3791
3696
|
scope: ExecutionScope | undefined,
|
|
3792
3697
|
initial: AbsoluteDeadline | undefined,
|
|
3793
|
-
{ operationId, tags, delivered, session }: OperationLog,
|
|
3698
|
+
{ operationId, tags, delivered, session, unrecorded }: OperationLog,
|
|
3794
3699
|
nudge: Exclude<AgentRunTextSpec["nudge"], false>,
|
|
3795
3700
|
): Promise<RunResult<JsonValue>> {
|
|
3796
3701
|
const schema = resultSchema(spec.schema);
|
|
@@ -3852,7 +3757,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3852
3757
|
].join("\n")
|
|
3853
3758
|
: "");
|
|
3854
3759
|
const events = createOperationEvents(this.options.runDir, operationId);
|
|
3855
|
-
const messages = this.operationMessages(operationId, `${spec.id
|
|
3760
|
+
const messages = this.operationMessages(operationId, `${spec.id}:message`, {
|
|
3856
3761
|
deadline: () => harnessDeadline(deadline),
|
|
3857
3762
|
authored,
|
|
3858
3763
|
session,
|
|
@@ -3863,6 +3768,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3863
3768
|
event: events.record,
|
|
3864
3769
|
operationId,
|
|
3865
3770
|
agentId: this.key,
|
|
3771
|
+
...(unrecorded === undefined ? {} : { unrecorded }),
|
|
3866
3772
|
question: spec.prompt,
|
|
3867
3773
|
schema,
|
|
3868
3774
|
...(deadline ? { deadline } : {}),
|
|
@@ -3884,7 +3790,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3884
3790
|
this.options.session
|
|
3885
3791
|
.start(
|
|
3886
3792
|
{
|
|
3887
|
-
id: spec.id
|
|
3793
|
+
id: spec.id,
|
|
3888
3794
|
prompt: [
|
|
3889
3795
|
operationPrompt(prompt, schema, this.options.launcher, operationId),
|
|
3890
3796
|
toolsLesson(
|
|
@@ -3904,7 +3810,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3904
3810
|
.then((turn) => this.confirmed(operationId, joined?.numbers ?? [], turn)),
|
|
3905
3811
|
successor: (turn, sequence, deliverySignal) =>
|
|
3906
3812
|
turn.nudge({
|
|
3907
|
-
id: `${spec.id
|
|
3813
|
+
id: `${spec.id}:check-in:${sequence}`,
|
|
3908
3814
|
deadline: harnessDeadline(deadline),
|
|
3909
3815
|
deliverySignal,
|
|
3910
3816
|
prompt: checkInPrompt(recoveryPrompt),
|
|
@@ -3918,24 +3824,25 @@ class LogicalAgent implements AgentRef {
|
|
|
3918
3824
|
return settle(request);
|
|
3919
3825
|
};
|
|
3920
3826
|
this.#operationStops.add(stop);
|
|
3921
|
-
this.#stopsByTurn.set(spec.id
|
|
3827
|
+
this.#stopsByTurn.set(spec.id, stop);
|
|
3922
3828
|
const remove = scope?.add(stop);
|
|
3923
3829
|
return () => {
|
|
3924
3830
|
this.#operationStops.delete(stop);
|
|
3925
|
-
if (this.#stopsByTurn.get(spec.id
|
|
3831
|
+
if (this.#stopsByTurn.get(spec.id) === stop) this.#stopsByTurn.delete(spec.id);
|
|
3926
3832
|
remove?.();
|
|
3927
3833
|
};
|
|
3928
3834
|
},
|
|
3929
3835
|
phase: (phase, reason, until) =>
|
|
3930
3836
|
this.options.progress.turnPhase(this.key, phase, reason, until),
|
|
3931
3837
|
delivered,
|
|
3838
|
+
keepScreen: (turn) => keepScreen(this.options.runDir, operationId, turn),
|
|
3932
3839
|
})
|
|
3933
3840
|
.catch(
|
|
3934
3841
|
(error: unknown): SupervisedOutcome => ({
|
|
3935
3842
|
kind: "failed",
|
|
3936
3843
|
reason: messageOf(error),
|
|
3937
3844
|
cleanupUnresolved: true,
|
|
3938
|
-
|
|
3845
|
+
reportedCosts: [],
|
|
3939
3846
|
settledAt: Date.now(),
|
|
3940
3847
|
}),
|
|
3941
3848
|
)
|
|
@@ -3955,7 +3862,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3955
3862
|
...(outcome.deliveredAt === undefined ? {} : { deliveredAt: outcome.deliveredAt }),
|
|
3956
3863
|
settledAt: outcome.settledAt,
|
|
3957
3864
|
},
|
|
3958
|
-
outcome.
|
|
3865
|
+
outcome.reportedCosts,
|
|
3959
3866
|
);
|
|
3960
3867
|
if (outcome.kind === "answered")
|
|
3961
3868
|
return {
|
|
@@ -4101,7 +4008,10 @@ class LogicalAgent implements AgentRef {
|
|
|
4101
4008
|
stage: ExecutionScope | undefined,
|
|
4102
4009
|
): Promise<void> {
|
|
4103
4010
|
const { messaging } = this.options;
|
|
4104
|
-
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
|
+
});
|
|
4105
4015
|
const ceiling = this.deadlineCeiling(stage)?.unixMilliseconds;
|
|
4106
4016
|
let deadline = Math.min(Date.now() + messaging.limits.operationMs, ceiling ?? Infinity);
|
|
4107
4017
|
messaging.opened({
|
|
@@ -4124,7 +4034,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4124
4034
|
let end: SupervisedOutcome["kind"] | undefined;
|
|
4125
4035
|
try {
|
|
4126
4036
|
const opening = messaging.take(operationId, "opens");
|
|
4127
|
-
// 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.
|
|
4128
4038
|
if (!opening) return;
|
|
4129
4039
|
if (opening.deadline !== undefined) deadline = opening.deadline;
|
|
4130
4040
|
const cause = { message: { from } };
|
|
@@ -4135,6 +4045,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4135
4045
|
const operation = this.operationLog(operationId, "turn", tags);
|
|
4136
4046
|
this.options.progress.turnStarted(this.key, tags);
|
|
4137
4047
|
try {
|
|
4048
|
+
if (lost !== undefined) throw new Error(lost);
|
|
4138
4049
|
const outcome = await this.superviseMessage(operationId, deadline, opening, {
|
|
4139
4050
|
cause,
|
|
4140
4051
|
tags,
|
|
@@ -4249,6 +4160,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4249
4160
|
phase: (phase, reason, until) =>
|
|
4250
4161
|
this.options.progress.turnPhase(this.key, phase, reason, until),
|
|
4251
4162
|
delivered: operation.delivered,
|
|
4163
|
+
keepScreen: (turn) => keepScreen(this.options.runDir, operationId, turn),
|
|
4252
4164
|
...(this.options.livenessPolicy ? { policy: this.options.livenessPolicy } : {}),
|
|
4253
4165
|
});
|
|
4254
4166
|
} finally {
|
|
@@ -4265,7 +4177,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4265
4177
|
...(outcome.deliveredAt === undefined ? {} : { deliveredAt: outcome.deliveredAt }),
|
|
4266
4178
|
settledAt: outcome.settledAt,
|
|
4267
4179
|
},
|
|
4268
|
-
outcome.
|
|
4180
|
+
outcome.reportedCosts,
|
|
4269
4181
|
);
|
|
4270
4182
|
// Recorded `answered` with no value: it did what it was woken for, and takes no result.
|
|
4271
4183
|
this.turnEnded(
|
|
@@ -4296,7 +4208,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4296
4208
|
const times: { deliveredAt?: number } = {};
|
|
4297
4209
|
const settle = (
|
|
4298
4210
|
native: HarnessTurnOutcome | "expired",
|
|
4299
|
-
/** 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. */
|
|
4300
4212
|
later?: Promise<HarnessTurnOutcome>,
|
|
4301
4213
|
): TurnOutcome<string> => {
|
|
4302
4214
|
const usage = entry.settle(
|
|
@@ -4304,7 +4216,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4304
4216
|
...times,
|
|
4305
4217
|
settledAt: Math.min(Date.now(), deadline.unixMilliseconds),
|
|
4306
4218
|
},
|
|
4307
|
-
native === "expired" ? [] : native.
|
|
4219
|
+
native === "expired" ? [] : native.reportedCostsUsd,
|
|
4308
4220
|
later,
|
|
4309
4221
|
);
|
|
4310
4222
|
if (native === "expired") {
|
|
@@ -4352,7 +4264,7 @@ class LogicalAgent implements AgentRef {
|
|
|
4352
4264
|
state: "failed",
|
|
4353
4265
|
detail: messageOf(error),
|
|
4354
4266
|
resultEvidence: { kind: "unavailable" },
|
|
4355
|
-
|
|
4267
|
+
reportedCostsUsd: [],
|
|
4356
4268
|
});
|
|
4357
4269
|
}
|
|
4358
4270
|
times.deliveredAt = Date.now();
|
|
@@ -4366,8 +4278,14 @@ class LogicalAgent implements AgentRef {
|
|
|
4366
4278
|
cancelTimer = scheduleAt(deadline, () => resolve("expired"));
|
|
4367
4279
|
}),
|
|
4368
4280
|
]).finally(() => cancelTimer?.());
|
|
4369
|
-
if (native === "expired")
|
|
4370
|
-
|
|
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;
|
|
4371
4289
|
} finally {
|
|
4372
4290
|
removeCanceller?.();
|
|
4373
4291
|
}
|
|
@@ -4464,6 +4382,10 @@ function sourceTake(what: string, branch: string, source: unknown): WaitTake {
|
|
|
4464
4382
|
throw new Error(
|
|
4465
4383
|
`${what}'s ${branch} is wf.at(), a select's: a race answers once, so it takes a timer, wf.timer(name, date)`,
|
|
4466
4384
|
);
|
|
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
|
+
);
|
|
4467
4389
|
throw new Error(`${what}'s ${branch} is not a source: a timer, a signal, or an agent`);
|
|
4468
4390
|
}
|
|
4469
4391
|
|
|
@@ -4487,32 +4409,27 @@ function signalSource(name: string, declared: ReadonlySet<string>): unknown {
|
|
|
4487
4409
|
throw new Error(
|
|
4488
4410
|
`signal ${name}: an agent's end is waited on by naming the agent, { done: ${awf.key} }, not a signal`,
|
|
4489
4411
|
);
|
|
4490
|
-
//
|
|
4412
|
+
// `in.jsonl` keeps a reply under its record name, from the participant's key.
|
|
4491
4413
|
const take = awf ? { name: "participant:reply", from: awf.key } : { name };
|
|
4492
4414
|
return signalOf({ name }, take);
|
|
4493
4415
|
}
|
|
4494
4416
|
|
|
4495
4417
|
/** Timer `name`'s source. */
|
|
4496
|
-
function timerSource(
|
|
4418
|
+
function timerSource(waits: RunWaits, name: string, due: string): Timer {
|
|
4497
4419
|
return source(
|
|
4498
|
-
{ name, due, cancel: () =>
|
|
4420
|
+
{ name, due, cancel: () => waits.cancelTimer(name) },
|
|
4499
4421
|
{ name: "timer:due", from: name },
|
|
4500
4422
|
) as unknown as Timer;
|
|
4501
4423
|
}
|
|
4502
4424
|
|
|
4503
|
-
/**
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
)
|
|
4510
|
-
|
|
4511
|
-
const is = (event: { name: string; from: string; data?: unknown }) => {
|
|
4512
|
-
deprecated("turn-is");
|
|
4513
|
-
return matches(event, take);
|
|
4514
|
-
};
|
|
4515
|
-
return asSource(Object.freeze({ id, cancel, is }), take) 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;
|
|
4516
4433
|
}
|
|
4517
4434
|
|
|
4518
4435
|
class ExecutionScope {
|
|
@@ -4689,6 +4606,17 @@ async function executeParallel<Item, Result>(
|
|
|
4689
4606
|
}
|
|
4690
4607
|
}
|
|
4691
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
|
+
|
|
4692
4620
|
/**
|
|
4693
4621
|
* Compaction keeps its existing bounded release behavior. Structured operations use the
|
|
4694
4622
|
* supervisor, which requires confirmed native completion before returning an answer.
|
|
@@ -4775,17 +4703,21 @@ type Warn = (message: string, fields?: JsonObject, stage?: string) => void;
|
|
|
4775
4703
|
type OperationLog = {
|
|
4776
4704
|
operationId: string;
|
|
4777
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;
|
|
4778
4708
|
delivered(at: number): void;
|
|
4779
4709
|
/** A session its host named while it ran: on its start if that is still to come. */
|
|
4780
4710
|
session(id: string): void;
|
|
4781
4711
|
failed(error: unknown): void;
|
|
4782
4712
|
};
|
|
4783
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 };
|
|
4784
4716
|
/** A native fork, and the parent's operation it was copied after: null when it ran none. */
|
|
4785
4717
|
type TakenFork = { native: NativeFork; after: string | null };
|
|
4786
4718
|
type TakeFork = (deadline: AbsoluteDeadline | undefined, into?: SessionCopy) => Promise<TakenFork>;
|
|
4787
4719
|
|
|
4788
|
-
/** A settled turn or compaction, kept in the run's `
|
|
4720
|
+
/** A settled turn or compaction, kept in the run's `log.jsonl`. */
|
|
4789
4721
|
type RecordTurn = (outcome: TurnOutcome<JsonValue>, kind: TurnRecord["kind"]) => void;
|
|
4790
4722
|
|
|
4791
4723
|
function pick<T extends object, K extends keyof T>(value: T, keys: readonly K[]): Pick<T, K> {
|
|
@@ -4948,7 +4880,10 @@ function withKnownEffort(execution: AgentExecution): AgentExecution {
|
|
|
4948
4880
|
/** What `set` changes. */
|
|
4949
4881
|
type SettingsChange = Partial<Pick<AgentExecution, "model" | "effort">>;
|
|
4950
4882
|
|
|
4951
|
-
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"]);
|
|
4952
4887
|
|
|
4953
4888
|
function sessionSettings({ model, effort }: AgentExecution): SessionSettings {
|
|
4954
4889
|
return { model, ...(effort === undefined ? {} : { effort }) };
|
|
@@ -4996,22 +4931,17 @@ function forkExecution(parent: AgentExecution, runtime: ForkRuntime = {}): Agent
|
|
|
4996
4931
|
|
|
4997
4932
|
const FORK_RUNTIME_FIELDS = ["placement", "metered", "effort"] as const;
|
|
4998
4933
|
|
|
4999
|
-
/**
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
if (
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
);
|
|
5011
|
-
}
|
|
5012
|
-
deprecated();
|
|
5013
|
-
return { ...rest, runtime: flat };
|
|
5014
|
-
}
|
|
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
|
+
);
|
|
5015
4945
|
if (runtime === undefined) return rest;
|
|
5016
4946
|
if (typeof runtime !== "object" || runtime === null || Array.isArray(runtime)) {
|
|
5017
4947
|
throw new Error(
|
|
@@ -5103,7 +5033,7 @@ function assertCompatibleAgent(
|
|
|
5103
5033
|
if (spec.skills !== undefined && !isDeepStrictEqual(existing.skills, requested.skills)) {
|
|
5104
5034
|
conflict(key, "skills");
|
|
5105
5035
|
}
|
|
5106
|
-
for (const field of ["instructions", "
|
|
5036
|
+
for (const field of ["instructions", "pane"] as const) {
|
|
5107
5037
|
if (spec[field] !== undefined && !isDeepStrictEqual(existing[field], requested[field])) {
|
|
5108
5038
|
conflict(key, field);
|
|
5109
5039
|
}
|
|
@@ -5118,18 +5048,14 @@ function stoppedError(key: string, reason: string): Error {
|
|
|
5118
5048
|
return new Error(`agent ${key} was stopped (${reason}); its key is not opened again`);
|
|
5119
5049
|
}
|
|
5120
5050
|
|
|
5121
|
-
/**
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
const session = turn.sessions.at(-1);
|
|
5130
|
-
if (session) kept.set(turn.agent, { harness: session.harness, sessionRef: session.id });
|
|
5131
|
-
}
|
|
5132
|
-
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
|
+
);
|
|
5133
5059
|
}
|
|
5134
5060
|
|
|
5135
5061
|
/**
|
|
@@ -5140,25 +5066,6 @@ type LogicalAgentTurns = {
|
|
|
5140
5066
|
begin(turn: TurnId, meta: JsonValue, execution: AgentExecution): Promise<void>;
|
|
5141
5067
|
end(turn: TurnId, meta: JsonValue, outcome: TurnOutcome<JsonValue>): Promise<void>;
|
|
5142
5068
|
restored(): RestoredTurn | undefined;
|
|
5143
|
-
/** Throws
|
|
5069
|
+
/** Throws unless the workflow declares this agent's `{key}:ended`. */
|
|
5144
5070
|
checkDeclared(): void;
|
|
5145
5071
|
};
|
|
5146
|
-
|
|
5147
|
-
/**
|
|
5148
|
-
* The run's deadline as `wf.deadline` hands it: a new `Date` each time, so one a workflow changes
|
|
5149
|
-
* isn't the run's, whose deprecated `unixMilliseconds` reports its use.
|
|
5150
|
-
*/
|
|
5151
|
-
function runDeadline(
|
|
5152
|
-
deadline: AbsoluteDeadline | undefined,
|
|
5153
|
-
deprecated: () => void,
|
|
5154
|
-
): WorkflowContext["deadline"] {
|
|
5155
|
-
if (!deadline) return undefined;
|
|
5156
|
-
const date = new Date(deadline.unixMilliseconds);
|
|
5157
|
-
Object.defineProperty(date, "unixMilliseconds", {
|
|
5158
|
-
get() {
|
|
5159
|
-
deprecated();
|
|
5160
|
-
return date.getTime();
|
|
5161
|
-
},
|
|
5162
|
-
});
|
|
5163
|
-
return date as WorkflowContext["deadline"];
|
|
5164
|
-
}
|