@timurproko/a1 0.1.7 → 0.1.8-dev.0b6d8cc
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/README.md +24 -13
- package/bin/cli.js +2 -2
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/src/cli/dispatch.d.ts +2 -1
- package/dist/src/cli/dispatch.js +75 -21
- package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -1
- package/dist/src/foundation/pi-component-adapter/index.js +1 -1
- package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.js +2 -1
- package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +7 -0
- package/dist/src/foundation/pi-engine-adapter/adapter.js +141 -31
- package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +189 -13
- package/dist/src/foundation/release/index.d.ts +0 -1
- package/dist/src/foundation/release/index.js +0 -1
- package/dist/src/foundation/release/update.d.ts +2 -0
- package/dist/src/foundation/release/update.js +82 -11
- package/docs/ci-release-runbook.md +4 -2
- package/package.json +1 -1
- package/dist/src/foundation/release/development-preview-release.d.ts +0 -49
- package/dist/src/foundation/release/development-preview-release.js +0 -114
|
@@ -11,6 +11,12 @@ import { createPiRuntimeIntegration } from "./runtime-integration.js";
|
|
|
11
11
|
import { PiSessionCommandIntegration } from "./session-integration.js";
|
|
12
12
|
import { PiSettingsIntegration } from "./settings-integration.js";
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
|
+
/**
|
|
15
|
+
* Engine events delivered before the queue hands the event loop a turn. Small enough
|
|
16
|
+
* that a streaming burst never holds input, large enough that an ordinary turn is one
|
|
17
|
+
* batch.
|
|
18
|
+
*/
|
|
19
|
+
const EVENT_DELIVERY_BATCH = 16;
|
|
14
20
|
const DEFAULT_SURFACE = {
|
|
15
21
|
columns: 100,
|
|
16
22
|
rows: 32,
|
|
@@ -53,6 +59,8 @@ export class PiEngineAdapter {
|
|
|
53
59
|
#activeCommandIds = [];
|
|
54
60
|
#completedCommands = new Map();
|
|
55
61
|
#transcript = [];
|
|
62
|
+
#transcriptIndex = new Map();
|
|
63
|
+
#transcriptSnapshot;
|
|
56
64
|
#messageBlockIds = new WeakMap();
|
|
57
65
|
#messageFallbackIds = new Map();
|
|
58
66
|
#toolBlockIds = new Map();
|
|
@@ -61,6 +69,8 @@ export class PiEngineAdapter {
|
|
|
61
69
|
#eventQueue = [];
|
|
62
70
|
#eventQueueProcessing;
|
|
63
71
|
#droppedEventCount = 0;
|
|
72
|
+
#agentRunActive = false;
|
|
73
|
+
#statusKind = null;
|
|
64
74
|
#sessionCommands;
|
|
65
75
|
#gitBranch = null;
|
|
66
76
|
#extensionUi;
|
|
@@ -143,7 +153,7 @@ export class PiEngineAdapter {
|
|
|
143
153
|
if (this.#disposed || updates.length === 0)
|
|
144
154
|
return;
|
|
145
155
|
const packages = updates.map(name => `- ${name}`).join("\n");
|
|
146
|
-
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run pi update --extensions\nPackages:\n${packages}`, true);
|
|
156
|
+
this.#addDiagnostic("info", "package-updates", `Package updates are available. Run ${PRODUCT_IDENTITY.commandName} pi update --extensions\nPackages:\n${packages}`, true);
|
|
147
157
|
this.#emitView();
|
|
148
158
|
}
|
|
149
159
|
onEvent(listener) {
|
|
@@ -278,6 +288,7 @@ export class PiEngineAdapter {
|
|
|
278
288
|
id: `extension-${resources.length}`,
|
|
279
289
|
sourcePath: extension.path,
|
|
280
290
|
resolvedPath: extension.resolvedPath,
|
|
291
|
+
sourceInfo: extensionSourceSummary(extension.sourceInfo),
|
|
281
292
|
loaded: true,
|
|
282
293
|
hidden: extension.hidden === true,
|
|
283
294
|
diagnostic: null,
|
|
@@ -373,9 +384,10 @@ export class PiEngineAdapter {
|
|
|
373
384
|
}
|
|
374
385
|
const extensionCommands = this.#session?.extensionRunner?.getRegisteredCommands?.();
|
|
375
386
|
if (Array.isArray(extensionCommands)) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
387
|
+
const registered = extensionCommands.filter(isRecord);
|
|
388
|
+
for (const command of registered) {
|
|
389
|
+
const name = stringProperty(command, "invocationName") ?? stringProperty(command, "name");
|
|
390
|
+
if (!name || usedNames.has(name) || isPiPrefixedCompatibilityAlias(command, registered))
|
|
379
391
|
continue;
|
|
380
392
|
commands.push({ name, description: stringProperty(command, "description") ?? "Extension command", source: "extension" });
|
|
381
393
|
usedNames.add(name);
|
|
@@ -724,7 +736,7 @@ export class PiEngineAdapter {
|
|
|
724
736
|
sessionId: this.#sessionId,
|
|
725
737
|
revision: this.#viewRevision,
|
|
726
738
|
lifecycle: this.#lifecycle,
|
|
727
|
-
transcript: this.#
|
|
739
|
+
transcript: this.#transcriptSnapshot ??= Object.freeze([...this.#transcript]),
|
|
728
740
|
editor: { ...this.#editor, queuedSubmissions: [...this.#editor.queuedSubmissions] },
|
|
729
741
|
status: {
|
|
730
742
|
...this.#status,
|
|
@@ -1350,6 +1362,8 @@ export class PiEngineAdapter {
|
|
|
1350
1362
|
submitEnabled: true,
|
|
1351
1363
|
};
|
|
1352
1364
|
this.#status = { ...this.#status, workingMessage: null, badges: [] };
|
|
1365
|
+
this.#statusKind = null;
|
|
1366
|
+
this.#agentRunActive = false;
|
|
1353
1367
|
this.#activeModel = readModel(session.model);
|
|
1354
1368
|
this.#reconcileActiveModelAvailability();
|
|
1355
1369
|
this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
|
|
@@ -1402,15 +1416,61 @@ export class PiEngineAdapter {
|
|
|
1402
1416
|
this.#emitView();
|
|
1403
1417
|
}
|
|
1404
1418
|
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Replaces the transcript and the index that finds a block by its identifier. Every
|
|
1421
|
+
* lookup goes through the index, so streaming a chunk costs the same in a long session
|
|
1422
|
+
* as in a new one.
|
|
1423
|
+
*/
|
|
1424
|
+
#setTranscript(blocks) {
|
|
1425
|
+
this.#transcript = blocks;
|
|
1426
|
+
this.#transcriptIndex.clear();
|
|
1427
|
+
for (const [index, block] of blocks.entries())
|
|
1428
|
+
this.#transcriptIndex.set(block.id, index);
|
|
1429
|
+
this.#transcriptSnapshot = undefined;
|
|
1430
|
+
}
|
|
1431
|
+
#transcriptBlock(id) {
|
|
1432
|
+
const index = this.#transcriptIndex.get(id);
|
|
1433
|
+
return index === undefined ? undefined : this.#transcript[index];
|
|
1434
|
+
}
|
|
1435
|
+
/** Shows the state named by `kind`, which becomes the state a later end can clear. */
|
|
1436
|
+
#enterWorkState(kind, message) {
|
|
1437
|
+
const wasBusy = this.#lifecycle === "busy";
|
|
1438
|
+
this.#statusKind = kind;
|
|
1439
|
+
this.#lifecycle = "busy";
|
|
1440
|
+
this.#status = { ...this.#status, workingMessage: message };
|
|
1441
|
+
if (!wasBusy)
|
|
1442
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
1443
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Ends one named state. A state the shell is not in is left alone, so a finished
|
|
1447
|
+
* compaction or retry cannot clear the working state it never replaced. While the run
|
|
1448
|
+
* continues, ending either of those returns to working rather than to idle.
|
|
1449
|
+
*/
|
|
1450
|
+
#endWorkState(kind) {
|
|
1451
|
+
if (this.#statusKind !== kind)
|
|
1452
|
+
return;
|
|
1453
|
+
if (this.#agentRunActive) {
|
|
1454
|
+
this.#enterWorkState("working", "Working...");
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
this.#leaveWorkStates();
|
|
1458
|
+
}
|
|
1459
|
+
/** Leaves every work state and reports the session idle. */
|
|
1460
|
+
#leaveWorkStates() {
|
|
1461
|
+
this.#statusKind = null;
|
|
1462
|
+
this.#lifecycle = "ready";
|
|
1463
|
+
this.#status = { ...this.#status, workingMessage: null };
|
|
1464
|
+
this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
|
|
1465
|
+
this.#emitEvent({ type: "status", status: this.#status });
|
|
1466
|
+
}
|
|
1405
1467
|
#handlePiEvent(event) {
|
|
1406
1468
|
if (!isRecord(event) || typeof event.type !== "string")
|
|
1407
1469
|
return;
|
|
1408
1470
|
switch (event.type) {
|
|
1409
1471
|
case "agent_start":
|
|
1410
|
-
this.#
|
|
1411
|
-
this.#
|
|
1412
|
-
this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
|
|
1413
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1472
|
+
this.#agentRunActive = true;
|
|
1473
|
+
this.#enterWorkState("working", "Working...");
|
|
1414
1474
|
return;
|
|
1415
1475
|
case "message_start":
|
|
1416
1476
|
this.#upsertMessageBlock(event.message, "live");
|
|
@@ -1452,11 +1512,18 @@ export class PiEngineAdapter {
|
|
|
1452
1512
|
if (finalMessages.length > 0)
|
|
1453
1513
|
this.#rebuildTranscript(finalMessages, "finalized");
|
|
1454
1514
|
else
|
|
1455
|
-
this.#
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1515
|
+
this.#setTranscript(this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized" } : block));
|
|
1516
|
+
// Ending a turn leaves the working state, as the recorded pinned baseline does, but
|
|
1517
|
+
// it leaves only that state: a compaction or retry being shown outlives the turn
|
|
1518
|
+
// that ended under it. Settlement ends the run, and with it every state — the
|
|
1519
|
+
// engine ends a turn for each continuation it makes and settles once.
|
|
1520
|
+
if (event.type === "agent_settled") {
|
|
1521
|
+
this.#agentRunActive = false;
|
|
1522
|
+
this.#leaveWorkStates();
|
|
1523
|
+
}
|
|
1524
|
+
else if (this.#statusKind === null || this.#statusKind === "working") {
|
|
1525
|
+
this.#leaveWorkStates();
|
|
1526
|
+
}
|
|
1460
1527
|
this.#emitView();
|
|
1461
1528
|
return;
|
|
1462
1529
|
}
|
|
@@ -1472,20 +1539,16 @@ export class PiEngineAdapter {
|
|
|
1472
1539
|
return;
|
|
1473
1540
|
}
|
|
1474
1541
|
case "auto_retry_start":
|
|
1475
|
-
this.#
|
|
1476
|
-
this.#status = { ...this.#status, workingMessage: "Retrying…" };
|
|
1477
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1542
|
+
this.#enterWorkState("retry", "Retrying…");
|
|
1478
1543
|
return;
|
|
1479
1544
|
case "auto_retry_end":
|
|
1480
|
-
|
|
1481
|
-
this.#lifecycle = "ready";
|
|
1482
|
-
this.#status = { ...this.#status, workingMessage: null };
|
|
1483
|
-
this.#emitEvent({ type: "status", status: this.#status });
|
|
1545
|
+
this.#endWorkState("retry");
|
|
1484
1546
|
return;
|
|
1485
1547
|
case "compaction_start":
|
|
1486
|
-
this.#
|
|
1487
|
-
|
|
1488
|
-
|
|
1548
|
+
this.#enterWorkState("compaction", "Compacting…");
|
|
1549
|
+
return;
|
|
1550
|
+
case "compaction_end":
|
|
1551
|
+
this.#endWorkState("compaction");
|
|
1489
1552
|
return;
|
|
1490
1553
|
case "thinking_level_changed":
|
|
1491
1554
|
this.#thinkingLevel = readThinkingLevel(event.level);
|
|
@@ -1575,7 +1638,7 @@ export class PiEngineAdapter {
|
|
|
1575
1638
|
}
|
|
1576
1639
|
}
|
|
1577
1640
|
}
|
|
1578
|
-
this.#
|
|
1641
|
+
this.#setTranscript(blocks);
|
|
1579
1642
|
}
|
|
1580
1643
|
#upsertMessageBlock(message, status) {
|
|
1581
1644
|
const blocks = this.#messageBlocks(message, status, this.#transcript.length);
|
|
@@ -1660,7 +1723,7 @@ export class PiEngineAdapter {
|
|
|
1660
1723
|
: this.#toolBlockIds.get(toolCallId) ?? `tool-${toolCallId}`;
|
|
1661
1724
|
if (toolCallId !== undefined)
|
|
1662
1725
|
this.#toolBlockIds.set(toolCallId, blockId);
|
|
1663
|
-
const existing = this.#
|
|
1726
|
+
const existing = this.#transcriptBlock(blockId);
|
|
1664
1727
|
const existingPayload = isRecord(existing?.payload) ? existing.payload : undefined;
|
|
1665
1728
|
return [{
|
|
1666
1729
|
id: blockId,
|
|
@@ -1751,11 +1814,14 @@ export class PiEngineAdapter {
|
|
|
1751
1814
|
});
|
|
1752
1815
|
}
|
|
1753
1816
|
#upsertTranscriptBlock(block) {
|
|
1754
|
-
const index = this.#
|
|
1755
|
-
if (index
|
|
1817
|
+
const index = this.#transcriptIndex.get(block.id);
|
|
1818
|
+
if (index !== undefined)
|
|
1756
1819
|
this.#transcript[index] = block;
|
|
1757
|
-
else
|
|
1820
|
+
else {
|
|
1821
|
+
this.#transcriptIndex.set(block.id, this.#transcript.length);
|
|
1758
1822
|
this.#transcript.push(block);
|
|
1823
|
+
}
|
|
1824
|
+
this.#transcriptSnapshot = undefined;
|
|
1759
1825
|
this.#emitEvent({ type: "transcript-block", block });
|
|
1760
1826
|
}
|
|
1761
1827
|
#messageBlockId(message, fallbackIndex, status, occurrence) {
|
|
@@ -1773,7 +1839,7 @@ export class PiEngineAdapter {
|
|
|
1773
1839
|
}
|
|
1774
1840
|
}
|
|
1775
1841
|
else {
|
|
1776
|
-
id = [...cached].reverse().find(candidate => this.#
|
|
1842
|
+
id = [...cached].reverse().find(candidate => this.#transcriptBlock(candidate)?.status === "live");
|
|
1777
1843
|
if (id === undefined && status === "finalized")
|
|
1778
1844
|
id = cached.at(-1);
|
|
1779
1845
|
if (id === undefined) {
|
|
@@ -1786,7 +1852,7 @@ export class PiEngineAdapter {
|
|
|
1786
1852
|
return id;
|
|
1787
1853
|
}
|
|
1788
1854
|
#nextBlockRevision(id) {
|
|
1789
|
-
const existing = this.#
|
|
1855
|
+
const existing = this.#transcriptBlock(id);
|
|
1790
1856
|
if (existing)
|
|
1791
1857
|
return existing.revision + 1;
|
|
1792
1858
|
this.#nextBlockSequence += 1;
|
|
@@ -1847,6 +1913,7 @@ export class PiEngineAdapter {
|
|
|
1847
1913
|
}
|
|
1848
1914
|
async #processEventQueue() {
|
|
1849
1915
|
try {
|
|
1916
|
+
let deliveredSinceYield = 0;
|
|
1850
1917
|
while (this.#eventQueue.length > 0) {
|
|
1851
1918
|
const event = this.#eventQueue.shift();
|
|
1852
1919
|
if (!event)
|
|
@@ -1859,6 +1926,14 @@ export class PiEngineAdapter {
|
|
|
1859
1926
|
this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
|
|
1860
1927
|
}
|
|
1861
1928
|
}
|
|
1929
|
+
deliveredSinceYield += 1;
|
|
1930
|
+
// A microtask chain runs to exhaustion before the loop turns, so a streaming
|
|
1931
|
+
// burst would hold typed input, pointer reports, and timed indicators until it
|
|
1932
|
+
// drained. Yielding on a macrotask hands those their turn between batches.
|
|
1933
|
+
if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.length > 0) {
|
|
1934
|
+
deliveredSinceYield = 0;
|
|
1935
|
+
await new Promise(resolve => { setImmediate(resolve); });
|
|
1936
|
+
}
|
|
1862
1937
|
}
|
|
1863
1938
|
}
|
|
1864
1939
|
finally {
|
|
@@ -2245,11 +2320,26 @@ function extensionResourceDiagnostic(index, sourcePath, diagnostic) {
|
|
|
2245
2320
|
id: `extension-diagnostic-${index}`,
|
|
2246
2321
|
sourcePath,
|
|
2247
2322
|
resolvedPath: null,
|
|
2323
|
+
sourceInfo: null,
|
|
2248
2324
|
loaded: false,
|
|
2249
2325
|
hidden: false,
|
|
2250
2326
|
diagnostic,
|
|
2251
2327
|
};
|
|
2252
2328
|
}
|
|
2329
|
+
function extensionSourceSummary(value) {
|
|
2330
|
+
if (!isRecord(value))
|
|
2331
|
+
return null;
|
|
2332
|
+
const source = stringProperty(value, "source");
|
|
2333
|
+
const scope = value.scope;
|
|
2334
|
+
const origin = value.origin;
|
|
2335
|
+
const baseDir = value.baseDir;
|
|
2336
|
+
if (!source
|
|
2337
|
+
|| (scope !== "user" && scope !== "project" && scope !== "temporary")
|
|
2338
|
+
|| (origin !== "package" && origin !== "top-level")
|
|
2339
|
+
|| (baseDir !== undefined && typeof baseDir !== "string"))
|
|
2340
|
+
return null;
|
|
2341
|
+
return { source, scope, origin, baseDir: baseDir ?? null };
|
|
2342
|
+
}
|
|
2253
2343
|
function collectionResult(value, key) {
|
|
2254
2344
|
if (!isRecord(value))
|
|
2255
2345
|
return { values: [], diagnostics: [] };
|
|
@@ -2274,6 +2364,26 @@ function stringProperty(value, key) {
|
|
|
2274
2364
|
const item = value[key];
|
|
2275
2365
|
return typeof item === "string" && item.length > 0 ? item : undefined;
|
|
2276
2366
|
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Some ecosystem extensions retain a `pi-<name>` slash-command alias beside
|
|
2369
|
+
* their unprefixed command. A1 presents the product-neutral command once while
|
|
2370
|
+
* leaving Pi's runner free to accept the compatibility alias when typed.
|
|
2371
|
+
*/
|
|
2372
|
+
function isPiPrefixedCompatibilityAlias(command, commands) {
|
|
2373
|
+
const name = stringProperty(command, "name");
|
|
2374
|
+
if (!name?.startsWith("pi-") || name.length === 3)
|
|
2375
|
+
return false;
|
|
2376
|
+
const canonicalName = name.slice(3);
|
|
2377
|
+
const description = stringProperty(command, "description");
|
|
2378
|
+
const sourcePath = extensionCommandSourcePath(command);
|
|
2379
|
+
return commands.some(candidate => candidate !== command
|
|
2380
|
+
&& stringProperty(candidate, "name") === canonicalName
|
|
2381
|
+
&& stringProperty(candidate, "description") === description
|
|
2382
|
+
&& extensionCommandSourcePath(candidate) === sourcePath);
|
|
2383
|
+
}
|
|
2384
|
+
function extensionCommandSourcePath(command) {
|
|
2385
|
+
return isRecord(command) ? stringProperty(command.sourceInfo, "path") : undefined;
|
|
2386
|
+
}
|
|
2277
2387
|
function compactResourceLabel(path) {
|
|
2278
2388
|
const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
|
|
2279
2389
|
return segments.at(-1) ?? path;
|