@agentswf/engine 0.1.0-beta.2 → 0.1.0-beta.4
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 +571 -0
- package/src/agent-cli/client.ts +115 -0
- package/src/agent-launcher.ts +15 -25
- package/src/allowance-command.ts +31 -21
- package/src/attempt-ending.ts +23 -5
- package/src/attempt-output.ts +48 -18
- package/src/attempt-view.ts +25 -15
- package/src/commands.ts +176 -0
- package/src/control-plane.ts +73 -22
- package/src/coordinator-port.ts +29 -0
- package/src/coordinator-prompt.ts +237 -0
- package/src/coordinator-socket.ts +236 -0
- package/src/deadlines.ts +47 -10
- package/src/decisions/directory.ts +8 -6
- package/src/duration.ts +29 -0
- package/src/emit-command.ts +119 -0
- package/src/emit.ts +246 -0
- package/src/files.ts +11 -1
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +13 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +13 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +44 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/here.ts +9 -3
- package/src/hold-run-lock.fixture.ts +8 -0
- package/src/holder-command.ts +279 -0
- package/src/inbox-command.ts +715 -0
- package/src/init-command.ts +29 -11
- package/src/init-template/AGENTS.md.template +4 -4
- package/src/init-template/hello/workflow.test.ts.template +2 -2
- package/src/init-template/hello/workflow.ts.template +16 -20
- package/src/keep-alive.ts +40 -0
- package/src/logs-command.ts +106 -35
- package/src/machine.ts +12 -1
- package/src/mailbox.ts +990 -0
- package/src/message-operation.ts +26 -3
- package/src/messaging/limits.ts +1 -1
- package/src/messaging/messaging.ts +478 -85
- package/src/messaging/routes.ts +11 -4
- package/src/messaging/text.ts +25 -22
- package/src/operation-liveness.ts +55 -9
- package/src/operator-cli.ts +290 -119
- package/src/operator-runtime.ts +8 -8
- package/src/progress-view.ts +49 -7
- package/src/projects.ts +57 -0
- package/src/result-slots.ts +43 -19
- package/src/result-validation.ts +18 -27
- package/src/run-command.ts +29 -66
- package/src/run-commands.ts +13 -2
- package/src/run-lock.ts +123 -0
- package/src/run-prepare.ts +94 -22
- package/src/run-state.ts +125 -6
- package/src/run-usage.ts +2 -2
- package/src/run-view.ts +0 -9
- package/src/runs-command.ts +43 -25
- package/src/runs.ts +142 -23
- package/src/sandboxes.ts +11 -4
- package/src/simple-input.ts +268 -0
- package/src/socket-exchange.ts +26 -0
- package/src/sources.ts +70 -0
- package/src/stage-ledger.ts +89 -14
- package/src/stage-plan.ts +23 -0
- package/src/stop-crashed.ts +39 -0
- package/src/test-command.ts +34 -11
- package/src/testing.ts +72 -4
- package/src/tick-command.ts +76 -0
- package/src/tools.ts +645 -0
- package/src/wake.ts +264 -0
- package/src/workflow-loader.ts +32 -22
- package/src/workflow-runner.ts +1277 -97
- package/src/workflow-testing/coordinator.typecheck.ts +54 -0
- package/src/workflow-testing/coordinators.ts +137 -0
- package/src/workflow-testing/host.ts +67 -7
- package/src/workflow-testing/index.ts +488 -34
- package/src/workflow-testing/script.ts +74 -4
- package/src/workflow-testing/workflow-testing.typecheck.ts +19 -2
package/src/workflow-runner.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { mkdir, realpath, rm } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { dirname, join, relative } from "node:path";
|
|
5
6
|
import { isDeepStrictEqual } from "node:util";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
PARTICIPANT,
|
|
9
|
+
participantRef,
|
|
10
|
+
RESERVED_PARTICIPANT_KEYS,
|
|
11
|
+
} from "@agentswf/contract/participant";
|
|
7
12
|
import type {
|
|
8
13
|
AgentPaneRecord,
|
|
9
14
|
AgentSkillsRecord,
|
|
@@ -16,6 +21,7 @@ import {
|
|
|
16
21
|
TURN_OPERATION_FIELDS,
|
|
17
22
|
TURN_RECORD_VERSION,
|
|
18
23
|
type TurnRecord,
|
|
24
|
+
type WaitTake,
|
|
19
25
|
} from "@agentswf/contract/records";
|
|
20
26
|
import {
|
|
21
27
|
formatErrors,
|
|
@@ -25,6 +31,7 @@ import {
|
|
|
25
31
|
} from "@agentswf/contract/schema";
|
|
26
32
|
import {
|
|
27
33
|
type AbsoluteDeadline,
|
|
34
|
+
type AgentEndedEvent,
|
|
28
35
|
type AgentExecution,
|
|
29
36
|
type AgentForkSpec,
|
|
30
37
|
type AgentKey,
|
|
@@ -32,35 +39,59 @@ import {
|
|
|
32
39
|
type AgentRef,
|
|
33
40
|
type AgentRunStructuredSpec,
|
|
34
41
|
type AgentRunTextSpec,
|
|
35
|
-
type
|
|
36
|
-
type
|
|
42
|
+
type AgentStartSpec,
|
|
43
|
+
type Branches,
|
|
44
|
+
type CodeParticipantRef,
|
|
37
45
|
type CompactionId,
|
|
38
46
|
type CompactSpec,
|
|
39
47
|
DeadlineExceededError,
|
|
48
|
+
type Duration,
|
|
49
|
+
type EmitOutcome,
|
|
50
|
+
type EventAgentOpenSpec,
|
|
51
|
+
type EventAgentRef,
|
|
52
|
+
type HarnessAllowance,
|
|
53
|
+
isEventWorkflow,
|
|
40
54
|
isJsonValue,
|
|
41
55
|
type JsonObject,
|
|
42
56
|
type JsonValue,
|
|
43
57
|
type KeepPane,
|
|
58
|
+
type Matched,
|
|
44
59
|
type MessageAccess,
|
|
60
|
+
type Next,
|
|
61
|
+
nameProblem,
|
|
45
62
|
type OperationRecord,
|
|
46
63
|
type OutputSchema,
|
|
47
64
|
type PaneLayout,
|
|
65
|
+
type ParticipantRef,
|
|
66
|
+
type ParticipantSpec,
|
|
48
67
|
type PlacementChoice,
|
|
49
68
|
placementOf,
|
|
69
|
+
type RaceBranches,
|
|
70
|
+
type ReceiveOptions,
|
|
50
71
|
type RunResult,
|
|
51
72
|
type RuntimeSelection,
|
|
73
|
+
type SelectOptions,
|
|
52
74
|
type SettingsSpec,
|
|
53
75
|
type SkillSource,
|
|
54
76
|
type StageOptions,
|
|
55
77
|
type StageSummary,
|
|
78
|
+
type StartedTurn,
|
|
79
|
+
type TimeRead,
|
|
80
|
+
type Timer,
|
|
81
|
+
type TurnId,
|
|
56
82
|
type TurnOutcome,
|
|
83
|
+
type WaitOptions,
|
|
84
|
+
type When,
|
|
57
85
|
type WorkflowContext,
|
|
58
86
|
type WorkflowDefinition,
|
|
87
|
+
type WorkflowEvent,
|
|
59
88
|
} from "@agentswf/contract/workflow";
|
|
60
89
|
import {
|
|
61
90
|
type AgentSkills,
|
|
62
91
|
effortRefusal,
|
|
63
92
|
findHarness,
|
|
93
|
+
HARNESS_NAMES,
|
|
94
|
+
type Harness,
|
|
64
95
|
hostHome,
|
|
65
96
|
settingsRefusal,
|
|
66
97
|
skillsLayout,
|
|
@@ -87,11 +118,13 @@ import {
|
|
|
87
118
|
type ResultControlPlane,
|
|
88
119
|
startResultControlPlane,
|
|
89
120
|
} from "./control-plane";
|
|
121
|
+
import type { CoordinatorPort } from "./coordinator-port";
|
|
90
122
|
import {
|
|
91
123
|
assertDeadline,
|
|
92
124
|
assertDeadlineValue,
|
|
93
125
|
deadlineWithin,
|
|
94
126
|
earlierDeadline,
|
|
127
|
+
harnessDeadline,
|
|
95
128
|
runUntilStopped,
|
|
96
129
|
scheduleAt,
|
|
97
130
|
WorkflowCancelledError,
|
|
@@ -99,9 +132,28 @@ import {
|
|
|
99
132
|
} from "./deadlines";
|
|
100
133
|
import { RunDecisions } from "./decisions/directory";
|
|
101
134
|
import type { DecisionInstallation } from "./decisions/seam";
|
|
135
|
+
import { parseWhen } from "./duration";
|
|
136
|
+
import { emitEvent } from "./emit";
|
|
102
137
|
import { messageOf } from "./errors";
|
|
138
|
+
import {
|
|
139
|
+
asTimeoutHandler,
|
|
140
|
+
checkedKeepAlive,
|
|
141
|
+
DEFAULT_WINDOW_MS,
|
|
142
|
+
inTimeoutHandler,
|
|
143
|
+
type KeepAlive,
|
|
144
|
+
} from "./keep-alive";
|
|
145
|
+
import {
|
|
146
|
+
AT_DUE,
|
|
147
|
+
type RestoredTurn,
|
|
148
|
+
RunMailbox,
|
|
149
|
+
RunParked,
|
|
150
|
+
recoverTurns,
|
|
151
|
+
type TurnEnd,
|
|
152
|
+
unrunUsage,
|
|
153
|
+
} from "./mailbox";
|
|
103
154
|
import { superviseMessageOperation } from "./message-operation";
|
|
104
155
|
import { canAwaitReply } from "./messaging/limits";
|
|
156
|
+
import type { HolderRequest } from "./messaging/messaging";
|
|
105
157
|
import { type AgentView, type Delivery, Messaging, type OperationEnd } from "./messaging/messaging";
|
|
106
158
|
import type { Scope as MessagingScope } from "./messaging/routes";
|
|
107
159
|
import { createOperationEvents } from "./operation-events";
|
|
@@ -129,7 +181,7 @@ import {
|
|
|
129
181
|
type OperationTags,
|
|
130
182
|
type RunLedger,
|
|
131
183
|
} from "./run-usage";
|
|
132
|
-
import { endTurnsLine, readStageRecords } from "./runs";
|
|
184
|
+
import { endTurnsLine, readStageRecords, readTurns } from "./runs";
|
|
133
185
|
import { type CredentialLocks, placeCarried, seedHome } from "./sandbox-homes";
|
|
134
186
|
import {
|
|
135
187
|
RUN_SANDBOX_ONLY,
|
|
@@ -137,15 +189,23 @@ import {
|
|
|
137
189
|
type RunSandboxOptions,
|
|
138
190
|
type SeatedAgent,
|
|
139
191
|
} from "./sandboxes";
|
|
192
|
+
import { QUOTING_RULE, simpleInput, takesText, usageLine } from "./simple-input";
|
|
140
193
|
import { placeSkills, RunSkills, readSkillSources } from "./skills/run-skills";
|
|
194
|
+
import { asSource, branchOf, branchValue, source, takeOf } from "./sources";
|
|
141
195
|
import { StageLedger, type StageProgress } from "./stage-ledger";
|
|
142
196
|
import { FromStageUnreached } from "./stopped";
|
|
197
|
+
import { inToolHandler, outsideTools, refusedInTool, ToolCalls, toolsLesson } from "./tools";
|
|
198
|
+
import type { EventWorkflow } from "./workflow-loader";
|
|
143
199
|
|
|
144
200
|
export { WorkflowCancelledError } from "./deadlines";
|
|
145
201
|
|
|
146
202
|
export type RunWorkflowOptions = {
|
|
147
203
|
/** Internal timing policy; workflow authors use the operation deadline. */
|
|
148
204
|
livenessPolicy?: LivenessPolicy;
|
|
205
|
+
/** A `keepAlive` window that gives no `timeoutMs`; absent, an hour. A test's is short. */
|
|
206
|
+
defaultWindowMs?: number;
|
|
207
|
+
/** The run's time, which its timers and `wf.now()` read; absent, the clock's. A test's is virtual. */
|
|
208
|
+
now?: () => number;
|
|
149
209
|
runRoot: string;
|
|
150
210
|
/** The run this is an attempt of, whose folder the operator claimed under `runRoot`. */
|
|
151
211
|
run: {
|
|
@@ -158,9 +218,30 @@ export type RunWorkflowOptions = {
|
|
|
158
218
|
fromStage?: string;
|
|
159
219
|
/** `--values`: the values of stages before `fromStage` that have no record to reuse. */
|
|
160
220
|
values?: ReadonlyMap<string, JsonValue>;
|
|
221
|
+
/** A run of a workflow with `events`. */
|
|
222
|
+
events?: true;
|
|
223
|
+
/** With `events`: runs `work` holding the run's lock, as every append to the mailbox is made. */
|
|
224
|
+
locked?<T>(work: () => Promise<T>): Promise<T>;
|
|
225
|
+
/**
|
|
226
|
+
* With `events`: false when the attempt before this one has no ending, its process gone with its
|
|
227
|
+
* agents perhaps still running: its sessions aren't reopened until they are stopped (Q17).
|
|
228
|
+
*/
|
|
229
|
+
resumeSessions?: boolean;
|
|
230
|
+
/** With `events`: calls `nudge` whenever awf tells the run its mailbox has a new event. */
|
|
231
|
+
listenNudges?(nudge: () => void): void;
|
|
232
|
+
/**
|
|
233
|
+
* Writes the attempt's `parked` ending, which the run's wait calls holding the run's lock: an
|
|
234
|
+
* event run's, or a test's. Absent, the run never parks and waits in its process.
|
|
235
|
+
*/
|
|
236
|
+
park?(wait: { label?: string; until?: string }): Promise<void>;
|
|
237
|
+
/** With `park`: the wait stays live to `until` for an agent's window instead. A test's hook. */
|
|
238
|
+
live?(until: number): Promise<void>;
|
|
239
|
+
/** With `park`: moves a virtual clock to the timer due at `until` rather than parking. A test's hook. */
|
|
240
|
+
skip?(until: number): Promise<boolean>;
|
|
161
241
|
};
|
|
162
242
|
runtime: AgentRuntimeConfig;
|
|
163
|
-
deadline
|
|
243
|
+
/** The run's deadline; absent, it has none, and only deadlines the workflow sets bound it. */
|
|
244
|
+
deadline?: AbsoluteDeadline;
|
|
164
245
|
cwd?: string;
|
|
165
246
|
onLog?: (message: string, fields?: JsonObject) => void;
|
|
166
247
|
/** The operator hands the session back once agents.stop has released it. */
|
|
@@ -172,13 +253,36 @@ export type RunWorkflowOptions = {
|
|
|
172
253
|
skillCache?: string;
|
|
173
254
|
/** The decision models workflows may ask; without them, `decisions.decide` is refused. */
|
|
174
255
|
decisions?: DecisionInstallation;
|
|
256
|
+
/** How `wf.allowance` reads a harness's plan; without it, every harness reads `none`. */
|
|
257
|
+
allowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
175
258
|
/**
|
|
176
259
|
* Where an agent may `--await-reply`: the engine's list by harness and placement unless given. A
|
|
177
260
|
* workflow's test allows it everywhere, as the fake harness can.
|
|
178
261
|
*/
|
|
179
262
|
awaitReply?: (execution: AgentExecution, confirmsDelivery: boolean) => boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Whatever answers for the run's outside participants, each told everything: `awf run`'s prompt,
|
|
265
|
+
* the socket `awf reply` and `awf send` reach, or a test's scripts. Unsupported outside the engine.
|
|
266
|
+
* @internal
|
|
267
|
+
*/
|
|
268
|
+
coordinators?: readonly CoordinatorPort[];
|
|
269
|
+
/**
|
|
270
|
+
* Sends `wf.emit`'s event through the one way in; absent, `emitEvent` on this machine. A
|
|
271
|
+
* test records them instead.
|
|
272
|
+
*/
|
|
273
|
+
emit?: Emitter;
|
|
180
274
|
};
|
|
181
275
|
|
|
276
|
+
/** Sends an event from a run to its target. */
|
|
277
|
+
export type Emitter = (
|
|
278
|
+
target: EventWorkflow,
|
|
279
|
+
event: { run: string; name: string; data: JsonValue; id?: string },
|
|
280
|
+
from: string,
|
|
281
|
+
) => Promise<EmitOutcome>;
|
|
282
|
+
|
|
283
|
+
const machineEmitter: Emitter = async (target, event, from) =>
|
|
284
|
+
(await emitEvent(target, event, { from, home: homedir(), environment: process.env })).outcome;
|
|
285
|
+
|
|
182
286
|
/** What a run is known by once it has ended, whether or not it succeeded. */
|
|
183
287
|
export type SettledRun = {
|
|
184
288
|
runId: string;
|
|
@@ -263,6 +367,8 @@ export type WorkflowRunSnapshot = {
|
|
|
263
367
|
key: AgentKey;
|
|
264
368
|
/** Messages held for it and not yet shown; absent when none are. */
|
|
265
369
|
queued?: number;
|
|
370
|
+
/** Idle with `keepAlive`, the end of its window: the run stays live to it. */
|
|
371
|
+
liveUntil?: number;
|
|
266
372
|
execution: AgentExecution;
|
|
267
373
|
state:
|
|
268
374
|
| "starting"
|
|
@@ -291,6 +397,8 @@ type AgentEntry = {
|
|
|
291
397
|
stopping?: Promise<boolean>;
|
|
292
398
|
stopComplete?: boolean;
|
|
293
399
|
releasedAt?: number;
|
|
400
|
+
/** As the open last gave it: a reopen that gives one replaces it. */
|
|
401
|
+
keepAlive?: KeepAlive;
|
|
294
402
|
/** How it was opened, or found for the calling session, which its forks reuse. */
|
|
295
403
|
opening?: {
|
|
296
404
|
sessions: AgentSessions;
|
|
@@ -345,6 +453,11 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
345
453
|
): Promise<WorkflowRunHandle<Result>> {
|
|
346
454
|
assertDeadline(options.deadline);
|
|
347
455
|
if (options.signal?.aborted) throw new WorkflowCancelledError(options.signal.reason);
|
|
456
|
+
// Before anything opens, so a refusal leaves nothing to close.
|
|
457
|
+
const withEvents = isEventWorkflow(definition);
|
|
458
|
+
const locked = options.run.locked ?? (<T>(work: () => Promise<T>) => work());
|
|
459
|
+
if (withEvents && (options.run.fromStage !== undefined || options.run.values))
|
|
460
|
+
throw new Error("a run of a workflow with events takes no --from-stage or --values");
|
|
348
461
|
const { dir: runDir, id: runId, attempt } = options.run;
|
|
349
462
|
const startedAt = Date.now();
|
|
350
463
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -369,7 +482,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
369
482
|
runId,
|
|
370
483
|
...(options.run.label === undefined ? {} : { label: options.run.label }),
|
|
371
484
|
cwd,
|
|
372
|
-
deadline: options.deadline,
|
|
485
|
+
deadline: harnessDeadline(options.deadline),
|
|
373
486
|
}),
|
|
374
487
|
);
|
|
375
488
|
try {
|
|
@@ -421,6 +534,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
421
534
|
...(options.run.fromStage === undefined ? {} : { fromStage: options.run.fromStage }),
|
|
422
535
|
...(options.run.values === undefined ? {} : { values: options.run.values }),
|
|
423
536
|
...(definition.meta.version === undefined ? {} : { workflowVersion: definition.meta.version }),
|
|
537
|
+
...(withEvents ? { byContent: true } : {}),
|
|
424
538
|
turns: () => turns,
|
|
425
539
|
event: (body, at) => events.event(body, at),
|
|
426
540
|
});
|
|
@@ -461,9 +575,47 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
461
575
|
});
|
|
462
576
|
const fatal = Promise.withResolvers<never>();
|
|
463
577
|
void fatal.promise.catch(() => undefined);
|
|
464
|
-
const
|
|
578
|
+
const now = options.now ?? Date.now;
|
|
579
|
+
// A turn an earlier attempt started and never ended, its process gone, ends failed before the code
|
|
580
|
+
// runs again (Q6); the turns whose ends no wait took are their agents' turns again.
|
|
581
|
+
const restoredTurns = withEvents ? await recoverTurns(runDir, attempt, locked, now) : undefined;
|
|
582
|
+
// An event run's agents reopen on the sessions its earlier attempts' turns ran on.
|
|
583
|
+
const keptSessions =
|
|
584
|
+
withEvents && options.run.resumeSessions !== false
|
|
585
|
+
? keptSessionsOf(await readTurns(runDir))
|
|
586
|
+
: undefined;
|
|
587
|
+
// Every run's timers live in a mailbox; a run of a workflow with events takes its events there too.
|
|
588
|
+
// A plain run without `park` waits for its timers in its process.
|
|
589
|
+
const waits = new RunMailbox({
|
|
590
|
+
runDir,
|
|
591
|
+
attempt,
|
|
592
|
+
locked,
|
|
593
|
+
now,
|
|
594
|
+
...(options.run.park
|
|
595
|
+
? {
|
|
596
|
+
parking: {
|
|
597
|
+
idle: (stage) => owner.idle(stage),
|
|
598
|
+
timeout: (signal) => owner.timeout(signal),
|
|
599
|
+
liveUntil: () => owner.liveUntil(),
|
|
600
|
+
park: options.run.park,
|
|
601
|
+
...(options.run.live ? { live: options.run.live } : {}),
|
|
602
|
+
...(options.run.skip ? { skip: options.run.skip } : {}),
|
|
603
|
+
},
|
|
604
|
+
}
|
|
605
|
+
: {}),
|
|
606
|
+
});
|
|
607
|
+
await waits.open();
|
|
608
|
+
const mailbox = withEvents ? waits : undefined;
|
|
609
|
+
if (mailbox) options.run.listenNudges?.(() => mailbox.nudge());
|
|
610
|
+
// The mailbox's parking asks the owner, made after it: by then it is.
|
|
611
|
+
const owner: WorkflowOwner = new WorkflowOwner({
|
|
612
|
+
...(mailbox ? { mailbox } : {}),
|
|
613
|
+
waits,
|
|
614
|
+
...(keptSessions ? { keptSessions } : {}),
|
|
615
|
+
...(restoredTurns ? { restoredTurns } : {}),
|
|
465
616
|
failRun: (error) => fatal.reject(error),
|
|
466
617
|
decisions,
|
|
618
|
+
...(options.allowance ? { allowance: options.allowance } : {}),
|
|
467
619
|
sandboxes,
|
|
468
620
|
skills,
|
|
469
621
|
runDir,
|
|
@@ -475,6 +627,7 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
475
627
|
runId,
|
|
476
628
|
attempt,
|
|
477
629
|
cwd,
|
|
630
|
+
now,
|
|
478
631
|
deadline: options.deadline,
|
|
479
632
|
runtime: options.runtime,
|
|
480
633
|
host,
|
|
@@ -486,7 +639,11 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
486
639
|
events,
|
|
487
640
|
onCallerStopped: options.onCallerStopped,
|
|
488
641
|
livenessPolicy: options.livenessPolicy,
|
|
642
|
+
...(options.defaultWindowMs === undefined ? {} : { defaultWindowMs: options.defaultWindowMs }),
|
|
489
643
|
awaitReply: options.awaitReply ?? ((execution, confirms) => canAwaitReply(execution, confirms)),
|
|
644
|
+
coordinators: options.coordinators ?? [],
|
|
645
|
+
emit: options.emit ?? machineEmitter,
|
|
646
|
+
workflow: definition.meta.name,
|
|
490
647
|
});
|
|
491
648
|
const stopped = new AbortController();
|
|
492
649
|
// Aborted by a stop that comes after the body ended; the one that ended it does not cut short the
|
|
@@ -510,6 +667,8 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
510
667
|
return returned;
|
|
511
668
|
}),
|
|
512
669
|
fatal.promise,
|
|
670
|
+
// A park ends the body, even code that caught it.
|
|
671
|
+
waits.whenParked,
|
|
513
672
|
]),
|
|
514
673
|
signal,
|
|
515
674
|
options.deadline,
|
|
@@ -524,6 +683,13 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
524
683
|
failed = true;
|
|
525
684
|
failure = error;
|
|
526
685
|
}
|
|
686
|
+
if (mailbox && !mailbox.parked)
|
|
687
|
+
await owner.endStartedTurns(failed ? "the workflow failed" : "the workflow returned");
|
|
688
|
+
// Once `parked` is written, the attempt is parked, whatever the code did after.
|
|
689
|
+
if (waits.parked) {
|
|
690
|
+
failed = true;
|
|
691
|
+
failure = waits.parked;
|
|
692
|
+
}
|
|
527
693
|
const ending = stages.settle(
|
|
528
694
|
failed ? failure : undefined,
|
|
529
695
|
signal.aborted || failure instanceof DeadlineExceededError,
|
|
@@ -619,10 +785,12 @@ export async function startWorkflow<Args extends JsonValue, Result extends JsonV
|
|
|
619
785
|
messages: known.messages,
|
|
620
786
|
agents: agents.map((agent) => {
|
|
621
787
|
const queued = owner.held(agent.key);
|
|
788
|
+
const liveUntil = owner.liveUntilOf(agent.key);
|
|
622
789
|
return {
|
|
623
790
|
...known.agents.get(agent.key),
|
|
624
791
|
...agent,
|
|
625
792
|
...(queued > 0 ? { queued } : {}),
|
|
793
|
+
...(liveUntil === undefined ? {} : { liveUntil }),
|
|
626
794
|
};
|
|
627
795
|
}),
|
|
628
796
|
};
|
|
@@ -681,7 +849,7 @@ type PlacedSkills = { given: AgentSkills; writeBack?: () => Promise<void> };
|
|
|
681
849
|
type AgentOpening = {
|
|
682
850
|
spec: AgentOpenSpec;
|
|
683
851
|
execution: AgentExecution;
|
|
684
|
-
deadline: AbsoluteDeadline;
|
|
852
|
+
deadline: AbsoluteDeadline | undefined;
|
|
685
853
|
sessions: AgentSessions;
|
|
686
854
|
ledger: AgentLedger;
|
|
687
855
|
/** Its skills' sources, resolved when it opens; absent when it has the operator's. */
|
|
@@ -713,6 +881,7 @@ class WorkflowOwner {
|
|
|
713
881
|
/** Each agent once it has opened, by key: what messaging asks about, and what a ref must be. */
|
|
714
882
|
readonly #logical = new Map<string, LogicalAgent>();
|
|
715
883
|
readonly #messaging: Messaging;
|
|
884
|
+
readonly #tools: ToolCalls<ExecutionScope>;
|
|
716
885
|
/** A stage's execution scope, by its messaging scope: what owns the operations its routes open. */
|
|
717
886
|
readonly #stageScopes = new Map<MessagingScope, ExecutionScope>();
|
|
718
887
|
/** Message operations routes outside any stage opened, which the attempt owns. */
|
|
@@ -722,12 +891,14 @@ class WorkflowOwner {
|
|
|
722
891
|
readonly #inFlight = new Set<Promise<unknown>>();
|
|
723
892
|
#closed = false;
|
|
724
893
|
#closing: Promise<unknown[]> | undefined;
|
|
725
|
-
/** The bundled `
|
|
894
|
+
/** The bundled `wfa`'s source, built once, at the first sandboxed agent. */
|
|
726
895
|
#bundle: Promise<string> | undefined;
|
|
727
896
|
/** The calling session, once the workflow has asked for it (ADR 0010). */
|
|
728
897
|
#caller: { key: string; state: Promise<LogicalAgent> } | undefined;
|
|
729
898
|
/** Stops a fork of the session the run was started from, whose process is not an agent's. */
|
|
730
899
|
readonly #closingRun = new AbortController();
|
|
900
|
+
/** Outside participants the workflow connected, by key: the refs a route's ends must be. */
|
|
901
|
+
readonly #outsideRefs = new Map<string, ParticipantRef>();
|
|
731
902
|
|
|
732
903
|
constructor(
|
|
733
904
|
private readonly options: {
|
|
@@ -736,14 +907,16 @@ class WorkflowOwner {
|
|
|
736
907
|
failRun(error: Error): void;
|
|
737
908
|
runDir: string;
|
|
738
909
|
livenessPolicy?: LivenessPolicy;
|
|
910
|
+
defaultWindowMs?: number;
|
|
739
911
|
locks: CredentialLocks;
|
|
740
912
|
environment: Readonly<Record<string, string | undefined>>;
|
|
741
913
|
decisions: RunDecisions;
|
|
914
|
+
allowance?: (harness: Harness) => Promise<HarnessAllowance>;
|
|
742
915
|
stages: StageLedger;
|
|
743
916
|
runId: string;
|
|
744
917
|
attempt: number;
|
|
745
918
|
cwd: string;
|
|
746
|
-
deadline: AbsoluteDeadline;
|
|
919
|
+
deadline: AbsoluteDeadline | undefined;
|
|
747
920
|
runtime: AgentRuntimeConfig;
|
|
748
921
|
host: AgentRunHost;
|
|
749
922
|
slots: ResultSlotRegistry;
|
|
@@ -758,15 +931,33 @@ class WorkflowOwner {
|
|
|
758
931
|
events: RunEventLog;
|
|
759
932
|
onCallerStopped?: RunWorkflowOptions["onCallerStopped"];
|
|
760
933
|
awaitReply: NonNullable<RunWorkflowOptions["awaitReply"]>;
|
|
934
|
+
coordinators: readonly CoordinatorPort[];
|
|
935
|
+
/** A run of a workflow with `events`: its mailbox, which its waits take from. */
|
|
936
|
+
mailbox?: RunMailbox;
|
|
937
|
+
/** The run's time. */
|
|
938
|
+
now: () => number;
|
|
939
|
+
/** Its timers: the mailbox for a run with `events`, one of its own for a plain run. */
|
|
940
|
+
waits: RunMailbox;
|
|
941
|
+
/** With `mailbox`: each root agent's last session in earlier attempts, which it reopens on. */
|
|
942
|
+
keptSessions?: ReadonlyMap<string, NativeFork>;
|
|
943
|
+
/** With `mailbox`: each agent's turn an earlier attempt started whose end no wait took. */
|
|
944
|
+
restoredTurns?: ReadonlyMap<string, RestoredTurn>;
|
|
945
|
+
emit: Emitter;
|
|
946
|
+
/** The workflow's name, which with the run's id is the ref its events are `from`. */
|
|
947
|
+
workflow: string;
|
|
761
948
|
},
|
|
762
949
|
) {
|
|
950
|
+
this.#restored = new Map(options.restoredTurns ?? []);
|
|
763
951
|
this.#messaging = new Messaging({
|
|
764
952
|
agent: (key) => this.#agentView(key),
|
|
765
953
|
canAwait: (key) => {
|
|
766
954
|
const agent = this.#logical.get(key);
|
|
767
|
-
|
|
955
|
+
// One with a started turn takes messages only once a wait has its end: a reply awaited from
|
|
956
|
+
// it could wait on code that is itself waiting.
|
|
957
|
+
if (!agent || agent.hasStartedTurn) return false;
|
|
958
|
+
return options.awaitReply(agent.execution, agent.confirmsDelivery);
|
|
768
959
|
},
|
|
769
|
-
launcher: (key) => this.#logical.get(key)?.launcher ?? "
|
|
960
|
+
launcher: (key) => this.#logical.get(key)?.launcher ?? "wfa",
|
|
770
961
|
changed: (key) => {
|
|
771
962
|
this.#messagesChanged();
|
|
772
963
|
this.#logical.get(key)?.messagesChanged();
|
|
@@ -788,9 +979,27 @@ class WorkflowOwner {
|
|
|
788
979
|
if (!(await options.events.event({ ...event, callPath: [] }))) {
|
|
789
980
|
throw new Error("the run's event log was not written");
|
|
790
981
|
}
|
|
982
|
+
for (const port of options.coordinators) {
|
|
983
|
+
try {
|
|
984
|
+
port.recorded(event);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
options.onLog?.(`awf: a coordinator did not take an event: ${messageOf(error)}`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
791
989
|
},
|
|
792
990
|
});
|
|
793
991
|
options.control.attach(this.#messaging);
|
|
992
|
+
this.#tools = new ToolCalls<ExecutionScope>({
|
|
993
|
+
record: (event) => options.events.event(event),
|
|
994
|
+
runIn: (scope, fn) => (scope ? scopes.run(scope, fn) : scopes.exit(fn)),
|
|
995
|
+
});
|
|
996
|
+
options.control.attachTools(this.#tools);
|
|
997
|
+
const holder = {
|
|
998
|
+
reply: (request: HolderRequest) => this.#messaging.holderReply(request),
|
|
999
|
+
send: (request: HolderRequest) => this.#messaging.holderSend(request),
|
|
1000
|
+
open: () => this.#messaging.openQuestions(),
|
|
1001
|
+
};
|
|
1002
|
+
for (const port of options.coordinators) port.attach(holder);
|
|
794
1003
|
this.context = {
|
|
795
1004
|
runId: options.runId,
|
|
796
1005
|
attempt: options.attempt,
|
|
@@ -830,6 +1039,8 @@ class WorkflowOwner {
|
|
|
830
1039
|
}
|
|
831
1040
|
},
|
|
832
1041
|
forkCaller: (spec) => {
|
|
1042
|
+
if ((spec as AgentForkSpec).tools !== undefined)
|
|
1043
|
+
return Promise.reject(new Error("a fork of the calling session takes no tools"));
|
|
833
1044
|
try {
|
|
834
1045
|
return this.forkCaller(spec);
|
|
835
1046
|
} catch (error) {
|
|
@@ -872,15 +1083,36 @@ class WorkflowOwner {
|
|
|
872
1083
|
},
|
|
873
1084
|
},
|
|
874
1085
|
participants: {
|
|
875
|
-
connect: () =>
|
|
876
|
-
get: () =>
|
|
1086
|
+
connect: async (spec) => this.connectParticipant(spec),
|
|
1087
|
+
get: async (key) =>
|
|
1088
|
+
(this.#messaging.connected().includes(key) ? this.#outsideRefs.get(key) : undefined) ??
|
|
1089
|
+
(this.#logical.get(key) as AgentRef | undefined) ??
|
|
1090
|
+
null,
|
|
877
1091
|
},
|
|
878
1092
|
messages: { allow: async (access) => this.allowRoute(access) },
|
|
879
1093
|
steps: {
|
|
880
1094
|
run: () => unavailable("steps.run"),
|
|
881
1095
|
sleep: () => unavailable("steps.sleep"),
|
|
882
1096
|
},
|
|
883
|
-
|
|
1097
|
+
emit: async (target, run, name, data, emitOptions) => {
|
|
1098
|
+
if (this.#closed || this.#cleanupFailures.length)
|
|
1099
|
+
throw new Error("workflow context is closed");
|
|
1100
|
+
scopes.getStore()?.assertAccepting();
|
|
1101
|
+
if (!isEventWorkflow(target))
|
|
1102
|
+
throw new Error("emit's target is a workflow with events, as its file exports it");
|
|
1103
|
+
return this.track(
|
|
1104
|
+
options.emit(
|
|
1105
|
+
target,
|
|
1106
|
+
{
|
|
1107
|
+
run,
|
|
1108
|
+
name,
|
|
1109
|
+
data: data as JsonValue,
|
|
1110
|
+
...(emitOptions?.eventId === undefined ? {} : { id: emitOptions.eventId }),
|
|
1111
|
+
},
|
|
1112
|
+
`${options.workflow}/${options.runId}`,
|
|
1113
|
+
),
|
|
1114
|
+
);
|
|
1115
|
+
},
|
|
884
1116
|
parallel: (items, operation, parallelOptions) => {
|
|
885
1117
|
const inherited = scopes.getStore()?.deadline ?? options.deadline;
|
|
886
1118
|
const deadline = parallelOptions?.deadline
|
|
@@ -899,8 +1131,357 @@ class WorkflowOwner {
|
|
|
899
1131
|
this.runStage(name, rest)) as WorkflowContext["stage"],
|
|
900
1132
|
call: () => unavailable("call"),
|
|
901
1133
|
usage: () => options.ledger.records(),
|
|
1134
|
+
allowance: async (harness) => {
|
|
1135
|
+
if (this.#closed || this.#cleanupFailures.length)
|
|
1136
|
+
throw new Error("workflow context is closed");
|
|
1137
|
+
if (!(HARNESS_NAMES as readonly string[]).includes(harness))
|
|
1138
|
+
throw new Error(`allowance: no harness ${harness}; awf has ${HARNESS_NAMES.join(", ")}`);
|
|
1139
|
+
if (!options.allowance)
|
|
1140
|
+
return { harness, read: "none", reason: "this run has no allowance reader" };
|
|
1141
|
+
return this.track(options.allowance(harness as Harness));
|
|
1142
|
+
},
|
|
902
1143
|
log: (message, fields) => options.log(message, fields),
|
|
1144
|
+
now: () => {
|
|
1145
|
+
this.#notReading("wf.now()");
|
|
1146
|
+
return new Date(options.now());
|
|
1147
|
+
},
|
|
1148
|
+
sleep: async (name, when) => {
|
|
1149
|
+
await this.race(
|
|
1150
|
+
options.waits,
|
|
1151
|
+
name,
|
|
1152
|
+
{ sleep: checkedWhen(when) } as RaceBranches,
|
|
1153
|
+
undefined,
|
|
1154
|
+
);
|
|
1155
|
+
},
|
|
903
1156
|
};
|
|
1157
|
+
const { mailbox } = options;
|
|
1158
|
+
Object.assign(
|
|
1159
|
+
this.context,
|
|
1160
|
+
mailbox
|
|
1161
|
+
? {
|
|
1162
|
+
receive: (spec?: ReceiveOptions) => this.receive(mailbox, spec),
|
|
1163
|
+
race: (name: string, branches: RaceBranches, raceOptions?: WaitOptions) =>
|
|
1164
|
+
this.race(mailbox, name, branches, raceOptions),
|
|
1165
|
+
select: (branches: Branches, selectOptions?: SelectOptions) =>
|
|
1166
|
+
this.select(mailbox, branches, selectOptions),
|
|
1167
|
+
timer: (name: string, when: When) => this.timer(mailbox, name, when),
|
|
1168
|
+
timers: () => source({}, { name: "timer:due" }),
|
|
1169
|
+
at: (read: () => Date | null) => timeRead(read),
|
|
1170
|
+
after: (duration: Duration) => {
|
|
1171
|
+
this.#notReading("wf.after()");
|
|
1172
|
+
if (typeof duration !== "string" || !/^\d+(?:\.\d+)?[dhm]$/.test(duration))
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`wf.after takes days, hours or minutes, "1d", "3h", "1.5h"; not ${String(duration)}`,
|
|
1175
|
+
);
|
|
1176
|
+
return new Date(parseWhen(duration, options.now()).due);
|
|
1177
|
+
},
|
|
1178
|
+
signal: (name: string) => signalSource(name),
|
|
1179
|
+
...(mailbox.started ? { started: mailbox.started } : {}),
|
|
1180
|
+
}
|
|
1181
|
+
: { receive: () => Promise.reject(new Error("receive needs a workflow with events")) },
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/** Set while a select reads a time from state. */
|
|
1186
|
+
#reading = false;
|
|
1187
|
+
|
|
1188
|
+
/** Refuses `what` in a time read from state: a time made at the read is never due. */
|
|
1189
|
+
#notReading(what: string): void {
|
|
1190
|
+
if (this.#reading)
|
|
1191
|
+
throw new Error(
|
|
1192
|
+
`${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
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/** Branch `branch`'s time, read from state now: an ISO time, or undefined for `null`. */
|
|
1197
|
+
#readTime(branch: string, read: () => Date | null): string | undefined {
|
|
1198
|
+
this.#reading = true;
|
|
1199
|
+
let value: unknown;
|
|
1200
|
+
try {
|
|
1201
|
+
value = read();
|
|
1202
|
+
} finally {
|
|
1203
|
+
this.#reading = false;
|
|
1204
|
+
}
|
|
1205
|
+
if (value === null) return undefined;
|
|
1206
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`select's ${branch}: wf.at reads a valid Date or null from the run's state, not ${
|
|
1209
|
+
value instanceof Date ? "an invalid Date" : (JSON.stringify(value) ?? String(value))
|
|
1210
|
+
}`,
|
|
1211
|
+
);
|
|
1212
|
+
return value.toISOString();
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
/** Checks a wait may begin here: one at a time, outside a tool's handler, in an open scope. */
|
|
1216
|
+
#beginWait(what: string): ExecutionScope | undefined {
|
|
1217
|
+
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1218
|
+
if (inToolHandler())
|
|
1219
|
+
throw new Error(`${what} is refused in a tool's handler: it would take the run's events`);
|
|
1220
|
+
const scope = scopes.getStore();
|
|
1221
|
+
scope?.assertAccepting();
|
|
1222
|
+
if (this.#waiting)
|
|
1223
|
+
throw new Error(`${what} was called while another wait is waiting; one wait at a time`);
|
|
1224
|
+
return scope;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/** Runs a wait, which ends with the run, as its owner closes, or with the scope it waits in. */
|
|
1228
|
+
async #holdingWait(
|
|
1229
|
+
scope: ExecutionScope | undefined,
|
|
1230
|
+
wait: (signal: AbortSignal) => Promise<WorkflowEvent<string, JsonValue>[]>,
|
|
1231
|
+
): Promise<WorkflowEvent<string, JsonValue>[]> {
|
|
1232
|
+
this.#waiting = true;
|
|
1233
|
+
const cancelled = new AbortController();
|
|
1234
|
+
const removeFromScope = scope?.add(async (stop) =>
|
|
1235
|
+
cancelled.abort(new Error(String(stop.reason ?? "the wait's scope was cancelled"))),
|
|
1236
|
+
);
|
|
1237
|
+
try {
|
|
1238
|
+
const taken = await wait(AbortSignal.any([this.#closingRun.signal, cancelled.signal]));
|
|
1239
|
+
// A turn's end taken: its agent takes other work again.
|
|
1240
|
+
for (const event of taken) {
|
|
1241
|
+
if (event.name !== "agent:ended") continue;
|
|
1242
|
+
const { turn } = event.data as unknown as TurnEnd;
|
|
1243
|
+
if (this.#restored.get(event.from)?.turn === turn) this.#restored.delete(event.from);
|
|
1244
|
+
this.#logical.get(event.from)?.endTaken(turn);
|
|
1245
|
+
}
|
|
1246
|
+
return taken;
|
|
1247
|
+
} finally {
|
|
1248
|
+
removeFromScope?.();
|
|
1249
|
+
this.#waiting = false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/** A timer, by name: the recorded one, or set now. A race's own are named `{race}@{branch}`. */
|
|
1254
|
+
private timer(mailbox: RunMailbox, name: string, when: When, own = false): Timer {
|
|
1255
|
+
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1256
|
+
if (typeof name !== "string") throw new Error("a timer's name is a string");
|
|
1257
|
+
if (!own) {
|
|
1258
|
+
const problem = name.includes("@")
|
|
1259
|
+
? "without @, which a race's own timers use"
|
|
1260
|
+
: nameProblem(name);
|
|
1261
|
+
if (problem) throw new Error(`timer ${name}: its name is ${problem}`);
|
|
1262
|
+
}
|
|
1263
|
+
const given = parseWhen(when, this.options.now());
|
|
1264
|
+
const { timer, asked } = mailbox.setTimer(name, given.due, given.when);
|
|
1265
|
+
if (timer.cancelled)
|
|
1266
|
+
this.options.onLog?.(
|
|
1267
|
+
`awf: timer ${name} was cancelled; a name stays cancelled, so it won't fire`,
|
|
1268
|
+
);
|
|
1269
|
+
else if (asked !== undefined)
|
|
1270
|
+
this.options.onLog?.(
|
|
1271
|
+
`awf: timer ${name} is due ${timer.due}, as first set (${timer.when}); ${asked} is ignored`,
|
|
1272
|
+
);
|
|
1273
|
+
return timerSource(mailbox, name, timer.due);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/** Waits for the first of `branches`, recorded under `name`. */
|
|
1277
|
+
private async race(
|
|
1278
|
+
mailbox: RunMailbox,
|
|
1279
|
+
name: string,
|
|
1280
|
+
branches: RaceBranches,
|
|
1281
|
+
raceOptions: WaitOptions | undefined,
|
|
1282
|
+
): Promise<Next<RaceBranches>> {
|
|
1283
|
+
const scope = this.#beginWait(`race ${name}`);
|
|
1284
|
+
if (typeof name !== "string" || nameProblem(name))
|
|
1285
|
+
throw new Error(
|
|
1286
|
+
`a race's name is ${typeof name === "string" ? nameProblem(name) : "a string"}`,
|
|
1287
|
+
);
|
|
1288
|
+
if (scope?.stage !== undefined)
|
|
1289
|
+
throw new Error(`race ${name} is in stage ${scope.stage}: a race is recorded on its own`);
|
|
1290
|
+
if (this.#raced.has(name))
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
`race ${name} has answered in this attempt; a wait in a loop is named per round, as approval-\${round}`,
|
|
1293
|
+
);
|
|
1294
|
+
this.#raced.add(name);
|
|
1295
|
+
const label = checkedLabel(raceOptions?.label);
|
|
1296
|
+
const given = branchesOf(`race ${name}`, branches, true);
|
|
1297
|
+
// A sleep's time may be a Date, its own timer as a duration is.
|
|
1298
|
+
const own = (source: unknown) => typeof source === "string" || source instanceof Date;
|
|
1299
|
+
// Every branch checked before a timer is set, so a refused race leaves none behind.
|
|
1300
|
+
const takes = new Map(
|
|
1301
|
+
given.flatMap(([branch, source]) =>
|
|
1302
|
+
own(source) ? [] : [[branch, sourceTake(`race ${name}`, branch, source)] as const],
|
|
1303
|
+
),
|
|
1304
|
+
);
|
|
1305
|
+
const owned = new Map<string, string>();
|
|
1306
|
+
const pairs: (readonly [string, WaitTake])[] = given.map(([branch, source]) => {
|
|
1307
|
+
if (!own(source)) return [branch, takes.get(branch) as WaitTake] as const;
|
|
1308
|
+
const timer = this.timer(mailbox, `${name}@${branch}`, source as When, true);
|
|
1309
|
+
owned.set(branch, timer.name);
|
|
1310
|
+
return [branch, { name: "timer:due", from: timer.name }] as const;
|
|
1311
|
+
});
|
|
1312
|
+
let won: { branch: string; event: WorkflowEvent<string, JsonValue> } | undefined;
|
|
1313
|
+
try {
|
|
1314
|
+
await this.#holdingWait(scope, async (signal) => {
|
|
1315
|
+
won = await mailbox.race(name, pairs, owned, label, signal);
|
|
1316
|
+
return [won.event];
|
|
1317
|
+
});
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
// Cut short, not parked: its own timers would fire into a later wait. A park's rerun races again.
|
|
1320
|
+
if (!(error instanceof RunParked))
|
|
1321
|
+
await Promise.all([...owned.values()].map((timer) => mailbox.cancelTimer(timer))).catch(
|
|
1322
|
+
() => undefined,
|
|
1323
|
+
);
|
|
1324
|
+
throw error;
|
|
1325
|
+
}
|
|
1326
|
+
const { branch, event } = won as { branch: string; event: WorkflowEvent<string, JsonValue> };
|
|
1327
|
+
return { key: branch, [branch]: branchValue(event) } as Next<RaceBranches>;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/** A loop over the events `branches` take, one at a time. */
|
|
1331
|
+
private select(
|
|
1332
|
+
mailbox: RunMailbox,
|
|
1333
|
+
branches: Branches,
|
|
1334
|
+
selectOptions: SelectOptions | undefined,
|
|
1335
|
+
): AsyncIterable<Next<Branches>> {
|
|
1336
|
+
const given = branchesOf("select", branches, false);
|
|
1337
|
+
const reads = new Map(
|
|
1338
|
+
given.flatMap(([branch, source]) => {
|
|
1339
|
+
const read = readOf(source);
|
|
1340
|
+
return read ? [[branch, read] as const] : [];
|
|
1341
|
+
}),
|
|
1342
|
+
);
|
|
1343
|
+
const pairs = given.map(
|
|
1344
|
+
([branch, source]) =>
|
|
1345
|
+
[
|
|
1346
|
+
branch,
|
|
1347
|
+
reads.has(branch) ? { name: AT_DUE, from: branch } : sourceTake("select", branch, source),
|
|
1348
|
+
] as const,
|
|
1349
|
+
);
|
|
1350
|
+
// Read at each pass of the wait: a time follows the state as it moves.
|
|
1351
|
+
const takes = (): WaitTake[] =>
|
|
1352
|
+
pairs.map(([branch, take]) => {
|
|
1353
|
+
const read = reads.get(branch);
|
|
1354
|
+
const due = read && this.#readTime(branch, read);
|
|
1355
|
+
return due === undefined ? take : { ...take, due };
|
|
1356
|
+
});
|
|
1357
|
+
const labelOf = () => {
|
|
1358
|
+
const given = selectOptions?.label;
|
|
1359
|
+
return checkedLabel(typeof given === "function" ? given() : given);
|
|
1360
|
+
};
|
|
1361
|
+
const next = async (): Promise<Next<Branches>> => {
|
|
1362
|
+
const scope = this.#beginWait("select");
|
|
1363
|
+
if (scope?.stage !== undefined)
|
|
1364
|
+
throw new Error(`select is in stage ${scope.stage}: a loop's wait is not a stage's`);
|
|
1365
|
+
const [event] = await this.#holdingWait(scope, async (signal) => [
|
|
1366
|
+
await mailbox.select(takes, labelOf(), signal),
|
|
1367
|
+
]);
|
|
1368
|
+
const taken = event as WorkflowEvent<string, JsonValue>;
|
|
1369
|
+
const branch = branchOf(pairs, taken) as string;
|
|
1370
|
+
return { key: branch, [branch]: branchValue(taken) } as Next<Branches>;
|
|
1371
|
+
};
|
|
1372
|
+
return {
|
|
1373
|
+
[Symbol.asyncIterator]: () => ({
|
|
1374
|
+
next: async () => ({ done: false, value: await next() }),
|
|
1375
|
+
return: async () => ({ done: true, value: undefined }),
|
|
1376
|
+
}),
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
#waiting = false;
|
|
1381
|
+
/** The races and sleeps this attempt has run, each by its name once. */
|
|
1382
|
+
readonly #raced = new Set<string>();
|
|
1383
|
+
/** Each agent's turn an earlier attempt started whose end no wait has taken yet. */
|
|
1384
|
+
readonly #restored: Map<string, RestoredTurn>;
|
|
1385
|
+
|
|
1386
|
+
/** How an agent of an event run records its started turns in the mailbox. */
|
|
1387
|
+
#turnsOf(key: string, mailbox: RunMailbox): LogicalAgentTurns {
|
|
1388
|
+
return {
|
|
1389
|
+
begin: (turn, meta, execution) => mailbox.beginTurn(key, turn, meta, execution),
|
|
1390
|
+
end: (turn, meta, outcome) => mailbox.endTurn(key, { turn, meta, outcome }),
|
|
1391
|
+
// Read when the agent opens: a wait before then may have taken the end.
|
|
1392
|
+
restored: () => this.#restored.get(key),
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* The code returned or threw: each started turn still running is cancelled, and its end written,
|
|
1398
|
+
* before the attempt's ending.
|
|
1399
|
+
*/
|
|
1400
|
+
async endStartedTurns(reason: string): Promise<void> {
|
|
1401
|
+
// Bounded as cleanup is: what is cut, a continue's recovery ends.
|
|
1402
|
+
await waitForDeadline(
|
|
1403
|
+
Promise.all([...this.#logical.values()].map((agent) => agent.endStarted(reason))),
|
|
1404
|
+
{ unixMilliseconds: Date.now() + CLEANUP_GRACE_MILLISECONDS },
|
|
1405
|
+
).catch((error: unknown) =>
|
|
1406
|
+
this.options.onLog?.(`awf: a started turn's end was not written: ${messageOf(error)}`),
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Nothing of the run is working but its wait: no agent's turn, message or compaction, and no
|
|
1412
|
+
* decision, sandbox or agent opening in flight.
|
|
1413
|
+
*/
|
|
1414
|
+
idle(waitStage?: string): boolean {
|
|
1415
|
+
const open = this.options.stages.openStage;
|
|
1416
|
+
return (
|
|
1417
|
+
(open === undefined || open === waitStage) &&
|
|
1418
|
+
this.#inFlight.size === 0 &&
|
|
1419
|
+
[...this.#logical.values()].every((agent) => !agent.busy)
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/** The end of an idle agent's open window. */
|
|
1424
|
+
liveUntilOf(key: AgentKey): number | undefined {
|
|
1425
|
+
const agent = this.#logical.get(key);
|
|
1426
|
+
return agent && !agent.busy ? agent.liveUntil() : undefined;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/** The end of the earliest window an agent holds open: the run stays live to it. */
|
|
1430
|
+
liveUntil(): number | undefined {
|
|
1431
|
+
const ends = [...this.#logical.values()].flatMap((agent) => agent.liveUntil() ?? []);
|
|
1432
|
+
return ends.length > 0 ? Math.min(...ends) : undefined;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Calls one agent's due `onTimeout`, in the scope of the wait that calls this, and says whether
|
|
1437
|
+
* it did. A stop or the scope's cancellation stops waiting on it.
|
|
1438
|
+
*/
|
|
1439
|
+
async timeout(signal?: AbortSignal): Promise<boolean> {
|
|
1440
|
+
for (const agent of this.#logical.values()) {
|
|
1441
|
+
const onTimeout = agent.takeTimeout();
|
|
1442
|
+
if (!onTimeout) continue;
|
|
1443
|
+
try {
|
|
1444
|
+
await runUntilStopped(
|
|
1445
|
+
async () => asTimeoutHandler(() => onTimeout(agent as unknown as EventAgentRef)),
|
|
1446
|
+
signal,
|
|
1447
|
+
undefined,
|
|
1448
|
+
);
|
|
1449
|
+
} catch (error) {
|
|
1450
|
+
if (signal?.aborted) throw error;
|
|
1451
|
+
this.options.onLog?.(`awf: agent ${agent.key}'s onTimeout threw: ${messageOf(error)}`);
|
|
1452
|
+
}
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1457
|
+
/** The batch a wait in a stage took, by stage, until the stage ends: what it must return. */
|
|
1458
|
+
readonly #stageBatches = new Map<string, WorkflowEvent<string, JsonValue>[]>();
|
|
1459
|
+
/** Stages around a wait this attempt entered: each is entered once, as a loop over one spins. */
|
|
1460
|
+
readonly #waitedStages = new Set<string>();
|
|
1461
|
+
|
|
1462
|
+
/** A wait: one at a time, and in a stage, the stage's batch. */
|
|
1463
|
+
private async receive(
|
|
1464
|
+
mailbox: RunMailbox,
|
|
1465
|
+
spec: ReceiveOptions | undefined,
|
|
1466
|
+
): Promise<WorkflowEvent<string, JsonValue>[]> {
|
|
1467
|
+
const scope = this.#beginWait("receive()");
|
|
1468
|
+
if (scope?.stage !== undefined && this.#stageBatches.has(scope.stage))
|
|
1469
|
+
throw new Error(`stage ${scope.stage} has waited once; a stage holds one wait`);
|
|
1470
|
+
try {
|
|
1471
|
+
const taken = await this.#holdingWait(scope, (signal) =>
|
|
1472
|
+
mailbox.receive(spec, scope?.stage, signal),
|
|
1473
|
+
);
|
|
1474
|
+
if (scope?.stage !== undefined) {
|
|
1475
|
+
this.#stageBatches.set(scope.stage, taken);
|
|
1476
|
+
this.#waitedStages.add(scope.stage);
|
|
1477
|
+
}
|
|
1478
|
+
return taken;
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
// Parked in a stage: left unended now, before anything closes it, for the next attempt.
|
|
1481
|
+
if (error instanceof RunParked && scope?.stage !== undefined)
|
|
1482
|
+
this.options.stages.abandon(scope.stage);
|
|
1483
|
+
throw error;
|
|
1484
|
+
}
|
|
904
1485
|
}
|
|
905
1486
|
|
|
906
1487
|
/** An agent as messaging sees it, from its entry and its operations. */
|
|
@@ -913,6 +1494,73 @@ class WorkflowOwner {
|
|
|
913
1494
|
return { state: agent.busy ? "working" : agent.hasRun ? "idle" : "not-started", ...caller };
|
|
914
1495
|
}
|
|
915
1496
|
|
|
1497
|
+
/** Connects a participant outside the run under a key no agent holds and none reserved. */
|
|
1498
|
+
private connectParticipant(spec: ParticipantSpec): ParticipantRef {
|
|
1499
|
+
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1500
|
+
scopes.getStore()?.assertAccepting();
|
|
1501
|
+
if (typeof spec !== "object" || spec === null || typeof spec.key !== "string" || !spec.key)
|
|
1502
|
+
throw new Error("a participant is connected as { key, labels? }");
|
|
1503
|
+
const { key, labels } = spec;
|
|
1504
|
+
this.#assertFreeKey(key, "a participant");
|
|
1505
|
+
if (this.#agents.has(key) || this.#logical.has(key) || this.#caller?.key === key)
|
|
1506
|
+
throw new Error(`${key} is an agent's key; a participant takes a key of its own`);
|
|
1507
|
+
if (
|
|
1508
|
+
labels !== undefined &&
|
|
1509
|
+
(typeof labels !== "object" ||
|
|
1510
|
+
labels === null ||
|
|
1511
|
+
Array.isArray(labels) ||
|
|
1512
|
+
!isJsonValue(labels))
|
|
1513
|
+
)
|
|
1514
|
+
throw new Error("a participant's labels are a JSON object");
|
|
1515
|
+
const to = (spec as { to?: unknown }).to;
|
|
1516
|
+
if (to !== undefined && to !== "code")
|
|
1517
|
+
throw new Error(`a participant's messages go to agents, or to: "code"; not ${String(to)}`);
|
|
1518
|
+
if (to === "code" && !this.options.mailbox)
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
"a participant routed to the code needs a workflow with events, whose waits take its replies",
|
|
1521
|
+
);
|
|
1522
|
+
this.#messaging.connect(
|
|
1523
|
+
key,
|
|
1524
|
+
labels === undefined ? undefined : structuredClone(labels),
|
|
1525
|
+
to === "code" ? "code" : undefined,
|
|
1526
|
+
);
|
|
1527
|
+
const ref: ParticipantRef = {
|
|
1528
|
+
...participantRef(key),
|
|
1529
|
+
release: async (reason) => {
|
|
1530
|
+
if (reason !== undefined && typeof reason !== "string")
|
|
1531
|
+
throw new Error("a release's reason is a string");
|
|
1532
|
+
return this.#messaging.release(key, reason);
|
|
1533
|
+
},
|
|
1534
|
+
};
|
|
1535
|
+
if (to === "code") {
|
|
1536
|
+
const routed = source(
|
|
1537
|
+
{
|
|
1538
|
+
...ref,
|
|
1539
|
+
post: async (text: string) => {
|
|
1540
|
+
if (typeof text !== "string" || text.trim() === "")
|
|
1541
|
+
throw new Error("post takes the text to write");
|
|
1542
|
+
await this.#messaging.post(key, text);
|
|
1543
|
+
},
|
|
1544
|
+
},
|
|
1545
|
+
{ name: "participant:reply", from: key },
|
|
1546
|
+
) as unknown as CodeParticipantRef;
|
|
1547
|
+
this.#outsideRefs.set(key, routed);
|
|
1548
|
+
return routed;
|
|
1549
|
+
}
|
|
1550
|
+
this.#outsideRefs.set(key, ref);
|
|
1551
|
+
return ref;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/** A key an agent may not take: a reserved one, or an outside participant's. */
|
|
1555
|
+
#assertFreeKey(key: string, who: string): void {
|
|
1556
|
+
if ((RESERVED_PARTICIPANT_KEYS as readonly string[]).includes(key.toLowerCase()))
|
|
1557
|
+
throw new Error(
|
|
1558
|
+
`${key} is reserved: an agent could read a message from it as authority; ${who} takes another key`,
|
|
1559
|
+
);
|
|
1560
|
+
if (this.#messaging.isOutside(key))
|
|
1561
|
+
throw new Error(`${key} is a participant outside the run; ${who} takes another key`);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
916
1564
|
#messagesChanged(): void {
|
|
917
1565
|
const change = this.#messagingChange;
|
|
918
1566
|
this.#messagingChange = Promise.withResolvers<void>();
|
|
@@ -930,13 +1578,16 @@ class WorkflowOwner {
|
|
|
930
1578
|
const end = (ref: unknown): string => {
|
|
931
1579
|
const key =
|
|
932
1580
|
typeof ref === "object" && ref !== null ? (ref as { key?: unknown }).key : undefined;
|
|
933
|
-
const
|
|
934
|
-
|
|
1581
|
+
const issued =
|
|
1582
|
+
typeof key === "string"
|
|
1583
|
+
? (this.#logical.get(key) ?? this.#outsideRefs.get(key))
|
|
1584
|
+
: undefined;
|
|
1585
|
+
if (!issued || issued !== ref) {
|
|
935
1586
|
throw new Error(
|
|
936
|
-
`a route's ends are agents this workflow opened; ${typeof key === "string" ? key : "this"} is not one`,
|
|
1587
|
+
`a route's ends are agents this workflow opened or participants it connected; ${typeof key === "string" ? key : "this"} is not one`,
|
|
937
1588
|
);
|
|
938
1589
|
}
|
|
939
|
-
return
|
|
1590
|
+
return issued.key;
|
|
940
1591
|
};
|
|
941
1592
|
if (typeof access !== "object" || access === null)
|
|
942
1593
|
throw new Error("a route is { from, to } or { between }, with a purpose");
|
|
@@ -952,6 +1603,7 @@ class WorkflowOwner {
|
|
|
952
1603
|
this.#messaging.grant(ends, {
|
|
953
1604
|
purpose: access.purpose,
|
|
954
1605
|
maxMessages: access.maxMessages,
|
|
1606
|
+
timeoutMs: access.timeoutMs,
|
|
955
1607
|
scope: scope?.messages ?? this.#messaging.attempt,
|
|
956
1608
|
});
|
|
957
1609
|
}
|
|
@@ -1007,7 +1659,8 @@ class WorkflowOwner {
|
|
|
1007
1659
|
|
|
1008
1660
|
/**
|
|
1009
1661
|
* The workflow returned: its routes outside any stage take nothing new, and the message
|
|
1010
|
-
* operations they opened are waited for, as a stage waits for its own.
|
|
1662
|
+
* operations they opened are waited for, as a stage waits for its own. A question open to an
|
|
1663
|
+
* outside participant keeps it waiting too; once none is, each one still connected is released.
|
|
1011
1664
|
*/
|
|
1012
1665
|
async settleMessages(): Promise<void> {
|
|
1013
1666
|
const attempt = this.#messaging.attempt;
|
|
@@ -1017,6 +1670,9 @@ class WorkflowOwner {
|
|
|
1017
1670
|
const failed = settled.find((each) => each.status === "rejected");
|
|
1018
1671
|
if (failed?.status === "rejected") throw failed.reason;
|
|
1019
1672
|
});
|
|
1673
|
+
for (const key of this.#messaging.connected()) {
|
|
1674
|
+
this.#messaging.release(key, "the workflow returned");
|
|
1675
|
+
}
|
|
1020
1676
|
}
|
|
1021
1677
|
|
|
1022
1678
|
async close(deadline: AbsoluteDeadline): Promise<unknown[]> {
|
|
@@ -1033,7 +1689,9 @@ class WorkflowOwner {
|
|
|
1033
1689
|
state
|
|
1034
1690
|
.stopOperations({
|
|
1035
1691
|
kind:
|
|
1036
|
-
Date.now() >= this.options.deadline.unixMilliseconds
|
|
1692
|
+
this.options.deadline && Date.now() >= this.options.deadline.unixMilliseconds
|
|
1693
|
+
? "timed-out"
|
|
1694
|
+
: "cancelled",
|
|
1037
1695
|
reason: "workflow closing",
|
|
1038
1696
|
})
|
|
1039
1697
|
.then(() => state.close("workflow complete")),
|
|
@@ -1119,7 +1777,19 @@ class WorkflowOwner {
|
|
|
1119
1777
|
});
|
|
1120
1778
|
if (entered.kind === "reuse") {
|
|
1121
1779
|
entered.release();
|
|
1122
|
-
|
|
1780
|
+
// A stage around a wait returns the batch it took, which its record doesn't hold.
|
|
1781
|
+
const batch =
|
|
1782
|
+
entered.value === undefined && !options
|
|
1783
|
+
? await this.options.mailbox?.batchOf(name)
|
|
1784
|
+
: undefined;
|
|
1785
|
+
if (batch) {
|
|
1786
|
+
if (this.#waitedStages.has(name))
|
|
1787
|
+
throw new Error(
|
|
1788
|
+
`stage ${name} around a wait was entered again in this attempt, and would return the same batch forever; name it by what it waits for`,
|
|
1789
|
+
);
|
|
1790
|
+
this.#waitedStages.add(name);
|
|
1791
|
+
}
|
|
1792
|
+
return (batch as JsonValue | undefined) ?? entered.value;
|
|
1123
1793
|
}
|
|
1124
1794
|
const open = entered.stage;
|
|
1125
1795
|
const messages = this.#messaging.scope(name);
|
|
@@ -1137,7 +1807,22 @@ class WorkflowOwner {
|
|
|
1137
1807
|
this.#messaging.finishing(messages);
|
|
1138
1808
|
scope.seal();
|
|
1139
1809
|
await this.drainMessages(messages, () => scope.settleOwned());
|
|
1810
|
+
// A timer it set is written before its record, which a rerun reuses without its work.
|
|
1811
|
+
await this.options.waits.flush();
|
|
1140
1812
|
if (this.options.stages.stopped?.stage === name) throw this.options.stages.caught();
|
|
1813
|
+
const batch = this.#stageBatches.get(name);
|
|
1814
|
+
if (batch && !options) {
|
|
1815
|
+
const same =
|
|
1816
|
+
Array.isArray(returned) &&
|
|
1817
|
+
returned.length === batch.length &&
|
|
1818
|
+
returned.every((event, i) => (event as { id?: unknown })?.id === batch[i]?.id);
|
|
1819
|
+
if (!same)
|
|
1820
|
+
throw new Error(
|
|
1821
|
+
`stage ${name} returned other events than its wait took; its work is the wait alone`,
|
|
1822
|
+
);
|
|
1823
|
+
await open.succeed(undefined);
|
|
1824
|
+
return batch as JsonValue;
|
|
1825
|
+
}
|
|
1141
1826
|
const value = stageValue(name, options, returned);
|
|
1142
1827
|
await open.succeed(value, summaryOf(name, options, value, this.options.onLog));
|
|
1143
1828
|
return value;
|
|
@@ -1149,12 +1834,15 @@ class WorkflowOwner {
|
|
|
1149
1834
|
}).catch(() => undefined);
|
|
1150
1835
|
const stopped = this.options.stages.stopped;
|
|
1151
1836
|
if (stopped && error === stopped && stopped.stage === name) await open.stop(stopped.reason);
|
|
1837
|
+
// Parked at a wait in it: not ended, so the next attempt runs it again.
|
|
1838
|
+
else if (error instanceof RunParked) this.options.stages.abandon(name);
|
|
1152
1839
|
else {
|
|
1153
1840
|
this.options.stages.failedIn(name, error);
|
|
1154
1841
|
await open.fail(messageOf(error));
|
|
1155
1842
|
}
|
|
1156
1843
|
throw error;
|
|
1157
1844
|
} finally {
|
|
1845
|
+
this.#stageBatches.delete(name);
|
|
1158
1846
|
this.#messaging.closeScope(messages);
|
|
1159
1847
|
this.#stageScopes.delete(messages);
|
|
1160
1848
|
removeFromParent?.();
|
|
@@ -1164,6 +1852,7 @@ class WorkflowOwner {
|
|
|
1164
1852
|
private openAgent(spec: AgentOpenSpec): Promise<AgentRef> {
|
|
1165
1853
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1166
1854
|
scopes.getStore()?.assertAccepting();
|
|
1855
|
+
this.#assertFreeKey(spec.key, "an agent");
|
|
1167
1856
|
if (this.#caller?.key === spec.key) {
|
|
1168
1857
|
throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
|
|
1169
1858
|
}
|
|
@@ -1172,6 +1861,12 @@ class WorkflowOwner {
|
|
|
1172
1861
|
throw new Error(`agent ${spec.key}: ${RUN_SANDBOX_ONLY}`);
|
|
1173
1862
|
const existing = this.#agents.get(spec.key);
|
|
1174
1863
|
if (existing?.stopped !== undefined) throw stoppedError(spec.key, existing.stopped);
|
|
1864
|
+
const given = (spec as EventAgentOpenSpec).keepAlive;
|
|
1865
|
+
if (given !== undefined && !this.options.mailbox)
|
|
1866
|
+
throw new Error(
|
|
1867
|
+
`agent ${spec.key}: keepAlive is for a workflow with events, whose runs park; this one's never do`,
|
|
1868
|
+
);
|
|
1869
|
+
const keepAlive = given === undefined ? undefined : checkedKeepAlive(spec.key, given);
|
|
1175
1870
|
const execution = existing
|
|
1176
1871
|
? constrainExistingExecution(spec.runtime, existing.identity.execution)
|
|
1177
1872
|
: withKnownEffort(resolveExecution(spec.runtime, this.options.runtime));
|
|
@@ -1198,6 +1893,8 @@ class WorkflowOwner {
|
|
|
1198
1893
|
assertDeadline(effectiveDeadline);
|
|
1199
1894
|
if (existing) {
|
|
1200
1895
|
assertCompatibleAgent(spec.key, existing.identity, identity, spec);
|
|
1896
|
+
this.#tools.reopen(spec.key, spec.tools);
|
|
1897
|
+
if (keepAlive) existing.keepAlive = keepAlive;
|
|
1201
1898
|
const seated =
|
|
1202
1899
|
spec.sandbox === undefined
|
|
1203
1900
|
? existing.state
|
|
@@ -1209,7 +1906,20 @@ class WorkflowOwner {
|
|
|
1209
1906
|
return attached;
|
|
1210
1907
|
}
|
|
1211
1908
|
|
|
1909
|
+
this.#tools.give(spec.key, spec.tools ?? {});
|
|
1212
1910
|
this.options.progress.agentOpened(spec.key, scope?.group);
|
|
1911
|
+
const found = this.options.keptSessions?.get(spec.key);
|
|
1912
|
+
// Reopened only where it can be: a key the code now opens on another harness starts afresh, and
|
|
1913
|
+
// so does an agent with a home of its own, skills or a sandbox, made anew at every open, which
|
|
1914
|
+
// its kept session isn't in. Either is given its instructions again.
|
|
1915
|
+
const homeOfItsOwn = spec.skills !== undefined || spec.sandbox !== undefined || inRunSandbox;
|
|
1916
|
+
const kept = found?.harness === execution.harness && !homeOfItsOwn ? found : undefined;
|
|
1917
|
+
if (found && !kept)
|
|
1918
|
+
this.options.onLog?.(
|
|
1919
|
+
homeOfItsOwn
|
|
1920
|
+
? `agent ${spec.key} has skills or a sandbox, whose home is made anew; it starts a new session`
|
|
1921
|
+
: `agent ${spec.key} now runs ${execution.harness}, not the ${found.harness} its session was kept on; it starts a new session`,
|
|
1922
|
+
);
|
|
1213
1923
|
// Opened before the session so registration below stays synchronous: two concurrent `agent()`
|
|
1214
1924
|
// calls for one key must not each build an agent.
|
|
1215
1925
|
const sessions: AgentSessions = { launcher: new Set(), forks: [] };
|
|
@@ -1228,10 +1938,13 @@ class WorkflowOwner {
|
|
|
1228
1938
|
...(identity.skills ? { skills: identity.skills } : {}),
|
|
1229
1939
|
activation: (cwd, seated, skills, home) => ({
|
|
1230
1940
|
key: spec.key,
|
|
1231
|
-
deadline: effectiveDeadline,
|
|
1941
|
+
deadline: harnessDeadline(effectiveDeadline),
|
|
1232
1942
|
cwd,
|
|
1233
1943
|
execution,
|
|
1234
|
-
|
|
1944
|
+
// A kept session already holds the instructions; they are not sent again.
|
|
1945
|
+
...(kept ? { continues: kept } : {}),
|
|
1946
|
+
...(spec.instructions === undefined || kept ? {} : { instructions: spec.instructions }),
|
|
1947
|
+
...(this.options.mailbox ? { paneAtFirstTurn: true as const } : {}),
|
|
1235
1948
|
...(spec.labels === undefined ? {} : { labels: spec.labels }),
|
|
1236
1949
|
...(seated ? { occupant: seated.occupant } : {}),
|
|
1237
1950
|
...(seated?.webSearch ? { webSearch: true } : {}),
|
|
@@ -1263,6 +1976,7 @@ class WorkflowOwner {
|
|
|
1263
1976
|
(opened) => opened,
|
|
1264
1977
|
() => undefined,
|
|
1265
1978
|
),
|
|
1979
|
+
...(keepAlive ? { keepAlive } : {}),
|
|
1266
1980
|
});
|
|
1267
1981
|
const activated = this.track(waitForDeadline(ownedState, effectiveDeadline));
|
|
1268
1982
|
scope?.track(activated);
|
|
@@ -1275,6 +1989,8 @@ class WorkflowOwner {
|
|
|
1275
1989
|
*/
|
|
1276
1990
|
private stopAgent(key: string, reason?: string): Promise<boolean> {
|
|
1277
1991
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1992
|
+
const inTool = refusedInTool(key, "agents.stop");
|
|
1993
|
+
if (inTool) throw inTool;
|
|
1278
1994
|
const scope = scopes.getStore();
|
|
1279
1995
|
scope?.assertAccepting();
|
|
1280
1996
|
this.options.stages.checkOperation();
|
|
@@ -1334,6 +2050,7 @@ class WorkflowOwner {
|
|
|
1334
2050
|
if (this.#closed || this.#cleanupFailures.length) throw new Error("workflow context is closed");
|
|
1335
2051
|
const scope = scopes.getStore();
|
|
1336
2052
|
scope?.assertAccepting();
|
|
2053
|
+
this.#assertFreeKey(key, "an agent");
|
|
1337
2054
|
const found = this.options.runtime.host.caller;
|
|
1338
2055
|
if (!found) return Promise.resolve(null);
|
|
1339
2056
|
if (this.#caller) {
|
|
@@ -1371,7 +2088,7 @@ class WorkflowOwner {
|
|
|
1371
2088
|
);
|
|
1372
2089
|
const session = await this.options.host.openAgent({
|
|
1373
2090
|
key,
|
|
1374
|
-
deadline: this.options.deadline,
|
|
2091
|
+
deadline: harnessDeadline(this.options.deadline),
|
|
1375
2092
|
cwd: found.cwd,
|
|
1376
2093
|
execution,
|
|
1377
2094
|
});
|
|
@@ -1659,6 +2376,7 @@ class WorkflowOwner {
|
|
|
1659
2376
|
key,
|
|
1660
2377
|
runDir: this.options.runDir,
|
|
1661
2378
|
messaging: this.#messaging,
|
|
2379
|
+
tools: this.#tools,
|
|
1662
2380
|
messageOwner: (scope) => this.#messageOwner(scope),
|
|
1663
2381
|
execution,
|
|
1664
2382
|
session,
|
|
@@ -1679,8 +2397,12 @@ class WorkflowOwner {
|
|
|
1679
2397
|
this.options.failRun(error);
|
|
1680
2398
|
},
|
|
1681
2399
|
livenessPolicy: this.options.livenessPolicy,
|
|
2400
|
+
...(this.options.mailbox ? { turns: this.#turnsOf(key, this.options.mailbox) } : {}),
|
|
1682
2401
|
fork: (spec, take, settings) => this.forkAgent(key, spec, take, settings),
|
|
1683
2402
|
keepPane: () => this.#agents.get(key)?.identity.keepPane,
|
|
2403
|
+
keepAlive: () => this.#agents.get(key)?.keepAlive,
|
|
2404
|
+
defaultWindowMs: this.options.defaultWindowMs ?? DEFAULT_WINDOW_MS,
|
|
2405
|
+
now: this.options.now,
|
|
1684
2406
|
// Its socket is its authority: a kept agent's harness lives on, and must not answer.
|
|
1685
2407
|
revoke: () =>
|
|
1686
2408
|
this.#agents
|
|
@@ -1723,12 +2445,17 @@ class WorkflowOwner {
|
|
|
1723
2445
|
if (this.#caller?.key === spec.key) {
|
|
1724
2446
|
throw new Error(`agent ${spec.key} is the calling session; it is not opened`);
|
|
1725
2447
|
}
|
|
1726
|
-
|
|
2448
|
+
this.#assertFreeKey(spec.key, "an agent");
|
|
2449
|
+
// Out before the clone, which throws on a handler, and out of the spec a repeat is compared by.
|
|
2450
|
+
const { tools, ...cloned } = spec;
|
|
2451
|
+
const forkSpec = structuredClone(cloned);
|
|
1727
2452
|
if (forkSpec.layout !== undefined) forkSpec.layout = storedLayout(forkSpec.layout);
|
|
1728
2453
|
const deadline = scope?.deadline ?? this.options.deadline;
|
|
1729
2454
|
assertDeadline(deadline);
|
|
1730
2455
|
// A fork of the calling session is one whichever way it was asked for.
|
|
1731
2456
|
const from = unopened || this.#caller?.key === parentKey ? CALLING : parentKey;
|
|
2457
|
+
if (from === CALLING && tools !== undefined)
|
|
2458
|
+
throw new Error(`agent ${spec.key}: a fork of the calling session takes no tools`);
|
|
1732
2459
|
const existing = this.#agents.get(spec.key);
|
|
1733
2460
|
if (existing) {
|
|
1734
2461
|
if (existing.stopped !== undefined) throw stoppedError(spec.key, existing.stopped);
|
|
@@ -1736,6 +2463,7 @@ class WorkflowOwner {
|
|
|
1736
2463
|
if (forked?.parent !== from || !isDeepStrictEqual(forked.spec, forkSpec)) {
|
|
1737
2464
|
throw new Error(`agent ${spec.key} is already open, not as this fork of ${from}`);
|
|
1738
2465
|
}
|
|
2466
|
+
this.#tools.reopen(spec.key, tools);
|
|
1739
2467
|
const attached = this.track(waitForDeadline(existing.state, deadline));
|
|
1740
2468
|
scope?.track(attached);
|
|
1741
2469
|
return attached;
|
|
@@ -1743,6 +2471,8 @@ class WorkflowOwner {
|
|
|
1743
2471
|
const parent = unopened ?? this.#agents.get(parentKey)!;
|
|
1744
2472
|
const opening = parent.opening!;
|
|
1745
2473
|
if (typeof take === "string") throw new Error(take);
|
|
2474
|
+
// The parent's tools, with the handlers they have now, unless it gives its own.
|
|
2475
|
+
this.#tools.give(spec.key, tools ?? this.#tools.of(parentKey));
|
|
1746
2476
|
const sandboxed = parent.identity.sandbox !== undefined || this.options.sandboxes.hasRunSandbox;
|
|
1747
2477
|
// A parent with a harness home of its own has its session copied out, and the fork made in the
|
|
1748
2478
|
// child's home: anything added to the parent's would be read as the parent's.
|
|
@@ -1829,7 +2559,7 @@ class WorkflowOwner {
|
|
|
1829
2559
|
...(sandboxed && copy ? { carried: { parent: parentKey, forked, copy } } : {}),
|
|
1830
2560
|
activation: (cwd, seated, given, home) => ({
|
|
1831
2561
|
key: spec.key,
|
|
1832
|
-
deadline,
|
|
2562
|
+
deadline: harnessDeadline(deadline),
|
|
1833
2563
|
cwd,
|
|
1834
2564
|
execution,
|
|
1835
2565
|
...(forkSpec.instructions === undefined ? {} : { instructions: forkSpec.instructions }),
|
|
@@ -1937,7 +2667,7 @@ class WorkflowOwner {
|
|
|
1937
2667
|
*/
|
|
1938
2668
|
private besideFallback(
|
|
1939
2669
|
layout: PaneLayout | undefined,
|
|
1940
|
-
deadline: AbsoluteDeadline,
|
|
2670
|
+
deadline: AbsoluteDeadline | undefined,
|
|
1941
2671
|
): () => Promise<string | undefined> {
|
|
1942
2672
|
const known = (reason: string | undefined) => () => Promise.resolve(reason);
|
|
1943
2673
|
const target = besideOf(layout);
|
|
@@ -2030,12 +2760,17 @@ class LogicalAgent implements AgentRef {
|
|
|
2030
2760
|
#hasRun = false;
|
|
2031
2761
|
/** Supervisors of its open operation, woken when something arrives for it. */
|
|
2032
2762
|
readonly #watchers = new Set<() => void>();
|
|
2763
|
+
/** When its last turn this attempt that opens a window ended. */
|
|
2764
|
+
#lastEnd: number | undefined;
|
|
2765
|
+
/** A turn that opens a window ended since its `onTimeout` was last called. */
|
|
2766
|
+
#timeoutOwed = false;
|
|
2033
2767
|
|
|
2034
2768
|
constructor(
|
|
2035
2769
|
private readonly options: {
|
|
2036
2770
|
key: string;
|
|
2037
2771
|
runDir: string;
|
|
2038
2772
|
messaging: Messaging;
|
|
2773
|
+
tools: ToolCalls<ExecutionScope>;
|
|
2039
2774
|
/**
|
|
2040
2775
|
* What owns a message operation a route in `scope` opens: its stage, or the attempt outside
|
|
2041
2776
|
* any stage. Undefined once that scope has closed: nothing opens for it then.
|
|
@@ -2049,7 +2784,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2049
2784
|
endpoint: string;
|
|
2050
2785
|
/** The path this agent is told to run; see `installAgentLauncher`. */
|
|
2051
2786
|
launcher: string;
|
|
2052
|
-
deadline: AbsoluteDeadline;
|
|
2787
|
+
deadline: AbsoluteDeadline | undefined;
|
|
2053
2788
|
ledger: AgentLedger;
|
|
2054
2789
|
progress: RunProgress;
|
|
2055
2790
|
recordTurn: RecordTurn;
|
|
@@ -2062,6 +2797,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2062
2797
|
livenessPolicy?: LivenessPolicy;
|
|
2063
2798
|
/** After each operation settles, whatever its outcome. */
|
|
2064
2799
|
afterOperation?(): Promise<void>;
|
|
2800
|
+
turns?: LogicalAgentTurns;
|
|
2065
2801
|
/**
|
|
2066
2802
|
* Opens `spec`'s agent on the native fork `take` queues in this agent's operations, at
|
|
2067
2803
|
* `settings`, this agent's once the operations before the fork have run.
|
|
@@ -2073,11 +2809,27 @@ class LogicalAgent implements AgentRef {
|
|
|
2073
2809
|
): Promise<AgentRef>;
|
|
2074
2810
|
/** As the workflow wrote it; decides, at close, whether its pane stays. */
|
|
2075
2811
|
keepPane(): KeepPane | undefined;
|
|
2812
|
+
/** As the workflow's last open of it gave it. */
|
|
2813
|
+
keepAlive(): KeepAlive | undefined;
|
|
2814
|
+
/** Its window when `keepAlive` gives no `timeoutMs`. */
|
|
2815
|
+
defaultWindowMs: number;
|
|
2816
|
+
/** The run's time, which its window is measured on. */
|
|
2817
|
+
now: () => number;
|
|
2076
2818
|
/** Closes its result channel, once its pane is kept. */
|
|
2077
2819
|
revoke(): Promise<void> | undefined;
|
|
2078
2820
|
},
|
|
2079
2821
|
) {
|
|
2080
2822
|
this.#execution = options.execution;
|
|
2823
|
+
asSource(this, { name: "agent:ended", from: options.key });
|
|
2824
|
+
// A turn an earlier attempt started whose end no wait took is this agent's turn again.
|
|
2825
|
+
const restored = options.turns?.restored();
|
|
2826
|
+
if (restored)
|
|
2827
|
+
this.#started = {
|
|
2828
|
+
ref: turnRef(options.key, restored.turn, async () => false),
|
|
2829
|
+
meta: restored.meta,
|
|
2830
|
+
ended: true,
|
|
2831
|
+
done: Promise.resolve(),
|
|
2832
|
+
};
|
|
2081
2833
|
}
|
|
2082
2834
|
|
|
2083
2835
|
get key(): string {
|
|
@@ -2109,21 +2861,173 @@ class LogicalAgent implements AgentRef {
|
|
|
2109
2861
|
return this.#closed ? (this.#closedReason ?? "closed") : undefined;
|
|
2110
2862
|
}
|
|
2111
2863
|
|
|
2864
|
+
/** The end of its window while one is open; a closed agent holds none. */
|
|
2865
|
+
liveUntil(now = this.options.now()): number | undefined {
|
|
2866
|
+
const keepAlive = this.options.keepAlive();
|
|
2867
|
+
if (this.#closed || !keepAlive || this.#lastEnd === undefined) return undefined;
|
|
2868
|
+
const until = this.#lastEnd + (keepAlive.timeoutMs ?? this.options.defaultWindowMs);
|
|
2869
|
+
return until > now ? until : undefined;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
/**
|
|
2873
|
+
* Its `onTimeout`, once its window has passed with it idle and a turn owed it, taken: it isn't
|
|
2874
|
+
* due again until another turn ends.
|
|
2875
|
+
*/
|
|
2876
|
+
takeTimeout(now = this.options.now()): KeepAlive["onTimeout"] {
|
|
2877
|
+
const onTimeout = this.options.keepAlive()?.onTimeout;
|
|
2878
|
+
if (!onTimeout || !this.#timeoutOwed || this.#closed || this.busy || this.#started)
|
|
2879
|
+
return undefined;
|
|
2880
|
+
if (this.liveUntil(now) !== undefined) return undefined;
|
|
2881
|
+
this.#timeoutOwed = false;
|
|
2882
|
+
return onTimeout;
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2112
2885
|
/**
|
|
2113
2886
|
* Something arrived for it: its open operation's supervisor looks again, and an agent with
|
|
2114
2887
|
* nothing queued wakes for a held message, in a message operation.
|
|
2115
2888
|
*/
|
|
2116
2889
|
messagesChanged(): void {
|
|
2117
2890
|
for (const wake of this.#watchers) wake();
|
|
2118
|
-
|
|
2891
|
+
// A started turn holds messages until a wait has taken its end.
|
|
2892
|
+
if (this.#queued > 0 || this.#started || this.#closed || this.options.isRunClosing()) return;
|
|
2119
2893
|
const opening = this.options.messaging.opening(this.key);
|
|
2120
2894
|
if (opening) this.startMessageOperation(opening.scope);
|
|
2121
2895
|
}
|
|
2122
2896
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2897
|
+
/** The turn `start()` began, until a wait takes its end. */
|
|
2898
|
+
#started: { ref: StartedTurn; meta: JsonValue; ended: boolean; done: Promise<void> } | undefined;
|
|
2899
|
+
/** While `start()` hands its turn to `run()`, which refuses one otherwise. */
|
|
2900
|
+
#launching = false;
|
|
2901
|
+
/** Each running turn's own stop, by its id: what a started turn's `cancel()` stops. */
|
|
2902
|
+
readonly #stopsByTurn = new Map<string, (stop: OperationStop) => Promise<void>>();
|
|
2903
|
+
/** Started turns cancelled while still queued behind other work: they settle cancelled. */
|
|
2904
|
+
readonly #cancelledTurns = new Map<string, string>();
|
|
2905
|
+
|
|
2906
|
+
get turn(): StartedTurn | null {
|
|
2907
|
+
return this.#started?.ref ?? null;
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
/** Whether a started turn is set: messages wait, and awaited replies are refused. */
|
|
2911
|
+
get hasStartedTurn(): boolean {
|
|
2912
|
+
return this.#started !== undefined;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
/** A wait took this turn's end: the agent takes other work again. */
|
|
2916
|
+
endTaken(turn: TurnId): void {
|
|
2917
|
+
if (this.#started?.ref.id !== turn) return;
|
|
2918
|
+
this.#started = undefined;
|
|
2919
|
+
this.messagesChanged();
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
/** Cancels a started turn still running and waits for its end: the code returned or threw. */
|
|
2923
|
+
async endStarted(reason: string): Promise<void> {
|
|
2924
|
+
const started = this.#started;
|
|
2925
|
+
if (!started || started.ended) return;
|
|
2926
|
+
await started.ref.cancel(reason);
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
async start(spec: AgentStartSpec): Promise<StartedTurn> {
|
|
2930
|
+
const { turns } = this.options;
|
|
2931
|
+
if (!turns) return unavailable("AgentRef.start");
|
|
2932
|
+
if (this.#closed) throw new Error("logical agent is closed");
|
|
2933
|
+
const inTool = refusedInTool(this.key, "start()");
|
|
2934
|
+
if (inTool) throw inTool;
|
|
2935
|
+
const scope = scopes.getStore();
|
|
2936
|
+
if (scope !== undefined)
|
|
2937
|
+
throw new Error(
|
|
2938
|
+
`agent ${this.key}: start() is called at the run's root, as a turn not awaited would outlive a stage or a parallel`,
|
|
2939
|
+
);
|
|
2940
|
+
const refused = this.#refusedWhileStarted("start");
|
|
2941
|
+
if (refused) throw refused;
|
|
2942
|
+
const { meta = null, ...run } = spec;
|
|
2943
|
+
const id = run.id ?? randomUUID();
|
|
2944
|
+
if (!isJsonValue(meta)) throw new Error("a turn's meta is a JSON value");
|
|
2945
|
+
const ended = Promise.withResolvers<void>();
|
|
2946
|
+
let written: TurnOutcome<JsonValue>["kind"] | undefined;
|
|
2947
|
+
const started = {
|
|
2948
|
+
ref: turnRef(this.key, id, async (reason?: string) => {
|
|
2949
|
+
if (started.ended) return false;
|
|
2950
|
+
// Its turn waits for the handler, which would wait for the turn's end.
|
|
2951
|
+
const inTool = refusedInTool(this.key, "cancel()");
|
|
2952
|
+
if (inTool) throw inTool;
|
|
2953
|
+
const stop = { kind: "cancelled" as const, reason: reason ?? "the turn was cancelled" };
|
|
2954
|
+
// Its own operation only: one queued behind a message settles cancelled when it is reached.
|
|
2955
|
+
const own = this.#stopsByTurn.get(id);
|
|
2956
|
+
if (own) await own(stop);
|
|
2957
|
+
else this.#cancelledTurns.set(id, stop.reason);
|
|
2958
|
+
await started.done;
|
|
2959
|
+
return written === "cancelled";
|
|
2960
|
+
}),
|
|
2961
|
+
meta,
|
|
2962
|
+
ended: false,
|
|
2963
|
+
done: ended.promise,
|
|
2964
|
+
};
|
|
2965
|
+
// Claimed before anything is awaited, so a second start() at once is refused.
|
|
2966
|
+
this.#started = started;
|
|
2967
|
+
try {
|
|
2968
|
+
// Recorded before the agent is launched: a crash between leaves a turn a continue ends.
|
|
2969
|
+
await turns.begin(id, meta, this.options.execution);
|
|
2970
|
+
} catch (error) {
|
|
2971
|
+
this.#started = undefined;
|
|
2972
|
+
// A message held while it began is taken now.
|
|
2973
|
+
this.messagesChanged();
|
|
2974
|
+
throw error;
|
|
2975
|
+
}
|
|
2976
|
+
this.#launching = true;
|
|
2977
|
+
let launched: Promise<RunResult<JsonValue>>;
|
|
2978
|
+
try {
|
|
2979
|
+
launched = outsideTools(() => this.run({ ...run, id } as AgentRunStructuredSpec<JsonValue>));
|
|
2980
|
+
} catch (error) {
|
|
2981
|
+
launched = Promise.reject(error);
|
|
2982
|
+
} finally {
|
|
2983
|
+
this.#launching = false;
|
|
2984
|
+
}
|
|
2985
|
+
// Tracked until the end is in the mailbox, so the run doesn't park with a turn's end on its way.
|
|
2986
|
+
this.options.track(
|
|
2987
|
+
launched
|
|
2988
|
+
.then(
|
|
2989
|
+
({ outcome }) => outcome,
|
|
2990
|
+
(error: unknown): TurnOutcome<JsonValue> => ({
|
|
2991
|
+
answered: false,
|
|
2992
|
+
kind: "failed",
|
|
2993
|
+
reason: messageOf(error),
|
|
2994
|
+
retryable: false,
|
|
2995
|
+
usage: unrunUsage(this.key, id, this.options.execution),
|
|
2996
|
+
}),
|
|
2997
|
+
)
|
|
2998
|
+
.then(async (outcome) => {
|
|
2999
|
+
await turns.end(id, meta, outcome);
|
|
3000
|
+
written = outcome.kind;
|
|
3001
|
+
})
|
|
3002
|
+
// An end that can't be written leaves the turn set for good: the run can't go on.
|
|
3003
|
+
.catch((error: unknown) =>
|
|
3004
|
+
this.options.cleanupUnresolved(
|
|
3005
|
+
`agent ${this.key}'s turn ${id} ended, and its end was not written: ${messageOf(error)}`,
|
|
3006
|
+
),
|
|
3007
|
+
)
|
|
3008
|
+
.finally(() => {
|
|
3009
|
+
started.ended = true;
|
|
3010
|
+
ended.resolve();
|
|
3011
|
+
}),
|
|
3012
|
+
);
|
|
3013
|
+
return started.ref;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
#refusedWhileStarted(what: string): Error | undefined {
|
|
3017
|
+
// First: from a handler this agent is in, its turn waits for the handler, started or not.
|
|
3018
|
+
const inTool = refusedInTool(this.key, `${what}()`);
|
|
3019
|
+
if (inTool) return inTool;
|
|
3020
|
+
if (this.#started && !this.#launching)
|
|
3021
|
+
return new Error(
|
|
3022
|
+
`agent ${this.key} has a turn started (${this.#started.ref.id}) whose end no wait has taken; ${what}() waits for it`,
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
is<Meta extends JsonValue = JsonValue, T extends JsonValue = JsonValue>(event: {
|
|
3027
|
+
readonly name: string;
|
|
3028
|
+
readonly from: string;
|
|
3029
|
+
}): event is AgentEndedEvent<Meta, T> & Matched {
|
|
3030
|
+
return event.name === "agent:ended" && event.from === this.key;
|
|
2127
3031
|
}
|
|
2128
3032
|
|
|
2129
3033
|
run(spec: AgentRunTextSpec): Promise<RunResult<string>>;
|
|
@@ -2132,6 +3036,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2132
3036
|
spec: AgentRunTextSpec | AgentRunStructuredSpec<T>,
|
|
2133
3037
|
): Promise<RunResult<string | T>> {
|
|
2134
3038
|
if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
|
|
3039
|
+
const refused = this.#refusedWhileStarted("run");
|
|
3040
|
+
if (refused) return Promise.reject(refused);
|
|
2135
3041
|
const scope = scopes.getStore();
|
|
2136
3042
|
try {
|
|
2137
3043
|
scope?.assertAccepting();
|
|
@@ -2139,6 +3045,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2139
3045
|
} catch (error) {
|
|
2140
3046
|
return Promise.reject(error);
|
|
2141
3047
|
}
|
|
3048
|
+
const opensWindow = !inTimeoutHandler();
|
|
2142
3049
|
const id = spec.id ?? randomUUID();
|
|
2143
3050
|
const completeSpec = structuredClone({ ...spec, id }) as
|
|
2144
3051
|
| AgentRunTextSpec
|
|
@@ -2155,7 +3062,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2155
3062
|
scope?.track(existing.result);
|
|
2156
3063
|
return existing.result as Promise<RunResult<string | T>>;
|
|
2157
3064
|
}
|
|
2158
|
-
let deadline: AbsoluteDeadline;
|
|
3065
|
+
let deadline: AbsoluteDeadline | undefined;
|
|
2159
3066
|
try {
|
|
2160
3067
|
deadline = this.operationDeadline(spec, scope);
|
|
2161
3068
|
const recoveryDeadline = spec.nudge ? spec.nudge.deadline : undefined;
|
|
@@ -2173,13 +3080,24 @@ class LogicalAgent implements AgentRef {
|
|
|
2173
3080
|
throw new Error("logical agent is closed");
|
|
2174
3081
|
}
|
|
2175
3082
|
scope?.assertActive();
|
|
3083
|
+
const cancelled = this.#cancelledTurns.get(id);
|
|
3084
|
+
if (cancelled !== undefined) {
|
|
3085
|
+
this.#cancelledTurns.delete(id);
|
|
3086
|
+
return {
|
|
3087
|
+
outcome: {
|
|
3088
|
+
kind: "cancelled",
|
|
3089
|
+
reason: cancelled,
|
|
3090
|
+
usage: unrunUsage(this.key, id, this.options.execution),
|
|
3091
|
+
},
|
|
3092
|
+
} as RunResult<JsonValue>;
|
|
3093
|
+
}
|
|
2176
3094
|
const tags = tagsOf(scope, completeSpec.label);
|
|
2177
3095
|
// Drawn here, so a turn that throws before it is delivered is still written as failed.
|
|
2178
3096
|
const operation = this.operationLog(randomUUID(), "turn", tags);
|
|
2179
3097
|
this.options.progress.turnStarted(this.key, tags);
|
|
2180
3098
|
try {
|
|
2181
3099
|
const settled = await this.executeOperation(completeSpec, scope, deadline, operation);
|
|
2182
|
-
this.turnEnded(settled.outcome, "turn");
|
|
3100
|
+
this.turnEnded(settled.outcome, "turn", opensWindow);
|
|
2183
3101
|
return settled;
|
|
2184
3102
|
} catch (error) {
|
|
2185
3103
|
operation.failed(error);
|
|
@@ -2197,6 +3115,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2197
3115
|
|
|
2198
3116
|
compact(spec: CompactSpec): Promise<TurnOutcome<string>> {
|
|
2199
3117
|
if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
|
|
3118
|
+
const refused = this.#refusedWhileStarted("compact");
|
|
3119
|
+
if (refused) return Promise.reject(refused);
|
|
2200
3120
|
const scope = scopes.getStore();
|
|
2201
3121
|
try {
|
|
2202
3122
|
scope?.assertAccepting();
|
|
@@ -2220,7 +3140,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2220
3140
|
}
|
|
2221
3141
|
let deadline: AbsoluteDeadline;
|
|
2222
3142
|
try {
|
|
2223
|
-
deadline = this.operationDeadline(spec, scope);
|
|
3143
|
+
deadline = harnessDeadline(this.operationDeadline(spec, scope));
|
|
2224
3144
|
} catch (error) {
|
|
2225
3145
|
const rejected = Promise.reject<TurnOutcome<string>>(error);
|
|
2226
3146
|
scope?.track(rejected);
|
|
@@ -2255,6 +3175,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2255
3175
|
/** Not a turn: progress counts none for it, and it asks the model nothing. */
|
|
2256
3176
|
set(spec: SettingsSpec): Promise<TurnOutcome<null>> {
|
|
2257
3177
|
if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
|
|
3178
|
+
const refused = this.#refusedWhileStarted("set");
|
|
3179
|
+
if (refused) return Promise.reject(refused);
|
|
2258
3180
|
const scope = scopes.getStore();
|
|
2259
3181
|
try {
|
|
2260
3182
|
scope?.assertAccepting();
|
|
@@ -2279,7 +3201,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2279
3201
|
let deadline: AbsoluteDeadline;
|
|
2280
3202
|
try {
|
|
2281
3203
|
change = this.settingsChange(spec);
|
|
2282
|
-
deadline = this.operationDeadline(spec, scope);
|
|
3204
|
+
deadline = harnessDeadline(this.operationDeadline(spec, scope));
|
|
2283
3205
|
} catch (error) {
|
|
2284
3206
|
const rejected = Promise.reject<TurnOutcome<null>>(error);
|
|
2285
3207
|
scope?.track(rejected);
|
|
@@ -2363,7 +3285,16 @@ class LogicalAgent implements AgentRef {
|
|
|
2363
3285
|
}
|
|
2364
3286
|
|
|
2365
3287
|
/** A turn or compaction that settled, as the view shows it and `turns.jsonl` keeps it. */
|
|
2366
|
-
private turnEnded(
|
|
3288
|
+
private turnEnded(
|
|
3289
|
+
outcome: TurnOutcome<JsonValue>,
|
|
3290
|
+
kind: TurnRecord["kind"],
|
|
3291
|
+
opensWindow = false,
|
|
3292
|
+
): void {
|
|
3293
|
+
// A failure has closed it by now, and a closed agent holds no window.
|
|
3294
|
+
if (opensWindow && !this.#closed) {
|
|
3295
|
+
this.#lastEnd = this.options.now();
|
|
3296
|
+
this.#timeoutOwed = true;
|
|
3297
|
+
}
|
|
2367
3298
|
this.#last = outcome.kind;
|
|
2368
3299
|
const reason = "reason" in outcome ? outcome.reason : undefined;
|
|
2369
3300
|
this.options.progress.turnSettled(this.key, outcome.kind, reason);
|
|
@@ -2372,6 +3303,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2372
3303
|
|
|
2373
3304
|
fork(spec: AgentForkSpec): Promise<AgentRef> {
|
|
2374
3305
|
if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
|
|
3306
|
+
const refused = this.#refusedWhileStarted("fork");
|
|
3307
|
+
if (refused) return Promise.reject(refused);
|
|
2375
3308
|
const scope = scopes.getStore();
|
|
2376
3309
|
const deadline = this.deadlineCeiling(scope);
|
|
2377
3310
|
const { session, execution } = this.options;
|
|
@@ -2419,7 +3352,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2419
3352
|
private operationDeadline(
|
|
2420
3353
|
spec: { deadline?: AbsoluteDeadline; timeoutMs?: number },
|
|
2421
3354
|
scope: ExecutionScope | undefined,
|
|
2422
|
-
): AbsoluteDeadline {
|
|
3355
|
+
): AbsoluteDeadline | undefined {
|
|
2423
3356
|
if (spec.deadline && spec.timeoutMs !== undefined) {
|
|
2424
3357
|
throw new Error("an operation cannot specify both deadline and timeoutMs");
|
|
2425
3358
|
}
|
|
@@ -2430,8 +3363,9 @@ class LogicalAgent implements AgentRef {
|
|
|
2430
3363
|
return earlierDeadline(spec.deadline, ceiling);
|
|
2431
3364
|
}
|
|
2432
3365
|
|
|
2433
|
-
|
|
2434
|
-
|
|
3366
|
+
/** The deadlines around an operation: its scope's and the run's, if either has one. */
|
|
3367
|
+
private deadlineCeiling(scope: ExecutionScope | undefined): AbsoluteDeadline | undefined {
|
|
3368
|
+
return earlierDeadline(scope?.deadline, this.options.deadline);
|
|
2435
3369
|
}
|
|
2436
3370
|
|
|
2437
3371
|
/** Its settings once every `set` queued so far has run. */
|
|
@@ -2490,7 +3424,12 @@ class LogicalAgent implements AgentRef {
|
|
|
2490
3424
|
const usage = () =>
|
|
2491
3425
|
entry.settle({ settledAt: Math.min(Date.now(), deadline.unixMilliseconds) }, []);
|
|
2492
3426
|
if (Date.now() >= deadline.unixMilliseconds) {
|
|
2493
|
-
return {
|
|
3427
|
+
return {
|
|
3428
|
+
answered: false,
|
|
3429
|
+
kind: "timed-out",
|
|
3430
|
+
reason: "settings deadline exceeded",
|
|
3431
|
+
usage: usage(),
|
|
3432
|
+
};
|
|
2494
3433
|
}
|
|
2495
3434
|
const next: AgentExecution = { ...this.#execution, ...change };
|
|
2496
3435
|
const pane = placementOf(next) === "pane";
|
|
@@ -2518,15 +3457,16 @@ class LogicalAgent implements AgentRef {
|
|
|
2518
3457
|
cancelled !== undefined ? "cancelled" : expired ? "timed-out" : "failed",
|
|
2519
3458
|
);
|
|
2520
3459
|
}
|
|
2521
|
-
if (cancelled !== undefined)
|
|
3460
|
+
if (cancelled !== undefined)
|
|
3461
|
+
return { answered: false, kind: "cancelled", reason, usage: usage() };
|
|
2522
3462
|
return expired
|
|
2523
|
-
? { kind: "timed-out", reason, usage: usage() }
|
|
2524
|
-
: { kind: "failed", reason, retryable: false, usage: usage() };
|
|
3463
|
+
? { answered: false, kind: "timed-out", reason, usage: usage() }
|
|
3464
|
+
: { answered: false, kind: "failed", reason, retryable: false, usage: usage() };
|
|
2525
3465
|
} finally {
|
|
2526
3466
|
removeCanceller?.();
|
|
2527
3467
|
}
|
|
2528
3468
|
this.#execution = next;
|
|
2529
|
-
return { kind: "answered", value: null, usage: usage() };
|
|
3469
|
+
return { answered: true, kind: "answered", value: null, usage: usage() };
|
|
2530
3470
|
}
|
|
2531
3471
|
|
|
2532
3472
|
/**
|
|
@@ -2619,7 +3559,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2619
3559
|
private async executeOperation(
|
|
2620
3560
|
spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
|
|
2621
3561
|
scope: ExecutionScope | undefined,
|
|
2622
|
-
operationDeadline: AbsoluteDeadline,
|
|
3562
|
+
operationDeadline: AbsoluteDeadline | undefined,
|
|
2623
3563
|
operation: OperationLog,
|
|
2624
3564
|
): Promise<RunResult<JsonValue>> {
|
|
2625
3565
|
const nudge = spec.nudge === false ? undefined : (spec.nudge ?? this.defaultNudge());
|
|
@@ -2631,7 +3571,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2631
3571
|
private async executeWaitingOperation(
|
|
2632
3572
|
spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
|
|
2633
3573
|
scope: ExecutionScope | undefined,
|
|
2634
|
-
|
|
3574
|
+
initial: AbsoluteDeadline | undefined,
|
|
2635
3575
|
{ operationId, tags, delivered, session }: OperationLog,
|
|
2636
3576
|
nudge: Exclude<AgentRunTextSpec["nudge"], false>,
|
|
2637
3577
|
): Promise<RunResult<JsonValue>> {
|
|
@@ -2639,14 +3579,25 @@ class LogicalAgent implements AgentRef {
|
|
|
2639
3579
|
const binding = { endpoint: this.options.endpoint, operationId };
|
|
2640
3580
|
const entry = this.options.ledger.reserve(operationId, () => this.#execution, tags);
|
|
2641
3581
|
const { messaging } = this.options;
|
|
3582
|
+
// Moves out by any wait on an outside participant, within the deadlines around it.
|
|
3583
|
+
let deadline = initial;
|
|
3584
|
+
const ceiling = this.deadlineCeiling(scope);
|
|
2642
3585
|
messaging.opened({
|
|
2643
3586
|
id: operationId,
|
|
2644
3587
|
agent: this.key,
|
|
2645
3588
|
kind: "workflow",
|
|
2646
|
-
deadline: deadline.unixMilliseconds,
|
|
2647
|
-
ceiling:
|
|
3589
|
+
...(deadline ? { deadline: deadline.unixMilliseconds } : {}),
|
|
3590
|
+
...(ceiling ? { ceiling: ceiling.unixMilliseconds } : {}),
|
|
2648
3591
|
scope: scope?.messages ?? messaging.attempt,
|
|
2649
3592
|
});
|
|
3593
|
+
this.options.tools.opened({
|
|
3594
|
+
id: operationId,
|
|
3595
|
+
agent: this.key,
|
|
3596
|
+
callPath: [],
|
|
3597
|
+
...(scope?.stage === undefined ? {} : { stage: scope.stage }),
|
|
3598
|
+
scope,
|
|
3599
|
+
deadline: () => deadline?.unixMilliseconds,
|
|
3600
|
+
});
|
|
2650
3601
|
// After its prompt: how to use its routes, then each message held for it, wrapped.
|
|
2651
3602
|
const joined = messaging.take(operationId, "joins");
|
|
2652
3603
|
const prompt = [spec.prompt, messaging.lesson(this.key), joined?.text]
|
|
@@ -2663,7 +3614,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2663
3614
|
const recoveryPrompt =
|
|
2664
3615
|
nudge?.prompt ??
|
|
2665
3616
|
(waitingSupported
|
|
2666
|
-
? "Report the result now, or use
|
|
3617
|
+
? "Report the result now, or use wfa waiting with a reason if you still need time."
|
|
2667
3618
|
: "You finished without reporting the requested result. Report it now.");
|
|
2668
3619
|
// The first prompt carries only the result command; waiting is rare, so it is offered only
|
|
2669
3620
|
// once a check-in finds the agent idle without an answer.
|
|
@@ -2675,12 +3626,16 @@ class LogicalAgent implements AgentRef {
|
|
|
2675
3626
|
"Finish or stop work needed for this answer before submitting the result.",
|
|
2676
3627
|
`If waiting for work to finish, report it now using: ${this.options.launcher} waiting ${operationId} --reason 'what you are waiting for' [--timeout 2m]`,
|
|
2677
3628
|
"Omit the square brackets when running the optional timeout flag. This command acknowledges your wait immediately; it does not sleep.",
|
|
2678
|
-
|
|
3629
|
+
...(deadline
|
|
3630
|
+
? [
|
|
3631
|
+
`The answer deadline is ${new Date(deadline.unixMilliseconds).toISOString()}. ${this.options.launcher} waiting never extends it.`,
|
|
3632
|
+
]
|
|
3633
|
+
: []),
|
|
2679
3634
|
].join("\n")
|
|
2680
3635
|
: "");
|
|
2681
3636
|
const events = createOperationEvents(this.options.runDir, operationId);
|
|
2682
3637
|
const messages = this.operationMessages(operationId, `${spec.id!}:message`, {
|
|
2683
|
-
deadline: () => deadline,
|
|
3638
|
+
deadline: () => harnessDeadline(deadline),
|
|
2684
3639
|
authored,
|
|
2685
3640
|
session,
|
|
2686
3641
|
record: events.record,
|
|
@@ -2692,7 +3647,17 @@ class LogicalAgent implements AgentRef {
|
|
|
2692
3647
|
agentId: this.key,
|
|
2693
3648
|
question: spec.prompt,
|
|
2694
3649
|
schema,
|
|
2695
|
-
deadline,
|
|
3650
|
+
...(deadline ? { deadline } : {}),
|
|
3651
|
+
...(ceiling ? { ceiling: ceiling.unixMilliseconds } : {}),
|
|
3652
|
+
extended: (moved, waiting) => {
|
|
3653
|
+
const next = moved === undefined ? undefined : { unixMilliseconds: moved };
|
|
3654
|
+
this.options.slots.extend(operationId, next);
|
|
3655
|
+
if (waiting || moved === undefined || moved <= (deadline?.unixMilliseconds ?? Infinity))
|
|
3656
|
+
return;
|
|
3657
|
+
deadline = next;
|
|
3658
|
+
messaging.extended(operationId, moved);
|
|
3659
|
+
events.record({ kind: "deadline-extended", at: Date.now(), sequence: 0, until: moved });
|
|
3660
|
+
},
|
|
2696
3661
|
slots: this.options.slots,
|
|
2697
3662
|
waitingSupported,
|
|
2698
3663
|
nudge: nudge !== undefined,
|
|
@@ -2702,8 +3667,17 @@ class LogicalAgent implements AgentRef {
|
|
|
2702
3667
|
.start(
|
|
2703
3668
|
{
|
|
2704
3669
|
id: spec.id!,
|
|
2705
|
-
prompt:
|
|
2706
|
-
|
|
3670
|
+
prompt: [
|
|
3671
|
+
operationPrompt(prompt, schema, this.options.launcher, operationId),
|
|
3672
|
+
toolsLesson(
|
|
3673
|
+
this.options.tools.taught(this.key),
|
|
3674
|
+
this.options.launcher,
|
|
3675
|
+
resultSaysQuoting(schema),
|
|
3676
|
+
),
|
|
3677
|
+
]
|
|
3678
|
+
.filter(Boolean)
|
|
3679
|
+
.join("\n\n"),
|
|
3680
|
+
deadline: harnessDeadline(deadline),
|
|
2707
3681
|
authored,
|
|
2708
3682
|
onSession: session,
|
|
2709
3683
|
},
|
|
@@ -2713,17 +3687,24 @@ class LogicalAgent implements AgentRef {
|
|
|
2713
3687
|
successor: (turn, sequence, deliverySignal) =>
|
|
2714
3688
|
turn.nudge({
|
|
2715
3689
|
id: `${spec.id!}:check-in:${sequence}`,
|
|
2716
|
-
deadline,
|
|
3690
|
+
deadline: harnessDeadline(deadline),
|
|
2717
3691
|
deliverySignal,
|
|
2718
3692
|
prompt: checkInPrompt(recoveryPrompt),
|
|
2719
3693
|
// A check-in delivers no messages: those its first prompt carried are not its own.
|
|
2720
3694
|
authored: { ...withoutMessages(authored), prompt: recoveryPrompt },
|
|
2721
3695
|
}),
|
|
2722
|
-
onStop: (
|
|
3696
|
+
onStop: (settle) => {
|
|
3697
|
+
// A cancel or stop reaches a running tool call at once: the turn may be waiting on it.
|
|
3698
|
+
const stop = (request: OperationStop) => {
|
|
3699
|
+
this.options.tools.abort(operationId, request.kind);
|
|
3700
|
+
return settle(request);
|
|
3701
|
+
};
|
|
2723
3702
|
this.#operationStops.add(stop);
|
|
3703
|
+
this.#stopsByTurn.set(spec.id!, stop);
|
|
2724
3704
|
const remove = scope?.add(stop);
|
|
2725
3705
|
return () => {
|
|
2726
3706
|
this.#operationStops.delete(stop);
|
|
3707
|
+
if (this.#stopsByTurn.get(spec.id!) === stop) this.#stopsByTurn.delete(spec.id!);
|
|
2727
3708
|
remove?.();
|
|
2728
3709
|
};
|
|
2729
3710
|
},
|
|
@@ -2741,6 +3722,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2741
3722
|
}),
|
|
2742
3723
|
)
|
|
2743
3724
|
.finally(() => events.close());
|
|
3725
|
+
// A call still running is stopped and waited for: run() resolves once its calls have settled.
|
|
3726
|
+
await this.options.tools.ended(operationId);
|
|
2744
3727
|
// Its wait is dropped and what it still owed fails, before a failure closes the agent.
|
|
2745
3728
|
messaging.ended(operationId, operationEnd(outcome.kind));
|
|
2746
3729
|
if (outcome.cleanupUnresolved) {
|
|
@@ -2757,14 +3740,23 @@ class LogicalAgent implements AgentRef {
|
|
|
2757
3740
|
outcome.charges,
|
|
2758
3741
|
);
|
|
2759
3742
|
if (outcome.kind === "answered")
|
|
2760
|
-
return {
|
|
3743
|
+
return {
|
|
3744
|
+
outcome: { answered: true, kind: "answered", value: outcome.value as JsonValue, usage },
|
|
3745
|
+
};
|
|
2761
3746
|
if (outcome.kind === "failed") {
|
|
2762
3747
|
const login = outcome.login ? { login: outcome.login } : {};
|
|
2763
3748
|
return {
|
|
2764
|
-
outcome: {
|
|
3749
|
+
outcome: {
|
|
3750
|
+
answered: false,
|
|
3751
|
+
kind: "failed",
|
|
3752
|
+
reason: outcome.reason,
|
|
3753
|
+
retryable: false,
|
|
3754
|
+
...login,
|
|
3755
|
+
usage,
|
|
3756
|
+
},
|
|
2765
3757
|
};
|
|
2766
3758
|
}
|
|
2767
|
-
return { outcome: { kind: outcome.kind, reason: outcome.reason, usage } };
|
|
3759
|
+
return { outcome: { answered: false, kind: outcome.kind, reason: outcome.reason, usage } };
|
|
2768
3760
|
}
|
|
2769
3761
|
|
|
2770
3762
|
/**
|
|
@@ -2870,14 +3862,17 @@ class LogicalAgent implements AgentRef {
|
|
|
2870
3862
|
private startMessageOperation(scope: MessagingScope): void {
|
|
2871
3863
|
const owner = this.options.messageOwner(scope);
|
|
2872
3864
|
if (!owner) return;
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3865
|
+
// Whatever code made the message due, a handler included, nothing waits for its operation.
|
|
3866
|
+
const result = outsideTools(() =>
|
|
3867
|
+
this.queue(async () => {
|
|
3868
|
+
if (this.#closed || this.options.isRunClosing()) return;
|
|
3869
|
+
// What is first now: a message dropped since this was queued leaves another route's, which
|
|
3870
|
+
// waits for an operation of its own scope.
|
|
3871
|
+
const opening = this.options.messaging.opening(this.key);
|
|
3872
|
+
if (opening?.scope !== scope) return;
|
|
3873
|
+
await this.executeMessageOperation(opening.from, scope, owner.stage);
|
|
3874
|
+
}),
|
|
3875
|
+
);
|
|
2881
3876
|
this.options.track(result).catch(() => undefined);
|
|
2882
3877
|
owner.adopt(result);
|
|
2883
3878
|
}
|
|
@@ -2889,8 +3884,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2889
3884
|
): Promise<void> {
|
|
2890
3885
|
const { messaging } = this.options;
|
|
2891
3886
|
const operationId = randomUUID();
|
|
2892
|
-
const ceiling = this.deadlineCeiling(stage)
|
|
2893
|
-
let deadline = Math.min(Date.now() + messaging.limits.operationMs, ceiling);
|
|
3887
|
+
const ceiling = this.deadlineCeiling(stage)?.unixMilliseconds;
|
|
3888
|
+
let deadline = Math.min(Date.now() + messaging.limits.operationMs, ceiling ?? Infinity);
|
|
2894
3889
|
messaging.opened({
|
|
2895
3890
|
id: operationId,
|
|
2896
3891
|
agent: this.key,
|
|
@@ -2899,6 +3894,14 @@ class LogicalAgent implements AgentRef {
|
|
|
2899
3894
|
ceiling,
|
|
2900
3895
|
scope,
|
|
2901
3896
|
});
|
|
3897
|
+
this.options.tools.opened({
|
|
3898
|
+
id: operationId,
|
|
3899
|
+
agent: this.key,
|
|
3900
|
+
callPath: [],
|
|
3901
|
+
...(stage?.stage === undefined ? {} : { stage: stage.stage }),
|
|
3902
|
+
scope: stage,
|
|
3903
|
+
deadline: () => deadline,
|
|
3904
|
+
});
|
|
2902
3905
|
/** How it ended; unset until its supervisor says, or if it never opened. */
|
|
2903
3906
|
let end: SupervisedOutcome["kind"] | undefined;
|
|
2904
3907
|
try {
|
|
@@ -2934,9 +3937,13 @@ class LogicalAgent implements AgentRef {
|
|
|
2934
3937
|
}
|
|
2935
3938
|
throw error;
|
|
2936
3939
|
} finally {
|
|
3940
|
+
// Its agent ended its turn with a call running, say: the call is stopped and waited for, so
|
|
3941
|
+
// nothing after it, nor the next operation, runs with this one's call still running.
|
|
3942
|
+
await this.options.tools.ended(operationId);
|
|
2937
3943
|
await this.options.afterOperation?.();
|
|
2938
3944
|
}
|
|
2939
3945
|
} finally {
|
|
3946
|
+
await this.options.tools.ended(operationId);
|
|
2940
3947
|
// A message operation that simply ends settles; its wait is dropped and what it owed fails.
|
|
2941
3948
|
messaging.ended(
|
|
2942
3949
|
operationId,
|
|
@@ -2968,16 +3975,32 @@ class LogicalAgent implements AgentRef {
|
|
|
2968
3975
|
cause,
|
|
2969
3976
|
messages: opening.messages,
|
|
2970
3977
|
};
|
|
3978
|
+
const extended = (later: number) => {
|
|
3979
|
+
if (later <= deadline) return;
|
|
3980
|
+
deadline = later;
|
|
3981
|
+
messaging.extended(operationId, later);
|
|
3982
|
+
this.options.tools.extended(operationId, later);
|
|
3983
|
+
events.record({ kind: "deadline-extended", at: Date.now(), sequence: 0, until: later });
|
|
3984
|
+
};
|
|
3985
|
+
const ceiling = this.deadlineCeiling(stage)?.unixMilliseconds;
|
|
2971
3986
|
let outcome: SupervisedOutcome;
|
|
2972
3987
|
try {
|
|
2973
3988
|
outcome = await superviseMessageOperation({
|
|
2974
3989
|
deadline: () => deadline,
|
|
3990
|
+
...(ceiling === undefined ? {} : { ceiling }),
|
|
3991
|
+
extended,
|
|
2975
3992
|
start: () =>
|
|
2976
3993
|
this.options.session
|
|
2977
3994
|
.start(
|
|
2978
3995
|
{
|
|
2979
3996
|
id: operationId,
|
|
2980
|
-
|
|
3997
|
+
// A message turn's tools block, as a workflow turn's: after what it opens on.
|
|
3998
|
+
prompt: [
|
|
3999
|
+
opening.text,
|
|
4000
|
+
toolsLesson(this.options.tools.taught(this.key), this.options.launcher, false),
|
|
4001
|
+
]
|
|
4002
|
+
.filter(Boolean)
|
|
4003
|
+
.join("\n\n"),
|
|
2981
4004
|
deadline: { unixMilliseconds: deadline },
|
|
2982
4005
|
authored,
|
|
2983
4006
|
onSession: operation.session,
|
|
@@ -2990,14 +4013,13 @@ class LogicalAgent implements AgentRef {
|
|
|
2990
4013
|
authored,
|
|
2991
4014
|
session: operation.session,
|
|
2992
4015
|
record: events.record,
|
|
2993
|
-
extended
|
|
2994
|
-
if (later <= deadline) return;
|
|
2995
|
-
deadline = later;
|
|
2996
|
-
messaging.extended(operationId, later);
|
|
2997
|
-
events.record({ kind: "deadline-extended", at: Date.now(), sequence: 0, until: later });
|
|
2998
|
-
},
|
|
4016
|
+
extended,
|
|
2999
4017
|
}),
|
|
3000
|
-
onStop: (
|
|
4018
|
+
onStop: (settle) => {
|
|
4019
|
+
const stop = (request: OperationStop) => {
|
|
4020
|
+
this.options.tools.abort(operationId, request.kind);
|
|
4021
|
+
return settle(request);
|
|
4022
|
+
};
|
|
3001
4023
|
this.#operationStops.add(stop);
|
|
3002
4024
|
const remove = stage?.add(stop);
|
|
3003
4025
|
return () => {
|
|
@@ -3030,11 +4052,12 @@ class LogicalAgent implements AgentRef {
|
|
|
3030
4052
|
// Recorded `answered` with no value: it did what it was woken for, and takes no result.
|
|
3031
4053
|
this.turnEnded(
|
|
3032
4054
|
outcome.kind === "answered"
|
|
3033
|
-
? { kind: "answered", value: null, usage }
|
|
4055
|
+
? { answered: true, kind: "answered", value: null, usage }
|
|
3034
4056
|
: outcome.kind === "failed"
|
|
3035
|
-
? { kind: "failed", reason: outcome.reason, retryable: false, usage }
|
|
3036
|
-
: { kind: outcome.kind, reason: outcome.reason, usage },
|
|
4057
|
+
? { answered: false, kind: "failed", reason: outcome.reason, retryable: false, usage }
|
|
4058
|
+
: { answered: false, kind: outcome.kind, reason: outcome.reason, usage },
|
|
3037
4059
|
"turn",
|
|
4060
|
+
true,
|
|
3038
4061
|
);
|
|
3039
4062
|
return outcome;
|
|
3040
4063
|
}
|
|
@@ -3067,17 +4090,23 @@ class LogicalAgent implements AgentRef {
|
|
|
3067
4090
|
later,
|
|
3068
4091
|
);
|
|
3069
4092
|
if (native === "expired") {
|
|
3070
|
-
return {
|
|
4093
|
+
return {
|
|
4094
|
+
answered: false,
|
|
4095
|
+
kind: "timed-out",
|
|
4096
|
+
reason: "compaction deadline exceeded",
|
|
4097
|
+
usage,
|
|
4098
|
+
};
|
|
3071
4099
|
}
|
|
3072
4100
|
if (native.state === "completed") {
|
|
3073
4101
|
return native.summary === undefined
|
|
3074
4102
|
? {
|
|
4103
|
+
answered: false,
|
|
3075
4104
|
kind: "failed",
|
|
3076
4105
|
reason: native.detail ?? "the harness did not confirm a compaction",
|
|
3077
4106
|
retryable: false,
|
|
3078
4107
|
usage,
|
|
3079
4108
|
}
|
|
3080
|
-
: { kind: "answered", value: native.summary, usage };
|
|
4109
|
+
: { answered: true, kind: "answered", value: native.summary, usage };
|
|
3081
4110
|
}
|
|
3082
4111
|
return reconcile<string>(native, usage);
|
|
3083
4112
|
};
|
|
@@ -3144,6 +4173,94 @@ class LogicalAgent implements AgentRef {
|
|
|
3144
4173
|
}
|
|
3145
4174
|
}
|
|
3146
4175
|
|
|
4176
|
+
/** A sleep's time, checked where it is given. */
|
|
4177
|
+
function checkedWhen(when: unknown): string | Date {
|
|
4178
|
+
if (when instanceof Date || typeof when === "string") return when;
|
|
4179
|
+
if (readOf(when))
|
|
4180
|
+
throw new Error(
|
|
4181
|
+
"sleep is for a duration or a Date; wf.at() is a select's branch, read at each of its waits",
|
|
4182
|
+
);
|
|
4183
|
+
throw new Error('sleep is for a duration, "2h", or until a Date');
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
function checkedLabel(label: unknown): string | undefined {
|
|
4187
|
+
if (label !== undefined && typeof label !== "string")
|
|
4188
|
+
throw new Error("a wait's label is a string");
|
|
4189
|
+
return label;
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
/** A wait's branches, by name, checked: a race may give a duration where a select may not. */
|
|
4193
|
+
function branchesOf(what: string, branches: unknown, durations: boolean): [string, unknown][] {
|
|
4194
|
+
if (typeof branches !== "object" || branches === null || Array.isArray(branches))
|
|
4195
|
+
throw new Error(`${what} takes its branches as an object, { name: source }`);
|
|
4196
|
+
const entries = Object.entries(branches);
|
|
4197
|
+
if (entries.length === 0) throw new Error(`${what} has no branches: it would never return`);
|
|
4198
|
+
if ("key" in branches) throw new Error(`${what}: no branch is named key, which the answer uses`);
|
|
4199
|
+
for (const [branch, source] of entries)
|
|
4200
|
+
if (typeof source === "string" && !durations)
|
|
4201
|
+
throw new Error(`${what}'s ${branch} is a duration: a timer in a loop is wf.timer()'s`);
|
|
4202
|
+
return entries;
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
/** What a time read from state reads, by its source. */
|
|
4206
|
+
const timeReads = new WeakMap<object, () => Date | null>();
|
|
4207
|
+
|
|
4208
|
+
/** `wf.at(read)`: a select's branch, due at the time `read` returns. */
|
|
4209
|
+
function timeRead(read: () => Date | null): TimeRead {
|
|
4210
|
+
if (typeof read !== "function")
|
|
4211
|
+
throw new Error(
|
|
4212
|
+
"wf.at takes a function that reads a Date or null from the run's state, as () => ledger.nextNudge()",
|
|
4213
|
+
);
|
|
4214
|
+
const value = Object.freeze({});
|
|
4215
|
+
timeReads.set(value, read);
|
|
4216
|
+
return value as unknown as TimeRead;
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
/** What a time read from state reads; undefined for any other value. */
|
|
4220
|
+
function readOf(value: unknown): (() => Date | null) | undefined {
|
|
4221
|
+
return typeof value === "object" && value !== null ? timeReads.get(value) : undefined;
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
/** What `source` takes, as a branch gives it. */
|
|
4225
|
+
function sourceTake(what: string, branch: string, source: unknown): WaitTake {
|
|
4226
|
+
const take = takeOf(source);
|
|
4227
|
+
if (take) return take;
|
|
4228
|
+
if (readOf(source))
|
|
4229
|
+
throw new Error(
|
|
4230
|
+
`${what}'s ${branch} is wf.at(), a select's: a race answers once, so it takes a timer, wf.timer(name, date)`,
|
|
4231
|
+
);
|
|
4232
|
+
throw new Error(
|
|
4233
|
+
`${what}'s ${branch} is not a source: a timer, a signal, a participant routed to the code, an agent, or a started turn`,
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
/** One of the workflow's own events, by name, as a source. */
|
|
4238
|
+
function signalSource(name: string): unknown {
|
|
4239
|
+
if (typeof name !== "string" || name.includes(":"))
|
|
4240
|
+
throw new Error(`a signal is one of the workflow's own events, by name; not ${String(name)}`);
|
|
4241
|
+
return source({ name }, { name });
|
|
4242
|
+
}
|
|
4243
|
+
|
|
4244
|
+
/** Timer `name`'s source. */
|
|
4245
|
+
function timerSource(mailbox: RunMailbox, name: string, due: string): Timer {
|
|
4246
|
+
return source(
|
|
4247
|
+
{ name, due, cancel: () => mailbox.cancelTimer(name) },
|
|
4248
|
+
{ name: "timer:due", from: name },
|
|
4249
|
+
) as unknown as Timer;
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
/** A started turn, as the code holds it: its id, its cancel, and a source of its end. */
|
|
4253
|
+
function turnRef(
|
|
4254
|
+
agent: string,
|
|
4255
|
+
id: string,
|
|
4256
|
+
cancel: (reason?: string) => Promise<boolean>,
|
|
4257
|
+
): StartedTurn {
|
|
4258
|
+
return source(
|
|
4259
|
+
{ id, cancel },
|
|
4260
|
+
{ name: "agent:ended", from: agent, turn: id },
|
|
4261
|
+
) as unknown as StartedTurn;
|
|
4262
|
+
}
|
|
4263
|
+
|
|
3147
4264
|
class ExecutionScope {
|
|
3148
4265
|
readonly #cancellers = new Set<(stop: OperationStop) => Promise<unknown>>();
|
|
3149
4266
|
readonly #owned = new Set<Promise<unknown>>();
|
|
@@ -3153,7 +4270,7 @@ class ExecutionScope {
|
|
|
3153
4270
|
#cancellation: Promise<void> | undefined;
|
|
3154
4271
|
|
|
3155
4272
|
constructor(
|
|
3156
|
-
readonly deadline: AbsoluteDeadline,
|
|
4273
|
+
readonly deadline: AbsoluteDeadline | undefined,
|
|
3157
4274
|
/** The labelled `parallel` it runs in, for progress. */
|
|
3158
4275
|
readonly group?: GroupProgress,
|
|
3159
4276
|
/** The workflow stage everything in this scope runs in. */
|
|
@@ -3167,8 +4284,8 @@ class ExecutionScope {
|
|
|
3167
4284
|
}
|
|
3168
4285
|
|
|
3169
4286
|
assertActive(): void {
|
|
3170
|
-
if (this.#cancelled || Date.now() >= this.deadline.unixMilliseconds) {
|
|
3171
|
-
throw new DeadlineExceededError(this.deadline);
|
|
4287
|
+
if (this.#cancelled || (this.deadline && Date.now() >= this.deadline.unixMilliseconds)) {
|
|
4288
|
+
throw new DeadlineExceededError(this.deadline ?? { unixMilliseconds: Date.now() });
|
|
3172
4289
|
}
|
|
3173
4290
|
}
|
|
3174
4291
|
|
|
@@ -3236,7 +4353,7 @@ class ExecutionScope {
|
|
|
3236
4353
|
function runParallel<Item, Result>(
|
|
3237
4354
|
items: readonly Item[],
|
|
3238
4355
|
operation: (item: Item, index: number) => Promise<Result>,
|
|
3239
|
-
deadline: AbsoluteDeadline,
|
|
4356
|
+
deadline: AbsoluteDeadline | undefined,
|
|
3240
4357
|
requestedConcurrency?: number,
|
|
3241
4358
|
openGroup?: () => GroupProgress | undefined,
|
|
3242
4359
|
): Promise<Result[]> {
|
|
@@ -3256,7 +4373,7 @@ function runParallel<Item, Result>(
|
|
|
3256
4373
|
async function executeParallel<Item, Result>(
|
|
3257
4374
|
items: readonly Item[],
|
|
3258
4375
|
operation: (item: Item, index: number) => Promise<Result>,
|
|
3259
|
-
deadline: AbsoluteDeadline,
|
|
4376
|
+
deadline: AbsoluteDeadline | undefined,
|
|
3260
4377
|
concurrency: number,
|
|
3261
4378
|
parent: ExecutionScope | undefined,
|
|
3262
4379
|
group?: GroupProgress,
|
|
@@ -3293,6 +4410,7 @@ async function executeParallel<Item, Result>(
|
|
|
3293
4410
|
});
|
|
3294
4411
|
let cancelTimer: (() => void) | undefined;
|
|
3295
4412
|
const expiry = new Promise<never>((_resolve, reject) => {
|
|
4413
|
+
if (!deadline) return;
|
|
3296
4414
|
cancelTimer = scheduleAt(deadline, () => {
|
|
3297
4415
|
void scope
|
|
3298
4416
|
.cancel({ kind: "timed-out", reason: "parallel deadline exceeded" })
|
|
@@ -3301,9 +4419,9 @@ async function executeParallel<Item, Result>(
|
|
|
3301
4419
|
});
|
|
3302
4420
|
try {
|
|
3303
4421
|
await Promise.race([completion, expiry]);
|
|
3304
|
-
if (scope.cancelled || Date.now() >= deadline.unixMilliseconds) {
|
|
4422
|
+
if (scope.cancelled || (deadline && Date.now() >= deadline.unixMilliseconds)) {
|
|
3305
4423
|
await scope.cancel({ kind: "timed-out", reason: "parallel deadline exceeded" });
|
|
3306
|
-
throw new DeadlineExceededError(deadline);
|
|
4424
|
+
throw new DeadlineExceededError(deadline ?? { unixMilliseconds: Date.now() });
|
|
3307
4425
|
}
|
|
3308
4426
|
return results;
|
|
3309
4427
|
} catch (error) {
|
|
@@ -3343,20 +4461,41 @@ function reconcile<T extends JsonValue>(
|
|
|
3343
4461
|
): TurnOutcome<T> {
|
|
3344
4462
|
switch (native.state) {
|
|
3345
4463
|
case "completed":
|
|
3346
|
-
return {
|
|
4464
|
+
return {
|
|
4465
|
+
answered: false,
|
|
4466
|
+
kind: "unanswered",
|
|
4467
|
+
reason: "agent settled without an accepted result",
|
|
4468
|
+
usage,
|
|
4469
|
+
};
|
|
3347
4470
|
case "blocked":
|
|
3348
|
-
return {
|
|
4471
|
+
return {
|
|
4472
|
+
answered: false,
|
|
4473
|
+
kind: "blocked",
|
|
4474
|
+
reason: native.detail ?? "agent is blocked",
|
|
4475
|
+
usage,
|
|
4476
|
+
};
|
|
3349
4477
|
case "timed-out":
|
|
3350
|
-
return {
|
|
4478
|
+
return {
|
|
4479
|
+
answered: false,
|
|
4480
|
+
kind: "timed-out",
|
|
4481
|
+
reason: native.detail ?? "operation timed out",
|
|
4482
|
+
usage,
|
|
4483
|
+
};
|
|
3351
4484
|
case "failed":
|
|
3352
4485
|
return {
|
|
4486
|
+
answered: false,
|
|
3353
4487
|
kind: "failed",
|
|
3354
4488
|
reason: native.detail ?? "harness operation failed",
|
|
3355
4489
|
retryable: false,
|
|
3356
4490
|
usage,
|
|
3357
4491
|
};
|
|
3358
4492
|
case "cancelled":
|
|
3359
|
-
return {
|
|
4493
|
+
return {
|
|
4494
|
+
answered: false,
|
|
4495
|
+
kind: "cancelled",
|
|
4496
|
+
reason: native.detail ?? "operation cancelled",
|
|
4497
|
+
usage,
|
|
4498
|
+
};
|
|
3360
4499
|
}
|
|
3361
4500
|
}
|
|
3362
4501
|
|
|
@@ -3470,6 +4609,11 @@ function resultSchema<T extends JsonValue>(schema: OutputSchema<T> | undefined):
|
|
|
3470
4609
|
return schema ? parseJsonSchema(schema) : { type: "string" };
|
|
3471
4610
|
}
|
|
3472
4611
|
|
|
4612
|
+
function resultSaysQuoting(schema: JsonSchema): boolean {
|
|
4613
|
+
const simple = simpleInput(schema);
|
|
4614
|
+
return simple !== undefined && takesText(simple);
|
|
4615
|
+
}
|
|
4616
|
+
|
|
3473
4617
|
function operationPrompt(
|
|
3474
4618
|
prompt: string,
|
|
3475
4619
|
schema: JsonSchema,
|
|
@@ -3480,7 +4624,19 @@ function operationPrompt(
|
|
|
3480
4624
|
// A quoted heredoc passes the JSON through untouched: a quoted argument broke on shell quoting
|
|
3481
4625
|
// (story 002). Not indented: a copied terminator with leading spaces never closes the heredoc.
|
|
3482
4626
|
// The schema itself, not a rendering of it: without its bounds, first answers were 0/160 valid
|
|
3483
|
-
// (E5). `
|
|
4627
|
+
// (E5). `wfa` says what to do about a rejection, so the prompt does not. A simple schema is taught
|
|
4628
|
+
// as flags instead, its usage line carrying every bound, which held them as well (story 035, M1).
|
|
4629
|
+
const simple = simpleInput(schema);
|
|
4630
|
+
if (simple) {
|
|
4631
|
+
return [
|
|
4632
|
+
prompt,
|
|
4633
|
+
"",
|
|
4634
|
+
"When the answer is ready, return it by running:",
|
|
4635
|
+
"",
|
|
4636
|
+
`${launcher} result ${callId} ${usageLine(simple)}`,
|
|
4637
|
+
...(takesText(simple) ? ["", QUOTING_RULE] : []),
|
|
4638
|
+
].join("\n");
|
|
4639
|
+
}
|
|
3484
4640
|
return [
|
|
3485
4641
|
prompt,
|
|
3486
4642
|
"",
|
|
@@ -3664,3 +4820,27 @@ function stoppedError(key: string, reason: string): Error {
|
|
|
3664
4820
|
function unavailable(name: string): Promise<never> {
|
|
3665
4821
|
return Promise.reject(new Error(`${name} is not implemented by the minimum workflow runner`));
|
|
3666
4822
|
}
|
|
4823
|
+
|
|
4824
|
+
/**
|
|
4825
|
+
* Each root agent's last native session in a run's turns, where its harness can resume it: what a
|
|
4826
|
+
* event run's continue reopens it on.
|
|
4827
|
+
*/
|
|
4828
|
+
export function keptSessionsOf(turns: readonly TurnRecord[]): Map<string, NativeFork> {
|
|
4829
|
+
const kept = new Map<string, NativeFork>();
|
|
4830
|
+
for (const turn of turns) {
|
|
4831
|
+
if ((turn.callPath?.length ?? 0) > 0) continue;
|
|
4832
|
+
const session = turn.sessions.at(-1);
|
|
4833
|
+
if (session) kept.set(turn.agent, { harness: session.harness, sessionRef: session.id });
|
|
4834
|
+
}
|
|
4835
|
+
return kept;
|
|
4836
|
+
}
|
|
4837
|
+
|
|
4838
|
+
/**
|
|
4839
|
+
* In a run of a workflow with `events`: how an agent records a turn `start()` begins and its end,
|
|
4840
|
+
* and the turn an earlier attempt started whose end no wait took, which `turn` is from the start.
|
|
4841
|
+
*/
|
|
4842
|
+
type LogicalAgentTurns = {
|
|
4843
|
+
begin(turn: TurnId, meta: JsonValue, execution: AgentExecution): Promise<void>;
|
|
4844
|
+
end(turn: TurnId, meta: JsonValue, outcome: TurnOutcome<JsonValue>): Promise<void>;
|
|
4845
|
+
restored(): RestoredTurn | undefined;
|
|
4846
|
+
};
|