beeos-claw 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/a2a/poll-pending.js +1 -1
- package/dist/a2a/tools.js +1 -1
- package/dist/a2a-orchestrator.js +1 -0
- package/dist/a2a-orchestrator.test.js +1 -0
- package/dist/acp-gateway/bridge-core.js +1 -1
- package/dist/acp-gateway/gateway-events.js +1 -1
- package/dist/acp-gateway/history-replay.js +1 -1
- package/dist/acp-gateway/prompt-resource-resolver.js +1 -1
- package/dist/acp-gateway/session-state.js +1 -1
- package/dist/acp-gateway/session-state.test.js +1 -0
- package/dist/acp-gateway/tool-result-payload-strategies.js +1 -1
- package/dist/acp-gateway/types.js +1 -1
- package/dist/acp-server.js +1 -1
- package/dist/agent-sync.js +1 -1
- package/dist/bridge-discovery.js +1 -0
- package/dist/canvas/action-router.js +1 -0
- package/dist/canvas/index.js +1 -0
- package/dist/canvas/relay-client.js +1 -0
- package/dist/canvas/state.js +1 -0
- package/dist/canvas/state.test.js +1 -0
- package/dist/canvas/tools.js +1 -0
- package/dist/canvas/ws-provider.js +1 -0
- package/dist/canvas-orchestrator.js +1 -0
- package/dist/channel-detection.js +1 -0
- package/dist/config.js +1 -1
- package/dist/device-identity.js +1 -0
- package/dist/discovery-cache.js +1 -0
- package/dist/errors/classify.js +1 -0
- package/dist/gateway-client.js +1 -1
- package/dist/index.js +1 -1
- package/dist/messaging/message-sdk.js +1 -1
- package/dist/surface/cron-resource.js +1 -0
- package/dist/surface/form-surface-builder.js +1 -0
- package/dist/surface/index.js +1 -0
- package/dist/surface/resource-surface-renderer.js +1 -0
- package/dist/terminal/bridge-handler.js +1 -0
- package/dist/terminal/index.js +1 -0
- package/dist/terminal/rpc.js +1 -0
- package/dist/upload/file-stage.js +1 -0
- package/dist/upload/index.js +1 -0
- package/dist/{upload-result-cache.js → upload/result-cache.js} +1 -1
- package/dist/upload/tool.js +1 -0
- package/openclaw.plugin.json +6 -6
- package/package.json +6 -3
- package/dist/canvas-action-router.js +0 -1
- package/dist/canvas-api-client.js +0 -1
- package/dist/canvas-relay-client.js +0 -1
- package/dist/canvas-state.js +0 -1
- package/dist/canvas-tools.js +0 -1
- package/dist/canvas-ws-provider.js +0 -1
- package/dist/cron-resource.js +0 -1
- package/dist/file-stage.js +0 -1
- package/dist/form-surface-builder.js +0 -1
- package/dist/resource-surface-renderer.js +0 -1
- package/dist/terminal-bridge-handler.js +0 -1
- package/dist/terminal-rpc.js +0 -1
- package/dist/upload-tool.js +0 -1
- /package/dist/{a2ui-helpers.js → canvas/a2ui-helpers.js} +0 -0
- /package/dist/{canvas-constants.js → canvas/constants.js} +0 -0
- /package/dist/{action-tool-generator.js → surface/action-tool-generator.js} +0 -0
- /package/dist/{cron-schemas.js → surface/cron-schemas.js} +0 -0
- /package/dist/{form-schema-registry.js → surface/form-schema-registry.js} +0 -0
- /package/dist/{render-form-tool.js → surface/render-form-tool.js} +0 -0
- /package/dist/{resource-descriptor-registry.js → surface/resource-descriptor-registry.js} +0 -0
- /package/dist/{resource-descriptor.js → surface/resource-descriptor.js} +0 -0
- /package/dist/{terminal-session-manager.js → terminal/session-manager.js} +0 -0
- /package/dist/{terminal-websocket.js → terminal/websocket.js} +0 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Add to your OpenClaw configuration (`~/.openclaw/openclaw.json`):
|
|
|
20
20
|
"package": "beeos-claw",
|
|
21
21
|
"enabled": true,
|
|
22
22
|
"config": {
|
|
23
|
-
"
|
|
23
|
+
"agentGatewayUrl": "https://api.beeos.ai",
|
|
24
24
|
"bridge": {
|
|
25
25
|
"url": "wss://bridge.beeos.ai",
|
|
26
26
|
"keyFile": "~/.openclaw/bridge-key.json"
|
|
@@ -46,11 +46,11 @@ Configuration is resolved from three sources in priority order:
|
|
|
46
46
|
2. **Environment variables** — prefixed with `BEEOS_` or `BRIDGE_`
|
|
47
47
|
3. **Defaults**
|
|
48
48
|
|
|
49
|
-
###
|
|
49
|
+
### Agent Gateway
|
|
50
50
|
|
|
51
51
|
| Config Key | Env Var | Default | Description |
|
|
52
52
|
|---|---|---|---|
|
|
53
|
-
| `
|
|
53
|
+
| `agentGatewayUrl` | `AGENT_GATEWAY_URL` | — | **(Required)** Base URL of the Agent Gateway API |
|
|
54
54
|
|
|
55
55
|
### Bridge
|
|
56
56
|
|
|
@@ -117,7 +117,7 @@ Configuration is resolved from three sources in priority order:
|
|
|
117
117
|
"package": "beeos-claw",
|
|
118
118
|
"enabled": true,
|
|
119
119
|
"config": {
|
|
120
|
-
"
|
|
120
|
+
"agentGatewayUrl": "https://api.beeos.ai",
|
|
121
121
|
"bridge": {
|
|
122
122
|
"url": "wss://bridge.beeos.ai",
|
|
123
123
|
"keyFile": "/home/agent/.openclaw/bridge-key.json",
|
package/dist/a2a/poll-pending.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a2aFetch}from"./tools.js";import{dispatchA2ATask}from"./task-executor.js";export async function pollPendingTasks(a,e,t){let s=0;try{const o=await a2aFetch(a,"/api/v1/a2a/tasks/pending");if(!o.ok)return a.logger.warn?.("[beeos-claw] Failed to poll pending tasks: %d %s",o.status,await o.text()),0;const n=
|
|
1
|
+
import{a2aFetch}from"./tools.js";import{dispatchA2ATask}from"./task-executor.js";export async function pollPendingTasks(a,e,t){let s=0;try{const o=await a2aFetch(a,"/api/v1/a2a/tasks/pending");if(!o.ok)return a.logger.warn?.("[beeos-claw] Failed to poll pending tasks: %d %s",o.status,await o.text()),0;const n=await o.json(),i=Array.isArray(n.data)?n.data:[];if(0===i.length)return a.logger.debug?.("[beeos-claw] No pending A2A tasks found on startup"),0;a.logger.info?.("[beeos-claw] Found %d pending A2A tasks to process",i.length);for(const o of i)try{await dispatchA2ATask(a,e,t,o.id,o.message,o.caller_principal_id,o.files),s++,a.logger.info?.("[beeos-claw] Dispatched pending task %s",o.id)}catch(e){a.logger.error?.("[beeos-claw] Failed to dispatch pending task %s:",o.id,e)}}catch(e){a.logger.warn?.("[beeos-claw] Pending task poll failed:",e)}return s}
|
package/dist/a2a/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{agentFetch}from"../agent-auth.js";import{TaskHandleRegistry}from"./task-handle.js";import{resolveAgentIdFromMentions}from"./mention-resolver.js";async function a2aFetch(e,t,a){const n=`${e.
|
|
1
|
+
import{agentFetch}from"../agent-auth.js";import{TaskHandleRegistry}from"./task-handle.js";import{resolveAgentIdFromMentions}from"./mention-resolver.js";async function a2aFetch(e,t,a){const n=`${e.agentGatewayUrl}${t}`;return e.agentKeys?agentFetch(n,e.agentKeys,a):fetch(n,a)}export{a2aFetch};const defaultRegistry=new TaskHandleRegistry;export function registerA2ATools(e,t,a=defaultRegistry,n,r,s){e.registerTool({name:"beeos_discover_agents",description:"Find other agents that can help with a task. Returns a list of agents matching the query. Results include your own agents and publicly available agents.",parameters:{type:"object",properties:{query:{type:"string",description:"What capability or skill you are looking for"}},required:["query"]},async execute(e,a){const{query:n}=a;try{const e=await a2aFetch(t,`/api/v1/a2a/discover?query=${encodeURIComponent(n)}&limit=20`);if(!e.ok)return{content:JSON.stringify({error:`discover failed: ${e.status}`})};const a=await e.json();return{content:JSON.stringify(a,null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}}),e.registerTool({name:"beeos_call_agent",description:"Ask another agent to perform a task. The task is sent asynchronously — returns a task ID immediately, and the result will be delivered via a2a_result message on your personal channel when ready.",parameters:{type:"object",properties:{agentId:{type:"string",description:"The target agent UUID (e.g. d04080d1-...) or display name. Prefer the UUID from discover results or @mention metadata when available."},message:{type:"string",description:"The task description or message to send to the agent"},files:{type:"array",items:{type:"string"},description:"Optional file URLs to include with the request"},contextId:{type:"string",description:"Optional context ID for multi-turn conversations with the same agent"},timeoutMs:{type:"number",description:"Task timeout in milliseconds. Defaults to 600000 (10 minutes). Set higher for long-running tasks."}},required:["agentId","message"]},async execute(e,o){let{agentId:i,message:c,files:l,contextId:d,timeoutMs:g}=o;const u=n?.(e),p=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;if(!p.test(i)){const a=r?.(e);if(a?.length){const e=resolveAgentIdFromMentions(a,i);e&&(t.logger.info?.(`beeos_call_agent: resolved "${i}" → ${e} via mention context`),i=e)}if(!p.test(i))try{const e=await a2aFetch(t,`/api/v1/a2a/discover?query=${encodeURIComponent(i)}&limit=10`);if(e.ok){const a=await e.json(),n=a?.data??[],r=i.toLowerCase(),s=n.find(e=>e.name.toLowerCase()===r)??n.find(e=>e.name.toLowerCase().includes(r));if(!s)return{content:JSON.stringify({error:`Could not resolve agent name "${i}". Available agents: ${n.map(e=>`${e.name} (${e.id})`).join(", ")||"none"}`})};t.logger.info?.(`beeos_call_agent: resolved "${i}" → ${s.id} (${s.name}) via discover`),i=s.id}}catch{}}const y=d||(u?`session:${u}`:"");try{const e=await a2aFetch(t,"/api/v1/a2a/tasks/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({target_agent_id:i,message:c,files:l??[],async:!0,context_id:y,timeout_ms:g&&g>0?g:6e5})});if(!e.ok){const t=await e.text();if(401===e.status||403===e.status){let e={};try{e=JSON.parse(t)}catch{}(e.external_auth_required||e.auth_required)&&s&&s("_beeos/auth_required",{type:"auth_required",target_agent_id:i,target_agent_name:e.agent_name||i,title:"Authentication Required",content:`Agent **${e.agent_name||i}** requires authentication. Please configure credentials in your contacts.`})}return{content:JSON.stringify({error:`send task failed: ${e.status} ${t}`})}}const n=await e.json(),r=n?.data?.id;let o;return r&&(o=a.create(r,i,d,u)),{content:JSON.stringify({...n,task_handle:o},null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}}),e.registerTool({name:"beeos_complete_task",description:"Complete an A2A task and send the result back to the caller agent. Use this when you have finished processing a task received from another agent.",parameters:{type:"object",properties:{taskId:{type:"string",description:"The task ID to complete"},result:{type:"string",description:"The result text to send back to the caller"},error:{type:"string",description:"If the task failed, provide an error message instead of result"}},required:["taskId"]},async execute(e,a){const{taskId:n,result:r,error:s}=a;try{const e=await a2aFetch(t,`/api/v1/a2a/tasks/${encodeURIComponent(n)}/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({result:r??"",error:s??""})});if(!e.ok){const t=await e.text();return{content:JSON.stringify({error:`complete task failed: ${e.status} ${t}`})}}const a=await e.json();return{content:JSON.stringify(a,null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}}),e.registerTool({name:"beeos_task_status",description:"Check the current status and result of an A2A task you previously sent. Accepts either a task_handle (e.g. bth_1) or a raw task ID.",parameters:{type:"object",properties:{taskId:{type:"string",description:"The task handle or task ID to check"}},required:["taskId"]},async execute(e,n){const{taskId:r}=n,s=a.resolveTaskId(r);try{const e=await a2aFetch(t,`/api/v1/a2a/tasks/${encodeURIComponent(s)}`);if(!e.ok)return{content:JSON.stringify({error:`get task failed: ${e.status}`})};const a=await e.json();return{content:JSON.stringify(a,null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}}),e.registerTool({name:"beeos_task_cancel",description:"Cancel an A2A task you previously sent. Only the caller can cancel a task. Accepts either a task_handle (e.g. bth_1) or a raw task ID.",parameters:{type:"object",properties:{taskId:{type:"string",description:"The task handle or task ID to cancel"}},required:["taskId"]},async execute(e,n){const{taskId:r}=n,s=a.resolveTaskId(r);try{const e=await a2aFetch(t,`/api/v1/a2a/tasks/${encodeURIComponent(s)}/cancel`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({})});if(!e.ok){const t=await e.text();return{content:JSON.stringify({error:`cancel task failed: ${e.status} ${t}`})}}const a=await e.json();return{content:JSON.stringify(a,null,2)}}catch(e){return{content:JSON.stringify({error:e instanceof Error?e.message:String(e)})}}}})}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class A2AOrchestrator{pendingA2ARuns=new Map;deps;constructor(e){this.deps=e}trackA2ARun(e,s,t){this.pendingA2ARuns.set(e,{taskId:s,completeFn:t})}cancelA2ARunByTaskId(e){for(const[s,t]of this.pendingA2ARuns)if(t.taskId===e)return this.pendingA2ARuns.delete(s),this.deps.logger.info?.("[a2a-orch] Removed pending A2A run %s for cancelled task %s",s,e),!0;return!1}onRunComplete(e,s){const t=this.pendingA2ARuns.get(e);t&&(this.pendingA2ARuns.delete(e),t.completeFn(s,"").catch(e=>this.deps.logger.warn?.("[a2a-orch] A2A CompleteTask failed for task %s: %s",t.taskId,e)))}onRunFailed(e,s){const t=this.pendingA2ARuns.get(e);t&&(this.pendingA2ARuns.delete(e),t.completeFn("",s).catch(e=>this.deps.logger.warn?.("[a2a-orch] A2A CompleteTask (error) failed for task %s: %s",t.taskId,e)))}emitA2ASystemNotification(e){let s;if(e.targetSessionKey){const t=this.deps.state.findSessionByKey(e.targetSessionKey);s=t?.sessionId,s||this.deps.logger.debug?.("[a2a-orch] emitA2ASystemNotification: targetSessionKey=%s not found, falling back",e.targetSessionKey)}if(!s){const e=this.deps.state.lastActiveSessionId,t=e?this.deps.state.sessions.get(e):void 0;s=t?.sessionId??e}if(!s)return void this.deps.logger.debug?.("[a2a-orch] emitA2ASystemNotification: no active session, dropping");const t={type:e.type,taskId:e.taskId,callerName:e.callerName,taskLink:`/a2a/tasks/${e.taskId}`};this.deps.transport.sendSessionUpdate(s,{sessionUpdate:"system_message_chunk",content:{type:"text",text:e.message},metadata:t})}drainNextDeferredSend(e,s){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),s(t.message,t.attachments,e.gatewaySessionKey,t.inputProvenance).catch(e=>this.deps.logger.warn?.("[a2a-orch] Deferred agentSend failed: %s",e)))}destroy(){this.pendingA2ARuns.clear()}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describe,it,expect,beforeEach,vi}from"vitest";import{A2AOrchestrator}from"./a2a-orchestrator.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";describe("A2AOrchestrator",()=>{let e,t;const n={info:vi.fn(),warn:vi.fn(),debug:vi.fn()},o={sendSessionUpdate:vi.fn().mockReturnValue(!0),sendResult:vi.fn(),sendAcpError:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn()};beforeEach(()=>{t=new AcpGatewaySessionState(n),e=new A2AOrchestrator({state:t,transport:o,logger:n}),vi.clearAllMocks()}),describe("trackA2ARun / onRunComplete",()=>{it("should call completeFn on run completion",async()=>{const t=vi.fn().mockResolvedValue(void 0);e.trackA2ARun("run-1","task-1",t),e.onRunComplete("run-1","Hello, result"),expect(t).toHaveBeenCalledWith("Hello, result","")}),it("should not call completeFn for unknown runId",()=>{const t=vi.fn().mockResolvedValue(void 0);e.trackA2ARun("run-1","task-1",t),e.onRunComplete("unknown-run","text"),expect(t).not.toHaveBeenCalled()})}),describe("onRunFailed",()=>{it("should call completeFn with error",async()=>{const t=vi.fn().mockResolvedValue(void 0);e.trackA2ARun("run-2","task-2",t),e.onRunFailed("run-2","Something went wrong"),expect(t).toHaveBeenCalledWith("","Something went wrong")})}),describe("cancelA2ARunByTaskId",()=>{it("should remove the run and return true",()=>{const t=vi.fn().mockResolvedValue(void 0);e.trackA2ARun("run-3","task-3",t),expect(e.cancelA2ARunByTaskId("task-3")).toBe(!0),e.onRunComplete("run-3","late result"),expect(t).not.toHaveBeenCalled()}),it("should return false for unknown task",()=>{expect(e.cancelA2ARunByTaskId("unknown")).toBe(!1)})}),describe("emitA2ASystemNotification",()=>{it("should send to targeted session",()=>{const n={sessionId:"s1",gatewaySessionKey:"agent:main:s1",agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set};t.sessions.set("s1",n),t.sessionsByGatewayKey.set("agent:main:s1",n),e.emitA2ASystemNotification({type:"a2a_task_notification",taskId:"t1",callerName:"Agent X",message:"New task from Agent X",targetSessionKey:"agent:main:s1"}),expect(o.sendSessionUpdate).toHaveBeenCalledWith("s1",expect.objectContaining({sessionUpdate:"system_message_chunk"}))}),it("should drop when no session available",()=>{e.emitA2ASystemNotification({type:"a2a_task_notification",taskId:"t1",callerName:"Agent X",message:"Dropped"}),expect(o.sendSessionUpdate).not.toHaveBeenCalled()})}),describe("destroy",()=>{it("should clear all pending runs",()=>{e.trackA2ARun("r1","t1",vi.fn().mockResolvedValue(void 0)),e.trackA2ARun("r2","t2",vi.fn().mockResolvedValue(void 0)),e.destroy(),expect(e.cancelA2ARunByTaskId("t1")).toBe(!1),expect(e.cancelA2ARunByTaskId("t2")).toBe(!1)})})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID,createHash}from"node:crypto";import{sanitizePromptPayload,stripTransportMetadata}from"../message-filter.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas-tools.js";import{parseMentionsParam,buildMentionInstruction}from"../a2a/mention-resolver.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,groupIntoTurns}from"./history-replay.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{Errors}from"../errors/index.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS}from"./types.js";const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function isSystemDefaultSessionKey(e){const t=e.split(":");if(t.length<3||"agent"!==t[0])return!1;const s=t.slice(2).join(":");return"main"===s||s===t[1]}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const n=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,t){const s=createHash("sha256").update(`${t}:${e}`).digest("hex").substring(0,32);return`session:${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20,32)}`}function parseAgentIdFromKey(e){const t=e.split(":");return(t.length>=3&&"agent"===t[0]?t[1]:"")||"main"}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;initialized=!1;agentMapping=null;_syncResolvers=[];setAgentMapping(e){this.agentMapping=e;for(const t of this.state.sessions.values())!t.platformAgentId&&t.agentId&&(t.platformAgentId=e.get(t.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[]}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(t=>{this._syncResolvers.push(t),setTimeout(()=>{const e=this._syncResolvers.indexOf(t);-1!==e&&this._syncResolvers.splice(e,1),t()},e)})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;handleSlashCommand=null;onPromptStart=null;instanceId;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway,this.instanceId=e.instanceId||"unknown"}resolvePlatformAgentId(e){return this.agentMapping?.get(e)?.id}getDefaultAgentId(){return this.agentMapping&&this.agentMapping.size>0?this.agentMapping.keys().next().value??"main":"main"}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});try{switch(e.method){case"initialize":this.handleInitialize(e);break;case"session/new":await this.handleSessionNew(e);break;case"session/load":await this.handleSessionLoad(e);break;case"session/list":await this.handleSessionList(e);break;case"session/rename":this.handleSessionRename(e);break;case"session/delete":await this.handleSessionDelete(e);break;case"session/clear":await this.handleSessionClear(e);break;case"session/prompt":await this.handleSessionPrompt(e);break;case"session/set_mode":await this.handleSessionSetMode(e);break;case"session/set_model":await this.handleSessionSetModel(e);break;case"skills/list":await this.handleSkillsList(e);break;case"session/cancel":this.handleSessionCancel(e.id,e.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(e);break;default:e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSessionDeleted=null;onSessionCleared=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/toggle":case"canvas/clear":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const t=e.params,s=t?.surfaceId,n=this.state.resolveSessionFromParams(t);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}n.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const t="string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId(),s=`agent:${t}:${randomUUID()}`,n=deriveSessionId(s,this.instanceId),i="string"==typeof e.params?.cwd?e.params.cwd:".",o=this.resolvePlatformAgentId(t),a={sessionId:n,gatewaySessionKey:s,agentId:t,platformAgentId:o,createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:i};this.state.sessions.set(n,a),this.state.sessionsByGatewayKey.set(s,a),this.transport.sendResult(e.id,{sessionId:n,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s,agentId:t,platformAgentId:o??null}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async recoverSessionFromGateway(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.sessionsList();if(!s||"object"!=typeof s)return;const n=s.sessions;if(!Array.isArray(n))return;for(const t of n){const s=t,n=s.sessionKey??s.key??s.id??s.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const t=parseAgentIdFromKey(n),s={sessionId:e,gatewaySessionKey:n,agentId:t,platformAgentId:this.resolvePlatformAgentId(t),createdAt:Date.now(),chatReplayDedupKeys:new Set};return this.state.sessions.set(e,s),this.state.sessionsByGatewayKey.set(n,s),s}}}catch{}}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});let s=this.state.sessions.get(t);if(!s&&t.startsWith("agent:")&&(s=this.state.findSessionByKey(t)),s||t.startsWith("agent:")||(s=await this.recoverSessionFromGateway(t)),!s){const e=t.startsWith("agent:"),n=e?deriveSessionId(t,this.instanceId):t,i=this.config.gateway.agentId||this.getDefaultAgentId(),o=e?t:`agent:${i}:${t}`,a=parseAgentIdFromKey(o);s={sessionId:n,gatewaySessionKey:o,agentId:a,platformAgentId:this.resolvePlatformAgentId(a),createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(n,s),this.state.sessionsByGatewayKey.set(o,s)}const n=s.sessionId;this.state.lastActiveSessionId=n,s.chatReplayDedupKeys.clear();try{const e=this.getGateway(),t=await loadHistory(e,s.gatewaySessionKey,50),i=groupIntoTurns(t),o=i.filter(e=>"user"===e.role).length,a=i.filter(e=>"assistant"===e.role).length,r=i.filter(e=>e.toolCalls?.length).length,d=i.reduce((e,t)=>e+(t.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${t.length} turns=${i.length} userTurns=${o} asstTurns=${a} turnsWithToolCalls=${r} totalToolCalls=${d}`);for(let e=0;e<i.length;e++){const t=i[e];if("user"===t.role&&"internal_system"===t.provenance?.kind){this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] internal_system user message`);continue}if("system"===t.role){this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${t.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:t.content},metadata:t.metadata});continue}this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${t.role} contentLen=${t.content.length} content=${JSON.stringify(t.content.slice(0,80))} tcCount=${t.toolCalls?.length??0}`);const s={sessionUpdate:"history_turn",role:t.role,content:t.content,reasoning:t.reasoning,toolCalls:t.toolCalls};t.metadata&&(s.metadata=t.metadata),this.transport.sendSessionUpdate(n,s)}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,s.gatewaySessionKey,s.platformAgentId);void 0!==s.pendingPromptId&&!s.promptDone&&s.assistantMq&&!s.assistantMq.closed&&this.replayCurrentAssistantStream(s).catch(()=>{}),this.pushSkillsList().catch(()=>{})}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const t=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,s=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let n=0;for(;Date.now()<s&&(e.currentRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const s=await Promise.race([e.assistantMq.read(n),new Promise(e=>setTimeout(()=>e(null),t))]);if(!s||0===s.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const t of s)this.transport.sendSessionUpdate(e.sessionId,t),n++}n>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:n}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)t.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(e.gatewaySessionKey)});const s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,n=s.sessionKey??s.key??s.id??s.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=t.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),a=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0;if(o){if(!o.title||"New chat"===o.title){const e=a||o.sessionId;o.title=e;const t=this.state.sessions.get(o.sessionId);t&&!t.title&&(t.title=e)}const e=s.updatedAt;e&&(o.updatedAt=e)}else{const e=i,o=parseAgentIdFromKey(n),r={sessionId:e,gatewaySessionKey:n,agentId:o,platformAgentId:this.resolvePlatformAgentId(o),createdAt:Date.now(),chatReplayDedupKeys:new Set,title:a||"New chat",cwd:s.cwd};this.state.sessions.set(e,r),this.state.sessionsByGatewayKey.set(n,r),t.push({sessionId:e,title:a||"New chat",cwd:s.cwd,updatedAt:s.updatedAt,agentId:o,platformAgentId:r.platformAgentId,deletable:!isSystemDefaultSessionKey(n)})}}}}catch{}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const n=s.slice(0,64),i=this.state.sessions.get(t);if(!i)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);i.title=n;const o=i.gatewaySessionKey,a=this.getGateway();a?.isConnected&&a.sessionsPatch(o,{label:n}).catch(()=>{}),this.transport.sendResult(e.id,{sessionId:t,title:n})}async handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.info?.(`[bridge-core] session/delete sessionId=${t} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=s.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${t} gatewayKey=${n} gwConnected=${!!this.getGateway()?.isConnected}`);let i=!1;const o=this.getGateway();if(o?.isConnected&&n)try{await o.sessionsDelete(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${n}`)}catch(t){const s=t instanceof Error?t.message:String(t);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${s}`),!s.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await o.sessionsReset(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${n}`)}catch(t){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${t instanceof Error?t.message:t}`),void this.transport.sendResult(e.id,{deleted:!1})}}else n&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${n}`);void 0!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId);const a=new Set;for(const[e,s]of this.state.toolCallToSessionMap)s===t&&(this.state.toolCallToSessionMap.delete(e),a.add(e));for(const[e,t]of this.state.toolCallIdMap)a.has(t)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(n),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${t} gatewayKey=${n} deleted=true`),this.transport.sendResult(e.id,{deleted:!0})}async handleSessionClear(e){this.logger.info?.(`[bridge-core] handleSessionClear called, params=${JSON.stringify(e.params)}`);const t=e.params?.sessionId;if(!t)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${t} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${t} gatewayKey=${s.gatewaySessionKey}`);const n=this.getGateway();if(n?.isConnected&&s.gatewaySessionKey)try{await n.sessionsReset(s.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${s.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${s.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n?.isConnected}, key=${s.gatewaySessionKey}`);s.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(t,s.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(n&&i?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(n.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(n&&i?.isConnected&&s)try{await i.sessionsPatch(n.gatewaySessionKey,{model:s})}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:s??"default"})}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();return t?.skills?t.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0})):[]}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(){const e=await this.fetchSimplifiedSkills();this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:e}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:e.length}})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});let n=this.state.sessions.get(t);if(!n&&t.startsWith("agent:")&&(n=this.state.findSessionByKey(t)),n||t.startsWith("agent:")||(n=await this.recoverSessionFromGateway(t)),!n){const e=t.startsWith("agent:"),s=e?deriveSessionId(t,this.instanceId):t,i=this.config.gateway.agentId||this.getDefaultAgentId(),o=e?t:`agent:${i}:${t}`,a=parseAgentIdFromKey(o);n={sessionId:s,gatewaySessionKey:o,agentId:a,platformAgentId:this.resolvePlatformAgentId(a),createdAt:Date.now(),chatReplayDedupKeys:new Set},this.state.sessions.set(s,n),this.state.sessionsByGatewayKey.set(o,n)}const i=n.sessionId;this.state.lastActiveSessionId=i,this.onPromptStart?.();const o=this.getGateway();if(!o?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let a=extractPromptBlocks(sanitizePromptPayload(s));if(!n.title){const e=a.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(n.title=e.text.slice(0,64).replace(/\n/g," ").trim(),o?.isConnected&&n.title&&o.sessionsPatch(n.gatewaySessionKey,{label:n.title}).catch(()=>{}))}const r=buildResolutionPlan(a);if(r.refs.length>0)try{const e=await resolveResources(r,this.writeObs);a=applyResolutions(a,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const d=toGatewayPrompt(a);if("error"in d)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:d.error});const{message:l,attachments:c}=d,p=resolveStandaloneSlashCommandName(a,l)??resolveSlashCmd(a,l),h="string"==typeof p;if(p&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(p)}catch(e){return t.logger.warn?.(`[bridge-core] /${p} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,a),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${p}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const g=h?"chat.send":"agent";let m=h?l:withUserMessagePrefix(l);if(this.config.canvasEnabled&&!h&&!1!==n.canvasActive){const e=n.promptsSinceCanvasInjection??0;!n.canvasPromptInjected||e>=10?(m=`${CANVAS_TERM_MAPPING}\n\n${m}`,n.canvasPromptInjected=!0,n.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):n.promptsSinceCanvasInjection=e+1}if(this.config.canvasEnabled&&!h&&!1!==n.canvasActive){const e=this.getCanvasState?.();if(e){const t=e.resolveCanvasForSession(n.gatewaySessionKey);m=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${m}`}const t=n.referencedSurfaceId;t&&(m=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${m}`,n.referencedSurfaceId=void 0)}if(!h){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){n.activeMentions=t;m=`${buildMentionInstruction(t)}\n${m}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${t.length} agent(s)`)}else n.activeMentions=void 0}h&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${i} command=/${p}`),this.state.resetForNewRun(n,i,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=g,this.emitPromptAsUserSessionUpdates(i,a),this.transport.sendSessionUpdate(i,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(n);try{const e=c.length>0?c.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let t;t=h?await o.chatSend(n.gatewaySessionKey,m,e):await o.agentSend(n.gatewaySessionKey,m,n.agentId||this.config.gateway.agentId||this.getDefaultAgentId(),e),n.currentRunId=t.runId,n.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(t.runId,n)}catch(t){this.state.clearPromptTimeout(n),void 0!==n.pendingPromptId&&(n.pendingPromptId=void 0,n.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:t instanceof Error?t.message:"failed to send prompt to gateway"})),n.pendingAgentSends?.length&&(this.logger.warn?.("[bridge-core] Discarding %d deferred agentSends after prompt send failure",n.pendingAgentSends.length),n.pendingAgentSends=void 0)}}emitPromptAsUserSessionUpdates(e,t){for(const s of t){const t=this.toUserSessionUpdateContent(s);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,n=t.mimeType??t.mime_type??e.mimeType??e.mime_type,i=t.text??e.text,o=t.data??e.data,a=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!i&&!o)return;return{type:"resource",resource:{...s?{uri:s}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...a?{fileName:a}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(s);if(!n)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway();i?.isConnected&&n.currentRunId&&i.chatAbort(n.gatewaySessionKey,n.currentRunId).catch(()=>{}),this.completePrompt(n,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close()}completePrompt(e,t){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const s=e.currentRunId;if(s&&this.onRunComplete){const t=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(s,t)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t}),this.cleanupPrompt(e)}failPrompt(e,t,s){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const n=e.currentRunId;n&&this.onRunFailed&&this.onRunFailed(n,s),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,t,s),this.cleanupPrompt(e)}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,s)}}pushToolResultContent(e,t,s,n){if(0===s.length)return;const i=(n?this.state.promptsByRunId.get(n):void 0)??(this.state.lastActiveSessionId?this.state.sessions.get(this.state.lastActiveSessionId):void 0);if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session (runId=${n})`);const o=i.sessionId;for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,t),i.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:o}})}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/",".");try{const n=await t.request(s,e.params??{});this.transport.sendResult(e.id,n??{})}catch(t){const n=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}isCronCorrelatedRun(e){return this.state.cronRunIds.has(e)}}
|
|
1
|
+
import{randomUUID,createHash}from"node:crypto";import{sanitizePromptPayload,stripTransportMetadata}from"../message-filter.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas/tools.js";import{parseMentionsParam,buildMentionInstruction}from"../a2a/mention-resolver.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,groupIntoTurns}from"./history-replay.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{resolveChannelFromSessionKey}from"../channel-detection.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS}from"./types.js";const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(s=>e.includes(s))}function isSystemDefaultSessionKey(e){const s=e.split(":");if(s.length<3||"agent"!==s[0])return!1;const t=s.slice(2).join(":");return"main"===t||t===s[1]}function cleanGatewayDerivedTitle(e){if(!e)return;let s=stripTransportMetadata(e);return/^\[System\b/.test(s)?void 0:(s=s.trim(),s||void 0)}function resolveStandaloneSlashCommandName(e,s){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const t=s.trim();if(!t||t.includes("\n")||!t.startsWith("/"))return;const n=t.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,s){const t=createHash("sha256").update(`${s}:${e}`).digest("hex").substring(0,32);return`session:${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20,32)}`}function parseAgentIdFromKey(e){const s=e.split(":");return(s.length>=3&&"agent"===s[0]?s[1]:"")||"main"}function createSessionObject(e){const s=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:s,platformAgentId:e.resolvePlatformAgentId?.(s),createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:e.cwd,title:e.title,channelType:resolveChannelFromSessionKey(e.gatewayKey,e.thirdPartyChannels)}}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;thirdPartyChannels;initialized=!1;agentMapping=null;_syncResolvers=[];setAgentMapping(e){this.agentMapping=e;for(const s of this.state.sessions.values())!s.platformAgentId&&s.agentId&&(s.platformAgentId=e.get(s.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[]}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(s=>{this._syncResolvers.push(s),setTimeout(()=>{const e=this._syncResolvers.indexOf(s);-1!==e&&this._syncResolvers.splice(e,1),s()},e)})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;handleSlashCommand=null;onPromptStart=null;instanceId;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway,this.instanceId=e.instanceId||"unknown",this.thirdPartyChannels=e.thirdPartyChannels??new Set}resolvePlatformAgentId(e){return this.agentMapping?.get(e)?.id}getDefaultAgentId(){return this.agentMapping&&this.agentMapping.size>0?this.agentMapping.keys().next().value??"main":"main"}registerSession(e){this.state.sessions.set(e.sessionId,e),this.state.sessionsByGatewayKey.set(e.gatewaySessionKey,e)}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});try{switch(e.method){case"initialize":this.handleInitialize(e);break;case"session/new":await this.handleSessionNew(e);break;case"session/load":await this.handleSessionLoad(e);break;case"session/list":await this.handleSessionList(e);break;case"session/rename":this.handleSessionRename(e);break;case"session/delete":await this.handleSessionDelete(e);break;case"session/clear":await this.handleSessionClear(e);break;case"session/prompt":await this.handleSessionPrompt(e);break;case"session/set_mode":await this.handleSessionSetMode(e);break;case"session/set_model":await this.handleSessionSetModel(e);break;case"skills/list":await this.handleSkillsList(e);break;case"session/cancel":this.handleSessionCancel(e.id,e.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(e);break;default:e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}catch(s){this.transport.sendAcpError(e.id,Errors.InternalError,{message:s instanceof Error?s.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSessionDeleted=null;onSessionCleared=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/toggle":case"canvas/clear":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const s=e.params,t=s?.surfaceId,n=this.state.resolveSessionFromParams(s);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${s?.sessionId}`);break}n.referencedSurfaceId=t||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${t?"set":"cleared"}: ${t??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const s=this.transport.forwardControl;!1===e.params?.forwardThinking&&(s.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(s.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const s=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,t="string"==typeof e.params?.cwd?e.params.cwd:".",n=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t});this.registerSession(n),this.transport.sendResult(e.id,{sessionId:n.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s,agentId:n.agentId,platformAgentId:n.platformAgentId??null}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const s=this.getGateway();s?.isConnected&&loadHistory(s,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async recoverSessionFromGateway(e){const s=this.getGateway();if(s?.isConnected)try{const t=await s.sessionsList();if(!t||"object"!=typeof t)return;const n=t.sessions;if(!Array.isArray(n))return;for(const s of n){const t=s,n=t.sessionKey??t.key??t.id??t.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(e),e}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let s=this.state.sessions.get(e);if(!s&&e.startsWith("agent:")&&(s=this.state.findSessionByKey(e)),s||e.startsWith("agent:")||(s=await this.recoverSessionFromGateway(e)),s)return{session:s,created:!1};const t=e.startsWith("agent:"),n=this.config.gateway.agentId||this.getDefaultAgentId();return s=createSessionObject({gatewayKey:t?e:`agent:${n}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(s),{session:s,created:!0}}async handleSessionLoad(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const{session:t}=await this.resolveOrCreateSession(s),n=t.sessionId;this.state.lastActiveSessionId=n,t.chatReplayDedupKeys.clear();try{const e=this.getGateway(),s=await loadHistory(e,t.gatewaySessionKey,50),i=groupIntoTurns(s),o=i.filter(e=>"user"===e.role).length,a=i.filter(e=>"assistant"===e.role).length,r=i.filter(e=>e.toolCalls?.length).length,d=i.reduce((e,s)=>e+(s.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${s.length} turns=${i.length} userTurns=${o} asstTurns=${a} turnsWithToolCalls=${r} totalToolCalls=${d}`);for(let e=0;e<i.length;e++){const s=i[e];if("user"===s.role&&"internal_system"===s.provenance?.kind){this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] internal_system user message`);continue}if("system"===s.role){this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${s.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:s.content},metadata:s.metadata});continue}this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${s.role} contentLen=${s.content.length} content=${JSON.stringify(s.content.slice(0,80))} tcCount=${s.toolCalls?.length??0}`);const t={sessionUpdate:"history_turn",role:s.role,content:s.content,reasoning:s.reasoning,toolCalls:s.toolCalls};s.metadata&&(t.metadata=s.metadata),this.transport.sendSessionUpdate(n,t)}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,t.gatewaySessionKey,t.platformAgentId);void 0!==t.pendingPromptId&&!t.promptDone&&t.assistantMq&&!t.assistantMq.closed&&this.replayCurrentAssistantStream(t).catch(e=>{this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const s=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,t=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let n=0;for(;Date.now()<t&&(e.currentRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const t=await Promise.race([e.assistantMq.read(n),new Promise(e=>setTimeout(()=>e(null),s))]);if(!t||0===t.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const s of t)this.transport.sendSessionUpdate(e.sessionId,s),n++}n>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:n}})}async handleSessionList(e){const s=[];for(const[,e]of this.state.sessions)s.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(e.gatewaySessionKey)});const t=this.getGateway();if(t?.isConnected)try{const e=await t.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const t=e.sessions;for(const e of t){const t=e,n=t.sessionKey??t.key??t.id??t.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=s.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),a=t.label||cleanGatewayDerivedTitle(t.derivedTitle)||t.title||void 0;if(o){if(!o.title||"New chat"===o.title){const e=a||o.sessionId;o.title=e;const s=this.state.sessions.get(o.sessionId);s&&!s.title&&(s.title=e)}const e=t.updatedAt;e&&(o.updatedAt=e)}else{const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t.cwd,title:a||"New chat"});this.registerSession(e),s.push({sessionId:e.sessionId,title:a||"New chat",cwd:t.cwd,updatedAt:t.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(n)})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}s.sort((e,s)=>(s.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(s.sessionId)),this.transport.sendResult(e.id,{sessions:s})}handleSessionRename(e){const s=e.params?.sessionId,t=e.params?.title;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const n=t.slice(0,64),i=this.state.sessions.get(s);if(!i)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);i.title=n;const o=i.gatewaySessionKey,a=this.getGateway();a?.isConnected&&a.sessionsPatch(o,{label:n}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (rename) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendResult(e.id,{sessionId:s,title:n})}async handleSessionDelete(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.info?.(`[bridge-core] session/delete sessionId=${s} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=t.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${s} gatewayKey=${n} gwConnected=${!!this.getGateway()?.isConnected}`);let i=!1;const o=this.getGateway();if(o?.isConnected&&n)try{await o.sessionsDelete(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${n}`)}catch(s){const t=s instanceof Error?s.message:String(s);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${t}`),!t.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await o.sessionsReset(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${n}`)}catch(s){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${s instanceof Error?s.message:s}`),void this.transport.sendResult(e.id,{deleted:!1})}}else n&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${n}`);void 0!==t.pendingPromptId&&this.failPrompt(t,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(t),this.state.clearStreamIdleTimer(t),t.assistantMq?.close(),t.currentRunId&&this.state.promptsByRunId.delete(t.currentRunId);const a=new Set;for(const[e,t]of this.state.toolCallToSessionMap)t===s&&(this.state.toolCallToSessionMap.delete(e),a.add(e));for(const[e,s]of this.state.toolCallIdMap)a.has(s)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(s),this.state.sessionsByGatewayKey.delete(n),this.transport.resetSessionUpdateCount(s),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${s} gatewayKey=${n} deleted=true`),this.transport.sendResult(e.id,{deleted:!0})}async handleSessionClear(e){this.logger.info?.(`[bridge-core] handleSessionClear called, params=${JSON.stringify(e.params)}`);const s=e.params?.sessionId;if(!s)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${s} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${s} gatewayKey=${t.gatewaySessionKey}`);const n=this.getGateway();if(n?.isConnected&&t.gatewaySessionKey)try{await n.sessionsReset(t.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${t.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${t.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n?.isConnected}, key=${t.gatewaySessionKey}`);t.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(s,t.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const s=e.params?.sessionId,t=e.params?.mode,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();if(n&&i?.isConnected&&t){const e={};if("verbose"===t||"detailed"===t?e.verboseLevel="full":"concise"!==t&&"default"!==t||(e.verboseLevel="default"),"reasoning"!==t&&"thinking"!==t||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(n.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:t??"default"})}async handleSessionSetModel(e){const s=e.params?.sessionId,t=e.params?.model,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();if(n&&i?.isConnected&&t)try{await i.sessionsPatch(n.gatewaySessionKey,{model:t})}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:t??"default"})}async handleSkillsList(e){const s=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:s})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const s=await e.skillsStatus();return s?.skills?s.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,s)=>e.always&&!s.always?-1:!e.always&&s.always?1:e.name.localeCompare(s.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0})):[]}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(){const e=await this.fetchSimplifiedSkills();this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:e}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:e.length}})}async handleSessionPrompt(e){const s=e.params?.sessionId,t=e.params?.prompt;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:n,created:i}=await this.resolveOrCreateSession(s);i&&this.onSessionLoaded?.(n.sessionId,n.gatewaySessionKey,n.platformAgentId);const o=n.sessionId;this.state.lastActiveSessionId=o,this.onPromptStart?.();const a=this.getGateway();if(!a?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let r=extractPromptBlocks(sanitizePromptPayload(t));if(!n.title){const e=r.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(n.title=e.text.slice(0,64).replace(/\n/g," ").trim(),a?.isConnected&&n.title&&a.sessionsPatch(n.gatewaySessionKey,{label:n.title}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}))}const d=buildResolutionPlan(r);if(d.refs.length>0)try{const e=await resolveResources(d,this.writeObs);r=applyResolutions(r,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const l=toGatewayPrompt(r);if("error"in l)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:l.error});const{message:c,attachments:h}=l,g=resolveStandaloneSlashCommandName(r,c)??resolveSlashCmd(r,c),m="string"==typeof g;if(g&&this.handleSlashCommand){const s=this;if(await(async()=>{try{return await s.handleSlashCommand(g)}catch(e){return s.logger.warn?.(`[bridge-core] /${g} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${g}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const p=m?"chat.send":"agent";let u=m?c:withUserMessagePrefix(c);const f=this.config.canvasEnabled&&!m&&!1!==n.canvasActive&&!n.channelType;if(f){const e=n.promptsSinceCanvasInjection??0;!n.canvasPromptInjected||e>=10?(u=`${CANVAS_TERM_MAPPING}\n\n${u}`,n.canvasPromptInjected=!0,n.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):n.promptsSinceCanvasInjection=e+1}if(f){const e=this.getCanvasState?.();if(e){const s=e.resolveCanvasForSession(n.gatewaySessionKey);u=`${buildActiveSurfacesContext(e,s)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${u}`}const s=n.referencedSurfaceId;s&&(u=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${s}" for editing. You MUST call render_ui with id="${s}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${u}`,n.referencedSurfaceId=void 0)}if(!m){const s=parseMentionsParam(e.params?.mentions);if(s.length>0){n.activeMentions=s;u=`${buildMentionInstruction(s)}\n${u}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${s.length} agent(s)`)}else n.activeMentions=void 0}m&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${g}`),this.state.resetForNewRun(n,o,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=p,this.emitPromptAsUserSessionUpdates(o,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(n);try{const e=h.length>0?h.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let s;s=m?await a.chatSend(n.gatewaySessionKey,u,e):await a.agentSend(n.gatewaySessionKey,u,n.agentId||this.config.gateway.agentId||this.getDefaultAgentId(),e),n.currentRunId=s.runId,n.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(s.runId,n)}catch(s){this.state.clearPromptTimeout(n),void 0!==n.pendingPromptId&&(n.pendingPromptId=void 0,n.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:s instanceof Error?s.message:"failed to send prompt to gateway"})),n.pendingAgentSends?.length&&(this.logger.warn?.("[bridge-core] Discarding %d deferred agentSends after prompt send failure",n.pendingAgentSends.length),n.pendingAgentSends=void 0)}}emitPromptAsUserSessionUpdates(e,s){for(const t of s){const s=this.toUserSessionUpdateContent(t);s&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:s})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const s=e.mimeType??e.mime_type??"application/octet-stream",t=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const s=e.mimeType??e.mime_type??"application/octet-stream",t=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}if("resource"===e.type){const s=e.resource??{},t=s.uri??e.uri,n=s.mimeType??s.mime_type??e.mimeType??e.mime_type,i=s.text??e.text,o=s.data??e.data,a=s.fileName??s.file_name??s.filename??s.name??e.fileName??e.file_name??e.filename??e.name;if(!t&&!i&&!o)return;return{type:"resource",resource:{...t?{uri:t}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...a?{fileName:a}:{}}}}}handleSessionCancel(e,s){const t=s?.sessionId;if(!t)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(t);if(!n)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway();i?.isConnected&&n.currentRunId&&i.chatAbort(n.gatewaySessionKey,n.currentRunId).catch(e=>{this.logger.debug?.(`[bridge-core] chatAbort failed: ${e instanceof Error?e.message:e}`)}),this.completePrompt(n,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const s=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;s<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${s}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${s}ms`))},s),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const s=e.currentRunId;if(s){this.state.promptsByRunId.delete(s);const e=`tc_${s}_`;for(const[s,t]of this.state.toolCallIdMap)t.startsWith(e)&&this.state.toolCallIdMap.delete(s)}e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close()}completePrompt(e,s){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const t=e.currentRunId;if(t&&this.onRunComplete){const s=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(t,s)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:s}),this.cleanupPrompt(e)}failPrompt(e,s,t){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const n=e.currentRunId;n&&this.onRunFailed&&this.onRunFailed(n,t),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,s,t),this.cleanupPrompt(e)}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}maybeCompletePromptFromChatState(e,s){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const t="string"==typeof s.state?s.state.toLowerCase():void 0;if(t)if("final"===t)this.completePrompt(e,"end_turn");else if("aborted"===t||"cancelled"===t||"cancel"===t)this.completePrompt(e,"cancelled");else if("error"===t){const t=("string"==typeof s.summary?s.summary:void 0)??("string"==typeof s.message?s.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(t))}}pushToolResultContent(e,s,t,n){if(0===t.length)return;const i=(n?this.state.promptsByRunId.get(n):void 0)??(this.state.lastActiveSessionId?this.state.sessions.get(this.state.lastActiveSessionId):void 0);if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session (runId=${n})`);const o=i.sessionId;for(const e of t){const s={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,s),i.assistantMq?.push(s)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:s,blockCount:t.length,sessionId:o}})}async handleCronRequest(e){const s=this.getGateway();if(!s?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const t=e.method.replace("/",".");try{const n=await s.request(t,e.params??{});this.transport.sendResult(e.id,n??{})}catch(s){const n=s instanceof Error?s.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${t} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Errors}from"../errors/index.js";import{stripTransportMetadata}from"../message-filter.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS,DEFAULT_CRON_FLUSH_TIMEOUT_MS,DEFAULT_CRON_SIGNAL_WINDOW_MS}from"./types.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;return t.replace(/\r\n/g,"\n").replace(/\r/g,"\n")||void 0}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;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}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()}sendToolCallSpacer(t){const e={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"}};this.transport.sendSessionUpdate(t.sessionId,e),t.assistantMq?.push(e)}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||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return void(this.state.isCronCandidate(t.runId,e)&&this.handleUncorrelatedCronChatEvent(t))}if("number"==typeof t.seq){if(t.seq<=(e.lastChatSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate chat event seq=${t.seq} (last=${e.lastChatSeq}) runId=${t.runId}`);e.lastChatSeq=t.seq}const s=e.sessionId,n=t.message;if(n){this.touchStreamActivity(e);if("assistant"===n.role)for(const o of n.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(a){if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const o=`chat:thinking:${t}`;if(e.chatReplayDedupKeys.has(o))continue;e.chatReplayDedupKeys.add(o);const a={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t}};this.transport.sendSessionUpdate(s,a)&&e.assistantMq?.push(a);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=isRecord(n.arguments)?n.arguments:isRecord(n.args)?n.args:{},r=`chat:tool_start:${t}:${JSON.stringify(a)}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call",toolCallId:t,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(n.result)||Array.isArray(n.result)?JSON.stringify(n.result,null,2):void 0);if(!a)continue;const r=`chat:tool_result:${t}:${a}`;if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call_update",toolCallId:t,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const o=stripTransportMetadata(n.text);if(!o)continue;const a=`chat:text:${o.slice(0,128)}`;if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const r=e.chatAssistantTextSoFar??"";let i;if(e.chatAssistantTextSoFar=o,o.startsWith(r))i=o.slice(r.length);else{if(r.startsWith(o))continue;i=o}if(!i)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${i.length} prevLen=${r.length}`);const l={sessionUpdate:"agent_message_chunk",content:{type:"text",text:i}};this.transport.sendSessionUpdate(s,l),e.assistantMq?.push(l)}}}}else"delta"===t.state&&this.touchStreamActivity(e);this.maybeCompletePromptFromChatState(e,t),"final"===t.state?this.completePrompt(e,t.stopReason||"end_turn"):"aborted"===t.state||"cancelled"===t.state||"cancel"===t.state?this.completePrompt(e,"cancelled"):"error"===t.state&&this.failPrompt(e,Errors.AgentError.rpcCode,t.errorMessage||Errors.AgentError.message)}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0)??this.state.findCronTargetSession();if(!e||!this.state.isCurrentStreamOwner(e,t.runId)){const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS;return this.state.isCronCandidate(t.runId,e)?void this.handleUncorrelatedCronAgentEvent(t):void 0}if("number"==typeof t.seq){if(t.seq<=(e.lastAgentSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate agent event seq=${t.seq} (last=${e.lastAgentSeq}) runId=${t.runId} stream=${t.stream}`);e.lastAgentSeq=t.seq}const s=e.sessionId;this.touchStreamActivity(e);const n=t=>{this.transport.sendSessionUpdate(s,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});o&&(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${o.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}}));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}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const s=this.resolveToolCallId(e,t,"start"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=isRecord(t.arguments)?t.arguments:isRecord(t.args)?t.args:{};n({sessionUpdate:"tool_call",toolCallId:s,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]});continue}if("toolResult"===o){this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0;const s=this.resolveToolCallId(e,t,"result"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=normalizeText(t.text)??normalizeText(t.result)??(isRecord(t.result)||Array.isArray(t.result)?JSON.stringify(t.result,null,2):void 0);a&&n({sessionUpdate:"tool_call_update",toolCallId:s,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]});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}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("start"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,s,"start"),o=asString(s?.name)??asString(s?.toolName)??asString(s?.tool_name)??"tool",a=isRecord(s?.arguments)?s.arguments:isRecord(s?.args)?s.args:{};n({sessionUpdate:"tool_call",toolCallId:t,title:o,kind:"other",status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]})}else if("result"===o)this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=s?.toolCall;if(t){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const o=asString(t?.name)??asString(s?.name)??asString(s?.toolName)??"tool";n({sessionUpdate:"tool_call",toolCallId:this.resolveToolCallId(e,{...s,...t},"start"),title:o,kind:"other",status:"in_progress"})}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("end"===n){this.flushCronBufferForRun(t.runId);const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"end_turn")}else if("cancelled"===n||"cancel"===n){const s=this.state.resolveCronStreamKey(t);s&&this.clearCronStreamTracking(s),this.completePrompt(e,"cancelled")}else if("error"===n){const n=s?.error,o=("string"==typeof s?.message?s.message:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n&&"object"==typeof n?JSON.stringify(n):void 0)??"gateway lifecycle error";this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${o} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,o)}}}async handleToolResult(t,e){const s=e.data,n=s?.toolCall,o=("string"==typeof n?.id?n.id:void 0)??("string"==typeof s?.toolCallId?s.toolCallId:void 0),a=(o?this.state.toolCallIdMap.get(o):void 0)??o??`tc_${e.runId}_${e.seq}`,r=("string"==typeof n?.name?n.name:void 0)??("string"==typeof s?.name?s.name:void 0)??"tool",i="string"==typeof s?.text?s.text:void 0,l=o??a;let c;try{c=await resolveToolResultBlocks({toolCallId:l,toolName:r,rawText:i,rawResult:e.data})}catch{c=i?[{type:"content",content:{type:"text",text:i}}]:[]}if(0===c.length&&i&&(c=[{type:"content",content:{type:"text",text:i}}]),c.length>0){const e=c.filter(t=>"text"===t.content?.type&&t.content?.text).map(t=>({type:"content",content:{type:"text",text:t.content.text}})),s={sessionUpdate:"tool_call_update",toolCallId:a,title:r,kind:"other",status:"completed",...e.length>0?{content:e}:{}};this.transport.sendSessionUpdate(t.sessionId,s)&&t.assistantMq?.push(s)}}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t,s="string"==typeof e?.runId?e.runId:void 0;if(this.state.lastCronSignalAtMs=Date.now(),s){this.state.cronRunIds.add(s);const t=this.state.cronRunIdTimers.get(s);t&&clearTimeout(t);const e=this.config.cronSignalWindowMs??DEFAULT_CRON_SIGNAL_WINDOW_MS,n=setTimeout(()=>{this.state.cronRunIds.delete(s),this.state.cronRunIdTimers.delete(s)},e);"object"==typeof n&&"unref"in n&&n.unref(),this.state.cronRunIdTimers.set(s,n)}this.pushCronWebhook(e,s);const n=this.state.findCronTargetSession();if(!n)return;const o=("string"==typeof e?.summary?e.summary:"")||("string"==typeof e?.text?e.text:"")||JSON.stringify(t),a=s?`run:${s}`:`signal:${Date.now()}`;this.bufferCronText(a,`[Cron] ${o}\n`,n.sessionId)}pushCronWebhook(t,e){if(!this.config.platformUrl||!this.agentFetchFn)return;const s=`${this.config.platformUrl}/api/v1/agent/webhooks/cron`,n=("string"==typeof t?.summary?t.summary:void 0)??("string"==typeof t?.text?t.text:void 0)??"",o={jobId:e??"",name:("string"==typeof t?.name?t.name:void 0)??("string"==typeof t?.jobName?t.jobName:void 0)??"",content:n,summary:n,type:"cron",action:"string"==typeof t?.action?t.action:"fired",status:"string"==typeof t?.status?t.status:""};this.agentFetchFn(s,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).catch(t=>{this.logger.warn?.(`[gateway-events] cron webhook push failed: ${t instanceof Error?t.message:t}`)})}bufferCronText(t,e,s){const n=this.state.cronTextByKey.get(t)??"";this.state.cronTextByKey.set(t,n+e),this.scheduleCronFlush(t,s)}scheduleCronFlush(t,e){const s=this.state.cronFlushTimersByKey.get(t);s&&clearTimeout(s);const n=this.config.cronFlushTimeoutMs??DEFAULT_CRON_FLUSH_TIMEOUT_MS,o=setTimeout(()=>{this.state.cronFlushTimersByKey.delete(t),this.flushCronBufferByKey(t,e,"timeout")},n);"object"==typeof o&&"unref"in o&&o.unref(),this.state.cronFlushTimersByKey.set(t,o)}flushCronBufferByKey(t,e,s){const n=this.state.cronFlushTimersByKey.get(t);n&&(clearTimeout(n),this.state.cronFlushTimersByKey.delete(t));const o=(this.state.cronTextByKey.get(t)??"").trim();if(this.state.cronTextByKey.delete(t),!o)return void("timeout"===s&&this.clearCronStreamTracking(t));const a=this.state.getCronRequestId(t),r={sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}};this.transport.sendCronSessionUpdate(e,r,{requestId:a,messageType:"cron",timestamp:Date.now()});const i=this.state.sessions.get(e);i?.assistantMq?.push(r),"timeout"===s&&this.clearCronStreamTracking(t)}flushCronBufferForRun(t){if(!t)return;const e=`run:${t}`;if(!this.state.cronTextByKey.has(e))return;const s=this.state.findCronTargetSession();s&&this.flushCronBufferByKey(e,s.sessionId,"end")}clearCronStreamTracking(t){this.state.cronRequestIdsByKey.delete(t),this.state.cronTextByKey.delete(t);const e=this.state.cronFlushTimersByKey.get(t);if(e&&(clearTimeout(e),this.state.cronFlushTimersByKey.delete(t)),t.startsWith("run:")){const e=t.slice(4);if(e){this.state.cronRunIds.delete(e);const t=this.state.cronRunIdTimers.get(e);t&&(clearTimeout(t),this.state.cronRunIdTimers.delete(e))}}}handleUncorrelatedCronAgentEvent(t){if("lifecycle"===t.stream){const e=t.data,s="string"==typeof e?.phase?e.phase:void 0;if("end"===s){this.flushCronBufferForRun(t.runId);const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}else if("cancelled"===s||"cancel"===s||"error"===s){const e=this.state.resolveCronStreamKey(t);e&&this.clearCronStreamTracking(e)}return}if("assistant"!==t.stream)return;const e=this.state.findCronTargetSession();if(!e)return;const s=("string"==typeof t.data?.delta?t.data.delta:void 0)??("string"==typeof t.data?.text?t.data.text:void 0);if(!s)return;const n=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`;this.bufferCronText(n,s,e.sessionId)}handleUncorrelatedCronChatEvent(t){if("user"!==t.message?.role)return;const e=this.state.findCronTargetSession();if(e&&t.message?.content)for(const s of t.message.content)if("text"===s.type&&s.text){const n=stripTransportMetadata(s.text);if(!n)continue;const o=this.state.resolveCronStreamKey(t)??`signal:${this.state.lastCronSignalAtMs}`,a=this.state.getCronRequestId(o),r={sessionUpdate:"user_message_chunk",content:{type:"text",text:n}};this.transport.sendCronSessionUpdate(e.sessionId,r,{requestId:a,messageType:"cron",timestamp:Date.now()})}}}
|
|
1
|
+
import{createHash}from"node:crypto";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata}from"../message-filter.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.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;return t.replace(/\r\n/g,"\n").replace(/\r/g,"\n")||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)}`}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;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}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()}sendToolCallSpacer(t){const e={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"}};this.transport.sendSessionUpdate(t.sessionId,e),t.assistantMq?.push(e)}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)){if(!t.runId||!1===e.promptDone)return;this.adoptBackgroundRun(e,t.runId)}if("number"==typeof t.seq){if(t.seq<=(e.lastChatSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate chat event seq=${t.seq} (last=${e.lastChatSeq}) runId=${t.runId}`);e.lastChatSeq=t.seq}const s=e.sessionId,n=t.message;if(n){this.touchStreamActivity(e);if("assistant"===n.role){if(!Array.isArray(n.content))return;for(const o of n.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(a){if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const o=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(o))continue;e.chatReplayDedupKeys.add(o);const a={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t}};this.transport.sendSessionUpdate(s,a)&&e.assistantMq?.push(a);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=isRecord(n.arguments)?n.arguments:isRecord(n.args)?n.args:{},r=dedupKey("chat:tool_start",t,JSON.stringify(a));if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call",toolCallId:t,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(n.result)||Array.isArray(n.result)?JSON.stringify(n.result,null,2):void 0);if(!a)continue;const r=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const i={sessionUpdate:"tool_call_update",toolCallId:t,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]};this.transport.sendSessionUpdate(s,i)&&e.assistantMq?.push(i);continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const o=stripTransportMetadata(n.text);if(!o)continue;const a=dedupKey("chat:text",o);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const r=e.chatAssistantTextSoFar??"";let i;if(e.chatAssistantTextSoFar=o,o.startsWith(r))i=o.slice(r.length);else{if(r.startsWith(o))continue;i=o}if(!i)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${i.length} prevLen=${r.length}`);const l={sessionUpdate:"agent_message_chunk",content:{type:"text",text:i}};this.transport.sendSessionUpdate(s,l),e.assistantMq?.push(l)}}}}}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)){if(!t.runId||!1===e.promptDone)return;this.adoptBackgroundRun(e,t.runId)}if("number"==typeof t.seq){if(t.seq<=(e.lastAgentSeq??-1))return void this.logger.debug?.(`[gateway-events] Dropping duplicate agent event seq=${t.seq} (last=${e.lastAgentSeq}) runId=${t.runId} stream=${t.stream}`);e.lastAgentSeq=t.seq}const s=e.sessionId;this.touchStreamActivity(e);const n=t=>{this.transport.sendSessionUpdate(s,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(e,t.data??{});o&&(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${o.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:o}}));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}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const s=this.resolveToolCallId(e,t,"start"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=isRecord(t.arguments)?t.arguments:isRecord(t.args)?t.args:{};n({sessionUpdate:"tool_call",toolCallId:s,title:o,status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]});continue}if("toolResult"===o){this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0;const s=this.resolveToolCallId(e,t,"result"),o=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",a=normalizeText(t.text)??normalizeText(t.result)??(isRecord(t.result)||Array.isArray(t.result)?JSON.stringify(t.result,null,2):void 0);a&&n({sessionUpdate:"tool_call_update",toolCallId:s,title:o,status:"completed",content:[{type:"content",content:{type:"text",text:a}}]});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}};this.transport.sendSessionUpdate(s,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("start"===o){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,s,"start"),o=asString(s?.name)??asString(s?.toolName)??asString(s?.tool_name)??"tool",a=isRecord(s?.arguments)?s.arguments:isRecord(s?.args)?s.args:{};n({sessionUpdate:"tool_call",toolCallId:t,title:o,kind:"other",status:"in_progress",content:[{type:"content",content:{type:"text",text:JSON.stringify(a,null,2)}}]})}else if("result"===o)this.sendToolCallSpacer(e),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=s?.toolCall;if(t){this.sendToolCallSpacer(e),e.hasAgentToolStartStream=!0;const o=asString(t?.name)??asString(s?.name)??asString(s?.toolName)??"tool";n({sessionUpdate:"tool_call",toolCallId:this.resolveToolCallId(e,{...s,...t},"start"),title:o,kind:"other",status:"in_progress"})}}}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){const s=e.data,n=s?.toolCall,o=("string"==typeof n?.id?n.id:void 0)??("string"==typeof s?.toolCallId?s.toolCallId:void 0),a=(o?this.state.toolCallIdMap.get(o):void 0)??o??`tc_${e.runId}_${e.seq}`,r=("string"==typeof n?.name?n.name:void 0)??("string"==typeof s?.name?s.name:void 0)??"tool",i="string"==typeof s?.text?s.text:void 0,l=o??a;let c;try{c=await resolveToolResultBlocks({toolCallId:l,toolName:r,rawText:i,rawResult:e.data})}catch{c=i?[{type:"content",content:{type:"text",text:i}}]:[]}if(0===c.length&&i&&(c=[{type:"content",content:{type:"text",text:i}}]),c.length>0){const e=c.filter(t=>"text"===t.content?.type&&t.content?.text).map(t=>({type:"content",content:{type:"text",text:t.content.text}})),s={sessionUpdate:"tool_call_update",toolCallId:a,title:r,kind:"other",status:"completed",...e.length>0?{content:e}:{}};this.transport.sendSessionUpdate(t.sessionId,s)&&t.assistantMq?.push(s)}}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.lastChatSeq=void 0,t.lastAgentSeq=void 0,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()}handleCronEvent(t){if(this.transport.sendNotification("_beeos/cron_event",{payload:t}),this.config.agentGatewayUrl&&this.agentFetchFn){const e=`${this.config.agentGatewayUrl}/api/v1/agent/events`;this.agentFetchFn(e,{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}`)})}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{stripTransportMetadata}from"../message-filter.js";const BEEOS_USER_MSG_MARKER_RE=/(?:^|\n)\s*User Message From \w+:\s*\n/i,CANVAS_STATE_RE=/^\[Canvas State\][^\n]*/,SYSTEM_NOTE_RE=/^\[System Note\][^\n]*/,CANVAS_INTERACTION_RE=/^\[Canvas Interaction\] The user clicked "([^"]*)" on surface "([^"]*)"/,ACTION_CONTEXT_RE=/\nAction context: (.+?)(?:\n|$)/s;function stripBeeosUserEnvelope(t){const o=BEEOS_USER_MSG_MARKER_RE.exec(t);if(o){return t.slice(o.index+o[0].length).trim()}let e=t;return e=e.replace(CANVAS_STATE_RE,"").trimStart(),e=e.replace(SYSTEM_NOTE_RE,"").trimStart(),stripTransportMetadata(e).trim()}export async function loadHistory(t,o,e=50){if(!t?.isConnected)return[];try{const n=parseGatewayHistory(await t.chatHistory(o,e));if(n.length>0){n.filter(t=>"user"===t.role).length,n.filter(t=>"assistant"===t.role).length}return n}catch{return[]}}export function groupIntoTurns(t){const o=[];let e=null;const n=()=>{e&&(o.push(e),e=null)};for(const s of t){if("assistant"===s.role&&(s.reasoning||s.toolCalls?.length)){n(),e={role:"assistant",content:s.content,reasoning:s.reasoning},s.toolCalls?.length&&(e.toolCalls=s.toolCalls.map(t=>({id:t.id,callId:t.id,name:t.name,arguments:t.arguments??"",status:t.status??"completed",output:t.output,isError:"error"===t.status||"failed"===t.status})));continue}if("system"===s.role){n();const t={role:"system",content:s.content};s.metadata&&(t.metadata=s.metadata),o.push(t);continue}if("user"===s.role){n();const t={role:"user",content:s.content};s.provenance&&(t.provenance=s.provenance),s.metadata&&(t.metadata=s.metadata),o.push(t);continue}e||(e={role:"assistant",content:""});switch(s.updateType??"agent_message_chunk"){case"agent_thought_chunk":e.reasoning=(e.reasoning??"")+s.content;break;case"tool_call":e.toolCalls||(e.toolCalls=[]),e.toolCalls.push({id:s.toolCallId??`tc_${e.toolCalls.length}`,callId:s.toolCallId??`tc_${e.toolCalls.length}`,name:s.toolName??"tool",arguments:s.content||"",status:"completed"});break;case"tool_call_update":{e.toolCalls||(e.toolCalls=[]);const t=s.toolCallId?e.toolCalls.find(t=>t.id===s.toolCallId):void 0;t?(t.output=s.content,t.status="completed"):e.toolCalls.push({id:s.toolCallId??`tc_${e.toolCalls.length}`,callId:s.toolCallId??`tc_${e.toolCalls.length}`,name:s.toolName??"tool",arguments:"",status:"completed",output:s.content});break}default:e.content+=s.content}}return n(),o}const TOOL_CALL_BLOCK_TYPES=new Set(["toolCall","toolUse","functionCall","tool_use","tool_call","toolcall"]),TOOL_RESULT_BLOCK_TYPES=new Set(["tool_result","tool_result_error","toolResult"]);function extractBlockText(t){if("string"==typeof t)return t;if(!Array.isArray(t))return"";let o="";for(const e of t)"string"!=typeof e?e&&"object"==typeof e&&"string"==typeof e.text&&(o+=e.text):o+=e;return o}function parseGatewayHistory(t){if(!t||"object"!=typeof t)return[];const o=t.messages;if(!Array.isArray(o))return[];const e=[];let n=null;for(const t of o){if(!t||"object"!=typeof t)continue;const o=t,s="string"==typeof o.role?o.role:"assistant";if("toolResult"===s||"tool"===s){if(!n?.toolCalls?.length)continue;const t=String(o.toolCallId??o.tool_call_id??o.toolUseId??o.tool_use_id??""),e=extractBlockText(o.content);if(t){const s=n.toolCalls.find(o=>o.id===t);if(s)s.output=e,!0!==o.is_error&&!0!==o.isError||(s.status="error");else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=e,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=e,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}continue}if("user"===s&&n?.toolCalls?.length&&Array.isArray(o.content)){let t=!1,e=!1;for(const n of o.content){if(!n||"object"!=typeof n)continue;const o=n,s="string"==typeof o.type?o.type:"";TOOL_RESULT_BLOCK_TYPES.has(s)?t=!0:"text"===s&&"string"==typeof o.text&&o.text.trim()&&(e=!0)}if(t&&!e){for(const t of o.content){if(!t||"object"!=typeof t)continue;const o=t,e="string"==typeof o.type?o.type:"";if(!TOOL_RESULT_BLOCK_TYPES.has(e))continue;const s=String(o.toolUseId??o.tool_use_id??o.toolCallId??o.tool_call_id??""),r="string"==typeof o.output?o.output:extractBlockText(o.content);if(s){const t=n.toolCalls.find(t=>t.id===s);if(t)t.output=r,!0!==o.is_error&&!0!==o.isError||(t.status="error");else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=r,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=r,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}continue}}const r="user"===s?"user":"assistant";let l,a,i,c,u="";if("string"==typeof o.content)u=o.content;else if(Array.isArray(o.content))for(const t of o.content){if("string"==typeof t){u+=t;continue}if(!t||"object"!=typeof t)continue;const o=t,e="string"==typeof o.type?o.type:"";if("text"===e&&"string"==typeof o.text)u+=o.text;else if("thinking"===e&&"string"==typeof o.thinking)a=(a??"")+o.thinking;else if(TOOL_CALL_BLOCK_TYPES.has(e)){l||(l=[]);const t=o.arguments??o.input,e="string"==typeof t?t:null!=t?JSON.stringify(t):"";l.push({id:String(o.id??o.toolCallId??o.tool_call_id??""),name:String(o.name??"tool"),arguments:e,status:"completed"})}else if(TOOL_RESULT_BLOCK_TYPES.has(e)){const t=String(o.toolUseId??o.tool_use_id??o.toolCallId??o.tool_call_id??""),e="string"==typeof o.output?o.output:extractBlockText(o.content),n=!0===o.is_error||!0===o.isError;if(l){const o=t?l.find(o=>o.id===t):void 0;if(o)o.output=e,n&&(o.status="error");else{const t=l.find(t=>void 0===t.output);t&&(t.output=e,n&&(t.status="error"))}}}}if(a||("string"==typeof o.reasoning&&o.reasoning?a=o.reasoning:"string"==typeof o.thinking&&o.thinking&&(a=o.thinking)),!l?.length){const t=o.tool_calls??o.toolCalls;if(Array.isArray(t)){l=[];for(const o of t){if(!o||"object"!=typeof o)continue;const t=o,e=t.function;l.push({id:String(t.id??t.toolCallId??""),name:String(e?.name??t.name??t.toolName??"tool"),arguments:"string"==typeof t.arguments?t.arguments:"string"==typeof e?.arguments?e.arguments:"",status:String(t.status??"completed"),output:"string"==typeof t.output?t.output:void 0})}0===l.length&&(l=void 0)}}if("user"===r&&u){u=stripBeeosUserEnvelope(u);const t=CANVAS_INTERACTION_RE.exec(u);if(t){const o=t[1];i={kind:"canvas_action"};const e={type:"canvas_action",actionName:o,surfaceId:t[2]},n=ACTION_CONTEXT_RE.exec(u);if(n)try{e.actionPayload=JSON.parse(n[1])}catch{}c=e,u=`Clicked "${o}"`}}if(!u&&!a&&!l?.length)continue;if(!i&&"user"===r&&o.provenance&&"object"==typeof o.provenance){const t=o.provenance;"string"==typeof t.kind&&(i={kind:t.kind},"string"==typeof t.sourceTool&&(i.sourceTool=t.sourceTool))}const f={role:r,content:u,reasoning:a,toolCalls:l,provenance:i,metadata:c};e.push(f),"assistant"===r&&(n=f)}e.filter(t=>t.toolCalls?.length),e.reduce((t,o)=>t+(o.toolCalls?.length??0),0),e.reduce((t,o)=>t+(o.toolCalls?.filter(t=>void 0!==t.output).length??0),0);return e}
|
|
1
|
+
import{stripTransportMetadata}from"../message-filter.js";const BEEOS_USER_MSG_MARKER_RE=/(?:^|\n)\s*User Message From \w+:\s*\n/i,CANVAS_STATE_RE=/^\[Canvas State\][^\n]*/,SYSTEM_NOTE_RE=/^\[System Note\][^\n]*/,CANVAS_INTERACTION_RE=/^\[Canvas Interaction\] The user clicked "([^"]*)" on surface "([^"]*)"/,ACTION_CONTEXT_RE=/\nAction context: (.+?)(?:\n|$)/s;function stripBeeosUserEnvelope(t){const o=BEEOS_USER_MSG_MARKER_RE.exec(t);if(o){return t.slice(o.index+o[0].length).trim()}let e=t;return e=e.replace(CANVAS_STATE_RE,"").trimStart(),e=e.replace(SYSTEM_NOTE_RE,"").trimStart(),stripTransportMetadata(e).trim()}export async function loadHistory(t,o,e=50){if(!t?.isConnected)return[];try{const n=parseGatewayHistory(await t.chatHistory(o,e));if(n.length>0){n.filter(t=>"user"===t.role).length,n.filter(t=>"assistant"===t.role).length}return n}catch(t){return[]}}export function groupIntoTurns(t){const o=[];let e=null;const n=()=>{e&&(o.push(e),e=null)};for(const s of t){if("assistant"===s.role&&(s.reasoning||s.toolCalls?.length)){n(),e={role:"assistant",content:s.content,reasoning:s.reasoning},s.toolCalls?.length&&(e.toolCalls=s.toolCalls.map(t=>({id:t.id,callId:t.id,name:t.name,arguments:t.arguments??"",status:t.status??"completed",output:t.output,isError:"error"===t.status||"failed"===t.status})));continue}if("system"===s.role){n();const t={role:"system",content:s.content};s.metadata&&(t.metadata=s.metadata),o.push(t);continue}if("user"===s.role){n();const t={role:"user",content:s.content};s.provenance&&(t.provenance=s.provenance),s.metadata&&(t.metadata=s.metadata),o.push(t);continue}e||(e={role:"assistant",content:""});switch(s.updateType??"agent_message_chunk"){case"agent_thought_chunk":e.reasoning=(e.reasoning??"")+s.content;break;case"tool_call":e.toolCalls||(e.toolCalls=[]),e.toolCalls.push({id:s.toolCallId??`tc_${e.toolCalls.length}`,callId:s.toolCallId??`tc_${e.toolCalls.length}`,name:s.toolName??"tool",arguments:s.content||"",status:"completed"});break;case"tool_call_update":{e.toolCalls||(e.toolCalls=[]);const t=s.toolCallId?e.toolCalls.find(t=>t.id===s.toolCallId):void 0;t?(t.output=s.content,t.status="completed"):e.toolCalls.push({id:s.toolCallId??`tc_${e.toolCalls.length}`,callId:s.toolCallId??`tc_${e.toolCalls.length}`,name:s.toolName??"tool",arguments:"",status:"completed",output:s.content});break}default:e.content+=s.content}}return n(),o}const TOOL_CALL_BLOCK_TYPES=new Set(["toolCall","toolUse","functionCall","tool_use","tool_call","toolcall"]),TOOL_RESULT_BLOCK_TYPES=new Set(["tool_result","tool_result_error","toolResult"]);function extractBlockText(t){if("string"==typeof t)return t;if(!Array.isArray(t))return"";let o="";for(const e of t)"string"!=typeof e?e&&"object"==typeof e&&"string"==typeof e.text&&(o+=e.text):o+=e;return o}function parseGatewayHistory(t){if(!t||"object"!=typeof t)return[];const o=t.messages;if(!Array.isArray(o))return[];const e=[];let n=null;for(const t of o){if(!t||"object"!=typeof t)continue;const o=t,s="string"==typeof o.role?o.role:"assistant";if("toolResult"===s||"tool"===s){if(!n?.toolCalls?.length)continue;const t=String(o.toolCallId??o.tool_call_id??o.toolUseId??o.tool_use_id??""),e=extractBlockText(o.content);if(t){const s=n.toolCalls.find(o=>o.id===t);if(s)s.output=e,!0!==o.is_error&&!0!==o.isError||(s.status="error");else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=e,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=e,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}continue}if("user"===s&&n?.toolCalls?.length&&Array.isArray(o.content)){let t=!1,e=!1;for(const n of o.content){if(!n||"object"!=typeof n)continue;const o=n,s="string"==typeof o.type?o.type:"";TOOL_RESULT_BLOCK_TYPES.has(s)?t=!0:"text"===s&&"string"==typeof o.text&&o.text.trim()&&(e=!0)}if(t&&!e){for(const t of o.content){if(!t||"object"!=typeof t)continue;const o=t,e="string"==typeof o.type?o.type:"";if(!TOOL_RESULT_BLOCK_TYPES.has(e))continue;const s=String(o.toolUseId??o.tool_use_id??o.toolCallId??o.tool_call_id??""),r="string"==typeof o.output?o.output:extractBlockText(o.content);if(s){const t=n.toolCalls.find(t=>t.id===s);if(t)t.output=r,!0!==o.is_error&&!0!==o.isError||(t.status="error");else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=r,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}else{const t=n.toolCalls.find(t=>void 0===t.output);t&&(t.output=r,!0!==o.is_error&&!0!==o.isError||(t.status="error"))}}continue}}const r="user"===s?"user":"assistant";let l,a,i,c,u="";if("string"==typeof o.content)u=o.content;else if(Array.isArray(o.content))for(const t of o.content){if("string"==typeof t){u+=t;continue}if(!t||"object"!=typeof t)continue;const o=t,e="string"==typeof o.type?o.type:"";if("text"===e&&"string"==typeof o.text)u+=o.text;else if("thinking"===e&&"string"==typeof o.thinking)a=(a??"")+o.thinking;else if(TOOL_CALL_BLOCK_TYPES.has(e)){l||(l=[]);const t=o.arguments??o.input,e="string"==typeof t?t:null!=t?JSON.stringify(t):"";l.push({id:String(o.id??o.toolCallId??o.tool_call_id??""),name:String(o.name??"tool"),arguments:e,status:"completed"})}else if(TOOL_RESULT_BLOCK_TYPES.has(e)){const t=String(o.toolUseId??o.tool_use_id??o.toolCallId??o.tool_call_id??""),e="string"==typeof o.output?o.output:extractBlockText(o.content),n=!0===o.is_error||!0===o.isError;if(l){const o=t?l.find(o=>o.id===t):void 0;if(o)o.output=e,n&&(o.status="error");else{const t=l.find(t=>void 0===t.output);t&&(t.output=e,n&&(t.status="error"))}}}}if(a||("string"==typeof o.reasoning&&o.reasoning?a=o.reasoning:"string"==typeof o.thinking&&o.thinking&&(a=o.thinking)),!l?.length){const t=o.tool_calls??o.toolCalls;if(Array.isArray(t)){l=[];for(const o of t){if(!o||"object"!=typeof o)continue;const t=o,e=t.function;l.push({id:String(t.id??t.toolCallId??""),name:String(e?.name??t.name??t.toolName??"tool"),arguments:"string"==typeof t.arguments?t.arguments:"string"==typeof e?.arguments?e.arguments:"",status:String(t.status??"completed"),output:"string"==typeof t.output?t.output:void 0})}0===l.length&&(l=void 0)}}if("user"===r&&u){u=stripBeeosUserEnvelope(u);const t=CANVAS_INTERACTION_RE.exec(u);if(t){const o=t[1];i={kind:"canvas_action"};const e={type:"canvas_action",actionName:o,surfaceId:t[2]},n=ACTION_CONTEXT_RE.exec(u);if(n)try{e.actionPayload=JSON.parse(n[1])}catch{}c=e,u=`Clicked "${o}"`}}if(!u&&!a&&!l?.length)continue;if(!i&&"user"===r&&o.provenance&&"object"==typeof o.provenance){const t=o.provenance;"string"==typeof t.kind&&(i={kind:t.kind},"string"==typeof t.sourceTool&&(i.sourceTool=t.sourceTool))}const f={role:r,content:u,reasoning:a,toolCalls:l,provenance:i,metadata:c};e.push(f),"assistant"===r&&(n=f)}e.filter(t=>t.toolCalls?.length),e.reduce((t,o)=>t+(o.toolCalls?.length??0),0),e.reduce((t,o)=>t+(o.toolCalls?.filter(t=>void 0!==t.output).length??0),0);return e}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createWriteStream}from"node:fs";import{mkdir,access,readdir,constants}from"node:fs/promises";import{join,basename}from"node:path";import{pipeline}from"node:stream/promises";import{Readable}from"node:stream";import{createHash}from"node:crypto";const OPENCLAW_HOME=process.env.OPENCLAW_HOME||join(process.env.HOME||"/home/node",".openclaw"),RESOURCE_CACHE_DIR=join(OPENCLAW_HOME,"workspace","resource_cache"),DOWNLOAD_TIMEOUT_MS=12e4,MAX_CONCURRENT_DOWNLOADS=3;export function buildResolutionPlan(e){const
|
|
1
|
+
import{createWriteStream}from"node:fs";import{mkdir,access,readdir,stat,unlink,constants}from"node:fs/promises";import{join,basename}from"node:path";import{pipeline}from"node:stream/promises";import{Readable}from"node:stream";import{createHash}from"node:crypto";const OPENCLAW_HOME=process.env.OPENCLAW_HOME||join(process.env.HOME||"/home/node",".openclaw"),RESOURCE_CACHE_DIR=join(OPENCLAW_HOME,"workspace","resource_cache"),DOWNLOAD_TIMEOUT_MS=12e4,MAX_CONCURRENT_DOWNLOADS=3,MAX_CACHED_FILES=200;export function buildResolutionPlan(e){const t=[],r=new Set;for(let n=0;n<e.length;n++){const o=e[n];if("resource_link"!==o.type||!o.uri)continue;const a=o.uri;if(!a.startsWith("http://")&&!a.startsWith("https://"))continue;const i=deriveFileId(a);t.push({blockIndex:n,uri:a,name:o.title??o.name,fileId:i}),r.add(i)}return{refs:t,fileIds:r}}export async function resolveResources(e,t){const r=new Map;if(0===e.refs.length)return r;await mkdir(RESOURCE_CACHE_DIR,{recursive:!0}).catch(()=>{});const n=e.refs.map(e=>async()=>{const n=await resolveOne(e,t);r.set(e.uri,n)});return await runWithConcurrency(n,3),r}export function applyResolutions(e,t){return e.map(e=>{if("resource_link"!==e.type||!e.uri)return e;const r=t.get(e.uri);return r?.localPath?{...e,_resolvedLocalPath:r.localPath}:e})}function deriveFileId(e){return createHash("sha256").update(e).digest("hex").slice(0,16)}async function resolveOne(e,t){const{uri:r,fileId:n,name:o}=e,a=await findCachedFile(n);if(a)return t?.({component:"resource-resolver",domain:"file",name:"cache_hit",severity:"debug",payload:{uri:r,fileId:n,localPath:a}}),{uri:r,fileId:n,localPath:a,cached:!0};const i=(o||extractFileName(r)||`resource_${n}`).replace(/[/\\:*?"<>|]/g,"_"),c=join(RESOURCE_CACHE_DIR,`${n}_${i}`),s=new AbortController,l=setTimeout(()=>s.abort(),12e4);"object"==typeof l&&"unref"in l&&l.unref();try{const e=await fetch(r,{signal:s.signal});if(!e.ok||!e.body){const o=classifyHttpError(e.status);return t?.({component:"resource-resolver",domain:"file",name:"download_failed",severity:"warn",payload:{uri:r,fileId:n,status:e.status,category:o}}),{uri:r,fileId:n,localPath:null,cached:!1,error:`HTTP ${e.status}`,errorCategory:o,retriable:"http_5xx"===o}}const o=createWriteStream(c);return await pipeline(Readable.fromWeb(e.body),o),t?.({component:"resource-resolver",domain:"file",name:"download_ok",severity:"debug",payload:{uri:r,fileId:n,localPath:c}}),pruneResourceCache().catch(()=>{}),{uri:r,fileId:n,localPath:c,cached:!1}}catch(e){const o=e instanceof Error&&"AbortError"===e.name?"timeout":"network_error",a=e instanceof Error?e.message:String(e);return t?.({component:"resource-resolver",domain:"file",name:"download_failed",severity:"warn",payload:{uri:r,fileId:n,error:a,category:o}}),{uri:r,fileId:n,localPath:null,cached:!1,error:a,errorCategory:o,retriable:!0}}finally{clearTimeout(l)}}async function findCachedFile(e){try{const t=await readdir(RESOURCE_CACHE_DIR),r=`${e}_`;for(const e of t)if(e.startsWith(r)){const t=join(RESOURCE_CACHE_DIR,e);try{return await access(t,constants.R_OK),t}catch{continue}}}catch{}return null}function extractFileName(e){try{const t=new URL(e),r=basename(t.pathname);if(r&&"."!==r&&"/"!==r)return decodeURIComponent(r)}catch{}}function classifyHttpError(e){return e>=400&&e<500?"http_4xx":e>=500?"http_5xx":"invalid_response"}async function runWithConcurrency(e,t){let r=0;async function n(){for(;r<e.length;){const t=r++;await e[t]()}}const o=[];for(let r=0;r<Math.min(t,e.length);r++)o.push(n());await Promise.all(o)}let pruneRunning=!1;async function pruneResourceCache(){if(!pruneRunning){pruneRunning=!0;try{const e=await readdir(RESOURCE_CACHE_DIR);if(e.length<=200)return;const t=[];for(const r of e)try{const e=await stat(join(RESOURCE_CACHE_DIR,r));e.isFile()&&t.push({name:r,mtimeMs:e.mtimeMs})}catch{}t.sort((e,t)=>e.mtimeMs-t.mtimeMs);const r=t.length-200;for(let e=0;e<r;e++)await unlink(join(RESOURCE_CACHE_DIR,t[e].name)).catch(()=>{})}catch{}finally{pruneRunning=!1}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{MessageQueue}from"../utils/mq.js";const MAX_TOOL_CALL_MAP_ENTRIES=5e3;export class AcpGatewaySessionState{sessions=new Map;promptsByRunId=new Map;sessionsByGatewayKey=new Map;toolCallIdMap=new Map;toolCallToSessionMap=new Map;nextToolCallSeq=0;lastActiveSessionId;sessionGcTimer=null;MAX_IDLE_SESSION_MS=864e5;SESSION_GC_INTERVAL_MS=3e5;logger;constructor(s){this.logger=s}findSessionByKey(s){return this.sessionsByGatewayKey.get(s)}resolveSessionFromParams(s){const e="string"==typeof s?.sessionId?s.sessionId:void 0;if(e)return this.sessions.get(e)}isCurrentStreamOwner(s,e){return!e||!!s.currentRunId&&e===s.currentRunId}clearPromptTimeout(s){s.promptTimeoutTimer&&(clearTimeout(s.promptTimeoutTimer),s.promptTimeoutTimer=void 0)}clearStreamIdleTimer(s){s.streamIdleTimer&&(clearTimeout(s.streamIdleTimer),s.streamIdleTimer=void 0)}resetForNewRun(s,e,t){this.clearStreamIdleTimer(s),s.currentRunId&&this.promptsByRunId.delete(s.currentRunId),s.currentRunId=void 0,s.assistantMq?.close(),s.assistantMq=new MessageQueue,s.chatReplayDedupKeys.clear(),s.hasAgentAssistantStream=!1,s.hasAgentThinkingStream=!1,s.hasAgentToolStartStream=!1,s.hasAgentToolResultStream=!1,s.chatAssistantTextSoFar=void 0,s.agentAssistantTextSoFar=void 0,s.lastAgentSeq=void 0,s.lastChatSeq=void 0,s.promptDone=!1,t.resetSessionUpdateCount(e),this.enforceToolCallMapLimits()}enforceToolCallMapLimits(){if(this.toolCallIdMap.size>5e3){const s=this.toolCallIdMap.size-5e3;let e=0;for(const t of this.toolCallIdMap.keys()){if(e>=s)break;this.toolCallToSessionMap.delete(this.toolCallIdMap.get(t)),this.toolCallIdMap.delete(t),e++}}if(this.toolCallToSessionMap.size>5e3){const s=this.toolCallToSessionMap.size-5e3;let e=0;for(const t of this.toolCallToSessionMap.keys()){if(e>=s)break;this.toolCallToSessionMap.delete(t),e++}}}extractAgentAssistantDelta(s,e){const t=(s.chatAssistantTextSoFar??"").length;if(e.delta){const o=s.agentAssistantTextSoFar??"",i=o+e.delta;if(s.agentAssistantTextSoFar=i,t>0&&o.length<t){if(i.length<=t)return;return i.slice(t)}return e.delta}const o=e.text;if(!o)return;const i=s.agentAssistantTextSoFar??"";s.agentAssistantTextSoFar=o;const a=Math.max(i.length,t);if(!(o.length<=a)){if(o.startsWith(i)||0===a)return o.slice(a)||void 0;if(!i.startsWith(o))return o}}startSessionGc(s,e){this.sessionGcTimer=setInterval(()=>this.gcSessions(s,e),this.SESSION_GC_INTERVAL_MS),"object"==typeof this.sessionGcTimer&&"unref"in this.sessionGcTimer&&this.sessionGcTimer.unref()}gcSessions(s,e){const t=Date.now()-this.MAX_IDLE_SESSION_MS;let o=0;for(const[i,a]of this.sessions){if(void 0!==a.pendingPromptId)continue;(a.lastStreamActivityTs??a.createdAt)<t&&(s(a),e?.(a),this.sessions.delete(i),this.sessionsByGatewayKey.delete(a.gatewaySessionKey),o++)}const i=new Set(Array.from(this.sessions.values()).map(s=>s.sessionId));for(const[s,e]of this.toolCallToSessionMap)i.has(e)||this.toolCallToSessionMap.delete(s);for(const[s,e]of this.toolCallIdMap)this.toolCallToSessionMap.has(e)||this.toolCallIdMap.delete(s);o>0&&(this.lastActiveSessionId&&!this.sessions.has(this.lastActiveSessionId)&&(this.lastActiveSessionId=void 0),this.logger.debug?.(`[session-state] GC evicted ${o} idle sessions, remaining=${this.sessions.size}`))}destroy(){this.sessionGcTimer&&(clearInterval(this.sessionGcTimer),this.sessionGcTimer=null);for(const s of this.sessions.values())this.clearPromptTimeout(s),this.clearStreamIdleTimer(s),s.assistantMq?.close();this.promptsByRunId.clear(),this.toolCallIdMap.clear(),this.toolCallToSessionMap.clear(),this.sessionsByGatewayKey.clear(),this.sessions.clear(),this.lastActiveSessionId=void 0}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describe,it,expect,beforeEach,vi}from"vitest";import{AcpGatewaySessionState}from"./session-state.js";function makeSession(e,s){return{sessionId:e,gatewaySessionKey:s??`agent:main:${e}`,agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set}}describe("AcpGatewaySessionState",()=>{let e;const s={info:vi.fn(),warn:vi.fn(),debug:vi.fn()};beforeEach(()=>{e=new AcpGatewaySessionState(s)}),it("should store and find sessions by key",()=>{const s=makeSession("s1","agent:main:abc");e.sessions.set("s1",s),e.sessionsByGatewayKey.set("agent:main:abc",s),expect(e.findSessionByKey("agent:main:abc")).toBe(s),expect(e.findSessionByKey("nonexistent")).toBeUndefined()}),describe("enforceToolCallMapLimits",()=>{it("should evict oldest entries when exceeding hard cap",()=>{for(let s=0;s<5010;s++)e.toolCallIdMap.set(`gw-${s}`,`acp-${s}`);expect(e.toolCallIdMap.size).toBe(5010),e.enforceToolCallMapLimits(),expect(e.toolCallIdMap.size).toBe(5e3),expect(e.toolCallIdMap.has("gw-0")).toBe(!1),expect(e.toolCallIdMap.has("gw-9")).toBe(!1),expect(e.toolCallIdMap.has("gw-5009")).toBe(!0)}),it("should not evict when under limit",()=>{e.toolCallIdMap.set("a","b"),e.enforceToolCallMapLimits(),expect(e.toolCallIdMap.size).toBe(1)})}),describe("resetForNewRun",()=>{it("should clear run-specific state and enforce limits",()=>{const s=makeSession("s1");s.currentRunId="old-run",s.promptDone=!0,s.chatReplayDedupKeys.add("old"),e.sessions.set("s1",s);const t={resetSessionUpdateCount:vi.fn(),sendSessionUpdate:vi.fn(),sendResult:vi.fn(),sendAcpError:vi.fn(),sendNotification:vi.fn()};e.resetForNewRun(s,"s1",t),expect(s.currentRunId).toBeUndefined(),expect(s.chatReplayDedupKeys.size).toBe(0),expect(s.promptDone).toBe(!1),expect(t.resetSessionUpdateCount).toHaveBeenCalledWith("s1")})}),describe("session GC",()=>{it("should clean up expired sessions",()=>{vi.useFakeTimers();const s=makeSession("expired");s.createdAt=Date.now()-9e7,e.sessions.set("expired",s),e.sessionsByGatewayKey.set(s.gatewaySessionKey,s);const t=vi.fn(),o=vi.fn();e.startSessionGc(t,o),vi.advanceTimersByTime(300100),expect(e.sessions.has("expired")).toBe(!1),expect(t).toHaveBeenCalled(),expect(o).toHaveBeenCalled(),e.destroy(),vi.useRealTimers()})})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readUploadToolResult}from"../upload
|
|
1
|
+
import{readUploadToolResult}from"../upload/result-cache.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.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){for(const e of strategies)if(e.canHandle(t))try{const n=await e.resolve(t);if(n&&n.length>0)return n}catch{}return[]}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 n=pickUploadPayload(e);return buildToolResultSessionContentBlocks(t.toolName,n??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 n=JSON.parse(e);if(isRecord(n)){const e=buildToolResultSessionContentBlocks(t.toolName,n);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 n=t[e];if(isUploadPayload(n))return n;if(isRecord(n))for(const t of["output","result","details"])if(isUploadPayload(n[t]))return n[t]}return Array.isArray(t.content)&&t.content.length>0||isUploadPayload(t)?t:void 0}}export function buildToolResultSessionContentBlocks(t,e){const n=[];if(!isRecord(e))return"string"==typeof e&&e.trim()&&n.push({type:"content",content:{type:"text",text:e.trim()}}),n;const o=e.content??e.result;if(Array.isArray(o)){for(const t of o){const e=toContentBlock(t);e&&n.push(e)}if(n.length>0)return n}if(Array.isArray(e.files)){for(const t of e.files){if(!isRecord(t))continue;const e=asStr(t.fileId)??asStr(t.file_id),o=e?`beeos-file://${e}`:asStr(t.uri)??asStr(t.downloadUrl)??asStr(t.url);if(!o)continue;const r=asStr(t.name)??asStr(t.fileName)??asStr(t.file_name),s=asStr(t.mimeType)??asStr(t.mime_type);n.push({type:"content",content:{type:"resource_link",uri:o,...r?{name:r}:{},...s?{mimeType:s}:{}}})}if(n.length>0)return n}const r=asStr(e.output)??asStr(e.text)??asStr(e.result);return r&&n.push({type:"content",content:{type:"text",text:r}}),n}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),n=asStr(t.uri);return e||n?{type:"content",content:{type:"image",...e?{data:e}:{},...n?{uri:n}:{},...t.mimeType?{mimeType:String(t.mimeType)}:{}}}:null}return null}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ACP_PROTOCOL_VERSION=1;export const DEFAULT_PROMPT_TIMEOUT_MS=18e5;export const DEFAULT_STREAM_IDLE_TIMEOUT_MS=15e3;export const DEFAULT_STREAM_STALE_TIMEOUT_MS=6e5;export const DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS=15e3;export const DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS=6e5;
|
|
1
|
+
export const ACP_PROTOCOL_VERSION=1;export const DEFAULT_PROMPT_TIMEOUT_MS=18e5;export const DEFAULT_STREAM_IDLE_TIMEOUT_MS=15e3;export const DEFAULT_STREAM_STALE_TIMEOUT_MS=6e5;export const DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS=15e3;export const DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS=6e5;
|
package/dist/acp-server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocketServer,WebSocket}from"ws";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal-session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal-bridge-handler.js";import{CANVAS_TERM_MAPPING}from"./canvas-tools.js";import{CanvasActionRouter,formatActionForAgent}from"./canvas-action-router.js";import{FormSchemaRegistry}from"./form-schema-registry.js";import{FormSurfaceBuilder}from"./form-surface-builder.js";import{ResourceSurfaceRenderer}from"./resource-surface-renderer.js";import{syncAgentsToPlatform}from"./agent-sync.js";const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;const s=toPascalCase(String(t.type??"Text")),n=t.properties??{},a={};for(const[e,t]of Object.entries(n))"children"===e&&Array.isArray(t)?a.children={explicitList:t}:STRING_PROPS.has(e)&&"string"==typeof t?a[e]={literalString:t}:a[e]=t;return{id:t.id,component:{[s]:a}}})}export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingToolCallSessionKey;pendingA2ARuns=new Map;pendingCanvasLinks=[];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).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){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const a=this.config.gateway.agentId||this.core.getDefaultAgentId();let i,r;if(s){if(i=this.state.findSessionByKey(s),!i)return void this.logger.warn?.("[acp-server] A2A result target session expired, discarding targetSessionKey=%s (known keys: %s)",s,[...this.state.sessionsByGatewayKey.keys()].join(", "));r=s,this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${i.sessionId}`)}else i=this.state.findCronTargetSession(),r=i?.gatewaySessionKey??`agent:${a}:a2a`,this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, using cron fallback key=${r}`);if(i&&!i.promptDone&&(void 0!==i.pendingPromptId||void 0!==i.currentRunId))return(i.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",i.pendingAgentSends.length);let o=e;if(this.canvasState)try{let e=this.canvasState.resolveCanvasForSession(r);if(this.logger.info?.(`[acp-server] agentSend canvas: sessionKey=${r} canvasId=${e} (from state)`),!e){const t=this.canvasState.getRelayClient();if(t)try{e=await t.ensureCanvas(r),this.canvasState.registerSession(r,e),this.logger.info?.(`[acp-server] agentSend canvas: WSS lookup result canvasId=${e}`)}catch{}}if(e){let t=this.canvasState.buildContextForCanvas(e);t||(await this.canvasState.lazyLoadCanvas(e),t=this.canvasState.buildContextForCanvas(e));o=`${CANVAS_TERM_MAPPING}\n\n${t??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${o}`,this.logger.info?.(`[acp-server] agentSend canvas: context injected (surfaceCtx=${t?"yes":"hint-only"})`)}else this.logger.warn?.(`[acp-server] agentSend canvas: NO canvasId resolved for session ${r} — canvas context NOT injected`)}catch(e){this.logger.warn?.("[acp-server] Canvas context injection failed: %s",e)}else this.logger.warn?.("[acp-server] agentSend: canvasState is null — no canvas injection");const c=await this.gateway.agentSend(r,o,a,t,n);return i&&c?.runId&&(this.state.resetForNewRun(i,i.sessionId,this.transport),i.currentRunId=c.runId,i.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(c.runId,i)),c}trackA2ARun(e,t,s){this.pendingA2ARuns.set(e,{taskId:t,completeFn:s})}cancelA2ARunByTaskId(e){for(const[t,s]of this.pendingA2ARuns)if(s.taskId===e)return this.pendingA2ARuns.delete(t),this.logger.info?.("[acp-server] Removed pending A2A run %s for cancelled task %s",t,e),!0;return!1}emitA2ASystemNotification(e){let t;if(e.targetSessionKey){const s=this.state.findSessionByKey(e.targetSessionKey);t=s?.sessionId,t||this.logger.debug?.("[acp-server] emitA2ASystemNotification: targetSessionKey=%s not found, falling back",e.targetSessionKey)}if(!t){const e=this.state.lastActiveSessionId,s=e?this.state.sessions.get(e):void 0;t=s?.sessionId??e}if(!t)return void this.logger.debug?.("[acp-server] emitA2ASystemNotification: no active session, dropping");const s={type:e.type,taskId:e.taskId,callerName:e.callerName,taskLink:`/a2a/tasks/${e.taskId}`};this.transport.sendSessionUpdate(t,{sessionUpdate:"system_message_chunk",content:{type:"text",text:e.message},metadata:s})}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch{}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,a){this.agentFetchFn=n,this.instanceId=a,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger),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}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>{this.core.completePrompt(e,t),this.actionRouter?.drainPending().catch(e=>{this.logger.warn?.(`[acp-server] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}),this.drainNextDeferredSend(e)},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),this.actionRouter?.clearPending(),this.drainNextDeferredSend(e)},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn}),this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.onRunComplete=(e,t)=>{const s=this.pendingA2ARuns.get(e);s&&(this.pendingA2ARuns.delete(e),s.completeFn(t,"").catch(e=>this.logger.warn?.("[acp-server] A2A CompleteTask failed for task %s: %s",s.taskId,e)))},this.core.onRunFailed=(e,t)=>{const s=this.pendingA2ARuns.get(e);s&&(this.pendingA2ARuns.delete(e),s.completeFn("",t).catch(e=>this.logger.warn?.("[acp-server] A2A CompleteTask (error) failed for task %s: %s",s.taskId,e)))},this.core.handleCanvasNotification=(e,t)=>{this.handleCanvasNotificationInternal(e,t)},this.core.getCanvasState=()=>this.canvasState,this.core.onSessionLoaded=(e,t,s)=>{if(!this.config.canvasEnabled)return;const n=this.canvasState?.getRelayClient();if(!n)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:t}),void this.logger.info?.(`[acp-server] Relay not ready, queued canvas link: acpSid=${e} gwKey=${t}`);this.ensureCanvasForSession(e,t)},this.core.onSessionDeleted=e=>{this.canvasState?.removeSession(e)},this.core.onSessionCleared=(e,t)=>{const s=this.canvasState?.resolveCanvasForSession(t);s&&(this.canvasState.clear(s),this.actionRouter?.reset())},this.core.handleSlashCommand=async e=>{const t=this.descriptorRegistry?.getBySlashCommand(e);return!(!t||!this.resourceRenderer)&&(await this.resourceRenderer.render(t,"inline"),!0)},this.core.onPromptStart=()=>{this.actionRouter?.clearPending()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>this.canvasState?.removeSession(e.gatewaySessionKey))}async start(){const e=this.config.bridge.url,t=this.config.bridge.keyFile;if(e&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch{this.logger.warn?.("[acp-server] Bridge SDK not available")}await this.connectGateway(),e&&t&&BridgeClientCtor?await this.startBridgeMode(e,t):(this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone())}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});s.on("connected",()=>{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 t=String(e[0]??"unknown");this.logger.warn?.(`[acp-server] Bridge disconnected: ${t}`),this.core.initialized=!1}),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)});try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.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,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(){if(this.client&&this.client.readyState===WebSocket.OPEN)if(this.gateway&&this.agentFetchFn&&this.config.platformUrl)try{const e=await syncAgentsToPlatform({gateway:this.gateway,platformUrl:this.config.platformUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0},agentFetch:this.agentFetchFn,logger:this.logger});e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")}`)):this.core.markAgentSyncDone()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),this.core.markAgentSyncDone()}else this.core.markAgentSyncDone()}async connectGateway(){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.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"})}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}isCronCorrelatedRun(e){return this.core.isCronCorrelatedRun(e)}setCanvasState(e,t){this.canvasState=e;const s=async(e,t)=>{if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)},n=()=>{const t=this.getSessionIdForToolCall();if(!t)return;const s=this.state.sessions.get(t),n=s?.gatewaySessionKey;return n?e.resolveCanvasForSession(n):void 0},a=t?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,gatewayRequest:s,resolveCanvasId:n,logger:this.logger});this.formBuilder=i,this.schemaRegistry=a,this.descriptorRegistry=t?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:s,schemaRegistry:a,formBuilder:i,resolveCanvasId:n,logger:this.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.logger,writeObs:e=>this.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const t=this.state.resolveSessionFromParams(e);t&&this.emitCanvasActionAsUserMessage(t.sessionId,e)}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),this.logger.debug?.("[acp-server] Canvas Relay action handler wired")):this.logger.warn?.("[acp-server] wireRelayActionHandler called but no relay client available")}isCanvasActive(e){const t=e??this.state.lastActiveSessionId,s=t?this.state.sessions.get(t):void 0;return!1!==s?.canvasActive}isCanvasActiveForGatewayKey(e){const t=this.state.sessionsByGatewayKey.get(e);return!1!==t?.canvasActive}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){if(this.pendingToolCallSessionKey){const t=this.state.findSessionByKey(this.pendingToolCallSessionKey);if(t?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,t.sessionId),t.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=${this.pendingToolCallSessionKey??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){this.pendingToolCallSessionKey=e}preMapToolCallSession(e){const t=this.pendingToolCallSessionKey;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0!==e.length){this.logger.info?.(`[acp-server] Flushing ${e.length} pending canvas link(s)`);for(const{acpSessionId:t,gatewaySessionKey:s}of e)this.ensureCanvasForSession(t,s)}}ensureCanvasForSession(e,t){const s=this.canvasState?.getRelayClient();if(!s)return;const n=e!==t?e:void 0;s.ensureCanvas(t,{linkSessionId:n}).then(async s=>{this.canvasState?.registerSession(t,s),await(this.canvasState?.joinCanvas(s,t)),this.logger.info?.(`[acp-server] Canvas ensured: canvas=${s} acpSid=${e} gwKey=${t}`)}).catch(s=>{this.logger.warn?.(`[acp-server] Failed to ensure canvas: acpSid=${e} gwKey=${t} err=${s}`)})}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.resourceRenderer}getDescriptorRegistry(){return this.descriptorRegistry}getFormBuilder(){return this.formBuilder}getSchemaRegistry(){return this.schemaRegistry}async forwardActionToAgent(e){const t=this.state.resolveSessionFromParams(e);if(!t)return void this.logger.warn?.("[acp-server] forwardActionToAgent dropped — no session resolved from params");if(!this.gateway?.isConnected)return;if(t.currentRunId&&!t.promptDone)return this.actionRouter?.enqueue(e),void this.logger.debug?.("[acp-server] Agent busy, canvas action queued");this.emitCanvasActionAsUserMessage(t.sessionId,e),this.state.resetForNewRun(t,t.sessionId,this.transport);const s=this.canvasState?.summarize(e.surfaceId??""),n=formatActionForAgent(e,s??void 0);try{const e=await this.gateway.agentSend(t.gatewaySessionKey,n,this.config.gateway.agentId||this.core.getDefaultAgentId());t.currentRunId=e.runId,t.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(e.runId,t)}catch(s){this.logger.warn?.(`[acp-server] Agent forward failed: ${s instanceof Error?s.message:s}`),this.transport.sendSessionUpdate(t.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,t){const s=t.userAction?.name??"unknown",n=`Clicked "${s}"`;this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:n},metadata:{type:"canvas_action",surfaceId:t.surfaceId,actionName:s,sourceComponentId:t.userAction?.sourceComponentId,actionPayload:t.userAction?.context,actionTimestamp:t.userAction?.timestamp}})}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.logger.warn?.("[acp-server] Relay action ignored — canvas not initialized");const{canvasId:t,surfaceId:s,userAction:n,requestId:a,senderClientId:i}=e,r=this.canvasState.resolveSessionForCanvas(t);if(!r)return this.logger.warn?.(`[acp-server] Relay action ignored — no session for canvas=${t}`),void this.sendRelayActionError(t,a,i,"no_session");const o=this.state.findSessionByKey(r);if(!o)return this.logger.warn?.(`[acp-server] Relay action ignored — session not found for gatewayKey=${r}`),void this.sendRelayActionError(t,a,i,"no_session");const c={surfaceId:s,userAction:{name:n.name,sourceComponentId:n.sourceComponentId,timestamp:n.timestamp,context:n.context},sessionId:o.sessionId};this.actionRouter.route(c,t).then(()=>{this.sendRelayActionResult(t,a,i,"ok")}).catch(e=>{this.logger.warn?.(`[acp-server] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(t,a,i,"processing_failed")}),this.logger.debug?.(`[acp-server] Relay action routed: canvas=${t} surface=${s} action=${n.name} session=${o.sessionId}`)}sendRelayActionResult(e,t,s,n){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:t,targetClientId:s,status:n})}sendRelayActionError(e,t,s,n){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:t,targetClientId:s,error:n})}handleCanvasNotificationInternal(e,t){if(!this.canvasState)return void this.logger.warn?.(`[acp-server] Canvas notification ${e} ignored — canvasState not initialized (canvasEnabled=${this.config.canvasEnabled})`);this.canvasState,this.logger;if("canvas/toggle"===e){const e=t,s=!1!==e.enabled,n=this.state.resolveSessionFromParams(e);if(n&&(n.canvasActive=s,s&&(n.canvasPromptInjected=!1,n.sessionId))){const e=n.sessionId,t=n.gatewaySessionKey,s=n.agentId;this.core.waitForAgentMapping().then(()=>{const a=this.core.resolvePlatformAgentId(s??"")??n.platformAgentId,i=t??e,r=t&&t!==e?e:void 0,o=this.canvasState?.getRelayClient();o&&o.ensureCanvas(i,{agentId:a,linkSessionId:r}).catch(e=>{this.logger.warn?.(`[acp-server] Failed to pre-create canvas on toggle: ${e}`)})})}this.logger.info?.(`[acp-server] Canvas toggled: ${s}`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:s}})}else if("canvas/clear"===e){const e=t,s=this.state.resolveSessionFromParams(e),n=s?this.canvasState.resolveCanvasForSession(s.gatewaySessionKey):void 0,a=n?this.canvasState.getAllActive(n).length:0;n&&this.canvasState.clear(n),this.actionRouter?.reset(),this.logger.info?.(`[acp-server] Canvas cleared (${a} surfaces removed)`),this.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const t=e,s=t.action;if("a2ui_push"===s||"surface_update"===s){const e=String(t.surfaceId??`openclaw-${Date.now()}`),s=t.components;if(s&&Array.isArray(s)){const n=sanitizeA2UIComponents(s),a=t.canvasId?String(t.canvasId):void 0;this.canvasState.has(e,a)?this.canvasState.update(e,n,a):this.canvasState.create(a??`_openclaw_${Date.now()}`,e,"inline",n,{origin:{kind:"openclaw"}}),this.logger.debug?.(`[acp-server] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===s||"url_open"===s){const e=String(t.url??"");if(e){const s=`openclaw-nav-${Date.now()}`,n=String(t.canvasId??`_openclaw_nav_${Date.now()}`),a=[{id:`${s}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(t.title??e)}}}}];this.canvasState.create(n,s,"inline",a,{origin:{kind:"openclaw"}})}}else this.logger.debug?.(`[acp-server] Unhandled OpenClaw canvas action: ${s}`)}async stop(){this.state.destroy(),this.pendingA2ARuns.clear(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop()),this.client&&(this.client.removeAllListeners(),this.client.close()),this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close()),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
|
|
1
|
+
import{WebSocketServer,WebSocket}from"ws";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{A2AOrchestrator}from"./a2a-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;a2aOrch;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=5;drainNextDeferredSend(e){this.a2aOrch.drainNextDeferredSend(e,(e,t,s,n)=>this.agentSend(e,t,s,n))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async agentSend(e,t,s,n){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const i=this.config.gateway.agentId||this.core.getDefaultAgentId();let a,r;if(s){if(a=this.state.findSessionByKey(s),!a)return void this.logger.warn?.("[acp-server] A2A result target session expired, discarding targetSessionKey=%s (known keys: %s)",s,[...this.state.sessionsByGatewayKey.keys()].join(", "));r=s,this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${a.sessionId}`)}else{const e=this.state.lastActiveSessionId;a=e?this.state.sessions.get(e):void 0,r=a?.gatewaySessionKey??`agent:${i}:a2a`,this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, using fallback key=${r}`)}if(a&&!a.promptDone&&(void 0!==a.pendingPromptId||void 0!==a.currentRunId))return(a.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",a.pendingAgentSends.length);const o=await this.canvasOrch.enrichMessageWithCanvasContext(e,r,this.thirdPartyChannels),c=await this.gateway.agentSend(r,o,i,t,n);return a&&c?.runId&&(this.state.resetForNewRun(a,a.sessionId,this.transport),a.currentRunId=c.runId,a.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(c.runId,a)),c}trackA2ARun(e,t,s){this.a2aOrch.trackA2ARun(e,t,s)}cancelA2ARunByTaskId(e){return this.a2aOrch.cancelA2ARunByTaskId(e)}emitA2ASystemNotification(e){this.a2aOrch.emitA2ASystemNotification(e)}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,i,a){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=a??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger),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}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.a2aOrch=new A2AOrchestrator({state:this.state,transport:this.transport,logger:this.logger}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>{this.core.completePrompt(e,t),this.canvasOrch.onPromptComplete(e),this.drainNextDeferredSend(e)},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn}),this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.onRunComplete=(e,t)=>{this.a2aOrch.onRunComplete(e,t)},this.core.onRunFailed=(e,t)=>{this.a2aOrch.onRunFailed(e,t)},this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,s)=>{this.canvasOrch.onSessionLoaded(e,t,s)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}async start(){const e=this.config.bridge.url,t=this.config.keyFile;if(e&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch{this.logger.warn?.("[acp-server] Bridge SDK not available")}await this.connectGateway(),e&&t&&BridgeClientCtor?await this.startBridgeMode(e,t):(this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone())}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});this.bridgeInstance=s,s.on("connected",()=>{this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync()}),s.on("disconnected",(...e)=>{const n=String(e[0]??"unknown");if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${n}`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n}, switching`),this.config.bridge.url=n,writeDiscoveryCache({bridge:{url:n,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n,t))}).catch(()=>{})}}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n;try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();const t=()=>{if(e+1>=6)return this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — giving up"),void this.core.markAgentSyncDone();const t=Math.min(5e3*2**e,12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>this.tryAgentSync(e+1),t)};try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0},agentFetch:this.agentFetchFn,logger:this.logger});e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")}`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}}async connectGateway(){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.canvasOrch.updateDeps({gateway:this.gateway?{isConnected:this.gateway.isConnected,request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,s)=>this.gateway.agentSend(e,t,s)}:null}),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"})}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const s=this.state.findSessionByKey(t);if(s?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,s.sessionId),s.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=this.state.toolCallIdMap.get(e);if(s&&s!==e){const e=this.state.toolCallToSessionMap.get(s);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(s?.agentId??"")??s?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.a2aOrch.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance=null,this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
|
package/dist/agent-sync.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export async function syncAgentsToPlatform(t){const{gateway:
|
|
1
|
+
export async function syncAgentsToPlatform(t){const{gateway:e,agentGatewayUrl:n,capabilities:a,agentFetch:s,logger:i}=t;if(!s)return i?.debug?.("[agent-sync] no agentFetch — skipping sync (no key pair)"),null;if(!n)return i?.debug?.("[agent-sync] no agentGatewayUrl — skipping sync"),null;try{const t=await e.request("agents.list",{}),r=t?.agents??[];if(0===r.length)return i?.debug?.("[agent-sync] no agents from Gateway — skipping sync"),null;const c=[];for(const t of r){let n=[];try{const a=await e.request("skills.status",{agentId:t.id});n=(a?.skills??[]).filter(t=>t.eligible&&!t.disabled).map(t=>({name:t.name,description:t.description,...t.emoji?{emoji:t.emoji}:{}}))}catch{i?.debug?.(`[agent-sync] skills.status failed for agent ${t.id} — continuing without skills`)}const s={};if(a)for(const[t,e]of Object.entries(a))e&&(s[t]=!0);c.push({name:t.id,description:t.name||t.identity?.name||t.id,version:"0.1.0",capabilities:s,skills:n,avatarUrl:t.identity?.avatarUrl||"",homepage:""})}const o=new URL("/api/v1/agents/sync",n),l=await s(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agents:c})});if(l.ok){const t=await l.json();i?.info?.(`[agent-sync] synced ${c.length} agent(s) — created=${t.created??0} updated=${t.updated??0} deactivated=${t.deactivated??0}`);const e=new Map,n=Array.isArray(t.data)?t.data:[];for(const t of n)t&&"string"==typeof t.name&&"string"==typeof t.id&&e.set(t.name,{id:t.id,instanceId:t.instanceId??"",name:t.name,displayName:t.displayName??t.name,description:t.description??"",version:t.version??"",avatarUrl:t.avatarUrl??"",status:t.status??"active"});return e.size>0?e:null}{const t=await l.text().catch(()=>"");return i?.warn?.(`[agent-sync] sync failed: HTTP ${l.status} — ${t.slice(0,200)}`),null}}catch(t){return i?.warn?.(`[agent-sync] sync error: ${t instanceof Error?t.message:String(t)}`),null}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export async function discoverBridgeFromGW(i,o){try{const r=await o(`${i}/api/v1/bridge/config`,{method:"GET"});if(r.ok){return(await r.json()).url||void 0}}catch{}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export class CanvasActionRouter{deps;lastActionTime=new Map;pendingActions=[];static DEBOUNCE_MS=300;static MAX_PENDING=5;constructor(t){this.deps=t}async route(t,n){const e=t.surfaceId??"",i=Date.now();if(i-(this.lastActionTime.get(e)??0)<CanvasActionRouter.DEBOUNCE_MS)return void this.deps.logger.debug?.(`[action-router] Debounced action on ${e}`);this.lastActionTime.set(e,i);const s=this.deps.stateManager.getActionHandler(e,n);if(s)try{this.deps.emitActionAsUserMessage?.(t),await s(t),this.emitObs(t,"bound_handler")}catch(n){throw this.emitObs(t,"bound_handler_error"),this.deps.logger.warn?.(`[action-router] Bound handler for ${e} threw: ${n instanceof Error?n.message:n}`),n}else this.emitObs(t,"agent_forward"),await this.deps.forwardToAgent(t)}enqueue(t){if(this.pendingActions.length>=CanvasActionRouter.MAX_PENDING){const t=this.pendingActions.shift();this.deps.logger.warn?.(`[action-router] Queue full (${CanvasActionRouter.MAX_PENDING}), dropping oldest action (surface=${t?.surfaceId??"?"})`)}this.pendingActions.push(t)}async drainPending(){if(0===this.pendingActions.length)return;const t=this.deduplicatePending();this.pendingActions=[];for(const n of t)await this.deps.forwardToAgent(n)}clearPending(){this.pendingActions=[]}reset(){this.lastActionTime.clear(),this.pendingActions=[]}deduplicatePending(){if(this.pendingActions.length<=1)return[...this.pendingActions];const t=new Map;for(const n of this.pendingActions)t.set(n.surfaceId??"",n);return Array.from(t.values())}emitObs(t,n){this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.action_routed",severity:n.endsWith("_error")?"warn":"info",payload:{surfaceId:t.surfaceId,actionName:t.userAction?.name,resolution:n}})}}export function formatActionForAgent(t,n){const e=t.surfaceId??"unknown",i=t.userAction?.name??"unknown",s=t.userAction?.context;let o=`[Canvas Interaction] The user clicked "${i}" on surface "${e}"`;if(n?.title&&(o+=` ("${n.title}")`),o+=".",n?.types?.length&&(o+=` Surface contains: ${n.types.join(", ")}.`),s&&Object.keys(s).length>0){const t=JSON.stringify(s);o+=`\nAction context: ${t.length>2e3?t.slice(0,2e3)+"…(truncated)":t}`}return o+=`\nRespond to this interaction. If the surface needs updating, use render_ui with id="${e}".`,o}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{CanvasStateManager}from"./state.js";export{CanvasRelayClient}from"./relay-client.js";export{CanvasActionRouter,formatActionForAgent}from"./action-router.js";export{CanvasWSProvider}from"./ws-provider.js";export{MSG_SYNC,MSG_SYNC_UPDATE,MSG_AWARENESS}from"./constants.js";export{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,CANVAS_SYSTEM_PROMPT,CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext,createRenderUiHandler,createCloseUiHandler}from"./tools.js";
|