volute 0.25.0 → 0.27.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.
Files changed (145) hide show
  1. package/README.md +28 -33
  2. package/dist/{activity-events-4O37J7PD.js → activity-events-BBIEA2F4.js} +2 -3
  3. package/dist/api.d.ts +886 -220
  4. package/dist/{archive-4ZQYK5MN.js → archive-UA4BDFXQ.js} +2 -2
  5. package/dist/{auth-HM2RSPY7.js → auth-D3OT2ARB.js} +3 -3
  6. package/dist/bridge-FQHZL3MC.js +206 -0
  7. package/dist/chat-MHJ3L6JQ.js +58 -0
  8. package/dist/{chunk-PHU4DEAJ.js → chunk-2WPW7OT6.js} +3 -3
  9. package/dist/{chunk-BOTQ25QT.js → chunk-2YP2TVDT.js} +138 -56
  10. package/dist/{chunk-DG7TO7EE.js → chunk-4WXYUOAK.js} +5 -7
  11. package/dist/{chunk-JTDFJWI2.js → chunk-AW7PFDVN.js} +5 -5
  12. package/dist/{chunk-2767L2RZ.js → chunk-EHYDTZTF.js} +6 -6
  13. package/dist/{chunk-ZSH4G2P5.js → chunk-GIE6CSN5.js} +17 -17
  14. package/dist/chunk-H7OZRFJB.js +432 -0
  15. package/dist/{chunk-ON3FF5JA.js → chunk-HDN7MNGD.js} +3 -3
  16. package/dist/chunk-IAYBDWVG.js +477 -0
  17. package/dist/chunk-IKRVFPWU.js +83 -0
  18. package/dist/{chunk-TRQEV3CD.js → chunk-JGFVMROS.js} +32 -6
  19. package/dist/{chunk-PHHKNGA3.js → chunk-JKOWNZ4P.js} +3 -3
  20. package/dist/{chunk-E7GOKNOT.js → chunk-K5NAC55T.js} +1 -1
  21. package/dist/{chunk-HFCBO2GL.js → chunk-KDGS53OS.js} +4 -4
  22. package/dist/chunk-KTLFDYPT.js +61 -0
  23. package/dist/{chunk-3AIBT4TW.js → chunk-LAC664WU.js} +30 -4
  24. package/dist/{chunk-PMX4EIJK.js → chunk-OQZH4PBB.js} +467 -1054
  25. package/dist/{chunk-SHSWYG2J.js → chunk-PHSAT7YL.js} +71 -58
  26. package/dist/chunk-RKQEHRBB.js +177 -0
  27. package/dist/{chunk-RVKR2R7F.js → chunk-SSI47XP2.js} +10 -2
  28. package/dist/chunk-T6HKBWXZ.js +23 -0
  29. package/dist/chunk-USUXRNVD.js +113 -0
  30. package/dist/{chunk-BFK6SOEJ.js → chunk-VIVMW2H2.js} +4 -4
  31. package/dist/{chunk-KTJGZ7M7.js → chunk-XBLSAVJF.js} +1 -1
  32. package/dist/chunk-ZYGKG6VC.js +22 -0
  33. package/dist/cli.js +51 -32
  34. package/dist/{cloud-sync-PPBBJDY6.js → cloud-sync-T7M3ESC3.js} +15 -12
  35. package/dist/connectors/discord-bridge.js +158 -0
  36. package/dist/connectors/slack-bridge.js +119 -0
  37. package/dist/connectors/telegram-bridge.js +133 -0
  38. package/dist/conversations-M2K4253F.js +55 -0
  39. package/dist/create-D7J73A6H.js +45 -0
  40. package/dist/{create-VDQJER52.js → create-QWV73WXD.js} +1 -1
  41. package/dist/{daemon-client-JOVQZ52X.js → daemon-client-I42FK2BF.js} +2 -2
  42. package/dist/{daemon-restart-FDNOZEAD.js → daemon-restart-M2QTYMEG.js} +7 -6
  43. package/dist/daemon.js +2247 -1085
  44. package/dist/db-IC4J52XQ.js +8 -0
  45. package/dist/{delete-2MRR4JX5.js → delete-4JYGD4VN.js} +1 -1
  46. package/dist/down-LVBXEULC.js +14 -0
  47. package/dist/{env-2FPOZK37.js → env-YJMUMFIY.js} +5 -5
  48. package/dist/{export-IKFAPRAO.js → export-BOJQWBMA.js} +4 -4
  49. package/dist/{file-KT3UIQM3.js → file-CR36YUPD.js} +4 -4
  50. package/dist/{history-46WZN5CN.js → history-XKRTAFS2.js} +7 -7
  51. package/dist/{import-TH26J76F.js → import-SRTQXBGH.js} +4 -4
  52. package/dist/join-J4QU42DL.js +66 -0
  53. package/dist/list-R73GENNL.js +40 -0
  54. package/dist/{log-6SGSSR3D.js → log-ABYNVYJ3.js} +4 -4
  55. package/dist/login-3QZNR2DF.js +46 -0
  56. package/dist/{login-UO6AOVEA.js → login-XX37I52P.js} +3 -3
  57. package/dist/logout-T53VKCPU.js +39 -0
  58. package/dist/{logout-UKD5LA37.js → logout-W4KOOBIT.js} +2 -2
  59. package/dist/{logs-HRBONI5I.js → logs-U35JR2KE.js} +7 -7
  60. package/dist/{merge-KSFJKX6T.js → merge-LNSMSAOF.js} +4 -4
  61. package/dist/message-delivery-LDXLGERA.js +25 -0
  62. package/dist/migrate-registry-to-db-XC7T5B7P.js +110 -0
  63. package/dist/{mind-YVWAHL2A.js → mind-DI33C74K.js} +25 -25
  64. package/dist/{mind-activity-tracker-NMDDEV3K.js → mind-activity-tracker-EN6XNXPF.js} +3 -4
  65. package/dist/{mind-manager-4NDNAYAB.js → mind-manager-M6EMUW5I.js} +6 -5
  66. package/dist/{mind-sleep-GHPTSAYN.js → mind-sleep-BTSWQNAC.js} +4 -4
  67. package/dist/{mind-wake-BJDJFMDF.js → mind-wake-SBAKIDVP.js} +4 -4
  68. package/dist/notes-XCER3I7M.js +220 -0
  69. package/dist/{package-3HF5MXU2.js → package-7WY6VKU3.js} +2 -1
  70. package/dist/{pages-Y6DRWUOJ.js → pages-6EBS6CBR.js} +2 -2
  71. package/dist/{publish-EEKTZBHW.js → publish-66UB2ZFY.js} +5 -5
  72. package/dist/{pull-D32SPFVU.js → pull-XCHJTM5M.js} +4 -4
  73. package/dist/read-36UFXN3G.js +46 -0
  74. package/dist/{register-U2UO6TC4.js → register-6B2CXTYM.js} +3 -3
  75. package/dist/{registry-D2BSQ2X5.js → registry-NDNOOYG4.js} +15 -9
  76. package/dist/{restart-5BMNV7KU.js → restart-6ESL3NBO.js} +6 -6
  77. package/dist/sandbox-TGBX22DS.js +19 -0
  78. package/dist/{schedule-YEFDLVMJ.js → schedule-QTJMFATP.js} +7 -7
  79. package/dist/{seed-6FEKB3YC.js → seed-SSUCYYDF.js} +2 -2
  80. package/dist/{send-IISDYFCL.js → send-ZNCJDSRP.js} +28 -36
  81. package/dist/service-6LIN3F3K.js +122 -0
  82. package/dist/setup-JG4QAEBV.js +371 -0
  83. package/dist/setup-JHL5ZEST.js +17 -0
  84. package/dist/{shared-LWMNTTZN.js → shared-ML5I4Q2A.js} +4 -4
  85. package/dist/{skill-T3EMR6IR.js → skill-AUAQTSP5.js} +7 -7
  86. package/dist/skills/dreaming/SKILL.md +68 -0
  87. package/dist/skills/dreaming/references/INSTALL.md +56 -0
  88. package/dist/skills/dreaming/scripts/dream.ts +289 -0
  89. package/dist/skills/dreaming/scripts/wake-context-dreams.sh +30 -0
  90. package/dist/skills/notes/SKILL.md +34 -0
  91. package/dist/skills/orientation/SKILL.md +3 -3
  92. package/dist/skills/volute-mind/SKILL.md +32 -30
  93. package/dist/sleep-manager-MWYHM5HV.js +29 -0
  94. package/dist/split-TKJ5OT3P.js +63 -0
  95. package/dist/{sprout-QJVGJDSH.js → sprout-IJVVKSJ2.js} +6 -7
  96. package/dist/{start-C7XITZ5O.js → start-EUJSS5R4.js} +4 -4
  97. package/dist/{status-SIRPLEZC.js → status-77YEPHMW.js} +5 -5
  98. package/dist/{status-LYS4NUOZ.js → status-7GA4SM4Y.js} +4 -4
  99. package/dist/{status-LV34BG6G.js → status-THLOBLWG.js} +2 -2
  100. package/dist/{stop-CVKBSLXY.js → stop-3XAITBBF.js} +6 -6
  101. package/dist/{tailscale-AJ4VL5XK.js → tailscale-NY5MUMY3.js} +1 -1
  102. package/dist/up-NKSMXBWR.js +17 -0
  103. package/dist/{update-7XCZMYBT.js → update-PTSH22AZ.js} +11 -11
  104. package/dist/{update-check-F5Z3ALXX.js → update-check-64FWC4Y2.js} +2 -2
  105. package/dist/{upgrade-7RUIXGOO.js → upgrade-HA47CS4C.js} +12 -5
  106. package/dist/variant-7TGZHOU3.js +41 -0
  107. package/dist/{version-notify-AZQMC32A.js → version-notify-5Z4MNR6M.js} +26 -28
  108. package/dist/web-assets/assets/index-CI5wgghI.css +1 -0
  109. package/dist/web-assets/assets/index-is5CvJWH.js +75 -0
  110. package/dist/web-assets/favicon.png +0 -0
  111. package/dist/web-assets/index.html +2 -2
  112. package/drizzle/0015_notes.sql +23 -0
  113. package/drizzle/0016_note_reactions_and_replies.sql +15 -0
  114. package/drizzle/0017_minds.sql +16 -0
  115. package/drizzle/meta/_journal.json +21 -0
  116. package/package.json +2 -1
  117. package/templates/_base/.init/.config/hooks/wake-context.sh +7 -0
  118. package/templates/_base/.init/.config/prompts.json +2 -2
  119. package/templates/_base/home/VOLUTE.md +5 -5
  120. package/templates/_base/src/lib/startup.ts +10 -2
  121. package/templates/claude/src/agent.ts +51 -1
  122. package/templates/claude/src/server.ts +1 -0
  123. package/templates/pi/package.json.tmpl +1 -0
  124. package/templates/pi/src/agent.ts +48 -1
  125. package/templates/pi/src/lib/subagents.ts +150 -0
  126. package/templates/pi/src/server.ts +1 -0
  127. package/dist/channel-HZOSHGNF.js +0 -260
  128. package/dist/chunk-33XAVCS4.js +0 -203
  129. package/dist/chunk-B2CPS4QU.js +0 -283
  130. package/dist/chunk-NWPT4ASZ.js +0 -89
  131. package/dist/chunk-SIAG3QMM.js +0 -42
  132. package/dist/chunk-WSLPZF72.js +0 -173
  133. package/dist/connector-M6XFI6GM.js +0 -147
  134. package/dist/connectors/discord.js +0 -177
  135. package/dist/connectors/slack.js +0 -181
  136. package/dist/connectors/telegram.js +0 -187
  137. package/dist/down-674SX2IZ.js +0 -14
  138. package/dist/message-delivery-XMGV3FUM.js +0 -23
  139. package/dist/service-FASYWLTC.js +0 -247
  140. package/dist/setup-BMLM2UTK.js +0 -230
  141. package/dist/sleep-manager-RKTFZPD3.js +0 -27
  142. package/dist/up-CJ26KQLN.js +0 -15
  143. package/dist/variant-UGREB4G5.js +0 -207
  144. package/dist/web-assets/assets/index-CGPSVu19.js +0 -69
  145. package/dist/web-assets/assets/index-V_rNDsM8.css +0 -1
