openvibers/openvibe.chat Commits

Vibe asks the person in the chat when their agent hands them a step on its desk (Desks plan D4), once per hand-off, with the link (#202)

OP OpenVibers committed 18 days ago · bde0c4ff78 · 2 files, +48 −1

Changes

ddiff --git a/server/rooms/workspace.js b/server/rooms/workspace.js
iindex 4fe3ae5..9d1d0bb 100644
---- a/server/rooms/workspace.js
++++ b/server/rooms/workspace.js
@@@ -245,7 +245,9 @@ async function poll(deps = {}) {
continue;
}
const state = task && task.state;
-- if (state === 'running' && t.status !== 'running') { await setStatus(room, t.id, { status: 'running' }); changed(room, { id: Number(t.id), status: 'running' }); continue; }
++ if (state === 'running' && t.status !== 'running') { await setStatus(room, t.id, { status: 'running' }); changed(room, { id: Number(t.id), status: 'running' }); }
++ // The agent handed its person a step on its desk (Desks plan D4): Vibe asks them here, once per hand-off.
++ if (state === 'running' && task.needs_you && task.needs_you.url) await askPerson(room, t, task.needs_you, deps).catch(() => {});
if (!DONE.has(state)) continue;
const r = task.result || {};
const sources = (Array.isArray(r.sources) ? r.sources : []).map((s) => (typeof s === 'string' ? s : s && s.url)).filter((u) => /^https?:\/\//.test(String(u || ''))).slice(0, 12);
@@@ -261,6 +263,26 @@ async function poll(deps = {}) {
}
}
++// Hand-offs already asked about (actor task | its step's seq), so a poll every few seconds asks once.
++const asked = new Set();
++
++/** Vibe asks the person to take the agent's desk for one step (a sign-in, a CAPTCHA), with the link to it. */
++async function askPerson(room, t, needs, deps = {}) {
++ const key = `${t.actor_task}|${Number(needs.seq)}`;
++ if (asked.has(key) || !/^https:\/\//.test(String(needs.url || ''))) return null;
++ if (asked.size > 2000) asked.clear();
++ asked.add(key);
++ const members = await db.all('SELECT * FROM room_ai_members WHERE room_id = ? AND enabled = 1 ORDER BY (handle = \'vibe\') DESC, id LIMIT 1', [room.id]);
++ const member = members[0];
++ if (!member) return null;
++ const what = oneLine(needs.what, 300) || 'one step only you can do';
++ const text = `🙋 Your agent needs you for "${t.title}": ${what}\n\nOpen its desk, take it over, do it, then let go; it goes on by itself: ${oneLine(needs.url, LIMITS.url)}`;
++ const roomsMod = deps.rooms || rooms;
++ const posted = await roomsMod.postAsAi(room, member, text);
++ try { require('../chat/chat-server').broadcastToRoom(room.id, { type: 'room_message', room: room.slug, message: posted }); } catch { /* sockets are optional */ }
++ return posted;
++}
++
/** The room hears how a task ended, from its Vibe (or its first AI member), once. */
async function announce(room, t, deps = {}) {
if (!t || t.posted_message || !DONE.has(t.status)) { changed(room, t && shape(t, room)); return null; }
ddiff --git a/test/vibe-modes.test.js b/test/vibe-modes.test.js
iindex f52a6fa..8bf4bcc 100644
---- a/test/vibe-modes.test.js
++++ b/test/vibe-modes.test.js
@@@ -95,6 +95,31 @@ t('Code in an existing chat: Codes on their own computer; Agent too; Chat mode a
assert.strictEqual(turns.length, 1, 'no instant answers to the tasks');
});
++t('an agent that hands its person a step on its desk: Vibe asks them in the chat, once per hand-off, with the link (Desks plan D4)', async () => {
++ const start = await vibeApi('POST', '', ann, { message: 'hi' });
++ const slug = start.body.room.slug;
++ actorTask = { id: 'tsk_h1', state: 'queued' };
++ const r = await vibeApi('POST', `/${slug}/ask`, ann, { message: 'Book the cheapest flight to Lisbon', mode: 'agent' });
++ assert.strictEqual(r.status, 201, r.text);
++ await new Promise((res) => setTimeout(res, 150));
++ const desk = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-0123456789';
++ actorTask = { id: 'tsk_h1', state: 'running', needs_you: { seq: 7, what: 'Sign in to the airline.', url: desk } };
++ await workspace.poll();
++ await workspace.poll();
++ let asks = (await messagesOf(slug)).filter((m) => m.ai_member_id && /needs you/.test(m.message));
++ assert.strictEqual(asks.length, 1, 'once per hand-off, however often the poller looks');
++ assert.strictEqual(asks[0].message, `🙋 Your agent needs you for "Book the cheapest flight to Lisbon": Sign in to the airline.\n\nOpen its desk, take it over, do it, then let go; it goes on by itself: ${desk}`);
++ // A later hand-off in the same task asks again; a link that is not https never goes out.
++ actorTask = { id: 'tsk_h1', state: 'running', needs_you: { seq: 12, what: 'Pay with your card.', url: desk } };
++ await workspace.poll();
++ actorTask = { id: 'tsk_h1', state: 'running', needs_you: { seq: 15, what: 'Look here.', url: 'http://evil.example/x' } };
++ await workspace.poll();
++ asks = (await messagesOf(slug)).filter((m) => m.ai_member_id && /needs you/.test(m.message));
++ assert.deepStrictEqual(asks.map((m) => /Pay with your card/.test(m.message)), [false, true]);
++ actorTask = { id: 'tsk_h1', state: 'cancelled' };
++ await workspace.poll();
++});
++
t('refusals: an unknown mode, someone else\'s chat, no sign-in', async () => {
const start = await vibeApi('POST', '', ann, { message: 'mine' });
const slug = start.body.room.slug;