@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.
@@ -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
- for (const command of extensionCommands.filter(isRecord)) {
377
- const name = stringProperty(command, "name");
378
- if (!name || usedNames.has(name))
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.#transcript.map(block => ({ ...block })),
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.#lifecycle = "busy";
1411
- this.#status = { ...this.#status, workingMessage: "Working..." };
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.#transcript = this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized" } : block);
1456
- this.#lifecycle = "ready";
1457
- this.#status = { ...this.#status, workingMessage: null };
1458
- this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
1459
- this.#emitEvent({ type: "status", status: this.#status });
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.#lifecycle = "busy";
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
- case "compaction_end":
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.#lifecycle = "busy";
1487
- this.#status = { ...this.#status, workingMessage: "Compacting…" };
1488
- this.#emitEvent({ type: "status", status: this.#status });
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.#transcript = blocks;
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.#transcript.find(block => block.id === blockId);
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.#transcript.findIndex(existing => existing.id === block.id);
1755
- if (index >= 0)
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.#transcript.some(block => block.id === candidate && block.status === "live"));
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.#transcript.find(block => block.id === id);
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;