@chatpanel/events 0.99.0 → 0.101.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/index.js +2 -1
- package/package.json +3 -1
- package/slash-commands.js +4 -1
- package/team-observe.js +118 -0
- package/team-org.js +37 -0
package/index.js
CHANGED
|
@@ -238,7 +238,8 @@ export { workLogFor, workLogText, workLogEvidence, describeCall, WORKLOG_KINDS }
|
|
|
238
238
|
export { normalizeRequest, subtaskFromRequest, takeUp, takeUpLine, holdsGrants, jobFromSubtask, extendDependents, taskTree, threadRows, MAX_SUBTASKS, MAX_DEPTH, MIN_TAKEUP_FIT } from './team-subtask.js';
|
|
239
239
|
export { teamLine, teamLanes } from './team-trail.js';
|
|
240
240
|
// The org, derived (F8 §17): roles as cards, starters whole, a team's health and shape, the roster, one colour per agent.
|
|
241
|
-
export { promoteRoles, starterTeam, missingStarters, teamHealth, teamShape, describeTeamShape, whereItWorks, rosterRows, agentKind, agentHue, agentColor, agentInitials, roleCardId, cardNumbers, upsertAgents, TEAM_SHAPES, ROSTER_KINDS } from './team-org.js';
|
|
241
|
+
export { promoteRoles, starterTeam, missingStarters, teamHealth, teamShape, describeTeamShape, whereItWorks, rosterRows, agentKind, agentHue, agentColor, agentInitials, roleCardId, cardNumbers, upsertAgents, soloTeam, teamsWithSolos, SOLO_BUDGET, TEAM_SHAPES, ROSTER_KINDS } from './team-org.js';
|
|
242
|
+
export { observeInbox, observeStrip, runLanes, spendRows, engineStrip } from './team-observe.js';
|
|
242
243
|
export { mcpDispatchProvider, MCP_TOOL_NAME } from './mcp-dispatch.js';
|
|
243
244
|
export { createManifest, ManifestError, SOURCES } from './manifest.js';
|
|
244
245
|
export { createKernel, meetDecisions, KernelError, REQUIRED_PLUGINS, ALLOW_ALL } from './kernel.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.101.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts — typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
"./team-worklog.js": "./team-worklog.js",
|
|
116
116
|
"./team.js": "./team.js",
|
|
117
117
|
"./team-org.js": "./team-org.js",
|
|
118
|
+
"./team-observe.js": "./team-observe.js",
|
|
118
119
|
"./text-search.js": "./text-search.js",
|
|
119
120
|
"./theme.js": "./theme.js",
|
|
120
121
|
"./titles.js": "./titles.js",
|
|
@@ -255,6 +256,7 @@
|
|
|
255
256
|
"team-worklog.js",
|
|
256
257
|
"team.js",
|
|
257
258
|
"team-org.js",
|
|
259
|
+
"team-observe.js",
|
|
258
260
|
"text-search.js",
|
|
259
261
|
"theme.js",
|
|
260
262
|
"titles.js",
|
package/slash-commands.js
CHANGED
|
@@ -40,7 +40,9 @@ function recipeItem(recipe) {
|
|
|
40
40
|
// A saved TEAM answers to a slash the same way: `/research <request>` is a request to run
|
|
41
41
|
// it, and the `team` tool does the rest.
|
|
42
42
|
function teamItem(team) {
|
|
43
|
-
|
|
43
|
+
// A solo team (team-org.js soloTeam) IS an agent, invokable on its own: drawn as one.
|
|
44
|
+
const solo = !!team.origin?.agent;
|
|
45
|
+
return { type: 'team', command: team.name || '', icon: solo ? '🧑💻' : '🧑🤝🧑', description: team.description || (solo ? 'Agent' : 'Agent team'), team, ...(solo ? { agent: team.origin.agent } : {}) };
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/** Skills that are switched on. Absence of the flag means enabled (older records have none). */
|
|
@@ -119,6 +121,7 @@ export function matchSlashTeam(text, teams = []) {
|
|
|
119
121
|
/** What the model receives for a team command: a request to run it, never a prompt expansion. */
|
|
120
122
|
export function teamInvocationText(team, args = '') {
|
|
121
123
|
const a = String(args || '').trim();
|
|
124
|
+
if (team.origin?.agent) return `Run the agent "${team.name}" (a one-role team of that name)${a ? ` on this request: ${a}` : ''}. Use the team tool; if the request is unclear, ask first.`;
|
|
122
125
|
return `Run the saved team "${team.name}"${a ? ` on this request: ${a}` : ''}. Use the team tool; if the request is unclear, ask first.`;
|
|
123
126
|
}
|
|
124
127
|
|
package/team-observe.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Observe (F8 §17.2) — what is happening, where the time went, what needs you: derived from
|
|
2
|
+
// the run store's rows and records and the engine ledger's cards, once, here, so the desktop's
|
|
3
|
+
// Observe tab, the extension's and a phone's read the same inbox, the same lanes and the
|
|
4
|
+
// same spend rows. Nothing renders; the honesty rule holds — a number is given only where
|
|
5
|
+
// it is known (a relayed agent tool reports no tokens: its row says time, never $0).
|
|
6
|
+
|
|
7
|
+
import { runState, spendOf, LIVE_RUN_STATUSES } from './team-record.js';
|
|
8
|
+
|
|
9
|
+
const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The inbox: every run that needs a person — an ask waiting (`waiting` or a `waiting` count
|
|
13
|
+
* on the row), a live run gone quiet (stalled) — newest first, each with the one action.
|
|
14
|
+
* `runs` are the store's list rows (`GET /v1/teams/runs`) or folded records.
|
|
15
|
+
*/
|
|
16
|
+
export function observeInbox(runs = [], { now = Date.now() } = {}) {
|
|
17
|
+
const items = [];
|
|
18
|
+
for (const r of Array.isArray(runs) ? runs : []) {
|
|
19
|
+
if (!r?.id) continue;
|
|
20
|
+
const st = runState(r, { now });
|
|
21
|
+
const waiting = r.status === 'waiting' || num(r.waiting) > 0 || st.key === 'waiting';
|
|
22
|
+
if (waiting) items.push({ kind: 'ask', runId: r.id, team: r.team || '', request: String(r.request || '').slice(0, 120), count: Math.max(1, num(r.waiting)), action: 'answer', at: num(r.lastEventAt) || num(r.createdAt), detail: st.detail || '' });
|
|
23
|
+
else if (st.key === 'stalled') items.push({ kind: 'stalled', runId: r.id, team: r.team || '', request: String(r.request || '').slice(0, 120), action: 'resume', at: num(r.lastEventAt) || num(r.createdAt), detail: st.detail || '' });
|
|
24
|
+
}
|
|
25
|
+
return items.sort((a, b) => (a.kind === b.kind ? b.at - a.at : a.kind === 'ask' ? -1 : 1));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The strip's numbers: live runs, stalled, asks waiting, tasks done this window, rotations seen. */
|
|
29
|
+
export function observeStrip(runs = [], { now = Date.now(), sinceMs = 7 * 24 * 3600 * 1000 } = {}) {
|
|
30
|
+
const list = (Array.isArray(runs) ? runs : []).filter((r) => r?.id && num(r.createdAt) >= now - sinceMs);
|
|
31
|
+
const live = list.filter((r) => LIVE_RUN_STATUSES.includes(r.status));
|
|
32
|
+
const stalled = live.filter((r) => runState(r, { now }).key === 'stalled').length;
|
|
33
|
+
const asks = observeInbox(list, { now }).filter((i) => i.kind === 'ask').reduce((n, i) => n + i.count, 0);
|
|
34
|
+
const tasks = list.reduce((a, r) => { for (const t of r.tasks || []) { if (t.status === 'ok') a.done += 1; else if (t.status === 'failed' || t.status === 'unassigned') a.failed += 1; } return a; }, { done: 0, failed: 0 });
|
|
35
|
+
const spend = list.reduce((a, r) => { const s = r.usage?.spent || {}; a.usd += num(s.usd); a.ms += num(s.ms); a.tokens += num(s.tokens); return a; }, { usd: 0, ms: 0, tokens: 0 });
|
|
36
|
+
return { runs: list.length, live: live.length, stalled, asks, tasks, spend, completed: list.filter((r) => r.status === 'completed').length };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One run's timeline: a lane per task with segments placed on the run's own clock (0..1 of
|
|
41
|
+
* `span`), so a client draws bars without knowing the record. Segments: `working` (started →
|
|
42
|
+
* ended or now), `waiting` (an ask), `declined` (an attempt that failed and rotated),
|
|
43
|
+
* `earlier` (a task that ended before the last start of a live run, drawn grey). A sub-task
|
|
44
|
+
* carries `parent` for nesting.
|
|
45
|
+
*/
|
|
46
|
+
export function runLanes(run, { now = Date.now() } = {}) {
|
|
47
|
+
const has = (v) => Number.isFinite(Number(v)) && v !== null && v !== undefined && v !== '';
|
|
48
|
+
const start = has(run?.startedAt) ? Number(run.startedAt) : has(run?.createdAt) ? Number(run.createdAt) : now;
|
|
49
|
+
const live = LIVE_RUN_STATUSES.includes(run?.status);
|
|
50
|
+
const end = live ? now : Math.max(start + 1, num(run?.endedAt) || num(run?.lastEventAt) || now);
|
|
51
|
+
const span = Math.max(1, end - start);
|
|
52
|
+
const at = (t) => Math.max(0, Math.min(1, (t - start) / span));
|
|
53
|
+
const lanes = [];
|
|
54
|
+
for (const t of run?.tasks || []) {
|
|
55
|
+
const segs = [];
|
|
56
|
+
const s0 = has(t.startedAt) ? Number(t.startedAt) : null;
|
|
57
|
+
const e0 = has(t.endedAt) ? Number(t.endedAt) : (t.status === 'running' || t.status === 'waiting' ? end : null);
|
|
58
|
+
for (const a of t.attempts || []) {
|
|
59
|
+
if (!has(a.startedAt) || !has(a.endedAt) || !(a.status === 'failed' || a.rotated)) continue;
|
|
60
|
+
const as = Number(a.startedAt), ae = Number(a.endedAt);
|
|
61
|
+
segs.push({ kind: 'declined', from: at(as), to: at(Math.max(ae, as + span / 200)), label: a.error || a.model || 'declined' });
|
|
62
|
+
}
|
|
63
|
+
if (s0 !== null && e0 !== null) segs.push({ kind: t.status === 'waiting' ? 'waiting' : 'working', from: at(s0), to: at(Math.max(e0, s0 + span / 200)), label: t.status });
|
|
64
|
+
const ms = s0 !== null ? (e0 ?? end) - s0 : num(t.ms);
|
|
65
|
+
lanes.push({ id: t.id, role: t.role || null, title: t.title || t.id, status: t.status, parent: t.parent || null, kind: t.kind || 'task', findings: num(t.findings), ms, segments: segs });
|
|
66
|
+
}
|
|
67
|
+
return { start, end, span, live, lanes };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Spend by agent (the role's card id when the role names one, else the role), by team, or
|
|
72
|
+
* by engine — from the run rows' `usage.spent` and each task's share where the record has
|
|
73
|
+
* it. `usd` is `null`, not 0, when no run priced it; `ms` is always known.
|
|
74
|
+
*/
|
|
75
|
+
export function spendRows(runs = [], { by = 'team', now = Date.now() } = {}) {
|
|
76
|
+
const rows = new Map();
|
|
77
|
+
const add = (key, label, spent, { priced }) => {
|
|
78
|
+
const r = rows.get(key) || { key, label, ms: 0, tokens: 0, usd: null, calls: 0, runs: 0 };
|
|
79
|
+
r.ms += num(spent.ms); r.tokens += num(spent.tokens); r.calls += num(spent.calls); r.runs += 1;
|
|
80
|
+
if (priced) r.usd = num(r.usd) + num(spent.usd);
|
|
81
|
+
rows.set(key, r);
|
|
82
|
+
};
|
|
83
|
+
for (const run of Array.isArray(runs) ? runs : []) {
|
|
84
|
+
if (!run?.id) continue;
|
|
85
|
+
const s = spendOf(run, { now })?.spent || run.usage?.spent || {};
|
|
86
|
+
const priced = num(s.usd) > 0 || num(s.tokens) > 0;
|
|
87
|
+
if (by === 'team') add(run.team || '?', run.team || '?', s, { priced });
|
|
88
|
+
else if (by === 'agent') {
|
|
89
|
+
const tasks = (run.tasks || []).filter((t) => t.role);
|
|
90
|
+
const share = tasks.length ? 1 / tasks.length : 0;
|
|
91
|
+
for (const t of tasks) {
|
|
92
|
+
const role = (run.roles || []).find((r) => r.id === t.role);
|
|
93
|
+
const key = role?.agent || `${run.team || '?'}-${t.role}`;
|
|
94
|
+
add(key, role?.agent || t.role, { ms: num(t.ms) || num(s.ms) * share, tokens: num(s.tokens) * share, usd: num(s.usd) * share, calls: num(s.calls) * share }, { priced });
|
|
95
|
+
}
|
|
96
|
+
} else if (by === 'engine') {
|
|
97
|
+
for (const t of run.tasks || []) { const k = t.engine ? `${t.engine.kind}:${t.engine.id}${t.engine.model ? `/${t.engine.model}` : ''}` : (t.model || '?'); add(k, k, { ms: num(t.ms), tokens: 0, usd: 0, calls: 0 }, { priced: false }); }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const out = [...rows.values()].sort((a, b) => (b.usd ?? 0) - (a.usd ?? 0) || b.ms - a.ms);
|
|
101
|
+
const max = Math.max(1, ...out.map((r) => (r.usd ?? 0) || 0), ...out.map((r) => r.ms / 60000));
|
|
102
|
+
return out.map((r) => ({ ...r, share: r.usd != null && r.usd > 0 ? r.usd / max : (r.ms / 60000) / max }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** An engine card as one strip: 24 hour cells (`ok` · `declined` · `none`), the rate, and whether it is declining now. */
|
|
106
|
+
export function engineStrip(card) {
|
|
107
|
+
const a = card?.availability || {};
|
|
108
|
+
const hours = Array.isArray(a.byHour) ? a.byHour : Array.from({ length: 24 }, () => ({ calls: 0, declines: 0 }));
|
|
109
|
+
return {
|
|
110
|
+
key: card?.key || '?',
|
|
111
|
+
label: card?.engine ? `${card.engine.id}${card.engine.model ? ` · ${card.engine.model}` : ''}` : (card?.key || '?'),
|
|
112
|
+
rate: a.rate ?? null,
|
|
113
|
+
decliningNow: !!a.decliningNow,
|
|
114
|
+
p50: card?.latency?.total?.p50 ?? null,
|
|
115
|
+
calls: num(card?.calls),
|
|
116
|
+
cells: hours.map((h) => (num(h.declines) > 0 && num(h.declines) >= num(h.calls) ? 'declined' : num(h.calls) > 0 ? 'ok' : 'none')),
|
|
117
|
+
};
|
|
118
|
+
}
|
package/team-org.js
CHANGED
|
@@ -309,3 +309,40 @@ export function upsertAgents(pool, cards) {
|
|
|
309
309
|
const add = (Array.isArray(cards) ? cards : []).filter((c) => c && c.id);
|
|
310
310
|
return [...list.map((a) => add.find((c) => c.id === a.id) || a), ...add.filter((c) => !list.some((a) => a.id === c.id))];
|
|
311
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.id === ASSISTANT_ID || 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
|
+
* The teams a chat can run: the saved ones, then a solo team per enabled pool agent whose
|
|
336
|
+
* id no saved team already claims. What the slash menu, the `team` tool and "run …" all read.
|
|
337
|
+
*/
|
|
338
|
+
export function teamsWithSolos(teams = [], pool = [], opts = {}) {
|
|
339
|
+
const saved = (Array.isArray(teams) ? teams : []).filter((t) => t && t.name);
|
|
340
|
+
const taken = new Set(saved.map((t) => String(t.name).toLowerCase()));
|
|
341
|
+
const solos = [];
|
|
342
|
+
for (const a of poolList(pool)) {
|
|
343
|
+
if (taken.has(String(a.id).toLowerCase()) || !(Array.isArray(a.appliesTo) ? a.appliesTo : ['jobs']).includes('jobs')) continue;
|
|
344
|
+
const t = soloTeam(a, opts);
|
|
345
|
+
if (t) { solos.push(t); taken.add(t.name); }
|
|
346
|
+
}
|
|
347
|
+
return [...saved, ...solos];
|
|
348
|
+
}
|