@fungi.computer/bird-dog 0.3.3 → 0.3.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/README.md +51 -108
- package/dist/effect.d.ts +10 -32
- package/dist/effect.js +11 -47
- package/dist/index.d.ts +5 -24
- package/dist/internal/promise-mail-room.js +3 -30
- package/docs/api/effect.md +12 -51
- package/docs/api/index.md +3 -28
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
# BirdDog (`@fungi.computer/bird-dog`)
|
|
2
2
|
|
|
3
|
-
BirdDog
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- It translates addressed agent work into an opaque Watchdog job, and decodes
|
|
12
|
-
that job's payload back after Watchdog claims it.
|
|
13
|
-
- It fans out replies when the job settles, stores them in your outbox, and
|
|
14
|
-
delivers them with retries through Carrier's Post outbox
|
|
15
|
-
([`@fungi.computer/carrier/post`](../carrier/README.md)).
|
|
16
|
-
|
|
17
|
-
Every sender and recipient is an ordinary agent session. A parent and a child
|
|
18
|
-
agent are both plain agents to BirdDog. Their relationship lives in your code,
|
|
19
|
-
and all agent work uses Watchdog's `default` lane. You own storage, transport,
|
|
20
|
-
the clock and wake scheduling. BirdDog owns the envelope format, the reply keys
|
|
21
|
-
and the retry schedule.
|
|
3
|
+
BirdDog translates addressed Agent work into Watchdog jobs and builds replies
|
|
4
|
+
when that work settles. It owns envelope grammar, reply keys and settlement
|
|
5
|
+
content. The host stages each reply batch through its existing Carrier Post
|
|
6
|
+
owner; Watchdog owns delivery attempts and wakes.
|
|
7
|
+
|
|
8
|
+
Every sender and recipient is an ordinary Agent Session. Parent and child
|
|
9
|
+
relationships remain in the host's domain records.
|
|
22
10
|
|
|
23
11
|
## Install or set up
|
|
24
12
|
|
|
@@ -26,10 +14,10 @@ and the retry schedule.
|
|
|
26
14
|
npm install @fungi.computer/bird-dog
|
|
27
15
|
```
|
|
28
16
|
|
|
29
|
-
BirdDog is ESM-only. It depends on `@fungi.computer/watchdog`,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
BirdDog is ESM-only. It depends on `@fungi.computer/watchdog` and `effect`,
|
|
18
|
+
which npm installs for you. It does no I/O of its own, so it runs in Node,
|
|
19
|
+
Cloudflare Workers or any runtime that can stage a batch of mail in its existing
|
|
20
|
+
delivery owner.
|
|
33
21
|
|
|
34
22
|
Entrypoints:
|
|
35
23
|
|
|
@@ -41,13 +29,11 @@ Entrypoints:
|
|
|
41
29
|
|
|
42
30
|
## Quick start
|
|
43
31
|
|
|
44
|
-
This
|
|
45
|
-
|
|
46
|
-
prints the reply instead of sending it anywhere.
|
|
32
|
+
This constructs a reply batch. The example prints it; a durable host stages the
|
|
33
|
+
batch in its existing Carrier Post transaction.
|
|
47
34
|
|
|
48
35
|
```ts
|
|
49
36
|
import { BirdDog, type BirdDogMail } from "@fungi.computer/bird-dog";
|
|
50
|
-
import { createMemoryOutbox } from "@fungi.computer/carrier/testing";
|
|
51
37
|
|
|
52
38
|
const parent = {
|
|
53
39
|
kind: "agent",
|
|
@@ -61,13 +47,8 @@ const child = {
|
|
|
61
47
|
} as const;
|
|
62
48
|
|
|
63
49
|
const room = await BirdDog.make({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
console.log(`to ${mail.to.agentId}:\n${mail.content}`);
|
|
67
|
-
return true; // the recipient accepted it
|
|
68
|
-
},
|
|
69
|
-
scheduleWake: async (at) => {
|
|
70
|
-
console.log(`wake me at ${at}`);
|
|
50
|
+
stage: async (mail: readonly BirdDogMail[]) => {
|
|
51
|
+
for (const reply of mail) console.log(reply.content);
|
|
71
52
|
},
|
|
72
53
|
now: () => Date.now(),
|
|
73
54
|
});
|
|
@@ -94,16 +75,16 @@ const job = BirdDog.agentJob({
|
|
|
94
75
|
const payload = BirdDog.decodeAgentJob(job);
|
|
95
76
|
console.log(payload.work.commandFactId); // "command-1"
|
|
96
77
|
|
|
97
|
-
// 3. When Watchdog settles the job,
|
|
78
|
+
// 3. When Watchdog settles the job, stage the replies.
|
|
98
79
|
await room.fanOut({
|
|
99
80
|
job: { ...job, state: "settled", outcome: "completed" },
|
|
100
81
|
content: "A test fixture was missing. Fixed it.",
|
|
101
82
|
});
|
|
102
|
-
await room.drainOutbox();
|
|
103
83
|
```
|
|
104
84
|
|
|
105
|
-
In a real host, the settled job comes from Watchdog
|
|
106
|
-
|
|
85
|
+
In a real host, the settled job comes from Watchdog and `stage` commits the
|
|
86
|
+
whole batch before the settlement cursor advances. Replaying a stable reply key
|
|
87
|
+
retains the first staged letter, including its original timestamp.
|
|
107
88
|
|
|
108
89
|
## Core concepts
|
|
109
90
|
|
|
@@ -125,9 +106,8 @@ Watchdog's `transactional.enqueueAcceptedJob`. `BirdDog.decodeAgentJob(job)`
|
|
|
125
106
|
parses the payload back on the executor side. Watchdog never reads it.
|
|
126
107
|
|
|
127
108
|
**Fan-out.** `fanOut({ job, content, diagnostic? })` takes a settled job, the
|
|
128
|
-
content owed to correspondents and the Session's terminal diagnostic. It
|
|
129
|
-
one reply per `replyTo` address
|
|
130
|
-
`outbox:<mail key>:reply:<index>`, then schedules one wake for the earliest.
|
|
109
|
+
content owed to correspondents and the Session's terminal diagnostic. It passes
|
|
110
|
+
one reply per `replyTo` address to `stage`, keyed `<mail key>:reply:<index>`.
|
|
131
111
|
Each reply's `from` is the mail's recipient, its `content` is your content
|
|
132
112
|
verbatim (or, when it is empty,
|
|
133
113
|
`The delegated work ended <outcome> without reply text.`, so a reply is always
|
|
@@ -155,31 +135,10 @@ The recipient's host owns authentication and presentation. It trusts the `reply`
|
|
|
155
135
|
facts only after checking the reply against the mail it sent, then decides who
|
|
156
136
|
authors the line and how its model sees it.
|
|
157
137
|
|
|
158
|
-
**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
**Receipts.** A delivered reply's row is replaced by a compact completion
|
|
164
|
-
receipt under the same key. Replaying the same settlement after a restart finds
|
|
165
|
-
the receipt and does not post the reply again. Keep those receipts as long as
|
|
166
|
-
source settlements can replay. BirdDog does not expire them.
|
|
167
|
-
`hasPendingDelivery(outbox)` reads only whether any reply still awaits a
|
|
168
|
-
delivery attempt, so a host can tell retained receipts and dead letters from
|
|
169
|
-
owed mail.
|
|
170
|
-
|
|
171
|
-
The Effect mail room's `nextWakeAt()` reads Carrier's earliest owed delivery,
|
|
172
|
-
including future retries. Runtime hosts include that hint in their pending-work
|
|
173
|
-
answer rather than clearing a retry when other runtime work is idle.
|
|
174
|
-
|
|
175
|
-
**Host ports.** `BirdDog.make` takes four things:
|
|
176
|
-
|
|
177
|
-
- `outbox`: string rows with `put`, `read`, ordered `list(limit, afterKey?)` and
|
|
178
|
-
`delete`. Use the same key ordering for sorting and the exclusive `afterKey`
|
|
179
|
-
filter so paging cannot skip or repeat rows.
|
|
180
|
-
- `post(mail)`: delivers one reply and reports acceptance.
|
|
181
|
-
- `scheduleWake(at)`: arms a durable timer for `drainOutbox`.
|
|
182
|
-
- `now()`: the current time in milliseconds.
|
|
138
|
+
**Host ports.** `BirdDog.make` takes `stage(mail)` and `now()`. Staging is one
|
|
139
|
+
batch operation so the host can commit all replies atomically. The host retains
|
|
140
|
+
stable keys, prepared delivery facts and receipts through Carrier Post. Watchdog
|
|
141
|
+
reserves attempts, resumes retries after reopen and reports the next wake.
|
|
183
142
|
|
|
184
143
|
## API reference
|
|
185
144
|
|
|
@@ -187,19 +146,19 @@ The generated reference is in [docs/api](docs/api/README.md).
|
|
|
187
146
|
|
|
188
147
|
- `@fungi.computer/bird-dog`: `BirdDog` with `agentJob`, `decodeAgentJob` and
|
|
189
148
|
`make`, plus the `BirdDogMail`, `BirdDogAgentReference`, `BirdDogAgentWork`,
|
|
190
|
-
`BirdDogAgentJobPayload`, `BirdDogReply`, `
|
|
149
|
+
`BirdDogAgentJobPayload`, `BirdDogReply`, `BirdDogOptions`,
|
|
191
150
|
`BirdDogSettlement` and `BirdDogRuntime` types.
|
|
192
151
|
- `@fungi.computer/bird-dog/effect`: `make`, `agentJobFromInput`,
|
|
193
|
-
`decodeAgentJob`, `settlementReplies`, `
|
|
194
|
-
`
|
|
195
|
-
`
|
|
196
|
-
`
|
|
197
|
-
`
|
|
198
|
-
`BirdDogAgentJobInputSchema`.
|
|
152
|
+
`decodeAgentJob`, `settlementReplies`, `boundedDiagnostic`,
|
|
153
|
+
`BIRD_DOG_DIAGNOSTIC_MAX_LENGTH`, `BirdDogError`, `EffectBirdDogOptions`,
|
|
154
|
+
`BirdDogEffect` and the schemas `BirdDogAgentReferenceSchema`,
|
|
155
|
+
`BirdDogReplySchema`, `BirdDogMailSchema`, `BirdDogAgentWorkSchema`,
|
|
156
|
+
`BirdDogAgentJobPayloadSchema` and `BirdDogAgentJobInputSchema`.
|
|
199
157
|
|
|
200
158
|
## Examples and checks
|
|
201
159
|
|
|
202
|
-
The Effect entrypoint takes the same ports as Effect values
|
|
160
|
+
The Effect entrypoint takes the same ports as Effect values. Using `job` from
|
|
161
|
+
the quick start, this prints the settlement reply batch:
|
|
203
162
|
|
|
204
163
|
```ts
|
|
205
164
|
import * as Effect from "effect/Effect";
|
|
@@ -207,18 +166,19 @@ import { make } from "@fungi.computer/bird-dog/effect";
|
|
|
207
166
|
|
|
208
167
|
const room = await Effect.runPromise(
|
|
209
168
|
make({
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
delete: () => Effect.void,
|
|
215
|
-
},
|
|
216
|
-
post: () => Effect.succeed(true),
|
|
217
|
-
scheduleWake: () => Effect.void,
|
|
169
|
+
stage: (mail) =>
|
|
170
|
+
Effect.sync(() => {
|
|
171
|
+
for (const reply of mail) console.log(reply.content);
|
|
172
|
+
}),
|
|
218
173
|
now: () => Date.now(),
|
|
219
174
|
}),
|
|
220
175
|
);
|
|
221
|
-
await Effect.runPromise(
|
|
176
|
+
await Effect.runPromise(
|
|
177
|
+
room.fanOut({
|
|
178
|
+
job: { ...job, state: "settled", outcome: "completed" },
|
|
179
|
+
content: "A test fixture was missing. Fixed it.",
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
222
182
|
```
|
|
223
183
|
|
|
224
184
|
Use the schemas to parse mail that arrives from outside your process:
|
|
@@ -230,11 +190,9 @@ import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
|
|
|
230
190
|
const mail = Schema.decodeUnknownSync(BirdDogMailSchema)(JSON.parse(body));
|
|
231
191
|
```
|
|
232
192
|
|
|
233
|
-
The package tests cover job translation,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
past full pages, retry exhaustion, receipt walks, wake failure after fan-out,
|
|
237
|
-
and parity between the Promise and Effect schemas. From the package directory:
|
|
193
|
+
The package tests cover job translation, typed settlement replies, bounded and
|
|
194
|
+
redacted diagnostics, and batch staging failures. Carrier and Local consumer
|
|
195
|
+
tests cover durable retries, receipt loss and physical reopen.
|
|
238
196
|
|
|
239
197
|
```sh
|
|
240
198
|
pnpm test
|
|
@@ -242,26 +200,11 @@ pnpm test
|
|
|
242
200
|
|
|
243
201
|
## Limits and errors
|
|
244
202
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
idempotently.
|
|
251
|
-
- **Malformed rows.** A row that does not parse is deleted during a drain. A
|
|
252
|
-
malformed row met during fan-out fails that fan-out.
|
|
253
|
-
- **Fan-out writes first.** Every reply is persisted before the wake is
|
|
254
|
-
scheduled. If `scheduleWake` fails, `fanOut` rejects but the replies are
|
|
255
|
-
already in the outbox, so replaying the settlement is safe.
|
|
256
|
-
- **Invalid input.** `agentJob` and `decodeAgentJob` throw a schema error on
|
|
257
|
-
input that does not match. `fanOut` rejects with
|
|
258
|
-
`BirdDogError("invalid agent job payload")` for a job whose payload is not
|
|
259
|
-
BirdDog's.
|
|
260
|
-
- **Host failures.** The Promise mail room wraps a failing port in a
|
|
261
|
-
`BirdDogError` and keeps your original error as its `cause`. The messages are
|
|
262
|
-
`outbox read failed`, `outbox write failed`, `outbox delete failed`,
|
|
263
|
-
`mail transport failed` and `wake scheduling failed`. A `post` that throws
|
|
264
|
-
during a drain is logged and retried, not rethrown.
|
|
203
|
+
`agentJob` and `decodeAgentJob` reject invalid input using the maintained
|
|
204
|
+
schema. A failing Promise staging port becomes
|
|
205
|
+
`BirdDogError("mail staging failed")` with the original error as its `cause`.
|
|
206
|
+
The caller must retain the settlement cursor until staging succeeds. Delivery
|
|
207
|
+
remains at least once: receivers must admit a repeated mail key idempotently.
|
|
265
208
|
|
|
266
209
|
## Contribute and license
|
|
267
210
|
|
package/dist/effect.d.ts
CHANGED
|
@@ -158,6 +158,7 @@ export declare const BirdDogAgentWorkSchema: Schema.Struct<{
|
|
|
158
158
|
readonly commandFactId: Schema.String;
|
|
159
159
|
readonly recovery: Schema.optional<Schema.Struct<{
|
|
160
160
|
readonly maxRecoveries: Schema.Int;
|
|
161
|
+
readonly windowMs: Schema.optionalKey<Schema.Int>;
|
|
161
162
|
}>>;
|
|
162
163
|
}>;
|
|
163
164
|
/** Parser for the payload that Watchdog stores without interpretation. */
|
|
@@ -237,6 +238,7 @@ export declare const BirdDogAgentJobInputSchema: Schema.Struct<{
|
|
|
237
238
|
readonly commandFactId: Schema.String;
|
|
238
239
|
readonly recovery: Schema.optional<Schema.Struct<{
|
|
239
240
|
readonly maxRecoveries: Schema.Int;
|
|
241
|
+
readonly windowMs: Schema.optionalKey<Schema.Int>;
|
|
240
242
|
}>>;
|
|
241
243
|
}>;
|
|
242
244
|
readonly delivery: Schema.Literals<readonly ["steer", "followUp"]>;
|
|
@@ -309,38 +311,19 @@ export declare class BirdDogError extends Error {
|
|
|
309
311
|
readonly _tag: "BirdDogError";
|
|
310
312
|
constructor(message: string, options?: ErrorOptions);
|
|
311
313
|
}
|
|
312
|
-
/**
|
|
314
|
+
/** Durable staging belongs to the host's existing Carrier Post transaction. */
|
|
313
315
|
export type EffectBirdDogOptions = Readonly<{
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
/** Read one encoded mail row, if it exists. */
|
|
318
|
-
read: (key: string) => Effect.Effect<string | undefined, BirdDogError>;
|
|
319
|
-
/** Read an ordered page of encoded mail rows. */
|
|
320
|
-
list: (limit: number, afterKey?: string) => Effect.Effect<readonly Readonly<{
|
|
321
|
-
key: string;
|
|
322
|
-
value: string;
|
|
323
|
-
}>[], BirdDogError>;
|
|
324
|
-
/** Remove an invalid encoded row; completed deliveries retain receipts. */
|
|
325
|
-
delete: (key: string) => Effect.Effect<void, BirdDogError>;
|
|
326
|
-
}>;
|
|
327
|
-
/** Deliver one reply mail and report whether it was accepted. */
|
|
328
|
-
post: (mail: BirdDogMail) => Effect.Effect<boolean, BirdDogError>;
|
|
329
|
-
/** Arrange a future wake for pending delivery work. */
|
|
330
|
-
scheduleWake: (at: number) => Effect.Effect<void, BirdDogError>;
|
|
331
|
-
/** Return the host's current wall-clock time in milliseconds. */
|
|
316
|
+
/** Commit the complete reply batch through the host delivery owner. */
|
|
317
|
+
stage: (mail: readonly BirdDogMail[]) => Effect.Effect<void, BirdDogError>;
|
|
318
|
+
/** Current time used when constructing a new reply. */
|
|
332
319
|
now: () => number;
|
|
333
320
|
}>;
|
|
334
|
-
/**
|
|
321
|
+
/** Agent reply construction and durable staging; Watchdog owns delivery progress. */
|
|
335
322
|
export type BirdDogEffect = Readonly<{
|
|
336
|
-
/**
|
|
323
|
+
/** Stage all reply letters for one settled command as one batch. */
|
|
337
324
|
fanOut: (settlement: BirdDogSettlement) => Effect.Effect<void, BirdDogError>;
|
|
338
|
-
/**
|
|
325
|
+
/** Stage a host-authored letter through the same durable delivery owner. */
|
|
339
326
|
postMail?: ((mail: BirdDogMail) => Effect.Effect<void, BirdDogError>) | undefined;
|
|
340
|
-
/** Deliver due replies and schedule any required retry wake. */
|
|
341
|
-
drainOutbox: () => Effect.Effect<void, BirdDogError>;
|
|
342
|
-
/** Earliest owed reply delivery, including retries; absent after delivery. */
|
|
343
|
-
nextWakeAt: () => Effect.Effect<number | undefined, BirdDogError>;
|
|
344
327
|
}>;
|
|
345
328
|
/** Validate and translate agent work into Watchdog's opaque job grammar. */
|
|
346
329
|
export declare const agentJobFromInput: (input: BirdDogAgentJobInput) => WatchdogQueuedJob;
|
|
@@ -362,10 +345,5 @@ export declare const decodeAgentJob: (job: WatchdogStoredJob) => BirdDogAgentJob
|
|
|
362
345
|
export declare function settlementReplies(settlement: BirdDogSettlement, createdAt: number): readonly BirdDogMail[];
|
|
363
346
|
/** Build the recipient's one stable acceptance notice for one original mail. */
|
|
364
347
|
export declare function acceptedNotice(mail: BirdDogMail, sessionId: string, commandId: string, createdAt: number): BirdDogMail;
|
|
365
|
-
/**
|
|
366
|
-
* Whether this mail room's outbox still owes any reply a delivery attempt.
|
|
367
|
-
* Delivered receipts and exhausted dead letters stay but owe nothing.
|
|
368
|
-
*/
|
|
369
|
-
export declare const hasPendingDelivery: (outbox: Pick<EffectBirdDogOptions["outbox"], "list">) => Effect.Effect<boolean, BirdDogError>;
|
|
370
|
-
/** Construct BirdDog's Effect-native mail room over Post's shared outbox. */
|
|
348
|
+
/** Construct BirdDog's reply semantics over the existing durable staging owner. */
|
|
371
349
|
export declare const make: (options: EffectBirdDogOptions) => Effect.Effect<BirdDogEffect>;
|
package/dist/effect.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
|
-
import { makePostOutbox, postOutboxPending, } from "@fungi.computer/carrier/post";
|
|
4
3
|
const Identifier = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
|
|
5
4
|
/** The maximum UTF-8 size of one mail body. */
|
|
6
5
|
export const BIRD_DOG_MAIL_CONTENT_MAX_BYTES = 256 * 1024;
|
|
@@ -147,6 +146,7 @@ export const BirdDogAgentWorkSchema = Schema.Struct({
|
|
|
147
146
|
commandFactId: Identifier,
|
|
148
147
|
recovery: Schema.optional(Schema.Struct({
|
|
149
148
|
maxRecoveries: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
149
|
+
windowMs: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
150
150
|
})),
|
|
151
151
|
});
|
|
152
152
|
/** Parser for the payload that Watchdog stores without interpretation. */
|
|
@@ -266,49 +266,13 @@ export function acceptedNotice(mail, sessionId, commandId, createdAt) {
|
|
|
266
266
|
reply,
|
|
267
267
|
};
|
|
268
268
|
}
|
|
269
|
-
/**
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
outbox: options.outbox,
|
|
280
|
-
deliver: options.post,
|
|
281
|
-
scheduleWake: options.scheduleWake,
|
|
282
|
-
now: options.now,
|
|
283
|
-
invalidRow: () => new BirdDogError("invalid outbox row"),
|
|
284
|
-
encodingFailed: () => new BirdDogError("could not encode outbox row"),
|
|
285
|
-
});
|
|
286
|
-
const fanOut = (settlement) => Effect.gen(function* () {
|
|
287
|
-
const createdAt = options.now();
|
|
288
|
-
const replies = yield* Effect.try({
|
|
289
|
-
try: () => settlementReplies(settlement, createdAt),
|
|
290
|
-
catch: () => new BirdDogError("invalid agent job payload"),
|
|
291
|
-
});
|
|
292
|
-
let wake;
|
|
293
|
-
for (const reply of replies) {
|
|
294
|
-
const hint = yield* post.enqueue(reply, createdAt);
|
|
295
|
-
if (hint !== undefined)
|
|
296
|
-
wake = wake === undefined ? hint : Math.min(wake, hint);
|
|
297
|
-
}
|
|
298
|
-
if (wake !== undefined)
|
|
299
|
-
yield* options.scheduleWake(wake);
|
|
300
|
-
});
|
|
301
|
-
const postMail = (mail) => Effect.gen(function* () {
|
|
302
|
-
const createdAt = options.now();
|
|
303
|
-
const hint = yield* post.enqueue(mail, createdAt);
|
|
304
|
-
if (hint !== undefined)
|
|
305
|
-
yield* options.scheduleWake(hint);
|
|
306
|
-
return;
|
|
307
|
-
});
|
|
308
|
-
return Effect.succeed({
|
|
309
|
-
fanOut,
|
|
310
|
-
postMail,
|
|
311
|
-
drainOutbox: post.drain,
|
|
312
|
-
nextWakeAt: post.nextWakeAt,
|
|
313
|
-
});
|
|
314
|
-
};
|
|
269
|
+
/** Construct BirdDog's reply semantics over the existing durable staging owner. */
|
|
270
|
+
export const make = (options) => Effect.succeed({
|
|
271
|
+
/** Stage the complete settlement reply batch. */
|
|
272
|
+
fanOut: (settlement) => Effect.try({
|
|
273
|
+
try: () => settlementReplies(settlement, options.now()),
|
|
274
|
+
catch: (cause) => new BirdDogError("invalid agent job payload", { cause }),
|
|
275
|
+
}).pipe(Effect.flatMap(options.stage)),
|
|
276
|
+
/** Stage one host-authored letter. */
|
|
277
|
+
postMail: (mail) => options.stage([mail]),
|
|
278
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -87,6 +87,7 @@ export type BirdDogAgentWork = Readonly<{
|
|
|
87
87
|
commandFactId: string;
|
|
88
88
|
recovery?: Readonly<{
|
|
89
89
|
maxRecoveries: number;
|
|
90
|
+
windowMs?: number;
|
|
90
91
|
}> | undefined;
|
|
91
92
|
}>;
|
|
92
93
|
/** Typed payload whose meaning belongs to BirdDog, never Watchdog. */
|
|
@@ -103,29 +104,11 @@ export type BirdDogAgentJobInput = Readonly<{
|
|
|
103
104
|
delivery: "steer" | "followUp";
|
|
104
105
|
mail?: BirdDogMail | undefined;
|
|
105
106
|
}>;
|
|
106
|
-
/**
|
|
107
|
-
export type BirdDogOutbox = Readonly<{
|
|
108
|
-
/** Store one encoded mail row under its durable key. */
|
|
109
|
-
put: (key: string, value: string) => Promise<void>;
|
|
110
|
-
/** Read one encoded mail row, if it exists. */
|
|
111
|
-
read: (key: string) => Promise<string | undefined>;
|
|
112
|
-
/** Read an ordered page of encoded mail rows. */
|
|
113
|
-
list: (limit: number, afterKey?: string) => Promise<readonly Readonly<{
|
|
114
|
-
key: string;
|
|
115
|
-
value: string;
|
|
116
|
-
}>[]>;
|
|
117
|
-
/** Remove an invalid encoded row; completed deliveries retain receipts. */
|
|
118
|
-
delete: (key: string) => Promise<void>;
|
|
119
|
-
}>;
|
|
120
|
-
/** Host ports for BirdDog's durable mail delivery. */
|
|
107
|
+
/** Host ports for reply staging in its existing Carrier Post owner. */
|
|
121
108
|
export type BirdDogOptions = Readonly<{
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
post: (mail: BirdDogMail) => Promise<boolean>;
|
|
126
|
-
/** Arrange a future wake for pending delivery work. */
|
|
127
|
-
scheduleWake: (at: number) => Promise<void>;
|
|
128
|
-
/** Return the host's current wall-clock time in milliseconds. */
|
|
109
|
+
/** Commit the complete reply batch through the host delivery owner. */
|
|
110
|
+
stage: (mail: readonly BirdDogMail[]) => Promise<void>;
|
|
111
|
+
/** Current time used when constructing a new reply. */
|
|
129
112
|
now: () => number;
|
|
130
113
|
}>;
|
|
131
114
|
/**
|
|
@@ -172,8 +155,6 @@ export type BirdDogWaitingItem = Readonly<{
|
|
|
172
155
|
export type BirdDogRuntime = Readonly<{
|
|
173
156
|
/** Queue replies owed by a settled agent job. */
|
|
174
157
|
fanOut: (settlement: BirdDogSettlement) => Promise<void>;
|
|
175
|
-
/** Deliver due replies and schedule any required retry wake. */
|
|
176
|
-
drainOutbox: () => Promise<void>;
|
|
177
158
|
}>;
|
|
178
159
|
/** BirdDog's small synchronous translation interface for owner transactions. */
|
|
179
160
|
export type BirdDogApi = Readonly<{
|
|
@@ -11,41 +11,14 @@ const runPromise = (effect) => Effect.runPromiseExit(effect).then((exit) => {
|
|
|
11
11
|
});
|
|
12
12
|
/** Lift Promise host ports once into BirdDog's Effect-owned mail room. */
|
|
13
13
|
export const createPromiseBirdDog = async (options) => {
|
|
14
|
-
const readOutbox = options.outbox.read;
|
|
15
|
-
const readPort = {
|
|
16
|
-
read: (key) => Effect.tryPromise({
|
|
17
|
-
try: () => readOutbox(key),
|
|
18
|
-
catch: (cause) => new BirdDogError("outbox read failed", { cause }),
|
|
19
|
-
}),
|
|
20
|
-
};
|
|
21
14
|
const runtime = await Effect.runPromise(make({
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
try: () => options.outbox.put(key, value),
|
|
26
|
-
catch: (cause) => new BirdDogError("outbox write failed", { cause }),
|
|
27
|
-
}),
|
|
28
|
-
list: (limit, afterKey) => Effect.tryPromise({
|
|
29
|
-
try: () => options.outbox.list(limit, afterKey),
|
|
30
|
-
catch: (cause) => new BirdDogError("outbox read failed", { cause }),
|
|
31
|
-
}),
|
|
32
|
-
delete: (key) => Effect.tryPromise({
|
|
33
|
-
try: () => options.outbox.delete(key),
|
|
34
|
-
catch: (cause) => new BirdDogError("outbox delete failed", { cause }),
|
|
35
|
-
}),
|
|
36
|
-
},
|
|
37
|
-
post: (mail) => Effect.tryPromise({
|
|
38
|
-
try: () => options.post(mail),
|
|
39
|
-
catch: (cause) => new BirdDogError("mail transport failed", { cause }),
|
|
40
|
-
}),
|
|
41
|
-
scheduleWake: (at) => Effect.tryPromise({
|
|
42
|
-
try: () => options.scheduleWake(at),
|
|
43
|
-
catch: (cause) => new BirdDogError("wake scheduling failed", { cause }),
|
|
15
|
+
stage: (mail) => Effect.tryPromise({
|
|
16
|
+
try: () => options.stage(mail),
|
|
17
|
+
catch: (cause) => new BirdDogError("mail staging failed", { cause }),
|
|
44
18
|
}),
|
|
45
19
|
now: options.now,
|
|
46
20
|
}));
|
|
47
21
|
return Object.freeze({
|
|
48
22
|
fanOut: (settlement) => runPromise(runtime.fanOut(settlement)),
|
|
49
|
-
drainOutbox: () => runPromise(runtime.drainOutbox()),
|
|
50
23
|
});
|
|
51
24
|
};
|
package/docs/api/effect.md
CHANGED
|
@@ -59,28 +59,12 @@ Stable discriminator for BirdDog-owned effect failures.
|
|
|
59
59
|
|
|
60
60
|
```ts
|
|
61
61
|
type EffectBirdDogOptions = Readonly<{
|
|
62
|
-
|
|
63
|
-
put: (key, value) => Effect.Effect<void, BirdDogError>;
|
|
64
|
-
read: (key) => Effect.Effect<string | undefined, BirdDogError>;
|
|
65
|
-
list: (
|
|
66
|
-
limit,
|
|
67
|
-
afterKey?,
|
|
68
|
-
) => Effect.Effect<
|
|
69
|
-
readonly Readonly<{
|
|
70
|
-
key: string;
|
|
71
|
-
value: string;
|
|
72
|
-
}>[],
|
|
73
|
-
BirdDogError
|
|
74
|
-
>;
|
|
75
|
-
delete: (key) => Effect.Effect<void, BirdDogError>;
|
|
76
|
-
}>;
|
|
77
|
-
post: (mail) => Effect.Effect<boolean, BirdDogError>;
|
|
78
|
-
scheduleWake: (at) => Effect.Effect<void, BirdDogError>;
|
|
62
|
+
stage: (mail) => Effect.Effect<void, BirdDogError>;
|
|
79
63
|
now: () => number;
|
|
80
64
|
}>;
|
|
81
65
|
```
|
|
82
66
|
|
|
83
|
-
|
|
67
|
+
Durable staging belongs to the host's existing Carrier Post transaction.
|
|
84
68
|
|
|
85
69
|
---
|
|
86
70
|
|
|
@@ -90,12 +74,10 @@ Effect host ports for the durable mail room.
|
|
|
90
74
|
type BirdDogEffect = Readonly<{
|
|
91
75
|
fanOut: (settlement) => Effect.Effect<void, BirdDogError>;
|
|
92
76
|
postMail?: (mail) => Effect.Effect<void, BirdDogError>;
|
|
93
|
-
drainOutbox: () => Effect.Effect<void, BirdDogError>;
|
|
94
|
-
nextWakeAt: () => Effect.Effect<number | undefined, BirdDogError>;
|
|
95
77
|
}>;
|
|
96
78
|
```
|
|
97
79
|
|
|
98
|
-
|
|
80
|
+
Agent reply construction and durable staging; Watchdog owns delivery progress.
|
|
99
81
|
|
|
100
82
|
## Variables
|
|
101
83
|
|
|
@@ -376,6 +358,7 @@ const BirdDogAgentWorkSchema: Struct<{
|
|
|
376
358
|
recovery: optional<
|
|
377
359
|
Struct<{
|
|
378
360
|
maxRecoveries: Int;
|
|
361
|
+
windowMs: optionalKey<Int>;
|
|
379
362
|
}>
|
|
380
363
|
>;
|
|
381
364
|
}>;
|
|
@@ -455,6 +438,7 @@ const BirdDogAgentJobInputSchema: Struct<{
|
|
|
455
438
|
commandFactId: String;
|
|
456
439
|
recovery: optional<Struct<{
|
|
457
440
|
maxRecoveries: Int;
|
|
441
|
+
windowMs: optionalKey<Int>;
|
|
458
442
|
}>>;
|
|
459
443
|
}>;
|
|
460
444
|
delivery: Literals<readonly ["steer", "followUp"]>;
|
|
@@ -560,8 +544,10 @@ Validate and translate agent work into Watchdog's opaque job grammar.
|
|
|
560
544
|
| `lane` | `string` |
|
|
561
545
|
| `priority` | `number` |
|
|
562
546
|
| `payload` | `unknown` |
|
|
547
|
+
| `notBefore?` | `number` |
|
|
563
548
|
| `recovery?` | `object` |
|
|
564
549
|
| `recovery.maxRecoveries` | `number` |
|
|
550
|
+
| `recovery.windowMs?` | `number` |
|
|
565
551
|
|
|
566
552
|
---
|
|
567
553
|
|
|
@@ -575,9 +561,9 @@ Decode the opaque payload at BirdDog's side of the Watchdog seam.
|
|
|
575
561
|
|
|
576
562
|
#### Parameters
|
|
577
563
|
|
|
578
|
-
| Parameter | Type
|
|
579
|
-
| --------- |
|
|
580
|
-
| `job` | \| \{ `state`: `"queued"`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `recovery?`: \{ `maxRecoveries`: `number`; \}; \} \| \{ `state`: `"running"`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `recovery?`: \{ `maxRecoveries`: `number`; \}; \} \| \{ `state`: `"settled"`; `outcome`: `"failed"` \| `"completed"` \| `"interrupted"` \| `"cancelled"` \| `"outcome_unknown"`; `failureReason?`: `string`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `recovery?`: \{ `maxRecoveries`: `number`; \}; \} |
|
|
564
|
+
| Parameter | Type |
|
|
565
|
+
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
566
|
+
| `job` | \| \{ `state`: `"queued"`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `notBefore?`: `number`; `recovery?`: \{ `maxRecoveries`: `number`; `windowMs?`: `number`; \}; \} \| \{ `state`: `"running"`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `notBefore?`: `number`; `recovery?`: \{ `maxRecoveries`: `number`; `windowMs?`: `number`; \}; \} \| \{ `state`: `"completing"`; `outcome`: `"failed"` \| `"completed"` \| `"interrupted"` \| `"cancelled"` \| `"outcome_unknown"`; `failureReason?`: `string`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `notBefore?`: `number`; `recovery?`: \{ `maxRecoveries`: `number`; `windowMs?`: `number`; \}; \} \| \{ `state`: `"settled"`; `outcome`: `"failed"` \| `"completed"` \| `"interrupted"` \| `"cancelled"` \| `"outcome_unknown"`; `failureReason?`: `string`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `notBefore?`: `number`; `recovery?`: \{ `maxRecoveries`: `number`; `windowMs?`: `number`; \}; \} |
|
|
581
567
|
|
|
582
568
|
#### Returns
|
|
583
569
|
|
|
@@ -639,27 +625,6 @@ Build the recipient's one stable acceptance notice for one original mail.
|
|
|
639
625
|
|
|
640
626
|
---
|
|
641
627
|
|
|
642
|
-
### hasPendingDelivery()
|
|
643
|
-
|
|
644
|
-
```ts
|
|
645
|
-
function hasPendingDelivery(outbox): Effect<boolean, BirdDogError>;
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
Whether this mail room's outbox still owes any reply a delivery attempt.
|
|
649
|
-
Delivered receipts and exhausted dead letters stay but owe nothing.
|
|
650
|
-
|
|
651
|
-
#### Parameters
|
|
652
|
-
|
|
653
|
-
| Parameter | Type |
|
|
654
|
-
| --------- | --------------------------------------------------------------------------------- |
|
|
655
|
-
| `outbox` | `Pick`\<[`EffectBirdDogOptions`](#effectbirddogoptions)\[`"outbox"`\], `"list"`\> |
|
|
656
|
-
|
|
657
|
-
#### Returns
|
|
658
|
-
|
|
659
|
-
`Effect`\<`boolean`, [`BirdDogError`](#birddogerror)\>
|
|
660
|
-
|
|
661
|
-
---
|
|
662
|
-
|
|
663
628
|
### make()
|
|
664
629
|
|
|
665
630
|
```ts
|
|
@@ -667,13 +632,11 @@ function make(options): Effect<
|
|
|
667
632
|
Readonly<{
|
|
668
633
|
fanOut: (settlement) => Effect<void, BirdDogError>;
|
|
669
634
|
postMail?: (mail) => Effect<void, BirdDogError>;
|
|
670
|
-
drainOutbox: () => Effect<void, BirdDogError>;
|
|
671
|
-
nextWakeAt: () => Effect<number | undefined, BirdDogError>;
|
|
672
635
|
}>
|
|
673
636
|
>;
|
|
674
637
|
```
|
|
675
638
|
|
|
676
|
-
Construct BirdDog's
|
|
639
|
+
Construct BirdDog's reply semantics over the existing durable staging owner.
|
|
677
640
|
|
|
678
641
|
#### Parameters
|
|
679
642
|
|
|
@@ -685,6 +648,4 @@ Construct BirdDog's Effect-native mail room over Post's shared outbox.
|
|
|
685
648
|
|
|
686
649
|
`Effect`\<`Readonly`\<\{ `fanOut`: (`settlement`) => `Effect`\<`void`,
|
|
687
650
|
[`BirdDogError`](#birddogerror)\>; `postMail?`: (`mail`) => `Effect`\<`void`,
|
|
688
|
-
[`BirdDogError`](#birddogerror)\>;
|
|
689
|
-
[`BirdDogError`](#birddogerror)\>; `nextWakeAt`: () => `Effect`\<`number` \|
|
|
690
|
-
`undefined`, [`BirdDogError`](#birddogerror)\>; \}\>\>
|
|
651
|
+
[`BirdDogError`](#birddogerror)\>; \}\>\>
|
package/docs/api/index.md
CHANGED
|
@@ -155,6 +155,7 @@ type BirdDogAgentWork = Readonly<{
|
|
|
155
155
|
commandFactId: string;
|
|
156
156
|
recovery?: Readonly<{
|
|
157
157
|
maxRecoveries: number;
|
|
158
|
+
windowMs?: number;
|
|
158
159
|
}>;
|
|
159
160
|
}>;
|
|
160
161
|
```
|
|
@@ -193,41 +194,16 @@ Input for projecting accepted agent work into Watchdog's generic grammar.
|
|
|
193
194
|
|
|
194
195
|
---
|
|
195
196
|
|
|
196
|
-
### BirdDogOutbox
|
|
197
|
-
|
|
198
|
-
```ts
|
|
199
|
-
type BirdDogOutbox = Readonly<{
|
|
200
|
-
put: (key, value) => Promise<void>;
|
|
201
|
-
read: (key) => Promise<string | undefined>;
|
|
202
|
-
list: (
|
|
203
|
-
limit,
|
|
204
|
-
afterKey?,
|
|
205
|
-
) => Promise<
|
|
206
|
-
readonly Readonly<{
|
|
207
|
-
key: string;
|
|
208
|
-
value: string;
|
|
209
|
-
}>[]
|
|
210
|
-
>;
|
|
211
|
-
delete: (key) => Promise<void>;
|
|
212
|
-
}>;
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
String-backed durable outbox owned by the BirdDog host.
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
197
|
### BirdDogOptions
|
|
220
198
|
|
|
221
199
|
```ts
|
|
222
200
|
type BirdDogOptions = Readonly<{
|
|
223
|
-
|
|
224
|
-
post: (mail) => Promise<boolean>;
|
|
225
|
-
scheduleWake: (at) => Promise<void>;
|
|
201
|
+
stage: (mail) => Promise<void>;
|
|
226
202
|
now: () => number;
|
|
227
203
|
}>;
|
|
228
204
|
```
|
|
229
205
|
|
|
230
|
-
Host ports for
|
|
206
|
+
Host ports for reply staging in its existing Carrier Post owner.
|
|
231
207
|
|
|
232
208
|
---
|
|
233
209
|
|
|
@@ -282,7 +258,6 @@ One open item named by a reply as waiting on its recipient.
|
|
|
282
258
|
```ts
|
|
283
259
|
type BirdDogRuntime = Readonly<{
|
|
284
260
|
fanOut: (settlement) => Promise<void>;
|
|
285
|
-
drainOutbox: () => Promise<void>;
|
|
286
261
|
}>;
|
|
287
262
|
```
|
|
288
263
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fungi.computer/bird-dog",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"author": "Levi <levi@technomancy.dev>",
|
|
5
5
|
"description": "Durable addressed agent mail over generic Watchdog work",
|
|
6
6
|
"type": "module",
|
|
@@ -23,8 +23,7 @@
|
|
|
23
23
|
"LICENSE"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@fungi.computer/
|
|
27
|
-
"@fungi.computer/watchdog": "0.1.0",
|
|
26
|
+
"@fungi.computer/watchdog": "0.1.1",
|
|
28
27
|
"effect": "4.0.2"
|
|
29
28
|
},
|
|
30
29
|
"devDependencies": {
|