@automatalabs/acp-agents 1.2.7 → 2.0.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.
Files changed (82) hide show
  1. package/README.md +84 -11
  2. package/dist/acp-client.d.ts +26 -6
  3. package/dist/acp-client.d.ts.map +1 -1
  4. package/dist/acp-client.js +30 -7
  5. package/dist/agent/acp-agent.d.ts +141 -0
  6. package/dist/agent/acp-agent.d.ts.map +1 -0
  7. package/dist/agent/acp-agent.js +837 -0
  8. package/dist/agent/errors.d.ts +17 -0
  9. package/dist/agent/errors.d.ts.map +1 -0
  10. package/dist/agent/errors.js +37 -0
  11. package/dist/agent/events.d.ts +26 -0
  12. package/dist/agent/events.d.ts.map +1 -0
  13. package/dist/agent/events.js +165 -0
  14. package/dist/agent/fork.d.ts +27 -0
  15. package/dist/agent/fork.d.ts.map +1 -0
  16. package/dist/agent/fork.js +28 -0
  17. package/dist/agent/probe.d.ts +14 -0
  18. package/dist/agent/probe.d.ts.map +1 -0
  19. package/dist/agent/probe.js +87 -0
  20. package/dist/agent/process-registry.d.ts +9 -0
  21. package/dist/agent/process-registry.d.ts.map +1 -0
  22. package/dist/agent/process-registry.js +28 -0
  23. package/dist/agent/queue.d.ts +19 -0
  24. package/dist/agent/queue.d.ts.map +1 -0
  25. package/dist/agent/queue.js +90 -0
  26. package/dist/agent/routing.d.ts +27 -0
  27. package/dist/agent/routing.d.ts.map +1 -0
  28. package/dist/agent/routing.js +87 -0
  29. package/dist/agent/structured.d.ts +47 -0
  30. package/dist/agent/structured.d.ts.map +1 -0
  31. package/dist/agent/structured.js +90 -0
  32. package/dist/agent/turn.d.ts +65 -0
  33. package/dist/agent/turn.d.ts.map +1 -0
  34. package/dist/agent/turn.js +187 -0
  35. package/dist/agent/types.d.ts +226 -0
  36. package/dist/agent/types.d.ts.map +1 -0
  37. package/dist/agent/types.js +9 -0
  38. package/dist/backend.d.ts +18 -5
  39. package/dist/backend.d.ts.map +1 -1
  40. package/dist/backends/claude.d.ts +5 -0
  41. package/dist/backends/claude.d.ts.map +1 -1
  42. package/dist/backends/claude.js +45 -17
  43. package/dist/backends/codex.d.ts +4 -0
  44. package/dist/backends/codex.d.ts.map +1 -1
  45. package/dist/backends/codex.js +14 -9
  46. package/dist/backends/custom.d.ts.map +1 -1
  47. package/dist/backends/custom.js +3 -2
  48. package/dist/backends/opencode.d.ts +4 -0
  49. package/dist/backends/opencode.d.ts.map +1 -1
  50. package/dist/backends/opencode.js +5 -1
  51. package/dist/backends/pi.d.ts +5 -2
  52. package/dist/backends/pi.d.ts.map +1 -1
  53. package/dist/backends/pi.js +15 -3
  54. package/dist/config-catalog.d.ts +173 -0
  55. package/dist/config-catalog.d.ts.map +1 -0
  56. package/dist/config-catalog.js +408 -0
  57. package/dist/index.d.ts +11 -3
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +12 -1
  60. package/dist/interactive.d.ts +4 -5
  61. package/dist/interactive.d.ts.map +1 -1
  62. package/dist/protocol-coverage.d.ts +82 -0
  63. package/dist/protocol-coverage.d.ts.map +1 -1
  64. package/dist/protocol-coverage.js +76 -0
  65. package/dist/registry.d.ts +12 -0
  66. package/dist/registry.d.ts.map +1 -1
  67. package/dist/registry.js +22 -0
  68. package/dist/routing.d.ts +14 -0
  69. package/dist/routing.d.ts.map +1 -0
  70. package/dist/routing.js +53 -0
  71. package/dist/runner.d.ts.map +1 -1
  72. package/dist/runner.js +9 -68
  73. package/dist/session-ref.d.ts +8 -0
  74. package/dist/session-ref.d.ts.map +1 -0
  75. package/dist/session-ref.js +21 -0
  76. package/dist/structured-tool.d.ts +4 -0
  77. package/dist/structured-tool.d.ts.map +1 -1
  78. package/dist/structured-tool.js +5 -0
  79. package/dist/system-prompt.d.ts +13 -0
  80. package/dist/system-prompt.d.ts.map +1 -0
  81. package/dist/system-prompt.js +67 -0
  82. package/package.json +4 -4
