openvibers/openvibe.shared Commits

4.24.1: agent-session reads OpenVibe.Actor's task events too (#151)

OP OpenVibers committed 16 days ago · 30806996d6 · 8 files, +101 −16

* 4.24.1: agent-session reads OpenVibe.Actor's task events (steps paired by tool, the head follows the agents, checks, states)

* 4.24.1: Actor's verifying state reads 'Checking the answer', still live

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex e88b29e..efe7145 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -6,6 +6,18 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
++## 4.24.1 — 2026-10-28
++
++- **`agent-session.js` reads OpenVibe.Actor's task events** (`actor.task-event@1`), so Actor's agents show in the same
++ view as every coding agent. The owner asked that it "handle that between all agents… in a smart way".
++ - **Mapping:** plan is a quiet line, text what it said (an error a note), a hand-off a divider, a check a note (a passed
++ check is good news, not a problem), and `tool_call`/`tool_result` are steps. A result without an id finds its step
++ by tool.
++ - **Agents:** any line may name its `agent`, and the head follows who is working. A hand-off without a `from` is the
++ agent that was working.
++ - **States:** `{ t: 'state' }` lines (Actor's task states) move the head.
++ - **No duplicates:** an agent's start after its hand-off no longer lists it twice.
++
## 4.24.0 — 2026-10-28
- **`agent-session.js`: a coding agent's session, as people read it.** Asked for by the owner, 2026-10-28, showing
ddiff --git a/README.md b/README.md
iindex 131be12..a00e107 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.24.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.24.1 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex f69ac6e..6b1bbf0 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.24.0",
++ "version": "4.24.1",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
ddiff --git a/agent-session.js b/agent-session.js
iindex 72e1a74..2ee4605 100644
---- a/agent-session.js
++++ b/agent-session.js
@@@ -8,9 +8,10 @@
* between all agents… in a smart way… way better than this idea."
*
* const view = OpenVibeAgentSession.mount(el, { title, status: 'running', startedAt })
-- * view.push(event) one event: a Codes job line ({ t: 'tool', … }) or any Claude Code stream-json event
-- * ({ type: 'assistant', … }); every OpenVibe.Codes harness adapter (Claude Code, Codex,
-- * OpenCode, Command Code, Aider, DeepSeek, OpenVibe.AI, your own model) speaks one of them
++ * view.push(event) one event: a Codes job line ({ t: 'tool', … }), any Claude Code stream-json event
++ * ({ type: 'assistant', … }) or an OpenVibe.Actor task event ({ kind: 'output', step, … },
++ * actor.task-event@1); every OpenVibe.Codes harness adapter (Claude Code, Codex, OpenCode,
++ * Command Code, Aider, DeepSeek, OpenVibe.AI, your own model) and every Actor agent speaks one
* view.pushAll(events)
* view.status('succeeded' | 'failed' | 'cancelled' | 'running' | 'queued' | …, label?)
* view.summary() → { agents, steps, commands, files, added, removed, problems, costUsd, durationMs }
@@@ -30,6 +31,8 @@
* { t: 'usage', input_tokens, output_tokens }
* { t: 'done', ok, text, duration_ms, cost_usd, turns }
* A line from a sub-agent carries `parent`: the id of the Task step that started it, so it nests under that step.
++ * Any line may carry `agent` (the head then follows who is working); a step without an id gets one, and the next result
++ * of the same tool without an id is its result. { t: 'state', state } moves the head (Actor's task states).
*
* Everything is text: no line's content ever becomes markup (links are https only). It needs nothing else; the look
* follows the page's theme tokens (kit.css's), light or dark.
@@@ -85,9 +88,25 @@
if (Array.isArray(content)) return content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join('\n');
return content == null ? '' : String(content);
}
++ // OpenVibe.Actor's task events (actor.task-event@1): { kind: 'state' | 'end', state } and { kind: 'output', step, agent,
++ // text, tool, input, is_error }, step one of plan, text, handoff, check, tool_call, tool_result.
++ function fromActor(e) {
++ const agent = e.agent ? { agent: String(e.agent) } : {};
++ if (e.kind === 'state' || e.kind === 'end') return e.state ? [{ t: 'state', state: String(e.state), ...agent }] : [];
++ if (e.kind !== 'output') return [];
++ const text = cut(e.text, MAX_TEXT);
++ if (e.step === 'text') return [e.is_error ? { t: 'note', level: 'error', text, ...agent } : { t: 'text', text, ...agent }];
++ if (e.step === 'plan') return [{ t: 'step', text, ...agent }];
++ if (e.step === 'handoff') return [{ t: 'handoff', from: null, to: e.agent || null, reason: text, ...agent }];
++ if (e.step === 'check') return [{ t: 'note', level: /^checked/i.test(text) ? 'info' : 'warning', text, ...agent }];
++ if (e.step === 'tool_call') return [{ t: 'tool', name: String(e.tool || 'tool'), what: typeof e.input === 'string' ? cut(e.input, 300) : '', input: e.input && typeof e.input === 'object' ? e.input : { description: typeof e.input === 'string' ? cut(e.input, 300) : '' }, ...agent }];
++ if (e.step === 'tool_result') return [{ t: 'tool_result', name: String(e.tool || 'tool'), ok: !e.is_error, output: cut(e.text, 8000), ...agent }];
++ return text ? [{ t: 'text', text, ...agent }] : [];
++ }
function normalize(e) {
if (!e || typeof e !== 'object') return [];
if (e.t) return [e];
++ if (e.kind) return fromActor(e);
const parent = e.parent_tool_use_id || null;
if (e.type === 'system' && e.subtype === 'init') return [{ t: 'agent', agent: e.agent || 'claude', model: e.model || null }];
if (e.type === 'system' && e.subtype === 'handoff') return [{ t: 'handoff', from: e.from, to: e.to || null, reason: cut(e.reason, 600) }];
@@@ -205,7 +224,7 @@
.ovas-head { position: sticky; top: var(--ovnav-real-h, 0px); z-index: 2; display: grid; gap: 8px; padding: 12px 14px; border-radius: 16px; border: 1px solid var(--ovas-b); background: color-mix(in srgb, var(--ovas-s) 92%, transparent); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.ovas-top { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ovas-pill { display: inline-flex; align-items: center; gap: 6px; flex: none; height: 26px; padding: 0 10px; border-radius: 999px; font-size: 12.5px; font-weight: 700; background: var(--ovas-t); color: var(--ovas-sec); }
--.ovas-pill[data-s="running"], .ovas-pill[data-s="placed"] { color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
++.ovas-pill[data-s="running"], .ovas-pill[data-s="placed"], .ovas-pill[data-s="verifying"] { color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
.ovas-pill[data-s="succeeded"] { color: var(--ovas-ok); background: color-mix(in srgb, #22c55e 15%, transparent); }
.ovas-pill[data-s="failed"], .ovas-pill[data-s="expired"] { color: var(--ovas-bad); background: color-mix(in srgb, #ef4444 15%, transparent); }
.ovas-title { flex: 1; min-width: 0; font-weight: 700; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@@ -289,6 +308,8 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
.ovas-hand small { font-size: 12.5px; color: var(--ovas-mut); line-height: 1.4; }
.ovas-note { display: flex; gap: 8px; align-items: flex-start; padding: 9px 12px; border-radius: 12px; font-size: 13.5px; background: color-mix(in srgb, #f59e0b 12%, transparent); color: var(--text-primary, #e6edf7); }
.ovas-note[data-level="error"] { background: color-mix(in srgb, #ef4444 13%, transparent); }
++.ovas-note[data-level="info"] { background: color-mix(in srgb, #22c55e 10%, transparent); }
++.ovas-note[data-level="info"] .ovas-ic { color: var(--ovas-ok); }
.ovas-note .ovas-ic { color: #f59e0b; margin-top: 2px; } .ovas-note[data-level="error"] .ovas-ic { color: var(--ovas-bad); }
.ovas-done { display: grid; gap: 6px; padding: 12px 14px; border-radius: 14px; border: 1px solid color-mix(in srgb, #22c55e 40%, transparent); background: color-mix(in srgb, #22c55e 8%, transparent); }
.ovas-done[data-ok="0"] { border-color: color-mix(in srgb, #ef4444 40%, transparent); background: color-mix(in srgb, #ef4444 8%, transparent); }
@@@ -414,10 +435,17 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
function feedFor(parent) { const p = parent && S.steps.get(parent); return p && p.subEl ? p.subEl : feed; }
function appendLi(target, li) { if (target === feed) { if (empty.parentNode) empty.remove(); feed.appendChild(li); } else target.appendChild(li); }
++ const pending = new Map(); // tool name → ids of its steps still waiting for a result (events without ids)
function onLine(x) {
const target = feedFor(x.parent);
++ // Who is working: a line that names a different agent than the last moves the head along.
++ if (x.agent && x.t !== 'agent' && x.t !== 'handoff' && (!S.agents.length || S.agents[S.agents.length - 1].agent !== x.agent)) { S.agents.push({ agent: x.agent }); paintHead(); }
++ if (x.t === 'tool' && !x.id) { x = { ...x, id: `a${S.steps.size + 1}` }; if (!pending.has(x.name)) pending.set(x.name, []); pending.get(x.name).push(x.id); }
++ if (x.t === 'tool_result' && !x.id) { const q = pending.get(x.name); x = { ...x, id: q && q.length ? q.shift() : '' }; }
++ if (x.t === 'state') { status(x.state); return; }
if (x.t === 'agent') {
-- S.agents.push({ agent: x.agent, model: x.model });
++ const last = S.agents[S.agents.length - 1];
++ if (last && last.agent === x.agent) { if (x.model) last.model = x.model; } else S.agents.push({ agent: x.agent, model: x.model });
paintHead(); return;
}
if (x.t === 'text') {
@@@ -455,13 +483,16 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
const li = el('li', 'ovas-handoff'); li.dataset.k = 'handoff'; li.dataset.p = '1';
const s = el('div', 'ovas-hand');
const row = el('div', 'ovas-hand-row');
-- row.appendChild(el('span', 'ovas-agent', agentName(x.from))); row.insertAdjacentHTML('beforeend', svg('handoff')); row.appendChild(el('span', 'ovas-agent is-now', x.to ? agentName(x.to) : 'the next agent'));
++ const from = x.from || (S.agents.length ? S.agents[S.agents.length - 1].agent : '');
++ if (x.to && (!S.agents.length || S.agents[S.agents.length - 1].agent !== x.to)) S.agents.push({ agent: x.to });
++ row.appendChild(el('span', 'ovas-agent', agentName(from))); row.insertAdjacentHTML('beforeend', svg('handoff')); row.appendChild(el('span', 'ovas-agent is-now', x.to ? agentName(x.to) : 'the next agent'));
s.appendChild(row); if (x.reason) s.appendChild(el('small', '', x.reason));
li.appendChild(s); appendLi(feed, li); S.problems++; paintHead(); return;
}
if (x.t === 'note') {
-- const li = el('li', 'ovas-note'); li.dataset.k = 'note'; li.dataset.p = '1'; li.dataset.level = x.level === 'warning' ? 'warning' : 'error';
-- li.innerHTML = svg('warn'); li.appendChild(el('span', '', x.text)); appendLi(target, li); S.problems++; paintHead(); return;
++ const li = el('li', 'ovas-note'); li.dataset.k = 'note'; li.dataset.level = x.level === 'warning' || x.level === 'info' ? x.level : 'error';
++ if (li.dataset.level !== 'info') li.dataset.p = '1';
++ li.innerHTML = svg(li.dataset.level === 'info' ? 'ok' : 'warn'); li.appendChild(el('span', '', x.text)); appendLi(target, li); if (li.dataset.level !== 'info') S.problems++; paintHead(); return;
}
if (x.t === 'step') {
const li = el('li', 'ovas-line'); li.dataset.k = 'line'; li.appendChild(el('span', '', x.text)); appendLi(feed, li); return;
@@@ -500,12 +531,12 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
problems: S.problems, costUsd: Math.round(S.cost * 1e4) / 1e4, durationMs: S.durationMs || ((S.endedAt || Date.now()) - S.startedAt), tokens: { ...S.tokens },
};
}
-- const LABEL = { queued: 'Waiting', placed: 'Starting', running: 'Running', succeeded: 'Finished', failed: 'Failed', cancelled: 'Cancelled', expired: 'Expired' };
++ const LABEL = { queued: 'Waiting', placed: 'Starting', running: 'Running', verifying: 'Checking the answer', succeeded: 'Finished', failed: 'Failed', cancelled: 'Cancelled', expired: 'Expired' };
function paintHead() {
const sm = summary();
const pill = head.querySelector('.ovas-pill');
pill.dataset.s = S.status;
-- pill.innerHTML = `${S.status === 'running' || S.status === 'placed' ? '<span class="ovas-spin"></span>' : S.status === 'succeeded' ? svg('ok') : S.status === 'failed' || S.status === 'expired' ? svg('fail') : ''}<span></span>`;
++ pill.innerHTML = `${S.status === 'running' || S.status === 'placed' || S.status === 'verifying' ? '<span class="ovas-spin"></span>' : S.status === 'succeeded' ? svg('ok') : S.status === 'failed' || S.status === 'expired' ? svg('fail') : ''}<span></span>`;
pill.lastChild.textContent = S.label || LABEL[S.status] || S.status;
const ag = head.querySelector('.ovas-agents'); ag.textContent = '';
S.agents.forEach((a, i) => { if (i) ag.appendChild(el('span', '', '→')); const c = el('span', `ovas-agent${i === S.agents.length - 1 ? ' is-now' : ''}`, agentName(a.agent)); if (a.model) c.title = a.model; ag.appendChild(c); });
@@@ -517,7 +548,7 @@ details[open] > summary .ovas-chev { transform: rotate(90deg); }
}
function status(s, label) {
S.status = s; S.label = label || '';
-- if (!['running', 'placed', 'queued'].includes(s)) { S.endedAt = S.endedAt || Date.now(); empty.textContent = 'Nothing was recorded for this run.'; }
++ if (!['running', 'placed', 'queued', 'verifying'].includes(s)) { S.endedAt = S.endedAt || Date.now(); empty.textContent = 'Nothing was recorded for this run.'; }
paintHead();
}
const tick = setInterval(() => { if (S.status === 'running' || S.status === 'placed') { S.steps.forEach((st) => { if (!st.done) setStepState(st); }); paintHead(); } }, 1000);
ddiff --git a/docs/agent-session.md b/docs/agent-session.md
iindex 065bc05..adfd8a3 100644
---- a/docs/agent-session.md
++++ b/docs/agent-session.md
@@@ -59,7 +59,7 @@ agents… in a smart way… way better than this idea."
**Theme:** the colours come from the page's theme tokens, so it reads in light and dark.
--## The two shapes it reads
++## The shapes it reads
**1. Claude Code's stream-json**, the shape every Codes harness adapter emits. See OpenVibe.Codes' `harness/events.js`:
`system/init`, `assistant` text and `tool_use`, `user` `tool_result`, `system` warning/error/handoff/usage, and one
@@@ -81,4 +81,16 @@ agents… in a smart way… way better than this idea."
| `{ t: 'usage', input_tokens, output_tokens }` | |
| `{ t: 'done', ok, text, duration_ms, cost_usd, turns }` | the end |
--A line with `parent` belongs to the helper whose Task step has that id.
++A line with `parent` belongs to the helper whose Task step has that id. Any line may carry `agent`; the head follows
++who is working. `{ t: 'state', state }` moves the head.
++
++**3. OpenVibe.Actor's task events** (`actor.task-event@1`, since 4.24.1):
++
++| Event | Shown as |
++| --- | --- |
++| `{ kind: 'state' \| 'end', state }` | the head's state |
++| `{ kind: 'output', step: 'plan', text }` | a quiet line |
++| `{ kind: 'output', step: 'text', text, is_error? }` | what it said (an error as a note) |
++| `{ kind: 'output', step: 'handoff', agent, text }` | a divider to `agent` |
++| `{ kind: 'output', step: 'check', text }` | a note ("Checked…" is good news) |
++| `{ kind: 'output', step: 'tool_call' \| 'tool_result', tool, input / text }` | steps, paired by tool |
ddiff --git a/package.json b/package.json
iindex 205be90..f651871 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.24.0",
++ "version": "4.24.1",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/agent-session-chrome.test.js b/test/agent-session-chrome.test.js
iindex 0e55fb9..c5a3d04 100644
---- a/test/agent-session-chrome.test.js
++++ b/test/agent-session-chrome.test.js
@@@ -88,6 +88,27 @@ const server = http.createServer((req, res) => {
await p.evaluate(`document.querySelector('.ovas-chip[data-f="problems"]').click(), 1`);
v = await p.evaluate(`[...document.querySelectorAll('.ovas-feed > li')].filter((li) => !li.hidden).map((li) => li.dataset.k)`);
assert.deepStrictEqual(v, ['steps', 'handoff'], 'Problems: the group with the failed step and the hand-off');
++ // OpenVibe.Actor's task events: a step and its result pair by tool (no ids), the head follows the agents, a check
++ // that passed is good news (not a problem), and the task's states move the head.
++ await p.evaluate(`(() => { const d = document.createElement('div'); d.id = 'actor'; document.body.appendChild(d); window.a = OpenVibeAgentSession.mount(d, { title: 'Find the ferry', status: 'queued' }); return 1; })()`);
++ const pa = (e) => p.evaluate(`window.a.push(${JSON.stringify(e)}), 1`);
++ await pa({ kind: 'state', state: 'running' });
++ await pa({ kind: 'output', step: 'plan', agent: 'deepseek', text: 'DeepSeek takes it' });
++ await pa({ kind: 'output', step: 'tool_call', agent: 'deepseek', tool: 'web_search', input: 'ferry times' });
++ await pa({ kind: 'output', step: 'tool_result', agent: 'deepseek', tool: 'web_search', text: '3 results' });
++ await pa({ kind: 'output', step: 'check', agent: 'deepseek', text: 'Checked by another model' });
++ await pa({ kind: 'output', step: 'handoff', agent: 'claude', text: 'Claude takes it: a longer answer' });
++ await pa({ kind: 'output', step: 'text', agent: 'claude', text: 'The ferry leaves at 8:40.' });
++ await pa({ kind: 'end', state: 'succeeded' });
++ v = await p.evaluate(`(() => { const r = document.getElementById('actor'); return { state: r.querySelector('.ovas-step summary .ovas-state').dataset.s, out: r.querySelector('.ovas-out').textContent,
++ agents: [...r.querySelectorAll('.ovas-head .ovas-agent')].map((x) => x.textContent), hand: [...r.querySelectorAll('.ovas-handoff .ovas-agent')].map((x) => x.textContent),
++ info: r.querySelector('.ovas-note').dataset.level, pill: r.querySelector('.ovas-pill').textContent, problems: window.a.summary().problems } })()`);
++ assert.deepStrictEqual([v.state, v.out], ['ok', '3 results'], 'the result found its step');
++ assert.deepStrictEqual(v.agents, ['DeepSeek', 'Claude Code'], 'the head follows who is working');
++ assert.deepStrictEqual(v.hand, ['DeepSeek', 'Claude Code'], 'a hand-off without a from: the agent that was working');
++ assert.strictEqual(v.info, 'info');
++ assert.strictEqual(v.pill, 'Finished');
++ assert.strictEqual(v.problems, 1, 'only the hand-off counts as a problem, not the passed check');
const errors = p.state.errors.filter((e) => !/Failed to load resource/.test(e.text));
assert.deepStrictEqual(errors, [], JSON.stringify(errors));
console.log('agent-session-chrome: groups, live output, failures, helpers, hand-offs, filters and safe text ok');
ddiff --git a/test/agent-session.test.js b/test/agent-session.test.js
iindex 1c88ec9..c7d51b0 100644
---- a/test/agent-session.test.js
++++ b/test/agent-session.test.js
@@@ -21,4 +21,13 @@ assert.deepStrictEqual(['Bash', 'run_command', 'Read', 'Grep', 'Edit', 'write_fi
['command', 'command', 'read', 'search', 'edit', 'write', 'web', 'agent', 'plan', 'command', 'tool'], 'every agent\'s tools map to the same kinds');
assert.deepStrictEqual(as.diffLines('a\nb\nc', 'a\nB\nc'), [['=', 'a'], ['-', 'b'], ['+', 'B'], ['=', 'c']], 'a changed line is a removal and an addition');
assert.deepStrictEqual(as.diffLines('', 'x'), [['-', ''], ['+', 'x']]);
++// OpenVibe.Actor's task events (actor.task-event@1) read like the rest (4.24.1).
++assert.deepStrictEqual(as.normalize({ kind: 'state', state: 'running', seq: 2 }), [{ t: 'state', state: 'running' }]);
++assert.deepStrictEqual(as.normalize({ kind: 'output', step: 'plan', agent: 'deepseek', text: 'DeepSeek takes it' }), [{ t: 'step', text: 'DeepSeek takes it', agent: 'deepseek' }]);
++assert.deepStrictEqual(as.normalize({ kind: 'output', step: 'tool_call', agent: 'deepseek', tool: 'web_search', input: 'ferry times' }),
++ [{ t: 'tool', name: 'web_search', what: 'ferry times', input: { description: 'ferry times' }, agent: 'deepseek' }], 'a tool call without an id');
++assert.deepStrictEqual(as.normalize({ kind: 'output', step: 'tool_result', tool: 'web_search', text: '3 results', is_error: false }), [{ t: 'tool_result', name: 'web_search', ok: true, output: '3 results' }]);
++assert.deepStrictEqual(as.normalize({ kind: 'output', step: 'check', text: 'Checked by another model' }), [{ t: 'note', level: 'info', text: 'Checked by another model' }]);
++assert.deepStrictEqual(as.normalize({ kind: 'output', step: 'text', text: 'stopped', is_error: true }), [{ t: 'note', level: 'error', text: 'stopped' }]);
++assert.deepStrictEqual(as.normalize({ kind: 'output', step: 'handoff', agent: 'claude', text: 'Claude takes it' }), [{ t: 'handoff', from: null, to: 'claude', reason: 'Claude takes it', agent: 'claude' }]);
console.log('agent-session: lines, kinds and diffs ok');