super-dsh 0.1.0 → 0.1.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 (178) hide show
  1. package/agent-agy/bridge/agy_bridge.py +250 -0
  2. package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
  3. package/agent-agy/cordis.patch.yml +49 -0
  4. package/agent-agy/dist/adapter.d.ts +15 -0
  5. package/agent-agy/dist/adapter.js +42 -0
  6. package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
  7. package/agent-agy/dist/agent-preset-agy.js +169 -0
  8. package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
  9. package/agent-agy/dist/agent-preset-projection.js +33 -0
  10. package/agent-agy/dist/agent.d.ts +137 -0
  11. package/agent-agy/dist/agent.js +493 -0
  12. package/agent-agy/dist/agy-cli-client.d.ts +137 -0
  13. package/agent-agy/dist/agy-cli-client.js +301 -0
  14. package/agent-agy/dist/agy-client.d.ts +160 -0
  15. package/agent-agy/dist/agy-client.js +252 -0
  16. package/agent-agy/dist/agy-sessions.d.ts +24 -0
  17. package/agent-agy/dist/agy-sessions.js +116 -0
  18. package/agent-agy/dist/agy-store.d.ts +39 -0
  19. package/agent-agy/dist/agy-store.js +83 -0
  20. package/agent-agy/dist/inbox.d.ts +26 -0
  21. package/agent-agy/dist/inbox.js +53 -0
  22. package/agent-agy/dist/index.d.ts +80 -0
  23. package/agent-agy/dist/index.js +505 -0
  24. package/agent-agy/dist/models.d.ts +41 -0
  25. package/agent-agy/dist/models.js +74 -0
  26. package/agent-agy/dist/world-plugin.d.ts +9 -0
  27. package/agent-agy/dist/world-plugin.js +97 -0
  28. package/agent-agy/package.json +64 -0
  29. package/agent-claude/cordis.patch.yml +42 -0
  30. package/agent-claude/dist/adapter.js +62 -0
  31. package/agent-claude/dist/agent-preset-claude.js +169 -0
  32. package/agent-claude/dist/agent-preset-projection.js +30 -0
  33. package/agent-claude/dist/agent.js +1271 -0
  34. package/agent-claude/dist/claude-client.js +357 -0
  35. package/agent-claude/dist/claude-events.js +223 -0
  36. package/agent-claude/dist/claude-home.js +29 -0
  37. package/agent-claude/dist/commands.js +182 -0
  38. package/agent-claude/dist/content.js +134 -0
  39. package/agent-claude/dist/inbox.js +53 -0
  40. package/agent-claude/dist/index.js +555 -0
  41. package/agent-claude/dist/input-queue.js +64 -0
  42. package/agent-claude/dist/interaction.js +271 -0
  43. package/agent-claude/dist/models.js +158 -0
  44. package/agent-claude/dist/permission.js +120 -0
  45. package/agent-claude/dist/session-id.js +20 -0
  46. package/agent-claude/dist/session-map.js +165 -0
  47. package/agent-claude/dist/world-plugin.js +14 -0
  48. package/agent-claude/package.json +74 -0
  49. package/agent-codex/README.md +74 -0
  50. package/agent-codex/cordis.patch.yml +77 -0
  51. package/agent-codex/dist/adapter.js +55 -0
  52. package/agent-codex/dist/agent-preset-codex.js +171 -0
  53. package/agent-codex/dist/agent-preset-projection.js +37 -0
  54. package/agent-codex/dist/agent.js +1056 -0
  55. package/agent-codex/dist/app-home.js +65 -0
  56. package/agent-codex/dist/codex-client.js +469 -0
  57. package/agent-codex/dist/codex-events.js +297 -0
  58. package/agent-codex/dist/codex-store.js +190 -0
  59. package/agent-codex/dist/inbox.js +53 -0
  60. package/agent-codex/dist/index.js +605 -0
  61. package/agent-codex/dist/models.js +152 -0
  62. package/agent-codex/dist/permission.js +71 -0
  63. package/agent-codex/dist/session-map.js +170 -0
  64. package/agent-codex/dist/spike.js +46 -0
  65. package/agent-codex/dist/world-plugin.js +96 -0
  66. package/agent-codex/package.json +78 -0
  67. package/agent-hermes/README.md +144 -0
  68. package/agent-hermes/cordis.patch.yml +83 -0
  69. package/agent-hermes/dist/adapter.js +83 -0
  70. package/agent-hermes/dist/agent-preset-hermes.js +171 -0
  71. package/agent-hermes/dist/agent-preset-projection.js +34 -0
  72. package/agent-hermes/dist/agent.js +1068 -0
  73. package/agent-hermes/dist/hermes-client.js +953 -0
  74. package/agent-hermes/dist/hermes-events.js +289 -0
  75. package/agent-hermes/dist/hermes-store.js +158 -0
  76. package/agent-hermes/dist/inbox.js +53 -0
  77. package/agent-hermes/dist/index.js +541 -0
  78. package/agent-hermes/dist/models.js +324 -0
  79. package/agent-hermes/dist/permission.js +60 -0
  80. package/agent-hermes/dist/world-plugin.js +101 -0
  81. package/agent-hermes/package.json +76 -0
  82. package/agent-hub/cordis.patch.yml +12 -0
  83. package/agent-hub/dist/agent-roster.d.ts +34 -0
  84. package/agent-hub/dist/agent-roster.js +27 -0
  85. package/agent-hub/dist/carrier.d.ts +43 -0
  86. package/agent-hub/dist/carrier.js +241 -0
  87. package/agent-hub/dist/client-shim.d.ts +28 -0
  88. package/agent-hub/dist/client-shim.js +251 -0
  89. package/agent-hub/dist/envelope.d.ts +54 -0
  90. package/agent-hub/dist/envelope.js +92 -0
  91. package/agent-hub/dist/gateway.d.ts +28 -0
  92. package/agent-hub/dist/gateway.js +18 -0
  93. package/agent-hub/dist/index-pass.d.ts +22 -0
  94. package/agent-hub/dist/index-pass.js +47 -0
  95. package/agent-hub/dist/index.d.ts +29 -0
  96. package/agent-hub/dist/index.js +29 -0
  97. package/agent-hub/dist/labels.d.ts +45 -0
  98. package/agent-hub/dist/labels.js +83 -0
  99. package/agent-hub/dist/ownership.d.ts +37 -0
  100. package/agent-hub/dist/ownership.js +54 -0
  101. package/agent-hub/dist/roster.d.ts +28 -0
  102. package/agent-hub/dist/roster.js +38 -0
  103. package/agent-hub/dist/rpc.d.ts +38 -0
  104. package/agent-hub/dist/rpc.js +52 -0
  105. package/agent-hub/dist/spawn-world.d.ts +46 -0
  106. package/agent-hub/dist/spawn-world.js +72 -0
  107. package/agent-hub/dist/targets.d.ts +33 -0
  108. package/agent-hub/dist/targets.js +30 -0
  109. package/agent-hub/dist/world-entry.d.ts +31 -0
  110. package/agent-hub/dist/world-entry.js +51 -0
  111. package/agent-hub/dist/world-host.d.ts +69 -0
  112. package/agent-hub/dist/world-host.js +44 -0
  113. package/agent-hub/dist/world-join.d.ts +16 -0
  114. package/agent-hub/dist/world-join.js +122 -0
  115. package/agent-hub/dist/world-mount.d.ts +52 -0
  116. package/agent-hub/dist/world-mount.js +52 -0
  117. package/agent-hub/dist/world-mux.d.ts +72 -0
  118. package/agent-hub/dist/world-mux.js +224 -0
  119. package/agent-hub/dist/world-provision.d.ts +20 -0
  120. package/agent-hub/dist/world-provision.js +122 -0
  121. package/agent-hub/dist/world-web-server.d.ts +162 -0
  122. package/agent-hub/dist/world-web-server.js +248 -0
  123. package/agent-hub/lib/client/index.js +242 -0
  124. package/agent-hub/package.json +65 -0
  125. package/agent-omp/README.md +77 -0
  126. package/agent-omp/cordis.patch.yml +92 -0
  127. package/agent-omp/dist/adapter.js +36 -0
  128. package/agent-omp/dist/agent-preset-omp.js +171 -0
  129. package/agent-omp/dist/agent-preset-projection.js +36 -0
  130. package/agent-omp/dist/agent.js +1145 -0
  131. package/agent-omp/dist/app-home.js +86 -0
  132. package/agent-omp/dist/discovery.js +116 -0
  133. package/agent-omp/dist/inbox.js +53 -0
  134. package/agent-omp/dist/index.js +574 -0
  135. package/agent-omp/dist/knobs.js +50 -0
  136. package/agent-omp/dist/lazy-rpc.js +161 -0
  137. package/agent-omp/dist/mobile/gesture.js +148 -0
  138. package/agent-omp/dist/mobile/zoom-guard.js +294 -0
  139. package/agent-omp/dist/mobile-boot.js +38 -0
  140. package/agent-omp/dist/models.js +299 -0
  141. package/agent-omp/dist/omp-cli.js +147 -0
  142. package/agent-omp/dist/omp-disk-discovery.js +178 -0
  143. package/agent-omp/dist/omp-store.js +518 -0
  144. package/agent-omp/dist/pairing.js +45 -0
  145. package/agent-omp/dist/permission.js +96 -0
  146. package/agent-omp/dist/protocol.js +13 -0
  147. package/agent-omp/dist/rpc-types.js +1 -0
  148. package/agent-omp/dist/sdk-client.js +378 -0
  149. package/agent-omp/dist/sidecar-client.js +195 -0
  150. package/agent-omp/dist/store/adapter.js +47 -0
  151. package/agent-omp/dist/store/db.js +210 -0
  152. package/agent-omp/dist/store/index.js +59 -0
  153. package/agent-omp/dist/store/reconcile.js +51 -0
  154. package/agent-omp/dist/store/schema.js +40 -0
  155. package/agent-omp/dist/world-plugin.js +96 -0
  156. package/agent-omp/package.json +97 -0
  157. package/agent-omp/sidecar/main.ts +481 -0
  158. package/agent-pi/README.md +67 -0
  159. package/agent-pi/cordis.patch.yml +80 -0
  160. package/agent-pi/dist/adapter.js +53 -0
  161. package/agent-pi/dist/agent-preset-pi.js +169 -0
  162. package/agent-pi/dist/agent-preset-projection.js +29 -0
  163. package/agent-pi/dist/agent.js +949 -0
  164. package/agent-pi/dist/inbox.js +53 -0
  165. package/agent-pi/dist/index.js +595 -0
  166. package/agent-pi/dist/knobs.js +24 -0
  167. package/agent-pi/dist/models.js +200 -0
  168. package/agent-pi/dist/permission.js +66 -0
  169. package/agent-pi/dist/pi-client.js +224 -0
  170. package/agent-pi/dist/pi-events.js +155 -0
  171. package/agent-pi/dist/pi-home.js +55 -0
  172. package/agent-pi/dist/safe-json.js +25 -0
  173. package/agent-pi/dist/session-map.js +119 -0
  174. package/agent-pi/dist/world-plugin.js +97 -0
  175. package/agent-pi/package.json +77 -0
  176. package/cordis.patch.yml +22 -19
  177. package/package.json +88 -25
  178. package/client.js +0 -4
