@chatpanel/events 0.102.0 → 0.103.1
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/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -442
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/team-run.js
CHANGED
|
@@ -1,656 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// The runner never speaks to a model and never runs a tool. `callModel` is the host's own
|
|
4
|
-
// model turn (the extension's streamChat, the desktop's gateway call — redaction on the way
|
|
5
|
-
// out, restore on the way back, the tool loop with its round, shield and gate inside it);
|
|
6
|
-
// `toolsFor(role)` is the host's own toolset narrowed to the role's grants. A team therefore
|
|
7
|
-
// cannot reach what a single turn cannot, and every guard a client earned stays in force.
|
|
8
|
-
//
|
|
9
|
-
// Tasks run in WAVES (team-plan.js): everything with its dependencies met runs together
|
|
10
|
-
// through the same pool a tool round uses; a dependent task waits and reads the board. A
|
|
11
|
-
// member may REQUEST a piece of its task be done by someone else (board tool `request`,
|
|
12
|
-
// team-subtask.js): the sub-task joins the plan with its own thread under the parent's, is
|
|
13
|
-
// offered to the members that fit, then to the pool through the host's `recruit`, then
|
|
14
|
-
// proposed as a new agent to the person — so the plan is a tree the board draws as one. A
|
|
15
|
-
// task ends with findings; the merge turns the board into ONE proposal — agreed claims
|
|
16
|
-
// (converge, W7), a judged answer, or the members' work side by side — and the proposal is
|
|
17
|
-
// what a person sees. Nothing here lands anywhere.
|
|
18
|
-
//
|
|
19
|
-
// Budget first. `createBudget` refuses a team without one; before every model call the run
|
|
20
|
-
// asks `canAfford`, and a run that cannot afford its next call stops with what it has and
|
|
21
|
-
// says so (`status: 'over-budget'`). Stop is one signal, fanned out.
|
|
1
|
+
import{normalizeTeam as Ie}from"./team.js";import{normalizeEngine as De,normalizeScm as Pe}from"./scorecard.js";import{createBudget as ut}from"./budget.js";import{fixedPlan as qe,plannerPrompt as pt,parsePlan as ct}from"./team-plan.js";import{createBoard as ft,parseFindings as gt,boardText as ht,findingsInstruction as mt,toBriefClaims as kt,RUNNER as y,PERSON as ze}from"./team-board.js";import{subtaskFromRequest as yt,takeUp as bt,takeUpLine as wt,jobFromSubtask as It,extendDependents as $t,MAX_SUBTASKS as Le,MAX_DEPTH as Tt}from"./team-subtask.js";import{proposalToAgent as At,proposalFromNeeds as St}from"./recruit.js";import{grantsNeededFor as Nt}from"./tool-need.js";import{boardToolProvider as jt,createAnswerBox as Et,withBoardTool as Ot,DEFAULT_ASK_TIMEOUT_MS as vt}from"./board-tool.js";import{createRunCache as xt,withRunCache as Rt}from"./team-cache.js";import{messagesFor as Bt,mergeTranscript as Ft,clipTranscript as _t,clipMessage as Mt,newSteps as Ct,continuationNote as pe,isThought as Ut}from"./team-task.js";import{converge as Dt}from"./promotion.js";const on=Object.freeze(["planning","running","merging","waiting","completed","partial","over-budget","stopped","failed"]),Pt=3;class He extends Error{constructor(h,S){super(S),this.name="TeamRunError",this.code=h}}async function qt(u,h,S){const E=new Array(u.length);let A=0;const M=async()=>{for(;A<u.length;){const l=A++;E[l]=await S(u[l],l)}};return await Promise.all(Array.from({length:Math.min(h,u.length)||1},M)),E}const We={cheap:0,balanced:1,strong:2},Ke=u=>[...u.roles].sort((h,S)=>(We[S.prefer]??1)-(We[h.prefer]??1))[0];function Xe(u){const h=String(u?.message||u||"");return/^Not sent: /.test(h)?!0:/model[_ ]not[_ ]found|not found|not deployed|inaccessible|does not exist|no such model|unknown model|unsupported model|not available|unavailable|no api key|not configured|"status":\s*(404|401|403|500|502|503)\b|\b(404|401|403|502|503)\b|exited \d+|returned no answer|did not answer|closed the connection|couldn't reach|could not reach|ECONNREFUSED|ECONNRESET|ENOTFOUND|EHOSTUNREACH|socket hang up|fetch failed|failed to fetch|load failed|network ?error|overloaded|capacity/i.test(h)}function rn(u,h,{appoint:S=null}={}){const E=Ie(u),A=E.roles.map(l=>{const R=S?S(l):null;return{id:l.id,name:l.name,mode:l.mode,prefer:l.prefer,model:R?.model||l.model||null,label:R?.label||R?.model||l.model||null,appointed:!!(R?.model||l.model),grants:l.grants,...l.recipe?{recipe:l.recipe}:{}}}),M=A.filter(l=>l.mode!=="recipe"&&!l.appointed).map(l=>l.id);return{name:E.name,description:E.description,plan:E.plan,merge:E.merge,judge:E.judge,budget:E.budget,roles:A,tasks:E.plan==="fixed"?qe(E,h):null,ok:M.length===0,missing:M}}async function zt({team:u,request:h,callModel:S,toolsFor:E=()=>{},appoint:A=null,runRecipe:M=null,now:l=()=>Date.now(),newId:R=()=>`run_${Math.random().toString(36).slice(2,10)}`,emit:Je=()=>{},signal:Z=null,maxConcurrency:Ge=Pt,runId:Qe=null,answers:Te=null,askTimeoutMs:Ve=vt,resume:$=null,control:Ae=null,recruit:oe=null,projectId:Ze=null,prior:Se=null}={}){if(typeof S!="function")throw new He("BAD_RUN","callModel required");const c=Ie(u),C=Qe||$?.runId||R(),N=ut($?.budget?.cap||c.budget,{now:l});$?.budget?.spent&&N.charge({tokens:$.budget.spent.tokens,calls:$.budget.spent.calls,usd:$.budget.spent.usd});const p=(e,t={})=>Je(e,{runId:C,at:l(),...t}),r=ft({now:l,state:$?.board||null,onEvent:(e,t)=>p(e,t)}),ce=xt(),Ne=Te||Et(),ee=Te?Ve:0,je=$?.startedAt||l(),T=($?.tasks||[]).filter(e=>e.status==="ok").map(e=>({...e})),Ee=new Set(T.map(e=>e.id)),et=new Map(($?.tasks||[]).filter(e=>e.status!=="ok"&&Array.isArray(e.transcript)&&e.transcript.length).map(e=>[e.id,e])),Oe=e=>c.roles.find(t=>t.id===e),fe=(e,t=null)=>t?.size&&e.model&&t.has(e.model)?A?A({...e,model:void 0},{exclude:t}):null:(A?A(e,{exclude:t}):null)||(e.model&&!t?.has(e.model)?{model:e.model,mode:e.mode}:null),tt=3,nt=(e,t,{attempt:i=1,exclude:n=null,handoff:o=null}={})=>{const s=Array.isArray(e.reasons)?e.reasons.map(String):[];return o?s.unshift(`handed off by ${o.by}${o.reason?` — ${o.reason}`:""}`):!s.length&&t.model&&e.model===t.model&&s.push("pinned by the role"),i>1&&n?.size&&s.push(`after ${[...n].join(", ")} (unavailable)`),{engine:De(e.engine||{id:e.model}),reasons:s,alternatives:(Array.isArray(e.alternatives)?e.alternatives:[]).slice(0,5).map(d=>De(d)).filter(Boolean),exploration:!1}},ve=new Set,ot=e=>new Set([...ve,...e||[]]),U=()=>!!Z?.aborted;p($?"run.resumed":"run.started",{team:c.name,request:String(h||""),budget:N.cap,roles:c.roles.map(e=>e.id),...$?{carried:[...Ee]}:{}});let b=$?.plan?.tasks?.length?$.plan.tasks:null,re=$?.plan?.by||"fixed";if(!b&&c.plan==="planner"){const e=Ke(c),t=fe(e);if(t&&N.canAfford({tokens:0})){const i=await S({runId:C,taskId:"plan",role:e.id,model:t.model,mode:"model",system:"",prompt:pt(c,h),tools:void 0,signal:Z});i?.usage&&N.charge(i.usage);const n=i?.ok?ct(i.text,c):[];n.length&&(b=n,re="planner")}}b||(b=qe(c,h)),p("plan.ready",{by:re,tasks:b.map(e=>({id:e.id,role:e.role,title:e.title,dependsOn:e.dependsOn,...e.parent?{parent:e.parent,requestedBy:e.requestedBy||null}:{},...e.grants?{grants:e.grants,why:e.why||""}:{}}))});for(const e of b)r.openThread({taskId:e.id,kind:"task",title:e.title||e.id,by:e.requestedBy||y,...e.parent?{parent:e.parent}:{},...e.role&&e.parent?{holder:e.role}:{}});if(!$&&Array.isArray(Se))for(const e of Se.filter(t=>t&&Array.isArray(t.findings)&&t.findings.length)){const t=Number.isFinite(e.at)?Math.max(0,Math.round((l()-e.at)/6e4)):null,i=r.openThread({kind:"discussion",title:`Prior work — run ${e.runId}${t!=null?` (${t<60?`${t} min`:`${Math.round(t/60)} h`} ago)`:""}`,by:y});r.post({threadId:i.id,by:y,kind:"note",text:`An earlier run answered ${e.request?`"${String(e.request).slice(0,200)}"`:"the same request"}. Its findings follow — read them, verify what is stale, and do not repeat lookups already made.`,refs:[`run:${e.runId}`]});let n=0;for(const o of e.findings)!o||!o.text||o.status==="rejected"||n>=40||(r.post({threadId:i.id,by:o.role||y,kind:"finding",text:String(o.text).slice(0,2e3),refs:[...Array.isArray(o.refs)?o.refs:[],`run:${e.runId}`].slice(0,8),finding:{kind:o.kind||"claim",confidence:o.confidence??null,prior:!0}}),n+=1);p("run.prior",{from:e.runId,threadId:i.id,findings:n})}if(!$&&b.some(e=>e.grants?.length)){const e=r.openThread({kind:"proposal",title:"Tools per task",by:c.roles.find(i=>i.id===b[0]?.role)?.id||y}),t=b.filter(i=>i.grants?.length).map(i=>{const n=c.roles.find(s=>s.id===i.role)?.grants||[],o=i.grants.filter(s=>!n.includes(s)&&!(s.startsWith("mcp:")&&n.includes("mcp")));return`${i.role} (${i.id}): ${i.grants.join(", ")}${i.why?` — ${i.why}`:""}${o.length?` (not granted: ${o.join(", ")})`:""}`});r.post({threadId:e.id,by:y,kind:"draft",status:"proposed",text:t.join(`
|
|
2
|
+
`)})}const L=(e,t={})=>{const i=N.snapshot(),n={runId:C,team:c.name,status:e,plan:{by:re,tasks:b},tasks:T,board:r.all(),threads:r.state(),usage:i,lookups:{distinct:ce.size,shared:ce.shared},startedAt:je,endedAt:l(),...t};return e!=="completed"&&(n.checkpoint={runId:C,startedAt:je,plan:{by:re,tasks:b},tasks:T.map(o=>({...o,findings:void 0})),board:r.state(),budget:{cap:N.cap,spent:i.spent},budgetAsked:!!t.budgetAsked}),p("run.done",{status:e,usage:i,proposal:n.proposal||null,failedTaskIds:T.filter(o=>o.status==="failed").map(o=>o.id),waitingTaskIds:T.filter(o=>o.status==="waiting").map(o=>o.id),...n.checkpoint?{checkpoint:n.checkpoint}:{}}),n},xe=async({type:e,text:t,options:i,taskId:n=null})=>{if(ee<=0)return null;const{thread:o}=r.ask({taskId:n,by:y,type:e,text:t,options:i,timeoutMs:ee});return p("run.waiting",{threadId:o.id,askType:e,text:t,options:i}),Ne.wait(o.id,ee,Z)};let X=!1,D=!1,ie=!!$?.budgetAsked;const ge=new Set,Re=e=>Ee.has(e)||T.some(t=>t.id===e),rt=()=>b.filter(e=>e.role&&e.kind!=="merge"&&!Re(e.id)&&!ge.has(e.id)&&(e.dependsOn||[]).every(Re));let he=b.filter(e=>e.parent).length;const it=async(e,t,i)=>{if(he>=Le)return{error:`This run has reached its limit of ${Le} sub-tasks. Do this yourself, or say what you could not do.`};if((Number(e.depth)||0)>=Tt)return{error:"A sub-task cannot delegate further. Do this yourself, or say what you could not do."};if(U()||D)return{error:"The run is stopping; do what you can and finish."};he+=1;const n=yt(i,e,{id:`${e.id}-s${he}`,by:t.id,now:l()});b.push(n),p("task.requested",{taskId:n.id,parent:e.id,by:t.id,title:n.title,prompt:n.prompt,needs:n.needs,dependsOn:n.dependsOn,depth:n.depth,wait:n.wait,postId:i.postId||null});const o=r.openThread({taskId:n.id,kind:"task",title:n.title,by:t.id,parent:e.id});let s=bt(n,c.roles,{exclude:[t.id]}),d=null;if(s.roleId)d={by:"fit",roleId:s.roleId,fit:s.fit,reasons:s.reasons};else if(typeof oe=="function"){const f=It(n,{runId:C,projectId:Ze||null,by:t.id});p("task.posted",{taskId:n.id,job:f,why:s.why}),r.post({threadId:o.id,by:y,kind:"note",text:`Posted on the job board — ${s.why}. Needs: ${Ye(n.needs)}.`});let g=null;try{g=await oe(f,{runId:C,requestedBy:t.id})}catch(j){r.post({threadId:o.id,by:y,kind:"note",text:`Recruiting failed: ${String(j?.message||j).slice(0,200)}`})}if(g?.role)d={by:"recruit",roleId:Be(g.role,f),agentId:g.agentId||g.role.agent||null,engine:g.engine||null,why:g.why||"",fit:g.fit??null};else{const j=g?.proposal||At(St(f),f,{by:"runner"}),w=j?.ok===!1?null:j?.agent||j,_=r.openThread({kind:"proposal",title:`New agent for "${n.title}"`,by:y,parent:e.id}),Y=r.post({threadId:_.id,by:y,kind:"draft",status:"proposed",text:`No one in the pool fits "${n.title}"${g?.why?` — ${g.why}`:""}. Proposed: ${w?.name||n.title}${w?.skills?.length?` — skills ${w.skills.join(", ")}`:""}${w?.grants?.length?`; grants ${w.grants.join(", ")}`:""}.`,refs:[`task:${n.id}`],...w?{proposal:{kind:"agent",agent:w,jobId:f.id}}:{}});p("task.proposed",{taskId:n.id,threadId:_.id,postId:Y.id,agent:w,job:f,why:g?.why||s.why});const v=w?await xe({type:"permission",taskId:n.id,text:`No one fits "${n.title}". Create the agent "${w.name}" (${Ye({skills:w.skills,grants:w.grants})}) and give it the job?`,options:["Create it","Skip"]}):null;if(v&&/create|yes|approve|allow|go/i.test(v.text)){r.decide(Y.id,"approved",v.by||ze);let m=null;try{m=await oe(f,{runId:C,requestedBy:t.id,create:w})}catch(de){r.post({threadId:o.id,by:y,kind:"note",text:`Creating the agent failed: ${String(de?.message||de).slice(0,200)}`})}m?.role&&(d={by:"created",roleId:Be(m.role,f),agentId:m.agentId||m.role.agent||null,engine:m.engine||null,why:m.why||"created for this job",fit:null})}else v&&r.decide(Y.id,"rejected",v.by||ze)}}if(!d){const f=typeof oe=="function"?"nobody in the run fits and no agent was recruited":`${s.why}; this run has no pool to recruit from`;return r.post({threadId:o.id,by:y,kind:"note",text:`Not taken: ${f}.`}),r.setThreadStatus(o.id,"failed"),p("task.unassigned",{taskId:n.id,why:f}),{taskId:n.id,takenBy:null,why:f,hint:"Nobody could take this. Do what you can yourself and say what is missing."}}if(n.role=d.roleId,r.setThreadStatus(o.id,"open",{holder:d.roleId}),r.post({threadId:o.id,by:y,kind:"decision",text:d.by==="fit"?wt(n,s):`${d.roleId} ${d.by==="created"?"was created and":"was recruited from the pool and"} took: ${n.title}${d.why?` — ${d.why}`:""}`}),p("task.taken",{taskId:n.id,role:d.roleId,by:d.by,fit:d.fit??null,reasons:d.reasons||[],agentId:d.agentId||null,engine:d.engine||null,why:d.why||""}),n.wait){const f=await me(n);return{taskId:n.id,takenBy:d.roleId,status:f.status,...f.error?{error:f.error}:{},findings:(f.findings||[]).map(g=>({kind:g.kind,text:g.text,refs:g.refs})),text:String(f.text||"").slice(0,4e3)}}const P=$t(b,e.id,n.id);return{taskId:n.id,takenBy:d.roleId,queued:!0,hint:`It runs after your task; ${P.length?`${P.join(", ")} will read it too`:"its findings will be on the board"}. Finish your task and say what depends on it.`}},Be=(e,t)=>{const i={...e,id:String(e.id||t.id),grants:e.grants||t.needs?.grants||["none"]},o={...Ie({name:c.name,roles:[i],budget:c.budget}).roles[0],...e.model?{model:e.model}:{},...e.engine?{engine:e.engine}:{},...e.skills?{skills:e.skills}:{},...e.workdir?{workdir:e.workdir}:{},recruited:!0};return c.roles.some(s=>s.id===o.id)||(c.roles.push(o),p("run.role-added",{role:{id:o.id,name:o.name,agent:o.agent||null,grants:o.grants,model:o.model||null,engine:o.engine||null},jobId:t.id})),o.id},me=async e=>{ge.add(e.id);try{return await st(e)}finally{ge.delete(e.id)}},st=async e=>{if(U()||D||X){const a={id:e.id,role:e.role,status:"skipped",text:"",findings:[]};return T.push(a),a}const t=Oe(e.role),i=l(),n=et.get(e.id)||null;p("task.started",{taskId:e.id,role:e.role,title:e.title,...e.parent?{parent:e.parent}:{},...n?{resumed:!0,steps:n.transcript.length}:{}});let o="",s=null,d="ok",P=null,f=n?[...n.transcript]:[];const g=n?.attempts?[...n.attempts]:[];let j=null,w=null,_=new AbortController;const Y=()=>_.abort();Z?.addEventListener?.("abort",Y,{once:!0});let v=null,m=Ae?._pendingFor?.(e.id)||null;const de=Ae?._subscribe?.(a=>a.type!=="handoff"||a.taskId!==e.id?!1:(m=a,_.abort(),!0))||null;let Fe=0;const _e=a=>Mt({...a,at:Number.isFinite(a?.at)?a.at:l(),attempt:Number.isFinite(a?.attempt)?a.attempt:Fe}),Me=(a,le)=>{const ue=Ct(a,le);ue.length&&p("task.step",{taskId:e.id,role:t.id,steps:ue.map(_e)})},ke=t?.grants||[],J=e.kind==="merge",ae=t?.mode==="model"&&!J?[...new Set([...Nt(e.prompt,{held:ke}),...(e.grants||[]).filter(a=>ke.includes(a)||a.startsWith("mcp:")&&ke.includes("mcp"))])]:[];let ye=!1;try{if(!t)throw new Error(`no role "${e.role}" in the team`);if(t.mode==="recipe"){if(typeof M!="function")throw new Error("this host cannot run recipes");const a=await M(t.recipe,{request:String(h||""),task:e.prompt});o=typeof a=="string"?a:JSON.stringify(a)}else{if(!N.canAfford({tokens:0}))throw D=!0,new Error("over budget");const a=ht(r,{taskIds:!J&&e.dependsOn?.length?e.dependsOn:null,role:t.id}),le=[e.prompt,a,J?"":mt()].filter(Boolean).join(`
|
|
22
3
|
|
|
23
|
-
|
|
24
|
-
import { normalizeEngine, normalizeScm } from './scorecard.js';
|
|
25
|
-
import { createBudget } from './budget.js';
|
|
26
|
-
import { fixedPlan, plannerPrompt, parsePlan } from './team-plan.js';
|
|
27
|
-
import { createBoard, parseFindings, boardText, findingsInstruction, toBriefClaims, RUNNER, PERSON } from './team-board.js';
|
|
28
|
-
import { subtaskFromRequest, takeUp, takeUpLine, jobFromSubtask, extendDependents, MAX_SUBTASKS, MAX_DEPTH } from './team-subtask.js';
|
|
29
|
-
import { proposalToAgent, proposalFromNeeds } from './recruit.js';
|
|
30
|
-
import { grantsNeededFor } from './tool-need.js';
|
|
31
|
-
import { boardToolProvider, createAnswerBox, withBoardTool, DEFAULT_ASK_TIMEOUT_MS } from './board-tool.js';
|
|
32
|
-
import { createRunCache, withRunCache } from './team-cache.js';
|
|
33
|
-
import { messagesFor, mergeTranscript, clipTranscript, clipMessage as clipTranscriptOne, newSteps, continuationNote, isThought } from './team-task.js';
|
|
34
|
-
import { converge } from './promotion.js';
|
|
4
|
+
`),ue=jt({board:r,role:t.id,taskId:e.id,taskIds:!J&&e.dependsOn?.length?e.dependsOn:null,askTimeoutMs:ee,signal:_.signal,onAsk:k=>{p("task.waiting",{taskId:e.id,role:t.id,threadId:k.id,text:k.title})},onRequest:k=>it(e,t,k),waitFor:ee>0?async(k,I,we)=>{const q=await Ne.wait(k,I,we);return!q&&!U()&&(v=k,_.abort()),q}:null}),at=Ot(Rt(await E(t),ce,{role:t.id}),ue),W=new Set((n?.attempts||[]).filter(k=>k&&k.model&&k.status==="error"&&Xe(k.error)).map(k=>k.model));let be="",Q=n?pe(n.status==="waiting"?{kind:"answer",answer:"see the board"}:{kind:"resume",reason:n.error||n.status}):null,K=n?.attempts?.at?.(-1)?.model||null;for(let k=1;;k++){let I;const we=m;if(m){I={model:m.model,mode:t.mode};const O=r.threadForTask(e.id);O&&r.post({threadId:O.id,by:y,kind:"decision",text:`Handed off from ${K||"the roster"} to ${m.model} by ${m.by}${m.reason?` — ${m.reason}`:""}.`}),p("task.handoff",{taskId:e.id,role:t.id,from:K,to:m.model,by:m.by,reason:m.reason||""}),Q=pe({kind:"handoff",from:K,to:m.model,reason:`handed off by ${m.by}`}),m=null,_=new AbortController,Z?.addEventListener?.("abort",Y,{once:!0})}else ye&&K&&!W.has(K)?I={model:K,mode:t.mode}:I=fe(t,ot(W));if(!I?.model)throw new Error(W.size?`no model left for role "${t.id}" after ${[...W].join(", ")}`:`no model for role "${t.id}"`);k>1&&be&&p("task.reappointed",{taskId:e.id,role:t.id,model:I.model,after:[...W],error:be}),p("task.model",{taskId:e.id,role:t.id,model:I.model,label:I.label||null,attempt:k}),j=nt(I,t,{attempt:k,exclude:W,handoff:we}),p("task.routed",{taskId:e.id,role:t.id,attempt:k,...j}),g.push({model:I.model,label:I.label||j.engine?.label||j.engine?.model||null,engine:j.engine,at:l(),continued:!!Q}),Fe=g.length;const q=Bt({transcript:f},{prompt:le,note:Q});let ne=q;const Ce=f.filter(O=>!Ut(O));q.length>Ce.length&&Me(Ce,q);const lt=O=>{O&&O.role&&(ne=[...ne,O],p("task.step",{taskId:e.id,role:t.id,steps:[_e(O)]}))},x=await S({runId:C,taskId:e.id,role:t.id,model:I.model,mode:I.mode||t.mode,system:t.prompt,prompt:le,messages:q,tools:at,signal:_.signal,onDelta:(O,F)=>p("task.delta",{taskId:e.id,role:t.id,delta:O,text:F}),onStep:lt});if(s=x?.usage||null,s&&N.charge(s),Pe(x?.scm)&&(w=Pe(x.scm),p("task.scm",{taskId:e.id,role:t.id,...w})),f=Ft(q,x),f.length<ne.length&&(f=ne),Me(ne,f),K=I.model,g[g.length-1].status=x?.ok?String(x?.text||"").trim()?"ok":"empty":"error",v){d="waiting",X=!0;break}if(m){Q=null;continue}if(x?.aborted||_.signal.aborted){d="stopped";break}if(x?.ok&&String(x?.text||"").trim()){if(!ye&&ae.length&&j?.engine?.kind!=="harness"&&!f.some(F=>F.role==="assistant"&&Array.isArray(F.tool_calls)&&F.tool_calls.some(z=>z.function?.name&&z.function.name!=="board"))&&N.canAfford({tokens:0})&&!U()){ye=!0,Q=pe({kind:"nudge",reason:ae.join(", ")});const F=r.threadForTask(e.id);F&&r.post({threadId:F.id,by:y,kind:"note",text:`${t.id} answered without using ${ae.join(", ")}, which the task calls for — asked once to use it before finishing.`}),p("task.nudged",{taskId:e.id,role:t.id,grants:ae});continue}o=String(x.text);break}if(x?.ok){const O=r.threadForTask(e.id),F=O?r.posts(O.id).filter(z=>z.by===t.id&&z.at>=i&&["note","draft","finding"].includes(z.kind)):[];if(F.length){o=F.map(z=>z.text).join(`
|
|
35
5
|
|
|
36
|
-
|
|
37
|
-
const DEFAULT_CONCURRENCY = 3;
|
|
6
|
+
`),p("task.note",{taskId:e.id,role:t.id,text:"answered from its board posts"});break}}const V=x?.ok?"the model returned no answer":x?.error||"the model did not answer";if(g[g.length-1].error=V,k>=tt||U()||!Xe(V))throw new Error(V);W.add(I.model),ve.add(I.model),be=V,Q=pe({kind:"handoff",from:I.model,reason:V});const Ue=r.threadForTask(e.id);Ue&&r.post({threadId:Ue.id,by:y,kind:"note",text:`${I.model} stopped (${V.slice(0,160)}); the task continues on the next model with its work so far.`})}}}catch(a){v?(d="waiting",X=!0):(d=D?"over-budget":"failed",P=String(a?.message||a))}finally{de?.()}const G=d==="ok"&&!J?gt(o,{role:t?.id,taskId:e.id}):[];if(G.length){r.add(G);for(const a of G)p("task.finding",{taskId:e.id,role:t?.id,finding:a})}const te=r.threadForTask(e.id);te&&d==="ok"?r.setThreadStatus(te.id,"resolved"):te&&d!=="waiting"&&(r.post({threadId:te.id,by:y,kind:"note",text:`${t?.id||e.role} ${d==="over-budget"?"stopped at the budget":"failed"}${P?`: ${String(P).slice(0,300)}`:""}${g.length>1?` (after ${g.length} models: ${g.map(a=>a.label||a.model).join(", ")})`:""}.`}),r.setThreadStatus(te.id,"failed"));const H={id:e.id,role:e.role,title:e.title,status:d,text:o,error:P,usage:s,ms:l()-i,findings:G,transcript:_t(f),attempts:g,...e.parent?{parent:e.parent}:{},...j?{routed:j}:{},...w?{scm:w}:{},...v?{waitingOn:v}:{}};return T.push(H),p(d==="ok"?"task.done":"task.failed",{taskId:e.id,role:e.role,status:d,error:P,ms:H.ms,findings:G.length,...v?{threadId:v}:{}}),t&&(d==="ok"||d==="failed")&&p("task.scored",{agentId:t.agent||t.id,taskId:e.id,role:t.id,model:Lt(g),engine:j?.engine||null,scm:w||void 0,outcome:d==="ok"?"task.done":"task.failed",size:{ms:H.ms,steps:(H.transcript||[]).length,tools:(H.transcript||[]).filter(a=>a.role==="tool").length,findings:G.length,tokens:s?Number(s.input_tokens||s.prompt_tokens||0)+Number(s.output_tokens||s.completion_tokens||0):0},roleKind:J?"orchestrator":"ic",tools:Ht(H.transcript),with:c.roles.filter(a=>a.id!==t.id).map(a=>a.agent||a.id),refs:[`run:${C}`,...r.threadForTask(e.id)?[`thread:${r.threadForTask(e.id).id}`]:[]],error:P||void 0,...e.parent?{parent:e.parent,requestedBy:e.requestedBy||null}:{}}),p("run.usage",{usage:N.snapshot()}),N.exhausted()&&(D=!0),H};for(;!(U()||D||X);){const e=rt();if(!e.length)break;if(await qt(e,Ge,me),D&&!ie&&!U()){ie=!0;const t=b.filter(s=>s.role&&s.kind!=="merge"&&!T.some(d=>d.id===s.id)).length,i=N.snapshot().spent,n=t?`${t} task${t===1?"":"s"} and the merge left`:"only the merge left",o=await xe({type:"budget",text:`The team has used its budget (${Object.entries(i).filter(([s])=>N.cap[s]!==void 0).map(([s,d])=>`${s} ${d} of ${N.cap[s]}`).join(", ")}) with ${n}. Raise it by half, or stop here with what it has?`,options:["Raise by half","Stop here"]});o&&/raise|allow|yes|more|continue/i.test(o.text)&&(N.raise(1.5),D=!1)}}for(const e of b)e.kind!=="merge"&&!T.some(t=>t.id===e.id)&&T.push({id:e.id,role:e.role,title:e.title,status:e.parent&&!e.role?"unassigned":"skipped",text:"",findings:[],...e.parent?{parent:e.parent}:{}});if(U())return L("stopped");if(X)return L("waiting",{proposal:null,budgetAsked:ie});if(D&&T.some(e=>e.status==="skipped"||e.status==="over-budget"))return L("over-budget",{proposal:$e(c,r.all(),T)});p("run.merging",{policy:c.merge});const se=T.filter(e=>e.status==="ok");let B;if(!se.length)return L("failed",{proposal:null});if(c.merge==="judge"){const e=Oe(c.judge)||Ke(c);let t=b.find(o=>o.kind==="merge");t||(t={id:"merge",kind:"merge",role:e.id,title:`merge (${e.name||e.id})`,prompt:[`You are the ${e.name||e.id} of team "${c.name}". The members' work is on the board below (and in the board tool). Write the team's FINAL ANSWER to the request: complete, well organised, only what the findings support, with the refs they came from. Say plainly what was not found or assumed. Flag anything the members disagreed on. Do not research from scratch — verify a figure with a tool only where the board is silent or contradictory.`,`Request: ${String(h||"").trim()}`].join(`
|
|
38
7
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
8
|
+
`),dependsOn:b.filter(o=>o.role&&o.kind!=="merge").map(o=>o.id)},b.push(t),p("task.added",{taskId:t.id,kind:"merge",role:t.role,title:t.title,dependsOn:t.dependsOn}),r.openThread({taskId:t.id,kind:"task",title:t.title,by:y,holder:e.id}));const n=T.find(o=>o.id===t.id&&o.status==="ok")||(fe(e)?.model&&N.canAfford({tokens:0})?await me(t):null);if(U())return L("stopped");if(X)return L("waiting",{proposal:null,budgetAsked:ie});B=n?.status==="ok"&&String(n.text||"").trim()?{kind:"answer",text:String(n.text),by:e.id,taskId:t.id}:$e(c,r.all(),T)}else if(c.merge==="converge"){const e=se.map(n=>({claims:kt(n.findings)})),{agreed:t,disputed:i}=Dt(e,{minAgree:Math.min(2,e.length)});B={kind:"claims",agreed:t,disputed:i,by:"converge"}}else c.merge==="first"?B={kind:"answer",text:se[0].text,by:se[0].role}:B=$e(c,r.all(),T);if(B?.text||B?.agreed){const e=r.openThread({kind:"proposal",title:`Proposal — ${c.merge}`,by:B.by||y});r.post({threadId:e.id,by:B.by||y,kind:"draft",status:"proposed",text:B.text||(B.agreed||[]).map(t=>t.text).join(`
|
|
9
|
+
`),refs:[]})}const dt=T.some(e=>e.status!=="ok");return L(dt?"partial":"completed",{proposal:B})}function sn({checkpoint:u,...h}={}){if(!u?.plan?.tasks)throw new He("BAD_RESUME","a checkpoint with a plan is required");return zt({...h,resume:u})}const Lt=u=>(u||[]).at(-1)?.model||null,Ye=u=>[u?.skills?.length?`skills ${u.skills.join(", ")}`:"",u?.grants?.length?`grants ${u.grants.join(", ")}`:"",u?.tools?.length?`tools ${u.tools.join(", ")}`:""].filter(Boolean).join("; ")||"nothing in particular",Ht=u=>[...new Set((u||[]).flatMap(h=>h.role==="assistant"&&Array.isArray(h.tool_calls)?h.tool_calls.map(S=>S.function?.name).filter(Boolean):[]))];function $e(u,h,S){return{kind:"answer",text:S.filter(A=>A.status==="ok").map(A=>{const M=u.roles.find(R=>R.id===A.role),l=(A.findings||[]).map(R=>`- ${R.text}${R.refs?.length?` (${R.refs.join(", ")})`:""}`).join(`
|
|
10
|
+
`);return`### ${M?.name||A.role}
|
|
11
|
+
${l||A.text}`}).join(`
|
|
42
12
|
|
|
43
|
-
|
|
44
|
-
const out = new Array(items.length);
|
|
45
|
-
let next = 0;
|
|
46
|
-
const worker = async () => { while (next < items.length) { const i = next++; out[i] = await fn(items[i], i); } };
|
|
47
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) || 1 }, worker));
|
|
48
|
-
return out;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const TIER = { cheap: 0, balanced: 1, strong: 2 };
|
|
52
|
-
const strongestRole = (team) => [...team.roles].sort((a, b) => (TIER[b.prefer] ?? 1) - (TIER[a.prefer] ?? 1))[0];
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* What a person sees before approving a run: every role with the model it would get, its
|
|
56
|
-
* grants and mode; the plan when it is fixed (a planner plans at run time); the budget.
|
|
57
|
-
*/
|
|
58
|
-
/**
|
|
59
|
-
* Was this failure the MODEL being unreachable — not found, not deployed, no key, gone —
|
|
60
|
-
* rather than the request being wrong? The runner re-appoints on these and gives up on the
|
|
61
|
-
* rest. Provider wording varies; what they share is that a different model would answer.
|
|
62
|
-
*/
|
|
63
|
-
export function isModelUnavailable(error) {
|
|
64
|
-
const m = String(error?.message || error || '');
|
|
65
|
-
// Also: a relayed agent that exited, a provider that closed the stream, and a turn that
|
|
66
|
-
// came back with nothing — the model did not answer, and the next one might. A refusal,
|
|
67
|
-
// a timeout the caller set, a bad request or a budget stop are not this.
|
|
68
|
-
// A server that is not there: Node says ECONNREFUSED, a browser says only "Failed to
|
|
69
|
-
// fetch" (the extension reports it as "network error") — a local model killed mid-run
|
|
70
|
-
// arrived as the latter and ended the task on its first attempt with Claude Code sitting
|
|
71
|
-
// idle on the roster.
|
|
72
|
-
// The source gate's refusal (source-gate.js) is about THIS model's reach, not the task:
|
|
73
|
-
// the next appointment, within reach, can do it.
|
|
74
|
-
if (/^Not sent: /.test(m)) return true;
|
|
75
|
-
return /model[_ ]not[_ ]found|not found|not deployed|inaccessible|does not exist|no such model|unknown model|unsupported model|not available|unavailable|no api key|not configured|"status":\s*(404|401|403|500|502|503)\b|\b(404|401|403|502|503)\b|exited \d+|returned no answer|did not answer|closed the connection|couldn't reach|could not reach|ECONNREFUSED|ECONNRESET|ENOTFOUND|EHOSTUNREACH|socket hang up|fetch failed|failed to fetch|load failed|network ?error|overloaded|capacity/i.test(m);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function dryRunTeam(team, request, { appoint = null } = {}) {
|
|
79
|
-
const t = normalizeTeam(team);
|
|
80
|
-
const roles = t.roles.map((r) => {
|
|
81
|
-
const a = appoint ? appoint(r) : null;
|
|
82
|
-
// `model` is what callModel will be handed (a target id in a client that resolves ids);
|
|
83
|
-
// `label` is what a person should read — the appointer says which, when it knows.
|
|
84
|
-
return { id: r.id, name: r.name, mode: r.mode, prefer: r.prefer, model: a?.model || r.model || null, label: a?.label || a?.model || r.model || null, appointed: !!(a?.model || r.model), grants: r.grants, ...(r.recipe ? { recipe: r.recipe } : {}) };
|
|
85
|
-
});
|
|
86
|
-
const missing = roles.filter((r) => r.mode !== 'recipe' && !r.appointed).map((r) => r.id);
|
|
87
|
-
return {
|
|
88
|
-
name: t.name, description: t.description, plan: t.plan, merge: t.merge, judge: t.judge, budget: t.budget, roles,
|
|
89
|
-
tasks: t.plan === 'fixed' ? fixedPlan(t, request) : null,
|
|
90
|
-
ok: missing.length === 0,
|
|
91
|
-
missing,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* @param callModel `async ({ runId, taskId, role, model, mode, system, prompt, tools, signal, onDelta }) =>
|
|
97
|
-
* { ok, text, usage?, error?, aborted?, scm? }` — the host's model turn; `scm`
|
|
98
|
-
* is what a harness did in a git checkout (`{ repo, branch, head, headAfter,
|
|
99
|
-
* commits }`), when the bridge reported one
|
|
100
|
-
* @param toolsFor `(role) => toolset | undefined` — narrowed to the role's grants by the host
|
|
101
|
-
* @param appoint `(role) => { model, mode, engine?, reasons?, alternatives? } | null` — the host's
|
|
102
|
-
* roster through cowriter-router. `engine` (`{ kind: 'model'|'harness', id,
|
|
103
|
-
* model? }`) says WHAT the model id is, so the record can split by it;
|
|
104
|
-
* `reasons` and `alternatives` are why this one and who else could have —
|
|
105
|
-
* said as `task.routed`, which every run records from here on (pillars §13)
|
|
106
|
-
* @param runRecipe `async (name, params) => result` for `mode: 'recipe'` roles (optional)
|
|
107
|
-
* @param recruit `async (job, { runId, requestedBy, create? }) => { role, agentId?, engine?, why?, fit? }
|
|
108
|
-
* | { proposal? , why? } | null` — the host's job board (recruit.js over its
|
|
109
|
-
* pool): a sub-task nobody in the run fits is posted as `job`; the host
|
|
110
|
-
* answers with the role to add (job.js jobToRole, resolved to a model), or
|
|
111
|
-
* nothing, optionally with the agent it would propose. With `create` (the
|
|
112
|
-
* card a person approved on the board) the host adds it to the pool first.
|
|
113
|
-
* Absent, a sub-task nobody fits is recorded unassigned.
|
|
114
|
-
* @param projectId the project a job posting belongs to, when the run has one (else the run id)
|
|
115
|
-
* @param emit `(type, payload)` — run.started · plan.ready · task.started · task.finding ·
|
|
116
|
-
* task.done · task.failed · run.merging · run.done; the host forwards them to
|
|
117
|
-
* its UI and to the gateway's run store
|
|
118
|
-
*/
|
|
119
|
-
export async function runTeam({
|
|
120
|
-
team, request, callModel, toolsFor = () => undefined, appoint = null, runRecipe = null,
|
|
121
|
-
now = () => Date.now(), newId = () => `run_${Math.random().toString(36).slice(2, 10)}`,
|
|
122
|
-
emit = () => {}, signal = null, maxConcurrency = DEFAULT_CONCURRENCY, runId = null,
|
|
123
|
-
// Asks: the box a person's answers arrive in (the host feeds it from its UI and from the
|
|
124
|
-
// run store's tail), and how long a member waits before the run checkpoints. 0 = a member
|
|
125
|
-
// that asks is told to proceed on its own assumption at once.
|
|
126
|
-
answers = null, askTimeoutMs = DEFAULT_ASK_TIMEOUT_MS,
|
|
127
|
-
// A checkpoint from a run that ended `waiting` — see resumeTeam.
|
|
128
|
-
resume = null,
|
|
129
|
-
// The host's control channel (team-task.js createControl): a person hands a task to another
|
|
130
|
-
// model from the board, on either client; the runner continues the task's transcript there.
|
|
131
|
-
control = null,
|
|
132
|
-
recruit = null, projectId = null,
|
|
133
|
-
// Earlier runs' findings for the same request (team-record.js priorWorkFor, fetched by the
|
|
134
|
-
// host): `[{ runId, at, request?, findings: [...] }]`. Posted as a "Prior work" thread every
|
|
135
|
-
// member reads before repeating a lookup — the librarian's first step (§12.2.6).
|
|
136
|
-
prior = null,
|
|
137
|
-
} = {}) {
|
|
138
|
-
if (typeof callModel !== 'function') throw new TeamRunError('BAD_RUN', 'callModel required');
|
|
139
|
-
const t = normalizeTeam(team); // throws on a team without a budget — O1
|
|
140
|
-
const id = runId || resume?.runId || newId();
|
|
141
|
-
const budget = createBudget(resume?.budget?.cap || t.budget, { now });
|
|
142
|
-
if (resume?.budget?.spent) budget.charge({ tokens: resume.budget.spent.tokens, calls: resume.budget.spent.calls, usd: resume.budget.spent.usd });
|
|
143
|
-
const say = (type, payload = {}) => emit(type, { runId: id, at: now(), ...payload });
|
|
144
|
-
// Every change to the board is an event the run store folds — the other client reads it live.
|
|
145
|
-
const board = createBoard({ now, state: resume?.board || null, onEvent: (type, ev) => say(type, ev) });
|
|
146
|
-
// No answer box from the host means nobody can answer: asks are off, a member that asks is
|
|
147
|
-
// told to proceed on its own assumption at once, and the budget stop is final.
|
|
148
|
-
const runCache = createRunCache(); // one lookup per run, across members
|
|
149
|
-
const box = answers || createAnswerBox();
|
|
150
|
-
const askMs = answers ? askTimeoutMs : 0;
|
|
151
|
-
const startedAt = resume?.startedAt || now();
|
|
152
|
-
// Tasks a checkpoint already finished are carried over, not re-run. Tasks it left
|
|
153
|
-
// interrupted — waiting, stopped, failed, or running when the process died — are resumed
|
|
154
|
-
// from their transcripts, not started again.
|
|
155
|
-
const tasksOut = (resume?.tasks || []).filter((x) => x.status === 'ok').map((x) => ({ ...x }));
|
|
156
|
-
const carried = new Set(tasksOut.map((x) => x.id));
|
|
157
|
-
const interrupted = new Map((resume?.tasks || []).filter((x) => x.status !== 'ok' && Array.isArray(x.transcript) && x.transcript.length).map((x) => [x.id, x]));
|
|
158
|
-
const roleOf = (rid) => t.roles.find((r) => r.id === rid);
|
|
159
|
-
// `exclude` holds what failed as unavailable this run; a re-appointment skips it. A role's
|
|
160
|
-
// pinned model is tried first and, when it is the one that failed, the roster steps in.
|
|
161
|
-
const modelFor = (r, exclude = null) => {
|
|
162
|
-
if (exclude?.size && r.model && exclude.has(r.model)) return appoint ? appoint({ ...r, model: undefined }, { exclude }) : null;
|
|
163
|
-
return (appoint ? appoint(r, { exclude }) : null) || (r.model && !exclude?.has(r.model) ? { model: r.model, mode: r.mode } : null);
|
|
164
|
-
};
|
|
165
|
-
const MAX_APPOINTMENTS = 3;
|
|
166
|
-
// The routing decision, on the record: which engine, why, who else could have. A host that
|
|
167
|
-
// does not say the kind gets `model` — the honest default for a bare id; the pilot's hosts
|
|
168
|
-
// both say. Exploration (a tier cheaper on purpose) is the project loop's, later; false here.
|
|
169
|
-
const routeOf = (m, role, { attempt = 1, exclude = null, handoff = null } = {}) => {
|
|
170
|
-
const reasons = Array.isArray(m.reasons) ? m.reasons.map(String) : [];
|
|
171
|
-
if (handoff) reasons.unshift(`handed off by ${handoff.by}${handoff.reason ? ` — ${handoff.reason}` : ''}`);
|
|
172
|
-
else if (!reasons.length && role.model && m.model === role.model) reasons.push('pinned by the role');
|
|
173
|
-
if (attempt > 1 && exclude?.size) reasons.push(`after ${[...exclude].join(', ')} (unavailable)`);
|
|
174
|
-
return {
|
|
175
|
-
engine: normalizeEngine(m.engine || { id: m.model }),
|
|
176
|
-
reasons,
|
|
177
|
-
alternatives: (Array.isArray(m.alternatives) ? m.alternatives : []).slice(0, 5).map((a) => normalizeEngine(a)).filter(Boolean),
|
|
178
|
-
exploration: false,
|
|
179
|
-
};
|
|
180
|
-
};
|
|
181
|
-
// A model that was not there for one member is not there for the next: what failed as
|
|
182
|
-
// unavailable anywhere in this run is skipped by every later appointment. Two members
|
|
183
|
-
// each spent two minutes finding out the same agent was down.
|
|
184
|
-
const runExclude = new Set();
|
|
185
|
-
const excluding = (local) => new Set([...runExclude, ...(local || [])]);
|
|
186
|
-
const stopped = () => !!signal?.aborted;
|
|
187
|
-
|
|
188
|
-
say(resume ? 'run.resumed' : 'run.started', { team: t.name, request: String(request || ''), budget: budget.cap, roles: t.roles.map((r) => r.id), ...(resume ? { carried: [...carried] } : {}) });
|
|
189
|
-
|
|
190
|
-
// ── plan ──────────────────────────────────────────────────────────────────────────────
|
|
191
|
-
let tasks = resume?.plan?.tasks?.length ? resume.plan.tasks : null;
|
|
192
|
-
let planBy = resume?.plan?.by || 'fixed';
|
|
193
|
-
if (!tasks && t.plan === 'planner') {
|
|
194
|
-
const planner = strongestRole(t);
|
|
195
|
-
const m = modelFor(planner);
|
|
196
|
-
if (m && budget.canAfford({ tokens: 0 })) {
|
|
197
|
-
const res = await callModel({ runId: id, taskId: 'plan', role: planner.id, model: m.model, mode: 'model', system: '', prompt: plannerPrompt(t, request), tools: undefined, signal });
|
|
198
|
-
if (res?.usage) budget.charge(res.usage);
|
|
199
|
-
const parsed = res?.ok ? parsePlan(res.text, t) : [];
|
|
200
|
-
if (parsed.length) { tasks = parsed; planBy = 'planner'; }
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (!tasks) tasks = fixedPlan(t, request);
|
|
204
|
-
say('plan.ready', { by: planBy, tasks: tasks.map((x) => ({ id: x.id, role: x.role, title: x.title, dependsOn: x.dependsOn, ...(x.parent ? { parent: x.parent, requestedBy: x.requestedBy || null } : {}), ...(x.grants ? { grants: x.grants, why: x.why || '' } : {}) })) });
|
|
205
|
-
// A thread per task, before anything runs: a member's findings and replies have a home.
|
|
206
|
-
for (const task of tasks) board.openThread({ taskId: task.id, kind: 'task', title: task.title || task.id, by: task.requestedBy || RUNNER, ...(task.parent ? { parent: task.parent } : {}), ...(task.role && task.parent ? { holder: task.role } : {}) });
|
|
207
|
-
// PRIOR WORK: what an earlier run already found for this request is on the board before
|
|
208
|
-
// anyone starts — a discussion thread per run, its findings as posts with their refs — so a
|
|
209
|
-
// second run of the same question reads instead of re-searching. Only findings a person did
|
|
210
|
-
// not reject; at most 40 per run.
|
|
211
|
-
if (!resume && Array.isArray(prior)) {
|
|
212
|
-
for (const pr of prior.filter((x) => x && Array.isArray(x.findings) && x.findings.length)) {
|
|
213
|
-
const age = Number.isFinite(pr.at) ? Math.max(0, Math.round((now() - pr.at) / 60000)) : null;
|
|
214
|
-
const th = board.openThread({ kind: 'discussion', title: `Prior work — run ${pr.runId}${age != null ? ` (${age < 60 ? `${age} min` : `${Math.round(age / 60)} h`} ago)` : ''}`, by: RUNNER });
|
|
215
|
-
board.post({ threadId: th.id, by: RUNNER, kind: 'note', text: `An earlier run answered ${pr.request ? `"${String(pr.request).slice(0, 200)}"` : 'the same request'}. Its findings follow — read them, verify what is stale, and do not repeat lookups already made.`, refs: [`run:${pr.runId}`] });
|
|
216
|
-
let n = 0;
|
|
217
|
-
for (const f of pr.findings) {
|
|
218
|
-
if (!f || !f.text || f.status === 'rejected' || n >= 40) continue;
|
|
219
|
-
board.post({ threadId: th.id, by: f.role || RUNNER, kind: 'finding', text: String(f.text).slice(0, 2000), refs: [...(Array.isArray(f.refs) ? f.refs : []), `run:${pr.runId}`].slice(0, 8), finding: { kind: f.kind || 'claim', confidence: f.confidence ?? null, prior: true } });
|
|
220
|
-
n += 1;
|
|
221
|
-
}
|
|
222
|
-
say('run.prior', { from: pr.runId, threadId: th.id, findings: n });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
// The planner's TOOL PROPOSAL (§15.2): which tools each task will need and why, as a
|
|
226
|
-
// proposal thread a person reads — and what the nudge below holds the member to.
|
|
227
|
-
if (!resume && tasks.some((x) => x.grants?.length)) {
|
|
228
|
-
const th = board.openThread({ kind: 'proposal', title: 'Tools per task', by: t.roles.find((r) => r.id === (tasks[0]?.role))?.id || RUNNER });
|
|
229
|
-
const lines = tasks.filter((x) => x.grants?.length).map((x) => { const held = t.roles.find((r) => r.id === x.role)?.grants || []; const missing = x.grants.filter((g) => !held.includes(g) && !(g.startsWith('mcp:') && held.includes('mcp'))); return `${x.role} (${x.id}): ${x.grants.join(', ')}${x.why ? ` — ${x.why}` : ''}${missing.length ? ` (not granted: ${missing.join(', ')})` : ''}`; });
|
|
230
|
-
board.post({ threadId: th.id, by: RUNNER, kind: 'draft', status: 'proposed', text: lines.join('\n') });
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const finish = (status, extra = {}) => {
|
|
234
|
-
const usage = budget.snapshot();
|
|
235
|
-
const out = { runId: id, team: t.name, status, plan: { by: planBy, tasks }, tasks: tasksOut, board: board.all(), threads: board.state(), usage, lookups: { distinct: runCache.size, shared: runCache.shared }, startedAt, endedAt: now(), ...extra };
|
|
236
|
-
// What a resume needs, on the record: the plan, what finished, the board, the spend.
|
|
237
|
-
// A run that did not complete can be resumed from its record — the plan, every task's
|
|
238
|
-
// transcript and status, the board, the spend — by any client, any time later.
|
|
239
|
-
if (status !== 'completed') out.checkpoint = { runId: id, startedAt, plan: { by: planBy, tasks }, tasks: tasksOut.map((x) => ({ ...x, findings: undefined })), board: board.state(), budget: { cap: budget.cap, spent: usage.spent }, budgetAsked: !!extra.budgetAsked };
|
|
240
|
-
say('run.done', { status, usage, proposal: out.proposal || null, failedTaskIds: tasksOut.filter((x) => x.status === 'failed').map((x) => x.id), waitingTaskIds: tasksOut.filter((x) => x.status === 'waiting').map((x) => x.id), ...(out.checkpoint ? { checkpoint: out.checkpoint } : {}) });
|
|
241
|
-
return out;
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* The runner asks the person (a budget, a direction). Same thread shape as a member's ask;
|
|
246
|
-
* answered from either client; null when nobody answered in time or asks are off.
|
|
247
|
-
*/
|
|
248
|
-
const askPerson = async ({ type, text, options, taskId = null }) => {
|
|
249
|
-
if (askMs <= 0) return null;
|
|
250
|
-
const { thread } = board.ask({ taskId, by: RUNNER, type, text, options, timeoutMs: askMs });
|
|
251
|
-
say('run.waiting', { threadId: thread.id, askType: type, text, options });
|
|
252
|
-
return box.wait(thread.id, askMs, signal);
|
|
253
|
-
};
|
|
254
|
-
let waitingOnPerson = false; // a task that timed out on its ask — the run checkpoints
|
|
255
|
-
|
|
256
|
-
// ── fan out: everything ready runs together; a sub-task requested mid-run joins the plan ──
|
|
257
|
-
let overBudget = false;
|
|
258
|
-
let budgetAsked = !!resume?.budgetAsked;
|
|
259
|
-
const running = new Set();
|
|
260
|
-
const isDone = (tid) => carried.has(tid) || tasksOut.some((x) => x.id === tid);
|
|
261
|
-
// A sub-task with no holder cannot run; it is recorded `unassigned` at the end.
|
|
262
|
-
const ready = () => tasks.filter((x) => x.role && x.kind !== 'merge' && !isDone(x.id) && !running.has(x.id) && (x.dependsOn || []).every(isDone));
|
|
263
|
-
let subtasks = tasks.filter((x) => x.parent).length;
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* A member's REQUEST (board tool `request`): a sub-task with its own thread, offered to the
|
|
267
|
-
* run's members first, then to the pool through the host's `recruit`, then proposed as a
|
|
268
|
-
* new agent to the person. With `wait` the requester gets the findings back in its turn;
|
|
269
|
-
* without, the sub-task runs after it and whoever depended on the requester reads it too.
|
|
270
|
-
*/
|
|
271
|
-
const onRequest = async (parentTask, role, req) => {
|
|
272
|
-
if (subtasks >= MAX_SUBTASKS) return { error: `This run has reached its limit of ${MAX_SUBTASKS} sub-tasks. Do this yourself, or say what you could not do.` };
|
|
273
|
-
if ((Number(parentTask.depth) || 0) >= MAX_DEPTH) return { error: 'A sub-task cannot delegate further. Do this yourself, or say what you could not do.' };
|
|
274
|
-
if (stopped() || overBudget) return { error: 'The run is stopping; do what you can and finish.' };
|
|
275
|
-
subtasks += 1;
|
|
276
|
-
const sub = subtaskFromRequest(req, parentTask, { id: `${parentTask.id}-s${subtasks}`, by: role.id, now: now() });
|
|
277
|
-
tasks.push(sub);
|
|
278
|
-
say('task.requested', { taskId: sub.id, parent: parentTask.id, by: role.id, title: sub.title, prompt: sub.prompt, needs: sub.needs, dependsOn: sub.dependsOn, depth: sub.depth, wait: sub.wait, postId: req.postId || null });
|
|
279
|
-
const thread = board.openThread({ taskId: sub.id, kind: 'task', title: sub.title, by: role.id, parent: parentTask.id });
|
|
280
|
-
// 1. The run's own members, by fit on skills and grants.
|
|
281
|
-
let pick = takeUp(sub, t.roles, { exclude: [role.id] });
|
|
282
|
-
let takenBy = null;
|
|
283
|
-
if (pick.roleId) {
|
|
284
|
-
takenBy = { by: 'fit', roleId: pick.roleId, fit: pick.fit, reasons: pick.reasons };
|
|
285
|
-
} else if (typeof recruit === 'function') {
|
|
286
|
-
// 2. The job board: the pool applies, the host recruits an (agent, engine) pair.
|
|
287
|
-
const job = jobFromSubtask(sub, { runId: id, projectId: projectId || null, by: role.id });
|
|
288
|
-
say('task.posted', { taskId: sub.id, job, why: pick.why });
|
|
289
|
-
board.post({ threadId: thread.id, by: RUNNER, kind: 'note', text: `Posted on the job board — ${pick.why}. Needs: ${describeNeeds(sub.needs)}.` });
|
|
290
|
-
let r = null;
|
|
291
|
-
try { r = await recruit(job, { runId: id, requestedBy: role.id }); } catch (e) { board.post({ threadId: thread.id, by: RUNNER, kind: 'note', text: `Recruiting failed: ${String(e?.message || e).slice(0, 200)}` }); }
|
|
292
|
-
if (r?.role) {
|
|
293
|
-
takenBy = { by: 'recruit', roleId: addRole(r.role, job), agentId: r.agentId || r.role.agent || null, engine: r.engine || null, why: r.why || '', fit: r.fit ?? null };
|
|
294
|
-
} else {
|
|
295
|
-
// 3. Nobody applies: propose the agent the job describes — a person decides; nothing
|
|
296
|
-
// is created here. With asks on, the person is asked now and the run goes on either way.
|
|
297
|
-
const card = r?.proposal || proposalToAgent(proposalFromNeeds(job), job, { by: 'runner' });
|
|
298
|
-
const agentCard = card?.ok === false ? null : (card?.agent || card);
|
|
299
|
-
const pth = board.openThread({ kind: 'proposal', title: `New agent for "${sub.title}"`, by: RUNNER, parent: parentTask.id });
|
|
300
|
-
const post = board.post({ threadId: pth.id, by: RUNNER, kind: 'draft', status: 'proposed', text: `No one in the pool fits "${sub.title}"${r?.why ? ` — ${r.why}` : ''}. Proposed: ${agentCard?.name || sub.title}${agentCard?.skills?.length ? ` — skills ${agentCard.skills.join(', ')}` : ''}${agentCard?.grants?.length ? `; grants ${agentCard.grants.join(', ')}` : ''}.`, refs: [`task:${sub.id}`], ...(agentCard ? { proposal: { kind: 'agent', agent: agentCard, jobId: job.id } } : {}) });
|
|
301
|
-
say('task.proposed', { taskId: sub.id, threadId: pth.id, postId: post.id, agent: agentCard, job, why: r?.why || pick.why });
|
|
302
|
-
const a = agentCard ? await askPerson({ type: 'permission', taskId: sub.id, text: `No one fits "${sub.title}". Create the agent "${agentCard.name}" (${describeNeeds({ skills: agentCard.skills, grants: agentCard.grants })}) and give it the job?`, options: ['Create it', 'Skip'] }) : null;
|
|
303
|
-
if (a && /create|yes|approve|allow|go/i.test(a.text)) {
|
|
304
|
-
board.decide(post.id, 'approved', a.by || PERSON);
|
|
305
|
-
let made = null;
|
|
306
|
-
try { made = await recruit(job, { runId: id, requestedBy: role.id, create: agentCard }); } catch (e) { board.post({ threadId: thread.id, by: RUNNER, kind: 'note', text: `Creating the agent failed: ${String(e?.message || e).slice(0, 200)}` }); }
|
|
307
|
-
if (made?.role) takenBy = { by: 'created', roleId: addRole(made.role, job), agentId: made.agentId || made.role.agent || null, engine: made.engine || null, why: made.why || 'created for this job', fit: null };
|
|
308
|
-
} else if (a) board.decide(post.id, 'rejected', a.by || PERSON);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
if (!takenBy) {
|
|
312
|
-
const why = typeof recruit === 'function' ? 'nobody in the run fits and no agent was recruited' : `${pick.why}; this run has no pool to recruit from`;
|
|
313
|
-
board.post({ threadId: thread.id, by: RUNNER, kind: 'note', text: `Not taken: ${why}.` });
|
|
314
|
-
board.setThreadStatus(thread.id, 'failed');
|
|
315
|
-
say('task.unassigned', { taskId: sub.id, why });
|
|
316
|
-
return { taskId: sub.id, takenBy: null, why, hint: 'Nobody could take this. Do what you can yourself and say what is missing.' };
|
|
317
|
-
}
|
|
318
|
-
sub.role = takenBy.roleId;
|
|
319
|
-
board.setThreadStatus(thread.id, 'open', { holder: takenBy.roleId });
|
|
320
|
-
board.post({ threadId: thread.id, by: RUNNER, kind: 'decision', text: takenBy.by === 'fit' ? takeUpLine(sub, pick) : `${takenBy.roleId} ${takenBy.by === 'created' ? 'was created and' : 'was recruited from the pool and'} took: ${sub.title}${takenBy.why ? ` — ${takenBy.why}` : ''}` });
|
|
321
|
-
say('task.taken', { taskId: sub.id, role: takenBy.roleId, by: takenBy.by, fit: takenBy.fit ?? null, reasons: takenBy.reasons || [], agentId: takenBy.agentId || null, engine: takenBy.engine || null, why: takenBy.why || '' });
|
|
322
|
-
if (sub.wait) {
|
|
323
|
-
// Nested: the requester's turn holds while the sub-task runs; its findings come back here.
|
|
324
|
-
const row = await runTask(sub);
|
|
325
|
-
return { taskId: sub.id, takenBy: takenBy.roleId, status: row.status, ...(row.error ? { error: row.error } : {}), findings: (row.findings || []).map((f) => ({ kind: f.kind, text: f.text, refs: f.refs })), text: String(row.text || '').slice(0, 4000) };
|
|
326
|
-
}
|
|
327
|
-
const extended = extendDependents(tasks, parentTask.id, sub.id);
|
|
328
|
-
return { taskId: sub.id, takenBy: takenBy.roleId, queued: true, hint: `It runs after your task; ${extended.length ? `${extended.join(', ')} will read it too` : 'its findings will be on the board'}. Finish your task and say what depends on it.` };
|
|
329
|
-
};
|
|
330
|
-
/** A recruited agent joins the run as a role — the job's grants narrow it — and is appointed like any other. */
|
|
331
|
-
const addRole = (role, job) => {
|
|
332
|
-
const base = { ...role, id: String(role.id || job.id), grants: role.grants || job.needs?.grants || ['none'] };
|
|
333
|
-
const nt = normalizeTeam({ name: t.name, roles: [base], budget: t.budget });
|
|
334
|
-
const r = { ...nt.roles[0], ...(role.model ? { model: role.model } : {}), ...(role.engine ? { engine: role.engine } : {}), ...(role.skills ? { skills: role.skills } : {}), ...(role.workdir ? { workdir: role.workdir } : {}), recruited: true };
|
|
335
|
-
if (!t.roles.some((x) => x.id === r.id)) { t.roles.push(r); say('run.role-added', { role: { id: r.id, name: r.name, agent: r.agent || null, grants: r.grants, model: r.model || null, engine: r.engine || null }, jobId: job.id }); }
|
|
336
|
-
return r.id;
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
const runTask = async (task) => {
|
|
340
|
-
running.add(task.id);
|
|
341
|
-
try { return await runTaskInner(task); } finally { running.delete(task.id); }
|
|
342
|
-
};
|
|
343
|
-
const runTaskInner = async (task) => {
|
|
344
|
-
if (stopped() || overBudget || waitingOnPerson) { const row = { id: task.id, role: task.role, status: 'skipped', text: '', findings: [] }; tasksOut.push(row); return row; }
|
|
345
|
-
const role = roleOf(task.role);
|
|
346
|
-
const t0 = now();
|
|
347
|
-
const was = interrupted.get(task.id) || null;
|
|
348
|
-
say('task.started', { taskId: task.id, role: task.role, title: task.title, ...(task.parent ? { parent: task.parent } : {}), ...(was ? { resumed: true, steps: was.transcript.length } : {}) });
|
|
349
|
-
let text = '';
|
|
350
|
-
let usage = null;
|
|
351
|
-
let status = 'ok';
|
|
352
|
-
let error = null;
|
|
353
|
-
// THE TASK'S TRANSCRIPT — its conversation so far, apart from any one model. An attempt
|
|
354
|
-
// continues it; a hand-off continues it on another model; a resume continues it from
|
|
355
|
-
// the record. Only a task that has never been attempted starts from the bare prompt.
|
|
356
|
-
let transcript = was ? [...was.transcript] : [];
|
|
357
|
-
const attempts = was?.attempts ? [...was.attempts] : [];
|
|
358
|
-
let routed = null; // the last routing decision, on the task row
|
|
359
|
-
let scm = null; // what the last attempt did in a checkout
|
|
360
|
-
// The task's own abort: an ask nobody answered in time stops THIS member's turn (the
|
|
361
|
-
// run then checkpoints), without stopping the run's other members. A person's hand-off
|
|
362
|
-
// aborts it too, and names where the task continues.
|
|
363
|
-
let taskAc = new AbortController();
|
|
364
|
-
const onRunAbort = () => taskAc.abort();
|
|
365
|
-
signal?.addEventListener?.('abort', onRunAbort, { once: true });
|
|
366
|
-
let askedAndWaiting = null;
|
|
367
|
-
let handoffTo = control?._pendingFor?.(task.id) || null;
|
|
368
|
-
const unsubscribe = control?._subscribe?.((req) => {
|
|
369
|
-
if (req.type !== 'handoff' || req.taskId !== task.id) return false;
|
|
370
|
-
handoffTo = req; taskAc.abort(); return true;
|
|
371
|
-
}) || null;
|
|
372
|
-
// Every recorded step says when and under which attempt — the board's work log and the
|
|
373
|
-
// scorecard read the task by these, so a step reported after the fact is stamped now.
|
|
374
|
-
let attemptNo = 0;
|
|
375
|
-
const stamp = (m) => clipTranscriptOne({ ...m, at: Number.isFinite(m?.at) ? m.at : now(), attempt: Number.isFinite(m?.attempt) ? m.attempt : attemptNo });
|
|
376
|
-
const recordSteps = (before, after) => {
|
|
377
|
-
const added = newSteps(before, after);
|
|
378
|
-
if (added.length) say('task.step', { taskId: task.id, role: role.id, steps: added.map(stamp) });
|
|
379
|
-
};
|
|
380
|
-
// The tool-choice guard (§15.2): what this task's wording — and the planner's proposal —
|
|
381
|
-
// say it needs among what the role holds. A model attempt that ends with zero calls
|
|
382
|
-
// while holding one of these is nudged once, then may finish.
|
|
383
|
-
const grantsHeld = role?.grants || [];
|
|
384
|
-
// THE MERGE IS A TASK LIKE THE OTHERS (its row, thread, transcript and work log), with
|
|
385
|
-
// three differences: it reads the whole board, it answers in prose rather than findings,
|
|
386
|
-
// and it is never nudged — "verify a figure with a tool" is its instruction, not a need.
|
|
387
|
-
const isMerge = task.kind === 'merge';
|
|
388
|
-
const mustUse = role?.mode === 'model' && !isMerge ? [...new Set([...grantsNeededFor(task.prompt, { held: grantsHeld }), ...(task.grants || []).filter((g) => grantsHeld.includes(g) || (g.startsWith('mcp:') && grantsHeld.includes('mcp')))])] : [];
|
|
389
|
-
let nudged = false;
|
|
390
|
-
try {
|
|
391
|
-
if (!role) throw new Error(`no role "${task.role}" in the team`);
|
|
392
|
-
if (role.mode === 'recipe') {
|
|
393
|
-
if (typeof runRecipe !== 'function') throw new Error('this host cannot run recipes');
|
|
394
|
-
const r = await runRecipe(role.recipe, { request: String(request || ''), task: task.prompt });
|
|
395
|
-
text = typeof r === 'string' ? r : JSON.stringify(r);
|
|
396
|
-
} else {
|
|
397
|
-
// A call's tokens are unknown until it returns; what can be asked beforehand is
|
|
398
|
-
// whether the budget is already exhausted and whether one more call is allowed.
|
|
399
|
-
if (!budget.canAfford({ tokens: 0 })) { overBudget = true; throw new Error('over budget'); }
|
|
400
|
-
// What this member reads: the threads of the tasks it depends on, answered asks (its
|
|
401
|
-
// own — a resumed task finds the person's answer here), settled discussions.
|
|
402
|
-
const prior = boardText(board, { taskIds: !isMerge && task.dependsOn?.length ? task.dependsOn : null, role: role.id });
|
|
403
|
-
const prompt = [task.prompt, prior, isMerge ? '' : findingsInstruction()].filter(Boolean).join('\n\n');
|
|
404
|
-
// A host may build a toolset asynchronously (connecting MCP servers takes time).
|
|
405
|
-
// The board tool rides on top of whatever the role was granted.
|
|
406
|
-
const boardTool = boardToolProvider({
|
|
407
|
-
board, role: role.id, taskId: task.id, taskIds: !isMerge && task.dependsOn?.length ? task.dependsOn : null, askTimeoutMs: askMs, signal: taskAc.signal,
|
|
408
|
-
onAsk: (thread) => { say('task.waiting', { taskId: task.id, role: role.id, threadId: thread.id, text: thread.title }); },
|
|
409
|
-
onRequest: (req) => onRequest(task, role, req),
|
|
410
|
-
waitFor: askMs > 0 ? async (threadId, ms, sig) => {
|
|
411
|
-
const a = await box.wait(threadId, ms, sig);
|
|
412
|
-
if (!a && !stopped()) { askedAndWaiting = threadId; taskAc.abort(); }
|
|
413
|
-
return a;
|
|
414
|
-
} : null,
|
|
415
|
-
});
|
|
416
|
-
const tools = withBoardTool(withRunCache(await toolsFor(role), runCache, { role: role.id }), boardTool);
|
|
417
|
-
// A model that is not there — not deployed, no key, gone — is not the task failing:
|
|
418
|
-
// the next model on the roster is appointed and the task tried again, up to three
|
|
419
|
-
// models. Anything else (a refusal, a timeout, a bad request) fails the task.
|
|
420
|
-
// A resumed task does not walk back into the models that were unavailable the last
|
|
421
|
-
// time it ran — that is how one task read `A → B → A → B → A → B` on the board.
|
|
422
|
-
const exclude = new Set((was?.attempts || []).filter((a) => a && a.model && a.status === 'error' && isModelUnavailable(a.error)).map((a) => a.model));
|
|
423
|
-
let lastErr = '';
|
|
424
|
-
// What the next attempt is told, when it continues rather than starts.
|
|
425
|
-
let note = was ? continuationNote(was.status === 'waiting' ? { kind: 'answer', answer: 'see the board' } : { kind: 'resume', reason: was.error || was.status }) : null;
|
|
426
|
-
let lastModel = was?.attempts?.at?.(-1)?.model || null;
|
|
427
|
-
for (let attempt = 1; ; attempt++) {
|
|
428
|
-
let m;
|
|
429
|
-
const handoffNow = handoffTo;
|
|
430
|
-
if (handoffTo) {
|
|
431
|
-
// A person's hand-off names the model; the task continues there whatever the
|
|
432
|
-
// roster would have chosen. Said on the board, so everyone knows who has it.
|
|
433
|
-
m = { model: handoffTo.model, mode: role.mode };
|
|
434
|
-
const th = board.threadForTask(task.id);
|
|
435
|
-
if (th) board.post({ threadId: th.id, by: RUNNER, kind: 'decision', text: `Handed off from ${lastModel || 'the roster'} to ${handoffTo.model} by ${handoffTo.by}${handoffTo.reason ? ` — ${handoffTo.reason}` : ''}.` });
|
|
436
|
-
say('task.handoff', { taskId: task.id, role: role.id, from: lastModel, to: handoffTo.model, by: handoffTo.by, reason: handoffTo.reason || '' });
|
|
437
|
-
note = continuationNote({ kind: 'handoff', from: lastModel, to: handoffTo.model, reason: `handed off by ${handoffTo.by}` });
|
|
438
|
-
handoffTo = null;
|
|
439
|
-
taskAc = new AbortController(); signal?.addEventListener?.('abort', onRunAbort, { once: true });
|
|
440
|
-
} else if (nudged && lastModel && !exclude.has(lastModel)) {
|
|
441
|
-
m = { model: lastModel, mode: role.mode }; // the nudge continues on the same model
|
|
442
|
-
} else {
|
|
443
|
-
m = modelFor(role, excluding(exclude));
|
|
444
|
-
}
|
|
445
|
-
if (!m?.model) throw new Error(exclude.size ? `no model left for role "${role.id}" after ${[...exclude].join(', ')}` : `no model for role "${role.id}"`);
|
|
446
|
-
if (attempt > 1 && lastErr) say('task.reappointed', { taskId: task.id, role: role.id, model: m.model, after: [...exclude], error: lastErr });
|
|
447
|
-
// Who is doing this task, for a ledger that shows the lanes — said per attempt.
|
|
448
|
-
say('task.model', { taskId: task.id, role: role.id, model: m.model, label: m.label || null, attempt });
|
|
449
|
-
routed = routeOf(m, role, { attempt, exclude, handoff: handoffNow });
|
|
450
|
-
say('task.routed', { taskId: task.id, role: role.id, attempt, ...routed });
|
|
451
|
-
// The label beside the id: a work log that reads `mqk41ucyhmz1au → mqqzh4970js34c` names
|
|
452
|
-
// nothing to the person reading it.
|
|
453
|
-
attempts.push({ model: m.model, label: m.label || routed.engine?.label || routed.engine?.model || null, engine: routed.engine, at: now(), continued: !!note });
|
|
454
|
-
attemptNo = attempts.length;
|
|
455
|
-
const sent = messagesFor({ transcript }, { prompt, note });
|
|
456
|
-
// The record grows AS THE ATTEMPT GOES: a host that reports each wire message the
|
|
457
|
-
// moment it exists (a tool call, its result) puts it on the record then, so a
|
|
458
|
-
// process that dies mid-attempt leaves the work so far behind it, not nothing.
|
|
459
|
-
let live = sent;
|
|
460
|
-
// (thoughts are on the record but not on the wire, so compare against the wire's view)
|
|
461
|
-
const wireBefore = transcript.filter((m) => !isThought(m));
|
|
462
|
-
if (sent.length > wireBefore.length) recordSteps(wireBefore, sent);
|
|
463
|
-
const onStep = (message) => { if (message && message.role) { live = [...live, message]; say('task.step', { taskId: task.id, role: role.id, steps: [stamp(message)] }); } };
|
|
464
|
-
const res = await callModel({
|
|
465
|
-
runId: id, taskId: task.id, role: role.id, model: m.model, mode: m.mode || role.mode,
|
|
466
|
-
system: role.prompt, prompt, messages: sent, tools, signal: taskAc.signal,
|
|
467
|
-
onDelta: (delta, full) => say('task.delta', { taskId: task.id, role: role.id, delta, text: full }),
|
|
468
|
-
onStep,
|
|
469
|
-
});
|
|
470
|
-
usage = res?.usage || null;
|
|
471
|
-
if (usage) budget.charge(usage);
|
|
472
|
-
// What the attempt did in a checkout, when the host's harness reported one (§14).
|
|
473
|
-
if (normalizeScm(res?.scm)) { scm = normalizeScm(res.scm); say('task.scm', { taskId: task.id, role: role.id, ...scm }); }
|
|
474
|
-
// Whatever the attempt did is the task's now — on the record, before any verdict.
|
|
475
|
-
// What the host already reported step by step is not reported again.
|
|
476
|
-
transcript = mergeTranscript(sent, res);
|
|
477
|
-
if (transcript.length < live.length) transcript = live;
|
|
478
|
-
recordSteps(live, transcript);
|
|
479
|
-
lastModel = m.model;
|
|
480
|
-
attempts[attempts.length - 1].status = res?.ok ? (String(res?.text || '').trim() ? 'ok' : 'empty') : 'error';
|
|
481
|
-
if (askedAndWaiting) { status = 'waiting'; waitingOnPerson = true; break; }
|
|
482
|
-
if (handoffTo) { note = null; continue; } // the person moved it: continue the transcript there
|
|
483
|
-
if (res?.aborted || taskAc.signal.aborted) { status = 'stopped'; break; }
|
|
484
|
-
// A turn that ended with nothing to say — an agent that exited, a stream that
|
|
485
|
-
// died after its tool calls — is not a done task: three members "completed" empty
|
|
486
|
-
// once, the run merged nothing, and the caller ran the team again. It is treated
|
|
487
|
-
// like an unavailable model, so the next one on the roster gets the task.
|
|
488
|
-
if (res?.ok && String(res?.text || '').trim()) {
|
|
489
|
-
// Answered from memory while holding a tool the task calls for: one nudge, on
|
|
490
|
-
// the same model, continuing the transcript — then whatever it says stands.
|
|
491
|
-
const usedNone = !nudged && mustUse.length && routed?.engine?.kind !== 'harness' && !transcript.some((x) => x.role === 'assistant' && Array.isArray(x.tool_calls) && x.tool_calls.some((c) => c.function?.name && c.function.name !== 'board'));
|
|
492
|
-
if (usedNone && budget.canAfford({ tokens: 0 }) && !stopped()) {
|
|
493
|
-
nudged = true;
|
|
494
|
-
note = continuationNote({ kind: 'nudge', reason: mustUse.join(', ') });
|
|
495
|
-
const th = board.threadForTask(task.id);
|
|
496
|
-
if (th) board.post({ threadId: th.id, by: RUNNER, kind: 'note', text: `${role.id} answered without using ${mustUse.join(', ')}, which the task calls for — asked once to use it before finishing.` });
|
|
497
|
-
say('task.nudged', { taskId: task.id, role: role.id, grants: mustUse });
|
|
498
|
-
continue;
|
|
499
|
-
}
|
|
500
|
-
text = String(res.text); break;
|
|
501
|
-
}
|
|
502
|
-
// A member that wrote on the board and then ran out of turn has still answered:
|
|
503
|
-
// what it posted in its own thread during this attempt is its answer. A relayed
|
|
504
|
-
// agent that posted its assessment and kept searching past the cap was being
|
|
505
|
-
// failed for the searching.
|
|
506
|
-
if (res?.ok) {
|
|
507
|
-
const th = board.threadForTask(task.id);
|
|
508
|
-
const mine = th ? board.posts(th.id).filter((p) => p.by === role.id && p.at >= t0 && ['note', 'draft', 'finding'].includes(p.kind)) : [];
|
|
509
|
-
if (mine.length) { text = mine.map((p) => p.text).join('\n\n'); say('task.note', { taskId: task.id, role: role.id, text: 'answered from its board posts' }); break; }
|
|
510
|
-
}
|
|
511
|
-
const err = res?.ok ? 'the model returned no answer' : (res?.error || 'the model did not answer');
|
|
512
|
-
attempts[attempts.length - 1].error = err;
|
|
513
|
-
if (attempt >= MAX_APPOINTMENTS || stopped() || !isModelUnavailable(err)) throw new Error(err);
|
|
514
|
-
exclude.add(m.model); runExclude.add(m.model); lastErr = err;
|
|
515
|
-
// The next model CONTINUES this transcript; it does not start over.
|
|
516
|
-
note = continuationNote({ kind: 'handoff', from: m.model, reason: err });
|
|
517
|
-
const th = board.threadForTask(task.id);
|
|
518
|
-
if (th) board.post({ threadId: th.id, by: RUNNER, kind: 'note', text: `${m.model} stopped (${err.slice(0, 160)}); the task continues on the next model with its work so far.` });
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
} catch (e) {
|
|
522
|
-
if (askedAndWaiting) { status = 'waiting'; waitingOnPerson = true; }
|
|
523
|
-
else { status = overBudget ? 'over-budget' : 'failed'; error = String(e?.message || e); }
|
|
524
|
-
} finally {
|
|
525
|
-
unsubscribe?.();
|
|
526
|
-
}
|
|
527
|
-
// The merge's answer is the proposal, not a finding of its own (it would double every claim).
|
|
528
|
-
const findings = status === 'ok' && !isMerge ? parseFindings(text, { role: role?.id, taskId: task.id }) : [];
|
|
529
|
-
if (findings.length) { board.add(findings); for (const f of findings) say('task.finding', { taskId: task.id, role: role?.id, finding: f }); }
|
|
530
|
-
const thread = board.threadForTask(task.id);
|
|
531
|
-
// The thread says how the task ended. A failure is posted in it as well — a person reading
|
|
532
|
-
// the board sees "researcher failed: network error" where it happened, and what was tried.
|
|
533
|
-
if (thread && status === 'ok') board.setThreadStatus(thread.id, 'resolved');
|
|
534
|
-
else if (thread && status !== 'waiting') {
|
|
535
|
-
board.post({ threadId: thread.id, by: RUNNER, kind: 'note', text: `${role?.id || task.role} ${status === 'over-budget' ? 'stopped at the budget' : 'failed'}${error ? `: ${String(error).slice(0, 300)}` : ''}${attempts.length > 1 ? ` (after ${attempts.length} models: ${attempts.map((a) => a.label || a.model).join(', ')})` : ''}.` });
|
|
536
|
-
board.setThreadStatus(thread.id, 'failed');
|
|
537
|
-
}
|
|
538
|
-
const row = { id: task.id, role: task.role, title: task.title, status, text, error, usage, ms: now() - t0, findings, transcript: clipTranscript(transcript), attempts, ...(task.parent ? { parent: task.parent } : {}), ...(routed ? { routed } : {}), ...(scm ? { scm } : {}), ...(askedAndWaiting ? { waitingOn: askedAndWaiting } : {}) };
|
|
539
|
-
tasksOut.push(row);
|
|
540
|
-
say(status === 'ok' ? 'task.done' : 'task.failed', { taskId: task.id, role: task.role, status, error, ms: row.ms, findings: findings.length, ...(askedAndWaiting ? { threadId: askedAndWaiting } : {}) });
|
|
541
|
-
// The fact for the member's scorecard (scorecard.js): how big, with what, alongside whom,
|
|
542
|
-
// in which role — produced here, attested by the store, never written by the agent.
|
|
543
|
-
if (role && (status === 'ok' || status === 'failed')) {
|
|
544
|
-
say('task.scored', {
|
|
545
|
-
agentId: role.agent || role.id, taskId: task.id, role: role.id, model: lastModelOf(attempts), engine: routed?.engine || null, scm: scm || undefined, outcome: status === 'ok' ? 'task.done' : 'task.failed',
|
|
546
|
-
size: { ms: row.ms, steps: (row.transcript || []).length, tools: (row.transcript || []).filter((m) => m.role === 'tool').length, findings: findings.length, tokens: usage ? Number(usage.input_tokens || usage.prompt_tokens || 0) + Number(usage.output_tokens || usage.completion_tokens || 0) : 0 },
|
|
547
|
-
roleKind: isMerge ? 'orchestrator' : 'ic', tools: toolNamesOf(row.transcript), with: t.roles.filter((r) => r.id !== role.id).map((r) => r.agent || r.id),
|
|
548
|
-
refs: [`run:${id}`, ...(board.threadForTask(task.id) ? [`thread:${board.threadForTask(task.id).id}`] : [])], error: error || undefined,
|
|
549
|
-
...(task.parent ? { parent: task.parent, requestedBy: task.requestedBy || null } : {}),
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
// The spend so far, after every task — a ledger reads it live instead of at the end.
|
|
553
|
-
say('run.usage', { usage: budget.snapshot() });
|
|
554
|
-
if (budget.exhausted()) overBudget = true;
|
|
555
|
-
return row;
|
|
556
|
-
};
|
|
557
|
-
|
|
558
|
-
for (;;) {
|
|
559
|
-
if (stopped() || overBudget || waitingOnPerson) break;
|
|
560
|
-
const wave = ready();
|
|
561
|
-
if (!wave.length) break;
|
|
562
|
-
await pool(wave, maxConcurrency, runTask);
|
|
563
|
-
// Over budget with work left: ask the person ONCE for more, on the board, before stopping.
|
|
564
|
-
if (overBudget && !budgetAsked && !stopped()) {
|
|
565
|
-
budgetAsked = true;
|
|
566
|
-
const left = tasks.filter((x) => x.role && x.kind !== 'merge' && !tasksOut.some((y) => y.id === x.id)).length;
|
|
567
|
-
const spent = budget.snapshot().spent;
|
|
568
|
-
const what = left ? `${left} task${left === 1 ? '' : 's'} and the merge left` : 'only the merge left';
|
|
569
|
-
const a = await askPerson({ type: 'budget', text: `The team has used its budget (${Object.entries(spent).filter(([k]) => budget.cap[k] !== undefined).map(([k, v]) => `${k} ${v} of ${budget.cap[k]}`).join(', ')}) with ${what}. Raise it by half, or stop here with what it has?`, options: ['Raise by half', 'Stop here'] });
|
|
570
|
-
if (a && /raise|allow|yes|more|continue/i.test(a.text)) { budget.raise(1.5); overBudget = false; }
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
// Every planned task gets a row — what never ran is recorded as skipped, not forgotten;
|
|
574
|
-
// a sub-task nobody took is `unassigned`, which is its own kind of undone.
|
|
575
|
-
for (const task of tasks) if (task.kind !== 'merge' && !tasksOut.some((x) => x.id === task.id)) tasksOut.push({ id: task.id, role: task.role, title: task.title, status: task.parent && !task.role ? 'unassigned' : 'skipped', text: '', findings: [], ...(task.parent ? { parent: task.parent } : {}) });
|
|
576
|
-
if (stopped()) return finish('stopped');
|
|
577
|
-
if (waitingOnPerson) return finish('waiting', { proposal: null, budgetAsked });
|
|
578
|
-
// Over budget is a STOP only when it left work undone; a budget spent on the last task
|
|
579
|
-
// is a run that finished, and the merge below falls back to the cheap one if it must.
|
|
580
|
-
if (overBudget && tasksOut.some((x) => x.status === 'skipped' || x.status === 'over-budget')) return finish('over-budget', { proposal: mergeCheap(t, board.all(), tasksOut) });
|
|
581
|
-
|
|
582
|
-
// ── merge ─────────────────────────────────────────────────────────────────────────────
|
|
583
|
-
say('run.merging', { policy: t.merge });
|
|
584
|
-
const okTasks = tasksOut.filter((x) => x.status === 'ok');
|
|
585
|
-
let proposal;
|
|
586
|
-
if (!okTasks.length) return finish('failed', { proposal: null });
|
|
587
|
-
if (t.merge === 'judge') {
|
|
588
|
-
const judge = roleOf(t.judge) || strongestRole(t);
|
|
589
|
-
// The judge's task IS the merge (team-plan.js fixedPlan) — and it is a TASK: a row on the
|
|
590
|
-
// record, a thread, a transcript, a work log, a scorecard entry with real evidence. Before
|
|
591
|
-
// this it was a bare model call whose task.started/task.done named a task the record did
|
|
592
|
-
// not have, so the fold dropped them and the writer of the final answer showed nowhere.
|
|
593
|
-
// A resumed run that died mid-merge finds the task on its plan and continues it.
|
|
594
|
-
let mergeTask = tasks.find((x) => x.kind === 'merge');
|
|
595
|
-
if (!mergeTask) {
|
|
596
|
-
mergeTask = {
|
|
597
|
-
id: 'merge', kind: 'merge', role: judge.id, title: `merge (${judge.name || judge.id})`,
|
|
598
|
-
prompt: [
|
|
599
|
-
`You are the ${judge.name || judge.id} of team "${t.name}". The members' work is on the board below (and in the board tool). Write the team's FINAL ANSWER to the request: complete, well organised, only what the findings support, with the refs they came from. Say plainly what was not found or assumed. Flag anything the members disagreed on. Do not research from scratch — verify a figure with a tool only where the board is silent or contradictory.`,
|
|
600
|
-
`Request: ${String(request || '').trim()}`,
|
|
601
|
-
].join('\n\n'),
|
|
602
|
-
dependsOn: tasks.filter((x) => x.role && x.kind !== 'merge').map((x) => x.id),
|
|
603
|
-
};
|
|
604
|
-
tasks.push(mergeTask);
|
|
605
|
-
say('task.added', { taskId: mergeTask.id, kind: 'merge', role: mergeTask.role, title: mergeTask.title, dependsOn: mergeTask.dependsOn });
|
|
606
|
-
board.openThread({ taskId: mergeTask.id, kind: 'task', title: mergeTask.title, by: RUNNER, holder: judge.id });
|
|
607
|
-
}
|
|
608
|
-
const done = tasksOut.find((x) => x.id === mergeTask.id && x.status === 'ok');
|
|
609
|
-
const row = done || (modelFor(judge)?.model && budget.canAfford({ tokens: 0 }) ? await runTask(mergeTask) : null);
|
|
610
|
-
// The merge is a task: stopped or waiting on a person mid-way, the run is too — and it
|
|
611
|
-
// resumes from the merge's transcript, like any other.
|
|
612
|
-
if (stopped()) return finish('stopped');
|
|
613
|
-
if (waitingOnPerson) return finish('waiting', { proposal: null, budgetAsked });
|
|
614
|
-
proposal = row?.status === 'ok' && String(row.text || '').trim() ? { kind: 'answer', text: String(row.text), by: judge.id, taskId: mergeTask.id } : mergeCheap(t, board.all(), tasksOut);
|
|
615
|
-
} else if (t.merge === 'converge') {
|
|
616
|
-
const drafts = okTasks.map((x) => ({ claims: toBriefClaims(x.findings) }));
|
|
617
|
-
const { agreed, disputed } = converge(drafts, { minAgree: Math.min(2, drafts.length) });
|
|
618
|
-
proposal = { kind: 'claims', agreed, disputed, by: 'converge' };
|
|
619
|
-
} else if (t.merge === 'first') {
|
|
620
|
-
proposal = { kind: 'answer', text: okTasks[0].text, by: okTasks[0].role };
|
|
621
|
-
} else {
|
|
622
|
-
proposal = mergeCheap(t, board.all(), tasksOut);
|
|
623
|
-
}
|
|
624
|
-
// The merge is a proposal thread: a draft the person approves, rejects or replies to.
|
|
625
|
-
if (proposal?.text || proposal?.agreed) {
|
|
626
|
-
const th = board.openThread({ kind: 'proposal', title: `Proposal — ${t.merge}`, by: proposal.by || RUNNER });
|
|
627
|
-
board.post({ threadId: th.id, by: proposal.by || RUNNER, kind: 'draft', status: 'proposed', text: proposal.text || (proposal.agreed || []).map((c) => c.text).join('\n'), refs: [] });
|
|
628
|
-
}
|
|
629
|
-
const failed = tasksOut.some((x) => x.status !== 'ok');
|
|
630
|
-
return finish(failed ? 'partial' : 'completed', { proposal });
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Continue a run from its checkpoint — one that waited on a person, was stopped, failed, or
|
|
635
|
-
* whose process died. Finished tasks are carried; interrupted ones continue from their
|
|
636
|
-
* transcripts on whatever model the roster gives them now (or a hand-off names); the board
|
|
637
|
-
* and the spend carry over. Any client, any time later: the checkpoint is on the record.
|
|
638
|
-
*/
|
|
639
|
-
export function resumeTeam({ checkpoint, ...deps } = {}) {
|
|
640
|
-
if (!checkpoint?.plan?.tasks) throw new TeamRunError('BAD_RESUME', 'a checkpoint with a plan is required');
|
|
641
|
-
return runTeam({ ...deps, resume: checkpoint });
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const lastModelOf = (attempts) => (attempts || []).at(-1)?.model || null;
|
|
645
|
-
const describeNeeds = (n) => [n?.skills?.length ? `skills ${n.skills.join(', ')}` : '', n?.grants?.length ? `grants ${n.grants.join(', ')}` : '', n?.tools?.length ? `tools ${n.tools.join(', ')}` : ''].filter(Boolean).join('; ') || 'nothing in particular';
|
|
646
|
-
const toolNamesOf = (transcript) => [...new Set((transcript || []).flatMap((m) => (m.role === 'assistant' && Array.isArray(m.tool_calls) ? m.tool_calls.map((c) => c.function?.name).filter(Boolean) : [])))];
|
|
647
|
-
|
|
648
|
-
/** No model: the members' work side by side, findings first — always available. */
|
|
649
|
-
function mergeCheap(team, findings, tasks) {
|
|
650
|
-
const sections = tasks.filter((x) => x.status === 'ok').map((x) => {
|
|
651
|
-
const role = team.roles.find((r) => r.id === x.role);
|
|
652
|
-
const own = (x.findings || []).map((f) => `- ${f.text}${f.refs?.length ? ` (${f.refs.join(', ')})` : ''}`).join('\n');
|
|
653
|
-
return `### ${role?.name || x.role}\n${own || x.text}`;
|
|
654
|
-
});
|
|
655
|
-
return { kind: 'answer', text: sections.join('\n\n'), by: 'concat' };
|
|
656
|
-
}
|
|
13
|
+
`),by:"concat"}}export{on as RUN_STATUSES,He as TeamRunError,rn as dryRunTeam,Xe as isModelUnavailable,sn as resumeTeam,zt as runTeam};
|