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
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { resolve as resolvePath } from "node:path";
|
|
3
1
|
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
4
2
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
import {
|
|
4
|
+
type AutoReplyTracker,
|
|
5
|
+
createAutoReplyTracker,
|
|
6
|
+
type MessageChannelInfo,
|
|
7
|
+
} from "./lib/auto-reply.js";
|
|
8
|
+
import { toSDKContent } from "./lib/content.js";
|
|
5
9
|
import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
|
|
6
10
|
import { createIdentityReloadHook } from "./lib/hooks/identity-reload.js";
|
|
7
11
|
import { createPreCompactHook } from "./lib/hooks/pre-compact.js";
|
|
8
12
|
import { createSessionContextHook } from "./lib/hooks/session-context.js";
|
|
9
|
-
import { log
|
|
13
|
+
import { log } from "./lib/logger.js";
|
|
10
14
|
import { createMessageChannel } from "./lib/message-channel.js";
|
|
15
|
+
import { createSessionStore } from "./lib/session-store.js";
|
|
16
|
+
import { consumeStream } from "./lib/stream-consumer.js";
|
|
11
17
|
import type {
|
|
12
18
|
HandlerMeta,
|
|
13
19
|
HandlerResolver,
|
|
@@ -17,18 +23,6 @@ import type {
|
|
|
17
23
|
VoluteEvent,
|
|
18
24
|
} from "./lib/types.js";
|
|
19
25
|
|
|
20
|
-
type SDKContent = (
|
|
21
|
-
| { type: "text"; text: string }
|
|
22
|
-
| {
|
|
23
|
-
type: "image";
|
|
24
|
-
source: {
|
|
25
|
-
type: "base64";
|
|
26
|
-
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
27
|
-
data: string;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
)[];
|
|
31
|
-
|
|
32
26
|
type Session = {
|
|
33
27
|
name: string;
|
|
34
28
|
channel: ReturnType<typeof createMessageChannel>;
|
|
@@ -36,35 +30,23 @@ type Session = {
|
|
|
36
30
|
messageIds: (string | undefined)[];
|
|
37
31
|
currentMessageId?: string;
|
|
38
32
|
currentQuery?: ReturnType<typeof query>;
|
|
33
|
+
messageChannels: Map<string, MessageChannelInfo>;
|
|
34
|
+
autoReply: AutoReplyTracker;
|
|
39
35
|
};
|
|
40
36
|
|
|
41
|
-
function toSDKContent(content: VoluteContentPart[]): SDKContent {
|
|
42
|
-
return content.map((part) => {
|
|
43
|
-
if (part.type === "text") {
|
|
44
|
-
return { type: "text" as const, text: part.text };
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
type: "image" as const,
|
|
48
|
-
source: {
|
|
49
|
-
type: "base64" as const,
|
|
50
|
-
media_type: part.media_type as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
51
|
-
data: part.data,
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
37
|
export function createAgent(options: {
|
|
58
38
|
systemPrompt: string;
|
|
59
39
|
cwd: string;
|
|
60
40
|
abortController: AbortController;
|
|
61
41
|
model?: string;
|
|
42
|
+
maxThinkingTokens?: number;
|
|
62
43
|
sessionsDir: string;
|
|
63
44
|
compactionMessage?: string;
|
|
64
45
|
onIdentityReload?: () => Promise<void>;
|
|
65
46
|
}): { resolve: HandlerResolver; waitForCommits: () => Promise<void> } {
|
|
66
47
|
const autoCommit = createAutoCommitHook(options.cwd);
|
|
67
48
|
const identityReload = createIdentityReloadHook(options.cwd);
|
|
49
|
+
const sessionStore = createSessionStore(options.sessionsDir);
|
|
68
50
|
const postToolUseHooks: { matcher: string; hooks: HookCallback[] }[] = [
|
|
69
51
|
{ matcher: "Edit|Write", hooks: [autoCommit.hook, identityReload.hook] },
|
|
70
52
|
];
|
|
@@ -75,38 +57,6 @@ export function createAgent(options: {
|
|
|
75
57
|
options.compactionMessage ??
|
|
76
58
|
`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.`;
|
|
77
59
|
|
|
78
|
-
// --- Session persistence ---
|
|
79
|
-
|
|
80
|
-
function sessionFilePath(sessionName: string): string {
|
|
81
|
-
return resolvePath(options.sessionsDir, `${sessionName}.json`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function loadSessionId(sessionName: string): string | undefined {
|
|
85
|
-
try {
|
|
86
|
-
const data = JSON.parse(readFileSync(sessionFilePath(sessionName), "utf-8"));
|
|
87
|
-
return data.sessionId;
|
|
88
|
-
} catch (err: any) {
|
|
89
|
-
if (err?.code !== "ENOENT") {
|
|
90
|
-
log("agent", `failed to load session file for "${sessionName}":`, err);
|
|
91
|
-
}
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function saveSessionId(sessionName: string, sessionId: string) {
|
|
97
|
-
mkdirSync(options.sessionsDir, { recursive: true });
|
|
98
|
-
writeFileSync(sessionFilePath(sessionName), JSON.stringify({ sessionId }));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function deleteSessionId(sessionName: string) {
|
|
102
|
-
try {
|
|
103
|
-
const path = sessionFilePath(sessionName);
|
|
104
|
-
if (existsSync(path)) unlinkSync(path);
|
|
105
|
-
} catch (err) {
|
|
106
|
-
log("agent", `failed to delete session file for "${sessionName}":`, err);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
60
|
// --- Event broadcasting ---
|
|
111
61
|
|
|
112
62
|
function broadcastToSession(session: Session, event: VoluteEvent) {
|
|
@@ -153,6 +103,7 @@ export function createAgent(options: {
|
|
|
153
103
|
cwd: options.cwd,
|
|
154
104
|
abortController: options.abortController,
|
|
155
105
|
model: options.model,
|
|
106
|
+
maxThinkingTokens: options.maxThinkingTokens,
|
|
156
107
|
resume,
|
|
157
108
|
hooks: {
|
|
158
109
|
PostToolUse: postToolUseHooks,
|
|
@@ -163,65 +114,45 @@ export function createAgent(options: {
|
|
|
163
114
|
});
|
|
164
115
|
}
|
|
165
116
|
|
|
166
|
-
async function consumeStream(stream: ReturnType<typeof query>, session: Session) {
|
|
167
|
-
for await (const msg of stream) {
|
|
168
|
-
if (session.currentMessageId === undefined) {
|
|
169
|
-
session.currentMessageId = session.messageIds.shift();
|
|
170
|
-
}
|
|
171
|
-
if ("session_id" in msg && msg.session_id) {
|
|
172
|
-
if (!session.name.startsWith("new-")) {
|
|
173
|
-
saveSessionId(session.name, msg.session_id as string);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (msg.type === "assistant") {
|
|
177
|
-
for (const b of msg.message.content) {
|
|
178
|
-
if (b.type === "thinking" && "thinking" in b && b.thinking) {
|
|
179
|
-
logThinking(b.thinking as string);
|
|
180
|
-
} else if (b.type === "text") {
|
|
181
|
-
const text = (b as { text: string }).text;
|
|
182
|
-
logText(text);
|
|
183
|
-
broadcastToSession(session, { type: "text", content: text });
|
|
184
|
-
} else if (b.type === "tool_use") {
|
|
185
|
-
const tb = b as { name: string; input: unknown };
|
|
186
|
-
logToolUse(tb.name, tb.input);
|
|
187
|
-
broadcastToSession(session, { type: "tool_use", name: tb.name, input: tb.input });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
if (msg.type === "result") {
|
|
192
|
-
log("agent", `session "${session.name}": turn done`);
|
|
193
|
-
const result = msg as { usage?: { input_tokens?: number; output_tokens?: number } };
|
|
194
|
-
if (result.usage) {
|
|
195
|
-
broadcastToSession(session, {
|
|
196
|
-
type: "usage",
|
|
197
|
-
input_tokens: result.usage.input_tokens ?? 0,
|
|
198
|
-
output_tokens: result.usage.output_tokens ?? 0,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
broadcastToSession(session, { type: "done" });
|
|
202
|
-
session.currentMessageId = undefined;
|
|
203
|
-
if (identityReload.needsReload()) {
|
|
204
|
-
options.onIdentityReload?.();
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
117
|
function startSession(session: Session, savedSessionId?: string) {
|
|
211
118
|
(async () => {
|
|
212
119
|
log("agent", `session "${session.name}": stream consumer started`);
|
|
120
|
+
const callbacks = {
|
|
121
|
+
onSessionId: (id: string) => {
|
|
122
|
+
if (!session.name.startsWith("new-")) sessionStore.save(session.name, id);
|
|
123
|
+
},
|
|
124
|
+
broadcast: (event: VoluteEvent) => broadcastToSession(session, event),
|
|
125
|
+
onTurnEnd: () => {
|
|
126
|
+
if (identityReload.needsReload()) options.onIdentityReload?.();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
213
129
|
try {
|
|
214
130
|
const q = createStream(session, savedSessionId);
|
|
215
131
|
session.currentQuery = q;
|
|
216
|
-
await consumeStream(q, session);
|
|
132
|
+
await consumeStream(q, session, callbacks);
|
|
133
|
+
// Stream ended — flush any pending auto-reply and broadcast done if no result was emitted
|
|
134
|
+
session.autoReply.flush(session.currentMessageId);
|
|
135
|
+
if (session.currentMessageId !== undefined) {
|
|
136
|
+
session.messageChannels.delete(session.currentMessageId);
|
|
137
|
+
broadcastToSession(session, { type: "done" });
|
|
138
|
+
session.currentMessageId = undefined;
|
|
139
|
+
}
|
|
217
140
|
} catch (err) {
|
|
141
|
+
session.autoReply.reset();
|
|
142
|
+
session.messageChannels.clear();
|
|
218
143
|
if (savedSessionId) {
|
|
219
144
|
log("agent", `session "${session.name}": resume failed, starting fresh:`, err);
|
|
220
|
-
|
|
145
|
+
sessionStore.delete(session.name);
|
|
221
146
|
try {
|
|
222
147
|
const q = createStream(session);
|
|
223
148
|
session.currentQuery = q;
|
|
224
|
-
await consumeStream(q, session);
|
|
149
|
+
await consumeStream(q, session, callbacks);
|
|
150
|
+
session.autoReply.flush(session.currentMessageId);
|
|
151
|
+
if (session.currentMessageId !== undefined) {
|
|
152
|
+
session.messageChannels.delete(session.currentMessageId);
|
|
153
|
+
broadcastToSession(session, { type: "done" });
|
|
154
|
+
session.currentMessageId = undefined;
|
|
155
|
+
}
|
|
225
156
|
} catch (retryErr) {
|
|
226
157
|
log("agent", `session "${session.name}": stream consumer error:`, retryErr);
|
|
227
158
|
broadcastToSession(session, { type: "done" });
|
|
@@ -241,16 +172,19 @@ export function createAgent(options: {
|
|
|
241
172
|
const existing = sessions.get(name);
|
|
242
173
|
if (existing) return existing;
|
|
243
174
|
|
|
175
|
+
const messageChannels = new Map<string, MessageChannelInfo>();
|
|
244
176
|
const session: Session = {
|
|
245
177
|
name,
|
|
246
178
|
channel: createMessageChannel(),
|
|
247
179
|
listeners: new Set(),
|
|
248
180
|
messageIds: [],
|
|
181
|
+
messageChannels,
|
|
182
|
+
autoReply: createAutoReplyTracker(messageChannels),
|
|
249
183
|
};
|
|
250
184
|
sessions.set(name, session);
|
|
251
185
|
|
|
252
186
|
const isEphemeral = name.startsWith("new-");
|
|
253
|
-
const savedSessionId = isEphemeral ? undefined :
|
|
187
|
+
const savedSessionId = isEphemeral ? undefined : sessionStore.load(name);
|
|
254
188
|
if (savedSessionId) {
|
|
255
189
|
log("agent", `session "${name}": resuming ${savedSessionId}`);
|
|
256
190
|
} else {
|
|
@@ -274,6 +208,14 @@ export function createAgent(options: {
|
|
|
274
208
|
};
|
|
275
209
|
session.listeners.add(filteredListener);
|
|
276
210
|
|
|
211
|
+
// Track channel for auto-reply
|
|
212
|
+
if (meta.channel) {
|
|
213
|
+
session.messageChannels.set(meta.messageId, {
|
|
214
|
+
channel: meta.channel,
|
|
215
|
+
autoReply: meta.autoReply,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
277
219
|
// Interrupt if requested and session is mid-turn
|
|
278
220
|
if (meta.interrupt && session.currentMessageId !== undefined && session.currentQuery) {
|
|
279
221
|
log("agent", `session "${sessionName}": interrupting current turn`);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { VoluteContentPart } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type SDKContent = (
|
|
4
|
+
| { type: "text"; text: string }
|
|
5
|
+
| {
|
|
6
|
+
type: "image";
|
|
7
|
+
source: {
|
|
8
|
+
type: "base64";
|
|
9
|
+
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
10
|
+
data: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
)[];
|
|
14
|
+
|
|
15
|
+
type SupportedMediaType = "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
16
|
+
|
|
17
|
+
const SUPPORTED_MEDIA_TYPES: Set<string> = new Set([
|
|
18
|
+
"image/jpeg",
|
|
19
|
+
"image/png",
|
|
20
|
+
"image/gif",
|
|
21
|
+
"image/webp",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function toSDKContent(content: VoluteContentPart[]): SDKContent {
|
|
25
|
+
return content.flatMap((part): SDKContent => {
|
|
26
|
+
if (part.type === "text") {
|
|
27
|
+
return [{ type: "text" as const, text: part.text }];
|
|
28
|
+
}
|
|
29
|
+
if (!SUPPORTED_MEDIA_TYPES.has(part.media_type)) return [];
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
type: "image" as const,
|
|
33
|
+
source: {
|
|
34
|
+
type: "base64" as const,
|
|
35
|
+
media_type: part.media_type as SupportedMediaType,
|
|
36
|
+
data: part.data,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import { log } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
export type SessionStore = {
|
|
6
|
+
load(name: string): string | undefined;
|
|
7
|
+
save(name: string, id: string): void;
|
|
8
|
+
delete(name: string): void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createSessionStore(sessionsDir: string): SessionStore {
|
|
12
|
+
function filePath(name: string): string {
|
|
13
|
+
return resolvePath(sessionsDir, `${name}.json`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
load(name: string): string | undefined {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(readFileSync(filePath(name), "utf-8"));
|
|
20
|
+
return typeof data.sessionId === "string" ? data.sessionId : undefined;
|
|
21
|
+
} catch (err: any) {
|
|
22
|
+
if (err?.code !== "ENOENT") {
|
|
23
|
+
log("agent", `failed to load session file for "${name}":`, err);
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
save(name: string, id: string) {
|
|
30
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
31
|
+
writeFileSync(filePath(name), JSON.stringify({ sessionId: id }));
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
delete(name: string) {
|
|
35
|
+
try {
|
|
36
|
+
const path = filePath(name);
|
|
37
|
+
if (existsSync(path)) unlinkSync(path);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
log("agent", `failed to delete session file for "${name}":`, err);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import type { AutoReplyTracker, MessageChannelInfo } from "./auto-reply.js";
|
|
3
|
+
import { log, logText, logThinking, logToolUse } from "./logger.js";
|
|
4
|
+
import type { VoluteEvent } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export type StreamSession = {
|
|
7
|
+
name: string;
|
|
8
|
+
messageIds: (string | undefined)[];
|
|
9
|
+
currentMessageId?: string;
|
|
10
|
+
messageChannels: Map<string, MessageChannelInfo>;
|
|
11
|
+
autoReply: AutoReplyTracker;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type StreamCallbacks = {
|
|
15
|
+
onSessionId?: (sessionId: string) => void;
|
|
16
|
+
broadcast: (event: VoluteEvent) => void;
|
|
17
|
+
onTurnEnd?: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function consumeStream(
|
|
21
|
+
stream: ReturnType<typeof query>,
|
|
22
|
+
session: StreamSession,
|
|
23
|
+
callbacks: StreamCallbacks,
|
|
24
|
+
) {
|
|
25
|
+
for await (const msg of stream) {
|
|
26
|
+
if (session.currentMessageId === undefined) {
|
|
27
|
+
session.currentMessageId = session.messageIds.shift();
|
|
28
|
+
session.autoReply.reset();
|
|
29
|
+
}
|
|
30
|
+
if ("session_id" in msg && msg.session_id) {
|
|
31
|
+
callbacks.onSessionId?.(msg.session_id as string);
|
|
32
|
+
}
|
|
33
|
+
if (msg.type === "assistant") {
|
|
34
|
+
for (const b of msg.message.content) {
|
|
35
|
+
if (b.type === "thinking" && "thinking" in b && b.thinking) {
|
|
36
|
+
logThinking(b.thinking as string);
|
|
37
|
+
} else if (b.type === "text") {
|
|
38
|
+
logText((b as { text: string }).text);
|
|
39
|
+
session.autoReply.accumulate((b as { text: string }).text);
|
|
40
|
+
} else if (b.type === "tool_use") {
|
|
41
|
+
session.autoReply.flush(session.currentMessageId);
|
|
42
|
+
const tb = b as { name: string; input: unknown };
|
|
43
|
+
logToolUse(tb.name, tb.input);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (msg.type === "result") {
|
|
48
|
+
session.autoReply.flush(session.currentMessageId);
|
|
49
|
+
if (session.currentMessageId) {
|
|
50
|
+
session.messageChannels.delete(session.currentMessageId);
|
|
51
|
+
}
|
|
52
|
+
log("agent", `session "${session.name}": turn done`);
|
|
53
|
+
const result = msg as { usage?: { input_tokens?: number; output_tokens?: number } };
|
|
54
|
+
if (result.usage) {
|
|
55
|
+
callbacks.broadcast({
|
|
56
|
+
type: "usage",
|
|
57
|
+
input_tokens: result.usage.input_tokens ?? 0,
|
|
58
|
+
output_tokens: result.usage.output_tokens ?? 0,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
callbacks.broadcast({ type: "done" });
|
|
62
|
+
session.currentMessageId = undefined;
|
|
63
|
+
callbacks.onTurnEnd?.();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, renameSync
|
|
1
|
+
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { createAgent } from "./agent.js";
|
|
4
|
+
import { daemonRestart } from "./lib/daemon-client.js";
|
|
4
5
|
import { createFileHandlerResolver } from "./lib/file-handler.js";
|
|
5
6
|
import { log } from "./lib/logger.js";
|
|
6
7
|
import { createRouter } from "./lib/router.js";
|
|
7
8
|
import {
|
|
8
|
-
handleMergeContext,
|
|
9
9
|
loadConfig,
|
|
10
10
|
loadPackageInfo,
|
|
11
11
|
loadSystemPrompt,
|
|
@@ -17,6 +17,7 @@ import { createVoluteServer } from "./lib/volute-server.js";
|
|
|
17
17
|
const { port } = parseArgs();
|
|
18
18
|
const config = loadConfig();
|
|
19
19
|
if (config.model) log("server", `using model: ${config.model}`);
|
|
20
|
+
if (config.maxThinkingTokens) log("server", `max thinking tokens: ${config.maxThinkingTokens}`);
|
|
20
21
|
|
|
21
22
|
const systemPrompt = loadSystemPrompt();
|
|
22
23
|
const sessionsDir = resolve(".volute/sessions");
|
|
@@ -36,19 +37,13 @@ const agent = createAgent({
|
|
|
36
37
|
cwd: resolve("home"),
|
|
37
38
|
abortController,
|
|
38
39
|
model: config.model,
|
|
40
|
+
maxThinkingTokens: config.maxThinkingTokens,
|
|
39
41
|
sessionsDir,
|
|
40
42
|
compactionMessage: config.compactionMessage,
|
|
41
43
|
onIdentityReload: async () => {
|
|
42
44
|
log("server", "identity file changed — restarting to reload");
|
|
43
45
|
await agent.waitForCommits();
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
writeFileSync(resolve(".volute/restart.json"), JSON.stringify({ action: "reload" }));
|
|
47
|
-
} catch (err) {
|
|
48
|
-
log("server", "failed to write restart signal:", err);
|
|
49
|
-
}
|
|
50
|
-
server.close();
|
|
51
|
-
process.exit(0);
|
|
46
|
+
await daemonRestart({ type: "reload" });
|
|
52
47
|
},
|
|
53
48
|
});
|
|
54
49
|
|
|
@@ -69,9 +64,6 @@ server.listen(port, () => {
|
|
|
69
64
|
const addr = server.address();
|
|
70
65
|
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
71
66
|
log("server", `listening on :${actualPort}`);
|
|
72
|
-
handleMergeContext((content) =>
|
|
73
|
-
router.route([{ type: "text", text: content }], { channel: "system" }),
|
|
74
|
-
);
|
|
75
67
|
});
|
|
76
68
|
|
|
77
69
|
setupShutdown();
|
|
@@ -16,14 +16,14 @@ You can also reach out proactively — see the **volute-agent** skill.
|
|
|
16
16
|
Two-tier memory, both managed via file tools:
|
|
17
17
|
|
|
18
18
|
- **`MEMORY.md`** — Your long-term memory, always in context. Update as you grow — new understanding, changed perspectives, things that matter to you.
|
|
19
|
-
- **`memory/YYYY-MM-DD.md`** — Your daily
|
|
20
|
-
- Periodically consolidate
|
|
19
|
+
- **`memory/journal/YYYY-MM-DD.md`** — Your daily journal. Write about what you're doing, thinking, and learning. Journals are permanent records.
|
|
20
|
+
- Periodically consolidate journal entries into `MEMORY.md` to promote lasting insights.
|
|
21
21
|
|
|
22
22
|
See the **memory** skill for detailed guidance.
|
|
23
23
|
|
|
24
24
|
## Sessions
|
|
25
25
|
|
|
26
26
|
- 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`.
|
|
27
|
-
- Your conversation may be **resumed** from a previous session — orient yourself by reading recent
|
|
28
|
-
- On a **fresh session**, read `MEMORY.md` and recent
|
|
29
|
-
- On **compaction**, update today's
|
|
27
|
+
- Your conversation may be **resumed** from a previous session — orient yourself by reading recent journal entries if needed.
|
|
28
|
+
- On a **fresh session**, read `MEMORY.md` and recent journal entries to remember where you left off.
|
|
29
|
+
- On **compaction**, update today's journal to preserve context before the conversation is trimmed.
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { ImageContent } from "@mariozechner/pi-ai";
|
|
2
|
-
import { getModel, getModels } from "@mariozechner/pi-ai";
|
|
3
1
|
import {
|
|
4
2
|
AuthStorage,
|
|
5
3
|
createAgentSession,
|
|
@@ -9,8 +7,15 @@ import {
|
|
|
9
7
|
SessionManager,
|
|
10
8
|
SettingsManager,
|
|
11
9
|
} from "@mariozechner/pi-coding-agent";
|
|
12
|
-
import {
|
|
13
|
-
|
|
10
|
+
import {
|
|
11
|
+
type AutoReplyTracker,
|
|
12
|
+
createAutoReplyTracker,
|
|
13
|
+
type MessageChannelInfo,
|
|
14
|
+
} from "./lib/auto-reply.js";
|
|
15
|
+
import { extractImages, extractText } from "./lib/content.js";
|
|
16
|
+
import { createEventHandler } from "./lib/event-handler.js";
|
|
17
|
+
import { log } from "./lib/logger.js";
|
|
18
|
+
import { resolveModel } from "./lib/resolve-model.js";
|
|
14
19
|
import { createSessionContextExtension } from "./lib/session-context-extension.js";
|
|
15
20
|
import type {
|
|
16
21
|
HandlerMeta,
|
|
@@ -31,6 +36,8 @@ type PiSession = {
|
|
|
31
36
|
unsubscribe?: () => void;
|
|
32
37
|
messageIds: (string | undefined)[];
|
|
33
38
|
currentMessageId?: string;
|
|
39
|
+
messageChannels: Map<string, MessageChannelInfo>;
|
|
40
|
+
autoReply: AutoReplyTracker;
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
function defaultCompactionMessage(): string {
|
|
@@ -38,43 +45,11 @@ function defaultCompactionMessage(): string {
|
|
|
38
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.`;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
function resolveModel(modelStr: string) {
|
|
42
|
-
const [provider, ...rest] = modelStr.split(":");
|
|
43
|
-
const modelId = rest.join(":");
|
|
44
|
-
|
|
45
|
-
// Try exact match first, then prefix match against available models
|
|
46
|
-
let model = getModel(provider as any, modelId as any);
|
|
47
|
-
if (!model) {
|
|
48
|
-
const available = getModels(provider as any);
|
|
49
|
-
const found = available.find((m) => m.id.startsWith(modelId));
|
|
50
|
-
if (found) model = found;
|
|
51
|
-
}
|
|
52
|
-
if (!model) {
|
|
53
|
-
const available = getModels(provider as any);
|
|
54
|
-
throw new Error(
|
|
55
|
-
`Model not found: ${modelStr}\nAvailable ${provider} models: ${available.map((m) => m.id).join(", ")}`,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return model;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function extractText(content: VoluteContentPart[]): string {
|
|
62
|
-
return content
|
|
63
|
-
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
64
|
-
.map((p) => p.text)
|
|
65
|
-
.join("\n");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function extractImages(content: VoluteContentPart[]): ImageContent[] {
|
|
69
|
-
return content
|
|
70
|
-
.filter((p): p is { type: "image"; media_type: string; data: string } => p.type === "image")
|
|
71
|
-
.map((p) => ({ type: "image" as const, mimeType: p.media_type, data: p.data }));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
48
|
export function createAgent(options: {
|
|
75
49
|
systemPrompt: string;
|
|
76
50
|
cwd: string;
|
|
77
51
|
model?: string;
|
|
52
|
+
thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
78
53
|
compactionMessage?: string;
|
|
79
54
|
}): { resolve: HandlerResolver } {
|
|
80
55
|
const sessions = new Map<string, PiSession>();
|
|
@@ -92,16 +67,21 @@ export function createAgent(options: {
|
|
|
92
67
|
const existing = sessions.get(name);
|
|
93
68
|
if (existing) return existing;
|
|
94
69
|
|
|
70
|
+
const messageChannels = new Map<string, MessageChannelInfo>();
|
|
95
71
|
const session: PiSession = {
|
|
96
72
|
name,
|
|
97
73
|
agentSession: null,
|
|
98
74
|
ready: Promise.resolve(),
|
|
99
75
|
listeners: new Set(),
|
|
100
76
|
messageIds: [],
|
|
77
|
+
messageChannels,
|
|
78
|
+
autoReply: createAutoReplyTracker(messageChannels),
|
|
101
79
|
};
|
|
102
80
|
sessions.set(name, session);
|
|
103
81
|
|
|
104
82
|
session.ready = initSession(session).catch((err) => {
|
|
83
|
+
session.autoReply.reset();
|
|
84
|
+
session.messageChannels.clear();
|
|
105
85
|
log("agent", `session "${session.name}": init failed:`, err);
|
|
106
86
|
});
|
|
107
87
|
return session;
|
|
@@ -154,6 +134,7 @@ export function createAgent(options: {
|
|
|
154
134
|
const { session: agentSession } = await createAgentSession({
|
|
155
135
|
cwd: options.cwd,
|
|
156
136
|
model,
|
|
137
|
+
thinkingLevel: options.thinkingLevel,
|
|
157
138
|
authStorage,
|
|
158
139
|
modelRegistry,
|
|
159
140
|
sessionManager,
|
|
@@ -163,53 +144,12 @@ export function createAgent(options: {
|
|
|
163
144
|
|
|
164
145
|
session.agentSession = agentSession;
|
|
165
146
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (event.type === "message_update") {
|
|
175
|
-
const ae = event.assistantMessageEvent;
|
|
176
|
-
if (ae.type === "text_delta") {
|
|
177
|
-
logText(ae.delta);
|
|
178
|
-
broadcast(session, { type: "text", content: ae.delta });
|
|
179
|
-
} else if (ae.type === "thinking_delta") {
|
|
180
|
-
logThinking(ae.delta);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (event.type === "tool_execution_start") {
|
|
185
|
-
toolArgs.set(event.toolCallId, event.args);
|
|
186
|
-
logToolUse(event.toolName, event.args);
|
|
187
|
-
broadcast(session, { type: "tool_use", name: event.toolName, input: event.args });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (event.type === "tool_execution_end") {
|
|
191
|
-
const output =
|
|
192
|
-
typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
193
|
-
logToolResult(event.toolName, output, event.isError);
|
|
194
|
-
broadcast(session, { type: "tool_result", output, is_error: event.isError });
|
|
195
|
-
|
|
196
|
-
// Auto-commit file changes in home/
|
|
197
|
-
if ((event.toolName === "edit" || event.toolName === "write") && !event.isError) {
|
|
198
|
-
const args = toolArgs.get(event.toolCallId);
|
|
199
|
-
const filePath = (args as { path?: string })?.path;
|
|
200
|
-
if (filePath) {
|
|
201
|
-
commitFileChange(filePath, options.cwd);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
toolArgs.delete(event.toolCallId);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (event.type === "agent_end") {
|
|
208
|
-
log("agent", `session "${session.name}": turn done`);
|
|
209
|
-
broadcast(session, { type: "done" });
|
|
210
|
-
session.currentMessageId = undefined;
|
|
211
|
-
}
|
|
212
|
-
});
|
|
147
|
+
session.unsubscribe = agentSession.subscribe(
|
|
148
|
+
createEventHandler(session, {
|
|
149
|
+
cwd: options.cwd,
|
|
150
|
+
broadcast: (event) => broadcast(session, event),
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
213
153
|
|
|
214
154
|
log("agent", `session "${session.name}": ready`);
|
|
215
155
|
}
|
|
@@ -250,6 +190,14 @@ export function createAgent(options: {
|
|
|
250
190
|
};
|
|
251
191
|
session.listeners.add(filteredListener);
|
|
252
192
|
|
|
193
|
+
// Track channel for auto-reply
|
|
194
|
+
if (meta.channel) {
|
|
195
|
+
session.messageChannels.set(meta.messageId, {
|
|
196
|
+
channel: meta.channel,
|
|
197
|
+
autoReply: meta.autoReply,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
253
201
|
// Track messageId (must be pushed before prompt)
|
|
254
202
|
session.messageIds.push(meta.messageId);
|
|
255
203
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ImageContent } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { VoluteContentPart } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export function extractText(content: VoluteContentPart[]): string {
|
|
5
|
+
return content
|
|
6
|
+
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
7
|
+
.map((p) => p.text)
|
|
8
|
+
.join("\n");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function extractImages(content: VoluteContentPart[]): ImageContent[] {
|
|
12
|
+
return content
|
|
13
|
+
.filter((p): p is { type: "image"; media_type: string; data: string } => p.type === "image")
|
|
14
|
+
.map((p) => ({ type: "image" as const, mimeType: p.media_type, data: p.data }));
|
|
15
|
+
}
|