@agents-ensemble/core 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/conductor/conductor-pi-auth.d.ts +5 -5
  2. package/dist/conductor/conductor-pi-auth.d.ts.map +1 -1
  3. package/dist/conductor/conductor-pi-auth.js +261 -80
  4. package/dist/conductor/conductor-pi-auth.js.map +1 -1
  5. package/dist/conductor/conductor-session.d.ts.map +1 -1
  6. package/dist/conductor/conductor-session.js +8 -3
  7. package/dist/conductor/conductor-session.js.map +1 -1
  8. package/dist/conductor/conductor-tool-pi-adapter.d.ts +6 -8
  9. package/dist/conductor/conductor-tool-pi-adapter.d.ts.map +1 -1
  10. package/dist/conductor/conductor-tool-pi-adapter.js +5 -8
  11. package/dist/conductor/conductor-tool-pi-adapter.js.map +1 -1
  12. package/dist/conductor/index.d.ts +1 -4
  13. package/dist/conductor/index.d.ts.map +1 -1
  14. package/dist/conductor/index.js +1 -2
  15. package/dist/conductor/index.js.map +1 -1
  16. package/dist/conductor/pi-conductor-agent.d.ts +22 -6
  17. package/dist/conductor/pi-conductor-agent.d.ts.map +1 -1
  18. package/dist/conductor/pi-conductor-agent.js +296 -101
  19. package/dist/conductor/pi-conductor-agent.js.map +1 -1
  20. package/dist/conductor/pi-resource-loader.d.ts +5 -5
  21. package/dist/conductor/pi-resource-loader.js +5 -5
  22. package/dist/mcp/load-mcp-config.d.ts +8 -0
  23. package/dist/mcp/load-mcp-config.d.ts.map +1 -1
  24. package/dist/mcp/load-mcp-config.js +24 -0
  25. package/dist/mcp/load-mcp-config.js.map +1 -1
  26. package/dist/session/session-sidecar.d.ts +6 -0
  27. package/dist/session/session-sidecar.d.ts.map +1 -1
  28. package/dist/session/session-sidecar.js +17 -0
  29. package/dist/session/session-sidecar.js.map +1 -1
  30. package/package.json +4 -5
  31. package/dist/conductor/pi-conductor-session.d.ts +0 -20
  32. package/dist/conductor/pi-conductor-session.d.ts.map +0 -1
  33. package/dist/conductor/pi-conductor-session.js +0 -60
  34. package/dist/conductor/pi-conductor-session.js.map +0 -1
  35. package/dist/conductor/pi-mcp-bridge.d.ts +0 -65
  36. package/dist/conductor/pi-mcp-bridge.d.ts.map +0 -1
  37. package/dist/conductor/pi-mcp-bridge.js +0 -259
  38. package/dist/conductor/pi-mcp-bridge.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { AuthStorage, ModelRegistry } from '@earendil-works/pi-coding-agent';
1
+ import { ModelRegistry, ModelRuntime } from '@earendil-works/pi-coding-agent';
2
2
  import { type OAuthLoginCallbacks } from '@earendil-works/pi-ai/compat';
3
3
  import { type PiResourceLoaderOptions, type PiResources } from './pi-resource-loader.js';
4
4
  /** Auth and model services for the Pi conductor's user resource layer. */
@@ -6,7 +6,7 @@ export interface PiConductorAuthContext {
6
6
  agentDir: string;
7
7
  authPath: string;
8
8
  modelsPath: string;
9
- authStorage: AuthStorage;
9
+ modelRuntime: ModelRuntime;
10
10
  modelRegistry: ModelRegistry;
11
11
  }
