@fungi.computer/shiitake 0.6.0 → 0.8.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 +59 -21
- package/dist/agent-control.d.ts +1 -1
- package/dist/agent-control.js +40 -21
- package/dist/client-visitor.d.ts +2 -0
- package/dist/client-visitor.js +6 -1
- package/dist/client-wire.d.ts +4 -0
- package/dist/client-wire.js +26 -15
- package/dist/client.d.ts +23 -11
- package/dist/client.js +22 -0
- package/dist/cloudflare.d.ts +20 -0
- package/dist/cloudflare.js +4 -1
- package/dist/delegation.d.ts +5 -0
- package/dist/delegation.js +24 -8
- package/dist/effect.d.ts +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -0
- package/dist/extensions.d.ts +1 -1
- package/dist/fleet.d.ts +14 -2
- package/dist/fleet.js +81 -19
- package/dist/http-routes.d.ts +12 -16
- package/dist/http.d.ts +1 -0
- package/dist/http.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal/admission-door.d.ts +1 -1
- package/dist/internal/admission-door.js +8 -4
- package/dist/internal/contracts.d.ts +5 -0
- package/dist/internal/coordinator.js +107 -50
- package/dist/internal/effect-contracts.d.ts +5 -0
- package/dist/internal/extension-runtime.js +5 -3
- package/dist/internal/observation-kernel.js +12 -3
- package/dist/internal/promise-facade.js +2 -0
- package/dist/internal/run-scope.d.ts +1 -0
- package/dist/internal/run-scope.js +29 -7
- package/dist/internal/runtime-coordinator.d.ts +14 -0
- package/dist/internal/runtime-coordinator.js +57 -10
- package/dist/internal/runtime-core.d.ts +3 -0
- package/dist/internal/runtime-core.js +1 -0
- package/dist/internal/runtime-reconciliation.js +4 -1
- package/dist/internal/session-context.d.ts +5 -5
- package/dist/internal/session-context.js +80 -83
- package/dist/internal/session-status.d.ts +4 -2
- package/dist/internal/session-status.js +22 -4
- package/dist/internal/woodstock/client-projection.js +5 -11
- package/dist/internal/woodstock/fleet-store.d.ts +3 -2
- package/dist/internal/woodstock/fleet-store.js +24 -4
- package/dist/internal/woodstock/index.d.ts +3 -3
- package/dist/internal/woodstock/index.js +18 -7
- package/dist/internal/woodstock/progress.d.ts +13 -1
- package/dist/internal/woodstock/progress.js +130 -24
- package/dist/internal/woodstock/session-delegator.d.ts +2 -1
- package/dist/internal/woodstock/session-store.d.ts +14 -2
- package/dist/internal/woodstock/session-store.js +329 -66
- package/dist/node.js +11 -2
- package/dist/server-visitor-stream.js +33 -24
- package/dist/testing.d.ts +2 -0
- package/dist/testing.js +2 -0
- package/docs/api/agent-control.md +3 -2
- package/docs/api/client.md +79 -19
- package/docs/api/cloudflare.md +16 -0
- package/docs/api/delegation.md +84 -50
- package/docs/api/effect.md +85 -7
- package/docs/api/extensions.md +8 -8
- package/docs/api/fleet.md +118 -29
- package/docs/api/http.md +28 -0
- package/docs/api/index.md +39 -15
- package/docs/api/testing.md +20 -7
- package/package.json +5 -5
|
@@ -7,11 +7,11 @@ import * as Option from "effect/Option";
|
|
|
7
7
|
import * as Stream from "effect/Stream";
|
|
8
8
|
import { ShiitakeError } from "../errors.js";
|
|
9
9
|
import { decodeClientReadOptions, decodeClientSettlementRequest, requireDecoded, decodeClientSessionCommand, decodeClientSessionId, decodeClientSubscribeOptions, } from "./client-boundary.js";
|
|
10
|
-
import { mergeObservationSignals, openObservationKernel, publishOwnerObservation, } from "./observation-kernel.js";
|
|
10
|
+
import { mergeObservationSignals, openObservationKernel, publishAgentExecutionChange, publishOwnerObservation, } from "./observation-kernel.js";
|
|
11
11
|
import { checkHostMeta } from "./host-meta.js";
|
|
12
|
-
import { readSessionActivity } from "./session-status.js";
|
|
13
|
-
import {
|
|
14
|
-
import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readCommandSettlement, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
|
|
12
|
+
import { readSessionActivity, } from "./session-status.js";
|
|
13
|
+
import { prepareSessionContext } from "./session-context.js";
|
|
14
|
+
import { progressPosition, readProcessProgressDelta, abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readCommandSettlement, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
|
|
15
15
|
/** Generic Agent-scope extension controls over the runtime's one call door. */
|
|
16
16
|
function agentExtensions(runtime) {
|
|
17
17
|
return Object.freeze({
|
|
@@ -20,12 +20,15 @@ function agentExtensions(runtime) {
|
|
|
20
20
|
call: (extensionId, command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "agent" }, extensionId, command, input))),
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
function
|
|
23
|
+
function currentSessionWork(runtime, sessionId) {
|
|
24
24
|
return runtime
|
|
25
25
|
.readQueueHead(sessionId)
|
|
26
26
|
.pipe(Effect.flatMap((head) => (head.state === "queued"
|
|
27
27
|
? runtime.runningWork(head.jobId)
|
|
28
|
-
: Effect.succeed(null)).pipe(Effect.map((runningWork) =>
|
|
28
|
+
: Effect.succeed(null)).pipe(Effect.map((runningWork) => ({ head, runningWork })))));
|
|
29
|
+
}
|
|
30
|
+
function currentSessionActivity(options, runtime, sessionId) {
|
|
31
|
+
return currentSessionWork(runtime, sessionId).pipe(Effect.map(({ head, runningWork }) => readSessionActivity(options.store, sessionId, head, runningWork)));
|
|
29
32
|
}
|
|
30
33
|
/** Carry the Session's context use on a baseline, when it has one. */
|
|
31
34
|
function withContext(snapshot, context) {
|
|
@@ -35,7 +38,10 @@ function withContext(snapshot, context) {
|
|
|
35
38
|
}
|
|
36
39
|
/** Read the baseline and its context use for one Session. */
|
|
37
40
|
function readSessionWithContext(options, runtime, sessionId, input) {
|
|
38
|
-
return
|
|
41
|
+
return currentSessionWork(runtime, sessionId).pipe(Effect.flatMap(({ head, runningWork }) => readSession(options.store, sessionId, () => readSessionActivity(options.store, sessionId, head, runningWork), input, () => runtime.extensions.project(sessionId), (snapshot) => ({
|
|
42
|
+
snapshot,
|
|
43
|
+
context: prepareSessionContext(options, sessionId, snapshot.cursor, input?.before === undefined),
|
|
44
|
+
}))), Effect.flatMap(({ snapshot, context }) => context.pipe(Effect.map((value) => withContext(snapshot, value)))));
|
|
39
45
|
}
|
|
40
46
|
function sessionOwnerOperation(runtime, sessionId, operation) {
|
|
41
47
|
return runtime.assertOpen.pipe(Effect.andThen(Effect.try({
|
|
@@ -92,7 +98,46 @@ function continueFromReset(frame) {
|
|
|
92
98
|
}),
|
|
93
99
|
];
|
|
94
100
|
}
|
|
95
|
-
function subscriptionFrames(options, runtime, registration, sessionId, after, delivery, includeSandboxFailures = false) {
|
|
101
|
+
function subscriptionFrames(options, runtime, registration, sessionId, after, delivery, includeSandboxFailures = false, baselineProgress = null) {
|
|
102
|
+
let position = progressPosition(baselineProgress);
|
|
103
|
+
let toolOutput = [];
|
|
104
|
+
const reset = () => resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map((frame) => {
|
|
105
|
+
position = progressPosition(frame.baseline.progress);
|
|
106
|
+
toolOutput = frame.baseline.toolOutput;
|
|
107
|
+
return continueFromReset(frame);
|
|
108
|
+
}));
|
|
109
|
+
const liveFrames = (activity) => {
|
|
110
|
+
const queue = activity.facts.queue;
|
|
111
|
+
const delta = queue.state !== "running"
|
|
112
|
+
? null
|
|
113
|
+
: readProcessProgressDelta(options.store, sessionId, queue.commandId, position);
|
|
114
|
+
if (delta === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
const frames = [];
|
|
117
|
+
if (delta !== null) {
|
|
118
|
+
frames.push({
|
|
119
|
+
kind: "progress",
|
|
120
|
+
progress: delta,
|
|
121
|
+
activity: activity.facts,
|
|
122
|
+
});
|
|
123
|
+
position = {
|
|
124
|
+
messageId: delta.messageId,
|
|
125
|
+
revision: delta.revision,
|
|
126
|
+
text: delta.text.offset + delta.text.append.length,
|
|
127
|
+
thinking: delta.thinking.offset + delta.thinking.append.length,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (toolOutput.length !== activity.toolOutput.length ||
|
|
131
|
+
toolOutput.some((slot, index) => slot !== activity.toolOutput[index])) {
|
|
132
|
+
toolOutput = activity.toolOutput;
|
|
133
|
+
frames.push({
|
|
134
|
+
kind: "tool_output",
|
|
135
|
+
toolOutput,
|
|
136
|
+
activity: activity.facts,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return frames;
|
|
140
|
+
};
|
|
96
141
|
const initial = {
|
|
97
142
|
after,
|
|
98
143
|
delivery,
|
|
@@ -106,64 +151,73 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
|
|
|
106
151
|
if (signal === "closed")
|
|
107
152
|
return Effect.succeed([[], Option.none()]);
|
|
108
153
|
if (signal === "reset")
|
|
109
|
-
return
|
|
154
|
+
return reset();
|
|
110
155
|
return runtime.assertOpen.pipe(Effect.andThen(currentSessionActivity(options, runtime, sessionId)), Effect.flatMap((activity) => {
|
|
111
156
|
registration.setQueued(activity.facts.queue.state === "queued");
|
|
112
|
-
if (signal === "
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (signal === "progress" && state.delivery === "live") {
|
|
157
|
+
if ((signal === "progress" || signal === "activity") &&
|
|
158
|
+
state.delivery === "live") {
|
|
159
|
+
const frames = liveFrames(activity);
|
|
160
|
+
if (frames === undefined)
|
|
161
|
+
return reset();
|
|
118
162
|
return Effect.succeed([
|
|
119
163
|
[
|
|
120
|
-
...(
|
|
121
|
-
? [
|
|
122
|
-
: [
|
|
164
|
+
...(signal === "activity"
|
|
165
|
+
? [
|
|
123
166
|
{
|
|
124
|
-
kind: "
|
|
125
|
-
progress: activity.progress,
|
|
167
|
+
kind: "activity",
|
|
126
168
|
activity: activity.facts,
|
|
127
169
|
},
|
|
128
|
-
]
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
toolOutput: activity.toolOutput,
|
|
132
|
-
activity: activity.facts,
|
|
133
|
-
},
|
|
170
|
+
]
|
|
171
|
+
: []),
|
|
172
|
+
...frames,
|
|
134
173
|
],
|
|
135
174
|
Option.some({ ...state, awaitSignal: true }),
|
|
136
175
|
]);
|
|
137
176
|
}
|
|
138
|
-
return readSessionReplayPage(options.store, sessionId, state.after, state.delivery, activity, () => runtime.extensions.project(sessionId), includeSandboxFailures
|
|
177
|
+
return readSessionReplayPage(options.store, sessionId, state.after, state.delivery, activity, () => runtime.extensions.project(sessionId), includeSandboxFailures, (page) => {
|
|
178
|
+
const latestCursor = page.kind === "page"
|
|
179
|
+
? page.frames
|
|
180
|
+
.flatMap((frame) => frame.kind === "progress" ||
|
|
181
|
+
frame.kind === "activity" ||
|
|
182
|
+
frame.kind === "tool_output"
|
|
183
|
+
? []
|
|
184
|
+
: [frame.cursor])
|
|
185
|
+
.at(-1)
|
|
186
|
+
: undefined;
|
|
187
|
+
// Prepare a figure only for the final moving update,
|
|
188
|
+
// while its cursor and stored context share the read.
|
|
189
|
+
const context = page.kind === "page" &&
|
|
190
|
+
!page.hasMore &&
|
|
191
|
+
page.frames.some((frame) => frame.kind === "update" &&
|
|
192
|
+
frame.cursor !== state.after)
|
|
193
|
+
? prepareSessionContext(options, sessionId, latestCursor ?? null)
|
|
194
|
+
: Effect.succeed(undefined);
|
|
195
|
+
return { page, latestCursor, context };
|
|
196
|
+
}).pipe(Effect.flatMap(({ page, latestCursor, context, }) => {
|
|
139
197
|
if (page.kind === "reset")
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
frame.kind === "tool_output"
|
|
198
|
+
return reset();
|
|
199
|
+
if (!page.hasMore)
|
|
200
|
+
toolOutput = activity.toolOutput;
|
|
201
|
+
const progressFrames = page.hasMore
|
|
145
202
|
? []
|
|
146
|
-
:
|
|
147
|
-
|
|
203
|
+
: liveFrames(activity);
|
|
204
|
+
if (progressFrames === undefined)
|
|
205
|
+
return reset();
|
|
148
206
|
const next = {
|
|
149
207
|
after: latestCursor ?? state.after,
|
|
150
208
|
delivery: page.hasMore ? state.delivery : "live",
|
|
151
209
|
awaitSignal: !page.hasMore,
|
|
152
210
|
};
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
: page.frames.map((frame) => frame.kind === "update" &&
|
|
164
|
-
frame.cursor === latestCursor
|
|
165
|
-
? Object.freeze({ ...frame, context })
|
|
166
|
-
: frame),
|
|
211
|
+
return context.pipe(Effect.map((context) => [
|
|
212
|
+
[
|
|
213
|
+
...(context === undefined
|
|
214
|
+
? page.frames
|
|
215
|
+
: page.frames.map((frame) => frame.kind === "update" &&
|
|
216
|
+
frame.cursor === latestCursor
|
|
217
|
+
? Object.freeze({ ...frame, context })
|
|
218
|
+
: frame)),
|
|
219
|
+
...progressFrames,
|
|
220
|
+
],
|
|
167
221
|
Option.some(next),
|
|
168
222
|
]));
|
|
169
223
|
}));
|
|
@@ -191,7 +245,7 @@ function watchSession(options, runtime, observation, sessionId, input) {
|
|
|
191
245
|
registration.setQueued(snapshot.activity.queue.state === "queued");
|
|
192
246
|
return Object.freeze({
|
|
193
247
|
snapshot,
|
|
194
|
-
changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true).pipe(Stream.ensuring(Effect.sync(registration.close))),
|
|
248
|
+
changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true, snapshot.progress).pipe(Stream.ensuring(Effect.sync(registration.close))),
|
|
195
249
|
close: Effect.sync(registration.close),
|
|
196
250
|
});
|
|
197
251
|
}))).pipe(Effect.onError(() => Effect.sync(registration.close)));
|
|
@@ -289,7 +343,9 @@ function executeSessionCommand(options, runtime, sessionId, meta, input) {
|
|
|
289
343
|
.commit(sessionOwnerOperation(runtime, decodedSessionId, (decoded) => {
|
|
290
344
|
setSessionTitle(options.store, decoded, command.title);
|
|
291
345
|
}))
|
|
292
|
-
.pipe(Effect.
|
|
346
|
+
.pipe(Effect.tap(() => Effect.sync(() => {
|
|
347
|
+
publishAgentExecutionChange(options.store);
|
|
348
|
+
})), Effect.as(undefined));
|
|
293
349
|
}
|
|
294
350
|
}));
|
|
295
351
|
}
|
|
@@ -455,6 +511,7 @@ export function acceptAddressedMail(runtime, mail, meta, reply, stageMail, sessi
|
|
|
455
511
|
const result = admit(meta === undefined ? request : { ...request, meta });
|
|
456
512
|
if (result.status !== "accepted")
|
|
457
513
|
throw new Error("Sessionless addressed mail was not admitted");
|
|
514
|
+
runtime.recordCreatedSessionDelegatorWithinTransaction(sessionId, addressed);
|
|
458
515
|
return {
|
|
459
516
|
status: "accepted",
|
|
460
517
|
commandId: result.commandId,
|
|
@@ -101,6 +101,11 @@ export type ShiitakeEffectOptions = Readonly<{
|
|
|
101
101
|
store: SqliteOwner;
|
|
102
102
|
/** Host storage-pressure check; absent never refuses for storage pressure. */
|
|
103
103
|
storagePressure?: StoragePressureCheck;
|
|
104
|
+
/** Host-owned synchronous new-work admission at the common door. */
|
|
105
|
+
admission?: Readonly<{
|
|
106
|
+
/** Check the host's current flag synchronously inside admission's transaction. */
|
|
107
|
+
assertOpen: () => void;
|
|
108
|
+
}>;
|
|
104
109
|
/** Compaction budgets; absent uses Pi's defaults for every model. */
|
|
105
110
|
compaction?: CompactionOptions;
|
|
106
111
|
/** One host lane in Shiitake's existing Watchdog, opened before recovery. */
|
|
@@ -340,9 +340,11 @@ export function openExtensionRuntime(input) {
|
|
|
340
340
|
reason: error instanceof ShiitakeError
|
|
341
341
|
? error.cause === "conflict"
|
|
342
342
|
? "conflict"
|
|
343
|
-
: error.cause === "
|
|
344
|
-
? "
|
|
345
|
-
: "
|
|
343
|
+
: error.cause === "archived"
|
|
344
|
+
? "archived"
|
|
345
|
+
: error.cause === "closed"
|
|
346
|
+
? "closed"
|
|
347
|
+
: "invalid"
|
|
346
348
|
: "invalid",
|
|
347
349
|
message: error instanceof ShiitakeError
|
|
348
350
|
? error.message
|
|
@@ -57,6 +57,14 @@ function deliverObservation(listener, reason) {
|
|
|
57
57
|
listener.reset = true;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
/** Queued views reconcile occupancy without replaying another Session's facts. */
|
|
61
|
+
function notifyQueuedViews(listeners, sourceSessionId) {
|
|
62
|
+
for (const listener of listeners)
|
|
63
|
+
if (listener.queued &&
|
|
64
|
+
!listener.settlementOnly &&
|
|
65
|
+
listener.sessionId !== sourceSessionId)
|
|
66
|
+
deliverObservation(listener, "activity");
|
|
67
|
+
}
|
|
60
68
|
/** Notify observers of one Session's durable or process-local change. */
|
|
61
69
|
export function publishOwnerObservation(store, sessionId, reason = "durable") {
|
|
62
70
|
const publishers = ownerPublishers.get(store);
|
|
@@ -131,11 +139,12 @@ export function openObservationKernel(store) {
|
|
|
131
139
|
};
|
|
132
140
|
const publish = (publication) => {
|
|
133
141
|
if (publication.kind === "agent-execution") {
|
|
134
|
-
|
|
135
|
-
if (listener.queued && !listener.settlementOnly)
|
|
136
|
-
deliverObservation(listener, "activity");
|
|
142
|
+
notifyQueuedViews(active, null);
|
|
137
143
|
return;
|
|
138
144
|
}
|
|
145
|
+
// Reuse bounded activity hints for the occupant's changing phase.
|
|
146
|
+
if (publication.sessionId !== null && publication.reason !== "settlement")
|
|
147
|
+
notifyQueuedViews(active, publication.sessionId);
|
|
139
148
|
const listeners = publication.sessionId === null
|
|
140
149
|
? active
|
|
141
150
|
: sessions.get(publication.sessionId);
|
|
@@ -139,6 +139,8 @@ function liftOptions(options, runtime) {
|
|
|
139
139
|
Object.assign(base, { meta: options.meta });
|
|
140
140
|
if (options.storagePressure !== undefined)
|
|
141
141
|
Object.assign(base, { storagePressure: options.storagePressure });
|
|
142
|
+
if (options.admission !== undefined)
|
|
143
|
+
Object.assign(base, { admission: options.admission });
|
|
142
144
|
if (options.compaction !== undefined)
|
|
143
145
|
Object.assign(base, { compaction: options.compaction });
|
|
144
146
|
const toolSource = options.tools;
|
|
@@ -7,6 +7,7 @@ import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
|
7
7
|
import type { CompactionHandoffV0, PromptSectionV0 } from "../extensions.js";
|
|
8
8
|
import { type ManualCompactionOutcome } from "./compaction/session-compaction.js";
|
|
9
9
|
import { type PreparedSessionRun, type RunContract } from "./woodstock/index.js";
|
|
10
|
+
export declare const runFinalizerGraceMs = 1000;
|
|
10
11
|
declare const toolSetupMessages: {
|
|
11
12
|
readonly missing: "Instruction capabilities have no tool runtime";
|
|
12
13
|
readonly runtime: "Tool runtime failed";
|
|
@@ -1,15 +1,32 @@
|
|
|
1
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
|
+
import * as Fiber from "effect/Fiber";
|
|
4
5
|
import * as Cause from "effect/Cause";
|
|
5
6
|
import * as Exit from "effect/Exit";
|
|
6
7
|
import * as Option from "effect/Option";
|
|
8
|
+
import * as Scope from "effect/Scope";
|
|
7
9
|
import { ShiitakeError } from "../errors.js";
|
|
8
10
|
import { compactSession, runWithCompaction, } from "./compaction/session-compaction.js";
|
|
9
11
|
import { openCompactionSettings } from "./compaction/settings.js";
|
|
10
12
|
import { acquireOwnedModel } from "./model-acquisition.js";
|
|
11
13
|
import { returnedFailureReason, unavailableRunReason, } from "./run-failure-reason.js";
|
|
12
14
|
import { isCompactCommand, openMuleRecorder, readSessionGrants, recordLiveRetryEvent, recordSandboxFailure, takeSessionControls, } from "./woodstock/index.js";
|
|
15
|
+
// A run writes its terminal before scope release. Allow one second per foreign
|
|
16
|
+
// release before detaching it: the held-model regression exercises the full
|
|
17
|
+
// grace and proves Watchdog can settle and claim the next priority-1 steer.
|
|
18
|
+
// Keep the test's eventual queue-idle budget tied to this product bound.
|
|
19
|
+
export const runFinalizerGraceMs = 1_000;
|
|
20
|
+
function boundedRunFinalizer(name, workId, release) {
|
|
21
|
+
return Effect.gen(function* () {
|
|
22
|
+
// A detached release may finish later; interrupting a finalizer itself
|
|
23
|
+
// cannot bound an uninterruptible scope.
|
|
24
|
+
const fiber = yield* Effect.forkDetach(release);
|
|
25
|
+
const finished = yield* Effect.raceFirst(Fiber.join(fiber).pipe(Effect.as(true)), Effect.sleep(runFinalizerGraceMs).pipe(Effect.as(false)));
|
|
26
|
+
if (!finished)
|
|
27
|
+
yield* Effect.logError("shiitake_run_finalizer_hung").pipe(Effect.annotateLogs({ workId, finalizer: name }));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
13
30
|
const toolSetupMessages = {
|
|
14
31
|
missing: "Instruction capabilities have no tool runtime",
|
|
15
32
|
runtime: "Tool runtime failed",
|
|
@@ -119,12 +136,12 @@ function acquiredTools(options, sessionId, workId, meta, instructions, extension
|
|
|
119
136
|
sessionGrants: () => readSessionGrants(options.store, sessionId)?.grants ?? {},
|
|
120
137
|
},
|
|
121
138
|
};
|
|
139
|
+
const toolScope = yield* Scope.make();
|
|
140
|
+
yield* Effect.addFinalizer(() => boundedRunFinalizer("tools", workId, Scope.close(toolScope, Exit.succeed(undefined))));
|
|
122
141
|
const runtime = yield* makeMycelium(toolSource.execution === undefined
|
|
123
142
|
? runtimeOptions
|
|
124
|
-
: { ...runtimeOptions, execution: toolSource.execution }).pipe(Effect.mapError(() => new ToolSetupError("runtime")));
|
|
125
|
-
const lease = yield* runtime
|
|
126
|
-
.acquire()
|
|
127
|
-
.pipe(Effect.mapError(() => new ToolSetupError("lease")));
|
|
143
|
+
: { ...runtimeOptions, execution: toolSource.execution }).pipe(Effect.provideService(Scope.Scope, toolScope), Effect.mapError(() => new ToolSetupError("runtime")));
|
|
144
|
+
const lease = yield* runtime.acquire().pipe(Effect.provideService(Scope.Scope, toolScope), Effect.mapError(() => new ToolSetupError("lease")));
|
|
128
145
|
return [lease.executeTool, ...lease.tools];
|
|
129
146
|
});
|
|
130
147
|
}
|
|
@@ -137,11 +154,16 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
137
154
|
});
|
|
138
155
|
const releaseInstructions = instructions.release;
|
|
139
156
|
if (releaseInstructions !== undefined)
|
|
140
|
-
yield* Effect.addFinalizer(() => Effect.sync(releaseInstructions));
|
|
141
|
-
const acquired = yield* acquireOwnedModel(options.model
|
|
157
|
+
yield* Effect.addFinalizer(() => boundedRunFinalizer("instructions", prepared.command.entry.workId, Effect.sync(releaseInstructions)));
|
|
158
|
+
const acquired = yield* acquireOwnedModel(options.model
|
|
159
|
+
.acquire({
|
|
142
160
|
sessionId: prepared.command.sessionId,
|
|
143
161
|
preference: prepared.modelPreference,
|
|
144
|
-
})
|
|
162
|
+
})
|
|
163
|
+
.pipe(Effect.map((model) => ({
|
|
164
|
+
...model,
|
|
165
|
+
release: boundedRunFinalizer("model", prepared.command.entry.workId, model.release),
|
|
166
|
+
}))));
|
|
145
167
|
const tools = yield* acquiredTools(options, prepared.command.sessionId, prepared.command.entry.workId, prepared.command.entry.meta, instructions, extensionModules, extensionFileTools);
|
|
146
168
|
const accepted = Object.freeze({
|
|
147
169
|
sessionId: prepared.command.sessionId,
|
|
@@ -9,10 +9,23 @@ import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
|
9
9
|
import { type AdmittedExtensionV0 } from "./extension-roster.js";
|
|
10
10
|
import { type ExtensionRuntime } from "./extension-runtime.js";
|
|
11
11
|
import { type RecordAcceptedCancellation, type StorageAdmission } from "./woodstock/index.js";
|
|
12
|
+
/** Owner-local archive blocker read within the caller's SQLite transaction. */
|
|
13
|
+
export type OwnerBusy = Readonly<{
|
|
14
|
+
kind: "session";
|
|
15
|
+
sessionId: string;
|
|
16
|
+
commandId: string;
|
|
17
|
+
}> | Readonly<{
|
|
18
|
+
kind: "delegation";
|
|
19
|
+
agentId: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
commandId: string;
|
|
22
|
+
}>;
|
|
12
23
|
/** Bound lifecycle and owner-transaction doors used by the public coordinator. */
|
|
13
24
|
export type RuntimeCoordinator = Readonly<{
|
|
14
25
|
prepareConfiguration: import("./effect-contracts.js").ShiitakeEffectOwner["prepareConfiguration"];
|
|
15
26
|
activeSessions: Effect.Effect<readonly string[]>;
|
|
27
|
+
/** No awaits: use inside the same transaction as the host's archive flag. */
|
|
28
|
+
busySync: () => OwnerBusy | undefined;
|
|
16
29
|
runningWork: (jobId: string) => Effect.Effect<AgentRunningWork | null, ShiitakeError>;
|
|
17
30
|
assertOpen: Effect.Effect<void, ShiitakeError>;
|
|
18
31
|
/** Rostered v1 extensions bound to core's doors and tables. */
|
|
@@ -22,6 +35,7 @@ export type RuntimeCoordinator = Readonly<{
|
|
|
22
35
|
door: AdmissionDoor;
|
|
23
36
|
/** Reserve a Session within the admission door's enclosing transaction. */
|
|
24
37
|
createSessionWithinTransaction: (requestId: string) => string;
|
|
38
|
+
recordCreatedSessionDelegatorWithinTransaction: (sessionId: string, mail: import("@fungi.computer/bird-dog").BirdDogMail) => void;
|
|
25
39
|
/** Write new-Session grants through the enclosing admission transaction. */
|
|
26
40
|
stageSessionGrantsWithinTransaction: (sessionId: string, delivery: GrantsDelivery) => void;
|
|
27
41
|
/** Storage-pressure check for new Sessions and new prompts. */
|
|
@@ -3,6 +3,8 @@ 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 { readSessionPhase } from "./session-status.js";
|
|
7
|
+
import { readOutstandingDelegationSync } from "../fleet.js";
|
|
6
8
|
import { publishAgentExecutionChange } from "./observation-kernel.js";
|
|
7
9
|
import { ShiitakeError } from "../errors.js";
|
|
8
10
|
import { agentTurnRecovery, executeAcceptedRun, } from "./accepted-run-executor.js";
|
|
@@ -11,7 +13,7 @@ import { openCompactionSettings } from "./compaction/settings.js";
|
|
|
11
13
|
import { admitExtensionRosterV0, } from "./extension-roster.js";
|
|
12
14
|
import { coreConsumer, reportConsumer, openExtensionRuntime, } from "./extension-runtime.js";
|
|
13
15
|
import { reconcileRuntime } from "./runtime-reconciliation.js";
|
|
14
|
-
import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
|
|
16
|
+
import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, recordSessionDelegator, projectSessionRun, openStorageAdmission, readCursor, readSessionInboxPage, } from "./woodstock/index.js";
|
|
15
17
|
function runtimeFailure(message) {
|
|
16
18
|
return new ShiitakeError("runtime", "rejected", message);
|
|
17
19
|
}
|
|
@@ -119,13 +121,42 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
119
121
|
? error
|
|
120
122
|
: runtimeFailure("Storage budget is unavailable"),
|
|
121
123
|
});
|
|
124
|
+
const admitNew = () => {
|
|
125
|
+
options.admission?.assertOpen();
|
|
126
|
+
storage.admitNew();
|
|
127
|
+
};
|
|
128
|
+
const busySync = () => {
|
|
129
|
+
let after;
|
|
130
|
+
do {
|
|
131
|
+
const page = readSessionInboxPage(options.store, after);
|
|
132
|
+
for (const session of page.sessions) {
|
|
133
|
+
if (session.state === "unavailable")
|
|
134
|
+
throw new ShiitakeError("store", "unavailable", `Session ${session.sessionId} is unreadable`);
|
|
135
|
+
const head = watchdog.transactional.readQueueHead(session.sessionId);
|
|
136
|
+
if (head.state !== "idle")
|
|
137
|
+
return {
|
|
138
|
+
kind: "session",
|
|
139
|
+
sessionId: session.sessionId,
|
|
140
|
+
commandId: head.jobId,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
after = page.next;
|
|
144
|
+
} while (after !== undefined);
|
|
145
|
+
const outstanding = readOutstandingDelegationSync(options.store);
|
|
146
|
+
return outstanding === undefined
|
|
147
|
+
? undefined
|
|
148
|
+
: { kind: "delegation", ...outstanding };
|
|
149
|
+
};
|
|
122
150
|
const door = openAdmissionDoor({
|
|
123
151
|
owner: options.store,
|
|
124
|
-
admitNew
|
|
152
|
+
admitNew,
|
|
125
153
|
hostMeta: options.meta,
|
|
126
154
|
contract,
|
|
127
155
|
enqueue: (work, mail) => {
|
|
128
|
-
const input = {
|
|
156
|
+
const input = {
|
|
157
|
+
work: { ...work, recovery: agentTurnRecovery },
|
|
158
|
+
delivery: work.delivery,
|
|
159
|
+
};
|
|
129
160
|
watchdog.transactional.enqueueAcceptedJob(agentJobFromInput(mail === undefined ? input : { ...input, mail }));
|
|
130
161
|
},
|
|
131
162
|
readQueueHead: (queue) => watchdog.transactional.readQueueHead(queue),
|
|
@@ -248,11 +279,24 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
248
279
|
};
|
|
249
280
|
}),
|
|
250
281
|
activeSessions: Effect.sync(() => [...activeSessions]),
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
282
|
+
busySync,
|
|
283
|
+
runningWork: (jobId) => watchdog.read(jobId).pipe(Effect.flatMap((result) => {
|
|
284
|
+
const work = result.status === "found"
|
|
285
|
+
? runningSlots.get(result.job.lane === options.hostJobs?.lane
|
|
286
|
+
? "host"
|
|
287
|
+
: "serial")
|
|
288
|
+
: undefined;
|
|
289
|
+
if (work === undefined)
|
|
290
|
+
return Effect.succeed(null);
|
|
291
|
+
if (work.kind === "host")
|
|
292
|
+
return Effect.succeed({ phase: "thinking" });
|
|
293
|
+
return Effect.try({
|
|
294
|
+
try: () => ({
|
|
295
|
+
phase: readSessionPhase(options.store, work.sessionId, watchdog.transactional.readQueueHead(work.sessionId)),
|
|
296
|
+
}),
|
|
297
|
+
catch: (cause) => new ShiitakeError("runtime", "rejected", "Running phase is unavailable", undefined, cause),
|
|
298
|
+
});
|
|
299
|
+
}), Effect.mapError((cause) => new ShiitakeError("runtime", "rejected", "Running work is unavailable", undefined, cause))),
|
|
256
300
|
extensions,
|
|
257
301
|
commit: bound.commit,
|
|
258
302
|
assertOpen: Effect.suspend(() => lifecycleState === "open"
|
|
@@ -260,11 +304,14 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
260
304
|
: Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"))),
|
|
261
305
|
beginClose,
|
|
262
306
|
door,
|
|
263
|
-
createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId,
|
|
307
|
+
createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId, admitNew),
|
|
308
|
+
recordCreatedSessionDelegatorWithinTransaction: (sessionId, mail) => {
|
|
309
|
+
recordSessionDelegator(options.store, sessionId, mail);
|
|
310
|
+
},
|
|
264
311
|
stageSessionGrantsWithinTransaction: (sessionId, delivery) => {
|
|
265
312
|
deliverSessionGrants(options.store, sessionId, delivery);
|
|
266
313
|
},
|
|
267
|
-
storage,
|
|
314
|
+
storage: { admitNew },
|
|
268
315
|
recordAcceptedCancellation: ({ jobId, cancellationId, onlyIfPending }) => watchdog.transactional.recordCancellationIntent({
|
|
269
316
|
jobId,
|
|
270
317
|
cancellationId,
|
|
@@ -8,12 +8,15 @@ import type { Accepted } from "../client.js";
|
|
|
8
8
|
import type { GrantsDelivery } from "./contracts.js";
|
|
9
9
|
import { ShiitakeError } from "../errors.js";
|
|
10
10
|
import type { ShiitakeEffectOptions, ShiitakeEffectOwner } from "./effect-contracts.js";
|
|
11
|
+
import type { OwnerBusy } from "./runtime-coordinator.js";
|
|
11
12
|
/** Private host composition with the addressed-mail ingress kept off Session. */
|
|
12
13
|
export type RuntimeCore = Readonly<{
|
|
13
14
|
agent: ShiitakeEffectOwner;
|
|
14
15
|
readSettlement: (sessionId: string, commandId: string) => Effect.Effect<CommandSettlement | undefined, ShiitakeError>;
|
|
15
16
|
/** Owner-local execution projection; never served by the remote client. */
|
|
16
17
|
activeSessions: Effect.Effect<readonly string[]>;
|
|
18
|
+
/** Synchronous archive blocker; the host supplies the enclosing transaction. */
|
|
19
|
+
busySync: () => OwnerBusy | undefined;
|
|
17
20
|
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply, stageAcceptedNotice?: (mail: BirdDogMail) => void, sessionGrantDelivery?: (sessionId: string) => GrantsDelivery) => Effect.Effect<Accepted, ShiitakeError>;
|
|
18
21
|
}>;
|
|
19
22
|
/** Open Runtime Core, optionally joining its owner-local BirdDog mail room. */
|
|
@@ -34,6 +34,7 @@ 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
|
+
busySync: runtime.busySync,
|
|
37
38
|
acceptMail: (mail, meta, reply, stageAcceptedNotice, sessionGrantDelivery) => acceptAddressedMail(runtime, mail, meta, reply, stageAcceptedNotice ??
|
|
38
39
|
((notice) => {
|
|
39
40
|
if (mailRoom?.postMail === undefined)
|
|
@@ -155,7 +155,10 @@ function settleTargetedControls(store, watchdog, job) {
|
|
|
155
155
|
outcome,
|
|
156
156
|
diagnostic: job.failureReason,
|
|
157
157
|
enqueue: (work) => {
|
|
158
|
-
watchdog.transactional.enqueueAcceptedJob(agentJobFromInput({
|
|
158
|
+
watchdog.transactional.enqueueAcceptedJob(agentJobFromInput({
|
|
159
|
+
work: { ...work, recovery: agentTurnRecovery },
|
|
160
|
+
delivery: work.delivery,
|
|
161
|
+
}));
|
|
159
162
|
},
|
|
160
163
|
});
|
|
161
164
|
});
|
|
@@ -2,9 +2,9 @@ import * as Effect from "effect/Effect";
|
|
|
2
2
|
import type { SessionContext } from "../client.js";
|
|
3
3
|
import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* null, never guessed.
|
|
5
|
+
* Read the stored figure synchronously with the Session baseline. Only model
|
|
6
|
+
* description remains in the returned Effect, outside the SQLite transaction
|
|
7
|
+
* and the request's ancestry scope. Undefined without readable context; a model
|
|
8
|
+
* window the host cannot describe is null, never guessed.
|
|
9
9
|
*/
|
|
10
|
-
export declare function
|
|
10
|
+
export declare function prepareSessionContext(options: ShiitakeEffectOptions, sessionId: string, cursor: string | null, cache?: boolean): Effect.Effect<SessionContext | undefined>;
|