openvibers/openvibe.actor Commits
A task waiting for its person says so on its record (needs_you: what and the desk's link), so Vibe can ask them in the chat (Desks plan D4) (#133)
Changes
@@@ -172,7 +172,12 @@ function createApi(ctx) {r.get('/tasks/:id', principal.requireCapability('actor.task.read'), limits.reads('actor.task.read'), async (req, res) => {const row = await load(req, res);-- if (row) res.json(wire(row));++ if (!row) return;++ const out = wire(row);++ // Waiting for its person on its desk (Desks plan D4): what, and the link to the desk.++ const needs = await store.needsYou(s, row);++ if (needs) out.needs_you = needs;++ res.json(out);});r.get('/tasks/:id/events', principal.requireCapability('actor.task.read'), limits.budget('actor.task.stream'), async (req, res) => {@@@ -66,6 +66,27 @@ async function appendEvent(s, taskId, body) {});}++/** A task's newest event, or null. */++async function lastEvent(s, taskId) {++ const row = await s.db.maybe('SELECT body FROM task_events WHERE task_id = $1 ORDER BY seq DESC LIMIT 1', [taskId]);++ if (!row) return null;++ return typeof row.body === 'string' ? JSON.parse(row.body) : row.body;++}++++/**++ * Desks plan D4: while an agent waits for its person (hand_to_person), its newest step is the "Needs you" line, until++ * the person has used the desk and let go. Then the task's record says so (needs_you: what, the link to the desk and++ * the step's seq), so whatever shows the task (Vibe's chat) can ask the person where they are.++ */++async function needsYou(s, row) {++ if (!row || !OPEN.includes(row.state)) return null;++ const e = await lastEvent(s, row.id);++ if (!e || e.step !== 'text' || !/^Needs you: /.test(String(e.text || ''))) return null;++ const text = String(e.text).replace(/^Needs you: /, '');++ const url = (text.match(/https:\/\/\S+$/) || [])[0] || null;++ return { seq: Number(e.seq), what: text.replace(/\s*Open your agent's desk.*$/s, '').trim().slice(0, 300), url };++}++async function eventsAfter(s, taskId, after = 0) {const rows = await s.db.many('SELECT body FROM task_events WHERE task_id = $1 AND seq > $2 ORDER BY seq', [taskId, after]);return rows.map((r) => parse(r.body));@@@ -160,4 +181,4 @@ function toWire(row, { baseUrl }) {return t;}--module.exports = { insertTask, getTask, byIdempotency, listTasks, updateTask, appendEvent, eventsAfter, addSpend, spendToday, spendDays, lockSpendToday, failInterrupted, prune, toWire, subjectRef, dayOf, parseJson: parse, OPEN, RETENTION_DAYS };++module.exports = { insertTask, getTask, byIdempotency, listTasks, updateTask, appendEvent, eventsAfter, lastEvent, needsYou, addSpend, spendToday, spendDays, lockSpendToday, failInterrupted, prune, toWire, subjectRef, dayOf, parseJson: parse, OPEN, RETENTION_DAYS };@@@ -110,6 +110,19 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImplassert.ok(!page.text.includes('href="https://evil.example'), 'another address stays text');assert.ok(page.text.includes('data-bot="https://openvibe.bot"'));});++ 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 id = t.ctx.s.newId('tsk');++ await store.insertTask(t.ctx.s, { id, requester: `user:${bea.subject}`, task: 'Book it', mode: 'balanced', budget_task: 0.05, budget_day: 1, created_at: t.ctx.s.iso() });++ const watch = `https://openvibe.bot/machines/${PC}/screen?desk=desk-0123456789`;++ await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'plan', agent: 'openvibe-runtime', text: 'OpenVibe runtime takes it.' });++ assert.strictEqual((await t.get(`/api/v1/tasks/${id}`, { as: bea })).json().needs_you, undefined, 'nothing waits yet');++ const e = await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'text', text: `Needs you: Sign in to the airline. Open your agent's desk, take it over, do it, then let go: ${watch}` });++ const waiting = (await t.get(`/api/v1/tasks/${id}`, { as: bea })).json();++ assert.deepStrictEqual(waiting.needs_you, { seq: e.seq, what: 'Sign in to the airline.', url: watch });++ await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'tool_result', tool: 'hand_to_person', text: '{"done":true}' });++ assert.strictEqual((await t.get(`/api/v1/tasks/${id}`, { as: bea })).json().needs_you, undefined, 'the person let go: nothing waits');++ });await t.close();done();})();
@@ -172,7 +172,12 @@ function createApi(ctx) {
@@ -172,7 +172,12 @@ function createApi(ctx) {
r.get('/tasks/:id', principal.requireCapability('actor.task.read'), limits.reads('actor.task.read'), async (req, res) => {
r.get('/tasks/:id', principal.requireCapability('actor.task.read'), limits.reads('actor.task.read'), async (req, res) => {
const row = await load(req, res);
const row = await load(req, res);
- if (row) res.json(wire(row));
+ if (!row) return;
+ const out = wire(row);
+ // Waiting for its person on its desk (Desks plan D4): what, and the link to the desk.
+ const needs = await store.needsYou(s, row);
+ if (needs) out.needs_you = needs;
+ res.json(out);
});
});
r.get('/tasks/:id/events', principal.requireCapability('actor.task.read'), limits.budget('actor.task.stream'), async (req, res) => {
r.get('/tasks/:id/events', principal.requireCapability('actor.task.read'), limits.budget('actor.task.stream'), async (req, res) => {
@@ -66,6 +66,27 @@ async function appendEvent(s, taskId, body) {
@@ -66,6 +66,27 @@ async function appendEvent(s, taskId, body) {
});
});
}
}
+/** A task's newest event, or null. */
+async function lastEvent(s, taskId) {
+ const row = await s.db.maybe('SELECT body FROM task_events WHERE task_id = $1 ORDER BY seq DESC LIMIT 1', [taskId]);
+ if (!row) return null;
+ return typeof row.body === 'string' ? JSON.parse(row.body) : row.body;
+}
+
+/**
+ * Desks plan D4: while an agent waits for its person (hand_to_person), its newest step is the "Needs you" line, until
+ * the person has used the desk and let go. Then the task's record says so (needs_you: what, the link to the desk and
+ * the step's seq), so whatever shows the task (Vibe's chat) can ask the person where they are.
+ */
+async function needsYou(s, row) {
+ if (!row || !OPEN.includes(row.state)) return null;
+ const e = await lastEvent(s, row.id);
+ if (!e || e.step !== 'text' || !/^Needs you: /.test(String(e.text || ''))) return null;
+ const text = String(e.text).replace(/^Needs you: /, '');
+ const url = (text.match(/https:\/\/\S+$/) || [])[0] || null;
+ return { seq: Number(e.seq), what: text.replace(/\s*Open your agent's desk.*$/s, '').trim().slice(0, 300), url };
+}
+
async function eventsAfter(s, taskId, after = 0) {
async function eventsAfter(s, taskId, after = 0) {
const rows = await s.db.many('SELECT body FROM task_events WHERE task_id = $1 AND seq > $2 ORDER BY seq', [taskId, after]);
const rows = await s.db.many('SELECT body FROM task_events WHERE task_id = $1 AND seq > $2 ORDER BY seq', [taskId, after]);
return rows.map((r) => parse(r.body));
return rows.map((r) => parse(r.body));
@@ -160,4 +181,4 @@ function toWire(row, { baseUrl }) {
@@ -160,4 +181,4 @@ function toWire(row, { baseUrl }) {
return t;
return t;
}
}
-module.exports = { insertTask, getTask, byIdempotency, listTasks, updateTask, appendEvent, eventsAfter, addSpend, spendToday, spendDays, lockSpendToday, failInterrupted, prune, toWire, subjectRef, dayOf, parseJson: parse, OPEN, RETENTION_DAYS };
+module.exports = { insertTask, getTask, byIdempotency, listTasks, updateTask, appendEvent, eventsAfter, lastEvent, needsYou, addSpend, spendToday, spendDays, lockSpendToday, failInterrupted, prune, toWire, subjectRef, dayOf, parseJson: parse, OPEN, RETENTION_DAYS };
@@ -110,6 +110,19 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
@@ -110,6 +110,19 @@ const toolsFor = (bot) => createOpenVibeTools({ config, fetchImpl: bot.fetchImpl
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"'));
});
});
+ 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 id = t.ctx.s.newId('tsk');
+ await store.insertTask(t.ctx.s, { id, requester: `user:${bea.subject}`, task: 'Book it', mode: 'balanced', budget_task: 0.05, budget_day: 1, created_at: t.ctx.s.iso() });
+ const watch = `https://openvibe.bot/machines/${PC}/screen?desk=desk-0123456789`;
+ await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'plan', agent: 'openvibe-runtime', text: 'OpenVibe runtime takes it.' });
+ assert.strictEqual((await t.get(`/api/v1/tasks/${id}`, { as: bea })).json().needs_you, undefined, 'nothing waits yet');
+ const e = await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'text', text: `Needs you: Sign in to the airline. Open your agent's desk, take it over, do it, then let go: ${watch}` });
+ const waiting = (await t.get(`/api/v1/tasks/${id}`, { as: bea })).json();
+ assert.deepStrictEqual(waiting.needs_you, { seq: e.seq, what: 'Sign in to the airline.', url: watch });
+ await store.appendEvent(t.ctx.s, id, { kind: 'output', step: 'tool_result', tool: 'hand_to_person', text: '{"done":true}' });
+ assert.strictEqual((await t.get(`/api/v1/tasks/${id}`, { as: bea })).json().needs_you, undefined, 'the person let go: nothing waits');
+ });
await t.close();
await t.close();
done();
done();
})();
})();