volute 0.13.2 → 0.14.1
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/dist/{channel-JZJJRRWT.js → channel-SLURLIRV.js} +28 -28
- package/dist/{chunk-KRJ6KCBI.js → chunk-2Y77MCFG.js} +3 -3
- package/dist/{chunk-AA5TDLXB.js → chunk-3FC42ZBM.js} +24 -24
- package/dist/{chunk-YYUSXARD.js → chunk-6BDNWYKG.js} +2 -2
- package/dist/{chunk-KN4WBLH2.js → chunk-BEFIBW5B.js} +2 -2
- package/dist/{chunk-FE5O5RSL.js → chunk-GSPWIM5E.js} +25 -25
- package/dist/{chunk-QRRXD2V7.js → chunk-J52CJCVI.js} +71 -69
- package/dist/{chunk-LGSW7T7K.js → chunk-M77QBTEH.js} +60 -57
- package/dist/{chunk-KXOFPDO6.js → chunk-MVSXRMJJ.js} +1 -1
- package/dist/chunk-NAOW2CLO.js +15 -0
- package/dist/{chunk-VQIJUR43.js → chunk-OJQ47SCA.js} +1 -1
- package/dist/{chunk-O4BN3ZIY.js → chunk-OYSZNX5I.js} +7 -7
- package/dist/{chunk-AOSGW3MX.js → chunk-PDLAZJGC.js} +28 -28
- package/dist/{chunk-XUA3JUFK.js → chunk-PO5Q2AYN.js} +2 -2
- package/dist/{chunk-6BQHEIDO.js → chunk-QJIIHU32.js} +2 -2
- package/dist/{chunk-NXT67PPK.js → chunk-ZCEYUUID.js} +19 -19
- package/dist/cli.js +42 -42
- package/dist/{connector-WFT5KK67.js → connector-JFAHYFQX.js} +21 -21
- package/dist/connectors/discord.js +7 -7
- package/dist/connectors/slack.js +7 -7
- package/dist/connectors/telegram.js +9 -9
- package/dist/{create-HT47ZH5T.js → create-ZWHCRT5F.js} +7 -7
- package/dist/{daemon-client-DEF7IFEJ.js → daemon-client-ODKDUYDE.js} +2 -2
- package/dist/{daemon-restart-P3FEE3QJ.js → daemon-restart-IZGEF4NA.js} +6 -6
- package/dist/daemon.js +2313 -1997
- package/dist/{delete-YG3RVURA.js → delete-6G6WEX4F.js} +8 -8
- package/dist/down-A56B5JLK.js +14 -0
- package/dist/{env-BQYYF4YL.js → env-6LXDUZDA.js} +25 -25
- package/dist/{history-I4KIKIUX.js → history-LKCJJMUV.js} +7 -7
- package/dist/{import-UHCK6PRC.js → import-EDGRLIGO.js} +3 -3
- package/dist/{logs-2DWFES6A.js → logs-GYOR3L2L.js} +8 -8
- package/dist/mind-OJN6RBZW.js +79 -0
- package/dist/mind-manager-PN5SUDJ4.js +15 -0
- package/dist/{package-MMTPOMUN.js → package-I7Z6G44Y.js} +4 -4
- package/dist/{restart-6PE3GWYZ.js → restart-YFAWFS5T.js} +9 -9
- package/dist/{schedule-5AYTQM3N.js → schedule-AGYLDMNS.js} +17 -17
- package/dist/{seed-3QQVFMBU.js → seed-AP4Q7RZ7.js} +9 -9
- package/dist/{send-FPFW7J5Q.js → send-SV4K2TDE.js} +32 -24
- package/dist/{service-5X5EKPVM.js → service-U7MZ2H7F.js} +4 -4
- package/dist/{setup-5NXV25ZS.js → setup-DJKIZKGW.js} +21 -16
- package/dist/{sprout-VOUJ4Y3I.js → sprout-TJ3BHVOG.js} +25 -18
- package/dist/{start-ICPSQ2ZK.js → start-3YYRXBKP.js} +7 -7
- package/dist/{status-JBT7ENQN.js → status-VSFZYX7S.js} +14 -14
- package/dist/{stop-IXJGAG4T.js → stop-AA5K5LYG.js} +9 -9
- package/dist/{up-ROC7LJ7G.js → up-C4MV6EXV.js} +5 -5
- package/dist/{update-GU6JYDSN.js → update-YAGN5ODG.js} +5 -5
- package/dist/{update-check-MUPZYTW4.js → update-check-APLTH4IN.js} +2 -2
- package/dist/{upgrade-275LKIEG.js → upgrade-KXZCQSZN.js} +8 -10
- package/dist/{variant-RE45F2IY.js → variant-X5QFG6KK.js} +30 -30
- package/dist/web-assets/assets/index-CeFLp8DZ.js +307 -0
- package/dist/web-assets/index.html +1 -1
- package/drizzle/0005_rename_agents_to_minds.sql +11 -0
- package/drizzle/meta/0005_snapshot.json +410 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/templates/_base/.init/.config/scripts/session-reader.ts +1 -1
- package/templates/_base/.init/SOUL.md +1 -1
- package/templates/_base/_skills/memory/SKILL.md +1 -1
- package/templates/_base/_skills/orientation/SKILL.md +6 -6
- package/templates/_base/_skills/sessions/SKILL.md +1 -1
- package/templates/_base/_skills/{volute-agent → volute-mind}/SKILL.md +21 -21
- package/templates/_base/home/VOLUTE.md +7 -7
- package/templates/_base/src/lib/auto-commit.ts +1 -1
- package/templates/_base/src/lib/auto-reply.ts +1 -1
- package/templates/_base/src/lib/daemon-client.ts +8 -8
- package/templates/_base/src/lib/router.ts +6 -6
- package/templates/_base/src/lib/routing.ts +9 -6
- package/templates/_base/src/lib/startup.ts +1 -1
- package/templates/_base/src/lib/volute-server.ts +1 -1
- package/templates/{agent-sdk → claude}/.init/CLAUDE.md +3 -3
- package/templates/{agent-sdk → claude}/src/agent.ts +10 -10
- package/templates/{agent-sdk → claude}/src/lib/hooks/pre-compact.ts +2 -2
- package/templates/{agent-sdk → claude}/src/lib/session-store.ts +2 -2
- package/templates/{agent-sdk → claude}/src/lib/stream-consumer.ts +1 -1
- package/templates/{agent-sdk → claude}/src/server.ts +4 -4
- package/templates/pi/.init/{AGENTS.md → MINDS.md} +3 -3
- package/templates/pi/home/.config/config.json.tmpl +1 -1
- package/templates/pi/src/agent.ts +12 -12
- package/templates/pi/src/lib/event-handler.ts +39 -4
- package/templates/pi/src/server.ts +3 -3
- package/dist/agent-IUSETOXJ.js +0 -79
- package/dist/agent-manager-4O4AC2S6.js +0 -15
- package/dist/chunk-AZEL2IEK.js +0 -15
- package/dist/down-36YCOZ7V.js +0 -14
- package/dist/web-assets/assets/index-TqXd1QOX.js +0 -307
- /package/templates/{agent-sdk → claude}/.init/.claude/settings.json +0 -0
- /package/templates/{agent-sdk → claude}/.init/.config/routes.json +0 -0
- /package/templates/{agent-sdk → claude}/package.json.tmpl +0 -0
- /package/templates/{agent-sdk → claude}/src/lib/content.ts +0 -0
- /package/templates/{agent-sdk → claude}/src/lib/hooks/auto-commit.ts +0 -0
- /package/templates/{agent-sdk → claude}/src/lib/hooks/identity-reload.ts +0 -0
- /package/templates/{agent-sdk → claude}/src/lib/hooks/session-context.ts +0 -0
- /package/templates/{agent-sdk → claude}/src/lib/message-channel.ts +0 -0
- /package/templates/{agent-sdk → claude}/volute-template.json +0 -0
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# Volute
|
|
1
|
+
# Volute Mind
|
|
2
2
|
|
|
3
|
-
You are a volute
|
|
3
|
+
You are a volute mind — a persistent being that communicates across multiple channels. Messages arrive from different sources, and you can also reach out proactively.
|
|
4
4
|
|
|
5
5
|
## How to Communicate
|
|
6
6
|
|
|
7
7
|
**Your text output stays in your session — it is not sent to anyone.** To send a message, you must use the `volute send` command:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
volute send @other-
|
|
10
|
+
volute send @other-mind "hello" # DM another mind or user
|
|
11
11
|
volute send discord:server/channel "hello" # send to a channel
|
|
12
12
|
volute send animal-chat "hello" # send to a volute channel
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
This applies to everything: replying to messages, talking to other
|
|
15
|
+
This applies to everything: replying to messages, talking to other minds, and reaching out on your own initiative. Piping from stdin avoids shell escaping issues:
|
|
16
16
|
```sh
|
|
17
|
-
echo "message with 'quotes' and $special chars" | volute send @other-
|
|
17
|
+
echo "message with 'quotes' and $special chars" | volute send @other-mind
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Channels
|
|
21
21
|
|
|
22
22
|
| Channel | Shows tool calls | Notes |
|
|
23
23
|
|---------|------------------|-------|
|
|
24
|
-
| Volute | Yes | Web UI, CLI,
|
|
24
|
+
| Volute | Yes | Web UI, CLI, mind-to-mind |
|
|
25
25
|
| System | No | Automated messages (schedules, upgrades) |
|
|
26
26
|
|
|
27
27
|
Connector channels (Discord, Slack, etc.) show text only — no tool calls.
|
|
@@ -36,4 +36,4 @@ Messages from unrecognized channels are held until you add a routing rule. You'l
|
|
|
36
36
|
|
|
37
37
|
## Reference
|
|
38
38
|
|
|
39
|
-
See the **volute-
|
|
39
|
+
See the **volute-mind** skill for routing config syntax, batch options, channel management, and all CLI commands.
|
|
@@ -14,7 +14,7 @@ function exec(cmd: string, args: string[], cwd: string): Promise<{ code: number;
|
|
|
14
14
|
let pending = Promise.resolve();
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Commit a file change in the
|
|
17
|
+
* Commit a file change in the mind's home directory.
|
|
18
18
|
* Called by the PostToolUse hook when Edit or Write completes.
|
|
19
19
|
*/
|
|
20
20
|
export function commitFileChange(filePath: string, cwd: string): void {
|
|
@@ -21,7 +21,7 @@ export function createAutoReplyTracker(
|
|
|
21
21
|
const info = currentMessageId ? messageChannels.get(currentMessageId) : undefined;
|
|
22
22
|
if (info?.autoReply && info.channel) {
|
|
23
23
|
daemonSend(info.channel, text).catch((err) => {
|
|
24
|
-
log("
|
|
24
|
+
log("mind", `auto-reply to ${info.channel} failed: ${err}`);
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const port = process.env.VOLUTE_DAEMON_PORT;
|
|
2
|
-
const
|
|
2
|
+
const mind = process.env.VOLUTE_MIND;
|
|
3
3
|
const token = process.env.VOLUTE_DAEMON_TOKEN;
|
|
4
4
|
|
|
5
5
|
function headers(): Record<string, string> {
|
|
@@ -14,12 +14,12 @@ export async function daemonRestart(context?: {
|
|
|
14
14
|
type: string;
|
|
15
15
|
[k: string]: unknown;
|
|
16
16
|
}): Promise<void> {
|
|
17
|
-
if (!port || !
|
|
18
|
-
console.error("[volute] daemonRestart: VOLUTE_DAEMON_PORT or
|
|
17
|
+
if (!port || !mind) {
|
|
18
|
+
console.error("[volute] daemonRestart: VOLUTE_DAEMON_PORT or VOLUTE_MIND not set");
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
try {
|
|
22
|
-
await fetch(`http://127.0.0.1:${port}/api/
|
|
22
|
+
await fetch(`http://127.0.0.1:${port}/api/minds/${encodeURIComponent(mind)}/restart`, {
|
|
23
23
|
method: "POST",
|
|
24
24
|
headers: headers(),
|
|
25
25
|
body: JSON.stringify({ context }),
|
|
@@ -30,19 +30,19 @@ export async function daemonRestart(context?: {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export async function daemonSend(channel: string, text: string): Promise<void> {
|
|
33
|
-
if (!port || !
|
|
34
|
-
console.error("[volute] daemonSend: VOLUTE_DAEMON_PORT or
|
|
33
|
+
if (!port || !mind) {
|
|
34
|
+
console.error("[volute] daemonSend: VOLUTE_DAEMON_PORT or VOLUTE_MIND not set");
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
const res = await fetch(
|
|
38
|
-
`http://127.0.0.1:${port}/api/
|
|
38
|
+
`http://127.0.0.1:${port}/api/minds/${encodeURIComponent(mind)}/message`,
|
|
39
39
|
{
|
|
40
40
|
method: "POST",
|
|
41
41
|
headers: headers(),
|
|
42
42
|
body: JSON.stringify({
|
|
43
43
|
content: text,
|
|
44
44
|
channel,
|
|
45
|
-
sender:
|
|
45
|
+
sender: mind,
|
|
46
46
|
}),
|
|
47
47
|
},
|
|
48
48
|
);
|
|
@@ -151,7 +151,7 @@ function formatInviteNotification(
|
|
|
151
151
|
|
|
152
152
|
export function createRouter(options: {
|
|
153
153
|
configPath?: string;
|
|
154
|
-
|
|
154
|
+
mindHandler: HandlerResolver;
|
|
155
155
|
fileHandler?: HandlerResolver;
|
|
156
156
|
}): Router {
|
|
157
157
|
const batchBuffers = new Map<string, BatchBuffer>();
|
|
@@ -209,7 +209,7 @@ export function createRouter(options: {
|
|
|
209
209
|
content = prependInstructions(content, sessionConfig.instructions);
|
|
210
210
|
|
|
211
211
|
const messageId = generateMessageId();
|
|
212
|
-
const handler = options.
|
|
212
|
+
const handler = options.mindHandler(buffer.sessionName);
|
|
213
213
|
|
|
214
214
|
// Batch flushes are fire-and-forget — no HTTP response is waiting, so listener is a noop
|
|
215
215
|
try {
|
|
@@ -292,7 +292,7 @@ export function createRouter(options: {
|
|
|
292
292
|
pendingChannels.add(channelKey);
|
|
293
293
|
const notification = formatInviteNotification(meta, filePath, text);
|
|
294
294
|
const notifContent: VoluteContentPart[] = [{ type: "text", text: notification }];
|
|
295
|
-
const handler = options.
|
|
295
|
+
const handler = options.mindHandler("main");
|
|
296
296
|
handler.handle(
|
|
297
297
|
notifContent,
|
|
298
298
|
{
|
|
@@ -327,7 +327,7 @@ export function createRouter(options: {
|
|
|
327
327
|
return { messageId, unsubscribe: noop };
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
//
|
|
330
|
+
// Mind destination
|
|
331
331
|
let sessionName = resolved.session;
|
|
332
332
|
if (sessionName === "$new") {
|
|
333
333
|
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -374,11 +374,11 @@ export function createRouter(options: {
|
|
|
374
374
|
return { messageId, unsubscribe: noop };
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
// Direct dispatch to
|
|
377
|
+
// Direct dispatch to mind
|
|
378
378
|
const formatted = applyPrefix(content, { ...meta, sessionName });
|
|
379
379
|
const withTyping = appendTypingSuffix(formatted, meta.typing);
|
|
380
380
|
const withInstructions = prependInstructions(withTyping, sessionConfig.instructions);
|
|
381
|
-
const handler = options.
|
|
381
|
+
const handler = options.mindHandler(sessionName);
|
|
382
382
|
const unsubscribe = handler.handle(
|
|
383
383
|
withInstructions,
|
|
384
384
|
{
|
|
@@ -9,7 +9,7 @@ export type BatchConfig = {
|
|
|
9
9
|
|
|
10
10
|
export type RoutingRule = {
|
|
11
11
|
session?: string;
|
|
12
|
-
destination?: "
|
|
12
|
+
destination?: "mind" | "file";
|
|
13
13
|
path?: string; // file path for file destination
|
|
14
14
|
channel?: string;
|
|
15
15
|
sender?: string;
|
|
@@ -40,7 +40,7 @@ export type RoutingConfig = {
|
|
|
40
40
|
|
|
41
41
|
export type ResolvedRoute =
|
|
42
42
|
| {
|
|
43
|
-
destination: "
|
|
43
|
+
destination: "mind";
|
|
44
44
|
session: string;
|
|
45
45
|
matched: boolean;
|
|
46
46
|
}
|
|
@@ -54,7 +54,10 @@ export function normalizeBatch(batch: number | BatchConfig): BatchConfig {
|
|
|
54
54
|
|
|
55
55
|
export function loadRoutingConfig(configPath: string): RoutingConfig {
|
|
56
56
|
try {
|
|
57
|
-
|
|
57
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
58
|
+
// Normalize flat arrays (e.g. [{channel, session}, ...]) to { rules: [...] }
|
|
59
|
+
if (Array.isArray(parsed)) return { rules: parsed };
|
|
60
|
+
return parsed;
|
|
58
61
|
} catch (err: any) {
|
|
59
62
|
if (err?.code !== "ENOENT") {
|
|
60
63
|
log("routing", `failed to load ${configPath}:`, err);
|
|
@@ -118,7 +121,7 @@ export function resolveRoute(config: RoutingConfig, meta: MatchMeta): ResolvedRo
|
|
|
118
121
|
const fallback = config.default ?? "main";
|
|
119
122
|
|
|
120
123
|
if (!config.rules) {
|
|
121
|
-
return { destination: "
|
|
124
|
+
return { destination: "mind", session: fallback, matched: false };
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
for (const rule of config.rules) {
|
|
@@ -131,14 +134,14 @@ export function resolveRoute(config: RoutingConfig, meta: MatchMeta): ResolvedRo
|
|
|
131
134
|
return { destination: "file", path: rule.path, matched: true };
|
|
132
135
|
}
|
|
133
136
|
return {
|
|
134
|
-
destination: "
|
|
137
|
+
destination: "mind",
|
|
135
138
|
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
136
139
|
matched: true,
|
|
137
140
|
};
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
|
|
141
|
-
return { destination: "
|
|
144
|
+
return { destination: "mind", session: fallback, matched: false };
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
/**
|
|
@@ -17,7 +17,7 @@ export function parseArgs(): { port: number } {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function loadConfig(): { model?: string; compactionMessage?: string } {
|
|
20
|
-
//
|
|
20
|
+
// Mind-own config lives in config.json; fall back to volute.json for older minds
|
|
21
21
|
for (const file of ["home/.config/config.json", "home/.config/volute.json"]) {
|
|
22
22
|
try {
|
|
23
23
|
return JSON.parse(readFileSync(resolve(file), "utf-8"));
|
|
@@ -57,7 +57,7 @@ export function createVoluteServer(options: {
|
|
|
57
57
|
done = true;
|
|
58
58
|
unsubscribe();
|
|
59
59
|
res.writeHead(504, { "Content-Type": "application/json" });
|
|
60
|
-
res.end(JSON.stringify({ ok: false, error: "
|
|
60
|
+
res.end(JSON.stringify({ ok: false, error: "Mind processing timed out" }));
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
5 * 60 * 1000,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mind Mechanics
|
|
2
2
|
|
|
3
|
-
You are an autonomous
|
|
3
|
+
You are an autonomous mind 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
|
|
|
@@ -9,7 +9,7 @@ Messages arrive with a context prefix:
|
|
|
9
9
|
[Discord: username in #general in My Server — 1/15/2025, 10:30:00 AM]
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
You can also reach out proactively — see the **volute-
|
|
12
|
+
You can also reach out proactively — see the **volute-mind** skill.
|
|
13
13
|
|
|
14
14
|
## Identity Files
|
|
15
15
|
|
|
@@ -34,7 +34,7 @@ type Session = {
|
|
|
34
34
|
autoReply: AutoReplyTracker;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
export function
|
|
37
|
+
export function createMind(options: {
|
|
38
38
|
systemPrompt: string;
|
|
39
39
|
cwd: string;
|
|
40
40
|
abortController: AbortController;
|
|
@@ -66,7 +66,7 @@ export function createAgent(options: {
|
|
|
66
66
|
try {
|
|
67
67
|
listener(tagged);
|
|
68
68
|
} catch (err) {
|
|
69
|
-
log("
|
|
69
|
+
log("mind", "listener threw during broadcast:", err);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -116,7 +116,7 @@ export function createAgent(options: {
|
|
|
116
116
|
|
|
117
117
|
function startSession(session: Session, savedSessionId?: string) {
|
|
118
118
|
(async () => {
|
|
119
|
-
log("
|
|
119
|
+
log("mind", `session "${session.name}": stream consumer started`);
|
|
120
120
|
const callbacks = {
|
|
121
121
|
onSessionId: (id: string) => {
|
|
122
122
|
if (!session.name.startsWith("new-")) sessionStore.save(session.name, id);
|
|
@@ -141,7 +141,7 @@ export function createAgent(options: {
|
|
|
141
141
|
session.autoReply.reset();
|
|
142
142
|
session.messageChannels.clear();
|
|
143
143
|
if (savedSessionId) {
|
|
144
|
-
log("
|
|
144
|
+
log("mind", `session "${session.name}": resume failed, starting fresh:`, err);
|
|
145
145
|
sessionStore.delete(session.name);
|
|
146
146
|
try {
|
|
147
147
|
const q = createStream(session);
|
|
@@ -154,17 +154,17 @@ export function createAgent(options: {
|
|
|
154
154
|
session.currentMessageId = undefined;
|
|
155
155
|
}
|
|
156
156
|
} catch (retryErr) {
|
|
157
|
-
log("
|
|
157
|
+
log("mind", `session "${session.name}": stream consumer error:`, retryErr);
|
|
158
158
|
broadcastToSession(session, { type: "done" });
|
|
159
159
|
sessions.delete(session.name);
|
|
160
160
|
}
|
|
161
161
|
} else {
|
|
162
|
-
log("
|
|
162
|
+
log("mind", `session "${session.name}": stream consumer error:`, err);
|
|
163
163
|
broadcastToSession(session, { type: "done" });
|
|
164
164
|
sessions.delete(session.name);
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
-
log("
|
|
167
|
+
log("mind", `session "${session.name}": stream consumer ended`);
|
|
168
168
|
})();
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -186,9 +186,9 @@ export function createAgent(options: {
|
|
|
186
186
|
const isEphemeral = name.startsWith("new-");
|
|
187
187
|
const savedSessionId = isEphemeral ? undefined : sessionStore.load(name);
|
|
188
188
|
if (savedSessionId) {
|
|
189
|
-
log("
|
|
189
|
+
log("mind", `session "${name}": resuming ${savedSessionId}`);
|
|
190
190
|
} else {
|
|
191
|
-
log("
|
|
191
|
+
log("mind", `session "${name}": starting fresh`);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
startSession(session, savedSessionId);
|
|
@@ -218,7 +218,7 @@ export function createAgent(options: {
|
|
|
218
218
|
|
|
219
219
|
// Interrupt if requested and session is mid-turn
|
|
220
220
|
if (meta.interrupt && session.currentMessageId !== undefined && session.currentQuery) {
|
|
221
|
-
log("
|
|
221
|
+
log("mind", `session "${sessionName}": interrupting current turn`);
|
|
222
222
|
session.currentQuery.interrupt();
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -7,12 +7,12 @@ export function createPreCompactHook(onCompact: () => void) {
|
|
|
7
7
|
const hook: HookCallback = async () => {
|
|
8
8
|
if (!compactBlocked) {
|
|
9
9
|
compactBlocked = true;
|
|
10
|
-
log("
|
|
10
|
+
log("mind", "blocking compaction — asking mind to update daily log first");
|
|
11
11
|
onCompact();
|
|
12
12
|
return { decision: "block" };
|
|
13
13
|
}
|
|
14
14
|
compactBlocked = false;
|
|
15
|
-
log("
|
|
15
|
+
log("mind", "allowing compaction");
|
|
16
16
|
return {};
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ export function createSessionStore(sessionsDir: string): SessionStore {
|
|
|
20
20
|
return typeof data.sessionId === "string" ? data.sessionId : undefined;
|
|
21
21
|
} catch (err: any) {
|
|
22
22
|
if (err?.code !== "ENOENT") {
|
|
23
|
-
log("
|
|
23
|
+
log("mind", `failed to load session file for "${name}":`, err);
|
|
24
24
|
}
|
|
25
25
|
return undefined;
|
|
26
26
|
}
|
|
@@ -36,7 +36,7 @@ export function createSessionStore(sessionsDir: string): SessionStore {
|
|
|
36
36
|
const path = filePath(name);
|
|
37
37
|
if (existsSync(path)) unlinkSync(path);
|
|
38
38
|
} catch (err) {
|
|
39
|
-
log("
|
|
39
|
+
log("mind", `failed to delete session file for "${name}":`, err);
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
};
|
|
@@ -49,7 +49,7 @@ export async function consumeStream(
|
|
|
49
49
|
if (session.currentMessageId) {
|
|
50
50
|
session.messageChannels.delete(session.currentMessageId);
|
|
51
51
|
}
|
|
52
|
-
log("
|
|
52
|
+
log("mind", `session "${session.name}": turn done`);
|
|
53
53
|
const result = msg as { usage?: { input_tokens?: number; output_tokens?: number } };
|
|
54
54
|
if (result.usage) {
|
|
55
55
|
callbacks.broadcast({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { createMind } from "./agent.js";
|
|
4
4
|
import { daemonRestart } from "./lib/daemon-client.js";
|
|
5
5
|
import { createFileHandlerResolver } from "./lib/file-handler.js";
|
|
6
6
|
import { log } from "./lib/logger.js";
|
|
@@ -32,7 +32,7 @@ if (existsSync(oldSessionPath) && !existsSync(resolve(sessionsDir, "main.json"))
|
|
|
32
32
|
|
|
33
33
|
const pkg = loadPackageInfo();
|
|
34
34
|
const abortController = new AbortController();
|
|
35
|
-
const
|
|
35
|
+
const mind = createMind({
|
|
36
36
|
systemPrompt,
|
|
37
37
|
cwd: resolve("home"),
|
|
38
38
|
abortController,
|
|
@@ -42,14 +42,14 @@ const agent = createAgent({
|
|
|
42
42
|
compactionMessage: config.compactionMessage,
|
|
43
43
|
onIdentityReload: async () => {
|
|
44
44
|
log("server", "identity file changed — restarting to reload");
|
|
45
|
-
await
|
|
45
|
+
await mind.waitForCommits();
|
|
46
46
|
await daemonRestart({ type: "reload" });
|
|
47
47
|
},
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
const router = createRouter({
|
|
51
51
|
configPath: resolve("home/.config/routes.json"),
|
|
52
|
-
|
|
52
|
+
mindHandler: mind.resolve,
|
|
53
53
|
fileHandler: createFileHandlerResolver(resolve("home")),
|
|
54
54
|
});
|
|
55
55
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Mind Mechanics
|
|
2
2
|
|
|
3
|
-
You are an autonomous
|
|
3
|
+
You are an autonomous mind running as a persistent server. Your working directory, identity, memory, and server code are all yours to understand and modify. Your state is managed across sessions.
|
|
4
4
|
|
|
5
5
|
## Message Format
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ Messages arrive with a context prefix:
|
|
|
9
9
|
[Discord: username in #general in My Server — 1/15/2025, 10:30:00 AM]
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
You can also reach out proactively — see the **volute-
|
|
12
|
+
You can also reach out proactively — see the **volute-mind** skill.
|
|
13
13
|
|
|
14
14
|
## Memory System
|
|
15
15
|
|
|
@@ -26,11 +26,11 @@ import type {
|
|
|
26
26
|
VoluteEvent,
|
|
27
27
|
} from "./lib/types.js";
|
|
28
28
|
|
|
29
|
-
type
|
|
29
|
+
type PiAgentSession = Awaited<ReturnType<typeof createAgentSession>>["session"];
|
|
30
30
|
|
|
31
31
|
type PiSession = {
|
|
32
32
|
name: string;
|
|
33
|
-
agentSession:
|
|
33
|
+
agentSession: PiAgentSession | null;
|
|
34
34
|
ready: Promise<void>;
|
|
35
35
|
listeners: Set<Listener>;
|
|
36
36
|
unsubscribe?: () => void;
|
|
@@ -45,7 +45,7 @@ function defaultCompactionMessage(): string {
|
|
|
45
45
|
return `Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${today}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function
|
|
48
|
+
export function createMind(options: {
|
|
49
49
|
systemPrompt: string;
|
|
50
50
|
cwd: string;
|
|
51
51
|
model?: string;
|
|
@@ -82,7 +82,7 @@ export function createAgent(options: {
|
|
|
82
82
|
session.ready = initSession(session).catch((err) => {
|
|
83
83
|
session.autoReply.reset();
|
|
84
84
|
session.messageChannels.clear();
|
|
85
|
-
log("
|
|
85
|
+
log("mind", `session "${session.name}": init failed:`, err);
|
|
86
86
|
});
|
|
87
87
|
return session;
|
|
88
88
|
}
|
|
@@ -94,7 +94,7 @@ export function createAgent(options: {
|
|
|
94
94
|
? SessionManager.inMemory()
|
|
95
95
|
: SessionManager.continueRecent(options.cwd, `.volute/pi-sessions/${session.name}`);
|
|
96
96
|
|
|
97
|
-
log("
|
|
97
|
+
log("mind", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
|
|
98
98
|
|
|
99
99
|
let compactBlocked = false;
|
|
100
100
|
const preCompactExtension: ExtensionFactory = (pi) => {
|
|
@@ -102,15 +102,15 @@ export function createAgent(options: {
|
|
|
102
102
|
if (!compactBlocked) {
|
|
103
103
|
compactBlocked = true;
|
|
104
104
|
log(
|
|
105
|
-
"
|
|
106
|
-
`session "${session.name}": blocking compaction — asking
|
|
105
|
+
"mind",
|
|
106
|
+
`session "${session.name}": blocking compaction — asking mind to update daily log`,
|
|
107
107
|
);
|
|
108
108
|
session.messageIds.push(undefined);
|
|
109
109
|
session.agentSession?.prompt(compactionMessage, { streamingBehavior: "followUp" });
|
|
110
110
|
return { cancel: true };
|
|
111
111
|
}
|
|
112
112
|
compactBlocked = false;
|
|
113
|
-
log("
|
|
113
|
+
log("mind", `session "${session.name}": allowing compaction`);
|
|
114
114
|
});
|
|
115
115
|
};
|
|
116
116
|
|
|
@@ -151,7 +151,7 @@ export function createAgent(options: {
|
|
|
151
151
|
}),
|
|
152
152
|
);
|
|
153
153
|
|
|
154
|
-
log("
|
|
154
|
+
log("mind", `session "${session.name}": ready`);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// --- Event broadcasting ---
|
|
@@ -163,7 +163,7 @@ export function createAgent(options: {
|
|
|
163
163
|
try {
|
|
164
164
|
listener(tagged);
|
|
165
165
|
} catch (err) {
|
|
166
|
-
log("
|
|
166
|
+
log("mind", "listener threw during broadcast:", err);
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
}
|
|
@@ -171,7 +171,7 @@ export function createAgent(options: {
|
|
|
171
171
|
function interruptSession(name: string) {
|
|
172
172
|
const session = sessions.get(name);
|
|
173
173
|
if (session?.currentMessageId !== undefined) {
|
|
174
|
-
log("
|
|
174
|
+
log("mind", `session "${name}": interrupting current turn`);
|
|
175
175
|
broadcast(session, { type: "done" });
|
|
176
176
|
session.currentMessageId = undefined;
|
|
177
177
|
}
|
|
@@ -219,7 +219,7 @@ export function createAgent(options: {
|
|
|
219
219
|
session.agentSession!.prompt(text, opts);
|
|
220
220
|
}
|
|
221
221
|
})().catch((err) => {
|
|
222
|
-
log("
|
|
222
|
+
log("mind", `session "${sessionName}": prompt failed:`, err);
|
|
223
223
|
broadcast(session, { type: "done" });
|
|
224
224
|
});
|
|
225
225
|
|
|
@@ -18,10 +18,32 @@ export type EventHandlerOptions = {
|
|
|
18
18
|
|
|
19
19
|
export function createEventHandler(session: EventSession, options: EventHandlerOptions) {
|
|
20
20
|
const toolArgs = new Map<string, any>();
|
|
21
|
+
let textBuf = "";
|
|
22
|
+
let thinkingBuf = "";
|
|
23
|
+
|
|
24
|
+
function flushText() {
|
|
25
|
+
if (textBuf) {
|
|
26
|
+
logText(textBuf);
|
|
27
|
+
textBuf = "";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function flushThinking() {
|
|
32
|
+
if (thinkingBuf) {
|
|
33
|
+
logThinking(thinkingBuf);
|
|
34
|
+
thinkingBuf = "";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function flushBuffers() {
|
|
39
|
+
flushThinking();
|
|
40
|
+
flushText();
|
|
41
|
+
}
|
|
21
42
|
|
|
22
43
|
return (event: any) => {
|
|
23
44
|
try {
|
|
24
45
|
if (session.currentMessageId === undefined) {
|
|
46
|
+
flushBuffers(); // flush any leftover from a turn that ended without agent_end
|
|
25
47
|
session.currentMessageId = session.messageIds.shift();
|
|
26
48
|
session.autoReply.reset();
|
|
27
49
|
}
|
|
@@ -29,14 +51,26 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
29
51
|
if (event.type === "message_update") {
|
|
30
52
|
const ae = event.assistantMessageEvent;
|
|
31
53
|
if (ae.type === "text_delta") {
|
|
32
|
-
|
|
54
|
+
if (thinkingBuf) flushThinking();
|
|
55
|
+
textBuf += ae.delta;
|
|
33
56
|
session.autoReply.accumulate(ae.delta);
|
|
57
|
+
// Log complete lines as they arrive
|
|
58
|
+
for (let nl = textBuf.indexOf("\n"); nl !== -1; nl = textBuf.indexOf("\n")) {
|
|
59
|
+
logText(textBuf.slice(0, nl + 1));
|
|
60
|
+
textBuf = textBuf.slice(nl + 1);
|
|
61
|
+
}
|
|
34
62
|
} else if (ae.type === "thinking_delta") {
|
|
35
|
-
|
|
63
|
+
if (textBuf) flushText();
|
|
64
|
+
thinkingBuf += ae.delta;
|
|
65
|
+
for (let nl = thinkingBuf.indexOf("\n"); nl !== -1; nl = thinkingBuf.indexOf("\n")) {
|
|
66
|
+
logThinking(thinkingBuf.slice(0, nl + 1));
|
|
67
|
+
thinkingBuf = thinkingBuf.slice(nl + 1);
|
|
68
|
+
}
|
|
36
69
|
}
|
|
37
70
|
}
|
|
38
71
|
|
|
39
72
|
if (event.type === "tool_execution_start") {
|
|
73
|
+
flushBuffers();
|
|
40
74
|
session.autoReply.flush(session.currentMessageId);
|
|
41
75
|
toolArgs.set(event.toolCallId, event.args);
|
|
42
76
|
logToolUse(event.toolName, event.args);
|
|
@@ -59,16 +93,17 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
59
93
|
}
|
|
60
94
|
|
|
61
95
|
if (event.type === "agent_end") {
|
|
96
|
+
flushBuffers();
|
|
62
97
|
session.autoReply.flush(session.currentMessageId);
|
|
63
98
|
if (session.currentMessageId) {
|
|
64
99
|
session.messageChannels.delete(session.currentMessageId);
|
|
65
100
|
}
|
|
66
|
-
log("
|
|
101
|
+
log("mind", `session "${session.name}": turn done`);
|
|
67
102
|
options.broadcast({ type: "done" });
|
|
68
103
|
session.currentMessageId = undefined;
|
|
69
104
|
}
|
|
70
105
|
} catch (err) {
|
|
71
|
-
log("
|
|
106
|
+
log("mind", `session "${session.name}": event handler error (${event?.type}):`, err);
|
|
72
107
|
}
|
|
73
108
|
};
|
|
74
109
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { createMind } from "./agent.js";
|
|
3
3
|
import { createFileHandlerResolver } from "./lib/file-handler.js";
|
|
4
4
|
import { log } from "./lib/logger.js";
|
|
5
5
|
import { createRouter } from "./lib/router.js";
|
|
@@ -21,7 +21,7 @@ if (config.thinkingLevel) log("server", `thinking level: ${config.thinkingLevel}
|
|
|
21
21
|
const systemPrompt = loadSystemPrompt();
|
|
22
22
|
const pkg = loadPackageInfo();
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const mind = createMind({
|
|
25
25
|
systemPrompt,
|
|
26
26
|
cwd: resolve("home"),
|
|
27
27
|
model: config.model,
|
|
@@ -31,7 +31,7 @@ const agent = createAgent({
|
|
|
31
31
|
|
|
32
32
|
const router = createRouter({
|
|
33
33
|
configPath: resolve("home/.config/routes.json"),
|
|
34
|
-
|
|
34
|
+
mindHandler: mind.resolve,
|
|
35
35
|
fileHandler: createFileHandlerResolver(resolve("home")),
|
|
36
36
|
});
|
|
37
37
|
|