@chatpanel/events 0.79.1 → 0.80.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/events",
3
- "version": "0.79.1",
3
+ "version": "0.80.1",
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
@@ -51,7 +51,10 @@ const strongestRole = (team) => [...team.roles].sort((a, b) => (TIER[b.prefer] ?
51
51
  */
52
52
  export function isModelUnavailable(error) {
53
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);
54
+ // Also: a relayed agent that exited, a provider that closed the stream, and a turn that
55
+ // came back with nothing — the model did not answer, and the next one might. A refusal,
56
+ // a timeout the caller set, a bad request or a budget stop are not this.
57
+ 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|500|502|503)\b|\b(404|401|403|502|503)\b|exited \d+|returned no answer|did not answer|closed the connection|couldn't reach|could not reach|ECONNREFUSED|overloaded|capacity/i.test(m);
55
58
  }
56
59
 
57
60
  export function dryRunTeam(team, request, { appoint = null } = {}) {
@@ -171,8 +174,12 @@ export async function runTeam({
171
174
  usage = res?.usage || null;
172
175
  if (usage) budget.charge(usage);
173
176
  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';
177
+ // A turn that ended with nothing to say — an agent that exited, a stream that
178
+ // died after its tool calls is not a done task: three members "completed" empty
179
+ // once, the run merged nothing, and the caller ran the team again. It is treated
180
+ // like an unavailable model, so the next one on the roster gets the task.
181
+ if (res?.ok && String(res?.text || '').trim()) { text = String(res.text); break; }
182
+ const err = res?.ok ? 'the model returned no answer' : (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);
178
185
  }
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
  },