@chatpanel/events 0.89.4 → 0.90.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 +1 -0
- package/package.json +3 -1
- package/team-run.js +11 -4
- package/team-task.js +12 -1
- package/team-worklog.js +146 -0
package/index.js
CHANGED
|
@@ -213,6 +213,7 @@ export { SCM_KINDS, validateConnection, normalizeConnection, parseRemote, connec
|
|
|
213
213
|
export { messagesFor, mergeTranscript, clipTranscript, clipMessage, newSteps, continuationNote, createControl, STEP_MAX_CHARS, TASK_TRANSCRIPT_MAX_CHARS } from './team-task.js';
|
|
214
214
|
export { runTeam, resumeTeam, dryRunTeam, isModelUnavailable, TeamRunError, RUN_STATUSES } from './team-run.js';
|
|
215
215
|
export { teamToolProvider, teamToolSpec, teamToolTimeoutMs, describeTeamForApproval, TEAM_TOOL_NAME } from './team-tool.js';
|
|
216
|
+
export { workLogFor, workLogText, workLogEvidence, describeCall, WORKLOG_KINDS } from './team-worklog.js';
|
|
216
217
|
export { teamLine, teamLanes } from './team-trail.js';
|
|
217
218
|
export { mcpDispatchProvider, MCP_TOOL_NAME } from './mcp-dispatch.js';
|
|
218
219
|
export { createManifest, ManifestError, SOURCES } from './manifest.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.90.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts \u2014 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",
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"./team-run.js": "./team-run.js",
|
|
108
108
|
"./team-tool.js": "./team-tool.js",
|
|
109
109
|
"./team-trail.js": "./team-trail.js",
|
|
110
|
+
"./team-worklog.js": "./team-worklog.js",
|
|
110
111
|
"./team.js": "./team.js",
|
|
111
112
|
"./text-search.js": "./text-search.js",
|
|
112
113
|
"./theme.js": "./theme.js",
|
|
@@ -238,6 +239,7 @@
|
|
|
238
239
|
"team-run.js",
|
|
239
240
|
"team-tool.js",
|
|
240
241
|
"team-trail.js",
|
|
242
|
+
"team-worklog.js",
|
|
241
243
|
"team.js",
|
|
242
244
|
"text-search.js",
|
|
243
245
|
"theme.js",
|
package/team-run.js
CHANGED
|
@@ -23,7 +23,7 @@ import { fixedPlan, plannerPrompt, parsePlan, waves } from './team-plan.js';
|
|
|
23
23
|
import { createBoard, parseFindings, boardText, findingsInstruction, toBriefClaims, RUNNER } from './team-board.js';
|
|
24
24
|
import { boardToolProvider, createAnswerBox, withBoardTool, DEFAULT_ASK_TIMEOUT_MS } from './board-tool.js';
|
|
25
25
|
import { createRunCache, withRunCache } from './team-cache.js';
|
|
26
|
-
import { messagesFor, mergeTranscript, clipTranscript, clipMessage as clipTranscriptOne, newSteps, continuationNote } from './team-task.js';
|
|
26
|
+
import { messagesFor, mergeTranscript, clipTranscript, clipMessage as clipTranscriptOne, newSteps, continuationNote, isThought } from './team-task.js';
|
|
27
27
|
import { converge } from './promotion.js';
|
|
28
28
|
|
|
29
29
|
export const RUN_STATUSES = Object.freeze(['planning', 'running', 'merging', 'waiting', 'completed', 'partial', 'over-budget', 'stopped', 'failed']);
|
|
@@ -239,9 +239,13 @@ export async function runTeam({
|
|
|
239
239
|
if (req.type !== 'handoff' || req.taskId !== task.id) return false;
|
|
240
240
|
handoffTo = req; taskAc.abort(); return true;
|
|
241
241
|
}) || null;
|
|
242
|
+
// Every recorded step says when and under which attempt — the board's work log and the
|
|
243
|
+
// scorecard read the task by these, so a step reported after the fact is stamped now.
|
|
244
|
+
let attemptNo = 0;
|
|
245
|
+
const stamp = (m) => clipTranscriptOne({ ...m, at: Number.isFinite(m?.at) ? m.at : now(), attempt: Number.isFinite(m?.attempt) ? m.attempt : attemptNo });
|
|
242
246
|
const recordSteps = (before, after) => {
|
|
243
247
|
const added = newSteps(before, after);
|
|
244
|
-
if (added.length) say('task.step', { taskId: task.id, role: role.id, steps: added.map(
|
|
248
|
+
if (added.length) say('task.step', { taskId: task.id, role: role.id, steps: added.map(stamp) });
|
|
245
249
|
};
|
|
246
250
|
try {
|
|
247
251
|
if (role.mode === 'recipe') {
|
|
@@ -299,13 +303,16 @@ export async function runTeam({
|
|
|
299
303
|
routed = routeOf(m, role, { attempt, exclude, handoff: handoffNow });
|
|
300
304
|
say('task.routed', { taskId: task.id, role: role.id, attempt, ...routed });
|
|
301
305
|
attempts.push({ model: m.model, engine: routed.engine, at: now(), continued: !!note });
|
|
306
|
+
attemptNo = attempts.length;
|
|
302
307
|
const sent = messagesFor({ transcript }, { prompt, note });
|
|
303
308
|
// The record grows AS THE ATTEMPT GOES: a host that reports each wire message the
|
|
304
309
|
// moment it exists (a tool call, its result) puts it on the record then, so a
|
|
305
310
|
// process that dies mid-attempt leaves the work so far behind it, not nothing.
|
|
306
311
|
let live = sent;
|
|
307
|
-
|
|
308
|
-
const
|
|
312
|
+
// (thoughts are on the record but not on the wire, so compare against the wire's view)
|
|
313
|
+
const wireBefore = transcript.filter((m) => !isThought(m));
|
|
314
|
+
if (sent.length > wireBefore.length) recordSteps(wireBefore, sent);
|
|
315
|
+
const onStep = (message) => { if (message && message.role) { live = [...live, message]; say('task.step', { taskId: task.id, role: role.id, steps: [stamp(message)] }); } };
|
|
309
316
|
const res = await callModel({
|
|
310
317
|
runId: id, taskId: task.id, role: role.id, model: m.model, mode: m.mode || role.mode,
|
|
311
318
|
system: role.prompt, prompt, messages: sent, tools, signal: taskAc.signal,
|
package/team-task.js
CHANGED
|
@@ -26,9 +26,20 @@ export function clipMessage(m) {
|
|
|
26
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
27
|
if (m.tool_call_id) out.tool_call_id = m.tool_call_id;
|
|
28
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;
|
|
29
35
|
return out;
|
|
30
36
|
}
|
|
31
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
|
+
|
|
32
43
|
/** The record's copy of a transcript: clipped per message and bounded as a whole (oldest tool traffic goes first). */
|
|
33
44
|
export function clipTranscript(messages) {
|
|
34
45
|
const list = (Array.isArray(messages) ? messages : []).filter((m) => m && m.role && m.role !== 'system').map(clipMessage);
|
|
@@ -67,7 +78,7 @@ export function continuationNote({ kind = 'handoff', from = '', to = '', reason
|
|
|
67
78
|
* it first) so a transcript never carries a role prompt that a later role might not share.
|
|
68
79
|
*/
|
|
69
80
|
export function messagesFor(task, { prompt, note = null } = {}) {
|
|
70
|
-
const transcript = Array.isArray(task?.transcript) ? task.transcript : [];
|
|
81
|
+
const transcript = (Array.isArray(task?.transcript) ? task.transcript : []).filter((m) => !isThought(m));
|
|
71
82
|
if (!transcript.length) return [{ role: 'user', content: String(prompt || '') }];
|
|
72
83
|
const last = transcript[transcript.length - 1];
|
|
73
84
|
// A transcript that ends in an unanswered tool call cannot be continued as-is: close it.
|
package/team-worklog.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// The work log — a task's thread as the record of the work, not just its posts.
|
|
2
|
+
//
|
|
3
|
+
// A task is a conversation (team-task.js): every step is on the record as it happens — the
|
|
4
|
+
// prompt, the member's text, its reasoning when the model streams it, each tool call and
|
|
5
|
+
// what came back — beside the attempts (which model, when, how it ended), the hand-offs, the
|
|
6
|
+
// posts in its thread, and how the task ended. The board drew the posts alone, and a task
|
|
7
|
+
// that failed before its first post looked like nothing had happened. This folds all of it
|
|
8
|
+
// into ONE ordered timeline, the same in both clients, so what a member did, tried, was told
|
|
9
|
+
// and produced is read in one place — by a person, by the next wave, and by whoever rates
|
|
10
|
+
// the member (evidence first: calls made, findings kept, models burned, budget spent).
|
|
11
|
+
//
|
|
12
|
+
// Pure: a run record (team-record.js) in, entries out. No rendering here.
|
|
13
|
+
|
|
14
|
+
import { isThought } from './team-task.js';
|
|
15
|
+
|
|
16
|
+
export const WORKLOG_KINDS = Object.freeze(['attempt', 'prompt', 'note', 'thought', 'text', 'call', 'result', 'handoff', 'post', 'end']);
|
|
17
|
+
|
|
18
|
+
const num = (v, d = 0) => (Number.isFinite(v) ? v : d);
|
|
19
|
+
const str = (v) => (v == null ? '' : typeof v === 'string' ? v : JSON.stringify(v));
|
|
20
|
+
|
|
21
|
+
function parseArgs(raw) {
|
|
22
|
+
if (raw && typeof raw === 'object') return raw;
|
|
23
|
+
try { return JSON.parse(String(raw || '{}')); } catch { return { raw: String(raw || '') }; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One line for a tool call: `find web_search "…"` — the dispatcher's action stands in front. */
|
|
27
|
+
export function describeCall(call) {
|
|
28
|
+
const name = call?.function?.name || call?.name || 'tool';
|
|
29
|
+
const args = parseArgs(call?.function?.arguments ?? call?.arguments);
|
|
30
|
+
const action = args?.action ? ` ${args.action}` : '';
|
|
31
|
+
const inner = args?.args && typeof args.args === 'object' ? args.args : args;
|
|
32
|
+
const first = inner && typeof inner === 'object' ? Object.entries(inner).find(([k, v]) => k !== 'action' && k !== 'args' && (typeof v === 'string' || typeof v === 'number')) : null;
|
|
33
|
+
return `${name}${action}${first ? ` ${first[0]}=${JSON.stringify(String(first[1]).slice(0, 120))}` : ''}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The timeline of one task: `[{ kind, at, by, attempt, ... }]`, oldest first. Every entry has
|
|
38
|
+
* an `at`; a step recorded without one (an older build) inherits its attempt's, so the order
|
|
39
|
+
* still holds. `by` is the role for what the member did, `runner` for the runner's lines, and
|
|
40
|
+
* whoever posted for a post.
|
|
41
|
+
*/
|
|
42
|
+
export function workLogFor(run, taskId) {
|
|
43
|
+
const task = (run?.tasks || []).find((t) => t.id === taskId);
|
|
44
|
+
if (!task) return [];
|
|
45
|
+
const by = task.role || task.id;
|
|
46
|
+
const out = [];
|
|
47
|
+
const attempts = Array.isArray(task.attempts) ? task.attempts : [];
|
|
48
|
+
const baseAt = num(task.startedAt, num(attempts[0]?.at, num(run?.startedAt, 0)));
|
|
49
|
+
attempts.forEach((a, i) => out.push({ kind: 'attempt', at: num(a.at, baseAt + i), by: 'runner', attempt: i + 1, model: a.model || '', engine: a.engine || null, continued: !!a.continued, status: a.status || null, error: a.error || null }));
|
|
50
|
+
|
|
51
|
+
// Steps: stamped ones sort by their time; unstamped ones follow their attempt in order.
|
|
52
|
+
const steps = Array.isArray(task.transcript) ? task.transcript : [];
|
|
53
|
+
const calls = new Map();
|
|
54
|
+
let lastAt = baseAt;
|
|
55
|
+
let attemptOf = 1;
|
|
56
|
+
steps.forEach((m, i) => {
|
|
57
|
+
if (!m || !m.role) return;
|
|
58
|
+
if (Number.isFinite(m.attempt)) attemptOf = m.attempt;
|
|
59
|
+
else if (attempts.length) attemptOf = Math.max(attemptOf, 1 + attempts.findLastIndex((x) => num(x.at, 0) <= lastAt + 1));
|
|
60
|
+
// Unstamped: after everything before it, and after its attempt began.
|
|
61
|
+
const at = Number.isFinite(m.at) ? m.at : Math.max(lastAt, num(attempts[attemptOf - 1]?.at, 0)) + 1;
|
|
62
|
+
lastAt = Math.max(lastAt, at);
|
|
63
|
+
const attempt = attemptOf;
|
|
64
|
+
if (m.role === 'user') {
|
|
65
|
+
out.push({ kind: i === 0 ? 'prompt' : 'note', at, by: 'runner', attempt, text: str(m.content) });
|
|
66
|
+
} else if (m.role === 'assistant') {
|
|
67
|
+
if (isThought(m)) { out.push({ kind: 'thought', at, by, attempt, text: m.thought }); return; }
|
|
68
|
+
if (typeof m.content === 'string' && m.content.trim()) out.push({ kind: 'text', at, by, attempt, text: m.content });
|
|
69
|
+
for (const c of Array.isArray(m.tool_calls) ? m.tool_calls : []) {
|
|
70
|
+
const id = c.id || `c${i}`;
|
|
71
|
+
calls.set(id, c);
|
|
72
|
+
out.push({ kind: 'call', at, by, attempt, callId: id, name: c.function?.name || 'tool', args: parseArgs(c.function?.arguments), text: describeCall(c) });
|
|
73
|
+
}
|
|
74
|
+
} else if (m.role === 'tool') {
|
|
75
|
+
const c = m.tool_call_id ? calls.get(m.tool_call_id) : null;
|
|
76
|
+
out.push({ kind: 'result', at, by: 'tool', attempt, callId: m.tool_call_id || null, name: c?.function?.name || m.name || 'tool', text: str(m.content), error: /^error[:\s]/i.test(str(m.content)) });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
for (const h of Array.isArray(task.handoffs) ? task.handoffs : []) out.push({ kind: 'handoff', at: num(h.at, lastAt), by: h.by || 'person', from: h.from || '', to: h.to || '', text: h.reason || '' });
|
|
81
|
+
|
|
82
|
+
// The thread's posts — a member's findings, a question, an answer, a decision, the runner's notes.
|
|
83
|
+
const thread = (run?.threads?.threads || []).find((t) => t.taskId === taskId && t.kind === 'task');
|
|
84
|
+
if (thread) for (const p of (run.threads.posts || []).filter((x) => x.threadId === thread.id)) out.push({ kind: 'post', at: num(p.at, lastAt), by: p.by || '', post: p, text: p.text || '' });
|
|
85
|
+
|
|
86
|
+
// What the member is saying right now — the attempt's text before it is a step.
|
|
87
|
+
const lastText = [...out].reverse().find((e) => e.kind === 'text');
|
|
88
|
+
if (task.status === 'running' && task.text && task.text !== lastText?.text) out.push({ kind: 'text', at: num(run?.lastEventAt, lastAt + 1), by, attempt: attemptOf, text: task.text, live: true });
|
|
89
|
+
|
|
90
|
+
if (task.endedAt || ['ok', 'failed', 'over-budget', 'stopped', 'waiting'].includes(task.status)) {
|
|
91
|
+
// Findings: the task's count, or the finding posts in its thread — a member's board posts
|
|
92
|
+
// count as its answer (team-run.js), and a record folded from an older run has only those.
|
|
93
|
+
const findings = Math.max(num(task.findings, 0), out.filter((e) => e.kind === 'post' && e.post?.kind === 'finding').length);
|
|
94
|
+
const tools = Math.max(num(task.tools, 0), out.filter((e) => e.kind === 'call').length);
|
|
95
|
+
out.push({ kind: 'end', at: num(task.endedAt, num(run?.lastEventAt, lastAt + 2)), by: 'runner', status: task.status, error: task.error || null, ms: num(task.ms, 0), findings, tools, text: endText({ ...task, findings, tools }, attempts) });
|
|
96
|
+
}
|
|
97
|
+
return out.sort((a, b) => a.at - b.at || WORKLOG_KINDS.indexOf(a.kind) - WORKLOG_KINDS.indexOf(b.kind));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function endText(task, attempts) {
|
|
101
|
+
const tried = attempts.length > 1 ? ` after ${attempts.length} models (${attempts.map((a) => a.model).join(' → ')})` : '';
|
|
102
|
+
const s = task.status;
|
|
103
|
+
if (s === 'ok') return `done${tried} · ${task.findings || 0} finding${task.findings === 1 ? '' : 's'} · ${task.tools || 0} tool call${task.tools === 1 ? '' : 's'}`;
|
|
104
|
+
if (s === 'waiting') return 'waiting on a person';
|
|
105
|
+
if (s === 'over-budget') return `stopped at the budget${tried}`;
|
|
106
|
+
if (s === 'stopped') return `stopped${tried}`;
|
|
107
|
+
return `failed${tried}${task.error ? `: ${task.error}` : ''}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The log as text — for a brief, a rating, a test: one line per entry, results shortened. */
|
|
111
|
+
export function workLogText(entries, { resultChars = 200 } = {}) {
|
|
112
|
+
return (entries || []).map((e) => {
|
|
113
|
+
switch (e.kind) {
|
|
114
|
+
case 'attempt': return `▸ attempt ${e.attempt}: ${e.model}${e.continued ? ' (continues)' : ''}`;
|
|
115
|
+
case 'prompt': return `▸ task: ${e.text}`;
|
|
116
|
+
case 'note': return `▸ runner → ${e.text}`;
|
|
117
|
+
case 'thought': return `${e.by} (thinking): ${e.text}`;
|
|
118
|
+
case 'text': return `${e.by}: ${e.text}`;
|
|
119
|
+
case 'call': return `${e.by} → ${e.text}`;
|
|
120
|
+
case 'result': return ` ← ${e.name}: ${e.text.length > resultChars ? `${e.text.slice(0, resultChars)}…` : e.text}`;
|
|
121
|
+
case 'handoff': return `▸ handed from ${e.from} to ${e.to} by ${e.by}${e.text ? ` — ${e.text}` : ''}`;
|
|
122
|
+
case 'post': return `${e.by} posted ${e.post?.kind || 'note'}: ${e.text}`;
|
|
123
|
+
case 'end': return `▸ ${e.text}`;
|
|
124
|
+
default: return '';
|
|
125
|
+
}
|
|
126
|
+
}).filter(Boolean).join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* What the log says about the work, as numbers — the evidence a rating starts from, before
|
|
131
|
+
* any opinion (a judge's, a peer's, a person's) is added beside it.
|
|
132
|
+
*/
|
|
133
|
+
export function workLogEvidence(entries) {
|
|
134
|
+
const ev = { calls: 0, results: 0, resultErrors: 0, thoughts: 0, texts: 0, attempts: 0, handoffs: 0, posts: 0, findings: 0, decided: { approved: 0, rejected: 0 }, status: null, ms: 0 };
|
|
135
|
+
for (const e of entries || []) {
|
|
136
|
+
if (e.kind === 'call') ev.calls++;
|
|
137
|
+
else if (e.kind === 'result') { ev.results++; if (e.error) ev.resultErrors++; }
|
|
138
|
+
else if (e.kind === 'thought') ev.thoughts++;
|
|
139
|
+
else if (e.kind === 'text') ev.texts++;
|
|
140
|
+
else if (e.kind === 'attempt') ev.attempts++;
|
|
141
|
+
else if (e.kind === 'handoff') ev.handoffs++;
|
|
142
|
+
else if (e.kind === 'post') { ev.posts++; if (e.post?.kind === 'finding') ev.findings++; if (e.post?.status === 'approved') ev.decided.approved++; if (e.post?.status === 'rejected') ev.decided.rejected++; }
|
|
143
|
+
else if (e.kind === 'end') { ev.status = e.status; ev.ms = e.ms; ev.findings = Math.max(ev.findings, e.findings || 0); }
|
|
144
|
+
}
|
|
145
|
+
return ev;
|
|
146
|
+
}
|