openvibers/openvibe.actor Commits
openvibe-contracts v0.214.0, with your agents' suggestions over the API (the route the new actor.agent.manage lists) (#120)
Changes
@@@ -52,7 +52,7 @@ Rate cards are typed in by a person from each provider's price page, with the pa| `GET /me/approvals`, `GET/POST /me/approvals/:id` (signed in) | the person | the approval inbox ([server/agents/approvals.js](server/agents/approvals.js)): when one of the person's own agents runs while they are away and would start a coding task on their machine (the `codes` agent), the run asks instead and ends with a link; nothing reaches Codes until the person approves, which starts it as their own task (declining runs nothing; a request expires after 7 days). In the account export (`approvals.json`) and deletion. || `GET/POST /api/v1/me/agents/:id/memory`, `PATCH/DELETE /api/v1/me/agents/:id/memory/:mem`, `DELETE /api/v1/me/agents/:id/memory` | `actor.agent.manage` for a token | control its memory || `POST /api/v1/me/agents/:id/run`, `GET /api/v1/me/agents/:id/runs` | `actor.agent.run` for a token | run now or list its tasks |--| `POST /me/agents/:id/suggestions/:pid` (signed in) | the person | what the agent suggested adding to its instructions (a run that passed its check may end with `IMPROVE: <line>`; at most three wait, the same line once), under "Suggestions from your agent" on its page: apply adds the line, dismiss drops it, undo restores the instructions while they are as it left them. In the account export (`agent-suggestions.json`) and deletion. The API routes follow once `actor.agent.manage` lists them (contracts). |++| `GET /api/v1/me/agents/:id/suggestions`, `POST /api/v1/me/agents/:id/suggestions/:pid/{apply,dismiss,undo}`; `POST /me/agents/:id/suggestions/:pid` (signed in) | `actor.agent.manage` for a token | what the agent suggested adding to its instructions (a run that passed its check may end with `IMPROVE: <line>`; at most three wait, the same line once), also under "Suggestions from your agent" on its page: apply adds the line, dismiss drops it, undo restores the instructions while they are as it left them. In the account export (`agent-suggestions.json`) and deletion. || `POST /api/v1/route` | public | dry run: which agent and why (heuristic class; nothing runs or is charged) || `GET /api/v1/resources` | `actor.resource.read` (service token) | `common.resource-list-result@1` for `actor.task`; filters: `project`, `kind`, `cursor`, `limit` || `GET /api/v1/resources/:ovrn` | `actor.resource.read` (service token) | one `common.resource-summary@1` for a project-owned task |@@@ -245,7 +245,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).<!-- versions:start -->--- openvibe-contracts: v0.213.0++- openvibe-contracts: v0.214.0- openvibe-sdk: v0.65.0- openvibe-shared: v4.13.0<!-- versions:end -->@@@ -7,11 +7,11 @@"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9","port": 4950,"domain": "openvibe.actor",-- "contracts": "openvibe-contracts v0.213.0",++ "contracts": "openvibe-contracts v0.214.0","sdk": "openvibe-sdk v0.65.0","shared": "openvibe-shared v4.13.0","packages": [-- "openvibe-contracts v0.213.0",++ "openvibe-contracts v0.214.0","openvibe-sdk v0.65.0","openvibe-shared v4.13.0"],@@@ -15,7 +15,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.213.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.214.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.13.0","pg": "^8.23.0"@@@ -871,9 +871,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.213.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.213.0",-- "integrity": "sha512-QeJamMrqMoGyvPoD80pyGZepF3GTaKJYVWBVeFsigVulZBmt8zeQOeJZ3TMwtUydT0bPje3kUt98/czwXvR7sg==",++ "version": "0.214.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.214.0",++ "integrity": "sha512-xlKYB4Zw0HueHXpnnSPref1BRDDYbhfMRn7EoZrPt2P5sOQmsWkaxWj06CKIJ32IfmrnKGIXmsvUfDOb8hwH3Q==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -28,7 +28,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.213.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.214.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.13.0","pg": "^8.23.0"@@@ -89,6 +89,17 @@ function createApi(ctx) {const page = await store.listTasks(s, row.owner, { limit, before, via: `agent:${row.id}` });res.json({ tasks: page.rows.map((t) => store.toWire(t, { baseUrl: config.baseUrl })), next: page.next });});++ // Suggestions the agent made for its instructions (agents/personal.js): list, then apply, dismiss or undo one.++ r.get('/me/agents/:id/suggestions', principal.requireCapability('actor.agent.manage'), owner, limits.reads('actor.agent.manage'), async (req, res) => {++ const row = await personal.get(s, req.principal.requester, req.params.id);++ if (!row) return problem(req, res, 404, 'actor.agent.not_found', 'No such agent.');++ res.json({ suggestions: await personal.proposals(s, row.owner, row.id) });++ });++ r.post('/me/agents/:id/suggestions/:pid/:action', principal.requireCapability('actor.agent.manage'), owner, limits.budget('actor.agent.manage'), async (req, res) => {++ const out = await personal.decide(s, req.principal.requester, req.params.id, req.params.pid, req.params.action);++ if (out.error) return problem(req, res, out.status, out.status === 404 ? 'actor.agent.not_found' : 'actor.agent.invalid', out.error);++ res.json({ suggestion: out.row, agent: out.agent });++ });r.get('/me/agents/:id/memory', principal.requireCapability('actor.agent.manage'), owner, limits.reads('actor.agent.manage'), async (req, res) => {const row = await loadAgent(req, res); if (row) res.json({ memory: await personal.memory(s, row.owner, row.id) });});@@@ -67,6 +67,16 @@ const SAME = { 'sec-fetch-site': 'same-origin' };const two = (await personal.proposals(t.ctx.s, owner, made.id)).find((p) => p.text === 'Two.');assert.strictEqual((await t.get(`/me/agents/${made.id}/suggestions/${two.id}`, { as: stranger, form: { decision: 'apply' }, headers: SAME })).status, 404);});++ await check('the API lists them and decides one (actor.agent.manage); a bad action is refused', async () => {++ const list = await t.get(`/api/v1/me/agents/${made.id}/suggestions`, { as: who });++ assert.strictEqual(list.status, 200);++ const two = list.json().suggestions.find((p) => p.text === 'Two.');++ assert.ok(two && two.state === 'pending');++ const applied = await t.get(`/api/v1/me/agents/${made.id}/suggestions/${two.id}/apply`, { as: who, method: 'POST', json: {}, headers: SAME });++ assert.strictEqual(applied.status, 200, applied.text);++ assert.match(applied.json().agent.instructions, /\n- Two\.$/);++ assert.strictEqual((await t.get(`/api/v1/me/agents/${made.id}/suggestions/${two.id}/explode`, { as: who, method: 'POST', json: {}, headers: SAME })).status, 404);++ });} finally { await t.close(); }done();})();
@@ -52,7 +52,7 @@ Rate cards are typed in by a person from each provider's price page, with the pa
@@ -52,7 +52,7 @@ Rate cards are typed in by a person from each provider's price page, with the pa
| `GET /me/approvals`, `GET/POST /me/approvals/:id` (signed in) | the person | the approval inbox ([server/agents/approvals.js](server/agents/approvals.js)): when one of the person's own agents runs while they are away and would start a coding task on their machine (the `codes` agent), the run asks instead and ends with a link; nothing reaches Codes until the person approves, which starts it as their own task (declining runs nothing; a request expires after 7 days). In the account export (`approvals.json`) and deletion. |
| `GET /me/approvals`, `GET/POST /me/approvals/:id` (signed in) | the person | the approval inbox ([server/agents/approvals.js](server/agents/approvals.js)): when one of the person's own agents runs while they are away and would start a coding task on their machine (the `codes` agent), the run asks instead and ends with a link; nothing reaches Codes until the person approves, which starts it as their own task (declining runs nothing; a request expires after 7 days). In the account export (`approvals.json`) and deletion. |
| `GET/POST /api/v1/me/agents/:id/memory`, `PATCH/DELETE /api/v1/me/agents/:id/memory/:mem`, `DELETE /api/v1/me/agents/:id/memory` | `actor.agent.manage` for a token | control its memory |
| `GET/POST /api/v1/me/agents/:id/memory`, `PATCH/DELETE /api/v1/me/agents/:id/memory/:mem`, `DELETE /api/v1/me/agents/:id/memory` | `actor.agent.manage` for a token | control its memory |
| `POST /api/v1/me/agents/:id/run`, `GET /api/v1/me/agents/:id/runs` | `actor.agent.run` for a token | run now or list its tasks |
| `POST /api/v1/me/agents/:id/run`, `GET /api/v1/me/agents/:id/runs` | `actor.agent.run` for a token | run now or list its tasks |
-| `POST /me/agents/:id/suggestions/:pid` (signed in) | the person | what the agent suggested adding to its instructions (a run that passed its check may end with `IMPROVE: <line>`; at most three wait, the same line once), under "Suggestions from your agent" on its page: apply adds the line, dismiss drops it, undo restores the instructions while they are as it left them. In the account export (`agent-suggestions.json`) and deletion. The API routes follow once `actor.agent.manage` lists them (contracts). |
+| `GET /api/v1/me/agents/:id/suggestions`, `POST /api/v1/me/agents/:id/suggestions/:pid/{apply,dismiss,undo}`; `POST /me/agents/:id/suggestions/:pid` (signed in) | `actor.agent.manage` for a token | what the agent suggested adding to its instructions (a run that passed its check may end with `IMPROVE: <line>`; at most three wait, the same line once), also under "Suggestions from your agent" on its page: apply adds the line, dismiss drops it, undo restores the instructions while they are as it left them. In the account export (`agent-suggestions.json`) and deletion. |
| `POST /api/v1/route` | public | dry run: which agent and why (heuristic class; nothing runs or is charged) |
| `POST /api/v1/route` | public | dry run: which agent and why (heuristic class; nothing runs or is charged) |
| `GET /api/v1/resources` | `actor.resource.read` (service token) | `common.resource-list-result@1` for `actor.task`; filters: `project`, `kind`, `cursor`, `limit` |
| `GET /api/v1/resources` | `actor.resource.read` (service token) | `common.resource-list-result@1` for `actor.task`; filters: `project`, `kind`, `cursor`, `limit` |
| `GET /api/v1/resources/:ovrn` | `actor.resource.read` (service token) | one `common.resource-summary@1` for a project-owned task |
| `GET /api/v1/resources/:ovrn` | `actor.resource.read` (service token) | one `common.resource-summary@1` for a project-owned task |
@@ -245,7 +245,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).
@@ -245,7 +245,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
<!-- versions:start -->
<!-- versions:start -->
-- openvibe-contracts: v0.213.0
+- openvibe-contracts: v0.214.0
- openvibe-sdk: v0.65.0
- openvibe-sdk: v0.65.0
- openvibe-shared: v4.13.0
- openvibe-shared: v4.13.0
<!-- versions:end -->
<!-- versions:end -->
@@ -7,11 +7,11 @@
@@ -7,11 +7,11 @@
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express 4, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4950,
"port": 4950,
"domain": "openvibe.actor",
"domain": "openvibe.actor",
- "contracts": "openvibe-contracts v0.213.0",
+ "contracts": "openvibe-contracts v0.214.0",
"sdk": "openvibe-sdk v0.65.0",
"sdk": "openvibe-sdk v0.65.0",
"shared": "openvibe-shared v4.13.0",
"shared": "openvibe-shared v4.13.0",
"packages": [
"packages": [
- "openvibe-contracts v0.213.0",
+ "openvibe-contracts v0.214.0",
"openvibe-sdk v0.65.0",
"openvibe-sdk v0.65.0",
"openvibe-shared v4.13.0"
"openvibe-shared v4.13.0"
],
],
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.213.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.214.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.13.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.13.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -871,9 +871,9 @@
@@ -871,9 +871,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.213.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.213.0",
- "integrity": "sha512-QeJamMrqMoGyvPoD80pyGZepF3GTaKJYVWBVeFsigVulZBmt8zeQOeJZ3TMwtUydT0bPje3kUt98/czwXvR7sg==",
+ "version": "0.214.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.214.0",
+ "integrity": "sha512-xlKYB4Zw0HueHXpnnSPref1BRDDYbhfMRn7EoZrPt2P5sOQmsWkaxWj06CKIJ32IfmrnKGIXmsvUfDOb8hwH3Q==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -28,7 +28,7 @@
@@ -28,7 +28,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.213.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.214.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.65.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.13.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.13.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -89,6 +89,17 @@ function createApi(ctx) {
@@ -89,6 +89,17 @@ function createApi(ctx) {
const page = await store.listTasks(s, row.owner, { limit, before, via: `agent:${row.id}` });
const page = await store.listTasks(s, row.owner, { limit, before, via: `agent:${row.id}` });
res.json({ tasks: page.rows.map((t) => store.toWire(t, { baseUrl: config.baseUrl })), next: page.next });
res.json({ tasks: page.rows.map((t) => store.toWire(t, { baseUrl: config.baseUrl })), next: page.next });
});
});
+ // Suggestions the agent made for its instructions (agents/personal.js): list, then apply, dismiss or undo one.
+ r.get('/me/agents/:id/suggestions', principal.requireCapability('actor.agent.manage'), owner, limits.reads('actor.agent.manage'), async (req, res) => {
+ const row = await personal.get(s, req.principal.requester, req.params.id);
+ if (!row) return problem(req, res, 404, 'actor.agent.not_found', 'No such agent.');
+ res.json({ suggestions: await personal.proposals(s, row.owner, row.id) });
+ });
+ r.post('/me/agents/:id/suggestions/:pid/:action', principal.requireCapability('actor.agent.manage'), owner, limits.budget('actor.agent.manage'), async (req, res) => {
+ const out = await personal.decide(s, req.principal.requester, req.params.id, req.params.pid, req.params.action);
+ if (out.error) return problem(req, res, out.status, out.status === 404 ? 'actor.agent.not_found' : 'actor.agent.invalid', out.error);
+ res.json({ suggestion: out.row, agent: out.agent });
+ });
r.get('/me/agents/:id/memory', principal.requireCapability('actor.agent.manage'), owner, limits.reads('actor.agent.manage'), async (req, res) => {
r.get('/me/agents/:id/memory', principal.requireCapability('actor.agent.manage'), owner, limits.reads('actor.agent.manage'), async (req, res) => {
const row = await loadAgent(req, res); if (row) res.json({ memory: await personal.memory(s, row.owner, row.id) });
const row = await loadAgent(req, res); if (row) res.json({ memory: await personal.memory(s, row.owner, row.id) });
});
});
@@ -67,6 +67,16 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
@@ -67,6 +67,16 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
const two = (await personal.proposals(t.ctx.s, owner, made.id)).find((p) => p.text === 'Two.');
const two = (await personal.proposals(t.ctx.s, owner, made.id)).find((p) => p.text === 'Two.');
assert.strictEqual((await t.get(`/me/agents/${made.id}/suggestions/${two.id}`, { as: stranger, form: { decision: 'apply' }, headers: SAME })).status, 404);
assert.strictEqual((await t.get(`/me/agents/${made.id}/suggestions/${two.id}`, { as: stranger, form: { decision: 'apply' }, headers: SAME })).status, 404);
});
});
+ await check('the API lists them and decides one (actor.agent.manage); a bad action is refused', async () => {
+ const list = await t.get(`/api/v1/me/agents/${made.id}/suggestions`, { as: who });
+ assert.strictEqual(list.status, 200);
+ const two = list.json().suggestions.find((p) => p.text === 'Two.');
+ assert.ok(two && two.state === 'pending');
+ const applied = await t.get(`/api/v1/me/agents/${made.id}/suggestions/${two.id}/apply`, { as: who, method: 'POST', json: {}, headers: SAME });
+ assert.strictEqual(applied.status, 200, applied.text);
+ assert.match(applied.json().agent.instructions, /\n- Two\.$/);
+ assert.strictEqual((await t.get(`/api/v1/me/agents/${made.id}/suggestions/${two.id}/explode`, { as: who, method: 'POST', json: {}, headers: SAME })).status, 404);
+ });
} finally { await t.close(); }
} finally { await t.close(); }
done();
done();
})();
})();