@agentswf/engine 0.1.0-beta.1 → 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
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
MessageLimits,
|
|
3
3
|
MessagingEvent,
|
|
4
|
+
OutsideVia,
|
|
4
5
|
RequestEnd,
|
|
5
6
|
RouteEnds,
|
|
6
7
|
UndeliveredReason,
|
|
7
8
|
} from "@agentswf/contract/records";
|
|
8
|
-
import type { PeerView, ReplyCode, SendCode } from "@agentswf/contract/wire";
|
|
9
|
-
import type { AgentKey } from "@agentswf/contract/workflow";
|
|
9
|
+
import type { ParticipantCode, PeerView, ReplyCode, SendCode } from "@agentswf/contract/wire";
|
|
10
|
+
import type { AgentKey, JsonObject } from "@agentswf/contract/workflow";
|
|
10
11
|
import type { AuthoredMessage } from "@agentswf/harness/adapter";
|
|
12
|
+
import { duration } from "../duration";
|
|
11
13
|
import { MESSAGE_LIMITS } from "./limits";
|
|
12
14
|
import {
|
|
13
15
|
directions,
|
|
@@ -65,10 +67,10 @@ export type OperationInfo = {
|
|
|
65
67
|
agent: AgentKey;
|
|
66
68
|
/** A workflow's own `run`, or one a message opened. */
|
|
67
69
|
kind: "workflow" | "message";
|
|
68
|
-
/** Unix milliseconds; a message operation's may move later. */
|
|
69
|
-
deadline
|
|
70
|
-
/** The latest its deadline may move to: the
|
|
71
|
-
ceiling
|
|
70
|
+
/** Unix milliseconds, absent when nothing bounds it; a message operation's may move later. */
|
|
71
|
+
deadline?: number;
|
|
72
|
+
/** The latest its deadline may move to: the deadlines around it, absent when there are none. */
|
|
73
|
+
ceiling?: number;
|
|
72
74
|
/** The scope it runs in: its route's for a message operation, the stage's for a `run`. */
|
|
73
75
|
scope: Scope;
|
|
74
76
|
};
|
|
@@ -100,6 +102,31 @@ export type Delivery = {
|
|
|
100
102
|
deadline?: number;
|
|
101
103
|
};
|
|
102
104
|
|
|
105
|
+
/**
|
|
106
|
+
* What a holder asks for, as `awf reply` and `awf send` do: `coordinator` is `--as`, `to` the
|
|
107
|
+
* question's number within the attempt.
|
|
108
|
+
*/
|
|
109
|
+
export type HolderRequest = {
|
|
110
|
+
coordinator?: AgentKey;
|
|
111
|
+
agent: AgentKey;
|
|
112
|
+
to?: number;
|
|
113
|
+
text: string;
|
|
114
|
+
via: OutsideVia;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** A holder's result: a reply's `message` is the question's number, a send's the new message's. */
|
|
118
|
+
export type HolderResult =
|
|
119
|
+
| { code: "delivered"; message: number }
|
|
120
|
+
| { code: Exclude<ParticipantCode, "not-found">; error: string; choices?: string[] };
|
|
121
|
+
|
|
122
|
+
/** A question open to an outside participant, and when it closes unanswered if it has a limit. */
|
|
123
|
+
export type OpenQuestion = {
|
|
124
|
+
coordinator: AgentKey;
|
|
125
|
+
message: number;
|
|
126
|
+
asker: AgentKey;
|
|
127
|
+
closesAt?: number;
|
|
128
|
+
};
|
|
129
|
+
|
|
103
130
|
/** What is held for an operation whose turn has ended, without deciding anything. */
|
|
104
131
|
export type Pending = "continues" | "held" | "none";
|
|
105
132
|
|
|
@@ -107,8 +134,11 @@ export type Pending = "continues" | "held" | "none";
|
|
|
107
134
|
export type TurnEnd =
|
|
108
135
|
/** Something continues it now: an answer, a failure, a question, or held messages. */
|
|
109
136
|
| { kind: "continue" }
|
|
110
|
-
/**
|
|
111
|
-
|
|
137
|
+
/**
|
|
138
|
+
* It awaits `peer`'s answer, until `until` if the wait has a limit: nothing runs, a pane stays, a
|
|
139
|
+
* headless process exits.
|
|
140
|
+
*/
|
|
141
|
+
| { kind: "wait"; peer: AgentKey; until?: number }
|
|
112
142
|
/** It owes answers: the one reminder, as its next prompt. */
|
|
113
143
|
| { kind: "remind"; text: string }
|
|
114
144
|
/** A workflow's operation with nothing owed or held: 021's check-in. */
|
|
@@ -142,10 +172,18 @@ type Obligation = {
|
|
|
142
172
|
owingOperation?: Operation;
|
|
143
173
|
/** Its owing operation was reminded of it once; the next turn's end that still owes fails it. */
|
|
144
174
|
reminded?: true;
|
|
145
|
-
/**
|
|
146
|
-
|
|
175
|
+
/**
|
|
176
|
+
* The asker waits until then: its `--timeout`, or its operation's deadline as that moves; absent
|
|
177
|
+
* when nothing limits it. On an outside participant, only its limit: the deadline does not count.
|
|
178
|
+
*/
|
|
179
|
+
until?: number;
|
|
147
180
|
/** It has a `--timeout` of its own, which only that fails it at. */
|
|
148
181
|
readonly timed: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* When it closes unanswered, on an outside participant: the route's `timeoutMs` or the asker's
|
|
184
|
+
* shorter `--timeout`, and which of the two.
|
|
185
|
+
*/
|
|
186
|
+
readonly closes?: { at: number; by: "route" | "own"; ms: number };
|
|
149
187
|
state: "open" | RequestEnd;
|
|
150
188
|
/** Its end, set once; a reply after it is refused `expired`. */
|
|
151
189
|
endedAt?: number;
|
|
@@ -155,6 +193,9 @@ type Obligation = {
|
|
|
155
193
|
type Failure = { kind: "no-reply"; for: Operation; from: AgentKey; reason: string };
|
|
156
194
|
type Held = Message | Failure;
|
|
157
195
|
|
|
196
|
+
/** A participant outside the run, which the workflow connected: it has no operation and no inbox. */
|
|
197
|
+
type Outside = { readonly key: AgentKey; released: boolean };
|
|
198
|
+
|
|
158
199
|
type Operation = OperationInfo & {
|
|
159
200
|
awaits?: Obligation;
|
|
160
201
|
readonly owes: Set<Obligation>;
|
|
@@ -184,6 +225,8 @@ export class Messaging {
|
|
|
184
225
|
readonly #obligations: Obligation[] = [];
|
|
185
226
|
/** Every message admitted, by its number. */
|
|
186
227
|
readonly #sentBy = new Map<number, Message>();
|
|
228
|
+
/** Outside participants connected in the attempt, released ones included, whose keys stay taken. */
|
|
229
|
+
readonly #outside = new Map<AgentKey, Outside>();
|
|
187
230
|
#closed = false;
|
|
188
231
|
|
|
189
232
|
constructor(host: MessagingHost, limits: MessageLimits = MESSAGE_LIMITS) {
|
|
@@ -201,7 +244,10 @@ export class Messaging {
|
|
|
201
244
|
* Grants a route in `scope`, atomically: anything invalid rejects with nothing granted. Granting
|
|
202
245
|
* the same route again does nothing; another route over an open pair rejects.
|
|
203
246
|
*/
|
|
204
|
-
grant(
|
|
247
|
+
grant(
|
|
248
|
+
ends: RouteEnds,
|
|
249
|
+
options: { purpose: unknown; maxMessages?: unknown; timeoutMs?: unknown; scope: Scope },
|
|
250
|
+
): void {
|
|
205
251
|
const { scope } = options;
|
|
206
252
|
if (this.#closed || scope.state !== "open")
|
|
207
253
|
throw new Error("routes are granted only while a scope is open");
|
|
@@ -217,7 +263,14 @@ export class Messaging {
|
|
|
217
263
|
const pairs = directions(ends);
|
|
218
264
|
for (const [from, to] of pairs) {
|
|
219
265
|
if (from === to) throw new Error(`a route from ${from} to itself is not allowed`);
|
|
266
|
+
if (this.#outside.has(from) && this.#outside.has(to))
|
|
267
|
+
throw new Error(`${from} and ${to} are both outside the run: nothing would run`);
|
|
220
268
|
for (const key of [from, to]) {
|
|
269
|
+
const outside = this.#outside.get(key);
|
|
270
|
+
if (outside) {
|
|
271
|
+
if (outside.released) throw new Error(`${key} has been released`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
221
274
|
const agent = this.#host.agent(key);
|
|
222
275
|
if (!agent) throw new Error(`${key} is not an agent this workflow opened`);
|
|
223
276
|
if (agent.caller)
|
|
@@ -225,12 +278,30 @@ export class Messaging {
|
|
|
225
278
|
if (agent.state === "stopped") throw new Error(`${key} has stopped`);
|
|
226
279
|
}
|
|
227
280
|
}
|
|
281
|
+
const timeoutMs = options.timeoutMs;
|
|
282
|
+
if (timeoutMs !== undefined) {
|
|
283
|
+
if (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1)
|
|
284
|
+
throw new Error("a route's timeoutMs is a positive whole number of milliseconds");
|
|
285
|
+
// Only where an agent can ask an outside participant: nothing else needs it yet.
|
|
286
|
+
if (!pairs.some(([, to]) => this.#outside.has(to)))
|
|
287
|
+
throw new Error(
|
|
288
|
+
"timeoutMs is only for a route an agent asks an outside participant over: between them, or to it",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
228
291
|
const open = pairs.map(([from, to]) => this.#open(from, to));
|
|
229
292
|
if (open.some(Boolean)) {
|
|
230
293
|
if (
|
|
231
294
|
open.every(
|
|
232
295
|
(route) =>
|
|
233
|
-
route &&
|
|
296
|
+
route &&
|
|
297
|
+
sameGrant(
|
|
298
|
+
route,
|
|
299
|
+
ends,
|
|
300
|
+
purpose,
|
|
301
|
+
requested as number | undefined,
|
|
302
|
+
timeoutMs as number | undefined,
|
|
303
|
+
scope,
|
|
304
|
+
),
|
|
234
305
|
) &&
|
|
235
306
|
new Set(open).size === 1
|
|
236
307
|
)
|
|
@@ -248,6 +319,7 @@ export class Messaging {
|
|
|
248
319
|
maxMessages,
|
|
249
320
|
...(asked !== undefined && asked > maxMessages ? { asked } : {}),
|
|
250
321
|
...(asked === undefined ? {} : { requested: asked }),
|
|
322
|
+
...(timeoutMs === undefined ? {} : { timeoutMs: timeoutMs as number }),
|
|
251
323
|
scope,
|
|
252
324
|
count: 0,
|
|
253
325
|
open: true,
|
|
@@ -263,9 +335,242 @@ export class Messaging {
|
|
|
263
335
|
purpose,
|
|
264
336
|
maxMessages,
|
|
265
337
|
...(route.asked === undefined ? {} : { asked: route.asked }),
|
|
338
|
+
...(route.timeoutMs === undefined ? {} : { timeoutMs: route.timeoutMs }),
|
|
266
339
|
...(scope.stage === undefined ? {} : { stage: scope.stage }),
|
|
267
340
|
});
|
|
268
|
-
for (const agent of new Set(pairs.flat()))
|
|
341
|
+
for (const agent of new Set(pairs.flat())) {
|
|
342
|
+
if (!this.#outside.has(agent)) this.#host.changed(agent);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Outside participants
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Connects a participant outside the run under `key`, which the runner has checked no agent
|
|
350
|
+
* holds and is not reserved. A key connected before in the attempt, released or not, rejects.
|
|
351
|
+
*/
|
|
352
|
+
connect(key: AgentKey, labels?: JsonObject, to?: "code"): void {
|
|
353
|
+
if (this.#closed) throw new Error("participants connect only while the attempt runs");
|
|
354
|
+
if (this.#outside.has(key)) throw new Error(`${key} is already connected`);
|
|
355
|
+
this.#outside.set(key, { key, released: false });
|
|
356
|
+
void this.#record({
|
|
357
|
+
kind: "participant-connected",
|
|
358
|
+
participant: key,
|
|
359
|
+
...(labels === undefined ? {} : { labels }),
|
|
360
|
+
...(to === undefined ? {} : { to }),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Writes `text` to a participant routed to the code, without a turn: an inbox item for it. */
|
|
365
|
+
async post(key: AgentKey, text: string): Promise<string> {
|
|
366
|
+
if (this.#closed) throw new Error("the attempt has ended; nothing more is posted");
|
|
367
|
+
if (!this.#outside.has(key)) throw new Error(`${key} is not connected`);
|
|
368
|
+
if (this.#outside.get(key)?.released)
|
|
369
|
+
throw new Error(`${key} was released; nothing is posted to it`);
|
|
370
|
+
// Numbered within the attempt; `awf inbox` puts the attempt before it.
|
|
371
|
+
this.#posts += 1;
|
|
372
|
+
const id = `p${this.#posts}`;
|
|
373
|
+
await this.#host.record({ kind: "participant-post", participant: key, id, text });
|
|
374
|
+
return id;
|
|
375
|
+
}
|
|
376
|
+
#posts = 0;
|
|
377
|
+
|
|
378
|
+
/** Whether `key` is an outside participant's, connected in this attempt. */
|
|
379
|
+
isOutside(key: AgentKey): boolean {
|
|
380
|
+
return this.#outside.has(key);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Outside participants connected and not released, in the order connected. */
|
|
384
|
+
connected(): AgentKey[] {
|
|
385
|
+
return [...this.#outside.values()].filter((each) => !each.released).map((each) => each.key);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Releases an outside participant: its routes close and every question open to it ends
|
|
390
|
+
* `released`, its asker told. False when it was released already.
|
|
391
|
+
*/
|
|
392
|
+
release(key: AgentKey, reason?: string): boolean {
|
|
393
|
+
const outside = this.#outside.get(key);
|
|
394
|
+
if (!outside || outside.released) return false;
|
|
395
|
+
outside.released = true;
|
|
396
|
+
const why = reason ? ` (${JSON.stringify(reason)})` : "";
|
|
397
|
+
for (const owed of this.#obligations) {
|
|
398
|
+
if (owed.owedBy === key && owed.state === "open") {
|
|
399
|
+
this.#end(owed, "released", `it was released${why} before replying`, { tell: true });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const route of this.#openRoutes()) {
|
|
403
|
+
if (directions(route.ends).some((pair) => pair.includes(key)))
|
|
404
|
+
this.#closeRoute(route, "released");
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Questions open to outside participants now, oldest first. */
|
|
410
|
+
openQuestions(): OpenQuestion[] {
|
|
411
|
+
return this.#obligations
|
|
412
|
+
.filter((owed) => owed.state === "open" && this.#outside.has(owed.owedBy))
|
|
413
|
+
.map((owed) => ({
|
|
414
|
+
coordinator: owed.owedBy,
|
|
415
|
+
message: owed.request.n,
|
|
416
|
+
asker: owed.asker,
|
|
417
|
+
...(owed.closes ? { closesAt: owed.closes.at } : {}),
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* A holder answers a question, as `awf reply` does: the first answer wins. Refusals come in
|
|
423
|
+
* `PARTICIPANT_CODES`' order, a question's own state before its route's.
|
|
424
|
+
*/
|
|
425
|
+
async holderReply(request: HolderRequest): Promise<HolderResult> {
|
|
426
|
+
if (this.#closed) return holderRefusal("ended", "the run has ended.");
|
|
427
|
+
const named =
|
|
428
|
+
request.to === undefined || request.coordinator !== undefined
|
|
429
|
+
? undefined
|
|
430
|
+
: this.#obligations.find(
|
|
431
|
+
(owed) => owed.request.n === request.to && this.#outside.has(owed.owedBy),
|
|
432
|
+
)?.owedBy;
|
|
433
|
+
const as = this.#holderAs(request.coordinator ?? named);
|
|
434
|
+
if (as.kind === "refused") return as.result;
|
|
435
|
+
const { agent } = request;
|
|
436
|
+
if (!this.#host.agent(agent)) return unknownAgent(agent);
|
|
437
|
+
const asked = (coordinator: AgentKey) =>
|
|
438
|
+
this.#obligations.filter(
|
|
439
|
+
(owed) => owed.asker === agent && owed.owedBy === coordinator && owed.state === "open",
|
|
440
|
+
);
|
|
441
|
+
let owed: Obligation | undefined;
|
|
442
|
+
if (request.to === undefined) {
|
|
443
|
+
if (as.key === undefined) return as.ambiguous();
|
|
444
|
+
const open = asked(as.key);
|
|
445
|
+
if (open.length > 1)
|
|
446
|
+
return holderRefusal(
|
|
447
|
+
"ambiguous",
|
|
448
|
+
`${agent} has ${open.length} questions open to ${as.key}; name one with --to.`,
|
|
449
|
+
open.map((each) => String(each.request.n)),
|
|
450
|
+
);
|
|
451
|
+
owed = open[0];
|
|
452
|
+
if (!owed) return holderRefusal("expired", `${agent} has no question open to ${as.key}.`);
|
|
453
|
+
} else {
|
|
454
|
+
owed = this.#obligations.find(
|
|
455
|
+
(each) =>
|
|
456
|
+
each.request.n === request.to &&
|
|
457
|
+
each.asker === agent &&
|
|
458
|
+
each.state === "open" &&
|
|
459
|
+
(as.key === undefined ? this.#outside.has(each.owedBy) : each.owedBy === as.key),
|
|
460
|
+
);
|
|
461
|
+
if (!owed)
|
|
462
|
+
return holderRefusal(
|
|
463
|
+
"expired",
|
|
464
|
+
`${request.to} is not a question open from ${agent}${as.key === undefined ? "" : ` to ${as.key}`}.`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
const route = owed.request.route;
|
|
468
|
+
if (!route.open) return holderRefusal("closed", `the route with ${agent} has closed.`);
|
|
469
|
+
const bytes = Buffer.byteLength(request.text);
|
|
470
|
+
if (bytes > this.limits.bytes) return tooLarge(bytes, this.limits.bytes);
|
|
471
|
+
owed.state = "replied";
|
|
472
|
+
owed.endedAt = this.#host.now();
|
|
473
|
+
owed.cancelTimer?.();
|
|
474
|
+
if (owed.askerOperation.awaits === owed) owed.askerOperation.awaits = undefined;
|
|
475
|
+
const message = this.#admit(owed.owedBy, agent, request.text, bytes, route, owed);
|
|
476
|
+
const sent = this.#sent(message, request.via);
|
|
477
|
+
void this.#record({
|
|
478
|
+
kind: "request-ended",
|
|
479
|
+
message: owed.request.n,
|
|
480
|
+
owedBy: owed.owedBy,
|
|
481
|
+
asker: owed.asker,
|
|
482
|
+
end: "replied",
|
|
483
|
+
reply: message.n,
|
|
484
|
+
});
|
|
485
|
+
try {
|
|
486
|
+
await sent;
|
|
487
|
+
} finally {
|
|
488
|
+
this.#host.changed(agent);
|
|
489
|
+
}
|
|
490
|
+
return { code: "delivered", message: owed.request.n };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** A holder messages an agent first, as `awf send` does, over a route the workflow allowed. */
|
|
494
|
+
async holderSend(request: HolderRequest): Promise<HolderResult> {
|
|
495
|
+
if (this.#closed || this.attempt.state !== "open")
|
|
496
|
+
return holderRefusal("ended", "the workflow has returned; it takes no new messages.");
|
|
497
|
+
const as = this.#holderAs(request.coordinator);
|
|
498
|
+
if (as.kind === "refused") return as.result;
|
|
499
|
+
const { agent } = request;
|
|
500
|
+
const view = this.#host.agent(agent);
|
|
501
|
+
if (!view) return unknownAgent(agent);
|
|
502
|
+
if (as.key === undefined) return as.ambiguous();
|
|
503
|
+
const from = as.key;
|
|
504
|
+
const initiating = this.#routes.get(pairKey(from, agent)) ?? [];
|
|
505
|
+
if (initiating.length === 0)
|
|
506
|
+
return holderRefusal("no-route", `the workflow allows no route from ${from} to ${agent}.`);
|
|
507
|
+
if (view.state === "stopped")
|
|
508
|
+
return holderRefusal(
|
|
509
|
+
"stopped",
|
|
510
|
+
`${agent} has stopped${view.reason ? ` (${JSON.stringify(view.reason)})` : ""}.`,
|
|
511
|
+
);
|
|
512
|
+
const route = initiating.find((each) => each.open);
|
|
513
|
+
if (route?.scope.state !== "open")
|
|
514
|
+
return holderRefusal(
|
|
515
|
+
"closed",
|
|
516
|
+
`the route to ${agent} has closed${route?.scope.stage ? `: its stage ${route.scope.stage} is finishing` : ""}.`,
|
|
517
|
+
);
|
|
518
|
+
const bytes = Buffer.byteLength(request.text);
|
|
519
|
+
if (bytes > this.limits.bytes) return tooLarge(bytes, this.limits.bytes);
|
|
520
|
+
if (route.count + 1 > route.maxMessages)
|
|
521
|
+
return holderRefusal(
|
|
522
|
+
"limit",
|
|
523
|
+
`${route.maxMessages} messages with ${agent}, this route's limit for the attempt.`,
|
|
524
|
+
);
|
|
525
|
+
const unread = this.#unread(agent);
|
|
526
|
+
if (unread >= this.limits.unread)
|
|
527
|
+
return holderRefusal(
|
|
528
|
+
"inbox-full",
|
|
529
|
+
`${agent} has ${unread} unread messages; try again later.`,
|
|
530
|
+
);
|
|
531
|
+
const message = this.#admit(from, agent, request.text, bytes, route);
|
|
532
|
+
try {
|
|
533
|
+
await this.#sent(message, request.via);
|
|
534
|
+
} finally {
|
|
535
|
+
this.#host.changed(agent);
|
|
536
|
+
}
|
|
537
|
+
return { code: "delivered", message: message.n };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* The outside participant a holder speaks as: `--as`, or the only one connected. Undefined when
|
|
542
|
+
* several are: `ambiguous` once the agent is known, unless a `--to` names the question.
|
|
543
|
+
*/
|
|
544
|
+
#holderAs(
|
|
545
|
+
key: AgentKey | undefined,
|
|
546
|
+
):
|
|
547
|
+
| { kind: "refused"; result: HolderResult }
|
|
548
|
+
| { kind: "ok"; key: AgentKey | undefined; ambiguous: () => HolderResult } {
|
|
549
|
+
const connected = this.connected();
|
|
550
|
+
const ambiguous = () =>
|
|
551
|
+
holderRefusal(
|
|
552
|
+
"ambiguous",
|
|
553
|
+
`the workflow connects ${connected.length} coordinators; say which with --as.`,
|
|
554
|
+
connected,
|
|
555
|
+
);
|
|
556
|
+
if (key !== undefined && !connected.includes(key))
|
|
557
|
+
return {
|
|
558
|
+
kind: "refused",
|
|
559
|
+
result: holderRefusal(
|
|
560
|
+
"no-participant",
|
|
561
|
+
`no coordinator ${key} is connected${this.#outside.has(key) ? ": it was released" : ""}.`,
|
|
562
|
+
),
|
|
563
|
+
};
|
|
564
|
+
if (connected.length === 0)
|
|
565
|
+
return {
|
|
566
|
+
kind: "refused",
|
|
567
|
+
result: holderRefusal("no-participant", "the run has no coordinator connected."),
|
|
568
|
+
};
|
|
569
|
+
return {
|
|
570
|
+
kind: "ok",
|
|
571
|
+
key: key ?? (connected.length === 1 ? connected[0] : undefined),
|
|
572
|
+
ambiguous,
|
|
573
|
+
};
|
|
269
574
|
}
|
|
270
575
|
|
|
271
576
|
/**
|
|
@@ -330,13 +635,15 @@ export class Messaging {
|
|
|
330
635
|
this.#current.set(info.agent, operation);
|
|
331
636
|
}
|
|
332
637
|
|
|
333
|
-
/**
|
|
638
|
+
/** An operation's deadline moved later, and with it any wait on an agent it has no `--timeout` for. */
|
|
334
639
|
extended(id: string, deadline: number): void {
|
|
335
640
|
const operation = this.#operations.get(id);
|
|
336
|
-
if (!operation || deadline <= operation.deadline) return;
|
|
641
|
+
if (!operation || deadline <= (operation.deadline ?? Infinity)) return;
|
|
337
642
|
operation.deadline = deadline;
|
|
338
643
|
const waited = operation.awaits;
|
|
339
|
-
|
|
644
|
+
// A wait on an outside participant is not bounded by the deadline at all.
|
|
645
|
+
if (waited?.state === "open" && !waited.timed && !this.#outside.has(waited.owedBy))
|
|
646
|
+
waited.until = deadline;
|
|
340
647
|
}
|
|
341
648
|
|
|
342
649
|
/**
|
|
@@ -351,10 +658,15 @@ export class Messaging {
|
|
|
351
658
|
const waited = operation.awaits;
|
|
352
659
|
operation.awaits = undefined;
|
|
353
660
|
if (waited?.state === "open") {
|
|
661
|
+
// On an outside participant, the asker timing out or being cancelled is `cancelled`; an
|
|
662
|
+
// attempt's end closes it `attempt-ended` before its operations stop.
|
|
663
|
+
const cut = this.#outside.has(waited.owedBy) && (how === "timed-out" || how === "cancelled");
|
|
354
664
|
this.#end(
|
|
355
665
|
waited,
|
|
356
|
-
waited.request.shown ? "released" : "withdrawn",
|
|
357
|
-
|
|
666
|
+
cut ? "cancelled" : waited.request.shown ? "released" : "withdrawn",
|
|
667
|
+
cut
|
|
668
|
+
? `${operation.agent}'s operation ${how === "timed-out" ? "timed out" : "was cancelled"}`
|
|
669
|
+
: `${operation.agent} finished first`,
|
|
358
670
|
);
|
|
359
671
|
}
|
|
360
672
|
// An answer accepted but not yet shown is withdrawn with it.
|
|
@@ -370,10 +682,18 @@ export class Messaging {
|
|
|
370
682
|
}
|
|
371
683
|
}
|
|
372
684
|
|
|
373
|
-
/**
|
|
374
|
-
|
|
685
|
+
/**
|
|
686
|
+
* The answer this operation awaits, while it does; `outside` when an outside participant owes it,
|
|
687
|
+
* whose wait its deadline does not count.
|
|
688
|
+
*/
|
|
689
|
+
awaiting(id: string): { peer: AgentKey; until?: number; outside?: true } | undefined {
|
|
375
690
|
const waited = this.#operations.get(id)?.awaits;
|
|
376
|
-
|
|
691
|
+
if (waited?.state !== "open") return undefined;
|
|
692
|
+
return {
|
|
693
|
+
peer: waited.owedBy,
|
|
694
|
+
...(waited.until === undefined ? {} : { until: waited.until }),
|
|
695
|
+
...(this.#outside.has(waited.owedBy) ? { outside: true as const } : {}),
|
|
696
|
+
};
|
|
377
697
|
}
|
|
378
698
|
|
|
379
699
|
/** The operation open on this agent, if any. */
|
|
@@ -391,7 +711,7 @@ export class Messaging {
|
|
|
391
711
|
command: "result" | "waiting",
|
|
392
712
|
):
|
|
393
713
|
| { kind: "refused"; code: "owes-reply" | "no-result"; error: string }
|
|
394
|
-
| { kind: "awaiting"; peer: AgentKey; until
|
|
714
|
+
| { kind: "awaiting"; peer: AgentKey; until?: number; notice: string }
|
|
395
715
|
| null {
|
|
396
716
|
const operation = this.#operations.get(id);
|
|
397
717
|
if (!operation || operation.agent !== agent) return null;
|
|
@@ -422,8 +742,8 @@ export class Messaging {
|
|
|
422
742
|
return {
|
|
423
743
|
kind: "awaiting",
|
|
424
744
|
peer: waited.owedBy,
|
|
425
|
-
until: waited.until,
|
|
426
|
-
notice: `you already await ${waited.owedBy}'s reply
|
|
745
|
+
...(waited.until === undefined ? {} : { until: waited.until }),
|
|
746
|
+
notice: `you already await ${waited.owedBy}'s reply${waited.until === undefined ? "" : `, until ${clock(waited.until, this.#host.now())}`}. End your turn, or finish with ${wf} result ${id}.`,
|
|
427
747
|
};
|
|
428
748
|
}
|
|
429
749
|
return null;
|
|
@@ -436,7 +756,7 @@ export class Messaging {
|
|
|
436
756
|
if (checked.kind === "refused") return this.#refused(from, request, "send", checked);
|
|
437
757
|
const { operation, route, bytes } = checked;
|
|
438
758
|
const message = this.#admit(from, request.peer, request.text, bytes, route);
|
|
439
|
-
const
|
|
759
|
+
const asked = request.awaitReply ? this.#ask(message, operation, request.timeoutMs) : undefined;
|
|
440
760
|
// Admitted whatever the record says: its recipient is woken even if the write fails.
|
|
441
761
|
try {
|
|
442
762
|
await this.#sent(message, operation);
|
|
@@ -446,8 +766,8 @@ export class Messaging {
|
|
|
446
766
|
return {
|
|
447
767
|
kind: "sent",
|
|
448
768
|
message: message.n,
|
|
449
|
-
...(until === undefined ? {} : { until }),
|
|
450
|
-
notice: this.#sentNotice(request.peer, operation,
|
|
769
|
+
...(asked?.until === undefined ? {} : { until: asked.until }),
|
|
770
|
+
notice: this.#sentNotice(request.peer, operation, asked),
|
|
451
771
|
};
|
|
452
772
|
}
|
|
453
773
|
|
|
@@ -462,7 +782,9 @@ export class Messaging {
|
|
|
462
782
|
owed.owingOperation?.owes.delete(owed);
|
|
463
783
|
if (owed.askerOperation.awaits === owed) owed.askerOperation.awaits = undefined;
|
|
464
784
|
const message = this.#admit(from, owed.asker, request.text, bytes, owed.request.route, owed);
|
|
465
|
-
const
|
|
785
|
+
const asking = request.awaitReply
|
|
786
|
+
? this.#ask(message, operation, request.timeoutMs)
|
|
787
|
+
: undefined;
|
|
466
788
|
// Its line first, then the question's end, in the log's order; the end is written either way.
|
|
467
789
|
const sent = this.#sent(message, operation);
|
|
468
790
|
void this.#record({
|
|
@@ -478,11 +800,11 @@ export class Messaging {
|
|
|
478
800
|
} finally {
|
|
479
801
|
this.#host.changed(owed.asker);
|
|
480
802
|
}
|
|
481
|
-
const asked =
|
|
803
|
+
const asked = asking ? ` ${this.#awaitNotice(operation, asking)}` : "";
|
|
482
804
|
return {
|
|
483
805
|
kind: "sent",
|
|
484
806
|
message: message.n,
|
|
485
|
-
...(until === undefined ? {} : { until }),
|
|
807
|
+
...(asking?.until === undefined ? {} : { until: asking.until }),
|
|
486
808
|
notice: `replied to ${request.peer}.${asked}`,
|
|
487
809
|
};
|
|
488
810
|
}
|
|
@@ -513,7 +835,7 @@ export class Messaging {
|
|
|
513
835
|
? replyOnly()
|
|
514
836
|
: refuse("no-route", `no route to ${peer}. Your routes: ${wf} peers`);
|
|
515
837
|
}
|
|
516
|
-
const agent = this.#host.agent(peer);
|
|
838
|
+
const agent = this.#outside.has(peer) ? undefined : this.#host.agent(peer);
|
|
517
839
|
if (agent?.state === "stopped")
|
|
518
840
|
return refuse(
|
|
519
841
|
"peer-stopped",
|
|
@@ -712,65 +1034,93 @@ export class Messaging {
|
|
|
712
1034
|
};
|
|
713
1035
|
// An answer's room was reserved when it was asked.
|
|
714
1036
|
if (!answers) route.count += 1;
|
|
715
|
-
|
|
1037
|
+
// An outside participant reads the run log: a message to it is shown once recorded.
|
|
1038
|
+
if (this.#outside.has(to)) message.shown = true;
|
|
1039
|
+
else this.#inbox(to).push(message);
|
|
716
1040
|
this.#sentBy.set(message.n, message);
|
|
717
1041
|
return message;
|
|
718
1042
|
}
|
|
719
1043
|
|
|
720
1044
|
/** Makes `message` a question its recipient owes an answer to, and its sender's wait. */
|
|
721
|
-
#ask(message: Message, operation: Operation, timeoutMs?: number): number {
|
|
1045
|
+
#ask(message: Message, operation: Operation, timeoutMs?: number): { until?: number } {
|
|
722
1046
|
message.route.count += 1;
|
|
723
1047
|
const now = this.#host.now();
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
1048
|
+
const routeMs = this.#outside.has(message.to) ? message.route.timeoutMs : undefined;
|
|
1049
|
+
// The route's limit, or the asker's own when shorter: the shorter wins.
|
|
1050
|
+
const limit =
|
|
1051
|
+
routeMs !== undefined && (timeoutMs === undefined || routeMs <= timeoutMs)
|
|
1052
|
+
? { by: "route" as const, ms: routeMs }
|
|
1053
|
+
: timeoutMs === undefined
|
|
1054
|
+
? undefined
|
|
1055
|
+
: { by: "own" as const, ms: timeoutMs };
|
|
1056
|
+
const outside = this.#outside.has(message.to);
|
|
1057
|
+
const limitAt = limit ? now + limit.ms : Infinity;
|
|
1058
|
+
// Waiting on an outside participant is not work: its deadline does not cut the wait, though
|
|
1059
|
+
// the deadlines around it do.
|
|
1060
|
+
const waitEnd = Math.min(
|
|
1061
|
+
outside ? (operation.ceiling ?? Infinity) : (operation.deadline ?? Infinity),
|
|
1062
|
+
limitAt,
|
|
727
1063
|
);
|
|
1064
|
+
const until = Number.isFinite(waitEnd) ? waitEnd : undefined;
|
|
728
1065
|
const obligation: Obligation = {
|
|
729
1066
|
request: message,
|
|
730
1067
|
asker: message.from,
|
|
731
1068
|
askerOperation: operation,
|
|
732
1069
|
owedBy: message.to,
|
|
733
|
-
until,
|
|
734
|
-
timed:
|
|
1070
|
+
...(until === undefined ? {} : { until }),
|
|
1071
|
+
timed: limitAt < (outside ? Infinity : (operation.deadline ?? Infinity)),
|
|
1072
|
+
...(limit && outside ? { closes: { ...limit, at: limitAt } } : {}),
|
|
735
1073
|
state: "open",
|
|
736
1074
|
};
|
|
737
1075
|
message.asks = obligation;
|
|
738
1076
|
operation.awaits = obligation;
|
|
739
1077
|
this.#obligations.push(obligation);
|
|
740
1078
|
// Only `--timeout` fails a question: reaching the operation's own deadline times it out.
|
|
741
|
-
|
|
742
|
-
|
|
1079
|
+
// Only its limit closes it; a deadline ends the operation, and the wait with it.
|
|
1080
|
+
const closesAt = obligation.timed ? limitAt : undefined;
|
|
1081
|
+
if (closesAt !== undefined) {
|
|
1082
|
+
obligation.cancelTimer = this.#host.schedule(closesAt, () => {
|
|
743
1083
|
if (obligation.state === "open") {
|
|
744
1084
|
this.#end(
|
|
745
1085
|
obligation,
|
|
746
1086
|
"timed-out",
|
|
747
|
-
|
|
1087
|
+
obligation.closes?.by === "route"
|
|
1088
|
+
? `the question closed unanswered after ${duration(obligation.closes.ms)}`
|
|
1089
|
+
: `your --timeout passed at ${clock(closesAt, this.#host.now())}`,
|
|
748
1090
|
);
|
|
749
1091
|
}
|
|
750
1092
|
});
|
|
751
1093
|
}
|
|
752
|
-
return until;
|
|
1094
|
+
return until === undefined ? {} : { until };
|
|
753
1095
|
}
|
|
754
1096
|
|
|
755
|
-
/**
|
|
756
|
-
|
|
1097
|
+
/**
|
|
1098
|
+
* Durable before `wf send` returns: a failed write rejects, and `wf` says it may have been
|
|
1099
|
+
* queued. An outside participant's message says what it came through instead of an operation.
|
|
1100
|
+
* A question to one carries when it closes unanswered, if it has a limit.
|
|
1101
|
+
*/
|
|
1102
|
+
async #sent(message: Message, by: Operation | OutsideVia): Promise<void> {
|
|
1103
|
+
const asks = message.asks;
|
|
1104
|
+
// To an outside participant, when it closes unanswered: its limit, not the asker's wait.
|
|
1105
|
+
const until = asks && this.#outside.has(asks.owedBy) ? asks.closes?.at : asks?.until;
|
|
757
1106
|
await this.#host.record({
|
|
758
1107
|
kind: "message-sent",
|
|
759
1108
|
message: message.n,
|
|
760
1109
|
from: message.from,
|
|
761
1110
|
to: message.to,
|
|
762
|
-
operationId:
|
|
1111
|
+
...(typeof by === "string" ? { via: by } : { operationId: by.id }),
|
|
763
1112
|
text: message.text,
|
|
764
1113
|
bytes: message.bytes,
|
|
765
|
-
...(
|
|
766
|
-
? { awaitsReply: { until: new Date(
|
|
1114
|
+
...(asks
|
|
1115
|
+
? { awaitsReply: until === undefined ? {} : { until: new Date(until).toISOString() } }
|
|
767
1116
|
: {}),
|
|
768
1117
|
...(message.answers ? { replyTo: message.answers.request.n } : {}),
|
|
769
1118
|
});
|
|
770
1119
|
}
|
|
771
1120
|
|
|
772
|
-
#sentNotice(peer: AgentKey, operation: Operation, until?: number): string {
|
|
773
|
-
if (
|
|
1121
|
+
#sentNotice(peer: AgentKey, operation: Operation, asked?: { until?: number }): string {
|
|
1122
|
+
if (asked) return `sent to ${peer}. ${this.#awaitNotice(operation, asked)}`;
|
|
1123
|
+
if (this.#outside.has(peer)) return `sent to ${peer}, outside the run; it reads it from there.`;
|
|
774
1124
|
const state = this.#host.agent(peer)?.state;
|
|
775
1125
|
const when =
|
|
776
1126
|
this.#current.get(peer)?.awaits?.state === "open"
|
|
@@ -783,10 +1133,10 @@ export class Messaging {
|
|
|
783
1133
|
return `sent to ${peer}; ${when}.`;
|
|
784
1134
|
}
|
|
785
1135
|
|
|
786
|
-
#awaitNotice(operation: Operation, until: number): string {
|
|
1136
|
+
#awaitNotice(operation: Operation, { until }: { until?: number }): string {
|
|
787
1137
|
const wf = this.#host.launcher(operation.agent);
|
|
788
1138
|
return [
|
|
789
|
-
`Its reply starts your next turn (deadline ${clock(until, this.#host.now())}).`,
|
|
1139
|
+
`Its reply starts your next turn (${until === undefined ? "no deadline" : `deadline ${clock(until, this.#host.now())}`}).`,
|
|
790
1140
|
"Go on with work that does not need the answer, then end your turn with one line saying what you await; do not poll or sleep.",
|
|
791
1141
|
...(operation.kind === "workflow"
|
|
792
1142
|
? [
|
|
@@ -896,14 +1246,13 @@ export class Messaging {
|
|
|
896
1246
|
*/
|
|
897
1247
|
#deadlineFor(operation: Operation): number | undefined {
|
|
898
1248
|
if (operation.kind !== "message") return undefined;
|
|
1249
|
+
const own = operation.deadline ?? Infinity;
|
|
1250
|
+
// An asker with no deadline adds none.
|
|
899
1251
|
const wanted = Math.min(
|
|
900
|
-
operation.ceiling,
|
|
901
|
-
Math.max(
|
|
902
|
-
operation.deadline,
|
|
903
|
-
...[...operation.owes].map((owed) => owed.askerOperation.deadline),
|
|
904
|
-
),
|
|
1252
|
+
operation.ceiling ?? Infinity,
|
|
1253
|
+
Math.max(own, ...[...operation.owes].map((owed) => owed.askerOperation.deadline ?? own)),
|
|
905
1254
|
);
|
|
906
|
-
return wanted >
|
|
1255
|
+
return wanted > own && Number.isFinite(wanted) ? wanted : undefined;
|
|
907
1256
|
}
|
|
908
1257
|
|
|
909
1258
|
/** What the harness confirmed of a delivery: the model read it, or nothing says it did. */
|
|
@@ -935,7 +1284,12 @@ export class Messaging {
|
|
|
935
1284
|
return { kind: operation?.kind === "message" ? "settle" : "check-in" };
|
|
936
1285
|
if (this.#pending(operation) === "continues") return { kind: "continue" };
|
|
937
1286
|
const waited = operation.awaits;
|
|
938
|
-
if (waited?.state === "open")
|
|
1287
|
+
if (waited?.state === "open")
|
|
1288
|
+
return {
|
|
1289
|
+
kind: "wait",
|
|
1290
|
+
peer: waited.owedBy,
|
|
1291
|
+
...(waited.until === undefined ? {} : { until: waited.until }),
|
|
1292
|
+
};
|
|
939
1293
|
// Each question gets one reminder; a turn that ends still owing one it was reminded of fails it.
|
|
940
1294
|
for (const owed of [...operation.owes]) {
|
|
941
1295
|
if (owed.reminded) this.#end(owed, "failed", "it did not reply after a reminder");
|
|
@@ -1012,11 +1366,19 @@ export class Messaging {
|
|
|
1012
1366
|
peers(agent: AgentKey): {
|
|
1013
1367
|
peers: PeerView[];
|
|
1014
1368
|
owes: AgentKey[];
|
|
1015
|
-
awaits?: { peer: AgentKey; until
|
|
1369
|
+
awaits?: { peer: AgentKey; until?: number };
|
|
1016
1370
|
text: string;
|
|
1017
1371
|
} {
|
|
1018
1372
|
const peers = this.#routesOf(agent, true).map((route): PeerView => {
|
|
1019
1373
|
const peer = peerOf(route, agent);
|
|
1374
|
+
if (this.#outside.has(peer))
|
|
1375
|
+
return {
|
|
1376
|
+
peer,
|
|
1377
|
+
can: this.#can(route, agent),
|
|
1378
|
+
state: "outside",
|
|
1379
|
+
queued: 0,
|
|
1380
|
+
purpose: route.purpose,
|
|
1381
|
+
};
|
|
1020
1382
|
const view = this.#host.agent(peer);
|
|
1021
1383
|
const waited = this.#current.get(peer)?.awaits;
|
|
1022
1384
|
const awaiting = waited?.state === "open" ? waited.owedBy : undefined;
|
|
@@ -1081,7 +1443,7 @@ export class Messaging {
|
|
|
1081
1443
|
this.#closed = true;
|
|
1082
1444
|
for (const owed of this.#obligations) {
|
|
1083
1445
|
if (owed.state === "open")
|
|
1084
|
-
this.#end(owed, "
|
|
1446
|
+
this.#end(owed, "attempt-ended", "the attempt ended", {
|
|
1085
1447
|
undelivered: "attempt-ended",
|
|
1086
1448
|
tell: false,
|
|
1087
1449
|
});
|
|
@@ -1146,13 +1508,20 @@ export class Messaging {
|
|
|
1146
1508
|
return [...operation.owes].filter((each) => each.state === "open").map((each) => each.asker);
|
|
1147
1509
|
}
|
|
1148
1510
|
|
|
1149
|
-
#standing(agent: AgentKey): { owes: AgentKey[]; awaits?: { peer: AgentKey; until
|
|
1511
|
+
#standing(agent: AgentKey): { owes: AgentKey[]; awaits?: { peer: AgentKey; until?: number } } {
|
|
1150
1512
|
const operation = this.#current.get(agent);
|
|
1151
1513
|
if (!operation) return { owes: [] };
|
|
1152
1514
|
const waited = operation.awaits?.state === "open" ? operation.awaits : undefined;
|
|
1153
1515
|
return {
|
|
1154
1516
|
owes: this.#owes(operation),
|
|
1155
|
-
...(waited
|
|
1517
|
+
...(waited
|
|
1518
|
+
? {
|
|
1519
|
+
awaits: {
|
|
1520
|
+
peer: waited.owedBy,
|
|
1521
|
+
...(waited.until === undefined ? {} : { until: waited.until }),
|
|
1522
|
+
},
|
|
1523
|
+
}
|
|
1524
|
+
: {}),
|
|
1156
1525
|
};
|
|
1157
1526
|
}
|
|
1158
1527
|
|
|
@@ -1185,7 +1554,8 @@ export class Messaging {
|
|
|
1185
1554
|
owed: Obligation,
|
|
1186
1555
|
end: Exclude<RequestEnd, "replied">,
|
|
1187
1556
|
reason: string,
|
|
1188
|
-
|
|
1557
|
+
/** `tell`: whether its asker is told, beside a failure or a timeout, which always are. */
|
|
1558
|
+
options: { gone?: true; undelivered?: UndeliveredReason; tell?: boolean } = {},
|
|
1189
1559
|
): void {
|
|
1190
1560
|
if (owed.state !== "open") return;
|
|
1191
1561
|
owed.state = end;
|
|
@@ -1219,7 +1589,8 @@ export class Messaging {
|
|
|
1219
1589
|
});
|
|
1220
1590
|
const operation = owed.askerOperation;
|
|
1221
1591
|
if (operation.awaits === owed) operation.awaits = undefined;
|
|
1222
|
-
|
|
1592
|
+
const told = options.tell ?? (end === "failed" || end === "timed-out");
|
|
1593
|
+
if (told && !operation.ended) {
|
|
1223
1594
|
this.#inbox(owed.asker).push({ kind: "no-reply", for: operation, from: owed.owedBy, reason });
|
|
1224
1595
|
this.#host.changed(owed.asker);
|
|
1225
1596
|
}
|
|
@@ -1265,7 +1636,10 @@ export class Messaging {
|
|
|
1265
1636
|
}
|
|
1266
1637
|
}
|
|
1267
1638
|
|
|
1268
|
-
#closeRoute(
|
|
1639
|
+
#closeRoute(
|
|
1640
|
+
route: Route,
|
|
1641
|
+
reason: "stage-ended" | "agent-stopped" | "released" | "attempt-ended",
|
|
1642
|
+
): void {
|
|
1269
1643
|
if (!route.open) return;
|
|
1270
1644
|
route.open = false;
|
|
1271
1645
|
route.closed = reason;
|
|
@@ -1301,3 +1675,22 @@ export class Messaging {
|
|
|
1301
1675
|
function refuse<C extends SendCode | ReplyCode>(code: C, error: string) {
|
|
1302
1676
|
return { kind: "refused" as const, code, error };
|
|
1303
1677
|
}
|
|
1678
|
+
|
|
1679
|
+
function holderRefusal(
|
|
1680
|
+
code: Exclude<ParticipantCode, "not-found">,
|
|
1681
|
+
error: string,
|
|
1682
|
+
choices?: string[],
|
|
1683
|
+
): HolderResult {
|
|
1684
|
+
return { code, error, ...(choices ? { choices } : {}) };
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function unknownAgent(agent: AgentKey): HolderResult {
|
|
1688
|
+
return holderRefusal("unknown-agent", `the run has no agent ${agent}.`);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function tooLarge(bytes: number, limit: number): HolderResult {
|
|
1692
|
+
return holderRefusal(
|
|
1693
|
+
"too-large",
|
|
1694
|
+
`${Math.ceil(bytes / 1024)} KB; the limit is ${limit / 1024} KB. Put it in a file and send its path.`,
|
|
1695
|
+
);
|
|
1696
|
+
}
|