beeos-claw 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describe,it,expect,vi}from"vitest";import{AcpGatewayEvents}from"./gateway-events.js";import{AcpGatewaySessionState}from"./session-state.js";function makeHarness(){const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()},t=new AcpGatewaySessionState(e),n={sendSessionUpdate:vi.fn().mockReturnValue(!0),sendResult:vi.fn(),sendAcpError:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn(),setSessionUpdateObserver:vi.fn(),isReady:()=>!0,get forwardControl(){return{forwardThinking:!0,forwardToolCalls:!0}}},a=vi.fn(),o=vi.fn().mockResolvedValue(new Response("{}",{status:200})),s=new Map;return{events:new AcpGatewayEvents({state:t,transport:n,config:{gateway:{agentId:"main"},agentGatewayUrl:"https://agent-gw.example.com"},logger:e,writeObs:vi.fn(),completePrompt:vi.fn(),failPrompt:vi.fn(),maybeCompletePromptFromChatState:vi.fn(),agentFetch:o,gatewayRequest:a,cronChannelCache:s}),state:t,gatewayRequest:a,agentFetch:o,logger:e}}function postedCronData(e){expect(e).toHaveBeenCalledTimes(1);const[t,n]=e.mock.calls[0];expect(t).toBe("https://agent-gw.example.com/api/v1/agent/events");const a=JSON.parse(n.body);return expect(a.type).toBe("cron"),a.data}describe("reportCronEventToAgentGateway — cron job name back-fill",()=>{it("uses payload.name verbatim when present, skipping cron.list",async()=>{const e=makeHarness();await e.events.reportCronEventToAgentGateway({jobId:"job-1",action:"finished",summary:"ok",name:"preset-name"}),expect(e.gatewayRequest).not.toHaveBeenCalled(),expect(postedCronData(e.agentFetch).name).toBe("preset-name")}),it("queries cron.list and posts the resolved name when payload lacks one",async()=>{const e=makeHarness();e.gatewayRequest.mockResolvedValueOnce({jobs:[{id:"other-job",name:"other-name"},{id:"job-2",name:"daily-report"}]}),await e.events.reportCronEventToAgentGateway({jobId:"job-2",action:"finished",summary:"all done"}),expect(e.gatewayRequest).toHaveBeenCalledTimes(1),expect(e.gatewayRequest).toHaveBeenCalledWith("cron.list",{}),expect(postedCronData(e.agentFetch).name).toBe("daily-report")}),it("posts without name (and does not throw) when cron.list rejects",async()=>{const e=makeHarness();e.gatewayRequest.mockRejectedValueOnce(new Error("gateway gone")),await e.events.reportCronEventToAgentGateway({jobId:"job-3",action:"finished"}),expect(e.agentFetch).toHaveBeenCalledTimes(1),expect(postedCronData(e.agentFetch).name).toBeUndefined()}),it("posts without name when cron.list returns but jobId is not in the list",async()=>{const e=makeHarness();e.gatewayRequest.mockResolvedValueOnce({jobs:[{id:"different-job",name:"irrelevant"}]}),await e.events.reportCronEventToAgentGateway({jobId:"job-4",action:"finished"}),expect(e.gatewayRequest).toHaveBeenCalledTimes(1),expect(postedCronData(e.agentFetch).name).toBeUndefined()}),it("skips cron.list lookup entirely when payload has no jobId",async()=>{const e=makeHarness();await e.events.reportCronEventToAgentGateway({action:"added",nextRunAtMs:0}),expect(e.gatewayRequest).not.toHaveBeenCalled(),expect(postedCronData(e.agentFetch).name).toBeUndefined()}),it("does not throw when agentFetch rejects (warn logged)",async()=>{const e=makeHarness();e.agentFetch.mockRejectedValueOnce(new Error("network down")),await expect(e.events.reportCronEventToAgentGateway({jobId:"job-5",action:"finished",name:"n"})).resolves.toBeUndefined(),expect(e.logger.warn).toHaveBeenCalledWith(expect.stringContaining("event report failed"))})}),describe("handleCronEvent — end-to-end happy path",()=>{it("forwards the upstream payload (with name resolved) without dropping existing fields",async()=>{const e=makeHarness();e.gatewayRequest.mockResolvedValueOnce({jobs:[{id:"job-e2e",name:"e2e-name"}]}),e.events.handleCronEvent({jobId:"job-e2e",action:"finished",status:"ok",summary:"Run complete",sessionKey:"agent:main:cron:job-e2e:run:r1",runAtMs:17e11}),await new Promise(e=>setImmediate(e));const t=postedCronData(e.agentFetch);expect(t.jobId).toBe("job-e2e"),expect(t.action).toBe("finished"),expect(t.summary).toBe("Run complete"),expect(t.sessionKey).toBe("agent:main:cron:job-e2e:run:r1"),expect(t.name).toBe("e2e-name")})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash,randomUUID}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata,normalizeTransportText,classifyHeartbeatFilter,previewFilteredText}from"../message-filter.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}function extractResourceLinksFromRawResult(t){if(!isRecord(t)||!Array.isArray(t.files))return;const e=[];for(const s of t.files){if(!isRecord(s))continue;const t=asString(s.uri)??(asString(s.fileId)?`beeos-file://${s.fileId}`:void 0);t&&e.push({uri:t,name:asString(s.name)??"file",...asString(s.mimeType)?{mimeType:asString(s.mimeType)}:{}})}return e.length>0?e:void 0}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=s)}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;onStreamDelta;getMessageSdk;getMsgPrincipalId;gatewayRequest;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.onStreamDelta=t.onStreamDelta,this.getMessageSdk=t.getMessageSdk,this.getMsgPrincipalId=t.getMsgPrincipalId,this.gatewayRequest=t.gatewayRequest,this.cronChannelCache=t.cronChannelCache}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"end_turn"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);return s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),!!t.sessionLoadInProgress||this.transport.sendSessionUpdate(t.sessionId,e,{turnId:s})}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s),this.onStreamDelta?.(t,s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&(e.assistantMq?.push(n),this.onStreamDelta?.(e,n));continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.sendTurnUpdate(e,i)&&(e.assistantMq?.push(i),this.onStreamDelta?.(e,i));continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i);const l=buildToolCallUpdatePayload({toolCallId:t,toolName:s,outputText:a,rawOutput:n,resourceLinks:extractResourceLinksFromRawResult(n),timestamp:r});this.sendTurnUpdate(e,l)&&(e.assistantMq?.push(l),this.onStreamDelta?.(e,l));continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const d={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,d),e.assistantMq?.push(d),this.onStreamDelta?.(e,d)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(this.maybeCompletePromptFromChatState(e,t),!e.promptDone)if("final"===t.state)this.completePrompt(e,t.stopReason||"end_turn");else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.completePrompt(e,"cancelled");else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message;this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t),this.onStreamDelta?.(e,t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&(e.assistantMq?.push(t),this.onStreamDelta?.(e,t))}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&n(buildToolCallUpdatePayload({toolCallId:o,toolName:a,outputText:i,rawOutput:r,resourceLinks:extractResourceLinksFromRawResult(r),timestamp:s}));continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&(e.assistantMq?.push(t),this.onStreamDelta?.(e,t))}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n)this.completePrompt(e,"end_turn");else if("cancelled"===n||"cancel"===n)this.completePrompt(e,"cancelled");else if("error"===n){const n=s?.error,o=n&&"object"==typeof n?n:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?n:void 0)??("string"==typeof o?.message?o.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(o?JSON.stringify(o):void 0)??"gateway lifecycle error",r=formatFriendlyError(a);this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${a} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,r)}}}async handleToolResult(t,e,s){const n=e.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,d=a??r,c=n?.result;let u;try{u=await resolveToolResultBlocks({toolCallId:d,toolName:i,rawText:l,rawResult:c})}catch{u=l?[{type:"content",content:{type:"text",text:l}}]:[]}0===u.length&&l&&(u=[{type:"content",content:{type:"text",text:l}}]);const h=[],g=[];for(const t of u)t.content&&("resource_link"===t.content.type&&t.content.uri?g.push({uri:t.content.uri,name:t.content.name??"file",...t.content.mimeType?{mimeType:t.content.mimeType}:{}}):t.content.text&&h.push(t.content.text));const m=buildToolCallUpdatePayload({toolCallId:r,toolName:i,outputText:h.join("\n"),rawOutput:c,resourceLinks:g,timestamp:s});this.sendTurnUpdate(t,m)&&(t.assistantMq?.push(m),this.onStreamDelta?.(t,m))}adoptBackgroundRun(t,e){this.logger.debug?.(`[gateway-events] Adopting background run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`),t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue)}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{},s="string"==typeof e.sessionKey?e.sessionKey:void 0,n=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(this.config.agentGatewayUrl&&this.agentFetchFn){const t={...e};s&&null==t.sessionKey&&(t.sessionKey=s),n&&(t.sessionId=n);const o=`${this.config.agentGatewayUrl}/api/v1/agent/events`;this.agentFetchFn(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:t})}).catch(t=>{this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)})}"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const d=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:d}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getMessageSdk?.();if(!e)return;const s=this.getMsgPrincipalId?.();if(!s)return;const n="string"==typeof t.jobId?t.jobId:void 0;if(!n)return;const o=t.summary;let a=this.cronChannelCache?.get(n);if(!a&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===n){a=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(!a)return;const r=randomUUID();try{await e.sendToChannel(a,"agent_reply",{text:o,cron_job_id:n,session_key:t.sessionKey??void 0},{inReplyTo:r}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${n}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${a} job=${n} err=${e}`),void this.cronChannelCache?.delete(n);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewFilteredText(t.cumulative),s=void 0!==t.delta?previewFilteredText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
|
|
1
|
+
import{createHash,randomUUID}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata,normalizeTransportText,classifyHeartbeatFilter,previewFilteredText}from"../message-filter.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}function extractResourceLinksFromRawResult(t){if(!isRecord(t)||!Array.isArray(t.files))return;const e=[];for(const s of t.files){if(!isRecord(s))continue;const t=asString(s.uri)??(asString(s.fileId)?`beeos-file://${s.fileId}`:void 0);t&&e.push({uri:t,name:asString(s.name)??"file",...asString(s.mimeType)?{mimeType:asString(s.mimeType)}:{}})}return e.length>0?e:void 0}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=s)}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;onStreamDelta;getMessageSdk;getMsgPrincipalId;gatewayRequest;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.onStreamDelta=t.onStreamDelta,this.getMessageSdk=t.getMessageSdk,this.getMsgPrincipalId=t.getMsgPrincipalId,this.gatewayRequest=t.gatewayRequest,this.cronChannelCache=t.cronChannelCache}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"end_turn"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);return s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),!!t.sessionLoadInProgress||this.transport.sendSessionUpdate(t.sessionId,e,{turnId:s})}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s),this.onStreamDelta?.(t,s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&(e.assistantMq?.push(n),this.onStreamDelta?.(e,n));continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.sendTurnUpdate(e,i)&&(e.assistantMq?.push(i),this.onStreamDelta?.(e,i));continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i);const l=buildToolCallUpdatePayload({toolCallId:t,toolName:s,outputText:a,rawOutput:n,resourceLinks:extractResourceLinksFromRawResult(n),timestamp:r});this.sendTurnUpdate(e,l)&&(e.assistantMq?.push(l),this.onStreamDelta?.(e,l));continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const c={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,c),e.assistantMq?.push(c),this.onStreamDelta?.(e,c)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(this.maybeCompletePromptFromChatState(e,t),!e.promptDone)if("final"===t.state)this.completePrompt(e,t.stopReason||"end_turn");else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.completePrompt(e,"cancelled");else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message;this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t),this.onStreamDelta?.(e,t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&(e.assistantMq?.push(t),this.onStreamDelta?.(e,t))}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&n(buildToolCallUpdatePayload({toolCallId:o,toolName:a,outputText:i,rawOutput:r,resourceLinks:extractResourceLinksFromRawResult(r),timestamp:s}));continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&(e.assistantMq?.push(t),this.onStreamDelta?.(e,t))}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n)this.completePrompt(e,"end_turn");else if("cancelled"===n||"cancel"===n)this.completePrompt(e,"cancelled");else if("error"===n){const n=s?.error,o=n&&"object"==typeof n?n:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?n:void 0)??("string"==typeof o?.message?o.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(o?JSON.stringify(o):void 0)??"gateway lifecycle error",r=formatFriendlyError(a);this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${a} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,r)}}}async handleToolResult(t,e,s){const n=e.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,c=a??r,d=n?.result;let u;try{u=await resolveToolResultBlocks({toolCallId:c,toolName:i,rawText:l,rawResult:d})}catch{u=l?[{type:"content",content:{type:"text",text:l}}]:[]}0===u.length&&l&&(u=[{type:"content",content:{type:"text",text:l}}]);const h=[],g=[];for(const t of u)t.content&&("resource_link"===t.content.type&&t.content.uri?g.push({uri:t.content.uri,name:t.content.name??"file",...t.content.mimeType?{mimeType:t.content.mimeType}:{}}):t.content.text&&h.push(t.content.text));const m=buildToolCallUpdatePayload({toolCallId:r,toolName:i,outputText:h.join("\n"),rawOutput:d,resourceLinks:g,timestamp:s});this.sendTurnUpdate(t,m)&&(t.assistantMq?.push(m),this.onStreamDelta?.(t,m))}adoptBackgroundRun(t,e){this.logger.debug?.(`[gateway-events] Adopting background run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`),t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue)}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};this.config.agentGatewayUrl&&this.agentFetchFn&&this.reportCronEventToAgentGateway(e),"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async reportCronEventToAgentGateway(t){if(!this.config.agentGatewayUrl||!this.agentFetchFn)return;const e={...t},s="string"==typeof t.sessionKey?t.sessionKey:void 0;s&&null==e.sessionKey&&(e.sessionKey=s);const n=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(n&&(e.sessionId=n),("string"!=typeof e.name||!e.name)&&"string"==typeof t.jobId&&t.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),n=s?.jobs;if(Array.isArray(n))for(const s of n)if(isRecord(s)&&s.id===t.jobId&&"string"==typeof s.name&&s.name){e.name=s.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}const o=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:e})})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${e}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${e}`);const c=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:c}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getMessageSdk?.();if(!e)return;const s=this.getMsgPrincipalId?.();if(!s)return;const n="string"==typeof t.jobId?t.jobId:void 0;if(!n)return;const o=t.summary;let a=this.cronChannelCache?.get(n);if(!a&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===n){a=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(!a)return;const r=randomUUID();try{await e.sendToChannel(a,"agent_reply",{text:o,cron_job_id:n,session_key:t.sessionKey??void 0},{inReplyTo:r}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${n}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${a} job=${n} err=${e}`),void this.cronChannelCache?.delete(n);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewFilteredText(t.cumulative),s=void 0!==t.delta?previewFilteredText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
|
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{syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;messageSdk=null;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;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,a){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const r=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:${r}: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),a&&o&&(o.inboundMessageId=a),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:a}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",o.pendingAgentSends.length);const l=await this.gateway.agentSend(c,e,r,t,n);return l?.runId&&(o?(this.state.resetForNewRun(o,o.sessionId,this.transport),o.currentRunId=l.runId,o.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(l.runId,o)):this.state.promptsByRunId.set(l.runId,{gatewaySessionKey:c,channelId:i,inboundMessageId:a})),l}setMessageSdk(e,t){this.messageSdk=e,this.msgPrincipalId=t}async publishAgentReply(e,t,s,n){const i=this.messageSdk,a=this.msgPrincipalId;if(i&&a){if(e&&i.sendToChannel)try{return await i.sendToChannel(e,s,n,{inReplyTo:t}),void this.logger.info?.(`[acp-server] ${s} published channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendToChannel failed channel=${e}: ${t instanceof Error?t.message:String(t)}; attempting persistent fallback`)}if(e&&i.sendChannelMessage)try{return await i.sendChannelMessage(e,{type:s,payload:n,inReplyTo:t,senderId:a}),void this.logger.info?.(`[acp-server] ${s} persisted channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendChannelMessage failed channel=${e}: ${t instanceof Error?t.message:String(t)}; falling back to personal channel`)}await i.sendToPersonal(a,s,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,a){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=a??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const r=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:r,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),maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageSdk:()=>{const e=this.messageSdk;return e?.sendToChannel?{sendToChannel:e.sendToChannel.bind(e)}:null},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.messageSdk||!this.msgPrincipalId)return;const s=e.gatewaySessionKey;if(!s)return;const n=e.channelId,i={session_key:s,update:t};(n&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(n,"agent_reply_delta",i):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply_delta",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,a=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunComplete runId=${e} session_key=${n} channel=${i} in_reply_to=${a??"-"} principal=${this.msgPrincipalId} hasSDK=${!!this.messageSdk}`),!n)return void this.logger.warn?.(`[acp-server] onRunComplete: no session_key for runId=${e}, cannot publish reply`);const r={session_key:n,text:t};this.publishAgentReply(i,a,"agent_reply",r).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,a=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunFailed runId=${e} session_key=${n} channel=${i} in_reply_to=${a??"-"}`),!n)return;const r={session_key:n,error:t||"execution failed"};this.publishAgentReply(i,a,"agent_reply",r).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,s.on("connected",()=>{this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync()}),s.on("disconnected",(...e)=>{const n=String(e[0]??"unknown");if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${n}`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,t))}).catch(()=>{})}}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n;const i=Date.now();try{await s.connect(),this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-i}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-i}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();const t=()=>{if(e+1>=6)return this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — giving up"),void this.core.markAgentSyncDone();const t=Math.min(5e3*2**e,12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>this.tryAgentSync(e+1),t)},s=Date.now();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});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()}}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.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):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`,a=await n(i,"utf-8"),r=JSON.parse(a);if(r.formSchemas?.length){for(const e of r.formSchemas)s.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${r.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance=null,this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
|
|
1
|
+
import{WebSocketServer,WebSocket}from"ws";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{syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;messageSdk=null;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;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,a){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const r=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:${r}: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),a&&o&&(o.inboundMessageId=a),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:a}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",o.pendingAgentSends.length);const l=await this.gateway.agentSend(c,e,r,t,n);return l?.runId&&(o?(this.state.resetForNewRun(o,o.sessionId,this.transport),o.currentRunId=l.runId,o.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(l.runId,o)):this.state.promptsByRunId.set(l.runId,{gatewaySessionKey:c,channelId:i,inboundMessageId:a})),l}setMessageSdk(e,t){this.messageSdk=e,this.msgPrincipalId=t}async publishAgentReply(e,t,s,n){const i=this.messageSdk,a=this.msgPrincipalId;if(i&&a){if(e&&i.sendToChannel)try{return await i.sendToChannel(e,s,n,{inReplyTo:t}),void this.logger.info?.(`[acp-server] ${s} published channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendToChannel failed channel=${e}: ${t instanceof Error?t.message:String(t)}; attempting persistent fallback`)}if(e&&i.sendChannelMessage)try{return await i.sendChannelMessage(e,{type:s,payload:n,inReplyTo:t,senderId:a}),void this.logger.info?.(`[acp-server] ${s} persisted channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendChannelMessage failed channel=${e}: ${t instanceof Error?t.message:String(t)}; falling back to personal channel`)}await i.sendToPersonal(a,s,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,a){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=a??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const r=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:r,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),maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageSdk:()=>{const e=this.messageSdk;return e?.sendToChannel?{sendToChannel:e.sendToChannel.bind(e)}:null},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.messageSdk||!this.msgPrincipalId)return;const s=e.gatewaySessionKey;if(!s)return;const n=e.channelId,i={session_key:s,update:t};(n&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(n,"agent_reply_delta",i):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply_delta",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,a=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunComplete runId=${e} session_key=${n} channel=${i} in_reply_to=${a??"-"} principal=${this.msgPrincipalId} hasSDK=${!!this.messageSdk}`),!n)return void this.logger.warn?.(`[acp-server] onRunComplete: no session_key for runId=${e}, cannot publish reply`);const r={session_key:n,text:t};this.publishAgentReply(i,a,"agent_reply",r).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,a=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunFailed runId=${e} session_key=${n} channel=${i} in_reply_to=${a??"-"}`),!n)return;const r={session_key:n,error:t||"execution failed"};this.publishAgentReply(i,a,"agent_reply",r).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,s.on("connected",()=>{this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync()}),s.on("disconnected",(...e)=>{const n=String(e[0]??"unknown");if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${n}`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,t))}).catch(()=>{})}}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n;const i=Date.now();try{await s.connect(),this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-i}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-i}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();const t=()=>{if(e+1>=6)return this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — giving up"),void this.core.markAgentSyncDone();const t=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();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});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()}}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.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):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`,a=await n(i,"utf-8"),r=JSON.parse(a);if(r.formSchemas?.length){for(const e of r.formSchemas)s.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${r.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance=null,this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
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>",
|