my-durable 0.0.0-stage → 0.0.2

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 (259) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +328 -2
  4. package/ai_docs/durable_pi/announcement.md +860 -0
  5. package/ai_docs/durable_pi/changelog.md +51 -0
  6. package/ai_docs/durable_pi/chord-delta-findings.md +362 -0
  7. package/ai_docs/durable_pi/coding-agent-integration.md +70 -0
  8. package/ai_docs/durable_pi/pico-v5-chord-usage.md +391 -0
  9. package/ai_docs/durable_pi/pico-v5-handoff.md +921 -0
  10. package/ai_docs/durable_pi/readme.md +617 -0
  11. package/ai_docs/durable_pi/sources.md +22 -0
  12. package/ai_docs/durable_pi/spec.md +4696 -0
  13. package/ai_docs/durable_pi/vacation-planner.md +57 -0
  14. package/ai_docs/pi/chord.md +268 -0
  15. package/ai_docs/pi/cli-integration.md +110 -0
  16. package/ai_docs/pi/cli.md +325 -0
  17. package/ai_docs/pi/codemode.md +211 -0
  18. package/ai_docs/pi/compaction.md +467 -0
  19. package/ai_docs/pi/configuration.md +51 -0
  20. package/ai_docs/pi/containerization.md +187 -0
  21. package/ai_docs/pi/custom-provider.md +177 -0
  22. package/ai_docs/pi/environment-variables.md +102 -0
  23. package/ai_docs/pi/extensions.md +278 -0
  24. package/ai_docs/pi/how-pi-works.md +53 -0
  25. package/ai_docs/pi/index.md +43 -0
  26. package/ai_docs/pi/json.md +230 -0
  27. package/ai_docs/pi/keybindings.md +198 -0
  28. package/ai_docs/pi/llama-cpp.md +118 -0
  29. package/ai_docs/pi/mcp.md +274 -0
  30. package/ai_docs/pi/message-types.md +265 -0
  31. package/ai_docs/pi/models.md +209 -0
  32. package/ai_docs/pi/overview.md +166 -0
  33. package/ai_docs/pi/packages.md +133 -0
  34. package/ai_docs/pi/pi-agent-core.md +568 -0
  35. package/ai_docs/pi/pi-ai.md +1942 -0
  36. package/ai_docs/pi/prompt-templates.md +63 -0
  37. package/ai_docs/pi/providers.md +204 -0
  38. package/ai_docs/pi/quickstart.md +140 -0
  39. package/ai_docs/pi/readme.md +99 -0
  40. package/ai_docs/pi/rpc-commands.md +864 -0
  41. package/ai_docs/pi/rpc-extension-ui.md +204 -0
  42. package/ai_docs/pi/rpc.md +197 -0
  43. package/ai_docs/pi/sdk.md +151 -0
  44. package/ai_docs/pi/security.md +103 -0
  45. package/ai_docs/pi/session-format.md +297 -0
  46. package/ai_docs/pi/sessions.md +70 -0
  47. package/ai_docs/pi/settings.md +173 -0
  48. package/ai_docs/pi/shell-aliases.md +97 -0
  49. package/ai_docs/pi/skills.md +97 -0
  50. package/ai_docs/pi/slash-commands.md +64 -0
  51. package/ai_docs/pi/sources.md +57 -0
  52. package/ai_docs/pi/terminal-setup.md +223 -0
  53. package/ai_docs/pi/termux.md +121 -0
  54. package/ai_docs/pi/themes.md +140 -0
  55. package/ai_docs/pi/tmux.md +59 -0
  56. package/ai_docs/pi/toolkit.md +168 -0
  57. package/ai_docs/pi/tui.md +129 -0
  58. package/ai_docs/pi/usage.md +98 -0
  59. package/ai_docs/pi/virtual-models.md +118 -0
  60. package/ai_docs/pi/windows.md +71 -0
  61. package/apps/dpi-backend/.nvmrc +1 -0
  62. package/apps/dpi-backend/API.md +99 -0
  63. package/apps/dpi-backend/README.md +119 -0
  64. package/apps/dpi-backend/deploy/backend.env.example +9 -0
  65. package/apps/dpi-backend/deploy/dpi-backend.plist +26 -0
  66. package/apps/dpi-backend/deploy/dpi-backend.service +22 -0
  67. package/apps/dpi-backend/package.json +33 -0
  68. package/apps/dpi-backend/prompts/one_sentence.md +3 -0
  69. package/apps/dpi-backend/prompts/smart_ass_opus_fix.md +169 -0
  70. package/apps/dpi-backend/scripts/check-contract.mjs +151 -0
  71. package/apps/dpi-backend/scripts/migrate-playground.ts +66 -0
  72. package/apps/dpi-backend/scripts/record-nested-fixture.mts +154 -0
  73. package/apps/dpi-backend/scripts/token.ts +57 -0
  74. package/apps/dpi-backend/src/addons.ts +139 -0
  75. package/apps/dpi-backend/src/auth.ts +20 -0
  76. package/apps/dpi-backend/src/config.ts +54 -0
  77. package/apps/dpi-backend/src/contract-check.ts +118 -0
  78. package/apps/dpi-backend/src/errors.ts +50 -0
  79. package/apps/dpi-backend/src/extensions.ts +510 -0
  80. package/apps/dpi-backend/src/faux.ts +80 -0
  81. package/apps/dpi-backend/src/history.ts +165 -0
  82. package/apps/dpi-backend/src/idempotency.ts +20 -0
  83. package/apps/dpi-backend/src/jev.ts +43 -0
  84. package/apps/dpi-backend/src/jobs.ts +790 -0
  85. package/apps/dpi-backend/src/main.ts +76 -0
  86. package/apps/dpi-backend/src/model-catalog.ts +180 -0
  87. package/apps/dpi-backend/src/pairing-files.ts +142 -0
  88. package/apps/dpi-backend/src/pairing.ts +459 -0
  89. package/apps/dpi-backend/src/routes.ts +217 -0
  90. package/apps/dpi-backend/src/shared-state.ts +85 -0
  91. package/apps/dpi-backend/src/store.ts +1240 -0
  92. package/apps/dpi-backend/src/streams.ts +256 -0
  93. package/apps/dpi-backend/src/tunnel.ts +955 -0
  94. package/apps/dpi-backend/tsconfig.json +13 -0
  95. package/apps/dpi-playground/README.md +83 -0
  96. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CFGc7lHT.js +77 -0
  97. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DFdKpwm_.css +1 -0
  98. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/anthropic.png +0 -0
  99. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/deepseek.png +0 -0
  100. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/durable-pi.svg +14 -0
  101. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/gemini.png +0 -0
  102. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/grok.png +0 -0
  103. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/kimi.png +0 -0
  104. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/lab.png +0 -0
  105. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/meta.png +0 -0
  106. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/minimax.png +0 -0
  107. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/openai.png +0 -0
  108. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/qwen.png +0 -0
  109. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/xiaomi.png +0 -0
  110. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/zai.png +0 -0
  111. package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +16 -0
  112. package/apps/dpi-playground/apps/dpi-playground-fe/index.html +15 -0
  113. package/apps/dpi-playground/apps/dpi-playground-fe/package.json +5 -0
  114. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/anthropic.png +0 -0
  115. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/deepseek.png +0 -0
  116. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/durable-pi.svg +14 -0
  117. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/gemini.png +0 -0
  118. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/grok.png +0 -0
  119. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/kimi.png +0 -0
  120. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/lab.png +0 -0
  121. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/meta.png +0 -0
  122. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/minimax.png +0 -0
  123. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/openai.png +0 -0
  124. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/qwen.png +0 -0
  125. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/xiaomi.png +0 -0
  126. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/zai.png +0 -0
  127. package/apps/dpi-playground/apps/dpi-playground-fe/src/App.vue +180 -0
  128. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/commands.ts +119 -0
  129. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/composables.ts +129 -0
  130. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/hub.ts +40 -0
  131. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/icon-path.ts +7 -0
  132. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/ids.ts +2 -0
  133. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/jobs.ts +80 -0
  134. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/link-policy.ts +16 -0
  135. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown-policy.ts +115 -0
  136. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown.ts +60 -0
  137. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-close.ts +23 -0
  138. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-handoff.ts +56 -0
  139. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair.ts +166 -0
  140. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/qrcode.ts +323 -0
  141. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/signature.ts +146 -0
  142. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/store.ts +373 -0
  143. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/stream.ts +183 -0
  144. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/AgentView.vue +342 -0
  145. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Composer.vue +198 -0
  146. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ContextBar.vue +254 -0
  147. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceCredentialsDialog.vue +40 -0
  148. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceIcon.vue +56 -0
  149. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DirBrowser.vue +95 -0
  150. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ExtensionsPanel.vue +96 -0
  151. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/HelpDialog.vue +69 -0
  152. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Inspector.vue +425 -0
  153. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/JobCard.vue +221 -0
  154. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/MobileNav.vue +39 -0
  155. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ModelPicker.vue +71 -0
  156. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NetworkSidebar.vue +236 -0
  157. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NewSessionDialog.vue +114 -0
  158. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/OfflinePanel.vue +128 -0
  159. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PairDeviceWizard.vue +433 -0
  160. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PromptDialog.vue +89 -0
  161. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/StateEditor.vue +179 -0
  162. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TasksPanel.vue +280 -0
  163. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Toast.vue +11 -0
  164. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TokenManager.vue +175 -0
  165. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TopBar.vue +114 -0
  166. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TranscriptItem.vue +152 -0
  167. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TreeRow.vue +195 -0
  168. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TunnelStatusBar.vue +107 -0
  169. package/apps/dpi-playground/apps/dpi-playground-fe/src/extensions.css +105 -0
  170. package/apps/dpi-playground/apps/dpi-playground-fe/src/jobs.css +655 -0
  171. package/apps/dpi-playground/apps/dpi-playground-fe/src/main.ts +22 -0
  172. package/apps/dpi-playground/apps/dpi-playground-fe/src/network.css +675 -0
  173. package/apps/dpi-playground/apps/dpi-playground-fe/src/style.css +3203 -0
  174. package/apps/dpi-playground/apps/dpi-playground-fe/tsconfig.json +16 -0
  175. package/apps/dpi-playground/apps/dpi-playground-fe/vite.config.ts +14 -0
  176. package/apps/dpi-playground/apps/dpi-playground-hub/package.json +5 -0
  177. package/apps/dpi-playground/apps/dpi-playground-hub/src/audit.ts +406 -0
  178. package/apps/dpi-playground/apps/dpi-playground-hub/src/client-ip.ts +72 -0
  179. package/apps/dpi-playground/apps/dpi-playground-hub/src/credentials.ts +265 -0
  180. package/apps/dpi-playground/apps/dpi-playground-hub/src/guard.ts +43 -0
  181. package/apps/dpi-playground/apps/dpi-playground-hub/src/http-errors.ts +156 -0
  182. package/apps/dpi-playground/apps/dpi-playground-hub/src/identity.ts +60 -0
  183. package/apps/dpi-playground/apps/dpi-playground-hub/src/limiter.ts +165 -0
  184. package/apps/dpi-playground/apps/dpi-playground-hub/src/main.ts +460 -0
  185. package/apps/dpi-playground/apps/dpi-playground-hub/src/monitor.ts +320 -0
  186. package/apps/dpi-playground/apps/dpi-playground-hub/src/mux.ts +277 -0
  187. package/apps/dpi-playground/apps/dpi-playground-hub/src/pairing.ts +816 -0
  188. package/apps/dpi-playground/apps/dpi-playground-hub/src/proxy.ts +128 -0
  189. package/apps/dpi-playground/apps/dpi-playground-hub/src/rotation.ts +583 -0
  190. package/apps/dpi-playground/apps/dpi-playground-hub/src/seed.ts +11 -0
  191. package/apps/dpi-playground/apps/dpi-playground-hub/src/transport.ts +108 -0
  192. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-token.ts +11 -0
  193. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-transport.ts +560 -0
  194. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel.ts +873 -0
  195. package/apps/dpi-playground/apps/dpi-playground-hub/tsconfig.json +13 -0
  196. package/apps/dpi-playground/package.json +37 -0
  197. package/apps/dpi-sdk/README.md +74 -0
  198. package/apps/dpi-sdk/package.json +20 -0
  199. package/apps/dpi-sdk/scripts/net.ts +73 -0
  200. package/apps/dpi-sdk/src/api.ts +631 -0
  201. package/apps/dpi-sdk/src/app-state.ts +111 -0
  202. package/apps/dpi-sdk/src/client.ts +330 -0
  203. package/apps/dpi-sdk/src/errors.ts +16 -0
  204. package/apps/dpi-sdk/src/jobs.ts +99 -0
  205. package/apps/dpi-sdk/src/models.ts +472 -0
  206. package/apps/dpi-sdk/src/node/chunk.ts +488 -0
  207. package/apps/dpi-sdk/src/node/config.ts +175 -0
  208. package/apps/dpi-sdk/src/node/drivers.ts +402 -0
  209. package/apps/dpi-sdk/src/node/ops.ts +114 -0
  210. package/apps/dpi-sdk/src/node/private-dir.ts +48 -0
  211. package/apps/dpi-sdk/src/node/registry-migration.ts +549 -0
  212. package/apps/dpi-sdk/src/node/registry.ts +390 -0
  213. package/apps/dpi-sdk/src/node/sse-writer.ts +151 -0
  214. package/apps/dpi-sdk/src/protocol/backoff.ts +31 -0
  215. package/apps/dpi-sdk/src/protocol/limits.ts +199 -0
  216. package/apps/dpi-sdk/src/protocol/pairing.ts +311 -0
  217. package/apps/dpi-sdk/src/protocol/rest-routes.ts +105 -0
  218. package/apps/dpi-sdk/src/protocol/tunnel.ts +331 -0
  219. package/apps/dpi-sdk/src/sse.ts +117 -0
  220. package/apps/dpi-sdk/src/transcript.ts +291 -0
  221. package/apps/dpi-sdk/tsconfig.json +13 -0
  222. package/apps/dpi-sdk/w +20 -0
  223. package/apps/dpi-tui/README.md +54 -0
  224. package/apps/dpi-tui/bin/dpi-tui.mjs +5 -0
  225. package/apps/dpi-tui/package.json +20 -0
  226. package/apps/dpi-tui/src/app.ts +433 -0
  227. package/apps/dpi-tui/src/commands.ts +151 -0
  228. package/apps/dpi-tui/src/main.ts +122 -0
  229. package/apps/dpi-tui/src/pairing.ts +287 -0
  230. package/apps/dpi-tui/src/pickers.ts +238 -0
  231. package/apps/dpi-tui/src/render.ts +164 -0
  232. package/apps/dpi-tui/src/theme.ts +57 -0
  233. package/apps/dpi-tui/tsconfig.json +14 -0
  234. package/bin/device.mjs +5 -0
  235. package/bin/hub.mjs +5 -0
  236. package/bin/tui.mjs +5 -0
  237. package/deploy/Caddyfile +120 -0
  238. package/docs/verification.md +102 -0
  239. package/dpi-network.example.yaml +62 -0
  240. package/images/01_durable_agent_network.svg +185 -0
  241. package/images/02_pi_made_durable.svg +204 -0
  242. package/images/03_restart_mid_tool.svg +168 -0
  243. package/images/07_one_api_every_client.svg +123 -0
  244. package/images/08_device_session_agent.svg +123 -0
  245. package/images/09_compact_keep_history.svg +143 -0
  246. package/images/10_shared_app_state.svg +177 -0
  247. package/images/11_durable_jobs_wait.svg +141 -0
  248. package/images/12_tools_change_mid_run.svg +231 -0
  249. package/images/13_hook_blocks_tool.svg +122 -0
  250. package/images/14_fork_at_any_entry.svg +119 -0
  251. package/images/15_web_client.png +0 -0
  252. package/images/16_terminal_client.svg +43 -0
  253. package/images/17_devices_are_data.svg +182 -0
  254. package/images/18_should_every_agent_be_durable.svg +118 -0
  255. package/images/19_sqlite_on_every_device.svg +155 -0
  256. package/images/20_admission_and_effects.svg +134 -0
  257. package/justfile +33 -0
  258. package/package.json +41 -4
  259. package/scripts/verify.mjs +210 -0
