@agentex/agent 0.0.23 → 0.0.26

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 (177) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -0
  4. package/dist/derived.d.ts +5 -3
  5. package/dist/derived.d.ts.map +1 -1
  6. package/dist/derived.js +11 -7
  7. package/dist/derived.js.map +1 -1
  8. package/dist/index.d.ts +7 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/providers/acp/index.d.ts +1 -1
  13. package/dist/providers/acp/index.d.ts.map +1 -1
  14. package/dist/providers/acp/index.js +5 -97
  15. package/dist/providers/acp/index.js.map +1 -1
  16. package/dist/providers/acp/session.d.ts +8 -1
  17. package/dist/providers/acp/session.d.ts.map +1 -1
  18. package/dist/providers/acp/session.js +94 -0
  19. package/dist/providers/acp/session.js.map +1 -1
  20. package/dist/providers/claude/attach.d.ts +8 -0
  21. package/dist/providers/claude/attach.d.ts.map +1 -0
  22. package/dist/providers/claude/attach.js +113 -0
  23. package/dist/providers/claude/attach.js.map +1 -0
  24. package/dist/providers/claude/execute.d.ts.map +1 -1
  25. package/dist/providers/claude/execute.js +17 -2
  26. package/dist/providers/claude/execute.js.map +1 -1
  27. package/dist/providers/claude/goal-capability.d.ts +15 -0
  28. package/dist/providers/claude/goal-capability.d.ts.map +1 -0
  29. package/dist/providers/claude/goal-capability.js +20 -0
  30. package/dist/providers/claude/goal-capability.js.map +1 -0
  31. package/dist/providers/claude/index.d.ts.map +1 -1
  32. package/dist/providers/claude/index.js +8 -4
  33. package/dist/providers/claude/index.js.map +1 -1
  34. package/dist/providers/claude/session.d.ts +11 -9
  35. package/dist/providers/claude/session.d.ts.map +1 -1
  36. package/dist/providers/claude/session.js +36 -14
  37. package/dist/providers/claude/session.js.map +1 -1
  38. package/dist/providers/codex/attach.d.ts +9 -0
  39. package/dist/providers/codex/attach.d.ts.map +1 -0
  40. package/dist/providers/codex/attach.js +93 -0
  41. package/dist/providers/codex/attach.js.map +1 -0
  42. package/dist/providers/codex/execute.d.ts.map +1 -1
  43. package/dist/providers/codex/execute.js +17 -3
  44. package/dist/providers/codex/execute.js.map +1 -1
  45. package/dist/providers/codex/goal-capability.d.ts +13 -0
  46. package/dist/providers/codex/goal-capability.d.ts.map +1 -0
  47. package/dist/providers/codex/goal-capability.js +18 -0
  48. package/dist/providers/codex/goal-capability.js.map +1 -0
  49. package/dist/providers/codex/index.d.ts +1 -0
  50. package/dist/providers/codex/index.d.ts.map +1 -1
  51. package/dist/providers/codex/index.js +9 -6
  52. package/dist/providers/codex/index.js.map +1 -1
  53. package/dist/providers/codex/session.d.ts +11 -7
  54. package/dist/providers/codex/session.d.ts.map +1 -1
  55. package/dist/providers/codex/session.js +37 -12
  56. package/dist/providers/codex/session.js.map +1 -1
  57. package/dist/providers/codex/transcript-normalize.d.ts +28 -0
  58. package/dist/providers/codex/transcript-normalize.d.ts.map +1 -0
  59. package/dist/providers/codex/transcript-normalize.js +191 -0
  60. package/dist/providers/codex/transcript-normalize.js.map +1 -0
  61. package/dist/providers/cursor/index.d.ts.map +1 -1
  62. package/dist/providers/cursor/index.js +2 -2
  63. package/dist/providers/cursor/index.js.map +1 -1
  64. package/dist/providers/openclaw/index.d.ts.map +1 -1
  65. package/dist/providers/openclaw/index.js +2 -2
  66. package/dist/providers/openclaw/index.js.map +1 -1
  67. package/dist/providers/opencode/index.d.ts.map +1 -1
  68. package/dist/providers/opencode/index.js +3 -5
  69. package/dist/providers/opencode/index.js.map +1 -1
  70. package/dist/providers/pi/index.d.ts.map +1 -1
  71. package/dist/providers/pi/index.js +3 -5
  72. package/dist/providers/pi/index.js.map +1 -1
  73. package/dist/providers/process/index.d.ts.map +1 -1
  74. package/dist/providers/process/index.js +2 -2
  75. package/dist/providers/process/index.js.map +1 -1
  76. package/dist/registry.d.ts +0 -1
  77. package/dist/registry.d.ts.map +1 -1
  78. package/dist/registry.js +0 -4
  79. package/dist/registry.js.map +1 -1
  80. package/dist/sessions/index.d.ts +3 -0
  81. package/dist/sessions/index.d.ts.map +1 -0
  82. package/dist/sessions/index.js +2 -0
  83. package/dist/sessions/index.js.map +1 -0
  84. package/dist/sessions/record.d.ts +43 -0
  85. package/dist/sessions/record.d.ts.map +1 -0
  86. package/dist/sessions/record.js +85 -0
  87. package/dist/sessions/record.js.map +1 -0
  88. package/dist/types.d.ts +176 -0
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js.map +1 -1
  91. package/dist/utils/endpoint.d.ts +38 -0
  92. package/dist/utils/endpoint.d.ts.map +1 -0
  93. package/dist/utils/endpoint.js +151 -0
  94. package/dist/utils/endpoint.js.map +1 -0
  95. package/dist/utils/env.d.ts.map +1 -1
  96. package/dist/utils/env.js +5 -1
  97. package/dist/utils/env.js.map +1 -1
  98. package/dist/utils/uuid.d.ts +7 -1
  99. package/dist/utils/uuid.d.ts.map +1 -1
  100. package/dist/utils/uuid.js +21 -1
  101. package/dist/utils/uuid.js.map +1 -1
  102. package/package.json +64 -7
  103. package/src/derived.ts +311 -0
  104. package/src/goals/controller.ts +442 -0
  105. package/src/goals/index.ts +21 -0
  106. package/src/goals/normalize.ts +173 -0
  107. package/src/goals/sentinel.ts +90 -0
  108. package/src/index.ts +270 -0
  109. package/src/providers/_shared/http-agent.ts +304 -0
  110. package/src/providers/acp/index.ts +103 -0
  111. package/src/providers/acp/parse.ts +131 -0
  112. package/src/providers/acp/session.ts +744 -0
  113. package/src/providers/claude/attach.ts +147 -0
  114. package/src/providers/claude/codec.ts +43 -0
  115. package/src/providers/claude/execute.ts +300 -0
  116. package/src/providers/claude/goal-capability.ts +21 -0
  117. package/src/providers/claude/index.ts +72 -0
  118. package/src/providers/claude/mcp.ts +82 -0
  119. package/src/providers/claude/parse.ts +824 -0
  120. package/src/providers/claude/session.ts +1192 -0
  121. package/src/providers/claude/transcript.ts +555 -0
  122. package/src/providers/codex/attach.ts +123 -0
  123. package/src/providers/codex/codec.ts +50 -0
  124. package/src/providers/codex/execute.ts +337 -0
  125. package/src/providers/codex/goal-capability.ts +19 -0
  126. package/src/providers/codex/index.ts +57 -0
  127. package/src/providers/codex/modes.ts +159 -0
  128. package/src/providers/codex/parse.ts +691 -0
  129. package/src/providers/codex/plan-mode.ts +49 -0
  130. package/src/providers/codex/session.ts +1287 -0
  131. package/src/providers/codex/transcript-normalize.ts +197 -0
  132. package/src/providers/codex/transcript.ts +487 -0
  133. package/src/providers/codex/usage-scanner.ts +178 -0
  134. package/src/providers/copilot/index.ts +19 -0
  135. package/src/providers/cursor/codec.ts +44 -0
  136. package/src/providers/cursor/execute.ts +271 -0
  137. package/src/providers/cursor/index.ts +25 -0
  138. package/src/providers/cursor/parse.ts +288 -0
  139. package/src/providers/gemini/index.ts +21 -0
  140. package/src/providers/openclaw/codec.ts +40 -0
  141. package/src/providers/openclaw/execute.ts +19 -0
  142. package/src/providers/openclaw/index.ts +29 -0
  143. package/src/providers/opencode/codec.ts +50 -0
  144. package/src/providers/opencode/event-parse.ts +141 -0
  145. package/src/providers/opencode/execute.ts +251 -0
  146. package/src/providers/opencode/http-session.ts +427 -0
  147. package/src/providers/opencode/index.ts +30 -0
  148. package/src/providers/opencode/parse.ts +203 -0
  149. package/src/providers/opencode/server.ts +0 -0
  150. package/src/providers/pi/codec.ts +44 -0
  151. package/src/providers/pi/execute.ts +297 -0
  152. package/src/providers/pi/index.ts +30 -0
  153. package/src/providers/pi/parse.ts +231 -0
  154. package/src/providers/pi/session.ts +381 -0
  155. package/src/providers/process/execute.ts +148 -0
  156. package/src/providers/process/index.ts +52 -0
  157. package/src/registry.ts +40 -0
  158. package/src/sessions/index.ts +8 -0
  159. package/src/sessions/record.ts +108 -0
  160. package/src/types.ts +1638 -0
  161. package/src/utils/ask-user-question.ts +57 -0
  162. package/src/utils/auth.ts +661 -0
  163. package/src/utils/binary.ts +179 -0
  164. package/src/utils/endpoint.ts +172 -0
  165. package/src/utils/env.ts +63 -0
  166. package/src/utils/execute-all.ts +68 -0
  167. package/src/utils/exit-plan-mode.ts +40 -0
  168. package/src/utils/instructions.ts +427 -0
  169. package/src/utils/process.ts +223 -0
  170. package/src/utils/runtime-config.ts +100 -0
  171. package/src/utils/runtime-homes.ts +49 -0
  172. package/src/utils/skill-commands.ts +493 -0
  173. package/src/utils/skills.ts +500 -0
  174. package/src/utils/template.ts +16 -0
  175. package/src/utils/tool-names.ts +51 -0
  176. package/src/utils/uuid.ts +21 -0
  177. package/src/utils/workspace.ts +156 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentex/agent",
