@chatpanel/events 0.78.2 → 0.79.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/cowriter-router.js +5 -2
- package/index.js +1 -1
- package/package.json +1 -1
- package/team-run.js +38 -13
- package/team-tool.js +6 -2
- package/team-trail.js +1 -0
package/cowriter-router.js
CHANGED
|
@@ -36,8 +36,11 @@ function withTierAndMode(c) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Appoint one role → the best available candidate (or null if none usable).
|
|
39
|
-
export function appoint(role, candidates, { overrides = {} } = {}) {
|
|
40
|
-
|
|
39
|
+
export function appoint(role, candidates, { overrides = {}, exclude = null } = {}) {
|
|
40
|
+
// `exclude` — ids (or model names) that failed this run: the next appointment is the
|
|
41
|
+
// nearest tier among what is left, which is what a person would do by hand.
|
|
42
|
+
const out = exclude ? new Set(exclude) : null;
|
|
43
|
+
const usable = (candidates || []).filter((c) => c && c.usable !== false && c.model && !(out && (out.has(c.id) || out.has(c.model))));
|
|
41
44
|
if (!usable.length) return null;
|
|
42
45
|
const ovId = overrides[role.id];
|
|
43
46
|
if (ovId) {
|
package/index.js
CHANGED
|
@@ -195,7 +195,7 @@ export { createBudget, validateBudget, normalizeBudget, usageOf, BudgetError, BU
|
|
|
195
195
|
export { defineTeam, validateTeam, normalizeTeam, normalizeGrants, grantAllows, describeRole, TeamError, ROLE_MODES, MERGE_POLICIES, PLAN_MODES, GRANTABLE, STARTER_TEAMS, starterTeams, blankTeam, teamFromForm, slugTeamName } from './team.js';
|
|
196
196
|
export { fixedPlan, parsePlan, plannerPrompt, waves, breakCycles, TEAM_PLAN_SCHEMA } from './team-plan.js';
|
|
197
197
|
export { createBoard, parseFindings, boardText, findingsInstruction, toBriefClaims, FINDINGS_SCHEMA, FINDING_KINDS } from './team-board.js';
|
|
198
|
-
export { runTeam, dryRunTeam, TeamRunError, RUN_STATUSES } from './team-run.js';
|
|
198
|
+
export { runTeam, dryRunTeam, isModelUnavailable, TeamRunError, RUN_STATUSES } from './team-run.js';
|
|
199
199
|
export { teamToolProvider, teamToolSpec, describeTeamForApproval, TEAM_TOOL_NAME } from './team-tool.js';
|
|
200
200
|
export { teamLine, teamLanes } from './team-trail.js';
|
|
201
201
|
export { mcpDispatchProvider, MCP_TOOL_NAME } from './mcp-dispatch.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.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",
|
package/team-run.js
CHANGED
|
@@ -44,6 +44,16 @@ const strongestRole = (team) => [...team.roles].sort((a, b) => (TIER[b.prefer] ?
|
|
|
44
44
|
* What a person sees before approving a run: every role with the model it would get, its
|
|
45
45
|
* grants and mode; the plan when it is fixed (a planner plans at run time); the budget.
|
|
46
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* Was this failure the MODEL being unreachable — not found, not deployed, no key, gone —
|
|
49
|
+
* rather than the request being wrong? The runner re-appoints on these and gives up on the
|
|
50
|
+
* rest. Provider wording varies; what they share is that a different model would answer.
|
|
51
|
+
*/
|
|
52
|
+
export function isModelUnavailable(error) {
|
|
53
|
+
const m = String(error?.message || error || '');
|
|
54
|
+
return /model[_ ]not[_ ]found|not found|not deployed|inaccessible|does not exist|no such model|unknown model|unsupported model|not available|unavailable|no api key|not configured|"status":\s*(404|401|403)\b|\b(404|401|403)\b/i.test(m);
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
export function dryRunTeam(team, request, { appoint = null } = {}) {
|
|
48
58
|
const t = normalizeTeam(team);
|
|
49
59
|
const roles = t.roles.map((r) => {
|
|
@@ -85,7 +95,13 @@ export async function runTeam({
|
|
|
85
95
|
const tasksOut = [];
|
|
86
96
|
const say = (type, payload = {}) => emit(type, { runId: id, at: now(), ...payload });
|
|
87
97
|
const roleOf = (rid) => t.roles.find((r) => r.id === rid);
|
|
88
|
-
|
|
98
|
+
// `exclude` holds what failed as unavailable this run; a re-appointment skips it. A role's
|
|
99
|
+
// pinned model is tried first and, when it is the one that failed, the roster steps in.
|
|
100
|
+
const modelFor = (r, exclude = null) => {
|
|
101
|
+
if (exclude?.size && r.model && exclude.has(r.model)) return appoint ? appoint({ ...r, model: undefined }, { exclude }) : null;
|
|
102
|
+
return (appoint ? appoint(r, { exclude }) : null) || (r.model && !exclude?.has(r.model) ? { model: r.model, mode: r.mode } : null);
|
|
103
|
+
};
|
|
104
|
+
const MAX_APPOINTMENTS = 3;
|
|
89
105
|
const stopped = () => !!signal?.aborted;
|
|
90
106
|
|
|
91
107
|
say('run.started', { team: t.name, request: String(request || ''), budget: t.budget, roles: t.roles.map((r) => r.id) });
|
|
@@ -132,8 +148,6 @@ export async function runTeam({
|
|
|
132
148
|
const r = await runRecipe(role.recipe, { request: String(request || ''), task: task.prompt });
|
|
133
149
|
text = typeof r === 'string' ? r : JSON.stringify(r);
|
|
134
150
|
} else {
|
|
135
|
-
const m = modelFor(role);
|
|
136
|
-
if (!m?.model) throw new Error(`no model for role "${role.id}"`);
|
|
137
151
|
// A call's tokens are unknown until it returns; what can be asked beforehand is
|
|
138
152
|
// whether the budget is already exhausted and whether one more call is allowed.
|
|
139
153
|
if (!budget.canAfford({ tokens: 0 })) { overBudget = true; throw new Error('over budget'); }
|
|
@@ -141,16 +155,27 @@ export async function runTeam({
|
|
|
141
155
|
const prompt = [task.prompt, prior, findingsInstruction()].filter(Boolean).join('\n\n');
|
|
142
156
|
// A host may build a toolset asynchronously (connecting MCP servers takes time).
|
|
143
157
|
const tools = await toolsFor(role);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
// A model that is not there — not deployed, no key, gone — is not the task failing:
|
|
159
|
+
// the next model on the roster is appointed and the task tried again, up to three
|
|
160
|
+
// models. Anything else (a refusal, a timeout, a bad request) fails the task.
|
|
161
|
+
const exclude = new Set();
|
|
162
|
+
for (let attempt = 1; ; attempt++) {
|
|
163
|
+
const m = modelFor(role, exclude);
|
|
164
|
+
if (!m?.model) throw new Error(exclude.size ? `no model left for role "${role.id}" after ${[...exclude].join(', ')}` : `no model for role "${role.id}"`);
|
|
165
|
+
if (attempt > 1) say('task.reappointed', { taskId: task.id, role: role.id, model: m.model, after: [...exclude] });
|
|
166
|
+
const res = await callModel({
|
|
167
|
+
runId: id, taskId: task.id, role: role.id, model: m.model, mode: m.mode || role.mode,
|
|
168
|
+
system: role.prompt, prompt, tools, signal,
|
|
169
|
+
onDelta: (delta, full) => say('task.delta', { taskId: task.id, role: role.id, delta, text: full }),
|
|
170
|
+
});
|
|
171
|
+
usage = res?.usage || null;
|
|
172
|
+
if (usage) budget.charge(usage);
|
|
173
|
+
if (res?.aborted) { status = 'stopped'; break; }
|
|
174
|
+
if (res?.ok) { text = String(res?.text || ''); break; }
|
|
175
|
+
const err = res?.error || 'the model did not answer';
|
|
176
|
+
if (attempt >= MAX_APPOINTMENTS || stopped() || !isModelUnavailable(err)) throw new Error(err);
|
|
177
|
+
exclude.add(m.model);
|
|
178
|
+
}
|
|
154
179
|
}
|
|
155
180
|
} catch (e) {
|
|
156
181
|
status = overBudget ? 'over-budget' : 'failed';
|
package/team-tool.js
CHANGED
|
@@ -16,8 +16,12 @@ import { dryRunTeam } from './team-run.js';
|
|
|
16
16
|
|
|
17
17
|
export const TEAM_TOOL_NAME = 'team';
|
|
18
18
|
|
|
19
|
+
// The teams the model may see and run: enabled, named, and whole. A record another client
|
|
20
|
+
// half-wrote (a name and nothing else) is not a team — Settings shows it for deleting.
|
|
21
|
+
const usable = (teams) => (teams || []).filter((t) => t?.name && t.enabled !== false && validateTeam(t).ok);
|
|
22
|
+
|
|
19
23
|
function catalogue(teams) {
|
|
20
|
-
const list = (teams
|
|
24
|
+
const list = usable(teams);
|
|
21
25
|
if (!list.length) return 'No teams saved yet.';
|
|
22
26
|
return `Saved teams: ${list.map((t) => `${t.name} (${(t.roles || []).map((r) => r.id).join(', ')})${t.description ? ` — ${t.description}` : ''}`).join('; ')}.`;
|
|
23
27
|
}
|
|
@@ -66,7 +70,7 @@ const json = (v) => JSON.stringify(v);
|
|
|
66
70
|
* @param saveTeam `async (team) => void`
|
|
67
71
|
*/
|
|
68
72
|
export function teamToolProvider({ teams = [], run = null, appoint = null, confirmSave = null, saveTeam = null } = {}) {
|
|
69
|
-
const byName = new Map((teams
|
|
73
|
+
const byName = new Map(usable(teams).map((t) => [t.name, t]));
|
|
70
74
|
let bound = null;
|
|
71
75
|
return {
|
|
72
76
|
id: 'team',
|
package/team-trail.js
CHANGED
|
@@ -10,6 +10,7 @@ export function teamLine(ev) {
|
|
|
10
10
|
case 'run.started': return { type: 'status', text: `team ${ev.team}: ${(ev.roles || []).join(', ')}` };
|
|
11
11
|
case 'plan.ready': return { type: 'status', text: `plan: ${(ev.tasks || []).length} task${(ev.tasks || []).length === 1 ? '' : 's'} (${ev.by})` };
|
|
12
12
|
case 'task.started': return { type: 'tool', name: role, text: `${role} · ${ev.title || ev.taskId}` };
|
|
13
|
+
case 'task.reappointed': return { type: 'status', text: `${role} → ${ev.model} (${(ev.after || []).join(', ')} unavailable)` };
|
|
13
14
|
case 'task.tool': return { type: 'tool', name: ev.name, text: `${role} ran ${ev.name}${ev.text ? ` — ${ev.text}` : ''}` };
|
|
14
15
|
case 'task.finding': return { type: 'status', text: `${role}: ${String(ev.finding?.text || '').slice(0, 140)}` };
|
|
15
16
|
case 'task.done': return { type: 'status', text: `${role} done · ${ev.findings || 0} finding${ev.findings === 1 ? '' : 's'}` };
|