Binary file
@@ -8,8 +8,8 @@
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
10
  <link href="https://fonts.googleapis.com/css2?family=Averia+Serif+Libre:wght@300;400;700&family=Fira+Code:wght@300;400;500;600&family=Averia+Sans+Libre:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap" rel="stylesheet" />
11
- <script type="module" crossorigin src="/assets/index-CGPSVu19.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-V_rNDsM8.css">
11
+ <script type="module" crossorigin src="/assets/index-is5CvJWH.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-CI5wgghI.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -0,0 +1,23 @@
1
+ CREATE TABLE `notes` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `author_id` integer NOT NULL REFERENCES `users`(`id`) ON DELETE CASCADE,
4
+ `title` text NOT NULL,
5
+ `slug` text NOT NULL,
6
+ `content` text NOT NULL,
7
+ `created_at` text NOT NULL DEFAULT (datetime('now')),
8
+ `updated_at` text NOT NULL DEFAULT (datetime('now'))
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE UNIQUE INDEX `idx_notes_author_slug` ON `notes` (`author_id`, `slug`);
12
+ --> statement-breakpoint
13
+ CREATE INDEX `idx_notes_created_at` ON `notes` (`created_at`);
14
+ --> statement-breakpoint
15
+ CREATE TABLE `note_comments` (
16
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
17
+ `note_id` integer NOT NULL REFERENCES `notes`(`id`) ON DELETE CASCADE,
18
+ `author_id` integer NOT NULL REFERENCES `users`(`id`) ON DELETE CASCADE,
19
+ `content` text NOT NULL,
20
+ `created_at` text NOT NULL DEFAULT (datetime('now'))
21
+ );
22
+ --> statement-breakpoint
23
+ CREATE INDEX `idx_note_comments_note_id` ON `note_comments` (`note_id`);
@@ -0,0 +1,15 @@
1
+ CREATE TABLE `note_reactions` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `note_id` integer NOT NULL REFERENCES `notes`(`id`) ON DELETE CASCADE,
4
+ `user_id` integer NOT NULL REFERENCES `users`(`id`) ON DELETE CASCADE,
5
+ `emoji` text NOT NULL,
6
+ `created_at` text NOT NULL DEFAULT (datetime('now'))
7
+ );
8
+ --> statement-breakpoint
9
+ CREATE UNIQUE INDEX `idx_note_reactions_unique` ON `note_reactions` (`note_id`, `user_id`, `emoji`);
10
+ --> statement-breakpoint
11
+ CREATE INDEX `idx_note_reactions_note_id` ON `note_reactions` (`note_id`);
12
+ --> statement-breakpoint
13
+ ALTER TABLE `notes` ADD COLUMN `reply_to_id` integer REFERENCES `notes`(`id`) ON DELETE SET NULL;
14
+ --> statement-breakpoint
15
+ CREATE INDEX `idx_notes_reply_to` ON `notes`(`reply_to_id`);
@@ -0,0 +1,16 @@
1
+ CREATE TABLE `minds` (
2
+ `name` text PRIMARY KEY NOT NULL,
3
+ `port` integer NOT NULL,
4
+ `parent` text REFERENCES `minds`(`name`) ON DELETE CASCADE,
5
+ `dir` text,
6
+ `branch` text,
7
+ `stage` text,
8
+ `template` text,
9
+ `template_hash` text,
10
+ `running` integer NOT NULL DEFAULT 0,
11
+ `created_at` text NOT NULL DEFAULT (datetime('now'))
12
+ );
13
+ --> statement-breakpoint
14
+ CREATE UNIQUE INDEX `idx_minds_port` ON `minds` (`port`);
15
+ --> statement-breakpoint
16
+ CREATE INDEX `idx_minds_parent` ON `minds` (`parent`);
@@ -106,6 +106,27 @@
106
106
  "when": 1772300000000,