3
- "version": "0.0.23",
3
+ "version": "0.0.26",
4
4
  "description": "Programmatic execution of AI coding agents (Claude Code, Codex, OpenClaw)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,14 +14,70 @@
14
14
  "type": "module",
15
15
  "exports": {
16
16
  ".": {
17
+ "types": "./dist/index.d.ts",
17
18
  "import": "./dist/index.js",
18
- "types": "./dist/index.d.ts"
19
- }
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./registry": {
22
+ "types": "./dist/registry.d.ts",
23
+ "import": "./dist/registry.js",
24
+ "default": "./dist/registry.js"
25
+ },
26
+ "./derived": {
27
+ "types": "./dist/derived.d.ts",
28
+ "import": "./dist/derived.js",
29
+ "default": "./dist/derived.js"
30
+ },
31
+ "./types": {
32
+ "types": "./dist/types.d.ts",
33
+ "import": "./dist/types.js",
34
+ "default": "./dist/types.js"
35
+ },
36
+ "./goals": {
37
+ "types": "./dist/goals/index.d.ts",
38
+ "import": "./dist/goals/index.js",
39
+ "default": "./dist/goals/index.js"
40
+ },
41
+ "./sessions": {
42
+ "types": "./dist/sessions/index.d.ts",
43
+ "import": "./dist/sessions/index.js",
44
+ "default": "./dist/sessions/index.js"
45
+ },
46
+ "./utils/*": {
47
+ "types": "./dist/utils/*.d.ts",
48
+ "import": "./dist/utils/*.js",
49
+ "default": "./dist/utils/*.js"
50
+ },
51
+ "./providers/*": {
52
+ "types": "./dist/providers/*/index.d.ts",
53
+ "import": "./dist/providers/*/index.js",
54
+ "default": "./dist/providers/*/index.js"
55
+ },
56
+ "./providers/claude/parse": {
57
+ "types": "./dist/providers/claude/parse.d.ts",
58
+ "import": "./dist/providers/claude/parse.js",
59
+ "default": "./dist/providers/claude/parse.js"
60
+ },
61
+ "./providers/claude/transcript": {
62
+ "types": "./dist/providers/claude/transcript.d.ts",
63
+ "import": "./dist/providers/claude/transcript.js",
64
+ "default": "./dist/providers/claude/transcript.js"
65
+ },
66
+ "./providers/codex/transcript": {
67
+ "types": "./dist/providers/codex/transcript.d.ts",
68
+ "import": "./dist/providers/codex/transcript.js",
69
+ "default": "./dist/providers/codex/transcript.js"
70
+ },
71
+ "./package.json": "./package.json"
20
72
  },
