volute 0.16.0 → 0.18.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/dist/chunk-AYB7XAWO.js +812 -0
- package/dist/{chunk-3FD4ZZUL.js → chunk-FW5API7X.js} +116 -10
- package/dist/{chunk-3FC42ZBM.js → chunk-GK4E7LM7.js} +3 -0
- package/dist/cli.js +18 -6
- package/dist/connectors/discord.js +1 -1
- package/dist/connectors/slack.js +1 -1
- package/dist/connectors/telegram.js +1 -1
- package/dist/{daemon-restart-MS5FI44G.js → daemon-restart-2HVTHZAT.js} +1 -1
- package/dist/daemon.js +1443 -592
- package/dist/history-YUEKTJ2N.js +108 -0
- package/dist/{mind-manager-PN5SUDJ4.js → mind-manager-Z7O7PN2O.js} +1 -1
- package/dist/{package-3QGV3KX6.js → package-OKLFO7UY.js} +8 -9
- package/dist/{send-KBBZNYG6.js → send-BNDTLUPM.js} +41 -9
- package/dist/skill-2Y42P4JY.js +287 -0
- package/dist/{up-GZLWZAQE.js → up-7B3BWF2U.js} +1 -1
- package/dist/web-assets/assets/index-CtiimdWK.css +1 -0
- package/dist/web-assets/assets/index-kt1_EcuO.js +63 -0
- package/dist/web-assets/index.html +2 -1
- package/drizzle/0006_mind_history.sql +20 -0
- package/drizzle/0007_system_prompts.sql +5 -0
- package/drizzle/0008_volute_channels.sql +24 -0
- package/drizzle/0009_shared_skills.sql +9 -0
- package/drizzle/meta/0006_snapshot.json +7 -0
- package/drizzle/meta/0007_snapshot.json +7 -0
- package/drizzle/meta/0008_snapshot.json +7 -0
- package/drizzle/meta/0009_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +8 -9
- package/templates/_base/.init/.config/prompts.json +5 -0
- package/templates/_base/_skills/volute-mind/SKILL.md +19 -5
- package/templates/_base/src/lib/daemon-client.ts +45 -0
- package/templates/_base/src/lib/logger.ts +19 -0
- package/templates/_base/src/lib/router.ts +48 -41
- package/templates/_base/src/lib/routing.ts +5 -8
- package/templates/_base/src/lib/startup.ts +43 -0
- package/templates/_base/src/lib/transparency.ts +89 -0
- package/templates/_base/src/lib/types.ts +0 -1
- package/templates/_base/src/lib/volute-server.ts +3 -35
- package/templates/claude/src/agent.ts +9 -22
- package/templates/claude/src/lib/hooks/reply-instructions.ts +6 -9
- package/templates/claude/src/lib/stream-consumer.ts +39 -12
- package/templates/pi/src/agent.ts +9 -22
- package/templates/pi/src/lib/event-handler.ts +58 -7
- package/templates/pi/src/lib/reply-instructions-extension.ts +6 -9
- package/dist/chunk-J52CJCVI.js +0 -447
- package/dist/history-LKCJJMUV.js +0 -50
- package/dist/web-assets/assets/index-B1XIIGCh.js +0 -307
- package/templates/_base/src/lib/auto-reply.ts +0 -38
- /package/dist/{chunk-LLBBVTEY.js → chunk-6DVBMLVN.js} +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { DaemonEvent, EventType } from "./daemon-client.js";
|
|
4
|
+
|
|
5
|
+
export type TransparencyPreset = "transparent" | "standard" | "private" | "silent";
|
|
6
|
+
|
|
7
|
+
type FilterableEventType = Exclude<EventType, "inbound" | "outbound">;
|
|
8
|
+
|
|
9
|
+
const PRESET_RULES: Record<
|
|
10
|
+
TransparencyPreset,
|
|
11
|
+
Record<FilterableEventType, "yes" | "name_only" | "no">
|
|
12
|
+
> = {
|
|
13
|
+
transparent: {
|
|
14
|
+
thinking: "yes",
|
|
15
|
+
text: "yes",
|
|
16
|
+
tool_use: "yes",
|
|
17
|
+
tool_result: "yes",
|
|
18
|
+
log: "yes",
|
|
19
|
+
usage: "yes",
|
|
20
|
+
session_start: "yes",
|
|
21
|
+
done: "yes",
|
|
22
|
+
},
|
|
23
|
+
standard: {
|
|
24
|
+
thinking: "no",
|
|
25
|
+
text: "yes",
|
|
26
|
+
tool_use: "name_only",
|
|
27
|
+
tool_result: "no",
|
|
28
|
+
log: "yes",
|
|
29
|
+
usage: "yes",
|
|
30
|
+
session_start: "yes",
|
|
31
|
+
done: "yes",
|
|
32
|
+
},
|
|
33
|
+
private: {
|
|
34
|
+
thinking: "no",
|
|
35
|
+
text: "no",
|
|
36
|
+
tool_use: "no",
|
|
37
|
+
tool_result: "no",
|
|
38
|
+
log: "no",
|
|
39
|
+
usage: "yes",
|
|
40
|
+
session_start: "yes",
|
|
41
|
+
done: "yes",
|
|
42
|
+
},
|
|
43
|
+
silent: {
|
|
44
|
+
thinking: "no",
|
|
45
|
+
text: "no",
|
|
46
|
+
tool_use: "no",
|
|
47
|
+
tool_result: "no",
|
|
48
|
+
log: "no",
|
|
49
|
+
usage: "no",
|
|
50
|
+
session_start: "no",
|
|
51
|
+
done: "no",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Communication records are always emitted (bypass transparency filtering)
|
|
56
|
+
const ALWAYS_ALLOWED: ReadonlySet<string> = new Set(["inbound", "outbound"]);
|
|
57
|
+
|
|
58
|
+
export function loadTransparencyPreset(): TransparencyPreset {
|
|
59
|
+
for (const file of ["home/.config/config.json", "home/.config/volute.json"]) {
|
|
60
|
+
try {
|
|
61
|
+
const config = JSON.parse(readFileSync(resolve(file), "utf-8"));
|
|
62
|
+
if (config.transparency && config.transparency in PRESET_RULES) {
|
|
63
|
+
return config.transparency as TransparencyPreset;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// try next
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return "standard";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function filterEvent(preset: TransparencyPreset, event: DaemonEvent): DaemonEvent | null {
|
|
73
|
+
if (ALWAYS_ALLOWED.has(event.type)) return event;
|
|
74
|
+
|
|
75
|
+
const rules = PRESET_RULES[preset];
|
|
76
|
+
const rule = rules[event.type as FilterableEventType];
|
|
77
|
+
|
|
78
|
+
if (!rule) {
|
|
79
|
+
// Unknown event types: pass through in transparent mode, drop otherwise
|
|
80
|
+
return preset === "transparent" ? event : null;
|
|
81
|
+
}
|
|
82
|
+
if (rule === "no") return null;
|
|
83
|
+
|
|
84
|
+
if (rule === "name_only" && event.type === "tool_use") {
|
|
85
|
+
return { ...event, content: undefined };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return event;
|
|
89
|
+
}
|
|
@@ -32,41 +32,9 @@ export function createVoluteServer(options: {
|
|
|
32
32
|
if (req.method === "POST" && url.pathname === "/message") {
|
|
33
33
|
try {
|
|
34
34
|
const body = JSON.parse(await readBody(req)) as VoluteRequest;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const { unsubscribe } = router.route(body.content, body, (event) => {
|
|
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
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
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: "Mind processing timed out" }));
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
5 * 60 * 1000,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
res.on("close", () => {
|
|
67
|
-
clearTimeout(timeout);
|
|
68
|
-
if (!done) unsubscribe();
|
|
69
|
-
});
|
|
35
|
+
router.route(body.content, body);
|
|
36
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
37
|
+
res.end(JSON.stringify({ ok: true }));
|
|
70
38
|
} catch (err) {
|
|
71
39
|
if (err instanceof SyntaxError) {
|
|
72
40
|
res.writeHead(400);
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
2
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
3
|
import { toSDKContent } from "./lib/content.js";
|
|
9
4
|
import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
|
|
10
5
|
import { createIdentityReloadHook } from "./lib/hooks/identity-reload.js";
|
|
@@ -14,6 +9,7 @@ import { createSessionContextHook } from "./lib/hooks/session-context.js";
|
|
|
14
9
|
import { log } from "./lib/logger.js";
|
|
15
10
|
import { createMessageChannel } from "./lib/message-channel.js";
|
|
16
11
|
import { createSessionStore } from "./lib/session-store.js";
|
|
12
|
+
import { loadPrompts } from "./lib/startup.js";
|
|
17
13
|
import { consumeStream } from "./lib/stream-consumer.js";
|
|
18
14
|
import type {
|
|
19
15
|
HandlerMeta,
|
|
@@ -31,8 +27,7 @@ type Session = {
|
|
|
31
27
|
messageIds: (string | undefined)[];
|
|
32
28
|
currentMessageId?: string;
|
|
33
29
|
currentQuery?: ReturnType<typeof query>;
|
|
34
|
-
messageChannels: Map<string,
|
|
35
|
-
autoReply: AutoReplyTracker;
|
|
30
|
+
messageChannels: Map<string, string>;
|
|
36
31
|
};
|
|
37
32
|
|
|
38
33
|
export function createMind(options: {
|
|
@@ -53,10 +48,10 @@ export function createMind(options: {
|
|
|
53
48
|
];
|
|
54
49
|
|
|
55
50
|
const sessions = new Map<string, Session>();
|
|
56
|
-
const
|
|
51
|
+
const prompts = loadPrompts();
|
|
52
|
+
const today = new Date().toLocaleDateString("en-CA");
|
|
57
53
|
const compactionMessage =
|
|
58
|
-
options.compactionMessage ??
|
|
59
|
-
`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.`;
|
|
54
|
+
options.compactionMessage ?? prompts.compaction_warning.replace("${date}", today);
|
|
60
55
|
|
|
61
56
|
// --- Event broadcasting ---
|
|
62
57
|
|
|
@@ -133,15 +128,13 @@ export function createMind(options: {
|
|
|
133
128
|
const q = createStream(session, savedSessionId);
|
|
134
129
|
session.currentQuery = q;
|
|
135
130
|
await consumeStream(q, session, callbacks);
|
|
136
|
-
// Stream ended —
|
|
137
|
-
session.autoReply.flush(session.currentMessageId);
|
|
131
|
+
// Stream ended — broadcast done if no result was emitted
|
|
138
132
|
if (session.currentMessageId !== undefined) {
|
|
139
133
|
session.messageChannels.delete(session.currentMessageId);
|
|
140
134
|
broadcastToSession(session, { type: "done" });
|
|
141
135
|
session.currentMessageId = undefined;
|
|
142
136
|
}
|
|
143
137
|
} catch (err) {
|
|
144
|
-
session.autoReply.reset();
|
|
145
138
|
session.messageChannels.clear();
|
|
146
139
|
if (savedSessionId) {
|
|
147
140
|
log("mind", `session "${session.name}": resume failed, starting fresh:`, err);
|
|
@@ -150,7 +143,6 @@ export function createMind(options: {
|
|
|
150
143
|
const q = createStream(session);
|
|
151
144
|
session.currentQuery = q;
|
|
152
145
|
await consumeStream(q, session, callbacks);
|
|
153
|
-
session.autoReply.flush(session.currentMessageId);
|
|
154
146
|
if (session.currentMessageId !== undefined) {
|
|
155
147
|
session.messageChannels.delete(session.currentMessageId);
|
|
156
148
|
broadcastToSession(session, { type: "done" });
|
|
@@ -175,14 +167,12 @@ export function createMind(options: {
|
|
|
175
167
|
const existing = sessions.get(name);
|
|
176
168
|
if (existing) return existing;
|
|
177
169
|
|
|
178
|
-
const messageChannels = new Map<string, MessageChannelInfo>();
|
|
179
170
|
const session: Session = {
|
|
180
171
|
name,
|
|
181
172
|
channel: createMessageChannel(),
|
|
182
173
|
listeners: new Set(),
|
|
183
174
|
messageIds: [],
|
|
184
|
-
messageChannels,
|
|
185
|
-
autoReply: createAutoReplyTracker(messageChannels),
|
|
175
|
+
messageChannels: new Map(),
|
|
186
176
|
};
|
|
187
177
|
sessions.set(name, session);
|
|
188
178
|
|
|
@@ -211,12 +201,9 @@ export function createMind(options: {
|
|
|
211
201
|
};
|
|
212
202
|
session.listeners.add(filteredListener);
|
|
213
203
|
|
|
214
|
-
// Track channel for
|
|
204
|
+
// Track channel for reply instructions
|
|
215
205
|
if (meta.channel) {
|
|
216
|
-
session.messageChannels.set(meta.messageId,
|
|
217
|
-
channel: meta.channel,
|
|
218
|
-
autoReply: meta.autoReply,
|
|
219
|
-
});
|
|
206
|
+
session.messageChannels.set(meta.messageId, meta.channel);
|
|
220
207
|
}
|
|
221
208
|
|
|
222
209
|
// Interrupt if requested and session is mid-turn
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import
|
|
2
|
+
import { loadPrompts } from "../startup.js";
|
|
3
3
|
|
|
4
|
-
export function createReplyInstructionsHook(messageChannels: Map<string,
|
|
4
|
+
export function createReplyInstructionsHook(messageChannels: Map<string, string>) {
|
|
5
5
|
let fired = false;
|
|
6
|
+
const prompts = loadPrompts();
|
|
6
7
|
|
|
7
8
|
const hook: HookCallback = async () => {
|
|
8
9
|
if (fired) return {};
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
11
|
+
const channel = messageChannels.values().next().value;
|
|
12
|
+
if (!channel) return {};
|
|
12
13
|
|
|
13
14
|
fired = true;
|
|
14
15
|
|
|
15
|
-
const context = entry.autoReply
|
|
16
|
-
? `Auto-reply is enabled for this session — your text output will automatically be sent back to ${entry.channel}. To send to a different channel: volute send <channel> "message"`
|
|
17
|
-
: `To reply to this message, use: volute send ${entry.channel} "your message"`;
|
|
18
|
-
|
|
19
16
|
return {
|
|
20
17
|
hookSpecificOutput: {
|
|
21
18
|
hookEventName: "UserPromptSubmit" as const,
|
|
22
|
-
additionalContext:
|
|
19
|
+
additionalContext: prompts.reply_instructions.replace(/\$\{channel\}/g, channel),
|
|
23
20
|
},
|
|
24
21
|
};
|
|
25
22
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import
|
|
2
|
+
import { daemonEmit, type EventType } from "./daemon-client.js";
|
|
3
3
|
import { log, logText, logThinking, logToolUse } from "./logger.js";
|
|
4
|
+
import { filterEvent, loadTransparencyPreset } from "./transparency.js";
|
|
4
5
|
import type { VoluteEvent } from "./types.js";
|
|
5
6
|
|
|
6
7
|
export type StreamSession = {
|
|
7
8
|
name: string;
|
|
8
9
|
messageIds: (string | undefined)[];
|
|
9
10
|
currentMessageId?: string;
|
|
10
|
-
messageChannels: Map<string,
|
|
11
|
-
autoReply: AutoReplyTracker;
|
|
11
|
+
messageChannels: Map<string, string>;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export type StreamCallbacks = {
|
|
@@ -17,15 +17,34 @@ export type StreamCallbacks = {
|
|
|
17
17
|
onTurnEnd?: () => void;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
// Loaded once at startup — mind restarts on config changes
|
|
21
|
+
const preset = loadTransparencyPreset();
|
|
22
|
+
|
|
23
|
+
function emit(
|
|
24
|
+
session: StreamSession,
|
|
25
|
+
event: { type: EventType; content?: string; metadata?: Record<string, unknown> },
|
|
26
|
+
) {
|
|
27
|
+
const channel = session.currentMessageId
|
|
28
|
+
? session.messageChannels.get(session.currentMessageId)
|
|
29
|
+
: undefined;
|
|
30
|
+
const filtered = filterEvent(preset, {
|
|
31
|
+
...event,
|
|
32
|
+
session: session.name,
|
|
33
|
+
channel,
|
|
34
|
+
messageId: session.currentMessageId,
|
|
35
|
+
});
|
|
36
|
+
if (filtered) daemonEmit(filtered);
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
export async function consumeStream(
|
|
21
40
|
stream: ReturnType<typeof query>,
|
|
22
41
|
session: StreamSession,
|
|
23
42
|
callbacks: StreamCallbacks,
|
|
24
43
|
) {
|
|
44
|
+
emit(session, { type: "session_start" });
|
|
25
45
|
for await (const msg of stream) {
|
|
26
46
|
if (session.currentMessageId === undefined) {
|
|
27
47
|
session.currentMessageId = session.messageIds.shift();
|
|
28
|
-
session.autoReply.reset();
|
|
29
48
|
}
|
|
30
49
|
if ("session_id" in msg && msg.session_id) {
|
|
31
50
|
callbacks.onSessionId?.(msg.session_id as string);
|
|
@@ -33,32 +52,40 @@ export async function consumeStream(
|
|
|
33
52
|
if (msg.type === "assistant") {
|
|
34
53
|
for (const b of msg.message.content) {
|
|
35
54
|
if (b.type === "thinking" && "thinking" in b && b.thinking) {
|
|
36
|
-
|
|
55
|
+
const text = b.thinking as string;
|
|
56
|
+
logThinking(text);
|
|
57
|
+
emit(session, { type: "thinking", content: text });
|
|
37
58
|
} else if (b.type === "text") {
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
const text = (b as { text: string }).text;
|
|
60
|
+
logText(text);
|
|
61
|
+
emit(session, { type: "text", content: text });
|
|
40
62
|
} else if (b.type === "tool_use") {
|
|
41
|
-
session.autoReply.flush(session.currentMessageId);
|
|
42
63
|
const tb = b as { name: string; input: unknown };
|
|
43
64
|
logToolUse(tb.name, tb.input);
|
|
65
|
+
emit(session, {
|
|
66
|
+
type: "tool_use",
|
|
67
|
+
content: JSON.stringify(tb.input),
|
|
68
|
+
metadata: { name: tb.name },
|
|
69
|
+
});
|
|
44
70
|
}
|
|
45
71
|
}
|
|
46
72
|
}
|
|
47
73
|
if (msg.type === "result") {
|
|
48
|
-
session.autoReply.flush(session.currentMessageId);
|
|
49
74
|
if (session.currentMessageId) {
|
|
50
75
|
session.messageChannels.delete(session.currentMessageId);
|
|
51
76
|
}
|
|
52
77
|
log("mind", `session "${session.name}": turn done`);
|
|
53
78
|
const result = msg as { usage?: { input_tokens?: number; output_tokens?: number } };
|
|
54
79
|
if (result.usage) {
|
|
55
|
-
|
|
56
|
-
type: "usage",
|
|
80
|
+
const usage = {
|
|
57
81
|
input_tokens: result.usage.input_tokens ?? 0,
|
|
58
82
|
output_tokens: result.usage.output_tokens ?? 0,
|
|
59
|
-
}
|
|
83
|
+
};
|
|
84
|
+
callbacks.broadcast({ type: "usage", ...usage });
|
|
85
|
+
emit(session, { type: "usage", metadata: usage });
|
|
60
86
|
}
|
|
61
87
|
callbacks.broadcast({ type: "done" });
|
|
88
|
+
emit(session, { type: "done" });
|
|
62
89
|
session.currentMessageId = undefined;
|
|
63
90
|
callbacks.onTurnEnd?.();
|
|
64
91
|
}
|
|
@@ -7,17 +7,13 @@ import {
|
|
|
7
7
|
SessionManager,
|
|
8
8
|
SettingsManager,
|
|
9
9
|
} from "@mariozechner/pi-coding-agent";
|
|
10
|
-
import {
|
|
11
|
-
type AutoReplyTracker,
|
|
12
|
-
createAutoReplyTracker,
|
|
13
|
-
type MessageChannelInfo,
|
|
14
|
-
} from "./lib/auto-reply.js";
|
|
15
10
|
import { extractImages, extractText } from "./lib/content.js";
|
|
16
11
|
import { createEventHandler } from "./lib/event-handler.js";
|
|
17
12
|
import { log } from "./lib/logger.js";
|
|
18
13
|
import { createReplyInstructionsExtension } from "./lib/reply-instructions-extension.js";
|
|
19
14
|
import { resolveModel } from "./lib/resolve-model.js";
|
|
20
15
|
import { createSessionContextExtension } from "./lib/session-context-extension.js";
|
|
16
|
+
import { loadPrompts } from "./lib/startup.js";
|
|
21
17
|
import type {
|
|
22
18
|
HandlerMeta,
|
|
23
19
|
HandlerResolver,
|
|
@@ -37,15 +33,9 @@ type PiSession = {
|
|
|
37
33
|
unsubscribe?: () => void;
|
|
38
34
|
messageIds: (string | undefined)[];
|
|
39
35
|
currentMessageId?: string;
|
|
40
|
-
messageChannels: Map<string,
|
|
41
|
-
autoReply: AutoReplyTracker;
|
|
36
|
+
messageChannels: Map<string, string>;
|
|
42
37
|
};
|
|
43
38
|
|
|
44
|
-
function defaultCompactionMessage(): string {
|
|
45
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
46
|
-
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.`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
39
|
export function createMind(options: {
|
|
50
40
|
systemPrompt: string;
|
|
51
41
|
cwd: string;
|
|
@@ -54,7 +44,10 @@ export function createMind(options: {
|
|
|
54
44
|
compactionMessage?: string;
|
|
55
45
|
}): { resolve: HandlerResolver } {
|
|
56
46
|
const sessions = new Map<string, PiSession>();
|
|
57
|
-
const
|
|
47
|
+
const prompts = loadPrompts();
|
|
48
|
+
const today = new Date().toLocaleDateString("en-CA");
|
|
49
|
+
const compactionMessage =
|
|
50
|
+
options.compactionMessage ?? prompts.compaction_warning.replace("${date}", today);
|
|
58
51
|
|
|
59
52
|
// Shared setup (created once)
|
|
60
53
|
const modelStr = options.model || process.env.PI_MODEL || "anthropic:claude-sonnet-4-20250514";
|
|
@@ -68,20 +61,17 @@ export function createMind(options: {
|
|
|
68
61
|
const existing = sessions.get(name);
|
|
69
62
|
if (existing) return existing;
|
|
70
63
|
|
|
71
|
-
const messageChannels = new Map<string, MessageChannelInfo>();
|
|
72
64
|
const session: PiSession = {
|
|
73
65
|
name,
|
|
74
66
|
agentSession: null,
|
|
75
67
|
ready: Promise.resolve(),
|
|
76
68
|
listeners: new Set(),
|
|
77
69
|
messageIds: [],
|
|
78
|
-
messageChannels,
|
|
79
|
-
autoReply: createAutoReplyTracker(messageChannels),
|
|
70
|
+
messageChannels: new Map(),
|
|
80
71
|
};
|
|
81
72
|
sessions.set(name, session);
|
|
82
73
|
|
|
83
74
|
session.ready = initSession(session).catch((err) => {
|
|
84
|
-
session.autoReply.reset();
|
|
85
75
|
session.messageChannels.clear();
|
|
86
76
|
log("mind", `session "${session.name}": init failed:`, err);
|
|
87
77
|
});
|
|
@@ -197,12 +187,9 @@ export function createMind(options: {
|
|
|
197
187
|
};
|
|
198
188
|
session.listeners.add(filteredListener);
|
|
199
189
|
|
|
200
|
-
// Track channel for
|
|
190
|
+
// Track channel for reply instructions
|
|
201
191
|
if (meta.channel) {
|
|
202
|
-
session.messageChannels.set(meta.messageId,
|
|
203
|
-
channel: meta.channel,
|
|
204
|
-
autoReply: meta.autoReply,
|
|
205
|
-
});
|
|
192
|
+
session.messageChannels.set(meta.messageId, meta.channel);
|
|
206
193
|
}
|
|
207
194
|
|
|
208
195
|
// Track messageId (must be pushed before prompt)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { commitFileChange } from "./auto-commit.js";
|
|
2
|
-
import
|
|
2
|
+
import { daemonEmit, type EventType } from "./daemon-client.js";
|
|
3
3
|
import { log, logText, logThinking, logToolResult, logToolUse } from "./logger.js";
|
|
4
|
+
import { filterEvent, loadTransparencyPreset } from "./transparency.js";
|
|
4
5
|
import type { VoluteEvent } from "./types.js";
|
|
5
6
|
|
|
6
7
|
export type EventSession = {
|
|
7
8
|
name: string;
|
|
8
9
|
messageIds: (string | undefined)[];
|
|
9
10
|
currentMessageId?: string;
|
|
10
|
-
messageChannels: Map<string,
|
|
11
|
-
autoReply: AutoReplyTracker;
|
|
11
|
+
messageChannels: Map<string, string>;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export type EventHandlerOptions = {
|
|
@@ -16,6 +16,25 @@ export type EventHandlerOptions = {
|
|
|
16
16
|
broadcast: (event: VoluteEvent) => void;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// Loaded once at startup — mind restarts on config changes
|
|
20
|
+
const preset = loadTransparencyPreset();
|
|
21
|
+
|
|
22
|
+
function emit(
|
|
23
|
+
session: EventSession,
|
|
24
|
+
event: { type: EventType; content?: string; metadata?: Record<string, unknown> },
|
|
25
|
+
) {
|
|
26
|
+
const channel = session.currentMessageId
|
|
27
|
+
? session.messageChannels.get(session.currentMessageId)
|
|
28
|
+
: undefined;
|
|
29
|
+
const filtered = filterEvent(preset, {
|
|
30
|
+
...event,
|
|
31
|
+
session: session.name,
|
|
32
|
+
channel,
|
|
33
|
+
messageId: session.currentMessageId,
|
|
34
|
+
});
|
|
35
|
+
if (filtered) daemonEmit(filtered);
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export function createEventHandler(session: EventSession, options: EventHandlerOptions) {
|
|
20
39
|
const toolArgs = new Map<string, any>();
|
|
21
40
|
let textBuf = "";
|
|
@@ -24,6 +43,7 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
24
43
|
function flushText() {
|
|
25
44
|
if (textBuf) {
|
|
26
45
|
logText(textBuf);
|
|
46
|
+
emit(session, { type: "text", content: textBuf });
|
|
27
47
|
textBuf = "";
|
|
28
48
|
}
|
|
29
49
|
}
|
|
@@ -31,6 +51,7 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
31
51
|
function flushThinking() {
|
|
32
52
|
if (thinkingBuf) {
|
|
33
53
|
logThinking(thinkingBuf);
|
|
54
|
+
emit(session, { type: "thinking", content: thinkingBuf });
|
|
34
55
|
thinkingBuf = "";
|
|
35
56
|
}
|
|
36
57
|
}
|
|
@@ -40,12 +61,18 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
40
61
|
flushText();
|
|
41
62
|
}
|
|
42
63
|
|
|
64
|
+
let sessionStarted = false;
|
|
65
|
+
|
|
43
66
|
return (event: any) => {
|
|
44
67
|
try {
|
|
68
|
+
if (!sessionStarted && event.type === "agent_start") {
|
|
69
|
+
sessionStarted = true;
|
|
70
|
+
emit(session, { type: "session_start" });
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
if (session.currentMessageId === undefined) {
|
|
46
74
|
flushBuffers(); // flush any leftover from a turn that ended without agent_end
|
|
47
75
|
session.currentMessageId = session.messageIds.shift();
|
|
48
|
-
session.autoReply.reset();
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
if (event.type === "message_update") {
|
|
@@ -53,7 +80,6 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
53
80
|
if (ae.type === "text_delta") {
|
|
54
81
|
if (thinkingBuf) flushThinking();
|
|
55
82
|
textBuf += ae.delta;
|
|
56
|
-
session.autoReply.accumulate(ae.delta);
|
|
57
83
|
// Log complete lines as they arrive
|
|
58
84
|
for (let nl = textBuf.indexOf("\n"); nl !== -1; nl = textBuf.indexOf("\n")) {
|
|
59
85
|
logText(textBuf.slice(0, nl + 1));
|
|
@@ -71,15 +97,24 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
71
97
|
|
|
72
98
|
if (event.type === "tool_execution_start") {
|
|
73
99
|
flushBuffers();
|
|
74
|
-
session.autoReply.flush(session.currentMessageId);
|
|
75
100
|
toolArgs.set(event.toolCallId, event.args);
|
|
76
101
|
logToolUse(event.toolName, event.args);
|
|
102
|
+
emit(session, {
|
|
103
|
+
type: "tool_use",
|
|
104
|
+
content: JSON.stringify(event.args),
|
|
105
|
+
metadata: { name: event.toolName },
|
|
106
|
+
});
|
|
77
107
|
}
|
|
78
108
|
|
|
79
109
|
if (event.type === "tool_execution_end") {
|
|
80
110
|
const output =
|
|
81
111
|
typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
82
112
|
logToolResult(event.toolName, output, event.isError);
|
|
113
|
+
emit(session, {
|
|
114
|
+
type: "tool_result",
|
|
115
|
+
content: output,
|
|
116
|
+
metadata: { name: event.toolName, is_error: event.isError },
|
|
117
|
+
});
|
|
83
118
|
|
|
84
119
|
// Auto-commit file changes in home/
|
|
85
120
|
if ((event.toolName === "edit" || event.toolName === "write") && !event.isError) {
|
|
@@ -94,12 +129,28 @@ export function createEventHandler(session: EventSession, options: EventHandlerO
|
|
|
94
129
|
|
|
95
130
|
if (event.type === "agent_end") {
|
|
96
131
|
flushBuffers();
|
|
97
|
-
session.autoReply.flush(session.currentMessageId);
|
|
98
132
|
if (session.currentMessageId) {
|
|
99
133
|
session.messageChannels.delete(session.currentMessageId);
|
|
100
134
|
}
|
|
101
135
|
log("mind", `session "${session.name}": turn done`);
|
|
136
|
+
// Sum usage from assistant messages
|
|
137
|
+
if (event.messages) {
|
|
138
|
+
let inputTokens = 0;
|
|
139
|
+
let outputTokens = 0;
|
|
140
|
+
for (const msg of event.messages as any[]) {
|
|
141
|
+
if (msg.role === "assistant" && msg.usage) {
|
|
142
|
+
inputTokens += msg.usage.input ?? 0;
|
|
143
|
+
outputTokens += msg.usage.output ?? 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
147
|
+
const usage = { input_tokens: inputTokens, output_tokens: outputTokens };
|
|
148
|
+
options.broadcast({ type: "usage", ...usage });
|
|
149
|
+
emit(session, { type: "usage", metadata: usage });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
102
152
|
options.broadcast({ type: "done" });
|
|
153
|
+
emit(session, { type: "done" });
|
|
103
154
|
session.currentMessageId = undefined;
|
|
104
155
|
}
|
|
105
156
|
} catch (err) {
|
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import
|
|
2
|
+
import { loadPrompts } from "./startup.js";
|
|
3
3
|
|
|
4
4
|
export function createReplyInstructionsExtension(
|
|
5
|
-
messageChannels: Map<string,
|
|
5
|
+
messageChannels: Map<string, string>,
|
|
6
6
|
): ExtensionFactory {
|
|
7
|
+
const prompts = loadPrompts();
|
|
7
8
|
return (pi) => {
|
|
8
9
|
let fired = false;
|
|
9
10
|
pi.on("before_agent_start", () => {
|
|
10
11
|
if (fired) return {};
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
-
if (!
|
|
13
|
+
const channel = messageChannels.values().next().value;
|
|
14
|
+
if (!channel) return {};
|
|
14
15
|
|
|
15
16
|
fired = true;
|
|
16
17
|
|
|
17
|
-
const content = entry.autoReply
|
|
18
|
-
? `Auto-reply is enabled for this session — your text output will automatically be sent back to ${entry.channel}. To send to a different channel: volute send <channel> "message"`
|
|
19
|
-
: `To reply to this message, use: volute send ${entry.channel} "your message"`;
|
|
20
|
-
|
|
21
18
|
return {
|
|
22
19
|
message: {
|
|
23
20
|
customType: "reply-instructions",
|
|
24
|
-
content,
|
|
21
|
+
content: prompts.reply_instructions.replace(/\$\{channel\}/g, channel),
|
|
25
22
|
display: true,
|
|
26
23
|
},
|
|
27
24
|
};
|