volute 0.7.0 → 0.8.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 +13 -13
- package/dist/{agent-7JF7MT73.js → agent-YORVRB6I.js} +10 -10
- package/dist/{agent-manager-IMZ7ZMBF.js → agent-manager-CMMH5KQQ.js} +4 -4
- package/dist/{channel-SMCNOIVQ.js → channel-RDGHBFSI.js} +16 -56
- package/dist/{chunk-JR4UXCTO.js → chunk-23L3MKEV.js} +1 -1
- package/dist/{chunk-5SKQ6J7T.js → chunk-5C5JWR2L.js} +15 -7
- package/dist/{chunk-UWHWAPGO.js → chunk-DP2DX4WV.js} +9 -1
- package/dist/{chunk-7ACDT3P2.js → chunk-ECPQXRLB.js} +1 -2
- package/dist/{chunk-LLJNZPCU.js → chunk-HZ5LTOEJ.js} +1 -1
- package/dist/{chunk-W76KWE23.js → chunk-IQXBMFZG.js} +6 -4
- package/dist/{chunk-ZZOOTYXK.js → chunk-LIPPXNIE.js} +60 -74
- package/dist/{chunk-BX7KI4S3.js → chunk-N6MLQ26B.js} +23 -96
- package/dist/{chunk-H7AMDUIA.js → chunk-QF22MYDJ.js} +6 -5
- package/dist/{chunk-NKXULRSW.js → chunk-RT6Y7AR3.js} +1 -1
- package/dist/{chunk-62X577Y7.js → chunk-W6TMWYU3.js} +126 -73
- package/dist/{chunk-EG45HBSJ.js → chunk-XSJ27WEM.js} +1 -1
- package/dist/cli.js +22 -20
- package/dist/{connector-Y7JPNROO.js → connector-ZP6MEFF4.js} +3 -3
- package/dist/connectors/discord.js +18 -59
- package/dist/connectors/slack.js +21 -38
- package/dist/connectors/telegram.js +31 -49
- package/dist/{create-G525LWEA.js → create-HGJHLABX.js} +22 -17
- package/dist/{daemon-client-442IV43D.js → daemon-client-54J3EIZD.js} +2 -2
- package/dist/{daemon-restart-4HVEKYFY.js → daemon-restart-CPBLMMRI.js} +3 -3
- package/dist/daemon.js +342 -402
- package/dist/{delete-UOU4AFQN.js → delete-45TGQC4N.js} +10 -5
- package/dist/{down-AZVH5TCD.js → down-O4EWZTVA.js} +2 -2
- package/dist/{env-7GLUJCWS.js → env-KMNYGVZ2.js} +7 -9
- package/dist/{history-H72ZUIBN.js → history-PXJVYLVY.js} +2 -2
- package/dist/{import-AVKQJDYC.js → import-CNEDF3TD.js} +6 -6
- package/dist/{logs-EDGK26AK.js → logs-TZB3MTLZ.js} +5 -4
- package/dist/{package-T2WAVJOU.js → package-5UCKNK6J.js} +1 -1
- package/dist/{restart-O4ETYLJF.js → restart-KVH3TK5N.js} +2 -2
- package/dist/{schedule-S6QVC5ON.js → schedule-HCUCBNQI.js} +2 -2
- package/dist/send-BNC2S5BY.js +162 -0
- package/dist/{service-HZNIDNJF.js → service-R4MCNBOA.js} +1 -1
- package/dist/{setup-F4TCWVSP.js → setup-JXDCJX7W.js} +25 -6
- package/dist/{start-VHQ7LNWM.js → start-QU73YTJW.js} +2 -2
- package/dist/{status-QAJWXKMZ.js → status-Q6ZQJXNI.js} +2 -2
- package/dist/{stop-CAGCT5NI.js → stop-N7U5N6A7.js} +2 -2
- package/dist/{up-RWZF6MLT.js → up-V6EAA7OZ.js} +2 -2
- package/dist/{update-F7QWV2LB.js → update-EUCZ7XGG.js} +3 -3
- package/dist/{update-check-B4J6IEQ4.js → update-check-SM4244SU.js} +2 -2
- package/dist/{upgrade-YXKPWDRU.js → upgrade-CZF6PN7Y.js} +4 -4
- package/dist/{variant-4Z6W3PP6.js → variant-RKXPN5DH.js} +20 -46
- package/dist/web-assets/assets/index-D-3zx6vs.js +307 -0
- package/dist/web-assets/index.html +1 -1
- package/drizzle/0004_magical_silverclaw.sql +1 -0
- package/drizzle/meta/0004_snapshot.json +410 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/templates/_base/_skills/volute-agent/SKILL.md +32 -16
- package/templates/_base/home/.config/routes.json +4 -8
- package/templates/_base/home/VOLUTE.md +16 -14
- package/templates/_base/src/lib/auto-reply.ts +38 -0
- package/templates/_base/src/lib/daemon-client.ts +53 -0
- package/templates/_base/src/lib/router.ts +66 -14
- package/templates/_base/src/lib/routing.ts +48 -9
- package/templates/_base/src/lib/startup.ts +1 -25
- package/templates/_base/src/lib/types.ts +2 -1
- package/templates/_base/src/lib/volute-server.ts +29 -14
- package/templates/agent-sdk/src/agent.ts +53 -111
- package/templates/agent-sdk/src/lib/content.ts +41 -0
- package/templates/agent-sdk/src/lib/session-store.ts +43 -0
- package/templates/agent-sdk/src/lib/stream-consumer.ts +66 -0
- package/templates/agent-sdk/src/server.ts +5 -13
- package/templates/pi/.init/AGENTS.md +5 -5
- package/templates/pi/src/agent.ts +32 -84
- package/templates/pi/src/lib/content.ts +15 -0
- package/templates/pi/src/lib/event-handler.ts +74 -0
- package/templates/pi/src/lib/resolve-model.ts +21 -0
- package/templates/pi/src/server.ts +3 -7
- package/dist/chunk-B3R6L2GW.js +0 -24
- package/dist/chunk-ZYGKG6VC.js +0 -22
- package/dist/message-SCOQDR3P.js +0 -32
- package/dist/send-G7PE4DOJ.js +0 -72
- package/dist/web-assets/assets/index-B1CqjUYD.js +0 -308
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { daemonSend } from "./daemon-client.js";
|
|
2
|
+
import { log } from "./logger.js";
|
|
3
|
+
|
|
4
|
+
export type MessageChannelInfo = { channel: string; autoReply: boolean };
|
|
5
|
+
|
|
6
|
+
export type AutoReplyTracker = {
|
|
7
|
+
accumulate(text: string): void;
|
|
8
|
+
flush(currentMessageId: string | undefined): void;
|
|
9
|
+
reset(): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function createAutoReplyTracker(
|
|
13
|
+
messageChannels: Map<string, MessageChannelInfo>,
|
|
14
|
+
): AutoReplyTracker {
|
|
15
|
+
let accumulator = "";
|
|
16
|
+
|
|
17
|
+
function flush(currentMessageId: string | undefined) {
|
|
18
|
+
const text = accumulator.trim();
|
|
19
|
+
accumulator = "";
|
|
20
|
+
if (!text) return;
|
|
21
|
+
const info = currentMessageId ? messageChannels.get(currentMessageId) : undefined;
|
|
22
|
+
if (info?.autoReply && info.channel) {
|
|
23
|
+
daemonSend(info.channel, text).catch((err) => {
|
|
24
|
+
log("agent", `auto-reply to ${info.channel} failed: ${err}`);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
accumulate(text: string) {
|
|
31
|
+
accumulator += text;
|
|
32
|
+
},
|
|
33
|
+
flush,
|
|
34
|
+
reset() {
|
|
35
|
+
accumulator = "";
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const port = process.env.VOLUTE_DAEMON_PORT;
|
|
2
|
+
const agent = process.env.VOLUTE_AGENT;
|
|
3
|
+
const token = process.env.VOLUTE_DAEMON_TOKEN;
|
|
4
|
+
|
|
5
|
+
function headers(): Record<string, string> {
|
|
6
|
+
const h: Record<string, string> = { "Content-Type": "application/json" };
|
|
7
|
+
if (token) h.Authorization = `Bearer ${token}`;
|
|
8
|
+
// Origin header required for CSRF checks on mutation requests
|
|
9
|
+
if (port) h.Origin = `http://127.0.0.1:${port}`;
|
|
10
|
+
return h;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function daemonRestart(context?: {
|
|
14
|
+
type: string;
|
|
15
|
+
[k: string]: unknown;
|
|
16
|
+
}): Promise<void> {
|
|
17
|
+
if (!port || !agent) {
|
|
18
|
+
console.error("[volute] daemonRestart: VOLUTE_DAEMON_PORT or VOLUTE_AGENT not set");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
await fetch(`http://127.0.0.1:${port}/api/agents/${encodeURIComponent(agent)}/restart`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: headers(),
|
|
25
|
+
body: JSON.stringify({ context }),
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
// Daemon may kill us before response arrives — expected
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function daemonSend(channel: string, text: string): Promise<void> {
|
|
33
|
+
if (!port || !agent) {
|
|
34
|
+
console.error("[volute] daemonSend: VOLUTE_DAEMON_PORT or VOLUTE_AGENT not set");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const res = await fetch(
|
|
38
|
+
`http://127.0.0.1:${port}/api/agents/${encodeURIComponent(agent)}/message`,
|
|
39
|
+
{
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: headers(),
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
content: text,
|
|
44
|
+
channel,
|
|
45
|
+
sender: agent,
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const body = await res.text().catch(() => "");
|
|
51
|
+
throw new Error(`daemonSend failed (${res.status}): ${body}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { formatPrefix, formatTypingSuffix } from "./format-prefix.js";
|
|
2
2
|
import { log, logMessage } from "./logger.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type BatchConfig,
|
|
5
|
+
loadRoutingConfig,
|
|
6
|
+
resolveRoute,
|
|
7
|
+
resolveSessionConfig,
|
|
8
|
+
} from "./routing.js";
|
|
4
9
|
import type { ChannelMeta, HandlerResolver, Listener, VoluteContentPart } from "./types.js";
|
|
5
10
|
|
|
6
11
|
export type Router = {
|
|
@@ -72,6 +77,24 @@ function appendTypingSuffix(
|
|
|
72
77
|
});
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
function prependInstructions(
|
|
81
|
+
content: VoluteContentPart[],
|
|
82
|
+
instructions: string | undefined,
|
|
83
|
+
): VoluteContentPart[] {
|
|
84
|
+
if (!instructions) return content;
|
|
85
|
+
const prefix = `[Session instructions: ${instructions}]\n\n`;
|
|
86
|
+
const firstTextIdx = content.findIndex((p) => p.type === "text");
|
|
87
|
+
if (firstTextIdx === -1) {
|
|
88
|
+
return [{ type: "text", text: prefix.trimEnd() }, ...content];
|
|
89
|
+
}
|
|
90
|
+
return content.map((part, i) => {
|
|
91
|
+
if (i === firstTextIdx) {
|
|
92
|
+
return { type: "text" as const, text: prefix + (part as { text: string }).text };
|
|
93
|
+
}
|
|
94
|
+
return part;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
function sanitizeChannelPath(channel: string): string {
|
|
76
99
|
return channel
|
|
77
100
|
.replace(/[/\\:]/g, "-")
|
|
@@ -107,20 +130,21 @@ function formatInviteNotification(
|
|
|
107
130
|
lines.push("");
|
|
108
131
|
lines.push(`Further messages will be saved to ${filePath}`);
|
|
109
132
|
lines.push("");
|
|
110
|
-
lines.push("To accept, add
|
|
133
|
+
lines.push("To accept, add to .config/routes.json:");
|
|
111
134
|
const suggestedSession = sanitizeChannelPath(meta.channel ?? "unknown");
|
|
112
135
|
const otherCount = (meta.participantCount ?? 1) - 1;
|
|
113
136
|
if (otherCount > 1) {
|
|
137
|
+
lines.push(` Rule: { "channel": "${meta.channel}", "session": "${suggestedSession}" }`);
|
|
114
138
|
lines.push(
|
|
115
|
-
`
|
|
139
|
+
` Session config: "${suggestedSession}": { "batch": { "debounce": 20, "maxWait": 120 } }`,
|
|
116
140
|
);
|
|
117
141
|
lines.push(
|
|
118
142
|
`(batch recommended — ${otherCount} other participants may generate frequent messages)`,
|
|
119
143
|
);
|
|
120
144
|
} else {
|
|
121
|
-
lines.push(` { "channel": "${meta.channel}", "session": "${suggestedSession}" }`);
|
|
145
|
+
lines.push(` Rule: { "channel": "${meta.channel}", "session": "${suggestedSession}" }`);
|
|
122
146
|
}
|
|
123
|
-
lines.push(`To respond, use: volute
|
|
147
|
+
lines.push(`To respond, use: volute send ${meta.channel ?? "unknown"} "your message"`);
|
|
124
148
|
lines.push(`To reject, delete ${filePath}`);
|
|
125
149
|
return lines.join("\n");
|
|
126
150
|
}
|
|
@@ -175,15 +199,25 @@ export function createRouter(options: {
|
|
|
175
199
|
|
|
176
200
|
const lastTyping = messages[messages.length - 1]?.typing;
|
|
177
201
|
const typingSuffix = formatTypingSuffix(lastTyping);
|
|
178
|
-
|
|
202
|
+
let content: VoluteContentPart[] = [
|
|
179
203
|
{ type: "text", text: `${header}\n\n${body}${typingSuffix}` },
|
|
180
204
|
];
|
|
205
|
+
|
|
206
|
+
// Resolve session config for instructions
|
|
207
|
+
const config = options.configPath ? loadRoutingConfig(options.configPath) : {};
|
|
208
|
+
const sessionConfig = resolveSessionConfig(config, buffer.sessionName);
|
|
209
|
+
content = prependInstructions(content, sessionConfig.instructions);
|
|
210
|
+
|
|
181
211
|
const messageId = generateMessageId();
|
|
182
212
|
const handler = options.agentHandler(buffer.sessionName);
|
|
183
213
|
|
|
184
214
|
// Batch flushes are fire-and-forget — no HTTP response is waiting, so listener is a noop
|
|
185
215
|
try {
|
|
186
|
-
handler.handle(
|
|
216
|
+
handler.handle(
|
|
217
|
+
content,
|
|
218
|
+
{ sessionName: buffer.sessionName, messageId, autoReply: false },
|
|
219
|
+
() => {},
|
|
220
|
+
);
|
|
187
221
|
} catch (err) {
|
|
188
222
|
log("router", `error flushing batch for session ${buffer.sessionName}:`, err);
|
|
189
223
|
return;
|
|
@@ -250,7 +284,7 @@ export function createRouter(options: {
|
|
|
250
284
|
if (options.fileHandler) {
|
|
251
285
|
const formatted = applyPrefix(content, meta);
|
|
252
286
|
const fileHandler = options.fileHandler(filePath);
|
|
253
|
-
fileHandler.handle(formatted, { ...meta, messageId }, noop);
|
|
287
|
+
fileHandler.handle(formatted, { ...meta, messageId, autoReply: false }, noop);
|
|
254
288
|
}
|
|
255
289
|
|
|
256
290
|
// First message from this channel — send invite notification
|
|
@@ -261,7 +295,12 @@ export function createRouter(options: {
|
|
|
261
295
|
const handler = options.agentHandler("main");
|
|
262
296
|
handler.handle(
|
|
263
297
|
notifContent,
|
|
264
|
-
{
|
|
298
|
+
{
|
|
299
|
+
sessionName: "main",
|
|
300
|
+
messageId: generateMessageId(),
|
|
301
|
+
interrupt: true,
|
|
302
|
+
autoReply: false,
|
|
303
|
+
},
|
|
265
304
|
noop,
|
|
266
305
|
);
|
|
267
306
|
}
|
|
@@ -275,7 +314,11 @@ export function createRouter(options: {
|
|
|
275
314
|
if (options.fileHandler) {
|
|
276
315
|
const formatted = applyPrefix(content, meta);
|
|
277
316
|
const handler = options.fileHandler(resolved.path);
|
|
278
|
-
const unsubscribe = handler.handle(
|
|
317
|
+
const unsubscribe = handler.handle(
|
|
318
|
+
formatted,
|
|
319
|
+
{ ...meta, messageId, autoReply: false },
|
|
320
|
+
safeListener,
|
|
321
|
+
);
|
|
279
322
|
return { messageId, unsubscribe };
|
|
280
323
|
}
|
|
281
324
|
// No file handler configured — emit done and discard
|
|
@@ -290,10 +333,12 @@ export function createRouter(options: {
|
|
|
290
333
|
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
291
334
|
}
|
|
292
335
|
|
|
336
|
+
const sessionConfig = resolveSessionConfig(config, sessionName);
|
|
337
|
+
|
|
293
338
|
// Batch mode: buffer the message and return immediate done
|
|
294
|
-
if (
|
|
339
|
+
if (sessionConfig.batch != null) {
|
|
295
340
|
const batchKey = `batch:${sessionName}`;
|
|
296
|
-
const batchConfig =
|
|
341
|
+
const batchConfig = sessionConfig.batch;
|
|
297
342
|
|
|
298
343
|
if (!batchBuffers.has(batchKey)) {
|
|
299
344
|
batchBuffers.set(batchKey, {
|
|
@@ -332,10 +377,17 @@ export function createRouter(options: {
|
|
|
332
377
|
// Direct dispatch to agent
|
|
333
378
|
const formatted = applyPrefix(content, { ...meta, sessionName });
|
|
334
379
|
const withTyping = appendTypingSuffix(formatted, meta.typing);
|
|
380
|
+
const withInstructions = prependInstructions(withTyping, sessionConfig.instructions);
|
|
335
381
|
const handler = options.agentHandler(sessionName);
|
|
336
382
|
const unsubscribe = handler.handle(
|
|
337
|
-
|
|
338
|
-
{
|
|
383
|
+
withInstructions,
|
|
384
|
+
{
|
|
385
|
+
...meta,
|
|
386
|
+
sessionName,
|
|
387
|
+
messageId,
|
|
388
|
+
interrupt: sessionConfig.interrupt,
|
|
389
|
+
autoReply: sessionConfig.autoReply,
|
|
390
|
+
},
|
|
339
391
|
safeListener,
|
|
340
392
|
);
|
|
341
393
|
return { messageId, unsubscribe };
|
|
@@ -11,16 +11,29 @@ export type RoutingRule = {
|
|
|
11
11
|
session?: string;
|
|
12
12
|
destination?: "agent" | "file";
|
|
13
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
14
|
channel?: string;
|
|
17
15
|
sender?: string;
|
|
18
16
|
isDM?: boolean; // match on isDM metadata
|
|
19
17
|
participants?: number; // match on participant count (e.g. 2 = DM)
|
|
20
18
|
};
|
|
21
19
|
|
|
20
|
+
export type SessionConfig = {
|
|
21
|
+
autoReply?: boolean;
|
|
22
|
+
batch?: number | BatchConfig;
|
|
23
|
+
interrupt?: boolean;
|
|
24
|
+
instructions?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ResolvedSessionConfig = {
|
|
28
|
+
autoReply: boolean;
|
|
29
|
+
batch?: BatchConfig;
|
|
30
|
+
interrupt: boolean;
|
|
31
|
+
instructions?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
22
34
|
export type RoutingConfig = {
|
|
23
35
|
rules?: RoutingRule[];
|
|
36
|
+
sessions?: Record<string, SessionConfig>;
|
|
24
37
|
default?: string;
|
|
25
38
|
gateUnmatched?: boolean;
|
|
26
39
|
};
|
|
@@ -29,8 +42,6 @@ export type ResolvedRoute =
|
|
|
29
42
|
| {
|
|
30
43
|
destination: "agent";
|
|
31
44
|
session: string;
|
|
32
|
-
interrupt: boolean;
|
|
33
|
-
batch?: BatchConfig;
|
|
34
45
|
matched: boolean;
|
|
35
46
|
}
|
|
36
47
|
| { destination: "file"; path: string; matched: boolean };
|
|
@@ -63,7 +74,7 @@ function globMatch(pattern: string, value: string): boolean {
|
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
const GLOB_MATCH_KEYS = new Set(["channel", "sender"]);
|
|
66
|
-
const NON_MATCH_KEYS = new Set(["session", "
|
|
77
|
+
const NON_MATCH_KEYS = new Set(["session", "destination", "path"]);
|
|
67
78
|
|
|
68
79
|
type MatchMeta = { channel?: string; sender?: string; isDM?: boolean; participantCount?: number };
|
|
69
80
|
|
|
@@ -107,7 +118,7 @@ export function resolveRoute(config: RoutingConfig, meta: MatchMeta): ResolvedRo
|
|
|
107
118
|
const fallback = config.default ?? "main";
|
|
108
119
|
|
|
109
120
|
if (!config.rules) {
|
|
110
|
-
return { destination: "agent", session: fallback,
|
|
121
|
+
return { destination: "agent", session: fallback, matched: false };
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
for (const rule of config.rules) {
|
|
@@ -122,14 +133,42 @@ export function resolveRoute(config: RoutingConfig, meta: MatchMeta): ResolvedRo
|
|
|
122
133
|
return {
|
|
123
134
|
destination: "agent",
|
|
124
135
|
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
125
|
-
interrupt: rule.interrupt ?? true,
|
|
126
|
-
batch: rule.batch != null ? normalizeBatch(rule.batch) : undefined,
|
|
127
136
|
matched: true,
|
|
128
137
|
};
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
return { destination: "agent", session: fallback,
|
|
141
|
+
return { destination: "agent", session: fallback, matched: false };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolve session config by matching session name against glob-pattern keys in config.sessions.
|
|
146
|
+
* First match wins. Returns defaults if no match.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveSessionConfig(
|
|
149
|
+
config: RoutingConfig,
|
|
150
|
+
sessionName: string,
|
|
151
|
+
): ResolvedSessionConfig {
|
|
152
|
+
const defaults: ResolvedSessionConfig = { autoReply: false, interrupt: true };
|
|
153
|
+
|
|
154
|
+
if (!config.sessions) return defaults;
|
|
155
|
+
|
|
156
|
+
for (const [pattern, sessionConfig] of Object.entries(config.sessions)) {
|
|
157
|
+
if (globMatch(pattern, sessionName)) {
|
|
158
|
+
const batch = sessionConfig.batch != null ? normalizeBatch(sessionConfig.batch) : undefined;
|
|
159
|
+
if (sessionConfig.autoReply && batch != null) {
|
|
160
|
+
log("routing", `autoReply is not supported with batch mode — autoReply will be ignored`);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
autoReply: batch != null ? false : (sessionConfig.autoReply ?? false),
|
|
164
|
+
batch,
|
|
165
|
+
interrupt: sessionConfig.interrupt ?? true,
|
|
166
|
+
instructions: sessionConfig.instructions,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return defaults;
|
|
133
172
|
}
|
|
134
173
|
|
|
135
174
|
function sanitizeSessionName(name: string): string {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { log } from "./logger.js";
|
|
5
5
|
|
|
@@ -61,30 +61,6 @@ export function loadPackageInfo(): { name: string; version: string } {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export function handleMergeContext(sendMessage: (content: string) => void): boolean {
|
|
65
|
-
const mergedPath = resolve(".volute/merged.json");
|
|
66
|
-
if (!existsSync(mergedPath)) return false;
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const merged = JSON.parse(readFileSync(mergedPath, "utf-8"));
|
|
70
|
-
unlinkSync(mergedPath);
|
|
71
|
-
|
|
72
|
-
const parts = [
|
|
73
|
-
`[system] Variant "${merged.name}" has been merged and you have been restarted.`,
|
|
74
|
-
];
|
|
75
|
-
if (merged.summary) parts.push(`Changes: ${merged.summary}`);
|
|
76
|
-
if (merged.justification) parts.push(`Why: ${merged.justification}`);
|
|
77
|
-
if (merged.memory) parts.push(`Context: ${merged.memory}`);
|
|
78
|
-
|
|
79
|
-
sendMessage(parts.join("\n"));
|
|
80
|
-
log("server", `sent post-merge orientation for variant: ${merged.name}`);
|
|
81
|
-
return true;
|
|
82
|
-
} catch (e) {
|
|
83
|
-
log("server", "failed to process merged.json:", e);
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
64
|
export async function handleStartupContext(sendMessage: (content: string) => void): Promise<void> {
|
|
89
65
|
const scriptPath = resolve("home/.config/hooks/startup-context.sh");
|
|
90
66
|
if (!existsSync(scriptPath)) return;
|
|
@@ -19,6 +19,7 @@ export type ChannelMeta = {
|
|
|
19
19
|
export type HandlerMeta = ChannelMeta & {
|
|
20
20
|
messageId: string;
|
|
21
21
|
interrupt?: boolean;
|
|
22
|
+
autoReply: boolean;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
export type VoluteRequest = {
|
|
@@ -37,7 +38,7 @@ export type VoluteEvent = { messageId?: string } & (
|
|
|
37
38
|
|
|
38
39
|
export type Listener = (event: VoluteEvent) => void;
|
|
39
40
|
|
|
40
|
-
/** A handler that processes a single routed message and
|
|
41
|
+
/** A handler that processes a single routed message and emits events to a listener callback. */
|
|
41
42
|
export type MessageHandler = {
|
|
42
43
|
handle(content: VoluteContentPart[], meta: HandlerMeta, listener: Listener): () => void; // returns unsubscribe
|
|
43
44
|
};
|
|
@@ -33,25 +33,40 @@ export function createVoluteServer(options: {
|
|
|
33
33
|
try {
|
|
34
34
|
const body = JSON.parse(await readBody(req)) as VoluteRequest;
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"Cache-Control": "no-cache",
|
|
39
|
-
Connection: "keep-alive",
|
|
40
|
-
});
|
|
36
|
+
let usage: { input_tokens: number; output_tokens: number } | undefined;
|
|
37
|
+
let done = false;
|
|
41
38
|
|
|
42
39
|
const { unsubscribe } = router.route(body.content, body, (event) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
if (event.type === "usage") {
|
|
41
|
+
usage = { input_tokens: event.input_tokens, output_tokens: event.output_tokens };
|
|
42
|
+
}
|
|
43
|
+
if (event.type === "done") {
|
|
44
|
+
done = true;
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
const response: { ok: true; usage?: { input_tokens: number; output_tokens: number } } =
|
|
47
|
+
{ ok: true };
|
|
48
|
+
if (usage) response.usage = usage;
|
|
49
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
50
|
+
res.end(JSON.stringify(response));
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
const timeout = setTimeout(
|
|
55
|
+
() => {
|
|
56
|
+
if (!done) {
|
|
57
|
+
done = true;
|
|
58
|
+
unsubscribe();
|
|
59
|
+
res.writeHead(504, { "Content-Type": "application/json" });
|
|
60
|
+
res.end(JSON.stringify({ ok: false, error: "Agent processing timed out" }));
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
5 * 60 * 1000,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
res.on("close", () => {
|
|
67
|
+
clearTimeout(timeout);
|
|
68
|
+
if (!done) unsubscribe();
|
|
69
|
+
});
|
|
55
70
|
} catch (err) {
|
|
56
71
|
if (err instanceof SyntaxError) {
|
|
57
72
|
res.writeHead(400);
|