@@ -0,0 +1,118 @@
1
+ > Source: [packages/coding-agent/docs/virtual-models.md](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/coding-agent/docs/virtual-models.md)
2
+ > Retrieved with Firecrawl: 2026-10-05. Repository snapshot: `b78e6a9085343ec0f308c3d377da72528b4cf7ee`.
3
+ > Relative documentation links point to local copies where available, otherwise to the source snapshot.
4
+
5
+ # Virtual Models
6
+
7
+ A virtual model is a selectable model that picks a physical model for each request. Use one to route by task, cost, or conversation state. For example, a router can send quick questions to a small model and hard problems to a large one, while the user selects a single model.
8
+
9
+ Register virtual models from an [extension](extensions.md). They appear in `/model`, `--model`, scoped models, and settings like any other model. A virtual model can be listed under any provider, including one with physical models, such as `openai-codex/auto`.
10
+
11
+ ## Selection and dispatch
12
+
13
+ A virtual model selects a model and a thinking level. A router maps that pair to a physical pair for each request:
14
+
15
+ ```
16
+ selected (virtual model, virtual level) -> dispatched (physical model, physical level)
17
+ jev/auto:low -> anthropic/claude-sonnet-4-5:high
18
+ ```
19
+
20
+ The virtual thinking level is an input to the router. Its meaning is up to the router; it need not correspond to a reasoning budget.
21
+
22
+ Pi keeps the two pairs apart:
23
+
24
+ | | Selection | Dispatch |
25
+ |---|---|---|
26
+ | Recorded in | `model_change` and `thinking_level_change` entries | Each assistant message: `provider`, `api`, `model`, `thinkingLevel` |
27
+ | Visible as | `ctx.model`, `ctx.thinkingLevel`, `PI_MODEL`, `PI_REASONING_LEVEL`, `/model` | The assistant message of each response |
28
+
29
+ Providers only receive physical models. Assistant messages name the physical model, so replaying a conversation across different physical models works the same as after a manual model switch. Resuming a session restores the virtual selection from its latest `model_change` entry. If the virtual model is no longer registered, Pi falls back to the physical model that answered last.
30
+
31
+ In interactive mode, the footer shows the routed model next to the selection, for example `auto • high → gpt-5.6-luna • medium`. `/session` lists the cost for each physical model.
32
+
33
+ Context usage uses the limits of the physical model that produced the latest response, even if that response came before switching to the virtual model. Without such a response, it uses the limits declared on the virtual model, if any. Compaction checks the same limits, and again the limits of the model each request is routed to. If that model's context window is too small for the conversation, Pi compacts before sending the request; the route stays as the router chose it.
34
+
35
+ ## Register a virtual model
36
+
37
+ ```typescript
38
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
39
+
40
+ export default function (pi: ExtensionAPI) {
41
+ pi.registerVirtualModel({
42
+ provider: "router",
43
+ id: "auto",
44
+ name: "Auto",
45
+ thinkingLevels: ["low", "high"],
46
+ route(request, ctx) {
47
+ // Tool follow-ups and retries stay on the model that handled the turn.
48
+ const sticky = request.failed ?? request.previous;
49
+ if (request.reason !== "user" && sticky) {
50
+ return { model: sticky.model, thinkingLevel: sticky.thinkingLevel ?? "medium" };
51
+ }
52
+ const id = request.thinkingLevel === "high" ? "claude-sonnet-4-5" : "claude-haiku-4-5";
53
+ return { model: ctx.modelRegistry.find("anthropic", id)!, thinkingLevel: "medium" };
54
+ },
55
+ });
56
+ }
57
+ ```
58
+
59
+ - `provider` is the provider the model is listed under. It can be any provider ID. A provider can list several virtual models next to its physical ones. On a physical provider, the virtual model is available when that provider has credentials. Under an ID that no provider uses, it is always available.
60
+ - `id` must not be the ID of a physical model of that provider. If a catalog refresh later adds a physical model with the same ID, the virtual model hides it.
61
+ - `thinkingLevels` lists the levels offered for selection. It defaults to `["off"]`.
62
+ - `contextWindow` and `maxTokens` are shown before the first response. Unset limits are unknown.
63
+ - `input` lists the input types offered for selection. It defaults to text and images; physical models without image support receive placeholders.
64
+
65
+ Registration follows the same queuing and reload rules as `pi.registerProvider()`. Registering the same provider and ID again replaces the virtual model. `pi.unregisterVirtualModel(provider, id)` removes it; `pi.unregisterProvider()` does not. SDK code can register one without an extension: `modelRuntime.registerVirtualModel(definition)`.
66
+
67
+ ## Route requests
68
+
69
+ `route(request, ctx)` runs before every request made with the virtual model and returns `{ model, thinkingLevel }`. The model can be any physical model in the catalog whose provider has credentials; look it up with `ctx.modelRegistry`. A virtual model cannot route to another virtual model. Pi clamps the thinking level to the returned model.
70
+
71
+ | Field | Meaning |
72
+ |---|---|
73
+ | `model`, `thinkingLevel` | The selected virtual model and level |
74
+ | `reason` | Why the request is made, see below |
75
+ | `previous` | Physical model and thinking level of the latest successful response in `messages` |
76
+ | `failed` | For `retry`: physical model, thinking level, and assistant `message` of the failed request, which `messages` no longer contains. The message carries `stopReason` and `errorMessage`. Absent when routing itself failed |
77
+ | `state` | Router state last returned on this session branch, see below |
78
+ | `messages` | The conversation for this request, including system messages |
79
+ | `signal` | Abort signal of the request |
80
+
81
+ | `reason` | Request |
82
+ |---|---|
83
+ | `user` | First request after a message the user wrote, including steering and follow-up messages |
84
+ | `continuation` | Any other request in the agent loop, such as after tool results or extension messages |
85
+ | `retry` | Automatic retry after a failed request, including after compaction for a context overflow |
86
+ | `direct` | Request made outside the agent loop, such as a compaction summary or an extension calling `ctx.modelRegistry.streamSimple()` |
87
+
88
+ Returning `previous` for `continuation` and `failed` for `retry` keeps prompt caches and thinking signatures valid. Switching models between turns is allowed but loses the prompt cache. A retry can also switch to another model, for example when `failed.message.errorMessage` reports that a provider is overloaded or the context overflowed.
89
+
90
+ If `route()` throws, or returns a virtual model or a model without credentials, the request ends with an error response.
91
+
92
+ ## Keep routing state
93
+
94
+ `route()` can return `state` next to the model. Pi stores it on the session branch and passes it back as `request.state` on later requests. Use it for decisions the transcript does not record, such as classifier results or a routing phase:
95
+
96
+ ```typescript
97
+ pi.registerVirtualModel<{ phase: "plan" | "build" }>({
98
+ provider: "router",
99
+ id: "phased",
100
+ name: "Phased",
101
+ route(request, ctx) {
102
+ const state = request.state ?? { phase: "plan" };
103
+ const id = state.phase === "plan" ? "claude-opus-4-5" : "claude-haiku-4-5";
104
+ return { model: ctx.modelRegistry.find("anthropic", id)!, thinkingLevel: "medium", state };
105
+ },
106
+ });
107
+ ```
108
+
109
+ - State must be JSON-serializable. Returning `undefined` or `request.state` itself keeps the current state.
110
+ - Pi stores any other returned object as new state, before the request is sent, even when it equals the current state. Return a new object only when the state changes. The state stays stored if the request later fails.
111
+ - State follows the session tree, so forks and `/tree` navigation see the state of their branch. It survives compaction.
112
+ - `direct` requests have no state, and Pi ignores state they return.
113
+
114
+ The transcript already records the selection and every dispatched model, and `ctx.sessionManager.getBranch()` exposes both.
115
+
116
+ Routers can call other models through `ctx.modelRegistry`, for example `ctx.modelRegistry.classify()` with a classifier model from `ctx.modelRegistry.findOfType("classifier", provider, id)`. The call adds latency before the first token of the turn.
117
+
118
+ See [`jev-router.ts`](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/coding-agent/examples/extensions/jev-router.ts) for a complete router. It plans on a strong OpenAI Codex model chosen by the Jev classifier, lets that model make the first edit, and then switches once to a cheaper model, accepting a single prompt-cache miss. It keeps the phase as router state.
@@ -0,0 +1,71 @@
1
+ > Source: [packages/coding-agent/docs/windows.md](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/coding-agent/docs/windows.md)
2
+ > Retrieved with Firecrawl: 2026-10-05. Repository snapshot: `b78e6a9085343ec0f308c3d377da72528b4cf7ee`.
3
+ > Relative documentation links point to local copies where available, otherwise to the source snapshot.
4
+
5
+ # Run Pi on Windows
6
+
7
+ Run Pi either as a native Windows process or inside Windows Subsystem for Linux (WSL). Native Windows uses Git Bash by default for Bash commands and can optionally expose PowerShell to the model. Pi inside WSL uses the Linux environment and its Bash installation.
8
+
9
+ Follow the main [Quickstart](quickstart.md) to install and authenticate Pi. Use this page to choose and configure its command environment.
10
+
11
+ ## Choose native Windows or WSL
12
+
13
+ | Environment | Command environment | Use it when |
14
+ |---|---|---|
15
+ | Native Windows with Git Bash | Git Bash for the built-in `bash` tool and `!` commands | Your files and development tools primarily live on Windows |
16
+ | Native Windows with the `powershell` tool | PowerShell for model tool calls; Bash remains available for `!` commands | The task depends on PowerShell modules or Windows-native commands |
17
+ | WSL | Linux Bash and tools inside the selected WSL distribution | Your files and toolchain already live in Linux or WSL |
18
+
19
+ ## Use Git Bash on native Windows
20
+
21
+ For most native Windows users, installing [Git for Windows](https://git-scm.com/download/win) is sufficient.
22
+
23
+ Pi resolves Bash in this order:
24
+
25
+ 1. `shellPath` from `~/.pi/agent/settings.json`
26
+ 2. Git Bash under `Program Files` or `Program Files (x86)`
27
+ 3. `bash.exe` on `PATH`, including Cygwin, MSYS2, or legacy WSL Bash
28
+
29
+ Start Pi and enter this command to verify the shell:
30
+
31
+ ```text
32
+ !printf 'Bash is working\n'
33
+ ```
34
+
35
+ If Pi cannot find Bash, it reports the locations it checked. Install Git for Windows, put another Bash executable on `PATH`, or configure `shellPath`.
36
+
37
+ ## Let the model use PowerShell
38
+
39
+ The optional `powershell` tool runs commands through `pwsh.exe` when available, then falls back to Windows PowerShell. It starts PowerShell with `-NoProfile -NonInteractive -ExecutionPolicy Bypass`. Administrator-enforced execution policies can still take precedence.
40
+
41
+ To replace the model-facing `bash` tool with `powershell`, add this to `~/.pi/agent/settings.json`:
42
+
43
+ ```json
44
+ {
45
+ "defaultTools": ["read", "powershell", "edit", "write"]
46
+ }
47
+ ```
48
+
49
+ `["-bash", "+powershell"]` does the same while keeping any other default tools you configured.
50
+
51
+ Restart Pi, then ask it to run a harmless PowerShell command. The `!` and `!!` editor commands continue to use Bash. The `powershell` tool is available only when Pi runs as a native Windows process.
52
+
53
+ See [Settings](settings.md#tools) for other tool combinations.
54
+
55
+ ## Use a custom Bash executable
56
+
57
+ Set `shellPath` when Bash is installed somewhere Pi does not discover automatically:
58
+
59
+ ```json
60
+ {
61
+ "shellPath": "C:\\cygwin64\\bin\\bash.exe"
62
+ }
63
+ ```
64
+
65
+ JSON uses backslashes for escape sequences. When you write a Windows path with backslashes, write each backslash twice, as shown above.
66
+
67
+ See [Configure shell commands](shell-aliases.md) for command prefixes, aliases, and the complete shell-resolution behavior.
68
+
69
+ ## Configure Windows Terminal
70
+
71
+ Windows Terminal reserves or rewrites some modified keys. See [Windows Terminal](terminal-setup.md#windows-terminal) to configure `Shift+Enter` and `Alt+Enter`, and [Keybindings](keybindings.md) for Pi's Windows and WSL shortcut defaults.
@@ -0,0 +1 @@
1
+ 24
@@ -0,0 +1,99 @@
1
+ # dpi-backend API v1
2
+
3
+ The contract every Durable Agent Network client uses: the web hub, `dpi-tui`, scripts, other agents. Types live in [`../dpi-sdk/src/api.ts`](../dpi-sdk/src/api.ts). [`../dpi-sdk/src/client.ts`](../dpi-sdk/src/client.ts) is a typed client for browsers and Node.
4
+
5
+ ## Conventions
6
+
7
+ - **Auth.** Every `/v1/*` route requires `Authorization: Bearer <DPI_TOKEN>`. `X-DPI-Token: <DPI_TOKEN>` also works, for proxies that strip `Authorization`. Only `GET /healthz` is open.
8
+ - **Vocabulary.** A **session** is one harness and one agent database on the device. An **agent** is a pi-durable conversation in that session: the root (`Main`), a fork, or a subagent. The agent id is the conversation id.
9
+ - **Addressing.** `:s` is a session UUID or its unique name (case-insensitive). `:a` is a numeric agent id, `main`, or a unique agent name. Ambiguous names return `409 ambiguous`.
10
+ - **Idempotency.** `POST .../messages` takes a required `requestId`, and pi-durable admits each one exactly once. `POST /sessions`, `POST .../agents` and `POST .../fork` accept an `Idempotency-Key` header of up to 128 characters. A repeat gets the stored response with `Idempotent-Replay: true`. Keys are kept for at least 24h, and reusing a key on another route returns `400 invalid`.
11
+ - **Errors.** Every error has the shape `{ "error": { "code", "message" } }`. Codes: `unauthorized` 401, `not_found` 404, `invalid` 400, `busy` 409 (reject-if-busy), `conflict` 409 (stale App State revision), `ambiguous` 409, `unavailable` 503 (session failed to open), `internal` 500. Clients can also see `offline`: the SDK raises it when a device cannot be reached, and the hub's proxy returns it with 502.
12
+ - **Versioning.** `/v1` and `apiVersion: 1` in `GET /v1/device` and in every `hello` frame. Changes inside v1 are additive.
13
+
14
+ ## Routes
15
+
16
+ | Route | Body / query | Returns |
17
+ |---|---|---|
18
+ | `GET /healthz` | (no auth) | `{ok:true}` |
19
+ | `GET /v1/device` | | `DeviceInfo`: uid, name, hostname, platform, `apiVersion`, `durableVersion`, `startedAt`, models, `defaultModel`, `startDirs`, storage, `codeDir` (the checkout it runs from), capabilities, `jobs` (durable job definitions) |
20
+ | `GET /v1/tree` | | `DeviceTree`: sessions with agents and busy/background |
21
+ | `GET /v1/events` | `?docs=app.shared-state` | device stream |
22
+ | `GET /v1/fs/dirs` | `?path=` (empty = start dirs) | `{path, parent, dirs[]}`, one level, directories only |
23
+ | `GET /v1/docs/app.shared-state` | | `{kind, value, revision}` |
24
+ | `GET /v1/sessions` | | `SessionSummary[]` |
25
+ | `POST /v1/sessions` | `{name, cwd, model:{provider,modelId}, thinkingLevel?}` | `201 SessionSummary` |
26
+ | `GET / PATCH / DELETE /v1/sessions/:s` | PATCH `{name}` | summary (with its `compaction` thresholds); DELETE aborts all work, moves the files to `archive/` and returns `{archived}` |
27
+ | `GET / PATCH /v1/sessions/:s/compaction` | PATCH `{enabled?, reserveTokens?, backgroundTokens?, keepRecentTokens?}` | the session's automatic compaction thresholds, read by pi-durable at its next request. Values are margins: the blocking summary starts at the model's context window minus `reserveTokens`, and the background one `backgroundTokens` earlier (0 turns it off). Default: a background summary at 200k tokens and a blocking one at 250k, or pi-durable's margins on smaller windows. Switching an agent to a window the thresholds do not fit refits them |
28
+ | `GET /v1/sessions/:s/events` | | session stream |
29
+ | `GET /v1/sessions/:s/tasks` | `?status=live\|terminal&limit=` | task nodes / recent outcomes |
30
+ | `POST /v1/sessions/:s/tasks/:taskId/abort` | | `{status}` |
31
+ | `GET /v1/sessions/:s/inspect` | | recovery inspection |
32
+ | `GET /v1/sessions/:s/submissions/:id` | `?waitMs=` (≤300000) | `SendResult`, optionally long-polled |
33
+ | `GET / POST /v1/sessions/:s/agents` | POST `{from?, name?}` | `AgentSummary[]` / `201 AgentSummary` |
34
+ | `GET / PATCH .../agents/:a` | PATCH `{name}` | `AgentDetail` (config with `readOnly`, every tool with `replay`, `writes` and `offered`, extensions) / summary |
35
+ | `GET .../agents/:a/events` | `?docs=demo.job-runs` | agent stream |
36
+ | `GET .../agents/:a/entries` | `?scope=all\|active&cursor=&limit=` | `{items, next}` stored history; `limit` defaults to 500 (max 2000). `scope=active` returns `{items}`, the active context, unpaged |
37
+ | `GET .../agents/:a/transcript` | `?format=json\|markdown` | normalized transcript |
38
+ | `POST .../agents/:a/messages` | `{content, whenBusy?: followUp\|steer\|reject, requestId, waitMs?}` (`whenBusy` defaults to `followUp`; `waitMs` ≤300000) | `202 {submissionId, status:"queued"\|"pending"}`, `200 {status:"done", answer:{entryId,text}}` or `200 {status:"unanswered", reason}` |
39
+ | `DELETE .../agents/:a/inbox/:submissionId` | | withdraw a queued input |
40
+ | `POST .../agents/:a/abort` | `{background?}` | stops foreground work (and background with `true`) |
41
+ | `POST .../agents/:a/fork` | `{entryId, name?}` | `201 AgentSummary` |
42
+ | `GET / PATCH .../agents/:a/config` | `{model?, thinkingLevel?, instructions?, cwd?, readOnly?}` | config; applies from the next request. `readOnly` filters the agent's tools to the ones that only read |
43
+ | `GET / PATCH .../agents/:a/extensions` | PATCH `{name, enabled}` or `{prompt, enabled}` | `{extensions, builtIn}`: the custom extensions `system-prompts` (a checklist of the prompts in `prompts/`) and `jev-damage-control` (Jev judges each bash command and blocks destructive ones; the command goes to the Jev API, and a Jev error lets it run). Both are off by default. A change applies from the agent's next request |
44
+ | `POST .../agents/:a/compact` | `{instructions?}` | `{taskId}` |
45
+ | `POST .../agents/:a/reset` | `{note?}` | `{ok}` |
46
+ | `GET .../agents/:a/docs/:kind` | `demo.job-runs` | `{kind, value}` |
47
+ | `POST .../agents/:a/demo/state` | `{operation, path, value?, expectedRevision?}` | `{ok, value, revision}`: the new App State. A stale `expectedRevision` returns `409 conflict` |
48
+ | `POST .../agents/:a/demo/jobs` | `{job, input?}` | `201 {taskId}`: a durable job run the conversation owns in the background, anchored by a `demo.job` entry |
49
+ | `POST .../agents/:a/demo/jobs/:taskId/approval` | `{approved}` | `{ok}`; `409 conflict` when the run is not waiting |
50
+
51
+ ## Agent tools
52
+
53
+ Every agent gets the coding tools `read`, `write`, `edit` and `bash`, plus the playground tools `read_state`, `update_state`, `list_durable_jobs`, `run_durable_job`, `check_durable_job`, `search_history`, `ping_on_call_engineer`, `subagent`, `start_session` and `handoff`. `readOnly` removes the eight that write: `bash`, `write`, `edit`, `update_state`, `run_durable_job`, `ping_on_call_engineer`, `subagent` and `start_session`. `GET .../agents/:a` lists each tool with its replay policy and whether it is offered.
54
+
55
+ ## Durable jobs
56
+
57
+ A durable job is a repeatable workflow with named phases. Each run is one pi-durable task (`demo.job.<id>`) whose checkpoint phases are the job's phases, so a restart resumes the current phase. The run's record, with every phase and step (commands, a subagent's tool calls, App State writes, checkpoints, the approval), is the conversation document `demo.job-runs`; types are in [`../dpi-sdk/src/jobs.ts`](../dpi-sdk/src/jobs.ts). Every run saves its result to App State under `/jobs/<id>`.
58
+
59
+ | Job | Phases | Inputs |
60
+ |---|---|---|
61
+ | `workspace-snapshot` | scan → measure → save → verify | `path` |
62
+ | `release-check` | plan → gather (subagent with read and bash) → approve (waits for a person) → publish → verify | `commits` |
63
+ | `change-watch` | baseline → watch (one checkpoint per check) → report | `path`, `checks`, `interval_seconds` |
64
+
65
+ Agents use the playground tools: `list_durable_jobs` (ids, phases, inputs, approval, result path and recent runs with their task ids), `run_durable_job {job, input?, wait?}` and `check_durable_job {task_id? | job?}` (one run's phases, steps and result, a job's latest run, or recent runs). A run starts in the background, owned by the conversation; with `wait: true` the call owns it and waits, so an interrupt stops it. The same extension adds `search_history {query?, tool?, entry?, kind?, from_entry?, to_entry?, order?, limit?, offset?}`, which pages through the agent's whole stored history including entries before a reset or compaction, and `ping_on_call_engineer {summary, severity?}`, a simulated pager (no external call) that is not replay-safe and works once per agent.
66
+
67
+ ## Streams
68
+
69
+ Server-Sent Events. Each connection sends `hello`, then a snapshot, then deltas, plus a `: ping` comment every 15s. Treat 45s of silence as a dead socket. Reconnects get a fresh snapshot and nothing is replayed (pi-durable's watch semantics). A viewer that falls 2 MB behind is disconnected. Closing a stream never stops work.
70
+
71
+ ```
72
+ id: 7
73
+ event: batch
74
+ data: {"type":"batch","events":[{"type":"message_update", ...}]}
75
+ ```
76
+
77
+ | Scope | Frames |
78
+ |---|---|
79
+ | device `/v1/events` | `tree {tree}` (deduped, coalesced 250ms), `activity {items:[{type:"agent.started"\|"agent.finished", sessionId, agentId, at, preview?}]}`, `doc {kind, value, revision}` |
80
+ | session `/v1/sessions/:s/events` | `session {agents, tasks, usage}`, then `agents`, `tasks`, `usage` |
81
+ | agent `.../agents/:a/events` | `snapshot {snapshot, inbox}`, `batch {events}` (pi-durable `watchEvents()` verbatim), `inbox {items}` (queued content), `doc {kind, value}` |
82
+ | all | `hello {apiVersion, scope, serverTime}` |
83
+
84
+ The hub's multiplexed stream (`/api/stream`) carries these frames per subscription and adds `stream {streamId}`, `network {devices}`, `fault {error}` and `disconnected {reason}`.
85
+
86
+ To render a transcript from the agent stream in JS, feed frames to `AgentModel` from the SDK. It handles text/thinking/tool-call deltas, tool output windows, results, resets and overflow snapshots.
87
+
88
+ ## Recipes
89
+
90
+ ```bash
91
+ T=<token>; U=https://my-dpi-vm-1.exe.xyz # any device URL from dpi-network.yaml
92
+ # Send work and wait for the answer, all in one request:
93
+ curl -s -X POST $U/v1/sessions/my-session/agents/main/messages -H "Authorization: Bearer $T" \
94
+ -H 'content-type: application/json' -d '{"content":"Summarize the failing tests","requestId":"ci-4812","waitMs":120000}'
95
+ # Read the whole conversation as markdown:
96
+ curl -s "$U/v1/sessions/my-session/agents/main/transcript?format=markdown" -H "Authorization: Bearer $T" | jq -r .text
97
+ # Watch everything on a device:
98
+ curl -sN $U/v1/events -H "Authorization: Bearer $T"
99
+ ```
@@ -0,0 +1,119 @@
1
+ # dpi-backend
2
+
3
+ The Durable Pi **device server**. Run one on every machine that should host durable agents: your laptop, a Mac mini, a Linux box, an exe.dev VM. It owns the harnesses, the SQLite databases, the tools and the filesystem. Clients connect with the device's **URL and bearer token** through the [v1 API](API.md).
4
+
5
+ ```
6
+ client (hub / dpi-tui / curl) ──HTTP + SSE, Bearer──► dpi-backend ──► sessions/<id>/agent.sqlite (pi-durable)
7
+ ```
8
+
9
+ ## Run it on this machine
10
+
11
+ Needs Node 22.19 or newer (`.nvmrc` pins 24).
12
+
13
+ ```bash
14
+ cd apps/dpi-backend && npm install
15
+ npm run token # creates ~/.dpi/backend.env with DPI_TOKEN and prints it (npm run token -- --rotate replaces it)
16
+ echo "OPENROUTER_API_KEY=sk-or-..." >> ~/.dpi/backend.env
17
+ npm start # 127.0.0.1:4780; data in ~/.dpi/backend
18
+ ```
19
+
20
+ With the repo's network config, `just net up local && just backend run` does the same thing. `just backend run` first stops whatever already listens on the backend's port.
21
+
22
+ ## Configuration: `~/.dpi/backend.env`
23
+
24
+ | Key | Default | Meaning |
25
+ |---|---|---|
26
+ | `DPI_TOKEN` | required, ≥32 chars | bearer token for every `/v1` call |
27
+ | `DPI_DEVICE_NAME` | hostname | name shown to clients |
28
+ | `DPI_PORT` / `DPI_HOST` | `4780` / `127.0.0.1` | listen address. `just net up` writes the YAML `host`, which defaults to `0.0.0.0` for `ssh` and `exe-dev` devices so other machines can reach them |
29
+ | `DPI_START_DIRS` | `~/dpi-work` | colon-separated folders where new sessions start, created at startup if missing: the directory browser opens there and relative cwds resolve against the first. Any existing directory is allowed as cwd |
30
+ | `DPI_DATA_DIR` | `$DPI_HOME/backend` | catalog, app state, `sessions/`, `archive/` |
31
+ | `DPI_HOME` | `~/.dpi` | where `backend.env` is read (set by the service unit) |
32
+ | `DPI_ENV_FILE` | `$DPI_HOME/backend.env` | the env file to read |
33
+ | `DPI_PROVIDER` | `openrouter` | `faux` gives a free deterministic model for tests |
34
+ | `OPENROUTER_API_KEY` | | model access, required unless `DPI_PROVIDER=faux`. Without it the device lists no models. Never sent to clients |
35
+ | `TYPESAFE_API_KEY` | | optional: `jev-damage-control` asks Jev with it, else through OpenRouter |
36
+
37
+ Flags `--port`, `--host`, `--name`, `--start-dirs`, `--data-dir` and `--env-file` override the env.
38
+
39
+ ## Deploy to other devices
40
+
41
+ Devices are declared in the repo root's `dpi-network.yaml` (gitignored; start from [`dpi-network.example.yaml`](../../dpi-network.example.yaml)). Nothing else names a machine:
42
+
43
+ ```bash
44
+ just net up <id> # (exe.dev: create →) provision → deploy → env → service → (exe.dev: expose →) register → wait online
45
+ just net status # probe every device
46
+ just net logs <id> | just net restart <id> | just net deploy <id> | just net down <id> | just net destroy <id>
47
+ just net rotate <id> # new token, restart, re-register
48
+ just net probe <id> # /healthz 200 and /v1/device 401 without a token
49
+ ```
50
+
51
+ | Kind | Service | Notes |
52
+ |---|---|---|
53
+ | `local` | none (`just backend run`) or launchd/systemd | binds 127.0.0.1 |
54
+ | `ssh` | `launchd` → `~/Library/LaunchAgents/dev.dpi.backend.plist`; `systemd` → `/etc/systemd/system/dpi-backend.service` | binds 0.0.0.0 by default; needs passwordless ssh; systemd needs `sudo -n` |
55
+ | `exe-dev` | systemd | VM created on demand; the HTTPS proxy is set public, so the token is the gate |
56
+ | `url` | none | connect only: `just net register <id> --token <t>` |
57
+
58
+ Templates live in [`deploy/`](deploy/). The operator skill [`.claude/skills/agent-sandboxes-exe-dev/cookbook/dpi-network.md`](../../.claude/skills/agent-sandboxes-exe-dev/cookbook/dpi-network.md) covers debugging per kind.
59
+
60
+ ## Layout on a device
61
+
62
+ ```
63
+ ~/dpi/apps/dpi-backend, ~/dpi/apps/dpi-sdk code (rsynced; safe to replace)
64
+ ~/.dpi/backend.env token + config (0600)
65
+ ~/.dpi/backend/catalog.sqlite sessions, agent names, device uid, idempotency keys, compaction thresholds
66
+ ~/.dpi/backend/app-state.sqlite the device's shared App State (demo extension)
67
+ ~/.dpi/backend/sessions/<uuid>/agent.sqlite one pi-durable harness per session
68
+ ~/.dpi/backend/archive/ archived sessions (DELETE never removes files)
69
+ ~/.dpi/backend.log service log
70
+ ```
71
+
72
+ ## Guarantees and boundaries
73
+
74
+ - Closing a client never stops work. A backend restart resumes unfinished tasks from their checkpoints. Replay-safe tools may rerun, and unsafe ones return an interrupted result.
75
+ - A token holder has the agent's shell and filesystem access on that device. Start dirs are where sessions begin, not a sandbox.
76
+ - macOS `ssh` devices run as a LaunchAgent in the logged-in session, so enable auto-login for unattended reboots. Agents working under `~/Documents` need Full Disk Access for the node binary.
77
+ - LAN `ssh` devices speak plain HTTP, so the token crosses the LAN in cleartext. exe.dev devices use HTTPS.
78
+ - `jev-damage-control` is off by default. When on, it sends each bash command to the Jev API, and the command runs if Jev fails. It is a guardrail, not a sandbox.
79
+
80
+ ## Demo runbook: hop into an already running agent
81
+
82
+ Any device id from `dpi-network.yaml` whose backend runs as a service works. `DEVICE=mac-mini` below.
83
+
84
+ ```bash
85
+ just net status # 1. every device online
86
+ just hub start # 2. web UI: http://localhost:4781
87
+ just tui new $DEVICE e2e-network --cwd '~/dpi/e2e' # 3. a session in the repo's e2e/ folder on the device, attached
88
+ ```
89
+
90
+ | Step | Do | Expect |
91
+ |---|---|---|
92
+ | 4 | In the TUI, prompt a multi-tool task (write a script, run it, improve it three times) | The TUI header shows `● live`, the status line `working…`, and tool rows with spinners |
93
+ | 5 | Open the web UI | The tree shows `$DEVICE › e2e-network › Main` with a pulsing dot. The transcript streams in both clients |
94
+ | 6 | In the web composer pick **Steer** and send "use bash only for verification runs" | The TUI shows `↪ steer: …`, then the message lands after the current tool round |
95
+ | 7 | Ctrl+C the TUI (it prints `just tui attach $DEVICE:<session>:<agent>`), close the browser, run `just net restart $DEVICE` while a tool runs | Nothing is watching. The service restarts in about a second |
96
+ | 8 | Reopen the web UI | The device dims, then returns online. The run resumes from its checkpoint, and an interrupted bash call shows its interrupted result |
97
+ | 9 | Fork from the web at an earlier reply, name it `e2e-fork`, then run `just tui attach $DEVICE:e2e-network:e2e-fork` | The TUI header shows `⑂ e2e-fork` with the history up to that entry |
98
+
99
+ `just test e2e $DEVICE` runs steps 3 to 9 unattended and checks the committed state. Its session works in `<code_dir>/e2e` on the device, one `fib-<time>/` folder per run.
100
+
101
+ The one-request client needs nothing but curl. The answer comes back in the same request:
102
+
103
+ ```bash
104
+ read U T < <(sqlite3 -separator ' ' ~/.dpi/network.sqlite "select url, token from devices where id='vm-1'")
105
+ H=(-H "Authorization: Bearer $T" -H 'content-type: application/json')
106
+ # cwd "." is the device's first start dir
107
+ curl -s "$U/v1/sessions" "${H[@]}" -d '{"name":"oneshot","cwd":".","model":{"provider":"openrouter","modelId":"anthropic/claude-sonnet-5.5"}}'
108
+ curl -s "$U/v1/sessions/oneshot/agents/main/messages" "${H[@]}" \
109
+ -d '{"content":"How many files are in this directory? Use bash.","requestId":"oneshot-1","waitMs":120000}'
110
+ # {"submissionId":…,"status":"done","answer":{"entryId":…,"text":"…"}} (on timeout: 202 "pending" or "queued"; poll /submissions/<id>?waitMs=)
111
+ ```
112
+
113
+ ## Development
114
+
115
+ ```bash
116
+ npm run dev # restart on file changes (just backend dev)
117
+ npm run check # strict types, including the pi-durable ↔ SDK wire contract check
118
+ npm test # api, streams, store (jobs, start_session, extensions, read only), search_history, shared state and SIGKILL recovery (faux provider, no network)
119
+ ```
@@ -0,0 +1,9 @@
1
+ # ~/.dpi/backend.env (mode 0600). `just net up <device>` writes this for you.
2
+ DPI_TOKEN=dpi_replace_with_32_random_bytes_base64url
3
+ DPI_DEVICE_NAME=my-device
4
+ DPI_PORT=4780
5
+ # 127.0.0.1 (the default) serves this machine only. ssh and exe-dev devices use 0.0.0.0 so clients and the exe.dev proxy can reach them.
6
+ DPI_HOST=127.0.0.1
7
+ # Colon-separated folders where new sessions start, created if missing (default ~/dpi-work).
8
+ DPI_START_DIRS=~/dpi-work
9
+ OPENROUTER_API_KEY=sk-or-...
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key><string>__LABEL__</string>
6
+ <key>ProgramArguments</key>
7
+ <array>
8
+ <string>__NODE__</string>
9
+ <string>__APP__/node_modules/tsx/dist/cli.mjs</string>
10
+ <string>src/main.ts</string>
11
+ </array>
12
+ <key>WorkingDirectory</key><string>__APP__</string>
13
+ <key>EnvironmentVariables</key>
14
+ <dict>
15
+ <key>PATH</key><string>__PATH__</string>
16
+ <key>HOME</key><string>__HOME__</string>
17
+ <key>DPI_HOME</key><string>__DPI_HOME__</string>
18
+ <key>NODE_NO_WARNINGS</key><string>1</string>
19
+ </dict>
20
+ <key>RunAtLoad</key><true/>
21
+ <key>KeepAlive</key><true/>
22
+ <key>ThrottleInterval</key><integer>5</integer>
23
+ <key>StandardOutPath</key><string>__LOG__</string>
24
+ <key>StandardErrorPath</key><string>__LOG__</string>
25
+ </dict>
26
+ </plist>
@@ -0,0 +1,22 @@
1
+ [Unit]
2
+ Description=Durable Pi device server (dpi-backend)
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ User=__USER__
8
+ WorkingDirectory=__APP__
9
+ ExecStart=__NODE__ __APP__/node_modules/tsx/dist/cli.mjs src/main.ts
10
+ Environment=PATH=__PATH__
11
+ Environment=HOME=__HOME__
12
+ Environment=DPI_HOME=__DPI_HOME__
13
+ Environment=NODE_NO_WARNINGS=1
14
+ Restart=always
15
+ RestartSec=2
16
+ KillSignal=SIGTERM
17
+ TimeoutStopSec=20
18
+ StandardOutput=append:__LOG__
19
+ StandardError=append:__LOG__
20
+
21
+ [Install]
22
+ WantedBy=multi-user.target
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "dpi-backend",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "Durable Pi device server: hosts durable sessions and exposes them over an authenticated HTTP + SSE API (v1).",
6
+ "scripts": {
7
+ "start": "tsx src/main.ts",
8
+ "dev": "tsx watch src/main.ts",
9
+ "check": "tsc --noEmit -p .",
10
+ "test": "tsx --test tests/*.test.ts",
11
+ "test:timing": "tsx --test --test-reporter=tap --test-concurrency=1 tests/timing/*.timing.ts",
12
+ "token": "tsx scripts/token.ts",
13
+ "migrate-playground": "tsx scripts/migrate-playground.ts",
14
+ "check:contract": "node scripts/check-contract.mjs"
15
+ },
16
+ "dependencies": {
17
+ "@earendil-works/chord": "1.3.0",
18
+ "@earendil-works/pi-ai": "1.3.0",
19
+ "@earendil-works/pi-durable": "1.3.0",
20
+ "express": "^5.1.0",
21
+ "tsx": "^4.20.0",
22
+ "ws": "8.22.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/express": "^5.0.0",
26
+ "@types/node": "^24.0.0",
27
+ "@types/ws": "8.18.2",
28
+ "typescript": "5.9.3"
29
+ },
30
+ "engines": {
31
+ "node": ">=22.19.0"
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ # One Sentence
2
+
3
+ Answer in one sentence unless the user asks for more detail. Tool calls are not limited; only your reply to the user is.