handmux 0.23.0 → 0.25.0
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/README.md +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { ConversationContractError } from '../agent-runtime/conversation.js';
|
|
3
|
+
import { ConversationActivationError } from '../agent-runtime/conversationActivation.js';
|
|
4
|
+
import { ConversationControlError } from '../agent-runtime/conversationControls.js';
|
|
5
|
+
import { ConversationLiveHub, ConversationLiveHubError, } from '../agent-runtime/conversationLiveHub.js';
|
|
6
|
+
import { InteractionContractError } from '../agent-runtime/interaction.js';
|
|
7
|
+
import { InteractionLiveHub, InteractionLiveHubError, } from '../agent-runtime/interactionLiveHub.js';
|
|
8
|
+
import { AgentResourceError } from '../agent-runtime/resources.js';
|
|
9
|
+
import { SessionControlContractError } from '../agent-runtime/sessionControl.js';
|
|
10
|
+
const RESOURCE_ID_RE = /^[a-zA-Z0-9_-]{16,256}$/;
|
|
11
|
+
function record(value) {
|
|
12
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
? value : null;
|
|
14
|
+
}
|
|
15
|
+
function text(value, max) {
|
|
16
|
+
return typeof value === 'string' && value.length > 0 && value.length <= max;
|
|
17
|
+
}
|
|
18
|
+
function notificationIds(value) {
|
|
19
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 256
|
|
20
|
+
|| value.some((item) => !text(item, 256)))
|
|
21
|
+
return null;
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function session(value) {
|
|
25
|
+
const item = record(value);
|
|
26
|
+
return item && text(item.agentId, 64) && text(item.sessionId, 1024)
|
|
27
|
+
? { agentId: item.agentId, sessionId: item.sessionId } : null;
|
|
28
|
+
}
|
|
29
|
+
function run(value) {
|
|
30
|
+
const item = record(value);
|
|
31
|
+
if (!item || !text(item.agentId, 64) || !text(item.paneId, 256) || !text(item.runId, 256)
|
|
32
|
+
|| (item.sessionId !== undefined && !text(item.sessionId, 1024)))
|
|
33
|
+
return null;
|
|
34
|
+
return {
|
|
35
|
+
agentId: item.agentId,
|
|
36
|
+
paneId: item.paneId,
|
|
37
|
+
runId: item.runId,
|
|
38
|
+
...(item.sessionId === undefined ? {} : { sessionId: item.sessionId }),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Auth is applied by the parent /api router. This is Handmux's own app facade, not a public Connector
|
|
42
|
+
// protocol: it exposes normalized Core objects and never socket paths, credentials, native payloads, or wire.
|
|
43
|
+
export function agentRoutes({ runtime }) {
|
|
44
|
+
const router = express.Router();
|
|
45
|
+
let conversationLive;
|
|
46
|
+
let interactionLive;
|
|
47
|
+
router.get('/agents/capabilities', (_req, res) => res.json({
|
|
48
|
+
adapters: runtime.capabilities(),
|
|
49
|
+
health: runtime.health(),
|
|
50
|
+
runs: runtime.activeRuns(),
|
|
51
|
+
}));
|
|
52
|
+
router.get('/agents/inbox', (_req, res) => res.json(runtime.inbox.read()));
|
|
53
|
+
router.get('/agents/usage', async (req, res, next) => {
|
|
54
|
+
if (!runtime.subscriptionUsage) {
|
|
55
|
+
return res.status(503).json({ error: 'subscription usage unsupported', code: 'unsupported' });
|
|
56
|
+
}
|
|
57
|
+
const refresh = req.query.refresh;
|
|
58
|
+
if (refresh !== undefined && refresh !== 'true' && refresh !== 'false') {
|
|
59
|
+
return res.status(400).json({ error: 'invalid subscription usage refresh' });
|
|
60
|
+
}
|
|
61
|
+
const target = req.query.target;
|
|
62
|
+
if (target !== undefined && (typeof target !== 'string' || refresh !== 'true'
|
|
63
|
+
|| !/^[a-z][a-z0-9._-]{0,127}$/.test(target))) {
|
|
64
|
+
return res.status(400).json({ error: 'invalid subscription usage target' });
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return res.json({ agents: await runtime.subscriptionUsage.snapshots({
|
|
68
|
+
refresh: refresh === 'true',
|
|
69
|
+
...(target === undefined ? {} : { targetAgentId: target }),
|
|
70
|
+
}) });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof RangeError && target !== undefined) {
|
|
74
|
+
return res.status(400).json({ error: 'invalid subscription usage target' });
|
|
75
|
+
}
|
|
76
|
+
return next(error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
router.get('/agents/session-control/model', async (req, res, next) => {
|
|
80
|
+
try {
|
|
81
|
+
if (!runtime.sessionControl) {
|
|
82
|
+
return res.status(503).json({ error: 'session control unsupported', code: 'unsupported' });
|
|
83
|
+
}
|
|
84
|
+
const ref = run(req.query);
|
|
85
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
86
|
+
if (!ref || !lease)
|
|
87
|
+
return res.status(ref ? 409 : 400).json({
|
|
88
|
+
error: ref ? 'stale agent run' : 'invalid agent run',
|
|
89
|
+
});
|
|
90
|
+
const refresh = req.query.refresh;
|
|
91
|
+
if (refresh !== undefined && refresh !== 'true' && refresh !== 'false') {
|
|
92
|
+
return res.status(400).json({ error: 'invalid session control refresh' });
|
|
93
|
+
}
|
|
94
|
+
const control = await runtime.sessionControl.readModelControl(lease, {
|
|
95
|
+
refresh: refresh === 'true',
|
|
96
|
+
});
|
|
97
|
+
return res.json({ control });
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof SessionControlContractError && error.code === 'unsupported') {
|
|
101
|
+
return res.status(409).json({ error: 'session control unsupported', code: 'unsupported' });
|
|
102
|
+
}
|
|
103
|
+
return next(error);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
router.patch('/agents/session-control/model', async (req, res, next) => {
|
|
107
|
+
try {
|
|
108
|
+
if (!runtime.sessionControl) {
|
|
109
|
+
return res.status(503).json({ error: 'session control unsupported', code: 'unsupported' });
|
|
110
|
+
}
|
|
111
|
+
const ref = run(req.body?.run);
|
|
112
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
113
|
+
const patch = record(req.body?.patch);
|
|
114
|
+
if (!ref || !lease || !patch)
|
|
115
|
+
return res.status(ref && patch ? 409 : 400).json({
|
|
116
|
+
error: ref && patch ? 'stale agent run' : 'invalid session control update',
|
|
117
|
+
});
|
|
118
|
+
return res.json({
|
|
119
|
+
control: await runtime.sessionControl.updateModelControl(lease, patch),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof SessionControlContractError) {
|
|
124
|
+
if (error.code === 'invalid_request') {
|
|
125
|
+
return res.status(400).json({ error: 'invalid session control update' });
|
|
126
|
+
}
|
|
127
|
+
if (error.code === 'unsupported') {
|
|
128
|
+
return res.status(409).json({ error: 'session control unsupported', code: 'unsupported' });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return next(error);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
router.post('/agents/inbox/notifications/read', async (req, res, next) => {
|
|
135
|
+
const ids = notificationIds(req.body?.notificationIds);
|
|
136
|
+
if (!ids)
|
|
137
|
+
return res.status(400).json({ error: 'invalid terminal notification ids' });
|
|
138
|
+
try {
|
|
139
|
+
return res.json(await runtime.inbox.markTerminalRead(ids));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return next(error);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const controlRun = (value) => {
|
|
146
|
+
const ref = run(value);
|
|
147
|
+
return { ref, lease: ref ? runtime.runs.resolve(ref) : null };
|
|
148
|
+
};
|
|
149
|
+
const controlError = (error, res) => {
|
|
150
|
+
if (!(error instanceof ConversationControlError))
|
|
151
|
+
return false;
|
|
152
|
+
const status = error.code === 'invalid_request' ? 400
|
|
153
|
+
: error.code === 'unsupported' ? 409 : 502;
|
|
154
|
+
res.status(status).json({ error: error.message, code: error.code });
|
|
155
|
+
return true;
|
|
156
|
+
};
|
|
157
|
+
const activationError = (error, res) => {
|
|
158
|
+
if (!(error instanceof ConversationActivationError))
|
|
159
|
+
return false;
|
|
160
|
+
const status = error.code === 'unsupported' ? 409
|
|
161
|
+
: error.code === 'in_progress' ? 409
|
|
162
|
+
: error.code === 'contract_violation' ? 502 : 503;
|
|
163
|
+
res.status(status).json({ error: error.message, code: error.code });
|
|
164
|
+
return true;
|
|
165
|
+
};
|
|
166
|
+
router.get('/agents/conversation-activation', async (req, res, next) => {
|
|
167
|
+
try {
|
|
168
|
+
if (!runtime.conversationActivation) {
|
|
169
|
+
return res.status(409).json({ error: 'conversation activation unsupported', code: 'unsupported' });
|
|
170
|
+
}
|
|
171
|
+
const { ref, lease } = controlRun(req.query);
|
|
172
|
+
if (!ref || !lease)
|
|
173
|
+
return res.status(ref ? 409 : 400).json({
|
|
174
|
+
error: ref ? 'stale agent run' : 'invalid agent run',
|
|
175
|
+
...(ref ? { code: 'stale_run' } : {}),
|
|
176
|
+
});
|
|
177
|
+
return res.json({ descriptor: await runtime.conversationActivation.describe(lease) });
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (activationError(error, res))
|
|
181
|
+
return;
|
|
182
|
+
return next(error);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
router.post('/agents/conversation-activation', async (req, res, next) => {
|
|
186
|
+
try {
|
|
187
|
+
if (!runtime.conversationActivation) {
|
|
188
|
+
return res.status(409).json({ error: 'conversation activation unsupported', code: 'unsupported' });
|
|
189
|
+
}
|
|
190
|
+
const { ref, lease } = controlRun(req.body?.run);
|
|
191
|
+
if (!ref || !lease)
|
|
192
|
+
return res.status(ref ? 409 : 400).json({
|
|
193
|
+
error: ref ? 'stale agent run' : 'invalid agent run',
|
|
194
|
+
...(ref ? { code: 'stale_run' } : {}),
|
|
195
|
+
});
|
|
196
|
+
const operation = new AbortController();
|
|
197
|
+
const cancel = () => {
|
|
198
|
+
if (!res.writableEnded)
|
|
199
|
+
operation.abort(new Error('Activation request disconnected'));
|
|
200
|
+
};
|
|
201
|
+
req.once('aborted', cancel);
|
|
202
|
+
res.once('close', cancel);
|
|
203
|
+
try {
|
|
204
|
+
await runtime.conversationActivation.activate(lease, operation.signal);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
req.removeListener('aborted', cancel);
|
|
208
|
+
res.removeListener('close', cancel);
|
|
209
|
+
}
|
|
210
|
+
return res.status(202).json({ accepted: true });
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (activationError(error, res))
|
|
214
|
+
return;
|
|
215
|
+
return next(error);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
router.get('/agents/conversation-controls', async (req, res, next) => {
|
|
219
|
+
try {
|
|
220
|
+
if (!runtime.conversationControls && !runtime.conversation) {
|
|
221
|
+
return res.status(409).json({ error: 'conversation controls unsupported', code: 'unsupported' });
|
|
222
|
+
}
|
|
223
|
+
const { ref, lease } = controlRun(req.query);
|
|
224
|
+
if (!ref || !lease)
|
|
225
|
+
return res.status(ref ? 409 : 400).json({
|
|
226
|
+
error: ref ? 'stale agent run' : 'invalid agent run',
|
|
227
|
+
});
|
|
228
|
+
let controls = {};
|
|
229
|
+
if (runtime.conversationControls) {
|
|
230
|
+
try {
|
|
231
|
+
controls = await runtime.conversationControls.read(lease);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
// Queue is a public Conversation capability. An Agent without optional Goal/Plan/Context
|
|
235
|
+
// controls must still reach the shared Queue snapshot; every other control failure remains fatal.
|
|
236
|
+
if (!(runtime.conversation && error instanceof ConversationControlError
|
|
237
|
+
&& error.code === 'unsupported'))
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const queue = runtime.conversation ? await runtime.conversation.queueSnapshot(lease) : undefined;
|
|
242
|
+
return res.json({ controls: { ...controls, ...(queue ? { queue, submissions: queue.submissions } : {}) } });
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (controlError(error, res))
|
|
246
|
+
return;
|
|
247
|
+
return next(error);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
router.post('/agents/conversation-queue/action', async (req, res, next) => {
|
|
251
|
+
try {
|
|
252
|
+
if (!runtime.conversation)
|
|
253
|
+
throw new ConversationControlError('Conversation queue unsupported', 'unsupported');
|
|
254
|
+
const { ref, lease } = controlRun(req.body?.run);
|
|
255
|
+
const request = record(req.body?.request);
|
|
256
|
+
if (!ref || !lease || !request)
|
|
257
|
+
return res.status(ref && request ? 409 : 400).json({
|
|
258
|
+
error: ref && request ? 'stale agent run' : 'invalid conversation queue action',
|
|
259
|
+
});
|
|
260
|
+
return res.json(await runtime.conversation.queueAction(lease, request));
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (controlError(error, res))
|
|
264
|
+
return;
|
|
265
|
+
return next(error);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
router.post('/agents/conversation-goal/action', async (req, res, next) => {
|
|
269
|
+
try {
|
|
270
|
+
if (!runtime.conversationControls)
|
|
271
|
+
throw new ConversationControlError('Conversation goal unsupported', 'unsupported');
|
|
272
|
+
const { ref, lease } = controlRun(req.body?.run);
|
|
273
|
+
const request = record(req.body?.request);
|
|
274
|
+
if (!ref || !lease || !request)
|
|
275
|
+
return res.status(ref && request ? 409 : 400).json({
|
|
276
|
+
error: ref && request ? 'stale agent run' : 'invalid conversation goal action',
|
|
277
|
+
});
|
|
278
|
+
return res.json({ goal: await runtime.conversationControls.goalAction(lease, request) });
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (controlError(error, res))
|
|
282
|
+
return;
|
|
283
|
+
return next(error);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
router.post('/agents/conversation-permission/action', async (req, res, next) => {
|
|
287
|
+
try {
|
|
288
|
+
if (!runtime.conversationControls)
|
|
289
|
+
throw new ConversationControlError('Conversation permission unsupported', 'unsupported');
|
|
290
|
+
const { ref, lease } = controlRun(req.body?.run);
|
|
291
|
+
const request = record(req.body?.request);
|
|
292
|
+
if (!ref || !lease || !request)
|
|
293
|
+
return res.status(ref && request ? 409 : 400).json({
|
|
294
|
+
error: ref && request ? 'stale agent run' : 'invalid conversation permission action',
|
|
295
|
+
});
|
|
296
|
+
return res.json({ permission: await runtime.conversationControls.permissionAction(lease, request) });
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (controlError(error, res))
|
|
300
|
+
return;
|
|
301
|
+
return next(error);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
router.post('/agents/conversation-command', async (req, res, next) => {
|
|
305
|
+
try {
|
|
306
|
+
if (!runtime.conversationControls)
|
|
307
|
+
throw new ConversationControlError('Conversation command unsupported', 'unsupported');
|
|
308
|
+
const { ref, lease } = controlRun(req.body?.run);
|
|
309
|
+
if (!ref || !lease)
|
|
310
|
+
return res.status(ref ? 409 : 400).json({
|
|
311
|
+
error: ref ? 'stale agent run' : 'invalid agent run',
|
|
312
|
+
});
|
|
313
|
+
await runtime.conversationControls.command(lease, req.body?.command);
|
|
314
|
+
return res.json({ ok: true });
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
if (controlError(error, res))
|
|
318
|
+
return;
|
|
319
|
+
return next(error);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
router.post('/agents/conversation/discover', async (req, res, next) => {
|
|
323
|
+
try {
|
|
324
|
+
if (!runtime.conversation)
|
|
325
|
+
return res.status(409).json({ error: 'conversation unsupported' });
|
|
326
|
+
const ref = run(req.body?.target);
|
|
327
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
328
|
+
if (ref && !lease)
|
|
329
|
+
return res.status(409).json({ error: 'stale agent run' });
|
|
330
|
+
const target = lease?.ref ?? session(req.body?.target);
|
|
331
|
+
if (!target)
|
|
332
|
+
return res.status(400).json({ error: 'invalid agent target' });
|
|
333
|
+
return res.json({ descriptor: await runtime.conversation.discover(target) });
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
return next(error);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
router.post('/agents/conversation/page', async (req, res, next) => {
|
|
340
|
+
try {
|
|
341
|
+
if (!runtime.conversation)
|
|
342
|
+
return res.status(409).json({ error: 'conversation unsupported' });
|
|
343
|
+
const ref = run(req.body?.run);
|
|
344
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
345
|
+
if (ref && !lease)
|
|
346
|
+
return res.status(409).json({ error: 'stale agent run' });
|
|
347
|
+
const target = lease?.ref ?? session(req.body?.session);
|
|
348
|
+
const request = record(req.body?.request);
|
|
349
|
+
if (!target || !request)
|
|
350
|
+
return res.status(400).json({ error: 'invalid conversation page request' });
|
|
351
|
+
return res.json(await runtime.conversation.readPage(target, request));
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
if (error instanceof ConversationContractError) {
|
|
355
|
+
if (error.code === 'invalid_request') {
|
|
356
|
+
return res.status(400).json({ error: 'invalid conversation page request' });
|
|
357
|
+
}
|
|
358
|
+
// Page cursors and view epochs are intentionally process-local. A Server restart or a concurrent
|
|
359
|
+
// history change is a recoverable rebase signal for the client, not an internal error/log storm.
|
|
360
|
+
if (error.code === 'page_stale') {
|
|
361
|
+
return res.status(409).json({ error: 'conversation page stale' });
|
|
362
|
+
}
|
|
363
|
+
if (error.code === 'session_unavailable') {
|
|
364
|
+
res.set('Retry-After', '1');
|
|
365
|
+
return res.status(503).json({
|
|
366
|
+
error: 'conversation session unavailable',
|
|
367
|
+
code: 'conversation_session_unavailable',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return next(error);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
router.get('/agents/conversation/resource', async (req, res, next) => {
|
|
375
|
+
const owner = session(req.query);
|
|
376
|
+
const resourceId = typeof req.query.resourceId === 'string' ? req.query.resourceId : '';
|
|
377
|
+
if (!owner || !RESOURCE_ID_RE.test(resourceId)) {
|
|
378
|
+
return res.status(400).json({ error: 'invalid conversation resource request' });
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
const content = await runtime.resources.read(owner, resourceId);
|
|
382
|
+
// Owner mismatch, expiry, revocation, and an unknown opaque ID are deliberately indistinguishable.
|
|
383
|
+
if (!content)
|
|
384
|
+
return res.status(404).json({ error: 'conversation resource unavailable' });
|
|
385
|
+
res.set({
|
|
386
|
+
'Cache-Control': 'private, no-store',
|
|
387
|
+
'X-Content-Type-Options': 'nosniff',
|
|
388
|
+
'Content-Length': String(content.size),
|
|
389
|
+
});
|
|
390
|
+
res.attachment(content.name ?? 'agent-resource');
|
|
391
|
+
res.type(content.mediaType);
|
|
392
|
+
return res.status(200).send(Buffer.from(content.data));
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
if (error instanceof AgentResourceError && error.code === 'resource-changed') {
|
|
396
|
+
return res.status(404).json({ error: 'conversation resource unavailable' });
|
|
397
|
+
}
|
|
398
|
+
return next(error);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
router.post('/agents/conversation/send', async (req, res, next) => {
|
|
402
|
+
try {
|
|
403
|
+
if (!runtime.conversation)
|
|
404
|
+
return res.status(409).json({ error: 'conversation unsupported' });
|
|
405
|
+
const ref = run(req.body?.run);
|
|
406
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
407
|
+
const request = record(req.body?.request);
|
|
408
|
+
if (!ref || !lease || !request)
|
|
409
|
+
return res.status(ref && request ? 409 : 400).json({
|
|
410
|
+
error: ref && request ? 'stale agent run' : 'invalid conversation send request',
|
|
411
|
+
});
|
|
412
|
+
return res.json(await runtime.conversation.send(lease, request));
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
return next(error);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
router.post('/agents/conversation/submission/query', (req, res, next) => {
|
|
419
|
+
try {
|
|
420
|
+
if (!runtime.conversation)
|
|
421
|
+
return res.status(409).json({ error: 'conversation unsupported' });
|
|
422
|
+
const ref = run(req.body?.run);
|
|
423
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
424
|
+
const submissionId = req.body?.submissionId;
|
|
425
|
+
const actionId = req.body?.actionId;
|
|
426
|
+
if (!ref || !lease || !text(submissionId, 256)
|
|
427
|
+
|| (actionId !== undefined && !text(actionId, 256))) {
|
|
428
|
+
return res.status(ref && submissionId ? 409 : 400).json({
|
|
429
|
+
error: ref && submissionId ? 'stale agent run' : 'invalid conversation submission query',
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return res.json(runtime.conversation.querySubmission(lease, submissionId, actionId));
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
return next(error);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
router.post('/agents/conversation/interrupt', async (req, res, next) => {
|
|
439
|
+
try {
|
|
440
|
+
if (!runtime.conversation)
|
|
441
|
+
return res.status(409).json({ error: 'conversation unsupported' });
|
|
442
|
+
const ref = run(req.body?.run);
|
|
443
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
444
|
+
if (!ref || !lease)
|
|
445
|
+
return res.status(ref ? 409 : 400).json({
|
|
446
|
+
error: ref ? 'stale agent run' : 'invalid agent run',
|
|
447
|
+
});
|
|
448
|
+
return res.json(await runtime.conversation.interrupt(lease));
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
return next(error);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
router.get('/agents/conversation/live', async (req, res, next) => {
|
|
455
|
+
let subscription;
|
|
456
|
+
let keepalive;
|
|
457
|
+
let closed = false;
|
|
458
|
+
const close = () => {
|
|
459
|
+
if (closed)
|
|
460
|
+
return;
|
|
461
|
+
closed = true;
|
|
462
|
+
if (keepalive)
|
|
463
|
+
clearInterval(keepalive);
|
|
464
|
+
subscription?.close();
|
|
465
|
+
};
|
|
466
|
+
req.once('aborted', close);
|
|
467
|
+
res.once('close', close);
|
|
468
|
+
try {
|
|
469
|
+
if (!runtime.conversation)
|
|
470
|
+
return res.status(409).json({ error: 'conversation unsupported' });
|
|
471
|
+
conversationLive ??= new ConversationLiveHub({ conversation: runtime.conversation });
|
|
472
|
+
const ref = run(req.query);
|
|
473
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
474
|
+
if (!ref || !lease)
|
|
475
|
+
return res.status(ref ? 409 : 400).json({
|
|
476
|
+
error: ref ? 'stale agent run' : 'invalid agent run',
|
|
477
|
+
});
|
|
478
|
+
const expectedViewId = typeof req.query.expectedViewId === 'string'
|
|
479
|
+
? req.query.expectedViewId : undefined;
|
|
480
|
+
if (expectedViewId !== undefined && !text(expectedViewId, 1024)) {
|
|
481
|
+
return res.status(400).json({ error: 'invalid conversation view' });
|
|
482
|
+
}
|
|
483
|
+
subscription = await conversationLive.subscribe(lease, {
|
|
484
|
+
...(expectedViewId === undefined ? {} : { expectedViewId }),
|
|
485
|
+
});
|
|
486
|
+
if (closed) {
|
|
487
|
+
subscription.close();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
res.status(200).set({
|
|
491
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
492
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
493
|
+
Connection: 'keep-alive',
|
|
494
|
+
'X-Accel-Buffering': 'no',
|
|
495
|
+
});
|
|
496
|
+
res.flushHeaders();
|
|
497
|
+
res.write(`data: ${JSON.stringify({ type: 'ready', checkpoint: subscription.checkpoint })}\n\n`);
|
|
498
|
+
keepalive = setInterval(() => {
|
|
499
|
+
if (!closed)
|
|
500
|
+
res.write(`: keepalive ${Date.now()}\n\n`);
|
|
501
|
+
}, 20_000);
|
|
502
|
+
keepalive.unref?.();
|
|
503
|
+
for await (const event of subscription) {
|
|
504
|
+
if (closed)
|
|
505
|
+
break;
|
|
506
|
+
const writable = res.write(`data: ${JSON.stringify({ type: 'event', event })}\n\n`);
|
|
507
|
+
if (!writable) {
|
|
508
|
+
await new Promise((resolve) => {
|
|
509
|
+
const done = () => {
|
|
510
|
+
res.off('drain', done);
|
|
511
|
+
res.off('close', done);
|
|
512
|
+
resolve();
|
|
513
|
+
};
|
|
514
|
+
res.once('drain', done);
|
|
515
|
+
res.once('close', done);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
close();
|
|
520
|
+
return res.end();
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
close();
|
|
524
|
+
if (res.headersSent) {
|
|
525
|
+
res.write(`data: ${JSON.stringify({ type: 'error', message: 'conversation live stream unavailable' })}\n\n`);
|
|
526
|
+
return res.end();
|
|
527
|
+
}
|
|
528
|
+
if (error instanceof ConversationLiveHubError || error instanceof ConversationContractError) {
|
|
529
|
+
return res.status(409).json({ error: 'conversation live stream unavailable' });
|
|
530
|
+
}
|
|
531
|
+
return next(error);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
router.post('/agents/interaction/respond', async (req, res, next) => {
|
|
535
|
+
try {
|
|
536
|
+
if (!runtime.interaction)
|
|
537
|
+
return res.status(409).json({ error: 'interaction unsupported' });
|
|
538
|
+
const ref = run(req.body?.run);
|
|
539
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
540
|
+
const request = record(req.body?.request);
|
|
541
|
+
if (!ref || !lease || !request)
|
|
542
|
+
return res.status(ref && request ? 409 : 400).json({
|
|
543
|
+
error: ref && request ? 'stale agent run' : 'invalid interaction response',
|
|
544
|
+
});
|
|
545
|
+
const result = await runtime.interaction.respond(lease, request);
|
|
546
|
+
return res.status(result.status === 'stale_run' ? 409 : 200).json(result);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
if (error instanceof InteractionContractError) {
|
|
550
|
+
return res.status(409).json({
|
|
551
|
+
error: 'interaction response unavailable', code: 'interaction_unavailable',
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return next(error);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
router.get('/agents/interaction/live', async (req, res, next) => {
|
|
558
|
+
let subscription;
|
|
559
|
+
let keepalive;
|
|
560
|
+
let closed = false;
|
|
561
|
+
const close = () => {
|
|
562
|
+
if (closed)
|
|
563
|
+
return;
|
|
564
|
+
closed = true;
|
|
565
|
+
if (keepalive)
|
|
566
|
+
clearInterval(keepalive);
|
|
567
|
+
subscription?.close();
|
|
568
|
+
};
|
|
569
|
+
req.once('aborted', close);
|
|
570
|
+
res.once('close', close);
|
|
571
|
+
try {
|
|
572
|
+
if (!runtime.interaction)
|
|
573
|
+
return res.status(409).json({ error: 'interaction unsupported' });
|
|
574
|
+
interactionLive ??= new InteractionLiveHub({ interaction: runtime.interaction });
|
|
575
|
+
const ref = run(req.query);
|
|
576
|
+
const lease = ref ? runtime.runs.resolve(ref) : null;
|
|
577
|
+
if (!ref || !lease)
|
|
578
|
+
return res.status(ref ? 409 : 400).json({
|
|
579
|
+
error: ref ? 'stale agent run' : 'invalid agent run',
|
|
580
|
+
});
|
|
581
|
+
subscription = await interactionLive.subscribe(lease);
|
|
582
|
+
if (closed) {
|
|
583
|
+
subscription.close();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
res.status(200).set({
|
|
587
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
588
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
589
|
+
Connection: 'keep-alive',
|
|
590
|
+
'X-Accel-Buffering': 'no',
|
|
591
|
+
});
|
|
592
|
+
res.flushHeaders();
|
|
593
|
+
res.write(`data: ${JSON.stringify({ type: 'ready', checkpoint: subscription.checkpoint })}\n\n`);
|
|
594
|
+
keepalive = setInterval(() => {
|
|
595
|
+
if (!closed)
|
|
596
|
+
res.write(`: keepalive ${Date.now()}\n\n`);
|
|
597
|
+
}, 20_000);
|
|
598
|
+
keepalive.unref?.();
|
|
599
|
+
for await (const event of subscription) {
|
|
600
|
+
if (closed)
|
|
601
|
+
break;
|
|
602
|
+
const writable = res.write(`data: ${JSON.stringify({ type: 'event', event })}\n\n`);
|
|
603
|
+
if (!writable) {
|
|
604
|
+
await new Promise((resolve) => {
|
|
605
|
+
const done = () => {
|
|
606
|
+
res.off('drain', done);
|
|
607
|
+
res.off('close', done);
|
|
608
|
+
resolve();
|
|
609
|
+
};
|
|
610
|
+
res.once('drain', done);
|
|
611
|
+
res.once('close', done);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
close();
|
|
616
|
+
return res.end();
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
close();
|
|
620
|
+
if (res.headersSent) {
|
|
621
|
+
res.write(`data: ${JSON.stringify({ type: 'error', message: 'interaction live stream unavailable' })}\n\n`);
|
|
622
|
+
return res.end();
|
|
623
|
+
}
|
|
624
|
+
if (error instanceof InteractionLiveHubError || error instanceof InteractionContractError) {
|
|
625
|
+
return res.status(409).json({ error: 'interaction live stream unavailable' });
|
|
626
|
+
}
|
|
627
|
+
return next(error);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
return router;
|
|
631
|
+
}
|