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,200 @@
1
+ /**
2
+ * models — the shared pi ModelRuntime seam and the memoized model catalog.
3
+ *
4
+ * ONE ModelRuntime per process (the SDK's app-level model/auth service: it
5
+ * reads the native `~/.pi/agent` tree — auth.json, models-store.json,
6
+ * models.json — so user-configured providers work identically; the
7
+ * 2026-09-17 native-home ruling means we do NOT redirect any of it). The
8
+ * runtime is injectable (`setPiModelRuntimeFactory`) so tests never load the
9
+ * real SDK.
10
+ *
11
+ * The catalog memo fills on `warmPiCatalog()` (provider boot calls it once,
12
+ * fire-and-forget); `readPiModelCatalog()` is a synchronous memo read for
13
+ * the call sites codex served from a sync disk read (route pinning, default
14
+ * model push). Cold → empty models + `defaultSelection: undefined`, with
15
+ * codex's placeholder-omit semantics at the call sites (nothing is invented
16
+ * and no placeholder value ever leaves this module).
17
+ *
18
+ * Model identity (omp parity, 2026-09-17 parity fixes): every entry carries
19
+ * its REAL pi provider slug (`deepseek`, `zai`, …) plus the BARE model id,
20
+ * and the picker groups by the distinct slug routes this adapter registers
21
+ * after the warm (`piProviderIds` → registration `handle.replace`). Model
22
+ * ids therefore never collide across providers and no umbrella re-branding
23
+ * collapses them into one "Pi" group. `PI_PROVIDER_ID` survives only as the
24
+ * boot placeholder route (`registerAdapter` must not be empty) and in the
25
+ * legacy split path: selections stored before the split arrive as
26
+ * `<provider>/<modelId>` composites under the umbrella route, which
27
+ * `splitCatalogId` (FIRST slash) still resolves.
28
+ */
29
+ import { existsSync, readFileSync } from "node:fs";
30
+ import { homedir } from "node:os";
31
+ import { join } from "node:path";
32
+ /**
33
+ * The BOOT placeholder route: `registerAdapter` must not be empty, so the
34
+ * provider registers this single route at boot and atomically swaps it for
35
+ * the real per-provider slugs once the catalog is warm. No selection can be
36
+ * made under it (the cold catalog lists nothing).
37
+ */
38
+ export const PI_PROVIDER_ID = "pi";
39
+ /** Default factory: the real SDK's ModelRuntime (dynamic import, cached). */
40
+ let runtimeFactory = async () => {
41
+ const sdk = (await import("@earendil-works/pi-coding-agent"));
42
+ return sdk.ModelRuntime.create();
43
+ };
44
+ /** Replace the runtime factory (tests inject a fake). */
45
+ export function setPiModelRuntimeFactory(factory) {
46
+ runtimeFactory = factory;
47
+ runtimePromise = undefined;
48
+ }
49
+ let runtimePromise;
50
+ /** The shared ModelRuntime (memoized; one per process). */
51
+ export function getPiModelRuntime() {
52
+ runtimePromise ??= runtimeFactory();
53
+ return runtimePromise;
54
+ }
55
+ let memo;
56
+ let warmPromise;
57
+ /** Split a composite `<provider>/<modelId>` on the FIRST slash; `undefined` when there is none.
58
+ *
59
+ * Legacy path only: pre-split stored selections arrive as composites under
60
+ * the umbrella `pi` route. Real-slug selections pass the bare id (which may
61
+ * itself contain slashes) and must NOT be split here. */
62
+ export function splitCatalogId(composite) {
63
+ const slash = composite.indexOf("/");
64
+ if (slash <= 0 || slash === composite.length - 1)
65
+ return undefined;
66
+ return { provider: composite.slice(0, slash), modelId: composite.slice(slash + 1) };
67
+ }
68
+ /** One warm pass: `getAvailable()` into the memo. Idempotent in-flight. */
69
+ export function warmPiCatalog() {
70
+ warmPromise ??= (async () => {
71
+ const runtime = await getPiModelRuntime();
72
+ const available = (await runtime.getAvailable());
73
+ const models = [];
74
+ for (const raw of available) {
75
+ const provider = typeof raw["provider"] === "string" ? raw["provider"] : "";
76
+ const id = typeof raw["id"] === "string" ? raw["id"] : "";
77
+ if (provider === "" || id === "")
78
+ continue;
79
+ models.push({
80
+ id,
81
+ label: typeof raw["name"] === "string" && raw["name"] !== "" ? raw["name"] : id,
82
+ provider,
83
+ ...(typeof raw["contextWindow"] === "number" && raw["contextWindow"] > 0
84
+ ? { contextWindow: raw["contextWindow"] }
85
+ : {}),
86
+ });
87
+ }
88
+ memo = models;
89
+ })();
90
+ return warmPromise;
91
+ }
92
+ /**
93
+ * The pi CLI's default-model facts: `settings.json` in the agent dir
94
+ * (`defaultProvider` + `defaultModel`). The agent dir resolution mirrors the
95
+ * SDK (`PI_CODING_AGENT_DIR` → `~/.pi/agent`) without importing the SDK on
96
+ * this synchronous path. Fail-soft: unreadable settings contribute nothing.
97
+ */
98
+ function settingsDefault() {
99
+ const env = process.env.PI_CODING_AGENT_DIR;
100
+ const agentDir = env !== undefined && env.trim() !== "" ? env : join(homedir(), ".pi", "agent");
101
+ const path = join(agentDir, "settings.json");
102
+ if (!existsSync(path))
103
+ return undefined;
104
+ try {
105
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
106
+ const provider = typeof parsed["defaultProvider"] === "string" ? parsed["defaultProvider"] : "";
107
+ const modelId = typeof parsed["defaultModel"] === "string" ? parsed["defaultModel"] : "";
108
+ if (provider === "" || modelId === "")
109
+ return undefined;
110
+ return { provider, modelId };
111
+ }
112
+ catch {
113
+ return undefined;
114
+ }
115
+ }
116
+ /**
117
+ * Synchronous catalog read. `defaultSelection` is the settings default only
118
+ * when it is actually served by the warmed catalog; otherwise `undefined`
119
+ * (call sites OMIT the model so pi's own default applies).
120
+ */
121
+ export function readPiModelCatalog() {
122
+ const models = memo ?? [];
123
+ const wanted = settingsDefault();
124
+ const served = wanted !== undefined && models.some((model) => model.provider === wanted.provider && model.id === wanted.modelId);
125
+ return {
126
+ provider: PI_PROVIDER_ID,
127
+ providerName: "Pi",
128
+ models,
129
+ defaultSelection: served && wanted !== undefined ? { provider: wanted.provider, model: wanted.modelId } : undefined,
130
+ };
131
+ }
132
+ /**
133
+ * The distinct provider slugs across the catalog — the picker routes this
134
+ * adapter registers. omp's ordering pattern: the operator-wired provider
135
+ * leads (for pi that is settings.json `defaultProvider`, the pi CLI's own
136
+ * default-model role), everything else follows alphabetically.
137
+ */
138
+ export function piProviderIds(models = readPiModelCatalog().models) {
139
+ const seen = new Set();
140
+ for (const model of models)
141
+ seen.add(model.provider);
142
+ const ordered = [];
143
+ const preferred = settingsDefault()?.provider ?? "";
144
+ if (preferred !== "" && seen.has(preferred))
145
+ ordered.push(preferred);
146
+ for (const provider of [...seen].sort((a, b) => a.localeCompare(b))) {
147
+ if (!ordered.includes(provider))
148
+ ordered.push(provider);
149
+ }
150
+ return ordered;
151
+ }
152
+ /** Human-readable provider name for `LlmProviderInfo.name` (omp pattern). */
153
+ const PROVIDER_TITLES = {
154
+ zai: "Z.AI",
155
+ xai: "xAI",
156
+ openrouter: "OpenRouter",
157
+ deepseek: "DeepSeek",
158
+ anthropic: "Anthropic",
159
+ openai: "OpenAI",
160
+ google: "Google",
161
+ moonshot: "Moonshot AI",
162
+ };
163
+ export function providerDisplayName(provider) {
164
+ return PROVIDER_TITLES[provider] ?? (provider.charAt(0).toUpperCase() + provider.slice(1));
165
+ }
166
+ /**
167
+ * Resolve a stored/observed model selection into the real route pair.
168
+ * Handles both shapes: a real-slug pair (`provider` = a catalog slug,
169
+ * `model` = the bare id, which may itself contain slashes) passes through
170
+ * untouched; a LEGACY composite rides the umbrella `pi` route with
171
+ * `<provider>/<modelId>` as the model and is split once. A warm catalog is
172
+ * the truth source: an unserved pair degrades to `undefined` so call sites
173
+ * omit the model (pi's own default applies) instead of forwarding a value
174
+ * the runtime would silently refuse.
175
+ */
176
+ export function resolvePiSelection(provider, model) {
177
+ if (typeof model !== "string" || model === "")
178
+ return undefined;
179
+ let resolved;
180
+ if (provider === undefined || provider === "" || provider === PI_PROVIDER_ID) {
181
+ const split = splitCatalogId(model);
182
+ resolved = split !== undefined ? { provider: split.provider, model: split.modelId } : { provider: provider ?? "", model };
183
+ }
184
+ else {
185
+ resolved = { provider, model };
186
+ }
187
+ if (resolved.provider === "" || resolved.model === "")
188
+ return undefined;
189
+ const memoed = memo;
190
+ if (memoed !== undefined && memoed.length > 0 && !memoed.some((m) => m.provider === resolved.provider && m.id === resolved.model)) {
191
+ return undefined;
192
+ }
193
+ return resolved;
194
+ }
195
+ /** Test hygiene: drop the memo and the runtime singleton. */
196
+ export function clearPiCatalogForTests() {
197
+ memo = undefined;
198
+ warmPromise = undefined;
199
+ runtimePromise = undefined;
200
+ }
@@ -0,0 +1,66 @@
1
+ /** The Dash preset table (mirrors the base bundle's permission plugin config). */
2
+ const PRESETS = {
3
+ "danger-full-access": { sandbox: "danger-full-access", approval: "never", toolset: undefined },
4
+ "workspace-write": { sandbox: "workspace-write", approval: "ask", toolset: undefined },
5
+ "read-only": { sandbox: "read-only", approval: "ask", toolset: ["read", "grep", "find", "ls"] },
6
+ };
7
+ /** Narrow an untrusted value to a known preset name. */
8
+ export function isPresetName(raw) {
9
+ return typeof raw === "string" && Object.hasOwn(PRESETS, raw);
10
+ }
11
+ /**
12
+ * `PI_APPROVAL_MODE` when it names a preset this adapter implements, else
13
+ * undefined. An invalid value is IGNORED (the preset mapping applies).
14
+ */
15
+ export function envApprovalMode() {
16
+ const raw = process.env.PI_APPROVAL_MODE;
17
+ return isPresetName(raw) ? raw : undefined;
18
+ }
19
+ /**
20
+ * Map a preset onto the launch-only pi toolset. `undefined` = pi's own
21
+ * default tool set (read, bash, edit, write). Unknown names degrade to the
22
+ * default.
23
+ */
24
+ export function piToolset(preset) {
25
+ return isPresetName(preset) ? PRESETS[preset].toolset : undefined;
26
+ }
27
+ /**
28
+ * The default preset for FUTURE sessions, as the permission service
29
+ * resolves it (settings-backed; the profile patch pins the base default).
30
+ * Undefined when the service is absent or its getter fails — callers fall
31
+ * back to the pi default toolset.
32
+ */
33
+ export function defaultPermissionPreset(ctx) {
34
+ const service = ctx.get("permissionPresets");
35
+ if (service === undefined)
36
+ return undefined;
37
+ try {
38
+ const preset = service.defaultPreset;
39
+ return isPresetName(preset) ? preset : undefined;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ /** The session's effective preset: the last `permission/preset` event. */
46
+ export function presetFromEvents(events) {
47
+ for (let index = events.length - 1; index >= 0; index -= 1) {
48
+ const event = events[index];
49
+ if (event.type === "permission/preset" && isPresetName(event.data?.preset))
50
+ return event.data.preset;
51
+ }
52
+ return undefined;
53
+ }
54
+ /**
55
+ * The three Dash events `pinInitialPermission` stamps for a fresh session,
56
+ * in its append order (preset, sandbox, approval). Cold replay prepends
57
+ * them so the UI shows the session's permission after wrapper restarts.
58
+ */
59
+ export function permissionEventsFor(preset, time) {
60
+ const spec = PRESETS[preset];
61
+ return [
62
+ { type: "permission/preset", time, data: { preset } },
63
+ { type: "sandbox/mode", time, data: { mode: spec.sandbox } },
64
+ { type: "approval/policy", time, data: { policy: spec.approval } },
65
+ ];
66
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * PiSessionClient — the SDK-line client seam: one pi `AgentSession` per Dash
3
+ * session, driven in-process (2026-09-17 "sdk" ruling; no sidecar, no
4
+ * `--mode rpc` subprocess).
5
+ *
6
+ * Lifecycle is LAZY by the omp iron law: `spawn()` builds a zero-IO shell and
7
+ * `ensureStarted()` (first real prompt) runs the injected factory — by default
8
+ * `createAgentSession` over the shared ModelRuntime with
9
+ * `SessionManager.create(cwd)` (create) or `SessionManager.open(file)` (resume).
10
+ * pi's native session JSONL materializes at that moment under `~/.pi`; the
11
+ * provider records the file into the mapping once it exists.
12
+ *
13
+ * Public surface mirrors the codex client seam (`spawn` / `on` / `onFailure` /
14
+ * `getState` / `prompt` / `followUp` / `steer` / `setModel` / `abort` /
15
+ * `ensureStarted` / `spawned` / `close`) so the PiAgent port compiles the same
16
+ * way CodexAgent compiles against CodexSdkClient, plus pi-only surfaces
17
+ * (`setThinkingLevel`, `compact` — pi compaction is real and wired).
18
+ *
19
+ * Events flow through `projectSessionEvent` before reaching listeners; pi
20
+ * emits its own terminal `agent_end`, so the client never synthesizes one.
21
+ */
22
+ import { projectSessionEvent } from "./pi-events.js";
23
+ import { getPiModelRuntime } from "./models.js";
24
+ import { trace } from "./knobs.js";
25
+ /** Default factory: the real SDK, over the shared ModelRuntime. */
26
+ let sessionFactory = async ({ cwd, mode }) => {
27
+ const sdk = (await import("@earendil-works/pi-coding-agent"));
28
+ const modelRuntime = await getPiModelRuntime();
29
+ const sessionManager = mode.kind === "resume" ? sdk.SessionManager.open(mode.sessionFile) : sdk.SessionManager.create(cwd);
30
+ const { session } = await sdk.createAgentSession({
31
+ modelRuntime,
32
+ sessionManager,
33
+ cwd,
34
+ ...(mode.kind === "create" && mode.tools !== undefined && mode.tools.length > 0 ? { tools: mode.tools } : {}),
35
+ });
36
+ return session;
37
+ };
38
+ /** Replace the session factory (tests inject a fake). */
39
+ export function setPiSessionFactory(factory) {
40
+ sessionFactory = factory;
41
+ }
42
+ function ok(command, data) {
43
+ return { type: "response", command, success: true, ...(data === undefined ? {} : { data }) };
44
+ }
45
+ export class PiSessionClient {
46
+ config;
47
+ #session;
48
+ #startPromise;
49
+ #closed = false;
50
+ #listeners = new Set();
51
+ #failureListeners = new Set();
52
+ constructor(config) {
53
+ this.config = config;
54
+ }
55
+ /**
56
+ * Build the client shell. Zero IO: the factory runs only at
57
+ * `ensureStarted()`. `args` is accepted for omp-seam parity and must be
58
+ * empty — unknown values fail loud (the SDK line has no spawn flags).
59
+ */
60
+ static async spawn(args, cwd, mode) {
61
+ if (args.length > 0) {
62
+ throw new Error(`PiSessionClient: unsupported spawn arg "${String(args[0])}" (SDK line)`);
63
+ }
64
+ return new PiSessionClient({ cwd, mode });
65
+ }
66
+ /** Register a wire event listener; returns the unsubscriber. */
67
+ on(listener) {
68
+ this.#listeners.add(listener);
69
+ return () => {
70
+ this.#listeners.delete(listener);
71
+ };
72
+ }
73
+ /** Register a failure listener; returns the unsubscriber. */
74
+ onFailure(listener) {
75
+ this.#failureListeners.add(listener);
76
+ return () => {
77
+ this.#failureListeners.delete(listener);
78
+ };
79
+ }
80
+ /** Whether the underlying pi session exists (and the client is not closed). */
81
+ get spawned() {
82
+ return this.#session !== undefined && !this.#closed;
83
+ }
84
+ /** pi's native session file; `null` until the session materializes it. */
85
+ get sessionFile() {
86
+ return this.#closed || this.#session === undefined ? null : this.#session.sessionFile ?? null;
87
+ }
88
+ /**
89
+ * pi's own system prompt, once the session started (undefined before —
90
+ * callers retry on later steps).
91
+ */
92
+ systemPrompt() {
93
+ if (this.#closed || this.#session === undefined)
94
+ return undefined;
95
+ const prompt = this.#session.systemPrompt;
96
+ return typeof prompt === "string" && prompt.trim() !== "" ? prompt : undefined;
97
+ }
98
+ /** Materialize the session once; memoized after that. */
99
+ ensureStarted() {
100
+ this.#usable();
101
+ this.#startPromise ??= (async () => {
102
+ const session = await sessionFactory(this.config);
103
+ this.#session = session;
104
+ session.subscribe((event) => this.#pump(event));
105
+ trace(`session started mode=${this.config.mode.kind} file=${session.sessionFile ?? "none"}`);
106
+ })();
107
+ return this.#startPromise;
108
+ }
109
+ /** Query streaming state. Does NOT start the session (cold reads stay cold). */
110
+ getState() {
111
+ this.#usable();
112
+ if (this.#session === undefined)
113
+ return { isStreaming: false };
114
+ return { isStreaming: this.#session.isStreaming === true };
115
+ }
116
+ /** Start a run with a user prompt (fire-and-forget past the lazy boundary). */
117
+ async prompt(message) {
118
+ this.#usable();
119
+ await this.ensureStarted();
120
+ void this.#session.prompt(message).catch((error) => this.#fail(error));
121
+ return ok("prompt");
122
+ }
123
+ /** Queue a steering message while streaming (pi supports this natively). */
124
+ async steer(message) {
125
+ this.#usable();
126
+ await this.ensureStarted();
127
+ void this.#session.steer(message).catch((error) => this.#fail(error));
128
+ return ok("steer");
129
+ }
130
+ /** Queue a follow-up for when the agent stops. */
131
+ async followUp(message) {
132
+ this.#usable();
133
+ await this.ensureStarted();
134
+ void this.#session.followUp(message).catch((error) => this.#fail(error));
135
+ return ok("follow_up");
136
+ }
137
+ /**
138
+ * Select the model by its real route pair: the provider slug plus the BARE
139
+ * model id, threaded straight into the shared runtime's `getModel`.
140
+ * Legacy composite `<provider>/<modelId>` selections are resolved
141
+ * upstream (agent.ts) before reaching here. An unknown pair logs and
142
+ * no-ops (never breaks the agent loop mid-session — codex catch
143
+ * semantics); an incomplete pair is likewise a logged no-op.
144
+ */
145
+ async setModel(provider, modelId) {
146
+ this.#usable();
147
+ if (provider === "" || modelId === "") {
148
+ trace(`set_model: incomplete route (${provider}/${modelId}), ignored`);
149
+ return ok("set_model");
150
+ }
151
+ await this.ensureStarted();
152
+ const runtime = await getPiModelRuntime();
153
+ const model = runtime.getModel(provider, modelId);
154
+ if (model === undefined) {
155
+ trace(`set_model: runtime has no ${provider}/${modelId}, ignored`);
156
+ return ok("set_model");
157
+ }
158
+ this.#session.setModel(model);
159
+ trace(`set_model → ${provider}/${modelId}`);
160
+ return ok("set_model");
161
+ }
162
+ /** Set the thinking level (off..max); no-op before the session exists. */
163
+ setThinkingLevel(level) {
164
+ this.#usable();
165
+ this.#session?.setThinkingLevel(level);
166
+ }
167
+ /** Run pi compaction (wired unlike codex); fire-and-forget with failure channel. */
168
+ async compact(customInstructions) {
169
+ this.#usable();
170
+ await this.ensureStarted();
171
+ void this.#session.compact(customInstructions).catch((error) => this.#fail(error));
172
+ return ok("compact");
173
+ }
174
+ /** Abort the current operation; resolves when the session is idle again. */
175
+ async abort() {
176
+ this.#usable();
177
+ if (this.#session !== undefined)
178
+ await this.#session.abort();
179
+ return ok("abort");
180
+ }
181
+ /** Dispose the session. Idempotent; all later operations fail. */
182
+ close() {
183
+ if (this.#closed)
184
+ return;
185
+ this.#closed = true;
186
+ try {
187
+ this.#session?.dispose();
188
+ }
189
+ catch (error) {
190
+ trace(`dispose threw: ${String(error)}`);
191
+ }
192
+ this.#fail(new Error("pi session client closed"));
193
+ }
194
+ // -------------------------------------------------------------------------
195
+ #usable() {
196
+ if (this.#closed)
197
+ throw new Error("pi session client is closed");
198
+ }
199
+ #pump(raw) {
200
+ const wire = projectSessionEvent(raw);
201
+ if (wire === null)
202
+ return;
203
+ for (const listener of [...this.#listeners]) {
204
+ try {
205
+ listener(wire);
206
+ }
207
+ catch (error) {
208
+ process.stderr.write(`pi sdk listener error: ${String(error)}\n`);
209
+ }
210
+ }
211
+ }
212
+ #fail(error) {
213
+ const resolved = error instanceof Error ? error : new Error(String(error));
214
+ trace(`failure: ${resolved.message}`);
215
+ for (const listener of [...this.#failureListeners]) {
216
+ try {
217
+ listener(resolved);
218
+ }
219
+ catch (listenerError) {
220
+ process.stderr.write(`pi sdk failure listener error: ${String(listenerError)}\n`);
221
+ }
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * pi-events — pure projection from pi's `AgentSessionEvent` stream onto the
3
+ * wire vocabulary the omp/codex bridges share (agent_start / turn_start /
4
+ * message_* / tool_execution_* / turn_end / agent_end), plus pi-specific
5
+ * events nothing upstream has: `session_title` (pi owns titles) and the
6
+ * `compaction_start|end` pair (pi compaction is real and wired here).
7
+ *
8
+ * Duck-typed on pi's event shapes (verified against @earendil-works/
9
+ * pi-agent-core dist 0.84.2): the projection never imports the SDK, so tests
10
+ * run without touching pi and future pi point releases fail loudly HERE
11
+ * instead of silently changing the DSH log. Unknown, unmapped, and non-object
12
+ * events project to `null` — never throw into the SDK's subscribe loop.
13
+ */
14
+ import { JSONStringifySafe } from "./safe-json.js";
15
+ // Events pi emits that this adapter deliberately does not project (V1):
16
+ // agent_settled (idle bookkeeping), queue_update (client-internal queue),
17
+ // entry_appended (the log is DSH's), thinking_level_changed, auto_retry_* /
18
+ // summarization_retry_* (folded into the turn's outcome), bash_execution_update.
19
+ function asRecord(value) {
20
+ if (value === null || typeof value !== "object" || Array.isArray(value))
21
+ return null;
22
+ return value;
23
+ }
24
+ function optionalString(value) {
25
+ return typeof value === "string" ? value : undefined;
26
+ }
27
+ function convertContent(blocks) {
28
+ if (!Array.isArray(blocks))
29
+ return [];
30
+ const out = [];
31
+ for (const raw of blocks) {
32
+ const block = asRecord(raw);
33
+ if (block === null)
34
+ continue;
35
+ switch (block["type"]) {
36
+ case "text":
37
+ if (typeof block["text"] === "string")
38
+ out.push({ type: "text", text: block["text"] });
39
+ break;
40
+ case "thinking":
41
+ if (typeof block["thinking"] === "string")
42
+ out.push({ type: "thinking", thinking: block["thinking"] });
43
+ break;
44
+ case "toolCall": {
45
+ // The DSH `tool/call` payload wants the RAW JSON string the model
46
+ // produced; pi parses it into an object, so re-stringify here.
47
+ const args = block["arguments"];
48
+ out.push({
49
+ type: "toolCall",
50
+ id: optionalString(block["id"]) ?? "",
51
+ name: optionalString(block["name"]) ?? "",
52
+ arguments: typeof args === "string" ? args : JSONStringifySafe(args ?? {}),
53
+ });
54
+ break;
55
+ }
56
+ default:
57
+ // images / unknown blocks are not bridged in V1.
58
+ break;
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ function convertMessage(raw) {
64
+ const message = asRecord(raw);
65
+ if (message === null)
66
+ return { role: "unknown", content: [] };
67
+ const wire = {
68
+ role: optionalString(message["role"]) ?? "unknown",
69
+ content: convertContent(message["content"]),
70
+ };
71
+ const provider = optionalString(message["provider"]);
72
+ const model = optionalString(message["model"]);
73
+ if (provider !== undefined)
74
+ wire.provider = provider;
75
+ if (model !== undefined)
76
+ wire.model = model;
77
+ const usage = asRecord(message["usage"]);
78
+ if (usage !== null)
79
+ wire.usage = usage;
80
+ const stopReason = optionalString(message["stopReason"]);
81
+ if (stopReason !== undefined)
82
+ wire.stopReason = stopReason;
83
+ const errorMessage = optionalString(message["errorMessage"]);
84
+ if (errorMessage !== undefined)
85
+ wire.errorMessage = errorMessage;
86
+ if (typeof message["errorStatus"] === "number")
87
+ wire.errorStatus = message["errorStatus"];
88
+ const toolCallId = optionalString(message["toolCallId"]);
89
+ if (toolCallId !== undefined)
90
+ wire.toolCallId = toolCallId;
91
+ if (typeof message["isError"] === "boolean")
92
+ wire.isError = message["isError"];
93
+ return wire;
94
+ }
95
+ /**
96
+ * Project one raw pi event. Returns the wire event, `null` for anything this
97
+ * adapter does not project. Pure and total: no imports from the SDK, no I/O,
98
+ * no throws.
99
+ */
100
+ export function projectSessionEvent(raw) {
101
+ const event = asRecord(raw);
102
+ if (event === null)
103
+ return null;
104
+ switch (event["type"]) {
105
+ case "agent_start":
106
+ return { type: "agent_start" };
107
+ case "agent_end":
108
+ return { type: "agent_end" };
109
+ case "turn_start":
110
+ return { type: "turn_start" };
111
+ case "turn_end":
112
+ return { type: "turn_end" };
113
+ case "message_start":
114
+ return { type: "message_start", message: convertMessage(event["message"]) };
115
+ case "message_end":
116
+ return { type: "message_end", message: convertMessage(event["message"]) };
117
+ case "message_update": {
118
+ const delta = asRecord(event["assistantMessageEvent"]);
119
+ if (delta === null)
120
+ return null;
121
+ return { type: "message_update", assistantMessageEvent: delta };
122
+ }
123
+ case "tool_execution_start": {
124
+ const toolCallId = optionalString(event["toolCallId"]);
125
+ if (toolCallId === undefined)
126
+ return null;
127
+ return {
128
+ type: "tool_execution_start",
129
+ toolCallId,
130
+ toolName: optionalString(event["toolName"]) ?? "",
131
+ args: event["args"],
132
+ };
133
+ }
134
+ case "tool_execution_end": {
135
+ const toolCallId = optionalString(event["toolCallId"]);
136
+ if (toolCallId === undefined)
137
+ return null;
138
+ return {
139
+ type: "tool_execution_end",
140
+ toolCallId,
141
+ toolName: optionalString(event["toolName"]) ?? "",
142
+ result: asRecord(event["result"]) ?? {},
143
+ isError: event["isError"] === true,
144
+ };
145
+ }
146
+ case "session_info_changed":
147
+ return { type: "session_title", name: optionalString(event["name"]) };
148
+ case "compaction_start":
149
+ return { type: "compaction_start", reason: optionalString(event["reason"]) ?? "manual" };
150
+ case "compaction_end":
151
+ return { type: "compaction_end", aborted: event["aborted"] === true };
152
+ default:
153
+ return null;
154
+ }
155
+ }