volute 0.1.0 → 0.2.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/README.md +1 -2
- package/dist/agent-manager-SSJUZWOV.js +13 -0
- package/dist/{channel-Q642YUZE.js → channel-2WJRM7PE.js} +2 -2
- package/dist/{chunk-H5XQARAP.js → chunk-4YXYAMFT.js} +3 -3
- package/dist/{chunk-5YW4B7CG.js → chunk-6UCG6MIX.js} +72 -23
- package/dist/{chunk-A5ZJEMHT.js → chunk-KFNNHQK7.js} +4 -4
- package/dist/chunk-L3BQEZ4Z.js +271 -0
- package/dist/{chunk-N4QN44LC.js → chunk-MY74SUOL.js} +29 -22
- package/dist/{chunk-KSMIWOCN.js → chunk-N4YNKR3Q.js} +6 -0
- package/dist/cli.js +23 -19
- package/dist/{connect-LW6G23AV.js → connect-X5V5IMRW.js} +3 -3
- package/dist/connectors/discord.js +9 -2
- package/dist/{create-3K6O2SDC.js → create-23AM7H5B.js} +1 -1
- package/dist/{daemon-client-ZTHW7ROS.js → daemon-client-VN24HM5T.js} +2 -2
- package/dist/daemon.js +394 -436
- package/dist/{delete-JNGY7ZFH.js → delete-GDMSOW3U.js} +2 -2
- package/dist/{disconnect-ACVTKTRE.js → disconnect-5JWFZ6RV.js} +2 -2
- package/dist/{down-FYCUYC5H.js → down-WTF73FE7.js} +5 -4
- package/dist/{env-7SLRN3MG.js → env-YKUJOFHE.js} +12 -5
- package/dist/{fork-BB3DZ426.js → fork-GRSVMBKI.js} +39 -32
- package/dist/history-7WVVKMUY.js +46 -0
- package/dist/{import-W2AMTEV5.js → import-42DOLBDT.js} +1 -1
- package/dist/{logs-BUHRIQ2L.js → logs-SYRQOL6B.js} +1 -1
- package/dist/{merge-446QTE7Q.js → merge-CSAVLSLY.js} +33 -36
- package/dist/{schedule-KKSOVUDF.js → schedule-J37XQM6E.js} +2 -2
- package/dist/{send-WQSVSRDD.js → send-PLOYEYER.js} +7 -5
- package/dist/{start-LKMWS6ZE.js → start-AG7QLULK.js} +2 -2
- package/dist/{status-CIEKUI3V.js → status-GCNU4M3K.js} +9 -2
- package/dist/{stop-YTOAGYE4.js → stop-IL5Q6NER.js} +2 -2
- package/dist/{up-AJJ4GCXY.js → up-ZC6G6K4K.js} +21 -37
- package/dist/{upgrade-JACA6YMO.js → upgrade-DD5TNJWU.js} +3 -5
- package/dist/{variants-HPY4DEWU.js → variants-QQIEKT6M.js} +2 -2
- package/drizzle/0000_flaky_mariko_yashida.sql +34 -0
- package/drizzle/0001_careless_warpath.sql +12 -0
- package/drizzle/meta/0000_snapshot.json +227 -0
- package/drizzle/meta/0001_snapshot.json +298 -0
- package/drizzle/meta/_journal.json +20 -0
- package/package.json +2 -1
- package/templates/_base/.init/.config/hooks/startup-context.sh +28 -0
- package/templates/_base/_skills/memory/SKILL.md +56 -13
- package/templates/_base/_skills/volute-agent/SKILL.md +27 -3
- package/templates/_base/home/VOLUTE.md +25 -0
- package/templates/_base/src/lib/format-prefix.ts +24 -0
- package/templates/_base/src/lib/sessions.ts +71 -0
- package/templates/_base/src/lib/startup.ts +132 -0
- package/templates/_base/src/lib/types.ts +3 -0
- package/templates/_base/src/lib/volute-server.ts +18 -2
- package/templates/agent-sdk/.init/.claude/settings.json +14 -0
- package/templates/agent-sdk/.init/.config/sessions.json +4 -0
- package/templates/agent-sdk/.init/CLAUDE.md +3 -2
- package/templates/agent-sdk/package.json.tmpl +1 -1
- package/templates/agent-sdk/src/agent.ts +101 -0
- package/templates/agent-sdk/src/lib/agent-sessions.ts +180 -0
- package/templates/agent-sdk/src/server.ts +33 -129
- package/templates/agent-sdk/volute-template.json +1 -1
- package/templates/pi/.init/.config/sessions.json +1 -0
- package/templates/pi/.init/AGENTS.md +2 -1
- package/templates/pi/src/agent.ts +61 -0
- package/templates/pi/src/lib/agent-sessions.ts +188 -0
- package/templates/pi/src/server.ts +28 -102
- package/templates/pi/volute-template.json +1 -1
- package/templates/agent-sdk/src/lib/agent.ts +0 -199
- package/templates/pi/src/lib/agent.ts +0 -205
- /package/templates/_base/.init/memory/{.gitkeep → journal/.gitkeep} +0 -0
- /package/templates/_base/{volute.json.tmpl → home/.config/volute.json.tmpl} +0 -0
- /package/templates/pi/{volute.json.tmpl → home/.config/volute.json.tmpl} +0 -0
|
@@ -1,121 +1,47 @@
|
|
|
1
|
-
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
1
|
import { resolve } from "node:path";
|
|
3
|
-
import { createAgent } from "./
|
|
2
|
+
import { createAgent } from "./agent.js";
|
|
4
3
|
import { log } from "./lib/logger.js";
|
|
4
|
+
import {
|
|
5
|
+
handleMergeContext,
|
|
6
|
+
handleStartupContext,
|
|
7
|
+
loadConfig,
|
|
8
|
+
loadPackageInfo,
|
|
9
|
+
loadSystemPrompt,
|
|
10
|
+
parseArgs,
|
|
11
|
+
setupShutdown,
|
|
12
|
+
} from "./lib/startup.js";
|
|
5
13
|
import { createVoluteServer } from "./lib/volute-server.js";
|
|
6
14
|
|
|
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
15
|
const { port } = parseArgs();
|
|
37
16
|
const config = loadConfig();
|
|
38
|
-
|
|
39
|
-
const soulPath = resolve("home/SOUL.md");
|
|
40
|
-
const memoryPath = resolve("home/MEMORY.md");
|
|
41
|
-
const volutePath = resolve("home/VOLUTE.md");
|
|
17
|
+
if (config.model) log("server", `using model: ${config.model}`);
|
|
42
18
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
console.error(`Could not read soul file: ${soulPath}`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
19
|
+
const systemPrompt = loadSystemPrompt();
|
|
20
|
+
const pkg = loadPackageInfo();
|
|
48
21
|
|
|
49
|
-
const
|
|
50
|
-
const volute = loadFile(volutePath);
|
|
51
|
-
|
|
52
|
-
const promptParts = [soul];
|
|
53
|
-
if (volute) promptParts.push(volute);
|
|
54
|
-
if (memory) promptParts.push(`## Memory\n\n${memory}`);
|
|
55
|
-
const systemPrompt = promptParts.join("\n\n---\n\n");
|
|
56
|
-
|
|
57
|
-
// Check if a session directory exists (indicates resumable session)
|
|
58
|
-
const sessionsDir = resolve("home/.pi/sessions");
|
|
59
|
-
const hasExistingSession = existsSync(sessionsDir);
|
|
60
|
-
if (hasExistingSession) {
|
|
61
|
-
log("server", "found existing sessions, will attempt resume");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Read name/version from package.json for health endpoint
|
|
65
|
-
let pkgName = "unknown";
|
|
66
|
-
let pkgVersion = "0.0.0";
|
|
67
|
-
try {
|
|
68
|
-
const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf-8"));
|
|
69
|
-
pkgName = pkg.name || pkgName;
|
|
70
|
-
pkgVersion = pkg.version || pkgVersion;
|
|
71
|
-
} catch {}
|
|
72
|
-
|
|
73
|
-
const agent = await createAgent({
|
|
22
|
+
const agent = createAgent({
|
|
74
23
|
systemPrompt,
|
|
75
24
|
cwd: resolve("home"),
|
|
76
|
-
model,
|
|
77
|
-
|
|
78
|
-
onCompact: () => {
|
|
79
|
-
log("server", "pre-compact — asking agent to update daily log");
|
|
80
|
-
agent.sendMessage(
|
|
81
|
-
"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.",
|
|
82
|
-
"system",
|
|
83
|
-
);
|
|
84
|
-
},
|
|
25
|
+
model: config.model,
|
|
26
|
+
compactionMessage: config.compactionMessage,
|
|
85
27
|
});
|
|
86
28
|
|
|
87
|
-
const server = createVoluteServer({
|
|
29
|
+
const server = createVoluteServer({
|
|
30
|
+
agent,
|
|
31
|
+
port,
|
|
32
|
+
name: pkg.name,
|
|
33
|
+
version: pkg.version,
|
|
34
|
+
sessionsConfigPath: resolve("home/.config/sessions.json"),
|
|
35
|
+
});
|
|
88
36
|
|
|
89
|
-
server.listen(port, () => {
|
|
37
|
+
server.listen(port, async () => {
|
|
90
38
|
const addr = server.address();
|
|
91
39
|
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
92
40
|
log("server", `listening on :${actualPort}`);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (existsSync(mergedPath)) {
|
|
97
|
-
try {
|
|
98
|
-
const merged = JSON.parse(readFileSync(mergedPath, "utf-8"));
|
|
99
|
-
unlinkSync(mergedPath);
|
|
100
|
-
|
|
101
|
-
const parts = [
|
|
102
|
-
`[system] Variant "${merged.name}" has been merged and you have been restarted.`,
|
|
103
|
-
];
|
|
104
|
-
if (merged.summary) parts.push(`Changes: ${merged.summary}`);
|
|
105
|
-
if (merged.justification) parts.push(`Why: ${merged.justification}`);
|
|
106
|
-
if (merged.memory) parts.push(`Context: ${merged.memory}`);
|
|
107
|
-
|
|
108
|
-
agent.sendMessage(parts.join("\n"));
|
|
109
|
-
log("server", `sent post-merge orientation for variant: ${merged.name}`);
|
|
110
|
-
} catch (e) {
|
|
111
|
-
log("server", "failed to process merged.json:", e);
|
|
112
|
-
}
|
|
41
|
+
const hasMerge = handleMergeContext((content) => agent.sendMessage(content));
|
|
42
|
+
if (!hasMerge) {
|
|
43
|
+
await handleStartupContext((content) => agent.sendMessage(content));
|
|
113
44
|
}
|
|
114
45
|
});
|
|
115
46
|
|
|
116
|
-
|
|
117
|
-
log("server", "shutdown signal received");
|
|
118
|
-
process.exit(0);
|
|
119
|
-
}
|
|
120
|
-
process.on("SIGINT", shutdown);
|
|
121
|
-
process.on("SIGTERM", shutdown);
|
|
47
|
+
setupShutdown();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"rename": {
|
|
3
3
|
"package.json.tmpl": "package.json",
|
|
4
4
|
"biome.json.tmpl": "biome.json",
|
|
5
|
-
"volute.json.tmpl": "volute.json"
|
|
5
|
+
"home/.config/volute.json.tmpl": "home/.config/volute.json"
|
|
6
6
|
},
|
|
7
7
|
"substitute": ["package.json", ".init/SOUL.md"],
|
|
8
8
|
"skillsDir": "home/.claude/skills"
|
|
@@ -1,199 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { getModel, getModels, type ImageContent } from "@mariozechner/pi-ai";
|
|
2
|
-
import {
|
|
3
|
-
AuthStorage,
|
|
4
|
-
createAgentSession,
|
|
5
|
-
DefaultResourceLoader,
|
|
6
|
-
type ExtensionFactory,
|
|
7
|
-
ModelRegistry,
|
|
8
|
-
SessionManager,
|
|
9
|
-
SettingsManager,
|
|
10
|
-
} from "@mariozechner/pi-coding-agent";
|
|
11
|
-
import { commitFileChange } from "./auto-commit.js";
|
|
12
|
-
import { log, logMessage, logText, logThinking, logToolResult, logToolUse } from "./logger.js";
|
|
13
|
-
import type { ChannelMeta, VoluteContentPart, VoluteEvent } from "./types.js";
|
|
14
|
-
|
|
15
|
-
type Listener = (event: VoluteEvent) => void;
|
|
16
|
-
|
|
17
|
-
function formatPrefix(meta: ChannelMeta | undefined): string {
|
|
18
|
-
if (!meta?.channel && !meta?.sender) return "";
|
|
19
|
-
const platform =
|
|
20
|
-
meta.platform ??
|
|
21
|
-
(() => {
|
|
22
|
-
const n = (meta.channel ?? "").split(":")[0];
|
|
23
|
-
return n.charAt(0).toUpperCase() + n.slice(1);
|
|
24
|
-
})();
|
|
25
|
-
let sender = meta.sender ?? "";
|
|
26
|
-
if (meta.isDM) {
|
|
27
|
-
sender += " in DM";
|
|
28
|
-
} else if (meta.channelName) {
|
|
29
|
-
sender += ` in #${meta.channelName}`;
|
|
30
|
-
if (meta.guildName) sender += ` in ${meta.guildName}`;
|
|
31
|
-
}
|
|
32
|
-
const parts = [platform, sender].filter(Boolean);
|
|
33
|
-
return parts.length > 0 ? `[${parts.join(": ")}]\n` : "";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function createAgent(options: {
|
|
37
|
-
systemPrompt: string;
|
|
38
|
-
cwd: string;
|
|
39
|
-
model?: string;
|
|
40
|
-
resume?: boolean;
|
|
41
|
-
onCompact?: () => void;
|
|
42
|
-
}) {
|
|
43
|
-
const listeners = new Set<Listener>();
|
|
44
|
-
|
|
45
|
-
// Block compaction once so the agent can update its daily log with full context
|
|
46
|
-
let compactBlocked = false;
|
|
47
|
-
const preCompactExtension: ExtensionFactory = (pi) => {
|
|
48
|
-
pi.on("session_before_compact", () => {
|
|
49
|
-
if (!compactBlocked) {
|
|
50
|
-
compactBlocked = true;
|
|
51
|
-
log("agent", "blocking compaction — asking agent to update daily log first");
|
|
52
|
-
if (options.onCompact) options.onCompact();
|
|
53
|
-
return { cancel: true };
|
|
54
|
-
}
|
|
55
|
-
compactBlocked = false;
|
|
56
|
-
log("agent", "allowing compaction");
|
|
57
|
-
});
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Parse model string: "provider:model-id" or use default
|
|
61
|
-
const modelStr = options.model || process.env.PI_MODEL || "anthropic:claude-sonnet-4-20250514";
|
|
62
|
-
const [provider, ...rest] = modelStr.split(":");
|
|
63
|
-
const modelId = rest.join(":");
|
|
64
|
-
|
|
65
|
-
// Try exact match first, then prefix match against available models
|
|
66
|
-
let model = getModel(provider as any, modelId as any);
|
|
67
|
-
if (!model) {
|
|
68
|
-
const available = getModels(provider as any);
|
|
69
|
-
const found = available.find((m) => m.id.startsWith(modelId));
|
|
70
|
-
if (found) model = found;
|
|
71
|
-
}
|
|
72
|
-
if (!model) {
|
|
73
|
-
const available = getModels(provider as any);
|
|
74
|
-
throw new Error(
|
|
75
|
-
`Model not found: ${modelStr}\nAvailable ${provider} models: ${available.map((m) => m.id).join(", ")}`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const authStorage = new AuthStorage();
|
|
80
|
-
const modelRegistry = new ModelRegistry(authStorage);
|
|
81
|
-
|
|
82
|
-
// Use persistent sessions in cwd for resume support, or in-memory if not resuming
|
|
83
|
-
const sessionManager = options.resume
|
|
84
|
-
? SessionManager.continueRecent(options.cwd)
|
|
85
|
-
: SessionManager.create(options.cwd);
|
|
86
|
-
|
|
87
|
-
const settingsManager = SettingsManager.inMemory({
|
|
88
|
-
retry: { enabled: true, maxRetries: 3 },
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
92
|
-
cwd: options.cwd,
|
|
93
|
-
settingsManager,
|
|
94
|
-
systemPrompt: options.systemPrompt,
|
|
95
|
-
extensionFactories: [preCompactExtension],
|
|
96
|
-
});
|
|
97
|
-
await resourceLoader.reload();
|
|
98
|
-
|
|
99
|
-
const { session } = await createAgentSession({
|
|
100
|
-
cwd: options.cwd,
|
|
101
|
-
model,
|
|
102
|
-
authStorage,
|
|
103
|
-
modelRegistry,
|
|
104
|
-
sessionManager,
|
|
105
|
-
settingsManager,
|
|
106
|
-
resourceLoader,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
function broadcast(event: VoluteEvent) {
|
|
110
|
-
for (const listener of listeners) {
|
|
111
|
-
try {
|
|
112
|
-
listener(event);
|
|
113
|
-
} catch (err) {
|
|
114
|
-
log("agent", "listener threw during broadcast:", err);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const toolArgs = new Map<string, any>();
|
|
120
|
-
|
|
121
|
-
session.subscribe((event) => {
|
|
122
|
-
if (event.type === "message_update") {
|
|
123
|
-
const ae = event.assistantMessageEvent;
|
|
124
|
-
if (ae.type === "text_delta") {
|
|
125
|
-
logText(ae.delta);
|
|
126
|
-
broadcast({ type: "text", content: ae.delta });
|
|
127
|
-
} else if (ae.type === "thinking_delta") {
|
|
128
|
-
logThinking(ae.delta);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (event.type === "tool_execution_start") {
|
|
133
|
-
toolArgs.set(event.toolCallId, event.args);
|
|
134
|
-
logToolUse(event.toolName, event.args);
|
|
135
|
-
broadcast({
|
|
136
|
-
type: "tool_use",
|
|
137
|
-
name: event.toolName,
|
|
138
|
-
input: event.args,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (event.type === "tool_execution_end") {
|
|
143
|
-
const output = typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
144
|
-
logToolResult(event.toolName, output, event.isError);
|
|
145
|
-
broadcast({
|
|
146
|
-
type: "tool_result",
|
|
147
|
-
output,
|
|
148
|
-
is_error: event.isError,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Auto-commit file changes in home/
|
|
152
|
-
if ((event.toolName === "Edit" || event.toolName === "Write") && !event.isError) {
|
|
153
|
-
const args = toolArgs.get(event.toolCallId);
|
|
154
|
-
const filePath = (args as { file_path?: string })?.file_path;
|
|
155
|
-
if (filePath) {
|
|
156
|
-
commitFileChange(filePath, options.cwd);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
toolArgs.delete(event.toolCallId);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (event.type === "agent_end") {
|
|
163
|
-
log("agent", "turn done");
|
|
164
|
-
broadcast({ type: "done" });
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
function sendMessage(content: string | VoluteContentPart[], meta?: ChannelMeta) {
|
|
169
|
-
const raw =
|
|
170
|
-
typeof content === "string"
|
|
171
|
-
? content
|
|
172
|
-
: content
|
|
173
|
-
.filter((p) => p.type === "text")
|
|
174
|
-
.map((p) => (p as { text: string }).text)
|
|
175
|
-
.join("\n");
|
|
176
|
-
logMessage("in", raw, meta?.channel);
|
|
177
|
-
|
|
178
|
-
// Build context prefix from channel metadata
|
|
179
|
-
const prefix = formatPrefix(meta);
|
|
180
|
-
const text = prefix + raw;
|
|
181
|
-
|
|
182
|
-
// Convert image parts to pi-ai ImageContent format
|
|
183
|
-
const images: ImageContent[] | undefined =
|
|
184
|
-
typeof content === "string"
|
|
185
|
-
? undefined
|
|
186
|
-
: content
|
|
187
|
-
.filter((p) => p.type === "image")
|
|
188
|
-
.map((p) => ({ type: "image" as const, mimeType: p.media_type, data: p.data }));
|
|
189
|
-
|
|
190
|
-
const opts = images?.length ? { images } : {};
|
|
191
|
-
|
|
192
|
-
if (session.isStreaming) {
|
|
193
|
-
session.prompt(text, { streamingBehavior: "followUp", ...opts });
|
|
194
|
-
} else {
|
|
195
|
-
session.prompt(text, opts);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function onMessage(listener: Listener): () => void {
|
|
200
|
-
listeners.add(listener);
|
|
201
|
-
return () => listeners.delete(listener);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return { sendMessage, onMessage };
|
|
205
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|