beeos-claw 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/a2a/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{agentFetch}from"../agent-auth.js";import{resolveAgentIdFromMentions}from"./mention-resolver.js";async function a2aFetch(e,
|
|
1
|
+
import{agentFetch}from"../agent-auth.js";import{resolveAgentIdFromMentions}from"./mention-resolver.js";async function a2aFetch(t,e,n){const a=`${t.agentGatewayUrl}${e}`;return t.agentKeys?agentFetch(a,t.agentKeys,n):fetch(a,n)}export{a2aFetch};export class TaskHandleRegistry{seq=0;handles=new Map;taskIdToHandle=new Map;create(t,e,n,a){const s="bth_"+ ++this.seq;return this.handles.set(s,{taskId:t,agentId:e,contextId:n,sessionKey:a}),this.taskIdToHandle.set(t,s),s}resolveTaskId(t){const e=this.handles.get(t);return e?e.taskId:t}}const defaultRegistry=new TaskHandleRegistry;export function registerA2ATools(t,e,n=defaultRegistry,a,s,r){t.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(t,n,a){const{query:s}=n;try{const t=await a2aFetch(e,`/api/v1/a2a/discover?query=${encodeURIComponent(s)}&limit=20`,{signal:a});if(!t.ok)return{content:JSON.stringify({error:`discover failed: ${t.status}`})};const n=await t.json();return{content:JSON.stringify(n,null,2)}}catch(t){if(a?.aborted)throw t;return{content:JSON.stringify({error:t instanceof Error?t.message:String(t)})}}}}),t.registerTool({name:"beeos_call_agent",description:"Ask another agent to perform a task. Waits for the agent to complete the task and returns the result. Timeout defaults to 10 minutes (configurable via timeoutMs).",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"},taskId:{type:"string",description:"Optional existing task ID to continue a multi-turn conversation. When provided, the message is appended to the existing task instead of creating a new one."},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(t,o,i){let{agentId:c,message:d,files:l,contextId:g,timeoutMs:u,taskId:f}=o;const m=a?.(t),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(c)){const n=s?.(t);if(n?.length){const t=resolveAgentIdFromMentions(n,c);t&&(e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${t} via mention context`),c=t)}if(!p.test(c))try{const t=await a2aFetch(e,`/api/v1/a2a/discover?query=${encodeURIComponent(c)}&limit=10`,{signal:i});if(t.ok){const n=await t.json(),a=n?.data??[],s=c.toLowerCase(),r=a.find(t=>t.name.toLowerCase()===s)??a.find(t=>t.name.toLowerCase().includes(s));if(!r)return{content:JSON.stringify({error:`Could not resolve agent name "${c}". Available agents: ${a.map(t=>`${t.name} (${t.id})`).join(", ")||"none"}`})};e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${r.id} (${r.name}) via discover`),c=r.id}}catch{}}const h=g||(m?`session:${m}`:""),y=u||6e5;try{const t={jsonrpc:"2.0",id:`claw-${Date.now()}`,method:"SendMessage",params:{message:{role:"user",parts:[{text:d}],...h?{contextId:h}:{},...f?{taskId:f}:{}},configuration:{timeoutMs:y}}},a=`/api/v1/a2a/${encodeURIComponent(c)}`,s=AbortSignal.timeout(y+1e4),o=i?AbortSignal.any([i,s]):s,l=await a2aFetch(e,a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),signal:o}),u=await l.text();let p;try{p=JSON.parse(u)}catch{p={}}if(!l.ok||p.error){if(401===l.status||403===l.status){const t=p.error??{};-32600===t.code&&r&&r("_beeos/auth_required",{type:"auth_required",target_agent_id:c,target_agent_name:c,title:"Authentication Required",content:`Agent **${c}** requires authentication.`})}const t=p.error?JSON.stringify(p.error):u;return{content:JSON.stringify({error:`send task failed: ${l.status} ${t}`})}}const k=p.result,w=k?.id,v=k?.status?.state;w&&n.create(w,c,g,m);const I=t=>{const e=t?.artifacts,n=e?.flatMap(t=>t.parts??[]).filter(t=>"text"===t.kind).map(t=>t.text).join("\n");return n||void 0};if("working"===v&&w){const t=2e3,n=Date.now()+y;for(;Date.now()<n;){if(await new Promise(e=>setTimeout(e,t)),i?.aborted)throw new DOMException("Aborted","AbortError");const n=await a2aFetch(e,`/api/v1/a2a/tasks/${encodeURIComponent(w)}`,{signal:i});if(!n.ok)break;const a=await n.json(),s=a.result??a,r=s?.status?.state;if("completed"===r){return{content:I(s)??JSON.stringify(s,null,2)}}if("failed"===r||"timeout"===r||"canceled"===r||"rejected"===r){const t=s?.status,e=t?.message?.parts,n=e?.[0]?.text??r??"task failed";return{content:JSON.stringify({error:n,task_id:w})}}}return{content:JSON.stringify({status:"working",task_id:w,message:`Task is still running. Use task_id "${w}" to check the result later.`})}}if("completed"===v){const t=I(k??{});if(t)return{content:t}}if("failed"===v){const t=k?.status,e=t?.message?.parts,n=e?.[0]?.text??"task failed";return{content:JSON.stringify({error:n})}}return{content:JSON.stringify(k,null,2)}}catch(t){if(i?.aborted)throw t;return t instanceof DOMException&&"TimeoutError"===t.name?{content:JSON.stringify({error:`task timed out after ${y}ms`,agent:c})}:{content:JSON.stringify({error:t instanceof Error?t.message:String(t)})}}}})}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,it,expect,beforeEach,vi}from"vitest";import{AcpGatewayBridgeCore}from"./bridge-core.js";import{AcpGatewaySessionState}from"./session-state.js";function makeBridge(){const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()},t=new AcpGatewaySessionState(e),a={sendResult:vi.fn(),sendAcpError:vi.fn(),sendSessionUpdate:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn(),setSessionUpdateObserver:vi.fn(),isReady:()=>!0,get forwardControl(){return{forwardThinking:!0,forwardToolCalls:!0}}},s={isConnected:!0,request:vi.fn()};return{bridge:new AcpGatewayBridgeCore({state:t,transport:a,config:{gateway:{agentId:"main"},cronMaxDurationDefaultMs:3e5,cronMaxDurationCapMs:72e5},logger:e,writeObs:vi.fn(),getGateway:()=>s,instanceId:"test-instance",thirdPartyChannels:new Set}),state:t,transport:a,gateway:s,logger:e}}async function seedOverride(e,t,a){await e.cronOverrides.set(t,a)}describe("handleCronRequest — read-side _beeos_meta hydration",()=>{let e;beforeEach(()=>{e=makeBridge()}),it("cron.list: attaches _beeos_meta.maxDurationMs to jobs with stored overrides",async()=>{await seedOverride(e.bridge,"job_with_override",18e5);e.gateway.request.mockResolvedValueOnce({jobs:[{id:"job_with_override",name:"a",description:null},{id:"job_no_override",name:"b",description:null}]}),await e.bridge.handleRequest({jsonrpc:"2.0",id:1,method:"cron/list",params:{}}),expect(e.transport.sendAcpError).not.toHaveBeenCalled();const[,t]=e.transport.sendResult.mock.calls[0],a=t.jobs;expect(a).toHaveLength(2),expect(a[0]).toMatchObject({id:"job_with_override",_beeos_meta:{maxDurationMs:18e5}}),expect(a[1]).not.toHaveProperty("_beeos_meta")}),it("cron.list: does NOT mutate the OpenClaw response in place",async()=>{await seedOverride(e.bridge,"job_test_xyz",36e5);const t={jobs:[{id:"job_test_xyz",name:"a"}]};e.gateway.request.mockResolvedValueOnce(t),await e.bridge.handleRequest({jsonrpc:"2.0",id:2,method:"cron/list",params:{}}),expect(t.jobs[0]).not.toHaveProperty("_beeos_meta")}),it("add → list round-trip: a freshly saved maxDurationMs surfaces on next list",async()=>{e.gateway.request.mockResolvedValueOnce({id:"job_rt_1"}),await e.bridge.handleRequest({jsonrpc:"2.0",id:1,method:"cron/add",params:{name:"round-trip",schedule:{kind:"cron",expression:"0 9 * * *"},sessionTarget:"isolated",payload:{kind:"agentTurn",message:"hi"},_beeos_meta:{maxDurationMs:27e5}}})
|
|
1
|
+
import{describe,it,expect,beforeEach,vi}from"vitest";import{AcpGatewayBridgeCore}from"./bridge-core.js";import{AcpGatewaySessionState}from"./session-state.js";function makeBridge(){const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()},t=new AcpGatewaySessionState(e),a={sendResult:vi.fn(),sendAcpError:vi.fn(),sendSessionUpdate:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn(),setSessionUpdateObserver:vi.fn(),isReady:()=>!0,get forwardControl(){return{forwardThinking:!0,forwardToolCalls:!0}}},s={isConnected:!0,request:vi.fn()};return{bridge:new AcpGatewayBridgeCore({state:t,transport:a,config:{gateway:{agentId:"main"},cronMaxDurationDefaultMs:3e5,cronMaxDurationCapMs:72e5},logger:e,writeObs:vi.fn(),getGateway:()=>s,instanceId:"test-instance",thirdPartyChannels:new Set}),state:t,transport:a,gateway:s,logger:e}}async function seedOverride(e,t,a){await e.cronOverrides.set(t,a)}describe("handleCronRequest — read-side _beeos_meta hydration",()=>{let e;beforeEach(()=>{e=makeBridge()}),it("cron.list: attaches _beeos_meta.maxDurationMs to jobs with stored overrides",async()=>{await seedOverride(e.bridge,"job_with_override",18e5);e.gateway.request.mockResolvedValueOnce({jobs:[{id:"job_with_override",name:"a",description:null},{id:"job_no_override",name:"b",description:null}]}),await e.bridge.handleRequest({jsonrpc:"2.0",id:1,method:"cron/list",params:{}}),expect(e.transport.sendAcpError).not.toHaveBeenCalled();const[,t]=e.transport.sendResult.mock.calls[0],a=t.jobs;expect(a).toHaveLength(2),expect(a[0]).toMatchObject({id:"job_with_override",_beeos_meta:{maxDurationMs:18e5}}),expect(a[1]).not.toHaveProperty("_beeos_meta")}),it("cron.list: does NOT mutate the OpenClaw response in place",async()=>{await seedOverride(e.bridge,"job_test_xyz",36e5);const t={jobs:[{id:"job_test_xyz",name:"a"}]};e.gateway.request.mockResolvedValueOnce(t),await e.bridge.handleRequest({jsonrpc:"2.0",id:2,method:"cron/list",params:{}}),expect(t.jobs[0]).not.toHaveProperty("_beeos_meta")}),it("add → list round-trip: a freshly saved maxDurationMs surfaces on next list",async()=>{e.gateway.request.mockResolvedValueOnce({id:"job_rt_1"}),await e.bridge.handleRequest({jsonrpc:"2.0",id:1,method:"cron/add",params:{name:"round-trip",schedule:{kind:"cron",expression:"0 9 * * *"},sessionTarget:"isolated",payload:{kind:"agentTurn",message:"hi"},_beeos_meta:{maxDurationMs:27e5}}});const t=e.bridge.cronOverrides;await vi.waitFor(()=>{expect(t.get("job_rt_1")).toBe(27e5)}),e.gateway.request.mockResolvedValueOnce({jobs:[{id:"job_rt_1",name:"round-trip"}]}),await e.bridge.handleRequest({jsonrpc:"2.0",id:2,method:"cron/list",params:{}});const a=e.transport.sendResult.mock.calls.findIndex(e=>{const t=e[1];return"object"==typeof t&&null!==t&&Array.isArray(t.jobs)});expect(a).toBeGreaterThanOrEqual(0);const[,s]=e.transport.sendResult.mock.calls[a],o=s.jobs;expect(o[0]).toMatchObject({id:"job_rt_1",_beeos_meta:{maxDurationMs:27e5}})}),it("cron.status: enriches keyed `jobs` map entries that carry an id",async()=>{await seedOverride(e.bridge,"job_status_1",9e5),e.gateway.request.mockResolvedValueOnce({jobs:{job_status_1:{id:"job_status_1",state:"running"},job_status_2:{id:"job_status_2",state:"idle"}}}),await e.bridge.handleRequest({jsonrpc:"2.0",id:3,method:"cron/status",params:{}});const[,t]=e.transport.sendResult.mock.calls[0],a=t.jobs;expect(a.job_status_1).toMatchObject({_beeos_meta:{maxDurationMs:9e5}}),expect(a.job_status_2).not.toHaveProperty("_beeos_meta")})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,it,expect,vi}from"vitest";import{AcpGatewayBridgeCore}from"./bridge-core.js";import{AcpGatewaySessionState}from"./session-state.js";import{AcpTransport}from"./transport.js";function makeLogger(){return{info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()}}function makeConfig(){return{agentGatewayUrl:"http://localhost:0",keyFile:"/tmp/key",bridge:{url:"",service:"bridge",shell:{enabled:!1}},gateway:{url:"",token:"",protocol:1,clientId:"c",agentId:"main"},retry:{baseMs:100,maxMs:1e3,maxAttempts:0},log:{enabled:!1,verbose:!1,dir:"/tmp"},promptTimeoutMs:6e4,terminalWsPort:0,terminalWsAllowedOrigins:[],streamIdleTimeoutMs:6e4,streamStaleTimeoutMs:6e4,cronSignalWindowMs:5e3,cronMaxDurationDefaultMs:3e5,cronMaxDurationCapMs:18e5,canvasRelayUrl:"",canvasEnabled:!1,openclawCanvasCompat:!1,messageBus:{enabled:!1,serviceUrl:"",url:"",principalId:"",a2aEnabled:!1}}}function buildCore(){const e=makeLogger(),s=vi.fn(),t=new AcpGatewaySessionState(e),n=vi.fn(),a=new AcpTransport(n,s);return{core:new AcpGatewayBridgeCore({state:t,transport:a,config:makeConfig(),logger:e,writeObs:s,getGateway:()=>null,instanceId:"test-instance"}),state:t,transport:a,logger:e,writeObs:s,sendBridgeMessage:n}}function registerSession(e,s,t={}){const n={sessionId:s,gatewaySessionKey:`agent:main:${s}`,agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set,...t};return e.sessions.set(n.sessionId,n),e.sessionsByGatewayKey.set(n.gatewaySessionKey,n),n}async function callSessionLoad(e,s,t){const n={jsonrpc:"2.0",id:s,method:"session/load",params:{sessionId:t}};await e.handleSessionLoad(n)}function collectEmittedUpdates(e){return e.mock.calls.map(e=>e[0]).filter(e=>"session/update"===e?.method).map(e=>e.params?.update).filter(e=>!!e)}function userChunkTexts(e){return e.filter(e=>"user_message_chunk"===e.sessionUpdate).map(e=>{const s=e.content;return"text"===s?.type?s.text??"":""})}describe("session/load — in-flight user buffer (Plan B)",()=>{it("replays pendingUserTurnUpdates when history has nothing yet (mirrors: prompt sent, OpenClaw not yet persisted)",async()=>{const e=buildCore(),s=registerSession(e.state,"session:inflight-user");s.pendingUserTurnUpdates=[{sessionUpdate:"user_message_chunk",content:{type:"text",text:"hello"},timestamp:(new Date).toISOString()}],await callSessionLoad(e.core,1,s.sessionId);const t=collectEmittedUpdates(e.sendBridgeMessage);expect(userChunkTexts(t)).toEqual(["hello"])}),it("
|
|
1
|
+
import{describe,it,expect,vi}from"vitest";import{AcpGatewayBridgeCore}from"./bridge-core.js";import{AcpGatewaySessionState}from"./session-state.js";import{AcpTransport}from"./transport.js";function makeLogger(){return{info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()}}function makeConfig(){return{agentGatewayUrl:"http://localhost:0",keyFile:"/tmp/key",bridge:{url:"",service:"bridge",shell:{enabled:!1}},gateway:{url:"",token:"",protocol:1,clientId:"c",agentId:"main"},retry:{baseMs:100,maxMs:1e3,maxAttempts:0},log:{enabled:!1,verbose:!1,dir:"/tmp"},promptTimeoutMs:6e4,terminalWsPort:0,terminalWsAllowedOrigins:[],streamIdleTimeoutMs:6e4,streamStaleTimeoutMs:6e4,cronSignalWindowMs:5e3,cronMaxDurationDefaultMs:3e5,cronMaxDurationCapMs:18e5,canvasRelayUrl:"",canvasEnabled:!1,openclawCanvasCompat:!1,messageBus:{enabled:!1,serviceUrl:"",url:"",principalId:"",a2aEnabled:!1}}}function buildCore(){const e=makeLogger(),s=vi.fn(),t=new AcpGatewaySessionState(e),n=vi.fn(),a=new AcpTransport(n,s);return{core:new AcpGatewayBridgeCore({state:t,transport:a,config:makeConfig(),logger:e,writeObs:s,getGateway:()=>null,instanceId:"test-instance"}),state:t,transport:a,logger:e,writeObs:s,sendBridgeMessage:n}}function registerSession(e,s,t={}){const n={sessionId:s,gatewaySessionKey:`agent:main:${s}`,agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set,...t};return e.sessions.set(n.sessionId,n),e.sessionsByGatewayKey.set(n.gatewaySessionKey,n),n}async function callSessionLoad(e,s,t){const n={jsonrpc:"2.0",id:s,method:"session/load",params:{sessionId:t}};await e.handleSessionLoad(n)}function collectEmittedUpdates(e){return e.mock.calls.map(e=>e[0]).filter(e=>"session/update"===e?.method).map(e=>e.params?.update).filter(e=>!!e)}function userChunkTexts(e){return e.filter(e=>"user_message_chunk"===e.sessionUpdate).map(e=>{const s=e.content;return"text"===s?.type?s.text??"":""})}describe("session/load — in-flight user buffer (Plan B)",()=>{it("replays pendingUserTurnUpdates when history has nothing yet (mirrors: prompt sent, OpenClaw not yet persisted)",async()=>{const e=buildCore(),s=registerSession(e.state,"session:inflight-user");s.pendingUserTurnUpdates=[{sessionUpdate:"user_message_chunk",content:{type:"text",text:"hello"},timestamp:(new Date).toISOString()}],await callSessionLoad(e.core,1,s.sessionId);const t=collectEmittedUpdates(e.sendBridgeMessage);expect(userChunkTexts(t)).toEqual(["hello"])}),it("RETAINS the buffer after handleSessionMessage(role=user) while history still lags (prevents lost user bubble)",async()=>{const e=buildCore(),s=registerSession(e.state,"session:persisted-user",{lastTurnIdByRole:{user:"req:t1"}});s.pendingUserTurnUpdates=[{sessionUpdate:"user_message_chunk",content:{type:"text",text:"hello"},timestamp:(new Date).toISOString()}];const t={sessionKey:s.gatewaySessionKey,messageId:"a1b2c3d4",message:{role:"user",timestamp:Date.now()}};e.core.handleSessionMessage(t),expect(s.pendingUserTurnUpdates).toHaveLength(1),e.sendBridgeMessage.mockClear(),await callSessionLoad(e.core,2,s.sessionId);const n=collectEmittedUpdates(e.sendBridgeMessage);expect(userChunkTexts(n)).toEqual(["hello"])}),it("clears the buffer on session/load once chat.history has indexed the user turn (prevents double-emit)",async()=>{const e=buildCore(),s=registerSession(e.state,"session:history-indexed",{lastTurnIdByRole:{user:"req:t1"}});s.pendingUserTurnUpdates=[{sessionUpdate:"user_message_chunk",content:{type:"text",text:"hello"},timestamp:(new Date).toISOString()}],e.core.handleSessionMessage({sessionKey:s.gatewaySessionKey,messageId:"a1b2c3d4",message:{role:"user",timestamp:Date.now()}}),expect(s.pendingUserTurnUpdates).toHaveLength(1),vi.spyOn(e.core.localSessionHistory,"readEntries").mockReturnValue([{role:"user",content:[{type:"text",text:"hello"}],timestamp:Date.now(),__openclaw:{id:"a1b2c3d4"}}]),await callSessionLoad(e.core,2,s.sessionId),expect(s.pendingUserTurnUpdates).toEqual([])}),it("cleanupPrompt discards pendingUserTurnUpdates so a stale buffer never outlives the run",async()=>{const e=buildCore(),s=registerSession(e.state,"session:cleanup-buffer");s.pendingUserTurnUpdates=[{sessionUpdate:"user_message_chunk",content:{type:"text",text:"stale"},timestamp:(new Date).toISOString()}],e.core.cleanupPrompt(s),expect(s.pendingUserTurnUpdates).toBeUndefined()})});
|
package/dist/canvas/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render a Google A2UI v0.8 component tree to the Canvas panel. Output a flat array of ComponentInstance objects with id references. ALWAYS prefer this over the canvas tool for displaying structured UI.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","root","components"],properties:{id:{type:"string",description:"Surface ID (e.g. 'sales-report'). Reuse same id to update an existing surface."},root:{type:"string",description:"ID of the root component (must reference one of the components below)."},components:{type:"array",description:"Flat array of A2UI ComponentInstance objects. Each has a unique id and exactly one component type key.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:"Unique component ID referenced by parent via child/children."},component:{type:"object",description:"Exactly ONE key = component type (Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart). Value = that component's properties object."}}}},data:{type:"object",description:"Optional data model for two-way binding. Keys are paths (e.g. '/name'), values are defaults."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=r&&r!==t?t:void 0,p=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(p,s),i.info?.(`[canvas] Joined canvas=${p} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),p}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,isCanvasActive:o,getGatewaySessionKey:i}=e;let r=0;return async(s,l)=>{if(o){const e=i?.(s);if(!o(e))return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."}}const c=l,d=c.id,p=c.root,u="panel";let m=c.components;if("string"==typeof m)try{m=JSON.parse(m)}catch{}if(!(d&&p&&Array.isArray(m)&&m.length))return{success:!1,error:"id, root, and components (array) are required"};const h=normalizeComponents(m);if(!t.validateComponentCount(h))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const g=c.data,y=e.getActiveSessionId?.(s);if(!y)return n.warn?.(`[canvas] No active session for toolCallId=${s}, skipping render_ui`),{success:!1,error:"No active session — canvas operation skipped"};const v=e.getGatewaySessionKey?.(s),f=await ensureRelayConnected(e,y,s,v),x=t.has(d,f??void 0),b=Date.now(),C=t.getRelayClient();f&&C&&b-r>6e4&&C.createSnapshot(f,"Pre-agent edit").then(()=>{r=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),x?t.update(d,h,f,p,g):t.create(f??`_fallback_${Date.now()}`,d,u,h,{origin:{kind:"agent"},rootComponentId:p,dataModel:g});const T=h.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:x?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:u,componentCount:h.length,componentTypes:T}}),n.debug?.(`[canvas] ${x?"Updated":"Created"} surface=${d} components=${h.length}`);const w={success:!0,surfaceId:d,presentation:u,isUpdate:x,activeSurfaces:f?t.getAllActive(f).map(e=>e.surfaceId):[d]};return g&&(w.dataModel=g),w}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI — Google A2UI v0.8\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.**\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n\nParameters:\n- `id`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- `root`: The ID of the top-level component (usually a Column).\n- `components`: Flat array of objects. **Each object MUST have `id` and `component`**:\n `{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}`\n The `component` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- `data` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same `id` updates existing surface; different `id` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: `{"literalString": "Hello"}`\n- Data binding: `{"path": "/fieldName"}` (reads from the `data` model)\n\nShorthand: `sv("text")` means `{"literalString": "text"}` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n`{Text: {text: sv("Title"), usageHint: "h2"}}`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}`\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n`{Icon: {name: sv("check")}}`\n\n**Video** — Embed a video.\n`{Video: {url: sv("https://youtube.com/...")}}`\n\n**AudioPlayer** — Audio playback.\n`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}`\n\n**Row** — Horizontal layout. Children referenced by ID.\n`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n`{Column: {children: {explicitList: ["child1", "child2"]}}}`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}`\n\n**Card** — Container with a single child.\n`{Card: {child: "contentColumnId"}}`\n\n**Tabs** — Tabbed container.\n`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}`\n\n**Divider** — Horizontal or vertical separator.\n`{Divider: {axis: "horizontal"}}`\n\n**Modal** — Dialog with trigger.\n`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}`\n\n**Button** — Clickable with action.\n`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}`\n\n**TextField** — Text input with data binding.\n`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}`\n\n**DateTimeInput** — Date/time picker.\n`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}`\n\n**MultipleChoice** — Selection from options.\n`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}`\n\n**Slider** — Numeric range input.\n`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}`\n\n**Chart** (BeeOS extension) — Data visualization.\n`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition Patterns\n\n**Simple text display:**\n```json\n{"id":"s1","root":"r","components":[\n {"id":"t","component":{"Text":{"text":{"literalString":"Hello World"},"usageHint":"h2"}}},\n {"id":"r","component":{"Column":{"children":{"explicitList":["t"]}}}}\n]}\n```\n\n**Dashboard with KPI cards:**\n```json\n{"id":"dashboard","root":"root","components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Dashboard"},"usageHint":"h2"}}},\n {"id":"rev-val","component":{"Text":{"text":{"literalString":"$1.2M"},"usageHint":"h2"}}},\n {"id":"rev-lbl","component":{"Text":{"text":{"literalString":"Revenue"},"usageHint":"caption"}}},\n {"id":"rev-col","component":{"Column":{"children":{"explicitList":["rev-lbl","rev-val"]},"alignment":"center"}}},\n {"id":"rev-card","component":{"Card":{"child":"rev-col"}}},\n {"id":"users-val","component":{"Text":{"text":{"literalString":"8,432"},"usageHint":"h2"}}},\n {"id":"users-lbl","component":{"Text":{"text":{"literalString":"Users"},"usageHint":"caption"}}},\n {"id":"users-col","component":{"Column":{"children":{"explicitList":["users-lbl","users-val"]},"alignment":"center"}}},\n {"id":"users-card","component":{"Card":{"child":"users-col"}}},\n {"id":"kpi-row","component":{"Row":{"children":{"explicitList":["rev-card","users-card"]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","kpi-row"]}}}}\n]}\n```\n\n**Table layout (Column + Row + Text):**\n```json\n{"id":"users-table","root":"root","components":[\n {"id":"h-name","component":{"Text":{"text":{"literalString":"Name"},"usageHint":"caption"}}},\n {"id":"h-role","component":{"Text":{"text":{"literalString":"Role"},"usageHint":"caption"}}},\n {"id":"header","component":{"Row":{"children":{"explicitList":["h-name","h-role"]}}}},\n {"id":"div","component":{"Divider":{"axis":"horizontal"}}},\n {"id":"r1-name","component":{"Text":{"text":{"literalString":"Alice"},"usageHint":"body"}}},\n {"id":"r1-role","component":{"Text":{"text":{"literalString":"Engineer"},"usageHint":"body"}}},\n {"id":"row1","component":{"Row":{"children":{"explicitList":["r1-name","r1-role"]}}}},\n {"id":"r2-name","component":{"Text":{"text":{"literalString":"Bob"},"usageHint":"body"}}},\n {"id":"r2-role","component":{"Text":{"text":{"literalString":"Designer"},"usageHint":"body"}}},\n {"id":"row2","component":{"Row":{"children":{"explicitList":["r2-name","r2-role"]}}}},\n {"id":"table-col","component":{"Column":{"children":{"explicitList":["header","div","row1","row2"]}}}},\n {"id":"root","component":{"Card":{"child":"table-col"}}}\n]}\n```\n\n**Form with data binding:**\n```json\n{"id":"settings","root":"root","data":{"/name":"","/email":"","/notify":false},"components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Settings"},"usageHint":"h3"}}},\n {"id":"f-name","component":{"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"},"type":"shortText"}}},\n {"id":"f-email","component":{"TextField":{"label":{"literalString":"Email"},"text":{"path":"/email"},"type":"shortText"}}},\n {"id":"f-notify","component":{"CheckBox":{"label":{"literalString":"Email notifications"},"value":{"path":"/notify"}}}},\n {"id":"btn-text","component":{"Text":{"text":{"literalString":"Save"},"usageHint":"body"}}},\n {"id":"btn","component":{"Button":{"child":"btn-text","action":{"name":"save_settings","context":[{"key":"name","value":{"path":"/name"}},{"key":"email","value":{"path":"/email"}},{"key":"notify","value":{"path":"/notify"}}]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","f-name","f-email","f-notify","btn"]}}}}\n]}\n```\n\n**Chart:**\n```json\n{"id":"sales-chart","root":"root","components":[\n {"id":"chart","component":{"Chart":{"chartType":{"literalString":"bar"},"title":{"literalString":"Monthly Sales"},"labels":["Jan","Feb","Mar"],"datasets":[{"label":"Revenue","data":[100,200,150]}]}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["chart"]}}}}\n]}\n```\n\n### render_form\nUse `render_form` for schema-driven forms when a domain schema exists.\nParameters: `domain`, `operation`, `data`, `entityId`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n';export const CANVAS_TERM_MAPPING='[System Note] Always use `render_ui` (not the canvas tool) for visual/structured content. Each component in the array MUST be: {"id":"x","component":{"TypeName":{props}}} — the "component" wrapper key is required. render_ui uses Google A2UI v0.8 format: flat component array with id references. Components: Text (usageHint: h1-h5/caption/body), Image, Icon, Video, AudioPlayer, Row (horizontal layout, children.explicitList), Column (vertical layout, children.explicitList), List, Card (single child), Tabs (tabItems), Divider, Modal, Button (child + action), TextField, CheckBox, MultipleChoice, Slider, DateTimeInput, Chart (BeeOS extension: chartType, labels, datasets). StringValue: {"literalString":"text"} or {"path":"/binding"}. root param = top-level component ID. Tables: Card > Column > Row > Text. Forms: TextField/CheckBox/Slider + Button with action.context paths. Chart: chartType(bar/line/pie/area/scatter/doughnut/radar/funnel/gauge/candlestick/sparkline), labels, datasets:[{label,data,color?}]. Candlestick: [{open,high,low,close}]. Gauge: value+max. Keep chat brief when canvas is used. Use `close_ui` to dismiss. Reuse same id to update; new id for new content.';export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id and provide the full updated component tree.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
|
|
1
|
+
const A2UI_TYPE_NAMES=new Set(["Text","Image","Icon","Video","AudioPlayer","Row","Column","List","Card","Tabs","Divider","Modal","Button","CheckBox","TextField","DateTimeInput","MultipleChoice","Slider","Chart","WebEmbed","FilePreview","MapView"]);function normalizeComponents(e){return e.map(e=>{if(!e||"object"!=typeof e)return e;const t=e;if(t.component&&"object"==typeof t.component)return t;for(const e of Object.keys(t))if(A2UI_TYPE_NAMES.has(e)&&"object"==typeof t[e])return{id:t.id,component:{[e]:t[e]}};return t})}export const RENDER_UI_TOOL_NAME="render_ui";export const CLOSE_UI_TOOL_NAME="close_ui";export const RENDER_UI_TOOL_DESCRIPTION="Render a Google A2UI v0.8 component tree to the Canvas panel. Output a flat array of ComponentInstance objects with id references. ALWAYS prefer this over the canvas tool for displaying structured UI.";export const RENDER_UI_TOOL_SCHEMA={type:"object",required:["id","root","components"],properties:{id:{type:"string",description:"Surface ID (e.g. 'sales-report'). Reuse same id to update an existing surface."},root:{type:"string",description:"ID of the root component (must reference one of the components below)."},components:{type:"array",description:"Flat array of A2UI ComponentInstance objects. Each has a unique id and exactly one component type key.",items:{type:"object",required:["id"],properties:{id:{type:"string",description:"Unique component ID referenced by parent via child/children."},component:{type:"object",description:"Exactly ONE key = component type (Text, Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Divider, Modal, Button, CheckBox, TextField, DateTimeInput, MultipleChoice, Slider, Chart). Value = that component's properties object."}}}},data:{type:"object",description:"Optional data model for two-way binding. Keys are paths (e.g. '/name'), values are defaults."}}};export const CLOSE_UI_TOOL_DESCRIPTION="Close and remove a UI surface that was previously rendered.";export const CLOSE_UI_TOOL_SCHEMA={type:"object",required:["id"],properties:{id:{type:"string",description:"The UI identifier to close"}}};const SESSION_READY_POLL_ATTEMPTS=3,SESSION_READY_POLL_INTERVAL_MS=400,delay=e=>new Promise(t=>setTimeout(t,e));async function ensureRelayConnected(e,t,n,a){const{stateManager:o,logger:i}=e,r=a??e.getGatewaySessionKey?.(n),s=r??t,l=o.resolveCanvasForSession(s);if(l&&o.isCanvasJoined(l))return l;let c=o.getRelayClient();if(c||(c=await o.waitForRelayClient(),c))try{e.waitForAgentSync&&await e.waitForAgentSync();const a=e.getPlatformAgentId?.(n),l=r??t,d=r&&r!==t?t:void 0,p=await c.ensureCanvas(l,{agentId:a,linkSessionId:d});return await o.joinCanvas(p,s),i.info?.(`[canvas] Joined canvas=${p} session=${t} gatewayKey=${r??"none"} agentId=${a??"none"}`),p}catch(e){return void i.warn?.(`[canvas] Failed to join canvas relay: ${e}`)}}export function createRenderUiHandler(e){const{stateManager:t,logger:n,writeObs:a,isCanvasActive:o,getGatewaySessionKey:i}=e;let r=0;return async(s,l)=>{if(o){const e=i?.(s);if(!o(e))return{success:!1,error:"Canvas is disabled by user. Respond with plain text instead."}}const c=l,d=c.id,p=c.root,u="panel";let m=c.components;if("string"==typeof m)try{m=JSON.parse(m)}catch{}if(!(d&&p&&Array.isArray(m)&&m.length))return{success:!1,error:"id, root, and components (array) are required"};const h=normalizeComponents(m);if(!t.validateComponentCount(h))return{success:!1,error:"Too many components (max 500)"};if(!t.checkRateLimit(d))return n.warn?.(`[canvas] Rate limited surfaceId=${d}`),a({component:"canvas",domain:"canvas",name:"canvas.rate_limited",severity:"warn",payload:{surfaceId:d}}),{success:!1,error:"Rate limited — too many updates per second"};const g=c.data;let y=e.getActiveSessionId?.(s);for(let t=0;!y&&t<3;t++)await delay(400),y=e.getActiveSessionId?.(s);if(!y)return n.warn?.(`[canvas] No active session for toolCallId=${s} after 3 polls, render_ui not ready`),{success:!1,retryable:!0,error:"Canvas session not ready yet (agent just connected) — retry render_ui shortly; do NOT fall back to plain text."};const f=e.getGatewaySessionKey?.(s),v=await ensureRelayConnected(e,y,s,f),x=t.has(d,v??void 0),b=Date.now(),C=t.getRelayClient();v&&C&&b-r>6e4&&C.createSnapshot(v,"Pre-agent edit").then(()=>{r=Date.now(),n.debug?.("[canvas] Pre-edit checkpoint created")}).catch(e=>n.warn?.(`[canvas] Pre-edit checkpoint failed: ${e}`)),x?t.update(d,h,v,p,g):t.create(v??`_fallback_${Date.now()}`,d,u,h,{origin:{kind:"agent"},rootComponentId:p,dataModel:g});const T=h.map(e=>e.component&&"object"==typeof e.component?Object.keys(e.component)[0]??"unknown":"unknown");a({component:"canvas",domain:"canvas",name:x?"canvas.update_sent":"canvas.surface_created",severity:"info",payload:{surfaceId:d,presentation:u,componentCount:h.length,componentTypes:T}}),n.debug?.(`[canvas] ${x?"Updated":"Created"} surface=${d} components=${h.length}`);const S={success:!0,surfaceId:d,presentation:u,isUpdate:x,activeSurfaces:v?t.getAllActive(v).map(e=>e.surfaceId):[d]};return g&&(S.dataModel=g),S}}export function createCloseUiHandler(e){const{stateManager:t,logger:n,writeObs:a}=e;return async(o,i)=>{const r=i.id;if(!r)return{success:!1,error:"id is required"};const s=t.get(r),l=s?Date.now()-s.createdAt:0,c=e.getGatewaySessionKey?.(o),d=c?t.resolveCanvasForSession(c):void 0;return t.delete(r,d),a({component:"canvas",domain:"canvas",name:"canvas.surface_closed",severity:"info",payload:{surfaceId:r,lifetime_ms:l}}),n.debug?.(`[canvas] Closed surface=${r}`),{success:!0,surfaceId:r,message:"UI closed"}}}export const CANVAS_SYSTEM_PROMPT='## Canvas UI — Google A2UI v0.8\n\n**IMPORTANT: Always use `render_ui` (not the built-in `canvas` tool) to display visual/structured content.**\n\n### How render_ui works\n\nYou output a **flat array of components** with unique IDs. Components reference each other by ID.\n\nParameters:\n- `id`: Surface ID (e.g. "sales-report"). Reuse same id to update.\n- `root`: The ID of the top-level component (usually a Column).\n- `components`: Flat array of objects. **Each object MUST have `id` and `component`**:\n `{"id": "myId", "component": {"Text": {"text": {"literalString": "Hello"}, "usageHint": "body"}}}`\n The `component` key wraps exactly ONE type key (Text, Card, Row, etc.) and its properties.\n- `data` (optional): Data model object for form two-way binding.\n\n### Rules\n- Keep chat reply to ONE short sentence when using render_ui. Do NOT repeat data shown in canvas.\n- Use render_ui for structured data, visualizations, forms, dashboards. Use plain text for explanations.\n- Canvas renders in a narrow sidebar (~400px).\n- Prefer fewer, larger components. One table is better than many small cards.\n- Use descriptive surface IDs (e.g. "sales-report", not "ui1").\n- Same `id` updates existing surface; different `id` creates new one.\n- When asked to modify existing content, reuse the original surface id from [Canvas State].\n\n### StringValue\n\nText properties use StringValue — an object with exactly one key:\n- Static text: `{"literalString": "Hello"}`\n- Data binding: `{"path": "/fieldName"}` (reads from the `data` model)\n\nShorthand: `sv("text")` means `{"literalString": "text"}` in examples below.\n\n### Component Reference\n\n**Text** — Display text with semantic sizing.\n`{Text: {text: sv("Title"), usageHint: "h2"}}`\nusageHint: "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body"\ntext also renders Markdown (bold, italic, lists, code blocks, links).\n\n**Image** — Display an image.\n`{Image: {url: sv("https://..."), usageHint: "mediumFeature"}}`\nusageHint: "icon" | "avatar" | "smallFeature" | "mediumFeature" | "largeFeature" | "header"\nfit (optional): "contain" | "cover" | "fill" | "none" | "scale-down"\n\n**Icon** — Material icon.\n`{Icon: {name: sv("check")}}`\n\n**Video** — Embed a video.\n`{Video: {url: sv("https://youtube.com/...")}}`\n\n**AudioPlayer** — Audio playback.\n`{AudioPlayer: {url: sv("https://..."), description: sv("Episode 1")}}`\n\n**Row** — Horizontal layout. Children referenced by ID.\n`{Row: {children: {explicitList: ["child1", "child2"]}, distribution: "spaceBetween"}}`\ndistribution: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly"\nalignment: "start" | "center" | "end" | "stretch"\n\n**Column** — Vertical layout. Children referenced by ID.\n`{Column: {children: {explicitList: ["child1", "child2"]}}}`\nSame distribution/alignment options as Row.\n\n**List** — Vertical or horizontal list.\n`{List: {children: {explicitList: ["item1", "item2"]}, direction: "vertical"}}`\n\n**Card** — Container with a single child.\n`{Card: {child: "contentColumnId"}}`\n\n**Tabs** — Tabbed container.\n`{Tabs: {tabItems: [{title: sv("Tab 1"), child: "panel1"}, {title: sv("Tab 2"), child: "panel2"}]}}`\n\n**Divider** — Horizontal or vertical separator.\n`{Divider: {axis: "horizontal"}}`\n\n**Modal** — Dialog with trigger.\n`{Modal: {entryPointChild: "triggerBtnId", contentChild: "dialogContentId"}}`\n\n**Button** — Clickable with action.\n`{Button: {child: "labelTextId", action: {name: "onClick", context: [{key: "id", value: {literalString: "item-1"}}]}}}`\n\n**TextField** — Text input with data binding.\n`{TextField: {label: sv("Name"), text: {path: "/name"}, type: "shortText"}}`\ntype: "shortText" | "number" | "date" | "longText"\n\n**CheckBox** — Boolean toggle.\n`{CheckBox: {label: sv("Agree"), value: {path: "/agree"}}}`\n\n**DateTimeInput** — Date/time picker.\n`{DateTimeInput: {value: {path: "/date"}, enableDate: true, enableTime: false}}`\n\n**MultipleChoice** — Selection from options.\n`{MultipleChoice: {selections: {path: "/country"}, options: [{label: sv("US"), value: "US"}, {label: sv("CN"), value: "CN"}]}}`\n\n**Slider** — Numeric range input.\n`{Slider: {value: {path: "/volume"}, minValue: 0, maxValue: 100}}`\n\n**Chart** (BeeOS extension) — Data visualization.\n`{Chart: {chartType: sv("bar"), title: sv("Sales"), labels: ["Q1","Q2","Q3"], datasets: [{label: "Revenue", data: [100,200,150]}]}}`\nchartType: bar, horizontal_bar, line, area, scatter, pie, doughnut, radar, gauge, candlestick, funnel, sparkline\ngauge uses value + max instead of labels/datasets.\ncandlestick data: [{open,high,low,close}] (full field names, NOT o/h/l/c).\n\n### Composition Patterns\n\n**Simple text display:**\n```json\n{"id":"s1","root":"r","components":[\n {"id":"t","component":{"Text":{"text":{"literalString":"Hello World"},"usageHint":"h2"}}},\n {"id":"r","component":{"Column":{"children":{"explicitList":["t"]}}}}\n]}\n```\n\n**Dashboard with KPI cards:**\n```json\n{"id":"dashboard","root":"root","components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Dashboard"},"usageHint":"h2"}}},\n {"id":"rev-val","component":{"Text":{"text":{"literalString":"$1.2M"},"usageHint":"h2"}}},\n {"id":"rev-lbl","component":{"Text":{"text":{"literalString":"Revenue"},"usageHint":"caption"}}},\n {"id":"rev-col","component":{"Column":{"children":{"explicitList":["rev-lbl","rev-val"]},"alignment":"center"}}},\n {"id":"rev-card","component":{"Card":{"child":"rev-col"}}},\n {"id":"users-val","component":{"Text":{"text":{"literalString":"8,432"},"usageHint":"h2"}}},\n {"id":"users-lbl","component":{"Text":{"text":{"literalString":"Users"},"usageHint":"caption"}}},\n {"id":"users-col","component":{"Column":{"children":{"explicitList":["users-lbl","users-val"]},"alignment":"center"}}},\n {"id":"users-card","component":{"Card":{"child":"users-col"}}},\n {"id":"kpi-row","component":{"Row":{"children":{"explicitList":["rev-card","users-card"]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","kpi-row"]}}}}\n]}\n```\n\n**Table layout (Column + Row + Text):**\n```json\n{"id":"users-table","root":"root","components":[\n {"id":"h-name","component":{"Text":{"text":{"literalString":"Name"},"usageHint":"caption"}}},\n {"id":"h-role","component":{"Text":{"text":{"literalString":"Role"},"usageHint":"caption"}}},\n {"id":"header","component":{"Row":{"children":{"explicitList":["h-name","h-role"]}}}},\n {"id":"div","component":{"Divider":{"axis":"horizontal"}}},\n {"id":"r1-name","component":{"Text":{"text":{"literalString":"Alice"},"usageHint":"body"}}},\n {"id":"r1-role","component":{"Text":{"text":{"literalString":"Engineer"},"usageHint":"body"}}},\n {"id":"row1","component":{"Row":{"children":{"explicitList":["r1-name","r1-role"]}}}},\n {"id":"r2-name","component":{"Text":{"text":{"literalString":"Bob"},"usageHint":"body"}}},\n {"id":"r2-role","component":{"Text":{"text":{"literalString":"Designer"},"usageHint":"body"}}},\n {"id":"row2","component":{"Row":{"children":{"explicitList":["r2-name","r2-role"]}}}},\n {"id":"table-col","component":{"Column":{"children":{"explicitList":["header","div","row1","row2"]}}}},\n {"id":"root","component":{"Card":{"child":"table-col"}}}\n]}\n```\n\n**Form with data binding:**\n```json\n{"id":"settings","root":"root","data":{"/name":"","/email":"","/notify":false},"components":[\n {"id":"title","component":{"Text":{"text":{"literalString":"Settings"},"usageHint":"h3"}}},\n {"id":"f-name","component":{"TextField":{"label":{"literalString":"Name"},"text":{"path":"/name"},"type":"shortText"}}},\n {"id":"f-email","component":{"TextField":{"label":{"literalString":"Email"},"text":{"path":"/email"},"type":"shortText"}}},\n {"id":"f-notify","component":{"CheckBox":{"label":{"literalString":"Email notifications"},"value":{"path":"/notify"}}}},\n {"id":"btn-text","component":{"Text":{"text":{"literalString":"Save"},"usageHint":"body"}}},\n {"id":"btn","component":{"Button":{"child":"btn-text","action":{"name":"save_settings","context":[{"key":"name","value":{"path":"/name"}},{"key":"email","value":{"path":"/email"}},{"key":"notify","value":{"path":"/notify"}}]}}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["title","f-name","f-email","f-notify","btn"]}}}}\n]}\n```\n\n**Chart:**\n```json\n{"id":"sales-chart","root":"root","components":[\n {"id":"chart","component":{"Chart":{"chartType":{"literalString":"bar"},"title":{"literalString":"Monthly Sales"},"labels":["Jan","Feb","Mar"],"datasets":[{"label":"Revenue","data":[100,200,150]}]}}},\n {"id":"root","component":{"Column":{"children":{"explicitList":["chart"]}}}}\n]}\n```\n\n### render_form\nUse `render_form` for schema-driven forms when a domain schema exists.\nParameters: `domain`, `operation`, `data`, `entityId`.\n\n### close_ui\nCloses a UI surface. Use when user moves to a new topic or explicitly dismisses.\n';export const CANVAS_TERM_MAPPING='[System Note] Always use `render_ui` (not the canvas tool) for visual/structured content. Each component in the array MUST be: {"id":"x","component":{"TypeName":{props}}} — the "component" wrapper key is required. render_ui uses Google A2UI v0.8 format: flat component array with id references. Components: Text (usageHint: h1-h5/caption/body), Image, Icon, Video, AudioPlayer, Row (horizontal layout, children.explicitList), Column (vertical layout, children.explicitList), List, Card (single child), Tabs (tabItems), Divider, Modal, Button (child + action), TextField, CheckBox, MultipleChoice, Slider, DateTimeInput, Chart (BeeOS extension: chartType, labels, datasets). StringValue: {"literalString":"text"} or {"path":"/binding"}. root param = top-level component ID. Tables: Card > Column > Row > Text. Forms: TextField/CheckBox/Slider + Button with action.context paths. Chart: chartType(bar/line/pie/area/scatter/doughnut/radar/funnel/gauge/candlestick/sparkline), labels, datasets:[{label,data,color?}]. Candlestick: [{open,high,low,close}]. Gauge: value+max. Keep chat brief when canvas is used. Use `close_ui` to dismiss. Reuse same id to update; new id for new content.';export const CANVAS_UPDATE_HINT="[Canvas Rule] Same id in render_ui = update existing surface. Different id = new surface. When a surface is referenced for editing, you MUST reuse its exact id and provide the full updated component tree.";const MAX_CONTEXT_SURFACES=8,MAX_CONTEXT_CHARS=500;export function buildActiveSurfacesContext(e,t){let n;if(!t)return null;if(n=e.getAllActive(t),0===n.length)return null;const a=[...n].sort((e,t)=>t.updatedAt-e.updatedAt).slice(0,8),o=n.length-a.length;let i=`[Canvas State] Active surfaces: ${a.map(t=>{const n=e.summarize(t.surfaceId);if(!n)return`"${t.surfaceId}"`;const a=n.types.slice(0,3).join(", "),o=n.title?`: "${n.title}"`:"";return`"${t.surfaceId}" (${a}${o})`}).join("; ")}.`;return o>0&&(i+=` ...and ${o} more.`),i+=" To modify existing content, reuse its id in render_ui.",i.length>500&&(i=i.slice(0,497)+"..."),i}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describe,it,expect,vi,beforeEach,afterEach}from"vitest";import{CanvasStateManager}from"./state.js";import{createRenderUiHandler}from"./tools.js";function baseParams(){return{id:"weather-shenzhen",root:"root",components:[{id:"root",component:{Text:{text:{literalString:"hi"}}}}]}}function mockRelay(){return{connected:!0,ensureCanvas:vi.fn().mockResolvedValue("canvas-test"),createSnapshot:vi.fn().mockResolvedValue(void 0)}}function makeOpts(e={}){return{stateManager:new CanvasStateManager,logger:{info:vi.fn(),warn:vi.fn(),debug:vi.fn()},writeObs:vi.fn(),getActiveSessionId:()=>{},getGatewaySessionKey:()=>{},...e}}describe("createRenderUiHandler — cold-start session race",()=>{beforeEach(()=>{vi.useFakeTimers()}),afterEach(()=>{vi.useRealTimers()}),it("returns a RETRYABLE (non-terminal) error when the session never binds",async()=>{const e=vi.fn(()=>{}),t=makeOpts({getActiveSessionId:e}),a=createRenderUiHandler(t)("tc-1",baseParams());await vi.advanceTimersByTimeAsync(1300);const s=await a;expect(s.success).toBe(!1),expect(s.retryable).toBe(!0),expect(String(s.error)).toMatch(/not ready/i),expect(String(s.error)).not.toMatch(/disabled by user/i),expect(e).toHaveBeenCalledTimes(4)}),it("succeeds once the toolCall→session mapping lands within the poll window",async()=>{const e=new CanvasStateManager;e.setRelayClient(mockRelay());let t=0;const a=makeOpts({stateManager:e,getActiveSessionId:()=>++t>=2?"session-abc":void 0}),s=createRenderUiHandler(a)("tc-2",baseParams());await vi.advanceTimersByTimeAsync(450);const n=await s;expect(t).toBeGreaterThanOrEqual(2),expect(n.success).toBe(!0),expect(n.surfaceId).toBe("weather-shenzhen")}),it("does not poll at all when the session is ready immediately",async()=>{const e=new CanvasStateManager;e.setRelayClient(mockRelay());const t=vi.fn(()=>"session-ready"),a=makeOpts({stateManager:e,getActiveSessionId:t}),s=createRenderUiHandler(a),n=await s("tc-3",baseParams());expect(n.success).toBe(!0),expect(t).toHaveBeenCalledTimes(1)})});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BeeOS <dev@beeos.ai>",
|