@fungi.computer/shiitake 0.1.0 → 0.3.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/README.md +155 -34
- package/dist/agent-control.d.ts +78 -11
- package/dist/agent-control.js +273 -70
- package/dist/client-entry.d.ts +15 -8
- package/dist/client-entry.js +7 -5
- package/dist/client-wire.d.ts +24 -8
- package/dist/client-wire.js +53 -11
- package/dist/client.d.ts +56 -11
- package/dist/client.js +1 -1
- package/dist/cloudflare.d.ts +42 -23
- package/dist/cloudflare.js +76 -54
- package/dist/cold-sessions.d.ts +28 -0
- package/dist/cold-sessions.js +1 -0
- package/dist/delegation.d.ts +214 -0
- package/dist/delegation.js +319 -0
- package/dist/effect.d.ts +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -2
- package/dist/extension-contract.d.ts +10 -2
- package/dist/extensions.d.ts +6 -2
- package/dist/fleet.d.ts +52 -34
- package/dist/fleet.js +101 -63
- package/dist/hosted.js +1 -0
- package/dist/http-routes.d.ts +106 -22
- package/dist/http-routes.js +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/internal/accepted-run-executor.d.ts +5 -0
- package/dist/internal/accepted-run-executor.js +10 -4
- package/dist/internal/admission-door.d.ts +3 -0
- package/dist/internal/admission-door.js +12 -2
- package/dist/internal/client-boundary.js +7 -1
- package/dist/internal/compaction/compaction-slot.d.ts +36 -0
- package/dist/internal/compaction/compaction-slot.js +41 -0
- package/dist/internal/compaction/compaction.d.ts +7 -4
- package/dist/internal/compaction/compaction.js +59 -30
- package/dist/internal/compaction/session-compaction.d.ts +8 -16
- package/dist/internal/compaction/session-compaction.js +358 -119
- package/dist/internal/compaction/settings.js +5 -0
- package/dist/internal/compaction/utils.d.ts +19 -16
- package/dist/internal/compaction/utils.js +30 -84
- package/dist/internal/contracts.d.ts +40 -13
- package/dist/internal/coordinator.js +28 -10
- package/dist/internal/effect-client.d.ts +4 -2
- package/dist/internal/effect-client.js +29 -0
- package/dist/internal/effect-contracts.d.ts +17 -4
- package/dist/internal/extension-runtime.d.ts +5 -0
- package/dist/internal/extension-runtime.js +184 -89
- package/dist/internal/host-meta.d.ts +7 -0
- package/dist/internal/model-acquisition.js +7 -1
- package/dist/internal/model-preference.d.ts +2 -0
- package/dist/internal/model-preference.js +2 -1
- package/dist/internal/promise-facade.js +4 -16
- package/dist/internal/run-failure-reason.d.ts +10 -0
- package/dist/internal/run-failure-reason.js +19 -0
- package/dist/internal/run-scope.d.ts +4 -1
- package/dist/internal/run-scope.js +38 -5
- package/dist/internal/runtime-coordinator.d.ts +2 -1
- package/dist/internal/runtime-coordinator.js +65 -4
- package/dist/internal/runtime-core.js +1 -0
- package/dist/internal/runtime-reconciliation.js +42 -6
- package/dist/internal/session-context.d.ts +1 -2
- package/dist/internal/session-context.js +8 -30
- package/dist/internal/session-status.d.ts +2 -1
- package/dist/internal/session-status.js +4 -1
- package/dist/internal/woodstock/client-projection.d.ts +6 -5
- package/dist/internal/woodstock/client-projection.js +79 -53
- package/dist/internal/woodstock/cold-sessions.d.ts +4 -0
- package/dist/internal/woodstock/cold-sessions.js +90 -0
- package/dist/internal/woodstock/extension-store.d.ts +2 -0
- package/dist/internal/woodstock/extension-store.js +4 -0
- package/dist/internal/woodstock/fleet-store.d.ts +3 -3
- package/dist/internal/woodstock/fleet-store.js +16 -11
- package/dist/internal/woodstock/index.d.ts +11 -3
- package/dist/internal/woodstock/index.js +33 -14
- package/dist/internal/woodstock/progress.d.ts +14 -1
- package/dist/internal/woodstock/progress.js +97 -19
- package/dist/internal/woodstock/run-recovery.js +6 -4
- package/dist/internal/woodstock/session-attention.d.ts +1 -1
- package/dist/internal/woodstock/session-attention.js +2 -2
- package/dist/internal/woodstock/session-entry.d.ts +109 -42
- package/dist/internal/woodstock/session-entry.js +63 -20
- package/dist/internal/woodstock/session-inbox.js +2 -0
- package/dist/internal/woodstock/session-store.d.ts +104 -24
- package/dist/internal/woodstock/session-store.js +937 -246
- package/dist/internal/woodstock/storage-admission.d.ts +8 -8
- package/dist/internal/woodstock/storage-admission.js +8 -34
- package/dist/internal/woodstock/storage-conformance.d.ts +15 -0
- package/dist/internal/woodstock/storage-conformance.js +489 -0
- package/dist/model-preference.d.ts +2 -2
- package/dist/model-preference.js +2 -2
- package/dist/node.js +1 -1
- package/dist/remote-client.js +8 -1
- package/dist/session-attention.d.ts +8 -1
- package/dist/session-attention.js +6 -1
- package/dist/testing.js +1 -0
- package/docs/api/README.md +18 -16
- package/docs/api/agent-control.md +172 -2
- package/docs/api/client-entry.md +26 -10
- package/docs/api/client.md +153 -37
- package/docs/api/cloudflare.md +76 -23
- package/docs/api/delegation.md +485 -0
- package/docs/api/effect.md +20 -2
- package/docs/api/extensions.md +21 -9
- package/docs/api/fleet.md +149 -122
- package/docs/api/http.md +4 -1
- package/docs/api/index.md +116 -20
- package/docs/api/internal/woodstock/storage-conformance.md +81 -0
- package/docs/api/model-preference.md +17 -0
- package/docs/api/testing.md +16 -4
- package/package.json +26 -14
- package/dist/internal/browser-sessions.d.ts +0 -31
- package/dist/internal/browser-sessions.js +0 -173
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { isContextOverflow, } from "@fungi.computer/mule/effect";
|
|
2
2
|
import { isRecoverableLength } from "@earendil-works/pi-ai/utils/overflow";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Fiber from "effect/Fiber";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
4
6
|
import { ShiitakeError } from "../../errors.js";
|
|
5
7
|
import { borrowedView, compact, prepareCompaction, } from "./compaction.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
+
import { claimSlot, releaseSlot, slotTaken, takeSummary, } from "./compaction-slot.js";
|
|
9
|
+
import { estimateContextTokens, estimateMessageTokens, } from "@earendil-works/pi-ai/utils/estimate";
|
|
10
|
+
import { compactionSummaryMessage, shouldCompact, shouldCompactInBackground, } from "./utils.js";
|
|
11
|
+
import { appendCompactionNotice, appendCompactionTransition, beginCompactionActivity, buildSessionContext, CompactionReceiptSchema, compactionHeadState, endCompactionActivity, withdrawnWorkIds, contextMessageIds, commandContextMessage, operatorContextMessage, placeCompactionTransition, promptCommand, readSessionContextPath, } from "../woodstock/index.js";
|
|
8
12
|
function runtimeFailure(message) {
|
|
9
13
|
return new ShiitakeError("runtime", "rejected", message);
|
|
10
14
|
}
|
|
11
15
|
const commitFailures = {
|
|
12
16
|
handoff: "The compaction handoff failed; the conversation context is unchanged",
|
|
17
|
+
handoffResult: "The compaction handoff failed: it returned an invalid result; the conversation context is unchanged",
|
|
13
18
|
conflict: "The Session changed during compaction; the conversation context is unchanged",
|
|
14
19
|
};
|
|
15
20
|
/**
|
|
@@ -18,9 +23,11 @@ const commitFailures = {
|
|
|
18
23
|
*/
|
|
19
24
|
export class CompactionCommitError extends ShiitakeError {
|
|
20
25
|
phase;
|
|
21
|
-
|
|
26
|
+
usage;
|
|
27
|
+
constructor(phase, usage) {
|
|
22
28
|
super(phase === "conflict" ? "store" : "runtime", phase === "conflict" ? "conflict" : "rejected", commitFailures[phase]);
|
|
23
29
|
this.phase = phase;
|
|
30
|
+
this.usage = usage;
|
|
24
31
|
}
|
|
25
32
|
get diagnostic() {
|
|
26
33
|
return commitFailures[this.phase];
|
|
@@ -57,14 +64,13 @@ function sourceEntries(path, activeWorkId) {
|
|
|
57
64
|
const compaction = {
|
|
58
65
|
id: row.id,
|
|
59
66
|
kind: "compaction",
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
usage: row.entry.usage,
|
|
67
|
+
head: row.entry.head,
|
|
68
|
+
model: row.entry.model[0],
|
|
69
|
+
usage: row.entry.data.usage,
|
|
64
70
|
};
|
|
65
|
-
entries.push(row.entry.details === undefined
|
|
71
|
+
entries.push(row.entry.data.details === undefined
|
|
66
72
|
? compaction
|
|
67
|
-
: { ...compaction, details: row.entry.details });
|
|
73
|
+
: { ...compaction, details: row.entry.data.details });
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
76
|
return entries;
|
|
@@ -73,36 +79,9 @@ function sourceEntries(path, activeWorkId) {
|
|
|
73
79
|
function readPath(store, sessionId, workId) {
|
|
74
80
|
return Effect.try({
|
|
75
81
|
try: () => readSessionContextPath(store, { sessionId, workId }),
|
|
76
|
-
catch: (
|
|
77
|
-
? error
|
|
78
|
-
: runtimeFailure("Woodstock selected lineage is unavailable"),
|
|
82
|
+
catch: () => runtimeFailure("Woodstock selected lineage is unavailable"),
|
|
79
83
|
});
|
|
80
84
|
}
|
|
81
|
-
/**
|
|
82
|
-
* The compaction owner's context estimate: Pi's usage-plus-trailing count,
|
|
83
|
-
* excluding provider usage that belongs before Woodstock's latest boundary.
|
|
84
|
-
*
|
|
85
|
-
* A provider's reported usage measures the context it actually read, so it
|
|
86
|
-
* is authoritative only while nothing has reshaped that context since. Once
|
|
87
|
-
* a compaction lands, every kept message's own usage figure is stale: it
|
|
88
|
-
* priced the pre-compaction history, not the summary that replaced it. The
|
|
89
|
-
* one safe fallback is a fresh chars/4 estimate of every message still in
|
|
90
|
-
* view, the same method `tokensAfterCompaction` uses when it records the
|
|
91
|
-
* marker's own `tokensAfter` at commit time; trailing-after-usage alone
|
|
92
|
-
* would undercount (even to zero, when the kept tail's own reply is the
|
|
93
|
-
* newest message and nothing followed it).
|
|
94
|
-
*/
|
|
95
|
-
export function estimateThresholdContextTokens(path, messages) {
|
|
96
|
-
const boundary = path.reduce((latest, row) => (row.entry.kind === "compaction" ? row.timestamp : latest), undefined);
|
|
97
|
-
const estimate = estimateContextTokens(messages);
|
|
98
|
-
const usageIndex = estimate.lastUsageIndex;
|
|
99
|
-
const usageMessage = usageIndex === null ? undefined : messages[usageIndex];
|
|
100
|
-
if (boundary === undefined ||
|
|
101
|
-
usageMessage?.role !== "assistant" ||
|
|
102
|
-
usageMessage.timestamp > boundary)
|
|
103
|
-
return estimate.tokens;
|
|
104
|
-
return messages.reduce((sum, message) => sum + estimateTokens(message), 0);
|
|
105
|
-
}
|
|
106
85
|
/** The context the model reads once a compaction row becomes the boundary. */
|
|
107
86
|
function tokensAfterCompaction(path, expected, entry, workId) {
|
|
108
87
|
const boundary = {
|
|
@@ -113,7 +92,7 @@ function tokensAfterCompaction(path, expected, entry, workId) {
|
|
|
113
92
|
timestamp: Date.now(),
|
|
114
93
|
entry,
|
|
115
94
|
};
|
|
116
|
-
return buildSessionContext([...path, boundary], workId).reduce((sum, message) => sum +
|
|
95
|
+
return buildSessionContext([...path, boundary], workId).reduce((sum, message) => sum + estimateMessageTokens(message), 0);
|
|
117
96
|
}
|
|
118
97
|
/** One exact preparation, handoff, CAS append, and authoritative rebuild. */
|
|
119
98
|
function commitCompaction(input, path, request) {
|
|
@@ -134,48 +113,104 @@ function commitCompactionEffect(input, path, request) {
|
|
|
134
113
|
if (expected === undefined)
|
|
135
114
|
return undefined;
|
|
136
115
|
const result = yield* compact(preparation, input.model, input.stream, request.instructions);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
116
|
+
const receipt = yield* handOff(input, preparation, result, expected.id);
|
|
117
|
+
return yield* appendSummary(input, path, result, request, expected.id, receipt);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* What a handoff may answer. A JS extension's result arrives untyped, so it is
|
|
122
|
+
* decoded here: an `accepted` receipt must satisfy the entry's own receipt
|
|
123
|
+
* rule, so an invalid one stops the compaction before anything is written.
|
|
124
|
+
*/
|
|
125
|
+
const HandoffResultSchema = Schema.Union([
|
|
126
|
+
Schema.Struct({
|
|
127
|
+
type: Schema.Literal("accepted"),
|
|
128
|
+
receipt: CompactionReceiptSchema,
|
|
129
|
+
}),
|
|
130
|
+
Schema.Struct({ type: Schema.Literal("discarded_by_policy") }),
|
|
131
|
+
]);
|
|
132
|
+
const decodeHandoffResult = Schema.decodeUnknownResult(HandoffResultSchema);
|
|
133
|
+
/**
|
|
134
|
+
* Zookeeper's handoff with the exact arrays, before the destructive append.
|
|
135
|
+
* Returns the archive receipt to store in the entry that commits, or
|
|
136
|
+
* `undefined` when no handoff is installed or it discarded by policy.
|
|
137
|
+
*/
|
|
138
|
+
function handOff(input, preparation, result, contextRevision) {
|
|
139
|
+
if (input.handoff === undefined)
|
|
140
|
+
return Effect.succeed(undefined);
|
|
141
|
+
return input.handoff
|
|
142
|
+
.consume(borrowedView(preparation, result, input.sessionId, contextRevision))
|
|
143
|
+
.pipe(Effect.mapError(() => new CompactionCommitError("handoff", result.usage)), Effect.flatMap((handed) => {
|
|
144
|
+
const decoded = decodeHandoffResult(handed);
|
|
145
|
+
if (decoded._tag === "Failure")
|
|
146
|
+
return Effect.fail(new CompactionCommitError("handoffResult", result.usage));
|
|
147
|
+
return Effect.succeed(decoded.success.type === "accepted"
|
|
148
|
+
? decoded.success.receipt
|
|
149
|
+
: undefined);
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The row a compaction appends on the leaf `path` ends at, with its measured
|
|
154
|
+
* `tokensAfter`. `contextRevision` names the last row the summary saw: that leaf
|
|
155
|
+
* for a blocking commit, the selection tail for a background one.
|
|
156
|
+
*/
|
|
157
|
+
function compactionRow(input, path, result, request, contextRevision, receipt) {
|
|
158
|
+
const expected = path.at(-1);
|
|
159
|
+
if (expected === undefined)
|
|
160
|
+
return undefined;
|
|
161
|
+
const tokensAfter = tokensAfterCompaction(path, expected, {
|
|
162
|
+
kind: "compaction",
|
|
163
|
+
workId: input.workId,
|
|
164
|
+
head: result.firstKeptEntryId,
|
|
165
|
+
model: [compactionSummaryMessage(result.summary, Date.now())],
|
|
166
|
+
data: {
|
|
167
|
+
reason: request.trigger,
|
|
143
168
|
resumesRun: request.resumesRun,
|
|
144
|
-
trigger: request.trigger,
|
|
145
|
-
workId: input.workId,
|
|
146
|
-
firstKeptEntryId: result.firstKeptEntryId,
|
|
147
|
-
summary: result.summary,
|
|
148
169
|
tokensBefore: result.tokensBefore,
|
|
170
|
+
// This probe row only measures the context; the figure it computes
|
|
171
|
+
// is what the appended row records.
|
|
172
|
+
tokensAfter: 0,
|
|
149
173
|
usage: result.usage,
|
|
150
|
-
contextRevision
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
contextRevision,
|
|
175
|
+
},
|
|
176
|
+
}, input.workId);
|
|
177
|
+
const row = {
|
|
178
|
+
sessionId: input.sessionId,
|
|
179
|
+
resumesRun: request.resumesRun,
|
|
180
|
+
trigger: request.trigger,
|
|
181
|
+
tokensAfter,
|
|
182
|
+
expectedLeafId: expected.id,
|
|
183
|
+
workId: input.workId,
|
|
184
|
+
head: result.firstKeptEntryId,
|
|
185
|
+
summary: result.summary,
|
|
186
|
+
tokensBefore: result.tokensBefore,
|
|
187
|
+
usage: result.usage,
|
|
188
|
+
contextRevision,
|
|
189
|
+
details: result.details,
|
|
190
|
+
};
|
|
191
|
+
return receipt === undefined ? row : { ...row, receipt };
|
|
192
|
+
}
|
|
193
|
+
/** The context the run continues from once `committed` ends `path`. */
|
|
194
|
+
function rebuilt(input, path, committed) {
|
|
195
|
+
const committedPath = [...path, committed];
|
|
196
|
+
return {
|
|
197
|
+
path: committedPath,
|
|
198
|
+
messages: buildSessionContext(committedPath, input.workId),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** CAS-append a blocking compaction on the leaf `path` ends at and rebuild. */
|
|
202
|
+
function appendSummary(input, path, result, request, contextRevision, receipt) {
|
|
203
|
+
const row = compactionRow(input, path, result, request, contextRevision, receipt);
|
|
204
|
+
if (row === undefined)
|
|
205
|
+
return Effect.succeed(undefined);
|
|
206
|
+
return Effect.try({
|
|
207
|
+
try: () => appendCompactionTransition(input.store, row),
|
|
208
|
+
catch: (error) => error instanceof ShiitakeError
|
|
209
|
+
? error.cause === "conflict"
|
|
210
|
+
? new CompactionCommitError("conflict")
|
|
211
|
+
: error
|
|
212
|
+
: runtimeFailure("Woodstock compaction append failed"),
|
|
213
|
+
}).pipe(Effect.map((committed) => rebuilt(input, path, committed)));
|
|
179
214
|
}
|
|
180
215
|
/** A failure's display-safe reason: owner-closed text or the model's message. */
|
|
181
216
|
function failureDiagnostic(error) {
|
|
@@ -185,23 +220,31 @@ function failureDiagnostic(error) {
|
|
|
185
220
|
return error.message;
|
|
186
221
|
return `Compaction failed (${error.boundary}: ${error.cause})`;
|
|
187
222
|
}
|
|
188
|
-
/**
|
|
189
|
-
function
|
|
223
|
+
/** Append one compaction notice, so clients can show what committed nothing. */
|
|
224
|
+
function writeNotice(input, notice) {
|
|
190
225
|
return Effect.try({
|
|
191
226
|
try: () => {
|
|
192
|
-
appendCompactionNotice(input.store,
|
|
193
|
-
sessionId: input.sessionId,
|
|
194
|
-
workId: input.workId,
|
|
195
|
-
trigger,
|
|
196
|
-
result: failure === undefined ? "nothing_to_compact" : "failed",
|
|
197
|
-
diagnostic: failure === undefined ? undefined : failureDiagnostic(failure),
|
|
198
|
-
});
|
|
227
|
+
appendCompactionNotice(input.store, notice);
|
|
199
228
|
},
|
|
200
229
|
catch: (error) => error instanceof ShiitakeError
|
|
201
230
|
? error
|
|
202
231
|
: runtimeFailure("Compaction notice could not be recorded"),
|
|
203
232
|
});
|
|
204
233
|
}
|
|
234
|
+
/** Record a compaction that found nothing to compact, or failed. */
|
|
235
|
+
function recordNotice(input, trigger, failure) {
|
|
236
|
+
const common = { sessionId: input.sessionId, workId: input.workId, trigger };
|
|
237
|
+
return writeNotice(input, failure === undefined
|
|
238
|
+
? { ...common, result: "nothing_to_compact" }
|
|
239
|
+
: {
|
|
240
|
+
...common,
|
|
241
|
+
result: "failed",
|
|
242
|
+
diagnostic: failureDiagnostic(failure),
|
|
243
|
+
usage: failure instanceof CompactionCommitError
|
|
244
|
+
? failure.usage
|
|
245
|
+
: undefined,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
205
248
|
/**
|
|
206
249
|
* Run one automatic compaction. A failure is recorded as a notice and the
|
|
207
250
|
* caller keeps its current context; automatic compaction never fails a run.
|
|
@@ -211,15 +254,177 @@ function autoCompaction(input, path, request) {
|
|
|
211
254
|
? Effect.succeed(result.success)
|
|
212
255
|
: recordNotice(input, request.trigger, result.failure).pipe(Effect.as(undefined))));
|
|
213
256
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
const thresholdRequest = {
|
|
258
|
+
trigger: "threshold",
|
|
259
|
+
resumesRun: false,
|
|
260
|
+
};
|
|
261
|
+
/**
|
|
262
|
+
* Start a background compaction when the context crossed the background
|
|
263
|
+
* threshold (tokens only), the owner's slot is free and this run has not
|
|
264
|
+
* stopped trying. `readContext` is the one context read, taken at fork time;
|
|
265
|
+
* the fiber keeps only the preparation and hands it back with its summary.
|
|
266
|
+
*/
|
|
267
|
+
function startBackground(input, background, contextTokens, readContext) {
|
|
268
|
+
if (background.stopped ||
|
|
269
|
+
slotTaken(input.store) ||
|
|
270
|
+
!shouldCompactInBackground(contextTokens, input.model.contextWindow, input.settings))
|
|
271
|
+
return Effect.void;
|
|
272
|
+
return readContext.pipe(Effect.flatMap((path) => forkBackground(input, background.scope, path)), Effect.map((slot) => {
|
|
273
|
+
if (slot === undefined)
|
|
274
|
+
background.stopped = true;
|
|
275
|
+
else
|
|
276
|
+
background.slot = slot;
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Claim the owner's slot and fork the summary into the run's scope:
|
|
281
|
+
* interrupting the run interrupts it, aborts the provider call and frees the
|
|
282
|
+
* slot (a deliberate Shiitake difference: Pi's background compaction
|
|
283
|
+
* outlives an abort). Nothing to compact forks nothing and stays silent, as
|
|
284
|
+
* automatic compaction does.
|
|
285
|
+
*/
|
|
286
|
+
function forkBackground(input, scope, path) {
|
|
287
|
+
const tail = path.at(-1);
|
|
288
|
+
const preparation = prepareCompaction(sourceEntries(path, input.workId), input.settings);
|
|
289
|
+
if (tail === undefined || preparation === undefined)
|
|
290
|
+
return Effect.succeed(undefined);
|
|
291
|
+
return claimSlot(input.store, scope, (handBack) => summarize(input, preparation, tail.id, handBack));
|
|
292
|
+
}
|
|
293
|
+
/** The fiber's work: the summary call, handed back through the slot. */
|
|
294
|
+
function summarize(input, preparation, selectionTail, handBack) {
|
|
295
|
+
return Effect.suspend(() => {
|
|
296
|
+
beginCompactionActivity(input.store, input.sessionId, "threshold");
|
|
297
|
+
return compact(preparation, input.model, input.stream).pipe(Effect.map((result) => {
|
|
298
|
+
handBack({ preparation, result, selectionTail });
|
|
299
|
+
}), Effect.ensuring(Effect.sync(() => {
|
|
300
|
+
endCompactionActivity(input.store, input.sessionId);
|
|
301
|
+
})));
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Settle this run's background compaction and place it: at a between-turn
|
|
306
|
+
* point only once it has finished (`join: false`), or by waiting for it
|
|
307
|
+
* when the blocking threshold, an overflow or the end of the run arrives.
|
|
308
|
+
* Joining instead of starting a second compaction is Shiitake's
|
|
309
|
+
* single-flight policy; Pi does not join.
|
|
310
|
+
* Undefined while it still runs, or when it settled failed or stale; a
|
|
311
|
+
* summary or handoff failure becomes the ordinary `failed` notice. Background
|
|
312
|
+
* notices (`failed`, `stale`) always record trigger `threshold`, the start that
|
|
313
|
+
* made them; a placed entry takes `request`, standing in for the compaction
|
|
314
|
+
* the run would have made at that point.
|
|
315
|
+
*/
|
|
316
|
+
function settleBackground(input, background, request, join) {
|
|
317
|
+
return Effect.suspend(() => {
|
|
318
|
+
const slot = background.slot;
|
|
319
|
+
const fiber = slot?.fiber;
|
|
320
|
+
if (slot === undefined ||
|
|
321
|
+
fiber === undefined ||
|
|
322
|
+
(!join && fiber.pollUnsafe() === undefined))
|
|
323
|
+
return Effect.succeed(undefined);
|
|
324
|
+
background.slot = undefined;
|
|
325
|
+
return Effect.result(Fiber.join(fiber).pipe(Effect.flatMap(() => handOffSummary(input, slot)), Effect.flatMap((handedOff) => handedOff === undefined
|
|
326
|
+
? Effect.succeed(undefined)
|
|
327
|
+
: appendPlaced(input, handedOff, request)))).pipe(Effect.flatMap((placed) => {
|
|
328
|
+
if (placed._tag === "Success" && placed.success !== undefined)
|
|
329
|
+
return Effect.succeed(placed.success);
|
|
330
|
+
background.stopped = true;
|
|
331
|
+
return placed._tag === "Success"
|
|
332
|
+
? Effect.succeed(undefined)
|
|
333
|
+
: recordNotice(input, "threshold", placed.failure).pipe(Effect.as(undefined));
|
|
334
|
+
}), Effect.ensuring(Effect.sync(() => {
|
|
335
|
+
releaseSlot(input.store, slot);
|
|
336
|
+
})));
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Record a finished summary dropped unplaced: no entry, only its spend.
|
|
341
|
+
* `stale`: its head moved. `insufficient`: placed, the context would still be
|
|
342
|
+
* over the limit.
|
|
343
|
+
*/
|
|
344
|
+
function recordDropped(input, result, reason) {
|
|
345
|
+
return writeNotice(input, {
|
|
346
|
+
sessionId: input.sessionId,
|
|
347
|
+
workId: input.workId,
|
|
220
348
|
trigger: "threshold",
|
|
221
|
-
|
|
222
|
-
|
|
349
|
+
result: reason,
|
|
350
|
+
usage: result.usage,
|
|
351
|
+
}).pipe(Effect.as(undefined));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Placement steps 1-3 (design §3): take the summary out of the slot, check
|
|
355
|
+
* its head, hand the fork-time arrays to Zookeeper, then release the slot.
|
|
356
|
+
* Only the result and the selection tail come back, so the summarized prefix
|
|
357
|
+
* is unreachable before placement reads the context again. A stale summary
|
|
358
|
+
* never reaches the handoff.
|
|
359
|
+
*
|
|
360
|
+
* A summary handed off here can still fail to commit (stale inside the
|
|
361
|
+
* append, insufficient, or a CAS conflict). Its accepted archive is then
|
|
362
|
+
* unreferenced and Z5 sweeps it, at most one per run: see the "view" steps
|
|
363
|
+
* of wiki/3-resources/shiitake-v0.0.1/zookeeper-compaction-handoff.md.
|
|
364
|
+
*/
|
|
365
|
+
function handOffSummary(input, slot) {
|
|
366
|
+
const summary = takeSummary(slot);
|
|
367
|
+
if (summary === undefined)
|
|
368
|
+
return Effect.succeed(undefined);
|
|
369
|
+
const { result, selectionTail } = summary;
|
|
370
|
+
return Effect.try({
|
|
371
|
+
try: () => compactionHeadState(input.store, {
|
|
372
|
+
sessionId: input.sessionId,
|
|
373
|
+
head: result.firstKeptEntryId,
|
|
374
|
+
}),
|
|
375
|
+
catch: (error) => error instanceof ShiitakeError
|
|
376
|
+
? error
|
|
377
|
+
: runtimeFailure("Woodstock compaction head is unavailable"),
|
|
378
|
+
}).pipe(Effect.flatMap((head) => head === "stale"
|
|
379
|
+
? recordDropped(input, result, "stale")
|
|
380
|
+
: handOff(input, summary.preparation, result, selectionTail).pipe(Effect.map((receipt) => ({ result, selectionTail, receipt })))), Effect.ensuring(Effect.sync(() => {
|
|
381
|
+
releaseSlot(input.store, slot);
|
|
382
|
+
})));
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Placement steps 4-5: one fresh read, then the append on the leaf as it is
|
|
386
|
+
* now, head-checked again inside the append transaction because the handoff
|
|
387
|
+
* ran in between. Rows added after the selection tail stay verbatim after
|
|
388
|
+
* the head; a head that moved meanwhile drops the summary as stale.
|
|
389
|
+
*
|
|
390
|
+
* Decided before appending (fix 3, option A): when the placed context would
|
|
391
|
+
* still be over the limit, the summary is not placed. It is recorded as
|
|
392
|
+
* `insufficient` and the caller's ordinary path (the blocking compaction, or the
|
|
393
|
+
* overflow recovery) runs on a fresh read. Placing it would leave a
|
|
394
|
+
* compaction at the tip, where Pi's preparation finds nothing to compact
|
|
395
|
+
* (compaction.ts:282), so that ordinary path could never help.
|
|
396
|
+
*/
|
|
397
|
+
function appendPlaced(input, handedOff, request) {
|
|
398
|
+
return readPath(input.store, input.sessionId, input.workId).pipe(Effect.flatMap((path) => {
|
|
399
|
+
const row = compactionRow(input, path, handedOff.result, request, handedOff.selectionTail, handedOff.receipt);
|
|
400
|
+
if (row === undefined)
|
|
401
|
+
return Effect.succeed(undefined);
|
|
402
|
+
if (shouldCompact(row.tokensAfter, input.model.contextWindow, input.settings))
|
|
403
|
+
return recordDropped(input, handedOff.result, "insufficient");
|
|
404
|
+
return Effect.try({
|
|
405
|
+
try: () => placeCompactionTransition(input.store, row),
|
|
406
|
+
catch: (error) => error instanceof ShiitakeError
|
|
407
|
+
? error.cause === "conflict"
|
|
408
|
+
? new CompactionCommitError("conflict")
|
|
409
|
+
: error
|
|
410
|
+
: runtimeFailure("Woodstock compaction append failed"),
|
|
411
|
+
}).pipe(Effect.flatMap((placed) => placed === "stale"
|
|
412
|
+
? recordDropped(input, handedOff.result, "stale")
|
|
413
|
+
: Effect.succeed(rebuilt(input, path, placed))));
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Apply the pre-run threshold check, preserving unchanged input on failure.
|
|
418
|
+
* Below it, the claimed context path may start a background compaction.
|
|
419
|
+
*/
|
|
420
|
+
function preRunMessages(input, background) {
|
|
421
|
+
const unchanged = { path: input.path, messages: input.messages };
|
|
422
|
+
if (input.resuming === true)
|
|
423
|
+
return Effect.succeed(unchanged);
|
|
424
|
+
const tokens = estimateContextTokens(input.messages).tokens;
|
|
425
|
+
if (!shouldCompact(tokens, input.model.contextWindow, input.settings))
|
|
426
|
+
return startBackground(input, background, tokens, Effect.succeed(input.path)).pipe(Effect.as(unchanged));
|
|
427
|
+
return autoCompaction(input, input.path, thresholdRequest).pipe(Effect.map((compacted) => compacted ?? unchanged));
|
|
223
428
|
}
|
|
224
429
|
function classifyRecovery(outcome, model) {
|
|
225
430
|
const overflow = isContextOverflow(outcome.message, model.contextWindow);
|
|
@@ -240,51 +445,74 @@ function removeFailedAssistant(messages) {
|
|
|
240
445
|
function postRunPath(input, working, outcome, recovery) {
|
|
241
446
|
if (!input.settings.enabled)
|
|
242
447
|
return Effect.succeed(undefined);
|
|
243
|
-
const threshold = shouldCompact(
|
|
448
|
+
const threshold = shouldCompact(estimateContextTokens([
|
|
244
449
|
...working.messages,
|
|
245
450
|
...outcome.messages.slice(working.newMessageCount),
|
|
246
|
-
]), input.model.contextWindow, input.settings);
|
|
451
|
+
]).tokens, input.model.contextWindow, input.settings);
|
|
247
452
|
return recovery.triggered || threshold
|
|
248
453
|
? readPath(input.store, input.sessionId, input.workId)
|
|
249
454
|
: Effect.succeed(undefined);
|
|
250
455
|
}
|
|
251
456
|
/**
|
|
252
|
-
* Pi's between-turn
|
|
253
|
-
*
|
|
254
|
-
*
|
|
457
|
+
* Pi's between-turn point, where Mule is paused and the run is the only
|
|
458
|
+
* writer. A finished background summary is placed here; past the blocking
|
|
459
|
+
* threshold a running one is joined, and only if that placed nothing does
|
|
460
|
+
* the blocking compaction run. Below it, a background compaction may start.
|
|
255
461
|
*/
|
|
256
|
-
function betweenTurns(input, working) {
|
|
462
|
+
function betweenTurns(input, working, background) {
|
|
257
463
|
return (context, newMessageCount) => Effect.gen(function* () {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
464
|
+
const replace = (compacted) => {
|
|
465
|
+
working.path = compacted.path;
|
|
466
|
+
working.messages = compacted.messages;
|
|
467
|
+
working.newMessageCount = newMessageCount;
|
|
468
|
+
return compacted.messages;
|
|
469
|
+
};
|
|
470
|
+
const tokens = estimateContextTokens(context).tokens;
|
|
471
|
+
const blocking = shouldCompact(tokens, input.model.contextWindow, input.settings);
|
|
472
|
+
const placed = yield* settleBackground(input, background, thresholdRequest, blocking);
|
|
473
|
+
if (placed !== undefined)
|
|
474
|
+
return replace(placed);
|
|
475
|
+
const readContext = readPath(input.store, input.sessionId, input.workId);
|
|
476
|
+
if (!blocking) {
|
|
477
|
+
yield* startBackground(input, background, tokens, readContext);
|
|
266
478
|
return undefined;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return compacted.messages;
|
|
479
|
+
}
|
|
480
|
+
const compacted = yield* autoCompaction(input, yield* readContext, thresholdRequest);
|
|
481
|
+
return compacted === undefined ? undefined : replace(compacted);
|
|
271
482
|
});
|
|
272
483
|
}
|
|
273
484
|
/** Run terminal outcomes through post-threshold and one-time recovery policy. */
|
|
274
|
-
function postRunOutcome(input, initial) {
|
|
485
|
+
function postRunOutcome(input, initial, background) {
|
|
275
486
|
return Effect.gen(function* () {
|
|
276
487
|
let current = initial;
|
|
277
488
|
let recoveryAttempted = initial.path.some(({ entry }) => entry.kind === "compaction" &&
|
|
278
489
|
entry.workId === input.workId &&
|
|
279
|
-
entry.resumesRun
|
|
490
|
+
entry.data.resumesRun);
|
|
280
491
|
for (;;) {
|
|
281
492
|
const working = {
|
|
282
493
|
path: current.path,
|
|
283
494
|
messages: current.messages,
|
|
284
495
|
newMessageCount: 0,
|
|
285
496
|
};
|
|
286
|
-
const outcome = yield* input.run(working.messages, betweenTurns(input, working));
|
|
497
|
+
const outcome = yield* input.run(working.messages, betweenTurns(input, working, background));
|
|
287
498
|
const recovery = classifyRecovery(outcome, input.model);
|
|
499
|
+
const retry = recovery.retry && !recoveryAttempted;
|
|
500
|
+
// A summary still running when the run ends is waited for and placed
|
|
501
|
+
// first; it then stands in for the post-run compaction.
|
|
502
|
+
const placed = yield* settleBackground(input, background, {
|
|
503
|
+
trigger: recovery.overflow ? "overflow" : "threshold",
|
|
504
|
+
resumesRun: retry,
|
|
505
|
+
}, true);
|
|
506
|
+
if (placed !== undefined) {
|
|
507
|
+
if (!retry)
|
|
508
|
+
return outcome;
|
|
509
|
+
current = {
|
|
510
|
+
path: placed.path,
|
|
511
|
+
messages: removeFailedAssistant(placed.messages),
|
|
512
|
+
};
|
|
513
|
+
recoveryAttempted = true;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
288
516
|
const path = yield* postRunPath(input, working, outcome, recovery);
|
|
289
517
|
if (path === undefined)
|
|
290
518
|
return outcome;
|
|
@@ -304,11 +532,22 @@ function postRunOutcome(input, initial) {
|
|
|
304
532
|
}
|
|
305
533
|
});
|
|
306
534
|
}
|
|
307
|
-
/**
|
|
535
|
+
/**
|
|
536
|
+
* Compose the retained pre-run, between-turn and post-run compaction phases.
|
|
537
|
+
* A background compaction is a child fiber in the run's own scope.
|
|
538
|
+
*/
|
|
308
539
|
export function runWithCompaction(input) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
540
|
+
return Effect.gen(function* () {
|
|
541
|
+
const background = {
|
|
542
|
+
scope: yield* Effect.scope,
|
|
543
|
+
slot: undefined,
|
|
544
|
+
stopped: false,
|
|
545
|
+
};
|
|
546
|
+
const initial = input.settings.enabled
|
|
547
|
+
? yield* preRunMessages(input, background)
|
|
548
|
+
: input;
|
|
549
|
+
return yield* postRunOutcome(input, initial, background);
|
|
550
|
+
});
|
|
312
551
|
}
|
|
313
552
|
/**
|
|
314
553
|
* Compact one Session now, as accepted work: the same commit the automatic
|
|
@@ -5,11 +5,13 @@ const tokens = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)), S
|
|
|
5
5
|
const BudgetSchema = Schema.Struct({
|
|
6
6
|
reserveTokens: Schema.optionalKey(tokens),
|
|
7
7
|
keepRecentTokens: Schema.optionalKey(tokens),
|
|
8
|
+
backgroundTokens: Schema.optionalKey(tokens),
|
|
8
9
|
});
|
|
9
10
|
const CompactionOptionsSchema = Schema.Struct({
|
|
10
11
|
enabled: Schema.optionalKey(Schema.Boolean),
|
|
11
12
|
reserveTokens: Schema.optionalKey(tokens),
|
|
12
13
|
keepRecentTokens: Schema.optionalKey(tokens),
|
|
14
|
+
backgroundTokens: Schema.optionalKey(tokens),
|
|
13
15
|
modelOverrides: Schema.optionalKey(Schema.Record(Schema.String, BudgetSchema)),
|
|
14
16
|
});
|
|
15
17
|
const decodeOptions = Schema.decodeUnknownResult(CompactionOptionsSchema, {
|
|
@@ -35,6 +37,9 @@ export function openCompactionSettings(options) {
|
|
|
35
37
|
keepRecentTokens: override?.keepRecentTokens ??
|
|
36
38
|
settings.keepRecentTokens ??
|
|
37
39
|
DEFAULT_COMPACTION_SETTINGS.keepRecentTokens,
|
|
40
|
+
backgroundTokens: override?.backgroundTokens ??
|
|
41
|
+
settings.backgroundTokens ??
|
|
42
|
+
DEFAULT_COMPACTION_SETTINGS.backgroundTokens,
|
|
38
43
|
};
|
|
39
44
|
};
|
|
40
45
|
}
|