@agentswf/engine 0.1.0-beta.2 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -6
- package/src/agent-cli/cli.ts +571 -0
- package/src/agent-cli/client.ts +115 -0
- package/src/agent-launcher.ts +15 -25
- package/src/allowance-command.ts +31 -21
- package/src/attempt-ending.ts +23 -5
- package/src/attempt-output.ts +48 -18
- package/src/attempt-view.ts +25 -15
- package/src/commands.ts +176 -0
- package/src/control-plane.ts +73 -22
- package/src/coordinator-port.ts +29 -0
- package/src/coordinator-prompt.ts +237 -0
- package/src/coordinator-socket.ts +236 -0
- package/src/deadlines.ts +47 -10
- package/src/decisions/directory.ts +8 -6
- package/src/duration.ts +29 -0
- package/src/emit-command.ts +119 -0
- package/src/emit.ts +246 -0
- package/src/files.ts +11 -1
- package/src/help/allowance.txt +13 -0
- package/src/help/emit.txt +13 -0
- package/src/help/inbox.txt +12 -0
- package/src/help/init.txt +12 -0
- package/src/help/logs.txt +13 -0
- package/src/help/reply.txt +18 -0
- package/src/help/run.txt +44 -0
- package/src/help/runs.txt +11 -0
- package/src/help/test.txt +9 -0
- package/src/help/tick.txt +12 -0
- package/src/here.ts +9 -3
- package/src/hold-run-lock.fixture.ts +8 -0
- package/src/holder-command.ts +279 -0
- package/src/inbox-command.ts +715 -0
- package/src/init-command.ts +29 -11
- package/src/init-template/AGENTS.md.template +4 -4
- package/src/init-template/hello/workflow.test.ts.template +2 -2
- package/src/init-template/hello/workflow.ts.template +16 -20
- package/src/keep-alive.ts +40 -0
- package/src/logs-command.ts +106 -35
- package/src/machine.ts +12 -1
- package/src/mailbox.ts +990 -0
- package/src/message-operation.ts +26 -3
- package/src/messaging/limits.ts +1 -1
- package/src/messaging/messaging.ts +478 -85
- package/src/messaging/routes.ts +11 -4
- package/src/messaging/text.ts +25 -22
- package/src/operation-liveness.ts +55 -9
- package/src/operator-cli.ts +290 -119
- package/src/operator-runtime.ts +8 -8
- package/src/progress-view.ts +49 -7
- package/src/projects.ts +57 -0
- package/src/result-slots.ts +43 -19
- package/src/result-validation.ts +18 -27
- package/src/run-command.ts +29 -66
- package/src/run-commands.ts +13 -2
- package/src/run-lock.ts +123 -0
- package/src/run-prepare.ts +94 -22
- package/src/run-state.ts +125 -6
- package/src/run-usage.ts +2 -2
- package/src/run-view.ts +0 -9
- package/src/runs-command.ts +43 -25
- package/src/runs.ts +142 -23
- package/src/sandboxes.ts +11 -4
- package/src/simple-input.ts +268 -0
- package/src/socket-exchange.ts +26 -0
- package/src/sources.ts +70 -0
- package/src/stage-ledger.ts +89 -14
- package/src/stage-plan.ts +23 -0
- package/src/stop-crashed.ts +39 -0
- package/src/test-command.ts +34 -11
- package/src/testing.ts +72 -4
- package/src/tick-command.ts +76 -0
- package/src/tools.ts +645 -0
- package/src/wake.ts +264 -0
- package/src/workflow-loader.ts +32 -22
- package/src/workflow-runner.ts +1277 -97
- package/src/workflow-testing/coordinator.typecheck.ts +54 -0
- package/src/workflow-testing/coordinators.ts +137 -0
- package/src/workflow-testing/host.ts +67 -7
- package/src/workflow-testing/index.ts +488 -34
- package/src/workflow-testing/script.ts +74 -4
- package/src/workflow-testing/workflow-testing.typecheck.ts +19 -2
|
@@ -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,
|
|
@@ -45,7 +47,7 @@ export interface MessagingHost {
|
|
|
45
47
|
agent(key: AgentKey): AgentView | undefined;
|
|
46
48
|
/** Whether it may `--await-reply` where it runs. */
|
|
47
49
|
canAwait(key: AgentKey): boolean;
|
|
48
|
-
/** The launcher it runs `
|
|
50
|
+
/** The launcher it runs `wfa` through. */
|
|
49
51
|
launcher(key: AgentKey): string;
|
|
50
52
|
/** Something is new for this agent: messages for it, or an end to its wait. */
|
|
51
53
|
changed(key: AgentKey): void;
|
|
@@ -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>;
|
|
@@ -166,7 +207,7 @@ type Operation = OperationInfo & {
|
|
|
166
207
|
/**
|
|
167
208
|
* The messaging logic of one attempt: routes, admission, inboxes and owed answers. It decides every
|
|
168
209
|
* case the design lists and writes the record; it starts nothing. The runner opens and continues
|
|
169
|
-
* operations when it says, and the control plane hands it `
|
|
210
|
+
* operations when it says, and the control plane hands it `wfa`'s commands.
|
|
170
211
|
*/
|
|
171
212
|
export class Messaging {
|
|
172
213
|
readonly limits: MessageLimits;
|
|
@@ -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. */
|
|
@@ -382,8 +702,8 @@ export class Messaging {
|
|
|
382
702
|
}
|
|
383
703
|
|
|
384
704
|
/**
|
|
385
|
-
* A refusal of `
|
|
386
|
-
* `
|
|
705
|
+
* A refusal of `wfa result` or `wfa waiting` given this call id, or the acknowledgement of a
|
|
706
|
+
* `wfa waiting` while an answer is awaited; null when the command goes on as before.
|
|
387
707
|
*/
|
|
388
708
|
checkFinish(
|
|
389
709
|
agent: AgentKey,
|
|
@@ -391,11 +711,11 @@ 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;
|
|
398
|
-
const
|
|
718
|
+
const wfa = this.#host.launcher(agent);
|
|
399
719
|
// A message operation's call takes no result, ended or not.
|
|
400
720
|
if (operation.kind === "message") {
|
|
401
721
|
return {
|
|
@@ -413,8 +733,8 @@ export class Messaging {
|
|
|
413
733
|
code: "owes-reply",
|
|
414
734
|
error:
|
|
415
735
|
command === "result"
|
|
416
|
-
? `you owe ${peer} a reply. Send it with ${
|
|
417
|
-
: `you owe ${peer} a reply; send it with ${
|
|
736
|
+
? `you owe ${peer} a reply. Send it with ${wfa} reply ${peer}, then run ${wfa} result ${id} again.`
|
|
737
|
+
: `you owe ${peer} a reply; send it with ${wfa} reply ${peer} first.`,
|
|
418
738
|
};
|
|
419
739
|
}
|
|
420
740
|
const waited = operation.awaits;
|
|
@@ -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 ${wfa} 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
|
}
|
|
@@ -493,7 +815,7 @@ export class Messaging {
|
|
|
493
815
|
):
|
|
494
816
|
| { kind: "ok"; operation: Operation; route: Route; bytes: number }
|
|
495
817
|
| { kind: "refused"; code: SendCode; error: string } {
|
|
496
|
-
const
|
|
818
|
+
const wfa = this.#host.launcher(from);
|
|
497
819
|
const operation = this.#current.get(from);
|
|
498
820
|
if (!operation) return refuse("no-operation", "this agent has no open operation.");
|
|
499
821
|
const { peer } = request;
|
|
@@ -504,16 +826,16 @@ export class Messaging {
|
|
|
504
826
|
return refuse(
|
|
505
827
|
"no-route",
|
|
506
828
|
owes
|
|
507
|
-
? `your route with ${peer} only answers its requests. Answer it with ${
|
|
829
|
+
? `your route with ${peer} only answers its requests. Answer it with ${wfa} reply ${peer}.`
|
|
508
830
|
: `your route with ${peer} only answers its requests, and it asked nothing. Nothing to send; end your turn.`,
|
|
509
831
|
);
|
|
510
832
|
};
|
|
511
833
|
if (initiating.length === 0) {
|
|
512
834
|
return this.#open(peer, from)
|
|
513
835
|
? replyOnly()
|
|
514
|
-
: refuse("no-route", `no route to ${peer}. Your routes: ${
|
|
836
|
+
: refuse("no-route", `no route to ${peer}. Your routes: ${wfa} 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",
|
|
@@ -567,7 +889,7 @@ export class Messaging {
|
|
|
567
889
|
):
|
|
568
890
|
| { kind: "ok"; operation: Operation; owed: Obligation; bytes: number }
|
|
569
891
|
| { kind: "refused"; code: ReplyCode; error: string } {
|
|
570
|
-
const
|
|
892
|
+
const wfa = this.#host.launcher(from);
|
|
571
893
|
const operation = this.#current.get(from);
|
|
572
894
|
if (!operation) return refuse("no-operation", "this agent has no open operation.");
|
|
573
895
|
const { peer } = request;
|
|
@@ -576,7 +898,7 @@ export class Messaging {
|
|
|
576
898
|
(each) => each.asker === peer && each.owedBy === from && each.request.shown,
|
|
577
899
|
);
|
|
578
900
|
if (!latest || latest.state === "replied") {
|
|
579
|
-
return refuse("none-owed", `you owe ${peer} nothing. Send it with ${
|
|
901
|
+
return refuse("none-owed", `you owe ${peer} nothing. Send it with ${wfa} send ${peer}.`);
|
|
580
902
|
}
|
|
581
903
|
if (latest.state !== "open" || latest.owingOperation !== operation) {
|
|
582
904
|
const agent = this.#host.agent(peer);
|
|
@@ -584,7 +906,7 @@ export class Messaging {
|
|
|
584
906
|
"expired",
|
|
585
907
|
agent?.state === "stopped"
|
|
586
908
|
? `${peer} has stopped${agent.reason ? ` (${JSON.stringify(agent.reason)})` : ""}; nothing reaches it now.`
|
|
587
|
-
: `${peer} stopped waiting at ${clock(latest.endedAt ?? this.#host.now(), this.#host.now())}. Send it with ${
|
|
909
|
+
: `${peer} stopped waiting at ${clock(latest.endedAt ?? this.#host.now(), this.#host.now())}. Send it with ${wfa} send ${peer}.`,
|
|
588
910
|
);
|
|
589
911
|
}
|
|
590
912
|
const route = latest.request.route;
|
|
@@ -628,7 +950,7 @@ export class Messaging {
|
|
|
628
950
|
/** The question a reply answers, which counts as answered here. */
|
|
629
951
|
answering?: Obligation,
|
|
630
952
|
): { kind: "refused"; code: "awaiting" | "unsupported" | "cycle"; error: string } | undefined {
|
|
631
|
-
const
|
|
953
|
+
const wfa = this.#host.launcher(from);
|
|
632
954
|
if (operation.awaits?.state === "open") {
|
|
633
955
|
return refuse(
|
|
634
956
|
"awaiting",
|
|
@@ -662,7 +984,7 @@ export class Messaging {
|
|
|
662
984
|
return refuse(
|
|
663
985
|
"cycle",
|
|
664
986
|
owing
|
|
665
|
-
? `${who}, and you owe ${last === peer ? "it" : last} a reply. Answer with ${
|
|
987
|
+
? `${who}, and you owe ${last === peer ? "it" : last} a reply. Answer with ${wfa} reply ${last}; it may --await-reply.`
|
|
666
988
|
: `${who}. Send without --await-reply and end your turn; ${last}'s request reaches you then.`,
|
|
667
989
|
);
|
|
668
990
|
}
|
|
@@ -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 `wfa send` returns: a failed write rejects, and `wfa` 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,14 +1133,14 @@ export class Messaging {
|
|
|
783
1133
|
return `sent to ${peer}; ${when}.`;
|
|
784
1134
|
}
|
|
785
1135
|
|
|
786
|
-
#awaitNotice(operation: Operation, until: number): string {
|
|
787
|
-
const
|
|
1136
|
+
#awaitNotice(operation: Operation, { until }: { until?: number }): string {
|
|
1137
|
+
const wfa = 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
|
? [
|
|
793
|
-
`If you find the answer first, or decide not to wait, finish with ${
|
|
1143
|
+
`If you find the answer first, or decide not to wait, finish with ${wfa} result ${operation.id}; the wait is dropped.`,
|
|
794
1144
|
]
|
|
795
1145
|
: []),
|
|
796
1146
|
].join("\n");
|
|
@@ -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");
|
|
@@ -969,7 +1323,7 @@ export class Messaging {
|
|
|
969
1323
|
/**
|
|
970
1324
|
* What is held for an operation whose turn has ended, deciding nothing: `continues` when its turn
|
|
971
1325
|
* end would go on with it, `held` when messages wait for a later operation or to cut a check-in's
|
|
972
|
-
* `
|
|
1326
|
+
* `wfa waiting` short, `none` otherwise.
|
|
973
1327
|
*/
|
|
974
1328
|
pending(id: string): Pending {
|
|
975
1329
|
const operation = this.#operations.get(id);
|
|
@@ -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;
|
|
@@ -1043,7 +1405,7 @@ export class Messaging {
|
|
|
1043
1405
|
};
|
|
1044
1406
|
}
|
|
1045
1407
|
|
|
1046
|
-
/** The line `
|
|
1408
|
+
/** The line `wfa` leads with while this agent's operation owes or awaits an answer. */
|
|
1047
1409
|
owing(agent: AgentKey): string | undefined {
|
|
1048
1410
|
const { owes, awaits } = this.#standing(agent);
|
|
1049
1411
|
return owingLine(owes, awaits, this.#host.launcher(agent), this.#host.now());
|
|
@@ -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
|
});
|
|
@@ -1114,7 +1476,7 @@ export class Messaging {
|
|
|
1114
1476
|
|
|
1115
1477
|
/**
|
|
1116
1478
|
* This agent's open routes, either end, in the order granted; with `stopped`, also those a peer's
|
|
1117
|
-
* stop closed, so `
|
|
1479
|
+
* stop closed, so `wfa peers` can say it stopped.
|
|
1118
1480
|
*/
|
|
1119
1481
|
#routesOf(agent: AgentKey, stopped = false): Route[] {
|
|
1120
1482
|
return [...new Set([...this.#routes.values()].flat())]
|
|
@@ -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
|
+
}
|