hive-am 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -1
- package/README.md +34 -4
- package/package.json +13 -2
- package/server/dist/api.js +42 -677
- package/server/dist/connections/addressing.js +87 -0
- package/server/dist/connections/channel-skill.js +62 -0
- package/server/dist/connections/fake.js +1 -0
- package/server/dist/connections/files.js +45 -1
- package/server/dist/connections/format.js +9 -1
- package/server/dist/connections/manager.js +60 -3
- package/server/dist/connections/outbound.js +2 -1
- package/server/dist/connections/prompt.js +12 -3
- package/server/dist/connections/rate.js +20 -0
- package/server/dist/connections/router.js +55 -22
- package/server/dist/connections/secrets.js +52 -0
- package/server/dist/connections/store.js +6 -43
- package/server/dist/connections/telegram.js +105 -14
- package/server/dist/cron.js +138 -0
- package/server/dist/db/json.js +16 -0
- package/server/dist/db/migrations.js +199 -0
- package/server/dist/db.js +98 -196
- package/server/dist/errors.js +11 -0
- package/server/dist/git/browse.js +99 -0
- package/server/dist/git/ops.js +136 -19
- package/server/dist/git/repo.js +2 -1
- package/server/dist/history/claude.js +33 -20
- package/server/dist/history/index.js +17 -9
- package/server/dist/history/kiro.js +17 -12
- package/server/dist/history/lines.js +12 -0
- package/server/dist/history/opencode.js +18 -1
- package/server/dist/http/body.js +46 -0
- package/server/dist/http/errors.js +21 -0
- package/server/dist/http/guards.js +44 -0
- package/server/dist/http/router.js +24 -0
- package/server/dist/http/routes/agents.js +103 -0
- package/server/dist/http/routes/channel.js +16 -0
- package/server/dist/http/routes/colonies.js +9 -0
- package/server/dist/http/routes/connections.js +36 -0
- package/server/dist/http/routes/fs.js +22 -0
- package/server/dist/http/routes/git.js +90 -0
- package/server/dist/http/routes/meta.js +11 -0
- package/server/dist/http/routes/notebook.js +34 -0
- package/server/dist/http/routes/orchestration.js +32 -0
- package/server/dist/http/routes/resources.js +68 -0
- package/server/dist/http/routes/schedules.js +40 -0
- package/server/dist/http/routes/skills.js +47 -0
- package/server/dist/http/routes/terminals.js +18 -0
- package/server/dist/http/routes/types.js +36 -0
- package/server/dist/http/validate.js +65 -0
- package/server/dist/index.js +74 -4
- package/server/dist/instructions.js +23 -11
- package/server/dist/objects/agent.js +49 -0
- package/server/dist/objects/database.js +451 -0
- package/server/dist/objects/db-history.js +79 -0
- package/server/dist/objects/docker-info.js +75 -0
- package/server/dist/objects/docker.js +285 -0
- package/server/dist/objects/driver-props.js +196 -0
- package/server/dist/objects/exec.js +16 -0
- package/server/dist/objects/http-history.js +44 -0
- package/server/dist/objects/http-script.js +644 -0
- package/server/dist/objects/http.js +735 -0
- package/server/dist/objects/index.js +445 -0
- package/server/dist/objects/model.js +250 -0
- package/server/dist/objects/runner.js +211 -0
- package/server/dist/objects/stats.js +106 -0
- package/server/dist/objects/store.js +115 -0
- package/server/dist/objects/supervisor.js +111 -0
- package/server/dist/objects/templates.js +67 -0
- package/server/dist/providers/claude.js +5 -13
- package/server/dist/providers/index.js +7 -1
- package/server/dist/providers/kiro.js +4 -5
- package/server/dist/providers/opencode-server.js +164 -0
- package/server/dist/providers/opencode.js +60 -37
- package/server/dist/providers/preamble.js +2 -0
- package/server/dist/providers/spawn.js +31 -3
- package/server/dist/runtime.js +10 -7
- package/server/dist/schedules.js +222 -0
- package/server/dist/seed.js +15 -11
- package/server/dist/services/agents.js +90 -0
- package/server/dist/services/colonies.js +38 -0
- package/server/dist/services/connections.js +76 -0
- package/server/dist/services/notebook.js +22 -0
- package/server/dist/services/providers.js +27 -0
- package/server/dist/services/schedules.js +18 -0
- package/server/dist/sessions-summary.js +51 -0
- package/server/dist/skills/defaults.js +120 -0
- package/server/dist/skills/notebook.js +3 -8
- package/server/dist/terminals.js +177 -0
- package/server/dist/wake.js +149 -0
- package/server/dist/watch.js +201 -0
- package/server/mcp/dispatch.mjs +169 -1
- package/web/.next/BUILD_ID +1 -1
- package/web/.next/app-build-manifest.json +145 -85
- package/web/.next/app-path-routes-manifest.json +5 -2
- package/web/.next/build-manifest.json +14 -14
- package/web/.next/prerender-manifest.json +85 -37
- package/web/.next/react-loadable-manifest.json +26 -1
- package/web/.next/required-server-files.json +13 -1
- package/web/.next/routes-manifest.json +32 -0
- package/web/.next/server/app/_not-found/page.js +2 -2
- package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/_not-found.html +1 -1
- package/web/.next/server/app/_not-found.rsc +12 -11
- package/web/.next/server/app/agents/[id]/page.js +4 -4
- package/web/.next/server/app/agents/[id]/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/[id]/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents/page.js +2 -2
- package/web/.next/server/app/agents/page.js.nft.json +1 -1
- package/web/.next/server/app/agents/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/agents.html +1 -1
- package/web/.next/server/app/agents.rsc +14 -13
- package/web/.next/server/app/connections/page.js +2 -2
- package/web/.next/server/app/connections/page.js.nft.json +1 -1
- package/web/.next/server/app/connections/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/connections.html +1 -1
- package/web/.next/server/app/connections.rsc +14 -13
- package/web/.next/server/app/index.html +1 -1
- package/web/.next/server/app/index.rsc +14 -13
- package/web/.next/server/app/page.js +2 -2
- package/web/.next/server/app/page.js.nft.json +1 -1
- package/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations/page.js +2 -2
- package/web/.next/server/app/relations/page.js.nft.json +1 -1
- package/web/.next/server/app/relations/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/relations.html +1 -1
- package/web/.next/server/app/relations.rsc +14 -13
- package/web/.next/server/app/resources/[id]/page.js +2 -0
- package/web/.next/server/app/resources/[id]/page.js.nft.json +1 -0
- package/web/.next/server/app/resources/[id]/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/resources/page.js +2 -0
- package/web/.next/server/app/resources/page.js.nft.json +1 -0
- package/web/.next/server/app/resources/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/resources.html +1 -0
- package/web/.next/server/app/resources.meta +15 -0
- package/web/.next/server/app/resources.rsc +25 -0
- package/web/.next/server/app/schedules/page.js +2 -0
- package/web/.next/server/app/schedules/page.js.nft.json +1 -0
- package/web/.next/server/app/schedules/page_client-reference-manifest.js +1 -0
- package/web/.next/server/app/schedules.html +1 -0
- package/web/.next/server/app/schedules.meta +15 -0
- package/web/.next/server/app/schedules.rsc +25 -0
- package/web/.next/server/app/sessions/page.js +2 -2
- package/web/.next/server/app/sessions/page.js.nft.json +1 -1
- package/web/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/sessions.html +1 -1
- package/web/.next/server/app/sessions.rsc +14 -13
- package/web/.next/server/app/skills/page.js +2 -2
- package/web/.next/server/app/skills/page.js.nft.json +1 -1
- package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/skills.html +1 -1
- package/web/.next/server/app/skills.rsc +14 -13
- package/web/.next/server/app/types/page.js +2 -2
- package/web/.next/server/app/types/page.js.nft.json +1 -1
- package/web/.next/server/app/types/page_client-reference-manifest.js +1 -1
- package/web/.next/server/app/types.html +1 -1
- package/web/.next/server/app/types.rsc +14 -13
- package/web/.next/server/app-paths-manifest.json +5 -2
- package/web/.next/server/chunks/143.js +1 -1
- package/web/.next/server/chunks/274.js +1 -0
- package/web/.next/server/chunks/306.js +1 -0
- package/web/.next/server/chunks/423.js +1 -0
- package/web/.next/server/chunks/505.js +6 -0
- package/web/.next/server/chunks/555.js +1 -0
- package/web/.next/server/chunks/612.js +1 -0
- package/web/.next/server/chunks/638.js +1 -0
- package/web/.next/server/chunks/692.js +18 -0
- package/web/.next/server/chunks/716.js +2 -0
- package/web/.next/server/chunks/790.js +11 -0
- package/web/.next/server/chunks/802.js +1 -0
- package/web/.next/server/chunks/890.js +1 -1
- package/web/.next/server/chunks/919.js +1 -0
- package/web/.next/server/chunks/950.js +6 -0
- package/web/.next/server/middleware-build-manifest.js +1 -1
- package/web/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/web/.next/server/next-font-manifest.js +1 -1
- package/web/.next/server/next-font-manifest.json +1 -1
- package/web/.next/server/pages/404.html +1 -1
- package/web/.next/server/pages/500.html +1 -1
- package/web/.next/server/server-reference-manifest.json +1 -1
- package/web/.next/static/chunks/0937d497-114a2a3de515565e.js +1 -0
- package/web/.next/static/chunks/176-aeaf907a6ea5627d.js +1 -0
- package/web/.next/static/chunks/291-efae53ea5f7ee92d.js +1 -0
- package/web/.next/static/chunks/2b9ff8ee-969e65cc354b2150.js +1 -0
- package/web/.next/static/chunks/30-9adcbe92b15a6815.js +1 -0
- package/web/.next/static/chunks/316-26da9cc233ba7557.js +1 -0
- package/web/.next/static/chunks/350-56599dc3bf06066d.js +1 -0
- package/web/.next/static/chunks/486-13e08542f3a1cbac.js +1 -0
- package/web/.next/static/chunks/491-6b4e6201f5110dff.js +1 -0
- package/web/.next/static/chunks/517-ccaf8cc5c733cbd8.js +1 -0
- package/web/.next/static/chunks/545.bfbd92b7632c09de.js +1 -0
- package/web/.next/static/chunks/{805-d946ebedb7e85f73.js → 562-7c2e33d838e869bf.js} +1 -1
- package/web/.next/static/chunks/581-3ad5c4343dad3f50.js +1 -0
- package/web/.next/static/chunks/586-f7017d585a84738d.js +1 -0
- package/web/.next/static/chunks/657.b4c39e676c3e20dc.js +1 -0
- package/web/.next/static/chunks/701.6abcaf5e60463eef.js +6 -0
- package/web/.next/static/chunks/705-a16e1fe09be1ac7f.js +1 -0
- package/web/.next/static/chunks/706-fa2be0f10ffebe6a.js +1 -0
- package/web/.next/static/chunks/837-33c7480524f48577.js +1 -0
- package/web/.next/static/chunks/839-23c5508622fae942.js +1 -0
- package/web/.next/static/chunks/844-fe55292e0701f436.js +1 -0
- package/web/.next/static/chunks/89-b194fcffe5120c44.js +1 -0
- package/web/.next/static/chunks/97.594543c2196de524.js +1 -0
- package/web/.next/static/chunks/app/_not-found/page-238a7b80bda6b4ae.js +1 -0
- package/web/.next/static/chunks/app/agents/[id]/page-4af6c246c4ca7a97.js +1 -0
- package/web/.next/static/chunks/app/agents/page-f4c4c80f955f3967.js +1 -0
- package/web/.next/static/chunks/app/connections/page-0936beaf02d863c6.js +1 -0
- package/web/.next/static/chunks/app/layout-87ebb70bd9cb8443.js +1 -0
- package/web/.next/static/chunks/app/page-a7f292117b7d76bf.js +1 -0
- package/web/.next/static/chunks/app/relations/page-fba25393c8688714.js +1 -0
- package/web/.next/static/chunks/app/resources/[id]/page-d110d89bb8b5fca7.js +1 -0
- package/web/.next/static/chunks/app/resources/page-bbd0b450d3f3131d.js +1 -0
- package/web/.next/static/chunks/app/schedules/page-a1124397fc760413.js +1 -0
- package/web/.next/static/chunks/app/sessions/page-0e322811b0cde724.js +1 -0
- package/web/.next/static/chunks/app/skills/page-22ea0ef89d9f7a13.js +1 -0
- package/web/.next/static/chunks/app/types/page-18c4e1db2724d41d.js +1 -0
- package/web/.next/static/chunks/fcafe1ac.366e4950d3b0d4b7.js +18 -0
- package/web/.next/static/chunks/framework-bbac72adbbe2d328.js +1 -0
- package/web/.next/static/chunks/main-app-6086068e5fb5eacd.js +1 -0
- package/web/.next/static/chunks/main-c15f3fee674e0303.js +1 -0
- package/web/.next/static/chunks/pages/_app-d1670a2c6ce392fc.js +1 -0
- package/web/.next/static/chunks/pages/_error-95aac014d202fa7a.js +1 -0
- package/web/.next/static/chunks/webpack-ef515e41a83a16ac.js +1 -0
- package/web/.next/static/css/945795da867f5250.css +1 -0
- package/web/.next/static/css/dd4e32c285f75776.css +1 -0
- package/web/.next/static/p4S3i9kvO359GW91DQiHu/_buildManifest.js +1 -0
- package/web/.next/trace +29 -23
- package/web/.next/types/app/resources/[id]/page.ts +84 -0
- package/web/.next/types/app/resources/page.ts +84 -0
- package/web/.next/types/app/schedules/page.ts +84 -0
- package/web/.next/types/routes.d.ts +7 -2
- package/web/.next/types/validator.ts +27 -0
- package/web/next.config.mjs +4 -0
- package/web/.next/server/chunks/149.js +0 -11
- package/web/.next/server/chunks/247.js +0 -1
- package/web/.next/server/chunks/851.js +0 -1
- package/web/.next/server/chunks/932.js +0 -2
- package/web/.next/static/84BpRSahg2BTwk3mX6AG8/_buildManifest.js +0 -1
- package/web/.next/static/chunks/0937d497-44abb0f9355ace92.js +0 -1
- package/web/.next/static/chunks/2b9ff8ee-c3b61ac1e608c57d.js +0 -1
- package/web/.next/static/chunks/351-65da8c1a6e25f93b.js +0 -1
- package/web/.next/static/chunks/463-6ae35cdefdc59169.js +0 -1
- package/web/.next/static/chunks/493-92266798be872b8f.js +0 -1
- package/web/.next/static/chunks/517-eb26c72f6059aa26.js +0 -1
- package/web/.next/static/chunks/657.f1e03b7b65eba1d9.js +0 -1
- package/web/.next/static/chunks/687-3a4205f51be38602.js +0 -1
- package/web/.next/static/chunks/837-fd7623601dc78ed8.js +0 -1
- package/web/.next/static/chunks/850-318f32484989dd59.js +0 -1
- package/web/.next/static/chunks/89-ace2e1aa63babfdf.js +0 -1
- package/web/.next/static/chunks/951-e250b541b67da7ce.js +0 -1
- package/web/.next/static/chunks/app/_not-found/page-bbd54d7b9a55d7ae.js +0 -1
- package/web/.next/static/chunks/app/agents/[id]/page-5eaf094fec246f1b.js +0 -1
- package/web/.next/static/chunks/app/agents/page-e9b17f1c3439ef98.js +0 -1
- package/web/.next/static/chunks/app/connections/page-c6afd90abd82d47a.js +0 -1
- package/web/.next/static/chunks/app/layout-c415d5a847cd94b3.js +0 -1
- package/web/.next/static/chunks/app/page-6fc87fc5a128a204.js +0 -1
- package/web/.next/static/chunks/app/relations/page-0355a93a8eb67989.js +0 -1
- package/web/.next/static/chunks/app/sessions/page-beae9a74a07d69e0.js +0 -1
- package/web/.next/static/chunks/app/skills/page-4f839ac913e26d32.js +0 -1
- package/web/.next/static/chunks/app/types/page-4f6b99af3232f861.js +0 -1
- package/web/.next/static/chunks/framework-0e68884e76d9a8d3.js +0 -1
- package/web/.next/static/chunks/main-app-d6f73ccd41f325c5.js +0 -1
- package/web/.next/static/chunks/main-cd22509db15764e2.js +0 -1
- package/web/.next/static/chunks/pages/_app-0f75d689c738a6fc.js +0 -1
- package/web/.next/static/chunks/pages/_error-d481f6d49fb4455d.js +0 -1
- package/web/.next/static/chunks/webpack-d40de72e52f65f79.js +0 -1
- package/web/.next/static/css/abe1a6216491f944.css +0 -1
- /package/web/.next/static/{84BpRSahg2BTwk3mX6AG8 → p4S3i9kvO359GW91DQiHu}/_ssgManifest.js +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { DATA_DIR } from '../db.js';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
const INBOX_ROOT = join(DATA_DIR, 'inbox');
|
|
4
|
-
import {
|
|
4
|
+
import { spawnJson } from './spawn.js';
|
|
5
5
|
import { dispatchMcpConfigOpencode } from '../mcp-config.js';
|
|
6
6
|
import { opencodeSessionDir } from '../history/opencode.js';
|
|
7
7
|
import { withInstructions } from './preamble.js';
|
|
8
|
+
import { opencodeServer, stopOpencodeServer } from './opencode-server.js';
|
|
8
9
|
/**
|
|
9
10
|
* What OpenCode may do for each permission level. It has no such modes, so they are written as its own rules (a `deny` holds even with `--auto`):
|
|
10
11
|
* read-only changes nothing; editing changes files of the project (including .env, never .git or keys), runs no commands and stays in the agent's folder
|
|
@@ -29,60 +30,82 @@ export function opencodePermissions(level) {
|
|
|
29
30
|
export async function* runOpencode(o) {
|
|
30
31
|
const { agent } = o;
|
|
31
32
|
const args = ['run', '--format', 'json', '--thinking', '--auto'];
|
|
32
|
-
// `opencode run` normally talks to OpenCode's background service, which ignores the config we pass through the
|
|
33
|
-
// environment. A private (standalone) server reads it, which we need for the `dispatch` MCP and to deny `question`.
|
|
34
|
-
args.splice(1, 0, '--standalone');
|
|
35
33
|
// Sessions created while the folder was wrong (older hive-am builds) would keep running there; start fresh instead.
|
|
36
34
|
const recorded = agent.session_id ? opencodeSessionDir(agent.session_id) : null;
|
|
37
35
|
const resume = agent.session_id && (!recorded || recorded === agent.cwd) ? agent.session_id : null;
|
|
38
36
|
if (resume)
|
|
39
37
|
args.push('-s', resume);
|
|
38
|
+
// The reasoning level goes as the model's `#variant` (low / medium / high).
|
|
40
39
|
if (agent.model)
|
|
41
|
-
args.push('-m', agent.model);
|
|
40
|
+
args.push('-m', o.effort && !agent.model.includes('#') ? `${agent.model}#${o.effort}` : agent.model);
|
|
42
41
|
const prompt = withInstructions(o.prompt, o.instructions, !!resume, o.refreshInstructions);
|
|
43
|
-
args.push(prompt);
|
|
44
42
|
// Turns are non-interactive: OpenCode's `question` tool would be dismissed and end the turn with exit code 1, so
|
|
45
43
|
// deny it and the agent asks in plain text instead.
|
|
46
44
|
const config = { permission: { question: 'deny', ...opencodePermissions(agent.permission) }, ...(o.mcpCaps.length ? dispatchMcpConfigOpencode(agent.id, o.mcpCaps) : {}) };
|
|
47
45
|
const env = { OPENCODE_CONFIG_CONTENT: JSON.stringify(config) };
|
|
46
|
+
// `opencode run` normally talks to OpenCode's shared background service, which ignores the config we pass through the environment.
|
|
47
|
+
// We need that config (the `hive` MCP, `question` denied, the permission rules), so the turn goes to a server of this agent that
|
|
48
|
+
// started with it and keeps hive connected between turns. If that server cannot start, a private one-off server is used instead.
|
|
49
|
+
let access = null;
|
|
50
|
+
// Only agents answering an external chat gain enough from it (somebody is waiting for a reply and the first `channel_reply` would fail); every other agent keeps the one-off server and costs nothing between turns.
|
|
51
|
+
if (o.mcpCaps.includes('channel') && !process.env.HIVE_AM_OPENCODE_STANDALONE) {
|
|
52
|
+
try {
|
|
53
|
+
access = await opencodeServer(agent.id, agent.cwd, config, o.signal);
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
console.warn(`[opencode] no persistent server for ${agent.name}, using a private one: ${e instanceof Error ? e.message : e}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (access) {
|
|
60
|
+
args.splice(1, 0, '--server', access.url);
|
|
61
|
+
env.OPENCODE_SERVER_PASSWORD = access.password;
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
args.splice(1, 0, '--standalone');
|
|
65
|
+
args.push(prompt);
|
|
66
|
+
// Stopping a turn must really stop the model: a client that is killed would leave the server working on, so the server goes down with it.
|
|
67
|
+
if (access)
|
|
68
|
+
o.signal.addEventListener('abort', () => stopOpencodeServer(agent.id), { once: true });
|
|
48
69
|
const seen = new Map();
|
|
49
70
|
const toolStarted = new Set();
|
|
50
71
|
let sawSession = false;
|
|
51
|
-
|
|
52
|
-
const ev
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
sawSession = true;
|
|
57
|
-
yield { t: 'session', sessionId: ev.sessionID };
|
|
58
|
-
}
|
|
59
|
-
const p = ev.part;
|
|
60
|
-
if (!p)
|
|
61
|
-
continue;
|
|
62
|
-
if (p.type === 'text' || p.type === 'reasoning') {
|
|
63
|
-
const text = p.text ?? '';
|
|
64
|
-
const prev = seen.get(p.id) ?? 0;
|
|
65
|
-
if (text.length > prev) {
|
|
66
|
-
seen.set(p.id, text.length);
|
|
67
|
-
yield { t: p.type === 'text' ? 'text' : 'thinking', delta: text.slice(prev) };
|
|
72
|
+
try {
|
|
73
|
+
for await (const ev of spawnJson({ cmd: 'opencode', args, cwd: agent.cwd, env, signal: o.signal })) {
|
|
74
|
+
if (ev.sessionID && !sawSession) {
|
|
75
|
+
sawSession = true;
|
|
76
|
+
yield { t: 'session', sessionId: ev.sessionID };
|
|
68
77
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
yield { t: 'tool', id: p.callID ?? p.id, name: p.tool ?? p.name ?? 'tool', input: st.input };
|
|
78
|
+
const p = ev.part;
|
|
79
|
+
if (!p)
|
|
80
|
+
continue;
|
|
81
|
+
if (p.type === 'text' || p.type === 'reasoning') {
|
|
82
|
+
const text = p.text ?? '';
|
|
83
|
+
const prev = seen.get(p.id) ?? 0;
|
|
84
|
+
if (text.length > prev) {
|
|
85
|
+
seen.set(p.id, text.length);
|
|
86
|
+
yield { t: p.type === 'text' ? 'text' : 'thinking', delta: text.slice(prev) };
|
|
87
|
+
}
|
|
80
88
|
}
|
|
81
|
-
if (
|
|
82
|
-
const
|
|
83
|
-
|
|
89
|
+
else if (p.type === 'step-finish') {
|
|
90
|
+
const tk = p.tokens;
|
|
91
|
+
if (tk)
|
|
92
|
+
yield { t: 'usage', cost: p.cost || undefined, usage: { input: tk.input, output: tk.output, reasoning: tk.reasoning, cacheRead: tk.cache?.read, cacheWrite: tk.cache?.write } };
|
|
93
|
+
}
|
|
94
|
+
else if (p.type === 'tool') {
|
|
95
|
+
const st = p.state ?? {};
|
|
96
|
+
if (!toolStarted.has(p.callID ?? p.id)) {
|
|
97
|
+
toolStarted.add(p.callID ?? p.id);
|
|
98
|
+
yield { t: 'tool', id: p.callID ?? p.id, name: p.tool ?? p.name ?? 'tool', input: st.input };
|
|
99
|
+
}
|
|
100
|
+
if (st.status === 'completed' || st.status === 'error') {
|
|
101
|
+
const out = typeof st.output === 'string' ? st.output : Array.isArray(st.content) ? st.content.map((c) => c.text ?? '').join('\n') : '';
|
|
102
|
+
yield { t: 'tool_result', id: p.callID ?? p.id, output: out, error: st.status === 'error' };
|
|
103
|
+
}
|
|
84
104
|
}
|
|
85
105
|
}
|
|
86
106
|
}
|
|
107
|
+
finally {
|
|
108
|
+
access?.release();
|
|
109
|
+
}
|
|
87
110
|
yield { t: 'done', ok: true };
|
|
88
111
|
}
|
|
@@ -11,3 +11,5 @@ export function withInstructions(prompt, instructions, resumed, refresh) {
|
|
|
11
11
|
return `<instructions update="true">\nYour configuration changed since the last message. These instructions replace any earlier ones; use them from now on.\n\n${instructions}\n</instructions>\n\n${prompt}`;
|
|
12
12
|
return prompt;
|
|
13
13
|
}
|
|
14
|
+
/** Takes the instructions block off the front of a user message read back from a CLI's own history: it is ours, not what the person wrote. */
|
|
15
|
+
export const stripInstructions = (text) => text.replace(/^<instructions[^>]*>[\s\S]*?<\/instructions>\s*/, '');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
+
const KILL_GRACE_MS = Number(process.env.HIVE_AM_KILL_GRACE_MS) || 5000;
|
|
3
4
|
/** Spawns a CLI and yields its stdout lines. Throws with stderr tail if it exits non-zero and nothing aborted it. */
|
|
4
5
|
export async function* spawnLines(o) {
|
|
5
6
|
const child = spawn(o.cmd, o.args, {
|
|
@@ -8,8 +9,17 @@ export async function* spawnLines(o) {
|
|
|
8
9
|
env: { ...process.env, PWD: o.cwd, ...o.env },
|
|
9
10
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
10
11
|
});
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
// SIGTERM first; a CLI that ignores it (busy inside a tool) is cut off after a grace period so a stopped turn never leaves a process behind.
|
|
13
|
+
let hardKill;
|
|
14
|
+
const kill = () => {
|
|
15
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
16
|
+
return;
|
|
17
|
+
if (!child.killed)
|
|
18
|
+
child.kill('SIGTERM');
|
|
19
|
+
hardKill ??= setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
|
|
20
|
+
child.kill('SIGKILL'); }, KILL_GRACE_MS);
|
|
21
|
+
hardKill.unref?.();
|
|
22
|
+
};
|
|
13
23
|
o.signal.addEventListener('abort', kill, { once: true });
|
|
14
24
|
let stderr = '';
|
|
15
25
|
child.stderr.on('data', (d) => { stderr = (stderr + d).slice(-4000); });
|
|
@@ -17,8 +27,10 @@ export async function* spawnLines(o) {
|
|
|
17
27
|
child.on('error', (e) => { stderr += String(e); res(-1); });
|
|
18
28
|
// 'exit' (not 'close'): a daemon the CLI forks (e.g. opencode's background service) can keep
|
|
19
29
|
// inheriting our stdout pipe forever, so 'close' would never fire. Drain briefly, then cut the pipe.
|
|
20
|
-
child.on('exit', (c) => {
|
|
30
|
+
child.on('exit', (c) => { if (hardKill)
|
|
31
|
+
clearTimeout(hardKill); setTimeout(() => child.stdout.destroy(), 250); res(c); });
|
|
21
32
|
});
|
|
33
|
+
child.stdin.on('error', () => { });
|
|
22
34
|
if (o.stdin !== undefined)
|
|
23
35
|
child.stdin.end(o.stdin);
|
|
24
36
|
else
|
|
@@ -44,3 +56,19 @@ export function safeJson(line) {
|
|
|
44
56
|
return null;
|
|
45
57
|
}
|
|
46
58
|
}
|
|
59
|
+
/** The JSON objects a CLI prints, one per line; lines that are not JSON (banners, warnings) are skipped. */
|
|
60
|
+
export async function* spawnJson(o) {
|
|
61
|
+
for await (const line of spawnLines(o)) {
|
|
62
|
+
const ev = safeJson(line);
|
|
63
|
+
if (ev)
|
|
64
|
+
yield ev;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** The text of a tool result, which CLIs send as a string, a list of blocks or any other JSON. */
|
|
68
|
+
export function flattenText(c) {
|
|
69
|
+
if (typeof c === 'string')
|
|
70
|
+
return c;
|
|
71
|
+
if (Array.isArray(c))
|
|
72
|
+
return c.map((x) => (typeof x === 'string' ? x : x?.text ?? '')).join('\n');
|
|
73
|
+
return c == null ? '' : JSON.stringify(c);
|
|
74
|
+
}
|
package/server/dist/runtime.js
CHANGED
|
@@ -19,6 +19,7 @@ export const liveTurn = (id) => states.get(id)?.live ?? null;
|
|
|
19
19
|
export const queueDepth = (id) => states.get(id)?.queued ?? 0;
|
|
20
20
|
/** The channel message the agent is answering right now, if the running turn came from one. */
|
|
21
21
|
export const liveOrigin = (id) => states.get(id)?.live?.origin;
|
|
22
|
+
export const liveSource = (id) => states.get(id)?.live?.source;
|
|
22
23
|
/** Whether the turn running now is the agent's own conversation (not work delegated to it by an orchestrator). */
|
|
23
24
|
export const liveIsDirect = (id) => states.get(id)?.live?.source === 'user';
|
|
24
25
|
/** Queue a prompt for an agent. Turns for one agent run strictly one at a time. */
|
|
@@ -57,21 +58,23 @@ async function execute(agentId, prompt, source, from, dispatchId, origin) {
|
|
|
57
58
|
agents.setStatus(agentId, 'running');
|
|
58
59
|
emit({ kind: 'turn_start', agentId, turnId, prompt, source, from, channel: origin && { platform: origin.platform, place: origin.place, user: origin.userName } });
|
|
59
60
|
emit({ kind: 'status', agentId, status: 'running', queued: s.queued });
|
|
60
|
-
const base = composeInstructions(agent, delegated);
|
|
61
|
-
const nbVersion = notebooks.get(agentId).version;
|
|
62
|
-
const instructions = [base, notebookBlock(agent)].filter(Boolean).join('\n\n');
|
|
63
|
-
const hash = createHash('sha1').update(base).digest('hex').slice(0, 16);
|
|
64
|
-
// The conversation is re-sent the instructions when its configuration changed, or when the notes were edited behind its back (by the user, or in another conversation).
|
|
65
|
-
const refreshInstructions = !delegated && !!agent.session_id && (raw.instr_hash !== hash || notebooks.get(agentId).seen < nbVersion);
|
|
66
61
|
let text = '';
|
|
67
62
|
let summary;
|
|
68
63
|
let error;
|
|
64
|
+
let hash = '', nbVersion = 0;
|
|
69
65
|
const push = (event) => { s.live?.events.push(event); emit({ kind: 'event', agentId, turnId, event }); };
|
|
70
66
|
try {
|
|
67
|
+
// Composing the instructions can fail too (a damaged skill row...): that is this turn's error, and it must not leave the agent "running" forever.
|
|
68
|
+
const base = composeInstructions(agent, delegated);
|
|
69
|
+
nbVersion = notebooks.get(agentId).version;
|
|
70
|
+
const instructions = [base, notebookBlock(agent)].filter(Boolean).join('\n\n');
|
|
71
|
+
hash = createHash('sha1').update(base).digest('hex').slice(0, 16);
|
|
72
|
+
// The conversation is re-sent the instructions when its configuration changed, or when the notes were edited behind its back (by the user, or in another conversation).
|
|
73
|
+
const refreshInstructions = !delegated && !!agent.session_id && (raw.instr_hash !== hash || notebooks.get(agentId).seen < nbVersion);
|
|
71
74
|
const stream = runners[agent.provider]({
|
|
72
75
|
agent, prompt, signal: controller.signal,
|
|
73
76
|
instructions, refreshInstructions,
|
|
74
|
-
mcpCaps: mcpCaps(agent),
|
|
77
|
+
mcpCaps: mcpCaps(agent), effort: origin?.effort,
|
|
75
78
|
});
|
|
76
79
|
for await (const ev of stream) {
|
|
77
80
|
if (ev.t === 'session') {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { agents, db } from './db.js';
|
|
3
|
+
import { CronError, minGapMinutes, nextCron, nextRuns, parseCron, serverTimezone, validTimezone } from './cron.js';
|
|
4
|
+
import { adapterFor } from './connections/manager.js';
|
|
5
|
+
import { deliverScheduled } from './connections/router.js';
|
|
6
|
+
import { connections, threads } from './connections/store.js';
|
|
7
|
+
import { liveOrigin, liveSource, queueDepth, sendTurn } from './runtime.js';
|
|
8
|
+
import { DomainError } from './errors.js';
|
|
9
|
+
/**
|
|
10
|
+
* Recurring schedules an agent sets for itself ("every weekday at 9, check the QA logs and tell me"). Like a wake-up, a run reaches the agent
|
|
11
|
+
* in the place where the schedule was made: the same chat thread, or its own web conversation. Cost is bounded: a minimum interval between runs,
|
|
12
|
+
* a limit per agent, no catching up after an outage (a missed run is skipped, never replayed) and no run while the agent is already behind.
|
|
13
|
+
*/
|
|
14
|
+
export class ScheduleError extends DomainError {
|
|
15
|
+
}
|
|
16
|
+
const MS_PER_MINUTE = Number(process.env.HIVE_AM_WAKE_MS_PER_MINUTE) || 60_000; // tests shrink a "minute" (only for "every N minutes")
|
|
17
|
+
export const MIN_INTERVAL_MINUTES = Number(process.env.HIVE_AM_SCHEDULE_MIN_MINUTES) || 15;
|
|
18
|
+
export const MAX_PER_AGENT = 10;
|
|
19
|
+
const MISSED_GRACE_MS = 5 * 60_000; // a run later than this (the server was down) is skipped
|
|
20
|
+
const BUSY_QUEUE = 2; // skip a run when this many messages already wait for the agent
|
|
21
|
+
const KEEP_RUNS = 50;
|
|
22
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
23
|
+
const fromRow = (r) => ({ ...r, enabled: !!r.enabled });
|
|
24
|
+
const store = {
|
|
25
|
+
get: (id) => { const r = db.prepare('SELECT * FROM agent_schedules WHERE id=?').get(id); return r ? fromRow(r) : undefined; },
|
|
26
|
+
all: () => db.prepare('SELECT * FROM agent_schedules ORDER BY created_at').all().map(fromRow),
|
|
27
|
+
forAgent: (agentId) => db.prepare('SELECT * FROM agent_schedules WHERE agent_id=? ORDER BY created_at').all(agentId).map(fromRow),
|
|
28
|
+
remove: (id) => db.prepare('DELETE FROM agent_schedules WHERE id=?').run(id).changes > 0,
|
|
29
|
+
setNext: (id, next) => db.prepare('UPDATE agent_schedules SET next_due=? WHERE id=?').run(next, id),
|
|
30
|
+
setEnabled: (id, on, next) => db.prepare('UPDATE agent_schedules SET enabled=?, next_due=? WHERE id=?').run(on ? 1 : 0, next, id),
|
|
31
|
+
record(id, status, detail, durationMs, firedAt) {
|
|
32
|
+
db.prepare('INSERT INTO schedule_runs (schedule_id,fired_at,status,detail,duration_ms) VALUES (?,?,?,?,?)').run(id, firedAt, status, detail, durationMs);
|
|
33
|
+
db.prepare(`DELETE FROM schedule_runs WHERE schedule_id=? AND id NOT IN (SELECT id FROM schedule_runs WHERE schedule_id=? ORDER BY id DESC LIMIT ${KEEP_RUNS})`).run(id, id);
|
|
34
|
+
db.prepare('UPDATE agent_schedules SET last_status=?, last_detail=?, last_fired_at=?, fire_count=fire_count+? WHERE id=?').run(status, detail, firedAt, status === 'delivered' ? 1 : 0, id);
|
|
35
|
+
},
|
|
36
|
+
runs: (id, limit = KEEP_RUNS) => db.prepare('SELECT id,fired_at,status,detail,duration_ms FROM schedule_runs WHERE schedule_id=? ORDER BY id DESC LIMIT ?').all(id, limit),
|
|
37
|
+
};
|
|
38
|
+
const hhmm = (t, tz) => new Date(t).toLocaleString('en-GB', { timeZone: tz, weekday: 'short', day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' });
|
|
39
|
+
export const describe = (s) => s.kind === 'every' ? `every ${s.expr} min` : `${s.expr} (${s.tz})`;
|
|
40
|
+
/** Next run after `after`. "every N" keeps its rhythm from the previous due time instead of drifting by the run time. */
|
|
41
|
+
function computeNext(s, after, from) {
|
|
42
|
+
if (s.kind === 'cron')
|
|
43
|
+
return nextCron(s.expr, s.tz, after);
|
|
44
|
+
const step = Number(s.expr) * MS_PER_MINUTE;
|
|
45
|
+
let t = (from ?? after) + step;
|
|
46
|
+
if (t <= after)
|
|
47
|
+
t += Math.ceil((after - t + 1) / step) * step;
|
|
48
|
+
return t;
|
|
49
|
+
}
|
|
50
|
+
// ---- timers ----
|
|
51
|
+
const timers = new Map();
|
|
52
|
+
function arm(s) {
|
|
53
|
+
const old = timers.get(s.id);
|
|
54
|
+
if (old)
|
|
55
|
+
clearTimeout(old);
|
|
56
|
+
timers.delete(s.id);
|
|
57
|
+
if (!s.enabled || s.next_due === null)
|
|
58
|
+
return;
|
|
59
|
+
const delay = Math.max(0, Math.min(s.next_due - Date.now(), MAX_TIMER_MS));
|
|
60
|
+
const t = setTimeout(() => { if (s.next_due !== null && s.next_due - Date.now() > 1000)
|
|
61
|
+
arm(store.get(s.id) ?? s);
|
|
62
|
+
else
|
|
63
|
+
void fire(s.id); }, delay);
|
|
64
|
+
t.unref?.();
|
|
65
|
+
timers.set(s.id, t);
|
|
66
|
+
}
|
|
67
|
+
const disarm = (id) => { const t = timers.get(id); if (t)
|
|
68
|
+
clearTimeout(t); timers.delete(id); };
|
|
69
|
+
function text(s, web) {
|
|
70
|
+
const tool = web ? '' : ' with channel_reply';
|
|
71
|
+
return `${web ? '🔔 ' : ''}Scheduled task (${describe(s)}${s.req_name ? `, asked by ${s.req_name}` : ''}). Your note: ${s.note}\n\n${s.req_name ? `hive-am mentions ${s.req_name} at the start of your first reply, so do not mention them yourself. ` : ''}Do what the note says now and tell the person the result${tool}. If there is nothing worth reporting, say so in one line. You can see or stop your schedules with schedule_list / schedule_cancel.`;
|
|
72
|
+
}
|
|
73
|
+
async function fire(id) {
|
|
74
|
+
timers.delete(id);
|
|
75
|
+
const s = store.get(id);
|
|
76
|
+
if (!s || !s.enabled)
|
|
77
|
+
return;
|
|
78
|
+
const now = Date.now(), due = s.next_due ?? now;
|
|
79
|
+
const next = computeNext(s, now, s.kind === 'every' ? due : null);
|
|
80
|
+
store.setNext(id, next); // the next run is fixed before anything else can fail
|
|
81
|
+
arm({ ...s, next_due: next });
|
|
82
|
+
const skip = (status, detail) => store.record(id, status, detail, null, now);
|
|
83
|
+
const agent = agents.get(s.agent_id);
|
|
84
|
+
if (!agent) {
|
|
85
|
+
store.remove(id);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (now - due > MISSED_GRACE_MS)
|
|
89
|
+
return skip('skipped_missed', `due ${hhmm(due, s.tz)}, ${Math.round((now - due) / 60000)} min late (the server was down)`);
|
|
90
|
+
if (queueDepth(agent.id) >= BUSY_QUEUE)
|
|
91
|
+
return skip('skipped_busy', 'the agent already had messages waiting');
|
|
92
|
+
const started = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
if (s.thread_id) {
|
|
95
|
+
const conn = connections.get(s.connection_id ?? ''), thread = threads.get(s.thread_id);
|
|
96
|
+
if (!conn || !thread) {
|
|
97
|
+
store.remove(id);
|
|
98
|
+
disarm(id);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!conn.enabled)
|
|
102
|
+
return skip('skipped_disabled', 'the connection is disabled');
|
|
103
|
+
if (conn.agent_id !== s.agent_id) {
|
|
104
|
+
store.setEnabled(id, false, null);
|
|
105
|
+
disarm(id);
|
|
106
|
+
return skip('skipped_reassigned', 'the connection now belongs to another agent; the schedule was paused');
|
|
107
|
+
}
|
|
108
|
+
if (thread.muted)
|
|
109
|
+
return skip('skipped_muted', 'the thread is muted');
|
|
110
|
+
const adapter = adapterFor(conn.id);
|
|
111
|
+
if (!adapter)
|
|
112
|
+
return skip('skipped_disabled', 'the connection was not running');
|
|
113
|
+
store.record(id, 'delivered', null, null, started);
|
|
114
|
+
await deliverScheduled(conn, adapter, thread, text(s, false), s.req_id ? { id: s.req_id, name: s.req_name || 'you' } : undefined);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
store.record(id, 'delivered', null, null, started);
|
|
118
|
+
await sendTurn(agent.id, text(s, true), 'user');
|
|
119
|
+
}
|
|
120
|
+
db.prepare('UPDATE schedule_runs SET duration_ms=? WHERE schedule_id=? AND fired_at=? AND status=?').run(Date.now() - started, id, started, 'delivered');
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
124
|
+
db.prepare('UPDATE schedule_runs SET status=?, detail=? WHERE schedule_id=? AND fired_at=? AND status=?').run('failed', msg, id, started, 'delivered');
|
|
125
|
+
db.prepare('UPDATE agent_schedules SET last_status=?, last_detail=?, fire_count=MAX(0,fire_count-1) WHERE id=?').run('failed', msg, id);
|
|
126
|
+
console.warn(`[schedules] run of "${s.note.slice(0, 40)}" failed: ${msg}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Arms every saved schedule (called once at startup). A run missed while the server was down is skipped, not replayed. */
|
|
130
|
+
export function armSchedules() {
|
|
131
|
+
for (const s of store.all()) {
|
|
132
|
+
if (!s.enabled)
|
|
133
|
+
continue;
|
|
134
|
+
if (s.next_due !== null && s.next_due < Date.now())
|
|
135
|
+
void fire(s.id);
|
|
136
|
+
else
|
|
137
|
+
arm(s);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export function createSchedule(agentId, input) {
|
|
141
|
+
if (!agents.get(agentId))
|
|
142
|
+
throw new ScheduleError('Unknown agent');
|
|
143
|
+
if (liveSource(agentId) === 'dispatch')
|
|
144
|
+
throw new ScheduleError('Schedules are not available while you work on a task delegated by an orchestrator.');
|
|
145
|
+
const note = String(input.note ?? '').trim();
|
|
146
|
+
if (!note)
|
|
147
|
+
throw new ScheduleError('Say in the note what to do at each run.');
|
|
148
|
+
if (note.length > 400)
|
|
149
|
+
throw new ScheduleError('The note is too long (400 characters at most).');
|
|
150
|
+
const tz = input.timezone === undefined || input.timezone === '' ? serverTimezone() : String(input.timezone);
|
|
151
|
+
if (!validTimezone(tz))
|
|
152
|
+
throw new ScheduleError(`Unknown time zone "${tz}". Use an IANA name such as America/Mexico_City.`);
|
|
153
|
+
const hasCron = input.cron !== undefined && String(input.cron).trim() !== '', hasEvery = input.every_minutes !== undefined && input.every_minutes !== null;
|
|
154
|
+
if (hasCron === hasEvery)
|
|
155
|
+
throw new ScheduleError('Give either "cron" (for example "0 9 * * MON-FRI") or "every_minutes", not both and not neither.');
|
|
156
|
+
let kind, expr, first;
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
if (hasCron) {
|
|
159
|
+
kind = 'cron';
|
|
160
|
+
expr = String(input.cron).trim();
|
|
161
|
+
try {
|
|
162
|
+
parseCron(expr);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
throw new ScheduleError(e instanceof CronError ? e.message : String(e));
|
|
166
|
+
}
|
|
167
|
+
const gap = minGapMinutes(expr, tz, now);
|
|
168
|
+
if (gap < MIN_INTERVAL_MINUTES)
|
|
169
|
+
throw new ScheduleError(`Runs would be only ${gap} min apart; the minimum is ${MIN_INTERVAL_MINUTES} min. Choose a less frequent schedule.`);
|
|
170
|
+
first = nextCron(expr, tz, now);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
kind = 'every';
|
|
174
|
+
const m = Number(input.every_minutes);
|
|
175
|
+
if (!Number.isFinite(m) || m < MIN_INTERVAL_MINUTES || m > 60 * 24 * 31)
|
|
176
|
+
throw new ScheduleError(`every_minutes must be between ${MIN_INTERVAL_MINUTES} and ${60 * 24 * 31}.`);
|
|
177
|
+
expr = String(Math.round(m));
|
|
178
|
+
first = now + Math.round(m) * MS_PER_MINUTE;
|
|
179
|
+
}
|
|
180
|
+
if (first === null)
|
|
181
|
+
throw new ScheduleError('That schedule never runs.');
|
|
182
|
+
const origin = liveOrigin(agentId), thread = origin ? threads.get(origin.threadId) : undefined;
|
|
183
|
+
if (origin && !thread)
|
|
184
|
+
throw new ScheduleError('The thread no longer exists.');
|
|
185
|
+
const mine = store.forAgent(agentId);
|
|
186
|
+
if (mine.length >= MAX_PER_AGENT)
|
|
187
|
+
throw new ScheduleError(`You already have ${MAX_PER_AGENT} schedules. Cancel one with schedule_cancel first.`);
|
|
188
|
+
const dup = mine.find((x) => x.kind === kind && x.expr === expr && x.tz === tz && x.note.toLowerCase() === note.toLowerCase() && x.thread_id === (thread?.id ?? null));
|
|
189
|
+
if (dup)
|
|
190
|
+
return { schedule: dup, next: kind === 'cron' ? nextRuns(expr, tz, now, 3).map((t) => hhmm(t, tz)) : [hhmm(dup.next_due ?? first, tz)], where: thread ? 'thread' : 'chat' };
|
|
191
|
+
const id = randomUUID();
|
|
192
|
+
db.prepare('INSERT INTO agent_schedules (id,agent_id,connection_id,thread_id,kind,expr,tz,note,enabled,next_due,created_at,req_id,req_name) VALUES (?,?,?,?,?,?,?,?,1,?,?,?,?)')
|
|
193
|
+
.run(id, agentId, origin?.connectionId ?? null, thread?.id ?? null, kind, expr, tz, note, first, now, origin?.group && origin.userId ? origin.userId : null, origin?.group && origin.userId ? origin.userName : null);
|
|
194
|
+
const schedule = store.get(id);
|
|
195
|
+
arm(schedule);
|
|
196
|
+
return { schedule, next: kind === 'cron' ? nextRuns(expr, tz, now, 3).map((t) => hhmm(t, tz)) : [hhmm(first, tz)], where: thread ? 'thread' : 'chat' };
|
|
197
|
+
}
|
|
198
|
+
export function listFor(agentId) {
|
|
199
|
+
return store.forAgent(agentId).map((s) => ({ id: s.id, schedule: describe(s), note: s.note, enabled: s.enabled, next: s.next_due ? hhmm(s.next_due, s.tz) : null, last: s.last_status }));
|
|
200
|
+
}
|
|
201
|
+
export function cancelFor(agentId, id) {
|
|
202
|
+
const s = store.get(id);
|
|
203
|
+
if (!s || s.agent_id !== agentId)
|
|
204
|
+
throw new ScheduleError('You have no schedule with that id. Use schedule_list to see yours.');
|
|
205
|
+
disarm(id);
|
|
206
|
+
return store.remove(id);
|
|
207
|
+
}
|
|
208
|
+
export function listAll() {
|
|
209
|
+
return store.all().map((s) => ({ ...s, agent_name: agents.get(s.agent_id)?.name ?? '?', place: s.thread_id ? (threads.get(s.thread_id)?.title || 'chat') : '', description: describe(s) }));
|
|
210
|
+
}
|
|
211
|
+
export const runsOf = (id) => store.runs(id);
|
|
212
|
+
export function setEnabled(id, on) {
|
|
213
|
+
const s = store.get(id);
|
|
214
|
+
if (!s)
|
|
215
|
+
return undefined;
|
|
216
|
+
const next = on ? computeNext(s, Date.now(), null) : null;
|
|
217
|
+
store.setEnabled(id, on, next);
|
|
218
|
+
const u = store.get(id);
|
|
219
|
+
arm(u);
|
|
220
|
+
return u;
|
|
221
|
+
}
|
|
222
|
+
export function removeSchedule(id) { disarm(id); return store.remove(id); }
|
package/server/dist/seed.js
CHANGED
|
@@ -4,30 +4,34 @@ import { NOTEBOOK_SKILL_ID } from './skills/defaults.js';
|
|
|
4
4
|
export function seedIfEmpty() {
|
|
5
5
|
if (types.list().length || agents.list().length || skills.list().some((s) => !s.id.startsWith('default-')))
|
|
6
6
|
return;
|
|
7
|
-
const s1 = skills.create({
|
|
8
|
-
name: 'concise-reports', description: 'Short, structured final answers',
|
|
9
|
-
content: 'Finish every task with a report: what you did, what you found, what is left. Use short bullet points. No filler.',
|
|
10
|
-
});
|
|
11
|
-
const s2 = skills.create({
|
|
12
|
-
name: 'careful-reviewer', description: 'Review code for bugs before style',
|
|
13
|
-
content: 'When reviewing, rank findings by severity. Report concrete failure scenarios, not preferences. Never edit files unless asked.',
|
|
14
|
-
});
|
|
15
7
|
types.create({
|
|
16
8
|
name: 'Queen', role: 'orchestrator', provider: 'claude', model: 'sonnet', permission: 'acceptEdits', color: '#E8A317',
|
|
17
9
|
description: 'Plans the work and delegates it to workers',
|
|
18
10
|
system_prompt: 'You are an orchestrator. Break the request into independent tasks, delegate each to the best subagent, then merge their answers into one clear result.',
|
|
19
|
-
skill_ids: [
|
|
11
|
+
skill_ids: [NOTEBOOK_SKILL_ID],
|
|
20
12
|
});
|
|
21
13
|
types.create({
|
|
22
14
|
name: 'Builder', role: 'worker', provider: 'claude', model: 'sonnet', permission: 'acceptEdits', color: '#D97757',
|
|
23
15
|
description: 'Implements features and fixes in the codebase',
|
|
24
16
|
system_prompt: 'You implement changes carefully and verify them by running the project’s tests when they exist.',
|
|
25
|
-
skill_ids: [
|
|
17
|
+
skill_ids: [NOTEBOOK_SKILL_ID],
|
|
26
18
|
});
|
|
27
19
|
types.create({
|
|
28
20
|
name: 'Reviewer', role: 'worker', provider: 'opencode', model: '', permission: 'plan', color: '#3E7BFA',
|
|
29
21
|
description: 'Reads code and reports problems without changing it',
|
|
30
22
|
system_prompt: 'You review code. You never modify files.',
|
|
31
|
-
skill_ids: [
|
|
23
|
+
skill_ids: [],
|
|
32
24
|
});
|
|
33
25
|
}
|
|
26
|
+
/** The two example skills older versions created on the first run are gone; where they are still as they were made, they are removed. */
|
|
27
|
+
const STARTER = [
|
|
28
|
+
{ name: 'concise-reports', content: 'Finish every task with a report: what you did, what you found, what is left. Use short bullet points. No filler.' },
|
|
29
|
+
{ name: 'careful-reviewer', content: 'When reviewing, rank findings by severity. Report concrete failure scenarios, not preferences. Never edit files unless asked.' },
|
|
30
|
+
];
|
|
31
|
+
export function dropStarterSkills() {
|
|
32
|
+
for (const d of STARTER) {
|
|
33
|
+
const s = skills.list().find((x) => x.name === d.name && x.content === d.content && !x.id.startsWith('default-'));
|
|
34
|
+
if (s)
|
|
35
|
+
skills.remove(s.id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { agents, db, resolved } from '../db.js';
|
|
2
|
+
import { channelPermissionError } from '../connections/rules.js';
|
|
3
|
+
import { liveTurn, notifyAgentsChanged, stopAgent } from '../runtime.js';
|
|
4
|
+
import { findRepo } from '../git/repo.js';
|
|
5
|
+
import { bad, notFound } from '../http/errors.js';
|
|
6
|
+
import { trimmed } from '../http/validate.js';
|
|
7
|
+
const NO_FOLDER = 'Choose a working folder, or put the agent in a colony that provides one.';
|
|
8
|
+
export const agentOrFail = (id) => agents.get(id) ?? (() => { throw notFound('Agent not found'); })();
|
|
9
|
+
/** The folder the agent really works in (its own, or its colony's when it inherits). */
|
|
10
|
+
export const agentCwd = (id) => resolved(agentOrFail(id)).cwd;
|
|
11
|
+
const uniqueName = (name, exceptId) => { const o = agents.byName(name); if (o && o.id !== exceptId)
|
|
12
|
+
throw bad(`An agent named "${name}" already exists`); };
|
|
13
|
+
/** Creates an agent. With no folder of its own and none inherited it could never run, so it is removed again. */
|
|
14
|
+
function create(p) {
|
|
15
|
+
const a = agents.create(p);
|
|
16
|
+
if (!a.effective.cwd) {
|
|
17
|
+
agents.remove(a.id);
|
|
18
|
+
throw bad(NO_FOLDER);
|
|
19
|
+
}
|
|
20
|
+
notifyAgentsChanged();
|
|
21
|
+
return a;
|
|
22
|
+
}
|
|
23
|
+
export function createAgent(p) {
|
|
24
|
+
const name = trimmed(p.name);
|
|
25
|
+
uniqueName(name);
|
|
26
|
+
return create({ ...p, name, cwd: p.cwd ?? '' });
|
|
27
|
+
}
|
|
28
|
+
/** A new agent that starts from a type: the type lends everything but name, description, folder and colony. */
|
|
29
|
+
export function spawnFromType(t, p) {
|
|
30
|
+
const name = trimmed(p.name);
|
|
31
|
+
uniqueName(name);
|
|
32
|
+
return create({
|
|
33
|
+
name, description: p.description ?? t.description, role: t.role, type_id: t.id, provider: t.provider, model: t.model,
|
|
34
|
+
system_prompt: t.system_prompt, permission: t.permission, cwd: p.cwd ?? '', skill_ids: t.skill_ids, skill_loads: t.skill_loads, colony_id: p.colony_id ?? null,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function updateAgent(id, p) {
|
|
38
|
+
const cur = agentOrFail(id);
|
|
39
|
+
if (p.name && p.name.toLowerCase() !== cur.name.toLowerCase())
|
|
40
|
+
uniqueName(p.name, id);
|
|
41
|
+
// Changing provider invalidates the native session pointer. (A changed folder is detected when the next turn starts.)
|
|
42
|
+
// All-or-nothing: a change that would leave a linked agent in Plan mode is rolled back.
|
|
43
|
+
const a = db.transaction(() => {
|
|
44
|
+
const u = agents.update(id, p);
|
|
45
|
+
const why = channelPermissionError(u.id);
|
|
46
|
+
if (why)
|
|
47
|
+
throw bad(why);
|
|
48
|
+
return u;
|
|
49
|
+
})();
|
|
50
|
+
if (p.provider && p.provider !== cur.provider)
|
|
51
|
+
agents.setSession(a, null);
|
|
52
|
+
if (!agents.get(a.id).effective.cwd)
|
|
53
|
+
throw bad('Choose a working folder, or keep inheriting the colony’s folder.');
|
|
54
|
+
notifyAgentsChanged();
|
|
55
|
+
return agents.get(id);
|
|
56
|
+
}
|
|
57
|
+
export function removeAgent(id) {
|
|
58
|
+
stopAgent(id);
|
|
59
|
+
if (!agents.remove(id))
|
|
60
|
+
throw notFound('Agent not found');
|
|
61
|
+
notifyAgentsChanged();
|
|
62
|
+
}
|
|
63
|
+
/** The session to read: the requested one must belong to the agent (its own or a delegated one). */
|
|
64
|
+
export function sessionToRead(a, requested) {
|
|
65
|
+
const sid = requested ?? a.session_id;
|
|
66
|
+
if (sid && sid !== a.session_id && !agents.sessions(a.id).some((s) => s.session_id === sid))
|
|
67
|
+
throw bad('That session does not belong to this agent');
|
|
68
|
+
return sid;
|
|
69
|
+
}
|
|
70
|
+
/** Only the agent's own conversations can be made current; delegated ones are read-only. */
|
|
71
|
+
export function resumeSession(a, sessionId) {
|
|
72
|
+
if (!agents.sessions(a.id).some((s) => s.session_id === sessionId && s.kind !== 'delegation'))
|
|
73
|
+
throw bad('Only the agent’s own conversations can be made current. Delegated sessions are read-only.');
|
|
74
|
+
agents.setSession(a, sessionId);
|
|
75
|
+
notifyAgentsChanged();
|
|
76
|
+
}
|
|
77
|
+
/** Which other agents are working in the same repository right now (a branch switch moves their files too). */
|
|
78
|
+
export async function othersInRepo(agentId, cwd) {
|
|
79
|
+
const others = [];
|
|
80
|
+
const here = await findRepo(cwd);
|
|
81
|
+
if (here.ok)
|
|
82
|
+
for (const a of agents.list()) {
|
|
83
|
+
if (a.id === agentId || !(a.status === 'running' || liveTurn(a.id)))
|
|
84
|
+
continue;
|
|
85
|
+
const there = await findRepo(resolved(a).cwd);
|
|
86
|
+
if (there.ok && there.repo.root === here.repo.root)
|
|
87
|
+
others.push({ id: a.id, name: a.name });
|
|
88
|
+
}
|
|
89
|
+
return others;
|
|
90
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { colonies, db } from '../db.js';
|
|
2
|
+
import { channelPermissionError } from '../connections/rules.js';
|
|
3
|
+
import { notifyAgentsChanged } from '../runtime.js';
|
|
4
|
+
import { bad, notFound } from '../http/errors.js';
|
|
5
|
+
import { trimmed } from '../http/validate.js';
|
|
6
|
+
const sameName = (name, exceptId) => colonies.list().some((c) => c.id !== exceptId && c.name.toLowerCase() === name.toLowerCase());
|
|
7
|
+
export function createColony(p) {
|
|
8
|
+
const name = trimmed(p.name);
|
|
9
|
+
if (sameName(name))
|
|
10
|
+
throw bad(`A colony named "${p.name}" already exists`);
|
|
11
|
+
const c = colonies.create({ ...p, name });
|
|
12
|
+
notifyAgentsChanged();
|
|
13
|
+
return c;
|
|
14
|
+
}
|
|
15
|
+
export function updateColony(id, p) {
|
|
16
|
+
const cur = colonies.get(id);
|
|
17
|
+
if (!cur)
|
|
18
|
+
throw notFound('Colony not found');
|
|
19
|
+
if (p.name && p.name.toLowerCase() !== cur.name.toLowerCase() && sameName(trimmed(p.name)))
|
|
20
|
+
throw bad(`A colony named "${p.name}" already exists`);
|
|
21
|
+
// A colony can lend Plan mode to an agent that has a channel: all-or-nothing.
|
|
22
|
+
const c = db.transaction(() => {
|
|
23
|
+
const u = colonies.update(id, p.name ? { ...p, name: trimmed(p.name) } : p);
|
|
24
|
+
for (const aid of u.agent_ids) {
|
|
25
|
+
const why = channelPermissionError(aid);
|
|
26
|
+
if (why)
|
|
27
|
+
throw bad(why);
|
|
28
|
+
}
|
|
29
|
+
return u;
|
|
30
|
+
})();
|
|
31
|
+
notifyAgentsChanged();
|
|
32
|
+
return c;
|
|
33
|
+
}
|
|
34
|
+
export function removeColony(id) {
|
|
35
|
+
if (!colonies.remove(id))
|
|
36
|
+
throw notFound('Colony not found');
|
|
37
|
+
notifyAgentsChanged();
|
|
38
|
+
}
|