@north-light/crouter 0.3.316 → 0.3.317
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.
|
@@ -298,14 +298,15 @@ interface FakeServices {
|
|
|
298
298
|
resourceLoader: ResourceLoader;
|
|
299
299
|
modelRuntime: FakeModelRuntime;
|
|
300
300
|
}
|
|
301
|
+
type FakeResourceLoaderOptions = {
|
|
302
|
+
additionalExtensionPaths?: string[];
|
|
303
|
+
appendSystemPrompt?: string[];
|
|
304
|
+
};
|
|
301
305
|
export declare function createAgentSessionServices(options: {
|
|
302
306
|
cwd: string;
|
|
303
307
|
agentDir?: string;
|
|
304
308
|
modelRuntime?: unknown;
|
|
305
|
-
resourceLoaderOptions?:
|
|
306
|
-
additionalExtensionPaths?: string[];
|
|
307
|
-
appendSystemPrompt?: string[];
|
|
308
|
-
};
|
|
309
|
+
resourceLoaderOptions?: FakeResourceLoaderOptions;
|
|
309
310
|
}): Promise<FakeServices>;
|
|
310
311
|
export declare function createAgentSessionFromServices(options: {
|
|
311
312
|
services: FakeServices;
|
|
@@ -31,11 +31,12 @@
|
|
|
31
31
|
// <nodeDir>/fake-pi.dialog.jsonl — one line per resolved unattended dialog (§5.4)
|
|
32
32
|
import { spawn } from 'node:child_process';
|
|
33
33
|
import { writeFileSync, readFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync, } from 'node:fs';
|
|
34
|
-
import { join, dirname } from 'node:path';
|
|
34
|
+
import { join, dirname, resolve } from 'node:path';
|
|
35
35
|
import { randomUUID } from 'node:crypto';
|
|
36
|
-
import { DefaultResourceLoader, getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
36
|
+
import { DefaultPackageManager, DefaultResourceLoader, getAgentDir, SettingsManager } from '@earendil-works/pi-coding-agent';
|
|
37
37
|
import { MANAGED_PROVIDER_COOLING_DIAGNOSTIC } from '../../runtime/managed-provider-cooling.js';
|
|
38
38
|
import { PROVIDER_FAILURE_DIAGNOSTIC } from '../../provider-failure.js';
|
|
39
|
+
import { CANVAS_EXTENSIONS } from '../../runtime/canvas-extensions.js';
|
|
39
40
|
import registerCanvasBashValve, { createValveOperations } from '../../../pi-extensions/canvas-bash-valve.js';
|
|
40
41
|
/** Test seams for prompt-scoped failures before session_start. The one-shot
|
|
41
42
|
* variant records its first failure per node so the next cycling retry can
|
|
@@ -678,6 +679,14 @@ class FakeSession {
|
|
|
678
679
|
// A turn consumes whatever was queued for it, as the real session does.
|
|
679
680
|
this.steering.length = 0;
|
|
680
681
|
this.followUps.length = 0;
|
|
682
|
+
let releaseGate;
|
|
683
|
+
if (process.env['PI_FAKE_ENGINE_HOLD_TURN'] === '1') {
|
|
684
|
+
let release;
|
|
685
|
+
let resolveSettled;
|
|
686
|
+
const settled = new Promise((resolve) => { resolveSettled = resolve; });
|
|
687
|
+
releaseGate = new Promise((resolve) => { release = resolve; });
|
|
688
|
+
this.heldTurn = { release, settled, resolveSettled };
|
|
689
|
+
}
|
|
681
690
|
this.streaming = true;
|
|
682
691
|
if (opts.runHooks === true) {
|
|
683
692
|
await this.fire('agent_start', {}, this.buildCtx());
|
|
@@ -685,14 +694,7 @@ class FakeSession {
|
|
|
685
694
|
this.emit({ type: 'agent_start' });
|
|
686
695
|
this.emit({ type: 'turn_start' });
|
|
687
696
|
this.emit({ type: 'message_start', message: small });
|
|
688
|
-
|
|
689
|
-
let release;
|
|
690
|
-
let resolveSettled;
|
|
691
|
-
const settled = new Promise((resolve) => { resolveSettled = resolve; });
|
|
692
|
-
const releaseGate = new Promise((resolve) => { release = resolve; });
|
|
693
|
-
this.heldTurn = { release, settled, resolveSettled };
|
|
694
|
-
await releaseGate;
|
|
695
|
-
}
|
|
697
|
+
await releaseGate;
|
|
696
698
|
// F2 (message_update coalescing): the broker now coalesces message_update
|
|
697
699
|
// relays (latest-wins, ~75 ms window), so a padded flood riding message_update
|
|
698
700
|
// can no longer climb a stalled viewer's backlog — the coalescer would shed
|
|
@@ -1291,14 +1293,93 @@ const FAKE_REGISTERED_MODELS = [
|
|
|
1291
1293
|
{ provider: 'openai-codex', id: 'gpt-5.6-luna' },
|
|
1292
1294
|
FAKE_AVAILABLE_MODEL,
|
|
1293
1295
|
];
|
|
1296
|
+
/** Source harnesses use native imports only for an isolated, mandatory-only
|
|
1297
|
+
* canvas fixture. Every other source stays on the SDK file-loader path. */
|
|
1298
|
+
async function sourceCanvasExtensionLoaderOptions(cwd, agentDir, options) {
|
|
1299
|
+
const extensionPaths = options?.additionalExtensionPaths;
|
|
1300
|
+
const mandatoryOnly = extensionPaths?.length === CANVAS_EXTENSIONS.length
|
|
1301
|
+
&& extensionPaths.every((path, index) => path === CANVAS_EXTENSIONS[index]);
|
|
1302
|
+
if (!import.meta.url.endsWith('.ts') || !mandatoryOnly)
|
|
1303
|
+
return { additionalExtensionPaths: extensionPaths };
|
|
1304
|
+
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
1305
|
+
const resolved = await new DefaultPackageManager({ cwd, agentDir, settingsManager }).resolve();
|
|
1306
|
+
if (resolved.extensions.some((extension) => extension.enabled)) {
|
|
1307
|
+
return { additionalExtensionPaths: extensionPaths, settingsManager };
|
|
1308
|
+
}
|
|
1309
|
+
const inlinePaths = new Map();
|
|
1310
|
+
const fileLoaderErrors = new Map();
|
|
1311
|
+
const missingPaths = [];
|
|
1312
|
+
const extensionFactories = [];
|
|
1313
|
+
for (const inputPath of extensionPaths) {
|
|
1314
|
+
const path = resolve(cwd, inputPath);
|
|
1315
|
+
if (!existsSync(path)) {
|
|
1316
|
+
missingPaths.push(path);
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
inlinePaths.set(`<inline:${path}>`, path);
|
|
1320
|
+
let factory;
|
|
1321
|
+
try {
|
|
1322
|
+
const loaded = await import(path);
|
|
1323
|
+
if (typeof loaded.default !== 'function') {
|
|
1324
|
+
fileLoaderErrors.set(path, `Extension does not export a valid factory function: ${path}`);
|
|
1325
|
+
factory = () => { throw new Error(fileLoaderErrors.get(path)); };
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
factory = loaded.default;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
// Let the original loader produce its own import/transform diagnostic.
|
|
1333
|
+
return { additionalExtensionPaths: extensionPaths, settingsManager };
|
|
1334
|
+
}
|
|
1335
|
+
extensionFactories.push({ name: path, factory });
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
additionalExtensionPaths: [],
|
|
1339
|
+
extensionFactories,
|
|
1340
|
+
settingsManager,
|
|
1341
|
+
restoreCliExtensionMetadata: (loader) => {
|
|
1342
|
+
const result = loader.getExtensions();
|
|
1343
|
+
for (const extension of result.extensions) {
|
|
1344
|
+
const path = inlinePaths.get(extension.path);
|
|
1345
|
+
if (path === undefined)
|
|
1346
|
+
continue;
|
|
1347
|
+
const sourceInfo = { path, source: 'cli', scope: 'temporary', origin: 'top-level', baseDir: undefined };
|
|
1348
|
+
extension.path = path;
|
|
1349
|
+
extension.resolvedPath = path;
|
|
1350
|
+
extension.sourceInfo = sourceInfo;
|
|
1351
|
+
for (const command of extension.commands.values())
|
|
1352
|
+
command.sourceInfo = sourceInfo;
|
|
1353
|
+
for (const tool of extension.tools.values())
|
|
1354
|
+
tool.sourceInfo = sourceInfo;
|
|
1355
|
+
}
|
|
1356
|
+
for (const error of result.errors) {
|
|
1357
|
+
const path = inlinePaths.get(error.path);
|
|
1358
|
+
if (path !== undefined) {
|
|
1359
|
+
error.path = path;
|
|
1360
|
+
const isConflict = /^(?:Tool|Flag) ".*" conflicts with /.test(error.error);
|
|
1361
|
+
error.error = fileLoaderErrors.get(path) ?? (isConflict ? error.error : `Failed to load extension: ${error.error}`);
|
|
1362
|
+
}
|
|
1363
|
+
for (const [inlinePath, filePath] of inlinePaths) {
|
|
1364
|
+
error.error = error.error.replaceAll(inlinePath, filePath);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
result.errors.push(...missingPaths.map((path) => ({ path, error: `Extension path does not exist: ${path}` })));
|
|
1368
|
+
},
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1294
1371
|
export async function createAgentSessionServices(options) {
|
|
1372
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
1373
|
+
const extensionOptions = await sourceCanvasExtensionLoaderOptions(options.cwd, agentDir, options.resourceLoaderOptions);
|
|
1374
|
+
const { restoreCliExtensionMetadata, ...loaderOptions } = extensionOptions;
|
|
1295
1375
|
const loader = new DefaultResourceLoader({
|
|
1296
1376
|
cwd: options.cwd,
|
|
1297
|
-
agentDir
|
|
1298
|
-
|
|
1377
|
+
agentDir,
|
|
1378
|
+
...loaderOptions,
|
|
1299
1379
|
appendSystemPrompt: options.resourceLoaderOptions?.appendSystemPrompt,
|
|
1300
1380
|
});
|
|
1301
1381
|
await loader.reload();
|
|
1382
|
+
restoreCliExtensionMetadata?.(loader);
|
|
1302
1383
|
// Zero-auth test seam. While <CRTR_HOME>/fake-pi.no-auth exists, the registry
|
|
1303
1384
|
// reports a GENUINE zero-auth boot (nothing registered, nothing authenticated)
|
|
1304
1385
|
// so resolveLaunchModel (broker.ts) falls all the way to a tier-3 model-less
|
|
@@ -367,6 +367,8 @@ test('reload_auth re-drives a parked fatal provider-auth turn exactly once', { t
|
|
|
367
367
|
await seedPool('openai-codex', false);
|
|
368
368
|
const child = await h.spawnHeadlessChild(parent, 'auth reload child', { model: OPENAI_STRONG.spec });
|
|
369
369
|
await h.waitFor(() => agentStartCount(child) >= 1, { label: 'initial auth fixture turn started' });
|
|
370
|
+
const kickoffObserver = await attachUntil(child, 'observer', 'auth-reload-kickoff-settled', (a) => a.welcome.snapshot.state.isStreaming === false, 'auth reload kickoff settled');
|
|
371
|
+
kickoffObserver.close();
|
|
370
372
|
const baselineStarts = agentStartCount(child);
|
|
371
373
|
h.fakeCmd(child, {
|
|
372
374
|
cmd: 'stop',
|
|
@@ -397,6 +399,8 @@ test('reload_auth leaves a parked auth fault with its original session when navi
|
|
|
397
399
|
await seedPool('openai-codex', false);
|
|
398
400
|
const child = await h.spawnHeadlessChild(parent, 'auth replacement child', { model: OPENAI_STRONG.spec });
|
|
399
401
|
await h.waitFor(() => agentStartCount(child) >= 1, { label: 'initial replacement fixture turn started' });
|
|
402
|
+
const kickoffObserver = await attachUntil(child, 'observer', 'auth-replacement-kickoff-settled', (a) => a.welcome.snapshot.state.isStreaming === false, 'auth replacement kickoff settled');
|
|
403
|
+
kickoffObserver.close();
|
|
400
404
|
h.fakeCmd(child, {
|
|
401
405
|
cmd: 'stop',
|
|
402
406
|
reason: 'error',
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.317",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.317",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"workspaces": [
|
|
@@ -5206,17 +5206,17 @@
|
|
|
5206
5206
|
},
|
|
5207
5207
|
"packages/crouter-api": {
|
|
5208
5208
|
"name": "@north-light/crouter-api",
|
|
5209
|
-
"version": "0.3.
|
|
5209
|
+
"version": "0.3.317",
|
|
5210
5210
|
"license": "UNLICENSED"
|
|
5211
5211
|
},
|
|
5212
5212
|
"packages/crouter-env-docker": {
|
|
5213
5213
|
"name": "@north-light/crouter-env-docker",
|
|
5214
|
-
"version": "0.3.
|
|
5214
|
+
"version": "0.3.317",
|
|
5215
5215
|
"license": "UNLICENSED"
|
|
5216
5216
|
},
|
|
5217
5217
|
"packages/crouter-sdk": {
|
|
5218
5218
|
"name": "@north-light/crouter-sdk",
|
|
5219
|
-
"version": "0.3.
|
|
5219
|
+
"version": "0.3.317",
|
|
5220
5220
|
"license": "UNLICENSED",
|
|
5221
5221
|
"dependencies": {
|
|
5222
5222
|
"@north-light/crouter-api": "^0.3.295"
|