pi-harness-runtime 0.2.0 → 0.3.0

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/index.ts CHANGED
@@ -19,16 +19,60 @@ import type {
19
19
  ExtensionCommandContext,
20
20
  } from "@earendil-works/pi-coding-agent";
21
21
  import { UsageTracker } from "./tracker.ts";
22
- import { MirrorStore, type MirrorRecord } from "./mirror.ts";
22
+ import { MirrorStore } from "./mirror.ts";
23
23
  import { aggregateWindows } from "./windows.ts";
24
24
  import { renderStatus } from "./renderer.ts";
25
25
  import { buildMirrorRecord, parseSyncValues } from "./sync-form.ts";
26
+ import {
27
+ JobStateMachine,
28
+ type CheckpointManager,
29
+ } from "./harness/job-state-machine.ts";
30
+ import { TaskGraphManager } from "./harness/task-graph.ts";
31
+ import { MasterPlanner } from "./harness/master-planner.ts";
32
+ import { RepairEngine } from "./harness/repair-engine.ts";
33
+ import {
34
+ type SharedBlackboard,
35
+ createBlackboard,
36
+ } from "./harness/blackboard.ts";
37
+ import { homedir } from "node:os";
38
+ import { existsSync, mkdirSync } from "node:fs";
39
+ import { join } from "node:path";
26
40
 
27
41
  const PROVIDER_DEFAULT = "minimax"; // can be changed via /usage sync form
28
42
 