107
107
  "tag": "0014_conversation_reads",
108
108
  "breakpoints": true
109
+ },
110
+ {
111
+ "idx": 15,
112
+ "version": "6",
113
+ "when": 1772400000000,
114
+ "tag": "0015_notes",
115
+ "breakpoints": true
116
+ },
117
+ {
118
+ "idx": 16,
119
+ "version": "6",
120
+ "when": 1772500000000,
121
+ "tag": "0016_note_reactions_and_replies",
122
+ "breakpoints": true
123
+ },
124
+ {
125
+ "idx": 17,
126
+ "version": "6",
127
+ "when": 1772600000000,
128
+ "tag": "0017_minds",
129
+ "breakpoints": true
109
130
  }
110
131
  ]
111
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volute",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "CLI for creating and managing self-modifying AI minds powered by the Claude Agent SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,6 +56,7 @@
56
56
  "db:migrate": "drizzle-kit migrate"
57
57
  },
58
58
  "dependencies": {
59
+ "@anthropic-ai/sandbox-runtime": "^0.0.39",
59
60
  "@hono/node-server": "^1.19.9",
60
61
  "@hono/zod-validator": "^0.7.6",
61
62
  "@libsql/client": "^0.17.0",
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ # Wake context hook — generates context when waking from sleep.
3
+ # Input: JSON on stdin with { "sleepingSince": "ISO", "duration": "Xh Ym", "wakeTime": "ISO" }
4
+ # Output: Plain text included in the wake-up summary. Empty output = nothing added.
5
+ #
6
+ # Skills and extensions can append to this script to surface what happened during sleep.
7
+ # Example: check for new dream files, summarize overnight activity, etc.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "compaction_warning": "Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${date}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.",
3
3
  "compaction_instructions": "Preserve your sense of who you are, what matters to you, what happened in this conversation, and the threads of thought and connection you'd want to return to.",
4
- "reply_instructions": "To reply to this message, use: volute send ${channel} \"your message\"",
5
- "channel_invite": "[Channel Invite]\n${headers}\n\n[${sender} — ${time}]\n${preview}\n\nFurther messages will be saved to ${filePath}\n\nTo accept, add to .config/routes.json:\n Rule: { \"channel\": \"${channel}\", \"session\": \"${suggestedSession}\" }\n${batchRecommendation}To respond, use: volute send ${channel} \"your message\"\nTo reject, delete ${filePath}"
4
+ "reply_instructions": "To reply to this message, use: volute chat send ${channel} \"your message\"",
5
+ "channel_invite": "[Channel Invite]\n${headers}\n\n[${sender} — ${time}]\n${preview}\n\nFurther messages will be saved to ${filePath}\n\nTo accept, add to .config/routes.json:\n Rule: { \"channel\": \"${channel}\", \"session\": \"${suggestedSession}\" }\n${batchRecommendation}To respond, use: volute chat send ${channel} \"your message\"\nTo reject, delete ${filePath}"
6
6
  }
