beeos-claw 0.5.13 → 0.5.14

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(t,e,a){const n=`${t.agentGatewayUrl}${e}`;return t.agentKeys?agentFetch(n,t.agentKeys,a):fetch(n,a)}export{a2aFetch};function normalizeState(t){if(!t)return"";const e=t.toLowerCase();return{task_state_submitted:"submitted",task_state_delivered:"submitted",task_state_working:"working",task_state_completed:"completed",task_state_failed:"failed",task_state_timeout:"timeout",task_state_canceled:"canceled",task_state_rejected:"rejected",task_state_input_required:"input-required",task_state_auth_required:"auth-required"}[e]??e}function unwrapTask(t){if(!t||"object"!=typeof t)return;const e=t,a=e.task;return a&&"object"==typeof a?a:e}function extractArtifactText(t){const e=t?.artifacts;if(!e)return;return e.flatMap(t=>t.parts??[]).filter(t=>"string"==typeof t.text&&(void 0===t.kind||"text"===t.kind)).map(t=>t.text).join("\n")||void 0}function extractStatusText(t){const e=t?.status,a=e?.message?.parts,n=a?.[0]?.text;return"string"==typeof n?n:void 0}function taskState(t){return normalizeState(t?.status?.state)}const TERMINAL_FAILURE_STATES=new Set(["failed","timeout","canceled","rejected"]);export{unwrapTask,extractArtifactText,extractStatusText,taskState,normalizeState};export class TaskHandleRegistry{seq=0;handles=new Map;taskIdToHandle=new Map;create(t,e,a,n){const r="bth_"+ ++this.seq;return this.handles.set(r,{taskId:t,agentId:e,contextId:a,sessionKey:n}),this.taskIdToHandle.set(t,r),r}resolveTaskId(t){const e=this.handles.get(t);return e?e.taskId:t}}const defaultRegistry=new TaskHandleRegistry;export function registerA2ATools(t,e,a=defaultRegistry,n,r,s,i){t.registerTool({name:"beeos_discover_agents",description:"List agents available for A2A collaboration. When called without a query, returns all visible agents (your own agents + public agents). When called with a query, filters by name, description, or skill. Results include an `isSelf` flag marking the agent that represents the current instance (do not call yourself). Each result includes `instanceName` (human-readable device/instance label) when available.",parameters:{type:"object",properties:{query:{type:"string",description:"Optional keyword to filter agents by name, description, or skill. Omit to list all agents."}},required:[]},async execute(t,a,n){const{query:r}=a??{};try{const t=new URLSearchParams({limit:"50"});r&&t.set("query",r);const a=await a2aFetch(e,`/api/v1/a2a/discover?${t}`,{signal:n});if(!a.ok)return{content:JSON.stringify({error:`discover failed: ${a.status}`})};const s=await a.json(),o=i?.();if(o&&Array.isArray(s?.data))for(const t of s.data)t.isSelf=t.instance_id===o;return{content:JSON.stringify(s,null,2)}}catch(t){if(n?.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,i,o){let{agentId:c,message:l,files:d,contextId:u,timeoutMs:g,taskId:f}=i;const p=n?.(t),m=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;if(!m.test(c)){const a=r?.(t);if(a?.length){const t=resolveAgentIdFromMentions(a,c);t&&(e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${t} via mention context`),c=t)}if(!m.test(c))try{const t=await a2aFetch(e,`/api/v1/a2a/discover?query=${encodeURIComponent(c)}&limit=10`,{signal:o});if(t.ok){const a=await t.json(),n=a?.data??[],r=c.toLowerCase(),s=n.find(t=>t.name.toLowerCase()===r)??n.find(t=>t.name.toLowerCase().includes(r));if(!s)return{content:JSON.stringify({error:`Could not resolve agent name "${c}". Available agents: ${n.map(t=>`${t.name} (${t.id})`).join(", ")||"none"}`})};e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${s.id} (${s.name}) via discover`),c=s.id}}catch{}}const h=u||(p?`session:${p}`:""),y=g||6e5;try{const t={jsonrpc:"2.0",id:`claw-${Date.now()}`,method:"SendMessage",params:{message:{role:"user",parts:[{text:l}],...h?{contextId:h}:{},...f?{taskId:f}:{}},configuration:{timeoutMs:y}}},n=`/api/v1/a2a/${encodeURIComponent(c)}`,r=AbortSignal.timeout(y+1e4),i=o?AbortSignal.any([o,r]):r,d=await a2aFetch(e,n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),signal:i}),g=await d.text();let m;try{m=JSON.parse(g)}catch{m={}}if(!d.ok||m.error){if(401===d.status||403===d.status){const t=m.error??{};-32600===t.code&&s&&s("_beeos/auth_required",{type:"auth_required",target_agent_id:c,target_agent_name:c,title:"Authentication Required",content:`Agent **${c}** requires authentication.`})}const t=m.error?JSON.stringify(m.error):g;return{content:JSON.stringify({error:`send task failed: ${d.status} ${t}`})}}const k=unwrapTask(m.result),_=k?.id,w=taskState(k);if(_&&a.create(_,c,u,p),"completed"===w){const t=extractArtifactText(k);if(t)return{content:t}}if(TERMINAL_FAILURE_STATES.has(w)){const t=extractStatusText(k)??w;return{content:JSON.stringify({error:t,task_id:_})}}if(_){const t=2e3,a=Date.now()+y;for(;Date.now()<a;){if(await new Promise(e=>setTimeout(e,t)),o?.aborted)throw new DOMException("Aborted","AbortError");const a=await a2aFetch(e,n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:`claw-poll-${Date.now()}`,method:"GetTask",params:{id:_}}),signal:o});if(!a.ok)break;let r;try{r=JSON.parse(await a.text())}catch{break}if(r.error)break;const s=unwrapTask(r.result),i=taskState(s);if("completed"===i){return{content:extractArtifactText(s)??JSON.stringify(s,null,2)}}if(TERMINAL_FAILURE_STATES.has(i)){const t=extractStatusText(s)??i??"task failed";return{content:JSON.stringify({error:t,task_id:_})}}}return{content:JSON.stringify({status:"working",task_id:_,message:`Task is still running. Use task_id "${_}" to check the result later.`})}}return{content:JSON.stringify(k??m.result,null,2)}}catch(t){if(o?.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
+ import{agentFetch}from"../agent-auth.js";import{resolveAgentIdFromMentions}from"./mention-resolver.js";async function a2aFetch(t,e,a){const n=`${t.agentGatewayUrl}${e}`;return t.agentKeys?agentFetch(n,t.agentKeys,a):fetch(n,a)}export{a2aFetch};function normalizeState(t){if(!t)return"";const e=t.toLowerCase();return{task_state_submitted:"submitted",task_state_delivered:"submitted",task_state_working:"working",task_state_completed:"completed",task_state_failed:"failed",task_state_timeout:"timeout",task_state_canceled:"canceled",task_state_rejected:"rejected",task_state_input_required:"input-required",task_state_auth_required:"auth-required"}[e]??e}function unwrapTask(t){if(!t||"object"!=typeof t)return;const e=t,a=e.task;return a&&"object"==typeof a?a:e}function extractArtifactText(t){const e=t?.artifacts;if(!e)return;return e.flatMap(t=>t.parts??[]).filter(t=>"string"==typeof t.text&&(void 0===t.kind||"text"===t.kind)).map(t=>t.text).join("\n")||void 0}function extractStatusText(t){const e=t?.status,a=e?.message?.parts,n=a?.[0]?.text;return"string"==typeof n?n:void 0}function taskState(t){return normalizeState(t?.status?.state)}const TERMINAL_FAILURE_STATES=new Set(["failed","timeout","canceled","rejected"]);function terminalFailureContent(t,e,a){const n=extractArtifactText(t);if(n)return JSON.stringify({status:e,task_id:a,result:n});const r=extractStatusText(t)??e;return JSON.stringify({error:r,task_id:a})}export{unwrapTask,extractArtifactText,extractStatusText,taskState,normalizeState,terminalFailureContent};export class TaskHandleRegistry{seq=0;handles=new Map;taskIdToHandle=new Map;create(t,e,a,n){const r="bth_"+ ++this.seq;return this.handles.set(r,{taskId:t,agentId:e,contextId:a,sessionKey:n}),this.taskIdToHandle.set(t,r),r}resolveTaskId(t){const e=this.handles.get(t);return e?e.taskId:t}resolveAgentId(t){const e=this.handles.get(t);if(e)return e.agentId;const a=this.taskIdToHandle.get(t);return a?this.handles.get(a)?.agentId:void 0}}const defaultRegistry=new TaskHandleRegistry;export function registerA2ATools(t,e,a=defaultRegistry,n,r,s,i){t.registerTool({name:"beeos_discover_agents",description:"List agents available for A2A collaboration. When called without a query, returns all visible agents (your own agents + public agents). When called with a query, filters by name, description, or skill. Results include an `isSelf` flag marking the agent that represents the current instance (do not call yourself). Each result includes `instanceName` (human-readable device/instance label) when available.",parameters:{type:"object",properties:{query:{type:"string",description:"Optional keyword to filter agents by name, description, or skill. Omit to list all agents."}},required:[]},async execute(t,a,n){const{query:r}=a??{};try{const t=new URLSearchParams({limit:"50"});r&&t.set("query",r);const a=await a2aFetch(e,`/api/v1/a2a/discover?${t}`,{signal:n});if(!a.ok)return{content:JSON.stringify({error:`discover failed: ${a.status}`})};const s=await a.json(),o=i?.();if(o&&Array.isArray(s?.data))for(const t of s.data)t.isSelf=t.instance_id===o;return{content:JSON.stringify(s,null,2)}}catch(t){if(n?.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,i,o){let{agentId:c,message:l,files:d,contextId:u,timeoutMs:g,taskId:f}=i;const p=n?.(t),h=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;if(!h.test(c)){const a=r?.(t);if(a?.length){const t=resolveAgentIdFromMentions(a,c);t&&(e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${t} via mention context`),c=t)}if(!h.test(c))try{const t=await a2aFetch(e,`/api/v1/a2a/discover?query=${encodeURIComponent(c)}&limit=10`,{signal:o});if(t.ok){const a=await t.json(),n=a?.data??[],r=c.toLowerCase(),s=n.find(t=>t.name.toLowerCase()===r)??n.find(t=>t.name.toLowerCase().includes(r));if(!s)return{content:JSON.stringify({error:`Could not resolve agent name "${c}". Available agents: ${n.map(t=>`${t.name} (${t.id})`).join(", ")||"none"}`})};e.logger.info?.(`beeos_call_agent: resolved "${c}" → ${s.id} (${s.name}) via discover`),c=s.id}}catch{}}const m=u||(p?`session:${p}`:""),k=g||6e5;try{const t={jsonrpc:"2.0",id:`claw-${Date.now()}`,method:"SendMessage",params:{message:{role:"user",parts:[{text:l}],...m?{contextId:m}:{},...f?{taskId:f}:{}},configuration:{timeoutMs:k}}},n=`/api/v1/a2a/${encodeURIComponent(c)}`,r=AbortSignal.timeout(k+1e4),i=o?AbortSignal.any([o,r]):r,d=await a2aFetch(e,n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t),signal:i}),g=await d.text();let h;try{h=JSON.parse(g)}catch{h={}}if(!d.ok||h.error){if(401===d.status||403===d.status){const t=h.error??{};-32600===t.code&&s&&s("_beeos/auth_required",{type:"auth_required",target_agent_id:c,target_agent_name:c,title:"Authentication Required",content:`Agent **${c}** requires authentication.`})}const t=h.error?JSON.stringify(h.error):g;return{content:JSON.stringify({error:`send task failed: ${d.status} ${t}`})}}const y=unwrapTask(h.result),_=y?.id,w=taskState(y);if(_&&a.create(_,c,u,p),"completed"===w){const t=extractArtifactText(y);if(t)return{content:t}}if(TERMINAL_FAILURE_STATES.has(w))return{content:terminalFailureContent(y,w,_)};if(_){const t=2e3,a=Date.now()+k;for(;Date.now()<a;){if(await new Promise(e=>setTimeout(e,t)),o?.aborted)throw new DOMException("Aborted","AbortError");const a=await a2aFetch(e,n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:`claw-poll-${Date.now()}`,method:"GetTask",params:{id:_}}),signal:o});if(!a.ok)break;let r;try{r=JSON.parse(await a.text())}catch{break}if(r.error)break;const s=unwrapTask(r.result),i=taskState(s);if("completed"===i){return{content:extractArtifactText(s)??JSON.stringify(s,null,2)}}if(TERMINAL_FAILURE_STATES.has(i))return{content:terminalFailureContent(s,i,_)}}return{content:JSON.stringify({status:"working",task_id:_,message:`Task is still running. Call beeos_get_task with task_id "${_}" to fetch the result later.`})}}return{content:JSON.stringify(y??h.result,null,2)}}catch(t){if(o?.aborted)throw t;return t instanceof DOMException&&"TimeoutError"===t.name?{content:JSON.stringify({error:`task timed out after ${k}ms`,agent:c})}:{content:JSON.stringify({error:t instanceof Error?t.message:String(t)})}}}}),t.registerTool({name:"beeos_get_task",description:'Fetch the current state and result of an existing A2A task by its task_id. Use this to check on a task that beeos_call_agent left running (it returned status "working" with a task_id), or to re-read the result of a task that already finished. Returns the agent\'s reply text when completed; the produced output plus the terminal state when the task ended as failed/canceled/timeout but still produced content; or a working status when it is still in progress. Do NOT call beeos_call_agent again to re-query an existing task — that creates a brand-new task.',parameters:{type:"object",properties:{taskId:{type:"string",description:"The task ID returned by a previous beeos_call_agent call."},agentId:{type:"string",description:"The target agent UUID that owns the task (the same agentId passed to beeos_call_agent). Optional when the task was created earlier in this session — it is resolved automatically."}},required:["taskId"]},async execute(t,n,r){const{taskId:s,agentId:i}=n??{};if(!s)return{content:JSON.stringify({error:"taskId is required"})};const o=i||a.resolveAgentId(s);if(!o)return{content:JSON.stringify({error:`agentId is required to query task "${s}" (it was not created in this session, so the target agent is unknown). Pass the agentId you originally called.`,task_id:s})};try{const t=`/api/v1/a2a/${encodeURIComponent(o)}`,a=await a2aFetch(e,t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({jsonrpc:"2.0",id:`claw-get-${Date.now()}`,method:"GetTask",params:{id:s}}),signal:r}),n=await a.text();let i;try{i=JSON.parse(n)}catch{i={}}if(!a.ok||i.error){const t=i.error?JSON.stringify(i.error):n;return{content:JSON.stringify({error:`get task failed: ${a.status} ${t}`,task_id:s})}}const c=unwrapTask(i.result),l=taskState(c);return"completed"===l?{content:extractArtifactText(c)??JSON.stringify(c,null,2)}:TERMINAL_FAILURE_STATES.has(l)?{content:terminalFailureContent(c,l,s)}:{content:JSON.stringify({status:l||"working",task_id:s,message:`Task is still ${l||"running"}.`})}}catch(t){if(r?.aborted)throw t;return{content:JSON.stringify({error:t instanceof Error?t.message:String(t)})}}}})}
@@ -1 +1 @@
1
- import{describe,it,expect,vi,beforeEach,afterEach}from"vitest";import{registerA2ATools,TaskHandleRegistry,unwrapTask,extractArtifactText,extractStatusText,taskState}from"./tools.js";const AGENT_ID="d04080d1-1111-2222-3333-444455556666";function makeCfg(){return{agentGatewayUrl:"https://gw.test",agentKeys:null,logger:{info:vi.fn(),warn:vi.fn(),error:vi.fn(),debug:vi.fn()}}}function registerTools(t){const e=new Map;return registerA2ATools({registerTool:t=>e.set(t.name,t)},t,new TaskHandleRegistry),e}function jsonResponse(t,e=!0,s=200){const a=JSON.stringify(t);return{ok:e,status:s,text:async()=>a,json:async()=>JSON.parse(a)}}describe("unwrapTask",()=>{it("unwraps the v1.0 SendMessage { task } envelope",()=>{const t=unwrapTask({task:{id:"t1",status:{state:"TASK_STATE_WORKING"}}});expect(t?.id).toBe("t1")}),it("returns the bare task for v0.3 / GetTask results",()=>{const t=unwrapTask({id:"t2",status:{state:"working"}});expect(t?.id).toBe("t2")}),it("returns undefined for non-object input",()=>{expect(unwrapTask(void 0)).toBeUndefined(),expect(unwrapTask("nope")).toBeUndefined()})}),describe("extractArtifactText",()=>{it("reads v1.0 text parts ({ text } without kind)",()=>{expect(extractArtifactText({artifacts:[{parts:[{text:"hello"},{text:"world"}]}]})).toBe("hello\nworld")}),it("reads v0.3 text parts ({ kind: 'text', text })",()=>{expect(extractArtifactText({artifacts:[{parts:[{kind:"text",text:"hi"}]}]})).toBe("hi")}),it("skips non-text parts (e.g. file/data)",()=>{expect(extractArtifactText({artifacts:[{parts:[{kind:"file",url:"u"},{text:"kept"}]}]})).toBe("kept")}),it("returns undefined when there are no artifacts",()=>{expect(extractArtifactText({status:{state:"working"}})).toBeUndefined(),expect(extractArtifactText(void 0)).toBeUndefined()})}),describe("extractStatusText",()=>{it("reads the agent status message text",()=>{expect(extractStatusText({status:{message:{parts:[{text:"boom"}]}}})).toBe("boom")}),it("returns undefined when no status message",()=>{expect(extractStatusText({status:{}})).toBeUndefined()})}),describe("taskState",()=>{it("normalizes v1.0 SCREAMING_SNAKE states",()=>{expect(taskState({status:{state:"TASK_STATE_COMPLETED"}})).toBe("completed"),expect(taskState({status:{state:"TASK_STATE_WORKING"}})).toBe("working")}),it("passes through v0.3 lowercase states",()=>{expect(taskState({status:{state:"failed"}})).toBe("failed")})}),describe("beeos_call_agent async polling",()=>{beforeEach(()=>{vi.useFakeTimers()}),afterEach(()=>{vi.useRealTimers(),vi.unstubAllGlobals()}),it("unwraps result.task, polls tasks/get, and returns the completed artifact text",async()=>{const t=[];let e=0;const s=vi.fn(async(s,a)=>{const r=a?.body?JSON.parse(a.body):{};if(t.push({url:s,method:a?.method,rpcMethod:r.method}),"SendMessage"===r.method)return jsonResponse({jsonrpc:"2.0",id:r.id,result:{task:{id:"task-async-1",contextId:"c1",status:{state:"TASK_STATE_WORKING"}}}});if("GetTask"===r.method)return e+=1,jsonResponse(e<2?{jsonrpc:"2.0",id:r.id,result:{id:"task-async-1",status:{state:"TASK_STATE_WORKING"}}}:{jsonrpc:"2.0",id:r.id,result:{id:"task-async-1",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{artifactId:"task-async-1-a0",parts:[{text:"final answer"}]}]}});throw new Error(`unexpected request method: ${r.method}`)});vi.stubGlobal("fetch",s);const a=registerTools(makeCfg()).get("beeos_call_agent").execute("tc-1",{agentId:AGENT_ID,message:"do it"});await vi.advanceTimersByTimeAsync(2e3),await vi.advanceTimersByTimeAsync(2e3);const r=await a;expect(r.content).toBe("final answer");const n=t.filter(t=>"GetTask"===t.rpcMethod);expect(n.length).toBeGreaterThanOrEqual(1);for(const t of n)expect(t.method).toBe("POST"),expect(t.url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`),expect(t.url).not.toContain("/tasks/")}),it("surfaces a failed task's error text from the poll",async()=>{const t=vi.fn(async(t,e)=>{const s=e?.body?JSON.parse(e.body):{};return"SendMessage"===s.method?jsonResponse({jsonrpc:"2.0",id:s.id,result:{task:{id:"task-fail",status:{state:"TASK_STATE_WORKING"}}}}):jsonResponse({jsonrpc:"2.0",id:s.id,result:{id:"task-fail",status:{state:"TASK_STATE_FAILED",message:{parts:[{text:"nope"}]}}}})});vi.stubGlobal("fetch",t);const e=registerTools(makeCfg()).get("beeos_call_agent").execute("tc-2",{agentId:AGENT_ID,message:"do it"});await vi.advanceTimersByTimeAsync(2e3);const s=await e,a=JSON.parse(s.content);expect(a.error).toBe("nope"),expect(a.task_id).toBe("task-fail")})});
1
+ import{describe,it,expect,vi,beforeEach,afterEach}from"vitest";import{registerA2ATools,TaskHandleRegistry,unwrapTask,extractArtifactText,extractStatusText,taskState,terminalFailureContent}from"./tools.js";const AGENT_ID="d04080d1-1111-2222-3333-444455556666";function makeCfg(){return{agentGatewayUrl:"https://gw.test",agentKeys:null,logger:{info:vi.fn(),warn:vi.fn(),error:vi.fn(),debug:vi.fn()}}}function registerTools(t,e=new TaskHandleRegistry){const s=new Map;return registerA2ATools({registerTool:t=>s.set(t.name,t)},t,e),s}function jsonResponse(t,e=!0,s=200){const a=JSON.stringify(t);return{ok:e,status:s,text:async()=>a,json:async()=>JSON.parse(a)}}describe("unwrapTask",()=>{it("unwraps the v1.0 SendMessage { task } envelope",()=>{const t=unwrapTask({task:{id:"t1",status:{state:"TASK_STATE_WORKING"}}});expect(t?.id).toBe("t1")}),it("returns the bare task for v0.3 / GetTask results",()=>{const t=unwrapTask({id:"t2",status:{state:"working"}});expect(t?.id).toBe("t2")}),it("returns undefined for non-object input",()=>{expect(unwrapTask(void 0)).toBeUndefined(),expect(unwrapTask("nope")).toBeUndefined()})}),describe("extractArtifactText",()=>{it("reads v1.0 text parts ({ text } without kind)",()=>{expect(extractArtifactText({artifacts:[{parts:[{text:"hello"},{text:"world"}]}]})).toBe("hello\nworld")}),it("reads v0.3 text parts ({ kind: 'text', text })",()=>{expect(extractArtifactText({artifacts:[{parts:[{kind:"text",text:"hi"}]}]})).toBe("hi")}),it("skips non-text parts (e.g. file/data)",()=>{expect(extractArtifactText({artifacts:[{parts:[{kind:"file",url:"u"},{text:"kept"}]}]})).toBe("kept")}),it("returns undefined when there are no artifacts",()=>{expect(extractArtifactText({status:{state:"working"}})).toBeUndefined(),expect(extractArtifactText(void 0)).toBeUndefined()})}),describe("extractStatusText",()=>{it("reads the agent status message text",()=>{expect(extractStatusText({status:{message:{parts:[{text:"boom"}]}}})).toBe("boom")}),it("returns undefined when no status message",()=>{expect(extractStatusText({status:{}})).toBeUndefined()})}),describe("taskState",()=>{it("normalizes v1.0 SCREAMING_SNAKE states",()=>{expect(taskState({status:{state:"TASK_STATE_COMPLETED"}})).toBe("completed"),expect(taskState({status:{state:"TASK_STATE_WORKING"}})).toBe("working")}),it("passes through v0.3 lowercase states",()=>{expect(taskState({status:{state:"failed"}})).toBe("failed")})}),describe("terminalFailureContent",()=>{it("surfaces produced content with the terminal state (canceled with artifact)",()=>{const t=JSON.parse(terminalFailureContent({status:{state:"canceled"},artifacts:[{parts:[{text:"partial work"}]}]},"canceled","t1"));expect(t).toEqual({status:"canceled",task_id:"t1",result:"partial work"})}),it("falls back to status-message text when there is no artifact",()=>{const t=JSON.parse(terminalFailureContent({status:{state:"failed",message:{parts:[{text:"boom"}]}}},"failed","t2"));expect(t).toEqual({error:"boom",task_id:"t2"})}),it("falls back to the bare state when there is neither artifact nor message",()=>{const t=JSON.parse(terminalFailureContent({status:{state:"timeout"}},"timeout","t3"));expect(t).toEqual({error:"timeout",task_id:"t3"})})}),describe("beeos_call_agent async polling",()=>{beforeEach(()=>{vi.useFakeTimers()}),afterEach(()=>{vi.useRealTimers(),vi.unstubAllGlobals()}),it("unwraps result.task, polls tasks/get, and returns the completed artifact text",async()=>{const t=[];let e=0;const s=vi.fn(async(s,a)=>{const r=a?.body?JSON.parse(a.body):{};if(t.push({url:s,method:a?.method,rpcMethod:r.method}),"SendMessage"===r.method)return jsonResponse({jsonrpc:"2.0",id:r.id,result:{task:{id:"task-async-1",contextId:"c1",status:{state:"TASK_STATE_WORKING"}}}});if("GetTask"===r.method)return e+=1,jsonResponse(e<2?{jsonrpc:"2.0",id:r.id,result:{id:"task-async-1",status:{state:"TASK_STATE_WORKING"}}}:{jsonrpc:"2.0",id:r.id,result:{id:"task-async-1",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{artifactId:"task-async-1-a0",parts:[{text:"final answer"}]}]}});throw new Error(`unexpected request method: ${r.method}`)});vi.stubGlobal("fetch",s);const a=registerTools(makeCfg()).get("beeos_call_agent").execute("tc-1",{agentId:AGENT_ID,message:"do it"});await vi.advanceTimersByTimeAsync(2e3),await vi.advanceTimersByTimeAsync(2e3);const r=await a;expect(r.content).toBe("final answer");const o=t.filter(t=>"GetTask"===t.rpcMethod);expect(o.length).toBeGreaterThanOrEqual(1);for(const t of o)expect(t.method).toBe("POST"),expect(t.url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`),expect(t.url).not.toContain("/tasks/")}),it("surfaces a failed task's error text from the poll",async()=>{const t=vi.fn(async(t,e)=>{const s=e?.body?JSON.parse(e.body):{};return"SendMessage"===s.method?jsonResponse({jsonrpc:"2.0",id:s.id,result:{task:{id:"task-fail",status:{state:"TASK_STATE_WORKING"}}}}):jsonResponse({jsonrpc:"2.0",id:s.id,result:{id:"task-fail",status:{state:"TASK_STATE_FAILED",message:{parts:[{text:"nope"}]}}}})});vi.stubGlobal("fetch",t);const e=registerTools(makeCfg()).get("beeos_call_agent").execute("tc-2",{agentId:AGENT_ID,message:"do it"});await vi.advanceTimersByTimeAsync(2e3);const s=await e,a=JSON.parse(s.content);expect(a.error).toBe("nope"),expect(a.task_id).toBe("task-fail")}),it("still surfaces the artifact when the polled task ends canceled with content",async()=>{const t=vi.fn(async(t,e)=>{const s=e?.body?JSON.parse(e.body):{};return"SendMessage"===s.method?jsonResponse({jsonrpc:"2.0",id:s.id,result:{task:{id:"task-cancel",status:{state:"TASK_STATE_WORKING"}}}}):jsonResponse({jsonrpc:"2.0",id:s.id,result:{id:"task-cancel",status:{state:"TASK_STATE_CANCELED"},artifacts:[{parts:[{text:"the plan I produced"}]}]}})});vi.stubGlobal("fetch",t);const e=registerTools(makeCfg()).get("beeos_call_agent").execute("tc-3",{agentId:AGENT_ID,message:"do it"});await vi.advanceTimersByTimeAsync(2e3);const s=await e,a=JSON.parse(s.content);expect(a.status).toBe("canceled"),expect(a.task_id).toBe("task-cancel"),expect(a.result).toBe("the plan I produced")})}),describe("beeos_get_task",()=>{function t(t){const e=[],s=vi.fn(async(s,a)=>{const r=a?.body?JSON.parse(a.body):{};if(e.push({url:s,method:a?.method,rpcMethod:r.method,params:r.params}),"GetTask"!==r.method)throw new Error(`unexpected method: ${r.method}`);return jsonResponse({jsonrpc:"2.0",id:r.id,result:t})});return vi.stubGlobal("fetch",s),e}afterEach(()=>{vi.unstubAllGlobals()}),it("returns the artifact text of a completed task and queries the right endpoint",async()=>{const e=t({id:"task-1",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"the answer"}]}]}),s=registerTools(makeCfg()).get("beeos_get_task"),a=await s.execute("tc",{taskId:"task-1",agentId:AGENT_ID});expect(a.content).toBe("the answer"),expect(e).toHaveLength(1),expect(e[0].method).toBe("POST"),expect(e[0].url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`),expect(e[0].params.id).toBe("task-1")}),it("surfaces partial output AND the terminal state for a canceled task that still produced an artifact",async()=>{t({id:"task-cx",status:{state:"TASK_STATE_CANCELED"},artifacts:[{parts:[{text:"partial plan"}]}]});const e=registerTools(makeCfg()).get("beeos_get_task"),s=await e.execute("tc",{taskId:"task-cx",agentId:AGENT_ID}),a=JSON.parse(s.content);expect(a.status).toBe("canceled"),expect(a.task_id).toBe("task-cx"),expect(a.result).toBe("partial plan")}),it("returns an error for a terminal task with no produced content",async()=>{t({id:"task-to",status:{state:"TASK_STATE_FAILED",message:{parts:[{text:"boom"}]}}});const e=registerTools(makeCfg()).get("beeos_get_task"),s=await e.execute("tc",{taskId:"task-to",agentId:AGENT_ID}),a=JSON.parse(s.content);expect(a.error).toBe("boom"),expect(a.task_id).toBe("task-to")}),it("reports a still-running task as working",async()=>{t({id:"task-w",status:{state:"TASK_STATE_WORKING"}});const e=registerTools(makeCfg()).get("beeos_get_task"),s=await e.execute("tc",{taskId:"task-w",agentId:AGENT_ID}),a=JSON.parse(s.content);expect(a.status).toBe("working"),expect(a.task_id).toBe("task-w")}),it("resolves agentId from the session registry when omitted",async()=>{const e=new TaskHandleRegistry;e.create("task-reg",AGENT_ID);const s=t({id:"task-reg",status:{state:"TASK_STATE_COMPLETED"},artifacts:[{parts:[{text:"ok"}]}]}),a=registerTools(makeCfg(),e).get("beeos_get_task"),r=await a.execute("tc",{taskId:"task-reg"});expect(r.content).toBe("ok"),expect(s[0].url).toBe(`https://gw.test/api/v1/a2a/${AGENT_ID}`)}),it("errors when agentId is neither passed nor resolvable",async()=>{const t=vi.fn();vi.stubGlobal("fetch",t);const e=registerTools(makeCfg()).get("beeos_get_task"),s=await e.execute("tc",{taskId:"unknown-task"}),a=JSON.parse(s.content);expect(a.error).toContain("agentId is required"),expect(t).not.toHaveBeenCalled()})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.5.13",
3
+ "version": "0.5.14",
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>",