@agentswf/engine 0.1.0-beta.2 → 0.1.0-beta.3
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 -5
- package/src/allowance-command.ts +29 -7
- 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 +72 -21
- 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 +130 -0
- package/src/emit.ts +246 -0
- package/src/files.ts +11 -1
- package/src/here.ts +8 -2
- package/src/hold-run-lock.fixture.ts +8 -0
- package/src/holder-command.ts +297 -0
- package/src/inbox-command.ts +727 -0
- package/src/init-command.ts +27 -0
- package/src/init-template/AGENTS.md.template +1 -1
- 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 +104 -20
- package/src/machine.ts +12 -1
- package/src/mailbox.ts +977 -0
- package/src/message-operation.ts +26 -3
- package/src/messaging/messaging.ts +458 -65
- package/src/messaging/routes.ts +10 -3
- package/src/messaging/text.ts +8 -5
- package/src/operation-liveness.ts +54 -8
- package/src/operator-cli.ts +290 -119
- package/src/operator-runtime.ts +5 -2
- 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 +36 -32
- 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 +1 -1
- package/src/run-view.ts +0 -9
- package/src/runs-command.ts +46 -17
- package/src/runs.ts +142 -23
- package/src/sandboxes.ts +8 -2
- package/src/simple-input.ts +258 -0
- package/src/socket-exchange.ts +26 -0
- package/src/sources.ts +70 -0
- package/src/stage-ledger.ts +88 -13
- package/src/stage-plan.ts +23 -0
- package/src/stop-crashed.ts +39 -0
- package/src/test-command.ts +32 -0
- package/src/testing.ts +71 -3
- package/src/tick-command.ts +87 -0
- package/src/tools.ts +636 -0
- package/src/wake.ts +264 -0
- package/src/workflow-loader.ts +32 -22
- package/src/workflow-runner.ts +1210 -77
- package/src/workflow-testing/coordinator.typecheck.ts +55 -0
- package/src/workflow-testing/coordinators.ts +137 -0
- package/src/workflow-testing/host.ts +61 -1
- package/src/workflow-testing/index.ts +488 -33
- package/src/workflow-testing/script.ts +72 -2
- 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, 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 `workflow.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 `workflow.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 `workflow.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. */
|
|
@@ -728,6 +897,8 @@ class WorkflowOwner {
|
|
|
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,13 +931,31 @@ 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
960
|
launcher: (key) => this.#logical.get(key)?.launcher ?? "wf",
|
|
770
961
|
changed: (key) => {
|
|
@@ -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("workflow.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("workflow.after()");
|
|
1172
|
+
if (typeof duration !== "string" || !/^\d+(?:\.\d+)?[dhm]$/.test(duration))
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`workflow.after takes days, hours or minutes, "1d", "3h", "1.5h"; not ${String(duration)}`,
|
|
1175
|
+
);
|
|
1176
|
+
return new Date(parseWhen(duration, options.now()).due);
|
|
1177
|
+
},
|
|
1178
|
+
signal: (name: string) => signalSource(name),
|
|
1179
|
+
...(mailbox.started ? { started: mailbox.started } : {}),
|
|
1180
|
+
}
|
|
1181
|
+
: { receive: () => Promise.reject(new Error("receive needs a workflow with events")) },
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
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 workflow.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}: workflow.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,172 @@ 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
|
+
kind: "failed",
|
|
2992
|
+
reason: messageOf(error),
|
|
2993
|
+
retryable: false,
|
|
2994
|
+
usage: unrunUsage(this.key, id, this.options.execution),
|
|
2995
|
+
}),
|
|
2996
|
+
)
|
|
2997
|
+
.then(async (outcome) => {
|
|
2998
|
+
await turns.end(id, meta, outcome);
|
|
2999
|
+
written = outcome.kind;
|
|
3000
|
+
})
|
|
3001
|
+
// An end that can't be written leaves the turn set for good: the run can't go on.
|
|
3002
|
+
.catch((error: unknown) =>
|
|
3003
|
+
this.options.cleanupUnresolved(
|
|
3004
|
+
`agent ${this.key}'s turn ${id} ended, and its end was not written: ${messageOf(error)}`,
|
|
3005
|
+
),
|
|
3006
|
+
)
|
|
3007
|
+
.finally(() => {
|
|
3008
|
+
started.ended = true;
|
|
3009
|
+
ended.resolve();
|
|
3010
|
+
}),
|
|
3011
|
+
);
|
|
3012
|
+
return started.ref;
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
#refusedWhileStarted(what: string): Error | undefined {
|
|
3016
|
+
// First: from a handler this agent is in, its turn waits for the handler, started or not.
|
|
3017
|
+
const inTool = refusedInTool(this.key, `${what}()`);
|
|
3018
|
+
if (inTool) return inTool;
|
|
3019
|
+
if (this.#started && !this.#launching)
|
|
3020
|
+
return new Error(
|
|
3021
|
+
`agent ${this.key} has a turn started (${this.#started.ref.id}) whose end no wait has taken; ${what}() waits for it`,
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
is<Meta extends JsonValue = JsonValue, T extends JsonValue = JsonValue>(event: {
|
|
3026
|
+
readonly name: string;
|
|
3027
|
+
readonly from: string;
|
|
3028
|
+
}): event is AgentEndedEvent<Meta, T> & Matched {
|
|
3029
|
+
return event.name === "agent:ended" && event.from === this.key;
|
|
2127
3030
|
}
|
|
2128
3031
|
|
|
2129
3032
|
run(spec: AgentRunTextSpec): Promise<RunResult<string>>;
|
|
@@ -2132,6 +3035,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2132
3035
|
spec: AgentRunTextSpec | AgentRunStructuredSpec<T>,
|
|
2133
3036
|
): Promise<RunResult<string | T>> {
|
|
2134
3037
|
if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
|
|
3038
|
+
const refused = this.#refusedWhileStarted("run");
|
|
3039
|
+
if (refused) return Promise.reject(refused);
|
|
2135
3040
|
const scope = scopes.getStore();
|
|
2136
3041
|
try {
|
|
2137
3042
|
scope?.assertAccepting();
|
|
@@ -2139,6 +3044,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2139
3044
|
} catch (error) {
|
|
2140
3045
|
return Promise.reject(error);
|
|
2141
3046
|
}
|
|
3047
|
+
const opensWindow = !inTimeoutHandler();
|
|
2142
3048
|
const id = spec.id ?? randomUUID();
|
|
2143
3049
|
const completeSpec = structuredClone({ ...spec, id }) as
|
|
2144
3050
|
| AgentRunTextSpec
|
|
@@ -2155,7 +3061,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2155
3061
|
scope?.track(existing.result);
|
|
2156
3062
|
return existing.result as Promise<RunResult<string | T>>;
|
|
2157
3063
|
}
|
|
2158
|
-
let deadline: AbsoluteDeadline;
|
|
3064
|
+
let deadline: AbsoluteDeadline | undefined;
|
|
2159
3065
|
try {
|
|
2160
3066
|
deadline = this.operationDeadline(spec, scope);
|
|
2161
3067
|
const recoveryDeadline = spec.nudge ? spec.nudge.deadline : undefined;
|
|
@@ -2173,13 +3079,24 @@ class LogicalAgent implements AgentRef {
|
|
|
2173
3079
|
throw new Error("logical agent is closed");
|
|
2174
3080
|
}
|
|
2175
3081
|
scope?.assertActive();
|
|
3082
|
+
const cancelled = this.#cancelledTurns.get(id);
|
|
3083
|
+
if (cancelled !== undefined) {
|
|
3084
|
+
this.#cancelledTurns.delete(id);
|
|
3085
|
+
return {
|
|
3086
|
+
outcome: {
|
|
3087
|
+
kind: "cancelled",
|
|
3088
|
+
reason: cancelled,
|
|
3089
|
+
usage: unrunUsage(this.key, id, this.options.execution),
|
|
3090
|
+
},
|
|
3091
|
+
} as RunResult<JsonValue>;
|
|
3092
|
+
}
|
|
2176
3093
|
const tags = tagsOf(scope, completeSpec.label);
|
|
2177
3094
|
// Drawn here, so a turn that throws before it is delivered is still written as failed.
|
|
2178
3095
|
const operation = this.operationLog(randomUUID(), "turn", tags);
|
|
2179
3096
|
this.options.progress.turnStarted(this.key, tags);
|
|
2180
3097
|
try {
|
|
2181
3098
|
const settled = await this.executeOperation(completeSpec, scope, deadline, operation);
|
|
2182
|
-
this.turnEnded(settled.outcome, "turn");
|
|
3099
|
+
this.turnEnded(settled.outcome, "turn", opensWindow);
|
|
2183
3100
|
return settled;
|
|
2184
3101
|
} catch (error) {
|
|
2185
3102
|
operation.failed(error);
|
|
@@ -2197,6 +3114,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2197
3114
|
|
|
2198
3115
|
compact(spec: CompactSpec): Promise<TurnOutcome<string>> {
|
|
2199
3116
|
if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
|
|
3117
|
+
const refused = this.#refusedWhileStarted("compact");
|
|
3118
|
+
if (refused) return Promise.reject(refused);
|
|
2200
3119
|
const scope = scopes.getStore();
|
|
2201
3120
|
try {
|
|
2202
3121
|
scope?.assertAccepting();
|
|
@@ -2220,7 +3139,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2220
3139
|
}
|
|
2221
3140
|
let deadline: AbsoluteDeadline;
|
|
2222
3141
|
try {
|
|
2223
|
-
deadline = this.operationDeadline(spec, scope);
|
|
3142
|
+
deadline = harnessDeadline(this.operationDeadline(spec, scope));
|
|
2224
3143
|
} catch (error) {
|
|
2225
3144
|
const rejected = Promise.reject<TurnOutcome<string>>(error);
|
|
2226
3145
|
scope?.track(rejected);
|
|
@@ -2255,6 +3174,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2255
3174
|
/** Not a turn: progress counts none for it, and it asks the model nothing. */
|
|
2256
3175
|
set(spec: SettingsSpec): Promise<TurnOutcome<null>> {
|
|
2257
3176
|
if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
|
|
3177
|
+
const refused = this.#refusedWhileStarted("set");
|
|
3178
|
+
if (refused) return Promise.reject(refused);
|
|
2258
3179
|
const scope = scopes.getStore();
|
|
2259
3180
|
try {
|
|
2260
3181
|
scope?.assertAccepting();
|
|
@@ -2279,7 +3200,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2279
3200
|
let deadline: AbsoluteDeadline;
|
|
2280
3201
|
try {
|
|
2281
3202
|
change = this.settingsChange(spec);
|
|
2282
|
-
deadline = this.operationDeadline(spec, scope);
|
|
3203
|
+
deadline = harnessDeadline(this.operationDeadline(spec, scope));
|
|
2283
3204
|
} catch (error) {
|
|
2284
3205
|
const rejected = Promise.reject<TurnOutcome<null>>(error);
|
|
2285
3206
|
scope?.track(rejected);
|
|
@@ -2363,7 +3284,16 @@ class LogicalAgent implements AgentRef {
|
|
|
2363
3284
|
}
|
|
2364
3285
|
|
|
2365
3286
|
/** A turn or compaction that settled, as the view shows it and `turns.jsonl` keeps it. */
|
|
2366
|
-
private turnEnded(
|
|
3287
|
+
private turnEnded(
|
|
3288
|
+
outcome: TurnOutcome<JsonValue>,
|
|
3289
|
+
kind: TurnRecord["kind"],
|
|
3290
|
+
opensWindow = false,
|
|
3291
|
+
): void {
|
|
3292
|
+
// A failure has closed it by now, and a closed agent holds no window.
|
|
3293
|
+
if (opensWindow && !this.#closed) {
|
|
3294
|
+
this.#lastEnd = this.options.now();
|
|
3295
|
+
this.#timeoutOwed = true;
|
|
3296
|
+
}
|
|
2367
3297
|
this.#last = outcome.kind;
|
|
2368
3298
|
const reason = "reason" in outcome ? outcome.reason : undefined;
|
|
2369
3299
|
this.options.progress.turnSettled(this.key, outcome.kind, reason);
|
|
@@ -2372,6 +3302,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2372
3302
|
|
|
2373
3303
|
fork(spec: AgentForkSpec): Promise<AgentRef> {
|
|
2374
3304
|
if (this.#closed) return Promise.reject(new Error("logical agent is closed"));
|
|
3305
|
+
const refused = this.#refusedWhileStarted("fork");
|
|
3306
|
+
if (refused) return Promise.reject(refused);
|
|
2375
3307
|
const scope = scopes.getStore();
|
|
2376
3308
|
const deadline = this.deadlineCeiling(scope);
|
|
2377
3309
|
const { session, execution } = this.options;
|
|
@@ -2419,7 +3351,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2419
3351
|
private operationDeadline(
|
|
2420
3352
|
spec: { deadline?: AbsoluteDeadline; timeoutMs?: number },
|
|
2421
3353
|
scope: ExecutionScope | undefined,
|
|
2422
|
-
): AbsoluteDeadline {
|
|
3354
|
+
): AbsoluteDeadline | undefined {
|
|
2423
3355
|
if (spec.deadline && spec.timeoutMs !== undefined) {
|
|
2424
3356
|
throw new Error("an operation cannot specify both deadline and timeoutMs");
|
|
2425
3357
|
}
|
|
@@ -2430,8 +3362,9 @@ class LogicalAgent implements AgentRef {
|
|
|
2430
3362
|
return earlierDeadline(spec.deadline, ceiling);
|
|
2431
3363
|
}
|
|
2432
3364
|
|
|
2433
|
-
|
|
2434
|
-
|
|
3365
|
+
/** The deadlines around an operation: its scope's and the run's, if either has one. */
|
|
3366
|
+
private deadlineCeiling(scope: ExecutionScope | undefined): AbsoluteDeadline | undefined {
|
|
3367
|
+
return earlierDeadline(scope?.deadline, this.options.deadline);
|
|
2435
3368
|
}
|
|
2436
3369
|
|
|
2437
3370
|
/** Its settings once every `set` queued so far has run. */
|
|
@@ -2619,7 +3552,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2619
3552
|
private async executeOperation(
|
|
2620
3553
|
spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
|
|
2621
3554
|
scope: ExecutionScope | undefined,
|
|
2622
|
-
operationDeadline: AbsoluteDeadline,
|
|
3555
|
+
operationDeadline: AbsoluteDeadline | undefined,
|
|
2623
3556
|
operation: OperationLog,
|
|
2624
3557
|
): Promise<RunResult<JsonValue>> {
|
|
2625
3558
|
const nudge = spec.nudge === false ? undefined : (spec.nudge ?? this.defaultNudge());
|
|
@@ -2631,7 +3564,7 @@ class LogicalAgent implements AgentRef {
|
|
|
2631
3564
|
private async executeWaitingOperation(
|
|
2632
3565
|
spec: AgentRunTextSpec | AgentRunStructuredSpec<JsonValue>,
|
|
2633
3566
|
scope: ExecutionScope | undefined,
|
|
2634
|
-
|
|
3567
|
+
initial: AbsoluteDeadline | undefined,
|
|
2635
3568
|
{ operationId, tags, delivered, session }: OperationLog,
|
|
2636
3569
|
nudge: Exclude<AgentRunTextSpec["nudge"], false>,
|
|
2637
3570
|
): Promise<RunResult<JsonValue>> {
|
|
@@ -2639,14 +3572,25 @@ class LogicalAgent implements AgentRef {
|
|
|
2639
3572
|
const binding = { endpoint: this.options.endpoint, operationId };
|
|
2640
3573
|
const entry = this.options.ledger.reserve(operationId, () => this.#execution, tags);
|
|
2641
3574
|
const { messaging } = this.options;
|
|
3575
|
+
// Moves out by any wait on an outside participant, within the deadlines around it.
|
|
3576
|
+
let deadline = initial;
|
|
3577
|
+
const ceiling = this.deadlineCeiling(scope);
|
|
2642
3578
|
messaging.opened({
|
|
2643
3579
|
id: operationId,
|
|
2644
3580
|
agent: this.key,
|
|
2645
3581
|
kind: "workflow",
|
|
2646
|
-
deadline: deadline.unixMilliseconds,
|
|
2647
|
-
ceiling:
|
|
3582
|
+
...(deadline ? { deadline: deadline.unixMilliseconds } : {}),
|
|
3583
|
+
...(ceiling ? { ceiling: ceiling.unixMilliseconds } : {}),
|
|
2648
3584
|
scope: scope?.messages ?? messaging.attempt,
|
|
2649
3585
|
});
|
|
3586
|
+
this.options.tools.opened({
|
|
3587
|
+
id: operationId,
|
|
3588
|
+
agent: this.key,
|
|
3589
|
+
callPath: [],
|
|
3590
|
+
...(scope?.stage === undefined ? {} : { stage: scope.stage }),
|
|
3591
|
+
scope,
|
|
3592
|
+
deadline: () => deadline?.unixMilliseconds,
|
|
3593
|
+
});
|
|
2650
3594
|
// After its prompt: how to use its routes, then each message held for it, wrapped.
|
|
2651
3595
|
const joined = messaging.take(operationId, "joins");
|
|
2652
3596
|
const prompt = [spec.prompt, messaging.lesson(this.key), joined?.text]
|
|
@@ -2675,12 +3619,16 @@ class LogicalAgent implements AgentRef {
|
|
|
2675
3619
|
"Finish or stop work needed for this answer before submitting the result.",
|
|
2676
3620
|
`If waiting for work to finish, report it now using: ${this.options.launcher} waiting ${operationId} --reason 'what you are waiting for' [--timeout 2m]`,
|
|
2677
3621
|
"Omit the square brackets when running the optional timeout flag. This command acknowledges your wait immediately; it does not sleep.",
|
|
2678
|
-
|
|
3622
|
+
...(deadline
|
|
3623
|
+
? [
|
|
3624
|
+
`The answer deadline is ${new Date(deadline.unixMilliseconds).toISOString()}. ${this.options.launcher} waiting never extends it.`,
|
|
3625
|
+
]
|
|
3626
|
+
: []),
|
|
2679
3627
|
].join("\n")
|
|
2680
3628
|
: "");
|
|
2681
3629
|
const events = createOperationEvents(this.options.runDir, operationId);
|
|
2682
3630
|
const messages = this.operationMessages(operationId, `${spec.id!}:message`, {
|
|
2683
|
-
deadline: () => deadline,
|
|
3631
|
+
deadline: () => harnessDeadline(deadline),
|
|
2684
3632
|
authored,
|
|
2685
3633
|
session,
|
|
2686
3634
|
record: events.record,
|
|
@@ -2692,7 +3640,17 @@ class LogicalAgent implements AgentRef {
|
|
|
2692
3640
|
agentId: this.key,
|
|
2693
3641
|
question: spec.prompt,
|
|
2694
3642
|
schema,
|
|
2695
|
-
deadline,
|
|
3643
|
+
...(deadline ? { deadline } : {}),
|
|
3644
|
+
...(ceiling ? { ceiling: ceiling.unixMilliseconds } : {}),
|
|
3645
|
+
extended: (moved, waiting) => {
|
|
3646
|
+
const next = moved === undefined ? undefined : { unixMilliseconds: moved };
|
|
3647
|
+
this.options.slots.extend(operationId, next);
|
|
3648
|
+
if (waiting || moved === undefined || moved <= (deadline?.unixMilliseconds ?? Infinity))
|
|
3649
|
+
return;
|
|
3650
|
+
deadline = next;
|
|
3651
|
+
messaging.extended(operationId, moved);
|
|
3652
|
+
events.record({ kind: "deadline-extended", at: Date.now(), sequence: 0, until: moved });
|
|
3653
|
+
},
|
|
2696
3654
|
slots: this.options.slots,
|
|
2697
3655
|
waitingSupported,
|
|
2698
3656
|
nudge: nudge !== undefined,
|
|
@@ -2702,8 +3660,17 @@ class LogicalAgent implements AgentRef {
|
|
|
2702
3660
|
.start(
|
|
2703
3661
|
{
|
|
2704
3662
|
id: spec.id!,
|
|
2705
|
-
prompt:
|
|
2706
|
-
|
|
3663
|
+
prompt: [
|
|
3664
|
+
operationPrompt(prompt, schema, this.options.launcher, operationId),
|
|
3665
|
+
toolsLesson(
|
|
3666
|
+
this.options.tools.taught(this.key),
|
|
3667
|
+
this.options.launcher,
|
|
3668
|
+
simpleInput(schema) !== undefined,
|
|
3669
|
+
),
|
|
3670
|
+
]
|
|
3671
|
+
.filter(Boolean)
|
|
3672
|
+
.join("\n\n"),
|
|
3673
|
+
deadline: harnessDeadline(deadline),
|
|
2707
3674
|
authored,
|
|
2708
3675
|
onSession: session,
|
|
2709
3676
|
},
|
|
@@ -2713,17 +3680,24 @@ class LogicalAgent implements AgentRef {
|
|
|
2713
3680
|
successor: (turn, sequence, deliverySignal) =>
|
|
2714
3681
|
turn.nudge({
|
|
2715
3682
|
id: `${spec.id!}:check-in:${sequence}`,
|
|
2716
|
-
deadline,
|
|
3683
|
+
deadline: harnessDeadline(deadline),
|
|
2717
3684
|
deliverySignal,
|
|
2718
3685
|
prompt: checkInPrompt(recoveryPrompt),
|
|
2719
3686
|
// A check-in delivers no messages: those its first prompt carried are not its own.
|
|
2720
3687
|
authored: { ...withoutMessages(authored), prompt: recoveryPrompt },
|
|
2721
3688
|
}),
|
|
2722
|
-
onStop: (
|
|
3689
|
+
onStop: (settle) => {
|
|
3690
|
+
// A cancel or stop reaches a running tool call at once: the turn may be waiting on it.
|
|
3691
|
+
const stop = (request: OperationStop) => {
|
|
3692
|
+
this.options.tools.abort(operationId, request.kind);
|
|
3693
|
+
return settle(request);
|
|
3694
|
+
};
|
|
2723
3695
|
this.#operationStops.add(stop);
|
|
3696
|
+
this.#stopsByTurn.set(spec.id!, stop);
|
|
2724
3697
|
const remove = scope?.add(stop);
|
|
2725
3698
|
return () => {
|
|
2726
3699
|
this.#operationStops.delete(stop);
|
|
3700
|
+
if (this.#stopsByTurn.get(spec.id!) === stop) this.#stopsByTurn.delete(spec.id!);
|
|
2727
3701
|
remove?.();
|
|
2728
3702
|
};
|
|
2729
3703
|
},
|
|
@@ -2741,6 +3715,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2741
3715
|
}),
|
|
2742
3716
|
)
|
|
2743
3717
|
.finally(() => events.close());
|
|
3718
|
+
// A call still running is stopped and waited for: run() resolves once its calls have settled.
|
|
3719
|
+
await this.options.tools.ended(operationId);
|
|
2744
3720
|
// Its wait is dropped and what it still owed fails, before a failure closes the agent.
|
|
2745
3721
|
messaging.ended(operationId, operationEnd(outcome.kind));
|
|
2746
3722
|
if (outcome.cleanupUnresolved) {
|
|
@@ -2870,14 +3846,17 @@ class LogicalAgent implements AgentRef {
|
|
|
2870
3846
|
private startMessageOperation(scope: MessagingScope): void {
|
|
2871
3847
|
const owner = this.options.messageOwner(scope);
|
|
2872
3848
|
if (!owner) return;
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3849
|
+
// Whatever code made the message due, a handler included, nothing waits for its operation.
|
|
3850
|
+
const result = outsideTools(() =>
|
|
3851
|
+
this.queue(async () => {
|
|
3852
|
+
if (this.#closed || this.options.isRunClosing()) return;
|
|
3853
|
+
// What is first now: a message dropped since this was queued leaves another route's, which
|
|
3854
|
+
// waits for an operation of its own scope.
|
|
3855
|
+
const opening = this.options.messaging.opening(this.key);
|
|
3856
|
+
if (opening?.scope !== scope) return;
|
|
3857
|
+
await this.executeMessageOperation(opening.from, scope, owner.stage);
|
|
3858
|
+
}),
|
|
3859
|
+
);
|
|
2881
3860
|
this.options.track(result).catch(() => undefined);
|
|
2882
3861
|
owner.adopt(result);
|
|
2883
3862
|
}
|
|
@@ -2889,8 +3868,8 @@ class LogicalAgent implements AgentRef {
|
|
|
2889
3868
|
): Promise<void> {
|
|
2890
3869
|
const { messaging } = this.options;
|
|
2891
3870
|
const operationId = randomUUID();
|
|
2892
|
-
const ceiling = this.deadlineCeiling(stage)
|
|
2893
|
-
let deadline = Math.min(Date.now() + messaging.limits.operationMs, ceiling);
|
|
3871
|
+
const ceiling = this.deadlineCeiling(stage)?.unixMilliseconds;
|
|
3872
|
+
let deadline = Math.min(Date.now() + messaging.limits.operationMs, ceiling ?? Infinity);
|
|
2894
3873
|
messaging.opened({
|
|
2895
3874
|
id: operationId,
|
|
2896
3875
|
agent: this.key,
|
|
@@ -2899,6 +3878,14 @@ class LogicalAgent implements AgentRef {
|
|
|
2899
3878
|
ceiling,
|
|
2900
3879
|
scope,
|
|
2901
3880
|
});
|
|
3881
|
+
this.options.tools.opened({
|
|
3882
|
+
id: operationId,
|
|
3883
|
+
agent: this.key,
|
|
3884
|
+
callPath: [],
|
|
3885
|
+
...(stage?.stage === undefined ? {} : { stage: stage.stage }),
|
|
3886
|
+
scope: stage,
|
|
3887
|
+
deadline: () => deadline,
|
|
3888
|
+
});
|
|
2902
3889
|
/** How it ended; unset until its supervisor says, or if it never opened. */
|
|
2903
3890
|
let end: SupervisedOutcome["kind"] | undefined;
|
|
2904
3891
|
try {
|
|
@@ -2934,9 +3921,13 @@ class LogicalAgent implements AgentRef {
|
|
|
2934
3921
|
}
|
|
2935
3922
|
throw error;
|
|
2936
3923
|
} finally {
|
|
3924
|
+
// Its agent ended its turn with a call running, say: the call is stopped and waited for, so
|
|
3925
|
+
// nothing after it, nor the next operation, runs with this one's call still running.
|
|
3926
|
+
await this.options.tools.ended(operationId);
|
|
2937
3927
|
await this.options.afterOperation?.();
|
|
2938
3928
|
}
|
|
2939
3929
|
} finally {
|
|
3930
|
+
await this.options.tools.ended(operationId);
|
|
2940
3931
|
// A message operation that simply ends settles; its wait is dropped and what it owed fails.
|
|
2941
3932
|
messaging.ended(
|
|
2942
3933
|
operationId,
|
|
@@ -2968,16 +3959,32 @@ class LogicalAgent implements AgentRef {
|
|
|
2968
3959
|
cause,
|
|
2969
3960
|
messages: opening.messages,
|
|
2970
3961
|
};
|
|
3962
|
+
const extended = (later: number) => {
|
|
3963
|
+
if (later <= deadline) return;
|
|
3964
|
+
deadline = later;
|
|
3965
|
+
messaging.extended(operationId, later);
|
|
3966
|
+
this.options.tools.extended(operationId, later);
|
|
3967
|
+
events.record({ kind: "deadline-extended", at: Date.now(), sequence: 0, until: later });
|
|
3968
|
+
};
|
|
3969
|
+
const ceiling = this.deadlineCeiling(stage)?.unixMilliseconds;
|
|
2971
3970
|
let outcome: SupervisedOutcome;
|
|
2972
3971
|
try {
|
|
2973
3972
|
outcome = await superviseMessageOperation({
|
|
2974
3973
|
deadline: () => deadline,
|
|
3974
|
+
...(ceiling === undefined ? {} : { ceiling }),
|
|
3975
|
+
extended,
|
|
2975
3976
|
start: () =>
|
|
2976
3977
|
this.options.session
|
|
2977
3978
|
.start(
|
|
2978
3979
|
{
|
|
2979
3980
|
id: operationId,
|
|
2980
|
-
|
|
3981
|
+
// A message turn's tools block, as a workflow turn's: after what it opens on.
|
|
3982
|
+
prompt: [
|
|
3983
|
+
opening.text,
|
|
3984
|
+
toolsLesson(this.options.tools.taught(this.key), this.options.launcher, false),
|
|
3985
|
+
]
|
|
3986
|
+
.filter(Boolean)
|
|
3987
|
+
.join("\n\n"),
|
|
2981
3988
|
deadline: { unixMilliseconds: deadline },
|
|
2982
3989
|
authored,
|
|
2983
3990
|
onSession: operation.session,
|
|
@@ -2990,14 +3997,13 @@ class LogicalAgent implements AgentRef {
|
|
|
2990
3997
|
authored,
|
|
2991
3998
|
session: operation.session,
|
|
2992
3999
|
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
|
-
},
|
|
4000
|
+
extended,
|
|
2999
4001
|
}),
|
|
3000
|
-
onStop: (
|
|
4002
|
+
onStop: (settle) => {
|
|
4003
|
+
const stop = (request: OperationStop) => {
|
|
4004
|
+
this.options.tools.abort(operationId, request.kind);
|
|
4005
|
+
return settle(request);
|
|
4006
|
+
};
|
|
3001
4007
|
this.#operationStops.add(stop);
|
|
3002
4008
|
const remove = stage?.add(stop);
|
|
3003
4009
|
return () => {
|
|
@@ -3035,6 +4041,7 @@ class LogicalAgent implements AgentRef {
|
|
|
3035
4041
|
? { kind: "failed", reason: outcome.reason, retryable: false, usage }
|
|
3036
4042
|
: { kind: outcome.kind, reason: outcome.reason, usage },
|
|
3037
4043
|
"turn",
|
|
4044
|
+
true,
|
|
3038
4045
|
);
|
|
3039
4046
|
return outcome;
|
|
3040
4047
|
}
|
|
@@ -3144,6 +4151,94 @@ class LogicalAgent implements AgentRef {
|
|
|
3144
4151
|
}
|
|
3145
4152
|
}
|
|
3146
4153
|
|
|
4154
|
+
/** A sleep's time, checked where it is given. */
|
|
4155
|
+
function checkedWhen(when: unknown): string | Date {
|
|
4156
|
+
if (when instanceof Date || typeof when === "string") return when;
|
|
4157
|
+
if (readOf(when))
|
|
4158
|
+
throw new Error(
|
|
4159
|
+
"sleep is for a duration or a Date; workflow.at() is a select's branch, read at each of its waits",
|
|
4160
|
+
);
|
|
4161
|
+
throw new Error('sleep is for a duration, "2h", or until a Date');
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
function checkedLabel(label: unknown): string | undefined {
|
|
4165
|
+
if (label !== undefined && typeof label !== "string")
|
|
4166
|
+
throw new Error("a wait's label is a string");
|
|
4167
|
+
return label;
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
/** A wait's branches, by name, checked: a race may give a duration where a select may not. */
|
|
4171
|
+
function branchesOf(what: string, branches: unknown, durations: boolean): [string, unknown][] {
|
|
4172
|
+
if (typeof branches !== "object" || branches === null || Array.isArray(branches))
|
|
4173
|
+
throw new Error(`${what} takes its branches as an object, { name: source }`);
|
|
4174
|
+
const entries = Object.entries(branches);
|
|
4175
|
+
if (entries.length === 0) throw new Error(`${what} has no branches: it would never return`);
|
|
4176
|
+
if ("key" in branches) throw new Error(`${what}: no branch is named key, which the answer uses`);
|
|
4177
|
+
for (const [branch, source] of entries)
|
|
4178
|
+
if (typeof source === "string" && !durations)
|
|
4179
|
+
throw new Error(`${what}'s ${branch} is a duration: a timer in a loop is workflow.timer()'s`);
|
|
4180
|
+
return entries;
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
/** What a time read from state reads, by its source. */
|
|
4184
|
+
const timeReads = new WeakMap<object, () => Date | null>();
|
|
4185
|
+
|
|
4186
|
+
/** `workflow.at(read)`: a select's branch, due at the time `read` returns. */
|
|
4187
|
+
function timeRead(read: () => Date | null): TimeRead {
|
|
4188
|
+
if (typeof read !== "function")
|
|
4189
|
+
throw new Error(
|
|
4190
|
+
"workflow.at takes a function that reads a Date or null from the run's state, as () => ledger.nextNudge()",
|
|
4191
|
+
);
|
|
4192
|
+
const value = Object.freeze({});
|
|
4193
|
+
timeReads.set(value, read);
|
|
4194
|
+
return value as unknown as TimeRead;
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
/** What a time read from state reads; undefined for any other value. */
|
|
4198
|
+
function readOf(value: unknown): (() => Date | null) | undefined {
|
|
4199
|
+
return typeof value === "object" && value !== null ? timeReads.get(value) : undefined;
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
/** What `source` takes, as a branch gives it. */
|
|
4203
|
+
function sourceTake(what: string, branch: string, source: unknown): WaitTake {
|
|
4204
|
+
const take = takeOf(source);
|
|
4205
|
+
if (take) return take;
|
|
4206
|
+
if (readOf(source))
|
|
4207
|
+
throw new Error(
|
|
4208
|
+
`${what}'s ${branch} is workflow.at(), a select's: a race answers once, so it takes a timer, workflow.timer(name, date)`,
|
|
4209
|
+
);
|
|
4210
|
+
throw new Error(
|
|
4211
|
+
`${what}'s ${branch} is not a source: a timer, a signal, a participant routed to the code, an agent, or a started turn`,
|
|
4212
|
+
);
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
/** One of the workflow's own events, by name, as a source. */
|
|
4216
|
+
function signalSource(name: string): unknown {
|
|
4217
|
+
if (typeof name !== "string" || name.includes(":"))
|
|
4218
|
+
throw new Error(`a signal is one of the workflow's own events, by name; not ${String(name)}`);
|
|
4219
|
+
return source({ name }, { name });
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
/** Timer `name`'s source. */
|
|
4223
|
+
function timerSource(mailbox: RunMailbox, name: string, due: string): Timer {
|
|
4224
|
+
return source(
|
|
4225
|
+
{ name, due, cancel: () => mailbox.cancelTimer(name) },
|
|
4226
|
+
{ name: "timer:due", from: name },
|
|
4227
|
+
) as unknown as Timer;
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
/** A started turn, as the code holds it: its id, its cancel, and a source of its end. */
|
|
4231
|
+
function turnRef(
|
|
4232
|
+
agent: string,
|
|
4233
|
+
id: string,
|
|
4234
|
+
cancel: (reason?: string) => Promise<boolean>,
|
|
4235
|
+
): StartedTurn {
|
|
4236
|
+
return source(
|
|
4237
|
+
{ id, cancel },
|
|
4238
|
+
{ name: "agent:ended", from: agent, turn: id },
|
|
4239
|
+
) as unknown as StartedTurn;
|
|
4240
|
+
}
|
|
4241
|
+
|
|
3147
4242
|
class ExecutionScope {
|
|
3148
4243
|
readonly #cancellers = new Set<(stop: OperationStop) => Promise<unknown>>();
|
|
3149
4244
|
readonly #owned = new Set<Promise<unknown>>();
|
|
@@ -3153,7 +4248,7 @@ class ExecutionScope {
|
|
|
3153
4248
|
#cancellation: Promise<void> | undefined;
|
|
3154
4249
|
|
|
3155
4250
|
constructor(
|
|
3156
|
-
readonly deadline: AbsoluteDeadline,
|
|
4251
|
+
readonly deadline: AbsoluteDeadline | undefined,
|
|
3157
4252
|
/** The labelled `parallel` it runs in, for progress. */
|
|
3158
4253
|
readonly group?: GroupProgress,
|
|
3159
4254
|
/** The workflow stage everything in this scope runs in. */
|
|
@@ -3167,8 +4262,8 @@ class ExecutionScope {
|
|
|
3167
4262
|
}
|
|
3168
4263
|
|
|
3169
4264
|
assertActive(): void {
|
|
3170
|
-
if (this.#cancelled || Date.now() >= this.deadline.unixMilliseconds) {
|
|
3171
|
-
throw new DeadlineExceededError(this.deadline);
|
|
4265
|
+
if (this.#cancelled || (this.deadline && Date.now() >= this.deadline.unixMilliseconds)) {
|
|
4266
|
+
throw new DeadlineExceededError(this.deadline ?? { unixMilliseconds: Date.now() });
|
|
3172
4267
|
}
|
|
3173
4268
|
}
|
|
3174
4269
|
|
|
@@ -3236,7 +4331,7 @@ class ExecutionScope {
|
|
|
3236
4331
|
function runParallel<Item, Result>(
|
|
3237
4332
|
items: readonly Item[],
|
|
3238
4333
|
operation: (item: Item, index: number) => Promise<Result>,
|
|
3239
|
-
deadline: AbsoluteDeadline,
|
|
4334
|
+
deadline: AbsoluteDeadline | undefined,
|
|
3240
4335
|
requestedConcurrency?: number,
|
|
3241
4336
|
openGroup?: () => GroupProgress | undefined,
|
|
3242
4337
|
): Promise<Result[]> {
|
|
@@ -3256,7 +4351,7 @@ function runParallel<Item, Result>(
|
|
|
3256
4351
|
async function executeParallel<Item, Result>(
|
|
3257
4352
|
items: readonly Item[],
|
|
3258
4353
|
operation: (item: Item, index: number) => Promise<Result>,
|
|
3259
|
-
deadline: AbsoluteDeadline,
|
|
4354
|
+
deadline: AbsoluteDeadline | undefined,
|
|
3260
4355
|
concurrency: number,
|
|
3261
4356
|
parent: ExecutionScope | undefined,
|
|
3262
4357
|
group?: GroupProgress,
|
|
@@ -3293,6 +4388,7 @@ async function executeParallel<Item, Result>(
|
|
|
3293
4388
|
});
|
|
3294
4389
|
let cancelTimer: (() => void) | undefined;
|
|
3295
4390
|
const expiry = new Promise<never>((_resolve, reject) => {
|
|
4391
|
+
if (!deadline) return;
|
|
3296
4392
|
cancelTimer = scheduleAt(deadline, () => {
|
|
3297
4393
|
void scope
|
|
3298
4394
|
.cancel({ kind: "timed-out", reason: "parallel deadline exceeded" })
|
|
@@ -3301,9 +4397,9 @@ async function executeParallel<Item, Result>(
|
|
|
3301
4397
|
});
|
|
3302
4398
|
try {
|
|
3303
4399
|
await Promise.race([completion, expiry]);
|
|
3304
|
-
if (scope.cancelled || Date.now() >= deadline.unixMilliseconds) {
|
|
4400
|
+
if (scope.cancelled || (deadline && Date.now() >= deadline.unixMilliseconds)) {
|
|
3305
4401
|
await scope.cancel({ kind: "timed-out", reason: "parallel deadline exceeded" });
|
|
3306
|
-
throw new DeadlineExceededError(deadline);
|
|
4402
|
+
throw new DeadlineExceededError(deadline ?? { unixMilliseconds: Date.now() });
|
|
3307
4403
|
}
|
|
3308
4404
|
return results;
|
|
3309
4405
|
} catch (error) {
|
|
@@ -3480,7 +4576,20 @@ function operationPrompt(
|
|
|
3480
4576
|
// A quoted heredoc passes the JSON through untouched: a quoted argument broke on shell quoting
|
|
3481
4577
|
// (story 002). Not indented: a copied terminator with leading spaces never closes the heredoc.
|
|
3482
4578
|
// The schema itself, not a rendering of it: without its bounds, first answers were 0/160 valid
|
|
3483
|
-
// (E5). `wf` says what to do about a rejection, so the prompt does not.
|
|
4579
|
+
// (E5). `wf` says what to do about a rejection, so the prompt does not. A simple schema is taught
|
|
4580
|
+
// as flags instead, its usage line carrying every bound, which held them as well (story 035, M1).
|
|
4581
|
+
const simple = simpleInput(schema);
|
|
4582
|
+
if (simple) {
|
|
4583
|
+
return [
|
|
4584
|
+
prompt,
|
|
4585
|
+
"",
|
|
4586
|
+
"When the answer is ready, return it by running:",
|
|
4587
|
+
"",
|
|
4588
|
+
`${launcher} result ${callId} ${usageLine(simple)}`,
|
|
4589
|
+
"",
|
|
4590
|
+
QUOTING_RULE,
|
|
4591
|
+
].join("\n");
|
|
4592
|
+
}
|
|
3484
4593
|
return [
|
|
3485
4594
|
prompt,
|
|
3486
4595
|
"",
|
|
@@ -3664,3 +4773,27 @@ function stoppedError(key: string, reason: string): Error {
|
|
|
3664
4773
|
function unavailable(name: string): Promise<never> {
|
|
3665
4774
|
return Promise.reject(new Error(`${name} is not implemented by the minimum workflow runner`));
|
|
3666
4775
|
}
|
|
4776
|
+
|
|
4777
|
+
/**
|
|
4778
|
+
* Each root agent's last native session in a run's turns, where its harness can resume it: what a
|
|
4779
|
+
* event run's continue reopens it on.
|
|
4780
|
+
*/
|
|
4781
|
+
export function keptSessionsOf(turns: readonly TurnRecord[]): Map<string, NativeFork> {
|
|
4782
|
+
const kept = new Map<string, NativeFork>();
|
|
4783
|
+
for (const turn of turns) {
|
|
4784
|
+
if ((turn.callPath?.length ?? 0) > 0) continue;
|
|
4785
|
+
const session = turn.sessions.at(-1);
|
|
4786
|
+
if (session) kept.set(turn.agent, { harness: session.harness, sessionRef: session.id });
|
|
4787
|
+
}
|
|
4788
|
+
return kept;
|
|
4789
|
+
}
|
|
4790
|
+
|
|
4791
|
+
/**
|
|
4792
|
+
* In a run of a workflow with `events`: how an agent records a turn `start()` begins and its end,
|
|
4793
|
+
* and the turn an earlier attempt started whose end no wait took, which `turn` is from the start.
|
|
4794
|
+
*/
|
|
4795
|
+
type LogicalAgentTurns = {
|
|
4796
|
+
begin(turn: TurnId, meta: JsonValue, execution: AgentExecution): Promise<void>;
|
|
4797
|
+
end(turn: TurnId, meta: JsonValue, outcome: TurnOutcome<JsonValue>): Promise<void>;
|
|
4798
|
+
restored(): RestoredTurn | undefined;
|
|
4799
|
+
};
|