@junctionpanel/server 0.1.76 → 0.1.78

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.
@@ -412,6 +412,25 @@ function asRecord(value) {
412
412
  }
413
413
  return value;
414
414
  }
415
+ function parseGeminiErrorPayload(value) {
416
+ const directRecord = asRecord(value);
417
+ if (directRecord) {
418
+ return directRecord;
419
+ }
420
+ if (typeof value !== "string") {
421
+ return null;
422
+ }
423
+ const trimmed = value.trim();
424
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
425
+ return null;
426
+ }
427
+ try {
428
+ return asRecord(JSON.parse(trimmed));
429
+ }
430
+ catch {
431
+ return null;
432
+ }
433
+ }
415
434
  function formatGeminiAcpError(error) {
416
435
  if (error instanceof acp.RequestError) {
417
436
  const directMessage = nonEmptyString(error.message);
@@ -426,9 +445,45 @@ function formatGeminiAcpError(error) {
426
445
  }
427
446
  return message;
428
447
  }
429
- const message = nonEmptyString(stringifyUnknown(error));
448
+ const rawMessage = stringifyUnknown(error);
449
+ const errorPayload = parseGeminiErrorPayload(rawMessage) ?? parseGeminiErrorPayload(error);
450
+ const payloadMessage = nonEmptyString(errorPayload?.message);
451
+ const message = payloadMessage ?? nonEmptyString(rawMessage);
452
+ const normalizedMessage = message?.toLowerCase() ?? "";
453
+ if (message &&
454
+ normalizedMessage.includes("no capacity available for model")) {
455
+ return `${message} Retry in a moment or switch to a Flash/Auto Gemini model.`;
456
+ }
430
457
  return message ?? "Gemini ACP request failed";
431
458
  }
