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)
Changes
@@@ -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"@@@ -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": {@@@ -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 () {});@@@ -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) : ''}@@@ -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',@@@ -113,6 +113,12 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImplassert.ok(page.text.includes(`<a href="${watch.replace(/&/g, '&')}" rel="noopener">your agent's desk</a>`) || page.text.includes(`<a href="${watch}" rel="noopener">your agent'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');
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.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-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"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -915,9 +915,9 @@
@@ -915,9 +915,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"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",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -30,7 +30,7 @@
@@ -30,7 +30,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.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-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"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -1,6 +1,8 @@
@@ -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 () {
(function () {
'use strict';
'use strict';
var LABEL = { tool_call: 'Used', tool_result: 'Got', text: 'Note' };
var LABEL = { tool_call: 'Used', tool_result: 'Got', text: 'Note' };
@@ -13,7 +15,30 @@
@@ -13,7 +15,30 @@
return n;
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() {
function start() {
+ var art0 = document.querySelector('article.task');
+ var view = art0 ? session(art0) : null;
var art = document.querySelector('article.task[data-open="1"]');
var art = document.querySelector('article.task[data-open="1"]');
if (!art || !window.EventSource || art.dataset.live) return;
if (!art || !window.EventSource || art.dataset.live) return;
art.dataset.live = '1';
art.dataset.live = '1';
@@ -88,6 +113,7 @@
@@ -88,6 +113,7 @@
function onOutput(e) {
function onOutput(e) {
onDesk(e);
onDesk(e);
onLook(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' || e.step === 'handoff') && e.agent) return attempt((e.text || e.agent).replace(/ takes it.*$/, ''));
if (e.step === 'plan') return undefined;
if (e.step === 'plan') return undefined;
if (e.step === 'check') {
if (e.step === 'check') {
@@ -105,7 +131,7 @@
@@ -105,7 +131,7 @@
var src = new EventSource('/api/v1/tasks/' + encodeURIComponent(id) + '/events?after=' + encodeURIComponent(art.getAttribute('data-last') || '0'));
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('output', function (m) { try { onOutput(JSON.parse(m.data)); } catch (err) { /* a malformed event is skipped */ } });
src.addEventListener('state', function (m) {
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) {
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)));
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 () {});
}).catch(function () {});
@@ -334,7 +334,10 @@ ${pageRows.next ? html`<p><a href="/tasks?before=${pageRows.next}">Older tasks</
@@ -334,7 +334,10 @@ ${pageRows.next ? html`<p><a href="/tasks?before=${pageRows.next}">Older tasks</
page(req, res, {
page(req, res, {
title: row.task.length > 60 ? `${row.task.slice(0, 59)}…` : row.task,
title: row.task.length > 60 ? `${row.task.slice(0, 59)}…` : row.task,
crumbs: [{ label: 'Your tasks', href: '/tasks' }, { label: '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],
styles: [showcase.STYLESHEET],
noReferrer: true,
noReferrer: true,
body: html`${open ? raw('<noscript><meta http-equiv="refresh" content="4"></noscript>') : ''}
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(
@@ -354,6 +357,7 @@ ${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(
</figure>` : ''}
</figure>` : ''}
<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>
<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>
<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>` : ''}
${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>
</section>
${t.explanation ? explanationBlock(t.explanation) : ''}
${t.explanation ? explanationBlock(t.explanation) : ''}
@@ -46,7 +46,7 @@ let RELEASE = 'dev';
@@ -46,7 +46,7 @@ let RELEASE = 'dev';
function setRelease(id) { if (id) RELEASE = String(id); }
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]
* crumbs [{ label, href }], jsonLd [nodes] (serialised by openvibe-shared/seo), styles [openvibe-shared stylesheet names]
*/
*/
function renderPage(o) {
function renderPage(o) {
@@ -77,7 +77,7 @@ function renderPage(o) {
@@ -77,7 +77,7 @@ function renderPage(o) {
const crumbs = (o.crumbs || []).length
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>`
? 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({
return shell.page({
name: SITE_NAME,
name: SITE_NAME,
lang: 'en',
lang: 'en',
@@ -113,6 +113,12 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
@@ -113,6 +113,12 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
assert.ok(page.text.includes(`<a href="${watch.replace(/&/g, '&')}" rel="noopener">your agent's desk</a>`) || page.text.includes(`<a href="${watch}" rel="noopener">your agent's desk</a>`), 'the desk link is clickable');
assert.ok(page.text.includes(`<a href="${watch.replace(/&/g, '&')}" rel="noopener">your agent's desk</a>`) || page.text.includes(`<a href="${watch}" rel="noopener">your agent'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('href="https://evil.example'), 'another address stays text');
assert.ok(page.text.includes('data-bot="https://openvibe.bot"'));
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 () => {
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');
const bea = t.network.addUser('desk-bea');