@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-subtask.js
CHANGED
|
@@ -1,192 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// A member breaks its task down by posting a REQUEST in its thread ("writer: I need the
|
|
4
|
-
// valuation checked"); the runner turns each into a sub-task with its own thread, a `parent`
|
|
5
|
-
// and the parent's dependencies, so the plan is a TREE. Take-up is proactive: the sub-task
|
|
6
|
-
// is offered to the run's other members first — `takeUp` scores each on the skills and
|
|
7
|
-
// grants the request names (scorecard.js `fit`, the same score a job posting uses) and the
|
|
8
|
-
// best fit that meets the hard needs claims it. Nobody fits → the job board: `jobFromSubtask`
|
|
9
|
-
// is the posting the pool applies to (recruit.js), and the host's `recruit` hook answers with
|
|
10
|
-
// a role or nothing; nothing → an agent is PROPOSED to a person, never created (§7).
|
|
11
|
-
//
|
|
12
|
-
// Pure: no model call, no clock beyond what is handed in. The runner (team-run.js) owns the
|
|
13
|
-
// flow; this module owns the shapes and the decisions that can be tested without a run.
|
|
1
|
+
import{fit as b}from"./scorecard.js";import{GRANT_RE as k,normalizeGrants as S}from"./team.js";import{normalizeJob as $}from"./job.js";const j=8,O=2,q=.5,x=4e3,h=(e,t)=>String(e||"").trim().slice(0,t),p=e=>(Array.isArray(e)?e:typeof e=="string"?e.split(/[,\s]+/):[]).map(t=>String(t).trim().toLowerCase()).filter(Boolean),w=e=>[...new Set(e)];function T(e){const t=h(e?.title,120),o=h(e?.brief??e?.text,x);if(!t&&!o)return{ok:!1,error:"a request needs a title and a brief — what to do and what done looks like"};const s=w(p(e?.grants).filter(r=>k.test(r)&&r!=="none"));return{ok:!0,request:{title:t||h(o,80),brief:o||t,needs:{skills:w(p(e?.skills)).slice(0,12),tools:w(p(e?.tools)).slice(0,12),grants:s.slice(0,8)},wait:e?.wait!==!1&&e?.wait!=="false"}}}function R(e,t,{id:o,by:s,now:r=Date.now()}={}){const i=e;return{id:o,role:null,title:i.title,prompt:[`Sub-task requested by ${s} while working on "${h(t?.title||t?.id,120)}": ${i.title}`,i.brief,"Do this one thing and finish with your findings; whoever asked reads them on the board."].join(`
|
|
14
2
|
|
|
15
|
-
|
|
16
|
-
import { GRANT_RE, normalizeGrants } from './team.js';
|
|
17
|
-
import { normalizeJob } from './job.js';
|
|
18
|
-
|
|
19
|
-
export const MAX_SUBTASKS = 8; // per run — a member that needs more than this is re-planning, not delegating
|
|
20
|
-
export const MAX_DEPTH = 2; // a sub-task may request a sub-sub-task; not deeper
|
|
21
|
-
export const MIN_TAKEUP_FIT = 0.5;
|
|
22
|
-
export const MAX_BRIEF = 4000;
|
|
23
|
-
|
|
24
|
-
const clip = (s, n) => String(s || '').trim().slice(0, n);
|
|
25
|
-
const lower = (xs) => (Array.isArray(xs) ? xs : typeof xs === 'string' ? xs.split(/[,\s]+/) : []).map((x) => String(x).trim().toLowerCase()).filter(Boolean);
|
|
26
|
-
const uniq = (xs) => [...new Set(xs)];
|
|
27
|
-
|
|
28
|
-
/** The request as a member states it, checked: a title, a brief, and what it needs. */
|
|
29
|
-
export function normalizeRequest(input) {
|
|
30
|
-
const title = clip(input?.title, 120);
|
|
31
|
-
const brief = clip(input?.brief ?? input?.text, MAX_BRIEF);
|
|
32
|
-
if (!title && !brief) return { ok: false, error: 'a request needs a title and a brief — what to do and what done looks like' };
|
|
33
|
-
const grants = uniq(lower(input?.grants).filter((g) => GRANT_RE.test(g) && g !== 'none'));
|
|
34
|
-
return {
|
|
35
|
-
ok: true,
|
|
36
|
-
request: {
|
|
37
|
-
title: title || clip(brief, 80),
|
|
38
|
-
brief: brief || title,
|
|
39
|
-
needs: { skills: uniq(lower(input?.skills)).slice(0, 12), tools: uniq(lower(input?.tools)).slice(0, 12), grants: grants.slice(0, 8) },
|
|
40
|
-
wait: input?.wait !== false && input?.wait !== 'false',
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The sub-task a request becomes: a task row like any planned one (team-plan.js) plus
|
|
47
|
-
* `parent`, `depth`, `needs`, `requestedBy`; `role` is null until someone takes it. It
|
|
48
|
-
* inherits the parent's dependencies so it may read what the parent read.
|
|
49
|
-
*/
|
|
50
|
-
export function subtaskFromRequest(request, parent, { id, by, now = Date.now() } = {}) {
|
|
51
|
-
const r = request;
|
|
52
|
-
return {
|
|
53
|
-
id,
|
|
54
|
-
role: null,
|
|
55
|
-
title: r.title,
|
|
56
|
-
prompt: [
|
|
57
|
-
`Sub-task requested by ${by} while working on "${clip(parent?.title || parent?.id, 120)}": ${r.title}`,
|
|
58
|
-
r.brief,
|
|
59
|
-
'Do this one thing and finish with your findings; whoever asked reads them on the board.',
|
|
60
|
-
].join('\n\n'),
|
|
61
|
-
dependsOn: uniq([...(parent?.dependsOn || [])]).filter((d) => d !== id),
|
|
62
|
-
parent: parent?.id || null,
|
|
63
|
-
depth: (Number(parent?.depth) || 0) + 1,
|
|
64
|
-
needs: { skills: [...r.needs.skills], tools: [...r.needs.tools], grants: [...r.needs.grants] },
|
|
65
|
-
requestedBy: by,
|
|
66
|
-
requestedAt: now,
|
|
67
|
-
wait: !!r.wait,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Does this role hold every grant the request names? `mcp` covers `mcp:<server>`. */
|
|
72
|
-
export function holdsGrants(role, grants) {
|
|
73
|
-
const have = new Set(normalizeGrants(role?.grants || []));
|
|
74
|
-
if (have.has('none') && (grants || []).length) return false;
|
|
75
|
-
return (grants || []).every((g) => have.has(g) || (g.startsWith('mcp:') && have.has('mcp')));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const coversSkills = (role, skills) => {
|
|
79
|
-
if (!skills?.length) return true;
|
|
80
|
-
const has = new Set(lower(role?.skills));
|
|
81
|
-
return skills.some((s) => has.has(s));
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Offer a sub-task to the run's members: the best fit among those that hold every grant it
|
|
86
|
-
* needs and at least one skill it names (when it names any), at or above `minFit`. The
|
|
87
|
-
* requester is excluded — a request is a delegation — as are recipe roles (a recipe cannot
|
|
88
|
-
* take an arbitrary task) and anything in `exclude`. `summaries` are scorecard cards by
|
|
89
|
-
* agent or role id, when the host has them. Returns `{ roleId, fit, reasons }` or null with
|
|
90
|
-
* `why` on the side: `{ roleId: null, why }`.
|
|
91
|
-
*/
|
|
92
|
-
export function takeUp(task, roles, { exclude = [], summaries = {}, minFit = MIN_TAKEUP_FIT } = {}) {
|
|
93
|
-
const skip = new Set([task?.requestedBy, ...exclude].filter(Boolean));
|
|
94
|
-
const needs = task?.needs || { skills: [], tools: [], grants: [] };
|
|
95
|
-
const candidates = (roles || []).filter((r) => r && !skip.has(r.id) && r.mode !== 'recipe');
|
|
96
|
-
if (!candidates.length) return { roleId: null, why: 'no other member in the run' };
|
|
97
|
-
const scored = [];
|
|
98
|
-
const rejected = [];
|
|
99
|
-
for (const r of candidates) {
|
|
100
|
-
if (!holdsGrants(r, needs.grants)) { rejected.push(`${r.id} lacks ${needs.grants.filter((g) => !holdsGrants(r, [g])).join(', ')}`); continue; }
|
|
101
|
-
if (!coversSkills(r, needs.skills)) { rejected.push(`${r.id} has none of: ${needs.skills.join(', ')}`); continue; }
|
|
102
|
-
const f = fit({ needs, size: { steps: 0 } }, { skills: r.skills || [], tools: r.grants || [], grants: r.grants || [] }, summaries[r.agent || r.id] || null, { adjust: false });
|
|
103
|
-
scored.push({ roleId: r.id, fit: f.score, reasons: f.reasons });
|
|
104
|
-
}
|
|
105
|
-
scored.sort((a, b) => b.fit - a.fit);
|
|
106
|
-
const best = scored[0];
|
|
107
|
-
if (best && best.fit >= minFit) return best;
|
|
108
|
-
const why = best ? `${best.roleId} fits best at ${Math.round(best.fit * 100)}%, under the ${Math.round(minFit * 100)}% floor` : (rejected.length ? rejected.join('; ') : 'no member fits');
|
|
109
|
-
return { roleId: null, why };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The posting the pool applies to when nobody in the run fits (job.js). The run stands in
|
|
114
|
-
* for a project when there is none: `projectId` is the run's. The brief carries who asked
|
|
115
|
-
* and for what, so the evaluator and a person read the context.
|
|
116
|
-
*/
|
|
117
|
-
export function jobFromSubtask(task, { runId, projectId = null, budget = null, by = null } = {}) {
|
|
118
|
-
return normalizeJob({
|
|
119
|
-
id: task.id,
|
|
120
|
-
projectId: projectId || runId,
|
|
121
|
-
title: task.title,
|
|
122
|
-
brief: task.prompt,
|
|
123
|
-
needs: task.needs,
|
|
124
|
-
...(budget ? { budget } : {}),
|
|
125
|
-
status: 'open',
|
|
126
|
-
postedBy: by || task.requestedBy || 'runner',
|
|
127
|
-
postedAt: task.requestedAt || Date.now(),
|
|
128
|
-
dependsOn: [],
|
|
129
|
-
origin: { kind: 'subtask', runId, parent: task.parent || null, requestedBy: task.requestedBy || null },
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* A queued sub-task (the requester did not wait for it) must be read by whoever would have
|
|
135
|
-
* read the requester: every task that depends on the parent now depends on the sub-task too.
|
|
136
|
-
* Mutates the plan in place; returns the ids it extended.
|
|
137
|
-
*/
|
|
138
|
-
export function extendDependents(tasks, parentId, subtaskId) {
|
|
139
|
-
const out = [];
|
|
140
|
-
for (const t of tasks || []) {
|
|
141
|
-
if (!t || t.id === subtaskId) continue;
|
|
142
|
-
if ((t.dependsOn || []).includes(parentId) && !(t.dependsOn || []).includes(subtaskId)) { t.dependsOn = [...t.dependsOn, subtaskId]; out.push(t.id); }
|
|
143
|
-
}
|
|
144
|
-
return out;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** The plan as a tree — roots first, each with its children — for a board that draws it as one. */
|
|
148
|
-
export function taskTree(tasks) {
|
|
149
|
-
const list = (tasks || []).filter(Boolean);
|
|
150
|
-
const ids = new Set(list.map((t) => t.id));
|
|
151
|
-
const byParent = new Map();
|
|
152
|
-
for (const t of list) {
|
|
153
|
-
const key = t.parent && ids.has(t.parent) ? t.parent : null;
|
|
154
|
-
if (!byParent.has(key)) byParent.set(key, []);
|
|
155
|
-
byParent.get(key).push(t);
|
|
156
|
-
}
|
|
157
|
-
const build = (key, seen) => (byParent.get(key) || []).filter((t) => !seen.has(t.id)).map((t) => ({ task: t, children: build(t.id, new Set([...seen, t.id])) }));
|
|
158
|
-
return build(null, new Set());
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** One line a person reads: "researcher took: check valuation (fit 85%)". */
|
|
162
|
-
export function takeUpLine(task, pick) {
|
|
163
|
-
if (!pick?.roleId) return `nobody in the run fits "${task.title}"${pick?.why ? `: ${pick.why}` : ''}`;
|
|
164
|
-
return `${pick.roleId} took: ${task.title} (fit ${Math.round((pick.fit || 0) * 100)}%${pick.reasons?.length ? ` — ${pick.reasons[0]}` : ''})`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* A run's threads as the board lists them: a sub-task's thread (one whose `parent` names a
|
|
169
|
-
* task) follows its parent's thread, indented — `depth` on each row — so the tree reads as
|
|
170
|
-
* one. Roots keep the order given (a board sorts them newest first); children come in the
|
|
171
|
-
* order they were requested. A thread whose parent is not on the board is a root.
|
|
172
|
-
*/
|
|
173
|
-
export function threadRows(threads) {
|
|
174
|
-
const list = (threads || []).filter(Boolean);
|
|
175
|
-
const byTask = new Map(list.filter((t) => t.kind === 'task' && t.taskId).map((t) => [t.taskId, t]));
|
|
176
|
-
const children = new Map();
|
|
177
|
-
for (const t of list) {
|
|
178
|
-
if (!t.parent || !byTask.has(t.parent) || byTask.get(t.parent) === t) continue;
|
|
179
|
-
const key = byTask.get(t.parent).id;
|
|
180
|
-
if (!children.has(key)) children.set(key, []);
|
|
181
|
-
children.get(key).push(t);
|
|
182
|
-
}
|
|
183
|
-
const isChild = new Set([...children.values()].flat().map((t) => t.id));
|
|
184
|
-
const out = [];
|
|
185
|
-
const walk = (t, depth, seen) => {
|
|
186
|
-
if (seen.has(t.id)) return;
|
|
187
|
-
out.push({ ...t, depth });
|
|
188
|
-
for (const c of (children.get(t.id) || []).sort((a, b) => (a.at || 0) - (b.at || 0))) walk(c, depth + 1, new Set([...seen, t.id]));
|
|
189
|
-
};
|
|
190
|
-
for (const t of list) if (!isChild.has(t.id)) walk(t, 0, new Set());
|
|
191
|
-
return out;
|
|
192
|
-
}
|
|
3
|
+
`),dependsOn:w([...t?.dependsOn||[]]).filter(a=>a!==o),parent:t?.id||null,depth:(Number(t?.depth)||0)+1,needs:{skills:[...i.needs.skills],tools:[...i.needs.tools],grants:[...i.needs.grants]},requestedBy:s,requestedAt:r,wait:!!i.wait}}function g(e,t){const o=new Set(S(e?.grants||[]));return o.has("none")&&(t||[]).length?!1:(t||[]).every(s=>o.has(s)||s.startsWith("mcp:")&&o.has("mcp"))}const B=(e,t)=>{if(!t?.length)return!0;const o=new Set(p(e?.skills));return t.some(s=>o.has(s))};function _(e,t,{exclude:o=[],summaries:s={},minFit:r=q}={}){const i=new Set([e?.requestedBy,...o].filter(Boolean)),a=e?.needs||{skills:[],tools:[],grants:[]},n=(t||[]).filter(l=>l&&!i.has(l.id)&&l.mode!=="recipe");if(!n.length)return{roleId:null,why:"no other member in the run"};const d=[],u=[];for(const l of n){if(!g(l,a.grants)){u.push(`${l.id} lacks ${a.grants.filter(y=>!g(l,[y])).join(", ")}`);continue}if(!B(l,a.skills)){u.push(`${l.id} has none of: ${a.skills.join(", ")}`);continue}const c=b({needs:a,size:{steps:0}},{skills:l.skills||[],tools:l.grants||[],grants:l.grants||[]},s[l.agent||l.id]||null,{adjust:!1});d.push({roleId:l.id,fit:c.score,reasons:c.reasons})}d.sort((l,c)=>c.fit-l.fit);const f=d[0];return f&&f.fit>=r?f:{roleId:null,why:f?`${f.roleId} fits best at ${Math.round(f.fit*100)}%, under the ${Math.round(r*100)}% floor`:u.length?u.join("; "):"no member fits"}}function v(e,{runId:t,projectId:o=null,budget:s=null,by:r=null}={}){return $({id:e.id,projectId:o||t,title:e.title,brief:e.prompt,needs:e.needs,...s?{budget:s}:{},status:"open",postedBy:r||e.requestedBy||"runner",postedAt:e.requestedAt||Date.now(),dependsOn:[],origin:{kind:"subtask",runId:t,parent:e.parent||null,requestedBy:e.requestedBy||null}})}function D(e,t,o){const s=[];for(const r of e||[])!r||r.id===o||(r.dependsOn||[]).includes(t)&&!(r.dependsOn||[]).includes(o)&&(r.dependsOn=[...r.dependsOn,o],s.push(r.id));return s}function z(e){const t=(e||[]).filter(Boolean),o=new Set(t.map(i=>i.id)),s=new Map;for(const i of t){const a=i.parent&&o.has(i.parent)?i.parent:null;s.has(a)||s.set(a,[]),s.get(a).push(i)}const r=(i,a)=>(s.get(i)||[]).filter(n=>!a.has(n.id)).map(n=>({task:n,children:r(n.id,new Set([...a,n.id]))}));return r(null,new Set)}function E(e,t){return t?.roleId?`${t.roleId} took: ${e.title} (fit ${Math.round((t.fit||0)*100)}%${t.reasons?.length?` — ${t.reasons[0]}`:""})`:`nobody in the run fits "${e.title}"${t?.why?`: ${t.why}`:""}`}function U(e){const t=(e||[]).filter(Boolean),o=new Map(t.filter(n=>n.kind==="task"&&n.taskId).map(n=>[n.taskId,n])),s=new Map;for(const n of t){if(!n.parent||!o.has(n.parent)||o.get(n.parent)===n)continue;const d=o.get(n.parent).id;s.has(d)||s.set(d,[]),s.get(d).push(n)}const r=new Set([...s.values()].flat().map(n=>n.id)),i=[],a=(n,d,u)=>{if(!u.has(n.id)){i.push({...n,depth:d});for(const f of(s.get(n.id)||[]).sort((m,l)=>(m.at||0)-(l.at||0)))a(f,d+1,new Set([...u,n.id]))}};for(const n of t)r.has(n.id)||a(n,0,new Set);return i}export{x as MAX_BRIEF,O as MAX_DEPTH,j as MAX_SUBTASKS,q as MIN_TAKEUP_FIT,D as extendDependents,g as holdsGrants,v as jobFromSubtask,T as normalizeRequest,R as subtaskFromRequest,_ as takeUp,E as takeUpLine,z as taskTree,U as threadRows};
|
package/team-task.js
CHANGED
|
@@ -1,124 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// A member's task keeps its TRANSCRIPT — the wire messages so far: the request, what the
|
|
4
|
-
// model said, the tools it called and what came back — apart from whichever model or CLI
|
|
5
|
-
// agent happens to be answering. When that model fails, is handed off, or the process
|
|
6
|
-
// itself dies and the run is resumed a month later from the store, the next model does not
|
|
7
|
-
// start over: it is given the transcript and told to continue. The same thing a chat window
|
|
8
|
-
// does when a person switches models mid-conversation; here it is what makes a run survive
|
|
9
|
-
// a process, a model, or an API going away.
|
|
10
|
-
//
|
|
11
|
-
// Every attempt, hand-off and continuation is said on the board (a runner post in the
|
|
12
|
-
// task's thread) and in the run's events, so the record — on the gateway, read by either
|
|
13
|
-
// client — is enough to pick the task up from anywhere.
|
|
14
|
-
|
|
15
|
-
export const STEP_MAX_CHARS = 4000; // one persisted message's content
|
|
16
|
-
export const TASK_TRANSCRIPT_MAX_CHARS = 60_000; // a task's whole persisted transcript
|
|
17
|
-
|
|
18
|
-
const clipStr = (s, n) => (typeof s === 'string' && s.length > n ? `${s.slice(0, n)}\n…[${s.length - n} more chars omitted from the record]` : s);
|
|
19
|
-
|
|
20
|
-
/** A wire message trimmed for the record: tool results and long answers are clipped. */
|
|
21
|
-
export function clipMessage(m) {
|
|
22
|
-
if (!m || typeof m !== 'object') return m;
|
|
23
|
-
const out = { role: m.role };
|
|
24
|
-
if (typeof m.content === 'string') out.content = clipStr(m.content, STEP_MAX_CHARS);
|
|
25
|
-
else if (m.content != null) out.content = m.content;
|
|
26
|
-
if (m.tool_calls) out.tool_calls = m.tool_calls.map((c) => ({ id: c.id, type: c.type || 'function', function: { name: c.function?.name, arguments: clipStr(String(c.function?.arguments ?? ''), STEP_MAX_CHARS) } }));
|
|
27
|
-
if (m.tool_call_id) out.tool_call_id = m.tool_call_id;
|
|
28
|
-
if (m.name) out.name = m.name;
|
|
29
|
-
// The member's reasoning, when the model streams it: on the record for the board and the
|
|
30
|
-
// scorecard, never sent back on the wire (messagesFor drops it).
|
|
31
|
-
if (typeof m.thought === 'string') out.thought = clipStr(m.thought, STEP_MAX_CHARS);
|
|
32
|
-
// When the step happened and under which attempt — the work log orders by these.
|
|
33
|
-
if (Number.isFinite(m.at)) out.at = m.at;
|
|
34
|
-
if (Number.isFinite(m.attempt)) out.attempt = m.attempt;
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** A step that is the member thinking aloud — no content, no call — recorded, not replayed. */
|
|
39
|
-
export function isThought(m) {
|
|
40
|
-
return !!m && m.role === 'assistant' && typeof m.thought === 'string' && m.content == null && !(Array.isArray(m.tool_calls) && m.tool_calls.length);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** The record's copy of a transcript: clipped per message and bounded as a whole (oldest tool traffic goes first). */
|
|
44
|
-
export function clipTranscript(messages) {
|
|
45
|
-
const list = (Array.isArray(messages) ? messages : []).filter((m) => m && m.role && m.role !== 'system').map(clipMessage);
|
|
46
|
-
let size = list.reduce((n, m) => n + JSON.stringify(m).length, 0);
|
|
47
|
-
// Drop the oldest tool exchanges (assistant tool_calls + tool results) until it fits,
|
|
48
|
-
// never the first user message (the task) and never the last message.
|
|
49
|
-
for (let i = 1; size > TASK_TRANSCRIPT_MAX_CHARS && i < list.length - 1; ) {
|
|
50
|
-
const m = list[i];
|
|
51
|
-
if (m.role === 'tool' || (m.role === 'assistant' && m.tool_calls)) { size -= JSON.stringify(m).length; list.splice(i, 1); } else i += 1;
|
|
52
|
-
}
|
|
53
|
-
return list;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** The messages beyond what the record already holds — what one attempt added. */
|
|
57
|
-
export function newSteps(prev, next) {
|
|
58
|
-
const a = Array.isArray(prev) ? prev.length : 0;
|
|
59
|
-
return (Array.isArray(next) ? next : []).slice(a);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* What the next model is told when it continues another's work. `kind`:
|
|
64
|
-
* handoff — the previous attempt (another model / agent) stopped: an error, a person's choice
|
|
65
|
-
* resume — the run itself was stopped or died and is being resumed from the record
|
|
66
|
-
* answer — the task waited on a person and the answer is now on the board
|
|
67
|
-
* nudge — the attempt answered without touching a tool the task was said to need (§15.2)
|
|
68
|
-
*/
|
|
69
|
-
export function continuationNote({ kind = 'handoff', from = '', to = '', reason = '', answer = '' } = {}) {
|
|
70
|
-
const who = from ? ` by ${from}` : '';
|
|
71
|
-
if (kind === 'nudge') return `You answered without using ${reason || 'the tools you were given'}, which this task calls for. Use them now — look it up rather than answer from memory — then finish with your findings. If a tool is truly not needed, say why in one line and finish.`;
|
|
72
|
-
if (kind === 'answer') return `The user has answered your question on the board (see the board, or: ${answer}). Continue the task from where you left off — do not repeat work already done above — and finish with your findings.`;
|
|
73
|
-
if (kind === 'resume') return `This task was interrupted (${reason || 'the run was stopped'}) and is being resumed${to ? ` by ${to}` : ''}. Everything above is the work done so far — read it, do not redo it. Continue from where it stopped and finish with your findings.`;
|
|
74
|
-
return `You are continuing this task. A previous attempt${who} stopped (${reason || 'it did not finish'}). Everything above is its work so far — read it, do not redo lookups already made. Continue from where it stopped and finish with your findings.`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* The messages to send for an attempt: the task's transcript plus a continuation note when
|
|
79
|
-
* there is one; the bare task when there is not. `system` travels separately (the host puts
|
|
80
|
-
* it first) so a transcript never carries a role prompt that a later role might not share.
|
|
81
|
-
*/
|
|
82
|
-
export function messagesFor(task, { prompt, note = null } = {}) {
|
|
83
|
-
const transcript = (Array.isArray(task?.transcript) ? task.transcript : []).filter((m) => !isThought(m));
|
|
84
|
-
if (!transcript.length) return [{ role: 'user', content: String(prompt || '') }];
|
|
85
|
-
const last = transcript[transcript.length - 1];
|
|
86
|
-
// A transcript that ends in an unanswered tool call cannot be continued as-is: close it.
|
|
87
|
-
const closed = last?.role === 'assistant' && Array.isArray(last.tool_calls) && last.tool_calls.length
|
|
88
|
-
? [...transcript, ...last.tool_calls.map((c) => ({ role: 'tool', tool_call_id: c.id, content: 'This call was not answered: the attempt stopped here.' }))]
|
|
89
|
-
: transcript;
|
|
90
|
-
return note ? [...closed, { role: 'user', content: note }] : closed;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Fold what a host returned into the task's transcript. A host that hands back its wire
|
|
95
|
-
* transcript (`res.transcript`) is taken as is (minus system); one that only returns text
|
|
96
|
-
* gets the messages it was sent plus one assistant message — still a continuation.
|
|
97
|
-
*/
|
|
98
|
-
export function mergeTranscript(sent, res) {
|
|
99
|
-
const returned = Array.isArray(res?.transcript) ? res.transcript.filter((m) => m && m.role !== 'system') : null;
|
|
100
|
-
if (returned && returned.length >= sent.length) return returned;
|
|
101
|
-
const text = String(res?.text || '');
|
|
102
|
-
return text ? [...sent, { role: 'assistant', content: text }] : sent;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* The control channel a host holds on a running team: a person's hand-off of a task to a
|
|
107
|
-
* named model (from the board, either client), or a stop of one task. The runner subscribes.
|
|
108
|
-
*/
|
|
109
|
-
export function createControl() {
|
|
110
|
-
const listeners = new Set();
|
|
111
|
-
const pending = new Map(); // taskId -> { model, by } asked before the task subscribed
|
|
112
|
-
const api = {
|
|
113
|
-
handoff(taskId, model, by = 'person', reason = '') {
|
|
114
|
-
const req = { type: 'handoff', taskId, model, by, reason };
|
|
115
|
-
let taken = false;
|
|
116
|
-
for (const fn of listeners) { if (fn(req) === true) taken = true; }
|
|
117
|
-
if (!taken) pending.set(taskId, req);
|
|
118
|
-
return taken;
|
|
119
|
-
},
|
|
120
|
-
_subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
|
|
121
|
-
_pendingFor(taskId) { const r = pending.get(taskId); if (r) pending.delete(taskId); return r || null; },
|
|
122
|
-
};
|
|
123
|
-
return api;
|
|
124
|
-
}
|
|
1
|
+
const p=4e3,d=6e4,a=(t,e)=>typeof t=="string"&&t.length>e?`${t.slice(0,e)}
|
|
2
|
+
…[${t.length-e} more chars omitted from the record]`:t;function f(t){if(!t||typeof t!="object")return t;const e={role:t.role};return typeof t.content=="string"?e.content=a(t.content,4e3):t.content!=null&&(e.content=t.content),t.tool_calls&&(e.tool_calls=t.tool_calls.map(r=>({id:r.id,type:r.type||"function",function:{name:r.function?.name,arguments:a(String(r.function?.arguments??""),4e3)}}))),t.tool_call_id&&(e.tool_call_id=t.tool_call_id),t.name&&(e.name=t.name),typeof t.thought=="string"&&(e.thought=a(t.thought,4e3)),Number.isFinite(t.at)&&(e.at=t.at),Number.isFinite(t.attempt)&&(e.attempt=t.attempt),e}function h(t){return!!t&&t.role==="assistant"&&typeof t.thought=="string"&&t.content==null&&!(Array.isArray(t.tool_calls)&&t.tool_calls.length)}function g(t){const e=(Array.isArray(t)?t:[]).filter(n=>n&&n.role&&n.role!=="system").map(f);let r=e.reduce((n,o)=>n+JSON.stringify(o).length,0);for(let n=1;r>6e4&&n<e.length-1;){const o=e[n];o.role==="tool"||o.role==="assistant"&&o.tool_calls?(r-=JSON.stringify(o).length,e.splice(n,1)):n+=1}return e}function y(t,e){const r=Array.isArray(t)?t.length:0;return(Array.isArray(e)?e:[]).slice(r)}function _({kind:t="handoff",from:e="",to:r="",reason:n="",answer:o=""}={}){const i=e?` by ${e}`:"";return t==="nudge"?`You answered without using ${n||"the tools you were given"}, which this task calls for. Use them now — look it up rather than answer from memory — then finish with your findings. If a tool is truly not needed, say why in one line and finish.`:t==="answer"?`The user has answered your question on the board (see the board, or: ${o}). Continue the task from where you left off — do not repeat work already done above — and finish with your findings.`:t==="resume"?`This task was interrupted (${n||"the run was stopped"}) and is being resumed${r?` by ${r}`:""}. Everything above is the work done so far — read it, do not redo it. Continue from where it stopped and finish with your findings.`:`You are continuing this task. A previous attempt${i} stopped (${n||"it did not finish"}). Everything above is its work so far — read it, do not redo lookups already made. Continue from where it stopped and finish with your findings.`}function A(t,{prompt:e,note:r=null}={}){const n=(Array.isArray(t?.transcript)?t.transcript:[]).filter(s=>!h(s));if(!n.length)return[{role:"user",content:String(e||"")}];const o=n[n.length-1],i=o?.role==="assistant"&&Array.isArray(o.tool_calls)&&o.tool_calls.length?[...n,...o.tool_calls.map(s=>({role:"tool",tool_call_id:s.id,content:"This call was not answered: the attempt stopped here."}))]:n;return r?[...i,{role:"user",content:r}]:i}function w(t,e){const r=Array.isArray(e?.transcript)?e.transcript.filter(o=>o&&o.role!=="system"):null;if(r&&r.length>=t.length)return r;const n=String(e?.text||"");return n?[...t,{role:"assistant",content:n}]:t}function S(){const t=new Set,e=new Map;return{handoff(n,o,i="person",s=""){const u={type:"handoff",taskId:n,model:o,by:i,reason:s};let l=!1;for(const c of t)c(u)===!0&&(l=!0);return l||e.set(n,u),l},_subscribe(n){return t.add(n),()=>t.delete(n)},_pendingFor(n){const o=e.get(n);return o&&e.delete(n),o||null}}}export{p as STEP_MAX_CHARS,d as TASK_TRANSCRIPT_MAX_CHARS,f as clipMessage,g as clipTranscript,_ as continuationNote,S as createControl,h as isThought,w as mergeTranscript,A as messagesFor,y as newSteps};
|
package/team-tool.js
CHANGED
|
@@ -1,196 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// follow-ups, done-when, every step on the project record a person reads on the board.
|
|
4
|
-
//
|
|
5
|
-
// The recipe tool's twin, on purpose: one registered tool whose description is the
|
|
6
|
-
// catalogue, `dry_run` showing what a person would approve, `save` proposing a team from the
|
|
7
|
-
// conversation and storing it only on the card's Allow, `run` executing through the host's
|
|
8
|
-
// own runner. A team run is a tool call inside an ordinary turn, so it streams, can be
|
|
9
|
-
// stopped, and is queued and steered like anything else the model does.
|
|
10
|
-
//
|
|
11
|
-
// Everything the model sees is here; the card, the store and the runner's `callModel` are
|
|
12
|
-
// the host's, injected. Bound late (`bind`) to the toolset it lives in: a team's members
|
|
13
|
-
// receive the host's toolset narrowed to their grants, and the host builds that from the
|
|
14
|
-
// same providers this tool is a member of.
|
|
15
|
-
|
|
16
|
-
import { validateTeam, normalizeTeam, describeRole, slugTeamName } from './team.js';
|
|
17
|
-
import { dryRunTeam } from './team-run.js';
|
|
18
|
-
|
|
19
|
-
export const TEAM_TOOL_NAME = 'team';
|
|
20
|
-
|
|
21
|
-
// The teams the model may see and run: enabled, named, and whole. A record another client
|
|
22
|
-
// half-wrote (a name and nothing else) is not a team — Settings shows it for deleting.
|
|
23
|
-
const usable = (teams) => (teams || []).filter((t) => t?.name && t.enabled !== false && validateTeam(t).ok);
|
|
24
|
-
|
|
25
|
-
function catalogue(teams) {
|
|
26
|
-
const list = usable(teams);
|
|
27
|
-
if (!list.length) return 'No teams saved yet.';
|
|
28
|
-
return `Saved teams: ${list.map((t) => `${t.name} (${(t.roles || []).map((r) => r.id).join(', ')})${t.description ? ` — ${t.description}` : ''}`).join('; ')}.`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* How long one call of the tool may take: the longest budget among the teams plus the merge,
|
|
33
|
-
* never under two minutes. A relay between a CLI agent and this tool (the bridge's MCP
|
|
34
|
-
* server) times a call by this; without it a 300 s team hit the relay's 120 s default,
|
|
35
|
-
* Claude Code was told "tool call timed out" and ran the team AGAIN while the first run was
|
|
36
|
-
* still working.
|
|
37
|
-
*/
|
|
38
|
-
export function teamToolTimeoutMs(teams) {
|
|
39
|
-
const longest = Math.max(0, ...(teams || []).map((t) => Number(t?.budget?.ms) || 0));
|
|
40
|
-
return Math.max(120_000, (longest || 10 * 60_000) + 60_000);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function teamToolSpec(teams) {
|
|
44
|
-
return {
|
|
45
|
-
name: TEAM_TOOL_NAME,
|
|
46
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
47
|
-
timeoutMs: teamToolTimeoutMs(teams),
|
|
48
|
-
description:
|
|
49
|
-
`Saved agent teams — several roles working a request in parallel, merged into one answer. ${catalogue(teams)} `
|
|
50
|
-
+ 'Actions: {"action":"run","name":"<team>","request":"<what to do>"} runs one (streams; may take a while); '
|
|
51
|
-
+ '{"action":"dry_run","name":"<team>","request":"…"} shows roles, models, tools and budget without running; '
|
|
52
|
-
+ '{"action":"save","team":{…}} proposes a NEW team after a task that would benefit from several roles — the user approves it on a card. '
|
|
53
|
-
+ '{"action":"project","goal":"<what done looks like>","title":"<short name>","doneWhen":"<a check the result can be held to>","budget":{"tokens":200000,"ms":3600000}} hands a GOAL to the executive: it posts jobs, recruits agents from the pool for each, runs them as teams in rounds, reviews the results against done-when, posts follow-ups, asks the user before spending or changing scope, and closes when done-when holds (may take a long while; use it for a goal with several parts, not a question). '
|
|
54
|
-
+ 'A team: {"name":"research" (a short identifier: letters, digits, - _; used as /research),"description":"…","roles":[{"id":"researcher","prompt":"…","prefer":"balanced","grants":["data","web"]},{"id":"writer","prompt":"…","prefer":"strong","grants":["none"]}],"merge":"judge","judge":"writer","budget":{"tokens":40000,"ms":300000}}. '
|
|
55
|
-
+ 'grants: none | data | web | history | mcp | mcp:<server> | shell | fs:write | scm:read | scm:push | scm:pr. A role may say "agent":"<id>" instead of a prompt to stand for an agent from the pool. merge: judge | converge | concat | first. A budget is required. '
|
|
56
|
-
+ 'Order the work with "dependsOn": a role that builds on another\'s findings (a budget checker on a researcher) lists it, so it runs after and reads the board instead of searching again. The judge does not need a task of its own - the merge is its work.',
|
|
57
|
-
parameters: {
|
|
58
|
-
type: 'object',
|
|
59
|
-
properties: {
|
|
60
|
-
action: { type: 'string', enum: ['run', 'dry_run', 'save', 'project'] },
|
|
61
|
-
goal: { type: 'string', description: 'For project: the goal, in the user\'s words — what done looks like.' },
|
|
62
|
-
title: { type: 'string', description: 'For project: a short name.' },
|
|
63
|
-
doneWhen: { type: 'string', description: 'For project: the check the result is held to.' },
|
|
64
|
-
budget: { type: 'object', description: 'For project: {"tokens","ms","usd"} for the whole project.', additionalProperties: true },
|
|
65
|
-
name: { type: 'string', description: 'Team name, for run / dry_run.' },
|
|
66
|
-
request: { type: 'string', description: 'What the team should do, for run / dry_run.' },
|
|
67
|
-
team: { type: 'object', description: 'The team to save, for save.', additionalProperties: true },
|
|
68
|
-
},
|
|
69
|
-
required: ['action'],
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The card a person approves a new team on. */
|
|
75
|
-
export function describeTeamForApproval(team, dry) {
|
|
76
|
-
const lines = [`${team.name}${team.description ? ` — ${team.description}` : ''}`, `Plan: ${team.plan || 'fixed'} · merge: ${team.merge || 'concat'}${team.judge ? ` (judge: ${team.judge})` : ''}`];
|
|
77
|
-
for (const r of dry?.roles || team.roles || []) lines.push(`• ${describeRole({ ...r, model: r.label || r.model || undefined })}${r.appointed === false ? ' — NO MODEL AVAILABLE' : ''}`);
|
|
78
|
-
const b = team.budget || {};
|
|
79
|
-
lines.push(`Budget: ${Object.entries(b).map(([k, v]) => `${k} ${v}`).join(' · ')}`);
|
|
80
|
-
lines.push('Runs go through your own models and tools; a team may not act on a page. Nothing a team produces lands without you.');
|
|
81
|
-
return lines.join('\n');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const json = (v) => JSON.stringify(v);
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* @param teams the saved list (the `teams` prefs section)
|
|
88
|
-
* @param run `async ({ team, request, onEvent }) => runResult` — the host's runTeam binding
|
|
89
|
-
* @param appoint `(role) => { model, mode } | null` for the dry run
|
|
90
|
-
* @param confirmSave `async (detail, team) => 'allow' | 'deny'`; absent = save refused
|
|
91
|
-
* @param saveTeam `async (team) => void`
|
|
92
|
-
* @param runProject `async ({ goal, title, doneWhen, budget, toolset }) => project result` — the
|
|
93
|
-
* host's executive loop (project-run.js runProject with its own deps);
|
|
94
|
-
* absent, the action is refused
|
|
95
|
-
*/
|
|
96
|
-
/**
|
|
97
|
-
* `resolve` is the host's `(team) => team` that fills roles standing for agents from the
|
|
98
|
-
* pool (agent.js resolveTeam) — applied before a dry run and before a run, never to what is
|
|
99
|
-
* saved: the stored team keeps its references, the run gets the cards as they are now.
|
|
100
|
-
*/
|
|
101
|
-
export function teamToolProvider({ teams = [], run = null, appoint = null, confirmSave = null, saveTeam = null, resolve = null, runProject = null } = {}) {
|
|
102
|
-
const byName = new Map(usable(teams).map((t) => [t.name, t]));
|
|
103
|
-
// A team whose roles stand for agents is filled from the pool on the way to a run; a
|
|
104
|
-
// resolver that throws (an agent missing from the pool) is the tool's error, not a crash.
|
|
105
|
-
const resolved = (t) => (typeof resolve === 'function' ? resolve(t) : t);
|
|
106
|
-
let bound = null;
|
|
107
|
-
// One run per team+request per turn. A run that failed, answered with nothing, or ran
|
|
108
|
-
// out of budget comes back as a result the model must REPORT — asking for it again in the
|
|
109
|
-
// same turn is the circle a person had to break by hand.
|
|
110
|
-
const ran = new Map();
|
|
111
|
-
return {
|
|
112
|
-
id: 'team',
|
|
113
|
-
specs: [teamToolSpec(teams)],
|
|
114
|
-
system: [
|
|
115
|
-
byName.size ? 'Saved agent teams exist (see the `team` tool). When a request is broad enough that several roles would do it better — research plus writing, several sources to reconcile — run the matching team rather than doing it all in one turn.' : '',
|
|
116
|
-
runProject ? 'A GOAL with several parts (a project, not a question) goes to the executive: call the `team` tool with {"action":"project","goal":…,"doneWhen":…} and report what it produced.' : '',
|
|
117
|
-
(confirmSave && saveTeam) ? 'When the user asks to create, make, set up or save a team (of agents / roles), do not run one: call the `team` tool with {"action":"save","team":{…}} — pick roles, prompts, grants and a budget from what they said, and ask only for what you cannot infer. The user approves it on a card.' : '',
|
|
118
|
-
].filter(Boolean).join(' '),
|
|
119
|
-
bind(toolset) { bound = toolset; },
|
|
120
|
-
async execute(name, input) {
|
|
121
|
-
if (name !== TEAM_TOOL_NAME) return json({ error: `Unknown tool: ${name}` });
|
|
122
|
-
const action = String(input?.action || '');
|
|
123
|
-
|
|
124
|
-
if (action === 'save') {
|
|
125
|
-
// A name in prose ("Research Team") becomes the /command it will be run by.
|
|
126
|
-
const team = input?.team && typeof input.team === 'object' ? { ...input.team, name: slugTeamName(input.team.name) } : input?.team;
|
|
127
|
-
const v = validateTeam(team);
|
|
128
|
-
if (!v.ok) return json({ error: 'The team is not valid.', problems: v.errors, hint: 'A team needs a name (letters, digits, - _), roles with prompts and grants, and a budget such as {"tokens":40000,"ms":300000}.' });
|
|
129
|
-
if (byName.has(team.name)) return json({ error: `A team named "${team.name}" already exists. Pick another name.` });
|
|
130
|
-
if (!confirmSave || !saveTeam) return json({ error: 'Saving a team needs the user\'s approval, which this surface cannot ask for. Describe the team and suggest saving it from the side panel or the desktop.' });
|
|
131
|
-
const norm = normalizeTeam(team);
|
|
132
|
-
const dry = dryRunTeam(resolved(norm), '', { appoint });
|
|
133
|
-
const decision = await confirmSave(describeTeamForApproval(norm, dry), norm);
|
|
134
|
-
if (decision !== 'allow') return json({ error: `The user did not save "${norm.name}". Do not propose it again this turn.`, declined: true });
|
|
135
|
-
const stored = { ...norm, createdAt: Date.now() };
|
|
136
|
-
await saveTeam(stored);
|
|
137
|
-
byName.set(stored.name, stored);
|
|
138
|
-
return json({ saved: stored.name, roles: stored.roles.map((r) => r.id), hint: `Run it with {"action":"run","name":"${stored.name}","request":"…"} or by typing /${stored.name}.` });
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (action === 'project') {
|
|
142
|
-
const goal = String(input?.goal || '').trim();
|
|
143
|
-
if (!goal) return json({ error: 'project needs a goal — what does done look like?' });
|
|
144
|
-
if (typeof runProject !== 'function') return json({ error: 'This surface cannot run a project. Run a team instead, or describe the jobs.' });
|
|
145
|
-
// One project per turn, like one run per team: a loop that asks the person and gets
|
|
146
|
-
// "stop" is not started again with a rephrased goal.
|
|
147
|
-
if (ran.has('project')) { const p = ran.get('project'); return json({ error: `A project already ran in this turn (${p.projectId}, ${p.status}). Report its result and ask the user how to proceed.`, ...p }); }
|
|
148
|
-
let result;
|
|
149
|
-
try {
|
|
150
|
-
result = await runProject({ goal, title: String(input?.title || '').trim(), doneWhen: String(input?.doneWhen || '').trim(), budget: input?.budget && typeof input.budget === 'object' ? input.budget : null, toolset: bound });
|
|
151
|
-
} catch (e) { return json({ error: e?.message || String(e) }); }
|
|
152
|
-
const out = { projectId: result.projectId, status: result.status, rounds: result.rounds, jobs: result.jobs, runs: result.runs, report: result.report, spend: result.spend, why: result.why || undefined };
|
|
153
|
-
ran.set('project', out);
|
|
154
|
-
const hint = result.status === 'done' ? 'Done-when holds and the project is closed. Present the report as the answer; the jobs and their runs are on the board.'
|
|
155
|
-
: result.status === 'over-budget' ? 'The project stopped at its budget; the report so far stands. Say so and ask whether to raise it.'
|
|
156
|
-
: result.status === 'failed' ? `The project FAILED — ${result.why || 'see the jobs'}. Do not start it again this turn; tell the user which job failed and why.`
|
|
157
|
-
: `The project is ${result.status}${result.why ? ` — ${result.why}` : ''}. Present the report so far and say what is left; do not start it again this turn.`;
|
|
158
|
-
return json({ ...out, hint });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const team = byName.get(String(input?.name || ''));
|
|
162
|
-
if (!team) return json({ error: `No team named "${input?.name}".`, available: [...byName.keys()] });
|
|
163
|
-
const request = String(input?.request || '').trim();
|
|
164
|
-
|
|
165
|
-
if (action === 'dry_run') {
|
|
166
|
-
let dry;
|
|
167
|
-
try { dry = dryRunTeam(resolved(team), request, { appoint }); } catch (e) { return json({ error: e?.message || String(e) }); }
|
|
168
|
-
return json({ name: team.name, ok: dry.ok, missing: dry.missing, roles: dry.roles, plan: dry.plan, tasks: dry.tasks, merge: dry.merge, budget: dry.budget });
|
|
169
|
-
}
|
|
170
|
-
if (action === 'run') {
|
|
171
|
-
if (!request) return json({ error: 'run needs a request — what should the team do?' });
|
|
172
|
-
if (typeof run !== 'function') return json({ error: 'This surface cannot run a team.' });
|
|
173
|
-
// ONE run per team per turn — keyed by the team, not the request: a model that
|
|
174
|
-
// re-runs after a partial result rephrases the request each time, and that was a
|
|
175
|
-
// second circle. Its partial result stands; the person decides what happens next.
|
|
176
|
-
const key = team.name;
|
|
177
|
-
const prior = ran.get(key);
|
|
178
|
-
if (prior) return json({ error: `The "${team.name}" team already ran in this turn (run ${prior.runId}, ${prior.status}). Do not run it again, even with a different request: report what it produced — ${prior.summary} — with its proposal, and ask the user how to proceed.`, runId: prior.runId, status: prior.status, tasks: prior.tasks, proposal: prior.proposal });
|
|
179
|
-
let dry;
|
|
180
|
-
try { dry = dryRunTeam(resolved(team), request, { appoint }); } catch (e) { return json({ error: e?.message || String(e) }); }
|
|
181
|
-
if (!dry.ok) return json({ error: `No model is available for role(s): ${dry.missing.join(', ')}.`, roles: dry.roles });
|
|
182
|
-
const result = await run({ team: resolved(team), request, toolset: bound });
|
|
183
|
-
const findings = (result.board || []).map((f) => ({ role: f.role, kind: f.kind, text: f.text, refs: f.refs }));
|
|
184
|
-
const tasks = (result.tasks || []).map((x) => ({ id: x.id, role: x.role, status: x.status, ms: x.ms, findings: (x.findings || []).length, error: x.error || undefined }));
|
|
185
|
-
const failed = tasks.filter((x) => x.status !== 'ok');
|
|
186
|
-
const summary = failed.length ? failed.map((x) => `${x.role} ${x.status}${x.error ? ` (${x.error})` : ''}`).join('; ') : `${findings.length} findings`;
|
|
187
|
-
ran.set(key, { runId: result.runId, status: result.status, summary, tasks, proposal: result.proposal });
|
|
188
|
-
const hint = result.status === 'over-budget' ? 'The team stopped at its budget; the proposal is what it had. Say so.'
|
|
189
|
-
: result.status === 'failed' ? `The run FAILED — ${summary}. Do not run the team again this turn. Tell the user exactly which role failed and why, and ask whether to retry, change the team\'s models in Settings → Teams, or answer without the team.`
|
|
190
|
-
: failed.length ? `Some roles did not finish — ${summary}. Present the proposal as the team's answer and say which role did not finish; do NOT run the team again this turn.` : undefined;
|
|
191
|
-
return json({ name: team.name, runId: result.runId, status: result.status, proposal: result.proposal, tasks, findings, usage: result.usage, hint });
|
|
192
|
-
}
|
|
193
|
-
return json({ error: `Unknown action "${action}". Use run, dry_run, save or project.` });
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
}
|
|
1
|
+
import{validateTeam as q,normalizeTeam as O,describeRole as D,slugTeamName as M}from"./team.js";import{dryRunTeam as S}from"./team-run.js";const I="team",_=t=>(t||[]).filter(d=>d?.name&&d.enabled!==!1&&q(d).ok);function W(t){const d=_(t);return d.length?`Saved teams: ${d.map(n=>`${n.name} (${(n.roles||[]).map(g=>g.id).join(", ")})${n.description?` — ${n.description}`:""}`).join("; ")}.`:"No teams saved yet."}function x(t){const d=Math.max(0,...(t||[]).map(n=>Number(n?.budget?.ms)||0));return Math.max(12e4,(d||10*6e4)+6e4)}function L(t){return{name:I,annotations:{readOnlyHint:!1,destructiveHint:!1,idempotentHint:!1},timeoutMs:x(t),description:`Saved agent teams — several roles working a request in parallel, merged into one answer. ${W(t)} Actions: {"action":"run","name":"<team>","request":"<what to do>"} runs one (streams; may take a while); {"action":"dry_run","name":"<team>","request":"…"} shows roles, models, tools and budget without running; {"action":"save","team":{…}} proposes a NEW team after a task that would benefit from several roles — the user approves it on a card. {"action":"project","goal":"<what done looks like>","title":"<short name>","doneWhen":"<a check the result can be held to>","budget":{"tokens":200000,"ms":3600000}} hands a GOAL to the executive: it posts jobs, recruits agents from the pool for each, runs them as teams in rounds, reviews the results against done-when, posts follow-ups, asks the user before spending or changing scope, and closes when done-when holds (may take a long while; use it for a goal with several parts, not a question). A team: {"name":"research" (a short identifier: letters, digits, - _; used as /research),"description":"…","roles":[{"id":"researcher","prompt":"…","prefer":"balanced","grants":["data","web"]},{"id":"writer","prompt":"…","prefer":"strong","grants":["none"]}],"merge":"judge","judge":"writer","budget":{"tokens":40000,"ms":300000}}. grants: none | data | web | history | mcp | mcp:<server> | shell | fs:write | scm:read | scm:push | scm:pr. A role may say "agent":"<id>" instead of a prompt to stand for an agent from the pool. merge: judge | converge | concat | first. A budget is required. Order the work with "dependsOn": a role that builds on another's findings (a budget checker on a researcher) lists it, so it runs after and reads the board instead of searching again. The judge does not need a task of its own - the merge is its work.`,parameters:{type:"object",properties:{action:{type:"string",enum:["run","dry_run","save","project"]},goal:{type:"string",description:"For project: the goal, in the user's words — what done looks like."},title:{type:"string",description:"For project: a short name."},doneWhen:{type:"string",description:"For project: the check the result is held to."},budget:{type:"object",description:'For project: {"tokens","ms","usd"} for the whole project.',additionalProperties:!0},name:{type:"string",description:"Team name, for run / dry_run."},request:{type:"string",description:"What the team should do, for run / dry_run."},team:{type:"object",description:"The team to save, for save.",additionalProperties:!0}},required:["action"]}}}function E(t,d){const n=[`${t.name}${t.description?` — ${t.description}`:""}`,`Plan: ${t.plan||"fixed"} · merge: ${t.merge||"concat"}${t.judge?` (judge: ${t.judge})`:""}`];for(const h of d?.roles||t.roles||[])n.push(`• ${D({...h,model:h.label||h.model||void 0})}${h.appointed===!1?" — NO MODEL AVAILABLE":""}`);const g=t.budget||{};return n.push(`Budget: ${Object.entries(g).map(([h,j])=>`${h} ${j}`).join(" · ")}`),n.push("Runs go through your own models and tools; a team may not act on a page. Nothing a team produces lands without you."),n.join(`
|
|
2
|
+
`)}const o=t=>JSON.stringify(t);function R({teams:t=[],run:d=null,appoint:n=null,confirmSave:g=null,saveTeam:h=null,resolve:j=null,runProject:v=null}={}){const f=new Map(_(t).map(c=>[c.name,c])),k=c=>typeof j=="function"?j(c):c;let T=null;const w=new Map;return{id:"team",specs:[L(t)],system:[f.size?"Saved agent teams exist (see the `team` tool). When a request is broad enough that several roles would do it better — research plus writing, several sources to reconcile — run the matching team rather than doing it all in one turn.":"",v?'A GOAL with several parts (a project, not a question) goes to the executive: call the `team` tool with {"action":"project","goal":…,"doneWhen":…} and report what it produced.':"",g&&h?'When the user asks to create, make, set up or save a team (of agents / roles), do not run one: call the `team` tool with {"action":"save","team":{…}} — pick roles, prompts, grants and a budget from what they said, and ask only for what you cannot infer. The user approves it on a card.':""].filter(Boolean).join(" "),bind(c){T=c},async execute(c,a){if(c!==I)return o({error:`Unknown tool: ${c}`});const y=String(a?.action||"");if(y==="save"){const s=a?.team&&typeof a.team=="object"?{...a.team,name:M(a.team.name)}:a?.team,e=q(s);if(!e.ok)return o({error:"The team is not valid.",problems:e.errors,hint:'A team needs a name (letters, digits, - _), roles with prompts and grants, and a budget such as {"tokens":40000,"ms":300000}.'});if(f.has(s.name))return o({error:`A team named "${s.name}" already exists. Pick another name.`});if(!g||!h)return o({error:"Saving a team needs the user's approval, which this surface cannot ask for. Describe the team and suggest saving it from the side panel or the desktop."});const u=O(s),i=S(k(u),"",{appoint:n});if(await g(E(u,i),u)!=="allow")return o({error:`The user did not save "${u.name}". Do not propose it again this turn.`,declined:!0});const l={...u,createdAt:Date.now()};return await h(l),f.set(l.name,l),o({saved:l.name,roles:l.roles.map(b=>b.id),hint:`Run it with {"action":"run","name":"${l.name}","request":"…"} or by typing /${l.name}.`})}if(y==="project"){const s=String(a?.goal||"").trim();if(!s)return o({error:"project needs a goal — what does done look like?"});if(typeof v!="function")return o({error:"This surface cannot run a project. Run a team instead, or describe the jobs."});if(w.has("project")){const m=w.get("project");return o({error:`A project already ran in this turn (${m.projectId}, ${m.status}). Report its result and ask the user how to proceed.`,...m})}let e;try{e=await v({goal:s,title:String(a?.title||"").trim(),doneWhen:String(a?.doneWhen||"").trim(),budget:a?.budget&&typeof a.budget=="object"?a.budget:null,toolset:T})}catch(m){return o({error:m?.message||String(m)})}const u={projectId:e.projectId,status:e.status,rounds:e.rounds,jobs:e.jobs,runs:e.runs,report:e.report,spend:e.spend,why:e.why||void 0};w.set("project",u);const i=e.status==="done"?"Done-when holds and the project is closed. Present the report as the answer; the jobs and their runs are on the board.":e.status==="over-budget"?"The project stopped at its budget; the report so far stands. Say so and ask whether to raise it.":e.status==="failed"?`The project FAILED — ${e.why||"see the jobs"}. Do not start it again this turn; tell the user which job failed and why.`:`The project is ${e.status}${e.why?` — ${e.why}`:""}. Present the report so far and say what is left; do not start it again this turn.`;return o({...u,hint:i})}const p=f.get(String(a?.name||""));if(!p)return o({error:`No team named "${a?.name}".`,available:[...f.keys()]});const $=String(a?.request||"").trim();if(y==="dry_run"){let s;try{s=S(k(p),$,{appoint:n})}catch(e){return o({error:e?.message||String(e)})}return o({name:p.name,ok:s.ok,missing:s.missing,roles:s.roles,plan:s.plan,tasks:s.tasks,merge:s.merge,budget:s.budget})}if(y==="run"){if(!$)return o({error:"run needs a request — what should the team do?"});if(typeof d!="function")return o({error:"This surface cannot run a team."});const s=p.name,e=w.get(s);if(e)return o({error:`The "${p.name}" team already ran in this turn (run ${e.runId}, ${e.status}). Do not run it again, even with a different request: report what it produced — ${e.summary} — with its proposal, and ask the user how to proceed.`,runId:e.runId,status:e.status,tasks:e.tasks,proposal:e.proposal});let u;try{u=S(k(p),$,{appoint:n})}catch(r){return o({error:r?.message||String(r)})}if(!u.ok)return o({error:`No model is available for role(s): ${u.missing.join(", ")}.`,roles:u.roles});const i=await d({team:k(p),request:$,toolset:T}),m=(i.board||[]).map(r=>({role:r.role,kind:r.kind,text:r.text,refs:r.refs})),l=(i.tasks||[]).map(r=>({id:r.id,role:r.role,status:r.status,ms:r.ms,findings:(r.findings||[]).length,error:r.error||void 0})),b=l.filter(r=>r.status!=="ok"),A=b.length?b.map(r=>`${r.role} ${r.status}${r.error?` (${r.error})`:""}`).join("; "):`${m.length} findings`;w.set(s,{runId:i.runId,status:i.status,summary:A,tasks:l,proposal:i.proposal});const N=i.status==="over-budget"?"The team stopped at its budget; the proposal is what it had. Say so.":i.status==="failed"?`The run FAILED — ${A}. Do not run the team again this turn. Tell the user exactly which role failed and why, and ask whether to retry, change the team's models in Settings → Teams, or answer without the team.`:b.length?`Some roles did not finish — ${A}. Present the proposal as the team's answer and say which role did not finish; do NOT run the team again this turn.`:void 0;return o({name:p.name,runId:i.runId,status:i.status,proposal:i.proposal,tasks:l,findings:m,usage:i.usage,hint:N})}return o({error:`Unknown action "${y}". Use run, dry_run, save or project.`})}}}export{I as TEAM_TOOL_NAME,E as describeTeamForApproval,R as teamToolProvider,L as teamToolSpec,x as teamToolTimeoutMs};
|