@cursor/july 0.1.62 → 0.1.73
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 +4 -5
- package/README.md +23 -11
- package/dist/bin/agent-serve.js +11 -2
- package/dist/channels/github/api.d.ts +2 -0
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +2 -1
- package/dist/channels/github/binding.d.ts +7 -0
- package/dist/channels/github/binding.d.ts.map +1 -1
- package/dist/channels/github/binding.js +4 -0
- package/dist/channels/github/checks.d.ts +99 -0
- package/dist/channels/github/checks.d.ts.map +1 -0
- package/dist/channels/github/checks.js +108 -0
- package/dist/channels/github/cursor-account.d.ts +4 -4
- package/dist/channels/github/cursor-account.d.ts.map +1 -1
- package/dist/channels/github/cursor-account.js +7 -5
- package/dist/channels/github/index.d.ts +1 -0
- package/dist/channels/github/index.d.ts.map +1 -1
- package/dist/channels/github/index.js +1 -0
- package/dist/channels/github/types.d.ts +8 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.d.ts +2 -1
- package/dist/channels/slack/channel-watch.d.ts.map +1 -1
- package/dist/channels/slack/channel-watch.js +7 -3
- package/dist/channels/slack/dispatch.d.ts +9 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +41 -21
- package/dist/channels/slack/manifest.d.ts +2 -2
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +14 -11
- package/dist/channels/slack/nudge-store.d.ts +16 -0
- package/dist/channels/slack/nudge-store.d.ts.map +1 -1
- package/dist/channels/slack/nudge-store.js +53 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +4 -1
- package/dist/channels/slack/types.d.ts +24 -7
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/connections.d.ts +17 -5
- package/dist/connections.d.ts.map +1 -1
- package/dist/connections.js +17 -5
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.B-biogkN.js → app.CdbcFD8T.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DQad--_z.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.DZqKtFZS.js → VPLocalSearchBox.D_NKImM7.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BwC5zP2r.js → theme.CMvz7cu9.js} +2 -2
- package/dist/docs/assets/{concepts.md.CqOsxbMU.js → concepts.md.BXAm6G-C.js} +1 -1
- package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.js → guides_mcp-oauth.md.CzEB6RaG.js} +1 -1
- package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.js → reference_agent-config.md.BZnEKfEc.js} +3 -4
- package/dist/docs/assets/{reference_agent-config.md.DHkkvcEZ.lean.js → reference_agent-config.md.BZnEKfEc.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.CSQiNzre.js → reference_connections.md.Cu3N-S3Q.js} +26 -9
- package/dist/docs/assets/reference_connections.md.Cu3N-S3Q.lean.js +1 -0
- package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.js → reference_sessions.md.B0DdlM-K.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.kJfJAP2m.js → reference_tools.md.DTg_kEsx.js} +2 -2
- package/dist/docs/assets/{reference_tools.md.kJfJAP2m.lean.js → reference_tools.md.DTg_kEsx.lean.js} +1 -1
- package/dist/docs/assets/troubleshooting.md.DQ-hYuZT.js +1 -0
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +5 -5
- 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/convert-automation.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +5 -5
- package/dist/docs/guides/opentelemetry.html +3 -3
- 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 +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +5 -6
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +3 -3
- package/dist/docs/reference/connections.html +28 -11
- package/dist/docs/reference/hooks.html +3 -3
- 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 +5 -5
- 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 +3 -3
- package/dist/docs/templates/agentic-owners.html +3 -3
- package/dist/docs/templates/demo.html +3 -3
- package/dist/docs/templates/pr-autofixer.html +3 -3
- package/dist/docs/templates/security-reviewer.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/files-backends/agent-store-presigned-url.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/advertise-tools.d.ts +79 -0
- package/dist/internal/advertise-tools.d.ts.map +1 -0
- package/dist/internal/advertise-tools.js +185 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +7 -1
- package/dist/internal/cli-manifest.d.ts +19 -0
- package/dist/internal/cli-manifest.d.ts.map +1 -0
- package/dist/internal/cli-manifest.js +49 -0
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +9 -1
- package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -1
- package/dist/internal/convert-automation/convert-workflow.js +2 -10
- package/dist/internal/cursor/account-mcp.d.ts +29 -3
- package/dist/internal/cursor/account-mcp.d.ts.map +1 -1
- package/dist/internal/cursor/account-mcp.js +147 -67
- package/dist/internal/cursor/backend-client.d.ts +11 -6
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +32 -10
- package/dist/internal/cursor-account-mcp-auth.d.ts +11 -0
- package/dist/internal/cursor-account-mcp-auth.d.ts.map +1 -1
- package/dist/internal/cursor-account-mcp-auth.js +69 -0
- package/dist/internal/cursor-agent-template.d.ts +9 -0
- package/dist/internal/cursor-agent-template.d.ts.map +1 -0
- package/dist/internal/cursor-agent-template.js +37 -0
- package/dist/internal/deploy-client.d.ts +1 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +18 -2
- package/dist/internal/deploy-manifest.d.ts +76 -0
- package/dist/internal/deploy-manifest.d.ts.map +1 -0
- package/dist/internal/deploy-manifest.js +117 -0
- package/dist/internal/deployment-scm-event-stream.d.ts +116 -0
- package/dist/internal/deployment-scm-event-stream.d.ts.map +1 -0
- package/dist/internal/deployment-scm-event-stream.js +474 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +111 -10
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +12 -123
- package/dist/internal/init-scaffold.d.ts +8 -0
- package/dist/internal/init-scaffold.d.ts.map +1 -0
- package/dist/internal/init-scaffold.js +168 -0
- package/dist/internal/inline-stdio-mcp.d.ts +2 -0
- package/dist/internal/inline-stdio-mcp.d.ts.map +1 -1
- package/dist/internal/inline-stdio-mcp.js +10 -8
- package/dist/internal/json-schema.d.ts +7 -0
- package/dist/internal/json-schema.d.ts.map +1 -1
- package/dist/internal/json-schema.js +18 -3
- package/dist/internal/mcp-endpoint.js +7 -7
- package/dist/internal/mcp-host.d.ts +18 -0
- package/dist/internal/mcp-host.d.ts.map +1 -1
- package/dist/internal/mcp-host.js +43 -5
- package/dist/internal/mcp-oauth.d.ts +22 -0
- package/dist/internal/mcp-oauth.d.ts.map +1 -1
- package/dist/internal/mcp-oauth.js +40 -1
- package/dist/internal/package-inline-stdio.d.ts.map +1 -1
- package/dist/internal/package-inline-stdio.js +5 -5
- package/dist/internal/review-comments.d.ts +97 -0
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +151 -0
- package/dist/internal/sanitize-mcp-tool-name.d.ts +20 -0
- package/dist/internal/sanitize-mcp-tool-name.d.ts.map +1 -0
- package/dist/internal/sanitize-mcp-tool-name.js +30 -0
- 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 +36 -13
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +18 -0
- package/dist/internal/session-engine.d.ts +66 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +428 -110
- package/dist/internal/shallow-clone.d.ts +170 -0
- package/dist/internal/shallow-clone.d.ts.map +1 -0
- package/dist/internal/shallow-clone.js +412 -0
- package/dist/internal/slack-provision-client.d.ts +2 -0
- package/dist/internal/slack-provision-client.d.ts.map +1 -1
- package/dist/internal/slack-provision-client.js +1 -0
- package/dist/internal/terminal-style.d.ts +18 -0
- package/dist/internal/terminal-style.d.ts.map +1 -1
- package/dist/internal/terminal-style.js +46 -1
- package/dist/internal/turn-lease-store.d.ts +44 -0
- package/dist/internal/turn-lease-store.d.ts.map +1 -0
- package/dist/internal/turn-lease-store.js +139 -0
- package/dist/internal/workspace.d.ts +14 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +33 -0
- package/dist/playground/assets/index-77uhu5Kr.css +1 -0
- package/dist/playground/assets/index-D4buox4m.js +85 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-protocol.d.ts +122 -0
- package/dist/storage-protocol.d.ts.map +1 -0
- package/dist/storage-protocol.js +173 -0
- package/dist/storage.d.ts +2 -72
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2 -82
- package/dist/tools.d.ts +7 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/types.d.ts +141 -6
- package/dist/types.d.ts.map +1 -1
- package/docs/concepts.md +4 -4
- package/docs/guides/mcp-oauth.md +3 -1
- package/docs/reference/agent-config.md +11 -12
- package/docs/reference/connections.md +108 -10
- package/docs/reference/sessions.md +2 -2
- package/docs/reference/tools.md +14 -0
- package/docs/troubleshooting.md +3 -2
- package/package.json +7 -2
- package/skills/create-agent/SKILL.md +12 -9
- package/skills/debug/SKILL.md +2 -1
- package/skills/framework-map/SKILL.md +6 -6
- package/skills/mcp-auth/SKILL.md +8 -6
- package/src/bin/agent-serve.ts +12 -2
- package/src/channels/github/api.ts +6 -1
- package/src/channels/github/binding.ts +21 -0
- package/src/channels/github/checks.ts +179 -0
- package/src/channels/github/cursor-account.ts +9 -6
- package/src/channels/github/index.ts +1 -0
- package/src/channels/github/types.ts +8 -0
- package/src/channels/slack/channel-watch.ts +6 -3
- package/src/channels/slack/dispatch.ts +40 -3
- package/src/channels/slack/manifest.ts +9 -10
- package/src/channels/slack/nudge-store.ts +56 -0
- package/src/channels/slack/slack-channel.ts +3 -0
- package/src/channels/slack/types.ts +24 -7
- package/src/connections.ts +17 -5
- package/src/index.ts +5 -0
- package/src/internal/advertise-tools.ts +278 -0
- package/src/internal/cli-deploy.ts +11 -1
- package/src/internal/cli-manifest.ts +63 -0
- package/src/internal/cli-slack.ts +10 -1
- package/src/internal/convert-automation/convert-workflow.ts +2 -11
- package/src/internal/cursor/account-mcp.ts +208 -73
- package/src/internal/cursor/backend-client.ts +45 -11
- package/src/internal/cursor-account-mcp-auth.ts +83 -0
- package/src/internal/cursor-agent-template.ts +46 -0
- package/src/internal/deploy-client.ts +25 -6
- package/src/internal/deploy-manifest.ts +161 -0
- package/src/internal/deployment-scm-event-stream.ts +593 -0
- package/src/internal/discovery.ts +132 -3
- package/src/internal/init-project.ts +18 -153
- package/src/internal/init-scaffold.ts +176 -0
- package/src/internal/inline-stdio-mcp.ts +13 -0
- package/src/internal/json-schema.ts +24 -3
- package/src/internal/mcp-endpoint.ts +6 -0
- package/src/internal/mcp-host.ts +70 -9
- package/src/internal/mcp-oauth.ts +55 -1
- package/src/internal/package-inline-stdio.ts +5 -1
- package/src/internal/review-comments.ts +245 -0
- package/src/internal/sanitize-mcp-tool-name.ts +33 -0
- package/src/internal/sdk-runner.ts +50 -11
- package/src/internal/server.ts +19 -0
- package/src/internal/session-engine.ts +392 -19
- package/src/internal/shallow-clone.ts +595 -0
- package/src/internal/slack-provision-client.ts +3 -0
- package/src/internal/terminal-style.ts +67 -1
- package/src/internal/turn-lease-store.ts +147 -0
- package/src/internal/workspace.ts +33 -0
- package/src/storage-protocol.ts +247 -0
- package/src/storage.ts +16 -110
- package/src/tools.ts +7 -0
- package/src/types.ts +149 -7
- package/templates/agentic-owners/agent/agent.ts +3 -0
- package/templates/demo/agent/agent.ts +3 -0
- package/templates/pr-autofixer/agent/lib/drive-schemas.ts +4 -1
- package/templates/security-reviewer/agent/agent.ts +3 -0
- package/templates/triage/agent/agent.ts +3 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.5ozYNf74.js +0 -1
- package/dist/docs/assets/reference_connections.md.CSQiNzre.lean.js +0 -1
- package/dist/docs/assets/troubleshooting.md.CuVztCKs.js +0 -1
- package/dist/playground/assets/index-B4rcOgeP.css +0 -1
- package/dist/playground/assets/index-DXcY42Jv.js +0 -85
- /package/dist/docs/assets/{concepts.md.CqOsxbMU.lean.js → concepts.md.BXAm6G-C.lean.js} +0 -0
- /package/dist/docs/assets/{guides_mcp-oauth.md.C7G7IykG.lean.js → guides_mcp-oauth.md.CzEB6RaG.lean.js} +0 -0
- /package/dist/docs/assets/{reference_sessions.md.DBVFi2Sx.lean.js → reference_sessions.md.B0DdlM-K.lean.js} +0 -0
- /package/dist/docs/assets/{troubleshooting.md.CuVztCKs.lean.js → troubleshooting.md.DQ-hYuZT.lean.js} +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable per-session turn ownership: `activeTurns` is in-memory, so servers
|
|
3
|
+
* sharing a state root would otherwise both run the same session.
|
|
4
|
+
*
|
|
5
|
+
* Exclusion, staleness and liveness refresh come from `proper-lockfile`, which
|
|
6
|
+
* the monorepo already uses for the git cache. Hand-rolling them means
|
|
7
|
+
* inferring that a process died — timeouts, heartbeats, and a takeover path
|
|
8
|
+
* that has to stay atomic while removing another owner's claim. That is where
|
|
9
|
+
* the subtle races live, and none of it is specific to turns.
|
|
10
|
+
*
|
|
11
|
+
* Liveness is lock age, not pid: containers have separate PID namespaces.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdir } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import * as lockfile from "proper-lockfile";
|
|
17
|
+
|
|
18
|
+
/** A held turn lease. Releasing is idempotent. */
|
|
19
|
+
export interface TurnLease {
|
|
20
|
+
release(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Reject path traversal; session ids are opaque strings from the caller. */
|
|
24
|
+
function safeKey(sessionId: string): string {
|
|
25
|
+
const sanitized = sessionId
|
|
26
|
+
.trim()
|
|
27
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "_")
|
|
28
|
+
.replace(/^\.+/, "")
|
|
29
|
+
.replace(/\.+/g, "_")
|
|
30
|
+
.slice(0, 180);
|
|
31
|
+
return sanitized === "" ? "_empty" : sanitized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface HeldLock {
|
|
35
|
+
release: () => Promise<void>;
|
|
36
|
+
/** The coalesce drain nests turns, so the same session is acquired twice. */
|
|
37
|
+
refs: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class TurnLeaseStore {
|
|
41
|
+
private readonly dir: string;
|
|
42
|
+
private readonly staleMs: number;
|
|
43
|
+
/** Locks this process holds. Their release closures cannot live on disk. */
|
|
44
|
+
private readonly held = new Map<string, HeldLock>();
|
|
45
|
+
|
|
46
|
+
constructor(dir: string, options: { ttlMs?: number } = {}) {
|
|
47
|
+
this.dir = dir;
|
|
48
|
+
// Several refreshes, so a slow owner is not mistaken for a dead one.
|
|
49
|
+
this.staleMs = options.ttlMs ?? 30_000;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private path(sessionId: string): string {
|
|
53
|
+
return join(this.dir, `${safeKey(sessionId)}.lease`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Undefined when another process holds this session and is still refreshing.
|
|
58
|
+
*
|
|
59
|
+
* `onCompromised` fires when the lock is lost mid-turn — the owner stalled
|
|
60
|
+
* past the stale window and a successor took over. No lease scheme can stop
|
|
61
|
+
* that; the callback is how the displaced turn learns to stand down instead
|
|
62
|
+
* of answering alongside its successor.
|
|
63
|
+
*/
|
|
64
|
+
async acquire(
|
|
65
|
+
sessionId: string,
|
|
66
|
+
onCompromised?: () => void
|
|
67
|
+
): Promise<TurnLease | undefined> {
|
|
68
|
+
const existing = this.held.get(sessionId);
|
|
69
|
+
if (existing !== undefined) {
|
|
70
|
+
existing.refs += 1;
|
|
71
|
+
return this.handle(sessionId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await mkdir(this.dir, { recursive: true });
|
|
75
|
+
let release: () => Promise<void>;
|
|
76
|
+
try {
|
|
77
|
+
release = await lockfile.lock(this.path(sessionId), {
|
|
78
|
+
realpath: false,
|
|
79
|
+
retries: 0,
|
|
80
|
+
stale: this.staleMs,
|
|
81
|
+
// How often the library refreshes the lock's mtime while we hold it.
|
|
82
|
+
update: 5_000,
|
|
83
|
+
onCompromised: () => {
|
|
84
|
+
this.held.delete(sessionId);
|
|
85
|
+
onCompromised?.();
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
// Held elsewhere, or the store is unusable. Either way this process must
|
|
90
|
+
// not run the turn: coalescing costs latency, two servers on one session
|
|
91
|
+
// costs the conversation.
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
this.held.set(sessionId, { release, refs: 1 });
|
|
95
|
+
return this.handle(sessionId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private handle(sessionId: string): TurnLease {
|
|
99
|
+
let released = false;
|
|
100
|
+
return {
|
|
101
|
+
release: async () => {
|
|
102
|
+
if (released) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
released = true;
|
|
106
|
+
const lock = this.held.get(sessionId);
|
|
107
|
+
if (lock === undefined) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
lock.refs -= 1;
|
|
111
|
+
if (lock.refs > 0) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.held.delete(sessionId);
|
|
115
|
+
// Throws if the lock was already compromised, which is not our problem
|
|
116
|
+
// to solve here: the successor owns it.
|
|
117
|
+
await lock.release().catch(() => {});
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Graceful shutdown: drop every claim so a successor need not wait it out. */
|
|
123
|
+
async releaseAll(): Promise<void> {
|
|
124
|
+
for (const [sessionId, lock] of [...this.held]) {
|
|
125
|
+
this.held.delete(sessionId);
|
|
126
|
+
await lock.release().catch(() => {});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The session is running in another server process. Mirrors the engine's
|
|
132
|
+
* in-memory `isTurnRunning`, which only ever sees this process.
|
|
133
|
+
*/
|
|
134
|
+
async runningElsewhere(sessionId: string): Promise<boolean> {
|
|
135
|
+
if (this.held.has(sessionId)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
return await lockfile.check(this.path(sessionId), {
|
|
140
|
+
realpath: false,
|
|
141
|
+
stale: this.staleMs,
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* all of this up natively through its project setting source.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
10
11
|
import {
|
|
11
12
|
chmod,
|
|
12
13
|
copyFile,
|
|
@@ -18,7 +19,9 @@ import {
|
|
|
18
19
|
symlink,
|
|
19
20
|
writeFile,
|
|
20
21
|
} from "node:fs/promises";
|
|
22
|
+
import { homedir } from "node:os";
|
|
21
23
|
import { dirname, join, parse, relative, resolve, sep } from "node:path";
|
|
24
|
+
import { promisify } from "node:util";
|
|
22
25
|
import { agentStoreKeys, DEPLOYMENT_STORE_MOUNT_PATH } from "../files.js";
|
|
23
26
|
import { MEMORY_DIR_NAME } from "../memory.js";
|
|
24
27
|
import type {
|
|
@@ -29,6 +32,8 @@ import type {
|
|
|
29
32
|
import { parseFrontmatter, serializeFrontmatter } from "./frontmatter.js";
|
|
30
33
|
import { buildIdentitySection } from "./prompt-context.js";
|
|
31
34
|
|
|
35
|
+
const execFileAsync = promisify(execFile);
|
|
36
|
+
|
|
32
37
|
export interface MaterializeWorkspaceOptions {
|
|
33
38
|
agent: ResolvedAgent;
|
|
34
39
|
workspaceDir: string;
|
|
@@ -268,6 +273,34 @@ export async function isNestedInGitRepo(dir: string): Promise<boolean> {
|
|
|
268
273
|
return false;
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
/** Default harness parent for a nested-git agent project. */
|
|
277
|
+
export function defaultLocalHarnessCwd(dirName: string): string {
|
|
278
|
+
return join(homedir(), ".cache", "agent-serve", dirName);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Give a session workspace its own git root so the harness roots *there*
|
|
283
|
+
* rather than at the enclosing repo (see {@link isNestedInGitRepo}). Without
|
|
284
|
+
* it the harness reads the outer repo's `.cursor/hooks.json`, rules, skills
|
|
285
|
+
* and `mcp.json`, and never sees the ones written into this workspace —
|
|
286
|
+
* instructions have an inline fallback, hooks have none.
|
|
287
|
+
*
|
|
288
|
+
* Best effort: a workspace that already has `.git` (a borrowed checkout or
|
|
289
|
+
* worktree) is left alone, and a failed `git init` leaves the harness where
|
|
290
|
+
* it already was.
|
|
291
|
+
*/
|
|
292
|
+
export async function ensureOwnGitRoot(dir: string): Promise<boolean> {
|
|
293
|
+
if (!(await isNestedInGitRepo(dir))) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
await execFileAsync("git", ["init", "--quiet"], { cwd: dir });
|
|
298
|
+
return true;
|
|
299
|
+
} catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
271
304
|
export async function materializeWorkspace(
|
|
272
305
|
options: MaterializeWorkspaceOptions
|
|
273
306
|
): Promise<void> {
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable session KV contract: key families plus a tolerant SessionRecord
|
|
3
|
+
* decoder. This is the lockstep surface for the control plane (see
|
|
4
|
+
* `backend/server/src/agentServe/durableSessionProtocol.ts`) so a dashboard
|
|
5
|
+
* list can read Bugbot KV without importing `@cursor/july`.
|
|
6
|
+
*
|
|
7
|
+
* Additive JSON only — unknown fields are ignored. Do not put ledger
|
|
8
|
+
* (`durable-session/`) types here; those are coordination-only and must not
|
|
9
|
+
* appear in the CP session list.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Framework-owned key root. All {@link storageKeys} values live under this
|
|
16
|
+
* prefix so a shared store can route or namespace Agent SDK data.
|
|
17
|
+
*/
|
|
18
|
+
export const STORAGE_KEY_ROOT = "agentkit/v1" as const;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Exact session-record keys: `agentkit/v1/{agent}/session/{sessionId}`.
|
|
22
|
+
* Rejects `session-events/`, `durable-session/`, and the CP-owned `_cp`
|
|
23
|
+
* agent name.
|
|
24
|
+
*/
|
|
25
|
+
const DURABLE_SESSION_RECORD_KEY_RE =
|
|
26
|
+
/^agentkit\/v1\/([^/]+)\/session\/([^/]+)$/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The framework-owned key scheme. Keys are a **stable, versioned contract**
|
|
30
|
+
* under {@link STORAGE_KEY_ROOT}: sinks may treat them as opaque strings, or
|
|
31
|
+
* route on prefixes (e.g. event chunks to object storage, everything else
|
|
32
|
+
* to a database). Channel ids and continuation tokens are the only segments
|
|
33
|
+
* that may contain caller-controlled characters; they are URI-encoded, and
|
|
34
|
+
* a segment whose encoding exceeds {@link MAX_KEY_SEGMENT_BYTES} is replaced
|
|
35
|
+
* by a `sha256:…` digest — so every minted key has a bounded length that any
|
|
36
|
+
* backend (VARCHAR columns, btree index tuples, S3 key limits) can store,
|
|
37
|
+
* no matter what a caller stuffs into a token. The substitution is
|
|
38
|
+
* deterministic: writes and continuation lookups build the same key.
|
|
39
|
+
*
|
|
40
|
+
* | Key | Value |
|
|
41
|
+
* | --- | --- |
|
|
42
|
+
* | `agentkit/v1/{agent}/session/{sessionId}` | `SessionRecord` |
|
|
43
|
+
* | `agentkit/v1/{agent}/session-events/{sessionId}/{index}` | `SessionEvent[]` chunk (index = first event's index, zero-padded) |
|
|
44
|
+
* | `agentkit/v1/{agent}/continuation/{channelId}/{token}` | `{ sessionId }` |
|
|
45
|
+
* | `agentkit/v1/{agent}/kv/{key}` | Author JSON via `HostContext.kv` |
|
|
46
|
+
*
|
|
47
|
+
* The `eval-runs` / `ab-samples` / `ab-snapshot` families are minted only
|
|
48
|
+
* by the tables `defineStorage` derives over the KV core; a backend that
|
|
49
|
+
* authors dedicated `StorageConfig.evals` / `StorageConfig.abs` groups never
|
|
50
|
+
* sees them.
|
|
51
|
+
*/
|
|
52
|
+
export const storageKeys = {
|
|
53
|
+
session: (agent: string, sessionId: string): string =>
|
|
54
|
+
`${STORAGE_KEY_ROOT}/${agent}/session/${sessionId}`,
|
|
55
|
+
sessionPrefix: (agent: string): string =>
|
|
56
|
+
`${STORAGE_KEY_ROOT}/${agent}/session/`,
|
|
57
|
+
sessionEvents: (
|
|
58
|
+
agent: string,
|
|
59
|
+
sessionId: string,
|
|
60
|
+
firstIndex: number
|
|
61
|
+
): string =>
|
|
62
|
+
`${STORAGE_KEY_ROOT}/${agent}/session-events/${sessionId}/${String(firstIndex).padStart(8, "0")}`,
|
|
63
|
+
sessionEventsPrefix: (agent: string, sessionId: string): string =>
|
|
64
|
+
`${STORAGE_KEY_ROOT}/${agent}/session-events/${sessionId}/`,
|
|
65
|
+
continuation: (
|
|
66
|
+
agent: string,
|
|
67
|
+
channelId: string,
|
|
68
|
+
continuationKey: string
|
|
69
|
+
): string =>
|
|
70
|
+
`${STORAGE_KEY_ROOT}/${agent}/continuation/${keySegment(channelId)}/${keySegment(continuationKey)}`,
|
|
71
|
+
/**
|
|
72
|
+
* Author-facing opaque JSON (see `HostContext.kv`). `key` is URI-encoded /
|
|
73
|
+
* digested like other caller-controlled segments.
|
|
74
|
+
*/
|
|
75
|
+
kv: (agent: string, key: string): string =>
|
|
76
|
+
`${STORAGE_KEY_ROOT}/${agent}/kv/${keySegment(key)}`,
|
|
77
|
+
kvPrefix: (agent: string): string => `${STORAGE_KEY_ROOT}/${agent}/kv/`,
|
|
78
|
+
/** One playground eval-run snapshot (derived `evals` table). */
|
|
79
|
+
evalRun: (agent: string, runId: string): string =>
|
|
80
|
+
`${STORAGE_KEY_ROOT}/${agent}/eval-runs/${keySegment(runId)}`,
|
|
81
|
+
evalRunsPrefix: (agent: string): string =>
|
|
82
|
+
`${STORAGE_KEY_ROOT}/${agent}/eval-runs/`,
|
|
83
|
+
/**
|
|
84
|
+
* One A/B metric sample (derived `abs` table); the segments mirror the
|
|
85
|
+
* dedicated table's row identity (experiment + sessionId + at).
|
|
86
|
+
*/
|
|
87
|
+
abSample: (
|
|
88
|
+
agent: string,
|
|
89
|
+
experiment: string,
|
|
90
|
+
sessionId: string,
|
|
91
|
+
at: string
|
|
92
|
+
): string =>
|
|
93
|
+
`${STORAGE_KEY_ROOT}/${agent}/ab-samples/${keySegment(experiment)}/${keySegment(sessionId)}/${keySegment(at)}`,
|
|
94
|
+
/** Latest aggregate A/B snapshot (derived `abs` table, LWW). */
|
|
95
|
+
abSnapshot: (agent: string): string =>
|
|
96
|
+
`${STORAGE_KEY_ROOT}/${agent}/ab-snapshot`,
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Max bytes a caller-controlled key segment may occupy after URI-encoding.
|
|
101
|
+
* Chosen so full keys stay well under common backend limits (Postgres btree
|
|
102
|
+
* index tuples cap at ~2704 bytes; S3 keys at 1024). Longer segments are
|
|
103
|
+
* replaced by their SHA-256 digest, keeping every minted key bounded.
|
|
104
|
+
*/
|
|
105
|
+
export const MAX_KEY_SEGMENT_BYTES = 256;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* URI-encode one caller-controlled key segment, substituting a `sha256:…`
|
|
109
|
+
* digest of the raw value when the encoding exceeds
|
|
110
|
+
* {@link MAX_KEY_SEGMENT_BYTES}. Deterministic, so key construction on the
|
|
111
|
+
* write path and the continuation-lookup path always agree.
|
|
112
|
+
*
|
|
113
|
+
* Exported for the session ledger, which builds its `session/` and
|
|
114
|
+
* `ledger/` keys with the same encoding rules.
|
|
115
|
+
*/
|
|
116
|
+
export function keySegment(raw: string): string {
|
|
117
|
+
const encoded = encodeURIComponent(raw);
|
|
118
|
+
if (Buffer.byteLength(encoded, "utf8") <= MAX_KEY_SEGMENT_BYTES) {
|
|
119
|
+
return encoded;
|
|
120
|
+
}
|
|
121
|
+
return `sha256:${createHash("sha256").update(raw, "utf8").digest("hex")}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isDurableSessionRecordKey(key: string): boolean {
|
|
125
|
+
const match = DURABLE_SESSION_RECORD_KEY_RE.exec(key);
|
|
126
|
+
return match !== null && match[1] !== "_cp";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** List fields of a persisted `SessionRecord`. Unknown properties are dropped. */
|
|
130
|
+
export type DurableSessionRecord = {
|
|
131
|
+
sessionId: string;
|
|
132
|
+
channelId: string;
|
|
133
|
+
mode: string;
|
|
134
|
+
continuationKey: string | null | undefined;
|
|
135
|
+
title: string | undefined;
|
|
136
|
+
turnCount: number;
|
|
137
|
+
createdAt: string;
|
|
138
|
+
updatedAt: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Session metadata for history UIs. `running` is omitted unless a live
|
|
143
|
+
* overlay (the engine process) supplies it — a KV-only reader must not
|
|
144
|
+
* claim a turn is in flight.
|
|
145
|
+
*/
|
|
146
|
+
export type DurableSessionSummary = {
|
|
147
|
+
sessionId: string;
|
|
148
|
+
channelId: string;
|
|
149
|
+
mode: string;
|
|
150
|
+
title: string | undefined;
|
|
151
|
+
turnCount: number;
|
|
152
|
+
createdAt: string;
|
|
153
|
+
updatedAt: string;
|
|
154
|
+
running: boolean | undefined;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
158
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function stringValue(value: unknown): string | undefined {
|
|
162
|
+
return typeof value === "string" ? value : undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Tolerant `SessionRecord` decoder. Extra fields (`auth`, `purpose`, ledger
|
|
167
|
+
* extras, future keys) are ignored. Returns undefined when required list
|
|
168
|
+
* fields are missing or the wrong type.
|
|
169
|
+
*/
|
|
170
|
+
export function parseDurableSessionRecord(
|
|
171
|
+
value: unknown
|
|
172
|
+
): DurableSessionRecord | undefined {
|
|
173
|
+
if (!isRecord(value)) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const sessionId = stringValue(value.sessionId);
|
|
177
|
+
const channelId = stringValue(value.channelId);
|
|
178
|
+
const mode = stringValue(value.mode);
|
|
179
|
+
const turnCount =
|
|
180
|
+
typeof value.turnCount === "number" ? value.turnCount : undefined;
|
|
181
|
+
const createdAt = stringValue(value.createdAt);
|
|
182
|
+
const updatedAt = stringValue(value.updatedAt);
|
|
183
|
+
if (
|
|
184
|
+
sessionId === undefined ||
|
|
185
|
+
sessionId.length === 0 ||
|
|
186
|
+
channelId === undefined ||
|
|
187
|
+
mode === undefined ||
|
|
188
|
+
turnCount === undefined ||
|
|
189
|
+
createdAt === undefined ||
|
|
190
|
+
updatedAt === undefined
|
|
191
|
+
) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
value.continuationKey !== undefined &&
|
|
196
|
+
value.continuationKey !== null &&
|
|
197
|
+
typeof value.continuationKey !== "string"
|
|
198
|
+
) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const continuationKey = value.continuationKey;
|
|
202
|
+
return {
|
|
203
|
+
sessionId,
|
|
204
|
+
channelId,
|
|
205
|
+
mode,
|
|
206
|
+
continuationKey,
|
|
207
|
+
title: typeof value.title === "string" ? value.title : undefined,
|
|
208
|
+
turnCount,
|
|
209
|
+
createdAt,
|
|
210
|
+
updatedAt,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function summarizeDurableSessionRecord(args: {
|
|
215
|
+
record: DurableSessionRecord;
|
|
216
|
+
running?: boolean;
|
|
217
|
+
}): DurableSessionSummary {
|
|
218
|
+
const { record, running } = args;
|
|
219
|
+
return {
|
|
220
|
+
sessionId: record.sessionId,
|
|
221
|
+
channelId: record.channelId,
|
|
222
|
+
mode: record.mode,
|
|
223
|
+
title: record.title,
|
|
224
|
+
turnCount: record.turnCount,
|
|
225
|
+
createdAt: record.createdAt,
|
|
226
|
+
updatedAt: record.updatedAt,
|
|
227
|
+
running,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Golden persisted record: required list fields plus extras a future engine
|
|
233
|
+
* might write. Both this package and the CP decoder must accept it.
|
|
234
|
+
*/
|
|
235
|
+
export const GOLDEN_DURABLE_SESSION_RECORD: Record<string, unknown> = {
|
|
236
|
+
sessionId: "ses_aaaaaaaaaaaaaaaaaaaaaaaa",
|
|
237
|
+
channelId: "playground",
|
|
238
|
+
mode: "chat",
|
|
239
|
+
purpose: "live",
|
|
240
|
+
continuationKey: "cont_key_1",
|
|
241
|
+
auth: { kind: "user", userId: 42 },
|
|
242
|
+
title: "Weather in NYC",
|
|
243
|
+
turnCount: 3,
|
|
244
|
+
createdAt: "2026-08-01T12:00:00.000Z",
|
|
245
|
+
updatedAt: "2026-08-01T12:05:00.000Z",
|
|
246
|
+
extraUnknownField: { nested: true },
|
|
247
|
+
};
|
package/src/storage.ts
CHANGED
|
@@ -52,13 +52,28 @@
|
|
|
52
52
|
* interface is the durable mirror.
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
|
-
import { createHash } from "node:crypto";
|
|
56
55
|
import type { ABMetricSample } from "./ab.js";
|
|
57
56
|
import type { EvalRunSnapshot } from "./evals.js";
|
|
58
57
|
import type { ABSnapshot } from "./internal/ab-snapshot.js";
|
|
59
58
|
import { brandDefinition } from "./internal/brand.js";
|
|
59
|
+
import { storageKeys } from "./storage-protocol.js";
|
|
60
60
|
import type { JsonValue, KvCasResult, KvVersionedEntry } from "./types.js";
|
|
61
61
|
|
|
62
|
+
export type {
|
|
63
|
+
DurableSessionRecord,
|
|
64
|
+
DurableSessionSummary,
|
|
65
|
+
} from "./storage-protocol.js";
|
|
66
|
+
export {
|
|
67
|
+
GOLDEN_DURABLE_SESSION_RECORD,
|
|
68
|
+
isDurableSessionRecordKey,
|
|
69
|
+
keySegment,
|
|
70
|
+
MAX_KEY_SEGMENT_BYTES,
|
|
71
|
+
parseDurableSessionRecord,
|
|
72
|
+
STORAGE_KEY_ROOT,
|
|
73
|
+
storageKeys,
|
|
74
|
+
summarizeDurableSessionRecord,
|
|
75
|
+
} from "./storage-protocol.js";
|
|
76
|
+
|
|
62
77
|
// ============================================================================
|
|
63
78
|
// Sink interface
|
|
64
79
|
// ============================================================================
|
|
@@ -424,115 +439,6 @@ function deriveAbsTable(config: StorageConfig): StorageABTable {
|
|
|
424
439
|
};
|
|
425
440
|
}
|
|
426
441
|
|
|
427
|
-
// ============================================================================
|
|
428
|
-
// Key scheme
|
|
429
|
-
// ============================================================================
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Framework-owned key root. All {@link storageKeys} values live under this
|
|
433
|
-
* prefix so a shared store can route or namespace Agent SDK data.
|
|
434
|
-
*/
|
|
435
|
-
export const STORAGE_KEY_ROOT = "agentkit/v1" as const;
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* The framework-owned key scheme. Keys are a **stable, versioned contract**
|
|
439
|
-
* under {@link STORAGE_KEY_ROOT}: sinks may treat them as opaque strings, or
|
|
440
|
-
* route on prefixes (e.g. event chunks to object storage, everything else
|
|
441
|
-
* to a database). Channel ids and continuation tokens are the only segments
|
|
442
|
-
* that may contain caller-controlled characters; they are URI-encoded, and
|
|
443
|
-
* a segment whose encoding exceeds {@link MAX_KEY_SEGMENT_BYTES} is replaced
|
|
444
|
-
* by a `sha256:…` digest — so every minted key has a bounded length that any
|
|
445
|
-
* backend (VARCHAR columns, btree index tuples, S3 key limits) can store,
|
|
446
|
-
* no matter what a caller stuffs into a token. The substitution is
|
|
447
|
-
* deterministic: writes and continuation lookups build the same key.
|
|
448
|
-
*
|
|
449
|
-
* | Key | Value |
|
|
450
|
-
* | --- | --- |
|
|
451
|
-
* | `agentkit/v1/{agent}/session/{sessionId}` | `SessionRecord` |
|
|
452
|
-
* | `agentkit/v1/{agent}/session-events/{sessionId}/{index}` | `SessionEvent[]` chunk (index = first event's index, zero-padded) |
|
|
453
|
-
* | `agentkit/v1/{agent}/continuation/{channelId}/{token}` | `{ sessionId }` |
|
|
454
|
-
* | `agentkit/v1/{agent}/kv/{key}` | Author JSON via {@link HostContext.kv} |
|
|
455
|
-
* | `agentkit/v1/{agent}/eval-runs/{runId}` | `EvalRunSnapshot` (derived `evals` table) |
|
|
456
|
-
* | `agentkit/v1/{agent}/ab-samples/{experiment}/{sessionId}/{at}` | `ABMetricSample` (derived `abs` table, append-only) |
|
|
457
|
-
* | `agentkit/v1/{agent}/ab-snapshot` | `ABSnapshot` (derived `abs` table, LWW) |
|
|
458
|
-
*
|
|
459
|
-
* The `eval-runs` / `ab-samples` / `ab-snapshot` families are minted only
|
|
460
|
-
* by the tables `defineStorage` derives over the KV core; a backend that
|
|
461
|
-
* authors dedicated {@link StorageConfig.evals} / {@link StorageConfig.abs}
|
|
462
|
-
* groups never sees them.
|
|
463
|
-
*/
|
|
464
|
-
export const storageKeys = {
|
|
465
|
-
session: (agent: string, sessionId: string): string =>
|
|
466
|
-
`${STORAGE_KEY_ROOT}/${agent}/session/${sessionId}`,
|
|
467
|
-
sessionPrefix: (agent: string): string =>
|
|
468
|
-
`${STORAGE_KEY_ROOT}/${agent}/session/`,
|
|
469
|
-
sessionEvents: (
|
|
470
|
-
agent: string,
|
|
471
|
-
sessionId: string,
|
|
472
|
-
firstIndex: number
|
|
473
|
-
): string =>
|
|
474
|
-
`${STORAGE_KEY_ROOT}/${agent}/session-events/${sessionId}/${String(firstIndex).padStart(8, "0")}`,
|
|
475
|
-
sessionEventsPrefix: (agent: string, sessionId: string): string =>
|
|
476
|
-
`${STORAGE_KEY_ROOT}/${agent}/session-events/${sessionId}/`,
|
|
477
|
-
continuation: (
|
|
478
|
-
agent: string,
|
|
479
|
-
channelId: string,
|
|
480
|
-
continuationKey: string
|
|
481
|
-
): string =>
|
|
482
|
-
`${STORAGE_KEY_ROOT}/${agent}/continuation/${keySegment(channelId)}/${keySegment(continuationKey)}`,
|
|
483
|
-
/**
|
|
484
|
-
* Author-facing opaque JSON (see {@link HostContext.kv}). `key` is
|
|
485
|
-
* URI-encoded / digested like other caller-controlled segments.
|
|
486
|
-
*/
|
|
487
|
-
kv: (agent: string, key: string): string =>
|
|
488
|
-
`${STORAGE_KEY_ROOT}/${agent}/kv/${keySegment(key)}`,
|
|
489
|
-
kvPrefix: (agent: string): string => `${STORAGE_KEY_ROOT}/${agent}/kv/`,
|
|
490
|
-
/** One playground eval-run snapshot (derived `evals` table). */
|
|
491
|
-
evalRun: (agent: string, runId: string): string =>
|
|
492
|
-
`${STORAGE_KEY_ROOT}/${agent}/eval-runs/${keySegment(runId)}`,
|
|
493
|
-
evalRunsPrefix: (agent: string): string =>
|
|
494
|
-
`${STORAGE_KEY_ROOT}/${agent}/eval-runs/`,
|
|
495
|
-
/**
|
|
496
|
-
* One A/B metric sample (derived `abs` table); the segments mirror the
|
|
497
|
-
* dedicated table's row identity (experiment + sessionId + at).
|
|
498
|
-
*/
|
|
499
|
-
abSample: (
|
|
500
|
-
agent: string,
|
|
501
|
-
experiment: string,
|
|
502
|
-
sessionId: string,
|
|
503
|
-
at: string
|
|
504
|
-
): string =>
|
|
505
|
-
`${STORAGE_KEY_ROOT}/${agent}/ab-samples/${keySegment(experiment)}/${keySegment(sessionId)}/${keySegment(at)}`,
|
|
506
|
-
/** Latest aggregate A/B snapshot (derived `abs` table, LWW). */
|
|
507
|
-
abSnapshot: (agent: string): string =>
|
|
508
|
-
`${STORAGE_KEY_ROOT}/${agent}/ab-snapshot`,
|
|
509
|
-
} as const;
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Max bytes a caller-controlled key segment may occupy after URI-encoding.
|
|
513
|
-
* Chosen so full keys stay well under common backend limits (Postgres btree
|
|
514
|
-
* index tuples cap at ~2704 bytes; S3 keys at 1024). Longer segments are
|
|
515
|
-
* replaced by their SHA-256 digest, keeping every minted key bounded.
|
|
516
|
-
*/
|
|
517
|
-
export const MAX_KEY_SEGMENT_BYTES = 256;
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* URI-encode one caller-controlled key segment, substituting a `sha256:…`
|
|
521
|
-
* digest of the raw value when the encoding exceeds
|
|
522
|
-
* {@link MAX_KEY_SEGMENT_BYTES}. Deterministic, so key construction on the
|
|
523
|
-
* write path and the continuation-lookup path always agree.
|
|
524
|
-
*
|
|
525
|
-
* Exported for the session ledger, which builds its `session/` and
|
|
526
|
-
* `ledger/` keys with the same encoding rules.
|
|
527
|
-
*/
|
|
528
|
-
export function keySegment(raw: string): string {
|
|
529
|
-
const encoded = encodeURIComponent(raw);
|
|
530
|
-
if (Buffer.byteLength(encoded, "utf8") <= MAX_KEY_SEGMENT_BYTES) {
|
|
531
|
-
return encoded;
|
|
532
|
-
}
|
|
533
|
-
return `sha256:${createHash("sha256").update(raw, "utf8").digest("hex")}`;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
442
|
// ============================================================================
|
|
537
443
|
// Policy resolution
|
|
538
444
|
// ============================================================================
|
package/src/tools.ts
CHANGED
|
@@ -85,6 +85,11 @@ type ServerToolConfig<TSchema> = {
|
|
|
85
85
|
description: string;
|
|
86
86
|
execution?: "server";
|
|
87
87
|
inputSchema?: TSchema;
|
|
88
|
+
/**
|
|
89
|
+
* JSON Schema for the tool's structured output (zod converted at
|
|
90
|
+
* discovery, never validated against). Advertised when declared.
|
|
91
|
+
*/
|
|
92
|
+
outputSchema?: z.ZodTypeAny | JsonSchemaObject;
|
|
88
93
|
needsApproval?: ToolNeedsApproval<ToolInput<TSchema>>;
|
|
89
94
|
execute: (
|
|
90
95
|
input: ToolInput<TSchema>,
|
|
@@ -97,6 +102,8 @@ type AgentToolConfig<TSchema> = {
|
|
|
97
102
|
description: string;
|
|
98
103
|
execution: "agent";
|
|
99
104
|
inputSchema?: TSchema;
|
|
105
|
+
/** Output JSON Schema; advertised when declared, never validated against. */
|
|
106
|
+
outputSchema?: z.ZodTypeAny | JsonSchemaObject;
|
|
100
107
|
/**
|
|
101
108
|
* Shell script written into the session workspace. Receives a JSON
|
|
102
109
|
* object on stdin and should print the result on stdout.
|