@cursor/july 0.1.43 → 0.1.46
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 +5 -3
- package/README.md +3 -1
- package/dist/channels/github/api.d.ts +25 -0
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +48 -0
- package/dist/channels/github/cursor-account.d.ts +7 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -1
- package/dist/channels/github/cursor-account.js +18 -0
- package/dist/channels/github/defaults.d.ts +1 -1
- package/dist/channels/github/defaults.js +1 -1
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +15 -7
- package/dist/channels/github/progress.d.ts +6 -5
- package/dist/channels/github/progress.d.ts.map +1 -1
- package/dist/channels/github/progress.js +58 -16
- package/dist/channels/github/state.d.ts +11 -0
- package/dist/channels/github/state.d.ts.map +1 -1
- package/dist/channels/github/state.js +1 -1
- package/dist/channels/github/types.d.ts +11 -3
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels.d.ts +17 -0
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +31 -0
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.DsTLo2j1.js → app.C2NFTb-h.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.B-FxvuoI.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DoVfutcV.js → VPLocalSearchBox.o1_FJGip.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DtuVtKkc.js → theme.BcGduE7g.js} +2 -2
- package/dist/docs/assets/{guides_github.md.DSg0j9qj.js → guides_github.md.B8QKtIpw.js} +7 -6
- package/dist/docs/assets/{guides_github.md.DSg0j9qj.lean.js → guides_github.md.B8QKtIpw.lean.js} +1 -1
- package/dist/docs/assets/guides_opentelemetry.md.Csn7ZI25.js +27 -0
- package/dist/docs/assets/guides_opentelemetry.md.Csn7ZI25.lean.js +1 -0
- package/dist/docs/assets/index.md.COeoSkcF.js +5 -0
- package/dist/docs/assets/index.md.COeoSkcF.lean.js +1 -0
- package/dist/docs/assets/{quickstart.md.rIbfahJM.js → quickstart.md.DZxBu44y.js} +18 -49
- package/dist/docs/assets/quickstart.md.DZxBu44y.lean.js +1 -0
- package/dist/docs/assets/{reference_agent-config.md.CRmkoxd6.js → reference_agent-config.md.DHkkvcEZ.js} +9 -4
- package/dist/docs/assets/{reference_agent-config.md.CRmkoxd6.lean.js → reference_agent-config.md.DHkkvcEZ.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.DqEqQpGn.js → reference_cli.md.BDmjPPAm.js} +2 -2
- package/dist/docs/assets/{reference_hooks.md.4cbRRIIu.js → reference_hooks.md.DJE5DXcT.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.lSrsTxYJ.js → reference_tools.md.kJfJAP2m.js} +1 -1
- package/dist/docs/assets/{storage.md.DcGSnIxX.js → storage.md.CJYd5YmN.js} +2 -2
- package/dist/docs/assets/{storage.md.DcGSnIxX.lean.js → storage.md.CJYd5YmN.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +3 -3
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +10 -9
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +12 -5
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +8 -28
- package/dist/docs/quickstart.html +24 -55
- package/dist/docs/reference/agent-config.html +11 -6
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +6 -6
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +5 -5
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +5 -5
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +5 -5
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/internal/auth.d.ts +4 -0
- package/dist/internal/auth.d.ts.map +1 -1
- package/dist/internal/auth.js +7 -0
- package/dist/internal/cli-ax.js +2 -2
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +38 -7
- package/dist/internal/deploy-client.d.ts +8 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +5 -2
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +46 -9
- package/dist/internal/durable-session-store.d.ts +35 -23
- package/dist/internal/durable-session-store.d.ts.map +1 -1
- package/dist/internal/durable-session-store.js +71 -26
- package/dist/internal/host-otel.d.ts +25 -0
- package/dist/internal/host-otel.d.ts.map +1 -0
- package/dist/internal/host-otel.js +162 -0
- package/dist/internal/init-project.js +10 -10
- package/dist/internal/ledger-serving.d.ts +135 -67
- package/dist/internal/ledger-serving.d.ts.map +1 -1
- package/dist/internal/ledger-serving.js +326 -107
- package/dist/internal/otel/collector.d.ts +5 -0
- package/dist/internal/otel/collector.d.ts.map +1 -1
- package/dist/internal/otel/collector.js +68 -27
- package/dist/internal/sdk-runner.d.ts +1 -1
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +1 -1
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +3 -2
- package/dist/internal/session-engine.d.ts +23 -18
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +83 -32
- package/dist/kv/memory-kv.d.ts +8 -0
- package/dist/kv/memory-kv.d.ts.map +1 -1
- package/dist/kv/memory-kv.js +8 -0
- package/dist/ledger/janitor.d.ts +60 -0
- package/dist/ledger/janitor.d.ts.map +1 -0
- package/dist/ledger/janitor.js +137 -0
- package/dist/ledger/session-ledger.d.ts +132 -43
- package/dist/ledger/session-ledger.d.ts.map +1 -1
- package/dist/ledger/session-ledger.js +282 -50
- package/dist/ledger.d.ts +9 -8
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +7 -7
- package/dist/playground/assets/{index-D-DIYQzw.js → index-6wW6SAc4.js} +19 -19
- package/dist/playground/assets/index-CSRYDAFw.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +5 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +6 -0
- package/dist/types.d.ts +100 -10
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +33 -106
- package/docs/guides/github.md +23 -18
- package/docs/guides/opentelemetry.md +30 -0
- package/docs/quickstart.md +89 -227
- package/docs/reference/agent-config.md +47 -0
- package/docs/reference/cli.md +2 -2
- package/docs/reference/hooks.md +1 -1
- package/docs/reference/tools.md +5 -4
- package/docs/storage.md +26 -9
- package/package.json +2 -2
- package/skills/otel/SKILL.md +20 -2
- package/src/channels/github/api.ts +122 -0
- package/src/channels/github/cursor-account.ts +23 -0
- package/src/channels/github/defaults.ts +1 -1
- package/src/channels/github/github-channel.ts +14 -1
- package/src/channels/github/progress.ts +84 -19
- package/src/channels/github/state.ts +14 -0
- package/src/channels/github/types.ts +7 -3
- package/src/channels.ts +38 -0
- package/src/index.ts +2 -0
- package/src/internal/auth.ts +10 -0
- package/src/internal/cli-ax.ts +2 -2
- package/src/internal/cli-deploy.ts +58 -3
- package/src/internal/deploy-client.ts +11 -0
- package/src/internal/discovery.ts +49 -0
- package/src/internal/durable-session-store.ts +66 -27
- package/src/internal/host-otel.ts +252 -0
- package/src/internal/init-project.ts +10 -10
- package/src/internal/ledger-serving.ts +439 -148
- package/src/internal/otel/collector.ts +135 -50
- package/src/internal/sdk-runner.ts +6 -0
- package/src/internal/server.ts +7 -2
- package/src/internal/session-engine.ts +127 -39
- package/src/kv/memory-kv.ts +8 -0
- package/src/ledger/janitor.ts +193 -0
- package/src/ledger/session-ledger.ts +372 -68
- package/src/ledger.ts +21 -6
- package/src/storage-backends/cursor-hosted.ts +17 -2
- package/src/types.ts +108 -10
- package/dist/docs/assets/chunks/@localSearchIndexroot.BZx4pmZL.js +0 -1
- package/dist/docs/assets/guides_opentelemetry.md.DmyUwC5R.js +0 -20
- package/dist/docs/assets/guides_opentelemetry.md.DmyUwC5R.lean.js +0 -1
- package/dist/docs/assets/index.md.CMc5FP7E.js +0 -25
- package/dist/docs/assets/index.md.CMc5FP7E.lean.js +0 -1
- package/dist/docs/assets/quickstart.md.rIbfahJM.lean.js +0 -1
- package/dist/playground/assets/index-DQTH622f.css +0 -1
- /package/dist/docs/assets/{reference_cli.md.DqEqQpGn.lean.js → reference_cli.md.BDmjPPAm.lean.js} +0 -0
- /package/dist/docs/assets/{reference_hooks.md.4cbRRIIu.lean.js → reference_hooks.md.DJE5DXcT.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.lSrsTxYJ.lean.js → reference_tools.md.kJfJAP2m.lean.js} +0 -0
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
12
|
import type { SDKCustomTool, SDKCustomToolResult } from "@cursor/sdk";
|
|
13
|
+
import type { Meter } from "@opentelemetry/api";
|
|
13
14
|
import {
|
|
14
15
|
type ABDefinition,
|
|
15
16
|
mergeABInstructions,
|
|
@@ -75,7 +76,7 @@ import { foldABStream } from "./ab-fold.js";
|
|
|
75
76
|
import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
|
|
76
77
|
import { ApprovalGate } from "./approval-gate.js";
|
|
77
78
|
import { ArtifactsStore } from "./artifacts-store.js";
|
|
78
|
-
import { samePrincipal } from "./auth.js";
|
|
79
|
+
import { SessionAuthError, samePrincipal } from "./auth.js";
|
|
79
80
|
import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
|
|
80
81
|
import {
|
|
81
82
|
applyCloudUsageToEvents,
|
|
@@ -95,6 +96,7 @@ import { TurnEventMapper } from "./event-mapper.js";
|
|
|
95
96
|
import { selectFrameworkStorageDefinition } from "./framework-storage-selection.js";
|
|
96
97
|
import { createHostFilesApi } from "./host-files.js";
|
|
97
98
|
import { createHostKvApi } from "./host-kv.js";
|
|
99
|
+
import { createHostOtelApi, type HostOtelSessionApi } from "./host-otel.js";
|
|
98
100
|
import {
|
|
99
101
|
createHostPlatforms,
|
|
100
102
|
createHostSlackClient,
|
|
@@ -147,14 +149,9 @@ export class SessionBusyError extends Error {
|
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
`Session ${sessionId} belongs to a different principal; pass the original caller auth on follow-up`
|
|
154
|
-
);
|
|
155
|
-
this.name = "SessionAuthError";
|
|
156
|
-
}
|
|
157
|
-
}
|
|
152
|
+
// Defined in auth.js (the ledger pump needs it without importing this
|
|
153
|
+
// module); re-exported here because this is its historical public home.
|
|
154
|
+
export { SessionAuthError } from "./auth.js";
|
|
158
155
|
|
|
159
156
|
export class UnknownChannelError extends Error {
|
|
160
157
|
constructor(channelId: string) {
|
|
@@ -232,10 +229,11 @@ export interface SessionEngineOptions {
|
|
|
232
229
|
/**
|
|
233
230
|
* Explicit durable-session store for `serving: "ledger"` agents — a KV
|
|
234
231
|
* dedicated to the ledger (session identity, leases, commit heads),
|
|
235
|
-
* separate from agent state. Must implement the
|
|
236
|
-
* (`
|
|
237
|
-
*
|
|
238
|
-
*
|
|
232
|
+
* separate from agent state. Must implement the full CAS-capable KV
|
|
233
|
+
* surface (`HostKvCasApi`: the CAS trio plus `get`/`put`/`delete` for
|
|
234
|
+
* mailbox payload blobs and `listKeys` for the janitor scan);
|
|
235
|
+
* construction fails fast otherwise. Supply the **raw** store: the
|
|
236
|
+
* engine owns key scoping and namespaces every ledger key under
|
|
239
237
|
* `agentkit/v1/<agent>/durable-session/` itself — a self-hoster never
|
|
240
238
|
* writes prefixes. Default when omitted: the authored storage backend
|
|
241
239
|
* (`agent/storage.ts`) when it implements the `cas` group, else — with
|
|
@@ -246,6 +244,8 @@ export interface SessionEngineOptions {
|
|
|
246
244
|
durableSessionStore?: HostKvApi;
|
|
247
245
|
/** Process-level OTel collector; observe-only, never fatal. */
|
|
248
246
|
otel?: OtelCollector;
|
|
247
|
+
/** Process-level meter for {@link HostContext.otel}. Absent → no-op counters. */
|
|
248
|
+
otelMeter?: Meter;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
/**
|
|
@@ -282,12 +282,13 @@ interface ActiveToolContext {
|
|
|
282
282
|
export interface EngineSendOptions extends SendMessageOptions {
|
|
283
283
|
mode?: "chat" | "task";
|
|
284
284
|
/**
|
|
285
|
-
* Internal (ledger serving):
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
285
|
+
* Internal (ledger serving): set only by the ledger pump when it
|
|
286
|
+
* executes an already-parked event under its lease, so the call must
|
|
287
|
+
* run the resident path. Without the flag, every send on a
|
|
288
|
+
* `serving: "ledger"` agent routes through the ledger (append → claim
|
|
289
|
+
* → drain → turn → commit) and the pump would recurse into itself.
|
|
289
290
|
*/
|
|
290
|
-
|
|
291
|
+
fromLedgerPump?: boolean;
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
interface CoalescedQueuedTurn {
|
|
@@ -362,6 +363,7 @@ export class SessionEngine {
|
|
|
362
363
|
private readonly approvals = new ApprovalGate();
|
|
363
364
|
private readonly abCollector: ABCollector;
|
|
364
365
|
private readonly otel: OtelCollector | undefined;
|
|
366
|
+
private readonly otelHost: HostOtelSessionApi;
|
|
365
367
|
private reminderApi: HostContext["reminders"];
|
|
366
368
|
private evalsApi: HostContext["evals"];
|
|
367
369
|
private readonly kvApi: HostContext["kv"];
|
|
@@ -395,6 +397,11 @@ export class SessionEngine {
|
|
|
395
397
|
this.getCloudUsage = options.getCloudUsage ?? defaultGetCloudUsage;
|
|
396
398
|
this.estimateTurnCost = options.estimateTurnCost ?? estimateTurnCostFromApi;
|
|
397
399
|
this.otel = options.otel;
|
|
400
|
+
this.otelHost = createHostOtelApi({
|
|
401
|
+
agentName: options.project.name,
|
|
402
|
+
meter: options.otelMeter,
|
|
403
|
+
collector: options.otel,
|
|
404
|
+
});
|
|
398
405
|
this.turnGovernor = new TurnGovernor(options.project.agent.concurrency);
|
|
399
406
|
this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
|
|
400
407
|
this.nudgeStore = new SlackNudgeStore(
|
|
@@ -472,11 +479,12 @@ export class SessionEngine {
|
|
|
472
479
|
this.channelsById.set(channel.id, channel.definition);
|
|
473
480
|
}
|
|
474
481
|
this.ledgerServing = this.buildLedgerServing(options);
|
|
482
|
+
this.ledgerServing?.start();
|
|
475
483
|
}
|
|
476
484
|
|
|
477
485
|
/**
|
|
478
486
|
* Ledger-mode wiring (`serving: "ledger"`): resolve a CAS-capable
|
|
479
|
-
* durable-session store and build the
|
|
487
|
+
* durable-session store and build the event pump. Resident agents
|
|
480
488
|
* (the default) return undefined and never touch ledger keys.
|
|
481
489
|
*/
|
|
482
490
|
private buildLedgerServing(
|
|
@@ -485,15 +493,46 @@ export class SessionEngine {
|
|
|
485
493
|
if (options.project.agent.serving !== "ledger") {
|
|
486
494
|
return undefined;
|
|
487
495
|
}
|
|
496
|
+
// The handle is already agent-scoped (resolveDurableSessionStore):
|
|
497
|
+
// two ledger-mode agents sharing one backend never resolve to each
|
|
498
|
+
// other's sessions or janitor-claim each other's parked envelopes.
|
|
488
499
|
const ledgerKv = this.resolveDurableSessionStore(options);
|
|
489
500
|
return new LedgerServing({
|
|
490
501
|
ledger: new SessionLedger({ kv: ledgerKv, podId: newLedgerPodId() }),
|
|
491
|
-
deliverTurn: (
|
|
492
|
-
this.send(
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
502
|
+
deliverTurn: (envelope) =>
|
|
503
|
+
this.send(envelope.channelId, envelope.message, {
|
|
504
|
+
continuationToken: envelope.continuationToken,
|
|
505
|
+
auth: envelope.auth,
|
|
506
|
+
...(envelope.title === undefined ? {} : { title: envelope.title }),
|
|
507
|
+
...(envelope.state === undefined ? {} : { state: envelope.state }),
|
|
508
|
+
...(envelope.mode === undefined ? {} : { mode: envelope.mode }),
|
|
509
|
+
...(envelope.purpose === undefined
|
|
510
|
+
? {}
|
|
511
|
+
: { purpose: envelope.purpose }),
|
|
512
|
+
// Isolation overrides ride the envelope: without them the
|
|
513
|
+
// drained turn would run local with the default harness cwd
|
|
514
|
+
// instead of the cloud VM / channel worktree the sender bound.
|
|
515
|
+
...(envelope.workspaceDir === undefined
|
|
516
|
+
? {}
|
|
517
|
+
: { workspaceDir: envelope.workspaceDir }),
|
|
518
|
+
...(envelope.cloud === undefined ? {} : { cloud: envelope.cloud }),
|
|
519
|
+
fromLedgerPump: true,
|
|
496
520
|
}),
|
|
521
|
+
findSession: async (channelId, continuationToken) => {
|
|
522
|
+
let record = await this.sessions.findByContinuation(
|
|
523
|
+
channelId,
|
|
524
|
+
continuationToken
|
|
525
|
+
);
|
|
526
|
+
if (record === undefined) {
|
|
527
|
+
record = await this.restoreSessionByContinuation(
|
|
528
|
+
channelId,
|
|
529
|
+
continuationToken
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
return record === undefined
|
|
533
|
+
? undefined
|
|
534
|
+
: this.channelSession(record.sessionId, record, false);
|
|
535
|
+
},
|
|
497
536
|
logger: this.logger,
|
|
498
537
|
});
|
|
499
538
|
}
|
|
@@ -511,13 +550,14 @@ export class SessionEngine {
|
|
|
511
550
|
*
|
|
512
551
|
* An authored backend without `cas` fails fast: silently falling back
|
|
513
552
|
* to local files would fork coordination away from the deployment's
|
|
514
|
-
* durable store. The check demands
|
|
515
|
-
*
|
|
553
|
+
* durable store. The check demands the full surface the inbox uses —
|
|
554
|
+
* the CAS trio plus `get`/`put`/`delete` for mailbox payload blobs and
|
|
555
|
+
* `listKeys` for the janitor scan and the orphan sweep.
|
|
516
556
|
*
|
|
517
557
|
* Whatever store wins, the returned handle is scoped here — once —
|
|
518
558
|
* under `agentkit/v1/<agent>/durable-session/`. The ledger speaks bare
|
|
519
|
-
* `session/…` / `ledger/…` keys; namespacing is this
|
|
520
|
-
* exactly like `host.kv`'s agent scoping.
|
|
559
|
+
* `session/…` / `ledger/…` / `mailbox/…` keys; namespacing is this
|
|
560
|
+
* boundary's job, exactly like `host.kv`'s agent scoping.
|
|
521
561
|
*/
|
|
522
562
|
private resolveDurableSessionStore(
|
|
523
563
|
options: SessionEngineOptions
|
|
@@ -527,7 +567,7 @@ export class SessionEngine {
|
|
|
527
567
|
if (explicit !== undefined) {
|
|
528
568
|
if (!supportsSessionLedgerKv(explicit)) {
|
|
529
569
|
throw new Error(
|
|
530
|
-
`Agent "${options.project.name}" declares serving: "ledger", but the configured durableSessionStore is not CAS-capable (getWithVersion / putIfAbsent / putIfVersion). Provide a store implementing
|
|
570
|
+
`Agent "${options.project.name}" declares serving: "ledger", but the configured durableSessionStore is not CAS-capable (getWithVersion / putIfAbsent / putIfVersion / listKeys). Provide a store implementing HostKvCasApi (see @cursor/july/kv).`
|
|
531
571
|
);
|
|
532
572
|
}
|
|
533
573
|
return namespacedKv(explicit, namespace);
|
|
@@ -574,7 +614,7 @@ export class SessionEngine {
|
|
|
574
614
|
this.cloudServerToolsConnection = connection;
|
|
575
615
|
}
|
|
576
616
|
|
|
577
|
-
/** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / Slack nudges). */
|
|
617
|
+
/** Shared host services (MCP / GitHub / Slack / reminders / evals / kv / files / otel / Slack nudges). */
|
|
578
618
|
get host(): HostContext {
|
|
579
619
|
return {
|
|
580
620
|
mcp: this.mcpHost,
|
|
@@ -584,6 +624,7 @@ export class SessionEngine {
|
|
|
584
624
|
evals: this.evalsApi,
|
|
585
625
|
kv: this.kvApi,
|
|
586
626
|
files: this.filesApi,
|
|
627
|
+
otel: this.otelHost,
|
|
587
628
|
slackNudges: this.slackNudgeHostApi(),
|
|
588
629
|
};
|
|
589
630
|
}
|
|
@@ -598,7 +639,11 @@ export class SessionEngine {
|
|
|
598
639
|
const host = this.host;
|
|
599
640
|
return sessionId === undefined
|
|
600
641
|
? host
|
|
601
|
-
: {
|
|
642
|
+
: {
|
|
643
|
+
...host,
|
|
644
|
+
files: this.filesApi.forSession(sessionId),
|
|
645
|
+
otel: this.otelHost.forSession(sessionId),
|
|
646
|
+
};
|
|
602
647
|
}
|
|
603
648
|
|
|
604
649
|
/** Unbound `ctx.artifacts` facade (channel handlers, scratch tool calls). */
|
|
@@ -714,10 +759,10 @@ export class SessionEngine {
|
|
|
714
759
|
message: string,
|
|
715
760
|
options: EngineSendOptions = {}
|
|
716
761
|
): Promise<ChannelSession> {
|
|
717
|
-
// Ledger-mode agents
|
|
718
|
-
//
|
|
719
|
-
// resident path.
|
|
720
|
-
if (this.ledgerServing !== undefined && options.
|
|
762
|
+
// Ledger-mode agents park the event durably and coordinate through
|
|
763
|
+
// the session lease before any turn runs; only the pump's own
|
|
764
|
+
// lease-holding deliveries fall through to the resident path.
|
|
765
|
+
if (this.ledgerServing !== undefined && options.fromLedgerPump !== true) {
|
|
721
766
|
return this.ledgerServing.deliver(channelId, message, options);
|
|
722
767
|
}
|
|
723
768
|
const auth = options.auth ?? null;
|
|
@@ -791,13 +836,20 @@ export class SessionEngine {
|
|
|
791
836
|
),
|
|
792
837
|
}));
|
|
793
838
|
}
|
|
839
|
+
const since = (await this.logs.get(record.sessionId)).count;
|
|
794
840
|
const coalesced = await this.startTurn(record.sessionId, message, {
|
|
795
841
|
admission: options.admission ?? "preempt",
|
|
796
842
|
...(options.coalesceSourceTs === undefined
|
|
797
843
|
? {}
|
|
798
844
|
: { sourceTs: options.coalesceSourceTs }),
|
|
799
845
|
});
|
|
800
|
-
return this.channelSession(
|
|
846
|
+
return this.channelSession(
|
|
847
|
+
record.sessionId,
|
|
848
|
+
record,
|
|
849
|
+
false,
|
|
850
|
+
coalesced,
|
|
851
|
+
since
|
|
852
|
+
);
|
|
801
853
|
}
|
|
802
854
|
|
|
803
855
|
const now = new Date().toISOString();
|
|
@@ -823,18 +875,26 @@ export class SessionEngine {
|
|
|
823
875
|
};
|
|
824
876
|
await this.sessions.save(record);
|
|
825
877
|
this.storage?.sessionRecord(record);
|
|
878
|
+
// Ledger serving: another pod can follow up mid-turn, so the session
|
|
879
|
+
// identity (record + continuation index) must hit the shared store
|
|
880
|
+
// before we return a session id. Default turnEnd flush is too late.
|
|
881
|
+
if (this.ledgerServing !== undefined && this.storage !== undefined) {
|
|
882
|
+
this.storage.flushSession(record.sessionId);
|
|
883
|
+
await this.storage.whenIdle();
|
|
884
|
+
}
|
|
826
885
|
await this.appendEvent(record.sessionId, {
|
|
827
886
|
type: "session.started",
|
|
828
887
|
data: { channelId },
|
|
829
888
|
});
|
|
830
889
|
record = await this.enrollABs(record);
|
|
890
|
+
const since = (await this.logs.get(record.sessionId)).count;
|
|
831
891
|
await this.startTurn(record.sessionId, message, {
|
|
832
892
|
admission: options.admission ?? "preempt",
|
|
833
893
|
...(options.coalesceSourceTs === undefined
|
|
834
894
|
? {}
|
|
835
895
|
: { sourceTs: options.coalesceSourceTs }),
|
|
836
896
|
});
|
|
837
|
-
return this.channelSession(record.sessionId, record, true, false);
|
|
897
|
+
return this.channelSession(record.sessionId, record, true, false, since);
|
|
838
898
|
}
|
|
839
899
|
|
|
840
900
|
/** Whether a continuation token already maps to a session on this channel. */
|
|
@@ -1359,6 +1419,9 @@ export class SessionEngine {
|
|
|
1359
1419
|
workspaceDir,
|
|
1360
1420
|
runnerStateDir: join(this.stateRoot, "runner"),
|
|
1361
1421
|
sandbox: agent.local?.sandbox === true,
|
|
1422
|
+
...(agent.allowedTools === undefined
|
|
1423
|
+
? {}
|
|
1424
|
+
: { tools: agent.allowedTools }),
|
|
1362
1425
|
mcpServers: this.buildMcpServers("local"),
|
|
1363
1426
|
subagents: buildRunnerSubagents(agent),
|
|
1364
1427
|
...(agent.model === undefined ? {} : { model: agent.model }),
|
|
@@ -1614,6 +1677,15 @@ export class SessionEngine {
|
|
|
1614
1677
|
agentRuntime: agent.runtime,
|
|
1615
1678
|
cloudOverride: record.cloudOverride,
|
|
1616
1679
|
});
|
|
1680
|
+
// Fail closed: the SDK cannot enforce the built-in tool allowlist on
|
|
1681
|
+
// cloud agents yet, and a restriction that silently does not apply is
|
|
1682
|
+
// worse than a refused turn. Cloud-runtime agents already fail
|
|
1683
|
+
// discovery; this guards hybrid agents' per-send cloud sessions.
|
|
1684
|
+
if (runtime === "cloud" && agent.allowedTools !== undefined) {
|
|
1685
|
+
throw new Error(
|
|
1686
|
+
`Agent "${agent.name}" declares a built-in tool allowlist (tools), which cannot be enforced on cloud turns yet. Refusing the cloud session rather than running it unrestricted.`
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1617
1689
|
const mergedCloud =
|
|
1618
1690
|
runtime === "cloud"
|
|
1619
1691
|
? mergeCloudOptions({
|
|
@@ -1668,6 +1740,9 @@ export class SessionEngine {
|
|
|
1668
1740
|
: { sdkAgentId: record.sdkAgentId }),
|
|
1669
1741
|
workspaceDir: this.harnessCwd(record),
|
|
1670
1742
|
sandbox: this.project.agent.local?.sandbox === true,
|
|
1743
|
+
...(runtime === "local" && agent.allowedTools !== undefined
|
|
1744
|
+
? { tools: agent.allowedTools }
|
|
1745
|
+
: {}),
|
|
1671
1746
|
runnerStateDir: join(this.stateRoot, "runner"),
|
|
1672
1747
|
prompt: turnPrompt,
|
|
1673
1748
|
isFirstTurn,
|
|
@@ -1996,6 +2071,7 @@ export class SessionEngine {
|
|
|
1996
2071
|
}),
|
|
1997
2072
|
getSession: (channelId, sessionId) =>
|
|
1998
2073
|
this.getChannelSession(channelId, sessionId),
|
|
2074
|
+
emit: args.emit,
|
|
1999
2075
|
};
|
|
2000
2076
|
try {
|
|
2001
2077
|
const needsGate = await evaluateNeedsApproval(
|
|
@@ -2284,6 +2360,16 @@ export class SessionEngine {
|
|
|
2284
2360
|
this.logger(`[session] tool ${toolName} start (direct ${callId})`);
|
|
2285
2361
|
let result: unknown;
|
|
2286
2362
|
let isError: boolean;
|
|
2363
|
+
const live = record;
|
|
2364
|
+
const emit =
|
|
2365
|
+
live === undefined
|
|
2366
|
+
? undefined
|
|
2367
|
+
: (payload: SessionEventPayload) => {
|
|
2368
|
+
void this.appendEvent(live.sessionId, {
|
|
2369
|
+
...payload,
|
|
2370
|
+
turnId: callId,
|
|
2371
|
+
}).catch(() => {});
|
|
2372
|
+
};
|
|
2287
2373
|
try {
|
|
2288
2374
|
result = normalizeToolResult(
|
|
2289
2375
|
await execute(validation.value as never, {
|
|
@@ -2310,6 +2396,7 @@ export class SessionEngine {
|
|
|
2310
2396
|
}),
|
|
2311
2397
|
getSession: (channelId, sessionId) =>
|
|
2312
2398
|
this.getChannelSession(channelId, sessionId),
|
|
2399
|
+
emit,
|
|
2313
2400
|
})
|
|
2314
2401
|
);
|
|
2315
2402
|
isError = isErrorEnvelope(result);
|
|
@@ -3009,7 +3096,8 @@ export class SessionEngine {
|
|
|
3009
3096
|
sessionId: string,
|
|
3010
3097
|
record: SessionRecord,
|
|
3011
3098
|
isNew: boolean,
|
|
3012
|
-
coalesced = false
|
|
3099
|
+
coalesced = false,
|
|
3100
|
+
since = 0
|
|
3013
3101
|
): ChannelSession {
|
|
3014
3102
|
return {
|
|
3015
3103
|
id: sessionId,
|
|
@@ -3018,7 +3106,7 @@ export class SessionEngine {
|
|
|
3018
3106
|
...(coalesced ? { coalesced: true } : {}),
|
|
3019
3107
|
info: this.sessionInfo(record),
|
|
3020
3108
|
getEventStream: (options?: { startIndex?: number }) =>
|
|
3021
|
-
this.eventStream(sessionId, options?.startIndex ??
|
|
3109
|
+
this.eventStream(sessionId, options?.startIndex ?? since),
|
|
3022
3110
|
waitForCompletion: () => this.waitForCompletion(sessionId),
|
|
3023
3111
|
setContinuationToken: (token: string | null) =>
|
|
3024
3112
|
this.setContinuationToken(sessionId, token),
|
package/src/kv/memory-kv.ts
CHANGED
|
@@ -86,6 +86,14 @@ export function createMemoryKv(): HostKvCasApi {
|
|
|
86
86
|
* fixtures, an off-hosting no-op sink). Non-durable and process-local by
|
|
87
87
|
* construction; a real deployment backend should implement CAS against
|
|
88
88
|
* its own store instead.
|
|
89
|
+
*
|
|
90
|
+
* **Never pair this with `serving: "ledger"` on a real backend.** The
|
|
91
|
+
* ledger derives its coordination store from the whole storage config,
|
|
92
|
+
* so a durable plain backend with a memory `cas` group silently voids
|
|
93
|
+
* ack-after-durable: leases, heads, and pending indexes live only in
|
|
94
|
+
* this process and vanish on restart while payload blobs go through the
|
|
95
|
+
* plain methods. Ledger state must live in the same durable store as
|
|
96
|
+
* the blobs.
|
|
89
97
|
*/
|
|
90
98
|
export function memoryCasTable(): StorageCasTable {
|
|
91
99
|
const kv = createMemoryKv();
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ledger janitor: lazy crash recovery has a hole — a session with
|
|
3
|
+
* pending events and no future events would wait forever — so serving runs
|
|
4
|
+
* this loop. It scans `ledger/` by prefix, finds pending-with-expired-lease
|
|
5
|
+
* sessions, claims them, and hands each claim to the execution callback.
|
|
6
|
+
* Self-host runs the same loop; it is part of the library, not the control
|
|
7
|
+
* plane.
|
|
8
|
+
*
|
|
9
|
+
* Nothing starts on construction: call `start()` for the interval loop or
|
|
10
|
+
* `runOnce()` for an explicit pass (what unit tests use).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ClaimResult,
|
|
15
|
+
LedgerLease,
|
|
16
|
+
SessionLedger,
|
|
17
|
+
} from "./session-ledger.js";
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_JANITOR_INTERVAL_MS = 15_000;
|
|
20
|
+
|
|
21
|
+
export interface JanitorClaimedSession {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
lease: LedgerLease;
|
|
24
|
+
head: number;
|
|
25
|
+
pending: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface JanitorRunResult {
|
|
29
|
+
/** Ledgers scanned this pass. */
|
|
30
|
+
scanned: number;
|
|
31
|
+
/** Sessions this pass claimed and handed to the callback. */
|
|
32
|
+
claimed: JanitorClaimedSession[];
|
|
33
|
+
/** Orphan blob ids deleted (only when the sweep is enabled). */
|
|
34
|
+
orphansDeleted: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LedgerJanitorOptions {
|
|
38
|
+
ledger: SessionLedger;
|
|
39
|
+
/**
|
|
40
|
+
* Executes a recovered session: drain, run the turn, commit. Awaited so
|
|
41
|
+
* one pass never claims more sessions than it can serve.
|
|
42
|
+
*/
|
|
43
|
+
onSession: (session: JanitorClaimedSession) => Promise<void> | void;
|
|
44
|
+
/** Scan cadence for `start()` (default {@link DEFAULT_JANITOR_INTERVAL_MS}). */
|
|
45
|
+
intervalMs?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Also sweep unreferenced mailbox blobs each pass. Off by default —
|
|
48
|
+
* enable with `{ olderThanMs }` (see `SessionLedger.sweepOrphans`).
|
|
49
|
+
*/
|
|
50
|
+
orphanSweep?: { olderThanMs?: number };
|
|
51
|
+
/** Called with per-session or sweep failures; the pass continues. */
|
|
52
|
+
onError?: (error: unknown, sessionId?: string) => void;
|
|
53
|
+
/** Time source for lease-expiry checks (default `Date.now`). */
|
|
54
|
+
clock?: () => number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LedgerJanitor {
|
|
58
|
+
start(): void;
|
|
59
|
+
/**
|
|
60
|
+
* Clear the interval AND await any in-flight pass: a running pass can
|
|
61
|
+
* claim a session and spawn a turn, so shutdown must not proceed while
|
|
62
|
+
* one is mid-scan.
|
|
63
|
+
*/
|
|
64
|
+
stop(): Promise<void>;
|
|
65
|
+
runOnce(): Promise<JanitorRunResult>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createLedgerJanitor(
|
|
69
|
+
options: LedgerJanitorOptions
|
|
70
|
+
): LedgerJanitor {
|
|
71
|
+
const intervalMs = options.intervalMs ?? DEFAULT_JANITOR_INTERVAL_MS;
|
|
72
|
+
const clock = options.clock ?? Date.now;
|
|
73
|
+
const reportError =
|
|
74
|
+
options.onError ??
|
|
75
|
+
((): void => {
|
|
76
|
+
// Recovery is best-effort per pass; an unobserved failure is retried
|
|
77
|
+
// on the next scan.
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
81
|
+
let running = false;
|
|
82
|
+
/** The settled-shape promise of the pass currently scanning, if any. */
|
|
83
|
+
let inFlight: Promise<void> | undefined;
|
|
84
|
+
|
|
85
|
+
const runPass = async (): Promise<JanitorRunResult> => {
|
|
86
|
+
// An unparseable ledger record is reported and skipped: it degrades
|
|
87
|
+
// that one session's recovery, never the whole agent's pass.
|
|
88
|
+
const ledgers = await options.ledger.listLedgers((key, error) => {
|
|
89
|
+
reportError(
|
|
90
|
+
new Error(
|
|
91
|
+
`janitor scan: skipping unparseable ledger record at ${key}: ${String(
|
|
92
|
+
error instanceof Error ? error.message : error
|
|
93
|
+
)}`
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
const claimed: JanitorClaimedSession[] = [];
|
|
98
|
+
for (const snapshot of ledgers) {
|
|
99
|
+
if (snapshot.pending.length === 0) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (snapshot.lease !== null && snapshot.lease.expiresAt > clock()) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let claim: ClaimResult;
|
|
106
|
+
try {
|
|
107
|
+
claim = await options.ledger.claim(snapshot.sessionId);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
reportError(error, snapshot.sessionId);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!claim.claimed) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const session: JanitorClaimedSession = {
|
|
116
|
+
sessionId: snapshot.sessionId,
|
|
117
|
+
lease: claim.lease,
|
|
118
|
+
head: claim.head,
|
|
119
|
+
pending: claim.pending,
|
|
120
|
+
};
|
|
121
|
+
claimed.push(session);
|
|
122
|
+
try {
|
|
123
|
+
await options.onSession(session);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
reportError(error, snapshot.sessionId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let orphansDeleted: string[] = [];
|
|
130
|
+
if (options.orphanSweep !== undefined) {
|
|
131
|
+
try {
|
|
132
|
+
const swept = await options.ledger.sweepOrphans({
|
|
133
|
+
olderThanMs: options.orphanSweep.olderThanMs,
|
|
134
|
+
});
|
|
135
|
+
orphansDeleted = swept.deleted;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
reportError(error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { scanned: ledgers.length, claimed, orphansDeleted };
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const runOnce = (): Promise<JanitorRunResult> => {
|
|
144
|
+
const pass = runPass();
|
|
145
|
+
const settled = pass.then(
|
|
146
|
+
() => undefined,
|
|
147
|
+
() => undefined
|
|
148
|
+
);
|
|
149
|
+
inFlight = settled;
|
|
150
|
+
void settled.finally(() => {
|
|
151
|
+
if (inFlight === settled) {
|
|
152
|
+
inFlight = undefined;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return pass;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const tick = async (): Promise<void> => {
|
|
159
|
+
// Skip a tick rather than overlap passes when a scan outlasts the
|
|
160
|
+
// cadence — overlapping claims would just fence each other.
|
|
161
|
+
if (running) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
running = true;
|
|
165
|
+
try {
|
|
166
|
+
await runOnce();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
reportError(error);
|
|
169
|
+
} finally {
|
|
170
|
+
running = false;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
start(): void {
|
|
176
|
+
if (timer !== undefined) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
timer = setInterval(() => {
|
|
180
|
+
void tick();
|
|
181
|
+
}, intervalMs);
|
|
182
|
+
timer.unref?.();
|
|
183
|
+
},
|
|
184
|
+
async stop(): Promise<void> {
|
|
185
|
+
if (timer !== undefined) {
|
|
186
|
+
clearInterval(timer);
|
|
187
|
+
timer = undefined;
|
|
188
|
+
}
|
|
189
|
+
await inFlight;
|
|
190
|
+
},
|
|
191
|
+
runOnce,
|
|
192
|
+
};
|
|
193
|
+
}
|