@fungi.computer/shiitake 0.4.0 → 0.6.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 +1 -10
- package/dist/agent-control.d.ts +98 -5
- package/dist/agent-control.js +111 -8
- package/dist/client-entry.d.ts +20 -0
- package/dist/client-entry.js +5 -0
- package/dist/client-visitor.d.ts +31 -0
- package/dist/client-visitor.js +226 -0
- package/dist/client-wire.d.ts +5 -0
- package/dist/client-wire.js +6 -0
- package/dist/client.d.ts +4 -3
- package/dist/client.js +1 -0
- package/dist/delegation.d.ts +100 -77
- package/dist/delegation.js +73 -23
- package/dist/effect.d.ts +25 -0
- package/dist/effect.js +14 -1
- package/dist/extensions.d.ts +21 -2
- package/dist/extensions.js +1 -1
- package/dist/fleet.d.ts +22 -2
- package/dist/fleet.js +127 -36
- package/dist/hosted.js +3 -0
- package/dist/http-routes.d.ts +11 -0
- package/dist/internal/accepted-run-executor.js +2 -1
- package/dist/internal/admission-door.d.ts +5 -0
- package/dist/internal/admission-door.js +34 -3
- package/dist/internal/contracts.d.ts +14 -1
- package/dist/internal/coordinator.js +8 -2
- package/dist/internal/effect-client.js +1 -1
- package/dist/internal/effect-contracts.d.ts +4 -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 +13 -5
- 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/run-scope.d.ts +14 -0
- package/dist/internal/run-scope.js +32 -9
- package/dist/internal/runtime-coordinator.js +8 -1
- package/dist/internal/runtime-reconciliation.js +119 -24
- 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 +9 -1
- package/dist/internal/woodstock/fleet-store.d.ts +1 -0
- package/dist/internal/woodstock/fleet-store.js +28 -1
- package/dist/internal/woodstock/index.d.ts +4 -0
- package/dist/internal/woodstock/index.js +7 -0
- package/dist/internal/woodstock/session-delegator.d.ts +12 -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 +2 -0
- package/dist/internal/woodstock/session-store.js +20 -0
- package/dist/internal/woodstock/work-senders.d.ts +32 -0
- package/dist/internal/woodstock/work-senders.js +70 -0
- package/dist/remote-client.js +7 -2
- package/dist/server-visitor-stream.d.ts +13 -0
- package/dist/server-visitor-stream.js +115 -0
- package/dist/session-attention.d.ts +30 -0
- package/dist/session-attention.js +14 -0
- package/docs/api/agent-control.md +209 -13
- package/docs/api/client-entry.md +3 -0
- package/docs/api/client.md +136 -1
- package/docs/api/cloudflare.md +7 -1
- package/docs/api/delegation.md +234 -61
- package/docs/api/effect.md +16 -6
- package/docs/api/extensions.md +65 -4
- package/docs/api/fleet.md +69 -16
- package/docs/api/index.md +6 -4
- package/package.json +10 -6
|
@@ -86,11 +86,16 @@ export function createFleetDelegationStore(storage) {
|
|
|
86
86
|
) STRICT`);
|
|
87
87
|
storage.sql.exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_held_delegations (
|
|
88
88
|
request_id TEXT PRIMARY KEY
|
|
89
|
+
) STRICT`);
|
|
90
|
+
storage.sql
|
|
91
|
+
.exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_undeliverable_delegations (
|
|
92
|
+
request_id TEXT PRIMARY KEY
|
|
89
93
|
) STRICT`);
|
|
90
94
|
return {
|
|
91
95
|
read: (requestId) => storage.sql
|
|
92
96
|
.exec(`SELECT delegation.input, delegation.mail,
|
|
93
97
|
CASE
|
|
98
|
+
WHEN terminal.request_id IS NOT NULL THEN 'undeliverable'
|
|
94
99
|
WHEN held.request_id IS NOT NULL THEN 'held'
|
|
95
100
|
WHEN delegation.receipt IS NOT NULL THEN 'accepted'
|
|
96
101
|
ELSE 'pending'
|
|
@@ -99,6 +104,8 @@ export function createFleetDelegationStore(storage) {
|
|
|
99
104
|
FROM shiitake_fleet_delegations AS delegation
|
|
100
105
|
LEFT JOIN shiitake_fleet_held_delegations AS held
|
|
101
106
|
ON held.request_id = delegation.request_id
|
|
107
|
+
LEFT JOIN shiitake_fleet_undeliverable_delegations AS terminal
|
|
108
|
+
ON terminal.request_id = delegation.request_id
|
|
102
109
|
WHERE delegation.request_id = ?`, requestId)
|
|
103
110
|
.toArray()[0],
|
|
104
111
|
/** Every recipient-accepted delegation, newest first. */
|
|
@@ -135,7 +142,27 @@ export function createFleetDelegationStore(storage) {
|
|
|
135
142
|
storage.transactionSync(() => {
|
|
136
143
|
storage.sql.exec(`INSERT OR IGNORE INTO shiitake_fleet_held_delegations (request_id)
|
|
137
144
|
SELECT request_id FROM shiitake_fleet_delegations
|
|
138
|
-
WHERE request_id = ? AND receipt IS NULL
|
|
145
|
+
WHERE request_id = ? AND receipt IS NULL
|
|
146
|
+
AND NOT EXISTS (
|
|
147
|
+
SELECT 1 FROM shiitake_fleet_undeliverable_delegations
|
|
148
|
+
WHERE request_id = ?
|
|
149
|
+
)`, requestId, requestId);
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
recordUndeliverable(requestId) {
|
|
153
|
+
return storage.transactionSync(() => {
|
|
154
|
+
const held = storage.sql
|
|
155
|
+
.exec(`SELECT delegation.request_id
|
|
156
|
+
FROM shiitake_fleet_delegations AS delegation
|
|
157
|
+
JOIN shiitake_fleet_held_delegations AS held
|
|
158
|
+
ON held.request_id = delegation.request_id
|
|
159
|
+
WHERE delegation.request_id = ? AND delegation.receipt IS NULL`, requestId)
|
|
160
|
+
.toArray()[0];
|
|
161
|
+
if (held === undefined)
|
|
162
|
+
return false;
|
|
163
|
+
storage.sql.exec("INSERT OR IGNORE INTO shiitake_fleet_undeliverable_delegations(request_id) VALUES (?)", requestId);
|
|
164
|
+
storage.sql.exec("DELETE FROM shiitake_fleet_held_delegations WHERE request_id = ?", requestId);
|
|
165
|
+
return true;
|
|
139
166
|
});
|
|
140
167
|
},
|
|
141
168
|
};
|
|
@@ -15,6 +15,8 @@ export * from "./extension-store.js";
|
|
|
15
15
|
export { readLatestPendingPrompt } from "./client-projection.js";
|
|
16
16
|
export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
|
|
17
17
|
export * from "./session-attention.js";
|
|
18
|
+
export * from "./work-senders.js";
|
|
19
|
+
export * from "./session-delegator.js";
|
|
18
20
|
export { deliverSessionGrants, prepareSessionGrants, readSessionGrants, type GrantsDelivery, type GrantsDeliveryResult, } from "./session-grants.js";
|
|
19
21
|
export * from "./progress.js";
|
|
20
22
|
export * from "./compaction-activity.js";
|
|
@@ -138,6 +140,8 @@ export declare function readSessionFactPage(owner: SqliteOwner, after?: string):
|
|
|
138
140
|
export declare function readColdSessionPage(owner: SqliteOwner, request: ColdSessionPageRequest): Effect.Effect<ColdSessionPage, ShiitakeError>;
|
|
139
141
|
/** Read one bounded inbox page in tier, then newest-activity order. */
|
|
140
142
|
export declare function readSessionInbox(owner: SqliteOwner, after: string | undefined): Effect.Effect<import("../../client.js").SessionSummaryPage, ShiitakeError>;
|
|
143
|
+
/** Read a Session's open attention items from the store alone. */
|
|
144
|
+
export declare function readStoredSessionAttention(owner: SqliteOwner, sessionId: string): Effect.Effect<readonly import("../../client.js").SessionAttention[], ShiitakeError>;
|
|
141
145
|
/** Read one strict-after page from Woodstock's selected lineage. */
|
|
142
146
|
export declare function readSessionReplayPage(owner: SqliteOwner, sessionId: string, after: string | undefined, delivery: "replay" | "live", activity: ObservedActivity, projectExtensions?: () => SessionExtensionSlots, includeSandboxFailures?: boolean): Effect.Effect<ClientReplayPage, ShiitakeError>;
|
|
143
147
|
/** Persist an operator-only sandbox diagnostic beside its accepted Session work. */
|
|
@@ -9,10 +9,13 @@ import { openProgress, openToolProgress } from "./progress.js";
|
|
|
9
9
|
import { encodeStoredSessionEntry, } from "./session-entry.js";
|
|
10
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
|
+
import { readSessionAttention } from "./session-attention.js";
|
|
12
13
|
export * from "./extension-store.js";
|
|
13
14
|
export { readLatestPendingPrompt } from "./client-projection.js";
|
|
14
15
|
export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
|
|
15
16
|
export * from "./session-attention.js";
|
|
17
|
+
export * from "./work-senders.js";
|
|
18
|
+
export * from "./session-delegator.js";
|
|
16
19
|
export { deliverSessionGrants, prepareSessionGrants, readSessionGrants, } from "./session-grants.js";
|
|
17
20
|
export * from "./progress.js";
|
|
18
21
|
export * from "./compaction-activity.js";
|
|
@@ -228,6 +231,10 @@ export function readColdSessionPage(owner, request) {
|
|
|
228
231
|
export function readSessionInbox(owner, after) {
|
|
229
232
|
return storeEffect(() => readSessionInboxPage(owner, after));
|
|
230
233
|
}
|
|
234
|
+
/** Read a Session's open attention items from the store alone. */
|
|
235
|
+
export function readStoredSessionAttention(owner, sessionId) {
|
|
236
|
+
return storeEffect(() => readSessionAttention(owner, sessionId));
|
|
237
|
+
}
|
|
231
238
|
/** Read one strict-after page from Woodstock's selected lineage. */
|
|
232
239
|
export function readSessionReplayPage(owner, sessionId, after, delivery, activity, projectExtensions = () => ({}), includeSandboxFailures = false) {
|
|
233
240
|
return storeEffect(() => readClientReplayPage(owner, decodeClientSessionId(sessionId), after, delivery, activity, projectExtensions, includeSandboxFailures));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BirdDogDelegator, BirdDogMail } from "@fungi.computer/bird-dog";
|
|
2
|
+
import type { SqliteOwner } from "../contracts.js";
|
|
3
|
+
/** Woodstock keeps each Session's delegator of record beside the Session. */
|
|
4
|
+
export declare function prepareSessionDelegators(owner: SqliteOwner): void;
|
|
5
|
+
/**
|
|
6
|
+
* Record who sent a Session its work first, in the admitting transaction.
|
|
7
|
+
* Only an original mail that asks for replies names a delegator; the first one
|
|
8
|
+
* stays, so the relationship never moves once it exists.
|
|
9
|
+
*/
|
|
10
|
+
export declare function recordSessionDelegator(owner: SqliteOwner, sessionId: string, mail: BirdDogMail | undefined): void;
|
|
11
|
+
/** The Session's delegator of record, if any Agent has sent it work. */
|
|
12
|
+
export declare function readSessionDelegator(owner: SqliteOwner, sessionId: string): BirdDogDelegator | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { BirdDogAgentReferenceSchema } from "@fungi.computer/bird-dog/effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
const decodeRow = Schema.decodeUnknownSync(Schema.Struct({
|
|
4
|
+
mail_key: Schema.String,
|
|
5
|
+
agent_id: Schema.String,
|
|
6
|
+
reply_to_json: Schema.fromJsonString(Schema.Array(BirdDogAgentReferenceSchema)),
|
|
7
|
+
}));
|
|
8
|
+
const encodeReplyTo = Schema.encodeSync(Schema.fromJsonString(Schema.Array(BirdDogAgentReferenceSchema)));
|
|
9
|
+
/** Woodstock keeps each Session's delegator of record beside the Session. */
|
|
10
|
+
export function prepareSessionDelegators(owner) {
|
|
11
|
+
owner.sql.exec(`CREATE TABLE IF NOT EXISTS session_delegators (
|
|
12
|
+
session_id TEXT PRIMARY KEY,
|
|
13
|
+
mail_key TEXT NOT NULL,
|
|
14
|
+
agent_id TEXT NOT NULL,
|
|
15
|
+
reply_to_json TEXT NOT NULL
|
|
16
|
+
)`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Record who sent a Session its work first, in the admitting transaction.
|
|
20
|
+
* Only an original mail that asks for replies names a delegator; the first one
|
|
21
|
+
* stays, so the relationship never moves once it exists.
|
|
22
|
+
*/
|
|
23
|
+
export function recordSessionDelegator(owner, sessionId, mail) {
|
|
24
|
+
if (mail === undefined ||
|
|
25
|
+
mail.reply !== undefined ||
|
|
26
|
+
mail.from.kind !== "agent" ||
|
|
27
|
+
mail.replyTo.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
owner.sql.exec("INSERT OR IGNORE INTO session_delegators (session_id, mail_key, agent_id, reply_to_json) VALUES (?, ?, ?, ?)", sessionId, mail.key, mail.to.agentId, encodeReplyTo(mail.replyTo));
|
|
30
|
+
}
|
|
31
|
+
/** The Session's delegator of record, if any Agent has sent it work. */
|
|
32
|
+
export function readSessionDelegator(owner, sessionId) {
|
|
33
|
+
const [row] = owner.sql
|
|
34
|
+
.exec("SELECT mail_key, agent_id, reply_to_json FROM session_delegators WHERE session_id = ?", sessionId)
|
|
35
|
+
.toArray();
|
|
36
|
+
if (row === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
const decoded = decodeRow(row);
|
|
39
|
+
return {
|
|
40
|
+
key: decoded.mail_key,
|
|
41
|
+
agentId: decoded.agent_id,
|
|
42
|
+
replyTo: decoded.reply_to_json,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -74,6 +74,11 @@ declare const StoredSessionEntrySchema: 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
|
+
readonly waitingOnYou: Schema.optionalKey<Schema.$Array<Schema.Struct<{
|
|
78
|
+
readonly itemId: Schema.String;
|
|
79
|
+
readonly kind: Schema.String;
|
|
80
|
+
readonly summary: Schema.String;
|
|
81
|
+
}>>>;
|
|
77
82
|
readonly agentId: Schema.String;
|
|
78
83
|
readonly sessionId: Schema.String;
|
|
79
84
|
}>, Schema.Struct<{
|
|
@@ -357,15 +362,20 @@ export declare function decodeStoredSessionEntry(value: string): Result.Result<{
|
|
|
357
362
|
readonly delivery?: "followUp" | "steer";
|
|
358
363
|
readonly meta?: import("../../extensions.js").Json;
|
|
359
364
|
readonly reply?: {
|
|
365
|
+
readonly agentId: string;
|
|
360
366
|
readonly sessionId: string;
|
|
361
367
|
readonly commandId: string;
|
|
362
368
|
readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
|
|
363
|
-
readonly agentId: string;
|
|
364
369
|
readonly diagnostic?: string;
|
|
370
|
+
readonly waitingOnYou?: readonly {
|
|
371
|
+
readonly itemId: string;
|
|
372
|
+
readonly kind: string;
|
|
373
|
+
readonly summary: string;
|
|
374
|
+
}[];
|
|
365
375
|
} | {
|
|
376
|
+
readonly agentId: string;
|
|
366
377
|
readonly reason: "recipient_offline" | "recipient_left";
|
|
367
378
|
readonly outcome: "undeliverable";
|
|
368
|
-
readonly agentId: string;
|
|
369
379
|
readonly sessionId?: string;
|
|
370
380
|
};
|
|
371
381
|
readonly targetWorkId?: string;
|
|
@@ -237,6 +237,8 @@ export declare function settleSessionControls(owner: SqliteOwner, input: Readonl
|
|
|
237
237
|
export declare function readSessionCommandAdmission(owner: SqliteOwner, sessionId: string, id: string): StoredCommandAdmission | undefined;
|
|
238
238
|
/** Read one stable private command admission by accepted request identity. */
|
|
239
239
|
export declare function readSessionCommandAdmissionByRequest(owner: SqliteOwner, sessionId: string, requestId: string): StoredCommandAdmission | undefined;
|
|
240
|
+
/** Probe the indexed terminal facts without materializing a live work's body. */
|
|
241
|
+
export declare function hasSessionWorkTerminal(owner: SqliteOwner, sessionId: string, workId: string): boolean;
|
|
240
242
|
/** Read entries correlated to one accepted work identity in Session order. */
|
|
241
243
|
export declare function readSessionEntriesForWork(owner: SqliteOwner, sessionId: string, workId: string): readonly StoredSessionRow[];
|
|
242
244
|
/** Read the latest retained live-retry event for one accepted work item. */
|
|
@@ -7,6 +7,8 @@ import { compactionSummaryMessage } from "../compaction/utils.js";
|
|
|
7
7
|
import { SessionTitleSchema, sessionTitleLimit } from "../client-boundary.js";
|
|
8
8
|
import { commandTimestamp, decodeStoredSessionEntry, encodeStoredSessionEntry, isCompactCommand, commandContextMessage, operatorContextMessage, promptCommand, } from "./session-entry.js";
|
|
9
9
|
import { prepareSessionAttention } from "./session-attention.js";
|
|
10
|
+
import { prepareWorkSenders } from "./work-senders.js";
|
|
11
|
+
import { prepareSessionDelegators } from "./session-delegator.js";
|
|
10
12
|
import { copySessionGrants, prepareSessionGrants } from "./session-grants.js";
|
|
11
13
|
import { prepareSessionInbox, touchSessionInbox } from "./session-inbox.js";
|
|
12
14
|
import { sessionIds } from "./identities.js";
|
|
@@ -680,6 +682,18 @@ function readSessionCommandAdmissionByWorkId(owner, sessionId, workId) {
|
|
|
680
682
|
export function readSessionCommandAdmissionByRequest(owner, sessionId, requestId) {
|
|
681
683
|
return readCommandAdmission(owner, sessionId, { requestId });
|
|
682
684
|
}
|
|
685
|
+
/** Probe the indexed terminal facts without materializing a live work's body. */
|
|
686
|
+
export function hasSessionWorkTerminal(owner, sessionId, workId) {
|
|
687
|
+
// Keep the readability check even when the work has no terminal rows.
|
|
688
|
+
readSessionRow(owner, sessionId);
|
|
689
|
+
return (owner.sql
|
|
690
|
+
.exec(`SELECT 1 AS present FROM session_entries
|
|
691
|
+
INDEXED BY idx_session_entries_work_terminal
|
|
692
|
+
WHERE session_id = ? AND work_id = ?
|
|
693
|
+
AND type IN ('run_terminal', 'work_terminal')
|
|
694
|
+
LIMIT 1`, sessionId, workId)
|
|
695
|
+
.toArray().length > 0);
|
|
696
|
+
}
|
|
683
697
|
/** Read entries correlated to one accepted work identity in Session order. */
|
|
684
698
|
export function readSessionEntriesForWork(owner, sessionId, workId) {
|
|
685
699
|
// Refuse the Session before selecting entries: a damaged work may have no
|
|
@@ -832,6 +846,7 @@ export function deleteStoredSession(owner, input) {
|
|
|
832
846
|
"DELETE FROM session_command_admissions WHERE session_id = ?",
|
|
833
847
|
// The inbox row references the Session; it goes in the same transaction.
|
|
834
848
|
"DELETE FROM session_attention WHERE session_id = ?",
|
|
849
|
+
"DELETE FROM session_work_senders WHERE session_id = ?",
|
|
835
850
|
"DELETE FROM session_tool_grants WHERE session_id = ?",
|
|
836
851
|
"DELETE FROM session_inbox WHERE session_id = ?",
|
|
837
852
|
"DELETE FROM session_creation_receipts WHERE session_id = ?",
|
|
@@ -1128,6 +1143,8 @@ export function prepareSessionStore(owner) {
|
|
|
1128
1143
|
createSessionEntryTable(owner);
|
|
1129
1144
|
prepareSessionDocuments(owner);
|
|
1130
1145
|
prepareSessionAttention(owner);
|
|
1146
|
+
prepareWorkSenders(owner);
|
|
1147
|
+
prepareSessionDelegators(owner);
|
|
1131
1148
|
prepareSessionGrants(owner);
|
|
1132
1149
|
prepareSessionInbox(owner);
|
|
1133
1150
|
owner.sql.exec(`
|
|
@@ -1143,6 +1160,9 @@ export function prepareSessionStore(owner) {
|
|
|
1143
1160
|
ON session_entries(session_id, request_id)
|
|
1144
1161
|
WHERE request_id IS NOT NULL`);
|
|
1145
1162
|
owner.sql.exec("CREATE INDEX IF NOT EXISTS idx_session_entries_work ON session_entries(work_id, sequence)");
|
|
1163
|
+
owner.sql.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_work_terminal
|
|
1164
|
+
ON session_entries(session_id, work_id)
|
|
1165
|
+
WHERE type IN ('run_terminal', 'work_terminal')`);
|
|
1146
1166
|
// Context and preference reads look up these rare older facts directly
|
|
1147
1167
|
// rather than walking the history behind them.
|
|
1148
1168
|
owner.sql.exec(`CREATE INDEX IF NOT EXISTS idx_session_entries_model_preference
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type AttentionRecipient } from "../../session-attention.js";
|
|
2
|
+
import type { SqliteOwner } from "../contracts.js";
|
|
3
|
+
/** One open item of a Session that waits on the Agent that sent its work. */
|
|
4
|
+
export type WaitingOnYou = Readonly<{
|
|
5
|
+
itemId: string;
|
|
6
|
+
kind: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* Woodstock records which Agent Session sent each accepted run, beside the
|
|
11
|
+
* run's admission. Attention that an extension raises during a run waits on
|
|
12
|
+
* this sender first; a run no Agent sent has no row.
|
|
13
|
+
*/
|
|
14
|
+
export declare function prepareWorkSenders(owner: SqliteOwner): void;
|
|
15
|
+
/** Record the sender of one accepted run; a replay writes the same row. */
|
|
16
|
+
export declare function recordWorkSender(owner: SqliteOwner, sessionId: string, workId: string, sender: AttentionRecipient): void;
|
|
17
|
+
/** The Agent Session that sent this run, or undefined when none did. */
|
|
18
|
+
export declare function readWorkSender(owner: SqliteOwner, sessionId: string, workId: string): AttentionRecipient | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The Session's open attention that waits on the sender of this run, newest
|
|
21
|
+
* first as stored, bounded. Escalated items wait on someone else and are
|
|
22
|
+
* absent; a run no Agent sent names none.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readWaitingOnSender(owner: SqliteOwner, sessionId: string, workId: string): readonly WaitingOnYou[];
|
|
25
|
+
/**
|
|
26
|
+
* The Session's open attention that waits on any of these Agent Sessions,
|
|
27
|
+
* newest first as stored, bounded: what a reply or report to them names.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readWaitingOn(owner: SqliteOwner, sessionId: string, recipients: readonly Readonly<{
|
|
30
|
+
agentId: string;
|
|
31
|
+
sessionId: string;
|
|
32
|
+
}>[]): readonly WaitingOnYou[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { AttentionRecipientSchema, } from "../../session-attention.js";
|
|
3
|
+
import { readSessionAttention } from "./session-attention.js";
|
|
4
|
+
/** The most open items one settlement names as waiting on its sender. */
|
|
5
|
+
const maximumWaitingOnYou = 8;
|
|
6
|
+
const decodeRow = Schema.decodeUnknownSync(Schema.Struct({
|
|
7
|
+
agent_id: Schema.String,
|
|
8
|
+
sender_session_id: Schema.String,
|
|
9
|
+
name: Schema.String,
|
|
10
|
+
}));
|
|
11
|
+
const decodeRecipient = Schema.decodeUnknownSync(AttentionRecipientSchema);
|
|
12
|
+
/**
|
|
13
|
+
* Woodstock records which Agent Session sent each accepted run, beside the
|
|
14
|
+
* run's admission. Attention that an extension raises during a run waits on
|
|
15
|
+
* this sender first; a run no Agent sent has no row.
|
|
16
|
+
*/
|
|
17
|
+
export function prepareWorkSenders(owner) {
|
|
18
|
+
owner.sql.exec(`CREATE TABLE IF NOT EXISTS session_work_senders (
|
|
19
|
+
session_id TEXT NOT NULL,
|
|
20
|
+
work_id TEXT NOT NULL,
|
|
21
|
+
agent_id TEXT NOT NULL,
|
|
22
|
+
sender_session_id TEXT NOT NULL,
|
|
23
|
+
name TEXT NOT NULL,
|
|
24
|
+
PRIMARY KEY (session_id, work_id)
|
|
25
|
+
)`);
|
|
26
|
+
}
|
|
27
|
+
/** Record the sender of one accepted run; a replay writes the same row. */
|
|
28
|
+
export function recordWorkSender(owner, sessionId, workId, sender) {
|
|
29
|
+
owner.sql.exec(`INSERT OR IGNORE INTO session_work_senders
|
|
30
|
+
(session_id, work_id, agent_id, sender_session_id, name)
|
|
31
|
+
VALUES (?, ?, ?, ?, ?)`, sessionId, workId, sender.agentId, sender.sessionId, sender.name);
|
|
32
|
+
}
|
|
33
|
+
/** The Agent Session that sent this run, or undefined when none did. */
|
|
34
|
+
export function readWorkSender(owner, sessionId, workId) {
|
|
35
|
+
const raw = owner.sql
|
|
36
|
+
.exec("SELECT agent_id, sender_session_id, name FROM session_work_senders WHERE session_id = ? AND work_id = ?", sessionId, workId)
|
|
37
|
+
.toArray()[0];
|
|
38
|
+
if (raw === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
const row = decodeRow(raw);
|
|
41
|
+
return decodeRecipient({
|
|
42
|
+
agentId: row.agent_id,
|
|
43
|
+
sessionId: row.sender_session_id,
|
|
44
|
+
name: row.name,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The Session's open attention that waits on the sender of this run, newest
|
|
49
|
+
* first as stored, bounded. Escalated items wait on someone else and are
|
|
50
|
+
* absent; a run no Agent sent names none.
|
|
51
|
+
*/
|
|
52
|
+
export function readWaitingOnSender(owner, sessionId, workId) {
|
|
53
|
+
const sender = readWorkSender(owner, sessionId, workId);
|
|
54
|
+
return sender === undefined ? [] : readWaitingOn(owner, sessionId, [sender]);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The Session's open attention that waits on any of these Agent Sessions,
|
|
58
|
+
* newest first as stored, bounded: what a reply or report to them names.
|
|
59
|
+
*/
|
|
60
|
+
export function readWaitingOn(owner, sessionId, recipients) {
|
|
61
|
+
return readSessionAttention(owner, sessionId)
|
|
62
|
+
.filter((item) => recipients.some((recipient) => item.waitingOn?.agentId === recipient.agentId &&
|
|
63
|
+
item.waitingOn.sessionId === recipient.sessionId))
|
|
64
|
+
.slice(0, maximumWaitingOnYou)
|
|
65
|
+
.map((item) => ({
|
|
66
|
+
itemId: item.id,
|
|
67
|
+
kind: item.kind,
|
|
68
|
+
summary: item.summary,
|
|
69
|
+
}));
|
|
70
|
+
}
|
package/dist/remote-client.js
CHANGED
|
@@ -72,7 +72,12 @@ function remoteEvents(url, clientClosed, sources, eventSource) {
|
|
|
72
72
|
closed: false,
|
|
73
73
|
};
|
|
74
74
|
const emit = (kind, event) => {
|
|
75
|
-
Queue.offerUnsafe(queue, { kind, event })
|
|
75
|
+
if (!Queue.offerUnsafe(queue, { kind, event })) {
|
|
76
|
+
// A dropped frame breaks this watch's ordering. Fail instead
|
|
77
|
+
// of delivering an incomplete history to the client.
|
|
78
|
+
Queue.failCauseUnsafe(queue, Cause.fail(unavailable("Session stream client fell behind")));
|
|
79
|
+
Effect.runSync(closeRemoteSource(active, sources));
|
|
80
|
+
}
|
|
76
81
|
};
|
|
77
82
|
active.source.addEventListener("snapshot", (event) => {
|
|
78
83
|
emit("snapshot", event);
|
|
@@ -91,7 +96,7 @@ function remoteEvents(url, clientClosed, sources, eventSource) {
|
|
|
91
96
|
return active;
|
|
92
97
|
},
|
|
93
98
|
catch: () => unavailable("Session stream could not open"),
|
|
94
|
-
}), (active) => closeRemoteSource(active, sources))).pipe(Stream.mapEffect(decodeRemoteEvent), Stream.interruptWhen(clientClosed.await));
|
|
99
|
+
}), (active) => closeRemoteSource(active, sources)), { bufferSize: 256, strategy: "dropping" }).pipe(Stream.mapEffect(decodeRemoteEvent), Stream.interruptWhen(clientClosed.await));
|
|
95
100
|
}
|
|
96
101
|
function sessionUrl(baseUrl, sessionId) {
|
|
97
102
|
return `${baseUrl}/sessions/${encodeURIComponent(sessionId)}`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SessionSnapshot, SessionWatch } from "./client.js";
|
|
2
|
+
export type VisitorStreamError = "channel_visitor_stream_expiry_failed" | "channel_visitor_stream_close_failed" | "channel_visitor_stream_heartbeat_failed" | "channel_visitor_stream_failed";
|
|
3
|
+
export declare function visitorStream(options: Readonly<{
|
|
4
|
+
watch: SessionWatch;
|
|
5
|
+
visitorId: string;
|
|
6
|
+
filter: (snapshot: SessionSnapshot) => SessionSnapshot;
|
|
7
|
+
expiresAt: number;
|
|
8
|
+
now: () => number;
|
|
9
|
+
release: () => void;
|
|
10
|
+
canRead: () => Promise<boolean>;
|
|
11
|
+
read: () => Promise<SessionSnapshot>;
|
|
12
|
+
onError: (event: VisitorStreamError, cause: unknown) => void;
|
|
13
|
+
}>): ReadableStream<Uint8Array>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { SSEStreamingApi } from "hono/streaming";
|
|
2
|
+
import { visitorSnapshot } from "./client-visitor.js";
|
|
3
|
+
export function visitorStream(options) {
|
|
4
|
+
const { readable, writable } = new TransformStream();
|
|
5
|
+
const stream = new SSEStreamingApi(writable, readable);
|
|
6
|
+
let closing;
|
|
7
|
+
let ending = false;
|
|
8
|
+
let heartbeat;
|
|
9
|
+
const expiry = setTimeout(() => {
|
|
10
|
+
void close().catch((cause) => {
|
|
11
|
+
options.onError("channel_visitor_stream_expiry_failed", cause);
|
|
12
|
+
});
|
|
13
|
+
}, Math.max(0, options.expiresAt - options.now()));
|
|
14
|
+
const close = () => {
|
|
15
|
+
if (closing !== undefined)
|
|
16
|
+
return closing;
|
|
17
|
+
ending = true;
|
|
18
|
+
clearTimeout(expiry);
|
|
19
|
+
clearTimeout(heartbeat);
|
|
20
|
+
options.release();
|
|
21
|
+
closing = Promise.resolve().then(async () => {
|
|
22
|
+
stream.abort();
|
|
23
|
+
try {
|
|
24
|
+
await options.watch.close();
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await stream.close();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return closing;
|
|
31
|
+
};
|
|
32
|
+
stream.onAbort(() => {
|
|
33
|
+
void close().catch((cause) => {
|
|
34
|
+
options.onError("channel_visitor_stream_close_failed", cause);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
const admitted = async () => !ending &&
|
|
38
|
+
options.now() < options.expiresAt &&
|
|
39
|
+
(await options.canRead()) &&
|
|
40
|
+
!ending;
|
|
41
|
+
const beat = async () => {
|
|
42
|
+
if (!(await admitted())) {
|
|
43
|
+
await close();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await stream.write(": heartbeat\n\n");
|
|
47
|
+
if (!ending)
|
|
48
|
+
heartbeat = setTimeout(() => {
|
|
49
|
+
void beat().catch((cause) => {
|
|
50
|
+
options.onError("channel_visitor_stream_heartbeat_failed", cause);
|
|
51
|
+
void close().catch((closeCause) => {
|
|
52
|
+
options.onError("channel_visitor_stream_close_failed", closeCause);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}, 30_000);
|
|
56
|
+
};
|
|
57
|
+
const publish = async (snapshot, progressOnly) => {
|
|
58
|
+
if (!(await admitted()))
|
|
59
|
+
return false;
|
|
60
|
+
const baseline = visitorSnapshot(options.filter(snapshot), options.visitorId);
|
|
61
|
+
await stream.writeSSE(progressOnly
|
|
62
|
+
? {
|
|
63
|
+
event: "frame",
|
|
64
|
+
data: JSON.stringify({
|
|
65
|
+
kind: "progress",
|
|
66
|
+
progress: baseline.progress,
|
|
67
|
+
activity: baseline.activity,
|
|
68
|
+
}),
|
|
69
|
+
}
|
|
70
|
+
: { event: "session", data: JSON.stringify(baseline) });
|
|
71
|
+
return !ending;
|
|
72
|
+
};
|
|
73
|
+
const pump = async () => {
|
|
74
|
+
try {
|
|
75
|
+
let snapshot = options.watch.snapshot;
|
|
76
|
+
if (!(await publish(snapshot, false)))
|
|
77
|
+
return;
|
|
78
|
+
heartbeat = setTimeout(() => {
|
|
79
|
+
void beat().catch((cause) => {
|
|
80
|
+
options.onError("channel_visitor_stream_heartbeat_failed", cause);
|
|
81
|
+
void close().catch((closeCause) => {
|
|
82
|
+
options.onError("channel_visitor_stream_close_failed", closeCause);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}, 30_000);
|
|
86
|
+
for await (const frame of options.watch.changes) {
|
|
87
|
+
if (ending)
|
|
88
|
+
break;
|
|
89
|
+
if (frame.kind === "tool_output")
|
|
90
|
+
continue;
|
|
91
|
+
snapshot =
|
|
92
|
+
frame.kind === "progress"
|
|
93
|
+
? {
|
|
94
|
+
...snapshot,
|
|
95
|
+
progress: frame.progress,
|
|
96
|
+
activity: frame.activity,
|
|
97
|
+
}
|
|
98
|
+
: frame.kind === "activity"
|
|
99
|
+
? { ...snapshot, activity: frame.activity }
|
|
100
|
+
: frame.kind === "reset"
|
|
101
|
+
? frame.baseline
|
|
102
|
+
: await options.read();
|
|
103
|
+
if (!(await publish(snapshot, frame.kind === "progress")))
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await close();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
void pump().catch((cause) => {
|
|
112
|
+
options.onError("channel_visitor_stream_failed", cause);
|
|
113
|
+
});
|
|
114
|
+
return stream.responseReadable.pipeThrough(new TransformStream());
|
|
115
|
+
}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
|
+
/**
|
|
3
|
+
* The Agent Session that sent the work an item belongs to. The host derives it
|
|
4
|
+
* from the accepted mail; it is the only party the item waits on until it is
|
|
5
|
+
* escalated.
|
|
6
|
+
*/
|
|
7
|
+
export declare const AttentionRecipientSchema: Schema.Struct<{
|
|
8
|
+
readonly agentId: Schema.NonEmptyString;
|
|
9
|
+
readonly sessionId: Schema.NonEmptyString;
|
|
10
|
+
readonly name: Schema.NonEmptyString;
|
|
11
|
+
}>;
|
|
12
|
+
/** Decoded Agent Session an attention item waits on. */
|
|
13
|
+
export type AttentionRecipient = typeof AttentionRecipientSchema.Type;
|
|
2
14
|
/**
|
|
3
15
|
* An extension-owned request for human attention; expiry is owned by its timer.
|
|
4
16
|
* Items marked `inbox: false` remain visible without changing the inbox state.
|
|
17
|
+
* An item with `waitingOn` waits on that Agent first and is never a person's
|
|
18
|
+
* inbox item until it is escalated past every Agent.
|
|
5
19
|
*/
|
|
6
20
|
export declare const AttentionItemSchema: Schema.Struct<{
|
|
7
21
|
readonly id: Schema.NonEmptyString;
|
|
@@ -11,6 +25,12 @@ export declare const AttentionItemSchema: Schema.Struct<{
|
|
|
11
25
|
readonly expiresAt: Schema.optional<Schema.Int>;
|
|
12
26
|
/** This item does not change the Session's inbox state; omission does. */
|
|
13
27
|
readonly inbox: Schema.optional<Schema.Literal<false>>;
|
|
28
|
+
/** The Agent this item waits on; absent means a person. */
|
|
29
|
+
readonly waitingOn: Schema.optional<Schema.Struct<{
|
|
30
|
+
readonly agentId: Schema.NonEmptyString;
|
|
31
|
+
readonly sessionId: Schema.NonEmptyString;
|
|
32
|
+
readonly name: Schema.NonEmptyString;
|
|
33
|
+
}>>;
|
|
14
34
|
}>;
|
|
15
35
|
/** Input to the granted attention door, scoped to the calling extension. */
|
|
16
36
|
export type AttentionItem = typeof AttentionItemSchema.Type;
|
|
@@ -23,6 +43,11 @@ export declare const SessionAttentionSchema: Schema.Struct<{
|
|
|
23
43
|
readonly since: Schema.Int;
|
|
24
44
|
readonly expiresAt: Schema.optional<Schema.Int>;
|
|
25
45
|
readonly inbox: Schema.optional<Schema.Literal<false>>;
|
|
46
|
+
readonly waitingOn: Schema.optional<Schema.Struct<{
|
|
47
|
+
readonly agentId: Schema.NonEmptyString;
|
|
48
|
+
readonly sessionId: Schema.NonEmptyString;
|
|
49
|
+
readonly name: Schema.NonEmptyString;
|
|
50
|
+
}>>;
|
|
26
51
|
}>;
|
|
27
52
|
/** One open attention item in a Session observation. */
|
|
28
53
|
export type SessionAttention = typeof SessionAttentionSchema.Type;
|
|
@@ -39,4 +64,9 @@ export declare const SessionAttentionListSchema: Schema.$Array<Schema.Struct<{
|
|
|
39
64
|
readonly since: Schema.Int;
|
|
40
65
|
readonly expiresAt: Schema.optional<Schema.Int>;
|
|
41
66
|
readonly inbox: Schema.optional<Schema.Literal<false>>;
|
|
67
|
+
readonly waitingOn: Schema.optional<Schema.Struct<{
|
|
68
|
+
readonly agentId: Schema.NonEmptyString;
|
|
69
|
+
readonly sessionId: Schema.NonEmptyString;
|
|
70
|
+
readonly name: Schema.NonEmptyString;
|
|
71
|
+
}>>;
|
|
42
72
|
}>>;
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
2
|
const identity = Schema.NonEmptyString.check(Schema.isMaxLength(256));
|
|
3
3
|
const instant = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
4
|
+
/**
|
|
5
|
+
* The Agent Session that sent the work an item belongs to. The host derives it
|
|
6
|
+
* from the accepted mail; it is the only party the item waits on until it is
|
|
7
|
+
* escalated.
|
|
8
|
+
*/
|
|
9
|
+
export const AttentionRecipientSchema = Schema.Struct({
|
|
10
|
+
agentId: identity,
|
|
11
|
+
sessionId: identity,
|
|
12
|
+
name: identity,
|
|
13
|
+
});
|
|
4
14
|
/**
|
|
5
15
|
* An extension-owned request for human attention; expiry is owned by its timer.
|
|
6
16
|
* Items marked `inbox: false` remain visible without changing the inbox state.
|
|
17
|
+
* An item with `waitingOn` waits on that Agent first and is never a person's
|
|
18
|
+
* inbox item until it is escalated past every Agent.
|
|
7
19
|
*/
|
|
8
20
|
export const AttentionItemSchema = Schema.Struct({
|
|
9
21
|
id: identity,
|
|
@@ -13,6 +25,8 @@ export const AttentionItemSchema = Schema.Struct({
|
|
|
13
25
|
expiresAt: Schema.optional(instant),
|
|
14
26
|
/** This item does not change the Session's inbox state; omission does. */
|
|
15
27
|
inbox: Schema.optional(Schema.Literal(false)),
|
|
28
|
+
/** The Agent this item waits on; absent means a person. */
|
|
29
|
+
waitingOn: Schema.optional(AttentionRecipientSchema),
|
|
16
30
|
});
|
|
17
31
|
/** Public identity includes the owner so two extensions can use the same id. */
|
|
18
32
|
export const SessionAttentionSchema = AttentionItemSchema.mapFields((fields) => ({
|