@fungi.computer/shiitake 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -34
- package/dist/agent-control.d.ts +77 -5
- package/dist/agent-control.js +271 -65
- package/dist/client-entry.d.ts +15 -8
- package/dist/client-entry.js +7 -5
- package/dist/client-wire.d.ts +24 -8
- package/dist/client-wire.js +53 -11
- package/dist/client.d.ts +56 -11
- package/dist/client.js +1 -1
- package/dist/cloudflare.d.ts +42 -23
- package/dist/cloudflare.js +76 -54
- package/dist/cold-sessions.d.ts +28 -0
- package/dist/cold-sessions.js +1 -0
- package/dist/delegation.d.ts +214 -0
- package/dist/delegation.js +319 -0
- package/dist/effect.d.ts +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -2
- package/dist/extension-contract.d.ts +10 -2
- package/dist/extensions.d.ts +6 -2
- package/dist/fleet.d.ts +43 -9
- package/dist/fleet.js +88 -25
- package/dist/hosted.js +1 -0
- package/dist/http-routes.d.ts +113 -29
- package/dist/http-routes.js +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/internal/accepted-run-executor.d.ts +5 -0
- package/dist/internal/accepted-run-executor.js +10 -4
- package/dist/internal/admission-door.d.ts +3 -0
- package/dist/internal/admission-door.js +12 -2
- package/dist/internal/client-boundary.js +7 -1
- package/dist/internal/compaction/compaction-slot.d.ts +36 -0
- package/dist/internal/compaction/compaction-slot.js +41 -0
- package/dist/internal/compaction/compaction.d.ts +11 -8
- package/dist/internal/compaction/compaction.js +62 -34
- package/dist/internal/compaction/session-compaction.d.ts +8 -16
- package/dist/internal/compaction/session-compaction.js +358 -119
- package/dist/internal/compaction/settings.d.ts +1 -1
- package/dist/internal/compaction/settings.js +5 -0
- package/dist/internal/compaction/utils.d.ts +19 -16
- package/dist/internal/compaction/utils.js +30 -84
- package/dist/internal/contracts.d.ts +40 -13
- package/dist/internal/coordinator.js +28 -10
- package/dist/internal/effect-client.d.ts +4 -2
- package/dist/internal/effect-client.js +29 -0
- package/dist/internal/effect-contracts.d.ts +17 -4
- package/dist/internal/extension-runtime.d.ts +5 -0
- package/dist/internal/extension-runtime.js +184 -89
- package/dist/internal/host-meta.d.ts +7 -0
- package/dist/internal/model-acquisition.js +7 -1
- package/dist/internal/model-preference.d.ts +4 -2
- package/dist/internal/model-preference.js +2 -1
- package/dist/internal/promise-facade.js +4 -16
- package/dist/internal/run-failure-reason.d.ts +10 -0
- package/dist/internal/run-failure-reason.js +19 -0
- package/dist/internal/run-scope.d.ts +4 -1
- package/dist/internal/run-scope.js +38 -5
- package/dist/internal/runtime-coordinator.d.ts +2 -1
- package/dist/internal/runtime-coordinator.js +65 -4
- package/dist/internal/runtime-core.js +1 -0
- package/dist/internal/runtime-reconciliation.js +42 -6
- package/dist/internal/session-context.d.ts +1 -2
- package/dist/internal/session-context.js +8 -30
- package/dist/internal/session-status.d.ts +2 -1
- package/dist/internal/session-status.js +4 -1
- package/dist/internal/woodstock/client-projection.d.ts +6 -5
- package/dist/internal/woodstock/client-projection.js +79 -53
- package/dist/internal/woodstock/cold-sessions.d.ts +4 -0
- package/dist/internal/woodstock/cold-sessions.js +90 -0
- package/dist/internal/woodstock/extension-store.d.ts +2 -0
- package/dist/internal/woodstock/extension-store.js +4 -0
- package/dist/internal/woodstock/fleet-store.d.ts +5 -1
- package/dist/internal/woodstock/fleet-store.js +17 -5
- package/dist/internal/woodstock/index.d.ts +11 -3
- package/dist/internal/woodstock/index.js +33 -14
- package/dist/internal/woodstock/progress.d.ts +14 -1
- package/dist/internal/woodstock/progress.js +97 -19
- package/dist/internal/woodstock/run-recovery.js +6 -4
- package/dist/internal/woodstock/session-attention.d.ts +1 -1
- package/dist/internal/woodstock/session-attention.js +2 -2
- package/dist/internal/woodstock/session-entry.d.ts +110 -43
- package/dist/internal/woodstock/session-entry.js +63 -20
- package/dist/internal/woodstock/session-inbox.js +2 -0
- package/dist/internal/woodstock/session-store.d.ts +104 -24
- package/dist/internal/woodstock/session-store.js +937 -246
- package/dist/internal/woodstock/storage-admission.d.ts +8 -8
- package/dist/internal/woodstock/storage-admission.js +8 -34
- package/dist/internal/woodstock/storage-conformance.d.ts +15 -0
- package/dist/internal/woodstock/storage-conformance.js +489 -0
- package/dist/model-preference.d.ts +2 -2
- package/dist/model-preference.js +2 -2
- package/dist/node.js +1 -1
- package/dist/remote-client.js +8 -1
- package/dist/session-attention.d.ts +8 -1
- package/dist/session-attention.js +6 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -0
- package/docs/api/README.md +18 -16
- package/docs/api/agent-control.md +171 -1
- package/docs/api/client-entry.md +26 -10
- package/docs/api/client.md +153 -37
- package/docs/api/cloudflare.md +76 -23
- package/docs/api/delegation.md +485 -0
- package/docs/api/effect.md +20 -2
- package/docs/api/extensions.md +21 -9
- package/docs/api/fleet.md +159 -104
- package/docs/api/http.md +4 -1
- package/docs/api/index.md +116 -20
- package/docs/api/internal/woodstock/storage-conformance.md +81 -0
- package/docs/api/model-preference.md +27 -10
- package/docs/api/testing.md +16 -4
- package/package.json +26 -14
- package/dist/internal/browser-sessions.d.ts +0 -31
- package/dist/internal/browser-sessions.js +0 -173
|
@@ -12,7 +12,7 @@ import { ShiitakeError } from "../errors.js";
|
|
|
12
12
|
import { AdmissionError, ExtensionCommandError, ExtensionRead, ExtensionStateError, ExtensionStop, ExtensionTx, } from "../extensions.js";
|
|
13
13
|
import { decodeClientSessionId } from "./client-boundary.js";
|
|
14
14
|
import { AsyncExtensionTransactionError, invokeExtensionHook, invokeSyncHandler, } from "./extension-hook.js";
|
|
15
|
-
import { setSessionAttention, clearSessionAttention, projectSessionAttention, readAttentionSessionPage, replaceAttentionGrants, hasStoredSession, hasPendingSessionSteer, hasSuccessfulToolResult, advanceCursor, declareSessionDocument, readSessionDocument, writeSessionDocument, deleteAllRecords, deleteRecord, deleteTimer, readAllRecords, readCursor, readDueTimers, readRecord, readRecordPage, readStateLedger, readStatus, readTimer, readTimerWake, writeRecord, writeStateLedger, writeStatus, writeTimer, } from "./woodstock/index.js";
|
|
15
|
+
import { setSessionAttention, clearSessionAttention, projectSessionAttention, readAttentionSessionPage, replaceAttentionGrants, hasStoredSession, readSessionLineage, hasPendingSessionSteer, hasSuccessfulToolResult, advanceCursor, declareSessionDocument, readSessionDocument, writeSessionDocument, deleteAllRecords, deleteRecord, deleteTimer, deleteExtensionTimers, readAllRecords, readCursor, readDueTimers, readRecord, readRecordPage, readStateLedger, readStatus, readTimer, readTimerWake, writeRecord, writeStateLedger, writeStatus, writeTimer, } from "./woodstock/index.js";
|
|
16
16
|
import { JsonValueSchema } from "./json-value.js";
|
|
17
17
|
import { publishOwnerObservation } from "./observation-kernel.js";
|
|
18
18
|
/** The settled consumer name core itself uses for mail and control repair. */
|
|
@@ -26,7 +26,7 @@ const decodePromptSections = Schema.decodeUnknownResult(Schema.Array(Schema.Stru
|
|
|
26
26
|
name: Schema.String.pipe(Schema.check(Schema.isPattern(/^[a-z][A-Za-z0-9_-]{0,63}$/u))),
|
|
27
27
|
text: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.makeFilter((text) => encoder.encode(text).byteLength <= 8192))),
|
|
28
28
|
})).pipe(Schema.check(Schema.isMaxLength(16))));
|
|
29
|
-
/** A
|
|
29
|
+
/** A declared migration failed or returned records the new state cannot decode. */
|
|
30
30
|
class IncompatibleStateMigrationError extends Error {
|
|
31
31
|
}
|
|
32
32
|
function boundReason(reason) {
|
|
@@ -277,9 +277,12 @@ export function openExtensionRuntime(input) {
|
|
|
277
277
|
projectSessionAttention(owner, sessionId, input.readQueueHead(sessionId));
|
|
278
278
|
frame.observed.add(sessionId);
|
|
279
279
|
},
|
|
280
|
-
catch: (error) => error instanceof
|
|
281
|
-
|
|
282
|
-
|
|
280
|
+
catch: (error) => error instanceof ExtensionStateError &&
|
|
281
|
+
error.reason === "session_absent"
|
|
282
|
+
? error
|
|
283
|
+
: error instanceof ShiitakeError && error.cause === "invalid"
|
|
284
|
+
? stateError("bound", error.message)
|
|
285
|
+
: stateError("unavailable", "Attention update is unavailable"),
|
|
283
286
|
})));
|
|
284
287
|
return {
|
|
285
288
|
set: (sessionId, item) => {
|
|
@@ -290,7 +293,7 @@ export function openExtensionRuntime(input) {
|
|
|
290
293
|
? Effect.fail(stateError("bound", "Attention item is invalid"))
|
|
291
294
|
: mutate(sessionId, () => {
|
|
292
295
|
if (!hasStoredSession(owner, sessionId))
|
|
293
|
-
throw
|
|
296
|
+
throw stateError("session_absent", "Attention Session is absent");
|
|
294
297
|
setSessionAttention(owner, sessionId, id, decoded.success);
|
|
295
298
|
});
|
|
296
299
|
},
|
|
@@ -564,6 +567,13 @@ export function openExtensionRuntime(input) {
|
|
|
564
567
|
send: sendFor(member),
|
|
565
568
|
timers: timersFor(member),
|
|
566
569
|
attention: attentionFor(member),
|
|
570
|
+
sessionPosition: (sessionId) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "state")), Effect.andThen(storeStep(() => {
|
|
571
|
+
const id = decodeClientSessionId(sessionId);
|
|
572
|
+
if (!hasStoredSession(owner, id))
|
|
573
|
+
throw stateError("unavailable", "Session timeline is unavailable");
|
|
574
|
+
return (readSessionLineage(owner, { sessionId: id })
|
|
575
|
+
?.newestSequence ?? 0);
|
|
576
|
+
}))),
|
|
567
577
|
hasPendingSteer: (run) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => hasPendingSessionSteer(owner, decodeClientSessionId(run.sessionId), run.workId)))),
|
|
568
578
|
hasSuccessfulToolResult: (run, toolCallId) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => hasSuccessfulToolResult(owner, decodeClientSessionId(run.sessionId), run.workId, toolCallId)))),
|
|
569
579
|
observe: (sessionId) => requireFrame(member.admitted.id).pipe(Effect.map((frame) => {
|
|
@@ -587,54 +597,60 @@ export function openExtensionRuntime(input) {
|
|
|
587
597
|
}),
|
|
588
598
|
});
|
|
589
599
|
}
|
|
590
|
-
/** Pure per-version migrations
|
|
600
|
+
/** Pure per-version migrations; prepare owns their transaction with declarations. */
|
|
591
601
|
function migrate(member) {
|
|
592
602
|
const { id, definition, revision } = member.admitted;
|
|
593
603
|
const declared = definition.state.version;
|
|
594
|
-
owner
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
rows = rows.flatMap((row) => step(row));
|
|
610
|
-
}
|
|
611
|
-
catch (cause) {
|
|
612
|
-
throw new IncompatibleStateMigrationError(`Failed ${id} state migration from ${String(version)}`, { cause });
|
|
613
|
-
}
|
|
604
|
+
const ledger = readStateLedger(owner, id);
|
|
605
|
+
if (ledger !== undefined && ledger.version > declared)
|
|
606
|
+
throw new ShiitakeError("extensions", "conflict", `Stored ${id} state version ${String(ledger.version)} is newer than ${String(declared)}`);
|
|
607
|
+
if (ledger !== undefined && ledger.version < declared) {
|
|
608
|
+
let rows = readAllRecords(owner, id).map((row) => ({
|
|
609
|
+
collection: row.collection,
|
|
610
|
+
key: row.key,
|
|
611
|
+
value: decodeStoredJson(row.valueJson),
|
|
612
|
+
}));
|
|
613
|
+
for (let version = ledger.version; version < declared; version += 1) {
|
|
614
|
+
const step = definition.state.migrations[version];
|
|
615
|
+
if (step === undefined)
|
|
616
|
+
throw new ShiitakeError("extensions", "state_reset_required", `State reset required for ${id}: stored version ${String(ledger.version)}, declared version ${String(declared)}. An operator must authorize the reset; stored records have not been changed.`);
|
|
617
|
+
try {
|
|
618
|
+
rows = rows.flatMap((row) => step(row));
|
|
614
619
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const declaredSpec = definition.state.collections[row.collection];
|
|
618
|
-
if (declaredSpec === undefined)
|
|
619
|
-
throw new IncompatibleStateMigrationError(`Migrated ${id} record names an undeclared collection`);
|
|
620
|
-
const decoded = declaredSpec.indexFromEncoded(row.value);
|
|
621
|
-
if (decoded._tag === "Failure")
|
|
622
|
-
throw new IncompatibleStateMigrationError(`Migrated ${id} ${row.collection} record is invalid`);
|
|
623
|
-
writeRecord(owner, id, row.collection, row.key, decoded.success, JSON.stringify(row.value));
|
|
620
|
+
catch (cause) {
|
|
621
|
+
throw new IncompatibleStateMigrationError(`Failed ${id} state migration from ${String(version)}`, { cause });
|
|
624
622
|
}
|
|
625
623
|
}
|
|
626
|
-
|
|
627
|
-
|
|
624
|
+
deleteAllRecords(owner, id);
|
|
625
|
+
for (const row of rows) {
|
|
626
|
+
const declaredSpec = definition.state.collections[row.collection];
|
|
627
|
+
if (declaredSpec === undefined)
|
|
628
|
+
throw new IncompatibleStateMigrationError(`Migrated ${id} record names an undeclared collection`);
|
|
629
|
+
const decoded = declaredSpec.indexFromEncoded(row.value);
|
|
630
|
+
if (decoded._tag === "Failure")
|
|
631
|
+
throw new IncompatibleStateMigrationError(`Migrated ${id} ${row.collection} record is invalid`);
|
|
632
|
+
writeRecord(owner, id, row.collection, row.key, decoded.success, JSON.stringify(row.value));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
writeStateLedger(owner, id, declared, revision);
|
|
628
636
|
}
|
|
629
|
-
function activateMember(member, scope) {
|
|
637
|
+
function activateMember(member, scope, strict = false) {
|
|
630
638
|
const context = contextFor(member);
|
|
631
|
-
return Effect.
|
|
639
|
+
return Effect.gen(function* () {
|
|
640
|
+
// Each activation owns its resources. Closing a failed candidate must
|
|
641
|
+
// not wait for Agent shutdown or release another member's resources.
|
|
642
|
+
const activationScope = yield* Scope.fork(scope);
|
|
643
|
+
member.scope = activationScope;
|
|
644
|
+
const exit = yield* Effect.suspend(() => member.admitted.definition.activate(context)).pipe(Effect.provideService(Scope.Scope, activationScope), Effect.exit);
|
|
632
645
|
if (Exit.isSuccess(exit)) {
|
|
633
646
|
member.hooks = exit.value;
|
|
634
|
-
return
|
|
647
|
+
return;
|
|
635
648
|
}
|
|
636
|
-
|
|
637
|
-
|
|
649
|
+
yield* Scope.close(activationScope, exit);
|
|
650
|
+
if (strict)
|
|
651
|
+
return yield* Effect.fail(new ShiitakeError("extensions", "rejected", `Extension ${member.admitted.id} failed to start: ${Cause.pretty(exit.cause)}`));
|
|
652
|
+
yield* recordStop(member, stopReason("activation", exit.cause), exit.cause, "own-transaction");
|
|
653
|
+
});
|
|
638
654
|
}
|
|
639
655
|
const running = (capability) => [...members.values()].filter((member) => member.status === "running" &&
|
|
640
656
|
member.hooks !== undefined &&
|
|
@@ -668,13 +684,14 @@ export function openExtensionRuntime(input) {
|
|
|
668
684
|
: Effect.void;
|
|
669
685
|
/**
|
|
670
686
|
* Run pending state migrations. A stored version newer than the declared one
|
|
671
|
-
* fails construction
|
|
687
|
+
* fails construction. A missing path isolates this extension with its rows
|
|
688
|
+
* retained; a failed declared migration also guards runs as incompatible.
|
|
672
689
|
*/
|
|
673
690
|
function prepare(member) {
|
|
674
691
|
return Effect.try({
|
|
675
692
|
try: () => {
|
|
676
|
-
migrate(member);
|
|
677
693
|
owner.transactionSync(() => {
|
|
694
|
+
migrate(member);
|
|
678
695
|
for (const [name, spec] of Object.entries(member.admitted.documents))
|
|
679
696
|
declareSessionDocument(owner, {
|
|
680
697
|
extensionId: member.admitted.id,
|
|
@@ -686,9 +703,15 @@ export function openExtensionRuntime(input) {
|
|
|
686
703
|
});
|
|
687
704
|
},
|
|
688
705
|
catch: (error) => error,
|
|
689
|
-
}).pipe(Effect.
|
|
690
|
-
|
|
691
|
-
|
|
706
|
+
}).pipe(Effect.tap(() => Effect.sync(() => {
|
|
707
|
+
delete member.stateResetRequired;
|
|
708
|
+
})), Effect.as(true), Effect.catch((error) => {
|
|
709
|
+
if (error instanceof ShiitakeError) {
|
|
710
|
+
if (error.cause !== "state_reset_required")
|
|
711
|
+
return Effect.fail(error);
|
|
712
|
+
member.stateResetRequired = error;
|
|
713
|
+
return recordStop(member, boundReason(error.message), Cause.fail(error), "own-transaction").pipe(Effect.as(false));
|
|
714
|
+
}
|
|
692
715
|
if (error instanceof IncompatibleStateMigrationError &&
|
|
693
716
|
(member.admitted.grants.has("run_tools") ||
|
|
694
717
|
member.admitted.grants.has("files") ||
|
|
@@ -702,49 +725,56 @@ export function openExtensionRuntime(input) {
|
|
|
702
725
|
}
|
|
703
726
|
return Effect.gen(function* () {
|
|
704
727
|
const scope = yield* Scope.make();
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
728
|
+
function startMember(admitted, strict) {
|
|
729
|
+
return Effect.gen(function* () {
|
|
730
|
+
const stored = readStatus(owner, admitted.id);
|
|
731
|
+
const keepStopped = stored?.status === "stopped" && stored.revision === admitted.revision;
|
|
732
|
+
const member = {
|
|
733
|
+
admitted,
|
|
734
|
+
hooks: undefined,
|
|
735
|
+
status: keepStopped ? "stopped" : "running",
|
|
736
|
+
reason: keepStopped ? stored.reason : null,
|
|
737
|
+
stoppedAt: keepStopped ? stored.stoppedAt : null,
|
|
738
|
+
};
|
|
739
|
+
// Every settled consumer has a cursor from its first roster on, so a
|
|
740
|
+
// later resume replays only what it missed.
|
|
741
|
+
yield* Effect.try({
|
|
742
|
+
try: () => {
|
|
743
|
+
if (admitted.grants.has("settled"))
|
|
744
|
+
owner.transactionSync(() => {
|
|
745
|
+
if (readCursor(owner, admitted.id) === undefined)
|
|
746
|
+
advanceCursor(owner, admitted.id, readCursor(owner, coreConsumer) ?? 0);
|
|
747
|
+
});
|
|
748
|
+
},
|
|
749
|
+
catch: () => new ShiitakeError("store", "unavailable", "Settled cursor is unavailable"),
|
|
750
|
+
});
|
|
751
|
+
const prepared = yield* prepare(member);
|
|
752
|
+
if (!prepared || keepStopped) {
|
|
753
|
+
if (strict)
|
|
754
|
+
return yield* Effect.fail(new ShiitakeError("extensions", "rejected", `Extension ${admitted.id} cannot start: ${member.reason ?? "stopped"}`));
|
|
755
|
+
return member;
|
|
756
|
+
}
|
|
757
|
+
yield* Effect.try({
|
|
758
|
+
try: () => {
|
|
721
759
|
owner.transactionSync(() => {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (!prepared)
|
|
730
|
-
continue;
|
|
731
|
-
if (keepStopped)
|
|
732
|
-
continue;
|
|
733
|
-
yield* Effect.try({
|
|
734
|
-
try: () => {
|
|
735
|
-
owner.transactionSync(() => {
|
|
736
|
-
writeStatus(owner, admitted.id, {
|
|
737
|
-
status: "running",
|
|
738
|
-
reason: null,
|
|
739
|
-
stoppedAt: null,
|
|
740
|
-
revision: admitted.revision,
|
|
760
|
+
writeStatus(owner, admitted.id, {
|
|
761
|
+
status: "running",
|
|
762
|
+
reason: null,
|
|
763
|
+
stoppedAt: null,
|
|
764
|
+
revision: admitted.revision,
|
|
765
|
+
});
|
|
766
|
+
refreshAttentionOwner(admitted.id);
|
|
741
767
|
});
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
|
|
768
|
+
},
|
|
769
|
+
catch: () => new ShiitakeError("store", "unavailable", "Extension status is unavailable"),
|
|
770
|
+
});
|
|
771
|
+
yield* activateMember(member, scope, strict);
|
|
772
|
+
return member;
|
|
746
773
|
});
|
|
747
|
-
|
|
774
|
+
}
|
|
775
|
+
for (const admitted of input.admitted) {
|
|
776
|
+
const member = yield* startMember(admitted, false).pipe(Effect.onError(() => Scope.close(scope, Exit.void)));
|
|
777
|
+
members.set(admitted.id, member);
|
|
748
778
|
}
|
|
749
779
|
yield* Effect.try({
|
|
750
780
|
try: () => {
|
|
@@ -759,6 +789,70 @@ export function openExtensionRuntime(input) {
|
|
|
759
789
|
catch: () => new ShiitakeError("store", "unavailable", "Attention grants are unavailable"),
|
|
760
790
|
});
|
|
761
791
|
const runtime = {
|
|
792
|
+
stage: (admitted) => Effect.gen(function* () {
|
|
793
|
+
const changed = admitted.filter((next) => {
|
|
794
|
+
const previous = members.get(next.id);
|
|
795
|
+
return (previous !== undefined &&
|
|
796
|
+
previous.admitted.fingerprint !== next.fingerprint);
|
|
797
|
+
});
|
|
798
|
+
if (changed.length > 0)
|
|
799
|
+
return yield* Effect.fail(new ShiitakeError("extensions", "rejected", `Extensions ${changed.map(({ id }) => id).join(", ")}: this change needs a restart: run fungi stop, then reopen fungi`));
|
|
800
|
+
const additions = new Map();
|
|
801
|
+
const discard = Effect.suspend(() => Effect.forEach([...additions.values()].reverse(), (member) => Effect.exit(member.scope === undefined
|
|
802
|
+
? Effect.void
|
|
803
|
+
: Scope.close(member.scope, Exit.void)), { concurrency: 1 }).pipe(Effect.tap(() => Effect.sync(() => {
|
|
804
|
+
for (const next of admitted)
|
|
805
|
+
if (!members.has(next.id))
|
|
806
|
+
incompatibleMigrations.delete(next.id);
|
|
807
|
+
})), Effect.flatMap(Exit.asVoidAll)));
|
|
808
|
+
yield* Effect.gen(function* () {
|
|
809
|
+
for (const next of admitted)
|
|
810
|
+
if (!members.has(next.id))
|
|
811
|
+
additions.set(next.id, yield* startMember(next, true));
|
|
812
|
+
}).pipe(Effect.onError(() => discard));
|
|
813
|
+
const target = new Set(admitted.map(({ id }) => id));
|
|
814
|
+
let settled = false;
|
|
815
|
+
return {
|
|
816
|
+
discard: Effect.suspend(() => {
|
|
817
|
+
if (settled)
|
|
818
|
+
return Effect.void;
|
|
819
|
+
settled = true;
|
|
820
|
+
return discard;
|
|
821
|
+
}),
|
|
822
|
+
apply: Effect.gen(function* () {
|
|
823
|
+
if (settled)
|
|
824
|
+
return yield* Effect.fail(new ShiitakeError("extensions", "rejected", "Roster transition already settled"));
|
|
825
|
+
// Finish fallible store work atomically before releasing live
|
|
826
|
+
// resources. A rolled-back publication must leave both the old
|
|
827
|
+
// timers and the old member scopes available.
|
|
828
|
+
yield* Effect.try({
|
|
829
|
+
try: () => {
|
|
830
|
+
owner.transactionSync(() => {
|
|
831
|
+
for (const [id, member] of members)
|
|
832
|
+
if (!target.has(member.admitted.id))
|
|
833
|
+
deleteExtensionTimers(owner, id);
|
|
834
|
+
const changed = replaceAttentionGrants(owner, admitted
|
|
835
|
+
.filter((entry) => entry.grants.has("attention"))
|
|
836
|
+
.map(({ id }) => id));
|
|
837
|
+
for (const id of changed)
|
|
838
|
+
refreshAttentionOwner(id);
|
|
839
|
+
});
|
|
840
|
+
},
|
|
841
|
+
catch: () => new ShiitakeError("store", "unavailable", "Roster publication storage is unavailable"),
|
|
842
|
+
});
|
|
843
|
+
for (const [id, member] of members)
|
|
844
|
+
if (!target.has(member.admitted.id)) {
|
|
845
|
+
if (member.scope !== undefined)
|
|
846
|
+
yield* Scope.close(member.scope, Exit.void);
|
|
847
|
+
members.delete(id);
|
|
848
|
+
incompatibleMigrations.delete(member.admitted.id);
|
|
849
|
+
}
|
|
850
|
+
for (const [id, member] of additions)
|
|
851
|
+
members.set(id, member);
|
|
852
|
+
settled = true;
|
|
853
|
+
}),
|
|
854
|
+
};
|
|
855
|
+
}),
|
|
762
856
|
runParticipants: () => [...members.values()]
|
|
763
857
|
.filter(isRunParticipant)
|
|
764
858
|
.map((member) => [member.admitted.id, member.admitted.fingerprint]),
|
|
@@ -931,7 +1025,8 @@ export function openExtensionRuntime(input) {
|
|
|
931
1025
|
if (member?.admitted.grants.has("commands") !== true)
|
|
932
1026
|
return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", "Extension is unavailable"));
|
|
933
1027
|
if (member.status !== "running" || member.hooks === undefined)
|
|
934
|
-
return yield* Effect.fail(
|
|
1028
|
+
return yield* Effect.fail(member.stateResetRequired ??
|
|
1029
|
+
new ShiitakeError("extensions", "unavailable", `Extension ${extensionId} is stopped: ${member.reason ?? "not activated"}`));
|
|
935
1030
|
const commands = member.admitted.definition.contract.commands;
|
|
936
1031
|
const spec = Object.hasOwn(commands, command)
|
|
937
1032
|
? commands[command]
|
|
@@ -31,6 +31,13 @@ export type StandardHostMetaResult = Readonly<{
|
|
|
31
31
|
*/
|
|
32
32
|
export type HostMetaOptions = Readonly<{
|
|
33
33
|
schema: Schema.Codec<unknown, Json> | StandardHostMetaSchema;
|
|
34
|
+
/**
|
|
35
|
+
* Host-selected SQLite JSON path to its stable principal identity.
|
|
36
|
+
* The host owns this metadata grammar. When bound metadata is present,
|
|
37
|
+
* pending lookup and exact cancellation stay within that identity.
|
|
38
|
+
* Unbound in-process owners retain operational cancellation authority.
|
|
39
|
+
*/
|
|
40
|
+
identityPath?: string;
|
|
34
41
|
}>;
|
|
35
42
|
/**
|
|
36
43
|
* Check one host-attached value: finite JSON within the cap, and accepted by
|
|
@@ -14,7 +14,13 @@ function asEffectAcquisition(acquisition) {
|
|
|
14
14
|
try: () => acquisition.release(),
|
|
15
15
|
catch: releaseFailure,
|
|
16
16
|
});
|
|
17
|
-
|
|
17
|
+
let shared = {
|
|
18
|
+
model: acquisition.model,
|
|
19
|
+
release,
|
|
20
|
+
retryable: acquisition.retryable,
|
|
21
|
+
};
|
|
22
|
+
if (acquisition.failureReason !== undefined)
|
|
23
|
+
shared = { ...shared, failureReason: acquisition.failureReason };
|
|
18
24
|
if (acquisition.stream === undefined)
|
|
19
25
|
return acquisition.retryAfterMs === undefined
|
|
20
26
|
? Object.freeze(shared)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
|
+
/** The thinking levels a model choice may name, shared by every model path. */
|
|
3
|
+
export declare const reasoningLevels: readonly ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
2
4
|
/** Secret-free request to the host's maintained model catalogue. */
|
|
3
5
|
export declare const ModelPreferenceSchema: Schema.Struct<{
|
|
4
6
|
readonly providerId: Schema.String;
|
|
@@ -11,11 +13,11 @@ export type ModelPreference = Schema.Schema.Type<typeof ModelPreferenceSchema>;
|
|
|
11
13
|
export declare const parseModelPreference: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
12
14
|
readonly providerId: string;
|
|
13
15
|
readonly modelId: string;
|
|
14
|
-
readonly reasoning?: "
|
|
16
|
+
readonly reasoning?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
|
|
15
17
|
} | null;
|
|
16
18
|
/** Decode the serialized preference with the same maintained schema. */
|
|
17
19
|
export declare const parseModelPreferenceJson: (input: unknown, options?: import("effect/SchemaAST").ParseOptions) => {
|
|
18
20
|
readonly providerId: string;
|
|
19
21
|
readonly modelId: string;
|
|
20
|
-
readonly reasoning?: "
|
|
22
|
+
readonly reasoning?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
|
|
21
23
|
} | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
2
|
const identifier = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256)));
|
|
3
|
-
|
|
3
|
+
/** The thinking levels a model choice may name, shared by every model path. */
|
|
4
|
+
export const reasoningLevels = [
|
|
4
5
|
"minimal",
|
|
5
6
|
"low",
|
|
6
7
|
"medium",
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Predicate from "effect/Predicate";
|
|
3
3
|
import * as Schema from "effect/Schema";
|
|
4
|
-
import { fromTypedSandbox } from "@fungi.computer/mycelium/effect";
|
|
5
4
|
import { ShiitakeError } from "../errors.js";
|
|
6
|
-
import { runShiitakePromise, toPromiseOwner } from "./effect-client.js";
|
|
5
|
+
import { liftTools, runShiitakePromise, toPromiseOwner, } from "./effect-client.js";
|
|
7
6
|
import { adaptModelSource } from "./model-acquisition.js";
|
|
8
7
|
function boundary(boundaryName, message) {
|
|
9
8
|
return new ShiitakeError(boundaryName, "rejected", message);
|
|
@@ -138,25 +137,14 @@ function liftOptions(options, runtime) {
|
|
|
138
137
|
};
|
|
139
138
|
if (options.meta !== undefined)
|
|
140
139
|
Object.assign(base, { meta: options.meta });
|
|
141
|
-
if (options.
|
|
142
|
-
Object.assign(base, {
|
|
140
|
+
if (options.storagePressure !== undefined)
|
|
141
|
+
Object.assign(base, { storagePressure: options.storagePressure });
|
|
143
142
|
if (options.compaction !== undefined)
|
|
144
143
|
Object.assign(base, { compaction: options.compaction });
|
|
145
144
|
const toolSource = options.tools;
|
|
146
145
|
if (toolSource === undefined)
|
|
147
146
|
return base;
|
|
148
|
-
|
|
149
|
-
modules: toolSource.modules ?? [],
|
|
150
|
-
sources: toolSource.sources ?? [],
|
|
151
|
-
sandbox: fromTypedSandbox(toolSource.sandbox),
|
|
152
|
-
access: toolSource.access ?? {},
|
|
153
|
-
};
|
|
154
|
-
return {
|
|
155
|
-
...base,
|
|
156
|
-
tools: toolSource.execution === undefined
|
|
157
|
-
? tools
|
|
158
|
-
: { ...tools, execution: toolSource.execution },
|
|
159
|
-
};
|
|
147
|
+
return { ...base, tools: liftTools(toolSource) };
|
|
160
148
|
}
|
|
161
149
|
/** Open the single Promise adaptation over the canonical Effect agent. */
|
|
162
150
|
export function openPromiseAgent(options, make) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MuleRunOutcome } from "@fungi.computer/mule/effect";
|
|
2
|
+
/** Observations, not a classification of arbitrary provider error text. */
|
|
3
|
+
export type UnavailableRunReason = Readonly<{
|
|
4
|
+
kind: "reason_unavailable";
|
|
5
|
+
runId: string;
|
|
6
|
+
provider: string | null;
|
|
7
|
+
model: string | null;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function unavailableRunReason(observations: Omit<UnavailableRunReason, "kind">): string;
|
|
10
|
+
export declare function returnedFailureReason(run: MuleRunOutcome): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function unavailableRunReason(observations) {
|
|
2
|
+
const reason = {
|
|
3
|
+
kind: "reason_unavailable",
|
|
4
|
+
...observations,
|
|
5
|
+
provider: observations.provider === "" ? null : observations.provider,
|
|
6
|
+
model: observations.model === "" ? null : observations.model,
|
|
7
|
+
};
|
|
8
|
+
// Watchdog bounds reasons to 200 characters. Bound identity metadata only;
|
|
9
|
+
// truncation is not redaction and must never make provider prose trusted.
|
|
10
|
+
return `${reason.kind}: run=${reason.runId}; provider=${reason.provider ?? "unknown"}; model=${reason.model ?? "unknown"}`.slice(0, 200);
|
|
11
|
+
}
|
|
12
|
+
export function returnedFailureReason(run) {
|
|
13
|
+
// Pi errorMessage can contain raw SDK messages and response bodies.
|
|
14
|
+
return unavailableRunReason({
|
|
15
|
+
runId: run.run.value,
|
|
16
|
+
provider: run.message.provider,
|
|
17
|
+
model: run.message.model,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -18,7 +18,10 @@ export declare class ToolSetupError extends ShiitakeError {
|
|
|
18
18
|
get diagnostic(): string;
|
|
19
19
|
}
|
|
20
20
|
/** Run one accepted command with Pi compaction explicitly composed around Mule. */
|
|
21
|
-
export declare function runAcceptedCommand(options: ShiitakeEffectOptions, prepared: PreparedSessionRun, executionContract: RunContract, extensionModules: readonly ModuleRegistration[], extensionFileTools: readonly FileTool[], compactionHandoff: CompactionHandoffV0 | undefined, promptSections?: () => Effect.Effect<readonly PromptSectionV0[], ShiitakeError>): Effect.Effect<
|
|
21
|
+
export declare function runAcceptedCommand(options: ShiitakeEffectOptions, prepared: PreparedSessionRun, executionContract: RunContract, extensionModules: readonly ModuleRegistration[], extensionFileTools: readonly FileTool[], compactionHandoff: CompactionHandoffV0 | undefined, promptSections?: () => Effect.Effect<readonly PromptSectionV0[], ShiitakeError>): Effect.Effect<{
|
|
22
|
+
run: import("@fungi.computer/mule/effect").MuleRunOutcome;
|
|
23
|
+
failureReason: string | undefined;
|
|
24
|
+
}, ShiitakeError, never>;
|
|
22
25
|
/**
|
|
23
26
|
* Run one accepted compaction request: the run's model acquisition and the
|
|
24
27
|
* Session's one compaction commit, with no model turn and no tools.
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { muleEffect, piCodingAgentRetryDefaults, piHttpIdleTimeoutMs, } 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 Cause from "effect/Cause";
|
|
5
|
+
import * as Exit from "effect/Exit";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
4
7
|
import { ShiitakeError } from "../errors.js";
|
|
5
8
|
import { compactSession, runWithCompaction, } from "./compaction/session-compaction.js";
|
|
6
9
|
import { openCompactionSettings } from "./compaction/settings.js";
|
|
7
10
|
import { acquireOwnedModel } from "./model-acquisition.js";
|
|
11
|
+
import { returnedFailureReason, unavailableRunReason, } from "./run-failure-reason.js";
|
|
8
12
|
import { isCompactCommand, openMuleRecorder, readSessionGrants, recordLiveRetryEvent, recordSandboxFailure, takeSessionControls, } from "./woodstock/index.js";
|
|
9
13
|
function runtimeFailure(message) {
|
|
10
14
|
return new ShiitakeError("runtime", "rejected", message);
|
|
@@ -134,7 +138,8 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
134
138
|
})));
|
|
135
139
|
let resume = prepared.resume;
|
|
136
140
|
const settings = openCompactionSettings(options.compaction)(acquired.model);
|
|
137
|
-
|
|
141
|
+
let failureReason;
|
|
142
|
+
const run = yield* runWithCompaction({
|
|
138
143
|
store: options.store,
|
|
139
144
|
sessionId: prepared.command.sessionId,
|
|
140
145
|
workId: prepared.command.entry.workId,
|
|
@@ -148,12 +153,23 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
148
153
|
run: (workingMessages, betweenTurns) => {
|
|
149
154
|
const continuation = resume;
|
|
150
155
|
resume = undefined;
|
|
151
|
-
|
|
156
|
+
// A failed lifecycle notification precedes Mule's returned outcome
|
|
157
|
+
// (or typed error). Join them before publishing the single terminal;
|
|
158
|
+
// no observer can win a race against a later diagnostic settlement.
|
|
159
|
+
let failedEnd;
|
|
160
|
+
const listener = (event) => {
|
|
161
|
+
if (event.type === "run_end" && event.outcome === "failed") {
|
|
162
|
+
failedEnd = event;
|
|
163
|
+
return Effect.void;
|
|
164
|
+
}
|
|
165
|
+
return recorder(event);
|
|
166
|
+
};
|
|
152
167
|
const input = {
|
|
153
168
|
model: acquired.model,
|
|
154
169
|
modelStreamIdleTimeoutMs: options.modelStreamIdleTimeoutMs ?? piHttpIdleTimeoutMs,
|
|
155
170
|
retry: options.retry ?? piCodingAgentRetryDefaults,
|
|
156
171
|
retryAfterMs: acquired.retryAfterMs,
|
|
172
|
+
retryable: acquired.retryable,
|
|
157
173
|
systemPrompt: instructions.text,
|
|
158
174
|
prepareSystemPrompt: () => (promptSections === undefined
|
|
159
175
|
? Effect.succeed([])
|
|
@@ -181,11 +197,28 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
181
197
|
.run(acquired.stream === undefined
|
|
182
198
|
? input
|
|
183
199
|
: { ...input, stream: acquired.stream }, listener)
|
|
184
|
-
.pipe(Effect.
|
|
185
|
-
|
|
186
|
-
|
|
200
|
+
.pipe(Effect.onExit((exit) => {
|
|
201
|
+
if (failedEnd === undefined)
|
|
202
|
+
return Effect.void;
|
|
203
|
+
if (Exit.isSuccess(exit)) {
|
|
204
|
+
failureReason = (acquired.failureReason?.(exit.value.message) ??
|
|
205
|
+
returnedFailureReason(exit.value)).slice(0, 200);
|
|
206
|
+
return recorder(failedEnd, failureReason);
|
|
207
|
+
}
|
|
208
|
+
const error = Cause.findErrorOption(exit.cause);
|
|
209
|
+
return recorder(failedEnd, Option.isSome(error) && error.value.phase === "model"
|
|
210
|
+
? `model:rejected: ${error.value.message}`.slice(0, 200)
|
|
211
|
+
: unavailableRunReason({
|
|
212
|
+
runId: failedEnd.run.value,
|
|
213
|
+
provider: acquired.model.provider,
|
|
214
|
+
model: acquired.model.id,
|
|
215
|
+
}));
|
|
216
|
+
}), Effect.mapError((error) => error.phase === "model"
|
|
217
|
+
? new ShiitakeError("model", "rejected", error.message, undefined, error)
|
|
218
|
+
: runtimeFailure("Mule rejected the durable command run")));
|
|
187
219
|
},
|
|
188
220
|
});
|
|
221
|
+
return { run, failureReason };
|
|
189
222
|
}));
|
|
190
223
|
}
|
|
191
224
|
/**
|
|
@@ -5,11 +5,12 @@ import type { AgentRunningWork } from "../client.js";
|
|
|
5
5
|
import { ShiitakeError } from "../errors.js";
|
|
6
6
|
import { type AdmissionDoor } from "./admission-door.js";
|
|
7
7
|
import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
8
|
-
import type
|
|
8
|
+
import { type AdmittedExtensionV0 } from "./extension-roster.js";
|
|
9
9
|
import { type ExtensionRuntime } from "./extension-runtime.js";
|
|
10
10
|
import { type RecordAcceptedCancellation, type StorageAdmission } from "./woodstock/index.js";
|
|
11
11
|
/** Bound lifecycle and owner-transaction doors used by the public coordinator. */
|
|
12
12
|
export type RuntimeCoordinator = Readonly<{
|
|
13
|
+
prepareConfiguration: import("./effect-contracts.js").ShiitakeEffectOwner["prepareConfiguration"];
|
|
13
14
|
activeSessions: Effect.Effect<readonly string[]>;
|
|
14
15
|
runningWork: (jobId: string) => Effect.Effect<AgentRunningWork | null, ShiitakeError>;
|
|
15
16
|
assertOpen: Effect.Effect<void, ShiitakeError>;
|