@chatpanel/events 0.79.0 → 0.80.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.
@@ -48,9 +48,12 @@ export function appoint(role, candidates, { overrides = {}, exclude = null } = {
48
48
  if (m) return withTierAndMode(m);
49
49
  }
50
50
  const want = TIER_RANK[role.prefer] ?? 1;
51
+ // Ties go to ROSTER ORDER, not to the alphabet: the host lists what it trusts first (the
52
+ // model the person is already chatting with, installed agents), and over a gateway that
53
+ // lists eight hundred models the alphabet picks a provider nobody has used.
51
54
  const best = usable
52
- .map((c) => ({ c: withTierAndMode(c), d: Math.abs((TIER_RANK[classifyModel(c.model)] ?? 1) - want) }))
53
- .sort((a, b) => a.d - b.d || (a.c.name || a.c.id).localeCompare(b.c.name || b.c.id))[0];
55
+ .map((c, i) => ({ c: withTierAndMode(c), d: Math.abs((TIER_RANK[classifyModel(c.model)] ?? 1) - want), i }))
56
+ .sort((a, b) => a.d - b.d || a.i - b.i)[0];
54
57
  return best.c;
55
58
  }
56
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/events",
3
- "version": "0.79.0",
3
+ "version": "0.80.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
@@ -171,7 +171,14 @@ export async function runTeam({
171
171
  usage = res?.usage || null;
172
172
  if (usage) budget.charge(usage);
173
173
  if (res?.aborted) { status = 'stopped'; break; }
174
- if (res?.ok) { text = String(res?.text || ''); break; }
174
+ if (res?.ok) {
175
+ text = String(res?.text || '');
176
+ // A turn that ended with nothing to say — an agent that exited, a stream that
177
+ // died after its tool calls — is not a done task. Three members "completed"
178
+ // empty once, the run merged nothing, and the caller ran the team again.
179
+ if (!text.trim()) throw new Error('the model returned no answer');
180
+ break;
181
+ }
175
182
  const err = res?.error || 'the model did not answer';
176
183
  if (attempt >= MAX_APPOINTMENTS || stopped() || !isModelUnavailable(err)) throw new Error(err);
177
184
  exclude.add(m.model);
package/team-tool.js CHANGED
@@ -72,6 +72,10 @@ const json = (v) => JSON.stringify(v);
72
72
  export function teamToolProvider({ teams = [], run = null, appoint = null, confirmSave = null, saveTeam = null } = {}) {
73
73
  const byName = new Map(usable(teams).map((t) => [t.name, t]));
74
74
  let bound = null;
75
+ // One run per team+request per turn. A run that failed, answered with nothing, or ran
76
+ // out of budget comes back as a result the model must REPORT — asking for it again in the
77
+ // same turn is the circle a person had to break by hand.
78
+ const ran = new Map();
75
79
  return {
76
80
  id: 'team',
77
81
  specs: [teamToolSpec(teams)],
@@ -112,18 +116,21 @@ export function teamToolProvider({ teams = [], run = null, appoint = null, confi
112
116
  if (action === 'run') {
113
117
  if (!request) return json({ error: 'run needs a request — what should the team do?' });
114
118
  if (typeof run !== 'function') return json({ error: 'This surface cannot run a team.' });
119
+ const key = `${team.name}\n${request}`;
120
+ const prior = ran.get(key);
121
+ if (prior) return json({ error: `The "${team.name}" team already ran this request in this turn (run ${prior.runId}, ${prior.status}). Do not run it again: tell the user what happened — ${prior.summary} — and ask how to proceed.`, runId: prior.runId, status: prior.status, tasks: prior.tasks });
115
122
  const dry = dryRunTeam(team, request, { appoint });
116
123
  if (!dry.ok) return json({ error: `No model is available for role(s): ${dry.missing.join(', ')}.`, roles: dry.roles });
117
124
  const result = await run({ team, request, toolset: bound });
118
125
  const findings = (result.board || []).map((f) => ({ role: f.role, kind: f.kind, text: f.text, refs: f.refs }));
119
- return json({
120
- name: team.name, runId: result.runId, status: result.status,
121
- proposal: result.proposal,
122
- tasks: (result.tasks || []).map((x) => ({ id: x.id, role: x.role, status: x.status, ms: x.ms, findings: (x.findings || []).length, error: x.error || undefined })),
123
- findings,
124
- usage: result.usage,
125
- hint: result.status === 'over-budget' ? 'The team stopped at its budget; the proposal is what it had. Say so.' : undefined,
126
- });
126
+ const tasks = (result.tasks || []).map((x) => ({ id: x.id, role: x.role, status: x.status, ms: x.ms, findings: (x.findings || []).length, error: x.error || undefined }));
127
+ const failed = tasks.filter((x) => x.status !== 'ok');
128
+ const summary = failed.length ? failed.map((x) => `${x.role} ${x.status}${x.error ? ` (${x.error})` : ''}`).join('; ') : `${findings.length} findings`;
129
+ ran.set(key, { runId: result.runId, status: result.status, summary, tasks });
130
+ const hint = result.status === 'over-budget' ? 'The team stopped at its budget; the proposal is what it had. Say so.'
131
+ : result.status === 'failed' ? `The run FAILED — ${summary}. Do not run the team again this turn. Tell the user exactly which role failed and why, and ask whether to retry, change the team\'s models in Settings → Teams, or answer without the team.`
132
+ : failed.length ? `Some roles did not finish ${summary}. Say so alongside the proposal.` : undefined;
133
+ return json({ name: team.name, runId: result.runId, status: result.status, proposal: result.proposal, tasks, findings, usage: result.usage, hint });
127
134
  }
128
135
  return json({ error: `Unknown action "${action}". Use run, dry_run or save.` });
129
136
  },