@fungi.computer/shiitake 0.5.0 → 0.6.0
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/dist/agent-control.d.ts +93 -1
- package/dist/agent-control.js +108 -6
- package/dist/client-entry.d.ts +20 -0
- package/dist/client-entry.js +5 -0
- package/dist/client-visitor.d.ts +31 -0
- package/dist/client-visitor.js +226 -0
- package/dist/client-wire.d.ts +5 -0
- package/dist/client-wire.js +6 -0
- package/dist/client.d.ts +4 -3
- package/dist/client.js +1 -0
- package/dist/delegation.d.ts +74 -53
- package/dist/delegation.js +4 -15
- package/dist/effect.d.ts +25 -0
- package/dist/effect.js +14 -1
- package/dist/extensions.d.ts +21 -2
- package/dist/extensions.js +1 -1
- package/dist/fleet.d.ts +15 -0
- package/dist/fleet.js +32 -6
- package/dist/hosted.js +3 -0
- package/dist/http-routes.d.ts +11 -0
- package/dist/internal/accepted-run-executor.js +2 -1
- package/dist/internal/admission-door.d.ts +5 -0
- package/dist/internal/admission-door.js +27 -1
- package/dist/internal/contracts.d.ts +14 -1
- package/dist/internal/coordinator.js +8 -2
- package/dist/internal/effect-client.js +1 -1
- package/dist/internal/effect-contracts.d.ts +4 -2
- package/dist/internal/extension-command-registry.d.ts +2 -1
- package/dist/internal/extension-command-registry.js +3 -1
- package/dist/internal/extension-runtime.d.ts +3 -1
- package/dist/internal/extension-runtime.js +13 -5
- package/dist/internal/model-acquisition.js +2 -0
- package/dist/internal/observation-kernel.d.ts +2 -0
- package/dist/internal/observation-kernel.js +17 -6
- package/dist/internal/run-scope.d.ts +14 -0
- package/dist/internal/run-scope.js +32 -9
- package/dist/internal/runtime-coordinator.js +8 -1
- package/dist/internal/runtime-reconciliation.js +119 -24
- package/dist/internal/woodstock/agent-reply.d.ts +5 -0
- package/dist/internal/woodstock/agent-reply.js +3 -0
- package/dist/internal/woodstock/client-projection.js +9 -1
- package/dist/internal/woodstock/index.d.ts +4 -0
- package/dist/internal/woodstock/index.js +7 -0
- package/dist/internal/woodstock/session-delegator.d.ts +12 -0
- package/dist/internal/woodstock/session-delegator.js +44 -0
- package/dist/internal/woodstock/session-entry.d.ts +12 -2
- package/dist/internal/woodstock/session-store.d.ts +2 -0
- package/dist/internal/woodstock/session-store.js +20 -0
- package/dist/internal/woodstock/work-senders.d.ts +32 -0
- package/dist/internal/woodstock/work-senders.js +70 -0
- package/dist/remote-client.js +7 -2
- package/dist/server-visitor-stream.d.ts +13 -0
- package/dist/server-visitor-stream.js +115 -0
- package/dist/session-attention.d.ts +30 -0
- package/dist/session-attention.js +14 -0
- package/docs/api/agent-control.md +209 -13
- package/docs/api/client-entry.md +3 -0
- package/docs/api/client.md +136 -1
- package/docs/api/cloudflare.md +7 -1
- package/docs/api/delegation.md +234 -61
- package/docs/api/effect.md +16 -6
- package/docs/api/extensions.md +65 -4
- package/docs/api/fleet.md +69 -16
- package/docs/api/index.md +6 -4
- package/package.json +10 -6
|
@@ -5,21 +5,19 @@ import * as Schema from "effect/Schema";
|
|
|
5
5
|
import { agentTurnRecovery } from "./accepted-run-executor.js";
|
|
6
6
|
import { ShiitakeError } from "../errors.js";
|
|
7
7
|
import { ExtensionId } from "../extension-contract.js";
|
|
8
|
-
import { coreConsumer } from "./extension-runtime.js";
|
|
8
|
+
import { coreConsumer, reportConsumer, } from "./extension-runtime.js";
|
|
9
9
|
import { publishOwnerObservation } from "./observation-kernel.js";
|
|
10
|
-
import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSettlementMeta, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
|
|
10
|
+
import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSessionDelegator, readSettlementMeta, readWaitingOn, readWaitingOnSender, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
|
|
11
11
|
const pageSize = 64;
|
|
12
12
|
function runtimeFailure(message) {
|
|
13
13
|
return new ShiitakeError("runtime", "rejected", message);
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/** BirdDog's settlement facts for one settled job, without its correspondents. */
|
|
16
|
+
function settlementFacts(store, job,
|
|
17
|
+
/** The open items this settlement names as waiting on its recipient. */
|
|
18
|
+
waitingOnYou) {
|
|
18
19
|
return Effect.gen(function* () {
|
|
19
|
-
const payload = yield*
|
|
20
|
-
try: () => decodeAgentJob(job),
|
|
21
|
-
catch: () => runtimeFailure("Agent work payload is invalid"),
|
|
22
|
-
});
|
|
20
|
+
const payload = yield* decodeJob(job);
|
|
23
21
|
const content = job.outcome === "cancelled"
|
|
24
22
|
? ""
|
|
25
23
|
: yield* readSettlementContent(store, payload.work);
|
|
@@ -27,14 +25,103 @@ function fanOutSettlement(store, mailRoom, job) {
|
|
|
27
25
|
const diagnostic = job.outcome === "completed" || job.outcome === "cancelled"
|
|
28
26
|
? undefined
|
|
29
27
|
: yield* readSettlementDiagnostic(store, payload.work);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
let settled = { job, content };
|
|
29
|
+
if (diagnostic !== undefined)
|
|
30
|
+
settled = { ...settled, diagnostic };
|
|
31
|
+
// A cancelled run names nothing; what it left open is the canceller's to see.
|
|
32
|
+
const waiting = job.outcome === "cancelled" ? [] : yield* waitingOnYou;
|
|
33
|
+
if (waiting.length > 0)
|
|
34
|
+
settled = { ...settled, waitingOnYou: waiting };
|
|
35
|
+
return meta === undefined ? settled : { ...settled, meta };
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
function decodeJob(job) {
|
|
39
|
+
return Effect.try({
|
|
40
|
+
try: () => decodeAgentJob(job),
|
|
41
|
+
catch: () => runtimeFailure("Agent work payload is invalid"),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function fanOut(mailRoom, settled) {
|
|
45
|
+
return mailRoom
|
|
46
|
+
.fanOut(settled)
|
|
47
|
+
.pipe(Effect.mapError(() => runtimeFailure("Agent reply fan-out is unavailable")));
|
|
48
|
+
}
|
|
49
|
+
/** Reply to the delegating mail that admitted this work, if it asked for one. */
|
|
50
|
+
function replyToMail(store, mailRoom, job) {
|
|
51
|
+
if (mailRoom === undefined)
|
|
52
|
+
return Effect.void;
|
|
53
|
+
return Effect.gen(function* () {
|
|
54
|
+
const payload = yield* decodeJob(job);
|
|
55
|
+
if ((payload.mail?.replyTo.length ?? 0) === 0)
|
|
56
|
+
return;
|
|
57
|
+
// The reply goes to the mail's sender: name what waits on that sender.
|
|
58
|
+
const waiting = Effect.try({
|
|
59
|
+
try: () => readWaitingOnSender(store, payload.work.sessionId, payload.work.commandFactId),
|
|
60
|
+
catch: () => runtimeFailure("Session attention is unavailable"),
|
|
61
|
+
});
|
|
62
|
+
yield* fanOut(mailRoom, yield* settlementFacts(store, job, waiting));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Report work no delegating mail answered to the Session's delegator of
|
|
67
|
+
* record. It runs after the extensions have seen the settlement, so a goal
|
|
68
|
+
* or loop that admitted the next turn keeps the Session busy and the stretch
|
|
69
|
+
* of turns reports once, when the Session finally rests. Another Agent's
|
|
70
|
+
* original mail is that Agent's correspondence and never reported here.
|
|
71
|
+
*/
|
|
72
|
+
function reportToDelegator(store, watchdog, mailRoom, job) {
|
|
73
|
+
if (mailRoom === undefined)
|
|
74
|
+
return Effect.void;
|
|
75
|
+
return Effect.gen(function* () {
|
|
76
|
+
const payload = yield* decodeJob(job);
|
|
77
|
+
const mail = payload.mail;
|
|
78
|
+
if (mail !== undefined && mail.replyTo.length > 0)
|
|
79
|
+
return;
|
|
80
|
+
const head = yield* watchdog
|
|
81
|
+
.readQueueHead(job.queue)
|
|
82
|
+
.pipe(Effect.mapError(() => runtimeFailure("Session queue head is unavailable")));
|
|
83
|
+
if (head.state !== "idle")
|
|
84
|
+
return;
|
|
85
|
+
const delegator = yield* Effect.try({
|
|
86
|
+
try: () => readSessionDelegator(store, payload.work.sessionId),
|
|
87
|
+
catch: () => runtimeFailure("Session delegator is unavailable"),
|
|
88
|
+
});
|
|
89
|
+
if (delegator === undefined)
|
|
90
|
+
return;
|
|
91
|
+
const from = mail?.reply === undefined ? mail?.from : undefined;
|
|
92
|
+
const anotherAgentsMail = from !== undefined &&
|
|
93
|
+
!(from.kind === "agent" &&
|
|
94
|
+
delegator.replyTo.some(({ agentId }) => agentId === from.agentId));
|
|
95
|
+
if (anotherAgentsMail)
|
|
96
|
+
return;
|
|
97
|
+
// The report goes to the delegator: name what waits on it.
|
|
98
|
+
const waiting = Effect.try({
|
|
99
|
+
try: () => readWaitingOn(store, payload.work.sessionId, delegator.replyTo),
|
|
100
|
+
catch: () => runtimeFailure("Session attention is unavailable"),
|
|
101
|
+
});
|
|
102
|
+
const settled = yield* settlementFacts(store, job, waiting);
|
|
103
|
+
yield* fanOut(mailRoom, { ...settled, delegator });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The `core-report` settled consumer: after the extensions' turn, report each
|
|
108
|
+
* Session's last settled job in the page, one record at a time.
|
|
109
|
+
*/
|
|
110
|
+
function reportSettled(store, watchdog, mailRoom, records) {
|
|
111
|
+
return Effect.forEach(records, (record, index) => Effect.gen(function* () {
|
|
112
|
+
const later = records
|
|
113
|
+
.slice(index + 1)
|
|
114
|
+
.some(({ job }) => job.queue === record.job.queue);
|
|
115
|
+
if (!later && !(yield* settledInRefusedSession(store, record.job)))
|
|
116
|
+
yield* reportToDelegator(store, watchdog, mailRoom, record.job);
|
|
117
|
+
yield* Effect.try({
|
|
118
|
+
try: () => {
|
|
119
|
+
advanceCursor(store, reportConsumer, record.sequence);
|
|
120
|
+
},
|
|
121
|
+
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
122
|
+
});
|
|
123
|
+
}), { discard: true });
|
|
124
|
+
}
|
|
38
125
|
function repairWorkSettlement(store, job) {
|
|
39
126
|
const outcome = job.outcome;
|
|
40
127
|
if (outcome !== "interrupted" &&
|
|
@@ -148,7 +235,7 @@ function settleCore(store, watchdog, mailRoom, records) {
|
|
|
148
235
|
if (!(yield* settledInRefusedSession(store, record.job))) {
|
|
149
236
|
yield* repairWorkSettlement(store, record.job);
|
|
150
237
|
yield* settleTargetedControls(store, watchdog, record.job);
|
|
151
|
-
yield*
|
|
238
|
+
yield* replyToMail(store, mailRoom, record.job);
|
|
152
239
|
}
|
|
153
240
|
yield* Effect.try({
|
|
154
241
|
try: () => {
|
|
@@ -172,26 +259,34 @@ export function reconcileRuntime(input) {
|
|
|
172
259
|
try: () => readCursor(input.store, coreConsumer) ?? 0,
|
|
173
260
|
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
174
261
|
});
|
|
262
|
+
const reportCursor = yield* Effect.try({
|
|
263
|
+
try: () => readCursor(input.store, reportConsumer) ?? coreCursor,
|
|
264
|
+
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
265
|
+
});
|
|
175
266
|
const consumers = input.extensions.settledConsumers();
|
|
176
|
-
const after = Math.min(coreCursor, ...consumers.map(({ cursor }) => cursor));
|
|
267
|
+
const after = Math.min(coreCursor, reportCursor, ...consumers.map(({ cursor }) => cursor));
|
|
177
268
|
const page = yield* input.watchdog.readSettled(pageSize, after);
|
|
178
269
|
const core = yield* Effect.exit(settleCore(input.store, input.watchdog, input.mailRoom, page.filter(({ sequence }) => sequence > coreCursor)));
|
|
270
|
+
const processed = yield* Effect.try({
|
|
271
|
+
try: () => readCursor(input.store, coreConsumer) ?? 0,
|
|
272
|
+
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
273
|
+
});
|
|
179
274
|
const runs = yield* Effect.try({
|
|
180
|
-
try: () =>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.filter(({ sequence }) => sequence <= processed)
|
|
184
|
-
.map((record) => settledRun(input.store, record));
|
|
185
|
-
},
|
|
275
|
+
try: () => page
|
|
276
|
+
.filter(({ sequence }) => sequence <= processed)
|
|
277
|
+
.map((record) => settledRun(input.store, record)),
|
|
186
278
|
catch: () => runtimeFailure("Settled run facts are unavailable"),
|
|
187
279
|
});
|
|
188
|
-
yield* input.extensions.deliverSettled(runs,
|
|
280
|
+
yield* input.extensions.deliverSettled(runs, processed);
|
|
281
|
+
const reports = yield* Effect.exit(reportSettled(input.store, input.watchdog, input.mailRoom, page.filter(({ sequence }) => sequence > reportCursor && sequence <= processed)));
|
|
189
282
|
const moreTimers = yield* input.extensions.fireDueTimers(Date.now());
|
|
190
283
|
if (page.length === pageSize || moreTimers)
|
|
191
284
|
yield* input.requestWake;
|
|
192
285
|
yield* drainMail(input.mailRoom);
|
|
193
286
|
if (Exit.isFailure(core))
|
|
194
287
|
return yield* Effect.failCause(core.cause);
|
|
288
|
+
if (Exit.isFailure(reports))
|
|
289
|
+
return yield* Effect.failCause(reports.cause);
|
|
195
290
|
}).pipe(Effect.mapError((failure) => failure instanceof ShiitakeError
|
|
196
291
|
? failure
|
|
197
292
|
: runtimeFailure("Runtime reconciliation failed")));
|
|
@@ -9,6 +9,11 @@ export declare const VerifiedAgentReplySchema: Schema.Union<readonly [Schema.Str
|
|
|
9
9
|
readonly commandId: Schema.String;
|
|
10
10
|
readonly outcome: Schema.Literals<readonly ["completed", "failed", "interrupted", "cancelled", "outcome_unknown"]>;
|
|
11
11
|
readonly diagnostic: Schema.optionalKey<Schema.String>;
|
|
12
|
+
readonly waitingOnYou: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
13
|
+
readonly itemId: Schema.String;
|
|
14
|
+
readonly kind: Schema.String;
|
|
15
|
+
readonly summary: Schema.String;
|
|
16
|
+
}>>>;
|
|
12
17
|
readonly agentId: Schema.String;
|
|
13
18
|
readonly sessionId: Schema.String;
|
|
14
19
|
}>, Schema.Struct<{
|
|
@@ -42,6 +42,9 @@ function opening(reply) {
|
|
|
42
42
|
attributes.push(["outcome", reply.outcome]);
|
|
43
43
|
if (reply.outcome !== "undeliverable" && reply.diagnostic !== undefined)
|
|
44
44
|
attributes.push(["diagnostic", reply.diagnostic]);
|
|
45
|
+
if (reply.outcome !== "undeliverable" && reply.waitingOnYou !== undefined)
|
|
46
|
+
// A host fact, outside the child's text: the items the child left waiting on the recipient.
|
|
47
|
+
attributes.push(["waiting_on_you", JSON.stringify(reply.waitingOnYou)]);
|
|
45
48
|
if (reply.outcome === "undeliverable")
|
|
46
49
|
attributes.push(["reason", reply.reason]);
|
|
47
50
|
const rendered = attributes
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { messageAuthor } from "@fungi.computer/agar/agents";
|
|
2
2
|
import { readSessionAttention } from "./session-attention.js";
|
|
3
|
+
import { readWaitingOnSender } from "./work-senders.js";
|
|
4
|
+
import { readSessionDelegator } from "./session-delegator.js";
|
|
3
5
|
import { ShiitakeError } from "../../errors.js";
|
|
4
6
|
import { decodeWoodstockCursor, encodeWoodstockCursor } from "./cursor.js";
|
|
5
7
|
import { operatorClientMessage, promptCommand } from "./session-entry.js";
|
|
6
|
-
import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, readNewestPendingSessionCommand, } from "./session-store.js";
|
|
8
|
+
import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, hasSessionWorkTerminal, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, readNewestPendingSessionCommand, } from "./session-store.js";
|
|
7
9
|
const maximumPagePayloadBytes = 2_097_152;
|
|
8
10
|
const replayPageLimit = 200;
|
|
9
11
|
const maximumRowsPerPage = lineageRowLimit;
|
|
@@ -503,6 +505,10 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
|
|
|
503
505
|
const admission = readSessionCommandAdmission(owner, sessionId, commandId);
|
|
504
506
|
if (admission === undefined)
|
|
505
507
|
throw unavailable("Accepted Session command is unavailable");
|
|
508
|
+
// A live work has no settlement to project. The owner's terminal index
|
|
509
|
+
// answers this without decoding the work again on every polling tick.
|
|
510
|
+
if (!hasSessionWorkTerminal(owner, sessionId, admission.entry.workId))
|
|
511
|
+
return undefined;
|
|
506
512
|
const rows = readSessionEntriesForWork(owner, sessionId, admission.entry.workId);
|
|
507
513
|
const work = workFacts(owner, sessionId, rows);
|
|
508
514
|
const terminal = rows
|
|
@@ -521,6 +527,7 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
|
|
|
521
527
|
const sender = messageAuthor(admission.entry.meta);
|
|
522
528
|
return {
|
|
523
529
|
acceptedSender: sender?.kind === "agent" ? sender.id : null,
|
|
530
|
+
delegator: readSessionDelegator(owner, sessionId)?.replyTo[0]?.agentId ?? null,
|
|
524
531
|
terminal: meta === undefined ? terminal : { ...terminal, meta },
|
|
525
532
|
content: terminal.outcome === "cancelled"
|
|
526
533
|
? ""
|
|
@@ -534,5 +541,6 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
|
|
|
534
541
|
usage,
|
|
535
542
|
})),
|
|
536
543
|
toolCalls: new Set(rows.flatMap(({ entry }) => entry.kind === "tool_intent" ? [entry.call.id] : [])).size,
|
|
544
|
+
waitingOnYou: readWaitingOnSender(owner, sessionId, admission.entry.workId),
|
|
537
545
|
};
|
|
538
546
|
}
|
|
@@ -15,6 +15,8 @@ export * from "./extension-store.js";
|
|
|
15
15
|
export { readLatestPendingPrompt } from "./client-projection.js";
|
|
16
16
|
export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
|
|
17
17
|
export * from "./session-attention.js";
|
|
18
|
+
export * from "./work-senders.js";
|
|
19
|
+
export * from "./session-delegator.js";
|
|
18
20
|
export { deliverSessionGrants, prepareSessionGrants, readSessionGrants, type GrantsDelivery, type GrantsDeliveryResult, } from "./session-grants.js";
|
|
19
21
|
export * from "./progress.js";
|
|
20
22
|
export * from "./compaction-activity.js";
|
|
@@ -138,6 +140,8 @@ export declare function readSessionFactPage(owner: SqliteOwner, after?: string):
|
|
|
138
140
|
export declare function readColdSessionPage(owner: SqliteOwner, request: ColdSessionPageRequest): Effect.Effect<ColdSessionPage, ShiitakeError>;
|
|
139
141
|
/** Read one bounded inbox page in tier, then newest-activity order. */
|
|
140
142
|
export declare function readSessionInbox(owner: SqliteOwner, after: string | undefined): Effect.Effect<import("../../client.js").SessionSummaryPage, ShiitakeError>;
|
|
143
|
+
/** Read a Session's open attention items from the store alone. */
|
|
144
|
+
export declare function readStoredSessionAttention(owner: SqliteOwner, sessionId: string): Effect.Effect<readonly import("../../client.js").SessionAttention[], ShiitakeError>;
|
|
141
145
|
/** Read one strict-after page from Woodstock's selected lineage. */
|
|
142
146
|
export declare function readSessionReplayPage(owner: SqliteOwner, sessionId: string, after: string | undefined, delivery: "replay" | "live", activity: ObservedActivity, projectExtensions?: () => SessionExtensionSlots, includeSandboxFailures?: boolean): Effect.Effect<ClientReplayPage, ShiitakeError>;
|
|
143
147
|
/** Persist an operator-only sandbox diagnostic beside its accepted Session work. */
|
|
@@ -9,10 +9,13 @@ import { openProgress, openToolProgress } from "./progress.js";
|
|
|
9
9
|
import { encodeStoredSessionEntry, } from "./session-entry.js";
|
|
10
10
|
import { appendSessionEntry, appendCompactionTransitionOwned, appendCompactionNoticeOwned, placeCompactionTransitionOwned, acceptSessionAbort, acceptSessionCancellation, completeSessionLeaf, prepareSessionRun, prepareSessionStore, readSessionEntriesForWork, readSessionCommandAdmission, settleSessionControls, takePendingSessionControls, readSessionEntryById, selectSessionBranch, forkSessionAt, readStoredSessionSummary as readSessionSummaryOwned, readStoredSessionFactPage, createStoredSession, createStoredSessionWithinTransaction, deleteStoredSession, appendOperatorEntry, } from "./session-store.js";
|
|
11
11
|
import { readSessionInboxPage, recordSessionInboxDeletion, } from "./session-inbox.js";
|
|
12
|
+
import { readSessionAttention } from "./session-attention.js";
|
|
12
13
|
export * from "./extension-store.js";
|
|
13
14
|
export { readLatestPendingPrompt } from "./client-projection.js";
|
|
14
15
|
export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
|
|
15
16
|
export * from "./session-attention.js";
|
|
17
|
+
export * from "./work-senders.js";
|
|
18
|
+
export * from "./session-delegator.js";
|
|
16
19
|
export { deliverSessionGrants, prepareSessionGrants, readSessionGrants, } from "./session-grants.js";
|
|
17
20
|
export * from "./progress.js";
|
|
18
21
|
export * from "./compaction-activity.js";
|
|
@@ -228,6 +231,10 @@ export function readColdSessionPage(owner, request) {
|
|
|
228
231
|
export function readSessionInbox(owner, after) {
|
|
229
232
|
return storeEffect(() => readSessionInboxPage(owner, after));
|
|
230
233
|
}
|
|
234
|
+
/** Read a Session's open attention items from the store alone. */
|
|
235
|
+
export function readStoredSessionAttention(owner, sessionId) {
|
|
236
|
+
return storeEffect(() => readSessionAttention(owner, sessionId));
|
|
237
|
+
}
|
|
231
238
|
/** Read one strict-after page from Woodstock's selected lineage. */
|
|
232
239
|
export function readSessionReplayPage(owner, sessionId, after, delivery, activity, projectExtensions = () => ({}), includeSandboxFailures = false) {
|
|
233
240
|
return storeEffect(() => readClientReplayPage(owner, decodeClientSessionId(sessionId), after, delivery, activity, projectExtensions, includeSandboxFailures));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BirdDogDelegator, BirdDogMail } from "@fungi.computer/bird-dog";
|
|
2
|
+
import type { SqliteOwner } from "../contracts.js";
|
|
3
|
+
/** Woodstock keeps each Session's delegator of record beside the Session. */
|
|
4
|
+
export declare function prepareSessionDelegators(owner: SqliteOwner): void;
|
|
5
|
+
/**
|
|
6
|
+
* Record who sent a Session its work first, in the admitting transaction.
|
|
7
|
+
* Only an original mail that asks for replies names a delegator; the first one
|
|
8
|
+
* stays, so the relationship never moves once it exists.
|
|
9
|
+
*/
|
|
10
|
+
export declare function recordSessionDelegator(owner: SqliteOwner, sessionId: string, mail: BirdDogMail | undefined): void;
|
|
11
|
+
/** The Session's delegator of record, if any Agent has sent it work. */
|
|
12
|
+
export declare function readSessionDelegator(owner: SqliteOwner, sessionId: string): BirdDogDelegator | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { BirdDogAgentReferenceSchema } from "@fungi.computer/bird-dog/effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
const decodeRow = Schema.decodeUnknownSync(Schema.Struct({
|
|
4
|
+
mail_key: Schema.String,
|
|
5
|
+
agent_id: Schema.String,
|
|
6
|
+
reply_to_json: Schema.fromJsonString(Schema.Array(BirdDogAgentReferenceSchema)),
|
|
7
|
+
}));
|
|
8
|
+
const encodeReplyTo = Schema.encodeSync(Schema.fromJsonString(Schema.Array(BirdDogAgentReferenceSchema)));
|
|
9
|
+
/** Woodstock keeps each Session's delegator of record beside the Session. */
|
|
10
|
+
export function prepareSessionDelegators(owner) {
|
|
11
|
+
owner.sql.exec(`CREATE TABLE IF NOT EXISTS session_delegators (
|
|
12
|
+
session_id TEXT PRIMARY KEY,
|
|
13
|
+
mail_key TEXT NOT NULL,
|
|
14
|
+
agent_id TEXT NOT NULL,
|
|
15
|
+
reply_to_json TEXT NOT NULL
|
|
16
|
+
)`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Record who sent a Session its work first, in the admitting transaction.
|
|
20
|
+
* Only an original mail that asks for replies names a delegator; the first one
|
|
21
|
+
* stays, so the relationship never moves once it exists.
|
|
22
|
+
*/
|
|
23
|
+
export function recordSessionDelegator(owner, sessionId, mail) {
|
|
24
|
+
if (mail === undefined ||
|
|
25
|
+
mail.reply !== undefined ||
|
|
26
|
+
mail.from.kind !== "agent" ||
|
|
27
|
+
mail.replyTo.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
owner.sql.exec("INSERT OR IGNORE INTO session_delegators (session_id, mail_key, agent_id, reply_to_json) VALUES (?, ?, ?, ?)", sessionId, mail.key, mail.to.agentId, encodeReplyTo(mail.replyTo));
|
|
30
|
+
}
|
|
31
|
+
/** The Session's delegator of record, if any Agent has sent it work. */
|
|
32
|
+
export function readSessionDelegator(owner, sessionId) {
|
|
33
|
+
const [row] = owner.sql
|
|
34
|
+
.exec("SELECT mail_key, agent_id, reply_to_json FROM session_delegators WHERE session_id = ?", sessionId)
|
|
35
|
+
.toArray();
|
|
36
|
+
if (row === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
const decoded = decodeRow(row);
|
|
39
|
+
return {
|
|
40
|
+
key: decoded.mail_key,
|
|
41
|
+
agentId: decoded.agent_id,
|
|
42
|
+
replyTo: decoded.reply_to_json,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -74,6 +74,11 @@ declare const StoredSessionEntrySchema: Schema.Union<readonly [Schema.Struct<{
|
|
|
74
74
|
readonly commandId: Schema.String;
|
|
75
75
|
readonly outcome: Schema.Literals<readonly ["completed", "failed", "interrupted", "cancelled", "outcome_unknown"]>;
|
|
76
76
|
readonly diagnostic: Schema.optionalKey<Schema.String>;
|
|
77
|
+
readonly waitingOnYou: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
78
|
+
readonly itemId: Schema.String;
|
|
79
|
+
readonly kind: Schema.String;
|
|
80
|
+
readonly summary: Schema.String;
|
|
81
|
+
}>>>;
|
|
77
82
|
readonly agentId: Schema.String;
|
|
78
83
|
readonly sessionId: Schema.String;
|
|
79
84
|
}>, Schema.Struct<{
|
|
@@ -357,15 +362,20 @@ export declare function decodeStoredSessionEntry(value: string): Result.Result<{
|
|
|
357
362
|
readonly delivery?: "followUp" | "steer";
|
|
358
363
|
readonly meta?: import("../../extensions.js").Json;
|
|
359
364
|
readonly reply?: {
|
|
365
|
+
readonly agentId: string;
|
|
360
366
|
readonly sessionId: string;
|
|
361
367
|
readonly commandId: string;
|
|
362
368
|
readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
|
|
363
|
-
readonly agentId: string;
|
|
364
369
|
readonly diagnostic?: string;
|
|
370
|
+
readonly waitingOnYou?: readonly {
|
|
371
|
+
readonly itemId: string;
|
|
372
|
+
readonly kind: string;
|
|
373
|
+
readonly summary: string;
|
|
374
|
+
}[];
|
|
365
375
|
} | {
|
|
376
|
+
readonly agentId: string;
|
|
366
377
|
readonly reason: "recipient_offline" | "recipient_left";
|
|
367
378
|
readonly outcome: "undeliverable";
|
|
368
|
-
readonly agentId: string;
|
|
369
379
|
readonly sessionId?: string;
|
|
370
380
|
};
|
|
371
381
|
readonly targetWorkId?: string;
|
|
@@ -237,6 +237,8 @@ export declare function settleSessionControls(owner: SqliteOwner, input: Readonl
|
|
|
237
237
|
export declare function readSessionCommandAdmission(owner: SqliteOwner, sessionId: string, id: string): StoredCommandAdmission | undefined;
|
|
238
238
|
/** Read one stable private command admission by accepted request identity. */
|
|
239
239
|
export declare function readSessionCommandAdmissionByRequest(owner: SqliteOwner, sessionId: string, requestId: string): StoredCommandAdmission | undefined;
|
|
240
|
+
/** Probe the indexed terminal facts without materializing a live work's body. */
|
|
241
|
+
export declare function hasSessionWorkTerminal(owner: SqliteOwner, sessionId: string, workId: string): boolean;
|
|
240
242
|
/** Read entries correlated to one accepted work identity in Session order. */
|
|
241
243
|
export declare function readSessionEntriesForWork(owner: SqliteOwner, sessionId: string, workId: string): readonly StoredSessionRow[];
|
|
242
244
|
/** Read the latest retained live-retry event for one accepted work item. */
|
|
@@ -7,6 +7,8 @@ import { compactionSummaryMessage } from "../compaction/utils.js";
|
|
|
7
7
|
import { SessionTitleSchema, sessionTitleLimit } from "../client-boundary.js";
|
|
8
8
|
import { commandTimestamp, decodeStoredSessionEntry, encodeStoredSessionEntry, isCompactCommand, commandContextMessage, operatorContextMessage, promptCommand, } from "./session-entry.js";
|
|
9
9
|
import { prepareSessionAttention } from "./session-attention.js";
|
|
10
|
+
import { prepareWorkSenders } from "./work-senders.js";
|
|
11
|
+
import { prepareSessionDelegators } from "./session-delegator.js";
|
|
10
12
|
import { copySessionGrants, prepareSessionGrants } from "./session-grants.js";
|
|
11
13
|
import { prepareSessionInbox, touchSessionInbox } from "./session-inbox.js";
|
|
12
14
|
import { sessionIds } from "./identities.js";
|
|
@@ -680,6 +682,18 @@ function readSessionCommandAdmissionByWorkId(owner, sessionId, workId) {
|
|
|
680
682
|
export function readSessionCommandAdmissionByRequest(owner, sessionId, requestId) {
|
|
681
683
|
return readCommandAdmission(owner, sessionId, { requestId });
|
|
682
684
|
}
|
|
685
|
+
/** Probe the indexed terminal facts without materializing a live work's body. */
|
|
686
|
+
export function hasSessionWorkTerminal(owner, sessionId, workId) {
|
|
687
|
+
// Keep the readability check even when the work has no terminal rows.
|
|
688
|
+
readSessionRow(owner, sessionId);
|
|
689
|
+
return (owner.sql
|
|
690
|
+
.exec(`SELECT 1 AS present FROM session_entries
|
|
691
|
+
INDEXED BY idx_session_entries_work_terminal
|
|
692
|
+
WHERE session_id = ? AND work_id = ?
|
|
693
|
+
AND type IN ('run_terminal', 'work_terminal')
|
|
694
|
+
LIMIT 1`, sessionId, workId)
|
|
695
|
+
.toArray().length > 0);
|
|
696
|
+
}
|
|
683
697
|
/** Read entries correlated to one accepted work identity in Session order. */
|
|
684
698
|
export function readSessionEntriesForWork(owner, sessionId, workId) {
|
|
685
699
|
// Refuse the Session before selecting entries: a damaged work may have no
|
|
@@ -832,6 +846,7 @@ export function deleteStoredSession(owner, input) {
|
|
|
832
846
|
"DELETE FROM session_command_admissions WHERE session_id = ?",
|
|
833
847
|
// The inbox row references the Session; it goes in the same transaction.
|
|
834
848
|
"DELETE FROM session_attention WHERE session_id = ?",
|
|
849
|
+
"DELETE FROM session_work_senders WHERE session_id = ?",
|
|
835
850
|
"DELETE FROM session_tool_grants WHERE session_id = ?",
|
|
836
851
|
"DELETE FROM session_inbox WHERE session_id = ?",
|
|
837
852
|
"DELETE FROM session_creation_receipts WHERE session_id = ?",
|
|
@@ -1128,6 +1143,8 @@ export function prepareSessionStore(owner) {
|
|
|
1128
1143
|
createSessionEntryTable(owner);
|
|
1129
1144
|
prepareSessionDocuments(owner);
|
|
1130
1145
|
prepareSessionAttention(owner);
|
|
1146
|
+
prepareWorkSenders(owner);
|
|
1147
|
+
prepareSessionDelegators(owner);
|
|
1131
1148
|
prepareSessionGrants(owner);
|
|
1132
1149
|
prepareSessionInbox(owner);
|
|
1133
1150
|
owner.sql.exec(`
|
|
@@ -1143,6 +1160,9 @@ export function prepareSessionStore(owner) {
|
|
|
1143
1160
|
ON session_entries(session_id, request_id)
|
|
1144
1161
|
WHERE request_id IS NOT NULL`);
|
|
1145
1162
|
owner.sql.exec("CREATE INDEX IF NOT EXISTS idx_session_entries_work ON session_entries(work_id, sequence)");
|
|
1163
|
+
owner.sql.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_work_terminal
|
|
1164
|
+
ON session_entries(session_id, work_id)
|
|
1165
|
+
WHERE type IN ('run_terminal', 'work_terminal')`);
|
|
1146
1166
|
// Context and preference reads look up these rare older facts directly
|
|
1147
1167
|
// rather than walking the history behind them.
|
|
1148
1168
|
owner.sql.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_model_preference
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type AttentionRecipient } from "../../session-attention.js";
|
|
2
|
+
import type { SqliteOwner } from "../contracts.js";
|
|
3
|
+
/** One open item of a Session that waits on the Agent that sent its work. */
|
|
4
|
+
export type WaitingOnYou = Readonly<{
|
|
5
|
+
itemId: string;
|
|
6
|
+
kind: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* Woodstock records which Agent Session sent each accepted run, beside the
|
|
11
|
+
* run's admission. Attention that an extension raises during a run waits on
|
|
12
|
+
* this sender first; a run no Agent sent has no row.
|
|
13
|
+
*/
|
|
14
|
+
export declare function prepareWorkSenders(owner: SqliteOwner): void;
|
|
15
|
+
/** Record the sender of one accepted run; a replay writes the same row. */
|
|
16
|
+
export declare function recordWorkSender(owner: SqliteOwner, sessionId: string, workId: string, sender: AttentionRecipient): void;
|
|
17
|
+
/** The Agent Session that sent this run, or undefined when none did. */
|
|
18
|
+
export declare function readWorkSender(owner: SqliteOwner, sessionId: string, workId: string): AttentionRecipient | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The Session's open attention that waits on the sender of this run, newest
|
|
21
|
+
* first as stored, bounded. Escalated items wait on someone else and are
|
|
22
|
+
* absent; a run no Agent sent names none.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readWaitingOnSender(owner: SqliteOwner, sessionId: string, workId: string): readonly WaitingOnYou[];
|
|
25
|
+
/**
|
|
26
|
+
* The Session's open attention that waits on any of these Agent Sessions,
|
|
27
|
+
* newest first as stored, bounded: what a reply or report to them names.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readWaitingOn(owner: SqliteOwner, sessionId: string, recipients: readonly Readonly<{
|
|
30
|
+
agentId: string;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
}>[]): readonly WaitingOnYou[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { AttentionRecipientSchema, } from "../../session-attention.js";
|
|
3
|
+
import { readSessionAttention } from "./session-attention.js";
|
|
4
|
+
/** The most open items one settlement names as waiting on its sender. */
|
|
5
|
+
const maximumWaitingOnYou = 8;
|
|
6
|
+
const decodeRow = Schema.decodeUnknownSync(Schema.Struct({
|
|
7
|
+
agent_id: Schema.String,
|
|
8
|
+
sender_session_id: Schema.String,
|
|
9
|
+
name: Schema.String,
|
|
10
|
+
}));
|
|
11
|
+
const decodeRecipient = Schema.decodeUnknownSync(AttentionRecipientSchema);
|
|
12
|
+
/**
|
|
13
|
+
* Woodstock records which Agent Session sent each accepted run, beside the
|
|
14
|
+
* run's admission. Attention that an extension raises during a run waits on
|
|
15
|
+
* this sender first; a run no Agent sent has no row.
|
|
16
|
+
*/
|
|
17
|
+
export function prepareWorkSenders(owner) {
|
|
18
|
+
owner.sql.exec(`CREATE TABLE IF NOT EXISTS session_work_senders (
|
|
19
|
+
session_id TEXT NOT NULL,
|
|
20
|
+
work_id TEXT NOT NULL,
|
|
21
|
+
agent_id TEXT NOT NULL,
|
|
22
|
+
sender_session_id TEXT NOT NULL,
|
|
23
|
+
name TEXT NOT NULL,
|
|
24
|
+
PRIMARY KEY (session_id, work_id)
|
|
25
|
+
)`);
|
|
26
|
+
}
|
|
27
|
+
/** Record the sender of one accepted run; a replay writes the same row. */
|
|
28
|
+
export function recordWorkSender(owner, sessionId, workId, sender) {
|
|
29
|
+
owner.sql.exec(`INSERT OR IGNORE INTO session_work_senders
|
|
30
|
+
(session_id, work_id, agent_id, sender_session_id, name)
|
|
31
|
+
VALUES (?, ?, ?, ?, ?)`, sessionId, workId, sender.agentId, sender.sessionId, sender.name);
|
|
32
|
+
}
|
|
33
|
+
/** The Agent Session that sent this run, or undefined when none did. */
|
|
34
|
+
export function readWorkSender(owner, sessionId, workId) {
|
|
35
|
+
const raw = owner.sql
|
|
36
|
+
.exec("SELECT agent_id, sender_session_id, name FROM session_work_senders WHERE session_id = ? AND work_id = ?", sessionId, workId)
|
|
37
|
+
.toArray()[0];
|
|
38
|
+
if (raw === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
const row = decodeRow(raw);
|
|
41
|
+
return decodeRecipient({
|
|
42
|
+
agentId: row.agent_id,
|
|
43
|
+
sessionId: row.sender_session_id,
|
|
44
|
+
name: row.name,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The Session's open attention that waits on the sender of this run, newest
|
|
49
|
+
* first as stored, bounded. Escalated items wait on someone else and are
|
|
50
|
+
* absent; a run no Agent sent names none.
|
|
51
|
+
*/
|
|
52
|
+
export function readWaitingOnSender(owner, sessionId, workId) {
|
|
53
|
+
const sender = readWorkSender(owner, sessionId, workId);
|
|
54
|
+
return sender === undefined ? [] : readWaitingOn(owner, sessionId, [sender]);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The Session's open attention that waits on any of these Agent Sessions,
|
|
58
|
+
* newest first as stored, bounded: what a reply or report to them names.
|
|
59
|
+
*/
|
|
60
|
+
export function readWaitingOn(owner, sessionId, recipients) {
|
|
61
|
+
return readSessionAttention(owner, sessionId)
|
|
62
|
+
.filter((item) => recipients.some((recipient) => item.waitingOn?.agentId === recipient.agentId &&
|
|
63
|
+
item.waitingOn.sessionId === recipient.sessionId))
|
|
64
|
+
.slice(0, maximumWaitingOnYou)
|
|
65
|
+
.map((item) => ({
|
|
66
|
+
itemId: item.id,
|
|
67
|
+
kind: item.kind,
|
|
68
|
+
summary: item.summary,
|
|
69
|
+
}));
|
|
70
|
+
}
|
package/dist/remote-client.js
CHANGED
|
@@ -72,7 +72,12 @@ function remoteEvents(url, clientClosed, sources, eventSource) {
|
|
|
72
72
|
closed: false,
|
|
73
73
|
};
|
|
74
74
|
const emit = (kind, event) => {
|
|
75
|
-
Queue.offerUnsafe(queue, { kind, event })
|
|
75
|
+
if (!Queue.offerUnsafe(queue, { kind, event })) {
|
|
76
|
+
// A dropped frame breaks this watch's ordering. Fail instead
|
|
77
|
+
// of delivering an incomplete history to the client.
|
|
78
|
+
Queue.failCauseUnsafe(queue, Cause.fail(unavailable("Session stream client fell behind")));
|
|
79
|
+
Effect.runSync(closeRemoteSource(active, sources));
|
|
80
|
+
}
|
|
76
81
|
};
|
|
77
82
|
active.source.addEventListener("snapshot", (event) => {
|
|
78
83
|
emit("snapshot", event);
|
|
@@ -91,7 +96,7 @@ function remoteEvents(url, clientClosed, sources, eventSource) {
|
|
|
91
96
|
return active;
|
|
92
97
|
},
|
|
93
98
|
catch: () => unavailable("Session stream could not open"),
|
|
94
|
-
}), (active) => closeRemoteSource(active, sources))).pipe(Stream.mapEffect(decodeRemoteEvent), Stream.interruptWhen(clientClosed.await));
|
|
99
|
+
}), (active) => closeRemoteSource(active, sources)), { bufferSize: 256, strategy: "dropping" }).pipe(Stream.mapEffect(decodeRemoteEvent), Stream.interruptWhen(clientClosed.await));
|
|
95
100
|
}
|
|
96
101
|
function sessionUrl(baseUrl, sessionId) {
|
|
97
102
|
return `${baseUrl}/sessions/${encodeURIComponent(sessionId)}`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SessionSnapshot, SessionWatch } from "./client.js";
|
|
2
|
+
export type VisitorStreamError = "channel_visitor_stream_expiry_failed" | "channel_visitor_stream_close_failed" | "channel_visitor_stream_heartbeat_failed" | "channel_visitor_stream_failed";
|
|
3
|
+
export declare function visitorStream(options: Readonly<{
|
|
4
|
+
watch: SessionWatch;
|
|
5
|
+
visitorId: string;
|
|
6
|
+
filter: (snapshot: SessionSnapshot) => SessionSnapshot;
|
|
7
|
+
expiresAt: number;
|
|
8
|
+
now: () => number;
|
|
9
|
+
release: () => void;
|
|
10
|
+
canRead: () => Promise<boolean>;
|
|
11
|
+
read: () => Promise<SessionSnapshot>;
|
|
12
|
+
onError: (event: VisitorStreamError, cause: unknown) => void;
|
|
13
|
+
}>): ReadableStream<Uint8Array>;
|