beeos-claw 0.5.30 → 0.5.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{describe,expect,it,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),s=vi.fn().mockReturnValue(!0),n={sendSessionUpdate:s,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=new AcpGatewayEvents({state:t,transport:n,config:{gateway:{agentId:"main"},canvasEnabled:!0},logger:e,writeObs:vi.fn(),completePrompt:vi.fn(),failPrompt:vi.fn(),schedulePromptTimeout:vi.fn(),maybeCompletePromptFromChatState:vi.fn()}),o={sessionId:"s1",gatewaySessionKey:"main/s1",agentId:"main",currentRunId:"run-1",pendingPromptId:1,promptDone:!1,createdAt:Date.now(),chatReplayDedupKeys:new Set};return t.sessions.set(o.sessionId,o),t.promptsByRunId.set("run-1",o),{events:a,sendSessionUpdate:s}}function sentUpdates(e){return e.mock.calls.map(e=>e[1])}describe("gateway-events file artifact presentation",()=>{it("presents structured files from chat.toolResult content wrappers as chat resource links",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,state:"delta",message:{role:"assistant",timestamp:Date.now(),content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",result:{content:JSON.stringify({result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})}}]}};e.events.handleChatEvent(t),await Promise.resolve();const s=sentUpdates(e.sendSessionUpdate);expect(s.some(e=>"tool_call_update"===e.sessionUpdate)).toBe(!0),expect(s).toContainEqual(expect.objectContaining({sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}))})});
1
+ import{describe,expect,it,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),s=vi.fn().mockReturnValue(!0),n={sendSessionUpdate:s,sendResult:vi.fn(),sendAcpError:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn(),setSessionUpdateObserver:vi.fn(),isReady:()=>!0,get forwardControl(){return{forwardThinking:!0,forwardToolCalls:!0}}},o=new AcpGatewayEvents({state:t,transport:n,config:{gateway:{agentId:"main"},canvasEnabled:!0},logger:e,writeObs:vi.fn(),completePrompt:vi.fn(),failPrompt:vi.fn(),schedulePromptTimeout:vi.fn(),maybeCompletePromptFromChatState:vi.fn()}),a={sessionId:"s1",gatewaySessionKey:"main/s1",agentId:"main",currentRunId:"run-1",pendingPromptId:1,promptDone:!1,createdAt:Date.now(),chatReplayDedupKeys:new Set};return t.sessions.set(a.sessionId,a),t.promptsByRunId.set("run-1",a),{events:o,sendSessionUpdate:s}}function sentUpdates(e){return e.mock.calls.map(e=>e[1])}async function flushAsyncToolResult(){await Promise.resolve(),await new Promise(e=>setImmediate(e)),await Promise.resolve()}function screenshotPayload(){return{status:"completed",result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]}}function expectToolUpdateResourceLink(e){expect(e).toContainEqual(expect.objectContaining({sessionUpdate:"tool_call_update",content:expect.arrayContaining([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}))}function expectPresenterResourceLink(e){expect(e).toContainEqual(expect.objectContaining({sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}))}describe("gateway-events file artifact presentation",()=>{it("presents structured files from chat.toolResult content wrappers as chat resource links",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,state:"delta",message:{role:"assistant",timestamp:Date.now(),content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",result:{content:JSON.stringify(screenshotPayload())}}]}};e.events.handleChatEvent(t),await flushAsyncToolResult();const s=sentUpdates(e.sendSessionUpdate);expectToolUpdateResourceLink(s),expectPresenterResourceLink(s)}),it("presents structured files from agent assistant toolResult blocks",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,stream:"assistant",ts:Date.now(),data:{content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",result:{content:JSON.stringify(screenshotPayload())}}]}};e.events.handleAgentEvent(t),await flushAsyncToolResult();const s=sentUpdates(e.sendSessionUpdate);expectToolUpdateResourceLink(s),expectPresenterResourceLink(s)}),it("presents structured files from live tool result events",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,stream:"tool",ts:Date.now(),data:{phase:"result",toolCall:{id:"tc-1",name:"beeos_get_task"},result:{content:JSON.stringify(screenshotPayload())}}};e.events.handleAgentEvent(t),await flushAsyncToolResult();const s=sentUpdates(e.sendSessionUpdate);expectToolUpdateResourceLink(s),expectPresenterResourceLink(s)}),it("does not present text-only beeos-file URI tool output as a resource link",async()=>{const e=makeHarness(),t={runId:"run-1",sessionKey:"main/s1",seq:1,state:"delta",message:{role:"assistant",timestamp:Date.now(),content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",text:"beeos-file://file-1"}]}};e.events.handleChatEvent(t),await flushAsyncToolResult();const s=sentUpdates(e.sendSessionUpdate);expect(s).not.toContainEqual(expect.objectContaining({sessionUpdate:"agent_message_chunk",content:expect.objectContaining({type:"resource_link"})})),expect(s).toContainEqual(expect.objectContaining({sessionUpdate:"tool_call_update",content:[{type:"content",content:{type:"text",text:"beeos-file://file-1"}}]}))})});
@@ -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{presentFileArtifacts}from"./file-artifact-presenter.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("string"==typeof t){const e=parseJsonObject(t);return e?extractResourceLinksFromRawResult(e):void 0}if(!isRecord(t))return;if(!Array.isArray(t.files)){for(const e of["content","output","result","details"]){const s=t[e];if(s===t)continue;const n=extractResourceLinksFromRawResult(s);if(n?.length)return n}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}function parseJsonObject(t){const e=t.trim();if(e.startsWith("{"))try{const t=JSON.parse(e);return isRecord(t)?t:void 0}catch{return}}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;schedulePromptTimeout;maybeCompletePromptFromChatState;agentFetchFn;getMessageClient;getMsgPrincipalId;gatewayRequest;getCanvasState;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.schedulePromptTimeout=t.schedulePromptTimeout,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.getMessageClient=t.getMessageClient,this.getMsgPrincipalId=t.getMsgPrincipalId,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,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,"timeout"))},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)}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);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);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=extractResourceLinksFromRawResult(n),c=buildToolCallUpdatePayload({toolCallId:t,toolName:s,outputText:a,rawOutput:n,resourceLinks:l,timestamp:r});this.sendTurnUpdate(e,c)&&e.assistantMq?.push(c),this.presentToolResultFiles(e,t,l).catch(t=>{this.logger.warn?.(`[gateway-events] File artifact presentation failed: ${t instanceof Error?t.message:t}`)});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)}}}}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)}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(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)}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);if(i){const t=extractResourceLinksFromRawResult(r);n(buildToolCallUpdatePayload({toolCallId:o,toolName:a,outputText:i,rawOutput:r,resourceLinks:t,timestamp:s})),this.presentToolResultFiles(e,o,t).catch(t=>{this.logger.warn?.(`[gateway-events] File artifact presentation failed: ${t instanceof Error?t.message:t}`)})}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)}}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){const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.completePrompt(e,t||"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 p=buildToolCallUpdatePayload({toolCallId:r,toolName:i,outputText:h.join("\n"),rawOutput:d,resourceLinks:g,timestamp:s});this.sendTurnUpdate(t,p)&&t.assistantMq?.push(p),await this.presentToolResultFiles(t,r,g)}async presentToolResultFiles(t,e,s){s?.length&&await presentFileArtifacts(t,e,s,{config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}})}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);const s=this.state.runIdToCronJobId.get(e);s&&(t.cronJobId=s),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.presentedFileArtifactKeys=new Set,t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),this.schedulePromptTimeout(t)}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),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.getMessageClient?.();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.messages.send({conversationId:a,id:r,type:"agent_reply",content:{text:o,cron_job_id:n,session_key:t.sessionKey??void 0}}),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{normalizeToolResultForAcpUpdate}from"./tool-result-payload-strategies.js";import{presentFileArtifacts}from"./file-artifact-presenter.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"))}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 o=roleFromSessionUpdate(e);o&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[o]=s)}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;schedulePromptTimeout;maybeCompletePromptFromChatState;agentFetchFn;getMessageClient;getMsgPrincipalId;gatewayRequest;getCanvasState;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.schedulePromptTimeout=t.schedulePromptTimeout,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.getMessageClient=t.getMessageClient,this.getMsgPrincipalId=t.getMsgPrincipalId,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,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,"timeout"))},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)}resolveToolCallId(t,e,s){const o=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(o){const e=this.state.toolCallIdMap.get(o);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(o,o),t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o)}const n=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n}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 o of s.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const o={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,o)&&e.assistantMq?.push(o);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=resolveToolArgs(n),a=dedupKey("chat:tool_start",t,JSON.stringify(o));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:o,timestamp:r});this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=n.result,a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(o)||Array.isArray(o)?JSON.stringify(o,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),this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:o,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)});continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(n.text);if(!s)continue;const o=classifyHeartbeatFilter(s);if(o.filtered){this.logHeartbeatSkip({source:"chat-text",reason:o.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)}}}}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)}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),o=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const n=this.state.extractAgentAssistantDelta(e,t.data??{});if(n){const a=normalizeTransportText(n);if(a){const n=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(n);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:n,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"}`),o({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 n=asString(t.type);if("thinking"===n){e.hasAgentThinkingStream=!0;const o=normalizeText(t.thinking)??normalizeText(t.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const n=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);o(buildToolCallPayload({toolCallId:n,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const n=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&&this.emitNormalizedToolResult(e,{toolCallId:n,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:o}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)});continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const o=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const n=t.data,a="string"==typeof n?.phase?n.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,n,"start"),a=asString(n?.name)??asString(n?.toolName)??asString(n?.tool_name)??"tool",r=resolveToolArgs(n);o(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=n?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(n?.name)??asString(n?.toolName)??"tool",r={...n,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);o(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("end"===o){const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.completePrompt(e,t||"end_turn")}else if("cancelled"===o||"cancel"===o)this.completePrompt(e,"cancelled");else if("error"===o){const o=s?.error,n=o&&"object"==typeof o?o:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof o?o:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n?JSON.stringify(n):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 o=e.data,n=o?.toolCall,a=("string"==typeof n?.id?n.id:void 0)??("string"==typeof o?.toolCallId?o.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof n?.name?n.name:void 0)??("string"==typeof o?.name?o.name:void 0)??"tool",l="string"==typeof o?.text?o.text:void 0,d=a??r,c=o?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:d,toolName:i,rawText:l,rawResult:c,timestamp:s})}async emitNormalizedToolResult(t,e){const s=await normalizeToolResultForAcpUpdate({toolCallId:e.normalizeToolCallId??e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult});if(!s.outputText&&!s.resourceLinks?.length)return;const o=buildToolCallUpdatePayload({toolCallId:e.toolCallId,toolName:e.toolName,outputText:s.outputText,rawOutput:e.rawResult,resourceLinks:s.resourceLinks,timestamp:e.timestamp});(e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}))(o),await this.presentToolResultFiles(t,e.toolCallId,s.resourceLinks)}async presentToolResultFiles(t,e,s){s?.length&&await presentFileArtifacts(t,e,s,{config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}})}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);const s=this.state.runIdToCronJobId.get(e);s&&(t.cronJobId=s),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.presentedFileArtifactKeys=new Set,t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),this.schedulePromptTimeout(t)}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),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 o=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(o&&(e.sessionId=o),("string"!=typeof e.name||!e.name)&&"string"==typeof t.jobId&&t.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),o=s?.jobs;if(Array.isArray(o))for(const s of o)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 n=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(n,{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",{}),o=t?.jobs;if(Array.isArray(o))for(const t of o){if(!t||"object"!=typeof t)continue;const o=t;if(o.id===e){s=o;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const o="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(o)){const t=decodeBeeosChannelId(o);return void(t&&this.setCronChannelCacheLocal(e,t))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const n=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(n&&"string"==typeof n.kind?n.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(o,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.getMessageClient?.();if(!e)return;const s=this.getMsgPrincipalId?.();if(!s)return;const o="string"==typeof t.jobId?t.jobId:void 0;if(!o)return;const n=t.summary;let a=this.cronChannelCache?.get(o);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===o){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.messages.send({conversationId:a,id:r,type:"agent_reply",content:{text:n,cron_job_id:o,session_key:t.sessionKey??void 0}}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${o}`)}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=${o} err=${e}`),void this.cronChannelCache?.delete(o);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 +1 @@
1
- import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{stripTransportMetadata}from"../message-filter.js";export function reconcileMissingArtifacts(t,e){const{session:s,turnId:n,messageId:o,runId:i}=e;s.agentId&&(s.hasAgentAssistantStream&&s.hasAgentThinkingStream&&s.hasAgentToolStartStream&&s.hasAgentToolResultStream||setImmediate(()=>{if(t.isSessionAlive(s.sessionId))try{const e=findEntryByMessageId(t.localHistory.readEntries({agentId:s.agentId,sessionKey:s.gatewaySessionKey,sessionId:s.sessionId}),o);if(!e)return;const r=Array.isArray(e.content)?e.content:[];if(0===r.length)return;const a="number"==typeof e.timestamp?new Date(e.timestamp).toISOString():"string"==typeof e.timestamp?e.timestamp:void 0;let l=0;const c=e=>{void 0===e._turnId&&(e._turnId=n),t.transport.sendSessionUpdate(s.sessionId,e,{turnId:n})&&(l+=1)};for(const t of r){if(!isRecord(t))continue;const e=asString(t.type);if(e){if("text"===e&&!s.hasAgentAssistantStream){const e=asString(t.text);if(!e)continue;const s=stripTransportMetadata(e);if(!s)continue;c({sessionUpdate:"agent_message_chunk",content:{type:"text",text:s},...a?{timestamp:a}:{}});continue}if("thinking"===e&&!s.hasAgentThinkingStream){const e=asString(t.thinking)??asString(t.text);if(!e)continue;c({sessionUpdate:"agent_thought_chunk",content:{type:"text",text:e},...a?{timestamp:a}:{}});continue}if("toolCall"===e&&!s.hasAgentToolStartStream){const e=resolveToolCallId(t),s=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",n=resolveToolArgs(t);c(buildToolCallPayload({toolCallId:e,toolName:s,args:n,timestamp:a}));continue}if("toolResult"===e&&!s.hasAgentToolResultStream){const e=resolveToolCallId(t),s=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",n=t.result,o=asString(t.text)??asString(t.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!o)continue;c(buildToolCallUpdatePayload({toolCallId:e,toolName:s,outputText:o,rawOutput:n,resourceLinks:void 0,timestamp:a}));continue}}}l>0&&(t.logger.info?.(`[missing-artifacts] reconciled ${l} block(s) sessionId=${s.sessionId} runId=${i??"n/a"} messageId=${o}`),t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.emitted",severity:"info",payload:{sessionId:s.sessionId,runId:i,messageId:o,emitted:l}}))}catch(e){t.logger.warn?.(`[missing-artifacts] reconcile failed sessionId=${s.sessionId} messageId=${o}: ${e instanceof Error?e.message:String(e)}`),t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.failed",severity:"warn",payload:{sessionId:s.sessionId,messageId:o,error:e instanceof Error?e.message:String(e)}})}else t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.skipped_stale",severity:"debug",payload:{sessionId:s.sessionId,messageId:o,runId:i}})}))}function findEntryByMessageId(t,e){for(let s=t.length-1;s>=0;s--){const n=t[s];if(!n)continue;const o=n.__openclaw;if(isRecord(o)&&o.id===e)return n;if(n.id===e)return n}}function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function asString(t){if("string"==typeof t&&t.length>0)return t}function resolveToolCallId(t){const e=asString(t.toolCallId)??asString(t.tool_call_id)??asString(t.id);return e||`reconcile_${hashStable(JSON.stringify(t))}`}function resolveToolArgs(t){const e=[t.arguments,t.args,t.input];for(const t of e){if(isRecord(t))return t;if("string"==typeof t)try{const e=JSON.parse(t);if(isRecord(e))return e}catch{}}return{}}function hashStable(t){let e=5381;for(let s=0;s<t.length;s++)e=(e<<5)+e+t.charCodeAt(s)|0;return(e>>>0).toString(16)}
1
+ import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{normalizeToolResultForAcpUpdate}from"./tool-result-payload-strategies.js";import{stripTransportMetadata}from"../message-filter.js";export function reconcileMissingArtifacts(t,e){const{session:s,turnId:o,messageId:n,runId:i}=e;s.agentId&&(s.hasAgentAssistantStream&&s.hasAgentThinkingStream&&s.hasAgentToolStartStream&&s.hasAgentToolResultStream||setImmediate(()=>{(async()=>{if(t.isSessionAlive(s.sessionId))try{const e=findEntryByMessageId(t.localHistory.readEntries({agentId:s.agentId,sessionKey:s.gatewaySessionKey,sessionId:s.sessionId}),n);if(!e)return;const r=Array.isArray(e.content)?e.content:[];if(0===r.length)return;const a="number"==typeof e.timestamp?new Date(e.timestamp).toISOString():"string"==typeof e.timestamp?e.timestamp:void 0;let l=0;const c=e=>{void 0===e._turnId&&(e._turnId=o),t.transport.sendSessionUpdate(s.sessionId,e,{turnId:o})&&(l+=1)};for(const t of r){if(!isRecord(t))continue;const e=asString(t.type);if(e){if("text"===e&&!s.hasAgentAssistantStream){const e=asString(t.text);if(!e)continue;const s=stripTransportMetadata(e);if(!s)continue;c({sessionUpdate:"agent_message_chunk",content:{type:"text",text:s},...a?{timestamp:a}:{}});continue}if("thinking"===e&&!s.hasAgentThinkingStream){const e=asString(t.thinking)??asString(t.text);if(!e)continue;c({sessionUpdate:"agent_thought_chunk",content:{type:"text",text:e},...a?{timestamp:a}:{}});continue}if("toolCall"===e&&!s.hasAgentToolStartStream){const e=resolveToolCallId(t),s=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",o=resolveToolArgs(t);c(buildToolCallPayload({toolCallId:e,toolName:s,args:o,timestamp:a}));continue}if("toolResult"===e&&!s.hasAgentToolResultStream){const e=resolveToolCallId(t),s=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",o=t.result,n=asString(t.text)??asString(t.result)??(isRecord(o)||Array.isArray(o)?JSON.stringify(o,null,2):void 0);if(!n)continue;const i=await normalizeToolResultForAcpUpdate({toolCallId:e,toolName:s,rawText:n,rawResult:o});if(!i.outputText&&!i.resourceLinks?.length)continue;c(buildToolCallUpdatePayload({toolCallId:e,toolName:s,outputText:i.outputText,rawOutput:o,resourceLinks:i.resourceLinks,timestamp:a}));continue}}}l>0&&(t.logger.info?.(`[missing-artifacts] reconciled ${l} block(s) sessionId=${s.sessionId} runId=${i??"n/a"} messageId=${n}`),t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.emitted",severity:"info",payload:{sessionId:s.sessionId,runId:i,messageId:n,emitted:l}}))}catch(e){t.logger.warn?.(`[missing-artifacts] reconcile failed sessionId=${s.sessionId} messageId=${n}: ${e instanceof Error?e.message:String(e)}`),t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.failed",severity:"warn",payload:{sessionId:s.sessionId,messageId:n,error:e instanceof Error?e.message:String(e)}})}else t.writeObs({component:"bridge-core",domain:"lifecycle",name:"reconcile.missing_artifacts.skipped_stale",severity:"debug",payload:{sessionId:s.sessionId,messageId:n,runId:i}})})()}))}function findEntryByMessageId(t,e){for(let s=t.length-1;s>=0;s--){const o=t[s];if(!o)continue;const n=o.__openclaw;if(isRecord(n)&&n.id===e)return o;if(o.id===e)return o}}function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function asString(t){if("string"==typeof t&&t.length>0)return t}function resolveToolCallId(t){const e=asString(t.toolCallId)??asString(t.tool_call_id)??asString(t.id);return e||`reconcile_${hashStable(JSON.stringify(t))}`}function resolveToolArgs(t){const e=[t.arguments,t.args,t.input];for(const t of e){if(isRecord(t))return t;if("string"==typeof t)try{const e=JSON.parse(t);if(isRecord(e))return e}catch{}}return{}}function hashStable(t){let e=5381;for(let s=0;s<t.length;s++)e=(e<<5)+e+t.charCodeAt(s)|0;return(e>>>0).toString(16)}
@@ -1 +1 @@
1
- import{describe,it,expect,vi}from"vitest";import{reconcileMissingArtifacts}from"./missing-artifacts-reconciler.js";function makeLogger(){return{info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()}}describe("reconcileMissingArtifacts — stale guard",()=>{it("skips dispatch when isSessionAlive returns false",async()=>{const e=vi.fn(),s=vi.fn(),t=vi.fn(),n=makeLogger(),a={sessionId:"s-stale",gatewaySessionKey:"agent:main:s1",agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set};reconcileMissingArtifacts({localHistory:{readEntries:e},transport:{sendSessionUpdate:s},logger:n,writeObs:t,isSessionAlive:()=>!1},{session:a,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),expect(s).not.toHaveBeenCalled(),expect(e).not.toHaveBeenCalled(),expect(t).toHaveBeenCalledWith(expect.objectContaining({name:"reconcile.missing_artifacts.skipped_stale",payload:expect.objectContaining({sessionId:"s-stale",messageId:"msg-1",runId:"run-1"})}))}),it("fast-paths (no setImmediate) when every stream flag is set",async()=>{const e=vi.fn(()=>!0),s=vi.fn(),t=vi.fn(),n=vi.fn(),a={sessionId:"s-fast",gatewaySessionKey:"agent:main:s1",agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set,hasAgentAssistantStream:!0,hasAgentThinkingStream:!0,hasAgentToolStartStream:!0,hasAgentToolResultStream:!0};reconcileMissingArtifacts({localHistory:{readEntries:s},transport:{sendSessionUpdate:t},logger:makeLogger(),writeObs:n,isSessionAlive:e},{session:a,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),expect(e).not.toHaveBeenCalled(),expect(s).not.toHaveBeenCalled(),expect(t).not.toHaveBeenCalled(),expect(n).not.toHaveBeenCalled()}),it("bails out synchronously when agentId is missing",async()=>{const e=vi.fn(()=>!0),s=vi.fn(),t=vi.fn(),n=vi.fn(),a={sessionId:"s-no-agent",gatewaySessionKey:"agent:main:s1",agentId:"",createdAt:Date.now(),chatReplayDedupKeys:new Set};reconcileMissingArtifacts({localHistory:{readEntries:s},transport:{sendSessionUpdate:t},logger:makeLogger(),writeObs:n,isSessionAlive:e},{session:a,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),expect(e).not.toHaveBeenCalled(),expect(s).not.toHaveBeenCalled(),expect(t).not.toHaveBeenCalled()})});
1
+ import{describe,it,expect,vi}from"vitest";import{reconcileMissingArtifacts}from"./missing-artifacts-reconciler.js";function makeLogger(){return{info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()}}describe("reconcileMissingArtifacts — stale guard",()=>{it("skips dispatch when isSessionAlive returns false",async()=>{const e=vi.fn(),t=vi.fn(),s=vi.fn(),n=makeLogger(),i={sessionId:"s-stale",gatewaySessionKey:"agent:main:s1",agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set};reconcileMissingArtifacts({localHistory:{readEntries:e},transport:{sendSessionUpdate:t},logger:n,writeObs:s,isSessionAlive:()=>!1},{session:i,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),expect(t).not.toHaveBeenCalled(),expect(e).not.toHaveBeenCalled(),expect(s).toHaveBeenCalledWith(expect.objectContaining({name:"reconcile.missing_artifacts.skipped_stale",payload:expect.objectContaining({sessionId:"s-stale",messageId:"msg-1",runId:"run-1"})}))}),it("fast-paths (no setImmediate) when every stream flag is set",async()=>{const e=vi.fn(()=>!0),t=vi.fn(),s=vi.fn(),n=vi.fn(),i={sessionId:"s-fast",gatewaySessionKey:"agent:main:s1",agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set,hasAgentAssistantStream:!0,hasAgentThinkingStream:!0,hasAgentToolStartStream:!0,hasAgentToolResultStream:!0};reconcileMissingArtifacts({localHistory:{readEntries:t},transport:{sendSessionUpdate:s},logger:makeLogger(),writeObs:n,isSessionAlive:e},{session:i,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),expect(e).not.toHaveBeenCalled(),expect(t).not.toHaveBeenCalled(),expect(s).not.toHaveBeenCalled(),expect(n).not.toHaveBeenCalled()}),it("bails out synchronously when agentId is missing",async()=>{const e=vi.fn(()=>!0),t=vi.fn(),s=vi.fn(),n=vi.fn(),i={sessionId:"s-no-agent",gatewaySessionKey:"agent:main:s1",agentId:"",createdAt:Date.now(),chatReplayDedupKeys:new Set};reconcileMissingArtifacts({localHistory:{readEntries:t},transport:{sendSessionUpdate:s},logger:makeLogger(),writeObs:n,isSessionAlive:e},{session:i,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),expect(e).not.toHaveBeenCalled(),expect(t).not.toHaveBeenCalled(),expect(s).not.toHaveBeenCalled()}),it("reconciles persisted tool result resource links through the shared normalizer",async()=>{const e=vi.fn().mockReturnValue([{__openclaw:{id:"msg-1"},timestamp:"2026-07-02T14:15:22.658Z",content:[{type:"toolResult",id:"tc-1",name:"beeos_get_task",text:JSON.stringify({content:JSON.stringify({status:"completed",result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})})}]}]),t=vi.fn().mockReturnValue(!0),s=vi.fn(),n={sessionId:"s-artifact",gatewaySessionKey:"agent:main:s1",agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set};reconcileMissingArtifacts({localHistory:{readEntries:e},transport:{sendSessionUpdate:t},logger:makeLogger(),writeObs:s,isSessionAlive:()=>!0},{session:n,turnId:"turn-1",messageId:"msg-1",runId:"run-1"}),await new Promise(e=>setImmediate(e)),await Promise.resolve();const i=t.mock.calls.map(e=>e[1]);expect(i).toContainEqual(expect.objectContaining({sessionUpdate:"tool_call_update",toolCallId:"tc-1",name:"beeos_get_task",content:expect.arrayContaining([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])})),expect(s).toHaveBeenCalledWith(expect.objectContaining({name:"reconcile.missing_artifacts.emitted",payload:expect.objectContaining({emitted:1})}))})});
@@ -1 +1 @@
1
- import{readUploadToolResult,writeEnrichmentCache}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{tryExtractLocalFiles}from"../upload/local-file-extractors.js";import{uploadLocalFile,getUploadDeps}from"../upload/upload-file.js";import{asTrimmedNonEmptyString as asStr,isPlainRecord as isRecord}from"../utils/text.js";const strategies=[{name:"beeos_upload_file_cache",canHandle:t=>t.toolName===BEEOS_UPLOAD_TOOL_NAME,resolve:strategyUploadCache},{name:"passthrough_gateway_result",canHandle:()=>!0,resolve:async t=>strategyPassthrough(t)}];export function registerToolResultStrategy(t,e){if(e?.prepend)strategies.unshift(t);else{const e=Math.max(0,strategies.length-1);strategies.splice(e,0,t)}}export function getToolResultStrategies(){return strategies}export async function resolveToolResultBlocks(t){let e=[];for(const o of strategies)if(o.canHandle(t))try{const n=await o.resolve(t);if(n&&n.length>0){e=n;break}}catch{}return enrichWithLocalFileUploads(t,e)}async function enrichWithLocalFileUploads(t,e){const o=getUploadDeps();if(!o)return e;const n=tryExtractLocalFiles(t);if(0===n.length)return e;const r=[...e],l=[];for(const e of n)try{o.logger?.debug?.(`[enrich] Uploading local file ${e.filePath} for toolCallId=${t.toolCallId}`);const n=await uploadLocalFile(e.filePath,o.gatewayUrl,o.keys,{fileNameOverride:e.suggestedName});o.logger?.info?.(`[enrich] Uploaded: fileId=${n.fileId} toolCallId=${t.toolCallId}`);const s={uri:n.uri,name:n.name,...n.mimeType?{mimeType:n.mimeType}:{}};l.push(s),r.push({type:"content",content:{type:"resource_link",...s}}),r.push({type:"content",content:{type:"text",text:`[Auto-uploaded: ${n.name}. Do NOT call beeos_upload_file for this file.]`}})}catch(t){o.logger?.warn?.(`[enrich] Upload failed for ${e.filePath}: ${t instanceof Error?t.message:String(t)}, skipping`)}return l.length>0&&writeEnrichmentCache(t.toolCallId,l).catch(e=>{o.logger?.warn?.(`[enrich] Cache write failed for toolCallId=${t.toolCallId}: ${e instanceof Error?e.message:String(e)}`)}),r}async function strategyUploadCache(t){if(t.toolName!==BEEOS_UPLOAD_TOOL_NAME)return null;let e;try{e=await readUploadToolResult(t.toolCallId)}catch{return null}if(!e)return null;const o=pickUploadPayload(e);return buildToolResultSessionContentBlocks(t.toolName,o??e)}function strategyPassthrough(t){if(t.rawResult&&isRecord(t.rawResult)){const e=buildToolResultSessionContentBlocks(t.toolName,t.rawResult);if(e.length>0)return e}const e=t.rawText?.trim();if(!e)return[];try{const o=JSON.parse(e);if(isRecord(o)){const e=buildToolResultSessionContentBlocks(t.toolName,o);if(e.length>0)return e}}catch{}return[{type:"content",content:{type:"text",text:e}}]}function isUploadPayload(t){return!!isRecord(t)&&(!0===t.ok||!1===t.ok||"files"in t||Array.isArray(t.content)&&t.content.length>0)}function pickUploadPayload(t){if(isRecord(t)){for(const e of["output","result","details"]){const o=t[e];if(isUploadPayload(o))return o;if(isRecord(o))for(const t of["output","result","details"])if(isUploadPayload(o[t]))return o[t]}return Array.isArray(t.content)&&t.content.length>0||isUploadPayload(t)?t:void 0}}export function buildToolResultSessionContentBlocks(t,e){const o=[];if(!isRecord(e))return"string"==typeof e&&e.trim()&&o.push({type:"content",content:{type:"text",text:e.trim()}}),o;const n=buildFileResourceBlocks(e),r=asStr(e.content);if(r){const e=buildBlocksFromJsonText(t,r);if(e.length>0)return appendFileBlocks(e,n)}const l=e.content??e.result;if(Array.isArray(l)){for(const t of l){const e=toContentBlock(t);e&&o.push(e)}if(o.length>0)return appendFileBlocks(o,n)}const s=e.result;if(s&&"object"==typeof s&&!Array.isArray(s)){const t=s.content;if(Array.isArray(t)){for(const e of t){const t=toContentBlock(e);t&&o.push(t)}if(o.length>0)return appendFileBlocks(o,n)}}const i=asStr(e.output)??asStr(e.text)??asStr(e.result)??r??asStr(e.message)??asStr(e.error);return i&&o.push({type:"content",content:{type:"text",text:i}}),appendFileBlocks(o,n)}function buildBlocksFromJsonText(t,e){const o=e.trim();if(!o||!o.startsWith("{")&&!o.startsWith("["))return[];try{return buildToolResultSessionContentBlocks(t,JSON.parse(o))}catch{return[]}}function buildFileResourceBlocks(t){if(!Array.isArray(t.files))return[];const e=[];for(const o of t.files){if(!isRecord(o))continue;const t=asStr(o.fileId)??asStr(o.file_id),n=t?`beeos-file://${t}`:asStr(o.uri)??asStr(o.downloadUrl)??asStr(o.url);if(!n)continue;const r=asStr(o.name)??asStr(o.fileName)??asStr(o.file_name),l=asStr(o.mimeType)??asStr(o.mime_type);e.push({type:"content",content:{type:"resource_link",uri:n,...r?{name:r}:{},...l?{mimeType:l}:{}}})}return e}function appendFileBlocks(t,e){if(0===e.length)return t;const o=new Set(e.map(t=>t.content.uri).filter(Boolean));return[...t.filter(t=>!("text"===t.content.type&&t.content.text&&o.has(t.content.text))),...e]}function toContentBlock(t){if(!isRecord(t))return"string"==typeof t&&t.trim()?{type:"content",content:{type:"text",text:t.trim()}}:null;const e=asStr(t.type);if(!e)return null;if("text"===e){const e=asStr(t.text);return e?{type:"content",content:{type:"text",text:e}}:null}if("resource_link"===e){const e=asStr(t.uri);return e?{type:"content",content:{type:"resource_link",uri:e,...t.name?{name:String(t.name)}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{},...t.title?{title:String(t.title)}:{}}}:null}if("image"===e){const e=asStr(t.data),o=asStr(t.uri);return e||o?{type:"content",content:{type:"image",...e?{data:e}:{},...o?{uri:o}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{}}}:null}return null}
1
+ import{readUploadToolResult,writeEnrichmentCache}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{tryExtractLocalFiles}from"../upload/local-file-extractors.js";import{uploadLocalFile,getUploadDeps}from"../upload/upload-file.js";import{asTrimmedNonEmptyString as asStr,isPlainRecord as isRecord}from"../utils/text.js";const strategies=[{name:"beeos_upload_file_cache",canHandle:t=>t.toolName===BEEOS_UPLOAD_TOOL_NAME,resolve:strategyUploadCache},{name:"passthrough_gateway_result",canHandle:()=>!0,resolve:async t=>strategyPassthrough(t)}];export function registerToolResultStrategy(t,e){if(e?.prepend)strategies.unshift(t);else{const e=Math.max(0,strategies.length-1);strategies.splice(e,0,t)}}export function getToolResultStrategies(){return strategies}export async function resolveToolResultBlocks(t){let e=[];for(const o of strategies)if(o.canHandle(t))try{const n=await o.resolve(t);if(n&&n.length>0){e=n;break}}catch{}return enrichWithLocalFileUploads(t,e)}export async function normalizeToolResultForAcpUpdate(t){let e;try{e=await resolveToolResultBlocks(t)}catch{e=t.rawText?[{type:"content",content:{type:"text",text:t.rawText}}]:[]}0===e.length&&t.rawText&&(e=[{type:"content",content:{type:"text",text:t.rawText}}]);const o=[],n=[];for(const t of e){const{content:e}=t;"resource_link"===e.type&&e.uri?n.push({uri:e.uri,name:e.name??"file",...e.mimeType?{mimeType:e.mimeType}:{}}):"text"===e.type&&e.text&&o.push(e.text)}return{blocks:e,outputText:o.join("\n"),rawOutput:t.rawResult,...n.length>0?{resourceLinks:n}:{}}}async function enrichWithLocalFileUploads(t,e){const o=getUploadDeps();if(!o)return e;const n=tryExtractLocalFiles(t);if(0===n.length)return e;const r=[...e],l=[];for(const e of n)try{o.logger?.debug?.(`[enrich] Uploading local file ${e.filePath} for toolCallId=${t.toolCallId}`);const n=await uploadLocalFile(e.filePath,o.gatewayUrl,o.keys,{fileNameOverride:e.suggestedName});o.logger?.info?.(`[enrich] Uploaded: fileId=${n.fileId} toolCallId=${t.toolCallId}`);const s={uri:n.uri,name:n.name,...n.mimeType?{mimeType:n.mimeType}:{}};l.push(s),r.push({type:"content",content:{type:"resource_link",...s}}),r.push({type:"content",content:{type:"text",text:`[Auto-uploaded: ${n.name}. Do NOT call beeos_upload_file for this file.]`}})}catch(t){o.logger?.warn?.(`[enrich] Upload failed for ${e.filePath}: ${t instanceof Error?t.message:String(t)}, skipping`)}return l.length>0&&writeEnrichmentCache(t.toolCallId,l).catch(e=>{o.logger?.warn?.(`[enrich] Cache write failed for toolCallId=${t.toolCallId}: ${e instanceof Error?e.message:String(e)}`)}),r}async function strategyUploadCache(t){if(t.toolName!==BEEOS_UPLOAD_TOOL_NAME)return null;let e;try{e=await readUploadToolResult(t.toolCallId)}catch{return null}if(!e)return null;const o=pickUploadPayload(e);return buildToolResultSessionContentBlocks(t.toolName,o??e)}function strategyPassthrough(t){if(t.rawResult&&isRecord(t.rawResult)){const e=buildToolResultSessionContentBlocks(t.toolName,t.rawResult);if(e.length>0)return e}const e=t.rawText?.trim();if(!e)return[];try{const o=JSON.parse(e);if(isRecord(o)){const e=buildToolResultSessionContentBlocks(t.toolName,o);if(e.length>0)return e}}catch{}return[{type:"content",content:{type:"text",text:e}}]}function isUploadPayload(t){return!!isRecord(t)&&(!0===t.ok||!1===t.ok||"files"in t||Array.isArray(t.content)&&t.content.length>0)}function pickUploadPayload(t){if(isRecord(t)){for(const e of["output","result","details"]){const o=t[e];if(isUploadPayload(o))return o;if(isRecord(o))for(const t of["output","result","details"])if(isUploadPayload(o[t]))return o[t]}return Array.isArray(t.content)&&t.content.length>0||isUploadPayload(t)?t:void 0}}export function buildToolResultSessionContentBlocks(t,e){const o=[];if(!isRecord(e))return"string"==typeof e&&e.trim()&&o.push({type:"content",content:{type:"text",text:e.trim()}}),o;const n=buildFileResourceBlocks(e),r=asStr(e.content);if(r){const e=buildBlocksFromJsonText(t,r);if(e.length>0)return appendFileBlocks(e,n)}const l=e.content??e.result;if(Array.isArray(l)){for(const t of l){const e=toContentBlock(t);e&&o.push(e)}if(o.length>0)return appendFileBlocks(o,n)}const s=e.result;if(s&&"object"==typeof s&&!Array.isArray(s)){const t=s.content;if(Array.isArray(t)){for(const e of t){const t=toContentBlock(e);t&&o.push(t)}if(o.length>0)return appendFileBlocks(o,n)}}const i=asStr(e.output)??asStr(e.text)??asStr(e.result)??r??asStr(e.message)??asStr(e.error);return i&&o.push({type:"content",content:{type:"text",text:i}}),appendFileBlocks(o,n)}function buildBlocksFromJsonText(t,e){const o=e.trim();if(!o||!o.startsWith("{")&&!o.startsWith("["))return[];try{return buildToolResultSessionContentBlocks(t,JSON.parse(o))}catch{return[]}}function buildFileResourceBlocks(t){if(!Array.isArray(t.files))return[];const e=[];for(const o of t.files){if(!isRecord(o))continue;const t=asStr(o.fileId)??asStr(o.file_id),n=t?`beeos-file://${t}`:asStr(o.uri)??asStr(o.downloadUrl)??asStr(o.url);if(!n)continue;const r=asStr(o.name)??asStr(o.fileName)??asStr(o.file_name),l=asStr(o.mimeType)??asStr(o.mime_type);e.push({type:"content",content:{type:"resource_link",uri:n,...r?{name:r}:{},...l?{mimeType:l}:{}}})}return e}function appendFileBlocks(t,e){if(0===e.length)return t;const o=new Set(e.map(t=>t.content.uri).filter(Boolean));return[...t.filter(t=>!("text"===t.content.type&&t.content.text&&o.has(t.content.text))),...e]}function toContentBlock(t){if(!isRecord(t))return"string"==typeof t&&t.trim()?{type:"content",content:{type:"text",text:t.trim()}}:null;const e=asStr(t.type);if(!e)return null;if("text"===e){const e=asStr(t.text);return e?{type:"content",content:{type:"text",text:e}}:null}if("resource_link"===e){const e=asStr(t.uri);return e?{type:"content",content:{type:"resource_link",uri:e,...t.name?{name:String(t.name)}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{},...t.title?{title:String(t.title)}:{}}}:null}if("image"===e){const e=asStr(t.data),o=asStr(t.uri);return e||o?{type:"content",content:{type:"image",...e?{data:e}:{},...o?{uri:o}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{}}}:null}return null}
@@ -1 +1 @@
1
- import{describe,expect,it}from"vitest";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";describe("buildToolResultSessionContentBlocks",()=>{it("converts structured files to resource_link blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("does not output duplicate raw URI text when result equals the file URI",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toHaveLength(1),expect(e[0]?.content.type).toBe("resource_link")}),it("keeps ordinary result text alongside structured files",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"Screenshot captured.",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("appends files even when the payload also has typed content blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:[{type:"text",text:"Screenshot captured."}],files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("extracts structured files from a JSON string content wrapper",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:JSON.stringify({result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])})});
1
+ import{describe,expect,it}from"vitest";import{buildToolResultSessionContentBlocks,normalizeToolResultForAcpUpdate}from"./tool-result-payload-strategies.js";describe("buildToolResultSessionContentBlocks",()=>{it("converts structured files to resource_link blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("does not output duplicate raw URI text when result equals the file URI",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toHaveLength(1),expect(e[0]?.content.type).toBe("resource_link")}),it("keeps ordinary result text alongside structured files",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{result:"Screenshot captured.",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("appends files even when the payload also has typed content blocks",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:[{type:"text",text:"Screenshot captured."}],files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]});expect(e).toEqual([{type:"content",content:{type:"text",text:"Screenshot captured."}},{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])}),it("extracts structured files from a JSON string content wrapper",()=>{const e=buildToolResultSessionContentBlocks("beeos_get_task",{content:JSON.stringify({result:"beeos-file://file-1",files:[{uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})});expect(e).toEqual([{type:"content",content:{type:"resource_link",uri:"beeos-file://file-1",name:"screenshot.jpeg",mimeType:"image/jpeg"}}])})}),describe("normalizeToolResultForAcpUpdate",()=>{it("normalizes beeos_get_task wrapped content JSON files into resource links",async()=>{const e=await normalizeToolResultForAcpUpdate({toolCallId:"tc-1",toolName:"beeos_get_task",rawText:JSON.stringify({content:JSON.stringify({status:"completed",result:"beeos-file://abc",files:[{uri:"beeos-file://abc",name:"screenshot.jpeg",mimeType:"image/jpeg"}]})})});expect(e.outputText).toBe(""),expect(e.resourceLinks).toEqual([{uri:"beeos-file://abc",name:"screenshot.jpeg",mimeType:"image/jpeg"}])}),it("does not turn text-only beeos-file URI into a resource link",async()=>{const e=await normalizeToolResultForAcpUpdate({toolCallId:"tc-1",toolName:"beeos_get_task",rawText:"beeos-file://abc"});expect(e.outputText).toBe("beeos-file://abc"),expect(e.resourceLinks).toBeUndefined()})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.5.30",
3
+ "version": "0.5.31",
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>",