@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli.js +3464 -3306
  3. package/dist/types/cli/bench-cli.d.ts +0 -5
  4. package/dist/types/cli/dry-balance-cli.d.ts +0 -5
  5. package/dist/types/cli/models-cli.d.ts +1 -1
  6. package/dist/types/config/inline-tool-descriptors-mode.d.ts +1 -2
  7. package/dist/types/config/model-registry.d.ts +1 -23
  8. package/dist/types/config/model-resolver.d.ts +10 -15
  9. package/dist/types/config/models-config-schema.d.ts +0 -6
  10. package/dist/types/config/models-config.d.ts +0 -6
  11. package/dist/types/config/settings-schema.d.ts +31 -1
  12. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +15 -0
  14. package/dist/types/extensibility/skills.d.ts +29 -0
  15. package/dist/types/lsp/client.d.ts +9 -3
  16. package/dist/types/modes/components/todo-reminder.d.ts +3 -1
  17. package/dist/types/modes/interactive-mode.d.ts +0 -1
  18. package/dist/types/modes/skill-command.d.ts +1 -1
  19. package/dist/types/modes/types.d.ts +0 -1
  20. package/dist/types/stt/asr-client.d.ts +7 -3
  21. package/dist/types/task/spawn-policy.d.ts +17 -0
  22. package/dist/types/task/spawn-policy.test.d.ts +1 -0
  23. package/dist/types/task/types.d.ts +8 -2
  24. package/dist/types/tools/__tests__/eval-description.test.d.ts +1 -0
  25. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +2 -2
  26. package/dist/types/tools/browser/registry.d.ts +2 -0
  27. package/dist/types/tools/browser/run-cancellation.d.ts +4 -0
  28. package/dist/types/tools/browser/tab-supervisor.d.ts +30 -1
  29. package/dist/types/tools/eval.d.ts +3 -6
  30. package/dist/types/tools/write.d.ts +1 -1
  31. package/dist/types/utils/image-vision-fallback.d.ts +1 -1
  32. package/package.json +14 -13
  33. package/scripts/bundle-dist.ts +23 -4
  34. package/scripts/generate-docs-index.ts +116 -24
  35. package/src/advisor/__tests__/advisor.test.ts +1 -1
  36. package/src/async/job-manager.ts +27 -3
  37. package/src/auto-thinking/classifier.ts +1 -1
  38. package/src/cli/bench-cli.ts +3 -12
  39. package/src/cli/dry-balance-cli.ts +1 -6
  40. package/src/cli/grep-cli.ts +1 -1
  41. package/src/cli/models-cli.ts +3 -81
  42. package/src/commands/models.ts +1 -2
  43. package/src/commit/model-selection.ts +4 -4
  44. package/src/config/inline-tool-descriptors-mode.ts +1 -2
  45. package/src/config/model-discovery.ts +150 -82
  46. package/src/config/model-registry.ts +11 -225
  47. package/src/config/model-resolver.ts +23 -135
  48. package/src/config/models-config-schema.ts +0 -22
  49. package/src/config/prompt-templates.ts +20 -0
  50. package/src/config/settings-schema.ts +35 -1
  51. package/src/config/settings.ts +60 -24
  52. package/src/eval/__tests__/agent-bridge.test.ts +17 -3
  53. package/src/eval/agent-bridge.ts +7 -9
  54. package/src/eval/completion-bridge.ts +1 -1
  55. package/src/eval/py/__tests__/runner-shell-output.test.ts +157 -0
  56. package/src/eval/py/runner.py +169 -18
  57. package/src/extensibility/extensions/model-api.ts +1 -3
  58. package/src/extensibility/extensions/runner.ts +62 -5
  59. package/src/extensibility/skills.ts +77 -0
  60. package/src/internal-urls/docs-index.generated.txt +2 -2
  61. package/src/lsp/client.ts +162 -45
  62. package/src/lsp/config.ts +17 -3
  63. package/src/lsp/index.ts +31 -21
  64. package/src/main.ts +0 -1
  65. package/src/mcp/transports/http.ts +19 -17
  66. package/src/mcp/transports/stdio.test.ts +51 -1
  67. package/src/mcp/transports/stdio.ts +19 -9
  68. package/src/memories/index.ts +0 -1
  69. package/src/mnemopi/backend.ts +1 -1
  70. package/src/modes/acp/acp-agent.ts +6 -9
  71. package/src/modes/components/model-selector.ts +32 -186
  72. package/src/modes/components/todo-reminder.ts +5 -1
  73. package/src/modes/controllers/event-controller.ts +1 -16
  74. package/src/modes/controllers/selector-controller.ts +57 -36
  75. package/src/modes/interactive-mode.ts +0 -6
  76. package/src/modes/rpc/rpc-mode.ts +5 -8
  77. package/src/modes/skill-command.ts +8 -20
  78. package/src/modes/types.ts +0 -1
  79. package/src/prompts/system/subagent-system-prompt.md +2 -2
  80. package/src/prompts/system/tool-call-loop-redirect.md +8 -0
  81. package/src/prompts/tools/eval.md +2 -2
  82. package/src/prompts/tools/task.md +1 -1
  83. package/src/sdk.ts +2 -9
  84. package/src/session/agent-session.ts +89 -8
  85. package/src/session/session-context.ts +2 -1
  86. package/src/session/session-manager.ts +2 -1
  87. package/src/session/unexpected-stop-classifier.ts +1 -1
  88. package/src/stt/asr-client.ts +87 -27
  89. package/src/stt/downloader.ts +8 -2
  90. package/src/task/executor.ts +1 -3
  91. package/src/task/index.ts +37 -32
  92. package/src/task/spawn-policy.test.ts +63 -0
  93. package/src/task/spawn-policy.ts +58 -0
  94. package/src/task/types.ts +77 -6
  95. package/src/tools/__tests__/eval-description.test.ts +19 -0
  96. package/src/tools/ast-grep.ts +34 -12
  97. package/src/tools/browser/cmux/cmux-tab.ts +66 -24
  98. package/src/tools/browser/registry.ts +25 -0
  99. package/src/tools/browser/run-cancellation.ts +47 -0
  100. package/src/tools/browser/tab-supervisor.ts +120 -7
  101. package/src/tools/browser/tab-worker.ts +22 -10
  102. package/src/tools/browser.ts +1 -0
  103. package/src/tools/eval.ts +15 -10
  104. package/src/tools/grep.ts +11 -8
  105. package/src/tools/inspect-image.ts +1 -1
  106. package/src/tools/write.ts +49 -9
  107. package/src/utils/commit-message-generator.ts +0 -1
  108. package/src/utils/git.ts +22 -1
  109. package/src/utils/image-vision-fallback.ts +2 -3
  110. package/src/utils/title-generator.ts +1 -1