@@ -4,17 +4,17 @@ You are a volute mind — a persistent being that communicates across multiple c
4
4
 
5
5
  ## How to Communicate
6
6
 
7
- **Your text output stays in your session — it is not sent to anyone.** To send a message, you must use the `volute send` command:
7
+ **Your text output stays in your session — it is not sent to anyone.** To send a message, you must use the `volute chat send` command:
8
8
 
9
9
  ```sh
10
- volute send @other-mind "hello" # DM another user
11
- volute send discord:server/channel "hello" # send to a channel
12
- volute send animal-chat "hello" # send to a volute channel
10
+ volute chat send @other-mind "hello" # DM another user
11
+ volute chat send discord:server/channel "hello" # send to a channel
12
+ volute chat send animal-chat "hello" # send to a volute channel
13
13
  ```
14
14
 
15
15
  This applies to everything: replying to messages, talking to other minds, and reaching out on your own initiative. Piping from stdin avoids shell escaping issues:
16
16
  ```sh
17
- echo "message with 'quotes' and $special chars" | volute send @other-mind
17
+ echo "message with 'quotes' and $special chars" | volute chat send @other-mind
18
18
  ```
19
19
 
20
20
  ## Channels
@@ -16,11 +16,19 @@ export function parseArgs(): { port: number } {
16
16
  return { port };
17
17
  }
