@cursor/july 0.1.7 → 0.1.9
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/dist/bin/agent-serve.js +31 -2
- package/dist/channels/slack/channel-watch.d.ts +6 -1
- package/dist/channels/slack/channel-watch.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.js +22 -1
- package/dist/channels/slack/cursor-account.d.ts +4 -0
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +12 -2
- package/dist/channels/slack/defaults.d.ts +10 -5
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +39 -17
- package/dist/channels/slack/dispatch.d.ts +18 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +141 -38
- package/dist/channels/slack/inbound.d.ts +13 -3
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +17 -9
- package/dist/channels/slack/index.d.ts +4 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +4 -0
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +1 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +3 -0
- package/dist/channels/slack/nudge-carrier.d.ts +19 -0
- package/dist/channels/slack/nudge-carrier.d.ts.map +1 -0
- package/dist/channels/slack/nudge-carrier.js +166 -0
- package/dist/channels/slack/nudge-store.d.ts +61 -0
- package/dist/channels/slack/nudge-store.d.ts.map +1 -0
- package/dist/channels/slack/nudge-store.js +266 -0
- package/dist/channels/slack/post-update-delivery.js +318 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +74 -25
- package/dist/channels/slack/stop.d.ts +14 -0
- package/dist/channels/slack/stop.d.ts.map +1 -0
- package/dist/channels/slack/stop.js +22 -0
- package/dist/channels/slack/thread-context.d.ts +81 -0
- package/dist/channels/slack/thread-context.d.ts.map +1 -0
- package/dist/channels/slack/thread-context.js +470 -0
- package/dist/channels/slack/types.d.ts +27 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +4 -4
- package/dist/docs/assets/{app.COTN7wgo.js → app.Ciosf24M.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.D154Detj.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BW3TBdT0.js → VPLocalSearchBox.BcRDly7n.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BEJW0vE7.js → theme.CMpbjdfx.js} +2 -2
- package/dist/docs/assets/{deployment.md.BtfEsc9S.js → deployment.md.DC5-ImcM.js} +1 -1
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.js → example-agents_approval-buddy.md.DFGBYLcc.js} +2 -2
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.lean.js → example-agents_approval-buddy.md.DFGBYLcc.lean.js} +1 -1
- package/dist/docs/assets/example-agents_index.md.BPXjUtRB.js +2 -0
- package/dist/docs/assets/example-agents_index.md.BPXjUtRB.lean.js +1 -0
- package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.js +10 -0
- package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.lean.js +1 -0
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.js → example-agents_security-reviewer.md.CE9v-aKm.js} +2 -2
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.lean.js → example-agents_security-reviewer.md.CE9v-aKm.lean.js} +1 -1
- package/dist/docs/assets/{guides_slack.md.CCwqHvSV.js → guides_slack.md.FWale6Ip.js} +4 -3
- package/dist/docs/assets/{guides_slack.md.CCwqHvSV.lean.js → guides_slack.md.FWale6Ip.lean.js} +1 -1
- package/dist/docs/assets/{reference_sessions.md.DdjWabnN.js → reference_sessions.md.DLd6mvbv.js} +2 -2
- package/dist/docs/assets/{reference_sessions.md.DdjWabnN.lean.js → reference_sessions.md.DLd6mvbv.lean.js} +1 -1
- package/dist/docs/assets/{storage.md.CVnInNiN.js → storage.md.DRTdnFvd.js} +9 -2
- package/dist/docs/assets/{storage.md.CVnInNiN.lean.js → storage.md.DRTdnFvd.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +6 -6
- package/dist/docs/evals.html +4 -4
- package/dist/docs/example-agents/approval-buddy.html +5 -5
- package/dist/docs/example-agents/benny.html +5 -5
- package/dist/docs/example-agents/bugbot.html +5 -5
- package/dist/docs/example-agents/codebase-wiki.html +4 -4
- package/dist/docs/example-agents/codeowners-review.html +4 -4
- package/dist/docs/example-agents/concierge.html +4 -4
- package/dist/docs/example-agents/fsd.html +4 -4
- package/dist/docs/example-agents/index.html +6 -6
- package/dist/docs/example-agents/knowledge-base.html +4 -4
- package/dist/docs/example-agents/oncall.html +34 -0
- package/dist/docs/example-agents/security-reviewer.html +6 -6
- package/dist/docs/example-agents/slack-agent.html +4 -4
- package/dist/docs/example-agents/weather-agent.html +4 -4
- package/dist/docs/guides/agent-to-agent.html +4 -4
- package/dist/docs/guides/cloud-runtime.html +4 -4
- package/dist/docs/guides/github.html +4 -4
- package/dist/docs/guides/human-in-the-loop.html +4 -4
- package/dist/docs/guides/slack.html +7 -6
- package/dist/docs/guides/webhooks.html +4 -4
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +4 -4
- package/dist/docs/index.html +4 -4
- package/dist/docs/quickstart.html +4 -4
- package/dist/docs/reference/agent-config.html +4 -4
- package/dist/docs/reference/channels.html +4 -4
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +4 -4
- package/dist/docs/reference/hooks.html +4 -4
- package/dist/docs/reference/http-api.html +4 -4
- package/dist/docs/reference/instructions.html +4 -4
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +4 -4
- package/dist/docs/reference/sessions.html +5 -5
- package/dist/docs/reference/skills.html +4 -4
- package/dist/docs/reference/subagents.html +4 -4
- package/dist/docs/reference/tools.html +4 -4
- package/dist/docs/scaffolding-agents.html +4 -4
- package/dist/docs/storage.html +13 -6
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +45 -18
- package/dist/internal/cli-mcp.d.ts +64 -0
- package/dist/internal/cli-mcp.d.ts.map +1 -0
- package/dist/internal/cli-mcp.js +570 -0
- package/dist/internal/deploy-client.d.ts +7 -1
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +3 -0
- package/dist/internal/deploy-source.d.ts +3 -0
- package/dist/internal/deploy-source.d.ts.map +1 -1
- package/dist/internal/deploy-source.js +25 -7
- package/dist/internal/discovery.js +1 -1
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +3 -0
- package/dist/internal/host-kv.d.ts +17 -0
- package/dist/internal/host-kv.d.ts.map +1 -0
- package/dist/internal/host-kv.js +77 -0
- package/dist/internal/hosting.d.ts +12 -2
- package/dist/internal/hosting.d.ts.map +1 -1
- package/dist/internal/hosting.js +21 -4
- package/dist/internal/json-dir-store.js +100 -0
- package/dist/internal/logs-client.d.ts +16 -2
- package/dist/internal/logs-client.d.ts.map +1 -1
- package/dist/internal/logs-client.js +51 -16
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +8 -0
- package/dist/internal/session-engine.d.ts +42 -7
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +276 -66
- package/dist/internal/storage-coordinator.d.ts +18 -1
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +43 -0
- package/dist/internal/workspace.d.ts +19 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +47 -13
- package/dist/playground/assets/{index-72vCOBWO.js → index-C0-QEG4O.js} +42 -42
- package/dist/playground/assets/index-z3JY9O2x.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +65 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -0
- package/dist/storage-backends/cursor-hosted.js +203 -0
- package/dist/storage-backends/file-kv.d.ts +42 -0
- package/dist/storage-backends/file-kv.d.ts.map +1 -0
- package/dist/storage-backends/file-kv.js +190 -0
- package/dist/storage-backends/postgres-kv.d.ts +82 -0
- package/dist/storage-backends/postgres-kv.d.ts.map +1 -0
- package/dist/storage-backends/postgres-kv.js +185 -0
- package/dist/storage.d.ts +10 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +10 -1
- package/dist/types.d.ts +79 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/.vitepress/cache/deps/package.json +3 -0
- package/docs/deployment.md +7 -3
- package/docs/example-agents/approval-buddy.md +1 -1
- package/docs/example-agents/index.md +15 -10
- package/docs/example-agents/oncall.md +216 -0
- package/docs/example-agents/security-reviewer.md +5 -5
- package/docs/guides/slack.md +11 -2
- package/docs/reference/sessions.md +13 -4
- package/docs/storage.md +41 -0
- package/package.json +22 -1
- package/skills/framework-map/SKILL.md +6 -4
- package/src/bin/agent-serve.ts +32 -2
- package/src/channels/slack/channel-watch.ts +28 -1
- package/src/channels/slack/cursor-account.ts +20 -0
- package/src/channels/slack/defaults.ts +51 -20
- package/src/channels/slack/dispatch.ts +215 -40
- package/src/channels/slack/inbound.ts +30 -10
- package/src/channels/slack/index.ts +4 -0
- package/src/channels/slack/live-delivery.ts +1 -0
- package/src/channels/slack/manifest.ts +3 -0
- package/src/channels/slack/nudge-carrier.ts +182 -0
- package/src/channels/slack/nudge-store.ts +308 -0
- package/src/channels/slack/slack-channel.ts +106 -31
- package/src/channels/slack/stop.ts +25 -0
- package/src/channels/slack/thread-context.ts +572 -0
- package/src/channels/slack/types.ts +27 -0
- package/src/internal/cli-deploy.ts +58 -20
- package/src/internal/cli-mcp.ts +708 -0
- package/src/internal/deploy-client.ts +11 -0
- package/src/internal/deploy-source.ts +24 -6
- package/src/internal/discovery.ts +1 -1
- package/src/internal/distribution.ts +3 -0
- package/src/internal/host-kv.ts +85 -0
- package/src/internal/hosting.ts +24 -4
- package/src/internal/logs-client.ts +67 -21
- package/src/internal/server.ts +31 -0
- package/src/internal/session-engine.ts +379 -61
- package/src/internal/storage-coordinator.ts +41 -0
- package/src/internal/workspace.ts +48 -11
- package/src/storage-backends/cursor-hosted.ts +246 -0
- package/src/storage-backends/file-kv.ts +221 -0
- package/src/storage-backends/postgres-kv.ts +253 -0
- package/src/storage.ts +11 -1
- package/src/types.ts +83 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +0 -1
- package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +0 -2
- package/dist/docs/assets/example-agents_index.md.CRqJlnIf.lean.js +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/index-BjnMwYoR.css +0 -1
- /package/dist/docs/assets/{deployment.md.BtfEsc9S.lean.js → deployment.md.DC5-ImcM.lean.js} +0 -0
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
resolveABSplit,
|
|
18
18
|
} from "../ab.js";
|
|
19
19
|
import { slackThreadPermalink } from "../channels/slack/links.js";
|
|
20
|
+
import {
|
|
21
|
+
SlackAskDedupeStore,
|
|
22
|
+
SlackNudgeStore,
|
|
23
|
+
} from "../channels/slack/nudge-store.js";
|
|
20
24
|
import {
|
|
21
25
|
type AgentProject,
|
|
22
26
|
type ApprovalDecision,
|
|
@@ -43,6 +47,7 @@ import {
|
|
|
43
47
|
type SessionInfo,
|
|
44
48
|
type SessionRecord,
|
|
45
49
|
type SessionSummary,
|
|
50
|
+
type SlackNudgeHostApi,
|
|
46
51
|
type ToolCallOutcome,
|
|
47
52
|
type ToolContext,
|
|
48
53
|
type ToolNeedsApproval,
|
|
@@ -57,6 +62,7 @@ import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
|
|
|
57
62
|
import { isDevMode } from "./dev-mode.js";
|
|
58
63
|
import { EventLogStore } from "./event-log.js";
|
|
59
64
|
import { TurnEventMapper } from "./event-mapper.js";
|
|
65
|
+
import { createHostKvApi } from "./host-kv.js";
|
|
60
66
|
import { createHostPlatforms, type HostPlatforms } from "./host-platforms.js";
|
|
61
67
|
import {
|
|
62
68
|
formatContinuationToken,
|
|
@@ -67,7 +73,6 @@ import {
|
|
|
67
73
|
import { validateToolInput } from "./json-schema.js";
|
|
68
74
|
import { McpHost } from "./mcp-host.js";
|
|
69
75
|
import {
|
|
70
|
-
buildIdentitySection,
|
|
71
76
|
buildSessionContextBlock,
|
|
72
77
|
SCHEDULE_CHANNEL_PREFIX,
|
|
73
78
|
} from "./prompt-context.js";
|
|
@@ -77,7 +82,8 @@ import { SessionStore } from "./session-store.js";
|
|
|
77
82
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
78
83
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
79
84
|
import {
|
|
80
|
-
|
|
85
|
+
buildAgentsMdContent,
|
|
86
|
+
isNestedInGitRepo,
|
|
81
87
|
materializeWorkspace,
|
|
82
88
|
writeWorkspaceFiles,
|
|
83
89
|
} from "./workspace.js";
|
|
@@ -196,10 +202,29 @@ export interface EngineSendOptions extends SendMessageOptions {
|
|
|
196
202
|
mode?: "chat" | "task";
|
|
197
203
|
}
|
|
198
204
|
|
|
205
|
+
interface CoalescedQueuedTurn {
|
|
206
|
+
message: string;
|
|
207
|
+
sourceTs?: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface AdmitTurnOptions {
|
|
211
|
+
admission?: "preempt" | "coalesce";
|
|
212
|
+
sourceTs?: string;
|
|
213
|
+
/**
|
|
214
|
+
* When true, skip `clearCoalesced` on the preempt admit path. Used by
|
|
215
|
+
* coalesce drain so asks that arrived during drain are not wiped.
|
|
216
|
+
*/
|
|
217
|
+
retainCoalesced?: boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
199
220
|
export class SessionEngine {
|
|
200
221
|
readonly project: AgentProject;
|
|
201
222
|
readonly stateRoot: string;
|
|
202
223
|
readonly sessions: SessionStore;
|
|
224
|
+
/** Durable Slack mid-turn nudges under `<stateRoot>/slack-nudges`. */
|
|
225
|
+
readonly nudgeStore: SlackNudgeStore;
|
|
226
|
+
/** Durable `channelId:ts` ask claims under `<stateRoot>/slack-asks`. */
|
|
227
|
+
readonly askDedupe: SlackAskDedupeStore;
|
|
203
228
|
/**
|
|
204
229
|
* Runtime for `agent/storage.ts` (`defineStorage`), when
|
|
205
230
|
* authored. All durable state changes funnel through it (session
|
|
@@ -226,6 +251,16 @@ export class SessionEngine {
|
|
|
226
251
|
private readonly turnAborts = new Map<string, AbortController>();
|
|
227
252
|
/** Serializes turn admission per session (latest follow-up wins). */
|
|
228
253
|
private readonly admissionChains = new Map<string, Promise<unknown>>();
|
|
254
|
+
/**
|
|
255
|
+
* Per-session coalesce queue for `admission: "coalesce"` (Slack). Drained
|
|
256
|
+
* into one follow-up turn when the active turn finishes without interrupt.
|
|
257
|
+
*/
|
|
258
|
+
private readonly coalesceQueues = new Map<string, CoalescedQueuedTurn[]>();
|
|
259
|
+
/**
|
|
260
|
+
* Sessions whose turn lifecycle is draining coalesced follow-ups. Counts as
|
|
261
|
+
* busy so inbound coalesce admits enqueue instead of racing `clearCoalesced`.
|
|
262
|
+
*/
|
|
263
|
+
private readonly coalesceDrains = new Map<string, Promise<void>>();
|
|
229
264
|
private readonly lastOutcomes = new Map<string, TurnOutcome>();
|
|
230
265
|
private readonly updateChains = new Map<string, Promise<unknown>>();
|
|
231
266
|
private readonly dispatchChains = new Map<string, Promise<void>>();
|
|
@@ -235,6 +270,7 @@ export class SessionEngine {
|
|
|
235
270
|
private readonly approvals = new ApprovalGate();
|
|
236
271
|
private readonly abCollector: ABCollector;
|
|
237
272
|
private reminderApi: HostContext["reminders"];
|
|
273
|
+
private readonly kvApi: HostContext["kv"];
|
|
238
274
|
/** Serve-time resolved connections (see {@link setResolvedConnections}). */
|
|
239
275
|
private resolvedConnections: EngineResolvedConnection[] = [];
|
|
240
276
|
|
|
@@ -244,6 +280,12 @@ export class SessionEngine {
|
|
|
244
280
|
this.runner = options.runner;
|
|
245
281
|
this.logger = options.logger ?? (() => {});
|
|
246
282
|
this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
|
|
283
|
+
this.nudgeStore = new SlackNudgeStore(
|
|
284
|
+
join(options.stateRoot, "slack-nudges")
|
|
285
|
+
);
|
|
286
|
+
this.askDedupe = new SlackAskDedupeStore(
|
|
287
|
+
join(options.stateRoot, "slack-asks")
|
|
288
|
+
);
|
|
247
289
|
this.logs = new EventLogStore(
|
|
248
290
|
(sessionId) => this.sessions.eventFilePath(sessionId),
|
|
249
291
|
(sessionId, error) =>
|
|
@@ -260,6 +302,11 @@ export class SessionEngine {
|
|
|
260
302
|
projectRoot: options.project.rootDir,
|
|
261
303
|
logger: this.logger,
|
|
262
304
|
});
|
|
305
|
+
this.kvApi = createHostKvApi({
|
|
306
|
+
agentName: options.project.name,
|
|
307
|
+
stateRoot: options.stateRoot,
|
|
308
|
+
...(this.storage === undefined ? {} : { storage: this.storage }),
|
|
309
|
+
});
|
|
263
310
|
this.abCollector = new ABCollector(
|
|
264
311
|
options.project.abs,
|
|
265
312
|
this.logger,
|
|
@@ -302,13 +349,32 @@ export class SessionEngine {
|
|
|
302
349
|
}
|
|
303
350
|
}
|
|
304
351
|
|
|
305
|
-
/** Shared host services (MCP / GitHub / Slack / reminders). */
|
|
352
|
+
/** Shared host services (MCP / GitHub / Slack / reminders / kv / Slack nudges). */
|
|
306
353
|
get host(): HostContext {
|
|
307
354
|
return {
|
|
308
355
|
mcp: this.mcpHost,
|
|
309
356
|
github: this.platforms.github,
|
|
310
357
|
slack: this.platforms.slack,
|
|
311
358
|
reminders: this.reminderApi,
|
|
359
|
+
kv: this.kvApi,
|
|
360
|
+
slackNudges: this.slackNudgeHostApi(),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private slackNudgeHostApi(): SlackNudgeHostApi {
|
|
365
|
+
return {
|
|
366
|
+
append: (input) => this.nudgeStore.append(input),
|
|
367
|
+
drainFormatted: (continuationKey) =>
|
|
368
|
+
this.nudgeStore.drainFormatted(continuationKey),
|
|
369
|
+
clear: (continuationKey) => this.nudgeStore.clear(continuationKey),
|
|
370
|
+
hasUndelivered: (continuationKey) =>
|
|
371
|
+
this.nudgeStore.hasUndelivered(continuationKey),
|
|
372
|
+
isDelivered: (continuationKey, sourceTs) =>
|
|
373
|
+
this.nudgeStore.isDelivered(continuationKey, sourceTs),
|
|
374
|
+
tryClaimAsk: (channelId, messageTs) =>
|
|
375
|
+
this.askDedupe.tryClaim(channelId, messageTs),
|
|
376
|
+
releaseAsk: (channelId, messageTs) =>
|
|
377
|
+
this.askDedupe.release(channelId, messageTs),
|
|
312
378
|
};
|
|
313
379
|
}
|
|
314
380
|
|
|
@@ -371,8 +437,13 @@ export class SessionEngine {
|
|
|
371
437
|
pendingWorkspaceFiles: options.workspaceFiles,
|
|
372
438
|
}));
|
|
373
439
|
}
|
|
374
|
-
await this.startTurn(record.sessionId, message
|
|
375
|
-
|
|
440
|
+
const coalesced = await this.startTurn(record.sessionId, message, {
|
|
441
|
+
admission: options.admission ?? "preempt",
|
|
442
|
+
...(options.coalesceSourceTs === undefined
|
|
443
|
+
? {}
|
|
444
|
+
: { sourceTs: options.coalesceSourceTs }),
|
|
445
|
+
});
|
|
446
|
+
return this.channelSession(record.sessionId, record, false, coalesced);
|
|
376
447
|
}
|
|
377
448
|
|
|
378
449
|
const now = new Date().toISOString();
|
|
@@ -402,8 +473,31 @@ export class SessionEngine {
|
|
|
402
473
|
data: { channelId },
|
|
403
474
|
});
|
|
404
475
|
record = await this.enrollABs(record);
|
|
405
|
-
await this.startTurn(record.sessionId, message
|
|
406
|
-
|
|
476
|
+
await this.startTurn(record.sessionId, message, {
|
|
477
|
+
admission: options.admission ?? "preempt",
|
|
478
|
+
...(options.coalesceSourceTs === undefined
|
|
479
|
+
? {}
|
|
480
|
+
: { sourceTs: options.coalesceSourceTs }),
|
|
481
|
+
});
|
|
482
|
+
return this.channelSession(record.sessionId, record, true, false);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Whether a continuation token already maps to a session on this channel. */
|
|
486
|
+
async hasContinuationSession(
|
|
487
|
+
channelId: string,
|
|
488
|
+
continuationToken: string
|
|
489
|
+
): Promise<boolean> {
|
|
490
|
+
let existing = await this.sessions.findByContinuation(
|
|
491
|
+
channelId,
|
|
492
|
+
continuationToken
|
|
493
|
+
);
|
|
494
|
+
if (existing === undefined) {
|
|
495
|
+
existing = await this.restoreSessionByContinuation(
|
|
496
|
+
channelId,
|
|
497
|
+
continuationToken
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
return existing !== undefined;
|
|
407
501
|
}
|
|
408
502
|
|
|
409
503
|
/**
|
|
@@ -559,17 +653,25 @@ export class SessionEngine {
|
|
|
559
653
|
// ==========================================================================
|
|
560
654
|
|
|
561
655
|
/**
|
|
562
|
-
* Admit a turn for `sessionId`.
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
656
|
+
* Admit a turn for `sessionId`.
|
|
657
|
+
*
|
|
658
|
+
* Default (`preempt`): a follow-up cancels the running turn and starts
|
|
659
|
+
* immediately (HTTP / MCP). With `admission: "coalesce"` (Slack), a busy
|
|
660
|
+
* session enqueues the message and returns without interrupting; the queue
|
|
661
|
+
* drains into one follow-up when the active turn finishes.
|
|
662
|
+
*
|
|
663
|
+
* Resolves to `true` when the message was coalesced (enqueued), `false`
|
|
664
|
+
* when a turn was started.
|
|
567
665
|
*/
|
|
568
|
-
private startTurn(
|
|
666
|
+
private startTurn(
|
|
667
|
+
sessionId: string,
|
|
668
|
+
message: string,
|
|
669
|
+
options: AdmitTurnOptions = {}
|
|
670
|
+
): Promise<boolean> {
|
|
569
671
|
const prior = this.admissionChains.get(sessionId) ?? Promise.resolve();
|
|
570
672
|
const admitted = prior
|
|
571
673
|
.catch(() => {})
|
|
572
|
-
.then(() => this.admitTurn(sessionId, message));
|
|
674
|
+
.then(() => this.admitTurn(sessionId, message, options));
|
|
573
675
|
this.admissionChains.set(
|
|
574
676
|
sessionId,
|
|
575
677
|
admitted.catch(() => {})
|
|
@@ -577,11 +679,88 @@ export class SessionEngine {
|
|
|
577
679
|
return admitted;
|
|
578
680
|
}
|
|
579
681
|
|
|
580
|
-
private
|
|
682
|
+
private enqueueCoalesced(sessionId: string, item: CoalescedQueuedTurn): void {
|
|
683
|
+
const queue = this.coalesceQueues.get(sessionId) ?? [];
|
|
684
|
+
if (
|
|
685
|
+
item.sourceTs !== undefined &&
|
|
686
|
+
queue.some((entry) => entry.sourceTs === item.sourceTs)
|
|
687
|
+
) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
queue.push(item);
|
|
691
|
+
this.coalesceQueues.set(sessionId, queue);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private clearCoalesced(sessionId: string): void {
|
|
695
|
+
this.coalesceQueues.delete(sessionId);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private formatCoalescedFollowups(
|
|
699
|
+
items: readonly CoalescedQueuedTurn[]
|
|
700
|
+
): string {
|
|
701
|
+
if (items.length === 1) {
|
|
702
|
+
return items[0]!.message;
|
|
703
|
+
}
|
|
704
|
+
const parts = [
|
|
705
|
+
"<slack_followups>",
|
|
706
|
+
`ANSWER-EACH: ${items.length} asks arrived while you were busy. Address each in order; keep replies compact.`,
|
|
707
|
+
"",
|
|
708
|
+
];
|
|
709
|
+
for (let i = 0; i < items.length; i++) {
|
|
710
|
+
parts.push(`### Ask ${i + 1}`);
|
|
711
|
+
parts.push(items[i]!.message);
|
|
712
|
+
parts.push("");
|
|
713
|
+
}
|
|
714
|
+
parts.push("</slack_followups>");
|
|
715
|
+
return parts.join("\n").trimEnd();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private async drainCoalescedFollowups(sessionId: string): Promise<void> {
|
|
719
|
+
const queued = this.coalesceQueues.get(sessionId) ?? [];
|
|
720
|
+
if (queued.length === 0) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
this.coalesceQueues.delete(sessionId);
|
|
724
|
+
const message = this.formatCoalescedFollowups(queued);
|
|
725
|
+
this.logger(
|
|
726
|
+
`[session] coalesce drain starting follow-up (${queued.length}) ${sessionId}`
|
|
727
|
+
);
|
|
728
|
+
await this.startTurn(sessionId, message, {
|
|
729
|
+
admission: "preempt",
|
|
730
|
+
retainCoalesced: true,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private async admitTurn(
|
|
735
|
+
sessionId: string,
|
|
736
|
+
message: string,
|
|
737
|
+
options: AdmitTurnOptions = {}
|
|
738
|
+
): Promise<boolean> {
|
|
739
|
+
const admission = options.admission ?? "preempt";
|
|
740
|
+
if (admission === "coalesce" && this.isTurnRunning(sessionId)) {
|
|
741
|
+
this.enqueueCoalesced(sessionId, {
|
|
742
|
+
message,
|
|
743
|
+
...(options.sourceTs === undefined
|
|
744
|
+
? {}
|
|
745
|
+
: { sourceTs: options.sourceTs }),
|
|
746
|
+
});
|
|
747
|
+
this.logger(`[session] coalesce enqueue ${sessionId}`);
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
|
|
581
751
|
// Interrupt any turn in flight and wait for it to settle so the SDK run is
|
|
582
752
|
// terminal before this turn's send (the follow-up also force-expires as a
|
|
583
|
-
// belt-and-suspenders — see buildTurnRequest).
|
|
584
|
-
|
|
753
|
+
// belt-and-suspenders — see buildTurnRequest). Drain-resume skips interrupt:
|
|
754
|
+
// the parent lifecycle already cleared activeTurns, and awaiting the in-
|
|
755
|
+
// flight coalesce drain here would deadlock on itself.
|
|
756
|
+
if (options.retainCoalesced !== true) {
|
|
757
|
+
await this.interruptActiveTurn(sessionId);
|
|
758
|
+
}
|
|
759
|
+
// Preempt owns the conversation — drop stale coalesced asks. Coalesce
|
|
760
|
+
// admits and drain-resume keep the queue (retainCoalesced / coalesce path).
|
|
761
|
+
if (admission === "preempt" && options.retainCoalesced !== true) {
|
|
762
|
+
this.clearCoalesced(sessionId);
|
|
763
|
+
}
|
|
585
764
|
this.pendingTurns.add(sessionId);
|
|
586
765
|
|
|
587
766
|
try {
|
|
@@ -607,31 +786,58 @@ export class SessionEngine {
|
|
|
607
786
|
|
|
608
787
|
const abort = new AbortController();
|
|
609
788
|
this.turnAborts.set(sessionId, abort);
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
789
|
+
// Lifecycle promise includes post-turn coalesce drain so waiters and
|
|
790
|
+
// interrupt settle after follow-ups are admitted (not only executeTurn).
|
|
791
|
+
// Assigned before the async body runs so self-identity checks typecheck.
|
|
792
|
+
let lifecycle!: Promise<TurnOutcome>;
|
|
793
|
+
lifecycle = (async (): Promise<TurnOutcome> => {
|
|
794
|
+
let outcome: TurnOutcome;
|
|
795
|
+
try {
|
|
796
|
+
outcome = await this.executeTurn(
|
|
797
|
+
record,
|
|
798
|
+
turnId,
|
|
799
|
+
message,
|
|
800
|
+
isFirstTurn,
|
|
801
|
+
abort.signal
|
|
802
|
+
);
|
|
803
|
+
} catch (error) {
|
|
804
|
+
outcome = {
|
|
805
|
+
status: "error",
|
|
806
|
+
errorMessage: describeError(error),
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
this.lastOutcomes.set(sessionId, outcome);
|
|
810
|
+
const wasCurrent = this.activeTurns.get(sessionId) === lifecycle;
|
|
811
|
+
if (wasCurrent) {
|
|
812
|
+
this.activeTurns.delete(sessionId);
|
|
813
|
+
}
|
|
814
|
+
if (this.turnAborts.get(sessionId) === abort) {
|
|
815
|
+
this.turnAborts.delete(sessionId);
|
|
816
|
+
}
|
|
817
|
+
if (wasCurrent && !abort.signal.aborted) {
|
|
818
|
+
const drain = this.drainCoalescedFollowups(sessionId).catch(
|
|
819
|
+
(error) => {
|
|
820
|
+
this.logger(
|
|
821
|
+
`[session] coalesce drain failed for ${sessionId}: ${describeError(error)}`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
this.coalesceDrains.set(sessionId, drain);
|
|
826
|
+
try {
|
|
827
|
+
await drain;
|
|
828
|
+
} finally {
|
|
829
|
+
if (this.coalesceDrains.get(sessionId) === drain) {
|
|
830
|
+
this.coalesceDrains.delete(sessionId);
|
|
831
|
+
}
|
|
630
832
|
}
|
|
631
|
-
}
|
|
833
|
+
}
|
|
834
|
+
return outcome;
|
|
835
|
+
})();
|
|
836
|
+
this.activeTurns.set(sessionId, lifecycle);
|
|
632
837
|
} finally {
|
|
633
838
|
this.pendingTurns.delete(sessionId);
|
|
634
839
|
}
|
|
840
|
+
return false;
|
|
635
841
|
}
|
|
636
842
|
|
|
637
843
|
/**
|
|
@@ -645,19 +851,102 @@ export class SessionEngine {
|
|
|
645
851
|
if (active !== undefined) {
|
|
646
852
|
await active.catch(() => {});
|
|
647
853
|
}
|
|
854
|
+
const drain = this.coalesceDrains.get(sessionId);
|
|
855
|
+
if (drain !== undefined) {
|
|
856
|
+
await drain.catch(() => {});
|
|
857
|
+
}
|
|
648
858
|
}
|
|
649
859
|
|
|
650
860
|
/**
|
|
651
861
|
* Abort the in-flight turn without starting a follow-up. Returns whether
|
|
652
|
-
* a turn was active (or pending abort) when called.
|
|
862
|
+
* a turn was active (or pending abort) when called. Also clears any
|
|
863
|
+
* coalesced follow-ups queued behind the session.
|
|
653
864
|
*/
|
|
654
865
|
async interrupt(sessionId: string): Promise<boolean> {
|
|
655
866
|
const hadWork =
|
|
656
|
-
this.turnAborts.has(sessionId) ||
|
|
867
|
+
this.turnAborts.has(sessionId) ||
|
|
868
|
+
this.activeTurns.has(sessionId) ||
|
|
869
|
+
this.coalesceDrains.has(sessionId) ||
|
|
870
|
+
(this.coalesceQueues.get(sessionId)?.length ?? 0) > 0;
|
|
871
|
+
this.clearCoalesced(sessionId);
|
|
872
|
+
const record = await this.sessions.get(sessionId);
|
|
873
|
+
const continuationKey = record?.continuationKey;
|
|
874
|
+
if (continuationKey !== undefined && continuationKey !== null) {
|
|
875
|
+
await this.nudgeStore.clear(continuationKey);
|
|
876
|
+
}
|
|
657
877
|
await this.interruptActiveTurn(sessionId);
|
|
658
878
|
return hadWork;
|
|
659
879
|
}
|
|
660
880
|
|
|
881
|
+
async isContinuationBusy(
|
|
882
|
+
channelId: string,
|
|
883
|
+
continuationToken: string
|
|
884
|
+
): Promise<boolean> {
|
|
885
|
+
let existing = await this.sessions.findByContinuation(
|
|
886
|
+
channelId,
|
|
887
|
+
continuationToken
|
|
888
|
+
);
|
|
889
|
+
if (existing === undefined) {
|
|
890
|
+
existing = await this.restoreSessionByContinuation(
|
|
891
|
+
channelId,
|
|
892
|
+
continuationToken
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
if (existing === undefined) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
return this.isTurnRunning(existing.sessionId);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Last agent-posted Slack message ts for warm `<slack_thread_delta>`, from
|
|
903
|
+
* the session's channel state when present.
|
|
904
|
+
*/
|
|
905
|
+
async getContinuationLastBotMessageTs(
|
|
906
|
+
channelId: string,
|
|
907
|
+
continuationToken: string
|
|
908
|
+
): Promise<string | undefined> {
|
|
909
|
+
let existing = await this.sessions.findByContinuation(
|
|
910
|
+
channelId,
|
|
911
|
+
continuationToken
|
|
912
|
+
);
|
|
913
|
+
if (existing === undefined) {
|
|
914
|
+
existing = await this.restoreSessionByContinuation(
|
|
915
|
+
channelId,
|
|
916
|
+
continuationToken
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
if (existing === undefined) {
|
|
920
|
+
return undefined;
|
|
921
|
+
}
|
|
922
|
+
const state = existing.channelState;
|
|
923
|
+
if (state === null || state === undefined || typeof state !== "object") {
|
|
924
|
+
return undefined;
|
|
925
|
+
}
|
|
926
|
+
const ts = (state as { lastBotMessageTs?: unknown }).lastBotMessageTs;
|
|
927
|
+
return typeof ts === "string" && ts.trim() !== "" ? ts.trim() : undefined;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async interruptContinuation(
|
|
931
|
+
channelId: string,
|
|
932
|
+
continuationToken: string
|
|
933
|
+
): Promise<boolean> {
|
|
934
|
+
let existing = await this.sessions.findByContinuation(
|
|
935
|
+
channelId,
|
|
936
|
+
continuationToken
|
|
937
|
+
);
|
|
938
|
+
if (existing === undefined) {
|
|
939
|
+
existing = await this.restoreSessionByContinuation(
|
|
940
|
+
channelId,
|
|
941
|
+
continuationToken
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
if (existing === undefined) {
|
|
945
|
+
return false;
|
|
946
|
+
}
|
|
947
|
+
return this.interrupt(existing.sessionId);
|
|
948
|
+
}
|
|
949
|
+
|
|
661
950
|
/**
|
|
662
951
|
* Local harness cwd for a session: per-send override, else
|
|
663
952
|
* `<agent.local.cwd>/<sessionId>`, else the durable session workspace.
|
|
@@ -730,12 +1019,22 @@ export class SessionEngine {
|
|
|
730
1019
|
return rest;
|
|
731
1020
|
});
|
|
732
1021
|
}
|
|
1022
|
+
// The harness only loads the scaffolded AGENTS.md when the session
|
|
1023
|
+
// workspace is its own project root. With a per-session cwd override
|
|
1024
|
+
// (PR worktree) the scaffold lands elsewhere, and a workspace nested
|
|
1025
|
+
// inside a git repo re-roots harness rules at that repo — in both
|
|
1026
|
+
// cases deliver instructions inline on the first turn instead.
|
|
1027
|
+
const instructionsInvisibleToHarness =
|
|
1028
|
+
isFirstTurn &&
|
|
1029
|
+
(record.workspaceOverride !== undefined ||
|
|
1030
|
+
(await isNestedInGitRepo(harnessCwd)));
|
|
733
1031
|
const request = this.buildTurnRequest(
|
|
734
1032
|
record,
|
|
735
1033
|
agent,
|
|
736
1034
|
turnId,
|
|
737
1035
|
message,
|
|
738
1036
|
isFirstTurn,
|
|
1037
|
+
instructionsInvisibleToHarness,
|
|
739
1038
|
emit,
|
|
740
1039
|
async (update) => {
|
|
741
1040
|
mapper.handleUpdate(update);
|
|
@@ -831,6 +1130,7 @@ export class SessionEngine {
|
|
|
831
1130
|
turnId: string,
|
|
832
1131
|
prompt: string,
|
|
833
1132
|
isFirstTurn: boolean,
|
|
1133
|
+
instructionsInvisibleToHarness: boolean,
|
|
834
1134
|
emit: (payload: SessionEventPayload) => void,
|
|
835
1135
|
onUpdate: RunnerTurnRequest["onUpdate"],
|
|
836
1136
|
signal: AbortSignal
|
|
@@ -858,15 +1158,21 @@ export class SessionEngine {
|
|
|
858
1158
|
});
|
|
859
1159
|
turnPrompt = `${contextBlock}\n\n${prompt}`;
|
|
860
1160
|
}
|
|
861
|
-
if (
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
.
|
|
869
|
-
|
|
1161
|
+
if (isFirstTurn) {
|
|
1162
|
+
if (runtime === "cloud") {
|
|
1163
|
+
// Cloud VMs have no materialized workspace: ship the full context,
|
|
1164
|
+
// including agent tool script bodies.
|
|
1165
|
+
const preamble = buildAgentsMdContent(agent, { includeScripts: true });
|
|
1166
|
+
turnPrompt = `${preamble}\n\n${turnPrompt}`;
|
|
1167
|
+
} else if (instructionsInvisibleToHarness) {
|
|
1168
|
+
// Local workspace whose AGENTS.md the harness will not load. With a
|
|
1169
|
+
// per-session harness cwd override, agent tool scripts live only in
|
|
1170
|
+
// the durable scaffold dir — embed bodies like cloud. Otherwise the
|
|
1171
|
+
// catalog can reference on-disk paths under the harness cwd.
|
|
1172
|
+
const includeScripts = record.workspaceOverride !== undefined;
|
|
1173
|
+
const preamble = buildAgentsMdContent(agent, { includeScripts });
|
|
1174
|
+
turnPrompt = `${preamble}\n\n${turnPrompt}`;
|
|
1175
|
+
}
|
|
870
1176
|
}
|
|
871
1177
|
return {
|
|
872
1178
|
sessionId: record.sessionId,
|
|
@@ -1498,15 +1804,20 @@ export class SessionEngine {
|
|
|
1498
1804
|
async waitForCompletion(sessionId: string): Promise<TurnOutcome> {
|
|
1499
1805
|
// Drain admission first so pendingTurns → activeTurns is visible before we
|
|
1500
1806
|
// decide the session is idle (reminders use this for queueBehindTurn).
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1807
|
+
// The active lifecycle promise also awaits coalesce drain + the drained
|
|
1808
|
+
// follow-up's admission, so Slack coalesced waiters see the resume turn.
|
|
1809
|
+
for (;;) {
|
|
1810
|
+
const chain = this.admissionChains.get(sessionId);
|
|
1811
|
+
if (chain !== undefined) {
|
|
1812
|
+
await chain.catch(() => {});
|
|
1813
|
+
}
|
|
1814
|
+
const active = this.activeTurns.get(sessionId);
|
|
1815
|
+
if (active !== undefined) {
|
|
1816
|
+
await active;
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
return this.lastOutcomes.get(sessionId) ?? { status: "finished" };
|
|
1508
1820
|
}
|
|
1509
|
-
return this.lastOutcomes.get(sessionId) ?? { status: "finished" };
|
|
1510
1821
|
}
|
|
1511
1822
|
|
|
1512
1823
|
/**
|
|
@@ -1674,9 +1985,14 @@ export class SessionEngine {
|
|
|
1674
1985
|
}
|
|
1675
1986
|
|
|
1676
1987
|
isTurnRunning(sessionId: string): boolean {
|
|
1677
|
-
// Include the admit window (pendingTurns)
|
|
1678
|
-
// turn that has
|
|
1679
|
-
|
|
1988
|
+
// Include the admit window (pendingTurns) and coalesce drain so busy
|
|
1989
|
+
// policies do not treat a turn that has finished executeTurn but not yet
|
|
1990
|
+
// resumed queued follow-ups as idle (avoids clearCoalesced races).
|
|
1991
|
+
return (
|
|
1992
|
+
this.activeTurns.has(sessionId) ||
|
|
1993
|
+
this.pendingTurns.has(sessionId) ||
|
|
1994
|
+
this.coalesceDrains.has(sessionId)
|
|
1995
|
+
);
|
|
1680
1996
|
}
|
|
1681
1997
|
|
|
1682
1998
|
sessionInfo(record: SessionRecord): SessionInfo {
|
|
@@ -1698,12 +2014,14 @@ export class SessionEngine {
|
|
|
1698
2014
|
private channelSession(
|
|
1699
2015
|
sessionId: string,
|
|
1700
2016
|
record: SessionRecord,
|
|
1701
|
-
isNew: boolean
|
|
2017
|
+
isNew: boolean,
|
|
2018
|
+
coalesced = false
|
|
1702
2019
|
): ChannelSession {
|
|
1703
2020
|
return {
|
|
1704
2021
|
id: sessionId,
|
|
1705
2022
|
continuationToken: record.continuationKey,
|
|
1706
2023
|
isNew,
|
|
2024
|
+
...(coalesced ? { coalesced: true } : {}),
|
|
1707
2025
|
info: this.sessionInfo(record),
|
|
1708
2026
|
getEventStream: (options?: { startIndex?: number }) =>
|
|
1709
2027
|
this.eventStream(sessionId, options?.startIndex ?? 0),
|
|
@@ -392,6 +392,47 @@ export class StorageCoordinator {
|
|
|
392
392
|
);
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
// ==========================================================================
|
|
396
|
+
// Author KV ({@link HostContext.kv})
|
|
397
|
+
// ==========================================================================
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Whether the sink can serve awaited author reads/writes (`get` is
|
|
401
|
+
* required — `put` is always present on {@link StorageConfig}).
|
|
402
|
+
*/
|
|
403
|
+
get supportsAuthorKv(): boolean {
|
|
404
|
+
return this.definition.get !== undefined;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Awaited author KV read. Unlike framework session mirrors, this awaits
|
|
409
|
+
* the sink and propagates errors — handlers need read-after-write.
|
|
410
|
+
*/
|
|
411
|
+
async kvGet(authorKey: string): Promise<JsonValue | undefined> {
|
|
412
|
+
const get = this.definition.get;
|
|
413
|
+
if (get === undefined) {
|
|
414
|
+
throw new Error("storage get is not configured");
|
|
415
|
+
}
|
|
416
|
+
const key = storageKeys.kv(this.agentName, authorKey);
|
|
417
|
+
// Preserve stored JSON `null` (distinct from a missing key).
|
|
418
|
+
return await get(key, this.context("restore"));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Awaited author KV write. Goes straight to the sink (not the
|
|
423
|
+
* at-most-once delivery queue) so callers observe durable success/failure.
|
|
424
|
+
*/
|
|
425
|
+
async kvPut(authorKey: string, value: JsonValue): Promise<void> {
|
|
426
|
+
const key = storageKeys.kv(this.agentName, authorKey);
|
|
427
|
+
await this.definition.put(key, value, this.context("policy"));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Awaited author KV delete. No-op when the sink omits `delete`. */
|
|
431
|
+
async kvDelete(authorKey: string): Promise<void> {
|
|
432
|
+
const key = storageKeys.kv(this.agentName, authorKey);
|
|
433
|
+
await this.definition.delete?.(key, this.context("policy"));
|
|
434
|
+
}
|
|
435
|
+
|
|
395
436
|
// ==========================================================================
|
|
396
437
|
// Evals (dedicated `evals` table)
|
|
397
438
|
// ==========================================================================
|