@cursor/july 0.1.1 → 0.1.2
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/AGENTS.md +24 -2
- package/README.md +25 -15
- package/dist/bin/agent-serve.js +101 -12
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +31 -14
- package/dist/channels/github/cursor-account.d.ts +43 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -0
- package/dist/channels/github/cursor-account.js +95 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +46 -9
- package/dist/channels/github/index.d.ts +2 -2
- package/dist/channels/github/index.js +2 -2
- package/dist/channels/github/types.d.ts +17 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.d.ts +8 -2
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +8 -0
- package/dist/channels/slack/types.d.ts +24 -3
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/channels/slack/types.js +15 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +8 -8
- package/dist/docs/assets/{ab.md.COdXkces.js → ab.md.BMCZ6Hd7.js} +3 -3
- package/dist/docs/assets/{ab.md.COdXkces.lean.js → ab.md.BMCZ6Hd7.lean.js} +1 -1
- package/dist/docs/assets/{app.DqfFEmJd.js → app.Oje4vhlk.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.zwQ9RCQ7.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BaLEdS15.js → VPLocalSearchBox.H5XZ2zCB.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CZRvu_0q.js → theme.CTR_TuaE.js} +2 -2
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.js → deployment.md.DTKwE15Z.js} +3 -3
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.lean.js → deployment.md.DTKwE15Z.lean.js} +1 -1
- package/dist/docs/assets/{evals.md.DPZ_MAnI.js → evals.md.DAgEc_hL.js} +3 -3
- package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.js → guides_agent-to-agent.md.Bpzgq2Pq.js} +1 -1
- package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.js → guides_cloud-runtime.md.gVzabdQL.js} +1 -1
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.js → guides_github.md.DOOCpqsW.js} +11 -4
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.lean.js → guides_github.md.DOOCpqsW.lean.js} +1 -1
- package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.js → guides_human-in-the-loop.md.DlUqsp1S.js} +2 -2
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.js → guides_slack.md.CCwqHvSV.js} +4 -4
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.lean.js → guides_slack.md.CCwqHvSV.lean.js} +1 -1
- package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.js → guides_webhooks.md.B1EswtUu.js} +2 -2
- package/dist/docs/assets/index.md.m81y7TY7.js +20 -0
- package/dist/docs/assets/{index.md.BPKcj5AI.lean.js → index.md.m81y7TY7.lean.js} +1 -1
- package/dist/docs/assets/quickstart.md.CfU8_uTC.js +192 -0
- package/dist/docs/assets/quickstart.md.CfU8_uTC.lean.js +1 -0
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.js → reference_agent-config.md.DrW2JUM8.js} +4 -4
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.lean.js → reference_agent-config.md.DrW2JUM8.lean.js} +1 -1
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.js → reference_channels.md.DdmiKgqf.js} +4 -4
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.lean.js → reference_channels.md.DdmiKgqf.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.C3vNH_DE.js → reference_connections.md.zaEYCLHT.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.js → reference_hooks.md.DyLVfE1O.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.lean.js → reference_hooks.md.DyLVfE1O.lean.js} +1 -1
- package/dist/docs/assets/{reference_http-api.md.DBAahtdz.js → reference_http-api.md.Dx_nmDG6.js} +1 -1
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.js → reference_instructions.md.CgoV-YEb.js} +9 -7
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.lean.js → reference_instructions.md.CgoV-YEb.lean.js} +1 -1
- package/dist/docs/assets/{reference_schedules.md.D7qijxLk.js → reference_schedules.md.w_F2mXB6.js} +2 -2
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.js → reference_skills.md.B_jHN7JL.js} +3 -3
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.lean.js → reference_skills.md.B_jHN7JL.lean.js} +1 -1
- package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.js → reference_subagents.md.zWAMNfi1.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.DF5kwlt0.js → reference_tools.md.CqgJroI0.js} +2 -2
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.js +1 -0
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.lean.js +1 -0
- package/dist/docs/assets/storage.md.CVnInNiN.js +17 -0
- package/dist/docs/assets/storage.md.CVnInNiN.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/evals.html +7 -7
- package/dist/docs/guides/agent-to-agent.html +6 -6
- package/dist/docs/guides/cloud-runtime.html +5 -5
- package/dist/docs/guides/github.html +14 -7
- package/dist/docs/guides/human-in-the-loop.html +6 -6
- package/dist/docs/guides/slack.html +8 -8
- package/dist/docs/guides/webhooks.html +6 -6
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +5 -5
- package/dist/docs/index.html +7 -7
- package/dist/docs/quickstart.html +180 -23
- package/dist/docs/reference/agent-config.html +7 -7
- package/dist/docs/reference/channels.html +8 -8
- package/dist/docs/reference/cli.html +4 -4
- package/dist/docs/reference/connections.html +5 -5
- package/dist/docs/reference/hooks.html +5 -5
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/instructions.html +13 -11
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +7 -7
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +6 -6
- package/dist/docs/reference/subagents.html +6 -6
- package/dist/docs/reference/tools.html +7 -7
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +41 -0
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/internal/cli-deploy.d.ts +52 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -0
- package/dist/internal/cli-deploy.js +731 -0
- package/dist/internal/cursor/backend-client.d.ts +10 -1
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +79 -1
- package/dist/internal/cursor/github-credentials.d.ts +44 -0
- package/dist/internal/cursor/github-credentials.d.ts.map +1 -0
- package/dist/internal/cursor/github-credentials.js +195 -0
- package/dist/internal/deploy-client.d.ts +176 -0
- package/dist/internal/deploy-client.d.ts.map +1 -0
- package/dist/internal/deploy-client.js +375 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +73 -5
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/eval-run-store.d.ts +22 -3
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +37 -19
- package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
- package/dist/internal/handleAgentServeTrigger.js +10 -12
- package/dist/internal/host-platforms.d.ts +7 -2
- package/dist/internal/host-platforms.d.ts.map +1 -1
- package/dist/internal/host-platforms.js +15 -10
- package/dist/internal/hosting.d.ts +37 -0
- package/dist/internal/hosting.d.ts.map +1 -0
- package/dist/internal/hosting.js +67 -0
- package/dist/internal/reminder-runner.d.ts +7 -0
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +50 -6
- package/dist/internal/reminder-store.d.ts +2 -0
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +18 -0
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +173 -42
- package/dist/internal/session-engine.d.ts +49 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +246 -17
- package/dist/internal/storage-coordinator.d.ts +139 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -0
- package/dist/internal/storage-coordinator.js +499 -0
- package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
- package/dist/playground/assets/index-B1Qc9h2u.css +1 -0
- package/dist/playground/assets/{index-FlWjhg3x.js → index-CpDYCj8W.js} +42 -42
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +204 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +153 -0
- package/dist/types.d.ts +44 -3
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -2
- package/docs/guides/github.md +43 -8
- package/docs/guides/slack.md +1 -1
- package/docs/quickstart.md +329 -51
- package/docs/reference/instructions.md +8 -6
- package/docs/scaffolding-agents.md +1 -1
- package/docs/storage.md +98 -0
- package/package.json +8 -1
- package/skills/github/SKILL.md +9 -1
- package/src/bin/agent-serve.ts +139 -0
- package/src/channels/github/api.ts +42 -23
- package/src/channels/github/cursor-account.ts +165 -0
- package/src/channels/github/github-channel.ts +66 -6
- package/src/channels/github/index.ts +2 -2
- package/src/channels/github/types.ts +19 -0
- package/src/channels/slack/slack-channel.ts +17 -3
- package/src/channels/slack/types.ts +44 -3
- package/src/index.ts +13 -0
- package/src/internal/cli-deploy.ts +940 -0
- package/src/internal/cursor/backend-client.ts +103 -1
- package/src/internal/cursor/github-credentials.ts +248 -0
- package/src/internal/deploy-client.ts +591 -0
- package/src/internal/discovery.ts +88 -1
- package/src/internal/distribution.ts +1 -0
- package/src/internal/eval-run-store.ts +48 -19
- package/src/internal/handleAgentServeTrigger.ts +10 -12
- package/src/internal/host-platforms.ts +28 -11
- package/src/internal/hosting.ts +77 -0
- package/src/internal/reminder-runner.ts +50 -6
- package/src/internal/reminder-store.ts +21 -0
- package/src/internal/server.ts +213 -27
- package/src/internal/session-engine.ts +285 -7
- package/src/internal/storage-coordinator.ts +615 -0
- package/src/storage.ts +325 -0
- package/src/types.ts +40 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +0 -1
- package/dist/docs/assets/index.md.BPKcj5AI.js +0 -20
- package/dist/docs/assets/quickstart.md.tVPiGK_L.js +0 -35
- package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +0 -1
- package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
- package/dist/playground/assets/index-1K-hG-7p.css +0 -1
- /package/dist/docs/assets/{evals.md.DPZ_MAnI.lean.js → evals.md.DAgEc_hL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.lean.js → guides_agent-to-agent.md.Bpzgq2Pq.lean.js} +0 -0
- /package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.lean.js → guides_cloud-runtime.md.gVzabdQL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.lean.js → guides_human-in-the-loop.md.DlUqsp1S.lean.js} +0 -0
- /package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.lean.js → guides_webhooks.md.B1EswtUu.lean.js} +0 -0
- /package/dist/docs/assets/{reference_connections.md.C3vNH_DE.lean.js → reference_connections.md.zaEYCLHT.lean.js} +0 -0
- /package/dist/docs/assets/{reference_http-api.md.DBAahtdz.lean.js → reference_http-api.md.Dx_nmDG6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_schedules.md.D7qijxLk.lean.js → reference_schedules.md.w_F2mXB6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.lean.js → reference_subagents.md.zWAMNfi1.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.DF5kwlt0.lean.js → reference_tools.md.CqgJroI0.lean.js} +0 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime for `defineStorage` (`agent/storage.ts`).
|
|
3
|
+
*
|
|
4
|
+
* The coordinator is the single funnel between the engine's hot paths and
|
|
5
|
+
* the author's key-value sink. The sink is four functions (`put` / `get` /
|
|
6
|
+
* `delete` / `list`); the coordinator owns everything else: it mints every
|
|
7
|
+
* key from the versioned scheme ({@link storageKeys}), coalesces
|
|
8
|
+
* session records and batches event chunks per turn (or debounce window),
|
|
9
|
+
* serializes delivery on one bounded queue per agent, and isolates
|
|
10
|
+
* failures — a throwing sink is logged and its write dropped; storage
|
|
11
|
+
* must never stall or fail a turn.
|
|
12
|
+
*
|
|
13
|
+
* Scaling shape: sessions, reminders, evals, and A/Bs all reduce to keyed
|
|
14
|
+
* puts on the same queue, so one author-owned sink covers every domain,
|
|
15
|
+
* and new domains are new key prefixes — not new config surface.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ABMetricSample, ABSamplePersistence } from "../ab.js";
|
|
19
|
+
import type { EvalRunPersistence, EvalRunSnapshot } from "../evals.js";
|
|
20
|
+
import {
|
|
21
|
+
type ResolvedStoragePolicy,
|
|
22
|
+
resolveStoragePolicy,
|
|
23
|
+
type StorageContext,
|
|
24
|
+
type StorageDefinition,
|
|
25
|
+
storageKeys,
|
|
26
|
+
} from "../storage.js";
|
|
27
|
+
import type { JsonValue, SessionEvent, SessionRecord } from "../types.js";
|
|
28
|
+
import type { ABSnapshot } from "./ab-snapshot.js";
|
|
29
|
+
import { describeError } from "./describe-error.js";
|
|
30
|
+
import { isReminderRecord, type ReminderRecord } from "./reminder-store.js";
|
|
31
|
+
|
|
32
|
+
/** Event types that close a unit of work — flush point for turn-end batching. */
|
|
33
|
+
const TURN_BOUNDARY_EVENTS: ReadonlySet<SessionEvent["type"]> = new Set([
|
|
34
|
+
"turn.completed",
|
|
35
|
+
"turn.failed",
|
|
36
|
+
"session.waiting",
|
|
37
|
+
"session.completed",
|
|
38
|
+
"session.failed",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Max writes waiting on the delivery queue before new ones are dropped.
|
|
43
|
+
* At-most-once delivery already tolerates loss; bounding the queue keeps a
|
|
44
|
+
* slow sink from growing memory without limit under sustained traffic.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_PENDING_OPS = 1_000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How long {@link StorageCoordinator.close} waits for the sink to
|
|
50
|
+
* drain before giving up. Keeps a hung sink from stalling shutdown past
|
|
51
|
+
* the pod grace period (where a SIGKILL would lose the flush anyway).
|
|
52
|
+
*/
|
|
53
|
+
const CLOSE_DRAIN_TIMEOUT_MS = 10_000;
|
|
54
|
+
|
|
55
|
+
/** Debounced event batching flushes early past this many buffered events. */
|
|
56
|
+
const DEBOUNCE_MAX_BATCH = 200;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Aggregate A/B snapshots are refreshed at most this often — the
|
|
60
|
+
* playground polls `GET /v1/abs` every few seconds, and the snapshot is a
|
|
61
|
+
* convenience backfill, not a source of truth.
|
|
62
|
+
*/
|
|
63
|
+
const AB_SNAPSHOT_MIN_INTERVAL_MS = 60_000;
|
|
64
|
+
|
|
65
|
+
/** One queued sink call. */
|
|
66
|
+
type StorageOp =
|
|
67
|
+
| { op: "put"; key: string; value: JsonValue }
|
|
68
|
+
| { op: "delete"; key: string };
|
|
69
|
+
|
|
70
|
+
export interface StorageCoordinatorOptions {
|
|
71
|
+
definition: StorageDefinition;
|
|
72
|
+
agentName: string;
|
|
73
|
+
projectRoot: string;
|
|
74
|
+
logger: (line: string) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface SessionBuffer {
|
|
78
|
+
events: SessionEvent[];
|
|
79
|
+
/** Coalesced latest record; only the newest at flush time is written. */
|
|
80
|
+
record?: SessionRecord;
|
|
81
|
+
debounceTimer?: ReturnType<typeof setTimeout>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class StorageCoordinator {
|
|
85
|
+
readonly policy: ResolvedStoragePolicy;
|
|
86
|
+
|
|
87
|
+
private readonly definition: StorageDefinition;
|
|
88
|
+
private readonly agentName: string;
|
|
89
|
+
private readonly projectRoot: string;
|
|
90
|
+
private readonly logger: (line: string) => void;
|
|
91
|
+
/** Per-session pending state (events + coalesced record). */
|
|
92
|
+
private readonly buffers = new Map<string, SessionBuffer>();
|
|
93
|
+
/**
|
|
94
|
+
* Last continuation key written to the sink per session, so a token
|
|
95
|
+
* change emits a delete for the stale index entry alongside the new put.
|
|
96
|
+
*/
|
|
97
|
+
private readonly continuationIndex = new Map<string, string | null>();
|
|
98
|
+
/** Single delivery chain: writes reach the sink serialized, in order. */
|
|
99
|
+
private queue: Promise<void> = Promise.resolve();
|
|
100
|
+
/** Ops on the queue not yet delivered (bounded by MAX_PENDING_OPS). */
|
|
101
|
+
private pending = 0;
|
|
102
|
+
/** Ops dropped because the queue was full. */
|
|
103
|
+
private dropped = 0;
|
|
104
|
+
private lastAbSnapshotAt = 0;
|
|
105
|
+
private closed = false;
|
|
106
|
+
|
|
107
|
+
constructor(options: StorageCoordinatorOptions) {
|
|
108
|
+
this.definition = options.definition;
|
|
109
|
+
this.policy = resolveStoragePolicy(options.definition.policy);
|
|
110
|
+
this.agentName = options.agentName;
|
|
111
|
+
this.projectRoot = options.projectRoot;
|
|
112
|
+
this.logger = options.logger;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ==========================================================================
|
|
116
|
+
// Sessions (writes)
|
|
117
|
+
// ==========================================================================
|
|
118
|
+
|
|
119
|
+
/** Durable session-record update (engine calls after every store save). */
|
|
120
|
+
sessionRecord(record: SessionRecord): void {
|
|
121
|
+
if (this.closed) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Coalesced: only the newest record at flush time reaches the sink.
|
|
125
|
+
this.buffer(record.sessionId).record = record;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** One appended session event (engine calls from its dispatch funnel). */
|
|
129
|
+
event(event: SessionEvent): void {
|
|
130
|
+
if (this.closed) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const buffer = this.buffer(event.sessionId);
|
|
134
|
+
buffer.events.push(event);
|
|
135
|
+
const events = this.policy.events;
|
|
136
|
+
if (events !== "turnEnd") {
|
|
137
|
+
// Debounce spans turn boundaries by design ("flush when the session
|
|
138
|
+
// goes quiet", not once per turn); the coalesced record rides the
|
|
139
|
+
// same debounced flush.
|
|
140
|
+
if (buffer.events.length >= DEBOUNCE_MAX_BATCH) {
|
|
141
|
+
this.flushSession(event.sessionId);
|
|
142
|
+
} else {
|
|
143
|
+
this.armDebounce(event.sessionId, buffer, events.debounceMs);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Turn boundaries do NOT flush here: the boundary event's own handlers
|
|
147
|
+
// (and their record updates) haven't run yet. The engine calls
|
|
148
|
+
// eventDispatched() once dispatch settles — that's the flush point.
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A boundary event's dispatch settled: channel/hook handlers ran and
|
|
153
|
+
* their record updates (channel state, continuation tokens) are
|
|
154
|
+
* buffered. This — not the append — is the turn-end flush point, so the
|
|
155
|
+
* persisted session record includes the boundary event's own handler
|
|
156
|
+
* mutations. Debounced batching intentionally ignores boundaries.
|
|
157
|
+
*/
|
|
158
|
+
eventDispatched(event: SessionEvent): void {
|
|
159
|
+
if (
|
|
160
|
+
this.closed ||
|
|
161
|
+
this.policy.events !== "turnEnd" ||
|
|
162
|
+
!TURN_BOUNDARY_EVENTS.has(event.type)
|
|
163
|
+
) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.flushSession(event.sessionId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Flush everything buffered for one session (ordered: events, record). */
|
|
170
|
+
flushSession(
|
|
171
|
+
sessionId: string,
|
|
172
|
+
reason: StorageContext["reason"] = "policy"
|
|
173
|
+
): void {
|
|
174
|
+
const buffer = this.buffers.get(sessionId);
|
|
175
|
+
if (buffer === undefined) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.buffers.delete(sessionId);
|
|
179
|
+
if (buffer.debounceTimer !== undefined) {
|
|
180
|
+
clearTimeout(buffer.debounceTimer);
|
|
181
|
+
}
|
|
182
|
+
const ops: StorageOp[] = [];
|
|
183
|
+
const first = buffer.events[0];
|
|
184
|
+
if (first !== undefined) {
|
|
185
|
+
ops.push({
|
|
186
|
+
op: "put",
|
|
187
|
+
key: storageKeys.sessionEvents(this.agentName, sessionId, first.index),
|
|
188
|
+
value: buffer.events as unknown as JsonValue,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (buffer.record !== undefined) {
|
|
192
|
+
ops.push({
|
|
193
|
+
op: "put",
|
|
194
|
+
key: storageKeys.session(this.agentName, sessionId),
|
|
195
|
+
value: buffer.record as unknown as JsonValue,
|
|
196
|
+
});
|
|
197
|
+
ops.push(...this.continuationOps(buffer.record));
|
|
198
|
+
}
|
|
199
|
+
if (ops.length > 0) {
|
|
200
|
+
this.enqueue(ops, reason);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Index maintenance for `continuation/{channelId}/{token}` → sessionId:
|
|
206
|
+
* put the current token, delete the previous one when it changed.
|
|
207
|
+
*/
|
|
208
|
+
private continuationOps(record: SessionRecord): StorageOp[] {
|
|
209
|
+
const previous = this.continuationIndex.get(record.sessionId);
|
|
210
|
+
const next = record.continuationKey;
|
|
211
|
+
if (previous === next) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
this.continuationIndex.set(record.sessionId, next);
|
|
215
|
+
const ops: StorageOp[] = [];
|
|
216
|
+
if (previous != null) {
|
|
217
|
+
ops.push({
|
|
218
|
+
op: "delete",
|
|
219
|
+
key: storageKeys.continuation(
|
|
220
|
+
this.agentName,
|
|
221
|
+
record.channelId,
|
|
222
|
+
previous
|
|
223
|
+
),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (next != null) {
|
|
227
|
+
ops.push({
|
|
228
|
+
op: "put",
|
|
229
|
+
key: storageKeys.continuation(this.agentName, record.channelId, next),
|
|
230
|
+
value: { sessionId: record.sessionId },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return ops;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ==========================================================================
|
|
237
|
+
// Sessions (reads / restore)
|
|
238
|
+
// ==========================================================================
|
|
239
|
+
|
|
240
|
+
/** Whether the sink can serve the startup bulk restore (`list`). */
|
|
241
|
+
get canBulkRestore(): boolean {
|
|
242
|
+
return this.definition.list !== undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Saved session records for this agent (startup restore). Errors are
|
|
247
|
+
* logged and read as "nothing saved" — a broken store must never block
|
|
248
|
+
* serve start.
|
|
249
|
+
*/
|
|
250
|
+
async listSessions(): Promise<SessionRecord[]> {
|
|
251
|
+
const entries = await this.tryList(
|
|
252
|
+
storageKeys.sessionPrefix(this.agentName)
|
|
253
|
+
);
|
|
254
|
+
return entries
|
|
255
|
+
.map((entry) => entry.value as unknown as SessionRecord)
|
|
256
|
+
.filter((record) => record != null);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Saved event stream for one session: chunk values concatenated in key
|
|
261
|
+
* order (chunks are keyed by their first event's index, zero-padded, so
|
|
262
|
+
* ascending key order is append order). Errors read as an empty stream.
|
|
263
|
+
*/
|
|
264
|
+
async listSessionEvents(sessionId: string): Promise<SessionEvent[]> {
|
|
265
|
+
const entries = await this.tryList(
|
|
266
|
+
storageKeys.sessionEventsPrefix(this.agentName, sessionId)
|
|
267
|
+
);
|
|
268
|
+
const events: SessionEvent[] = [];
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
if (Array.isArray(entry.value)) {
|
|
271
|
+
events.push(...(entry.value as unknown as SessionEvent[]));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return events;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* One saved session by channel continuation key (lazy restore). Unlike
|
|
279
|
+
* the startup reads, sink errors **propagate**: a failing store must
|
|
280
|
+
* fail the follow-up (which the caller can retry) rather than read as
|
|
281
|
+
* "unknown token" — that would mint a new session under the same
|
|
282
|
+
* continuation key and permanently shadow the real one on this host.
|
|
283
|
+
* A definitive miss (no index entry, or a write-only sink without
|
|
284
|
+
* `get`) resolves undefined.
|
|
285
|
+
*/
|
|
286
|
+
async getSessionByContinuation(
|
|
287
|
+
channelId: string,
|
|
288
|
+
continuationKey: string
|
|
289
|
+
): Promise<SessionRecord | undefined> {
|
|
290
|
+
const get = this.definition.get;
|
|
291
|
+
if (get === undefined) {
|
|
292
|
+
return undefined;
|
|
293
|
+
}
|
|
294
|
+
const ctx = this.context("restore");
|
|
295
|
+
const pointer = await get(
|
|
296
|
+
storageKeys.continuation(this.agentName, channelId, continuationKey),
|
|
297
|
+
ctx
|
|
298
|
+
);
|
|
299
|
+
if (pointer == null) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
const sessionId =
|
|
303
|
+
typeof pointer === "object" && !Array.isArray(pointer)
|
|
304
|
+
? pointer.sessionId
|
|
305
|
+
: undefined;
|
|
306
|
+
if (typeof sessionId !== "string" || sessionId === "") {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`storage returned a malformed continuation entry for channel ${channelId}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const record = (await get(
|
|
312
|
+
storageKeys.session(this.agentName, sessionId),
|
|
313
|
+
ctx
|
|
314
|
+
)) as SessionRecord | null | undefined;
|
|
315
|
+
if (record == null) {
|
|
316
|
+
// Index entry without its record (e.g. the record write was shed
|
|
317
|
+
// under at-most-once). The session is unrecoverable; treat as a miss
|
|
318
|
+
// so the conversation can continue rather than failing forever.
|
|
319
|
+
this.logger(
|
|
320
|
+
`[agent-serve] storage continuation entry points at missing session ${sessionId}; treating as a miss`
|
|
321
|
+
);
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
// The sink must answer for the exact key it was asked about — a
|
|
325
|
+
// mismatched row is a broken store, not a miss; treating it as a miss
|
|
326
|
+
// would fork the continuation token onto a new session.
|
|
327
|
+
if (
|
|
328
|
+
record.sessionId !== sessionId ||
|
|
329
|
+
record.channelId !== channelId ||
|
|
330
|
+
record.continuationKey !== continuationKey
|
|
331
|
+
) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`storage returned a session that does not match the requested continuation (channel ${channelId})`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return record;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ==========================================================================
|
|
340
|
+
// Reminders
|
|
341
|
+
// ==========================================================================
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Mirror one reminder record (create / fire / cancel / disarm). Local
|
|
345
|
+
* disk under `--state-root/reminders` remains the live source of truth;
|
|
346
|
+
* this is the durable copy for host replacement.
|
|
347
|
+
*/
|
|
348
|
+
reminder(record: ReminderRecord): void {
|
|
349
|
+
if (this.closed) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.enqueue(
|
|
353
|
+
[
|
|
354
|
+
{
|
|
355
|
+
op: "put",
|
|
356
|
+
key: storageKeys.reminder(this.agentName, record.id),
|
|
357
|
+
value: record as unknown as JsonValue,
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
"policy"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Saved reminder records for this agent (startup hydrate into local
|
|
366
|
+
* disk). Errors read as empty — same posture as session bulk restore.
|
|
367
|
+
*/
|
|
368
|
+
async listReminders(): Promise<ReminderRecord[]> {
|
|
369
|
+
const entries = await this.tryList(
|
|
370
|
+
storageKeys.reminderPrefix(this.agentName)
|
|
371
|
+
);
|
|
372
|
+
return entries.flatMap((entry) =>
|
|
373
|
+
isReminderRecord(entry.value) ? [entry.value] : []
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ==========================================================================
|
|
378
|
+
// Evals
|
|
379
|
+
// ==========================================================================
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Adapter for {@link EvalRunPersistence} so the playground eval store
|
|
383
|
+
* can fall back to this sink when `evals.config.ts` sets no
|
|
384
|
+
* `persistRuns`.
|
|
385
|
+
*/
|
|
386
|
+
asEvalRunPersistence(): EvalRunPersistence {
|
|
387
|
+
return {
|
|
388
|
+
load: async () => {
|
|
389
|
+
const entries = await this.tryList(
|
|
390
|
+
storageKeys.evalRunPrefix(this.agentName)
|
|
391
|
+
);
|
|
392
|
+
return entries
|
|
393
|
+
.map((entry) => entry.value as unknown as EvalRunSnapshot)
|
|
394
|
+
.filter((run) => run != null);
|
|
395
|
+
},
|
|
396
|
+
save: (run: EvalRunSnapshot) => {
|
|
397
|
+
this.enqueue(
|
|
398
|
+
[
|
|
399
|
+
{
|
|
400
|
+
op: "put",
|
|
401
|
+
key: storageKeys.evalRun(this.agentName, run.runId),
|
|
402
|
+
value: run as unknown as JsonValue,
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
"policy"
|
|
406
|
+
);
|
|
407
|
+
},
|
|
408
|
+
delete: (runId: string) => {
|
|
409
|
+
this.enqueue(
|
|
410
|
+
[
|
|
411
|
+
{
|
|
412
|
+
op: "delete",
|
|
413
|
+
key: storageKeys.evalRun(this.agentName, runId),
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
"policy"
|
|
417
|
+
);
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ==========================================================================
|
|
423
|
+
// A/Bs
|
|
424
|
+
// ==========================================================================
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Adapter for {@link ABSamplePersistence} so the AB collector can fall
|
|
428
|
+
* back to this sink when `ab.config.ts` sets no `persistSamples`.
|
|
429
|
+
*/
|
|
430
|
+
asABSamplePersistence(): ABSamplePersistence {
|
|
431
|
+
return {
|
|
432
|
+
save: (sample: ABMetricSample) => {
|
|
433
|
+
this.enqueue(
|
|
434
|
+
[
|
|
435
|
+
{
|
|
436
|
+
op: "put",
|
|
437
|
+
key: storageKeys.abSample(
|
|
438
|
+
this.agentName,
|
|
439
|
+
sample.sessionId,
|
|
440
|
+
sample.at
|
|
441
|
+
),
|
|
442
|
+
value: sample as unknown as JsonValue,
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
"policy"
|
|
446
|
+
);
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Refresh the persisted aggregate A/B snapshot, throttled to once per
|
|
453
|
+
* {@link AB_SNAPSHOT_MIN_INTERVAL_MS} (the playground recomputes the
|
|
454
|
+
* fold on every `GET /v1/abs` poll).
|
|
455
|
+
*/
|
|
456
|
+
abSnapshot(snapshot: ABSnapshot): void {
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
if (
|
|
459
|
+
this.closed ||
|
|
460
|
+
now - this.lastAbSnapshotAt < AB_SNAPSHOT_MIN_INTERVAL_MS
|
|
461
|
+
) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.lastAbSnapshotAt = now;
|
|
465
|
+
this.enqueue(
|
|
466
|
+
[
|
|
467
|
+
{
|
|
468
|
+
op: "put",
|
|
469
|
+
key: storageKeys.abSnapshot(this.agentName),
|
|
470
|
+
value: snapshot as unknown as JsonValue,
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
"policy"
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Latest persisted aggregate A/B snapshot; errors read as absent. */
|
|
478
|
+
async getLatestAbSnapshot(): Promise<ABSnapshot | undefined> {
|
|
479
|
+
const get = this.definition.get;
|
|
480
|
+
if (get === undefined) {
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
const value = await get(
|
|
485
|
+
storageKeys.abSnapshot(this.agentName),
|
|
486
|
+
this.context("restore")
|
|
487
|
+
);
|
|
488
|
+
return value == null ? undefined : (value as unknown as ABSnapshot);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
this.logger(
|
|
491
|
+
`[agent-serve] storage get(ab-snapshot) failed: ${describeError(error)}`
|
|
492
|
+
);
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ==========================================================================
|
|
498
|
+
// Delivery
|
|
499
|
+
// ==========================================================================
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Flush all buffers and drain the queue, giving the sink at most
|
|
503
|
+
* {@link CLOSE_DRAIN_TIMEOUT_MS} — a hung sink must not stall shutdown
|
|
504
|
+
* past the pod grace period. Called from engine close.
|
|
505
|
+
*/
|
|
506
|
+
async close(): Promise<void> {
|
|
507
|
+
if (!this.closed) {
|
|
508
|
+
for (const sessionId of [...this.buffers.keys()]) {
|
|
509
|
+
this.flushSession(sessionId, "shutdown");
|
|
510
|
+
}
|
|
511
|
+
this.closed = true;
|
|
512
|
+
}
|
|
513
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
514
|
+
const drained = await Promise.race([
|
|
515
|
+
this.queue.then(() => true),
|
|
516
|
+
new Promise<boolean>((resolve) => {
|
|
517
|
+
timer = setTimeout(() => resolve(false), CLOSE_DRAIN_TIMEOUT_MS);
|
|
518
|
+
timer.unref?.();
|
|
519
|
+
}),
|
|
520
|
+
]);
|
|
521
|
+
clearTimeout(timer);
|
|
522
|
+
if (!drained) {
|
|
523
|
+
this.logger(
|
|
524
|
+
`[agent-serve] storage shutdown drain timed out after ${CLOSE_DRAIN_TIMEOUT_MS}ms with ${this.pending} write(s) undelivered`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Pending deliveries (exposed for tests and drain instrumentation). */
|
|
530
|
+
whenIdle(): Promise<void> {
|
|
531
|
+
return this.queue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private buffer(sessionId: string): SessionBuffer {
|
|
535
|
+
let buffer = this.buffers.get(sessionId);
|
|
536
|
+
if (buffer === undefined) {
|
|
537
|
+
buffer = { events: [] };
|
|
538
|
+
this.buffers.set(sessionId, buffer);
|
|
539
|
+
}
|
|
540
|
+
return buffer;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private armDebounce(
|
|
544
|
+
sessionId: string,
|
|
545
|
+
buffer: SessionBuffer,
|
|
546
|
+
debounceMs: number
|
|
547
|
+
): void {
|
|
548
|
+
if (buffer.debounceTimer !== undefined) {
|
|
549
|
+
clearTimeout(buffer.debounceTimer);
|
|
550
|
+
}
|
|
551
|
+
buffer.debounceTimer = setTimeout(() => {
|
|
552
|
+
buffer.debounceTimer = undefined;
|
|
553
|
+
this.flushSession(sessionId);
|
|
554
|
+
}, debounceMs);
|
|
555
|
+
// Never keep the process alive just for a pending storage flush.
|
|
556
|
+
buffer.debounceTimer.unref?.();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** `list` wrapper for the startup reads: missing hook or throw ⇒ empty. */
|
|
560
|
+
private async tryList(
|
|
561
|
+
prefix: string
|
|
562
|
+
): Promise<Array<{ key: string; value: JsonValue }>> {
|
|
563
|
+
const list = this.definition.list;
|
|
564
|
+
if (list === undefined) {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
const entries = await list(prefix, this.context("restore"));
|
|
569
|
+
return Array.isArray(entries) ? entries : [];
|
|
570
|
+
} catch (error) {
|
|
571
|
+
this.logger(
|
|
572
|
+
`[agent-serve] storage list(${prefix}) failed: ${describeError(error)}`
|
|
573
|
+
);
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private enqueue(ops: StorageOp[], reason: StorageContext["reason"]): void {
|
|
579
|
+
if (this.closed && reason !== "shutdown") {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (this.pending + ops.length > MAX_PENDING_OPS) {
|
|
583
|
+
this.dropped += ops.length;
|
|
584
|
+
if (this.dropped <= ops.length || this.dropped % 1_000 < ops.length) {
|
|
585
|
+
this.logger(
|
|
586
|
+
`[agent-serve] storage queue full (${MAX_PENDING_OPS} pending writes); dropped ${this.dropped} write(s) so far — the sink is too slow for the traffic`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.pending += ops.length;
|
|
592
|
+
const ctx = this.context(reason);
|
|
593
|
+
this.queue = this.queue.then(async () => {
|
|
594
|
+
for (const op of ops) {
|
|
595
|
+
try {
|
|
596
|
+
if (op.op === "put") {
|
|
597
|
+
await this.definition.put(op.key, op.value, ctx);
|
|
598
|
+
} else {
|
|
599
|
+
await this.definition.delete?.(op.key, ctx);
|
|
600
|
+
}
|
|
601
|
+
} catch (error) {
|
|
602
|
+
this.logger(
|
|
603
|
+
`[agent-serve] storage ${op.op}(${op.key}) failed: ${describeError(error)}`
|
|
604
|
+
);
|
|
605
|
+
} finally {
|
|
606
|
+
this.pending -= 1;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private context(reason: StorageContext["reason"]): StorageContext {
|
|
613
|
+
return { agentName: this.agentName, projectRoot: this.projectRoot, reason };
|
|
614
|
+
}
|
|
615
|
+
}
|