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,111 @@
1
+ export type JsonValue =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | null
6
+ | JsonValue[]
7
+ | { [key: string]: JsonValue };
8
+ export type AppState = { [key: string]: JsonValue };
9
+ export type StateChange = {
10
+ operation: "create" | "update" | "delete" | "increment";
11
+ path: string;
12
+ value?: JsonValue;
13
+ };
14
+
15
+ export function statePath(parent: string, key: string) {
16
+ return `${parent}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`;
17
+ }
18
+
19
+ function parts(path: string) {
20
+ if (path === "") return [];
21
+ if (!path.startsWith("/")) throw Error("Use a JSON Pointer path, such as /counter");
22
+ return path
23
+ .slice(1)
24
+ .split("/")
25
+ .map((part) => {
26
+ if (/~(?![01])/u.test(part)) throw Error("Invalid JSON Pointer escape");
27
+ const key = part.replaceAll("~1", "/").replaceAll("~0", "~");
28
+ if (["__proto__", "constructor", "prototype"].includes(key))
29
+ throw Error("Reserved field name");
30
+ return key;
31
+ });
32
+ }
33
+
34
+ function child(value: JsonValue, key: string): JsonValue {
35
+ if (value === null || typeof value !== "object" || !Object.hasOwn(value, key))
36
+ throw Error(`Field does not exist: ${key}`);
37
+ if (Array.isArray(value) && !/^(0|[1-9]\d*)$/u.test(key)) throw Error("Invalid array index");
38
+ return (value as AppState)[key]!;
39
+ }
40
+
41
+ export function readState(state: AppState, path = ""): JsonValue {
42
+ return parts(path).reduce<JsonValue>(child, state);
43
+ }
44
+
45
+ function validate(value: unknown, depth = 0): asserts value is JsonValue {
46
+ if (depth > 24) throw Error("State is too deeply nested");
47
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
48
+ if (typeof value === "number" && Number.isFinite(value)) return;
49
+ if (typeof value !== "object" || value === null) throw Error("Value must be valid JSON");
50
+ for (const [key, item] of Object.entries(value)) {
51
+ if (["__proto__", "constructor", "prototype"].includes(key)) throw Error("Reserved field name");
52
+ validate(item, depth + 1);
53
+ }
54
+ }
55
+
56
+ export function changeState(state: AppState, input: StateChange): AppState {
57
+ if (!["create", "update", "delete", "increment"].includes(input.operation))
58
+ throw Error("Invalid state operation");
59
+ if (typeof input.path !== "string") throw Error("State path is required");
60
+ const path = parts(input.path);
61
+ const next: AppState = JSON.parse(JSON.stringify(state));
62
+ if (!path.length) {
63
+ if (input.operation === "delete") return {};
64
+ if (
65
+ input.operation !== "update" ||
66
+ input.value === null ||
67
+ Array.isArray(input.value) ||
68
+ typeof input.value !== "object"
69
+ )
70
+ throw Error("The state root must be a JSON object. Use update to replace it.");
71
+ validate(input.value);
72
+ if (JSON.stringify(input.value).length > 100000)
73
+ throw Error("App State must be under 100,000 characters");
74
+ return JSON.parse(JSON.stringify(input.value));
75
+ }
76
+ const key = path.pop()!;
77
+ const parent = path.reduce<JsonValue>(child, next);
78
+ if (parent === null || typeof parent !== "object")
79
+ throw Error("Parent must be an object or array");
80
+ const exists = Object.hasOwn(parent, key);
81
+ const array = Array.isArray(parent);
82
+ const append = array && (key === "-" || key === String(parent.length));
83
+ if (array && !append && (!/^(0|[1-9]\d*)$/u.test(key) || Number(key) >= parent.length))
84
+ throw Error("Invalid array index");
85
+ if (input.operation === "create" && exists) throw Error("Field already exists. Use update.");
86
+ if (input.operation !== "create" && !exists) throw Error("Field does not exist. Use create.");
87
+ if (input.operation === "delete") {
88
+ if (array) parent.splice(Number(key), 1);
89
+ else delete parent[key];
90
+ } else {
91
+ let value = input.value;
92
+ if (input.operation === "increment") {
93
+ const current = (parent as AppState)[key];
94
+ if (typeof current !== "number" || typeof value !== "number")
95
+ throw Error("Increment requires numbers");
96
+ value = current + value;
97
+ }
98
+ validate(value);
99
+ if (array) parent[append ? parent.length : Number(key)] = value;
100
+ else parent[key] = value;
101
+ }
102
+ validate(next);
103
+ if (JSON.stringify(next).length > 100000)
104
+ throw Error("App State must be under 100,000 characters");
105
+ return next;
106
+ }
107
+
108
+ export function replaceState(target: AppState, next: AppState) {
109
+ for (const key of Object.keys(target)) delete target[key];
110
+ Object.assign(target, next);
111
+ }
@@ -0,0 +1,330 @@
1
+ import type {
2
+ AgentConfig,
3
+ AgentDetail,
4
+ AgentExtensions,
5
+ AgentSummary,
6
+ CompactionSettings,
7
+ CreateAgentBody,
8
+ CreateSessionBody,
9
+ DeviceInfo,
10
+ DeviceTree,
11
+ DirListing,
12
+ ErrorCode,
13
+ ForkBody,
14
+ Frame,
15
+ Page,
16
+ SendResult,
17
+ SessionSummary,
18
+ TaskOutcome,
19
+ WhenBusy,
20
+ WireEntry,
21
+ } from "./api.js";
22
+ import { DpiError } from "./errors.js";
23
+ import { Backoff } from "./protocol/backoff.js";
24
+ import { RECONNECT_BACKOFF_S } from "./protocol/limits.js";
25
+ import { readSse } from "./sse.js";
26
+
27
+ export interface RequestOptions {
28
+ signal?: AbortSignal;
29
+ timeoutMs?: number;
30
+ idempotencyKey?: string;
31
+ }
32
+
33
+ const enc = encodeURIComponent;
34
+ const newId = (): string =>
35
+ globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
36
+
37
+ /**
38
+ * Typed client for one dpi-backend. Works in browsers and Node (fetch + streams).
39
+ * `token` is optional: the playground hub injects it, so browsers never hold one.
40
+ */
41
+ export class DpiClient {
42
+ readonly baseUrl: string;
43
+ constructor(
44
+ baseUrl: string,
45
+ private readonly token?: string,
46
+ ) {
47
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
48
+ }
49
+
50
+ private headers(json: boolean, extra: Record<string, string> = {}) {
51
+ return {
52
+ ...(this.token ? { authorization: `Bearer ${this.token}`, "x-dpi-token": this.token } : {}),
53
+ ...(json ? { "content-type": "application/json" } : {}),
54
+ ...extra,
55
+ };
56
+ }
57
+
58
+ async request<T>(
59
+ method: string,
60
+ path: string,
61
+ body?: unknown,
62
+ options: RequestOptions = {},
63
+ ): Promise<T> {
64
+ const timeout = AbortSignal.timeout(options.timeoutMs ?? 20_000);
65
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
66
+ let response: Response;
67
+ try {
68
+ response = await fetch(this.baseUrl + path, {
69
+ method,
70
+ signal,
71
+ headers: this.headers(
72
+ body !== undefined,
73
+ options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {},
74
+ ),
75
+ body: body === undefined ? undefined : JSON.stringify(body),
76
+ });
77
+ } catch (cause) {
78
+ if (options.signal?.aborted) throw cause;
79
+ throw new DpiError("offline", `Cannot reach ${this.baseUrl}`, 0, { cause });
80
+ }
81
+ const text = await response.text();
82
+ let data: unknown;
83
+ try {
84
+ data = text ? JSON.parse(text) : undefined;
85
+ } catch {
86
+ data = undefined;
87
+ }
88
+ if (!response.ok) {
89
+ const error = (data as { error?: { code?: ErrorCode; message?: string } } | undefined)?.error;
90
+ const code: ErrorCode =
91
+ error?.code ??
92
+ (response.status === 401 ? "unauthorized" : response.status === 404 ? "not_found" : "internal");
93
+ throw new DpiError(code, error?.message ?? `${response.status} ${response.statusText}`, response.status);
94
+ }
95
+ return data as T;
96
+ }
97
+
98
+ /** Open one SSE stream and yield its frames until it ends. Throws `DpiError` on failure. */
99
+ async *stream(path: string, signal: AbortSignal, idleMs?: number): AsyncGenerator<Frame> {
100
+ let response: Response;
101
+ try {
102
+ response = await fetch(this.baseUrl + path, {
103
+ headers: { ...this.headers(false), accept: "text/event-stream" },
104
+ signal,
105
+ });
106
+ } catch (cause) {
107
+ if (signal.aborted) return;
108
+ throw new DpiError("offline", `Cannot reach ${this.baseUrl}`, 0, { cause });
109
+ }
110
+ if (!response.ok || !response.body) {
111
+ const body = (await response.json().catch(() => undefined)) as
112
+ | { error?: { code?: ErrorCode; message?: string } }
113
+ | undefined;
114
+ const code: ErrorCode =
115
+ body?.error?.code ?? (response.status === 401 ? "unauthorized" : "unavailable");
116
+ throw new DpiError(code, body?.error?.message ?? `Stream failed (${response.status})`, response.status);
117
+ }
118
+ try {
119
+ for await (const message of readSse(response.body, idleMs)) yield JSON.parse(message.data) as Frame;
120
+ } catch (error) {
121
+ if (signal.aborted) return;
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ // ------------------------------------------------------------ device
127
+ device(options?: RequestOptions) {
128
+ return this.request<DeviceInfo>("GET", "/v1/device", undefined, options);
129
+ }
130
+ tree() {
131
+ return this.request<DeviceTree>("GET", "/v1/tree");
132
+ }
133
+ dirs(path = "") {
134
+ return this.request<DirListing>("GET", `/v1/fs/dirs?path=${enc(path)}`);
135
+ }
136
+ deviceDoc<T = unknown>(kind: string) {
137
+ return this.request<{ kind: string; value: T; revision?: number }>("GET", `/v1/docs/${enc(kind)}`);
138
+ }
139
+ deviceEvents(signal: AbortSignal, docs: string[] = []) {
140
+ return this.stream(`/v1/events${docs.length ? `?docs=${docs.map(enc).join(",")}` : ""}`, signal);
141
+ }
142
+
143
+ // ------------------------------------------------------------ sessions
144
+ sessions() {
145
+ return this.request<SessionSummary[]>("GET", "/v1/sessions");
146
+ }
147
+ createSession(body: CreateSessionBody, idempotencyKey = newId()) {
148
+ return this.request<SessionSummary>("POST", "/v1/sessions", body, { idempotencyKey });
149
+ }
150
+ session(ref: string) {
151
+ return new SessionHandle(this, ref);
152
+ }
153
+ agent(session: string, agent: string | number) {
154
+ return new AgentHandle(this, session, String(agent));
155
+ }
156
+ }
157
+
158
+ export class SessionHandle {
159
+ constructor(
160
+ readonly client: DpiClient,
161
+ readonly ref: string,
162
+ ) {}
163
+ get base() {
164
+ return `/v1/sessions/${enc(this.ref)}`;
165
+ }
166
+ get() {
167
+ return this.client.request<SessionSummary>("GET", this.base);
168
+ }
169
+ rename(name: string) {
170
+ return this.client.request<SessionSummary>("PATCH", this.base, { name });
171
+ }
172
+ archive() {
173
+ return this.client.request<{ archived: string }>("DELETE", this.base);
174
+ }
175
+ agents() {
176
+ return this.client.request<AgentSummary[]>("GET", `${this.base}/agents`);
177
+ }
178
+ createAgent(body: CreateAgentBody = {}, idempotencyKey = newId()) {
179
+ return this.client.request<AgentSummary>("POST", `${this.base}/agents`, body, { idempotencyKey });
180
+ }
181
+ tasks(status: "live" | "terminal" = "terminal", limit = 20) {
182
+ return this.client.request<TaskOutcome[]>("GET", `${this.base}/tasks?status=${status}&limit=${limit}`);
183
+ }
184
+ abortTask(taskId: number) {
185
+ return this.client.request<{ status: string }>("POST", `${this.base}/tasks/${taskId}/abort`, {});
186
+ }
187
+ inspect() {
188
+ return this.client.request<unknown>("GET", `${this.base}/inspect`);
189
+ }
190
+ compaction() {
191
+ return this.client.request<CompactionSettings>("GET", `${this.base}/compaction`);
192
+ }
193
+ /** Change the session's automatic compaction thresholds; they apply from the next request. */
194
+ setCompaction(patch: Partial<CompactionSettings>) {
195
+ return this.client.request<CompactionSettings>("PATCH", `${this.base}/compaction`, patch);
196
+ }
197
+ submission(id: number, waitMs = 0) {
198
+ return this.client.request<SendResult>("GET", `${this.base}/submissions/${id}?waitMs=${waitMs}`, undefined, {
199
+ timeoutMs: waitMs + 20_000,
200
+ });
201
+ }
202
+ events(signal: AbortSignal) {
203
+ return this.client.stream(`${this.base}/events`, signal);
204
+ }
205
+ }
206
+
207
+ export class AgentHandle {
208
+ constructor(
209
+ readonly client: DpiClient,
210
+ readonly session: string,
211
+ readonly agent: string,
212
+ ) {}
213
+ get base() {
214
+ return `/v1/sessions/${enc(this.session)}/agents/${enc(this.agent)}`;
215
+ }
216
+ get() {
217
+ return this.client.request<AgentDetail>("GET", this.base);
218
+ }
219
+ rename(name: string) {
220
+ return this.client.request<AgentSummary>("PATCH", this.base, { name });
221
+ }
222
+ send(content: string, whenBusy: WhenBusy = "followUp", requestId = newId(), waitMs?: number) {
223
+ return this.client.request<SendResult>(
224
+ "POST",
225
+ `${this.base}/messages`,
226
+ { content, whenBusy, requestId, waitMs },
227
+ { timeoutMs: (waitMs ?? 0) + 20_000 },
228
+ );
229
+ }
230
+ withdraw(submissionId: number) {
231
+ return this.client.request<{ status: string }>("DELETE", `${this.base}/inbox/${submissionId}`);
232
+ }
233
+ abort(background = false) {
234
+ return this.client.request<{ ok: true }>("POST", `${this.base}/abort`, { background });
235
+ }
236
+ fork(body: ForkBody, idempotencyKey = newId()) {
237
+ return this.client.request<AgentSummary>("POST", `${this.base}/fork`, body, { idempotencyKey });
238
+ }
239
+ config() {
240
+ return this.client.request<AgentConfig>("GET", `${this.base}/config`);
241
+ }
242
+ configure(patch: Partial<AgentConfig>) {
243
+ return this.client.request<AgentConfig>("PATCH", `${this.base}/config`, patch);
244
+ }
245
+ compact(instructions?: string) {
246
+ return this.client.request<{ taskId: number }>("POST", `${this.base}/compact`, { instructions });
247
+ }
248
+ reset(note?: string) {
249
+ return this.client.request<{ ok: true }>("POST", `${this.base}/reset`, { note });
250
+ }
251
+ entries(options: { scope?: "active" | "all"; cursor?: string; limit?: number } = {}) {
252
+ const q = new URLSearchParams({
253
+ scope: options.scope ?? "all",
254
+ limit: String(options.limit ?? 500),
255
+ ...(options.cursor ? { cursor: options.cursor } : {}),
256
+ });
257
+ return this.client.request<Page<WireEntry>>("GET", `${this.base}/entries?${q}`);
258
+ }
259
+ transcript(format: "json" | "markdown" = "json") {
260
+ return this.client.request<{ format: string; text?: string; items?: unknown[] }>(
261
+ "GET",
262
+ `${this.base}/transcript?format=${format}`,
263
+ );
264
+ }
265
+ doc<T = unknown>(kind: string) {
266
+ return this.client.request<{ kind: string; value: T }>("GET", `${this.base}/docs/${enc(kind)}`);
267
+ }
268
+ extensions() {
269
+ return this.client.request<AgentExtensions>("GET", `${this.base}/extensions`);
270
+ }
271
+ /** Turn a custom extension on or off, or check or uncheck one of system-prompts' prompts. */
272
+ setExtension(patch: { name: string; enabled: boolean } | { prompt: string; enabled: boolean }) {
273
+ return this.client.request<AgentExtensions>("PATCH", `${this.base}/extensions`, patch);
274
+ }
275
+ demo<T = unknown>(path: string, body: unknown, method = "POST") {
276
+ return this.client.request<T>(method, `${this.base}/demo/${path}`, body);
277
+ }
278
+ events(signal: AbortSignal, docs: string[] = []) {
279
+ return this.client.stream(
280
+ `${this.base}/events${docs.length ? `?docs=${docs.map(enc).join(",")}` : ""}`,
281
+ signal,
282
+ );
283
+ }
284
+ }
285
+
286
+ export type ConnectionState = "connecting" | "live" | "offline" | "unauthorized" | "gone";
287
+
288
+ const sleep = (ms: number, signal: AbortSignal) =>
289
+ new Promise<void>((resolve) => {
290
+ if (signal.aborted) return resolve();
291
+ const timer = setTimeout(resolve, ms);
292
+ signal.addEventListener("abort", () => (clearTimeout(timer), resolve()), { once: true });
293
+ });
294
+
295
+ /**
296
+ * Keep one stream open: reconnect with backoff, and start every reconnect from the server's fresh
297
+ * snapshot (streams never replay). Stops for good on `unauthorized` or `not_found`.
298
+ */
299
+ export async function follow(
300
+ open: (signal: AbortSignal) => AsyncIterable<Frame>,
301
+ onFrame: (frame: Frame) => void,
302
+ onState: (state: ConnectionState, error?: DpiError) => void,
303
+ signal: AbortSignal,
304
+ ) {
305
+ const backoff = new Backoff(RECONNECT_BACKOFF_S);
306
+ while (!signal.aborted) {
307
+ onState("connecting");
308
+ try {
309
+ for await (const frame of open(signal)) {
310
+ if (frame.type === "hello") {
311
+ backoff.reset();
312
+ onState("live");
313
+ }
314
+ onFrame(frame);
315
+ }
316
+ if (signal.aborted) return;
317
+ onFrame({ type: "disconnected", reason: "Stream ended" });
318
+ } catch (error) {
319
+ if (signal.aborted) return;
320
+ const e =
321
+ error instanceof DpiError ? error : new DpiError("offline", String((error as Error)?.message ?? error));
322
+ onFrame({ type: "disconnected", reason: e.message });
323
+ if (e.code === "unauthorized") return onState("unauthorized", e);
324
+ if (e.code === "not_found") return onState("gone", e);
325
+ onState("offline", e);
326
+ }
327
+ await sleep(backoff.ms, signal);
328
+ backoff.advance();
329
+ }
330
+ }
@@ -0,0 +1,16 @@
1
+ import type { ErrorCode } from "./api.js";
2
+
3
+ /** Every client-side failure: `offline` when the device can't be reached, otherwise the server's error code. */
4
+ export class DpiError extends Error {
5
+ constructor(
6
+ readonly code: ErrorCode,
7
+ message: string,
8
+ readonly status = 0,
9
+ options?: { cause?: unknown },
10
+ ) {
11
+ super(message, options);
12
+ this.name = "DpiError";
13
+ }
14
+ }
15
+
16
+ export const isDpiError = (e: unknown): e is DpiError => e instanceof DpiError;
@@ -0,0 +1,99 @@
1
+ // Durable jobs: repeatable workflows. Each run is one pi-durable task whose checkpoint phases are the job's named
2
+ // phases. The run's record lives in the conversation document `demo.job-runs`, so every client renders the same
3
+ // phases, steps and approval, live and after a restart. Types are aliases (not interfaces) so they stay JSON documents.
4
+
5
+ export const JOB_RUNS_DOC = "demo.job-runs";
6
+
7
+ export type JobInputValue = string | number | boolean;
8
+
9
+ export type JobInputSpec = {
10
+ name: string;
11
+ type: "string" | "integer";
12
+ description: string;
13
+ default: JobInputValue;
14
+ min?: number;
15
+ max?: number;
16
+ };
17
+
18
+ export type JobPhaseSpec = {
19
+ id: string;
20
+ label: string;
21
+ summary: string;
22
+ /** `approval` phases wait for a person's decision; `repeat` phases save one checkpoint per iteration. */
23
+ kind?: "approval" | "repeat";
24
+ };
25
+
26
+ export type JobDef = {
27
+ id: string;
28
+ title: string;
29
+ summary: string;
30
+ phases: JobPhaseSpec[];
31
+ inputs: JobInputSpec[];
32
+ /** The run pauses in an approval phase until a person approves or declines. */
33
+ approval: boolean;
34
+ /** App State path where every run saves its result. */
35
+ resultPath: string;
36
+ /** Typical run time in seconds, without the approval wait. */
37
+ seconds: number;
38
+ };
39
+
40
+ export type JobStepKind = "command" | "tool" | "agent" | "state" | "approval" | "checkpoint" | "note";
41
+ export type JobStepStatus = "running" | "done" | "error" | "waiting";
42
+ export type JobPhaseStatus = "pending" | "running" | "waiting" | "done" | "failed" | "aborted" | "skipped";
43
+ export type JobRunStatus = "running" | "waiting" | "completed" | "failed" | "aborted" | "declined";
44
+
45
+ export type JobStep = {
46
+ id: string;
47
+ kind: JobStepKind;
48
+ label: string;
49
+ detail?: string;
50
+ status: JobStepStatus;
51
+ /** The phase iteration that recorded the step; a resumed iteration replaces its own steps. */
52
+ iteration: number;
53
+ at: number;
54
+ ms?: number;
55
+ /** For subagent steps: the child conversation that made the call. */
56
+ conversationId?: number;
57
+ };
58
+
59
+ export type JobPhaseRun = {
60
+ id: string;
61
+ label: string;
62
+ status: JobPhaseStatus;
63
+ startedAt?: number;
64
+ endedAt?: number;
65
+ summary?: string;
66
+ steps: JobStep[];
67
+ progress?: { done: number; total: number };
68
+ };
69
+
70
+ export type JobRun = {
71
+ /** The job's durable task id. */
72
+ id: number;
73
+ job: string;
74
+ title: string;
75
+ /** The nth run of this job in this conversation. */
76
+ run: number;
77
+ input: Record<string, JobInputValue>;
78
+ startedBy: "agent" | "you";
79
+ /** Set when the agent started the run: the run_durable_job call that owns it. */
80
+ toolCallId?: string;
81
+ background: boolean;
82
+ status: JobRunStatus;
83
+ phase?: string;
84
+ phases: JobPhaseRun[];
85
+ approval?: "pending" | "approved" | "declined" | "cancelled";
86
+ childConversationId?: number;
87
+ /** How many times the run continued from a checkpoint after a restart. */
88
+ resumed: number;
89
+ result?: string;
90
+ error?: string;
91
+ startedAt: number;
92
+ updatedAt: number;
93
+ endedAt?: number;
94
+ };
95
+
96
+ export type JobRunsDoc = { counts: Record<string, number>; items: Record<string, JobRun> };
97
+
98
+ export const jobRunDone = (run: Pick<JobRun, "status">) =>
99
+ run.status !== "running" && run.status !== "waiting";