openvibers/openvibe.actor Commits

Show AG-UI run steps on Actor task pages (#432)

OP OpenVibers committed 2 days ago · 5af6e22aa3 · 6 files, +53 −11

* Show AG-UI run steps and live steering on Actor tasks

* Merge remote-tracking branch 'origin/main' into feat/354-agent-view-actor

# Conflicts:
#	package-lock.json
#	package.json

Changes

ddiff --git a/docs/screenshots/ask-354-1280.png b/docs/screenshots/ask-354-1280.png
nnew file mode 100644
iindex 0000000..6d0da74
BBinary files /dev/null and b/docs/screenshots/ask-354-1280.png differ
ddiff --git a/docs/screenshots/ask-354-390.png b/docs/screenshots/ask-354-390.png
nnew file mode 100644
iindex 0000000..72caa71
BBinary files /dev/null and b/docs/screenshots/ask-354-390.png differ
ddiff --git a/package-lock.json b/package-lock.json
iindex adf7515..0b93571 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.403.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.204.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0",
"pg": "^8.23.0",
"sharp": "^0.34.5"
},
@@@ -1756,9 +1756,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "4.204.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.204.0",
-- "integrity": "sha512-T6xQehJHnzHdQFQrs8J44vT7jcziKC8kRpn+3VxyLxZvCcM1QZ6jv5LFCPUNYhppvwtpu4fPc7fmvrgfXjdQ6Q==",
++ "version": "4.205.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0",
++ "integrity": "sha512-Ch4rkxgGhjsGrdFDsJlZ6lzRrtH90MLdi00Huw7nJB8SDt7D8S3wpnxgOs5eQYx4wLHcFDWRmCKQ5FlVzsCaWA==",
"license": "MIT",
"dependencies": {
"clean-css": "^5.3.3",
ddiff --git a/package.json b/package.json
iindex 8131bcc..44c8521 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.403.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.204.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.205.0",
"pg": "^8.23.0",
"sharp": "^0.34.5"
},
ddiff --git a/public/js/task.js b/public/js/task.js
iindex 1c0f30a..f30c5f2 100644
---- a/public/js/task.js
++++ b/public/js/task.js
@@@ -17,6 +17,46 @@
return n;
}
++ function agentRun(art, host, oldView) {
++ if (!window.EventSource || !window.OpenVibeAgentSession) return;
++ var id = art.getAttribute('data-task');
++ var url = '/api/v1/runs/' + encodeURIComponent(id);
++ fetch(url, { credentials: 'same-origin' }).then(function (r) { return r.ok ? r.json() : null; }).then(function (run) {
++ if (!run || !host.isConnected) return;
++ oldView.destroy();
++ host.textContent = '';
++ var state = run.status || 'running';
++ var view = window.OpenVibeAgentSession.mount(host, {
++ title: run.title || 'Task', status: state, follow: art.getAttribute('data-open') === '1', files: false,
++ onSend: art.getAttribute('data-owner') === '1' && ['queued', 'running', 'verifying', 'succeeded', 'needs_you'].indexOf(state) !== -1 ? function (text) {
++ var working = ['queued', 'running', 'verifying'].indexOf(state) !== -1;
++ var action = working ? '/chat/' + encodeURIComponent(id) + '/steer'
++ : '/tasks/' + encodeURIComponent(id) + '/' + (state === 'needs_you' ? 'answer' : 'follow-up');
++ var body = new URLSearchParams(); body.set(state === 'needs_you' ? 'answer' : 'task', text);
++ return fetch(action, {
++ method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' }, body: body.toString()
++ }).then(function (r) {
++ if (!r.ok) throw new Error('The message could not be sent.');
++ if (!working) window.location.assign(r.url);
++ });
++ } : undefined
++ });
++ art.dataset.agentUi = '1';
++ var source = new EventSource(url + '/events');
++ ['RUN_STARTED', 'RUN_FINISHED', 'RUN_ERROR', 'STEP_STARTED', 'STEP_FINISHED', 'TEXT_MESSAGE_START',
++ 'TEXT_MESSAGE_CONTENT', 'TEXT_MESSAGE_END', 'TOOL_CALL_START', 'TOOL_CALL_ARGS', 'TOOL_CALL_END',
++ 'TOOL_CALL_RESULT', 'REASONING_START', 'REASONING_MESSAGE_START', 'REASONING_MESSAGE_CONTENT',
++ 'REASONING_MESSAGE_END', 'REASONING_END', 'REASONING_ENCRYPTED_VALUE', 'STATE_SNAPSHOT', 'STATE_DELTA',
++ 'ACTIVITY_SNAPSHOT', 'ACTIVITY_DELTA', 'CUSTOM'].forEach(function (type) {
++ source.addEventListener(type, function (e) {
++ try { view.push(JSON.parse(e.data)); } catch (err) { /* a malformed event is skipped */ }
++ if (type === 'RUN_FINISHED' || type === 'RUN_ERROR') source.close();
++ });
++ });
++ window.addEventListener('pagehide', function () { source.close(); }, { once: true });
++ }).catch(function () { /* the task event view stays usable */ });
++ }
++
// 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');
@@@ -40,6 +80,7 @@
var startedAt = open && window.OpenVibeTaskEvents ? window.OpenVibeTaskEvents.firstAt(events) : 0;
var view = window.OpenVibeAgentSession.mount(host, { title: (document.querySelector('.task-text') || {}).textContent || 'Task', status: open ? 'running' : state, startedAt: startedAt || undefined, follow: open, empty: 'Waiting for an agent to start…', link: function (u) { return ownDesk.test(u); } });
view.pushAll(lines);
++ agentRun(art, host, view);
return view;
}
@@@ -220,7 +261,7 @@
onDesk(e);
onLook(e);
if (e.step === 'plan') paintPlan(e.text);
-- if (view) { view.push(attach(e)); return undefined; }
++ if (view) { if (art.dataset.agentUi !== '1') view.push(attach(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') {
@@@ -238,7 +279,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 ev = JSON.parse(m.data), s = ev.state; if (liveState && STATE[s]) liveState.textContent = STATE[s] + '…'; if (view) view.push(ev); } catch (err) { /* ignore */ }
++ try { var ev = JSON.parse(m.data), s = ev.state; if (liveState && STATE[s]) liveState.textContent = STATE[s] + '…'; if (view && art.dataset.agentUi !== '1') 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 7e2790c..ab6b216 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -1100,13 +1100,13 @@ ${existing ? html`<div class="notice" role="status">Already offered ${time(exist
crumbs: [{ label: 'Your tasks', href: '/tasks' }, { label: 'Task' }],
// 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: waiting ? [] : ['js/task-events.js', 'js/task.js'],
-- sharedScripts: [...(waiting ? [] : ['agent-session.js']), 'ov-share.js'],
++ scripts: ['js/task-events.js', 'js/task.js'],
++ sharedScripts: ['agent-session.js', 'ov-share.js'],
styles: [showcase.STYLESHEET, 'ov-share.css'],
css: ['css/task.css'],
noReferrer: true,
body: html`${live ? raw('<noscript><meta http-equiv="refresh" content="4"></noscript>') : ''}
--<article class="task state-${t.state}" data-task="${row.id}" data-last="${row.last_seq}" data-open="${live ? '1' : ''}" data-bot="${botPages}">
++<article class="task state-${t.state}" data-task="${row.id}" data-last="${row.last_seq}" data-open="${live ? '1' : ''}" data-owner="${shared ? '0' : '1'}" data-bot="${botPages}">
<header class="task-head">
<p class="task-status"><span class="pill pill-${t.state}">${STATE_TEXT[t.state]}</span>${kind ? html`<span class="chip">${kind}</span>` : ''}<span class="chip">${MODE_LABEL[t.mode]}</span><span class="chip" id="task-cost">${t.cost ? usd(t.cost.usd) : '$0'} of ${usd(t.budget.per_task_usd)}</span>${time(t.created_at)}</p>
${parentTask ? html`<p class="follows-on">${row.fork_of_step != null
@@@ -1134,7 +1134,8 @@ ${open || look ? html`<figure class="last-look" id="last-look"${look ? '' : raw(
<a id="last-look-link" href="${look ? `/tasks/${row.id}/look?at=${look.at}` : '#'}" target="_blank" rel="noopener" title="Open it full size"><img id="last-look-img" src="${look ? `/tasks/${row.id}/look?at=${look.at}` : ''}" alt="The screen your agent last looked at"${look && look.width ? raw(` width="${Number(look.width)}" height="${Number(look.height)}"`) : ''} loading="lazy"></a>
<figcaption class="muted small">What your agent last saw on the screen${look ? html` · ${time(new Date(look.at).toISOString())}` : ''}</figcaption>
</figure>` : ''}
--${waiting ? html`<p class="muted small live-note" id="waiting-note">Waiting for your answer.${actingCard ? '' : ' The task keeps its place and its budget while you decide.'}</p>` : html`<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>
++${waiting ? html`<p class="muted small live-note" id="waiting-note">Waiting for your answer.${actingCard ? '' : ' The task keeps its place and its budget while you decide.'}</p>
++<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></section>` : html`<section class="how" aria-labelledby="h-how"><h2 id="h-how">How it went</h2>
${!open && plan ? planCard(plan) : ''}
<div class="attempts" id="attempts">${attemptsOf(events).map(attemptBlock)}</div>
${traceBlock(spans, row.id)}