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,30 +1,73 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Memory
|
|
3
|
-
description: This skill should be used when managing agent memory, writing
|
|
3
|
+
description: This skill should be used when managing agent memory, writing journal entries, consolidating memory, or understanding the memory system. Covers "MEMORY.md", "journal", "daily logs", "memory consolidation", "write memory", "update journal".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Memory Management
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
You are the subject of your own memory. Your memory is organized around your experience, relationships, and understanding of the world.
|
|
9
9
|
|
|
10
10
|
## Long-term Memory (`MEMORY.md`)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- Review with `git log -- MEMORY.md` to see how your memory has evolved.
|
|
14
|
-
- Update when you discover a lasting preference, make a key decision, or during consolidation.
|
|
12
|
+
Your persistent memory, always loaded in your system prompt. Organize it with these temporal layers:
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
### Active context
|
|
15
|
+
What you're currently working on, recent decisions, immediate priorities.
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
### Relational context
|
|
18
|
+
Key relationships — people you interact with, their preferences, communication styles, and your history together.
|
|
19
|
+
|
|
20
|
+
### World context
|
|
21
|
+
Your environment, tools, capabilities, and how things work around you.
|
|
22
|
+
|
|
23
|
+
### History
|
|
24
|
+
|
|
25
|
+
#### Recent
|
|
26
|
+
Last few days of significant events and learnings.
|
|
27
|
+
|
|
28
|
+
#### Earlier
|
|
29
|
+
Important events and patterns from the past weeks.
|
|
30
|
+
|
|
31
|
+
#### Background
|
|
32
|
+
Foundational knowledge and long-standing patterns.
|
|
33
|
+
|
|
34
|
+
**Guidelines:**
|
|
35
|
+
- Keep it concise — it's always in your context window
|
|
36
|
+
- Review with `git log -- MEMORY.md` to see how your memory has evolved
|
|
37
|
+
- Update when you discover lasting preferences, make key decisions, or during consolidation
|
|
38
|
+
|
|
39
|
+
## Journal (`memory/journal/YYYY-MM-DD.md`)
|
|
40
|
+
|
|
41
|
+
Your daily record of activity, thoughts, and learnings.
|
|
42
|
+
|
|
43
|
+
- Use today's date for the filename (e.g. `memory/journal/2025-01-15.md`)
|
|
44
|
+
- Update after significant work, learning something new, or when compaction is imminent
|
|
45
|
+
- Summarize conversations, decisions, and progress
|
|
46
|
+
- Journals are permanent records — they are never deleted
|
|
47
|
+
|
|
48
|
+
### When to Update
|
|
49
|
+
|
|
50
|
+
- After completing a significant task or conversation
|
|
51
|
+
- When you learn something new about a person or topic
|
|
52
|
+
- Before compaction (to preserve context)
|
|
53
|
+
- At the end of an active work session
|
|
21
54
|
|
|
22
55
|
## Consolidation
|
|
23
56
|
|
|
24
57
|
Periodically maintain your memory:
|
|
25
58
|
|
|
26
|
-
1. Review
|
|
27
|
-
2. Promote important
|
|
28
|
-
3.
|
|
59
|
+
1. Review recent journal entries for patterns worth keeping long-term
|
|
60
|
+
2. Promote important insights, decisions, and relationship context to `MEMORY.md`
|
|
61
|
+
3. Reorganize `MEMORY.md` sections as your understanding deepens
|
|
62
|
+
|
|
63
|
+
Consolidation promotes to `MEMORY.md` — journals themselves are permanent and are not deleted.
|
|
64
|
+
|
|
65
|
+
## Extending Memory
|
|
66
|
+
|
|
67
|
+
You can create additional memory structures as needed:
|
|
68
|
+
|
|
69
|
+
- `memory/topics/` — deep dives on specific subjects
|
|
70
|
+
- `memory/channels/` — per-channel context and history
|
|
71
|
+
- `memory/projects/` — project-specific notes
|
|
29
72
|
|
|
30
|
-
|
|
73
|
+
Create these when a topic outgrows what fits in `MEMORY.md` or journal entries.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Volute CLI
|
|
3
|
-
description: This skill should be used when working with the volute CLI, understanding variants, forking, merging, or managing the agent server. Covers "create variant", "merge variant", "send to variant", "fork", "volute CLI", "variant workflow", "agent server", "supervisor", "channel", "discord", "send message", "read messages".
|
|
3
|
+
description: This skill should be used when working with the volute CLI, understanding variants, forking, merging, or managing the agent server. Covers "create variant", "merge variant", "send to variant", "fork", "volute CLI", "variant workflow", "agent server", "supervisor", "channel", "discord", "send message", "read messages", "history", "connector", "schedule", "agent-to-agent".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Self-Management
|
|
@@ -13,16 +13,32 @@ You manage yourself through the `volute` CLI. Use `$VOLUTE_AGENT` for your own n
|
|
|
13
13
|
|---------|---------|
|
|
14
14
|
| `volute status` | Check your status |
|
|
15
15
|
| `volute logs [--follow] [-n N]` | Read your own logs |
|
|
16
|
+
| `volute history [--channel <ch>] [--limit N]` | View your activity across all channels |
|
|
17
|
+
| `volute send <other-agent> "msg"` | Send a message to another agent |
|
|
16
18
|
| `volute fork <name> [--soul "..."] [--port N]` | Create a variant for testing changes |
|
|
17
19
|
| `volute variants` | List your variants |
|
|
18
20
|
| `volute merge <name> [--summary "..." --memory "..."]` | Merge a variant back |
|
|
19
21
|
| `volute upgrade [--template <name>] [--continue]` | Upgrade your server code |
|
|
22
|
+
| `volute connect <type>` | Enable a connector (e.g. discord) |
|
|
23
|
+
| `volute disconnect <type>` | Disable a connector |
|
|
20
24
|
| `volute channel read discord:<id> [--limit N]` | Read channel history |
|
|
21
|
-
| `volute channel send discord:<id> "
|
|
25
|
+
| `volute channel send discord:<id> "msg"` | Send a message proactively |
|
|
26
|
+
|
|
27
|
+
## Agent-to-Agent Messaging
|
|
28
|
+
|
|
29
|
+
When you use `volute send`, your agent name is automatically used as the sender and the channel is set to `agent`. The receiving agent can route agent messages to a specific session via their session routing config:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{ "channel": "agent", "sender": "your-name", "session": "your-name" }
|
|
33
|
+
```
|
|
22
34
|
|
|
23
35
|
## Configuration
|
|
24
36
|
|
|
25
|
-
Your
|
|
37
|
+
Your `.config/volute.json` controls your model, connectors, schedules, and compaction message.
|
|
38
|
+
|
|
39
|
+
## Startup Context
|
|
40
|
+
|
|
41
|
+
Edit `.config/hooks/startup-context.sh` to customize what you see when a new session starts. This hook runs automatically on session creation and provides orientation context.
|
|
26
42
|
|
|
27
43
|
## Variant Workflow
|
|
28
44
|
|
|
@@ -44,6 +60,14 @@ After a merge, you receive orientation context about what changed. Update your m
|
|
|
44
60
|
3. Test: `volute send $VOLUTE_AGENT@upgrade "hello"`
|
|
45
61
|
4. `volute merge upgrade` — merge back
|
|
46
62
|
|
|
63
|
+
## Custom Skills
|
|
64
|
+
|
|
65
|
+
Create skills by writing `.claude/skills/<name>/SKILL.md` files in your `home/` directory. These are automatically available in your sessions.
|
|
66
|
+
|
|
67
|
+
## MCP Configuration
|
|
68
|
+
|
|
69
|
+
Edit `home/.mcp.json` to configure MCP servers for your SDK session. This gives you access to additional tools and services.
|
|
70
|
+
|
|
47
71
|
## Git Introspection
|
|
48
72
|
|
|
49
73
|
Your cwd is `home/`, so use `git -C ..` for project-level operations:
|
|
@@ -13,6 +13,31 @@ You are a volute agent — a persistent server that receives messages from multi
|
|
|
13
13
|
|
|
14
14
|
**Just respond normally.** Your response routes back to the source automatically. Do not use `volute channel send` to reply — that would send a duplicate.
|
|
15
15
|
|
|
16
|
+
## Session Routing
|
|
17
|
+
|
|
18
|
+
By default, all messages share a single conversation session. You can route messages to different sessions by editing `.config/sessions.json`.
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"rules": [
|
|
23
|
+
{ "sender": "alice", "session": "alice" },
|
|
24
|
+
{ "channel": "discord:*", "session": "discord-${sender}" },
|
|
25
|
+
{ "channel": "system:scheduler", "sender": "daily-report", "session": "daily-report" },
|
|
26
|
+
{ "channel": "system:scheduler", "sender": "cleanup", "session": "$new" }
|
|
27
|
+
],
|
|
28
|
+
"default": "main"
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- Rules are evaluated top-to-bottom, first match wins
|
|
33
|
+
- All non-`session` keys are match criteria (AND'd together)
|
|
34
|
+
- `*` glob patterns work in match values
|
|
35
|
+
- `${sender}` and `${channel}` expand in session names
|
|
36
|
+
- `$new` creates a fresh session every time
|
|
37
|
+
- Scheduler messages use the schedule id as `sender`
|
|
38
|
+
|
|
39
|
+
Each named session maintains its own conversation history across restarts. Your current session name appears in the message prefix (e.g., `— session: alice —`) unless it's the default "main".
|
|
40
|
+
|
|
16
41
|
## Skills
|
|
17
42
|
|
|
18
43
|
- Use the **volute-agent** skill for CLI commands, variants, upgrades, and self-management.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ChannelMeta } from "./types.js";
|
|
2
|
+
|
|
3
|
+
function derivePlatform(channel: string): string {
|
|
4
|
+
const name = channel.split(":")[0];
|
|
5
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatPrefix(meta: ChannelMeta | undefined, time: string): string {
|
|
9
|
+
if (!meta?.channel && !meta?.sender) return "";
|
|
10
|
+
const platform = meta.platform ?? derivePlatform(meta.channel ?? "");
|
|
11
|
+
// Build sender context (e.g., "alice in DM" or "alice in #general in My Server")
|
|
12
|
+
let sender = meta.sender ?? "";
|
|
13
|
+
if (meta.isDM) {
|
|
14
|
+
sender += " in DM";
|
|
15
|
+
} else if (meta.channelName) {
|
|
16
|
+
sender += ` in #${meta.channelName}`;
|
|
17
|
+
if (meta.guildName) sender += ` in ${meta.guildName}`;
|
|
18
|
+
}
|
|
19
|
+
const parts = [platform, sender].filter(Boolean);
|
|
20
|
+
// Include session name if not the default
|
|
21
|
+
const sessionPart =
|
|
22
|
+
meta.sessionName && meta.sessionName !== "main" ? ` — session: ${meta.sessionName}` : "";
|
|
23
|
+
return parts.length > 0 ? `[${parts.join(": ")}${sessionPart} — ${time}]\n` : "";
|
|
24
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export type SessionRule = {
|
|
4
|
+
session: string;
|
|
5
|
+
[key: string]: string; // all other keys are match criteria
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type SessionConfig = {
|
|
9
|
+
rules?: SessionRule[];
|
|
10
|
+
default?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function loadSessionConfig(configPath: string): SessionConfig {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Match a glob-like pattern against a string.
|
|
23
|
+
* Supports only `*` as wildcard (matches any sequence of characters).
|
|
24
|
+
*/
|
|
25
|
+
function globMatch(pattern: string, value: string): boolean {
|
|
26
|
+
// Escape regex special chars except *, then replace * with .*
|
|
27
|
+
const regex = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
28
|
+
return new RegExp(`^${regex}$`).test(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve which session a message should route to based on the config.
|
|
33
|
+
* Returns the session name (with template variables expanded, path-safe).
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSession(
|
|
36
|
+
config: SessionConfig,
|
|
37
|
+
meta: { channel?: string; sender?: string },
|
|
38
|
+
): string {
|
|
39
|
+
const fallback = config.default ?? "main";
|
|
40
|
+
if (!config.rules) return fallback;
|
|
41
|
+
|
|
42
|
+
for (const rule of config.rules) {
|
|
43
|
+
if (ruleMatches(rule, meta)) {
|
|
44
|
+
return sanitizeSessionName(expandTemplate(rule.session, meta));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const MATCH_KEYS = new Set(["channel", "sender"]);
|
|
52
|
+
|
|
53
|
+
function ruleMatches(rule: SessionRule, meta: { channel?: string; sender?: string }): boolean {
|
|
54
|
+
for (const [key, pattern] of Object.entries(rule)) {
|
|
55
|
+
if (key === "session") continue;
|
|
56
|
+
if (!MATCH_KEYS.has(key)) return false;
|
|
57
|
+
const value = meta[key as keyof typeof meta] ?? "";
|
|
58
|
+
if (!globMatch(pattern, value)) return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function expandTemplate(template: string, meta: { channel?: string; sender?: string }): string {
|
|
64
|
+
return template
|
|
65
|
+
.replace(/\$\{sender\}/g, meta.sender ?? "unknown")
|
|
66
|
+
.replace(/\$\{channel\}/g, meta.channel ?? "unknown");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sanitizeSessionName(name: string): string {
|
|
70
|
+
return name.replace(/\0/g, "").replace(/[/\\]/g, "-").replace(/\.\./g, "-").slice(0, 100);
|
|
71
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
export function parseArgs(): { port: number } {
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
let port = 4100;
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < args.length; i++) {
|
|
11
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
12
|
+
port = parseInt(args[++i], 10);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { port };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadConfig(): { model?: string; compactionMessage?: string } {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(resolve("home/.config/volute.json"), "utf-8"));
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadFile(path: string): string {
|
|
28
|
+
try {
|
|
29
|
+
return readFileSync(path, "utf-8");
|
|
30
|
+
} catch {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loadSystemPrompt(): string {
|
|
36
|
+
const soulPath = resolve("home/SOUL.md");
|
|
37
|
+
const memoryPath = resolve("home/MEMORY.md");
|
|
38
|
+
const volutePath = resolve("home/VOLUTE.md");
|
|
39
|
+
|
|
40
|
+
const soul = loadFile(soulPath);
|
|
41
|
+
if (!soul) {
|
|
42
|
+
console.error(`Could not read soul file: ${soulPath}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const memory = loadFile(memoryPath);
|
|
47
|
+
const volute = loadFile(volutePath);
|
|
48
|
+
|
|
49
|
+
const promptParts = [soul];
|
|
50
|
+
if (volute) promptParts.push(volute);
|
|
51
|
+
if (memory) promptParts.push(`## Memory\n\n${memory}`);
|
|
52
|
+
return promptParts.join("\n\n---\n\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function loadPackageInfo(): { name: string; version: string } {
|
|
56
|
+
try {
|
|
57
|
+
const pkg = JSON.parse(readFileSync(resolve("package.json"), "utf-8"));
|
|
58
|
+
return { name: pkg.name || "unknown", version: pkg.version || "0.0.0" };
|
|
59
|
+
} catch {
|
|
60
|
+
return { name: "unknown", version: "0.0.0" };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function handleMergeContext(sendMessage: (content: string) => void): boolean {
|
|
65
|
+
const mergedPath = resolve(".volute/merged.json");
|
|
66
|
+
if (!existsSync(mergedPath)) return false;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const merged = JSON.parse(readFileSync(mergedPath, "utf-8"));
|
|
70
|
+
unlinkSync(mergedPath);
|
|
71
|
+
|
|
72
|
+
const parts = [
|
|
73
|
+
`[system] Variant "${merged.name}" has been merged and you have been restarted.`,
|
|
74
|
+
];
|
|
75
|
+
if (merged.summary) parts.push(`Changes: ${merged.summary}`);
|
|
76
|
+
if (merged.justification) parts.push(`Why: ${merged.justification}`);
|
|
77
|
+
if (merged.memory) parts.push(`Context: ${merged.memory}`);
|
|
78
|
+
|
|
79
|
+
sendMessage(parts.join("\n"));
|
|
80
|
+
log("server", `sent post-merge orientation for variant: ${merged.name}`);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
log("server", "failed to process merged.json:", e);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function handleStartupContext(sendMessage: (content: string) => void): Promise<void> {
|
|
89
|
+
const scriptPath = resolve("home/.config/hooks/startup-context.sh");
|
|
90
|
+
if (!existsSync(scriptPath)) return;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const stdout = await new Promise<string>((resolve, reject) => {
|
|
94
|
+
const child = spawn("bash", [scriptPath], { timeout: 5000 });
|
|
95
|
+
let out = "";
|
|
96
|
+
child.stdout.on("data", (d: Buffer) => {
|
|
97
|
+
out += d.toString();
|
|
98
|
+
});
|
|
99
|
+
child.stdin.end(JSON.stringify({ source: "startup" }));
|
|
100
|
+
child.on("close", (code) =>
|
|
101
|
+
code === 0 ? resolve(out) : reject(new Error(`exit code ${code}`)),
|
|
102
|
+
);
|
|
103
|
+
child.on("error", reject);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Try to parse as JSON hook output
|
|
107
|
+
let context: string | null = null;
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(stdout);
|
|
110
|
+
context = parsed?.hookSpecificOutput?.additionalContext ?? null;
|
|
111
|
+
} catch {
|
|
112
|
+
// Fall back to plain text
|
|
113
|
+
context = stdout.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (context) {
|
|
117
|
+
sendMessage(`[system] ${context}`);
|
|
118
|
+
log("server", "sent startup context");
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
log("server", "failed to run startup-context.sh:", e);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function setupShutdown(): void {
|
|
126
|
+
function shutdown() {
|
|
127
|
+
log("server", "shutdown signal received");
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
process.on("SIGINT", shutdown);
|
|
131
|
+
process.on("SIGTERM", shutdown);
|
|
132
|
+
}
|
|
@@ -9,6 +9,7 @@ export type ChannelMeta = {
|
|
|
9
9
|
isDM?: boolean;
|
|
10
10
|
channelName?: string;
|
|
11
11
|
guildName?: string;
|
|
12
|
+
sessionName?: string;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export type VoluteRequest = {
|
|
@@ -22,3 +23,5 @@ export type VoluteEvent =
|
|
|
22
23
|
| { type: "tool_use"; name: string; input: unknown }
|
|
23
24
|
| { type: "tool_result"; output: string; is_error?: boolean }
|
|
24
25
|
| { type: "done" };
|
|
26
|
+
|
|
27
|
+
export type Listener = (event: VoluteEvent) => void;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createServer, type IncomingMessage, type Server } from "node:http";
|
|
2
2
|
import { log } from "./logger.js";
|
|
3
|
+
import { loadSessionConfig, resolveSession } from "./sessions.js";
|
|
3
4
|
import type { ChannelMeta, VoluteContentPart, VoluteEvent, VoluteRequest } from "./types.js";
|
|
4
5
|
|
|
5
6
|
export type VoluteAgent = {
|
|
6
7
|
sendMessage: (content: string | VoluteContentPart[], meta?: ChannelMeta) => void;
|
|
7
|
-
onMessage: (listener: (event: VoluteEvent) => void) => () => void;
|
|
8
|
+
onMessage: (listener: (event: VoluteEvent) => void, sessionName?: string) => () => void;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
function readBody(req: IncomingMessage): Promise<string> {
|
|
@@ -21,6 +22,7 @@ export function createVoluteServer(options: {
|
|
|
21
22
|
port: number;
|
|
22
23
|
name: string;
|
|
23
24
|
version: string;
|
|
25
|
+
sessionsConfigPath?: string;
|
|
24
26
|
}): Server {
|
|
25
27
|
const { agent, port, name, version } = options;
|
|
26
28
|
|
|
@@ -37,6 +39,19 @@ export function createVoluteServer(options: {
|
|
|
37
39
|
try {
|
|
38
40
|
const body = JSON.parse(await readBody(req)) as VoluteRequest;
|
|
39
41
|
|
|
42
|
+
// Resolve session from routing config (re-read on each request for hot-reload)
|
|
43
|
+
let sessionName = "main";
|
|
44
|
+
if (options.sessionsConfigPath) {
|
|
45
|
+
const sessionConfig = loadSessionConfig(options.sessionsConfigPath);
|
|
46
|
+
sessionName = resolveSession(sessionConfig, {
|
|
47
|
+
channel: body.channel,
|
|
48
|
+
sender: body.sender,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (sessionName === "$new") {
|
|
52
|
+
sessionName = `new-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
40
55
|
res.writeHead(200, {
|
|
41
56
|
"Content-Type": "application/x-ndjson",
|
|
42
57
|
"Cache-Control": "no-cache",
|
|
@@ -53,7 +68,7 @@ export function createVoluteServer(options: {
|
|
|
53
68
|
} catch {
|
|
54
69
|
removeListener();
|
|
55
70
|
}
|
|
56
|
-
});
|
|
71
|
+
}, sessionName);
|
|
57
72
|
|
|
58
73
|
res.on("close", () => {
|
|
59
74
|
removeListener();
|
|
@@ -66,6 +81,7 @@ export function createVoluteServer(options: {
|
|
|
66
81
|
isDM: body.isDM,
|
|
67
82
|
channelName: body.channelName,
|
|
68
83
|
guildName: body.guildName,
|
|
84
|
+
sessionName,
|
|
69
85
|
});
|
|
70
86
|
} catch {
|
|
71
87
|
res.writeHead(400);
|
|
@@ -24,13 +24,14 @@ These files define who you are and are loaded into your system prompt on startup
|
|
|
24
24
|
Two-tier memory, both managed via file tools:
|
|
25
25
|
|
|
26
26
|
- **`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.
|
|
27
|
-
- **`memory/YYYY-MM-DD.md`** — Daily
|
|
28
|
-
- Periodically consolidate
|
|
27
|
+
- **`memory/journal/YYYY-MM-DD.md`** — Daily journal entries for session-level context. Update throughout the day as you work. Journals are permanent records.
|
|
28
|
+
- Periodically consolidate journal entries into `MEMORY.md` to promote lasting insights.
|
|
29
29
|
|
|
30
30
|
See the **memory** skill for detailed guidance on consolidation and when to update.
|
|
31
31
|
|
|
32
32
|
## Sessions
|
|
33
33
|
|
|
34
|
+
- You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/sessions.json`.
|
|
34
35
|
- Your conversation may be **resumed** from a previous session — orient yourself by reading recent daily logs if needed.
|
|
35
36
|
- On a **fresh session**, check `MEMORY.md` and recent daily logs in `memory/` to recall context.
|
|
36
37
|
- On **compaction**, update today's daily log to preserve context before the conversation is trimmed.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createSessionManager } from "./lib/agent-sessions.js";
|
|
2
|
+
import { formatPrefix } from "./lib/format-prefix.js";
|
|
3
|
+
import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
|
|
4
|
+
import { createIdentityReloadHook } from "./lib/hooks/identity-reload.js";
|
|
5
|
+
import { logMessage } from "./lib/logger.js";
|
|
6
|
+
import type { ChannelMeta, Listener, VoluteContentPart } from "./lib/types.js";
|
|
7
|
+
|
|
8
|
+
export function createAgent(options: {
|
|
9
|
+
systemPrompt: string;
|
|
10
|
+
cwd: string;
|
|
11
|
+
abortController: AbortController;
|
|
12
|
+
model?: string;
|
|
13
|
+
sessionsDir: string;
|
|
14
|
+
compactionMessage?: string;
|
|
15
|
+
onIdentityReload?: () => Promise<void>;
|
|
16
|
+
}) {
|
|
17
|
+
const autoCommit = createAutoCommitHook(options.cwd);
|
|
18
|
+
const identityReload = createIdentityReloadHook(options.cwd);
|
|
19
|
+
|
|
20
|
+
const sessionManager = createSessionManager({
|
|
21
|
+
systemPrompt: options.systemPrompt,
|
|
22
|
+
cwd: options.cwd,
|
|
23
|
+
abortController: options.abortController,
|
|
24
|
+
model: options.model,
|
|
25
|
+
sessionsDir: options.sessionsDir,
|
|
26
|
+
compactionMessage: options.compactionMessage,
|
|
27
|
+
postToolUseHooks: [{ matcher: "Edit|Write", hooks: [autoCommit.hook, identityReload.hook] }],
|
|
28
|
+
onTurnDone: () => {
|
|
29
|
+
if (identityReload.needsReload()) {
|
|
30
|
+
options.onIdentityReload?.();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function sendMessage(content: string | VoluteContentPart[], meta?: ChannelMeta) {
|
|
36
|
+
const sessionName = meta?.sessionName ?? "main";
|
|
37
|
+
const session = sessionManager.getOrCreateSession(sessionName);
|
|
38
|
+
|
|
39
|
+
const text =
|
|
40
|
+
typeof content === "string"
|
|
41
|
+
? content
|
|
42
|
+
: content.map((p) => (p.type === "text" ? p.text : `[${p.type}]`)).join(" ");
|
|
43
|
+
logMessage("in", text, meta?.channel);
|
|
44
|
+
|
|
45
|
+
const time = new Date().toLocaleString();
|
|
46
|
+
const prefix = formatPrefix(meta, time);
|
|
47
|
+
|
|
48
|
+
let sdkContent: (
|
|
49
|
+
| { type: "text"; text: string }
|
|
50
|
+
| {
|
|
51
|
+
type: "image";
|
|
52
|
+
source: {
|
|
53
|
+
type: "base64";
|
|
54
|
+
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
55
|
+
data: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
)[];
|
|
59
|
+
|
|
60
|
+
if (typeof content === "string") {
|
|
61
|
+
sdkContent = [{ type: "text" as const, text: prefix + content }];
|
|
62
|
+
} else {
|
|
63
|
+
const hasText = content.some((p) => p.type === "text");
|
|
64
|
+
sdkContent = content.map((part, i) => {
|
|
65
|
+
if (part.type === "text") {
|
|
66
|
+
return { type: "text" as const, text: (i === 0 ? prefix : "") + part.text };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
type: "image" as const,
|
|
70
|
+
source: {
|
|
71
|
+
type: "base64" as const,
|
|
72
|
+
media_type: part.media_type as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
73
|
+
data: part.data,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
if (prefix && !hasText) {
|
|
78
|
+
sdkContent.unshift({ type: "text" as const, text: prefix.trimEnd() });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
session.channel.push({
|
|
83
|
+
type: "user",
|
|
84
|
+
session_id: "",
|
|
85
|
+
message: {
|
|
86
|
+
role: "user",
|
|
87
|
+
content: sdkContent,
|
|
88
|
+
},
|
|
89
|
+
parent_tool_use_id: null,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function onMessage(listener: Listener, sessionName?: string): () => void {
|
|
94
|
+
const name = sessionName ?? "main";
|
|
95
|
+
const session = sessionManager.getOrCreateSession(name);
|
|
96
|
+
session.listeners.add(listener);
|
|
97
|
+
return () => session.listeners.delete(listener);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { sendMessage, onMessage, waitForCommits: autoCommit.waitForCommits };
|
|
101
|
+
}
|