@fungi.computer/shiitake 0.5.0 → 0.7.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 +94 -2
- package/dist/agent-control.js +147 -26
- package/dist/client-entry.d.ts +20 -0
- package/dist/client-entry.js +5 -0
- package/dist/client-visitor.d.ts +33 -0
- package/dist/client-visitor.js +231 -0
- package/dist/client-wire.d.ts +9 -0
- package/dist/client-wire.js +24 -3
- package/dist/client.d.ts +27 -6
- package/dist/client.js +23 -0
- package/dist/cloudflare.d.ts +20 -0
- package/dist/cloudflare.js +4 -1
- package/dist/delegation.d.ts +79 -53
- package/dist/delegation.js +26 -21
- package/dist/effect.d.ts +26 -1
- package/dist/effect.js +14 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -0
- package/dist/extensions.d.ts +22 -3
- package/dist/extensions.js +1 -1
- package/dist/fleet.d.ts +25 -2
- package/dist/fleet.js +108 -24
- package/dist/hosted.js +3 -0
- package/dist/http-routes.d.ts +23 -9
- package/dist/http.d.ts +1 -0
- package/dist/http.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal/accepted-run-executor.js +2 -1
- package/dist/internal/admission-door.d.ts +6 -1
- package/dist/internal/admission-door.js +33 -3
- package/dist/internal/contracts.d.ts +19 -1
- package/dist/internal/coordinator.js +115 -52
- package/dist/internal/effect-client.js +1 -1
- package/dist/internal/effect-contracts.d.ts +9 -2
- package/dist/internal/extension-command-registry.d.ts +2 -1
- package/dist/internal/extension-command-registry.js +3 -1
- package/dist/internal/extension-runtime.d.ts +3 -1
- package/dist/internal/extension-runtime.js +18 -8
- package/dist/internal/model-acquisition.js +2 -0
- package/dist/internal/observation-kernel.d.ts +2 -0
- package/dist/internal/observation-kernel.js +17 -6
- package/dist/internal/promise-facade.js +2 -0
- package/dist/internal/run-scope.d.ts +14 -0
- package/dist/internal/run-scope.js +58 -15
- package/dist/internal/runtime-coordinator.d.ts +14 -0
- package/dist/internal/runtime-coordinator.js +60 -11
- package/dist/internal/runtime-core.d.ts +3 -0
- package/dist/internal/runtime-core.js +1 -0
- package/dist/internal/runtime-reconciliation.js +123 -25
- package/dist/internal/session-context.d.ts +5 -5
- package/dist/internal/session-context.js +80 -83
- package/dist/internal/session-status.d.ts +2 -2
- package/dist/internal/session-status.js +5 -3
- package/dist/internal/woodstock/agent-reply.d.ts +5 -0
- package/dist/internal/woodstock/agent-reply.js +3 -0
- package/dist/internal/woodstock/client-projection.js +13 -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 +7 -3
- package/dist/internal/woodstock/index.js +23 -6
- package/dist/internal/woodstock/progress.d.ts +11 -1
- package/dist/internal/woodstock/progress.js +126 -25
- package/dist/internal/woodstock/session-delegator.d.ts +13 -0
- package/dist/internal/woodstock/session-delegator.js +44 -0
- package/dist/internal/woodstock/session-entry.d.ts +12 -2
- package/dist/internal/woodstock/session-store.d.ts +16 -2
- package/dist/internal/woodstock/session-store.js +349 -66
- package/dist/internal/woodstock/work-senders.d.ts +32 -0
- package/dist/internal/woodstock/work-senders.js +70 -0
- package/dist/node.js +11 -2
- package/dist/remote-client.js +7 -2
- package/dist/server-visitor-stream.d.ts +13 -0
- package/dist/server-visitor-stream.js +124 -0
- package/dist/session-attention.d.ts +30 -0
- package/dist/session-attention.js +14 -0
- package/docs/api/agent-control.md +212 -15
- package/docs/api/client-entry.md +3 -0
- package/docs/api/client.md +209 -5
- package/docs/api/cloudflare.md +23 -1
- package/docs/api/delegation.md +284 -77
- package/docs/api/effect.md +100 -12
- package/docs/api/extensions.md +73 -12
- package/docs/api/fleet.md +138 -25
- package/docs/api/http.md +28 -0
- package/docs/api/index.md +43 -17
- package/docs/api/testing.md +9 -7
- package/package.json +10 -6
|
@@ -1,17 +1,29 @@
|
|
|
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";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
// A run writes its terminal before scope release. Give each foreign release
|
|
16
|
+
// one second to clean up without blocking the next Watchdog job indefinitely.
|
|
17
|
+
const runFinalizerGraceMs = 1_000;
|
|
18
|
+
function boundedRunFinalizer(name, workId, release) {
|
|
19
|
+
return Effect.gen(function* () {
|
|
20
|
+
// A detached release may finish later; interrupting a finalizer itself
|
|
21
|
+
// cannot bound an uninterruptible scope.
|
|
22
|
+
const fiber = yield* Effect.forkDetach(release);
|
|
23
|
+
const finished = yield* Effect.raceFirst(Fiber.join(fiber).pipe(Effect.as(true)), Effect.sleep(runFinalizerGraceMs).pipe(Effect.as(false)));
|
|
24
|
+
if (!finished)
|
|
25
|
+
yield* Effect.logError("shiitake_run_finalizer_hung").pipe(Effect.annotateLogs({ workId, finalizer: name }));
|
|
26
|
+
});
|
|
15
27
|
}
|
|
16
28
|
const toolSetupMessages = {
|
|
17
29
|
missing: "Instruction capabilities have no tool runtime",
|
|
@@ -29,6 +41,28 @@ export class ToolSetupError extends ShiitakeError {
|
|
|
29
41
|
return toolSetupMessages[this.phase];
|
|
30
42
|
}
|
|
31
43
|
}
|
|
44
|
+
/** Closed reasons a rejected run names, never a foreign error's own text. */
|
|
45
|
+
const runRejectedDiagnostics = {
|
|
46
|
+
model: "model-failed",
|
|
47
|
+
tool: "tool-failed",
|
|
48
|
+
control: "control-failed",
|
|
49
|
+
listener: "listener-failed",
|
|
50
|
+
};
|
|
51
|
+
/** A run Mule ended with a non-model error; it names a closed reason. */
|
|
52
|
+
export class RunRejectedError extends ShiitakeError {
|
|
53
|
+
reason;
|
|
54
|
+
constructor(reason, underlying) {
|
|
55
|
+
super("runtime", "rejected", "Mule rejected the durable command run", undefined, underlying);
|
|
56
|
+
this.reason = reason;
|
|
57
|
+
}
|
|
58
|
+
get diagnostic() {
|
|
59
|
+
return runRejectedDiagnostics[this.reason];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Name a non-model Mule error by its closed reason. */
|
|
63
|
+
function rejectedRun(error) {
|
|
64
|
+
return new RunRejectedError(error.phase, error);
|
|
65
|
+
}
|
|
32
66
|
/**
|
|
33
67
|
* Map Mule's retry transitions onto Woodstock's retained `live_retry` facts.
|
|
34
68
|
* A started attempt ends the backoff countdown before streaming resumes.
|
|
@@ -100,12 +134,12 @@ function acquiredTools(options, sessionId, workId, meta, instructions, extension
|
|
|
100
134
|
sessionGrants: () => readSessionGrants(options.store, sessionId)?.grants ?? {},
|
|
101
135
|
},
|
|
102
136
|
};
|
|
137
|
+
const toolScope = yield* Scope.make();
|
|
138
|
+
yield* Effect.addFinalizer(() => boundedRunFinalizer("tools", workId, Scope.close(toolScope, Exit.succeed(undefined))));
|
|
103
139
|
const runtime = yield* makeMycelium(toolSource.execution === undefined
|
|
104
140
|
? runtimeOptions
|
|
105
|
-
: { ...runtimeOptions, execution: toolSource.execution }).pipe(Effect.mapError(() => new ToolSetupError("runtime")));
|
|
106
|
-
const lease = yield* runtime
|
|
107
|
-
.acquire()
|
|
108
|
-
.pipe(Effect.mapError(() => new ToolSetupError("lease")));
|
|
141
|
+
: { ...runtimeOptions, execution: toolSource.execution }).pipe(Effect.provideService(Scope.Scope, toolScope), Effect.mapError(() => new ToolSetupError("runtime")));
|
|
142
|
+
const lease = yield* runtime.acquire().pipe(Effect.provideService(Scope.Scope, toolScope), Effect.mapError(() => new ToolSetupError("lease")));
|
|
109
143
|
return [lease.executeTool, ...lease.tools];
|
|
110
144
|
});
|
|
111
145
|
}
|
|
@@ -118,11 +152,16 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
118
152
|
});
|
|
119
153
|
const releaseInstructions = instructions.release;
|
|
120
154
|
if (releaseInstructions !== undefined)
|
|
121
|
-
yield* Effect.addFinalizer(() => Effect.sync(releaseInstructions));
|
|
122
|
-
const acquired = yield* acquireOwnedModel(options.model
|
|
155
|
+
yield* Effect.addFinalizer(() => boundedRunFinalizer("instructions", prepared.command.entry.workId, Effect.sync(releaseInstructions)));
|
|
156
|
+
const acquired = yield* acquireOwnedModel(options.model
|
|
157
|
+
.acquire({
|
|
123
158
|
sessionId: prepared.command.sessionId,
|
|
124
159
|
preference: prepared.modelPreference,
|
|
125
|
-
})
|
|
160
|
+
})
|
|
161
|
+
.pipe(Effect.map((model) => ({
|
|
162
|
+
...model,
|
|
163
|
+
release: boundedRunFinalizer("model", prepared.command.entry.workId, model.release),
|
|
164
|
+
}))));
|
|
126
165
|
const tools = yield* acquiredTools(options, prepared.command.sessionId, prepared.command.entry.workId, prepared.command.entry.meta, instructions, extensionModules, extensionFileTools);
|
|
127
166
|
const accepted = Object.freeze({
|
|
128
167
|
sessionId: prepared.command.sessionId,
|
|
@@ -229,22 +268,26 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
229
268
|
acquired.failureReason?.(exit.value.message) ??
|
|
230
269
|
returnedFailureReason(exit.value);
|
|
231
270
|
failureReason = (failedModelAttempts > 1 &&
|
|
232
|
-
exit.value.modelLimitReason === undefined
|
|
271
|
+
exit.value.modelLimitReason === undefined &&
|
|
272
|
+
acquired.rejection?.(exit.value.message)?.code !==
|
|
273
|
+
"usage-limit-reached"
|
|
233
274
|
? `The model failed ${String(failedModelAttempts)} times on this conversation: ${cause}`
|
|
234
275
|
: cause).slice(0, 200);
|
|
235
276
|
return recorder(failedEnd, failureReason);
|
|
236
277
|
}
|
|
237
278
|
const error = Cause.findErrorOption(exit.cause);
|
|
238
|
-
return recorder(failedEnd, Option.
|
|
239
|
-
?
|
|
240
|
-
: unavailableRunReason({
|
|
279
|
+
return recorder(failedEnd, Option.isNone(error)
|
|
280
|
+
? unavailableRunReason({
|
|
241
281
|
runId: failedEnd.run.value,
|
|
242
282
|
provider: acquired.model.provider,
|
|
243
283
|
model: acquired.model.id,
|
|
244
|
-
})
|
|
284
|
+
})
|
|
285
|
+
: error.value.phase === "model"
|
|
286
|
+
? `model:rejected: ${error.value.message}`.slice(0, 200)
|
|
287
|
+
: `runtime:rejected: ${rejectedRun(error.value).diagnostic}`);
|
|
245
288
|
}), Effect.mapError((error) => error.phase === "model"
|
|
246
289
|
? new ShiitakeError("model", "rejected", error.message, undefined, error)
|
|
247
|
-
:
|
|
290
|
+
: rejectedRun(error)));
|
|
248
291
|
},
|
|
249
292
|
});
|
|
250
293
|
return { run, failureReason };
|
|
@@ -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,15 +3,16 @@ 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 { readOutstandingDelegationSync } from "../fleet.js";
|
|
6
7
|
import { publishAgentExecutionChange } from "./observation-kernel.js";
|
|
7
8
|
import { ShiitakeError } from "../errors.js";
|
|
8
9
|
import { agentTurnRecovery, executeAcceptedRun, } from "./accepted-run-executor.js";
|
|
9
10
|
import { openAdmissionDoor } from "./admission-door.js";
|
|
10
11
|
import { openCompactionSettings } from "./compaction/settings.js";
|
|
11
12
|
import { admitExtensionRosterV0, } from "./extension-roster.js";
|
|
12
|
-
import { coreConsumer, openExtensionRuntime, } from "./extension-runtime.js";
|
|
13
|
+
import { coreConsumer, reportConsumer, openExtensionRuntime, } from "./extension-runtime.js";
|
|
13
14
|
import { reconcileRuntime } from "./runtime-reconciliation.js";
|
|
14
|
-
import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
|
|
15
|
+
import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, recordSessionDelegator, projectSessionRun, openStorageAdmission, readCursor, readSessionInboxPage, readSessionSummary, } from "./woodstock/index.js";
|
|
15
16
|
function runtimeFailure(message) {
|
|
16
17
|
return new ShiitakeError("runtime", "rejected", message);
|
|
17
18
|
}
|
|
@@ -119,13 +120,42 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
119
120
|
? error
|
|
120
121
|
: runtimeFailure("Storage budget is unavailable"),
|
|
121
122
|
});
|
|
123
|
+
const admitNew = () => {
|
|
124
|
+
options.admission?.assertOpen();
|
|
125
|
+
storage.admitNew();
|
|
126
|
+
};
|
|
127
|
+
const busySync = () => {
|
|
128
|
+
let after;
|
|
129
|
+
do {
|
|
130
|
+
const page = readSessionInboxPage(options.store, after);
|
|
131
|
+
for (const session of page.sessions) {
|
|
132
|
+
if (session.state === "unavailable")
|
|
133
|
+
throw new ShiitakeError("store", "unavailable", `Session ${session.sessionId} is unreadable`);
|
|
134
|
+
const head = watchdog.transactional.readQueueHead(session.sessionId);
|
|
135
|
+
if (head.state !== "idle")
|
|
136
|
+
return {
|
|
137
|
+
kind: "session",
|
|
138
|
+
sessionId: session.sessionId,
|
|
139
|
+
commandId: head.jobId,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
after = page.next;
|
|
143
|
+
} while (after !== undefined);
|
|
144
|
+
const outstanding = readOutstandingDelegationSync(options.store);
|
|
145
|
+
return outstanding === undefined
|
|
146
|
+
? undefined
|
|
147
|
+
: { kind: "delegation", ...outstanding };
|
|
148
|
+
};
|
|
122
149
|
const door = openAdmissionDoor({
|
|
123
150
|
owner: options.store,
|
|
124
|
-
admitNew
|
|
151
|
+
admitNew,
|
|
125
152
|
hostMeta: options.meta,
|
|
126
153
|
contract,
|
|
127
154
|
enqueue: (work, mail) => {
|
|
128
|
-
const input = {
|
|
155
|
+
const input = {
|
|
156
|
+
work: { ...work, recovery: agentTurnRecovery },
|
|
157
|
+
delivery: work.delivery,
|
|
158
|
+
};
|
|
129
159
|
watchdog.transactional.enqueueAcceptedJob(agentJobFromInput(mail === undefined ? input : { ...input, mail }));
|
|
130
160
|
},
|
|
131
161
|
readQueueHead: (queue) => watchdog.transactional.readQueueHead(queue),
|
|
@@ -141,6 +171,12 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
141
171
|
options.store.transactionSync(() => {
|
|
142
172
|
advanceCursor(options.store, coreConsumer, 0);
|
|
143
173
|
});
|
|
174
|
+
// A store that predates reports starts them at the core cursor
|
|
175
|
+
// instead of reporting its history.
|
|
176
|
+
if (readCursor(options.store, reportConsumer) === undefined)
|
|
177
|
+
options.store.transactionSync(() => {
|
|
178
|
+
advanceCursor(options.store, reportConsumer, readCursor(options.store, coreConsumer) ?? 0);
|
|
179
|
+
});
|
|
144
180
|
},
|
|
145
181
|
catch: () => new ShiitakeError("store", "unavailable", "Extension store is unavailable"),
|
|
146
182
|
});
|
|
@@ -165,6 +201,7 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
165
201
|
const cursors = yield* Effect.try({
|
|
166
202
|
try: () => [
|
|
167
203
|
readCursor(options.store, coreConsumer) ?? 0,
|
|
204
|
+
readCursor(options.store, reportConsumer) ?? 0,
|
|
168
205
|
...extensions.settledConsumers().map(({ cursor }) => cursor),
|
|
169
206
|
],
|
|
170
207
|
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
@@ -241,11 +278,20 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
241
278
|
};
|
|
242
279
|
}),
|
|
243
280
|
activeSessions: Effect.sync(() => [...activeSessions]),
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
281
|
+
busySync,
|
|
282
|
+
runningWork: (jobId) => watchdog.read(jobId).pipe(Effect.flatMap((result) => {
|
|
283
|
+
const work = result.status === "found"
|
|
284
|
+
? runningSlots.get(result.job.lane === options.hostJobs?.lane
|
|
285
|
+
? "host"
|
|
286
|
+
: "serial")
|
|
287
|
+
: undefined;
|
|
288
|
+
if (work === undefined || work.kind === "host")
|
|
289
|
+
return Effect.succeed(work ?? null);
|
|
290
|
+
return readSessionSummary(options.store, work.sessionId).pipe(Effect.map((summary) => ({
|
|
291
|
+
...work,
|
|
292
|
+
title: summary?.title ?? "",
|
|
293
|
+
})));
|
|
294
|
+
}), Effect.mapError((cause) => new ShiitakeError("runtime", "rejected", "Running work is unavailable", undefined, cause))),
|
|
249
295
|
extensions,
|
|
250
296
|
commit: bound.commit,
|
|
251
297
|
assertOpen: Effect.suspend(() => lifecycleState === "open"
|
|
@@ -253,11 +299,14 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
253
299
|
: Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"))),
|
|
254
300
|
beginClose,
|
|
255
301
|
door,
|
|
256
|
-
createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId,
|
|
302
|
+
createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId, admitNew),
|
|
303
|
+
recordCreatedSessionDelegatorWithinTransaction: (sessionId, mail) => {
|
|
304
|
+
recordSessionDelegator(options.store, sessionId, mail);
|
|
305
|
+
},
|
|
257
306
|
stageSessionGrantsWithinTransaction: (sessionId, delivery) => {
|
|
258
307
|
deliverSessionGrants(options.store, sessionId, delivery);
|
|
259
308
|
},
|
|
260
|
-
storage,
|
|
309
|
+
storage: { admitNew },
|
|
261
310
|
recordAcceptedCancellation: ({ jobId, cancellationId, onlyIfPending }) => watchdog.transactional.recordCancellationIntent({
|
|
262
311
|
jobId,
|
|
263
312
|
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)
|
|
@@ -5,21 +5,19 @@ import * as Schema from "effect/Schema";
|
|
|
5
5
|
import { agentTurnRecovery } from "./accepted-run-executor.js";
|
|
6
6
|
import { ShiitakeError } from "../errors.js";
|
|
7
7
|
import { ExtensionId } from "../extension-contract.js";
|
|
8
|
-
import { coreConsumer } from "./extension-runtime.js";
|
|
8
|
+
import { coreConsumer, reportConsumer, } from "./extension-runtime.js";
|
|
9
9
|
import { publishOwnerObservation } from "./observation-kernel.js";
|
|
10
|
-
import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSettlementMeta, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
|
|
10
|
+
import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSessionDelegator, readSettlementMeta, readWaitingOn, readWaitingOnSender, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
|
|
11
11
|
const pageSize = 64;
|
|
12
12
|
function runtimeFailure(message) {
|
|
13
13
|
return new ShiitakeError("runtime", "rejected", message);
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/** BirdDog's settlement facts for one settled job, without its correspondents. */
|
|
16
|
+
function settlementFacts(store, job,
|
|
17
|
+
/** The open items this settlement names as waiting on its recipient. */
|
|
18
|
+
waitingOnYou) {
|
|
18
19
|
return Effect.gen(function* () {
|
|
19
|
-
const payload = yield*
|
|
20
|
-
try: () => decodeAgentJob(job),
|
|
21
|
-
catch: () => runtimeFailure("Agent work payload is invalid"),
|
|
22
|
-
});
|
|
20
|
+
const payload = yield* decodeJob(job);
|
|
23
21
|
const content = job.outcome === "cancelled"
|
|
24
22
|
? ""
|
|
25
23
|
: yield* readSettlementContent(store, payload.work);
|
|
@@ -27,14 +25,103 @@ function fanOutSettlement(store, mailRoom, job) {
|
|
|
27
25
|
const diagnostic = job.outcome === "completed" || job.outcome === "cancelled"
|
|
28
26
|
? undefined
|
|
29
27
|
: yield* readSettlementDiagnostic(store, payload.work);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
let settled = { job, content };
|
|
29
|
+
if (diagnostic !== undefined)
|
|
30
|
+
settled = { ...settled, diagnostic };
|
|
31
|
+
// A cancelled run names nothing; what it left open is the canceller's to see.
|
|
32
|
+
const waiting = job.outcome === "cancelled" ? [] : yield* waitingOnYou;
|
|
33
|
+
if (waiting.length > 0)
|
|
34
|
+
settled = { ...settled, waitingOnYou: waiting };
|
|
35
|
+
return meta === undefined ? settled : { ...settled, meta };
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
function decodeJob(job) {
|
|
39
|
+
return Effect.try({
|
|
40
|
+
try: () => decodeAgentJob(job),
|
|
41
|
+
catch: () => runtimeFailure("Agent work payload is invalid"),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function fanOut(mailRoom, settled) {
|
|
45
|
+
return mailRoom
|
|
46
|
+
.fanOut(settled)
|
|
47
|
+
.pipe(Effect.mapError(() => runtimeFailure("Agent reply fan-out is unavailable")));
|
|
48
|
+
}
|
|
49
|
+
/** Reply to the delegating mail that admitted this work, if it asked for one. */
|
|
50
|
+
function replyToMail(store, mailRoom, job) {
|
|
51
|
+
if (mailRoom === undefined)
|
|
52
|
+
return Effect.void;
|
|
53
|
+
return Effect.gen(function* () {
|
|
54
|
+
const payload = yield* decodeJob(job);
|
|
55
|
+
if ((payload.mail?.replyTo.length ?? 0) === 0)
|
|
56
|
+
return;
|
|
57
|
+
// The reply goes to the mail's sender: name what waits on that sender.
|
|
58
|
+
const waiting = Effect.try({
|
|
59
|
+
try: () => readWaitingOnSender(store, payload.work.sessionId, payload.work.commandFactId),
|
|
60
|
+
catch: () => runtimeFailure("Session attention is unavailable"),
|
|
61
|
+
});
|
|
62
|
+
yield* fanOut(mailRoom, yield* settlementFacts(store, job, waiting));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Report work no delegating mail answered to the Session's delegator of
|
|
67
|
+
* record. It runs after the extensions have seen the settlement, so a goal
|
|
68
|
+
* or loop that admitted the next turn keeps the Session busy and the stretch
|
|
69
|
+
* of turns reports once, when the Session finally rests. Another Agent's
|
|
70
|
+
* original mail is that Agent's correspondence and never reported here.
|
|
71
|
+
*/
|
|
72
|
+
function reportToDelegator(store, watchdog, mailRoom, job) {
|
|
73
|
+
if (mailRoom === undefined)
|
|
74
|
+
return Effect.void;
|
|
75
|
+
return Effect.gen(function* () {
|
|
76
|
+
const payload = yield* decodeJob(job);
|
|
77
|
+
const mail = payload.mail;
|
|
78
|
+
if (mail !== undefined && mail.replyTo.length > 0)
|
|
79
|
+
return;
|
|
80
|
+
const head = yield* watchdog
|
|
81
|
+
.readQueueHead(job.queue)
|
|
82
|
+
.pipe(Effect.mapError(() => runtimeFailure("Session queue head is unavailable")));
|
|
83
|
+
if (head.state !== "idle")
|
|
84
|
+
return;
|
|
85
|
+
const delegator = yield* Effect.try({
|
|
86
|
+
try: () => readSessionDelegator(store, payload.work.sessionId),
|
|
87
|
+
catch: () => runtimeFailure("Session delegator is unavailable"),
|
|
88
|
+
});
|
|
89
|
+
if (delegator === undefined)
|
|
90
|
+
return;
|
|
91
|
+
const from = mail?.reply === undefined ? mail?.from : undefined;
|
|
92
|
+
const anotherAgentsMail = from !== undefined &&
|
|
93
|
+
!(from.kind === "agent" &&
|
|
94
|
+
delegator.replyTo.some(({ agentId }) => agentId === from.agentId));
|
|
95
|
+
if (anotherAgentsMail)
|
|
96
|
+
return;
|
|
97
|
+
// The report goes to the delegator: name what waits on it.
|
|
98
|
+
const waiting = Effect.try({
|
|
99
|
+
try: () => readWaitingOn(store, payload.work.sessionId, delegator.replyTo),
|
|
100
|
+
catch: () => runtimeFailure("Session attention is unavailable"),
|
|
101
|
+
});
|
|
102
|
+
const settled = yield* settlementFacts(store, job, waiting);
|
|
103
|
+
yield* fanOut(mailRoom, { ...settled, delegator });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The `core-report` settled consumer: after the extensions' turn, report each
|
|
108
|
+
* Session's last settled job in the page, one record at a time.
|
|
109
|
+
*/
|
|
110
|
+
function reportSettled(store, watchdog, mailRoom, records) {
|
|
111
|
+
return Effect.forEach(records, (record, index) => Effect.gen(function* () {
|
|
112
|
+
const later = records
|
|
113
|
+
.slice(index + 1)
|
|
114
|
+
.some(({ job }) => job.queue === record.job.queue);
|
|
115
|
+
if (!later && !(yield* settledInRefusedSession(store, record.job)))
|
|
116
|
+
yield* reportToDelegator(store, watchdog, mailRoom, record.job);
|
|
117
|
+
yield* Effect.try({
|
|
118
|
+
try: () => {
|
|
119
|
+
advanceCursor(store, reportConsumer, record.sequence);
|
|
120
|
+
},
|
|
121
|
+
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
122
|
+
});
|
|
123
|
+
}), { discard: true });
|
|
124
|
+
}
|
|
38
125
|
function repairWorkSettlement(store, job) {
|
|
39
126
|
const outcome = job.outcome;
|
|
40
127
|
if (outcome !== "interrupted" &&
|
|
@@ -68,7 +155,10 @@ function settleTargetedControls(store, watchdog, job) {
|
|
|
68
155
|
outcome,
|
|
69
156
|
diagnostic: job.failureReason,
|
|
70
157
|
enqueue: (work) => {
|
|
71
|
-
watchdog.transactional.enqueueAcceptedJob(agentJobFromInput({
|
|
158
|
+
watchdog.transactional.enqueueAcceptedJob(agentJobFromInput({
|
|
159
|
+
work: { ...work, recovery: agentTurnRecovery },
|
|
160
|
+
delivery: work.delivery,
|
|
161
|
+
}));
|
|
72
162
|
},
|
|
73
163
|
});
|
|
74
164
|
});
|
|
@@ -148,7 +238,7 @@ function settleCore(store, watchdog, mailRoom, records) {
|
|
|
148
238
|
if (!(yield* settledInRefusedSession(store, record.job))) {
|
|
149
239
|
yield* repairWorkSettlement(store, record.job);
|
|
150
240
|
yield* settleTargetedControls(store, watchdog, record.job);
|
|
151
|
-
yield*
|
|
241
|
+
yield* replyToMail(store, mailRoom, record.job);
|
|
152
242
|
}
|
|
153
243
|
yield* Effect.try({
|
|
154
244
|
try: () => {
|
|
@@ -172,26 +262,34 @@ export function reconcileRuntime(input) {
|
|
|
172
262
|
try: () => readCursor(input.store, coreConsumer) ?? 0,
|
|
173
263
|
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
174
264
|
});
|
|
265
|
+
const reportCursor = yield* Effect.try({
|
|
266
|
+
try: () => readCursor(input.store, reportConsumer) ?? coreCursor,
|
|
267
|
+
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
268
|
+
});
|
|
175
269
|
const consumers = input.extensions.settledConsumers();
|
|
176
|
-
const after = Math.min(coreCursor, ...consumers.map(({ cursor }) => cursor));
|
|
270
|
+
const after = Math.min(coreCursor, reportCursor, ...consumers.map(({ cursor }) => cursor));
|
|
177
271
|
const page = yield* input.watchdog.readSettled(pageSize, after);
|
|
178
272
|
const core = yield* Effect.exit(settleCore(input.store, input.watchdog, input.mailRoom, page.filter(({ sequence }) => sequence > coreCursor)));
|
|
273
|
+
const processed = yield* Effect.try({
|
|
274
|
+
try: () => readCursor(input.store, coreConsumer) ?? 0,
|
|
275
|
+
catch: () => runtimeFailure("Settled cursor is unavailable"),
|
|
276
|
+
});
|
|
179
277
|
const runs = yield* Effect.try({
|
|
180
|
-
try: () =>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.filter(({ sequence }) => sequence <= processed)
|
|
184
|
-
.map((record) => settledRun(input.store, record));
|
|
185
|
-
},
|
|
278
|
+
try: () => page
|
|
279
|
+
.filter(({ sequence }) => sequence <= processed)
|
|
280
|
+
.map((record) => settledRun(input.store, record)),
|
|
186
281
|
catch: () => runtimeFailure("Settled run facts are unavailable"),
|
|
187
282
|
});
|
|
188
|
-
yield* input.extensions.deliverSettled(runs,
|
|
283
|
+
yield* input.extensions.deliverSettled(runs, processed);
|
|
284
|
+
const reports = yield* Effect.exit(reportSettled(input.store, input.watchdog, input.mailRoom, page.filter(({ sequence }) => sequence > reportCursor && sequence <= processed)));
|
|
189
285
|
const moreTimers = yield* input.extensions.fireDueTimers(Date.now());
|
|
190
286
|
if (page.length === pageSize || moreTimers)
|
|
191
287
|
yield* input.requestWake;
|
|
192
288
|
yield* drainMail(input.mailRoom);
|
|
193
289
|
if (Exit.isFailure(core))
|
|
194
290
|
return yield* Effect.failCause(core.cause);
|
|
291
|
+
if (Exit.isFailure(reports))
|
|
292
|
+
return yield* Effect.failCause(reports.cause);
|
|
195
293
|
}).pipe(Effect.mapError((failure) => failure instanceof ShiitakeError
|
|
196
294
|
? failure
|
|
197
295
|
: runtimeFailure("Runtime reconciliation failed")));
|
|
@@ -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>;
|