@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-board.js
CHANGED
|
@@ -1,337 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// in its thread, and a later task reads the threads it depends on, threaded and sized like a
|
|
8
|
-
// shielded tool result. A member that disagrees replies where it disagrees; a member that
|
|
9
|
-
// is stuck ASKS, and its task waits for a person — on either client — to answer. A person
|
|
10
|
-
// posting is a member posting, and a person's decision on any post wins.
|
|
11
|
-
//
|
|
12
|
-
// The board is the run's record: durable (every change is an event the run store folds with
|
|
13
|
-
// `foldBoard`), attributable per member, and — after a run — what can become draft briefs
|
|
14
|
-
// and be promoted on convergence (W7), rather than evaporating with the run.
|
|
15
|
-
//
|
|
16
|
-
// Findings are parsed GENEROUSLY from a model's answer through the structured layer, and a
|
|
17
|
-
// task whose answer cannot be read as findings is not lost: its whole answer becomes one
|
|
18
|
-
// `draft` finding. A member that only wrote prose still contributed.
|
|
19
|
-
|
|
20
|
-
import { defineSchema, describeSchema, coerce } from './structured.js';
|
|
21
|
-
|
|
22
|
-
export const FINDING_KINDS = Object.freeze(['claim', 'draft', 'link', 'question', 'answer']);
|
|
23
|
-
export const MAX_FINDINGS_PER_TASK = 40;
|
|
24
|
-
export const BOARD_TEXT_MAX = 12_000;
|
|
25
|
-
|
|
26
|
-
export const FINDINGS_SCHEMA = defineSchema({
|
|
27
|
-
name: 'findings',
|
|
28
|
-
purpose: 'what this task established, each item on its own with where it came from',
|
|
29
|
-
fields: {
|
|
30
|
-
findings: {
|
|
31
|
-
type: 'object[]', required: true, maxItems: MAX_FINDINGS_PER_TASK,
|
|
32
|
-
describe: 'one entry per fact, draft, link or open question — never a paragraph of several',
|
|
33
|
-
fields: {
|
|
34
|
-
kind: { type: 'enum', values: FINDING_KINDS, default: 'claim' },
|
|
35
|
-
text: { type: 'string', required: true, max: 2000 },
|
|
36
|
-
refs: { type: 'string[]', maxItems: 8, describe: 'record ids or URLs this rests on, when any' },
|
|
37
|
-
confidence: { type: 'number', describe: '0–1, how sure' },
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
nothing: { findings: [] },
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
/** The instruction appended to every task so the answer can be read as findings. */
|
|
45
|
-
export function findingsInstruction() {
|
|
46
|
-
return `When you are done, end your answer with your findings in this shape:\n${describeSchema(FINDINGS_SCHEMA)}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Read a task's answer into findings. The JSON block, when present; otherwise the whole
|
|
51
|
-
* answer as one draft — a member that only wrote prose still contributed.
|
|
52
|
-
*/
|
|
53
|
-
export function parseFindings(text, { role, taskId } = {}) {
|
|
54
|
-
const raw = String(text || '').trim();
|
|
55
|
-
if (!raw) return [];
|
|
56
|
-
const got = coerce(raw, FINDINGS_SCHEMA);
|
|
57
|
-
const list = Array.isArray(got?.value?.findings) ? got.value.findings.filter((f) => f && String(f.text || '').trim()) : [];
|
|
58
|
-
const stamp = (f, i) => ({
|
|
59
|
-
id: `${taskId || 't'}:${i + 1}`,
|
|
60
|
-
kind: FINDING_KINDS.includes(f.kind) ? f.kind : 'claim',
|
|
61
|
-
text: String(f.text).trim().slice(0, 2000),
|
|
62
|
-
refs: Array.isArray(f.refs) ? f.refs.map(String).filter(Boolean).slice(0, 8) : [],
|
|
63
|
-
// Absent or zero reads as "not stated": a model that gives no number is not 0% sure.
|
|
64
|
-
confidence: Number.isFinite(Number(f.confidence)) && Number(f.confidence) > 0 ? Math.min(1, Number(f.confidence)) : null,
|
|
65
|
-
role: role || null,
|
|
66
|
-
taskId: taskId || null,
|
|
67
|
-
});
|
|
68
|
-
if (list.length) return list.slice(0, MAX_FINDINGS_PER_TASK).map(stamp);
|
|
69
|
-
// No JSON block: the prose is the finding. Strip a fenced JSON tail that failed to parse.
|
|
70
|
-
const prose = raw.replace(/```json[\s\S]*$/i, '').trim() || raw;
|
|
71
|
-
return [stamp({ kind: 'draft', text: prose.slice(0, 2000), refs: [] }, 0)];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ── threads, posts, asks ────────────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
export const THREAD_KINDS = Object.freeze(['task', 'ask', 'discussion', 'proposal']);
|
|
77
|
-
// `failed`: the task behind the thread ended without an answer (every model on the roster
|
|
78
|
-
// tried, or a hard error) — not `resolved`, which read as "done" on the board.
|
|
79
|
-
export const THREAD_STATUSES = Object.freeze(['open', 'waiting', 'resolved', 'failed', 'approved', 'rejected']);
|
|
80
|
-
// `request`: a member asks for a piece of its task to be done by someone else — the runner
|
|
81
|
-
// turns it into a sub-task with its own thread (team-subtask.js).
|
|
82
|
-
export const POST_KINDS = Object.freeze(['finding', 'note', 'question', 'answer', 'draft', 'decision', 'request']);
|
|
83
|
-
export const POST_STATUSES = Object.freeze(['open', 'proposed', 'approved', 'rejected']);
|
|
84
|
-
export const ASK_TYPES = Object.freeze(['info', 'budget', 'permission', 'direction']);
|
|
85
|
-
export const RUNNER = 'runner';
|
|
86
|
-
export const PERSON = 'person';
|
|
87
|
-
export const MAX_POST_TEXT = 4000;
|
|
88
|
-
|
|
89
|
-
const clip = (t, n) => String(t || '').trim().slice(0, n);
|
|
90
|
-
const refsOf = (r) => (Array.isArray(r) ? r.map(String).filter(Boolean).slice(0, 8) : []);
|
|
91
|
-
|
|
92
|
-
/** The empty folded state — what a run record holds, what `foldBoard` grows. */
|
|
93
|
-
export function emptyBoardState() { return { threads: [], posts: [] }; }
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Fold one board event into a state (pure; the gateway's run store and both clients use it).
|
|
97
|
-
* Ids dedupe: an answer posted through the gateway and echoed by the running client's runner
|
|
98
|
-
* arrives twice with one id and lands once.
|
|
99
|
-
*/
|
|
100
|
-
export function foldBoard(state, ev) {
|
|
101
|
-
const s = state && Array.isArray(state.threads) && Array.isArray(state.posts) ? state : emptyBoardState();
|
|
102
|
-
const type = String(ev?.type || '');
|
|
103
|
-
const p = ev?.payload && typeof ev.payload === 'object' ? ev.payload : ev || {};
|
|
104
|
-
if (type === 'board.thread' && p.thread?.id) {
|
|
105
|
-
if (!s.threads.some((t) => t.id === p.thread.id)) s.threads.push({ ...p.thread });
|
|
106
|
-
} else if (type === 'board.post' && p.post?.id) {
|
|
107
|
-
if (Array.isArray(s.removed) && s.removed.includes(p.post.threadId)) return s;
|
|
108
|
-
if (!s.posts.some((x) => x.id === p.post.id)) s.posts.push({ ...p.post });
|
|
109
|
-
const t = s.threads.find((x) => x.id === p.post.threadId);
|
|
110
|
-
if (t) { t.lastAt = p.post.at; t.lastBy = p.post.by; t.posts = (t.posts || 0) + 1; }
|
|
111
|
-
} else if (type === 'board.decision' && p.postId) {
|
|
112
|
-
const x = s.posts.find((q) => q.id === p.postId);
|
|
113
|
-
if (x) { x.status = p.status; x.decidedBy = p.by; x.decidedAt = p.at ?? ev?.at; }
|
|
114
|
-
} else if (type === 'board.thread-status' && p.threadId) {
|
|
115
|
-
const t = s.threads.find((x) => x.id === p.threadId);
|
|
116
|
-
if (t) { t.status = p.status; if (p.status !== 'waiting') t.waitingOn = null; if (p.holder) t.holder = p.holder; }
|
|
117
|
-
} else if (type === 'board.thread-removed' && p.threadId) {
|
|
118
|
-
// A person took the thread off the board: it and its posts go, and its id is kept so a
|
|
119
|
-
// post a member still makes in it (its runner did not see the removal) lands nowhere.
|
|
120
|
-
s.threads = s.threads.filter((x) => x.id !== p.threadId);
|
|
121
|
-
s.posts = s.posts.filter((x) => x.threadId !== p.threadId);
|
|
122
|
-
if (!Array.isArray(s.removed)) s.removed = [];
|
|
123
|
-
if (!s.removed.includes(p.threadId)) s.removed.push(p.threadId);
|
|
124
|
-
}
|
|
125
|
-
return s;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* The live board a run works on. `state` seeds it (a resume); `onEvent` receives every
|
|
130
|
-
* change as the event the run store folds. The legacy findings API (`add`, `all`, `byTask`)
|
|
131
|
-
* stays: a finding is a post of kind `finding` in its task's thread.
|
|
132
|
-
*/
|
|
133
|
-
export function createBoard({ now = () => Date.now(), newId = null, state = null, onEvent = null } = {}) {
|
|
134
|
-
const st = state && Array.isArray(state.threads) ? { threads: state.threads.map((t) => ({ ...t })), posts: (state.posts || []).map((x) => ({ ...x })), ...(Array.isArray(state.removed) && state.removed.length ? { removed: [...state.removed] } : {}) } : emptyBoardState();
|
|
135
|
-
const listeners = new Set();
|
|
136
|
-
let seq = st.posts.length + st.threads.length;
|
|
137
|
-
const mk = (prefix) => (newId ? newId(prefix) : `${prefix}_${(++seq).toString(36)}${Math.random().toString(36).slice(2, 6)}`);
|
|
138
|
-
const say = (type, payload) => { const ev = { type, at: now(), ...payload }; if (onEvent) onEvent(type, ev); return ev; };
|
|
139
|
-
const threadOf = (id) => st.threads.find((t) => t.id === id);
|
|
140
|
-
const postOf = (id) => st.posts.find((x) => x.id === id);
|
|
141
|
-
const threadForTask = (taskId) => st.threads.find((t) => t.kind === 'task' && t.taskId === taskId);
|
|
142
|
-
|
|
143
|
-
const api = {
|
|
144
|
-
/** Open a thread. A task's thread is opened once; asking for it again returns it. */
|
|
145
|
-
openThread({ id = null, taskId = null, kind = 'discussion', title = '', by = RUNNER, status = 'open', ask = null, parent = null, holder = null } = {}) {
|
|
146
|
-
if (kind === 'task' && taskId) { const had = threadForTask(taskId); if (had) return had; }
|
|
147
|
-
// `parent`: the task this one was requested from (a sub-task's thread hangs under its
|
|
148
|
-
// parent's on the board); `holder`: the member that took it.
|
|
149
|
-
const thread = { id: id || mk('th'), kind: THREAD_KINDS.includes(kind) ? kind : 'discussion', taskId, title: clip(title, 200), by, status: THREAD_STATUSES.includes(status) ? status : 'open', at: now(), posts: 0, ...(ask ? { ask } : {}), ...(parent ? { parent } : {}), ...(holder ? { holder } : {}) };
|
|
150
|
-
st.threads.push(thread);
|
|
151
|
-
say('board.thread', { thread: { ...thread } });
|
|
152
|
-
return thread;
|
|
153
|
-
},
|
|
154
|
-
/** A post in a thread; `replyTo` makes it a reply. */
|
|
155
|
-
/** `proposal` rides on a draft a person decides on — `{ kind: 'agent', agent, jobId }` (§15.2): the card the host creates when the post is approved. */
|
|
156
|
-
post({ id = null, threadId, by, kind = 'note', text = '', refs = [], replyTo = null, status = 'open', finding = null, ask = null, proposal = null } = {}) {
|
|
157
|
-
const t = threadOf(threadId);
|
|
158
|
-
if (!t) throw new Error(`no thread ${threadId}`);
|
|
159
|
-
if (id && postOf(id)) return postOf(id);
|
|
160
|
-
const post = {
|
|
161
|
-
id: id || mk('p'), threadId, by: String(by || RUNNER), kind: POST_KINDS.includes(kind) ? kind : 'note',
|
|
162
|
-
text: clip(text, MAX_POST_TEXT), refs: refsOf(refs), replyTo, status: POST_STATUSES.includes(status) ? status : 'open', at: now(),
|
|
163
|
-
...(finding ? { finding } : {}), ...(ask ? { ask } : {}), ...(proposal && typeof proposal === 'object' ? { proposal } : {}),
|
|
164
|
-
};
|
|
165
|
-
st.posts.push(post);
|
|
166
|
-
t.lastAt = post.at; t.lastBy = post.by; t.posts = (t.posts || 0) + 1;
|
|
167
|
-
say('board.post', { post: { ...post } });
|
|
168
|
-
for (const l of listeners) l(post);
|
|
169
|
-
return post;
|
|
170
|
-
},
|
|
171
|
-
/** A reply hangs off a post and lives in that post's thread. */
|
|
172
|
-
reply({ postId, ...args }) {
|
|
173
|
-
const target = postOf(postId || args.replyTo);
|
|
174
|
-
if (!target) throw new Error(`no post ${postId}`);
|
|
175
|
-
return api.post({ ...args, threadId: target.threadId, replyTo: target.id });
|
|
176
|
-
},
|
|
177
|
-
/** A person's (or the judge's) decision on a post. */
|
|
178
|
-
decide(postId, status, by = PERSON) {
|
|
179
|
-
const x = postOf(postId);
|
|
180
|
-
if (!x || !['approved', 'rejected', 'proposed', 'open'].includes(status)) return null;
|
|
181
|
-
x.status = status; x.decidedBy = by; x.decidedAt = now();
|
|
182
|
-
say('board.decision', { postId, status, by, at: x.decidedAt });
|
|
183
|
-
return x;
|
|
184
|
-
},
|
|
185
|
-
setThreadStatus(threadId, status, extra = {}) {
|
|
186
|
-
const t = threadOf(threadId);
|
|
187
|
-
if (!t || !THREAD_STATUSES.includes(status)) return null;
|
|
188
|
-
t.status = status; if (status !== 'waiting') t.waitingOn = null; Object.assign(t, extra);
|
|
189
|
-
say('board.thread-status', { threadId, status, ...extra });
|
|
190
|
-
return t;
|
|
191
|
-
},
|
|
192
|
-
/**
|
|
193
|
-
* A person takes a thread off the board (from either client — the store appends the
|
|
194
|
-
* event, the running client's tail applies it here). Its posts go with it; a member's
|
|
195
|
-
* later post in it lands nowhere. A waiting ask is not removed: answer it or stop the run.
|
|
196
|
-
*/
|
|
197
|
-
removeThread(threadId, { by = PERSON } = {}) {
|
|
198
|
-
const t = threadOf(threadId);
|
|
199
|
-
if (!t || (t.kind === 'ask' && t.status === 'waiting')) return null;
|
|
200
|
-
st.threads = st.threads.filter((x) => x.id !== threadId);
|
|
201
|
-
st.posts = st.posts.filter((x) => x.threadId !== threadId);
|
|
202
|
-
(st.removed ||= []).push(threadId);
|
|
203
|
-
say('board.thread-removed', { threadId, by });
|
|
204
|
-
return t;
|
|
205
|
-
},
|
|
206
|
-
/**
|
|
207
|
-
* A member is stuck: open an ask thread (status `waiting`) with the question as its
|
|
208
|
-
* first post. The runner waits on it; a person answers from either client.
|
|
209
|
-
*/
|
|
210
|
-
ask({ taskId = null, by, type = 'info', text, options = [], timeoutMs = 0, title = '' } = {}) {
|
|
211
|
-
const ask = { type: ASK_TYPES.includes(type) ? type : 'info', options: (Array.isArray(options) ? options : []).map(String).slice(0, 6), timeoutMs };
|
|
212
|
-
const thread = api.openThread({ taskId, kind: 'ask', title: title || clip(text, 120), by, status: 'waiting', ask });
|
|
213
|
-
thread.waitingOn = PERSON;
|
|
214
|
-
const post = api.post({ threadId: thread.id, by, kind: 'question', text, ask });
|
|
215
|
-
return { thread, post };
|
|
216
|
-
},
|
|
217
|
-
/** The answer to an ask — from a person, on any client. Resolves the thread. */
|
|
218
|
-
answer(threadId, { id = null, text, by = PERSON } = {}) {
|
|
219
|
-
const t = threadOf(threadId);
|
|
220
|
-
if (!t) return null;
|
|
221
|
-
const post = api.post({ id, threadId, by, kind: 'answer', text });
|
|
222
|
-
api.setThreadStatus(threadId, 'resolved', { answeredAt: post.at });
|
|
223
|
-
return post;
|
|
224
|
-
},
|
|
225
|
-
thread: threadOf,
|
|
226
|
-
threadForTask,
|
|
227
|
-
threads: () => st.threads.map((t) => ({ ...t })),
|
|
228
|
-
posts: (threadId = null) => st.posts.filter((x) => !threadId || x.threadId === threadId).map((x) => ({ ...x })),
|
|
229
|
-
postById: postOf,
|
|
230
|
-
/** Every ask still waiting — what a client pins at the top. */
|
|
231
|
-
waiting: () => st.threads.filter((t) => t.kind === 'ask' && t.status === 'waiting').map((t) => ({ ...t })),
|
|
232
|
-
/** The whole board, for the run record and for a resume. */
|
|
233
|
-
state: () => ({ threads: st.threads.map((t) => ({ ...t })), posts: st.posts.map((x) => ({ ...x })), ...(st.removed?.length ? { removed: [...st.removed] } : {}) }),
|
|
234
|
-
|
|
235
|
-
// ── findings, as before: a finding is a post in its task's thread ──
|
|
236
|
-
add(list, { by = null } = {}) {
|
|
237
|
-
let n = 0;
|
|
238
|
-
for (const f of Array.isArray(list) ? list : [list]) {
|
|
239
|
-
if (!f || !f.text) continue;
|
|
240
|
-
const t = f.taskId ? (threadForTask(f.taskId) || api.openThread({ taskId: f.taskId, kind: 'task', title: f.taskId })) : (st.threads.find((x) => x.kind === 'discussion' && x.title === 'findings') || api.openThread({ kind: 'discussion', title: 'findings' }));
|
|
241
|
-
api.post({ id: f.id, threadId: t.id, by: by || f.role || RUNNER, kind: 'finding', text: f.text, refs: f.refs, finding: { kind: f.kind || 'claim', confidence: f.confidence ?? null } });
|
|
242
|
-
n += 1;
|
|
243
|
-
}
|
|
244
|
-
return n;
|
|
245
|
-
},
|
|
246
|
-
all: () => st.posts.filter((x) => x.kind === 'finding').map(asFinding(st)),
|
|
247
|
-
byTask: (taskId) => api.all().filter((f) => f.taskId === taskId),
|
|
248
|
-
byRole: (role) => api.all().filter((f) => f.role === role),
|
|
249
|
-
onFinding(fn) { const l = (p) => { if (p.kind === 'finding') fn(asFinding(st)(p)); }; listeners.add(l); return () => listeners.delete(l); },
|
|
250
|
-
get size() { return st.posts.filter((x) => x.kind === 'finding').length; },
|
|
251
|
-
};
|
|
252
|
-
return api;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** A finding post in the legacy finding shape (what merge, briefs and lanes read). */
|
|
256
|
-
function asFinding(st) {
|
|
257
|
-
return (p) => {
|
|
258
|
-
const t = st.threads.find((x) => x.id === p.threadId);
|
|
259
|
-
return { id: p.id, kind: p.finding?.kind || 'claim', text: p.text, refs: p.refs || [], confidence: p.finding?.confidence ?? null, role: p.by, taskId: t?.taskId || null, at: p.at, status: p.status, ...(p.finding?.prior ? { prior: true } : {}) };
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** Findings from a folded state (a run record read from the store). */
|
|
264
|
-
export function findingsOf(state) {
|
|
265
|
-
const st = state && Array.isArray(state.posts) ? state : emptyBoardState();
|
|
266
|
-
return st.posts.filter((x) => x.kind === 'finding').map(asFinding(st));
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* What a later task READS: the findings of the tasks it depends on (or everything so far),
|
|
271
|
-
* sized. Newest are kept whole; the oldest are what get cut, and the cut is stated.
|
|
272
|
-
*/
|
|
273
|
-
export function boardText(source, { taskIds = null, max = BOARD_TEXT_MAX, role = null } = {}) {
|
|
274
|
-
// A board (threads + posts) reads threaded; a bare findings array reads as before.
|
|
275
|
-
const state = source && typeof source.state === 'function' ? source.state() : (source && Array.isArray(source.posts) ? source : null);
|
|
276
|
-
const lines = state ? threadedLines(state, { taskIds, role }) : findingLines(source, taskIds);
|
|
277
|
-
if (!lines.length) return '';
|
|
278
|
-
const head = state ? 'The board so far' : 'Findings so far';
|
|
279
|
-
const out = lines.join('\n');
|
|
280
|
-
if (out.length <= max) return `${head}:\n${out}`;
|
|
281
|
-
const kept = [];
|
|
282
|
-
let size = 0;
|
|
283
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
284
|
-
if (size + lines[i].length + 1 > max) break;
|
|
285
|
-
kept.unshift(lines[i]);
|
|
286
|
-
size += lines[i].length + 1;
|
|
287
|
-
}
|
|
288
|
-
return `${head} (${lines.length - kept.length} earlier lines omitted for length):\n${kept.join('\n')}`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function findingLines(findings, taskIds) {
|
|
292
|
-
return (findings || []).filter((f) => !taskIds || taskIds.includes(f.taskId))
|
|
293
|
-
.map((f) => `- [${f.kind}${f.role ? ` · ${f.role}` : ''}${f.confidence != null ? ` · ${Math.round(f.confidence * 100)}%` : ''}] ${f.text}${f.refs?.length ? ` (refs: ${f.refs.join(', ')})` : ''}`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* What a member READS: the task threads it depends on (or every task thread), the answered
|
|
298
|
-
* asks and settled discussions — never rejected posts — threaded, a reply under its post.
|
|
299
|
-
* A person's decision is marked so a member treats it as settled.
|
|
300
|
-
*/
|
|
301
|
-
function threadedLines(state, { taskIds, role }) {
|
|
302
|
-
const out = [];
|
|
303
|
-
const posts = state.posts;
|
|
304
|
-
const threads = state.threads.filter((t) => {
|
|
305
|
-
if (t.kind === 'task') return !taskIds || taskIds.includes(t.taskId);
|
|
306
|
-
if (t.kind === 'ask') return t.status === 'resolved' && (!role || t.by === role || t.by === RUNNER);
|
|
307
|
-
if (t.kind === 'discussion') return true;
|
|
308
|
-
return false; // a proposal is the run's output, not a member's input
|
|
309
|
-
});
|
|
310
|
-
for (const t of threads) {
|
|
311
|
-
const own = posts.filter((x) => x.threadId === t.id && x.status !== 'rejected');
|
|
312
|
-
if (!own.length) continue;
|
|
313
|
-
out.push(`## ${t.kind}${t.by && t.by !== RUNNER ? ` by ${t.by}` : ''}: ${t.title}${t.holder ? ` (held by ${t.holder})` : ''}${t.status === 'resolved' && t.kind === 'ask' ? ' (answered)' : ''}`);
|
|
314
|
-
const line = (x, depth) => {
|
|
315
|
-
const tag = [x.kind === 'finding' ? (x.finding?.kind || 'claim') : x.kind, x.by, x.finding?.confidence != null ? `${Math.round(x.finding.confidence * 100)}%` : null, x.status === 'approved' ? 'APPROVED' : x.status === 'proposed' ? 'proposed' : null, x.kind === 'decision' || x.by === PERSON ? 'SETTLED' : null].filter(Boolean).join(' · ');
|
|
316
|
-
out.push(`${' '.repeat(depth)}- [${tag}] ${x.text}${x.refs?.length ? ` (refs: ${x.refs.join(', ')})` : ''}`);
|
|
317
|
-
for (const r of own.filter((y) => y.replyTo === x.id)) line(r, depth + 1);
|
|
318
|
-
};
|
|
319
|
-
for (const x of own.filter((y) => !y.replyTo)) line(x, 0);
|
|
320
|
-
}
|
|
321
|
-
return out;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Findings → the claim shape a draft brief takes (W7): text + refs as `{ kind, id }`. */
|
|
325
|
-
export function toBriefClaims(findings) {
|
|
326
|
-
return (findings || [])
|
|
327
|
-
.filter((f) => f.kind === 'claim' && f.text)
|
|
328
|
-
.map((f) => ({
|
|
329
|
-
text: f.text,
|
|
330
|
-
refs: (f.refs || []).map((r) => {
|
|
331
|
-
const m = /^([a-z][a-z0-9_-]*):(?!\/\/)(.+)$/.exec(String(r));
|
|
332
|
-
return m ? { kind: m[1], id: m[2] } : { kind: 'url', id: String(r) };
|
|
333
|
-
}),
|
|
334
|
-
by: f.role || 'team',
|
|
335
|
-
confidence: f.confidence,
|
|
336
|
-
}));
|
|
337
|
-
}
|
|
1
|
+
import{defineSchema as R,describeSchema as F,coerce as q}from"./structured.js";const N=Object.freeze(["claim","draft","link","question","answer"]),E=40,M=12e3,B=R({name:"findings",purpose:"what this task established, each item on its own with where it came from",fields:{findings:{type:"object[]",required:!0,maxItems:E,describe:"one entry per fact, draft, link or open question — never a paragraph of several",fields:{kind:{type:"enum",values:N,default:"claim"},text:{type:"string",required:!0,max:2e3},refs:{type:"string[]",maxItems:8,describe:"record ids or URLs this rests on, when any"},confidence:{type:"number",describe:"0–1, how sure"}}}},nothing:{findings:[]}});function W(){return`When you are done, end your answer with your findings in this shape:
|
|
2
|
+
${F(B)}`}function Y(d,{role:i,taskId:n}={}){const f=String(d||"").trim();if(!f)return[];const t=q(f,B),o=Array.isArray(t?.value?.findings)?t.value.findings.filter(u=>u&&String(u.text||"").trim()):[],a=(u,c)=>({id:`${n||"t"}:${c+1}`,kind:N.includes(u.kind)?u.kind:"claim",text:String(u.text).trim().slice(0,2e3),refs:Array.isArray(u.refs)?u.refs.map(String).filter(Boolean).slice(0,8):[],confidence:Number.isFinite(Number(u.confidence))&&Number(u.confidence)>0?Math.min(1,Number(u.confidence)):null,role:i||null,taskId:n||null});if(o.length)return o.slice(0,E).map(a);const m=f.replace(/```json[\s\S]*$/i,"").trim()||f;return[a({kind:"draft",text:m.slice(0,2e3),refs:[]},0)]}const P=Object.freeze(["task","ask","discussion","proposal"]),D=Object.freeze(["open","waiting","resolved","failed","approved","rejected"]),K=Object.freeze(["finding","note","question","answer","draft","decision","request"]),X=Object.freeze(["open","proposed","approved","rejected"]),L=Object.freeze(["info","budget","permission","direction"]),v="runner",x="person",U=4e3,j=(d,i)=>String(d||"").trim().slice(0,i),G=d=>Array.isArray(d)?d.map(String).filter(Boolean).slice(0,8):[];function O(){return{threads:[],posts:[]}}function J(d,i){const n=d&&Array.isArray(d.threads)&&Array.isArray(d.posts)?d:O(),f=String(i?.type||""),t=i?.payload&&typeof i.payload=="object"?i.payload:i||{};if(f==="board.thread"&&t.thread?.id)n.threads.some(o=>o.id===t.thread.id)||n.threads.push({...t.thread});else if(f==="board.post"&&t.post?.id){if(Array.isArray(n.removed)&&n.removed.includes(t.post.threadId))return n;n.posts.some(a=>a.id===t.post.id)||n.posts.push({...t.post});const o=n.threads.find(a=>a.id===t.post.threadId);o&&(o.lastAt=t.post.at,o.lastBy=t.post.by,o.posts=(o.posts||0)+1)}else if(f==="board.decision"&&t.postId){const o=n.posts.find(a=>a.id===t.postId);o&&(o.status=t.status,o.decidedBy=t.by,o.decidedAt=t.at??i?.at)}else if(f==="board.thread-status"&&t.threadId){const o=n.threads.find(a=>a.id===t.threadId);o&&(o.status=t.status,t.status!=="waiting"&&(o.waitingOn=null),t.holder&&(o.holder=t.holder))}else f==="board.thread-removed"&&t.threadId&&(n.threads=n.threads.filter(o=>o.id!==t.threadId),n.posts=n.posts.filter(o=>o.threadId!==t.threadId),Array.isArray(n.removed)||(n.removed=[]),n.removed.includes(t.threadId)||n.removed.push(t.threadId));return n}function Q({now:d=()=>Date.now(),newId:i=null,state:n=null,onEvent:f=null}={}){const t=n&&Array.isArray(n.threads)?{threads:n.threads.map(e=>({...e})),posts:(n.posts||[]).map(e=>({...e})),...Array.isArray(n.removed)&&n.removed.length?{removed:[...n.removed]}:{}}:O(),o=new Set;let a=t.posts.length+t.threads.length;const m=e=>i?i(e):`${e}_${(++a).toString(36)}${Math.random().toString(36).slice(2,6)}`,u=(e,r)=>{const l={type:e,at:d(),...r};return f&&f(e,l),l},c=e=>t.threads.find(r=>r.id===e),p=e=>t.posts.find(r=>r.id===e),w=e=>t.threads.find(r=>r.kind==="task"&&r.taskId===e),h={openThread({id:e=null,taskId:r=null,kind:l="discussion",title:s="",by:y=v,status:g="open",ask:S=null,parent:b=null,holder:k=null}={}){if(l==="task"&&r){const $=w(r);if($)return $}const A={id:e||m("th"),kind:P.includes(l)?l:"discussion",taskId:r,title:j(s,200),by:y,status:D.includes(g)?g:"open",at:d(),posts:0,...S?{ask:S}:{},...b?{parent:b}:{},...k?{holder:k}:{}};return t.threads.push(A),u("board.thread",{thread:{...A}}),A},post({id:e=null,threadId:r,by:l,kind:s="note",text:y="",refs:g=[],replyTo:S=null,status:b="open",finding:k=null,ask:A=null,proposal:$=null}={}){const I=c(r);if(!I)throw new Error(`no thread ${r}`);if(e&&p(e))return p(e);const T={id:e||m("p"),threadId:r,by:String(l||v),kind:K.includes(s)?s:"note",text:j(y,U),refs:G(g),replyTo:S,status:X.includes(b)?b:"open",at:d(),...k?{finding:k}:{},...A?{ask:A}:{},...$&&typeof $=="object"?{proposal:$}:{}};t.posts.push(T),I.lastAt=T.at,I.lastBy=T.by,I.posts=(I.posts||0)+1,u("board.post",{post:{...T}});for(const z of o)z(T);return T},reply({postId:e,...r}){const l=p(e||r.replyTo);if(!l)throw new Error(`no post ${e}`);return h.post({...r,threadId:l.threadId,replyTo:l.id})},decide(e,r,l=x){const s=p(e);return!s||!["approved","rejected","proposed","open"].includes(r)?null:(s.status=r,s.decidedBy=l,s.decidedAt=d(),u("board.decision",{postId:e,status:r,by:l,at:s.decidedAt}),s)},setThreadStatus(e,r,l={}){const s=c(e);return!s||!D.includes(r)?null:(s.status=r,r!=="waiting"&&(s.waitingOn=null),Object.assign(s,l),u("board.thread-status",{threadId:e,status:r,...l}),s)},removeThread(e,{by:r=x}={}){const l=c(e);return!l||l.kind==="ask"&&l.status==="waiting"?null:(t.threads=t.threads.filter(s=>s.id!==e),t.posts=t.posts.filter(s=>s.threadId!==e),(t.removed||=[]).push(e),u("board.thread-removed",{threadId:e,by:r}),l)},ask({taskId:e=null,by:r,type:l="info",text:s,options:y=[],timeoutMs:g=0,title:S=""}={}){const b={type:L.includes(l)?l:"info",options:(Array.isArray(y)?y:[]).map(String).slice(0,6),timeoutMs:g},k=h.openThread({taskId:e,kind:"ask",title:S||j(s,120),by:r,status:"waiting",ask:b});k.waitingOn=x;const A=h.post({threadId:k.id,by:r,kind:"question",text:s,ask:b});return{thread:k,post:A}},answer(e,{id:r=null,text:l,by:s=x}={}){if(!c(e))return null;const g=h.post({id:r,threadId:e,by:s,kind:"answer",text:l});return h.setThreadStatus(e,"resolved",{answeredAt:g.at}),g},thread:c,threadForTask:w,threads:()=>t.threads.map(e=>({...e})),posts:(e=null)=>t.posts.filter(r=>!e||r.threadId===e).map(r=>({...r})),postById:p,waiting:()=>t.threads.filter(e=>e.kind==="ask"&&e.status==="waiting").map(e=>({...e})),state:()=>({threads:t.threads.map(e=>({...e})),posts:t.posts.map(e=>({...e})),...t.removed?.length?{removed:[...t.removed]}:{}}),add(e,{by:r=null}={}){let l=0;for(const s of Array.isArray(e)?e:[e]){if(!s||!s.text)continue;const y=s.taskId?w(s.taskId)||h.openThread({taskId:s.taskId,kind:"task",title:s.taskId}):t.threads.find(g=>g.kind==="discussion"&&g.title==="findings")||h.openThread({kind:"discussion",title:"findings"});h.post({id:s.id,threadId:y.id,by:r||s.role||v,kind:"finding",text:s.text,refs:s.refs,finding:{kind:s.kind||"claim",confidence:s.confidence??null}}),l+=1}return l},all:()=>t.posts.filter(e=>e.kind==="finding").map(_(t)),byTask:e=>h.all().filter(r=>r.taskId===e),byRole:e=>h.all().filter(r=>r.role===e),onFinding(e){const r=l=>{l.kind==="finding"&&e(_(t)(l))};return o.add(r),()=>o.delete(r)},get size(){return t.posts.filter(e=>e.kind==="finding").length}};return h}function _(d){return i=>{const n=d.threads.find(f=>f.id===i.threadId);return{id:i.id,kind:i.finding?.kind||"claim",text:i.text,refs:i.refs||[],confidence:i.finding?.confidence??null,role:i.by,taskId:n?.taskId||null,at:i.at,status:i.status,...i.finding?.prior?{prior:!0}:{}}}}function Z(d){const i=d&&Array.isArray(d.posts)?d:O();return i.posts.filter(n=>n.kind==="finding").map(_(i))}function ee(d,{taskIds:i=null,max:n=M,role:f=null}={}){const t=d&&typeof d.state=="function"?d.state():d&&Array.isArray(d.posts)?d:null,o=t?C(t,{taskIds:i,role:f}):H(d,i);if(!o.length)return"";const a=t?"The board so far":"Findings so far",m=o.join(`
|
|
3
|
+
`);if(m.length<=n)return`${a}:
|
|
4
|
+
${m}`;const u=[];let c=0;for(let p=o.length-1;p>=0&&!(c+o[p].length+1>n);p-=1)u.unshift(o[p]),c+=o[p].length+1;return`${a} (${o.length-u.length} earlier lines omitted for length):
|
|
5
|
+
${u.join(`
|
|
6
|
+
`)}`}function H(d,i){return(d||[]).filter(n=>!i||i.includes(n.taskId)).map(n=>`- [${n.kind}${n.role?` · ${n.role}`:""}${n.confidence!=null?` · ${Math.round(n.confidence*100)}%`:""}] ${n.text}${n.refs?.length?` (refs: ${n.refs.join(", ")})`:""}`)}function C(d,{taskIds:i,role:n}){const f=[],t=d.posts,o=d.threads.filter(a=>a.kind==="task"?!i||i.includes(a.taskId):a.kind==="ask"?a.status==="resolved"&&(!n||a.by===n||a.by===v):a.kind==="discussion");for(const a of o){const m=t.filter(c=>c.threadId===a.id&&c.status!=="rejected");if(!m.length)continue;f.push(`## ${a.kind}${a.by&&a.by!==v?` by ${a.by}`:""}: ${a.title}${a.holder?` (held by ${a.holder})`:""}${a.status==="resolved"&&a.kind==="ask"?" (answered)":""}`);const u=(c,p)=>{const w=[c.kind==="finding"?c.finding?.kind||"claim":c.kind,c.by,c.finding?.confidence!=null?`${Math.round(c.finding.confidence*100)}%`:null,c.status==="approved"?"APPROVED":c.status==="proposed"?"proposed":null,c.kind==="decision"||c.by===x?"SETTLED":null].filter(Boolean).join(" · ");f.push(`${" ".repeat(p)}- [${w}] ${c.text}${c.refs?.length?` (refs: ${c.refs.join(", ")})`:""}`);for(const h of m.filter(e=>e.replyTo===c.id))u(h,p+1)};for(const c of m.filter(p=>!p.replyTo))u(c,0)}return f}function te(d){return(d||[]).filter(i=>i.kind==="claim"&&i.text).map(i=>({text:i.text,refs:(i.refs||[]).map(n=>{const f=/^([a-z][a-z0-9_-]*):(?!\/\/)(.+)$/.exec(String(n));return f?{kind:f[1],id:f[2]}:{kind:"url",id:String(n)}}),by:i.role||"team",confidence:i.confidence}))}export{L as ASK_TYPES,M as BOARD_TEXT_MAX,B as FINDINGS_SCHEMA,N as FINDING_KINDS,E as MAX_FINDINGS_PER_TASK,U as MAX_POST_TEXT,x as PERSON,K as POST_KINDS,X as POST_STATUSES,v as RUNNER,P as THREAD_KINDS,D as THREAD_STATUSES,ee as boardText,Q as createBoard,O as emptyBoardState,W as findingsInstruction,Z as findingsOf,J as foldBoard,Y as parseFindings,te as toBriefClaims};
|
package/team-cache.js
CHANGED
|
@@ -1,63 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// Three members of a travel team searched "Snoqualmie Valley School District calendar" five
|
|
4
|
-
// times between them. Each member's own turn already dedupes its own repeats; the RUN did
|
|
5
|
-
// not, and members in one wave cannot read each other's findings yet. So: a read-only call
|
|
6
|
-
// with the same name and arguments, from any member, runs once per run; the rest get the
|
|
7
|
-
// first answer back, marked as shared. Only tools that declare themselves read-only (MCP
|
|
8
|
-
// annotations, the shared retrieval tools) are cached; a dispatcher's paging (`get_result`)
|
|
9
|
-
// and the board are per-member and never are.
|
|
10
|
-
|
|
11
|
-
const NEVER = new Set(['get_result', 'board', 'team', 'recipe']);
|
|
12
|
-
const READ_ONLY_NAME_RE = /^(find|web_search|history_search|history_get_source|history_list|meeting_live_transcript|read|fetch|get_|list_|search)/i;
|
|
13
|
-
|
|
14
|
-
function stableKey(name, input) {
|
|
15
|
-
const walk = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? Object.keys(v).sort().reduce((o, k) => { o[k] = walk(v[k]); return o; }, {}) : Array.isArray(v) ? v.map(walk) : v);
|
|
16
|
-
try { return `${name} ${JSON.stringify(walk(input ?? {}))}`; } catch { return `${name} ?`; }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function createRunCache() {
|
|
20
|
-
const hits = new Map(); // key -> { result, by, at }
|
|
21
|
-
let shared = 0;
|
|
22
|
-
return {
|
|
23
|
-
get: (k) => hits.get(k) || null,
|
|
24
|
-
set: (k, v) => { hits.set(k, v); },
|
|
25
|
-
delete: (k) => { hits.delete(k); },
|
|
26
|
-
countShared: () => { shared += 1; },
|
|
27
|
-
get size() { return hits.size; },
|
|
28
|
-
get shared() { return shared; },
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isReadOnly(toolset, name) {
|
|
33
|
-
if (NEVER.has(name)) return false;
|
|
34
|
-
const spec = (toolset.specs || []).find((s) => s.name === name);
|
|
35
|
-
if (spec?.annotations?.readOnlyHint === true) return true;
|
|
36
|
-
if (spec?.annotations && spec.annotations.readOnlyHint === false) return false;
|
|
37
|
-
const t = toolset.traits instanceof Map ? toolset.traits.get(name) : null;
|
|
38
|
-
if (t && typeof t.readOnly === 'boolean') return t.readOnly;
|
|
39
|
-
return READ_ONLY_NAME_RE.test(name);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The toolset with its read-only calls served from the run's cache. */
|
|
43
|
-
export function withRunCache(toolset, cache, { role = '' } = {}) {
|
|
44
|
-
if (!toolset || !cache) return toolset;
|
|
45
|
-
return {
|
|
46
|
-
...toolset,
|
|
47
|
-
execute: async (name, input, ...rest) => {
|
|
48
|
-
if (!isReadOnly(toolset, name)) return toolset.execute(name, input, ...rest);
|
|
49
|
-
const key = stableKey(name, input);
|
|
50
|
-
const hit = cache.get(key);
|
|
51
|
-
if (hit) {
|
|
52
|
-
// Members in one wave ask at the same moment: the second waits on the first's call.
|
|
53
|
-
cache.countShared();
|
|
54
|
-
const result = await hit.promise;
|
|
55
|
-
const note = `[shared: ${hit.by || 'another member'} already ran this in this run]\n`;
|
|
56
|
-
return typeof result === 'string' ? note + result : result;
|
|
57
|
-
}
|
|
58
|
-
const promise = Promise.resolve().then(() => toolset.execute(name, input, ...rest));
|
|
59
|
-
cache.set(key, { promise, by: role, at: Date.now() });
|
|
60
|
-
try { return await promise; } catch (e) { cache.delete(key); throw e; }
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
1
|
+
const f=new Set(["get_result","board","team","recipe"]),h=/^(find|web_search|history_search|history_get_source|history_list|meeting_live_transcript|read|fetch|get_|list_|search)/i;function d(t,r){const n=e=>e&&typeof e=="object"&&!Array.isArray(e)?Object.keys(e).sort().reduce((s,i)=>(s[i]=n(e[i]),s),{}):Array.isArray(e)?e.map(n):e;try{return`${t} ${JSON.stringify(n(r??{}))}`}catch{return`${t} ?`}}function _(){const t=new Map;let r=0;return{get:n=>t.get(n)||null,set:(n,e)=>{t.set(n,e)},delete:n=>{t.delete(n)},countShared:()=>{r+=1},get size(){return t.size},get shared(){return r}}}function l(t,r){if(f.has(r))return!1;const n=(t.specs||[]).find(s=>s.name===r);if(n?.annotations?.readOnlyHint===!0)return!0;if(n?.annotations&&n.annotations.readOnlyHint===!1)return!1;const e=t.traits instanceof Map?t.traits.get(r):null;return e&&typeof e.readOnly=="boolean"?e.readOnly:h.test(r)}function g(t,r,{role:n=""}={}){return!t||!r?t:{...t,execute:async(e,s,...i)=>{if(!l(t,e))return t.execute(e,s,...i);const u=d(e,s),c=r.get(u);if(c){r.countShared();const a=await c.promise,y=`[shared: ${c.by||"another member"} already ran this in this run]
|
|
2
|
+
`;return typeof a=="string"?y+a:a}const o=Promise.resolve().then(()=>t.execute(e,s,...i));r.set(u,{promise:o,by:n,at:Date.now()});try{return await o}catch(a){throw r.delete(u),a}}}}export{_ as createRunCache,g as withRunCache};
|
package/team-observe.js
CHANGED
|
@@ -1,118 +1 @@
|
|
|
1
|
-
|
|
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
|
-
}
|
|
1
|
+
import{runState as A,spendOf as w,LIVE_RUN_STATUSES as h}from"./team-record.js";const n=a=>Number.isFinite(Number(a))?Number(a):0;function y(a=[],{now:o=Date.now()}={}){const u=[];for(const e of Array.isArray(a)?a:[]){if(!e?.id)continue;const r=A(e,{now:o});e.status==="waiting"||n(e.waiting)>0||r.key==="waiting"?u.push({kind:"ask",runId:e.id,team:e.team||"",request:String(e.request||"").slice(0,120),count:Math.max(1,n(e.waiting)),action:"answer",at:n(e.lastEventAt)||n(e.createdAt),detail:r.detail||""}):r.key==="stalled"&&u.push({kind:"stalled",runId:e.id,team:e.team||"",request:String(e.request||"").slice(0,120),action:"resume",at:n(e.lastEventAt)||n(e.createdAt),detail:r.detail||""})}return u.sort((e,r)=>e.kind===r.kind?r.at-e.at:e.kind==="ask"?-1:1)}function N(a=[],{now:o=Date.now(),sinceMs:u=168*3600*1e3}={}){const e=(Array.isArray(a)?a:[]).filter(t=>t?.id&&n(t.createdAt)>=o-u),r=e.filter(t=>h.includes(t.status)),c=r.filter(t=>A(t,{now:o}).key==="stalled").length,g=y(e,{now:o}).filter(t=>t.kind==="ask").reduce((t,l)=>t+l.count,0),s=e.reduce((t,l)=>{for(const i of l.tasks||[])i.status==="ok"?t.done+=1:(i.status==="failed"||i.status==="unassigned")&&(t.failed+=1);return t},{done:0,failed:0}),d=e.reduce((t,l)=>{const i=l.usage?.spent||{};return t.usd+=n(i.usd),t.ms+=n(i.ms),t.tokens+=n(i.tokens),t},{usd:0,ms:0,tokens:0});return{runs:e.length,live:r.length,stalled:c,asks:g,tasks:s,spend:d,completed:e.filter(t=>t.status==="completed").length}}function M(a,{now:o=Date.now()}={}){const u=t=>Number.isFinite(Number(t))&&t!==null&&t!==void 0&&t!=="",e=u(a?.startedAt)?Number(a.startedAt):u(a?.createdAt)?Number(a.createdAt):o,r=h.includes(a?.status),c=r?o:Math.max(e+1,n(a?.endedAt)||n(a?.lastEventAt)||o),g=Math.max(1,c-e),s=t=>Math.max(0,Math.min(1,(t-e)/g)),d=[];for(const t of a?.tasks||[]){const l=[],i=u(t.startedAt)?Number(t.startedAt):null,m=u(t.endedAt)?Number(t.endedAt):t.status==="running"||t.status==="waiting"?c:null;for(const f of t.attempts||[]){if(!u(f.startedAt)||!u(f.endedAt)||!(f.status==="failed"||f.rotated))continue;const p=Number(f.startedAt),b=Number(f.endedAt);l.push({kind:"declined",from:s(p),to:s(Math.max(b,p+g/200)),label:f.error||f.model||"declined"})}i!==null&&m!==null&&l.push({kind:t.status==="waiting"?"waiting":"working",from:s(i),to:s(Math.max(m,i+g/200)),label:t.status});const k=i!==null?(m??c)-i:n(t.ms);d.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:n(t.findings),ms:k,segments:l})}return{start:e,end:c,span:g,live:r,lanes:d}}function $(a=[],{by:o="team",now:u=Date.now()}={}){const e=new Map,r=(s,d,t,{priced:l})=>{const i=e.get(s)||{key:s,label:d,ms:0,tokens:0,usd:null,calls:0,runs:0};i.ms+=n(t.ms),i.tokens+=n(t.tokens),i.calls+=n(t.calls),i.runs+=1,l&&(i.usd=n(i.usd)+n(t.usd)),e.set(s,i)};for(const s of Array.isArray(a)?a:[]){if(!s?.id)continue;const d=w(s,{now:u})?.spent||s.usage?.spent||{},t=n(d.usd)>0||n(d.tokens)>0;if(o==="team")r(s.team||"?",s.team||"?",d,{priced:t});else if(o==="agent"){const l=(s.tasks||[]).filter(m=>m.role),i=l.length?1/l.length:0;for(const m of l){const k=(s.roles||[]).find(p=>p.id===m.role),f=k?.agent||`${s.team||"?"}-${m.role}`;r(f,k?.agent||m.role,{ms:n(m.ms)||n(d.ms)*i,tokens:n(d.tokens)*i,usd:n(d.usd)*i,calls:n(d.calls)*i},{priced:t})}}else if(o==="engine")for(const l of s.tasks||[]){const i=l.engine?`${l.engine.kind}:${l.engine.id}${l.engine.model?`/${l.engine.model}`:""}`:l.model||"?";r(i,i,{ms:n(l.ms),tokens:0,usd:0,calls:0},{priced:!1})}}const c=[...e.values()].sort((s,d)=>(d.usd??0)-(s.usd??0)||d.ms-s.ms),g=Math.max(1,...c.map(s=>(s.usd??0)||0),...c.map(s=>s.ms/6e4));return c.map(s=>({...s,share:s.usd!=null&&s.usd>0?s.usd/g:s.ms/6e4/g}))}function S(a){const o=a?.availability||{},u=Array.isArray(o.byHour)?o.byHour:Array.from({length:24},()=>({calls:0,declines:0}));return{key:a?.key||"?",label:a?.engine?`${a.engine.id}${a.engine.model?` · ${a.engine.model}`:""}`:a?.key||"?",rate:o.rate??null,decliningNow:!!o.decliningNow,p50:a?.latency?.total?.p50??null,calls:n(a?.calls),cells:u.map(e=>n(e.declines)>0&&n(e.declines)>=n(e.calls)?"declined":n(e.calls)>0?"ok":"none")}}export{S as engineStrip,y as observeInbox,N as observeStrip,M as runLanes,$ as spendRows};
|