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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { agents, db } from './db.js';
|
|
3
|
+
import { adapterFor } from './connections/manager.js';
|
|
4
|
+
import { deliverScheduled } from './connections/router.js';
|
|
5
|
+
import { connections, threads } from './connections/store.js';
|
|
6
|
+
import { liveOrigin, liveSource, sendTurn } from './runtime.js';
|
|
7
|
+
import { DomainError } from './errors.js';
|
|
8
|
+
/**
|
|
9
|
+
* Wake-ups. An agent only runs while it handles a message, so "I'll tell you when it finishes" cannot be kept by an agent that just
|
|
10
|
+
* says it: it has to ask hive-am to wake it. `wake_me` stores the request; when it is due, the agent receives a message and answers.
|
|
11
|
+
* Asked from a chat (Telegram, Slack…) the wake-up comes back to that same thread; asked from the hive-am web chat it comes back as a
|
|
12
|
+
* message of the agent's own conversation.
|
|
13
|
+
*/
|
|
14
|
+
export class WakeError extends DomainError {
|
|
15
|
+
}
|
|
16
|
+
export const WAKE_MIN_MINUTES = 1, WAKE_MAX_MINUTES = 240, WAKE_MAX_PENDING = 5;
|
|
17
|
+
const WAKE_MS = Number(process.env.HIVE_AM_WAKE_MS_PER_MINUTE) || 60_000; // tests shrink a "minute"
|
|
18
|
+
const RETRY_MS = Number(process.env.HIVE_AM_WAKE_RETRY_MS) || 5000; // wait before trying a delivery again
|
|
19
|
+
const TOO_LATE_MS = 2 * 3_600_000; // after a long outage, an old reminder is no longer useful
|
|
20
|
+
const rows = {
|
|
21
|
+
get: (id) => db.prepare('SELECT * FROM agent_wakeups WHERE id=?').get(id),
|
|
22
|
+
all: () => db.prepare('SELECT * FROM agent_wakeups ORDER BY due_at').all(),
|
|
23
|
+
remove: (id) => db.prepare('DELETE FROM agent_wakeups WHERE id=?').run(id).changes > 0,
|
|
24
|
+
pending: (agentId, threadId) => (threadId
|
|
25
|
+
? db.prepare('SELECT COUNT(*) AS n FROM agent_wakeups WHERE thread_id=?').get(threadId)
|
|
26
|
+
: db.prepare('SELECT COUNT(*) AS n FROM agent_wakeups WHERE agent_id=? AND thread_id IS NULL').get(agentId)),
|
|
27
|
+
same: (agentId, threadId, note) => (threadId
|
|
28
|
+
? db.prepare('SELECT * FROM agent_wakeups WHERE thread_id=? AND lower(note)=lower(?)').get(threadId, note)
|
|
29
|
+
: db.prepare('SELECT * FROM agent_wakeups WHERE agent_id=? AND thread_id IS NULL AND lower(note)=lower(?)').get(agentId, note)),
|
|
30
|
+
add(w) {
|
|
31
|
+
const row = { ...w, id: randomUUID(), created_at: Date.now() };
|
|
32
|
+
db.prepare('INSERT INTO agent_wakeups (id,agent_id,connection_id,thread_id,due_at,note,created_at,req_id,req_name) VALUES (?,?,?,?,?,?,?,?,?)').run(row.id, row.agent_id, row.connection_id, row.thread_id, row.due_at, row.note, row.created_at, row.req_id, row.req_name);
|
|
33
|
+
return row;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const wakeText = (w, replyTool) => `Scheduled wake-up (you set it at ${hhmm(w.created_at)}${w.req_name ? `, asked by ${w.req_name}` : ''}). Your note: ${w.note}\n\n${w.req_name ? `hive-am mentions ${w.req_name} at the start of your first reply, so do not mention them yourself. ` : ''}Check now what you were waiting for and tell the person${replyTool ? ` with ${replyTool}` : ''}: done, still running (then schedule another wake-up with wake_me) or failed. Give the actual result, not just that you woke up.`;
|
|
37
|
+
const hhmm = (t) => new Date(t).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
38
|
+
const timers = new Map();
|
|
39
|
+
function arm(w, tries = 0) {
|
|
40
|
+
const t = setTimeout(() => { void fire(w.id, tries); }, Math.max(0, w.due_at - Date.now()));
|
|
41
|
+
t.unref?.();
|
|
42
|
+
timers.set(w.id, t);
|
|
43
|
+
}
|
|
44
|
+
let override = null;
|
|
45
|
+
/** Tests replace the delivery to watch what would be sent without running a model. */
|
|
46
|
+
export const setDeliveryForTests = (fn) => { override = fn; };
|
|
47
|
+
/**
|
|
48
|
+
* Hands a message to the agent in the place where it was asked: the chat thread (where the person who asked is mentioned) or the web conversation.
|
|
49
|
+
* `retry` means the platform is not connected yet (right after a start); `dropped` that there is nobody to tell (thread gone, connection off or
|
|
50
|
+
* reassigned, muted on purpose). `beforeSend` runs once the message is certain to go, so the caller can forget the request before the turn starts.
|
|
51
|
+
*/
|
|
52
|
+
export const deliverTo = async (to, threadText, webText, beforeSend) => {
|
|
53
|
+
if (override)
|
|
54
|
+
return override(to, threadText, webText, beforeSend);
|
|
55
|
+
const agent = agents.get(to.agent_id);
|
|
56
|
+
if (!agent)
|
|
57
|
+
return 'dropped';
|
|
58
|
+
if (to.thread_id) {
|
|
59
|
+
const conn = connections.get(to.connection_id ?? ''), thread = threads.get(to.thread_id);
|
|
60
|
+
if (!conn || !conn.enabled || !thread)
|
|
61
|
+
return 'dropped';
|
|
62
|
+
const adapter = adapterFor(conn.id);
|
|
63
|
+
if (!adapter)
|
|
64
|
+
return 'retry';
|
|
65
|
+
beforeSend?.();
|
|
66
|
+
if (thread.muted || conn.agent_id !== to.agent_id)
|
|
67
|
+
return 'dropped';
|
|
68
|
+
await deliverScheduled(conn, adapter, thread, threadText, to.req_id ? { id: to.req_id, name: to.req_name || 'you' } : undefined);
|
|
69
|
+
return 'sent';
|
|
70
|
+
}
|
|
71
|
+
beforeSend?.();
|
|
72
|
+
await sendTurn(agent.id, webText, 'user');
|
|
73
|
+
return 'sent';
|
|
74
|
+
};
|
|
75
|
+
async function fire(id, tries) {
|
|
76
|
+
timers.delete(id);
|
|
77
|
+
const w = rows.get(id);
|
|
78
|
+
if (!w)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const out = await deliverTo(w, wakeText(w, 'channel_reply'), `🔔 ${wakeText(w)}`, () => { rows.remove(id); });
|
|
82
|
+
if (out === 'retry') {
|
|
83
|
+
if (tries < 12) {
|
|
84
|
+
w.due_at = Date.now() + RETRY_MS;
|
|
85
|
+
arm(w, tries + 1);
|
|
86
|
+
}
|
|
87
|
+
else
|
|
88
|
+
rows.remove(id);
|
|
89
|
+
}
|
|
90
|
+
else if (out === 'dropped')
|
|
91
|
+
rows.remove(id);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
console.warn(`[wake] wake-up of agent ${agents.get(w.agent_id)?.name ?? w.agent_id} failed: ${e instanceof Error ? e.message : e}`);
|
|
95
|
+
// A failure before the message was handed over (the platform erred) must not strand the row: with no timer it would count as pending until the next restart. Try again a few times, then give up.
|
|
96
|
+
if (rows.get(id)) {
|
|
97
|
+
if (tries < 12) {
|
|
98
|
+
w.due_at = Date.now() + RETRY_MS;
|
|
99
|
+
arm(w, tries + 1);
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
rows.remove(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Re-arms the wake-ups saved before a restart (called once at startup). */
|
|
107
|
+
export function armWakeups() {
|
|
108
|
+
for (const w of rows.all()) {
|
|
109
|
+
if (Date.now() - w.due_at > TOO_LATE_MS)
|
|
110
|
+
rows.remove(w.id);
|
|
111
|
+
else
|
|
112
|
+
arm(w);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** `wake_me`: the agent asks to be woken after some minutes. */
|
|
116
|
+
export function scheduleWake(agentId, minutes, note) {
|
|
117
|
+
if (!agents.get(agentId))
|
|
118
|
+
throw new WakeError('Unknown agent');
|
|
119
|
+
if (liveSource(agentId) === 'dispatch')
|
|
120
|
+
throw new WakeError('Wake-ups are not available while you work on a task delegated by an orchestrator: finish it and report what is left.');
|
|
121
|
+
const mins = Number(minutes);
|
|
122
|
+
if (!(mins >= WAKE_MIN_MINUTES && mins <= WAKE_MAX_MINUTES))
|
|
123
|
+
throw new WakeError(`minutes must be between ${WAKE_MIN_MINUTES} and ${WAKE_MAX_MINUTES}.`);
|
|
124
|
+
const text = String(note ?? '').trim();
|
|
125
|
+
if (!text)
|
|
126
|
+
throw new WakeError('Say in the note what to check when you wake up.');
|
|
127
|
+
if (text.length > 400)
|
|
128
|
+
throw new WakeError('The note is too long (400 characters at most).');
|
|
129
|
+
const origin = liveOrigin(agentId);
|
|
130
|
+
const thread = origin ? threads.get(origin.threadId) : undefined;
|
|
131
|
+
if (origin && !thread)
|
|
132
|
+
throw new WakeError('The thread no longer exists.');
|
|
133
|
+
// The same request twice (a model that retried after a hiccup) is one wake-up, not two.
|
|
134
|
+
const dup = rows.same(agentId, thread?.id ?? null, text);
|
|
135
|
+
if (dup)
|
|
136
|
+
return { at: hhmm(dup.due_at), minutes: Math.max(1, Math.round((dup.due_at - Date.now()) / WAKE_MS)), where: thread ? 'thread' : 'chat' };
|
|
137
|
+
if (rows.pending(agentId, thread?.id ?? null).n >= WAKE_MAX_PENDING)
|
|
138
|
+
throw new WakeError(`There are already ${WAKE_MAX_PENDING} wake-ups pending ${thread ? 'in this thread' : 'for you'}. Wait for one to fire.`);
|
|
139
|
+
const w = rows.add({ agent_id: agentId, connection_id: origin?.connectionId ?? null, thread_id: thread?.id ?? null, due_at: Date.now() + mins * WAKE_MS, note: text, req_id: origin?.group && origin.userId ? origin.userId : null, req_name: origin?.group && origin.userId ? origin.userName : null });
|
|
140
|
+
arm(w);
|
|
141
|
+
return { at: hhmm(w.due_at), minutes: mins, where: thread ? 'thread' : 'chat' };
|
|
142
|
+
}
|
|
143
|
+
export const pendingWakeups = () => rows.all();
|
|
144
|
+
/** For the interface: pending one-shot wake-ups, and a way to cancel them. */
|
|
145
|
+
export const listWakeups = () => rows.all();
|
|
146
|
+
export const removeWakeup = (id) => { const t = timers.get(id); if (t) {
|
|
147
|
+
clearTimeout(t);
|
|
148
|
+
timers.delete(id);
|
|
149
|
+
} return rows.remove(id); };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { agents, db, resolved } from './db.js';
|
|
7
|
+
import { liveOrigin, liveSource } from './runtime.js';
|
|
8
|
+
import { adapterFor } from './connections/manager.js';
|
|
9
|
+
import { connections, threadMessages, threads } from './connections/store.js';
|
|
10
|
+
import { deliverTo } from './wake.js';
|
|
11
|
+
import { DomainError } from './errors.js';
|
|
12
|
+
/**
|
|
13
|
+
* "Tell me the moment it finishes." The agent starts a long command in the background itself (`nohup … > log 2>&1 &`) and asks hive-am to
|
|
14
|
+
* watch its process: when it ends (or a marker file appears, or the waiting time runs out) the agent is woken in the same place where it
|
|
15
|
+
* was asked, to read the result and report. hive-am only checks that a process is alive; it never runs anything. A job on another machine
|
|
16
|
+
* works too when it is started through a local `ssh`, which lives as long as the remote command does.
|
|
17
|
+
*/
|
|
18
|
+
export class WatchError extends DomainError {
|
|
19
|
+
}
|
|
20
|
+
const POLL_MS = Number(process.env.HIVE_AM_WATCH_POLL_MS) || 1500;
|
|
21
|
+
const MS_PER_MINUTE = Number(process.env.HIVE_AM_WAKE_MS_PER_MINUTE) || 60_000;
|
|
22
|
+
export const WATCH_DEFAULT_MINUTES = 120, WATCH_MAX_MINUTES = 240, WATCH_MAX_PENDING = 5;
|
|
23
|
+
const rows = {
|
|
24
|
+
all: () => db.prepare('SELECT * FROM agent_watches ORDER BY started_at').all(),
|
|
25
|
+
forAgent: (agentId) => db.prepare('SELECT * FROM agent_watches WHERE agent_id=?').all(agentId),
|
|
26
|
+
remove: (id) => db.prepare('DELETE FROM agent_watches WHERE id=?').run(id).changes > 0,
|
|
27
|
+
add(w) {
|
|
28
|
+
const row = { ...w, id: randomUUID() };
|
|
29
|
+
db.prepare('INSERT INTO agent_watches (id,agent_id,connection_id,thread_id,pid,pid_stamp,file,log,note,started_at,deadline,req_id,req_name) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)')
|
|
30
|
+
.run(row.id, row.agent_id, row.connection_id, row.thread_id, row.pid, row.pid_stamp, row.file, row.log, row.note, row.started_at, row.deadline, row.req_id, row.req_name);
|
|
31
|
+
return row;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
/** An identifier of "this very process" (its start time), so that a PID the system reuses for something else is not mistaken for the job. null = not running. */
|
|
35
|
+
export function processStamp(pid) {
|
|
36
|
+
try {
|
|
37
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
38
|
+
const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
39
|
+
return rest[0] === 'Z' || rest[0] === 'X' ? null : rest[19] ?? null; // a finished job nobody has collected yet (zombie) is finished
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
if (e.code !== 'ENOENT' || existsSync('/proc/self'))
|
|
43
|
+
return null; // Linux: no such process
|
|
44
|
+
}
|
|
45
|
+
try { // macOS and others
|
|
46
|
+
const out = execFileSync('ps', ['-o', 'stat=,lstart=', '-p', String(pid)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
47
|
+
if (!out || out.startsWith('Z'))
|
|
48
|
+
return null;
|
|
49
|
+
return out.replace(/^\S+\s+/, '');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const isAlive = (pid, stamp) => { const s = processStamp(pid); return s !== null && (stamp === null || s === stamp); };
|
|
56
|
+
/** How long it took, in plain words. */
|
|
57
|
+
const took = (since) => { const s = Math.max(1, Math.round((Date.now() - since) / 1000)); return s < 90 ? `${s} s` : s < 5400 ? `${Math.round(s / 60)} min` : `${(s / 3600).toFixed(1)} h`; };
|
|
58
|
+
const hhmm = (t) => new Date(t).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
59
|
+
const inside = (child, root) => child === root || child.startsWith(root.endsWith(sep) ? root : root + sep);
|
|
60
|
+
export function createWatch(agentId, input) {
|
|
61
|
+
const agent = agents.get(agentId);
|
|
62
|
+
if (!agent)
|
|
63
|
+
throw new WatchError('Unknown agent');
|
|
64
|
+
if (liveSource(agentId) === 'dispatch')
|
|
65
|
+
throw new WatchError('Waiting for a process is not available while you work on a task delegated by an orchestrator.');
|
|
66
|
+
const note = String(input.note ?? '').trim();
|
|
67
|
+
if (!note)
|
|
68
|
+
throw new WatchError('Say in the note what to check and report when it finishes.');
|
|
69
|
+
if (note.length > 400)
|
|
70
|
+
throw new WatchError('The note is too long (400 characters at most).');
|
|
71
|
+
const pid = Number(input.pid);
|
|
72
|
+
if (!Number.isInteger(pid) || pid < 2)
|
|
73
|
+
throw new WatchError('pid must be the number of the process to wait for (for example the output of `echo $!` right after starting it in the background).');
|
|
74
|
+
if (pid === process.pid || pid === process.ppid)
|
|
75
|
+
throw new WatchError('That is not the process you started.');
|
|
76
|
+
const stamp = processStamp(pid);
|
|
77
|
+
if (stamp === null)
|
|
78
|
+
throw new WatchError(`Process ${pid} is not running (it may have finished already): look at its result now instead of waiting.`);
|
|
79
|
+
let file = null;
|
|
80
|
+
if (input.file !== undefined && input.file !== null && String(input.file).trim() !== '') {
|
|
81
|
+
const cwd = resolved(agent).cwd;
|
|
82
|
+
const p = resolve(isAbsolute(String(input.file)) ? String(input.file) : join(cwd, String(input.file)));
|
|
83
|
+
if (![cwd, tmpdir(), homedir()].filter(Boolean).some((r) => inside(p, resolve(r))))
|
|
84
|
+
throw new WatchError('The marker file must be inside your working folder, your home folder or the temp folder.');
|
|
85
|
+
file = p;
|
|
86
|
+
}
|
|
87
|
+
const log = input.log === undefined || input.log === null || String(input.log).trim() === '' ? null : String(input.log).trim().slice(0, 300);
|
|
88
|
+
const minutes = input.max_minutes === undefined || input.max_minutes === null ? WATCH_DEFAULT_MINUTES : Number(input.max_minutes);
|
|
89
|
+
if (!(minutes >= 1 && minutes <= WATCH_MAX_MINUTES))
|
|
90
|
+
throw new WatchError(`max_minutes must be between 1 and ${WATCH_MAX_MINUTES}.`);
|
|
91
|
+
const origin = liveOrigin(agentId), thread = origin ? threads.get(origin.threadId) : undefined;
|
|
92
|
+
if (origin && !thread)
|
|
93
|
+
throw new WatchError('The thread no longer exists.');
|
|
94
|
+
const mine = rows.forAgent(agentId);
|
|
95
|
+
const same = mine.find((w) => w.pid === pid && w.pid_stamp === stamp);
|
|
96
|
+
if (same)
|
|
97
|
+
return { id: same.id, until: hhmm(same.deadline), where: same.thread_id ? 'thread' : 'chat', reused: true };
|
|
98
|
+
if (mine.length >= WATCH_MAX_PENDING)
|
|
99
|
+
throw new WatchError(`You are already waiting for ${WATCH_MAX_PENDING} processes. Wait for one to finish.`);
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const w = rows.add({
|
|
102
|
+
agent_id: agentId, connection_id: origin?.connectionId ?? null, thread_id: thread?.id ?? null, pid, pid_stamp: stamp, file, log, note, started_at: now, deadline: now + minutes * MS_PER_MINUTE,
|
|
103
|
+
req_id: origin?.group && origin.userId ? origin.userId : null, req_name: origin?.group && origin.userId ? origin.userName : null,
|
|
104
|
+
});
|
|
105
|
+
poll.ensure();
|
|
106
|
+
return { id: w.id, until: hhmm(w.deadline), where: thread ? 'thread' : 'chat', reused: false };
|
|
107
|
+
}
|
|
108
|
+
function text(w, reason, replyTool) {
|
|
109
|
+
const dur = took(w.started_at);
|
|
110
|
+
const who = w.req_name ? `hive-am mentions ${w.req_name} at the start of your first reply, so do not mention them yourself. ` : '';
|
|
111
|
+
const how = `tell the person${replyTool ? ` with ${replyTool}` : ''}`;
|
|
112
|
+
const logLine = w.log ? ` Its output is in ${w.log}.` : '';
|
|
113
|
+
if (reason === 'timeout') {
|
|
114
|
+
return `You were waiting for the process ${w.pid}${w.req_name ? ` (asked by ${w.req_name})` : ''}, and after ${dur} it is still running.${logLine} Your note: ${w.note}\n\n${who}Look at how it is going (its log) and ${how}. If it is worth waiting longer, call wake_when_done again; otherwise say what you saw.`;
|
|
115
|
+
}
|
|
116
|
+
const what = reason === 'file' ? `The marker file ${w.file} you were waiting for appeared (${dur} after you asked).` : `The process ${w.pid} you were waiting for has ended (${dur} after you asked, at ${hhmm(Date.now())}).`;
|
|
117
|
+
return `${what}${logLine}${w.req_name ? ` Asked by ${w.req_name}.` : ''} Your note: ${w.note}\n\n${who}Look at the result now (read the output or check the outcome) and ${how}: whether it succeeded or failed, with the evidence (the lines that show it). Never paste secrets or a whole log.`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The instant the process ends hive-am tells the thread itself, in one line (the agent still has to read the result, which takes it several
|
|
121
|
+
* seconds): "it finished" arrives right away and the agent's report follows. Nothing is sent when the thread is muted or the platform is down.
|
|
122
|
+
*/
|
|
123
|
+
async function noticeNow(w, reason) {
|
|
124
|
+
if (!w.thread_id || reason === 'timeout')
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
const conn = connections.get(w.connection_id ?? ''), thread = threads.get(w.thread_id), adapter = conn && adapterFor(conn.id);
|
|
128
|
+
if (!conn || !conn.enabled || !thread || thread.muted || conn.agent_id !== w.agent_id || !adapter)
|
|
129
|
+
return;
|
|
130
|
+
const dur = took(w.started_at);
|
|
131
|
+
const who = w.req_id && adapter.mention ? `${adapter.mention({ id: w.req_id, name: w.req_name || 'you' })} ` : '';
|
|
132
|
+
const es = conn.config.lang !== 'en';
|
|
133
|
+
const body = reason === 'file'
|
|
134
|
+
? (es ? `Apareció el archivo que esperabas (${dur}). Reviso el resultado…` : `The file you were waiting for appeared (${dur}). Checking the result…`)
|
|
135
|
+
: (es ? `Terminó el proceso que esperabas (PID ${w.pid}, ${dur}). Reviso el resultado…` : `The process you were waiting for (PID ${w.pid}) has finished (${dur}). Checking the result…`);
|
|
136
|
+
const msg = `🔔 ${who}${body}`;
|
|
137
|
+
const { externalId } = await adapter.send(thread.target, msg);
|
|
138
|
+
threadMessages.record(thread.id, 'out', externalId, { name: agents.get(w.agent_id)?.name }, msg);
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
console.warn(`[watch] the immediate notice failed: ${e instanceof Error ? e.message : e}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const firing = new Set();
|
|
145
|
+
async function fire(w, reason) {
|
|
146
|
+
if (firing.has(w.id))
|
|
147
|
+
return;
|
|
148
|
+
firing.add(w.id);
|
|
149
|
+
try {
|
|
150
|
+
await noticeNow(w, reason);
|
|
151
|
+
const out = await deliverTo(w, text(w, reason, 'channel_reply'), `🔔 ${text(w, reason)}`, () => { rows.remove(w.id); });
|
|
152
|
+
if (out === 'dropped')
|
|
153
|
+
rows.remove(w.id);
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
console.warn(`[watch] telling agent ${agents.get(w.agent_id)?.name ?? w.agent_id} failed: ${e instanceof Error ? e.message : e}`);
|
|
157
|
+
rows.remove(w.id);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
firing.delete(w.id);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const poll = {
|
|
164
|
+
timer: null,
|
|
165
|
+
ensure() {
|
|
166
|
+
if (this.timer)
|
|
167
|
+
return;
|
|
168
|
+
this.timer = setInterval(() => this.tick(), POLL_MS);
|
|
169
|
+
this.timer.unref?.();
|
|
170
|
+
},
|
|
171
|
+
tick() {
|
|
172
|
+
const all = rows.all();
|
|
173
|
+
if (all.length === 0 && this.timer) {
|
|
174
|
+
clearInterval(this.timer);
|
|
175
|
+
this.timer = null;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
for (const w of all) {
|
|
180
|
+
if (firing.has(w.id))
|
|
181
|
+
continue;
|
|
182
|
+
if (w.file && existsSync(w.file))
|
|
183
|
+
void fire(w, 'file');
|
|
184
|
+
else if (!isAlive(w.pid, w.pid_stamp))
|
|
185
|
+
void fire(w, 'ended');
|
|
186
|
+
else if (now >= w.deadline)
|
|
187
|
+
void fire(w, 'timeout');
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
/** Starts watching what was saved before a restart (the jobs themselves keep running without hive-am). */
|
|
192
|
+
export function armWatches() { if (rows.all().length)
|
|
193
|
+
poll.ensure(); }
|
|
194
|
+
export const pendingWatches = () => rows.all();
|
|
195
|
+
export function removeWatch(id) { return rows.remove(id); }
|
|
196
|
+
export function cancelWatchFor(agentId, id) {
|
|
197
|
+
const w = rows.forAgent(agentId).find((x) => x.id === id);
|
|
198
|
+
if (!w)
|
|
199
|
+
throw new WatchError('You are not waiting for anything with that id.');
|
|
200
|
+
return rows.remove(id);
|
|
201
|
+
}
|
package/server/mcp/dispatch.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Minimal MCP stdio server for hive-am agents. Tool groups come from HIVE_CAPS:
|
|
3
3
|
// dispatch → list_agents, dispatch (orchestrators with a team)
|
|
4
|
-
// channel → channel_reply (agents linked to Telegram/Slack)
|
|
4
|
+
// channel → channel_reply, channel_send_file, channel_react, channel_mute (agents linked to Telegram/Slack)
|
|
5
|
+
// wake → wake_me, wake_when_done, schedule_create, schedule_list, schedule_cancel (agents with the Wake-ups skill: to be woken later in the same place)
|
|
6
|
+
// objects → object_list, object_logs, object_action (agents with the Colony objects skill: the servers and containers of their colony)
|
|
5
7
|
// It only talks to the hive-am HTTP API; all rules (assignments, queueing) live there.
|
|
6
8
|
import { createInterface } from 'node:readline';
|
|
7
9
|
|
|
@@ -100,6 +102,17 @@ const allTools = [
|
|
|
100
102
|
additionalProperties: false,
|
|
101
103
|
},
|
|
102
104
|
},
|
|
105
|
+
{
|
|
106
|
+
cap: 'channel',
|
|
107
|
+
name: 'channel_react',
|
|
108
|
+
description: 'React with ONE emoji to the message you are handling (a thumbs-up to say "got it", a heart to say thanks). It is lighter than a reply and counts as your answer when nothing else needs saying. Each platform allows only some emoji (on Telegram a fixed list: 👍 👎 ❤ 🔥 👏 🎉 🙏 👌 👀 💯 🤔 😁 😢 ✍ 🫡 …); an emoji that is not allowed is refused and the error lists the ones you can use. Follow the "Chat channels" skill for when to react.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: { emoji: { type: 'string', description: 'A single emoji, e.g. 👍.' } },
|
|
112
|
+
required: ['emoji'],
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
103
116
|
{
|
|
104
117
|
cap: 'channel',
|
|
105
118
|
name: 'channel_mute',
|
|
@@ -111,6 +124,99 @@ const allTools = [
|
|
|
111
124
|
additionalProperties: false,
|
|
112
125
|
},
|
|
113
126
|
},
|
|
127
|
+
{
|
|
128
|
+
cap: 'objects',
|
|
129
|
+
name: 'object_list',
|
|
130
|
+
description: 'List the objects of your colony (servers and Docker containers that hive-am keeps running): name, kind, state (running, starting, stopped, error, unknown) and the reason when there is one. Use the exact names in the other object tools.',
|
|
131
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
cap: 'objects',
|
|
135
|
+
name: 'object_logs',
|
|
136
|
+
description: 'Read the latest output of an object of your colony (a server or a container). Read it before restarting something that fails. Logs can contain secrets: never repeat them in a chat.',
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
name: { type: 'string', description: 'The exact name of the object, as object_list shows it.' },
|
|
141
|
+
tail: { type: 'number', description: 'How many of the last lines to read (default 80, at most 500).' },
|
|
142
|
+
},
|
|
143
|
+
required: ['name'],
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
cap: 'objects',
|
|
149
|
+
name: 'object_action',
|
|
150
|
+
description: 'Start, stop or restart an object of your colony. Not available in read-only mode. It answers with the state it ended in: check it with object_list a moment later, since a server can take a while to come up. Starting, stopping and restarting interrupt whoever uses it: do not do it for something that runs fine unless you were asked.',
|
|
151
|
+
inputSchema: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
name: { type: 'string', description: 'The exact name of the object.' },
|
|
155
|
+
action: { type: 'string', enum: ['start', 'stop', 'restart'] },
|
|
156
|
+
},
|
|
157
|
+
required: ['name', 'action'],
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
cap: 'wake',
|
|
163
|
+
name: 'wake_me',
|
|
164
|
+
description: 'Schedule yourself to be woken up after some minutes, in the same place where you were asked (the same chat thread, or the same web conversation). You can only write while you are handling a message, so this is the ONLY way to tell the person something later (for example when a long deploy or job should be finished). When the time comes you receive a message with your note, check what you were waiting for and answer. Never promise "I will let you know" without calling this first.',
|
|
165
|
+
inputSchema: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
minutes: { type: 'number', description: 'Minutes from now, between 1 and 240.' },
|
|
169
|
+
note: { type: 'string', description: 'What to check when you wake up and what to tell the person (for example: check ~/job.log, report if it finished or failed). Up to 400 characters.' },
|
|
170
|
+
},
|
|
171
|
+
required: ['minutes', 'note'],
|
|
172
|
+
additionalProperties: false,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
cap: 'wake',
|
|
177
|
+
name: 'wake_when_done',
|
|
178
|
+
description: 'Be woken up THE MOMENT a command you started in the background finishes, in the same place where you were asked (the same chat thread, or the same web conversation), to read the result and tell the person. Start the command yourself in the background with its output in a log (for example `nohup ./deploy.sh > ~/deploy.log 2>&1 & echo $!`; for a job on another machine run the ssh itself in the background: `nohup ssh host \'./job.sh\' > ~/job.log 2>&1 & echo $!`, because the local ssh lives as long as the remote command), then call this with that pid. hive-am only watches the process; it never runs anything. If the process is already gone, check the result now instead. You can also give a marker file that the job creates when it ends. If it is still running after max_minutes you are woken anyway. Never promise "I will tell you when it finishes" without calling this first.',
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
pid: { type: 'number', description: 'Process id of the background command (the output of `echo $!` right after starting it).' },
|
|
183
|
+
log: { type: 'string', description: 'Path of the file where its output goes; you will be told it when you wake up.' },
|
|
184
|
+
file: { type: 'string', description: 'Optional: a marker file that the job creates when it ends; waking up also happens when it appears (must be in your working folder, home or temp).' },
|
|
185
|
+
note: { type: 'string', description: 'What to check and report when it finishes (up to 400 characters).' },
|
|
186
|
+
max_minutes: { type: 'number', description: 'How long to wait at most, 1 to 240 (default 120).' },
|
|
187
|
+
},
|
|
188
|
+
required: ['pid', 'note'],
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
cap: 'wake',
|
|
194
|
+
name: 'schedule_create',
|
|
195
|
+
description: 'Create a RECURRING schedule: at each run you are woken up in the same place where you were asked (the same chat thread, or the same web conversation), with your note, and you do what it says and tell the person. Use "cron" (5 fields: minute hour day-of-month month day-of-week, e.g. "0 9 * * MON-FRI") or "every_minutes". The minimum gap between runs is 15 minutes. For something that happens once, use wake_me instead. The answer gives the next run times: tell them to the person.',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
cron: { type: 'string', description: 'Cron expression, e.g. "30 8 * * MON-FRI" (weekdays 08:30) or "0 */6 * * *" (every 6 hours).' },
|
|
200
|
+
every_minutes: { type: 'number', description: 'Alternative to cron: a fixed interval in minutes (15 or more).' },
|
|
201
|
+
timezone: { type: 'string', description: 'IANA time zone for cron, e.g. "America/Mexico_City". Defaults to the server time zone.' },
|
|
202
|
+
note: { type: 'string', description: 'What to do at each run and what to report (up to 400 characters).' },
|
|
203
|
+
},
|
|
204
|
+
required: ['note'],
|
|
205
|
+
additionalProperties: false,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
cap: 'wake',
|
|
210
|
+
name: 'schedule_list',
|
|
211
|
+
description: 'List your recurring schedules (id, when, note, next run, last result).',
|
|
212
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
cap: 'wake',
|
|
216
|
+
name: 'schedule_cancel',
|
|
217
|
+
description: 'Cancel one of your recurring schedules by id (see schedule_list).',
|
|
218
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'The schedule id.' } }, required: ['id'], additionalProperties: false },
|
|
219
|
+
},
|
|
114
220
|
];
|
|
115
221
|
const tools = allTools.filter((t) => CAPS.has(t.cap)).map(({ cap, ...t }) => t);
|
|
116
222
|
|
|
@@ -169,6 +275,15 @@ async function call(name, args) {
|
|
|
169
275
|
if (!r.ok) throw new Error(j.error ?? `channel_send_file failed (${r.status})`);
|
|
170
276
|
return `Sent ${j.name} (${j.kind}).`;
|
|
171
277
|
}
|
|
278
|
+
if (name === 'channel_react') {
|
|
279
|
+
const r = await fetch(`${API}/api/channel/react`, {
|
|
280
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
281
|
+
body: JSON.stringify({ from: FROM, emoji: args.emoji }),
|
|
282
|
+
});
|
|
283
|
+
const j = await r.json();
|
|
284
|
+
if (!r.ok) throw new Error(j.error ?? `channel_react failed (${r.status})`);
|
|
285
|
+
return `Reacted with ${j.emoji}.`;
|
|
286
|
+
}
|
|
172
287
|
if (name === 'channel_mute') {
|
|
173
288
|
const r = await fetch(`${API}/api/channel/mute`, {
|
|
174
289
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
@@ -178,6 +293,59 @@ async function call(name, args) {
|
|
|
178
293
|
if (!r.ok) throw new Error(j.error ?? `channel_mute failed (${r.status})`);
|
|
179
294
|
return j.muted ? `Muted in this thread (${j.place}). You will only be woken when someone mentions you or replies to you.` : `Unmuted in this thread (${j.place}).`;
|
|
180
295
|
}
|
|
296
|
+
if (name === 'object_list' || name === 'object_logs' || name === 'object_action') {
|
|
297
|
+
const r = await fetch(`${API}/api/agent-objects/${name.slice('object_'.length)}`, {
|
|
298
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
299
|
+
body: JSON.stringify({ from: FROM, ...args }),
|
|
300
|
+
});
|
|
301
|
+
const j = await r.json();
|
|
302
|
+
if (!r.ok) throw new Error(j.error ?? `${name} failed (${r.status})`);
|
|
303
|
+
if (name === 'object_list') {
|
|
304
|
+
if (!j.objects.length) return 'Your colony has no objects.';
|
|
305
|
+
return j.objects.map((o) => `${o.name} · ${o.kind} · ${o.status}${o.detail ? ` (${o.detail})` : ''}`).join('\n');
|
|
306
|
+
}
|
|
307
|
+
if (name === 'object_logs') return j.text.trim() ? `Last lines of ${j.name}:\n${j.text}` : `${j.name} has not printed anything.`;
|
|
308
|
+
return `${j.name}: ${j.status}${j.detail ? ` (${j.detail})` : ''}. Check again with object_list in a moment.`;
|
|
309
|
+
}
|
|
310
|
+
if (name === 'wake_me') {
|
|
311
|
+
const r = await fetch(`${API}/api/wake`, {
|
|
312
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
313
|
+
body: JSON.stringify({ from: FROM, minutes: args.minutes, note: args.note }),
|
|
314
|
+
});
|
|
315
|
+
const j = await r.json();
|
|
316
|
+
if (!r.ok) throw new Error(j.error ?? `wake_me failed (${r.status})`);
|
|
317
|
+
return `Wake-up scheduled for ${j.at} (in ${j.minutes} min), ${j.where === 'thread' ? 'in this same thread' : 'in this same conversation'}. Tell the person that exact time.`;
|
|
318
|
+
}
|
|
319
|
+
if (name === 'wake_when_done') {
|
|
320
|
+
const r = await fetch(`${API}/api/wake-when-done`, {
|
|
321
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
322
|
+
body: JSON.stringify({ from: FROM, pid: args.pid, log: args.log, file: args.file, note: args.note, max_minutes: args.max_minutes }),
|
|
323
|
+
});
|
|
324
|
+
const j = await r.json();
|
|
325
|
+
if (!r.ok) throw new Error(j.error ?? `wake_when_done failed (${r.status})`);
|
|
326
|
+
return `${j.reused ? 'Already watching this process. ' : ''}Watching the process: you will be woken ${j.where === 'thread' ? 'in this same thread' : 'in this same conversation'} when it finishes (or at ${j.until} at the latest). Tell the person that, and that you will report the result then.`;
|
|
327
|
+
}
|
|
328
|
+
if (name === 'schedule_create') {
|
|
329
|
+
const r = await fetch(`${API}/api/schedules/create`, {
|
|
330
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
331
|
+
body: JSON.stringify({ from: FROM, cron: args.cron, every_minutes: args.every_minutes, timezone: args.timezone, note: args.note }),
|
|
332
|
+
});
|
|
333
|
+
const j = await r.json();
|
|
334
|
+
if (!r.ok) throw new Error(j.error ?? `schedule_create failed (${r.status})`);
|
|
335
|
+
return `Schedule created (id ${j.id}): ${j.schedule}, ${j.where === 'thread' ? 'in this same thread' : 'in this same conversation'}. Next runs: ${j.next.join(' · ')}. Tell the person.`;
|
|
336
|
+
}
|
|
337
|
+
if (name === 'schedule_list') {
|
|
338
|
+
const r = await fetch(`${API}/api/schedules/list`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ from: FROM }) });
|
|
339
|
+
const j = await r.json();
|
|
340
|
+
if (!r.ok) throw new Error(j.error ?? `schedule_list failed (${r.status})`);
|
|
341
|
+
return j.schedules.length ? j.schedules.map((s) => `- ${s.id}: ${s.schedule}${s.enabled ? '' : ' [paused]'} · next ${s.next ?? '-'} · last ${s.last ?? 'never'} · ${s.note}`).join('\n') : 'You have no recurring schedules.';
|
|
342
|
+
}
|
|
343
|
+
if (name === 'schedule_cancel') {
|
|
344
|
+
const r = await fetch(`${API}/api/schedules/cancel`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ from: FROM, id: args.id }) });
|
|
345
|
+
const j = await r.json();
|
|
346
|
+
if (!r.ok) throw new Error(j.error ?? `schedule_cancel failed (${r.status})`);
|
|
347
|
+
return 'Schedule cancelled.';
|
|
348
|
+
}
|
|
181
349
|
throw new Error(`Unknown tool ${name}`);
|
|
182
350
|
}
|
|
183
351
|
|
package/web/.next/BUILD_ID
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
p4S3i9kvO359GW91DQiHu
|