beeos-claw 0.1.12 → 0.1.13

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.
@@ -0,0 +1 @@
1
+ export function createA2ATaskCancelHandler(a,t){return async e=>{let n=e.payload;"a2a_task_cancel"===n?.type&&n?.payload&&"object"==typeof n.payload&&(n=n.payload);const o="string"==typeof n?.task_id?n.task_id:void 0,s="string"==typeof n?.reason?n.reason:"unknown";if(!o)return{ok:!1,error:"missing task_id"};const r=t(o),c="timeout"===s?`A2A task ${o} was terminated — the caller's timeout expired.`:`A2A task ${o} was cancelled by the caller.`;try{await a(`[A2A Task Cancelled]\n\n${c}\n\nYou may stop working on this task.`,void 0,void 0,{kind:"internal_system",sourceTool:"a2a_task_cancel"})}catch(a){}return{ok:!0,cancelled:r}}}
@@ -0,0 +1 @@
1
+ export function parseMentionsParam(e){return Array.isArray(e)?e.filter(e=>"object"==typeof e&&null!==e&&"string"==typeof e.agentId&&"string"==typeof e.name):[]}export function buildMentionInstruction(e){if(!e.length)return"";return["[A2A Delegation Request]","The user explicitly @mentioned the following agent(s). You MUST delegate to them using beeos_call_agent.",...e.map(e=>`- Agent "${e.name}" → agentId: "${e.agentId}"`),"IMPORTANT: Always pass the exact agentId (UUID) shown above, NOT the agent name.","Pass the user's message (excluding the @mention text) as the task message.",""].join("\n")}export function resolveAgentIdFromMentions(e,t){if(!e.length)return;const n=t.toLowerCase(),a=e.find(e=>e.name.toLowerCase()===n);return a?.agentId}
@@ -0,0 +1 @@
1
+ import{a2aFetch}from"./tools.js";import{dispatchA2ATask}from"./task-executor.js";export async function pollPendingTasks(a,e,t){let s=0;try{const o=await a2aFetch(a,"/api/v1/a2a/tasks/pending");if(!o.ok)return a.logger.warn?.("[beeos-claw] Failed to poll pending tasks: %d %s",o.status,await o.text()),0;const n=(await o.json()).data??[];if(0===n.length)return a.logger.debug?.("[beeos-claw] No pending A2A tasks found on startup"),0;a.logger.info?.("[beeos-claw] Found %d pending A2A tasks to process",n.length);for(const o of n)try{await dispatchA2ATask(a,e,t,o.id,o.message,o.caller_principal_id,o.files),s++,a.logger.info?.("[beeos-claw] Dispatched pending task %s",o.id)}catch(e){a.logger.error?.("[beeos-claw] Failed to dispatch pending task %s:",o.id,e)}}catch(e){a.logger.warn?.("[beeos-claw] Pending task poll failed:",e)}return s}
@@ -0,0 +1 @@
1
+ function extractResultText(t){if(!t)return"";if("string"==typeof t)return t;if("object"==typeof t){const e=t;if("string"==typeof e.text)return e.text;if("object"==typeof e.data&&e.data)try{return JSON.stringify(e.data,null,2)}catch{}if("string"==typeof e.result)return e.result;const a=Object.keys(e);if(1===a.length&&"status"===a[0])return"";try{return JSON.stringify(e,null,2)}catch{}}return String(t)}export function createA2AResultHandler(t,e,a){return async n=>{let s=n.payload;"a2a_result"===s?.type&&s?.payload&&"object"==typeof s.payload&&(s=s.payload);const r="string"==typeof s?.task_id?s.task_id:void 0,i="string"==typeof s?.status?s.status:void 0,o=s?.result,l="string"==typeof s?.context_id?s.context_id:void 0,u="string"==typeof s?.target_name?s.target_name:void 0;if(!r)return{ok:!1,error:"missing task_id"};let c,d,y=e?.resolveSessionKey(r);if(!y&&l?.startsWith("session:")&&(y=l.slice(8)),c="completed"===i?`A2A task ${r} completed successfully.`:"failed"===i?`A2A task ${r} failed.`:"cancelled"===i?`A2A task ${r} was cancelled.`:"timeout"===i?`A2A task ${r} timed out — the target agent did not complete in time.`:`A2A task ${r} finished with status: ${i??"unknown"}.`,a){a({type:"a2a_result_notification",taskId:r,callerName:u??r,message:c,targetSessionKey:y})}try{d="string"==typeof o?o:JSON.stringify(o,null,2)}catch{d=String(o)}const f=extractResultText(o),p=`[A2A Result]\n\n${c}\n\nResult:\n${f||d}${f.length>20?"\n\n[INSTRUCTION] The task returned data. You MUST call render_ui to display this data visually on the canvas as a card or table. Do NOT repeat the data in chat text — put it all in render_ui. Keep your chat reply to one brief sentence.":""}`;try{await t(p,void 0,y,{kind:"internal_system",sourceTool:"a2a_result"})}catch(t){}return{ok:!0}}}
@@ -0,0 +1 @@
1
+ import{a2aFetch}from"./tools.js";export function extractTextResult(t){if("string"==typeof t)return t;if(t&&"object"==typeof t){const e=t;if("string"==typeof e.content)return e.content;if("string"==typeof e.text)return e.text;if("string"==typeof e.message)return e.message}try{return JSON.stringify(t)}catch{return String(t)}}export async function completeA2ATask(t,e,n,o){await a2aFetch(t,`/api/v1/a2a/tasks/${encodeURIComponent(e)}/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({result:o?"":n,error:o})})}export async function dispatchA2ATask(t,e,n,o,r,a,s){try{const c=`[A2A Task #${o}${a?` from ${a}`:""}]\n\n${r}`,i=await e(c,s),p=i?.runId;p?n(p,o,(e,n)=>completeA2ATask(t,o,e,n)):t.logger.warn?.("[beeos-claw] agentSend returned no runId for A2A task %s, task will rely on manual completion or timeout",o)}catch(e){const n=e instanceof Error?e.message:String(e);t.logger.warn?.("[beeos-claw] Failed to dispatch A2A task %s: %s",o,n);try{await completeA2ATask(t,o,"",n)}catch{}}}
@@ -0,0 +1 @@
1
+ const MAX_HANDLES=200;export class TaskHandleRegistry{handles=new Map;counter=0;create(e,t,s,n){const r=`bth_${(++this.counter).toString(36)}`;return this.handles.set(r,{taskId:e,targetAgentId:t,contextId:s,gatewaySessionKey:n,createdAt:Date.now()}),this.evict(),r}resolve(e){if(this.handles.has(e))return this.handles.get(e);for(const t of this.handles.values())if(t.taskId===e)return t}resolveTaskId(e){const t=this.resolve(e);return t?.taskId??e}resolveSessionKey(e){const t=this.resolve(e);return t?.gatewaySessionKey}evict(){if(this.handles.size<=200)return;const e=[...this.handles.entries()].sort((e,t)=>e[1].createdAt-t[1].createdAt),t=e.length-200;for(let s=0;s<t;s++)this.handles.delete(e[s][0])}}
@@ -0,0 +1 @@
1
+ import{dispatchA2ATask}from"./task-executor.js";export function createA2ATaskHandler(e,a,s,t){return async r=>{const i=r.payload,n="string"==typeof i?.message?i.message:void 0;if(!n)return{ok:!1,error:"missing message"};const o=Array.isArray(i?.files)?i.files:void 0,c="string"==typeof i?.task_id?i.task_id:void 0,l="string"==typeof i?.caller?i.caller:void 0,f="string"==typeof i?.caller_name?i.caller_name:l??"Unknown Agent";if(c&&t){const e=n.length>100?n.slice(0,100)+"...":n;t({type:"a2a_task_notification",taskId:c,callerName:f,message:`Received A2A task from ${f}: ${e}`})}if(!c||!a||!s){return e(`[A2A Task${c?` #${c}`:""}${l?` from ${l}`:""}]\n\n${n}`,o)}return await dispatchA2ATask(a,e,s,c,n,l,o),{ok:!0,taskId:c}}}
@@ -0,0 +1 @@
1
+ import{agentFetch}from"../agent-auth.js";import{TaskHandleRegistry}from"./task-handle.js";import{resolveAgentIdFromMentions}from"./mention-resolver.js";async function a2aFetch(e,t,a){const n=`${e.platformUrl}${t}`;return e.agentKeys?agentFetch(n,e.agentKeys,a):fetch(n,a)}export{a2aFetch};const defaultRegistry=new TaskHandleRegistry;export function registerA2ATools(e,t,a=defaultRegistry,n,r,s){e.registerTool({name:"beeos_discover_agents",description:"Find other agents that can help with a task. Returns a list of agents matching the query. Results include your own agents and publicly available agents.",parameters:{type:"object",properties:{query:{type:"string",description:"What capability or skill you are looking for"}},required:["query"]},async execute(e,a){const{query:n}=a;try{const e=await a2aFetch(t,`/api/v1/a2a/discover?query=${encodeURIComponent(n)}&limit=20`);if(!e.ok)return{content:JSON.stringify({error:`discover failed: ${e.status}`})};const a=await e.json();return{content:JSON.stringify(a,null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}}),e.registerTool({name:"beeos_call_agent",description:"Ask another agent to perform a task. The task is sent asynchronously — returns a task ID immediately, and the result will be delivered via a2a_result message on your personal channel when ready.",parameters:{type:"object",properties:{agentId:{type:"string",description:"The target agent UUID (e.g. d04080d1-...) or display name. Prefer the UUID from discover results or @mention metadata when available."},message:{type:"string",description:"The task description or message to send to the agent"},files:{type:"array",items:{type:"string"},description:"Optional file URLs to include with the request"},contextId:{type:"string",description:"Optional context ID for multi-turn conversations with the same agent"},timeoutMs:{type:"number",description:"Task timeout in milliseconds. Defaults to 600000 (10 minutes). Set higher for long-running tasks."}},required:["agentId","message"]},async execute(e,o){let{agentId:i,message:c,files:l,contextId:d,timeoutMs:g}=o;const u=n?.(e),p=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;if(!p.test(i)){const a=r?.(e);if(a?.length){const e=resolveAgentIdFromMentions(a,i);e&&(t.logger.info?.(`beeos_call_agent: resolved "${i}" → ${e} via mention context`),i=e)}if(!p.test(i))try{const e=await a2aFetch(t,`/api/v1/a2a/discover?query=${encodeURIComponent(i)}&limit=10`);if(e.ok){const a=await e.json(),n=a?.data??[],r=i.toLowerCase(),s=n.find(e=>e.name.toLowerCase()===r)??n.find(e=>e.name.toLowerCase().includes(r));if(!s)return{content:JSON.stringify({error:`Could not resolve agent name "${i}". Available agents: ${n.map(e=>`${e.name} (${e.id})`).join(", ")||"none"}`})};t.logger.info?.(`beeos_call_agent: resolved "${i}" → ${s.id} (${s.name}) via discover`),i=s.id}}catch{}}const y=d||(u?`session:${u}`:"");try{const e=await a2aFetch(t,"/api/v1/a2a/tasks/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({target_agent_id:i,message:c,files:l??[],async:!0,context_id:y,timeout_ms:g&&g>0?g:6e5})});if(!e.ok){const t=await e.text();if(401===e.status||403===e.status){let e={};try{e=JSON.parse(t)}catch{}(e.external_auth_required||e.auth_required)&&s&&s("_beeos/auth_required",{type:"auth_required",target_agent_id:i,target_agent_name:e.agent_name||i,title:"Authentication Required",content:`Agent **${e.agent_name||i}** requires authentication. Please configure credentials in your contacts.`})}return{content:JSON.stringify({error:`send task failed: ${e.status} ${t}`})}}const n=await e.json(),r=n?.data?.id;let o;return r&&(o=a.create(r,i,d,u)),{content:JSON.stringify({...n,task_handle:o},null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}}),e.registerTool({name:"beeos_complete_task",description:"Complete an A2A task and send the result back to the caller agent. Use this when you have finished processing a task received from another agent.",parameters:{type:"object",properties:{taskId:{type:"string",description:"The task ID to complete"},result:{type:"string",description:"The result text to send back to the caller"},error:{type:"string",description:"If the task failed, provide an error message instead of result"}},required:["taskId"]},async execute(e,a){const{taskId:n,result:r,error:s}=a;try{const e=await a2aFetch(t,`/api/v1/a2a/tasks/${encodeURIComponent(n)}/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({result:r??"",error:s??""})});if(!e.ok){const t=await e.text();return{content:JSON.stringify({error:`complete task failed: ${e.status} ${t}`})}}const a=await e.json();return{content:JSON.stringify(a,null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}}),e.registerTool({name:"beeos_task_status",description:"Check the current status and result of an A2A task you previously sent. Accepts either a task_handle (e.g. bth_1) or a raw task ID.",parameters:{type:"object",properties:{taskId:{type:"string",description:"The task handle or task ID to check"}},required:["taskId"]},async execute(e,n){const{taskId:r}=n,s=a.resolveTaskId(r);try{const e=await a2aFetch(t,`/api/v1/a2a/tasks/${encodeURIComponent(s)}`);if(!e.ok)return{content:JSON.stringify({error:`get task failed: ${e.status}`})};const a=await e.json();return{content:JSON.stringify(a,null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}}),e.registerTool({name:"beeos_task_cancel",description:"Cancel an A2A task you previously sent. Only the caller can cancel a task. Accepts either a task_handle (e.g. bth_1) or a raw task ID.",parameters:{type:"object",properties:{taskId:{type:"string",description:"The task handle or task ID to cancel"}},required:["taskId"]},async execute(e,n){const{taskId:r}=n,s=a.resolveTaskId(r);try{const e=await a2aFetch(t,`/api/v1/a2a/tasks/${encodeURIComponent(s)}/cancel`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})});if(!e.ok){const t=await e.text();return{content:JSON.stringify({error:`cancel task failed: ${e.status} ${t}`})}}const a=await e.json();return{content:JSON.stringify(a,null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}})}
@@ -0,0 +1 @@
1
+ export function sv(e){if(null==e)return{literalString:""};if("string"==typeof e)return{literalString:e};if("number"==typeof e||"boolean"==typeof e)return{literalString: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{literalString:String(t[e])};try{return{literalString:JSON.stringify(e)}}catch{return{literalString:"[Object]"}}}return{literalString:String(e)}}const A2UI_VERSION="0.8";export function makeBeginRendering(e,t){return{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:e,message:{beginRendering:{surfaceId:e,root:t}}}}export function makeSurfaceUpdate(e,t,n){return{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:e,presentation:n,message:{surfaceUpdate:{surfaceId:e,components:t}}}}export function makeDeleteSurface(e){return{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:e,message:{deleteSurface:{surfaceId:e}}}}export function findRootIds(e){const t=new Set;for(const n of e){const e=Object.values(n.component)[0];if(!e)continue;"string"==typeof e.child&&t.add(e.child);const r=e.children;if(r?.explicitList)for(const e of r.explicitList)t.add(e);const i=e.tabItems;if(i)for(const e of i)e.child&&t.add(e.child)}return e.filter(e=>!t.has(e.id)).map(e=>e.id)}let _idCounter=0;export function nextComponentId(e){return`${e}_${(++_idCounter).toString(36)}_${Date.now().toString(36)}`}
@@ -1 +1 @@
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
+ import{randomUUID,createHash}from"node:crypto";import{sanitizePromptPayload,stripTransportMetadata}from"../message-filter.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas-tools.js";import{parseMentionsParam,buildMentionInstruction}from"../a2a/mention-resolver.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,groupIntoTurns}from"./history-replay.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 isSystemDefaultSessionKey(e){const t=e.split(":");if(t.length<3||"agent"!==t[0])return!1;const s=t.slice(2).join(":");return"main"===s||s===t[1]}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 n=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,t){const s=createHash("sha256").update(`${t}:${e}`).digest("hex").substring(0,32);return`session:${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20,32)}`}function parseAgentIdFromKey(e){const t=e.split(":");return(t.length>=3&&"agent"===t[0]?t[1]:"")||"main"}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;initialized=!1;agentMapping=null;_syncResolvers=[];setAgentMapping(e){this.agentMapping=e;for(const t of this.state.sessions.values())!t.platformAgentId&&t.agentId&&(t.platformAgentId=e.get(t.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[]}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(t=>{this._syncResolvers.push(t),setTimeout(()=>{const e=this._syncResolvers.indexOf(t);-1!==e&&this._syncResolvers.splice(e,1),t()},e)})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;handleSlashCommand=null;onPromptStart=null;instanceId;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,this.instanceId=e.instanceId||"unknown"}resolvePlatformAgentId(e){return this.agentMapping?.get(e)?.id}getDefaultAgentId(){return this.agentMapping&&this.agentMapping.size>0?this.agentMapping.keys().next().value??"main":"main"}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":await this.handleSessionDelete(e);break;case"session/clear":await this.handleSessionClear(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;onSessionLoaded=null;onSessionDeleted=null;onSessionCleared=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/toggle":case"canvas/clear":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,n=this.state.resolveSessionFromParams(t);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}n.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="string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId(),s=`agent:${t}:${randomUUID()}`,n=deriveSessionId(s,this.instanceId),i="string"==typeof e.params?.cwd?e.params.cwd:".",o=this.resolvePlatformAgentId(t),a={sessionId:n,gatewaySessionKey:s,agentId:t,platformAgentId:o,createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:i};this.state.sessions.set(n,a),this.state.sessionsByGatewayKey.set(s,a),this.transport.sendResult(e.id,{sessionId:n,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s,agentId:t,platformAgentId:o??null}}),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 recoverSessionFromGateway(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.sessionsList();if(!s||"object"!=typeof s)return;const n=s.sessions;if(!Array.isArray(n))return;for(const t of n){const s=t,n=s.sessionKey??s.key??s.id??s.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const t=parseAgentIdFromKey(n),s={sessionId:e,gatewaySessionKey:n,agentId:t,platformAgentId:this.resolvePlatformAgentId(t),createdAt:Date.now(),chatReplayDedupKeys:new Set};return this.state.sessions.set(e,s),this.state.sessionsByGatewayKey.set(n,s),s}}}catch{}}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&&t.startsWith("agent:")&&(s=this.state.findSessionByKey(t)),s||t.startsWith("agent:")||(s=await this.recoverSessionFromGateway(t)),!s){const e=t.startsWith("agent:"),n=e?deriveSessionId(t,this.instanceId):t,i=this.config.gateway.agentId||this.getDefaultAgentId(),o=e?t:`agent:${i}:${t}`,a=parseAgentIdFromKey(o);s={sessionId:n,gatewaySessionKey:o,agentId:a,platformAgentId:this.resolvePlatformAgentId(a),createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(n,s),this.state.sessionsByGatewayKey.set(o,s)}const n=s.sessionId;this.state.lastActiveSessionId=n,s.chatReplayDedupKeys.clear();try{const e=this.getGateway(),t=await loadHistory(e,s.gatewaySessionKey,50),i=groupIntoTurns(t),o=i.filter(e=>"user"===e.role).length,a=i.filter(e=>"assistant"===e.role).length,r=i.filter(e=>e.toolCalls?.length).length,d=i.reduce((e,t)=>e+(t.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${t.length} turns=${i.length} userTurns=${o} asstTurns=${a} turnsWithToolCalls=${r} totalToolCalls=${d}`);for(let e=0;e<i.length;e++){const t=i[e];if("user"===t.role&&"internal_system"===t.provenance?.kind){this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] internal_system user message`);continue}if("system"===t.role){this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${t.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:t.content},metadata:t.metadata});continue}this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${t.role} contentLen=${t.content.length} content=${JSON.stringify(t.content.slice(0,80))} tcCount=${t.toolCalls?.length??0}`);const s={sessionUpdate:"history_turn",role:t.role,content:t.content,reasoning:t.reasoning,toolCalls:t.toolCalls};t.metadata&&(s.metadata=t.metadata),this.transport.sendSessionUpdate(n,s)}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,s.gatewaySessionKey,s.platformAgentId);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 n=0;for(;Date.now()<s&&(e.currentRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const s=await Promise.race([e.assistantMq.read(n),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),n++}n>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:n}})}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,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(e.gatewaySessionKey)});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,n=s.sessionKey??s.key??s.id??s.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=t.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),a=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0;if(o){if(!o.title||"New chat"===o.title){const e=a||o.sessionId;o.title=e;const t=this.state.sessions.get(o.sessionId);t&&!t.title&&(t.title=e)}const e=s.updatedAt;e&&(o.updatedAt=e)}else{const e=i,o=parseAgentIdFromKey(n),r={sessionId:e,gatewaySessionKey:n,agentId:o,platformAgentId:this.resolvePlatformAgentId(o),createdAt:Date.now(),chatReplayDedupKeys:new Set,title:a||"New chat",cwd:s.cwd};this.state.sessions.set(e,r),this.state.sessionsByGatewayKey.set(n,r),t.push({sessionId:e,title:a||"New chat",cwd:s.cwd,updatedAt:s.updatedAt,agentId:o,platformAgentId:r.platformAgentId,deletable:!isSystemDefaultSessionKey(n)})}}}}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 n=s.slice(0,64),i=this.state.sessions.get(t);if(!i)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);i.title=n;const o=i.gatewaySessionKey,a=this.getGateway();a?.isConnected&&a.sessionsPatch(o,{label:n}).catch(()=>{}),this.transport.sendResult(e.id,{sessionId:t,title:n})}async 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);if(!s)return this.logger.info?.(`[bridge-core] session/delete sessionId=${t} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=s.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${t} gatewayKey=${n} gwConnected=${!!this.getGateway()?.isConnected}`);let i=!1;const o=this.getGateway();if(o?.isConnected&&n)try{await o.sessionsDelete(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${n}`)}catch(t){const s=t instanceof Error?t.message:String(t);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${s}`),!s.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await o.sessionsReset(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${n}`)}catch(t){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${t instanceof Error?t.message:t}`),void this.transport.sendResult(e.id,{deleted:!1})}}else n&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${n}`);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);const a=new Set;for(const[e,s]of this.state.toolCallToSessionMap)s===t&&(this.state.toolCallToSessionMap.delete(e),a.add(e));for(const[e,t]of this.state.toolCallIdMap)a.has(t)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(n),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${t} gatewayKey=${n} deleted=true`),this.transport.sendResult(e.id,{deleted:!0})}async handleSessionClear(e){this.logger.info?.(`[bridge-core] handleSessionClear called, params=${JSON.stringify(e.params)}`);const t=e.params?.sessionId;if(!t)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${t} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${t} gatewayKey=${s.gatewaySessionKey}`);const n=this.getGateway();if(n?.isConnected&&s.gatewaySessionKey)try{await n.sessionsReset(s.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${s.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${s.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n?.isConnected}, key=${s.gatewaySessionKey}`);s.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(t,s.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(n&&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(n.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,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(n&&i?.isConnected&&s)try{await i.sessionsPatch(n.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 n=this.state.sessions.get(t);if(!n&&t.startsWith("agent:")&&(n=this.state.findSessionByKey(t)),n||t.startsWith("agent:")||(n=await this.recoverSessionFromGateway(t)),!n){const e=t.startsWith("agent:"),s=e?deriveSessionId(t,this.instanceId):t,i=this.config.gateway.agentId||this.getDefaultAgentId(),o=e?t:`agent:${i}:${t}`,a=parseAgentIdFromKey(o);n={sessionId:s,gatewaySessionKey:o,agentId:a,platformAgentId:this.resolvePlatformAgentId(a),createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(s,n),this.state.sessionsByGatewayKey.set(o,n)}const i=n.sessionId;this.state.lastActiveSessionId=i,this.onPromptStart?.();const o=this.getGateway();if(!o?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let a=extractPromptBlocks(sanitizePromptPayload(s));if(!n.title){const e=a.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(n.title=e.text.slice(0,64).replace(/\n/g," ").trim(),o?.isConnected&&n.title&&o.sessionsPatch(n.gatewaySessionKey,{label:n.title}).catch(()=>{}))}const r=buildResolutionPlan(a);if(r.refs.length>0)try{const e=await resolveResources(r,this.writeObs);a=applyResolutions(a,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const d=toGatewayPrompt(a);if("error"in d)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:d.error});const{message:l,attachments:c}=d,p=resolveStandaloneSlashCommandName(a,l)??resolveSlashCmd(a,l),h="string"==typeof p;if(p&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(p)}catch(e){return t.logger.warn?.(`[bridge-core] /${p} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,a),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${p}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const g=h?"chat.send":"agent";let m=h?l:withUserMessagePrefix(l);if(this.config.canvasEnabled&&!h&&!1!==n.canvasActive){const e=n.promptsSinceCanvasInjection??0;!n.canvasPromptInjected||e>=10?(m=`${CANVAS_TERM_MAPPING}\n\n${m}`,n.canvasPromptInjected=!0,n.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):n.promptsSinceCanvasInjection=e+1}if(this.config.canvasEnabled&&!h&&!1!==n.canvasActive){const e=this.getCanvasState?.();if(e){const t=e.resolveCanvasForSession(n.gatewaySessionKey);m=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${m}`}const t=n.referencedSurfaceId;t&&(m=`${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${m}`,n.referencedSurfaceId=void 0)}if(!h){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){n.activeMentions=t;m=`${buildMentionInstruction(t)}\n${m}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${t.length} agent(s)`)}else n.activeMentions=void 0}h&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${i} command=/${p}`),this.state.resetForNewRun(n,i,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=g,this.emitPromptAsUserSessionUpdates(i,a),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(n);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=h?await o.chatSend(n.gatewaySessionKey,m,e):await o.agentSend(n.gatewaySessionKey,m,n.agentId||this.config.gateway.agentId||this.getDefaultAgentId(),e),n.currentRunId=t.runId,n.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(t.runId,n)}catch(t){this.state.clearPromptTimeout(n),void 0!==n.pendingPromptId&&(n.pendingPromptId=void 0,n.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:t instanceof Error?t.message:"failed to send prompt to gateway"})),n.pendingAgentSends?.length&&(this.logger.warn?.("[bridge-core] Discarding %d deferred agentSends after prompt send failure",n.pendingAgentSends.length),n.pendingAgentSends=void 0)}}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,n=t.mimeType??t.mime_type??e.mimeType??e.mime_type,i=t.text??e.text,o=t.data??e.data,a=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}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...a?{fileName:a}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(s);if(!n)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway();i?.isConnected&&n.currentRunId&&i.chatAbort(n.gatewaySessionKey,n.currentRunId).catch(()=>{}),this.completePrompt(n,"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.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close()}completePrompt(e,t){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const s=e.currentRunId;if(s&&this.onRunComplete){const t=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(s,t)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t}),this.cleanupPrompt(e)}failPrompt(e,t,s){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const n=e.currentRunId;n&&this.onRunFailed&&this.onRunFailed(n,s),void 0!==e.pendingPromptId&&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,n){if(0===s.length)return;const i=(n?this.state.promptsByRunId.get(n):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=${n})`);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 n=await t.request(s,e.params??{});this.transport.sendResult(e.id,n??{})}catch(t){const n=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}isCronCorrelatedRun(e){return this.state.cronRunIds.has(e)}}
@@ -1 +1 @@
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
+ import{Errors}from"../errors/index.js";import{stripTransportMetadata}from"../message-filter.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)}}]});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}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(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)}}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)}}]})}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";n({sessionUpdate:"tool_call",toolCallId:this.resolveToolCallId(e,{...s,...t},"start"),title:o,kind:"other",status:"in_progress"})}}}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)}}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
- import{readLocalHistory}from"./local-session-history.js";export async function loadHistory(t,o,e=50){if(t?.isConnected)try{const l=parseGatewayHistory(await t.chatHistory(o,e));if(l.length>0)return l}catch{}const l=readLocalHistory(o,e);return l.length>0?l.map(t=>{let o;switch(t.entryType){case"thought":o="agent_thought_chunk";break;case"tool_call":o="tool_call";break;case"tool_result":o="tool_call_update";break;default:o="user"===t.role?"user_message_chunk":"agent_message_chunk"}return{role:t.role,content:t.content,dedupKey:t.dedupKey,updateType:o,toolCallId:t.toolCallId,toolName:t.toolName}}):[]}export function groupIntoTurns(t){const o=[];let e=null;const l=()=>{e&&(o.push(e),e=null)};for(const n of t){if("assistant"===n.role&&(n.reasoning||n.toolCalls?.length)){l(),e={role:"assistant",content:n.content,reasoning:n.reasoning},n.toolCalls?.length&&(e.toolCalls=n.toolCalls.map(t=>({id:t.id,callId:t.id,name:t.name,arguments:t.arguments??"",status:t.status??"completed",output:t.output,isError:"error"===t.status||"failed"===t.status})));continue}if("user"===n.role){l(),o.push({role:"user",content:n.content});continue}e||(e={role:"assistant",content:""});switch(n.updateType??"agent_message_chunk"){case"agent_thought_chunk":e.reasoning=(e.reasoning??"")+n.content;break;case"tool_call":e.toolCalls||(e.toolCalls=[]),e.toolCalls.push({id:n.toolCallId??`tc_${e.toolCalls.length}`,callId:n.toolCallId??`tc_${e.toolCalls.length}`,name:n.toolName??"tool",arguments:n.content||"",status:"completed"});break;case"tool_call_update":{e.toolCalls||(e.toolCalls=[]);const t=n.toolCallId?e.toolCalls.find(t=>t.id===n.toolCallId):void 0;t?(t.output=n.content,t.status="completed"):e.toolCalls.push({id:n.toolCallId??`tc_${e.toolCalls.length}`,callId:n.toolCallId??`tc_${e.toolCalls.length}`,name:n.toolName??"tool",arguments:"",status:"completed",output:n.content});break}default:e.content+=n.content}}return l(),o}const TOOL_CALL_BLOCK_TYPES=new Set(["toolCall","toolUse","functionCall","tool_use","tool_call","toolcall"]),TOOL_RESULT_BLOCK_TYPES=new Set(["tool_result","tool_result_error","toolResult"]);function extractBlockText(t){if("string"==typeof t)return t;if(!Array.isArray(t))return"";let o="";for(const e of t)"string"!=typeof e?e&&"object"==typeof e&&"string"==typeof e.text&&(o+=e.text):o+=e;return o}function parseGatewayHistory(t){if(!t||"object"!=typeof t)return[];const o=t.messages;if(!Array.isArray(o))return[];const e=[];let l=null;for(const t of o){if(!t||"object"!=typeof t)continue;const o=t,n="string"==typeof o.role?o.role:"assistant";if("toolResult"===n||"tool"===n){if(!l?.toolCalls?.length)continue;const t=String(o.toolCallId??o.tool_call_id??o.toolUseId??o.tool_use_id??""),e=extractBlockText(o.content);if(t){const n=l.toolCalls.find(o=>o.id===t);if(n)n.output=e,!0!==o.is_error&&!0!==o.isError||(n.status="error");else{const t=l.toolCalls.find(t=>void 0===t.output);t&&(t.output=e,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}else{const t=l.toolCalls.find(t=>void 0===t.output);t&&(t.output=e,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}continue}if("user"===n&&l?.toolCalls?.length&&Array.isArray(o.content)){let t=!1,e=!1;for(const l of o.content){if(!l||"object"!=typeof l)continue;const o=l,n="string"==typeof o.type?o.type:"";TOOL_RESULT_BLOCK_TYPES.has(n)?t=!0:"text"===n&&"string"==typeof o.text&&o.text.trim()&&(e=!0)}if(t&&!e){for(const t of o.content){if(!t||"object"!=typeof t)continue;const o=t,e="string"==typeof o.type?o.type:"";if(!TOOL_RESULT_BLOCK_TYPES.has(e))continue;const n=String(o.toolUseId??o.tool_use_id??o.toolCallId??o.tool_call_id??""),s="string"==typeof o.output?o.output:extractBlockText(o.content);if(n){const t=l.toolCalls.find(t=>t.id===n);if(t)t.output=s,!0!==o.is_error&&!0!==o.isError||(t.status="error");else{const t=l.toolCalls.find(t=>void 0===t.output);t&&(t.output=s,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}else{const t=l.toolCalls.find(t=>void 0===t.output);t&&(t.output=s,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}continue}}const s="user"===n?"user":"assistant";let r,i,a="";if("string"==typeof o.content)a=o.content;else if(Array.isArray(o.content))for(const t of o.content){if("string"==typeof t){a+=t;continue}if(!t||"object"!=typeof t)continue;const o=t,e="string"==typeof o.type?o.type:"";if("text"===e&&"string"==typeof o.text)a+=o.text;else if("thinking"===e&&"string"==typeof o.thinking)i=(i??"")+o.thinking;else if(TOOL_CALL_BLOCK_TYPES.has(e)){r||(r=[]);const t=o.arguments??o.input,e="string"==typeof t?t:null!=t?JSON.stringify(t):"";r.push({id:String(o.id??o.toolCallId??o.tool_call_id??""),name:String(o.name??"tool"),arguments:e,status:"completed"})}else if(TOOL_RESULT_BLOCK_TYPES.has(e)){const t=String(o.toolUseId??o.tool_use_id??o.toolCallId??o.tool_call_id??""),e="string"==typeof o.output?o.output:extractBlockText(o.content),l=!0===o.is_error||!0===o.isError;if(r){const o=t?r.find(o=>o.id===t):void 0;if(o)o.output=e,l&&(o.status="error");else{const t=r.find(t=>void 0===t.output);t&&(t.output=e,l&&(t.status="error"))}}}}if(i||("string"==typeof o.reasoning&&o.reasoning?i=o.reasoning:"string"==typeof o.thinking&&o.thinking&&(i=o.thinking)),!r?.length){const t=o.tool_calls??o.toolCalls;if(Array.isArray(t)){r=[];for(const o of t){if(!o||"object"!=typeof o)continue;const t=o,e=t.function;r.push({id:String(t.id??t.toolCallId??""),name:String(e?.name??t.name??t.toolName??"tool"),arguments:"string"==typeof t.arguments?t.arguments:"string"==typeof e?.arguments?e.arguments:"",status:String(t.status??"completed"),output:"string"==typeof t.output?t.output:void 0})}0===r.length&&(r=void 0)}}if(!a&&!i&&!r?.length)continue;const u={role:s,content:a,reasoning:i,toolCalls:r};e.push(u),"assistant"===s&&(l=u)}e.filter(t=>t.toolCalls?.length),e.reduce((t,o)=>t+(o.toolCalls?.length??0),0),e.reduce((t,o)=>t+(o.toolCalls?.filter(t=>void 0!==t.output).length??0),0);return e}
1
+ import{stripTransportMetadata}from"../message-filter.js";const BEEOS_USER_MSG_MARKER_RE=/(?:^|\n)\s*User Message From \w+:\s*\n/i,CANVAS_STATE_RE=/^\[Canvas State\][^\n]*/,SYSTEM_NOTE_RE=/^\[System Note\][^\n]*/,CANVAS_INTERACTION_RE=/^\[Canvas Interaction\] The user clicked "([^"]*)" on surface "([^"]*)"/,ACTION_CONTEXT_RE=/\nAction context: (.+?)(?:\n|$)/s;function stripBeeosUserEnvelope(t){const o=BEEOS_USER_MSG_MARKER_RE.exec(t);if(o){return t.slice(o.index+o[0].length).trim()}let e=t;return e=e.replace(CANVAS_STATE_RE,"").trimStart(),e=e.replace(SYSTEM_NOTE_RE,"").trimStart(),stripTransportMetadata(e).trim()}export async function loadHistory(t,o,e=50){if(!t?.isConnected)return[];try{const n=parseGatewayHistory(await t.chatHistory(o,e));if(n.length>0){n.filter(t=>"user"===t.role).length,n.filter(t=>"assistant"===t.role).length}return n}catch{return[]}}export function groupIntoTurns(t){const o=[];let e=null;const n=()=>{e&&(o.push(e),e=null)};for(const s of t){if("assistant"===s.role&&(s.reasoning||s.toolCalls?.length)){n(),e={role:"assistant",content:s.content,reasoning:s.reasoning},s.toolCalls?.length&&(e.toolCalls=s.toolCalls.map(t=>({id:t.id,callId:t.id,name:t.name,arguments:t.arguments??"",status:t.status??"completed",output:t.output,isError:"error"===t.status||"failed"===t.status})));continue}if("system"===s.role){n();const t={role:"system",content:s.content};s.metadata&&(t.metadata=s.metadata),o.push(t);continue}if("user"===s.role){n();const t={role:"user",content:s.content};s.provenance&&(t.provenance=s.provenance),s.metadata&&(t.metadata=s.metadata),o.push(t);continue}e||(e={role:"assistant",content:""});switch(s.updateType??"agent_message_chunk"){case"agent_thought_chunk":e.reasoning=(e.reasoning??"")+s.content;break;case"tool_call":e.toolCalls||(e.toolCalls=[]),e.toolCalls.push({id:s.toolCallId??`tc_${e.toolCalls.length}`,callId:s.toolCallId??`tc_${e.toolCalls.length}`,name:s.toolName??"tool",arguments:s.content||"",status:"completed"});break;case"tool_call_update":{e.toolCalls||(e.toolCalls=[]);const t=s.toolCallId?e.toolCalls.find(t=>t.id===s.toolCallId):void 0;t?(t.output=s.content,t.status="completed"):e.toolCalls.push({id:s.toolCallId??`tc_${e.toolCalls.length}`,callId:s.toolCallId??`tc_${e.toolCalls.length}`,name:s.toolName??"tool",arguments:"",status:"completed",output:s.content});break}default:e.content+=s.content}}return n(),o}const TOOL_CALL_BLOCK_TYPES=new Set(["toolCall","toolUse","functionCall","tool_use","tool_call","toolcall"]),TOOL_RESULT_BLOCK_TYPES=new Set(["tool_result","tool_result_error","toolResult"]);function extractBlockText(t){if("string"==typeof t)return t;if(!Array.isArray(t))return"";let o="";for(const e of t)"string"!=typeof e?e&&"object"==typeof e&&"string"==typeof e.text&&(o+=e.text):o+=e;return o}function parseGatewayHistory(t){if(!t||"object"!=typeof t)return[];const o=t.messages;if(!Array.isArray(o))return[];const e=[];let n=null;for(const t of o){if(!t||"object"!=typeof t)continue;const o=t,s="string"==typeof o.role?o.role:"assistant";if("toolResult"===s||"tool"===s){if(!n?.toolCalls?.length)continue;const t=String(o.toolCallId??o.tool_call_id??o.toolUseId??o.tool_use_id??""),e=extractBlockText(o.content);if(t){const s=n.toolCalls.find(o=>o.id===t);if(s)s.output=e,!0!==o.is_error&&!0!==o.isError||(s.status="error");else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=e,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=e,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}continue}if("user"===s&&n?.toolCalls?.length&&Array.isArray(o.content)){let t=!1,e=!1;for(const n of o.content){if(!n||"object"!=typeof n)continue;const o=n,s="string"==typeof o.type?o.type:"";TOOL_RESULT_BLOCK_TYPES.has(s)?t=!0:"text"===s&&"string"==typeof o.text&&o.text.trim()&&(e=!0)}if(t&&!e){for(const t of o.content){if(!t||"object"!=typeof t)continue;const o=t,e="string"==typeof o.type?o.type:"";if(!TOOL_RESULT_BLOCK_TYPES.has(e))continue;const s=String(o.toolUseId??o.tool_use_id??o.toolCallId??o.tool_call_id??""),r="string"==typeof o.output?o.output:extractBlockText(o.content);if(s){const t=n.toolCalls.find(t=>t.id===s);if(t)t.output=r,!0!==o.is_error&&!0!==o.isError||(t.status="error");else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=r,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=r,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}continue}}const r="user"===s?"user":"assistant";let l,a,i,c,u="";if("string"==typeof o.content)u=o.content;else if(Array.isArray(o.content))for(const t of o.content){if("string"==typeof t){u+=t;continue}if(!t||"object"!=typeof t)continue;const o=t,e="string"==typeof o.type?o.type:"";if("text"===e&&"string"==typeof o.text)u+=o.text;else if("thinking"===e&&"string"==typeof o.thinking)a=(a??"")+o.thinking;else if(TOOL_CALL_BLOCK_TYPES.has(e)){l||(l=[]);const t=o.arguments??o.input,e="string"==typeof t?t:null!=t?JSON.stringify(t):"";l.push({id:String(o.id??o.toolCallId??o.tool_call_id??""),name:String(o.name??"tool"),arguments:e,status:"completed"})}else if(TOOL_RESULT_BLOCK_TYPES.has(e)){const t=String(o.toolUseId??o.tool_use_id??o.toolCallId??o.tool_call_id??""),e="string"==typeof o.output?o.output:extractBlockText(o.content),n=!0===o.is_error||!0===o.isError;if(l){const o=t?l.find(o=>o.id===t):void 0;if(o)o.output=e,n&&(o.status="error");else{const t=l.find(t=>void 0===t.output);t&&(t.output=e,n&&(t.status="error"))}}}}if(a||("string"==typeof o.reasoning&&o.reasoning?a=o.reasoning:"string"==typeof o.thinking&&o.thinking&&(a=o.thinking)),!l?.length){const t=o.tool_calls??o.toolCalls;if(Array.isArray(t)){l=[];for(const o of t){if(!o||"object"!=typeof o)continue;const t=o,e=t.function;l.push({id:String(t.id??t.toolCallId??""),name:String(e?.name??t.name??t.toolName??"tool"),arguments:"string"==typeof t.arguments?t.arguments:"string"==typeof e?.arguments?e.arguments:"",status:String(t.status??"completed"),output:"string"==typeof t.output?t.output:void 0})}0===l.length&&(l=void 0)}}if("user"===r&&u){u=stripBeeosUserEnvelope(u);const t=CANVAS_INTERACTION_RE.exec(u);if(t){const o=t[1];i={kind:"canvas_action"};const e={type:"canvas_action",actionName:o,surfaceId:t[2]},n=ACTION_CONTEXT_RE.exec(u);if(n)try{e.actionPayload=JSON.parse(n[1])}catch{}c=e,u=`Clicked "${o}"`}}if(!u&&!a&&!l?.length)continue;if(!i&&"user"===r&&o.provenance&&"object"==typeof o.provenance){const t=o.provenance;"string"==typeof t.kind&&(i={kind:t.kind},"string"==typeof t.sourceTool&&(i.sourceTool=t.sourceTool))}const f={role:r,content:u,reasoning:a,toolCalls:l,provenance:i,metadata:c};e.push(f),"assistant"===r&&(n=f)}e.filter(t=>t.toolCalls?.length),e.reduce((t,o)=>t+(o.toolCalls?.length??0),0),e.reduce((t,o)=>t+(o.toolCalls?.filter(t=>void 0!==t.output).length??0),0);return e}
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";export class AcpGatewaySessionState{sessions=new Map;promptsByRunId=new Map;sessionsByGatewayKey=new Map;toolCallIdMap=new Map;toolCallToSessionMap=new Map;nextToolCallSeq=0;cronRunIds=new Set;cronRunIdTimers=new Map;lastCronSignalAtMs=0;cronTextByKey=new Map;cronFlushTimersByKey=new Map;cronRequestIdsByKey=new Map;lastActiveSessionId;sessionGcTimer=null;MAX_IDLE_SESSION_MS=864e5;SESSION_GC_INTERVAL_MS=3e5;logger;constructor(s){this.logger=s}findSessionByKey(s){return this.sessionsByGatewayKey.get(s)}findCronTargetSession(){if(this.lastActiveSessionId)return this.sessions.get(this.lastActiveSessionId)}resolveSessionFromParams(s){const e="string"==typeof s?.sessionId?s.sessionId:void 0;if(e)return this.sessions.get(e)}isCurrentStreamOwner(s,e){return!e||!!s.assistantStreamOwnerRunId&&e===s.assistantStreamOwnerRunId}clearPromptTimeout(s){s.promptTimeoutTimer&&(clearTimeout(s.promptTimeoutTimer),s.promptTimeoutTimer=void 0)}clearStreamIdleTimer(s){s.streamIdleTimer&&(clearTimeout(s.streamIdleTimer),s.streamIdleTimer=void 0)}extractAgentAssistantDelta(s,e){const t=(s.chatAssistantTextSoFar??"").length;if(e.delta){const i=s.agentAssistantTextSoFar??"",n=i+e.delta;if(s.agentAssistantTextSoFar=n,t>0&&i.length<t){if(n.length<=t)return;return n.slice(t)}return e.delta}const i=e.text;if(!i)return;const n=s.agentAssistantTextSoFar??"";s.agentAssistantTextSoFar=i;const o=Math.max(n.length,t);if(!(i.length<=o)){if(i.startsWith(n)||0===o)return i.slice(o)||void 0;if(!n.startsWith(i))return i}}isCronCandidate(s,e){return!(!s||!this.cronRunIds.has(s))||Date.now()-this.lastCronSignalAtMs<=e&&(s&&this.cronRunIds.add(s),!0)}resolveCronStreamKey(s){if(s.runId)return`run:${s.runId}`}getCronRequestId(s){let e=this.cronRequestIdsByKey.get(s);return e||(e=randomUUID(),this.cronRequestIdsByKey.set(s,e)),e}startSessionGc(s){this.sessionGcTimer=setInterval(()=>this.gcSessions(s),this.SESSION_GC_INTERVAL_MS),"object"==typeof this.sessionGcTimer&&"unref"in this.sessionGcTimer&&this.sessionGcTimer.unref()}gcSessions(s){const e=Date.now()-this.MAX_IDLE_SESSION_MS;let t=0;for(const[i,n]of this.sessions){if(void 0!==n.pendingPromptId)continue;(n.lastStreamActivityTs??n.createdAt)<e&&(s(n),this.sessions.delete(i),this.sessionsByGatewayKey.delete(n.gatewaySessionKey),t++)}const i=new Set(this.promptsByRunId.keys());for(const[s,e]of this.toolCallIdMap){const t=e.split("_")[1];t&&!i.has(t)&&this.toolCallIdMap.delete(s)}const n=new Set(Array.from(this.sessions.values()).map(s=>s.sessionId));for(const[s,e]of this.toolCallToSessionMap)n.has(e)||this.toolCallToSessionMap.delete(s);t>0&&this.logger.debug?.(`[session-state] GC evicted ${t} idle sessions, remaining=${this.sessions.size}`)}destroy(){this.sessionGcTimer&&(clearInterval(this.sessionGcTimer),this.sessionGcTimer=null);for(const s of this.sessions.values())this.clearPromptTimeout(s),this.clearStreamIdleTimer(s),s.assistantMq?.close();for(const s of this.cronFlushTimersByKey.values())clearTimeout(s);this.cronFlushTimersByKey.clear(),this.cronTextByKey.clear(),this.cronRequestIdsByKey.clear();for(const s of this.cronRunIdTimers.values())clearTimeout(s);this.cronRunIdTimers.clear(),this.cronRunIds.clear(),this.promptsByRunId.clear(),this.toolCallIdMap.clear(),this.toolCallToSessionMap.clear(),this.sessionsByGatewayKey.clear(),this.sessions.clear(),this.lastActiveSessionId=void 0}}
1
+ import{randomUUID}from"node:crypto";import{MessageQueue}from"../utils/mq.js";export class AcpGatewaySessionState{sessions=new Map;promptsByRunId=new Map;sessionsByGatewayKey=new Map;toolCallIdMap=new Map;toolCallToSessionMap=new Map;nextToolCallSeq=0;cronRunIds=new Set;cronRunIdTimers=new Map;lastCronSignalAtMs=0;cronTextByKey=new Map;cronFlushTimersByKey=new Map;cronRequestIdsByKey=new Map;lastActiveSessionId;sessionGcTimer=null;MAX_IDLE_SESSION_MS=864e5;SESSION_GC_INTERVAL_MS=3e5;logger;constructor(s){this.logger=s}findSessionByKey(s){return this.sessionsByGatewayKey.get(s)}findCronTargetSession(){if(this.lastActiveSessionId)return this.sessions.get(this.lastActiveSessionId)}resolveSessionFromParams(s){const e="string"==typeof s?.sessionId?s.sessionId:void 0;if(e)return this.sessions.get(e)}isCurrentStreamOwner(s,e){return!e||!!s.currentRunId&&e===s.currentRunId}clearPromptTimeout(s){s.promptTimeoutTimer&&(clearTimeout(s.promptTimeoutTimer),s.promptTimeoutTimer=void 0)}clearStreamIdleTimer(s){s.streamIdleTimer&&(clearTimeout(s.streamIdleTimer),s.streamIdleTimer=void 0)}resetForNewRun(s,e,t){this.clearStreamIdleTimer(s),s.currentRunId&&this.promptsByRunId.delete(s.currentRunId),s.currentRunId=void 0,s.assistantMq?.close(),s.assistantMq=new MessageQueue,s.chatReplayDedupKeys.clear(),s.hasAgentAssistantStream=!1,s.hasAgentThinkingStream=!1,s.hasAgentToolStartStream=!1,s.hasAgentToolResultStream=!1,s.chatAssistantTextSoFar=void 0,s.agentAssistantTextSoFar=void 0,s.lastAgentSeq=void 0,s.lastChatSeq=void 0,s.promptDone=!1,t.resetSessionUpdateCount(e)}extractAgentAssistantDelta(s,e){const t=(s.chatAssistantTextSoFar??"").length;if(e.delta){const i=s.agentAssistantTextSoFar??"",n=i+e.delta;if(s.agentAssistantTextSoFar=n,t>0&&i.length<t){if(n.length<=t)return;return n.slice(t)}return e.delta}const i=e.text;if(!i)return;const n=s.agentAssistantTextSoFar??"";s.agentAssistantTextSoFar=i;const o=Math.max(n.length,t);if(!(i.length<=o)){if(i.startsWith(n)||0===o)return i.slice(o)||void 0;if(!n.startsWith(i))return i}}isCronCandidate(s,e){return!(!s||!this.cronRunIds.has(s))||Date.now()-this.lastCronSignalAtMs<=e&&(s&&this.cronRunIds.add(s),!0)}resolveCronStreamKey(s){if(s.runId)return`run:${s.runId}`}getCronRequestId(s){let e=this.cronRequestIdsByKey.get(s);return e||(e=randomUUID(),this.cronRequestIdsByKey.set(s,e)),e}startSessionGc(s,e){this.sessionGcTimer=setInterval(()=>this.gcSessions(s,e),this.SESSION_GC_INTERVAL_MS),"object"==typeof this.sessionGcTimer&&"unref"in this.sessionGcTimer&&this.sessionGcTimer.unref()}gcSessions(s,e){const t=Date.now()-this.MAX_IDLE_SESSION_MS;let i=0;for(const[n,o]of this.sessions){if(void 0!==o.pendingPromptId)continue;(o.lastStreamActivityTs??o.createdAt)<t&&(s(o),e?.(o),this.sessions.delete(n),this.sessionsByGatewayKey.delete(o.gatewaySessionKey),i++)}const n=new Set(Array.from(this.sessions.values()).map(s=>s.sessionId));for(const[s,e]of this.toolCallToSessionMap)n.has(e)||this.toolCallToSessionMap.delete(s);for(const[s,e]of this.toolCallIdMap)this.toolCallToSessionMap.has(e)||this.toolCallIdMap.delete(s);i>0&&(this.lastActiveSessionId&&!this.sessions.has(this.lastActiveSessionId)&&(this.lastActiveSessionId=void 0),this.logger.debug?.(`[session-state] GC evicted ${i} idle sessions, remaining=${this.sessions.size}`))}destroy(){this.sessionGcTimer&&(clearInterval(this.sessionGcTimer),this.sessionGcTimer=null);for(const s of this.sessions.values())this.clearPromptTimeout(s),this.clearStreamIdleTimer(s),s.assistantMq?.close();for(const s of this.cronFlushTimersByKey.values())clearTimeout(s);this.cronFlushTimersByKey.clear(),this.cronTextByKey.clear(),this.cronRequestIdsByKey.clear();for(const s of this.cronRunIdTimers.values())clearTimeout(s);this.cronRunIdTimers.clear(),this.cronRunIds.clear(),this.promptsByRunId.clear(),this.toolCallIdMap.clear(),this.toolCallToSessionMap.clear(),this.sessionsByGatewayKey.clear(),this.sessions.clear(),this.lastActiveSessionId=void 0}}
@@ -1 +1 @@
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
+ 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)}enrichWithTurnId(e,t){return t?{...e,_turnId:t}:e}sendSessionUpdate(e,t,s){const r=t,o=r?.sessionUpdate;if("agent_thought_chunk"===o&&!this.forward.forwardThinking)return!1;if(("tool_call"===o||"tool_call_update"===o)&&!this.forward.forwardToolCalls)return!1;const n=(this.sessionUpdateCounts.get(e)??0)+1;return this.sessionUpdateCounts.set(e,n),n%50==0&&this.obs({component:"acp-transport",domain:"session-update",name:"send.rate",severity:"debug",payload:{sessionId:e,totalSent:n,lastType:o,_debug_index:this.debugIndex}}),this.sendNotification("session/update",{sessionId:e,update:this.enrichWithTurnId(t,s)}),!0}resetSessionUpdateCount(e){this.sessionUpdateCounts.delete(e)}sendCronSessionUpdate(e,t,s){return this.sendNotification("session/update",{sessionId:e,update:this.enrichWithTurnId(t,s.requestId),_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{WebSocketServer,WebSocket}from"ws";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal-session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal-bridge-handler.js";import{CanvasActionRouter,formatActionForAgent}from"./canvas-action-router.js";import{FormSchemaRegistry}from"./form-schema-registry.js";import{FormSurfaceBuilder}from"./form-surface-builder.js";import{ResourceSurfaceRenderer}from"./resource-surface-renderer.js";const SIMPLE_REMAP={Callout:"Alert",Carousel:"List"};function sanitizeA2UIComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e,s=String(t.type??""),n=SIMPLE_REMAP[s];return n?{...t,type:n}:t})}export class ACPServer{agentFetchFn;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch{}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n){this.agentFetchFn=n,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger),this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>{this.core.completePrompt(e,t),void 0===e.pendingPromptId&&e.currentRunId&&(e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.promptDone=void 0),this.actionRouter?.drainPending().catch(e=>{this.logger.warn?.(`[acp-server] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)})},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),void 0===e.pendingPromptId&&e.currentRunId&&(e.currentRunId=void 0,e.assistantStreamOwnerRunId=void 0,e.promptDone=void 0),this.actionRouter?.clearPending()},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn}),this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.handleCanvasNotificationInternal(e,t)},this.core.getCanvasState=()=>this.canvasState,this.core.handleSlashCommand=async e=>{const t=this.descriptorRegistry?.getBySlashCommand(e);return!(!t||!this.resourceRenderer)&&(await this.resourceRenderer.render(t,"inline"),!0)},this.core.onPromptStart=()=>{this.actionRouter?.clearPending()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e))}async start(){const e=this.config.bridge.url,t=this.config.bridge.keyFile;if(e&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch{this.logger.warn?.("[acp-server] Bridge SDK not available")}e&&t&&BridgeClientCtor?await this.startBridgeMode(e,t):this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),await this.connectGateway()}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});s.on("connected",()=>{this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"})}),s.on("disconnected",(...e)=>{const t=String(e[0]??"unknown");this.logger.warn?.(`[acp-server] Bridge disconnected: ${t}`),this.core.initialized=!1}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n,this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async connectGateway(){const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"})}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}isCronCorrelatedRun(e){return this.core.isCronCorrelatedRun(e)}setCanvasState(e,t){this.canvasState=e;const s=(e,t)=>this.sendCanvasUpdate(e,t),n=async(e,t)=>{if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)},r=t?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,sendCanvasUpdate:s,gatewayRequest:n,logger:this.logger});this.formBuilder=i,this.schemaRegistry=r,this.descriptorRegistry=t?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,sendCanvasUpdate:s,gatewayRequest:n,schemaRegistry:r,formBuilder:i,logger:this.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.logger,writeObs:e=>this.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e)})}sendCanvasUpdate(e,t){const s=e&&"_active"!==e&&"_openclaw"!==e?e:this.state.lastActiveSessionId,n=s?this.state.sessions.get(s):void 0;if(!1===n?.canvasActive)return;const r=n?.sessionId??s;r&&this.transport.sendSessionUpdate(r,t)}isCanvasActive(e){const t=e??this.state.lastActiveSessionId,s=t?this.state.sessions.get(t):void 0;return!1!==s?.canvasActive}isCanvasActiveForGatewayKey(e){const t=this.state.sessionsByGatewayKey.get(e);return!1!==t?.canvasActive}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=e.split("_");if(s.length>=3&&"tc"===s[0]){const e=s[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}for(const[,e]of this.state.sessions)if(void 0!==e.pendingPromptId&&!e.promptDone)return e.sessionId;return this.state.lastActiveSessionId}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}getResourceRenderer(){return this.resourceRenderer}getDescriptorRegistry(){return this.descriptorRegistry}getFormBuilder(){return this.formBuilder}getSchemaRegistry(){return this.schemaRegistry}sendCanvasReset(e,t){const s=t??this.state.lastActiveSessionId,n=s?this.state.sessions.get(s):void 0,r=n?.sessionId??s;if(!r)return;const i={sessionUpdate:"canvas.reset"};e&&(i.surfaceId=e),this.transport.sendSessionUpdate(r,i)}async forwardActionToAgent(e){const t=this.state.resolveSessionFromParams(e);if(!t)return void this.logger.warn?.("[acp-server] forwardActionToAgent dropped — no session resolved from params");if(!this.gateway?.isConnected)return;if(t.currentRunId&&!t.promptDone)return this.actionRouter?.enqueue(e),void this.logger.debug?.("[acp-server] Agent busy, canvas action queued");const s=this.canvasState?.summarize(e.surfaceId??""),n=formatActionForAgent(e,s??void 0);try{const e=await this.gateway.agentSend(t.gatewaySessionKey,n,this.config.gateway.agentId||"main");t.currentRunId=e.runId,t.assistantStreamOwnerRunId=e.runId,t.promptDone=!1}catch(s){this.logger.warn?.(`[acp-server] Agent forward failed: ${s instanceof Error?s.message:s}`),this.transport.sendSessionUpdate(t.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}handleCanvasNotificationInternal(e,t){if(!this.canvasState)return void this.logger.warn?.(`[acp-server] Canvas notification ${e} ignored — canvasState not initialized (canvasEnabled=${this.config.canvasEnabled})`);this.canvasState,this.logger;if("canvas/action"===e)this.actionRouter?.route(t).catch(e=>{this.logger.warn?.(`[acp-server] Canvas action routing failed: ${e instanceof Error?e.message:e}`)});else if("canvas/toggle"===e){const e=t,s=!1!==e.enabled,n=this.state.resolveSessionFromParams(e);n&&(n.canvasActive=s,s&&(n.canvasPromptInjected=!1)),this.logger.info?.(`[acp-server] Canvas toggled: ${s}`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:s}})}else if("canvas/clear"===e){const e=t,s="string"==typeof e?.sessionId?e.sessionId:void 0,n=this.canvasState.getAllActive().length;this.canvasState.clear(),this.actionRouter?.reset(),this.sendCanvasReset(void 0,s),this.logger.info?.(`[acp-server] Canvas cleared (${n} surfaces removed)`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}else"canvas/delete"===e&&this.logger.debug?.("[acp-server] canvas/delete via ACP is deprecated — deletions should go through Relay")}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const t=e,s=t.action;if("a2ui_push"===s||"surface_update"===s){const e=String(t.surfaceId??`openclaw-${Date.now()}`),s=t.components;if(s&&Array.isArray(s)){const t=sanitizeA2UIComponents(s);this.canvasState.has(e)?this.canvasState.update(e,t):this.canvasState.create(e,"inline",t,{origin:{kind:"openclaw"}}),this.logger.debug?.(`[acp-server] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===s||"url_open"===s){const e=String(t.url??"");if(e){const s=`openclaw-nav-${Date.now()}`,n=[{id:`${s}-embed`,type:"WebEmbed",properties:{url:e,title:String(t.title??e)}}];this.canvasState.create(s,"inline",n,{origin:{kind:"openclaw"}})}}else this.logger.debug?.(`[acp-server] Unhandled OpenClaw canvas action: ${s}`)}async stop(){this.state.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop()),this.client&&(this.client.removeAllListeners(),this.client.close()),this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close()),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
1
+ import{WebSocketServer,WebSocket}from"ws";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal-session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal-bridge-handler.js";import{CANVAS_TERM_MAPPING}from"./canvas-tools.js";import{CanvasActionRouter,formatActionForAgent}from"./canvas-action-router.js";import{FormSchemaRegistry}from"./form-schema-registry.js";import{FormSurfaceBuilder}from"./form-surface-builder.js";import{ResourceSurfaceRenderer}from"./resource-surface-renderer.js";import{syncAgentsToPlatform}from"./agent-sync.js";const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;const s=toPascalCase(String(t.type??"Text")),n=t.properties??{},a={};for(const[e,t]of Object.entries(n))"children"===e&&Array.isArray(t)?a.children={explicitList:t}:STRING_PROPS.has(e)&&"string"==typeof t?a[e]={literalString:t}:a[e]=t;return{id:t.id,component:{[s]:a}}})}export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingToolCallSessionKey;pendingA2ARuns=new Map;pendingCanvasLinks=[];drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey,t.inputProvenance).catch(e=>this.logger.warn?.("[acp-server] Deferred agentSend failed: %s",e)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async agentSend(e,t,s,n){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const a=this.config.gateway.agentId||this.core.getDefaultAgentId();let i,r;if(s){if(i=this.state.findSessionByKey(s),!i)return void this.logger.warn?.("[acp-server] A2A result target session expired, discarding targetSessionKey=%s (known keys: %s)",s,[...this.state.sessionsByGatewayKey.keys()].join(", "));r=s,this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${i.sessionId}`)}else i=this.state.findCronTargetSession(),r=i?.gatewaySessionKey??`agent:${a}:a2a`,this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, using cron fallback key=${r}`);if(i&&!i.promptDone&&(void 0!==i.pendingPromptId||void 0!==i.currentRunId))return(i.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",i.pendingAgentSends.length);let o=e;if(this.canvasState)try{let e=this.canvasState.resolveCanvasForSession(r);if(this.logger.info?.(`[acp-server] agentSend canvas: sessionKey=${r} canvasId=${e} (from state)`),!e){const t=this.canvasState.getRelayClient();if(t)try{e=await t.ensureCanvas(r),this.canvasState.registerSession(r,e),this.logger.info?.(`[acp-server] agentSend canvas: WSS lookup result canvasId=${e}`)}catch{}}if(e){let t=this.canvasState.buildContextForCanvas(e);t||(await this.canvasState.lazyLoadCanvas(e),t=this.canvasState.buildContextForCanvas(e));o=`${CANVAS_TERM_MAPPING}\n\n${t??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${o}`,this.logger.info?.(`[acp-server] agentSend canvas: context injected (surfaceCtx=${t?"yes":"hint-only"})`)}else this.logger.warn?.(`[acp-server] agentSend canvas: NO canvasId resolved for session ${r} — canvas context NOT injected`)}catch(e){this.logger.warn?.("[acp-server] Canvas context injection failed: %s",e)}else this.logger.warn?.("[acp-server] agentSend: canvasState is null — no canvas injection");const c=await this.gateway.agentSend(r,o,a,t,n);return i&&c?.runId&&(this.state.resetForNewRun(i,i.sessionId,this.transport),i.currentRunId=c.runId,i.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(c.runId,i)),c}trackA2ARun(e,t,s){this.pendingA2ARuns.set(e,{taskId:t,completeFn:s})}cancelA2ARunByTaskId(e){for(const[t,s]of this.pendingA2ARuns)if(s.taskId===e)return this.pendingA2ARuns.delete(t),this.logger.info?.("[acp-server] Removed pending A2A run %s for cancelled task %s",t,e),!0;return!1}emitA2ASystemNotification(e){let t;if(e.targetSessionKey){const s=this.state.findSessionByKey(e.targetSessionKey);t=s?.sessionId,t||this.logger.debug?.("[acp-server] emitA2ASystemNotification: targetSessionKey=%s not found, falling back",e.targetSessionKey)}if(!t){const e=this.state.lastActiveSessionId,s=e?this.state.sessions.get(e):void 0;t=s?.sessionId??e}if(!t)return void this.logger.debug?.("[acp-server] emitA2ASystemNotification: no active session, dropping");const s={type:e.type,taskId:e.taskId,callerName:e.callerName,taskLink:`/a2a/tasks/${e.taskId}`};this.transport.sendSessionUpdate(t,{sessionUpdate:"system_message_chunk",content:{type:"text",text:e.message},metadata:s})}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch{}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,a){this.agentFetchFn=n,this.instanceId=a,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger),this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>{this.core.completePrompt(e,t),this.actionRouter?.drainPending().catch(e=>{this.logger.warn?.(`[acp-server] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}),this.drainNextDeferredSend(e)},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),this.actionRouter?.clearPending(),this.drainNextDeferredSend(e)},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn}),this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.onRunComplete=(e,t)=>{const s=this.pendingA2ARuns.get(e);s&&(this.pendingA2ARuns.delete(e),s.completeFn(t,"").catch(e=>this.logger.warn?.("[acp-server] A2A CompleteTask failed for task %s: %s",s.taskId,e)))},this.core.onRunFailed=(e,t)=>{const s=this.pendingA2ARuns.get(e);s&&(this.pendingA2ARuns.delete(e),s.completeFn("",t).catch(e=>this.logger.warn?.("[acp-server] A2A CompleteTask (error) failed for task %s: %s",s.taskId,e)))},this.core.handleCanvasNotification=(e,t)=>{this.handleCanvasNotificationInternal(e,t)},this.core.getCanvasState=()=>this.canvasState,this.core.onSessionLoaded=(e,t,s)=>{if(!this.config.canvasEnabled)return;const n=this.canvasState?.getRelayClient();if(!n)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:t}),void this.logger.info?.(`[acp-server] Relay not ready, queued canvas link: acpSid=${e} gwKey=${t}`);this.ensureCanvasForSession(e,t)},this.core.onSessionDeleted=e=>{this.canvasState?.removeSession(e)},this.core.onSessionCleared=(e,t)=>{const s=this.canvasState?.resolveCanvasForSession(t);s&&(this.canvasState.clear(s),this.actionRouter?.reset())},this.core.handleSlashCommand=async e=>{const t=this.descriptorRegistry?.getBySlashCommand(e);return!(!t||!this.resourceRenderer)&&(await this.resourceRenderer.render(t,"inline"),!0)},this.core.onPromptStart=()=>{this.actionRouter?.clearPending()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>this.canvasState?.removeSession(e.gatewaySessionKey))}async start(){const e=this.config.bridge.url,t=this.config.bridge.keyFile;if(e&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch{this.logger.warn?.("[acp-server] Bridge SDK not available")}await this.connectGateway(),e&&t&&BridgeClientCtor?await this.startBridgeMode(e,t):(this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone())}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});s.on("connected",()=>{this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync()}),s.on("disconnected",(...e)=>{const t=String(e[0]??"unknown");this.logger.warn?.(`[acp-server] Bridge disconnected: ${t}`),this.core.initialized=!1}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n,this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(){if(this.client&&this.client.readyState===WebSocket.OPEN)if(this.gateway&&this.agentFetchFn&&this.config.platformUrl)try{const e=await syncAgentsToPlatform({gateway:this.gateway,platformUrl:this.config.platformUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0},agentFetch:this.agentFetchFn,logger:this.logger});e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")}`)):this.core.markAgentSyncDone()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),this.core.markAgentSyncDone()}else this.core.markAgentSyncDone()}async connectGateway(){const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"})}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}isCronCorrelatedRun(e){return this.core.isCronCorrelatedRun(e)}setCanvasState(e,t){this.canvasState=e;const s=async(e,t)=>{if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)},n=()=>{const t=this.getSessionIdForToolCall();if(!t)return;const s=this.state.sessions.get(t),n=s?.gatewaySessionKey;return n?e.resolveCanvasForSession(n):void 0},a=t?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,gatewayRequest:s,resolveCanvasId:n,logger:this.logger});this.formBuilder=i,this.schemaRegistry=a,this.descriptorRegistry=t?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:s,schemaRegistry:a,formBuilder:i,resolveCanvasId:n,logger:this.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.logger,writeObs:e=>this.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const t=this.state.resolveSessionFromParams(e);t&&this.emitCanvasActionAsUserMessage(t.sessionId,e)}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),this.logger.debug?.("[acp-server] Canvas Relay action handler wired")):this.logger.warn?.("[acp-server] wireRelayActionHandler called but no relay client available")}isCanvasActive(e){const t=e??this.state.lastActiveSessionId,s=t?this.state.sessions.get(t):void 0;return!1!==s?.canvasActive}isCanvasActiveForGatewayKey(e){const t=this.state.sessionsByGatewayKey.get(e);return!1!==t?.canvasActive}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){if(this.pendingToolCallSessionKey){const t=this.state.findSessionByKey(this.pendingToolCallSessionKey);if(t?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,t.sessionId),t.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=this.state.toolCallIdMap.get(e);if(s&&s!==e){const e=this.state.toolCallToSessionMap.get(s);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${this.pendingToolCallSessionKey??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){this.pendingToolCallSessionKey=e}preMapToolCallSession(e){const t=this.pendingToolCallSessionKey;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0!==e.length){this.logger.info?.(`[acp-server] Flushing ${e.length} pending canvas link(s)`);for(const{acpSessionId:t,gatewaySessionKey:s}of e)this.ensureCanvasForSession(t,s)}}ensureCanvasForSession(e,t){const s=this.canvasState?.getRelayClient();if(!s)return;const n=e!==t?e:void 0;s.ensureCanvas(t,{linkSessionId:n}).then(async s=>{this.canvasState?.registerSession(t,s),await(this.canvasState?.joinCanvas(s,t)),this.logger.info?.(`[acp-server] Canvas ensured: canvas=${s} acpSid=${e} gwKey=${t}`)}).catch(s=>{this.logger.warn?.(`[acp-server] Failed to ensure canvas: acpSid=${e} gwKey=${t} err=${s}`)})}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(s?.agentId??"")??s?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.activeMentions}getResourceRenderer(){return this.resourceRenderer}getDescriptorRegistry(){return this.descriptorRegistry}getFormBuilder(){return this.formBuilder}getSchemaRegistry(){return this.schemaRegistry}async forwardActionToAgent(e){const t=this.state.resolveSessionFromParams(e);if(!t)return void this.logger.warn?.("[acp-server] forwardActionToAgent dropped — no session resolved from params");if(!this.gateway?.isConnected)return;if(t.currentRunId&&!t.promptDone)return this.actionRouter?.enqueue(e),void this.logger.debug?.("[acp-server] Agent busy, canvas action queued");this.emitCanvasActionAsUserMessage(t.sessionId,e),this.state.resetForNewRun(t,t.sessionId,this.transport);const s=this.canvasState?.summarize(e.surfaceId??""),n=formatActionForAgent(e,s??void 0);try{const e=await this.gateway.agentSend(t.gatewaySessionKey,n,this.config.gateway.agentId||this.core.getDefaultAgentId());t.currentRunId=e.runId,t.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(e.runId,t)}catch(s){this.logger.warn?.(`[acp-server] Agent forward failed: ${s instanceof Error?s.message:s}`),this.transport.sendSessionUpdate(t.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,t){const s=t.userAction?.name??"unknown",n=`Clicked "${s}"`;this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:n},metadata:{type:"canvas_action",surfaceId:t.surfaceId,actionName:s,sourceComponentId:t.userAction?.sourceComponentId,actionPayload:t.userAction?.context,actionTimestamp:t.userAction?.timestamp}})}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.logger.warn?.("[acp-server] Relay action ignored — canvas not initialized");const{canvasId:t,surfaceId:s,userAction:n,requestId:a,senderClientId:i}=e,r=this.canvasState.resolveSessionForCanvas(t);if(!r)return this.logger.warn?.(`[acp-server] Relay action ignored — no session for canvas=${t}`),void this.sendRelayActionError(t,a,i,"no_session");const o=this.state.findSessionByKey(r);if(!o)return this.logger.warn?.(`[acp-server] Relay action ignored — session not found for gatewayKey=${r}`),void this.sendRelayActionError(t,a,i,"no_session");const c={surfaceId:s,userAction:{name:n.name,sourceComponentId:n.sourceComponentId,timestamp:n.timestamp,context:n.context},sessionId:o.sessionId};this.actionRouter.route(c,t).then(()=>{this.sendRelayActionResult(t,a,i,"ok")}).catch(e=>{this.logger.warn?.(`[acp-server] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(t,a,i,"processing_failed")}),this.logger.debug?.(`[acp-server] Relay action routed: canvas=${t} surface=${s} action=${n.name} session=${o.sessionId}`)}sendRelayActionResult(e,t,s,n){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:t,targetClientId:s,status:n})}sendRelayActionError(e,t,s,n){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:t,targetClientId:s,error:n})}handleCanvasNotificationInternal(e,t){if(!this.canvasState)return void this.logger.warn?.(`[acp-server] Canvas notification ${e} ignored — canvasState not initialized (canvasEnabled=${this.config.canvasEnabled})`);this.canvasState,this.logger;if("canvas/toggle"===e){const e=t,s=!1!==e.enabled,n=this.state.resolveSessionFromParams(e);if(n&&(n.canvasActive=s,s&&(n.canvasPromptInjected=!1,n.sessionId))){const e=n.sessionId,t=n.gatewaySessionKey,s=n.agentId;this.core.waitForAgentMapping().then(()=>{const a=this.core.resolvePlatformAgentId(s??"")??n.platformAgentId,i=t??e,r=t&&t!==e?e:void 0,o=this.canvasState?.getRelayClient();o&&o.ensureCanvas(i,{agentId:a,linkSessionId:r}).catch(e=>{this.logger.warn?.(`[acp-server] Failed to pre-create canvas on toggle: ${e}`)})})}this.logger.info?.(`[acp-server] Canvas toggled: ${s}`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:s}})}else if("canvas/clear"===e){const e=t,s=this.state.resolveSessionFromParams(e),n=s?this.canvasState.resolveCanvasForSession(s.gatewaySessionKey):void 0,a=n?this.canvasState.getAllActive(n).length:0;n&&this.canvasState.clear(n),this.actionRouter?.reset(),this.logger.info?.(`[acp-server] Canvas cleared (${a} surfaces removed)`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const t=e,s=t.action;if("a2ui_push"===s||"surface_update"===s){const e=String(t.surfaceId??`openclaw-${Date.now()}`),s=t.components;if(s&&Array.isArray(s)){const n=sanitizeA2UIComponents(s),a=t.canvasId?String(t.canvasId):void 0;this.canvasState.has(e,a)?this.canvasState.update(e,n,a):this.canvasState.create(a??`_openclaw_${Date.now()}`,e,"inline",n,{origin:{kind:"openclaw"}}),this.logger.debug?.(`[acp-server] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===s||"url_open"===s){const e=String(t.url??"");if(e){const s=`openclaw-nav-${Date.now()}`,n=String(t.canvasId??`_openclaw_nav_${Date.now()}`),a=[{id:`${s}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(t.title??e)}}}}];this.canvasState.create(n,s,"inline",a,{origin:{kind:"openclaw"}})}}else this.logger.debug?.(`[acp-server] Unhandled OpenClaw canvas action: ${s}`)}async stop(){this.state.destroy(),this.pendingA2ARuns.clear(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop()),this.client&&(this.client.removeAllListeners(),this.client.close()),this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close()),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
@@ -0,0 +1 @@
1
+ export async function syncAgentsToPlatform(t){const{gateway:n,platformUrl:e,capabilities:a,agentFetch:s,logger:i}=t;if(!s)return i?.debug?.("[agent-sync] no agentFetch — skipping sync (no key pair)"),null;if(!e)return i?.debug?.("[agent-sync] no platformUrl — skipping sync"),null;try{const t=await n.request("agents.list",{}),r=t?.agents??[];if(0===r.length)return i?.debug?.("[agent-sync] no agents from Gateway — skipping sync"),null;const o=[];for(const t of r){let e=[];try{const a=await n.request("skills.status",{agentId:t.id});e=(a?.skills??[]).filter(t=>t.eligible&&!t.disabled).map(t=>({name:t.name,description:t.description,...t.emoji?{emoji:t.emoji}:{}}))}catch{i?.debug?.(`[agent-sync] skills.status failed for agent ${t.id} — continuing without skills`)}const s={};if(a)for(const[t,n]of Object.entries(a))n&&(s[t]=!0);o.push({name:t.id,description:t.name||t.identity?.name||t.id,version:"0.1.0",capabilities:s,skills:e,avatarUrl:t.identity?.avatarUrl||"",homepage:""})}const c=new URL("/api/v1/agents/sync",e),l=await s(c,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agents:o})});if(l.ok){const t=await l.json();i?.info?.(`[agent-sync] synced ${o.length} agent(s) — created=${t.created??0} updated=${t.updated??0} deactivated=${t.deactivated??0}`);const n=new Map,e=Array.isArray(t.data)?t.data:[];for(const t of e)t&&"string"==typeof t.name&&"string"==typeof t.id&&n.set(t.name,{id:t.id,instanceId:t.instanceId??"",name:t.name,displayName:t.displayName??t.name,description:t.description??"",version:t.version??"",avatarUrl:t.avatarUrl??"",status:t.status??"active"});return n.size>0?n:null}{const t=await l.text().catch(()=>"");return i?.warn?.(`[agent-sync] sync failed: HTTP ${l.status} — ${t.slice(0,200)}`),null}}catch(t){return i?.warn?.(`[agent-sync] sync error: ${t instanceof Error?t.message:String(t)}`),null}}
@@ -1 +1 @@
1
- export class CanvasActionRouter{deps;lastActionTime=new Map;pendingActions=[];static DEBOUNCE_MS=300;static MAX_PENDING=5;constructor(t){this.deps=t}async route(t){const n=t.surfaceId??"",e=Date.now();if(e-(this.lastActionTime.get(n)??0)<CanvasActionRouter.DEBOUNCE_MS)return void this.deps.logger.debug?.(`[action-router] Debounced action on ${n}`);this.lastActionTime.set(n,e);const i=this.deps.stateManager.getActionHandler(n);if(i)try{await i(t),this.emitObs(t,"bound_handler")}catch(e){this.emitObs(t,"bound_handler_error"),this.deps.logger.warn?.(`[action-router] Bound handler for ${n} threw: ${e instanceof Error?e.message:e}`)}else this.emitObs(t,"agent_forward"),await this.deps.forwardToAgent(t)}enqueue(t){this.pendingActions.length>=CanvasActionRouter.MAX_PENDING&&this.pendingActions.shift(),this.pendingActions.push(t)}async drainPending(){if(0===this.pendingActions.length)return;const t=this.deduplicatePending();this.pendingActions=[];for(const n of t)await this.deps.forwardToAgent(n)}clearPending(){this.pendingActions=[]}reset(){this.lastActionTime.clear(),this.pendingActions=[]}deduplicatePending(){if(this.pendingActions.length<=1)return[...this.pendingActions];const t=new Map;for(const n of this.pendingActions)t.set(n.surfaceId??"",n);return Array.from(t.values())}emitObs(t,n){this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.action_routed",severity:n.endsWith("_error")?"warn":"info",payload:{surfaceId:t.surfaceId,actionName:t.userAction?.name,resolution:n}})}}export function formatActionForAgent(t,n){const e=t.surfaceId??"unknown",i=t.userAction?.name??"unknown",s=t.userAction?.context;let o=`[Canvas Interaction] The user clicked "${i}" on surface "${e}"`;if(n?.title&&(o+=` ("${n.title}")`),o+=".",n?.types?.length&&(o+=` Surface contains: ${n.types.join(", ")}.`),s&&Object.keys(s).length>0){const t=JSON.stringify(s);o+=`\nAction context: ${t.length>2e3?t.slice(0,2e3)+"…(truncated)":t}`}return o+=`\nRespond to this interaction. If the surface needs updating, use render_ui with id="${e}".`,o}
1
+ export class CanvasActionRouter{deps;lastActionTime=new Map;pendingActions=[];static DEBOUNCE_MS=300;static MAX_PENDING=5;constructor(t){this.deps=t}async route(t,n){const e=t.surfaceId??"",i=Date.now();if(i-(this.lastActionTime.get(e)??0)<CanvasActionRouter.DEBOUNCE_MS)return void this.deps.logger.debug?.(`[action-router] Debounced action on ${e}`);this.lastActionTime.set(e,i);const s=this.deps.stateManager.getActionHandler(e,n);if(s)try{this.deps.emitActionAsUserMessage?.(t),await s(t),this.emitObs(t,"bound_handler")}catch(n){throw this.emitObs(t,"bound_handler_error"),this.deps.logger.warn?.(`[action-router] Bound handler for ${e} threw: ${n instanceof Error?n.message:n}`),n}else this.emitObs(t,"agent_forward"),await this.deps.forwardToAgent(t)}enqueue(t){this.pendingActions.length>=CanvasActionRouter.MAX_PENDING&&this.pendingActions.shift(),this.pendingActions.push(t)}async drainPending(){if(0===this.pendingActions.length)return;const t=this.deduplicatePending();this.pendingActions=[];for(const n of t)await this.deps.forwardToAgent(n)}clearPending(){this.pendingActions=[]}reset(){this.lastActionTime.clear(),this.pendingActions=[]}deduplicatePending(){if(this.pendingActions.length<=1)return[...this.pendingActions];const t=new Map;for(const n of this.pendingActions)t.set(n.surfaceId??"",n);return Array.from(t.values())}emitObs(t,n){this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.action_routed",severity:n.endsWith("_error")?"warn":"info",payload:{surfaceId:t.surfaceId,actionName:t.userAction?.name,resolution:n}})}}export function formatActionForAgent(t,n){const e=t.surfaceId??"unknown",i=t.userAction?.name??"unknown",s=t.userAction?.context;let o=`[Canvas Interaction] The user clicked "${i}" on surface "${e}"`;if(n?.title&&(o+=` ("${n.title}")`),o+=".",n?.types?.length&&(o+=` Surface contains: ${n.types.join(", ")}.`),s&&Object.keys(s).length>0){const t=JSON.stringify(s);o+=`\nAction context: ${t.length>2e3?t.slice(0,2e3)+"…(truncated)":t}`}return o+=`\nRespond to this interaction. If the surface needs updating, use render_ui with id="${e}".`,o}
@@ -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}async createSnapshot(s,a){return this.request("POST",`/api/v1/canvas/${s}/snapshots`,{description:a??"Pre-agent edit"})}}
1
+ import{Buffer}from"node:buffer";import{agentFetch}from"./agent-auth.js";export class CanvasApiClient{baseUrl;keys;constructor(a){this.baseUrl=a.platformUrl.replace(/\/+$/,""),this.keys=a.agentKeys}async request(a,t,s){const e=await agentFetch(`${this.baseUrl}${t}`,this.keys,{method:a,headers:{"Content-Type":"application/json"},body:s?JSON.stringify(s):void 0}),n=await e.json();if(!n.success)throw new Error(n.error??`Canvas API ${a} ${t} failed: ${e.status}`);return n.data??null}async getCanvasForSession(a){try{return await this.request("GET",`/api/v1/canvas-session/${a}`)}catch(a){const t=a instanceof Error?a.message:String(a);if(t.includes("404")||t.includes("not found")||t.includes("null"))return null;throw a}}async createCanvas(a){const t=await this.request("POST","/api/v1/canvas/agent",{title:a?.title??"Agent Canvas",sessionId:a?.sessionId,...a?.agentId?{agentId:a.agentId}:{}});if(!t)throw new Error("Canvas creation returned no data");return t}async linkChat(a,t){await this.request("POST",`/api/v1/canvas/${a}/link`,{sessionId:t})}async ensureCanvasForSession(a,t){const s=t?.gatewaySessionKey;if(s){const e=await this.getCanvasForSession(s);if(e)return s!==a&&this.linkChat(e.canvasId,a).catch(()=>{}),t?.agentId&&this.backfillCollaboratorAgentId(e.canvasId,t.agentId).catch(()=>{}),e.canvasId}if(!s||s===a){const s=await this.getCanvasForSession(a);if(s)return t?.agentId&&this.backfillCollaboratorAgentId(s.canvasId,t.agentId).catch(()=>{}),s.canvasId}const e=s??a,n=await this.createCanvas({sessionId:e,agentId:t?.agentId});return s&&s!==a&&this.linkChat(n.id,a).catch(()=>{}),n.id}async backfillCollaboratorAgentId(a,t){const s=Buffer.from(this.keys.publicKey).toString("base64");await this.request("POST",`/api/v1/canvas/${a}/collaborators`,{collaboratorType:"agent",collaboratorId:s,role:"owner",agentId:t})}async createSnapshot(a,t){return this.request("POST",`/api/v1/canvas/${a}/snapshots`,{description:t??"Pre-agent edit"})}}
@@ -0,0 +1 @@
1
+ import{randomUUID}from"node:crypto";import WebSocket from"ws";import{agentAuthHeaders}from"./agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./canvas-constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){if(this.closed)return;const e="/ws/agent";let t=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`;this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),this.opts.logger?.info?.(`[canvas-relay] Connecting to ${t}`);const s=agentAuthHeaders("GET",e,this.opts.agentKeys),n=new WebSocket(t,{headers:s});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=n,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[]}),n.on("message",(e,t)=>{if(!t&&"string"!=typeof e){const t=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(t)}if("string"==typeof e)return void this.handleTextMessage(e);const s=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(s)}),n.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),n.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),n.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e)}async joinCanvas(e,t){if(this.canvasHandlers.set(e,t),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((t,s)=>{this.pendingJoins.set(e,{resolve:t,reject:s}),this.sendJoinCommand(e);const n=setTimeout(()=>{const t=this.pendingJoins.get(e);t&&(this.pendingJoins.delete(e),t.resolve())},5e3),o=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(n),o.resolve()},reject:e=>{clearTimeout(n),o.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendControlMessage({type:"leave",canvasId:e})}sendUpdate(e,t){this.sendTagged(e,t)}sendAwareness(e,t){this.sendTagged(e,t)}async ensureCanvas(e,t){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:t?.agentId,title:t?.title,linkSessionId:t?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,t){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:t})).snapshotId??null}onAction(e){this.actionHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){const t=randomUUID();return e.reqId=t,new Promise((s,n)=>{const o=setTimeout(()=>{this.pendingRequests.delete(t),n(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(t,{resolve:s,reject:n,timer:o}),this.sendControlMessage(e)})}sendTagged(e,t){if(!this.connected)return void this.pendingData.push({canvasId:e,data:t});const s=(new TextEncoder).encode(e),n=new Uint8Array(1+s.length+t.length);n[0]=s.length,n.set(s,1),n.set(t,1+s.length),this.ws.send(n)}handleTextMessage(e){try{const t=JSON.parse(e),s=t.type;if(s?.endsWith(":result")&&t.reqId){const e=this.pendingRequests.get(t.reqId);return void(e&&(this.pendingRequests.delete(t.reqId),clearTimeout(e.timer),t.error?e.reject(new Error(t.error)):e.resolve(t)))}switch(s){case"joined":{const e=this.pendingJoins.get(t.canvasId??"");e&&(this.pendingJoins.delete(t.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${t.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${t.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${t.canvasId}: ${t.message}`);break;case"action":this.actionHandler&&this.actionHandler(t);break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${s}`)}}catch{this.opts.logger?.warn?.("[canvas-relay] Failed to parse control message")}}handleBinaryMessage(e){if(e.length<2)return;const t=e[0];if(e.length<1+t+1)return;const s=(new TextDecoder).decode(e.slice(1,1+t)),n=e.slice(1+t),o=this.canvasHandlers.get(s);if(!o)return;const a=n[0];a===MSG_SYNC&&o.onUpdate?o.onUpdate(n):a===MSG_AWARENESS&&o.onAwareness&&o.onAwareness(n)}sendJoinCommand(e){this.sendControlMessage({type:"join",canvasId:e})}sendControlMessage(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
@@ -1 +1 @@
1
- import{CanvasWSProvider}from"./canvas-ws-provider.js";import{MSG_SYNC,MSG_SYNC_UPDATE}from"./canvas-constants.js";const MAX_SURFACES_PER_SESSION=20,MAX_STATE_MAP_BYTES=2097152,MAX_COMPONENTS_PER_SURFACE=500,RATE_LIMIT_PER_SURFACE_PER_SEC=30;export class CanvasStateManager{surfaces=new Map;actionHandlers=new Map;transport=null;activeCanvasId=null;lastResolvedSessionId=null;relayOpts=null;transportFactory;setRelayOpts(e){this.relayOpts=e}setTransportFactory(e){this.transportFactory=e}createTransport(e){return this.transportFactory?this.transportFactory(e):new CanvasWSProvider(e)}connectForCanvas(e,t){if(this.activeCanvasId===e&&this.transport?.connected)return void(t&&(this.lastResolvedSessionId=t));if(!this.relayOpts)return;this.transport&&this.transport.disconnect(),this.activeCanvasId&&this.activeCanvasId!==e&&this.clear(),this.activeCanvasId=e,this.lastResolvedSessionId=t??null,this.transport=this.createTransport(this.relayOpts);const s=this.relayOpts.logger;this.transport.connect(e,{onUpdate:e=>{s?.debug?.(`[canvas-state] Received update from relay: ${e.length} bytes`),this.processIncomingRelayMessage(e)},onAwareness:e=>{s?.debug?.(`[canvas-state] Received awareness from relay: ${e.length} bytes`)}})}disconnectFromCanvasService(){this.transport&&(this.transport.disconnect(),this.transport=null),this.activeCanvasId=null,this.lastResolvedSessionId=null}get canvasServiceConnected(){return null!==this.activeCanvasId&&null!==this.transport}get currentCanvasId(){return this.activeCanvasId}get currentSessionId(){return this.lastResolvedSessionId}syncDeleteToRelay(e){if(this.transport)try{const t=JSON.stringify({surfaceId:e,deleted:!0}),s=(new TextEncoder).encode(t),r=new Uint8Array(2+s.length);r[0]=MSG_SYNC,r[1]=MSG_SYNC_UPDATE,r.set(s,2),this.transport.sendUpdate(r)}catch{}}syncSurfaceToRelay(e,t,s){if(this.transport)try{const r=JSON.stringify({surfaceId:e,components:t,presentation:s??"panel"}),a=(new TextEncoder).encode(r),n=new Uint8Array(2+a.length);n[0]=MSG_SYNC,n[1]=MSG_SYNC_UPDATE,n.set(a,2),this.transport.sendUpdate(n),this.relayOpts?.logger?.info?.(`[canvas-state] Queued/sent ${n.length} bytes to relay for surfaceId=${e}`)}catch(e){this.relayOpts?.logger?.warn?.(`[canvas-state] syncSurfaceToRelay failed: ${e}`)}}processIncomingRelayMessage(e){if(!(e.length<3)&&e[0]===MSG_SYNC&&e[1]===MSG_SYNC_UPDATE)try{const t=(new TextDecoder).decode(e.subarray(2)),s=JSON.parse(t);if(!s.surfaceId)return;s.deleted&&(this.removeSurface(s.surfaceId),this.relayOpts?.logger?.info?.(`[canvas-state] Surface ${s.surfaceId} deleted by remote participant`))}catch{}}has(e){return this.surfaces.has(e)}get(e){return this.surfaces.get(e)}getAll(){return Array.from(this.surfaces.values())}getAllActive(){return Array.from(this.surfaces.values())}create(e,t,s,r){this.enforceMaxSurfaces();const a={surfaceId:e,presentation:t,components:s,dataModels:new Map,origin:r?.origin??{kind:"agent"},createdAt:Date.now(),updatedAt:Date.now(),tokenBucket:{tokens:30,lastRefill:Date.now()}};return this.surfaces.set(e,a),r?.actionHandler&&this.actionHandlers.set(e,r.actionHandler),this.enforceMemoryBudget(),this.syncSurfaceToRelay(e,s,t),a}update(e,t){const s=this.surfaces.get(e);if(s)return s.components=t,s.updatedAt=Date.now(),this.enforceMemoryBudget(),this.syncSurfaceToRelay(e,t,s.presentation),s}delete(e){const t=this.removeSurface(e);return t&&this.syncDeleteToRelay(e),t}clear(){this.actionHandlers.clear(),this.surfaces.clear()}get size(){return this.surfaces.size}getActionHandler(e){return this.actionHandlers.get(e)}bindActionHandler(e,t){this.actionHandlers.set(e,t)}summarize(e){const t=this.surfaces.get(e);if(!t||!Array.isArray(t.components))return null;const s=t.components,r=[...new Set(s.map(e=>e.type).filter(Boolean))],a=s.find(e=>e.properties?.title);return{types:r,title:a?.properties?.title}}checkRateLimit(e){const t=this.surfaces.get(e);if(!t)return!0;const s=Date.now(),r=(s-t.tokenBucket.lastRefill)/1e3;return t.tokenBucket.tokens=Math.min(30,t.tokenBucket.tokens+30*r),t.tokenBucket.lastRefill=s,t.tokenBucket.tokens>=1&&(t.tokenBucket.tokens-=1,!0)}validateComponentCount(e){return!Array.isArray(e)||e.length<=500}removeSurface(e){return this.actionHandlers.delete(e),this.surfaces.delete(e)}enforceMaxSurfaces(){if(this.surfaces.size<20)return;let e,t=1/0;for(const[s,r]of this.surfaces)r.createdAt<t&&(t=r.createdAt,e=s);e&&this.removeSurface(e)}enforceMemoryBudget(){let e=0;for(const[,t]of this.surfaces)e+=this.estimateSize(t);if(e<=2097152)return;const t=Array.from(this.surfaces.entries()).sort(([,e],[,t])=>e.updatedAt-t.updatedAt);for(const[s,r]of t){if(e<=2097152)break;e-=this.estimateSize(r),this.removeSurface(s)}}estimateSize(e){try{let t=2*JSON.stringify(e.components).length;if(e.dataModels.size>0)for(const s of e.dataModels.values())t+=2*JSON.stringify(s).length;return t}catch{return 1024}}}
1
+ import{CanvasWSProvider}from"./canvas-ws-provider.js";import{MSG_SYNC,MSG_SYNC_UPDATE,MSG_AWARENESS}from"./canvas-constants.js";const MAX_CACHED_CANVASES=20,MAX_SURFACES_PER_CANVAS=20,MAX_BYTES_PER_CANVAS=524288,MAX_TOTAL_BYTES=4194304,MAX_COMPONENTS_PER_SURFACE=500,RATE_LIMIT_PER_SURFACE_PER_SEC=30;export class CanvasStateManager{canvasEntries=new Map;sessionMap=new Map;canvasSessionMap=new Map;relayClient=null;joinedCanvases=new Set;agentId;editingClearTimer=null;relayOpts=null;transportFactory;logger;setRelayClient(e){this.relayClient=e}getRelayClient(){return this.relayClient}setAgentId(e){this.agentId=e}setRelayOpts(e){this.relayOpts=e,this.logger=e.logger}setTransportFactory(e){this.transportFactory=e}createTransport(e){return this.transportFactory?this.transportFactory(e):new CanvasWSProvider(e)}touchEntry(e){let t=this.canvasEntries.get(e);return t?(this.canvasEntries.delete(e),t.lastAccessed=Date.now(),this.canvasEntries.set(e,t)):(t={canvasId:e,surfaces:new Map,actionHandlers:new Map,lastAccessed:Date.now(),cachedBytes:0},this.canvasEntries.set(e,t),this.enforceCanvasCountLimit()),t}findEntryBySurfaceId(e){for(const t of this.canvasEntries.values())if(t.surfaces.has(e))return t}async joinCanvas(e,t){t&&this.registerSession(t,e),this.joinedCanvases.has(e)||(this.touchEntry(e),this.relayClient&&(await this.relayClient.joinCanvas(e,{onUpdate:t=>{this.logger?.debug?.(`[canvas-state] Received update from relay for canvas=${e}: ${t.length} bytes`);const s=this.canvasEntries.get(e);s&&this.processRelayMessageForEntry(s,t)},onAwareness:t=>{this.logger?.debug?.(`[canvas-state] Received awareness from relay for canvas=${e}: ${t.length} bytes`)}}),this.joinedCanvases.add(e)))}leaveCanvas(e){this.joinedCanvases.delete(e),this.relayClient?.leaveCanvas(e)}isCanvasJoined(e){return this.joinedCanvases.has(e)}get canvasServiceConnected(){return this.relayClient?.connected??!1}registerSession(e,t){this.sessionMap.set(e,t),this.canvasSessionMap.set(t,e)}resolveCanvasForSession(e){return this.sessionMap.get(e)}resolveSessionForCanvas(e){return this.canvasSessionMap.get(e)}removeSession(e){const t=this.sessionMap.get(e);this.sessionMap.delete(e),t&&([...this.sessionMap.values()].includes(t)||(this.canvasSessionMap.delete(t),this.canvasEntries.delete(t),this.leaveCanvas(t),this.relayClient?.deleteCanvas(t).catch(e=>{this.logger?.warn?.(`[canvas-state] Failed to delete canvas ${t}: ${e}`)})))}async lazyLoadCanvas(e,t=3e3){const s=this.canvasEntries.get(e);if(s&&s.surfaces.size>0)return!0;if(this.relayClient){const s=this.touchEntry(e);return await this.joinCanvas(e),await new Promise(e=>setTimeout(e,Math.min(t,1500))),s.surfaces.size>0}if(!this.relayOpts)return!1;const n=this.touchEntry(e),a=this.createTransport(this.relayOpts);return new Promise(s=>{let r=0,i=!1;const o=e=>{i||(i=!0,clearTimeout(c),clearInterval(l),a.disconnect(),this.recomputeEntryBytes(n),s(e))},c=setTimeout(()=>o(n.surfaces.size>0),t),l=setInterval(()=>{r>0&&Date.now()-r>500&&o(!0)},100);a.connect(e,{onUpdate:e=>{r=Date.now(),this.processRelayMessageForEntry(n,e)}})})}static MAX_CTX_SURFACES=8;static MAX_CTX_CHARS=500;buildContextForCanvas(e){const t=this.canvasEntries.get(e);if(!t||0===t.surfaces.size)return null;this.touchEntry(e);const s=[...t.surfaces.values()].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,CanvasStateManager.MAX_CTX_SURFACES),n=t.surfaces.size-s.length;let a=`[Canvas State] Active surfaces: ${s.map(e=>{const s=this.summarizeFromEntry(t,e.surfaceId);if(!s)return`"${e.surfaceId}"`;const n=s.types.slice(0,3).join(", "),a=s.title?`: "${s.title}"`:"";return`"${e.surfaceId}" (${n}${a})`}).join("; ")}.`;return n>0&&(a+=` ...and ${n} more.`),a+=" To modify existing content, reuse its id in render_ui.",a.length>CanvasStateManager.MAX_CTX_CHARS&&(a=a.slice(0,CanvasStateManager.MAX_CTX_CHARS-3)+"..."),a}has(e,t){const s=t?this.canvasEntries.get(t):this.findEntryBySurfaceId(e);return s?.surfaces.has(e)??!1}get(e){return this.findEntryBySurfaceId(e)?.surfaces.get(e)}getAll(e){const t=this.canvasEntries.get(e);return t?Array.from(t.surfaces.values()):[]}getAllActive(e){return this.getAll(e)}create(e,t,s,n,a){let r=this.canvasEntries.get(e);return r||(r=this.touchEntry(e)),this.createInEntry(r,t,s,n,a)}createInEntry(e,t,s,n,a){this.enforceMaxSurfacesForEntry(e);const r=Date.now(),i={surfaceId:t,presentation:s,components:n,rootComponentId:a?.rootComponentId,dataModels:new Map,origin:a?.origin??{kind:"agent"},createdAt:r,updatedAt:r,tokenBucket:{tokens:30,lastRefill:r}};return a?.dataModel&&i.dataModels.set(t,a.dataModel),e.surfaces.set(t,i),a?.actionHandler&&e.actionHandlers.set(t,a.actionHandler),this.recomputeEntryBytes(e),this.enforceEntryMemoryBudget(e),this.enforceGlobalMemoryBudget(),this.syncSurfaceToRelay(e.canvasId,t,n,s,i.rootComponentId,a?.dataModel),i}update(e,t,s,n,a){const r=s?this.canvasEntries.get(s):this.findEntryBySurfaceId(e);if(!r)return;const i=r.surfaces.get(e);return i?(i.components=t,n&&(i.rootComponentId=n),a&&i.dataModels.set(e,a),i.updatedAt=Date.now(),this.recomputeEntryBytes(r),this.enforceEntryMemoryBudget(r),this.enforceGlobalMemoryBudget(),this.syncSurfaceToRelay(r.canvasId,e,t,i.presentation,i.rootComponentId,a),i):void 0}delete(e,t){const s=t?this.canvasEntries.get(t):this.findEntryBySurfaceId(e);if(!s)return!1;const n=this.removeSurfaceFromEntry(s,e);return n&&(this.recomputeEntryBytes(s),this.syncDeleteToRelay(s.canvasId,e)),n}clear(e){const t=this.canvasEntries.get(e);if(t){for(const s of t.surfaces.keys())this.syncDeleteToRelay(e,s);t.actionHandlers.clear(),t.surfaces.clear(),t.cachedBytes=0}}sizeOf(e){return this.canvasEntries.get(e)?.surfaces.size??0}getActionHandler(e,t){const s=t?this.canvasEntries.get(t):this.findEntryBySurfaceId(e);return s?.actionHandlers.get(e)}bindActionHandler(e,t,s){const n=s?this.canvasEntries.get(s):this.findEntryBySurfaceId(e);n&&n.actionHandlers.set(e,t)}summarize(e){const t=this.findEntryBySurfaceId(e);return t?this.summarizeFromEntry(t,e):null}summarizeFromEntry(e,t){const s=e.surfaces.get(t);if(!s||!Array.isArray(s.components))return null;const n=s.components,a=[];let r;for(const e of n){if(!e.component)continue;const t=Object.keys(e.component)[0];if(t&&a.push(t),"Text"===t){const t=e.component.Text,s=t?.usageHint;if(!r&&s&&["h1","h2","h3","h4"].includes(s)){const e=t?.text;e?.literalString&&(r=e.literalString)}}}return{types:[...new Set(a)],title:r}}checkRateLimit(e){const t=this.findEntryBySurfaceId(e);if(!t)return!0;const s=t.surfaces.get(e);if(!s)return!0;const n=Date.now(),a=(n-s.tokenBucket.lastRefill)/1e3;return s.tokenBucket.tokens=Math.min(30,s.tokenBucket.tokens+30*a),s.tokenBucket.lastRefill=n,s.tokenBucket.tokens>=1&&(s.tokenBucket.tokens-=1,!0)}validateComponentCount(e){return!Array.isArray(e)||e.length<=500}syncDeleteToRelay(e,t){if(this.relayClient)try{const s=JSON.stringify({surfaceId:t,deleted:!0}),n=(new TextEncoder).encode(s),a=new Uint8Array(2+n.length);a[0]=MSG_SYNC,a[1]=MSG_SYNC_UPDATE,a.set(n,2),this.relayClient.sendUpdate(e,a)}catch{}}syncSurfaceToRelay(e,t,s,n,a,r){if(this.relayClient){try{const i={surfaceId:t,components:s,presentation:n??"panel"};a&&(i.root=a),r&&Object.keys(r).length>0&&(i.dataModel=r);const o=JSON.stringify(i),c=(new TextEncoder).encode(o),l=new Uint8Array(2+c.length);l[0]=MSG_SYNC,l[1]=MSG_SYNC_UPDATE,l.set(c,2),this.relayClient.sendUpdate(e,l),this.logger?.info?.(`[canvas-state] Queued/sent ${l.length} bytes to relay for canvas=${e} surfaceId=${t}`)}catch(e){this.logger?.warn?.(`[canvas-state] syncSurfaceToRelay failed: ${e}`)}this.sendEditingAwareness(e,t)}else this.logger?.warn?.(`[canvas-state] syncSurfaceToRelay: relayClient is null for surfaceId=${t}, data NOT synced to relay`)}sendEditingAwareness(e,t){if(this.relayClient){this.editingClearTimer&&(clearTimeout(this.editingClearTimer),this.editingClearTimer=null);try{const s=JSON.stringify({agentId:this.agentId??null,editingSurface:t,timestamp:Date.now()}),n=(new TextEncoder).encode(s),a=new Uint8Array(1+n.length);a[0]=MSG_AWARENESS,a.set(n,1),this.relayClient.sendAwareness(e,a)}catch{}this.editingClearTimer=setTimeout(()=>{this.editingClearTimer=null,this.sendClearEditingAwareness(e)},3e3)}}sendClearEditingAwareness(e){if(this.relayClient)try{const t=JSON.stringify({agentId:this.agentId??null,editingSurface:"",timestamp:Date.now()}),s=(new TextEncoder).encode(t),n=new Uint8Array(1+s.length);n[0]=MSG_AWARENESS,n.set(s,1),this.relayClient.sendAwareness(e,n)}catch{}}processRelayMessageForEntry(e,t){if(!(t.length<3)&&t[0]===MSG_SYNC&&t[1]===MSG_SYNC_UPDATE)try{const s=(new TextDecoder).decode(t.subarray(2)),n=JSON.parse(s);if(!n.surfaceId)return;if(n.deleted)return this.removeSurfaceFromEntry(e,n.surfaceId),this.recomputeEntryBytes(e),void this.logger?.info?.(`[canvas-state] Surface ${n.surfaceId} deleted by remote participant`);if(n.components&&!e.surfaces.has(n.surfaceId)){const t="inline"===n.presentation||"panel"===n.presentation?n.presentation:"panel",s=Date.now(),a={surfaceId:n.surfaceId,presentation:t,components:n.components,dataModels:new Map,origin:{kind:"agent"},createdAt:s,updatedAt:s,tokenBucket:{tokens:30,lastRefill:s}};e.surfaces.set(n.surfaceId,a),this.recomputeEntryBytes(e),this.logger?.info?.(`[canvas-state] Restored surface ${n.surfaceId} from relay`)}}catch{}}removeSurfaceFromEntry(e,t){return e.actionHandlers.delete(t),e.surfaces.delete(t)}enforceMaxSurfacesForEntry(e){if(e.surfaces.size<20)return;let t,s=1/0;for(const[n,a]of e.surfaces)a.createdAt<s&&(s=a.createdAt,t=n);t&&this.removeSurfaceFromEntry(e,t)}recomputeEntryBytes(e){let t=0;for(const s of e.surfaces.values())t+=this.estimateSize(s);e.cachedBytes=t}enforceEntryMemoryBudget(e){if(e.cachedBytes<=524288)return;const t=[...e.surfaces.entries()].sort(([,e],[,t])=>e.updatedAt-t.updatedAt);for(const[s]of t){if(e.cachedBytes<=524288)break;const t=e.surfaces.get(s);t&&(e.cachedBytes-=this.estimateSize(t)),this.removeSurfaceFromEntry(e,s)}}enforceGlobalMemoryBudget(){let e=0;for(const t of this.canvasEntries.values())e+=t.cachedBytes;if(!(e<=4194304))for(const[t,s]of this.canvasEntries){if(e<=4194304)break;this.joinedCanvases.has(t)||(e-=s.cachedBytes,this.canvasEntries.delete(t))}}enforceCanvasCountLimit(){for(;this.canvasEntries.size>20;){let e=!1;for(const[t]of this.canvasEntries)if(!this.joinedCanvases.has(t)){this.canvasEntries.delete(t),e=!0;break}if(!e)break}}estimateSize(e){try{let t=2*JSON.stringify(e.components).length;if(e.dataModels.size>0)for(const s of e.dataModels.values())t+=2*JSON.stringify(s).length;return t}catch{return 1024}}}
@@ -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(),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}
1
+ import{sv,findRootIds,nextComponentId}from"./a2ui-helpers.js";function nextCid(){return nextComponentId("c")}function textNode(e,t,n="body"){return{id:e,component:{Text:{text:sv(t),usageHint:sv(n)}}}}function imageNode(e,t,n="mediumFeature",s){const i={url:sv(t),usageHint:sv(n)};return s&&(i.fit=sv(s)),{id:e,component:{Image:i}}}function iconNode(e,t){return{id:e,component:{Icon:{name:sv(t)}}}}function dividerNode(e,t="horizontal"){return{id:e,component:{Divider:{axis:sv(t)}}}}function buttonNode(e,t,n,s){const i={name:n};return s&&(i.context=s),{id:e,component:{Button:{child:t,action:i}}}}function columnNode(e,t,n,s){const i={children:{explicitList:t}};return n&&(i.distribution=sv(n)),s&&(i.alignment=sv(s)),{id:e,component:{Column:i}}}function rowNode(e,t,n,s){const i={children:{explicitList:t}};return n&&(i.distribution=sv(n)),s&&(i.alignment=sv(s)),{id:e,component:{Row:i}}}function cardNode(e,t){return{id:e,component:{Card:{child:t}}}}function tabsNode(e,t){return{id:e,component:{Tabs:{tabItems:t.map(e=>({title:sv(e.title),child:e.childId}))}}}}function textFieldNode(e,t,n="shortText",s){const i={label:sv(t),type:sv(n)};return s&&(i.text={path:s}),{id:e,component:{TextField:i}}}function checkboxNode(e,t,n){return{id:e,component:{CheckBox:{label:sv(t),value:n?{path:n}:{literalBoolean:!1}}}}}function multipleChoiceNode(e,t,n,s,i){const o={label:sv(t),options:n.map(e=>({label:sv(e.label),value:sv(e.value)})),selections:{path:s}};return i&&(o.description=sv(i)),{id:e,component:{MultipleChoice:o}}}function translateComponent(e){if(!e||"object"!=typeof e){return[textNode(nextCid(),"[invalid component]")]}const t="string"==typeof e.type?e.type.toLowerCase():"text";switch(t){case"text":return buildText(e);case"card":return buildCard(e);case"table":return buildTable(e);case"form":return buildForm(e);case"image":return buildImage(e);case"button":return buildButton(e);case"grid":return buildGrid(e);case"stat":return buildStat(e);case"key_value":return buildKeyValue(e);case"list":return buildList(e);case"alert":case"callout":return buildAlert(e);case"code":return buildCode(e);case"markdown":return buildMarkdown(e);case"chart":return buildChart(e);case"divider":return buildDivider(e);case"tabs":return buildTabs(e);case"link":return buildLink(e);case"web_embed":case"video":case"audio":case"map":case"file_preview":return buildEmbed(e);case"badge":return buildBadge(e);case"progress":return buildProgress(e);case"timeline":return buildTimeline(e);case"accordion":return buildAccordion(e);case"row":case"column":{const n=(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e)),s=findRootIds(n),i=nextCid(),o="row"===t?rowNode(i,s):columnNode(i,s);return[...n,o]}default:return[textNode(nextCid(),`[Unsupported: ${t}]`)]}}function s(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);try{return JSON.stringify(e)}catch{return"[Object]"}}function buildText(e){return[textNode(nextCid(),s(e.content??e.text??""))]}function buildCard(e){const t=[],n=[];if(e.image){const i=nextCid();t.push(imageNode(i,s(e.image),"mediumFeature","cover")),n.push(i)}if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h3")),n.push(i)}if(e.subtitle){const i=nextCid();t.push(textNode(i,s(e.subtitle),"caption")),n.push(i)}if(e.description){const i=nextCid();t.push(textNode(i,s(e.description),"body")),n.push(i)}const i=Array.isArray(e.fields)?e.fields:[];for(const e of i){if(!e||"object"!=typeof e)continue;const i=e,o=nextCid(),r=nextCid(),a=nextCid();t.push(textNode(r,s(i.label??i.key),"caption")),t.push(textNode(a,s(i.value))),t.push(rowNode(o,[r,a],"spaceBetween")),n.push(o)}const o=Array.isArray(e.actions)?e.actions:[];if(o.length>0){const e=[];for(const n of o){if(!n||"object"!=typeof n)continue;const i=n,o=nextCid(),r=nextCid(),a=s(i.label??"Action"),c=s(i.actionId??i.id??nextCid());t.push(textNode(r,a)),t.push(buttonNode(o,r,c,[{key:"actionId",value:{literalString:c}},{key:"label",value:{literalString:a}}])),e.push(o)}if(e.length>0){const s=nextCid();t.push(rowNode(s,e)),n.push(s)}}if(e.footer){const i=nextCid();t.push(textNode(i,s(e.footer),"caption")),n.push(i)}const r=nextCid();t.push(columnNode(r,n));const a=nextCid();return t.push(cardNode(a,r)),t}function buildTable(e){const t=[],n=[],i=Array.isArray(e.columns)?e.columns:[],o=Array.isArray(e.rows)?e.rows:[];if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h4")),n.push(i)}const r=[],a=[];for(const e of i)if("string"==typeof e)r.push(e),a.push(e);else if(e&&"object"==typeof e){const t=e,n=s(t.key??t.id??t.field??t.name??t.label??"");r.push(s(t.label??t.title??t.header??t.name??n)),a.push(n)}else{const t=s(e);r.push(t),a.push(t)}if(r.length>0){const e=[];for(const n of r){const s=nextCid();t.push(textNode(s,n,"caption")),e.push(s)}const s=nextCid();t.push(rowNode(s,e)),n.push(s);const i=nextCid();t.push(dividerNode(i)),n.push(i)}for(const e of o){let i;if(Array.isArray(e))i=e;else if(e&&"object"==typeof e&&a.length>0){const t=e;i=a.map(e=>{const n=Object.keys(t).find(t=>t===e||t.toLowerCase()===e.toLowerCase());return null!=n?t[n]:""})}else i=[e];const o=[];for(const e of i){const n=nextCid();t.push(textNode(n,s(e))),o.push(n)}const r=nextCid();t.push(rowNode(r,o)),n.push(r)}const c=nextCid();t.push(columnNode(c,n));const d=nextCid();return t.push(cardNode(d,c)),t}function buildForm(e){const t=[],n=[],i=Array.isArray(e.fields)?e.fields:[];if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h3")),n.push(i)}if(e.description){const i=nextCid();t.push(textNode(i,s(e.description),"body")),n.push(i)}const o=[];for(const e of i){if(!e||"object"!=typeof e)continue;const i=e,r=nextCid(),a=s(i.name??i.label??r),c=s(i.inputType??i.type??"text"),d=`/${a}`;if(o.push(a),"checkbox"===c||"boolean"===c||"toggle"===c)t.push(checkboxNode(r,s(i.label??i.name??""),d));else if("select"===c||"multi_select"===c||"dropdown"===c||"radio"===c){const e=(Array.isArray(i.options)?i.options:[]).map(e=>{if("string"==typeof e)return{label:e,value:e};const t=e;return{label:s(t.label??t.value??""),value:s(t.value??t.label??"")}});t.push(multipleChoiceNode(r,s(i.label??i.name??""),e,d))}else if("datetime"===c||"time"===c){const e="datetime"===c;t.push({id:r,component:{DateTimeInput:{value:{path:d},enableDate:e,enableTime:!0}}})}else if("range"===c){const e="number"==typeof i.min?i.min:0,n="number"==typeof i.max?i.max:100;t.push({id:r,component:{Slider:{label:sv(s(i.label??i.name??"")),value:{path:d},minValue:e,maxValue:n}}})}else{const e="number"===c?"number":"date"===c?"date":"textarea"===c||"longText"===c?"longText":"shortText";t.push(textFieldNode(r,s(i.label??i.name??""),e,d))}n.push(r)}const r=o.map(e=>({key:e,value:{path:`/${e}`}})),a=nextCid(),c=nextCid();t.push(textNode(a,s(e.submitLabel??"Submit"))),t.push(buttonNode(c,a,s(e.submitActionId??"form_submit"),r)),n.push(c);const d=nextCid();return t.push(columnNode(d,n)),t}function buildImage(e){return[imageNode(nextCid(),s(e.url??e.src??""))]}function buildButton(e){const t=nextCid(),n=nextCid(),i=s(e.label??"Button"),o=s(e.actionId??n);return[textNode(t,i),buttonNode(n,t,o,[{key:"actionId",value:{literalString:o}},{key:"label",value:{literalString:i}}])]}function buildGrid(e){const t=(Array.isArray(e.children)?e.children:Array.isArray(e.components)?e.components:[]).flatMap(e=>translateComponent(e)),n=Number(e.columns??2),s=findRootIds(t),i=[],o=[];for(let e=0;e<s.length;e+=n){const t=s.slice(e,e+n),r=nextCid();i.push(rowNode(r,t)),o.push(r)}const r=nextCid();return[...t,...i,columnNode(r,o)]}function buildStat(e){const t=[],n=[];if(e.icon){const i=nextCid();t.push(iconNode(i,s(e.icon))),n.push(i)}if(e.label){const i=nextCid();t.push(textNode(i,s(e.label),"caption")),n.push(i)}const i=nextCid();if(t.push(textNode(i,s(e.value??"0"),"h2")),n.push(i),e.change){const i=nextCid(),o="positive"===e.changeType?"↑ ":"negative"===e.changeType?"↓ ":"";t.push(textNode(i,`${o}${s(e.change)}`,"caption")),n.push(i)}if(e.description){const i=nextCid();t.push(textNode(i,s(e.description),"caption")),n.push(i)}const o=nextCid();t.push(columnNode(o,n,void 0,"center"));const r=nextCid();return t.push(cardNode(r,o)),t}function buildKeyValue(e){const t=[],n=[],i=Array.isArray(e.items)?e.items:[];if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h4")),n.push(i)}for(const e of i){if(!e||"object"!=typeof e)continue;const i=e,o=nextCid(),r=nextCid(),a=nextCid();t.push(textNode(r,s(i.key??i.label??""),"caption")),t.push(textNode(a,s(i.value??""))),t.push(rowNode(o,[r,a],"spaceBetween")),n.push(o)}const o=nextCid();t.push(columnNode(o,n));const r=nextCid();return t.push(cardNode(r,o)),t}function buildList(e){const t=[],n=[],i=Array.isArray(e.items)?e.items:[];if(e.title){const i=nextCid();t.push(textNode(i,s(e.title),"h4")),n.push(i)}for(const e of i){if(!e||"object"!=typeof e)continue;const i=e,o=[];if(i.image){const e=nextCid();t.push(imageNode(e,s(i.image),"avatar")),o.push(e)}if(i.icon){const e=nextCid();t.push(iconNode(e,s(i.icon))),o.push(e)}const r=[];if(i.title||i.name){const e=nextCid();t.push(textNode(e,s(i.title??i.name),"body")),r.push(e)}if(i.subtitle||i.description){const e=nextCid();t.push(textNode(e,s(i.subtitle??i.description),"caption")),r.push(e)}if(r.length>0){const e=nextCid();t.push(columnNode(e,r)),o.push(e)}if(i.badge||i.value){const e=nextCid();t.push(textNode(e,s(i.badge??i.value),"caption")),o.push(e)}const a=nextCid();t.push(rowNode(a,o,"start","center")),n.push(a)}const o=nextCid();return t.push(columnNode(o,n)),t}function buildAlert(e){const t=[],n=[],i=s(e.variant??"info"),o=nextCid();t.push(iconNode(o,{info:"info",success:"check",warning:"error",error:"error",tip:"info",caution:"error",important:"error",note:"info"}[i]??"info")),n.push(o);const r=[];if(e.title){const n=nextCid();t.push(textNode(n,s(e.title),"h5")),r.push(n)}if(e.message||e.content){const n=nextCid();t.push(textNode(n,s(e.message??e.content))),r.push(n)}if(0===r.length){const n=nextCid();t.push(textNode(n,s(e.type??"Alert"))),r.push(n)}const a=nextCid();t.push(columnNode(a,r)),n.push(a);const c=nextCid();t.push(rowNode(c,n,"start","start"));const d=nextCid();return t.push(cardNode(d,c)),t}function buildCode(e){const t=s(e.code??""),n=s(e.language??""),i=n?`\`\`\`${n}\n${t}\n\`\`\``:`\`\`\`\n${t}\n\`\`\``;return[textNode(nextCid(),i)]}function buildMarkdown(e){return[textNode(nextCid(),s(e.content??""))]}function buildChart(e){const t=nextCid(),n=s(e.chartType??"chart");return[textNode(t,`[${e.title?s(e.title):n} — chart visualization not available in standard catalog]`,"caption")]}function buildDivider(e){return[dividerNode(nextCid())]}function buildTabs(e){const t=Array.isArray(e.tabs)?e.tabs:Array.isArray(e.children)?e.children:Array.isArray(e.items)?e.items:[];if(0===t.length){return[textNode(nextCid(),s(e.title??""))]}const n=[],i=[];for(const e of t){const t=s(e.label??e.title??e.id??"Tab"),o=(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e));n.push(...o);const r=findRootIds(o),a=nextCid();n.push(columnNode(a,r)),i.push({title:t,childId:a})}const o=nextCid();return n.push(tabsNode(o,i)),n}function buildLink(e){const t=nextCid(),n=nextCid();return[textNode(t,s(e.text??e.label??e.url??"Link")),buttonNode(n,t,"navigate",[{key:"url",value:{literalString:s(e.url??"")}}])]}function buildEmbed(e){const t=s(e.url??"");if(/youtube|youtu\.be|vimeo/i.test(t)){return[{id:nextCid(),component:{Video:{url:sv(t)}}}]}return buildLink({text:s(e.title??t),url:t})}function buildBadge(e){return[textNode(nextCid(),s(e.text??e.label??""),"caption")]}function buildProgress(e){const t=Number(e.value??0),n=Number(e.max??100),i=n>0?Math.round(t/n*100):0;return[textNode(nextCid(),`${s(e.label)||"Progress"}: ${i}% (${t}/${n})`)]}function buildTimeline(e){const t=(Array.isArray(e.items)?e.items:[]).filter(e=>null!=e&&"object"==typeof e).map(e=>`${s(e.time??e.date??"")} — ${s(e.title??"")}${e.description?`: ${s(e.description)}`:""}`),n=e.title?`**${s(e.title)}**\n`:"";return[textNode(nextCid(),n+(t.length>0?t.join("\n"):"(empty timeline)"))]}function buildAccordion(e){const t=Array.isArray(e.items)?e.items:Array.isArray(e.children)?e.children:[];if(0===t.length){return[textNode(nextCid(),s(e.title??""))]}const n=[];for(const e of t){const t=nextCid();n.push(textNode(t,s(e.title??e.label??""),"h5"));const i=Array.isArray(e.children)?e.children.flatMap(e=>translateComponent(e)):[];n.push(...i)}return n}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"},subtitle:{type:"string"},badge:{type:"string"},badgeVariant:{type:"string",enum:["default","success","warning","error","info"]},footer:{type:"string"},layout:{type:"string",enum:["vertical","horizontal"]},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"]},columns:{type:"array",description:"Column headers or grid column count"},rows:{type:"array",description:"Row data as arrays matching column order"},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"},labels:{type:"array"},datasets:{type:"array"},language:{type:"string"},code:{type:"string"},items:{type:"array"},variant:{type:"string"},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,n,s){const{stateManager:i,logger:o}=e,r=s??e.getGatewaySessionKey?.(n),a=r??t,c=i.resolveCanvasForSession(a);if(c&&i.isCanvasJoined(c))return c;const d=i.getRelayClient();if(d)try{e.waitForAgentSync&&await e.waitForAgentSync();const s=e.getPlatformAgentId?.(n),c=r??t,u=r&&r!==t?t:void 0,l=await d.ensureCanvas(c,{agentId:s,linkSessionId:u});return await i.joinCanvas(l,a),o.info?.(`[canvas] Joined canvas=${l} session=${t} gatewayKey=${r??"none"} agentId=${s??"none"}`),l}catch(e){return void o.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:s,isCanvasActive:i}=e;let o=0;return async(r,a)=>{if(i&&!i())return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."};const c=a,d=c.id,u="panel";let l=c.components;if("string"==typeof l)try{l=JSON.parse(l)}catch{}if(!d||!Array.isArray(l)||!l.length)return{success:!1,error:"id and components (array) are required"};if(!t.validateComponentCount(l))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),s({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const p=l.flatMap(translateComponent),m=findRootIds(p),f=nextCid(),h=columnNode(f,m),b=[...p,h],g=e.getActiveSessionId?.(r);if(!g)return n.warn?.(`[canvas] No active session for toolCallId=${r}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};const y=e.getGatewaySessionKey?.(r),x=await ensureRelayConnected(e,g,r,y),v=t.has(d,x??void 0),C=Date.now(),N=t.getRelayClient();x&&N&&C-o>6e4&&N.createSnapshot(x,"Pre-agent edit").then(()=>{o=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),v?t.update(d,b,x,f):t.create(x??`_fallback_${Date.now()}`,d,u,b,{origin:{kind:"agent"},rootComponentId:f});const w=l.map(e=>e.type);s({component:"canvas",domain:"canvas",name:v?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:u,componentCount:l.length,componentTypes:w}}),n.debug?.(`[canvas] ${v?"Updated":"Created"} surface=${d} components=${l.length}`);return{success:!0,surfaceId:d,presentation:u,isUpdate:v,activeSurfaces:x?t.getAllActive(x).map(e=>e.surfaceId):[d]}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:s}=e;return async(i,o)=>{const r=o.id;if(!r)return{success:!1,error:"id is required"};const a=t.get(r),c=a?Date.now()-a.createdAt:0,d=e.getGatewaySessionKey?.(i),u=d?t.resolveCanvasForSession(d):void 0;return t.delete(r,u),s({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:c}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}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, footer, actions:[{label,actionId}], fields:[{label,value}]), table (columns, rows), stat (label, value, change, changeType, icon), key_value (items:[{key,value}]), list (items:[{title,subtitle,description,image,icon,badge,value}])\n\n**Text & Code**: text (content), markdown (content — supports full Markdown including bold, italic, lists, code blocks, links), code (language, code)\n\n**Media**: image (url, alt), web_embed (url — video URLs auto-detected), link (text, url)\n\n**Feedback**: alert (variant:info/success/warning/error, title, message)\n\n**Interactive**: form (fields:[{name,label,inputType,options:[{label,value}]}], submitLabel), button (label, actionId)\n - inputType: text (default), number, date, textarea, checkbox, boolean, toggle, select, multi_select, radio, datetime, time, range\n - select/multi_select/radio requires an `options` array — each item is {label, value} or a plain string\n - range renders a slider (optional min, max — default 0..100)\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"}])\n- **Detail card**: card(title, description, fields:[{label:"Price",value:"$29"},{label:"Stock",value:"142"}])\n- **Form with select**: form(title:"Settings", fields:[{name:"country", label:"Country", inputType:"select", options:[{label:"China",value:"CN"},{label:"US",value:"US"}]}, {name:"email", label:"Email"}], submitLabel:"Save")\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 (tables, stats, key-value, lists, cards, grids, alerts, forms, links, 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, actions, fields:[{label,value}]. List items support: image, title, subtitle, description, badge, value. Markdown content supports full formatting. Use web_embed for embedding URLs. Use link for navigation. Form supports inputType: text/number/date/textarea/checkbox/toggle/select/multi_select/radio/datetime/time/range; select/multi_select/radio requires options:[{label,value}]. 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,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const s=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),i=n.length-s.length;let o=`[Canvas State] Active surfaces: ${s.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const s=n.types.slice(0,3).join(", "),i=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${s}${i})`}).join("; ")}.`;return i>0&&(o+=` ...and ${i} more.`),o+=" To modify existing content, reuse its id in render_ui.",o.length>500&&(o=o.slice(0,497)+"..."),o}
@@ -1 +1 @@
1
- import WebSocket from"ws";import{agentAuthHeaders}from"./agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./canvas-constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4;export class CanvasWSProvider{ws=null;canvasId=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;onUpdate=null;onAwareness=null;pendingMessages=[];constructor(s){this.opts=s}connect(s,e){this.canvasId=s,this.onUpdate=e.onUpdate,this.onAwareness=e.onAwareness??null,this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){if(this.closed||!this.canvasId)return;const s=`/ws/agent/${this.canvasId}`,e=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${s}`;this.opts.logger?.info?.(`[canvas-ws] Connecting to ${e}`);const t=agentAuthHeaders("GET",s,this.opts.agentKeys),n=new WebSocket(e,{headers:t});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.(`[canvas-ws] Connected to canvas ${this.canvasId}`),this.reconnectAttempts=0,this.ws=n;for(const s of this.pendingMessages)n.send(s);this.pendingMessages=[]}),n.on("message",s=>{const e=new Uint8Array(s instanceof ArrayBuffer?s:s.buffer.slice(s.byteOffset,s.byteOffset+s.byteLength));if(0===e.length)return;const t=e[0];t===MSG_AWARENESS&&this.onAwareness?this.onAwareness(e):t===MSG_SYNC&&this.onUpdate&&this.onUpdate(e)}),n.on("close",()=>{this.opts.logger?.info?.(`[canvas-ws] Disconnected from canvas ${this.canvasId}`),this.ws=null,this.scheduleReconnect()}),n.on("error",s=>{this.opts.logger?.warn?.("[canvas-ws] Error:",s.message),n.close()})}scheduleReconnect(){if(this.closed)return;const s=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-ws] Reconnecting in ${s}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>{this.doConnect()},s)}sendUpdate(s){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(s):this.pendingMessages.push(s)}sendAwareness(s){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(s):this.pendingMessages.push(s)}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.canvasId=null,this.pendingMessages=[]}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}get currentCanvasId(){return this.canvasId}}
1
+ import WebSocket from"ws";import{agentAuthHeaders}from"./agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./canvas-constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4;export class CanvasWSProvider{ws=null;canvasId=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;onUpdate=null;onAwareness=null;pendingMessages=[];constructor(s){this.opts=s}connect(s,e){this.canvasId=s,this.onUpdate=e.onUpdate,this.onAwareness=e.onAwareness??null,this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){if(this.closed||!this.canvasId)return;const s=`/ws/agent/${this.canvasId}`;let e=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${s}`;this.opts.agentId&&(e+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),this.opts.logger?.info?.(`[canvas-ws] Connecting to ${e}`);const t=agentAuthHeaders("GET",s,this.opts.agentKeys),n=new WebSocket(e,{headers:t});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.(`[canvas-ws] Connected to canvas ${this.canvasId}`),this.reconnectAttempts=0,this.ws=n;for(const s of this.pendingMessages)n.send(s);this.pendingMessages=[]}),n.on("message",s=>{const e=new Uint8Array(s instanceof ArrayBuffer?s:s.buffer.slice(s.byteOffset,s.byteOffset+s.byteLength));if(0===e.length)return;const t=e[0];t===MSG_AWARENESS&&this.onAwareness?this.onAwareness(e):t===MSG_SYNC&&this.onUpdate&&this.onUpdate(e)}),n.on("close",()=>{this.opts.logger?.info?.(`[canvas-ws] Disconnected from canvas ${this.canvasId}`),this.ws=null,this.scheduleReconnect()}),n.on("error",s=>{this.opts.logger?.warn?.("[canvas-ws] Error:",s.message),n.close()})}scheduleReconnect(){if(this.closed)return;const s=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-ws] Reconnecting in ${s}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>{this.doConnect()},s)}sendUpdate(s){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(s):this.pendingMessages.push(s)}sendAwareness(s){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(s):this.pendingMessages.push(s)}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.canvasId=null,this.pendingMessages=[]}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}get currentCanvasId(){return this.canvasId}}
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{mkdirSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";function trimStr(e){if("string"==typeof e){const r=e.trim();return r.length>0?r:void 0}}function readInt(e){if("number"==typeof e&&Number.isFinite(e))return Math.trunc(e);if("string"==typeof e){const r=parseInt(e,10);return Number.isFinite(r)?r:void 0}}function readBool(e){return"boolean"==typeof e?e:"true"===e||"false"!==e&&void 0}function pick(e,r){for(const r of e)if(void 0!==r.value)return{value:r.value,source:r.source};return r}const DEFAULT_GATEWAY_URL="ws://127.0.0.1:18789",DEFAULT_LOG_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-logs"),DEFAULT_OPENCLAW_CONFIG_PATH=join(homedir(),".openclaw","openclaw.json");export function resolveConfigWithMeta(e){const r=e.pluginConfig??{},o=e.openclawConfig??{},a=e.openclawConfigPath??DEFAULT_OPENCLAW_CONFIG_PATH,u=pick([{source:"pluginConfig",value:trimStr(r.platformUrl)},{source:"env",value:trimStr(process.env.BEEOS_PLATFORM_URL)}],{source:"default",value:""}),l=r.gateway??{},n=(o.gateway??{}).auth??{},t=pick([{source:"pluginConfig",value:trimStr(l.url)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_URL)}],{source:"default",value:DEFAULT_GATEWAY_URL}),s=pick([{source:"openclawConfig",value:trimStr(n.token)},{source:"pluginConfig",value:trimStr(l.token)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_TOKEN)}],{source:"default",value:""}),i=pick([{source:"pluginConfig",value:readInt(l.protocol)}],{source:"default",value:3}),c=pick([{source:"pluginConfig",value:trimStr(l.clientId)}],{source:"default",value:`beeos-claw-${process.pid}`}),v=pick([{source:"pluginConfig",value:trimStr(l.agentId)}],{source:"default",value:"main"}),p=r.bridge??{},d=pick([{source:"pluginConfig",value:trimStr(p.url)},{source:"env",value:trimStr(process.env.BRIDGE_URL)}],{source:"default",value:""}),m=pick([{source:"pluginConfig",value:trimStr(p.keyFile)},{source:"env",value:trimStr(process.env.BRIDGE_KEY_FILE)}],{source:"default",value:""}),f=pick([{source:"pluginConfig",value:trimStr(p.service)},{source:"env",value:trimStr(process.env.BRIDGE_SERVICE)}],{source:"default",value:"acp"}),g=pick([{source:"pluginConfig",value:readBool((p.shell??{}).enabled)},{source:"env",value:readBool(process.env.BEEOS_SHELL_ENABLED)}],{source:"default",value:!0}),E=r.retry??{},_=pick([{source:"pluginConfig",value:readInt(E.baseMs)}],{source:"default",value:1e3}),S=pick([{source:"pluginConfig",value:readInt(E.maxMs)}],{source:"default",value:6e5}),M=pick([{source:"pluginConfig",value:readInt(E.maxAttempts)}],{source:"default",value:0}),I=r.log??{},O=pick([{source:"pluginConfig",value:readBool(I.enabled)},{source:"env",value:readBool(process.env.BEEOS_LOG_ENABLED)}],{source:"default",value:!0}),A=pick([{source:"pluginConfig",value:readBool(I.verbose)}],{source:"default",value:!1}),C=pick([{source:"pluginConfig",value:trimStr(I.dir)}],{source:"default",value:DEFAULT_LOG_DIR}),T=pick([{source:"pluginConfig",value:readInt(p.promptTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_PROMPT_TIMEOUT_MS)}],{source:"default",value:3e5}),R=pick([{source:"pluginConfig",value:readInt(r.terminalWsPort)},{source:"env",value:readInt(process.env.BEEOS_TERMINAL_WS_PORT)}],{source:"default",value:18801}),L=trimStr(r.terminalWsAllowedOrigins)??trimStr(process.env.BEEOS_TERMINAL_WS_ALLOWED_ORIGINS),k=pick([{source:L?"pluginConfig":"default",value:L?L.split(",").map(e=>e.trim()).filter(Boolean):void 0}],{source:"default",value:["localhost","127.0.0.1","::1"]}),y=pick([{source:"pluginConfig",value:readInt(r.streamIdleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_STREAM_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),h=pick([{source:"pluginConfig",value:readInt(r.streamStaleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_STREAM_STALE_TIMEOUT_MS)}],{source:"default",value:6e5}),w=pick([{source:"pluginConfig",value:readInt(r.loadReplayIdleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_LOAD_REPLAY_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),b=pick([{source:"pluginConfig",value:readInt(r.cronFlushTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_FLUSH_TIMEOUT_MS)}],{source:"default",value:12e4}),B=pick([{source:"pluginConfig",value:readInt(r.cronSignalWindowMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_SIGNAL_WINDOW_MS)}],{source:"default",value:6e4}),P=pick([{source:"env",value:trimStr(process.env.CANVAS_RELAY_URL)},{source:"pluginConfig",value:trimStr(r.canvasRelayUrl)}],{source:"default",value:"https://canvas-relay.beeos.ai"}),N=pick([{source:"pluginConfig",value:readBool(r.canvasEnabled)},{source:"env",value:readBool(process.env.BEEOS_CANVAS_ENABLED)}],{source:"default",value:!0}),U=pick([{source:"pluginConfig",value:readBool(r.openclawCanvasCompat)},{source:"env",value:readBool(process.env.BEEOS_OPENCLAW_CANVAS_COMPAT)}],{source:"default",value:!1}),W={platformUrl:u.value.replace(/\/+$/,""),bridge:{url:d.value,keyFile:m.value,service:f.value,mode:trimStr(p.mode),shell:{enabled:g.value},promptTimeoutMs:T.value,historyPendingTimeoutMs:readInt(p.historyPendingTimeoutMs)},gateway:{url:t.value,token:s.value,protocol:i.value,clientId:c.value,agentId:v.value},retry:{baseMs:Math.max(0,_.value),maxMs:Math.max(0,S.value),maxAttempts:Math.max(0,M.value)},log:{enabled:O.value,verbose:A.value,dir:C.value},promptTimeoutMs:T.value,terminalWsPort:R.value,terminalWsAllowedOrigins:k.value,streamIdleTimeoutMs:Math.max(0,y.value),streamStaleTimeoutMs:Math.max(0,h.value),loadReplayIdleTimeoutMs:Math.max(0,w.value),cronFlushTimeoutMs:Math.max(0,b.value),cronSignalWindowMs:Math.max(0,B.value),canvasRelayUrl:P.value.replace(/\/+$/,""),canvasEnabled:N.value,openclawCanvasCompat:U.value},F={platformUrl:u.source,bridge:{url:d.source,keyFile:m.source,service:f.source,shell:{enabled:g.source}},gateway:{url:t.source,token:s.source,protocol:i.source,clientId:c.source,agentId:v.source},retry:{baseMs:_.source,maxMs:S.source,maxAttempts:M.source},log:{enabled:O.source,verbose:A.source,dir:C.source},promptTimeoutMs:T.source,terminalWsPort:R.source,terminalWsAllowedOrigins:k.source,streamIdleTimeoutMs:y.source,streamStaleTimeoutMs:h.source,loadReplayIdleTimeoutMs:w.source,cronFlushTimeoutMs:b.source,cronSignalWindowMs:B.source,canvasRelayUrl:P.source,canvasEnabled:N.source,openclawCanvasCompat:U.source},G=[];return W.platformUrl||G.push({code:"CONFIG_PLATFORM_URL_MISSING",severity:"error",message:"platformUrl is required for file uploads and platform callbacks",nextSteps:[`Set plugins.entries.beeos-claw.config.platformUrl in ${a}`,"Or set the BEEOS_PLATFORM_URL environment variable"]}),W.gateway.token||G.push({code:"CONFIG_GATEWAY_TOKEN_MISSING",severity:"warn",message:"gateway.token is empty; Gateway auth may fail if authentication is enabled",nextSteps:[`Set gateway.auth.token in ${a}`,`Or set plugins.entries.beeos-claw.config.gateway.token in ${a}`]}),{config:W,sources:F,validation:G,openclawConfigPath:a}}export function resolveConfig(e){const{config:r,validation:o}=resolveConfigWithMeta({pluginConfig:e}),a=o.find(e=>"error"===e.severity);if(a)throw new Error(`[beeos-claw] ${a.message}`);return r}const MIRROR_PATH=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","openclaw.json");export function mirrorOpenClawConfig(e){if(!e||"object"!=typeof e||Array.isArray(e))return{copied:!1,destinationPath:MIRROR_PATH,reason:"config_invalid"};try{return mkdirSync(dirname(MIRROR_PATH),{recursive:!0}),writeFileSync(MIRROR_PATH,JSON.stringify(e,null,2)+"\n",{encoding:"utf8",mode:384}),{copied:!0,destinationPath:MIRROR_PATH}}catch(e){return{copied:!1,destinationPath:MIRROR_PATH,reason:e instanceof Error?e.message:"write_failed"}}}
1
+ import{mkdirSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";function trimStr(e){if("string"==typeof e){const r=e.trim();return r.length>0?r:void 0}}function readInt(e){if("number"==typeof e&&Number.isFinite(e))return Math.trunc(e);if("string"==typeof e){const r=parseInt(e,10);return Number.isFinite(r)?r:void 0}}function readBool(e){return"boolean"==typeof e?e:"true"===e||"false"!==e&&void 0}function pick(e,r){for(const r of e)if(void 0!==r.value)return{value:r.value,source:r.source};return r}const DEFAULT_GATEWAY_URL="ws://127.0.0.1:18789",DEFAULT_LOG_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-logs"),DEFAULT_OPENCLAW_CONFIG_PATH=join(homedir(),".openclaw","openclaw.json");export function resolveConfigWithMeta(e){const r=e.pluginConfig??{},o=e.openclawConfig??{},a=e.openclawConfigPath??DEFAULT_OPENCLAW_CONFIG_PATH,u=pick([{source:"pluginConfig",value:trimStr(r.platformUrl)},{source:"env",value:trimStr(process.env.BEEOS_PLATFORM_URL)}],{source:"default",value:""}),l=r.gateway??{},s=(o.gateway??{}).auth??{},n=pick([{source:"pluginConfig",value:trimStr(l.url)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_URL)}],{source:"default",value:DEFAULT_GATEWAY_URL}),i=pick([{source:"openclawConfig",value:trimStr(s.token)},{source:"pluginConfig",value:trimStr(l.token)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_TOKEN)}],{source:"default",value:""}),t=pick([{source:"pluginConfig",value:readInt(l.protocol)}],{source:"default",value:3}),c=pick([{source:"pluginConfig",value:trimStr(l.clientId)}],{source:"default",value:`beeos-claw-${process.pid}`}),v=pick([{source:"pluginConfig",value:trimStr(l.agentId)}],{source:"default",value:""}),p=r.bridge??{},d=pick([{source:"pluginConfig",value:trimStr(p.url)},{source:"env",value:trimStr(process.env.BRIDGE_URL)}],{source:"default",value:""}),m=pick([{source:"pluginConfig",value:trimStr(p.keyFile)},{source:"env",value:trimStr(process.env.BRIDGE_KEY_FILE)}],{source:"default",value:""}),f=pick([{source:"pluginConfig",value:trimStr(p.service)},{source:"env",value:trimStr(process.env.BRIDGE_SERVICE)}],{source:"default",value:"acp"}),g=pick([{source:"pluginConfig",value:readBool((p.shell??{}).enabled)},{source:"env",value:readBool(process.env.BEEOS_SHELL_ENABLED)}],{source:"default",value:!0}),E=r.retry??{},_=pick([{source:"pluginConfig",value:readInt(E.baseMs)}],{source:"default",value:1e3}),S=pick([{source:"pluginConfig",value:readInt(E.maxMs)}],{source:"default",value:6e5}),I=pick([{source:"pluginConfig",value:readInt(E.maxAttempts)}],{source:"default",value:0}),M=r.log??{},A=pick([{source:"pluginConfig",value:readBool(M.enabled)},{source:"env",value:readBool(process.env.BEEOS_LOG_ENABLED)}],{source:"default",value:!0}),C=pick([{source:"pluginConfig",value:readBool(M.verbose)}],{source:"default",value:!1}),O=pick([{source:"pluginConfig",value:trimStr(M.dir)}],{source:"default",value:DEFAULT_LOG_DIR}),T=pick([{source:"pluginConfig",value:readInt(p.promptTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_PROMPT_TIMEOUT_MS)}],{source:"default",value:3e5}),R=pick([{source:"pluginConfig",value:readInt(r.terminalWsPort)},{source:"env",value:readInt(process.env.BEEOS_TERMINAL_WS_PORT)}],{source:"default",value:18801}),k=trimStr(r.terminalWsAllowedOrigins)??trimStr(process.env.BEEOS_TERMINAL_WS_ALLOWED_ORIGINS),L=pick([{source:k?"pluginConfig":"default",value:k?k.split(",").map(e=>e.trim()).filter(Boolean):void 0}],{source:"default",value:["localhost","127.0.0.1","::1"]}),B=pick([{source:"pluginConfig",value:readInt(r.streamIdleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_STREAM_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),y=pick([{source:"pluginConfig",value:readInt(r.streamStaleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_STREAM_STALE_TIMEOUT_MS)}],{source:"default",value:6e5}),h=pick([{source:"pluginConfig",value:readInt(r.loadReplayIdleTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_LOAD_REPLAY_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),w=pick([{source:"pluginConfig",value:readInt(r.cronFlushTimeoutMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_FLUSH_TIMEOUT_MS)}],{source:"default",value:12e4}),b=pick([{source:"pluginConfig",value:readInt(r.cronSignalWindowMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_SIGNAL_WINDOW_MS)}],{source:"default",value:6e4}),U=pick([{source:"env",value:trimStr(process.env.CANVAS_RELAY_URL)},{source:"pluginConfig",value:trimStr(r.canvasRelayUrl)}],{source:"default",value:"https://canvas-relay.beeos.ai"}),P=pick([{source:"pluginConfig",value:readBool(r.canvasEnabled)},{source:"env",value:readBool(process.env.BEEOS_CANVAS_ENABLED)}],{source:"default",value:!0}),N=pick([{source:"pluginConfig",value:readBool(r.openclawCanvasCompat)},{source:"env",value:readBool(process.env.BEEOS_OPENCLAW_CANVAS_COMPAT)}],{source:"default",value:!1}),F=r.messageBus??{},W=pick([{source:"pluginConfig",value:trimStr(F.serviceUrl)},{source:"env",value:trimStr(process.env.MESSAGE_SERVICE_URL)}],{source:"default",value:""}),G=pick([{source:"pluginConfig",value:trimStr(F.apiKey)},{source:"env",value:trimStr(process.env.MESSAGE_API_KEY)}],{source:"default",value:""}),D=pick([{source:"pluginConfig",value:trimStr(F.url)},{source:"env",value:trimStr(process.env.CENTRIFUGO_URL)}],{source:"default",value:""}),x=pick([{source:"pluginConfig",value:trimStr(F.principalId)},{source:"env",value:trimStr(process.env.BEEOS_PRINCIPAL_ID)}],{source:"default",value:""}),H=pick([{source:"pluginConfig",value:readBool(F.a2aEnabled)},{source:"env",value:readBool(process.env.BEEOS_A2A_ENABLED)}],{source:"default",value:!0}),j=pick([{source:"pluginConfig",value:readBool(F.enabled)},{source:"env",value:readBool(process.env.BEEOS_MESSAGE_BUS_ENABLED)}],{source:"default",value:!(!W.value&&!u.value)}),Y={platformUrl:u.value.replace(/\/+$/,""),bridge:{url:d.value,keyFile:m.value,service:f.value,mode:trimStr(p.mode),shell:{enabled:g.value},promptTimeoutMs:T.value,historyPendingTimeoutMs:readInt(p.historyPendingTimeoutMs)},gateway:{url:n.value,token:i.value,protocol:t.value,clientId:c.value,agentId:v.value},retry:{baseMs:Math.max(0,_.value),maxMs:Math.max(0,S.value),maxAttempts:Math.max(0,I.value)},log:{enabled:A.value,verbose:C.value,dir:O.value},promptTimeoutMs:T.value,terminalWsPort:R.value,terminalWsAllowedOrigins:L.value,streamIdleTimeoutMs:Math.max(0,B.value),streamStaleTimeoutMs:Math.max(0,y.value),loadReplayIdleTimeoutMs:Math.max(0,h.value),cronFlushTimeoutMs:Math.max(0,w.value),cronSignalWindowMs:Math.max(0,b.value),canvasRelayUrl:U.value.replace(/\/+$/,""),canvasEnabled:P.value,openclawCanvasCompat:N.value,messageBus:{enabled:j.value,serviceUrl:W.value,apiKey:G.value,url:D.value,principalId:x.value,a2aEnabled:H.value}},K={platformUrl:u.source,bridge:{url:d.source,keyFile:m.source,service:f.source,shell:{enabled:g.source}},gateway:{url:n.source,token:i.source,protocol:t.source,clientId:c.source,agentId:v.source},retry:{baseMs:_.source,maxMs:S.source,maxAttempts:I.source},log:{enabled:A.source,verbose:C.source,dir:O.source},promptTimeoutMs:T.source,terminalWsPort:R.source,terminalWsAllowedOrigins:L.source,streamIdleTimeoutMs:B.source,streamStaleTimeoutMs:y.source,loadReplayIdleTimeoutMs:h.source,cronFlushTimeoutMs:w.source,cronSignalWindowMs:b.source,canvasRelayUrl:U.source,canvasEnabled:P.source,openclawCanvasCompat:N.source,messageBus:{enabled:j.source,serviceUrl:W.source,apiKey:G.source,url:D.source,principalId:x.source,a2aEnabled:H.source}},$=[];return Y.platformUrl||$.push({code:"CONFIG_PLATFORM_URL_MISSING",severity:"error",message:"platformUrl is required for file uploads and platform callbacks",nextSteps:[`Set plugins.entries.beeos-claw.config.platformUrl in ${a}`,"Or set the BEEOS_PLATFORM_URL environment variable"]}),Y.gateway.token||$.push({code:"CONFIG_GATEWAY_TOKEN_MISSING",severity:"warn",message:"gateway.token is empty; Gateway auth may fail if authentication is enabled",nextSteps:[`Set gateway.auth.token in ${a}`,`Or set plugins.entries.beeos-claw.config.gateway.token in ${a}`]}),{config:Y,sources:K,validation:$,openclawConfigPath:a}}export function resolveConfig(e){const{config:r,validation:o}=resolveConfigWithMeta({pluginConfig:e}),a=o.find(e=>"error"===e.severity);if(a)throw new Error(`[beeos-claw] ${a.message}`);return r}const MIRROR_PATH=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","openclaw.json");export function mirrorOpenClawConfig(e){if(!e||"object"!=typeof e||Array.isArray(e))return{copied:!1,destinationPath:MIRROR_PATH,reason:"config_invalid"};try{return mkdirSync(dirname(MIRROR_PATH),{recursive:!0}),writeFileSync(MIRROR_PATH,JSON.stringify(e,null,2)+"\n",{encoding:"utf8",mode:384}),{copied:!0,destinationPath:MIRROR_PATH}}catch(e){return{copied:!1,destinationPath:MIRROR_PATH,reason:e instanceof Error?e.message:"write_failed"}}}
@@ -1 +1 @@
1
- const SHARED_FIELDS=[{name:"name",label:"Task Name",inputType:"text",required:!0,placeholder:"e.g. daily-report",apiPath:"name"},{name:"description",label:"Description",inputType:"textarea",placeholder:"What does this task do?",apiPath:"description"},{name:"enabled",label:"Enabled",inputType:"toggle",defaultValue:!0,apiPath:"enabled"},{name:"scheduleKind",label:"Schedule Type",inputType:"select",required:!0,options:[{label:"Interval (every N ms)",value:"every"},{label:"Cron Expression",value:"cron"},{label:"Specific Time",value:"at"}],apiPath:"schedule.kind"},{name:"everyMs",label:"Interval (ms)",inputType:"number",placeholder:"3600000 = 1 hour",validation:{min:1e3},apiPath:"schedule.everyMs"},{name:"cronExpr",label:"Cron Expression",inputType:"text",placeholder:"e.g. 0 9 * * *",apiPath:"schedule.expr"},{name:"at",label:"Run At (ISO 8601)",inputType:"text",placeholder:"e.g. 2026-04-01T09:00:00Z",apiPath:"schedule.at"},{name:"tz",label:"Timezone",inputType:"text",placeholder:"e.g. Asia/Shanghai (optional)",apiPath:"schedule.tz"},{name:"message",label:"Agent Message",inputType:"textarea",required:!0,placeholder:"What should the agent do when this task fires?",apiPath:"payload.message"}];function cronTransformPayload(e){const a={};void 0!==e.name&&(a.name=e.name),void 0!==e.description&&(a.description=e.description),void 0!==e.enabled&&(a.enabled=e.enabled);const n=e.scheduleKind;if(n){const t={kind:n};"every"===n&&e.everyMs&&(t.everyMs=Number(e.everyMs)),"cron"===n&&e.cronExpr&&(t.expr=e.cronExpr),"at"===n&&e.at&&(t.at=e.at),e.tz&&(t.tz=e.tz),a.schedule=t}const t=e.message;return void 0!==t&&(a.payload={kind:"agentTurn",message:String(t)}),a}function cronCreateTransform(e){const a=cronTransformPayload(e);return a.name||(a.name="Untitled"),void 0===a.enabled&&(a.enabled=!0),a.schedule||(a.schedule={kind:"every",everyMs:36e5}),a.sessionTarget="main",a.wakeMode="now",a.payload||(a.payload={kind:"agentTurn",message:""}),a}export const CRON_CREATE_SCHEMA={domain:"cron",operation:"create",title:"Create Scheduled Task",description:"Add a new recurring or one-time agent task.",fields:SHARED_FIELDS,submitLabel:"Create",targetRpc:"cron.add",transformPayload:cronCreateTransform};export const CRON_UPDATE_SCHEMA={domain:"cron",operation:"update",title:"Edit Scheduled Task",description:"Modify an existing scheduled task.",fields:SHARED_FIELDS,submitLabel:"Update",targetRpc:"cron.update",transformPayload:cronTransformPayload};
1
+ const SHARED_FIELDS=[{name:"name",label:"Task Name",inputType:"text",required:!0,placeholder:"e.g. daily-report",apiPath:"name"},{name:"description",label:"Description",inputType:"textarea",placeholder:"What does this task do?",apiPath:"description"},{name:"enabled",label:"Enabled",inputType:"toggle",defaultValue:!0,apiPath:"enabled"},{name:"scheduleKind",label:"Schedule Type",inputType:"select",required:!0,options:[{label:"Interval (every N ms)",value:"every"},{label:"Cron Expression",value:"cron"},{label:"Specific Time",value:"at"}],apiPath:"schedule.kind"},{name:"everyMs",label:"Interval (ms)",inputType:"number",placeholder:"3600000 = 1 hour",validation:{min:1e3},apiPath:"schedule.everyMs"},{name:"cronExpr",label:"Cron Expression",inputType:"text",placeholder:"e.g. 0 9 * * *",apiPath:"schedule.expr"},{name:"at",label:"Run At (ISO 8601)",inputType:"text",placeholder:"e.g. 2026-04-01T09:00:00Z",apiPath:"schedule.at"},{name:"tz",label:"Timezone",inputType:"text",placeholder:"e.g. Asia/Shanghai (optional)",apiPath:"schedule.tz"},{name:"message",label:"Agent Message",inputType:"textarea",required:!0,placeholder:"What should the agent do when this task fires?",apiPath:"payload.message"}];function cronTransformPayload(e){const a={};void 0!==e.name&&(a.name=e.name),void 0!==e.description&&(a.description=e.description),void 0!==e.enabled&&(a.enabled=e.enabled);const t=e.scheduleKind;if(t){const n={kind:t};"every"===t&&e.everyMs&&(n.everyMs=Number(e.everyMs)),"cron"===t&&e.cronExpr&&(n.expr=e.cronExpr),"at"===t&&e.at&&(n.at=e.at),e.tz&&(n.tz=e.tz),a.schedule=n}const n=e.message;if(void 0!==n){const t="isolated"===e.sessionTarget;a.payload=t?{kind:"agentTurn",message:String(n)}:{kind:"systemEvent",text:String(n)}}return a}function cronCreateTransform(e){const a=cronTransformPayload(e);return a.name||(a.name="Untitled"),void 0===a.enabled&&(a.enabled=!0),a.schedule||(a.schedule={kind:"every",everyMs:36e5}),a.sessionTarget="main",a.wakeMode="now",a.payload||(a.payload={kind:"systemEvent",text:""}),a}export const CRON_CREATE_SCHEMA={domain:"cron",operation:"create",title:"Create Scheduled Task",description:"Add a new recurring or one-time agent task.",fields:SHARED_FIELDS,submitLabel:"Create",targetRpc:"cron.add",transformPayload:cronCreateTransform};export const CRON_UPDATE_SCHEMA={domain:"cron",operation:"update",title:"Edit Scheduled Task",description:"Modify an existing scheduled task.",fields:SHARED_FIELDS,submitLabel:"Update",targetRpc:"cron.update",transformPayload:cronTransformPayload};
@@ -1 +1 @@
1
- const A2UI_VERSION="0.8",BEEOS_EXTENDED_CATALOG="https://beeos.ai/a2ui/catalog/v1";function getByPath(e,t){if(!e)return;const a=t.split(".");let s=e;for(const e of a){if(null==s||"object"!=typeof s)return;s=s[e]}return s}function setByPath(e,t,a){const s=t.split(".");let i=e;for(let e=0;e<s.length-1;e++){const t=s[e];null!=i[t]&&"object"==typeof i[t]||(i[t]={}),i=i[t]}i[s[s.length-1]]=a}let idCounter=0;function nextId(){return`fsb_${++idCounter}_${Date.now().toString(36)}`}export class FormSurfaceBuilder{deps;constructor(e){this.deps=e}build(e){const{schema:t,data:a,entityId:s,onSuccess:i}=e,n=e.surfaceId??`${t.domain}-${t.operation}-form`,o=e.presentation??"panel",d=`${t.domain}_${t.operation}_submit`,r=t.fields.map(e=>this.schemaFieldToA2UI(e,a)),c=[{id:nextId(),type:"Form",properties:{title:t.title,...t.description?{description:t.description}:{},fields:r,submitLabel:t.submitLabel,submitActionId:d}}],p=this.deps.canvasState.has(n),l=async e=>{if((e.userAction?.name??"")!==d)return;const a=e.userAction?.context??{};await this.handleSubmit(t,a,n,c,s,i)},u=this.buildEnvelope(n,{surfaceUpdate:{surfaceId:n,components:c}});if(p)this.deps.canvasState.update(n,c),this.deps.canvasState.bindActionHandler(n,l),this.deps.sendCanvasUpdate("_active",u);else{const e=this.buildEnvelope(n,{beginRendering:{surfaceId:n,catalogId:BEEOS_EXTENDED_CATALOG}},o);this.deps.canvasState.create(n,o,c,{origin:{kind:"system",handlerId:`${t.domain}-form`},actionHandler:l}),this.deps.sendCanvasUpdate("_active",u),this.deps.sendCanvasUpdate("_active",e)}this.deps.logger.debug?.(`[form-builder] ${p?"Updated":"Created"} form surface=${n} domain=${t.domain} op=${t.operation}`)}schemaFieldToA2UI(e,t){const a={name:e.name,label:e.label,inputType:e.inputType};e.placeholder&&(a.placeholder=e.placeholder),e.required&&(a.required=!0),e.options&&(a.options=e.options),e.validation&&(a.validation=e.validation);let s=e.defaultValue;return void 0===s&&t&&e.apiPath&&(s=getByPath(t,e.apiPath)),void 0!==s&&(a.defaultValue=s),a}async handleSubmit(e,t,a,s,i,n){try{const s=e.transformPayload?e.transformPayload(t):this.autoTransform(e.fields,t),o="update"===e.operation&&i?{id:i,patch:s}:s;await this.deps.gatewayRequest(e.targetRpc,o),this.deps.logger.info?.(`[form-builder] Submit success: ${e.targetRpc} (${e.domain}:${e.operation})`);const d={id:nextId(),type:"Alert",properties:{variant:"success",title:"Done",message:`${e.title} saved successfully.`,dismissible:!0}};this.deps.sendCanvasUpdate("_active",this.buildEnvelope(a,{surfaceUpdate:{surfaceId:a,components:[d]}})),this.deps.canvasState.update(a,[d]),setTimeout(()=>{this.deps.sendCanvasUpdate("_active",this.buildEnvelope(a,{deleteSurface:{surfaceId:a}})),this.deps.canvasState.delete(a)},3e3),n&&await n()}catch(t){const i=t instanceof Error?t.message:String(t);this.deps.logger.warn?.(`[form-builder] Submit failed: ${e.targetRpc} — ${i}`);const n=[...s,{id:nextId(),type:"Alert",properties:{variant:"error",title:"Failed",message:i,dismissible:!0}}];this.deps.sendCanvasUpdate("_active",this.buildEnvelope(a,{surfaceUpdate:{surfaceId:a,components:n}})),this.deps.canvasState.update(a,n)}}autoTransform(e,t){const a={};for(const s of e){const e=t[s.name];if(void 0===e||""===e)continue;setByPath(a,s.apiPath??s.name,e)}return a}buildEnvelope(e,t,a){return{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:e,...a?{presentation:a}:{},message:t}}}
1
+ import{sv,nextComponentId}from"./a2ui-helpers.js";function getByPath(e,t){if(!e)return;const n=t.split(".");let a=e;for(const e of n){if(null==a||"object"!=typeof a)return;a=a[e]}return a}function setByPath(e,t,n){if(!t)return;const a=t.split(".");let o=e;for(let e=0;e<a.length-1;e++){const t=a[e];null!=o[t]&&"object"==typeof o[t]||(o[t]={}),o=o[t]}o[a[a.length-1]]=n}function nextId(){return nextComponentId("fsb")}export class FormSurfaceBuilder{deps;constructor(e){this.deps=e}cid(e){return this.deps.resolveCanvasId?.()??e??`_form_${Date.now()}`}build(e){const{schema:t,data:n,entityId:a,onSuccess:o}=e,i=e.surfaceId??`${t.domain}-${t.operation}-form`,s=e.presentation??"panel",l=`${t.domain}_${t.operation}_submit`,p=[],d=[];if(t.title){const e=nextId();p.push({id:e,component:{Text:{text:sv(t.title),usageHint:sv("h3")}}}),d.push(e)}if(t.description){const e=nextId();p.push({id:e,component:{Text:{text:sv(t.description)}}}),d.push(e)}const u={};for(const e of t.fields){const t=nextId(),a=this.schemaFieldToA2UI(e,n),o=`/${e.name}`;if("checkbox"===a.inputType||"boolean"===a.inputType||"toggle"===a.inputType){const n=!!a.defaultValue;u[e.name]=n,p.push({id:t,component:{CheckBox:{label:sv(String(a.label??"")),value:{path:o}}}})}else if("select"===a.inputType||"multi_select"===a.inputType||"dropdown"===a.inputType||"radio"===a.inputType){const n=Array.isArray(a.options)?a.options:[],i=n.map(e=>"string"==typeof e?{label:sv(e),value:sv(e)}:{label:sv(String(e.label??e.value??"")),value:sv(String(e.value??e.label??""))}),s={label:sv(String(a.label??"")),options:i,selections:{path:o}};if(null!=a.defaultValue)u[e.name]=String(a.defaultValue);else if(n.length>0){const t=n[0];u[e.name]="string"==typeof t?t:String(t.value??"")}else u[e.name]="";p.push({id:t,component:{MultipleChoice:s}})}else if("datetime"===a.inputType||"time"===a.inputType){const n="datetime"===a.inputType,i=!0;u[e.name]=null!=a.defaultValue?String(a.defaultValue):"",p.push({id:t,component:{DateTimeInput:{value:{path:o},enableDate:n,enableTime:i}}})}else if("range"===a.inputType){const n="number"==typeof a.min?a.min:0,i="number"==typeof a.max?a.max:100;u[e.name]=null!=a.defaultValue?Number(a.defaultValue):n,p.push({id:t,component:{Slider:{label:sv(String(a.label??"")),value:{path:o},minValue:n,maxValue:i}}})}else{const n="number"===a.inputType?"number":"date"===a.inputType?"date":"textarea"===a.inputType?"longText":"shortText",i={label:sv(String(a.label??"")),type:sv(n),text:{path:o}};u[e.name]=null!=a.defaultValue?String(a.defaultValue):"",p.push({id:t,component:{TextField:i}})}d.push(t)}const r=t.fields.map(e=>({key:e.name,value:{path:`/${e.name}`}})),c=nextId();p.push({id:c,component:{Text:{text:sv(t.submitLabel??"Submit")}}});const m=nextId();p.push({id:m,component:{Button:{child:c,action:{name:l,context:r}}}}),d.push(m);const h=nextId();p.push({id:h,component:{Column:{children:{explicitList:d}}}});const f=this.cid(e.canvasId),x=this.deps.canvasState.has(i,f),b=async e=>{if((e.userAction?.name??"")!==l)return;const n=e.userAction?.context??{};await this.handleSubmit(t,n,i,p,f,a,o)};x?(this.deps.canvasState.update(i,p,f,h,u),this.deps.canvasState.bindActionHandler(i,b,f)):this.deps.canvasState.create(f,i,s,p,{origin:{kind:"system",handlerId:`${t.domain}-form`},actionHandler:b,rootComponentId:h,dataModel:u}),this.deps.logger.debug?.(`[form-builder] ${x?"Updated":"Created"} form surface=${i} domain=${t.domain} op=${t.operation}`)}schemaFieldToA2UI(e,t){const n={name:e.name,label:e.label,inputType:e.inputType};e.placeholder&&(n.placeholder=e.placeholder),e.required&&(n.required=!0),e.options&&(n.options=e.options);let a=e.defaultValue;if(void 0===a&&t){a=getByPath(t,e.apiPath??e.name)}return null!=a&&(n.defaultValue=a),n}async handleSubmit(e,t,n,a,o,i,s){try{const a=e.transformPayload?e.transformPayload(t):this.autoTransform(e.fields,t),l="update"===e.operation&&i?{id:i,patch:a}:a;await this.deps.gatewayRequest(e.targetRpc,l),this.deps.logger.info?.(`[form-builder] Submit success: ${e.targetRpc} (${e.domain}:${e.operation})`);const p=nextId(),d=nextId(),u=nextId(),r=nextId(),c=[{id:p,component:{Icon:{name:sv("check")}}},{id:d,component:{Text:{text:sv(`${e.title} saved successfully.`)}}},{id:u,component:{Row:{children:{explicitList:[p,d]}}}},{id:r,component:{Card:{child:u}}}];this.deps.canvasState.update(n,c,o),s&&await s()}catch(t){const i=t instanceof Error?t.message:String(t);this.deps.logger.warn?.(`[form-builder] Submit failed: ${e.targetRpc} — ${i}`);const s=nextId(),l=nextId(),p=nextId(),d=nextId(),u=[...a,...[{id:s,component:{Icon:{name:sv("error")}}},{id:l,component:{Text:{text:sv(i)}}},{id:p,component:{Row:{children:{explicitList:[s,l]}}}},{id:d,component:{Card:{child:p}}}]];this.deps.canvasState.update(n,u,o)}}autoTransform(e,t){const n={};for(const a of e){const e=t[a.name];if(void 0===e||""===e)continue;setByPath(n,a.apiPath??a.name,e)}return n}}
@@ -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: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
+ 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:s}=generateKeyPairSync("ed25519"),r=i.export({type:"spki",format:"der"}).subarray(-32),o=s.export({type:"pkcs8",format:"der"}),c=createHash("sha256").update(r).digest("hex"),a=base64urlEncode(r),h={deviceId:c,publicKeyB64url:a,privateKeyDer:o};return writeFileSync(n,JSON.stringify({deviceId:c,publicKey:a,privateKey:o.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(),s=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:s,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 s=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),o=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(()=>{s=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)o(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===s){if(i.ok){const e=i.payload;o(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"],s={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});s.nonce=e,s.signature=signDeviceAuth(this.deviceIdentity,r)}const r=randomUUID(),o={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:s}};return this.ws?.send(JSON.stringify(o)),r}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const i=randomUUID(),s={type:"req",id:i,method:e,params:t};return new Promise((t,r)=>{const o=setTimeout(()=>{this.pending.delete(i),r(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(i,{resolve:t,reject:r,timer:o}),this.ws.send(JSON.stringify(s))})}async agentSend(e,t,n,i,s){const r={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};i?.length&&(r.attachments=i),s&&(r.inputProvenance=s);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})}async sessionsDelete(e){return this.request("sessions.delete",{key:e})}async sessionsReset(e){return this.request("sessions.reset",{key:e,reason:"new"})}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}}
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload-result-cache.js";import{handleFileStage}from"./file-stage.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal-websocket.js";import{TerminalManager}from"./terminal-session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{CanvasStateManager}from"./canvas-state.js";import{CanvasApiClient}from"./canvas-api-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas-tools.js";import{FormSchemaRegistry}from"./form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./render-form-tool.js";import{ResourceDescriptorRegistry}from"./resource-descriptor-registry.js";import{CRON_RESOURCE}from"./cron-resource.js";import{generateActionTools,RENDER_RESOURCE_TOOL_NAME,RENDER_RESOURCE_TOOL_SCHEMA,buildRenderResourceDescription,createRenderResourceHandler}from"./action-tool-generator.js";const PLUGIN_VERSION=resolvePluginVersion();let pluginConfig=null,acpServer=null,agentKeys=null;const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return void e.logger.error?.("[beeos-claw] Failed to resolve config:",r)}const a=o.config;pluginConfig=a;const t=a.log.enabled?createObsEventWriter({dir:a.log.dir,redaction:{gatewayToken:a.gateway.token},logger:e.logger}):()=>{};t({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),t({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:a,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)t({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.bridge.keyFile)try{agentKeys=loadAgentKeyPair(a.bridge.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const s=await executeUpload(o,a,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${s.ok}`),t({component:"upload-tool",domain:"tool",name:s.ok?"upload.success":"upload.failure",severity:s.ok?"info":"warn",payload:{toolCallId:r,ok:s.ok,fileCount:s.files?.length??0,error:s.error}}),s.ok&&s.content&&acpServer&&acpServer.pushToolResultContent(r,BEEOS_UPLOAD_TOOL_NAME,s.content),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(s)}}),e.registerHttpRoute({path:"/beeos/files/stage",async handler(e,r){await handleFileStage(e,r,a)}});const s=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0;if(acpServer=new ACPServer(a,e.logger,t,s),e.registerService({id:"beeos-acp",async start(){await acpServer.start(),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await(acpServer?.stop()),e.logger.info?.("[beeos-claw] ACP Server stopped")}}),a.canvasEnabled){const r=new CanvasStateManager;let o=null;a.platformUrl&&agentKeys?(o=new CanvasApiClient({platformUrl:a.platformUrl,agentKeys:agentKeys}),r.setRelayOpts({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,logger:e.logger}),e.logger.info?.("[beeos-claw] Canvas API client + Relay opts configured")):e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: platformUrl or agentKeys missing");const s={stateManager:r,sendCanvasUpdate:(e,r)=>{acpServer.sendCanvasUpdate(e,r)},logger:e.logger,writeObs:e=>t(e),isCanvasActive:()=>acpServer.isCanvasActive(),canvasApiClient:o,getActiveSessionId:e=>acpServer.getSessionIdForToolCall(e)},n=new FormSchemaRegistry;n.register(CRON_CREATE_SCHEMA),n.register(CRON_UPDATE_SCHEMA);const i=new ResourceDescriptorRegistry;i.register(CRON_RESOURCE),e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:createRenderUiHandler(s)}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:createCloseUiHandler(s)}),acpServer.setCanvasState(r,{schemaRegistry:n,descriptorRegistry:i});const l=acpServer.getFormBuilder();l&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:createRenderFormHandler({registry:n,builder:l,logger:e.logger})});const c=acpServer.getResourceRenderer();if(c){generateActionTools(i,e,async(e,r)=>{const o=acpServer?.gateway;if(!o?.isConnected)throw new Error("Gateway not connected");return o.request(e,r)},c,e.logger),e.registerTool({name:RENDER_RESOURCE_TOOL_NAME,description:buildRenderResourceDescription(i),parameters:RENDER_RESOURCE_TOOL_SCHEMA,execute:createRenderResourceHandler(i,c,e.logger)})}const g=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME,RENDER_RESOURCE_TOOL_NAME]);if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, render_form, or render_resource. Respond with plain text only."}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(!g.has(e.toolName))return;return(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form, render_resource + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const n=new TerminalManager,i=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:n,logger:e.logger,writeObs:t});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){i.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){i.stop(),n.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r){const o=mirrorOpenClawConfig(r);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}e.logger.info?.("[beeos-claw] Plugin registered successfully")}};export default plugin;
1
+ import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageSDK}from"./messaging/message-sdk.js";import{registerA2ATools}from"./a2a/tools.js";import{createA2ATaskHandler}from"./a2a/task-handler.js";import{createA2AResultHandler}from"./a2a/result-handler.js";import{createA2ATaskCancelHandler}from"./a2a/cancel-handler.js";import{TaskHandleRegistry}from"./a2a/task-handle.js";import{pollPendingTasks}from"./a2a/poll-pending.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload-tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload-result-cache.js";import{handleFileStage}from"./file-stage.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal-websocket.js";import{TerminalManager}from"./terminal-session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{CanvasStateManager}from"./canvas-state.js";import{CanvasRelayClient}from"./canvas-relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas-tools.js";import{FormSchemaRegistry}from"./form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./render-form-tool.js";import{ResourceDescriptorRegistry}from"./resource-descriptor-registry.js";import{CRON_RESOURCE}from"./cron-resource.js";import{generateActionTools,RENDER_RESOURCE_TOOL_NAME,RENDER_RESOURCE_TOOL_SCHEMA,buildRenderResourceDescription,createRenderResourceHandler}from"./action-tool-generator.js";const PLUGIN_VERSION=resolvePluginVersion();let pluginConfig=null,acpServer=null,agentKeys=null;const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){let r,o;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return void e.logger.error?.("[beeos-claw] Failed to resolve config:",r)}const a=o.config;pluginConfig=a;const s=a.log.enabled?createObsEventWriter({dir:a.log.dir,redaction:{gatewayToken:a.gateway.token},logger:e.logger}):()=>{};s({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),s({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:a,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const r of o.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.bridge.keyFile)try{agentKeys=loadAgentKeyPair(a.bridge.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,o){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(o)}`);const t=await executeUpload(o,a,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${t.ok}`),s({component:"upload-tool",domain:"tool",name:t.ok?"upload.success":"upload.failure",severity:t.ok?"info":"warn",payload:{toolCallId:r,ok:t.ok,fileCount:t.files?.length??0,error:t.error}}),t.ok&&t.content&&acpServer&&acpServer.pushToolResultContent(r,BEEOS_UPLOAD_TOOL_NAME,t.content),(async o=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(o)}`)}return o})(t)}}),e.registerHttpRoute({path:"/beeos/files/stage",async handler(e,r){await handleFileStage(e,r,a)}});const t=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,n=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;acpServer=new ACPServer(a,e.logger,s,t,n),e.registerService({id:"beeos-acp",async start(){await acpServer.start(),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await(acpServer?.stop()),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const l=new TaskHandleRegistry;let i=null;const{enabled:c,url:g,principalId:d,a2aEnabled:p}=a.messageBus;if(c&&a.messageBus.serviceUrl){i=new MessageSDK({serviceUrl:a.messageBus.serviceUrl,apiKey:a.messageBus.apiKey,centrifugoUrl:g},e.logger);const r=d;e.registerService({id:"beeos-message-bus",async start(){let o=r;if(o||(o=process.env.BEEOS_PRINCIPAL_ID||""),!o&&acpServer)try{await acpServer.waitForAgentMapping(),o=acpServer.getDefaultPlatformAgentId()||""}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}o?(await i.connect(o,g),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s)",o)):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(i?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const o=(e,r,o,a)=>acpServer.agentSend(e,r,o,a),s=(e,r,o)=>acpServer.trackA2ARun(e,r,o),t=e=>acpServer.emitA2ASystemNotification(e),n=a.platformUrl?{platformUrl:a.platformUrl,agentKeys:agentKeys,logger:e.logger}:void 0;i.onMessage("a2a_task",createA2ATaskHandler(o,n,s,t)),i.onMessage("a2a_result",createA2AResultHandler(o,l,t)),i.onMessage("a2a_task_cancel",createA2ATaskCancelHandler(o,e=>acpServer.cancelA2ARunByTaskId(e))),n&&e.registerService({id:"beeos-a2a-poll",async start(){await acpServer.waitForAgentMapping();const r=await pollPendingTasks(n,o,s);r>0&&e.logger.info?.("[beeos-claw] Dispatched %d pending A2A tasks on startup",r)}})}else c?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",c);if(!1!==p&&a.platformUrl){const r=e=>acpServer?.getGatewaySessionKeyForToolCall(e),o=e=>acpServer?.getMentionsForToolCall(e),s=(e,r)=>{acpServer?.broadcastNotification(e,r)};registerA2ATools(e,{platformUrl:a.platformUrl,agentKeys:agentKeys,logger:e.logger},l,r,o,s),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(a.canvasEnabled){const r=new CanvasStateManager;if(a.platformUrl&&agentKeys){r.setRelayOpts({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,logger:e.logger});(async()=>{try{await acpServer.waitForAgentMapping();const o=acpServer.getDefaultPlatformAgent(),s=new CanvasRelayClient({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,agentId:o?.id,displayName:o?.displayName,avatarUrl:o?.avatarUrl,logger:e.logger});s.connect(),r.setRelayClient(s),o?.id&&r.setAgentId(o.id),acpServer.wireRelayActionHandler(),acpServer.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${o?.id??"none"}, name=${o?.name??"none"})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}})(),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: platformUrl or agentKeys missing");const o={stateManager:r,logger:e.logger,writeObs:e=>s(e),isCanvasActive:()=>acpServer.isCanvasActive(),getActiveSessionId:e=>acpServer.getSessionIdForToolCall(e),getPlatformAgentId:e=>acpServer.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>acpServer.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>acpServer.waitForAgentMapping()},t=new FormSchemaRegistry;t.register(CRON_CREATE_SCHEMA),t.register(CRON_UPDATE_SCHEMA);const n=new ResourceDescriptorRegistry;n.register(CRON_RESOURCE);const l=e=>async(r,o)=>(acpServer.preMapToolCallSession(r),e(r,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:l(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:l(createCloseUiHandler(o))}),acpServer.setCanvasState(r,{schemaRegistry:t,descriptorRegistry:n});const i=acpServer.getFormBuilder();i&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(t),parameters:RENDER_FORM_TOOL_SCHEMA,execute:l(createRenderFormHandler({registry:t,builder:i,logger:e.logger}))});const c=acpServer.getResourceRenderer();if(c){generateActionTools(n,e,async(e,r)=>{const o=acpServer?.gateway;if(!o?.isConnected)throw new Error("Gateway not connected");return o.request(e,r)},c,e.logger),e.registerTool({name:RENDER_RESOURCE_TOOL_NAME,description:buildRenderResourceDescription(n),parameters:RENDER_RESOURCE_TOOL_SCHEMA,execute:l(createRenderResourceHandler(n,c,e.logger))})}const g=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME,RENDER_RESOURCE_TOOL_NAME]);if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, render_form, or render_resource. Respond with plain text only."}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(r?.sessionKey&&acpServer.setPendingToolCallSessionKey(r.sessionKey),!g.has(e.toolName))return;return(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form, render_resource + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,_=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:s});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){_.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){_.stop(),m.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r){const o=mirrorOpenClawConfig(r);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}e.logger.info?.("[beeos-claw] Plugin registered successfully")}};export default plugin;
@@ -0,0 +1 @@
1
+ import{Centrifuge}from"centrifuge";import WebSocket from"ws";export class MessageSDK{config;client=null;personalSub=null;principalId="";handlers=new Map;logger;constructor(e,t){this.config=e,this.logger=t}async getToken(e,t,n){const s={principal_id:e};return t&&(s.claims=t),n&&(s.ttl_seconds=n),this.restRequest("POST","/api/v1/tokens",s)}async send(e,t,n){await this.restRequest("POST","/api/v1/messages/send",{target_id:e,message_type:t,payload:n})}async isOnline(e){return(await this.restRequest("GET",`/api/v1/connections/${encodeURIComponent(e)}/online`)).online}async registerWebhook(e,t,n){const s={url:e,events:t};return n&&(s.secret=n),this.restRequest("POST","/api/v1/webhooks",s)}async removeWebhook(e){await this.restRequest("DELETE",`/api/v1/webhooks/${encodeURIComponent(e)}`)}async connect(e,t){this.client&&await this.disconnect(),this.principalId=e;const n=await this.getToken(e),s=t||this.config.centrifugoUrl||n.centrifugo_url;if(!s)throw new Error("No Centrifugo URL available");this.client=new Centrifuge(s,{token:n.token,getToken:async()=>(await this.getToken(e)).token,websocket:WebSocket}),this.client.on("connected",e=>{this.logger.info?.(`[message-sdk] Connected (client=${e.client})`)}),this.client.on("disconnected",e=>{this.logger.warn?.(`[message-sdk] Disconnected: code=${e.code} reason=${e.reason}`)}),this.client.on("error",e=>{this.logger.error?.("[message-sdk] Error:",e.error)}),this.client.connect(),this.personalSub=this.client.newSubscription(`personal:${e}`),this.personalSub.on("publication",e=>{this.dispatch(e.data).catch(e=>{this.logger.error?.("[message-sdk] dispatch error:",e)})}),this.personalSub.subscribe()}async disconnect(){this.personalSub?.unsubscribe(),this.client?.disconnect(),this.client=null,this.personalSub=null}onMessage(e,t){this.handlers.set(e,t)}async joinGroup(e,t){if(!this.client)throw new Error("Not connected");const n=this.client.newSubscription(`group:${e}`);return n.on("publication",n=>{t(n.data).catch(t=>{this.logger.error?.(`[message-sdk] group handler error (${e}):`,t)})}),n.subscribe(),n}async publishToGroup(e,t){if(!this.client)throw new Error("Not connected");await this.client.publish(`group:${e}`,{from:this.principalId,content:t,timestamp:Date.now()})}get isConnected(){return"connected"===this.client?.state}get currentPrincipalId(){return this.principalId}async dispatch(e){const t=this.handlers.get(e.type);t?await t(e):this.logger.debug?.(`[message-sdk] No handler for type=${e.type}`)}async restRequest(e,t,n){const s=`${this.config.serviceUrl}${t}`,i={"X-API-Key":this.config.apiKey},o={method:e,headers:i};void 0!==n&&(i["Content-Type"]="application/json",o.body=JSON.stringify(n));const r=await fetch(s,o);if(!r.ok){const n=await r.text().catch(()=>"");throw new Error(`Message Service ${e} ${t} returned ${r.status}: ${n}`)}if(204!==r.status)return await r.json()}}
@@ -1 +1 @@
1
- import{resolveColumnValue,resolveActionLabel}from"./resource-descriptor.js";const A2UI_VERSION="0.8",BEEOS_EXTENDED_CATALOG="https://beeos.ai/a2ui/catalog/v1";let idCounter=0;function nextId(){return`rsr_${++idCounter}_${Date.now().toString(36)}`}export class ResourceSurfaceRenderer{deps;constructor(e){this.deps=e}async render(e,t="panel"){try{const n=await this.deps.gatewayRequest(e.listRpc,e.listParams??{}),s=e.extractItems(n);this.buildAndSendSurface(e,s,t)}catch(t){this.deps.logger.warn?.(`[resource-renderer] Failed to render ${e.domain}: ${t instanceof Error?t.message:t}`)}}buildAndSendSurface(e,t,n){const s=this.surfaceId(e,n),r=this.buildComponents(e,t),i=this.deps.canvasState.has(s),o=this.envelope(s,{surfaceUpdate:{surfaceId:s,components:r}}),a=t=>this.handleAction(e,t,n);if(i)this.deps.canvasState.update(s,r),this.deps.canvasState.bindActionHandler(s,a),this.deps.sendCanvasUpdate("_active",o);else{const t=this.envelope(s,{beginRendering:{surfaceId:s,catalogId:BEEOS_EXTENDED_CATALOG}},n);this.deps.canvasState.create(s,n,r,{origin:{kind:"system",handlerId:`${e.domain}-resource`},actionHandler:a}),this.deps.sendCanvasUpdate("_active",o),this.deps.sendCanvasUpdate("_active",t)}this.deps.logger.debug?.(`[resource-renderer] ${i?"Updated":"Created"} surface=${s} domain=${e.domain} items=${t.length}`)}buildComponents(e,t){const n=[],s=nextId();if(n.push({id:s,type:"Text",properties:{text:e.title,variant:"heading"}}),0===t.length){const t=nextId();n.push({id:t,type:"Callout",properties:{variant:"note",title:e.emptyTitle??"No items",content:e.emptyContent??""}});const r=nextId();return n.push({id:r,type:"Card",properties:{},children:[s,t]}),n}const r=e.columns.map(e=>({key:e.key,label:e.label})),i=t.map(t=>({cells:e.columns.map(e=>resolveColumnValue(e,t))})),o=nextId();n.push({id:o,type:"Table",properties:{title:"",columns:r,rows:i}});const a=[];for(const s of t)for(const t of e.itemActions){const r=nextId(),i=resolveActionLabel(t.label,s),o=`${e.domain}_${t.id}`;let d;d="rpc"===t.type?t.buildParams(s):{__itemId:e.itemId(s),...t.buildFormData?t.buildFormData(s):{}},n.push({id:r,type:"Button",properties:{label:i,actionId:o,context:d}}),a.push(r)}const d=nextId();n.push({id:d,type:"Grid",properties:{columns:Math.min(e.itemActions.length,4),gap:"sm"},children:a});const c=[];for(const t of e.globalActions){const s=nextId(),r=`${e.domain}_${t.id}`;n.push({id:s,type:"Button",properties:{label:t.label,actionId:r}}),c.push(s)}const p=[s,o,d,...c],l=nextId();return n.push({id:l,type:"Card",properties:{},children:p}),n}async handleAction(e,t,n){const s=t.userAction?.name??"",r=t.userAction?.context??{},i=`${e.domain}_`;if(!s.startsWith(i))return void this.deps.logger.debug?.(`[resource-renderer] Ignoring action ${s} (not in domain ${e.domain})`);const o=s.slice(i.length),a=e.globalActions.find(e=>e.id===o);if(a){if("refresh"===a.type)return void await this.render(e,n);if("form"===a.type)return void this.openForm(a.formDomain,a.formOperation,void 0,void 0,e,n)}const d=e.itemActions.find(e=>e.id===o);if(d){try{if("rpc"===d.type)await this.deps.gatewayRequest(d.rpc,r);else if("form"===d.type){const t=r.__itemId??void 0,s={...r};return delete s.__itemId,void this.openForm(d.formDomain,d.formOperation,Object.keys(s).length>0?s:void 0,t,e,n)}}catch(e){this.deps.logger.warn?.(`[resource-renderer] Action ${s} failed: ${e instanceof Error?e.message:e}`)}await this.render(e,n)}else this.deps.logger.debug?.(`[resource-renderer] Unknown action: ${s}`)}openForm(e,t,n,s,r,i){const o=this.deps.schemaRegistry.get(e,t);o?this.deps.formBuilder.build({schema:o,data:n,entityId:s,onSuccess:()=>this.render(r,i)}):this.deps.logger.warn?.(`[resource-renderer] Form schema not found: ${e}:${t}`)}surfaceId(e,t){return"inline"===t?`${e.domain}-resource-inline`:`${e.domain}-resource`}envelope(e,t,n){return{sessionUpdate:"canvas.update",a2uiVersion:"0.8",surfaceId:e,...n?{presentation:n}:{},message:t}}}
1
+ import{resolveColumnValue,resolveActionLabel}from"./resource-descriptor.js";import{sv,nextComponentId}from"./a2ui-helpers.js";function textComp(e,t="body"){return{Text:{text:sv(e),usageHint:sv(t)}}}function nextId(){return nextComponentId("rsr")}export class ResourceSurfaceRenderer{deps;constructor(e){this.deps=e}cid(){return this.deps.resolveCanvasId?.()??`_resource_${Date.now()}`}async render(e,t="panel",n){try{const o=await this.deps.gatewayRequest(e.listRpc,e.listParams??{}),s=e.extractItems(o);this.buildAndSendSurface(e,s,t,n)}catch(t){this.deps.logger.warn?.(`[resource-renderer] Failed to render ${e.domain}: ${t instanceof Error?t.message:t}`)}}buildAndSendSurface(e,t,n,o){const s=this.surfaceIdFor(e,n),{components:i,rootId:r}=this.buildComponents(e,t),c=o??this.cid(),d=this.deps.canvasState.has(s,c),a=t=>this.handleAction(e,t,n,c);d?(this.deps.canvasState.update(s,i,c,r),this.deps.canvasState.bindActionHandler(s,a,c)):this.deps.canvasState.create(c,s,n,i,{origin:{kind:"system",handlerId:`${e.domain}-resource`},actionHandler:a,rootComponentId:r}),this.deps.logger.debug?.(`[resource-renderer] ${d?"Updated":"Created"} surface=${s} domain=${e.domain} items=${t.length}`)}buildComponents(e,t){const n=[],o=[],s=nextId();if(n.push({id:s,component:textComp(e.title,"h3")}),o.push(s),0===t.length){const t=nextId();n.push({id:t,component:{Icon:{name:sv("info")}}});const s=nextId();n.push({id:s,component:textComp(e.emptyTitle??"No items")});const i=nextId();n.push({id:i,component:{Row:{children:{explicitList:[t,s]}}}}),o.push(i);const r=nextId();n.push({id:r,component:{Column:{children:{explicitList:o}}}});const c=nextId();return n.push({id:c,component:{Card:{child:r}}}),{components:n,rootId:c}}const i=[];for(const t of e.columns){const e=nextId();n.push({id:e,component:textComp(t.label,"caption")}),i.push(e)}const r=nextId();n.push({id:r,component:{Row:{children:{explicitList:i}}}}),o.push(r);const c=nextId();n.push({id:c,component:{Divider:{axis:sv("horizontal")}}}),o.push(c);for(const s of t){const t=[];for(const o of e.columns){const e=nextId(),i=resolveColumnValue(o,s);n.push({id:e,component:textComp(String(i))}),t.push(e)}const i=nextId();n.push({id:i,component:{Row:{children:{explicitList:t}}}}),o.push(i)}if(e.itemActions.length>0){const s=nextId();n.push({id:s,component:{Divider:{axis:sv("horizontal")}}}),o.push(s);for(const s of t){const t=[];for(const o of e.itemActions){const i=nextId(),r=resolveActionLabel(o.label,s);n.push({id:i,component:textComp(r)});const c=nextId(),d=`${e.domain}_${o.id}`;let a;if("rpc"===o.type){const e=o.buildParams(s);a=Object.entries(e).map(([e,t])=>({key:e,value:{literalString:String(t??"")}}))}else{if(a=[{key:"__itemId",value:{literalString:e.itemId(s)}}],o.buildFormData){const e=o.buildFormData(s);for(const[t,n]of Object.entries(e))a.push({key:t,value:{literalString:String(n??"")}})}}n.push({id:c,component:{Button:{child:i,action:{name:d,context:a}}}}),t.push(c)}const i=nextId();n.push({id:i,component:{Row:{children:{explicitList:t}}}}),o.push(i)}}for(const t of e.globalActions){const s=nextId();n.push({id:s,component:textComp(t.label)});const i=nextId(),r=`${e.domain}_${t.id}`;n.push({id:i,component:{Button:{child:s,action:{name:r}}}}),o.push(i)}const d=nextId();n.push({id:d,component:{Column:{children:{explicitList:o}}}});const a=nextId();return n.push({id:a,component:{Card:{child:d}}}),{components:n,rootId:a}}async handleAction(e,t,n,o){const s=t.userAction?.name??"",i=t.userAction?.context??{},r=`${e.domain}_`;if(!s.startsWith(r))return;const c=s.slice(r.length),d=e.globalActions.find(e=>e.id===c);if(d){if("refresh"===d.type)return void await this.render(e,n,o);if("form"===d.type)return void this.openForm(d.formDomain,d.formOperation,void 0,void 0,e,n,o)}const a=e.itemActions.find(e=>e.id===c);if(a){try{if("rpc"===a.type)await this.deps.gatewayRequest(a.rpc,i);else if("form"===a.type){const t=i.__itemId??void 0,s={...i};return delete s.__itemId,void this.openForm(a.formDomain,a.formOperation,Object.keys(s).length>0?s:void 0,t,e,n,o)}}catch(e){this.deps.logger.warn?.(`[resource-renderer] Action ${s} failed: ${e instanceof Error?e.message:e}`)}await this.render(e,n,o)}}openForm(e,t,n,o,s,i,r){const c=this.deps.schemaRegistry.get(e,t);c?this.deps.formBuilder.build({schema:c,data:n,entityId:o,canvasId:r,onSuccess:()=>this.render(s,i,r)}):this.deps.logger.warn?.(`[resource-renderer] Form schema not found: ${e}:${t}`)}surfaceIdFor(e,t){return"inline"===t?`${e.domain}-resource-inline`:`${e.domain}-resource`}}
@@ -1 +1 @@
1
- import{WebSocketServer,WebSocket}from"ws";const HEARTBEAT_TIMEOUT_MS=6e4;export function createTerminalWebSocketService(e){const{config:t,terminalMgr:n,logger:o,writeObs:r}=e,i=r??(()=>{});let a=null;const c=new Set;function s(e){return e.replace(/^https?:\/\//,"").replace(/:\d+$/,"").replace(/^(localhost|127\.0\.0\.1|::1|\[::1\])$/i,"localhost")}function l(e,r){const a=function(e){const t=e.headers["x-forwarded-for"];return("string"==typeof t?t.split(",")[0]?.trim():void 0)||e.socket?.remoteAddress||"unknown"}(r),l=r.headers.origin;if(!function(e){if(!t.allowedOrigins||0===t.allowedOrigins.length)return!0;if(!e)return!1;const n=s(e);return t.allowedOrigins.some(e=>s(e)===n)}(l))return i({component:"terminal-ws",domain:"auth",name:"origin_rejected",severity:"warn",payload:{origin:l,clientIdentity:a}}),void e.close(1008,"Origin not allowed");const d=function(e){try{return new URL(e.url??"/","ws://localhost").searchParams.get("token")??void 0}catch{return}}(r);if(d!==t.token)return i({component:"terminal-ws",domain:"auth",name:"token_rejected",severity:"warn",payload:{clientIdentity:a}}),void e.close(1008,"Unauthorized");const f=n.create(),m={ws:e,terminalId:f.id,clientIdentity:a};c.add(m),i({component:"terminal-ws",domain:"lifecycle",name:"client.connected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}});const u=setInterval(()=>{const t=n.get(f.id);if(t){if(t.output.length>0){const n=t.output.splice(0).join("");n&&e.readyState===WebSocket.OPEN&&e.send(n)}"closed"===t.state&&(clearInterval(u),e.readyState===WebSocket.OPEN&&e.close(1e3,`exit:${t.exitCode??0}`))}else clearInterval(u)},50);let p=Date.now();const w=setInterval(()=>{Date.now()-p>6e4&&(o?.warn?.(`[terminal-ws] heartbeat timeout terminalId=${f.id}`),e.close(1001,"Heartbeat timeout"))},3e4);"object"==typeof w&&"unref"in w&&w.unref(),e.on("message",(t,o)=>{p=Date.now();const r=Buffer.isBuffer(t)?t.toString("utf-8"):"string"==typeof t?t:Buffer.from(t).toString("utf-8");if(o)n.write(f.id,r);else try{const t=JSON.parse(r),o=t.action??t.type;if("heartbeat"===o)p=Date.now(),e.send(JSON.stringify({type:"heartbeat_ack"}));else if("resize"===o){const e="number"==typeof t.cols?t.cols:80,o="number"==typeof t.rows?t.rows:24;n.resize(f.id,e,o)}else"input"===o&&"string"==typeof t.data&&n.write(f.id,t.data)}catch{n.write(f.id,r)}}),e.on("close",()=>{clearInterval(u),clearInterval(w),c.delete(m),n.release(f.id),i({component:"terminal-ws",domain:"lifecycle",name:"client.disconnected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}})})}return{start(){a=new WebSocketServer({port:t.port}),a.on("connection",l),o?.info?.(`[terminal-ws] Listening on ws://0.0.0.0:${t.port}`),i({component:"terminal-ws",domain:"lifecycle",name:"server.started",severity:"info",payload:{port:t.port}})},stop(){for(const e of c)try{e.ws.close(1001,"Server shutting down")}catch{}c.clear(),a?.close(),a=null}}}
1
+ import{WebSocketServer,WebSocket}from"ws";const HEARTBEAT_TIMEOUT_MS=6e4;export function createTerminalWebSocketService(e){const{config:t,terminalMgr:n,logger:o,writeObs:r}=e,i=r??(()=>{});let a=null;const c=new Set;function s(e){return e.replace(/^https?:\/\//,"").replace(/:\d+$/,"").replace(/^(localhost|127\.0\.0\.1|::1|\[::1\])$/i,"localhost")}function l(e,r){const a=function(e){const t=e.headers["x-forwarded-for"];return("string"==typeof t?t.split(",")[0]?.trim():void 0)||e.socket?.remoteAddress||"unknown"}(r),l=r.headers.origin;if(!function(e){if(!t.allowedOrigins||0===t.allowedOrigins.length)return!0;if(!e)return!1;const n=s(e);return t.allowedOrigins.some(e=>s(e)===n)}(l))return i({component:"terminal-ws",domain:"auth",name:"origin_rejected",severity:"warn",payload:{origin:l,clientIdentity:a}}),void e.close(1008,"Origin not allowed");const d=function(e){try{return new URL(e.url??"/","ws://localhost").searchParams.get("token")??void 0}catch{return}}(r);if(!t.token||d!==t.token)return i({component:"terminal-ws",domain:"auth",name:"token_rejected",severity:"warn",payload:{clientIdentity:a}}),void e.close(1008,"Unauthorized");const f=n.create(),m={ws:e,terminalId:f.id,clientIdentity:a};c.add(m),i({component:"terminal-ws",domain:"lifecycle",name:"client.connected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}});const u=setInterval(()=>{const t=n.get(f.id);if(t){if(t.output.length>0){const n=t.output.splice(0).join("");n&&e.readyState===WebSocket.OPEN&&e.send(n)}"closed"===t.state&&(clearInterval(u),e.readyState===WebSocket.OPEN&&e.close(1e3,`exit:${t.exitCode??0}`))}else clearInterval(u)},50);let p=Date.now();const w=setInterval(()=>{Date.now()-p>6e4&&(o?.warn?.(`[terminal-ws] heartbeat timeout terminalId=${f.id}`),e.close(1001,"Heartbeat timeout"))},3e4);"object"==typeof w&&"unref"in w&&w.unref(),e.on("message",(t,o)=>{p=Date.now();const r=Buffer.isBuffer(t)?t.toString("utf-8"):"string"==typeof t?t:Buffer.from(t).toString("utf-8");if(o)n.write(f.id,r);else try{const t=JSON.parse(r),o=t.action??t.type;if("heartbeat"===o)p=Date.now(),e.send(JSON.stringify({type:"heartbeat_ack"}));else if("resize"===o){const e="number"==typeof t.cols?t.cols:80,o="number"==typeof t.rows?t.rows:24;n.resize(f.id,e,o)}else"input"===o&&"string"==typeof t.data&&n.write(f.id,t.data)}catch{n.write(f.id,r)}}),e.on("close",()=>{clearInterval(u),clearInterval(w),c.delete(m),n.release(f.id),i({component:"terminal-ws",domain:"lifecycle",name:"client.disconnected",severity:"info",payload:{terminalId:f.id,clientIdentity:a}})})}return{start(){a=new WebSocketServer({port:t.port}),a.on("connection",l),o?.info?.(`[terminal-ws] Listening on ws://0.0.0.0:${t.port}`),i({component:"terminal-ws",domain:"lifecycle",name:"server.started",severity:"info",payload:{port:t.port}})},stop(){for(const e of c)try{e.ws.close(1001,"Server shutting down")}catch{}c.clear(),a?.close(),a=null}}}
@@ -77,7 +77,7 @@
77
77
  },
78
78
  "agentId": {
79
79
  "type": "string",
80
- "description": "Agent ID to use (default: main)"
80
+ "description": "Optional override for the OpenClaw agent ID used to route sessions. If omitted, automatically resolved from the Gateway agent list."
81
81
  }
82
82
  }
83
83
  },
@@ -114,6 +114,36 @@
114
114
  "description": "Directory for log files (default: ~/.openclaw/beeos-logs)"
115
115
  }
116
116
  }
117
+ },
118
+ "messageBus": {
119
+ "type": "object",
120
+ "description": "Message SDK configuration. Connects to Message Service (REST + Centrifugo WSS) for A2A messaging and real-time communication.",
121
+ "properties": {
122
+ "enabled": {
123
+ "type": "boolean",
124
+ "description": "Enable message bus. Auto-detected from serviceUrl or platformUrl. Also readable from BEEOS_MESSAGE_BUS_ENABLED env var."
125
+ },
126
+ "serviceUrl": {
127
+ "type": "string",
128
+ "description": "Message Service REST URL (e.g. http://localhost:8089). Also readable from MESSAGE_SERVICE_URL env var."
129
+ },
130
+ "apiKey": {
131
+ "type": "string",
132
+ "description": "Message Service API Key for REST authentication. Also readable from MESSAGE_API_KEY env var."
133
+ },
134
+ "url": {
135
+ "type": "string",
136
+ "description": "Override: Centrifugo WebSocket endpoint. Normally obtained from Message Service token response. Also readable from CENTRIFUGO_URL env var."
137
+ },
138
+ "principalId": {
139
+ "type": "string",
140
+ "description": "Override: messaging principal ID. Normally resolved from Gateway. Also readable from BEEOS_PRINCIPAL_ID env var."
141
+ },
142
+ "a2aEnabled": {
143
+ "type": "boolean",
144
+ "description": "Enable A2A tools (beeos_discover_agents, beeos_call_agent). Default: true"
145
+ }
146
+ }
117
147
  }
118
148
  }
119
149
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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>",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@beeos-ai/bridge-client": "^0.1.0",
40
- "@beeos-ai/cli": "^0.0.2",
40
+ "centrifuge": "^5.5.3",
41
41
  "ws": "^8.18.0"
42
42
  },
43
43
  "devDependencies": {
@@ -1 +0,0 @@
1
- import{mkdirSync,existsSync,appendFileSync,openSync,readSync,closeSync,statSync}from"node:fs";import{join}from"node:path";import{homedir}from"node:os";const STATE_DIR=process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),HISTORY_DIR=join(STATE_DIR,"beeos-claw","session_history"),TAIL_CHUNK_BYTES=65536,TAIL_MAX_BYTES=1048576,TAIL_MAX_LINES=6400;function sessionFilePath(n){const e=n.replace(/[^a-zA-Z0-9._-]/g,"_");return join(HISTORY_DIR,`${e}.jsonl`)}function ensureDir(){existsSync(HISTORY_DIR)||mkdirSync(HISTORY_DIR,{recursive:!0})}export function appendHistoryEntry(n,e,t){try{ensureDir();const t=JSON.stringify(e)+"\n";appendFileSync(sessionFilePath(n),t,"utf8")}catch(e){t?.debug?.(`[local-history] write failed sessionKey=${n}: ${e instanceof Error?e.message:e}`)}}export function readLocalHistory(n,e=50){const t=sessionFilePath(n);if(!existsSync(t))return[];try{const n=readTailLines(t),r=[];for(const t of n){try{r.push(JSON.parse(t))}catch{}if(r.length>=e)break}return r.reverse()}catch{return[]}}function readTailLines(n){let e,t;try{e=statSync(n).size}catch{return[]}if(e<=0)return[];try{t=openSync(n,"r");let r=e,s=1048576,o="";const i=[];for(;r>0&&s>0&&i.length<6400;){const n=Math.min(65536,r,s),e=r-n,c=Buffer.allocUnsafe(n),a=readSync(t,c,0,n,e);if(a<=0)break;r=e,s-=a;const l=(c.toString("utf-8",0,a)+o).split("\n");o=l.shift()??"";for(let n=l.length-1;n>=0;n--){const e=l[n]?.trim();if(e&&(i.push(e),i.length>=6400))break}}const c=o.trim();return c&&i.length<6400&&i.push(c),i}catch{return[]}finally{if(void 0!==t)try{closeSync(t)}catch{}}}