@cursor/july 0.1.1
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 +290 -0
- package/LICENSE.md +3 -0
- package/README.md +1233 -0
- package/dist/ab.d.ts +290 -0
- package/dist/ab.d.ts.map +1 -0
- package/dist/ab.js +386 -0
- package/dist/bin/agent-serve.d.ts +15 -0
- package/dist/bin/agent-serve.d.ts.map +1 -0
- package/dist/bin/agent-serve.js +754 -0
- package/dist/channels/github/api.d.ts +101 -0
- package/dist/channels/github/api.d.ts.map +1 -0
- package/dist/channels/github/api.js +229 -0
- package/dist/channels/github/auth.d.ts +67 -0
- package/dist/channels/github/auth.d.ts.map +1 -0
- package/dist/channels/github/auth.js +239 -0
- package/dist/channels/github/binding.d.ts +73 -0
- package/dist/channels/github/binding.d.ts.map +1 -0
- package/dist/channels/github/binding.js +114 -0
- package/dist/channels/github/defaults.d.ts +24 -0
- package/dist/channels/github/defaults.d.ts.map +1 -0
- package/dist/channels/github/defaults.js +100 -0
- package/dist/channels/github/events.d.ts +18 -0
- package/dist/channels/github/events.d.ts.map +1 -0
- package/dist/channels/github/events.js +59 -0
- package/dist/channels/github/forward.d.ts +137 -0
- package/dist/channels/github/forward.d.ts.map +1 -0
- package/dist/channels/github/forward.js +271 -0
- package/dist/channels/github/github-channel.d.ts +127 -0
- package/dist/channels/github/github-channel.d.ts.map +1 -0
- package/dist/channels/github/github-channel.js +1688 -0
- package/dist/channels/github/index.d.ts +52 -0
- package/dist/channels/github/index.d.ts.map +1 -0
- package/dist/channels/github/index.js +51 -0
- package/dist/channels/github/replay.d.ts +115 -0
- package/dist/channels/github/replay.d.ts.map +1 -0
- package/dist/channels/github/replay.js +282 -0
- package/dist/channels/github/state.d.ts +26 -0
- package/dist/channels/github/state.d.ts.map +1 -0
- package/dist/channels/github/state.js +50 -0
- package/dist/channels/github/types.d.ts +385 -0
- package/dist/channels/github/types.d.ts.map +1 -0
- package/dist/channels/github/types.js +1 -0
- package/dist/channels/slack/api.d.ts +117 -0
- package/dist/channels/slack/api.d.ts.map +1 -0
- package/dist/channels/slack/api.js +437 -0
- package/dist/channels/slack/approvals.d.ts +32 -0
- package/dist/channels/slack/approvals.d.ts.map +1 -0
- package/dist/channels/slack/approvals.js +157 -0
- package/dist/channels/slack/assistant.d.ts +23 -0
- package/dist/channels/slack/assistant.d.ts.map +1 -0
- package/dist/channels/slack/assistant.js +67 -0
- package/dist/channels/slack/auth.d.ts +40 -0
- package/dist/channels/slack/auth.d.ts.map +1 -0
- package/dist/channels/slack/auth.js +71 -0
- package/dist/channels/slack/channel-watch.d.ts +46 -0
- package/dist/channels/slack/channel-watch.d.ts.map +1 -0
- package/dist/channels/slack/channel-watch.js +180 -0
- package/dist/channels/slack/constants.d.ts +17 -0
- package/dist/channels/slack/constants.d.ts.map +1 -0
- package/dist/channels/slack/constants.js +27 -0
- package/dist/channels/slack/credentials.d.ts +35 -0
- package/dist/channels/slack/credentials.d.ts.map +1 -0
- package/dist/channels/slack/credentials.js +88 -0
- package/dist/channels/slack/cursor-account.d.ts +76 -0
- package/dist/channels/slack/cursor-account.d.ts.map +1 -0
- package/dist/channels/slack/cursor-account.js +88 -0
- package/dist/channels/slack/defaults.d.ts +32 -0
- package/dist/channels/slack/defaults.d.ts.map +1 -0
- package/dist/channels/slack/defaults.js +380 -0
- package/dist/channels/slack/dispatch.d.ts +43 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -0
- package/dist/channels/slack/dispatch.js +185 -0
- package/dist/channels/slack/doctor.d.ts +24 -0
- package/dist/channels/slack/doctor.d.ts.map +1 -0
- package/dist/channels/slack/doctor.js +115 -0
- package/dist/channels/slack/event-id-dedupe.d.ts +10 -0
- package/dist/channels/slack/event-id-dedupe.d.ts.map +1 -0
- package/dist/channels/slack/event-id-dedupe.js +23 -0
- package/dist/channels/slack/inbound.d.ts +41 -0
- package/dist/channels/slack/inbound.d.ts.map +1 -0
- package/dist/channels/slack/inbound.js +166 -0
- package/dist/channels/slack/index.d.ts +45 -0
- package/dist/channels/slack/index.d.ts.map +1 -0
- package/dist/channels/slack/index.js +44 -0
- package/dist/channels/slack/init.d.ts +32 -0
- package/dist/channels/slack/init.d.ts.map +1 -0
- package/dist/channels/slack/init.js +82 -0
- package/dist/channels/slack/interactive.d.ts +19 -0
- package/dist/channels/slack/interactive.d.ts.map +1 -0
- package/dist/channels/slack/interactive.js +142 -0
- package/dist/channels/slack/links.d.ts +10 -0
- package/dist/channels/slack/links.d.ts.map +1 -0
- package/dist/channels/slack/links.js +19 -0
- package/dist/channels/slack/live-delivery.d.ts +75 -0
- package/dist/channels/slack/live-delivery.d.ts.map +1 -0
- package/dist/channels/slack/live-delivery.js +143 -0
- package/dist/channels/slack/log.d.ts +25 -0
- package/dist/channels/slack/log.d.ts.map +1 -0
- package/dist/channels/slack/log.js +47 -0
- package/dist/channels/slack/manifest.d.ts +57 -0
- package/dist/channels/slack/manifest.d.ts.map +1 -0
- package/dist/channels/slack/manifest.js +110 -0
- package/dist/channels/slack/pr-ref.d.ts +18 -0
- package/dist/channels/slack/pr-ref.d.ts.map +1 -0
- package/dist/channels/slack/pr-ref.js +36 -0
- package/dist/channels/slack/setup.d.ts +14 -0
- package/dist/channels/slack/setup.d.ts.map +1 -0
- package/dist/channels/slack/setup.js +86 -0
- package/dist/channels/slack/slack-channel.d.ts +15 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -0
- package/dist/channels/slack/slack-channel.js +281 -0
- package/dist/channels/slack/socket-mode.d.ts +26 -0
- package/dist/channels/slack/socket-mode.d.ts.map +1 -0
- package/dist/channels/slack/socket-mode.js +116 -0
- package/dist/channels/slack/stream.d.ts +26 -0
- package/dist/channels/slack/stream.d.ts.map +1 -0
- package/dist/channels/slack/stream.js +39 -0
- package/dist/channels/slack/tool-approval-events.d.ts +33 -0
- package/dist/channels/slack/tool-approval-events.d.ts.map +1 -0
- package/dist/channels/slack/tool-approval-events.js +129 -0
- package/dist/channels/slack/types.d.ts +326 -0
- package/dist/channels/slack/types.d.ts.map +1 -0
- package/dist/channels/slack/types.js +4 -0
- package/dist/channels.d.ts +116 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +294 -0
- package/dist/connections.d.ts +75 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +75 -0
- package/dist/docs/404.html +22 -0
- package/dist/docs/ab.html +87 -0
- package/dist/docs/assets/ab.md.COdXkces.js +63 -0
- package/dist/docs/assets/ab.md.COdXkces.lean.js +1 -0
- package/dist/docs/assets/app.DqfFEmJd.js +1 -0
- package/dist/docs/assets/building-with-agents.md.CJCtZCyi.js +13 -0
- package/dist/docs/assets/building-with-agents.md.CJCtZCyi.lean.js +1 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.CcVk1uKq.js +1 -0
- package/dist/docs/assets/chunks/VPLocalSearchBox.BaLEdS15.js +9 -0
- package/dist/docs/assets/chunks/framework.CAZyNGu9.js +19 -0
- package/dist/docs/assets/chunks/theme.CZRvu_0q.js +2 -0
- package/dist/docs/assets/concepts.md.Cfb9b-k1.js +4 -0
- package/dist/docs/assets/concepts.md.Cfb9b-k1.lean.js +1 -0
- package/dist/docs/assets/deployment.md.Dx1TYNk5.js +14 -0
- package/dist/docs/assets/deployment.md.Dx1TYNk5.lean.js +1 -0
- package/dist/docs/assets/evals.md.DPZ_MAnI.js +85 -0
- package/dist/docs/assets/evals.md.DPZ_MAnI.lean.js +1 -0
- package/dist/docs/assets/guides_agent-to-agent.md.CrtrsySy.js +10 -0
- package/dist/docs/assets/guides_agent-to-agent.md.CrtrsySy.lean.js +1 -0
- package/dist/docs/assets/guides_cloud-runtime.md.CYlNMTNp.js +9 -0
- package/dist/docs/assets/guides_cloud-runtime.md.CYlNMTNp.lean.js +1 -0
- package/dist/docs/assets/guides_github.md.DwbKhCeS.js +20 -0
- package/dist/docs/assets/guides_github.md.DwbKhCeS.lean.js +1 -0
- package/dist/docs/assets/guides_human-in-the-loop.md.Dvuctx7s.js +28 -0
- package/dist/docs/assets/guides_human-in-the-loop.md.Dvuctx7s.lean.js +1 -0
- package/dist/docs/assets/guides_slack.md.bv41fHfW.js +37 -0
- package/dist/docs/assets/guides_slack.md.bv41fHfW.lean.js +1 -0
- package/dist/docs/assets/guides_webhooks.md.hFTik3lf.js +50 -0
- package/dist/docs/assets/guides_webhooks.md.hFTik3lf.lean.js +1 -0
- package/dist/docs/assets/hillclimbing.md.DHNast08.js +4 -0
- package/dist/docs/assets/hillclimbing.md.DHNast08.lean.js +1 -0
- package/dist/docs/assets/index.md.BPKcj5AI.js +20 -0
- package/dist/docs/assets/index.md.BPKcj5AI.lean.js +1 -0
- package/dist/docs/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/dist/docs/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/dist/docs/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/dist/docs/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/dist/docs/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/dist/docs/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/dist/docs/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/dist/docs/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/dist/docs/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/dist/docs/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/dist/docs/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/dist/docs/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/dist/docs/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/dist/docs/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/dist/docs/assets/quickstart.md.tVPiGK_L.js +35 -0
- package/dist/docs/assets/quickstart.md.tVPiGK_L.lean.js +1 -0
- package/dist/docs/assets/reference_agent-config.md.Bpd7HQwf.js +34 -0
- package/dist/docs/assets/reference_agent-config.md.Bpd7HQwf.lean.js +1 -0
- package/dist/docs/assets/reference_channels.md.D7JTR03W.js +53 -0
- package/dist/docs/assets/reference_channels.md.D7JTR03W.lean.js +1 -0
- package/dist/docs/assets/reference_cli.md.DA730zCu.js +33 -0
- package/dist/docs/assets/reference_cli.md.DA730zCu.lean.js +1 -0
- package/dist/docs/assets/reference_connections.md.C3vNH_DE.js +26 -0
- package/dist/docs/assets/reference_connections.md.C3vNH_DE.lean.js +1 -0
- package/dist/docs/assets/reference_hooks.md.BCEc3MyM.js +12 -0
- package/dist/docs/assets/reference_hooks.md.BCEc3MyM.lean.js +1 -0
- package/dist/docs/assets/reference_http-api.md.DBAahtdz.js +11 -0
- package/dist/docs/assets/reference_http-api.md.DBAahtdz.lean.js +1 -0
- package/dist/docs/assets/reference_instructions.md.BC05LEQ8.js +12 -0
- package/dist/docs/assets/reference_instructions.md.BC05LEQ8.lean.js +1 -0
- package/dist/docs/assets/reference_playground.md.CPZhfYaO.js +1 -0
- package/dist/docs/assets/reference_playground.md.CPZhfYaO.lean.js +1 -0
- package/dist/docs/assets/reference_project-layout.md.Bd_CKtNS.js +31 -0
- package/dist/docs/assets/reference_project-layout.md.Bd_CKtNS.lean.js +1 -0
- package/dist/docs/assets/reference_schedules.md.D7qijxLk.js +82 -0
- package/dist/docs/assets/reference_schedules.md.D7qijxLk.lean.js +1 -0
- package/dist/docs/assets/reference_sessions.md.DdjWabnN.js +8 -0
- package/dist/docs/assets/reference_sessions.md.DdjWabnN.lean.js +1 -0
- package/dist/docs/assets/reference_skills.md.VQnlBT3Q.js +15 -0
- package/dist/docs/assets/reference_skills.md.VQnlBT3Q.lean.js +1 -0
- package/dist/docs/assets/reference_subagents.md.CIRAVcPK.js +10 -0
- package/dist/docs/assets/reference_subagents.md.CIRAVcPK.lean.js +1 -0
- package/dist/docs/assets/reference_tools.md.DF5kwlt0.js +46 -0
- package/dist/docs/assets/reference_tools.md.DF5kwlt0.lean.js +1 -0
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.js +1 -0
- package/dist/docs/assets/scaffolding-agents.md.CyYfWGdc.lean.js +1 -0
- package/dist/docs/assets/style.Bj-IXa5W.css +1 -0
- package/dist/docs/assets/troubleshooting.md.DkovjS7h.js +1 -0
- package/dist/docs/assets/troubleshooting.md.DkovjS7h.lean.js +1 -0
- package/dist/docs/building-with-agents.html +37 -0
- package/dist/docs/concepts.html +28 -0
- package/dist/docs/deployment.html +38 -0
- package/dist/docs/evals.html +109 -0
- package/dist/docs/guides/agent-to-agent.html +34 -0
- package/dist/docs/guides/cloud-runtime.html +33 -0
- package/dist/docs/guides/github.html +44 -0
- package/dist/docs/guides/human-in-the-loop.html +52 -0
- package/dist/docs/guides/slack.html +61 -0
- package/dist/docs/guides/webhooks.html +74 -0
- package/dist/docs/hashmap.json +1 -0
- package/dist/docs/hillclimbing.html +28 -0
- package/dist/docs/index.html +44 -0
- package/dist/docs/quickstart.html +59 -0
- package/dist/docs/reference/agent-config.html +58 -0
- package/dist/docs/reference/channels.html +77 -0
- package/dist/docs/reference/cli.html +57 -0
- package/dist/docs/reference/connections.html +50 -0
- package/dist/docs/reference/hooks.html +36 -0
- package/dist/docs/reference/http-api.html +35 -0
- package/dist/docs/reference/instructions.html +36 -0
- package/dist/docs/reference/playground.html +25 -0
- package/dist/docs/reference/project-layout.html +55 -0
- package/dist/docs/reference/schedules.html +106 -0
- package/dist/docs/reference/sessions.html +32 -0
- package/dist/docs/reference/skills.html +39 -0
- package/dist/docs/reference/subagents.html +34 -0
- package/dist/docs/reference/tools.html +70 -0
- package/dist/docs/scaffolding-agents.html +25 -0
- package/dist/docs/troubleshooting.html +25 -0
- package/dist/docs/vp-icons.css +0 -0
- package/dist/evals.d.ts +312 -0
- package/dist/evals.d.ts.map +1 -0
- package/dist/evals.js +254 -0
- package/dist/hooks.d.ts +23 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +23 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/internal/ab-collector.d.ts +42 -0
- package/dist/internal/ab-collector.d.ts.map +1 -0
- package/dist/internal/ab-collector.js +153 -0
- package/dist/internal/ab-fold.d.ts +36 -0
- package/dist/internal/ab-fold.d.ts.map +1 -0
- package/dist/internal/ab-fold.js +171 -0
- package/dist/internal/ab-snapshot.d.ts +70 -0
- package/dist/internal/ab-snapshot.d.ts.map +1 -0
- package/dist/internal/ab-snapshot.js +208 -0
- package/dist/internal/approval-gate.d.ts +34 -0
- package/dist/internal/approval-gate.d.ts.map +1 -0
- package/dist/internal/approval-gate.js +85 -0
- package/dist/internal/auth.d.ts +8 -0
- package/dist/internal/auth.d.ts.map +1 -0
- package/dist/internal/auth.js +27 -0
- package/dist/internal/authored-alias-hooks.d.ts +31 -0
- package/dist/internal/authored-alias-hooks.d.ts.map +1 -0
- package/dist/internal/authored-alias-hooks.js +43 -0
- package/dist/internal/authored-loaders.d.ts +25 -0
- package/dist/internal/authored-loaders.d.ts.map +1 -0
- package/dist/internal/authored-loaders.js +129 -0
- package/dist/internal/bounded-int.d.ts +11 -0
- package/dist/internal/bounded-int.d.ts.map +1 -0
- package/dist/internal/bounded-int.js +13 -0
- package/dist/internal/brand.d.ts +6 -0
- package/dist/internal/brand.d.ts.map +1 -0
- package/dist/internal/brand.js +17 -0
- package/dist/internal/chat-client.d.ts +19 -0
- package/dist/internal/chat-client.d.ts.map +1 -0
- package/dist/internal/chat-client.js +327 -0
- package/dist/internal/cli-ax.d.ts +58 -0
- package/dist/internal/cli-ax.d.ts.map +1 -0
- package/dist/internal/cli-ax.js +345 -0
- package/dist/internal/cli-cursor.d.ts +20 -0
- package/dist/internal/cli-cursor.d.ts.map +1 -0
- package/dist/internal/cli-cursor.js +227 -0
- package/dist/internal/cli-github.d.ts +47 -0
- package/dist/internal/cli-github.d.ts.map +1 -0
- package/dist/internal/cli-github.js +644 -0
- package/dist/internal/cli-slack.d.ts +18 -0
- package/dist/internal/cli-slack.d.ts.map +1 -0
- package/dist/internal/cli-slack.js +144 -0
- package/dist/internal/cloud-merge.d.ts +23 -0
- package/dist/internal/cloud-merge.d.ts.map +1 -0
- package/dist/internal/cloud-merge.js +28 -0
- package/dist/internal/cron.d.ts +17 -0
- package/dist/internal/cron.d.ts.map +1 -0
- package/dist/internal/cron.js +60 -0
- package/dist/internal/cursor/account-mcp.d.ts +95 -0
- package/dist/internal/cursor/account-mcp.d.ts.map +1 -0
- package/dist/internal/cursor/account-mcp.js +342 -0
- package/dist/internal/cursor/backend-client.d.ts +118 -0
- package/dist/internal/cursor/backend-client.d.ts.map +1 -0
- package/dist/internal/cursor/backend-client.js +477 -0
- package/dist/internal/cursor/credentials.d.ts +112 -0
- package/dist/internal/cursor/credentials.d.ts.map +1 -0
- package/dist/internal/cursor/credentials.js +329 -0
- package/dist/internal/cursor-event-relay.d.ts +111 -0
- package/dist/internal/cursor-event-relay.d.ts.map +1 -0
- package/dist/internal/cursor-event-relay.js +181 -0
- package/dist/internal/cursor-relay-core.d.ts +113 -0
- package/dist/internal/cursor-relay-core.d.ts.map +1 -0
- package/dist/internal/cursor-relay-core.js +364 -0
- package/dist/internal/cursor-slack-relay.d.ts +94 -0
- package/dist/internal/cursor-slack-relay.d.ts.map +1 -0
- package/dist/internal/cursor-slack-relay.js +171 -0
- package/dist/internal/describe-error.d.ts +3 -0
- package/dist/internal/describe-error.d.ts.map +1 -0
- package/dist/internal/describe-error.js +4 -0
- package/dist/internal/dev-mode.d.ts +13 -0
- package/dist/internal/dev-mode.d.ts.map +1 -0
- package/dist/internal/dev-mode.js +17 -0
- package/dist/internal/discovery.d.ts +55 -0
- package/dist/internal/discovery.d.ts.map +1 -0
- package/dist/internal/discovery.js +1427 -0
- package/dist/internal/distribution.d.ts +47 -0
- package/dist/internal/distribution.d.ts.map +1 -0
- package/dist/internal/distribution.js +82 -0
- package/dist/internal/docs-site.d.ts +28 -0
- package/dist/internal/docs-site.d.ts.map +1 -0
- package/dist/internal/docs-site.js +169 -0
- package/dist/internal/duration.d.ts +8 -0
- package/dist/internal/duration.d.ts.map +1 -0
- package/dist/internal/duration.js +35 -0
- package/dist/internal/eval-run-store.d.ts +81 -0
- package/dist/internal/eval-run-store.d.ts.map +1 -0
- package/dist/internal/eval-run-store.js +396 -0
- package/dist/internal/eval-runner.d.ts +76 -0
- package/dist/internal/eval-runner.d.ts.map +1 -0
- package/dist/internal/eval-runner.js +336 -0
- package/dist/internal/event-log.d.ts +51 -0
- package/dist/internal/event-log.d.ts.map +1 -0
- package/dist/internal/event-log.js +167 -0
- package/dist/internal/event-mapper.d.ts +46 -0
- package/dist/internal/event-mapper.d.ts.map +1 -0
- package/dist/internal/event-mapper.js +226 -0
- package/dist/internal/frontmatter.d.ts +12 -0
- package/dist/internal/frontmatter.d.ts.map +1 -0
- package/dist/internal/frontmatter.js +61 -0
- package/dist/internal/github-fanout.d.ts +32 -0
- package/dist/internal/github-fanout.d.ts.map +1 -0
- package/dist/internal/github-fanout.js +122 -0
- package/dist/internal/handleAgentServeTrigger.d.ts +59 -0
- package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -0
- package/dist/internal/handleAgentServeTrigger.js +115 -0
- package/dist/internal/host-platforms.d.ts +40 -0
- package/dist/internal/host-platforms.d.ts.map +1 -0
- package/dist/internal/host-platforms.js +113 -0
- package/dist/internal/http-channel.d.ts +54 -0
- package/dist/internal/http-channel.d.ts.map +1 -0
- package/dist/internal/http-channel.js +382 -0
- package/dist/internal/ids.d.ts +19 -0
- package/dist/internal/ids.d.ts.map +1 -0
- package/dist/internal/ids.js +34 -0
- package/dist/internal/init-project.d.ts +22 -0
- package/dist/internal/init-project.d.ts.map +1 -0
- package/dist/internal/init-project.js +125 -0
- package/dist/internal/json-dir-store.d.ts +32 -0
- package/dist/internal/json-dir-store.d.ts.map +1 -0
- package/dist/internal/json-dir-store.js +100 -0
- package/dist/internal/json-schema.d.ts +18 -0
- package/dist/internal/json-schema.d.ts.map +1 -0
- package/dist/internal/json-schema.js +53 -0
- package/dist/internal/log-ring.d.ts +23 -0
- package/dist/internal/log-ring.d.ts.map +1 -0
- package/dist/internal/log-ring.js +42 -0
- package/dist/internal/map-pool.d.ts +6 -0
- package/dist/internal/map-pool.d.ts.map +1 -0
- package/dist/internal/map-pool.js +41 -0
- package/dist/internal/mcp-endpoint.d.ts +59 -0
- package/dist/internal/mcp-endpoint.d.ts.map +1 -0
- package/dist/internal/mcp-endpoint.js +355 -0
- package/dist/internal/mcp-host.d.ts +28 -0
- package/dist/internal/mcp-host.d.ts.map +1 -0
- package/dist/internal/mcp-host.js +162 -0
- package/dist/internal/net.d.ts +5 -0
- package/dist/internal/net.d.ts.map +1 -0
- package/dist/internal/net.js +30 -0
- package/dist/internal/peer-connections.d.ts +44 -0
- package/dist/internal/peer-connections.d.ts.map +1 -0
- package/dist/internal/peer-connections.js +67 -0
- package/dist/internal/playground/static.d.ts +35 -0
- package/dist/internal/playground/static.d.ts.map +1 -0
- package/dist/internal/playground/static.js +162 -0
- package/dist/internal/playground/toolchain.d.ts +57 -0
- package/dist/internal/playground/toolchain.d.ts.map +1 -0
- package/dist/internal/playground/toolchain.js +226 -0
- package/dist/internal/reminder-runner.d.ts +62 -0
- package/dist/internal/reminder-runner.d.ts.map +1 -0
- package/dist/internal/reminder-runner.js +582 -0
- package/dist/internal/reminder-schedule.d.ts +22 -0
- package/dist/internal/reminder-schedule.d.ts.map +1 -0
- package/dist/internal/reminder-schedule.js +82 -0
- package/dist/internal/reminder-store.d.ts +58 -0
- package/dist/internal/reminder-store.d.ts.map +1 -0
- package/dist/internal/reminder-store.js +106 -0
- package/dist/internal/resolved-connections.d.ts +41 -0
- package/dist/internal/resolved-connections.d.ts.map +1 -0
- package/dist/internal/resolved-connections.js +73 -0
- package/dist/internal/review-comments.d.ts +50 -0
- package/dist/internal/review-comments.d.ts.map +1 -0
- package/dist/internal/review-comments.js +194 -0
- package/dist/internal/router.d.ts +33 -0
- package/dist/internal/router.d.ts.map +1 -0
- package/dist/internal/router.js +93 -0
- package/dist/internal/run-client.d.ts +49 -0
- package/dist/internal/run-client.d.ts.map +1 -0
- package/dist/internal/run-client.js +208 -0
- package/dist/internal/schedule-runner.d.ts +31 -0
- package/dist/internal/schedule-runner.d.ts.map +1 -0
- package/dist/internal/schedule-runner.js +126 -0
- package/dist/internal/sdk-runner.d.ts +41 -0
- package/dist/internal/sdk-runner.d.ts.map +1 -0
- package/dist/internal/sdk-runner.js +221 -0
- package/dist/internal/server.d.ts +25 -0
- package/dist/internal/server.d.ts.map +1 -0
- package/dist/internal/server.js +1374 -0
- package/dist/internal/session-engine.d.ts +287 -0
- package/dist/internal/session-engine.d.ts.map +1 -0
- package/dist/internal/session-engine.js +1377 -0
- package/dist/internal/session-store.d.ts +29 -0
- package/dist/internal/session-store.d.ts.map +1 -0
- package/dist/internal/session-store.js +140 -0
- package/dist/internal/session-urls.d.ts +10 -0
- package/dist/internal/session-urls.d.ts.map +1 -0
- package/dist/internal/session-urls.js +25 -0
- package/dist/internal/stream-progress.d.ts +14 -0
- package/dist/internal/stream-progress.d.ts.map +1 -0
- package/dist/internal/stream-progress.js +195 -0
- package/dist/internal/tool-result.d.ts +20 -0
- package/dist/internal/tool-result.d.ts.map +1 -0
- package/dist/internal/tool-result.js +45 -0
- package/dist/internal/trajectory.d.ts +56 -0
- package/dist/internal/trajectory.d.ts.map +1 -0
- package/dist/internal/trajectory.js +248 -0
- package/dist/internal/workspace.d.ts +42 -0
- package/dist/internal/workspace.d.ts.map +1 -0
- package/dist/internal/workspace.js +199 -0
- package/dist/playground/assets/cursor-icons-outline-oY2V_mvK.woff2 +0 -0
- package/dist/playground/assets/index-1K-hG-7p.css +1 -0
- package/dist/playground/assets/index-FlWjhg3x.js +79 -0
- package/dist/playground/index.html +17 -0
- package/dist/reminders.d.ts +23 -0
- package/dist/reminders.d.ts.map +1 -0
- package/dist/reminders.js +22 -0
- package/dist/schedules.d.ts +23 -0
- package/dist/schedules.d.ts.map +1 -0
- package/dist/schedules.js +26 -0
- package/dist/skills.d.ts +22 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +22 -0
- package/dist/tools.d.ts +92 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +68 -0
- package/dist/types.d.ts +1609 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +80 -0
- package/docs/README.md +174 -0
- package/docs/ab.md +355 -0
- package/docs/building-with-agents.md +129 -0
- package/docs/concepts.md +177 -0
- package/docs/deployment.md +218 -0
- package/docs/evals.md +383 -0
- package/docs/guides/agent-to-agent.md +118 -0
- package/docs/guides/cloud-runtime.md +122 -0
- package/docs/guides/github.md +172 -0
- package/docs/guides/human-in-the-loop.md +131 -0
- package/docs/guides/slack.md +245 -0
- package/docs/guides/webhooks.md +201 -0
- package/docs/hillclimbing.md +93 -0
- package/docs/quickstart.md +183 -0
- package/docs/reference/agent-config.md +154 -0
- package/docs/reference/channels.md +206 -0
- package/docs/reference/cli.md +257 -0
- package/docs/reference/connections.md +137 -0
- package/docs/reference/hooks.md +74 -0
- package/docs/reference/http-api.md +219 -0
- package/docs/reference/instructions.md +77 -0
- package/docs/reference/playground.md +67 -0
- package/docs/reference/project-layout.md +119 -0
- package/docs/reference/schedules.md +220 -0
- package/docs/reference/sessions.md +165 -0
- package/docs/reference/skills.md +79 -0
- package/docs/reference/subagents.md +74 -0
- package/docs/reference/tools.md +203 -0
- package/docs/scaffolding-agents.md +113 -0
- package/docs/troubleshooting.md +95 -0
- package/package.json +185 -0
- package/skills/ab/SKILL.md +139 -0
- package/skills/create-agent/SKILL.md +219 -0
- package/skills/debug/SKILL.md +71 -0
- package/skills/evals/SKILL.md +217 -0
- package/skills/framework-map/SKILL.md +142 -0
- package/skills/github/SKILL.md +144 -0
- package/skills/hillclimb/SKILL.md +162 -0
- package/skills/setup-slack/SKILL.md +180 -0
- package/src/ab.ts +675 -0
- package/src/bin/agent-serve.ts +949 -0
- package/src/channels/github/api.ts +374 -0
- package/src/channels/github/auth.ts +275 -0
- package/src/channels/github/binding.ts +223 -0
- package/src/channels/github/defaults.ts +111 -0
- package/src/channels/github/events.ts +72 -0
- package/src/channels/github/forward.ts +381 -0
- package/src/channels/github/github-channel.ts +2270 -0
- package/src/channels/github/index.ts +52 -0
- package/src/channels/github/replay.ts +387 -0
- package/src/channels/github/state.ts +95 -0
- package/src/channels/github/types.ts +490 -0
- package/src/channels/slack/api.ts +580 -0
- package/src/channels/slack/approvals.ts +198 -0
- package/src/channels/slack/assistant.ts +76 -0
- package/src/channels/slack/auth.ts +112 -0
- package/src/channels/slack/channel-watch.ts +219 -0
- package/src/channels/slack/constants.ts +30 -0
- package/src/channels/slack/credentials.ts +90 -0
- package/src/channels/slack/cursor-account.ts +166 -0
- package/src/channels/slack/defaults.ts +450 -0
- package/src/channels/slack/dispatch.ts +293 -0
- package/src/channels/slack/doctor.ts +135 -0
- package/src/channels/slack/event-id-dedupe.ts +30 -0
- package/src/channels/slack/inbound.ts +219 -0
- package/src/channels/slack/index.ts +45 -0
- package/src/channels/slack/init.ts +133 -0
- package/src/channels/slack/interactive.ts +194 -0
- package/src/channels/slack/links.ts +23 -0
- package/src/channels/slack/live-delivery.ts +201 -0
- package/src/channels/slack/log.ts +83 -0
- package/src/channels/slack/manifest.ts +179 -0
- package/src/channels/slack/pr-ref.ts +50 -0
- package/src/channels/slack/setup.ts +97 -0
- package/src/channels/slack/slack-channel.ts +374 -0
- package/src/channels/slack/socket-mode.ts +154 -0
- package/src/channels/slack/stream.ts +63 -0
- package/src/channels/slack/tool-approval-events.ts +136 -0
- package/src/channels/slack/types.ts +349 -0
- package/src/channels.ts +385 -0
- package/src/connections.ts +89 -0
- package/src/evals.ts +501 -0
- package/src/hooks.ts +33 -0
- package/src/index.ts +184 -0
- package/src/internal/ab-collector.ts +210 -0
- package/src/internal/ab-fold.ts +228 -0
- package/src/internal/ab-snapshot.ts +335 -0
- package/src/internal/approval-gate.ts +115 -0
- package/src/internal/auth.ts +39 -0
- package/src/internal/authored-alias-hooks.ts +69 -0
- package/src/internal/authored-loaders.ts +138 -0
- package/src/internal/bounded-int.ts +27 -0
- package/src/internal/brand.ts +29 -0
- package/src/internal/chat-client.ts +407 -0
- package/src/internal/cli-ax.ts +474 -0
- package/src/internal/cli-cursor.ts +275 -0
- package/src/internal/cli-github.ts +849 -0
- package/src/internal/cli-slack.ts +175 -0
- package/src/internal/cloud-merge.ts +41 -0
- package/src/internal/cron.ts +78 -0
- package/src/internal/cursor/account-mcp.ts +472 -0
- package/src/internal/cursor/backend-client.ts +614 -0
- package/src/internal/cursor/credentials.ts +401 -0
- package/src/internal/cursor-event-relay.ts +281 -0
- package/src/internal/cursor-relay-core.ts +444 -0
- package/src/internal/cursor-slack-relay.ts +242 -0
- package/src/internal/describe-error.ts +4 -0
- package/src/internal/dev-mode.ts +20 -0
- package/src/internal/discovery.ts +1709 -0
- package/src/internal/distribution.ts +97 -0
- package/src/internal/docs-site.ts +191 -0
- package/src/internal/duration.ts +39 -0
- package/src/internal/eval-run-store.ts +519 -0
- package/src/internal/eval-runner.ts +522 -0
- package/src/internal/event-log.ts +175 -0
- package/src/internal/event-mapper.ts +272 -0
- package/src/internal/frontmatter.ts +77 -0
- package/src/internal/github-fanout.ts +159 -0
- package/src/internal/handleAgentServeTrigger.ts +156 -0
- package/src/internal/host-platforms.ts +144 -0
- package/src/internal/http-channel.ts +546 -0
- package/src/internal/ids.ts +44 -0
- package/src/internal/init-project.ts +165 -0
- package/src/internal/json-dir-store.ts +109 -0
- package/src/internal/json-schema.ts +69 -0
- package/src/internal/log-ring.ts +59 -0
- package/src/internal/map-pool.ts +32 -0
- package/src/internal/mcp-endpoint.ts +518 -0
- package/src/internal/mcp-host.ts +211 -0
- package/src/internal/net.ts +31 -0
- package/src/internal/peer-connections.ts +100 -0
- package/src/internal/playground/static.ts +189 -0
- package/src/internal/playground/toolchain.ts +277 -0
- package/src/internal/reminder-runner.ts +756 -0
- package/src/internal/reminder-schedule.ts +106 -0
- package/src/internal/reminder-store.ts +145 -0
- package/src/internal/resolved-connections.ts +145 -0
- package/src/internal/review-comments.ts +268 -0
- package/src/internal/router.ts +145 -0
- package/src/internal/run-client.ts +320 -0
- package/src/internal/schedule-runner.ts +161 -0
- package/src/internal/sdk-runner.ts +315 -0
- package/src/internal/server.ts +1737 -0
- package/src/internal/session-engine.ts +1810 -0
- package/src/internal/session-store.ts +147 -0
- package/src/internal/session-urls.ts +40 -0
- package/src/internal/stream-progress.ts +254 -0
- package/src/internal/tool-result.ts +54 -0
- package/src/internal/trajectory.ts +306 -0
- package/src/internal/workspace.ts +223 -0
- package/src/reminders.ts +35 -0
- package/src/schedules.ts +37 -0
- package/src/skills.ts +27 -0
- package/src/tools.ts +127 -0
- package/src/types.ts +1848 -0
|
@@ -0,0 +1,1810 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session engine owns durable sessions and turn execution. A session is
|
|
3
|
+
* one Cursor agent conversation plus a materialized workspace, an event log,
|
|
4
|
+
* and channel bookkeeping (continuation keys, per-session channel state).
|
|
5
|
+
* Turns are executed by the Cursor SDK runner and their streaming updates
|
|
6
|
+
* are mapped onto the channel event vocabulary and dispatched to channel
|
|
7
|
+
* event handlers and hooks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { SDKCustomTool } from "@cursor/sdk";
|
|
13
|
+
import {
|
|
14
|
+
type ABDefinition,
|
|
15
|
+
mergeABInstructions,
|
|
16
|
+
resolveABMaxPlaygroundSessions,
|
|
17
|
+
resolveABSplit,
|
|
18
|
+
} from "../ab.js";
|
|
19
|
+
import { slackThreadPermalink } from "../channels/slack/links.js";
|
|
20
|
+
import {
|
|
21
|
+
type AgentProject,
|
|
22
|
+
type ApprovalDecision,
|
|
23
|
+
type AuthContext,
|
|
24
|
+
type CallToolOptions,
|
|
25
|
+
type ChannelDefinition,
|
|
26
|
+
type ChannelEventHandler,
|
|
27
|
+
type ChannelSession,
|
|
28
|
+
type ConnectionTransport,
|
|
29
|
+
DIRECT_TOOL_CALL_CHANNEL_ID,
|
|
30
|
+
type HookHandler,
|
|
31
|
+
type HostContext,
|
|
32
|
+
type HttpChannelDefinition,
|
|
33
|
+
isSymbolicConnectionTransport,
|
|
34
|
+
type JsonValue,
|
|
35
|
+
type PendingApproval,
|
|
36
|
+
type ReceiveInput,
|
|
37
|
+
type ResolvedAgent,
|
|
38
|
+
type RunnerTurnRequest,
|
|
39
|
+
type SendMessageFn,
|
|
40
|
+
type SendMessageOptions,
|
|
41
|
+
type SessionEvent,
|
|
42
|
+
type SessionEventPayload,
|
|
43
|
+
type SessionInfo,
|
|
44
|
+
type SessionRecord,
|
|
45
|
+
type SessionSummary,
|
|
46
|
+
type ToolCallOutcome,
|
|
47
|
+
type ToolContext,
|
|
48
|
+
type ToolNeedsApproval,
|
|
49
|
+
type TurnOutcome,
|
|
50
|
+
} from "../types.js";
|
|
51
|
+
import { ABCollector } from "./ab-collector.js";
|
|
52
|
+
import { foldABStream } from "./ab-fold.js";
|
|
53
|
+
import { type ABSnapshot, buildABSnapshot } from "./ab-snapshot.js";
|
|
54
|
+
import { ApprovalGate } from "./approval-gate.js";
|
|
55
|
+
import { samePrincipal } from "./auth.js";
|
|
56
|
+
import { mergeCloudOptions, resolveSessionRuntime } from "./cloud-merge.js";
|
|
57
|
+
import { EventLogStore } from "./event-log.js";
|
|
58
|
+
import { TurnEventMapper } from "./event-mapper.js";
|
|
59
|
+
import { createHostPlatforms, type HostPlatforms } from "./host-platforms.js";
|
|
60
|
+
import {
|
|
61
|
+
formatContinuationToken,
|
|
62
|
+
newContinuationKey,
|
|
63
|
+
newSessionId,
|
|
64
|
+
newToolCallId,
|
|
65
|
+
} from "./ids.js";
|
|
66
|
+
import { validateToolInput } from "./json-schema.js";
|
|
67
|
+
import { McpHost } from "./mcp-host.js";
|
|
68
|
+
import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections.js";
|
|
69
|
+
import type { AgentRunner } from "./sdk-runner.js";
|
|
70
|
+
import { SessionStore } from "./session-store.js";
|
|
71
|
+
import { normalizeToolResult, toolCallErrorMessage } from "./tool-result.js";
|
|
72
|
+
import {
|
|
73
|
+
buildAgentToolsCatalog,
|
|
74
|
+
materializeWorkspace,
|
|
75
|
+
writeWorkspaceFiles,
|
|
76
|
+
} from "./workspace.js";
|
|
77
|
+
|
|
78
|
+
export const HTTP_CHANNEL_ID = "http";
|
|
79
|
+
export const SCHEDULE_CHANNEL_PREFIX = "schedule:";
|
|
80
|
+
|
|
81
|
+
export class SessionBusyError extends Error {
|
|
82
|
+
constructor(sessionId: string) {
|
|
83
|
+
super(
|
|
84
|
+
`Session ${sessionId} already has a running turn; wait for session.waiting before sending a follow-up`
|
|
85
|
+
);
|
|
86
|
+
this.name = "SessionBusyError";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class SessionAuthError extends Error {
|
|
91
|
+
constructor(sessionId: string) {
|
|
92
|
+
super(
|
|
93
|
+
`Session ${sessionId} belongs to a different principal; pass the original caller auth on follow-up`
|
|
94
|
+
);
|
|
95
|
+
this.name = "SessionAuthError";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class UnknownChannelError extends Error {
|
|
100
|
+
constructor(channelId: string) {
|
|
101
|
+
super(`Unknown channel "${channelId}"`);
|
|
102
|
+
this.name = "UnknownChannelError";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class UnknownToolError extends Error {
|
|
107
|
+
constructor(
|
|
108
|
+
toolName: string,
|
|
109
|
+
readonly availableToolNames: string[]
|
|
110
|
+
) {
|
|
111
|
+
super(`Unknown tool "${toolName}"`);
|
|
112
|
+
this.name = "UnknownToolError";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class ToolNotDirectlyCallableError extends Error {
|
|
117
|
+
constructor(toolName: string) {
|
|
118
|
+
super(
|
|
119
|
+
`Tool "${toolName}" runs in the agent environment (execution: "agent") and cannot be called directly on the host`
|
|
120
|
+
);
|
|
121
|
+
this.name = "ToolNotDirectlyCallableError";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class ToolInputError extends Error {
|
|
126
|
+
constructor(args: { toolName: string; detail: string }) {
|
|
127
|
+
super(`Invalid input for tool "${args.toolName}": ${args.detail}`);
|
|
128
|
+
this.name = "ToolInputError";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export class UnknownSessionError extends Error {
|
|
133
|
+
constructor(sessionId: string) {
|
|
134
|
+
super(`Unknown session ${sessionId}`);
|
|
135
|
+
this.name = "UnknownSessionError";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class ApprovalNotFoundError extends Error {
|
|
140
|
+
constructor(sessionId: string, callId: string) {
|
|
141
|
+
super(`No pending approval for session ${sessionId} call ${callId}`);
|
|
142
|
+
this.name = "ApprovalNotFoundError";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class ApprovalInterruptedError extends Error {
|
|
147
|
+
constructor(sessionId: string, callId: string) {
|
|
148
|
+
super(
|
|
149
|
+
`Approval for session ${sessionId} call ${callId} is no longer live (host may have restarted while the tool was parked)`
|
|
150
|
+
);
|
|
151
|
+
this.name = "ApprovalInterruptedError";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface SessionEngineOptions {
|
|
156
|
+
project: AgentProject;
|
|
157
|
+
stateRoot: string;
|
|
158
|
+
runner: AgentRunner;
|
|
159
|
+
logger?: (line: string) => void;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A connection the serve host resolved once it was listening — a **peer**
|
|
164
|
+
* (`defineConnection({ agent })` mapped to the target agent's MCP endpoint)
|
|
165
|
+
* or the **Cursor account bridge** (`defineConnection({ cursorAccount })`
|
|
166
|
+
* mapped to the loopback bridge route). Local turns (and host-side MCP
|
|
167
|
+
* calls) use {@link localUrl}; cloud turns use {@link cloudUrl} when the
|
|
168
|
+
* serve host configured a public URL (peers and the account bridge).
|
|
169
|
+
*/
|
|
170
|
+
export interface EngineResolvedConnection {
|
|
171
|
+
/** MCP server name the model sees (the connection filename). */
|
|
172
|
+
name: string;
|
|
173
|
+
/** Loopback MCP URL, e.g. `http://127.0.0.1:3000/<slug>/v1/mcp`. */
|
|
174
|
+
localUrl: string;
|
|
175
|
+
/** Publicly reachable MCP URL for cloud turns, when configured. */
|
|
176
|
+
cloudUrl?: string;
|
|
177
|
+
/** Auth headers calls must present (host bearer / bridge secret). */
|
|
178
|
+
headers?: Record<string, string>;
|
|
179
|
+
/**
|
|
180
|
+
* When set, cloud turns must reach the account bridge via {@link cloudUrl}
|
|
181
|
+
* so the filter is enforced — omitting the bridge must not fail open to
|
|
182
|
+
* the account's broader native connector set.
|
|
183
|
+
*/
|
|
184
|
+
accountServersFilter?: readonly string[];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface EngineSendOptions extends SendMessageOptions {
|
|
188
|
+
mode?: "chat" | "task";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export class SessionEngine {
|
|
192
|
+
readonly project: AgentProject;
|
|
193
|
+
readonly stateRoot: string;
|
|
194
|
+
readonly sessions: SessionStore;
|
|
195
|
+
|
|
196
|
+
private readonly runner: AgentRunner;
|
|
197
|
+
private readonly logger: (line: string) => void;
|
|
198
|
+
private readonly logs: EventLogStore;
|
|
199
|
+
private readonly channelsById = new Map<string, ChannelDefinition>();
|
|
200
|
+
/** Sessions with a turn being admitted (before the run promise exists). */
|
|
201
|
+
private readonly pendingTurns = new Set<string>();
|
|
202
|
+
private readonly activeTurns = new Map<string, Promise<TurnOutcome>>();
|
|
203
|
+
/** Aborts the in-flight turn per session so a follow-up can preempt it. */
|
|
204
|
+
private readonly turnAborts = new Map<string, AbortController>();
|
|
205
|
+
/** Serializes turn admission per session (latest follow-up wins). */
|
|
206
|
+
private readonly admissionChains = new Map<string, Promise<unknown>>();
|
|
207
|
+
private readonly lastOutcomes = new Map<string, TurnOutcome>();
|
|
208
|
+
private readonly updateChains = new Map<string, Promise<unknown>>();
|
|
209
|
+
private readonly dispatchChains = new Map<string, Promise<void>>();
|
|
210
|
+
private readonly backgroundWork = new Set<Promise<unknown>>();
|
|
211
|
+
private readonly mcpHost: McpHost;
|
|
212
|
+
private readonly platforms: HostPlatforms;
|
|
213
|
+
private readonly approvals = new ApprovalGate();
|
|
214
|
+
private readonly abCollector: ABCollector;
|
|
215
|
+
private reminderApi: HostContext["reminders"];
|
|
216
|
+
/** Serve-time resolved connections (see {@link setResolvedConnections}). */
|
|
217
|
+
private resolvedConnections: EngineResolvedConnection[] = [];
|
|
218
|
+
|
|
219
|
+
constructor(options: SessionEngineOptions) {
|
|
220
|
+
this.project = options.project;
|
|
221
|
+
this.stateRoot = options.stateRoot;
|
|
222
|
+
this.runner = options.runner;
|
|
223
|
+
this.logger = options.logger ?? (() => {});
|
|
224
|
+
this.sessions = new SessionStore(join(options.stateRoot, "sessions"));
|
|
225
|
+
this.logs = new EventLogStore(
|
|
226
|
+
(sessionId) => this.sessions.eventFilePath(sessionId),
|
|
227
|
+
(sessionId, error) =>
|
|
228
|
+
this.logger(
|
|
229
|
+
`[agent-serve] failed to persist events for ${sessionId}: ${describeError(error)}`
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
this.abCollector = new ABCollector(
|
|
233
|
+
options.project.abs,
|
|
234
|
+
this.logger,
|
|
235
|
+
async (sessionId) => (await this.logs.get(sessionId)).snapshot(0),
|
|
236
|
+
{
|
|
237
|
+
projectRoot: options.project.rootDir,
|
|
238
|
+
...(options.project.abConfig?.persistSamples === undefined
|
|
239
|
+
? {}
|
|
240
|
+
: { persistSamples: options.project.abConfig.persistSamples }),
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
// Peer and Cursor-account transports are symbolic until the serve host
|
|
244
|
+
// resolves them; the host-side MCP registry starts with the concrete
|
|
245
|
+
// connections and gains the rest via setResolvedConnections.
|
|
246
|
+
this.mcpHost = new McpHost(
|
|
247
|
+
options.project.agent.connections.filter(
|
|
248
|
+
(connection) => !isSymbolicConnectionTransport(connection.transport)
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
this.platforms = createHostPlatforms();
|
|
252
|
+
for (const channel of options.project.channels) {
|
|
253
|
+
this.channelsById.set(channel.id, channel.definition);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Attach serve-time resolved connections (peers, Cursor account bridge).
|
|
259
|
+
* They join the per-turn SDK `mcpServers` map (loopback URL for local
|
|
260
|
+
* turns, `cloudUrl` for cloud turns) and the host-side registry
|
|
261
|
+
* (`ctx.host.mcp`, always loopback).
|
|
262
|
+
*/
|
|
263
|
+
setResolvedConnections(connections: EngineResolvedConnection[]): void {
|
|
264
|
+
this.resolvedConnections = connections;
|
|
265
|
+
for (const connection of connections) {
|
|
266
|
+
this.mcpHost.addConnection({
|
|
267
|
+
name: connection.name,
|
|
268
|
+
transport: {
|
|
269
|
+
url: connection.localUrl,
|
|
270
|
+
...(connection.headers === undefined
|
|
271
|
+
? {}
|
|
272
|
+
: { headers: connection.headers }),
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Shared host services (MCP / GitHub / Slack / reminders). */
|
|
279
|
+
get host(): HostContext {
|
|
280
|
+
return {
|
|
281
|
+
mcp: this.mcpHost,
|
|
282
|
+
github: this.platforms.github,
|
|
283
|
+
slack: this.platforms.slack,
|
|
284
|
+
reminders: this.reminderApi,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Attach the serve-host reminder API (called from startServer). */
|
|
289
|
+
setReminders(api: HostContext["reminders"]): void {
|
|
290
|
+
this.reminderApi = api;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ==========================================================================
|
|
294
|
+
// Sending messages
|
|
295
|
+
// ==========================================================================
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Start or resume a session on a channel. When `continuationToken`
|
|
299
|
+
* addresses an existing session this is a follow-up turn; otherwise a new
|
|
300
|
+
* session is created (keyed by the provided token, or a fresh one).
|
|
301
|
+
*/
|
|
302
|
+
async send(
|
|
303
|
+
channelId: string,
|
|
304
|
+
message: string,
|
|
305
|
+
options: EngineSendOptions = {}
|
|
306
|
+
): Promise<ChannelSession> {
|
|
307
|
+
const auth = options.auth ?? null;
|
|
308
|
+
const existing =
|
|
309
|
+
options.continuationToken === undefined
|
|
310
|
+
? undefined
|
|
311
|
+
: await this.sessions.findByContinuation(
|
|
312
|
+
channelId,
|
|
313
|
+
options.continuationToken
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
if (existing !== undefined) {
|
|
317
|
+
if (!samePrincipal(existing.auth, auth)) {
|
|
318
|
+
throw new SessionAuthError(existing.sessionId);
|
|
319
|
+
}
|
|
320
|
+
// Refresh harness cwd when the caller supplies a newer worktree (e.g. PR
|
|
321
|
+
// synchronize). Leave the durable continuation key unchanged.
|
|
322
|
+
let record = existing;
|
|
323
|
+
if (
|
|
324
|
+
options.workspaceDir !== undefined &&
|
|
325
|
+
options.workspaceDir !== existing.workspaceOverride
|
|
326
|
+
) {
|
|
327
|
+
record = await this.updateRecord(existing.sessionId, (current) => ({
|
|
328
|
+
...current,
|
|
329
|
+
workspaceOverride: options.workspaceDir,
|
|
330
|
+
pendingWorkspaceFiles:
|
|
331
|
+
options.workspaceFiles ?? current.pendingWorkspaceFiles,
|
|
332
|
+
}));
|
|
333
|
+
} else if (options.workspaceFiles !== undefined) {
|
|
334
|
+
record = await this.updateRecord(existing.sessionId, (current) => ({
|
|
335
|
+
...current,
|
|
336
|
+
pendingWorkspaceFiles: options.workspaceFiles,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
await this.startTurn(record.sessionId, message);
|
|
340
|
+
return this.channelSession(record.sessionId, record, false);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const now = new Date().toISOString();
|
|
344
|
+
const channelState = this.initialChannelState(channelId, options.state);
|
|
345
|
+
const purpose = options.purpose === "eval" ? "eval" : "live";
|
|
346
|
+
let record: SessionRecord = {
|
|
347
|
+
sessionId: newSessionId(),
|
|
348
|
+
channelId,
|
|
349
|
+
mode: options.mode ?? "chat",
|
|
350
|
+
purpose,
|
|
351
|
+
continuationKey: options.continuationToken ?? newContinuationKey(),
|
|
352
|
+
auth,
|
|
353
|
+
title: options.title,
|
|
354
|
+
channelState,
|
|
355
|
+
workspaceOverride: options.workspaceDir,
|
|
356
|
+
pendingWorkspaceFiles: options.workspaceFiles,
|
|
357
|
+
cloudOverride: options.cloud,
|
|
358
|
+
sdkAgentId: options.sdkAgentId,
|
|
359
|
+
turnCount: 0,
|
|
360
|
+
createdAt: now,
|
|
361
|
+
updatedAt: now,
|
|
362
|
+
};
|
|
363
|
+
await this.sessions.save(record);
|
|
364
|
+
await this.appendEvent(record.sessionId, {
|
|
365
|
+
type: "session.started",
|
|
366
|
+
data: { channelId },
|
|
367
|
+
});
|
|
368
|
+
record = await this.enrollABs(record);
|
|
369
|
+
await this.startTurn(record.sessionId, message);
|
|
370
|
+
return this.channelSession(record.sessionId, record, true);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Sticky A/B enrollment at session creation — before the first turn, so
|
|
375
|
+
* variants are in the durable stream (and on {@link SessionRecord.abs})
|
|
376
|
+
* ahead of model/instructions work. Failures persist as `variant: null`
|
|
377
|
+
* (sticky skip). Eval harness sessions (`purpose: "eval"`) are never
|
|
378
|
+
* enrolled.
|
|
379
|
+
*/
|
|
380
|
+
private async enrollABs(record: SessionRecord): Promise<SessionRecord> {
|
|
381
|
+
if (!this.abCollector.enabled || record.purpose === "eval") {
|
|
382
|
+
return record;
|
|
383
|
+
}
|
|
384
|
+
const sessionInfo = this.sessionInfo(record);
|
|
385
|
+
const abs: Record<string, string | null> = {};
|
|
386
|
+
for (const experiment of this.project.abs) {
|
|
387
|
+
const def = experiment.definition as ABDefinition;
|
|
388
|
+
const variantKeys = Object.keys(def.variants);
|
|
389
|
+
let variant: string | null = null;
|
|
390
|
+
try {
|
|
391
|
+
variant = await resolveABSplit(def.split, {
|
|
392
|
+
agent: { name: this.project.name },
|
|
393
|
+
channel: { id: record.channelId },
|
|
394
|
+
session: sessionInfo,
|
|
395
|
+
experiment: experiment.name,
|
|
396
|
+
variants: variantKeys,
|
|
397
|
+
});
|
|
398
|
+
if (variant !== null && !(variant in def.variants)) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`split resolved to ${JSON.stringify(variant)}; expected one of ${variantKeys.join(", ")} or null to skip`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
this.logger(
|
|
405
|
+
`[agent-serve] ab "${experiment.name}" enrollment failed (sticky skip): ${describeError(error)}`
|
|
406
|
+
);
|
|
407
|
+
variant = null;
|
|
408
|
+
}
|
|
409
|
+
abs[experiment.name] = variant;
|
|
410
|
+
await this.appendEvent(record.sessionId, {
|
|
411
|
+
type: "ab.assigned",
|
|
412
|
+
data: { experiment: experiment.name, variant },
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return this.updateRecord(record.sessionId, (current) => ({
|
|
416
|
+
...current,
|
|
417
|
+
abs,
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Ensure {@link SessionRecord.abs} is populated (hydrate from the durable
|
|
423
|
+
* `ab.assigned` stream when missing — e.g. older sessions after upgrade).
|
|
424
|
+
*/
|
|
425
|
+
private async ensureAbs(record: SessionRecord): Promise<SessionRecord> {
|
|
426
|
+
if (record.abs !== undefined || record.purpose === "eval") {
|
|
427
|
+
return record;
|
|
428
|
+
}
|
|
429
|
+
if (!this.abCollector.enabled) {
|
|
430
|
+
return record;
|
|
431
|
+
}
|
|
432
|
+
const events = await this.listEvents(record.sessionId, 0);
|
|
433
|
+
const folded = foldABStream(events);
|
|
434
|
+
if (folded.arms.size === 0) {
|
|
435
|
+
return record;
|
|
436
|
+
}
|
|
437
|
+
const abs = Object.fromEntries(folded.arms);
|
|
438
|
+
return this.updateRecord(record.sessionId, (current) => ({
|
|
439
|
+
...current,
|
|
440
|
+
abs,
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Agent config for this session: base project agent plus any per-variant
|
|
446
|
+
* `instructions` overlays from enrolled A/B arms.
|
|
447
|
+
*/
|
|
448
|
+
private agentForSession(record: SessionRecord): ResolvedAgent {
|
|
449
|
+
const base = this.project.agent;
|
|
450
|
+
const instructions = mergeABInstructions(
|
|
451
|
+
base.instructions,
|
|
452
|
+
record.abs,
|
|
453
|
+
this.project.abs.map((experiment) => ({
|
|
454
|
+
name: experiment.name,
|
|
455
|
+
definition: experiment.definition as ABDefinition,
|
|
456
|
+
}))
|
|
457
|
+
);
|
|
458
|
+
if (instructions === base.instructions) {
|
|
459
|
+
return base;
|
|
460
|
+
}
|
|
461
|
+
return { ...base, instructions };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Look up a session as a channel-scoped handle. */
|
|
465
|
+
async getChannelSession(
|
|
466
|
+
channelId: string,
|
|
467
|
+
sessionId: string
|
|
468
|
+
): Promise<ChannelSession | null> {
|
|
469
|
+
const record = await this.sessions.get(sessionId);
|
|
470
|
+
if (record === undefined || record.channelId !== channelId) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
return this.channelSession(sessionId, record, false);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Cross-channel hand-off (schedules and channel route handlers). */
|
|
477
|
+
async receive(
|
|
478
|
+
channel: ChannelDefinition | HttpChannelDefinition | string,
|
|
479
|
+
input: ReceiveInput
|
|
480
|
+
): Promise<ChannelSession> {
|
|
481
|
+
const channelId = this.resolveChannelId(channel);
|
|
482
|
+
const definition = this.channelsById.get(channelId);
|
|
483
|
+
|
|
484
|
+
if (definition?.receive !== undefined) {
|
|
485
|
+
let lastCreated: ChannelSession | undefined;
|
|
486
|
+
const send: SendMessageFn = async (message, options = {}) => {
|
|
487
|
+
const session = await this.send(channelId, message, options);
|
|
488
|
+
lastCreated = session;
|
|
489
|
+
return session;
|
|
490
|
+
};
|
|
491
|
+
const returned = await definition.receive(input, { send });
|
|
492
|
+
const session = returned ?? lastCreated;
|
|
493
|
+
if (session === undefined) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Channel "${channelId}" receive hook did not start a session`
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
return session;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return this.send(channelId, input.message, {
|
|
502
|
+
auth: input.auth,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Fire-and-forget task session (schedules' markdown form). */
|
|
507
|
+
async runTask(args: {
|
|
508
|
+
channelId: string;
|
|
509
|
+
prompt: string;
|
|
510
|
+
auth: AuthContext | null;
|
|
511
|
+
title?: string;
|
|
512
|
+
}): Promise<{ sessionId: string; done: Promise<TurnOutcome> }> {
|
|
513
|
+
const session = await this.send(args.channelId, args.prompt, {
|
|
514
|
+
auth: args.auth,
|
|
515
|
+
mode: "task",
|
|
516
|
+
...(args.title === undefined ? {} : { title: args.title }),
|
|
517
|
+
});
|
|
518
|
+
return { sessionId: session.id, done: this.waitForCompletion(session.id) };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ==========================================================================
|
|
522
|
+
// Turn execution
|
|
523
|
+
// ==========================================================================
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Admit a turn for `sessionId`. A follow-up that arrives while a turn is in
|
|
527
|
+
* flight preempts it: the running turn is cancelled and its SDK run marked
|
|
528
|
+
* terminal before the new turn starts. Admissions are serialized per session
|
|
529
|
+
* so a burst of follow-ups can't race two runs onto the same SDK agent — the
|
|
530
|
+
* last message wins. Resolves once the new turn has started (not finished).
|
|
531
|
+
*/
|
|
532
|
+
private startTurn(sessionId: string, message: string): Promise<void> {
|
|
533
|
+
const prior = this.admissionChains.get(sessionId) ?? Promise.resolve();
|
|
534
|
+
const admitted = prior
|
|
535
|
+
.catch(() => {})
|
|
536
|
+
.then(() => this.admitTurn(sessionId, message));
|
|
537
|
+
this.admissionChains.set(
|
|
538
|
+
sessionId,
|
|
539
|
+
admitted.catch(() => {})
|
|
540
|
+
);
|
|
541
|
+
return admitted;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private async admitTurn(sessionId: string, message: string): Promise<void> {
|
|
545
|
+
// Interrupt any turn in flight and wait for it to settle so the SDK run is
|
|
546
|
+
// terminal before this turn's send (the follow-up also force-expires as a
|
|
547
|
+
// belt-and-suspenders — see buildTurnRequest).
|
|
548
|
+
await this.interruptActiveTurn(sessionId);
|
|
549
|
+
this.pendingTurns.add(sessionId);
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
let record = await this.updateRecord(sessionId, (current) => ({
|
|
553
|
+
...current,
|
|
554
|
+
turnCount: current.turnCount + 1,
|
|
555
|
+
}));
|
|
556
|
+
// Arms must be on the record before tools/instructions see SessionInfo.
|
|
557
|
+
record = await this.ensureAbs(record);
|
|
558
|
+
const turnId = `turn_${record.turnCount}`;
|
|
559
|
+
const isFirstTurn = record.turnCount === 1;
|
|
560
|
+
|
|
561
|
+
await this.appendEvent(sessionId, {
|
|
562
|
+
type: "message.received",
|
|
563
|
+
turnId,
|
|
564
|
+
data: { text: message },
|
|
565
|
+
});
|
|
566
|
+
await this.appendEvent(sessionId, {
|
|
567
|
+
type: "turn.started",
|
|
568
|
+
turnId,
|
|
569
|
+
data: {},
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const abort = new AbortController();
|
|
573
|
+
this.turnAborts.set(sessionId, abort);
|
|
574
|
+
const turnPromise = this.executeTurn(
|
|
575
|
+
record,
|
|
576
|
+
turnId,
|
|
577
|
+
message,
|
|
578
|
+
isFirstTurn,
|
|
579
|
+
abort.signal
|
|
580
|
+
);
|
|
581
|
+
this.activeTurns.set(sessionId, turnPromise);
|
|
582
|
+
void turnPromise
|
|
583
|
+
.then((outcome) => {
|
|
584
|
+
this.lastOutcomes.set(sessionId, outcome);
|
|
585
|
+
})
|
|
586
|
+
.catch(() => {})
|
|
587
|
+
.finally(() => {
|
|
588
|
+
// Guard against clobbering a newer turn that already replaced us.
|
|
589
|
+
if (this.activeTurns.get(sessionId) === turnPromise) {
|
|
590
|
+
this.activeTurns.delete(sessionId);
|
|
591
|
+
}
|
|
592
|
+
if (this.turnAborts.get(sessionId) === abort) {
|
|
593
|
+
this.turnAborts.delete(sessionId);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
} finally {
|
|
597
|
+
this.pendingTurns.delete(sessionId);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Cancel the turn in flight for `sessionId` (if any) and wait for it to
|
|
603
|
+
* settle, so its SDK run is terminal before the next turn's send. Safe to
|
|
604
|
+
* call when nothing is running.
|
|
605
|
+
*/
|
|
606
|
+
private async interruptActiveTurn(sessionId: string): Promise<void> {
|
|
607
|
+
this.turnAborts.get(sessionId)?.abort();
|
|
608
|
+
const active = this.activeTurns.get(sessionId);
|
|
609
|
+
if (active !== undefined) {
|
|
610
|
+
await active.catch(() => {});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Abort the in-flight turn without starting a follow-up. Returns whether
|
|
616
|
+
* a turn was active (or pending abort) when called.
|
|
617
|
+
*/
|
|
618
|
+
async interrupt(sessionId: string): Promise<boolean> {
|
|
619
|
+
const hadWork =
|
|
620
|
+
this.turnAborts.has(sessionId) || this.activeTurns.has(sessionId);
|
|
621
|
+
await this.interruptActiveTurn(sessionId);
|
|
622
|
+
return hadWork;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Local harness cwd for a session: per-send override, else
|
|
627
|
+
* `<agent.local.cwd>/<sessionId>`, else the durable session workspace.
|
|
628
|
+
*/
|
|
629
|
+
harnessCwd(record: SessionRecord): string {
|
|
630
|
+
if (record.workspaceOverride !== undefined) {
|
|
631
|
+
return record.workspaceOverride;
|
|
632
|
+
}
|
|
633
|
+
const agentCwd = this.project.agent.local?.cwd;
|
|
634
|
+
if (agentCwd !== undefined) {
|
|
635
|
+
return join(agentCwd, record.sessionId);
|
|
636
|
+
}
|
|
637
|
+
return this.sessions.workspaceDir(record.sessionId);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Directory that receives AGENTS.md / skills / agent tools. When the
|
|
642
|
+
* harness cwd is a one-off override (e.g. PR worktree), scaffold stays in
|
|
643
|
+
* the durable session workspace; otherwise scaffold lands in the harness.
|
|
644
|
+
*/
|
|
645
|
+
private scaffoldDir(record: SessionRecord): string {
|
|
646
|
+
if (record.workspaceOverride !== undefined) {
|
|
647
|
+
return this.sessions.workspaceDir(record.sessionId);
|
|
648
|
+
}
|
|
649
|
+
return this.harnessCwd(record);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private async executeTurn(
|
|
653
|
+
record: SessionRecord,
|
|
654
|
+
turnId: string,
|
|
655
|
+
message: string,
|
|
656
|
+
isFirstTurn: boolean,
|
|
657
|
+
signal: AbortSignal
|
|
658
|
+
): Promise<TurnOutcome> {
|
|
659
|
+
const sessionId = record.sessionId;
|
|
660
|
+
const workspaceDir = this.scaffoldDir(record);
|
|
661
|
+
const harnessCwd = this.harnessCwd(record);
|
|
662
|
+
// Hold the open log so tool events append synchronously. Fire-and-forget
|
|
663
|
+
// `appendEvent` races parallel tool completions and delays the live stream
|
|
664
|
+
// until the next await tick — often after *all* parallel tools finish.
|
|
665
|
+
const log = await this.logs.get(sessionId);
|
|
666
|
+
const emit = (payload: SessionEventPayload): void => {
|
|
667
|
+
try {
|
|
668
|
+
const event = log.append({ ...payload, turnId });
|
|
669
|
+
this.queueDispatch(sessionId, event);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
this.logger(
|
|
672
|
+
`[agent-serve] failed to record event for ${sessionId}: ${describeError(error)}`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
const mapper = new TurnEventMapper(emit);
|
|
677
|
+
|
|
678
|
+
let outcome: TurnOutcome;
|
|
679
|
+
try {
|
|
680
|
+
// Scaffold agent files into the session workspace even when the harness
|
|
681
|
+
// cwd is overridden (e.g. a PR worktree). A/B variant instruction
|
|
682
|
+
// overlays are applied via agentForSession.
|
|
683
|
+
const agent = this.agentForSession(record);
|
|
684
|
+
await materializeWorkspace({
|
|
685
|
+
agent,
|
|
686
|
+
workspaceDir,
|
|
687
|
+
seed: isFirstTurn,
|
|
688
|
+
});
|
|
689
|
+
const pendingFiles = record.pendingWorkspaceFiles;
|
|
690
|
+
if (pendingFiles !== undefined && Object.keys(pendingFiles).length > 0) {
|
|
691
|
+
await writeWorkspaceFiles(harnessCwd, pendingFiles);
|
|
692
|
+
await this.updateRecord(sessionId, (current) => {
|
|
693
|
+
const { pendingWorkspaceFiles: _cleared, ...rest } = current;
|
|
694
|
+
return rest;
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
const request = this.buildTurnRequest(
|
|
698
|
+
record,
|
|
699
|
+
agent,
|
|
700
|
+
turnId,
|
|
701
|
+
message,
|
|
702
|
+
isFirstTurn,
|
|
703
|
+
emit,
|
|
704
|
+
async (update) => {
|
|
705
|
+
mapper.handleUpdate(update);
|
|
706
|
+
// Yield so the HTTP NDJSON writer can flush tool start/result
|
|
707
|
+
// events while sibling parallel tools are still executing.
|
|
708
|
+
if (
|
|
709
|
+
update.type === "tool-call-started" ||
|
|
710
|
+
update.type === "tool-call-completed"
|
|
711
|
+
) {
|
|
712
|
+
await yieldEventLoop();
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
signal
|
|
716
|
+
);
|
|
717
|
+
outcome = await this.runner.runTurn(request);
|
|
718
|
+
} catch (error) {
|
|
719
|
+
this.approvals.cancelSession(sessionId, "deny");
|
|
720
|
+
outcome = { status: "error", errorMessage: describeError(error) };
|
|
721
|
+
} finally {
|
|
722
|
+
this.approvals.cancelSession(sessionId, "deny");
|
|
723
|
+
await this.clearPendingApprovals(sessionId);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
mapper.finish();
|
|
727
|
+
|
|
728
|
+
if (mapper.title !== undefined) {
|
|
729
|
+
const title = mapper.title;
|
|
730
|
+
await this.updateRecord(sessionId, (current) => ({ ...current, title }));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const finalResult = outcome.result ?? mapper.finalText;
|
|
734
|
+
const finalUsage = outcome.usage ?? mapper.usage;
|
|
735
|
+
|
|
736
|
+
if (outcome.status === "finished") {
|
|
737
|
+
await this.appendEvent(sessionId, {
|
|
738
|
+
type: "turn.completed",
|
|
739
|
+
turnId,
|
|
740
|
+
data: {
|
|
741
|
+
...(finalResult === undefined ? {} : { result: finalResult }),
|
|
742
|
+
...(finalUsage === undefined ? {} : { usage: finalUsage }),
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
} else {
|
|
746
|
+
const interrupted = signal.aborted && outcome.status === "cancelled";
|
|
747
|
+
await this.appendEvent(sessionId, {
|
|
748
|
+
type: "turn.failed",
|
|
749
|
+
turnId,
|
|
750
|
+
data: {
|
|
751
|
+
message: interrupted
|
|
752
|
+
? "turn interrupted"
|
|
753
|
+
: (outcome.errorMessage ??
|
|
754
|
+
(outcome.status === "cancelled"
|
|
755
|
+
? "turn cancelled"
|
|
756
|
+
: "turn failed")),
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const current = await this.sessions.get(sessionId);
|
|
762
|
+
const mode = current?.mode ?? record.mode;
|
|
763
|
+
if (mode === "task") {
|
|
764
|
+
await this.appendEvent(
|
|
765
|
+
sessionId,
|
|
766
|
+
outcome.status === "finished"
|
|
767
|
+
? { type: "session.completed", turnId, data: {} }
|
|
768
|
+
: {
|
|
769
|
+
type: "session.failed",
|
|
770
|
+
turnId,
|
|
771
|
+
data: { message: outcome.errorMessage ?? "task failed" },
|
|
772
|
+
}
|
|
773
|
+
);
|
|
774
|
+
} else {
|
|
775
|
+
await this.appendEvent(sessionId, {
|
|
776
|
+
type: "session.waiting",
|
|
777
|
+
turnId,
|
|
778
|
+
data: {},
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
...outcome,
|
|
784
|
+
...(finalResult === undefined ? {} : { result: finalResult }),
|
|
785
|
+
...(finalUsage === undefined ? {} : { usage: finalUsage }),
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
private buildTurnRequest(
|
|
790
|
+
record: SessionRecord,
|
|
791
|
+
agent: ResolvedAgent,
|
|
792
|
+
turnId: string,
|
|
793
|
+
prompt: string,
|
|
794
|
+
isFirstTurn: boolean,
|
|
795
|
+
emit: (payload: SessionEventPayload) => void,
|
|
796
|
+
onUpdate: RunnerTurnRequest["onUpdate"],
|
|
797
|
+
signal: AbortSignal
|
|
798
|
+
): RunnerTurnRequest {
|
|
799
|
+
// Per-session cloud attach (channel/tool handoff) forces cloud runtime
|
|
800
|
+
// even when the agent defaults to local for chat server tools.
|
|
801
|
+
const runtime = resolveSessionRuntime({
|
|
802
|
+
agentRuntime: agent.runtime,
|
|
803
|
+
cloudOverride: record.cloudOverride,
|
|
804
|
+
});
|
|
805
|
+
const mergedCloud =
|
|
806
|
+
runtime === "cloud"
|
|
807
|
+
? mergeCloudOptions({
|
|
808
|
+
base: agent.cloud,
|
|
809
|
+
override: record.cloudOverride,
|
|
810
|
+
})
|
|
811
|
+
: undefined;
|
|
812
|
+
let turnPrompt = prompt;
|
|
813
|
+
if (runtime === "cloud" && isFirstTurn) {
|
|
814
|
+
const preamble = [
|
|
815
|
+
agent.instructions?.trim() ?? "",
|
|
816
|
+
buildAgentToolsCatalog(agent.tools, { includeScripts: true }),
|
|
817
|
+
]
|
|
818
|
+
.filter((part) => part !== "")
|
|
819
|
+
.join("\n\n");
|
|
820
|
+
turnPrompt = preamble === "" ? prompt : `${preamble}\n\n${prompt}`;
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
sessionId: record.sessionId,
|
|
824
|
+
...(record.sdkAgentId === undefined
|
|
825
|
+
? {}
|
|
826
|
+
: { sdkAgentId: record.sdkAgentId }),
|
|
827
|
+
workspaceDir: this.harnessCwd(record),
|
|
828
|
+
runnerStateDir: join(this.stateRoot, "runner"),
|
|
829
|
+
prompt: turnPrompt,
|
|
830
|
+
isFirstTurn,
|
|
831
|
+
...(agent.model === undefined ? {} : { model: agent.model }),
|
|
832
|
+
runtime,
|
|
833
|
+
...(mergedCloud === undefined ? {} : { cloud: mergedCloud }),
|
|
834
|
+
customTools:
|
|
835
|
+
runtime === "local" ? this.buildCustomTools(record, turnId, emit) : {},
|
|
836
|
+
mcpServers: this.buildMcpServers(runtime),
|
|
837
|
+
subagents: agent.subagents.map((subagent) => ({
|
|
838
|
+
name: subagent.name,
|
|
839
|
+
description: subagent.description ?? subagent.name,
|
|
840
|
+
prompt: subagentPrompt(subagent),
|
|
841
|
+
...(subagent.model === undefined ? {} : { model: subagent.model }),
|
|
842
|
+
})),
|
|
843
|
+
onUpdate,
|
|
844
|
+
signal,
|
|
845
|
+
// Local follow-ups expire any lingering active run so a preempted turn
|
|
846
|
+
// (or one orphaned by a crashed process) never blocks this send.
|
|
847
|
+
forceExpireActiveRun: runtime === "local" && !isFirstTurn,
|
|
848
|
+
onSdkAgentId: async (sdkAgentId) => {
|
|
849
|
+
if (record.sdkAgentId === sdkAgentId) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
await this.updateRecord(record.sessionId, (current) => ({
|
|
853
|
+
...current,
|
|
854
|
+
sdkAgentId,
|
|
855
|
+
}));
|
|
856
|
+
record.sdkAgentId = sdkAgentId;
|
|
857
|
+
await this.appendEvent(record.sessionId, {
|
|
858
|
+
type: "agent.bound",
|
|
859
|
+
turnId,
|
|
860
|
+
data: {
|
|
861
|
+
sdkAgentId,
|
|
862
|
+
...(sdkAgentId.startsWith("bc-")
|
|
863
|
+
? { url: `https://cursor.com/agents/${sdkAgentId}` }
|
|
864
|
+
: {}),
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
},
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* MCP servers for a turn: authored concrete connections plus resolved
|
|
873
|
+
* serve-time resolved connections (peers, Cursor account bridge).
|
|
874
|
+
* Resolved connections pick their URL by runtime — loopback for local
|
|
875
|
+
* turns, `cloudUrl` for cloud turns — and are omitted from cloud turns
|
|
876
|
+
* when no cloud URL exists unless a `servers` filter requires the bridge
|
|
877
|
+
* (see {@link assertCloudCanHonorAccountServersFilters}).
|
|
878
|
+
*/
|
|
879
|
+
private buildMcpServers(
|
|
880
|
+
runtime: "local" | "cloud"
|
|
881
|
+
): Record<string, ConnectionTransport> {
|
|
882
|
+
assertCloudCanHonorAccountServersFilters({
|
|
883
|
+
runtime,
|
|
884
|
+
resolvedConnections: this.resolvedConnections,
|
|
885
|
+
});
|
|
886
|
+
const servers: Record<string, ConnectionTransport> = {};
|
|
887
|
+
for (const connection of this.project.agent.connections) {
|
|
888
|
+
if (isSymbolicConnectionTransport(connection.transport)) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
servers[connection.name] = connection.transport;
|
|
892
|
+
}
|
|
893
|
+
for (const resolved of this.resolvedConnections) {
|
|
894
|
+
const url = runtime === "cloud" ? resolved.cloudUrl : resolved.localUrl;
|
|
895
|
+
if (url === undefined) {
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
servers[resolved.name] = {
|
|
899
|
+
url,
|
|
900
|
+
...(resolved.headers === undefined
|
|
901
|
+
? {}
|
|
902
|
+
: { headers: resolved.headers }),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
return servers;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private buildCustomTools(
|
|
909
|
+
record: SessionRecord,
|
|
910
|
+
turnId: string,
|
|
911
|
+
emit: (payload: SessionEventPayload) => void
|
|
912
|
+
): Record<string, SDKCustomTool> {
|
|
913
|
+
const tools: Record<string, SDKCustomTool> = {};
|
|
914
|
+
for (const tool of this.project.agent.tools) {
|
|
915
|
+
if (tool.execution !== "server") {
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
const execute = tool.definition.execute;
|
|
919
|
+
if (typeof execute !== "function") {
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
tools[tool.name] = {
|
|
923
|
+
description: tool.description,
|
|
924
|
+
inputSchema: tool.inputSchema as SDKCustomTool["inputSchema"],
|
|
925
|
+
execute: async (args, context) => {
|
|
926
|
+
const validation = validateToolInput(
|
|
927
|
+
tool.definition.inputSchema,
|
|
928
|
+
args
|
|
929
|
+
);
|
|
930
|
+
if (!validation.ok) {
|
|
931
|
+
return {
|
|
932
|
+
content: [
|
|
933
|
+
{
|
|
934
|
+
type: "text",
|
|
935
|
+
text: `Invalid input for tool "${tool.name}": ${validation.message}`,
|
|
936
|
+
},
|
|
937
|
+
],
|
|
938
|
+
isError: true,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
const toolCallId = context.toolCallId ?? newToolCallId(tool.name);
|
|
942
|
+
const toolCtx: ToolContext = {
|
|
943
|
+
toolCallId,
|
|
944
|
+
session: this.sessionInfo(record),
|
|
945
|
+
workspaceDir: this.harnessCwd(record),
|
|
946
|
+
host: this.host,
|
|
947
|
+
send: (channelId, message, sendOptions = {}) =>
|
|
948
|
+
this.send(channelId, message, {
|
|
949
|
+
...sendOptions,
|
|
950
|
+
auth:
|
|
951
|
+
sendOptions.auth !== undefined
|
|
952
|
+
? sendOptions.auth
|
|
953
|
+
: record.auth,
|
|
954
|
+
}),
|
|
955
|
+
getSession: (channelId, sessionId) =>
|
|
956
|
+
this.getChannelSession(channelId, sessionId),
|
|
957
|
+
};
|
|
958
|
+
try {
|
|
959
|
+
const needsGate = await evaluateNeedsApproval(
|
|
960
|
+
tool.definition.needsApproval,
|
|
961
|
+
validation.value
|
|
962
|
+
);
|
|
963
|
+
if (needsGate) {
|
|
964
|
+
const decision = await this.parkForApproval({
|
|
965
|
+
sessionId: record.sessionId,
|
|
966
|
+
turnId,
|
|
967
|
+
callId: toolCallId,
|
|
968
|
+
toolName: tool.name,
|
|
969
|
+
args: validation.value,
|
|
970
|
+
emit,
|
|
971
|
+
});
|
|
972
|
+
if (decision === "deny") {
|
|
973
|
+
return {
|
|
974
|
+
content: [
|
|
975
|
+
{
|
|
976
|
+
type: "text",
|
|
977
|
+
text: `Tool "${tool.name}" was denied by a human approver.`,
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
isError: true,
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const started = Date.now();
|
|
985
|
+
this.logger(`[session] tool ${tool.name} start`);
|
|
986
|
+
const result = await execute(validation.value as never, toolCtx);
|
|
987
|
+
this.logger(
|
|
988
|
+
`[session] tool ${tool.name} done (${Date.now() - started}ms)`
|
|
989
|
+
);
|
|
990
|
+
return normalizeToolResult(result);
|
|
991
|
+
} catch (error) {
|
|
992
|
+
this.logger(
|
|
993
|
+
`[session] tool ${tool.name} failed: ${describeError(error)}`
|
|
994
|
+
);
|
|
995
|
+
return {
|
|
996
|
+
content: [
|
|
997
|
+
{
|
|
998
|
+
type: "text",
|
|
999
|
+
text: `Tool "${tool.name}" failed: ${describeError(error)}`,
|
|
1000
|
+
},
|
|
1001
|
+
],
|
|
1002
|
+
isError: true,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
return tools;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Park a server-tool call until a human approves or denies. Emits
|
|
1013
|
+
* `action.approval_requested` / `action.approval_resolved` on the
|
|
1014
|
+
* held turn log so the playground and stream see the gate.
|
|
1015
|
+
*/
|
|
1016
|
+
private async parkForApproval(args: {
|
|
1017
|
+
sessionId: string;
|
|
1018
|
+
turnId: string;
|
|
1019
|
+
callId: string;
|
|
1020
|
+
toolName: string;
|
|
1021
|
+
args: unknown;
|
|
1022
|
+
emit: (payload: SessionEventPayload) => void;
|
|
1023
|
+
}): Promise<ApprovalDecision> {
|
|
1024
|
+
const requestedAt = new Date().toISOString();
|
|
1025
|
+
const pendingRecord: PendingApproval = {
|
|
1026
|
+
callId: args.callId,
|
|
1027
|
+
turnId: args.turnId,
|
|
1028
|
+
toolName: args.toolName,
|
|
1029
|
+
args: args.args,
|
|
1030
|
+
requestedAt,
|
|
1031
|
+
};
|
|
1032
|
+
const resolutionPromise = this.approvals.wait({
|
|
1033
|
+
sessionId: args.sessionId,
|
|
1034
|
+
...pendingRecord,
|
|
1035
|
+
});
|
|
1036
|
+
await this.updateRecord(args.sessionId, (current) => ({
|
|
1037
|
+
...current,
|
|
1038
|
+
pendingApprovals: [
|
|
1039
|
+
...(current.pendingApprovals ?? []).filter(
|
|
1040
|
+
(item) => item.callId !== args.callId
|
|
1041
|
+
),
|
|
1042
|
+
pendingRecord,
|
|
1043
|
+
],
|
|
1044
|
+
}));
|
|
1045
|
+
args.emit({
|
|
1046
|
+
type: "action.approval_requested",
|
|
1047
|
+
data: {
|
|
1048
|
+
callId: args.callId,
|
|
1049
|
+
toolName: args.toolName,
|
|
1050
|
+
args: args.args,
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
this.logger(
|
|
1054
|
+
`[session] tool ${args.toolName} awaiting approval (${args.callId})`
|
|
1055
|
+
);
|
|
1056
|
+
const { decision, by } = await resolutionPromise;
|
|
1057
|
+
await this.updateRecord(args.sessionId, (current) => ({
|
|
1058
|
+
...current,
|
|
1059
|
+
pendingApprovals: (current.pendingApprovals ?? []).filter(
|
|
1060
|
+
(item) => item.callId !== args.callId
|
|
1061
|
+
),
|
|
1062
|
+
}));
|
|
1063
|
+
args.emit({
|
|
1064
|
+
type: "action.approval_resolved",
|
|
1065
|
+
data: {
|
|
1066
|
+
callId: args.callId,
|
|
1067
|
+
toolName: args.toolName,
|
|
1068
|
+
decision,
|
|
1069
|
+
...(by === undefined ? {} : { by }),
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
this.logger(
|
|
1073
|
+
`[session] tool ${args.toolName} approval ${decision} (${args.callId})`
|
|
1074
|
+
);
|
|
1075
|
+
return decision;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private async clearPendingApprovals(sessionId: string): Promise<void> {
|
|
1079
|
+
const current = await this.sessions.get(sessionId);
|
|
1080
|
+
if (
|
|
1081
|
+
current === undefined ||
|
|
1082
|
+
current.pendingApprovals === undefined ||
|
|
1083
|
+
current.pendingApprovals.length === 0
|
|
1084
|
+
) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
await this.updateRecord(sessionId, (record) => ({
|
|
1088
|
+
...record,
|
|
1089
|
+
pendingApprovals: [],
|
|
1090
|
+
}));
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/** Pending tool approvals for a session (live waiters ∪ persisted). */
|
|
1094
|
+
async listPendingApprovals(sessionId: string): Promise<PendingApproval[]> {
|
|
1095
|
+
const record = await this.sessions.get(sessionId);
|
|
1096
|
+
if (record === undefined) {
|
|
1097
|
+
return [];
|
|
1098
|
+
}
|
|
1099
|
+
const live = this.approvals.list(sessionId);
|
|
1100
|
+
if (live.length > 0) {
|
|
1101
|
+
return live;
|
|
1102
|
+
}
|
|
1103
|
+
return record.pendingApprovals ?? [];
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Approve or deny a parked server-tool call. Unblocks the in-flight
|
|
1108
|
+
* SDK custom-tool callback for the same `callId`.
|
|
1109
|
+
*
|
|
1110
|
+
* Pass `asSessionOwner: true` only from an explicit host bridge (e.g. `--dev`
|
|
1111
|
+
* playground resolving a Slack-owned parked tool). Ownership is checked
|
|
1112
|
+
* against the stored session principal; `by` still records the clicker.
|
|
1113
|
+
*/
|
|
1114
|
+
async resolveApproval(
|
|
1115
|
+
sessionId: string,
|
|
1116
|
+
callId: string,
|
|
1117
|
+
decision: ApprovalDecision,
|
|
1118
|
+
auth: AuthContext | null,
|
|
1119
|
+
options?: { asSessionOwner?: boolean }
|
|
1120
|
+
): Promise<PendingApproval> {
|
|
1121
|
+
const record = await this.sessions.get(sessionId);
|
|
1122
|
+
if (record === undefined) {
|
|
1123
|
+
throw new ApprovalNotFoundError(sessionId, callId);
|
|
1124
|
+
}
|
|
1125
|
+
const ownershipAuth = options?.asSessionOwner === true ? record.auth : auth;
|
|
1126
|
+
if (!samePrincipal(record.auth, ownershipAuth)) {
|
|
1127
|
+
throw new SessionAuthError(sessionId);
|
|
1128
|
+
}
|
|
1129
|
+
const pending =
|
|
1130
|
+
(record.pendingApprovals ?? []).find((item) => item.callId === callId) ??
|
|
1131
|
+
this.approvals.list(sessionId).find((item) => item.callId === callId);
|
|
1132
|
+
if (pending === undefined) {
|
|
1133
|
+
throw new ApprovalNotFoundError(sessionId, callId);
|
|
1134
|
+
}
|
|
1135
|
+
const resolvedBy = approvalResolverLabel(auth);
|
|
1136
|
+
if (!this.approvals.resolve(sessionId, callId, decision, resolvedBy)) {
|
|
1137
|
+
await this.removePendingApproval(sessionId, callId);
|
|
1138
|
+
throw new ApprovalInterruptedError(sessionId, callId);
|
|
1139
|
+
}
|
|
1140
|
+
return pending;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
private async removePendingApproval(
|
|
1144
|
+
sessionId: string,
|
|
1145
|
+
callId: string
|
|
1146
|
+
): Promise<void> {
|
|
1147
|
+
await this.updateRecord(sessionId, (current) => ({
|
|
1148
|
+
...current,
|
|
1149
|
+
pendingApprovals: (current.pendingApprovals ?? []).filter(
|
|
1150
|
+
(item) => item.callId !== callId
|
|
1151
|
+
),
|
|
1152
|
+
}));
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// ==========================================================================
|
|
1156
|
+
// Direct tool calls (deterministic invocation, no model turn)
|
|
1157
|
+
// ==========================================================================
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Call an authored server tool deterministically: validate `input`
|
|
1161
|
+
* against the tool's schema (zod schemas validate and transform; plain
|
|
1162
|
+
* JSON Schema inputs pass through unvalidated, matching the model path),
|
|
1163
|
+
* run `execute` in-process, and return the normalized result exactly as
|
|
1164
|
+
* the model would receive it.
|
|
1165
|
+
*
|
|
1166
|
+
* With `options.sessionId` the tool runs inside that session (its info
|
|
1167
|
+
* and materialized workspace) and the call is recorded on the session's
|
|
1168
|
+
* event stream as `actions.requested` / `action.result` under a
|
|
1169
|
+
* per-call `turnId` (the call id), so it shows up in trajectories.
|
|
1170
|
+
* Rejected with {@link SessionBusyError} while a model turn is running.
|
|
1171
|
+
* Without a session, the tool runs against an **ephemeral** scratch
|
|
1172
|
+
* workspace under `<stateRoot>/tool-calls/<callId>` (materialized with
|
|
1173
|
+
* seed files, removed after the call returns).
|
|
1174
|
+
*
|
|
1175
|
+
* Throws {@link UnknownToolError}, {@link ToolNotDirectlyCallableError},
|
|
1176
|
+
* {@link ToolInputError}, or {@link UnknownSessionError} before the tool
|
|
1177
|
+
* body runs; a tool body that throws is reported as an `isError` outcome
|
|
1178
|
+
* with the same `{ content, isError }` envelope the model would see, not
|
|
1179
|
+
* an exception.
|
|
1180
|
+
*/
|
|
1181
|
+
async callTool(
|
|
1182
|
+
toolName: string,
|
|
1183
|
+
input: unknown = {},
|
|
1184
|
+
options: CallToolOptions = {}
|
|
1185
|
+
): Promise<ToolCallOutcome> {
|
|
1186
|
+
const tool = this.project.agent.tools.find((t) => t.name === toolName);
|
|
1187
|
+
if (tool === undefined) {
|
|
1188
|
+
throw new UnknownToolError(
|
|
1189
|
+
toolName,
|
|
1190
|
+
this.project.agent.tools
|
|
1191
|
+
.filter((t) => t.execution === "server")
|
|
1192
|
+
.map((t) => t.name)
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
const execute = tool.definition.execute;
|
|
1196
|
+
if (tool.execution !== "server" || typeof execute !== "function") {
|
|
1197
|
+
throw new ToolNotDirectlyCallableError(toolName);
|
|
1198
|
+
}
|
|
1199
|
+
const validation = validateToolInput(tool.definition.inputSchema, input);
|
|
1200
|
+
if (!validation.ok) {
|
|
1201
|
+
throw new ToolInputError({ toolName, detail: validation.message });
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const callId = newToolCallId(toolName);
|
|
1205
|
+
const auth = options.auth ?? null;
|
|
1206
|
+
let record: SessionRecord | undefined;
|
|
1207
|
+
let session: SessionInfo;
|
|
1208
|
+
let workspaceDir: string;
|
|
1209
|
+
let scratchDir: string | undefined;
|
|
1210
|
+
if (options.sessionId !== undefined) {
|
|
1211
|
+
record = await this.sessions.get(options.sessionId);
|
|
1212
|
+
if (record === undefined) {
|
|
1213
|
+
throw new UnknownSessionError(options.sessionId);
|
|
1214
|
+
}
|
|
1215
|
+
// Direct calls respect the same serialization as follow-up sends: a
|
|
1216
|
+
// running model turn owns the session workspace and event stream.
|
|
1217
|
+
if (
|
|
1218
|
+
this.activeTurns.has(record.sessionId) ||
|
|
1219
|
+
this.pendingTurns.has(record.sessionId)
|
|
1220
|
+
) {
|
|
1221
|
+
throw new SessionBusyError(record.sessionId);
|
|
1222
|
+
}
|
|
1223
|
+
session = this.sessionInfo(record);
|
|
1224
|
+
workspaceDir = this.harnessCwd(record);
|
|
1225
|
+
} else {
|
|
1226
|
+
session = {
|
|
1227
|
+
id: callId,
|
|
1228
|
+
channelId: DIRECT_TOOL_CALL_CHANNEL_ID,
|
|
1229
|
+
mode: "task",
|
|
1230
|
+
auth,
|
|
1231
|
+
};
|
|
1232
|
+
scratchDir = join(this.stateRoot, "tool-calls", callId);
|
|
1233
|
+
workspaceDir = scratchDir;
|
|
1234
|
+
}
|
|
1235
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
1236
|
+
// Match what a model turn would see: session workspaces get the same
|
|
1237
|
+
// scaffold as executeTurn (seeds only before the first turn); scratch
|
|
1238
|
+
// workspaces are materialized fresh.
|
|
1239
|
+
await materializeWorkspace({
|
|
1240
|
+
agent: this.project.agent,
|
|
1241
|
+
workspaceDir:
|
|
1242
|
+
record === undefined ? workspaceDir : this.scaffoldDir(record),
|
|
1243
|
+
seed: record === undefined || record.turnCount === 0,
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
// A per-call turnId keeps these events visible in trajectories (which
|
|
1247
|
+
// drop turn-less action events) without attributing them to a model turn.
|
|
1248
|
+
if (record !== undefined) {
|
|
1249
|
+
await this.appendEvent(record.sessionId, {
|
|
1250
|
+
type: "actions.requested",
|
|
1251
|
+
turnId: callId,
|
|
1252
|
+
data: { calls: [{ callId, toolName, args: validation.value }] },
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const started = Date.now();
|
|
1257
|
+
this.logger(`[session] tool ${toolName} start (direct ${callId})`);
|
|
1258
|
+
let result: unknown;
|
|
1259
|
+
let isError: boolean;
|
|
1260
|
+
try {
|
|
1261
|
+
result = normalizeToolResult(
|
|
1262
|
+
await execute(validation.value as never, {
|
|
1263
|
+
toolCallId: callId,
|
|
1264
|
+
session,
|
|
1265
|
+
workspaceDir,
|
|
1266
|
+
host: this.host,
|
|
1267
|
+
send: (channelId, message, sendOptions = {}) =>
|
|
1268
|
+
this.send(channelId, message, {
|
|
1269
|
+
...sendOptions,
|
|
1270
|
+
auth:
|
|
1271
|
+
sendOptions.auth !== undefined
|
|
1272
|
+
? sendOptions.auth
|
|
1273
|
+
: session.auth,
|
|
1274
|
+
}),
|
|
1275
|
+
getSession: (channelId, sessionId) =>
|
|
1276
|
+
this.getChannelSession(channelId, sessionId),
|
|
1277
|
+
})
|
|
1278
|
+
);
|
|
1279
|
+
isError = isErrorEnvelope(result);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
this.logger(
|
|
1282
|
+
`[session] tool ${toolName} failed (direct ${callId}): ${describeError(error)}`
|
|
1283
|
+
);
|
|
1284
|
+
// Same envelope the model path returns for a throwing tool body.
|
|
1285
|
+
result = {
|
|
1286
|
+
content: [
|
|
1287
|
+
{
|
|
1288
|
+
type: "text",
|
|
1289
|
+
text: `Tool "${toolName}" failed: ${describeError(error)}`,
|
|
1290
|
+
},
|
|
1291
|
+
],
|
|
1292
|
+
isError: true,
|
|
1293
|
+
};
|
|
1294
|
+
isError = true;
|
|
1295
|
+
} finally {
|
|
1296
|
+
if (scratchDir !== undefined) {
|
|
1297
|
+
await rm(scratchDir, { recursive: true, force: true }).catch(() => {});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const durationMs = Date.now() - started;
|
|
1301
|
+
this.logger(
|
|
1302
|
+
`[session] tool ${toolName} done (direct ${callId}, ${durationMs}ms)`
|
|
1303
|
+
);
|
|
1304
|
+
|
|
1305
|
+
if (record !== undefined) {
|
|
1306
|
+
await this.appendEvent(record.sessionId, {
|
|
1307
|
+
type: "action.result",
|
|
1308
|
+
turnId: callId,
|
|
1309
|
+
data: { callId, toolName, output: result, isError },
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const errorMessage = isError ? toolCallErrorMessage(result) : undefined;
|
|
1314
|
+
return {
|
|
1315
|
+
toolName,
|
|
1316
|
+
callId,
|
|
1317
|
+
isError,
|
|
1318
|
+
result,
|
|
1319
|
+
durationMs,
|
|
1320
|
+
...(errorMessage === undefined ? {} : { errorMessage }),
|
|
1321
|
+
...(record === undefined ? {} : { sessionId: record.sessionId }),
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// ==========================================================================
|
|
1326
|
+
// Events: append, dispatch to channel handlers + hooks
|
|
1327
|
+
// ==========================================================================
|
|
1328
|
+
|
|
1329
|
+
private async appendEvent(
|
|
1330
|
+
sessionId: string,
|
|
1331
|
+
payload: SessionEventPayload & { turnId?: string }
|
|
1332
|
+
): Promise<SessionEvent> {
|
|
1333
|
+
const log = await this.logs.get(sessionId);
|
|
1334
|
+
const event = log.append(payload);
|
|
1335
|
+
this.queueDispatch(sessionId, event);
|
|
1336
|
+
return event;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
private queueDispatch(sessionId: string, event: SessionEvent): void {
|
|
1340
|
+
const chain = this.dispatchChains.get(sessionId) ?? Promise.resolve();
|
|
1341
|
+
const next = chain
|
|
1342
|
+
.then(() => this.dispatchEvent(sessionId, event))
|
|
1343
|
+
.catch((error) => {
|
|
1344
|
+
this.logger(
|
|
1345
|
+
`[agent-serve] event dispatch failed for ${sessionId}: ${describeError(error)}`
|
|
1346
|
+
);
|
|
1347
|
+
});
|
|
1348
|
+
this.dispatchChains.set(sessionId, next);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
private async dispatchEvent(
|
|
1352
|
+
sessionId: string,
|
|
1353
|
+
event: SessionEvent
|
|
1354
|
+
): Promise<void> {
|
|
1355
|
+
const record = await this.sessions.get(sessionId);
|
|
1356
|
+
if (record === undefined) {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const sessionCtx = { session: this.sessionInfo(record) };
|
|
1361
|
+
const channelEvents = this.channelEventHandlersFor(record.channelId);
|
|
1362
|
+
if (channelEvents !== undefined) {
|
|
1363
|
+
// Handlers are declared against the narrowed event for their key; the
|
|
1364
|
+
// dispatch key always matches, so widening here is sound.
|
|
1365
|
+
const handlers: Array<ChannelEventHandler | undefined> = [
|
|
1366
|
+
channelEvents[event.type] as ChannelEventHandler | undefined,
|
|
1367
|
+
channelEvents["*"],
|
|
1368
|
+
];
|
|
1369
|
+
const channelCtx = {
|
|
1370
|
+
channelId: record.channelId,
|
|
1371
|
+
state: (record.channelState ?? null) as JsonValue,
|
|
1372
|
+
continuationToken: record.continuationKey,
|
|
1373
|
+
setContinuationToken: (token: string | null) =>
|
|
1374
|
+
this.setContinuationToken(sessionId, token),
|
|
1375
|
+
};
|
|
1376
|
+
for (const handler of handlers) {
|
|
1377
|
+
if (handler === undefined) {
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
try {
|
|
1381
|
+
await handler(event, channelCtx, sessionCtx);
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
this.logger(
|
|
1384
|
+
`[agent-serve] channel "${record.channelId}" handler for ${event.type} threw: ${describeError(error)}`
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
// Handlers mutate channel state in place; persist after each batch.
|
|
1389
|
+
await this.updateRecord(sessionId, (current) => ({ ...current }));
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
for (const hook of this.project.hooks) {
|
|
1393
|
+
const handlers: Array<HookHandler | undefined> = [
|
|
1394
|
+
hook.definition.events[event.type] as HookHandler | undefined,
|
|
1395
|
+
hook.definition.events["*"],
|
|
1396
|
+
];
|
|
1397
|
+
for (const handler of handlers) {
|
|
1398
|
+
if (handler === undefined) {
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
try {
|
|
1402
|
+
await handler(event, {
|
|
1403
|
+
agent: { name: this.project.name },
|
|
1404
|
+
channel: {
|
|
1405
|
+
id: record.channelId,
|
|
1406
|
+
continuationToken: record.continuationKey,
|
|
1407
|
+
},
|
|
1408
|
+
session: this.sessionInfo(record),
|
|
1409
|
+
});
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
this.logger(
|
|
1412
|
+
`[agent-serve] hook "${hook.name}" handler for ${event.type} threw: ${describeError(error)}`
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
if (this.abCollector.enabled) {
|
|
1419
|
+
await this.abCollector.onEvent(
|
|
1420
|
+
event,
|
|
1421
|
+
this.sessionInfo(record),
|
|
1422
|
+
this.project.name
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private channelEventHandlersFor(
|
|
1428
|
+
channelId: string
|
|
1429
|
+
): ChannelDefinition["events"] | undefined {
|
|
1430
|
+
if (channelId === HTTP_CHANNEL_ID) {
|
|
1431
|
+
return this.project.httpChannel.events;
|
|
1432
|
+
}
|
|
1433
|
+
return this.channelsById.get(channelId)?.events;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// ==========================================================================
|
|
1437
|
+
// Session handles
|
|
1438
|
+
// ==========================================================================
|
|
1439
|
+
|
|
1440
|
+
async waitForCompletion(sessionId: string): Promise<TurnOutcome> {
|
|
1441
|
+
// Drain admission first so pendingTurns → activeTurns is visible before we
|
|
1442
|
+
// decide the session is idle (reminders use this for queueBehindTurn).
|
|
1443
|
+
const chain = this.admissionChains.get(sessionId);
|
|
1444
|
+
if (chain !== undefined) {
|
|
1445
|
+
await chain.catch(() => {});
|
|
1446
|
+
}
|
|
1447
|
+
const active = this.activeTurns.get(sessionId);
|
|
1448
|
+
if (active !== undefined) {
|
|
1449
|
+
return active;
|
|
1450
|
+
}
|
|
1451
|
+
return this.lastOutcomes.get(sessionId) ?? { status: "finished" };
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Sessions visible to `caller`, newest first (`GET /v1/sessions`).
|
|
1456
|
+
* Callers only see sessions whose stored principal matches — except when
|
|
1457
|
+
* `includeAll` is set (dev mode), where the listing becomes a full
|
|
1458
|
+
* observability surface across every principal (channel-driven runs,
|
|
1459
|
+
* schedule/task sessions, other callers). The route auth chain still
|
|
1460
|
+
* gates who may call this at all.
|
|
1461
|
+
*/
|
|
1462
|
+
async listSessions(
|
|
1463
|
+
caller: AuthContext | null,
|
|
1464
|
+
options?: { includeAll?: boolean }
|
|
1465
|
+
): Promise<SessionSummary[]> {
|
|
1466
|
+
const includeAll = options?.includeAll ?? false;
|
|
1467
|
+
const records = await this.sessions.list();
|
|
1468
|
+
return records
|
|
1469
|
+
.filter((record) => includeAll || samePrincipal(record.auth, caller))
|
|
1470
|
+
.map((record) => {
|
|
1471
|
+
const externalUrl = externalUrlForSession(record);
|
|
1472
|
+
return {
|
|
1473
|
+
sessionId: record.sessionId,
|
|
1474
|
+
channelId: record.channelId,
|
|
1475
|
+
mode: record.mode,
|
|
1476
|
+
...(record.title === undefined ? {} : { title: record.title }),
|
|
1477
|
+
turnCount: record.turnCount,
|
|
1478
|
+
running:
|
|
1479
|
+
this.activeTurns.has(record.sessionId) ||
|
|
1480
|
+
this.pendingTurns.has(record.sessionId),
|
|
1481
|
+
createdAt: record.createdAt,
|
|
1482
|
+
updatedAt: record.updatedAt,
|
|
1483
|
+
continuationToken:
|
|
1484
|
+
record.continuationKey === null
|
|
1485
|
+
? null
|
|
1486
|
+
: formatContinuationToken(
|
|
1487
|
+
record.channelId,
|
|
1488
|
+
record.continuationKey
|
|
1489
|
+
),
|
|
1490
|
+
...(externalUrl === undefined ? {} : { externalUrl }),
|
|
1491
|
+
};
|
|
1492
|
+
})
|
|
1493
|
+
.reverse();
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Fold durable session streams into per-session A/B assignments and
|
|
1498
|
+
* aggregate arm metrics (`GET /v1/abs`). Collector-independent: always
|
|
1499
|
+
* reads `events.ndjson`.
|
|
1500
|
+
*/
|
|
1501
|
+
async abSnapshot(
|
|
1502
|
+
caller: AuthContext | null,
|
|
1503
|
+
options?: { includeAll?: boolean }
|
|
1504
|
+
): Promise<ABSnapshot> {
|
|
1505
|
+
const sessions = await this.listSessions(caller, options);
|
|
1506
|
+
const abConfig = this.project.abConfig;
|
|
1507
|
+
const maxPlaygroundSessions = resolveABMaxPlaygroundSessions(
|
|
1508
|
+
abConfig?.maxPlaygroundSessions
|
|
1509
|
+
);
|
|
1510
|
+
const foldedSessions = sessions.slice(0, maxPlaygroundSessions);
|
|
1511
|
+
const snapshot = await buildABSnapshot({
|
|
1512
|
+
experiments: this.project.abs,
|
|
1513
|
+
agentName: this.project.name,
|
|
1514
|
+
sessions: foldedSessions,
|
|
1515
|
+
loadRecord: (sessionId) => this.sessions.get(sessionId),
|
|
1516
|
+
loadEvents: (sessionId) => this.listEvents(sessionId, 0),
|
|
1517
|
+
sessionInfo: (record) => this.sessionInfo(record),
|
|
1518
|
+
logger: this.logger,
|
|
1519
|
+
config: {
|
|
1520
|
+
maxPlaygroundSessions,
|
|
1521
|
+
durableSamples: abConfig?.persistSamples !== undefined,
|
|
1522
|
+
durableSnapshots: abConfig?.persistSnapshots !== undefined,
|
|
1523
|
+
},
|
|
1524
|
+
});
|
|
1525
|
+
// Side effect on the read path: playground polls GET /v1/abs ~every 4s.
|
|
1526
|
+
// Authors who set persistSnapshots accept that write cadence.
|
|
1527
|
+
const persistSnapshots = abConfig?.persistSnapshots;
|
|
1528
|
+
if (persistSnapshots !== undefined) {
|
|
1529
|
+
try {
|
|
1530
|
+
await persistSnapshots.save(snapshot, {
|
|
1531
|
+
projectRoot: this.project.rootDir,
|
|
1532
|
+
});
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
this.logger(
|
|
1535
|
+
`[agent-serve] ab persistSnapshots.save threw: ${describeError(error)}`
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return snapshot;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
async eventStream(
|
|
1543
|
+
sessionId: string,
|
|
1544
|
+
startIndex = 0
|
|
1545
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
1546
|
+
const log = await this.logs.get(sessionId);
|
|
1547
|
+
return log.stream(startIndex);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Append an assistant message to a session transcript without a model turn
|
|
1552
|
+
* and without a user `message.received` bubble. Used by host-driven channel
|
|
1553
|
+
* tasks (e.g. webhook stages) to publish a final summary.
|
|
1554
|
+
*/
|
|
1555
|
+
async emitAssistantMessage(sessionId: string, text: string): Promise<void> {
|
|
1556
|
+
if (this.isTurnRunning(sessionId)) {
|
|
1557
|
+
throw new SessionBusyError(sessionId);
|
|
1558
|
+
}
|
|
1559
|
+
const existing = await this.sessions.get(sessionId);
|
|
1560
|
+
if (existing === undefined) {
|
|
1561
|
+
throw new Error(`Unknown session: ${sessionId}`);
|
|
1562
|
+
}
|
|
1563
|
+
const record = await this.updateRecord(sessionId, (current) => ({
|
|
1564
|
+
...current,
|
|
1565
|
+
turnCount: current.turnCount + 1,
|
|
1566
|
+
}));
|
|
1567
|
+
const turnId = `turn_${record.turnCount}`;
|
|
1568
|
+
await this.appendEvent(sessionId, {
|
|
1569
|
+
type: "turn.started",
|
|
1570
|
+
turnId,
|
|
1571
|
+
data: {},
|
|
1572
|
+
});
|
|
1573
|
+
await this.appendEvent(sessionId, {
|
|
1574
|
+
type: "message.completed",
|
|
1575
|
+
turnId,
|
|
1576
|
+
data: { text, finishReason: "stop" },
|
|
1577
|
+
});
|
|
1578
|
+
await this.appendEvent(sessionId, {
|
|
1579
|
+
type: "turn.completed",
|
|
1580
|
+
turnId,
|
|
1581
|
+
data: { result: text },
|
|
1582
|
+
});
|
|
1583
|
+
await this.appendEvent(sessionId, {
|
|
1584
|
+
type: "session.waiting",
|
|
1585
|
+
data: {},
|
|
1586
|
+
});
|
|
1587
|
+
this.lastOutcomes.set(sessionId, {
|
|
1588
|
+
status: "finished",
|
|
1589
|
+
result: text,
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/** Snapshot of recorded events (for download / offline trace). */
|
|
1594
|
+
async listEvents(sessionId: string, startIndex = 0): Promise<SessionEvent[]> {
|
|
1595
|
+
const log = await this.logs.get(sessionId);
|
|
1596
|
+
return log.snapshot(startIndex);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
async setContinuationToken(
|
|
1600
|
+
sessionId: string,
|
|
1601
|
+
token: string | null
|
|
1602
|
+
): Promise<void> {
|
|
1603
|
+
await this.updateRecord(sessionId, (current) => ({
|
|
1604
|
+
...current,
|
|
1605
|
+
continuationKey: token,
|
|
1606
|
+
}));
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
isTurnRunning(sessionId: string): boolean {
|
|
1610
|
+
// Include the admit window (pendingTurns) so busy policies do not treat a
|
|
1611
|
+
// turn that has been accepted but not yet marked active as idle.
|
|
1612
|
+
return this.activeTurns.has(sessionId) || this.pendingTurns.has(sessionId);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
sessionInfo(record: SessionRecord): SessionInfo {
|
|
1616
|
+
return {
|
|
1617
|
+
id: record.sessionId,
|
|
1618
|
+
channelId: record.channelId,
|
|
1619
|
+
mode: record.mode,
|
|
1620
|
+
purpose: record.purpose === "eval" ? "eval" : "live",
|
|
1621
|
+
auth: record.auth,
|
|
1622
|
+
continuationKey: record.continuationKey,
|
|
1623
|
+
...(record.abs === undefined ? {} : { abs: record.abs }),
|
|
1624
|
+
...(record.title === undefined ? {} : { title: record.title }),
|
|
1625
|
+
...(record.sdkAgentId === undefined
|
|
1626
|
+
? {}
|
|
1627
|
+
: { sdkAgentId: record.sdkAgentId }),
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
private channelSession(
|
|
1632
|
+
sessionId: string,
|
|
1633
|
+
record: SessionRecord,
|
|
1634
|
+
isNew: boolean
|
|
1635
|
+
): ChannelSession {
|
|
1636
|
+
return {
|
|
1637
|
+
id: sessionId,
|
|
1638
|
+
continuationToken: record.continuationKey,
|
|
1639
|
+
isNew,
|
|
1640
|
+
info: this.sessionInfo(record),
|
|
1641
|
+
getEventStream: (options?: { startIndex?: number }) =>
|
|
1642
|
+
this.eventStream(sessionId, options?.startIndex ?? 0),
|
|
1643
|
+
waitForCompletion: () => this.waitForCompletion(sessionId),
|
|
1644
|
+
setContinuationToken: (token: string | null) =>
|
|
1645
|
+
this.setContinuationToken(sessionId, token),
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
private resolveChannelId(
|
|
1650
|
+
channel: ChannelDefinition | HttpChannelDefinition | string
|
|
1651
|
+
): string {
|
|
1652
|
+
if (typeof channel === "string") {
|
|
1653
|
+
if (
|
|
1654
|
+
channel !== HTTP_CHANNEL_ID &&
|
|
1655
|
+
!this.channelsById.has(channel) &&
|
|
1656
|
+
!channel.startsWith(SCHEDULE_CHANNEL_PREFIX)
|
|
1657
|
+
) {
|
|
1658
|
+
throw new UnknownChannelError(channel);
|
|
1659
|
+
}
|
|
1660
|
+
return channel;
|
|
1661
|
+
}
|
|
1662
|
+
if (channel.__agentServe === "http-channel") {
|
|
1663
|
+
return HTTP_CHANNEL_ID;
|
|
1664
|
+
}
|
|
1665
|
+
for (const [id, definition] of this.channelsById) {
|
|
1666
|
+
if (definition === channel) {
|
|
1667
|
+
return id;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
throw new UnknownChannelError("<unregistered channel definition>");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
private initialChannelState(
|
|
1674
|
+
channelId: string,
|
|
1675
|
+
override: JsonValue | undefined
|
|
1676
|
+
): JsonValue | undefined {
|
|
1677
|
+
if (override !== undefined) {
|
|
1678
|
+
return structuredClone(override);
|
|
1679
|
+
}
|
|
1680
|
+
const definition = this.channelsById.get(channelId);
|
|
1681
|
+
if (definition?.state === undefined) {
|
|
1682
|
+
return undefined;
|
|
1683
|
+
}
|
|
1684
|
+
return structuredClone(definition.state);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/** Serialized, immutable record updates (latest-wins per session). */
|
|
1688
|
+
private updateRecord(
|
|
1689
|
+
sessionId: string,
|
|
1690
|
+
updater: (current: SessionRecord) => SessionRecord
|
|
1691
|
+
): Promise<SessionRecord> {
|
|
1692
|
+
const chain = this.updateChains.get(sessionId) ?? Promise.resolve();
|
|
1693
|
+
const next = chain.then(async () => {
|
|
1694
|
+
const current = await this.sessions.get(sessionId);
|
|
1695
|
+
if (current === undefined) {
|
|
1696
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
1697
|
+
}
|
|
1698
|
+
const updated = {
|
|
1699
|
+
...updater(current),
|
|
1700
|
+
updatedAt: new Date().toISOString(),
|
|
1701
|
+
};
|
|
1702
|
+
await this.sessions.save(updated);
|
|
1703
|
+
return updated;
|
|
1704
|
+
});
|
|
1705
|
+
this.updateChains.set(
|
|
1706
|
+
sessionId,
|
|
1707
|
+
next.catch(() => {})
|
|
1708
|
+
);
|
|
1709
|
+
return next;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// ==========================================================================
|
|
1713
|
+
// Lifecycle
|
|
1714
|
+
// ==========================================================================
|
|
1715
|
+
|
|
1716
|
+
trackBackground(promise: Promise<unknown>): void {
|
|
1717
|
+
this.backgroundWork.add(promise);
|
|
1718
|
+
void promise
|
|
1719
|
+
.catch(() => {})
|
|
1720
|
+
.finally(() => {
|
|
1721
|
+
this.backgroundWork.delete(promise);
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/** Drain running turns, dispatches, and pending writes, then dispose. */
|
|
1726
|
+
async close(): Promise<void> {
|
|
1727
|
+
this.approvals.cancelAll("deny");
|
|
1728
|
+
await Promise.allSettled([...this.activeTurns.values()]);
|
|
1729
|
+
await Promise.allSettled([...this.backgroundWork]);
|
|
1730
|
+
await Promise.allSettled([...this.dispatchChains.values()]);
|
|
1731
|
+
await Promise.allSettled([...this.updateChains.values()]);
|
|
1732
|
+
await this.logs.flushAll();
|
|
1733
|
+
await this.mcpHost.close();
|
|
1734
|
+
await this.runner.dispose?.();
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function subagentPrompt(subagent: ResolvedAgent): string {
|
|
1739
|
+
if (subagent.instructions !== undefined && subagent.instructions !== "") {
|
|
1740
|
+
return subagent.instructions;
|
|
1741
|
+
}
|
|
1742
|
+
return subagent.description ?? subagent.name;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function describeError(error: unknown): string {
|
|
1746
|
+
return error instanceof Error ? error.message : String(error);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/** Detects the `{ content, isError: true }` rich-result error envelope. */
|
|
1750
|
+
function isErrorEnvelope(result: unknown): boolean {
|
|
1751
|
+
return (
|
|
1752
|
+
typeof result === "object" &&
|
|
1753
|
+
result !== null &&
|
|
1754
|
+
!Array.isArray(result) &&
|
|
1755
|
+
Array.isArray((result as { content?: unknown }).content) &&
|
|
1756
|
+
(result as { isError?: unknown }).isError === true
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
async function evaluateNeedsApproval(
|
|
1761
|
+
needsApproval: ToolNeedsApproval<never> | undefined,
|
|
1762
|
+
input: unknown
|
|
1763
|
+
): Promise<boolean> {
|
|
1764
|
+
if (needsApproval === undefined || needsApproval === false) {
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
if (needsApproval === true) {
|
|
1768
|
+
return true;
|
|
1769
|
+
}
|
|
1770
|
+
return Boolean(await needsApproval(input as never));
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function approvalResolverLabel(auth: AuthContext | null): string | undefined {
|
|
1774
|
+
if (auth === null) {
|
|
1775
|
+
return undefined;
|
|
1776
|
+
}
|
|
1777
|
+
const slackUser = auth.attributes?.user_id;
|
|
1778
|
+
if (
|
|
1779
|
+
auth.authenticator === "slack" &&
|
|
1780
|
+
slackUser !== undefined &&
|
|
1781
|
+
slackUser !== ""
|
|
1782
|
+
) {
|
|
1783
|
+
return slackUser;
|
|
1784
|
+
}
|
|
1785
|
+
return auth.principalId;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/** Deep link to the external conversation (Slack thread today). */
|
|
1789
|
+
function externalUrlForSession(record: SessionRecord): string | undefined {
|
|
1790
|
+
const auth = record.auth;
|
|
1791
|
+
if (auth === null || auth.authenticator !== "slack") {
|
|
1792
|
+
return undefined;
|
|
1793
|
+
}
|
|
1794
|
+
const channelId = auth.attributes?.channel_id ?? "";
|
|
1795
|
+
const threadTs = auth.attributes?.thread_ts ?? "";
|
|
1796
|
+
const teamId = auth.attributes?.team_id;
|
|
1797
|
+
const url = slackThreadPermalink({
|
|
1798
|
+
channelId,
|
|
1799
|
+
threadTs,
|
|
1800
|
+
...(teamId === undefined ? {} : { teamId }),
|
|
1801
|
+
});
|
|
1802
|
+
return url ?? undefined;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
/** Let the HTTP NDJSON writer flush before the next SDK update. */
|
|
1806
|
+
function yieldEventLoop(): Promise<void> {
|
|
1807
|
+
return new Promise((resolve) => {
|
|
1808
|
+
setImmediate(resolve);
|
|
1809
|
+
});
|
|
1810
|
+
}
|