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.
- package/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- 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
|
+
}
|