43
+ // ─── Harness Runtime State ────────────────────────────────────────────
44
+ const HARNESS_ROOT_DIR = join(homedir(), ".pi", "harness");
45
+
46
+ interface HarnessSession {
47
+ jobId: string;
48
+ machine: JobStateMachine;
49
+ graph: TaskGraphManager;
50
+ blackboard: SharedBlackboard;
51
+ repairEngine: RepairEngine;
52
+ createdAt: string;
53
+ }
54
+
55
+ let currentSession: HarnessSession | null = null;
56
+
57
+ function ensureHarnessDir() {
58
+ if (!existsSync(HARNESS_ROOT_DIR)) {
59
+ mkdirSync(HARNESS_ROOT_DIR, { recursive: true });
60
+ }
61
+ }
62
+
63
+ async function getCheckpointManager(): Promise<CheckpointManager> {
64
+ const { JsonCheckpointManager } = await import(
65
+ "./packages/checkpoint/src/checkpoint-manager.ts"
66
+ );
67
+ return new JsonCheckpointManager(
68
+ HARNESS_ROOT_DIR,
69
+ ) as unknown as CheckpointManager;
70
+ }
71
+
29
72
  export default function (pi: ExtensionAPI) {
30
73
  const tracker = new UsageTracker();
31
74
  const mirrorStore = new MirrorStore();
75
+ ensureHarnessDir();
32
76
 
33
77
  // ─── Auto-track every assistant message ──────────────────────────────
34
78
  pi.on("message_end", async (event, ctx) => {
@@ -165,6 +209,255 @@ export default function (pi: ExtensionAPI) {
165
209
  },
166
210
  });
167
211
 
212
+ // ─── /harness start — Start a new harness job ──────────────────────
213
+ pi.registerCommand("harness-start", {
214
+ description: "Start a new harness job: /harness start <requirement>",
215
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
216
+ if (!args.trim()) {
217
+ ctx.ui.notify("Usage: /harness start <requirement>", "error");
218
+ return;
219
+ }
220
+
221
+ const jobId = `job-${Date.now()}`;
222
+ const requirement = args.trim();
223
+
224
+ ctx.ui.notify(`Starting harness job ${jobId}...`, "info");
225
+
226
+ try {
227
+ const cm = await getCheckpointManager();
228
+ const machine = new JobStateMachine({ checkpointManager: cm });
229
+ const result = await machine.createJob(jobId, requirement);
230
+
231
+ if (!result.success) {
232
+ ctx.ui.notify(`Failed to create job: ${result.error}`, "error");
233
+ return;
234
+ }
235
+
236
+ // Create task graph using heuristic planner
237
+ const planner = new MasterPlanner();
238
+ const planResult = await planner.createPlan(
239
+ requirement,
240
+ jobId,
241
+ HARNESS_ROOT_DIR,
242
+ );
243
+
244
+ if (!planResult.success) {
245
+ ctx.ui.notify(`Failed to create plan: ${planResult.error}`, "error");
246
+ return;
247
+ }
248
+
249
+ // Create blackboard
250
+ const blackboard = createBlackboard(
251
+ jobId,
252
+ HARNESS_ROOT_DIR,
253
+ planResult.graph!,
254
+ );
255
+
256
+ // Create repair engine
257
+ const repairEngine = new RepairEngine(HARNESS_ROOT_DIR);
258
+
259
+ // Store session
260
+ currentSession = {
261
+ jobId,
262
+ machine,
263
+ graph: new TaskGraphManager({ jobId }),
264
+ blackboard,
265
+ repairEngine,
266
+ createdAt: new Date().toISOString(),
267
+ };
268
+
269
+ const taskCount = planResult.graph?.nodes
270
+ ? Object.keys(planResult.graph.nodes).length
271
+ : 0;
272
+ ctx.ui.notify(
273
+ `Job ${jobId} created with ${taskCount} tasks.\n` +
274
+ `Requirement: ${requirement}\n\n` +
275
+ `Run /harness status to see tasks, or /harness tasks to list them.`,
276
+ "info",
277
+ );
278
+ } catch (e) {
279
+ ctx.ui.notify(`Error starting harness: ${e}`, "error");
280
+ }
281
+ },
282
+ });
283
+
284
+ // ─── /harness status — Show harness job status ─────────────────────
285
+ pi.registerCommand("harness-status", {
286
+ description: "Show current harness job status",
287
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
288
+ if (!currentSession) {
289
+ ctx.ui.notify(
290
+ "No active harness job. Run /harness start <requirement> to begin.",
291
+ "info",
292
+ );
293
+ return;
294
+ }
295
+
296
+ const summary = currentSession.machine.getStatusSummary();
297
+ if (!summary) {
298
+ ctx.ui.notify("Failed to get job status.", "error");
299
+ return;
300
+ }
301
+
302
+ const progress = currentSession.graph.getProgressSummary();
303
+ const lines = [
304
+ `Harness Job Status`,
305
+ `${"─".repeat(40)}`,
306
+ `Job ID: ${currentSession.jobId}`,
307
+ `Status: ${summary.status}`,
308
+ `Terminal: ${summary.isTerminal ? "Yes" : "No"}`,
309
+ `Can Resume: ${summary.canResume ? "Yes" : "No"}`,
310
+ `${"─".repeat(40)}`,
311
+ `Tasks: ${progress.done}/${progress.total} done, ${progress.running} running, ${progress.failed} failed`,
312
+ `Created: ${currentSession.createdAt}`,
313
+ `${"─".repeat(40)}`,
314
+ `Run /harness tasks for task list`,
315
+ ];
316
+ ctx.ui.notify(lines.join("\n"), "info");
317
+ },
318
+ });
319
+
320
+ // ─── /harness tasks — List all tasks ───────────────────────────────
321
+ pi.registerCommand("harness-tasks", {
322
+ description: "List all tasks in the current harness job",
323
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
324
+ if (!currentSession) {
325
+ ctx.ui.notify(
326
+ "No active harness job. Run /harness start <requirement> to begin.",
327
+ "info",
328
+ );
329
+ return;
330
+ }
331
+
332
+ const tasks = currentSession.graph.getAllTasks();
333
+ if (tasks.length === 0) {
334
+ ctx.ui.notify(
335
+ "No tasks found. The job may not have been planned yet.",
336
+ "info",
337
+ );
338
+ return;
339
+ }
340
+
341
+ const lines = [
342
+ `Tasks for Job ${currentSession.jobId}`,
343
+ `${"─".repeat(50)}`,
344
+ ];
345
+
346
+ for (const task of tasks) {
347
+ const status = task.status.padEnd(10);
348
+ const retry = task.retryCount ? ` (${task.retryCount} retries)` : "";
349
+ lines.push(`[${task.id}] ${status} ${task.title}${retry}`);
350
+ if (task.dependencies.length > 0) {
351
+ lines.push(` deps: ${task.dependencies.join(", ")}`);
352
+ }
353
+ }
354
+
355
+ lines.push(`${"─".repeat(50)}`);
356
+ const ready = currentSession.graph.getReadyTasks();
357
+ lines.push(`${ready.length} tasks ready to execute.`);
358
+ ctx.ui.notify(lines.join("\n"), "info");
359
+ },
360
+ });
361
+
362
+ // ─── /harness pause — Pause the harness job ───────────────────────
363
+ pi.registerCommand("harness-pause", {
364
+ description: "Pause the current harness job",
365
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
366
+ if (!currentSession) {
367
+ ctx.ui.notify("No active harness job to pause.", "info");
368
+ return;
369
+ }
370
+
371
+ const checkpoint = currentSession.machine.getCheckpoint();
372
+ if (!checkpoint) {
373
+ ctx.ui.notify("Failed to get checkpoint.", "error");
374
+ return;
375
+ }
376
+
377
+ const result = await currentSession.machine.transition("paused_quota");
378
+ if (!result.success) {
379
+ ctx.ui.notify(`Failed to pause: ${result.error}`, "error");
380
+ return;
381
+ }
382
+
383
+ ctx.ui.notify(
384
+ `Job ${currentSession.jobId} paused.\n` +
385
+ `Current status: paused_quota\n` +
386
+ `Run /harness resume to continue.`,
387
+ "info",
388
+ );
389
+ },
390
+ });
391
+
392
+ // ─── /harness resume — Resume the harness job ───────────────────────
393
+ pi.registerCommand("harness-resume", {
394
+ description: "Resume a paused harness job",
395
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
396
+ if (!currentSession) {
397
+ ctx.ui.notify("No active harness job to resume.", "info");
398
+ return;
399
+ }
400
+
401
+ const checkpoint = currentSession.machine.getCheckpoint();
402
+ if (!checkpoint || checkpoint.status !== "paused_quota") {
403
+ ctx.ui.notify(
404
+ "Job is not paused. Run /harness start to begin a new job.",
405
+ "info",
406
+ );
407
+ return;
408
+ }
409
+
410
+ const result = await currentSession.machine.transition("running");
411
+ if (!result.success) {
412
+ ctx.ui.notify(`Failed to resume: ${result.error}`, "error");
413
+ return;
414
+ }
415
+
416
+ ctx.ui.notify(
417
+ `Job ${currentSession.jobId} resumed.\n` +
418
+ `Current status: running\n` +
419
+ `Run /harness status to monitor progress.`,
420
+ "info",
421
+ );
422
+ },
423
+ });
424
+
425
+ // ─── /harness cancel — Cancel the harness job ──────────────────────
426
+ pi.registerCommand("harness-cancel", {
427
+ description: "Cancel the current harness job",
428
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
429
+ if (!currentSession) {
430
+ ctx.ui.notify("No active harness job to cancel.", "info");
431
+ return;
432
+ }
433
+
434
+ const ok = await ctx.ui.confirm(
435
+ `Cancel job ${currentSession.jobId}?`,
436
+ "This will mark the job as cancelled. Task state is preserved but work stops.",
437
+ );
438
+
439
+ if (!ok) {
440
+ ctx.ui.notify("Cancelled", "info");
441
+ return;
442
+ }
443
+
444
+ const result = await currentSession.machine.transition("cancelled");
445
+ if (!result.success) {
446
+ ctx.ui.notify(`Failed to cancel: ${result.error}`, "error");
447
+ return;
448
+ }
449
+
450
+ const sessionJobId = currentSession.jobId;
451
+ currentSession = null;
452
+
453
+ ctx.ui.notify(
454
+ `Job ${sessionJobId} cancelled.\n` +
455
+ `Run /harness start to begin a new job.`,
456
+ "info",
457
+ );
458
+ },
459
+ });
460
+
168
461
  // ─── Footer status (persistent badge) ────────────────────────────────
