beeos-claw 0.5.15 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/acp-server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;messageClient=null;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey,t.inputProvenance,t.channelId,t.inboundMessageId).catch(e=>this.logger.warn?.("[acp-server] Deferred agentSend failed: %s",e)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async agentSend(e,t,s,n,i,r){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const a=this.config.gateway.agentId||this.core.getDefaultAgentId();let o,c;if(s?(o=this.state.findSessionByKey(s),c=s,o?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${o.sessionId}`):(o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: created session for key=${s} sid=${o.sessionId}`))):(c=`agent:${a}:a2a:${randomUUID()}`,o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created A2A session key=${c}`)),i&&o&&(o.channelId=i),r&&o&&(o.inboundMessageId=r),o&&!o.promptDone&&(void 0!==o.pendingPromptId||void 0!==o.currentRunId||void 0!==o.gatewayRequestId))return(o.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n,channelId:i,inboundMessageId:r}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",o.pendingAgentSends.length);const g=await this.gateway.agentSend(c,e,a,t,n);return g?.runId&&(o?(this.state.resetForNewRun(o,o.sessionId,this.transport),o.currentRunId=g.runId,o.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(g.runId,o)):this.state.promptsByRunId.set(g.runId,{gatewaySessionKey:c,channelId:i,inboundMessageId:r})),g}setMessageClient(e,t){this.messageClient=e,this.msgPrincipalId=t}async publishAgentReply(e,t,s,n){const i=this.messageClient,r=this.msgPrincipalId;if(i&&r){if(e)try{return await i.messages.send({conversationId:e,type:s,content:n,replyTo:t}),void this.logger.info?.(`[acp-server] ${s} persisted channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] messages.send failed channel=${e}: ${t instanceof Error?t.message:String(t)}; falling back to personal channel`)}await i.identities.send({identityId:r,type:s,content:n})}}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,i,r){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=r??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const a=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:a,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>this.core.completePrompt(e,t),failPrompt:(e,t,s)=>this.core.failPrompt(e,t,s),schedulePromptTimeout:e=>this.core.schedulePromptTimeout(e),maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageClient:()=>this.messageClient,getMsgPrincipalId:()=>this.msgPrincipalId??null,gatewayRequest:(e,t)=>{const s=this.gateway;return s?.isConnected?s.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache,onStreamDelta:(e,t)=>{if(!this.messageClient||!this.msgPrincipalId)return;const s=e.gatewaySessionKey;if(!s)return;const n=e.channelId,i={session_key:s,update:t};(n?this.messageClient.messages.send({conversationId:n,type:"agent_reply_delta",content:i}):this.messageClient.identities.send({identityId:this.msgPrincipalId,type:"agent_reply_delta",content:i})).catch(e=>this.logger.debug?.(`[acp-server] agent_reply_delta publish failed: ${e}`))}}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.onRunComplete=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId,r=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunComplete runId=${e} session_key=${n} channel=${i} in_reply_to=${r??"-"} principal=${this.msgPrincipalId} hasClient=${!!this.messageClient}`),!n)return void this.logger.warn?.(`[acp-server] onRunComplete: no session_key for runId=${e}, cannot publish reply`);const a={session_key:n,text:t};this.publishAgentReply(i,r,"agent_reply",a).catch(e=>this.logger.warn?.(`[acp-server] agent_reply publish failed: ${e instanceof Error?e.message:JSON.stringify(e)}`))},this.core.onRunFailed=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId,r=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunFailed runId=${e} session_key=${n} channel=${i} in_reply_to=${r??"-"}`),!n)return;const a={session_key:n,error:t||"execution failed"};this.publishAgentReply(i,r,"agent_reply",a).catch(e=>this.logger.warn?.(`[acp-server] agent_reply error publish failed: ${e instanceof Error?e.message:String(e)}`))},this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,s)=>{this.canvasOrch.onSessionLoaded(e,t,s)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}async start(e){const t=Date.now();this.logger.info?.("[acp-server] start() begin"),e&&(this.config.bridge.url=e);const s=this.config.bridge.url,n=this.config.keyFile;if(s&&!BridgeClientCtor)try{const e=Date.now(),t=await import("@beeos-ai/bridge-client");BridgeClientCtor=t.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-e}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}const i=Date.now();if(await this.connectGateway(),this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-i}ms`),s&&n&&BridgeClientCtor){Date.now();await this.startBridgeMode(s,n)}else this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone();this.logger.info?.(`[acp-server] start() total ${Date.now()-t}ms`)}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});this.bridgeInstance=s;const n=()=>this.bridgeInstance===s;s.on("connected",()=>{n()&&(this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync())}),s.on("disconnected",(...e)=>{if(!n())return;const i=String(e[0]??"unknown"),r=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${i} (detail=${r})`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,t))}).catch(()=>{})}}),s.on("message",(...e)=>{if(!n())return;const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{if(!n())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const i={readyState:WebSocket.OPEN,send:(e,t)=>{s.send(e).then(()=>t?.(),e=>{const s=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${s.message}`),t?.(s)})},close:()=>{s.close().catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{s.close(!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>i,once:()=>i,removeListener:()=>i,removeAllListeners:()=>i,ping:()=>{}};Object.defineProperty(i,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=i;const r=Date.now();try{await s.connect(),this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-r}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-r}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();if(this.agentSyncInFlight)return;const t=()=>{if(e+1>=6)return void this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — waiting for Gateway reconnect");const t=e<2?1e3*(e+1):Math.min(5e3*2**(e-2),12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(e+1)},t)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0,streaming:!0},agentFetch:this.agentFetchFn,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-s}ms)`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}finally{this.agentSyncInFlight=!1}}async connectGateway(){this.gateway&&(this.gateway.stop(),this.gateway=null);const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.gateway.on("sessionMessage",e=>this.core.handleSessionMessage(e)),this.gateway.on("response",e=>this.core.handleGatewayResponse(e));const t=this.gateway;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,s)=>this.gateway.agentSend(e,t,s)}:null}),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.tryAgentSync()}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.logger.info?.(`[acp-server] transport.${e.event} durationMs=${e.durationMs??"?"}`),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(s?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,s.sessionId),s.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=this.state.toolCallIdMap.get(e);if(s&&s!==e){const e=this.state.toolCallToSessionMap.get(s);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(s?.agentId??"")??s?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const s=this.canvasOrch.schemaRegistry;s&&import("node:fs/promises").then(async({readFile:n})=>{try{const i=`${t}/skill.json`,r=await n(i,"utf-8"),a=JSON.parse(r);if(a.formSchemas?.length){for(const e of a.formSchemas)s.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.stop(),this.gateway.removeAllListeners(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance){try{await this.bridgeInstance.close()}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}this.bridgeInstance=null}this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
|
|
1
|
+
import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;messageClient=null;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;started=!1;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey,t.inputProvenance,t.channelId,t.inboundMessageId).catch(e=>this.logger.warn?.("[acp-server] Deferred agentSend failed: %s",e)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async agentSend(e,t,s,n,i,r){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const a=this.config.gateway.agentId||this.core.getDefaultAgentId();let o,c;if(s?(o=this.state.findSessionByKey(s),c=s,o?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${o.sessionId}`):(o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: created session for key=${s} sid=${o.sessionId}`))):(c=`agent:${a}:a2a:${randomUUID()}`,o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created A2A session key=${c}`)),i&&o&&(o.channelId=i),r&&o&&(o.inboundMessageId=r),o&&!o.promptDone&&(void 0!==o.pendingPromptId||void 0!==o.currentRunId||void 0!==o.gatewayRequestId))return(o.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n,channelId:i,inboundMessageId:r}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",o.pendingAgentSends.length);const g=await this.gateway.agentSend(c,e,a,t,n);return g?.runId&&(o?(this.state.resetForNewRun(o,o.sessionId,this.transport),o.currentRunId=g.runId,o.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(g.runId,o)):this.state.promptsByRunId.set(g.runId,{gatewaySessionKey:c,channelId:i,inboundMessageId:r})),g}setMessageClient(e,t){this.messageClient=e,this.msgPrincipalId=t}async publishAgentReply(e,t,s,n){const i=this.messageClient,r=this.msgPrincipalId;if(i&&r){if(e)try{return await i.messages.send({conversationId:e,type:s,content:n,replyTo:t}),void this.logger.info?.(`[acp-server] ${s} persisted channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] messages.send failed channel=${e}: ${t instanceof Error?t.message:String(t)}; falling back to personal channel`)}await i.identities.send({identityId:r,type:s,content:n})}}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,i,r){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=r??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const a=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:a,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>this.core.completePrompt(e,t),failPrompt:(e,t,s)=>this.core.failPrompt(e,t,s),schedulePromptTimeout:e=>this.core.schedulePromptTimeout(e),maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageClient:()=>this.messageClient,getMsgPrincipalId:()=>this.msgPrincipalId??null,gatewayRequest:(e,t)=>{const s=this.gateway;return s?.isConnected?s.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache,onStreamDelta:(e,t)=>{if(!this.messageClient||!this.msgPrincipalId)return;const s=e.gatewaySessionKey;if(!s)return;const n=e.channelId,i={session_key:s,update:t};(n?this.messageClient.messages.send({conversationId:n,type:"agent_reply_delta",content:i}):this.messageClient.identities.send({identityId:this.msgPrincipalId,type:"agent_reply_delta",content:i})).catch(e=>this.logger.debug?.(`[acp-server] agent_reply_delta publish failed: ${e}`))}}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.onRunComplete=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId,r=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunComplete runId=${e} session_key=${n} channel=${i} in_reply_to=${r??"-"} principal=${this.msgPrincipalId} hasClient=${!!this.messageClient}`),!n)return void this.logger.warn?.(`[acp-server] onRunComplete: no session_key for runId=${e}, cannot publish reply`);const a={session_key:n,text:t};this.publishAgentReply(i,r,"agent_reply",a).catch(e=>this.logger.warn?.(`[acp-server] agent_reply publish failed: ${e instanceof Error?e.message:JSON.stringify(e)}`))},this.core.onRunFailed=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId,r=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunFailed runId=${e} session_key=${n} channel=${i} in_reply_to=${r??"-"}`),!n)return;const a={session_key:n,error:t||"execution failed"};this.publishAgentReply(i,r,"agent_reply",a).catch(e=>this.logger.warn?.(`[acp-server] agent_reply error publish failed: ${e instanceof Error?e.message:String(e)}`))},this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,s)=>{this.canvasOrch.onSessionLoaded(e,t,s)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}async start(e){if(this.started)return void this.logger.info?.("[acp-server] start() skipped — already started (singleton reuse)");this.started=!0;const t=Date.now();this.logger.info?.("[acp-server] start() begin"),e&&(this.config.bridge.url=e);const s=this.config.bridge.url,n=this.config.keyFile;if(s&&!BridgeClientCtor)try{const e=Date.now(),t=await import("@beeos-ai/bridge-client");BridgeClientCtor=t.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-e}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}const i=Date.now();if(await this.connectGateway(),this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-i}ms`),s&&n&&BridgeClientCtor){Date.now();await this.startBridgeMode(s,n)}else this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone();this.logger.info?.(`[acp-server] start() total ${Date.now()-t}ms`)}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});this.bridgeInstance=s;const n=()=>this.bridgeInstance===s;s.on("connected",()=>{n()&&(this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync())}),s.on("disconnected",(...e)=>{if(!n())return;const i=String(e[0]??"unknown"),r=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${i} (detail=${r})`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,t))}).catch(()=>{})}}),s.on("message",(...e)=>{if(!n())return;const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{if(!n())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const i={readyState:WebSocket.OPEN,send:(e,t)=>{s.send(e).then(()=>t?.(),e=>{const s=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${s.message}`),t?.(s)})},close:()=>{s.close().catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{s.close(!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>i,once:()=>i,removeListener:()=>i,removeAllListeners:()=>i,ping:()=>{}};Object.defineProperty(i,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=i;const r=Date.now();try{await s.connect(),this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-r}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-r}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();if(this.agentSyncInFlight)return;const t=()=>{if(e+1>=6)return void this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — waiting for Gateway reconnect");const t=e<2?1e3*(e+1):Math.min(5e3*2**(e-2),12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(e+1)},t)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0,streaming:!0},agentFetch:this.agentFetchFn,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-s}ms)`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}finally{this.agentSyncInFlight=!1}}async connectGateway(){this.gateway&&(this.gateway.stop(),this.gateway=null);const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.gateway.on("sessionMessage",e=>this.core.handleSessionMessage(e)),this.gateway.on("response",e=>this.core.handleGatewayResponse(e));const t=this.gateway;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,s)=>this.gateway.agentSend(e,t,s)}:null}),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.tryAgentSync()}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.logger.info?.(`[acp-server] transport.${e.event} durationMs=${e.durationMs??"?"}`),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(s?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,s.sessionId),s.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=this.state.toolCallIdMap.get(e);if(s&&s!==e){const e=this.state.toolCallToSessionMap.get(s);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(s?.agentId??"")??s?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const s=this.canvasOrch.schemaRegistry;s&&import("node:fs/promises").then(async({readFile:n})=>{try{const i=`${t}/skill.json`,r=await n(i,"utf-8"),a=JSON.parse(r);if(a.formSchemas?.length){for(const e of a.formSchemas)s.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){if(this.started=!1,this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.stop(),this.gateway.removeAllListeners(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance){try{await this.bridgeInstance.close()}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}this.bridgeInstance=null}this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
|
package/dist/canvas/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render a Google A2UI v0.8 component tree to the Canvas panel. Output a flat array of ComponentInstance objects with id references. ALWAYS prefer this over the canvas tool for displaying structured UI.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","root","components"],properties:{id:{type:"string",description:"Surface ID (e.g. 'sales-report'). Reuse same id to update an existing surface."},root:{type:"string",description:"ID of the root component (must reference one of the components below)."},components:{type:"array",description:"Flat array of A2UI ComponentInstance objects. Each has a unique id and exactly one component type key.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:"Unique component ID referenced by parent via child/children."},component:{type:"object",description:"Exactly ONE key = component type (Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart). Value = that component's properties object."}}}},data:{type:"object",description:"Optional data model for two-way binding. Keys are paths (e.g. '/name'), values are defaults."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};const SESSION_READY_POLL_ATTEMPTS=3,SESSION_READY_POLL_INTERVAL_MS=400,delay=e=>new Promise(t=>setTimeout(t,e));async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=r&&r!==t?t:void 0,p=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(p,s),i.info?.(`[canvas] Joined canvas=${p} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),p}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,isCanvasActive:o,getGatewaySessionKey:i}=e;let r=0;return async(s,l)=>{if(o){const e=i?.(s);if(!o(e))return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."}}const c=l,d=c.id,p=c.root,u="panel";let m=c.components;if("string"==typeof m)try{m=JSON.parse(m)}catch{}if(!(d&&p&&Array.isArray(m)&&m.length))return{success:!1,error:"id, root, and components (array) are required"};const h=normalizeComponents(m);if(!t.validateComponentCount(h))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const g=c.data;let y=e.getActiveSessionId?.(s);for(let t=0;!y&&t<3;t++)await delay(400),y=e.getActiveSessionId?.(s);if(!y)return n.warn?.(`[canvas] No active session for toolCallId=${s} after 3 polls, render_ui not ready`),{success:!1,retryable:!0,error:"Canvas session not ready yet (agent just connected) — retry render_ui shortly; do NOT fall back to plain text."};const f=e.getGatewaySessionKey?.(s),v=await ensureRelayConnected(e,y,s,f),x=t.has(d,v??void 0),b=Date.now(),C=t.getRelayClient();v&&C&&b-r>6e4&&C.createSnapshot(v,"Pre-agent edit").then(()=>{r=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),x?t.update(d,h,v,p,g):t.create(v??`_fallback_${Date.now()}`,d,u,h,{origin:{kind:"agent"},rootComponentId:p,dataModel:g});const T=h.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:x?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:u,componentCount:h.length,componentTypes:T}}),n.debug?.(`[canvas] ${x?"Updated":"Created"} surface=${d} components=${h.length}`);const S={success:!0,surfaceId:d,presentation:u,isUpdate:x,activeSurfaces:v?t.getAllActive(v).map(e=>e.surfaceId):[d]};return g&&(S.dataModel=g),S}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI — Google A2UI v0.8\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.**\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n\nParameters:\n- `id`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- `root`: The ID of the top-level component (usually a Column).\n- `components`: Flat array of objects. **Each object MUST have `id` and `component`**:\n `{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}`\n The `component` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- `data` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same `id` updates existing surface; different `id` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: `{"literalString": "Hello"}`\n- Data binding: `{"path": "/fieldName"}` (reads from the `data` model)\n\nShorthand: `sv("text")` means `{"literalString": "text"}` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n`{Text: {text: sv("Title"), usageHint: "h2"}}`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}`\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n`{Icon: {name: sv("check")}}`\n\n**Video** — Embed a video.\n`{Video: {url: sv("https://youtube.com/...")}}`\n\n**AudioPlayer** — Audio playback.\n`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}`\n\n**Row** — Horizontal layout. Children referenced by ID.\n`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n`{Column: {children: {explicitList: ["child1", "child2"]}}}`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}`\n\n**Card** — Container with a single child.\n`{Card: {child: "contentColumnId"}}`\n\n**Tabs** — Tabbed container.\n`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}`\n\n**Divider** — Horizontal or vertical separator.\n`{Divider: {axis: "horizontal"}}`\n\n**Modal** — Dialog with trigger.\n`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}`\n\n**Button** — Clickable with action.\n`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}`\n\n**TextField** — Text input with data binding.\n`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}`\n\n**DateTimeInput** — Date/time picker.\n`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}`\n\n**MultipleChoice** — Selection from options.\n`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}`\n\n**Slider** — Numeric range input.\n`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}`\n\n**Chart** (BeeOS extension) — Data visualization.\n`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition Patterns\n\n**Simple text display:**\n```json\n{"id":"s1","root":"r","components":[\n {"id":"t","component":{"Text":{"text":{"literalString":"Hello World"},"usageHint":"h2"}}},\n {"id":"r","component":{"Column":{"children":{"explicitList":["t"]}}}}\n]}\n```\n\n**Dashboard with KPI cards:**\n```json\n{"id":"dashboard","root":"root","components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Dashboard"},"usageHint":"h2"}}},\n {"id":"rev-val","component":{"Text":{"text":{"literalString":"$1.2M"},"usageHint":"h2"}}},\n {"id":"rev-lbl","component":{"Text":{"text":{"literalString":"Revenue"},"usageHint":"caption"}}},\n {"id":"rev-col","component":{"Column":{"children":{"explicitList":["rev-lbl","rev-val"]},"alignment":"center"}}},\n {"id":"rev-card","component":{"Card":{"child":"rev-col"}}},\n {"id":"users-val","component":{"Text":{"text":{"literalString":"8,432"},"usageHint":"h2"}}},\n {"id":"users-lbl","component":{"Text":{"text":{"literalString":"Users"},"usageHint":"caption"}}},\n {"id":"users-col","component":{"Column":{"children":{"explicitList":["users-lbl","users-val"]},"alignment":"center"}}},\n {"id":"users-card","component":{"Card":{"child":"users-col"}}},\n {"id":"kpi-row","component":{"Row":{"children":{"explicitList":["rev-card","users-card"]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","kpi-row"]}}}}\n]}\n```\n\n**Table layout (Column + Row + Text):**\n```json\n{"id":"users-table","root":"root","components":[\n {"id":"h-name","component":{"Text":{"text":{"literalString":"Name"},"usageHint":"caption"}}},\n {"id":"h-role","component":{"Text":{"text":{"literalString":"Role"},"usageHint":"caption"}}},\n {"id":"header","component":{"Row":{"children":{"explicitList":["h-name","h-role"]}}}},\n {"id":"div","component":{"Divider":{"axis":"horizontal"}}},\n {"id":"r1-name","component":{"Text":{"text":{"literalString":"Alice"},"usageHint":"body"}}},\n {"id":"r1-role","component":{"Text":{"text":{"literalString":"Engineer"},"usageHint":"body"}}},\n {"id":"row1","component":{"Row":{"children":{"explicitList":["r1-name","r1-role"]}}}},\n {"id":"r2-name","component":{"Text":{"text":{"literalString":"Bob"},"usageHint":"body"}}},\n {"id":"r2-role","component":{"Text":{"text":{"literalString":"Designer"},"usageHint":"body"}}},\n {"id":"row2","component":{"Row":{"children":{"explicitList":["r2-name","r2-role"]}}}},\n {"id":"table-col","component":{"Column":{"children":{"explicitList":["header","div","row1","row2"]}}}},\n {"id":"root","component":{"Card":{"child":"table-col"}}}\n]}\n```\n\n**Form with data binding:**\n```json\n{"id":"settings","root":"root","data":{"/name":"","/email":"","/notify":false},"components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Settings"},"usageHint":"h3"}}},\n {"id":"f-name","component":{"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"},"type":"shortText"}}},\n {"id":"f-email","component":{"TextField":{"label":{"literalString":"Email"},"text":{"path":"/email"},"type":"shortText"}}},\n {"id":"f-notify","component":{"CheckBox":{"label":{"literalString":"Email notifications"},"value":{"path":"/notify"}}}},\n {"id":"btn-text","component":{"Text":{"text":{"literalString":"Save"},"usageHint":"body"}}},\n {"id":"btn","component":{"Button":{"child":"btn-text","action":{"name":"save_settings","context":[{"key":"name","value":{"path":"/name"}},{"key":"email","value":{"path":"/email"}},{"key":"notify","value":{"path":"/notify"}}]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","f-name","f-email","f-notify","btn"]}}}}\n]}\n```\n\n**Chart:**\n```json\n{"id":"sales-chart","root":"root","components":[\n {"id":"chart","component":{"Chart":{"chartType":{"literalString":"bar"},"title":{"literalString":"Monthly Sales"},"labels":["Jan","Feb","Mar"],"datasets":[{"label":"Revenue","data":[100,200,150]}]}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["chart"]}}}}\n]}\n```\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. Each component in the array MUST be: {"id":"x","component":{"TypeName":{props}}} — the "component" wrapper key is required. render_ui uses Google A2UI v0.8 format: flat component array with id references. Components: Text (usageHint: h1-h5/caption/body), Image, Icon, Video, AudioPlayer, Row (horizontal layout, children.explicitList), Column (vertical layout, children.explicitList), List, Card (single child), Tabs (tabItems), Divider, Modal, Button (child + action), TextField, CheckBox, MultipleChoice, Slider, DateTimeInput, Chart (BeeOS extension: chartType, labels, datasets). StringValue: {"literalString":"text"} or {"path":"/binding"}. root param = top-level component ID. Tables: Card > Column > Row > Text. Forms: TextField/CheckBox/Slider + Button with action.context paths. Chart: chartType(bar/line/pie/area/scatter/doughnut/radar/funnel/gauge/candlestick/sparkline), labels, datasets:[{label,data,color?}]. Candlestick: [{open,high,low,close}]. Gauge: 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 and provide the full updated component tree.";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 a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
|
|
1
|
+
const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render a Google A2UI v0.8 component tree to the Canvas panel. Output a flat array of ComponentInstance objects with id references. ALWAYS prefer this over the canvas tool for displaying structured UI.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","root","components"],properties:{id:{type:"string",description:"Surface ID (e.g. 'sales-report'). Reuse same id to update an existing surface."},root:{type:"string",description:"ID of the root component (must reference one of the components below)."},components:{type:"array",description:"Flat array of A2UI ComponentInstance objects. Each has a unique id and exactly one component type key.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:"Unique component ID referenced by parent via child/children."},component:{type:"object",description:"Exactly ONE key = component type (Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart). Value = that component's properties object."}}}},data:{type:"object",description:"Optional data model for two-way binding. Keys are paths (e.g. '/name'), values are defaults."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};const SESSION_READY_POLL_ATTEMPTS=3,SESSION_READY_POLL_INTERVAL_MS=400,delay=e=>new Promise(t=>setTimeout(t,e));async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=r&&r!==t?t:void 0,p=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(p,s),i.info?.(`[canvas] Joined canvas=${p} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),p}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,isCanvasActive:o,getGatewaySessionKey:i}=e;let r=0;return async(s,l)=>{if(o){const e=i?.(s);if(!o(e))return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."}}const c=l,d=c.id,p=c.root,u="panel";let m=c.components;if("string"==typeof m)try{m=JSON.parse(m)}catch{}if(!(d&&p&&Array.isArray(m)&&m.length))return{success:!1,error:"id, root, and components (array) are required"};const h=normalizeComponents(m);if(!t.validateComponentCount(h))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const g=c.data;let y=e.getActiveSessionId?.(s);for(let t=0;!y&&t<3;t++)await delay(400),y=e.getActiveSessionId?.(s);if(!y&&e.waitForAgentSync){try{await e.waitForAgentSync()}catch{}y=e.getActiveSessionId?.(s)}if(!y)return n.warn?.(`[canvas] No active session for toolCallId=${s} after 3 polls, render_ui not ready`),{success:!1,retryable:!0,error:"Canvas session not ready yet (agent just connected) — retry render_ui shortly; do NOT fall back to plain text."};const f=e.getGatewaySessionKey?.(s),v=await ensureRelayConnected(e,y,s,f),x=t.has(d,v??void 0),b=Date.now(),C=t.getRelayClient();v&&C&&b-r>6e4&&C.createSnapshot(v,"Pre-agent edit").then(()=>{r=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),x?t.update(d,h,v,p,g):t.create(v??`_fallback_${Date.now()}`,d,u,h,{origin:{kind:"agent"},rootComponentId:p,dataModel:g});const S=h.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:x?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:u,componentCount:h.length,componentTypes:S}}),n.debug?.(`[canvas] ${x?"Updated":"Created"} surface=${d} components=${h.length}`);const T={success:!0,surfaceId:d,presentation:u,isUpdate:x,activeSurfaces:v?t.getAllActive(v).map(e=>e.surfaceId):[d]};return g&&(T.dataModel=g),T}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI — Google A2UI v0.8\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.**\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n\nParameters:\n- `id`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- `root`: The ID of the top-level component (usually a Column).\n- `components`: Flat array of objects. **Each object MUST have `id` and `component`**:\n `{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}`\n The `component` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- `data` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same `id` updates existing surface; different `id` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: `{"literalString": "Hello"}`\n- Data binding: `{"path": "/fieldName"}` (reads from the `data` model)\n\nShorthand: `sv("text")` means `{"literalString": "text"}` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n`{Text: {text: sv("Title"), usageHint: "h2"}}`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}`\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n`{Icon: {name: sv("check")}}`\n\n**Video** — Embed a video.\n`{Video: {url: sv("https://youtube.com/...")}}`\n\n**AudioPlayer** — Audio playback.\n`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}`\n\n**Row** — Horizontal layout. Children referenced by ID.\n`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n`{Column: {children: {explicitList: ["child1", "child2"]}}}`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}`\n\n**Card** — Container with a single child.\n`{Card: {child: "contentColumnId"}}`\n\n**Tabs** — Tabbed container.\n`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}`\n\n**Divider** — Horizontal or vertical separator.\n`{Divider: {axis: "horizontal"}}`\n\n**Modal** — Dialog with trigger.\n`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}`\n\n**Button** — Clickable with action.\n`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}`\n\n**TextField** — Text input with data binding.\n`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}`\n\n**DateTimeInput** — Date/time picker.\n`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}`\n\n**MultipleChoice** — Selection from options.\n`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}`\n\n**Slider** — Numeric range input.\n`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}`\n\n**Chart** (BeeOS extension) — Data visualization.\n`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition Patterns\n\n**Simple text display:**\n```json\n{"id":"s1","root":"r","components":[\n {"id":"t","component":{"Text":{"text":{"literalString":"Hello World"},"usageHint":"h2"}}},\n {"id":"r","component":{"Column":{"children":{"explicitList":["t"]}}}}\n]}\n```\n\n**Dashboard with KPI cards:**\n```json\n{"id":"dashboard","root":"root","components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Dashboard"},"usageHint":"h2"}}},\n {"id":"rev-val","component":{"Text":{"text":{"literalString":"$1.2M"},"usageHint":"h2"}}},\n {"id":"rev-lbl","component":{"Text":{"text":{"literalString":"Revenue"},"usageHint":"caption"}}},\n {"id":"rev-col","component":{"Column":{"children":{"explicitList":["rev-lbl","rev-val"]},"alignment":"center"}}},\n {"id":"rev-card","component":{"Card":{"child":"rev-col"}}},\n {"id":"users-val","component":{"Text":{"text":{"literalString":"8,432"},"usageHint":"h2"}}},\n {"id":"users-lbl","component":{"Text":{"text":{"literalString":"Users"},"usageHint":"caption"}}},\n {"id":"users-col","component":{"Column":{"children":{"explicitList":["users-lbl","users-val"]},"alignment":"center"}}},\n {"id":"users-card","component":{"Card":{"child":"users-col"}}},\n {"id":"kpi-row","component":{"Row":{"children":{"explicitList":["rev-card","users-card"]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","kpi-row"]}}}}\n]}\n```\n\n**Table layout (Column + Row + Text):**\n```json\n{"id":"users-table","root":"root","components":[\n {"id":"h-name","component":{"Text":{"text":{"literalString":"Name"},"usageHint":"caption"}}},\n {"id":"h-role","component":{"Text":{"text":{"literalString":"Role"},"usageHint":"caption"}}},\n {"id":"header","component":{"Row":{"children":{"explicitList":["h-name","h-role"]}}}},\n {"id":"div","component":{"Divider":{"axis":"horizontal"}}},\n {"id":"r1-name","component":{"Text":{"text":{"literalString":"Alice"},"usageHint":"body"}}},\n {"id":"r1-role","component":{"Text":{"text":{"literalString":"Engineer"},"usageHint":"body"}}},\n {"id":"row1","component":{"Row":{"children":{"explicitList":["r1-name","r1-role"]}}}},\n {"id":"r2-name","component":{"Text":{"text":{"literalString":"Bob"},"usageHint":"body"}}},\n {"id":"r2-role","component":{"Text":{"text":{"literalString":"Designer"},"usageHint":"body"}}},\n {"id":"row2","component":{"Row":{"children":{"explicitList":["r2-name","r2-role"]}}}},\n {"id":"table-col","component":{"Column":{"children":{"explicitList":["header","div","row1","row2"]}}}},\n {"id":"root","component":{"Card":{"child":"table-col"}}}\n]}\n```\n\n**Form with data binding:**\n```json\n{"id":"settings","root":"root","data":{"/name":"","/email":"","/notify":false},"components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Settings"},"usageHint":"h3"}}},\n {"id":"f-name","component":{"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"},"type":"shortText"}}},\n {"id":"f-email","component":{"TextField":{"label":{"literalString":"Email"},"text":{"path":"/email"},"type":"shortText"}}},\n {"id":"f-notify","component":{"CheckBox":{"label":{"literalString":"Email notifications"},"value":{"path":"/notify"}}}},\n {"id":"btn-text","component":{"Text":{"text":{"literalString":"Save"},"usageHint":"body"}}},\n {"id":"btn","component":{"Button":{"child":"btn-text","action":{"name":"save_settings","context":[{"key":"name","value":{"path":"/name"}},{"key":"email","value":{"path":"/email"}},{"key":"notify","value":{"path":"/notify"}}]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","f-name","f-email","f-notify","btn"]}}}}\n]}\n```\n\n**Chart:**\n```json\n{"id":"sales-chart","root":"root","components":[\n {"id":"chart","component":{"Chart":{"chartType":{"literalString":"bar"},"title":{"literalString":"Monthly Sales"},"labels":["Jan","Feb","Mar"],"datasets":[{"label":"Revenue","data":[100,200,150]}]}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["chart"]}}}}\n]}\n```\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. Each component in the array MUST be: {"id":"x","component":{"TypeName":{props}}} — the "component" wrapper key is required. render_ui uses Google A2UI v0.8 format: flat component array with id references. Components: Text (usageHint: h1-h5/caption/body), Image, Icon, Video, AudioPlayer, Row (horizontal layout, children.explicitList), Column (vertical layout, children.explicitList), List, Card (single child), Tabs (tabItems), Divider, Modal, Button (child + action), TextField, CheckBox, MultipleChoice, Slider, DateTimeInput, Chart (BeeOS extension: chartType, labels, datasets). StringValue: {"literalString":"text"} or {"path":"/binding"}. root param = top-level component ID. Tables: Card > Column > Row > Text. Forms: TextField/CheckBox/Slider + Button with action.context paths. Chart: chartType(bar/line/pie/area/scatter/doughnut/radar/funnel/gauge/candlestick/sparkline), labels, datasets:[{label,data,color?}]. Candlestick: [{open,high,low,close}]. Gauge: 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 and provide the full updated component tree.";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 a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{afterEach,describe,expect,it,vi}from"vitest";import{ACPServer}from"./acp-server.js";import{CanvasStateManager}from"./canvas/state.js";import{createRenderUiHandler}from"./canvas/tools.js";const gatewayMock=vi.hoisted(()=>({instances:[]}));function makeConfig(){return{agentGatewayUrl:"https://agent-gw.example.test",keyFile:"/tmp/test.key",bridge:{url:"wss://bridge.example.test",service:"acp",shell:{enabled:!1}},gateway:{url:"ws://127.0.0.1:18789",token:"test-token",protocol:3,clientId:"test-client",agentId:"main"},retry:{baseMs:1,maxMs:10,maxAttempts:0},log:{enabled:!1,verbose:!1,dir:"/tmp"},promptTimeoutMs:1e3,terminalWsPort:0,terminalWsAllowedOrigins:[],streamIdleTimeoutMs:1e3,streamStaleTimeoutMs:1e3,cronSignalWindowMs:1e3,cronMaxDurationDefaultMs:3e5,cronMaxDurationCapMs:18e5,canvasRelayUrl:"wss://canvas-relay.example.test",canvasEnabled:!0,openclawCanvasCompat:!1,messageBus:{enabled:!1,serviceUrl:"",url:"",principalId:"",a2aEnabled:!1}}}function makeLogger(){return{info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()}}function mockRelay(){return{connected:!0,ensureCanvas:vi.fn().mockResolvedValue("canvas-test"),createSnapshot:vi.fn().mockResolvedValue(void 0)}}function renderParams(){return{id:"world-cup-groups",root:"root",components:[{id:"root",component:{Text:{text:{literalString:"Group A"}}}}]}}function makeServerOpts(e,t){return{stateManager:e,logger:makeLogger(),writeObs:vi.fn(),getActiveSessionId:e=>t.getSessionIdForToolCall(e),getGatewaySessionKey:e=>t.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>t.waitForAgentMapping()}}vi.mock("./gateway-client.js",()=>({GatewayClient:class{connected=!1;request=vi.fn();handlers=new Map;constructor(){gatewayMock.instances.push(this)}get isConnected(){return this.connected}on(e,t){const a=this.handlers.get(e)??[];return a.push(t),this.handlers.set(e,a),this}async start(){}stop(){this.connected=!1}emit(e,t){for(const a of this.handlers.get(e)??[])a(t)}}}));const SESSION_KEY="agent:main:s1",TOOL_CALL_ID="call_00_DWmRWzffCw61OKMxB4hG0823";describe("canvas render_ui across plugin re-register (singleton ACPServer)",()=>{afterEach(()=>{vi.useRealTimers()}),it("survives a re-register: the singleton server is reused, so the session table is preserved",async()=>{const e=new CanvasStateManager;e.setRelayClient(mockRelay());const t=new ACPServer(makeConfig(),makeLogger(),void 0,vi.fn(),"inst_test");t.core.agentMapping=new Map([["main",{id:"main",instanceId:"inst_test",name:"main"}]]),t.setPendingToolCallSessionKey(SESSION_KEY),expect(t.getSessionIdForToolCall(TOOL_CALL_ID)).toBeTruthy();const a=createRenderUiHandler(makeServerOpts(e,t)),s=await a(TOOL_CALL_ID,renderParams());expect(s.success).toBe(!0),expect(t.getSessionIdForToolCall(TOOL_CALL_ID)).toBeTruthy();const n=createRenderUiHandler(makeServerOpts(e,t)),r=await n(TOOL_CALL_ID,renderParams());expect(r.success).toBe(!0),expect(r.surfaceId).toBe("world-cup-groups")}),it("cold start: render_ui recovers once agent-sync lands the session (no fixed-window giveup)",async()=>{const e=new CanvasStateManager;e.setRelayClient(mockRelay());let t=!1;const a=vi.fn(()=>t?"session-after-sync":void 0),s=vi.fn(async()=>{t=!0}),n={stateManager:e,logger:makeLogger(),writeObs:vi.fn(),getActiveSessionId:a,getGatewaySessionKey:()=>{},waitForAgentSync:s},r=createRenderUiHandler(n);vi.useFakeTimers();const o=r(TOOL_CALL_ID,renderParams());await vi.advanceTimersByTimeAsync(1300);const i=await o;expect(s).toHaveBeenCalled(),expect(i.success).toBe(!0),expect(i.surfaceId).toBe("world-cup-groups")}),it("last resort: still returns a RETRYABLE 'not ready' when the session never binds",async()=>{const e=vi.fn(()=>{}),t=vi.fn(async()=>{}),a={stateManager:new CanvasStateManager,logger:makeLogger(),writeObs:vi.fn(),getActiveSessionId:e,getGatewaySessionKey:()=>{},waitForAgentSync:t},s=createRenderUiHandler(a);vi.useFakeTimers();const n=s(TOOL_CALL_ID,renderParams());await vi.advanceTimersByTimeAsync(1300);const r=await n;expect(t).toHaveBeenCalledTimes(1),expect(r.success).toBe(!1),expect(r.retryable).toBe(!0),expect(String(r.error)).toMatch(/not ready/i)})});
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageClient}from"@beeos-ai/message-sdk";import{extractChatPrompt}from"@beeos-ai/message-sdk/chat-envelope";import{createTokenProvider}from"@beeos-ai/message-sdk/auth";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,_initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async o=>{const{createPrivateKey:a,sign:r}=await import("node:crypto"),t=a({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(r(null,Buffer.from(o),t))}}}let unhandledRejectionObserverInstalled=!1;function installUnhandledRejectionObserver(e,o){unhandledRejectionObserverInstalled||(unhandledRejectionObserverInstalled=!0,process.on("unhandledRejection",a=>{const r=a instanceof Error?a.message:String(a);e.logger.error?.("[beeos-claw] unhandledRejection:",a),o({component:"plugin",domain:"fatal",name:"unhandled_rejection",severity:"error",summary:r,error:{code:"unhandled_rejection",message:r}})}))}function initializePlugin(e){let o,a;try{o=e.runtime?.config?.loadConfig?.()}catch{}try{a=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:o})}catch(o){return e.logger.error?.("[beeos-claw] Failed to resolve config:",o),null}const r=a.config,t=buildThirdPartyChannelSet(o);t.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...t].join(", ")}`);const s=r.log.enabled?createObsEventWriter({dir:r.log.dir,redaction:{gatewayToken:r.gateway.token},logger:e.logger}):()=>{};installUnhandledRejectionObserver(e,s),s({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),s({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:r,sources:a.sources,openclawConfigPath:a.openclawConfigPath}});for(const o of a.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:o.severity,summary:o.message,error:{code:o.code,message:o.message,nextSteps:o.nextSteps}}),"error"===o.severity?e.logger.error?.(`[beeos-claw] ${o.message}`):e.logger.warn?.(`[beeos-claw] ${o.message}`);if(a.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),r.keyFile)try{agentKeys=loadAgentKeyPair(r.keyFile);const o=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${o})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${o}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&r.agentGatewayUrl&&(setUploadDeps({gatewayUrl:r.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:r,writeObs:s,thirdPartyChannels:t,openclawConfig:o}}function registerUploadTools(e,o,a){const{cfg:r,writeObs:t}=o;cleanupUploadToolResultCache().catch(o=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(o)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(o,a,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} params=${JSON.stringify(a)}`);const n=await executeUpload(a,r,agentKeys,s);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} ok=${n.ok}`),t({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:o,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),(async a=>{try{await writeUploadToolResult(o,BEEOS_UPLOAD_TOOL_NAME,a),await cleanupUploadToolResultCache()}catch(a){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${o}: ${String(a)}`)}return a})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,a){await handleFileStage(e,a,o.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,o){handleStatusRequest(e,o,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}})}function registerServices(e,o,a){const{cfg:r,writeObs:t,thirdPartyChannels:s}=o,n=agentKeys?(e,o)=>agentFetch(e,agentKeys,o):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0,l=new ACPServer(r,e.logger,t,n,i,s);a.current=l,e.registerService({id:"beeos-acp",async start(){const o=Date.now(),a=_pluginRegisteredAt?o-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${a}ms, bridge.url=${r.bridge.url||"(empty)"}`),!r.bridge.url&&r.agentGatewayUrl&&n){const o=Date.now(),a=await discoverBridgeFromGW(r.agentGatewayUrl,n);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-o}ms result=${a?.url??"null"}`),a)r.bridge.url=a.url,writeDiscoveryCache({bridge:{url:a.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${a.url}`);else{const o=readDiscoveryCache();o?.bridge?.url?(r.bridge.url=o.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${o.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(r.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-o}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const g=new TaskHandleRegistry;let c=null;const{enabled:d,url:p,principalId:u,a2aEnabled:f}=r.messageBus;let y;if(r.agentGatewayUrl&&agentKeys){const e=agentKeyPairToIdentity(agentKeys);y=createTokenProvider({agentGatewayUrl:r.agentGatewayUrl,identity:e})}if(d&&(y||r.messageBus.serviceUrl)){c=new MessageClient({serviceUrl:r.messageBus.serviceUrl||"https://msg.invalid",centrifugoUrl:p,tokenProvider:y,logger:e.logger});const o=u,a=async o=>{c.once("connect",()=>{l.setMessageClient(c,o),e.logger.info?.("[beeos-claw] MessageClient connected (principalId=%s, via=%s)",o,y?"gateway":"direct")}),await c.connect({identityId:o})};e.registerService({id:"beeos-message-bus",async start(){let r=o;if(r||(r=process.env.BEEOS_PRINCIPAL_ID||""),!r&&l)try{await l.waitForAgentMapping(),r=l.getDefaultPlatformAgent()?.instanceId||""}catch(o){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",o)}r?await a(r):l?(e.logger.info?.("[beeos-claw] Message bus deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const o=l.getDefaultPlatformAgent()?.instanceId;o?a(o).catch(o=>{e.logger.error?.("[beeos-claw] Deferred message bus connect failed:",o)}):e.logger.warn?.("[beeos-claw] Agent mapping ready but no principalId, message bus stays disabled")})):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(c?.disconnect()),e.logger.info?.("[beeos-claw] MessageClient disconnected")}});const t=async(o,a)=>{const r=extractChatPrompt(a,{source:o,localAgentId:l.getLocalAgentId()});r&&(e.logger.debug?.(`[beeos-claw] ${o} received (sessionKey=%s, contextId=%s)`,r.sessionKey,r.contextId??"-"),await l.agentSend(r.message,r.files,r.sessionKey,void 0,r.channelId,r.messageId))};let s=!1;c.on("message",o=>{(async()=>{"chat_message"===o.type?await t("chat_message",o):"agent_request"===o.type&&(s||(e.logger.warn?.("[beeos-claw] Received legacy 'agent_request' message — this path is deprecated and will be removed once all upstreams migrate to 'chat_message' (L0 AgentInvocationService)."),s=!0),await t("agent_request",o))})().catch(o=>{e.logger.error?.("[beeos-claw] message handler failed:",o)})})}else d?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",d);if(!1!==f&&r.agentGatewayUrl){const o=e=>l.getGatewaySessionKeyForToolCall(e),a=e=>l.getMentionsForToolCall(e),t=(e,o)=>{l.broadcastNotification(e,o)};registerA2ATools(e,{agentGatewayUrl:r.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},g,o,a,t,()=>l?.getDefaultPlatformAgent()?.instanceId),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(r.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const o=sharedCanvasState;if(r.agentGatewayUrl&&agentKeys){let a;o.setLogger(e.logger);{const e=r.agentGatewayUrl,o=agentKeys;a=async()=>{const a=await agentFetch(`${e}/api/v1/canvas/token`,o,{method:"POST"});if(!a.ok){const e=await a.text().catch(()=>"");throw new Error(`Canvas token returned ${a.status}: ${e}`)}return a.json()}}const t=t=>{const s=new CanvasRelayClient({canvasRelayUrl:r.canvasRelayUrl,agentKeys:agentKeys,agentId:t.id,displayName:t.displayName,avatarUrl:t.avatarUrl,tokenProvider:a,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});s.connect(),o.setRelayClient(s),o.setAgentId(t.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${t.id}, name=${t.name??"none"})`)};e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const o=l.getDefaultPlatformAgent();if(o?.id)return void t(o);e.logger.info?.("[beeos-claw] Canvas Relay deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const o=l.getDefaultPlatformAgent();if(o?.id)try{t(o)}catch(o){e.logger.warn?.(`[beeos-claw] Deferred Canvas Relay start failed: ${o}`)}else e.logger.warn?.("[beeos-claw] Agent mapping ready but no platform agent; Canvas Relay stays disabled")})}catch(o){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${o}`)}},stop(){o.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const a={stateManager:o,logger:e.logger,writeObs:e=>t(e),isCanvasActive:e=>!e||l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},n=new FormSchemaRegistry,i=buildCronSchemas(s,{capMs:r.cronMaxDurationCapMs});n.register(i.create),n.register(i.update);const g=new ResourceDescriptorRegistry;g.register(CRON_RESOURCE);const c=e=>async(o,a,r)=>(l.preMapToolCallSession(o),e(o,a));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:c(createRenderUiHandler(a))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:c(createCloseUiHandler(a))}),l.setCanvasState(o,{schemaRegistry:n,descriptorRegistry:g});const d=l.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:c(createRenderFormHandler({registry:n,builder:d,logger:e.logger}))});const p=l.getResourceRenderer();if(p){generateActionTools(g,e,async(e,o)=>l.gatewayRequest(e,o),p,e.logger)}const u='\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.',f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),y=256,m=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,o)=>{if(o?.sessionKey){if(m.size>=y&&!m.has(o.sessionKey)){const e=m.keys().next().value;void 0!==e&&m.delete(e)}m.set(o.sessionKey,o.channelId)}if(isThirdPartyChannel(s,{sessionKey:o?.sessionKey,channelId:o?.channelId}))return{appendSystemContext:'This conversation is in a third-party channel (Lark/Telegram/Discord/Slack); Canvas surfaces are not visible here. Do NOT use render_ui, close_ui, or render_form. Use built-in tools directly (e.g. the `cron` tool with `action:"add"` for scheduled tasks) and respond with plain text.'};if(!o?.sessionKey)return{appendSystemContext:u};return l.isCanvasActiveForGatewayKey(o.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,o)=>{if(o?.sessionKey&&l.setPendingToolCallSessionKey(o.sessionKey),!f.has(e.toolName))return;const a=o?.sessionKey?m.get(o.sessionKey):void 0;return isThirdPartyChannel(s,{sessionKey:o?.sessionKey,channelId:a})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:o?.sessionKey?l.isCanvasActiveForGatewayKey(o.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,b=createTerminalWebSocketService({config:{port:r.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:r.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:t});if(r.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${r.terminalWsPort}`)},stop(){b.stop(),m.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),o.openclawConfig){const a=mirrorOpenClawConfig(o.openclawConfig);a.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${a.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${a.reason}`)}}const CAPABILITY_REGISTRATION_MODES=new Set(["full","discovery","tool-discovery"]),plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(!e.registrationMode||CAPABILITY_REGISTRATION_MODES.has(e.registrationMode)){_initializing=!0;try{const o=initializePlugin(e);if(!o)return;const a={current:null};registerUploadTools(e,o,a),registerServices(e,o,a),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}else e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`)}};export default plugin;
|
|
1
|
+
import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageClient}from"@beeos-ai/message-sdk";import{extractChatPrompt}from"@beeos-ai/message-sdk/chat-envelope";import{createTokenProvider}from"@beeos-ai/message-sdk/auth";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{buildCronSchemas}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,sharedAcpServer=null,_initializing=!1,_pluginRegisteredAt=0;const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function agentKeyPairToIdentity(e){return{publicKeyBase64:()=>Buffer.from(e.publicKey).toString("base64"),sign:async o=>{const{createPrivateKey:r,sign:a}=await import("node:crypto"),t=r({key:32===e.privateKey.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e.privateKey)]):Buffer.from(e.privateKey),format:"der",type:"pkcs8"});return new Uint8Array(a(null,Buffer.from(o),t))}}}let unhandledRejectionObserverInstalled=!1;function installUnhandledRejectionObserver(e,o){unhandledRejectionObserverInstalled||(unhandledRejectionObserverInstalled=!0,process.on("unhandledRejection",r=>{const a=r instanceof Error?r.message:String(r);e.logger.error?.("[beeos-claw] unhandledRejection:",r),o({component:"plugin",domain:"fatal",name:"unhandled_rejection",severity:"error",summary:a,error:{code:"unhandled_rejection",message:a}})}))}function initializePlugin(e){let o,r;try{o=e.runtime?.config?.loadConfig?.()}catch{}try{r=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:o})}catch(o){return e.logger.error?.("[beeos-claw] Failed to resolve config:",o),null}const a=r.config,t=buildThirdPartyChannelSet(o);t.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...t].join(", ")}`);const s=a.log.enabled?createObsEventWriter({dir:a.log.dir,redaction:{gatewayToken:a.gateway.token},logger:e.logger}):()=>{};installUnhandledRejectionObserver(e,s),s({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),s({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:a,sources:r.sources,openclawConfigPath:r.openclawConfigPath}});for(const o of r.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:o.severity,summary:o.message,error:{code:o.code,message:o.message,nextSteps:o.nextSteps}}),"error"===o.severity?e.logger.error?.(`[beeos-claw] ${o.message}`):e.logger.warn?.(`[beeos-claw] ${o.message}`);if(r.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.keyFile)try{agentKeys=loadAgentKeyPair(a.keyFile);const o=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${o})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${o}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&a.agentGatewayUrl&&(setUploadDeps({gatewayUrl:a.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:a,writeObs:s,thirdPartyChannels:t,openclawConfig:o}}function registerUploadTools(e,o,r){const{cfg:a,writeObs:t}=o;cleanupUploadToolResultCache().catch(o=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(o)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(o,r,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} params=${JSON.stringify(r)}`);const n=await executeUpload(r,a,agentKeys,s);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} ok=${n.ok}`),t({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:o,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),(async r=>{try{await writeUploadToolResult(o,BEEOS_UPLOAD_TOOL_NAME,r),await cleanupUploadToolResultCache()}catch(r){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${o}: ${String(r)}`)}return r})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,r){await handleFileStage(e,r,o.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,o){handleStatusRequest(e,o,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}})}function registerServices(e,o,r){const{cfg:a,writeObs:t,thirdPartyChannels:s}=o,n=agentKeys?(e,o)=>agentFetch(e,agentKeys,o):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;sharedAcpServer||(sharedAcpServer=new ACPServer(a,e.logger,t,n,i,s));const l=sharedAcpServer;r.current=l,e.registerService({id:"beeos-acp",async start(){const o=Date.now(),r=_pluginRegisteredAt?o-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${r}ms, bridge.url=${a.bridge.url||"(empty)"}`),!a.bridge.url&&a.agentGatewayUrl&&n){const o=Date.now(),r=await discoverBridgeFromGW(a.agentGatewayUrl,n);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-o}ms result=${r?.url??"null"}`),r)a.bridge.url=r.url,writeDiscoveryCache({bridge:{url:r.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${r.url}`);else{const o=readDiscoveryCache();o?.bridge?.url?(a.bridge.url=o.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${o.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(a.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-o}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const g=new TaskHandleRegistry;let c=null;const{enabled:d,url:p,principalId:u,a2aEnabled:f}=a.messageBus;let y;if(a.agentGatewayUrl&&agentKeys){const e=agentKeyPairToIdentity(agentKeys);y=createTokenProvider({agentGatewayUrl:a.agentGatewayUrl,identity:e})}if(d&&(y||a.messageBus.serviceUrl)){c=new MessageClient({serviceUrl:a.messageBus.serviceUrl||"https://msg.invalid",centrifugoUrl:p,tokenProvider:y,logger:e.logger});const o=u,r=async o=>{c.once("connect",()=>{l.setMessageClient(c,o),e.logger.info?.("[beeos-claw] MessageClient connected (principalId=%s, via=%s)",o,y?"gateway":"direct")}),await c.connect({identityId:o})};e.registerService({id:"beeos-message-bus",async start(){let a=o;if(a||(a=process.env.BEEOS_PRINCIPAL_ID||""),!a&&l)try{await l.waitForAgentMapping(),a=l.getDefaultPlatformAgent()?.instanceId||""}catch(o){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",o)}a?await r(a):l?(e.logger.info?.("[beeos-claw] Message bus deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const o=l.getDefaultPlatformAgent()?.instanceId;o?r(o).catch(o=>{e.logger.error?.("[beeos-claw] Deferred message bus connect failed:",o)}):e.logger.warn?.("[beeos-claw] Agent mapping ready but no principalId, message bus stays disabled")})):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(c?.disconnect()),e.logger.info?.("[beeos-claw] MessageClient disconnected")}});const t=async(o,r)=>{const a=extractChatPrompt(r,{source:o,localAgentId:l.getLocalAgentId()});a&&(e.logger.debug?.(`[beeos-claw] ${o} received (sessionKey=%s, contextId=%s)`,a.sessionKey,a.contextId??"-"),await l.agentSend(a.message,a.files,a.sessionKey,void 0,a.channelId,a.messageId))};let s=!1;c.on("message",o=>{(async()=>{"chat_message"===o.type?await t("chat_message",o):"agent_request"===o.type&&(s||(e.logger.warn?.("[beeos-claw] Received legacy 'agent_request' message — this path is deprecated and will be removed once all upstreams migrate to 'chat_message' (L0 AgentInvocationService)."),s=!0),await t("agent_request",o))})().catch(o=>{e.logger.error?.("[beeos-claw] message handler failed:",o)})})}else d?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",d);if(!1!==f&&a.agentGatewayUrl){const o=e=>l.getGatewaySessionKeyForToolCall(e),r=e=>l.getMentionsForToolCall(e),t=(e,o)=>{l.broadcastNotification(e,o)};registerA2ATools(e,{agentGatewayUrl:a.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},g,o,r,t,()=>l?.getDefaultPlatformAgent()?.instanceId),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(a.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const o=sharedCanvasState;if(a.agentGatewayUrl&&agentKeys){let r;o.setLogger(e.logger);{const e=a.agentGatewayUrl,o=agentKeys;r=async()=>{const r=await agentFetch(`${e}/api/v1/canvas/token`,o,{method:"POST"});if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`Canvas token returned ${r.status}: ${e}`)}return r.json()}}const t=t=>{const s=new CanvasRelayClient({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,agentId:t.id,displayName:t.displayName,avatarUrl:t.avatarUrl,tokenProvider:r,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});s.connect(),o.setRelayClient(s),o.setAgentId(t.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${t.id}, name=${t.name??"none"})`)};e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const o=l.getDefaultPlatformAgent();if(o?.id)return void t(o);e.logger.info?.("[beeos-claw] Canvas Relay deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const o=l.getDefaultPlatformAgent();if(o?.id)try{t(o)}catch(o){e.logger.warn?.(`[beeos-claw] Deferred Canvas Relay start failed: ${o}`)}else e.logger.warn?.("[beeos-claw] Agent mapping ready but no platform agent; Canvas Relay stays disabled")})}catch(o){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${o}`)}},stop(){o.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const r={stateManager:o,logger:e.logger,writeObs:e=>t(e),isCanvasActive:e=>!e||l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},n=new FormSchemaRegistry,i=buildCronSchemas(s,{capMs:a.cronMaxDurationCapMs});n.register(i.create),n.register(i.update);const g=new ResourceDescriptorRegistry;g.register(CRON_RESOURCE);const c=e=>async(o,r,a)=>(l.preMapToolCallSession(o),e(o,r));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:c(createRenderUiHandler(r))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:c(createCloseUiHandler(r))}),l.setCanvasState(o,{schemaRegistry:n,descriptorRegistry:g});const d=l.getFormBuilder();d&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:c(createRenderFormHandler({registry:n,builder:d,logger:e.logger}))});const p=l.getResourceRenderer();if(p){generateActionTools(g,e,async(e,o)=>l.gatewayRequest(e,o),p,e.logger)}const u='\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.',f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),y=256,m=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,o)=>{if(o?.sessionKey){if(m.size>=y&&!m.has(o.sessionKey)){const e=m.keys().next().value;void 0!==e&&m.delete(e)}m.set(o.sessionKey,o.channelId)}if(isThirdPartyChannel(s,{sessionKey:o?.sessionKey,channelId:o?.channelId}))return{appendSystemContext:'This conversation is in a third-party channel (Lark/Telegram/Discord/Slack); Canvas surfaces are not visible here. Do NOT use render_ui, close_ui, or render_form. Use built-in tools directly (e.g. the `cron` tool with `action:"add"` for scheduled tasks) and respond with plain text.'};if(!o?.sessionKey)return{appendSystemContext:u};return l.isCanvasActiveForGatewayKey(o.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+u}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+u}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,o)=>{if(o?.sessionKey&&l.setPendingToolCallSessionKey(o.sessionKey),!f.has(e.toolName))return;const r=o?.sessionKey?m.get(o.sessionKey):void 0;return isThirdPartyChannel(s,{sessionKey:o?.sessionKey,channelId:r})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:o?.sessionKey?l.isCanvasActiveForGatewayKey(o.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,b=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:t});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){b.stop(),m.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),o.openclawConfig){const r=mirrorOpenClawConfig(o.openclawConfig);r.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${r.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${r.reason}`)}}const CAPABILITY_REGISTRATION_MODES=new Set(["full","discovery","tool-discovery"]),plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(!e.registrationMode||CAPABILITY_REGISTRATION_MODES.has(e.registrationMode)){_initializing=!0;try{const o=initializePlugin(e);if(!o)return;const r={current:null};registerUploadTools(e,o,r),registerServices(e,o,r),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}else e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`)}};export default plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
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>",
|