@cursor/july 0.1.1 → 0.1.3
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 +40 -7
- package/README.md +33 -24
- package/dist/bin/agent-serve.d.ts +3 -1
- package/dist/bin/agent-serve.d.ts.map +1 -1
- package/dist/bin/agent-serve.js +466 -140
- package/dist/channels/github/api.d.ts.map +1 -1
- package/dist/channels/github/api.js +31 -14
- package/dist/channels/github/cursor-account.d.ts +43 -0
- package/dist/channels/github/cursor-account.d.ts.map +1 -0
- package/dist/channels/github/cursor-account.js +95 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -1
- package/dist/channels/github/github-channel.js +46 -9
- package/dist/channels/github/index.d.ts +2 -2
- package/dist/channels/github/index.js +2 -2
- package/dist/channels/github/types.d.ts +17 -0
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.d.ts +8 -2
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +8 -0
- package/dist/channels/slack/types.d.ts +24 -3
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/channels/slack/types.js +15 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +8 -8
- package/dist/docs/assets/{ab.md.COdXkces.js → ab.md.BMCZ6Hd7.js} +3 -3
- package/dist/docs/assets/{ab.md.COdXkces.lean.js → ab.md.BMCZ6Hd7.lean.js} +1 -1
- package/dist/docs/assets/{app.DqfFEmJd.js → app.BR0RbIdx.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DtIOQzGj.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BaLEdS15.js → VPLocalSearchBox.qjsVTneI.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CZRvu_0q.js → theme.2Kg8jIp_.js} +2 -2
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.js → deployment.md.DTKwE15Z.js} +3 -3
- package/dist/docs/assets/{deployment.md.Dx1TYNk5.lean.js → deployment.md.DTKwE15Z.lean.js} +1 -1
- package/dist/docs/assets/{evals.md.DPZ_MAnI.js → evals.md.DAgEc_hL.js} +3 -3
- package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.js → guides_agent-to-agent.md.Bpzgq2Pq.js} +1 -1
- package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.js → guides_cloud-runtime.md.gVzabdQL.js} +1 -1
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.js → guides_github.md.DOOCpqsW.js} +11 -4
- package/dist/docs/assets/{guides_github.md.DwbKhCeS.lean.js → guides_github.md.DOOCpqsW.lean.js} +1 -1
- package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.js → guides_human-in-the-loop.md.DlUqsp1S.js} +2 -2
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.js → guides_slack.md.CCwqHvSV.js} +4 -4
- package/dist/docs/assets/{guides_slack.md.bv41fHfW.lean.js → guides_slack.md.CCwqHvSV.lean.js} +1 -1
- package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.js → guides_webhooks.md.B1EswtUu.js} +2 -2
- package/dist/docs/assets/index.md.m81y7TY7.js +20 -0
- package/dist/docs/assets/{index.md.BPKcj5AI.lean.js → index.md.m81y7TY7.lean.js} +1 -1
- package/dist/docs/assets/quickstart.md.BU6Iwi_9.js +204 -0
- package/dist/docs/assets/quickstart.md.BU6Iwi_9.lean.js +1 -0
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.js → reference_agent-config.md.DrW2JUM8.js} +4 -4
- package/dist/docs/assets/{reference_agent-config.md.Bpd7HQwf.lean.js → reference_agent-config.md.DrW2JUM8.lean.js} +1 -1
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.js → reference_channels.md.DdmiKgqf.js} +4 -4
- package/dist/docs/assets/{reference_channels.md.D7JTR03W.lean.js → reference_channels.md.DdmiKgqf.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.DA730zCu.js → reference_cli.md.Bv6pOxcF.js} +11 -6
- package/dist/docs/assets/{reference_cli.md.DA730zCu.lean.js → reference_cli.md.Bv6pOxcF.lean.js} +1 -1
- package/dist/docs/assets/{reference_connections.md.C3vNH_DE.js → reference_connections.md.zaEYCLHT.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.js → reference_hooks.md.DyLVfE1O.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BCEc3MyM.lean.js → reference_hooks.md.DyLVfE1O.lean.js} +1 -1
- package/dist/docs/assets/{reference_http-api.md.DBAahtdz.js → reference_http-api.md.Dx_nmDG6.js} +1 -1
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.js → reference_instructions.md.CgoV-YEb.js} +9 -7
- package/dist/docs/assets/{reference_instructions.md.BC05LEQ8.lean.js → reference_instructions.md.CgoV-YEb.lean.js} +1 -1
- package/dist/docs/assets/{reference_schedules.md.D7qijxLk.js → reference_schedules.md.w_F2mXB6.js} +2 -2
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.js → reference_skills.md.B_jHN7JL.js} +3 -3
- package/dist/docs/assets/{reference_skills.md.VQnlBT3Q.lean.js → reference_skills.md.B_jHN7JL.lean.js} +1 -1
- package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.js → reference_subagents.md.zWAMNfi1.js} +1 -1
- package/dist/docs/assets/{reference_tools.md.DF5kwlt0.js → reference_tools.md.CqgJroI0.js} +2 -2
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.js +1 -0
- package/dist/docs/assets/scaffolding-agents.md.C3pTrmoE.lean.js +1 -0
- package/dist/docs/assets/storage.md.CVnInNiN.js +17 -0
- package/dist/docs/assets/storage.md.CVnInNiN.lean.js +1 -0
- package/dist/docs/building-with-agents.html +4 -4
- package/dist/docs/concepts.html +4 -4
- package/dist/docs/deployment.html +7 -7
- package/dist/docs/evals.html +7 -7
- package/dist/docs/guides/agent-to-agent.html +6 -6
- package/dist/docs/guides/cloud-runtime.html +5 -5
- package/dist/docs/guides/github.html +14 -7
- package/dist/docs/guides/human-in-the-loop.html +6 -6
- package/dist/docs/guides/slack.html +8 -8
- package/dist/docs/guides/webhooks.html +6 -6
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +5 -5
- package/dist/docs/index.html +7 -7
- package/dist/docs/quickstart.html +195 -26
- package/dist/docs/reference/agent-config.html +7 -7
- package/dist/docs/reference/channels.html +8 -8
- package/dist/docs/reference/cli.html +14 -9
- package/dist/docs/reference/connections.html +5 -5
- package/dist/docs/reference/hooks.html +5 -5
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/instructions.html +13 -11
- package/dist/docs/reference/playground.html +4 -4
- package/dist/docs/reference/project-layout.html +4 -4
- package/dist/docs/reference/schedules.html +7 -7
- package/dist/docs/reference/sessions.html +4 -4
- package/dist/docs/reference/skills.html +6 -6
- package/dist/docs/reference/subagents.html +6 -6
- package/dist/docs/reference/tools.html +7 -7
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/storage.html +41 -0
- package/dist/docs/troubleshooting.html +4 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/internal/chat-client.d.ts +29 -3
- package/dist/internal/chat-client.d.ts.map +1 -1
- package/dist/internal/chat-client.js +180 -27
- package/dist/internal/cli-ax.d.ts +75 -2
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +600 -52
- package/dist/internal/cli-cursor.d.ts.map +1 -1
- package/dist/internal/cli-cursor.js +8 -36
- package/dist/internal/cli-deploy.d.ts +108 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -0
- package/dist/internal/cli-deploy.js +1009 -0
- package/dist/internal/cursor/backend-client.d.ts +10 -1
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +79 -1
- package/dist/internal/cursor/credentials.d.ts +9 -0
- package/dist/internal/cursor/credentials.d.ts.map +1 -1
- package/dist/internal/cursor/credentials.js +12 -0
- package/dist/internal/cursor/github-credentials.d.ts +44 -0
- package/dist/internal/cursor/github-credentials.d.ts.map +1 -0
- package/dist/internal/cursor/github-credentials.js +195 -0
- package/dist/internal/deploy-client.d.ts +184 -0
- package/dist/internal/deploy-client.d.ts.map +1 -0
- package/dist/internal/deploy-client.js +395 -0
- package/dist/internal/deploy-source.d.ts +35 -0
- package/dist/internal/deploy-source.d.ts.map +1 -0
- package/dist/internal/deploy-source.js +118 -0
- package/dist/internal/discovery.d.ts +11 -0
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +116 -19
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +1 -0
- package/dist/internal/eval-run-store.d.ts +22 -3
- package/dist/internal/eval-run-store.d.ts.map +1 -1
- package/dist/internal/eval-run-store.js +39 -20
- package/dist/internal/eval-runner.d.ts +2 -0
- package/dist/internal/eval-runner.d.ts.map +1 -1
- package/dist/internal/eval-runner.js +2 -0
- package/dist/internal/event-mapper.d.ts +54 -1
- package/dist/internal/event-mapper.d.ts.map +1 -1
- package/dist/internal/event-mapper.js +151 -41
- package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
- package/dist/internal/handleAgentServeTrigger.js +10 -12
- package/dist/internal/host-platforms.d.ts +7 -2
- package/dist/internal/host-platforms.d.ts.map +1 -1
- package/dist/internal/host-platforms.js +15 -10
- package/dist/internal/hosting.d.ts +37 -0
- package/dist/internal/hosting.d.ts.map +1 -0
- package/dist/internal/hosting.js +67 -0
- package/dist/internal/init-project.d.ts +88 -1
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +221 -31
- package/dist/internal/install-cursor-skills.d.ts +64 -0
- package/dist/internal/install-cursor-skills.d.ts.map +1 -0
- package/dist/internal/install-cursor-skills.js +274 -0
- package/dist/internal/logs-client.d.ts +60 -0
- package/dist/internal/logs-client.d.ts.map +1 -0
- package/dist/internal/logs-client.js +311 -0
- package/dist/internal/open-browser.d.ts +5 -0
- package/dist/internal/open-browser.d.ts.map +1 -0
- package/dist/internal/open-browser.js +34 -0
- package/dist/internal/playground/toolchain.d.ts.map +1 -1
- package/dist/internal/playground/toolchain.js +16 -11
- package/dist/internal/playground-proxy.d.ts +69 -0
- package/dist/internal/playground-proxy.d.ts.map +1 -0
- package/dist/internal/playground-proxy.js +468 -0
- package/dist/internal/prompt-context.d.ts +35 -0
- package/dist/internal/prompt-context.d.ts.map +1 -0
- package/dist/internal/prompt-context.js +71 -0
- package/dist/internal/reminder-runner.d.ts +7 -0
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +50 -6
- package/dist/internal/reminder-store.d.ts +2 -0
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +18 -0
- package/dist/internal/request-headers.d.ts +11 -0
- package/dist/internal/request-headers.d.ts.map +1 -0
- package/dist/internal/request-headers.js +14 -0
- package/dist/internal/resolve-prod-target.d.ts +42 -0
- package/dist/internal/resolve-prod-target.d.ts.map +1 -0
- package/dist/internal/resolve-prod-target.js +111 -0
- package/dist/internal/run-client.d.ts +2 -2
- package/dist/internal/run-client.d.ts.map +1 -1
- package/dist/internal/run-client.js +38 -23
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +182 -45
- package/dist/internal/session-engine.d.ts +50 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +267 -23
- package/dist/internal/sessions-client.d.ts +21 -0
- package/dist/internal/sessions-client.d.ts.map +1 -0
- package/dist/internal/sessions-client.js +168 -0
- package/dist/internal/storage-coordinator.d.ts +139 -0
- package/dist/internal/storage-coordinator.d.ts.map +1 -0
- package/dist/internal/storage-coordinator.js +499 -0
- package/dist/internal/stream-progress.d.ts.map +1 -1
- package/dist/internal/stream-progress.js +31 -3
- package/dist/internal/terminal-style.d.ts +22 -0
- package/dist/internal/terminal-style.d.ts.map +1 -0
- package/dist/internal/terminal-style.js +49 -0
- package/dist/internal/trajectory.d.ts +10 -5
- package/dist/internal/trajectory.d.ts.map +1 -1
- package/dist/internal/trajectory.js +82 -10
- package/dist/internal/workspace.d.ts +11 -0
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +38 -4
- package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
- package/dist/playground/assets/index-D-vo2lV_.css +1 -0
- package/dist/playground/assets/index-x60b9q2j.js +312 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage.d.ts +204 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +153 -0
- package/dist/types.d.ts +70 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -2
- package/docs/guides/github.md +43 -8
- package/docs/guides/slack.md +1 -1
- package/docs/quickstart.md +350 -57
- package/docs/reference/cli.md +51 -3
- package/docs/reference/instructions.md +8 -6
- package/docs/scaffolding-agents.md +1 -1
- package/docs/storage.md +98 -0
- package/package.json +10 -1
- package/skills/ab/SKILL.md +8 -8
- package/skills/create-agent/SKILL.md +28 -22
- package/skills/debug/SKILL.md +11 -11
- package/skills/evals/SKILL.md +16 -16
- package/skills/framework-map/SKILL.md +6 -6
- package/skills/github/SKILL.md +22 -14
- package/skills/hillclimb/SKILL.md +10 -10
- package/skills/setup-slack/SKILL.md +13 -13
- package/src/bin/agent-serve.ts +552 -179
- package/src/channels/github/api.ts +42 -23
- package/src/channels/github/cursor-account.ts +165 -0
- package/src/channels/github/github-channel.ts +66 -6
- package/src/channels/github/index.ts +2 -2
- package/src/channels/github/types.ts +19 -0
- package/src/channels/slack/slack-channel.ts +17 -3
- package/src/channels/slack/types.ts +44 -3
- package/src/index.ts +13 -0
- package/src/internal/chat-client.ts +252 -37
- package/src/internal/cli-ax.ts +722 -72
- package/src/internal/cli-cursor.ts +14 -42
- package/src/internal/cli-deploy.ts +1319 -0
- package/src/internal/cursor/backend-client.ts +103 -1
- package/src/internal/cursor/credentials.ts +14 -0
- package/src/internal/cursor/github-credentials.ts +248 -0
- package/src/internal/deploy-client.ts +632 -0
- package/src/internal/deploy-source.ts +133 -0
- package/src/internal/discovery.ts +141 -17
- package/src/internal/distribution.ts +1 -0
- package/src/internal/eval-run-store.ts +50 -20
- package/src/internal/eval-runner.ts +5 -0
- package/src/internal/event-mapper.ts +222 -42
- package/src/internal/handleAgentServeTrigger.ts +10 -12
- package/src/internal/host-platforms.ts +28 -11
- package/src/internal/hosting.ts +77 -0
- package/src/internal/init-project.ts +333 -51
- package/src/internal/install-cursor-skills.ts +327 -0
- package/src/internal/logs-client.ts +442 -0
- package/src/internal/open-browser.ts +41 -0
- package/src/internal/playground/toolchain.ts +15 -13
- package/src/internal/playground-proxy.ts +576 -0
- package/src/internal/prompt-context.ts +95 -0
- package/src/internal/reminder-runner.ts +50 -6
- package/src/internal/reminder-store.ts +21 -0
- package/src/internal/request-headers.ts +23 -0
- package/src/internal/resolve-prod-target.ts +150 -0
- package/src/internal/run-client.ts +39 -36
- package/src/internal/server.ts +223 -28
- package/src/internal/session-engine.ts +307 -10
- package/src/internal/sessions-client.ts +182 -0
- package/src/internal/storage-coordinator.ts +615 -0
- package/src/internal/stream-progress.ts +36 -2
- package/src/internal/terminal-style.ts +74 -0
- package/src/internal/trajectory.ts +91 -13
- package/src/internal/workspace.ts +42 -4
- package/src/storage.ts +325 -0
- package/src/types.ts +82 -8
- package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +0 -1
- package/dist/docs/assets/index.md.BPKcj5AI.js +0 -20
- package/dist/docs/assets/quickstart.md.tVPiGK_L.js +0 -35
- package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +0 -1
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +0 -1
- package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
- package/dist/playground/assets/index-1K-hG-7p.css +0 -1
- package/dist/playground/assets/index-FlWjhg3x.js +0 -79
- /package/dist/docs/assets/{evals.md.DPZ_MAnI.lean.js → evals.md.DAgEc_hL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_agent-to-agent.md.CrtrsySy.lean.js → guides_agent-to-agent.md.Bpzgq2Pq.lean.js} +0 -0
- /package/dist/docs/assets/{guides_cloud-runtime.md.CYlNMTNp.lean.js → guides_cloud-runtime.md.gVzabdQL.lean.js} +0 -0
- /package/dist/docs/assets/{guides_human-in-the-loop.md.Dvuctx7s.lean.js → guides_human-in-the-loop.md.DlUqsp1S.lean.js} +0 -0
- /package/dist/docs/assets/{guides_webhooks.md.hFTik3lf.lean.js → guides_webhooks.md.B1EswtUu.lean.js} +0 -0
- /package/dist/docs/assets/{reference_connections.md.C3vNH_DE.lean.js → reference_connections.md.zaEYCLHT.lean.js} +0 -0
- /package/dist/docs/assets/{reference_http-api.md.DBAahtdz.lean.js → reference_http-api.md.Dx_nmDG6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_schedules.md.D7qijxLk.lean.js → reference_schedules.md.w_F2mXB6.lean.js} +0 -0
- /package/dist/docs/assets/{reference_subagents.md.CIRAVcPK.lean.js → reference_subagents.md.zWAMNfi1.lean.js} +0 -0
- /package/dist/docs/assets/{reference_tools.md.DF5kwlt0.lean.js → reference_tools.md.CqgJroI0.lean.js} +0 -0
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* event handlers and hooks.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { mkdir, rm } from "node:fs/promises";
|
|
11
|
-
import { join } from "node:path";
|
|
10
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
12
|
import type { SDKCustomTool } from "@cursor/sdk";
|
|
13
13
|
import {
|
|
14
14
|
type ABDefinition,
|
|
@@ -54,6 +54,7 @@ import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
|
|
|
54
54
|
import { ApprovalGate } from "./approval-gate.js";
|
|
55
55
|
import { samePrincipal } from "./auth.js";
|
|
56
56
|
import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
|
|
57
|
+
import { isDevMode } from "./dev-mode.js";
|
|
57
58
|
import { EventLogStore } from "./event-log.js";
|
|
58
59
|
import { TurnEventMapper } from "./event-mapper.js";
|
|
59
60
|
import { createHostPlatforms, type HostPlatforms } from "./host-platforms.js";
|
|
@@ -65,9 +66,15 @@ import {
|
|
|
65
66
|
} from "./ids.js";
|
|
66
67
|
import { validateToolInput } from "./json-schema.js";
|
|
67
68
|
import { McpHost } from "./mcp-host.js";
|
|
69
|
+
import {
|
|
70
|
+
buildIdentitySection,
|
|
71
|
+
buildSessionContextBlock,
|
|
72
|
+
SCHEDULE_CHANNEL_PREFIX,
|
|
73
|
+
} from "./prompt-context.js";
|
|
68
74
|
import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections.js";
|
|
69
75
|
import type { AgentRunner } from "./sdk-runner.js";
|
|
70
76
|
import { SessionStore } from "./session-store.js";
|
|
77
|
+
import { StorageCoordinator } from "./storage-coordinator.js";
|
|
71
78
|
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
72
79
|
import {
|
|
73
80
|
buildAgentToolsCatalog,
|
|
@@ -76,7 +83,7 @@ import {
|
|
|
76
83
|
} from "./workspace.js";
|
|
77
84
|
|
|
78
85
|
export const HTTP_CHANNEL_ID = "http";
|
|
79
|
-
export
|
|
86
|
+
export { SCHEDULE_CHANNEL_PREFIX } from "./prompt-context.js";
|
|
80
87
|
|
|
81
88
|
export class SessionBusyError extends Error {
|
|
82
89
|
constructor(sessionId: string) {
|
|
@@ -157,6 +164,7 @@ export interface SessionEngineOptions {
|
|
|
157
164
|
stateRoot: string;
|
|
158
165
|
runner: AgentRunner;
|
|
159
166
|
logger?: (line: string) => void;
|
|
167
|
+
platforms?: HostPlatforms;
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
/**
|
|
@@ -192,6 +200,21 @@ export class SessionEngine {
|
|
|
192
200
|
readonly project: AgentProject;
|
|
193
201
|
readonly stateRoot: string;
|
|
194
202
|
readonly sessions: SessionStore;
|
|
203
|
+
/**
|
|
204
|
+
* Runtime for `agent/storage.ts` (`defineStorage`), when
|
|
205
|
+
* authored. All durable state changes funnel through it (session
|
|
206
|
+
* records, event chunks, A/B samples/snapshots); the eval run store
|
|
207
|
+
* attaches via {@link StorageCoordinator.asEvalRunPersistence} at
|
|
208
|
+
* serve start.
|
|
209
|
+
*/
|
|
210
|
+
readonly storage: StorageCoordinator | undefined;
|
|
211
|
+
/** In-flight lazy restores, deduped per channel + continuation key. */
|
|
212
|
+
private readonly restoreInFlight = new Map<
|
|
213
|
+
string,
|
|
214
|
+
Promise<SessionRecord | undefined>
|
|
215
|
+
>();
|
|
216
|
+
/** Once-per-process cache for the A/B snapshot backfill (see abSnapshot). */
|
|
217
|
+
private restoredAbSnapshot: Promise<ABSnapshot | undefined> | undefined;
|
|
195
218
|
|
|
196
219
|
private readonly runner: AgentRunner;
|
|
197
220
|
private readonly logger: (line: string) => void;
|
|
@@ -229,15 +252,26 @@ export class SessionEngine {
|
|
|
229
252
|
`[agent-serve] failed to persist events for ${sessionId}: ${describeError(error)}`
|
|
230
253
|
)
|
|
231
254
|
);
|
|
255
|
+
this.storage =
|
|
256
|
+
options.project.storage === undefined
|
|
257
|
+
? undefined
|
|
258
|
+
: new StorageCoordinator({
|
|
259
|
+
definition: options.project.storage,
|
|
260
|
+
agentName: options.project.name,
|
|
261
|
+
projectRoot: options.project.rootDir,
|
|
262
|
+
logger: this.logger,
|
|
263
|
+
});
|
|
264
|
+
// Domain-specific hooks win; the defineStorage sink is the fallback.
|
|
265
|
+
const persistSamples =
|
|
266
|
+
options.project.abConfig?.persistSamples ??
|
|
267
|
+
this.storage?.asABSamplePersistence();
|
|
232
268
|
this.abCollector = new ABCollector(
|
|
233
269
|
options.project.abs,
|
|
234
270
|
this.logger,
|
|
235
271
|
async (sessionId) => (await this.logs.get(sessionId)).snapshot(0),
|
|
236
272
|
{
|
|
237
273
|
projectRoot: options.project.rootDir,
|
|
238
|
-
|
|
239
|
-
? {}
|
|
240
|
-
: { persistSamples: options.project.abConfig.persistSamples }),
|
|
274
|
+
persistSamples,
|
|
241
275
|
}
|
|
242
276
|
);
|
|
243
277
|
// Peer and Cursor-account transports are symbolic until the serve host
|
|
@@ -248,7 +282,7 @@ export class SessionEngine {
|
|
|
248
282
|
(connection) => !isSymbolicConnectionTransport(connection.transport)
|
|
249
283
|
)
|
|
250
284
|
);
|
|
251
|
-
this.platforms = createHostPlatforms();
|
|
285
|
+
this.platforms = options.platforms ?? createHostPlatforms();
|
|
252
286
|
for (const channel of options.project.channels) {
|
|
253
287
|
this.channelsById.set(channel.id, channel.definition);
|
|
254
288
|
}
|
|
@@ -305,13 +339,21 @@ export class SessionEngine {
|
|
|
305
339
|
options: EngineSendOptions = {}
|
|
306
340
|
): Promise<ChannelSession> {
|
|
307
341
|
const auth = options.auth ?? null;
|
|
308
|
-
|
|
342
|
+
let existing =
|
|
309
343
|
options.continuationToken === undefined
|
|
310
344
|
? undefined
|
|
311
345
|
: await this.sessions.findByContinuation(
|
|
312
346
|
channelId,
|
|
313
347
|
options.continuationToken
|
|
314
348
|
);
|
|
349
|
+
if (existing === undefined && options.continuationToken !== undefined) {
|
|
350
|
+
// Local miss: the session may live only in the storage sink (it
|
|
351
|
+
// fell outside the startup restore window, or restore is lazy-only).
|
|
352
|
+
existing = await this.restoreSessionByContinuation(
|
|
353
|
+
channelId,
|
|
354
|
+
options.continuationToken
|
|
355
|
+
);
|
|
356
|
+
}
|
|
315
357
|
|
|
316
358
|
if (existing !== undefined) {
|
|
317
359
|
if (!samePrincipal(existing.auth, auth)) {
|
|
@@ -361,6 +403,7 @@ export class SessionEngine {
|
|
|
361
403
|
updatedAt: now,
|
|
362
404
|
};
|
|
363
405
|
await this.sessions.save(record);
|
|
406
|
+
this.storage?.sessionRecord(record);
|
|
364
407
|
await this.appendEvent(record.sessionId, {
|
|
365
408
|
type: "session.started",
|
|
366
409
|
data: { channelId },
|
|
@@ -707,7 +750,10 @@ export class SessionEngine {
|
|
|
707
750
|
// events while sibling parallel tools are still executing.
|
|
708
751
|
if (
|
|
709
752
|
update.type === "tool-call-started" ||
|
|
710
|
-
update.type === "tool-call-completed"
|
|
753
|
+
update.type === "tool-call-completed" ||
|
|
754
|
+
// Nested subagent progress (newer SDKs); string check keeps
|
|
755
|
+
// typecheck green against older published `@cursor/sdk` pins.
|
|
756
|
+
(update as { type: string }).type === "tool-call-delta"
|
|
711
757
|
) {
|
|
712
758
|
await yieldEventLoop();
|
|
713
759
|
}
|
|
@@ -810,14 +856,24 @@ export class SessionEngine {
|
|
|
810
856
|
})
|
|
811
857
|
: undefined;
|
|
812
858
|
let turnPrompt = prompt;
|
|
859
|
+
if (isFirstTurn) {
|
|
860
|
+
const contextBlock = buildSessionContextBlock({
|
|
861
|
+
sessionId: record.sessionId,
|
|
862
|
+
channelId: record.channelId,
|
|
863
|
+
channelKind: this.channelsById.get(record.channelId)?.meta?.kind,
|
|
864
|
+
environment: isDevMode() ? "local dev server" : "hosted deployment",
|
|
865
|
+
});
|
|
866
|
+
turnPrompt = `${contextBlock}\n\n${prompt}`;
|
|
867
|
+
}
|
|
813
868
|
if (runtime === "cloud" && isFirstTurn) {
|
|
814
869
|
const preamble = [
|
|
815
870
|
agent.instructions?.trim() ?? "",
|
|
871
|
+
buildIdentitySection(agent),
|
|
816
872
|
buildAgentToolsCatalog(agent.tools, { includeScripts: true }),
|
|
817
873
|
]
|
|
818
874
|
.filter((part) => part !== "")
|
|
819
875
|
.join("\n\n");
|
|
820
|
-
turnPrompt =
|
|
876
|
+
turnPrompt = `${preamble}\n\n${turnPrompt}`;
|
|
821
877
|
}
|
|
822
878
|
return {
|
|
823
879
|
sessionId: record.sessionId,
|
|
@@ -1337,6 +1393,9 @@ export class SessionEngine {
|
|
|
1337
1393
|
}
|
|
1338
1394
|
|
|
1339
1395
|
private queueDispatch(sessionId: string, event: SessionEvent): void {
|
|
1396
|
+
// Single funnel for both append paths (appendEvent + the held-log emit),
|
|
1397
|
+
// so the storage sink sees every durable event exactly once.
|
|
1398
|
+
this.storage?.event(event);
|
|
1340
1399
|
const chain = this.dispatchChains.get(sessionId) ?? Promise.resolve();
|
|
1341
1400
|
const next = chain
|
|
1342
1401
|
.then(() => this.dispatchEvent(sessionId, event))
|
|
@@ -1344,6 +1403,12 @@ export class SessionEngine {
|
|
|
1344
1403
|
this.logger(
|
|
1345
1404
|
`[agent-serve] event dispatch failed for ${sessionId}: ${describeError(error)}`
|
|
1346
1405
|
);
|
|
1406
|
+
})
|
|
1407
|
+
.then(() => {
|
|
1408
|
+
// Turn-end flush happens here — after the boundary event's handlers
|
|
1409
|
+
// and their record updates settled — so the persisted record carries
|
|
1410
|
+
// the turn's final channel state and continuation token.
|
|
1411
|
+
this.storage?.eventDispatched(event);
|
|
1347
1412
|
});
|
|
1348
1413
|
this.dispatchChains.set(sessionId, next);
|
|
1349
1414
|
}
|
|
@@ -1508,6 +1573,25 @@ export class SessionEngine {
|
|
|
1508
1573
|
abConfig?.maxPlaygroundSessions
|
|
1509
1574
|
);
|
|
1510
1575
|
const foldedSessions = sessions.slice(0, maxPlaygroundSessions);
|
|
1576
|
+
// Backfill for a replacement host with nothing local to fold (e.g.
|
|
1577
|
+
// lazy-only restore before any session resumed): serve the persisted
|
|
1578
|
+
// aggregate instead of an empty surface. Gated on the host having no
|
|
1579
|
+
// sessions AT ALL — the persisted snapshot spans every principal, so a
|
|
1580
|
+
// caller who merely owns no sessions must not see it. Loaded once per
|
|
1581
|
+
// process (playground polls this route ~every 4s); skip the persist
|
|
1582
|
+
// side effect below — re-saving a restored snapshot would echo stale
|
|
1583
|
+
// data back into the store.
|
|
1584
|
+
if (
|
|
1585
|
+
foldedSessions.length === 0 &&
|
|
1586
|
+
this.storage !== undefined &&
|
|
1587
|
+
(await this.sessions.list()).length === 0
|
|
1588
|
+
) {
|
|
1589
|
+
this.restoredAbSnapshot ??= this.storage.getLatestAbSnapshot();
|
|
1590
|
+
const restoredSnapshot = await this.restoredAbSnapshot;
|
|
1591
|
+
if (restoredSnapshot !== undefined) {
|
|
1592
|
+
return restoredSnapshot;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1511
1595
|
const snapshot = await buildABSnapshot({
|
|
1512
1596
|
experiments: this.project.abs,
|
|
1513
1597
|
agentName: this.project.name,
|
|
@@ -1535,6 +1619,9 @@ export class SessionEngine {
|
|
|
1535
1619
|
`[agent-serve] ab persistSnapshots.save threw: ${describeError(error)}`
|
|
1536
1620
|
);
|
|
1537
1621
|
}
|
|
1622
|
+
} else {
|
|
1623
|
+
// defineStorage fallback (throttled inside the coordinator).
|
|
1624
|
+
this.storage?.abSnapshot(snapshot);
|
|
1538
1625
|
}
|
|
1539
1626
|
return snapshot;
|
|
1540
1627
|
}
|
|
@@ -1700,6 +1787,7 @@ export class SessionEngine {
|
|
|
1700
1787
|
updatedAt: new Date().toISOString(),
|
|
1701
1788
|
};
|
|
1702
1789
|
await this.sessions.save(updated);
|
|
1790
|
+
this.storage?.sessionRecord(updated);
|
|
1703
1791
|
return updated;
|
|
1704
1792
|
});
|
|
1705
1793
|
this.updateChains.set(
|
|
@@ -1713,6 +1801,203 @@ export class SessionEngine {
|
|
|
1713
1801
|
// Lifecycle
|
|
1714
1802
|
// ==========================================================================
|
|
1715
1803
|
|
|
1804
|
+
/**
|
|
1805
|
+
* Restore sessions from the authored storage sink. Called once at
|
|
1806
|
+
* serve start, before channels accept traffic: any persisted session
|
|
1807
|
+
* missing from the local `--state-root` is materialized there — events
|
|
1808
|
+
* as `events.ndjson`, record as `session.json` — after which every
|
|
1809
|
+
* existing read path (continuation lookup, event replay/streams, A/B
|
|
1810
|
+
* fold hydration) works unchanged. Local state always wins over the
|
|
1811
|
+
* sink, so restarts on the same host are no-ops.
|
|
1812
|
+
*
|
|
1813
|
+
* Failures are logged and skipped; a broken store (including rows that
|
|
1814
|
+
* violate the SessionRecord shape) never blocks serve start.
|
|
1815
|
+
*/
|
|
1816
|
+
async restoreStoredSessions(): Promise<void> {
|
|
1817
|
+
const storage = this.storage;
|
|
1818
|
+
if (storage === undefined || !storage.canBulkRestore) {
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const policy = storage.policy.restore;
|
|
1822
|
+
if (policy === "off") {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
try {
|
|
1826
|
+
await this.runBulkRestore(storage, policy);
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
this.logger(
|
|
1829
|
+
`[agent-serve] session restore failed: ${describeError(error)}`
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
private async runBulkRestore(
|
|
1835
|
+
storage: StorageCoordinator,
|
|
1836
|
+
policy: { maxSessions: number; maxAgeMs: number; maxTotalBytes: number }
|
|
1837
|
+
): Promise<void> {
|
|
1838
|
+
const loaded = await storage.listSessions();
|
|
1839
|
+
// Guardrails on whatever the sink returned: drop invalid rows and
|
|
1840
|
+
// sessions older than the age window, then restore newest-first under
|
|
1841
|
+
// the count cap. Anything filtered out here stays reachable through
|
|
1842
|
+
// the lazy continuation-lookup path.
|
|
1843
|
+
const cutoff = Date.now() - policy.maxAgeMs;
|
|
1844
|
+
const candidates = loaded
|
|
1845
|
+
.map((record) => ({
|
|
1846
|
+
record,
|
|
1847
|
+
// Numeric timestamp for both the age filter and the sort; a
|
|
1848
|
+
// non-string updatedAt (e.g. a raw DB Date from an untyped sink)
|
|
1849
|
+
// parses to NaN and is dropped instead of crashing serve start.
|
|
1850
|
+
updatedAt:
|
|
1851
|
+
typeof record?.updatedAt === "string"
|
|
1852
|
+
? Date.parse(record.updatedAt)
|
|
1853
|
+
: Number.NaN,
|
|
1854
|
+
}))
|
|
1855
|
+
.filter(
|
|
1856
|
+
({ record, updatedAt }) =>
|
|
1857
|
+
typeof record?.sessionId === "string" &&
|
|
1858
|
+
record.sessionId !== "" &&
|
|
1859
|
+
updatedAt >= cutoff
|
|
1860
|
+
)
|
|
1861
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
1862
|
+
.slice(0, policy.maxSessions)
|
|
1863
|
+
.map(({ record }) => record);
|
|
1864
|
+
|
|
1865
|
+
let restored = 0;
|
|
1866
|
+
let budget = policy.maxTotalBytes;
|
|
1867
|
+
// Sink round trips dominate restore latency, so event streams are
|
|
1868
|
+
// fetched with bounded parallelism; writes stay sequential, newest
|
|
1869
|
+
// first, so the byte budget is enforced deterministically.
|
|
1870
|
+
outer: for (
|
|
1871
|
+
let i = 0;
|
|
1872
|
+
i < candidates.length;
|
|
1873
|
+
i += RESTORE_FETCH_CONCURRENCY
|
|
1874
|
+
) {
|
|
1875
|
+
const chunk = candidates.slice(i, i + RESTORE_FETCH_CONCURRENCY);
|
|
1876
|
+
const fetched = await Promise.all(
|
|
1877
|
+
chunk.map(async (record) => {
|
|
1878
|
+
try {
|
|
1879
|
+
// Local state always wins over the sink.
|
|
1880
|
+
if ((await this.sessions.get(record.sessionId)) !== undefined) {
|
|
1881
|
+
return undefined;
|
|
1882
|
+
}
|
|
1883
|
+
const events = await storage.listSessionEvents(record.sessionId);
|
|
1884
|
+
return { record, events };
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
this.logger(
|
|
1887
|
+
`[agent-serve] failed to restore session ${record.sessionId}: ${describeError(error)}`
|
|
1888
|
+
);
|
|
1889
|
+
return undefined;
|
|
1890
|
+
}
|
|
1891
|
+
})
|
|
1892
|
+
);
|
|
1893
|
+
for (const item of fetched) {
|
|
1894
|
+
if (item === undefined) {
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
const ndjson = eventsNdjson(item.events);
|
|
1898
|
+
// Budget is checked BEFORE anything is written: one oversized
|
|
1899
|
+
// session cannot blow past maxTotalBytes on disk. Skipped sessions
|
|
1900
|
+
// stay reachable through the lazy path.
|
|
1901
|
+
const bytes =
|
|
1902
|
+
Buffer.byteLength(JSON.stringify(item.record), "utf8") +
|
|
1903
|
+
(ndjson === undefined ? 0 : Buffer.byteLength(ndjson, "utf8"));
|
|
1904
|
+
if (bytes > budget) {
|
|
1905
|
+
this.logger(
|
|
1906
|
+
`[agent-serve] session restore stopped at the ${policy.maxTotalBytes}-byte budget after ${restored} session(s); remaining sessions restore lazily on resume`
|
|
1907
|
+
);
|
|
1908
|
+
break outer;
|
|
1909
|
+
}
|
|
1910
|
+
try {
|
|
1911
|
+
await this.materializeSession(item.record, ndjson);
|
|
1912
|
+
budget -= bytes;
|
|
1913
|
+
restored += 1;
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
this.logger(
|
|
1916
|
+
`[agent-serve] failed to restore session ${item.record.sessionId}: ${describeError(error)}`
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (restored > 0) {
|
|
1922
|
+
this.logger(`[agent-serve] restored ${restored} session(s) from storage`);
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/**
|
|
1927
|
+
* Lazy restore: a continuation lookup missed locally, so ask the sink
|
|
1928
|
+
* for that one session and materialize it before resuming. Disk grows
|
|
1929
|
+
* with sessions that are actually resumed, not with history.
|
|
1930
|
+
*
|
|
1931
|
+
* Concurrent misses for the same key (webhook redeliveries, retries)
|
|
1932
|
+
* share one in-flight restore — a second materialization racing the
|
|
1933
|
+
* first resumed turn could truncate events the turn already appended.
|
|
1934
|
+
* Sink errors propagate and fail the send (see
|
|
1935
|
+
* {@link StorageCoordinator.getSessionByContinuation}).
|
|
1936
|
+
*/
|
|
1937
|
+
private restoreSessionByContinuation(
|
|
1938
|
+
channelId: string,
|
|
1939
|
+
continuationKey: string
|
|
1940
|
+
): Promise<SessionRecord | undefined> {
|
|
1941
|
+
const storage = this.storage;
|
|
1942
|
+
if (storage === undefined) {
|
|
1943
|
+
return Promise.resolve(undefined);
|
|
1944
|
+
}
|
|
1945
|
+
const key = `${channelId}\u0000${continuationKey}`;
|
|
1946
|
+
let pending = this.restoreInFlight.get(key);
|
|
1947
|
+
if (pending === undefined) {
|
|
1948
|
+
pending = this.loadAndMaterializeByContinuation(
|
|
1949
|
+
storage,
|
|
1950
|
+
channelId,
|
|
1951
|
+
continuationKey
|
|
1952
|
+
).finally(() => this.restoreInFlight.delete(key));
|
|
1953
|
+
this.restoreInFlight.set(key, pending);
|
|
1954
|
+
}
|
|
1955
|
+
return pending;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
private async loadAndMaterializeByContinuation(
|
|
1959
|
+
storage: StorageCoordinator,
|
|
1960
|
+
channelId: string,
|
|
1961
|
+
continuationKey: string
|
|
1962
|
+
): Promise<SessionRecord | undefined> {
|
|
1963
|
+
const record = await storage.getSessionByContinuation(
|
|
1964
|
+
channelId,
|
|
1965
|
+
continuationKey
|
|
1966
|
+
);
|
|
1967
|
+
if (record === undefined) {
|
|
1968
|
+
// Definitive miss: the caller starts a fresh session under this token.
|
|
1969
|
+
return undefined;
|
|
1970
|
+
}
|
|
1971
|
+
const local = await this.sessions.get(record.sessionId);
|
|
1972
|
+
if (local !== undefined) {
|
|
1973
|
+
return local;
|
|
1974
|
+
}
|
|
1975
|
+
const events = await storage.listSessionEvents(record.sessionId);
|
|
1976
|
+
await this.materializeSession(record, eventsNdjson(events));
|
|
1977
|
+
this.logger(
|
|
1978
|
+
`[agent-serve] lazily restored session ${record.sessionId} from storage`
|
|
1979
|
+
);
|
|
1980
|
+
return record;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* Write one restored session to the local state root. Events land on
|
|
1985
|
+
* disk before the record: a session becomes visible (and addressable via
|
|
1986
|
+
* its continuation key) only with its full stream in place, and the
|
|
1987
|
+
* lazily opened event log hydrates from this file.
|
|
1988
|
+
*/
|
|
1989
|
+
private async materializeSession(
|
|
1990
|
+
record: SessionRecord,
|
|
1991
|
+
eventsNdjson: string | undefined
|
|
1992
|
+
): Promise<void> {
|
|
1993
|
+
if (eventsNdjson !== undefined) {
|
|
1994
|
+
const filePath = this.sessions.eventFilePath(record.sessionId);
|
|
1995
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1996
|
+
await writeFile(filePath, eventsNdjson, "utf8");
|
|
1997
|
+
}
|
|
1998
|
+
await this.sessions.save(record);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1716
2001
|
trackBackground(promise: Promise<unknown>): void {
|
|
1717
2002
|
this.backgroundWork.add(promise);
|
|
1718
2003
|
void promise
|
|
@@ -1730,11 +2015,23 @@ export class SessionEngine {
|
|
|
1730
2015
|
await Promise.allSettled([...this.dispatchChains.values()]);
|
|
1731
2016
|
await Promise.allSettled([...this.updateChains.values()]);
|
|
1732
2017
|
await this.logs.flushAll();
|
|
2018
|
+
await this.storage?.close();
|
|
1733
2019
|
await this.mcpHost.close();
|
|
1734
2020
|
await this.runner.dispose?.();
|
|
1735
2021
|
}
|
|
1736
2022
|
}
|
|
1737
2023
|
|
|
2024
|
+
/** Parallel event-stream loads during the startup bulk restore. */
|
|
2025
|
+
const RESTORE_FETCH_CONCURRENCY = 8;
|
|
2026
|
+
|
|
2027
|
+
/** NDJSON payload for a restored event stream; undefined when empty. */
|
|
2028
|
+
function eventsNdjson(events: SessionEvent[]): string | undefined {
|
|
2029
|
+
if (events.length === 0) {
|
|
2030
|
+
return undefined;
|
|
2031
|
+
}
|
|
2032
|
+
return `${events.map((event) => JSON.stringify(event)).join("\n")}\n`;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1738
2035
|
function subagentPrompt(subagent: ResolvedAgent): string {
|
|
1739
2036
|
if (subagent.instructions !== undefined && subagent.instructions !== "") {
|
|
1740
2037
|
return subagent.instructions;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session list / event fetch for CLI (`sessions`, `session`, chat reattach).
|
|
3
|
+
*
|
|
4
|
+
* Customer-facing errors never include hosting URLs or infra jargon.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SessionEvent, SessionSummary } from "../types.js";
|
|
8
|
+
import { sanitizeCustomerError } from "./logs-client.js";
|
|
9
|
+
import { mergeRequestHeaders, type RequestAuth } from "./request-headers.js";
|
|
10
|
+
|
|
11
|
+
export async function listSessions(args: {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
auth?: RequestAuth;
|
|
14
|
+
fetchImpl?: typeof fetch;
|
|
15
|
+
}): Promise<SessionSummary[]> {
|
|
16
|
+
const baseUrl = args.baseUrl.replace(/\/$/, "");
|
|
17
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
18
|
+
let resp: Response;
|
|
19
|
+
try {
|
|
20
|
+
resp = await fetchImpl(`${baseUrl}/v1/sessions`, {
|
|
21
|
+
headers: mergeRequestHeaders(args.auth),
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
sanitizeCustomerError(
|
|
26
|
+
error instanceof Error ? error.message : "Could not list sessions."
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
if (!resp.ok) {
|
|
31
|
+
throw new Error(`Could not list sessions (HTTP ${resp.status}).`);
|
|
32
|
+
}
|
|
33
|
+
const body = (await resp.json()) as { sessions?: unknown };
|
|
34
|
+
if (!Array.isArray(body.sessions)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const out: SessionSummary[] = [];
|
|
38
|
+
for (const row of body.sessions) {
|
|
39
|
+
const parsed = parseSessionSummary(row);
|
|
40
|
+
if (parsed !== undefined) {
|
|
41
|
+
out.push(parsed);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function fetchSessionEvents(args: {
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
sessionId: string;
|
|
50
|
+
auth?: RequestAuth;
|
|
51
|
+
fetchImpl?: typeof fetch;
|
|
52
|
+
}): Promise<SessionEvent[]> {
|
|
53
|
+
const baseUrl = args.baseUrl.replace(/\/$/, "");
|
|
54
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
55
|
+
const path = `/v1/session/${encodeURIComponent(args.sessionId)}/events`;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const jsonResp = await fetchImpl(`${baseUrl}${path}?format=json`, {
|
|
59
|
+
headers: mergeRequestHeaders(args.auth),
|
|
60
|
+
});
|
|
61
|
+
if (jsonResp.ok) {
|
|
62
|
+
const body = (await jsonResp.json()) as { events?: unknown };
|
|
63
|
+
if (Array.isArray(body.events)) {
|
|
64
|
+
return body.events as SessionEvent[];
|
|
65
|
+
}
|
|
66
|
+
} else if (jsonResp.status === 404) {
|
|
67
|
+
throw new Error(`Session ${args.sessionId} not found.`);
|
|
68
|
+
} else if (jsonResp.status === 403) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Session ${args.sessionId} is not readable by this caller.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof Error && error.message.startsWith("Session ")) {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
// Fall through to NDJSON.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const resp = await fetchImpl(`${baseUrl}${path}`, {
|
|
82
|
+
headers: mergeRequestHeaders(args.auth),
|
|
83
|
+
});
|
|
84
|
+
if (resp.status === 404) {
|
|
85
|
+
throw new Error(`Session ${args.sessionId} not found.`);
|
|
86
|
+
}
|
|
87
|
+
if (resp.status === 403) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Session ${args.sessionId} is not readable by this caller.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (!resp.ok) {
|
|
93
|
+
throw new Error(`Could not fetch session events (HTTP ${resp.status}).`);
|
|
94
|
+
}
|
|
95
|
+
const text = await resp.text();
|
|
96
|
+
const events: SessionEvent[] = [];
|
|
97
|
+
for (const line of text.split("\n")) {
|
|
98
|
+
if (line.trim() === "") {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
events.push(JSON.parse(line) as SessionEvent);
|
|
103
|
+
} catch {
|
|
104
|
+
// skip malformed lines
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return events;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (error instanceof Error && error.message.startsWith("Session ")) {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
if (error instanceof Error && error.message.startsWith("Could not fetch")) {
|
|
113
|
+
throw new Error(sanitizeCustomerError(error.message));
|
|
114
|
+
}
|
|
115
|
+
throw new Error(
|
|
116
|
+
sanitizeCustomerError(
|
|
117
|
+
error instanceof Error
|
|
118
|
+
? error.message
|
|
119
|
+
: "Could not fetch session events."
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Lenient parse so partial stubs / older servers still list by sessionId. */
|
|
126
|
+
function parseSessionSummary(value: unknown): SessionSummary | undefined {
|
|
127
|
+
if (typeof value !== "object" || value === null) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const row = value as Record<string, unknown>;
|
|
131
|
+
if (typeof row.sessionId !== "string" || row.sessionId === "") {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const continuationToken =
|
|
135
|
+
typeof row.continuationToken === "string"
|
|
136
|
+
? row.continuationToken
|
|
137
|
+
: row.continuationToken === null
|
|
138
|
+
? null
|
|
139
|
+
: null;
|
|
140
|
+
return {
|
|
141
|
+
sessionId: row.sessionId,
|
|
142
|
+
channelId: typeof row.channelId === "string" ? row.channelId : "",
|
|
143
|
+
mode: row.mode === "task" ? "task" : "chat",
|
|
144
|
+
title: typeof row.title === "string" ? row.title : undefined,
|
|
145
|
+
turnCount: typeof row.turnCount === "number" ? row.turnCount : 0,
|
|
146
|
+
running: row.running === true,
|
|
147
|
+
createdAt: typeof row.createdAt === "string" ? row.createdAt : "",
|
|
148
|
+
updatedAt: typeof row.updatedAt === "string" ? row.updatedAt : "",
|
|
149
|
+
continuationToken,
|
|
150
|
+
externalUrl:
|
|
151
|
+
typeof row.externalUrl === "string" ? row.externalUrl : undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Format a sessions table (id, channel, mode, turns, running, updated). */
|
|
156
|
+
export function formatSessionsTable(sessions: SessionSummary[]): string {
|
|
157
|
+
if (sessions.length === 0) {
|
|
158
|
+
return "No sessions.\n";
|
|
159
|
+
}
|
|
160
|
+
const header = ["SESSION", "CHANNEL", "MODE", "TURNS", "RUNNING", "UPDATED"];
|
|
161
|
+
const rows = sessions.map((s) => [
|
|
162
|
+
s.sessionId,
|
|
163
|
+
s.channelId,
|
|
164
|
+
s.mode,
|
|
165
|
+
String(s.turnCount),
|
|
166
|
+
s.running ? "yes" : "no",
|
|
167
|
+
s.updatedAt,
|
|
168
|
+
]);
|
|
169
|
+
return `${renderTable(header, rows)}\n`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderTable(header: string[], rows: string[][]): string {
|
|
173
|
+
const widths = header.map((h, i) =>
|
|
174
|
+
Math.max(h.length, ...rows.map((row) => (row[i] ?? "").length))
|
|
175
|
+
);
|
|
176
|
+
const renderRow = (row: string[]): string =>
|
|
177
|
+
row
|
|
178
|
+
.map((cell, i) => cell.padEnd(widths[i] ?? 0))
|
|
179
|
+
.join(" ")
|
|
180
|
+
.trimEnd();
|
|
181
|
+
return [renderRow(header), ...rows.map(renderRow)].join("\n");
|
|
182
|
+
}
|