pi-var 1.0.13 → 1.0.15

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.14
4
+
5
+ - Pin development SDKs to Pi 1.0.0 while retaining host-owned wildcard peers.
6
+ - Exercise actual SDK and bundled host lifecycle, identity, native nested validation/loadouts, reload and shutdown offline.
7
+
3
8
  ## 1.0.13
4
9
 
5
10
  - Validate against Pi 0.99.0, including an offline real-host package-loading probe.
package/README.md CHANGED
@@ -11,6 +11,10 @@ _Work on multiple features simultaneously with isolated workspaces — automatic
11
11
 
12
12
  </div>
13
13
 
14
+ ## Pi 1.0 compatibility (1.0.14)
15
+
16
+ Development SDKs are pinned to **1.0.0**; host-provided dependencies remain wildcard peers. Run `bun run test:pi` for offline real-host registrations, prompt/tool loadouts, nested calls, reload and shutdown. Set `PI1_HOST_PACKAGE` to an installed Pi package directory and `PI1_HOST_ENTRY=bundle` to check its bundled runtime.
17
+
14
18
  ---
15
19
 
16
20
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-var",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Copy-on-write variations for pi — work on multiple features simultaneously with isolated workspaces",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -48,8 +48,8 @@
48
48
  "simple-git-hooks": "2.13.1",
49
49
  "typescript": "6.0.3",
50
50
  "vitest": "4.1.7",
51
- "@earendil-works/pi-coding-agent": "0.99.0",
52
- "@earendil-works/pi-ai": "0.99.0",
51
+ "@earendil-works/pi-coding-agent": "1.1.0",
52
+ "@earendil-works/pi-ai": "1.1.0",
53
53
  "typebox": "1.3.27"
54
54
  },
55
55
  "peerDependencies": {
@@ -58,12 +58,12 @@
58
58
  "typebox": "*"
59
59
  },
60
60
  "overrides": {
61
- "brace-expansion": "5.0.6",
61
+ "brace-expansion": "5.0.12",
62
62
  "fast-xml-builder": "1.2.0",
63
63
  "protobufjs": "8.4.0",
64
64
  "ws": "8.20.1",
65
- "@earendil-works/pi-ai": "0.99.0",
66
- "@earendil-works/pi-coding-agent": "0.99.0",
67
- "@earendil-works/pi-tui": "0.99.0"
65
+ "@earendil-works/pi-ai": "1.1.0",
66
+ "@earendil-works/pi-coding-agent": "1.1.0",
67
+ "@earendil-works/pi-tui": "1.1.0"
68
68
  }
69
69
  }
@@ -1,50 +1,332 @@
1
+ // Offline Pi 1.1 SDK/bundle lifecycle, identity, loadout, nested-tool and renderer probe.
1
2
  import assert from 'node:assert/strict';
2
- import { mkdtemp, readFile, rm } from 'node:fs/promises';
3
+ import { findPackageJSON } from 'node:module';
4
+ import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises';
3
5
  import { tmpdir } from 'node:os';
4
6
  import { dirname, join, resolve } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
8
 
7
9
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
- const home = await mkdtemp(join(tmpdir(), 'pi-extension-compat-'));
9
- const previousHome = process.env.PI_CODING_AGENT_DIR;
10
+ const home = await mkdtemp(join(tmpdir(), 'pi1-workflow-'));
11
+ const previousCwd = process.cwd();
10
12
  const previousFetch = globalThis.fetch;
13
+ const environment = ['PI_CODING_AGENT_DIR', 'PI_SWARM_SPAWNED', 'MORPH_API_KEY'];
14
+ const previousEnv = Object.fromEntries(environment.map((key) => [key, process.env[key]]));
11
15
  process.env.PI_CODING_AGENT_DIR = home;
16
+ process.env.PI_SWARM_SPAWNED = '1'; // Do not start the reason-harness service.
17
+ delete process.env.MORPH_API_KEY;
18
+ process.chdir(home); // Keep lifecycle aliases/session-id files in the disposable fixture.
12
19
  globalThis.fetch = async () => new Response('', { status: 503 });
