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,291 @@
1
+ // Normalize pi-durable entries + live generation state into renderable transcript items.
2
+ // The web UI, the TUI and the backend's /transcript endpoint all use this, so every
3
+ // interface stitches tool calls, results and live output the same way.
4
+ import type {
5
+ WireAssistantMessage,
6
+ WireEntry,
7
+ WireMessage,
8
+ WireToolResultMessage,
9
+ WireToolSlot,
10
+ WireUsage,
11
+ WireUsageState,
12
+ } from "./api.js";
13
+
14
+ export type ToolStatus = "pending" | "running" | "done" | "error";
15
+
16
+ export interface ToolCallView {
17
+ callId: string;
18
+ name: string;
19
+ args: unknown;
20
+ status: ToolStatus;
21
+ /** Live output while running. */
22
+ output?: string;
23
+ /** Result text once the result entry exists, or once a nested call returns its result. */
24
+ result?: string;
25
+ resultEntryId?: number;
26
+ /** Nested call metadata: the parent call that made it, and the call's own task. */
27
+ nested?: { parentCallId: string; parentTaskId: number; taskId: number };
28
+ /** One-line summary of how a nested call ended (duration, spend, error). */
29
+ summary?: string;
30
+ }
31
+
32
+ export type AssistantBlockView =
33
+ | { type: "text"; text: string }
34
+ | { type: "thinking"; text: string }
35
+ | { type: "tool"; call: ToolCallView };
36
+
37
+ export type TranscriptItem =
38
+ | { kind: "user"; id: number; text: string }
39
+ | {
40
+ kind: "assistant";
41
+ id: number | "live";
42
+ streaming: boolean;
43
+ blocks: AssistantBlockView[];
44
+ error?: string;
45
+ model?: string;
46
+ }
47
+ | { kind: "tool-result"; id: number; name: string; text: string; isError: boolean }
48
+ | { kind: "tool"; id: string; call: ToolCallView }
49
+ | { kind: "event"; id: number; entryKind: string; text: string; note?: string; data?: unknown };
50
+
51
+ export interface LiveTool {
52
+ name: string;
53
+ status: "pending" | "running" | "done";
54
+ output: string;
55
+ args?: unknown;
56
+ /** Set for a nested call (one a running tool made through `executeTool()`). */
57
+ nested?: { parentCallId: string; parentTaskId: number; taskId: number };
58
+ /** A nested result kept for display when no transcript entry holds it. */
59
+ result?: { text: string; isError: boolean };
60
+ /** How a nested call ended, for a status line (`NestedToolSummary`). */
61
+ summary?: { isError: boolean; durationMs?: number; usage?: unknown; error?: string };
62
+ }
63
+
64
+ const isAssistant = (m: WireMessage): m is WireAssistantMessage => m.role === "assistant";
65
+ const isToolResult = (m: WireMessage): m is WireToolResultMessage => m.role === "toolResult";
66
+
67
+ export function messageText(message: WireMessage): string {
68
+ const content = (message as { content?: unknown }).content;
69
+ if (typeof content === "string") return content;
70
+ if (!Array.isArray(content)) return "";
71
+ return content
72
+ .map((block: { type?: string; text?: string }) => (block.type === "text" ? (block.text ?? "") : ""))
73
+ .join("");
74
+ }
75
+
76
+ export function assistantError(message: WireAssistantMessage): string | undefined {
77
+ if (message.errorMessage) return message.errorMessage;
78
+ if (message.stopReason === "aborted") return "Interrupted. Partial response preserved.";
79
+ if (message.stopReason === "error") return "Provider request failed.";
80
+ return undefined;
81
+ }
82
+
83
+ function eventText(entry: WireEntry): string {
84
+ const data = entry.data as { text?: unknown } | undefined;
85
+ if (typeof data?.text === "string") return data.text;
86
+ if (entry.kind === "pi.reset") return "New context started. Earlier entries remain stored.";
87
+ if (entry.kind === "pi.compaction") return "Context compacted. Earlier entries remain stored.";
88
+ return entry.kind;
89
+ }
90
+
91
+ export function toTranscript(
92
+ entries: readonly WireEntry[],
93
+ live?: WireAssistantMessage,
94
+ tools: ReadonlyMap<string, LiveTool> = new Map(),
95
+ ): TranscriptItem[] {
96
+ const results = new Map<string, { entryId: number; message: WireToolResultMessage }>();
97
+ const callIds = new Set<string>();
98
+ for (const entry of entries) {
99
+ for (const message of entry.model ?? []) {
100
+ if (entry.kind === "pi.tool-result" && isToolResult(message))
101
+ results.set(message.toolCallId, { entryId: entry.id, message });
102
+ if (entry.kind === "pi.assistant" && isAssistant(message))
103
+ for (const block of message.content) if (block.type === "toolCall") callIds.add(block.id);
104
+ }
105
+ }
106
+ if (live) for (const block of live.content) if (block.type === "toolCall") callIds.add(block.id);
107
+
108
+ const toolView = (id: string, name: string, args: unknown): ToolCallView => {
109
+ const result = results.get(id);
110
+ const running = tools.get(id);
111
+ const nested = running?.nested;
112
+ if (result)
113
+ return {
114
+ callId: id,
115
+ name,
116
+ args,
117
+ status: result.message.isError ? "error" : "done",
118
+ result: messageText(result.message),
119
+ resultEntryId: result.entryId,
120
+ ...(nested ? { nested } : {}),
121
+ };
122
+ // A nested call's result lives in its caller's document, not the transcript: render it from the live slot.
123
+ if (running?.result)
124
+ return {
125
+ callId: id,
126
+ name,
127
+ args,
128
+ status: running.result.isError ? "error" : "done",
129
+ result: running.result.text,
130
+ ...(nested ? { nested } : {}),
131
+ ...(running.summary ? { summary: summaryLine(running.summary) } : {}),
132
+ };
133
+ return {
134
+ callId: id,
135
+ name,
136
+ args,
137
+ status: running?.status ?? "pending",
138
+ output: running?.output || undefined,
139
+ ...(nested ? { nested } : {}),
140
+ ...(running?.summary ? { summary: summaryLine(running.summary) } : {}),
141
+ };
142
+ };
143
+
144
+ const assistantBlocks = (message: WireAssistantMessage): AssistantBlockView[] =>
145
+ message.content.flatMap((block): AssistantBlockView[] => {
146
+ if (block.type === "text") return block.text ? [{ type: "text", text: block.text }] : [];
147
+ if (block.type === "thinking")
148
+ return block.thinking ? [{ type: "thinking", text: block.thinking }] : [];
149
+ return [{ type: "tool", call: toolView(block.id, block.name, block.arguments) }];
150
+ });
151
+
152
+ const items: TranscriptItem[] = [];
153
+ for (const entry of entries) {
154
+ if (entry.kind === "pi.system") continue;
155
+ if (entry.kind === "pi.user") {
156
+ items.push({
157
+ kind: "user",
158
+ id: entry.id,
159
+ text: (entry.model ?? []).map(messageText).join("\n"),
160
+ });
161
+ } else if (entry.kind === "pi.assistant") {
162
+ const messages = (entry.model ?? []).filter(isAssistant);
163
+ items.push({
164
+ kind: "assistant",
165
+ id: entry.id,
166
+ streaming: false,
167
+ blocks: messages.flatMap(assistantBlocks),
168
+ error: messages.map(assistantError).find(Boolean),
169
+ model: messages[0]?.model,
170
+ });
171
+ } else if (entry.kind === "pi.tool-result") {
172
+ for (const message of (entry.model ?? []).filter(isToolResult)) {
173
+ if (callIds.has(message.toolCallId)) continue;
174
+ items.push({
175
+ kind: "tool-result",
176
+ id: entry.id,
177
+ name: message.toolName,
178
+ text: messageText(message),
179
+ isError: message.isError,
180
+ });
181
+ }
182
+ } else {
183
+ items.push({
184
+ kind: "event",
185
+ id: entry.id,
186
+ entryKind: entry.kind,
187
+ text: eventText(entry),
188
+ note: entry.model?.length ? (entry.model ?? []).map(messageText).join("\n") : undefined,
189
+ ...(entry.data !== undefined ? { data: entry.data } : {}),
190
+ });
191
+ }
192
+ }
193
+ if (live)
194
+ items.push({
195
+ kind: "assistant",
196
+ id: "live",
197
+ streaming: true,
198
+ blocks: assistantBlocks(live),
199
+ model: live.model,
200
+ });
201
+ for (const [callId, tool] of tools)
202
+ // A nested call is presentation of the running tool round: render it even once done (its result is not in the
203
+ // transcript). A model-issued call renders from its result entry instead, so its stale slot is dropped.
204
+ if ((tool.status !== "done" || tool.result || tool.nested) && !callIds.has(callId))
205
+ items.push({ kind: "tool", id: callId, call: toolView(callId, tool.name, tool.args) });
206
+ return items;
207
+ }
208
+
209
+ /** "1.2s · error text" style one-liner from a nested call's summary. */
210
+ function summaryLine(summary: { isError: boolean; durationMs?: number; error?: string }): string {
211
+ const parts: string[] = [];
212
+ if (summary.durationMs !== undefined) parts.push(`${(summary.durationMs / 1000).toFixed(1)}s`);
213
+ if (summary.error) parts.push(summary.error);
214
+ return parts.join(" · ");
215
+ }
216
+
217
+ export function toMarkdown(items: readonly TranscriptItem[]): string {
218
+ const out: string[] = [];
219
+ for (const item of items) {
220
+ if (item.kind === "user") out.push(`### You (#${item.id})\n\n${item.text}`);
221
+ else if (item.kind === "assistant") {
222
+ const parts = item.blocks.map((block) =>
223
+ block.type === "text"
224
+ ? block.text
225
+ : block.type === "thinking"
226
+ ? `> _thinking:_ ${block.text.replaceAll("\n", " ").slice(0, 400)}`
227
+ : `- \`${block.call.name}\` ${block.call.status}${block.call.result ? `\n\n\`\`\`\n${block.call.result.slice(0, 2000)}\n\`\`\`` : ""}`,
228
+ );
229
+ if (item.error) parts.push(`> ${item.error}`);
230
+ out.push(`### Agent (${item.id === "live" ? "working" : `#${item.id}`})\n\n${parts.join("\n\n")}`);
231
+ } else if (item.kind === "tool-result")
232
+ out.push(`- \`${item.name}\` ${item.isError ? "error" : "result"}\n\n\`\`\`\n${item.text.slice(0, 2000)}\n\`\`\``);
233
+ else if (item.kind === "tool") out.push(`- \`${item.call.name}\` ${item.call.status}`);
234
+ else out.push(`_${item.text}_`);
235
+ }
236
+ return out.join("\n\n") + "\n";
237
+ }
238
+
239
+ /** Text of the last assistant entry, for previews and simple clients. */
240
+ export function lastAssistantText(entries: readonly WireEntry[]): string | undefined {
241
+ for (let i = entries.length - 1; i >= 0; i--) {
242
+ const entry = entries[i];
243
+ if (entry.kind !== "pi.assistant") continue;
244
+ const text = (entry.model ?? []).filter(isAssistant).map(messageText).join("").trim();
245
+ if (text) return text;
246
+ }
247
+ return undefined;
248
+ }
249
+
250
+ export interface UsageSummary {
251
+ tokens: number;
252
+ cost: number;
253
+ input: number;
254
+ output: number;
255
+ cacheRead: number;
256
+ cacheWrite: number;
257
+ inputCost: number;
258
+ outputCost: number;
259
+ cacheReadCost: number;
260
+ cacheWriteCost: number;
261
+ }
262
+
263
+ export function sumUsage(state?: Pick<WireUsageState, "models"> | { models: Record<string, WireUsage> }): UsageSummary {
264
+ const total: UsageSummary = {
265
+ tokens: 0,
266
+ cost: 0,
267
+ input: 0,
268
+ output: 0,
269
+ cacheRead: 0,
270
+ cacheWrite: 0,
271
+ inputCost: 0,
272
+ outputCost: 0,
273
+ cacheReadCost: 0,
274
+ cacheWriteCost: 0,
275
+ };
276
+ for (const u of Object.values(state?.models ?? {})) {
277
+ total.tokens += u.totalTokens ?? u.input + u.output + u.cacheRead + u.cacheWrite;
278
+ total.cost += u.cost?.total ?? 0;
279
+ total.input += u.input ?? 0;
280
+ total.output += u.output ?? 0;
281
+ total.cacheRead += u.cacheRead ?? 0;
282
+ total.cacheWrite += u.cacheWrite ?? 0;
283
+ total.inputCost += u.cost?.input ?? 0;
284
+ total.outputCost += u.cost?.output ?? 0;
285
+ total.cacheReadCost += u.cost?.cacheRead ?? 0;
286
+ total.cacheWriteCost += u.cost?.cacheWrite ?? 0;
287
+ }
288
+ return total;
289
+ }
290
+
291
+ export type { WireToolSlot };
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "types": ["node"]
11
+ },
12
+ "include": ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts"]
13
+ }
package/apps/dpi-sdk/w ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "dpi-sdk",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "Durable Agent Network contract, client, models and device registry. Browser-safe core; src/node needs yaml; src/protocol needs @sinclair/typebox (browser-safe, no Node builtins).",
6
+ "scripts": {
7
+ "check": "tsc --noEmit -p .",
8
+ "test": "tsx --test tests/*.test.ts",
9
+ "net": "tsx scripts/net.ts"
10
+ },
11
+ "dependencies": {
12
+ "@sinclair/typebox": "0.34.52",
13
+ "yaml": "^2.8.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^24.0.0",
17
+ "tsx": "^4.20.0",
18
+ "typescript": "5.9.3"
19
+ }
20
+ }
@@ -0,0 +1,54 @@
1
+ # dpi-tui
2
+
3
+ A pi-like terminal client for one Durable Agent Network device. It talks straight to a device's `dpi-backend` over the [v1 API](../dpi-backend/API.md) with the device's bearer token, and renders the same transcript items as the web client (`AgentModel` + `toTranscript` from [`dpi-sdk`](../dpi-sdk)). Closing it never stops the agent.
4
+
5
+ ```bash
6
+ just tui attach mac-mini # pick a session on mac-mini (or create one in any directory)
7
+ just tui attach mac-mini:triage # session by name or id; its root agent
8
+ just tui attach mac-mini:triage:alt-path # a specific agent (id, "main" or name)
9
+ just tui new vm-1 review # in the device's first start dir; --cwd takes a path on the device
10
+ cd apps/dpi-tui && npm start -- --url https://my-vm.exe.xyz --token $DPI_TOKEN --save my-vm
11
+ ```
12
+
13
+ Device ids come from `~/.dpi/network.sqlite`, seeded from the repo's `dpi-network.yaml` (created from `dpi-network.example.yaml`). Tokens come from `just net register <id>`. `--url/--token` connects to any backend, and `--save <id>` stores it.
14
+
15
+ `-s/--session` and `-a/--agent` pick a session and agent. `--new <name>` creates a session, with `--cwd <dir>` on the device and `--model <id>`. `DPI_URL` and `DPI_TOKEN` stand in for `--url` and `--token`, and `DPI_DEVICE` names the default device. `--help` lists every option.
16
+
17
+ ## Keys
18
+
19
+ | Key | Action |
20
+ |---|---|
21
+ | Enter | send; while the agent works it steers (joins after the current tool round) |
22
+ | Alt+Enter | queue as a follow-up |
23
+ | Esc | interrupt foreground work (background jobs continue) |
24
+ | Shift+Tab | cycle thinking level |
25
+ | Ctrl+L | model picker |
26
+ | Ctrl+O | expand tool output and thinking |
27
+ | Ctrl+C | close an open picker or panel, else clear the input; on an empty input, exit and print the reattach command (session and agent by name when unique, else ids) |
28
+ | Ctrl+D | exit (on an empty input) |
29
+
30
+ Commands: `/sessions`, `/agents`, `/model`, `/devices`, `/new [name]`, `/fork [entryId] [name]`, `/thinking <level>`, `/compact [instructions]`, `/reset [note]`, `/name <name>`, `/tasks`, `/help`, `/quit`. Any other text starting with `/` goes to the agent.
31
+
32
+ A send that fails on the network retries with the same `requestId`, so the device admits it once. A failed send keeps the editor text.
33
+
34
+ ## Layout
35
+
36
+ ```
37
+ src/main.ts args, device resolution, --new
38
+ src/app.ts TuiAltScreen layout, agent + session streams (SDK follow), keys, submit
39
+ src/render.ts transcript items → components, cached by item id
40
+ src/pickers.ts sessions (+ new session with a remote directory walk), agents, models, devices
41
+ src/commands.ts slash commands
42
+ src/theme.ts colors and pi-tui themes
43
+ bin/dpi-tui.mjs runs src/main.ts through tsx, for npm link or npx dpi-tui
44
+ ```
45
+
46
+ ## Tests
47
+
48
+ ```bash
49
+ npm run check && npm test # types; every item kind fits widths 40/80/200 (CJK, emoji, long args)
50
+ tests/smoke.sh faux # tmux end to end against a faux-provider backend: streaming, steer, Esc, fork, reattach by name
51
+ tests/smoke.sh <device> # real model on a device: streaming, exit mid-tool, reattach
52
+ ```
53
+
54
+ From the repo root, `just tui check` and `just test smoke [faux|<id>]` run the same. The smoke script needs tmux, curl and openssl. The device run archives its session through the hub at `http://localhost:4781`, so start the hub first or archive the session yourself.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Run the TypeScript entry through tsx so `npm link` / `npx dpi-tui` work without a build step.
3
+ import { register } from "tsx/esm/api";
4
+ register();
5
+ await import("../src/main.ts");
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "dpi-tui",
3
+ "private": true,
4
+ "type": "module",
5
+ "bin": { "dpi-tui": "./bin/dpi-tui.mjs" },
6
+ "scripts": {
7
+ "start": "tsx src/main.ts",
8
+ "check": "tsc --noEmit -p .",
9
+ "test": "tsx --test tests/*.test.ts"
10
+ },
11
+ "dependencies": {
12
+ "@earendil-works/pi-tui": "1.3.0",
13
+ "tsx": "^4.20.0",
14
+ "yaml": "^2.8.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^24.0.0",
18
+ "typescript": "5.9.3"
19
+ }
20
+ }