openvibers/openvibe.actor Commits

Actor's task page shows the agents' session (openvibe-shared/agent-session.js): every step with its call and result, each agent that tried, checks and hand-offs, live and after the end; only the person's desk links; openvibe-shared v4.24.3 (#160)

OP OpenVibers committed 16 days ago · 8bb146ae6c · 6 files, +48 −12

Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex 4b8e7d3..0b8d42d 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.23.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.24.3",
"pg": "^8.23.0"
},
"devDependencies": {
@@@ -915,9 +915,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.23.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.23.0",
-- "integrity": "sha512-oLcUXmHOOC2CCpAuwCN95TIoadukLG5lC6YHPhPbYKcS/rZh0iwLbEA0aAzYHjalKGrqu9mw896LcFc3cd5xWg==",
++ "version": "4.24.3",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.24.3",
++ "integrity": "sha512-WCloIW+ggGE6xKWzdRH4Ojhk3p1XTO5kVUCDB99i7Lo1V+O7YRVBINQrArpEglO5n16Fk+yRhKnorCVIUs2rxQ==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex 95cef57..b96a162 100644
---- a/package.json
++++ b/package.json
@@@ -30,7 +30,7 @@
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.23.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.24.3",
"pg": "^8.23.0"
},
"engines": {
ddiff --git a/public/js/task.js b/public/js/task.js
iindex 9610567..0f8f0fe 100644
---- a/public/js/task.js
++++ b/public/js/task.js
@@@ -1,6 +1,8 @@
--/* A running task's page: follows its live stream (GET /api/v1/tasks/:id/events, actor.task-event@1) and draws each step
-- * the way the server renders a finished task (one block per agent that tries, its steps, its check); when the task ends
-- * the page reloads to show the outcome. Without this script the page refreshes itself every few seconds (<noscript>). */
++/* A task's page. With openvibe-shared/agent-session.js on the page, How it went is the agents' session: every step (its
++ * call and its result), each agent that tried, its checks and hand-offs, from the events the page carries and, while the
++ * task runs, from its live stream (GET /api/v1/tasks/:id/events, actor.task-event@1). Without it, a running task's steps
++ * are drawn the way the server renders a finished one. When the task ends the page reloads to show the outcome. Without
++ * this script the page refreshes itself every few seconds (<noscript>). */
(function () {
'use strict';
var LABEL = { tool_call: 'Used', tool_result: 'Got', text: 'Note' };
@@@ -13,7 +15,30 @@
return n;
}
++ // The session view over How it went: the events the page carries, then (while open) the live ones.
++ function session(art) {
++ var data = document.getElementById('task-events');
++ var box = document.getElementById('attempts');
++ if (!window.OpenVibeAgentSession || !data || !box || art.dataset.session) return null;
++ var events;
++ try { events = JSON.parse(data.textContent); } catch (err) { return null; }
++ art.dataset.session = '1';
++ var host = el('div', 'task-session');
++ box.parentNode.insertBefore(host, box);
++ box.hidden = true; box.style.display = 'none'; // .attempts sets a display of its own, which [hidden] alone does not beat
++ // Only the person's own desk becomes a link (Desks plan D4): an agent reads untrusted pages, so any other address stays text.
++ var botRe = (art.getAttribute('data-bot') || 'https://openvibe.bot').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
++ var ownDesk = new RegExp('^' + botRe + '/machines/dev_[0-9A-HJKMNP-TV-Z]{26}/screen(?:\\?desk=[a-z0-9-]{1,40})?$');
++ var view = window.OpenVibeAgentSession.mount(host, { title: (document.querySelector('.task-text') || {}).textContent || 'Task', status: art.getAttribute('data-open') === '1' ? 'running' : 'queued', follow: art.getAttribute('data-open') === '1', empty: 'Waiting for an agent to start…', link: function (u) { return ownDesk.test(u); } });
++ view.pushAll(events);
++ var pill = art.className.match(/state-([a-z_]+)/);
++ if (pill && art.getAttribute('data-open') !== '1') view.status(pill[1] === 'done' ? 'succeeded' : pill[1]);
++ return view;
++ }
++
function start() {
++ var art0 = document.querySelector('article.task');
++ var view = art0 ? session(art0) : null;
var art = document.querySelector('article.task[data-open="1"]');
if (!art || !window.EventSource || art.dataset.live) return;
art.dataset.live = '1';
@@@ -88,6 +113,7 @@
function onOutput(e) {
onDesk(e);
onLook(e);
++ if (view) { view.push(e); return undefined; }
if ((e.step === 'plan' || e.step === 'handoff') && e.agent) return attempt((e.text || e.agent).replace(/ takes it.*$/, ''));
if (e.step === 'plan') return undefined;
if (e.step === 'check') {
@@@ -105,7 +131,7 @@
var src = new EventSource('/api/v1/tasks/' + encodeURIComponent(id) + '/events?after=' + encodeURIComponent(art.getAttribute('data-last') || '0'));
src.addEventListener('output', function (m) { try { onOutput(JSON.parse(m.data)); } catch (err) { /* a malformed event is skipped */ } });
src.addEventListener('state', function (m) {
-- try { var s = JSON.parse(m.data).state; if (liveState && STATE[s]) liveState.textContent = STATE[s] + '…'; } catch (err) { /* ignore */ }
++ try { var ev = JSON.parse(m.data), s = ev.state; if (liveState && STATE[s]) liveState.textContent = STATE[s] + '…'; if (view) view.push(ev); } catch (err) { /* ignore */ }
fetch('/api/v1/tasks/' + encodeURIComponent(id), { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (t) {
if (t && t.cost && cost) cost.textContent = cost.textContent.replace(/^\$[0-9.]+/, '$' + (t.cost.usd < 0.01 ? t.cost.usd.toFixed(4) : t.cost.usd.toFixed(2)));
}).catch(function () {});
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex a98cee1..054f9ec 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -334,7 +334,10 @@ ${pageRows.next ? html`<p><a href="/tasks?before=${pageRows.next}">Older tasks</
page(req, res, {
title: row.task.length > 60 ? `${row.task.slice(0, 59)}…` : row.task,
crumbs: [{ label: 'Your tasks', href: '/tasks' }, { label: 'Task' }],
-- scripts: open ? ['js/task.js'] : [],
++ // How it went is the agents' session (openvibe-shared/agent-session.js, js/task.js mounts it from the events
++ // below and, while the task runs, from its live stream); the attempts drawn here stay for a page without scripts.
++ scripts: ['js/task.js'],
++ sharedScripts: ['agent-session.js'],
styles: [showcase.STYLESHEET],
noReferrer: true,
body: html`${open ? raw('<noscript><meta http-equiv="refresh" content="4"></noscript>') : ''}
@@@ -354,6 +357,7 @@ ${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(
</figure>` : ''}
<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>
<div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div>
++<script type="application/json" id="task-events">${raw(JSON.stringify(events.slice(-3000)).replace(/</g, '\\u003c'))}</script>
${open ? html`<p class="muted small live-note" id="live-note"><span class="dot-pulse" aria-hidden="true"></span> Updating as the agent works.</p>` : ''}
</section>
${t.explanation ? explanationBlock(t.explanation) : ''}
ddiff --git a/server/render/layout.js b/server/render/layout.js
iindex e57b82e..af118eb 100644
---- a/server/render/layout.js
++++ b/server/render/layout.js
@@@ -46,7 +46,7 @@ let RELEASE = 'dev';
function setRelease(id) { if (id) RELEASE = String(id); }
/**
-- * o: title, description, body (html), viewer, config, path, index (default false), scripts [rel],
++ * o: title, description, body (html), viewer, config, path, index (default false), scripts [rel], sharedScripts [openvibe-shared file names],
* crumbs [{ label, href }], jsonLd [nodes] (serialised by openvibe-shared/seo), styles [openvibe-shared stylesheet names]
*/
function renderPage(o) {
@@@ -77,7 +77,7 @@ function renderPage(o) {
const crumbs = (o.crumbs || []).length
? html`<nav class="crumbs" aria-label="Breadcrumbs">${o.crumbs.map((c, i) => html`${i ? ' / ' : ''}${c.href ? html`<a href="${c.href}">${c.label}</a>` : c.label}`)}</nav>`
: '';
-- const scripts = (o.scripts || []).map((rel) => html`<script src="${asset(rel)}" defer></script>`);
++ const scripts = (o.sharedScripts || []).map((name) => html`<script src="${ovServe.url(name)}" defer></script>`).concat((o.scripts || []).map((rel) => html`<script src="${asset(rel)}" defer></script>`));
return shell.page({
name: SITE_NAME,
lang: 'en',
ddiff --git a/test/desk-handoff.test.js b/test/desk-handoff.test.js
iindex cce32d3..a1ffb82 100644
---- a/test/desk-handoff.test.js
++++ b/test/desk-handoff.test.js
@@@ -113,6 +113,12 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
assert.ok(page.text.includes(`<a href="${watch.replace(/&/g, '&amp;')}" rel="noopener">your agent&#39;s desk</a>`) || page.text.includes(`<a href="${watch}" rel="noopener">your agent&#39;s desk</a>`), 'the desk link is clickable');
assert.ok(!page.text.includes('href="https://evil.example'), 'another address stays text');
assert.ok(page.text.includes('data-bot="https://openvibe.bot"'));
++ // How it went is the agents' session (openvibe-shared/agent-session.js): the page carries its events as data,
++ // where nothing an agent wrote can end the script early.
++ assert.ok(/<script src="[^"]*agent-session\.js[^"]*" defer>/.test(page.text), 'the session view');
++ const data = /<script type="application\/json" id="task-events">([\s\S]*?)<\/script>/.exec(page.text);
++ assert.ok(data && JSON.parse(data[1]).some((e) => e.step === 'plan'), 'the events, as data');
++ assert.ok(!/<\/?[a-z]/i.test(data[1]), 'no markup inside the data');
});
await check('the task\'s record says it needs its person while the hand-off waits, and stops saying so after', async () => {
const bea = t.network.addUser('desk-bea');