@pencil-agent/nano-pencil 1.14.2 → 1.14.4

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.
Files changed (104) hide show
  1. package/dist/build-meta.json +3 -3
  2. package/dist/builtin-extensions.d.ts +4 -0
  3. package/dist/builtin-extensions.js +16 -16
  4. package/dist/core/config/settings-manager.d.ts +8 -1
  5. package/dist/core/config/settings-manager.js +9 -0
  6. package/dist/core/extensions/runner.d.ts +70 -4
  7. package/dist/core/extensions/runner.js +188 -8
  8. package/dist/core/extensions/types.d.ts +8 -1
  9. package/dist/core/i18n/slash-commands.d.ts +12 -0
  10. package/dist/core/i18n/slash-commands.js +16 -4
  11. package/dist/core/i18n/slash-commands.zh.d.ts +12 -0
  12. package/dist/core/i18n/slash-commands.zh.js +16 -4
  13. package/dist/core/runtime/agent-session.d.ts +5 -0
  14. package/dist/core/runtime/agent-session.js +85 -27
  15. package/dist/core/runtime/extension-core-bindings.d.ts +3 -3
  16. package/dist/core/runtime/extension-core-bindings.js +73 -20
  17. package/dist/core/runtime/retry-coordinator.d.ts +10 -1
  18. package/dist/core/runtime/retry-coordinator.js +20 -4
  19. package/dist/core/runtime/sdk.js +2 -1
  20. package/dist/core/runtime/slash-command-catalog.d.ts +2 -1
  21. package/dist/core/runtime/slash-command-catalog.js +9 -1
  22. package/dist/core/slash-commands.d.ts +12 -1
  23. package/dist/core/slash-commands.js +81 -32
  24. package/dist/core/telemetry/batching-dispatcher.d.ts +41 -0
  25. package/dist/core/telemetry/batching-dispatcher.js +89 -0
  26. package/dist/core/telemetry/build-meta.d.ts +12 -0
  27. package/dist/core/telemetry/build-meta.js +57 -0
  28. package/dist/core/telemetry/caller-context.d.ts +32 -0
  29. package/dist/core/telemetry/caller-context.js +19 -0
  30. package/dist/core/telemetry/credentials.d.ts +27 -0
  31. package/dist/core/telemetry/credentials.js +87 -0
  32. package/dist/core/telemetry/ext-events.d.ts +89 -0
  33. package/dist/core/telemetry/ext-events.js +189 -0
  34. package/dist/core/telemetry/index.d.ts +13 -0
  35. package/dist/core/telemetry/index.js +6 -0
  36. package/dist/core/telemetry/insforge-base.d.ts +37 -0
  37. package/dist/core/telemetry/insforge-base.js +160 -0
  38. package/dist/core/telemetry/types.d.ts +33 -0
  39. package/dist/core/telemetry/types.js +7 -0
  40. package/dist/extensions/defaults/browser/index.js +14 -6
  41. package/dist/extensions/defaults/btw/index.js +2 -2
  42. package/dist/extensions/defaults/debug/index.js +38 -3
  43. package/dist/extensions/defaults/diagnostics/index.js +12 -0
  44. package/dist/extensions/defaults/grub/grub-parser.d.ts +15 -1
  45. package/dist/extensions/defaults/grub/grub-parser.js +31 -1
  46. package/dist/extensions/defaults/grub/index.d.ts +1 -1
  47. package/dist/extensions/defaults/grub/index.js +4 -3
  48. package/dist/extensions/defaults/interview/index.js +2 -2
  49. package/dist/extensions/defaults/link-world/index.js +14 -6
  50. package/dist/extensions/defaults/loop/cron/cron-scheduler.js +19 -0
  51. package/dist/extensions/defaults/loop/index.js +35 -0
  52. package/dist/extensions/defaults/mcp/index.js +18 -0
  53. package/dist/extensions/defaults/plan/index.js +29 -11
  54. package/dist/extensions/defaults/presence/index.d.ts +12 -2
  55. package/dist/extensions/defaults/presence/index.js +77 -23
  56. package/dist/extensions/defaults/presence/presence-memory.d.ts +2 -1
  57. package/dist/extensions/defaults/presence/presence-memory.js +37 -1
  58. package/dist/extensions/defaults/recap/index.js +12 -0
  59. package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +6 -17
  60. package/dist/extensions/defaults/sal/eval/insforge-sink.js +40 -183
  61. package/dist/extensions/defaults/sal/index.js +31 -8
  62. package/dist/extensions/defaults/sal/sal-config.d.ts +5 -0
  63. package/dist/extensions/defaults/sal/sal-config.js +15 -82
  64. package/dist/extensions/defaults/security-audit/index.js +141 -83
  65. package/dist/extensions/defaults/subagent/index.js +29 -5
  66. package/dist/extensions/defaults/team/index.js +10 -9
  67. package/dist/extensions/defaults/team/team-parser.d.ts +18 -0
  68. package/dist/extensions/defaults/team/team-parser.js +91 -3
  69. package/dist/extensions/defaults/team/team-ui.js +4 -14
  70. package/dist/extensions/defaults/token-save/index.js +14 -1
  71. package/dist/extensions/optional/export-html/index.js +19 -5
  72. package/dist/extensions/optional/simplify/index.js +11 -5
  73. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  74. package/dist/modes/interactive/interactive-mode.js +68 -19
  75. package/dist/modes/interactive/slash-command-arguments.d.ts +16 -0
  76. package/dist/modes/interactive/slash-command-arguments.js +97 -0
  77. package/dist/modes/rpc/rpc-mode.d.ts +3 -0
  78. package/dist/modes/rpc/rpc-mode.js +40 -31
  79. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  80. package/dist/node_modules/@pencil-agent/agent-core/agent.d.ts +15 -1
  81. package/dist/node_modules/@pencil-agent/agent-core/agent.js +13 -1
  82. package/dist/node_modules/@pencil-agent/agent-core/index.d.ts +2 -1
  83. package/dist/node_modules/@pencil-agent/agent-core/index.js +2 -1
  84. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.d.ts +1 -1
  85. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +293 -20
  86. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.d.ts +33 -0
  87. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.js +189 -0
  88. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.d.ts +9 -0
  89. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.js +30 -5
  90. package/dist/node_modules/@pencil-agent/agent-core/types.d.ts +90 -3
  91. package/dist/node_modules/@pencil-agent/agent-core/types.js +1 -1
  92. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +0 -17
  93. package/dist/node_modules/@pencil-agent/ai/models.generated.js +29 -46
  94. package/dist/node_modules/@pencil-agent/tui/autocomplete.d.ts +8 -1
  95. package/dist/node_modules/@pencil-agent/tui/autocomplete.js +18 -2
  96. package/dist/node_modules/@pencil-agent/tui/index.d.ts +1 -1
  97. package/dist/node_modules/@pencil-agent/tui/tui.d.ts +8 -3
  98. package/dist/node_modules/@pencil-agent/tui/tui.js +68 -33
  99. package/dist/packages/mem-core/extension.js +154 -83
  100. package/dist/packages/mem-core/full-insights-sections.d.ts +48 -0
  101. package/dist/packages/mem-core/full-insights-sections.js +231 -0
  102. package/dist/packages/mem-core/full-insights.d.ts +1 -1
  103. package/dist/packages/mem-core/full-insights.js +102 -42
  104. package/package.json +2 -2
