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,472 @@
1
+ // Client-side reducers. Each consumes stream frames and exposes plain state plus change
2
+ // notifications, so Vue, the TUI or any other renderer can sit on top without knowing the wire.
3
+ import type {
4
+ AgentSummary,
5
+ DeviceStatus,
6
+ Frame,
7
+ InboxItem,
8
+ NetworkDevice,
9
+ SessionSummary,
10
+ TaskNode,
11
+ UsageTotals,
12
+ WireAgentEvent,
13
+ WireAgentState,
14
+ WireAssistantBlock,
15
+ WireAssistantMessage,
16
+ WireCompactionFailure,
17
+ WireEntry,
18
+ WireMessageChange,
19
+ WireNestedToolResult,
20
+ WireSnapshotEvent,
21
+ WireUsageState,
22
+ } from "./api.js";
23
+ import { toTranscript, type LiveTool, type TranscriptItem } from "./transcript.js";
24
+
25
+ type Listener = () => void;
26
+
27
+ class Emitter {
28
+ private listeners = new Set<Listener>();
29
+ subscribe(fn: Listener) {
30
+ this.listeners.add(fn);
31
+ return () => void this.listeners.delete(fn);
32
+ }
33
+ protected emit() {
34
+ for (const fn of this.listeners) fn();
35
+ }
36
+ }
37
+
38
+ // ---------------------------------------------------------------- agent
39
+
40
+ export interface AgentViewState {
41
+ /** Normalized transcript of the active context plus live generation and tools. */
42
+ items: TranscriptItem[];
43
+ /** Committed entries of the active context, ordered by id. */
44
+ entries: WireEntry[];
45
+ /** True between run_start and run_end (snapshot: `run` present). */
46
+ running: boolean;
47
+ generating: boolean;
48
+ inbox: InboxItem[];
49
+ agent: WireAgentState;
50
+ usage: WireUsageState;
51
+ retry?: { at: number; error: string };
52
+ compactions: number[];
53
+ /** The newest compaction failure (`pi.live.compactionFailure`); cleared by a compaction that produces a summary. */
54
+ compactionFailure?: WireCompactionFailure;
55
+ notices: string[];
56
+ docs: Record<string, unknown>;
57
+ /** Set once the first snapshot arrives. */
58
+ ready: boolean;
59
+ }
60
+
61
+ const emptyAgentState = (): AgentViewState => ({
62
+ items: [],
63
+ entries: [],
64
+ running: false,
65
+ generating: false,
66
+ inbox: [],
67
+ agent: {},
68
+ usage: { models: {}, tools: {} },
69
+ compactions: [],
70
+ notices: [],
71
+ docs: {},
72
+ ready: false,
73
+ });
74
+
75
+ function setPath(target: Record<string | number, unknown>, path: readonly (string | number)[], delta: string) {
76
+ let node = target;
77
+ for (let i = 0; i < path.length - 1; i++) {
78
+ const key = path[i];
79
+ if (typeof node[key] !== "object" || node[key] === null)
80
+ node[key] = typeof path[i + 1] === "number" ? [] : {};
81
+ node = node[key] as Record<string | number, unknown>;
82
+ }
83
+ const last = path[path.length - 1];
84
+ node[last] = String(node[last] ?? "") + delta;
85
+ }
86
+
87
+ export function applyMessageChange(message: WireAssistantMessage, change: WireMessageChange): WireAssistantMessage {
88
+ if (change.type === "message") return structuredClone(change.message);
89
+ const content = [...message.content] as WireAssistantBlock[];
90
+ const i = change.contentIndex;
91
+ if (change.type === "block" || change.type.endsWith("_start")) {
92
+ content[i] = structuredClone((change as { block: WireAssistantBlock }).block);
93
+ } else if (change.type === "text_delta") {
94
+ const block = content[i];
95
+ content[i] = { type: "text", text: (block?.type === "text" ? block.text : "") + change.delta };
96
+ } else if (change.type === "thinking_delta") {
97
+ const block = content[i];
98
+ content[i] = {
99
+ type: "thinking",
100
+ thinking: (block?.type === "thinking" ? block.thinking : "") + change.delta,
101
+ };
102
+ } else if (change.type === "toolcall_delta") {
103
+ const block = structuredClone(content[i] ?? { type: "toolCall", id: "", name: "", arguments: {} });
104
+ setPath(block as unknown as Record<string, unknown>, change.path, change.delta);
105
+ content[i] = block;
106
+ }
107
+ return { ...message, content };
108
+ }
109
+
110
+ export class AgentModel extends Emitter {
111
+ state: AgentViewState = emptyAgentState();
112
+ private entries = new Map<number, WireEntry>();
113
+ private live?: WireAssistantMessage;
114
+ private tools = new Map<string, LiveTool>();
115
+
116
+ /** Apply one stream frame; returns true when state changed. */
117
+ apply(frame: Frame): boolean {
118
+ switch (frame.type) {
119
+ case "snapshot":
120
+ this.reset(frame.snapshot);
121
+ this.state.inbox = frame.inbox;
122
+ break;
123
+ case "batch":
124
+ for (const event of frame.events) this.event(event);
125
+ break;
126
+ case "inbox":
127
+ this.state.inbox = frame.items;
128
+ break;
129
+ case "doc":
130
+ this.state.docs = { ...this.state.docs, [frame.kind]: frame.value };
131
+ break;
132
+ default:
133
+ return false;
134
+ }
135
+ this.rebuild();
136
+ this.emit();
137
+ return true;
138
+ }
139
+
140
+ /** Re-render without new data (for example after a UI toggle). */
141
+ touch() {
142
+ this.emit();
143
+ }
144
+
145
+ clear() {
146
+ this.entries.clear();
147
+ this.tools.clear();
148
+ this.live = undefined;
149
+ this.state = emptyAgentState();
150
+ this.emit();
151
+ }
152
+
153
+ private rebuild() {
154
+ const entries = [...this.entries.values()].sort((a, b) => a.id - b.id);
155
+ this.state.entries = entries;
156
+ this.state.generating = !!this.live;
157
+ this.state.items = toTranscript(entries, this.live, this.tools);
158
+ }
159
+
160
+ private reset(snapshot: WireSnapshotEvent) {
161
+ this.entries = new Map(snapshot.entries.map((entry) => [entry.id, entry]));
162
+ this.live = snapshot.generation?.message ? structuredClone(snapshot.generation.message) : undefined;
163
+ this.tools = new Map(
164
+ snapshot.tools.map((slot) => [
165
+ slot.callId,
166
+ { name: slot.name, status: slot.status, output: slot.output ?? "" },
167
+ ]),
168
+ );
169
+ // Nested calls of running tools: they have no assistant entry, so the snapshot is the only source on attach.
170
+ for (const slot of snapshot.nestedTools) {
171
+ const existing = this.tools.get(slot.callId);
172
+ this.tools.set(slot.callId, {
173
+ name: slot.name,
174
+ status: slot.status,
175
+ output: slot.output ?? "",
176
+ args: slot.arguments,
177
+ nested: { parentCallId: slot.parentCallId, parentTaskId: slot.parentTaskId, taskId: slot.taskId },
178
+ ...(slot.summary ? { summary: slot.summary } : {}),
179
+ ...(existing ?? {}),
180
+ });
181
+ }
182
+ this.state.running = !!snapshot.run;
183
+ this.state.agent = snapshot.agent;
184
+ this.state.usage = snapshot.usage;
185
+ this.state.retry = snapshot.generation?.retry;
186
+ this.state.compactions = snapshot.compactions.map((c) => c.taskId);
187
+ this.state.compactionFailure = snapshot.compactionFailure;
188
+ this.state.ready = true;
189
+ }
190
+
191
+ /** Store a committed entry. Resets (sent as message_end) and compactions start a new context head. */
192
+ private commit(entry: WireEntry) {
193
+ this.entries.set(entry.id, entry);
194
+ if (entry.kind === "pi.reset" || entry.kind === "pi.compaction") {
195
+ const head = entry.head ?? entry.id;
196
+ for (const id of this.entries.keys()) if (id < head) this.entries.delete(id);
197
+ }
198
+ }
199
+
200
+ private event(event: WireAgentEvent) {
201
+ switch (event.type) {
202
+ case "snapshot":
203
+ return this.reset(event);
204
+ case "run_start":
205
+ this.state.running = true;
206
+ return;
207
+ case "run_end":
208
+ this.state.running = false;
209
+ this.live = undefined;
210
+ return;
211
+ case "message_start":
212
+ if (event.message.role === "assistant")
213
+ this.live = structuredClone(event.message as WireAssistantMessage);
214
+ return;
215
+ case "message_update":
216
+ if (!this.live)
217
+ this.live = {
218
+ role: "assistant",
219
+ content: [],
220
+ provider: "",
221
+ model: "",
222
+ usage: event.usage,
223
+ stopReason: "pending",
224
+ };
225
+ for (const change of event.changes) this.live = applyMessageChange(this.live, change);
226
+ this.live = { ...this.live, usage: event.usage };
227
+ return;
228
+ case "message_end":
229
+ this.live = undefined;
230
+ return this.commit(event.entry);
231
+ case "entry_appended":
232
+ return this.commit(event.entry);
233
+ case "tool_execution_start":
234
+ this.tools.set(event.toolCallId, {
235
+ name: event.toolName,
236
+ status: "running",
237
+ output: "",
238
+ args: event.args,
239
+ // A nested call names the call that made it; a model-issued call has no parent.
240
+ ...(event.parentToolCallId !== undefined && event.parentTaskId !== undefined && event.taskId !== undefined
241
+ ? { nested: { parentCallId: event.parentToolCallId, parentTaskId: event.parentTaskId, taskId: event.taskId } }
242
+ : {}),
243
+ });
244
+ return;
245
+ case "tool_execution_update": {
246
+ const tool = this.tools.get(event.toolCallId) ?? {
247
+ name: event.toolName,
248
+ status: "running" as const,
249
+ output: "",
250
+ };
251
+ const output = event.output;
252
+ if (output && "set" in output) tool.output = output.set;
253
+ else if (output) tool.output = tool.output.slice(output.trimStart ?? 0) + (output.append ?? "");
254
+ tool.status = "running";
255
+ this.tools.set(event.toolCallId, tool);
256
+ return;
257
+ }
258
+ case "tool_execution_end":
259
+ if (event.entry) {
260
+ // A model-issued call renders from its result entry; drop the live slot so it is never duplicated.
261
+ this.tools.delete(event.toolCallId);
262
+ this.entries.set(event.entry.id, event.entry);
263
+ return;
264
+ }
265
+ if (event.result) {
266
+ // A nested call has no entry: keep the slot so its result still renders, exactly once.
267
+ const tool = this.tools.get(event.toolCallId) ?? { name: event.toolName, status: "done" as const, output: "" };
268
+ tool.status = "done";
269
+ tool.result = { text: nestedResultText(event.result), isError: event.result.isError };
270
+ tool.summary = { isError: event.result.isError, durationMs: event.result.durationMs };
271
+ this.tools.set(event.toolCallId, tool);
272
+ return;
273
+ }
274
+ // Faulted, orphaned, or an unfinished slot: no entry and no result. Drop it without inventing a result.
275
+ this.tools.delete(event.toolCallId);
276
+ return;
277
+ case "agent_changed":
278
+ this.state.agent = event.agent;
279
+ return;
280
+ case "usage_changed":
281
+ this.state.usage = event.usage;
282
+ return;
283
+ case "auto_retry_start":
284
+ this.state.retry = { at: event.at, error: event.errorMessage };
285
+ return;
286
+ case "auto_retry_end":
287
+ this.state.retry = undefined;
288
+ return;
289
+ case "compaction_start":
290
+ if (!this.state.compactions.includes(event.taskId)) this.state.compactions.push(event.taskId);
291
+ return;
292
+ case "compaction_end":
293
+ this.state.compactions = this.state.compactions.filter((id) => id !== event.taskId);
294
+ // `error` marks a failure, fault or orphaning; its absence means a summary was produced.
295
+ this.state.compactionFailure = event.error
296
+ ? { taskId: event.taskId, reason: event.reason, error: event.error }
297
+ : undefined;
298
+ return;
299
+ case "task_failed":
300
+ this.state.notices = [...this.state.notices.slice(-9), `${event.kind} failed: ${event.message}`];
301
+ return;
302
+ default:
303
+ return;
304
+ }
305
+ }
306
+ }
307
+
308
+ // ---------------------------------------------------------------- session
309
+
310
+ export interface SessionViewState {
311
+ agents: AgentSummary[];
312
+ tasks: TaskNode[];
313
+ usage: UsageTotals;
314
+ ready: boolean;
315
+ }
316
+
317
+ export function agentLabel(agent: Pick<AgentSummary, "id" | "name">) {
318
+ return agent.name;
319
+ }
320
+
321
+ export class SessionModel extends Emitter {
322
+ state: SessionViewState = { agents: [], tasks: [], usage: { models: {}, tools: {} }, ready: false };
323
+
324
+ apply(frame: Frame): boolean {
325
+ if (frame.type === "session") {
326
+ this.state = { agents: frame.agents, tasks: frame.tasks, usage: frame.usage, ready: true };
327
+ } else if (frame.type === "agents") this.state = { ...this.state, agents: frame.agents };
328
+ else if (frame.type === "tasks") this.state = { ...this.state, tasks: frame.tasks };
329
+ else if (frame.type === "usage") this.state = { ...this.state, usage: frame.usage };
330
+ else return false;
331
+ this.emit();
332
+ return true;
333
+ }
334
+
335
+ clear() {
336
+ this.state = { agents: [], tasks: [], usage: { models: {}, tools: {} }, ready: false };
337
+ this.emit();
338
+ }
339
+
340
+ agent(id: number) {
341
+ return this.state.agents.find((a) => a.id === id);
342
+ }
343
+
344
+ /** Root → … → id, following fork parents. */
345
+ lineage(id: number): AgentSummary[] {
346
+ return lineage(this.state.agents, id);
347
+ }
348
+
349
+ foregroundBusy(agentId: number) {
350
+ return this.state.tasks.some((t) => !t.background && t.conversationId === agentId);
351
+ }
352
+
353
+ backgroundTasks(agentId?: number) {
354
+ return this.state.tasks.filter((t) => t.background && (agentId === undefined || t.conversationId === agentId));
355
+ }
356
+ }
357
+
358
+ export function lineage(agents: readonly AgentSummary[], id: number): AgentSummary[] {
359
+ const byId = new Map(agents.map((a) => [a.id, a]));
360
+ const out: AgentSummary[] = [];
361
+ const seen = new Set<number>();
362
+ let current: number | undefined = id;
363
+ while (current !== undefined && !seen.has(current)) {
364
+ seen.add(current);
365
+ const agent = byId.get(current);
366
+ if (!agent) break;
367
+ out.unshift(agent);
368
+ current = agent.parentId;
369
+ }
370
+ return out;
371
+ }
372
+
373
+ // ---------------------------------------------------------------- network tree
374
+
375
+ export type TreeNode =
376
+ | {
377
+ type: "device";
378
+ key: string;
379
+ device: NetworkDevice;
380
+ status: DeviceStatus;
381
+ stale: boolean;
382
+ children: TreeNode[];
383
+ }
384
+ | {
385
+ type: "session";
386
+ key: string;
387
+ deviceId: string;
388
+ session: SessionSummary;
389
+ stale: boolean;
390
+ children: TreeNode[];
391
+ }
392
+ | {
393
+ type: "agent";
394
+ key: string;
395
+ deviceId: string;
396
+ sessionId: string;
397
+ agent: AgentSummary;
398
+ stale: boolean;
399
+ children: TreeNode[];
400
+ };
401
+
402
+ export const deviceKey = (deviceId: string) => deviceId;
403
+ export const sessionKey = (deviceId: string, sessionId: string) => `${deviceId}/s/${sessionId}`;
404
+ export const agentKey = (deviceId: string, sessionId: string, agentId: number) =>
405
+ `${deviceId}/s/${sessionId}/a/${agentId}`;
406
+
407
+ /** Nest agents under the agent they forked from or the agent whose task owns them. */
408
+ export function agentTree(deviceId: string, session: SessionSummary, stale: boolean): TreeNode[] {
409
+ const nodes = new Map<number, Extract<TreeNode, { type: "agent" }>>();
410
+ for (const agent of session.agents)
411
+ nodes.set(agent.id, {
412
+ type: "agent",
413
+ key: agentKey(deviceId, session.id, agent.id),
414
+ deviceId,
415
+ sessionId: session.id,
416
+ agent,
417
+ stale,
418
+ children: [],
419
+ });
420
+ const roots: TreeNode[] = [];
421
+ for (const node of nodes.values()) {
422
+ const parent = nodes.get(node.agent.parentId ?? node.agent.ownerAgentId ?? -1);
423
+ if (parent && parent !== node) parent.children.push(node);
424
+ else roots.push(node);
425
+ }
426
+ return roots;
427
+ }
428
+
429
+ export function networkTree(devices: readonly NetworkDevice[]): TreeNode[] {
430
+ return devices.map((device) => {
431
+ const stale = device.status !== "online";
432
+ return {
433
+ type: "device",
434
+ key: deviceKey(device.id),
435
+ device,
436
+ status: device.status,
437
+ stale,
438
+ children: (device.tree?.sessions ?? []).map((session) => ({
439
+ type: "session" as const,
440
+ key: sessionKey(device.id, session.id),
441
+ deviceId: device.id,
442
+ session,
443
+ stale,
444
+ children: agentTree(device.id, session, stale),
445
+ })),
446
+ };
447
+ });
448
+ }
449
+
450
+ export class NetworkModel extends Emitter {
451
+ devices: NetworkDevice[] = [];
452
+ apply(frame: Frame): boolean {
453
+ if (frame.type !== "network") return false;
454
+ this.devices = frame.devices;
455
+ this.emit();
456
+ return true;
457
+ }
458
+ device(id: string) {
459
+ return this.devices.find((d) => d.id === id);
460
+ }
461
+ tree() {
462
+ return networkTree(this.devices);
463
+ }
464
+ }
465
+
466
+ /** Display text for a nested call's returned value: its structured output, or nothing when absent. */
467
+ function nestedResultText(result: WireNestedToolResult): string {
468
+ const value = result.structuredOutput;
469
+ if (value === undefined || value === null) return "";
470
+ const text = typeof value === "string" ? value : JSON.stringify(value);
471
+ return text.length > 4000 ? `${text.slice(0, 4000)}…` : text;
472
+ }