12
12
  export interface PiProviderAuthStatus {
@@ -41,8 +41,8 @@ export interface PiConductorAuthOptions extends PiResourceLoaderOptions {
41
41
  provider?: string;
42
42
  modelId?: string;
43
43
  }
44
- /** Create the Pi credential store and model registry for the user root. */
45
- export declare function createPiConductorAuthContext(options: PiResourceLoaderOptions): PiConductorAuthContext;
44
+ /** Create the Pi 1.x model runtime and registry for the resolved resource roots. */
45
+ export declare function createPiConductorAuthContext(options: PiResourceLoaderOptions, resources?: PiResources): Promise<PiConductorAuthContext>;
46
46
  /** Resolve the provider used by `ensemble auth` from an explicit/default model. */
47
47
  export declare function resolvePiConductorProvider(options: PiConductorAuthOptions): Promise<string>;
48
48
  /** Login to a Pi provider. API-key login persists only in the user auth file. */
@@ -60,7 +60,7 @@ export declare function getPiConductorAuthStatus(options: PiConductorAuthOptions
60
60
  export declare function listPiConductorModels(options?: PiConductorAuthOptions): Promise<PiConductorModelListEntry[]>;
61
61
  /** Resolve a key while retaining project-over-user auth precedence. */
62
62
  export declare function resolvePiConductorApiKey(options: {
63
- authStorage: AuthStorage;
63
+ modelRuntime: ModelRuntime;
64
64
  resources: Pick<PiResources, 'roots' | 'authLayers' | 'settings' | 'models'>;
65
65
  provider: string;
66
66
  explicitApiKey?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"conductor-pi-auth.d.ts","sourceRoot":"","sources":["../../src/conductor/conductor-pi-auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"conductor-pi-auth.d.ts","sourceRoot":"","sources":["../../src/conductor/conductor-pi-auth.ts"],"names":[],"mappings":"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"}
@@ -1,41 +1,41 @@
1
1
  import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { AuthStorage, ModelRegistry, } from '@earendil-works/pi-coding-agent';
4
- import { findEnvKeys, getEnvApiKey, } from '@earendil-works/pi-ai/compat';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { ModelRegistry, ModelRuntime, } from '@earendil-works/pi-coding-agent';
5
+ import { findEnvKeys, getEnvApiKey, getModels, getProviders, } from '@earendil-works/pi-ai/compat';
5
6
  import { loadPiResources, resolvePiResourceRoots, } from './pi-resource-loader.js';
6
- /** Create the Pi credential store and model registry for the user root. */
7
- export function createPiConductorAuthContext(options) {
7
+ /** Create the Pi 1.x model runtime and registry for the resolved resource roots. */
8
+ export async function createPiConductorAuthContext(options, resources) {
8
9
  const roots = resolvePiResourceRoots(options);
9
10
  const authPath = join(roots.agentDir, 'auth.json');
10
11
  const modelsPath = join(roots.agentDir, 'models.json');
11
- const authStorage = AuthStorage.create(authPath);
12
+ const resolvedResources = resources ?? (await loadPiResources(options));
13
+ const modelRuntime = await createPiModelRuntime(options, resolvedResources);
12
14
  return {
13
15
  agentDir: roots.agentDir,
14
16
  authPath,
15
17
  modelsPath,
16
- authStorage,
17
- modelRegistry: ModelRegistry.create(authStorage, modelsPath),
18
+ modelRuntime,
19
+ modelRegistry: new ModelRegistry(modelRuntime),
18
20
  };
19
21
  }
20
22
  /** Resolve the provider used by `ensemble auth` from an explicit/default model. */
21
23
  export async function resolvePiConductorProvider(options) {
22
24
  const resources = await loadPiResources(options);
23
- const context = createPiConductorAuthContext(options);
25
+ const context = await createPiConductorAuthContext(options, resources);
24
26
  return resolvePiConductorProviderFromResources(options, resources, context);
25
27
  }
26
28
  /** Login to a Pi provider. API-key login persists only in the user auth file. */
27
29
  export async function loginPiConductor(options) {
28
30
  const resources = await loadPiResources(options);
29
- const context = createPiConductorAuthContext(options);
30
- const provider = resolveConductorProviderOrThrow(options, resources, context);
31
- const oauthProvider = context.authStorage
32
- .getOAuthProviders()
33
- .some((candidate) => candidate.id === provider);
31
+ const context = await createPiConductorAuthContext(options, resources);
32
+ const provider = await resolveConductorProviderOrThrow(options, resources, context);
33
+ const oauthProvider = Boolean(context.modelRuntime.getProvider(provider)?.auth.oauth);
34
34
  if (oauthProvider) {
35
35
  if (!options.oauthCallbacks) {
36
36
  throw new Error(`Pi provider "${provider}" uses OAuth. Run this command from a TTY so the OAuth login flow can interact with you.`);
37
37
  }
38
- await context.authStorage.login(provider, options.oauthCallbacks);
38
+ await context.modelRuntime.login(provider, 'oauth', toPiAuthInteraction(options.oauthCallbacks));
39
39
  return {
40
40
  backend: 'pi',
41
41
  provider,
@@ -47,7 +47,8 @@ export async function loginPiConductor(options) {
47
47
  if (!apiKey) {
48
48
  throw new Error(`Pi provider "${provider}" requires an API key. Pass a secret API-key prompt value or set ${formatPiProviderEnvHint(provider)}.`);
49
49
  }
50
- context.authStorage.set(provider, { type: 'api_key', key: apiKey });
50
+ await writePiApiKey(context.authPath, provider, apiKey);
51
+ await context.modelRuntime.setRuntimeApiKey(provider, apiKey);
51
52
  return {
52
53
  backend: 'pi',
53
54
  provider,
@@ -58,18 +59,16 @@ export async function loginPiConductor(options) {
58
59
  /** Whether the selected Pi provider exposes Pi's native OAuth login flow. */
59
60
  export async function isPiConductorOAuthProvider(options) {
60
61
  const resources = await loadPiResources(options);
61
- const context = createPiConductorAuthContext(options);
62
- const provider = resolveConductorProviderOrThrow(options, resources, context);
63
- return context.authStorage
64
- .getOAuthProviders()
65
- .some((candidate) => candidate.id === provider);
62
+ const context = await createPiConductorAuthContext(options, resources);
63
+ const provider = await resolveConductorProviderOrThrow(options, resources, context);
64
+ return Boolean(context.modelRuntime.getProvider(provider)?.auth.oauth);
66
65
  }
67
66
  /** Remove a Pi provider's user-layer credential. */
68
67
  export async function logoutPiConductor(options) {
69
68
  const resources = await loadPiResources(options);
70
- const context = createPiConductorAuthContext(options);
71
- const provider = resolveConductorProviderOrThrow(options, resources, context);
72
- context.authStorage.logout(provider);
69
+ const context = await createPiConductorAuthContext(options, resources);
70
+ const provider = await resolveConductorProviderOrThrow(options, resources, context);
71
+ await context.modelRuntime.logout(provider);
73
72
  return {
74
73
  backend: 'pi',
75
74
  provider,
@@ -79,8 +78,8 @@ export async function logoutPiConductor(options) {
79
78
  /** Return non-secret Pi auth metadata for the selected/default provider. */
80
79
  export async function getPiConductorAuthStatus(options) {
81
80
  const resources = await loadPiResources(options);
82
- const context = createPiConductorAuthContext(options);
83
- const providers = resolveStatusProviders(options, resources, context);
81
+ const context = await createPiConductorAuthContext(options, resources);
82
+ const providers = await resolveStatusProviders(options, resources, context);
84
83
  return {
85
84
  backend: 'pi',
86
85
  authPath: context.authPath,
@@ -90,12 +89,11 @@ export async function getPiConductorAuthStatus(options) {
90
89
  /** List only Pi models whose provider has usable configured auth. */
91
90
  export async function listPiConductorModels(options = { cwd: process.cwd() }) {
92
91
  const resources = await loadPiResources(options);
93
- const context = createPiConductorAuthContext(options);
92
+ const context = await createPiConductorAuthContext(options, resources);
94
93
  const requestedProvider = normalizeProvider(options.provider) ?? providerFromModelId(options.modelId);
95
94
  const entries = new Map();
96
- // ModelRegistry's fast `getAvailable()` only sees its user AuthStorage. The
97
- // conductor also honors project auth.json, so filter the complete registry
98
- // through the merged project-over-user auth view before listing models.
95
+ // Filter the complete Pi 1.x runtime catalog through the merged
96
+ // project-over-user auth view before listing models.
99
97
  for (const model of context.modelRegistry.getAll()) {
100
98
  if (requestedProvider && model.provider !== requestedProvider)
101
99
  continue;
@@ -104,18 +102,8 @@ export async function listPiConductorModels(options = { cwd: process.cwd() }) {
104
102
  }
105
103
  addPiModelEntry(entries, model);
106
104
  }
107
- // ModelRegistry reads the user models.json. Project models remain governed by
108
- // the existing resource loader, so add those merged definitions explicitly.
109
- for (const [provider, config] of Object.entries(resources.models.providers)) {
110
- if (requestedProvider && provider !== requestedProvider)
111
- continue;
112
- if (!hasPiProviderAuth(provider, context, resources, options.env ?? process.env)) {
113
- continue;
114
- }
115
- for (const model of config.models ?? []) {
116
- addPiCustomModelEntry(entries, provider, model);
117
- }
118
- }
105
+ // The runtime is built from the merged models resource, so custom models are
106
+ // already included in the registry above.
119
107
  const result = [...entries.values()].sort((left, right) => left.id.localeCompare(right.id));
120
108
  if (result.length === 0) {
121
109
  const providerHint = requestedProvider ? ` for provider "${requestedProvider}"` : '';
@@ -131,27 +119,56 @@ export async function resolvePiConductorApiKey(options) {
131
119
  if (options.explicitApiKey !== undefined)
132
120
  return options.explicitApiKey;
133
121
  // A project auth file intentionally remains above the user auth layer. When
134
- // both roots point at the same file, AuthStorage is the canonical reader so
122
+ // both roots point at the same file, ModelRuntime is the canonical reader so
135
123
  // OAuth refresh is not bypassed by the resource-layer compatibility parser.
124
+ const projectEntry = options.resources.authLayers.at(-1)?.[options.provider];
125
+ const projectKey = options.resources.roots.agentDir !== options.resources.roots.projectDir
126
+ ? resolvePiAuthEntry(projectEntry, env)
127
+ : undefined;
128
+ const settingsKey = resolvePiSettingsApiKey(options.resources.settings, options.provider, env);
129
+ const modelsKey = resolvePiModelsApiKey(options.resources.models.providers[options.provider], env);
136
130
  if (options.resources.roots.agentDir !== options.resources.roots.projectDir) {
137
- const projectEntry = options.resources.authLayers.at(-1)?.[options.provider];
138
131
  if (isPiOAuthAuthEntry(projectEntry)) {
139
132
  throw new Error(formatProjectOAuthAuthError(options.provider));
140
133
  }
141
- const projectKey = resolvePiAuthEntry(projectEntry, env);
142
- if (projectKey !== undefined)
134
+ if (projectKey !== undefined && !isPiCommandConfigValue(projectKey)) {
143
135
  return projectKey;
136
+ }
137
+ if (projectKey !== undefined) {
138
+ // A project auth entry intentionally outranks the user credential. Use
139
+ // the Pi resolver through an isolated provider id so ModelRuntime does
140
+ // not select the lower-precedence user auth.json entry first.
141
+ return resolvePiProjectCommandApiKey(options.modelRuntime, options.provider, projectKey);
142
+ }
144
143
  }
145
- const userStatus = options.authStorage.getAuthStatus(options.provider);
146
- if (userStatus.source === 'stored' || userStatus.source === 'runtime') {
147
- // AuthStorage owns OAuth refresh and locking. Keep this call dynamic per
148
- // request rather than capturing its result during agent construction.
149
- return options.authStorage.getApiKey(options.provider);
144
+ // Pi resolves command references from the provider configuration at request
145
+ // time. Settings/auth are loaded by ensemble rather than Pi's resource
146
+ // loader, so register command references through the same public runtime API
147
+ // before asking the runtime for the request credential. Project auth keeps
148
+ // its higher precedence over settings and models.
149
+ if (settingsKey !== undefined && isPiCommandConfigValue(settingsKey)) {
150
+ options.modelRuntime.registerProvider(options.provider, { apiKey: settingsKey });
151
+ }
152
+ // ModelRuntime owns the Pi 1.x credential store, OAuth refresh, and locking.
153
+ // Resolve it on every request instead of capturing the key at construction.
154
+ let runtimeError;
155
+ try {
156
+ const runtimeAuth = await options.modelRuntime.getAuth(options.provider);
157
+ if (runtimeAuth?.auth.apiKey !== undefined)
158
+ return runtimeAuth.auth.apiKey;
159
+ }
160
+ catch (error) {
161
+ runtimeError = error;
162
+ // Non-command references can still use the ensemble fallback chain. A
163
+ // command reference is reported below instead of returning the raw command
164
+ // as an API key.
165
+ }
166
+ const commandKey = [settingsKey, modelsKey].find((value) => value !== undefined && isPiCommandConfigValue(value));
167
+ if (commandKey !== undefined) {
168
+ throw formatPiCommandResolutionError(options.provider, commandKey, runtimeError);
150
169
  }
151
- const settingsKey = resolvePiSettingsApiKey(options.resources.settings, options.provider, env);
152
170
  if (settingsKey !== undefined)
153
171
  return settingsKey;
154
- const modelsKey = resolvePiModelsApiKey(options.resources.models.providers[options.provider], env);
155
172
  if (modelsKey !== undefined)
156
173
  return modelsKey;
157
174
  return getEnvApiKey(options.provider, toProviderEnv(env));
@@ -163,18 +180,18 @@ export function hasPiProviderAuth(provider, context, resources, env = process.en
163
180
  }
164
181
  /** Synchronous readiness check used by CLI/e2e guards before async loading. */
165
182
  export function hasPiConductorAuth(options) {
166
- const context = createPiConductorAuthContext(options);
167
183
  const env = options.env ?? process.env;
168
184
  const roots = resolvePiResourceRoots(options);
185
+ const userAuth = readPiJsonSync(join(roots.agentDir, 'auth.json'));
169
186
  const projectAuth = readPiJsonSync(join(roots.projectDir, 'auth.json'));
170
187
  const settings = readPiSettingsSync(roots);
171
188
  const models = readPiModelsSync(roots);
172
- const selection = resolvePiConductorSelectionSync(options, settings, models, context);
189
+ const selection = resolvePiConductorSelectionSync(options, settings, models, userAuth);
173
190
  if (selection.provider) {
174
191
  return hasPiProviderAuthSync({
175
192
  provider: selection.provider,
176
- context,
177
193
  roots,
194
+ userAuth,
178
195
  projectAuth,
179
196
  settings,
180
197
  models,
@@ -187,8 +204,8 @@ export function hasPiConductorAuth(options) {
187
204
  if (selection.modelId)
188
205
  return false;
189
206
  return hasAnyPiConductorAuthSync({
190
- context,
191
207
  roots,
208
+ userAuth,
192
209
  projectAuth,
193
210
  settings,
194
211
  models,
@@ -199,7 +216,7 @@ export function hasPiConductorAuth(options) {
199
216
  * Mirror PiConductorAgent's synchronous provider/model selection for guards.
200
217
  * The async agent receives the same merged settings and model layers.
201
218
  */
202
- function resolvePiConductorSelectionSync(options, settings, models, context) {
219
+ function resolvePiConductorSelectionSync(options, settings, models, userAuth) {
203
220
  const requestedModel = normalizeModelId(options.modelId);
204
221
  const configuredModel = normalizeModelId(firstString(settings.defaultModel, settings.model, settings.modelId));
205
222
  const selectedModel = requestedModel ?? configuredModel;
@@ -216,7 +233,7 @@ function resolvePiConductorSelectionSync(options, settings, models, context) {
216
233
  provider = normalizeProvider(settings.defaultProvider);
217
234
  if (!provider && modelId) {
218
235
  const matches = new Set();
219
- for (const model of context.modelRegistry.getAll()) {
236
+ for (const model of knownPiModelsSync(models, userAuth)) {
220
237
  if (model.id === modelId)
221
238
  matches.add(model.provider);
222
239
  }
@@ -241,14 +258,7 @@ function hasPiProviderAuthSync(options) {
241
258
  }
242
259
  if (resolvePiAuthEntry(projectEntry, options.env) !== undefined)
243
260
  return true;
244
- if (options.context.authStorage.has(options.provider))
245
- return true;
246
- // Keep readiness aligned with ModelRegistry, including command-backed or
247
- // otherwise registry-native user models, while the explicit checks below
248
- // honor the injected test/runtime environment.
249
- if (options.context.modelRegistry
250
- .getAvailable()
251
- .some((model) => model.provider === options.provider)) {
261
+ if (resolvePiAuthEntry(options.userAuth?.[options.provider], options.env) !== undefined) {
252
262
  return true;
253
263
  }
254
264
  if (resolvePiSettingsApiKey(options.settings, options.provider, options.env) !== undefined) {
@@ -260,14 +270,12 @@ function hasPiProviderAuthSync(options) {
260
270
  return getEnvApiKey(options.provider, toProviderEnv(options.env)) !== undefined;
261
271
  }
262
272
  function hasAnyPiConductorAuthSync(options) {
263
- if (options.context.modelRegistry.getAvailable().length > 0)
264
- return true;
265
273
  const providers = new Set([
266
- ...options.context.authStorage.list(),
267
- ...options.context.modelRegistry.getAll().map((model) => model.provider),
274
+ ...Object.keys(options.userAuth ?? {}),
275
+ ...knownPiModelsSync(options.models, options.userAuth).map((model) => model.provider),
268
276
  ...Object.keys(options.models.providers),
269
277
  ...Object.keys(options.projectAuth ?? {}),
270
- ...options.context.authStorage.getOAuthProviders().map((provider) => provider.id),
278
+ ...getProviders(),
271
279
  ]);
272
280
  for (const provider of providers) {
273
281
  if (hasPiProviderAuthSync({ ...options, provider }))
@@ -291,7 +299,7 @@ function readPiModelsSync(roots) {
291
299
  : {},
292
300
  };
293
301
  }
294
- function resolveConductorProviderOrThrow(options, resources, context) {
302
+ async function resolveConductorProviderOrThrow(options, resources, context) {
295
303
  const provider = resolvePiConductorProviderFromResources(options, resources, context);
296
304
  if (provider)
297
305
  return provider;
@@ -326,7 +334,7 @@ function resolvePiConductorProviderFromResources(options, resources, context) {
326
334
  }
327
335
  return '';
328
336
  }
329
- function resolveStatusProviders(options, resources, context) {
337
+ async function resolveStatusProviders(options, resources, context) {
330
338
  const explicit = normalizeProvider(options.provider);
331
339
  if (explicit)
332
340
  return [explicit];
@@ -334,10 +342,9 @@ function resolveStatusProviders(options, resources, context) {
334
342
  if (configured)
335
343
  return [configured];
336
344
  const providers = new Set([
337
- ...context.authStorage.list(),
345
+ ...(await context.modelRuntime.listCredentials()).map((entry) => entry.providerId),
338
346
  ...context.modelRegistry.getAll().map((model) => model.provider),
339
347
  ...Object.keys(resources.models.providers),
340
- ...context.authStorage.getOAuthProviders().map((provider) => provider.id),
341
348
  ]);
342
349
  return [...providers].sort();
343
350
  }
@@ -356,7 +363,7 @@ function getPiProviderAuthStatus(provider, context, resources, env) {
356
363
  return { provider, configured: true, source: 'project_stored' };
357
364
  }
358
365
  }
359
- const registryStatus = context.modelRegistry.getProviderAuthStatus(provider);
366
+ const registryStatus = context.modelRuntime.getProviderAuthStatus(provider);
360
367
  if (registryStatus.configured || registryStatus.source === 'stored') {
361
368
  return normalizePiAuthStatus(provider, registryStatus, true);
362
369
  }
@@ -423,7 +430,7 @@ function isPiOAuthAuthEntry(entry) {
423
430
  function formatProjectOAuthAuthError(provider) {
424
431
  return (`Project Pi OAuth credentials for provider "${provider}" are not supported in ` +
425
432
  'project .ensemble/pi/auth.json because that layer cannot refresh them safely. ' +
426
- `Run ensemble auth login --provider ${provider} to store the credential in the user AuthStorage.`);
433
+ `Run ensemble auth login --provider ${provider} to store the credential in the user Pi runtime.`);
427
434
  }
428
435
  function resolvePiSettingsApiKey(settings, provider, env) {
429
436
  if (typeof settings.apiKey === 'string') {
@@ -440,14 +447,188 @@ function resolvePiModelsApiKey(providerConfig, env) {
440
447
  : undefined;
441
448
  }
442
449
  function expandEnvReference(value, env) {
443
- if (value.startsWith('!'))
444
- return undefined;
450
+ // Keep Pi command references opaque here. ModelRuntime's public provider
451
+ // registration path resolves `!command` with Pi's resolver at request time.
445
452
  const variable = /^\$\{([^}]+)\}$/.exec(value) ?? /^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(value);
446
453
  return variable ? env[variable[1]] : value;
447
454
  }
455
+ function isPiCommandConfigValue(value) {
456
+ return value.startsWith('!');
457
+ }
458
+ function formatPiCommandResolutionError(provider, command, cause) {
459
+ const causeMessage = cause instanceof Error ? ` (${cause.message})` : '';
460
+ return new Error(`Failed to resolve API key for Pi provider "${provider}" from shell command: ${command.slice(1)}${causeMessage}`, cause instanceof Error ? { cause } : undefined);
461
+ }
462
+ async function resolvePiProjectCommandApiKey(modelRuntime, provider, command) {
463
+ const isolatedProvider = `__agents_ensemble_project_auth__${provider}`;
464
+ modelRuntime.registerProvider(isolatedProvider, { apiKey: command });
465
+ try {
466
+ const runtimeAuth = await modelRuntime.getAuth(isolatedProvider);
467
+ if (runtimeAuth?.auth.apiKey !== undefined) {
468
+ // Keep the resolved project credential as an in-memory runtime override
469
+ // so the actual conductor request also uses project-over-user auth.
470
+ await modelRuntime.setRuntimeApiKey(provider, runtimeAuth.auth.apiKey);
471
+ return runtimeAuth.auth.apiKey;
472
+ }
473
+ }
474
+ catch (error) {
475
+ throw formatPiCommandResolutionError(provider, command, error);
476
+ }
477
+ throw formatPiCommandResolutionError(provider, command, undefined);
478
+ }
448
479
  function toProviderEnv(env) {
449
480
  return Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === 'string'));
450
481
  }
482
+ /** Build the Pi 1.x runtime from the already-resolved ensemble resource view. */
483
+ async function createPiModelRuntime(options, resources) {
484
+ const roots = resolvePiResourceRoots(options);
485
+ const runtime = await ModelRuntime.create({
486
+ authPath: join(roots.agentDir, 'auth.json'),
487
+ // The ensemble resource loader has already merged user and project models.
488
+ // Avoid letting ModelRuntime read an unmerged second view of models.json.
489
+ modelsPath: null,
490
+ allowModelNetwork: false,
491
+ refreshOnCreate: false,
492
+ });
493
+ const env = options.env ?? process.env;
494
+ for (const [provider, config] of Object.entries(resources.models.providers)) {
495
+ runtime.registerProvider(provider, toPiRuntimeProviderConfig(config, env));
496
+ }
497
+ if (roots.agentDir !== roots.projectDir) {
498
+ for (const [provider, entry] of Object.entries(resources.authLayers.at(-1) ?? {})) {
499
+ if (isPiOAuthAuthEntry(entry))
500
+ continue;
501
+ const key = resolvePiAuthEntry(entry, env);
502
+ if (key !== undefined && !isPiCommandConfigValue(key)) {
503
+ await runtime.setRuntimeApiKey(provider, key);
504
+ }
505
+ }
506
+ }
507
+ if (isRecord(resources.settings.apiKeys)) {
508
+ for (const [provider, value] of Object.entries(resources.settings.apiKeys)) {
509
+ if (typeof value !== 'string')
510
+ continue;
511
+ const key = expandEnvReference(value, env);
512
+ if (key !== undefined && !isPiCommandConfigValue(key)) {
513
+ await runtime.setRuntimeApiKey(provider, key);
514
+ }
515
+ }
516
+ }
517
+ await runtime.refresh({ allowNetwork: false });
518
+ return runtime;
519
+ }
520
+ function toPiRuntimeProviderConfig(config, env) {
521
+ const models = config.models?.map((model) => ({
522
+ type: 'chat',
523
+ id: model.id,
524
+ name: model.name ?? model.id,
525
+ ...(model.api ?? config.api ? { api: model.api ?? config.api } : {}),
526
+ ...(model.baseUrl ?? config.baseUrl
527
+ ? { baseUrl: model.baseUrl ?? config.baseUrl }
528
+ : {}),
529
+ input: model.input ?? ['text'],
530
+ reasoning: model.reasoning ?? false,
531
+ ...(model.thinkingLevelMap ? { thinkingLevelMap: model.thinkingLevelMap } : {}),
532
+ cost: model.cost ?? emptyPiModelCost(),
533
+ contextWindow: model.contextWindow ?? 128000,
534
+ maxTokens: model.maxTokens ?? 16384,
535
+ ...(model.headers ? { headers: model.headers } : {}),
536
+ ...(model.compat ? { compat: model.compat } : {}),
537
+ }));
538
+ return {
539
+ ...(config.name ? { name: config.name } : {}),
540
+ ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
541
+ ...(config.apiKey
542
+ ? { apiKey: expandEnvReference(config.apiKey, env) ?? undefined }
543
+ : {}),
544
+ ...(config.api ? { api: config.api } : {}),
545
+ ...(config.headers ? { headers: config.headers } : {}),
546
+ ...(config.authHeader !== undefined ? { authHeader: config.authHeader } : {}),
547
+ ...(models ? { models } : {}),
548
+ };
549
+ }
550
+ async function writePiApiKey(authPath, provider, apiKey) {
551
+ await mkdir(dirname(authPath), { recursive: true });
552
+ let auth = {};
553
+ try {
554
+ const parsed = JSON.parse(await readFile(authPath, 'utf8'));
555
+ if (isRecord(parsed))
556
+ auth = parsed;
557
+ }
558
+ catch (error) {
559
+ if (!isRecord(error) || error.code !== 'ENOENT')
560
+ throw error;
561
+ }
562
+ auth[provider] = { type: 'api_key', key: apiKey };
563
+ await writeFile(authPath, `${JSON.stringify(auth, null, 2)}\n`, 'utf8');
564
+ }
565
+ function toPiAuthInteraction(callbacks) {
566
+ return {
567
+ signal: callbacks.signal,
568
+ prompt: async (prompt) => {
569
+ if (prompt.type === 'select') {
570
+ const selected = await callbacks.onSelect({
571
+ message: prompt.message,
572
+ options: prompt.options.map((option) => ({
573
+ id: option.id,
574
+ label: option.label,
575
+ })),
576
+ });
577
+ if (selected === undefined)
578
+ throw new Error('OAuth login was cancelled.');
579
+ return selected;
580
+ }
581
+ if (prompt.type === 'manual_code' && callbacks.onManualCodeInput) {
582
+ return callbacks.onManualCodeInput();
583
+ }
584
+ return callbacks.onPrompt({
585
+ message: prompt.message,
586
+ ...(prompt.placeholder ? { placeholder: prompt.placeholder } : {}),
587
+ });
588
+ },
589
+ notify: (event) => {
590
+ if (event.type === 'auth_url') {
591
+ callbacks.onAuth({ url: event.url, instructions: event.instructions });
592
+ }
593
+ else if (event.type === 'device_code') {
594
+ callbacks.onDeviceCode({
595
+ userCode: event.userCode,
596
+ verificationUri: event.verificationUri,
597
+ ...(event.intervalSeconds !== undefined
598
+ ? { intervalSeconds: event.intervalSeconds }
599
+ : {}),
600
+ ...(event.expiresInSeconds !== undefined
601
+ ? { expiresInSeconds: event.expiresInSeconds }
602
+ : {}),
603
+ });
604
+ }
605
+ else if (callbacks.onProgress) {
606
+ callbacks.onProgress(event.message);
607
+ }
608
+ },
609
+ };
610
+ }
611
+ function knownPiModelsSync(models, _userAuth) {
612
+ const result = [];
613
+ for (const provider of getProviders()) {
614
+ try {
615
+ for (const model of getModels(provider) ?? [])
616
+ result.push(model);
617
+ }
618
+ catch {
619
+ // A static provider catalog failure must not make the sync auth guard fail.
620
+ }
621
+ }
622
+ for (const [provider, config] of Object.entries(models.providers)) {
623
+ for (const model of config.models ?? []) {
624
+ result.push({ provider, id: model.id, name: model.name });
625
+ }
626
+ }
627
+ return result;
628
+ }
629
+ function emptyPiModelCost() {
630
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
631
+ }
451
632
  function normalizeProvider(value) {
452
633
  if (typeof value !== 'string')
453
634
  return undefined;