@fungi.computer/shiitake 0.2.0 → 0.3.1
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 +77 -5
- package/dist/agent-control.js +271 -65
- 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 +43 -9
- package/dist/fleet.js +88 -25
- package/dist/hosted.js +1 -0
- package/dist/http-routes.d.ts +113 -29
- 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 +11 -8
- package/dist/internal/compaction/compaction.js +62 -34
- package/dist/internal/compaction/session-compaction.d.ts +8 -16
- package/dist/internal/compaction/session-compaction.js +358 -119
- package/dist/internal/compaction/settings.d.ts +1 -1
- 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 +4 -2
- 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 +5 -1
- package/dist/internal/woodstock/fleet-store.js +17 -5
- 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 +110 -43
- 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.d.ts +1 -1
- package/dist/testing.js +1 -0
- package/docs/api/README.md +18 -16
- package/docs/api/agent-control.md +171 -1
- 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 +159 -104
- 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 +27 -10
- 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
|
@@ -2,9 +2,9 @@ import * as Effect from "effect/Effect";
|
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
3
|
import { checkHostMeta } from "./host-meta.js";
|
|
4
4
|
import { ShiitakeError } from "../errors.js";
|
|
5
|
-
import { decodeClientAcceptedCommand, decodeClientSessionId, requireDecoded, } from "./client-boundary.js";
|
|
5
|
+
import { decodeClientAcceptedCommand, decodeClientSessionId, decodeClientSessionCreationRequestId, requireDecoded, } from "./client-boundary.js";
|
|
6
6
|
import { publishOwnerObservation } from "./observation-kernel.js";
|
|
7
|
-
import { AdmissionOriginSchema, acceptSessionCommandOwned, readSessionCommandAdmissionByRequest, hasStoredSession, VerifiedAgentReplySchema, } from "./woodstock/index.js";
|
|
7
|
+
import { AdmissionOriginSchema, acceptSessionCommandOwned, readSessionCommandAdmissionByRequest, withdrawSessionRequestOwned, assertSessionRequestNotWithdrawn, hasStoredSession, VerifiedAgentReplySchema, } from "./woodstock/index.js";
|
|
8
8
|
const decodeOrigin = Schema.decodeUnknownResult(AdmissionOriginSchema, {
|
|
9
9
|
onExcessProperty: "error",
|
|
10
10
|
});
|
|
@@ -42,6 +42,7 @@ export function openAdmissionDoor(input) {
|
|
|
42
42
|
const admitDecoded = (request, touched) => {
|
|
43
43
|
const { sessionId, origin } = request;
|
|
44
44
|
const command = request.command;
|
|
45
|
+
assertSessionRequestNotWithdrawn(input.owner, sessionId, command.requestId);
|
|
45
46
|
if (request.requireExistingSession &&
|
|
46
47
|
!hasStoredSession(input.owner, sessionId))
|
|
47
48
|
throw new ShiitakeError("contract", "closed", "Session target is unavailable");
|
|
@@ -95,6 +96,15 @@ export function openAdmissionDoor(input) {
|
|
|
95
96
|
const transact = (body, boundary = "request") => run((admit) => body((request) => admit(decode(request))), boundary);
|
|
96
97
|
return Object.freeze({
|
|
97
98
|
transact,
|
|
99
|
+
withdraw: (sessionId, requestId) => Effect.try({
|
|
100
|
+
try: () => ({
|
|
101
|
+
sessionId: decodeClientSessionId(sessionId),
|
|
102
|
+
requestId: decodeClientSessionCreationRequestId({ requestId }),
|
|
103
|
+
}),
|
|
104
|
+
catch: (error) => error instanceof ShiitakeError
|
|
105
|
+
? error
|
|
106
|
+
: new ShiitakeError("contract", "invalid", "Withdrawal request is invalid"),
|
|
107
|
+
}).pipe(Effect.flatMap((decoded) => run(() => withdrawSessionRequestOwned(input.owner, decoded.sessionId, decoded.requestId), "request"))),
|
|
98
108
|
admit: (request, boundary) => Effect.try({
|
|
99
109
|
try: () => decode(request),
|
|
100
110
|
catch: (error) => error instanceof ShiitakeError
|
|
@@ -5,7 +5,13 @@ const encoder = new TextEncoder();
|
|
|
5
5
|
function boundedText(maximumBytes) {
|
|
6
6
|
return Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maximumBytes), Schema.makeFilter((value) => encoder.encode(value).byteLength <= maximumBytes));
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/** A lone UTF-16 surrogate; SQLite's UTF-8 text columns cannot store one. */
|
|
9
|
+
const loneSurrogate = /\p{Cs}/u;
|
|
10
|
+
/**
|
|
11
|
+
* Session and request ids are indexed SQLite text, so they must be well-formed
|
|
12
|
+
* Unicode: a lone surrogate would be stored as U+FFFD and never match again.
|
|
13
|
+
*/
|
|
14
|
+
const boundedId = boundedText(256).check(Schema.makeFilter((value) => !loneSurrogate.test(value)));
|
|
9
15
|
/**
|
|
10
16
|
* The one prompt-text grammar. Human, mail, host and extension prompts are all
|
|
11
17
|
* admitted through the accepted-command door under this bound. Text is
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as Fiber from "effect/Fiber";
|
|
3
|
+
import * as Scope from "effect/Scope";
|
|
4
|
+
import type { SqliteOwner } from "../contracts.js";
|
|
5
|
+
/**
|
|
6
|
+
* An owner's one compaction slot (background compaction design §5). From
|
|
7
|
+
* claim until release it holds the only extra decoded copy of a
|
|
8
|
+
* conversation the owner may have: the summary fiber while it runs, then the
|
|
9
|
+
* summary it hands back. Every other trigger joins it instead of starting a
|
|
10
|
+
* second one. Process memory only: a crash loses it, unwritten.
|
|
11
|
+
*
|
|
12
|
+
* Deliberate Shiitake difference, not Pi parity: Pi Durable's background
|
|
13
|
+
* compaction is a durable task that survives a Conversation abort and
|
|
14
|
+
* resumes after a crash, and Pi does not join triggers into one. Ours is
|
|
15
|
+
* scoped to the run and single-flight; that is what bounds an owner to one
|
|
16
|
+
* extra copy of one conversation.
|
|
17
|
+
*/
|
|
18
|
+
export type CompactionSlot<A, E> = {
|
|
19
|
+
fiber: Fiber.Fiber<void, E> | undefined;
|
|
20
|
+
summary: A | undefined;
|
|
21
|
+
};
|
|
22
|
+
/** Whether a compaction already holds this owner's slot. */
|
|
23
|
+
export declare function slotTaken(owner: SqliteOwner): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Claim the owner's slot and fork `work` into `scope`, which interrupts it
|
|
26
|
+
* when the run ends. The release finalizer is registered before the fiber
|
|
27
|
+
* or the slot entry exist, and the entry is written in the same synchronous
|
|
28
|
+
* step that receives the fiber, so an interrupt anywhere leaves no entry.
|
|
29
|
+
* `work` hands its summary back through the slot, never through the fiber's
|
|
30
|
+
* exit, so releasing the slot makes the summary unreachable.
|
|
31
|
+
*/
|
|
32
|
+
export declare function claimSlot<A, E>(owner: SqliteOwner, scope: Scope.Scope, work: (handBack: (summary: A) => void) => Effect.Effect<void, E>): Effect.Effect<CompactionSlot<A, E>>;
|
|
33
|
+
/** Take the finished summary out of the slot, which no longer reaches it. */
|
|
34
|
+
export declare function takeSummary<A, E>(slot: CompactionSlot<A, E>): A | undefined;
|
|
35
|
+
/** Drop every reference the slot holds and free it, however it settled. */
|
|
36
|
+
export declare function releaseSlot<A, E>(owner: SqliteOwner, slot: CompactionSlot<A, E>): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Scope from "effect/Scope";
|
|
3
|
+
const slots = new WeakMap();
|
|
4
|
+
/** Whether a compaction already holds this owner's slot. */
|
|
5
|
+
export function slotTaken(owner) {
|
|
6
|
+
return slots.has(owner);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Claim the owner's slot and fork `work` into `scope`, which interrupts it
|
|
10
|
+
* when the run ends. The release finalizer is registered before the fiber
|
|
11
|
+
* or the slot entry exist, and the entry is written in the same synchronous
|
|
12
|
+
* step that receives the fiber, so an interrupt anywhere leaves no entry.
|
|
13
|
+
* `work` hands its summary back through the slot, never through the fiber's
|
|
14
|
+
* exit, so releasing the slot makes the summary unreachable.
|
|
15
|
+
*/
|
|
16
|
+
export function claimSlot(owner, scope, work) {
|
|
17
|
+
const slot = { fiber: undefined, summary: undefined };
|
|
18
|
+
const task = work((summary) => {
|
|
19
|
+
slot.summary = summary;
|
|
20
|
+
});
|
|
21
|
+
return Scope.addFinalizer(scope, Effect.sync(() => {
|
|
22
|
+
releaseSlot(owner, slot);
|
|
23
|
+
})).pipe(Effect.andThen(Effect.forkIn(task, scope)), Effect.map((fiber) => {
|
|
24
|
+
slot.fiber = fiber;
|
|
25
|
+
slots.set(owner, slot);
|
|
26
|
+
return slot;
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
/** Take the finished summary out of the slot, which no longer reaches it. */
|
|
30
|
+
export function takeSummary(slot) {
|
|
31
|
+
const summary = slot.summary;
|
|
32
|
+
slot.summary = undefined;
|
|
33
|
+
return summary;
|
|
34
|
+
}
|
|
35
|
+
/** Drop every reference the slot holds and free it, however it settled. */
|
|
36
|
+
export function releaseSlot(owner, slot) {
|
|
37
|
+
slot.fiber = undefined;
|
|
38
|
+
slot.summary = undefined;
|
|
39
|
+
if (slots.get(owner) === slot)
|
|
40
|
+
slots.delete(owner);
|
|
41
|
+
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Pi v0.84.4 semantic compaction, adapted only at Woodstock's entry boundary.
|
|
3
3
|
* The retained preparation/result vocabulary is intentionally visible here;
|
|
4
4
|
* Shiitake supplies native Message values and its selected model stream.
|
|
5
|
+
* Range selection follows Pi Durable spec §8.7 (`earendil-works/pi` at
|
|
6
|
+
* 8efd314f) and token estimates are pi-ai's own.
|
|
5
7
|
*/
|
|
6
|
-
import type { Api, Model, Usage } from "@
|
|
8
|
+
import type { Api, Model, Usage } from "@fungi.computer/mule/effect";
|
|
7
9
|
import type { MuleStreamFunction } from "@fungi.computer/mule/effect";
|
|
8
10
|
import * as Effect from "effect/Effect";
|
|
9
11
|
import type { BorrowedCompactionViewV0 } from "../../extensions.js";
|
|
@@ -12,13 +14,14 @@ import { createFileOps, type CompactionSettings } from "./utils.js";
|
|
|
12
14
|
export type CompactionSourceEntry = Readonly<{
|
|
13
15
|
id: string;
|
|
14
16
|
kind: "message";
|
|
15
|
-
message: import("@
|
|
17
|
+
message: import("@fungi.computer/mule/effect").Message;
|
|
16
18
|
}> | Readonly<{
|
|
17
19
|
id: string;
|
|
18
20
|
kind: "compaction";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
/** Pi's entry head: the first entry the summary keeps. */
|
|
22
|
+
head: string;
|
|
23
|
+
/** Pi's entry model message: the summary the model reads. */
|
|
24
|
+
model: import("@fungi.computer/mule/effect").UserMessage;
|
|
22
25
|
usage: Usage;
|
|
23
26
|
details?: Readonly<{
|
|
24
27
|
readFiles: readonly string[];
|
|
@@ -27,8 +30,8 @@ export type CompactionSourceEntry = Readonly<{
|
|
|
27
30
|
}>;
|
|
28
31
|
export type CompactionPreparation = Readonly<{
|
|
29
32
|
firstKeptEntryId: string;
|
|
30
|
-
messagesToSummarize: readonly import("@
|
|
31
|
-
turnPrefixMessages: readonly import("@
|
|
33
|
+
messagesToSummarize: readonly import("@fungi.computer/mule/effect").Message[];
|
|
34
|
+
turnPrefixMessages: readonly import("@fungi.computer/mule/effect").Message[];
|
|
32
35
|
isSplitTurn: boolean;
|
|
33
36
|
tokensBefore: number;
|
|
34
37
|
previousSummary?: string;
|
|
@@ -51,4 +54,4 @@ export declare function prepareCompaction(entries: readonly CompactionSourceEntr
|
|
|
51
54
|
export declare function compact(preparation: CompactionPreparation, model: Model<Api>, streamFn?: MuleStreamFunction,
|
|
52
55
|
/** A manual compaction's focus, appended to the history summary prompt. */
|
|
53
56
|
instructions?: string): Effect.Effect<CompactionResult, ShiitakeError>;
|
|
54
|
-
export declare function borrowedView(preparation: CompactionPreparation, result: CompactionResult, contextRevision: string): BorrowedCompactionViewV0;
|
|
57
|
+
export declare function borrowedView(preparation: CompactionPreparation, result: CompactionResult, sessionId: string, contextRevision: string): BorrowedCompactionViewV0;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
2
|
+
import { streamModel } from "@fungi.computer/mule/effect";
|
|
1
3
|
// Leaf helpers, not Pi's root entry, which also evaluates its validators.
|
|
4
|
+
import { estimateContextTokens, estimateMessageTokens, } from "@earendil-works/pi-ai/utils/estimate";
|
|
2
5
|
import { retryAssistantCall } from "@earendil-works/pi-ai/utils/retry";
|
|
3
6
|
import { contentText } from "@earendil-works/pi-ai/utils/text";
|
|
4
|
-
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
5
7
|
import { uuidv7 } from "@earendil-works/pi-ai/utils/uuid";
|
|
6
8
|
import * as Effect from "effect/Effect";
|
|
7
9
|
import { ShiitakeError } from "../../errors.js";
|
|
8
|
-
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, serializeConversation, SUMMARIZATION_PROMPT, SUMMARIZATION_SYSTEM_PROMPT, TURN_PREFIX_SUMMARIZATION_PROMPT, UPDATE_SUMMARIZATION_PROMPT,
|
|
10
|
+
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, serializeConversation, SUMMARIZATION_PROMPT, SUMMARIZATION_SYSTEM_PROMPT, TURN_PREFIX_SUMMARIZATION_PROMPT, UPDATE_SUMMARIZATION_PROMPT, compactionSummaryText, summaryFailure, } from "./utils.js";
|
|
9
11
|
/** A summary is a model call: its bounded provider message is shown. */
|
|
10
12
|
function failure(message) {
|
|
11
13
|
return new ShiitakeError("model", "rejected", message);
|
|
@@ -41,14 +43,11 @@ function contextMessages(entries) {
|
|
|
41
43
|
const latestCompaction = latest;
|
|
42
44
|
const keptIndex = latestCompaction === undefined
|
|
43
45
|
? 0
|
|
44
|
-
: Math.max(0, entries.findIndex(({ id }) => id === latestCompaction.
|
|
46
|
+
: Math.max(0, entries.findIndex(({ id }) => id === latestCompaction.head));
|
|
45
47
|
const messages = entries.flatMap((entry, index) => entry.kind === "message" && index >= keptIndex ? [entry.message] : []);
|
|
46
48
|
if (latestCompaction === undefined)
|
|
47
49
|
return messages;
|
|
48
|
-
return [
|
|
49
|
-
compactionSummaryMessage(latestCompaction.summary, latestCompaction.timestamp),
|
|
50
|
-
...messages,
|
|
51
|
-
];
|
|
50
|
+
return [latestCompaction.model, ...messages];
|
|
52
51
|
}
|
|
53
52
|
function extractFileOperations(messages, entries, previousIndex) {
|
|
54
53
|
const fileOps = createFileOps();
|
|
@@ -63,35 +62,65 @@ function extractFileOperations(messages, entries, previousIndex) {
|
|
|
63
62
|
extractFileOpsFromMessage(message, fileOps);
|
|
64
63
|
return fileOps;
|
|
65
64
|
}
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Whether a result for one of `calls` follows `from` before the next assistant
|
|
67
|
+
* message, so a user message there sits between a tool call and its result.
|
|
68
|
+
*/
|
|
69
|
+
function resultFollows(entries, from, endIndex, calls) {
|
|
70
|
+
for (let index = from; index < endIndex; index += 1) {
|
|
71
|
+
const entry = entries[index];
|
|
72
|
+
if (entry?.kind !== "message")
|
|
73
|
+
continue;
|
|
74
|
+
if (entry.message.role === "assistant")
|
|
75
|
+
return false;
|
|
76
|
+
if (entry.message.role === "toolResult" &&
|
|
77
|
+
calls.has(entry.message.toolCallId))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Pi Durable §8.7 rule 1: cut candidates are user and assistant messages.
|
|
84
|
+
* Tool results and system messages never are, so a kept assistant message
|
|
85
|
+
* keeps its tool results; a user message is not one while a result for a call
|
|
86
|
+
* of the assistant before it still follows it.
|
|
87
|
+
*/
|
|
88
|
+
function cutPoints(entries, startIndex, endIndex) {
|
|
89
|
+
const points = [];
|
|
90
|
+
let openCalls = new Set();
|
|
91
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
92
|
+
const entry = entries[index];
|
|
93
|
+
if (entry?.kind !== "message")
|
|
94
|
+
continue;
|
|
95
|
+
const message = entry.message;
|
|
96
|
+
if (message.role === "assistant") {
|
|
97
|
+
points.push(index);
|
|
98
|
+
openCalls = new Set(message.content.flatMap((block) => block.type === "toolCall" ? [block.id] : []));
|
|
99
|
+
}
|
|
100
|
+
else if (message.role === "user" &&
|
|
101
|
+
!resultFollows(entries, index + 1, endIndex, openCalls))
|
|
102
|
+
points.push(index);
|
|
103
|
+
}
|
|
104
|
+
return points;
|
|
68
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Pi Durable §8.7 rules 2-3: walk newest to oldest adding each message's
|
|
108
|
+
* estimate; where the sum reaches `keepRecentTokens`, cut at the first
|
|
109
|
+
* candidate at or after that entry, or the newest candidate when none
|
|
110
|
+
* follows. Nothing is compacted while the sum never reaches it.
|
|
111
|
+
*/
|
|
69
112
|
function selectCutIndex(entries, startIndex, endIndex, keepRecentTokens) {
|
|
70
|
-
const points = entries
|
|
71
|
-
index >= startIndex &&
|
|
72
|
-
index < endIndex &&
|
|
73
|
-
isCutPoint(entry.message)
|
|
74
|
-
? [index]
|
|
75
|
-
: []);
|
|
76
|
-
const firstPoint = points[0];
|
|
77
|
-
if (firstPoint === undefined)
|
|
78
|
-
return undefined;
|
|
113
|
+
const points = cutPoints(entries, startIndex, endIndex);
|
|
79
114
|
let accumulated = 0;
|
|
80
|
-
let cut = firstPoint;
|
|
81
115
|
for (let index = endIndex - 1; index >= startIndex; index -= 1) {
|
|
82
116
|
const entry = entries[index];
|
|
83
117
|
if (entry?.kind !== "message")
|
|
84
118
|
continue;
|
|
85
|
-
accumulated +=
|
|
86
|
-
if (accumulated >= keepRecentTokens)
|
|
87
|
-
|
|
88
|
-
// their tool call (the last cut point), never fall back to the start.
|
|
89
|
-
cut =
|
|
90
|
-
points.find((candidate) => candidate >= index) ?? points.at(-1) ?? cut;
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
119
|
+
accumulated += estimateMessageTokens(entry.message);
|
|
120
|
+
if (accumulated >= keepRecentTokens)
|
|
121
|
+
return points.find((candidate) => candidate >= index) ?? points.at(-1);
|
|
93
122
|
}
|
|
94
|
-
return
|
|
123
|
+
return undefined;
|
|
95
124
|
}
|
|
96
125
|
function classifyCut(entries, startIndex, cut) {
|
|
97
126
|
const cutEntry = entries[cut];
|
|
@@ -132,9 +161,9 @@ export function prepareCompaction(entries, settings) {
|
|
|
132
161
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
133
162
|
const entry = entries[index];
|
|
134
163
|
if (entry?.kind === "compaction") {
|
|
135
|
-
previousSummary = entry.
|
|
164
|
+
previousSummary = compactionSummaryText(entry.model);
|
|
136
165
|
previousIndex = index;
|
|
137
|
-
const kept = entries.findIndex((candidate) => candidate.id === entry.
|
|
166
|
+
const kept = entries.findIndex((candidate) => candidate.id === entry.head);
|
|
138
167
|
boundaryStart = kept >= 0 ? kept : index + 1;
|
|
139
168
|
break;
|
|
140
169
|
}
|
|
@@ -182,9 +211,7 @@ function completeSummarization(model, context, options, streamFn) {
|
|
|
182
211
|
cacheRetention: "none",
|
|
183
212
|
sessionId: options.sessionId ?? uuidv7(),
|
|
184
213
|
};
|
|
185
|
-
return retryAssistantCall(() => streamFn
|
|
186
|
-
? Promise.resolve(streamFn(model, normalizeContext(context), requestOptions)).then((stream) => stream.result())
|
|
187
|
-
: import("@earendil-works/pi-ai/compat").then(({ completeSimple }) => completeSimple(model, context, requestOptions)), RETRY_POLICY, requestOptions.signal);
|
|
214
|
+
return retryAssistantCall(() => Promise.resolve((streamFn ?? streamModel)(model, normalizeContext(context), requestOptions)).then((stream) => stream.result()), RETRY_POLICY, requestOptions.signal);
|
|
188
215
|
}
|
|
189
216
|
function buildContext(promptText) {
|
|
190
217
|
return {
|
|
@@ -281,8 +308,9 @@ instructions) {
|
|
|
281
308
|
catch: (error) => failure(error instanceof Error ? error.message : String(error)),
|
|
282
309
|
});
|
|
283
310
|
}
|
|
284
|
-
export function borrowedView(preparation, result, contextRevision) {
|
|
311
|
+
export function borrowedView(preparation, result, sessionId, contextRevision) {
|
|
285
312
|
return {
|
|
313
|
+
sessionId,
|
|
286
314
|
messagesToSummarize: preparation.messagesToSummarize,
|
|
287
315
|
turnPrefixMessages: preparation.turnPrefixMessages,
|
|
288
316
|
result,
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { type Message, type Model, type Api, type MuleRunOutcome, type MuleStreamFunction } from "@fungi.computer/mule/effect";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
+
import type * as Scope from "effect/Scope";
|
|
3
4
|
import type { CompactionHandoffV0 } from "../../extensions.js";
|
|
4
5
|
import { ShiitakeError } from "../../errors.js";
|
|
6
|
+
import { type CompactionResult } from "./compaction.js";
|
|
5
7
|
import { type CompactionSettings } from "./utils.js";
|
|
6
8
|
import { type StoredSessionRow } from "../woodstock/index.js";
|
|
7
9
|
import type { SqliteOwner } from "../contracts.js";
|
|
8
10
|
declare const commitFailures: {
|
|
9
11
|
readonly handoff: "The compaction handoff failed; the conversation context is unchanged";
|
|
12
|
+
readonly handoffResult: "The compaction handoff failed: it returned an invalid result; the conversation context is unchanged";
|
|
10
13
|
readonly conflict: "The Session changed during compaction; the conversation context is unchanged";
|
|
11
14
|
};
|
|
12
15
|
/**
|
|
@@ -15,7 +18,8 @@ declare const commitFailures: {
|
|
|
15
18
|
*/
|
|
16
19
|
export declare class CompactionCommitError extends ShiitakeError {
|
|
17
20
|
readonly phase: keyof typeof commitFailures;
|
|
18
|
-
|
|
21
|
+
readonly usage?: CompactionResult["usage"] | undefined;
|
|
22
|
+
constructor(phase: keyof typeof commitFailures, usage?: CompactionResult["usage"] | undefined);
|
|
19
23
|
get diagnostic(): string;
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
@@ -39,22 +43,10 @@ export type SessionCompactionInput = Readonly<{
|
|
|
39
43
|
run: (messages: readonly Message[], betweenTurns: BetweenTurns) => Effect.Effect<MuleRunOutcome, ShiitakeError>;
|
|
40
44
|
}>;
|
|
41
45
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* A provider's reported usage measures the context it actually read, so it
|
|
46
|
-
* is authoritative only while nothing has reshaped that context since. Once
|
|
47
|
-
* a compaction lands, every kept message's own usage figure is stale: it
|
|
48
|
-
* priced the pre-compaction history, not the summary that replaced it. The
|
|
49
|
-
* one safe fallback is a fresh chars/4 estimate of every message still in
|
|
50
|
-
* view, the same method `tokensAfterCompaction` uses when it records the
|
|
51
|
-
* marker's own `tokensAfter` at commit time; trailing-after-usage alone
|
|
52
|
-
* would undercount (even to zero, when the kept tail's own reply is the
|
|
53
|
-
* newest message and nothing followed it).
|
|
46
|
+
* Compose the retained pre-run, between-turn and post-run compaction phases.
|
|
47
|
+
* A background compaction is a child fiber in the run's own scope.
|
|
54
48
|
*/
|
|
55
|
-
export declare function
|
|
56
|
-
/** Compose the retained pre-run, between-turn and post-run compaction phases. */
|
|
57
|
-
export declare function runWithCompaction(input: SessionCompactionInput): Effect.Effect<MuleRunOutcome, ShiitakeError>;
|
|
49
|
+
export declare function runWithCompaction(input: SessionCompactionInput): Effect.Effect<MuleRunOutcome, ShiitakeError, Scope.Scope>;
|
|
58
50
|
/** What a manual compaction did; a failure is the Effect's error. */
|
|
59
51
|
export type ManualCompactionOutcome = "compacted" | "nothing_to_compact";
|
|
60
52
|
/**
|