@agentdeploymentco/bishop 0.3.3 → 0.3.5

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.
Files changed (43) hide show
  1. package/README.md +12 -11
  2. package/dist/cli.js +15 -15
  3. package/dist/commands/run.js +3 -0
  4. package/dist/commands/syntax.js +3 -0
  5. package/dist/db.js +44 -11
  6. package/dist/gmail/create.js +1 -1
  7. package/dist/gmail/history.js +1 -1
  8. package/dist/gmail/interface.js +2 -2
  9. package/dist/harness/principal.js +12 -9
  10. package/dist/ids.js +1 -1
  11. package/dist/interface/authorize.js +1 -1
  12. package/dist/interface/history.js +1 -1
  13. package/dist/interface/verbosity.js +1 -1
  14. package/dist/principals.js +4 -0
  15. package/dist/schedule/cli.js +7 -7
  16. package/dist/schedule/prompt.js +2 -2
  17. package/dist/schedule/runner.js +3 -3
  18. package/dist/sessions/store.js +13 -7
  19. package/dist/slack/app.js +2 -2
  20. package/dist/slack/context.js +1 -0
  21. package/dist/slack/handlers.js +1 -1
  22. package/dist/slack/history.js +1 -1
  23. package/dist/slack/interface.js +2 -2
  24. package/dist/slack/members.js +1 -1
  25. package/dist/slack/principal.js +1 -0
  26. package/dist/slack/users.js +1 -1
  27. package/dist/teams/command.js +1 -0
  28. package/dist/teams/create.js +2 -2
  29. package/dist/teams/fake.js +1 -1
  30. package/dist/teams/graph.js +1 -1
  31. package/dist/teams/history.js +1 -0
  32. package/dist/teams/html.js +6 -0
  33. package/dist/teams/interface.js +3 -3
  34. package/dist/teams/presence.js +3 -3
  35. package/dist/teams/principal.js +1 -0
  36. package/dist/teams/thread.js +1 -1
  37. package/dist/tools/schedules.js +5 -5
  38. package/dist/tools/server.js +1 -1
  39. package/dist/turn.js +3 -1
  40. package/package.json +11 -6
  41. package/dist/slack/commands/run.js +0 -3
  42. package/dist/slack/commands/syntax.js +0 -3
  43. package/dist/slack/verbosity.js +0 -1
