volute 0.1.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/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/channel-Q642YUZE.js +90 -0
- package/dist/chunk-5YW4B7CG.js +181 -0
- package/dist/chunk-A5ZJEMHT.js +40 -0
- package/dist/chunk-D424ZQGI.js +31 -0
- package/dist/chunk-GSPKUPKU.js +120 -0
- package/dist/chunk-H5XQARAP.js +48 -0
- package/dist/chunk-KSMIWOCN.js +84 -0
- package/dist/chunk-N4QN44LC.js +74 -0
- package/dist/chunk-XZN4WPNC.js +34 -0
- package/dist/cli.js +95 -0
- package/dist/connect-LW6G23AV.js +48 -0
- package/dist/connectors/discord.js +213 -0
- package/dist/create-3K6O2SDC.js +62 -0
- package/dist/daemon-client-ZTHW7ROS.js +10 -0
- package/dist/daemon.js +1731 -0
- package/dist/delete-JNGY7ZFH.js +54 -0
- package/dist/disconnect-ACVTKTRE.js +30 -0
- package/dist/down-FYCUYC5H.js +71 -0
- package/dist/env-7SLRN3MG.js +159 -0
- package/dist/fork-BB3DZ426.js +112 -0
- package/dist/import-W2AMTEV5.js +410 -0
- package/dist/logs-BUHRIQ2L.js +35 -0
- package/dist/merge-446QTE7Q.js +219 -0
- package/dist/schedule-KKSOVUDF.js +113 -0
- package/dist/send-WQSVSRDD.js +50 -0
- package/dist/start-LKMWS6ZE.js +29 -0
- package/dist/status-CIEKUI3V.js +50 -0
- package/dist/stop-YTOAGYE4.js +29 -0
- package/dist/up-AJJ4GCXY.js +111 -0
- package/dist/upgrade-JACA6YMO.js +211 -0
- package/dist/variants-HPY4DEWU.js +60 -0
- package/dist/web-assets/assets/index-DNNPoxMn.js +158 -0
- package/dist/web-assets/index.html +15 -0
- package/package.json +76 -0
- package/templates/_base/.init/MEMORY.md +2 -0
- package/templates/_base/.init/SOUL.md +2 -0
- package/templates/_base/.init/memory/.gitkeep +0 -0
- package/templates/_base/_skills/memory/SKILL.md +30 -0
- package/templates/_base/_skills/volute-agent/SKILL.md +53 -0
- package/templates/_base/biome.json.tmpl +21 -0
- package/templates/_base/home/VOLUTE.md +19 -0
- package/templates/_base/src/lib/auto-commit.ts +46 -0
- package/templates/_base/src/lib/logger.ts +47 -0
- package/templates/_base/src/lib/types.ts +24 -0
- package/templates/_base/src/lib/volute-server.ts +98 -0
- package/templates/_base/tsconfig.json +13 -0
- package/templates/_base/volute.json.tmpl +3 -0
- package/templates/agent-sdk/.init/CLAUDE.md +36 -0
- package/templates/agent-sdk/package.json.tmpl +20 -0
- package/templates/agent-sdk/src/lib/agent.ts +199 -0
- package/templates/agent-sdk/src/lib/hooks/auto-commit.ts +14 -0
- package/templates/agent-sdk/src/lib/hooks/identity-reload.ts +26 -0
- package/templates/agent-sdk/src/lib/hooks/pre-compact.ts +20 -0
- package/templates/agent-sdk/src/lib/message-channel.ts +37 -0
- package/templates/agent-sdk/src/server.ts +158 -0
- package/templates/agent-sdk/volute-template.json +9 -0
- package/templates/pi/.init/AGENTS.md +26 -0
- package/templates/pi/package.json.tmpl +20 -0
- package/templates/pi/src/lib/agent.ts +205 -0
- package/templates/pi/src/server.ts +121 -0
- package/templates/pi/volute-template.json +9 -0
- package/templates/pi/volute.json.tmpl +3 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { createAutoCommitHook } from "./hooks/auto-commit.js";
|
|
3
|
+
import { createIdentityReloadHook } from "./hooks/identity-reload.js";
|
|
4
|
+
import { createPreCompactHook } from "./hooks/pre-compact.js";
|
|
5
|
+
import { log, logMessage, logText, logThinking, logToolUse } from "./logger.js";
|
|
6
|
+
import { createMessageChannel } from "./message-channel.js";
|
|
7
|
+
import type { ChannelMeta, VoluteContentPart, VoluteEvent } from "./types.js";
|
|
8
|
+
|
|
9
|
+
type Listener = (event: VoluteEvent) => void;
|
|
10
|
+
|
|
11
|
+
function formatPrefix(meta: ChannelMeta | undefined, time: string): string {
|
|
12
|
+
if (!meta?.channel && !meta?.sender) return "";
|
|
13
|
+
// Use explicit platform name or capitalize from channel URI prefix
|
|
14
|
+
const platform =
|
|
15
|
+
meta.platform ??
|
|
16
|
+
(() => {
|
|
17
|
+
const n = (meta.channel ?? "").split(":")[0];
|
|
18
|
+
return n.charAt(0).toUpperCase() + n.slice(1);
|
|
19
|
+
})();
|
|
20
|
+
// Build sender context (e.g., "χθ in DM" or "χθ in #general in My Server")
|
|
21
|
+
let sender = meta.sender ?? "";
|
|
22
|
+
if (meta.isDM) {
|
|
23
|
+
sender += " in DM";
|
|
24
|
+
} else if (meta.channelName) {
|
|
25
|
+
sender += ` in #${meta.channelName}`;
|
|
26
|
+
if (meta.guildName) sender += ` in ${meta.guildName}`;
|
|
27
|
+
}
|
|
28
|
+
const parts = [platform, sender].filter(Boolean);
|
|
29
|
+
return parts.length > 0 ? `[${parts.join(": ")} — ${time}]\n` : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createAgent(options: {
|
|
33
|
+
systemPrompt: string;
|
|
34
|
+
cwd: string;
|
|
35
|
+
abortController: AbortController;
|
|
36
|
+
model?: string;
|
|
37
|
+
resume?: string;
|
|
38
|
+
onSessionId?: (id: string) => void;
|
|
39
|
+
onStreamError?: (err: unknown) => void;
|
|
40
|
+
onCompact?: () => void;
|
|
41
|
+
onIdentityReload?: () => void;
|
|
42
|
+
}) {
|
|
43
|
+
const channel = createMessageChannel();
|
|
44
|
+
const listeners = new Set<Listener>();
|
|
45
|
+
|
|
46
|
+
const autoCommit = createAutoCommitHook(options.cwd);
|
|
47
|
+
const identityReload = createIdentityReloadHook(options.cwd);
|
|
48
|
+
const preCompact = createPreCompactHook(() => {
|
|
49
|
+
if (options.onCompact) options.onCompact();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function broadcast(event: VoluteEvent) {
|
|
53
|
+
for (const listener of listeners) {
|
|
54
|
+
try {
|
|
55
|
+
listener(event);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
log("agent", "listener threw during broadcast:", err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createStream(resume?: string) {
|
|
63
|
+
return query({
|
|
64
|
+
prompt: channel.iterable,
|
|
65
|
+
options: {
|
|
66
|
+
systemPrompt: options.systemPrompt,
|
|
67
|
+
permissionMode: "bypassPermissions",
|
|
68
|
+
allowDangerouslySkipPermissions: true,
|
|
69
|
+
settingSources: ["project"],
|
|
70
|
+
cwd: options.cwd,
|
|
71
|
+
abortController: options.abortController,
|
|
72
|
+
model: options.model,
|
|
73
|
+
resume,
|
|
74
|
+
hooks: {
|
|
75
|
+
PostToolUse: [{ matcher: "Edit|Write", hooks: [autoCommit.hook, identityReload.hook] }],
|
|
76
|
+
PreCompact: [{ hooks: [preCompact.hook] }],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function consumeStream(stream: ReturnType<typeof query>) {
|
|
83
|
+
for await (const msg of stream) {
|
|
84
|
+
if ("session_id" in msg && msg.session_id && options.onSessionId) {
|
|
85
|
+
options.onSessionId(msg.session_id as string);
|
|
86
|
+
}
|
|
87
|
+
if (msg.type === "assistant") {
|
|
88
|
+
for (const b of msg.message.content) {
|
|
89
|
+
if (b.type === "thinking" && "thinking" in b && b.thinking) {
|
|
90
|
+
logThinking(b.thinking as string);
|
|
91
|
+
} else if (b.type === "text") {
|
|
92
|
+
const text = (b as { text: string }).text;
|
|
93
|
+
logText(text);
|
|
94
|
+
broadcast({ type: "text", content: text });
|
|
95
|
+
} else if (b.type === "tool_use") {
|
|
96
|
+
const tb = b as { name: string; input: unknown };
|
|
97
|
+
logToolUse(tb.name, tb.input);
|
|
98
|
+
broadcast({ type: "tool_use", name: tb.name, input: tb.input });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (msg.type === "result") {
|
|
103
|
+
log("agent", "turn done");
|
|
104
|
+
broadcast({ type: "done" });
|
|
105
|
+
if (identityReload.needsReload()) {
|
|
106
|
+
options.onIdentityReload?.();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Consume the SDK stream and broadcast VoluteEvent events
|
|
113
|
+
(async () => {
|
|
114
|
+
log("agent", "stream consumer started");
|
|
115
|
+
try {
|
|
116
|
+
await consumeStream(createStream(options.resume));
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (options.resume) {
|
|
119
|
+
log("agent", "session resume failed, starting fresh:", err);
|
|
120
|
+
if (options.onStreamError) options.onStreamError(err);
|
|
121
|
+
try {
|
|
122
|
+
await consumeStream(createStream());
|
|
123
|
+
} catch (retryErr) {
|
|
124
|
+
log("agent", "stream consumer error:", retryErr);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
log("agent", "stream consumer error:", err);
|
|
129
|
+
if (options.onStreamError) options.onStreamError(err);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
log("agent", "stream consumer ended");
|
|
134
|
+
})();
|
|
135
|
+
|
|
136
|
+
function sendMessage(content: string | VoluteContentPart[], meta?: ChannelMeta) {
|
|
137
|
+
const text =
|
|
138
|
+
typeof content === "string"
|
|
139
|
+
? content
|
|
140
|
+
: content.map((p) => (p.type === "text" ? p.text : `[${p.type}]`)).join(" ");
|
|
141
|
+
logMessage("in", text, meta?.channel);
|
|
142
|
+
|
|
143
|
+
// Build context prefix from channel metadata
|
|
144
|
+
const time = new Date().toLocaleString();
|
|
145
|
+
const prefix = formatPrefix(meta, time);
|
|
146
|
+
|
|
147
|
+
let sdkContent: (
|
|
148
|
+
| { type: "text"; text: string }
|
|
149
|
+
| {
|
|
150
|
+
type: "image";
|
|
151
|
+
source: {
|
|
152
|
+
type: "base64";
|
|
153
|
+
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
154
|
+
data: string;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
)[];
|
|
158
|
+
|
|
159
|
+
if (typeof content === "string") {
|
|
160
|
+
sdkContent = [{ type: "text" as const, text: prefix + content }];
|
|
161
|
+
} else {
|
|
162
|
+
const hasText = content.some((p) => p.type === "text");
|
|
163
|
+
sdkContent = content.map((part, i) => {
|
|
164
|
+
if (part.type === "text") {
|
|
165
|
+
return { type: "text" as const, text: (i === 0 ? prefix : "") + part.text };
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
type: "image" as const,
|
|
169
|
+
source: {
|
|
170
|
+
type: "base64" as const,
|
|
171
|
+
media_type: part.media_type as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
172
|
+
data: part.data,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
// If no text parts but we have a prefix, prepend a text part
|
|
177
|
+
if (prefix && !hasText) {
|
|
178
|
+
sdkContent.unshift({ type: "text" as const, text: prefix.trimEnd() });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
channel.push({
|
|
183
|
+
type: "user",
|
|
184
|
+
session_id: "",
|
|
185
|
+
message: {
|
|
186
|
+
role: "user",
|
|
187
|
+
content: sdkContent,
|
|
188
|
+
},
|
|
189
|
+
parent_tool_use_id: null,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function onMessage(listener: Listener): () => void {
|
|
194
|
+
listeners.add(listener);
|
|
195
|
+
return () => listeners.delete(listener);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { sendMessage, onMessage, waitForCommits: autoCommit.waitForCommits };
|
|
199
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { commitFileChange, waitForCommits } from "../auto-commit.js";
|
|
3
|
+
|
|
4
|
+
export function createAutoCommitHook(cwd: string) {
|
|
5
|
+
const hook: HookCallback = async (input) => {
|
|
6
|
+
const filePath = (input as { tool_input?: { file_path?: string } }).tool_input?.file_path;
|
|
7
|
+
if (filePath) {
|
|
8
|
+
commitFileChange(filePath, cwd);
|
|
9
|
+
}
|
|
10
|
+
return {};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return { hook, waitForCommits };
|
|
14
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
|
|
4
|
+
const IDENTITY_FILES = ["SOUL.md", "MEMORY.md", "VOLUTE.md"];
|
|
5
|
+
|
|
6
|
+
export function createIdentityReloadHook(cwd: string) {
|
|
7
|
+
let reloadNeeded = false;
|
|
8
|
+
|
|
9
|
+
const hook: HookCallback = async (input) => {
|
|
10
|
+
const filePath = (input as { tool_input?: { file_path?: string } }).tool_input?.file_path;
|
|
11
|
+
if (filePath) {
|
|
12
|
+
const resolved = resolve(cwd, filePath);
|
|
13
|
+
const fileName = resolved.slice(resolved.lastIndexOf("/") + 1);
|
|
14
|
+
if (IDENTITY_FILES.includes(fileName) && resolved.startsWith(resolve(cwd))) {
|
|
15
|
+
reloadNeeded = true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return {};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function needsReload(): boolean {
|
|
22
|
+
return reloadNeeded;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { hook, needsReload };
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { log } from "../logger.js";
|
|
3
|
+
|
|
4
|
+
export function createPreCompactHook(onCompact: () => void) {
|
|
5
|
+
let compactBlocked = false;
|
|
6
|
+
|
|
7
|
+
const hook: HookCallback = async () => {
|
|
8
|
+
if (!compactBlocked) {
|
|
9
|
+
compactBlocked = true;
|
|
10
|
+
log("agent", "blocking compaction — asking agent to update daily log first");
|
|
11
|
+
onCompact();
|
|
12
|
+
return { decision: "block" };
|
|
13
|
+
}
|
|
14
|
+
compactBlocked = false;
|
|
15
|
+
log("agent", "allowing compaction");
|
|
16
|
+
return {};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return { hook };
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
|
|
3
|
+
export type MessageChannel = {
|
|
4
|
+
push: (msg: SDKUserMessage) => void;
|
|
5
|
+
iterable: AsyncIterable<SDKUserMessage>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createMessageChannel(): MessageChannel {
|
|
9
|
+
const queue: SDKUserMessage[] = [];
|
|
10
|
+
let resolve: ((value: IteratorResult<SDKUserMessage>) => void) | null = null;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
push(msg: SDKUserMessage) {
|
|
14
|
+
if (resolve) {
|
|
15
|
+
const r = resolve;
|
|
16
|
+
resolve = null;
|
|
17
|
+
r({ value: msg, done: false });
|
|
18
|
+
} else {
|
|
19
|
+
queue.push(msg);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
iterable: {
|
|
23
|
+
[Symbol.asyncIterator]() {
|
|
24
|
+
return {
|
|
25
|
+
next(): Promise<IteratorResult<SDKUserMessage>> {
|
|
26
|
+
if (queue.length > 0) {
|
|
27
|
+
return Promise.resolve({ value: queue.shift()!, done: false });
|
|
28
|
+
}
|
|
29
|
+
return new Promise((r) => {
|
|
30
|
+
resolve = r;
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { createAgent } from "./lib/agent.js";
|
|
4
|
+
import { log } from "./lib/logger.js";
|
|
5
|
+
import { createVoluteServer } from "./lib/volute-server.js";
|
|
6
|
+
|
|
7
|
+
function parseArgs() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
let port = 4100;
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
13
|
+
port = parseInt(args[++i], 10);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { port };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadConfig(): { model?: string } {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(resolve("volute.json"), "utf-8"));
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadFile(path: string): string {
|
|
29
|
+
try {
|
|
30
|
+
return readFileSync(path, "utf-8");
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { port } = parseArgs();
|
|
37
|
+
const config = loadConfig();
|
|
38
|
+
const model = config.model;
|
|
39
|
+
if (model) {
|
|
40
|
+
log("server", `using model: ${model}`);
|
|
41
|
+
}
|
|
42
|
+
const soulPath = resolve("home/SOUL.md");
|
|
43
|
+
const memoryPath = resolve("home/MEMORY.md");
|
|
44
|
+
const volutePath = resolve("home/VOLUTE.md");
|
|
45
|
+
|
|
46
|
+
const soul = loadFile(soulPath);
|
|
47
|
+
if (!soul) {
|
|
48
|
+
console.error(`Could not read soul file: ${soulPath}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const memory = loadFile(memoryPath);
|
|
53
|
+
const volute = loadFile(volutePath);
|
|
54
|
+
|
|
55
|
+
const promptParts = [soul];
|
|
56
|
+
if (volute) promptParts.push(volute);
|
|
57
|
+
if (memory) promptParts.push(`## Memory\n\n${memory}`);
|
|
58
|
+
const systemPrompt = promptParts.join("\n\n---\n\n");
|
|
59
|
+
|
|
60
|
+
const sessionPath = resolve(".volute/session.json");
|
|
61
|
+
|
|
62
|
+
function loadSessionId(): string | undefined {
|
|
63
|
+
try {
|
|
64
|
+
const data = JSON.parse(readFileSync(sessionPath, "utf-8"));
|
|
65
|
+
return data.sessionId;
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function saveSessionId(sessionId: string) {
|
|
72
|
+
mkdirSync(dirname(sessionPath), { recursive: true });
|
|
73
|
+
writeFileSync(sessionPath, JSON.stringify({ sessionId }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let shuttingDown = false;
|
|
77
|
+
|
|
78
|
+
function deleteSessionFile() {
|
|
79
|
+
if (shuttingDown) return;
|
|
80
|
+
try {
|
|
81
|
+
unlinkSync(sessionPath);
|
|
82
|
+
log("server", "deleted session file");
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Read name/version from package.json for health endpoint
|
|
87
|
+
let pkgName = "unknown";
|
|
88
|
+
let pkgVersion = "0.0.0";
|
|
89
|
+
try {
|
|
90
|
+
const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf-8"));
|
|
91
|
+
pkgName = pkg.name || pkgName;
|
|
92
|
+
pkgVersion = pkg.version || pkgVersion;
|
|
93
|
+
} catch {}
|
|
94
|
+
|
|
95
|
+
const abortController = new AbortController();
|
|
96
|
+
const savedSessionId = loadSessionId();
|
|
97
|
+
if (savedSessionId) {
|
|
98
|
+
log("server", `resuming session: ${savedSessionId}`);
|
|
99
|
+
}
|
|
100
|
+
const agent = createAgent({
|
|
101
|
+
systemPrompt,
|
|
102
|
+
cwd: resolve("home"),
|
|
103
|
+
abortController,
|
|
104
|
+
model,
|
|
105
|
+
resume: savedSessionId,
|
|
106
|
+
onSessionId: saveSessionId,
|
|
107
|
+
onStreamError: deleteSessionFile,
|
|
108
|
+
onCompact: () => {
|
|
109
|
+
log("server", "pre-compact — asking agent to update daily log");
|
|
110
|
+
agent.sendMessage(
|
|
111
|
+
"Conversation is about to be compacted. Please update today's daily log with a summary of what we've discussed and accomplished so far, so context is preserved before compaction.",
|
|
112
|
+
"system",
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
onIdentityReload: async () => {
|
|
116
|
+
log("server", "identity file changed — restarting to reload");
|
|
117
|
+
await agent.waitForCommits();
|
|
118
|
+
server.close();
|
|
119
|
+
process.exit(0);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const server = createVoluteServer({ agent, port, name: pkgName, version: pkgVersion });
|
|
124
|
+
|
|
125
|
+
server.listen(port, () => {
|
|
126
|
+
const addr = server.address();
|
|
127
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
128
|
+
log("server", `listening on :${actualPort}`);
|
|
129
|
+
|
|
130
|
+
// Check for post-merge context
|
|
131
|
+
const mergedPath = resolve(".volute/merged.json");
|
|
132
|
+
if (existsSync(mergedPath)) {
|
|
133
|
+
try {
|
|
134
|
+
const merged = JSON.parse(readFileSync(mergedPath, "utf-8"));
|
|
135
|
+
unlinkSync(mergedPath);
|
|
136
|
+
|
|
137
|
+
const parts = [
|
|
138
|
+
`[system] Variant "${merged.name}" has been merged and you have been restarted.`,
|
|
139
|
+
];
|
|
140
|
+
if (merged.summary) parts.push(`Changes: ${merged.summary}`);
|
|
141
|
+
if (merged.justification) parts.push(`Why: ${merged.justification}`);
|
|
142
|
+
if (merged.memory) parts.push(`Context: ${merged.memory}`);
|
|
143
|
+
|
|
144
|
+
agent.sendMessage(parts.join("\n"));
|
|
145
|
+
log("server", `sent post-merge orientation for variant: ${merged.name}`);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
log("server", "failed to process merged.json:", e);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
function shutdown() {
|
|
153
|
+
shuttingDown = true;
|
|
154
|
+
log("server", "shutdown signal received");
|
|
155
|
+
process.exit(0);
|
|
156
|
+
}
|
|
157
|
+
process.on("SIGINT", shutdown);
|
|
158
|
+
process.on("SIGTERM", shutdown);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Agent Mechanics
|
|
2
|
+
|
|
3
|
+
You are a volute agent running as a persistent server. Your state is managed across sessions.
|
|
4
|
+
|
|
5
|
+
## Message Format
|
|
6
|
+
|
|
7
|
+
Messages arrive with a context prefix built by your server code:
|
|
8
|
+
```
|
|
9
|
+
[Discord: username in #general in My Server]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Memory System
|
|
13
|
+
|
|
14
|
+
Two-tier memory, both managed via file tools:
|
|
15
|
+
|
|
16
|
+
- **`MEMORY.md`** — Long-term knowledge, key decisions, learned preferences. Loaded into your system prompt on every startup. Update when you learn something worth keeping permanently.
|
|
17
|
+
- **`memory/YYYY-MM-DD.md`** — Daily logs for session-level context. The two most recent logs are included in your system prompt. Update throughout the day as you work.
|
|
18
|
+
- Periodically consolidate old daily log entries into `MEMORY.md` and clean up the daily logs.
|
|
19
|
+
|
|
20
|
+
See the **memory** skill for detailed guidance on consolidation and when to update.
|
|
21
|
+
|
|
22
|
+
## Sessions
|
|
23
|
+
|
|
24
|
+
- Your conversation may be **resumed** from a previous session. If so, context from before is preserved — orient yourself by reading your recent daily logs if needed.
|
|
25
|
+
- If this is a **fresh session**, check your memory files (`MEMORY.md` and recent daily logs in `memory/`) to recall what you've been working on.
|
|
26
|
+
- On **conversation compaction**, update today's daily log with a summary of what happened so far, so context is preserved.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "tsx watch src/server.ts",
|
|
7
|
+
"lint": "biome check src/",
|
|
8
|
+
"lint:fix": "biome check --write src/",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@mariozechner/pi-coding-agent": "^0.51.0",
|
|
13
|
+
"tsx": "^4.0.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@biomejs/biome": "2.3.14",
|
|
17
|
+
"@types/node": "^25.2.0",
|
|
18
|
+
"typescript": "^5.7.0"
|
|
19
|
+
}
|
|
20
|
+
}
|