@@ -0,0 +1,481 @@
1
+ /**
2
+ * omp-web sidecar entry — run under bun.
3
+ *
4
+ * OMP_HOME=~/.omp bun run sidecar/main.ts
5
+ *
6
+ * Wraps @oh-my-pi/pi-coding-agent and speaks the v0 JSON-lines protocol
7
+ * defined in ../src/protocol.ts. One sidecar = one OMP "app instance":
8
+ * shared authStorage / modelRegistry across all sessions it hosts.
9
+ *
10
+ * Sessions are addressed by a sidecar-minted `handle` (h1, h2, ...) rather
11
+ * than the OMP session id, because AgentSession-level operations
12
+ * (newSession / switchSession / fork) mint a NEW OMP session id while the
13
+ * bridge-side handle must stay stable for the lifetime of the client.
14
+ */
15
+ import { createAgentSession, SessionManager, discoverAuthStorage, ModelRegistry, Settings, loadSessionMessagesReadOnly, parseSessionEntries, discoverSkills, discoverSlashCommands, AgentRegistry, getThemeByName, setThemeInstance } from "@oh-my-pi/pi-coding-agent";
16
+ import { PROTOCOL_VERSION, type EventFrame, type RequestFrame, type ResponseFrame } from "../dist/protocol.js";
17
+ // The SDK's lsp tool formats results through the module-level `theme`
18
+ // instance, which only TUI mode initializes. In this headless sidecar any
19
+ // `theme.status` access crashes ("undefined is not an object") — the lsp
20
+ // device was unusable in the bridge line. Initialize the configured theme
21
+ // (config.yml `theme.dark`) at startup; fall back to a minimal identity stub
22
+ // so startup can never fail over cosmetics.
23
+ try {
24
+ // The lsp formatter only touches `theme.status` icons, so the identity stub
25
+ // is the guaranteed floor; the configured theme (config.yml theme.dark,
26
+ // "titanium" in this deployment) is preferred when loadable.
27
+ const stub = {
28
+ status: { success: "✓", warning: "⚠", error: "✗" },
29
+ fg: (_role: unknown, text?: string) => (typeof text === "string" ? text : ""),
30
+ } as unknown as Parameters<typeof setThemeInstance>[0];
31
+ setThemeInstance((await getThemeByName("titanium")) ?? stub);
32
+ console.error("[sidecar] theme initialized");
33
+ }
34
+ catch (error) {
35
+ console.error(`[sidecar] theme init failed (lsp device may crash): ${error}`);
36
+ if (error instanceof Error) console.error(error.stack?.split("\n").slice(0, 8).join("\n") ?? "(no stack)");
37
+ }
38
+
39
+ // ---------- outbound ----------
40
+
41
+ function send(frame: ResponseFrame | EventFrame): void {
42
+ process.stdout.write(JSON.stringify(frame) + "\n");
43
+ }
44
+
45
+ // ---------- shared app-level state (A-lane) ----------
46
+
47
+ const piPkg = (await import("@oh-my-pi/pi-coding-agent/package.json")) as any;
48
+ const PKG_VERSION: string = piPkg?.default?.version ?? piPkg?.version ?? "unknown";
49
+
50
+ const authStorage = await discoverAuthStorage();
51
+ const modelRegistry = new ModelRegistry(authStorage);
52
+ void (modelRegistry as any).refreshInBackground?.()?.catch?.(() => {});
53
+
54
+ // Lazily-cached PERSISTENT Settings instance for modelRoles writes. Cached so
55
+ // repeated writes reuse one instance (and one agent.db handle); the SDK's
56
+ // whole-object `set` captures the on-disk generation at write time, so a cached
57
+ // instance does NOT serve stale data to the generation check. (readOnly
58
+ // instances never persist, so a fresh instance is not an option here.)
59
+ let writableSettingsPromise: Promise<Settings> | null = null;
60
+ function getWritableSettings(): Promise<Settings> {
61
+ if (writableSettingsPromise === null) {
62
+ writableSettingsPromise = Settings.loadIsolated();
63
+ }
64
+ return writableSettingsPromise;
65
+ }
66
+
67
+ interface HeldSession {
68
+ session: Awaited<ReturnType<typeof createAgentSession>>["session"];
69
+ unsubscribe: () => void;
70
+ }
71
+ const sessions = new Map<string, HeldSession>();
72
+ let nextHandle = 0;
73
+ /**
74
+ * Per-session agent identity for `createAgentSession`. The SDK's session init
75
+ * registers the top-level agent in the PROCESS-GLOBAL roster keyed by agent id
76
+ * — the default id ("Main") collides whenever a second session initializes
77
+ * while a first is still registered, failing with `Agent "Main" was replaced
78
+ * during session initialization` (upstream changelog: embedders pass a unique
79
+ * id / private registry for exactly this). Unique ids keep concurrent live
80
+ * sessions out of each other's way; `AgentRegistry.global().list()`
81
+ * consumers here filter `kind === "sub"`, so these roster entries never leak
82
+ * into the subagents surface.
83
+ */
84
+ let nextAgentSeq = 0;
85
+ function nextAgentId(): string {
86
+ return `omp-web-${process.pid}-${++nextAgentSeq}`;
87
+ }
88
+
89
+ function hold(session: HeldSession["session"]): string {
90
+ const handle = `h${++nextHandle}`;
91
+ const unsubscribe = session.subscribe((event: any) => {
92
+ send({ event: "session:event", sessionId: handle, payload: event });
93
+ });
94
+ sessions.set(handle, { session, unsubscribe });
95
+ return handle;
96
+ }
97
+
98
+ async function get(handle: string) {
99
+ const held = sessions.get(handle);
100
+ if (!held) throw new Error(`unknown session handle: ${handle}`);
101
+ return held.session;
102
+ }
103
+
104
+ function drop(handle: string): HeldSession | undefined {
105
+ const held = sessions.get(handle);
106
+ if (held) sessions.delete(handle);
107
+ return held;
108
+ }
109
+
110
+ async function createSession(params: any): Promise<string> {
111
+ const opts: Record<string, unknown> = {};
112
+ if (params?.cwd) opts.cwd = params.cwd;
113
+ if (params?.model) {
114
+ const found = modelRegistry.find?.(params.model);
115
+ if (found) opts.model = found;
116
+ }
117
+ if (params?.systemPrompt !== undefined) opts.systemPrompt = params.systemPrompt;
118
+ if (params?.appendSystemPrompt !== undefined) opts.appendSystemPrompt = params.appendSystemPrompt;
119
+ // approval parity with `omp --approval-mode`: yolo = fully auto-approved.
120
+ if (params?.approvalMode === "yolo") opts.autoApprove = true;
121
+ // omp SDK >= 18.1 made the SessionManager constructors async (they return
122
+ // promises); await keeps compatibility with the older sync surface too.
123
+ if (params?.resumeFile) opts.sessionManager = await SessionManager.open(params.resumeFile);
124
+ else if (params?.persistence === "file") opts.sessionManager = await SessionManager.create(params.cwd ?? process.cwd());
125
+ else opts.sessionManager = await SessionManager.inMemory();
126
+ opts.agentId = nextAgentId();
127
+
128
+ const { session } = await createAgentSession(opts as any);
129
+ return hold(session);
130
+ }
131
+
132
+ function describe(session: HeldSession["session"]) {
133
+ const model: any = session.model;
134
+ return {
135
+ sessionId: session.sessionId,
136
+ sessionFile: session.sessionFile ?? undefined,
137
+ model: model ? { id: model.id, provider: model.provider, name: model.name } : undefined,
138
+ thinkingLevel: String(session.thinkingLevel),
139
+ isStreaming: session.isStreaming,
140
+ messageCount: session.messages.length,
141
+ };
142
+ }
143
+
144
+ /** Normalize a SDK timestamp (Date | number | ISO string) to epoch ms. */
145
+ function toEpochMs(value: unknown): number | undefined {
146
+ if (value instanceof Date) return value.getTime();
147
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
148
+ if (typeof value === "string") {
149
+ const t = Date.parse(value);
150
+ return Number.isFinite(t) ? t : undefined;
151
+ }
152
+ return undefined;
153
+ }
154
+
155
+ // ---------- method implementations ----------
156
+
157
+ type Handler = (params: any) => Promise<unknown>;
158
+
159
+ const table: Record<string, Handler> = {
160
+ "sys.ping": async () => ({ pong: true, sdk: PKG_VERSION, bun: Bun.version }),
161
+
162
+ "models.list": async ({ refresh }: { refresh?: boolean }) => {
163
+ if (refresh) await modelRegistry.refresh();
164
+ const all = modelRegistry.getAvailable();
165
+ return {
166
+ models: all.map((m: any) => ({
167
+ provider: m.provider,
168
+ id: m.id,
169
+ name: m.name,
170
+ ...(m.api !== undefined ? { api: m.api } : {}),
171
+ ...(m.baseUrl !== undefined ? { baseUrl: m.baseUrl } : {}),
172
+ ...(typeof m.reasoning === "boolean" ? { reasoning: m.reasoning } : {}),
173
+ ...(Array.isArray(m.input) ? { input: m.input } : {}),
174
+ ...(typeof m.contextWindow === "number"
175
+ ? { contextWindow: m.contextWindow }
176
+ : typeof m.context_length === "number"
177
+ ? { contextWindow: m.context_length }
178
+ : {}),
179
+ ...(typeof m.maxTokens === "number" ? { maxTokens: m.maxTokens } : {}),
180
+ ...(m.thinking !== null && typeof m.thinking === "object"
181
+ ? {
182
+ thinking: {
183
+ ...(typeof m.thinking.mode === "string" ? { mode: m.thinking.mode } : {}),
184
+ ...(Array.isArray(m.thinking.efforts) ? { efforts: m.thinking.efforts } : {}),
185
+ ...(typeof m.thinking.defaultLevel === "string" ? { defaultLevel: m.thinking.defaultLevel } : {}),
186
+ },
187
+ }
188
+ : {}),
189
+ })),
190
+ providers: [...new Set(all.map((m: any) => m.provider))],
191
+ };
192
+ },
193
+
194
+ // ---- settings: modelRoles (A1) ----
195
+ //
196
+ // Read path reads FRESH on every call (Settings.loadReadOnly) rather than a
197
+ // sidecar-cached Settings instance: the OMP TUI is a separate process that
198
+ // rewrites config.yml when the operator changes the default model, and a
199
+ // long-lived sidecar's cached Settings instance would keep serving the stale
200
+ // in-memory value (the SDK does not watch config for a running session).
201
+ // modelRoles reads happen at the bridge's reconcile-tick cadence (~30s), so a
202
+ // fresh disk read per get is cheap and is the faithful equivalent of
203
+ // `omp config get modelRoles --json`.
204
+ "settings.modelRoles.get": async () => {
205
+ const s = await Settings.loadReadOnly();
206
+ return { modelRoles: s.getModelRoles() };
207
+ },
208
+
209
+ // Whole-object write, matching `omp config set modelRoles` (dotted keys are
210
+ // rejected; the entire modelRoles object is the unit). `set()` + `flush()`
211
+ // makes the write durable before we answer; the SDK re-reads config.yml under
212
+ // a file lock and writes atomically, preserving a concurrent edit to a sibling
213
+ // top-level key while modelRoles is replaced as a unit. A read-back confirms
214
+ // the write actually landed (the SDK silently skips a stale generation), so
215
+ // the bridge's shadow-key sync only advances on a real write.
216
+ "settings.modelRoles.set": async ({ modelRoles }: any) => {
217
+ if (modelRoles === null || typeof modelRoles !== "object" || Array.isArray(modelRoles)) {
218
+ throw new Error("settings.modelRoles.set: modelRoles must be a record");
219
+ }
220
+ const s = await getWritableSettings();
221
+ s.set("modelRoles", modelRoles as Record<string, string>);
222
+ await s.flush();
223
+ const fresh = await Settings.loadReadOnly();
224
+ const onDisk = fresh.getModelRoles();
225
+ return { ok: Bun.deepEquals(onDisk, modelRoles as Record<string, string>) };
226
+ },
227
+
228
+ "session.create": async (params) => {
229
+ const handle = await createSession(params);
230
+ return { handle, ...describe(await get(handle)) };
231
+ },
232
+
233
+ "session.dispose": async ({ handle }: any) => {
234
+ const held = drop(handle);
235
+ if (!held) return { disposed: true };
236
+ held.unsubscribe();
237
+ await held.session.dispose();
238
+ return { disposed: true };
239
+ },
240
+
241
+ // get_state parity
242
+ "session.state": async ({ handle }: any) => describe(await get(handle)),
243
+
244
+ // get_messages parity
245
+ "session.messages": async ({ handle }: any) => {
246
+ const session = await get(handle);
247
+ return { messages: session.messages };
248
+ },
249
+
250
+ // Render the held session's full base system prompt (live path). The prompt
251
+ // is lazy (a short placeholder until the first refresh), so force a rebuild
252
+ // and return the rendered blocks joined into one text.
253
+ "session.systemPrompt": async ({ handle }: any) => {
254
+ const session: any = await get(handle);
255
+ await session.refreshBaseSystemPrompt?.();
256
+ const blocks = session.systemPrompt ?? session.state?.systemPrompt ?? [];
257
+ const text = Array.isArray(blocks) ? blocks.join("\n\n") : String(blocks ?? "");
258
+ return { systemPrompt: text };
259
+ },
260
+
261
+ // get_session_stats parity (live sessions)
262
+ "session.stats": async ({ handle }: any) => {
263
+ const session: any = await get(handle);
264
+ try {
265
+ return (await session.getSessionStats?.()) ?? {};
266
+ } catch {
267
+ return {};
268
+ }
269
+ },
270
+
271
+ // /compact parity: run the SDK's own context compaction on the held
272
+ // session. The TUI's /compact awaits compact() then prints the
273
+ // context-usage delta, so this resolves only when it settles.
274
+ "session.compact": async ({ handle, instructions }: any) => {
275
+ const session: any = await get(handle);
276
+ const text = typeof instructions === "string" && instructions.trim() !== "" ? instructions.trim() : undefined;
277
+ await session.compact(text);
278
+ return { compacted: true };
279
+ },
280
+
281
+ // Context-usage snapshot (sync SDK getter) — pre/post compact metering.
282
+ "session.contextUsage": async ({ handle }: any) => {
283
+ const session: any = await get(handle);
284
+ const usage = session.getContextUsage?.();
285
+ return { usage: usage ?? undefined };
286
+ },
287
+
288
+ // get_subagents parity — SDK subagent registry exposure TBD; fail-soft.
289
+ // Subagent registry: enumerate the session's own subagents (kind === "sub"
290
+ // with parentId === this session's registry id). The sidecar's per-session
291
+ // registry would give a faithful tree; the global registry is the fallback.
292
+ "session.subagents": async ({ handle }: any) => {
293
+ const session: any = await get(handle);
294
+ const myId = session.getAgentId?.();
295
+ const subagents = AgentRegistry.global().list()
296
+ .filter((r: any) => r.kind === "sub" && (myId === undefined || r.parentId === myId))
297
+ .map((r: any) => ({
298
+ id: r.id, displayName: r.displayName, parentId: r.parentId, kind: r.kind,
299
+ status: r.status, sessionFile: r.sessionFile, cwd: r.session?.cwd,
300
+ createdAt: r.createdAt, lastActivity: r.lastActivity, activity: r.activity,
301
+ }));
302
+ return { subagents };
303
+ },
304
+
305
+ // Skills + slash commands from OMP's extensibility store (fail-soft).
306
+ "skills.list": async ({ cwd }: any) => {
307
+ try {
308
+ const { skills } = await discoverSkills(cwd);
309
+ return { skills: skills.map((s: any) => ({ name: s.name, description: s.description, filePath: s.filePath, baseDir: s.baseDir, source: s.source })) };
310
+ } catch {
311
+ return { skills: [] };
312
+ }
313
+ },
314
+
315
+ "slashCommands.list": async ({ cwd }: any) => {
316
+ try {
317
+ const commands = await discoverSlashCommands(cwd);
318
+ return { commands: commands.map((c: any) => ({ name: c.name, description: c.description, content: c.content, source: c.source })) };
319
+ } catch {
320
+ return { commands: [] };
321
+ }
322
+ },
323
+
324
+ "session.prompt": async ({ handle, text, streamingBehavior }: any) => {
325
+ const session = await get(handle);
326
+ // Fire-and-forget, mirroring RPC's prompt semantics: the response reports
327
+ // acceptance only; the turn itself streams through session:event frames.
328
+ // Turn-level failures arrive as events (`message_update` error / notice).
329
+ void Promise.resolve(
330
+ session.prompt(text, streamingBehavior ? { streamingBehavior } : undefined),
331
+ ).catch(() => {});
332
+ return { accepted: true };
333
+ },
334
+
335
+ "session.steer": async ({ handle, text }: any) => {
336
+ const session = await get(handle);
337
+ await session.steer(text);
338
+ return { accepted: true };
339
+ },
340
+
341
+ "session.followUp": async ({ handle, text }: any) => {
342
+ const session = await get(handle);
343
+ await session.followUp(text);
344
+ return { accepted: true };
345
+ },
346
+
347
+ "session.abort": async ({ handle }: any) => {
348
+ const session = await get(handle);
349
+ await session.abort();
350
+ return { accepted: true };
351
+ },
352
+
353
+ "session.setModel": async ({ handle, provider, modelId }: any) => {
354
+ const session = await get(handle);
355
+ const all = modelRegistry.getAvailable();
356
+ const target = all.find((m: any) => m.provider === provider && m.id === modelId);
357
+ if (!target) throw new Error(`model not available: ${provider}/${modelId}`);
358
+ await session.setModel(target as any);
359
+ return { accepted: true };
360
+ },
361
+
362
+ // new_session parity: replace the held AgentSession with a fresh one under
363
+ // the same handle. The old session is disposed after the swap.
364
+ "session.new": async ({ handle, cwd }: any) => {
365
+ const held = sessions.get(handle);
366
+ if (!held) throw new Error(`unknown session handle: ${handle}`);
367
+ const fresh = await createSession({ cwd: cwd ?? (held.session as any).cwd ?? process.cwd(), persistence: "file" });
368
+ // hold() minted a new handle for the fresh session; steal its entry.
369
+ const freshHeld = drop(fresh)!;
370
+ held.unsubscribe();
371
+ sessions.set(handle, freshHeld);
372
+ try { await held.session.dispose(); } catch {}
373
+ return describe(freshHeld.session);
374
+ },
375
+
376
+ "sessions.list": async ({ cwd, all }: any) => {
377
+ // `SessionManager.listAll` takes no cwd — it walks the whole sessions root,
378
+ // so the `all` branch must NOT pass the cwd through (the pre-P4 code did,
379
+ // which fed the string into the `storage` slot and returned an empty list).
380
+ const entries = all
381
+ ? await SessionManager.listAll()
382
+ : await SessionManager.list(cwd ?? process.cwd());
383
+ const arr = (entries as any[]) ?? [];
384
+ return {
385
+ sessions: arr.map((e: any) => ({
386
+ id: e.id ?? e.sessionId,
387
+ file: e.file ?? e.path,
388
+ title: e.title,
389
+ timestamp: e.timestamp,
390
+ })),
391
+ };
392
+ },
393
+
394
+ // A6 — full-library native session index: one listAll across every cwd group.
395
+ // Serialized as epoch ms so the Node side needs no Date/ISO ambiguity.
396
+ "sessions.listAll": async () => {
397
+ const arr = ((await SessionManager.listAll()) as any[]) ?? [];
398
+ return {
399
+ sessions: arr.map((e: any) => ({
400
+ id: e.id,
401
+ file: e.path ?? e.file,
402
+ ...(typeof e.cwd === "string" ? { cwd: e.cwd } : {}),
403
+ ...(typeof e.title === "string" && e.title !== "" ? { title: e.title } : {}),
404
+ ...(typeof e.firstMessage === "string" && e.firstMessage !== "" ? { firstMessage: e.firstMessage } : {}),
405
+ ...(e.created !== undefined ? { createdAt: toEpochMs(e.created) } : {}),
406
+ ...(e.modified !== undefined ? { mtimeMs: toEpochMs(e.modified) } : {}),
407
+ ...(typeof e.size === "number" ? { size: e.size } : {}),
408
+ })),
409
+ };
410
+ },
411
+
412
+ // A6 — read-only transcript extraction (resume/replay). Messages come from the
413
+ // SDK's canonical reader (migration + blob refs + compaction aware); model
414
+ // changes have no dedicated reader, so they are recovered from a lenient parse.
415
+ "sessions.messagesReadOnly": async ({ file }: any) => {
416
+ const messages = await loadSessionMessagesReadOnly(file);
417
+ let modelChanges: Array<{ model: string; role?: string }> = [];
418
+ try {
419
+ const content = await Bun.file(file).text();
420
+ const entries = parseSessionEntries(content);
421
+ modelChanges = entries
422
+ .filter((e: any) => e.type === "model_change" && typeof e.model === "string" && e.model !== "")
423
+ .map((e: any) => ({ model: e.model, ...(typeof e.role === "string" ? { role: e.role } : {}) }));
424
+ } catch {
425
+ // model changes are optional — messages alone still serve replay.
426
+ }
427
+ return { messages, modelChanges };
428
+ },
429
+ };
430
+
431
+ // ---------- inbound loop ----------
432
+
433
+ const decoder = new TextDecoder();
434
+ let buffer = "";
435
+ process.stdin.on("data", (chunk: Uint8Array) => {
436
+ buffer += decoder.decode(chunk, { stream: true });
437
+ let nl: number;
438
+ while ((nl = buffer.indexOf("\n")) >= 0) {
439
+ const raw = buffer.slice(0, nl).trim();
440
+ buffer = buffer.slice(nl + 1);
441
+ if (!raw) continue;
442
+ void handleRaw(raw);
443
+ }
444
+ });
445
+
446
+ async function handleRaw(raw: string): Promise<void> {
447
+ let frame: any;
448
+ try {
449
+ frame = JSON.parse(raw);
450
+ } catch {
451
+ send({ id: -1, ok: false, error: `malformed frame: ${raw.slice(0, 120)}` });
452
+ return;
453
+ }
454
+ if (frame?.method && typeof frame.id === "number") {
455
+ const handler = table[frame.method];
456
+ if (!handler) {
457
+ send({ id: frame.id, ok: false, error: `unknown method: ${frame.method}` });
458
+ return;
459
+ }
460
+ try {
461
+ const result = await handler(frame.params ?? {});
462
+ send({ id: frame.id, ok: true, result });
463
+ } catch (err: any) {
464
+ send({ id: frame.id, ok: false, error: String(err?.message ?? err) });
465
+ }
466
+ }
467
+ }
468
+
469
+ process.stdin.on("end", () => {
470
+ void (async () => {
471
+ for (const [, held] of sessions) {
472
+ try {
473
+ held.unsubscribe();
474
+ await held.session.dispose();
475
+ } catch {}
476
+ }
477
+ process.exit(0);
478
+ })();
479
+ });
480
+
481
+ send({ event: "ready", payload: { protocol: PROTOCOL_VERSION, sdk: PKG_VERSION, sessions: 0 } });
@@ -0,0 +1,67 @@
1
+ # @pgmi-builds/agent-adapter-pi (Agent Worlds AW-F)
2
+
3
+ pi coding agent provider for DeepSeek Harness — an `AgentFactory` dsh plugin that embeds the
4
+ **pi coding agent** (`@earendil-works/pi-coding-agent@0.84.2`) **in-process via its SDK**
5
+ (`createAgentSession` / `AgentSession` / `ModelRuntime`) and bridges pi's session surface into
6
+ the Dash Agent/Session contracts. Structure follows `agent-omp` (same pi-agent-core lineage);
7
+ lifecycle follows `agent-codex` (AW-E SDK line, DSH-native session persistence).
8
+
9
+ ## Per-adapter rulings (2026-09-17 user; decisions recorded per dev-rules §4/§12)
10
+
11
+ 1. **Native data home `~/.pi`.** Unlike every prior adapter (codex `<dshHome>/agents/codex`,
12
+ omp `<dshHome>/agents/omp/.omp`), pi keeps its **native home**: the adapter passes no
13
+ `agentDir` override, so the SDK's `getAgentDir()` default applies — auth, models, settings,
14
+ skills, extensions and project trust are exactly what the user's pi CLI sees. No app-home
15
+ seeding, no config one-way-valve. Dev-rules §5/§13's app-home ruling is superseded for pi's
16
+ runtime data by this directive. Tests redirect via `PI_CODING_AGENT_DIR` /
17
+ `PI_CODING_AGENT_SESSION_DIR` env only.
18
+ 2. **`<dshHome>/agents/pi/` = adapter-owned DSH state only** — currently the mapping file
19
+ `dsh-sessions.json`. pi never reads it; it is DSH-side bookkeeping in the same class as the
20
+ DSH session log itself.
21
+ 3. **Session storage duplication accepted**: pi writes its native session JSONL under
22
+ `~/.pi/agent/sessions/<encoded-cwd>/`; the adapter writes the DSH log through upstream
23
+ `session-persistence-jsonl` (factory holds the write channel). The WebUI reads the DSH copy;
24
+ the native copy stays pi's authority; no back-read of pi transcripts for list/replay.
25
+ 4. **Session identity = "mapping only"** (codex model): the DSH session id is the authority;
26
+ the pi session file path is `null` in the map until the first prompt materializes it; resume
27
+ resolves through the map and **fails closed** when unknown. Lazy iron law: zero pi objects
28
+ before the first real prompt.
29
+ 5. **In-process SDK** (user choice "sdk, refer to omp agent adapter"): no sidecar, no
30
+ `--mode rpc` subprocess. Shared module-level `ModelRuntime`; one `AgentSession` per session.
31
+ 6. **Model route**: single provider id `pi` on `ctx.llm`; model ids are composite
32
+ `<provider>/<modelId>` (pi is multi-provider). Selection is live (`session.setModel`) — an
33
+ upgrade over the codex SDK line's next-turn-only switch. `steer` is also real (pi supports it).
34
+ 7. **Permission presets → launch-only toolsets** (pi has no per-action approval):
35
+ `danger-full-access` / `workspace-write` → pi default tools (`read,bash,edit,write`);
36
+ `read-only` → `read,grep,find,ls`. `/permission` is shadowed per-session with a clean
37
+ refusal; `PI_APPROVAL_MODE=<preset-name>` overrides for headless runs.
38
+ 8. **Compaction is wired** (unlike codex): `/compact` works via `session.compact()`;
39
+ pi `compaction_start|end` → DSH `compaction/start|end` with a generated `compactionId`.
40
+ 9. **Titles**: pi owns them — `session_info_changed` → DSH `session/title`
41
+ (`source: {kind:"provider", provider:"pi"}`); the dsh `session-title-llm` row stays disabled.
42
+
43
+ ## Layout
44
+
45
+ `src/`: `index.ts` (PiProvider + `setupAndPublish` transaction), `agent.ts` (PiAgent shim),
46
+ `pi-client.ts` (SDK client seam + lazy start), `pi-events.ts` (pi → wire vocabulary projection),
47
+ `session-map.ts`, `models.ts` (ModelRuntime singleton + catalog memo), `adapter.ts` (LlmAdapter),
48
+ `permission.ts`, `agent-preset-pi.ts` + `agent-preset-projection.ts` (roster/chip), `pi-home.ts`
49
+ (state dir), `knobs.ts`, `inbox.ts`, `world-plugin.ts` (hub bridge).
50
+
51
+ ## Testing
52
+
53
+ `npm run build` then `node --test test/*.test.mjs` — tests import `dist/`, use temp homes, and
54
+ inject fake pi sessions via `setPiSessionFactory`; the SDK seam is never hit by unit tests.
55
+ Acceptance = standalone `dsh + adapter` real instance (form A) with a real pi turn, then the
56
+ world form (form B) via the hub.
57
+
58
+ ## Trap: `npm install` materializes the hub link
59
+
60
+ `dependencies["file:../agent-hub"]` gets materialized by `npm install` as a
61
+ **physical copy** under `node_modules/@pgmi-builds/agent-hub` — a second hub
62
+ module instance whose rosters/mount-registry are dead. Symptom in world form:
63
+ the world's virtual webServer never appears (`9 entries did not activate`,
64
+ everything pending on webServer). After ANY `npm install` in this package,
65
+ re-link: `rm -rf node_modules/@pgmi-builds/agent-hub && ln -sfn ../../../agent-hub
66
+ node_modules/@pgmi-builds/agent-hub` (codex symlink discipline; same class as
67
+ the `@deepseek-ai/*` heal).
@@ -0,0 +1,80 @@
1
+ # @pgmi-builds/agent-adapter-pi bundle patch (AW-F SDK line, codex form).
2
+ # This file is the `dsh.bundle.patch` layer of the adapter package: when the pi
3
+ # world profile mounts the bundle, the boot merges THIS patch — mount the pi
4
+ # provider plus disable the native components it replaces.
5
+ #
6
+ # Profile-specific values are deliberately NOT here: `webserver.port` (port
7
+ # choice) belongs to the world/profile patch layer.
8
+ #
9
+ # Home ruling (2026-09-17 user): the pi runtime keeps its NATIVE data home
10
+ # `~/.pi` (via the SDK's getAgentDir() default); this adapter seeds no app home
11
+ # and never redirects PI_CODING_AGENT_DIR. `<DSH_HOME>/agents/pi/` holds only
12
+ # adapter-owned DSH state (the dsh↔pi session mapping).
13
+
14
+ # Mount the pi provider: a Cordis Service that registers an AgentFactory on
15
+ # ctx.agents. The provider drives @earendil-works/pi-coding-agent AgentSessions
16
+ # in-process (one PiSessionClient per session) plus resume via the recorded
17
+ # native session file; the model selector is served by a Pi LlmAdapter reading
18
+ # the shared ModelRuntime catalog (auth/models-store from ~/.pi).
19
+ - insert:
20
+ - id: pi-provider
21
+ name: '@pgmi-builds/agent-adapter-pi'
22
+
23
+ # pi owns the live agent transcript (its native session JSONL under
24
+ # ~/.pi/agent/sessions); disable the built-in loop so it does not double-run
25
+ # alongside the SDK session.
26
+ - id: agent-loop
27
+ disabled: true
28
+
29
+ # The native routes register deepseek-official / pi-ai providers; the pi
30
+ # provider registers its own adapter (the single `pi` route) on ctx.llm.
31
+ # Leaving them mounted would advertise native models alongside pi's.
32
+ - id: llm-deepseek
33
+ disabled: true
34
+ - id: llm-pi-ai
35
+ disabled: true
36
+
37
+ # pi is single-mode: the SDK session drives tools itself, so no Dash preset
38
+ # composition is needed (the provider ships the fixed single-preset roster).
39
+ - id: agent-presets
40
+ disabled: true
41
+
42
+ # The upstream jsonl persistence row STAYS MOUNTED: the provider writes the
43
+ # DSH session log through it (create/open/append/close), so list/read/replay/
44
+ # workspace grouping are ordinary DSH services over that log. Session storage
45
+ # is duplicated by design (DSH copy for the WebUI, pi's native copy as the
46
+ # runtime authority) — accepted 2026-09-17.
47
+
48
+ # pi has no per-action approval channel — the adapter maps the Dash preset
49
+ # onto a launch-only toolset (src/permission.ts). Default to
50
+ # danger-full-access. Restate the full 3-preset table: a patch with only
51
+ # defaultPreset silently drops the bundle's 3-preset table to the plugin's
52
+ # built-in 2, breaking the 3:3 Dash<->pi pairing.
53
+ - id: permission
54
+ config:
55
+ presets:
56
+ read-only:
57
+ sandbox: read-only
58
+ approval: ask
59
+ workspace-write:
60
+ sandbox: workspace-write
61
+ approval: ask
62
+ danger-full-access:
63
+ sandbox: danger-full-access
64
+ approval: never
65
+ defaultPreset: danger-full-access
66
+
67
+ # Directory picker: the stock `directory-picker-auto` RESOLVES the interaction
68
+ # once per boot and mounts BOTH faces through the loader: the host backend
69
+ # package AND the matching CLIENT SURFACE package. The client surface occupies
70
+ # the workspace picker's directory-flow hole; NEVER just `disabled: true` this
71
+ # row — that removed the "add workspace" button (dev-rules §4.1). Pin the
72
+ # in-app browser by mounting both faces of `browse` ourselves and unmounting
73
+ # the auto row.
74
+ - insert:
75
+ - id: directory-picker-browse
76
+ name: '@deepseek-ai/dsh-host-directory-picker-browse'
77
+ - id: directory-picker-browse-surface
78
+ name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'
79
+ - id: directory-picker
80
+ disabled: true
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Pi-backed `LlmAdapter` registered on `ctx.llm` by the pi provider.
3
+ *
4
+ * The browser model selector never touches the pi agent directly: it is an
5
+ * RPC round-trip through the apiproxy, which reads `ctx.llm.listProviders /
6
+ * listModels / resolveModel` to build the catalog. This adapter answers those
7
+ * queries from the warmed catalog memo (`models.ts`: the shared ModelRuntime's
8
+ * available models). Like omp, it serves REAL per-provider routes: boot
9
+ * registers the single placeholder route `pi` and the provider swaps in the
10
+ * distinct catalog slugs (`piProviderIds` → `handle.replace`) once warm, so
11
+ * the picker groups models under DeepSeek/ZAI/… instead of one flat "Pi"
12
+ * group — never the native deepseek-official / pi-ai routes, which the
13
+ * profile disables.
14
+ *
15
+ * Model ids are the BARE provider-scoped id per route (omp parity: the
16
+ * composite `<provider>/<modelId>` prefix is dropped at the picker boundary;
17
+ * legacy composite selections are split upstream before they reach the
18
+ * runtime). `stream` is never dispatched: pi owns generation through its SDK
19
+ * session (`PiAgent`), so the adapter only serves catalog/metadata queries.
20
+ * It throws rather than fabricating a wire route the harness would misuse.
21
+ */
22
+ import { LlmAdapter, LlmError, } from "@deepseek-ai/dsh-llm";
23
+ import { PI_PROVIDER_ID, providerDisplayName, readPiModelCatalog } from "./models.js";
24
+ export { PI_PROVIDER_ID };
25
+ export class PiLlmAdapter extends LlmAdapter {
26
+ providerInfo(provider) {
27
+ return { id: provider, name: providerDisplayName(provider) };
28
+ }
29
+ async listModels(provider) {
30
+ return readPiModelCatalog()
31
+ .models.filter((model) => model.provider === provider)
32
+ .map((model) => ({
33
+ provider,
34
+ id: model.id,
35
+ name: model.label,
36
+ }));
37
+ }
38
+ stream(_options) {
39
+ throw new LlmError("the Pi adapter does not stream: pi owns generation through its SDK agent", "UNSUPPORTED_STREAM");
40
+ }
41
+ async resolveModel(provider, model) {
42
+ const found = readPiModelCatalog().models.find((candidate) => candidate.provider === provider && candidate.id === model);
43
+ if (found === undefined) {
44
+ throw new LlmError(`Pi provider "${provider}" does not serve model "${model}"`, "MODEL_NOT_FOUND");
45
+ }
46
+ return {
47
+ provider,
48
+ id: found.id,
49
+ name: found.label,
50
+ ...(found.contextWindow !== undefined && found.contextWindow > 0 ? { context: { contextWindow: found.contextWindow } } : {}),
51
+ };
52
+ }
53
+ }