@chatpanel/events 0.102.0 → 0.104.0
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 +4 -5
- package/pdf-layout.js +7 -253
- package/policy.js +1 -0
- 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-org.js
CHANGED
|
@@ -1,442 +1 @@
|
|
|
1
|
-
// The org, derived — what the Agent Teams surface draws (F8 §17), computed once here so the
|
|
2
|
-
// desktop, the extension and a phone render the same roster, the same shape and the same
|
|
3
|
-
// colours from the same two sections (`agents`, `teams`) and the same records.
|
|
4
|
-
//
|
|
5
|
-
// Why this exists: the Agents tab and the Teams tab disagreed. `research` and `review` kept
|
|
6
|
-
// their roles INLINE — prompt, tier and grants written into the team — so the roles ran, were
|
|
7
|
-
// scored, and were never in the pool; `feature` and friends referenced `agent: architect` by
|
|
8
|
-
// id, which existed only after "+ the engineering org" was clicked, so a team could be saved
|
|
9
|
-
// full of holes and the only place that was said was a `<select>` option. Both are answered
|
|
10
|
-
// by the same rule: EVERY ROLE THAT CAN RUN IS A CARD IN THE POOL, a hole is a state a client
|
|
11
|
-
// draws, and a starter team brings the agents it stands on.
|
|
12
|
-
//
|
|
13
|
-
// Nothing here renders. A client maps `columns` to boxes and arrows, `hue` to a colour, and
|
|
14
|
-
// `kind` to a word; the SVG is its own.
|
|
15
|
-
|
|
16
|
-
import { normalizeTeam, validateTeam, starterTeams, TeamError, GRANT_RE } from './team.js';
|
|
17
|
-
import { normalizeAgent, engineOf, starterAgents, assistantAgent, STARTER_AGENTS, ASSISTANT_ID } from './agent.js';
|
|
18
|
-
|
|
19
|
-
const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
20
|
-
const poolList = (pool) => (Array.isArray(pool) ? pool : []).filter((a) => a && a.id);
|
|
21
|
-
|
|
22
|
-
/** The card a team's inline role becomes: `<team>-<role>`, an id the pool and the gateway's routes accept. */
|
|
23
|
-
export function roleCardId(teamName, roleId) {
|
|
24
|
-
return `${String(teamName || '').toLowerCase()}-${String(roleId || '').toLowerCase()}`.replace(/[^a-z0-9_-]+/g, '-').replace(/^[^a-z]+/, '').slice(0, 64);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const titleCase = (id) => String(id || '').replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()).slice(0, 60);
|
|
28
|
-
const firstSentence = (text) => String(text || '').trim().split(/(?<=[.!?])\s+/)[0]?.slice(0, 300) || '';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* A team as it is SAVED: every inline `model` role becomes a pool card (`agents` to upsert —
|
|
32
|
-
* the role's prompt, grants, engine, skills and working directory move onto it, stamped
|
|
33
|
-
* `createdBy: team:<name>` and `origin: { team, role }`) and the role is rewritten to
|
|
34
|
-
* `agent: <card id>` keeping only what is the team's — its id, name, dependencies. A role
|
|
35
|
-
* that already names an agent, a `recipe` or `subagent` role, is left as it is.
|
|
36
|
-
*
|
|
37
|
-
* Idempotent: saving the same team again yields the same cards (a card's `createdAt` is kept
|
|
38
|
-
* from the pool). The caller writes BOTH sections; this is what both clients' save paths
|
|
39
|
-
* — the form and the chat card — call, so a role can never again run without being seen.
|
|
40
|
-
*/
|
|
41
|
-
export function promoteRoles(team, pool = [], { now = Date.now } = {}) {
|
|
42
|
-
const t = normalizeTeam(team);
|
|
43
|
-
const byId = new Map(poolList(pool).map((a) => [String(a.id), a]));
|
|
44
|
-
const agents = [];
|
|
45
|
-
const roles = t.roles.map((r) => {
|
|
46
|
-
if (r.agent || r.mode !== 'model') return r;
|
|
47
|
-
const id = roleCardId(t.name, r.id);
|
|
48
|
-
const existing = byId.get(id);
|
|
49
|
-
const card = normalizeAgent({
|
|
50
|
-
id,
|
|
51
|
-
name: r.name && r.name !== r.id ? r.name : titleCase(r.id),
|
|
52
|
-
purpose: firstSentence(r.prompt),
|
|
53
|
-
prompt: r.prompt || `You are the ${r.id} of the ${t.name} team.`,
|
|
54
|
-
skills: r.skills || [],
|
|
55
|
-
grants: r.grants || ['none'],
|
|
56
|
-
engine: r.engine || engineOf(r),
|
|
57
|
-
...(r.workdir ? { workdir: r.workdir } : {}),
|
|
58
|
-
...(r.egress ? { egress: r.egress } : {}),
|
|
59
|
-
...(r.memoryScope ? { memoryScope: r.memoryScope } : {}),
|
|
60
|
-
appliesTo: ['jobs'],
|
|
61
|
-
createdBy: `team:${t.name}`,
|
|
62
|
-
origin: { team: t.name, role: r.id },
|
|
63
|
-
createdAt: existing?.createdAt || now(),
|
|
64
|
-
enabled: true,
|
|
65
|
-
});
|
|
66
|
-
agents.push(card);
|
|
67
|
-
return {
|
|
68
|
-
id: r.id,
|
|
69
|
-
...(r.name && r.name !== r.id ? { name: r.name } : {}),
|
|
70
|
-
mode: r.mode,
|
|
71
|
-
agent: id,
|
|
72
|
-
...(r.dependsOn?.length ? { dependsOn: [...r.dependsOn] } : {}),
|
|
73
|
-
...(r.model ? { model: r.model } : {}),
|
|
74
|
-
};
|
|
75
|
-
});
|
|
76
|
-
return { team: normalizeTeam({ ...t, roles }), agents };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* A starter team WITH the agents it stands on: the inline roles promoted, plus every
|
|
81
|
-
* built-in agent it references that the pool lacks. "+ research starter" adds two cards
|
|
82
|
-
* and a team; "+ feature starter" adds the team and the Architect, Implementer, Reviewer,
|
|
83
|
-
* Tester and Scribe if they are not there yet. Never half-installed.
|
|
84
|
-
*/
|
|
85
|
-
export function starterTeam(name, pool = [], opts = {}) {
|
|
86
|
-
const src = starterTeams().find((t) => t.name === name);
|
|
87
|
-
if (!src) return null;
|
|
88
|
-
const { team, agents } = promoteRoles(src, pool, opts);
|
|
89
|
-
const have = new Set([...poolList(pool).map((a) => String(a.id)), ...agents.map((a) => a.id)]);
|
|
90
|
-
const builtins = starterAgents().filter((a) => team.roles.some((r) => r.agent === a.id) && !have.has(a.id));
|
|
91
|
-
return { team, agents: [...agents, ...builtins] };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Which built-in agents a team names that the pool lacks — what "Add the built-in Tester" adds. */
|
|
95
|
-
export function missingStarters(team, pool = []) {
|
|
96
|
-
const have = new Set(poolList(pool).map((a) => String(a.id)));
|
|
97
|
-
return starterAgents().filter((a) => (team?.roles || []).some((r) => r.agent === a.id) && !have.has(a.id));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Can this team run as it stands? A hole is a role naming an agent that is not in the pool
|
|
102
|
-
* (`fix: 'add-builtin'` when a starter has that id, else `'pick'`); a disabled agent is its
|
|
103
|
-
* own row. `resolveTeam` still throws NO_AGENT beneath — this is the state a client draws
|
|
104
|
-
* BEFORE the run button, with the reason in one line.
|
|
105
|
-
*/
|
|
106
|
-
export function teamHealth(team, pool = []) {
|
|
107
|
-
const byId = new Map(poolList(pool).map((a) => [String(a.id), a]));
|
|
108
|
-
const holes = [];
|
|
109
|
-
const disabled = [];
|
|
110
|
-
const valid = validateTeam(team).ok;
|
|
111
|
-
for (const r of team?.roles || []) {
|
|
112
|
-
if (!r?.agent || r.agent === ASSISTANT_ID) continue;
|
|
113
|
-
const a = byId.get(String(r.agent));
|
|
114
|
-
if (!a) holes.push({ role: r.id, agent: r.agent, fix: STARTER_AGENTS.some((s) => s.id === r.agent) ? 'add-builtin' : 'pick' });
|
|
115
|
-
else if (a.enabled === false) disabled.push({ role: r.id, agent: r.agent });
|
|
116
|
-
}
|
|
117
|
-
// Roles written into the team before §17.1 (or by an older client): they run, they are
|
|
118
|
-
// scored, and they are not on the roster. Not a hole — a save promotes them.
|
|
119
|
-
const inline = (team?.roles || []).filter((r) => r && !r.agent && (r.mode || 'model') === 'model').map((r) => r.id);
|
|
120
|
-
const off = team?.enabled === false;
|
|
121
|
-
const ready = valid && !off && !holes.length && !disabled.length;
|
|
122
|
-
const reason = !valid ? 'the team is not complete'
|
|
123
|
-
: off ? 'the team is off'
|
|
124
|
-
: holes.length ? `${holes.length === 1 ? 'a role names an agent' : `${holes.length} roles name agents`} not in the pool: ${holes.map((h) => h.agent).join(', ')}`
|
|
125
|
-
: disabled.length ? `${disabled.map((d) => d.agent).join(', ')} ${disabled.length === 1 ? 'is' : 'are'} off`
|
|
126
|
-
: '';
|
|
127
|
-
return { ready, valid, holes, disabled, inline, reason };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** A role's depth: 0 with no dependencies, else one past the deepest; a cycle or an unknown dependency counts as 0. */
|
|
131
|
-
function depths(roles) {
|
|
132
|
-
const byId = new Map(roles.map((r) => [r.id, r]));
|
|
133
|
-
const memo = new Map();
|
|
134
|
-
const depth = (id, seen) => {
|
|
135
|
-
if (memo.has(id)) return memo.get(id);
|
|
136
|
-
if (seen.has(id)) return 0;
|
|
137
|
-
const r = byId.get(id);
|
|
138
|
-
const deps = (r?.dependsOn || []).filter((d) => byId.has(d));
|
|
139
|
-
const d = deps.length ? 1 + Math.max(...deps.map((x) => depth(x, new Set([...seen, id])))) : 0;
|
|
140
|
-
memo.set(id, d);
|
|
141
|
-
return d;
|
|
142
|
-
};
|
|
143
|
-
for (const r of roles) depth(r.id, new Set());
|
|
144
|
-
return memo;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export const TEAM_SHAPES = Object.freeze(['solo', 'quorum', 'sequence', 'hierarchy']);
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* The shape of a team, from its roles — the one drawing both clients make:
|
|
151
|
-
* • `columns` — roles grouped by dependency depth; one column runs in parallel, the next
|
|
152
|
-
* waits for it. The judge (merge: judge) is not in them: the merge IS its task, so it
|
|
153
|
-
* stands as the last column on its own (`judge`).
|
|
154
|
-
* • `kind` — `solo` (one role), `quorum` (one parallel column, merged), `sequence` (more
|
|
155
|
-
* than one column), `hierarchy` (a role whose engine is another team — A2, not built;
|
|
156
|
-
* read from `mode: 'team'` so the word is ready when the mode is).
|
|
157
|
-
* • `lands: 'person'` — always. Nothing lands without one; the drawing ends on you.
|
|
158
|
-
* Node fields are the team's own (`id`, `agent`, `name`, `dependsOn`); a client joins the
|
|
159
|
-
* pool for the card and `agentHue` for the colour.
|
|
160
|
-
*/
|
|
161
|
-
export function teamShape(team) {
|
|
162
|
-
const roles = (team?.roles || []).filter((r) => r && r.id);
|
|
163
|
-
const judgeId = team?.merge === 'judge' ? (team.judge || roles[roles.length - 1]?.id || null) : null;
|
|
164
|
-
const working = roles.filter((r) => r.id !== judgeId);
|
|
165
|
-
const d = depths(working);
|
|
166
|
-
const byDepth = new Map();
|
|
167
|
-
for (const r of working) {
|
|
168
|
-
const k = d.get(r.id) || 0;
|
|
169
|
-
if (!byDepth.has(k)) byDepth.set(k, []);
|
|
170
|
-
byDepth.get(k).push({ id: r.id, name: r.name || r.id, agent: r.agent || null, mode: r.mode || 'model', dependsOn: [...(r.dependsOn || [])] });
|
|
171
|
-
}
|
|
172
|
-
const columns = [...byDepth.keys()].sort((a, b) => a - b).map((depth) => ({ depth, parallel: byDepth.get(depth).length > 1, roles: byDepth.get(depth) }));
|
|
173
|
-
const j = roles.find((r) => r.id === judgeId);
|
|
174
|
-
const judge = j ? { id: j.id, name: j.name || j.id, agent: j.agent || null } : null;
|
|
175
|
-
const kind = roles.some((r) => r.mode === 'team') ? 'hierarchy'
|
|
176
|
-
: roles.length <= 1 ? 'solo'
|
|
177
|
-
: columns.length <= 1 ? 'quorum'
|
|
178
|
-
: 'sequence';
|
|
179
|
-
return { kind, columns, judge, merge: team?.merge || 'concat', lands: 'person' };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** A shape in a sentence — the card's subtitle, the same on every client. */
|
|
183
|
-
export function describeTeamShape(shape) {
|
|
184
|
-
const s = shape || {};
|
|
185
|
-
const n = (s.columns || []).reduce((a, c) => a + c.roles.length, 0) + (s.judge ? 1 : 0);
|
|
186
|
-
if (s.kind === 'solo') return 'one role';
|
|
187
|
-
if (s.kind === 'quorum') return `${n} roles in parallel${s.judge ? `, ${s.judge.name} judges` : s.merge === 'converge' ? ', reconciled' : ''}`;
|
|
188
|
-
if (s.kind === 'hierarchy') return `${n} roles, one delegates to a team`;
|
|
189
|
-
return `${n} roles in ${s.columns.length} steps${s.judge ? `, ${s.judge.name} judges` : ''}`;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Where an agent works: the teams whose roles name it and the project jobs it was recruited
|
|
194
|
-
* for or holds. `projects` are project records (`foldProject`); a job's `recruited.agentId`
|
|
195
|
-
* or `takenBy.agentId` is the link.
|
|
196
|
-
*/
|
|
197
|
-
export function whereItWorks(agentId, { teams = [], projects = [] } = {}) {
|
|
198
|
-
const id = String(agentId || '');
|
|
199
|
-
const onTeams = (Array.isArray(teams) ? teams : []).filter((t) => t && (t.roles || []).some((r) => r?.agent === id)).map((t) => ({ team: t.name, role: (t.roles || []).find((r) => r?.agent === id)?.id || null }));
|
|
200
|
-
const jobs = [];
|
|
201
|
-
for (const p of Array.isArray(projects) ? projects : []) {
|
|
202
|
-
for (const j of p?.jobs || []) {
|
|
203
|
-
const who = j?.recruited?.agentId || j?.takenBy?.agentId || null;
|
|
204
|
-
if (who === id) jobs.push({ project: p.id, title: p.page?.title || p.id, job: j.id, status: j.status || 'open' });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return { teams: onTeams, jobs };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export const ROSTER_KINDS = Object.freeze(['builtin', 'mine', 'team-role', 'created', 'proposed', 'missing']);
|
|
211
|
-
|
|
212
|
-
/** What kind of card this is, from how it came to be — never declared by the card. */
|
|
213
|
-
export function agentKind(agent) {
|
|
214
|
-
const a = agent || {};
|
|
215
|
-
if (a.id === ASSISTANT_ID || a.builtin || STARTER_AGENTS.some((s) => s.id === a.id)) return 'builtin';
|
|
216
|
-
const by = String(a.createdBy || 'person');
|
|
217
|
-
if (by.startsWith('team:')) return 'team-role';
|
|
218
|
-
if (by !== 'person') return 'created';
|
|
219
|
-
return 'mine';
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* The roster — one row per thing the Agents tab shows: every pool card with its kind and
|
|
224
|
-
* where it works; every PROPOSED card (from a run's `proposal` decisions or a project's —
|
|
225
|
-
* pass them as `proposals: [{ agent, from }]`), and every MISSING agent a team names,
|
|
226
|
-
* deduplicated, with the fix. Sorted: what needs a decision first, then holes, then the
|
|
227
|
-
* pool by name. `counts` is the filter bar.
|
|
228
|
-
*/
|
|
229
|
-
export function rosterRows(pool = [], { teams = [], projects = [], proposals = [] } = {}) {
|
|
230
|
-
const rows = [];
|
|
231
|
-
for (const a of poolList(pool)) {
|
|
232
|
-
rows.push({ kind: agentKind(a), agent: a, where: whereItWorks(a.id, { teams, projects }), hue: agentHue(a.id) });
|
|
233
|
-
}
|
|
234
|
-
const seen = new Set(rows.map((r) => r.agent.id));
|
|
235
|
-
for (const p of Array.isArray(proposals) ? proposals : []) {
|
|
236
|
-
const a = p?.agent && isRecord(p.agent) ? p.agent : null;
|
|
237
|
-
if (!a?.id || seen.has(a.id)) continue;
|
|
238
|
-
seen.add(a.id);
|
|
239
|
-
rows.push({ kind: 'proposed', agent: a, from: p.from || null, where: { teams: [], jobs: [] }, hue: agentHue(a.id) });
|
|
240
|
-
}
|
|
241
|
-
for (const t of Array.isArray(teams) ? teams : []) {
|
|
242
|
-
for (const h of teamHealth(t, pool).holes) {
|
|
243
|
-
if (seen.has(h.agent)) { rows.find((r) => r.agent.id === h.agent)?.namedBy?.push(t.name); continue; }
|
|
244
|
-
seen.add(h.agent);
|
|
245
|
-
rows.push({ kind: 'missing', agent: { id: h.agent, name: h.agent }, fix: h.fix, namedBy: [t.name], where: { teams: [{ team: t.name, role: h.role }], jobs: [] }, hue: agentHue(h.agent) });
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const order = { proposed: 0, missing: 1, builtin: 3, 'team-role': 3, created: 3, mine: 3 };
|
|
249
|
-
rows.sort((a, b) => (order[a.kind] - order[b.kind]) || String(a.agent.name || a.agent.id).localeCompare(String(b.agent.name || b.agent.id)));
|
|
250
|
-
const counts = { all: rows.length };
|
|
251
|
-
for (const k of ROSTER_KINDS) counts[k] = rows.filter((r) => r.kind === k).length;
|
|
252
|
-
counts.onTeam = rows.filter((r) => r.where.teams.length).length;
|
|
253
|
-
return { rows, counts };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* An agent's colour is data: one hue per id, the same on every client and every tab, so a
|
|
258
|
-
* team's shape reads without labels. FNV-1a over the id → 0..359; the built-in org keeps
|
|
259
|
-
* fixed, well-separated hues so the seven starters never land next to each other.
|
|
260
|
-
*/
|
|
261
|
-
const FIXED_HUES = Object.freeze({ assistant: 240, executive: 262, architect: 212, implementer: 158, reviewer: 38, tester: 330, librarian: 190, scribe: 0, release: 280 });
|
|
262
|
-
export function agentHue(id) {
|
|
263
|
-
const key = String(id || '');
|
|
264
|
-
if (FIXED_HUES[key] !== undefined) return FIXED_HUES[key];
|
|
265
|
-
let h = 0x811c9dc5;
|
|
266
|
-
for (let i = 0; i < key.length; i++) { h ^= key.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
|
|
267
|
-
return h % 360;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** The CSS colour for a hue — muted on a light ground, lifted on a dark one; the scribe's 0 is a grey, not a red. */
|
|
271
|
-
export function agentColor(id, { dark = false } = {}) {
|
|
272
|
-
const h = agentHue(id);
|
|
273
|
-
if (id === 'scribe') return dark ? 'hsl(220 8% 62%)' : 'hsl(220 8% 45%)';
|
|
274
|
-
return dark ? `hsl(${h} 58% 62%)` : `hsl(${h} 60% 44%)`;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Two letters for the avatar: "Budget checker" → "Bc", "researcher" → "Re". */
|
|
278
|
-
export function agentInitials(agentOrId) {
|
|
279
|
-
const name = typeof agentOrId === 'string' ? agentOrId : (agentOrId?.name || agentOrId?.id || '');
|
|
280
|
-
const words = String(name).trim().split(/[\s_-]+/).filter(Boolean);
|
|
281
|
-
if (!words.length) return '?';
|
|
282
|
-
if (words.length === 1) return (words[0][0].toUpperCase() + (words[0][1] || '')).slice(0, 2);
|
|
283
|
-
return (words[0][0] + words[1][0].toLowerCase()).slice(0, 2).replace(/^./, (c) => c.toUpperCase());
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export { TeamError };
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* The four numbers on an agent's card, from the gateway's scorecard (`summarize()` +
|
|
290
|
-
* `attested`) — the same four on every client, "—" where nothing is known. A run through an
|
|
291
|
-
* agent tool reports no tokens, so cost is never one of them; time and count always are.
|
|
292
|
-
*/
|
|
293
|
-
export function cardNumbers(summary, { attested = null } = {}) {
|
|
294
|
-
const s = summary && typeof summary === 'object' ? summary : null;
|
|
295
|
-
const n = s?.entries || 0;
|
|
296
|
-
const pct = (v) => (Number.isFinite(v) ? `${Math.round(v * 100)}%` : '—');
|
|
297
|
-
const tasks = (s?.jobsDone || 0) + (s?.jobsFailed || 0);
|
|
298
|
-
return [
|
|
299
|
-
{ key: 'tasks', label: 'tasks', value: n ? String(tasks) : '—', detail: n ? `${s.jobsDone} done · ${s.jobsFailed} failed` : 'nothing yet' },
|
|
300
|
-
{ key: 'rating', label: 'rating', value: n ? pct(s.rating?.avg) : '—', detail: n && s.rating?.count ? `${s.rating.count} rating${s.rating.count === 1 ? '' : 's'}` : 'not rated' },
|
|
301
|
-
{ key: 'engines', label: 'engines', value: n ? String((s.byEngine || []).length) : '—', detail: s?.engineIndependence != null ? `independence ${pct(s.engineIndependence)}` : 'one so far' },
|
|
302
|
-
{ key: 'record', label: 'record', value: n ? (attested?.ok ? '✓' : n ? '·' : '—') : '—', detail: n ? `${n} entr${n === 1 ? 'y' : 'ies'}${attested?.ok ? ', attested' : ''}${s.scm?.commits ? ` · ${s.scm.commits} commits` : ''}` : 'nothing yet' },
|
|
303
|
-
];
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** Cards onto the pool: an existing id is replaced in place, a new one appended — the order a person made stays. */
|
|
307
|
-
export function upsertAgents(pool, cards) {
|
|
308
|
-
const list = poolList(pool);
|
|
309
|
-
const add = (Array.isArray(cards) ? cards : []).filter((c) => c && c.id);
|
|
310
|
-
return [...list.map((a) => add.find((c) => c.id === a.id) || a), ...add.filter((c) => !list.some((a) => a.id === c.id))];
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* An agent, INVOKABLE on its own: a one-role team named after it (`/researcher`, "ask the
|
|
315
|
-
* researcher to…"), the role standing for the card, the answer the role's own (`merge:
|
|
316
|
-
* first`), under a default budget since a team without one does not run (O1). `origin.agent`
|
|
317
|
-
* marks it so a client can draw it as an agent, not a team. Never stored — derived from the
|
|
318
|
-
* pool every time, so a card edit lands at once and a deleted card takes its command with it.
|
|
319
|
-
*/
|
|
320
|
-
export const SOLO_BUDGET = Object.freeze({ tokens: 40000, ms: 300000 });
|
|
321
|
-
export function soloTeam(agent, { budget = SOLO_BUDGET } = {}) {
|
|
322
|
-
const a = agent && agent.id ? agent : null;
|
|
323
|
-
if (!a || a.enabled === false) return null;
|
|
324
|
-
return normalizeTeam({
|
|
325
|
-
name: String(a.id).toLowerCase(),
|
|
326
|
-
description: `Just ${a.name || a.id}${a.purpose ? ` — ${a.purpose}` : ''}`,
|
|
327
|
-
plan: 'fixed', merge: 'first',
|
|
328
|
-
roles: [{ id: String(a.id).toLowerCase().slice(0, 32), agent: a.id }],
|
|
329
|
-
budget: { ...budget },
|
|
330
|
-
origin: { agent: a.id },
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* The teams a chat can run: the saved ones, then a solo team per enabled pool agent whose
|
|
337
|
-
* id no saved team already claims. What the slash menu, the `team` tool and "run …" all read.
|
|
338
|
-
*/
|
|
339
|
-
export function teamsWithSolos(teams = [], pool = [], opts = {}) {
|
|
340
|
-
const org = builtinOrg(teams, pool);
|
|
341
|
-
const saved = org.teams;
|
|
342
|
-
const taken = new Set(saved.map((t) => String(t.name).toLowerCase()));
|
|
343
|
-
const solos = [];
|
|
344
|
-
for (const a of org.pool) {
|
|
345
|
-
if (taken.has(String(a.id).toLowerCase()) || !(Array.isArray(a.appliesTo) ? a.appliesTo : ['jobs']).includes('jobs')) continue;
|
|
346
|
-
const t = soloTeam(a, opts);
|
|
347
|
-
if (t) { solos.push(t); taken.add(t.name); }
|
|
348
|
-
}
|
|
349
|
-
// The Assistant too: `/assistant <request>` runs the chat's own model as a one-role team —
|
|
350
|
-
// on the board, on a scorecard — where a plain turn is neither.
|
|
351
|
-
if (!taken.has(ASSISTANT_ID)) { const t = soloTeam(assistantAgent(), opts); if (t) solos.push({ ...t, builtin: true }); }
|
|
352
|
-
return [...saved, ...solos];
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* THE BUILT-IN ORG — the standing agents and the starter teams ship with the product, present
|
|
357
|
-
* and runnable without a click (the mock's "built-in" chips), and DERIVED, never stored: the
|
|
358
|
-
* `agents` and `teams` sections hold only what a person made, edited or switched off. A saved
|
|
359
|
-
* record with a built-in's id or name REPLACES it (an edit, or `enabled: false`), so the
|
|
360
|
-
* sections stay small and a product update reaches every unedited built-in.
|
|
361
|
-
*
|
|
362
|
-
* • every starter team a person has not saved is here, promoted (its roles as cards) and
|
|
363
|
-
* marked `builtin`, with the cards it stands on;
|
|
364
|
-
* • every starter agent not in the pool is here, marked `builtin`;
|
|
365
|
-
* • the Assistant stays the fixed card it is (agent.js assistantAgent).
|
|
366
|
-
* Returns `{ teams, pool }` — what every list, the slash menu and the runner read.
|
|
367
|
-
*/
|
|
368
|
-
export function builtinOrg(teams = [], pool = []) {
|
|
369
|
-
const savedTeams = (Array.isArray(teams) ? teams : []).filter((t) => t && t.name);
|
|
370
|
-
const savedNames = new Set(savedTeams.map((t) => String(t.name).toLowerCase()));
|
|
371
|
-
const outPool = [...poolList(pool)];
|
|
372
|
-
const have = () => new Set(outPool.map((a) => String(a.id)));
|
|
373
|
-
const outTeams = [...savedTeams];
|
|
374
|
-
for (const st of starterTeams()) {
|
|
375
|
-
if (savedNames.has(st.name)) continue;
|
|
376
|
-
const { team, agents } = starterTeam(st.name, outPool, { now: () => 0 });
|
|
377
|
-
outTeams.push({ ...team, builtin: true });
|
|
378
|
-
const ids = have();
|
|
379
|
-
for (const a of agents) if (!ids.has(a.id)) { const { createdAt, ...card } = a; outPool.push({ ...card, builtin: true }); }
|
|
380
|
-
}
|
|
381
|
-
const ids = have();
|
|
382
|
-
for (const a of starterAgents()) if (!ids.has(a.id)) outPool.push({ ...a, builtin: true });
|
|
383
|
-
return { teams: outTeams, pool: outPool };
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/** Is this record the product's, not a person's — drawn with the built-in chip, toggled rather than deleted. */
|
|
387
|
-
export const isBuiltin = (rec) => !!rec?.builtin;
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* The GRANTS a person can give, as choices — not a free field of ids nobody has seen. Grouped
|
|
391
|
-
* the way they read: what the agent may reach, which connected servers, what work it may do
|
|
392
|
-
* (only an agent-tool engine can use those). `servers` are the connected MCP servers
|
|
393
|
-
* (`{ id, name? }`), each its own `mcp:<id>` choice under the umbrella `mcp`.
|
|
394
|
-
*/
|
|
395
|
-
export const GRANT_INFO = Object.freeze({
|
|
396
|
-
data: ['Your data', 'notes, meetings and past chats — search and read'],
|
|
397
|
-
web: ['Web', 'search the web and fetch pages'],
|
|
398
|
-
history: ['Chat history', 'search past chats only'],
|
|
399
|
-
mcp: ['Every connected server', 'all MCP servers you connected; pick servers below to narrow'],
|
|
400
|
-
shell: ['Run commands', 'a shell in its working directory'],
|
|
401
|
-
'fs:write': ['Write files', 'edit files in its working directory'],
|
|
402
|
-
'scm:read': ['Read the repository', 'the checkout and its hub'],
|
|
403
|
-
'scm:push': ['Push its branch', 'its own cp/<project>/<job> branch, never main'],
|
|
404
|
-
'scm:pr': ['Open a pull request', ''],
|
|
405
|
-
'scm:merge': ['Merge', 'only where the org\'s gate allows it'],
|
|
406
|
-
});
|
|
407
|
-
export function grantChoices({ servers = [] } = {}) {
|
|
408
|
-
const item = (id) => ({ id, label: GRANT_INFO[id]?.[0] || id, hint: GRANT_INFO[id]?.[1] || '' });
|
|
409
|
-
const groups = [
|
|
410
|
-
{ id: 'reach', label: 'May reach', items: ['data', 'web', 'history', 'mcp'].map(item) },
|
|
411
|
-
];
|
|
412
|
-
const srv = (Array.isArray(servers) ? servers : []).filter((s) => s && s.id);
|
|
413
|
-
if (srv.length) groups.push({ id: 'servers', label: 'Connected servers', items: srv.map((s) => ({ id: `mcp:${s.id}`, label: s.name || s.id, hint: 'this server only' })) });
|
|
414
|
-
groups.push({ id: 'work', label: 'May do (agent-tool engines only)', items: ['shell', 'fs:write', 'scm:read', 'scm:push', 'scm:pr', 'scm:merge'].map(item) });
|
|
415
|
-
return groups;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Grants from checked ids: nothing checked is `none`; `mcp` swallows the per-server picks. */
|
|
419
|
-
export function grantsFromChoices(checked = []) {
|
|
420
|
-
const set = new Set((Array.isArray(checked) ? checked : []).filter((g) => g && g !== 'none' && GRANT_RE.test(String(g))));
|
|
421
|
-
if (set.has('mcp')) for (const g of [...set]) if (g.startsWith('mcp:')) set.delete(g);
|
|
422
|
-
return set.size ? [...set] : ['none'];
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* The SKILLS a person can attach, from the skills they have: the shared `skills` section
|
|
427
|
-
* (written or installed, `{ command, name?, description?, enabled? }`) and any names an
|
|
428
|
-
* agent already carries that are not there (kept, marked `missing`, so a card never loses a
|
|
429
|
-
* skill the list forgot). A skill's id is its command — what `skill_open` and `fit` match on.
|
|
430
|
-
*/
|
|
431
|
-
export function skillChoices(skills = [], { current = [] } = {}) {
|
|
432
|
-
const out = [];
|
|
433
|
-
const seen = new Set();
|
|
434
|
-
for (const s of Array.isArray(skills) ? skills : []) {
|
|
435
|
-
const id = String(s?.command || s?.name || '').trim();
|
|
436
|
-
if (!id || seen.has(id) || s?.enabled === false) continue;
|
|
437
|
-
seen.add(id);
|
|
438
|
-
out.push({ id, label: s.name && s.name !== id ? `${s.name} (/${id})` : `/${id}`, hint: String(s.description || '').slice(0, 120) });
|
|
439
|
-
}
|
|
440
|
-
for (const id of Array.isArray(current) ? current : []) if (id && !seen.has(id)) { seen.add(id); out.push({ id, label: id, hint: 'not among your skills now', missing: true }); }
|
|
441
|
-
return out;
|
|
442
|
-
}
|
|
1
|
+
import{normalizeTeam as f,validateTeam as j,starterTeams as y,TeamError as x,GRANT_RE as T}from"./team.js";import{normalizeAgent as C,engineOf as v,starterAgents as h,assistantAgent as O,STARTER_AGENTS as w,ASSISTANT_ID as b}from"./agent.js";const I=o=>o!==null&&typeof o=="object"&&!Array.isArray(o),u=o=>(Array.isArray(o)?o:[]).filter(t=>t&&t.id);function M(o,t){return`${String(o||"").toLowerCase()}-${String(t||"").toLowerCase()}`.replace(/[^a-z0-9_-]+/g,"-").replace(/^[^a-z]+/,"").slice(0,64)}const E=o=>String(o||"").replace(/[_-]+/g," ").replace(/\b\w/g,t=>t.toUpperCase()).slice(0,60),R=o=>String(o||"").trim().split(/(?<=[.!?])\s+/)[0]?.slice(0,300)||"";function _(o,t=[],{now:e=Date.now}={}){const r=f(o),n=new Map(u(t).map(s=>[String(s.id),s])),l=[],c=r.roles.map(s=>{if(s.agent||s.mode!=="model")return s;const i=M(r.name,s.id),d=n.get(i),a=C({id:i,name:s.name&&s.name!==s.id?s.name:E(s.id),purpose:R(s.prompt),prompt:s.prompt||`You are the ${s.id} of the ${r.name} team.`,skills:s.skills||[],grants:s.grants||["none"],engine:s.engine||v(s),...s.workdir?{workdir:s.workdir}:{},...s.egress?{egress:s.egress}:{},...s.memoryScope?{memoryScope:s.memoryScope}:{},appliesTo:["jobs"],createdBy:`team:${r.name}`,origin:{team:r.name,role:s.id},createdAt:d?.createdAt||e(),enabled:!0});return l.push(a),{id:s.id,...s.name&&s.name!==s.id?{name:s.name}:{},mode:s.mode,agent:i,...s.dependsOn?.length?{dependsOn:[...s.dependsOn]}:{},...s.model?{model:s.model}:{}}});return{team:f({...r,roles:c}),agents:l}}function z(o,t=[],e={}){const r=y().find(i=>i.name===o);if(!r)return null;const{team:n,agents:l}=_(r,t,e),c=new Set([...u(t).map(i=>String(i.id)),...l.map(i=>i.id)]),s=h().filter(i=>n.roles.some(d=>d.agent===i.id)&&!c.has(i.id));return{team:n,agents:[...l,...s]}}function P(o,t=[]){const e=new Set(u(t).map(r=>String(r.id)));return h().filter(r=>(o?.roles||[]).some(n=>n.agent===r.id)&&!e.has(r.id))}function L(o,t=[]){const e=new Map(u(t).map(a=>[String(a.id),a])),r=[],n=[],l=j(o).ok;for(const a of o?.roles||[]){if(!a?.agent||a.agent===b)continue;const m=e.get(String(a.agent));m?m.enabled===!1&&n.push({role:a.id,agent:a.agent}):r.push({role:a.id,agent:a.agent,fix:w.some(g=>g.id===a.agent)?"add-builtin":"pick"})}const c=(o?.roles||[]).filter(a=>a&&!a.agent&&(a.mode||"model")==="model").map(a=>a.id),s=o?.enabled===!1,i=l&&!s&&!r.length&&!n.length,d=l?s?"the team is off":r.length?`${r.length===1?"a role names an agent":`${r.length} roles name agents`} not in the pool: ${r.map(a=>a.agent).join(", ")}`:n.length?`${n.map(a=>a.agent).join(", ")} ${n.length===1?"is":"are"} off`:"":"the team is not complete";return{ready:i,valid:l,holes:r,disabled:n,inline:c,reason:d}}function N(o){const t=new Map(o.map(n=>[n.id,n])),e=new Map,r=(n,l)=>{if(e.has(n))return e.get(n);if(l.has(n))return 0;const s=(t.get(n)?.dependsOn||[]).filter(d=>t.has(d)),i=s.length?1+Math.max(...s.map(d=>r(d,new Set([...l,n])))):0;return e.set(n,i),i};for(const n of o)r(n.id,new Set);return e}const K=Object.freeze(["solo","quorum","sequence","hierarchy"]);function Y(o){const t=(o?.roles||[]).filter(a=>a&&a.id),e=o?.merge==="judge"&&(o.judge||t[t.length-1]?.id)||null,r=t.filter(a=>a.id!==e),n=N(r),l=new Map;for(const a of r){const m=n.get(a.id)||0;l.has(m)||l.set(m,[]),l.get(m).push({id:a.id,name:a.name||a.id,agent:a.agent||null,mode:a.mode||"model",dependsOn:[...a.dependsOn||[]]})}const c=[...l.keys()].sort((a,m)=>a-m).map(a=>({depth:a,parallel:l.get(a).length>1,roles:l.get(a)})),s=t.find(a=>a.id===e),i=s?{id:s.id,name:s.name||s.id,agent:s.agent||null}:null;return{kind:t.some(a=>a.mode==="team")?"hierarchy":t.length<=1?"solo":c.length<=1?"quorum":"sequence",columns:c,judge:i,merge:o?.merge||"concat",lands:"person"}}function J(o){const t=o||{},e=(t.columns||[]).reduce((r,n)=>r+n.roles.length,0)+(t.judge?1:0);return t.kind==="solo"?"one role":t.kind==="quorum"?`${e} roles in parallel${t.judge?`, ${t.judge.name} judges`:t.merge==="converge"?", reconciled":""}`:t.kind==="hierarchy"?`${e} roles, one delegates to a team`:`${e} roles in ${t.columns.length} steps${t.judge?`, ${t.judge.name} judges`:""}`}function B(o,{teams:t=[],projects:e=[]}={}){const r=String(o||""),n=(Array.isArray(t)?t:[]).filter(c=>c&&(c.roles||[]).some(s=>s?.agent===r)).map(c=>({team:c.name,role:(c.roles||[]).find(s=>s?.agent===r)?.id||null})),l=[];for(const c of Array.isArray(e)?e:[])for(const s of c?.jobs||[])(s?.recruited?.agentId||s?.takenBy?.agentId||null)===r&&l.push({project:c.id,title:c.page?.title||c.id,job:s.id,status:s.status||"open"});return{teams:n,jobs:l}}const D=Object.freeze(["builtin","mine","team-role","created","proposed","missing"]);function q(o){const t=o||{};if(t.id===b||t.builtin||w.some(r=>r.id===t.id))return"builtin";const e=String(t.createdBy||"person");return e.startsWith("team:")?"team-role":e!=="person"?"created":"mine"}function X(o=[],{teams:t=[],projects:e=[],proposals:r=[]}={}){const n=[];for(const i of u(o))n.push({kind:q(i),agent:i,where:B(i.id,{teams:t,projects:e}),hue:p(i.id)});const l=new Set(n.map(i=>i.agent.id));for(const i of Array.isArray(r)?r:[]){const d=i?.agent&&I(i.agent)?i.agent:null;!d?.id||l.has(d.id)||(l.add(d.id),n.push({kind:"proposed",agent:d,from:i.from||null,where:{teams:[],jobs:[]},hue:p(d.id)}))}for(const i of Array.isArray(t)?t:[])for(const d of L(i,o).holes){if(l.has(d.agent)){n.find(a=>a.agent.id===d.agent)?.namedBy?.push(i.name);continue}l.add(d.agent),n.push({kind:"missing",agent:{id:d.agent,name:d.agent},fix:d.fix,namedBy:[i.name],where:{teams:[{team:i.name,role:d.role}],jobs:[]},hue:p(d.agent)})}const c={proposed:0,missing:1,builtin:3,"team-role":3,created:3,mine:3};n.sort((i,d)=>c[i.kind]-c[d.kind]||String(i.agent.name||i.agent.id).localeCompare(String(d.agent.name||d.agent.id)));const s={all:n.length};for(const i of D)s[i]=n.filter(d=>d.kind===i).length;return s.onTeam=n.filter(i=>i.where.teams.length).length,{rows:n,counts:s}}const S=Object.freeze({assistant:240,executive:262,architect:212,implementer:158,reviewer:38,tester:330,librarian:190,scribe:0,release:280});function p(o){const t=String(o||"");if(S[t]!==void 0)return S[t];let e=2166136261;for(let r=0;r<t.length;r++)e^=t.charCodeAt(r),e=Math.imul(e,16777619)>>>0;return e%360}function Q(o,{dark:t=!1}={}){const e=p(o);return o==="scribe"?t?"hsl(220 8% 62%)":"hsl(220 8% 45%)":t?`hsl(${e} 58% 62%)`:`hsl(${e} 60% 44%)`}function V(o){const t=typeof o=="string"?o:o?.name||o?.id||"",e=String(t).trim().split(/[\s_-]+/).filter(Boolean);return e.length?e.length===1?(e[0][0].toUpperCase()+(e[0][1]||"")).slice(0,2):(e[0][0]+e[1][0].toLowerCase()).slice(0,2).replace(/^./,r=>r.toUpperCase()):"?"}function Z(o,{attested:t=null}={}){const e=o&&typeof o=="object"?o:null,r=e?.entries||0,n=c=>Number.isFinite(c)?`${Math.round(c*100)}%`:"—",l=(e?.jobsDone||0)+(e?.jobsFailed||0);return[{key:"tasks",label:"tasks",value:r?String(l):"—",detail:r?`${e.jobsDone} done · ${e.jobsFailed} failed`:"nothing yet"},{key:"rating",label:"rating",value:r?n(e.rating?.avg):"—",detail:r&&e.rating?.count?`${e.rating.count} rating${e.rating.count===1?"":"s"}`:"not rated"},{key:"engines",label:"engines",value:r?String((e.byEngine||[]).length):"—",detail:e?.engineIndependence!=null?`independence ${n(e.engineIndependence)}`:"one so far"},{key:"record",label:"record",value:r?t?.ok?"✓":r?"·":"—":"—",detail:r?`${r} entr${r===1?"y":"ies"}${t?.ok?", attested":""}${e.scm?.commits?` · ${e.scm.commits} commits`:""}`:"nothing yet"}]}function ee(o,t){const e=u(o),r=(Array.isArray(t)?t:[]).filter(n=>n&&n.id);return[...e.map(n=>r.find(l=>l.id===n.id)||n),...r.filter(n=>!e.some(l=>l.id===n.id))]}const F=Object.freeze({tokens:4e4,ms:3e5});function A(o,{budget:t=F}={}){const e=o&&o.id?o:null;return!e||e.enabled===!1?null:f({name:String(e.id).toLowerCase(),description:`Just ${e.name||e.id}${e.purpose?` — ${e.purpose}`:""}`,plan:"fixed",merge:"first",roles:[{id:String(e.id).toLowerCase().slice(0,32),agent:e.id}],budget:{...t},origin:{agent:e.id}})}function ne(o=[],t=[],e={}){const r=W(o,t),n=r.teams,l=new Set(n.map(s=>String(s.name).toLowerCase())),c=[];for(const s of r.pool){if(l.has(String(s.id).toLowerCase())||!(Array.isArray(s.appliesTo)?s.appliesTo:["jobs"]).includes("jobs"))continue;const i=A(s,e);i&&(c.push(i),l.add(i.name))}if(!l.has(b)){const s=A(O(),e);s&&c.push({...s,builtin:!0})}return[...n,...c]}function W(o=[],t=[]){const e=(Array.isArray(o)?o:[]).filter(i=>i&&i.name),r=new Set(e.map(i=>String(i.name).toLowerCase())),n=[...u(t)],l=()=>new Set(n.map(i=>String(i.id))),c=[...e];for(const i of y()){if(r.has(i.name))continue;const{team:d,agents:a}=z(i.name,n,{now:()=>0});c.push({...d,builtin:!0});const m=l();for(const g of a)if(!m.has(g.id)){const{createdAt:U,...$}=g;n.push({...$,builtin:!0})}}const s=l();for(const i of h())s.has(i.id)||n.push({...i,builtin:!0});return{teams:c,pool:n}}const te=o=>!!o?.builtin,k=Object.freeze({data:["Your data","notes, meetings and past chats — search and read"],web:["Web","search the web and fetch pages"],history:["Chat history","search past chats only"],mcp:["Every connected server","all MCP servers you connected; pick servers below to narrow"],shell:["Run commands","a shell in its working directory"],"fs:write":["Write files","edit files in its working directory"],"scm:read":["Read the repository","the checkout and its hub"],"scm:push":["Push its branch","its own cp/<project>/<job> branch, never main"],"scm:pr":["Open a pull request",""],"scm:merge":["Merge","only where the org's gate allows it"]});function oe({servers:o=[]}={}){const t=n=>({id:n,label:k[n]?.[0]||n,hint:k[n]?.[1]||""}),e=[{id:"reach",label:"May reach",items:["data","web","history","mcp"].map(t)}],r=(Array.isArray(o)?o:[]).filter(n=>n&&n.id);return r.length&&e.push({id:"servers",label:"Connected servers",items:r.map(n=>({id:`mcp:${n.id}`,label:n.name||n.id,hint:"this server only"}))}),e.push({id:"work",label:"May do (agent-tool engines only)",items:["shell","fs:write","scm:read","scm:push","scm:pr","scm:merge"].map(t)}),e}function se(o=[]){const t=new Set((Array.isArray(o)?o:[]).filter(e=>e&&e!=="none"&&T.test(String(e))));if(t.has("mcp"))for(const e of[...t])e.startsWith("mcp:")&&t.delete(e);return t.size?[...t]:["none"]}function re(o=[],{current:t=[]}={}){const e=[],r=new Set;for(const n of Array.isArray(o)?o:[]){const l=String(n?.command||n?.name||"").trim();!l||r.has(l)||n?.enabled===!1||(r.add(l),e.push({id:l,label:n.name&&n.name!==l?`${n.name} (/${l})`:`/${l}`,hint:String(n.description||"").slice(0,120)}))}for(const n of Array.isArray(t)?t:[])n&&!r.has(n)&&(r.add(n),e.push({id:n,label:n,hint:"not among your skills now",missing:!0}));return e}export{k as GRANT_INFO,D as ROSTER_KINDS,F as SOLO_BUDGET,K as TEAM_SHAPES,x as TeamError,Q as agentColor,p as agentHue,V as agentInitials,q as agentKind,W as builtinOrg,Z as cardNumbers,J as describeTeamShape,oe as grantChoices,se as grantsFromChoices,te as isBuiltin,P as missingStarters,_ as promoteRoles,M as roleCardId,X as rosterRows,re as skillChoices,A as soloTeam,z as starterTeam,L as teamHealth,Y as teamShape,ne as teamsWithSolos,ee as upsertAgents,B as whereItWorks};
|
package/team-plan.js
CHANGED
|
@@ -1,132 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// declaration order with the dependencies the roles declare; no model is asked how to
|
|
5
|
-
// split the work, so the plan is free and reproducible. `planner` — the strongest appointed
|
|
6
|
-
// role is asked, through the shared structured layer (never a hand-typed JSON prompt), to
|
|
7
|
-
// break the request into tasks and assign each to a role it may use. A planner that returns
|
|
8
|
-
// nothing readable falls back to the fixed plan, which is why a run can always start.
|
|
9
|
-
//
|
|
10
|
-
// A task's `dependsOn` is what the fan-out turns into barriers: a task with no unmet
|
|
11
|
-
// dependency runs alongside the others (the round's pool), a dependent one waits and reads
|
|
12
|
-
// the board. That is the whole scheduling model — the same one a tool round uses.
|
|
1
|
+
import{defineSchema as c,describeSchema as m,coerce as u}from"./structured.js";import{GRANT_RE as g}from"./team.js";const p=12,h=c({name:"team_plan",purpose:"the tasks a team will run for a request, each assigned to one role",fields:{tasks:{type:"object[]",required:!0,maxItems:p,describe:"concrete, independent where possible; a task that needs another's result names it in dependsOn",fields:{id:{type:"string",required:!0,max:32,describe:"a short id like t1"},role:{type:"string",required:!0,max:32,describe:"the id of the role that runs it"},title:{type:"string",required:!0,max:80},prompt:{type:"string",required:!0,max:1200,describe:"the focused instruction for this task"},dependsOn:{type:"string[]",maxItems:6,describe:"task ids whose findings this one needs"},grants:{type:"string[]",maxItems:6,describe:"the tools this task needs, from the role's own: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr — or none"},why:{type:"string",max:160,describe:"why those tools, in a few words"}}}},nothing:{tasks:[]}});function S(r,o){const s=(r.roles||[]).map(n=>`- ${n.id}: ${n.name||n.id} — ${n.prompt.slice(0,200)}${n.grants?.length?` (tools: ${n.grants.join(", ")})`:""}`).join(`
|
|
2
|
+
`);return[`You are the planner of a team named "${r.name}". Break the request into 2–${Math.min(p,Math.max(2,(r.roles||[]).length*2))} tasks and assign each to ONE of these roles by id:`,s,"",`Request: ${String(o||"").trim()}`,"","Prefer tasks that can run at the same time; use dependsOn only when a task truly needs another's findings. Do not assign a role a task it has no tools for. For each task, say which of the role's tools it will need (grants) and why — do not miss a tool that would help.","",m(h)].join(`
|
|
3
|
+
`)}function x(r,o){const s=String(o||"").trim(),n=r.merge==="judge"?r.judge:null;return(r.roles||[]).filter(e=>e.id!==n||(r.roles||[]).length===1).map(e=>({id:`t_${e.id}`,role:e.id,title:e.name||e.id,prompt:e.prompt?`${e.prompt}
|
|
13
4
|
|
|
14
|
-
|
|
15
|
-
import { GRANT_RE } from './team.js';
|
|
16
|
-
|
|
17
|
-
export const MAX_TASKS = 12;
|
|
18
|
-
|
|
19
|
-
export const TEAM_PLAN_SCHEMA = defineSchema({
|
|
20
|
-
name: 'team_plan',
|
|
21
|
-
purpose: 'the tasks a team will run for a request, each assigned to one role',
|
|
22
|
-
fields: {
|
|
23
|
-
tasks: {
|
|
24
|
-
type: 'object[]', required: true, maxItems: MAX_TASKS,
|
|
25
|
-
describe: 'concrete, independent where possible; a task that needs another\'s result names it in dependsOn',
|
|
26
|
-
fields: {
|
|
27
|
-
id: { type: 'string', required: true, max: 32, describe: 'a short id like t1' },
|
|
28
|
-
role: { type: 'string', required: true, max: 32, describe: 'the id of the role that runs it' },
|
|
29
|
-
title: { type: 'string', required: true, max: 80 },
|
|
30
|
-
prompt: { type: 'string', required: true, max: 1200, describe: 'the focused instruction for this task' },
|
|
31
|
-
dependsOn: { type: 'string[]', maxItems: 6, describe: 'task ids whose findings this one needs' },
|
|
32
|
-
// The planner's TOOL PROPOSAL (§15.2): which of the role's tools this task will need,
|
|
33
|
-
// and why — posted on the board as a proposal a person reads; a task that then ends
|
|
34
|
-
// without touching a tool it was said to need is nudged once before it may finish.
|
|
35
|
-
grants: { type: 'string[]', maxItems: 6, describe: 'the tools this task needs, from the role\'s own: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr — or none' },
|
|
36
|
-
why: { type: 'string', max: 160, describe: 'why those tools, in a few words' },
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
nothing: { tasks: [] },
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
/** The planner's instruction — the request, the roles it may use, the shape it must answer in. */
|
|
44
|
-
export function plannerPrompt(team, request) {
|
|
45
|
-
const roles = (team.roles || []).map((r) => `- ${r.id}: ${r.name || r.id} — ${r.prompt.slice(0, 200)}${r.grants?.length ? ` (tools: ${r.grants.join(', ')})` : ''}`).join('\n');
|
|
46
|
-
return [
|
|
47
|
-
`You are the planner of a team named "${team.name}". Break the request into 2–${Math.min(MAX_TASKS, Math.max(2, (team.roles || []).length * 2))} tasks and assign each to ONE of these roles by id:`,
|
|
48
|
-
roles,
|
|
49
|
-
'',
|
|
50
|
-
`Request: ${String(request || '').trim()}`,
|
|
51
|
-
'',
|
|
52
|
-
'Prefer tasks that can run at the same time; use dependsOn only when a task truly needs another\'s findings. Do not assign a role a task it has no tools for. For each task, say which of the role\'s tools it will need (grants) and why — do not miss a tool that would help.',
|
|
53
|
-
'',
|
|
54
|
-
describeSchema(TEAM_PLAN_SCHEMA),
|
|
55
|
-
].join('\n');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** One task per role, in declaration order, with the roles' own dependencies. */
|
|
59
|
-
export function fixedPlan(team, request) {
|
|
60
|
-
const req = String(request || '').trim();
|
|
61
|
-
// The judge's work IS the merge: it reads the whole board and writes the answer. Giving it
|
|
62
|
-
// a task as well made a planner plan twice — once blind, in the first wave, and once at
|
|
63
|
-
// the end — and the first, done with no findings to read, was pure duplicate research.
|
|
64
|
-
const judge = team.merge === 'judge' ? team.judge : null;
|
|
65
|
-
return (team.roles || []).filter((r) => r.id !== judge || (team.roles || []).length === 1).map((r) => ({
|
|
66
|
-
id: `t_${r.id}`,
|
|
67
|
-
role: r.id,
|
|
68
|
-
title: r.name || r.id,
|
|
69
|
-
prompt: r.prompt ? `${r.prompt}\n\nRequest: ${req}` : req,
|
|
70
|
-
dependsOn: (r.dependsOn || []).map((d) => `t_${d}`),
|
|
71
|
-
}));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Read a planner's answer into tasks, dropping what cannot run: an unknown role, a
|
|
76
|
-
* dependency on nothing, a cycle. Returns [] when nothing survives, so the caller falls back.
|
|
77
|
-
*/
|
|
78
|
-
export function parsePlan(text, team) {
|
|
79
|
-
const got = coerce(text, TEAM_PLAN_SCHEMA);
|
|
80
|
-
const roles = new Set((team.roles || []).map((r) => r.id));
|
|
81
|
-
const raw = Array.isArray(got?.value?.tasks) ? got.value.tasks : [];
|
|
82
|
-
const tasks = raw
|
|
83
|
-
.filter((t) => t && roles.has(String(t.role || '')) && String(t.prompt || '').trim())
|
|
84
|
-
.map((t, i) => ({
|
|
85
|
-
id: String(t.id || `t${i + 1}`).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 32) || `t${i + 1}`,
|
|
86
|
-
role: String(t.role),
|
|
87
|
-
title: String(t.title || t.prompt).slice(0, 80),
|
|
88
|
-
prompt: String(t.prompt).trim(),
|
|
89
|
-
dependsOn: Array.isArray(t.dependsOn) ? t.dependsOn.map(String) : [],
|
|
90
|
-
...(Array.isArray(t.grants) && t.grants.length ? { grants: [...new Set(t.grants.map((g) => String(g).trim().toLowerCase()).filter((g) => GRANT_RE.test(g) && g !== 'none'))].slice(0, 6), ...(t.why ? { why: String(t.why).slice(0, 160) } : {}) } : {}),
|
|
91
|
-
}))
|
|
92
|
-
.slice(0, MAX_TASKS);
|
|
93
|
-
const ids = new Set(tasks.map((t) => t.id));
|
|
94
|
-
for (const t of tasks) t.dependsOn = t.dependsOn.filter((d) => ids.has(d) && d !== t.id);
|
|
95
|
-
return breakCycles(tasks);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** A task whose dependencies cannot all be satisfied loses them, rather than never running. */
|
|
99
|
-
export function breakCycles(tasks) {
|
|
100
|
-
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
101
|
-
const state = new Map(); // id -> 'visiting' | 'done'
|
|
102
|
-
const visit = (t, stack) => {
|
|
103
|
-
if (state.get(t.id) === 'done') return;
|
|
104
|
-
if (state.get(t.id) === 'visiting') return;
|
|
105
|
-
state.set(t.id, 'visiting');
|
|
106
|
-
t.dependsOn = t.dependsOn.filter((d) => {
|
|
107
|
-
const dep = byId.get(d);
|
|
108
|
-
if (!dep) return false;
|
|
109
|
-
if (state.get(d) === 'visiting' || stack.has(d)) return false; // a cycle: drop the edge
|
|
110
|
-
visit(dep, new Set([...stack, t.id]));
|
|
111
|
-
return true;
|
|
112
|
-
});
|
|
113
|
-
state.set(t.id, 'done');
|
|
114
|
-
};
|
|
115
|
-
for (const t of tasks) visit(t, new Set());
|
|
116
|
-
return tasks;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Tasks in waves: everything runnable now, then what those unblock — the barrier order. */
|
|
120
|
-
export function waves(tasks) {
|
|
121
|
-
const out = [];
|
|
122
|
-
const done = new Set();
|
|
123
|
-
let rest = [...tasks];
|
|
124
|
-
while (rest.length) {
|
|
125
|
-
const ready = rest.filter((t) => (t.dependsOn || []).every((d) => done.has(d)));
|
|
126
|
-
if (!ready.length) { out.push(rest); break; } // cannot happen after breakCycles; never loop forever
|
|
127
|
-
out.push(ready);
|
|
128
|
-
for (const t of ready) done.add(t.id);
|
|
129
|
-
rest = rest.filter((t) => !done.has(t.id));
|
|
130
|
-
}
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
5
|
+
Request: ${s}`:s,dependsOn:(e.dependsOn||[]).map(i=>`t_${i}`)}))}function k(r,o){const s=u(r,h),n=new Set((o.roles||[]).map(t=>t.id)),i=(Array.isArray(s?.value?.tasks)?s.value.tasks:[]).filter(t=>t&&n.has(String(t.role||""))&&String(t.prompt||"").trim()).map((t,d)=>({id:String(t.id||`t${d+1}`).replace(/[^a-zA-Z0-9_-]/g,"").slice(0,32)||`t${d+1}`,role:String(t.role),title:String(t.title||t.prompt).slice(0,80),prompt:String(t.prompt).trim(),dependsOn:Array.isArray(t.dependsOn)?t.dependsOn.map(String):[],...Array.isArray(t.grants)&&t.grants.length?{grants:[...new Set(t.grants.map(l=>String(l).trim().toLowerCase()).filter(l=>g.test(l)&&l!=="none"))].slice(0,6),...t.why?{why:String(t.why).slice(0,160)}:{}}:{}})).slice(0,p),a=new Set(i.map(t=>t.id));for(const t of i)t.dependsOn=t.dependsOn.filter(d=>a.has(d)&&d!==t.id);return f(i)}function f(r){const o=new Map(r.map(e=>[e.id,e])),s=new Map,n=(e,i)=>{s.get(e.id)!=="done"&&s.get(e.id)!=="visiting"&&(s.set(e.id,"visiting"),e.dependsOn=e.dependsOn.filter(a=>{const t=o.get(a);return!t||s.get(a)==="visiting"||i.has(a)?!1:(n(t,new Set([...i,e.id])),!0)}),s.set(e.id,"done"))};for(const e of r)n(e,new Set);return r}function b(r){const o=[],s=new Set;let n=[...r];for(;n.length;){const e=n.filter(i=>(i.dependsOn||[]).every(a=>s.has(a)));if(!e.length){o.push(n);break}o.push(e);for(const i of e)s.add(i.id);n=n.filter(i=>!s.has(i.id))}return o}export{p as MAX_TASKS,h as TEAM_PLAN_SCHEMA,f as breakCycles,x as fixedPlan,k as parsePlan,S as plannerPrompt,b as waves};
|