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,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
|
+
}
|