@@ -2,8 +2,9 @@
2
2
  // pi-acp consumes the same client-hosted StructuredOutput MCP server as other HTTP-capable agents.
3
3
  import { dirname, join } from "node:path";
4
4
  import { createRequire } from "node:module";
5
+ import { META_KEYS } from "@automatalabs/shared-types";
5
6
  import { splitArgs } from "../backend.js";
6
- import { BUILTIN_PROTOCOL_COVERAGE } from "../protocol-coverage.js";
7
+ import { BUILTIN_PROTOCOL_COVERAGE, systemPromptSupport } from "../protocol-coverage.js";
7
8
  import { defineBuiltinBackend } from "./define.js";
8
9
  const require = createRequire(import.meta.url);
9
10
  // pi-acp advertises a single ambient method since ACP schema 1.21.0 removed the `env_var` variant
@@ -30,6 +31,9 @@ export class PiBackend {
30
31
  }
31
32
  embedSchemaInPrompt = true;
32
33
  injectStructuredOutputTool = true;
34
+ /** pi-acp reads `_meta.systemPrompt` `{ replace?, append? }` on session/new, resume, load, and
35
+ * fork and folds it into pi's `DefaultResourceLoader` system-prompt overrides. */
36
+ systemPrompt = systemPromptSupport("pi");
33
37
  classifyProviderError(error, metadata) {
34
38
  const providerCode = errorDataString(error, "errorKind");
35
39
  if (providerCode === "rate_limit" || providerCode === "billing_error") {
@@ -64,8 +68,16 @@ export class PiBackend {
64
68
  return { command: "npx", args: ["-y", "@automatalabs/pi-acp"], env };
65
69
  }
66
70
  }
67
- sessionMeta() {
68
- return undefined;
71
+ sessionMeta(_schema, inputs) {
72
+ // The schema rides the client-hosted StructuredOutput tool and the prompt, never session
73
+ // `_meta`. The neutral instructions ride pi-acp's `_meta.systemPrompt` object VERBATIM (only
74
+ // the defined fields), so an unconfigured session sends no `_meta` at all.
75
+ const systemPrompt = inputs?.systemPrompt;
76
+ const value = {
77
+ ...(systemPrompt?.replace !== undefined ? { replace: systemPrompt.replace } : {}),
78
+ ...(systemPrompt?.append !== undefined ? { append: systemPrompt.append } : {}),
79
+ };
80
+ return Object.keys(value).length > 0 ? { [META_KEYS.systemPrompt]: value } : undefined;
69
81
  }
70
82
  promptMeta(_schema) {
71
83
  return undefined;
@@ -0,0 +1,173 @@
1
+ import type { SessionConfigOption, SessionModeState } from "@agentclientprotocol/sdk";
2
+ import { type CustomBackendConfig } from "./registry.js";
3
+ import { type ProbedConfigOptions } from "./runner.js";
4
+ export interface ValidateProbeRunner {
5
+ probeConfigOptions(spec?: string, opts?: {
6
+ cwd?: string;
7
+ selectModel?: boolean;
8
+ backends?: Record<string, CustomBackendConfig>;
9
+ signal?: AbortSignal;
10
+ }): Promise<ProbedConfigOptions>;
11
+ /** Every host-routable backend name. Used by protocol-native discovery. */
12
+ listBackends?(): string[];
13
+ /** Host-registered custom names, including deliberate built-in shadows. */
14
+ listCustomBackends?(): string[];
15
+ /** The host's backend selected when a workflow omits model. */
16
+ defaultBackendId?(): string;
17
+ /** Present on owned probe runners; shared host runners are never disposed by validation. */
18
+ dispose?(): Promise<void>;
19
+ }
20
+ export type ConfigProbeFactory = (backends: Record<string, CustomBackendConfig> | undefined) => ValidateProbeRunner;
21
+ /** Package-internal hermetic test seam. Deliberately absent from the public index export. */
22
+ export declare function setConfigProbeFactoryForTests(factory: ConfigProbeFactory): () => void;
23
+ export interface ValidateHarnessOptions {
24
+ backendId: string;
25
+ /** AgentPrism's explicit mode when the call omits mode; absent for no-mode/custom backends. */
26
+ defaultModeId?: string;
27
+ /** The call's verbatim selected model; absent means the harness/session default. */
28
+ model?: string;
29
+ probed: boolean;
30
+ /** Present when probed=false: the harness's spawn/auth/session error. */
31
+ error?: string;
32
+ /** Effective advertised ACP modes; null means this backend/model supports no session modes. */
33
+ modes?: SessionModeState | null;
34
+ options?: SessionConfigOption[];
35
+ }
36
+ type SelectConfigOption = Extract<SessionConfigOption, {
37
+ type: "select";
38
+ }>;
39
+ /** Every leaf {value,label} the select advertises, flattening any advertised optgroups. */
40
+ export declare function selectChoicePairs(option: SelectConfigOption): {
41
+ value: string;
42
+ label?: string;
43
+ }[];
44
+ export interface SelectChoiceGroup {
45
+ group: string;
46
+ count: number;
47
+ }
48
+ export interface SelectChoiceSummary {
49
+ total: number;
50
+ groups: SelectChoiceGroup[];
51
+ }
52
+ /**
53
+ * Group a select's choices for summary display. Prefers the harness-advertised optgroup
54
+ * labels; absent those, groups by the first "/"-segment of each value. Groups come back
55
+ * largest-first, ties broken by first appearance.
56
+ */
57
+ export declare function summarizeSelectChoices(option: SelectConfigOption): SelectChoiceSummary;
58
+ export interface ProbeHarnessConfigOptions {
59
+ /** Harness names to probe (built-in `claude` / `codex` / `opencode` / `pi` or a registered
60
+ * custom name; any model spec routes like an agent() call's). Default: every routable
61
+ * harness — the four built-ins plus each registered custom backend. */
62
+ harnesses?: string[];
63
+ /** Exact routed model specs to select before reading their model-specific option catalogs. */
64
+ modelSpecs?: string[];
65
+ /** Programmatic custom-backend registry, merged over the AGENTPRISM_BACKENDS env var
66
+ * exactly like `createAcpRunner({ backends })`. */
67
+ backends?: Record<string, CustomBackendConfig>;
68
+ /** Session cwd for the probes. Default `process.cwd()` — harnesses may resolve
69
+ * project-level configuration (and hence their catalog) from it. */
70
+ cwd?: string;
71
+ /** Host-owned no-prompt probe runner. When supplied it is reused and never disposed. */
72
+ probeRunner?: ValidateProbeRunner;
73
+ /** Per-probe cancellation deadline in milliseconds. Default 60,000; lifecycle
74
+ * diagnostics can use a shorter bound. Must be a positive timer-safe integer. */
75
+ probeTimeoutMs?: number;
76
+ /** Maximum concurrent probes, from 1 to 16. Default 4. */
77
+ probeConcurrency?: number;
78
+ /** Shared discovery cancellation budget. Completed catalogs are retained; active
79
+ * probes are aborted and queued targets become failed entries without starting. */
80
+ signal?: AbortSignal;
81
+ }
82
+ export interface HarnessConfigReport {
83
+ /** True when every requested harness probed successfully. */
84
+ ok: boolean;
85
+ /** 0 = all probed; 1 = at least one probe failed. */
86
+ exitCode: 0 | 1;
87
+ /** One entry per requested harness, in request order — the same shape validate reports. */
88
+ harnessOptions: ValidateHarnessOptions[];
89
+ /** Bounded presentation alongside the complete supported catalog. */
90
+ authoringSummary?: HarnessConfigSummary;
91
+ }
92
+ export declare const DEFAULT_PROBE_TIMEOUT_MS = 60000;
93
+ /**
94
+ * Probe each requested harness's advertised config-option catalog. A per-harness
95
+ * spawn/auth/session failure (or timeout) is reported as `probed:false` on that entry —
96
+ * never thrown. Only caller configuration errors throw: a malformed AGENTPRISM_BACKENDS /
97
+ * `backends` registry (loud at construction, mirroring `createAcpRunner`) or invalid options.
98
+ */
99
+ export declare function probeHarnessConfig(options?: ProbeHarnessConfigOptions): Promise<HarnessConfigReport>;
100
+ /** One harness's slice of the `config <harness> --models[=<filter>]` view. Without a
101
+ * filter it carries the provider/group breakdown (never the leaf ids); with a filter it
102
+ * carries only the matching leaf ids. There is no unfiltered leaf dump on any surface. */
103
+ export interface HarnessModelsView {
104
+ backendId: string;
105
+ probed: boolean;
106
+ /** Present when probed=false. */
107
+ error?: string;
108
+ /** False when the harness advertises no `model` select option. */
109
+ hasModelOption: boolean;
110
+ /** The filter as given, when one was supplied. */
111
+ filter?: string;
112
+ /** Breakdown mode (no filter): total leaf count and per-group counts. */
113
+ total?: number;
114
+ groups?: SelectChoiceGroup[];
115
+ /** Filter mode: the leaf model ids matching the filter. */
116
+ matches?: string[];
117
+ }
118
+ /** Compile a `--models=<filter>` value into a leaf-value matcher. A value wrapped in
119
+ * slashes (`/.../`) is a case-insensitive regex; anything else is a case-insensitive
120
+ * substring. Throws a TypeError on an invalid regex (surfaced as a CLI usage error). */
121
+ export declare function buildModelFilter(filter: string): (value: string) => boolean;
122
+ /** Build the per-harness `--models` view. `filter` undefined = breakdown mode. */
123
+ export declare function buildHarnessModelsView(report: HarnessConfigReport, filter?: string): HarnessModelsView[];
124
+ export interface HarnessConfigSummaryModel {
125
+ modelId: string;
126
+ /** Exact executable route; browse selectors never appear here. */
127
+ route: string;
128
+ }
129
+ export interface HarnessConfigSummaryGroup {
130
+ provider: string;
131
+ count: number;
132
+ kind: "provider" | "aggregator";
133
+ /** Presentation only. Expand with modelFilter; never pass to agent(). */
134
+ selector?: string;
135
+ modelFilter: string;
136
+ }
137
+ export interface HarnessConfigSummaryEntry {
138
+ backendId: string;
139
+ /** Exact-model probe scope, when selected before reading options. */
140
+ model?: string;
141
+ probed: boolean;
142
+ error?: string;
143
+ hasModelOption: boolean;
144
+ /** Actual complete live catalog size, before presentation limits/preferences. */
145
+ total: number;
146
+ /** Current selection is separate from preferred models; it need not be preferred. */
147
+ currentModel?: string;
148
+ /** Present only when the current model is an advertised executable leaf. */
149
+ currentRoute?: string;
150
+ omittedCurrentModel?: true;
151
+ models: HarnessConfigSummaryModel[];
152
+ omittedModels: number;
153
+ groups: HarnessConfigSummaryGroup[];
154
+ omittedGroups: number;
155
+ omittedGroupModels: number;
156
+ preferenceSource?: "enabledModels";
157
+ /** Available preferred models before presentation limits. */
158
+ preferredTotal?: number;
159
+ unmatched: string[];
160
+ omittedUnmatched: number;
161
+ }
162
+ export interface HarnessConfigSummary {
163
+ /** One entry per requested probe, including unavailable backends, in request order. */
164
+ harnesses: HarnessConfigSummaryEntry[];
165
+ }
166
+ /** Compact live authoring guidance. Does not mutate or restrict the supported catalog.
167
+ * Lists are bounded per harness; omitted counts and exact expansion filters are explicit.
168
+ * Options such as effort remain authoritative only for the exact probed model. */
169
+ export declare function buildHarnessConfigSummary(report: Pick<HarnessConfigReport, "harnessOptions">): HarnessConfigSummary;
170
+ /** Format the bounded summary for both explicit discovery and missing-route diagnostics. */
171
+ export declare function formatHarnessConfigSummary(summary: HarnessConfigSummary): string;
172
+ export {};
173
+ //# sourceMappingURL=config-catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-catalog.d.ts","sourceRoot":"","sources":["../src/config-catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,408 @@
1
+ // Harness config discovery — the token-free catalog probe behind `agentprism-workflows config`
2
+ // (the CLI lives in @automatalabs/workflows; that facade re-exports everything here) and the
3
+ // SDK's `AcpAgent.probe`. Where validate probes only the harnesses a script routes to, this
4
+ // probes any requested (or every routable) harness WITHOUT a script: one no-prompt ACP session
5
+ // per harness, returning the agent-advertised session config-option catalog plus the effective
6
+ // ACP session-mode catalog — model ids (including bracket variants), effort levels, modes, and
7
+ // every other negotiable option. Zero tokens. Authoring flows run this FIRST so `model` /
8
+ // `configOptions` values come from the live catalog, not memory.
9
+ import { redactText } from "@automatalabs/shared-types";
10
+ import { BUILTIN_BACKEND_IDS } from "./backends/builtins.js";
11
+ import { resolveBackendRegistry } from "./registry.js";
12
+ import { AcpAgentRunner } from "./runner.js";
13
+ let probeFactory = (backends) => new AcpAgentRunner({ backends });
14
+ /** Package-internal hermetic test seam. Deliberately absent from the public index export. */
15
+ export function setConfigProbeFactoryForTests(factory) {
16
+ const previous = probeFactory;
17
+ probeFactory = factory;
18
+ return () => {
19
+ probeFactory = previous;
20
+ };
21
+ }
22
+ /** Every leaf {value,label} the select advertises, flattening any advertised optgroups. */
23
+ export function selectChoicePairs(option) {
24
+ return option.options
25
+ .flatMap((entry) => ("options" in entry ? entry.options : [entry]))
26
+ .map((entry) => ({ value: entry.value, label: entry.name }));
27
+ }
28
+ /** The group a bare (ungrouped) choice value belongs to: its first "/"-segment
29
+ * (pi/opencode ids are "<provider>/<model>"); a value with no "/" is "(ungrouped)". */
30
+ function groupOfValue(value) {
31
+ const slash = value.indexOf("/");
32
+ return slash > 0 ? value.slice(0, slash) : "(ungrouped)";
33
+ }
34
+ /**
35
+ * Group a select's choices for summary display. Prefers the harness-advertised optgroup
36
+ * labels; absent those, groups by the first "/"-segment of each value. Groups come back
37
+ * largest-first, ties broken by first appearance.
38
+ */
39
+ export function summarizeSelectChoices(option) {
40
+ const counts = new Map();
41
+ const order = [];
42
+ const bump = (name, n) => {
43
+ if (!counts.has(name))
44
+ order.push(name);
45
+ counts.set(name, (counts.get(name) ?? 0) + n);
46
+ };
47
+ const hasAdvertisedGroups = option.options.some((entry) => "options" in entry);
48
+ for (const entry of option.options) {
49
+ if ("options" in entry)
50
+ bump(entry.name ?? entry.group, entry.options.length);
51
+ else if (hasAdvertisedGroups)
52
+ bump(groupOfValue(entry.value), 1); // stray leaf beside groups
53
+ else
54
+ bump(groupOfValue(entry.value), 1);
55
+ }
56
+ const total = [...counts.values()].reduce((sum, n) => sum + n, 0);
57
+ const groups = order
58
+ .map((group) => ({ group, count: counts.get(group) ?? 0 }))
59
+ .sort((a, b) => b.count - a.count || order.indexOf(a.group) - order.indexOf(b.group));
60
+ return { total, groups };
61
+ }
62
+ export const DEFAULT_PROBE_TIMEOUT_MS = 60_000;
63
+ /**
64
+ * Probe each requested harness's advertised config-option catalog. A per-harness
65
+ * spawn/auth/session failure (or timeout) is reported as `probed:false` on that entry —
66
+ * never thrown. Only caller configuration errors throw: a malformed AGENTPRISM_BACKENDS /
67
+ * `backends` registry (loud at construction, mirroring `createAcpRunner`) or invalid options.
68
+ */
69
+ export async function probeHarnessConfig(options = {}) {
70
+ const timeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
71
+ const concurrency = options.probeConcurrency ?? 4;
72
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
73
+ throw new TypeError("probeTimeoutMs must be a positive integer no greater than 2147483647");
74
+ }
75
+ if (!Number.isInteger(concurrency) || concurrency <= 0 || concurrency > 16) {
76
+ throw new TypeError("probeConcurrency must be an integer between 1 and 16");
77
+ }
78
+ const cwd = options.cwd ?? process.cwd();
79
+ const registry = resolveBackendRegistry(options.backends);
80
+ const defaultHarnesses = options.probeRunner?.listBackends?.() ?? [...BUILTIN_BACKEND_IDS, ...registry.keys()];
81
+ const harnessTargets = options.harnesses && options.harnesses.length > 0
82
+ ? options.harnesses
83
+ : options.modelSpecs && options.modelSpecs.length > 0
84
+ ? []
85
+ : defaultHarnesses;
86
+ const targets = [
87
+ ...new Map([
88
+ ...harnessTargets.map((spec) => ({ spec, selectModel: false })),
89
+ ...(options.modelSpecs ?? []).map((spec) => ({ spec, selectModel: true })),
90
+ ].map((target) => [JSON.stringify([target.spec, target.selectModel]), target])).values(),
91
+ ];
92
+ const harnessOptions = new Array(targets.length);
93
+ const ownsRunner = options.probeRunner === undefined;
94
+ const runner = options.probeRunner ?? probeFactory(options.backends);
95
+ try {
96
+ let nextTarget = 0;
97
+ await Promise.all(Array.from({ length: Math.min(concurrency, targets.length) }, async () => {
98
+ while (nextTarget < targets.length) {
99
+ const index = nextTarget++;
100
+ const target = targets[index];
101
+ try {
102
+ const result = await withProbeTimeout((signal) => runner.probeConfigOptions(target.spec, {
103
+ cwd,
104
+ selectModel: target.selectModel,
105
+ backends: options.backends,
106
+ signal,
107
+ }), timeoutMs, options.signal);
108
+ harnessOptions[index] = {
109
+ backendId: result.backendId,
110
+ ...(result.defaultModeId === undefined ? {} : { defaultModeId: result.defaultModeId }),
111
+ ...(target.selectModel ? { model: target.spec } : {}),
112
+ probed: true,
113
+ modes: result.modes ?? null,
114
+ options: result.options,
115
+ };
116
+ }
117
+ catch (error) {
118
+ harnessOptions[index] = {
119
+ backendId: target.spec.split("/", 1)[0] ?? target.spec,
120
+ ...(target.selectModel ? { model: target.spec } : {}),
121
+ probed: false,
122
+ error: probeErrorMessage(error),
123
+ };
124
+ }
125
+ }
126
+ }));
127
+ }
128
+ finally {
129
+ if (ownsRunner) {
130
+ try {
131
+ // Always initiate owned cleanup, including after cancellation, without
132
+ // letting stalled disposal extend the caller's discovery budget.
133
+ const disposal = Promise.resolve().then(() => runner.dispose?.());
134
+ void disposal.catch(() => { });
135
+ await withProbeTimeout(() => disposal, timeoutMs, options.signal);
136
+ }
137
+ catch {
138
+ // Probe results are already complete; disposal (e.g. of a timed-out process) is best-effort.
139
+ }
140
+ }
141
+ }
142
+ const ok = harnessOptions.every((harness) => harness.probed);
143
+ return {
144
+ ok, exitCode: ok ? 0 : 1, harnessOptions,
145
+ authoringSummary: buildHarnessConfigSummary({ harnessOptions }),
146
+ };
147
+ }
148
+ /** The `model` select option a harness advertises, if any. */
149
+ function modelSelectOption(harness) {
150
+ return (harness.options ?? []).find((option) => option.type === "select" && option.id === "model");
151
+ }
152
+ /** Compile a `--models=<filter>` value into a leaf-value matcher. A value wrapped in
153
+ * slashes (`/.../`) is a case-insensitive regex; anything else is a case-insensitive
154
+ * substring. Throws a TypeError on an invalid regex (surfaced as a CLI usage error). */
155
+ export function buildModelFilter(filter) {
156
+ if (filter.length >= 2 && filter.startsWith("/") && filter.endsWith("/")) {
157
+ let re;
158
+ try {
159
+ re = new RegExp(filter.slice(1, -1), "i");
160
+ }
161
+ catch (error) {
162
+ throw new TypeError(`--models: invalid regex ${filter} — ${error instanceof Error ? error.message : String(error)}`);
163
+ }
164
+ return (value) => re.test(value);
165
+ }
166
+ const needle = filter.toLowerCase();
167
+ return (value) => value.toLowerCase().includes(needle);
168
+ }
169
+ /** Build the per-harness `--models` view. `filter` undefined = breakdown mode. */
170
+ export function buildHarnessModelsView(report, filter) {
171
+ const match = filter === undefined ? undefined : buildModelFilter(filter);
172
+ return report.harnessOptions.map((harness) => {
173
+ if (!harness.probed) {
174
+ return { backendId: harness.backendId, probed: false, error: harness.error, hasModelOption: false };
175
+ }
176
+ const model = modelSelectOption(harness);
177
+ if (!model) {
178
+ return { backendId: harness.backendId, probed: true, hasModelOption: false };
179
+ }
180
+ if (match === undefined) {
181
+ const { total, groups } = summarizeSelectChoices(model);
182
+ return { backendId: harness.backendId, probed: true, hasModelOption: true, total, groups };
183
+ }
184
+ const matches = selectChoicePairs(model)
185
+ .map((pair) => pair.value)
186
+ .filter((value) => match(value));
187
+ return { backendId: harness.backendId, probed: true, hasModelOption: true, filter, matches };
188
+ });
189
+ }
190
+ /** Bound one probe; the underlying promise keeps its handlers, so a late settle is inert. */
191
+ function withProbeTimeout(op, ms, signal) {
192
+ return new Promise((resolve, reject) => {
193
+ if (signal?.aborted) {
194
+ reject(signal.reason ?? new Error("config discovery cancelled"));
195
+ return;
196
+ }
197
+ const controller = new AbortController();
198
+ const cleanup = () => {
199
+ clearTimeout(timer);
200
+ signal?.removeEventListener("abort", onAbort);
201
+ };
202
+ const abort = (reason) => {
203
+ cleanup();
204
+ controller.abort(reason);
205
+ reject(reason);
206
+ };
207
+ const onAbort = () => abort(signal?.reason ?? new Error("config discovery cancelled"));
208
+ const timer = setTimeout(() => abort(new Error(`probe timed out after ${ms}ms`)), ms);
209
+ signal?.addEventListener("abort", onAbort, { once: true });
210
+ // Keep the deadline alive even if a stalled runner has no referenced handles.
211
+ // Defer invocation so synchronous throws also clear the timer through this path.
212
+ Promise.resolve().then(() => {
213
+ controller.signal.throwIfAborted();
214
+ return op(controller.signal);
215
+ }).then((value) => {
216
+ cleanup();
217
+ resolve(value);
218
+ }, (error) => {
219
+ cleanup();
220
+ reject(error);
221
+ });
222
+ });
223
+ }
224
+ function probeErrorMessage(error) {
225
+ return redactText(error instanceof Error ? error.message : String(error)).value;
226
+ }
227
+ // ── The bounded authoring summary ──
228
+ const MAX_ENTRIES = 24;
229
+ const MAX_VALUE_LENGTH = 240;
230
+ // Deliberate classification by exact provider id, never a substring/name heuristic.
231
+ const OPENCODE_AGGREGATORS = new Set([
232
+ "openrouter", "opencode", "opencode-go", "huggingface", "amazon-bedrock", "github-copilot",
233
+ ]);
234
+ const MODEL_DISCOVERY_META = "@automatalabs/agentprism.modelDiscovery";
235
+ function boundedLabel(value) {
236
+ return value.length <= MAX_VALUE_LENGTH
237
+ ? value
238
+ : `${value.slice(0, MAX_VALUE_LENGTH)}… (${value.length - MAX_VALUE_LENGTH} characters omitted)`;
239
+ }
240
+ function display(value) {
241
+ return boundedLabel(redactText(value).value);
242
+ }
243
+ function providerOf(value) {
244
+ const slash = value.indexOf("/");
245
+ return slash > 0 ? value.slice(0, slash) : "(ungrouped)";
246
+ }
247
+ function providerFilter(provider) {
248
+ return provider === "(ungrouped)"
249
+ ? "/^[^/]+$/"
250
+ : `/^${provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\//`;
251
+ }
252
+ function preferences(value) {
253
+ if (!value || typeof value !== "object")
254
+ return undefined;
255
+ const meta = value;
256
+ if (meta.source !== "enabledModels" || !Array.isArray(meta.preferred) || !Array.isArray(meta.unmatched)) {
257
+ return undefined;
258
+ }
259
+ if (!meta.preferred.every((id) => typeof id === "string") || !meta.unmatched.every((id) => typeof id === "string")) {
260
+ return undefined;
261
+ }
262
+ return { preferred: meta.preferred, unmatched: meta.unmatched };
263
+ }
264
+ /** Compact live authoring guidance. Does not mutate or restrict the supported catalog.
265
+ * Lists are bounded per harness; omitted counts and exact expansion filters are explicit.
266
+ * Options such as effort remain authoritative only for the exact probed model. */
267
+ export function buildHarnessConfigSummary(report) {
268
+ return {
269
+ harnesses: report.harnessOptions.map((harness) => {
270
+ const entry = {
271
+ backendId: boundedLabel(harness.backendId),
272
+ ...(harness.model === undefined ? {} : { model: boundedLabel(harness.model) }),
273
+ probed: harness.probed,
274
+ ...(!harness.probed ? { error: display(harness.error ?? "unknown error") } : {}),
275
+ hasModelOption: false,
276
+ total: 0, models: [], omittedModels: 0, groups: [], omittedGroups: 0,
277
+ omittedGroupModels: 0, unmatched: [], omittedUnmatched: 0,
278
+ };
279
+ if (!harness.probed)
280
+ return entry;
281
+ const model = harness.options?.find((option) => option.id === "model" && option.type === "select");
282
+ if (!model || model.type !== "select")
283
+ return entry;
284
+ entry.hasModelOption = true;
285
+ const ids = selectChoicePairs(model).map(({ value }) => value);
286
+ entry.total = ids.length;
287
+ if (model.currentValue) {
288
+ const route = `${harness.backendId}/${model.currentValue}`;
289
+ if (route.length <= MAX_VALUE_LENGTH) {
290
+ entry.currentModel = model.currentValue;
291
+ if (ids.includes(model.currentValue) && !model.currentValue.includes("*"))
292
+ entry.currentRoute = route;
293
+ }
294
+ else
295
+ entry.omittedCurrentModel = true;
296
+ }
297
+ const groups = new Map();
298
+ for (const id of ids) {
299
+ const provider = providerOf(id);
300
+ groups.set(provider, (groups.get(provider) ?? 0) + 1);
301
+ }
302
+ const isAggregator = (provider) => harness.backendId === "opencode" && OPENCODE_AGGREGATORS.has(provider);
303
+ // Stable sorting preserves catalog provider order within each classification.
304
+ const orderedGroups = [...groups].sort(([a], [b]) => Number(isAggregator(a)) - Number(isAggregator(b)));
305
+ for (const [provider, count] of orderedGroups) {
306
+ if (entry.groups.length >= MAX_ENTRIES || provider.length > MAX_VALUE_LENGTH) {
307
+ entry.omittedGroups++;
308
+ entry.omittedGroupModels += count;
309
+ continue;
310
+ }
311
+ entry.groups.push({
312
+ provider, count,
313
+ kind: isAggregator(provider) ? "aggregator" : "provider",
314
+ ...(isAggregator(provider) ? { selector: `${provider}/*` } : {}),
315
+ modelFilter: providerFilter(provider),
316
+ });
317
+ }
318
+ const preferred = harness.backendId === "pi" ? preferences(model._meta?.[MODEL_DISCOVERY_META]) : undefined;
319
+ let candidates;
320
+ if (preferred) {
321
+ const available = new Set(ids);
322
+ candidates = [...new Set(preferred.preferred)].filter((id) => available.has(id));
323
+ entry.preferenceSource = "enabledModels";
324
+ entry.preferredTotal = candidates.length;
325
+ entry.unmatched = preferred.unmatched.slice(0, MAX_ENTRIES).map(display);
326
+ entry.omittedUnmatched = preferred.unmatched.length - entry.unmatched.length;
327
+ }
328
+ else if (harness.backendId === "pi") {
329
+ candidates = [];
330
+ }
331
+ else if (harness.backendId === "opencode") {
332
+ // When direct catalogs exceed the display bound, represent each configured
333
+ // direct provider before filling more rows from any one provider.
334
+ const direct = new Map();
335
+ for (const id of ids) {
336
+ const provider = providerOf(id);
337
+ if (isAggregator(provider))
338
+ continue;
339
+ const group = direct.get(provider) ?? [];
340
+ group.push(id);
341
+ direct.set(provider, group);
342
+ }
343
+ candidates = [];
344
+ for (let index = 0; [...direct.values()].some((values) => index < values.length); index++) {
345
+ for (const values of direct.values())
346
+ if (values[index] !== undefined)
347
+ candidates.push(values[index]);
348
+ }
349
+ }
350
+ else {
351
+ candidates = ids.filter((id) => !isAggregator(providerOf(id)));
352
+ }
353
+ for (const id of candidates) {
354
+ const route = `${harness.backendId}/${id}`;
355
+ if (entry.models.length >= MAX_ENTRIES || route.length > MAX_VALUE_LENGTH || id.includes("*"))
356
+ continue;
357
+ entry.models.push({ modelId: id, route });
358
+ }
359
+ entry.omittedModels = entry.total - entry.models.length;
360
+ return entry;
361
+ }),
362
+ };
363
+ }
364
+ /** Format the bounded summary for both explicit discovery and missing-route diagnostics. */
365
+ export function formatHarnessConfigSummary(summary) {
366
+ const lines = ["authoring model summary:"];
367
+ if (!summary.harnesses.length)
368
+ lines.push(" (no harnesses requested)");
369
+ for (const harness of summary.harnesses) {
370
+ const label = harness.model ?? harness.backendId;
371
+ if (!harness.probed) {
372
+ lines.push(` ${JSON.stringify(label)}: unavailable — ${JSON.stringify(harness.error)}`);
373
+ continue;
374
+ }
375
+ if (!harness.hasModelOption) {
376
+ lines.push(` ${JSON.stringify(label)}: no model option advertised`);
377
+ continue;
378
+ }
379
+ lines.push(` ${JSON.stringify(label)}: ${harness.total} supported model(s)`);
380
+ if (harness.currentModel !== undefined) {
381
+ lines.push(` current model: ${JSON.stringify(harness.currentRoute ?? harness.currentModel)}`);
382
+ }
383
+ if (harness.omittedCurrentModel)
384
+ lines.push(" current model omitted by summary length limit; inspect the model config option");
385
+ if (harness.preferenceSource) {
386
+ lines.push(` Pi enabledModels: ${harness.preferredTotal} available preferred model(s); presentation shortlist, not an execution allowlist`);
387
+ }
388
+ else if (harness.backendId === "pi") {
389
+ lines.push(" No enabledModels preference metadata; showing available provider groups");
390
+ }
391
+ for (const model of harness.models)
392
+ lines.push(` model: ${JSON.stringify(model.route)}`);
393
+ if (harness.omittedModels)
394
+ lines.push(` ${harness.omittedModels} supported model(s) not listed as exact routes here; expand with modelFilter`);
395
+ for (const group of harness.groups) {
396
+ lines.push(` ${group.kind === "aggregator" ? "browse only (not executable)" : "provider"}: ${JSON.stringify(group.selector ?? group.provider)} (${group.count} models); modelFilter: ${JSON.stringify(group.modelFilter)}`);
397
+ }
398
+ if (harness.omittedGroups)
399
+ lines.push(` ${harness.omittedGroups} provider group(s), ${harness.omittedGroupModels} models omitted by summary limits; use config modelFilter`);
400
+ if (harness.unmatched.length)
401
+ lines.push(` unmatched enabledModels patterns: ${harness.unmatched.map((value) => JSON.stringify(value)).join(", ")}`);
402
+ if (harness.omittedUnmatched)
403
+ lines.push(` ${harness.omittedUnmatched} additional unmatched enabledModels pattern(s) omitted by summary limits`);
404
+ }
405
+ lines.push(' Browse wildcards are not executable. Expand with config harnesses:["<backend>"], modelFilter:"<substring or /regex/>"; use an exact returned leaf route in agent(prompt, { model }).');
406
+ lines.push(' Default-model options do not describe every model. Probe config modelSpecs:["<exact route>"] before choosing mode/effort/configOptions. A backend-only model route explicitly selects its configured default.');
407
+ return lines.join("\n");
408
+ }