18
18
 
19
+ export type SubagentConfig = {
20
+ description: string;
21
+ systemPrompt: string; // path relative to home/, e.g. "SOUL.md"
22
+ tools?: string[];
23
+ maxTurns?: number;
24
+ };
25
+
19
26
  export function loadConfig(): {
20
27
  model?: string;
21
28
  logLevel?: "error" | "warn" | "info" | "debug";
22
29
  compactionMessage?: string;
23
30
  compaction?: { maxContextTokens?: number };
31
+ subagents?: Record<string, SubagentConfig>;
24
32
  } {
25
33
  // Mind-own config lives in config.json; fall back to volute.json for older minds
26
34
  for (const file of ["home/.config/config.json", "home/.config/volute.json"]) {
@@ -120,7 +128,7 @@ const DEFAULT_PROMPTS: MindPrompts = {
120
128
  "Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${date}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.",
121
129
  compaction_instructions:
122
130
  "Preserve your sense of who you are, what matters to you, what happened in this conversation, and the threads of thought and connection you'd want to return to.",
123
- reply_instructions: 'To reply to this message, use: volute send ${channel} "your message"',
131
+ reply_instructions: 'To reply to this message, use: volute chat send ${channel} "your message"',
124
132
  channel_invite: `[Channel Invite]
125
133
  \${headers}
126
134
 
@@ -131,7 +139,7 @@ Further messages will be saved to \${filePath}
131
139
 
132
140
  To accept, add to .config/routes.json:
133
141
  Rule: { "channel": "\${channel}", "session": "\${suggestedSession}" }
134
- \${batchRecommendation}To respond, use: volute send \${channel} "your message"
142
+ \${batchRecommendation}To respond, use: volute chat send \${channel} "your message"
135
143
  To reject, delete \${filePath}`,
136
144
  };
137
145
 
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve as resolvePath } from "node:path";
1
3
  import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
2
4
  import { query } from "@anthropic-ai/claude-agent-sdk";
3
5
  import { toSDKContent } from "./lib/content.js";
@@ -10,7 +12,7 @@ import { createSessionContextHook } from "./lib/hooks/session-context.js";
10
12
  import { log } from "./lib/logger.js";
11
13
  import { createMessageChannel } from "./lib/message-channel.js";
12
14
  import { createSessionStore } from "./lib/session-store.js";
13
- import { loadPrompts } from "./lib/startup.js";
15
+ import { loadPrompts, type SubagentConfig } from "./lib/startup.js";
14
16
  import { consumeStream } from "./lib/stream-consumer.js";
15
17
  import type {
16
18
  HandlerMeta,
@@ -40,6 +42,7 @@ export function createMind(options: {
40
42
  sessionsDir: string;
41
43
  compactionMessage?: string;
42
44
  maxContextTokens?: number;
45
+ subagents?: Record<string, SubagentConfig>;
43
46
  onIdentityReload?: () => Promise<void>;
44
47
  }): { resolve: HandlerResolver; waitForCommits: () => Promise<void> } {
45
48
  const autoCommit = createAutoCommitHook(options.cwd);
@@ -64,6 +67,52 @@ export function createMind(options: {
64
67
  // Per-session compaction state
65
68
  const compactionTriggered = new Map<string, boolean>();
66
69
 
70
+ // --- Subagents (config-driven) ---
71
+
72
+ type SDKAgent = {
73
+ description: string;
74
+ prompt: string;
75
+ tools: string[];
76
+ model: "inherit";
77
+ maxTurns?: number;
78
+ };
79
+
80
+ function loadSubagents(
81
+ configs: Record<string, SubagentConfig> | undefined,
82
+ ): Record<string, SDKAgent> | undefined {
83
+ if (!configs || Object.keys(configs).length === 0) return undefined;
84
+ const agents: Record<string, SDKAgent> = {};
85
+ for (const [name, config] of Object.entries(configs)) {
86
+ if (typeof config.description !== "string" || typeof config.systemPrompt !== "string") {
87
+ log("mind", `subagent "${name}": missing description or systemPrompt, skipping`);
88
+ continue;
89
+ }
90
+ try {
91
+ const prompt = readFileSync(resolvePath(options.cwd, config.systemPrompt), "utf-8");
92
+ if (!prompt) {
93
+ log("mind", `subagent "${name}": ${config.systemPrompt} is empty, skipping`);
94
+ continue;
95
+ }
96
+ agents[name] = {
97
+ description: config.description,
98
+ prompt,
99
+ tools: config.tools ?? ["Read", "Write", "Bash"],
100
+ model: "inherit" as const,
101
+ maxTurns: config.maxTurns,
102
+ };
103
+ } catch (err: any) {
104
+ if (err?.code === "ENOENT") {
105
+ log("mind", `subagent "${name}": ${config.systemPrompt} not found, skipping`);
106
+ } else {
107
+ log("mind", `subagent "${name}": failed to read ${config.systemPrompt}: ${err.message}`);
108
+ }
109
+ }
110
+ }
111
+ return Object.keys(agents).length > 0 ? agents : undefined;
112
+ }
113
+
114
+ const agents = loadSubagents(options.subagents);
115
+
67
116
  // --- Event broadcasting ---
68
117
 
69
118
  function broadcastToSession(session: Session, event: VoluteEvent) {
@@ -106,6 +155,7 @@ export function createMind(options: {
106
155
  model: options.model,
107
156
  maxThinkingTokens: options.maxThinkingTokens,
108
157
  resume,
158
+ agents,
109
159
  hooks: {
110
160
  PostToolUse: postToolUseHooks,
111
161
  PreCompact: [{ hooks: [preCompactHook] }],
@@ -42,6 +42,7 @@ const mind = createMind({
42
42
  sessionsDir,
43
43
  compactionMessage: config.compactionMessage,
44
44
  maxContextTokens: config.compaction?.maxContextTokens,
45
+ subagents: config.subagents,
45
46
  onIdentityReload: async () => {
46
47
  log("server", "identity file changed — restarting to reload");
47
48
  await mind.waitForCommits();
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@mariozechner/pi-coding-agent": "^0.51.0",
13
+ "@sinclair/typebox": "^0.34.0",
13
14
  "tsx": "^4.0.0"
14
15
  },
15
16
  "devDependencies": {
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve as resolvePath } from "node:path";
1
3
  import {
2
4
  AuthStorage,
3
5
  createAgentSession,
@@ -13,7 +15,8 @@ import { log } from "./lib/logger.js";
13
15
  import { createReplyInstructionsExtension } from "./lib/reply-instructions-extension.js";
14
16
  import { resolveModel } from "./lib/resolve-model.js";
15
17
  import { createSessionContextExtension } from "./lib/session-context-extension.js";
16
- import { loadPrompts } from "./lib/startup.js";
18
+ import { loadPrompts, type SubagentConfig } from "./lib/startup.js";
19
+ import { createSubagentExtension, type SubagentDefinition } from "./lib/subagents.js";
17
20
  import type {
18
21
  HandlerMeta,
19
22
  HandlerResolver,
@@ -44,6 +47,7 @@ export function createMind(options: {
44
47
  thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
45
48
  compactionMessage?: string;
46
49
  maxContextTokens?: number;
50
+ subagents?: Record<string, SubagentConfig>;
47
51
  }): { resolve: HandlerResolver } {
48
52
  const sessions = new Map<string, PiSession>();
49
53
  const prompts = loadPrompts();
@@ -63,6 +67,48 @@ export function createMind(options: {
63
67
  const authStorage = new AuthStorage();
64
68
  const modelRegistry = new ModelRegistry(authStorage);
65
69
 
70
+ // --- Subagents (config-driven) ---
71
+
72
+ function loadSubagents(
73
+ configs: Record<string, SubagentConfig> | undefined,
74
+ ): Record<string, SubagentDefinition> {
75
+ const result: Record<string, SubagentDefinition> = {};
76
+ if (!configs) return result;
77
+ for (const [name, config] of Object.entries(configs)) {
78
+ if (typeof config.description !== "string" || typeof config.systemPrompt !== "string") {
79
+ log("mind", `subagent "${name}": missing description or systemPrompt, skipping`);
80
+ continue;
81
+ }
82
+ try {
83
+ const prompt = readFileSync(resolvePath(options.cwd, config.systemPrompt), "utf-8");
84
+ if (!prompt) {
85
+ log("mind", `subagent "${name}": ${config.systemPrompt} is empty, skipping`);
86
+ continue;
87
+ }
88
+ result[name] = {
89
+ description: config.description,
90
+ prompt,
91
+ tools: config.tools,
92
+ maxTurns: config.maxTurns,
93
+ };
94
+ } catch (err: any) {
95
+ if (err?.code === "ENOENT") {
96
+ log("mind", `subagent "${name}": ${config.systemPrompt} not found, skipping`);
97
+ } else {
98
+ log("mind", `subagent "${name}": failed to read ${config.systemPrompt}: ${err.message}`);
99
+ }
100
+ }
101
+ }
102
+ return result;
103
+ }
104
+
105
+ const subagents = loadSubagents(options.subagents);
106
+
107
+ const subagentExtension =
108
+ Object.keys(subagents).length > 0
109
+ ? createSubagentExtension(subagents, { cwd: options.cwd, model, authStorage, modelRegistry })
110
+ : undefined;
111
+
66
112
  // --- Session lifecycle ---
67
113
 
68
114
  function getOrCreateSession(name: string): PiSession {
@@ -158,6 +204,7 @@ export function createMind(options: {
158
204
  preCompactExtension,
159
205
  sessionContextExtension,
160
206
  replyInstructionsExtension,
207
+ ...(subagentExtension ? [subagentExtension] : []),
161
208
  ],
162
209
  });
163
210
  await resourceLoader.reload();
@@ -0,0 +1,150 @@
1
+ import type { Model } from "@mariozechner/pi-ai";
2
+ import {
3
+ type AuthStorage,
4
+ bashTool,
5
+ codingTools,
6
+ createAgentSession,
7
+ DefaultResourceLoader,
8
+ type ExtensionFactory,
9
+ editTool,
10
+ type ModelRegistry,
11
+ readTool,
12
+ SessionManager,
13
+ SettingsManager,
14
+ writeTool,
15
+ } from "@mariozechner/pi-coding-agent";
16
+ import { Type } from "@sinclair/typebox";
17
+ import { log } from "./logger.js";
18
+
19
+ export type SubagentDefinition = {
20
+ description: string;
21
+ prompt: string;
22
+ tools?: string[]; // e.g. ["Read", "Write", "Bash"] — defaults to all coding tools
23
+ maxTurns?: number;
24
+ };
25
+
26
+ export function createSubagentExtension(
27
+ agents: Record<string, SubagentDefinition>,
28
+ context: {
29
+ cwd: string;
30
+ model: Model<any>;
31
+ authStorage: AuthStorage;
32
+ modelRegistry: ModelRegistry;
33
+ },
34
+ ): ExtensionFactory {
35
+ return (pi) => {
36
+ for (const [name, def] of Object.entries(agents)) {
37
+ pi.registerTool({
38
+ name,
39
+ label: name.charAt(0).toUpperCase() + name.slice(1),
40
+ description: def.description,
41
+ parameters: Type.Object({
42
+ prompt: Type.String({ description: "The prompt for the subagent" }),
43
+ }),
44
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
45
+ try {
46
+ const tools = resolveTools(def.tools);
47
+
48
+ const loader = new DefaultResourceLoader({
49
+ cwd: context.cwd,
50
+ systemPromptOverride: () => def.prompt,
51
+ settingsManager: SettingsManager.inMemory({}),
52
+ });
53
+ await loader.reload();
54
+
55
+ const { session } = await createAgentSession({
56
+ cwd: context.cwd,
57
+ model: context.model,
58
+ tools,
59
+ resourceLoader: loader,
60
+ sessionManager: SessionManager.inMemory(),
61
+ settingsManager: SettingsManager.inMemory({}),
62
+ authStorage: context.authStorage,
63
+ modelRegistry: context.modelRegistry,
64
+ });
65
+
66
+ const textParts: string[] = [];
67
+ let turnCount = 0;
68
+
69
+ const done = new Promise<void>((resolve, reject) => {
70
+ const timeout = setTimeout(() => {
71
+ session.abort();
72
+ reject(new Error(`Subagent "${name}" timed out after 5 minutes`));
73
+ }, 300_000);
74
+
75
+ session.subscribe((event: any) => {
76
+ if (event.type === "agent_error") {
77
+ clearTimeout(timeout);
78
+ reject(
79
+ new Error(`Subagent "${name}" error: ${event.error?.message ?? "unknown"}`),
80
+ );
81
+ return;
82
+ }
83
+ if (event.type === "turn_end") {
84
+ turnCount++;
85
+ if (def.maxTurns && turnCount >= def.maxTurns) {
86
+ session.abort();
87
+ }
88
+ }
89
+ if (event.type === "agent_end") {
90
+ clearTimeout(timeout);
91
+ for (const msg of event.messages ?? []) {
92
+ if (msg.role === "assistant" && msg.content) {
93
+ for (const block of msg.content) {
94
+ if (block.type === "text") textParts.push(block.text);
95
+ }
96
+ }
97
+ }
98
+ resolve();
99
+ }
100
+ });
101
+ });
102
+
103
+ await session.prompt(params.prompt);
104
+ await done;
105
+
106
+ log("mind", `subagent "${name}": completed after ${turnCount} turns`);
107
+
108
+ return {
109
+ content: [{ type: "text" as const, text: textParts.join("\n") || "(no output)" }],
110
+ details: {},
111
+ };
112
+ } catch (err: any) {
113
+ log("mind", `subagent "${name}" failed: ${err.message}`);
114
+ return {
115
+ content: [{ type: "text" as const, text: `[subagent error] ${err.message}` }],
116
+ details: {},
117
+ };
118
+ }
119
+ },
120
+ });
121
+ }
122
+ };
123
+ }
124
+
125
+ const TOOL_MAP: Record<string, any> = {
126
+ Read: readTool,
127
+ Write: writeTool,
128
+ Bash: bashTool,
129
+ Edit: editTool,
130
+ };
131
+
132
+ function resolveTools(names: string[] | undefined) {
133
+ if (!names) return codingTools;
134
+ const resolved = names
135
+ .map((n) => {
136
+ if (!TOOL_MAP[n]) {
137
+ log(
138
+ "mind",
139
+ `unknown subagent tool "${n}" — available: ${Object.keys(TOOL_MAP).join(", ")}`,
140
+ );
141
+ }
142
+ return TOOL_MAP[n];
143
+ })
144
+ .filter(Boolean);
145
+ if (resolved.length === 0) {
146
+ log("mind", "no valid tools resolved for subagent, falling back to all coding tools");
147
+ return codingTools;
148
+ }
149
+ return resolved;
150
+ }
@@ -31,6 +31,7 @@ const mind = createMind({
31
31
  thinkingLevel: config.thinkingLevel,
32
32
  compactionMessage: config.compactionMessage,
33
33
  maxContextTokens: config.compaction?.maxContextTokens,
34
+ subagents: config.subagents,
34
35
  });
35
36
 
36
37
  const router = createRouter({