73
+ "sideEffects": false,
21
74
  "main": "./dist/index.js",
22
75
  "types": "./dist/index.d.ts",
23
76
  "files": [
24
- "dist"
77
+ "dist",
78
+ "src",
79
+ "CHANGELOG.md",
80
+ "LICENSE"
25
81
  ],
26
82
  "keywords": [
27
83
  "ai",
@@ -35,19 +91,19 @@
35
91
  "author": "",
36
92
  "license": "MIT",
37
93
  "engines": {
38
- "node": ">=18.0.0"
94
+ "node": ">=20.19.0"
39
95
  },
40
96
  "devDependencies": {
41
97
  "@mariozechner/pi-coding-agent": "^0.62.0",
42
98
  "@types/node": "^25.3.5",
99
+ "esbuild": "^0.27.0",
43
100
  "opencode-ai": "^1.3.2",
44
101
  "tsx": "^4.21.0",
45
102
  "typescript": "^5.7.0",
46
103
  "vitest": "^3.0.0"
47
104
  },
48
105
  "dependencies": {
49
- "@agentclientprotocol/sdk": "^0.24.0",
50
- "uuid": "^13.0.0"
106
+ "@agentclientprotocol/sdk": "^0.24.0"
51
107
  },
52
108
  "scripts": {
53
109
  "build": "tsc",
@@ -57,6 +113,7 @@
57
113
  "smoke": "tsx scripts/smoke.ts",
58
114
  "smoke:mock": "tsx scripts/smoke.ts --mock",
59
115
  "smoke:concurrent": "tsx scripts/concurrent-smoke.ts",
116
+ "demo:durable": "tsx scripts/durable-session-demo.ts",
60
117
  "list-models": "tsx scripts/list-models.ts"
61
118
  }
62
119
  }
