@hybridaione/hybridclaw 0.2.2 → 0.2.6
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/.github/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +19 -3
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +16 -8
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +563 -73
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +179 -24
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +48 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +29 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/src/health.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import http, { type IncomingMessage, type ServerResponse } from 'http';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
|
|
5
|
-
import {
|
|
4
|
+
import { runDiscordToolAction } from './channels/discord/runtime.js';
|
|
5
|
+
import type { DiscordToolActionRequest } from './channels/discord/tool-actions.js';
|
|
6
|
+
import {
|
|
7
|
+
GATEWAY_API_TOKEN,
|
|
8
|
+
HEALTH_HOST,
|
|
9
|
+
HEALTH_PORT,
|
|
10
|
+
WEB_API_TOKEN,
|
|
11
|
+
} from './config.js';
|
|
6
12
|
import {
|
|
13
|
+
type GatewayChatRequest,
|
|
14
|
+
type GatewayCommandRequest,
|
|
7
15
|
getGatewayHistory,
|
|
8
16
|
getGatewayStatus,
|
|
9
17
|
handleGatewayCommand,
|
|
10
18
|
handleGatewayMessage,
|
|
11
|
-
type GatewayCommandRequest,
|
|
12
|
-
type GatewayChatRequest,
|
|
13
19
|
} from './gateway-service.js';
|
|
14
|
-
import {
|
|
15
|
-
import { type DiscordToolActionRequest } from './channels/discord/tool-actions.js';
|
|
16
|
-
import { runDiscordToolAction } from './channels/discord/runtime.js';
|
|
17
|
-
import { type ToolProgressEvent } from './types.js';
|
|
20
|
+
import type { GatewayChatRequestBody } from './gateway-types.js';
|
|
18
21
|
import { logger } from './logger.js';
|
|
22
|
+
import type { ToolProgressEvent } from './types.js';
|
|
19
23
|
|
|
20
24
|
const SITE_DIR = path.resolve(process.cwd(), 'docs');
|
|
21
25
|
const MAX_REQUEST_BYTES = 1_000_000; // 1MB
|
|
@@ -40,7 +44,8 @@ function isLoopbackAddress(address: string | undefined): boolean {
|
|
|
40
44
|
|
|
41
45
|
function hasApiAuth(req: IncomingMessage): boolean {
|
|
42
46
|
const authHeader = req.headers.authorization || '';
|
|
43
|
-
const gatewayTokenMatch =
|
|
47
|
+
const gatewayTokenMatch =
|
|
48
|
+
Boolean(GATEWAY_API_TOKEN) && authHeader === `Bearer ${GATEWAY_API_TOKEN}`;
|
|
44
49
|
|
|
45
50
|
if (!WEB_API_TOKEN) {
|
|
46
51
|
return gatewayTokenMatch || isLoopbackAddress(req.socket.remoteAddress);
|
|
@@ -49,8 +54,14 @@ function hasApiAuth(req: IncomingMessage): boolean {
|
|
|
49
54
|
return gatewayTokenMatch;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
function sendJson(
|
|
53
|
-
res
|
|
57
|
+
function sendJson(
|
|
58
|
+
res: ServerResponse,
|
|
59
|
+
statusCode: number,
|
|
60
|
+
payload: unknown,
|
|
61
|
+
): void {
|
|
62
|
+
res.writeHead(statusCode, {
|
|
63
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
64
|
+
});
|
|
54
65
|
res.end(JSON.stringify(payload, null, 2));
|
|
55
66
|
}
|
|
56
67
|
|
|
@@ -81,12 +92,15 @@ function resolveSiteFile(pathname: string): string | null {
|
|
|
81
92
|
const normalized = path.normalize(cleanPath).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
82
93
|
const candidate = path.resolve(SITE_DIR, `.${normalized}`);
|
|
83
94
|
if (!candidate.startsWith(SITE_DIR)) return null;
|
|
84
|
-
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile())
|
|
95
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile())
|
|
96
|
+
return null;
|
|
85
97
|
return candidate;
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
function serveStatic(pathname: string, res: ServerResponse): boolean {
|
|
89
|
-
const filePath = resolveSiteFile(
|
|
101
|
+
const filePath = resolveSiteFile(
|
|
102
|
+
pathname === '/chat' ? '/chat.html' : pathname,
|
|
103
|
+
);
|
|
90
104
|
if (!filePath) return false;
|
|
91
105
|
const ext = path.extname(filePath).toLowerCase();
|
|
92
106
|
const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
@@ -95,8 +109,11 @@ function serveStatic(pathname: string, res: ServerResponse): boolean {
|
|
|
95
109
|
return true;
|
|
96
110
|
}
|
|
97
111
|
|
|
98
|
-
async function handleApiChat(
|
|
99
|
-
|
|
112
|
+
async function handleApiChat(
|
|
113
|
+
req: IncomingMessage,
|
|
114
|
+
res: ServerResponse,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const body = (await readJsonBody(req)) as Partial<ApiChatRequestBody>;
|
|
100
117
|
const wantsStream = body.stream === true;
|
|
101
118
|
|
|
102
119
|
const content = body.content?.trim();
|
|
@@ -177,7 +194,10 @@ async function handleApiChatStream(
|
|
|
177
194
|
error: errorMessage,
|
|
178
195
|
},
|
|
179
196
|
});
|
|
180
|
-
logger.error(
|
|
197
|
+
logger.error(
|
|
198
|
+
{ error, reqUrl: '/api/chat' },
|
|
199
|
+
'Gateway streaming chat failed',
|
|
200
|
+
);
|
|
181
201
|
} finally {
|
|
182
202
|
if (!res.writableEnded) {
|
|
183
203
|
res.end();
|
|
@@ -191,11 +211,18 @@ async function handleApiChatStream(
|
|
|
191
211
|
});
|
|
192
212
|
}
|
|
193
213
|
|
|
194
|
-
async function handleApiCommand(
|
|
195
|
-
|
|
196
|
-
|
|
214
|
+
async function handleApiCommand(
|
|
215
|
+
req: IncomingMessage,
|
|
216
|
+
res: ServerResponse,
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
const body = (await readJsonBody(req)) as Partial<GatewayCommandRequest>;
|
|
219
|
+
const args = Array.isArray(body.args)
|
|
220
|
+
? body.args.map((value) => String(value))
|
|
221
|
+
: [];
|
|
197
222
|
if (args.length === 0) {
|
|
198
|
-
sendJson(res, 400, {
|
|
223
|
+
sendJson(res, 400, {
|
|
224
|
+
error: 'Missing command. Provide non-empty `args` array.',
|
|
225
|
+
});
|
|
199
226
|
return;
|
|
200
227
|
}
|
|
201
228
|
|
|
@@ -209,12 +236,20 @@ async function handleApiCommand(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
209
236
|
sendJson(res, result.kind === 'error' ? 400 : 200, result);
|
|
210
237
|
}
|
|
211
238
|
|
|
212
|
-
async function handleApiDiscordAction(
|
|
213
|
-
|
|
239
|
+
async function handleApiDiscordAction(
|
|
240
|
+
req: IncomingMessage,
|
|
241
|
+
res: ServerResponse,
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
const body = (await readJsonBody(req)) as ApiDiscordActionRequestBody;
|
|
214
244
|
const action = typeof body.action === 'string' ? body.action.trim() : '';
|
|
215
|
-
if (
|
|
245
|
+
if (
|
|
246
|
+
action !== 'read' &&
|
|
247
|
+
action !== 'member-info' &&
|
|
248
|
+
action !== 'channel-info'
|
|
249
|
+
) {
|
|
216
250
|
sendJson(res, 400, {
|
|
217
|
-
error:
|
|
251
|
+
error:
|
|
252
|
+
'Invalid `action`. Allowed: "read", "member-info", "channel-info".',
|
|
218
253
|
});
|
|
219
254
|
return;
|
|
220
255
|
}
|
|
@@ -268,7 +303,9 @@ export function startHealthServer(): void {
|
|
|
268
303
|
|
|
269
304
|
if (pathname.startsWith('/api/')) {
|
|
270
305
|
if (!hasApiAuth(req)) {
|
|
271
|
-
sendJson(res, 401, {
|
|
306
|
+
sendJson(res, 401, {
|
|
307
|
+
error: 'Unauthorized. Set `Authorization: Bearer <WEB_API_TOKEN>`.',
|
|
308
|
+
});
|
|
272
309
|
return;
|
|
273
310
|
}
|
|
274
311
|
|
|
@@ -312,6 +349,9 @@ export function startHealthServer(): void {
|
|
|
312
349
|
});
|
|
313
350
|
|
|
314
351
|
server.listen(HEALTH_PORT, HEALTH_HOST, () => {
|
|
315
|
-
logger.info(
|
|
352
|
+
logger.info(
|
|
353
|
+
{ host: HEALTH_HOST, port: HEALTH_PORT },
|
|
354
|
+
'Gateway HTTP server started',
|
|
355
|
+
);
|
|
316
356
|
});
|
|
317
357
|
}
|
package/src/heartbeat.ts
CHANGED
|
@@ -2,17 +2,35 @@
|
|
|
2
2
|
* Heartbeat — periodic poll so the agent can proactively check tasks,
|
|
3
3
|
* maintain memory, and reach out when needed.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import { runAgent } from './agent.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
emitToolExecutionAuditEvents,
|
|
9
|
+
makeAuditRunId,
|
|
10
|
+
recordAuditEvent,
|
|
11
|
+
} from './audit-events.js';
|
|
12
|
+
import {
|
|
13
|
+
HEARTBEAT_CHANNEL,
|
|
14
|
+
HEARTBEAT_ENABLED,
|
|
15
|
+
HYBRIDAI_CHATBOT_ID,
|
|
16
|
+
HYBRIDAI_ENABLE_RAG,
|
|
17
|
+
HYBRIDAI_MODEL,
|
|
18
|
+
} from './config.js';
|
|
19
|
+
import { buildConversationContext } from './conversation.js';
|
|
20
|
+
import { getTasksForSession } from './db.js';
|
|
8
21
|
import { logger } from './logger.js';
|
|
9
|
-
import {
|
|
22
|
+
import { memoryService } from './memory-service.js';
|
|
23
|
+
import {
|
|
24
|
+
isWithinActiveHours,
|
|
25
|
+
proactiveWindowLabel,
|
|
26
|
+
} from './proactive-policy.js';
|
|
10
27
|
import { maybeCompactSession } from './session-maintenance.js';
|
|
11
28
|
import { appendSessionTranscript } from './session-transcripts.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
import { processSideEffects } from './side-effects.js';
|
|
30
|
+
import {
|
|
31
|
+
estimateTokenCountFromMessages,
|
|
32
|
+
estimateTokenCountFromText,
|
|
33
|
+
} from './token-efficiency.js';
|
|
16
34
|
|
|
17
35
|
const HEARTBEAT_PROMPT =
|
|
18
36
|
'[Heartbeat poll] Check HEARTBEAT.md for periodic tasks. If nothing needs attention, reply HEARTBEAT_OK.';
|
|
@@ -53,7 +71,10 @@ let timer: ReturnType<typeof setInterval> | null = null;
|
|
|
53
71
|
let running = false;
|
|
54
72
|
|
|
55
73
|
function isHeartbeatOk(text: string): boolean {
|
|
56
|
-
const normalized = text
|
|
74
|
+
const normalized = text
|
|
75
|
+
.trim()
|
|
76
|
+
.replace(/[^a-z_]/gi, '')
|
|
77
|
+
.toUpperCase();
|
|
57
78
|
return normalized === 'HEARTBEATOK' || normalized.startsWith('HEARTBEATOK');
|
|
58
79
|
}
|
|
59
80
|
|
|
@@ -75,7 +96,10 @@ export function startHeartbeat(
|
|
|
75
96
|
return;
|
|
76
97
|
}
|
|
77
98
|
if (!isWithinActiveHours()) {
|
|
78
|
-
logger.debug(
|
|
99
|
+
logger.debug(
|
|
100
|
+
{ activeHours: proactiveWindowLabel() },
|
|
101
|
+
'Heartbeat skipped — outside active hours window',
|
|
102
|
+
);
|
|
79
103
|
return;
|
|
80
104
|
}
|
|
81
105
|
running = true;
|
|
@@ -87,13 +111,24 @@ export function startHeartbeat(
|
|
|
87
111
|
let turnIndex = 1;
|
|
88
112
|
|
|
89
113
|
try {
|
|
90
|
-
const session = getOrCreateSession(
|
|
114
|
+
const session = memoryService.getOrCreateSession(
|
|
115
|
+
sessionId,
|
|
116
|
+
null,
|
|
117
|
+
channelId,
|
|
118
|
+
);
|
|
91
119
|
turnIndex = session.message_count + 1;
|
|
92
120
|
|
|
93
|
-
const history = getConversationHistory(
|
|
121
|
+
const history = memoryService.getConversationHistory(
|
|
122
|
+
sessionId,
|
|
123
|
+
MAX_HEARTBEAT_HISTORY,
|
|
124
|
+
);
|
|
125
|
+
const memoryContext = memoryService.buildPromptMemoryContext({
|
|
126
|
+
session,
|
|
127
|
+
query: HEARTBEAT_PROMPT,
|
|
128
|
+
});
|
|
94
129
|
const { messages } = buildConversationContext({
|
|
95
130
|
agentId,
|
|
96
|
-
sessionSummary:
|
|
131
|
+
sessionSummary: memoryContext.promptSummary,
|
|
97
132
|
history,
|
|
98
133
|
});
|
|
99
134
|
messages.push({ role: 'user', content: HEARTBEAT_PROMPT });
|
|
@@ -141,13 +176,20 @@ export function startHeartbeat(
|
|
|
141
176
|
toolExecutions: output.toolExecutions || [],
|
|
142
177
|
});
|
|
143
178
|
const tokenUsage = output.tokenUsage;
|
|
144
|
-
const estimatedPromptTokens =
|
|
145
|
-
|
|
146
|
-
|
|
179
|
+
const estimatedPromptTokens =
|
|
180
|
+
tokenUsage?.estimatedPromptTokens ||
|
|
181
|
+
estimateTokenCountFromMessages(messages);
|
|
182
|
+
const estimatedCompletionTokens =
|
|
183
|
+
tokenUsage?.estimatedCompletionTokens ||
|
|
184
|
+
estimateTokenCountFromText(output.result || '');
|
|
185
|
+
const estimatedTotalTokens =
|
|
186
|
+
tokenUsage?.estimatedTotalTokens ||
|
|
187
|
+
estimatedPromptTokens + estimatedCompletionTokens;
|
|
147
188
|
const apiUsageAvailable = tokenUsage?.apiUsageAvailable === true;
|
|
148
189
|
const apiPromptTokens = tokenUsage?.apiPromptTokens || 0;
|
|
149
190
|
const apiCompletionTokens = tokenUsage?.apiCompletionTokens || 0;
|
|
150
|
-
const apiTotalTokens =
|
|
191
|
+
const apiTotalTokens =
|
|
192
|
+
tokenUsage?.apiTotalTokens || apiPromptTokens + apiCompletionTokens;
|
|
151
193
|
recordAuditEvent({
|
|
152
194
|
sessionId,
|
|
153
195
|
runId,
|
|
@@ -158,9 +200,15 @@ export function startHeartbeat(
|
|
|
158
200
|
durationMs: Date.now() - startedAt,
|
|
159
201
|
toolCallCount: (output.toolExecutions || []).length,
|
|
160
202
|
modelCalls: tokenUsage ? Math.max(1, tokenUsage.modelCalls) : 0,
|
|
161
|
-
promptTokens: apiUsageAvailable
|
|
162
|
-
|
|
163
|
-
|
|
203
|
+
promptTokens: apiUsageAvailable
|
|
204
|
+
? apiPromptTokens
|
|
205
|
+
: estimatedPromptTokens,
|
|
206
|
+
completionTokens: apiUsageAvailable
|
|
207
|
+
? apiCompletionTokens
|
|
208
|
+
: estimatedCompletionTokens,
|
|
209
|
+
totalTokens: apiUsageAvailable
|
|
210
|
+
? apiTotalTokens
|
|
211
|
+
: estimatedTotalTokens,
|
|
164
212
|
estimatedPromptTokens,
|
|
165
213
|
estimatedCompletionTokens,
|
|
166
214
|
estimatedTotalTokens,
|
|
@@ -241,8 +289,19 @@ export function startHeartbeat(
|
|
|
241
289
|
}
|
|
242
290
|
|
|
243
291
|
// Real content — persist and deliver
|
|
244
|
-
|
|
245
|
-
|
|
292
|
+
memoryService.storeTurn({
|
|
293
|
+
sessionId,
|
|
294
|
+
user: {
|
|
295
|
+
userId: 'heartbeat',
|
|
296
|
+
username: 'heartbeat',
|
|
297
|
+
content: HEARTBEAT_PROMPT,
|
|
298
|
+
},
|
|
299
|
+
assistant: {
|
|
300
|
+
userId: 'assistant',
|
|
301
|
+
username: null,
|
|
302
|
+
content: result,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
246
305
|
appendSessionTranscript(agentId, {
|
|
247
306
|
sessionId,
|
|
248
307
|
channelId: heartbeatChannelId,
|
|
@@ -267,7 +326,10 @@ export function startHeartbeat(
|
|
|
267
326
|
model: HYBRIDAI_MODEL,
|
|
268
327
|
channelId: heartbeatChannelId,
|
|
269
328
|
});
|
|
270
|
-
logger.info(
|
|
329
|
+
logger.info(
|
|
330
|
+
{ length: result.length },
|
|
331
|
+
'Heartbeat: agent has something to say',
|
|
332
|
+
);
|
|
271
333
|
recordAuditEvent({
|
|
272
334
|
sessionId,
|
|
273
335
|
runId,
|
package/src/hybridai-bots.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getHybridAIApiKey, HYBRIDAI_BASE_URL } from './config.js';
|
|
2
2
|
import type { HybridAIBot } from './types.js';
|
|
3
3
|
|
|
4
4
|
interface BotCacheEntry {
|
|
@@ -10,18 +10,27 @@ let botCache: BotCacheEntry | null = null;
|
|
|
10
10
|
|
|
11
11
|
function normalizeBots(payload: unknown): HybridAIBot[] {
|
|
12
12
|
const data = payload as
|
|
13
|
-
| {
|
|
13
|
+
| {
|
|
14
|
+
data?: Record<string, unknown>[];
|
|
15
|
+
bots?: Record<string, unknown>[];
|
|
16
|
+
items?: Record<string, unknown>[];
|
|
17
|
+
}
|
|
14
18
|
| Record<string, unknown>[];
|
|
15
|
-
const raw = Array.isArray(data)
|
|
19
|
+
const raw = Array.isArray(data)
|
|
20
|
+
? data
|
|
21
|
+
: data.data || data.bots || data.items || [];
|
|
16
22
|
|
|
17
23
|
return raw.map((item) => ({
|
|
18
24
|
id: String(item.id ?? item._id ?? item.chatbot_id ?? item.bot_id ?? ''),
|
|
19
25
|
name: String(item.bot_name ?? item.name ?? 'Unnamed'),
|
|
20
|
-
description:
|
|
26
|
+
description:
|
|
27
|
+
item.description != null ? String(item.description) : undefined,
|
|
21
28
|
}));
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
export async function fetchHybridAIBots(options?: {
|
|
31
|
+
export async function fetchHybridAIBots(options?: {
|
|
32
|
+
cacheTtlMs?: number;
|
|
33
|
+
}): Promise<HybridAIBot[]> {
|
|
25
34
|
const cacheTtlMs = Math.max(0, options?.cacheTtlMs ?? 0);
|
|
26
35
|
const now = Date.now();
|
|
27
36
|
|
|
@@ -22,7 +22,10 @@ function ensureAuditReady(): boolean {
|
|
|
22
22
|
initDatabase({ quiet: true });
|
|
23
23
|
auditReady = true;
|
|
24
24
|
} catch (err) {
|
|
25
|
-
logger.warn(
|
|
25
|
+
logger.warn(
|
|
26
|
+
{ err },
|
|
27
|
+
'Failed to initialize DB for instruction approval audit',
|
|
28
|
+
);
|
|
26
29
|
auditReady = false;
|
|
27
30
|
}
|
|
28
31
|
return auditReady;
|
|
@@ -2,9 +2,18 @@ import { createHash } from 'crypto';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
-
export const INSTRUCTION_FILES = [
|
|
5
|
+
export const INSTRUCTION_FILES = [
|
|
6
|
+
'AGENTS.md',
|
|
7
|
+
'SECURITY.md',
|
|
8
|
+
'TRUST_MODEL.md',
|
|
9
|
+
] as const;
|
|
6
10
|
export const INSTRUCTION_BASELINE_VERSION = 1;
|
|
7
|
-
export const INSTRUCTION_BASELINE_PATH = path.join(
|
|
11
|
+
export const INSTRUCTION_BASELINE_PATH = path.join(
|
|
12
|
+
process.cwd(),
|
|
13
|
+
'data',
|
|
14
|
+
'audit',
|
|
15
|
+
'instruction-hashes.json',
|
|
16
|
+
);
|
|
8
17
|
|
|
9
18
|
export interface InstructionHashBaseline {
|
|
10
19
|
version: number;
|
|
@@ -29,7 +38,9 @@ export interface InstructionIntegrityResult {
|
|
|
29
38
|
files: InstructionFileResult[];
|
|
30
39
|
}
|
|
31
40
|
|
|
32
|
-
export function summarizeInstructionIntegrity(
|
|
41
|
+
export function summarizeInstructionIntegrity(
|
|
42
|
+
result: InstructionIntegrityResult,
|
|
43
|
+
): string {
|
|
33
44
|
if (result.baselineError) return `baseline.invalid (${result.baselineError})`;
|
|
34
45
|
if (!result.baseline) return 'baseline.missing';
|
|
35
46
|
|
|
@@ -62,19 +73,24 @@ export function loadInstructionBaseline(): InstructionHashBaseline | null {
|
|
|
62
73
|
|
|
63
74
|
const raw = fs.readFileSync(INSTRUCTION_BASELINE_PATH, 'utf-8');
|
|
64
75
|
const parsed = JSON.parse(raw) as unknown;
|
|
65
|
-
if (!isRecord(parsed))
|
|
76
|
+
if (!isRecord(parsed))
|
|
77
|
+
throw new Error('Instruction baseline is not a JSON object.');
|
|
66
78
|
|
|
67
79
|
const version = parsed.version;
|
|
68
80
|
const approvedAt = parsed.approvedAt;
|
|
69
81
|
const files = parsed.files;
|
|
70
|
-
if (typeof version !== 'number')
|
|
82
|
+
if (typeof version !== 'number')
|
|
83
|
+
throw new Error('Instruction baseline is missing numeric `version`.');
|
|
71
84
|
if (version !== INSTRUCTION_BASELINE_VERSION) {
|
|
72
|
-
throw new Error(
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Instruction baseline version ${String(version)} is unsupported.`,
|
|
87
|
+
);
|
|
73
88
|
}
|
|
74
89
|
if (typeof approvedAt !== 'string' || !approvedAt.trim()) {
|
|
75
90
|
throw new Error('Instruction baseline is missing `approvedAt`.');
|
|
76
91
|
}
|
|
77
|
-
if (!isRecord(files))
|
|
92
|
+
if (!isRecord(files))
|
|
93
|
+
throw new Error('Instruction baseline is missing `files` object.');
|
|
78
94
|
|
|
79
95
|
const normalizedFiles: Record<string, string> = {};
|
|
80
96
|
for (const relPath of INSTRUCTION_FILES) {
|
|
@@ -95,7 +111,9 @@ export function approveInstructionBaseline(): InstructionHashBaseline {
|
|
|
95
111
|
const hashes = computeCurrentHashes();
|
|
96
112
|
const missing = INSTRUCTION_FILES.filter((relPath) => !hashes[relPath]);
|
|
97
113
|
if (missing.length > 0) {
|
|
98
|
-
throw new Error(
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Approval failed: missing instruction files (${missing.join(', ')}).`,
|
|
116
|
+
);
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
const baseline: InstructionHashBaseline = {
|
|
@@ -165,7 +183,10 @@ export function verifyInstructionBaseline(): InstructionIntegrityResult {
|
|
|
165
183
|
};
|
|
166
184
|
});
|
|
167
185
|
|
|
168
|
-
const ok =
|
|
186
|
+
const ok =
|
|
187
|
+
baselineError === null &&
|
|
188
|
+
baseline !== null &&
|
|
189
|
+
files.every((file) => file.status === 'ok');
|
|
169
190
|
return {
|
|
170
191
|
ok,
|
|
171
192
|
baselinePath: INSTRUCTION_BASELINE_PATH,
|
package/src/ipc.ts
CHANGED
|
@@ -46,7 +46,11 @@ export function ensureAgentDirs(agentId: string): void {
|
|
|
46
46
|
* When omitApiKey is set, the apiKey field is excluded from the file on disk
|
|
47
47
|
* (the container already has the key in memory from the initial stdin payload).
|
|
48
48
|
*/
|
|
49
|
-
export function writeInput(
|
|
49
|
+
export function writeInput(
|
|
50
|
+
sessionId: string,
|
|
51
|
+
input: ContainerInput,
|
|
52
|
+
opts?: { omitApiKey?: boolean },
|
|
53
|
+
): string {
|
|
50
54
|
const dir = ipcDir(sessionId);
|
|
51
55
|
const inputPath = path.join(dir, 'input.json');
|
|
52
56
|
const toWrite = opts?.omitApiKey ? { ...input, apiKey: '' } : input;
|
|
@@ -67,7 +71,10 @@ function interruptedOutput(): ContainerOutput {
|
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
async function sleepWithAbort(
|
|
74
|
+
async function sleepWithAbort(
|
|
75
|
+
ms: number,
|
|
76
|
+
signal?: AbortSignal,
|
|
77
|
+
): Promise<boolean> {
|
|
71
78
|
if (!signal) {
|
|
72
79
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
73
80
|
return false;
|
|
@@ -109,8 +116,16 @@ export async function readOutput(
|
|
|
109
116
|
const stat = fs.statSync(outputPath);
|
|
110
117
|
if (stat.size > CONTAINER_MAX_OUTPUT_SIZE) {
|
|
111
118
|
fs.unlinkSync(outputPath);
|
|
112
|
-
logger.warn(
|
|
113
|
-
|
|
119
|
+
logger.warn(
|
|
120
|
+
{ sessionId, size: stat.size, limit: CONTAINER_MAX_OUTPUT_SIZE },
|
|
121
|
+
'Container output exceeded size limit',
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
status: 'error',
|
|
125
|
+
result: null,
|
|
126
|
+
toolsUsed: [],
|
|
127
|
+
error: `Output too large (${stat.size} bytes, limit ${CONTAINER_MAX_OUTPUT_SIZE})`,
|
|
128
|
+
};
|
|
114
129
|
}
|
|
115
130
|
try {
|
|
116
131
|
const raw = fs.readFileSync(outputPath, 'utf-8');
|
|
@@ -152,7 +167,10 @@ export function cleanupIpc(sessionId: string): void {
|
|
|
152
167
|
/**
|
|
153
168
|
* Get host paths for container mounting.
|
|
154
169
|
*/
|
|
155
|
-
export function getSessionPaths(
|
|
170
|
+
export function getSessionPaths(
|
|
171
|
+
sessionId: string,
|
|
172
|
+
agentId: string,
|
|
173
|
+
): {
|
|
156
174
|
ipcPath: string;
|
|
157
175
|
workspacePath: string;
|
|
158
176
|
} {
|
package/src/logger.ts
CHANGED
|
@@ -12,7 +12,10 @@ export const logger = pino({
|
|
|
12
12
|
onRuntimeConfigChange((next, prev) => {
|
|
13
13
|
if (next.ops.logLevel !== prev.ops.logLevel) {
|
|
14
14
|
logger.level = next.ops.logLevel;
|
|
15
|
-
logger.info(
|
|
15
|
+
logger.info(
|
|
16
|
+
{ level: next.ops.logLevel },
|
|
17
|
+
'Logger level updated from config.json',
|
|
18
|
+
);
|
|
16
19
|
}
|
|
17
20
|
});
|
|
18
21
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MemoryBackend } from './memory-service.js';
|
|
2
|
+
|
|
3
|
+
export interface MemoryConsolidationConfig {
|
|
4
|
+
decayRate: number;
|
|
5
|
+
staleAfterDays: number;
|
|
6
|
+
minConfidence: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MemoryConsolidationReport {
|
|
10
|
+
memoriesDecayed: number;
|
|
11
|
+
durationMs: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class MemoryConsolidationEngine {
|
|
15
|
+
private readonly backend: MemoryBackend;
|
|
16
|
+
private readonly config: MemoryConsolidationConfig;
|
|
17
|
+
|
|
18
|
+
constructor(backend: MemoryBackend, config: MemoryConsolidationConfig) {
|
|
19
|
+
this.backend = backend;
|
|
20
|
+
this.config = config;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
consolidate(
|
|
24
|
+
overrides?: Partial<MemoryConsolidationConfig>,
|
|
25
|
+
): MemoryConsolidationReport {
|
|
26
|
+
const start = Date.now();
|
|
27
|
+
const config = {
|
|
28
|
+
...this.config,
|
|
29
|
+
...(overrides || {}),
|
|
30
|
+
};
|
|
31
|
+
const memoriesDecayed = this.backend.decaySemanticMemories({
|
|
32
|
+
decayRate: config.decayRate,
|
|
33
|
+
staleAfterDays: config.staleAfterDays,
|
|
34
|
+
minConfidence: config.minConfidence,
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
memoriesDecayed,
|
|
38
|
+
durationMs: Math.max(0, Date.now() - start),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|