@cursor/july 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agent-serve.js +9 -0
- package/dist/channels/slack/post-update-delivery.d.ts +85 -0
- package/dist/channels/slack/post-update-delivery.d.ts.map +1 -0
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.BP8mAVMW.js → app.CY2buaEE.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.OfDZ5CXg.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.1FXIZbz8.js → VPLocalSearchBox.Ix954SFJ.js} +1 -1
- package/dist/docs/assets/chunks/{theme.Dm1R02v9.js → theme.PDwuJbyi.js} +2 -2
- package/dist/docs/assets/{deployment.md.BtfEsc9S.js → deployment.md.DC5-ImcM.js} +1 -1
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.js → example-agents_approval-buddy.md.DFGBYLcc.js} +2 -2
- package/dist/docs/assets/{example-agents_approval-buddy.md.8R5phXb5.lean.js → example-agents_approval-buddy.md.DFGBYLcc.lean.js} +1 -1
- package/dist/docs/assets/{example-agents_index.md.CwtWu5ix.js → example-agents_index.md.BPXjUtRB.js} +2 -2
- package/dist/docs/assets/{example-agents_index.md.CwtWu5ix.lean.js → example-agents_index.md.BPXjUtRB.lean.js} +1 -1
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.js → example-agents_security-reviewer.md.CE9v-aKm.js} +2 -2
- package/dist/docs/assets/{example-agents_security-reviewer.md.D2rtwDTO.lean.js → example-agents_security-reviewer.md.CE9v-aKm.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.D189RBCH.js → reference_cli.md.CjfiV2Q-.js} +2 -1
- package/dist/docs/assets/{reference_connections.md.zaEYCLHT.js → reference_connections.md.FbWupyzW.js} +1 -1
- package/dist/docs/assets/{storage.md.Cu3MwM2a.js → storage.md.DRTdnFvd.js} +7 -1
- package/dist/docs/assets/{storage.md.Cu3MwM2a.lean.js → storage.md.DRTdnFvd.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 +5 -5
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +5 -5
- 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 +5 -5
- 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 +6 -6
- 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 +3 -3
- package/dist/docs/guides/human-in-the-loop.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 +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +6 -5
- package/dist/docs/reference/connections.html +5 -5
- 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/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 +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +11 -5
- package/dist/docs/troubleshooting.html +3 -3
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +7 -4
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +45 -18
- package/dist/internal/deploy-client.d.ts +7 -1
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +3 -0
- package/dist/internal/deploy-source.d.ts +3 -0
- package/dist/internal/deploy-source.d.ts.map +1 -1
- package/dist/internal/deploy-source.js +25 -7
- package/dist/internal/discovery.js +1 -1
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/hosting.d.ts +12 -2
- package/dist/internal/hosting.d.ts.map +1 -1
- package/dist/internal/hosting.js +21 -4
- package/dist/internal/json-dir-store.d.ts +32 -0
- package/dist/internal/json-dir-store.d.ts.map +1 -0
- package/dist/internal/logs-client.d.ts +16 -2
- package/dist/internal/logs-client.d.ts.map +1 -1
- package/dist/internal/logs-client.js +51 -16
- package/dist/internal/persistence-coordinator.d.ts +127 -0
- package/dist/internal/persistence-coordinator.d.ts.map +1 -0
- package/dist/internal/resolved-connections.d.ts +6 -0
- package/dist/internal/resolved-connections.d.ts.map +1 -1
- package/dist/internal/resolved-connections.js +6 -2
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +11 -1
- package/dist/internal/session-engine.d.ts +23 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +100 -26
- package/dist/internal/sessions-client.d.ts +16 -2
- package/dist/internal/sessions-client.d.ts.map +1 -1
- package/dist/internal/sessions-client.js +28 -12
- package/dist/internal/storage-coordinator.d.ts +11 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -1
- package/dist/internal/storage-coordinator.js +30 -0
- package/dist/internal/workspace.d.ts +19 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +47 -13
- package/dist/persistence.d.ts +184 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/playground/assets/index-C0-QEG4O.js +86 -0
- package/dist/playground/assets/index-z3JY9O2x.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +75 -0
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -0
- package/dist/storage-backends/cursor-hosted.js +217 -0
- package/dist/storage-backends/postgres-kv.d.ts +5 -0
- package/dist/storage-backends/postgres-kv.d.ts.map +1 -1
- package/dist/storage-backends/postgres-kv.js +5 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/deployment.md +7 -3
- package/docs/example-agents/approval-buddy.md +1 -1
- package/docs/example-agents/index.md +1 -1
- package/docs/example-agents/security-reviewer.md +5 -5
- package/docs/reference/cli.md +2 -0
- package/docs/reference/connections.md +4 -2
- package/docs/storage.md +18 -4
- package/package.json +8 -1
- package/src/bin/agent-serve.ts +10 -0
- package/src/internal/cli-ax.ts +10 -4
- package/src/internal/cli-deploy.ts +58 -20
- package/src/internal/deploy-client.ts +11 -0
- package/src/internal/deploy-source.ts +24 -6
- package/src/internal/discovery.ts +1 -1
- package/src/internal/distribution.ts +1 -0
- package/src/internal/hosting.ts +24 -4
- package/src/internal/logs-client.ts +67 -21
- package/src/internal/resolved-connections.ts +14 -4
- package/src/internal/server.ts +15 -1
- package/src/internal/session-engine.ts +105 -13
- package/src/internal/sessions-client.ts +49 -14
- package/src/internal/storage-coordinator.ts +36 -0
- package/src/internal/workspace.ts +48 -11
- package/src/storage-backends/cursor-hosted.ts +267 -0
- package/src/storage-backends/postgres-kv.ts +5 -0
- package/src/types.ts +16 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.DevOabKJ.js +0 -1
- package/dist/playground/assets/index-7GJIUFQj.css +0 -1
- package/dist/playground/assets/index-Bpr4Th4M.js +0 -319
- /package/dist/docs/assets/{deployment.md.BtfEsc9S.lean.js → deployment.md.DC5-ImcM.lean.js} +0 -0
- /package/dist/docs/assets/{reference_cli.md.D189RBCH.lean.js → reference_cli.md.CjfiV2Q-.lean.js} +0 -0
- /package/dist/docs/assets/{reference_connections.md.zaEYCLHT.lean.js → reference_connections.md.FbWupyzW.lean.js} +0 -0
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type SessionEvent,
|
|
46
46
|
type SessionEventPayload,
|
|
47
47
|
type SessionInfo,
|
|
48
|
+
type SessionListSource,
|
|
48
49
|
type SessionRecord,
|
|
49
50
|
type SessionSummary,
|
|
50
51
|
type SlackNudgeHostApi,
|
|
@@ -73,7 +74,6 @@ import {
|
|
|
73
74
|
import { validateToolInput } from "./json-schema.js";
|
|
74
75
|
import { McpHost } from "./mcp-host.js";
|
|
75
76
|
import {
|
|
76
|
-
buildIdentitySection,
|
|
77
77
|
buildSessionContextBlock,
|
|
78
78
|
SCHEDULE_CHANNEL_PREFIX,
|
|
79
79
|
} from "./prompt-context.js";
|
|
@@ -83,7 +83,8 @@ import { SessionStore } from "./session-store.js";
|
|
|
83
83
|
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
84
84
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
85
85
|
import {
|
|
86
|
-
|
|
86
|
+
buildAgentsMdContent,
|
|
87
|
+
isNestedInGitRepo,
|
|
87
88
|
materializeWorkspace,
|
|
88
89
|
writeWorkspaceFiles,
|
|
89
90
|
} from "./workspace.js";
|
|
@@ -1019,12 +1020,22 @@ export class SessionEngine {
|
|
|
1019
1020
|
return rest;
|
|
1020
1021
|
});
|
|
1021
1022
|
}
|
|
1023
|
+
// The harness only loads the scaffolded AGENTS.md when the session
|
|
1024
|
+
// workspace is its own project root. With a per-session cwd override
|
|
1025
|
+
// (PR worktree) the scaffold lands elsewhere, and a workspace nested
|
|
1026
|
+
// inside a git repo re-roots harness rules at that repo — in both
|
|
1027
|
+
// cases deliver instructions inline on the first turn instead.
|
|
1028
|
+
const instructionsInvisibleToHarness =
|
|
1029
|
+
isFirstTurn &&
|
|
1030
|
+
(record.workspaceOverride !== undefined ||
|
|
1031
|
+
(await isNestedInGitRepo(harnessCwd)));
|
|
1022
1032
|
const request = this.buildTurnRequest(
|
|
1023
1033
|
record,
|
|
1024
1034
|
agent,
|
|
1025
1035
|
turnId,
|
|
1026
1036
|
message,
|
|
1027
1037
|
isFirstTurn,
|
|
1038
|
+
instructionsInvisibleToHarness,
|
|
1028
1039
|
emit,
|
|
1029
1040
|
async (update) => {
|
|
1030
1041
|
mapper.handleUpdate(update);
|
|
@@ -1120,6 +1131,7 @@ export class SessionEngine {
|
|
|
1120
1131
|
turnId: string,
|
|
1121
1132
|
prompt: string,
|
|
1122
1133
|
isFirstTurn: boolean,
|
|
1134
|
+
instructionsInvisibleToHarness: boolean,
|
|
1123
1135
|
emit: (payload: SessionEventPayload) => void,
|
|
1124
1136
|
onUpdate: RunnerTurnRequest["onUpdate"],
|
|
1125
1137
|
signal: AbortSignal
|
|
@@ -1147,15 +1159,21 @@ export class SessionEngine {
|
|
|
1147
1159
|
});
|
|
1148
1160
|
turnPrompt = `${contextBlock}\n\n${prompt}`;
|
|
1149
1161
|
}
|
|
1150
|
-
if (
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
.
|
|
1158
|
-
|
|
1162
|
+
if (isFirstTurn) {
|
|
1163
|
+
if (runtime === "cloud") {
|
|
1164
|
+
// Cloud VMs have no materialized workspace: ship the full context,
|
|
1165
|
+
// including agent tool script bodies.
|
|
1166
|
+
const preamble = buildAgentsMdContent(agent, { includeScripts: true });
|
|
1167
|
+
turnPrompt = `${preamble}\n\n${turnPrompt}`;
|
|
1168
|
+
} else if (instructionsInvisibleToHarness) {
|
|
1169
|
+
// Local workspace whose AGENTS.md the harness will not load. With a
|
|
1170
|
+
// per-session harness cwd override, agent tool scripts live only in
|
|
1171
|
+
// the durable scaffold dir — embed bodies like cloud. Otherwise the
|
|
1172
|
+
// catalog can reference on-disk paths under the harness cwd.
|
|
1173
|
+
const includeScripts = record.workspaceOverride !== undefined;
|
|
1174
|
+
const preamble = buildAgentsMdContent(agent, { includeScripts });
|
|
1175
|
+
turnPrompt = `${preamble}\n\n${turnPrompt}`;
|
|
1176
|
+
}
|
|
1159
1177
|
}
|
|
1160
1178
|
return {
|
|
1161
1179
|
sessionId: record.sessionId,
|
|
@@ -1810,13 +1828,88 @@ export class SessionEngine {
|
|
|
1810
1828
|
* observability surface across every principal (channel-driven runs,
|
|
1811
1829
|
* schedule/task sessions, other callers). The route auth chain still
|
|
1812
1830
|
* gates who may call this at all.
|
|
1831
|
+
*
|
|
1832
|
+
* When the project uses `cursorHostedStorage`, prefers durable Bugbot rows
|
|
1833
|
+
* (merged with any newer local records not flushed yet). Falls back to the
|
|
1834
|
+
* local `--state-root` store when the sink is unavailable.
|
|
1813
1835
|
*/
|
|
1814
1836
|
async listSessions(
|
|
1815
1837
|
caller: AuthContext | null,
|
|
1816
1838
|
options?: { includeAll?: boolean }
|
|
1817
1839
|
): Promise<SessionSummary[]> {
|
|
1840
|
+
return (await this.listSessionsWithSource(caller, options)).sessions;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
/**
|
|
1844
|
+
* Same as {@link listSessions}, plus which store answered
|
|
1845
|
+
* (`cursor-hosted` = durable DB via the control-plane proxy; `local` =
|
|
1846
|
+
* `--state-root`).
|
|
1847
|
+
*/
|
|
1848
|
+
async listSessionsWithSource(
|
|
1849
|
+
caller: AuthContext | null,
|
|
1850
|
+
options?: { includeAll?: boolean }
|
|
1851
|
+
): Promise<{ sessions: SessionSummary[]; source: SessionListSource }> {
|
|
1818
1852
|
const includeAll = options?.includeAll ?? false;
|
|
1853
|
+
if (this.storage?.isCursorHosted === true) {
|
|
1854
|
+
try {
|
|
1855
|
+
const records = await this.mergeStoredAndLocalSessions();
|
|
1856
|
+
return {
|
|
1857
|
+
sessions: this.summarizeSessionRecords(records, caller, includeAll),
|
|
1858
|
+
source: "cursor-hosted",
|
|
1859
|
+
};
|
|
1860
|
+
} catch (error) {
|
|
1861
|
+
this.logger(
|
|
1862
|
+
`[agentkit] cursor-hosted session list failed; falling back to local: ${describeError(error)}`
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1819
1866
|
const records = await this.sessions.list();
|
|
1867
|
+
// Local store is createdAt-asc; reverse so newest created leads.
|
|
1868
|
+
return {
|
|
1869
|
+
sessions: this.summarizeSessionRecords(
|
|
1870
|
+
[...records].reverse(),
|
|
1871
|
+
caller,
|
|
1872
|
+
includeAll
|
|
1873
|
+
),
|
|
1874
|
+
source: "local",
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/**
|
|
1879
|
+
* Durable sink rows plus local records (local wins on the same id when
|
|
1880
|
+
* newer — covers debounce windows that have not flushed yet). Newest
|
|
1881
|
+
* `updatedAt` first.
|
|
1882
|
+
*/
|
|
1883
|
+
private async mergeStoredAndLocalSessions(): Promise<SessionRecord[]> {
|
|
1884
|
+
const storage = this.storage;
|
|
1885
|
+
if (storage === undefined) {
|
|
1886
|
+
throw new Error("storage is not configured");
|
|
1887
|
+
}
|
|
1888
|
+
const stored = await storage.listSessionsFromSink();
|
|
1889
|
+
const local = await this.sessions.list();
|
|
1890
|
+
const byId = new Map<string, SessionRecord>();
|
|
1891
|
+
for (const record of stored) {
|
|
1892
|
+
byId.set(record.sessionId, record);
|
|
1893
|
+
}
|
|
1894
|
+
for (const record of local) {
|
|
1895
|
+
const existing = byId.get(record.sessionId);
|
|
1896
|
+
if (
|
|
1897
|
+
existing === undefined ||
|
|
1898
|
+
record.updatedAt.localeCompare(existing.updatedAt) >= 0
|
|
1899
|
+
) {
|
|
1900
|
+
byId.set(record.sessionId, record);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return [...byId.values()].sort((a, b) =>
|
|
1904
|
+
b.updatedAt.localeCompare(a.updatedAt)
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
private summarizeSessionRecords(
|
|
1909
|
+
records: readonly SessionRecord[],
|
|
1910
|
+
caller: AuthContext | null,
|
|
1911
|
+
includeAll: boolean
|
|
1912
|
+
): SessionSummary[] {
|
|
1820
1913
|
return records
|
|
1821
1914
|
.filter((record) => includeAll || samePrincipal(record.auth, caller))
|
|
1822
1915
|
.map((record) => {
|
|
@@ -1841,8 +1934,7 @@ export class SessionEngine {
|
|
|
1841
1934
|
),
|
|
1842
1935
|
...(externalUrl === undefined ? {} : { externalUrl }),
|
|
1843
1936
|
};
|
|
1844
|
-
})
|
|
1845
|
-
.reverse();
|
|
1937
|
+
});
|
|
1846
1938
|
}
|
|
1847
1939
|
|
|
1848
1940
|
/**
|
|
@@ -4,15 +4,33 @@
|
|
|
4
4
|
* Customer-facing errors never include hosting URLs or infra jargon.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
SessionEvent,
|
|
9
|
+
SessionListSource,
|
|
10
|
+
SessionSummary,
|
|
11
|
+
} from "../types.js";
|
|
8
12
|
import { sanitizeCustomerError } from "./logs-client.js";
|
|
9
13
|
import { mergeRequestHeaders, type RequestAuth } from "./request-headers.js";
|
|
10
14
|
|
|
15
|
+
export interface ListedSessions {
|
|
16
|
+
sessions: SessionSummary[];
|
|
17
|
+
/** Present when the server reports which store answered. */
|
|
18
|
+
source?: SessionListSource;
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
export async function listSessions(args: {
|
|
12
22
|
baseUrl: string;
|
|
13
23
|
auth?: RequestAuth;
|
|
14
24
|
fetchImpl?: typeof fetch;
|
|
15
25
|
}): Promise<SessionSummary[]> {
|
|
26
|
+
return (await listSessionsWithSource(args)).sessions;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function listSessionsWithSource(args: {
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
auth?: RequestAuth;
|
|
32
|
+
fetchImpl?: typeof fetch;
|
|
33
|
+
}): Promise<ListedSessions> {
|
|
16
34
|
const baseUrl = args.baseUrl.replace(/\/$/, "");
|
|
17
35
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
18
36
|
let resp: Response;
|
|
@@ -30,18 +48,21 @@ export async function listSessions(args: {
|
|
|
30
48
|
if (!resp.ok) {
|
|
31
49
|
throw new Error(`Could not list sessions (HTTP ${resp.status}).`);
|
|
32
50
|
}
|
|
33
|
-
const body = (await resp.json()) as { sessions?: unknown };
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
out.push(parsed);
|
|
51
|
+
const body = (await resp.json()) as { sessions?: unknown; source?: unknown };
|
|
52
|
+
const sessions: SessionSummary[] = [];
|
|
53
|
+
if (Array.isArray(body.sessions)) {
|
|
54
|
+
for (const row of body.sessions) {
|
|
55
|
+
const parsed = parseSessionSummary(row);
|
|
56
|
+
if (parsed !== undefined) {
|
|
57
|
+
sessions.push(parsed);
|
|
58
|
+
}
|
|
42
59
|
}
|
|
43
60
|
}
|
|
44
|
-
|
|
61
|
+
const source =
|
|
62
|
+
body.source === "cursor-hosted" || body.source === "local"
|
|
63
|
+
? body.source
|
|
64
|
+
: undefined;
|
|
65
|
+
return source === undefined ? { sessions } : { sessions, source };
|
|
45
66
|
}
|
|
46
67
|
|
|
47
68
|
export async function fetchSessionEvents(args: {
|
|
@@ -186,10 +207,24 @@ export async function resolvePreviousSessionId(args: {
|
|
|
186
207
|
return pickPreviousSession(sessions)?.sessionId;
|
|
187
208
|
}
|
|
188
209
|
|
|
210
|
+
/** Human label for {@link SessionListSource} in CLI output. */
|
|
211
|
+
export function formatSessionListSource(source: SessionListSource): string {
|
|
212
|
+
return source === "cursor-hosted"
|
|
213
|
+
? "Listing from cursor-hosted storage (durable DB)."
|
|
214
|
+
: "Listing from local session store.";
|
|
215
|
+
}
|
|
216
|
+
|
|
189
217
|
/** Format a sessions table (id, channel, mode, turns, running, updated). */
|
|
190
|
-
export function formatSessionsTable(
|
|
218
|
+
export function formatSessionsTable(
|
|
219
|
+
sessions: SessionSummary[],
|
|
220
|
+
options?: { source?: SessionListSource }
|
|
221
|
+
): string {
|
|
222
|
+
const sourceLine =
|
|
223
|
+
options?.source === undefined
|
|
224
|
+
? ""
|
|
225
|
+
: `${formatSessionListSource(options.source)}\n`;
|
|
191
226
|
if (sessions.length === 0) {
|
|
192
|
-
return
|
|
227
|
+
return `${sourceLine}No sessions.\n`;
|
|
193
228
|
}
|
|
194
229
|
const header = ["SESSION", "CHANNEL", "MODE", "TURNS", "RUNNING", "UPDATED"];
|
|
195
230
|
const rows = sessions.map((s) => [
|
|
@@ -200,7 +235,7 @@ export function formatSessionsTable(sessions: SessionSummary[]): string {
|
|
|
200
235
|
s.running ? "yes" : "no",
|
|
201
236
|
s.updatedAt,
|
|
202
237
|
]);
|
|
203
|
-
return `${renderTable(header, rows)}\n`;
|
|
238
|
+
return `${sourceLine}${renderTable(header, rows)}\n`;
|
|
204
239
|
}
|
|
205
240
|
|
|
206
241
|
function renderTable(header: string[], rows: string[][]): string {
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type StorageDefinition,
|
|
25
25
|
storageKeys,
|
|
26
26
|
} from "../storage.js";
|
|
27
|
+
import { isCursorHostedStorage } from "../storage-backends/cursor-hosted.js";
|
|
27
28
|
import type { JsonValue, SessionEvent, SessionRecord } from "../types.js";
|
|
28
29
|
import type { ABSnapshot } from "./ab-snapshot.js";
|
|
29
30
|
import { describeError } from "./describe-error.js";
|
|
@@ -261,6 +262,14 @@ export class StorageCoordinator {
|
|
|
261
262
|
return this.definition.list !== undefined;
|
|
262
263
|
}
|
|
263
264
|
|
|
265
|
+
/**
|
|
266
|
+
* True when the authored sink is `cursorHostedStorage` (Bugbot via the
|
|
267
|
+
* control-plane proxy). `GET /v1/sessions` prefers this durable list.
|
|
268
|
+
*/
|
|
269
|
+
get isCursorHosted(): boolean {
|
|
270
|
+
return isCursorHostedStorage(this.definition);
|
|
271
|
+
}
|
|
272
|
+
|
|
264
273
|
/**
|
|
265
274
|
* Saved session records for this agent (startup restore). Errors are
|
|
266
275
|
* logged and read as "nothing saved" — a broken store must never block
|
|
@@ -275,6 +284,33 @@ export class StorageCoordinator {
|
|
|
275
284
|
.filter((record) => record != null);
|
|
276
285
|
}
|
|
277
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Session records from the sink for the sessions API. Unlike
|
|
289
|
+
* {@link listSessions}, missing `list` or a thrown sink error **propagates**
|
|
290
|
+
* so the caller can fall back to the local session store.
|
|
291
|
+
*/
|
|
292
|
+
async listSessionsFromSink(): Promise<SessionRecord[]> {
|
|
293
|
+
const list = this.definition.list;
|
|
294
|
+
if (list === undefined) {
|
|
295
|
+
throw new Error("storage sink has no list");
|
|
296
|
+
}
|
|
297
|
+
const entries = await list(
|
|
298
|
+
storageKeys.sessionPrefix(this.agentName),
|
|
299
|
+
this.context("restore")
|
|
300
|
+
);
|
|
301
|
+
if (!Array.isArray(entries)) {
|
|
302
|
+
throw new Error("storage list returned a non-array");
|
|
303
|
+
}
|
|
304
|
+
return entries
|
|
305
|
+
.map((entry) => entry.value as unknown as SessionRecord)
|
|
306
|
+
.filter(
|
|
307
|
+
(record) =>
|
|
308
|
+
record != null &&
|
|
309
|
+
typeof record.sessionId === "string" &&
|
|
310
|
+
record.sessionId !== ""
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
278
314
|
/**
|
|
279
315
|
* Saved event stream for one session: chunk values concatenated in key
|
|
280
316
|
* order (chunks are keyed by their first event's index, zero-padded, so
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { chmod, copyFile, mkdir, stat, writeFile } from "node:fs/promises";
|
|
11
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
11
|
+
import { dirname, join, parse, relative, resolve } from "node:path";
|
|
12
12
|
import type {
|
|
13
13
|
DiscoveredSkill,
|
|
14
14
|
DiscoveredTool,
|
|
@@ -123,22 +123,59 @@ export function buildAgentToolsCatalog(
|
|
|
123
123
|
return `${parts.join("\n").trimEnd()}\n`;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The full AGENTS.md-equivalent context for an agent: instructions, identity
|
|
128
|
+
* section, and the agent-side tool catalog. Written to the session workspace
|
|
129
|
+
* as `AGENTS.md`, and inlined into the first turn's prompt when the harness
|
|
130
|
+
* cannot discover that file (cloud runtime, or a local workspace the harness
|
|
131
|
+
* will not treat as the project root — see {@link isNestedInGitRepo}).
|
|
132
|
+
*/
|
|
133
|
+
export function buildAgentsMdContent(
|
|
134
|
+
agent: ResolvedAgent,
|
|
135
|
+
options: { includeScripts: boolean }
|
|
136
|
+
): string {
|
|
137
|
+
return [
|
|
138
|
+
agent.instructions?.trim() ?? "",
|
|
139
|
+
buildIdentitySection(agent),
|
|
140
|
+
buildAgentToolsCatalog(agent.tools, {
|
|
141
|
+
includeScripts: options.includeScripts,
|
|
142
|
+
}),
|
|
143
|
+
]
|
|
144
|
+
.filter((part) => part !== "")
|
|
145
|
+
.join("\n\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* True when `dir` sits strictly inside a git repository (a `.git` exists in
|
|
150
|
+
* an ancestor directory but not in `dir` itself). The Cursor harness resolves
|
|
151
|
+
* its project root by walking up to the nearest git root, so a nested session
|
|
152
|
+
* workspace loses its own `AGENTS.md` / `.cursor` scaffold: the enclosing
|
|
153
|
+
* repo's rules load instead, and the (necessarily gitignored) workspace files
|
|
154
|
+
* are skipped by nested rule discovery.
|
|
155
|
+
*/
|
|
156
|
+
export async function isNestedInGitRepo(dir: string): Promise<boolean> {
|
|
157
|
+
let current = resolve(dir);
|
|
158
|
+
if (await exists(join(current, ".git"))) {
|
|
159
|
+
// The workspace is itself a repo root; the harness roots rules here.
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const { root } = parse(current);
|
|
163
|
+
while (current !== root) {
|
|
164
|
+
current = dirname(current);
|
|
165
|
+
if (await exists(join(current, ".git"))) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
126
172
|
export async function materializeWorkspace(
|
|
127
173
|
options: MaterializeWorkspaceOptions
|
|
128
174
|
): Promise<void> {
|
|
129
175
|
const { agent, workspaceDir, seed } = options;
|
|
130
176
|
await mkdir(workspaceDir, { recursive: true });
|
|
131
177
|
|
|
132
|
-
const
|
|
133
|
-
includeScripts: false,
|
|
134
|
-
});
|
|
135
|
-
const agentsMd = [
|
|
136
|
-
agent.instructions?.trim() ?? "",
|
|
137
|
-
buildIdentitySection(agent),
|
|
138
|
-
agentToolsSection,
|
|
139
|
-
]
|
|
140
|
-
.filter((part) => part !== "")
|
|
141
|
-
.join("\n\n");
|
|
178
|
+
const agentsMd = buildAgentsMdContent(agent, { includeScripts: false });
|
|
142
179
|
await writeFile(join(workspaceDir, "AGENTS.md"), agentsMd, "utf8");
|
|
143
180
|
|
|
144
181
|
for (const tool of agent.tools) {
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor-managed durable storage for hosted agents.
|
|
3
|
+
*
|
|
4
|
+
* Talks to the control-plane storage proxy
|
|
5
|
+
* (`POST /internal/agent-serve/storage/*`) using the engine's pod
|
|
6
|
+
* `CURSOR_API_KEY`. The server resolves `deployment_id` from that service
|
|
7
|
+
* account — engines never receive a Bugbot DSN and cannot address another
|
|
8
|
+
* deployment's rows.
|
|
9
|
+
*
|
|
10
|
+
* On Cursor-managed hosting the start script pins `CURSOR_API_BASE_URL` to
|
|
11
|
+
* `https://api.cursor.com` (where the proxy is mounted). Locally, point
|
|
12
|
+
* `CURSOR_API_BASE_URL` at a backend that mounts Agent Serve routes and use
|
|
13
|
+
* a deployment SA key, or use `@cursor/july/storage/postgres-kv` /
|
|
14
|
+
* `file-kv` instead.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { defineStorage } from "@cursor/july/storage";
|
|
18
|
+
* import { cursorHostedStorage } from "@cursor/july/storage/cursor-hosted";
|
|
19
|
+
*
|
|
20
|
+
* export default defineStorage({
|
|
21
|
+
* ...cursorHostedStorage(),
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { ABSnapshot } from "../ab.js";
|
|
27
|
+
import type { EvalRunSnapshot } from "../evals.js";
|
|
28
|
+
import type {
|
|
29
|
+
StorageABTable,
|
|
30
|
+
StorageConfig,
|
|
31
|
+
StorageEntry,
|
|
32
|
+
StorageEvalsTable,
|
|
33
|
+
} from "../storage.js";
|
|
34
|
+
import type { JsonValue } from "../types.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Same default as {@link cursorExternalApiUrl} in the CLI credentials module,
|
|
38
|
+
* inlined here so this storage entrypoint does not import CLI/zod code.
|
|
39
|
+
* Hosted monorepo examples load this file from `packages/agent-serve/src`
|
|
40
|
+
* while `npm install` runs in the example package — a credentials import
|
|
41
|
+
* would resolve `zod` from the wrong `node_modules` tree and crash serve.
|
|
42
|
+
*/
|
|
43
|
+
const DEFAULT_CURSOR_EXTERNAL_API_URL = "https://api.cursor.com";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reserved so user `secrets set` cannot smuggle a Bugbot DSN into the pod.
|
|
47
|
+
* Not injected by Cursor hosting (storage is HTTP-proxied).
|
|
48
|
+
*/
|
|
49
|
+
export const AGENT_SERVE_HOSTED_STORAGE_DATABASE_URL_ENV_VAR =
|
|
50
|
+
"BUGBOTDB_URL" as const;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Diagnostic / self-host deployment id. Not the hosted-storage authz
|
|
54
|
+
* boundary — the proxy scopes rows from the authenticated service account.
|
|
55
|
+
*/
|
|
56
|
+
export const AGENT_SERVE_DEPLOYMENT_ID_ENV_VAR =
|
|
57
|
+
"AGENT_SERVE_DEPLOYMENT_ID" as const;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Platform-managed secret / env names. User `secrets set` / DELETE cannot
|
|
61
|
+
* claim these.
|
|
62
|
+
*/
|
|
63
|
+
export const CURSOR_HOSTED_STORAGE_ENV_VARS: readonly [
|
|
64
|
+
typeof AGENT_SERVE_HOSTED_STORAGE_DATABASE_URL_ENV_VAR,
|
|
65
|
+
typeof AGENT_SERVE_DEPLOYMENT_ID_ENV_VAR,
|
|
66
|
+
] = [
|
|
67
|
+
AGENT_SERVE_HOSTED_STORAGE_DATABASE_URL_ENV_VAR,
|
|
68
|
+
AGENT_SERVE_DEPLOYMENT_ID_ENV_VAR,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
export interface CursorHostedStorageOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Control-plane base URL (default: `CURSOR_API_BASE_URL`, else
|
|
74
|
+
* `https://api.cursor.com`).
|
|
75
|
+
*/
|
|
76
|
+
baseUrl?: string;
|
|
77
|
+
/** Bearer credential (default: `CURSOR_API_KEY`). */
|
|
78
|
+
apiKey?: string;
|
|
79
|
+
/** Injected for tests. */
|
|
80
|
+
fetch?: typeof fetch;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Marker stamped on {@link cursorHostedStorage} so the engine can prefer
|
|
85
|
+
* durable Bugbot rows for `GET /v1/sessions` (survives `defineStorage` spread).
|
|
86
|
+
* Explicit `unique symbol` annotation required for `--isolatedDeclarations`
|
|
87
|
+
* (TS9010).
|
|
88
|
+
*/
|
|
89
|
+
export const CURSOR_HOSTED_STORAGE: unique symbol = Symbol.for(
|
|
90
|
+
"@cursor/july.storage.cursor-hosted"
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
/** True when `value` is (or was built from) {@link cursorHostedStorage}. */
|
|
94
|
+
export function isCursorHostedStorage(value: unknown): boolean {
|
|
95
|
+
return (
|
|
96
|
+
typeof value === "object" &&
|
|
97
|
+
value !== null &&
|
|
98
|
+
(value as Record<symbol, unknown>)[CURSOR_HOSTED_STORAGE] === true
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type CursorHostedStorageMethods = Required<
|
|
103
|
+
Pick<StorageConfig, "put" | "get" | "delete" | "list">
|
|
104
|
+
> & {
|
|
105
|
+
readonly [CURSOR_HOSTED_STORAGE]: true;
|
|
106
|
+
evals: StorageEvalsTable;
|
|
107
|
+
abs: StorageABTable;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function resolveApiKey(options: CursorHostedStorageOptions): string {
|
|
111
|
+
const key = (options.apiKey ?? process.env.CURSOR_API_KEY)?.trim();
|
|
112
|
+
if (key === undefined || key === "") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
"cursorHostedStorage requires CURSOR_API_KEY (deployment pod credential on Cursor hosting)"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return key;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveBaseUrl(options: CursorHostedStorageOptions): string {
|
|
121
|
+
const raw = (
|
|
122
|
+
options.baseUrl ??
|
|
123
|
+
process.env.CURSOR_API_BASE_URL ??
|
|
124
|
+
DEFAULT_CURSOR_EXTERNAL_API_URL
|
|
125
|
+
).trim();
|
|
126
|
+
return raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function storageRpc<T>(
|
|
130
|
+
options: CursorHostedStorageOptions,
|
|
131
|
+
path: string,
|
|
132
|
+
body: Record<string, unknown>
|
|
133
|
+
): Promise<T> {
|
|
134
|
+
const baseUrl = resolveBaseUrl(options);
|
|
135
|
+
const apiKey = resolveApiKey(options);
|
|
136
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
137
|
+
if (typeof fetchImpl !== "function") {
|
|
138
|
+
throw new Error("cursorHostedStorage requires fetch");
|
|
139
|
+
}
|
|
140
|
+
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: {
|
|
143
|
+
Authorization: `Bearer ${apiKey}`,
|
|
144
|
+
"Content-Type": "application/json",
|
|
145
|
+
},
|
|
146
|
+
body: JSON.stringify(body),
|
|
147
|
+
});
|
|
148
|
+
const text = await response.text();
|
|
149
|
+
let parsed: unknown;
|
|
150
|
+
try {
|
|
151
|
+
parsed = text.length === 0 ? {} : JSON.parse(text);
|
|
152
|
+
} catch {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`cursorHostedStorage ${path} returned non-JSON (${response.status})`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
const error =
|
|
159
|
+
parsed !== null &&
|
|
160
|
+
typeof parsed === "object" &&
|
|
161
|
+
"error" in parsed &&
|
|
162
|
+
typeof (parsed as { error: unknown }).error === "string"
|
|
163
|
+
? (parsed as { error: string }).error
|
|
164
|
+
: `HTTP ${response.status}`;
|
|
165
|
+
throw new Error(`cursorHostedStorage ${path}: ${error}`);
|
|
166
|
+
}
|
|
167
|
+
return parsed as T;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Cursor-hosted `defineStorage` methods (control-plane storage proxy →
|
|
172
|
+
* Bugbot `agent_serve_*` tables). Spread into `defineStorage({...})`.
|
|
173
|
+
*/
|
|
174
|
+
export function cursorHostedStorage(
|
|
175
|
+
options: CursorHostedStorageOptions = {}
|
|
176
|
+
): CursorHostedStorageMethods {
|
|
177
|
+
return {
|
|
178
|
+
[CURSOR_HOSTED_STORAGE]: true,
|
|
179
|
+
async put(key, value): Promise<void> {
|
|
180
|
+
await storageRpc(options, "/internal/agent-serve/storage/kv/put", {
|
|
181
|
+
key,
|
|
182
|
+
value,
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
async get(key): Promise<JsonValue | undefined> {
|
|
186
|
+
const result = await storageRpc<{
|
|
187
|
+
found: boolean;
|
|
188
|
+
value: JsonValue | null;
|
|
189
|
+
}>(options, "/internal/agent-serve/storage/kv/get", { key });
|
|
190
|
+
if (!result.found) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
return result.value ?? undefined;
|
|
194
|
+
},
|
|
195
|
+
async delete(key): Promise<void> {
|
|
196
|
+
await storageRpc(options, "/internal/agent-serve/storage/kv/delete", {
|
|
197
|
+
key,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
async list(prefix): Promise<StorageEntry[]> {
|
|
201
|
+
const result = await storageRpc<{
|
|
202
|
+
entries: Array<{ key: string; value: JsonValue }>;
|
|
203
|
+
}>(options, "/internal/agent-serve/storage/kv/list", { prefix });
|
|
204
|
+
return result.entries;
|
|
205
|
+
},
|
|
206
|
+
evals: {
|
|
207
|
+
async put(run, ctx): Promise<void> {
|
|
208
|
+
await storageRpc(options, "/internal/agent-serve/storage/evals/put", {
|
|
209
|
+
agentName: ctx.agentName,
|
|
210
|
+
run,
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
async delete(runId, ctx): Promise<void> {
|
|
214
|
+
await storageRpc(
|
|
215
|
+
options,
|
|
216
|
+
"/internal/agent-serve/storage/evals/delete",
|
|
217
|
+
{
|
|
218
|
+
agentName: ctx.agentName,
|
|
219
|
+
runId,
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
async list(ctx): Promise<EvalRunSnapshot[]> {
|
|
224
|
+
const result = await storageRpc<{ runs: EvalRunSnapshot[] }>(
|
|
225
|
+
options,
|
|
226
|
+
"/internal/agent-serve/storage/evals/list",
|
|
227
|
+
{ agentName: ctx.agentName }
|
|
228
|
+
);
|
|
229
|
+
return result.runs;
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
abs: {
|
|
233
|
+
async putSample(sample, ctx): Promise<void> {
|
|
234
|
+
await storageRpc(
|
|
235
|
+
options,
|
|
236
|
+
"/internal/agent-serve/storage/abs/put-sample",
|
|
237
|
+
{
|
|
238
|
+
agentName: ctx.agentName,
|
|
239
|
+
sample,
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
async putSnapshot(snapshot, ctx): Promise<void> {
|
|
244
|
+
await storageRpc(
|
|
245
|
+
options,
|
|
246
|
+
"/internal/agent-serve/storage/abs/put-snapshot",
|
|
247
|
+
{
|
|
248
|
+
agentName: ctx.agentName,
|
|
249
|
+
snapshot,
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
async getSnapshot(ctx): Promise<ABSnapshot | undefined> {
|
|
254
|
+
const result = await storageRpc<{
|
|
255
|
+
found: boolean;
|
|
256
|
+
snapshot: ABSnapshot | null;
|
|
257
|
+
}>(options, "/internal/agent-serve/storage/abs/get-snapshot", {
|
|
258
|
+
agentName: ctx.agentName,
|
|
259
|
+
});
|
|
260
|
+
if (!result.found) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
return result.snapshot ?? undefined;
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|