459
+ function appendGeminiStreamText(current, incoming) {
460
+ if (incoming.length === 0) {
461
+ return current;
462
+ }
463
+ if (current.length === 0) {
464
+ return incoming;
465
+ }
466
+ if (incoming.length > current.length && incoming.startsWith(current)) {
467
+ return incoming;
468
+ }
469
+ const maxOverlap = Math.min(current.length, Math.max(incoming.length - 1, 0));
470
+ for (let overlapLength = maxOverlap; overlapLength > 0; overlapLength -= 1) {
471
+ if (current.endsWith(incoming.slice(0, overlapLength))) {
472
+ return current + incoming.slice(overlapLength);
473
+ }
474
+ }
475
+ return current + incoming;
476
+ }
477
+ function shouldPreferRecordedGeminiTurnOutput(modelId) {
478
+ const normalizedModelId = modelId?.trim().toLowerCase() ?? "";
479
+ return (normalizedModelId.includes("gemini-3.1-pro-preview") ||
480
+ normalizedModelId.includes("gemini-3.1-pro-preview-customtools"));
481
+ }
482
+ function shouldDeferGeminiTextStreaming(modelId) {
483
+ const normalizedModelId = modelId?.trim().toLowerCase() ?? "";
484
+ return (shouldPreferRecordedGeminiTurnOutput(normalizedModelId) ||
485
+ normalizedModelId.includes("auto-gemini-3"));
486
+ }
432
487
  export function normalizeGeminiMode(modeId) {
433
488
  if (!modeId) {
434
489
  return DEFAULT_GEMINI_MODE;
@@ -849,6 +904,16 @@ function getGeminiRecordedTurnMessages(session, turnStartedAt) {
849
904
  function hasGeminiRecordedAssistantMessage(messages) {
850
905
  return messages.some((message) => message.type === "gemini");
851
906
  }
907
+ function buildGeminiRecordedTurnOutputItems(messages) {
908
+ const items = [];
909
+ for (const message of messages) {
910
+ if (message.type !== "gemini") {
911
+ continue;
912
+ }
913
+ items.push(...buildGeminiRecordedMessageTimelineItems(message).filter((item) => item.type === "assistant_message" || item.type === "reasoning"));
914
+ }
915
+ return items;
916
+ }
852
917
  function countGeminiRecordedHistoryEvents(session) {
853
918
  let count = 0;
854
919
  for (const message of session.messages) {
@@ -1513,6 +1578,7 @@ class GeminiAgentSession {
1513
1578
  this.pendingPermissions = new Map();
1514
1579
  this.toolStates = new Map();
1515
1580
  this.activeTurnQueue = null;
1581
+ this.activeTurnState = null;
1516
1582
  this.historyEvents = [];
1517
1583
  this.historyReadyPromise = null;
1518
1584
  this.resolveHistoryReady = null;
@@ -1544,6 +1610,44 @@ class GeminiAgentSession {
1544
1610
  await this.openConnection();
1545
1611
  this.initialized = true;
1546
1612
  }
1613
+ beginActiveTurn(queue, startedAt) {
1614
+ const turnState = {
1615
+ startedAt,
1616
+ queue,
1617
+ terminalState: "open",
1618
+ deferTextStreaming: shouldDeferGeminiTextStreaming(this.currentModelId),
1619
+ liveAssistantText: "",
1620
+ liveReasoningText: "",
1621
+ deferredOutputItems: [],
1622
+ };
1623
+ this.activeTurnState = turnState;
1624
+ return turnState;
1625
+ }
1626
+ finalizeActiveTurn(turnState, terminalState) {
1627
+ if (turnState.terminalState !== "open") {
1628
+ return false;
1629
+ }
1630
+ turnState.terminalState = terminalState;
1631
+ return true;
1632
+ }
1633
+ trackLiveTurnOutput(turnState, item) {
1634
+ if (!turnState || turnState.terminalState !== "open") {
1635
+ return;
1636
+ }
1637
+ if (item.type === "assistant_message") {
1638
+ turnState.liveAssistantText = appendGeminiStreamText(turnState.liveAssistantText, item.text);
1639
+ if (turnState.deferTextStreaming) {
1640
+ turnState.deferredOutputItems.push(item);
1641
+ }
1642
+ return;
1643
+ }
1644
+ if (item.type === "reasoning") {
1645
+ turnState.liveReasoningText = appendGeminiStreamText(turnState.liveReasoningText, item.text);
1646
+ if (turnState.deferTextStreaming) {
1647
+ turnState.deferredOutputItems.push(item);
1648
+ }
1649
+ }
1650
+ }
1547
1651
  async run(prompt, options) {
1548
1652
  const timeline = [];
1549
1653
  let finalText = "";
@@ -1553,7 +1657,7 @@ class GeminiAgentSession {
1553
1657
  if (event.type === "timeline") {
1554
1658
  timeline.push(event.item);
1555
1659
  if (event.item.type === "assistant_message") {
1556
- finalText += event.item.text;
1660
+ finalText = appendGeminiStreamText(finalText, event.item.text);
1557
1661
  }
1558
1662
  }
1559
1663
  else if (event.type === "turn_completed") {
@@ -1585,48 +1689,60 @@ class GeminiAgentSession {
1585
1689
  const queue = new AsyncEventQueue();
1586
1690
  this.activeTurnQueue = queue;
1587
1691
  this.currentRunInterrupted = false;
1692
+ const connection = this.connection;
1588
1693
  const turnStartedAt = new Date();
1694
+ const turnState = this.beginActiveTurn(queue, turnStartedAt);
1589
1695
  queue.push({
1590
1696
  type: "turn_started",
1591
1697
  provider: GEMINI_PROVIDER,
1592
1698
  });
1593
- void this.connection
1594
- .prompt({
1595
- sessionId: this.sessionId,
1596
- prompt: toGeminiPromptBlocks(prompt),
1597
- })
1598
- .then(async (response) => {
1599
- if (response.stopReason === "cancelled") {
1600
- queue.push({
1601
- type: "turn_canceled",
1602
- provider: GEMINI_PROVIDER,
1603
- reason: "Interrupted",
1699
+ void (async () => {
1700
+ try {
1701
+ const response = await connection.prompt({
1702
+ sessionId: this.sessionId,
1703
+ prompt: toGeminiPromptBlocks(prompt),
1604
1704
  });
1705
+ if (response.stopReason === "cancelled") {
1706
+ if (!this.finalizeActiveTurn(turnState, "canceled")) {
1707
+ return;
1708
+ }
1709
+ queue.push({
1710
+ type: "turn_canceled",
1711
+ provider: GEMINI_PROVIDER,
1712
+ reason: "Interrupted",
1713
+ });
1714
+ }
1715
+ else {
1716
+ if (!this.finalizeActiveTurn(turnState, "completed")) {
1717
+ return;
1718
+ }
1719
+ const recordedTurnCompletion = await this.readRecordedTurnCompletion(turnStartedAt);
1720
+ await this.emitRecordedTurnOutput(turnState, recordedTurnCompletion);
1721
+ queue.push({
1722
+ type: "turn_completed",
1723
+ provider: GEMINI_PROVIDER,
1724
+ ...(recordedTurnCompletion?.summary
1725
+ ? {
1726
+ usage: recordedTurnCompletion.summary.usage,
1727
+ modelId: recordedTurnCompletion.summary.modelId,
1728
+ }
1729
+ : {}),
1730
+ });
1731
+ }
1732
+ queue.close();
1605
1733
  }
1606
- else {
1607
- // ACP does not report usage, so we synthesize it from Gemini's recorded session file.
1608
- const recordedTurnSummary = await this.readRecordedTurnSummary(turnStartedAt);
1734
+ catch (error) {
1735
+ if (!this.finalizeActiveTurn(turnState, "failed")) {
1736
+ return;
1737
+ }
1609
1738
  queue.push({
1610
- type: "turn_completed",
1739
+ type: "turn_failed",
1611
1740
  provider: GEMINI_PROVIDER,
1612
- ...(recordedTurnSummary
1613
- ? {
1614
- usage: recordedTurnSummary.usage,
1615
- modelId: recordedTurnSummary.modelId,
1616
- }
1617
- : {}),
1741
+ error: formatGeminiAcpError(error),
1618
1742
  });
1743
+ queue.close();
1619
1744
  }
1620
- queue.close();
1621
- })
1622
- .catch((error) => {
1623
- queue.push({
1624
- type: "turn_failed",
1625
- provider: GEMINI_PROVIDER,
1626
- error: formatGeminiAcpError(error),
1627
- });
1628
- queue.close();
1629
- });
1745
+ })();
1630
1746
  try {
1631
1747
  while (true) {
1632
1748
  const nextEvent = await queue.shift();
@@ -1640,6 +1756,9 @@ class GeminiAgentSession {
1640
1756
  if (this.activeTurnQueue === queue) {
1641
1757
  this.activeTurnQueue = null;
1642
1758
  }
1759
+ if (this.activeTurnState === turnState) {
1760
+ this.activeTurnState = null;
1761
+ }
1643
1762
  this.currentRunInterrupted = false;
1644
1763
  }
1645
1764
  }
@@ -1802,6 +1921,7 @@ class GeminiAgentSession {
1802
1921
  }
1803
1922
  this.activeTurnQueue?.close();
1804
1923
  this.activeTurnQueue = null;
1924
+ this.activeTurnState = null;
1805
1925
  if (this.child?.stdin) {
1806
1926
  this.child.stdin.end();
1807
1927
  }
@@ -1830,6 +1950,7 @@ class GeminiAgentSession {
1830
1950
  if (this.closePromise === params.closePromise) {
1831
1951
  this.closePromise = Promise.resolve();
1832
1952
  }
1953
+ this.activeTurnState = null;
1833
1954
  }
1834
1955
  async setModel(modelId) {
1835
1956
  await this.initialize();
@@ -1933,13 +2054,26 @@ class GeminiAgentSession {
1933
2054
  this.connection = null;
1934
2055
  }
1935
2056
  if (!this.closed) {
1936
- if (this.activeTurnQueue) {
1937
- this.activeTurnQueue.push({
1938
- type: "turn_failed",
1939
- provider: GEMINI_PROVIDER,
1940
- error: message,
1941
- });
1942
- this.activeTurnQueue.close();
2057
+ const activeTurnState = this.activeTurnState;
2058
+ if (activeTurnState && this.activeTurnQueue) {
2059
+ const terminalState = this.currentRunInterrupted ? "canceled" : "failed";
2060
+ if (this.finalizeActiveTurn(activeTurnState, terminalState)) {
2061
+ if (terminalState === "canceled") {
2062
+ this.activeTurnQueue.push({
2063
+ type: "turn_canceled",
2064
+ provider: GEMINI_PROVIDER,
2065
+ reason: "Interrupted",
2066
+ });
2067
+ }
2068
+ else {
2069
+ this.activeTurnQueue.push({
2070
+ type: "turn_failed",
2071
+ provider: GEMINI_PROVIDER,
2072
+ error: message,
2073
+ });
2074
+ }
2075
+ this.activeTurnQueue.close();
2076
+ }
1943
2077
  }
1944
2078
  this.logger.warn({ code, signal, stderr }, "Gemini ACP process closed");
1945
2079
  }
@@ -2138,6 +2272,45 @@ class GeminiAgentSession {
2138
2272
  this.currentModeId = notification.update.currentModeId;
2139
2273
  return;
2140
2274
  }
2275
+ const activeTurnState = this.activeTurnState;
2276
+ if (notification.update.sessionUpdate === "agent_message_chunk") {
2277
+ if (notification.update.content.type !== "text") {
2278
+ return;
2279
+ }
2280
+ const text = notification.update.content.text;
2281
+ if (text.length === 0) {
2282
+ return;
2283
+ }
2284
+ if (activeTurnState && activeTurnState.terminalState !== "open") {
2285
+ return;
2286
+ }
2287
+ this.trackLiveTurnOutput(activeTurnState, {
2288
+ type: "assistant_message",
2289
+ text,
2290
+ });
2291
+ if (activeTurnState?.deferTextStreaming) {
2292
+ return;
2293
+ }
2294
+ }
2295
+ if (notification.update.sessionUpdate === "agent_thought_chunk") {
2296
+ if (notification.update.content.type !== "text") {
2297
+ return;
2298
+ }
2299
+ const text = notification.update.content.text;
2300
+ if (text.length === 0) {
2301
+ return;
2302
+ }
2303
+ if (activeTurnState && activeTurnState.terminalState !== "open") {
2304
+ return;
2305
+ }
2306
+ this.trackLiveTurnOutput(activeTurnState, {
2307
+ type: "reasoning",
2308
+ text,
2309
+ });
2310
+ if (activeTurnState?.deferTextStreaming) {
2311
+ return;
2312
+ }
2313
+ }
2141
2314
  const includeUserMessages = this.historyCaptureActive;
2142
2315
  const events = sessionUpdateToEvents(notification.update, this.toolStates, {
2143
2316
  includeUserMessages,
@@ -2158,13 +2331,48 @@ class GeminiAgentSession {
2158
2331
  return;
2159
2332
  }
2160
2333
  for (const event of events) {
2334
+ if (activeTurnState?.terminalState !== "open" &&
2335
+ event.type === "timeline" &&
2336
+ (event.item.type === "assistant_message" || event.item.type === "reasoning")) {
2337
+ continue;
2338
+ }
2161
2339
  this.emitLiveEvent(event);
2162
2340
  }
2163
2341
  }
2164
2342
  emitLiveEvent(event) {
2165
2343
  this.activeTurnQueue?.push(event);
2166
2344
  }
2167
- async readRecordedTurnSummary(turnStartedAt) {
2345
+ async emitRecordedTurnOutput(turnState, completion) {
2346
+ const recordedOutputItems = completion
2347
+ ? buildGeminiRecordedTurnOutputItems(completion.messages)
2348
+ : [];
2349
+ const preferRecordedOutput = shouldPreferRecordedGeminiTurnOutput(completion?.summary?.modelId) ||
2350
+ turnState.deferTextStreaming;
2351
+ const outputItems = recordedOutputItems.length > 0
2352
+ ? recordedOutputItems.filter((item) => {
2353
+ if (preferRecordedOutput) {
2354
+ return true;
2355
+ }
2356
+ if (item.type === "assistant_message") {
2357
+ return turnState.liveAssistantText.length === 0;
2358
+ }
2359
+ if (item.type === "reasoning") {
2360
+ return turnState.liveReasoningText.length === 0;
2361
+ }
2362
+ return false;
2363
+ })
2364
+ : turnState.deferTextStreaming
2365
+ ? turnState.deferredOutputItems
2366
+ : [];
2367
+ for (const item of outputItems) {
2368
+ turnState.queue.push({
2369
+ type: "timeline",
2370
+ provider: GEMINI_PROVIDER,
2371
+ item,
2372
+ });
2373
+ }
2374
+ }
2375
+ async readRecordedTurnCompletion(turnStartedAt) {
2168
2376
  if (!this.sessionId) {
2169
2377
  return null;
2170
2378
  }
@@ -2177,10 +2385,10 @@ class GeminiAgentSession {
2177
2385
  (this.hasObservedRecordedSession && !this.recordedSessionFilePath
2178
2386
  ? GEMINI_RECORDED_SESSION_DISCOVERY_TIMEOUT_MS
2179
2387
  : GEMINI_HISTORY_FALLBACK_IDLE_MS);
2180
- let latestFingerprint = null;
2388
+ let latestSummaryFingerprint = null;
2181
2389
  let observedSummary = false;
2182
- let latestEmptyTurnFingerprint = null;
2183
- let observedEmptyTurn = false;
2390
+ let latestTurnFingerprint = null;
2391
+ let observedTurn = false;
2184
2392
  let sawRecordedSession = false;
2185
2393
  while (Date.now() <= deadline) {
2186
2394
  let recordedSession = null;
@@ -2202,24 +2410,32 @@ class GeminiAgentSession {
2202
2410
  this.hasObservedRecordedSession = true;
2203
2411
  const turnMessages = getGeminiRecordedTurnMessages(recordedSession, turnStartedAt);
2204
2412
  const nextSummary = summarizeGeminiRecordedMessages(turnMessages);
2413
+ const turnFingerprint = JSON.stringify(turnMessages);
2205
2414
  if (nextSummary) {
2206
- const nextFingerprint = JSON.stringify(nextSummary);
2207
- if (observedSummary && nextFingerprint === latestFingerprint) {
2208
- return nextSummary;
2415
+ const nextSummaryFingerprint = JSON.stringify(nextSummary);
2416
+ if (observedSummary &&
2417
+ nextSummaryFingerprint === latestSummaryFingerprint &&
2418
+ observedTurn &&
2419
+ turnFingerprint === latestTurnFingerprint) {
2420
+ return {
2421
+ summary: nextSummary,
2422
+ messages: turnMessages,
2423
+ };
2209
2424
  }
2210
- latestFingerprint = nextFingerprint;
2425
+ latestSummaryFingerprint = nextSummaryFingerprint;
2211
2426
  observedSummary = true;
2212
- observedEmptyTurn = false;
2213
- latestEmptyTurnFingerprint = null;
2427
+ latestTurnFingerprint = turnFingerprint;
2428
+ observedTurn = true;
2214
2429
  }
2215
2430
  else if (hasGeminiRecordedAssistantMessage(turnMessages)) {
2216
- const nextEmptyTurnFingerprint = JSON.stringify(turnMessages);
2217
- if (observedEmptyTurn &&
2218
- nextEmptyTurnFingerprint === latestEmptyTurnFingerprint) {
2219
- return null;
2431
+ if (observedTurn && turnFingerprint === latestTurnFingerprint) {
2432
+ return {
2433
+ summary: null,
2434
+ messages: turnMessages,
2435
+ };
2220
2436
  }
2221
- observedEmptyTurn = true;
2222
- latestEmptyTurnFingerprint = nextEmptyTurnFingerprint;
2437
+ observedTurn = true;
2438
+ latestTurnFingerprint = turnFingerprint;
2223
2439
  }
2224
2440
  }
2225
2441
  else if (!sawRecordedSession && Date.now() >= discoveryDeadline) {