@fungi.computer/shiitake 0.3.1 → 0.5.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 +19 -18
- package/dist/agent-control.d.ts +24 -19
- package/dist/agent-control.js +69 -128
- package/dist/client-wire.d.ts +12 -3
- package/dist/client-wire.js +33 -8
- package/dist/client.d.ts +34 -3
- package/dist/client.js +1 -1
- package/dist/cloudflare.d.ts +3 -1
- package/dist/cloudflare.js +2 -2
- package/dist/delegation.d.ts +151 -17
- package/dist/delegation.js +174 -41
- package/dist/effect.d.ts +1 -1
- package/dist/fleet.d.ts +37 -10
- package/dist/fleet.js +217 -48
- package/dist/http-routes.d.ts +84 -12
- package/dist/http-routes.js +15 -1
- package/dist/index.d.ts +2 -2
- package/dist/internal/admission-door.d.ts +2 -0
- package/dist/internal/admission-door.js +9 -2
- package/dist/internal/client-boundary.d.ts +10 -0
- package/dist/internal/client-boundary.js +10 -0
- package/dist/internal/compaction/compaction.js +1 -1
- package/dist/internal/contracts.d.ts +2 -0
- package/dist/internal/coordinator.d.ts +3 -1
- package/dist/internal/coordinator.js +98 -13
- package/dist/internal/effect-client.d.ts +3 -1
- package/dist/internal/effect-client.js +3 -2
- package/dist/internal/effect-contracts.d.ts +6 -2
- package/dist/internal/model-acquisition.js +2 -0
- package/dist/internal/observation-kernel.d.ts +8 -4
- package/dist/internal/observation-kernel.js +44 -9
- package/dist/internal/promise-facade.d.ts +1 -1
- package/dist/internal/promise-facade.js +1 -1
- package/dist/internal/run-scope.js +37 -8
- package/dist/internal/runtime-coordinator.d.ts +5 -0
- package/dist/internal/runtime-coordinator.js +8 -4
- package/dist/internal/runtime-core.d.ts +2 -1
- package/dist/internal/runtime-core.js +6 -1
- package/dist/internal/session-context.d.ts +1 -1
- package/dist/internal/session-context.js +84 -13
- package/dist/internal/woodstock/agent-reply.d.ts +7 -2
- package/dist/internal/woodstock/agent-reply.js +22 -13
- package/dist/internal/woodstock/fleet-store.d.ts +4 -2
- package/dist/internal/woodstock/fleet-store.js +70 -6
- package/dist/internal/woodstock/index.d.ts +2 -0
- package/dist/internal/woodstock/index.js +11 -5
- package/dist/internal/woodstock/run-recovery.js +4 -1
- package/dist/internal/woodstock/session-entry.d.ts +13 -3
- package/dist/internal/woodstock/session-entry.js +1 -0
- package/dist/internal/woodstock/session-store.d.ts +4 -0
- package/dist/internal/woodstock/session-store.js +26 -19
- package/dist/remote-client.js +5 -1
- package/dist/testing.d.ts +5 -2
- package/dist/testing.js +4 -2
- package/docs/api/agent-control.md +18 -12
- package/docs/api/client.md +78 -0
- package/docs/api/delegation.md +72 -55
- package/docs/api/effect.md +5 -0
- package/docs/api/fleet.md +23 -23
- package/docs/api/http.md +2 -1
- package/docs/api/index.md +13 -0
- package/docs/api/testing.md +3 -3
- package/package.json +9 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { muleEffect, piCodingAgentRetryDefaults,
|
|
1
|
+
import { muleEffect, piCodingAgentRetryDefaults, } from "@fungi.computer/mule/effect";
|
|
2
2
|
import { make as makeMycelium } from "@fungi.computer/mycelium/effect";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
4
|
import * as Cause from "effect/Cause";
|
|
@@ -33,13 +33,21 @@ export class ToolSetupError extends ShiitakeError {
|
|
|
33
33
|
* Map Mule's retry transitions onto Woodstock's retained `live_retry` facts.
|
|
34
34
|
* A started attempt ends the backoff countdown before streaming resumes.
|
|
35
35
|
*/
|
|
36
|
-
function toLiveRetryEvent(event) {
|
|
36
|
+
function toLiveRetryEvent(event, model) {
|
|
37
37
|
if (event.type === "retry_attempt_started")
|
|
38
38
|
return {
|
|
39
39
|
type: "auto_retry_attempt",
|
|
40
40
|
run: event.run,
|
|
41
41
|
attempt: event.attempt,
|
|
42
42
|
};
|
|
43
|
+
// Mule's retry text is Pi's private error body. Woodstock's retry journal
|
|
44
|
+
// feeds status and recovery, not the owner's failed-turn conversation.
|
|
45
|
+
const safeReason = ("Model request failed. Retry the request. " +
|
|
46
|
+
unavailableRunReason({
|
|
47
|
+
runId: event.run.value,
|
|
48
|
+
provider: model.provider,
|
|
49
|
+
model: model.id,
|
|
50
|
+
})).slice(0, 200);
|
|
43
51
|
return event.type === "retry_scheduled"
|
|
44
52
|
? {
|
|
45
53
|
type: "auto_retry_start",
|
|
@@ -47,7 +55,7 @@ function toLiveRetryEvent(event) {
|
|
|
47
55
|
attempt: event.attempt,
|
|
48
56
|
maxAttempts: event.maxAttempts,
|
|
49
57
|
delayMs: event.delayMs,
|
|
50
|
-
errorMessage:
|
|
58
|
+
errorMessage: safeReason,
|
|
51
59
|
}
|
|
52
60
|
: event.finalError === undefined
|
|
53
61
|
? {
|
|
@@ -61,7 +69,7 @@ function toLiveRetryEvent(event) {
|
|
|
61
69
|
run: event.run,
|
|
62
70
|
attempt: event.attempt,
|
|
63
71
|
success: event.success,
|
|
64
|
-
finalError:
|
|
72
|
+
finalError: safeReason,
|
|
65
73
|
};
|
|
66
74
|
}
|
|
67
75
|
/** Acquire the run's tools for exactly one Session, so hosts can project per Session. */
|
|
@@ -139,6 +147,7 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
139
147
|
let resume = prepared.resume;
|
|
140
148
|
const settings = openCompactionSettings(options.compaction)(acquired.model);
|
|
141
149
|
let failureReason;
|
|
150
|
+
let failedModelAttempts = 0;
|
|
142
151
|
const run = yield* runWithCompaction({
|
|
143
152
|
store: options.store,
|
|
144
153
|
sessionId: prepared.command.sessionId,
|
|
@@ -151,6 +160,7 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
151
160
|
messages: prepared.messages,
|
|
152
161
|
resuming: resume !== undefined,
|
|
153
162
|
run: (workingMessages, betweenTurns) => {
|
|
163
|
+
failedModelAttempts = 0;
|
|
154
164
|
const continuation = resume;
|
|
155
165
|
resume = undefined;
|
|
156
166
|
// A failed lifecycle notification precedes Mule's returned outcome
|
|
@@ -162,11 +172,21 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
162
172
|
failedEnd = event;
|
|
163
173
|
return Effect.void;
|
|
164
174
|
}
|
|
175
|
+
if (event.type === "message_end" &&
|
|
176
|
+
event.value.role === "assistant" &&
|
|
177
|
+
event.value.stopReason === "error") {
|
|
178
|
+
const providerError = acquired.providerError?.(event.value);
|
|
179
|
+
if (providerError !== undefined)
|
|
180
|
+
return recorder({
|
|
181
|
+
...event,
|
|
182
|
+
value: { ...event.value, providerError },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
165
185
|
return recorder(event);
|
|
166
186
|
};
|
|
167
187
|
const input = {
|
|
168
188
|
model: acquired.model,
|
|
169
|
-
modelStreamIdleTimeoutMs: options.modelStreamIdleTimeoutMs
|
|
189
|
+
modelStreamIdleTimeoutMs: options.modelStreamIdleTimeoutMs,
|
|
170
190
|
retry: options.retry ?? piCodingAgentRetryDefaults,
|
|
171
191
|
retryAfterMs: acquired.retryAfterMs,
|
|
172
192
|
retryable: acquired.retryable,
|
|
@@ -191,7 +211,11 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
191
211
|
phase: "control",
|
|
192
212
|
message: error.message,
|
|
193
213
|
}))),
|
|
194
|
-
onRetry: (event) =>
|
|
214
|
+
onRetry: (event) => {
|
|
215
|
+
if (event.type === "retry_finished" && !event.success)
|
|
216
|
+
failedModelAttempts = event.attempt + 1;
|
|
217
|
+
return recordLiveRetryEvent(options.store, accepted, toLiveRetryEvent(event, acquired.model), event.turn);
|
|
218
|
+
},
|
|
195
219
|
};
|
|
196
220
|
return muleEffect
|
|
197
221
|
.run(acquired.stream === undefined
|
|
@@ -201,8 +225,13 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
201
225
|
if (failedEnd === undefined)
|
|
202
226
|
return Effect.void;
|
|
203
227
|
if (Exit.isSuccess(exit)) {
|
|
204
|
-
|
|
205
|
-
|
|
228
|
+
const cause = exit.value.modelLimitReason ??
|
|
229
|
+
acquired.failureReason?.(exit.value.message) ??
|
|
230
|
+
returnedFailureReason(exit.value);
|
|
231
|
+
failureReason = (failedModelAttempts > 1 &&
|
|
232
|
+
exit.value.modelLimitReason === undefined
|
|
233
|
+
? `The model failed ${String(failedModelAttempts)} times on this conversation: ${cause}`
|
|
234
|
+
: cause).slice(0, 200);
|
|
206
235
|
return recorder(failedEnd, failureReason);
|
|
207
236
|
}
|
|
208
237
|
const error = Cause.findErrorOption(exit.cause);
|
|
@@ -2,6 +2,7 @@ import { type BirdDogEffect } from "@fungi.computer/bird-dog/effect";
|
|
|
2
2
|
import { type WatchdogQueueHead } from "@fungi.computer/watchdog/effect";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
4
|
import type { AgentRunningWork } from "../client.js";
|
|
5
|
+
import type { GrantsDelivery } from "./contracts.js";
|
|
5
6
|
import { ShiitakeError } from "../errors.js";
|
|
6
7
|
import { type AdmissionDoor } from "./admission-door.js";
|
|
7
8
|
import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
@@ -19,6 +20,10 @@ export type RuntimeCoordinator = Readonly<{
|
|
|
19
20
|
beginClose: Effect.Effect<void>;
|
|
20
21
|
/** The one accepted-command door every prompt producer admits through. */
|
|
21
22
|
door: AdmissionDoor;
|
|
23
|
+
/** Reserve a Session within the admission door's enclosing transaction. */
|
|
24
|
+
createSessionWithinTransaction: (requestId: string) => string;
|
|
25
|
+
/** Write new-Session grants through the enclosing admission transaction. */
|
|
26
|
+
stageSessionGrantsWithinTransaction: (sessionId: string, delivery: GrantsDelivery) => void;
|
|
22
27
|
/** Storage-pressure check for new Sessions and new prompts. */
|
|
23
28
|
storage: StorageAdmission;
|
|
24
29
|
recordAcceptedCancellation: RecordAcceptedCancellation;
|
|
@@ -3,7 +3,7 @@ import { WatchdogError } from "@fungi.computer/watchdog";
|
|
|
3
3
|
import { makeRuntime as makeWatchdogRuntime, } from "@fungi.computer/watchdog/effect";
|
|
4
4
|
import * as Effect from "effect/Effect";
|
|
5
5
|
import * as Latch from "effect/Latch";
|
|
6
|
-
import {
|
|
6
|
+
import { publishAgentExecutionChange } from "./observation-kernel.js";
|
|
7
7
|
import { ShiitakeError } from "../errors.js";
|
|
8
8
|
import { agentTurnRecovery, executeAcceptedRun, } from "./accepted-run-executor.js";
|
|
9
9
|
import { openAdmissionDoor } from "./admission-door.js";
|
|
@@ -11,7 +11,7 @@ import { openCompactionSettings } from "./compaction/settings.js";
|
|
|
11
11
|
import { admitExtensionRosterV0, } from "./extension-roster.js";
|
|
12
12
|
import { coreConsumer, openExtensionRuntime, } from "./extension-runtime.js";
|
|
13
13
|
import { reconcileRuntime } from "./runtime-reconciliation.js";
|
|
14
|
-
import { advanceCursor, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
|
|
14
|
+
import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
|
|
15
15
|
function runtimeFailure(message) {
|
|
16
16
|
return new ShiitakeError("runtime", "rejected", message);
|
|
17
17
|
}
|
|
@@ -64,7 +64,7 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
64
64
|
if (runningWork.kind === "session")
|
|
65
65
|
activeSessions.add(job.queue);
|
|
66
66
|
// A different Session's wait changes when this executor changes.
|
|
67
|
-
|
|
67
|
+
publishAgentExecutionChange(options.store);
|
|
68
68
|
}), () => job.lane === options.hostJobs?.lane
|
|
69
69
|
? (hostExecutor.current?.execute(job) ??
|
|
70
70
|
Effect.fail(new WatchdogError("execution_failed")))
|
|
@@ -82,7 +82,7 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
82
82
|
if (job.lane !== options.hostJobs?.lane)
|
|
83
83
|
activeSessions.delete(job.queue);
|
|
84
84
|
runningSlots.delete(job.lane === options.hostJobs?.lane ? "host" : "serial");
|
|
85
|
-
|
|
85
|
+
publishAgentExecutionChange(options.store);
|
|
86
86
|
})),
|
|
87
87
|
},
|
|
88
88
|
wake: {
|
|
@@ -253,6 +253,10 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
253
253
|
: Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"))),
|
|
254
254
|
beginClose,
|
|
255
255
|
door,
|
|
256
|
+
createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId, storage.admitNew),
|
|
257
|
+
stageSessionGrantsWithinTransaction: (sessionId, delivery) => {
|
|
258
|
+
deliverSessionGrants(options.store, sessionId, delivery);
|
|
259
|
+
},
|
|
256
260
|
storage,
|
|
257
261
|
recordAcceptedCancellation: ({ jobId, cancellationId, onlyIfPending }) => watchdog.transactional.recordCancellationIntent({
|
|
258
262
|
jobId,
|
|
@@ -5,6 +5,7 @@ import type { VerifiedAgentReply } from "./woodstock/index.js";
|
|
|
5
5
|
import type { BirdDogEffect } from "@fungi.computer/bird-dog/effect";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import type { Accepted } from "../client.js";
|
|
8
|
+
import type { GrantsDelivery } from "./contracts.js";
|
|
8
9
|
import { ShiitakeError } from "../errors.js";
|
|
9
10
|
import type { ShiitakeEffectOptions, ShiitakeEffectOwner } from "./effect-contracts.js";
|
|
10
11
|
/** Private host composition with the addressed-mail ingress kept off Session. */
|
|
@@ -13,7 +14,7 @@ export type RuntimeCore = Readonly<{
|
|
|
13
14
|
readSettlement: (sessionId: string, commandId: string) => Effect.Effect<CommandSettlement | undefined, ShiitakeError>;
|
|
14
15
|
/** Owner-local execution projection; never served by the remote client. */
|
|
15
16
|
activeSessions: Effect.Effect<readonly string[]>;
|
|
16
|
-
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply) => Effect.Effect<Accepted, ShiitakeError>;
|
|
17
|
+
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply, stageAcceptedNotice?: (mail: BirdDogMail) => void, sessionGrantDelivery?: (sessionId: string) => GrantsDelivery) => Effect.Effect<Accepted, ShiitakeError>;
|
|
17
18
|
}>;
|
|
18
19
|
/** Open Runtime Core, optionally joining its owner-local BirdDog mail room. */
|
|
19
20
|
export declare function openRuntimeCore(options: ShiitakeEffectOptions, mailRoom?: BirdDogEffect): Effect.Effect<RuntimeCore, ShiitakeError>;
|
|
@@ -34,7 +34,12 @@ export function openRuntimeCore(options, mailRoom) {
|
|
|
34
34
|
agent: owner,
|
|
35
35
|
readSettlement: (sessionId, commandId) => runtime.assertOpen.pipe(Effect.andThen(readCommandSettlement(options.store, sessionId, commandId))),
|
|
36
36
|
activeSessions: runtime.activeSessions,
|
|
37
|
-
acceptMail: (mail, meta, reply) => acceptAddressedMail(runtime, mail, meta, reply
|
|
37
|
+
acceptMail: (mail, meta, reply, stageAcceptedNotice, sessionGrantDelivery) => acceptAddressedMail(runtime, mail, meta, reply, stageAcceptedNotice ??
|
|
38
|
+
((notice) => {
|
|
39
|
+
if (mailRoom?.postMail === undefined)
|
|
40
|
+
throw new Error("Recipient Agent mail outbox is unavailable");
|
|
41
|
+
Effect.runSync(mailRoom.postMail(notice));
|
|
42
|
+
}), sessionGrantDelivery),
|
|
38
43
|
});
|
|
39
44
|
});
|
|
40
45
|
}
|
|
@@ -7,4 +7,4 @@ import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
|
7
7
|
* context or its context cannot be read; a window the host cannot describe is
|
|
8
8
|
* null, never guessed.
|
|
9
9
|
*/
|
|
10
|
-
export declare function readSessionContext(options: ShiitakeEffectOptions, sessionId: string): Effect.Effect<SessionContext | undefined>;
|
|
10
|
+
export declare function readSessionContext(options: ShiitakeEffectOptions, sessionId: string, cursor: string | null, cache?: boolean): Effect.Effect<SessionContext | undefined>;
|
|
@@ -2,6 +2,7 @@ import * as Effect from "effect/Effect";
|
|
|
2
2
|
import { estimateContextTokens } from "@earendil-works/pi-ai/utils/estimate";
|
|
3
3
|
import { reportCatchFailure } from "./failure.js";
|
|
4
4
|
import { buildSessionContext, readSessionContextPath, readSessionModelPreference, } from "./woodstock/index.js";
|
|
5
|
+
const contextByOwner = new WeakMap();
|
|
5
6
|
/**
|
|
6
7
|
* The context size compaction measures against the model's window, for the
|
|
7
8
|
* Session's settled context: the next run starts from it. Null before the
|
|
@@ -18,28 +19,98 @@ function settledContextTokens(store, sessionId) {
|
|
|
18
19
|
* context or its context cannot be read; a window the host cannot describe is
|
|
19
20
|
* null, never guessed.
|
|
20
21
|
*/
|
|
21
|
-
export function readSessionContext(options, sessionId) {
|
|
22
|
+
export function readSessionContext(options, sessionId, cursor, cache = true) {
|
|
22
23
|
return Effect.gen(function* () {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
try: () => ({
|
|
26
|
-
tokens: settledContextTokens(options.store, sessionId),
|
|
27
|
-
preference: readSessionModelPreference(options.store, sessionId),
|
|
28
|
-
}),
|
|
24
|
+
const savedPreference = yield* Effect.result(Effect.try({
|
|
25
|
+
try: () => readSessionModelPreference(options.store, sessionId),
|
|
29
26
|
catch: (error) => error,
|
|
30
27
|
}));
|
|
31
|
-
if (
|
|
28
|
+
if (savedPreference._tag === "Failure") {
|
|
29
|
+
reportCatchFailure("session-context.preference", savedPreference.failure);
|
|
32
30
|
return undefined;
|
|
33
|
-
|
|
31
|
+
}
|
|
32
|
+
const preference = savedPreference.success;
|
|
33
|
+
const preferenceKey = JSON.stringify(preference);
|
|
34
|
+
const ownerCache = contextByOwner.get(options) ?? new Map();
|
|
35
|
+
contextByOwner.set(options, ownerCache);
|
|
36
|
+
const previous = cache ? ownerCache.get(sessionId) : undefined;
|
|
34
37
|
const describe = options.model.describe;
|
|
35
|
-
if (
|
|
36
|
-
|
|
38
|
+
if (previous?.cursor === cursor && previous.preference === preferenceKey) {
|
|
39
|
+
if (previous.value === undefined || describe === undefined)
|
|
40
|
+
return previous.value;
|
|
41
|
+
const previousValue = previous.value;
|
|
42
|
+
// A null Session override follows a mutable Agent default. Check the
|
|
43
|
+
// selected window without decoding history again; a changed window is
|
|
44
|
+
// a distinct effective preference for this cursor.
|
|
45
|
+
return yield* describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => {
|
|
46
|
+
if (previousValue.window === contextWindow)
|
|
47
|
+
return previousValue;
|
|
48
|
+
const value = Object.freeze({
|
|
49
|
+
tokens: previousValue.tokens,
|
|
50
|
+
window: contextWindow,
|
|
51
|
+
});
|
|
52
|
+
if (cache && ownerCache.get(sessionId)?.cursor === cursor)
|
|
53
|
+
ownerCache.set(sessionId, {
|
|
54
|
+
cursor,
|
|
55
|
+
preference: preferenceKey,
|
|
56
|
+
value,
|
|
57
|
+
});
|
|
58
|
+
return value;
|
|
59
|
+
}), Effect.catch((error) => Effect.sync(() => {
|
|
60
|
+
reportCatchFailure("session-context.describe", error);
|
|
61
|
+
return previousValue;
|
|
62
|
+
})));
|
|
63
|
+
}
|
|
64
|
+
// At most one cached whole-window read per Session, cursor, and
|
|
65
|
+
// preference: a late describe may publish only while this cursor still
|
|
66
|
+
// owns the cache entry. Reads before the tip bypass the cache and each read
|
|
67
|
+
// the whole window. A cold-cache connection also reads the whole kept
|
|
68
|
+
// window.
|
|
69
|
+
const contextRead = yield* Effect.result(Effect.try({
|
|
70
|
+
try: () => settledContextTokens(options.store, sessionId),
|
|
71
|
+
catch: (error) => error,
|
|
72
|
+
}));
|
|
73
|
+
if (contextRead._tag === "Failure") {
|
|
74
|
+
reportCatchFailure("session-context.figure", contextRead.failure);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const tokens = contextRead.success;
|
|
78
|
+
if (tokens === null) {
|
|
79
|
+
if (cache)
|
|
80
|
+
ownerCache.set(sessionId, {
|
|
81
|
+
cursor,
|
|
82
|
+
preference: preferenceKey,
|
|
83
|
+
value: undefined,
|
|
84
|
+
});
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (describe === undefined) {
|
|
88
|
+
const value = Object.freeze({ tokens, window: null });
|
|
89
|
+
if (cache)
|
|
90
|
+
ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
// Publish the decoded figure before yielding to the host's model
|
|
94
|
+
// description. Concurrent watchers then reuse these tokens rather than
|
|
95
|
+
// decoding the same Session window while `describe` is in flight.
|
|
96
|
+
if (cache)
|
|
97
|
+
ownerCache.set(sessionId, {
|
|
98
|
+
cursor,
|
|
99
|
+
preference: preferenceKey,
|
|
100
|
+
value: Object.freeze({ tokens, window: null }),
|
|
101
|
+
});
|
|
37
102
|
// A null Session override follows a mutable Agent default. Resolve through
|
|
38
103
|
// the host's turn-model selector on every read, without acquiring a turn.
|
|
39
|
-
const window = yield* describe({
|
|
104
|
+
const window = yield* describe({
|
|
105
|
+
sessionId,
|
|
106
|
+
preference,
|
|
107
|
+
}).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
|
|
40
108
|
reportCatchFailure("session-context.describe", error);
|
|
41
109
|
return null;
|
|
42
110
|
})));
|
|
43
|
-
|
|
111
|
+
const value = Object.freeze({ tokens, window });
|
|
112
|
+
if (cache && ownerCache.get(sessionId)?.cursor === cursor)
|
|
113
|
+
ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
|
|
114
|
+
return value;
|
|
44
115
|
});
|
|
45
116
|
}
|
|
@@ -5,13 +5,18 @@ import * as Schema from "effect/Schema";
|
|
|
5
5
|
* the mail it sent: the replying Agent and Session plus BirdDog's settlement
|
|
6
6
|
* facts. Only `acceptMail` stores it, and only the host passes it.
|
|
7
7
|
*/
|
|
8
|
-
export declare const VerifiedAgentReplySchema: Schema.Struct<{
|
|
8
|
+
export declare const VerifiedAgentReplySchema: Schema.Union<readonly [Schema.Struct<{
|
|
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
12
|
readonly agentId: Schema.String;
|
|
13
13
|
readonly sessionId: Schema.String;
|
|
14
|
-
}
|
|
14
|
+
}>, Schema.Struct<{
|
|
15
|
+
readonly outcome: Schema.Literal<"undeliverable">;
|
|
16
|
+
readonly reason: Schema.Literals<readonly ["recipient_offline", "recipient_left"]>;
|
|
17
|
+
readonly agentId: Schema.String;
|
|
18
|
+
readonly sessionId: Schema.optionalKey<Schema.String>;
|
|
19
|
+
}>]>;
|
|
15
20
|
/** A delegated Agent's reply after the admitting host authenticated it. */
|
|
16
21
|
export type VerifiedAgentReply = typeof VerifiedAgentReplySchema.Type;
|
|
17
22
|
/** The tag that marks a host-verified Agent reply in model context. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SettledBirdDogReplySchema, UndeliverableBirdDogReplySchema, } from "@fungi.computer/bird-dog/effect";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
3
|
const Identifier = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
|
|
4
4
|
/**
|
|
@@ -6,11 +6,18 @@ const Identifier = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength
|
|
|
6
6
|
* the mail it sent: the replying Agent and Session plus BirdDog's settlement
|
|
7
7
|
* facts. Only `acceptMail` stores it, and only the host passes it.
|
|
8
8
|
*/
|
|
9
|
-
export const VerifiedAgentReplySchema = Schema.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
export const VerifiedAgentReplySchema = Schema.Union([
|
|
10
|
+
Schema.Struct({
|
|
11
|
+
agentId: Identifier,
|
|
12
|
+
sessionId: Identifier,
|
|
13
|
+
...SettledBirdDogReplySchema.fields,
|
|
14
|
+
}),
|
|
15
|
+
Schema.Struct({
|
|
16
|
+
agentId: Identifier,
|
|
17
|
+
sessionId: Schema.optionalKey(Identifier),
|
|
18
|
+
...UndeliverableBirdDogReplySchema.fields,
|
|
19
|
+
}),
|
|
20
|
+
]);
|
|
14
21
|
/** The tag that marks a host-verified Agent reply in model context. */
|
|
15
22
|
export const AGENT_REPLY_TAG = "fungi_agent_reply";
|
|
16
23
|
// `<` before the tag name, allowing the spacing and case a model would still read as the tag.
|
|
@@ -27,14 +34,16 @@ function attribute(value) {
|
|
|
27
34
|
.replaceAll(">", ">");
|
|
28
35
|
}
|
|
29
36
|
function opening(reply) {
|
|
30
|
-
const attributes = [
|
|
31
|
-
|
|
32
|
-
["session_id", reply.sessionId]
|
|
33
|
-
|
|
34
|
-
["
|
|
35
|
-
];
|
|
36
|
-
if (reply.diagnostic !== undefined)
|
|
37
|
+
const attributes = [["agent_id", reply.agentId]];
|
|
38
|
+
if (reply.sessionId !== undefined)
|
|
39
|
+
attributes.push(["session_id", reply.sessionId]);
|
|
40
|
+
if (reply.outcome !== "undeliverable")
|
|
41
|
+
attributes.push(["command_id", reply.commandId]);
|
|
42
|
+
attributes.push(["outcome", reply.outcome]);
|
|
43
|
+
if (reply.outcome !== "undeliverable" && reply.diagnostic !== undefined)
|
|
37
44
|
attributes.push(["diagnostic", reply.diagnostic]);
|
|
45
|
+
if (reply.outcome === "undeliverable")
|
|
46
|
+
attributes.push(["reason", reply.reason]);
|
|
38
47
|
const rendered = attributes
|
|
39
48
|
.map(([name, value]) => `${name}="${attribute(value)}"`)
|
|
40
49
|
.join(" ");
|
|
@@ -27,10 +27,12 @@ export declare function createFleetDelegationStore(storage: WatchdogSqliteOwner)
|
|
|
27
27
|
read: (requestId: string) => {
|
|
28
28
|
readonly [x: string]: string | number | ArrayBuffer | null;
|
|
29
29
|
} | undefined;
|
|
30
|
-
/** Every delegation
|
|
30
|
+
/** Every recipient-accepted delegation, newest first. */
|
|
31
31
|
sent: () => readonly {
|
|
32
32
|
readonly [x: string]: string | number | ArrayBuffer | null;
|
|
33
33
|
}[];
|
|
34
34
|
reserve(requestId: string, input: string, mail: string): void;
|
|
35
|
-
recordReceipt(requestId: string, receipt: string): void;
|
|
35
|
+
recordReceipt(requestId: string, receipt: string, expectedMail: string, acceptedMail: string): void;
|
|
36
|
+
recordHeld(requestId: string): void;
|
|
37
|
+
recordUndeliverable(requestId: string): boolean;
|
|
36
38
|
};
|
|
@@ -81,24 +81,88 @@ export function createFleetDirectoryStore(sql) {
|
|
|
81
81
|
/** Durable correspondence is frozen before delivery; retries reuse the same row. */
|
|
82
82
|
export function createFleetDelegationStore(storage) {
|
|
83
83
|
storage.sql.exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_delegations (
|
|
84
|
-
request_id TEXT PRIMARY KEY, input TEXT NOT NULL, mail TEXT NOT NULL,
|
|
84
|
+
request_id TEXT PRIMARY KEY, input TEXT NOT NULL, mail TEXT NOT NULL,
|
|
85
|
+
receipt TEXT
|
|
86
|
+
) STRICT`);
|
|
87
|
+
storage.sql.exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_held_delegations (
|
|
88
|
+
request_id TEXT PRIMARY KEY
|
|
89
|
+
) STRICT`);
|
|
90
|
+
storage.sql
|
|
91
|
+
.exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_undeliverable_delegations (
|
|
92
|
+
request_id TEXT PRIMARY KEY
|
|
85
93
|
) STRICT`);
|
|
86
94
|
return {
|
|
87
95
|
read: (requestId) => storage.sql
|
|
88
|
-
.exec(
|
|
96
|
+
.exec(`SELECT delegation.input, delegation.mail,
|
|
97
|
+
CASE
|
|
98
|
+
WHEN terminal.request_id IS NOT NULL THEN 'undeliverable'
|
|
99
|
+
WHEN held.request_id IS NOT NULL THEN 'held'
|
|
100
|
+
WHEN delegation.receipt IS NOT NULL THEN 'accepted'
|
|
101
|
+
ELSE 'pending'
|
|
102
|
+
END AS state,
|
|
103
|
+
delegation.receipt
|
|
104
|
+
FROM shiitake_fleet_delegations AS delegation
|
|
105
|
+
LEFT JOIN shiitake_fleet_held_delegations AS held
|
|
106
|
+
ON held.request_id = delegation.request_id
|
|
107
|
+
LEFT JOIN shiitake_fleet_undeliverable_delegations AS terminal
|
|
108
|
+
ON terminal.request_id = delegation.request_id
|
|
109
|
+
WHERE delegation.request_id = ?`, requestId)
|
|
89
110
|
.toArray()[0],
|
|
90
|
-
/** Every delegation
|
|
111
|
+
/** Every recipient-accepted delegation, newest first. */
|
|
91
112
|
sent: () => storage.sql
|
|
92
|
-
.exec(
|
|
113
|
+
.exec(`SELECT input, mail, 'accepted' AS state, receipt
|
|
114
|
+
FROM shiitake_fleet_delegations
|
|
115
|
+
WHERE receipt IS NOT NULL
|
|
116
|
+
ORDER BY rowid DESC`)
|
|
93
117
|
.toArray(),
|
|
94
118
|
reserve(requestId, input, mail) {
|
|
95
119
|
storage.transactionSync(() => {
|
|
96
120
|
storage.sql.exec("INSERT OR IGNORE INTO shiitake_fleet_delegations(request_id, input, mail) VALUES (?, ?, ?)", requestId, input, mail);
|
|
97
121
|
});
|
|
98
122
|
},
|
|
99
|
-
recordReceipt(requestId, receipt) {
|
|
123
|
+
recordReceipt(requestId, receipt, expectedMail, acceptedMail) {
|
|
100
124
|
storage.transactionSync(() => {
|
|
101
|
-
|
|
125
|
+
const row = storage.sql
|
|
126
|
+
.exec("SELECT mail, receipt FROM shiitake_fleet_delegations WHERE request_id = ?", requestId)
|
|
127
|
+
.toArray()[0];
|
|
128
|
+
if (row === undefined)
|
|
129
|
+
throw new Error("Cannot record acceptance without frozen delegation mail");
|
|
130
|
+
if (row.receipt !== null) {
|
|
131
|
+
if (row.receipt === receipt && row.mail === acceptedMail)
|
|
132
|
+
return;
|
|
133
|
+
throw new Error("Delegation acceptance conflicts with its recorded receipt");
|
|
134
|
+
}
|
|
135
|
+
if (row.mail !== expectedMail)
|
|
136
|
+
throw new Error("Delegation mail changed before acceptance was recorded");
|
|
137
|
+
storage.sql.exec("UPDATE shiitake_fleet_delegations SET mail = ?, receipt = ? WHERE request_id = ? AND receipt IS NULL", acceptedMail, receipt, requestId);
|
|
138
|
+
storage.sql.exec("DELETE FROM shiitake_fleet_held_delegations WHERE request_id = ?", requestId);
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
recordHeld(requestId) {
|
|
142
|
+
storage.transactionSync(() => {
|
|
143
|
+
storage.sql.exec(`INSERT OR IGNORE INTO shiitake_fleet_held_delegations (request_id)
|
|
144
|
+
SELECT request_id FROM shiitake_fleet_delegations
|
|
145
|
+
WHERE request_id = ? AND receipt IS NULL
|
|
146
|
+
AND NOT EXISTS (
|
|
147
|
+
SELECT 1 FROM shiitake_fleet_undeliverable_delegations
|
|
148
|
+
WHERE request_id = ?
|
|
149
|
+
)`, requestId, requestId);
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
recordUndeliverable(requestId) {
|
|
153
|
+
return storage.transactionSync(() => {
|
|
154
|
+
const held = storage.sql
|
|
155
|
+
.exec(`SELECT delegation.request_id
|
|
156
|
+
FROM shiitake_fleet_delegations AS delegation
|
|
157
|
+
JOIN shiitake_fleet_held_delegations AS held
|
|
158
|
+
ON held.request_id = delegation.request_id
|
|
159
|
+
WHERE delegation.request_id = ? AND delegation.receipt IS NULL`, requestId)
|
|
160
|
+
.toArray()[0];
|
|
161
|
+
if (held === undefined)
|
|
162
|
+
return false;
|
|
163
|
+
storage.sql.exec("INSERT OR IGNORE INTO shiitake_fleet_undeliverable_delegations(request_id) VALUES (?)", requestId);
|
|
164
|
+
storage.sql.exec("DELETE FROM shiitake_fleet_held_delegations WHERE request_id = ?", requestId);
|
|
165
|
+
return true;
|
|
102
166
|
});
|
|
103
167
|
},
|
|
104
168
|
};
|
|
@@ -125,6 +125,8 @@ export declare function readSessionSummary(owner: SqliteOwner, sessionId: string
|
|
|
125
125
|
* `admitNew` may refuse a new Session; a replayed request is never refused.
|
|
126
126
|
*/
|
|
127
127
|
export declare function createSession(owner: SqliteOwner, requestId: string, admitNew: () => void): Effect.Effect<string, ShiitakeError>;
|
|
128
|
+
/** Reserve or replay a Session inside an enclosing owner transaction. */
|
|
129
|
+
export declare function createSessionWithinTransaction(owner: SqliteOwner, requestId: string, admitNew: () => void): string;
|
|
128
130
|
/** Permanently delete one idle Session; false when it does not exist. */
|
|
129
131
|
export declare function deleteSession(owner: SqliteOwner, input: Readonly<{
|
|
130
132
|
sessionId: string;
|
|
@@ -7,7 +7,7 @@ import { diagnosticsByWorkId, projectCommandSettlement, readClientReplayPage, re
|
|
|
7
7
|
import { sessionIds } from "./identities.js";
|
|
8
8
|
import { openProgress, openToolProgress } from "./progress.js";
|
|
9
9
|
import { encodeStoredSessionEntry, } from "./session-entry.js";
|
|
10
|
-
import { appendSessionEntry, appendCompactionTransitionOwned, appendCompactionNoticeOwned, placeCompactionTransitionOwned, acceptSessionAbort, acceptSessionCancellation, completeSessionLeaf, prepareSessionRun, prepareSessionStore, readSessionEntriesForWork, readSessionCommandAdmission, settleSessionControls, takePendingSessionControls, readSessionEntryById, selectSessionBranch, forkSessionAt, readStoredSessionSummary as readSessionSummaryOwned, readStoredSessionFactPage, createStoredSession, deleteStoredSession, appendOperatorEntry, } from "./session-store.js";
|
|
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
12
|
export * from "./extension-store.js";
|
|
13
13
|
export { readLatestPendingPrompt } from "./client-projection.js";
|
|
@@ -66,7 +66,7 @@ export function cancelSession(owner, sessionId, commandId, onlyIfPending, readQu
|
|
|
66
66
|
return storeEffect(() => {
|
|
67
67
|
const cancellation = acceptSessionCancellation(owner, sessionId, commandId, readQueueHead, record, onlyIfPending, meta);
|
|
68
68
|
if (cancellation?.delivery === "new")
|
|
69
|
-
publishOwnerObservation(owner, sessionId);
|
|
69
|
+
publishOwnerObservation(owner, sessionId, "settlement");
|
|
70
70
|
return cancellation;
|
|
71
71
|
});
|
|
72
72
|
}
|
|
@@ -83,14 +83,14 @@ export function takeSessionControls(owner, input) {
|
|
|
83
83
|
export function settleControls(owner, input) {
|
|
84
84
|
return storeEffect(() => {
|
|
85
85
|
settleSessionControls(owner, input);
|
|
86
|
-
publishOwnerObservation(owner, input.sessionId);
|
|
86
|
+
publishOwnerObservation(owner, input.sessionId, "settlement");
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
/** Resolve Session-scoped abort to private work and record it atomically. */
|
|
90
90
|
export function abortSession(owner, sessionId, readQueueHead, record, meta) {
|
|
91
91
|
return storeEffect(() => {
|
|
92
92
|
const cancellation = acceptSessionAbort(owner, sessionId, readQueueHead, record, meta);
|
|
93
|
-
publishOwnerObservation(owner, sessionId);
|
|
93
|
+
publishOwnerObservation(owner, sessionId, "settlement");
|
|
94
94
|
return cancellation?.accepted;
|
|
95
95
|
});
|
|
96
96
|
}
|
|
@@ -202,6 +202,10 @@ export function readSessionSummary(owner, sessionId) {
|
|
|
202
202
|
export function createSession(owner, requestId, admitNew) {
|
|
203
203
|
return storeEffect(() => createStoredSession(owner, decodeClientSessionCreationRequestId({ requestId }), admitNew));
|
|
204
204
|
}
|
|
205
|
+
/** Reserve or replay a Session inside an enclosing owner transaction. */
|
|
206
|
+
export function createSessionWithinTransaction(owner, requestId, admitNew) {
|
|
207
|
+
return createStoredSessionWithinTransaction(owner, decodeClientSessionCreationRequestId({ requestId }), admitNew);
|
|
208
|
+
}
|
|
205
209
|
/** Permanently delete one idle Session; false when it does not exist. */
|
|
206
210
|
export function deleteSession(owner, input) {
|
|
207
211
|
return storeEffect(() => deleteStoredSession(owner, {
|
|
@@ -273,7 +277,9 @@ function appendRecordedEntry(owner, accepted, entry, findExisting, beforeAppend)
|
|
|
273
277
|
throw rejected("Session fact identity allocation failed");
|
|
274
278
|
});
|
|
275
279
|
if (delivery === "new")
|
|
276
|
-
publishOwnerObservation(owner, accepted.sessionId
|
|
280
|
+
publishOwnerObservation(owner, accepted.sessionId, entry.kind === "run_terminal" || entry.kind === "work_terminal"
|
|
281
|
+
? "settlement"
|
|
282
|
+
: "durable");
|
|
277
283
|
}
|
|
278
284
|
/** Persist an operator-only sandbox diagnostic beside its accepted Session work. */
|
|
279
285
|
export function recordSandboxFailure(owner, accepted, event) {
|
|
@@ -56,7 +56,10 @@ export function contextMessageIds(path, activeWorkId) {
|
|
|
56
56
|
if (entry.value.role === "assistant") {
|
|
57
57
|
lastWorkAssistant.set(entry.workId, row.id);
|
|
58
58
|
}
|
|
59
|
-
if (terminalWork.has(entry.workId) || entry.workId === activeWorkId)
|
|
59
|
+
if ((terminalWork.has(entry.workId) || entry.workId === activeWorkId) &&
|
|
60
|
+
(entry.value.role !== "assistant" ||
|
|
61
|
+
(entry.value.stopReason !== "error" &&
|
|
62
|
+
entry.value.stopReason !== "aborted")))
|
|
60
63
|
messages.add(row.id);
|
|
61
64
|
}
|
|
62
65
|
if ((entry.kind === "compaction" && entry.data.resumesRun) ||
|