beeos-claw 0.2.31 → 0.2.32

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.
@@ -1 +1 @@
1
- import{randomUUID,createHash}from"node:crypto";import{mkdir,writeFile,rm,lstat,readlink,stat,readdir,rename,cp}from"node:fs/promises";import path from"node:path";import{join,resolve,relative,sep}from"node:path";import{homedir}from"node:os";import AdmZip from"adm-zip";import{sanitizePromptPayload,stripTransportMetadata,isHeartbeatPromptText,isHeartbeatOnlyText}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,readEnrichmentCache}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.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}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-",":a2a:"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function isSystemDefaultSessionKey(e){const t=e.split(":");if(t.length<3||"agent"!==t[0])return!1;const s=t.slice(2).join(":");return"main"===s||s===t[1]}function isPlaceholderTitle(e){return!e||"New chat"===e||(!!/^[0-9a-f]{6,12}\s*\(20\d{2}/i.test(e)||(!!/^Sender\s*\(untrusted/i.test(e)||!!/^Conversation\s+info\s*\(untrusted/i.test(e)))}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const n=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,t){const s=createHash("sha256").update(`${t}:${e}`).digest("hex").substring(0,32);return`session:${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20,32)}`}function parseAgentIdFromKey(e){const t=e.split(":");return(t.length>=3&&"agent"===t[0]?t[1]:"")||"main"}function createSessionObject(e){const t=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:t,platformAgentId:e.resolvePlatformAgentId?.(t),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 t of this.state.sessions.values())!t.platformAgentId&&t.agentId&&(t.platformAgentId=e.get(t.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[];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(t=>{this._syncResolvers.push(t);const s=setTimeout(()=>{const e=this._syncResolvers.indexOf(t);-1!==e&&this._syncResolvers.splice(e,1),t()},e);"object"==typeof s&&"unref"in s&&s.unref()})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;onRunEnded=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),this.ensureVerboseFull(e)}async ensureVerboseFull(e){if(e.verbosePatched)return;const t=this.getGateway();if(t?.isConnected){e.verbosePatched=!0;try{await t.sessionsPatch(e.gatewaySessionKey,{verboseLevel:"full"})}catch(t){e.verbosePatched=!1,this.logger.debug?.(`[bridge-core] ensureVerboseFull failed: ${t instanceof Error?t.message:t}`)}}}ensureSessionForKey(e){const t=this.state.findSessionByKey(e);if(t)return t;const s=createSessionObject({gatewayKey:e,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(s),this.logger.debug?.(`[bridge-core] ensureSessionForKey: lazily created session sid=${s.sessionId} key=${e}`),s}async preloadSessionsFromGateway(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.sessionsList();if(!t||"object"!=typeof t)return;const s=t.sessions;if(!Array.isArray(s))return;let n=0;for(const e of s){const t=e,s=t.sessionKey??t.key??t.id??t.name;if(!s||"string"!=typeof s)continue;if(this.state.findSessionByKey(s))continue;const i=t.label||t.derivedTitle||t.title||void 0,o=i&&!isPlaceholderTitle(i)?i:void 0,r=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:o});this.registerSession(r),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"skills/install":await this.handleSkillsInstall(e);break;case"skills/uninstall":await this.handleSkillsUninstall(e);break;case"skills/update":await this.handleSkillsUpdate(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(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSkillInstalled=null;onSessionDeleted=null;onSessionCleared=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/toggle":case"canvas/clear":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const t=e.params,s=t?.surfaceId,n=this.state.resolveSessionFromParams(t);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}n.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,s="string"==typeof e.params?.cwd?e.params.cwd:".",n=createSessionObject({gatewayKey:t,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s});this.registerSession(n),this.transport.sendResult(e.id,{sessionId:n.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:t,agentId:n.agentId,platformAgentId:n.platformAgentId??null}}),this.fetchHistoryInBackground(t)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async recoverSessionFromGateway(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.sessionsList();if(!s||"object"!=typeof s)return;const n=s.sessions;if(!Array.isArray(n))return;for(const t of n){const s=t,n=s.sessionKey??s.key??s.id??s.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const e=s.label||s.derivedTitle||s.title||void 0,t=e&&!isPlaceholderTitle(e)?e:void 0,i=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:t});return this.registerSession(i),i}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let t=this.state.sessions.get(e);if(!t&&e.startsWith("agent:")&&(t=this.state.findSessionByKey(e)),t||e.startsWith("agent:")||(t=await this.recoverSessionFromGateway(e)),t)return{session:t,created:!1};const s=e.startsWith("agent:"),n=this.config.gateway.agentId||this.getDefaultAgentId();return t=createSessionObject({gatewayKey:s?e:`agent:${n}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(t),{session:t,created:!0}}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const{session:s}=await this.resolveOrCreateSession(t),n=s.sessionId;this.state.lastActiveSessionId=n,s.chatReplayDedupKeys.clear(),this.transport.resetSeq(n);let i=[];try{const e=this.getGateway(),t=await loadHistory(e,s.gatewaySessionKey,50);i=groupIntoTurns(t);const o=i.filter(e=>"user"===e.role).length,r=i.filter(e=>"assistant"===e.role).length,a=i.filter(e=>e.toolCalls?.length).length,l=i.reduce((e,t)=>e+(t.toolCalls?.length??0),0);if(this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${t.length} turns=${i.length} userTurns=${o} asstTurns=${r} turnsWithToolCalls=${a} totalToolCalls=${l}`),i.length>0&&isPlaceholderTitle(s.title)&&"manual"!==s.titleSource){const e=i.find(e=>{if("user"!==e.role)return!1;if("internal_system"===e.provenance?.kind)return!1;const t=stripTransportMetadata(e.content).trim();return t.length>0&&!/^Sender\s*\(untrusted/i.test(t)});if(e?.content){const t=stripTransportMetadata(e.content).trim(),i=t.slice(0,64).replace(/\n/g," ").trim();if(i){s.title=i,s.titleSource="truncated";const e=this.getGateway();e?.isConnected&&e.sessionsPatch(s.gatewaySessionKey,{label:i}).catch(()=>{}),this.transport.sendSessionUpdate(n,{sessionUpdate:"title_update",title:i}),this.generateTitleAsync(s,n,t)}}}for(let e=0;e<i.length;e++){const t=i[e];"user"!==t.role||"internal_system"!==t.provenance?.kind?"user"===t.role&&isHeartbeatPromptText(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] heartbeat cron prompt`):"assistant"===t.role&&isHeartbeatOnlyText(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] heartbeat assistant reply`):"system"!==t.role?(this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${t.role} contentLen=${t.content.length} content=${JSON.stringify(t.content.slice(0,80))} tcCount=${t.toolCalls?.length??0}`),"user"===t.role?this.replayUserTurnAsChunks(n,t.content,t.metadata):(await this.replayAssistantTurnAsStream(n,t),t.toolCalls?.length&&await this.replayUploadFileBlocks(n,t.toolCalls))):(this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${t.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:t.content},metadata:t.metadata})):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 o=new Set;for(const e of i)if(e.toolCalls)for(const t of e.toolCalls)t.id&&o.add(t.id),t.callId&&t.callId!==t.id&&o.add(t.callId);let r="",a="";for(let e=i.length-1;e>=0;e--)if("assistant"===i[e].role){r=i[e].content.slice(-200),a=(i[e].reasoning??"").slice(-200);break}if(void 0!==s.pendingPromptId&&!s.promptDone&&s.assistantMq&&!s.assistantMq.closed)try{this.replayCurrentAssistantStream(s,o,r,a)}catch(e){this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}(()=>{this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,s.gatewaySessionKey,s.platformAgentId),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})})()}async replayUploadFileBlocks(e,t){for(const s of t){if(s.name!==BEEOS_UPLOAD_TOOL_NAME)continue;let t=[];try{const e=await readUploadToolResult(s.id);e&&(t=buildToolResultSessionContentBlocks(s.name,e))}catch{}if(0===t.length){const e=extractBeeosFileUrisFromOutput(s.output);for(const s of e)t.push({type:"content",content:{type:"resource_link",uri:s,name:"file"}})}const n=t.filter(e=>"resource_link"===e.content?.type);for(const t of n)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:t.content});n.length>0&&this.logger.debug?.(`[bridge-core] replayUploadFileBlocks tcId=${s.id} sent ${n.length} resource_link block(s)`)}}replayCurrentAssistantStream(e,t,s="",n=""){if(!e.assistantMq||e.assistantMq.closed)return;const i=e.assistantMq.peek(0);if(0===i.length)return;let o=0,r=0,a="",l="";for(const d of i)if(t?.size&&d.toolCallId&&t.has(d.toolCallId)&&("tool_call"===d.sessionUpdate||"tool_call_update"===d.sessionUpdate))r++;else{if(s.length>0&&"agent_message_chunk"===d.sessionUpdate){const e=d.content?.text;if("string"==typeof e&&(a+=e,a.length<=s.length)){r++;continue}}if(n.length>0&&"agent_thought_chunk"===d.sessionUpdate){const e=d.content?.text;if("string"==typeof e&&(l+=e,l.length<=n.length)){r++;continue}}this.transport.sendSessionUpdate(e.sessionId,d),o++}r>0&&this.logger.debug?.(`[bridge-core] replayCurrentAssistantStream skipped ${r} duplicate tool events (already in history)`),o>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:o}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)isBackgroundSessionKey(e.gatewaySessionKey)||t.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(e.gatewaySessionKey)});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,n=s.sessionKey??s.key??s.id??s.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=t.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),r=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0,a=r&&!isPlaceholderTitle(r)?r:void 0,l=s.model,d=s.modelProvider;if(o){if(a&&isPlaceholderTitle(o.title)){o.title=a;const e=this.state.sessions.get(o.sessionId);e&&isPlaceholderTitle(e.title)&&(e.title=a)}const e=s.updatedAt;e&&(o.updatedAt=e),l&&(o.model=l),d&&(o.modelProvider=d)}else{const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s.cwd,title:a});this.registerSession(e),t.push({sessionId:e.sessionId,title:a||"",cwd:s.cwd,updatedAt:s.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(n),model:l,modelProvider:d})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const n=s.slice(0,64),i=this.state.sessions.get(t);if(!i)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);i.title=n,i.titleSource="manual";const o=i.gatewaySessionKey,r=this.getGateway();r?.isConnected&&r.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:t,title:n})}static TITLE_TEMPLATES=[{match:["今日日报","日报"],zh:"今日工作日报",en:"Daily Work Log"},{match:["周报","周报总结","本周周报"],zh:"本周工作周报",en:"Weekly Work Report"},{match:["月报","月度汇报","月度总结"],zh:"本月工作月报",en:"Monthly Work Report"},{match:["工作日志","日志"],zh:"工作日志整理",en:"Work Log Review"},{match:["晨会","今日晨会"],zh:"今日晨会纪要",en:"Daily Standup Notes"},{match:["复盘","今日复盘"],zh:"今日工作复盘",en:"Daily Retrospective"},{match:["新会话","new chat","new conversation"],zh:"新会话",en:"New Conversation"}];static TITLE_SYSTEM_PROMPT='You are a conversation title generator. Your only task is to generate a concise, human-readable title summarizing the user\'s first message.\n\n### Rules:\n- Length: 3-6 words in English, or 6-20 characters in Chinese (up to 24 if needed to keep all key domain terms intact).\n- Language: Match the user\'s input language exactly. Mixed zh/en input → use the dominant language.\n- Extract the core topic (noun-centric), not the action verb.\n- No punctuation, no quotation marks, no emojis, no markdown.\n- No prefix like "Title:" or "Here is". Output the title only.\n- Avoid generic words: "help", "question", "chat", "problem", "task".\n- If the message is too vague (e.g. "hi", "continue", "ok", "?"), return "新会话" (for zh) or "New Conversation" (for en).\n\n### Output Format:\nRespond ONLY with valid JSON, no other text:\n{"title": "<your title here>"}\n\n### Examples:\nInput: "帮我写一个关于BeeOS设备接入的PRD"\nOutput: {"title": "BeeOS 设备接入 PRD"}\n\nInput: "What\'s the best way to integrate x402 payment protocol?"\nOutput: {"title": "x402 Payment Integration"}\n\nInput: "hi"\nOutput: {"title": "新会话"}\n\nInput: "分析一下OKX CeDeFi PM这个岗位的机会"\nOutput: {"title": "OKX CeDeFi 岗位分析"}';matchTitleTemplate(e){const t=e.trim().toLowerCase();for(const s of AcpGatewayBridgeCore.TITLE_TEMPLATES)if(s.match.some(e=>e.toLowerCase()===t))return/[\u4e00-\u9fff]/.test(e)?s.zh:s.en}isGuardMessage(e){const t=e.trim();return[...t].length<3||[...t].every(e=>/[\p{P}\p{S}\s]/u.test(e))}generateTitleAsync(e,t,s){if(this.isGuardMessage(s))return void this.logger.info?.(`[bridge-core] title-gen-async SKIP guard message sid=${t}`);const n=this.matchTitleTemplate(s);if(n){e.title=n,e.titleSource="template";const s=this.getGateway();return s?.isConnected&&s.sessionsPatch(e.gatewaySessionKey,{label:n}).catch(()=>{}),void this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:n})}const i=s.length>2e3?s.slice(0,1500):s,o=(process.env.AROUTER_BASE_URL||"").replace(/\/$/,""),r=process.env.AROUTER_API_KEY||"";o&&r?(this.logger.info?.(`[bridge-core] title-gen-async CALLING LLM sid=${t} msg="${s.slice(0,60)}"`),fetch(`${o}/v1/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify({model:"openai/gpt-4o-mini",max_tokens:32,temperature:.3,messages:[{role:"system",content:AcpGatewayBridgeCore.TITLE_SYSTEM_PROMPT},{role:"user",content:i}]})}).then(async s=>{if(!s.ok){const e=await s.text().catch(()=>"");return void this.logger.info?.(`[bridge-core] title-gen-async FAILED sid=${t} status=${s.status} body=${e.slice(0,300)}`)}const n=await s.json(),i=n.choices?.[0]?.message?.content?.trim();if(!i)return;const o=i.replace(/^```json\s*/,"").replace(/```$/,"").trim();let r;try{r=JSON.parse(o).title?.trim()??""}catch{return void this.logger.debug?.(`[bridge-core] title gen JSON parse failed: ${o}`)}if(!r)return;if("manual"===e.titleSource)return void this.logger.debug?.("[bridge-core] title gen discarded: user already renamed");this.logger.info?.(`[bridge-core] title-gen-async OK sid=${t} title="${r}"`),e.title=r,e.titleSource="auto";const a=this.getGateway();a?.isConnected&&a.sessionsPatch(e.gatewaySessionKey,{label:r}).catch(()=>{}),this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:r})}).catch(e=>{this.logger.info?.(`[bridge-core] title-gen-async ERROR sid=${t}: ${e instanceof Error?e.message:e}`)})):this.logger.info?.(`[bridge-core] title-gen-async SKIP sid=${t}: no AROUTER env vars`)}async handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.info?.(`[bridge-core] session/delete sessionId=${t} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=s.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${t} gatewayKey=${n} gwConnected=${!!this.getGateway()?.isConnected}`);let i=!1;const o=this.getGateway();if(o?.isConnected&&n)try{await o.sessionsDelete(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${n}`)}catch(t){const s=t instanceof Error?t.message:String(t);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${s}`),!s.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await o.sessionsReset(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${n}`)}catch(t){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${t instanceof Error?t.message:t}`),void this.transport.sendResult(e.id,{deleted:!1})}}else n&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${n}`);void 0!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId);const r=new Set;for(const[e,s]of this.state.toolCallToSessionMap)s===t&&(this.state.toolCallToSessionMap.delete(e),r.add(e));for(const[e,t]of this.state.toolCallIdMap)r.has(t)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(n),this.transport.resetSessionUpdateCount(t),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${t} gatewayKey=${n} deleted=true`),this.transport.sendResult(e.id,{deleted:!0})}async handleSessionClear(e){this.logger.info?.(`[bridge-core] handleSessionClear called, params=${JSON.stringify(e.params)}`);const t=e.params?.sessionId;if(!t)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${t} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${t} gatewayKey=${s.gatewaySessionKey}`);const n=this.getGateway();if(n?.isConnected&&s.gatewaySessionKey)try{await n.sessionsReset(s.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${s.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${s.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n?.isConnected}, key=${s.gatewaySessionKey}`);s.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(t,s.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(n&&i?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(n.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();let o=s??null,r=null;if(n&&i?.isConnected&&s)try{const e=await i.sessionsPatch(n.gatewaySessionKey,{model:s}),t=e?.resolved;t?.model&&(o=t.model),t?.modelProvider&&(r=t.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:r})}async handleModelsList(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.request("models.list",{});this.transport.sendResult(e.id,s??{models:[]})}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Failed to list models: ${t instanceof Error?t.message:t}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();return t?.skills?t.skills.filter(e=>!1!==e.eligible||!0===e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0,disabled:e.disabled||void 0,skillKey:e.skillKey||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}})}getSkillsDir(){const e=process.env.OPENCLAW_HOME||join(homedir(),".openclaw");return join(e,"skills")}async handleSkillsInstall(e){const t=e.params,s=t?.zipUrl,n=t?.files;let i;try{i=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill slug"})}if(!n?.length&&!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skill files or zipUrl"});const o=resolveSkillInstallDir(this.getSkillsDir(),i);try{if(await mkdir(o,{recursive:!0}),s){const e=await fetchWithRetry(s,{maxRetries:4,logger:this.logger});await extractZipToDir(e,o)}else if(n?.length){const e=resolve(o);for(const t of n){const s=join(o,t.path);if(!resolve(s).startsWith(e+sep))throw new Error(`File path escapes skill directory: ${t.path}`);await mkdir(join(s,".."),{recursive:!0});const n=await fetch(t.downloadUrl,{signal:AbortSignal.timeout(12e4)});if(!n.ok)throw new Error(`Failed to download ${t.path}: ${n.status}`);const i=Buffer.from(await n.arrayBuffer());await writeFile(s,i)}}this.logger.info?.(`[bridge-core] Installed skill "${i}" via ${s?"zip":"files"}`),this.transport.sendResult(e.id,{success:!0}),this.onSkillInstalled?.(i,o),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to install skill "${i}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Install failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUninstall(e){const t=e.params;let s;try{s=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill name"})}const n=resolveSkillInstallDir(this.getSkillsDir(),s),i=resolveSkillInstallDir(join(homedir(),".agents","skills"),s);try{let t=!1;try{const e=await lstat(n);if(e.isSymbolicLink()){const e=await readlink(n);await rm(n,{force:!0}),await rm(e,{recursive:!0,force:!0}),t=!0}else e.isDirectory()&&(await rm(n,{recursive:!0,force:!0}),t=!0)}catch{}try{const e=await lstat(i);(e.isDirectory()||e.isSymbolicLink())&&(await rm(i,{recursive:!0,force:!0}),t=!0)}catch{}t||this.logger.warn?.(`[bridge-core] Skill "${s}" not found in any known location`),this.logger.info?.(`[bridge-core] Uninstalled skill "${s}"`),this.transport.sendResult(e.id,{success:!0}),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to uninstall skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Uninstall failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUpdate(e){const t=e.params,s=t?.skillKey,n=t?.enabled;if(!s||"boolean"!=typeof n)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skillKey or enabled"});const i=this.getGateway();if(i?.isConnected)try{const t=await i.skillsUpdate(s,n);this.logger.info?.(`[bridge-core] Updated skill "${s}" enabled=${n}`),this.transport.sendResult(e.id,{ok:t?.ok??!0,skillKey:s,enabled:n}),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to update skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Update failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.InternalError,{message:"Gateway not connected"})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:n,created:i}=await this.resolveOrCreateSession(t);i&&this.onSessionLoaded?.(n.sessionId,n.gatewaySessionKey,n.platformAgentId);const o=n.sessionId;this.state.lastActiveSessionId=o,this.onPromptStart?.();const r=this.getGateway();if(!r?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);await this.ensureVerboseFull(n);let a=extractPromptBlocks(sanitizePromptPayload(s));if(!n.title||isPlaceholderTitle(n.title)){const e=a.find(e=>"text"===e.type&&e.text);if(e&&"text"in e&&e.text){const t=stripTransportMetadata(e.text).trim();if(this.logger.info?.(`[bridge-core] title-gen sid=${o} existing="${n.title}" cleanText="${t.slice(0,80)}" isPlaceholder=${isPlaceholderTitle(t)}`),t&&!isPlaceholderTitle(t)){const e=t.slice(0,64).replace(/\n/g," ").trim();n.title=e,n.titleSource="truncated",r?.isConnected&&e&&r.sessionsPatch(n.gatewaySessionKey,{label:e}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendSessionUpdate(o,{sessionUpdate:"title_update",title:e}),this.generateTitleAsync(n,o,t)}}}const l=buildResolutionPlan(a);if(l.refs.length>0)try{const e=await resolveResources(l,this.writeObs);a=applyResolutions(a,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}let d;if(this.beeosFileResolver){const e=this.beeosFileResolver.buildResolutionPlan(a);if(e.refs.length>0)try{d=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(a,{beeosFileResolutionsByUri:d});if("error"in c)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:c.error});const{message:h,attachments:p}=c,g=resolveStandaloneSlashCommandName(a,h)??resolveSlashCmd(a,h),m="string"==typeof g;if(g&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(g)}catch(e){return t.logger.warn?.(`[bridge-core] /${g} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,a),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${g}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const u="chat.send";let f=m?h:withUserMessagePrefix(h);const y=this.config.canvasEnabled&&!m&&!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 t=e.resolveCanvasForSession(n.gatewaySessionKey);f=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${f}`}const t=n.referencedSurfaceId;t&&(f=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${f}`,n.referencedSurfaceId=void 0)}if(!m){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){n.activeMentions=t;f=`${buildMentionInstruction(t)}\n${f}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${t.length} agent(s)`)}else n.activeMentions=void 0}m&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${g}`),n.promptDone||void 0===n.pendingPromptId&&void 0===n.currentRunId||(this.logger.warn?.(`[bridge-core] handleSessionPrompt: force-ending stale run sid=${o} oldRunId=${n.currentRunId} oldPromptId=${n.pendingPromptId}`),(n.currentRunId||n.gatewayRequestId)&&r.agentCancel(n.gatewaySessionKey,{runId:n.currentRunId,requestId:n.gatewayRequestId}),this.endRun(n,{kind:"complete",stopReason:"cancelled"})),this.state.resetForNewRun(n,o,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=u,this.emitPromptAsUserSessionUpdates(o,a),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(n);const S=p.length>0?p.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0,w=`req_${randomUUID().replace(/-/g,"")}`,I={sessionKey:n.gatewaySessionKey,message:f,deliver:!1,idempotencyKey:randomUUID()};S?.length&&(I.attachments=S);if(!r.sendFrame({type:"req",id:w,method:u,params:I}))return this.state.clearPromptTimeout(n),n.pendingPromptId=void 0,n.assistantMq?.close(),void this.transport.sendAcpError(e.id,Errors.SendFailed,{message:"failed to send prompt to gateway"});n.gatewayRequestId=w,this.state.promptsByGatewayRequestId.set(w,n)}replayUserTurnAsChunks(e,t,s){const n=splitUserReplayBlocks(t);for(const t of n){let n;if("text"===t.type){if(!t.text)continue;n={type:"text",text:t.text}}else n="resource_link"===t.type?{type:"resource_link",uri:t.uri??"",...t.fileName?{name:t.fileName}:{},...t.mimeType?{mimeType:t.mimeType}:{}}:{type:t.type,...t.mimeType?{mimeType:t.mimeType}:{},...t.fileName?{fileName:t.fileName}:{}};this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:n,...s?{metadata:s}:{}})}}async replayAssistantTurnAsStream(e,t){if(!t.sourceMessages?.length){const s={sessionUpdate:"history_turn",role:t.role,content:t.content,reasoning:t.reasoning,toolCalls:t.toolCalls};if(t.metadata&&(s.metadata=t.metadata),this.transport.sendSessionUpdate(e,s),t.toolCalls?.length)for(const s of t.toolCalls){const t=await readEnrichmentCache(s.id).catch(()=>{});if(t?.length)for(const s of t)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}}})}if(t.files?.length)for(const s of t.files)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,...s.name?{name:s.name}:{},...s.mimeType?{mimeType:s.mimeType}:{}}});return}for(const s of t.sourceMessages){switch(s.updateType??"agent_message_chunk"){case"agent_message_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:s.content}});break;case"agent_thought_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_thought_chunk",content:{type:"text",text:s.content}});break;case"tool_call":{const n=s.toolCallId??"",i=t.toolCalls?.find(e=>e.id===n),o=s.toolName??"tool",r=s.content??"";let a={};if(r)try{a=JSON.parse(r)}catch{}this.transport.sendSessionUpdate(e,buildToolCallPayload({toolCallId:n,toolName:o,args:a,kind:i?.kind,status:i?.status??"in_progress"}));break}case"tool_call_update":{const n=s.toolCallId??"",i=t.toolCalls?.find(e=>e.id===n),o=s.toolName??i?.name??"tool",r=s.content??i?.output??"";let a=await readEnrichmentCache(n).catch(()=>{});if(!a){const e=extractBeeosFileUrisFromOutput(r);e.length>0&&(a=e.map(e=>({uri:e,name:"file"})))}this.transport.sendSessionUpdate(e,buildToolCallUpdatePayload({toolCallId:n,toolName:o,outputText:r,kind:i?.kind,status:i?.status??"completed",resourceLinks:a}));break}}if(s.files?.length)for(const t of s.files)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:t.uri,...t.name?{name:t.name}:{},...t.mimeType?{mimeType:t.mimeType}:{}}})}if(t.files?.length){const s=new Set(t.sourceMessages.flatMap(e=>e.files?.map(e=>e.uri)??[]));for(const n of t.files)s.has(n.uri)||this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:n.uri,...n.name?{name:n.name}:{},...n.mimeType?{mimeType:n.mimeType}:{}}})}}emitPromptAsUserSessionUpdates(e,t){for(const s of t){const t=this.toUserSessionUpdateContent(s);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,n=t.mimeType??t.mime_type??e.mimeType??e.mime_type,i=t.text??e.text,o=t.data??e.data,r=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!i&&!o)return;return{type:"resource",resource:{...s?{uri:s}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...r?{fileName:r}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(s);if(!n)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway(),o=i?.isConnected,r=!(!n.currentRunId&&!n.gatewayRequestId);if(o&&r){i.agentCancel(n.gatewaySessionKey,{runId:n.currentRunId,requestId:n.gatewayRequestId})}this.endRun(n,{kind:"complete",stopReason:"cancelled"})||this.transport.sendSessionUpdate(n.sessionId,{sessionUpdate:"stream_complete",stopReason:"cancelled"}),void 0!==e&&this.transport.sendResult(e,{})}endRun(e,t){if(e.promptDone)return this.logger.debug?.(`[bridge-core] endRun skipped (promptDone=true) sessionId=${e.sessionId} outcome=${t.kind}`),!1;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return this.logger.debug?.(`[bridge-core] endRun skipped (no pendingPromptId and no currentRunId) sessionId=${e.sessionId} outcome=${t.kind}`),!1;e.promptDone=!0;const s=e.currentRunId;if("complete"===t.kind){if(s&&this.onRunComplete){const t=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(s,t)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t.stopReason}),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:t.stopReason})}else s&&this.onRunFailed&&this.onRunFailed(s,t.message),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,t.code,t.message),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:"error",error:t.message});return this.cleanupPrompt(e),this.onRunEnded?.(e,t),!0}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t),this.state.markRunCompleted(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.gatewayRequestId&&this.state.promptsByGatewayRequestId.delete(e.gatewayRequestId),e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close()}completePrompt(e,t){this.endRun(e,{kind:"complete",stopReason:t})}failPrompt(e,t,s){this.endRun(e,{kind:"error",code:t,message:s})}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}handleGatewayResponse(e){const t=e.id;if(!t)return;const s=this.state.promptsByGatewayRequestId.get(t);if(!s||s.promptDone)return;if(!e.ok){const n=e.error,i=n?.message??"gateway returned error";return this.logger.warn?.(`[bridge-core] handleGatewayResponse: gateway rejected prompt gatewayRequestId=${t} error=${i}`),void this.failPrompt(s,Errors.SendFailed.rpcCode,i)}const n=e.payload,i=n?.runId;i?(s.currentRunId=i,s.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(i,s),this.logger.debug?.(`[bridge-core] handleGatewayResponse: bound runId=${i} gatewayRequestId=${t} sessionId=${s.sessionId}`)):this.logger.debug?.(`[bridge-core] handleGatewayResponse: no runId in payload gatewayRequestId=${t} sessionId=${s.sessionId}`)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}pushToolResultContent(e,t,s,n){if(0===s.length)return;const i=n?this.state.promptsByRunId.get(n):void 0;if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session for runId=${n??"(none)"}`);const o=i.sessionId;for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,t),i.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:o}})}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/",".");try{const n=await t.request(s,e.params??{});this.transport.sendResult(e.id,n??{})}catch(t){const n=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}}async function fetchWithRetry(e,t={}){const s=t.maxRetries??3;let n;for(let i=0;i<=s;i++){const o=await fetch(e,{signal:AbortSignal.timeout(12e4),redirect:"follow"});if(o.ok)return Buffer.from(await o.arrayBuffer());if(429===o.status||o.status>=500){const r=Number(o.headers.get("retry-after"))||0,a=1e3*Math.max(r,2**i);if(n=new Error(`HTTP ${o.status} from ${e}`),i<s){t.logger?.info?.(`[bridge-core] Download ${o.status}, retry ${i+1}/${s} in ${(a/1e3).toFixed(0)}s`),await new Promise(e=>setTimeout(e,a));continue}}throw new Error(`Failed to download zip: ${o.status}`)}throw n??new Error("Download failed after retries")}const SKILL_ROOT_MARKERS=["SKILL.md","skill.md","SKILL.MD"];async function resolveSkillRoot(e){for(const t of SKILL_ROOT_MARKERS)try{return await stat(join(e,t)),e}catch{}const t=(await readdir(e,{withFileTypes:!0})).filter(e=>e.isDirectory()&&!e.name.startsWith(".")&&"__MACOSX"!==e.name);if(1===t.length){const s=join(e,t[0].name);for(const e of SKILL_ROOT_MARKERS)try{return await stat(join(s,e)),s}catch{}return s}return e}async function extractZipToDir(e,t){const s=new AdmZip(e),n=join(t,".beeos-extract-tmp-"+Date.now());try{await mkdir(n,{recursive:!0}),s.extractAllTo(n,!0);const e=await resolveSkillRoot(n),i=await readdir(e,{withFileTypes:!0});let o=0;for(const s of i){if(s.name.startsWith(".beeos-extract-tmp")||"__MACOSX"===s.name)continue;const n=join(e,s.name),i=join(t,s.name);await rename(n,i).catch(async()=>{await cp(n,i,{recursive:!0})}),o++}if(0===o)throw new Error("Zip archive contained no extractable files")}finally{await rm(n,{recursive:!0,force:!0}).catch(()=>{})}}const VALID_SLUG_PATTERN=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,NON_ASCII_PATTERN=/[^\x00-\x7F]/;function validateSkillSlug(e){const t=e.trim();if(!t||t.includes("/")||t.includes("\\")||t.includes(".."))throw new Error(`Invalid skill slug: ${e}`);if(NON_ASCII_PATTERN.test(t)||!VALID_SLUG_PATTERN.test(t))throw new Error(`Invalid skill slug: ${e}`);return t}function safeDirName(e){const t=e.trim();return t?t.replaceAll("/","__").replaceAll("\\","__"):t}function resolveSkillInstallDir(e,t){const s=safeDirName(t),n=join(e,s),i=resolve(e),o=resolve(n),r=relative(i,o);if(!r||".."===r||r.startsWith(`..${sep}`)||path.isAbsolute(r))throw new Error("Invalid skill target path");return n}
1
+ import{randomUUID,createHash}from"node:crypto";import{mkdir,writeFile,rm,lstat,readlink,stat,readdir,rename,cp}from"node:fs/promises";import path from"node:path";import{join,resolve,relative,sep}from"node:path";import{homedir}from"node:os";import AdmZip from"adm-zip";import{sanitizePromptPayload,stripTransportMetadata,isHeartbeatPromptText,isHeartbeatOnlyText}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,readEnrichmentCache}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.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}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-",":a2a:"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function isSystemDefaultSessionKey(e){const t=e.split(":");if(t.length<3||"agent"!==t[0])return!1;const s=t.slice(2).join(":");return"main"===s||s===t[1]}function isPlaceholderTitle(e){return!e||"New chat"===e||(!!/^[0-9a-f]{6,12}\s*\(20\d{2}/i.test(e)||(!!/^Sender\s*\(untrusted/i.test(e)||!!/^Conversation\s+info\s*\(untrusted/i.test(e)))}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const n=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,t){const s=createHash("sha256").update(`${t}:${e}`).digest("hex").substring(0,32);return`session:${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20,32)}`}function parseAgentIdFromKey(e){const t=e.split(":");return(t.length>=3&&"agent"===t[0]?t[1]:"")||"main"}function createSessionObject(e){const t=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:t,platformAgentId:e.resolvePlatformAgentId?.(t),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 t of this.state.sessions.values())!t.platformAgentId&&t.agentId&&(t.platformAgentId=e.get(t.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[];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(t=>{this._syncResolvers.push(t);const s=setTimeout(()=>{const e=this._syncResolvers.indexOf(t);-1!==e&&this._syncResolvers.splice(e,1),t()},e);"object"==typeof s&&"unref"in s&&s.unref()})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;onRunEnded=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),this.ensureVerboseFull(e)}async ensureVerboseFull(e){if(e.verbosePatched)return;const t=this.getGateway();if(t?.isConnected){e.verbosePatched=!0;try{await t.sessionsPatch(e.gatewaySessionKey,{verboseLevel:"full"})}catch(t){e.verbosePatched=!1,this.logger.debug?.(`[bridge-core] ensureVerboseFull failed: ${t instanceof Error?t.message:t}`)}}}ensureSessionForKey(e){const t=this.state.findSessionByKey(e);if(t)return t;const s=createSessionObject({gatewayKey:e,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(s),this.logger.debug?.(`[bridge-core] ensureSessionForKey: lazily created session sid=${s.sessionId} key=${e}`),s}async preloadSessionsFromGateway(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.sessionsList();if(!t||"object"!=typeof t)return;const s=t.sessions;if(!Array.isArray(s))return;let n=0;for(const e of s){const t=e,s=t.sessionKey??t.key??t.id??t.name;if(!s||"string"!=typeof s)continue;if(this.state.findSessionByKey(s))continue;const i=t.label||t.derivedTitle||t.title||void 0,o=i&&!isPlaceholderTitle(i)?i:void 0,r=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:o});this.registerSession(r),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"skills/install":await this.handleSkillsInstall(e);break;case"skills/uninstall":await this.handleSkillsUninstall(e);break;case"skills/update":await this.handleSkillsUpdate(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(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSkillInstalled=null;onSessionDeleted=null;onSessionCleared=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/toggle":case"canvas/clear":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const t=e.params,s=t?.surfaceId,n=this.state.resolveSessionFromParams(t);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}n.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,s="string"==typeof e.params?.cwd?e.params.cwd:".",n=createSessionObject({gatewayKey:t,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s});this.registerSession(n),this.transport.sendResult(e.id,{sessionId:n.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:t,agentId:n.agentId,platformAgentId:n.platformAgentId??null}}),this.fetchHistoryInBackground(t)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async recoverSessionFromGateway(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.sessionsList();if(!s||"object"!=typeof s)return;const n=s.sessions;if(!Array.isArray(n))return;for(const t of n){const s=t,n=s.sessionKey??s.key??s.id??s.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const e=s.label||s.derivedTitle||s.title||void 0,t=e&&!isPlaceholderTitle(e)?e:void 0,i=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:t});return this.registerSession(i),i}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let t=this.state.sessions.get(e);if(!t&&e.startsWith("agent:")&&(t=this.state.findSessionByKey(e)),t||e.startsWith("agent:")||(t=await this.recoverSessionFromGateway(e)),t)return{session:t,created:!1};const s=e.startsWith("agent:"),n=this.config.gateway.agentId||this.getDefaultAgentId();return t=createSessionObject({gatewayKey:s?e:`agent:${n}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(t),{session:t,created:!0}}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const{session:s}=await this.resolveOrCreateSession(t),n=s.sessionId;this.state.lastActiveSessionId=n,s.chatReplayDedupKeys.clear(),this.transport.resetSeq(n);let i=[];try{const e=this.getGateway(),t=await loadHistory(e,s.gatewaySessionKey,50);i=groupIntoTurns(t);const o=i.filter(e=>"user"===e.role).length,r=i.filter(e=>"assistant"===e.role).length,a=i.filter(e=>e.toolCalls?.length).length,l=i.reduce((e,t)=>e+(t.toolCalls?.length??0),0);if(this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${t.length} turns=${i.length} userTurns=${o} asstTurns=${r} turnsWithToolCalls=${a} totalToolCalls=${l}`),i.length>0&&isPlaceholderTitle(s.title)&&"manual"!==s.titleSource){const e=i.find(e=>{if("user"!==e.role)return!1;if("internal_system"===e.provenance?.kind)return!1;const t=stripTransportMetadata(e.content).trim();return t.length>0&&!/^Sender\s*\(untrusted/i.test(t)});if(e?.content){const t=stripTransportMetadata(e.content).trim(),i=t.slice(0,64).replace(/\n/g," ").trim();if(i){s.title=i,s.titleSource="truncated";const e=this.getGateway();e?.isConnected&&e.sessionsPatch(s.gatewaySessionKey,{label:i}).catch(()=>{}),this.transport.sendSessionUpdate(n,{sessionUpdate:"title_update",title:i}),this.generateTitleAsync(s,n,t)}}}for(let e=0;e<i.length;e++){const t=i[e];"user"!==t.role||"internal_system"!==t.provenance?.kind?"user"===t.role&&isHeartbeatPromptText(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] heartbeat cron prompt`):"assistant"===t.role&&isHeartbeatOnlyText(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] heartbeat assistant reply`):"system"!==t.role?(this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${t.role} contentLen=${t.content.length} content=${JSON.stringify(t.content.slice(0,80))} tcCount=${t.toolCalls?.length??0}`),"user"===t.role?this.replayUserTurnAsChunks(n,t.content,t.metadata):(await this.replayAssistantTurnAsStream(n,t),t.toolCalls?.length&&await this.replayUploadFileBlocks(n,t.toolCalls))):(this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${t.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:t.content},metadata:t.metadata})):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 o=new Set;for(const e of i)if(e.toolCalls)for(const t of e.toolCalls)t.id&&o.add(t.id),t.callId&&t.callId!==t.id&&o.add(t.callId);let r="",a="";for(let e=i.length-1;e>=0;e--)if("assistant"===i[e].role){r=i[e].content.slice(-200),a=(i[e].reasoning??"").slice(-200);break}if(void 0!==s.pendingPromptId&&!s.promptDone&&s.assistantMq&&!s.assistantMq.closed)try{this.replayCurrentAssistantStream(s,o,r,a)}catch(e){this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}(()=>{this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,s.gatewaySessionKey,s.platformAgentId),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})})()}async replayUploadFileBlocks(e,t){for(const s of t){if(s.name!==BEEOS_UPLOAD_TOOL_NAME)continue;let t=[];try{const e=await readUploadToolResult(s.id);e&&(t=buildToolResultSessionContentBlocks(s.name,e))}catch{}if(0===t.length){const e=extractBeeosFileUrisFromOutput(s.output);for(const s of e)t.push({type:"content",content:{type:"resource_link",uri:s,name:"file"}})}const n=t.filter(e=>"resource_link"===e.content?.type);for(const t of n)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:t.content});n.length>0&&this.logger.debug?.(`[bridge-core] replayUploadFileBlocks tcId=${s.id} sent ${n.length} resource_link block(s)`)}}replayCurrentAssistantStream(e,t,s="",n=""){if(!e.assistantMq||e.assistantMq.closed)return;const i=e.assistantMq.peek(0);if(0===i.length)return;let o=0,r=0,a="",l="";for(const d of i)if(t?.size&&d.toolCallId&&t.has(d.toolCallId)&&("tool_call"===d.sessionUpdate||"tool_call_update"===d.sessionUpdate))r++;else{if(s.length>0&&"agent_message_chunk"===d.sessionUpdate){const e=d.content?.text;if("string"==typeof e&&(a+=e,a.length<=s.length)){r++;continue}}if(n.length>0&&"agent_thought_chunk"===d.sessionUpdate){const e=d.content?.text;if("string"==typeof e&&(l+=e,l.length<=n.length)){r++;continue}}this.transport.sendSessionUpdate(e.sessionId,d),o++}r>0&&this.logger.debug?.(`[bridge-core] replayCurrentAssistantStream skipped ${r} duplicate tool events (already in history)`),o>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:o}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)isBackgroundSessionKey(e.gatewaySessionKey)||t.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(e.gatewaySessionKey)});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,n=s.sessionKey??s.key??s.id??s.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=t.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),r=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0,a=r&&!isPlaceholderTitle(r)?r:void 0,l=s.model,d=s.modelProvider;if(o){if(a&&isPlaceholderTitle(o.title)){o.title=a;const e=this.state.sessions.get(o.sessionId);e&&isPlaceholderTitle(e.title)&&(e.title=a)}const e=s.updatedAt;e&&(o.updatedAt=e),l&&(o.model=l),d&&(o.modelProvider=d)}else{const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s.cwd,title:a});this.registerSession(e),t.push({sessionId:e.sessionId,title:a||"",cwd:s.cwd,updatedAt:s.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(n),model:l,modelProvider:d})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const n=s.slice(0,64),i=this.state.sessions.get(t);if(!i)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);i.title=n,i.titleSource="manual";const o=i.gatewaySessionKey,r=this.getGateway();r?.isConnected&&r.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:t,title:n})}static TITLE_TEMPLATES=[{match:["今日日报","日报"],zh:"今日工作日报",en:"Daily Work Log"},{match:["周报","周报总结","本周周报"],zh:"本周工作周报",en:"Weekly Work Report"},{match:["月报","月度汇报","月度总结"],zh:"本月工作月报",en:"Monthly Work Report"},{match:["工作日志","日志"],zh:"工作日志整理",en:"Work Log Review"},{match:["晨会","今日晨会"],zh:"今日晨会纪要",en:"Daily Standup Notes"},{match:["复盘","今日复盘"],zh:"今日工作复盘",en:"Daily Retrospective"},{match:["新会话","new chat","new conversation"],zh:"新会话",en:"New Conversation"}];static TITLE_SYSTEM_PROMPT='You are a conversation title generator. Your only task is to generate a concise, human-readable title summarizing the user\'s first message.\n\n### Rules:\n- Length: 3-6 words in English, or 6-20 characters in Chinese (up to 24 if needed to keep all key domain terms intact).\n- Language: Match the user\'s input language exactly. Mixed zh/en input → use the dominant language.\n- Extract the core topic (noun-centric), not the action verb.\n- No punctuation, no quotation marks, no emojis, no markdown.\n- No prefix like "Title:" or "Here is". Output the title only.\n- Avoid generic words: "help", "question", "chat", "problem", "task".\n- If the message is too vague (e.g. "hi", "continue", "ok", "?"), return "新会话" (for zh) or "New Conversation" (for en).\n\n### Output Format:\nRespond ONLY with valid JSON, no other text:\n{"title": "<your title here>"}\n\n### Examples:\nInput: "帮我写一个关于BeeOS设备接入的PRD"\nOutput: {"title": "BeeOS 设备接入 PRD"}\n\nInput: "What\'s the best way to integrate x402 payment protocol?"\nOutput: {"title": "x402 Payment Integration"}\n\nInput: "hi"\nOutput: {"title": "新会话"}\n\nInput: "分析一下OKX CeDeFi PM这个岗位的机会"\nOutput: {"title": "OKX CeDeFi 岗位分析"}';matchTitleTemplate(e){const t=e.trim().toLowerCase();for(const s of AcpGatewayBridgeCore.TITLE_TEMPLATES)if(s.match.some(e=>e.toLowerCase()===t))return/[\u4e00-\u9fff]/.test(e)?s.zh:s.en}isGuardMessage(e){const t=e.trim();return[...t].length<3||[...t].every(e=>/[\p{P}\p{S}\s]/u.test(e))}generateTitleAsync(e,t,s){if(this.isGuardMessage(s))return void this.logger.info?.(`[bridge-core] title-gen-async SKIP guard message sid=${t}`);const n=this.matchTitleTemplate(s);if(n){e.title=n,e.titleSource="template";const s=this.getGateway();return s?.isConnected&&s.sessionsPatch(e.gatewaySessionKey,{label:n}).catch(()=>{}),void this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:n})}const i=s.length>2e3?s.slice(0,1500):s,o=(process.env.AROUTER_BASE_URL||"").replace(/\/$/,""),r=process.env.AROUTER_API_KEY||"";o&&r?(this.logger.info?.(`[bridge-core] title-gen-async CALLING LLM sid=${t} msg="${s.slice(0,60)}"`),fetch(`${o}/v1/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify({model:"openai/gpt-4o-mini",max_tokens:32,temperature:.3,messages:[{role:"system",content:AcpGatewayBridgeCore.TITLE_SYSTEM_PROMPT},{role:"user",content:i}]})}).then(async s=>{if(!s.ok){const e=await s.text().catch(()=>"");return void this.logger.info?.(`[bridge-core] title-gen-async FAILED sid=${t} status=${s.status} body=${e.slice(0,300)}`)}const n=await s.json(),i=n.choices?.[0]?.message?.content?.trim();if(!i)return;const o=i.replace(/^```json\s*/,"").replace(/```$/,"").trim();let r;try{r=JSON.parse(o).title?.trim()??""}catch{return void this.logger.debug?.(`[bridge-core] title gen JSON parse failed: ${o}`)}if(!r)return;if("manual"===e.titleSource)return void this.logger.debug?.("[bridge-core] title gen discarded: user already renamed");this.logger.info?.(`[bridge-core] title-gen-async OK sid=${t} title="${r}"`),e.title=r,e.titleSource="auto";const a=this.getGateway();a?.isConnected&&a.sessionsPatch(e.gatewaySessionKey,{label:r}).catch(()=>{}),this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:r})}).catch(e=>{this.logger.info?.(`[bridge-core] title-gen-async ERROR sid=${t}: ${e instanceof Error?e.message:e}`)})):this.logger.info?.(`[bridge-core] title-gen-async SKIP sid=${t}: no AROUTER env vars`)}async handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.info?.(`[bridge-core] session/delete sessionId=${t} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=s.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${t} gatewayKey=${n} gwConnected=${!!this.getGateway()?.isConnected}`);let i=!1;const o=this.getGateway();if(o?.isConnected&&n)try{await o.sessionsDelete(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${n}`)}catch(t){const s=t instanceof Error?t.message:String(t);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${s}`),!s.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await o.sessionsReset(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${n}`)}catch(t){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${t instanceof Error?t.message:t}`),void this.transport.sendResult(e.id,{deleted:!1})}}else n&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${n}`);void 0!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId);const r=new Set;for(const[e,s]of this.state.toolCallToSessionMap)s===t&&(this.state.toolCallToSessionMap.delete(e),r.add(e));for(const[e,t]of this.state.toolCallIdMap)r.has(t)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(n),this.transport.resetSessionUpdateCount(t),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${t} gatewayKey=${n} deleted=true`),this.transport.sendResult(e.id,{deleted:!0})}async handleSessionClear(e){this.logger.info?.(`[bridge-core] handleSessionClear called, params=${JSON.stringify(e.params)}`);const t=e.params?.sessionId;if(!t)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${t} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${t} gatewayKey=${s.gatewaySessionKey}`);const n=this.getGateway(),i=n?.isConnected;if(s.promptDone||void 0===s.pendingPromptId&&void 0===s.currentRunId||(this.logger.info?.(`[bridge-core] session/clear: cancelling active run sid=${t} runId=${s.currentRunId}`),i&&(s.currentRunId||s.gatewayRequestId)&&n.agentCancel(s.gatewaySessionKey,{runId:s.currentRunId,requestId:s.gatewayRequestId}),this.endRun(s,{kind:"complete",stopReason:"cancelled"})),i&&s.gatewaySessionKey)try{await n.sessionsReset(s.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${s.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${s.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${i}, key=${s.gatewaySessionKey}`);s.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(t,s.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(n&&i?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(n.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();let o=s??null,r=null;if(n&&i?.isConnected&&s)try{const e=await i.sessionsPatch(n.gatewaySessionKey,{model:s}),t=e?.resolved;t?.model&&(o=t.model),t?.modelProvider&&(r=t.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:r})}async handleModelsList(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.request("models.list",{});this.transport.sendResult(e.id,s??{models:[]})}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Failed to list models: ${t instanceof Error?t.message:t}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();return t?.skills?t.skills.filter(e=>!1!==e.eligible||!0===e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0,disabled:e.disabled||void 0,skillKey:e.skillKey||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}})}getSkillsDir(){const e=process.env.OPENCLAW_HOME||join(homedir(),".openclaw");return join(e,"skills")}async handleSkillsInstall(e){const t=e.params,s=t?.zipUrl,n=t?.files;let i;try{i=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill slug"})}if(!n?.length&&!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skill files or zipUrl"});const o=resolveSkillInstallDir(this.getSkillsDir(),i);try{if(await mkdir(o,{recursive:!0}),s){const e=await fetchWithRetry(s,{maxRetries:4,logger:this.logger});await extractZipToDir(e,o)}else if(n?.length){const e=resolve(o);for(const t of n){const s=join(o,t.path);if(!resolve(s).startsWith(e+sep))throw new Error(`File path escapes skill directory: ${t.path}`);await mkdir(join(s,".."),{recursive:!0});const n=await fetch(t.downloadUrl,{signal:AbortSignal.timeout(12e4)});if(!n.ok)throw new Error(`Failed to download ${t.path}: ${n.status}`);const i=Buffer.from(await n.arrayBuffer());await writeFile(s,i)}}this.logger.info?.(`[bridge-core] Installed skill "${i}" via ${s?"zip":"files"}`),this.transport.sendResult(e.id,{success:!0}),this.onSkillInstalled?.(i,o),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to install skill "${i}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Install failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUninstall(e){const t=e.params;let s;try{s=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill name"})}const n=resolveSkillInstallDir(this.getSkillsDir(),s),i=resolveSkillInstallDir(join(homedir(),".agents","skills"),s);try{let t=!1;try{const e=await lstat(n);if(e.isSymbolicLink()){const e=await readlink(n);await rm(n,{force:!0}),await rm(e,{recursive:!0,force:!0}),t=!0}else e.isDirectory()&&(await rm(n,{recursive:!0,force:!0}),t=!0)}catch{}try{const e=await lstat(i);(e.isDirectory()||e.isSymbolicLink())&&(await rm(i,{recursive:!0,force:!0}),t=!0)}catch{}t||this.logger.warn?.(`[bridge-core] Skill "${s}" not found in any known location`),this.logger.info?.(`[bridge-core] Uninstalled skill "${s}"`),this.transport.sendResult(e.id,{success:!0}),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to uninstall skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Uninstall failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUpdate(e){const t=e.params,s=t?.skillKey,n=t?.enabled;if(!s||"boolean"!=typeof n)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skillKey or enabled"});const i=this.getGateway();if(i?.isConnected)try{const t=await i.skillsUpdate(s,n);this.logger.info?.(`[bridge-core] Updated skill "${s}" enabled=${n}`),this.transport.sendResult(e.id,{ok:t?.ok??!0,skillKey:s,enabled:n}),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to update skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Update failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.InternalError,{message:"Gateway not connected"})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:n,created:i}=await this.resolveOrCreateSession(t);i&&this.onSessionLoaded?.(n.sessionId,n.gatewaySessionKey,n.platformAgentId);const o=n.sessionId;this.state.lastActiveSessionId=o,this.onPromptStart?.();const r=this.getGateway();if(!r?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);await this.ensureVerboseFull(n);let a=extractPromptBlocks(sanitizePromptPayload(s));if(!n.title||isPlaceholderTitle(n.title)){const e=a.find(e=>"text"===e.type&&e.text);if(e&&"text"in e&&e.text){const t=stripTransportMetadata(e.text).trim();if(this.logger.info?.(`[bridge-core] title-gen sid=${o} existing="${n.title}" cleanText="${t.slice(0,80)}" isPlaceholder=${isPlaceholderTitle(t)}`),t&&!isPlaceholderTitle(t)){const e=t.slice(0,64).replace(/\n/g," ").trim();n.title=e,n.titleSource="truncated",r?.isConnected&&e&&r.sessionsPatch(n.gatewaySessionKey,{label:e}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendSessionUpdate(o,{sessionUpdate:"title_update",title:e}),this.generateTitleAsync(n,o,t)}}}const l=buildResolutionPlan(a);if(l.refs.length>0)try{const e=await resolveResources(l,this.writeObs);a=applyResolutions(a,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}let d;if(this.beeosFileResolver){const e=this.beeosFileResolver.buildResolutionPlan(a);if(e.refs.length>0)try{d=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(a,{beeosFileResolutionsByUri:d});if("error"in c)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:c.error});const{message:h,attachments:p}=c,g=resolveStandaloneSlashCommandName(a,h)??resolveSlashCmd(a,h),m="string"==typeof g;if(g&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(g)}catch(e){return t.logger.warn?.(`[bridge-core] /${g} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,a),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${g}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const u="chat.send";let f=m?h:withUserMessagePrefix(h);const y=this.config.canvasEnabled&&!m&&!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 t=e.resolveCanvasForSession(n.gatewaySessionKey);f=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${f}`}const t=n.referencedSurfaceId;t&&(f=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${f}`,n.referencedSurfaceId=void 0)}if(!m){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){n.activeMentions=t;f=`${buildMentionInstruction(t)}\n${f}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${t.length} agent(s)`)}else n.activeMentions=void 0}m&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${g}`),n.promptDone||void 0===n.pendingPromptId&&void 0===n.currentRunId||(this.logger.warn?.(`[bridge-core] handleSessionPrompt: force-ending stale run sid=${o} oldRunId=${n.currentRunId} oldPromptId=${n.pendingPromptId}`),(n.currentRunId||n.gatewayRequestId)&&r.agentCancel(n.gatewaySessionKey,{runId:n.currentRunId,requestId:n.gatewayRequestId}),this.endRun(n,{kind:"complete",stopReason:"cancelled"})),this.state.resetForNewRun(n,o,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=u,this.emitPromptAsUserSessionUpdates(o,a),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(n);const S=p.length>0?p.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0,w=`req_${randomUUID().replace(/-/g,"")}`,I={sessionKey:n.gatewaySessionKey,message:f,deliver:!1,idempotencyKey:randomUUID()};S?.length&&(I.attachments=S);if(!r.sendFrame({type:"req",id:w,method:u,params:I}))return this.state.clearPromptTimeout(n),n.pendingPromptId=void 0,n.assistantMq?.close(),void this.transport.sendAcpError(e.id,Errors.SendFailed,{message:"failed to send prompt to gateway"});n.gatewayRequestId=w,this.state.promptsByGatewayRequestId.set(w,n)}replayUserTurnAsChunks(e,t,s){const n=splitUserReplayBlocks(t);for(const t of n){let n;if("text"===t.type){if(!t.text)continue;n={type:"text",text:t.text}}else n="resource_link"===t.type?{type:"resource_link",uri:t.uri??"",...t.fileName?{name:t.fileName}:{},...t.mimeType?{mimeType:t.mimeType}:{}}:{type:t.type,...t.mimeType?{mimeType:t.mimeType}:{},...t.fileName?{fileName:t.fileName}:{}};this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:n,...s?{metadata:s}:{}})}}async replayAssistantTurnAsStream(e,t){if(!t.sourceMessages?.length){const s={sessionUpdate:"history_turn",role:t.role,content:t.content,reasoning:t.reasoning,toolCalls:t.toolCalls};if(t.metadata&&(s.metadata=t.metadata),this.transport.sendSessionUpdate(e,s),t.toolCalls?.length)for(const s of t.toolCalls){const t=await readEnrichmentCache(s.id).catch(()=>{});if(t?.length)for(const s of t)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}}})}if(t.files?.length)for(const s of t.files)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,...s.name?{name:s.name}:{},...s.mimeType?{mimeType:s.mimeType}:{}}});return}for(const s of t.sourceMessages){switch(s.updateType??"agent_message_chunk"){case"agent_message_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:s.content}});break;case"agent_thought_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_thought_chunk",content:{type:"text",text:s.content}});break;case"tool_call":{const n=s.toolCallId??"",i=t.toolCalls?.find(e=>e.id===n),o=s.toolName??"tool",r=s.content??"";let a={};if(r)try{a=JSON.parse(r)}catch{}this.transport.sendSessionUpdate(e,buildToolCallPayload({toolCallId:n,toolName:o,args:a,kind:i?.kind,status:i?.status??"in_progress"}));break}case"tool_call_update":{const n=s.toolCallId??"",i=t.toolCalls?.find(e=>e.id===n),o=s.toolName??i?.name??"tool",r=s.content??i?.output??"";let a=await readEnrichmentCache(n).catch(()=>{});if(!a){const e=extractBeeosFileUrisFromOutput(r);e.length>0&&(a=e.map(e=>({uri:e,name:"file"})))}this.transport.sendSessionUpdate(e,buildToolCallUpdatePayload({toolCallId:n,toolName:o,outputText:r,kind:i?.kind,status:i?.status??"completed",resourceLinks:a}));break}}if(s.files?.length)for(const t of s.files)this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:t.uri,...t.name?{name:t.name}:{},...t.mimeType?{mimeType:t.mimeType}:{}}})}if(t.files?.length){const s=new Set(t.sourceMessages.flatMap(e=>e.files?.map(e=>e.uri)??[]));for(const n of t.files)s.has(n.uri)||this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:n.uri,...n.name?{name:n.name}:{},...n.mimeType?{mimeType:n.mimeType}:{}}})}}emitPromptAsUserSessionUpdates(e,t){for(const s of t){const t=this.toUserSessionUpdateContent(s);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,n=t.mimeType??t.mime_type??e.mimeType??e.mime_type,i=t.text??e.text,o=t.data??e.data,r=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!i&&!o)return;return{type:"resource",resource:{...s?{uri:s}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...r?{fileName:r}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(s);if(!n)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway(),o=i?.isConnected,r=!(!n.currentRunId&&!n.gatewayRequestId);if(o&&r){i.agentCancel(n.gatewaySessionKey,{runId:n.currentRunId,requestId:n.gatewayRequestId})}this.endRun(n,{kind:"complete",stopReason:"cancelled"})||this.transport.sendSessionUpdate(n.sessionId,{sessionUpdate:"stream_complete",stopReason:"cancelled"}),void 0!==e&&this.transport.sendResult(e,{})}endRun(e,t){if(e.promptDone)return this.logger.debug?.(`[bridge-core] endRun skipped (promptDone=true) sessionId=${e.sessionId} outcome=${t.kind}`),!1;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return this.logger.debug?.(`[bridge-core] endRun skipped (no pendingPromptId and no currentRunId) sessionId=${e.sessionId} outcome=${t.kind}`),!1;e.promptDone=!0;const s=e.currentRunId;if("complete"===t.kind){if(s&&this.onRunComplete){const t=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(s,t)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t.stopReason}),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:t.stopReason})}else s&&this.onRunFailed&&this.onRunFailed(s,t.message),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,t.code,t.message),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:"error",error:t.message});return this.cleanupPrompt(e),this.onRunEnded?.(e,t),!0}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t),this.state.markRunCompleted(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.gatewayRequestId&&this.state.promptsByGatewayRequestId.delete(e.gatewayRequestId),e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close()}completePrompt(e,t){this.endRun(e,{kind:"complete",stopReason:t})}failPrompt(e,t,s){this.endRun(e,{kind:"error",code:t,message:s})}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}handleGatewayResponse(e){const t=e.id;if(!t)return;const s=this.state.promptsByGatewayRequestId.get(t);if(!s||s.promptDone)return;if(!e.ok){const n=e.error,i=n?.message??"gateway returned error";return this.logger.warn?.(`[bridge-core] handleGatewayResponse: gateway rejected prompt gatewayRequestId=${t} error=${i}`),void this.failPrompt(s,Errors.SendFailed.rpcCode,i)}const n=e.payload,i=n?.runId;i?(s.currentRunId=i,s.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(i,s),this.logger.debug?.(`[bridge-core] handleGatewayResponse: bound runId=${i} gatewayRequestId=${t} sessionId=${s.sessionId}`)):this.logger.debug?.(`[bridge-core] handleGatewayResponse: no runId in payload gatewayRequestId=${t} sessionId=${s.sessionId}`)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}pushToolResultContent(e,t,s,n){if(0===s.length)return;const i=n?this.state.promptsByRunId.get(n):void 0;if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session for runId=${n??"(none)"}`);const o=i.sessionId;for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,t),i.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:o}})}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/",".");try{const n=await t.request(s,e.params??{});this.transport.sendResult(e.id,n??{})}catch(t){const n=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}}async function fetchWithRetry(e,t={}){const s=t.maxRetries??3;let n;for(let i=0;i<=s;i++){const o=await fetch(e,{signal:AbortSignal.timeout(12e4),redirect:"follow"});if(o.ok)return Buffer.from(await o.arrayBuffer());if(429===o.status||o.status>=500){const r=Number(o.headers.get("retry-after"))||0,a=1e3*Math.max(r,2**i);if(n=new Error(`HTTP ${o.status} from ${e}`),i<s){t.logger?.info?.(`[bridge-core] Download ${o.status}, retry ${i+1}/${s} in ${(a/1e3).toFixed(0)}s`),await new Promise(e=>setTimeout(e,a));continue}}throw new Error(`Failed to download zip: ${o.status}`)}throw n??new Error("Download failed after retries")}const SKILL_ROOT_MARKERS=["SKILL.md","skill.md","SKILL.MD"];async function resolveSkillRoot(e){for(const t of SKILL_ROOT_MARKERS)try{return await stat(join(e,t)),e}catch{}const t=(await readdir(e,{withFileTypes:!0})).filter(e=>e.isDirectory()&&!e.name.startsWith(".")&&"__MACOSX"!==e.name);if(1===t.length){const s=join(e,t[0].name);for(const e of SKILL_ROOT_MARKERS)try{return await stat(join(s,e)),s}catch{}return s}return e}async function extractZipToDir(e,t){const s=new AdmZip(e),n=join(t,".beeos-extract-tmp-"+Date.now());try{await mkdir(n,{recursive:!0}),s.extractAllTo(n,!0);const e=await resolveSkillRoot(n),i=await readdir(e,{withFileTypes:!0});let o=0;for(const s of i){if(s.name.startsWith(".beeos-extract-tmp")||"__MACOSX"===s.name)continue;const n=join(e,s.name),i=join(t,s.name);await rename(n,i).catch(async()=>{await cp(n,i,{recursive:!0})}),o++}if(0===o)throw new Error("Zip archive contained no extractable files")}finally{await rm(n,{recursive:!0,force:!0}).catch(()=>{})}}const VALID_SLUG_PATTERN=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,NON_ASCII_PATTERN=/[^\x00-\x7F]/;function validateSkillSlug(e){const t=e.trim();if(!t||t.includes("/")||t.includes("\\")||t.includes(".."))throw new Error(`Invalid skill slug: ${e}`);if(NON_ASCII_PATTERN.test(t)||!VALID_SLUG_PATTERN.test(t))throw new Error(`Invalid skill slug: ${e}`);return t}function safeDirName(e){const t=e.trim();return t?t.replaceAll("/","__").replaceAll("\\","__"):t}function resolveSkillInstallDir(e,t){const s=safeDirName(t),n=join(e,s),i=resolve(e),o=resolve(n),r=relative(i,o);if(!r||".."===r||r.startsWith(`..${sep}`)||path.isAbsolute(r))throw new Error("Invalid skill target path");return n}
@@ -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 o={url:sv(t),usageHint:n};return s&&(o.fit=s),a&&(o.altText=sv(a)),{id:e,component:{Image:o}}}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 o={label:sv(t),options:n.map(e=>({label:sv(e.label),value:e.value})),selections:{path:s}};return a&&(o.description=sv(a)),{id:e,component:{MultipleChoice:o}}}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(),o="row"===t?rowNode(a,s):columnNode(a,s);return[...n,o]}default:return[textNode(nextCid(),`[Unsupported: ${t}]`)]}}function s(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);try{return JSON.stringify(e)}catch{return"[Object]"}}function buildText(e){return[textNode(nextCid(),s(e.content??e.text??""))]}function buildCard(e){const t=[],n=[];if(e.image){const 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,o=nextCid(),i=nextCid(),r=nextCid();t.push(textNode(i,s(a.label??a.key),"caption")),t.push(textNode(r,s(a.value))),t.push(rowNode(o,[i,r],"spaceBetween")),n.push(o)}const o=Array.isArray(e.actions)?e.actions:[];if(o.length>0){const e=[];for(const n of o){if(!n||"object"!=typeof n)continue;const a=n,o=nextCid(),i=nextCid(),r=s(a.label??"Action"),l=s(a.actionId??a.id??nextCid());t.push(textNode(i,r)),t.push(buttonNode(o,i,l,[{key:"actionId",value:{literalString:l}},{key:"label",value:{literalString:r}}])),e.push(o)}if(e.length>0){const s=nextCid();t.push(rowNode(s,e)),n.push(s)}}const i=Array.isArray(e.children)?e.children:[];for(const e of i){if(!e||"object"!=typeof e)continue;const s=translateComponent(e);t.push(...s);const a=findRootIds(s);n.push(...a)}if(e.footer){const a=nextCid();t.push(textNode(a,s(e.footer),"caption")),n.push(a)}const r=nextCid();t.push(columnNode(r,n));const l=nextCid();return t.push(cardNode(l,r)),t}function buildTable(e){const t=[],n=[],a=Array.isArray(e.columns)?e.columns:[],o=Array.isArray(e.rows)?e.rows:[];if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h4")),n.push(a)}const i=[],r=[];for(const e of a)if("string"==typeof e)i.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??"");i.push(s(t.label??t.title??t.header??t.name??n)),r.push(n)}else{const t=s(e);i.push(t),r.push(t)}if(i.length>0){const e=[];for(const n of i){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 o){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 o=[];for(const e of a){const n=nextCid();t.push(textNode(n,s(e))),o.push(n)}const i=nextCid();t.push(rowNode(i,o)),n.push(i)}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:[],o={};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 i=[];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}`;i.push(l);const u=a.defaultValue??a.default??a.value;if("checkbox"===c||"boolean"===c||"toggle"===c)o[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)o[l]=String(u);else if(e.length>0){const t=e[0];o[l]="string"==typeof t?t:s(t.value??"")}else o[l]="";t.push(multipleChoiceNode(r,s(a.label??a.name??""),n,d))}else if("datetime"===c||"time"===c){o[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;o[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{o[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=i.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:o}}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"),o=s(e.actionId??n);return[textNode(t,a),buttonNode(n,t,o,[{key:"actionId",value:{literalString:o}},{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=[],o=[];for(let e=0;e<s.length;e+=n){const t=s.slice(e,e+n),i=nextCid();a.push(rowNode(i,t)),o.push(i)}const i=nextCid();return[...t,...a,columnNode(i,o)]}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(),o="positive"===e.changeType?"↑ ":"negative"===e.changeType?"↓ ":"";t.push(textNode(a,`${o}${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 o=nextCid();t.push(columnNode(o,n,void 0,"center"));const i=nextCid();return t.push(cardNode(i,o)),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,o=nextCid(),i=nextCid(),r=nextCid();t.push(textNode(i,s(a.key??a.label??""),"caption")),t.push(textNode(r,s(a.value??""))),t.push(rowNode(o,[i,r],"spaceBetween")),n.push(o)}const o=nextCid();t.push(columnNode(o,n));const i=nextCid();return t.push(cardNode(i,o)),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,o=[];if(a.image){const e=nextCid();t.push(imageNode(e,s(a.image),"avatar")),o.push(e)}if(a.icon){const e=nextCid();t.push(iconNode(e,s(a.icon))),o.push(e)}const i=[];if(a.title||a.name){const e=nextCid();t.push(textNode(e,s(a.title??a.name),"body")),i.push(e)}if(a.subtitle||a.description){const e=nextCid();t.push(textNode(e,s(a.subtitle??a.description),"caption")),i.push(e)}if(i.length>0){const e=nextCid();t.push(columnNode(e,i)),o.push(e)}if(a.badge||a.value){const e=nextCid();t.push(textNode(e,s(a.badge??a.value),"caption")),o.push(e)}const r=nextCid();t.push(rowNode(r,o,"start","center")),n.push(r)}const o=nextCid();return t.push(columnNode(o,n)),t}function buildAlert(e){const t=[],n=[],a=s(e.variant??"info"),o=nextCid();t.push(iconNode(o,{info:"info",success:"check",warning:"error",error:"error",tip:"info",caution:"error",important:"error",note:"info"}[a]??"info")),n.push(o);const i=[];if(e.title){const n=nextCid();t.push(textNode(n,s(e.title),"h5")),i.push(n)}if(e.message||e.content){const n=nextCid();t.push(textNode(n,s(e.message??e.content))),i.push(n)}if(0===i.length){const n=nextCid();t.push(textNode(n,s(e.type??"Alert"))),i.push(n)}const r=nextCid();t.push(columnNode(r,i)),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 o=Array.isArray(e.datasets)?e.datasets:Array.isArray(e.stages)?e.stages:null;return o&&(a.datasets=normalizeDatasets(o)),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"),o=(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e));n.push(...o);const i=findRootIds(o),r=nextCid();n.push(columnNode(r,i)),a.push({title:t,childId:r})}const o=nextCid();return n.push(tabsNode(o,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:o}=e,i=s??e.getGatewaySessionKey?.(n),r=i??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=i??t,d=i&&i!==t?t:void 0,u=await c.ensureCanvas(l,{agentId:s,linkSessionId:d});return await a.joinCanvas(u,r),o.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${i??"none"} agentId=${s??"none"}`),u}catch(e){return void o.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:s,isCanvasActive:a,getGatewaySessionKey:o}=e;let i=0;return async(r,l)=>{if(a){const e=o?.(r);if(!a(e))return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."}}const c=l,d=c.id,u="panel";let p=c.components;if("string"==typeof p)try{p=JSON.parse(p)}catch{}if(!d||!Array.isArray(p)||!p.length)return{success:!1,error:"id and components (array) are required"};if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),s({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const m=translateComponentsWithDataModel(p),h=m.components,f=Object.keys(m.dataModel).length>0?m.dataModel:void 0,b=findRootIds(h),g=nextCid(),y=columnNode(g,b),x=[...h,y],v=e.getActiveSessionId?.(r);if(!v)return n.warn?.(`[canvas] No active session for toolCallId=${r}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};const C=e.getGatewaySessionKey?.(r),N=await ensureRelayConnected(e,v,r,C),w=t.has(d,N??void 0),A=Date.now(),k=t.getRelayClient();N&&k&&A-i>6e4&&k.createSnapshot(N,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),w?t.update(d,x,N,g,f):t.create(N??`_fallback_${Date.now()}`,d,u,x,{origin:{kind:"agent"},rootComponentId:g,dataModel:f});const T=p.map(e=>e.type);s({component:"canvas",domain:"canvas",name:w?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:u,componentCount:p.length,componentTypes:T}}),n.debug?.(`[canvas] ${w?"Updated":"Created"} surface=${d} components=${p.length}`);return{success:!0,surfaceId:d,presentation:u,isUpdate:w,activeSurfaces:N?t.getAllActive(N).map(e=>e.surfaceId):[d]}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:s}=e;return async(a,o)=>{const i=o.id;if(!i)return{success:!1,error:"id is required"};const r=t.get(i),l=r?Date.now()-r.createdAt:0,c=e.getGatewaySessionKey?.(a),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(i,d),s({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:i,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${i}`),{success:!0,surfaceId:i,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 o=`[Canvas State] Active surfaces: ${s.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const s=n.types.slice(0,3).join(", "),a=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${s}${a})`}).join("; ")}.`;return a>0&&(o+=` ...and ${a} more.`),o+=" To modify existing content, reuse its id in render_ui.",o.length>500&&(o=o.slice(0,497)+"..."),o}
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 o={url:sv(t),usageHint:n};return s&&(o.fit=s),a&&(o.altText=sv(a)),{id:e,component:{Image:o}}}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 o={label:sv(t),options:n.map(e=>({label:sv(e.label),value:e.value})),selections:{path:s}};return a&&(o.description=sv(a)),{id:e,component:{MultipleChoice:o}}}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(),o="row"===t?rowNode(a,s):columnNode(a,s);return[...n,o]}default:return[textNode(nextCid(),`[Unsupported: ${t}]`)]}}function s(e){if(null==e)return"";if("string"==typeof e)return e;if("number"==typeof e||"boolean"==typeof e)return String(e);try{return JSON.stringify(e)}catch{return"[Object]"}}function buildText(e){return[textNode(nextCid(),s(e.content??e.text??""))]}function buildCard(e){const t=[],n=[];if(e.image){const 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,o=nextCid(),i=nextCid(),r=nextCid();t.push(textNode(i,s(a.label??a.key),"caption")),t.push(textNode(r,s(a.value))),t.push(rowNode(o,[i,r],"spaceBetween")),n.push(o)}const o=Array.isArray(e.actions)?e.actions:[];if(o.length>0){const e=[];for(const n of o){if(!n||"object"!=typeof n)continue;const a=n,o=nextCid(),i=nextCid(),r=s(a.label??"Action"),l=s(a.actionId??a.id??nextCid());t.push(textNode(i,r)),t.push(buttonNode(o,i,l,[{key:"actionId",value:{literalString:l}},{key:"label",value:{literalString:r}}])),e.push(o)}if(e.length>0){const s=nextCid();t.push(rowNode(s,e)),n.push(s)}}const i=Array.isArray(e.children)?e.children:[];for(const e of i){if(!e||"object"!=typeof e)continue;const s=translateComponent(e);t.push(...s);const a=findRootIds(s);n.push(...a)}if(e.footer){const a=nextCid();t.push(textNode(a,s(e.footer),"caption")),n.push(a)}const r=nextCid();t.push(columnNode(r,n));const l=nextCid();return t.push(cardNode(l,r)),t}function buildTable(e){const t=[],n=[],a=Array.isArray(e.columns)?e.columns:[],o=Array.isArray(e.rows)?e.rows:[];if(e.title){const a=nextCid();t.push(textNode(a,s(e.title),"h4")),n.push(a)}const i=[],r=[];for(const e of a)if("string"==typeof e)i.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??"");i.push(s(t.label??t.title??t.header??t.name??n)),r.push(n)}else{const t=s(e);i.push(t),r.push(t)}if(i.length>0){const e=[];for(const n of i){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 o){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 o=[];for(const e of a){const n=nextCid();t.push(textNode(n,s(e))),o.push(n)}const i=nextCid();t.push(rowNode(i,o)),n.push(i)}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:[],o={};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 i=[];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}`;i.push(l);const u=a.defaultValue??a.default??a.value;if("checkbox"===c||"boolean"===c||"toggle"===c)o[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)o[l]=String(u);else if(e.length>0){const t=e[0];o[l]="string"==typeof t?t:s(t.value??"")}else o[l]="";t.push(multipleChoiceNode(r,s(a.label??a.name??""),n,d))}else if("datetime"===c||"time"===c){o[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;o[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{o[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=i.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:o}}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"),o=s(e.actionId??n);return[textNode(t,a),buttonNode(n,t,o,[{key:"actionId",value:{literalString:o}},{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=[],o=[];for(let e=0;e<s.length;e+=n){const t=s.slice(e,e+n),i=nextCid();a.push(rowNode(i,t)),o.push(i)}const i=nextCid();return[...t,...a,columnNode(i,o)]}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(),o="positive"===e.changeType?"↑ ":"negative"===e.changeType?"↓ ":"";t.push(textNode(a,`${o}${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 o=nextCid();t.push(columnNode(o,n,void 0,"center"));const i=nextCid();return t.push(cardNode(i,o)),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,o=nextCid(),i=nextCid(),r=nextCid();t.push(textNode(i,s(a.key??a.label??""),"caption")),t.push(textNode(r,s(a.value??""))),t.push(rowNode(o,[i,r],"spaceBetween")),n.push(o)}const o=nextCid();t.push(columnNode(o,n));const i=nextCid();return t.push(cardNode(i,o)),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,o=[];if(a.image){const e=nextCid();t.push(imageNode(e,s(a.image),"avatar")),o.push(e)}if(a.icon){const e=nextCid();t.push(iconNode(e,s(a.icon))),o.push(e)}const i=[];if(a.title||a.name){const e=nextCid();t.push(textNode(e,s(a.title??a.name),"body")),i.push(e)}if(a.subtitle||a.description){const e=nextCid();t.push(textNode(e,s(a.subtitle??a.description),"caption")),i.push(e)}if(i.length>0){const e=nextCid();t.push(columnNode(e,i)),o.push(e)}if(a.badge||a.value){const e=nextCid();t.push(textNode(e,s(a.badge??a.value),"caption")),o.push(e)}const r=nextCid();t.push(rowNode(r,o,"start","center")),n.push(r)}const o=nextCid();return t.push(columnNode(o,n)),t}function buildAlert(e){const t=[],n=[],a=s(e.variant??"info"),o=nextCid();t.push(iconNode(o,{info:"info",success:"check",warning:"error",error:"error",tip:"info",caution:"error",important:"error",note:"info"}[a]??"info")),n.push(o);const i=[];if(e.title){const n=nextCid();t.push(textNode(n,s(e.title),"h5")),i.push(n)}if(e.message||e.content){const n=nextCid();t.push(textNode(n,s(e.message??e.content))),i.push(n)}if(0===i.length){const n=nextCid();t.push(textNode(n,s(e.type??"Alert"))),i.push(n)}const r=nextCid();t.push(columnNode(r,i)),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 asArr(e){return Array.isArray(e)?e:null}function normalizeChartInput(e){let t=asArr(e.datasets)??asArr(e.stages)??asArr(e.series);!t&&asArr(e.data)&&(t=[{label:"",data:e.data}]),!t&&asArr(e.values)&&(t=[{label:"",data:e.values}]);const n=(t??[]).map(e=>{if(!e||"object"!=typeof e)return{label:"",data:[],color:void 0};const t=s(e.label??e.name??"");let n=asArr(e.data)??asArr(e.values)??[];const a=e.color?s(e.color):void 0;return n=n.map(e=>e&&"object"==typeof e?"y"in e?e.y:"value"in e&&!("open"in e)?e.value:"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),{label:t,data:n,color:a}});let a=asArr(e.labels)?.map(e=>"string"==typeof e?e:String(e??""))??[];if(0===a.length&&t){const e=t[0];a=(asArr(e?.data)??asArr(e?.values)??[]).map(e=>e&&"object"==typeof e?"x"in e?s(e.x):"name"in e?s(e.name):"label"in e?s(e.label):null:null).filter(e=>null!==e)}return{labels:a,datasets:n}}function buildChart(e){const t=nextCid(),n=s(e.chartType??e.chart_type??"bar"),a={chartType:sv(n)};e.title&&(a.title=sv(s(e.title)));const{labels:o,datasets:i}=normalizeChartInput(e);return o.length>0&&(a.labels=o),i.length>0&&(a.datasets=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"),o=(Array.isArray(e.children)?e.children:[]).flatMap(e=>translateComponent(e));n.push(...o);const i=findRootIds(o),r=nextCid();n.push(columnNode(r,i)),a.push({title:t,childId:r})}const o=nextCid();return n.push(tabsNode(o,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:o}=e,i=s??e.getGatewaySessionKey?.(n),r=i??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=i??t,d=i&&i!==t?t:void 0,u=await c.ensureCanvas(l,{agentId:s,linkSessionId:d});return await a.joinCanvas(u,r),o.info?.(`[canvas] Joined canvas=${u} session=${t} gatewayKey=${i??"none"} agentId=${s??"none"}`),u}catch(e){return void o.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:s,isCanvasActive:a,getGatewaySessionKey:o}=e;let i=0;return async(r,l)=>{if(a){const e=o?.(r);if(!a(e))return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."}}const c=l,d=c.id,u="panel";let p=c.components;if("string"==typeof p)try{p=JSON.parse(p)}catch{}if(!d||!Array.isArray(p)||!p.length)return{success:!1,error:"id and components (array) are required"};if(!t.validateComponentCount(p))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),s({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const m=translateComponentsWithDataModel(p),h=m.components,f=Object.keys(m.dataModel).length>0?m.dataModel:void 0,b=findRootIds(h),g=nextCid(),y=columnNode(g,b),x=[...h,y],v=e.getActiveSessionId?.(r);if(!v)return n.warn?.(`[canvas] No active session for toolCallId=${r}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};const C=e.getGatewaySessionKey?.(r),N=await ensureRelayConnected(e,v,r,C),w=t.has(d,N??void 0),A=Date.now(),k=t.getRelayClient();N&&k&&A-i>6e4&&k.createSnapshot(N,"Pre-agent edit").then(()=>{i=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),w?t.update(d,x,N,g,f):t.create(N??`_fallback_${Date.now()}`,d,u,x,{origin:{kind:"agent"},rootComponentId:g,dataModel:f});const I=p.map(e=>e.type);s({component:"canvas",domain:"canvas",name:w?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:u,componentCount:p.length,componentTypes:I}}),n.debug?.(`[canvas] ${w?"Updated":"Created"} surface=${d} components=${p.length}`);return{success:!0,surfaceId:d,presentation:u,isUpdate:w,activeSurfaces:N?t.getAllActive(N).map(e=>e.surfaceId):[d]}}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:s}=e;return async(a,o)=>{const i=o.id;if(!i)return{success:!1,error:"id is required"};const r=t.get(i),l=r?Date.now()-r.createdAt:0,c=e.getGatewaySessionKey?.(a),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(i,d),s({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:i,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${i}`),{success:!0,surfaceId:i,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 o=`[Canvas State] Active surfaces: ${s.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const s=n.types.slice(0,3).join(", "),a=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${s}${a})`}).join("; ")}.`;return a>0&&(o+=` ...and ${a} more.`),o+=" To modify existing content, reuse its id in render_ui.",o.length>500&&(o=o.slice(0,497)+"..."),o}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
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>",