@@ -5,16 +5,27 @@
5
5
  * [HERE]: core/i18n/slash-commands.zh.ts - Chinese slash command translations
6
6
  */
7
7
  export const slashCommands = {
8
+ categories: {
9
+ core: "核心",
10
+ model: "模型",
11
+ memory: "记忆",
12
+ session: "会话",
13
+ workflow: "工作流",
14
+ agents: "Agent",
15
+ tools: "工具",
16
+ admin: "管理",
17
+ },
8
18
  settings: "打开设置菜单",
9
19
  model: "选择模型(打开选择器界面)",
10
- "agent-loop": "设置本会话的 standard/弱模型兼容 agent loop",
11
- "scoped-models": "启用/禁用 Ctrl+P 循环的模型",
20
+ thinking: "选择当前模型的推理深度",
21
+ "agent-loop": "选择 agent 推进任务的方式",
22
+ "scoped-models": "选择快速切换里出现的模型",
12
23
  apikey: "更新当前提供商的 API 密钥",
13
24
  mcp: "管理 MCP 服务器(列出、启用、禁用)",
14
25
  soul: "显示 AI 人格和统计(灵魂)",
15
26
  persona: "切换 AI 人格/个性包",
16
27
  memory: "显示项目记忆和知识(纳米记忆)",
17
- dream: "整合项目记忆(纳米记忆)",
28
+ dream: "刷新长期项目记忆(纳米记忆)",
18
29
  export: "将会话导出为 HTML 文件",
19
30
  share: "将会话分享为保密的 GitHub gist",
20
31
  copy: "复制上一条 AI 消息到剪贴板",
@@ -24,6 +35,7 @@ export const slashCommands = {
24
35
  usage: "显示 token 使用量和费用统计",
25
36
  changelog: "显示更新日志条目",
26
37
  hotkeys: "显示所有键盘快捷键",
38
+ resources: "显示已加载的扩展、提示、技能和主题",
27
39
  fork: "从上一条消息创建新分支",
28
40
  tree: "导航会话树(切换分支)",
29
41
  login: "通过 OAuth 提供商登录",
@@ -34,7 +46,7 @@ export const slashCommands = {
34
46
  compact: "手动压缩会话上下文",
35
47
  resume: "恢复其他会话",
36
48
  reload: "重新加载扩展、技能、提示和主题",
37
- "link-world": "安装 link-world 以获得互联网访问(Twitter、YouTube 等)",
49
+ "link-world": "设置联网访问工具",
38
50
  quit: "退出 NanoPencil",
39
51
  language: "切换语言(English/中文)",
40
52
  };
@@ -342,6 +342,10 @@ export declare class AgentSession {
342
342
  prompt(text: string, options?: PromptOptions): Promise<void>;
343
343
  /**
344
344
  * Try to execute an extension command. Returns true if command was found and executed.
345
+ *
346
+ * Delegates to ExtensionRunner.invokeCommand() so command dispatch, error
347
+ * routing (emitError), and telemetry (ext_command_events) all happen in one
348
+ * place rather than being scattered across modes.
345
349
  */
346
350
  private _tryExecuteExtensionCommand;
347
351
  /**
@@ -516,6 +520,7 @@ export declare class AgentSession {
516
520
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
517
521
  */
518
522
  private _checkCompaction;
523
+ private _recoverModelErrorInLoop;
519
524
  /**
520
525
  * Internal: Run auto-compaction with events.
521
526
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: AgentSession class, session lifecycle, event emission
2
+ * [WHO]: AgentSession class, session lifecycle, event emission, in-loop context-overflow recovery adapter
3
3
  * [FROM]: Depends on agent-core, ai, core/tools/*, core/session/*, core/config/*
4
4
  * [TO]: Consumed by core/index.ts, core/runtime/sdk.ts, modes/interactive/interactive-mode.ts, modes/print-mode.ts, modes/rpc/rpc-mode.ts, modes/acp/acp-mode.ts, modes/rpc/rpc-types.ts, modes/rpc/rpc-client.ts, modes/interactive/components/footer.ts, modes/interactive/components/skill-invocation-message.ts
5
5
  * [HERE]: Central runtime hub; all modes delegate to this class
@@ -29,6 +29,7 @@ import { CompactionCoordinator } from "../session/compaction/compaction-coordina
29
29
  import { ToolOrchestrator } from "../tools/orchestrator.js";
30
30
  import { ModelSwitcher } from "../model/index.js";
31
31
  import { DEFAULT_THINKING_LEVEL } from "../defaults.js";
32
+ import { createExtensionTelemetrySink } from "../telemetry/index.js";
32
33
  import { exportSessionToHtml, } from "../export-html/index.js";
33
34
  import { createToolHtmlRenderer } from "../export-html/tool-renderer.js";
34
35
  import { ExtensionRunner, wrapRegisteredTools, wrapToolsWithExtensions, } from "../extensions/index.js";
@@ -158,6 +159,7 @@ export class AgentSession {
158
159
  this._soulManager = config.soulManager;
159
160
  this._initialActiveToolNames = config.initialActiveToolNames;
160
161
  this._baseToolsOverride = config.baseToolsOverride;
162
+ this.agent.setModelErrorRecovery((event) => this._recoverModelErrorInLoop(event));
161
163
  // Always subscribe to agent events for internal handling
162
164
  // (session persistence, extensions, auto-compaction, retry logic)
163
165
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
@@ -844,32 +846,22 @@ export class AgentSession {
844
846
  }
845
847
  /**
846
848
  * Try to execute an extension command. Returns true if command was found and executed.
849
+ *
850
+ * Delegates to ExtensionRunner.invokeCommand() so command dispatch, error
851
+ * routing (emitError), and telemetry (ext_command_events) all happen in one
852
+ * place rather than being scattered across modes.
847
853
  */
848
854
  async _tryExecuteExtensionCommand(text) {
849
855
  if (!this._extensionRunner)
850
856
  return false;
851
- // Parse command name and args
852
857
  const spaceIndex = text.indexOf(" ");
853
858
  const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
854
859
  const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1);
855
- const command = this._extensionRunner.getCommand(commandName);
856
- if (!command)
857
- return false;
858
- // Get command context from extension runner (includes session control methods)
859
860
  const ctx = this._extensionRunner.createCommandContext();
860
- try {
861
- await command.handler(args, ctx);
862
- return true;
863
- }
864
- catch (err) {
865
- // Emit error via extension runner
866
- this._extensionRunner.emitError({
867
- extensionPath: `command:${commandName}`,
868
- event: "command",
869
- error: err instanceof Error ? err.message : String(err),
870
- });
871
- return true;
872
- }
861
+ const result = await this._extensionRunner.invokeCommand(commandName, args, ctx, {
862
+ sessionId: this.sessionManager.getSessionId(),
863
+ });
864
+ return result.found;
873
865
  }
874
866
  /**
875
867
  * Expand skill commands (/skill:name args) to their full content.
@@ -1585,12 +1577,72 @@ export class AgentSession {
1585
1577
  await this._runAutoCompaction("threshold", false);
1586
1578
  }
1587
1579
  }
1580
+ async _recoverModelErrorInLoop(event) {
1581
+ const settings = this.settingsManager.getCompactionSettings();
1582
+ if (event.message.role !== "assistant")
1583
+ return { action: "stop" };
1584
+ const assistantMessage = event.message;
1585
+ if (event.errorSubtype !== "context_overflow") {
1586
+ if (!this._retryCoordinator.isRetryableError(assistantMessage)) {
1587
+ return { action: "stop" };
1588
+ }
1589
+ const shouldRetry = await this._retryCoordinator.handleErrorInLoop(assistantMessage);
1590
+ if (!shouldRetry)
1591
+ return { action: "stop" };
1592
+ const messages = this.agent.state.messages;
1593
+ const retryMessages = messages.at(-1)?.role === "assistant" ? messages.slice(0, -1) : messages;
1594
+ this.agent.replaceMessages(retryMessages);
1595
+ return {
1596
+ action: "retry",
1597
+ messages: retryMessages,
1598
+ transition: {
1599
+ reason: "model_error_recovery",
1600
+ subtype: event.errorSubtype,
1601
+ attempt: event.attempt,
1602
+ },
1603
+ };
1604
+ }
1605
+ if (!settings.enabled)
1606
+ return { action: "stop" };
1607
+ const contextWindow = this.model?.contextWindow ?? 0;
1608
+ const sameModel = this.model &&
1609
+ assistantMessage.provider === this.model.provider &&
1610
+ assistantMessage.model === this.model.id;
1611
+ if (!sameModel || !isContextOverflow(assistantMessage, contextWindow)) {
1612
+ return { action: "stop" };
1613
+ }
1614
+ const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
1615
+ const errorIsFromBeforeCompaction = compactionEntry !== null &&
1616
+ assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
1617
+ if (errorIsFromBeforeCompaction)
1618
+ return { action: "stop" };
1619
+ const messages = this.agent.state.messages;
1620
+ if (messages.length > 0 &&
1621
+ messages[messages.length - 1].role === "assistant") {
1622
+ this.agent.replaceMessages(messages.slice(0, -1));
1623
+ }
1624
+ const recoveredMessages = await this._runAutoCompaction("overflow", true, {
1625
+ triggerContinue: false,
1626
+ });
1627
+ if (!recoveredMessages)
1628
+ return { action: "stop" };
1629
+ return {
1630
+ action: "retry",
1631
+ messages: recoveredMessages,
1632
+ transition: {
1633
+ reason: "model_error_recovery",
1634
+ subtype: event.errorSubtype,
1635
+ attempt: event.attempt,
1636
+ },
1637
+ };
1638
+ }
1588
1639
  /**
1589
1640
  * Internal: Run auto-compaction with events.
1590
1641
  */
1591
- async _runAutoCompaction(reason, willRetry) {
1642
+ async _runAutoCompaction(reason, willRetry, options) {
1592
1643
  this._logger.info("Auto-compaction triggered", { reason, willRetry });
1593
1644
  const settings = this.settingsManager.getCompactionSettings();
1645
+ const triggerContinue = options?.triggerContinue ?? true;
1594
1646
  this._emit({ type: "auto_compaction_start", reason });
1595
1647
  this._autoCompactionAbortController = new AbortController();
1596
1648
  try {
@@ -1601,7 +1653,7 @@ export class AgentSession {
1601
1653
  aborted: false,
1602
1654
  willRetry: false,
1603
1655
  });
1604
- return;
1656
+ return undefined;
1605
1657
  }
1606
1658
  const apiKey = await this._modelRegistry.getApiKey(this.model);
1607
1659
  if (!apiKey) {
@@ -1611,7 +1663,7 @@ export class AgentSession {
1611
1663
  aborted: false,
1612
1664
  willRetry: false,
1613
1665
  });
1614
- return;
1666
+ return undefined;
1615
1667
  }
1616
1668
  const pathEntries = this.sessionManager.getBranch();
1617
1669
  const preparation = prepareCompaction(pathEntries, settings);
@@ -1622,7 +1674,7 @@ export class AgentSession {
1622
1674
  aborted: false,
1623
1675
  willRetry: false,
1624
1676
  });
1625
- return;
1677
+ return undefined;
1626
1678
  }
