@fungi.computer/shiitake 0.3.1 → 0.4.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 +18 -8
- package/dist/agent-control.d.ts +19 -15
- package/dist/agent-control.js +66 -126
- package/dist/client-wire.d.ts +12 -3
- package/dist/client-wire.js +33 -8
- package/dist/client.d.ts +34 -3
- package/dist/client.js +1 -1
- package/dist/cloudflare.d.ts +3 -1
- package/dist/cloudflare.js +2 -2
- package/dist/delegation.d.ts +145 -13
- package/dist/delegation.js +109 -37
- package/dist/effect.d.ts +1 -1
- package/dist/fleet.d.ts +30 -8
- package/dist/fleet.js +142 -38
- package/dist/http-routes.d.ts +84 -12
- package/dist/http-routes.js +15 -1
- package/dist/index.d.ts +2 -2
- package/dist/internal/admission-door.d.ts +2 -0
- package/dist/internal/admission-door.js +2 -0
- package/dist/internal/client-boundary.d.ts +10 -0
- package/dist/internal/client-boundary.js +10 -0
- package/dist/internal/compaction/compaction.js +1 -1
- package/dist/internal/contracts.d.ts +2 -0
- package/dist/internal/coordinator.d.ts +3 -1
- package/dist/internal/coordinator.js +98 -13
- package/dist/internal/effect-client.d.ts +3 -1
- package/dist/internal/effect-client.js +3 -2
- package/dist/internal/effect-contracts.d.ts +6 -2
- package/dist/internal/model-acquisition.js +2 -0
- package/dist/internal/observation-kernel.d.ts +8 -4
- package/dist/internal/observation-kernel.js +44 -9
- package/dist/internal/promise-facade.d.ts +1 -1
- package/dist/internal/promise-facade.js +1 -1
- package/dist/internal/run-scope.js +37 -8
- package/dist/internal/runtime-coordinator.d.ts +5 -0
- package/dist/internal/runtime-coordinator.js +8 -4
- package/dist/internal/runtime-core.d.ts +2 -1
- package/dist/internal/runtime-core.js +6 -1
- package/dist/internal/session-context.d.ts +1 -1
- package/dist/internal/session-context.js +84 -13
- package/dist/internal/woodstock/agent-reply.d.ts +7 -2
- package/dist/internal/woodstock/agent-reply.js +22 -13
- package/dist/internal/woodstock/fleet-store.d.ts +3 -2
- package/dist/internal/woodstock/fleet-store.js +43 -6
- package/dist/internal/woodstock/index.d.ts +2 -0
- package/dist/internal/woodstock/index.js +11 -5
- package/dist/internal/woodstock/run-recovery.js +4 -1
- package/dist/internal/woodstock/session-entry.d.ts +13 -3
- package/dist/internal/woodstock/session-entry.js +1 -0
- package/dist/internal/woodstock/session-store.d.ts +4 -0
- package/dist/internal/woodstock/session-store.js +26 -19
- package/dist/remote-client.js +5 -1
- package/dist/testing.d.ts +5 -2
- package/dist/testing.js +4 -2
- package/docs/api/agent-control.md +18 -12
- package/docs/api/client.md +78 -0
- package/docs/api/delegation.md +72 -55
- package/docs/api/effect.md +5 -0
- package/docs/api/fleet.md +23 -23
- package/docs/api/http.md +2 -1
- package/docs/api/index.md +13 -0
- package/docs/api/testing.md +3 -3
- package/package.json +9 -8
|
@@ -7,4 +7,4 @@ import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
|
7
7
|
* context or its context cannot be read; a window the host cannot describe is
|
|
8
8
|
* null, never guessed.
|
|
9
9
|
*/
|
|
10
|
-
export declare function readSessionContext(options: ShiitakeEffectOptions, sessionId: string): Effect.Effect<SessionContext | undefined>;
|
|
10
|
+
export declare function readSessionContext(options: ShiitakeEffectOptions, sessionId: string, cursor: string | null, cache?: boolean): Effect.Effect<SessionContext | undefined>;
|
|
@@ -2,6 +2,7 @@ import * as Effect from "effect/Effect";
|
|
|
2
2
|
import { estimateContextTokens } from "@earendil-works/pi-ai/utils/estimate";
|
|
3
3
|
import { reportCatchFailure } from "./failure.js";
|
|
4
4
|
import { buildSessionContext, readSessionContextPath, readSessionModelPreference, } from "./woodstock/index.js";
|
|
5
|
+
const contextByOwner = new WeakMap();
|
|
5
6
|
/**
|
|
6
7
|
* The context size compaction measures against the model's window, for the
|
|
7
8
|
* Session's settled context: the next run starts from it. Null before the
|
|
@@ -18,28 +19,98 @@ function settledContextTokens(store, sessionId) {
|
|
|
18
19
|
* context or its context cannot be read; a window the host cannot describe is
|
|
19
20
|
* null, never guessed.
|
|
20
21
|
*/
|
|
21
|
-
export function readSessionContext(options, sessionId) {
|
|
22
|
+
export function readSessionContext(options, sessionId, cursor, cache = true) {
|
|
22
23
|
return Effect.gen(function* () {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
try: () => ({
|
|
26
|
-
tokens: settledContextTokens(options.store, sessionId),
|
|
27
|
-
preference: readSessionModelPreference(options.store, sessionId),
|
|
28
|
-
}),
|
|
24
|
+
const savedPreference = yield* Effect.result(Effect.try({
|
|
25
|
+
try: () => readSessionModelPreference(options.store, sessionId),
|
|
29
26
|
catch: (error) => error,
|
|
30
27
|
}));
|
|
31
|
-
if (
|
|
28
|
+
if (savedPreference._tag === "Failure") {
|
|
29
|
+
reportCatchFailure("session-context.preference", savedPreference.failure);
|
|
32
30
|
return undefined;
|
|
33
|
-
|
|
31
|
+
}
|
|
32
|
+
const preference = savedPreference.success;
|
|
33
|
+
const preferenceKey = JSON.stringify(preference);
|
|
34
|
+
const ownerCache = contextByOwner.get(options) ?? new Map();
|
|
35
|
+
contextByOwner.set(options, ownerCache);
|
|
36
|
+
const previous = cache ? ownerCache.get(sessionId) : undefined;
|
|
34
37
|
const describe = options.model.describe;
|
|
35
|
-
if (
|
|
36
|
-
|
|
38
|
+
if (previous?.cursor === cursor && previous.preference === preferenceKey) {
|
|
39
|
+
if (previous.value === undefined || describe === undefined)
|
|
40
|
+
return previous.value;
|
|
41
|
+
const previousValue = previous.value;
|
|
42
|
+
// A null Session override follows a mutable Agent default. Check the
|
|
43
|
+
// selected window without decoding history again; a changed window is
|
|
44
|
+
// a distinct effective preference for this cursor.
|
|
45
|
+
return yield* describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => {
|
|
46
|
+
if (previousValue.window === contextWindow)
|
|
47
|
+
return previousValue;
|
|
48
|
+
const value = Object.freeze({
|
|
49
|
+
tokens: previousValue.tokens,
|
|
50
|
+
window: contextWindow,
|
|
51
|
+
});
|
|
52
|
+
if (cache && ownerCache.get(sessionId)?.cursor === cursor)
|
|
53
|
+
ownerCache.set(sessionId, {
|
|
54
|
+
cursor,
|
|
55
|
+
preference: preferenceKey,
|
|
56
|
+
value,
|
|
57
|
+
});
|
|
58
|
+
return value;
|
|
59
|
+
}), Effect.catch((error) => Effect.sync(() => {
|
|
60
|
+
reportCatchFailure("session-context.describe", error);
|
|
61
|
+
return previousValue;
|
|
62
|
+
})));
|
|
63
|
+
}
|
|
64
|
+
// At most one cached whole-window read per Session, cursor, and
|
|
65
|
+
// preference: a late describe may publish only while this cursor still
|
|
66
|
+
// owns the cache entry. Reads before the tip bypass the cache and each read
|
|
67
|
+
// the whole window. A cold-cache connection also reads the whole kept
|
|
68
|
+
// window.
|
|
69
|
+
const contextRead = yield* Effect.result(Effect.try({
|
|
70
|
+
try: () => settledContextTokens(options.store, sessionId),
|
|
71
|
+
catch: (error) => error,
|
|
72
|
+
}));
|
|
73
|
+
if (contextRead._tag === "Failure") {
|
|
74
|
+
reportCatchFailure("session-context.figure", contextRead.failure);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const tokens = contextRead.success;
|
|
78
|
+
if (tokens === null) {
|
|
79
|
+
if (cache)
|
|
80
|
+
ownerCache.set(sessionId, {
|
|
81
|
+
cursor,
|
|
82
|
+
preference: preferenceKey,
|
|
83
|
+
value: undefined,
|
|
84
|
+
});
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (describe === undefined) {
|
|
88
|
+
const value = Object.freeze({ tokens, window: null });
|
|
89
|
+
if (cache)
|
|
90
|
+
ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
// Publish the decoded figure before yielding to the host's model
|
|
94
|
+
// description. Concurrent watchers then reuse these tokens rather than
|
|
95
|
+
// decoding the same Session window while `describe` is in flight.
|
|
96
|
+
if (cache)
|
|
97
|
+
ownerCache.set(sessionId, {
|
|
98
|
+
cursor,
|
|
99
|
+
preference: preferenceKey,
|
|
100
|
+
value: Object.freeze({ tokens, window: null }),
|
|
101
|
+
});
|
|
37
102
|
// A null Session override follows a mutable Agent default. Resolve through
|
|
38
103
|
// the host's turn-model selector on every read, without acquiring a turn.
|
|
39
|
-
const window = yield* describe({
|
|
104
|
+
const window = yield* describe({
|
|
105
|
+
sessionId,
|
|
106
|
+
preference,
|
|
107
|
+
}).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
|
|
40
108
|
reportCatchFailure("session-context.describe", error);
|
|
41
109
|
return null;
|
|
42
110
|
})));
|
|
43
|
-
|
|
111
|
+
const value = Object.freeze({ tokens, window });
|
|
112
|
+
if (cache && ownerCache.get(sessionId)?.cursor === cursor)
|
|
113
|
+
ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
|
|
114
|
+
return value;
|
|
44
115
|
});
|
|
45
116
|
}
|
|
@@ -5,13 +5,18 @@ import * as Schema from "effect/Schema";
|
|
|
5
5
|
* the mail it sent: the replying Agent and Session plus BirdDog's settlement
|
|
6
6
|
* facts. Only `acceptMail` stores it, and only the host passes it.
|
|
7
7
|
*/
|
|
8
|
-
export declare const VerifiedAgentReplySchema: Schema.Struct<{
|
|
8
|
+
export declare const VerifiedAgentReplySchema: Schema.Union<readonly [Schema.Struct<{
|
|
9
9
|
readonly commandId: Schema.String;
|
|
10
10
|
readonly outcome: Schema.Literals<readonly ["completed", "failed", "interrupted", "cancelled", "outcome_unknown"]>;
|
|
11
11
|
readonly diagnostic: Schema.optionalKey<Schema.String>;
|
|
12
12
|
readonly agentId: Schema.String;
|
|
13
13
|
readonly sessionId: Schema.String;
|
|
14
|
-
}
|
|
14
|
+
}>, Schema.Struct<{
|
|
15
|
+
readonly outcome: Schema.Literal<"undeliverable">;
|
|
16
|
+
readonly reason: Schema.Literals<readonly ["recipient_offline", "recipient_left"]>;
|
|
17
|
+
readonly agentId: Schema.String;
|
|
18
|
+
readonly sessionId: Schema.optionalKey<Schema.String>;
|
|
19
|
+
}>]>;
|
|
15
20
|
/** A delegated Agent's reply after the admitting host authenticated it. */
|
|
16
21
|
export type VerifiedAgentReply = typeof VerifiedAgentReplySchema.Type;
|
|
17
22
|
/** The tag that marks a host-verified Agent reply in model context. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SettledBirdDogReplySchema, UndeliverableBirdDogReplySchema, } from "@fungi.computer/bird-dog/effect";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
3
|
const Identifier = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
|
|
4
4
|
/**
|
|
@@ -6,11 +6,18 @@ const Identifier = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength
|
|
|
6
6
|
* the mail it sent: the replying Agent and Session plus BirdDog's settlement
|
|
7
7
|
* facts. Only `acceptMail` stores it, and only the host passes it.
|
|
8
8
|
*/
|
|
9
|
-
export const VerifiedAgentReplySchema = Schema.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
export const VerifiedAgentReplySchema = Schema.Union([
|
|
10
|
+
Schema.Struct({
|
|
11
|
+
agentId: Identifier,
|
|
12
|
+
sessionId: Identifier,
|
|
13
|
+
...SettledBirdDogReplySchema.fields,
|
|
14
|
+
}),
|
|
15
|
+
Schema.Struct({
|
|
16
|
+
agentId: Identifier,
|
|
17
|
+
sessionId: Schema.optionalKey(Identifier),
|
|
18
|
+
...UndeliverableBirdDogReplySchema.fields,
|
|
19
|
+
}),
|
|
20
|
+
]);
|
|
14
21
|
/** The tag that marks a host-verified Agent reply in model context. */
|
|
15
22
|
export const AGENT_REPLY_TAG = "fungi_agent_reply";
|
|
16
23
|
// `<` before the tag name, allowing the spacing and case a model would still read as the tag.
|
|
@@ -27,14 +34,16 @@ function attribute(value) {
|
|
|
27
34
|
.replaceAll(">", ">");
|
|
28
35
|
}
|
|
29
36
|
function opening(reply) {
|
|
30
|
-
const attributes = [
|
|
31
|
-
|
|
32
|
-
["session_id", reply.sessionId]
|
|
33
|
-
|
|
34
|
-
["
|
|
35
|
-
];
|
|
36
|
-
if (reply.diagnostic !== undefined)
|
|
37
|
+
const attributes = [["agent_id", reply.agentId]];
|
|
38
|
+
if (reply.sessionId !== undefined)
|
|
39
|
+
attributes.push(["session_id", reply.sessionId]);
|
|
40
|
+
if (reply.outcome !== "undeliverable")
|
|
41
|
+
attributes.push(["command_id", reply.commandId]);
|
|
42
|
+
attributes.push(["outcome", reply.outcome]);
|
|
43
|
+
if (reply.outcome !== "undeliverable" && reply.diagnostic !== undefined)
|
|
37
44
|
attributes.push(["diagnostic", reply.diagnostic]);
|
|
45
|
+
if (reply.outcome === "undeliverable")
|
|
46
|
+
attributes.push(["reason", reply.reason]);
|
|
38
47
|
const rendered = attributes
|
|
39
48
|
.map(([name, value]) => `${name}="${attribute(value)}"`)
|
|
40
49
|
.join(" ");
|
|
@@ -27,10 +27,11 @@ export declare function createFleetDelegationStore(storage: WatchdogSqliteOwner)
|
|
|
27
27
|
read: (requestId: string) => {
|
|
28
28
|
readonly [x: string]: string | number | ArrayBuffer | null;
|
|
29
29
|
} | undefined;
|
|
30
|
-
/** Every delegation
|
|
30
|
+
/** Every recipient-accepted delegation, newest first. */
|
|
31
31
|
sent: () => readonly {
|
|
32
32
|
readonly [x: string]: string | number | ArrayBuffer | null;
|
|
33
33
|
}[];
|
|
34
34
|
reserve(requestId: string, input: string, mail: string): void;
|
|
35
|
-
recordReceipt(requestId: string, receipt: string): void;
|
|
35
|
+
recordReceipt(requestId: string, receipt: string, expectedMail: string, acceptedMail: string): void;
|
|
36
|
+
recordHeld(requestId: string): void;
|
|
36
37
|
};
|
|
@@ -81,24 +81,61 @@ export function createFleetDirectoryStore(sql) {
|
|
|
81
81
|
/** Durable correspondence is frozen before delivery; retries reuse the same row. */
|
|
82
82
|
export function createFleetDelegationStore(storage) {
|
|
83
83
|
storage.sql.exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_delegations (
|
|
84
|
-
request_id TEXT PRIMARY KEY, input TEXT NOT NULL, mail TEXT NOT NULL,
|
|
84
|
+
request_id TEXT PRIMARY KEY, input TEXT NOT NULL, mail TEXT NOT NULL,
|
|
85
|
+
receipt TEXT
|
|
86
|
+
) STRICT`);
|
|
87
|
+
storage.sql.exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_held_delegations (
|
|
88
|
+
request_id TEXT PRIMARY KEY
|
|
85
89
|
) STRICT`);
|
|
86
90
|
return {
|
|
87
91
|
read: (requestId) => storage.sql
|
|
88
|
-
.exec(
|
|
92
|
+
.exec(`SELECT delegation.input, delegation.mail,
|
|
93
|
+
CASE
|
|
94
|
+
WHEN held.request_id IS NOT NULL THEN 'held'
|
|
95
|
+
WHEN delegation.receipt IS NOT NULL THEN 'accepted'
|
|
96
|
+
ELSE 'pending'
|
|
97
|
+
END AS state,
|
|
98
|
+
delegation.receipt
|
|
99
|
+
FROM shiitake_fleet_delegations AS delegation
|
|
100
|
+
LEFT JOIN shiitake_fleet_held_delegations AS held
|
|
101
|
+
ON held.request_id = delegation.request_id
|
|
102
|
+
WHERE delegation.request_id = ?`, requestId)
|
|
89
103
|
.toArray()[0],
|
|
90
|
-
/** Every delegation
|
|
104
|
+
/** Every recipient-accepted delegation, newest first. */
|
|
91
105
|
sent: () => storage.sql
|
|
92
|
-
.exec(
|
|
106
|
+
.exec(`SELECT input, mail, 'accepted' AS state, receipt
|
|
107
|
+
FROM shiitake_fleet_delegations
|
|
108
|
+
WHERE receipt IS NOT NULL
|
|
109
|
+
ORDER BY rowid DESC`)
|
|
93
110
|
.toArray(),
|
|
94
111
|
reserve(requestId, input, mail) {
|
|
95
112
|
storage.transactionSync(() => {
|
|
96
113
|
storage.sql.exec("INSERT OR IGNORE INTO shiitake_fleet_delegations(request_id, input, mail) VALUES (?, ?, ?)", requestId, input, mail);
|
|
97
114
|
});
|
|
98
115
|
},
|
|
99
|
-
recordReceipt(requestId, receipt) {
|
|
116
|
+
recordReceipt(requestId, receipt, expectedMail, acceptedMail) {
|
|
117
|
+
storage.transactionSync(() => {
|
|
118
|
+
const row = storage.sql
|
|
119
|
+
.exec("SELECT mail, receipt FROM shiitake_fleet_delegations WHERE request_id = ?", requestId)
|
|
120
|
+
.toArray()[0];
|
|
121
|
+
if (row === undefined)
|
|
122
|
+
throw new Error("Cannot record acceptance without frozen delegation mail");
|
|
123
|
+
if (row.receipt !== null) {
|
|
124
|
+
if (row.receipt === receipt && row.mail === acceptedMail)
|
|
125
|
+
return;
|
|
126
|
+
throw new Error("Delegation acceptance conflicts with its recorded receipt");
|
|
127
|
+
}
|
|
128
|
+
if (row.mail !== expectedMail)
|
|
129
|
+
throw new Error("Delegation mail changed before acceptance was recorded");
|
|
130
|
+
storage.sql.exec("UPDATE shiitake_fleet_delegations SET mail = ?, receipt = ? WHERE request_id = ? AND receipt IS NULL", acceptedMail, receipt, requestId);
|
|
131
|
+
storage.sql.exec("DELETE FROM shiitake_fleet_held_delegations WHERE request_id = ?", requestId);
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
recordHeld(requestId) {
|
|
100
135
|
storage.transactionSync(() => {
|
|
101
|
-
storage.sql.exec(
|
|
136
|
+
storage.sql.exec(`INSERT OR IGNORE INTO shiitake_fleet_held_delegations (request_id)
|
|
137
|
+
SELECT request_id FROM shiitake_fleet_delegations
|
|
138
|
+
WHERE request_id = ? AND receipt IS NULL`, requestId);
|
|
102
139
|
});
|
|
103
140
|
},
|
|
104
141
|
};
|
|
@@ -125,6 +125,8 @@ export declare function readSessionSummary(owner: SqliteOwner, sessionId: string
|
|
|
125
125
|
* `admitNew` may refuse a new Session; a replayed request is never refused.
|
|
126
126
|
*/
|
|
127
127
|
export declare function createSession(owner: SqliteOwner, requestId: string, admitNew: () => void): Effect.Effect<string, ShiitakeError>;
|
|
128
|
+
/** Reserve or replay a Session inside an enclosing owner transaction. */
|
|
129
|
+
export declare function createSessionWithinTransaction(owner: SqliteOwner, requestId: string, admitNew: () => void): string;
|
|
128
130
|
/** Permanently delete one idle Session; false when it does not exist. */
|
|
129
131
|
export declare function deleteSession(owner: SqliteOwner, input: Readonly<{
|
|
130
132
|
sessionId: string;
|
|
@@ -7,7 +7,7 @@ import { diagnosticsByWorkId, projectCommandSettlement, readClientReplayPage, re
|
|
|
7
7
|
import { sessionIds } from "./identities.js";
|
|
8
8
|
import { openProgress, openToolProgress } from "./progress.js";
|
|
9
9
|
import { encodeStoredSessionEntry, } from "./session-entry.js";
|
|
10
|
-
import { appendSessionEntry, appendCompactionTransitionOwned, appendCompactionNoticeOwned, placeCompactionTransitionOwned, acceptSessionAbort, acceptSessionCancellation, completeSessionLeaf, prepareSessionRun, prepareSessionStore, readSessionEntriesForWork, readSessionCommandAdmission, settleSessionControls, takePendingSessionControls, readSessionEntryById, selectSessionBranch, forkSessionAt, readStoredSessionSummary as readSessionSummaryOwned, readStoredSessionFactPage, createStoredSession, deleteStoredSession, appendOperatorEntry, } from "./session-store.js";
|
|
10
|
+
import { appendSessionEntry, appendCompactionTransitionOwned, appendCompactionNoticeOwned, placeCompactionTransitionOwned, acceptSessionAbort, acceptSessionCancellation, completeSessionLeaf, prepareSessionRun, prepareSessionStore, readSessionEntriesForWork, readSessionCommandAdmission, settleSessionControls, takePendingSessionControls, readSessionEntryById, selectSessionBranch, forkSessionAt, readStoredSessionSummary as readSessionSummaryOwned, readStoredSessionFactPage, createStoredSession, createStoredSessionWithinTransaction, deleteStoredSession, appendOperatorEntry, } from "./session-store.js";
|
|
11
11
|
import { readSessionInboxPage, recordSessionInboxDeletion, } from "./session-inbox.js";
|
|
12
12
|
export * from "./extension-store.js";
|
|
13
13
|
export { readLatestPendingPrompt } from "./client-projection.js";
|
|
@@ -66,7 +66,7 @@ export function cancelSession(owner, sessionId, commandId, onlyIfPending, readQu
|
|
|
66
66
|
return storeEffect(() => {
|
|
67
67
|
const cancellation = acceptSessionCancellation(owner, sessionId, commandId, readQueueHead, record, onlyIfPending, meta);
|
|
68
68
|
if (cancellation?.delivery === "new")
|
|
69
|
-
publishOwnerObservation(owner, sessionId);
|
|
69
|
+
publishOwnerObservation(owner, sessionId, "settlement");
|
|
70
70
|
return cancellation;
|
|
71
71
|
});
|
|
72
72
|
}
|
|
@@ -83,14 +83,14 @@ export function takeSessionControls(owner, input) {
|
|
|
83
83
|
export function settleControls(owner, input) {
|
|
84
84
|
return storeEffect(() => {
|
|
85
85
|
settleSessionControls(owner, input);
|
|
86
|
-
publishOwnerObservation(owner, input.sessionId);
|
|
86
|
+
publishOwnerObservation(owner, input.sessionId, "settlement");
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
/** Resolve Session-scoped abort to private work and record it atomically. */
|
|
90
90
|
export function abortSession(owner, sessionId, readQueueHead, record, meta) {
|
|
91
91
|
return storeEffect(() => {
|
|
92
92
|
const cancellation = acceptSessionAbort(owner, sessionId, readQueueHead, record, meta);
|
|
93
|
-
publishOwnerObservation(owner, sessionId);
|
|
93
|
+
publishOwnerObservation(owner, sessionId, "settlement");
|
|
94
94
|
return cancellation?.accepted;
|
|
95
95
|
});
|
|
96
96
|
}
|
|
@@ -202,6 +202,10 @@ export function readSessionSummary(owner, sessionId) {
|
|
|
202
202
|
export function createSession(owner, requestId, admitNew) {
|
|
203
203
|
return storeEffect(() => createStoredSession(owner, decodeClientSessionCreationRequestId({ requestId }), admitNew));
|
|
204
204
|
}
|
|
205
|
+
/** Reserve or replay a Session inside an enclosing owner transaction. */
|
|
206
|
+
export function createSessionWithinTransaction(owner, requestId, admitNew) {
|
|
207
|
+
return createStoredSessionWithinTransaction(owner, decodeClientSessionCreationRequestId({ requestId }), admitNew);
|
|
208
|
+
}
|
|
205
209
|
/** Permanently delete one idle Session; false when it does not exist. */
|
|
206
210
|
export function deleteSession(owner, input) {
|
|
207
211
|
return storeEffect(() => deleteStoredSession(owner, {
|
|
@@ -273,7 +277,9 @@ function appendRecordedEntry(owner, accepted, entry, findExisting, beforeAppend)
|
|
|
273
277
|
throw rejected("Session fact identity allocation failed");
|
|
274
278
|
});
|
|
275
279
|
if (delivery === "new")
|
|
276
|
-
publishOwnerObservation(owner, accepted.sessionId
|
|
280
|
+
publishOwnerObservation(owner, accepted.sessionId, entry.kind === "run_terminal" || entry.kind === "work_terminal"
|
|
281
|
+
? "settlement"
|
|
282
|
+
: "durable");
|
|
277
283
|
}
|
|
278
284
|
/** Persist an operator-only sandbox diagnostic beside its accepted Session work. */
|
|
279
285
|
export function recordSandboxFailure(owner, accepted, event) {
|
|
@@ -56,7 +56,10 @@ export function contextMessageIds(path, activeWorkId) {
|
|
|
56
56
|
if (entry.value.role === "assistant") {
|
|
57
57
|
lastWorkAssistant.set(entry.workId, row.id);
|
|
58
58
|
}
|
|
59
|
-
if (terminalWork.has(entry.workId) || entry.workId === activeWorkId)
|
|
59
|
+
if ((terminalWork.has(entry.workId) || entry.workId === activeWorkId) &&
|
|
60
|
+
(entry.value.role !== "assistant" ||
|
|
61
|
+
(entry.value.stopReason !== "error" &&
|
|
62
|
+
entry.value.stopReason !== "aborted")))
|
|
60
63
|
messages.add(row.id);
|
|
61
64
|
}
|
|
62
65
|
if ((entry.kind === "compaction" && entry.data.resumesRun) ||
|
|
@@ -70,13 +70,18 @@ declare const StoredSessionEntrySchema: Schema.Union<readonly [Schema.Struct<{
|
|
|
70
70
|
readonly kind: Schema.Literal<"command">;
|
|
71
71
|
readonly meta: Schema.optionalKey<Schema.Codec<import("../../extensions.js").Json, import("../../extensions.js").Json, never, never>>;
|
|
72
72
|
/** Present only when the host admitted this line as a verified Agent reply. */
|
|
73
|
-
readonly reply: Schema.optionalKey<Schema.Struct<{
|
|
73
|
+
readonly reply: Schema.optionalKey<Schema.Union<readonly [Schema.Struct<{
|
|
74
74
|
readonly commandId: Schema.String;
|
|
75
75
|
readonly outcome: Schema.Literals<readonly ["completed", "failed", "interrupted", "cancelled", "outcome_unknown"]>;
|
|
76
76
|
readonly diagnostic: Schema.optionalKey<Schema.String>;
|
|
77
77
|
readonly agentId: Schema.String;
|
|
78
78
|
readonly sessionId: Schema.String;
|
|
79
|
-
}
|
|
79
|
+
}>, Schema.Struct<{
|
|
80
|
+
readonly outcome: Schema.Literal<"undeliverable">;
|
|
81
|
+
readonly reason: Schema.Literals<readonly ["recipient_offline", "recipient_left"]>;
|
|
82
|
+
readonly agentId: Schema.String;
|
|
83
|
+
readonly sessionId: Schema.optionalKey<Schema.String>;
|
|
84
|
+
}>]>>;
|
|
80
85
|
readonly delivery: Schema.optionalKey<Schema.Literals<readonly ["steer", "followUp"]>>;
|
|
81
86
|
readonly requestId: Schema.String;
|
|
82
87
|
readonly targetWorkId: Schema.optionalKey<Schema.String>;
|
|
@@ -357,6 +362,11 @@ export declare function decodeStoredSessionEntry(value: string): Result.Result<{
|
|
|
357
362
|
readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
|
|
358
363
|
readonly agentId: string;
|
|
359
364
|
readonly diagnostic?: string;
|
|
365
|
+
} | {
|
|
366
|
+
readonly reason: "recipient_offline" | "recipient_left";
|
|
367
|
+
readonly outcome: "undeliverable";
|
|
368
|
+
readonly agentId: string;
|
|
369
|
+
readonly sessionId?: string;
|
|
360
370
|
};
|
|
361
371
|
readonly targetWorkId?: string;
|
|
362
372
|
} | {
|
|
@@ -513,8 +523,8 @@ export declare function decodeStoredSessionEntry(value: string): Result.Result<{
|
|
|
513
523
|
readonly workId: string;
|
|
514
524
|
readonly result: "failed";
|
|
515
525
|
readonly trigger: "manual" | "threshold" | "overflow";
|
|
516
|
-
readonly usage?: Usage;
|
|
517
526
|
readonly diagnostic?: string;
|
|
527
|
+
readonly usage?: Usage;
|
|
518
528
|
} | {
|
|
519
529
|
readonly kind: "compaction_notice";
|
|
520
530
|
readonly workId: string;
|
|
@@ -147,6 +147,7 @@ const assistantMessage = Schema.Struct({
|
|
|
147
147
|
data: Schema.optionalKey(jsonValue),
|
|
148
148
|
})),
|
|
149
149
|
errorMessage: Schema.optionalKey(Schema.String),
|
|
150
|
+
providerError: Schema.optionalKey(Schema.String),
|
|
150
151
|
rawStopReason: Schema.optionalKey(Schema.String),
|
|
151
152
|
providerThinkingLevel: Schema.optionalKey(Schema.String),
|
|
152
153
|
endTurn: Schema.optionalKey(Schema.Boolean),
|
|
@@ -254,6 +254,10 @@ export declare function readActiveToolForWork(owner: SqliteOwner, sessionId: str
|
|
|
254
254
|
export declare function createStoredSession(owner: SqliteOwner, requestId: string,
|
|
255
255
|
/** Refuses a new Session under storage pressure; replays skip it. */
|
|
256
256
|
admitNew: () => void): string;
|
|
257
|
+
/** Reserve a Session while the caller owns the enclosing SQLite transaction. */
|
|
258
|
+
export declare function createStoredSessionWithinTransaction(owner: SqliteOwner, requestId: string,
|
|
259
|
+
/** Refuses a new Session under storage pressure; replays skip it. */
|
|
260
|
+
admitNew: () => void): string;
|
|
257
261
|
/**
|
|
258
262
|
* Permanently remove one idle Session: its entries, command admissions,
|
|
259
263
|
* creation receipt and inbox row, so a retried creation request reserves a
|
|
@@ -790,23 +790,27 @@ function ensureSession(owner, sessionId) {
|
|
|
790
790
|
export function createStoredSession(owner, requestId,
|
|
791
791
|
/** Refuses a new Session under storage pressure; replays skip it. */
|
|
792
792
|
admitNew) {
|
|
793
|
-
return owner.transactionSync(() =>
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
793
|
+
return owner.transactionSync(() => createStoredSessionWithinTransaction(owner, requestId, admitNew));
|
|
794
|
+
}
|
|
795
|
+
/** Reserve a Session while the caller owns the enclosing SQLite transaction. */
|
|
796
|
+
export function createStoredSessionWithinTransaction(owner, requestId,
|
|
797
|
+
/** Refuses a new Session under storage pressure; replays skip it. */
|
|
798
|
+
admitNew) {
|
|
799
|
+
const existing = owner.sql
|
|
800
|
+
.exec("SELECT session_id FROM session_creation_receipts WHERE request_id = ?", requestId)
|
|
801
|
+
.toArray();
|
|
802
|
+
const first = existing[0];
|
|
803
|
+
if (first !== undefined) {
|
|
804
|
+
const decoded = decodeSessionCreationReceiptRow(first);
|
|
805
|
+
if (decoded._tag === "Failure")
|
|
806
|
+
throw unavailable("Session creation receipt is invalid");
|
|
807
|
+
return decoded.success.session_id;
|
|
808
|
+
}
|
|
809
|
+
admitNew();
|
|
810
|
+
const sessionId = sessionIds.session.generate();
|
|
811
|
+
ensureSession(owner, sessionId);
|
|
812
|
+
owner.sql.exec("INSERT INTO session_creation_receipts (request_id, session_id) VALUES (?, ?)", requestId, sessionId);
|
|
813
|
+
return sessionId;
|
|
810
814
|
}
|
|
811
815
|
/**
|
|
812
816
|
* Permanently remove one idle Session: its entries, command admissions,
|
|
@@ -2280,9 +2284,12 @@ reply) {
|
|
|
2280
2284
|
function sameReply(left, right) {
|
|
2281
2285
|
return (left?.agentId === right?.agentId &&
|
|
2282
2286
|
left?.sessionId === right?.sessionId &&
|
|
2283
|
-
left?.commandId === right?.commandId &&
|
|
2284
2287
|
left?.outcome === right?.outcome &&
|
|
2285
|
-
left?.
|
|
2288
|
+
(left?.outcome === "undeliverable"
|
|
2289
|
+
? right?.outcome === "undeliverable" && left.reason === right.reason
|
|
2290
|
+
: right?.outcome !== "undeliverable" &&
|
|
2291
|
+
left?.commandId === right?.commandId &&
|
|
2292
|
+
left?.diagnostic === right?.diagnostic));
|
|
2286
2293
|
}
|
|
2287
2294
|
/**
|
|
2288
2295
|
* Write one decoded admission (a prompt or a compaction request) and its one
|
package/dist/remote-client.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as Schema from "effect/Schema";
|
|
|
10
10
|
import * as Scope from "effect/Scope";
|
|
11
11
|
import * as Sink from "effect/Sink";
|
|
12
12
|
import * as Stream from "effect/Stream";
|
|
13
|
-
import { parseAccepted, parseWithdrawResult, parseCancelResult, parseLatestPendingPrompt, parseForkedSession, parseCreatedSession, parseDeletedSession, parseExtensionOutput, parseExtensionStatus, parseExtensionStatuses, parseSessionFrame, parseSessionSnapshot, parseSessionSummaryPage, parseShiitakeError, } from "./client-wire.js";
|
|
13
|
+
import { parseAccepted, parseCommandSettlement, parseWithdrawResult, parseCancelResult, parseLatestPendingPrompt, parseForkedSession, parseCreatedSession, parseDeletedSession, parseExtensionOutput, parseExtensionStatus, parseExtensionStatuses, parseSessionFrame, parseSessionSnapshot, parseSessionSummaryPage, parseShiitakeError, } from "./client-wire.js";
|
|
14
14
|
import { ShiitakeError } from "./errors.js";
|
|
15
15
|
import { decodeClientReadOptions, decodeClientSessionCreationRequestId, decodeClientPageCursor, decodeClientSessionId, decodeClientSubscribeOptions, } from "./internal/client-boundary.js";
|
|
16
16
|
import { toPromiseAgent } from "./internal/effect-client.js";
|
|
@@ -248,6 +248,10 @@ function makeRemoteEffectAgent(options) {
|
|
|
248
248
|
params.set("includeSandboxFailures", "true");
|
|
249
249
|
return request((signal) => fetch(`${addressedUrl}?${params.toString()}`, { signal }), parseSessionSnapshot, "Session read");
|
|
250
250
|
})),
|
|
251
|
+
readSettlement: (commandId, timeoutMs = 0) => ensureOpen.pipe(Effect.andThen(request((signal) => fetch(`${addressedUrl}/settlement?${new URLSearchParams({
|
|
252
|
+
commandId,
|
|
253
|
+
timeoutMs: String(timeoutMs),
|
|
254
|
+
}).toString()}`, { signal }), parseCommandSettlement, "Command settlement"))),
|
|
251
255
|
subscribe: (input) => Stream.unwrap(ensureOpen.pipe(Effect.andThen(Effect.try({
|
|
252
256
|
try: () => decodeClientSubscribeOptions(input),
|
|
253
257
|
catch: (error) => error instanceof ShiitakeError
|
package/dist/testing.d.ts
CHANGED
|
@@ -40,8 +40,11 @@ export type MemoryHost = Readonly<{
|
|
|
40
40
|
agent: ShiitakeOwnerAgent;
|
|
41
41
|
/** Invoke one canonical reconciliation tick; no timer runs in the background. */
|
|
42
42
|
reconcile: () => Promise<void>;
|
|
43
|
-
/** Drain pending Agent work with a 1–1,000 tick budget (default 1,000).
|
|
44
|
-
|
|
43
|
+
/** Drain pending Agent work with a 1–1,000 tick budget (default 1,000).
|
|
44
|
+
* The optional test hook runs after pending work ends, before the snapshot
|
|
45
|
+
* context read, so load tests can end a timed turn at the terminal.
|
|
46
|
+
*/
|
|
47
|
+
settle: (sessionId: string, maxTicks?: number, beforeRead?: () => Promise<void>) => Promise<SessionSnapshot>;
|
|
45
48
|
/** Close the Agent and only the database owned by this helper. */
|
|
46
49
|
close: () => Promise<void>;
|
|
47
50
|
}>;
|
package/dist/testing.js
CHANGED
|
@@ -176,12 +176,14 @@ export async function memoryHost(options = {}) {
|
|
|
176
176
|
return {
|
|
177
177
|
agent,
|
|
178
178
|
reconcile: async () => requireBound().reconcile(),
|
|
179
|
-
settle: async (sessionId, maxTicks) => {
|
|
179
|
+
settle: async (sessionId, maxTicks, beforeRead) => {
|
|
180
180
|
const budget = settlementBudgetSchema.parse(maxTicks);
|
|
181
181
|
for (let tick = 0; tick < budget; tick += 1) {
|
|
182
182
|
const host = requireBound();
|
|
183
|
-
if (!(await host.pendingWork()).pending)
|
|
183
|
+
if (!(await host.pendingWork()).pending) {
|
|
184
|
+
await beforeRead?.();
|
|
184
185
|
return agent.session(sessionId).read();
|
|
186
|
+
}
|
|
185
187
|
await host.reconcile();
|
|
186
188
|
}
|
|
187
189
|
throw new Error(`Memory host did not settle within ${String(budget)} reconciliation ticks`);
|
|
@@ -219,7 +219,12 @@ Host-only identity and admitted command metadata for one delegated send.
|
|
|
219
219
|
```ts
|
|
220
220
|
type AgentControlDirectory = Readonly<{
|
|
221
221
|
list: (signal, options?) => Promise<readonly AgentControlAgent[]>;
|
|
222
|
-
send?: (
|
|
222
|
+
send?: (
|
|
223
|
+
input,
|
|
224
|
+
sourceSessionId,
|
|
225
|
+
signal,
|
|
226
|
+
invocation?,
|
|
227
|
+
) => Promise<Accepted | Held>;
|
|
223
228
|
recall?: (input, sourceSessionId, signal) => Promise<CancelResult>;
|
|
224
229
|
create?: (input, signal) => Promise<AgentControlAgent>;
|
|
225
230
|
setModel?: (input, signal) => Promise<void>;
|
|
@@ -262,10 +267,10 @@ report back, its store predates the current schema, or the Agent is archived.
|
|
|
262
267
|
|
|
263
268
|
## Functions
|
|
264
269
|
|
|
265
|
-
###
|
|
270
|
+
### waitForAgentSettlement()
|
|
266
271
|
|
|
267
272
|
```ts
|
|
268
|
-
function
|
|
273
|
+
function waitForAgentSettlement(
|
|
269
274
|
session,
|
|
270
275
|
commandId,
|
|
271
276
|
timeoutMs,
|
|
@@ -273,16 +278,16 @@ function waitForAgentTerminal(
|
|
|
273
278
|
startedAt?,
|
|
274
279
|
): Promise<
|
|
275
280
|
Readonly<{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
+
acceptedSender: string | null;
|
|
282
|
+
terminal: SessionTerminal;
|
|
283
|
+
content: string;
|
|
284
|
+
usage: readonly Pick<AssistantMessage, "model" | "provider" | "usage">[];
|
|
285
|
+
toolCalls: number;
|
|
281
286
|
}>
|
|
282
287
|
>;
|
|
283
288
|
```
|
|
284
289
|
|
|
285
|
-
|
|
290
|
+
Await one exact durable terminal without opening a transcript observation.
|
|
286
291
|
|
|
287
292
|
#### Parameters
|
|
288
293
|
|
|
@@ -296,9 +301,10 @@ Observe one durable terminal; timeout or abort closes observation, not the work.
|
|
|
296
301
|
|
|
297
302
|
#### Returns
|
|
298
303
|
|
|
299
|
-
`Promise`\<`Readonly`\<\{ `
|
|
300
|
-
`
|
|
301
|
-
`
|
|
304
|
+
`Promise`\<`Readonly`\<\{ `acceptedSender`: `string` \| `null`; `terminal`:
|
|
305
|
+
[`SessionTerminal`](client.md#sessionterminal); `content`: `string`; `usage`:
|
|
306
|
+
readonly `Pick`\<`AssistantMessage`, `"model"` \| `"provider"` \| `"usage"`\>[];
|
|
307
|
+
`toolCalls`: `number`; \}\>\>
|
|
302
308
|
|
|
303
309
|
---
|
|
304
310
|
|