@@ -19,7 +19,7 @@ import {
19
19
  OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW,
20
20
  OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS,
21
21
  } from "@oh-my-pi/pi-catalog/provider-models/openai-compat";
22
- import type { ModelSpec } from "@oh-my-pi/pi-catalog/types";
22
+ import type { ModelSpec, OpenAICompat } from "@oh-my-pi/pi-catalog/types";
23
23
  import { isRecord } from "@oh-my-pi/pi-utils";
24
24
  import type { ProviderDiscovery } from "./models-config-schema";
25
25
 
@@ -35,6 +35,31 @@ import type { ProviderDiscovery } from "./models-config-schema";
35
35
  export const DISCOVERY_DEFAULT_CONTEXT_WINDOW = OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW;
36
36
  export const DISCOVERY_DEFAULT_MAX_TOKENS = OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS;
37
37
 
38
+ /**
39
+ * Run `fn` with an abort signal that fires after `timeoutMs`, clearing the
40
+ * backing timer the instant the operation settles.
41
+ *
42
+ * Unlike the built-in abort-signal timeout API, the timer never outlives the
43
+ * request: on the success path it is cancelled before `fn` resolves, so the
44
+ * signal is never aborted and no pending callback lingers on the heap. A leaked
45
+ * abort-signal timeout (e.g. discovery against a mocked fetch that resolves
46
+ * instantly) fires seconds later and sets its abort `reason` — which crashed
47
+ * Bun's concurrent GC while it marked the signal's wrapped reason during an
48
+ * unrelated allocation (`JSAbortSignal::visitAdditionalChildren`).
49
+ */
50
+ async function withTimeoutSignal<T>(timeoutMs: number, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
51
+ const controller = new AbortController();
52
+ const timer = setTimeout(
53
+ () => controller.abort(new DOMException("The operation timed out.", "TimeoutError")),
54
+ timeoutMs,
55
+ );
56
+ try {
57
+ return await fn(controller.signal);
58
+ } finally {
59
+ clearTimeout(timer);
60
+ }
61
+ }
62
+
38
63
  const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
39
64
  const OLLAMA_HOST_DEFAULT_PORT = "11434";
40
65
 
