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)
Changes
@@@ -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; }@@@ -95,6 +95,31 @@ t('Code in an existing chat: Codes on their own computer; Agent too; Chat mode aassert.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;
@@ -245,7 +245,9 @@ async function poll(deps = {}) {
@@ -245,7 +245,9 @@ async function poll(deps = {}) {
continue;
continue;
}
}
const state = task && task.state;
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;
if (!DONE.has(state)) continue;
const r = task.result || {};
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);
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 = {}) {
@@ -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. */
/** The room hears how a task ended, from its Vibe (or its first AI member), once. */
async function announce(room, t, deps = {}) {
async function announce(room, t, deps = {}) {
if (!t || t.posted_message || !DONE.has(t.status)) { changed(room, t && shape(t, room)); return null; }
if (!t || t.posted_message || !DONE.has(t.status)) { changed(room, t && shape(t, room)); return null; }
@@ -95,6 +95,31 @@ t('Code in an existing chat: Codes on their own computer; Agent too; Chat mode a
@@ -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');
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 () => {
t('refusals: an unknown mode, someone else\'s chat, no sign-in', async () => {
const start = await vibeApi('POST', '', ann, { message: 'mine' });
const start = await vibeApi('POST', '', ann, { message: 'mine' });
const slug = start.body.room.slug;
const slug = start.body.room.slug;