1627
1679
  let extensionCompaction;
1628
1680
  let fromExtension = false;
@@ -1641,7 +1693,7 @@ export class AgentSession {
1641
1693
  aborted: true,
1642
1694
  willRetry: false,
1643
1695
  });
1644
- return;
1696
+ return undefined;
1645
1697
  }
1646
1698
  if (extensionResult?.compaction) {
1647
1699
  extensionCompaction = extensionResult.compaction;
@@ -1674,7 +1726,7 @@ export class AgentSession {
1674
1726
  aborted: true,
1675
1727
  willRetry: false,
1676
1728
  });
1677
- return;
1729
+ return undefined;
1678
1730
  }
1679
1731
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
1680
1732
  const newEntries = this.sessionManager.getEntries();
@@ -1701,7 +1753,7 @@ export class AgentSession {
1701
1753
  aborted: false,
1702
1754
  willRetry,
1703
1755
  });
1704
- if (willRetry) {
1756
+ if (willRetry && triggerContinue) {
1705
1757
  const messages = this.agent.state.messages;
1706
1758
  const lastMsg = messages[messages.length - 1];
1707
1759
  if (lastMsg?.role === "assistant" &&
@@ -1712,13 +1764,14 @@ export class AgentSession {
1712
1764
  this.agent.continue().catch(() => { });
1713
1765
  }, 100);
1714
1766
  }
1715
- else if (this.agent.hasQueuedMessages()) {
1767
+ else if (!willRetry && this.agent.hasQueuedMessages()) {
1716
1768
  // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
1717
1769
  // Kick the loop so queued messages are actually delivered.
1718
1770
  setTimeout(() => {
1719
1771
  this.agent.continue().catch(() => { });
1720
1772
  }, 100);
1721
1773
  }
1774
+ return sessionContext.messages;
1722
1775
  }
1723
1776
  catch (error) {
1724
1777
  const errorMessage = error instanceof Error ? error.message : "compaction failed";
@@ -1731,6 +1784,7 @@ export class AgentSession {
1731
1784
  ? `Context overflow recovery failed: ${errorMessage}`
1732
1785
  : `Auto-compaction failed: ${errorMessage}`,
1733
1786
  });
1787
+ return undefined;
1734
1788
  }
