openvibers/openvibe.codes Commits

A6: Codes publishes the agent's step (agent.step.started|ended) (#300)

OP OpenVibers committed 6 days ago · 0f0b80072c · 2 files, +98 −2

Changes

ddiff --git a/server/events/outbox.js b/server/events/outbox.js
iindex 2f6fcc0..dfa772f 100644
---- a/server/events/outbox.js
++++ b/server/events/outbox.js
@@@ -89,6 +89,35 @@ function payloadOf(row) {
};
}
++// ── The agent's step (design 2026-11-07: one agent, one timeline behind the Dock) ──
++// Codes publishes what its task is doing as agent.step.started|updated|ended, with the person's subject, so the
++// one timeline in OpenVibe.Actor shows it. The payload is a title, an outcome and a link: never the task's prompt,
++// never a diff, never a file.
++const AGENT_TOPICS = Object.freeze({ started: 'agent.step.started', updated: 'agent.step.updated', ended: 'agent.step.ended' });
++
++/** The agent's step for a task change: a title, an outcome and a link. */
++function agentStepOf(row, status) {
++ const repo = repoSlug(row.repo);
++ const result = objectOf(row.result) || {};
++ const cost = typeof result.cost_usd === 'number' && Number.isFinite(result.cost_usd) && result.cost_usd >= 0 ? result.cost_usd : null;
++ const at = (v) => (typeof v === 'number' ? new Date(v).toISOString() : isoOf(v));
++ return {
++ step_id: row.id,
++ mode: 'code',
++ product: 'codes',
++ ref: row.id,
++ // Never the prompt: a title names what the task is, not what it said.
++ title: repo ? 'Coding task on ' + repo : 'Coding task',
++ status,
++ started_at: at(row.created_at),
++ ended_at: status === 'running' ? null : at(row.updated_at),
++ placement: { where: 'On your machine', why: 'it needs your files and your sign-in, and it is free' },
++ cost_usd: cost,
++ link: 'https://codes.openvibe.network/tasks/' + encodeURIComponent(row.id),
++ reason: status === 'failed' ? (row.error == null ? null : String(row.error).slice(0, 240)) : null,
++ };
++}
++
function createOutbox({ store, log = console }) {
const db = store.db;
@@@ -104,10 +133,34 @@ function createOutbox({ store, log = console }) {
VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $5::timestamptz)`,
[eventId, topic, row.subject, JSON.stringify(payloadOf(row)), at],
);
++ await emitAgentStep(row);
++ return eventId;
++ }
++
++ /**
++ * The agent's step for the same change (design 2026-11-07): the task starts, or it ends. It is written in the
++ * same transaction, so a step exists if and only if the change committed.
++ */
++ async function emitAgentStep(row) {
++ const group = groupOf(row.state);
++ const type = group === 'running' ? AGENT_TOPICS.started
++ : group === 'succeeded' ? AGENT_TOPICS.ended
++ : group === 'failed' ? AGENT_TOPICS.ended
++ : group === 'cancelled' ? AGENT_TOPICS.ended
++ : null;
++ if (!type) return null;
++ const status = group === 'running' ? 'running' : group === 'succeeded' ? 'done' : group === 'failed' ? 'failed' : 'stopped';
++ const eventId = ids.newId('event', store.now());
++ const at = store.iso();
++ await db.exec(
++ `INSERT INTO codes_outbox (event_id, topic, subject, payload, created_at, next_attempt_at)
++ VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $5::timestamptz)`,
++ [eventId, type, row.subject, JSON.stringify(agentStepOf(row, status)), at],
++ );
return eventId;
}
-- return { emitTask, payloadOf, topicFor, groupOf };
++ return { emitTask, emitAgentStep, payloadOf, topicFor, groupOf };
}
function createPublisher({ config, store, fetchImpl = globalThis.fetch, log = console, intervalMs = (config.events && config.events.intervalMs) || 5000 }) {
@@@ -280,4 +333,4 @@ function createPublisher({ config, store, fetchImpl = globalThis.fetch, log = co
};
}
--module.exports = { createOutbox, createPublisher, TOPICS, TOPIC_SET, topicFor, groupOf, payloadOf, repoSlug, MAX_BACKOFF_MS, DEAD_AFTER_REFUSALS, DEAD_AFTER_ATTEMPTS };
++module.exports = { createOutbox, createPublisher, TOPICS, TOPIC_SET, AGENT_TOPICS, agentStepOf, topicFor, groupOf, payloadOf, repoSlug, MAX_BACKOFF_MS, DEAD_AFTER_REFUSALS, DEAD_AFTER_ATTEMPTS };
ddiff --git a/test/agent-step.test.js b/test/agent-step.test.js
nnew file mode 100644
iindex 0000000..5a7bd57
---- /dev/null
++++ b/test/agent-step.test.js
@@@ -0,0 +1,43 @@
++'use strict';
++/**
++ * The agent's step (design 2026-11-07): Codes publishes what its task is doing as agent.step.started|updated|ended,
++ * so the one timeline behind the Dock shows it. The payload is a title, an outcome and a link: never the task's
++ * prompt, never a diff.
++ */
++const assert = require('assert');
++const { agentStepOf, AGENT_TOPICS } = require('../server/events/outbox');
++
++const ROW = {
++ id: 'tsk_01JAB2C3D4E5F6G7H8J9K0MNPS',
++ subject: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPS',
++ repo: 'https://github.com/ada/site',
++ state: 'running',
++ prompt: 'the prompt the person typed, which never leaves here',
++ created_at: 1762000000000,
++ updated_at: 1762000001000,
++ result: { cost_usd: 0.12 },
++};
++
++assert.strictEqual(AGENT_TOPICS.started, 'agent.step.started');
++assert.strictEqual(AGENT_TOPICS.updated, 'agent.step.updated');
++assert.strictEqual(AGENT_TOPICS.ended, 'agent.step.ended');
++
++const step = agentStepOf(ROW, 'running');
++assert.strictEqual(step.step_id, ROW.id);
++assert.strictEqual(step.mode, 'code');
++assert.strictEqual(step.product, 'codes');
++assert.strictEqual(step.status, 'running');
++assert.ok(step.title.includes('ada/site'), 'the title names the repository');
++assert.ok(!JSON.stringify(step).includes('the prompt the person typed'), 'the prompt never leaves Codes');
++assert.ok(!('prompt' in step) && !('diff' in step) && !('files' in step), 'there is no field for a prompt or a diff');
++assert.strictEqual(step.cost_usd, 0.12);
++assert.ok(step.link.includes(ROW.id), 'the step links to its own page');
++assert.ok(step.started_at && !Number.isNaN(Date.parse(step.started_at)), 'the start is a time');
++
++const ended = agentStepOf(Object.assign({}, ROW, { state: 'failed', error: 'the build failed' }), 'failed');
++assert.strictEqual(ended.status, 'failed');
++assert.strictEqual(ended.reason, 'the build failed');
++assert.ok(ended.ended_at, 'an ended step says when it ended');
++
++console.log('codes agent step: a title, an outcome and a link — never a prompt or a diff');
++console.log('all passed');