beeos-claw 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-gateway/bridge-core.js +1 -1
- package/dist/acp-gateway/gateway-events.js +1 -1
- package/dist/acp-gateway/transport.js +1 -1
- package/dist/canvas-api-client.js +1 -1
- package/dist/canvas-tools.js +1 -1
- package/dist/errors/codes.js +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/file-stage.js +1 -1
- package/dist/gateway-client.js +1 -1
- package/dist/terminal-bridge-handler.js +1 -1
- package/dist/terminal-rpc.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID}from"node:crypto";import{sanitizePromptPayload}from"../message-filter.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas-tools.js";import{MessageQueue}from"../utils/mq.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,groupIntoTurns}from"./history-replay.js";import{appendHistoryEntry}from"./local-session-history.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS}from"./types.js";const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const a=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return a&&!a.includes("/")&&SLASH_COMMAND_NAME_RE.test(a)?a.toLowerCase():void 0}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;initialized=!1;handleTerminalRequest=null;handleTerminalNotification=null;handleSlashCommand=null;onPromptStart=null;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});try{switch(e.method){case"initialize":this.handleInitialize(e);break;case"session/new":await this.handleSessionNew(e);break;case"session/load":await this.handleSessionLoad(e);break;case"session/list":await this.handleSessionList(e);break;case"session/rename":this.handleSessionRename(e);break;case"session/delete":this.handleSessionDelete(e);break;case"session/prompt":await this.handleSessionPrompt(e);break;case"session/set_mode":await this.handleSessionSetMode(e);break;case"session/set_model":await this.handleSessionSetModel(e);break;case"skills/list":await this.handleSkillsList(e);break;case"session/cancel":this.handleSessionCancel(e.id,e.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(e);break;default:e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendError(e.id,-32601,`Method not found: ${e.method}`)}}catch(t){this.transport.sendError(e.id,-32603,t instanceof Error?t.message:"Internal error")}}handleCanvasNotification=null;getCanvasState=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/action":case"canvas/toggle":case"canvas/clear":case"canvas/delete":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const t=e.params,s=t?.surfaceId,a=this.state.resolveSessionFromParams(t);if(!a){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}a.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t=`session:${randomUUID()}`,s=`agent:main:${randomUUID()}`,a="string"==typeof e.params?.cwd?e.params.cwd:".",i={sessionId:t,gatewaySessionKey:s,createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:a};this.state.sessions.set(t,i),this.state.sessionsByGatewayKey.set(s,i),this.transport.sendResult(e.id,{sessionId:t,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendError(e.id,-32602,"sessionId required");let s=this.state.sessions.get(t);if(!s){const e=t.startsWith("agent:")?t:`agent:main:${t}`;s={sessionId:t,gatewaySessionKey:e,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(t,s),this.state.sessionsByGatewayKey.set(e,s)}this.state.lastActiveSessionId=t,s.chatReplayDedupKeys.clear();try{const e=this.getGateway(),a=await loadHistory(e,s.gatewaySessionKey,50),i=groupIntoTurns(a),n=i.filter(e=>e.toolCalls?.length).length,o=i.reduce((e,t)=>e+(t.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${t} messages=${a.length} turns=${i.length} turnsWithToolCalls=${n} totalToolCalls=${o}`);for(const e of i){const a=e.toolCalls?.length?`:tc=${e.toolCalls.map(e=>e.id).join(",")}`:"",i=`${e.role}:${e.content.slice(0,64)}${a}`;s.chatReplayDedupKeys.has(i)||(s.chatReplayDedupKeys.add(i),this.transport.sendSessionUpdate(t,{sessionUpdate:"history_turn",role:e.role,content:e.content,reasoning:e.reasoning,toolCalls:e.toolCalls}))}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}this.transport.sendResult(e.id,{sessionId:t});void 0!==s.pendingPromptId&&!s.promptDone&&s.assistantMq&&!s.assistantMq.closed&&this.replayCurrentAssistantStream(s).catch(()=>{}),this.pushSkillsList().catch(()=>{})}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const t=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,s=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let a=0;for(;Date.now()<s&&(e.assistantStreamOwnerRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const s=await Promise.race([e.assistantMq.read(a),new Promise(e=>setTimeout(()=>e(null),t))]);if(!s||0===s.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const t of s)this.transport.sendSessionUpdate(e.sessionId,t),a++}a>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:a}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)t.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,a=s.sessionKey??s.key??s.id??s.name;if(!a||"string"!=typeof a)continue;if(isBackgroundSessionKey(a))continue;t.find(e=>e.sessionId===a||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===a)||t.push({sessionId:a,title:s.title||a,cwd:s.cwd,updatedAt:s.updatedAt})}}}catch{}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendError(e.id,-32602,"sessionId and title required");const a=this.state.sessions.get(t);if(!a)return void this.transport.sendError(e.id,-32602,"unknown sessionId");a.title=s.slice(0,100);const i=this.getGateway();i?.isConnected&&i.sessionsPatch(a.gatewaySessionKey,{title:a.title}).catch(()=>{}),this.transport.sendResult(e.id,{sessionId:t,title:a.title})}handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendError(e.id,-32602,"sessionId required");const s=this.state.sessions.get(t);s?(void 0!==s.pendingPromptId&&this.failPrompt(s,-32001,"Session deleted"),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId),this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(s.gatewaySessionKey),this.transport.sendResult(e.id,{deleted:!0})):this.transport.sendError(e.id,-32602,"unknown sessionId")}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,a=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(a&&i?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(a.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,a=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(a&&i?.isConnected&&s)try{await i.sessionsPatch(a.gatewaySessionKey,{model:s})}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:s??"default"})}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();return t?.skills?t.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0})):[]}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(){const e=await this.fetchSimplifiedSkills();this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:e}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:e.length}})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendError(e.id,-32602,"sessionId and prompt required");let a=this.state.sessions.get(t);if(!a){const e=t.startsWith("agent:")?t:`agent:main:${t}`;a={sessionId:t,gatewaySessionKey:e,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(t,a),this.state.sessionsByGatewayKey.set(e,a)}this.state.lastActiveSessionId=t,this.onPromptStart?.();const i=this.getGateway();if(!i?.isConnected)return void this.transport.sendError(e.id,-32001,"Gateway not connected");let n=extractPromptBlocks(sanitizePromptPayload(s));if(!a.title){const e=n.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(a.title=e.text.slice(0,80).replace(/\n/g," ").trim(),i?.isConnected&&a.title&&i.sessionsPatch(a.gatewaySessionKey,{title:a.title}).catch(()=>{}))}const o=buildResolutionPlan(n);if(o.refs.length>0)try{const e=await resolveResources(o,this.writeObs);n=applyResolutions(n,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const r=toGatewayPrompt(n);if("error"in r)return void this.transport.sendError(e.id,-32602,r.error);const{message:d,attachments:l}=r,c=resolveStandaloneSlashCommandName(n,d)??resolveSlashCmd(n,d),m="string"==typeof c;if(c&&this.handleSlashCommand){const s=this;if(await(async()=>{try{return await s.handleSlashCommand(c)}catch(e){return s.logger.warn?.(`[bridge-core] /${c} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(a.sessionId,n),this.transport.sendSessionUpdate(t,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${c}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const p=m?"chat.send":"agent";let h=m?d:withUserMessagePrefix(d);if(!this.config.canvasEnabled||m||a.canvasPromptInjected||!1===a.canvasActive||(h=`${CANVAS_TERM_MAPPING}\n\n${h}`,a.canvasPromptInjected=!0,this.logger.debug?.("[bridge-core] Canvas term mapping injected into first message")),this.config.canvasEnabled&&!m&&!1!==a.canvasActive){const e=this.getCanvasState?.();if(e){const t=buildActiveSurfacesContext(e);t&&(h=`${t}\n\n${h}`)}const t=a.referencedSurfaceId;t&&(h=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${h}`,a.referencedSurfaceId=void 0)}m&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${t} command=/${c}`),a.currentRunId&&this.state.promptsByRunId.delete(a.currentRunId),a.currentRunId=void 0,a.assistantStreamOwnerRunId=void 0,a.pendingPromptId=e.id,a.promptDone=!1,a.gatewayMethod=p,appendHistoryEntry(a.gatewaySessionKey,{ts:Date.now(),role:"user",content:d}),a.assistantMq?.close(),a.assistantMq=new MessageQueue,a.chatReplayDedupKeys.clear(),a.hasAgentAssistantStream=!1,a.hasAgentThinkingStream=!1,a.hasAgentToolStartStream=!1,a.hasAgentToolResultStream=!1,a.chatAssistantTextSoFar=void 0,a.agentAssistantTextSoFar=void 0,a.lastAgentSeq=void 0,a.lastChatSeq=void 0,this.transport.resetSessionUpdateCount(t),this.emitPromptAsUserSessionUpdates(a.sessionId,n),this.transport.sendSessionUpdate(t,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(a);try{const e=l.length>0?l.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let t;t=m?await i.chatSend(a.gatewaySessionKey,h,e):await i.agentSend(a.gatewaySessionKey,h,this.config.gateway.agentId||"main",e),a.currentRunId=t.runId,a.assistantStreamOwnerRunId=t.runId,a.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(t.runId,a)}catch(t){this.state.clearPromptTimeout(a),void 0!==a.pendingPromptId&&(a.pendingPromptId=void 0,a.assistantMq?.close(),this.transport.sendError(e.id,-32001,t instanceof Error?t.message:"failed to send prompt to gateway"))}}emitPromptAsUserSessionUpdates(e,t){for(const s of t){const t=this.toUserSessionUpdateContent(s);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,a=t.mimeType??t.mime_type??e.mimeType??e.mime_type,i=t.text??e.text,n=t.data??e.data,o=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!i&&!n)return;return{type:"resource",resource:{...s?{uri:s}:{},...a?{mimeType:a}:{},...i?{text:i}:{},...n?{data:n}:{},...o?{fileName:o}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendError(e,-32602,"sessionId is required"));const a=this.state.sessions.get(s);if(!a)return void(void 0!==e&&this.transport.sendError(e,-32602,"unknown sessionId"));const i=this.getGateway();i?.isConnected&&a.currentRunId&&i.chatAbort(a.gatewaySessionKey,a.currentRunId).catch(()=>{}),this.completePrompt(a,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,-32022,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.gatewayRequestId=void 0,e.assistantMq?.close()}completePrompt(e,t){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendResult(e.pendingPromptId,{stopReason:t}),this.cleanupPrompt(e))}failPrompt(e,t,s){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendError(e.pendingPromptId,t,s),this.cleanupPrompt(e))}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,-32001,"Gateway disconnected")}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,-32021,s)}}pushToolResultContent(e,t,s,a){if(0===s.length)return;const i=(a?this.state.promptsByRunId.get(a):void 0)??(this.state.lastActiveSessionId?this.state.sessions.get(this.state.lastActiveSessionId):void 0);if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session (runId=${a})`);const n=i.sessionId;for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(n,t),i.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:n}})}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendError(e.id,-32001,"Gateway not connected");const s=e.method.replace("/",".");try{const a=await t.request(s,e.params??{});this.transport.sendResult(e.id,a??{})}catch(t){const a=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${a}`),this.transport.sendError(e.id,-32001,a)}}isCronCorrelatedRun(e){return this.state.cronRunIds.has(e)}}
|
|
1
|
+
import{randomUUID}from"node:crypto";import{sanitizePromptPayload,stripTransportMetadata}from"../message-filter.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas-tools.js";import{MessageQueue}from"../utils/mq.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,groupIntoTurns}from"./history-replay.js";import{appendHistoryEntry}from"./local-session-history.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{Errors}from"../errors/index.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS}from"./types.js";const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const a=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return a&&!a.includes("/")&&SLASH_COMMAND_NAME_RE.test(a)?a.toLowerCase():void 0}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;initialized=!1;handleTerminalRequest=null;handleTerminalNotification=null;handleSlashCommand=null;onPromptStart=null;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});try{switch(e.method){case"initialize":this.handleInitialize(e);break;case"session/new":await this.handleSessionNew(e);break;case"session/load":await this.handleSessionLoad(e);break;case"session/list":await this.handleSessionList(e);break;case"session/rename":this.handleSessionRename(e);break;case"session/delete":this.handleSessionDelete(e);break;case"session/prompt":await this.handleSessionPrompt(e);break;case"session/set_mode":await this.handleSessionSetMode(e);break;case"session/set_model":await this.handleSessionSetModel(e);break;case"skills/list":await this.handleSkillsList(e);break;case"session/cancel":this.handleSessionCancel(e.id,e.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(e);break;default:e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/action":case"canvas/toggle":case"canvas/clear":case"canvas/delete":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const t=e.params,s=t?.surfaceId,a=this.state.resolveSessionFromParams(t);if(!a){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}a.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t=`session:${randomUUID()}`,s=`agent:main:${randomUUID()}`,a="string"==typeof e.params?.cwd?e.params.cwd:".",i={sessionId:t,gatewaySessionKey:s,createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:a};this.state.sessions.set(t,i),this.state.sessionsByGatewayKey.set(s,i),this.transport.sendResult(e.id,{sessionId:t,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});let s=this.state.sessions.get(t);if(!s){const e=t.startsWith("agent:"),a=e?`session:${randomUUID()}`:t,i=e?t:`agent:main:${t}`;s={sessionId:a,gatewaySessionKey:i,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(a,s),this.state.sessionsByGatewayKey.set(i,s)}const a=s.sessionId;this.state.lastActiveSessionId=a,s.chatReplayDedupKeys.clear();try{const e=this.getGateway(),t=await loadHistory(e,s.gatewaySessionKey,50),i=groupIntoTurns(t),o=i.filter(e=>e.toolCalls?.length).length,n=i.reduce((e,t)=>e+(t.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${a} messages=${t.length} turns=${i.length} turnsWithToolCalls=${o} totalToolCalls=${n}`);for(const e of i){const t=e.toolCalls?.length?`:tc=${e.toolCalls.map(e=>e.id).join(",")}`:"",i=`${e.role}:${e.content.slice(0,64)}${t}`;s.chatReplayDedupKeys.has(i)||(s.chatReplayDedupKeys.add(i),this.transport.sendSessionUpdate(a,{sessionUpdate:"history_turn",role:e.role,content:e.content,reasoning:e.reasoning,toolCalls:e.toolCalls}))}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}this.transport.sendResult(e.id,{sessionId:a});void 0!==s.pendingPromptId&&!s.promptDone&&s.assistantMq&&!s.assistantMq.closed&&this.replayCurrentAssistantStream(s).catch(()=>{}),this.pushSkillsList().catch(()=>{})}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const t=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,s=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let a=0;for(;Date.now()<s&&(e.assistantStreamOwnerRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const s=await Promise.race([e.assistantMq.read(a),new Promise(e=>setTimeout(()=>e(null),t))]);if(!s||0===s.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const t of s)this.transport.sendSessionUpdate(e.sessionId,t),a++}a>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:a}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)t.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,a=s.sessionKey??s.key??s.id??s.name;if(!a||"string"!=typeof a)continue;if(isBackgroundSessionKey(a))continue;const i=t.find(e=>e.sessionId===a||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===a),o=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0;if(i){if(!i.title||"New chat"===i.title){const e=o||i.sessionId;i.title=e;const t=this.state.sessions.get(i.sessionId);t&&!t.title&&(t.title=e)}const e=s.updatedAt;e&&(i.updatedAt=e)}else{const e=`session:${randomUUID()}`,i={sessionId:e,gatewaySessionKey:a,createdAt:Date.now(),chatReplayDedupKeys:new Set,title:o||"New chat",cwd:s.cwd};this.state.sessions.set(e,i),this.state.sessionsByGatewayKey.set(a,i),t.push({sessionId:e,title:o||"New chat",cwd:s.cwd,updatedAt:s.updatedAt})}}}}catch{}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const a=s.slice(0,64),i=this.state.sessions.get(t);i&&(i.title=a);const o=i?.gatewaySessionKey??`agent:main:${t}`,n=this.getGateway();n?.isConnected&&n.sessionsPatch(o,{label:a}).catch(()=>{}),this.transport.sendResult(e.id,{sessionId:t,title:a})}handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);s?(void 0!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId),this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(s.gatewaySessionKey),this.transport.sendResult(e.id,{deleted:!0})):this.transport.sendAcpError(e.id,Errors.SessionNotFound)}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,a=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(a&&i?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(a.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,a=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(a&&i?.isConnected&&s)try{await i.sessionsPatch(a.gatewaySessionKey,{model:s})}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:s??"default"})}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();return t?.skills?t.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0})):[]}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(){const e=await this.fetchSimplifiedSkills();this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:e}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:e.length}})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});let a=this.state.sessions.get(t);if(!a){const e=t.startsWith("agent:"),s=e?`session:${randomUUID()}`:t,i=e?t:`agent:main:${t}`;a={sessionId:s,gatewaySessionKey:i,createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(s,a),this.state.sessionsByGatewayKey.set(i,a)}const i=a.sessionId;this.state.lastActiveSessionId=i,this.onPromptStart?.();const o=this.getGateway();if(!o?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let n=extractPromptBlocks(sanitizePromptPayload(s));if(!a.title){const e=n.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(a.title=e.text.slice(0,64).replace(/\n/g," ").trim(),o?.isConnected&&a.title&&o.sessionsPatch(a.gatewaySessionKey,{label:a.title}).catch(()=>{}))}const r=buildResolutionPlan(n);if(r.refs.length>0)try{const e=await resolveResources(r,this.writeObs);n=applyResolutions(n,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const d=toGatewayPrompt(n);if("error"in d)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:d.error});const{message:l,attachments:c}=d,m=resolveStandaloneSlashCommandName(n,l)??resolveSlashCmd(n,l),p="string"==typeof m;if(m&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(m)}catch(e){return t.logger.warn?.(`[bridge-core] /${m} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(a.sessionId,n),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${m}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const h=p?"chat.send":"agent";let u=p?l:withUserMessagePrefix(l);if(!this.config.canvasEnabled||p||a.canvasPromptInjected||!1===a.canvasActive||(u=`${CANVAS_TERM_MAPPING}\n\n${u}`,a.canvasPromptInjected=!0,this.logger.debug?.("[bridge-core] Canvas term mapping injected into first message")),this.config.canvasEnabled&&!p&&!1!==a.canvasActive){const e=this.getCanvasState?.();if(e){const t=buildActiveSurfacesContext(e);t&&(u=`${t}\n\n${u}`)}const t=a.referencedSurfaceId;t&&(u=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${u}`,a.referencedSurfaceId=void 0)}p&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${i} command=/${m}`),a.currentRunId&&this.state.promptsByRunId.delete(a.currentRunId),a.currentRunId=void 0,a.assistantStreamOwnerRunId=void 0,a.pendingPromptId=e.id,a.promptDone=!1,a.gatewayMethod=h,appendHistoryEntry(a.gatewaySessionKey,{ts:Date.now(),role:"user",content:l}),a.assistantMq?.close(),a.assistantMq=new MessageQueue,a.chatReplayDedupKeys.clear(),a.hasAgentAssistantStream=!1,a.hasAgentThinkingStream=!1,a.hasAgentToolStartStream=!1,a.hasAgentToolResultStream=!1,a.chatAssistantTextSoFar=void 0,a.agentAssistantTextSoFar=void 0,a.lastAgentSeq=void 0,a.lastChatSeq=void 0,this.transport.resetSessionUpdateCount(i),this.emitPromptAsUserSessionUpdates(i,n),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(a);try{const e=c.length>0?c.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let t;t=p?await o.chatSend(a.gatewaySessionKey,u,e):await o.agentSend(a.gatewaySessionKey,u,this.config.gateway.agentId||"main",e),a.currentRunId=t.runId,a.assistantStreamOwnerRunId=t.runId,a.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(t.runId,a)}catch(t){this.state.clearPromptTimeout(a),void 0!==a.pendingPromptId&&(a.pendingPromptId=void 0,a.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:t instanceof Error?t.message:"failed to send prompt to gateway"}))}}emitPromptAsUserSessionUpdates(e,t){for(const s of t){const t=this.toUserSessionUpdateContent(s);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,a=t.mimeType??t.mime_type??e.mimeType??e.mime_type,i=t.text??e.text,o=t.data??e.data,n=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!i&&!o)return;return{type:"resource",resource:{...s?{uri:s}:{},...a?{mimeType:a}:{},...i?{text:i}:{},...o?{data:o}:{},...n?{fileName:n}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const a=this.state.sessions.get(s);if(!a)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway();i?.isConnected&&a.currentRunId&&i.chatAbort(a.gatewaySessionKey,a.currentRunId).catch(()=>{}),this.completePrompt(a,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.gatewayRequestId=void 0,e.assistantMq?.close()}completePrompt(e,t){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendResult(e.pendingPromptId,{stopReason:t}),this.cleanupPrompt(e))}failPrompt(e,t,s){e.promptDone||void 0===e.pendingPromptId||(e.promptDone=!0,this.transport.sendError(e.pendingPromptId,t,s),this.cleanupPrompt(e))}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,s)}}pushToolResultContent(e,t,s,a){if(0===s.length)return;const i=(a?this.state.promptsByRunId.get(a):void 0)??(this.state.lastActiveSessionId?this.state.sessions.get(this.state.lastActiveSessionId):void 0);if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session (runId=${a})`);const o=i.sessionId;for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,t),i.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:o}})}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/",".");try{const a=await t.request(s,e.params??{});this.transport.sendResult(e.id,a??{})}catch(t){const a=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${a}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:a})}}isCronCorrelatedRun(e){return this.state.cronRunIds.has(e)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{stripTransportMetadata}from"../message-filter.js";import{appendHistoryEntry}from"./local-session-history.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS,DEFAULT_CRON_FLUSH_TIMEOUT_MS,DEFAULT_CRON_SIGNAL_WINDOW_MS}from"./types.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"!=typeof t)return;return t.replace(/\r\n/g,"\n").replace(/\r/g,"\n")||void 0}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"end_turn"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendToolCallSpacer(t){const e={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"}};this.transport.sendSessionUpdate(t.sessionId,e),t.assistantMq?.push(e)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return void(this.state.isCronCandidate(t.runId,e)&&this.handleUncorrelatedCronChatEvent(t))}if("number"==typeof t.seq){if(t.seq<=(e.lastChatSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate chat event seq=${t.seq} (last=${e.lastChatSeq}) runId=${t.runId}`);e.lastChatSeq=t.seq}const s=e.sessionId,n=t.message;if(n){this.touchStreamActivity(e);if("assistant"===n.role)for(const o of n.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(a){if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const o=`chat:thinking:${t}`;if(e.chatReplayDedupKeys.has(o))continue;e.chatReplayDedupKeys.add(o);const a={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t}};this.transport.sendSessionUpdate(s,a)&&e.assistantMq?.push(a);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=isRecord(n.arguments)?n.arguments:isRecord(n.args)?n.args:{},r=`chat:tool_start:${t}:${JSON.stringify(a)}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call",toolCallId:t,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(n.result)||Array.isArray(n.result)?JSON.stringify(n.result,null,2):void 0);if(!a)continue;const r=`chat:tool_result:${t}:${a}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call_update",toolCallId:t,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const o=stripTransportMetadata(n.text);if(!o)continue;const a=`chat:text:${o.slice(0,128)}`;if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const r=e.chatAssistantTextSoFar??"";let i;if(e.chatAssistantTextSoFar=o,o.startsWith(r))i=o.slice(r.length);else{if(r.startsWith(o))continue;i=o}if(!i)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${i.length} prevLen=${r.length}`);const l={sessionUpdate:"agent_message_chunk",content:{type:"text",text:i}};this.transport.sendSessionUpdate(s,l),e.assistantMq?.push(l)}}}}else"delta"===t.state&&this.touchStreamActivity(e);this.maybeCompletePromptFromChatState(e,t),"final"===t.state?this.completePrompt(e,t.stopReason||"end_turn"):"aborted"===t.state||"cancelled"===t.state||"cancel"===t.state?this.completePrompt(e,"cancelled"):"error"===t.state&&this.failPrompt(e,-32021,t.errorMessage||"Agent error")}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0)??this.state.findCronTargetSession();if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return this.state.isCronCandidate(t.runId,e)?void this.handleUncorrelatedCronAgentEvent(t):void 0}if("number"==typeof t.seq){if(t.seq<=(e.lastAgentSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate agent event seq=${t.seq} (last=${e.lastAgentSeq}) runId=${t.runId} stream=${t.stream}`);e.lastAgentSeq=t.seq}const s=e.sessionId;this.touchStreamActivity(e);const n=t=>{this.transport.sendSessionUpdate(s,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});o&&(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${o.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}}));const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const s=this.resolveToolCallId(e,t,"start"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=isRecord(t.arguments)?t.arguments:isRecord(t.args)?t.args:{};n({sessionUpdate:"tool_call",toolCallId:s,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:JSON.stringify(a,null,2),entryType:"tool_call",toolCallId:s,toolName:o});continue}if("toolResult"===o){this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0;const s=this.resolveToolCallId(e,t,"result"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=normalizeText(t.text)??normalizeText(t.result)??(isRecord(t.result)||Array.isArray(t.result)?JSON.stringify(t.result,null,2):void 0);a&&n({sessionUpdate:"tool_call_update",toolCallId:s,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]});continue}}appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:o??""})}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=t.data?.delta||t.data?.text;if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:n,entryType:"thought"})}}else if("tool"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("start"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,s,"start"),o=asString(s?.name)??asString(s?.toolName)??asString(s?.tool_name)??"tool",a=isRecord(s?.arguments)?s.arguments:isRecord(s?.args)?s.args:{};n({sessionUpdate:"tool_call",toolCallId:t,title:o,kind:"other",status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:JSON.stringify(a,null,2),entryType:"tool_call",toolCallId:t,toolName:o})}else if("result"===o)this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=s?.toolCall;if(t){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const o=asString(t?.name)??asString(s?.name)??asString(s?.toolName)??"tool",a=this.resolveToolCallId(e,{...s,...t},"start");n({sessionUpdate:"tool_call",toolCallId:a,title:o,kind:"other",status:"in_progress"}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:"",entryType:"tool_call",toolCallId:a,toolName:o})}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n){this.flushCronBufferForRun(t.runId);const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"end_turn")}else if("cancelled"===n||"cancel"===n){const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"cancelled")}else if("error"===n){const n=s?.error,o=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n&&"object"==typeof n?JSON.stringify(n):void 0)??"gateway lifecycle error";this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${o} data=${JSON.stringify(s)}`),this.failPrompt(e,-32021,o)}}}async handleToolResult(t,e){const s=e.data,n=s?.toolCall,o=("string"==typeof n?.id?n.id:void 0)??("string"==typeof s?.toolCallId?s.toolCallId:void 0),a=(o?this.state.toolCallIdMap.get(o):void 0)??o??`tc_${e.runId}_${e.seq}`,r=("string"==typeof n?.name?n.name:void 0)??("string"==typeof s?.name?s.name:void 0)??"tool",i="string"==typeof s?.text?s.text:void 0,l=o??a;let c;try{c=await resolveToolResultBlocks({toolCallId:l,toolName:r,rawText:i,rawResult:e.data})}catch{c=i?[{type:"content",content:{type:"text",text:i}}]:[]}if(0===c.length&&i&&(c=[{type:"content",content:{type:"text",text:i}}]),c.length>0){const e=c.filter(t=>"text"===t.content?.type&&t.content?.text).map(t=>({type:"content",content:{type:"text",text:t.content.text}})),s={sessionUpdate:"tool_call_update",toolCallId:a,title:r,kind:"other",status:"completed",...e.length>0?{content:e}:{}};this.transport.sendSessionUpdate(t.sessionId,s)&&t.assistantMq?.push(s),appendHistoryEntry(t.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:i??"",entryType:"tool_result",toolCallId:a,toolName:r})}}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t,s="string"==typeof e?.runId?e.runId:void 0;if(this.state.lastCronSignalAtMs=Date.now(),s){this.state.cronRunIds.add(s);const t=this.state.cronRunIdTimers.get(s);t&&clearTimeout(t);const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS,n=setTimeout(()=>{this.state.cronRunIds.delete(s),this.state.cronRunIdTimers.delete(s)},e);"object"==typeof n&&"unref"in n&&n.unref(),this.state.cronRunIdTimers.set(s,n)}this.pushCronWebhook(e,s);const n=this.state.findCronTargetSession();if(!n)return;const o=("string"==typeof e?.summary?e.summary:"")||("string"==typeof e?.text?e.text:"")||JSON.stringify(t),a=s?`run:${s}`:`signal:${Date.now()}`;this.bufferCronText(a,`[Cron] ${o}\n`,n.sessionId)}pushCronWebhook(t,e){if(!this.config.platformUrl||!this.agentFetchFn)return;const s=`${this.config.platformUrl}/api/v1/agent/webhooks/cron`,n=("string"==typeof t?.summary?t.summary:void 0)??("string"==typeof t?.text?t.text:void 0)??"",o={jobId:e??"",name:("string"==typeof t?.name?t.name:void 0)??("string"==typeof t?.jobName?t.jobName:void 0)??"",content:n,summary:n,type:"cron",action:"string"==typeof t?.action?t.action:"fired",status:"string"==typeof t?.status?t.status:""};this.agentFetchFn(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).catch(t=>{this.logger.warn?.(`[gateway-events] cron webhook push failed: ${t instanceof Error?t.message:t}`)})}bufferCronText(t,e,s){const n=this.state.cronTextByKey.get(t)??"";this.state.cronTextByKey.set(t,n+e),this.scheduleCronFlush(t,s)}scheduleCronFlush(t,e){const s=this.state.cronFlushTimersByKey.get(t);s&&clearTimeout(s);const n=this.config.cronFlushTimeoutMs??DEFAULT_CRON_FLUSH_TIMEOUT_MS,o=setTimeout(()=>{this.state.cronFlushTimersByKey.delete(t),this.flushCronBufferByKey(t,e,"timeout")},n);"object"==typeof o&&"unref"in o&&o.unref(),this.state.cronFlushTimersByKey.set(t,o)}flushCronBufferByKey(t,e,s){const n=this.state.cronFlushTimersByKey.get(t);n&&(clearTimeout(n),this.state.cronFlushTimersByKey.delete(t));const o=(this.state.cronTextByKey.get(t)??"").trim();if(this.state.cronTextByKey.delete(t),!o)return void("timeout"===s&&this.clearCronStreamTracking(t));const a=this.state.getCronRequestId(t),r={sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}};this.transport.sendCronSessionUpdate(e,r,{requestId:a,messageType:"cron",timestamp:Date.now()});const i=this.state.sessions.get(e);i?.assistantMq?.push(r),"timeout"===s&&this.clearCronStreamTracking(t)}flushCronBufferForRun(t){if(!t)return;const e=`run:${t}`;if(!this.state.cronTextByKey.has(e))return;const s=this.state.findCronTargetSession();s&&this.flushCronBufferByKey(e,s.sessionId,"end")}clearCronStreamTracking(t){this.state.cronRequestIdsByKey.delete(t),this.state.cronTextByKey.delete(t);const e=this.state.cronFlushTimersByKey.get(t);if(e&&(clearTimeout(e),this.state.cronFlushTimersByKey.delete(t)),t.startsWith("run:")){const e=t.slice(4);if(e){this.state.cronRunIds.delete(e);const t=this.state.cronRunIdTimers.get(e);t&&(clearTimeout(t),this.state.cronRunIdTimers.delete(e))}}}handleUncorrelatedCronAgentEvent(t){if("lifecycle"===t.stream){const e=t.data,s="string"==typeof e?.phase?e.phase:void 0;if("end"===s){this.flushCronBufferForRun(t.runId);const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}else if("cancelled"===s||"cancel"===s||"error"===s){const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}return}if("assistant"!==t.stream)return;const e=this.state.findCronTargetSession();if(!e)return;const s=("string"==typeof t.data?.delta?t.data.delta:void 0)??("string"==typeof t.data?.text?t.data.text:void 0);if(!s)return;const n=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`;this.bufferCronText(n,s,e.sessionId)}handleUncorrelatedCronChatEvent(t){if("user"!==t.message?.role)return;const e=this.state.findCronTargetSession();if(e&&t.message?.content)for(const s of t.message.content)if("text"===s.type&&s.text){const n=stripTransportMetadata(s.text);if(!n)continue;const o=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`,a=this.state.getCronRequestId(o),r={sessionUpdate:"user_message_chunk",content:{type:"text",text:n}};this.transport.sendCronSessionUpdate(e.sessionId,r,{requestId:a,messageType:"cron",timestamp:Date.now()})}}}
|
|
1
|
+
import{Errors}from"../errors/index.js";import{stripTransportMetadata}from"../message-filter.js";import{appendHistoryEntry}from"./local-session-history.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS,DEFAULT_CRON_FLUSH_TIMEOUT_MS,DEFAULT_CRON_SIGNAL_WINDOW_MS}from"./types.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"!=typeof t)return;return t.replace(/\r\n/g,"\n").replace(/\r/g,"\n")||void 0}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"end_turn"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendToolCallSpacer(t){const e={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"}};this.transport.sendSessionUpdate(t.sessionId,e),t.assistantMq?.push(e)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return void(this.state.isCronCandidate(t.runId,e)&&this.handleUncorrelatedCronChatEvent(t))}if("number"==typeof t.seq){if(t.seq<=(e.lastChatSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate chat event seq=${t.seq} (last=${e.lastChatSeq}) runId=${t.runId}`);e.lastChatSeq=t.seq}const s=e.sessionId,n=t.message;if(n){this.touchStreamActivity(e);if("assistant"===n.role)for(const o of n.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(a){if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const o=`chat:thinking:${t}`;if(e.chatReplayDedupKeys.has(o))continue;e.chatReplayDedupKeys.add(o);const a={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t}};this.transport.sendSessionUpdate(s,a)&&e.assistantMq?.push(a);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=isRecord(n.arguments)?n.arguments:isRecord(n.args)?n.args:{},r=`chat:tool_start:${t}:${JSON.stringify(a)}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call",toolCallId:t,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(n.result)||Array.isArray(n.result)?JSON.stringify(n.result,null,2):void 0);if(!a)continue;const r=`chat:tool_result:${t}:${a}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call_update",toolCallId:t,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const o=stripTransportMetadata(n.text);if(!o)continue;const a=`chat:text:${o.slice(0,128)}`;if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const r=e.chatAssistantTextSoFar??"";let i;if(e.chatAssistantTextSoFar=o,o.startsWith(r))i=o.slice(r.length);else{if(r.startsWith(o))continue;i=o}if(!i)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${i.length} prevLen=${r.length}`);const l={sessionUpdate:"agent_message_chunk",content:{type:"text",text:i}};this.transport.sendSessionUpdate(s,l),e.assistantMq?.push(l)}}}}else"delta"===t.state&&this.touchStreamActivity(e);this.maybeCompletePromptFromChatState(e,t),"final"===t.state?this.completePrompt(e,t.stopReason||"end_turn"):"aborted"===t.state||"cancelled"===t.state||"cancel"===t.state?this.completePrompt(e,"cancelled"):"error"===t.state&&this.failPrompt(e,Errors.AgentError.rpcCode,t.errorMessage||Errors.AgentError.message)}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0)??this.state.findCronTargetSession();if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return this.state.isCronCandidate(t.runId,e)?void this.handleUncorrelatedCronAgentEvent(t):void 0}if("number"==typeof t.seq){if(t.seq<=(e.lastAgentSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate agent event seq=${t.seq} (last=${e.lastAgentSeq}) runId=${t.runId} stream=${t.stream}`);e.lastAgentSeq=t.seq}const s=e.sessionId;this.touchStreamActivity(e);const n=t=>{this.transport.sendSessionUpdate(s,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});o&&(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${o.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}}));const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const s=this.resolveToolCallId(e,t,"start"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=isRecord(t.arguments)?t.arguments:isRecord(t.args)?t.args:{};n({sessionUpdate:"tool_call",toolCallId:s,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:JSON.stringify(a,null,2),entryType:"tool_call",toolCallId:s,toolName:o});continue}if("toolResult"===o){this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0;const s=this.resolveToolCallId(e,t,"result"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=normalizeText(t.text)??normalizeText(t.result)??(isRecord(t.result)||Array.isArray(t.result)?JSON.stringify(t.result,null,2):void 0);a&&n({sessionUpdate:"tool_call_update",toolCallId:s,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]});continue}}appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:o??""})}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=t.data?.delta||t.data?.text;if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:n,entryType:"thought"})}}else if("tool"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("start"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,s,"start"),o=asString(s?.name)??asString(s?.toolName)??asString(s?.tool_name)??"tool",a=isRecord(s?.arguments)?s.arguments:isRecord(s?.args)?s.args:{};n({sessionUpdate:"tool_call",toolCallId:t,title:o,kind:"other",status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:JSON.stringify(a,null,2),entryType:"tool_call",toolCallId:t,toolName:o})}else if("result"===o)this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=s?.toolCall;if(t){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const o=asString(t?.name)??asString(s?.name)??asString(s?.toolName)??"tool",a=this.resolveToolCallId(e,{...s,...t},"start");n({sessionUpdate:"tool_call",toolCallId:a,title:o,kind:"other",status:"in_progress"}),appendHistoryEntry(e.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:"",entryType:"tool_call",toolCallId:a,toolName:o})}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n){this.flushCronBufferForRun(t.runId);const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"end_turn")}else if("cancelled"===n||"cancel"===n){const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"cancelled")}else if("error"===n){const n=s?.error,o=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n&&"object"==typeof n?JSON.stringify(n):void 0)??"gateway lifecycle error";this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${o} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,o)}}}async handleToolResult(t,e){const s=e.data,n=s?.toolCall,o=("string"==typeof n?.id?n.id:void 0)??("string"==typeof s?.toolCallId?s.toolCallId:void 0),a=(o?this.state.toolCallIdMap.get(o):void 0)??o??`tc_${e.runId}_${e.seq}`,r=("string"==typeof n?.name?n.name:void 0)??("string"==typeof s?.name?s.name:void 0)??"tool",i="string"==typeof s?.text?s.text:void 0,l=o??a;let c;try{c=await resolveToolResultBlocks({toolCallId:l,toolName:r,rawText:i,rawResult:e.data})}catch{c=i?[{type:"content",content:{type:"text",text:i}}]:[]}if(0===c.length&&i&&(c=[{type:"content",content:{type:"text",text:i}}]),c.length>0){const e=c.filter(t=>"text"===t.content?.type&&t.content?.text).map(t=>({type:"content",content:{type:"text",text:t.content.text}})),s={sessionUpdate:"tool_call_update",toolCallId:a,title:r,kind:"other",status:"completed",...e.length>0?{content:e}:{}};this.transport.sendSessionUpdate(t.sessionId,s)&&t.assistantMq?.push(s),appendHistoryEntry(t.gatewaySessionKey,{ts:Date.now(),role:"assistant",content:i??"",entryType:"tool_result",toolCallId:a,toolName:r})}}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t,s="string"==typeof e?.runId?e.runId:void 0;if(this.state.lastCronSignalAtMs=Date.now(),s){this.state.cronRunIds.add(s);const t=this.state.cronRunIdTimers.get(s);t&&clearTimeout(t);const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS,n=setTimeout(()=>{this.state.cronRunIds.delete(s),this.state.cronRunIdTimers.delete(s)},e);"object"==typeof n&&"unref"in n&&n.unref(),this.state.cronRunIdTimers.set(s,n)}this.pushCronWebhook(e,s);const n=this.state.findCronTargetSession();if(!n)return;const o=("string"==typeof e?.summary?e.summary:"")||("string"==typeof e?.text?e.text:"")||JSON.stringify(t),a=s?`run:${s}`:`signal:${Date.now()}`;this.bufferCronText(a,`[Cron] ${o}\n`,n.sessionId)}pushCronWebhook(t,e){if(!this.config.platformUrl||!this.agentFetchFn)return;const s=`${this.config.platformUrl}/api/v1/agent/webhooks/cron`,n=("string"==typeof t?.summary?t.summary:void 0)??("string"==typeof t?.text?t.text:void 0)??"",o={jobId:e??"",name:("string"==typeof t?.name?t.name:void 0)??("string"==typeof t?.jobName?t.jobName:void 0)??"",content:n,summary:n,type:"cron",action:"string"==typeof t?.action?t.action:"fired",status:"string"==typeof t?.status?t.status:""};this.agentFetchFn(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).catch(t=>{this.logger.warn?.(`[gateway-events] cron webhook push failed: ${t instanceof Error?t.message:t}`)})}bufferCronText(t,e,s){const n=this.state.cronTextByKey.get(t)??"";this.state.cronTextByKey.set(t,n+e),this.scheduleCronFlush(t,s)}scheduleCronFlush(t,e){const s=this.state.cronFlushTimersByKey.get(t);s&&clearTimeout(s);const n=this.config.cronFlushTimeoutMs??DEFAULT_CRON_FLUSH_TIMEOUT_MS,o=setTimeout(()=>{this.state.cronFlushTimersByKey.delete(t),this.flushCronBufferByKey(t,e,"timeout")},n);"object"==typeof o&&"unref"in o&&o.unref(),this.state.cronFlushTimersByKey.set(t,o)}flushCronBufferByKey(t,e,s){const n=this.state.cronFlushTimersByKey.get(t);n&&(clearTimeout(n),this.state.cronFlushTimersByKey.delete(t));const o=(this.state.cronTextByKey.get(t)??"").trim();if(this.state.cronTextByKey.delete(t),!o)return void("timeout"===s&&this.clearCronStreamTracking(t));const a=this.state.getCronRequestId(t),r={sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}};this.transport.sendCronSessionUpdate(e,r,{requestId:a,messageType:"cron",timestamp:Date.now()});const i=this.state.sessions.get(e);i?.assistantMq?.push(r),"timeout"===s&&this.clearCronStreamTracking(t)}flushCronBufferForRun(t){if(!t)return;const e=`run:${t}`;if(!this.state.cronTextByKey.has(e))return;const s=this.state.findCronTargetSession();s&&this.flushCronBufferByKey(e,s.sessionId,"end")}clearCronStreamTracking(t){this.state.cronRequestIdsByKey.delete(t),this.state.cronTextByKey.delete(t);const e=this.state.cronFlushTimersByKey.get(t);if(e&&(clearTimeout(e),this.state.cronFlushTimersByKey.delete(t)),t.startsWith("run:")){const e=t.slice(4);if(e){this.state.cronRunIds.delete(e);const t=this.state.cronRunIdTimers.get(e);t&&(clearTimeout(t),this.state.cronRunIdTimers.delete(e))}}}handleUncorrelatedCronAgentEvent(t){if("lifecycle"===t.stream){const e=t.data,s="string"==typeof e?.phase?e.phase:void 0;if("end"===s){this.flushCronBufferForRun(t.runId);const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}else if("cancelled"===s||"cancel"===s||"error"===s){const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}return}if("assistant"!==t.stream)return;const e=this.state.findCronTargetSession();if(!e)return;const s=("string"==typeof t.data?.delta?t.data.delta:void 0)??("string"==typeof t.data?.text?t.data.text:void 0);if(!s)return;const n=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`;this.bufferCronText(n,s,e.sessionId)}handleUncorrelatedCronChatEvent(t){if("user"!==t.message?.role)return;const e=this.state.findCronTargetSession();if(e&&t.message?.content)for(const s of t.message.content)if("text"===s.type&&s.text){const n=stripTransportMetadata(s.text);if(!n)continue;const o=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`,a=this.state.getCronRequestId(o),r={sessionUpdate:"user_message_chunk",content:{type:"text",text:n}};this.transport.sendCronSessionUpdate(e.sessionId,r,{requestId:a,messageType:"cron",timestamp:Date.now()})}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export class AcpTransport{debugIndex=0;sendBridgeMessage;obs;forward;sessionUpdateCounts=new Map;constructor(e,t,s){this.sendBridgeMessage=e,this.obs=t??(()=>{}),this.forward=s??{forwardThinking:!0,forwardToolCalls:!0}}get forwardControl(){return this.forward}nextMeta(){return{_debug_index:this.debugIndex++,_ts:Date.now()}}sendResult(e,t){const s=this.nextMeta();this.detectProtocolError(e,t,s._debug_index);const
|
|
1
|
+
import{acpError}from"../errors/index.js";export class AcpTransport{debugIndex=0;sendBridgeMessage;obs;forward;sessionUpdateCounts=new Map;constructor(e,t,s){this.sendBridgeMessage=e,this.obs=t??(()=>{}),this.forward=s??{forwardThinking:!0,forwardToolCalls:!0}}get forwardControl(){return this.forward}nextMeta(){return{_debug_index:this.debugIndex++,_ts:Date.now()}}sendResult(e,t){const s=this.nextMeta();this.detectProtocolError(e,t,s._debug_index);const r={jsonrpc:"2.0",id:e,result:this.attachMeta(t,s),_debug_index:s._debug_index,_ts:s._ts};this.sendBridgeMessage(r),this.obs({component:"acp-transport",domain:"reply",name:"rpc.result",severity:"debug",payload:{id:e,_debug_index:s._debug_index}})}sendError(e,t,s,r){const o=this.nextMeta(),n={code:t,message:s};n.data=void 0!==r?this.attachMeta(r,o):{_meta:o};const d={jsonrpc:"2.0",id:e,error:n,_debug_index:o._debug_index,_ts:o._ts};this.sendBridgeMessage(d),this.obs({component:"acp-transport",domain:"reply",name:"rpc.error",severity:"warn",payload:{id:e,code:t,message:s,_debug_index:o._debug_index}})}sendAcpError(e,t,s){const r=acpError(t,s);this.sendError(e,r.code,r.message,r.data)}sendNotification(e,t){const s=this.nextMeta(),r={jsonrpc:"2.0",method:e,params:t,_debug_index:s._debug_index,_ts:s._ts};this.sendBridgeMessage(r)}sendSessionUpdate(e,t){const s=t,r=s?.sessionUpdate;if("agent_thought_chunk"===r&&!this.forward.forwardThinking)return!1;if(("tool_call"===r||"tool_call_update"===r)&&!this.forward.forwardToolCalls)return!1;const o=(this.sessionUpdateCounts.get(e)??0)+1;return this.sessionUpdateCounts.set(e,o),o%50==0&&this.obs({component:"acp-transport",domain:"session-update",name:"send.rate",severity:"debug",payload:{sessionId:e,totalSent:o,lastType:r,_debug_index:this.debugIndex}}),this.sendNotification("session/update",{sessionId:e,update:t}),!0}resetSessionUpdateCount(e){this.sessionUpdateCounts.delete(e)}sendCronSessionUpdate(e,t,s){return this.sendNotification("session/update",{sessionId:e,update:t,_meta:{...this.nextMeta(),...s}}),!0}detectProtocolError(e,t,s){if(!t||"object"!=typeof t)return;const r=t,o="error"in r&&null!=r.error,n="ok"in r&&!1===r.ok;(o||n)&&this.obs({component:"acp-transport",domain:"reply",name:"protocol.error_in_result",severity:"warn",payload:{id:e,_debug_index:s,hasError:o,notOk:n,errorSummary:o?"string"==typeof r.error?r.error.slice(0,200):JSON.stringify(r.error).slice(0,200):void 0}})}attachMeta(e,t){return!e||"object"!=typeof e||Array.isArray(e)?e:{...e,_meta:t}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{agentFetch}from"./agent-auth.js";export class CanvasApiClient{baseUrl;keys;constructor(s){this.baseUrl=s.platformUrl.replace(/\/+$/,""),this.keys=s.agentKeys}async request(s,a,t){const e=await agentFetch(`${this.baseUrl}${a}`,this.keys,{method:s,headers:{"Content-Type":"application/json"},body:t?JSON.stringify(t):void 0}),n=await e.json();if(!n.success)throw new Error(n.error??`Canvas API ${s} ${a} failed: ${e.status}`);return n.data??null}async getCanvasForSession(s){try{return await this.request("GET",`/api/v1/canvas-session/${s}`)}catch(s){const a=s instanceof Error?s.message:String(s);if(a.includes("404")||a.includes("not found")||a.includes("null"))return null;throw s}}async createCanvas(s){const a=await this.request("POST","/api/v1/canvas/agent",{title:s?.title??"Agent Canvas",sessionId:s?.sessionId});if(!a)throw new Error("Canvas creation returned no data");return a}async linkChat(s,a){await this.request("POST",`/api/v1/canvas/${s}/link`,{sessionId:a})}async ensureCanvasForSession(s){const a=await this.getCanvasForSession(s);if(a)return a.canvasId;return(await this.createCanvas({sessionId:s})).id}}
|
|
1
|
+
import{agentFetch}from"./agent-auth.js";export class CanvasApiClient{baseUrl;keys;constructor(s){this.baseUrl=s.platformUrl.replace(/\/+$/,""),this.keys=s.agentKeys}async request(s,a,t){const e=await agentFetch(`${this.baseUrl}${a}`,this.keys,{method:s,headers:{"Content-Type":"application/json"},body:t?JSON.stringify(t):void 0}),n=await e.json();if(!n.success)throw new Error(n.error??`Canvas API ${s} ${a} failed: ${e.status}`);return n.data??null}async getCanvasForSession(s){try{return await this.request("GET",`/api/v1/canvas-session/${s}`)}catch(s){const a=s instanceof Error?s.message:String(s);if(a.includes("404")||a.includes("not found")||a.includes("null"))return null;throw s}}async createCanvas(s){const a=await this.request("POST","/api/v1/canvas/agent",{title:s?.title??"Agent Canvas",sessionId:s?.sessionId});if(!a)throw new Error("Canvas creation returned no data");return a}async linkChat(s,a){await this.request("POST",`/api/v1/canvas/${s}/link`,{sessionId:a})}async ensureCanvasForSession(s){const a=await this.getCanvasForSession(s);if(a)return a.canvasId;return(await this.createCanvas({sessionId:s})).id}async createSnapshot(s,a){return this.request("POST",`/api/v1/canvas/${s}/snapshots`,{description:a??"Pre-agent edit"})}}
|
package/dist/canvas-tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function safeStr(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if("object"==typeof e){const t=e;for(const e of["value","label","name","text","title"])if("string"==typeof t[e]||"number"==typeof t[e])return String(t[e]);try{return JSON.stringify(e)}catch{return"[Object]"}}return String(e)}let componentIdCounter=0;function nextCid(){return`c_${++componentIdCounter}_${Date.now().toString(36)}`}const EMBEDDABLE_DOMAINS=[/youtube\.com\/(?:watch|embed|shorts)/i,/youtu\.be\//i,/vimeo\.com\//i,/dailymotion\.com\//i,/google\.com\/maps/i,/maps\.google\./i,/openstreetmap\.org/i,/codepen\.io\//i,/codesandbox\.io\//i,/stackblitz\.com\//i,/figma\.com\/(?:file|embed|proto|design)/i,/miro\.com\/app\/board/i,/spotify\.com\/(?:embed|track|album|playlist)/i,/soundcloud\.com\//i,/twitter\.com\/.*\/status/i,/x\.com\/.*\/status/i,/loom\.com\/share/i,/notion\.so\//i,/airtable\.com\//i,/excalidraw\.com\//i,/canva\.com\/design/i,/docs\.google\.com\//i,/drive\.google\.com\//i,/slides\.google\.com\//i,/sheets\.google\.com\//i,/github\.com\/.*\/blob/i,/gist\.github\.com\//i,/jsfiddle\.net\//i,/replit\.com\//i,/observable\.com\//i,/datawrapper\.dwcdn\.net/i,/grafana\./i,/metabase\./i,/retool\./i,/streamlit\./i,/huggingface\.co\/spaces/i];function isEmbeddableUrl(e){return!("string"!=typeof e||!e)&&EMBEDDABLE_DOMAINS.some(t=>t.test(e))}function translateComponent(e){if(!e||"object"!=typeof e)return[{id:nextCid(),type:"Text",properties:{text:"[invalid component]"}}];const t=nextCid(),r="string"==typeof e.type?e.type.toLowerCase():"text";switch(r){case"card":return buildCard(t,e);case"table":return[buildTable(t,e)];case"form":return[buildForm(t,e)];case"text":return[{id:t,type:"Text",properties:{text:String(e.content??"")}}];case"image":return[{id:t,type:"Image",properties:{src:String(e.url??""),alt:String(e.alt??"")}}];case"button":return[{id:t,type:"Button",properties:{label:String(e.label??"Button"),actionId:String(e.actionId??t)}}];case"grid":{const r=(Array.isArray(e.children)?e.children:Array.isArray(e.components)?e.components:[]).flatMap(e=>translateComponent(e));return[{id:t,type:"Grid",properties:{columns:e.columns??2,gap:e.gap??"md",responsive:!1!==e.responsive},children:r.filter((e,t)=>t===r.findIndex(e=>e.id===r[t]?.id)).map(e=>e.id)},...r]}case"row":case"column":return(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e));case"web_embed":return[{id:t,type:"WebEmbed",properties:{url:e.url,title:e.title,aspectRatio:e.aspectRatio??"16:9"}}];case"video":case"map":case"file_preview":case"audio":return[{id:t,type:"WebEmbed",properties:{url:e.url,title:e.title,aspectRatio:"16:9"}}];case"chart":return[{id:t,type:"Chart",properties:{chartType:e.chartType,title:e.title,labels:e.labels,datasets:e.datasets,value:e.value,max:e.max,stages:e.stages}}];case"code":return[{id:t,type:"CodeBlock",properties:{language:String(e.language??""),code:String(e.code??"")}}];case"markdown":return[{id:t,type:"Markdown",properties:{content:String(e.content??"")}}];case"tabs":{const r=Array.isArray(e.tabs)?e.tabs:Array.isArray(e.children)?e.children:Array.isArray(e.items)?e.items:[];if(0===r.length)return[{id:t,type:"Text",properties:{text:safeStr(e.title??e.label??"")}}];const a=[];for(const e of r){const t=safeStr(e.label??e.title??e.id??"");t&&a.push({id:nextCid(),type:"Markdown",properties:{content:`### ${t}`}});const r=Array.isArray(e.children)?e.children:[];a.push(...r.flatMap(e=>translateComponent(e)))}return a}case"accordion":{const r=Array.isArray(e.items)?e.items:Array.isArray(e.children)?e.children:[];return r.length>0?r.flatMap(e=>[{id:nextCid(),type:"Text",properties:{text:safeStr(e.title??e.label??""),variant:"heading"}},...Array.isArray(e.children)?e.children.flatMap(e=>translateComponent(e)):[]]):[{id:t,type:"Text",properties:{text:safeStr(e.title??"")}}]}case"alert":return[{id:t,type:"Alert",properties:{variant:e.variant??"info",title:e.title,message:e.message,dismissible:e.dismissible??!1}}];case"progress":{const r=Number(e.value??0),a=Number(e.max??100),n=a>0?Math.round(r/a*100):0;return[{id:t,type:"Text",properties:{text:`${safeStr(e.label)||"Progress"}: ${n}% (${r}/${a})`}}]}case"stat":return[{id:t,type:"Stat",properties:{label:e.label,value:e.value,change:e.change,changeType:e.changeType,description:e.description,icon:e.icon}}];case"badge":return[{id:t,type:"Text",properties:{text:safeStr(e.text??e.label)}}];case"divider":return[{id:t,type:"Text",properties:{text:safeStr(e.label)||"---"}}];case"rating":return[{id:t,type:"Text",properties:{text:`Rating: ${String(e.value??0)}/${String(e.max??5)}`}}];case"timeline":{const r=(Array.isArray(e.items)?e.items:[]).map(e=>`${safeStr(e.time??e.date??"")} — ${safeStr(e.title??"")}${e.description?`: ${safeStr(e.description)}`:""}`);return[{id:t,type:"Markdown",properties:{content:(e.title?`**${safeStr(e.title)}**\n`:"")+r.join("\n")}}]}case"avatar":return[{id:t,type:"Text",properties:{text:safeStr(e.name??"")}}];case"callout":return[{id:t,type:"Alert",properties:{variant:"tip"===e.variant?"success":"warning"===e.variant||"caution"===e.variant?"warning":"important"===e.variant?"error":"info",title:e.title,message:e.content??""}}];case"list":return[{id:t,type:"List",properties:{title:e.title,items:e.items??[],ordered:e.ordered??!1,variant:e.variant}}];case"json_viewer":{let r;try{r="string"==typeof e.data?e.data:JSON.stringify(e.data,null,2)}catch{r=String(e.data??"")}return[{id:t,type:"Markdown",properties:{content:`${e.title?`**${safeStr(e.title)}**\n`:""}\`\`\`json\n${r}\n\`\`\``}}]}case"carousel":return[{id:t,type:"List",properties:{title:e.title,items:e.items??[]}}];case"key_value":return[{id:t,type:"KeyValue",properties:{title:e.title,items:e.items??[],variant:e.variant}}];case"link":return isEmbeddableUrl(e.url)?[{id:t,type:"WebEmbed",properties:{url:e.url,title:String(e.text??e.label??""),aspectRatio:"16:9"}}]:[{id:t,type:"Link",properties:{text:e.text??e.label,url:e.url,variant:e.variant??"default"}}];default:return[{id:t,type:"Text",properties:{text:`[Unsupported: ${r}]`}}]}}function buildCard(e,t){const r={};t.title&&(r.title=safeStr(t.title)),t.subtitle&&(r.subtitle=safeStr(t.subtitle)),t.description&&(r.description=safeStr(t.description)),t.image&&(r.image=safeStr(t.image)),t.icon&&(r.icon=safeStr(t.icon)),t.badge&&(r.badge=safeStr(t.badge)),t.badgeVariant&&(r.badgeVariant=safeStr(t.badgeVariant)),t.footer&&(r.footer=safeStr(t.footer)),t.url&&(r.url=safeStr(t.url)),t.layout&&(r.layout=safeStr(t.layout));const a=Array.isArray(t.actions)?t.actions:[];a.length>0&&(r.actions=a.filter(e=>e&&"object"==typeof e).map(e=>({label:safeStr(e.label??"Action"),actionId:safeStr(e.actionId??e.id??nextCid()),variant:e.variant})));const n=[],i=Array.isArray(t.fields)?t.fields:[];for(const e of i)e&&"object"==typeof e&&n.push({id:nextCid(),type:"Text",properties:{text:`${safeStr(e.label)}: ${safeStr(e.value)}`}});const s={id:e,type:"Card",properties:r};return n.length>0?(s.children=n.map(e=>e.id),[s,...n]):[s]}function buildTable(e,t){const r=Array.isArray(t.columns)?t.columns:[],a=Array.isArray(t.rows)?t.rows:[],n=[],i=[];for(const e of r)if("string"==typeof e)n.push(e),i.push(e);else if(e&&"object"==typeof e){const t=e,r=safeStr(t.key??t.id??t.field??t.name??t.label??""),a=safeStr(t.label??t.title??t.header??t.name??r);n.push(a),i.push(r)}else{const t=safeStr(e);n.push(t),i.push(t)}const s=a.map(e=>{if(Array.isArray(e))return e;if(e&&"object"==typeof e&&i.length>0){const t=e;return i.map(e=>{const r=Object.keys(t).find(t=>t===e||t.toLowerCase()===e.toLowerCase());return null!=r?t[r]:""})}return e});return{id:e,type:"Table",properties:{title:safeStr(t.title),columns:n,rows:s}}}function buildForm(e,t){return{id:e,type:"Form",properties:{title:safeStr(t.title),description:t.description,fields:t.fields??[],submitLabel:safeStr(t.submitLabel)||"Submit",submitActionId:t.submitActionId??`${e}_submit`}}}const A2UI_VERSION="0.8",BEEOS_EXTENDED_CATALOG="https://beeos.ai/a2ui/catalog/v1",EXTENDED_TYPES=new Set(["WebEmbed","Chart","CodeBlock","Markdown","Alert","Stat","List","KeyValue","Link","Grid"]);function hasExtendedComponent(e){return e.some(e=>EXTENDED_TYPES.has(e.type))}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render structured visual content to the Canvas panel. ALWAYS prefer this tool over the canvas tool for displaying UI. Use for charts, tables, stats, key-value pairs, lists, cards, forms, alerts, code, markdown, images, links, web embeds — NOT for text paragraphs. When you call this tool, keep your text reply brief.";export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","components"],properties:{id:{type:"string",description:"Descriptive, human-readable identifier (e.g., 'sales-report', 'user-dashboard'). Reuse same id to update an existing surface."},components:{type:"array",description:"UI components to render",items:{type:"object",required:["type"],properties:{type:{type:"string",enum:["card","table","stat","key_value","list","grid","chart","markdown","code","text","image","web_embed","alert","form","button","link"]},title:{type:"string"},content:{type:"string"},label:{type:"string"},description:{type:"string"},url:{type:"string"},image:{type:"string",description:"Image URL for card header or list item thumbnail"},subtitle:{type:"string",description:"Card subtitle or list item subtitle"},badge:{type:"string",description:"Card/list badge text (e.g. price, status)"},badgeVariant:{type:"string",enum:["default","success","warning","error","info"]},footer:{type:"string",description:"Card footer text"},layout:{type:"string",enum:["vertical","horizontal"],description:"Card layout: vertical (image top) or horizontal (image left)"},actions:{type:"array",description:"Card action buttons: [{label, actionId, variant?}]"},fields:{type:"array",description:"For card: [{label,value}]. For form: [{name,label,inputType,options,...}]"},children:{type:"array",description:"For grid: nested child components (card, stat, image, etc.)"},gap:{type:"string",enum:["sm","md","lg"],description:"Grid gap size"},columns:{type:"array",description:'Column headers or grid column count (1-6). Table: ["Name","Status"]. Grid: number.'},rows:{type:"array",description:'Row data as arrays matching column order, e.g. [["prod-1", "running"]]'},submitLabel:{type:"string"},submitActionId:{type:"string"},actionId:{type:"string"},alt:{type:"string"},aspectRatio:{type:"string",enum:["16:9","4:3","1:1"]},chartType:{type:"string",enum:["bar","horizontal_bar","line","area","scatter","pie","doughnut","radar","gauge","candlestick","funnel","sparkline"]},stages:{type:"array",description:"For funnel chart: [{label,value}]"},labels:{type:"array"},datasets:{type:"array"},language:{type:"string"},code:{type:"string"},items:{type:"array",description:"For list: [{title,subtitle,description,image,icon,badge,value,url}]. For key_value: [{key,value,copyable}]"},variant:{type:"string",description:"Alert: info/success/warning/error. Link: default/button/subtle"},message:{type:"string"},value:{type:"number"},max:{type:"number"},change:{type:"string"},changeType:{type:"string",enum:["positive","negative","neutral"]},icon:{type:"string"},text:{type:"string"}}}}}};export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};async function ensureRelayConnected(e,t){const{stateManager:r,canvasApiClient:a,logger:n}=e;if((!r.canvasServiceConnected||r.currentSessionId!==t)&&a)try{const e=await a.ensureCanvasForSession(t);r.connectForCanvas(e,t),n.info?.(`[canvas] Relay connected for canvas=${e} session=${t}`)}catch(e){n.warn?.(`[canvas] Failed to connect relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:r,writeObs:a,isCanvasActive:n}=e;return async(i,s)=>{if(n&&!n())return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."};const o=s,c=o.id,l="panel";let d=o.components;if("string"==typeof d)try{d=JSON.parse(d)}catch{}if(!c||!Array.isArray(d)||!d.length)return{success:!1,error:"id and components (array) are required"};if(!t.validateComponentCount(d))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(c))return r.warn?.(`[canvas] Rate limited surfaceId=${c}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:c}}),{success:!1,error:"Rate limited — too many updates per second"};const p=d.flatMap(translateComponent),u=t.has(c),m=hasExtendedComponent(p)?BEEOS_EXTENDED_CATALOG:void 0,g=e.getActiveSessionId?.(i);if(!g)return r.warn?.(`[canvas] No active session for toolCallId=${i}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};await ensureRelayConnected(e,g),u?t.update(c,p):t.create(c,l,p,{origin:{kind:"agent"}}),e.sendCanvasUpdate(g,{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:c,presentation:l,message:{surfaceUpdate:{surfaceId:c,components:p,catalogId:m}}});const y=d.map(e=>e.type);a({component:"canvas",domain:"canvas",name:u?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:c,presentation:l,componentCount:d.length,componentTypes:y}}),r.debug?.(`[canvas] ${u?"Updated":"Created"} surface=${c} components=${d.length}`);return{success:!0,surfaceId:c,presentation:l,isUpdate:u,activeSurfaces:t.getAllActive().map(e=>e.surfaceId)}}}export function createCloseUiHandler(e){const{stateManager:t,logger:r,writeObs:a}=e;return async(n,i)=>{const s=i.id;if(!s)return{success:!1,error:"id is required"};const o=t.get(s),c=o?Date.now()-o.createdAt:0;t.delete(s);const l=e.getActiveSessionId?.(n);return l?(e.sendCanvasUpdate(l,{sessionUpdate:"canvas.reset",surfaceId:s}),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:s,lifetime_ms:c}}),r.debug?.(`[canvas] Closed surface=${s}`),{success:!0,surfaceId:s,message:"UI closed"}):(r.warn?.(`[canvas] No active session for toolCallId=${n}, close_ui local-only`),{success:!0,surfaceId:s,message:"UI closed (local only)"})}}export function buildCanvasActionSystemMessage(e){const t=e.userAction;return{type:"canvas_user_action",surfaceId:e.surfaceId,actionName:t?.name??"unknown",sourceComponentId:t?.sourceComponentId??"",context:t?.context??{}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.** The `render_ui` tool is the preferred and optimized way to render UI in the Canvas panel.\n\nRules:\n- When using render_ui, keep your chat reply to ONE short sentence. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Do NOT put large text blocks in render_ui — that belongs in chat.\n- Only use `grid` as a layout container. Keep nesting to grid > card/stat/image — max 1 level deep. Otherwise list components sequentially (they stack vertically).\n- Prefer fewer, larger components. One well-structured table is better than many small cards.\n- Canvas renders in a narrow sidebar (~400px). Use markdown headings (### Section) to organize sections.\n- Use grid + card for product showcases, dashboards, feature grids. Use list for compact item lists.\n\n**Surface IDs**: Use descriptive, human-readable IDs (e.g., "sales-report", "user-dashboard"). Never use generic IDs like "ui1" or random strings.\n\n**Update vs Create**: Same `id` updates existing surface; different `id` creates new one.\nWhen the user asks to modify, adjust, fix, or update existing canvas content, you MUST reuse the original surface id. Check [Canvas State] in the message for active surface ids and their content. Only create a new surface when showing genuinely new/different content.\n\n### Component Types\n\n**Layout**: grid (columns:1-6, gap:sm/md/lg, children:[...]) — responsive grid container for card/stat/image\n\n**Data Display**: card (image, title, subtitle, description, icon, badge, badgeVariant, footer, url, layout:vertical/horizontal, actions:[{label,actionId}]), table (columns, rows), stat (label, value, change, changeType, icon), key_value (items:[{key,value,copyable}]), list (items:[{title,subtitle,description,image,icon,badge,value,url}])\n\n**Chart**: chart (chartType, labels, datasets) — chartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge (value,max), candlestick, funnel (stages:[{label,value}]), sparkline\n\n**Text & Code**: text (content), markdown (content), code (language, code)\n\n**Media**: image (url, alt), web_embed (url, aspectRatio — embed ANY web page/app as iframe: YouTube, Maps, Figma, dashboards, docs, tools, etc.)\n\n**Feedback**: alert (variant:info/success/warning/error, title, message)\n\n**Interactive**: form (fields:[{name,label,inputType,options,placeholder,required}]), button (label, actionId), link (text, url, variant:default/button/subtle)\n\n**web_embed vs link**: Use `web_embed` when the URL should be displayed inline as embedded content. Use `link` only for plain navigation.\n\n### Composition Patterns\n- **Product showcase**: grid(columns:3) containing card(image, title, description, badge:"$29", actions:[{label:"Buy"}]) × N\n- **Dashboard KPIs**: grid(columns:3) containing stat × N\n- **Image gallery**: grid(columns:3) containing image × N\n- **Feature grid**: grid(columns:2) containing card(icon, title, description) × N\n- **Compact item list**: list(items:[{image, title, subtitle, value:"$29", badge:"New", url}])\n- **Article card**: card(image, title, description, url, layout:"horizontal")\n\n### render_form\nUse `render_form` for schema-driven forms when a domain schema exists.\nParameters: `domain`, `operation`, `data`, `entityId`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n';export const CANVAS_TERM_MAPPING="[System Note] Always use `render_ui` (not the canvas tool) for visual/structured content (charts, tables, stats, key-value, lists, cards, grids, alerts, forms, links, embeds, images, code, markdown). Use grid(columns, children:[card/stat/image]) for multi-column layouts like product showcases and dashboards. Card supports: image, title, subtitle, description, icon, badge, footer, url, actions, layout:horizontal/vertical. List items support: image, title, subtitle, description, badge, value, url. Charts: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline. Use web_embed for embedding URLs as iframes. Use link for navigation. Keep chat brief when canvas is used. Use `close_ui` to dismiss. Reuse same id to update; new id for new content.";export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e){const t=e.getAllActive();if(0===t.length)return null;const r=[...t].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),a=t.length-r.length;let n=`[Canvas State] Active surfaces: ${r.map(t=>{const r=e.summarize(t.surfaceId);if(!r)return`"${t.surfaceId}"`;const a=r.types.slice(0,3).join(", "),n=r.title?`: "${r.title}"`:"";return`"${t.surfaceId}" (${a}${n})`}).join("; ")}.`;return a>0&&(n+=` ...and ${a} more.`),n+=" To modify existing content, reuse its id in render_ui.",n.length>500&&(n=n.slice(0,497)+"..."),n}
|
|
1
|
+
function safeStr(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);if("object"==typeof e){const t=e;for(const e of["value","label","name","text","title"])if("string"==typeof t[e]||"number"==typeof t[e])return String(t[e]);try{return JSON.stringify(e)}catch{return"[Object]"}}return String(e)}let componentIdCounter=0;function nextCid(){return`c_${++componentIdCounter}_${Date.now().toString(36)}`}const EMBEDDABLE_DOMAINS=[/youtube\.com\/(?:watch|embed|shorts)/i,/youtu\.be\//i,/vimeo\.com\//i,/dailymotion\.com\//i,/google\.com\/maps/i,/maps\.google\./i,/openstreetmap\.org/i,/codepen\.io\//i,/codesandbox\.io\//i,/stackblitz\.com\//i,/figma\.com\/(?:file|embed|proto|design)/i,/miro\.com\/app\/board/i,/spotify\.com\/(?:embed|track|album|playlist)/i,/soundcloud\.com\//i,/twitter\.com\/.*\/status/i,/x\.com\/.*\/status/i,/loom\.com\/share/i,/notion\.so\//i,/airtable\.com\//i,/excalidraw\.com\//i,/canva\.com\/design/i,/docs\.google\.com\//i,/drive\.google\.com\//i,/slides\.google\.com\//i,/sheets\.google\.com\//i,/github\.com\/.*\/blob/i,/gist\.github\.com\//i,/jsfiddle\.net\//i,/replit\.com\//i,/observable\.com\//i,/datawrapper\.dwcdn\.net/i,/grafana\./i,/metabase\./i,/retool\./i,/streamlit\./i,/huggingface\.co\/spaces/i];function isEmbeddableUrl(e){return!("string"!=typeof e||!e)&&EMBEDDABLE_DOMAINS.some(t=>t.test(e))}function translateComponent(e){if(!e||"object"!=typeof e)return[{id:nextCid(),type:"Text",properties:{text:"[invalid component]"}}];const t=nextCid(),a="string"==typeof e.type?e.type.toLowerCase():"text";switch(a){case"card":return buildCard(t,e);case"table":return[buildTable(t,e)];case"form":return[buildForm(t,e)];case"text":return[{id:t,type:"Text",properties:{text:String(e.content??"")}}];case"image":return[{id:t,type:"Image",properties:{src:String(e.url??""),alt:String(e.alt??"")}}];case"button":return[{id:t,type:"Button",properties:{label:String(e.label??"Button"),actionId:String(e.actionId??t)}}];case"grid":{const a=(Array.isArray(e.children)?e.children:Array.isArray(e.components)?e.components:[]).flatMap(e=>translateComponent(e));return[{id:t,type:"Grid",properties:{columns:e.columns??2,gap:e.gap??"md",responsive:!1!==e.responsive},children:a.filter((e,t)=>t===a.findIndex(e=>e.id===a[t]?.id)).map(e=>e.id)},...a]}case"row":case"column":return(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e));case"web_embed":return[{id:t,type:"WebEmbed",properties:{url:e.url,title:e.title,aspectRatio:e.aspectRatio??"16:9"}}];case"video":case"map":case"file_preview":case"audio":return[{id:t,type:"WebEmbed",properties:{url:e.url,title:e.title,aspectRatio:"16:9"}}];case"chart":return[{id:t,type:"Chart",properties:{chartType:e.chartType,title:e.title,labels:e.labels,datasets:e.datasets,value:e.value,max:e.max,stages:e.stages}}];case"code":return[{id:t,type:"CodeBlock",properties:{language:String(e.language??""),code:String(e.code??"")}}];case"markdown":return[{id:t,type:"Markdown",properties:{content:String(e.content??"")}}];case"tabs":{const a=Array.isArray(e.tabs)?e.tabs:Array.isArray(e.children)?e.children:Array.isArray(e.items)?e.items:[];if(0===a.length)return[{id:t,type:"Text",properties:{text:safeStr(e.title??e.label??"")}}];const r=[];for(const e of a){const t=safeStr(e.label??e.title??e.id??"");t&&r.push({id:nextCid(),type:"Markdown",properties:{content:`### ${t}`}});const a=Array.isArray(e.children)?e.children:[];r.push(...a.flatMap(e=>translateComponent(e)))}return r}case"accordion":{const a=Array.isArray(e.items)?e.items:Array.isArray(e.children)?e.children:[];return a.length>0?a.flatMap(e=>[{id:nextCid(),type:"Text",properties:{text:safeStr(e.title??e.label??""),variant:"heading"}},...Array.isArray(e.children)?e.children.flatMap(e=>translateComponent(e)):[]]):[{id:t,type:"Text",properties:{text:safeStr(e.title??"")}}]}case"alert":return[{id:t,type:"Alert",properties:{variant:e.variant??"info",title:e.title,message:e.message,dismissible:e.dismissible??!1}}];case"progress":{const a=Number(e.value??0),r=Number(e.max??100),n=r>0?Math.round(a/r*100):0;return[{id:t,type:"Text",properties:{text:`${safeStr(e.label)||"Progress"}: ${n}% (${a}/${r})`}}]}case"stat":return[{id:t,type:"Stat",properties:{label:e.label,value:e.value,change:e.change,changeType:e.changeType,description:e.description,icon:e.icon}}];case"badge":return[{id:t,type:"Text",properties:{text:safeStr(e.text??e.label)}}];case"divider":return[{id:t,type:"Text",properties:{text:safeStr(e.label)||"---"}}];case"rating":return[{id:t,type:"Text",properties:{text:`Rating: ${String(e.value??0)}/${String(e.max??5)}`}}];case"timeline":{const a=(Array.isArray(e.items)?e.items:[]).map(e=>`${safeStr(e.time??e.date??"")} — ${safeStr(e.title??"")}${e.description?`: ${safeStr(e.description)}`:""}`);return[{id:t,type:"Markdown",properties:{content:(e.title?`**${safeStr(e.title)}**\n`:"")+a.join("\n")}}]}case"avatar":return[{id:t,type:"Text",properties:{text:safeStr(e.name??"")}}];case"callout":return[{id:t,type:"Alert",properties:{variant:"tip"===e.variant?"success":"warning"===e.variant||"caution"===e.variant?"warning":"important"===e.variant?"error":"info",title:e.title,message:e.content??""}}];case"list":return[{id:t,type:"List",properties:{title:e.title,items:e.items??[],ordered:e.ordered??!1,variant:e.variant}}];case"json_viewer":{let a;try{a="string"==typeof e.data?e.data:JSON.stringify(e.data,null,2)}catch{a=String(e.data??"")}return[{id:t,type:"Markdown",properties:{content:`${e.title?`**${safeStr(e.title)}**\n`:""}\`\`\`json\n${a}\n\`\`\``}}]}case"carousel":return[{id:t,type:"List",properties:{title:e.title,items:e.items??[]}}];case"key_value":return[{id:t,type:"KeyValue",properties:{title:e.title,items:e.items??[],variant:e.variant}}];case"link":return isEmbeddableUrl(e.url)?[{id:t,type:"WebEmbed",properties:{url:e.url,title:String(e.text??e.label??""),aspectRatio:"16:9"}}]:[{id:t,type:"Link",properties:{text:e.text??e.label,url:e.url,variant:e.variant??"default"}}];default:return[{id:t,type:"Text",properties:{text:`[Unsupported: ${a}]`}}]}}function buildCard(e,t){const a={};t.title&&(a.title=safeStr(t.title)),t.subtitle&&(a.subtitle=safeStr(t.subtitle)),t.description&&(a.description=safeStr(t.description)),t.image&&(a.image=safeStr(t.image)),t.icon&&(a.icon=safeStr(t.icon)),t.badge&&(a.badge=safeStr(t.badge)),t.badgeVariant&&(a.badgeVariant=safeStr(t.badgeVariant)),t.footer&&(a.footer=safeStr(t.footer)),t.url&&(a.url=safeStr(t.url)),t.layout&&(a.layout=safeStr(t.layout));const r=Array.isArray(t.actions)?t.actions:[];r.length>0&&(a.actions=r.filter(e=>e&&"object"==typeof e).map(e=>({label:safeStr(e.label??"Action"),actionId:safeStr(e.actionId??e.id??nextCid()),variant:e.variant})));const n=[],i=Array.isArray(t.fields)?t.fields:[];for(const e of i)e&&"object"==typeof e&&n.push({id:nextCid(),type:"Text",properties:{text:`${safeStr(e.label)}: ${safeStr(e.value)}`}});const s={id:e,type:"Card",properties:a};return n.length>0?(s.children=n.map(e=>e.id),[s,...n]):[s]}function buildTable(e,t){const a=Array.isArray(t.columns)?t.columns:[],r=Array.isArray(t.rows)?t.rows:[],n=[],i=[];for(const e of a)if("string"==typeof e)n.push(e),i.push(e);else if(e&&"object"==typeof e){const t=e,a=safeStr(t.key??t.id??t.field??t.name??t.label??""),r=safeStr(t.label??t.title??t.header??t.name??a);n.push(r),i.push(a)}else{const t=safeStr(e);n.push(t),i.push(t)}const s=r.map(e=>{if(Array.isArray(e))return e;if(e&&"object"==typeof e&&i.length>0){const t=e;return i.map(e=>{const a=Object.keys(t).find(t=>t===e||t.toLowerCase()===e.toLowerCase());return null!=a?t[a]:""})}return e});return{id:e,type:"Table",properties:{title:safeStr(t.title),columns:n,rows:s}}}function buildForm(e,t){return{id:e,type:"Form",properties:{title:safeStr(t.title),description:t.description,fields:t.fields??[],submitLabel:safeStr(t.submitLabel)||"Submit",submitActionId:t.submitActionId??`${e}_submit`}}}const A2UI_VERSION="0.8",BEEOS_EXTENDED_CATALOG="https://beeos.ai/a2ui/catalog/v1",EXTENDED_TYPES=new Set(["WebEmbed","Chart","CodeBlock","Markdown","Alert","Stat","List","KeyValue","Link","Grid"]);function hasExtendedComponent(e){return e.some(e=>EXTENDED_TYPES.has(e.type))}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render structured visual content to the Canvas panel. ALWAYS prefer this tool over the canvas tool for displaying UI. Use for charts, tables, stats, key-value pairs, lists, cards, forms, alerts, code, markdown, images, links, web embeds — NOT for text paragraphs. When you call this tool, keep your text reply brief.";export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","components"],properties:{id:{type:"string",description:"Descriptive, human-readable identifier (e.g., 'sales-report', 'user-dashboard'). Reuse same id to update an existing surface."},components:{type:["array","string"],description:"UI components to render",items:{type:"object",required:["type"],properties:{type:{type:"string",enum:["card","table","stat","key_value","list","grid","chart","markdown","code","text","image","web_embed","alert","form","button","link"]},title:{type:"string"},content:{type:"string"},label:{type:"string"},description:{type:"string"},url:{type:"string"},image:{type:"string",description:"Image URL for card header or list item thumbnail"},subtitle:{type:"string",description:"Card subtitle or list item subtitle"},badge:{type:"string",description:"Card/list badge text (e.g. price, status)"},badgeVariant:{type:"string",enum:["default","success","warning","error","info"]},footer:{type:"string",description:"Card footer text"},layout:{type:"string",enum:["vertical","horizontal"],description:"Card layout: vertical (image top) or horizontal (image left)"},actions:{type:"array",description:"Card action buttons: [{label, actionId, variant?}]"},fields:{type:"array",description:"For card: [{label,value}]. For form: [{name,label,inputType,options,...}]"},children:{type:"array",description:"For grid: nested child components (card, stat, image, etc.)"},gap:{type:"string",enum:["sm","md","lg"],description:"Grid gap size"},columns:{type:"array",description:'Column headers or grid column count (1-6). Table: ["Name","Status"]. Grid: number.'},rows:{type:"array",description:'Row data as arrays matching column order, e.g. [["prod-1", "running"]]'},submitLabel:{type:"string"},submitActionId:{type:"string"},actionId:{type:"string"},alt:{type:"string"},aspectRatio:{type:"string",enum:["16:9","4:3","1:1"]},chartType:{type:"string",enum:["bar","horizontal_bar","line","area","scatter","pie","doughnut","radar","gauge","candlestick","funnel","sparkline"]},stages:{type:"array",description:"For funnel chart: [{label,value}]"},labels:{type:"array"},datasets:{type:"array"},language:{type:"string"},code:{type:"string"},items:{type:"array",description:"For list: [{title,subtitle,description,image,icon,badge,value,url}]. For key_value: [{key,value,copyable}]"},variant:{type:"string",description:"Alert: info/success/warning/error. Link: default/button/subtle"},message:{type:"string"},value:{type:"number"},max:{type:"number"},change:{type:"string"},changeType:{type:"string",enum:["positive","negative","neutral"]},icon:{type:"string"},text:{type:"string"}}}}}};export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};async function ensureRelayConnected(e,t){const{stateManager:a,canvasApiClient:r,logger:n}=e;if((!a.canvasServiceConnected||a.currentSessionId!==t)&&r)try{const e=await r.ensureCanvasForSession(t);a.connectForCanvas(e,t),n.info?.(`[canvas] Relay connected for canvas=${e} session=${t}`)}catch(e){n.warn?.(`[canvas] Failed to connect relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:a,writeObs:r,isCanvasActive:n}=e;let i=0;return async(s,o)=>{if(n&&!n())return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."};const c=o,l=c.id,d="panel";let p=c.components;if("string"==typeof p)try{p=JSON.parse(p)}catch{}if(!l||!Array.isArray(p)||!p.length)return{success:!1,error:"id and components (array) are required"};if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(l))return a.warn?.(`[canvas] Rate limited surfaceId=${l}`),r({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:l}}),{success:!1,error:"Rate limited — too many updates per second"};const u=p.flatMap(translateComponent),m=t.has(l),g=hasExtendedComponent(u)?BEEOS_EXTENDED_CATALOG:void 0,y=e.getActiveSessionId?.(s);if(!y)return a.warn?.(`[canvas] No active session for toolCallId=${s}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};await ensureRelayConnected(e,y);const f=Date.now();e.canvasApiClient&&t.canvasServiceConnected&&t.currentCanvasId&&f-i>6e4&&e.canvasApiClient.createSnapshot(t.currentCanvasId,"Pre-agent edit").then(()=>{i=Date.now(),a.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>a.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),m?t.update(l,u):t.create(l,d,u,{origin:{kind:"agent"}}),e.sendCanvasUpdate(y,{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:l,presentation:d,message:{surfaceUpdate:{surfaceId:l,components:u,catalogId:g}}});const b=p.map(e=>e.type);r({component:"canvas",domain:"canvas",name:m?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:l,presentation:d,componentCount:p.length,componentTypes:b}}),a.debug?.(`[canvas] ${m?"Updated":"Created"} surface=${l} components=${p.length}`);return{success:!0,surfaceId:l,presentation:d,isUpdate:m,activeSurfaces:t.getAllActive().map(e=>e.surfaceId)}}}export function createCloseUiHandler(e){const{stateManager:t,logger:a,writeObs:r}=e;return async(n,i)=>{const s=i.id;if(!s)return{success:!1,error:"id is required"};const o=t.get(s),c=o?Date.now()-o.createdAt:0;t.delete(s);const l=e.getActiveSessionId?.(n);return l?(e.sendCanvasUpdate(l,{sessionUpdate:"canvas.reset",surfaceId:s}),r({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:s,lifetime_ms:c}}),a.debug?.(`[canvas] Closed surface=${s}`),{success:!0,surfaceId:s,message:"UI closed"}):(a.warn?.(`[canvas] No active session for toolCallId=${n}, close_ui local-only`),{success:!0,surfaceId:s,message:"UI closed (local only)"})}}export function buildCanvasActionSystemMessage(e){const t=e.userAction;return{type:"canvas_user_action",surfaceId:e.surfaceId,actionName:t?.name??"unknown",sourceComponentId:t?.sourceComponentId??"",context:t?.context??{}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.** The `render_ui` tool is the preferred and optimized way to render UI in the Canvas panel.\n\nRules:\n- When using render_ui, keep your chat reply to ONE short sentence. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Do NOT put large text blocks in render_ui — that belongs in chat.\n- Only use `grid` as a layout container. Keep nesting to grid > card/stat/image — max 1 level deep. Otherwise list components sequentially (they stack vertically).\n- Prefer fewer, larger components. One well-structured table is better than many small cards.\n- Canvas renders in a narrow sidebar (~400px). Use markdown headings (### Section) to organize sections.\n- Use grid + card for product showcases, dashboards, feature grids. Use list for compact item lists.\n\n**Surface IDs**: Use descriptive, human-readable IDs (e.g., "sales-report", "user-dashboard"). Never use generic IDs like "ui1" or random strings.\n\n**Update vs Create**: Same `id` updates existing surface; different `id` creates new one.\nWhen the user asks to modify, adjust, fix, or update existing canvas content, you MUST reuse the original surface id. Check [Canvas State] in the message for active surface ids and their content. Only create a new surface when showing genuinely new/different content.\n\n### Component Types\n\n**Layout**: grid (columns:1-6, gap:sm/md/lg, children:[...]) — responsive grid container for card/stat/image\n\n**Data Display**: card (image, title, subtitle, description, icon, badge, badgeVariant, footer, url, layout:vertical/horizontal, actions:[{label,actionId}]), table (columns, rows), stat (label, value, change, changeType, icon), key_value (items:[{key,value,copyable}]), list (items:[{title,subtitle,description,image,icon,badge,value,url}])\n\n**Chart**: chart (chartType, labels, datasets) — chartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge (value,max), candlestick, funnel (stages:[{label,value}]), sparkline\n\n**Text & Code**: text (content), markdown (content), code (language, code)\n\n**Media**: image (url, alt), web_embed (url, aspectRatio — embed ANY web page/app as iframe: YouTube, Maps, Figma, dashboards, docs, tools, etc.)\n\n**Feedback**: alert (variant:info/success/warning/error, title, message)\n\n**Interactive**: form (fields:[{name,label,inputType,options,placeholder,required}]), button (label, actionId), link (text, url, variant:default/button/subtle)\n\n**web_embed vs link**: Use `web_embed` when the URL should be displayed inline as embedded content. Use `link` only for plain navigation.\n\n### Composition Patterns\n- **Product showcase**: grid(columns:3) containing card(image, title, description, badge:"$29", actions:[{label:"Buy"}]) × N\n- **Dashboard KPIs**: grid(columns:3) containing stat × N\n- **Image gallery**: grid(columns:3) containing image × N\n- **Feature grid**: grid(columns:2) containing card(icon, title, description) × N\n- **Compact item list**: list(items:[{image, title, subtitle, value:"$29", badge:"New", url}])\n- **Article card**: card(image, title, description, url, layout:"horizontal")\n\n### render_form\nUse `render_form` for schema-driven forms when a domain schema exists.\nParameters: `domain`, `operation`, `data`, `entityId`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n';export const CANVAS_TERM_MAPPING="[System Note] Always use `render_ui` (not the canvas tool) for visual/structured content (charts, tables, stats, key-value, lists, cards, grids, alerts, forms, links, embeds, images, code, markdown). Use grid(columns, children:[card/stat/image]) for multi-column layouts like product showcases and dashboards. Card supports: image, title, subtitle, description, icon, badge, footer, url, actions, layout:horizontal/vertical. List items support: image, title, subtitle, description, badge, value, url. Charts: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline. Use web_embed for embedding URLs as iframes. Use link for navigation. Keep chat brief when canvas is used. Use `close_ui` to dismiss. Reuse same id to update; new id for new content.";export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e){const t=e.getAllActive();if(0===t.length)return null;const a=[...t].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),r=t.length-a.length;let n=`[Canvas State] Active surfaces: ${a.map(t=>{const a=e.summarize(t.surfaceId);if(!a)return`"${t.surfaceId}"`;const r=a.types.slice(0,3).join(", "),n=a.title?`: "${a.title}"`:"";return`"${t.surfaceId}" (${r}${n})`}).join("; ")}.`;return r>0&&(n+=` ...and ${r} more.`),n+=" To modify existing content, reuse its id in render_ui.",n.length>500&&(n=n.slice(0,497)+"..."),n}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const Errors={MethodNotFound:{rpcCode:-32601,code:"method_not_found",message:"Method not found"},InvalidParams:{rpcCode:-32602,code:"invalid_params",message:"Invalid params"},MissingParam:{rpcCode:-32602,code:"missing_param",message:"Missing required parameter"},InternalError:{rpcCode:-32603,code:"internal_error",message:"Internal error"},GatewayNotConnected:{rpcCode:-32001,code:"gateway_not_connected",message:"Gateway not connected"},GatewayDisconnected:{rpcCode:-32001,code:"gateway_disconnected",message:"Gateway disconnected"},SendFailed:{rpcCode:-32001,code:"send_failed",message:"Failed to send to gateway"},SessionNotFound:{rpcCode:-32602,code:"session_not_found",message:"Unknown session"},SessionDeleted:{rpcCode:-32001,code:"session_deleted",message:"Session deleted"},PromptConvertFailed:{rpcCode:-32602,code:"prompt_convert_failed",message:"Prompt conversion failed"},PromptTimeout:{rpcCode:-32022,code:"prompt_timeout",message:"Prompt timed out"},ShellDisabled:{rpcCode:-32010,code:"shell_disabled",message:"Shell is disabled"},TerminalNotFound:{rpcCode:-32011,code:"terminal_not_found",message:"Terminal not found"},TerminalClosed:{rpcCode:-32012,code:"terminal_closed",message:"Terminal already closed"},TerminalQuotaExceeded:{rpcCode:-32013,code:"terminal_quota_exceeded",message:"Terminal quota exceeded"},TerminalTimeout:{rpcCode:-32014,code:"terminal_timeout",message:"Terminal timeout"},AgentError:{rpcCode:-32021,code:"agent_error",message:"Agent error"},GeneralError:{rpcCode:-32e3,code:"general_error",message:"An error occurred"},Unauthorized:{rpcCode:0,code:"unauthorized",message:"Unauthorized"},InvalidBody:{rpcCode:0,code:"invalid_body",message:"Invalid JSON body"},MissingField:{rpcCode:0,code:"missing_field",message:"Required field missing"},FsError:{rpcCode:0,code:"fs_error",message:"Filesystem error"},DownloadFailed:{rpcCode:0,code:"download_failed",message:"Download failed"}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{Errors}from"./codes.js";export function acpError(e,r){return{code:e.rpcCode,message:r?.message??e.message,data:{code:e.code,...r?.param?{param:r.param}:{}}}}export function sendHttpError(e,r,o,a){e.statusCode=r,e.setHeader("Content-Type","application/json"),e.end(JSON.stringify({ok:!1,error:a?.message??o.message,errorCode:o.code,...a?.extra}))}
|
package/dist/file-stage.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createWriteStream}from"node:fs";import{mkdir,access,readdir,constants}from"node:fs/promises";import{join}from"node:path";import{pipeline}from"node:stream/promises";import{Readable}from"node:stream";const OPENCLAW_HOME=process.env.OPENCLAW_HOME||join(process.env.HOME||"/home/node",".openclaw"),UPLOAD_DIR=join(OPENCLAW_HOME,"workspace","uploads"),DOWNLOAD_TIMEOUT_MS=3e5;function classifyDownloadError(e
|
|
1
|
+
import{createWriteStream}from"node:fs";import{mkdir,access,readdir,constants}from"node:fs/promises";import{join}from"node:path";import{pipeline}from"node:stream/promises";import{Readable}from"node:stream";import{Errors,sendHttpError}from"./errors/index.js";const OPENCLAW_HOME=process.env.OPENCLAW_HOME||join(process.env.HOME||"/home/node",".openclaw"),UPLOAD_DIR=join(OPENCLAW_HOME,"workspace","uploads"),DOWNLOAD_TIMEOUT_MS=3e5;function classifyDownloadError(r,e){if(r instanceof Error&&"AbortError"===r.name)return{message:"download timed out",category:"timeout",retriable:!0};if(e)return 429===e||e>=500?{message:`HTTP ${e}`,category:"http_5xx",retriable:!0,httpStatus:e}:e>=400?{message:`HTTP ${e}`,category:"http_4xx",retriable:!1,httpStatus:e}:{message:`HTTP ${e}`,category:"invalid_response",retriable:!1,httpStatus:e};const t=r instanceof Error?r.message:String(r),o=r.code;return"ENOSPC"===o||"EACCES"===o||"EROFS"===o?{message:t,category:"fs_error",retriable:!1}:{message:t,category:"network_error",retriable:!0}}export async function handleFileStage(r,e,t){const o=r,a=e,s=getHeader(o,"x-beeos-token")||getHeader(o,"authorization")?.replace(/^Bearer\s+/i,""),n=process.env.BRIDGE_INTERNAL_SECRET;if(n&&s!==n)return void sendHttpError(a,401,Errors.Unauthorized);let i;try{i=await readJSONBody(o)}catch{return void sendHttpError(a,400,Errors.InvalidBody)}if(!i.fileId||!i.fileName||!i.presignedUrl)return void sendHttpError(a,400,Errors.MissingField,{message:"fileId, fileName, and presignedUrl are required"});const c=i.targetDir?join(OPENCLAW_HOME,"workspace",i.targetDir):UPLOAD_DIR,d=i.fileName.replace(/[/\\:*?"<>|]/g,"_"),l=`${i.fileId}_${d}`,f=join(c,l);try{return await access(f,constants.F_OK),void sendJSON(a,200,{ok:!0,localPath:f,cached:!0})}catch{const r=await findByFileIdPrefix(c,i.fileId);if(r)return void sendJSON(a,200,{ok:!0,localPath:r,cached:!0})}try{await mkdir(c,{recursive:!0})}catch(r){return void sendHttpError(a,500,Errors.FsError,{message:`Failed to create target directory: ${r instanceof Error?r.message:String(r)}`})}const u=new AbortController,p=setTimeout(()=>u.abort(),3e5);"object"==typeof p&&"unref"in p&&p.unref();try{const r=await fetch(i.presignedUrl,{signal:u.signal});if(!r.ok||!r.body){const e=classifyDownloadError(null,r.status);return void sendHttpError(a,502,Errors.DownloadFailed,{message:`Download failed: ${e.message}`,extra:{errorCategory:e.category,retriable:e.retriable,httpStatus:e.httpStatus}})}const e=createWriteStream(f);await pipeline(Readable.fromWeb(r.body),e)}catch(r){const e=classifyDownloadError(r);return void sendHttpError(a,502,Errors.DownloadFailed,{message:`Download failed: ${e.message}`,extra:{errorCategory:e.category,retriable:e.retriable}})}finally{clearTimeout(p)}sendJSON(a,200,{ok:!0,localPath:f,cached:!1})}function getHeader(r,e){const t=r.headers[e];return Array.isArray(t)?t[0]:t}function readJSONBody(r){return new Promise((e,t)=>{const o=[];r.on("data",r=>{o.push(Buffer.isBuffer(r)?r:Buffer.from(r))}),r.on("end",()=>{try{e(JSON.parse(Buffer.concat(o).toString("utf-8")))}catch(r){t(r)}}),r.on("error",t)})}function sendJSON(r,e,t){r.statusCode=e,r.setHeader("Content-Type","application/json"),r.end(JSON.stringify(t))}async function findByFileIdPrefix(r,e){try{const t=await readdir(r),o=`${e}_`;for(const e of t)if(e.startsWith(o)){const t=join(r,e);try{return await access(t,constants.R_OK),t}catch{continue}}}catch{}return null}
|
package/dist/gateway-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,generateKeyPairSync,createHash,sign as cryptoSign,createPrivateKey}from"node:crypto";import{readFileSync,writeFileSync,mkdirSync,existsSync}from"node:fs";import{join,dirname}from"node:path";import{homedir}from"node:os";const PROTOCOL_VERSION=3,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,AUTH_FAILURE_CLOSE_CODE=4001,CONNECT_DELAY_MS=750,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect";function base64urlEncode(e){return e.toString("base64url")}function verifyDeviceFingerprint(e){try{const t=Buffer.from(e.publicKey,"base64url");return createHash("sha256").update(t).digest("hex")===e.deviceId}catch{return!1}}function loadOrCreateDeviceIdentity(e){const t=e||join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw");mkdirSync(t,{recursive:!0});const n=join(t,"device-key.json");if(existsSync(n))try{const e=JSON.parse(readFileSync(n,"utf8"));if(verifyDeviceFingerprint(e))return{deviceId:e.deviceId,publicKeyB64url:e.publicKey,privateKeyDer:Buffer.from(e.privateKey,"base64")}}catch{}const{publicKey:i,privateKey:o}=generateKeyPairSync("ed25519"),r=i.export({type:"spki",format:"der"}).subarray(-32),s=o.export({type:"pkcs8",format:"der"}),c=createHash("sha256").update(r).digest("hex"),a=base64urlEncode(r),h={deviceId:c,publicKeyB64url:a,privateKeyDer:s};return writeFileSync(n,JSON.stringify({deviceId:c,publicKey:a,privateKey:s.toString("base64")},null,2)),h}function buildDeviceAuthPayload(e){return["v2",e.deviceId,e.clientId,e.clientMode,e.role,e.scopes.join(","),String(e.signedAtMs),e.token,e.nonce].join("|")}function signDeviceAuth(e,t){const n=createPrivateKey({key:e.privateKeyDer,format:"der",type:"pkcs8"});return base64urlEncode(cryptoSign(null,Buffer.from(t),n))}function ensureDevicePaired(e){const t=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"devices","paired.json");let n={};try{n=JSON.parse(readFileSync(t,"utf8"))}catch{}if(n[e.deviceId])return;const i=Date.now(),o=base64urlEncode(Buffer.from(randomUUID().replace(/-/g,""),"hex"));n[e.deviceId]={deviceId:e.deviceId,publicKey:e.publicKeyB64url,clientId:"cli",clientMode:"cli",role:"operator",roles:["operator"],scopes:["operator.admin","operator.approvals","operator.pairing"],tokens:{operator:{token:o,role:"operator",scopes:["operator.admin","operator.approvals","operator.pairing"],createdAtMs:i}},createdAtMs:i,approvedAtMs:i},mkdirSync(dirname(t),{recursive:!0}),writeFileSync(t,JSON.stringify(n,null,2))}const MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch{}}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const i=new WebSocket(this.config.url);this.ws=i;let o=null;const r=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),i.close(),t(new Error("Gateway connection timeout"))},3e4),s=t=>{clearTimeout(r),this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},c=e=>{clearTimeout(r),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:e,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:e}),i.close(),t(new Error(e))};i.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),i.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");if("pong"===t)return void(this.lastPongTs=Date.now());let i;this.emitMessageHash(t);try{i=JSON.parse(t)}catch{return}const r=i.method;r&&!i.id&&(this.isReconnectNotification(r)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",r,i.params));const a=i.type;if("event"===a&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===a){const e=i.event,t=i.payload;if("connect.challenge"===e){this.emit("transport",{event:"handshake.challenge_recv",durationMs:Date.now()-n});const e=t?.nonce;setTimeout(()=>{o=this._sendConnect(e),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n})},750)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):this.emit("gateway-event",e,t)}else if("hello-ok"===a)s(i);else if("hello-error"===a||"connect-error"===a)c("Authentication rejected by Gateway");else if("res"===a){const e=i.id;if(e&&e===o){if(i.ok){const e=i.payload;s(e??i)}else{const e=i.error;c(e?.message||"Gateway connect rejected")}return}const t=this.pending.get(e);if(t)if(this.pending.delete(e),clearTimeout(t.timer),i.ok)t.resolve(i.payload);else{const e=i.error;t.reject(new Error(e?.message||"Gateway request failed"))}}}),i.on("pong",()=>{this.lastPongTs=Date.now()}),i.on("close",e=>{this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),i.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.connected||(clearTimeout(r),t(e)),this.emit("error",e)})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);const t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs);this.reconnectAttempt++,this.emit("reconnecting",{attempt:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))}))}_sendConnect(e){const t=Date.now(),n="operator",i=["operator.admin","operator.approvals","operator.pairing"],o={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:t};if(e){const r=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:n,scopes:i,signedAtMs:t,token:this.config.token,nonce:e});o.nonce=e,o.signature=signDeviceAuth(this.deviceIdentity,r)}const r=randomUUID(),s={type:"req",id:r,method:"connect",params:{minProtocol:3,maxProtocol:3,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:n,scopes:i,auth:{token:this.config.token},device:o}};return this.ws?.send(JSON.stringify(s)),r}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const i=randomUUID(),o={type:"req",id:i,method:e,params:t};return new Promise((t,r)=>{const s=setTimeout(()=>{this.pending.delete(i),r(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(i,{resolve:t,reject:r,timer:s}),this.ws.send(JSON.stringify(o))})}async agentSend(e,t,n,i){const o={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};i?.length&&(o.attachments=i);return await this.request("agent",o,12e4)}async chatSend(e,t,n){const i={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(i.attachments=n);return await this.request("chat.send",i,12e4)}async chatAbort(e,t){await this.request("chat.abort",{sessionKey:e,runId:t})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async skillsStatus(){return this.request("skills.status",{})}async sessionsList(){return this.request("sessions.list",{})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{sessionKey:e,...t})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
|
|
1
|
+
import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,generateKeyPairSync,createHash,sign as cryptoSign,createPrivateKey}from"node:crypto";import{readFileSync,writeFileSync,mkdirSync,existsSync}from"node:fs";import{join,dirname}from"node:path";import{homedir}from"node:os";const PROTOCOL_VERSION=3,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,AUTH_FAILURE_CLOSE_CODE=4001,CONNECT_DELAY_MS=750,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect";function base64urlEncode(e){return e.toString("base64url")}function verifyDeviceFingerprint(e){try{const t=Buffer.from(e.publicKey,"base64url");return createHash("sha256").update(t).digest("hex")===e.deviceId}catch{return!1}}function loadOrCreateDeviceIdentity(e){const t=e||join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw");mkdirSync(t,{recursive:!0});const n=join(t,"device-key.json");if(existsSync(n))try{const e=JSON.parse(readFileSync(n,"utf8"));if(verifyDeviceFingerprint(e))return{deviceId:e.deviceId,publicKeyB64url:e.publicKey,privateKeyDer:Buffer.from(e.privateKey,"base64")}}catch{}const{publicKey:i,privateKey:r}=generateKeyPairSync("ed25519"),o=i.export({type:"spki",format:"der"}).subarray(-32),s=r.export({type:"pkcs8",format:"der"}),c=createHash("sha256").update(o).digest("hex"),a=base64urlEncode(o),h={deviceId:c,publicKeyB64url:a,privateKeyDer:s};return writeFileSync(n,JSON.stringify({deviceId:c,publicKey:a,privateKey:s.toString("base64")},null,2)),h}function buildDeviceAuthPayload(e){return["v2",e.deviceId,e.clientId,e.clientMode,e.role,e.scopes.join(","),String(e.signedAtMs),e.token,e.nonce].join("|")}function signDeviceAuth(e,t){const n=createPrivateKey({key:e.privateKeyDer,format:"der",type:"pkcs8"});return base64urlEncode(cryptoSign(null,Buffer.from(t),n))}function ensureDevicePaired(e){const t=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"devices","paired.json");let n={};try{n=JSON.parse(readFileSync(t,"utf8"))}catch{}if(n[e.deviceId])return;const i=Date.now(),r=base64urlEncode(Buffer.from(randomUUID().replace(/-/g,""),"hex"));n[e.deviceId]={deviceId:e.deviceId,publicKey:e.publicKeyB64url,clientId:"cli",clientMode:"cli",role:"operator",roles:["operator"],scopes:["operator.admin","operator.approvals","operator.pairing"],tokens:{operator:{token:r,role:"operator",scopes:["operator.admin","operator.approvals","operator.pairing"],createdAtMs:i}},createdAtMs:i,approvedAtMs:i},mkdirSync(dirname(t),{recursive:!0}),writeFileSync(t,JSON.stringify(n,null,2))}const MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch{}}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const i=new WebSocket(this.config.url);this.ws=i;let r=null;const o=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),i.close(),t(new Error("Gateway connection timeout"))},3e4),s=t=>{clearTimeout(o),this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},c=e=>{clearTimeout(o),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:e,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:e}),i.close(),t(new Error(e))};i.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),i.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");if("pong"===t)return void(this.lastPongTs=Date.now());let i;this.emitMessageHash(t);try{i=JSON.parse(t)}catch{return}const o=i.method;o&&!i.id&&(this.isReconnectNotification(o)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",o,i.params));const a=i.type;if("event"===a&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===a){const e=i.event,t=i.payload;if("connect.challenge"===e){this.emit("transport",{event:"handshake.challenge_recv",durationMs:Date.now()-n});const e=t?.nonce;setTimeout(()=>{r=this._sendConnect(e),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n})},750)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):this.emit("gateway-event",e,t)}else if("hello-ok"===a)s(i);else if("hello-error"===a||"connect-error"===a)c("Authentication rejected by Gateway");else if("res"===a){const e=i.id;if(e&&e===r){if(i.ok){const e=i.payload;s(e??i)}else{const e=i.error;c(e?.message||"Gateway connect rejected")}return}const t=this.pending.get(e);if(t)if(this.pending.delete(e),clearTimeout(t.timer),i.ok)t.resolve(i.payload);else{const e=i.error;t.reject(new Error(e?.message||"Gateway request failed"))}}}),i.on("pong",()=>{this.lastPongTs=Date.now()}),i.on("close",e=>{this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),i.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.connected||(clearTimeout(o),t(e)),this.emit("error",e)})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);const t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs);this.reconnectAttempt++,this.emit("reconnecting",{attempt:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))}))}_sendConnect(e){const t=Date.now(),n="operator",i=["operator.admin","operator.approvals","operator.pairing"],r={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:t};if(e){const o=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:n,scopes:i,signedAtMs:t,token:this.config.token,nonce:e});r.nonce=e,r.signature=signDeviceAuth(this.deviceIdentity,o)}const o=randomUUID(),s={type:"req",id:o,method:"connect",params:{minProtocol:3,maxProtocol:3,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:n,scopes:i,auth:{token:this.config.token},device:r}};return this.ws?.send(JSON.stringify(s)),o}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const i=randomUUID(),r={type:"req",id:i,method:e,params:t};return new Promise((t,o)=>{const s=setTimeout(()=>{this.pending.delete(i),o(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(i,{resolve:t,reject:o,timer:s}),this.ws.send(JSON.stringify(r))})}async agentSend(e,t,n,i){const r={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};i?.length&&(r.attachments=i);return await this.request("agent",r,12e4)}async chatSend(e,t,n){const i={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(i.attachments=n);return await this.request("chat.send",i,12e4)}async chatAbort(e,t){await this.request("chat.abort",{sessionKey:e,runId:t})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async skillsStatus(){return this.request("skills.status",{})}async sessionsList(){return this.request("sessions.list",{includeDerivedTitles:!0})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{key:e,...t})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{TERMINAL_UPDATE_METHOD,parseTerminalOpenParams,parseTerminalInputParams,parseTerminalResizeParams,parseTerminalCloseParams,parseTerminalWaitParams}from"./terminal-rpc.js";import{Errors}from"./errors/index.js";export class TerminalBridgeHandler{terminalMgr;transport;config;logger;writeObs;interactiveTerminals=new Map;HEARTBEAT_TIMEOUT_MS=6e4;constructor(e){this.terminalMgr=e.terminalMgr,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs}setupListeners(){this.terminalMgr.addListeners({onOutput:e=>{this.interactiveTerminals.has(e.terminalId)&&this.transport.sendNotification(TERMINAL_UPDATE_METHOD,{terminalId:e.terminalId,event:"stdout",dataBase64:e.data.toString("base64"),seq:e.seq})},onExit:e=>{const r=this.interactiveTerminals.get(e.terminalId);r&&(clearTimeout(r.heartbeatTimer),this.interactiveTerminals.delete(e.terminalId),this.transport.sendNotification(TERMINAL_UPDATE_METHOD,{terminalId:e.terminalId,event:"exit",code:e.code,signal:e.signal,...e.reason?{reason:e.reason}:{}}))},onStateChange:e=>{this.interactiveTerminals.has(e.terminalId)&&this.transport.sendNotification(TERMINAL_UPDATE_METHOD,{terminalId:e.terminalId,event:"state",from:e.from,to:e.to})}})}async handleRequest(e){switch(e.method){case"terminal/open":await this.handleTerminalOpen(e);break;case"terminal/input":this.handleTerminalInput(e);break;case"terminal/resize":this.handleTerminalResize(e);break;case"terminal/close":await this.handleTerminalClose(e);break;case"terminal/create":await this.handleTerminalCreate(e);break;case"terminal/wait":await this.handleTerminalWait(e);break;case"terminal/release":await this.handleTerminalRelease(e);break;case"terminal/kill":await this.handleTerminalKill(e);break;default:this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}handleNotification(e,r){switch(e){case"terminal/input":this.handleTerminalInputNotification(r);break;case"terminal/resize":this.handleTerminalResizeNotification(r);break;default:this.logger.debug?.(`[terminal-handler] Unknown notification: ${e}`)}}handleTerminalInputNotification(e){if(!this.config.bridge.shell.enabled)return;const r=parseTerminalInputParams(e);if(!r.ok)return;let a;if(r.value.dataBase64)try{a=Buffer.from(r.value.dataBase64,"base64")}catch{return}else a=Buffer.from(r.value.data??"","utf8");try{this.terminalMgr.writeToSession(r.value.terminalId,a),this.refreshTerminalHeartbeat(r.value.terminalId)}catch{}}handleTerminalResizeNotification(e){if(!this.config.bridge.shell.enabled)return;const r=parseTerminalResizeParams(e);if(r.ok)try{this.terminalMgr.resizeSession(r.value.terminalId,r.value.cols,r.value.rows),this.refreshTerminalHeartbeat(r.value.terminalId)}catch{}}async handleTerminalCreate(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalOpenParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);this.terminalMgr.sessionCount>=this.terminalMgr.maxConcurrentLimit&&this.logger.warn?.("[terminal-handler] Terminal quota exceeded, evicting oldest");const a=this.terminalMgr.create(r.value.command);this.writeObs({component:"terminal-handler",domain:"terminal",name:"terminal.created",severity:"info",payload:{terminalId:a.id,command:a.command,activeSessions:this.terminalMgr.sessionCount}}),this.transport.sendResult(e.id,{terminalId:a.id,command:a.command})}async handleTerminalWait(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalWaitParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const a=this.terminalMgr.get(r.value.terminalId);a?this.transport.sendResult(e.id,{terminalId:r.value.terminalId,exitCode:a.exitCode,output:a.output.join(""),state:a.state}):this.transport.sendAcpError(e.id,Errors.TerminalNotFound)}async handleTerminalRelease(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);this.terminalMgr.get(r.value.terminalId)?(this.terminalMgr.release(r.value.terminalId),this.transport.sendResult(e.id,{})):this.transport.sendAcpError(e.id,Errors.TerminalNotFound)}async handleTerminalKill(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const a=this.terminalMgr.get(r.value.terminalId);a?"closed"!==a.state?(this.terminalMgr.kill(r.value.terminalId,r.value.signal),this.transport.sendResult(e.id,{})):this.transport.sendAcpError(e.id,Errors.TerminalClosed):this.transport.sendAcpError(e.id,Errors.TerminalNotFound)}async handleTerminalOpen(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalOpenParams(e.params);if(r.ok)try{const a=this.terminalMgr.openSession({sessionId:r.value.sessionId,command:r.value.command,cols:r.value.cols,rows:r.value.rows,cwd:r.value.cwd}),t=setTimeout(()=>{this.interactiveTerminals.delete(a.terminalId),this.terminalMgr.closeSession(a.terminalId,"heartbeat_timeout")},this.HEARTBEAT_TIMEOUT_MS);"object"==typeof t&&"unref"in t&&t.unref(),this.interactiveTerminals.set(a.terminalId,{heartbeatTimer:t}),this.writeObs({component:"terminal-handler",domain:"terminal",name:"terminal.opened",severity:"info",payload:{terminalId:a.terminalId,shell:a.shell,cwd:a.cwd,activeSessions:this.terminalMgr.sessionCount}}),this.transport.sendResult(e.id,{terminalId:a.terminalId,command:a.shell,shell:a.shell,cwd:a.cwd,cols:a.cols,rows:a.rows,state:a.state})}catch(r){const a=r instanceof Error?r.message:String(r);a.includes("quota")?this.transport.sendAcpError(e.id,Errors.TerminalQuotaExceeded,{message:a}):this.transport.sendAcpError(e.id,Errors.GeneralError,{message:a})}else this.sendTerminalParseError(e.id,r)}handleTerminalInput(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalInputParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);let a;if(r.value.dataBase64)try{a=Buffer.from(r.value.dataBase64,"base64")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"invalid dataBase64"})}else a=Buffer.from(r.value.data??"","utf8");try{this.terminalMgr.writeToSession(r.value.terminalId,a),this.refreshTerminalHeartbeat(r.value.terminalId),this.transport.sendResult(e.id,{})}catch(r){const a=r instanceof Error?r.message:String(r);this.transport.sendAcpError(e.id,Errors.TerminalNotFound,{message:a})}}handleTerminalResize(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalResizeParams(e.params);if(r.ok)try{this.terminalMgr.resizeSession(r.value.terminalId,r.value.cols,r.value.rows),this.refreshTerminalHeartbeat(r.value.terminalId),this.transport.sendResult(e.id,{})}catch(r){const a=r instanceof Error?r.message:String(r);this.transport.sendAcpError(e.id,Errors.TerminalNotFound,{message:a})}else this.sendTerminalParseError(e.id,r)}async handleTerminalClose(e){if(!this.config.bridge.shell.enabled)return void this.transport.sendAcpError(e.id,Errors.ShellDisabled);const r=parseTerminalCloseParams(e.params);if(!r.ok)return void this.sendTerminalParseError(e.id,r);const a=this.interactiveTerminals.get(r.value.terminalId);a&&(clearTimeout(a.heartbeatTimer),this.interactiveTerminals.delete(r.value.terminalId)),this.terminalMgr.closeSession(r.value.terminalId),this.transport.sendResult(e.id,{})}refreshTerminalHeartbeat(e){const r=this.interactiveTerminals.get(e);r&&(clearTimeout(r.heartbeatTimer),r.heartbeatTimer=setTimeout(()=>{this.logger.warn?.(`[terminal-handler] Terminal heartbeat timeout terminalId=${e}`),this.interactiveTerminals.delete(e),this.terminalMgr.closeSession(e,"heartbeat_timeout")},this.HEARTBEAT_TIMEOUT_MS),"object"==typeof r.heartbeatTimer&&"unref"in r.heartbeatTimer&&r.heartbeatTimer.unref())}sendTerminalParseError(e,r){this.transport.sendAcpError(e,Errors.InvalidParams,{message:r.error.message})}destroy(){for(const e of this.interactiveTerminals.values())clearTimeout(e.heartbeatTimer);this.interactiveTerminals.clear()}}
|
package/dist/terminal-rpc.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const TERMINAL_METHODS={open:"terminal/open",input:"terminal/input",resize:"terminal/resize",close:"terminal/close",create:"terminal/create",wait:"terminal/wait",release:"terminal/release",kill:"terminal/kill"};export const TERMINAL_UPDATE_METHOD="terminal/update";export const TERMINAL_ERROR_CODES={shellDisabled:-32010,terminalNotFound:-32011,terminalClosed:-32012,terminalQuotaExceeded:-32013,terminalTimeout:-32014};export const TERMINAL_INVALID_PARAMS={code:-32602,message:"invalid params"};function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function readString(e,
|
|
1
|
+
import{Errors}from"./errors/index.js";export const TERMINAL_METHODS={open:"terminal/open",input:"terminal/input",resize:"terminal/resize",close:"terminal/close",create:"terminal/create",wait:"terminal/wait",release:"terminal/release",kill:"terminal/kill"};export const TERMINAL_UPDATE_METHOD="terminal/update";export const TERMINAL_ERROR_CODES={shellDisabled:-32010,terminalNotFound:-32011,terminalClosed:-32012,terminalQuotaExceeded:-32013,terminalTimeout:-32014};export const TERMINAL_INVALID_PARAMS={code:-32602,message:"invalid params"};function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function readString(e,r){const t=e[r];if("string"==typeof t){const e=t.trim();return e.length>0?e:void 0}}function readPositiveInt(e,r){const t=e[r];if("number"==typeof t&&Number.isInteger(t)&&t>0)return t}function invalid(e,r){return{ok:!1,error:{code:Errors.InvalidParams.rpcCode,message:Errors.InvalidParams.message,data:{field:e,reason:r}}}}function parseBase(e){return isRecord(e)?{ok:!0,value:e}:invalid("params","must be an object")}export function parseTerminalOpenParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readPositiveInt(t,"cols")??80,a=readPositiveInt(t,"rows")??24,i=readString(t,"sessionId"),s=readString(t,"command"),o=readString(t,"cwd");return{ok:!0,value:{sessionId:i,command:s,cols:n,rows:a,...o?{cwd:o}:{}}}}export function parseTerminalInputParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(t,"data"),i=readString(t,"dataBase64");return a||i?{ok:!0,value:{terminalId:n,data:a,dataBase64:i}}:invalid("data","data or dataBase64 must be provided")}export function parseTerminalResizeParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(t,"cols");if(!a)return invalid("cols","must be a positive integer");const i=readPositiveInt(t,"rows");return i?{ok:!0,value:{terminalId:n,cols:a,rows:i}}:invalid("rows","must be a positive integer")}export function parseTerminalCloseParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readString(t,"signal");return{ok:!0,value:{terminalId:n,...a?{signal:a}:{}}}}export function parseTerminalWaitParams(e){const r=parseBase(e);if(!r.ok)return r;const t=r.value,n=readString(t,"terminalId");if(!n)return invalid("terminalId","must be a non-empty string");const a=readPositiveInt(t,"timeoutMs");return{ok:!0,value:{terminalId:n,...a?{timeoutMs:a}:{}}}}export function isTerminalRpcMethod(e){return Object.values(TERMINAL_METHODS).includes(e)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BeeOS <dev@beeos.ai>",
|