@oh-my-pi/pi-coding-agent 15.0.2 → 15.1.1

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 (138) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/examples/custom-tools/README.md +11 -7
  3. package/examples/custom-tools/hello/index.ts +2 -2
  4. package/examples/extensions/README.md +19 -8
  5. package/examples/extensions/api-demo.ts +15 -19
  6. package/examples/extensions/hello.ts +5 -6
  7. package/examples/extensions/plan-mode.ts +1 -1
  8. package/examples/extensions/reload-runtime.ts +4 -3
  9. package/examples/extensions/with-deps/index.ts +4 -3
  10. package/examples/sdk/06-extensions.ts +4 -2
  11. package/package.json +7 -17
  12. package/src/autoresearch/tools/init-experiment.ts +38 -41
  13. package/src/autoresearch/tools/log-experiment.ts +32 -41
  14. package/src/autoresearch/tools/run-experiment.ts +3 -3
  15. package/src/autoresearch/tools/update-notes.ts +11 -11
  16. package/src/commit/agentic/tools/analyze-file.ts +4 -4
  17. package/src/commit/agentic/tools/git-file-diff.ts +4 -4
  18. package/src/commit/agentic/tools/git-hunk.ts +5 -5
  19. package/src/commit/agentic/tools/git-overview.ts +4 -4
  20. package/src/commit/agentic/tools/propose-changelog.ts +13 -13
  21. package/src/commit/agentic/tools/propose-commit.ts +6 -6
  22. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  23. package/src/commit/agentic/tools/schemas.ts +28 -28
  24. package/src/commit/agentic/tools/split-commit.ts +22 -21
  25. package/src/commit/analysis/summary.ts +4 -4
  26. package/src/commit/changelog/generate.ts +7 -11
  27. package/src/commit/shared-llm.ts +22 -34
  28. package/src/config/config-file.ts +35 -13
  29. package/src/config/model-registry.ts +9 -190
  30. package/src/config/models-config-schema.ts +166 -0
  31. package/src/config/settings-schema.ts +18 -0
  32. package/src/edit/index.ts +2 -2
  33. package/src/edit/modes/apply-patch.ts +7 -6
  34. package/src/edit/modes/patch.ts +18 -25
  35. package/src/edit/modes/replace.ts +18 -20
  36. package/src/eval/js/shared/rewrite-imports.ts +131 -10
  37. package/src/eval/py/executor.ts +233 -623
  38. package/src/eval/py/kernel.ts +27 -2
  39. package/src/exa/factory.ts +5 -4
  40. package/src/exa/mcp-client.ts +1 -1
  41. package/src/exa/researcher.ts +9 -20
  42. package/src/exa/search.ts +26 -52
  43. package/src/exa/types.ts +1 -1
  44. package/src/exa/websets.ts +54 -53
  45. package/src/exec/bash-executor.ts +2 -1
  46. package/src/extensibility/custom-commands/loader.ts +5 -3
  47. package/src/extensibility/custom-commands/types.ts +4 -2
  48. package/src/extensibility/custom-tools/loader.ts +5 -3
  49. package/src/extensibility/custom-tools/types.ts +7 -6
  50. package/src/extensibility/custom-tools/wrapper.ts +1 -1
  51. package/src/extensibility/extensions/loader.ts +7 -3
  52. package/src/extensibility/extensions/types.ts +9 -5
  53. package/src/extensibility/extensions/wrapper.ts +1 -2
  54. package/src/extensibility/hooks/loader.ts +3 -1
  55. package/src/extensibility/hooks/tool-wrapper.ts +1 -1
  56. package/src/extensibility/hooks/types.ts +4 -2
  57. package/src/extensibility/plugins/legacy-pi-compat.ts +30 -0
  58. package/src/extensibility/shared-events.ts +1 -1
  59. package/src/extensibility/typebox.ts +391 -0
  60. package/src/goals/tools/goal-tool.ts +6 -12
  61. package/src/hashline/types.ts +4 -4
  62. package/src/hindsight/state.ts +2 -2
  63. package/src/index.ts +0 -2
  64. package/src/internal-urls/docs-index.generated.ts +7 -7
  65. package/src/lsp/types.ts +30 -38
  66. package/src/mcp/manager.ts +1 -1
  67. package/src/mcp/tool-bridge.ts +1 -1
  68. package/src/modes/components/session-observer-overlay.ts +12 -1
  69. package/src/modes/components/status-line/segments.ts +2 -1
  70. package/src/modes/controllers/command-controller.ts +27 -2
  71. package/src/modes/controllers/event-controller.ts +3 -4
  72. package/src/modes/interactive-mode.ts +1 -1
  73. package/src/modes/rpc/host-tools.ts +1 -1
  74. package/src/modes/rpc/rpc-client.ts +1 -1
  75. package/src/modes/rpc/rpc-types.ts +1 -1
  76. package/src/modes/theme/theme.ts +111 -117
  77. package/src/modes/types.ts +1 -1
  78. package/src/modes/utils/context-usage.ts +2 -2
  79. package/src/sdk.ts +31 -8
  80. package/src/session/agent-session.ts +74 -104
  81. package/src/session/messages.ts +16 -51
  82. package/src/session/session-manager.ts +22 -2
  83. package/src/session/streaming-output.ts +16 -6
  84. package/src/task/executor.ts +208 -86
  85. package/src/task/index.ts +15 -11
  86. package/src/task/render.ts +32 -5
  87. package/src/task/types.ts +54 -39
  88. package/src/tools/ask.ts +12 -12
  89. package/src/tools/ast-edit.ts +11 -15
  90. package/src/tools/ast-grep.ts +9 -10
  91. package/src/tools/bash.ts +9 -23
  92. package/src/tools/browser.ts +39 -53
  93. package/src/tools/calculator.ts +12 -11
  94. package/src/tools/checkpoint.ts +7 -7
  95. package/src/tools/debug.ts +40 -43
  96. package/src/tools/eval.ts +6 -8
  97. package/src/tools/find.ts +10 -13
  98. package/src/tools/gh.ts +71 -128
  99. package/src/tools/hindsight-recall.ts +4 -6
  100. package/src/tools/hindsight-reflect.ts +5 -5
  101. package/src/tools/hindsight-retain.ts +15 -17
  102. package/src/tools/image-gen.ts +32 -82
  103. package/src/tools/index.ts +4 -1
  104. package/src/tools/inspect-image.ts +8 -9
  105. package/src/tools/irc.ts +15 -27
  106. package/src/tools/job.ts +14 -21
  107. package/src/tools/read.ts +7 -8
  108. package/src/tools/recipe/index.ts +7 -9
  109. package/src/tools/render-mermaid.ts +12 -12
  110. package/src/tools/report-tool-issue.ts +4 -4
  111. package/src/tools/resolve.ts +11 -11
  112. package/src/tools/review.ts +14 -26
  113. package/src/tools/search-tool-bm25.ts +7 -9
  114. package/src/tools/search.ts +19 -22
  115. package/src/tools/ssh.ts +7 -7
  116. package/src/tools/todo-write.ts +26 -34
  117. package/src/tools/vim.ts +10 -26
  118. package/src/tools/write.ts +5 -5
  119. package/src/tools/yield.ts +100 -54
  120. package/src/web/search/index.ts +9 -24
  121. package/src/prompts/compaction/branch-summary-context.md +0 -5
  122. package/src/prompts/compaction/branch-summary-preamble.md +0 -2
  123. package/src/prompts/compaction/branch-summary.md +0 -30
  124. package/src/prompts/compaction/compaction-short-summary.md +0 -9
  125. package/src/prompts/compaction/compaction-summary-context.md +0 -5
  126. package/src/prompts/compaction/compaction-summary.md +0 -38
  127. package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
  128. package/src/prompts/compaction/compaction-update-summary.md +0 -45
  129. package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
  130. package/src/prompts/system/file-operations.md +0 -10
  131. package/src/prompts/system/handoff-document.md +0 -49
  132. package/src/prompts/system/summarization-system.md +0 -3
  133. package/src/session/compaction/branch-summarization.ts +0 -324
  134. package/src/session/compaction/compaction.ts +0 -1420
  135. package/src/session/compaction/errors.ts +0 -31
  136. package/src/session/compaction/index.ts +0 -8
  137. package/src/session/compaction/pruning.ts +0 -91
  138. package/src/session/compaction/utils.ts +0 -184
