@pasko70/pibo 2.0.0 → 2.1.1

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.
@@ -0,0 +1,581 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { unsupportedAgentRuntimeCapability, } from "../../agent-runtime/capabilities.js";
3
+ import { AgentRuntimeAuthError, AgentRuntimeBindingMissingError, AgentRuntimeUnavailableError, } from "../../agent-runtime/errors.js";
4
+ import { OmpAuthController, OMP_AUTH_METHODS, unknownOmpStatusForAdapter } from "./auth.js";
5
+ import { OmpRpcClient, OmpRpcResponseError } from "./client.js";
6
+ import { defaultOmpRuntimeConfig, OMP_RUNTIME_CONFIG_SCHEMA, parseOmpRuntimeConfig } from "./config.js";
7
+ import { OmpHostToolBridge } from "./host-tools.js";
8
+ import { emptyOmpHistoryPage, inspectOmpHistory, readOmpHistory } from "./history.js";
9
+ import { OMP_MODEL_OPTIONS_SCHEMA, OMP_MODEL_PROVIDER_ID, OMP_REASONING_VALUES, parseOmpReasoning, readOmpModelCatalog, setOmpModel, } from "./models.js";
10
+ import { buildOmpProcessEnvironment, diagnoseOmpRuntime, disposeOmpSessionPaths, prepareOmpSessionPaths, resolveOmpCommand, } from "./process.js";
11
+ import { OmpResourceDelivery } from "./resource-delivery.js";
12
+ import { OMP_ADAPTER_ID, OMP_ADAPTER_VERSION, OmpThreadController, readOmpAvailableCommands } from "./thread.js";
13
+ import { OmpRpcTurnController } from "./turn.js";
14
+ export { OMP_ADAPTER_ID } from "./thread.js";
15
+ export const OMP_RUNTIME_PROTOCOL_NAME = "omp-rpc";
16
+ export const OMP_RUNTIME_SUPPORTED_RANGE = "2";
17
+ function ompCapabilities() {
18
+ return {
19
+ lifecycle: {
20
+ persistent: true,
21
+ lazyBinding: false,
22
+ resume: true,
23
+ attach: true,
24
+ listNativeSessions: true,
25
+ fork: true,
26
+ clone: false,
27
+ tree: false,
28
+ },
29
+ input: {
30
+ text: true,
31
+ images: true,
32
+ audio: false,
33
+ steering: true,
34
+ structuredOutput: false,
35
+ },
36
+ output: {
37
+ assistantDeltas: true,
38
+ reasoning: true,
39
+ toolEvents: true,
40
+ usage: true,
41
+ plans: false,
42
+ diffs: false,
43
+ rawNativeEvents: false,
44
+ },
45
+ tools: {
46
+ piboManaged: { support: "direct" },
47
+ nativeToolInspection: {
48
+ support: "degraded",
49
+ mode: "observed-runtime-items",
50
+ reason: "OMP exposes its native tool inventory via get_state.dumpTools and runtime tool_execution items; a complete pre-turn inventory is only partially exposed through RPC.",
51
+ },
52
+ nativeToolYielding: unsupportedAgentRuntimeCapability("OMP native tools remain harness-owned and are not wrapped as Pibo yielded tools."),
53
+ },
54
+ mcp: {
55
+ externalServers: { support: "unsupported", reason: "OMP manages its own MCP; external MCP delivery is not wired in the initial OMP adapter." },
56
+ statusInspection: false,
57
+ },
58
+ skills: { support: "materialized", modes: ["omp-custom-directories"] },
59
+ context: {
60
+ support: "unsupported",
61
+ reason: "OMP loads project context via its own AGENTS.md/rules discovery in the session cwd; Pibo has no injection seam and does not mutate the user's workspace.",
62
+ },
63
+ auth: {
64
+ status: true,
65
+ methods: OMP_AUTH_METHODS,
66
+ cancel: true,
67
+ logout: true,
68
+ credentialScope: "runtime-instance",
69
+ },
70
+ models: {
71
+ catalog: true,
72
+ switchInSession: true,
73
+ optionsSchema: OMP_MODEL_OPTIONS_SCHEMA,
74
+ },
75
+ reasoning: {
76
+ supported: true,
77
+ values: [...OMP_REASONING_VALUES],
78
+ },
79
+ approvals: {
80
+ supported: false,
81
+ structuredUserInput: false,
82
+ },
83
+ maintenance: {
84
+ compaction: true,
85
+ contextUsage: true,
86
+ history: true,
87
+ health: true,
88
+ },
89
+ };
90
+ }
91
+ export const OMP_RUNTIME_CAPABILITIES = ompCapabilities();
92
+ function validateOpenBinding(input, runtimeInstanceId) {
93
+ const binding = input.binding
94
+ ? structuredClone(input.binding)
95
+ : {
96
+ piboSessionId: input.piboSession.id,
97
+ runtimeInstanceId,
98
+ adapterId: OMP_ADAPTER_ID,
99
+ state: "unbound",
100
+ };
101
+ if (binding.piboSessionId !== input.piboSession.id) {
102
+ throw new AgentRuntimeUnavailableError(runtimeInstanceId, "The OMP binding belongs to a different Pibo Session.");
103
+ }
104
+ if (binding.runtimeInstanceId !== runtimeInstanceId || binding.adapterId !== OMP_ADAPTER_ID) {
105
+ throw new AgentRuntimeUnavailableError(runtimeInstanceId, "The OMP binding does not match the configured runtime instance.");
106
+ }
107
+ if (binding.state === "missing") {
108
+ throw new AgentRuntimeBindingMissingError(binding.piboSessionId, runtimeInstanceId, binding.nativeSessionId);
109
+ }
110
+ if (binding.state === "error") {
111
+ throw new AgentRuntimeUnavailableError(runtimeInstanceId, "The persisted OMP binding is in an error state.");
112
+ }
113
+ return binding;
114
+ }
115
+ function bindingForOmp(piboSessionId, runtimeInstanceId, previous) {
116
+ return {
117
+ ...(previous ? structuredClone(previous) : {}),
118
+ piboSessionId,
119
+ runtimeInstanceId,
120
+ adapterId: OMP_ADAPTER_ID,
121
+ protocol: OMP_RUNTIME_PROTOCOL_NAME,
122
+ protocolVersion: OMP_RUNTIME_SUPPORTED_RANGE,
123
+ adapterVersion: OMP_ADAPTER_VERSION,
124
+ locator: { kind: "adapter-resolved" },
125
+ state: "bound",
126
+ };
127
+ }
128
+ export class OmpSession {
129
+ runtimeInstanceId;
130
+ bundle;
131
+ config;
132
+ emitWarning;
133
+ adapter;
134
+ adapterId = OMP_ADAPTER_ID;
135
+ cwd;
136
+ capabilities;
137
+ controls;
138
+ listeners = new Set();
139
+ binding;
140
+ disposed = false;
141
+ operationInFlight = false;
142
+ client;
143
+ paths;
144
+ turn;
145
+ thread;
146
+ hostTools;
147
+ resourceDelivery;
148
+ constructor(runtimeInstanceId, bundle, config, emitWarning, adapter) {
149
+ this.runtimeInstanceId = runtimeInstanceId;
150
+ this.bundle = bundle;
151
+ this.config = config;
152
+ this.emitWarning = emitWarning;
153
+ this.adapter = adapter;
154
+ this.client = bundle.client;
155
+ this.paths = bundle.paths;
156
+ this.thread = bundle.threads;
157
+ this.resourceDelivery = bundle.resourceDelivery;
158
+ this.cwd = bundle.threads.current.cwd;
159
+ this.binding = bindingForOmp("", runtimeInstanceId, undefined);
160
+ this.capabilities = ompCapabilities();
161
+ this.turn = new OmpRpcTurnController(this.client, (event) => this.emit(event));
162
+ this.hostTools = new OmpHostToolBridge(this.client, undefined, this.toolExecutionContext(), (m) => this.emitWarning(m));
163
+ this.controls = {
164
+ getCurrentSession: () => this.thread.getSessionSnapshot(this.runtimeInstanceId),
165
+ listSessions: () => this.thread.listSessions(this.runtimeInstanceId),
166
+ getForkCandidates: () => this.forkCandidates(),
167
+ forkSession: async (entryId) => await this.runIdleOperation(async () => {
168
+ const previous = this.thread.getSessionSnapshot(this.runtimeInstanceId);
169
+ const result = await this.thread.forkSession(this.runtimeInstanceId, entryId);
170
+ this.updateBinding();
171
+ return { previous, current: result.current, cancelled: result.cancelled };
172
+ }),
173
+ getReasoning: () => parseOmpReasoning(undefined),
174
+ setReasoning: (value) => {
175
+ this.assertIdle();
176
+ const info = parseOmpReasoning(value);
177
+ // Send the real OMP thinking-level change (best-effort; contract is sync).
178
+ const level = info.value ?? "medium";
179
+ void this.client.request({ type: "set_thinking_level", level }, "set_thinking_level").catch((error) => {
180
+ this.emitWarning("OMP set_thinking_level failed: " + (error instanceof Error ? error.message : String(error)));
181
+ });
182
+ return info;
183
+ },
184
+ setFastMode: (enabled) => {
185
+ this.assertIdle();
186
+ void this.client.request({ type: "set_fast_mode", enabled }, "set_fast_mode").catch((error) => {
187
+ this.emitWarning("OMP set_fast_mode failed: " + (error instanceof Error ? error.message : String(error)));
188
+ });
189
+ return { mode: enabled ? "fast" : "normal", supported: true, changed: true };
190
+ },
191
+ getFastMode: () => ({ mode: "normal", supported: true }),
192
+ setModel: async (model) => await this.runIdleOperation(async () => {
193
+ const provider = model.provider ?? this.config.defaultProvider ?? "";
194
+ const modelId = model.id ?? this.config.defaultModel ?? "";
195
+ if (!provider || !modelId) {
196
+ throw new Error("OMP model switch requires a provider and model id.");
197
+ }
198
+ await setOmpModel(this.client, provider, modelId);
199
+ return { provider, id: modelId };
200
+ }),
201
+ compact: async (customInstructions) => {
202
+ this.assertIdle();
203
+ return await this.client.request({ type: "compact", ...(customInstructions ? { customInstructions } : {}) }, "compact");
204
+ },
205
+ };
206
+ }
207
+ toolExecutionContext() {
208
+ return {
209
+ cwd: this.cwd,
210
+ runtimeInstanceId: this.runtimeInstanceId,
211
+ adapterId: OMP_ADAPTER_ID,
212
+ };
213
+ }
214
+ forkCandidates() {
215
+ return this.thread.cachedForkCandidates();
216
+ }
217
+ async setOmpModel(provider, modelId) {
218
+ const info = await setOmpModel(this.client, provider, modelId);
219
+ return info;
220
+ }
221
+ updateBinding() {
222
+ const snapshot = this.thread.getSessionSnapshot(this.runtimeInstanceId);
223
+ if (snapshot.nativeSessionId) {
224
+ this.binding = bindingForOmp(this.binding.piboSessionId, this.runtimeInstanceId, this.binding);
225
+ this.binding = {
226
+ ...this.binding,
227
+ nativeSessionId: snapshot.nativeSessionId,
228
+ locator: snapshot.locator,
229
+ metadata: {
230
+ ...(this.binding.metadata ?? {}),
231
+ nativePresenceExpected: true,
232
+ ...(snapshot.name ? { sessionName: snapshot.name } : {}),
233
+ },
234
+ };
235
+ }
236
+ }
237
+ setPiboSessionId(sessionId) {
238
+ this.binding = { ...this.binding, piboSessionId: sessionId };
239
+ }
240
+ /** Apply the resolved native session id from OMP state. */
241
+ bindNativeSessionId(sessionId) {
242
+ this.binding = {
243
+ ...this.binding,
244
+ nativeSessionId: sessionId,
245
+ state: "bound",
246
+ locator: { kind: "adapter-resolved", value: sessionId },
247
+ };
248
+ }
249
+ /**
250
+ * Persist the on-disk OMP transcript path so a later `openSession` can pass
251
+ * it to `switch_session` (which expects the .jsonl file path, NOT the
252
+ * nativeSessionId UUID) to resume the same transcript.
253
+ */
254
+ bindNativeSessionFile(sessionFile) {
255
+ if (!sessionFile)
256
+ return;
257
+ this.binding = {
258
+ ...this.binding,
259
+ metadata: { ...(this.binding.metadata ?? {}), nativeSessionFile: sessionFile },
260
+ };
261
+ }
262
+ /** Wire a real host-tool bridge backed by the Pibo portable tool session. */
263
+ attachHostToolBridge(bridge) {
264
+ this.hostTools = bridge;
265
+ }
266
+ /** Adapter-level reads route history/models/auth through the live client. */
267
+ getClient() {
268
+ return this.client;
269
+ }
270
+ getBinding() {
271
+ this.updateBinding();
272
+ return structuredClone(this.binding);
273
+ }
274
+ subscribe(listener) {
275
+ this.assertActive();
276
+ this.listeners.add(listener);
277
+ return () => this.listeners.delete(listener);
278
+ }
279
+ emit(event) {
280
+ for (const listener of this.listeners)
281
+ listener(event);
282
+ }
283
+ async prompt(input) {
284
+ this.assertActive();
285
+ this.operationInFlight = true;
286
+ try {
287
+ await this.turn.prompt(input.text);
288
+ this.updateBinding();
289
+ }
290
+ finally {
291
+ this.operationInFlight = false;
292
+ }
293
+ }
294
+ async steer(input) {
295
+ this.assertActive();
296
+ await this.turn.steer(input.text);
297
+ }
298
+ async abort() {
299
+ this.assertActive();
300
+ await this.turn.interrupt();
301
+ }
302
+ async dispose() {
303
+ if (this.disposed)
304
+ return;
305
+ this.disposed = true;
306
+ this.turn.dispose();
307
+ await this.hostTools.cancelAll();
308
+ this.hostTools.dispose();
309
+ this.client.dispose();
310
+ await disposeOmpSessionPaths(this.paths);
311
+ // Notify the owning adapter so adapter-level reads stop routing to us.
312
+ this.adapter?.detachLiveSession(this);
313
+ }
314
+ getStatus() {
315
+ const hostInstalled = this.hostTools?.installedNames ?? [];
316
+ // Report the tools Pibo actually mounted (host-tool bridge). OMP's own
317
+ // native tools (bash/edit/…) remain engine-owned and are intentionally
318
+ // not exported here — we do not claim an inventory we do not observe.
319
+ return {
320
+ streaming: this.turn.streaming,
321
+ enabledTools: hostInstalled,
322
+ cwd: this.cwd,
323
+ reasoning: {
324
+ supported: true,
325
+ availableValues: [...OMP_REASONING_VALUES],
326
+ },
327
+ };
328
+ }
329
+ assertIdle() {
330
+ this.assertActive();
331
+ if (this.operationInFlight)
332
+ throw new Error("OMP session is busy with another operation.");
333
+ }
334
+ assertActive() {
335
+ if (this.disposed)
336
+ throw new Error("OMP session is disposed.");
337
+ }
338
+ async runIdleOperation(operation) {
339
+ this.assertIdle();
340
+ this.operationInFlight = true;
341
+ try {
342
+ return await operation();
343
+ }
344
+ finally {
345
+ this.operationInFlight = false;
346
+ }
347
+ }
348
+ }
349
+ export class OmpAgentRuntimeAdapter {
350
+ instanceId;
351
+ descriptor;
352
+ config;
353
+ displayName;
354
+ enabled;
355
+ parsed;
356
+ /** Handle to the currently-open session so history/auth/models route to it. */
357
+ live;
358
+ constructor(input, driver) {
359
+ this.instanceId = input.instanceId;
360
+ this.descriptor = driver.descriptor;
361
+ this.config = structuredClone(input.config);
362
+ this.displayName = input.displayName;
363
+ this.enabled = input.enabled;
364
+ this.parsed = parseOmpRuntimeConfig(input.config);
365
+ }
366
+ async diagnose() {
367
+ return await diagnoseOmpRuntime(this.parsed, this.instanceId);
368
+ }
369
+ validateProfile(input) {
370
+ // Truthful capability validation is delegated to the profile resolver;
371
+ // unsupported selections are rejected by the registry.
372
+ return [];
373
+ }
374
+ async openSession(input) {
375
+ const binding = validateOpenBinding(input, this.instanceId);
376
+ if (binding.state === "bound" && !binding.nativeSessionId) {
377
+ throw new AgentRuntimeUnavailableError(this.instanceId, "The persisted OMP binding has no native session id.");
378
+ }
379
+ const paths = await prepareOmpSessionPaths({
380
+ config: this.parsed,
381
+ runtimeInstanceId: this.instanceId,
382
+ piboSessionId: input.piboSession.id,
383
+ sessionGeneration: randomUUID(),
384
+ });
385
+ // Materialize BEFORE spawn (MUST-FIX #3): OMP reads config.yml at startup.
386
+ const resourceDelivery = new OmpResourceDelivery(this.parsed, paths, input.services?.resources);
387
+ await resourceDelivery.prepare();
388
+ const environment = buildOmpProcessEnvironment({
389
+ paths,
390
+ config: this.parsed,
391
+ baseEnvironment: process.env,
392
+ });
393
+ const command = resolveOmpCommand(this.parsed, paths);
394
+ const client = new OmpRpcClient({
395
+ startupTimeoutMs: this.parsed.startupTimeoutMs,
396
+ requestTimeoutMs: this.parsed.requestTimeoutMs,
397
+ });
398
+ try {
399
+ await client.connect(command, { cwd: input.workspace, env: environment });
400
+ }
401
+ catch (error) {
402
+ await client.dispose();
403
+ await disposeOmpSessionPaths(paths);
404
+ if (error instanceof OmpRpcResponseError)
405
+ throw error;
406
+ throw new AgentRuntimeUnavailableError(this.instanceId, `Failed to start OMP: ${error instanceof Error ? error.message : String(error)}`);
407
+ }
408
+ // Determine native session id (+ transcript file path for later resume).
409
+ let nativeSessionId = binding.nativeSessionId;
410
+ let nativeSessionFile;
411
+ try {
412
+ const state = await client.request({ type: "get_state" }, "get_state");
413
+ const data = state["data"];
414
+ if (data && typeof data === "object" && !Array.isArray(data)) {
415
+ const record = data;
416
+ if (typeof record.sessionId === "string")
417
+ nativeSessionId = record.sessionId;
418
+ if (typeof record.sessionFile === "string")
419
+ nativeSessionFile = record.sessionFile;
420
+ }
421
+ }
422
+ catch {
423
+ // state is best-effort; binding stays as resolved
424
+ }
425
+ const initial = {
426
+ sessionId: nativeSessionId ?? binding.nativeSessionId ?? randomUUID(),
427
+ cwd: input.workspace,
428
+ };
429
+ // Build the session and thread controllers with a client that supports
430
+ // host-tool frames.
431
+ const threads = new OmpThreadController(client, input.workspace, { sessionId: initial.sessionId });
432
+ const bundle = { client, paths, threads, resourceDelivery };
433
+ const session = new OmpSession(this.instanceId, bundle, this.parsed, (m) => {
434
+ // Warning surfaced via session events is delivered by the turn controller.
435
+ }, this);
436
+ session.setPiboSessionId(input.piboSession.id);
437
+ // Wire host tools after session construction so the executor is available.
438
+ const portableTools = input.services?.portableTools;
439
+ // Rebuild the session's hostTools with the real portable session (the
440
+ // constructor used a placeholder). We recreate the bridge to avoid keeping
441
+ // a hidden reference.
442
+ const hb = new OmpHostToolBridge(client, portableTools, {
443
+ cwd: input.workspace,
444
+ runtimeInstanceId: this.instanceId,
445
+ adapterId: OMP_ADAPTER_ID,
446
+ }, (m) => session["emitWarning"]?.(m));
447
+ client.subscribeFrames((frame) => {
448
+ if (frame && typeof frame === "object" && frame.type === "host_tool_call") {
449
+ void hb.handleFrame(frame);
450
+ }
451
+ });
452
+ try {
453
+ await hb.install();
454
+ await threads.refresh();
455
+ // Resume/F4: if this Pibo Session was previously bound to an OMP
456
+ // native session, switch the new child into that persisted transcript
457
+ // so history/context carry over instead of starting a fresh session.
458
+ if (binding.state === "bound" && binding.nativeSessionId) {
459
+ // switch_session takes the .jsonl transcript PATH, not the session
460
+ // id UUID. Prefer the persisted transcript file (F4); fall back to
461
+ // the id only when no file was recorded.
462
+ const resumePath = (binding.metadata && typeof binding.metadata.nativeSessionFile === "string"
463
+ ? binding.metadata.nativeSessionFile
464
+ : undefined) ?? binding.nativeSessionId;
465
+ try {
466
+ await client.request({ type: "switch_session", sessionPath: resumePath }, "switch_session");
467
+ await threads.refresh();
468
+ // OMP regenerates the session id on switch but restores the
469
+ // transcript FILE. Re-read state so we persist the RESUMED
470
+ // transcript path (not the fresh pre-switch session's file).
471
+ const resumed = await client.request({ type: "get_state" }, "get_state");
472
+ const resumedData = resumed["data"];
473
+ if (resumedData && typeof resumedData === "object" && !Array.isArray(resumedData)) {
474
+ const rr = resumedData;
475
+ if (typeof rr.sessionFile === "string")
476
+ nativeSessionFile = rr.sessionFile;
477
+ }
478
+ }
479
+ catch (resumeError) {
480
+ // Keep the fresh session; a failed switch is not fatal.
481
+ // (bindNativeSessionId below still sets the binding.)
482
+ }
483
+ }
484
+ // Prime fork candidates (get_branch_messages) for the sync SPI.
485
+ void threads.loadForkCandidates(this.instanceId);
486
+ }
487
+ catch (error) {
488
+ await client.dispose();
489
+ await disposeOmpSessionPaths(paths);
490
+ throw error;
491
+ }
492
+ session.attachHostToolBridge(hb);
493
+ session.bindNativeSessionId(threads.current.sessionId);
494
+ session.bindNativeSessionFile(nativeSessionFile);
495
+ this.attachLiveSession(session);
496
+ return session;
497
+ }
498
+ /** Record the live session so adapter-level reads can route to it. */
499
+ attachLiveSession(session) {
500
+ this.live = session;
501
+ }
502
+ detachLiveSession(session) {
503
+ if (this.live === session)
504
+ this.live = undefined;
505
+ }
506
+ async listModels() {
507
+ if (this.live) {
508
+ try {
509
+ return await readOmpModelCatalog(this.live.getClient(), this.instanceId);
510
+ }
511
+ catch {
512
+ // fall through to empty on transient engine error
513
+ }
514
+ }
515
+ return { runtimeInstanceId: this.instanceId, models: [] };
516
+ }
517
+ async getAuthStatus() {
518
+ if (this.live) {
519
+ try {
520
+ const controller = new OmpAuthController(this.live.getClient());
521
+ return await controller.getStatus();
522
+ }
523
+ catch {
524
+ // fall through to unknown on transient engine error
525
+ }
526
+ }
527
+ return [unknownOmpStatusForAdapter()];
528
+ }
529
+ async startAuth(input) {
530
+ throw new AgentRuntimeAuthError("orp_auth_unavailable", "OMP auth requires an open session.");
531
+ }
532
+ async cancelAuth(input) {
533
+ return { providerId: input.providerId, configured: false, state: "disconnected", message: "Login canceled." };
534
+ }
535
+ async logoutAuth(input) {
536
+ return { providerId: input.providerId, configured: false, state: "disconnected" };
537
+ }
538
+ async inspectHistory(input) {
539
+ return inspectOmpHistory(input, this.instanceId);
540
+ }
541
+ async readHistory(input) {
542
+ if (this.live) {
543
+ return await readOmpHistory(this.live.getClient(), input, this.instanceId, input.binding);
544
+ }
545
+ return emptyOmpHistoryPage(this.instanceId);
546
+ }
547
+ async resolveBinding() {
548
+ return {
549
+ piboSessionId: "",
550
+ runtimeInstanceId: this.instanceId,
551
+ adapterId: OMP_ADAPTER_ID,
552
+ state: "unbound",
553
+ };
554
+ }
555
+ }
556
+ export const OMP_AGENT_RUNTIME_DRIVER = {
557
+ descriptor: {
558
+ id: OMP_ADAPTER_ID,
559
+ displayName: "Oh My Pi",
560
+ transport: "stdio-rpc",
561
+ configSchema: OMP_RUNTIME_CONFIG_SCHEMA,
562
+ capabilities: ompCapabilities(),
563
+ protocol: { name: OMP_RUNTIME_PROTOCOL_NAME, supportedRange: OMP_RUNTIME_SUPPORTED_RANGE },
564
+ supportsMultipleInstances: true,
565
+ },
566
+ defaultConfig() {
567
+ return defaultOmpRuntimeConfig();
568
+ },
569
+ parseConfig(value) {
570
+ return parseOmpRuntimeConfig(value);
571
+ },
572
+ create(input) {
573
+ return new OmpAgentRuntimeAdapter({ instanceId: input.instanceId, displayName: input.displayName ?? "Oh My Pi", enabled: input.enabled, config: input.config }, { descriptor: OMP_AGENT_RUNTIME_DRIVER.descriptor });
574
+ },
575
+ };
576
+ void OMP_MODEL_PROVIDER_ID;
577
+ void OmpAuthController;
578
+ void OmpRpcResponseError;
579
+ void readOmpAvailableCommands;
580
+ void inspectOmpHistory;
581
+ void OMP_RUNTIME_CAPABILITIES;
@@ -0,0 +1,109 @@
1
+ import { AgentRuntimeAuthError } from "../../agent-runtime/errors.js";
2
+ function isRecord(value) {
3
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ export const OMP_AUTH_METHODS = [
6
+ { id: "api_key", completion: "immediate" },
7
+ ];
8
+ /**
9
+ * OMP auth status/surface.
10
+ *
11
+ * V1 truthfulness: OMP's RPC exposes `get_login_providers`/`login`. We declare
12
+ * `api_key` (immediate, env / models.yml configured keys) as the supported
13
+ * method. OAuth / device-code browser-step methods are surfaced only when the
14
+ * `open_url` extension_ui_request bridge is wired; until then they are not
15
+ * advertised.
16
+ */
17
+ export class OmpAuthController {
18
+ client;
19
+ flowCounter = 0;
20
+ constructor(client) {
21
+ this.client = client;
22
+ }
23
+ async getStatus() {
24
+ try {
25
+ const result = await this.client.request({ type: "get_login_providers" }, "get_login_providers");
26
+ const data = result["data"];
27
+ if (!isRecord(data) || !Array.isArray(data.providers)) {
28
+ return [unknownOmpStatus()];
29
+ }
30
+ const statuses = [];
31
+ for (const provider of data.providers) {
32
+ if (!isRecord(provider) || typeof provider.id !== "string")
33
+ continue;
34
+ const authenticated = provider.authenticated === true;
35
+ const available = provider.available === true;
36
+ statuses.push({
37
+ id: provider.id,
38
+ displayName: typeof provider.name === "string" ? provider.name : provider.id,
39
+ state: ompAuthState(authenticated, available),
40
+ configured: authenticated,
41
+ methods: OMP_AUTH_METHODS,
42
+ details: { accountType: authenticated ? "unknown" : "api_key" },
43
+ });
44
+ }
45
+ return statuses.length > 0 ? statuses : [unknownOmpStatus()];
46
+ }
47
+ catch {
48
+ return [unknownOmpStatus()];
49
+ }
50
+ }
51
+ async start(input) {
52
+ if (input.method === "api_key") {
53
+ // API-key auth is configured via provider config (env / models.yml);
54
+ // report as connected when a provider is selected.
55
+ return {
56
+ providerId: input.providerId,
57
+ state: "connected",
58
+ configured: true,
59
+ details: { accountType: "api_key" },
60
+ };
61
+ }
62
+ // OAuth/device-code: the browser step is surfaced via an open_url
63
+ // extension_ui_request only when the bridge is wired. Until then, report
64
+ // as unsupported rather than inventing support (truthful capability).
65
+ throw new AgentRuntimeAuthError("orp_auth_unsupported", `OMP OAuth/device-code authentication (${input.method}) is not wired in the OMP adapter; use an API key.`);
66
+ }
67
+ async complete(input) {
68
+ throw new AgentRuntimeAuthError("orp_auth_unsupported", "OMP API-key auth completes via provider configuration, not an RPC completion step.");
69
+ }
70
+ async cancel(input) {
71
+ return { providerId: input.providerId, state: "disconnected", configured: false, message: "Login canceled." };
72
+ }
73
+ async logout(input) {
74
+ // OMP persists credentials in its own store; Pibo reports the action
75
+ // without deleting user-global OMP state.
76
+ return { providerId: input.providerId, state: "disconnected", configured: false };
77
+ }
78
+ async dispose() {
79
+ // no owned resources
80
+ }
81
+ }
82
+ function ompAuthState(authenticated, available) {
83
+ if (authenticated)
84
+ return "connected";
85
+ if (!available)
86
+ return "unsupported";
87
+ return "disconnected";
88
+ }
89
+ function unknownOmpStatus() {
90
+ return {
91
+ id: "orp",
92
+ displayName: "OMP providers",
93
+ state: "unsupported",
94
+ configured: false,
95
+ methods: OMP_AUTH_METHODS,
96
+ details: { accountType: "api_key" },
97
+ };
98
+ }
99
+ /** Product-safe fallback status when the engine is unavailable. */
100
+ export function unknownOmpStatusForAdapter() {
101
+ return {
102
+ id: "orp",
103
+ displayName: "OMP providers",
104
+ state: "unsupported",
105
+ configured: false,
106
+ methods: OMP_AUTH_METHODS,
107
+ details: { accountType: "api_key" },
108
+ };
109
+ }