@agentswf/engine 0.1.0-beta.3 → 0.1.0-beta.5
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/package.json +5 -6
- package/src/agent-cli/cli.ts +611 -0
- package/src/agent-cli/client.ts +115 -0
- package/src/agent-launcher.ts +15 -25
- package/src/allowance-command.ts +3 -15
- package/src/attempt-ending.ts +2 -4
- package/src/attempt-output.ts +3 -2
- package/src/attempt-view.ts +1 -1
- package/src/control-plane.ts +38 -6
- package/src/coordinator-socket.ts +15 -6
- package/src/deadlines.ts +69 -2
- package/src/decisions/directory.ts +9 -11
- package/src/deprecations.ts +177 -0
- package/src/duration.ts +50 -20
- package/src/emit-command.ts +57 -54
- package/src/emit.ts +124 -15
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +21 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +14 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +58 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/herdr-run-session.ts +2 -2
- package/src/here.ts +1 -1
- package/src/holder-command.ts +38 -51
- package/src/inbox-command.ts +9 -24
- package/src/init-command.ts +2 -11
- package/src/init-template/AGENTS.md.template +3 -3
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +6 -6
- package/src/keep-alive.ts +28 -13
- package/src/logs-command.ts +14 -27
- package/src/mailbox.ts +16 -21
- package/src/messaging/limits.ts +5 -5
- package/src/messaging/messaging.ts +48 -43
- package/src/messaging/routes.ts +2 -2
- package/src/messaging/text.ts +24 -22
- package/src/operation-liveness.ts +1 -1
- package/src/operator-cli.ts +23 -9
- package/src/operator-runtime.ts +9 -12
- package/src/pane-layout.ts +125 -32
- package/src/progress-view.ts +8 -8
- package/src/result-validation.ts +1 -1
- package/src/run-command.ts +27 -47
- package/src/run-commands.ts +1 -1
- package/src/run-prepare.ts +46 -8
- package/src/run-usage.ts +2 -2
- package/src/run-view.ts +9 -4
- package/src/runs-command.ts +14 -23
- package/src/runs.ts +44 -8
- package/src/sandboxes.ts +3 -2
- package/src/simple-input.ts +13 -3
- package/src/sources.ts +42 -6
- package/src/stage-ledger.ts +43 -13
- package/src/stage-plan.ts +37 -1
- package/src/test-command.ts +9 -14
- package/src/testing.ts +8 -9
- package/src/tick-command.ts +2 -13
- package/src/tools.ts +62 -25
- package/src/workflow-loader.ts +5 -2
- package/src/workflow-runner.ts +636 -271
- package/src/workflow-testing/coordinator.typecheck.ts +12 -13
- package/src/workflow-testing/coordinators.ts +2 -2
- package/src/workflow-testing/host.ts +115 -46
- package/src/workflow-testing/index.ts +45 -837
- package/src/workflow-testing/run-test.ts +1213 -0
- package/src/workflow-testing/script.ts +106 -45
- package/src/workflow-testing/workflow-testing.typecheck.ts +13 -10
package/src/workflow-runner.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
AgentPaneRecord,
|
|
14
14
|
AgentSkillsRecord,
|
|
15
15
|
RunAccounting,
|
|
16
|
+
RunPanes,
|
|
16
17
|
SandboxRecord,
|
|
17
18
|
SettledDecision,
|
|
18
19
|
SettledOperation,
|
|
@@ -31,7 +32,8 @@ import {
|
|
|
31
32
|
} from "@agentswf/contract/schema";
|
|
32
33
|
import {
|
|
33
34
|
type AbsoluteDeadline,
|
|
34
|
-
type
|
|
35
|
+
type AgentEnded,
|
|
36
|
+
type AgentEndOf,
|
|
35
37
|
type AgentExecution,
|
|
36
38
|
type AgentForkSpec,
|
|
37
39
|
type AgentKey,
|
|
@@ -40,42 +42,49 @@ import {
|
|
|
40
42
|
type AgentRunStructuredSpec,
|
|
41
43
|
type AgentRunTextSpec,
|
|
42
44
|
type AgentStartSpec,
|
|
45
|
+
awfEventOf,
|
|
43
46
|
type Branches,
|
|
47
|
+
type CallerForkSpec,
|
|
44
48
|
type CodeParticipantRef,
|
|
45
49
|
type CompactionId,
|
|
46
50
|
type CompactSpec,
|
|
51
|
+
DEPRECATED_DEFINITION,
|
|
47
52
|
DeadlineExceededError,
|
|
48
53
|
type Duration,
|
|
54
|
+
durationMilliseconds,
|
|
49
55
|
type EmitOutcome,
|
|
50
|
-
type
|
|
51
|
-
type
|
|
56
|
+
type EventLike,
|
|
57
|
+
type ForkRuntime,
|
|
52
58
|
type HarnessAllowance,
|
|
53
59
|
isEventWorkflow,
|
|
54
60
|
isJsonValue,
|
|
55
61
|
type JsonObject,
|
|
56
62
|
type JsonValue,
|
|
57
63
|
type KeepPane,
|
|
58
|
-
type Matched,
|
|
59
64
|
type MessageAccess,
|
|
60
65
|
type Next,
|
|
61
66
|
nameProblem,
|
|
62
67
|
type OperationRecord,
|
|
63
68
|
type OutputSchema,
|
|
64
69
|
type PaneLayout,
|
|
70
|
+
type PaneSpec,
|
|
71
|
+
type ParticipantDirectory,
|
|
65
72
|
type ParticipantRef,
|
|
66
73
|
type ParticipantSpec,
|
|
67
74
|
type PlacementChoice,
|
|
68
75
|
placementOf,
|
|
76
|
+
positiveDurationMilliseconds,
|
|
69
77
|
type RaceBranches,
|
|
70
|
-
|
|
78
|
+
REPORT_DEPRECATED,
|
|
71
79
|
type RunResult,
|
|
72
80
|
type RuntimeSelection,
|
|
73
|
-
type SelectOptions,
|
|
74
81
|
type SettingsSpec,
|
|
75
82
|
type SkillSource,
|
|
83
|
+
type Source,
|
|
76
84
|
type StageOptions,
|
|
77
85
|
type StageSummary,
|
|
78
86
|
type StartedTurn,
|
|
87
|
+
type Taken,
|
|
79
88
|
type TimeRead,
|
|
80
89
|
type Timer,
|
|
81
90
|
type TurnId,
|
|
@@ -83,8 +92,8 @@ import {
|
|
|
83
92
|
type WaitOptions,
|
|
84
93
|
type When,
|
|
85
94
|
type WorkflowContext,
|
|
86
|
-
type WorkflowDefinition,
|
|
87
95
|
type WorkflowEvent,
|
|
96
|
+
type WorkflowMeta,
|
|
88
97
|
} from "@agentswf/contract/workflow";
|
|
89
98
|
import {
|
|
90
99
|
type AgentSkills,
|
|
@@ -122,8 +131,10 @@ import type { CoordinatorPort } from "./coordinator-port";
|
|
|
122
131
|
import {
|
|
123
132
|
assertDeadline,
|
|
124
133
|
assertDeadlineValue,
|
|
134
|
+
boundedDeadline,
|
|
125
135
|
deadlineWithin,
|
|
126
136
|
earlierDeadline,
|
|
137
|
+
type GivenBound,
|
|
127
138
|
harnessDeadline,
|
|
128
139
|
runUntilStopped,
|
|
129
140
|
scheduleAt,
|
|
@@ -132,6 +143,7 @@ import {
|
|
|
132
143
|
} from "./deadlines";
|
|
133
144
|
import { RunDecisions } from "./decisions/directory";
|
|
134
145
|
import type { DecisionInstallation } from "./decisions/seam";
|
|
146
|
+
import { type Deprecated, RunDeprecations } from "./deprecations";
|
|
135
147
|
import { parseWhen } from "./duration";
|
|
136
148
|
import { emitEvent } from "./emit";
|
|
137
149
|
import { messageOf } from "./errors";
|
|
@@ -165,7 +177,14 @@ import {
|
|
|
165
177
|
type SupervisedOutcome,
|
|
166
178
|
superviseOperation,
|
|
167
179
|
} from "./operation-liveness";
|
|
168
|
-
import {
|
|
180
|
+
import {
|
|
181
|
+
besideOf,
|
|
182
|
+
checkPaneOptions,
|
|
183
|
+
keepsPane,
|
|
184
|
+
type PaneAsked,
|
|
185
|
+
paneAsked,
|
|
186
|
+
storedLayout,
|
|
187
|
+
} from "./pane-layout";
|
|
169
188
|
import { createResultSlotRegistry, type ResultSlotRegistry } from "./result-slots";
|
|
170
189
|
import { RunEventLog } from "./run-event-log";
|
|
171
190
|
import {
|
|
@@ -181,7 +200,7 @@ import {
|
|
|
181
200
|
type OperationTags,
|
|
182
201
|
type RunLedger,
|
|
183
202
|
} from "./run-usage";
|
|
184
|
-
import { endTurnsLine, readStageRecords, readTurns } from "./runs";
|
|
203
|
+
import { endTurnsLine, readAttempts, readStageRecords, readTurns } from "./runs";
|
|
185
204
|
import { type CredentialLocks, placeCarried, seedHome } from "./sandbox-homes";
|
|
186
205
|
import {
|
|
187
206
|
RUN_SANDBOX_ONLY,
|
|
@@ -189,22 +208,39 @@ import {
|
|
|
189
208
|
type RunSandboxOptions,
|
|
190
209
|
type SeatedAgent,
|
|
191
210
|
} from "./sandboxes";
|
|
192
|
-
import { QUOTING_RULE, simpleInput, usageLine } from "./simple-input";
|
|
211
|
+
import { QUOTING_RULE, simpleInput, takesText, usageLine } from "./simple-input";
|
|
193
212
|
import { placeSkills, RunSkills, readSkillSources } from "./skills/run-skills";
|
|
194
|
-
import {
|
|
213
|
+
import {
|
|
214
|
+
asSource,
|
|
215
|
+
branchOf,
|
|
216
|
+
branchValue,
|
|
217
|
+
isSignal,
|
|
218
|
+
matches,
|
|
219
|
+
presented,
|
|
220
|
+
signalOf,
|
|
221
|
+
source,
|
|
222
|
+
takeOf,
|
|
223
|
+
} from "./sources";
|
|
195
224
|
import { StageLedger, type StageProgress } from "./stage-ledger";
|
|
225
|
+
import { lastRedo } from "./stage-plan";
|
|
196
226
|
import { FromStageUnreached } from "./stopped";
|
|
197
227
|
import { inToolHandler, outsideTools, refusedInTool, ToolCalls, toolsLesson } from "./tools";
|
|
198
228
|
import type { EventWorkflow } from "./workflow-loader";
|
|
199
229
|
|
|
200
230
|
export { WorkflowCancelledError } from "./deadlines";
|
|
201
231
|
|
|
232
|
+
/** What the runner needs of a workflow: its meta and its run. */
|
|
233
|
+
export type RunnableWorkflow<Args extends JsonValue, Result extends JsonValue> = {
|
|
234
|
+
readonly meta: WorkflowMeta;
|
|
235
|
+
run(context: WorkflowContext, args: Args): Promise<Result>;
|
|
236
|
+
};
|
|
237
|
+
|
|
202
238
|
export type RunWorkflowOptions = {
|
|
203
239
|
/** Internal timing policy; workflow authors use the operation deadline. */
|
|
204
240
|
livenessPolicy?: LivenessPolicy;
|
|
205
|
-
/** A `keepAlive`
|
|
241
|
+
/** A `keepAlive` that gives no `window`; absent, an hour. A test's is short. */
|
|
206
242
|
defaultWindowMs?: number;
|
|
207
|
-
/** The run's time, which its timers and `
|
|
243
|
+
/** The run's time, which its timers and `wf.now()` read; absent, the clock's. A test's is virtual. */
|
|
208
244
|
now?: () => number;
|
|
209
245
|
runRoot: string;
|
|
210
246
|
/** The run this is an attempt of, whose folder the operator claimed under `runRoot`. */
|
|
@@ -218,6 +254,8 @@ export type RunWorkflowOptions = {
|
|
|
218
254
|
fromStage?: string;
|
|
219
255
|
/** `--values`: the values of stages before `fromStage` that have no record to reuse. */
|
|
220
256
|
values?: ReadonlyMap<string, JsonValue>;
|
|
257
|
+
/** `--workspace` and `--keep-panes`, as the run's record keeps them for every attempt. */
|
|
258
|
+
panes?: RunPanes;
|
|
221
259
|
/** A run of a workflow with `events`. */
|
|
222
260
|
events?: true;
|
|
223
261
|
/** With `events`: runs `work` holding the run's lock, as every append to the mailbox is made. */
|
|
@@ -244,7 +282,7 @@ export type RunWorkflowOptions = {
|
|
|
244
282
|
deadline?: AbsoluteDeadline;
|
|
245
283
|
cwd?: string;
|
|
246
284
|
onLog?: (message: string, fields?: JsonObject) => void;
|
|
247
|
-
/** The operator hands the session back once
|
|
285
|
+
/** The operator hands the session back once close() has released it. */
|
|
248
286
|
onCallerStopped?: (reason: string) => Promise<void>;
|
|
249
287
|
signal?: AbortSignal;
|
|
250
288
|
/** The sandbox providers agents may run in; without them, a sandboxed agent is refused. */
|
|
@@ -253,7 +291,7 @@ export type RunWorkflowOptions = {
|
|
|
253
291
|
skillCache?: string;
|
|
254
292
|
/** The decision models workflows may ask; without them, `decisions.decide` is refused. */
|
|
255
293
|
decisions?: DecisionInstallation;
|
|
256
|
-
/** How `
|
|
294
|
+
/** How `wf.allowance` reads a harness's plan; without it, every harness reads `none`. */
|
|
257
295
|
allowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
258
296
|
/**
|
|
259
297
|
* Where an agent may `--await-reply`: the engine's list by harness and placement unless given. A
|
|
@@ -267,7 +305,7 @@ export type RunWorkflowOptions = {
|
|
|
267
305
|
*/
|
|
268
306
|
coordinators?: readonly CoordinatorPort[];
|
|
269
307
|
/**
|
|
270
|
-
* Sends `
|
|
308
|
+
* Sends `wf.emit`'s event through the one way in; absent, `emitEvent` on this machine. A
|
|
271
309
|
* test records them instead.
|
|
272
310
|
*/
|
|
273
311
|
emit?: Emitter;
|
|
@@ -304,7 +342,7 @@ export type SettledRun = {
|
|
|
304
342
|
sandboxes?: SandboxRecord[];
|
|
305
343
|
/** Each agent's skills; absent when no agent opened. */
|
|
306
344
|
skills?: AgentSkillsRecord[];
|
|
307
|
-
/** Where each
|
|
345
|
+
/** Where each TUI agent's pane went; absent when the run placed none. */
|
|
308
346
|
panes?: AgentPaneRecord[];
|
|
309
347
|
/** Every decision the run asked, in the order asked; absent when it asked none. */
|
|
310
348
|
decisions?: SettledDecision[];
|
|
@@ -392,7 +430,7 @@ type AgentEntry = {
|
|
|
392
430
|
* `undefined` when the channel never opened; the agent state carries the reason.
|
|
393
431
|
*/
|
|
394
432
|
channel: Promise<ResultChannel | undefined>;
|
|
395
|
-
/** Why `
|
|
433
|
+
/** Why `close()` ended it; its key is not opened again. */
|
|
396
434
|
stopped?: string;
|
|
397
435
|
stopping?: Promise<boolean>;
|
|
398
436
|
stopComplete?: boolean;
|
|
@@ -418,10 +456,13 @@ type AgentIdentity = {
|
|
|
418
456
|
instructions?: string;
|
|
419
457
|
labels?: AgentOpenSpec["labels"];
|
|
420
458
|
/** The agent it was forked from, and the spec it was forked with. */
|
|
421
|
-
forkedFrom?: { parent: AgentKey; spec:
|
|
459
|
+
forkedFrom?: { parent: AgentKey; spec: CallerForkSpec };
|
|
422
460
|
/** As written; a reopen compares them so, never where the pane ended up. */
|
|
461
|
+
pane?: PaneSpec;
|
|
423
462
|
layout?: PaneLayout;
|
|
424
463
|
keepPane?: KeepPane;
|
|
464
|
+
/** What its host is asked for its pane: these, with the operator's flags applied. */
|
|
465
|
+
asked?: PaneAsked;
|
|
425
466
|
};
|
|
426
467
|
|
|
427
468
|
const CLEANUP_GRACE_MILLISECONDS = 5_000;
|
|
@@ -434,7 +475,7 @@ const FORK_TIMEOUT_MS = 60_000;
|
|
|
434
475
|
const scopes = new AsyncLocalStorage<ExecutionScope>();
|
|
435
476
|
|
|
436
477
|
export async function runWorkflow<Args extends JsonValue, Result extends JsonValue>(
|
|
437
|
-
definition:
|
|
478
|
+
definition: RunnableWorkflow<Args, Result>,
|
|
438
479
|
args: Args,
|
|
439
480
|
options: RunWorkflowOptions,
|
|
440
481
|
): Promise<WorkflowRunResult<Result>> {
|
|
@@ -447,7 +488,7 @@ export async function runWorkflow<Args extends JsonValue, Result extends JsonVal
|
|
|
447
488
|
* handle's `result`'s.
|
|
448
489
|
*/
|
|
449
490
|
export async function startWorkflow<Args extends JsonValue, Result extends JsonValue>(
|
|
450
|
-
definition:
|
|
491
|
+
definition: RunnableWorkflow<Args, Result>,
|
|
451
492
|
args: Args,
|
|
452
493
|
options: RunWorkflowOptions,
|
|
453
494
|
): Promise<WorkflowRunHandle<Result>> {
|
|
@@ -456,14 +497,20 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
456
497
|
// Before anything opens, so a refusal leaves nothing to close.
|
|
457
498
|
const withEvents = isEventWorkflow(definition);
|
|
458
499
|
const locked = options.run.locked ?? (<T>(work: () => Promise<T>) => work());
|
|
459
|
-
if (withEvents &&
|
|
460
|
-
throw new Error(
|
|
500
|
+
if (withEvents && options.run.values)
|
|
501
|
+
throw new Error(
|
|
502
|
+
"a run of a workflow with events takes no --values: before --from-stage, a stage with no record runs",
|
|
503
|
+
);
|
|
461
504
|
const { dir: runDir, id: runId, attempt } = options.run;
|
|
462
505
|
const startedAt = Date.now();
|
|
463
506
|
const cwd = options.cwd ?? process.cwd();
|
|
464
507
|
// Before anything opens: a record this awf can't read refuses the attempt, and a crash may have
|
|
465
508
|
// torn the last line an earlier attempt appended.
|
|
466
509
|
const recorded = await readStageRecords(runDir);
|
|
510
|
+
// The last --from-stage an earlier attempt redid, whose stale records still rerun as reached.
|
|
511
|
+
const redo = withEvents
|
|
512
|
+
? lastRedo(await readAttempts({ dir: runDir }), recorded, attempt)
|
|
513
|
+
: undefined;
|
|
467
514
|
// The log is observation: one that cannot be written says so as it is, and fails nothing.
|
|
468
515
|
await endTurnsLine(runDir).catch(() => undefined);
|
|
469
516
|
const slots = createResultSlotRegistry({
|
|
@@ -512,6 +559,10 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
512
559
|
});
|
|
513
560
|
};
|
|
514
561
|
const warn = say("warning");
|
|
562
|
+
const deprecations = new RunDeprecations((text) => warn(text));
|
|
563
|
+
if (definition.meta.whenToUse !== undefined) deprecations.used("whenToUse");
|
|
564
|
+
if ((definition as { [DEPRECATED_DEFINITION]?: unknown })[DEPRECATED_DEFINITION])
|
|
565
|
+
deprecations.used("defineExecutableWorkflow");
|
|
515
566
|
// Each turn as it settles, so an attempt that dies keeps the sessions and times of its turns.
|
|
516
567
|
const turns: TurnRecord[] = [];
|
|
517
568
|
const ledger = createRunLedger({ accounting: options.runtime.host.accounting, startedAt });
|
|
@@ -535,6 +586,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
535
586
|
...(options.run.values === undefined ? {} : { values: options.run.values }),
|
|
536
587
|
...(definition.meta.version === undefined ? {} : { workflowVersion: definition.meta.version }),
|
|
537
588
|
...(withEvents ? { byContent: true } : {}),
|
|
589
|
+
...(redo === undefined ? {} : { redo }),
|
|
538
590
|
turns: () => turns,
|
|
539
591
|
event: (body, at) => events.event(body, at),
|
|
540
592
|
});
|
|
@@ -586,18 +638,26 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
586
638
|
: undefined;
|
|
587
639
|
// Every run's timers live in a mailbox; a run of a workflow with events takes its events there too.
|
|
588
640
|
// A plain run without `park` waits for its timers in its process.
|
|
641
|
+
const { park } = options.run;
|
|
589
642
|
const waits = new RunMailbox({
|
|
590
643
|
runDir,
|
|
591
644
|
attempt,
|
|
592
645
|
locked,
|
|
593
646
|
now,
|
|
594
|
-
...(
|
|
647
|
+
...(park
|
|
595
648
|
? {
|
|
596
649
|
parking: {
|
|
597
650
|
idle: (stage) => owner.idle(stage),
|
|
598
651
|
timeout: (signal) => owner.timeout(signal),
|
|
599
652
|
liveUntil: () => owner.liveUntil(),
|
|
600
|
-
park:
|
|
653
|
+
park: async (wait) => {
|
|
654
|
+
// No wake carries `--from-stage`: parked short of it, the redo would be lost unsaid.
|
|
655
|
+
const unreached = stages.fromStageUnreached;
|
|
656
|
+
if (unreached === undefined) return park(wait);
|
|
657
|
+
const stop = new FromStageUnreached(unreached, stages.recorded);
|
|
658
|
+
fatal.reject(stop);
|
|
659
|
+
throw stop;
|
|
660
|
+
},
|
|
601
661
|
...(options.run.live ? { live: options.run.live } : {}),
|
|
602
662
|
...(options.run.skip ? { skip: options.run.skip } : {}),
|
|
603
663
|
},
|
|
@@ -636,6 +696,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
636
696
|
ledger,
|
|
637
697
|
onLog: warn,
|
|
638
698
|
log: say("log"),
|
|
699
|
+
deprecations,
|
|
639
700
|
events,
|
|
640
701
|
onCallerStopped: options.onCallerStopped,
|
|
641
702
|
livenessPolicy: options.livenessPolicy,
|
|
@@ -644,6 +705,8 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
644
705
|
coordinators: options.coordinators ?? [],
|
|
645
706
|
emit: options.emit ?? machineEmitter,
|
|
646
707
|
workflow: definition.meta.name,
|
|
708
|
+
declared: new Set(withEvents ? Object.keys(definition.events) : []),
|
|
709
|
+
...(options.run.panes ? { panes: options.run.panes } : {}),
|
|
647
710
|
});
|
|
648
711
|
const stopped = new AbortController();
|
|
649
712
|
// Aborted by a stop that comes after the body ended; the one that ended it does not cut short the
|
|
@@ -699,6 +762,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
699
762
|
failure = ending;
|
|
700
763
|
}
|
|
701
764
|
bodyEnded = true;
|
|
765
|
+
deprecations.settle();
|
|
702
766
|
// What the body left running enters no stage; one left open is failed once its turns settle.
|
|
703
767
|
stages.seal(failed ? messageOf(failure) : "the workflow returned before the stage ended");
|
|
704
768
|
const cutReading = () => reading.abort();
|
|
@@ -891,7 +955,7 @@ class WorkflowOwner {
|
|
|
891
955
|
readonly #inFlight = new Set<Promise<unknown>>();
|
|
892
956
|
#closed = false;
|
|
893
957
|
#closing: Promise<unknown[]> | undefined;
|
|
894
|
-
/** The bundled `
|
|
958
|
+
/** The bundled `wfa`'s source, built once, at the first sandboxed agent. */
|
|
895
959
|
#bundle: Promise<string> | undefined;
|
|
896
960
|
/** The calling session, once the workflow has asked for it (ADR 0010). */
|
|
897
961
|
#caller: { key: string; state: Promise<LogicalAgent> } | undefined;
|
|
@@ -928,6 +992,7 @@ class WorkflowOwner {
|
|
|
928
992
|
onLog?: Warn;
|
|
929
993
|
/** The workflow's own log lines. */
|
|
930
994
|
log(message: string, fields?: JsonObject): void;
|
|
995
|
+
deprecations: RunDeprecations;
|
|
931
996
|
events: RunEventLog;
|
|
932
997
|
onCallerStopped?: RunWorkflowOptions["onCallerStopped"];
|
|
933
998
|
awaitReply: NonNullable<RunWorkflowOptions["awaitReply"]>;
|
|
@@ -945,6 +1010,10 @@ class WorkflowOwner {
|
|
|
945
1010
|
emit: Emitter;
|
|
946
1011
|
/** The workflow's name, which with the run's id is the ref its events are `from`. */
|
|
947
1012
|
workflow: string;
|
|
1013
|
+
/** The names its `events` declares, which `signal` takes. */
|
|
1014
|
+
declared: ReadonlySet<string>;
|
|
1015
|
+
/** The operator's choices for every TUI agent's pane on the host. */
|
|
1016
|
+
panes?: RunPanes;
|
|
948
1017
|
},
|
|
949
1018
|
) {
|
|
950
1019
|
this.#restored = new Map(options.restoredTurns ?? []);
|
|
@@ -957,7 +1026,7 @@ class WorkflowOwner {
|
|
|
957
1026
|
if (!agent || agent.hasStartedTurn) return false;
|
|
958
1027
|
return options.awaitReply(agent.execution, agent.confirmsDelivery);
|
|
959
1028
|
},
|
|
960
|
-
launcher: (key) => this.#logical.get(key)?.launcher ?? "
|
|
1029
|
+
launcher: (key) => this.#logical.get(key)?.launcher ?? "wfa",
|
|
961
1030
|
changed: (key) => {
|
|
962
1031
|
this.#messagesChanged();
|
|
963
1032
|
this.#logical.get(key)?.messagesChanged();
|
|
@@ -992,6 +1061,7 @@ class WorkflowOwner {
|
|
|
992
1061
|
this.#tools = new ToolCalls<ExecutionScope>({
|
|
993
1062
|
record: (event) => options.events.event(event),
|
|
994
1063
|
runIn: (scope, fn) => (scope ? scopes.run(scope, fn) : scopes.exit(fn)),
|
|
1064
|
+
deprecated: (name, call) => options.deprecations.used(name, call),
|
|
995
1065
|
});
|
|
996
1066
|
options.control.attachTools(this.#tools);
|
|
997
1067
|
const holder = {
|
|
@@ -1004,7 +1074,9 @@ class WorkflowOwner {
|
|
|
1004
1074
|
runId: options.runId,
|
|
1005
1075
|
attempt: options.attempt,
|
|
1006
1076
|
cwd: options.cwd,
|
|
1007
|
-
deadline
|
|
1077
|
+
get deadline() {
|
|
1078
|
+
return runDeadline(options.deadline, () => options.deprecations.used("run-deadline-ms"));
|
|
1079
|
+
},
|
|
1008
1080
|
agents: {
|
|
1009
1081
|
open: (spec) => {
|
|
1010
1082
|
try {
|
|
@@ -1013,20 +1085,10 @@ class WorkflowOwner {
|
|
|
1013
1085
|
return Promise.reject(error);
|
|
1014
1086
|
}
|
|
1015
1087
|
},
|
|
1016
|
-
attach: (key, runtime) => {
|
|
1017
|
-
if (this.#caller?.key !== key) return unavailable("agents.attach");
|
|
1018
|
-
const stopped = this.#agents.get(key)?.stopped;
|
|
1019
|
-
if (stopped !== undefined) return Promise.reject(stoppedError(key, stopped));
|
|
1020
|
-
if (runtime !== undefined) {
|
|
1021
|
-
return Promise.reject(
|
|
1022
|
-
new Error(`agent ${key} is the calling session, whose runtime is the operator's`),
|
|
1023
|
-
);
|
|
1024
|
-
}
|
|
1025
|
-
return this.#caller.state;
|
|
1026
|
-
},
|
|
1027
1088
|
stop: (key, reason) => {
|
|
1089
|
+
options.deprecations.used("agents-stop");
|
|
1028
1090
|
try {
|
|
1029
|
-
return this.stopAgent(key, reason);
|
|
1091
|
+
return this.stopAgent(key, reason, "agents.stop");
|
|
1030
1092
|
} catch (error) {
|
|
1031
1093
|
return Promise.reject(error);
|
|
1032
1094
|
}
|
|
@@ -1074,6 +1136,7 @@ class WorkflowOwner {
|
|
|
1074
1136
|
const decided = this.track(
|
|
1075
1137
|
options.decisions.decide(spec, {
|
|
1076
1138
|
deadline: scope?.deadline ?? options.deadline,
|
|
1139
|
+
deprecated: (name, call) => options.deprecations.used(name, call),
|
|
1077
1140
|
...(scope ? { add: (cancel) => scope.add((stop) => cancel(stop.reason)) } : {}),
|
|
1078
1141
|
...(scope?.stage === undefined ? {} : { stage: scope.stage }),
|
|
1079
1142
|
}),
|
|
@@ -1083,17 +1146,10 @@ class WorkflowOwner {
|
|
|
1083
1146
|
},
|
|
1084
1147
|
},
|
|
1085
1148
|
participants: {
|
|
1086
|
-
connect: async (spec) =>
|
|
1087
|
-
|
|
1088
|
-
(this.#messaging.connected().includes(key) ? this.#outsideRefs.get(key) : undefined) ??
|
|
1089
|
-
(this.#logical.get(key) as AgentRef | undefined) ??
|
|
1090
|
-
null,
|
|
1149
|
+
connect: (async (spec: ParticipantSpec) =>
|
|
1150
|
+
this.connectParticipant(spec)) as ParticipantDirectory["connect"],
|
|
1091
1151
|
},
|
|
1092
1152
|
messages: { allow: async (access) => this.allowRoute(access) },
|
|
1093
|
-
steps: {
|
|
1094
|
-
run: () => unavailable("steps.run"),
|
|
1095
|
-
sleep: () => unavailable("steps.sleep"),
|
|
1096
|
-
},
|
|
1097
1153
|
emit: async (target, run, name, data, emitOptions) => {
|
|
1098
1154
|
if (this.#closed || this.#cleanupFailures.length)
|
|
1099
1155
|
throw new Error("workflow context is closed");
|
|
@@ -1115,9 +1171,17 @@ class WorkflowOwner {
|
|
|
1115
1171
|
},
|
|
1116
1172
|
parallel: (items, operation, parallelOptions) => {
|
|
1117
1173
|
const inherited = scopes.getStore()?.deadline ?? options.deadline;
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1174
|
+
let deadline: AbsoluteDeadline | undefined;
|
|
1175
|
+
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
|
+
);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
return Promise.reject(error);
|
|
1184
|
+
}
|
|
1121
1185
|
const label = parallelOptions?.label;
|
|
1122
1186
|
return runParallel(items, operation, deadline, parallelOptions?.concurrency, () =>
|
|
1123
1187
|
label === undefined ? undefined : options.progress.group(label, items.length),
|
|
@@ -1129,7 +1193,6 @@ class WorkflowOwner {
|
|
|
1129
1193
|
},
|
|
1130
1194
|
stage: ((name: string, ...rest: unknown[]) =>
|
|
1131
1195
|
this.runStage(name, rest)) as WorkflowContext["stage"],
|
|
1132
|
-
call: () => unavailable("call"),
|
|
1133
1196
|
usage: () => options.ledger.records(),
|
|
1134
1197
|
allowance: async (harness) => {
|
|
1135
1198
|
if (this.#closed || this.#cleanupFailures.length)
|
|
@@ -1142,7 +1205,7 @@ class WorkflowOwner {
|
|
|
1142
1205
|
},
|
|
1143
1206
|
log: (message, fields) => options.log(message, fields),
|
|
1144
1207
|
now: () => {
|
|
1145
|
-
this.#notReading("
|
|
1208
|
+
this.#notReading("wf.now()");
|
|
1146
1209
|
return new Date(options.now());
|
|
1147
1210
|
},
|
|
1148
1211
|
sleep: async (name, when) => {
|
|
@@ -1153,33 +1216,29 @@ class WorkflowOwner {
|
|
|
1153
1216
|
undefined,
|
|
1154
1217
|
);
|
|
1155
1218
|
},
|
|
1219
|
+
// Every run waits on its own mailbox; one without events never parks, so it waits here.
|
|
1220
|
+
receive: (spec?: WaitOptions) => this.receive(options.waits, spec),
|
|
1221
|
+
race: ((name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
|
|
1222
|
+
this.race(options.waits, name, branches, raceOptions)) as WorkflowContext["race"],
|
|
1223
|
+
select: ((branches: Branches, selectOptions?: WaitOptions) =>
|
|
1224
|
+
this.select(options.waits, branches, selectOptions)) as WorkflowContext["select"],
|
|
1225
|
+
timer: (name: string, when: When) => this.timer(options.waits, name, when),
|
|
1226
|
+
at: (read: () => Date | null) => timeRead(read),
|
|
1227
|
+
after: (duration: Duration) => {
|
|
1228
|
+
this.#notReading("wf.after()");
|
|
1229
|
+
let milliseconds: number;
|
|
1230
|
+
try {
|
|
1231
|
+
milliseconds = durationMilliseconds(duration);
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
throw new Error(`wf.after takes a duration: ${messageOf(error)}`);
|
|
1234
|
+
}
|
|
1235
|
+
return new Date(options.now() + milliseconds);
|
|
1236
|
+
},
|
|
1237
|
+
signal: ((name: string) => signalSource(name, options.declared)) as WorkflowContext["signal"],
|
|
1238
|
+
...(options.mailbox?.started
|
|
1239
|
+
? { started: options.mailbox.started as WorkflowContext["started"] }
|
|
1240
|
+
: {}),
|
|
1156
1241
|
};
|
|
1157
|
-
const { mailbox } = options;
|
|
1158
|
-
Object.assign(
|
|
1159
|
-
this.context,
|
|
1160
|
-
mailbox
|
|
1161
|
-
? {
|
|
1162
|
-
receive: (spec?: ReceiveOptions) => this.receive(mailbox, spec),
|
|
1163
|
-
race: (name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
|
|
1164
|
-
this.race(mailbox, name, branches, raceOptions),
|
|
1165
|
-
select: (branches: Branches, selectOptions?: SelectOptions) =>
|
|
1166
|
-
this.select(mailbox, branches, selectOptions),
|
|
1167
|
-
timer: (name: string, when: When) => this.timer(mailbox, name, when),
|
|
1168
|
-
timers: () => source({}, { name: "timer:due" }),
|
|
1169
|
-
at: (read: () => Date | null) => timeRead(read),
|
|
1170
|
-
after: (duration: Duration) => {
|
|
1171
|
-
this.#notReading("workflow.after()");
|
|
1172
|
-
if (typeof duration !== "string" || !/^\d+(?:\.\d+)?[dhm]$/.test(duration))
|
|
1173
|
-
throw new Error(
|
|
1174
|
-
`workflow.after takes days, hours or minutes, "1d", "3h", "1.5h"; not ${String(duration)}`,
|
|
1175
|
-
);
|
|
1176
|
-
return new Date(parseWhen(duration, options.now()).due);
|
|
1177
|
-
},
|
|
1178
|
-
signal: (name: string) => signalSource(name),
|
|
1179
|
-
...(mailbox.started ? { started: mailbox.started } : {}),
|
|
1180
|
-
}
|
|
1181
|
-
: { receive: () => Promise.reject(new Error("receive needs a workflow with events")) },
|
|
1182
|
-
);
|
|
1183
1242
|
}
|
|
1184
1243
|
|
|
1185
1244
|
/** Set while a select reads a time from state. */
|
|
@@ -1189,7 +1248,7 @@ class WorkflowOwner {
|
|
|
1189
1248
|
#notReading(what: string): void {
|
|
1190
1249
|
if (this.#reading)
|
|
1191
1250
|
throw new Error(
|
|
1192
|
-
`${what} in
|
|
1251
|
+
`${what} in wf.at's read: a time made at the read moves with every wait and never comes due; keep the time in the run's state where it is set`,
|
|
1193
1252
|
);
|
|
1194
1253
|
}
|
|
1195
1254
|
|
|
@@ -1205,7 +1264,7 @@ class WorkflowOwner {
|
|
|
1205
1264
|
if (value === null) return undefined;
|
|
1206
1265
|
if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
|
|
1207
1266
|
throw new Error(
|
|
1208
|
-
`select's ${branch}:
|
|
1267
|
+
`select's ${branch}: wf.at reads a valid Date or null from the run's state, not ${
|
|
1209
1268
|
value instanceof Date ? "an invalid Date" : (JSON.stringify(value) ?? String(value))
|
|
1210
1269
|
}`,
|
|
1211
1270
|
);
|
|
@@ -1239,11 +1298,13 @@ class WorkflowOwner {
|
|
|
1239
1298
|
// A turn's end taken: its agent takes other work again.
|
|
1240
1299
|
for (const event of taken) {
|
|
1241
1300
|
if (event.name !== "agent:ended") continue;
|
|
1301
|
+
this.#reporting((event.data as { outcome?: unknown }).outcome);
|
|
1242
1302
|
const { turn } = event.data as unknown as TurnEnd;
|
|
1243
1303
|
if (this.#restored.get(event.from)?.turn === turn) this.#restored.delete(event.from);
|
|
1244
1304
|
this.#logical.get(event.from)?.endTaken(turn);
|
|
1245
1305
|
}
|
|
1246
|
-
|
|
1306
|
+
// The mailbox keeps awf's record names; the code is handed the names it declares.
|
|
1307
|
+
return taken.map((event) => presented(event, this.options.declared));
|
|
1247
1308
|
} finally {
|
|
1248
1309
|
removeFromScope?.();
|
|
1249
1310
|
this.#waiting = false;
|
|
@@ -1291,8 +1352,7 @@ class WorkflowOwner {
|
|
|
1291
1352
|
throw new Error(
|
|
1292
1353
|
`race ${name} has answered in this attempt; a wait in a loop is named per round, as approval-\${round}`,
|
|
1293
1354
|
);
|
|
1294
|
-
|
|
1295
|
-
const label = checkedLabel(raceOptions?.label);
|
|
1355
|
+
const label = labelOf(raceOptions?.label);
|
|
1296
1356
|
const given = branchesOf(`race ${name}`, branches, true);
|
|
1297
1357
|
// A sleep's time may be a Date, its own timer as a duration is.
|
|
1298
1358
|
const own = (source: unknown) => typeof source === "string" || source instanceof Date;
|
|
@@ -1302,6 +1362,9 @@ class WorkflowOwner {
|
|
|
1302
1362
|
own(source) ? [] : [[branch, sourceTake(`race ${name}`, branch, source)] as const],
|
|
1303
1363
|
),
|
|
1304
1364
|
);
|
|
1365
|
+
this.#checkSources(`race ${name}`, takes.entries());
|
|
1366
|
+
// Only once its label and branches are checked: a refused race leaves its name to a retry.
|
|
1367
|
+
this.#raced.add(name);
|
|
1305
1368
|
const owned = new Map<string, string>();
|
|
1306
1369
|
const pairs: (readonly [string, WaitTake])[] = given.map(([branch, source]) => {
|
|
1307
1370
|
if (!own(source)) return [branch, takes.get(branch) as WaitTake] as const;
|
|
@@ -1309,29 +1372,33 @@ class WorkflowOwner {
|
|
|
1309
1372
|
owned.set(branch, timer.name);
|
|
1310
1373
|
return [branch, { name: "timer:due", from: timer.name }] as const;
|
|
1311
1374
|
});
|
|
1312
|
-
let
|
|
1375
|
+
let branch: string | undefined;
|
|
1376
|
+
let event: WorkflowEvent<string, JsonValue>;
|
|
1313
1377
|
try {
|
|
1314
|
-
await this.#holdingWait(scope, async (signal) => {
|
|
1315
|
-
won = await mailbox.race(name, pairs, owned, label, signal);
|
|
1378
|
+
[event] = (await this.#holdingWait(scope, async (signal) => {
|
|
1379
|
+
const won = await mailbox.race(name, pairs, owned, label, signal);
|
|
1380
|
+
branch = won.branch;
|
|
1316
1381
|
return [won.event];
|
|
1317
|
-
});
|
|
1382
|
+
})) as [WorkflowEvent<string, JsonValue>];
|
|
1318
1383
|
} catch (error) {
|
|
1319
|
-
// Cut short, not parked: its own timers would fire into a later wait. A park's rerun races
|
|
1320
|
-
|
|
1384
|
+
// Cut short, not parked: its own timers would fire into a later wait. A park's rerun races
|
|
1385
|
+
// again, and so does a continue's after a park refused short of its --from-stage.
|
|
1386
|
+
if (!(error instanceof RunParked || error instanceof FromStageUnreached))
|
|
1321
1387
|
await Promise.all([...owned.values()].map((timer) => mailbox.cancelTimer(timer))).catch(
|
|
1322
1388
|
() => undefined,
|
|
1323
1389
|
);
|
|
1324
1390
|
throw error;
|
|
1325
1391
|
}
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1392
|
+
const key = branch as string;
|
|
1393
|
+
const whole = isSignal(given.find(([each]) => each === key)?.[1]);
|
|
1394
|
+
return { key, [key]: branchValue(event, whole) } as Next<RaceBranches>;
|
|
1328
1395
|
}
|
|
1329
1396
|
|
|
1330
1397
|
/** A loop over the events `branches` take, one at a time. */
|
|
1331
1398
|
private select(
|
|
1332
1399
|
mailbox: RunMailbox,
|
|
1333
1400
|
branches: Branches,
|
|
1334
|
-
selectOptions:
|
|
1401
|
+
selectOptions: WaitOptions | undefined,
|
|
1335
1402
|
): AsyncIterable<Next<Branches>> {
|
|
1336
1403
|
const given = branchesOf("select", branches, false);
|
|
1337
1404
|
const reads = new Map(
|
|
@@ -1347,6 +1414,7 @@ class WorkflowOwner {
|
|
|
1347
1414
|
reads.has(branch) ? { name: AT_DUE, from: branch } : sourceTake("select", branch, source),
|
|
1348
1415
|
] as const,
|
|
1349
1416
|
);
|
|
1417
|
+
this.#checkSources("select", pairs);
|
|
1350
1418
|
// Read at each pass of the wait: a time follows the state as it moves.
|
|
1351
1419
|
const takes = (): WaitTake[] =>
|
|
1352
1420
|
pairs.map(([branch, take]) => {
|
|
@@ -1354,20 +1422,17 @@ class WorkflowOwner {
|
|
|
1354
1422
|
const due = read && this.#readTime(branch, read);
|
|
1355
1423
|
return due === undefined ? take : { ...take, due };
|
|
1356
1424
|
});
|
|
1357
|
-
const labelOf = () => {
|
|
1358
|
-
const given = selectOptions?.label;
|
|
1359
|
-
return checkedLabel(typeof given === "function" ? given() : given);
|
|
1360
|
-
};
|
|
1361
1425
|
const next = async (): Promise<Next<Branches>> => {
|
|
1362
1426
|
const scope = this.#beginWait("select");
|
|
1363
1427
|
if (scope?.stage !== undefined)
|
|
1364
1428
|
throw new Error(`select is in stage ${scope.stage}: a loop's wait is not a stage's`);
|
|
1365
1429
|
const [event] = await this.#holdingWait(scope, async (signal) => [
|
|
1366
|
-
await mailbox.select(takes, labelOf(), signal),
|
|
1430
|
+
await mailbox.select(takes, labelOf(selectOptions?.label), signal),
|
|
1367
1431
|
]);
|
|
1368
1432
|
const taken = event as WorkflowEvent<string, JsonValue>;
|
|
1369
1433
|
const branch = branchOf(pairs, taken) as string;
|
|
1370
|
-
|
|
1434
|
+
const whole = isSignal(given.find(([each]) => each === branch)?.[1]);
|
|
1435
|
+
return { key: branch, [branch]: branchValue(taken, whole) } as Next<Branches>;
|
|
1371
1436
|
};
|
|
1372
1437
|
return {
|
|
1373
1438
|
[Symbol.asyncIterator]: () => ({
|
|
@@ -1377,6 +1442,36 @@ class WorkflowOwner {
|
|
|
1377
1442
|
};
|
|
1378
1443
|
}
|
|
1379
1444
|
|
|
1445
|
+
/** A started turn among a wait's branches, which naming its agent replaces. */
|
|
1446
|
+
/**
|
|
1447
|
+
* A wait's branches as a run can take them: a started turn among them is deprecated, and a run
|
|
1448
|
+
* without events, which hears no agent's end or reply to the code, refuses a branch waiting on
|
|
1449
|
+
* one, which would never come.
|
|
1450
|
+
*/
|
|
1451
|
+
#checkSources(what: string, takes: Iterable<readonly [string, WaitTake]>): void {
|
|
1452
|
+
const given = [...takes];
|
|
1453
|
+
if (!this.options.mailbox) {
|
|
1454
|
+
const unheard = given.find(([, take]) => HEARD_WITH_EVENTS.has(take.name));
|
|
1455
|
+
if (unheard)
|
|
1456
|
+
throw new Error(
|
|
1457
|
+
`${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
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
if (given.some(([, take]) => take.turn !== undefined))
|
|
1461
|
+
this.options.deprecations.used("turn-source");
|
|
1462
|
+
}
|
|
1463
|
+
|
|
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
|
+
|
|
1380
1475
|
#waiting = false;
|
|
1381
1476
|
/** The races and sleeps this attempt has run, each by its name once. */
|
|
1382
1477
|
readonly #raced = new Set<string>();
|
|
@@ -1388,6 +1483,18 @@ class WorkflowOwner {
|
|
|
1388
1483
|
return {
|
|
1389
1484
|
begin: (turn, meta, execution) => mailbox.beginTurn(key, turn, meta, execution),
|
|
1390
1485
|
end: (turn, meta, outcome) => mailbox.endTurn(key, { turn, meta, outcome }),
|
|
1486
|
+
checkDeclared: () => {
|
|
1487
|
+
const { declared, deprecations } = this.options;
|
|
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"))
|
|
1491
|
+
throw new Error(
|
|
1492
|
+
key.includes(":")
|
|
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`,
|
|
1495
|
+
);
|
|
1496
|
+
deprecations.used("undeclared-ended", key);
|
|
1497
|
+
},
|
|
1391
1498
|
// Read when the agent opens: a wait before then may have taken the end.
|
|
1392
1499
|
restored: () => this.#restored.get(key),
|
|
1393
1500
|
};
|
|
@@ -1442,7 +1549,7 @@ class WorkflowOwner {
|
|
|
1442
1549
|
if (!onTimeout) continue;
|
|
1443
1550
|
try {
|
|
1444
1551
|
await runUntilStopped(
|
|
1445
|
-
async () => asTimeoutHandler(() => onTimeout(agent
|
|
1552
|
+
async () => asTimeoutHandler(() => onTimeout(agent)),
|
|
1446
1553
|
signal,
|
|
1447
1554
|
undefined,
|
|
1448
1555
|
);
|
|
@@ -1462,14 +1569,16 @@ class WorkflowOwner {
|
|
|
1462
1569
|
/** A wait: one at a time, and in a stage, the stage's batch. */
|
|
1463
1570
|
private async receive(
|
|
1464
1571
|
mailbox: RunMailbox,
|
|
1465
|
-
spec:
|
|
1572
|
+
spec: WaitOptions | undefined,
|
|
1466
1573
|
): Promise<WorkflowEvent<string, JsonValue>[]> {
|
|
1467
1574
|
const scope = this.#beginWait("receive()");
|
|
1575
|
+
const label = receiveLabel(spec);
|
|
1576
|
+
if (scope?.stage !== undefined) this.options.deprecations.used("stage-receive");
|
|
1468
1577
|
if (scope?.stage !== undefined && this.#stageBatches.has(scope.stage))
|
|
1469
1578
|
throw new Error(`stage ${scope.stage} has waited once; a stage holds one wait`);
|
|
1470
1579
|
try {
|
|
1471
1580
|
const taken = await this.#holdingWait(scope, (signal) =>
|
|
1472
|
-
mailbox.receive(
|
|
1581
|
+
mailbox.receive(label, scope?.stage, signal),
|
|
1473
1582
|
);
|
|
1474
1583
|
if (scope?.stage !== undefined) {
|
|
1475
1584
|
this.#stageBatches.set(scope.stage, taken);
|
|
@@ -1515,17 +1624,32 @@ class WorkflowOwner {
|
|
|
1515
1624
|
const to = (spec as { to?: unknown }).to;
|
|
1516
1625
|
if (to !== undefined && to !== "code")
|
|
1517
1626
|
throw new Error(`a participant's messages go to agents, or to: "code"; not ${String(to)}`);
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
)
|
|
1627
|
+
const { declared, deprecations } = this.options;
|
|
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
|
+
}
|
|
1522
1641
|
this.#messaging.connect(
|
|
1523
1642
|
key,
|
|
1524
1643
|
labels === undefined ? undefined : structuredClone(labels),
|
|
1525
|
-
to === "code" ? "code" : undefined,
|
|
1644
|
+
to === "code" || replies ? "code" : undefined,
|
|
1526
1645
|
);
|
|
1527
1646
|
const ref: ParticipantRef = {
|
|
1528
1647
|
...participantRef(key),
|
|
1648
|
+
post: async (text: string) => {
|
|
1649
|
+
if (typeof text !== "string" || text.trim() === "")
|
|
1650
|
+
throw new Error("post takes the text to write");
|
|
1651
|
+
await this.#messaging.post(key, text);
|
|
1652
|
+
},
|
|
1529
1653
|
release: async (reason) => {
|
|
1530
1654
|
if (reason !== undefined && typeof reason !== "string")
|
|
1531
1655
|
throw new Error("a release's reason is a string");
|
|
@@ -1533,16 +1657,16 @@ class WorkflowOwner {
|
|
|
1533
1657
|
},
|
|
1534
1658
|
};
|
|
1535
1659
|
if (to === "code") {
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1660
|
+
const take = { name: "participant:reply", from: key };
|
|
1661
|
+
const routed = asSource(
|
|
1662
|
+
Object.freeze({
|
|
1538
1663
|
...ref,
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
await this.#messaging.post(key, text);
|
|
1664
|
+
is: (event: { name: string; from: string }) => {
|
|
1665
|
+
deprecations.used("participant-is");
|
|
1666
|
+
return matches(event, take);
|
|
1543
1667
|
},
|
|
1544
|
-
},
|
|
1545
|
-
|
|
1668
|
+
}),
|
|
1669
|
+
take,
|
|
1546
1670
|
) as unknown as CodeParticipantRef;
|
|
1547
1671
|
this.#outsideRefs.set(key, routed);
|
|
1548
1672
|
return routed;
|
|
@@ -1553,6 +1677,8 @@ class WorkflowOwner {
|
|
|
1553
1677
|
|
|
1554
1678
|
/** A key an agent may not take: a reserved one, or an outside participant's. */
|
|
1555
1679
|
#assertFreeKey(key: string, who: string): void {
|
|
1680
|
+
if (typeof key === "string" && key.includes(":"))
|
|
1681
|
+
this.options.deprecations.used("colon-key", key);
|
|
1556
1682
|
if ((RESERVED_PARTICIPANT_KEYS as readonly string[]).includes(key.toLowerCase()))
|
|
1557
1683
|
throw new Error(
|
|
1558
1684
|
`${key} is reserved: an agent could read a message from it as authority; ${who} takes another key`,
|
|
@@ -1600,12 +1726,20 @@ class WorkflowOwner {
|
|
|
1600
1726
|
throw new Error("a route between agents names two of them");
|
|
1601
1727
|
ends = { between: [end(shape.between[0]), end(shape.between[1])] };
|
|
1602
1728
|
} 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
|
+
const timeoutMs =
|
|
1732
|
+
access.timeout == null
|
|
1733
|
+
? (access.timeoutMs ?? undefined)
|
|
1734
|
+
: positiveDurationMilliseconds(access.timeout, "a route's timeout");
|
|
1603
1735
|
this.#messaging.grant(ends, {
|
|
1604
1736
|
purpose: access.purpose,
|
|
1605
1737
|
maxMessages: access.maxMessages,
|
|
1606
|
-
timeoutMs
|
|
1738
|
+
timeoutMs,
|
|
1607
1739
|
scope: scope?.messages ?? this.#messaging.attempt,
|
|
1608
1740
|
});
|
|
1741
|
+
if (access.timeout == null && access.timeoutMs != null)
|
|
1742
|
+
this.options.deprecations.used("timeout-ms", "on a route");
|
|
1609
1743
|
}
|
|
1610
1744
|
|
|
1611
1745
|
/** Who owns a message operation a route in `scope` opens: its stage, or the attempt. */
|
|
@@ -1694,7 +1828,7 @@ class WorkflowOwner {
|
|
|
1694
1828
|
: "cancelled",
|
|
1695
1829
|
reason: "workflow closing",
|
|
1696
1830
|
})
|
|
1697
|
-
.then(() => state.
|
|
1831
|
+
.then(() => state.terminate("workflow complete")),
|
|
1698
1832
|
() => undefined,
|
|
1699
1833
|
),
|
|
1700
1834
|
);
|
|
@@ -1780,9 +1914,12 @@ class WorkflowOwner {
|
|
|
1780
1914
|
// A stage around a wait returns the batch it took, which its record doesn't hold.
|
|
1781
1915
|
const batch =
|
|
1782
1916
|
entered.value === undefined && !options
|
|
1783
|
-
? await this.options.mailbox?.batchOf(name)
|
|
1917
|
+
? (await this.options.mailbox?.batchOf(name))?.map((event) =>
|
|
1918
|
+
presented(event, this.options.declared),
|
|
1919
|
+
)
|
|
1784
1920
|
: undefined;
|
|
1785
1921
|
if (batch) {
|
|
1922
|
+
this.options.deprecations.used("stage-receive");
|
|
1786
1923
|
if (this.#waitedStages.has(name))
|
|
1787
1924
|
throw new Error(
|
|
1788
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`,
|
|
@@ -1849,8 +1986,24 @@ class WorkflowOwner {
|
|
|
1849
1986
|
}
|
|
1850
1987
|
}
|
|
1851
1988
|
|
|
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
|
+
|
|
1852
2004
|
private openAgent(spec: AgentOpenSpec): Promise<AgentRef> {
|
|
1853
2005
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
2006
|
+
if (spec.labels !== undefined) this.options.deprecations.used("labels");
|
|
1854
2007
|
scopes.getStore()?.assertAccepting();
|
|
1855
2008
|
this.#assertFreeKey(spec.key, "an agent");
|
|
1856
2009
|
if (this.#caller?.key === spec.key) {
|
|
@@ -1861,20 +2014,27 @@ class WorkflowOwner {
|
|
|
1861
2014
|
throw new Error(`agent ${spec.key}: ${RUN_SANDBOX_ONLY}`);
|
|
1862
2015
|
const existing = this.#agents.get(spec.key);
|
|
1863
2016
|
if (existing?.stopped !== undefined) throw stoppedError(spec.key, existing.stopped);
|
|
1864
|
-
const given =
|
|
2017
|
+
const given = spec.keepAlive;
|
|
1865
2018
|
if (given !== undefined && !this.options.mailbox)
|
|
1866
2019
|
throw new Error(
|
|
1867
2020
|
`agent ${spec.key}: keepAlive is for a workflow with events, whose runs park; this one's never do`,
|
|
1868
2021
|
);
|
|
1869
|
-
const keepAlive =
|
|
2022
|
+
const keepAlive =
|
|
2023
|
+
given === undefined
|
|
2024
|
+
? undefined
|
|
2025
|
+
: checkedKeepAlive(spec.key, given, () =>
|
|
2026
|
+
this.options.deprecations.used("keep-alive-timeout-ms"),
|
|
2027
|
+
);
|
|
1870
2028
|
const execution = existing
|
|
1871
2029
|
? constrainExistingExecution(spec.runtime, existing.identity.execution)
|
|
1872
2030
|
: withKnownEffort(resolveExecution(spec.runtime, this.options.runtime));
|
|
1873
|
-
|
|
2031
|
+
const paneless = {
|
|
1874
2032
|
headless: placementOf(execution) === "headless",
|
|
1875
2033
|
sandboxed:
|
|
1876
2034
|
spec.sandbox !== undefined || existing?.identity.sandbox !== undefined || inRunSandbox,
|
|
1877
|
-
}
|
|
2035
|
+
};
|
|
2036
|
+
checkPaneOptions(spec.key, spec, paneless);
|
|
2037
|
+
const written = this.writtenPane(spec);
|
|
1878
2038
|
const identity: AgentIdentity = {
|
|
1879
2039
|
execution,
|
|
1880
2040
|
cwd: spec.cwd ?? this.options.cwd,
|
|
@@ -1882,14 +2042,17 @@ class WorkflowOwner {
|
|
|
1882
2042
|
...(spec.labels === undefined ? {} : { labels: structuredClone(spec.labels) }),
|
|
1883
2043
|
...(spec.sandbox === undefined ? {} : { sandbox: spec.sandbox }),
|
|
1884
2044
|
...(spec.skills === undefined ? {} : { skills: readSkillSources(spec.skills) }),
|
|
1885
|
-
...
|
|
1886
|
-
|
|
2045
|
+
...written,
|
|
2046
|
+
asked: paneAsked(written, this.options.panes, !paneless.headless && !paneless.sandboxed),
|
|
1887
2047
|
};
|
|
1888
2048
|
const scope = scopes.getStore();
|
|
1889
2049
|
const inheritedDeadline = scope?.deadline ?? this.options.deadline;
|
|
1890
|
-
const effectiveDeadline =
|
|
1891
|
-
|
|
1892
|
-
|
|
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
|
+
);
|
|
1893
2056
|
assertDeadline(effectiveDeadline);
|
|
1894
2057
|
if (existing) {
|
|
1895
2058
|
assertCompatibleAgent(spec.key, existing.identity, identity, spec);
|
|
@@ -1950,9 +2113,9 @@ class WorkflowOwner {
|
|
|
1950
2113
|
...(seated?.webSearch ? { webSearch: true } : {}),
|
|
1951
2114
|
...(skills ? { skills } : {}),
|
|
1952
2115
|
...(home ? { home } : {}),
|
|
1953
|
-
...
|
|
2116
|
+
...identity.asked,
|
|
1954
2117
|
}),
|
|
1955
|
-
layoutFallback: this.besideFallback(identity.layout, effectiveDeadline)(),
|
|
2118
|
+
layoutFallback: this.besideFallback(identity.asked?.layout, effectiveDeadline)(),
|
|
1956
2119
|
};
|
|
1957
2120
|
let skills: Promise<PlacedSkills | undefined> = Promise.resolve(undefined);
|
|
1958
2121
|
let opened: { state: Promise<LogicalAgent>; channel: Promise<ResultChannel> };
|
|
@@ -1987,9 +2150,9 @@ class WorkflowOwner {
|
|
|
1987
2150
|
* Ends an agent the workflow has no more use for, as an operation that fails ends it: what it
|
|
1988
2151
|
* is running is cancelled, and its pane closed or, where `keepPane` says, kept and released.
|
|
1989
2152
|
*/
|
|
1990
|
-
private stopAgent(key: string, reason
|
|
2153
|
+
private stopAgent(key: string, reason: string | undefined, what: string): Promise<boolean> {
|
|
1991
2154
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1992
|
-
const inTool = refusedInTool(key,
|
|
2155
|
+
const inTool = refusedInTool(key, what);
|
|
1993
2156
|
if (inTool) throw inTool;
|
|
1994
2157
|
const scope = scopes.getStore();
|
|
1995
2158
|
scope?.assertAccepting();
|
|
@@ -2014,7 +2177,7 @@ class WorkflowOwner {
|
|
|
2014
2177
|
}
|
|
2015
2178
|
this.options.progress.agentStopped(key, entry.stopped!);
|
|
2016
2179
|
try {
|
|
2017
|
-
await state.
|
|
2180
|
+
await state.terminate(entry.stopped);
|
|
2018
2181
|
} finally {
|
|
2019
2182
|
await entry.channel.then((channel) => channel?.close());
|
|
2020
2183
|
}
|
|
@@ -2044,7 +2207,7 @@ class WorkflowOwner {
|
|
|
2044
2207
|
/**
|
|
2045
2208
|
* The session the run was started from, under the key the workflow names: `null` when there is
|
|
2046
2209
|
* none. It is found, not opened: its harness, directory and skills are the operator's, and it
|
|
2047
|
-
* answers through a launcher by path as any
|
|
2210
|
+
* answers through a launcher by path as any TUI agent does (ADR 0010).
|
|
2048
2211
|
*/
|
|
2049
2212
|
private caller(key: string): Promise<AgentRef | null> {
|
|
2050
2213
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
@@ -2121,7 +2284,7 @@ class WorkflowOwner {
|
|
|
2121
2284
|
* it is that agent's own fork, taken in its queue; otherwise it is taken now, from a parent the
|
|
2122
2285
|
* run never opened. `null`, and the reason logged, where there is none to fork.
|
|
2123
2286
|
*/
|
|
2124
|
-
private forkCaller(spec:
|
|
2287
|
+
private forkCaller(spec: CallerForkSpec): Promise<AgentRef | null> {
|
|
2125
2288
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
2126
2289
|
const scope = scopes.getStore();
|
|
2127
2290
|
scope?.assertAccepting();
|
|
@@ -2150,11 +2313,10 @@ class WorkflowOwner {
|
|
|
2150
2313
|
channel: Promise.resolve(undefined),
|
|
2151
2314
|
};
|
|
2152
2315
|
parent.state.catch(() => undefined);
|
|
2153
|
-
const deadline = scope?.deadline ?? this.options.deadline;
|
|
2154
2316
|
return this.forkAgent(
|
|
2155
2317
|
CALLING,
|
|
2156
2318
|
spec,
|
|
2157
|
-
(into) =>
|
|
2319
|
+
(deadline, into) =>
|
|
2158
2320
|
calling
|
|
2159
2321
|
.fork(deadlineWithin(FORK_TIMEOUT_MS, deadline), into, this.#closingRun.signal)
|
|
2160
2322
|
.then((native) => ({ native, after: null })),
|
|
@@ -2399,7 +2561,16 @@ class WorkflowOwner {
|
|
|
2399
2561
|
livenessPolicy: this.options.livenessPolicy,
|
|
2400
2562
|
...(this.options.mailbox ? { turns: this.#turnsOf(key, this.options.mailbox) } : {}),
|
|
2401
2563
|
fork: (spec, take, settings) => this.forkAgent(key, spec, take, settings),
|
|
2402
|
-
|
|
2564
|
+
close: (reason) => {
|
|
2565
|
+
try {
|
|
2566
|
+
return this.stopAgent(key, reason, "close()");
|
|
2567
|
+
} catch (error) {
|
|
2568
|
+
return Promise.reject(error);
|
|
2569
|
+
}
|
|
2570
|
+
},
|
|
2571
|
+
deprecated: (name, subject) => this.options.deprecations.used(name, subject),
|
|
2572
|
+
reporting: (outcome) => this.#reporting(outcome),
|
|
2573
|
+
keepPane: () => this.#agents.get(key)?.identity.asked?.keepPane,
|
|
2403
2574
|
keepAlive: () => this.#agents.get(key)?.keepAlive,
|
|
2404
2575
|
defaultWindowMs: this.options.defaultWindowMs ?? DEFAULT_WINDOW_MS,
|
|
2405
2576
|
now: this.options.now,
|
|
@@ -2431,14 +2602,18 @@ class WorkflowOwner {
|
|
|
2431
2602
|
private forkAgent(
|
|
2432
2603
|
parentKey: string,
|
|
2433
2604
|
spec: AgentForkSpec,
|
|
2434
|
-
/**
|
|
2435
|
-
|
|
2605
|
+
/**
|
|
2606
|
+
* Queues the native fork, or its copy `into` another home, within the fork's `deadline`; or why
|
|
2607
|
+
* its host cannot fork.
|
|
2608
|
+
*/
|
|
2609
|
+
take: TakeFork | string,
|
|
2436
2610
|
/** The parent's settings once every `set` queued before the fork has run. */
|
|
2437
2611
|
parentSettings: AgentExecution,
|
|
2438
2612
|
/** A parent the run never opened: the session it was started from. */
|
|
2439
2613
|
unopened?: AgentEntry,
|
|
2440
2614
|
): Promise<AgentRef> {
|
|
2441
2615
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
2616
|
+
if (spec.labels !== undefined) this.options.deprecations.used("labels");
|
|
2442
2617
|
const scope = scopes.getStore();
|
|
2443
2618
|
scope?.assertAccepting();
|
|
2444
2619
|
this.options.stages.checkOperation();
|
|
@@ -2447,10 +2622,19 @@ class WorkflowOwner {
|
|
|
2447
2622
|
}
|
|
2448
2623
|
this.#assertFreeKey(spec.key, "an agent");
|
|
2449
2624
|
// Out before the clone, which throws on a handler, and out of the spec a repeat is compared by.
|
|
2450
|
-
|
|
2451
|
-
const
|
|
2625
|
+
// A bound is the call's, not the fork's: a repeat with another is the same fork.
|
|
2626
|
+
const { tools, timeout, ...given } = spec;
|
|
2627
|
+
const forkSpec = structuredClone(
|
|
2628
|
+
forkSpecOf(given, () => this.options.deprecations.used("fork-runtime")),
|
|
2629
|
+
);
|
|
2452
2630
|
if (forkSpec.layout !== undefined) forkSpec.layout = storedLayout(forkSpec.layout);
|
|
2453
|
-
|
|
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
|
+
);
|
|
2454
2638
|
assertDeadline(deadline);
|
|
2455
2639
|
// A fork of the calling session is one whichever way it was asked for.
|
|
2456
2640
|
const from = unopened || this.#caller?.key === parentKey ? CALLING : parentKey;
|
|
@@ -2480,15 +2664,14 @@ class WorkflowOwner {
|
|
|
2480
2664
|
sandboxed || opening.accounted.home !== undefined
|
|
2481
2665
|
? join(this.options.runDir, "forks", randomUUID())
|
|
2482
2666
|
: undefined;
|
|
2483
|
-
const execution = withKnownEffort(forkExecution(parentSettings, forkSpec));
|
|
2667
|
+
const execution = withKnownEffort(forkExecution(parentSettings, forkSpec.runtime));
|
|
2484
2668
|
// A fork of the calling session is an agent the run opened, on the model the session's files
|
|
2485
2669
|
// show, which the fork reads: `""` until then (ADR 0010).
|
|
2486
2670
|
const ofCaller = execution.caller === true;
|
|
2487
2671
|
delete execution.caller;
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
});
|
|
2672
|
+
const headless = placementOf(execution) === "headless";
|
|
2673
|
+
checkPaneOptions(spec.key, forkSpec, { headless, sandboxed });
|
|
2674
|
+
const written = this.writtenPane(forkSpec);
|
|
2492
2675
|
const identity: AgentIdentity = {
|
|
2493
2676
|
execution,
|
|
2494
2677
|
cwd: parent.identity.cwd,
|
|
@@ -2497,8 +2680,8 @@ class WorkflowOwner {
|
|
|
2497
2680
|
...(parent.identity.skills ? { skills: parent.identity.skills } : {}),
|
|
2498
2681
|
...(parent.identity.sandbox === undefined ? {} : { sandbox: parent.identity.sandbox }),
|
|
2499
2682
|
forkedFrom: { parent: from, spec: forkSpec },
|
|
2500
|
-
...
|
|
2501
|
-
|
|
2683
|
+
...written,
|
|
2684
|
+
asked: paneAsked(written, this.options.panes, !headless && !sandboxed),
|
|
2502
2685
|
};
|
|
2503
2686
|
|
|
2504
2687
|
this.options.progress.agentOpened(spec.key, scope?.group);
|
|
@@ -2512,11 +2695,11 @@ class WorkflowOwner {
|
|
|
2512
2695
|
sessions: () => reportedSessions(sessions),
|
|
2513
2696
|
forkedAt: () => forkedAt,
|
|
2514
2697
|
};
|
|
2515
|
-
const besideAtFork = this.besideFallback(identity.layout, deadline);
|
|
2698
|
+
const besideAtFork = this.besideFallback(identity.asked?.layout, deadline);
|
|
2516
2699
|
let continues: NativeFork | undefined;
|
|
2517
2700
|
// The fork is asked for now, in the parent's queue; the child opens once it is made. One that
|
|
2518
2701
|
// was not made leaves nothing behind: its key may be forked again, after the parent's turn.
|
|
2519
|
-
const forked = take(copy === undefined ? undefined : { directory: copy })
|
|
2702
|
+
const forked = take(deadline, copy === undefined ? undefined : { directory: copy })
|
|
2520
2703
|
.then(({ native: fork, after }) => {
|
|
2521
2704
|
if (ofCaller) {
|
|
2522
2705
|
if (!fork.model) throw new Error(`agent ${parentKey}'s fork named no model to run on`);
|
|
@@ -2569,7 +2752,7 @@ class WorkflowOwner {
|
|
|
2569
2752
|
...(given ? { skills: given } : {}),
|
|
2570
2753
|
...(home ? { home } : {}),
|
|
2571
2754
|
...(continues ? { continues } : {}),
|
|
2572
|
-
...
|
|
2755
|
+
...identity.asked,
|
|
2573
2756
|
}),
|
|
2574
2757
|
// The target as it stands when `fork` is called, waited for once the fork is made.
|
|
2575
2758
|
layoutFallback: forked.then(besideAtFork, () => undefined),
|
|
@@ -2690,16 +2873,17 @@ class WorkflowOwner {
|
|
|
2690
2873
|
);
|
|
2691
2874
|
}
|
|
2692
2875
|
|
|
2693
|
-
/** Where each
|
|
2876
|
+
/** Where each TUI agent's pane went, as its host reported it, in the order opened. */
|
|
2694
2877
|
panes(): AgentPaneRecord[] {
|
|
2695
2878
|
return [...this.#agents].flatMap(([agent, entry]) => {
|
|
2696
2879
|
const placed = entry.opening?.sessions.harness?.pane?.();
|
|
2697
2880
|
if (!placed) return [];
|
|
2698
|
-
const { layout, keepPane } = entry.identity;
|
|
2881
|
+
const { pane, layout, keepPane } = entry.identity;
|
|
2699
2882
|
return [
|
|
2700
2883
|
{
|
|
2701
2884
|
callPath: [],
|
|
2702
2885
|
agent,
|
|
2886
|
+
...(pane === undefined ? {} : { pane }),
|
|
2703
2887
|
...(layout === undefined ? {} : { layout }),
|
|
2704
2888
|
...(keepPane === undefined ? {} : { keepPane }),
|
|
2705
2889
|
placed,
|
|
@@ -2726,6 +2910,11 @@ class WorkflowOwner {
|
|
|
2726
2910
|
}
|
|
2727
2911
|
}
|
|
2728
2912
|
|
|
2913
|
+
// What a wait takes of it is type-only, and `asSource` marks it at run time.
|
|
2914
|
+
// biome-ignore lint/correctness/noUnusedVariables: merged into the class below.
|
|
2915
|
+
interface LogicalAgent extends Source<Taken<AgentEnded>> {}
|
|
2916
|
+
|
|
2917
|
+
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: the interface adds a type-only member.
|
|
2729
2918
|
class LogicalAgent implements AgentRef {
|
|
2730
2919
|
readonly [PARTICIPANT] = true as const;
|
|
2731
2920
|
readonly #operationStops = new Set<(stop: OperationStop) => Promise<void>>();
|
|
@@ -2804,14 +2993,19 @@ class LogicalAgent implements AgentRef {
|
|
|
2804
2993
|
*/
|
|
2805
2994
|
fork(
|
|
2806
2995
|
spec: AgentForkSpec,
|
|
2807
|
-
take:
|
|
2996
|
+
take: TakeFork | string,
|
|
2808
2997
|
settings: AgentExecution,
|
|
2809
2998
|
): Promise<AgentRef>;
|
|
2999
|
+
/** The workflow ends it: the run tears it down and its key is not opened again. */
|
|
3000
|
+
close(reason: string | undefined): Promise<boolean>;
|
|
3001
|
+
deprecated(name: Deprecated, subject?: string): void;
|
|
3002
|
+
/** An outcome as the workflow is handed it, which a deprecated name it is given reports on. */
|
|
3003
|
+
reporting<T>(outcome: T): T;
|
|
2810
3004
|
/** As the workflow wrote it; decides, at close, whether its pane stays. */
|
|
2811
3005
|
keepPane(): KeepPane | undefined;
|
|
2812
3006
|
/** As the workflow's last open of it gave it. */
|
|
2813
3007
|
keepAlive(): KeepAlive | undefined;
|
|
2814
|
-
/** Its window when `keepAlive` gives no `
|
|
3008
|
+
/** Its window when `keepAlive` gives no `window`. */
|
|
2815
3009
|
defaultWindowMs: number;
|
|
2816
3010
|
/** The run's time, which its window is measured on. */
|
|
2817
3011
|
now: () => number;
|
|
@@ -2825,7 +3019,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2825
3019
|
const restored = options.turns?.restored();
|
|
2826
3020
|
if (restored)
|
|
2827
3021
|
this.#started = {
|
|
2828
|
-
ref: turnRef(options.key, restored.turn, async () => false),
|
|
3022
|
+
ref: turnRef(options.key, restored.turn, async () => false, options.deprecated),
|
|
2829
3023
|
meta: restored.meta,
|
|
2830
3024
|
ended: true,
|
|
2831
3025
|
done: Promise.resolve(),
|
|
@@ -2865,7 +3059,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2865
3059
|
liveUntil(now = this.options.now()): number | undefined {
|
|
2866
3060
|
const keepAlive = this.options.keepAlive();
|
|
2867
3061
|
if (this.#closed || !keepAlive || this.#lastEnd === undefined) return undefined;
|
|
2868
|
-
const until = this.#lastEnd + (keepAlive.
|
|
3062
|
+
const until = this.#lastEnd + (keepAlive.windowMs ?? this.options.defaultWindowMs);
|
|
2869
3063
|
return until > now ? until : undefined;
|
|
2870
3064
|
}
|
|
2871
3065
|
|
|
@@ -2928,8 +3122,12 @@ class LogicalAgent implements AgentRef {
|
|
|
2928
3122
|
|
|
2929
3123
|
async start(spec: AgentStartSpec): Promise<StartedTurn> {
|
|
2930
3124
|
const { turns } = this.options;
|
|
2931
|
-
if (!turns)
|
|
3125
|
+
if (!turns)
|
|
3126
|
+
throw new Error(
|
|
3127
|
+
`agent ${this.key}: start() is for a workflow with events, at whose waits its end arrives; run() waits for the end here`,
|
|
3128
|
+
);
|
|
2932
3129
|
if (this.#closed) throw new Error("logical agent is closed");
|
|
3130
|
+
turns.checkDeclared();
|
|
2933
3131
|
const inTool = refusedInTool(this.key, "start()");
|
|
2934
3132
|
if (inTool) throw inTool;
|
|
2935
3133
|
const scope = scopes.getStore();
|
|
@@ -2940,24 +3138,30 @@ class LogicalAgent implements AgentRef {
|
|
|
2940
3138
|
const refused = this.#refusedWhileStarted("start");
|
|
2941
3139
|
if (refused) throw refused;
|
|
2942
3140
|
const { meta = null, ...run } = spec;
|
|
3141
|
+
if (run.id !== undefined) this.options.deprecated("call-id");
|
|
2943
3142
|
const id = run.id ?? randomUUID();
|
|
2944
3143
|
if (!isJsonValue(meta)) throw new Error("a turn's meta is a JSON value");
|
|
2945
3144
|
const ended = Promise.withResolvers<void>();
|
|
2946
3145
|
let written: TurnOutcome<JsonValue>["kind"] | undefined;
|
|
2947
3146
|
const started = {
|
|
2948
|
-
ref: turnRef(
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3147
|
+
ref: turnRef(
|
|
3148
|
+
this.key,
|
|
3149
|
+
id,
|
|
3150
|
+
async (reason?: string) => {
|
|
3151
|
+
if (started.ended) return false;
|
|
3152
|
+
// Its turn waits for the handler, which would wait for the turn's end.
|
|
3153
|
+
const inTool = refusedInTool(this.key, "cancel()");
|
|
3154
|
+
if (inTool) throw inTool;
|
|
3155
|
+
const stop = { kind: "cancelled" as const, reason: reason ?? "the turn was cancelled" };
|
|
3156
|
+
// Its own operation only: one queued behind a message settles cancelled when it is reached.
|
|
3157
|
+
const own = this.#stopsByTurn.get(id);
|
|
3158
|
+
if (own) await own(stop);
|
|
3159
|
+
else this.#cancelledTurns.set(id, stop.reason);
|
|
3160
|
+
await started.done;
|
|
3161
|
+
return written === "cancelled";
|
|
3162
|
+
},
|
|
3163
|
+
this.options.deprecated,
|
|
3164
|
+
),
|
|
2961
3165
|
meta,
|
|
2962
3166
|
ended: false,
|
|
2963
3167
|
done: ended.promise,
|
|
@@ -2988,6 +3192,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2988
3192
|
.then(
|
|
2989
3193
|
({ outcome }) => outcome,
|
|
2990
3194
|
(error: unknown): TurnOutcome<JsonValue> => ({
|
|
3195
|
+
answered: false,
|
|
2991
3196
|
kind: "failed",
|
|
2992
3197
|
reason: messageOf(error),
|
|
2993
3198
|
retryable: false,
|
|
@@ -3022,11 +3227,12 @@ class LogicalAgent implements AgentRef {
|
|
|
3022
3227
|
);
|
|
3023
3228
|
}
|
|
3024
3229
|
|
|
3025
|
-
is<
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3230
|
+
is<
|
|
3231
|
+
Meta extends JsonValue = JsonValue,
|
|
3232
|
+
T extends JsonValue = JsonValue,
|
|
3233
|
+
E extends EventLike = EventLike,
|
|
3234
|
+
>(event: E): event is E & AgentEndOf<E, Meta, T> {
|
|
3235
|
+
return matches(event, { name: "agent:ended", from: this.key });
|
|
3030
3236
|
}
|
|
3031
3237
|
|
|
3032
3238
|
run(spec: AgentRunTextSpec): Promise<RunResult<string>>;
|
|
@@ -3045,6 +3251,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3045
3251
|
return Promise.reject(error);
|
|
3046
3252
|
}
|
|
3047
3253
|
const opensWindow = !inTimeoutHandler();
|
|
3254
|
+
if (spec.id !== undefined && !this.#launching) this.options.deprecated("call-id");
|
|
3048
3255
|
const id = spec.id ?? randomUUID();
|
|
3049
3256
|
const completeSpec = structuredClone({ ...spec, id }) as
|
|
3050
3257
|
| AgentRunTextSpec
|
|
@@ -3063,11 +3270,13 @@ class LogicalAgent implements AgentRef {
|
|
|
3063
3270
|
}
|
|
3064
3271
|
let deadline: AbsoluteDeadline | undefined;
|
|
3065
3272
|
try {
|
|
3066
|
-
deadline = this.operationDeadline(spec, scope);
|
|
3273
|
+
deadline = this.operationDeadline(spec, scope, this.#launching ? "on start" : "on run");
|
|
3067
3274
|
const recoveryDeadline = spec.nudge ? spec.nudge.deadline : undefined;
|
|
3068
3275
|
if (recoveryDeadline) {
|
|
3069
|
-
|
|
3070
|
-
|
|
3276
|
+
this.options.deprecated("nudge-deadline");
|
|
3277
|
+
const own = { unixMilliseconds: recoveryDeadline.unixMilliseconds };
|
|
3278
|
+
assertDeadlineValue(own);
|
|
3279
|
+
deadline = earlierDeadline(deadline, own);
|
|
3071
3280
|
}
|
|
3072
3281
|
} catch (error) {
|
|
3073
3282
|
const rejected = Promise.reject<RunResult<string | T>>(error);
|
|
@@ -3106,7 +3315,12 @@ class LogicalAgent implements AgentRef {
|
|
|
3106
3315
|
await this.options.afterOperation?.();
|
|
3107
3316
|
}
|
|
3108
3317
|
});
|
|
3109
|
-
const tracked = this.options.track(
|
|
3318
|
+
const tracked = this.options.track(
|
|
3319
|
+
result.then((settled) => {
|
|
3320
|
+
this.options.reporting(settled.outcome);
|
|
3321
|
+
return settled;
|
|
3322
|
+
}),
|
|
3323
|
+
);
|
|
3110
3324
|
scope?.track(tracked);
|
|
3111
3325
|
this.#operations.set(id, { spec: completeSpec, result: tracked });
|
|
3112
3326
|
return tracked as Promise<RunResult<string | T>>;
|
|
@@ -3123,6 +3337,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3123
3337
|
} catch (error) {
|
|
3124
3338
|
return Promise.reject(error);
|
|
3125
3339
|
}
|
|
3340
|
+
if (spec.id !== undefined) this.options.deprecated("call-id");
|
|
3126
3341
|
const id = spec.id ?? randomUUID();
|
|
3127
3342
|
const completeSpec = structuredClone({ ...spec, id });
|
|
3128
3343
|
const existing = this.#compactions.get(id);
|
|
@@ -3139,7 +3354,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3139
3354
|
}
|
|
3140
3355
|
let deadline: AbsoluteDeadline;
|
|
3141
3356
|
try {
|
|
3142
|
-
deadline = harnessDeadline(this.operationDeadline(spec, scope));
|
|
3357
|
+
deadline = harnessDeadline(this.operationDeadline(spec, scope, "on compact"));
|
|
3143
3358
|
} catch (error) {
|
|
3144
3359
|
const rejected = Promise.reject<TurnOutcome<string>>(error);
|
|
3145
3360
|
scope?.track(rejected);
|
|
@@ -3165,7 +3380,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3165
3380
|
await this.options.afterOperation?.();
|
|
3166
3381
|
}
|
|
3167
3382
|
});
|
|
3168
|
-
const tracked = this.options.track(result);
|
|
3383
|
+
const tracked = this.options.track(result.then(this.options.reporting));
|
|
3169
3384
|
scope?.track(tracked);
|
|
3170
3385
|
this.#compactions.set(id, { spec: completeSpec, result: tracked });
|
|
3171
3386
|
return tracked;
|
|
@@ -3182,6 +3397,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3182
3397
|
} catch (error) {
|
|
3183
3398
|
return Promise.reject(error);
|
|
3184
3399
|
}
|
|
3400
|
+
if (spec.id !== undefined) this.options.deprecated("call-id");
|
|
3185
3401
|
const id = spec.id ?? randomUUID();
|
|
3186
3402
|
const completeSpec = structuredClone({ ...spec, id });
|
|
3187
3403
|
const existing = this.#sets.get(id);
|
|
@@ -3200,7 +3416,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3200
3416
|
let deadline: AbsoluteDeadline;
|
|
3201
3417
|
try {
|
|
3202
3418
|
change = this.settingsChange(spec);
|
|
3203
|
-
deadline = harnessDeadline(this.operationDeadline(spec, scope));
|
|
3419
|
+
deadline = harnessDeadline(this.operationDeadline(spec, scope, "on set"));
|
|
3204
3420
|
} catch (error) {
|
|
3205
3421
|
const rejected = Promise.reject<TurnOutcome<null>>(error);
|
|
3206
3422
|
scope?.track(rejected);
|
|
@@ -3220,7 +3436,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3220
3436
|
this.#pendingSets.splice(this.#pendingSets.indexOf(change), 1);
|
|
3221
3437
|
}
|
|
3222
3438
|
});
|
|
3223
|
-
const tracked = this.options.track(result);
|
|
3439
|
+
const tracked = this.options.track(result.then(this.options.reporting));
|
|
3224
3440
|
scope?.track(tracked);
|
|
3225
3441
|
this.#sets.set(id, { spec: completeSpec, result: tracked });
|
|
3226
3442
|
return tracked;
|
|
@@ -3305,7 +3521,6 @@ class LogicalAgent implements AgentRef {
|
|
|
3305
3521
|
const refused = this.#refusedWhileStarted("fork");
|
|
3306
3522
|
if (refused) return Promise.reject(refused);
|
|
3307
3523
|
const scope = scopes.getStore();
|
|
3308
|
-
const deadline = this.deadlineCeiling(scope);
|
|
3309
3524
|
const { session, execution } = this.options;
|
|
3310
3525
|
const settings = this.queuedExecution();
|
|
3311
3526
|
const fork = session.fork?.bind(session);
|
|
@@ -3324,7 +3539,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3324
3539
|
}
|
|
3325
3540
|
// The harness bounds its fork by this deadline and stops it there, so the session is idle again
|
|
3326
3541
|
// when the fork rejects.
|
|
3327
|
-
const take = (into
|
|
3542
|
+
const take: TakeFork = (deadline, into) =>
|
|
3328
3543
|
this.queue(async () => {
|
|
3329
3544
|
if (this.#closed || this.options.isRunClosing()) {
|
|
3330
3545
|
throw new Error("logical agent is closed");
|
|
@@ -3347,19 +3562,19 @@ class LogicalAgent implements AgentRef {
|
|
|
3347
3562
|
}
|
|
3348
3563
|
}
|
|
3349
3564
|
|
|
3350
|
-
/**
|
|
3565
|
+
/** Its `timeout`, never later than the scope's or the run's; else the earlier of those. */
|
|
3351
3566
|
private operationDeadline(
|
|
3352
|
-
spec:
|
|
3567
|
+
spec: GivenBound,
|
|
3353
3568
|
scope: ExecutionScope | undefined,
|
|
3569
|
+
call: string,
|
|
3354
3570
|
): AbsoluteDeadline | undefined {
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
return earlierDeadline(spec.deadline, ceiling);
|
|
3571
|
+
const { timeout, deadline, timeoutMs } = spec;
|
|
3572
|
+
return boundedDeadline(
|
|
3573
|
+
{ timeout, deadline, timeoutMs },
|
|
3574
|
+
this.deadlineCeiling(scope),
|
|
3575
|
+
this.options.deprecated,
|
|
3576
|
+
call,
|
|
3577
|
+
);
|
|
3363
3578
|
}
|
|
3364
3579
|
|
|
3365
3580
|
/** The deadlines around an operation: its scope's and the run's, if either has one. */
|
|
@@ -3423,10 +3638,15 @@ class LogicalAgent implements AgentRef {
|
|
|
3423
3638
|
const usage = () =>
|
|
3424
3639
|
entry.settle({ settledAt: Math.min(Date.now(), deadline.unixMilliseconds) }, []);
|
|
3425
3640
|
if (Date.now() >= deadline.unixMilliseconds) {
|
|
3426
|
-
return {
|
|
3641
|
+
return {
|
|
3642
|
+
answered: false,
|
|
3643
|
+
kind: "timed-out",
|
|
3644
|
+
reason: "settings deadline exceeded",
|
|
3645
|
+
usage: usage(),
|
|
3646
|
+
};
|
|
3427
3647
|
}
|
|
3428
3648
|
const next: AgentExecution = { ...this.#execution, ...change };
|
|
3429
|
-
const pane = placementOf(next) === "
|
|
3649
|
+
const pane = placementOf(next) === "tui";
|
|
3430
3650
|
let cancelled: string | undefined;
|
|
3431
3651
|
// A pane's switch waits on its screen; a scope cancelled meanwhile closes the agent, as a
|
|
3432
3652
|
// half-made switch leaves it at settings nobody knows. A headless one takes no time.
|
|
@@ -3434,7 +3654,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3434
3654
|
? scope?.add(async (stop) => {
|
|
3435
3655
|
cancelled = stop.reason;
|
|
3436
3656
|
this.#last = "cancelled";
|
|
3437
|
-
await this.
|
|
3657
|
+
await this.terminate(stop.reason);
|
|
3438
3658
|
})
|
|
3439
3659
|
: undefined;
|
|
3440
3660
|
try {
|
|
@@ -3451,15 +3671,16 @@ class LogicalAgent implements AgentRef {
|
|
|
3451
3671
|
cancelled !== undefined ? "cancelled" : expired ? "timed-out" : "failed",
|
|
3452
3672
|
);
|
|
3453
3673
|
}
|
|
3454
|
-
if (cancelled !== undefined)
|
|
3674
|
+
if (cancelled !== undefined)
|
|
3675
|
+
return { answered: false, kind: "cancelled", reason, usage: usage() };
|
|
3455
3676
|
return expired
|
|
3456
|
-
? { kind: "timed-out", reason, usage: usage() }
|
|
3457
|
-
: { kind: "failed", reason, retryable: false, usage: usage() };
|
|
3677
|
+
? { answered: false, kind: "timed-out", reason, usage: usage() }
|
|
3678
|
+
: { answered: false, kind: "failed", reason, retryable: false, usage: usage() };
|
|
3458
3679
|
} finally {
|
|
3459
3680
|
removeCanceller?.();
|
|
3460
3681
|
}
|
|
3461
3682
|
this.#execution = next;
|
|
3462
|
-
return { kind: "answered", value: null, usage: usage() };
|
|
3683
|
+
return { answered: true, kind: "answered", value: null, usage: usage() };
|
|
3463
3684
|
}
|
|
3464
3685
|
|
|
3465
3686
|
/**
|
|
@@ -3470,10 +3691,15 @@ class LogicalAgent implements AgentRef {
|
|
|
3470
3691
|
if (this.options.execution.caller) return;
|
|
3471
3692
|
// Before its outcome is recorded: whether its pane stays turns on how this one ended.
|
|
3472
3693
|
this.#last = ended;
|
|
3473
|
-
void this.
|
|
3694
|
+
void this.terminate(reason).catch(() => undefined);
|
|
3474
3695
|
}
|
|
3475
3696
|
|
|
3476
|
-
close(reason?: string): Promise<
|
|
3697
|
+
close(reason?: string): Promise<boolean> {
|
|
3698
|
+
return this.options.close(reason);
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
/** What `close()`, a failed operation and the run's end each come to. */
|
|
3702
|
+
terminate(reason?: string): Promise<void> {
|
|
3477
3703
|
const first = !this.#closed;
|
|
3478
3704
|
this.#closed = true;
|
|
3479
3705
|
if (first) {
|
|
@@ -3557,7 +3783,6 @@ class LogicalAgent implements AgentRef {
|
|
|
3557
3783
|
): Promise<RunResult<JsonValue>> {
|
|
3558
3784
|
const nudge = spec.nudge === false ? undefined : (spec.nudge ?? this.defaultNudge());
|
|
3559
3785
|
scope?.assertActive();
|
|
3560
|
-
if (nudge?.deadline) assertDeadlineValue(nudge.deadline);
|
|
3561
3786
|
return this.executeWaitingOperation(spec, scope, operationDeadline, operation, nudge);
|
|
3562
3787
|
}
|
|
3563
3788
|
|
|
@@ -3607,7 +3832,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3607
3832
|
const recoveryPrompt =
|
|
3608
3833
|
nudge?.prompt ??
|
|
3609
3834
|
(waitingSupported
|
|
3610
|
-
? "Report the result now, or use
|
|
3835
|
+
? "Report the result now, or use wfa waiting with a reason if you still need time."
|
|
3611
3836
|
: "You finished without reporting the requested result. Report it now.");
|
|
3612
3837
|
// The first prompt carries only the result command; waiting is rare, so it is offered only
|
|
3613
3838
|
// once a check-in finds the agent idle without an answer.
|
|
@@ -3665,7 +3890,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3665
3890
|
toolsLesson(
|
|
3666
3891
|
this.options.tools.taught(this.key),
|
|
3667
3892
|
this.options.launcher,
|
|
3668
|
-
|
|
3893
|
+
resultSaysQuoting(schema),
|
|
3669
3894
|
),
|
|
3670
3895
|
]
|
|
3671
3896
|
.filter(Boolean)
|
|
@@ -3733,14 +3958,23 @@ class LogicalAgent implements AgentRef {
|
|
|
3733
3958
|
outcome.charges,
|
|
3734
3959
|
);
|
|
3735
3960
|
if (outcome.kind === "answered")
|
|
3736
|
-
return {
|
|
3961
|
+
return {
|
|
3962
|
+
outcome: { answered: true, kind: "answered", value: outcome.value as JsonValue, usage },
|
|
3963
|
+
};
|
|
3737
3964
|
if (outcome.kind === "failed") {
|
|
3738
3965
|
const login = outcome.login ? { login: outcome.login } : {};
|
|
3739
3966
|
return {
|
|
3740
|
-
outcome: {
|
|
3967
|
+
outcome: {
|
|
3968
|
+
answered: false,
|
|
3969
|
+
kind: "failed",
|
|
3970
|
+
reason: outcome.reason,
|
|
3971
|
+
retryable: false,
|
|
3972
|
+
...login,
|
|
3973
|
+
usage,
|
|
3974
|
+
},
|
|
3741
3975
|
};
|
|
3742
3976
|
}
|
|
3743
|
-
return { outcome: { kind: outcome.kind, reason: outcome.reason, usage } };
|
|
3977
|
+
return { outcome: { answered: false, kind: outcome.kind, reason: outcome.reason, usage } };
|
|
3744
3978
|
}
|
|
3745
3979
|
|
|
3746
3980
|
/**
|
|
@@ -4036,10 +4270,10 @@ class LogicalAgent implements AgentRef {
|
|
|
4036
4270
|
// Recorded `answered` with no value: it did what it was woken for, and takes no result.
|
|
4037
4271
|
this.turnEnded(
|
|
4038
4272
|
outcome.kind === "answered"
|
|
4039
|
-
? { kind: "answered", value: null, usage }
|
|
4273
|
+
? { answered: true, kind: "answered", value: null, usage }
|
|
4040
4274
|
: outcome.kind === "failed"
|
|
4041
|
-
? { kind: "failed", reason: outcome.reason, retryable: false, usage }
|
|
4042
|
-
: { kind: outcome.kind, reason: outcome.reason, usage },
|
|
4275
|
+
? { answered: false, kind: "failed", reason: outcome.reason, retryable: false, usage }
|
|
4276
|
+
: { answered: false, kind: outcome.kind, reason: outcome.reason, usage },
|
|
4043
4277
|
"turn",
|
|
4044
4278
|
true,
|
|
4045
4279
|
);
|
|
@@ -4074,17 +4308,23 @@ class LogicalAgent implements AgentRef {
|
|
|
4074
4308
|
later,
|
|
4075
4309
|
);
|
|
4076
4310
|
if (native === "expired") {
|
|
4077
|
-
return {
|
|
4311
|
+
return {
|
|
4312
|
+
answered: false,
|
|
4313
|
+
kind: "timed-out",
|
|
4314
|
+
reason: "compaction deadline exceeded",
|
|
4315
|
+
usage,
|
|
4316
|
+
};
|
|
4078
4317
|
}
|
|
4079
4318
|
if (native.state === "completed") {
|
|
4080
4319
|
return native.summary === undefined
|
|
4081
4320
|
? {
|
|
4321
|
+
answered: false,
|
|
4082
4322
|
kind: "failed",
|
|
4083
4323
|
reason: native.detail ?? "the harness did not confirm a compaction",
|
|
4084
4324
|
retryable: false,
|
|
4085
4325
|
usage,
|
|
4086
4326
|
}
|
|
4087
|
-
: { kind: "answered", value: native.summary, usage };
|
|
4327
|
+
: { answered: true, kind: "answered", value: native.summary, usage };
|
|
4088
4328
|
}
|
|
4089
4329
|
return reconcile<string>(native, usage);
|
|
4090
4330
|
};
|
|
@@ -4156,15 +4396,32 @@ function checkedWhen(when: unknown): string | Date {
|
|
|
4156
4396
|
if (when instanceof Date || typeof when === "string") return when;
|
|
4157
4397
|
if (readOf(when))
|
|
4158
4398
|
throw new Error(
|
|
4159
|
-
"sleep is for a duration or a Date;
|
|
4399
|
+
"sleep is for a duration or a Date; wf.at() is a select's branch, read at each of its waits",
|
|
4160
4400
|
);
|
|
4161
4401
|
throw new Error('sleep is for a duration, "2h", or until a Date');
|
|
4162
4402
|
}
|
|
4163
4403
|
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4404
|
+
/** Receive's options as given, checked: what a typed caller can't get wrong, a plain one can. */
|
|
4405
|
+
function receiveLabel(options: unknown): string | undefined {
|
|
4406
|
+
if (options === undefined) return undefined;
|
|
4407
|
+
if (typeof options !== "object" || options === null) throw new Error("receive takes { label? }");
|
|
4408
|
+
// `take: "all"` was the default, so only "oldest" asked for what receive no longer does.
|
|
4409
|
+
const { take } = options as { take?: unknown };
|
|
4410
|
+
if (take !== undefined && take !== "all")
|
|
4411
|
+
throw new Error(
|
|
4412
|
+
"receive takes every event not yet acknowledged; wf.select takes one at a time",
|
|
4413
|
+
);
|
|
4414
|
+
if ("until" in options)
|
|
4415
|
+
throw new Error("receive has no until: a timer, wf.timer(name, when), wakes it");
|
|
4416
|
+
return labelOf((options as WaitOptions).label);
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
/** A wait's label, a function's asked now. */
|
|
4420
|
+
function labelOf(label: unknown): string | undefined {
|
|
4421
|
+
const given = typeof label === "function" ? label() : label;
|
|
4422
|
+
if (given !== undefined && typeof given !== "string")
|
|
4423
|
+
throw new Error("a wait's label is a string, or a function that returns one");
|
|
4424
|
+
return given;
|
|
4168
4425
|
}
|
|
4169
4426
|
|
|
4170
4427
|
/** A wait's branches, by name, checked: a race may give a duration where a select may not. */
|
|
@@ -4176,18 +4433,18 @@ function branchesOf(what: string, branches: unknown, durations: boolean): [strin
|
|
|
4176
4433
|
if ("key" in branches) throw new Error(`${what}: no branch is named key, which the answer uses`);
|
|
4177
4434
|
for (const [branch, source] of entries)
|
|
4178
4435
|
if (typeof source === "string" && !durations)
|
|
4179
|
-
throw new Error(`${what}'s ${branch} is a duration: a timer in a loop is
|
|
4436
|
+
throw new Error(`${what}'s ${branch} is a duration: a timer in a loop is wf.timer()'s`);
|
|
4180
4437
|
return entries;
|
|
4181
4438
|
}
|
|
4182
4439
|
|
|
4183
4440
|
/** What a time read from state reads, by its source. */
|
|
4184
4441
|
const timeReads = new WeakMap<object, () => Date | null>();
|
|
4185
4442
|
|
|
4186
|
-
/** `
|
|
4443
|
+
/** `wf.at(read)`: a select's branch, due at the time `read` returns. */
|
|
4187
4444
|
function timeRead(read: () => Date | null): TimeRead {
|
|
4188
4445
|
if (typeof read !== "function")
|
|
4189
4446
|
throw new Error(
|
|
4190
|
-
"
|
|
4447
|
+
"wf.at takes a function that reads a Date or null from the run's state, as () => ledger.nextNudge()",
|
|
4191
4448
|
);
|
|
4192
4449
|
const value = Object.freeze({});
|
|
4193
4450
|
timeReads.set(value, read);
|
|
@@ -4205,18 +4462,34 @@ function sourceTake(what: string, branch: string, source: unknown): WaitTake {
|
|
|
4205
4462
|
if (take) return take;
|
|
4206
4463
|
if (readOf(source))
|
|
4207
4464
|
throw new Error(
|
|
4208
|
-
`${what}'s ${branch} is
|
|
4465
|
+
`${what}'s ${branch} is wf.at(), a select's: a race answers once, so it takes a timer, wf.timer(name, date)`,
|
|
4209
4466
|
);
|
|
4210
|
-
throw new Error(
|
|
4211
|
-
`${what}'s ${branch} is not a source: a timer, a signal, a participant routed to the code, an agent, or a started turn`,
|
|
4212
|
-
);
|
|
4467
|
+
throw new Error(`${what}'s ${branch} is not a source: a timer, a signal, or an agent`);
|
|
4213
4468
|
}
|
|
4214
4469
|
|
|
4470
|
+
/** The sources only a run of a workflow with events hears. */
|
|
4471
|
+
const HEARD_WITH_EVENTS: ReadonlySet<string> = new Set(["agent:ended", "participant:reply"]);
|
|
4472
|
+
|
|
4215
4473
|
/** One of the workflow's own events, by name, as a source. */
|
|
4216
|
-
function signalSource(name: string): unknown {
|
|
4217
|
-
if (typeof name !== "string"
|
|
4218
|
-
throw new Error(
|
|
4219
|
-
|
|
4474
|
+
function signalSource(name: string, declared: ReadonlySet<string>): unknown {
|
|
4475
|
+
if (typeof name !== "string")
|
|
4476
|
+
throw new Error(
|
|
4477
|
+
`a signal is one of the workflow's declared events, by name; not ${String(name)}`,
|
|
4478
|
+
);
|
|
4479
|
+
if (!declared.has(name))
|
|
4480
|
+
throw new Error(
|
|
4481
|
+
declared.size === 0
|
|
4482
|
+
? `signal ${name}: the workflow declares no events`
|
|
4483
|
+
: `signal ${name}: the workflow declares no such event; its events are ${[...declared].join(", ")}`,
|
|
4484
|
+
);
|
|
4485
|
+
const awf = awfEventOf(name);
|
|
4486
|
+
if (awf?.kind === "ended")
|
|
4487
|
+
throw new Error(
|
|
4488
|
+
`signal ${name}: an agent's end is waited on by naming the agent, { done: ${awf.key} }, not a signal`,
|
|
4489
|
+
);
|
|
4490
|
+
// The mailbox keeps a reply under its record name, from the participant's key.
|
|
4491
|
+
const take = awf ? { name: "participant:reply", from: awf.key } : { name };
|
|
4492
|
+
return signalOf({ name }, take);
|
|
4220
4493
|
}
|
|
4221
4494
|
|
|
4222
4495
|
/** Timer `name`'s source. */
|
|
@@ -4232,11 +4505,14 @@ function turnRef(
|
|
|
4232
4505
|
agent: string,
|
|
4233
4506
|
id: string,
|
|
4234
4507
|
cancel: (reason?: string) => Promise<boolean>,
|
|
4508
|
+
deprecated: (name: Deprecated) => void,
|
|
4235
4509
|
): StartedTurn {
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4510
|
+
const take = { name: "agent:ended", from: agent, turn: id };
|
|
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;
|
|
4240
4516
|
}
|
|
4241
4517
|
|
|
4242
4518
|
class ExecutionScope {
|
|
@@ -4439,20 +4715,41 @@ function reconcile<T extends JsonValue>(
|
|
|
4439
4715
|
): TurnOutcome<T> {
|
|
4440
4716
|
switch (native.state) {
|
|
4441
4717
|
case "completed":
|
|
4442
|
-
return {
|
|
4718
|
+
return {
|
|
4719
|
+
answered: false,
|
|
4720
|
+
kind: "unanswered",
|
|
4721
|
+
reason: "agent settled without an accepted result",
|
|
4722
|
+
usage,
|
|
4723
|
+
};
|
|
4443
4724
|
case "blocked":
|
|
4444
|
-
return {
|
|
4725
|
+
return {
|
|
4726
|
+
answered: false,
|
|
4727
|
+
kind: "blocked",
|
|
4728
|
+
reason: native.detail ?? "agent is blocked",
|
|
4729
|
+
usage,
|
|
4730
|
+
};
|
|
4445
4731
|
case "timed-out":
|
|
4446
|
-
return {
|
|
4732
|
+
return {
|
|
4733
|
+
answered: false,
|
|
4734
|
+
kind: "timed-out",
|
|
4735
|
+
reason: native.detail ?? "operation timed out",
|
|
4736
|
+
usage,
|
|
4737
|
+
};
|
|
4447
4738
|
case "failed":
|
|
4448
4739
|
return {
|
|
4740
|
+
answered: false,
|
|
4449
4741
|
kind: "failed",
|
|
4450
4742
|
reason: native.detail ?? "harness operation failed",
|
|
4451
4743
|
retryable: false,
|
|
4452
4744
|
usage,
|
|
4453
4745
|
};
|
|
4454
4746
|
case "cancelled":
|
|
4455
|
-
return {
|
|
4747
|
+
return {
|
|
4748
|
+
answered: false,
|
|
4749
|
+
kind: "cancelled",
|
|
4750
|
+
reason: native.detail ?? "operation cancelled",
|
|
4751
|
+
usage,
|
|
4752
|
+
};
|
|
4456
4753
|
}
|
|
4457
4754
|
}
|
|
4458
4755
|
|
|
@@ -4486,6 +4783,7 @@ type OperationLog = {
|
|
|
4486
4783
|
|
|
4487
4784
|
/** A native fork, and the parent's operation it was copied after: null when it ran none. */
|
|
4488
4785
|
type TakenFork = { native: NativeFork; after: string | null };
|
|
4786
|
+
type TakeFork = (deadline: AbsoluteDeadline | undefined, into?: SessionCopy) => Promise<TakenFork>;
|
|
4489
4787
|
|
|
4490
4788
|
/** A settled turn or compaction, kept in the run's `turns.jsonl`. */
|
|
4491
4789
|
type RecordTurn = (outcome: TurnOutcome<JsonValue>, kind: TurnRecord["kind"]) => void;
|
|
@@ -4566,6 +4864,11 @@ function resultSchema<T extends JsonValue>(schema: OutputSchema<T> | undefined):
|
|
|
4566
4864
|
return schema ? parseJsonSchema(schema) : { type: "string" };
|
|
4567
4865
|
}
|
|
4568
4866
|
|
|
4867
|
+
function resultSaysQuoting(schema: JsonSchema): boolean {
|
|
4868
|
+
const simple = simpleInput(schema);
|
|
4869
|
+
return simple !== undefined && takesText(simple);
|
|
4870
|
+
}
|
|
4871
|
+
|
|
4569
4872
|
function operationPrompt(
|
|
4570
4873
|
prompt: string,
|
|
4571
4874
|
schema: JsonSchema,
|
|
@@ -4576,7 +4879,7 @@ function operationPrompt(
|
|
|
4576
4879
|
// A quoted heredoc passes the JSON through untouched: a quoted argument broke on shell quoting
|
|
4577
4880
|
// (story 002). Not indented: a copied terminator with leading spaces never closes the heredoc.
|
|
4578
4881
|
// The schema itself, not a rendering of it: without its bounds, first answers were 0/160 valid
|
|
4579
|
-
// (E5). `
|
|
4882
|
+
// (E5). `wfa` says what to do about a rejection, so the prompt does not. A simple schema is taught
|
|
4580
4883
|
// as flags instead, its usage line carrying every bound, which held them as well (story 035, M1).
|
|
4581
4884
|
const simple = simpleInput(schema);
|
|
4582
4885
|
if (simple) {
|
|
@@ -4586,8 +4889,7 @@ function operationPrompt(
|
|
|
4586
4889
|
"When the answer is ready, return it by running:",
|
|
4587
4890
|
"",
|
|
4588
4891
|
`${launcher} result ${callId} ${usageLine(simple)}`,
|
|
4589
|
-
"",
|
|
4590
|
-
QUOTING_RULE,
|
|
4892
|
+
...(takesText(simple) ? ["", QUOTING_RULE] : []),
|
|
4591
4893
|
].join("\n");
|
|
4592
4894
|
}
|
|
4593
4895
|
return [
|
|
@@ -4646,46 +4948,101 @@ function withKnownEffort(execution: AgentExecution): AgentExecution {
|
|
|
4646
4948
|
/** What `set` changes. */
|
|
4647
4949
|
type SettingsChange = Partial<Pick<AgentExecution, "model" | "effort">>;
|
|
4648
4950
|
|
|
4649
|
-
const SETTINGS_FIELDS = new Set(["id", "model", "effort", "deadline", "timeoutMs"]);
|
|
4951
|
+
const SETTINGS_FIELDS = new Set(["id", "model", "effort", "timeout", "deadline", "timeoutMs"]);
|
|
4650
4952
|
|
|
4651
4953
|
function sessionSettings({ model, effort }: AgentExecution): SessionSettings {
|
|
4652
4954
|
return { model, ...(effort === undefined ? {} : { effort }) };
|
|
4653
4955
|
}
|
|
4654
4956
|
|
|
4655
4957
|
/**
|
|
4656
|
-
* A
|
|
4958
|
+
* A TUI is the default and is left unsaid, and `metered` is kept only where it means something,
|
|
4657
4959
|
* so the same agent always has one identity.
|
|
4658
4960
|
*/
|
|
4659
4961
|
function storedPlacement(choice: PlacementChoice): PlacementChoice {
|
|
4660
|
-
|
|
4962
|
+
knownPlacement(choice);
|
|
4963
|
+
return placementOf(choice) === "headless"
|
|
4964
|
+
? { placement: "headless", ...(choice.metered === true ? { metered: true as const } : {}) }
|
|
4965
|
+
: {};
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
/** A workflow is untyped JavaScript at run time; a misspelt placement must not become a TUI. */
|
|
4969
|
+
function knownPlacement(choice: PlacementChoice): void {
|
|
4661
4970
|
if (
|
|
4662
4971
|
choice.placement !== undefined &&
|
|
4663
|
-
choice.placement !== "
|
|
4972
|
+
choice.placement !== "tui" &&
|
|
4664
4973
|
choice.placement !== "headless"
|
|
4665
4974
|
) {
|
|
4975
|
+
const renamed = (choice.placement as string) === "pane" ? ` ("pane" is now "tui")` : "";
|
|
4666
4976
|
throw new Error(
|
|
4667
|
-
`unknown placement ${JSON.stringify(choice.placement)}; expected
|
|
4977
|
+
`unknown placement ${JSON.stringify(choice.placement)}; expected tui or headless${renamed}`,
|
|
4668
4978
|
);
|
|
4669
4979
|
}
|
|
4670
|
-
return placementOf(choice) === "headless"
|
|
4671
|
-
? { placement: "headless", ...(choice.metered === true ? { metered: true as const } : {}) }
|
|
4672
|
-
: {};
|
|
4673
4980
|
}
|
|
4674
4981
|
|
|
4675
4982
|
/**
|
|
4676
4983
|
* The parent's harness, model and effort, in the placement and at the effort the fork names, else
|
|
4677
4984
|
* its parent's.
|
|
4678
4985
|
*/
|
|
4679
|
-
function forkExecution(parent: AgentExecution,
|
|
4986
|
+
function forkExecution(parent: AgentExecution, runtime: ForkRuntime = {}): AgentExecution {
|
|
4680
4987
|
const { placement: _placement, metered: _metered, ...target } = parent;
|
|
4681
|
-
const choice =
|
|
4988
|
+
const choice =
|
|
4989
|
+
runtime.placement === undefined && runtime.metered === undefined ? parent : runtime;
|
|
4682
4990
|
return {
|
|
4683
4991
|
...target,
|
|
4684
|
-
...(
|
|
4992
|
+
...(runtime.effort === undefined ? {} : { effort: runtime.effort }),
|
|
4685
4993
|
...storedPlacement(choice),
|
|
4686
4994
|
};
|
|
4687
4995
|
}
|
|
4688
4996
|
|
|
4997
|
+
const FORK_RUNTIME_FIELDS = ["placement", "metered", "effort"] as const;
|
|
4998
|
+
|
|
4999
|
+
/**
|
|
5000
|
+
* A fork's spec with its runtime written one way, `runtime`, so a repeat in either form compares
|
|
5001
|
+
* equal. The flat fields are deprecated, and refused beside `runtime` rather than merged with it.
|
|
5002
|
+
*/
|
|
5003
|
+
function forkSpecOf(spec: CallerForkSpec, deprecated: () => void): CallerForkSpec {
|
|
5004
|
+
const { placement, metered, effort, runtime, ...rest } = spec;
|
|
5005
|
+
const flat = definedFields({ placement, metered, effort });
|
|
5006
|
+
if (Object.keys(flat).length > 0) {
|
|
5007
|
+
if (runtime !== undefined) {
|
|
5008
|
+
throw new Error(
|
|
5009
|
+
`agent ${spec.key}: a fork's placement, metered and effort go in its runtime, not beside it`,
|
|
5010
|
+
);
|
|
5011
|
+
}
|
|
5012
|
+
deprecated();
|
|
5013
|
+
return { ...rest, runtime: flat };
|
|
5014
|
+
}
|
|
5015
|
+
if (runtime === undefined) return rest;
|
|
5016
|
+
if (typeof runtime !== "object" || runtime === null || Array.isArray(runtime)) {
|
|
5017
|
+
throw new Error(
|
|
5018
|
+
`agent ${spec.key}: a fork's runtime is { placement?, metered?, effort? }, not ${JSON.stringify(runtime)}`,
|
|
5019
|
+
);
|
|
5020
|
+
}
|
|
5021
|
+
// A workflow is untyped JavaScript at run time; a harness or model asked for must not be dropped.
|
|
5022
|
+
const others = Object.keys(runtime).filter(
|
|
5023
|
+
(field) =>
|
|
5024
|
+
!(FORK_RUNTIME_FIELDS as readonly string[]).includes(field) &&
|
|
5025
|
+
(runtime as Record<string, unknown>)[field] !== undefined,
|
|
5026
|
+
);
|
|
5027
|
+
if (others.length > 0) {
|
|
5028
|
+
throw new Error(
|
|
5029
|
+
`agent ${spec.key}: a fork continues its parent's session on its harness and model; its runtime takes placement, metered and effort, not ${others.join(", ")}`,
|
|
5030
|
+
);
|
|
5031
|
+
}
|
|
5032
|
+
const given = definedFields({
|
|
5033
|
+
placement: runtime.placement,
|
|
5034
|
+
metered: runtime.metered,
|
|
5035
|
+
effort: runtime.effort,
|
|
5036
|
+
});
|
|
5037
|
+
return Object.keys(given).length === 0 ? rest : { ...rest, runtime: given };
|
|
5038
|
+
}
|
|
5039
|
+
|
|
5040
|
+
function definedFields<T extends object>(fields: T): Partial<T> {
|
|
5041
|
+
return Object.fromEntries(
|
|
5042
|
+
Object.entries(fields).filter(([, value]) => value !== undefined),
|
|
5043
|
+
) as Partial<T>;
|
|
5044
|
+
}
|
|
5045
|
+
|
|
4689
5046
|
function constrainExistingExecution(
|
|
4690
5047
|
selection: RuntimeSelection,
|
|
4691
5048
|
existing: AgentExecution,
|
|
@@ -4710,6 +5067,7 @@ function constrainExistingExecution(
|
|
|
4710
5067
|
if (selection.effort !== undefined && selection.effort !== existing.effort) {
|
|
4711
5068
|
throw new Error("existing agent was opened at a different effort");
|
|
4712
5069
|
}
|
|
5070
|
+
knownPlacement(selection);
|
|
4713
5071
|
const placed = placementOf(existing);
|
|
4714
5072
|
if (
|
|
4715
5073
|
(selection.placement !== undefined && placementOf(selection) !== placed) ||
|
|
@@ -4745,23 +5103,13 @@ function assertCompatibleAgent(
|
|
|
4745
5103
|
if (spec.skills !== undefined && !isDeepStrictEqual(existing.skills, requested.skills)) {
|
|
4746
5104
|
conflict(key, "skills");
|
|
4747
5105
|
}
|
|
4748
|
-
for (const field of ["instructions", "labels", "layout", "keepPane"] as const) {
|
|
5106
|
+
for (const field of ["instructions", "labels", "pane", "layout", "keepPane"] as const) {
|
|
4749
5107
|
if (spec[field] !== undefined && !isDeepStrictEqual(existing[field], requested[field])) {
|
|
4750
5108
|
conflict(key, field);
|
|
4751
5109
|
}
|
|
4752
5110
|
}
|
|
4753
5111
|
}
|
|
4754
5112
|
|
|
4755
|
-
function paneOptions({
|
|
4756
|
-
layout,
|
|
4757
|
-
keepPane,
|
|
4758
|
-
}: AgentIdentity): Pick<HarnessActivation, "layout" | "keepPane"> {
|
|
4759
|
-
return {
|
|
4760
|
-
...(layout === undefined ? {} : { layout }),
|
|
4761
|
-
...(keepPane === undefined ? {} : { keepPane }),
|
|
4762
|
-
};
|
|
4763
|
-
}
|
|
4764
|
-
|
|
4765
5113
|
function conflict(key: string, field: string): never {
|
|
4766
5114
|
throw new Error(`agent ${key} is already open with different ${field}`);
|
|
4767
5115
|
}
|
|
@@ -4770,10 +5118,6 @@ function stoppedError(key: string, reason: string): Error {
|
|
|
4770
5118
|
return new Error(`agent ${key} was stopped (${reason}); its key is not opened again`);
|
|
4771
5119
|
}
|
|
4772
5120
|
|
|
4773
|
-
function unavailable(name: string): Promise<never> {
|
|
4774
|
-
return Promise.reject(new Error(`${name} is not implemented by the minimum workflow runner`));
|
|
4775
|
-
}
|
|
4776
|
-
|
|
4777
5121
|
/**
|
|
4778
5122
|
* Each root agent's last native session in a run's turns, where its harness can resume it: what a
|
|
4779
5123
|
* event run's continue reopens it on.
|
|
@@ -4796,4 +5140,25 @@ type LogicalAgentTurns = {
|
|
|
4796
5140
|
begin(turn: TurnId, meta: JsonValue, execution: AgentExecution): Promise<void>;
|
|
4797
5141
|
end(turn: TurnId, meta: JsonValue, outcome: TurnOutcome<JsonValue>): Promise<void>;
|
|
4798
5142
|
restored(): RestoredTurn | undefined;
|
|
5143
|
+
/** Throws, or logs the deprecation, unless the workflow declares this agent's `{key}:ended`. */
|
|
5144
|
+
checkDeclared(): void;
|
|
4799
5145
|
};
|
|
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
|
+
}
|