13
20
  let session;
14
21
  try {
15
- const { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, VERSION } = await import('@earendil-works/pi-coding-agent');
16
- assert.equal(VERSION, '0.99.0', 'test the actual pinned Pi host, not a stale override');
22
+ const host = process.env.PI1_HOST_PACKAGE;
23
+ const hostEntry =
24
+ process.env.PI1_HOST_ENTRY === 'bundle' ? 'dist/bundle/index.js' : 'dist/index.js';
25
+ const sdk = await import(
26
+ host
27
+ ? pathToFileURL(join(host, hostEntry)).href
28
+ : import.meta.resolve('@earendil-works/pi-coding-agent')
29
+ );
30
+ const {
31
+ createAgentSession,
32
+ DefaultResourceLoader,
33
+ ModelRuntime,
34
+ SessionManager,
35
+ SettingsManager,
36
+ VERSION,
37
+ } = sdk;
38
+ assert.equal(VERSION, '1.1.0', 'actual executing Pi host');
39
+ const localSdk = await import('@earendil-works/pi-coding-agent');
40
+ assert.equal(localSdk.VERSION, '1.1.0', 'exact pinned development host');
41
+ const aiPath = findPackageJSON(
42
+ '@earendil-works/pi-ai/compat',
43
+ pathToFileURL(join(sdk.getPackageDir(), 'package.json'))
44
+ );
45
+ const aiManifest = JSON.parse(await readFile(aiPath, 'utf8'));
46
+ assert.equal(aiManifest.version, '1.1.0');
47
+ const ai = await import(
48
+ pathToFileURL(join(dirname(aiPath), aiManifest.exports['./compat'].import)).href
49
+ );
17
50
  const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
18
- for (const name of ['@earendil-works/pi-ai', '@earendil-works/pi-agent-core', '@earendil-works/pi-coding-agent', '@earendil-works/pi-tui', 'typebox']) {
19
- assert.equal(manifest.dependencies?.[name], undefined, `${name}: host packages must not be runtime dependencies`);
20
- if (manifest.peerDependencies?.[name] !== undefined) assert.equal(manifest.peerDependencies[name], '*');
51
+ for (const name of [
52
+ '@earendil-works/pi-ai',
53
+ '@earendil-works/pi-agent-core',
54
+ '@earendil-works/pi-coding-agent',
55
+ '@earendil-works/pi-tui',
56
+ 'typebox',
57
+ ]) {
58
+ assert.equal(manifest.dependencies?.[name], undefined, `${name}: never bundle host packages`);
59
+ if (manifest.peerDependencies?.[name] !== undefined)
60
+ assert.equal(manifest.peerDependencies[name], '*');
61
+ if (manifest.devDependencies?.[name] !== undefined && name !== 'typebox')
62
+ assert.equal(manifest.devDependencies[name], '1.1.0');
21
63
  }
22
- const settingsManager = SettingsManager.inMemory({ packages: [root], compaction: { enabled: false }, retry: { enabled: false } });
23
- const resourceLoader = new DefaultResourceLoader({ cwd: home, agentDir: home, settingsManager,
24
- noSkills: true, noPromptTemplates: true, noThemes: true, noContextFiles: true });
25
- await resourceLoader.reload();
26
- const loaded = resourceLoader.getExtensions();
27
- assert.deepEqual(loaded.errors, []);
28
- assert.deepEqual(loaded.warnings ?? [], []);
29
- assert.ok(loaded.extensions.length > 0, 'manifest entrypoints must load');
30
- const modelRuntime = await ModelRuntime.create({ authPath: join(home, 'auth.json'), modelsPath: null, modelsStorePath: join(home, 'models-cache'), allowModelNetwork: false });
31
- ({ session } = await createAgentSession({ cwd: home, agentDir: home, resourceLoader, modelRuntime, settingsManager, sessionManager: SessionManager.inMemory(home) }));
64
+ const identityPath = join(home, 'identity.ts');
65
+ globalThis[Symbol.for('pi1.workflow.identity')] = sdk.AgentSession;
66
+ await writeFile(
67
+ identityPath,
68
+ `import { AgentSession } from '@earendil-works/pi-coding-agent';
69
+ export default function () {
70
+ if (AgentSession !== globalThis[Symbol.for('pi1.workflow.identity')]) throw new Error('Duplicate host constructor');
71
+ }`
72
+ );
73
+ const errors = [],
74
+ events = [];
75
+ let api;
76
+ const settingsManager = SettingsManager.inMemory({
77
+ packages: [root],
78
+ compaction: { enabled: false },
79
+ retry: { enabled: false },
80
+ });
81
+ const resourceLoader = new DefaultResourceLoader({
82
+ cwd: home,
83
+ agentDir: home,
84
+ settingsManager,
85
+ noSkills: true,
86
+ noPromptTemplates: true,
87
+ noThemes: true,
88
+ noContextFiles: true,
89
+ additionalExtensionPaths: [identityPath],
90
+ extensionFactories: [
91
+ (pi) => {
92
+ api = pi;
93
+ for (const type of ['session_start', 'session_shutdown', 'tool_result'])
94
+ pi.on(type, (event) => {
95
+ events.push(event);
96
+ });
97
+ pi.registerTool({
98
+ name: 'offline_native_probe',
99
+ label: 'Offline probe',
100
+ description: 'Native probe',
101
+ exposure: 'model-only',
102
+ parameters: { type: 'object', properties: {} },
103
+ prepareLoadout: (loadout) => ({
104
+ hiddenDeclarations: loadout.callable.map((tool) => tool.name),
105
+ }),
106
+ async execute(_id, _args, signal, _update, ctx) {
107
+ assert(!ctx.tools.some((tool) => tool.name === 'offline_native_probe'));
108
+ assert(ctx.tools.some((tool) => tool.name === 'read'));
109
+ const result = await ctx.executeTool('read', { path: identityPath }, { signal });
110
+ assert.equal(result.isError, false, JSON.stringify(result));
111
+ assert(
112
+ result.result.content.some(
113
+ (block) =>
114
+ block.type === 'text' && block.text.includes('Duplicate host constructor')
115
+ )
116
+ );
117
+ const invalid = await ctx.executeTool('read', {}, { signal });
118
+ assert.equal(invalid.isError, true, 'native nested argument validation');
119
+ return { content: [{ type: 'text', text: 'offline-native-ok' }], details: undefined };
120
+ },
121
+ });
122
+ },
123
+ ],
124
+ });
125
+ const faux = ai.fauxProvider({
126
+ provider: 'pi1-workflow',
127
+ api: 'pi1-workflow-api',
128
+ models: [{ id: 'offline', name: 'Offline', reasoning: false }],
129
+ tokenSize: { min: 100, max: 100 },
130
+ });
131
+ const modelRuntime = await ModelRuntime.create({
132
+ authPath: join(home, 'auth.json'),
133
+ modelsPath: null,
134
+ refreshOnCreate: false,
135
+ });
136
+ modelRuntime.registerNativeProvider(faux.provider);
137
+ await modelRuntime.refresh({ allowNetwork: false });
138
+ async function loadSession() {
139
+ await resourceLoader.reload();
140
+ const loaded = resourceLoader.getExtensions();
141
+ assert.deepEqual(loaded.errors, []);
142
+ assert.deepEqual(loaded.warnings ?? [], []);
143
+ assert(
144
+ loaded.extensions.some((extension) => extension.resolvedPath.startsWith(root)),
145
+ 'manifest entrypoints load'
146
+ );
147
+ ({ session } = await createAgentSession({
148
+ cwd: home,
149
+ agentDir: home,
150
+ resourceLoader,
151
+ modelRuntime,
152
+ model: modelRuntime.getModel('pi1-workflow', 'offline'),
153
+ settingsManager,
154
+ sessionManager: SessionManager.inMemory(home),
155
+ }));
156
+ session.extensionRunner.onError((error) => errors.push(error));
157
+ await session.bindExtensions({});
158
+ return loaded;
159
+ }
160
+ const loaded = await loadSession();
161
+ const owned = loaded.extensions.filter((extension) => extension.resolvedPath.startsWith(root));
32
162
  const names = new Set();
33
- for (const extension of loaded.extensions) {
163
+ for (const extension of owned) {
164
+ assert(
165
+ extension.handlers.size + extension.commands.size + extension.tools.size > 0,
166
+ 'owned public registrations'
167
+ );
34
168
  for (const [name, { definition }] of extension.tools) {
35
169
  assert.equal(definition.name, name);
36
170
  assert.equal(typeof definition.execute, 'function');
37
171
  assert.equal(typeof definition.parameters, 'object');
38
- assert.ok(!names.has(name), `duplicate tool ${name}`);
172
+ assert(!names.has(name), `duplicate tool ${name}`);
39
173
  names.add(name);
40
- assert.ok(session.getAllTools().some(tool => tool.name === name), `${name}: tool must be installed in the real session`);
174
+ assert(session.getAllTools().some((tool) => tool.name === name));
41
175
  }
42
176
  }
43
- console.log(`${manifest.name}: Pi ${VERSION} warning-free manifest load; ${loaded.extensions.length} extensions, ${names.size} tools registered`);
177
+ const command = {
178
+ 'pi-reason-harness': 'reason',
179
+ 'pi-recurse': 'recurse-status',
180
+ '@monotykamary/pi-supervisor': 'supervise',
181
+ '@monotykamary/pi-tps': 'tps-export',
182
+ 'pi-tps-web': 'tps-web',
183
+ 'pi-var': 'var',
184
+ }[manifest.name];
185
+ if (command)
186
+ assert(session.extensionRunner.getRegisteredCommands().some((item) => item.name === command));
187
+ faux.setResponses([
188
+ (context) => {
189
+ assert(
190
+ ai.getCurrentTools(context.messages).some((tool) => tool.name === 'offline_native_probe')
191
+ );
192
+ assert(!ai.getCurrentTools(context.messages).some((tool) => tool.name === 'read'));
193
+ const prompt = ai.getCurrentSystemPrompt(context.messages);
194
+ if (manifest.name === 'pi-recurse') {
195
+ assert(prompt.includes('Recursive Agent Context'));
196
+ assert(
197
+ context.messages.some(
198
+ (message) =>
199
+ message.role === 'system' &&
200
+ message.sections?.recursion?.includes('Recursive Agent Context')
201
+ ),
202
+ 'recursion remains a structured prompt section'
203
+ );
204
+ }
205
+ if (manifest.name === 'pi-morph-plugin') {
206
+ assert(prompt.includes('Morph remote tools are currently unavailable'));
207
+ assert(
208
+ context.messages.some(
209
+ (message) =>
210
+ message.role === 'system' &&
211
+ message.sections?.morph?.includes('Morph remote tools are currently unavailable')
212
+ ),
213
+ 'Morph routing remains a structured prompt section'
214
+ );
215
+ }
216
+ return ai.fauxAssistantMessage(
217
+ [ai.fauxToolCall('offline_native_probe', {}, { id: 'outer' })],
218
+ { stopReason: 'toolUse' }
219
+ );
220
+ },
221
+ ai.fauxAssistantMessage('offline workflow complete'),
222
+ ]);
223
+ await session.prompt('Verify this workflow locally without remote tools.');
224
+ assert.equal(
225
+ session.getLastAssistantText(),
226
+ 'offline workflow complete',
227
+ JSON.stringify(session.messages.at(-1))
228
+ );
229
+ assert(
230
+ events.some(
231
+ (event) =>
232
+ event.type === 'tool_result' &&
233
+ event.parentToolCallId === 'outer' &&
234
+ event.toolName === 'read'
235
+ )
236
+ );
237
+ assert(
238
+ !session.messages.some(
239
+ (message) => message.role === 'toolResult' && message.toolName === 'read'
240
+ )
241
+ );
242
+ assert(
243
+ session.messages.find(
244
+ (message) => message.role === 'toolResult' && message.toolCallId === 'outer'
245
+ )?.nestedCalls
246
+ );
247
+ if (manifest.name === '@monotykamary/pi-tps') {
248
+ const telemetry = session.sessionManager
249
+ .getBranch()
250
+ .filter((entry) => entry.type === 'custom' && entry.customType === 'tps');
251
+ assert.equal(telemetry.length, 2, 'one persisted telemetry record per real provider turn');
252
+ assert(
253
+ telemetry.every(
254
+ (entry) => entry.data.model.provider === 'pi1-workflow' && entry.data.tokens.output > 0
255
+ )
256
+ );
257
+ }
258
+ let renderers = 0;
259
+ if (manifest.name === 'pi-morph-plugin' || manifest.name === 'pi-recurse') {
260
+ const themeModule = await import(
261
+ pathToFileURL(join(sdk.getPackageDir(), 'dist/modes/interactive/theme/theme.js')).href
262
+ );
263
+ const tuiPackage = findPackageJSON(
264
+ '@earendil-works/pi-tui',
265
+ pathToFileURL(join(sdk.getPackageDir(), 'package.json'))
266
+ );
267
+ const tuiManifest = JSON.parse(await readFile(tuiPackage, 'utf8'));
268
+ const tui = await import(pathToFileURL(join(dirname(tuiPackage), tuiManifest.main)).href);
269
+ const theme = themeModule.getThemeByName('dark');
270
+ assert(theme);
271
+ const args = {
272
+ mode: 'single',
273
+ prompt: 'Offline 漢字 café',
274
+ target_filepath: '测试-é.ts',
275
+ instructions: 'offline',
276
+ search_term: 'offline',
277
+ owner_repo: 'offline/repo',
278
+ };
279
+ for (const extension of owned)
280
+ for (const { definition } of extension.tools.values()) {
281
+ const context = { args, toolCallId: 'offline-render', cwd: home, isError: false };
282
+ const components = [];
283
+ if (definition.renderCall) components.push(definition.renderCall(args, theme, context));
284
+ if (definition.renderResult)
285
+ for (const expanded of [false, true])
286
+ components.push(
287
+ definition.renderResult(
288
+ { content: [], details: undefined },
289
+ { expanded, isPartial: true },
290
+ theme,
291
+ context
292
+ )
293
+ );
294
+ for (const component of components)
295
+ if (component) {
296
+ for (const width of [24, 80])
297
+ assert(component.render(width).every((line) => tui.visibleWidth(line) <= width));
298
+ component.invalidate();
299
+ renderers++;
300
+ }
301
+ }
302
+ }
303
+ await session.extensionRunner.emit({ type: 'session_shutdown', reason: 'reload' });
304
+ assert.deepEqual(errors, []);
305
+ session.dispose();
306
+ assert.throws(() => api.getActiveTools(), /stale|inactive|invalid/i);
307
+ await loadSession();
308
+ assert(session.getActiveToolNames().includes('offline_native_probe'));
309
+ await session.extensionRunner.emit({ type: 'session_shutdown', reason: 'quit' });
310
+ assert.deepEqual(errors, []);
311
+ console.log(
312
+ JSON.stringify({
313
+ name: manifest.name,
314
+ pi: VERSION,
315
+ hostEntry,
316
+ extensions: owned.length,
317
+ tools: names.size,
318
+ nestedRead: true,
319
+ reload: true,
320
+ renderers,
321
+ })
322
+ );
44
323
  } finally {
45
324
  session?.dispose();
46
325
  globalThis.fetch = previousFetch;
47
- if (previousHome === undefined) delete process.env.PI_CODING_AGENT_DIR;
48
- else process.env.PI_CODING_AGENT_DIR = previousHome;
326
+ process.chdir(previousCwd);
327
+ for (const key of environment)
328
+ if (previousEnv[key] === undefined) delete process.env[key];
329
+ else process.env[key] = previousEnv[key];
330
+ delete globalThis[Symbol.for('pi1.workflow.identity')];
49
331
  await rm(home, { recursive: true, force: true });
50
332
  }