package/src/lsp/types.ts CHANGED
@@ -1,49 +1,41 @@
1
- import { StringEnum } from "@oh-my-pi/pi-ai";
2
1
  import type { ptree } from "@oh-my-pi/pi-utils";
3
- import { type Static, Type } from "@sinclair/typebox";
2
+ import * as z from "zod/v4";
4
3
 
5
4
  // =============================================================================
6
5
  // Tool Schema
7
6
  // =============================================================================
8
7
 
9
- export const lspSchema = Type.Object({
10
- action: StringEnum(
11
- [
12
- "diagnostics",
13
- "definition",
14
- "references",
15
- "hover",
16
- "symbols",
17
- "rename",
18
- "rename_file",
19
- "code_actions",
20
- "type_definition",
21
- "implementation",
22
- "status",
23
- "reload",
24
- "capabilities",
25
- "request",
26
- ],
27
- { description: "LSP operation" },
28
- ),
29
- file: Type.Optional(Type.String({ description: "File path or source path for rename_file" })),
30
- line: Type.Optional(Type.Number({ description: "Line number (1-indexed)" })),
31
- symbol: Type.Optional(Type.String({ description: "Symbol/substring to locate on the line" })),
32
- query: Type.Optional(
33
- Type.String({ description: "Search query, code-action selector, or LSP method name for action=request" }),
34
- ),
35
- new_name: Type.Optional(Type.String({ description: "New name for rename, or destination path for rename_file" })),
36
- apply: Type.Optional(Type.Boolean({ description: "Apply edits (default: true for rename/rename_file)" })),
37
- timeout: Type.Optional(Type.Number({ description: "Request timeout in seconds" })),
38
- payload: Type.Optional(
39
- Type.String({
40
- description:
41
- "JSON-encoded params for action=request. When omitted, params are auto-built from file/line/symbol.",
42
- }),
43
- ),
8
+ export const lspSchema = z.object({
9
+ action: z.enum([
10
+ "diagnostics",
11
+ "definition",
12
+ "references",
13
+ "hover",
14
+ "symbols",
15
+ "rename",
16
+ "rename_file",
17
+ "code_actions",
18
+ "type_definition",
19
+ "implementation",
20
+ "status",
21
+ "reload",
22
+ "capabilities",
23
+ "request",
24
+ ]),
25
+ file: z.string().describe("File path or source path for rename_file").optional(),
26
+ line: z.number().describe("Line number (1-indexed)").optional(),
27
+ symbol: z.string().describe("Symbol/substring to locate on the line").optional(),
28
+ query: z.string().describe("Search query, code-action selector, or LSP method name for action=request").optional(),
29
+ new_name: z.string().describe("New name for rename, or destination path for rename_file").optional(),
30
+ apply: z.boolean().describe("Apply edits (default: true for rename/rename_file)").optional(),
31
+ timeout: z.number().describe("Request timeout in seconds").optional(),
32
+ payload: z
33
+ .string()
34
+ .describe("JSON-encoded params for action=request. When omitted, params are auto-built from file/line/symbol.")
35
+ .optional(),
44
36
  });
45
37
 
46
- export type LspParams = Static<typeof lspSchema>;
38
+ export type LspParams = z.infer<typeof lspSchema>;
47
39
 
48
40
  export interface LspToolDetails {
49
41
  serverName?: string;
@@ -6,8 +6,8 @@
6
6
  */
7
7
  import * as path from "node:path";
8
8
  import * as url from "node:url";
9
+ import type { TSchema } from "@oh-my-pi/pi-ai";
9
10
  import { logger } from "@oh-my-pi/pi-utils";
10
- import type { TSchema } from "@sinclair/typebox";
11
11
  import type { SourceMeta } from "../capability/types";
12
12
  import { resolveConfigValue } from "../config/resolve-config-value";
13
13
  import type { CustomTool } from "../extensibility/custom-tools/types";
@@ -4,9 +4,9 @@
4
4
  * Converts MCP tool definitions to CustomTool format for the agent.
5
5
  */
6
6
  import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
+ import type { TSchema } from "@oh-my-pi/pi-ai";
7
8
  import { sanitizeSchemaForMCP } from "@oh-my-pi/pi-ai/utils/schema";
8
9
  import { untilAborted } from "@oh-my-pi/pi-utils";
9
- import type { TSchema } from "@sinclair/typebox";
10
10
  import type { SourceMeta } from "../capability/types";
11
11
  import type {
12
12
  CustomTool,
@@ -266,7 +266,18 @@ export class SessionObserverOverlayComponent extends Container {
266
266
  if (!progress) return "";
267
267
  const stats: string[] = [];
268
268
  if (progress.toolCount > 0) stats.push(`${formatNumber(progress.toolCount)} tools`);
269
- if (progress.tokens > 0) stats.push(`${formatNumber(progress.tokens)} tokens`);
269
+ // Current per-turn context — what the user reads as "how full is the context".
270
+ // Falls back to cumulative billing volume (Σ-prefixed) when context size is unknown.
271
+ if (progress.contextTokens && progress.contextTokens > 0) {
272
+ const ctx =
273
+ progress.contextWindow && progress.contextWindow > 0
274
+ ? `${formatNumber(progress.contextTokens)}/${formatNumber(progress.contextWindow)} ctx`
275
+ : `${formatNumber(progress.contextTokens)} ctx`;
276
+ stats.push(ctx);
277
+ if (progress.tokens > 0) stats.push(`Σ${formatNumber(progress.tokens)}`);
278
+ } else if (progress.tokens > 0) {
279
+ stats.push(`Σ${formatNumber(progress.tokens)}`);
280
+ }
270
281
  if (progress.durationMs > 0) stats.push(formatDuration(progress.durationMs));
271
282
  const parts: string[] = [];
272
283
  if (stats.length > 0) parts.push(theme.fg("dim", stats.join(theme.sep.dot)));
@@ -199,7 +199,8 @@ const pathSegment: StatusLineSegment = {
199
199
  pwd = `${ellipsis}${pwd.slice(-sliceLen)}`;
200
200
  }
201
201
 
202
- const icon = scratch ? theme.icon.scratchFolder : theme.icon.folder;
202
+ const showScratchIcon = scratch && opts.stripWorkPrefix !== false;
203
+ const icon = showScratchIcon ? theme.icon.scratchFolder : theme.icon.folder;
203
204
  const content = withIcon(icon, pwd);
204
205
  return { content: theme.fg("statusLinePath", content), visible: true };
205
206
  },
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
+ import { CompactionCancelledError, type CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
4
5
  import {
5
6
  getEnvApiKey,
6
7
  getProviderDetails,
@@ -37,7 +38,6 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
37
38
  import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
38
39
  import type { AsyncJobSnapshotItem } from "../../session/agent-session";
39
40
  import type { AuthStorage } from "../../session/auth-storage";
40
- import { CompactionCancelledError, type CompactionOutcome } from "../../session/compaction";
41
41
  import type { NewSessionOptions } from "../../session/session-manager";
42
42
  import { outputMeta } from "../../tools/output-meta";
43
43
  import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
@@ -1175,8 +1175,29 @@ export class CommandController {
1175
1175
  return;
1176
1176
  }
1177
1177
 
1178
+ if (this.ctx.loadingAnimation) {
1179
+ this.ctx.loadingAnimation.stop();
1180
+ this.ctx.loadingAnimation = undefined;
1181
+ }
1182
+ this.ctx.statusContainer.clear();
1183
+
1184
+ const originalOnEscape = this.ctx.editor.onEscape;
1185
+ this.ctx.editor.onEscape = () => {
1186
+ this.ctx.session.abortHandoff();
1187
+ };
1188
+
1189
+ const handoffLoader = new Loader(
1190
+ this.ctx.ui,
1191
+ spinner => theme.fg("accent", spinner),
1192
+ text => theme.fg("muted", text),
1193
+ "Generating handoff… (esc to cancel)",
1194
+ getSymbolTheme().spinnerFrames,
1195
+ );
1196
+ this.ctx.statusContainer.addChild(handoffLoader);
1197
+ this.ctx.ui.requestRender();
1198
+
1178
1199
  try {
1179
- // The agent will visibly generate the handoff document in chat
1200
+ // Handoff generation runs as a oneshot request; the new session is shown after it completes.
1180
1201
  const result = await this.ctx.session.handoff(customInstructions);
1181
1202
 
1182
1203
  if (!result) {
@@ -1206,6 +1227,10 @@ export class CommandController {
1206
1227
  } else {
1207
1228
  this.ctx.showError(`Handoff failed: ${message}`);
1208
1229
  }
1230
+ } finally {
1231
+ handoffLoader.stop();
1232
+ this.ctx.statusContainer.clear();
1233
+ this.ctx.editor.onEscape = originalOnEscape;
1209
1234
  }
1210
1235
  this.ctx.ui.requestRender();
1211
1236
  }
@@ -1,4 +1,5 @@
1
1
  import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
2
+ import { calculatePromptTokens } from "@oh-my-pi/pi-agent-core/compaction/compaction";
2
3
  import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
3
4
  import { type Component, Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
4
5
  import { settings } from "../../config/settings";
@@ -15,7 +16,6 @@ import { getSymbolTheme, theme } from "../../modes/theme/theme";
15
16
  import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
16
17
  import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
17
18
  import type { AgentSessionEvent } from "../../session/agent-session";
18
- import { calculatePromptTokens } from "../../session/compaction/compaction";
19
19
  import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
20
20
  import type { ResolveToolDetails } from "../../tools/resolve";
21
21
 
@@ -719,9 +719,8 @@ export class EventController {
719
719
 
720
720
  #scheduleIdleCompaction(): void {
721
721
  this.#cancelIdleCompaction();
722
- // Don't schedule while compaction/handoff is already running the agent_end from a
723
- // handoff agent turn still has the old session's bloated token counts, and scheduling
724
- // here would fire after the session resets, trying to handoff an empty session.
722
+ // Don't schedule idle work while context maintenance is already running; the
723
+ // maintenance flow may reset the session before this timer fires.
725
724
  if (this.ctx.session.isCompacting) return;
726
725
 
727
726
  const idleSettings = settings.getGroup("compaction");
@@ -5,6 +5,7 @@
5
5
  import * as fs from "node:fs/promises";
6
6
  import * as path from "node:path";
7
7
  import { type Agent, type AgentMessage, type AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
+ import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
8
9
  import {
9
10
  type AssistantMessage,
10
11
  type ImageContent,
@@ -46,7 +47,6 @@ import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compa
46
47
  type: "text",
47
48
  };
48
49
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
49
- import type { CompactionOutcome } from "../session/compaction";
50
50
  import { HistoryStorage } from "../session/history-storage";
51
51
  import type { SessionContext, SessionManager } from "../session/session-manager";
52
52
  import { getRecentSessions } from "../session/session-manager";
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import type { Static, TSchema } from "@oh-my-pi/pi-ai";
2
3
  import { Snowflake } from "@oh-my-pi/pi-utils";
3
- import type { Static, TSchema } from "@sinclair/typebox";
4
4
  import { applyToolProxy } from "../../extensibility/tool-proxy";
5
5
  import type { Theme } from "../../modes/theme/theme";
6
6
  import type {
@@ -4,11 +4,11 @@
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
6
  import type { AgentEvent, AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
+ import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
7
8
  import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
8
9
  import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
9
10
  import type { BashResult } from "../../exec/bash-executor";
10
11
  import type { SessionStats } from "../../session/agent-session";
11
- import type { CompactionResult } from "../../session/compaction";
12
12
  import type {
13
13
  RpcCommand,
14
14
  RpcExtensionUIRequest,
@@ -5,11 +5,11 @@
5
5
  * Responses and events are emitted as JSON lines on stdout.
6
6
  */
7
7
  import type { AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
+ import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
8
9
  import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
9
10
  import type { BashResult } from "../../exec/bash-executor";
10
11
  import type { ContextUsage } from "../../extensibility/extensions/types";
11
12
  import type { SessionStats } from "../../session/agent-session";
12
- import type { CompactionResult } from "../../session/compaction";
13
13
  import type { TodoPhase } from "../../tools/todo-write";
14
14
 
15
15
  // ============================================================================
@@ -11,9 +11,8 @@ import {
11
11
  } from "@oh-my-pi/pi-natives";
12
12
  import type { EditorTheme, MarkdownTheme, SelectListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
13
13
  import { adjustHsv, getCustomThemesDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
14
- import { type Static, Type } from "@sinclair/typebox";
15
- import { TypeCompiler } from "@sinclair/typebox/compiler";
16
14
  import chalk from "chalk";
15
+ import * as z from "zod/v4";
17
16
  // Embed theme JSON files at build time
18
17
  import darkThemeJson from "./dark.json" with { type: "json" };
19
18
  import { defaultThemes } from "./defaults";
@@ -807,118 +806,111 @@ const SPINNER_FRAMES: Record<SymbolPreset, Record<SpinnerType, string[]>> = {
807
806
  // Types & Schema
808
807
  // ============================================================================
809
808
 
810
- const ColorValueSchema = Type.Union([
811
- Type.String(), // hex "#ff0000", var ref "primary", or empty ""
812
- Type.Integer({ minimum: 0, maximum: 255 }), // 256-color index
809
+ const colorValueSchema = z.union([
810
+ z.string(), // hex "#ff0000", var ref "primary", or empty ""
811
+ z.number().int().min(0).max(255), // 256-color index
813
812
  ]);
814
813
 
815
- type ColorValue = Static<typeof ColorValueSchema>;
816
-
817
- // Use Type.Union here (not StringEnum) because TypeCompiler doesn't support Type.Unsafe
818
- const SymbolPresetSchema = Type.Union([Type.Literal("unicode"), Type.Literal("nerd"), Type.Literal("ascii")]);
819
-
820
- const SymbolsSchema = Type.Optional(
821
- Type.Object({
822
- preset: Type.Optional(SymbolPresetSchema),
823
- overrides: Type.Optional(Type.Record(Type.String(), Type.String())),
824
- }),
814
+ type ColorValue = z.infer<typeof colorValueSchema>;
815
+
816
+ const THEME_COLOR_KEYS = [
817
+ "accent",
818
+ "border",
819
+ "borderAccent",
820
+ "borderMuted",
821
+ "success",
822
+ "error",
823
+ "warning",
824
+ "muted",
825
+ "dim",
826
+ "text",
827
+ "thinkingText",
828
+ "selectedBg",
829
+ "userMessageBg",
830
+ "userMessageText",
831
+ "customMessageBg",
832
+ "customMessageText",
833
+ "customMessageLabel",
834
+ "toolPendingBg",
835
+ "toolSuccessBg",
836
+ "toolErrorBg",
837
+ "toolTitle",
838
+ "toolOutput",
839
+ "mdHeading",
840
+ "mdLink",
841
+ "mdLinkUrl",
842
+ "mdCode",
843
+ "mdCodeBlock",
844
+ "mdCodeBlockBorder",
845
+ "mdQuote",
846
+ "mdQuoteBorder",
847
+ "mdHr",
848
+ "mdListBullet",
849
+ "toolDiffAdded",
850
+ "toolDiffRemoved",
851
+ "toolDiffContext",
852
+ "syntaxComment",
853
+ "syntaxKeyword",
854
+ "syntaxFunction",
855
+ "syntaxVariable",
856
+ "syntaxString",
857
+ "syntaxNumber",
858
+ "syntaxType",
859
+ "syntaxOperator",
860
+ "syntaxPunctuation",
861
+ "thinkingOff",
862
+ "thinkingMinimal",
863
+ "thinkingLow",
864
+ "thinkingMedium",
865
+ "thinkingHigh",
866
+ "thinkingXhigh",
867
+ "bashMode",
868
+ "pythonMode",
869
+ "statusLineBg",
870
+ "statusLineSep",
871
+ "statusLineModel",
872
+ "statusLinePath",
873
+ "statusLineGitClean",
874
+ "statusLineGitDirty",
875
+ "statusLineContext",
876
+ "statusLineSpend",
877
+ "statusLineStaged",
878
+ "statusLineDirty",
879
+ "statusLineUntracked",
880
+ "statusLineOutput",
881
+ "statusLineCost",
882
+ "statusLineSubagents",
883
+ ] as const;
884
+
885
+ const themeColorsSchema = z.object(
886
+ Object.fromEntries(THEME_COLOR_KEYS.map(key => [key, colorValueSchema])) as unknown as {
887
+ [K in (typeof THEME_COLOR_KEYS)[number]]: typeof colorValueSchema;
888
+ },
825
889
  );
826
890
 
827
- const ThemeJsonSchema = Type.Object({
828
- $schema: Type.Optional(Type.String()),
829
- name: Type.String(),
830
- vars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)),
831
- colors: Type.Object({
832
- // Core UI (10 colors)
833
- accent: ColorValueSchema,
834
- border: ColorValueSchema,
835
- borderAccent: ColorValueSchema,
836
- borderMuted: ColorValueSchema,
837
- success: ColorValueSchema,
838
- error: ColorValueSchema,
839
- warning: ColorValueSchema,
840
- muted: ColorValueSchema,
841
- dim: ColorValueSchema,
842
- text: ColorValueSchema,
843
- thinkingText: ColorValueSchema,
844
- // Backgrounds & Content Text (11 colors)
845
- selectedBg: ColorValueSchema,
846
- userMessageBg: ColorValueSchema,
847
- userMessageText: ColorValueSchema,
848
- customMessageBg: ColorValueSchema,
849
- customMessageText: ColorValueSchema,
850
- customMessageLabel: ColorValueSchema,
851
- toolPendingBg: ColorValueSchema,
852
- toolSuccessBg: ColorValueSchema,
853
- toolErrorBg: ColorValueSchema,
854
- toolTitle: ColorValueSchema,
855
- toolOutput: ColorValueSchema,
856
- // Markdown (10 colors)
857
- mdHeading: ColorValueSchema,
858
- mdLink: ColorValueSchema,
859
- mdLinkUrl: ColorValueSchema,
860
- mdCode: ColorValueSchema,
861
- mdCodeBlock: ColorValueSchema,
862
- mdCodeBlockBorder: ColorValueSchema,
863
- mdQuote: ColorValueSchema,
864
- mdQuoteBorder: ColorValueSchema,
865
- mdHr: ColorValueSchema,
866
- mdListBullet: ColorValueSchema,
867
- // Tool Diffs (3 colors)
868
- toolDiffAdded: ColorValueSchema,
869
- toolDiffRemoved: ColorValueSchema,
870
- toolDiffContext: ColorValueSchema,
871
- // Syntax Highlighting (9 colors)
872
- syntaxComment: ColorValueSchema,
873
- syntaxKeyword: ColorValueSchema,
874
- syntaxFunction: ColorValueSchema,
875
- syntaxVariable: ColorValueSchema,
876
- syntaxString: ColorValueSchema,
877
- syntaxNumber: ColorValueSchema,
878
- syntaxType: ColorValueSchema,
879
- syntaxOperator: ColorValueSchema,
880
- syntaxPunctuation: ColorValueSchema,
881
- // Thinking Level Borders (6 colors)
882
- thinkingOff: ColorValueSchema,
883
- thinkingMinimal: ColorValueSchema,
884
- thinkingLow: ColorValueSchema,
885
- thinkingMedium: ColorValueSchema,
886
- thinkingHigh: ColorValueSchema,
887
- thinkingXhigh: ColorValueSchema,
888
- // Bash Mode (1 color)
889
- bashMode: ColorValueSchema,
890
- // Python Mode (1 color)
891
- pythonMode: ColorValueSchema,
892
- // Footer Status Line
893
- statusLineBg: ColorValueSchema,
894
- statusLineSep: ColorValueSchema,
895
- statusLineModel: ColorValueSchema,
896
- statusLinePath: ColorValueSchema,
897
- statusLineGitClean: ColorValueSchema,
898
- statusLineGitDirty: ColorValueSchema,
899
- statusLineContext: ColorValueSchema,
900
- statusLineSpend: ColorValueSchema,
901
- statusLineStaged: ColorValueSchema,
902
- statusLineDirty: ColorValueSchema,
903
- statusLineUntracked: ColorValueSchema,
904
- statusLineOutput: ColorValueSchema,
905
- statusLineCost: ColorValueSchema,
906
- statusLineSubagents: ColorValueSchema,
907
- }),
908
- export: Type.Optional(
909
- Type.Object({
910
- pageBg: Type.Optional(ColorValueSchema),
911
- cardBg: Type.Optional(ColorValueSchema),
912
- infoBg: Type.Optional(ColorValueSchema),
913
- }),
914
- ),
915
- symbols: SymbolsSchema,
891
+ const symbolPresetSchema = z.enum(["unicode", "nerd", "ascii"]);
892
+
893
+ const themeJsonSchema = z.object({
894
+ $schema: z.string().optional(),
895
+ name: z.string(),
896
+ vars: z.record(z.string(), colorValueSchema).optional(),
897
+ colors: themeColorsSchema,
898
+ export: z
899
+ .object({
900
+ pageBg: colorValueSchema.optional(),
901
+ cardBg: colorValueSchema.optional(),
902
+ infoBg: colorValueSchema.optional(),
903
+ })
904
+ .optional(),
905
+ symbols: z
906
+ .object({
907
+ preset: symbolPresetSchema.optional(),
908
+ overrides: z.record(z.string(), z.string()).optional(),
909
+ })
910
+ .optional(),
916
911
  });
917
912
 
918
- type ThemeJson = Static<typeof ThemeJsonSchema>;
919
-
920
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TypeBox CJS/ESM type mismatch
921
- const validateThemeJson = TypeCompiler.Compile(ThemeJsonSchema as any);
913
+ type ThemeJson = z.infer<typeof themeJsonSchema>;
922
914
 
923
915
  export type ThemeColor =
924
916
  | "accent"
@@ -1638,18 +1630,20 @@ async function loadThemeJson(name: string): Promise<ThemeJson> {
1638
1630
  } catch (error) {
1639
1631
  throw new Error(`Failed to parse theme ${name}: ${error}`);
1640
1632
  }
1641
- if (!validateThemeJson.Check(json)) {
1642
- const errors = Array.from(validateThemeJson.Errors(json));
1633
+ const parsed = themeJsonSchema.safeParse(json);
1634
+ if (!parsed.success) {
1643
1635
  const missingColors: string[] = [];
1644
1636
  const otherErrors: string[] = [];
1645
1637
 
1646
- for (const e of errors) {
1647
- // Check for missing required color properties
1648
- const match = e.path.match(/^\/colors\/(\w+)$/);
1649
- if (match && e.message.includes("Required")) {
1650
- missingColors.push(match[1]);
1638
+ for (const issue of parsed.error.issues) {
1639
+ const parts = issue.path;
1640
+ const colorKey = parts.length === 2 && parts[0] === "colors" && typeof parts[1] === "string" ? parts[1] : null;
1641
+
1642
+ if (colorKey && issue.code === "invalid_type" && (issue as { received?: unknown }).received === undefined) {
1643
+ missingColors.push(colorKey);
1651
1644
  } else {
1652
- otherErrors.push(` - ${e.path}: ${e.message}`);
1645
+ const pathStr = parts.length === 0 ? "/" : `/${parts.map(String).join("/")}`;
1646
+ otherErrors.push(` - ${pathStr}: ${issue.message}`);
1653
1647
  }
1654
1648
  }
1655
1649
 
@@ -1666,7 +1660,7 @@ async function loadThemeJson(name: string): Promise<ThemeJson> {
1666
1660
 
1667
1661
  throw new Error(errorMessage);
1668
1662
  }
1669
- return json as ThemeJson;
1663
+ return parsed.data;
1670
1664
  }
1671
1665
 
1672
1666
  interface CreateThemeOptions {
@@ -1,4 +1,5 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
2
3
  import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
3
4
  import type { Component, Container, EditorTheme, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
4
5
  import type { KeybindingsManager } from "../config/keybindings";
@@ -13,7 +14,6 @@ import type { CompactOptions } from "../extensibility/extensions/types";
13
14
  import type { MCPManager } from "../mcp";
14
15
  import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
15
16
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
16
- import type { CompactionOutcome } from "../session/compaction";
17
17
  import type { HistoryStorage } from "../session/history-storage";
18
18
  import type { SessionContext, SessionManager } from "../session/session-manager";
19
19
  import type { LspStartupServerInfo } from "../tools";
@@ -1,10 +1,10 @@
1
+ import type { CompactionSettings } from "@oh-my-pi/pi-agent-core/compaction";
2
+ import { effectiveReserveTokens, estimateTokens, resolveThresholdTokens } from "@oh-my-pi/pi-agent-core/compaction";
1
3
  import type { Model } from "@oh-my-pi/pi-ai";
2
4
  import { countTokens } from "@oh-my-pi/pi-natives";
3
5
  import { formatNumber } from "@oh-my-pi/pi-utils";
4
6
  import type { Skill } from "../../extensibility/skills";
5
7
  import type { AgentSession } from "../../session/agent-session";
6
- import type { CompactionSettings } from "../../session/compaction";
7
- import { effectiveReserveTokens, estimateTokens, resolveThresholdTokens } from "../../session/compaction";
8
8
  import type { Tool } from "../../tools";
9
9
  import type { theme as Theme } from "../theme/theme";
10
10
 
package/src/sdk.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  Agent,
3
3
  type AgentEvent,
4
4
  type AgentMessage,
5
+ type AgentTelemetryConfig,
5
6
  type AgentTool,
6
7
  INTENT_FIELD,
7
8
  type ThinkingLevel,
@@ -244,6 +245,17 @@ export interface CreateAgentSessionOptions {
244
245
 
245
246
  /** Whether UI is available (enables interactive tools like ask). Default: false */
246
247
  hasUI?: boolean;
248
+
249
+ /**
250
+ * Opt-in OpenTelemetry instrumentation forwarded to the underlying Agent.
251
+ * Passing `{}` enables the loop's GenAI-semantic-convention spans. See
252
+ * {@link AgentTelemetryConfig} for the full surface (hooks, content capture,
253
+ * cost estimator, agent identity).
254
+ *
255
+ * Safe to enable without an OTEL SDK registered in the host: the
256
+ * `@opentelemetry/api` package returns a no-op tracer in that case.
257
+ */
258
+ telemetry?: AgentTelemetryConfig;
247
259
  }
248
260
 
249
261
  /** Result from createAgentSession */
@@ -920,18 +932,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
920
932
  // it. On timeout we forward `undefined` to ToolSession; buildSystemPromptInternal
921
933
  // will re-race the same promise through its own withDeadline path. Background
922
934
  // work continues so caches still warm.
923
- const raceWithDeadline = <T>(name: string, work: Promise<T>): Promise<T | undefined> =>
924
- Promise.race([
935
+ const raceWithDeadline = async <T>(name: string, work: Promise<T>): Promise<T | undefined> => {
936
+ let timedOut = false;
937
+ const result = await Promise.race([
925
938
  work,
926
939
  Bun.sleep(STARTUP_SCAN_DEADLINE_MS).then(() => {
927
- logger.warn("Startup scan exceeded deadline; deferring to system prompt fallback", {
928
- name,
929
- timeoutMs: STARTUP_SCAN_DEADLINE_MS,
930
- cwd,
931
- });
940
+ timedOut = true;
932
941
  return undefined;
933
942
  }),
934
943
  ]);
944
+ if (timedOut) {
945
+ logger.warn("Startup scan exceeded deadline; deferring to system prompt fallback", {
946
+ name,
947
+ timeoutMs: STARTUP_SCAN_DEADLINE_MS,
948
+ cwd,
949
+ });
950
+ }
951
+ return result;
952
+ };
935
953
  const [contextFiles, resolvedWorkspaceTree] = await Promise.all([
936
954
  contextFilesPromise,
937
955
  raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
@@ -1093,6 +1111,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1093
1111
  settings,
1094
1112
  authStorage,
1095
1113
  modelRegistry,
1114
+ getTelemetry: () => agent?.telemetry,
1096
1115
  };
1097
1116
 
1098
1117
  // Wire process-wide internal URL singletons owned by their real classes.
@@ -1746,6 +1765,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1746
1765
  },
1747
1766
  intentTracing: !!intentField,
1748
1767
  getToolChoice: () => session?.nextToolChoice(),
1768
+ telemetry: options.telemetry,
1749
1769
  });
1750
1770
 
1751
1771
  cursorEventEmitter = event => agent.emitExternalEvent(event);
@@ -1754,11 +1774,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1754
1774
  if (hasExistingSession) {
1755
1775
  agent.replaceMessages(existingSession.messages);
1756
1776
  } else {
1757
- // Save initial model and thinking level for new sessions so they can be restored on resume
1777
+ // Save initial model, thinking level, and service tier for new sessions so they can be restored on resume.
1758
1778
  if (model) {
1759
1779
  sessionManager.appendModelChange(`${model.provider}/${model.id}`);
1760
1780
  }
1761
1781
  sessionManager.appendThinkingLevelChange(thinkingLevel);
1782
+ if (initialServiceTier) {
1783
+ sessionManager.appendServiceTierChange(initialServiceTier);
1784
+ }
1762
1785
  }
1763
1786
 
1764
1787
  session = new AgentSession({