1735
1789
  finally {
1736
1790
  this._autoCompactionAbortController = undefined;
@@ -1881,6 +1935,10 @@ export class AgentSession {
1881
1935
  if (this._extensionRunner) {
1882
1936
  this._bindExtensionCore(this._extensionRunner);
1883
1937
  this._applyExtensionBindings(this._extensionRunner);
1938
+ // P1 extension telemetry: every /command invocation writes one row to
1939
+ // ext_command_events. Returns a noop sink when no insforge credentials
1940
+ // are configured, so this is zero-cost for users not opted in.
1941
+ this._extensionRunner.setTelemetrySink(createExtensionTelemetrySink({ workspaceRoot: this._cwd }));
1884
1942
  }
1885
1943
  const registeredTools = this._extensionRunner?.getAllRegisteredTools() ?? [];
1886
1944
  const allCustomTools = [
@@ -1,8 +1,8 @@
1
1
  /**
2
- * [WHO]: Provides bindExtensionCore()
3
- * [FROM]: Depends on ExtensionRunner, model/session/resource abstractions, slash command metadata
2
+ * [WHO]: Provides bindExtensionCore() — wires AgentSession host methods to ExtensionRunner APIs, including LLM call telemetry (every ctx.completeSimple / completeSimpleWithUsage / completeJson emits one ext_llm_calls row)
3
+ * [FROM]: Depends on ExtensionRunner, model/session/resource abstractions, slash command metadata, core/telemetry (getExtCallerContext for caller attribution + LlmCallEventInput shape)
4
4
  * [TO]: Consumed by AgentSession when initializing extension runtime capabilities
5
- * [HERE]: core/runtime/extension-core-bindings.ts - adapts AgentSession host methods to ExtensionRunner APIs
5
+ * [HERE]: core/runtime/extension-core-bindings.ts - the LLM-call instrumentation point; reads AsyncLocalStorage context pushed by runner.invokeCommand (user-initiated=true) or runner.invokeHookHandler (user-initiated=false) to attribute each call. is_user_initiated=false rows grouped by extension_name + caller_context are the idle-thinking bug detector.
6
6
  */
7
7
  import type { Model } from "@pencil-agent/ai";
8
8
  import type { ThinkingLevel } from "@pencil-agent/agent-core";
@@ -1,4 +1,5 @@
1
1
  import { completeSimple } from "@pencil-agent/ai";
2
+ import { getExtCallerContext } from "../telemetry/index.js";
2
3
  import { buildExtensionSlashCommands } from "./slash-command-catalog.js";
3
4
  function getStructuredToolChoice(model, toolName) {
4
5
  switch (model.api) {
@@ -21,33 +22,73 @@ function isTextContent(block) {
21
22
  function isNamedToolCall(block, toolName) {
22
23
  return block.type === "toolCall" && "name" in block && block.name === toolName;
23
24
  }
24
- async function completeTextWithCurrentModel(host, systemPrompt, userMessage) {
25
- const result = await completeTextWithUsage(host, systemPrompt, userMessage);
25
+ /**
26
+ * Build the LlmCallEventInput row for one extension-initiated LLM call. Reads
27
+ * the AsyncLocalStorage caller context pushed by ExtensionRunner — that's how
28
+ * we know which extension and whether the path was user-initiated. Falls back
29
+ * to `extension_name="unknown"` + `is_user_initiated=false` when no context is
30
+ * set (defensive: that combination shows up in dashboards as "unattributed",
31
+ * a useful signal in itself).
32
+ */
33
+ function buildLlmCallEvent(args) {
34
+ const callerCtx = getExtCallerContext();
35
+ return {
36
+ extensionName: callerCtx?.extensionName ?? "unknown",
37
+ callerContext: callerCtx?.callerContext ?? "unknown",
38
+ isUserInitiated: callerCtx?.isUserInitiated ?? false,
39
+ modelId: args.model?.id ?? null,
40
+ tokensIn: args.usage?.input ?? null,
41
+ tokensOut: args.usage?.output ?? null,
42
+ costTotal: args.usage?.cost?.total ?? null,
43
+ durationMs: Math.round(performance.now() - args.startPerf),
44
+ ok: args.ok,
45
+ errorCode: args.errorCode,
46
+ startedAt: args.startedAt,
47
+ endedAt: new Date(),
48
+ sessionId: callerCtx?.sessionId ?? args.host.sessionManager.getSessionId(),
49
+ runId: callerCtx?.runId ?? null,
50
+ variant: callerCtx?.variant ?? null,
51
+ };
52
+ }
53
+ async function completeTextWithCurrentModel(runner, host, systemPrompt, userMessage) {
54
+ const result = await completeTextWithUsage(runner, host, systemPrompt, userMessage);
26
55
  return result?.text;
27
56
  }
28
- async function completeTextWithUsage(host, systemPrompt, userMessage) {
57
+ async function completeTextWithUsage(runner, host, systemPrompt, userMessage) {
29
58
  const model = host.model;
30
59
  if (!model)
31
60
  return undefined;
32
61
  const apiKey = await host.modelRegistry.getApiKey(model);
33
62
  if (!apiKey)
34
63
  return undefined;
64
+ const startedAt = new Date();
65
+ const startPerf = performance.now();
66
+ let ok = true;
67
+ let errorCode = null;
68
+ let usage;
69
+ let text;
35
70
  try {
36
71
  const response = await completeSimple(model, {
37
72
  systemPrompt,
38
73
  messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
39
74
  }, { maxTokens: 1500, temperature: 0.2, apiKey });
40
- const text = response.content
41
- ?.filter(isTextContent)
42
- .map((block) => block.text ?? "")
43
- .join("") ?? "";
44
- return { text, usage: response.usage };
75
+ usage = response.usage;
76
+ text =
77
+ response.content
78
+ ?.filter(isTextContent)
79
+ .map((block) => block.text ?? "")
80
+ .join("") ?? "";
45
81
  }
46
- catch {
47
- return undefined;
82
+ catch (err) {
83
+ ok = false;
84
+ errorCode = err instanceof Error ? err.constructor.name : "unknown";
48
85
  }
86
+ runner.writeLlmCallEvent(buildLlmCallEvent({ host, model, startedAt, startPerf, ok, errorCode, usage }));
87
+ if (!ok || text === undefined || usage === undefined)
88
+ return undefined;
89
+ return { text, usage };
49
90
  }
50
- async function completeJsonWithCurrentModel(host, systemPrompt, userMessage, schema, options) {
91
+ async function completeJsonWithCurrentModel(runner, host, systemPrompt, userMessage, schema, options) {
51
92
  const model = host.model;
52
93
  if (!model)
53
94
  return undefined;
@@ -55,6 +96,12 @@ async function completeJsonWithCurrentModel(host, systemPrompt, userMessage, sch
55
96
  if (!apiKey)
56
97
  return undefined;
57
98
  const toolName = options?.toolName || "submit_json";
99
+ const startedAt = new Date();
100
+ const startPerf = performance.now();
101
+ let ok = true;
102
+ let errorCode = null;
103
+ let usage;
104
+ let payloadString;
58
105
  try {
59
106
  const toolSchema = schema;
60
107
  const completionOptions = {
@@ -74,15 +121,21 @@ async function completeJsonWithCurrentModel(host, systemPrompt, userMessage, sch
74
121
  },
75
122
  ],
76
123
  }, completionOptions);
124
+ usage = response.usage;
77
125
  const toolCall = response.content?.find((block) => isNamedToolCall(block, toolName));
78
- if (!toolCall)
79
- return undefined;
80
- const payload = options?.resultKey ? toolCall.arguments?.[options.resultKey] : toolCall.arguments;
81
- return JSON.stringify(payload);
126
+ if (toolCall) {
127
+ const payload = options?.resultKey ? toolCall.arguments?.[options.resultKey] : toolCall.arguments;
128
+ payloadString = JSON.stringify(payload);
129
+ }
82
130
  }
83
- catch {
84
- return undefined;
131
+ catch (err) {
132
+ ok = false;
133
+ errorCode = err instanceof Error ? err.constructor.name : "unknown";
85
134
  }
135
+ runner.writeLlmCallEvent(buildLlmCallEvent({ host, model, startedAt, startPerf, ok, errorCode, usage }));
136
+ if (!ok)
137
+ return undefined;
138
+ return payloadString;
86
139
  }
87
140
  export function bindExtensionCore(runner, host) {
88
141
  runner.bindCore({
@@ -146,9 +199,9 @@ export function bindExtensionCore(runner, host) {
146
199
  setThinkingLevel: (level) => host.setThinkingLevel(level),
147
200
  }, {
148
201
  getModel: () => host.model,
149
- completeSimple: (systemPrompt, userMessage) => completeTextWithCurrentModel(host, systemPrompt, userMessage),
150
- completeSimpleWithUsage: (systemPrompt, userMessage) => completeTextWithUsage(host, systemPrompt, userMessage),
151
- completeJson: (systemPrompt, userMessage, schema, options) => completeJsonWithCurrentModel(host, systemPrompt, userMessage, schema, options),
202
+ completeSimple: (systemPrompt, userMessage) => completeTextWithCurrentModel(runner, host, systemPrompt, userMessage),
203
+ completeSimpleWithUsage: (systemPrompt, userMessage) => completeTextWithUsage(runner, host, systemPrompt, userMessage),
204
+ completeJson: (systemPrompt, userMessage, schema, options) => completeJsonWithCurrentModel(runner, host, systemPrompt, userMessage, schema, options),
152
205
  getSettings: () => host.settingsManager.getSettings(),
153
206
  isIdle: () => !host.isStreaming,
154
207
  abort: () => {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: RetryCoordinator class, auto-retry with exponential backoff
2
+ * [WHO]: RetryCoordinator class, auto-retry with exponential backoff, in-loop retry preparation
3
3
  * [FROM]: Depends on agent-core (AssistantMessage), ai (isContextOverflow), core/runtime/utils/sleep
4
4
  * [TO]: Consumed by core/runtime/agent-session.ts
5
5
  * [HERE]: core/runtime/retry-coordinator.ts - retry orchestration extracted from AgentSession
@@ -62,6 +62,15 @@ export declare class RetryCoordinator {
62
62
  * @returns true if retry was initiated, false if max retries exceeded or disabled
63
63
  */
64
64
  handleError(message: AssistantMessage): Promise<boolean>;
65
+ /**
66
+ * Handle a retryable error inside an active agent loop.
67
+ *
68
+ * Unlike handleError(), this does not mutate host messages and does not
69
+ * schedule agent.continue(); the caller remains responsible for replacing
70
+ * loop context and continuing in-place.
71
+ */
72
+ handleErrorInLoop(message: AssistantMessage): Promise<boolean>;
73
+ private _prepareRetry;
65
74
  /**
66
75
  * Called when an assistant response succeeds — resets retry counter
67
76
  * and resolves the pending retry promise.
@@ -44,6 +44,26 @@ export class RetryCoordinator {
44
44
  * @returns true if retry was initiated, false if max retries exceeded or disabled
45
45
  */
46
46
  async handleError(message) {
47
+ const shouldRetry = await this._prepareRetry(message);
48
+ if (!shouldRetry)
49
+ return false;
50
+ // Remove error message from agent state
51
+ this._host.removeLastAssistantMessage();
52
+ // Trigger retry via continue()
53
+ this._host.triggerContinue();
54
+ return true;
55
+ }
56
+ /**
57
+ * Handle a retryable error inside an active agent loop.
58
+ *
59
+ * Unlike handleError(), this does not mutate host messages and does not
60
+ * schedule agent.continue(); the caller remains responsible for replacing
61
+ * loop context and continuing in-place.
62
+ */
63
+ async handleErrorInLoop(message) {
64
+ return this._prepareRetry(message);
65
+ }
66
+ async _prepareRetry(message) {
47
67
  const settings = this._host.getRetrySettings();
48
68
  if (!settings.enabled)
49
69
  return false;
@@ -73,8 +93,6 @@ export class RetryCoordinator {
73
93
  delayMs,
74
94
  errorMessage: message.errorMessage || "Unknown error",
75
95
  });
76
- // Remove error message from agent state
77
- this._host.removeLastAssistantMessage();
78
96
  // Wait with exponential backoff (abortable)
79
97
  this._retryAbortController = new AbortController();
80
98
  try {
@@ -94,8 +112,6 @@ export class RetryCoordinator {
94
112
  return false;
95
113
  }
96
114
  this._retryAbortController = undefined;
97
- // Trigger retry via continue()
98
- this._host.triggerContinue();
99
115
  return true;
100
116
  }
101
117
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
- * [WHO]: createAgentSession(options) → AgentSession + load results
2
+ * [WHO]: createAgentSession(options) → AgentSession + load results, runtime loop setting wiring
3
3
  * [FROM]: Depends on agent-core, ai, core/config/*, core/tools/*, core/session/*, core/mcp-*, i18n/*
4
4
  * [TO]: Consumed by index.ts, main.ts, test/presence-opening.test.ts, extensions/defaults/team/index.ts
5
5
  * [HERE]: SDK factory; creates all services with DI, wires up extensions
@@ -250,6 +250,7 @@ export async function createAgentSession(options = {}) {
250
250
  agentLoopFramework: settingsManager.getAgentLoopFramework(),
251
251
  thinkingBudgets: settingsManager.getThinkingBudgets(),
252
252
  maxRetryDelayMs: settingsManager.getRetrySettings().maxDelayMs,
253
+ maxToolResultBatchSizeChars: settingsManager.getAgentLoopSettings().maxToolResultBatchSizeChars,
253
254
  getApiKey: async (provider) => {
254
255
  // Use the provider argument from the in-flight request;
255
256
  // agent.state.model may already be switched mid-turn.
@@ -7,11 +7,12 @@
7
7
  import type { ResourceLoader } from "../config/resource-loader.js";
8
8
  import type { ExtensionRunner } from "../extensions/index.js";
9
9
  import type { PromptTemplate } from "../prompt/prompt-templates.js";
10
- import { getLocalizedCommands, type SlashCommandInfo } from "../slash-commands.js";
10
+ import { getLocalizedCommands, type SlashCommandInfo, type SlashCommandCategory } from "../slash-commands.js";
11
11
  export interface SessionSlashCommandDescriptor {
12
12
  name: string;
13
13
  description?: string;
14
14
  source: "builtin" | SlashCommandInfo["source"];
15
+ category?: SlashCommandCategory;
15
16
  }
16
17
  type Translate = Parameters<typeof getLocalizedCommands>[0];
17
18
  export interface SlashCommandCatalogSource {
@@ -1,4 +1,4 @@
1
- import { BUILTIN_SLASH_COMMANDS, getLocalizedCommands, } from "../slash-commands.js";
1
+ import { BUILTIN_SLASH_COMMANDS, getLocalizedCommands, inferSlashCommandCategory, } from "../slash-commands.js";
2
2
  function normalizeLocation(source) {
3
3
  if (source === "user" || source === "project" || source === "path") {
4
4
  return source;
@@ -16,6 +16,7 @@ function getExtensionCommands(runner, reservedBuiltins) {
16
16
  name: command.name,
17
17
  description: command.description,
18
18
  path: extensionPath,
19
+ category: inferSlashCommandCategory(command.name, "extension"),
19
20
  })) ?? []);
20
21
  }
21
22
  export function buildSessionSlashCommands(source, translate) {
@@ -23,22 +24,26 @@ export function buildSessionSlashCommands(source, translate) {
23
24
  name: command.name,
24
25
  description: command.description,
25
26
  source: "builtin",
27
+ category: command.category,
26
28
  }));
27
29
  const reservedBuiltins = getReservedBuiltinNames();
28
30
  const extensionCommands = getExtensionCommands(source.extensionRunner, reservedBuiltins).map((command) => ({
29
31
  name: command.name,
30
32
  description: command.description,
31
33
  source: "extension",
34
+ category: command.category,
32
35
  }));
33
36
  const promptCommands = source.promptTemplates.map((template) => ({
34
37
  name: template.name,
35
38
  description: template.description,
36
39
  source: "prompt",
40
+ category: inferSlashCommandCategory(template.name, "prompt"),
37
41
  }));
38
42
  const skillCommands = source.resourceLoader.getSkills().skills.map((skill) => ({
39
43
  name: `skill:${skill.name}`,
40
44
  description: skill.description,
41
45
  source: "skill",
46
+ category: inferSlashCommandCategory(skill.name, "skill"),
42
47
  }));
43
48
  return [
44
49
  ...builtins,
@@ -53,12 +58,14 @@ export function buildExtensionSlashCommands(source) {
53
58
  name: command.name,
54
59
  description: command.description,
55
60
  source: "extension",
61
+ category: command.category,
56
62
  path: command.path,
57
63
  }));
58
64
  const templates = source.promptTemplates.map((template) => ({
59
65
  name: template.name,
60
66
  description: template.description,
61
67
  source: "prompt",
68
+ category: inferSlashCommandCategory(template.name, "prompt"),
62
69
  location: normalizeLocation(template.source),
63
70
  path: template.filePath,
64
71
  }));
@@ -68,6 +75,7 @@ export function buildExtensionSlashCommands(source) {
68
75
  name: `skill:${skill.name}`,
69
76
  description: skill.description,
70
77
  source: "skill",
78
+ category: inferSlashCommandCategory(skill.name, "skill"),
71
79
  location: normalizeLocation(skill.source),
72
80
  path: skill.filePath,
73
81
  }));
@@ -1,25 +1,36 @@
1
1
  /**
2
- * [WHO]: SlashCommandInfo, BuiltinSlashCommand, slashCommand definitions, getLocalizedCommands()
2
+ * [WHO]: SlashCommandInfo, BuiltinSlashCommand, slashCommand definitions, category helpers, getLocalizedCommands()
3
3
  * [FROM]: No external dependencies
4
4
  * [TO]: Consumed by modes/interactive/interactive-mode.ts, modes/acp/acp-mode.ts
5
5
  * [HERE]: core/slash-commands.ts - slash command types and registry
6
6
  */
7
7
  export type SlashCommandSource = "extension" | "prompt" | "skill";
8
8
  export type SlashCommandLocation = "user" | "project" | "path";
9
+ export type SlashCommandCategory = "core" | "model" | "memory" | "session" | "workflow" | "agents" | "tools" | "admin";
10
+ export type SlashCommandImplementation = "core" | "extension";
9
11
  export interface SlashCommandInfo {
10
12
  name: string;
11
13
  description?: string;
12
14
  source: SlashCommandSource;
15
+ category?: SlashCommandCategory;
13
16
  location?: SlashCommandLocation;
14
17
  path?: string;
15
18
  }
16
19
  export interface BuiltinSlashCommand {
17
20
  name: string;
18
21
  descriptionKey: string;
22
+ category: SlashCommandCategory;
23
+ implementation?: SlashCommandImplementation;
19
24
  }
20
25
  export declare const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand>;
26
+ export declare function getExtensionBackedBuiltinCommandNames(): Set<string>;
27
+ export declare function inferSlashCommandCategory(name: string, source?: SlashCommandSource): SlashCommandCategory;
28
+ export declare function getSlashCommandCategoryLabel(category: SlashCommandCategory, t: (key: string) => string): string;
29
+ export declare function formatSlashCommandDescription(description: string | undefined, category: SlashCommandCategory | undefined, t: (key: string) => string): string | undefined;
21
30
  export interface LocalizedSlashCommand {
22
31
  name: string;
23
32
  description: string;
33
+ category: SlashCommandCategory;
34
+ categoryLabel: string;
24
35
  }
25
36
  export declare function getLocalizedCommands(t: (key: string) => string): LocalizedSlashCommand[];