package/src/derived.ts ADDED
@@ -0,0 +1,311 @@
1
+ import type {
2
+ ProviderCapabilities,
3
+ ProviderConfig,
4
+ ProviderModel,
5
+ ProviderModule,
6
+ } from "./types.js";
7
+ import { getProvider, registerProvider } from "./registry.js";
8
+ import { acpProvider } from "./providers/acp/index.js";
9
+
10
+ /**
11
+ * Declarative configuration for a *derived* provider — a new provider id that
12
+ * inherits a built-in provider's behavior with an environment / command / model
13
+ * overlay. The canonical use is BYOK gateways: point Claude at an
14
+ * Anthropic-compatible endpoint (z.ai, Qwen, a local proxy) by extending
15
+ * `"claude"` and setting `env.ANTHROPIC_BASE_URL`, with no new code.
16
+ */
17
+ export interface DerivedProviderConfig {
18
+ /** Unique id for the new provider (used with `getProvider`/`registerProvider`). */
19
+ id: string;
20
+ /** A registered provider to inherit from (e.g. "claude", "codex"). For ACP
21
+ * agents use the config-file loader's `extends: "acp"` form instead. */
22
+ extends: string;
23
+ /** Display label (informational). */
24
+ label?: string;
25
+ /** Description (informational). */
26
+ description?: string;
27
+ /** Override the binary/command the base provider spawns. Per-call
28
+ * `config.command` still wins. */
29
+ command?: string;
30
+ /** Environment overlay applied to every execute / session / auth / modes /
31
+ * quota call (e.g. `ANTHROPIC_BASE_URL`). Per-call `ctx.env` overrides it. */
32
+ env?: Record<string, string>;
33
+ /** Replace the model list surfaced by `listModels()`. */
34
+ models?: ProviderModel[];
35
+ /** Default `config.modeId` applied when the caller doesn't set one. */
36
+ modeId?: string;
37
+ /** When `false`, `loadProvidersFromConfig` skips this entry. */
38
+ enabled?: boolean;
39
+ }
40
+
41
+ /** Thrown when a provider config object is malformed. Branch on it; don't parse. */
42
+ export class MalformedProviderConfigError extends Error {
43
+ readonly path?: string;
44
+ constructor(message: string, path?: string) {
45
+ super(message);
46
+ this.name = "MalformedProviderConfigError";
47
+ this.path = path;
48
+ }
49
+ }
50
+
51
+ /** Signature of the ACP factory registered by the ACP provider module. */
52
+ export type AcpFactory = (config: {
53
+ id: string;
54
+ command: string[];
55
+ env?: Record<string, string>;
56
+ label?: string;
57
+ models?: ProviderModel[];
58
+ modeId?: string;
59
+ }) => ProviderModule;
60
+
61
+ let acpFactory: AcpFactory | null = null;
62
+
63
+ /**
64
+ * Override the factory used to build `extends: "acp"` providers. Optional — the
65
+ * loader defaults to the built-in `acpProvider` (imported statically here, which
66
+ * is cheap: the ACP SDK is only loaded when a session/execute actually runs).
67
+ * Kept for tests/hosts that need to swap in a custom ACP factory; it is no
68
+ * longer required before `loadProvidersFromConfig`.
69
+ */
70
+ export function registerAcpFactory(factory: AcpFactory): void {
71
+ acpFactory = factory;
72
+ }
73
+
74
+ /** @internal — for tests. */
75
+ export function getAcpFactory(): AcpFactory | null {
76
+ return acpFactory;
77
+ }
78
+
79
+ /**
80
+ * Build a derived `ProviderModule` from a base provider plus an overlay. The
81
+ * returned module is NOT auto-registered — pass it to `registerProvider`, or
82
+ * use `loadProvidersFromConfig` which registers for you.
83
+ */
84
+ export function defineDerivedProvider(cfg: DerivedProviderConfig): ProviderModule {
85
+ if (!cfg.id || typeof cfg.id !== "string") {
86
+ throw new MalformedProviderConfigError("derived provider requires a non-empty string id");
87
+ }
88
+ if (cfg.extends === "acp") {
89
+ throw new MalformedProviderConfigError(
90
+ `provider "${cfg.id}": extends "acp" is built by loadProvidersFromConfig (or acpProvider() directly), not defineDerivedProvider`,
91
+ cfg.id,
92
+ );
93
+ }
94
+ const base = getProvider(cfg.extends); // throws on unknown base
95
+
96
+ const overlayEnv = (env?: Record<string, string>): Record<string, string> => ({
97
+ ...cfg.env,
98
+ ...env,
99
+ });
100
+ const overlayConfig = (config?: ProviderConfig): ProviderConfig => ({
101
+ ...config,
102
+ ...(cfg.command && !config?.command ? { command: cfg.command } : {}),
103
+ ...(cfg.modeId && !config?.modeId ? { modeId: cfg.modeId } : {}),
104
+ });
105
+
106
+ const capabilities: ProviderCapabilities = {
107
+ ...base.capabilities,
108
+ ...(cfg.models ? { modelDiscovery: true } : {}),
109
+ };
110
+
111
+ const derived: ProviderModule = {
112
+ ...base,
113
+ type: cfg.id,
114
+ capabilities,
115
+ execute: (ctx) =>
116
+ base.execute({ ...ctx, env: overlayEnv(ctx.env), config: overlayConfig(ctx.config) }),
117
+ resolveAuth: (authCtx) =>
118
+ base.resolveAuth({
119
+ ...authCtx,
120
+ env: overlayEnv(authCtx?.env),
121
+ command: authCtx?.command ?? cfg.command,
122
+ }),
123
+ };
124
+
125
+ if (base.createSession) {
126
+ const baseCreate = base.createSession.bind(base);
127
+ derived.createSession = (ctx) =>
128
+ baseCreate({ ...ctx, env: overlayEnv(ctx.env), config: overlayConfig(ctx.config) });
129
+ }
130
+ if (cfg.models) {
131
+ const models = cfg.models;
132
+ derived.listModels = async () => models;
133
+ } else if (base.listModels) {
134
+ const baseList = base.listModels.bind(base);
135
+ derived.listModels = (opts) => baseList(opts);
136
+ }
137
+ if (base.listModes) {
138
+ const baseModes = base.listModes.bind(base);
139
+ derived.listModes = (opts) =>
140
+ baseModes({ ...opts, env: overlayEnv(opts?.env), config: overlayConfig(opts?.config) });
141
+ }
142
+ if (base.checkQuota) {
143
+ const baseQuota = base.checkQuota.bind(base);
144
+ derived.checkQuota = (qctx) =>
145
+ baseQuota({ ...qctx, env: overlayEnv(qctx?.env), config: overlayConfig(qctx?.config) });
146
+ }
147
+
148
+ return derived;
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Config-file loader
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /** A raw provider entry in a config file (id comes from the map key). */
156
+ interface RawProviderEntry {
157
+ extends?: unknown;
158
+ label?: unknown;
159
+ description?: unknown;
160
+ command?: unknown;
161
+ env?: unknown;
162
+ models?: unknown;
163
+ modeId?: unknown;
164
+ enabled?: unknown;
165
+ }
166
+
167
+ function isRecord(v: unknown): v is Record<string, unknown> {
168
+ return typeof v === "object" && v !== null && !Array.isArray(v);
169
+ }
170
+
171
+ function toCommandArray(command: unknown, path: string): string[] {
172
+ if (typeof command === "string") {
173
+ const parts = command.trim().split(/\s+/).filter(Boolean);
174
+ if (parts.length === 0) throw new MalformedProviderConfigError(`${path}.command is empty`, path);
175
+ return parts;
176
+ }
177
+ if (Array.isArray(command) && command.every((c) => typeof c === "string") && command.length > 0) {
178
+ return command as string[];
179
+ }
180
+ throw new MalformedProviderConfigError(
181
+ `${path}.command must be a non-empty string or string[] for an ACP provider`,
182
+ path,
183
+ );
184
+ }
185
+
186
+ function toCommandString(command: unknown, path: string): string | undefined {
187
+ if (command === undefined) return undefined;
188
+ if (typeof command === "string") return command;
189
+ if (Array.isArray(command)) {
190
+ // agentex's `config.command` is a single binary (args go through the agent's
191
+ // own config), so a multi-element array would silently drop everything past
192
+ // the binary — reject it rather than mislead. (ACP providers use the array
193
+ // form via the `extends: "acp"` branch instead.)
194
+ if (command.length === 1 && typeof command[0] === "string") return command[0];
195
+ throw new MalformedProviderConfigError(
196
+ `${path}.command for a non-ACP provider must be a single binary string; got an array of ${command.length} (use extends: "acp" for binary+args)`,
197
+ path,
198
+ );
199
+ }
200
+ return undefined;
201
+ }
202
+
203
+ function validateEnv(env: unknown, path: string): Record<string, string> | undefined {
204
+ if (env === undefined) return undefined;
205
+ if (!isRecord(env)) throw new MalformedProviderConfigError(`${path}.env must be an object`, path);
206
+ const out: Record<string, string> = {};
207
+ for (const [k, v] of Object.entries(env)) {
208
+ if (typeof v !== "string") {
209
+ throw new MalformedProviderConfigError(`${path}.env.${k} must be a string`, path);
210
+ }
211
+ out[k] = v;
212
+ }
213
+ return out;
214
+ }
215
+
216
+ function validateModels(models: unknown, path: string): ProviderModel[] | undefined {
217
+ if (models === undefined) return undefined;
218
+ if (!Array.isArray(models)) throw new MalformedProviderConfigError(`${path}.models must be an array`, path);
219
+ return models.map((m, i) => {
220
+ if (!isRecord(m) || typeof m["id"] !== "string") {
221
+ throw new MalformedProviderConfigError(`${path}.models[${i}] must have a string id`, path);
222
+ }
223
+ return {
224
+ id: m["id"],
225
+ name: typeof m["name"] === "string" ? m["name"] : m["id"],
226
+ ...(typeof m["provider"] === "string" ? { provider: m["provider"] } : {}),
227
+ };
228
+ });
229
+ }
230
+
231
+ /** Locate the providers map, accepting `{ providers }` or Paseo's
232
+ * `{ agents: { providers } }` nesting. */
233
+ function findProvidersMap(raw: unknown): Record<string, unknown> {
234
+ if (!isRecord(raw)) throw new MalformedProviderConfigError("config must be an object");
235
+ if (isRecord(raw["providers"])) return raw["providers"];
236
+ if (isRecord(raw["agents"]) && isRecord((raw["agents"] as Record<string, unknown>)["providers"])) {
237
+ return (raw["agents"] as Record<string, unknown>)["providers"] as Record<string, unknown>;
238
+ }
239
+ throw new MalformedProviderConfigError(
240
+ "config must contain a `providers` (or `agents.providers`) object",
241
+ );
242
+ }
243
+
244
+ /**
245
+ * Build provider modules from a config object and (by default) register them.
246
+ * Supports `extends: "<builtin>"` (env/command/model overlay) and
247
+ * `extends: "acp"` (build an ACP provider from `command`). Throws
248
+ * `MalformedProviderConfigError` on a bad shape.
249
+ */
250
+ export function loadProvidersFromConfig(
251
+ raw: unknown,
252
+ options?: { register?: boolean },
253
+ ): ProviderModule[] {
254
+ const register = options?.register ?? true;
255
+ const providersMap = findProvidersMap(raw);
256
+ const built: ProviderModule[] = [];
257
+
258
+ for (const [id, value] of Object.entries(providersMap)) {
259
+ const path = `providers.${id}`;
260
+ if (!isRecord(value)) throw new MalformedProviderConfigError(`${path} must be an object`, path);
261
+ const entry = value as RawProviderEntry;
262
+
263
+ if (entry.enabled === false) continue;
264
+ if (typeof entry.extends !== "string" || entry.extends.length === 0) {
265
+ throw new MalformedProviderConfigError(`${path}.extends must be a non-empty string`, path);
266
+ }
267
+ const label = typeof entry.label === "string" ? entry.label : undefined;
268
+ const description = typeof entry.description === "string" ? entry.description : undefined;
269
+ const env = validateEnv(entry.env, path);
270
+ const models = validateModels(entry.models, path);
271
+ const modeId = typeof entry.modeId === "string" ? entry.modeId : undefined;
272
+
273
+ let provider: ProviderModule;
274
+ if (entry.extends === "acp") {
275
+ // The ACP provider module is imported statically above, so `acpProvider`
276
+ // is always the default factory. `registerAcpFactory` stays supported as
277
+ // an override hook (tests/hosts can swap in a custom factory).
278
+ const factory = acpFactory ?? acpProvider;
279
+ provider = factory({
280
+ id,
281
+ command: toCommandArray(entry.command, path),
282
+ ...(env ? { env } : {}),
283
+ ...(label ? { label } : {}),
284
+ ...(models ? { models } : {}),
285
+ ...(modeId ? { modeId } : {}),
286
+ });
287
+ } else {
288
+ const command = toCommandString(entry.command, path);
289
+ provider = defineDerivedProvider({
290
+ id,
291
+ extends: entry.extends,
292
+ ...(label ? { label } : {}),
293
+ ...(description ? { description } : {}),
294
+ ...(command ? { command } : {}),
295
+ ...(env ? { env } : {}),
296
+ ...(models ? { models } : {}),
297
+ ...(modeId ? { modeId } : {}),
298
+ });
299
+ }
300
+
301
+ built.push(provider);
302
+ }
303
+
304
+ if (register) {
305
+ // Register only after every entry validates, so a bad entry doesn't leave a
306
+ // half-applied config registered.
307
+ for (const provider of built) registerProvider(provider);
308
+ }
309
+
310
+ return built;
311
+ }