@@ -288,16 +313,18 @@ async function discoverOllamaModelMetadata(
288
313
  ): Promise<OllamaDiscoveredModelMetadata | null> {
289
314
  const showUrl = `${endpoint}/api/show`;
290
315
  try {
291
- const response = await ctx.fetch(showUrl, {
292
- method: "POST",
293
- headers: { ...(headers ?? {}), "Content-Type": "application/json" },
294
- body: JSON.stringify({ model: modelId }),
295
- signal: AbortSignal.timeout(150),
316
+ const payload = await withTimeoutSignal(150, async signal => {
317
+ const response = await ctx.fetch(showUrl, {
318
+ method: "POST",
319
+ headers: { ...(headers ?? {}), "Content-Type": "application/json" },
320
+ body: JSON.stringify({ model: modelId }),
321
+ signal,
322
+ });
323
+ if (!response.ok) {
324
+ return null;
325
+ }
326
+ return (await response.json()) as unknown;
296
327
  });
297
- if (!response.ok) {
298
- return null;
299
- }
300
- const payload = (await response.json()) as unknown;
301
328
  if (!isRecord(payload)) {
302
329
  return null;
303
330
  }
@@ -339,14 +366,16 @@ export async function discoverOllamaModels(
339
366
  const endpoint = normalizeOllamaBaseUrl(providerConfig.baseUrl);
340
367
  const tagsUrl = `${endpoint}/api/tags`;
341
368
  const headers = { ...(providerConfig.headers ?? {}) };
342
- const response = await ctx.fetch(tagsUrl, {
343
- headers,
344
- signal: AbortSignal.timeout(250),
369
+ const payload = await withTimeoutSignal(250, async signal => {
370
+ const response = await ctx.fetch(tagsUrl, {
371
+ headers,
372
+ signal,
373
+ });
374
+ if (!response.ok) {
375
+ throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
376
+ }
377
+ return (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
345
378
  });
346
- if (!response.ok) {
347
- throw new Error(`HTTP ${response.status} from ${tagsUrl}`);
348
- }
349
- const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
350
379
  const entries = (payload.models ?? []).flatMap(item => {
351
380
  const id = item.model || item.name;
352
381
  return id ? [{ id, name: item.name || id }] : [];
@@ -384,14 +413,16 @@ async function discoverLlamaCppServerMetadata(
384
413
  ): Promise<LlamaCppDiscoveredServerMetadata | null> {
385
414
  const propsUrl = `${toLlamaCppNativeBaseUrl(baseUrl)}/props`;
386
415
  try {
387
- const response = await ctx.fetch(propsUrl, {
388
- headers,
389
- signal: AbortSignal.timeout(150),
416
+ const payload = await withTimeoutSignal(150, async signal => {
417
+ const response = await ctx.fetch(propsUrl, {
418
+ headers,
419
+ signal,
420
+ });
421
+ if (!response.ok) {
422
+ return null;
423
+ }
424
+ return (await response.json()) as unknown;
390
425
  });
391
- if (!response.ok) {
392
- return null;
393
- }
394
- const payload = (await response.json()) as unknown;
395
426
  if (!isRecord(payload)) {
396
427
  return null;
397
428
  }
@@ -415,24 +446,26 @@ export async function discoverLlamaCppModels(
415
446
  const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
416
447
  let headers = baseHeaders;
417
448
  const attempt = async (h: Record<string, string>) => {
418
- const [response, metadata] = await Promise.all([
419
- ctx.fetch(modelsUrl, {
420
- headers: h,
421
- signal: AbortSignal.timeout(250),
449
+ const [payload, metadata] = await Promise.all([
450
+ withTimeoutSignal(250, async signal => {
451
+ const response = await ctx.fetch(modelsUrl, {
452
+ headers: h,
453
+ signal,
454
+ });
455
+ if (!response.ok) {
456
+ throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
457
+ }
458
+ headers = h;
459
+ return (await response.json()) as unknown;
422
460
  }),
423
461
  discoverLlamaCppServerMetadata(ctx, baseUrl, h),
424
462
  ]);
425
- if (!response.ok) {
426
- throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
427
- }
428
- headers = h;
429
- return [response, metadata] as const;
463
+ return [payload, metadata] as const;
430
464
  };
431
465
  const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
432
- const [response, serverMetadata] = apiKey
466
+ const [payload, serverMetadata] = apiKey
433
467
  ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
434
468
  : await attempt(baseHeaders);
435
- const payload = (await response.json()) as unknown;
436
469
  const models = parseLlamaCppModelList(payload);
437
470
  const discovered: Model<Api>[] = [];
438
471
  for (const item of models) {
@@ -476,17 +509,22 @@ export async function discoverLlamaCppModelRuntimeMetadata(
476
509
  const modelsUrl = `${baseUrl}/models`;
477
510
  const baseHeaders: Record<string, string> = { ...(model.headers ?? {}) };
478
511
  const attempt = async (headers: Record<string, string>) => {
479
- const [response, serverMetadata] = await Promise.all([
480
- ctx.fetch(modelsUrl, {
481
- headers,
482
- signal: AbortSignal.timeout(250),
512
+ const [entries, serverMetadata] = await Promise.all([
513
+ withTimeoutSignal(250, async signal => {
514
+ const response = await ctx.fetch(modelsUrl, {
515
+ headers,
516
+ signal,
517
+ });
518
+ if (!response.ok) {
519
+ return undefined;
520
+ }
521
+ return parseLlamaCppModelList(await response.json());
483
522
  }),
484
523
  discoverLlamaCppServerMetadata(ctx, baseUrl, headers),
485
524
  ]);
486
- if (!response.ok) {
525
+ if (!entries) {
487
526
  return undefined;
488
527
  }
489
- const entries = parseLlamaCppModelList(await response.json());
490
528
  const entry = entries.find(entry => entry.id === model.id);
491
529
  if (!entry) {
492
530
  return undefined;
@@ -524,55 +562,83 @@ export async function discoverOpenAIModelsList(
524
562
  providerConfig.discovery.type === "lm-studio"
525
563
  ? fetchLmStudioNativeModelMetadata(baseUrl, ctx.fetch, { headers: h })
526
564
  : Promise.resolve(null);
527
- const [res, nativeMetadata] = await Promise.all([
528
- ctx.fetch(modelsUrl, {
529
- headers: h,
530
- signal: AbortSignal.timeout(10_000),
565
+ const [payload, nativeMetadata] = await Promise.all([
566
+ withTimeoutSignal(10_000, async signal => {
567
+ const res = await ctx.fetch(modelsUrl, {
568
+ headers: h,
569
+ signal,
570
+ });
571
+ if (!res.ok) {
572
+ throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
573
+ }
574
+ headers = h;
575
+ return (await res.json()) as {
576
+ data?: Array<{ id?: string; max_model_len?: unknown; context_length?: unknown }>;
577
+ };
531
578
  }),
532
579
  nativeMetadataPromise,
533
580
  ]);
534
- if (!res.ok) {
535
- throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
536
- }
537
- headers = h;
538
- return [res, nativeMetadata] as const;
581
+ return [payload, nativeMetadata] as const;
539
582
  };
540
583
  const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
541
- const [response, nativeMetadata] = apiKey
584
+ const [payload, nativeMetadata] = apiKey
542
585
  ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
543
586
  : await attempt(baseHeaders);
544
- const payload = (await response.json()) as {
545
- data?: Array<{ id?: string; max_model_len?: unknown; context_length?: unknown }>;
546
- };
547
587
  const models = payload.data ?? [];
588
+ const references = getBundledModelReferenceIndex();
548
589
  const discovered: Model<Api>[] = [];
549
590
  for (const item of models) {
550
591
  const id = item.id;
551
592
  if (!id) continue;
552
593
  const nativeMetadataForModel = nativeMetadata?.get(id);
594
+ // Thin OpenAI-compatible proxies frequently omit `context_length`/
595
+ // `max_model_len` on `/v1/models`, leaving discovered models pinned at
596
+ // the 128K default even when the underlying model is e.g. a proxied
597
+ // Claude with a 1M window. Resolve the id against the bundled catalog
598
+ // (same pattern as `discoverProxyModels` and `discoverLiteLLMModels`) so
599
+ // intrinsic metadata — context/output limits, display name, modality,
600
+ // reasoning support — flows through when the provider is silent. Local
601
+ // runtime state and provider-reported values still win; proxy-specific
602
+ // headers/baseUrl/cost stay local.
603
+ const reference = resolveModelReference(id, references) as ModelSpec<Api> | undefined;
604
+ const referenceCompat = reference?.compat as OpenAICompat | undefined;
553
605
  const contextWindow =
554
606
  toPositiveNumberOrUndefined(item.max_model_len) ??
555
607
  toPositiveNumberOrUndefined(item.context_length) ??
556
608
  nativeMetadataForModel?.contextWindow ??
609
+ reference?.contextWindow ??
557
610
  DISCOVERY_DEFAULT_CONTEXT_WINDOW;
558
611
  discovered.push(
559
612
  buildModel({
560
613
  id,
561
- name: id,
614
+ name: reference?.name ?? id,
562
615
  api: providerConfig.api,
563
616
  provider: providerConfig.provider,
564
617
  baseUrl,
565
- reasoning: false,
566
- input: nativeMetadataForModel?.input ?? ["text"],
618
+ reasoning: reference?.reasoning ?? false,
619
+ thinking: reference?.thinking,
620
+ input: nativeMetadataForModel?.input ?? reference?.input ?? ["text"],
567
621
  ...(providerConfig.discovery.type === "lm-studio" ? { imageInputDecoder: "stb" as const } : {}),
622
+ // Proxy/gateway pricing is provider-specific and rarely matches
623
+ // upstream bundled catalogs, so keep costs local-unknown even
624
+ // when we successfully recover the upstream model identity.
568
625
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
569
626
  contextWindow,
570
- maxTokens: Math.min(contextWindow, discoveryDefaultMaxTokens(providerConfig.api)),
627
+ // Cap the reference's output limit at the discovered context
628
+ // window so an ID collision with a larger bundled model can
629
+ // never request more tokens than the local runtime advertises.
630
+ maxTokens: Math.min(reference?.maxTokens ?? discoveryDefaultMaxTokens(providerConfig.api), contextWindow),
571
631
  headers,
572
632
  compat: {
573
633
  supportsStore: false,
574
634
  supportsDeveloperRole: false,
575
- supportsReasoningEffort: false,
635
+ supportsReasoningEffort: referenceCompat?.supportsReasoningEffort ?? false,
636
+ ...(referenceCompat?.reasoningEffortMap
637
+ ? { reasoningEffortMap: referenceCompat.reasoningEffortMap }
638
+ : {}),
639
+ ...(referenceCompat?.omitReasoningEffort !== undefined
640
+ ? { omitReasoningEffort: referenceCompat.omitReasoningEffort }
641
+ : {}),
576
642
  },
577
643
  } as ModelSpec<Api>),
578
644
  );
@@ -600,15 +666,17 @@ export async function discoverLiteLLMModels(
600
666
  }
601
667
  return response;
602
668
  };
603
- const models = await fetchLiteLLMRichModels({
604
- api: providerConfig.api,
605
- provider: providerConfig.provider,
606
- baseUrl,
607
- headers: h,
608
- fetch: authAwareFetch,
609
- referenceResolver: resolveReference,
610
- signal: AbortSignal.timeout(10_000),
611
- });
669
+ const models = await withTimeoutSignal(10_000, signal =>
670
+ fetchLiteLLMRichModels({
671
+ api: providerConfig.api,
672
+ provider: providerConfig.provider,
673
+ baseUrl,
674
+ headers: h,
675
+ fetch: authAwareFetch,
676
+ referenceResolver: resolveReference,
677
+ signal,
678
+ }),
679
+ );
612
680
  if (authError && models === null) {
613
681
  throw authError;
614
682
  }
@@ -657,24 +725,24 @@ export async function discoverProxyModels(
657
725
 
658
726
  const baseHeaders: Record<string, string> = { ...(providerConfig.headers ?? {}) };
659
727
  let headers = baseHeaders;
660
- const attempt = async (h: Record<string, string>) => {
661
- const res = await ctx.fetch(modelsUrl, {
662
- headers: h,
663
- signal: AbortSignal.timeout(10_000),
728
+ const attempt = async (h: Record<string, string>) =>
729
+ withTimeoutSignal(10_000, async signal => {
730
+ const res = await ctx.fetch(modelsUrl, {
731
+ headers: h,
732
+ signal,
733
+ });
734
+ if (!res.ok) {
735
+ throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
736
+ }
737
+ headers = h;
738
+ return (await res.json()) as {
739
+ data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[]; context_length?: number }>;
740
+ };
664
741
  });
665
- if (!res.ok) {
666
- throw new Error(`HTTP ${res.status} from ${modelsUrl}`);
667
- }
668
- headers = h;
669
- return res;
670
- };
671
742
  const apiKey = await ctx.getBearerApiKeyResolver(providerConfig.provider);
672
- const response = apiKey
743
+ const payload = apiKey
673
744
  ? await withAuth(apiKey, key => attempt({ ...baseHeaders, Authorization: `Bearer ${key}` }))
674
745
  : await attempt(baseHeaders);
675
- const payload = (await response.json()) as {
676
- data?: Array<{ id?: string; name?: string; supported_endpoint_types?: string[]; context_length?: number }>;
677
- };
678
746
  const items = payload.data ?? [];
679
747
  const discovered: Model<Api>[] = [];
680
748
  for (const item of items) {