@@ -1,8 +1,8 @@
1
- import{z as a}from"zod";import{BishopError as y}from"../errors.js";import{NO_OUTPUT as T}from"../harness/principal.js";import{log as p}from"../log.js";import{describeTime as w,nextRun as R,systemTimeZone as g,validateCron as z,validateTimeZone as _}from"../schedule/cron.js";import{defineTool as l,ToolRefusal as d}from"./types.js";const A=20;function m(e){return`${e.origin.name}:${e.principal.id}`}function $(e){try{return e()}catch(n){throw n instanceof y?new d(n.message):n}}async function k(e){try{return await e()}catch(n){throw n instanceof y?new d(n.message):n}}function B(e,n){const t=[`${e.id} ${e.name}${e.enabled?"":" (disabled)"}`,` runs ${e.cron} (${e.tz})`,` next ${e.enabled?w(e.nextRunAt,n):"never, while it is off"}`,` to ${e.destination.label}`,` prompt ${e.prompt}`,` by ${e.createdBy}`];return e.lastError&&t.push(` error ${e.lastError} (${e.consecutiveFailures} in a row)`),t.join(`
2
- `)}async function b(e,n){const t=await n.destination(),i=m(n);return e.schedules.all().filter(o=>o.createdBy===i||e.sameConversation(o.destination,t))}function E(e){const n=e.now??Date.now;return l({name:"schedule_create",description:["Runs a prompt on a cron schedule and posts the answer where you say.","Each firing is a fresh session with no memory of the last one, so the prompt has to","carry everything it needs.","",`The prompt must say when to stay quiet, and say that answering ${T} is how.`,"A schedule that reports every time it runs is one people learn to ignore."].join(`
3
- `),input:{name:a.string().trim().min(1).describe("What a person sees in a list, and an email subject"),cron:a.string().trim().min(1).describe("Five-field cron, or @daily and friends. No more than once every five minutes"),prompt:a.string().trim().min(1).describe("What the agent is asked at each firing, including when to say nothing"),to:a.string().trim().min(1).optional().describe("Where the answer goes: #channel or @person for Slack, or an email address. Leave it out to post back into this conversation."),tz:a.string().trim().min(1).optional().describe("IANA zone the expression is read in, like Europe/London. Defaults to Bishop's")},async run(t,i){const o=m(i);if(!e.authorized(o))throw new d(`Bishop can't run a schedule created by ${o}, so making one would write something that never fires. Ask an operator to add it with \`bishop schedules add\`.`);const s=e.schedules.all().filter(v=>v.createdBy===o).length;if(s>=A)throw new d(`You already have ${s} schedules, which is the limit. Remove one with schedule_delete before adding another.`);const u=t.tz,c=u?$(()=>_(u)):g();$(()=>z(t.cron,c));const f=t.to,h=f?await k(()=>e.resolve(f)):await i.destination(),r=e.schedules.add({name:t.name,cron:t.cron,tz:c,prompt:t.prompt,destination:h,createdBy:o,nextRunAt:R(t.cron,c,n())});return p.info({schedule:r.id,name:r.name,cron:r.cron,tz:r.tz,by:r.createdBy,to:h.label},"the agent created a schedule"),`Created ${r.id} "${r.name}", running ${r.cron} (${c}).
4
- Next ${w(r.nextRunAt,c)}, posting to ${h.label}.`}})}function N(e){return l({name:"schedule_list",description:"Lists the schedules you created, and any that post into this conversation. Not schedules other people aimed elsewhere.",input:{},async run(n,t){const i=await b(e,t);if(i.length===0)return"Nothing is scheduled here, and you have not made any.";const o=g();return`${i.map(s=>B(s,o)).join(`
1
+ import{z as s}from"zod";import{BishopError as f}from"../errors.js";import{NO_OUTPUT as T}from"../harness/principal.js";import{log as y}from"../log.js";import{principalLabel as R}from"../principals.js";import{describeTime as w,nextRun as z,systemTimeZone as g,validateCron as _,validateTimeZone as A}from"../schedule/cron.js";import{defineTool as l,ToolRefusal as d}from"./types.js";const k=20;function m(e){return e.principal.id}function b(e){try{return e()}catch(n){throw n instanceof f?new d(n.message):n}}async function B(e){try{return await e()}catch(n){throw n instanceof f?new d(n.message):n}}function E(e,n,t){const i=[`${e.id} ${e.name}${e.enabled?"":" (disabled)"}`,` runs ${e.cron} (${e.tz})`,` next ${e.enabled?w(e.nextRunAt,n):"never, while it is off"}`,` to ${e.destination.label}`,` prompt ${e.prompt}`,` by ${R(e.createdBy,t)}`];return e.lastError&&i.push(` error ${e.lastError} (${e.consecutiveFailures} in a row)`),i.join(`
2
+ `)}async function $(e,n){const t=await n.destination(),i=m(n);return e.schedules.all().filter(o=>o.createdBy===i||e.sameConversation(o.destination,t))}function N(e){const n=e.now??Date.now;return l({name:"schedule_create",description:["Runs a prompt on a cron schedule and posts the answer where you say.","Each firing is a fresh session with no memory of the last one, so the prompt has to","carry everything it needs.","",`The prompt must say when to stay quiet, and say that answering ${T} is how.`,"A schedule that reports every time it runs is one people learn to ignore."].join(`
3
+ `),input:{name:s.string().trim().min(1).describe("What a person sees in a list, and an email subject"),cron:s.string().trim().min(1).describe("Five-field cron, or @daily and friends. No more than once every five minutes"),prompt:s.string().trim().min(1).describe("What the agent is asked at each firing, including when to say nothing"),to:s.string().trim().min(1).optional().describe("Where the answer goes: #channel or @person for Slack, or an email address. Leave it out to post back into this conversation."),tz:s.string().trim().min(1).optional().describe("IANA zone the expression is read in, like Europe/London. Defaults to Bishop's")},async run(t,i){const o=m(i);if(!e.authorized(o))throw new d(`Bishop can't run a schedule created by ${i.principal.name}, so making one would write something that never fires. Ask an operator to add it with \`bishop schedules add\`.`);const a=e.schedules.all().filter(v=>v.createdBy===o).length;if(a>=k)throw new d(`You already have ${a} schedules, which is the limit. Remove one with schedule_delete before adding another.`);const u=t.tz,c=u?b(()=>A(u)):g();b(()=>_(t.cron,c));const p=t.to,h=p?await B(()=>e.resolve(p)):await i.destination(),r=e.schedules.add({name:t.name,cron:t.cron,tz:c,prompt:t.prompt,destination:h,createdBy:o,nextRunAt:z(t.cron,c,n())});return y.info({schedule:r.id,name:r.name,cron:r.cron,tz:r.tz,by:r.createdBy,to:h.label},"the agent created a schedule"),`Created ${r.id} "${r.name}", running ${r.cron} (${c}).
4
+ Next ${w(r.nextRunAt,c)}, posting to ${h.label}.`}})}function x(e){return l({name:"schedule_list",description:"Lists the schedules you created, and any that post into this conversation. Not schedules other people aimed elsewhere.",input:{},async run(n,t){const i=await $(e,t);if(i.length===0)return"Nothing is scheduled here, and you have not made any.";const o=g();return`${i.map(a=>E(a,o,e.principals)).join(`
5
5
 
6
6
  `)}
7
7
 
8
- Times in ${o}.`}})}function x(e){return l({name:"schedule_delete",description:"Removes a schedule by id. Only ones schedule_list shows you.",input:{id:a.string().trim().min(1).describe("The id schedule_list reports")},async run(n,t){const o=(await b(e,t)).find(s=>s.id===n.id);if(!o)throw new d(`No schedule ${n.id} that you can remove. schedule_list has the ones you can.`);return e.schedules.remove(o.id),p.info({schedule:o.id,name:o.name,by:m(t)},"the agent removed a schedule"),`Removed ${o.id} "${o.name}".`}})}function F(e){return[E(e),N(e),x(e)]}export{A as MAX_PER_CREATOR,m as createdBy,F as scheduleTools};
8
+ Times in ${o}.`}})}function j(e){return l({name:"schedule_delete",description:"Removes a schedule by id. Only ones schedule_list shows you.",input:{id:s.string().trim().min(1).describe("The id schedule_list reports")},async run(n,t){const o=(await $(e,t)).find(a=>a.id===n.id);if(!o)throw new d(`No schedule ${n.id} that you can remove. schedule_list has the ones you can.`);return e.schedules.remove(o.id),y.info({schedule:o.id,name:o.name,by:m(t)},"the agent removed a schedule"),`Removed ${o.id} "${o.name}".`}})}function U(e){return[N(e),x(e),j(e)]}export{k as MAX_PER_CREATOR,m as createdBy,U as scheduleTools};
@@ -1 +1 @@
1
- import{randomBytes as T,randomUUID as b}from"node:crypto";import{createServer as k}from"node:http";import{McpServer as B}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as O}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as _}from"@modelcontextprotocol/sdk/types.js";import{log as h}from"../log.js";import{ToolRefusal as y}from"./types.js";const x="bishop",v="/mcp",R="BISHOP_TOOL_TOKEN",A=1048576;function N(i){const r=i.headers.authorization;return r&&/^Bearer\s+(.+)$/i.exec(r.trim())?.[1]?.trim()||void 0}function p(i,r,a){i.writeHead(r,{"content-type":"application/json"}),i.end(JSON.stringify({jsonrpc:"2.0",id:null,error:{code:-32001,message:a}}))}async function I(i){const r=[];let a=0;for await(const n of i){if(a+=n.length,a>A)throw new Error("request body is too large");r.push(n)}if(r.length!==0)return JSON.parse(Buffer.concat(r).toString("utf8"))}const P="Bishop's tools answer to a person, and this message came from another bot. Ask someone in the conversation to do it.";function z(i,r){const a=new B({name:x,version:"1"});for(const n of i)a.registerTool(n.name,{description:n.description,inputSchema:n.input},async w=>{try{if(r.principal.isBot)throw new y(P);return{content:[{type:"text",text:await n.run(w,r)}]}}catch(c){const f=c instanceof Error?c.message:String(c);return c instanceof y?h.info({tool:n.name,thread:r.thread.threadKey,reason:f},"a tool refused"):h.error({tool:n.name,thread:r.thread.threadKey,err:c},"a tool failed"),{content:[{type:"text",text:f}],isError:!0}}});return a}async function D(i){const r=i.host??"127.0.0.1",a=new Map,n=new Map;async function w(e,t){const o=new O({sessionIdGenerator:()=>b(),onsessioninitialized:s=>{if(a.get(e)!==t){o.close().catch(()=>{});return}n.set(s,{token:e,transport:o}),t.sessions.add(s)}});return o.onclose=()=>{const s=o.sessionId;s&&(n.delete(s),t.sessions.delete(s))},await z(t.tools,t.context).connect(o),o}const c=k((e,t)=>{(async()=>{try{if(!e.url?.startsWith(v))return p(t,404,"no such endpoint");const o=N(e),s=o?a.get(o):void 0;if(!s||!o)return p(t,401,"this call carries no valid turn credential");const l=e.method==="POST"?await I(e):void 0,d=e.headers["mcp-session-id"],m=typeof d=="string"?n.get(d):void 0;if(m){if(m.token!==o)return p(t,404,"no such session");await m.transport.handleRequest(e,t,l);return}if(typeof d=="string")return p(t,404,"no such session");if(!_(l))return p(t,400,"the first call on a connection must be initialize");await(await w(o,s)).handleRequest(e,t,l)}catch(o){h.error({err:o},"a tool call could not be handled"),t.headersSent?t.end():p(t,400,"that request could not be handled")}})()});await new Promise((e,t)=>{c.once("error",t),c.listen(0,r,()=>{c.removeListener("error",t),e()})});const f=c.address();if(!f||typeof f=="string")throw new Error("the tool endpoint started without a port");const g=`http://${r}:${f.port}${v}`;return h.info({url:g,tools:i.tools.map(e=>e.name)},"serving the agent its own tools"),{url:g,toolNames:i.tools.map(e=>e.name),grant(e,t){const o=T(32).toString("base64url"),s=t?.only,l=s?i.tools.filter(u=>s.includes(u.name)):i.tools;s&&l.length!==s.length&&h.error({asked:[...s],serving:l.map(u=>u.name)},"a turn asked for tools this endpoint does not serve");const d={context:e,tools:l,sessions:new Set};return a.set(o,d),{access:{url:g,tokenEnvVar:R,token:o},revoke:()=>{if(a.delete(o)){for(const u of d.sessions){const S=n.get(u);n.delete(u),S?.transport.close().catch(E=>{h.debug({err:E},"a tool connection did not close cleanly")})}d.sessions.clear()}}}},async stop(){for(const{transport:e}of n.values())await e.close().catch(()=>{});n.clear(),a.clear(),c.closeAllConnections(),await new Promise(e=>c.close(()=>e()))}}}export{v as PATH,x as SERVER_NAME,R as TOKEN_ENV_VAR,D as startToolServer};
1
+ import{randomBytes as T,randomUUID as b}from"node:crypto";import{createServer as k}from"node:http";import{McpServer as O}from"@modelcontextprotocol/sdk/server/mcp.js";import{StreamableHTTPServerTransport as _}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{isInitializeRequest as x}from"@modelcontextprotocol/sdk/types.js";import{log as h}from"../log.js";import{ToolRefusal as y}from"./types.js";const B="bishop",v="/mcp",R="BISHOP_TOOL_TOKEN",A=1048576;function N(i){const r=i.headers.authorization;return r&&/^Bearer\s+(.+)$/i.exec(r.trim())?.[1]?.trim()||void 0}function p(i,r,a){i.writeHead(r,{"content-type":"application/json"}),i.end(JSON.stringify({jsonrpc:"2.0",id:null,error:{code:-32001,message:a}}))}async function I(i){const r=[];let a=0;for await(const n of i){if(a+=n.length,a>A)throw new Error("request body is too large");r.push(n)}if(r.length!==0)return JSON.parse(Buffer.concat(r).toString("utf8"))}const P="Bishop's tools answer to a person, and this message came from another bot. Ask someone in the conversation to do it.";function z(i,r){const a=new O({name:B,version:"1"});for(const n of i)a.registerTool(n.name,{description:n.description,inputSchema:n.input},async w=>{try{if(r.principal.kind==="bot")throw new y(P);return{content:[{type:"text",text:await n.run(w,r)}]}}catch(c){const f=c instanceof Error?c.message:String(c);return c instanceof y?h.info({tool:n.name,thread:r.thread.threadKey,reason:f},"a tool refused"):h.error({tool:n.name,thread:r.thread.threadKey,err:c},"a tool failed"),{content:[{type:"text",text:f}],isError:!0}}});return a}async function D(i){const r=i.host??"127.0.0.1",a=new Map,n=new Map;async function w(e,t){const o=new _({sessionIdGenerator:()=>b(),onsessioninitialized:s=>{if(a.get(e)!==t){o.close().catch(()=>{});return}n.set(s,{token:e,transport:o}),t.sessions.add(s)}});return o.onclose=()=>{const s=o.sessionId;s&&(n.delete(s),t.sessions.delete(s))},await z(t.tools,t.context).connect(o),o}const c=k((e,t)=>{(async()=>{try{if(!e.url?.startsWith(v))return p(t,404,"no such endpoint");const o=N(e),s=o?a.get(o):void 0;if(!s||!o)return p(t,401,"this call carries no valid turn credential");const d=e.method==="POST"?await I(e):void 0,l=e.headers["mcp-session-id"],m=typeof l=="string"?n.get(l):void 0;if(m){if(m.token!==o)return p(t,404,"no such session");await m.transport.handleRequest(e,t,d);return}if(typeof l=="string")return p(t,404,"no such session");if(!x(d))return p(t,400,"the first call on a connection must be initialize");await(await w(o,s)).handleRequest(e,t,d)}catch(o){h.error({err:o},"a tool call could not be handled"),t.headersSent?t.end():p(t,400,"that request could not be handled")}})()});await new Promise((e,t)=>{c.once("error",t),c.listen(0,r,()=>{c.removeListener("error",t),e()})});const f=c.address();if(!f||typeof f=="string")throw new Error("the tool endpoint started without a port");const g=`http://${r}:${f.port}${v}`;return h.info({url:g,tools:i.tools.map(e=>e.name)},"serving the agent its own tools"),{url:g,toolNames:i.tools.map(e=>e.name),grant(e,t){const o=T(32).toString("base64url"),s=t?.only,d=s?i.tools.filter(u=>s.includes(u.name)):i.tools;s&&d.length!==s.length&&h.error({asked:[...s],serving:d.map(u=>u.name)},"a turn asked for tools this endpoint does not serve");const l={context:e,tools:d,sessions:new Set};return a.set(o,l),{access:{url:g,tokenEnvVar:R,token:o},revoke:()=>{if(a.delete(o)){for(const u of l.sessions){const S=n.get(u);n.delete(u),S?.transport.close().catch(E=>{h.debug({err:E},"a tool connection did not close cleanly")})}l.sessions.clear()}}}},async stop(){for(const{transport:e}of n.values())await e.close().catch(()=>{});n.clear(),a.clear(),c.closeAllConnections(),await new Promise(e=>c.close(()=>e()))}}}export{v as PATH,B as SERVER_NAME,R as TOKEN_ENV_VAR,D as startToolServer};
package/dist/turn.js CHANGED
@@ -1 +1,3 @@
1
- import{log as d}from"./log.js";import{worktreeName as u}from"./worktrees.js";function A(n){const h=new Map,y=new Set;async function w(e){const a=n.sharedDirNotice;if(!(!a||y.has(e.thread.threadKey))){y.add(e.thread.threadKey);try{await e.notify(a)}catch(o){d.debug({thread:e.thread.threadKey,err:o},"could not deliver the shared-directory notice")}}}function K(e,a,o){if(!n.tools||e.tools===!1)return;const i=Array.isArray(e.tools)?e.tools:void 0;if(i&&i.length===0)return;const r=o.share?.bind(o);return n.tools.grant({thread:e.thread,principal:a,origin:e.origin,destination:()=>e.destination(),...r?{share:r}:{}},...i?[{only:i}]:[])}async function p(e){await w(e);const a=new AbortController,o=e.presence();let i;try{h.set(e.thread.threadKey,a);const r=e.freshSession?void 0:await n.sessions.get(e.thread.id),c=await n.cwd(e.thread),[b,g]=await Promise.all([e.messages(),r?void 0:e.history?.().catch(t=>{d.warn({thread:e.thread.threadKey,err:t},"could not read the history")})]),[f,...m]=b;if(!f)throw new Error("a turn with no messages");const k=m.at(-1)?.principal??f.principal;if(a.signal.aborted){d.info({thread:e.thread.threadKey},"turn stopped before the agent started");return}i=K(e,k,o);const l=n.model?.of(e.thread.id)??{};let s;for await(const t of n.harness.runTurn({messages:[f,...m],cwd:c,threadKey:e.thread.threadKey,worktree:u(e.thread.dirKey),...r?{sessionId:r}:{},...l.model?{model:l.model}:{},...l.effort?{effort:l.effort}:{},...e.note?{note:e.note}:{},...g?.length?{history:g}:{},origin:e.origin,...i?{tools:i.access}:{},signal:a.signal}))t.type==="session"&&(t.model&&(s=t.model),t.sessionId!==r&&await n.sessions.set(e.thread.id,t.sessionId)),t.type==="text"&&d.debug({thread:e.thread.threadKey,chars:t.text.length},"agent said something"),t.type==="tool"&&d.debug({thread:e.thread.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&d.info({thread:e.thread.threadKey,...s?{model:s}:{},...t.usage},"turn complete"),t.type==="error"&&d.error({thread:e.thread.threadKey,...s?{model:s}:{},kind:t.kind,err:t.message},"turn errored"),await o.handle(t)}catch(r){const c=r instanceof Error?r.message:String(r);throw d.error({thread:e.thread.threadKey,err:r},"turn failed before the agent answered"),await o.handle({type:"error",kind:"turn_failed",message:c}),r}finally{i?.revoke(),h.get(e.thread.threadKey)===a&&h.delete(e.thread.threadKey),await o.finish()}}return{run:p,stop(e){const a=h.get(e);return a?(a.abort(),!0):!1}}}export{A as makeTurnRunner};
1
+ import{contextChangeNote as I}from"./harness/principal.js";import{log as n}from"./log.js";import{worktreeName as P}from"./worktrees.js";function B(r){const{location:i,visibility:g,audience:m}=r;return{...i?{location:{kind:i.kind,...i.name?{name:i.name}:{}}}:{},visibility:g,audience:m}}const v=Symbol("not yet");function J(r){if(r)try{return JSON.parse(r)}catch{return}}function F(r){const i=new Map,g=new Set;async function m(e){const a=r.sharedDirNotice;if(!(!a||g.has(e.thread.threadKey))){g.add(e.thread.threadKey);try{await e.notify(a)}catch(d){n.debug({thread:e.thread.threadKey,err:d},"could not deliver the shared-directory notice")}}}function S(e,a,d){if(!r.tools||e.tools===!1)return;const s=Array.isArray(e.tools)?e.tools:void 0;if(s&&s.length===0)return;const o=d.share?.bind(d);return r.tools.grant({thread:e.thread,principal:a,origin:e.origin,destination:()=>e.destination(),...o?{share:o}:{}},...s?[{only:s}]:[])}async function O(e){await m(e);const a=new AbortController,d=e.presence();let s;try{i.set(e.thread.threadKey,a);const o=e.observe?.().catch(t=>(n.warn({thread:e.thread.threadKey,err:t},"could not read where this conversation takes place"),{visibility:"unknown",audience:"unknown"})),c=e.freshSession?void 0:await r.sessions.get(e.thread.id),T=await r.cwd(e.thread),[A,k]=await Promise.all([e.messages(),c?void 0:e.history?.().catch(t=>{n.warn({thread:e.thread.threadKey,err:t},"could not read the history")})]),w=o&&await Promise.race([o,Promise.resolve(v)]),K=w===v?void 0:w;w===v&&o&&(n.debug({thread:e.thread.threadKey},"where this conversation takes place isn't read yet; not waiting for it"),o.then(t=>r.contexts?.record(e.thread.id,e.origin.name,t)).catch(t=>{n.warn({thread:e.thread.threadKey,err:t},"could not record where this conversation takes place")}));const[b,...p]=A;if(!b)throw new Error("a turn with no messages");const C=p.at(-1)?.principal??b.principal;if(a.signal.aborted){n.info({thread:e.thread.threadKey},"turn stopped before the agent started");return}s=S(e,C,d),r.contexts?.record(e.thread.id,e.origin.name,K);const u=r.contexts?.told(e.thread.id),h=K?B(K):o&&J(u),l=h&&JSON.stringify(h),x=!!(c&&l&&u!==l);x&&h&&n.info({thread:e.thread.threadKey,...h},"this conversation changed since the agent was told about it; telling it again");const N=[e.note,x&&h?I(h):void 0].filter(Boolean).join(`
2
+
3
+ `),E=h?{...e.origin,context:h}:e.origin,y=r.model?.of(e.thread.id)??{};let f;for await(const t of r.harness.runTurn({messages:[b,...p],cwd:T,threadKey:e.thread.threadKey,worktree:P(e.thread.dirKey),...c?{sessionId:c}:{},...y.model?{model:y.model}:{},...y.effort?{effort:y.effort}:{},...N?{note:N}:{},...k?.length?{history:k}:{},origin:E,...s?{tools:s.access}:{},signal:a.signal}))t.type==="session"&&(t.model&&(f=t.model),t.sessionId!==c&&await r.sessions.set(e.thread.id,t.sessionId),l&&r.contexts?.told(e.thread.id)!==l&&r.contexts?.tell(e.thread.id,l)),t.type==="text"&&n.debug({thread:e.thread.threadKey,chars:t.text.length},"agent said something"),t.type==="tool"&&n.debug({thread:e.thread.threadKey,tool:t.name,detail:t.summary??t.detail},"tool activity"),t.type==="done"&&n.info({thread:e.thread.threadKey,...f?{model:f}:{},...t.usage},"turn complete"),t.type==="error"&&n.error({thread:e.thread.threadKey,...f?{model:f}:{},kind:t.kind,err:t.message},"turn errored"),await d.handle(t)}catch(o){const c=o instanceof Error?o.message:String(o);throw n.error({thread:e.thread.threadKey,err:o},"turn failed before the agent answered"),await d.handle({type:"error",kind:"turn_failed",message:c}),o}finally{s?.revoke(),i.get(e.thread.threadKey)===a&&i.delete(e.thread.threadKey),await d.finish()}}return{run:O,stop(e){const a=i.get(e);return a?(a.abort(),!0):!1}}}export{F as makeTurnRunner};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentdeploymentco/bishop",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "An agent gateway. Connects an agent to Slack and email.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -22,11 +22,16 @@
22
22
  "build:release": "rm -rf dist && tsc -p tsconfig.release.json && node scripts/minify.mjs && chmod +x dist/cli.js",
23
23
  "prepublishOnly": "npm run build:release && node scripts/check-package.mjs",
24
24
  "dev": "tsx src/cli.ts",
25
- "dev:slack": "./scripts/dev.sh start",
26
- "dev:slack:stop": "./scripts/dev.sh stop",
27
- "dev:slack:restart": "./scripts/dev.sh restart",
28
- "dev:slack:status": "./scripts/dev.sh status",
29
- "dev:slack:logs": "./scripts/dev.sh logs",
25
+ "dev:slack": "./scripts/dev.sh slack start",
26
+ "dev:slack:stop": "./scripts/dev.sh slack stop",
27
+ "dev:slack:restart": "./scripts/dev.sh slack restart",
28
+ "dev:slack:status": "./scripts/dev.sh slack status",
29
+ "dev:slack:logs": "./scripts/dev.sh slack logs",
30
+ "dev:teams": "./scripts/dev.sh teams start",
31
+ "dev:teams:stop": "./scripts/dev.sh teams stop",
32
+ "dev:teams:restart": "./scripts/dev.sh teams restart",
33
+ "dev:teams:status": "./scripts/dev.sh teams status",
34
+ "dev:teams:logs": "./scripts/dev.sh teams logs",
30
35
  "typecheck": "tsc --noEmit",
31
36
  "test": "vitest run",
32
37
  "test:watch": "vitest",
@@ -1,3 +0,0 @@
1
- import{Command as L,CommanderError as N,InvalidArgumentError as g}from"commander";import{VERBOSITY as y}from"../../config.js";import{log as b}from"../../log.js";import{isHarnessName as S,MODEL_SETTINGS as O}from"../../thread-model.js";import{parseVerbosity as E}from"../verbosity.js";import{commandOutput as T,tokenize as j}from"./syntax.js";const a="default";function k(o){if(o.trim().toLowerCase()===a)return a;const e=E(o);if(!e)throw new g(`Use one of ${y.join(", ")}, or ${a}.`);return e}function l(o){const e=o.trim();if(!e)throw new g(`Name one, or ${a} to follow the configured one.`);if(!S(e))throw new g(`"${e}" is not a model name. Letters, digits, and . : / @ - _ only.`);return e}function I(o,e,t){return e?`${o} is ${e}, set in this thread`:t?`${o} is ${t}, the default`:`${o} is whatever the harness runs by default`}function C(o,e){const t=e.model.of(o);return[I("Model",t.model,e.configured.model),I("Effort",t.effort,e.configured.effort)]}function H(o,e,t){const u=[];let h;const f=new L;f.name("!").description("Bishop commands. These are handled here and never reach the agent.").exitOverride().configureOutput({writeOut:r=>u.push(r),writeErr:r=>u.push(r),outputError:(r,n)=>n(r)}),f.command("verbosity").description("How much of a turn this thread shows while it is running.").argument("[level]",`${y.join(", ")}, or ${a} to follow the configured level. Omit it to see the current one.`,k).action(r=>{if(!r){const c=t.verbosity.of(e.thread.id),d=c.fromThread?"":" (the configured default)";h=`Thread verbosity is ${c.level}${d}`;return}if(r===a){t.verbosity.clear(e.thread.id),h=`Thread verbosity follows the configured default, which is ${t.verbosity.of(e.thread.id).level}`;return}const n=E(r);n&&(t.verbosity.set(e.thread.id,n),h=`Thread verbosity set to ${n}`)}),f.command("model").description("Which model this thread runs, and how hard it thinks.").argument("[model]",`a model name in the harness's own naming, or ${a} to follow the configured one. Omit everything to see where the thread stands.`,l).argument("[effort]",`how hard it should think, in the harness's own naming, or ${a}.`,l).option("-m, --model <model>","the model, named as a flag rather than in order",l).option("-e, --effort <effort>","the effort, named as a flag rather than in order",l).action((r,n,c)=>{const d=e.thread.id,s={...c},w=O.filter(i=>s[i]===void 0),v=[r,n].filter(i=>i!==void 0);if(v.length>w.length){h="Name each half once: a model and an effort, in order or as flags.";return}if(v.forEach((i,m)=>{const p=w[m];p&&(s[p]=i)}),!s.model&&!s.effort){h=C(d,t).join(`
2
- `);return}const $={};for(const i of O){const m=s[i];m&&(m.toLowerCase()===a?t.model.clear(d,i):$[i]=m)}t.model.set(d,$),b.info({thread:e.thread.threadKey,...s},"a thread changed the model it runs"),h=["Set. It takes effect on this thread's next turn.",...C(d,t)].join(`
3
- `)});try{f.parse(j(o),{from:"user"})}catch(r){if(!(r instanceof N)){const n=r instanceof Error?r.message:String(r);return b.debug({thread:e.thread.threadKey,line:o,err:r},"a command failed"),T(n)}}return T(h??(u.join("").trim()||f.helpInformation()))}export{H as runCommand};
@@ -1,3 +0,0 @@
1
- const f="!>";function o(i){const n=i.trim();if(n.startsWith("```")&&n.endsWith("```")&&n.length>=6){const t=n.slice(3,-3);return t.includes("```")?void 0:t}if(n.startsWith("`")&&n.endsWith("`")&&n.length>=2){const t=n.slice(1,-1);return t.includes("`")?void 0:t}}const c=/^-{0,2}[a-z][a-z0-9-]*$/i;function d(i){const n=o(i)?.trim();if(!n)return;const t=/^!\s+(\S+)(?:\s+([\s\S]*))?$/.exec(n),e=t?.[1];if(!e||!c.test(e))return;const r=t?.[2]?.trim();return r?`${e} ${r}`:e}function l(i){const n=[];let t="",e=!1,r;for(let u=0;u<i.length;u+=1){const s=i[u];if(s==="\\"&&r!=="'"&&u+1<i.length){t+=i[++u],e=!0;continue}if(r){s===r?r=void 0:t+=s;continue}if(s==='"'||s==="'"){r=s,e=!0;continue}if(/\s/.test(s)){e&&n.push(t),t="",e=!1;continue}t+=s,e=!0}if(r)throw new Error(`unterminated ${r==='"'?"double":"single"} quote`);return e&&n.push(t),n}function a(i){const n=i.trim().split(`
2
- `),[t="",...e]=n;return["```",`!> ${t}`,...e,"```"].join(`
3
- `)}export{d as commandLine,a as commandOutput,l as tokenize};
@@ -1 +0,0 @@
1
- import{DEFAULT_VERBOSITY as l,VERBOSITY as f}from"../config.js";import{log as a}from"../log.js";const n="slack.verbosity";function u(t){const o=t.trim().toLowerCase();return f.includes(o)?o:void 0}function m(t,o=l){const s=new Set;return{of(e){const r=t.get(e,n);if(r===void 0)return{level:o,fromThread:!1};const i=u(r);return i?{level:i,fromThread:!0}:(s.has(`${e} ${r}`)||(s.add(`${e} ${r}`),a.warn({thread:e,stored:r},"unknown verbosity on this thread; using the default")),{level:o,fromThread:!1})},set(e,r){t.set(e,n,r)},clear(e){t.clear(e,n)}}}export{u as parseVerbosity,m as threadVerbosity};