beeos-claw 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2a/task-executor.js +1 -1
- package/dist/a2a/task-handler.js +1 -1
- package/dist/a2a/tools.js +1 -1
- package/dist/acp-gateway/bridge-core.js +1 -1
- package/dist/acp-server.js +1 -1
- package/dist/canvas/state.js +1 -1
- package/dist/canvas/tools.js +1 -1
- package/dist/index.js +1 -1
- package/dist/messaging/message-sdk.js +1 -1
- package/package.json +1 -1
|
@@ -1 +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,
|
|
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,a,s){await a2aFetch(t,`/api/v1/a2a/tasks/${encodeURIComponent(e)}/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({result:s?"":a,error:s})})}export async function updateTaskStatus(t,e,a,s){try{const n=await a2aFetch(t,`/api/v1/a2a/tasks/${encodeURIComponent(e)}/status`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({state:a,message:s})});n.ok||t.logger.warn?.("[beeos-claw] updateTaskStatus %s -> %s failed: %d",e,a,n.status)}catch(s){t.logger.warn?.("[beeos-claw] updateTaskStatus %s -> %s error: %s",e,a,s instanceof Error?s.message:String(s))}}export async function dispatchA2ATask(t,e,a,s,n,o,r){try{await updateTaskStatus(t,s,"working");const i=`[A2A Task #${s}${o?` from ${o}`:""}]\n\n${n}`,c=await e(i,r),p=c?.runId;p?a(p,s,(e,a)=>completeA2ATask(t,s,e,a)):t.logger.warn?.("[beeos-claw] agentSend returned no runId for A2A task %s, task will rely on manual completion or timeout",s)}catch(e){const a=e instanceof Error?e.message:String(e);t.logger.warn?.("[beeos-claw] Failed to dispatch A2A task %s: %s",s,a);try{await completeA2ATask(t,s,"",a)}catch{}}}
|
package/dist/a2a/task-handler.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{dispatchA2ATask}from"./task-executor.js";export function createA2ATaskHandler(e,
|
|
1
|
+
import{dispatchA2ATask}from"./task-executor.js";export function createA2ATaskHandler(t,e,s,a){return async i=>{const n=i.payload,r="string"==typeof n?.message?n.message:void 0;if(!r)return{ok:!1,error:"missing message"};const o=Array.isArray(n?.files)?n.files:void 0,f="string"==typeof n?.task_id?n.task_id:void 0,l="string"==typeof n?.caller?n.caller:void 0,c="string"==typeof n?.caller_name?n.caller_name:l??"Unknown Agent",d="string"==typeof n?.context_id?n.context_id:void 0,p=Array.isArray(n?.parts)?n.parts:void 0;if(f&&a){const t="a2a_task"===i.type&&d,e=r.length>100?r.slice(0,100)+"...":r;a({type:"a2a_task_notification",taskId:f,callerName:c,message:t?`Continued A2A task from ${c}: ${e}`:`Received A2A task from ${c}: ${e}`})}let A=r;if(p&&p.length>0){const t=[];for(const e of p)if("string"==typeof e.text&&e.text!==r)t.push(e.text);else if(e.file&&"object"==typeof e.file){const s=e.file;"string"==typeof s.url&&t.push(`[File: ${s.name||s.url}]`)}t.length>0&&(A=r+"\n\n"+t.join("\n"))}if(!f||!e||!s){return t(`[A2A Task${f?` #${f}`:""}${l?` from ${l}`:""}]\n\n${A}`,o)}return await dispatchA2ATask(e,t,s,f,A,l,o),{ok:!0,taskId:f,contextId:d}}}
|
package/dist/a2a/tools.js
CHANGED
|
@@ -1 +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,
|
|
1
|
+
import{agentFetch}from"../agent-auth.js";import{TaskHandleRegistry}from"./task-handle.js";import{resolveAgentIdFromMentions}from"./mention-resolver.js";async function a2aFetch(e,t,r){const a=`${e.agentGatewayUrl}${t}`;return e.agentKeys?agentFetch(a,e.agentKeys,r):fetch(a,r)}export{a2aFetch};const defaultRegistry=new TaskHandleRegistry;export function registerA2ATools(e,t,r=defaultRegistry,a,n,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,r){const{query:a}=r;try{const e=await a2aFetch(t,`/api/v1/a2a/discover?query=${encodeURIComponent(a)}&limit=20`);if(!e.ok)return{content:JSON.stringify({error:`discover failed: ${e.status}`})};const r=await e.json();return{content:JSON.stringify(r,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"},taskId:{type:"string",description:"Optional existing task ID to continue a multi-turn conversation. When provided, the message is appended to the existing task instead of creating a new one."},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,taskId:p}=o;const u=a?.(e),y=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;if(!y.test(i)){const r=n?.(e);if(r?.length){const e=resolveAgentIdFromMentions(r,i);e&&(t.logger.info?.(`beeos_call_agent: resolved "${i}" → ${e} via mention context`),i=e)}if(!y.test(i))try{const e=await a2aFetch(t,`/api/v1/a2a/discover?query=${encodeURIComponent(i)}&limit=10`);if(e.ok){const r=await e.json(),a=r?.data??[],n=i.toLowerCase(),s=a.find(e=>e.name.toLowerCase()===n)??a.find(e=>e.name.toLowerCase().includes(n));if(!s)return{content:JSON.stringify({error:`Could not resolve agent name "${i}". Available agents: ${a.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 h=d||(u?`session:${u}`:"");try{const e={jsonrpc:"2.0",id:`claw-${Date.now()}`,method:"SendMessage",params:{message:{role:"user",parts:[{text:c}],...h?{contextId:h}:{},...p?{taskId:p}:{}},configuration:{returnImmediately:!0}}},a=`/api/v1/a2a/${encodeURIComponent(i)}`,n=await a2aFetch(t,a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),o=await n.text();let l;try{l=JSON.parse(o)}catch{l={}}if(!n.ok||l.error){if(401===n.status||403===n.status){const e=l.error??{};-32600===e.code&&s&&s("_beeos/auth_required",{type:"auth_required",target_agent_id:i,target_agent_name:i,title:"Authentication Required",content:`Agent **${i}** requires authentication.`})}const e=l.error?JSON.stringify(l.error):o;return{content:JSON.stringify({error:`send task failed: ${n.status} ${e}`})}}const g=l.result,y=g?.id;let f;return y&&(f=r.create(y,i,d,u)),{content:JSON.stringify({result:g,task_handle:f},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,r){const{taskId:a,result:n,error:s}=r;try{const e={jsonrpc:"2.0",id:`claw-complete-${Date.now()}`,method:"CompleteTask",params:{task_id:a,result:n??"",error:s??""}},r=await a2aFetch(t,"/api/v1/a2a/_self",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),o=await r.text();let i;try{i=JSON.parse(o)}catch{i={}}if(!r.ok||i.error){const e=i.error?JSON.stringify(i.error):o;return{content:JSON.stringify({error:`complete task failed: ${r.status} ${e}`})}}return{content:JSON.stringify(i.result,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,a){const{taskId:n}=a,s=r.resolveTaskId(n);try{const e={jsonrpc:"2.0",id:`claw-get-${Date.now()}`,method:"GetTask",params:{id:s}},r=await a2aFetch(t,"/api/v1/a2a/_self",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),a=await r.text();let n;try{n=JSON.parse(a)}catch{n={}}if(!r.ok||n.error){const e=n.error?JSON.stringify(n.error):a;return{content:JSON.stringify({error:`get task failed: ${r.status} ${e}`})}}return{content:JSON.stringify(n.result,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,a){const{taskId:n}=a,s=r.resolveTaskId(n);try{const e={jsonrpc:"2.0",id:`claw-cancel-${Date.now()}`,method:"CancelTask",params:{id:s}},r=await a2aFetch(t,"/api/v1/a2a/_self",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)}),a=await r.text();let n;try{n=JSON.parse(a)}catch{n={}}if(!r.ok||n.error){const e=n.error?JSON.stringify(n.error):a;return{content:JSON.stringify({error:`cancel task failed: ${r.status} ${e}`})}}return{content:JSON.stringify(n.result,null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}})}
|
|
@@ -1 +1 @@
|
|
|
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,extractBeeosFileUrisFromOutput,splitUserReplayBlocks}from"./history-replay.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{readUploadToolResult}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{resolveChannelFromSessionKey}from"../channel-detection.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(s=>e.includes(s))}function isSystemDefaultSessionKey(e){const s=e.split(":");if(s.length<3||"agent"!==s[0])return!1;const t=s.slice(2).join(":");return"main"===t||t===s[1]}function cleanGatewayDerivedTitle(e){if(!e)return;let s=stripTransportMetadata(e);return/^\[System\b/.test(s)?void 0:(s=s.trim(),s||void 0)}function resolveStandaloneSlashCommandName(e,s){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const t=s.trim();if(!t||t.includes("\n")||!t.startsWith("/"))return;const n=t.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,s){const t=createHash("sha256").update(`${s}:${e}`).digest("hex").substring(0,32);return`session:${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20,32)}`}function parseAgentIdFromKey(e){const s=e.split(":");return(s.length>=3&&"agent"===s[0]?s[1]:"")||"main"}function createSessionObject(e){const s=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:s,platformAgentId:e.resolvePlatformAgentId?.(s),createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:e.cwd,title:e.title,channelType:resolveChannelFromSessionKey(e.gatewayKey,e.thirdPartyChannels)}}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;thirdPartyChannels;beeosFileResolver;initialized=!1;agentMapping=null;_syncResolvers=[];_mappingReadyCallbacks=[];setAgentMapping(e){this.agentMapping=e;for(const s of this.state.sessions.values())!s.platformAgentId&&s.agentId&&(s.platformAgentId=e.get(s.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[];for(const e of this._mappingReadyCallbacks)e();this._mappingReadyCallbacks=[]}onceAgentMappingReady(e){this.agentMapping&&this.agentMapping.size>0?e():this._mappingReadyCallbacks.push(e)}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(s=>{this._syncResolvers.push(s);const t=setTimeout(()=>{const e=this._syncResolvers.indexOf(s);-1!==e&&this._syncResolvers.splice(e,1),s()},e);"object"==typeof t&&"unref"in t&&t.unref()})}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",this.thirdPartyChannels=e.thirdPartyChannels??new Set,this.beeosFileResolver=e.beeosFileResolver}resolvePlatformAgentId(e){return this.agentMapping?.get(e)?.id}getDefaultAgentId(){return this.agentMapping&&this.agentMapping.size>0?this.agentMapping.keys().next().value??"main":"main"}registerSession(e){this.state.sessions.set(e.sessionId,e),this.state.sessionsByGatewayKey.set(e.gatewaySessionKey,e)}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"models/list":await this.handleModelsList(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(s){this.transport.sendAcpError(e.id,Errors.InternalError,{message:s instanceof Error?s.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 s=e.params,t=s?.surfaceId,n=this.state.resolveSessionFromParams(s);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${s?.sessionId}`);break}n.referencedSurfaceId=t||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${t?"set":"cleared"}: ${t??"(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 s=this.transport.forwardControl;!1===e.params?.forwardThinking&&(s.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(s.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 s=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,t="string"==typeof e.params?.cwd?e.params.cwd:".",n=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t});this.registerSession(n),this.transport.sendResult(e.id,{sessionId:n.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s,agentId:n.agentId,platformAgentId:n.platformAgentId??null}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const s=this.getGateway();s?.isConnected&&loadHistory(s,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async recoverSessionFromGateway(e){const s=this.getGateway();if(s?.isConnected)try{const t=await s.sessionsList();if(!t||"object"!=typeof t)return;const n=t.sessions;if(!Array.isArray(n))return;for(const s of n){const t=s,n=t.sessionKey??t.key??t.id??t.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(e),e}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let s=this.state.sessions.get(e);if(!s&&e.startsWith("agent:")&&(s=this.state.findSessionByKey(e)),s||e.startsWith("agent:")||(s=await this.recoverSessionFromGateway(e)),s)return{session:s,created:!1};const t=e.startsWith("agent:"),n=this.config.gateway.agentId||this.getDefaultAgentId();return s=createSessionObject({gatewayKey:t?e:`agent:${n}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(s),{session:s,created:!0}}async handleSessionLoad(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const{session:t}=await this.resolveOrCreateSession(s),n=t.sessionId;this.state.lastActiveSessionId=n,t.chatReplayDedupKeys.clear();try{const e=this.getGateway(),s=await loadHistory(e,t.gatewaySessionKey,50),i=groupIntoTurns(s),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,s)=>e+(s.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${s.length} turns=${i.length} userTurns=${o} asstTurns=${a} turnsWithToolCalls=${r} totalToolCalls=${d}`);for(let e=0;e<i.length;e++){const s=i[e];if("user"!==s.role||"internal_system"!==s.provenance?.kind)if("system"!==s.role){if(this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${s.role} contentLen=${s.content.length} content=${JSON.stringify(s.content.slice(0,80))} tcCount=${s.toolCalls?.length??0}`),"user"===s.role)this.replayUserTurnAsChunks(n,s.content,s.metadata);else{const e={sessionUpdate:"history_turn",role:s.role,content:s.content,reasoning:s.reasoning,toolCalls:s.toolCalls};s.metadata&&(e.metadata=s.metadata),this.transport.sendSessionUpdate(n,e)}if("assistant"===s.role){if(s.files?.length)for(const e of s.files)this.transport.sendSessionUpdate(n,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:e.uri,...e.name?{name:e.name}:{},...e.mimeType?{mimeType:e.mimeType}:{}}});s.toolCalls?.length&&await this.replayUploadFileBlocks(n,s.toolCalls)}}else this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${s.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:s.content},metadata:s.metadata});else this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] internal_system user message`)}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}const i=()=>{this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,t.gatewaySessionKey,t.platformAgentId),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})};void 0!==t.pendingPromptId&&!t.promptDone&&t.assistantMq&&!t.assistantMq.closed?this.replayCurrentAssistantStream(t).catch(e=>{this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}).finally(i):i()}async replayUploadFileBlocks(e,s){for(const t of s){if(t.name!==BEEOS_UPLOAD_TOOL_NAME)continue;let s=[];try{const e=await readUploadToolResult(t.id);e&&(s=buildToolResultSessionContentBlocks(t.name,e))}catch{}if(0===s.length){const e=extractBeeosFileUrisFromOutput(t.output);for(const t of e)s.push({type:"content",content:{type:"resource_link",uri:t,name:"file"}})}const n=s.filter(e=>"resource_link"===e.content?.type);for(const s of n)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:s.content});n.length>0&&this.logger.debug?.(`[bridge-core] replayUploadFileBlocks tcId=${t.id} sent ${n.length} resource_link block(s)`)}}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const s=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,t=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let n=0;for(;Date.now()<t&&(e.currentRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const t=await Promise.race([e.assistantMq.read(n),new Promise(e=>setTimeout(()=>e(null),s))]);if(!t||0===t.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const s of t)this.transport.sendSessionUpdate(e.sessionId,s),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 s=[];for(const[,e]of this.state.sessions)s.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 t=this.getGateway();if(t?.isConnected)try{const e=await t.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const t=e.sessions;for(const e of t){const t=e,n=t.sessionKey??t.key??t.id??t.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=s.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),a=t.label||cleanGatewayDerivedTitle(t.derivedTitle)||t.title||void 0,r=t.model,d=t.modelProvider;if(o){if(!o.title||"New chat"===o.title){const e=a||o.sessionId;o.title=e;const s=this.state.sessions.get(o.sessionId);s&&!s.title&&(s.title=e)}const e=t.updatedAt;e&&(o.updatedAt=e),r&&(o.model=r),d&&(o.modelProvider=d)}else{const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t.cwd,title:a||"New chat"});this.registerSession(e),s.push({sessionId:e.sessionId,title:a||"New chat",cwd:t.cwd,updatedAt:t.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(n),model:r,modelProvider:d})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}s.sort((e,s)=>(s.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(s.sessionId)),this.transport.sendResult(e.id,{sessions:s})}handleSessionRename(e){const s=e.params?.sessionId,t=e.params?.title;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const n=t.slice(0,64),i=this.state.sessions.get(s);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(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (rename) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendResult(e.id,{sessionId:s,title:n})}async handleSessionDelete(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.info?.(`[bridge-core] session/delete sessionId=${s} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=t.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${s} 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(s){const t=s instanceof Error?s.message:String(s);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${t}`),!t.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(s){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${s instanceof Error?s.message:s}`),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!==t.pendingPromptId&&this.failPrompt(t,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(t),this.state.clearStreamIdleTimer(t),t.assistantMq?.close(),t.currentRunId&&this.state.promptsByRunId.delete(t.currentRunId);const a=new Set;for(const[e,t]of this.state.toolCallToSessionMap)t===s&&(this.state.toolCallToSessionMap.delete(e),a.add(e));for(const[e,s]of this.state.toolCallIdMap)a.has(s)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(s),this.state.sessionsByGatewayKey.delete(n),this.transport.resetSessionUpdateCount(s),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${s} 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 s=e.params?.sessionId;if(!s)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${s} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${s} gatewayKey=${t.gatewaySessionKey}`);const n=this.getGateway();if(n?.isConnected&&t.gatewaySessionKey)try{await n.sessionsReset(t.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${t.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${t.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n?.isConnected}, key=${t.gatewaySessionKey}`);t.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(s,t.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const s=e.params?.sessionId,t=e.params?.mode,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();if(n&&i?.isConnected&&t){const e={};if("verbose"===t||"detailed"===t?e.verboseLevel="full":"concise"!==t&&"default"!==t||(e.verboseLevel="default"),"reasoning"!==t&&"thinking"!==t||(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:t??"default"})}async handleSessionSetModel(e){const s=e.params?.sessionId,t=e.params?.model,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();let o=t??null,a=null;if(n&&i?.isConnected&&t)try{const e=await i.sessionsPatch(n.gatewaySessionKey,{model:t}),s=e?.resolved;s?.model&&(o=s.model),s?.modelProvider&&(a=s.modelProvider)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:o,modelProvider:a})}async handleModelsList(e){const s=this.getGateway();if(s?.isConnected)try{const t=await s.request("models.list",{});this.transport.sendResult(e.id,t??{models:[]})}catch(s){this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Failed to list models: ${s instanceof Error?s.message:s}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleSkillsList(e){const s=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:s})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const s=await e.skillsStatus();return s?.skills?s.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,s)=>e.always&&!s.always?-1:!e.always&&s.always?1:e.name.localeCompare(s.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 s=e.params?.sessionId,t=e.params?.prompt;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:n,created:i}=await this.resolveOrCreateSession(s);i&&this.onSessionLoaded?.(n.sessionId,n.gatewaySessionKey,n.platformAgentId);const o=n.sessionId;this.state.lastActiveSessionId=o,this.onPromptStart?.();const a=this.getGateway();if(!a?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let r=extractPromptBlocks(sanitizePromptPayload(t));if(!n.title){const e=r.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(n.title=e.text.slice(0,64).replace(/\n/g," ").trim(),a?.isConnected&&n.title&&a.sessionsPatch(n.gatewaySessionKey,{label:n.title}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}))}const d=buildResolutionPlan(r);if(d.refs.length>0)try{const e=await resolveResources(d,this.writeObs);r=applyResolutions(r,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}let l;if(this.beeosFileResolver){const e=this.beeosFileResolver.buildResolutionPlan(r);if(e.refs.length>0)try{l=await this.beeosFileResolver.resolveMetadataForPrompt(e)}catch(e){this.logger.warn?.(`[bridge-core] BeeOS file resolution failed: ${e instanceof Error?e.message:e}`)}}const c=toGatewayPrompt(r,{beeosFileResolutionsByUri:l});if("error"in c)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:c.error});const{message:m,attachments:p}=c,h=resolveStandaloneSlashCommandName(r,m)??resolveSlashCmd(r,m),g="string"==typeof h;if(h&&this.handleSlashCommand){const s=this;if(await(async()=>{try{return await s.handleSlashCommand(h)}catch(e){return s.logger.warn?.(`[bridge-core] /${h} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${h}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const u=g?"chat.send":"agent";let f=g?m:withUserMessagePrefix(m);const y=this.config.canvasEnabled&&!g&&!1!==n.canvasActive&&!n.channelType;if(y){const e=n.promptsSinceCanvasInjection??0;!n.canvasPromptInjected||e>=10?(f=`${CANVAS_TERM_MAPPING}\n\n${f}`,n.canvasPromptInjected=!0,n.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):n.promptsSinceCanvasInjection=e+1}if(y){const e=this.getCanvasState?.();if(e){const s=e.resolveCanvasForSession(n.gatewaySessionKey);f=`${buildActiveSurfacesContext(e,s)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${f}`}const s=n.referencedSurfaceId;s&&(f=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${s}" for editing. You MUST call render_ui with id="${s}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${f}`,n.referencedSurfaceId=void 0)}if(!g){const s=parseMentionsParam(e.params?.mentions);if(s.length>0){n.activeMentions=s;f=`${buildMentionInstruction(s)}\n${f}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${s.length} agent(s)`)}else n.activeMentions=void 0}g&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${h}`),this.state.resetForNewRun(n,o,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=u,this.emitPromptAsUserSessionUpdates(o,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(n);try{const e=p.length>0?p.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let s;s=g?await a.chatSend(n.gatewaySessionKey,f,e):await a.agentSend(n.gatewaySessionKey,f,n.agentId||this.config.gateway.agentId||this.getDefaultAgentId(),e),n.currentRunId=s.runId,n.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(s.runId,n)}catch(s){this.state.clearPromptTimeout(n),void 0!==n.pendingPromptId&&(n.pendingPromptId=void 0,n.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:s instanceof Error?s.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)}}replayUserTurnAsChunks(e,s,t){const n=splitUserReplayBlocks(s);for(const s of n){let n;if("text"===s.type){if(!s.text)continue;n={type:"text",text:s.text}}else n="resource_link"===s.type?{type:"resource_link",uri:s.uri??"",...s.fileName?{name:s.fileName}:{},...s.mimeType?{mimeType:s.mimeType}:{}}:{type:s.type,...s.mimeType?{mimeType:s.mimeType}:{},...s.fileName?{fileName:s.fileName}:{}};this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:n,...t?{metadata:t}:{}})}}emitPromptAsUserSessionUpdates(e,s){for(const t of s){const s=this.toUserSessionUpdateContent(t);s&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:s})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const s=e.mimeType??e.mime_type??"application/octet-stream",t=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}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}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 s=e.mimeType??e.mime_type??"application/octet-stream",t=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}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}if("resource"===e.type){const s=e.resource??{},t=s.uri??e.uri,n=s.mimeType??s.mime_type??e.mimeType??e.mime_type,i=s.text??e.text,o=s.data??e.data,a=s.fileName??s.file_name??s.filename??s.name??e.fileName??e.file_name??e.filename??e.name;if(!t&&!i&&!o)return;return{type:"resource",resource:{...t?{uri:t}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...a?{fileName:a}:{}}}}}handleSessionCancel(e,s){const t=s?.sessionId;if(!t)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(t);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(e=>{this.logger.debug?.(`[bridge-core] chatAbort failed: ${e instanceof Error?e.message:e}`)}),this.completePrompt(n,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const s=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;s<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${s}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${s}ms`))},s),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const s=e.currentRunId;if(s){this.state.promptsByRunId.delete(s);const e=`tc_${s}_`;for(const[s,t]of this.state.toolCallIdMap)t.startsWith(e)&&this.state.toolCallIdMap.delete(s)}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,s){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const t=e.currentRunId;if(t&&this.onRunComplete){const s=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(t,s)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:s}),this.cleanupPrompt(e)}failPrompt(e,s,t){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,t),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,s,t),this.cleanupPrompt(e)}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}maybeCompletePromptFromChatState(e,s){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const t="string"==typeof s.state?s.state.toLowerCase():void 0;if(t)if("final"===t)this.completePrompt(e,"end_turn");else if("aborted"===t||"cancelled"===t||"cancel"===t)this.completePrompt(e,"cancelled");else if("error"===t){const t=("string"==typeof s.summary?s.summary:void 0)??("string"==typeof s.message?s.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(t))}}pushToolResultContent(e,s,t,n){if(0===t.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 t){const s={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,s),i.assistantMq?.push(s)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:s,blockCount:t.length,sessionId:o}})}async handleCronRequest(e){const s=this.getGateway();if(!s?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const t=e.method.replace("/",".");try{const n=await s.request(t,e.params??{});this.transport.sendResult(e.id,n??{})}catch(s){const n=s instanceof Error?s.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${t} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}}
|
|
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,extractBeeosFileUrisFromOutput,splitUserReplayBlocks}from"./history-replay.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{readUploadToolResult}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{resolveChannelFromSessionKey}from"../channel-detection.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(s=>e.includes(s))}function isSystemDefaultSessionKey(e){const s=e.split(":");if(s.length<3||"agent"!==s[0])return!1;const t=s.slice(2).join(":");return"main"===t||t===s[1]}function cleanGatewayDerivedTitle(e){if(!e)return;let s=stripTransportMetadata(e);return/^\[System\b/.test(s)?void 0:(s=s.trim(),s||void 0)}function resolveStandaloneSlashCommandName(e,s){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const t=s.trim();if(!t||t.includes("\n")||!t.startsWith("/"))return;const n=t.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,s){const t=createHash("sha256").update(`${s}:${e}`).digest("hex").substring(0,32);return`session:${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20,32)}`}function parseAgentIdFromKey(e){const s=e.split(":");return(s.length>=3&&"agent"===s[0]?s[1]:"")||"main"}function createSessionObject(e){const s=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:s,platformAgentId:e.resolvePlatformAgentId?.(s),createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:e.cwd,title:e.title,channelType:resolveChannelFromSessionKey(e.gatewayKey,e.thirdPartyChannels)}}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;thirdPartyChannels;beeosFileResolver;initialized=!1;agentMapping=null;_syncResolvers=[];_mappingReadyCallbacks=[];setAgentMapping(e){this.agentMapping=e;for(const s of this.state.sessions.values())!s.platformAgentId&&s.agentId&&(s.platformAgentId=e.get(s.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[];for(const e of this._mappingReadyCallbacks)e();this._mappingReadyCallbacks=[]}onceAgentMappingReady(e){this.agentMapping&&this.agentMapping.size>0?e():this._mappingReadyCallbacks.push(e)}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(s=>{this._syncResolvers.push(s);const t=setTimeout(()=>{const e=this._syncResolvers.indexOf(s);-1!==e&&this._syncResolvers.splice(e,1),s()},e);"object"==typeof t&&"unref"in t&&t.unref()})}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",this.thirdPartyChannels=e.thirdPartyChannels??new Set,this.beeosFileResolver=e.beeosFileResolver}resolvePlatformAgentId(e){return this.agentMapping?.get(e)?.id}getDefaultAgentId(){return this.agentMapping&&this.agentMapping.size>0?this.agentMapping.keys().next().value??"main":"main"}registerSession(e){this.state.sessions.set(e.sessionId,e),this.state.sessionsByGatewayKey.set(e.gatewaySessionKey,e)}ensureSessionForKey(e){const s=this.state.findSessionByKey(e);if(s)return s;const t=createSessionObject({gatewayKey:e,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(t),this.logger.debug?.(`[bridge-core] ensureSessionForKey: lazily created session sid=${t.sessionId} key=${e}`),t}async preloadSessionsFromGateway(){const e=this.getGateway();if(e?.isConnected)try{const s=await e.sessionsList();if(!s||"object"!=typeof s)return;const t=s.sessions;if(!Array.isArray(t))return;let n=0;for(const e of t){const s=e,t=s.sessionKey??s.key??s.id??s.name;if(!t||"string"!=typeof t)continue;if(this.state.findSessionByKey(t))continue;const i=s.label||s.derivedTitle||s.title||void 0,o=createSessionObject({gatewayKey:t,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:i||"New chat"});this.registerSession(o),n++}n>0&&this.logger.info?.(`[bridge-core] preloadSessions: loaded ${n} session(s) from gateway`)}catch(e){this.logger.debug?.("[bridge-core] preloadSessions failed:",e)}else this.logger.debug?.("[bridge-core] preloadSessions skipped: gateway not connected")}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"models/list":await this.handleModelsList(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(s){this.transport.sendAcpError(e.id,Errors.InternalError,{message:s instanceof Error?s.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 s=e.params,t=s?.surfaceId,n=this.state.resolveSessionFromParams(s);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${s?.sessionId}`);break}n.referencedSurfaceId=t||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${t?"set":"cleared"}: ${t??"(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 s=this.transport.forwardControl;!1===e.params?.forwardThinking&&(s.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(s.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 s=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,t="string"==typeof e.params?.cwd?e.params.cwd:".",n=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t});this.registerSession(n),this.transport.sendResult(e.id,{sessionId:n.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s,agentId:n.agentId,platformAgentId:n.platformAgentId??null}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const s=this.getGateway();s?.isConnected&&loadHistory(s,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async recoverSessionFromGateway(e){const s=this.getGateway();if(s?.isConnected)try{const t=await s.sessionsList();if(!t||"object"!=typeof t)return;const n=t.sessions;if(!Array.isArray(n))return;for(const s of n){const t=s,n=t.sessionKey??t.key??t.id??t.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(e),e}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let s=this.state.sessions.get(e);if(!s&&e.startsWith("agent:")&&(s=this.state.findSessionByKey(e)),s||e.startsWith("agent:")||(s=await this.recoverSessionFromGateway(e)),s)return{session:s,created:!1};const t=e.startsWith("agent:"),n=this.config.gateway.agentId||this.getDefaultAgentId();return s=createSessionObject({gatewayKey:t?e:`agent:${n}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(s),{session:s,created:!0}}async handleSessionLoad(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const{session:t}=await this.resolveOrCreateSession(s),n=t.sessionId;this.state.lastActiveSessionId=n,t.chatReplayDedupKeys.clear();try{const e=this.getGateway(),s=await loadHistory(e,t.gatewaySessionKey,50),i=groupIntoTurns(s),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,s)=>e+(s.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${s.length} turns=${i.length} userTurns=${o} asstTurns=${a} turnsWithToolCalls=${r} totalToolCalls=${d}`);for(let e=0;e<i.length;e++){const s=i[e];if("user"!==s.role||"internal_system"!==s.provenance?.kind)if("system"!==s.role){if(this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${s.role} contentLen=${s.content.length} content=${JSON.stringify(s.content.slice(0,80))} tcCount=${s.toolCalls?.length??0}`),"user"===s.role)this.replayUserTurnAsChunks(n,s.content,s.metadata);else{const e={sessionUpdate:"history_turn",role:s.role,content:s.content,reasoning:s.reasoning,toolCalls:s.toolCalls};s.metadata&&(e.metadata=s.metadata),this.transport.sendSessionUpdate(n,e)}if("assistant"===s.role){if(s.files?.length)for(const e of s.files)this.transport.sendSessionUpdate(n,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:e.uri,...e.name?{name:e.name}:{},...e.mimeType?{mimeType:e.mimeType}:{}}});s.toolCalls?.length&&await this.replayUploadFileBlocks(n,s.toolCalls)}}else this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${s.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:s.content},metadata:s.metadata});else this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] internal_system user message`)}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}const i=()=>{this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,t.gatewaySessionKey,t.platformAgentId),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})};void 0!==t.pendingPromptId&&!t.promptDone&&t.assistantMq&&!t.assistantMq.closed?this.replayCurrentAssistantStream(t).catch(e=>{this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}).finally(i):i()}async replayUploadFileBlocks(e,s){for(const t of s){if(t.name!==BEEOS_UPLOAD_TOOL_NAME)continue;let s=[];try{const e=await readUploadToolResult(t.id);e&&(s=buildToolResultSessionContentBlocks(t.name,e))}catch{}if(0===s.length){const e=extractBeeosFileUrisFromOutput(t.output);for(const t of e)s.push({type:"content",content:{type:"resource_link",uri:t,name:"file"}})}const n=s.filter(e=>"resource_link"===e.content?.type);for(const s of n)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:s.content});n.length>0&&this.logger.debug?.(`[bridge-core] replayUploadFileBlocks tcId=${t.id} sent ${n.length} resource_link block(s)`)}}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const s=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,t=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let n=0;for(;Date.now()<t&&(e.currentRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const t=await Promise.race([e.assistantMq.read(n),new Promise(e=>setTimeout(()=>e(null),s))]);if(!t||0===t.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const s of t)this.transport.sendSessionUpdate(e.sessionId,s),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 s=[];for(const[,e]of this.state.sessions)s.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 t=this.getGateway();if(t?.isConnected)try{const e=await t.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const t=e.sessions;for(const e of t){const t=e,n=t.sessionKey??t.key??t.id??t.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=s.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),a=t.label||cleanGatewayDerivedTitle(t.derivedTitle)||t.title||void 0,r=t.model,d=t.modelProvider;if(o){if(!o.title||"New chat"===o.title){const e=a||o.sessionId;o.title=e;const s=this.state.sessions.get(o.sessionId);s&&!s.title&&(s.title=e)}const e=t.updatedAt;e&&(o.updatedAt=e),r&&(o.model=r),d&&(o.modelProvider=d)}else{const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t.cwd,title:a||"New chat"});this.registerSession(e),s.push({sessionId:e.sessionId,title:a||"New chat",cwd:t.cwd,updatedAt:t.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(n),model:r,modelProvider:d})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}s.sort((e,s)=>(s.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(s.sessionId)),this.transport.sendResult(e.id,{sessions:s})}handleSessionRename(e){const s=e.params?.sessionId,t=e.params?.title;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const n=t.slice(0,64),i=this.state.sessions.get(s);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(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (rename) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendResult(e.id,{sessionId:s,title:n})}async handleSessionDelete(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.info?.(`[bridge-core] session/delete sessionId=${s} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=t.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${s} 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(s){const t=s instanceof Error?s.message:String(s);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${t}`),!t.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(s){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${s instanceof Error?s.message:s}`),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!==t.pendingPromptId&&this.failPrompt(t,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(t),this.state.clearStreamIdleTimer(t),t.assistantMq?.close(),t.currentRunId&&this.state.promptsByRunId.delete(t.currentRunId);const a=new Set;for(const[e,t]of this.state.toolCallToSessionMap)t===s&&(this.state.toolCallToSessionMap.delete(e),a.add(e));for(const[e,s]of this.state.toolCallIdMap)a.has(s)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(s),this.state.sessionsByGatewayKey.delete(n),this.transport.resetSessionUpdateCount(s),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${s} 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 s=e.params?.sessionId;if(!s)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${s} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${s} gatewayKey=${t.gatewaySessionKey}`);const n=this.getGateway();if(n?.isConnected&&t.gatewaySessionKey)try{await n.sessionsReset(t.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${t.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${t.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n?.isConnected}, key=${t.gatewaySessionKey}`);t.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(s,t.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const s=e.params?.sessionId,t=e.params?.mode,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();if(n&&i?.isConnected&&t){const e={};if("verbose"===t||"detailed"===t?e.verboseLevel="full":"concise"!==t&&"default"!==t||(e.verboseLevel="default"),"reasoning"!==t&&"thinking"!==t||(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:t??"default"})}async handleSessionSetModel(e){const s=e.params?.sessionId,t=e.params?.model,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();let o=t??null,a=null;if(n&&i?.isConnected&&t)try{const e=await i.sessionsPatch(n.gatewaySessionKey,{model:t}),s=e?.resolved;s?.model&&(o=s.model),s?.modelProvider&&(a=s.modelProvider)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:o,modelProvider:a})}async handleModelsList(e){const s=this.getGateway();if(s?.isConnected)try{const t=await s.request("models.list",{});this.transport.sendResult(e.id,t??{models:[]})}catch(s){this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Failed to list models: ${s instanceof Error?s.message:s}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleSkillsList(e){const s=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:s})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const s=await e.skillsStatus();return s?.skills?s.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,s)=>e.always&&!s.always?-1:!e.always&&s.always?1:e.name.localeCompare(s.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 s=e.params?.sessionId,t=e.params?.prompt;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:n,created:i}=await this.resolveOrCreateSession(s);i&&this.onSessionLoaded?.(n.sessionId,n.gatewaySessionKey,n.platformAgentId);const o=n.sessionId;this.state.lastActiveSessionId=o,this.onPromptStart?.();const a=this.getGateway();if(!a?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let r=extractPromptBlocks(sanitizePromptPayload(t));if(!n.title){const e=r.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(n.title=e.text.slice(0,64).replace(/\n/g," ").trim(),a?.isConnected&&n.title&&a.sessionsPatch(n.gatewaySessionKey,{label:n.title}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}))}const d=buildResolutionPlan(r);if(d.refs.length>0)try{const e=await resolveResources(d,this.writeObs);r=applyResolutions(r,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}let l;if(this.beeosFileResolver){const e=this.beeosFileResolver.buildResolutionPlan(r);if(e.refs.length>0)try{l=await this.beeosFileResolver.resolveMetadataForPrompt(e)}catch(e){this.logger.warn?.(`[bridge-core] BeeOS file resolution failed: ${e instanceof Error?e.message:e}`)}}const c=toGatewayPrompt(r,{beeosFileResolutionsByUri:l});if("error"in c)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:c.error});const{message:h,attachments:g}=c,m=resolveStandaloneSlashCommandName(r,h)??resolveSlashCmd(r,h),p="string"==typeof m;if(m&&this.handleSlashCommand){const s=this;if(await(async()=>{try{return await s.handleSlashCommand(m)}catch(e){return s.logger.warn?.(`[bridge-core] /${m} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${m}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const u=p?"chat.send":"agent";let f=p?h:withUserMessagePrefix(h);const y=this.config.canvasEnabled&&!p&&!1!==n.canvasActive&&!n.channelType;if(y){const e=n.promptsSinceCanvasInjection??0;!n.canvasPromptInjected||e>=10?(f=`${CANVAS_TERM_MAPPING}\n\n${f}`,n.canvasPromptInjected=!0,n.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):n.promptsSinceCanvasInjection=e+1}if(y){const e=this.getCanvasState?.();if(e){const s=e.resolveCanvasForSession(n.gatewaySessionKey);f=`${buildActiveSurfacesContext(e,s)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${f}`}const s=n.referencedSurfaceId;s&&(f=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${s}" for editing. You MUST call render_ui with id="${s}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${f}`,n.referencedSurfaceId=void 0)}if(!p){const s=parseMentionsParam(e.params?.mentions);if(s.length>0){n.activeMentions=s;f=`${buildMentionInstruction(s)}\n${f}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${s.length} agent(s)`)}else n.activeMentions=void 0}p&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${m}`),this.state.resetForNewRun(n,o,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=u,this.emitPromptAsUserSessionUpdates(o,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(n);try{const e=g.length>0?g.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let s;s=p?await a.chatSend(n.gatewaySessionKey,f,e):await a.agentSend(n.gatewaySessionKey,f,n.agentId||this.config.gateway.agentId||this.getDefaultAgentId(),e),n.currentRunId=s.runId,n.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(s.runId,n)}catch(s){this.state.clearPromptTimeout(n),void 0!==n.pendingPromptId&&(n.pendingPromptId=void 0,n.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:s instanceof Error?s.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)}}replayUserTurnAsChunks(e,s,t){const n=splitUserReplayBlocks(s);for(const s of n){let n;if("text"===s.type){if(!s.text)continue;n={type:"text",text:s.text}}else n="resource_link"===s.type?{type:"resource_link",uri:s.uri??"",...s.fileName?{name:s.fileName}:{},...s.mimeType?{mimeType:s.mimeType}:{}}:{type:s.type,...s.mimeType?{mimeType:s.mimeType}:{},...s.fileName?{fileName:s.fileName}:{}};this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:n,...t?{metadata:t}:{}})}}emitPromptAsUserSessionUpdates(e,s){for(const t of s){const s=this.toUserSessionUpdateContent(t);s&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:s})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const s=e.mimeType??e.mime_type??"application/octet-stream",t=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}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}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 s=e.mimeType??e.mime_type??"application/octet-stream",t=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}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}if("resource"===e.type){const s=e.resource??{},t=s.uri??e.uri,n=s.mimeType??s.mime_type??e.mimeType??e.mime_type,i=s.text??e.text,o=s.data??e.data,a=s.fileName??s.file_name??s.filename??s.name??e.fileName??e.file_name??e.filename??e.name;if(!t&&!i&&!o)return;return{type:"resource",resource:{...t?{uri:t}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...a?{fileName:a}:{}}}}}handleSessionCancel(e,s){const t=s?.sessionId;if(!t)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(t);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(e=>{this.logger.debug?.(`[bridge-core] chatAbort failed: ${e instanceof Error?e.message:e}`)}),this.completePrompt(n,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const s=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;s<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${s}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${s}ms`))},s),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const s=e.currentRunId;if(s){this.state.promptsByRunId.delete(s);const e=`tc_${s}_`;for(const[s,t]of this.state.toolCallIdMap)t.startsWith(e)&&this.state.toolCallIdMap.delete(s)}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,s){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const t=e.currentRunId;if(t&&this.onRunComplete){const s=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(t,s)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:s}),this.cleanupPrompt(e)}failPrompt(e,s,t){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,t),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,s,t),this.cleanupPrompt(e)}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}maybeCompletePromptFromChatState(e,s){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const t="string"==typeof s.state?s.state.toLowerCase():void 0;if(t)if("final"===t)this.completePrompt(e,"end_turn");else if("aborted"===t||"cancelled"===t||"cancel"===t)this.completePrompt(e,"cancelled");else if("error"===t){const t=("string"==typeof s.summary?s.summary:void 0)??("string"==typeof s.message?s.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(t))}}pushToolResultContent(e,s,t,n){if(0===t.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 t){const s={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,s),i.assistantMq?.push(s)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:s,blockCount:t.length,sessionId:o}})}async handleCronRequest(e){const s=this.getGateway();if(!s?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const t=e.method.replace("/",".");try{const n=await s.request(t,e.params??{});this.transport.sendResult(e.id,n??{})}catch(s){const n=s instanceof Error?s.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${t} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}}
|
package/dist/acp-server.js
CHANGED
|
@@ -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{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{A2AOrchestrator}from"./a2a-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;a2aOrch;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=5;drainNextDeferredSend(e){this.a2aOrch.drainNextDeferredSend(e,(e,t,s,n)=>this.agentSend(e,t,s,n))}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 i=this.config.gateway.agentId||this.core.getDefaultAgentId();let a,r;if(s){if(a=this.state.findSessionByKey(s),!a)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=${a.sessionId}`)}else{const e=this.state.lastActiveSessionId;a=e?this.state.sessions.get(e):void 0,r=a?.gatewaySessionKey??`agent:${i}:a2a`,this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, using fallback key=${r}`)}if(a&&!a.promptDone&&(void 0!==a.pendingPromptId||void 0!==a.currentRunId))return(a.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",a.pendingAgentSends.length);const o=await this.canvasOrch.enrichMessageWithCanvasContext(e,r,this.thirdPartyChannels),c=await this.gateway.agentSend(r,o,i,t,n);return a&&c?.runId&&(this.state.resetForNewRun(a,a.sessionId,this.transport),a.currentRunId=c.runId,a.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(c.runId,a)),c}trackA2ARun(e,t,s){this.a2aOrch.trackA2ARun(e,t,s)}cancelA2ARunByTaskId(e){return this.a2aOrch.cancelA2ARunByTaskId(e)}emitA2ASystemNotification(e){this.a2aOrch.emitA2ASystemNotification(e)}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,i,a){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=a??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger);const r=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;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,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:r}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.a2aOrch=new A2AOrchestrator({state:this.state,transport:this.transport,logger:this.logger}),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.canvasOrch.onPromptComplete(e),this.drainNextDeferredSend(e)},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),this.canvasOrch.onPromptFailed(),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)=>{this.a2aOrch.onRunComplete(e,t)},this.core.onRunFailed=(e,t)=>{this.a2aOrch.onRunFailed(e,t)},this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,s)=>{this.canvasOrch.onSessionLoaded(e,t,s)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}async start(e){e&&(this.config.bridge.url=e);const t=this.config.bridge.url,s=this.config.keyFile;if(t&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}await this.connectGateway(),t&&s&&BridgeClientCtor?await this.startBridgeMode(t,s):(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});this.bridgeInstance=s,s.on("connected",()=>{this.bridgeConsecutiveFailures=0,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 n=String(e[0]??"unknown");if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${n}`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,t))}).catch(()=>{})}}),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)});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;try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}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(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();const t=()=>{if(e+1>=6)return this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — giving up"),void this.core.markAgentSyncDone();const t=Math.min(5e3*2**e,12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>this.tryAgentSync(e+1),t)};try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,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(", ")}`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}}async connectGateway(){this.gateway&&(this.gateway.stop(),this.gateway=null);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));const t=this.gateway;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,s)=>this.gateway.agentSend(e,t,s)}:null}),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.canvasOrch.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)}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const s=this.state.findSessionByKey(t);if(s?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,s.sessionId),s.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=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;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()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}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.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.a2aOrch.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance=null,this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),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{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{A2AOrchestrator}from"./a2a-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;a2aOrch;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=5;drainNextDeferredSend(e){this.a2aOrch.drainNextDeferredSend(e,(e,t,s,n)=>this.agentSend(e,t,s,n))}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 i=this.config.gateway.agentId||this.core.getDefaultAgentId();let a,r;if(s){if(a=this.state.findSessionByKey(s),!a)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=${a.sessionId}`)}else{const e=this.state.lastActiveSessionId;a=e?this.state.sessions.get(e):void 0,r=a?.gatewaySessionKey??`agent:${i}:a2a`,this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, using fallback key=${r}`)}if(a&&!a.promptDone&&(void 0!==a.pendingPromptId||void 0!==a.currentRunId))return(a.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",a.pendingAgentSends.length);const o=await this.canvasOrch.enrichMessageWithCanvasContext(e,r,this.thirdPartyChannels),c=await this.gateway.agentSend(r,o,i,t,n);return a&&c?.runId&&(this.state.resetForNewRun(a,a.sessionId,this.transport),a.currentRunId=c.runId,a.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(c.runId,a)),c}trackA2ARun(e,t,s){this.a2aOrch.trackA2ARun(e,t,s)}cancelA2ARunByTaskId(e){return this.a2aOrch.cancelA2ARunByTaskId(e)}emitA2ASystemNotification(e){this.a2aOrch.emitA2ASystemNotification(e)}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,i,a){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=a??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger);const r=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;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,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:r}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.a2aOrch=new A2AOrchestrator({state:this.state,transport:this.transport,logger:this.logger}),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.canvasOrch.onPromptComplete(e),this.drainNextDeferredSend(e)},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),this.canvasOrch.onPromptFailed(),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)=>{this.a2aOrch.onRunComplete(e,t)},this.core.onRunFailed=(e,t)=>{this.a2aOrch.onRunFailed(e,t)},this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,s)=>{this.canvasOrch.onSessionLoaded(e,t,s)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}async start(e){e&&(this.config.bridge.url=e);const t=this.config.bridge.url,s=this.config.keyFile;if(t&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}await this.connectGateway(),t&&s&&BridgeClientCtor?await this.startBridgeMode(t,s):(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});this.bridgeInstance=s,s.on("connected",()=>{this.bridgeConsecutiveFailures=0,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 n=String(e[0]??"unknown");if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${n}`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,t))}).catch(()=>{})}}),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)});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;try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}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(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();const t=()=>{if(e+1>=6)return this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — giving up"),void this.core.markAgentSyncDone();const t=Math.min(5e3*2**e,12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>this.tryAgentSync(e+1),t)};try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,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(", ")}`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}}async connectGateway(){this.gateway&&(this.gateway.stop(),this.gateway=null);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));const t=this.gateway;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,s)=>this.gateway.agentSend(e,t,s)}:null}),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.canvasOrch.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)}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(s?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,s.sessionId),s.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=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}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.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.a2aOrch.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance=null,this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
|
package/dist/canvas/state.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{MSG_SYNC,MSG_SYNC_UPDATE,MSG_AWARENESS}from"./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;canvasRefCount=new Map;relayClient=null;joinedCanvases=new Set;agentId;editingClearTimer=null;logger;setRelayClient(e){this.relayClient=e}getRelayClient(){return this.relayClient}setAgentId(e){this.agentId=e}setLogger(e){this.logger=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){const s=this.sessionMap.get(e);if(s&&s!==t){const e=(this.canvasRefCount.get(s)??1)-1;e<=0?this.canvasRefCount.delete(s):this.canvasRefCount.set(s,e)}this.sessionMap.set(e,t),this.canvasSessionMap.set(t,e),this.canvasRefCount.set(t,(this.canvasRefCount.get(t)??0)+(s===t?0:1))}resolveCanvasForSession(e){return this.sessionMap.get(e)}resolveSessionForCanvas(e){return this.canvasSessionMap.get(e)}removeSession(e){const t=this.sessionMap.get(e);if(this.sessionMap.delete(e),t){const e=(this.canvasRefCount.get(t)??1)-1;e<=0?(this.canvasRefCount.delete(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}`)})):this.canvasRefCount.set(t,e)}}async lazyLoadCanvas(e,t=3e3){const s=this.canvasEntries.get(e);if(s&&s.surfaces.size>0)return!0;if(!this.relayClient)return!1;const n=this.touchEntry(e);return await this.joinCanvas(e),await new Promise(e=>setTimeout(e,Math.min(t,1500))),n.surfaces.size>0}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(s){this.logger?.warn?.(`[canvas-state] syncDeleteToRelay failed for canvas=${e} surface=${t}:`,s)}}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}}destroy(){this.editingClearTimer&&(clearTimeout(this.editingClearTimer),this.editingClearTimer=null),this.relayClient?.disconnect(),this.relayClient=null,this.canvasEntries.clear(),this.sessionMap.clear(),this.canvasSessionMap.clear(),this.canvasRefCount.clear(),this.joinedCanvases.clear()}}
|
|
1
|
+
import{MSG_SYNC,MSG_SYNC_UPDATE,MSG_AWARENESS}from"./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;canvasRefCount=new Map;relayClient=null;joinedCanvases=new Set;agentId;editingClearTimer=null;relayReadyResolve=null;relayReadyPromise=new Promise(e=>{this.relayReadyResolve=e});logger;setRelayClient(e){this.relayClient=e,this.relayReadyResolve?.()}getRelayClient(){return this.relayClient}waitForRelayClient(e=15e3){return this.relayClient?Promise.resolve(this.relayClient):Promise.race([this.relayReadyPromise.then(()=>this.relayClient),new Promise(t=>setTimeout(()=>t(null),e))])}setAgentId(e){this.agentId=e}setLogger(e){this.logger=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){const s=this.sessionMap.get(e);if(s&&s!==t){const e=(this.canvasRefCount.get(s)??1)-1;e<=0?this.canvasRefCount.delete(s):this.canvasRefCount.set(s,e)}this.sessionMap.set(e,t),this.canvasSessionMap.set(t,e),this.canvasRefCount.set(t,(this.canvasRefCount.get(t)??0)+(s===t?0:1))}resolveCanvasForSession(e){return this.sessionMap.get(e)}resolveSessionForCanvas(e){return this.canvasSessionMap.get(e)}removeSession(e){const t=this.sessionMap.get(e);if(this.sessionMap.delete(e),t){const e=(this.canvasRefCount.get(t)??1)-1;e<=0?(this.canvasRefCount.delete(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}`)})):this.canvasRefCount.set(t,e)}}async lazyLoadCanvas(e,t=3e3){const s=this.canvasEntries.get(e);if(s&&s.surfaces.size>0)return!0;if(!this.relayClient)return!1;const n=this.touchEntry(e);return await this.joinCanvas(e),await new Promise(e=>setTimeout(e,Math.min(t,1500))),n.surfaces.size>0}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(s){this.logger?.warn?.(`[canvas-state] syncDeleteToRelay failed for canvas=${e} surface=${t}:`,s)}}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}}destroy(){this.editingClearTimer&&(clearTimeout(this.editingClearTimer),this.editingClearTimer=null),this.relayClient?.disconnect(),this.relayClient=null,this.canvasEntries.clear(),this.sessionMap.clear(),this.canvasSessionMap.clear(),this.canvasRefCount.clear(),this.joinedCanvases.clear()}}
|
package/dist/canvas/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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:n}}}}function imageNode(e,t,n="mediumFeature",s,a){const i={url:sv(t),usageHint:n};return s&&(i.fit=s),a&&(i.altText=sv(a)),{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:t}}}}function buttonNode(e,t,n,s){const a={name:n};return s&&(a.context=s),{id:e,component:{Button:{child:t,action:a}}}}function columnNode(e,t,n,s){const a={children:{explicitList:t}};return n&&(a.distribution=n),s&&(a.alignment=s),{id:e,component:{Column:a}}}function rowNode(e,t,n,s){const a={children:{explicitList:t}};return n&&(a.distribution=n),s&&(a.alignment=s),{id:e,component:{Row:a}}}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 a={label:sv(t),type:n};return s&&(a.text={path:s}),{id:e,component:{TextField:a}}}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,a){const i={label:sv(t),options:n.map(e=>({label:sv(e.label),value:e.value})),selections:{path:s}};return a&&(i.description=sv(a)),{id:e,component:{MultipleChoice:i}}}function translateComponentsWithDataModel(e){const t={},n=[];for(const s of e){if(!s||"object"!=typeof s){const e=nextCid();n.push(textNode(e,"[invalid component]"));continue}if("form"===("string"==typeof s.type?s.type.toLowerCase():"text")){const e=buildFormWithDataModel(s);n.push(...e.nodes),Object.assign(t,e.dataModel)}else n.push(...translateComponent(s))}return{components:n,dataModel:t}}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),a=nextCid(),i="row"===t?rowNode(a,s):columnNode(a,s);return[...n,i]}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 a=nextCid();t.push(imageNode(a,s(e.image),"mediumFeature","cover")),n.push(a)}if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h3")),n.push(a)}if(e.subtitle){const a=nextCid();t.push(textNode(a,s(e.subtitle),"caption")),n.push(a)}if(e.description){const a=nextCid();t.push(textNode(a,s(e.description),"body")),n.push(a)}const a=Array.isArray(e.fields)?e.fields:[];for(const e of a){if(!e||"object"!=typeof e)continue;const a=e,i=nextCid(),o=nextCid(),r=nextCid();t.push(textNode(o,s(a.label??a.key),"caption")),t.push(textNode(r,s(a.value))),t.push(rowNode(i,[o,r],"spaceBetween")),n.push(i)}const i=Array.isArray(e.actions)?e.actions:[];if(i.length>0){const e=[];for(const n of i){if(!n||"object"!=typeof n)continue;const a=n,i=nextCid(),o=nextCid(),r=s(a.label??"Action"),l=s(a.actionId??a.id??nextCid());t.push(textNode(o,r)),t.push(buttonNode(i,o,l,[{key:"actionId",value:{literalString:l}},{key:"label",value:{literalString:r}}])),e.push(i)}if(e.length>0){const s=nextCid();t.push(rowNode(s,e)),n.push(s)}}if(e.footer){const a=nextCid();t.push(textNode(a,s(e.footer),"caption")),n.push(a)}const o=nextCid();t.push(columnNode(o,n));const r=nextCid();return t.push(cardNode(r,o)),t}function buildTable(e){const t=[],n=[],a=Array.isArray(e.columns)?e.columns:[],i=Array.isArray(e.rows)?e.rows:[];if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h4")),n.push(a)}const o=[],r=[];for(const e of a)if("string"==typeof e)o.push(e),r.push(e);else if(e&&"object"==typeof e){const t=e,n=s(t.key??t.id??t.field??t.name??t.label??"");o.push(s(t.label??t.title??t.header??t.name??n)),r.push(n)}else{const t=s(e);o.push(t),r.push(t)}if(o.length>0){const e=[];for(const n of o){const s=nextCid();t.push(textNode(s,n,"caption")),e.push(s)}const s=nextCid();t.push(rowNode(s,e)),n.push(s);const a=nextCid();t.push(dividerNode(a)),n.push(a)}for(const e of i){let a;if(Array.isArray(e))a=e;else if(e&&"object"==typeof e&&r.length>0){const t=e;a=r.map(e=>{const n=Object.keys(t).find(t=>t===e||t.toLowerCase()===e.toLowerCase());return null!=n?t[n]:""})}else a=[e];const i=[];for(const e of a){const n=nextCid();t.push(textNode(n,s(e))),i.push(n)}const o=nextCid();t.push(rowNode(o,i)),n.push(o)}const l=nextCid();t.push(columnNode(l,n));const c=nextCid();return t.push(cardNode(c,l)),t}function buildForm(e){return buildFormWithDataModel(e).nodes}function buildFormWithDataModel(e){const t=[],n=[],a=Array.isArray(e.fields)?e.fields:[],i={};if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h3")),n.push(a)}if(e.description){const a=nextCid();t.push(textNode(a,s(e.description),"body")),n.push(a)}const o=[];for(const e of a){if(!e||"object"!=typeof e)continue;const a=e,r=nextCid(),l=s(a.name??a.label??r),c=s(a.inputType??a.type??"text"),d=`/${l}`;o.push(l);const u=a.defaultValue??a.default??a.value;if("checkbox"===c||"boolean"===c||"toggle"===c)i[l]=null!=u&&!!u,t.push(checkboxNode(r,s(a.label??a.name??""),d));else if("select"===c||"multi_select"===c||"dropdown"===c||"radio"===c){const e=Array.isArray(a.options)?a.options:[],n=e.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??"")}});if(null!=u)i[l]=String(u);else if(e.length>0){const t=e[0];i[l]="string"==typeof t?t:s(t.value??"")}else i[l]="";t.push(multipleChoiceNode(r,s(a.label??a.name??""),n,d))}else if("datetime"===c||"time"===c){i[l]=null!=u?String(u):"";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 a.min?a.min:0,n="number"==typeof a.max?a.max:100;i[l]=null!=u?Number(u):e,t.push({id:r,component:{Slider:{label:sv(s(a.label??a.name??"")),value:{path:d},minValue:e,maxValue:n}}})}else{i[l]=null!=u?String(u):"";const e="number"===c?"number":"date"===c?"date":"textarea"===c||"longText"===c?"longText":"shortText";t.push(textFieldNode(r,s(a.label??a.name??""),e,d))}n.push(r)}const r=o.map(e=>({key:e,value:{path:`/${e}`}})),l=nextCid(),c=nextCid();t.push(textNode(l,s(e.submitLabel??"Submit"))),t.push(buttonNode(c,l,s(e.submitActionId??"form_submit"),r)),n.push(c);const d=nextCid();return t.push(columnNode(d,n)),{nodes:t,dataModel:i}}function buildImage(e){const t=nextCid(),n=e.alt??e.altText;return[imageNode(t,s(e.url??e.src??""),"mediumFeature",void 0,n?s(n):void 0)]}function buildButton(e){const t=nextCid(),n=nextCid(),a=s(e.label??"Button"),i=s(e.actionId??n);return[textNode(t,a),buttonNode(n,t,i,[{key:"actionId",value:{literalString:i}},{key:"label",value:{literalString:a}}])]}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),a=[],i=[];for(let e=0;e<s.length;e+=n){const t=s.slice(e,e+n),o=nextCid();a.push(rowNode(o,t)),i.push(o)}const o=nextCid();return[...t,...a,columnNode(o,i)]}function buildStat(e){const t=[],n=[];if(e.icon){const a=nextCid();t.push(iconNode(a,s(e.icon))),n.push(a)}if(e.label){const a=nextCid();t.push(textNode(a,s(e.label),"caption")),n.push(a)}const a=nextCid();if(t.push(textNode(a,s(e.value??"0"),"h2")),n.push(a),e.change){const a=nextCid(),i="positive"===e.changeType?"↑ ":"negative"===e.changeType?"↓ ":"";t.push(textNode(a,`${i}${s(e.change)}`,"caption")),n.push(a)}if(e.description){const a=nextCid();t.push(textNode(a,s(e.description),"caption")),n.push(a)}const i=nextCid();t.push(columnNode(i,n,void 0,"center"));const o=nextCid();return t.push(cardNode(o,i)),t}function buildKeyValue(e){const t=[],n=[],a=Array.isArray(e.items)?e.items:[];if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h4")),n.push(a)}for(const e of a){if(!e||"object"!=typeof e)continue;const a=e,i=nextCid(),o=nextCid(),r=nextCid();t.push(textNode(o,s(a.key??a.label??""),"caption")),t.push(textNode(r,s(a.value??""))),t.push(rowNode(i,[o,r],"spaceBetween")),n.push(i)}const i=nextCid();t.push(columnNode(i,n));const o=nextCid();return t.push(cardNode(o,i)),t}function buildList(e){const t=[],n=[],a=Array.isArray(e.items)?e.items:[];if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h4")),n.push(a)}for(const e of a){if(!e||"object"!=typeof e)continue;const a=e,i=[];if(a.image){const e=nextCid();t.push(imageNode(e,s(a.image),"avatar")),i.push(e)}if(a.icon){const e=nextCid();t.push(iconNode(e,s(a.icon))),i.push(e)}const o=[];if(a.title||a.name){const e=nextCid();t.push(textNode(e,s(a.title??a.name),"body")),o.push(e)}if(a.subtitle||a.description){const e=nextCid();t.push(textNode(e,s(a.subtitle??a.description),"caption")),o.push(e)}if(o.length>0){const e=nextCid();t.push(columnNode(e,o)),i.push(e)}if(a.badge||a.value){const e=nextCid();t.push(textNode(e,s(a.badge??a.value),"caption")),i.push(e)}const r=nextCid();t.push(rowNode(r,i,"start","center")),n.push(r)}const i=nextCid();return t.push(columnNode(i,n)),t}function buildAlert(e){const t=[],n=[],a=s(e.variant??"info"),i=nextCid();t.push(iconNode(i,{info:"info",success:"check",warning:"error",error:"error",tip:"info",caution:"error",important:"error",note:"info"}[a]??"info")),n.push(i);const o=[];if(e.title){const n=nextCid();t.push(textNode(n,s(e.title),"h5")),o.push(n)}if(e.message||e.content){const n=nextCid();t.push(textNode(n,s(e.message??e.content))),o.push(n)}if(0===o.length){const n=nextCid();t.push(textNode(n,s(e.type??"Alert"))),o.push(n)}const r=nextCid();t.push(columnNode(r,o)),n.push(r);const l=nextCid();t.push(rowNode(l,n,"start","start"));const c=nextCid();return t.push(cardNode(c,l)),t}function buildCode(e){const t=s(e.code??""),n=s(e.language??""),a=n?`\`\`\`${n}\n${t}\n\`\`\``:`\`\`\`\n${t}\n\`\`\``;return[textNode(nextCid(),a)]}function buildMarkdown(e){return[textNode(nextCid(),s(e.content??""))]}function normalizeDatasets(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t={label:e.label??e.name??"",data:Array.isArray(e.data)?e.data.map(e=>e&&"object"==typeof e&&("o"in e||"h"in e||"l"in e||"c"in e)?{open:e.o??e.open,high:e.h??e.high,low:e.l??e.low,close:e.c??e.close}:e):e.data};return e.color&&(t.color=e.color),t})}function buildChart(e){const t=nextCid(),n=s(e.chartType??"bar"),a={chartType:sv(n)};e.title&&(a.title=sv(s(e.title))),Array.isArray(e.labels)&&(a.labels=e.labels.map(e=>"string"==typeof e?e:String(e??"")));const i=Array.isArray(e.datasets)?e.datasets:Array.isArray(e.stages)?e.stages:null;return i&&(a.datasets=normalizeDatasets(i)),void 0!==e.value&&(a.value="number"==typeof e.value?e.value:Number(e.value)||0),void 0!==e.max&&(a.max="number"==typeof e.max?e.max:Number(e.max)||100),[{id:t,component:{Chart:a}}]}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=[],a=[];for(const e of t){const t=s(e.label??e.title??e.id??"Tab"),i=(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e));n.push(...i);const o=findRootIds(i),r=nextCid();n.push(columnNode(r,o)),a.push({title:t,childId:r})}const i=nextCid();return n.push(tabsNode(i,a)),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),a=n>0?Math.round(t/n*100):0;return[textNode(nextCid(),`${s(e.label)||"Progress"}: ${a}% (${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 a=Array.isArray(e.children)?e.children.flatMap(e=>translateComponent(e)):[];n.push(...a)}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:a,logger:i}=e,o=s??e.getGatewaySessionKey?.(n),r=o??t,l=a.resolveCanvasForSession(r);if(l&&a.isCanvasJoined(l))return l;const c=a.getRelayClient();if(c)try{e.waitForAgentSync&&await e.waitForAgentSync();const s=e.getPlatformAgentId?.(n),l=o??t,d=o&&o!==t?t:void 0,u=await c.ensureCanvas(l,{agentId:s,linkSessionId:d});return await a.joinCanvas(u,r),i.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${o??"none"} agentId=${s??"none"}`),u}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:s,isCanvasActive:a}=e;let i=0;return async(o,r)=>{if(a&&!a())return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."};const l=r,c=l.id,d="panel";let u=l.components;if("string"==typeof u)try{u=JSON.parse(u)}catch{}if(!c||!Array.isArray(u)||!u.length)return{success:!1,error:"id and components (array) are required"};if(!t.validateComponentCount(u))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(c))return n.warn?.(`[canvas] Rate limited surfaceId=${c}`),s({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:c}}),{success:!1,error:"Rate limited — too many updates per second"};const p=translateComponentsWithDataModel(u),m=p.components,h=Object.keys(p.dataModel).length>0?p.dataModel:void 0,f=findRootIds(m),b=nextCid(),g=columnNode(b,f),y=[...m,g],x=e.getActiveSessionId?.(o);if(!x)return n.warn?.(`[canvas] No active session for toolCallId=${o}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};const v=e.getGatewaySessionKey?.(o),C=await ensureRelayConnected(e,x,o,v),N=t.has(c,C??void 0),w=Date.now(),A=t.getRelayClient();C&&A&&w-i>6e4&&A.createSnapshot(C,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),N?t.update(c,y,C,b,h):t.create(C??`_fallback_${Date.now()}`,c,d,y,{origin:{kind:"agent"},rootComponentId:b,dataModel:h});const k=u.map(e=>e.type);s({component:"canvas",domain:"canvas",name:N?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:c,presentation:d,componentCount:u.length,componentTypes:k}}),n.debug?.(`[canvas] ${N?"Updated":"Created"} surface=${c} components=${u.length}`);return{success:!0,surfaceId:c,presentation:d,isUpdate:N,activeSurfaces:C?t.getAllActive(C).map(e=>e.surfaceId):[c]}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:s}=e;return async(a,i)=>{const o=i.id;if(!o)return{success:!1,error:"id is required"};const r=t.get(o),l=r?Date.now()-r.createdAt:0,c=e.getGatewaySessionKey?.(a),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(o,d),s({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:o,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${o}`),{success:!0,surfaceId:o,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**Charts**: chart (chartType, title?, labels, datasets:[{label, data, color?}])\n - chartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, funnel, gauge, candlestick, sparkline\n - labels: x-axis labels array (e.g. ["Jan","Feb","Mar"])\n - datasets: array of series — each has label (series name), data (number[]), optional color (hex)\n - candlestick data format: datasets:[{label, data:[{open,high,low,close}]}] — MUST use full field names (open/high/low/close), NOT abbreviations (o/h/l/c)\n - gauge: uses value + max instead of labels/datasets (e.g. chart(chartType:"gauge", title:"CPU", value:73, max:100))\n - sparkline: compact inline chart, no axes — use for trend indicators\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- **Bar chart**: chart(chartType:"bar", title:"Monthly Sales", labels:["Jan","Feb","Mar"], datasets:[{label:"Revenue", data:[100,200,150]}])\n- **Multi-series line**: chart(chartType:"line", title:"Trends", labels:["Q1","Q2","Q3"], datasets:[{label:"2024", data:[10,20,15]}, {label:"2025", data:[15,25,30]}])\n- **Pie chart**: chart(chartType:"pie", title:"Market Share", labels:["A","B","C"], datasets:[{label:"share", data:[40,35,25]}])\n- **Candlestick**: chart(chartType:"candlestick", title:"BTC/USD", labels:["Mon","Tue","Wed"], datasets:[{label:"price", data:[{open:100,high:110,low:95,close:105},{open:105,high:115,low:100,close:108},{open:108,high:120,low:105,close:118}]}])\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, charts). 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}]. Chart supports: chartType(bar/horizontal_bar/line/area/scatter/pie/doughnut/radar/funnel/gauge/candlestick/sparkline), title, labels, datasets:[{label,data,color?}]. Candlestick data:[{open,high,low,close}] (use full names, not o/h/l/c). Dataset uses "label" field (not "name"). Gauge uses value+max. 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),a=n.length-s.length;let i=`[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(", "),a=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${s}${a})`}).join("; ")}.`;return a>0&&(i+=` ...and ${a} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
|
|
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:n}}}}function imageNode(e,t,n="mediumFeature",s,a){const i={url:sv(t),usageHint:n};return s&&(i.fit=s),a&&(i.altText=sv(a)),{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:t}}}}function buttonNode(e,t,n,s){const a={name:n};return s&&(a.context=s),{id:e,component:{Button:{child:t,action:a}}}}function columnNode(e,t,n,s){const a={children:{explicitList:t}};return n&&(a.distribution=n),s&&(a.alignment=s),{id:e,component:{Column:a}}}function rowNode(e,t,n,s){const a={children:{explicitList:t}};return n&&(a.distribution=n),s&&(a.alignment=s),{id:e,component:{Row:a}}}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 a={label:sv(t),type:n};return s&&(a.text={path:s}),{id:e,component:{TextField:a}}}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,a){const i={label:sv(t),options:n.map(e=>({label:sv(e.label),value:e.value})),selections:{path:s}};return a&&(i.description=sv(a)),{id:e,component:{MultipleChoice:i}}}function translateComponentsWithDataModel(e){const t={},n=[];for(const s of e){if(!s||"object"!=typeof s){const e=nextCid();n.push(textNode(e,"[invalid component]"));continue}if("form"===("string"==typeof s.type?s.type.toLowerCase():"text")){const e=buildFormWithDataModel(s);n.push(...e.nodes),Object.assign(t,e.dataModel)}else n.push(...translateComponent(s))}return{components:n,dataModel:t}}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),a=nextCid(),i="row"===t?rowNode(a,s):columnNode(a,s);return[...n,i]}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 a=nextCid();t.push(imageNode(a,s(e.image),"mediumFeature","cover")),n.push(a)}if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h3")),n.push(a)}if(e.subtitle){const a=nextCid();t.push(textNode(a,s(e.subtitle),"caption")),n.push(a)}if(e.description){const a=nextCid();t.push(textNode(a,s(e.description),"body")),n.push(a)}const a=Array.isArray(e.fields)?e.fields:[];for(const e of a){if(!e||"object"!=typeof e)continue;const a=e,i=nextCid(),o=nextCid(),r=nextCid();t.push(textNode(o,s(a.label??a.key),"caption")),t.push(textNode(r,s(a.value))),t.push(rowNode(i,[o,r],"spaceBetween")),n.push(i)}const i=Array.isArray(e.actions)?e.actions:[];if(i.length>0){const e=[];for(const n of i){if(!n||"object"!=typeof n)continue;const a=n,i=nextCid(),o=nextCid(),r=s(a.label??"Action"),l=s(a.actionId??a.id??nextCid());t.push(textNode(o,r)),t.push(buttonNode(i,o,l,[{key:"actionId",value:{literalString:l}},{key:"label",value:{literalString:r}}])),e.push(i)}if(e.length>0){const s=nextCid();t.push(rowNode(s,e)),n.push(s)}}if(e.footer){const a=nextCid();t.push(textNode(a,s(e.footer),"caption")),n.push(a)}const o=nextCid();t.push(columnNode(o,n));const r=nextCid();return t.push(cardNode(r,o)),t}function buildTable(e){const t=[],n=[],a=Array.isArray(e.columns)?e.columns:[],i=Array.isArray(e.rows)?e.rows:[];if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h4")),n.push(a)}const o=[],r=[];for(const e of a)if("string"==typeof e)o.push(e),r.push(e);else if(e&&"object"==typeof e){const t=e,n=s(t.key??t.id??t.field??t.name??t.label??"");o.push(s(t.label??t.title??t.header??t.name??n)),r.push(n)}else{const t=s(e);o.push(t),r.push(t)}if(o.length>0){const e=[];for(const n of o){const s=nextCid();t.push(textNode(s,n,"caption")),e.push(s)}const s=nextCid();t.push(rowNode(s,e)),n.push(s);const a=nextCid();t.push(dividerNode(a)),n.push(a)}for(const e of i){let a;if(Array.isArray(e))a=e;else if(e&&"object"==typeof e&&r.length>0){const t=e;a=r.map(e=>{const n=Object.keys(t).find(t=>t===e||t.toLowerCase()===e.toLowerCase());return null!=n?t[n]:""})}else a=[e];const i=[];for(const e of a){const n=nextCid();t.push(textNode(n,s(e))),i.push(n)}const o=nextCid();t.push(rowNode(o,i)),n.push(o)}const l=nextCid();t.push(columnNode(l,n));const c=nextCid();return t.push(cardNode(c,l)),t}function buildForm(e){return buildFormWithDataModel(e).nodes}function buildFormWithDataModel(e){const t=[],n=[],a=Array.isArray(e.fields)?e.fields:[],i={};if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h3")),n.push(a)}if(e.description){const a=nextCid();t.push(textNode(a,s(e.description),"body")),n.push(a)}const o=[];for(const e of a){if(!e||"object"!=typeof e)continue;const a=e,r=nextCid(),l=s(a.name??a.label??r),c=s(a.inputType??a.type??"text"),d=`/${l}`;o.push(l);const u=a.defaultValue??a.default??a.value;if("checkbox"===c||"boolean"===c||"toggle"===c)i[l]=null!=u&&!!u,t.push(checkboxNode(r,s(a.label??a.name??""),d));else if("select"===c||"multi_select"===c||"dropdown"===c||"radio"===c){const e=Array.isArray(a.options)?a.options:[],n=e.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??"")}});if(null!=u)i[l]=String(u);else if(e.length>0){const t=e[0];i[l]="string"==typeof t?t:s(t.value??"")}else i[l]="";t.push(multipleChoiceNode(r,s(a.label??a.name??""),n,d))}else if("datetime"===c||"time"===c){i[l]=null!=u?String(u):"";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 a.min?a.min:0,n="number"==typeof a.max?a.max:100;i[l]=null!=u?Number(u):e,t.push({id:r,component:{Slider:{label:sv(s(a.label??a.name??"")),value:{path:d},minValue:e,maxValue:n}}})}else{i[l]=null!=u?String(u):"";const e="number"===c?"number":"date"===c?"date":"textarea"===c||"longText"===c?"longText":"shortText";t.push(textFieldNode(r,s(a.label??a.name??""),e,d))}n.push(r)}const r=o.map(e=>({key:e,value:{path:`/${e}`}})),l=nextCid(),c=nextCid();t.push(textNode(l,s(e.submitLabel??"Submit"))),t.push(buttonNode(c,l,s(e.submitActionId??"form_submit"),r)),n.push(c);const d=nextCid();return t.push(columnNode(d,n)),{nodes:t,dataModel:i}}function buildImage(e){const t=nextCid(),n=e.alt??e.altText;return[imageNode(t,s(e.url??e.src??""),"mediumFeature",void 0,n?s(n):void 0)]}function buildButton(e){const t=nextCid(),n=nextCid(),a=s(e.label??"Button"),i=s(e.actionId??n);return[textNode(t,a),buttonNode(n,t,i,[{key:"actionId",value:{literalString:i}},{key:"label",value:{literalString:a}}])]}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),a=[],i=[];for(let e=0;e<s.length;e+=n){const t=s.slice(e,e+n),o=nextCid();a.push(rowNode(o,t)),i.push(o)}const o=nextCid();return[...t,...a,columnNode(o,i)]}function buildStat(e){const t=[],n=[];if(e.icon){const a=nextCid();t.push(iconNode(a,s(e.icon))),n.push(a)}if(e.label){const a=nextCid();t.push(textNode(a,s(e.label),"caption")),n.push(a)}const a=nextCid();if(t.push(textNode(a,s(e.value??"0"),"h2")),n.push(a),e.change){const a=nextCid(),i="positive"===e.changeType?"↑ ":"negative"===e.changeType?"↓ ":"";t.push(textNode(a,`${i}${s(e.change)}`,"caption")),n.push(a)}if(e.description){const a=nextCid();t.push(textNode(a,s(e.description),"caption")),n.push(a)}const i=nextCid();t.push(columnNode(i,n,void 0,"center"));const o=nextCid();return t.push(cardNode(o,i)),t}function buildKeyValue(e){const t=[],n=[],a=Array.isArray(e.items)?e.items:[];if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h4")),n.push(a)}for(const e of a){if(!e||"object"!=typeof e)continue;const a=e,i=nextCid(),o=nextCid(),r=nextCid();t.push(textNode(o,s(a.key??a.label??""),"caption")),t.push(textNode(r,s(a.value??""))),t.push(rowNode(i,[o,r],"spaceBetween")),n.push(i)}const i=nextCid();t.push(columnNode(i,n));const o=nextCid();return t.push(cardNode(o,i)),t}function buildList(e){const t=[],n=[],a=Array.isArray(e.items)?e.items:[];if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h4")),n.push(a)}for(const e of a){if(!e||"object"!=typeof e)continue;const a=e,i=[];if(a.image){const e=nextCid();t.push(imageNode(e,s(a.image),"avatar")),i.push(e)}if(a.icon){const e=nextCid();t.push(iconNode(e,s(a.icon))),i.push(e)}const o=[];if(a.title||a.name){const e=nextCid();t.push(textNode(e,s(a.title??a.name),"body")),o.push(e)}if(a.subtitle||a.description){const e=nextCid();t.push(textNode(e,s(a.subtitle??a.description),"caption")),o.push(e)}if(o.length>0){const e=nextCid();t.push(columnNode(e,o)),i.push(e)}if(a.badge||a.value){const e=nextCid();t.push(textNode(e,s(a.badge??a.value),"caption")),i.push(e)}const r=nextCid();t.push(rowNode(r,i,"start","center")),n.push(r)}const i=nextCid();return t.push(columnNode(i,n)),t}function buildAlert(e){const t=[],n=[],a=s(e.variant??"info"),i=nextCid();t.push(iconNode(i,{info:"info",success:"check",warning:"error",error:"error",tip:"info",caution:"error",important:"error",note:"info"}[a]??"info")),n.push(i);const o=[];if(e.title){const n=nextCid();t.push(textNode(n,s(e.title),"h5")),o.push(n)}if(e.message||e.content){const n=nextCid();t.push(textNode(n,s(e.message??e.content))),o.push(n)}if(0===o.length){const n=nextCid();t.push(textNode(n,s(e.type??"Alert"))),o.push(n)}const r=nextCid();t.push(columnNode(r,o)),n.push(r);const l=nextCid();t.push(rowNode(l,n,"start","start"));const c=nextCid();return t.push(cardNode(c,l)),t}function buildCode(e){const t=s(e.code??""),n=s(e.language??""),a=n?`\`\`\`${n}\n${t}\n\`\`\``:`\`\`\`\n${t}\n\`\`\``;return[textNode(nextCid(),a)]}function buildMarkdown(e){return[textNode(nextCid(),s(e.content??""))]}function normalizeDatasets(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t={label:e.label??e.name??"",data:Array.isArray(e.data)?e.data.map(e=>e&&"object"==typeof e&&("o"in e||"h"in e||"l"in e||"c"in e)?{open:e.o??e.open,high:e.h??e.high,low:e.l??e.low,close:e.c??e.close}:e):e.data};return e.color&&(t.color=e.color),t})}function buildChart(e){const t=nextCid(),n=s(e.chartType??"bar"),a={chartType:sv(n)};e.title&&(a.title=sv(s(e.title))),Array.isArray(e.labels)&&(a.labels=e.labels.map(e=>"string"==typeof e?e:String(e??"")));const i=Array.isArray(e.datasets)?e.datasets:Array.isArray(e.stages)?e.stages:null;return i&&(a.datasets=normalizeDatasets(i)),void 0!==e.value&&(a.value="number"==typeof e.value?e.value:Number(e.value)||0),void 0!==e.max&&(a.max="number"==typeof e.max?e.max:Number(e.max)||100),[{id:t,component:{Chart:a}}]}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=[],a=[];for(const e of t){const t=s(e.label??e.title??e.id??"Tab"),i=(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e));n.push(...i);const o=findRootIds(i),r=nextCid();n.push(columnNode(r,o)),a.push({title:t,childId:r})}const i=nextCid();return n.push(tabsNode(i,a)),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),a=n>0?Math.round(t/n*100):0;return[textNode(nextCid(),`${s(e.label)||"Progress"}: ${a}% (${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 a=Array.isArray(e.children)?e.children.flatMap(e=>translateComponent(e)):[];n.push(...a)}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:a,logger:i}=e,o=s??e.getGatewaySessionKey?.(n),r=o??t,l=a.resolveCanvasForSession(r);if(l&&a.isCanvasJoined(l))return l;let c=a.getRelayClient();if(c||(c=await a.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const s=e.getPlatformAgentId?.(n),l=o??t,d=o&&o!==t?t:void 0,u=await c.ensureCanvas(l,{agentId:s,linkSessionId:d});return await a.joinCanvas(u,r),i.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${o??"none"} agentId=${s??"none"}`),u}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:s,isCanvasActive:a}=e;let i=0;return async(o,r)=>{if(a&&!a())return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."};const l=r,c=l.id,d="panel";let u=l.components;if("string"==typeof u)try{u=JSON.parse(u)}catch{}if(!c||!Array.isArray(u)||!u.length)return{success:!1,error:"id and components (array) are required"};if(!t.validateComponentCount(u))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(c))return n.warn?.(`[canvas] Rate limited surfaceId=${c}`),s({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:c}}),{success:!1,error:"Rate limited — too many updates per second"};const p=translateComponentsWithDataModel(u),m=p.components,h=Object.keys(p.dataModel).length>0?p.dataModel:void 0,f=findRootIds(m),b=nextCid(),g=columnNode(b,f),y=[...m,g],x=e.getActiveSessionId?.(o);if(!x)return n.warn?.(`[canvas] No active session for toolCallId=${o}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};const v=e.getGatewaySessionKey?.(o),C=await ensureRelayConnected(e,x,o,v),N=t.has(c,C??void 0),w=Date.now(),A=t.getRelayClient();C&&A&&w-i>6e4&&A.createSnapshot(C,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),N?t.update(c,y,C,b,h):t.create(C??`_fallback_${Date.now()}`,c,d,y,{origin:{kind:"agent"},rootComponentId:b,dataModel:h});const k=u.map(e=>e.type);s({component:"canvas",domain:"canvas",name:N?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:c,presentation:d,componentCount:u.length,componentTypes:k}}),n.debug?.(`[canvas] ${N?"Updated":"Created"} surface=${c} components=${u.length}`);return{success:!0,surfaceId:c,presentation:d,isUpdate:N,activeSurfaces:C?t.getAllActive(C).map(e=>e.surfaceId):[c]}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:s}=e;return async(a,i)=>{const o=i.id;if(!o)return{success:!1,error:"id is required"};const r=t.get(o),l=r?Date.now()-r.createdAt:0,c=e.getGatewaySessionKey?.(a),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(o,d),s({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:o,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${o}`),{success:!0,surfaceId:o,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**Charts**: chart (chartType, title?, labels, datasets:[{label, data, color?}])\n - chartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, funnel, gauge, candlestick, sparkline\n - labels: x-axis labels array (e.g. ["Jan","Feb","Mar"])\n - datasets: array of series — each has label (series name), data (number[]), optional color (hex)\n - candlestick data format: datasets:[{label, data:[{open,high,low,close}]}] — MUST use full field names (open/high/low/close), NOT abbreviations (o/h/l/c)\n - gauge: uses value + max instead of labels/datasets (e.g. chart(chartType:"gauge", title:"CPU", value:73, max:100))\n - sparkline: compact inline chart, no axes — use for trend indicators\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- **Bar chart**: chart(chartType:"bar", title:"Monthly Sales", labels:["Jan","Feb","Mar"], datasets:[{label:"Revenue", data:[100,200,150]}])\n- **Multi-series line**: chart(chartType:"line", title:"Trends", labels:["Q1","Q2","Q3"], datasets:[{label:"2024", data:[10,20,15]}, {label:"2025", data:[15,25,30]}])\n- **Pie chart**: chart(chartType:"pie", title:"Market Share", labels:["A","B","C"], datasets:[{label:"share", data:[40,35,25]}])\n- **Candlestick**: chart(chartType:"candlestick", title:"BTC/USD", labels:["Mon","Tue","Wed"], datasets:[{label:"price", data:[{open:100,high:110,low:95,close:105},{open:105,high:115,low:100,close:108},{open:108,high:120,low:105,close:118}]}])\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, charts). 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}]. Chart supports: chartType(bar/horizontal_bar/line/area/scatter/pie/doughnut/radar/funnel/gauge/candlestick/sparkline), title, labels, datasets:[{label,data,color?}]. Candlestick data:[{open,high,low,close}] (use full names, not o/h/l/c). Dataset uses "label" field (not "name"). Gauge uses value+max. 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),a=n.length-s.length;let i=`[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(", "),a=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${s}${a})`}).join("; ")}.`;return a>0&&(i+=` ...and ${a} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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"./upload/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{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.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"./surface/form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null;function initializePlugin(e){let a,o;try{a=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:a})}catch(a){return e.logger.error?.("[beeos-claw] Failed to resolve config:",a),null}const r=o.config,t=buildThirdPartyChannelSet(a);t.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...t].join(", ")}`);const s=r.log.enabled?createObsEventWriter({dir:r.log.dir,redaction:{gatewayToken:r.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:r,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const a of o.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:a.severity,summary:a.message,error:{code:a.code,message:a.message,nextSteps:a.nextSteps}}),"error"===a.severity?e.logger.error?.(`[beeos-claw] ${a.message}`):e.logger.warn?.(`[beeos-claw] ${a.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),r.keyFile)try{agentKeys=loadAgentKeyPair(r.keyFile);const a=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${a})`)}catch(a){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${a}`),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)`);return{cfg:r,writeObs:s,thirdPartyChannels:t,openclawConfig:a}}function registerUploadTools(e,a,o){const{cfg:r,writeObs:t}=a;cleanupUploadToolResultCache().catch(a=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(a)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(a,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${a} params=${JSON.stringify(s)}`);const n=await executeUpload(s,r,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${a} ok=${n.ok}`),t({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:a,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),n.ok&&n.content&&o.current&&o.current.pushToolResultContent(a,BEEOS_UPLOAD_TOOL_NAME,n.content),(async o=>{try{await writeUploadToolResult(a,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${a}: ${String(o)}`)}return o})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,o){await handleFileStage(e,o,a.cfg)}})}function registerServices(e,a,o){const{cfg:r,writeObs:t,thirdPartyChannels:s}=a,n=agentKeys?(e,a)=>agentFetch(e,agentKeys,a):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0,l=new ACPServer(r,e.logger,t,n,i,s);o.current=l,e.registerService({id:"beeos-acp",async start(){if(!r.bridge.url&&r.agentGatewayUrl&&n){const a=await discoverBridgeFromGW(r.agentGatewayUrl,n);if(a)r.bridge.url=a.url,writeDiscoveryCache({bridge:{url:a.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${a.url} (region: ${a.region??"any"})`);else{const a=readDiscoveryCache();a?.bridge?.url?(r.bridge.url=a.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${a.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(r.bridge.url||void 0),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const g=new TaskHandleRegistry;let c=null;const{enabled:d,url:p,principalId:f,a2aEnabled:y}=r.messageBus;let u;if(r.agentGatewayUrl&&agentKeys){const e=r.agentGatewayUrl,a=agentKeys;u=async o=>{const r=await agentFetch(`${e}/api/v1/messaging/token`,a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({principal_id:o})});if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`Gateway messaging token returned ${r.status}: ${e}`)}return r.json()}}if(d&&(u||r.messageBus.serviceUrl)){c=new MessageSDK({serviceUrl:u?void 0:r.messageBus.serviceUrl,centrifugoUrl:p},e.logger);const a=f;e.registerService({id:"beeos-message-bus",async start(){let o=a;if(o||(o=process.env.BEEOS_PRINCIPAL_ID||""),!o&&l)try{await l.waitForAgentMapping(),o=l.getDefaultPlatformAgentId()||""}catch(a){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",a)}o?(await c.connect(o,{centrifugoUrl:p,tokenProvider:u}),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s, via=%s)",o,u?"gateway":"direct")):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(c?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const o=(e,a,o,r)=>l.agentSend(e,a,o,r),t=(e,a,o)=>l.trackA2ARun(e,a,o),s=e=>l.emitA2ASystemNotification(e),n=r.agentGatewayUrl?{agentGatewayUrl:r.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger}:void 0;c.onMessage("a2a_task",createA2ATaskHandler(o,n,t,s)),c.onMessage("a2a_result",createA2AResultHandler(o,g,s)),c.onMessage("a2a_task_cancel",createA2ATaskCancelHandler(o,e=>l.cancelA2ARunByTaskId(e))),n&&e.registerService({id:"beeos-a2a-poll",async start(){await l.waitForAgentMapping();const a=await pollPendingTasks(n,o,t);a>0&&e.logger.info?.("[beeos-claw] Dispatched %d pending A2A tasks on startup",a)}})}else d?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)",d);if(!1!==y&&r.agentGatewayUrl){const a=e=>l.getGatewaySessionKeyForToolCall(e),o=e=>l.getMentionsForToolCall(e),t=(e,a)=>{l.broadcastNotification(e,a)};registerA2ATools(e,{agentGatewayUrl:r.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},g,a,o,t),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(r.canvasEnabled){const a=new CanvasStateManager;if(r.agentGatewayUrl&&agentKeys){let o;a.setLogger(e.logger);{const e=r.agentGatewayUrl,a=agentKeys;o=async()=>{const o=await agentFetch(`${e}/api/v1/canvas/token`,a,{method:"POST"});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Canvas token returned ${o.status}: ${e}`)}return o.json()}}e.registerService({id:"beeos-canvas-relay",async start(){const t=()=>{try{const t=l.getDefaultPlatformAgent();if(!t?.id)return void e.logger.info?.("[beeos-claw] Canvas Relay skipped: no agentId available");const s=new CanvasRelayClient({canvasRelayUrl:r.canvasRelayUrl,agentKeys:agentKeys,agentId:t.id,displayName:t.displayName,avatarUrl:t.avatarUrl,tokenProvider:o,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});s.connect(),a.setRelayClient(s),a.setAgentId(t.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${t.id}, name=${t.name??"none"})`)}catch(a){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${a}`)}};try{await l.waitForAgentMapping();const a=l.getDefaultPlatformAgent();a?.id?t():(e.logger.info?.("[beeos-claw] Canvas Relay deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{e.logger.info?.("[beeos-claw] Canvas Relay: agent mapping now available, starting relay client"),t()}))}catch(a){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${a}`)}},stop(){a.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const o={stateManager:a,logger:e.logger,writeObs:e=>t(e),isCanvasActive:()=>l.isCanvasActive(),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},n=new FormSchemaRegistry;n.register(CRON_CREATE_SCHEMA),n.register(CRON_UPDATE_SCHEMA);const i=new ResourceDescriptorRegistry;i.register(CRON_RESOURCE);const g=e=>async(a,o)=>(l.preMapToolCallSession(a),e(a,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(o))}),l.setCanvasState(a,{schemaRegistry:n,descriptorRegistry:i});const c=l.getFormBuilder();c&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:n,builder:c,logger:e.logger}))});const d=l.getResourceRenderer();if(d){generateActionTools(i,e,async(e,a)=>l.gatewayRequest(e,a),d,e.logger)}const p=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),f=256,y=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,a)=>{if(a?.sessionKey){if(y.size>=f&&!y.has(a.sessionKey)){const e=y.keys().next().value;void 0!==e&&y.delete(e)}y.set(a.sessionKey,a.channelId)}if(isThirdPartyChannel(s,{sessionKey:a?.sessionKey,channelId:a?.channelId}))return;return(a?.sessionKey?l.isCanvasActiveForGatewayKey(a.sessionKey):l.isCanvasActive())?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. 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,a)=>{if(a?.sessionKey&&l.setPendingToolCallSessionKey(a.sessionKey),!p.has(e.toolName))return;if(isThirdPartyChannel(s,{sessionKey:a?.sessionKey}))return{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."};return(a?.sessionKey?l.isCanvasActiveForGatewayKey(a.sessionKey):l.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 + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,b=createTerminalWebSocketService({config:{port:r.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:r.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:t});if(r.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${r.terminalWsPort}`)},stop(){b.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)"),a.openclawConfig){const o=mirrorOpenClawConfig(a.openclawConfig);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}}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){if(e.registrationMode&&"full"!==e.registrationMode)return void e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`);const a=initializePlugin(e);if(!a)return;const o={current:null};registerUploadTools(e,a,o),registerServices(e,a,o),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"./upload/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{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.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"./surface/form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null;function initializePlugin(e){let a,o;try{a=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:a})}catch(a){return e.logger.error?.("[beeos-claw] Failed to resolve config:",a),null}const r=o.config,t=buildThirdPartyChannelSet(a);t.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...t].join(", ")}`);const s=r.log.enabled?createObsEventWriter({dir:r.log.dir,redaction:{gatewayToken:r.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:r,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const a of o.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:a.severity,summary:a.message,error:{code:a.code,message:a.message,nextSteps:a.nextSteps}}),"error"===a.severity?e.logger.error?.(`[beeos-claw] ${a.message}`):e.logger.warn?.(`[beeos-claw] ${a.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),r.keyFile)try{agentKeys=loadAgentKeyPair(r.keyFile);const a=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${a})`)}catch(a){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${a}`),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)`);return{cfg:r,writeObs:s,thirdPartyChannels:t,openclawConfig:a}}function registerUploadTools(e,a,o){const{cfg:r,writeObs:t}=a;cleanupUploadToolResultCache().catch(a=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(a)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(a,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${a} params=${JSON.stringify(s)}`);const n=await executeUpload(s,r,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${a} ok=${n.ok}`),t({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:a,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),n.ok&&n.content&&o.current&&o.current.pushToolResultContent(a,BEEOS_UPLOAD_TOOL_NAME,n.content),(async o=>{try{await writeUploadToolResult(a,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${a}: ${String(o)}`)}return o})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,o){await handleFileStage(e,o,a.cfg)}})}function registerServices(e,a,o){const{cfg:r,writeObs:t,thirdPartyChannels:s}=a,n=agentKeys?(e,a)=>agentFetch(e,agentKeys,a):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0,l=new ACPServer(r,e.logger,t,n,i,s);o.current=l,e.registerService({id:"beeos-acp",async start(){if(!r.bridge.url&&r.agentGatewayUrl&&n){const a=await discoverBridgeFromGW(r.agentGatewayUrl,n);if(a)r.bridge.url=a.url,writeDiscoveryCache({bridge:{url:a.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${a.url}`);else{const a=readDiscoveryCache();a?.bridge?.url?(r.bridge.url=a.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${a.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(r.bridge.url||void 0),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const g=new TaskHandleRegistry;let c=null;const{enabled:d,url:p,principalId:f,a2aEnabled:u}=r.messageBus;let y;if(r.agentGatewayUrl&&agentKeys){const e=r.agentGatewayUrl,a=agentKeys;y=async o=>{const r=await agentFetch(`${e}/api/v1/messaging/token`,a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({principal_id:o})});if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`Gateway messaging token returned ${r.status}: ${e}`)}return r.json()}}if(d&&(y||r.messageBus.serviceUrl)){c=new MessageSDK({serviceUrl:y?void 0:r.messageBus.serviceUrl,centrifugoUrl:p},e.logger);const a=f,o=async a=>{await c.connect(a,{centrifugoUrl:p,tokenProvider:y}),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s, via=%s)",a,y?"gateway":"direct")};e.registerService({id:"beeos-message-bus",async start(){let r=a;if(r||(r=process.env.BEEOS_PRINCIPAL_ID||""),!r&&l)try{await l.waitForAgentMapping(),r=l.getDefaultPlatformAgentId()||""}catch(a){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",a)}r?await o(r):l?(e.logger.info?.("[beeos-claw] Message bus deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const a=l.getDefaultPlatformAgentId();a?o(a).catch(a=>{e.logger.error?.("[beeos-claw] Deferred message bus connect failed:",a)}):e.logger.warn?.("[beeos-claw] Agent mapping ready but no principalId, message bus stays disabled")})):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(c?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const t=(e,a,o,r)=>l.agentSend(e,a,o,r),s=(e,a,o)=>l.trackA2ARun(e,a,o),n=e=>l.emitA2ASystemNotification(e),i=r.agentGatewayUrl?{agentGatewayUrl:r.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger}:void 0;c.onMessage("a2a_task",createA2ATaskHandler(t,i,s,n)),c.onMessage("a2a_result",createA2AResultHandler(t,g,n)),c.onMessage("a2a_task_cancel",createA2ATaskCancelHandler(t,e=>l.cancelA2ARunByTaskId(e))),i&&e.registerService({id:"beeos-a2a-poll",async start(){await l.waitForAgentMapping();const a=await pollPendingTasks(i,t,s);a>0&&e.logger.info?.("[beeos-claw] Dispatched %d pending A2A tasks on startup",a)}})}else d?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)",d);if(!1!==u&&r.agentGatewayUrl){const a=e=>l.getGatewaySessionKeyForToolCall(e),o=e=>l.getMentionsForToolCall(e),t=(e,a)=>{l.broadcastNotification(e,a)};registerA2ATools(e,{agentGatewayUrl:r.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},g,a,o,t),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(r.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const a=sharedCanvasState;if(r.agentGatewayUrl&&agentKeys){let o;a.setLogger(e.logger);{const e=r.agentGatewayUrl,a=agentKeys;o=async()=>{const o=await agentFetch(`${e}/api/v1/canvas/token`,a,{method:"POST"});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Canvas token returned ${o.status}: ${e}`)}return o.json()}}e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const t=l.getDefaultPlatformAgent();if(!t?.id)return void e.logger.info?.("[beeos-claw] Canvas Relay skipped: no agentId after sync");const s=new CanvasRelayClient({canvasRelayUrl:r.canvasRelayUrl,agentKeys:agentKeys,agentId:t.id,displayName:t.displayName,avatarUrl:t.avatarUrl,tokenProvider:o,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});s.connect(),a.setRelayClient(s),a.setAgentId(t.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${t.id}, name=${t.name??"none"})`)}catch(a){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${a}`)}},stop(){a.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const o={stateManager:a,logger:e.logger,writeObs:e=>t(e),isCanvasActive:()=>l.isCanvasActive(),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},n=new FormSchemaRegistry;n.register(CRON_CREATE_SCHEMA),n.register(CRON_UPDATE_SCHEMA);const i=new ResourceDescriptorRegistry;i.register(CRON_RESOURCE);const g=e=>async(a,o)=>(l.preMapToolCallSession(a),e(a,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(o))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(o))}),l.setCanvasState(a,{schemaRegistry:n,descriptorRegistry:i});const c=l.getFormBuilder();c&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:n,builder:c,logger:e.logger}))});const d=l.getResourceRenderer();if(d){generateActionTools(i,e,async(e,a)=>l.gatewayRequest(e,a),d,e.logger)}const p=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),f=256,u=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,a)=>{if(a?.sessionKey){if(u.size>=f&&!u.has(a.sessionKey)){const e=u.keys().next().value;void 0!==e&&u.delete(e)}u.set(a.sessionKey,a.channelId)}if(isThirdPartyChannel(s,{sessionKey:a?.sessionKey,channelId:a?.channelId}))return;return(a?.sessionKey?l.isCanvasActiveForGatewayKey(a.sessionKey):l.isCanvasActive())?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. 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,a)=>{if(a?.sessionKey&&l.setPendingToolCallSessionKey(a.sessionKey),!p.has(e.toolName))return;if(isThirdPartyChannel(s,{sessionKey:a?.sessionKey}))return{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."};return(a?.sessionKey?l.isCanvasActiveForGatewayKey(a.sessionKey):l.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 + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,b=createTerminalWebSocketService({config:{port:r.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:r.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:t});if(r.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${r.terminalWsPort}`)},stop(){b.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)"),a.openclawConfig){const o=mirrorOpenClawConfig(a.openclawConfig);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}}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){if(e.registrationMode&&"full"!==e.registrationMode)return void e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`);const a=initializePlugin(e);if(!a)return;const o={current:null};registerUploadTools(e,a,o),registerServices(e,a,o),e.logger.info?.("[beeos-claw] Plugin registered successfully")}};export default plugin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Centrifuge}from"centrifuge";import WebSocket from"ws";export class MessageSDK{config;client=null;personalSub=null;principalId="";handlers=new Map;connectLock=null;logger;constructor(e,
|
|
1
|
+
import{Centrifuge}from"centrifuge";import WebSocket from"ws";export class MessageSDK{config;client=null;personalSub=null;principalId="";handlers=new Map;connectLock=null;_resubscribeTimer=null;_resubscribeAttempts=0;static MAX_RESUBSCRIBE_ATTEMPTS=5;logger;constructor(e,s){this.config=e,this.logger=s}async getToken(e,s,t){const i={principal_id:e};return s&&(i.claims=s),t&&(i.ttl_seconds=t),this.restRequest("POST","/api/v1/tokens",i)}async send(e,s,t){await this.restRequest("POST","/api/v1/messages/send",{target_id:e,message_type:s,payload:t})}async isOnline(e){return(await this.restRequest("GET",`/api/v1/connections/${encodeURIComponent(e)}/online`)).online}async registerWebhook(e,s,t){const i={url:e,events:s};return t&&(i.secret=t),this.restRequest("POST","/api/v1/webhooks",i)}async removeWebhook(e){await this.restRequest("DELETE",`/api/v1/webhooks/${encodeURIComponent(e)}`)}async connect(e,s){let t;this.connectLock&&await this.connectLock,this.connectLock=new Promise(e=>{t=e});try{await this._doConnect(e,s)}finally{this.connectLock=null,t()}}async _doConnect(e,s){this.client&&(this.logger.info?.("[message-sdk] Disconnecting previous client before reconnect"),await this.disconnect()),this.principalId=e;const t=s?.tokenProvider??(e=>this.getToken(e)),i=await t(e),r=s?.centrifugoUrl||this.config.centrifugoUrl||i.centrifugo_url;if(!r)throw new Error("No Centrifugo URL available");const n=`personal:${e}`,o=Array.isArray(i.channels)&&i.channels.includes(n);this.logger.info?.(`[message-sdk] Connecting as principal=${e} url=${r} serverSideSub=${o}`);this.client=new Centrifuge(r,{token:i.token,getToken:async()=>(await t(e)).token,websocket:WebSocket}),this.client.on("connecting",e=>{this.logger.info?.(`[message-sdk] Connecting: code=${e.code} reason=${e.reason}`)}),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)}),o&&(this.client.on("subscribed",e=>{e.channel===n&&this.logger.info?.(`[message-sdk] Server-sub subscribed: channel=${n} recoverable=${e.recoverable} recovered=${e.recovered}`)}),this.client.on("subscribing",e=>{e.channel===n&&this.logger.info?.(`[message-sdk] Server-sub subscribing: channel=${n}`)}),this.client.on("unsubscribed",e=>{e.channel===n&&this.logger.warn?.(`[message-sdk] Server-sub unsubscribed: channel=${n}`)}),this.client.on("publication",e=>{e.channel===n&&this.dispatch(e.data).catch(e=>{this.logger.error?.("[message-sdk] dispatch error:",e)})})),this.client.connect(),o||(this.personalSub=this.client.newSubscription(n),this.personalSub.on("subscribing",e=>{this.logger.info?.(`[message-sdk] Subscription subscribing: code=${e.code} reason=${e.reason} channel=${n}`)}),this.personalSub.on("subscribed",e=>{this._resubscribeAttempts=0,this.logger.info?.(`[message-sdk] Subscription subscribed: channel=${n} recoverable=${e.recoverable} recovered=${e.recovered}`)}),this.personalSub.on("unsubscribed",e=>{this.logger.warn?.(`[message-sdk] Subscription unsubscribed: code=${e.code} reason=${e.reason} channel=${n}`),this._scheduleResubscribe(n,e.code)}),this.personalSub.on("error",e=>{this.logger.error?.(`[message-sdk] Subscription error: type=${e.type} channel=${n}`,e.error)}),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._resubscribeTimer&&(clearTimeout(this._resubscribeTimer),this._resubscribeTimer=null),this._resubscribeAttempts=0,this.personalSub&&(this.personalSub.unsubscribe(),this.personalSub=null),this.client?.disconnect(),this.client=null}onMessage(e,s){this.handlers.set(e,s)}async joinGroup(e,s){if(!this.client)throw new Error("Not connected");const t=this.client.newSubscription(`group:${e}`);return t.on("publication",t=>{s(t.data).catch(s=>{this.logger.error?.(`[message-sdk] group handler error (${e}):`,s)})}),t.subscribe(),t}async publishToGroup(e,s){if(!this.client)throw new Error("Not connected");await this.client.publish(`group:${e}`,{from:this.principalId,content:s,timestamp:Date.now()})}get isConnected(){return"connected"===this.client?.state}get currentPrincipalId(){return this.principalId}_scheduleResubscribe(e,s){if(0===s)return;if(this._resubscribeAttempts>=MessageSDK.MAX_RESUBSCRIBE_ATTEMPTS)return void this.logger.error?.(`[message-sdk] Resubscribe exhausted after ${this._resubscribeAttempts} attempts, channel=${e}`);this._resubscribeTimer&&clearTimeout(this._resubscribeTimer);const t=Math.min(1e3*Math.pow(2,this._resubscribeAttempts),3e4);this._resubscribeAttempts++,this.logger.info?.(`[message-sdk] Scheduling resubscribe in ${t}ms (attempt ${this._resubscribeAttempts}/${MessageSDK.MAX_RESUBSCRIBE_ATTEMPTS}) channel=${e}`),this._resubscribeTimer=setTimeout(()=>{this._resubscribeTimer=null,this.personalSub&&this.client&&"unsubscribed"===this.personalSub.state&&(this.logger.info?.(`[message-sdk] Attempting resubscribe channel=${e}`),this.personalSub.subscribe())},t)}async dispatch(e){const s=this.handlers.get(e.type);s?await s(e):this.logger.debug?.(`[message-sdk] No handler for type=${e.type}`)}async restRequest(e,s,t){if(!this.config.serviceUrl)throw new Error("Message Service REST API requires serviceUrl in config");const i=`${this.config.serviceUrl}${s}`,r={},n={method:e,headers:r};void 0!==t&&(r["Content-Type"]="application/json",n.body=JSON.stringify(t));const o=await fetch(i,n);if(!o.ok){const t=await o.text().catch(()=>"");throw new Error(`Message Service ${e} ${s} returned ${o.status}: ${t}`)}if(204!==o.status)return await o.json()}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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>",
|