volute 0.3.1 → 0.5.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/README.md +29 -29
- package/dist/agent-Z2B6EFEQ.js +75 -0
- package/dist/{agent-manager-AUCKMGPR.js → agent-manager-PXBKA2GK.js} +4 -4
- package/dist/channel-MK5OK2SI.js +113 -0
- package/dist/chunk-5X7HGB6L.js +107 -0
- package/dist/{chunk-YGFIWIOF.js → chunk-7L4AN5D4.js} +1 -1
- package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
- package/dist/chunk-B3R6L2GW.js +24 -0
- package/dist/{chunk-DNOXHLE5.js → chunk-HE67X4T6.js} +1 -1
- package/dist/{chunk-I6OHXCMV.js → chunk-MW2KFO3B.js} +47 -9
- package/dist/chunk-MXUCNIBG.js +168 -0
- package/dist/chunk-SMISE4SV.js +226 -0
- package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
- package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
- package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
- package/dist/chunk-ZYGKG6VC.js +22 -0
- package/dist/cli.js +98 -75
- package/dist/connector-LYEMXQEV.js +157 -0
- package/dist/connectors/discord.js +104 -161
- package/dist/connectors/slack.js +179 -0
- package/dist/connectors/telegram.js +175 -0
- package/dist/conversation-ERXEQZTY.js +163 -0
- package/dist/create-RVCZN6HE.js +91 -0
- package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
- package/dist/daemon.js +824 -252
- package/dist/{delete-GQ7JEK2S.js → delete-3QH7VYIN.js} +8 -9
- package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
- package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
- package/dist/{history-3VRUBGGV.js → history-OEONB53Z.js} +5 -5
- package/dist/{import-K4MP2GX7.js → import-MXJB2EII.js} +23 -8
- package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
- package/dist/message-ADHWFHSI.js +32 -0
- package/dist/package-VQOE7JNH.js +89 -0
- package/dist/{schedule-4I5TYHFH.js → schedule-NAG6F463.js} +12 -7
- package/dist/send-66QMKRUH.js +75 -0
- package/dist/{setup-SRS7AUAA.js → setup-RPRRGG2F.js} +6 -6
- package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
- package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
- package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
- package/dist/{up-UT3IMKCA.js → up-7ILD7GU7.js} +2 -2
- package/dist/update-LPSIAWQ2.js +140 -0
- package/dist/update-check-Y33QDCFL.js +17 -0
- package/dist/{upgrade-CDKECCGN.js → upgrade-FX2TKJ2S.js} +16 -15
- package/dist/{variant-CVYM3EQG.js → variant-LAB67OC2.js} +17 -12
- package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0003_clean_ego.sql +12 -0
- package/drizzle/meta/0003_snapshot.json +417 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +3 -1
- package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
- package/templates/_base/_skills/volute-agent/SKILL.md +112 -16
- package/templates/_base/home/.config/routes.json +10 -0
- package/templates/_base/home/VOLUTE.md +19 -28
- package/templates/_base/src/lib/file-handler.ts +46 -0
- package/templates/_base/src/lib/format-prefix.ts +1 -1
- package/templates/_base/src/lib/router.ts +327 -0
- package/templates/_base/src/lib/routing.ts +137 -0
- package/templates/_base/src/lib/types.ts +16 -3
- package/templates/_base/src/lib/volute-server.ts +20 -48
- package/templates/agent-sdk/.init/.config/routes.json +5 -0
- package/templates/agent-sdk/.init/CLAUDE.md +2 -2
- package/templates/agent-sdk/src/agent.ts +269 -82
- package/templates/agent-sdk/src/server.ts +19 -4
- package/templates/agent-sdk/volute-template.json +1 -1
- package/templates/pi/.init/.config/routes.json +5 -0
- package/templates/pi/.init/AGENTS.md +1 -1
- package/templates/pi/src/agent.ts +279 -58
- package/templates/pi/src/server.ts +15 -4
- package/templates/pi/volute-template.json +1 -1
- package/dist/channel-7FZ6D25H.js +0 -90
- package/dist/chunk-N4YNKR3Q.js +0 -90
- package/dist/connector-TVJULIRT.js +0 -96
- package/dist/create-BRG2DBWI.js +0 -79
- package/dist/send-UK3JBZIB.js +0 -53
- package/dist/web-assets/assets/index-BC5eSqbY.js +0 -296
- package/templates/_base/src/lib/sessions.ts +0 -71
- package/templates/agent-sdk/.init/.config/sessions.json +0 -4
- package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
- package/templates/pi/.init/.config/sessions.json +0 -1
- package/templates/pi/src/lib/agent-sessions.ts +0 -210
- package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { log } from "./logger.js";
|
|
3
|
+
|
|
4
|
+
export type BatchConfig = {
|
|
5
|
+
debounce?: number; // seconds of quiet before flush
|
|
6
|
+
maxWait?: number; // max seconds before forced flush
|
|
7
|
+
triggers?: string[]; // patterns that cause immediate flush
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type RoutingRule = {
|
|
11
|
+
session?: string;
|
|
12
|
+
destination?: "agent" | "file";
|
|
13
|
+
path?: string; // file path for file destination
|
|
14
|
+
interrupt?: boolean; // interrupt in-progress agent turn (default: true for agent)
|
|
15
|
+
batch?: number | BatchConfig; // number = minutes (legacy), object = fine-grained control
|
|
16
|
+
channel?: string;
|
|
17
|
+
sender?: string;
|
|
18
|
+
isDM?: boolean; // match on isDM metadata
|
|
19
|
+
participants?: number; // match on participant count (e.g. 2 = DM)
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type RoutingConfig = {
|
|
23
|
+
rules?: RoutingRule[];
|
|
24
|
+
default?: string;
|
|
25
|
+
gateUnmatched?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ResolvedRoute =
|
|
29
|
+
| {
|
|
30
|
+
destination: "agent";
|
|
31
|
+
session: string;
|
|
32
|
+
interrupt: boolean;
|
|
33
|
+
batch?: BatchConfig;
|
|
34
|
+
matched: boolean;
|
|
35
|
+
}
|
|
36
|
+
| { destination: "file"; path: string; matched: boolean };
|
|
37
|
+
|
|
38
|
+
/** Normalize batch config: number (minutes) → { maxWait } in seconds. */
|
|
39
|
+
export function normalizeBatch(batch: number | BatchConfig): BatchConfig {
|
|
40
|
+
if (typeof batch === "number") return { maxWait: batch * 60 };
|
|
41
|
+
return batch;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadRoutingConfig(configPath: string): RoutingConfig {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
if (err?.code !== "ENOENT") {
|
|
49
|
+
log("routing", `failed to load ${configPath}:`, err);
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Match a glob-like pattern against a string.
|
|
57
|
+
* Supports only `*` as wildcard (matches any sequence of characters).
|
|
58
|
+
*/
|
|
59
|
+
function globMatch(pattern: string, value: string): boolean {
|
|
60
|
+
// Escape regex special chars except *, then replace * with .*
|
|
61
|
+
const regex = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
62
|
+
return new RegExp(`^${regex}$`).test(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const GLOB_MATCH_KEYS = new Set(["channel", "sender"]);
|
|
66
|
+
const NON_MATCH_KEYS = new Set(["session", "batch", "destination", "path", "interrupt"]);
|
|
67
|
+
|
|
68
|
+
type MatchMeta = { channel?: string; sender?: string; isDM?: boolean; participantCount?: number };
|
|
69
|
+
|
|
70
|
+
function ruleMatches(rule: RoutingRule, meta: MatchMeta): boolean {
|
|
71
|
+
for (const [key, pattern] of Object.entries(rule)) {
|
|
72
|
+
if (NON_MATCH_KEYS.has(key)) continue;
|
|
73
|
+
|
|
74
|
+
// Boolean match: isDM
|
|
75
|
+
if (key === "isDM") {
|
|
76
|
+
if (typeof pattern !== "boolean") return false;
|
|
77
|
+
if ((meta.isDM ?? false) !== pattern) return false;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Numeric match: participants
|
|
82
|
+
if (key === "participants") {
|
|
83
|
+
if (typeof pattern !== "number") return false;
|
|
84
|
+
if ((meta.participantCount ?? 0) !== pattern) return false;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Glob string match: channel, sender
|
|
89
|
+
if (typeof pattern !== "string") return false;
|
|
90
|
+
if (!GLOB_MATCH_KEYS.has(key)) return false;
|
|
91
|
+
const value = meta[key as "channel" | "sender"] ?? "";
|
|
92
|
+
if (!globMatch(pattern, value)) return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function expandTemplate(template: string, meta: MatchMeta): string {
|
|
98
|
+
return template
|
|
99
|
+
.replace(/\$\{sender\}/g, meta.sender ?? "unknown")
|
|
100
|
+
.replace(/\$\{channel\}/g, meta.channel ?? "unknown");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the full route for a message: destination type, session/path, interrupt, batch.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveRoute(config: RoutingConfig, meta: MatchMeta): ResolvedRoute {
|
|
107
|
+
const fallback = config.default ?? "main";
|
|
108
|
+
|
|
109
|
+
if (!config.rules) {
|
|
110
|
+
return { destination: "agent", session: fallback, interrupt: true, matched: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const rule of config.rules) {
|
|
114
|
+
if (ruleMatches(rule, meta)) {
|
|
115
|
+
if (rule.destination === "file") {
|
|
116
|
+
if (!rule.path) {
|
|
117
|
+
log("routing", `file destination rule missing path — falling through`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
return { destination: "file", path: rule.path, matched: true };
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
destination: "agent",
|
|
124
|
+
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
125
|
+
interrupt: rule.interrupt ?? true,
|
|
126
|
+
batch: rule.batch != null ? normalizeBatch(rule.batch) : undefined,
|
|
127
|
+
matched: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { destination: "agent", session: fallback, interrupt: true, matched: false };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sanitizeSessionName(name: string): string {
|
|
136
|
+
return name.replace(/\0/g, "").replace(/[/\\]/g, "-").replace(/\.\./g, "-").slice(0, 100);
|
|
137
|
+
}
|
|
@@ -8,9 +8,16 @@ export type ChannelMeta = {
|
|
|
8
8
|
platform?: string;
|
|
9
9
|
isDM?: boolean;
|
|
10
10
|
channelName?: string;
|
|
11
|
-
|
|
11
|
+
serverName?: string;
|
|
12
12
|
sessionName?: string;
|
|
13
|
-
|
|
13
|
+
participants?: string[];
|
|
14
|
+
participantCount?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** ChannelMeta enriched by the router with dispatch info. */
|
|
18
|
+
export type HandlerMeta = ChannelMeta & {
|
|
19
|
+
messageId: string;
|
|
20
|
+
interrupt?: boolean;
|
|
14
21
|
};
|
|
15
22
|
|
|
16
23
|
export type VoluteRequest = {
|
|
@@ -28,4 +35,10 @@ export type VoluteEvent = { messageId?: string } & (
|
|
|
28
35
|
|
|
29
36
|
export type Listener = (event: VoluteEvent) => void;
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
/** A handler that processes a single routed message and streams events to a listener. */
|
|
39
|
+
export type MessageHandler = {
|
|
40
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener: Listener): () => void; // returns unsubscribe
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Resolves a key (session name, file path, etc.) to a MessageHandler. */
|
|
44
|
+
export type HandlerResolver = (key: string) => MessageHandler;
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import { createServer, type IncomingMessage, type Server } from "node:http";
|
|
2
2
|
import { log } from "./logger.js";
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
export type VoluteAgent = {
|
|
7
|
-
sendMessage: (content: string | VoluteContentPart[], meta?: ChannelMeta) => void;
|
|
8
|
-
onMessage: (listener: (event: VoluteEvent) => void, sessionName?: string) => () => void;
|
|
9
|
-
};
|
|
3
|
+
import type { Router } from "./router.js";
|
|
4
|
+
import type { VoluteRequest } from "./types.js";
|
|
10
5
|
|
|
11
6
|
function readBody(req: IncomingMessage): Promise<string> {
|
|
12
7
|
return new Promise((resolve, reject) => {
|
|
@@ -18,13 +13,12 @@ function readBody(req: IncomingMessage): Promise<string> {
|
|
|
18
13
|
}
|
|
19
14
|
|
|
20
15
|
export function createVoluteServer(options: {
|
|
21
|
-
|
|
16
|
+
router: Router;
|
|
22
17
|
port: number;
|
|
23
18
|
name: string;
|
|
24
19
|
version: string;
|
|
25
|
-
sessionsConfigPath?: string;
|
|
26
20
|
}): Server {
|
|
27
|
-
const {
|
|
21
|
+
const { router, port, name, version } = options;
|
|
28
22
|
|
|
29
23
|
const server = createServer(async (req, res) => {
|
|
30
24
|
const url = new URL(req.url!, "http://localhost");
|
|
@@ -39,58 +33,34 @@ export function createVoluteServer(options: {
|
|
|
39
33
|
try {
|
|
40
34
|
const body = JSON.parse(await readBody(req)) as VoluteRequest;
|
|
41
35
|
|
|
42
|
-
// Resolve session from routing config (re-read on each request for hot-reload)
|
|
43
|
-
let sessionName = "main";
|
|
44
|
-
if (options.sessionsConfigPath) {
|
|
45
|
-
const sessionConfig = loadSessionConfig(options.sessionsConfigPath);
|
|
46
|
-
sessionName = resolveSession(sessionConfig, {
|
|
47
|
-
channel: body.channel,
|
|
48
|
-
sender: body.sender,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
if (sessionName === "$new") {
|
|
52
|
-
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const messageId = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
56
|
-
|
|
57
36
|
res.writeHead(200, {
|
|
58
37
|
"Content-Type": "application/x-ndjson",
|
|
59
38
|
"Cache-Control": "no-cache",
|
|
60
39
|
Connection: "keep-alive",
|
|
61
40
|
});
|
|
62
41
|
|
|
63
|
-
const
|
|
42
|
+
const { unsubscribe } = router.route(body.content, body, (event) => {
|
|
64
43
|
try {
|
|
65
|
-
// Only forward events from our message (skip startup/other messages)
|
|
66
|
-
if (event.messageId !== messageId) return;
|
|
67
44
|
res.write(`${JSON.stringify(event)}\n`);
|
|
68
45
|
if (event.type === "done") {
|
|
69
|
-
removeListener();
|
|
70
46
|
res.end();
|
|
71
47
|
}
|
|
72
|
-
} catch {
|
|
73
|
-
|
|
48
|
+
} catch (err) {
|
|
49
|
+
log("server", "write error, disconnecting:", err);
|
|
50
|
+
unsubscribe();
|
|
74
51
|
}
|
|
75
|
-
}, sessionName);
|
|
76
|
-
|
|
77
|
-
res.on("close", () => {
|
|
78
|
-
removeListener();
|
|
79
52
|
});
|
|
80
53
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
} catch {
|
|
92
|
-
res.writeHead(400);
|
|
93
|
-
res.end("Bad Request");
|
|
54
|
+
res.on("close", () => unsubscribe());
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err instanceof SyntaxError) {
|
|
57
|
+
res.writeHead(400);
|
|
58
|
+
res.end("Bad Request");
|
|
59
|
+
} else {
|
|
60
|
+
log("server", "error handling /message:", err);
|
|
61
|
+
res.writeHead(500);
|
|
62
|
+
res.end("Internal Server Error");
|
|
63
|
+
}
|
|
94
64
|
}
|
|
95
65
|
return;
|
|
96
66
|
}
|
|
@@ -99,6 +69,8 @@ export function createVoluteServer(options: {
|
|
|
99
69
|
res.end("Not Found");
|
|
100
70
|
});
|
|
101
71
|
|
|
72
|
+
server.on("close", () => router.close());
|
|
73
|
+
|
|
102
74
|
let retries = 0;
|
|
103
75
|
const maxRetries = 5;
|
|
104
76
|
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Mechanics
|
|
2
2
|
|
|
3
|
-
You are an autonomous agent running as a persistent server in a git repository. Your working directory is `home/`
|
|
3
|
+
You are an autonomous agent running as a persistent server in a git repository. Your working directory is already set to `home/` — all file paths you use (`.config/routes.json`, `inbox/`, `memory/`, etc.) are relative to it. Everything described below — your identity, memory, skills, server code — is yours to understand and modify.
|
|
4
4
|
|
|
5
5
|
## Message Format
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ See the **memory** skill for detailed guidance.
|
|
|
33
33
|
|
|
34
34
|
## Sessions
|
|
35
35
|
|
|
36
|
-
- You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/
|
|
36
|
+
- You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/routes.json`.
|
|
37
37
|
- Your conversation may be **resumed** from a previous session — orient yourself by reading recent journal entries if needed.
|
|
38
38
|
- On a **fresh session**, read `MEMORY.md` and recent journal entries to remember where you left off.
|
|
39
39
|
- On **compaction**, update today's journal to preserve context before the conversation is trimmed.
|