169
462
  pi.on("session_start", async (_event, ctx) => {
170
463
  await refreshFooterStatus(ctx, mirrorStore, tracker);
@@ -185,7 +478,6 @@ async function refreshFooterStatus(
185
478
  ) {
186
479
  const local = aggregateWindows(tracker.all());
187
480
  const mirror = mirrorStore.read();
188
- const now = Date.now();
189
481
 
190
482
  const todayStr = `${(local.today.tokens / 1000).toFixed(1)}k tok · $${local.today.cost.toFixed(3)}`;
191
483
  let summary = `today: ${todayStr}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Codex-style /usage status for pi: local token tracking + provider mirror",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -19,6 +19,8 @@
19
19
  "renderer.ts",
20
20
  "sync-form.ts",
21
21
  "cli.ts",
22
+ "harness",
23
+ "packages",
22
24
  "skills",
23
25
  "package.json",
24
26
  "README.md",
@@ -60,6 +62,7 @@
60
62
  "typebox": "*"
61
63
  },
62
64
  "devDependencies": {
65
+ "@types/node": "^20.14.0",
63
66
  "standard-version": "^9.5.0"
64
67
  }
65
- }
68
+ }
@@ -0,0 +1,3 @@
1
+ # packages/checkpoint
2
+
3
+ Checkpoint persistence and restore logic will live here.
@@ -0,0 +1,38 @@
1
+ import { mkdir, readFile, rename, writeFile, appendFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import type { RuntimeCheckpoint, RuntimeEvent } from "../../types/src/runtime-types";
4
+
5
+ export class JsonCheckpointManager {
6
+ constructor(private readonly rootDir: string) {}
7
+
8
+ private jobDir(jobId: string): string {
9
+ return join(this.rootDir, "jobs", jobId);
10
+ }
11
+
12
+ private checkpointPath(jobId: string): string {
13
+ return join(this.jobDir(jobId), "checkpoint.json");
14
+ }
15
+
16
+ async save(checkpoint: RuntimeCheckpoint): Promise<void> {
17
+ const path = this.checkpointPath(checkpoint.jobId);
18
+ await mkdir(dirname(path), { recursive: true });
19
+ const tmp = `${path}.tmp`;
20
+ await writeFile(tmp, JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
21
+ await rename(tmp, path);
22
+ }
23
+
24
+ async load(jobId: string): Promise<RuntimeCheckpoint | null> {
25
+ try {
26
+ const text = await readFile(this.checkpointPath(jobId), "utf-8");
27
+ return JSON.parse(text) as RuntimeCheckpoint;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ async appendEvent(jobId: string, event: RuntimeEvent): Promise<void> {
34
+ const path = join(this.jobDir(jobId), "events.jsonl");
35
+ await mkdir(dirname(path), { recursive: true });
36
+ await appendFile(path, JSON.stringify(event) + "\n", "utf-8");
37
+ }
38
+ }
@@ -0,0 +1,42 @@
1
+ import type { ProviderSelection, RuntimeContext, RuntimeTask } from "../../types/src/runtime-types";
2
+
3
+ export interface RoutingPolicy {
4
+ plannerProvider: string;
5
+ codeProviders: string[];
6
+ reviewProvider: string;
7
+ fallbackProviders: string[];
8
+ }
9
+
10
+ export class SimpleProviderRouter {
11
+ constructor(private readonly policy: RoutingPolicy) {}
12
+
13
+ async selectProvider(task: RuntimeTask, context: RuntimeContext): Promise<ProviderSelection> {
14
+ const candidates = this.candidatesForTask(task);
15
+
16
+ for (const providerId of candidates) {
17
+ const state = context.providerStates[providerId] ?? "unknown";
18
+ if (state === "available" || state === "unknown") {
19
+ return {
20
+ providerId,
21
+ reason: `selected ${providerId} for task ${task.id}; state=${state}`,
22
+ };
23
+ }
24
+ }
25
+
26
+ throw new Error(`No available provider for task ${task.id}`);
27
+ }
28
+
29
+ private candidatesForTask(task: RuntimeTask): string[] {
30
+ const title = task.title.toLowerCase();
31
+
32
+ if (title.includes("plan") || title.includes("architecture")) {
33
+ return [this.policy.plannerProvider, ...this.policy.fallbackProviders];
34
+ }
35
+
36
+ if (title.includes("review") || title.includes("diff")) {
37
+ return [this.policy.reviewProvider, ...this.policy.fallbackProviders];
38
+ }
39
+
40
+ return [...this.policy.codeProviders, ...this.policy.fallbackProviders];
41
+ }
42
+ }
@@ -0,0 +1,3 @@
1
+ # packages/providers
2
+
3
+ Provider adapter interfaces and provider-specific implementations will live here.
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Provider Adapter — RFC-0002
3
+ *
4
+ * Normalizes provider-specific behavior for:
5
+ * - model invocation
6
+ * - error detection
7
+ * - quota detection
8
+ * - reset time discovery
9
+ * - retry policy
10
+ * - model capability metadata
11
+ */
12
+
13
+ import type {
14
+ ProviderConfig,
15
+ ProviderCapability,
16
+ ProviderRequest,
17
+ ProviderResponse,
18
+ } from "../../packages/types/src/runtime-types.ts";
19
+
20
+ export interface AdapterConfig {
21
+ provider: ProviderConfig;
22
+ quotaSignalExtractor?: (error: unknown) => QuotaSignal | null;
23
+ }
24
+
25
+ export interface QuotaSignal {
26
+ exhausted: boolean;
27
+ resetsAt?: string;
28
+ retryAfterMs?: number;
29
+ }
30
+
31
+ export interface AdapterResult {
32
+ response: ProviderResponse;
33
+ quotaSignal?: QuotaSignal;
34
+ retryable: boolean;
35
+ }
36
+
37
+ export interface ProviderAdapter {
38
+ readonly id: string;
39
+ readonly name: string;
40
+
41
+ invoke(request: ProviderRequest): Promise<AdapterResult>;
42
+
43
+ parseError(error: unknown): {
44
+ quotaExceeded: boolean;
45
+ rateLimited: boolean;
46
+ timeout: boolean;
47
+ serverError: boolean;
48
+ clientError: boolean;
49
+ quotaSignal?: QuotaSignal;
50
+ };
51
+
52
+ getCapabilities(): ProviderCapability[];
53
+
54
+ supportsModel(model: string): boolean;
55
+
56
+ getDefaultModel(): string;
57
+
58
+ getMaxTokens(model?: string): number;
59
+ }
60
+
61
+ /**
62
+ * Base adapter with common functionality
63
+ */
64
+ export abstract class BaseProviderAdapter implements ProviderAdapter {
65
+ abstract readonly id: string;
66
+ abstract readonly name: string;
67
+
68
+ constructor(protected config: ProviderConfig) {}
69
+
70
+ abstract invoke(request: ProviderRequest): Promise<AdapterResult>;
71
+
72
+ abstract parseError(error: unknown): {
73
+ quotaExceeded: boolean;
74
+ rateLimited: boolean;
75
+ timeout: boolean;
76
+ serverError: boolean;
77
+ clientError: boolean;
78
+ quotaSignal?: QuotaSignal;
79
+ };
80
+
81
+ getCapabilities(): ProviderCapability[] {
82
+ return this.config.capabilities;
83
+ }
84
+
85
+ supportsModel(model: string): boolean {
86
+ return this.config.models.includes(model);
87
+ }
88
+
89
+ getDefaultModel(): string {
90
+ return this.config.models[0] ?? "default";
91
+ }
92
+
93
+ getMaxTokens(model?: string): number {
94
+ // Default token limits per model family
95
+ const limits: Record<string, number> = {
96
+ "minimax/MiniMax-M3": 32768,
97
+ "minimax/MiniMax-Text-01": 1000000,
98
+ "anthropic/claude-3-5-sonnet": 200000,
99
+ "openai/gpt-4o": 128000,
100
+ "openai/gpt-4-turbo": 128000,
101
+ };
102
+ return limits[model ?? this.getDefaultModel()] ?? 4096;
103
+ }
104
+
105
+ /**
106
+ * Parse usage from response
107
+ */
108
+ protected parseUsage(response: unknown): ProviderResponse["usage"] {
109
+ const r = response as Record<string, unknown>;
110
+ return {
111
+ input: (r.input_tokens as number) ?? (r.prompt_tokens as number) ?? 0,
112
+ output:
113
+ (r.output_tokens as number) ?? (r.completion_tokens as number) ?? 0,
114
+ cacheRead: (r.cache_read_tokens as number) ?? 0,
115
+ cacheWrite: (r.cache_write_tokens as number) ?? 0,
116
+ cost: (r.cost as number) ?? 0,
117
+ };
118
+ }
119
+ }
120
+
121
+ /**
122
+ * MiniMax adapter
123
+ */
124
+ export class MiniMaxAdapter extends BaseProviderAdapter {
125
+ readonly id = "minimax";
126
+ readonly name = "MiniMax";
127
+
128
+ async invoke(request: ProviderRequest): Promise<AdapterResult> {
129
+ // In practice, this would call the MiniMax API
130
+ // For now, return a mock response
131
+ return {
132
+ response: {
133
+ content: "Mock response",
134
+ usage: { input: 100, output: 200, cost: 0.001 },
135
+ model: request.model,
136
+ finishReason: "stop",
137
+ },
138
+ retryable: false,
139
+ };
140
+ }
141
+
142
+ parseError(error: unknown): {
143
+ quotaExceeded: boolean;
144
+ rateLimited: boolean;
145
+ timeout: boolean;
146
+ serverError: boolean;
147
+ clientError: boolean;
148
+ quotaSignal?: QuotaSignal;
149
+ } {
150
+ const e = error as Record<string, unknown>;
151
+ const msg = String(e.message ?? e.error ?? "").toLowerCase();
152
+
153
+ return {
154
+ quotaExceeded: msg.includes("2056") || msg.includes("quota"),
155
+ rateLimited: msg.includes("rate limit") || msg.includes("429"),
156
+ timeout: msg.includes("timeout") || msg.includes("timed out"),
157
+ serverError:
158
+ msg.includes("500") || msg.includes("502") || msg.includes("503"),
159
+ clientError:
160
+ msg.includes("400") || msg.includes("401") || msg.includes("403"),
161
+ quotaSignal:
162
+ msg.includes("quota") || msg.includes("2056")
163
+ ? { exhausted: true, resetsAt: undefined }
164
+ : undefined,
165
+ };
166
+ }
167
+ }
168
+
169
+ /**
170
+ * OpenAI adapter
171
+ */
172
+ export class OpenAIAdapter extends BaseProviderAdapter {
173
+ readonly id = "openai";
174
+ readonly name = "OpenAI";
175
+
176
+ async invoke(request: ProviderRequest): Promise<AdapterResult> {
177
+ return {
178
+ response: {
179
+ content: "Mock response",
180
+ usage: { input: 100, output: 200, cost: 0.002 },
181
+ model: request.model,
182
+ finishReason: "stop",
183
+ },
184
+ retryable: false,
185
+ };
186
+ }
187
+
188
+ parseError(error: unknown): {
189
+ quotaExceeded: boolean;
190
+ rateLimited: boolean;
191
+ timeout: boolean;
192
+ serverError: boolean;
193
+ clientError: boolean;
194
+ quotaSignal?: QuotaSignal;
195
+ } {
196
+ const e = error as Record<string, unknown>;
197
+ const msg = String(e.message ?? e.error ?? "").toLowerCase();
198
+ const code = String(e.code ?? "");
199
+
200
+ return {
201
+ quotaExceeded:
202
+ code === "insufficient_quota" || code === "context_length_exceeded",
203
+ rateLimited: code === "rate_limit_exceeded" || msg.includes("429"),
204
+ timeout: msg.includes("timeout"),
205
+ serverError: code.startsWith("5"),
206
+ clientError: code.startsWith("4"),
207
+ quotaSignal:
208
+ code === "insufficient_quota"
209
+ ? { exhausted: true, resetsAt: undefined }
210
+ : undefined,
211
+ };
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Adapter registry
217
+ */
218
+ export class AdapterRegistry {
219
+ private adapters: Map<string, ProviderAdapter> = new Map();
220
+
221
+ register(adapter: ProviderAdapter): void {
222
+ this.adapters.set(adapter.id, adapter);
223
+ }
224
+
225
+ get(id: string): ProviderAdapter | undefined {
226
+ return this.adapters.get(id);
227
+ }
228
+
229
+ list(): ProviderAdapter[] {
230
+ return Array.from(this.adapters.values());
231
+ }
232
+
233
+ /**
234
+ * Create default registry with standard adapters
235
+ */
236
+ static createDefault(): AdapterRegistry {
237
+ const registry = new AdapterRegistry();
238
+
239
+ registry.register(
240
+ new MiniMaxAdapter({
241
+ id: "minimax",
242
+ name: "MiniMax",
243
+ models: ["minimax/MiniMax-M3", "minimax/MiniMax-Text-01"],
244
+ capabilities: ["code", "review", "plan", "test"],
245
+ rateLimits: {},
246
+ }),
247
+ );
248
+
249
+ registry.register(
250
+ new OpenAIAdapter({
251
+ id: "openai",
252
+ name: "OpenAI",
253
+ models: ["openai/gpt-4o", "openai/gpt-4-turbo"],
254
+ capabilities: ["code", "review", "plan", "test", "refactor"],
255
+ rateLimits: {},
256
+ }),
257
+ );
258
+
259
+ return registry;
260
+ }
261
+ }
@@ -0,0 +1,3 @@
1
+ # packages/quota-manager
2
+
3
+ Quota detection, reset-time handling, and pause/resume logic will live here.