@code-yeongyu/senpi 2026.7.16 → 2026.7.17-3

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 (176) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/core/dynamic-prompt/build.d.ts +7 -0
  3. package/dist/core/dynamic-prompt/build.d.ts.map +1 -1
  4. package/dist/core/dynamic-prompt/build.js +5 -0
  5. package/dist/core/dynamic-prompt/build.js.map +1 -1
  6. package/dist/core/dynamic-prompt/workstation.d.ts +35 -0
  7. package/dist/core/dynamic-prompt/workstation.d.ts.map +1 -0
  8. package/dist/core/dynamic-prompt/workstation.js +81 -0
  9. package/dist/core/dynamic-prompt/workstation.js.map +1 -0
  10. package/dist/core/extensions/builtin/anthropic-web-search/index.d.ts +6 -2
  11. package/dist/core/extensions/builtin/anthropic-web-search/index.d.ts.map +1 -1
  12. package/dist/core/extensions/builtin/anthropic-web-search/index.js +73 -5
  13. package/dist/core/extensions/builtin/anthropic-web-search/index.js.map +1 -1
  14. package/dist/core/extensions/builtin/goal/command-registration.d.ts +1 -0
  15. package/dist/core/extensions/builtin/goal/command-registration.d.ts.map +1 -1
  16. package/dist/core/extensions/builtin/goal/command-registration.js +4 -5
  17. package/dist/core/extensions/builtin/goal/command-registration.js.map +1 -1
  18. package/dist/core/extensions/builtin/goal/elapsed-ticker.d.ts +43 -0
  19. package/dist/core/extensions/builtin/goal/elapsed-ticker.d.ts.map +1 -0
  20. package/dist/core/extensions/builtin/goal/elapsed-ticker.js +58 -0
  21. package/dist/core/extensions/builtin/goal/elapsed-ticker.js.map +1 -0
  22. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  23. package/dist/core/extensions/builtin/goal/index.js +39 -14
  24. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  25. package/dist/core/extensions/builtin/goal/tool-registration.d.ts +1 -0
  26. package/dist/core/extensions/builtin/goal/tool-registration.d.ts.map +1 -1
  27. package/dist/core/extensions/builtin/goal/tool-registration.js +3 -4
  28. package/dist/core/extensions/builtin/goal/tool-registration.js.map +1 -1
  29. package/dist/core/extensions/builtin/goal/ui.d.ts +2 -2
  30. package/dist/core/extensions/builtin/goal/ui.d.ts.map +1 -1
  31. package/dist/core/extensions/builtin/goal/ui.js +8 -4
  32. package/dist/core/extensions/builtin/goal/ui.js.map +1 -1
  33. package/dist/core/extensions/builtin/mcp/expose/native-search.d.ts +1 -1
  34. package/dist/core/extensions/builtin/mcp/expose/native-search.d.ts.map +1 -1
  35. package/dist/core/extensions/builtin/mcp/expose/native-search.js +1 -1
  36. package/dist/core/extensions/builtin/mcp/expose/native-search.js.map +1 -1
  37. package/dist/core/extensions/builtin/mcp/expose/policy.js +1 -1
  38. package/dist/core/extensions/builtin/mcp/expose/policy.js.map +1 -1
  39. package/dist/core/extensions/builtin/mcp/expose/status.js +1 -1
  40. package/dist/core/extensions/builtin/mcp/expose/status.js.map +1 -1
  41. package/dist/core/extensions/builtin/mcp/expose/tier-b.d.ts +3 -3
  42. package/dist/core/extensions/builtin/mcp/expose/tier-b.d.ts.map +1 -1
  43. package/dist/core/extensions/builtin/mcp/expose/tier-b.js +10 -10
  44. package/dist/core/extensions/builtin/mcp/expose/tier-b.js.map +1 -1
  45. package/dist/core/extensions/builtin/mcp/expose/tool-search.d.ts +3 -3
  46. package/dist/core/extensions/builtin/mcp/expose/tool-search.d.ts.map +1 -1
  47. package/dist/core/extensions/builtin/mcp/expose/tool-search.js +14 -14
  48. package/dist/core/extensions/builtin/mcp/expose/tool-search.js.map +1 -1
  49. package/dist/core/extensions/builtin/mcp/index.js +5 -5
  50. package/dist/core/extensions/builtin/mcp/index.js.map +1 -1
  51. package/dist/core/extensions/builtin/mcp/service.d.ts +2 -2
  52. package/dist/core/extensions/builtin/mcp/service.d.ts.map +1 -1
  53. package/dist/core/extensions/builtin/mcp/service.js +2 -2
  54. package/dist/core/extensions/builtin/mcp/service.js.map +1 -1
  55. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.d.ts.map +1 -1
  56. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +5 -1
  57. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js.map +1 -1
  58. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.d.ts.map +1 -1
  59. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js +5 -1
  60. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-5.js.map +1 -1
  61. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.d.ts.map +1 -1
  62. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js +5 -1
  63. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-6.js.map +1 -1
  64. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.d.ts.map +1 -1
  65. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js +5 -1
  66. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-7.js.map +1 -1
  67. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.d.ts.map +1 -1
  68. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js +5 -1
  69. package/dist/core/extensions/builtin/prompt-preset/claude-opus-4-8.js.map +1 -1
  70. package/dist/core/extensions/builtin/prompt-preset/glm-5-2.js +1 -1
  71. package/dist/core/extensions/builtin/prompt-preset/glm-5-2.js.map +1 -1
  72. package/dist/core/extensions/builtin/prompt-preset/gpt-5.2.js +1 -1
  73. package/dist/core/extensions/builtin/prompt-preset/gpt-5.2.js.map +1 -1
  74. package/dist/core/extensions/builtin/prompt-preset/gpt-5.3-codex.js +1 -1
  75. package/dist/core/extensions/builtin/prompt-preset/gpt-5.3-codex.js.map +1 -1
  76. package/dist/core/extensions/builtin/prompt-preset/gpt-5.4.js +1 -1
  77. package/dist/core/extensions/builtin/prompt-preset/gpt-5.4.js.map +1 -1
  78. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +1 -1
  79. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js.map +1 -1
  80. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +1 -1
  81. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js.map +1 -1
  82. package/dist/core/extensions/builtin/prompt-preset/gpt-5.js +1 -1
  83. package/dist/core/extensions/builtin/prompt-preset/gpt-5.js.map +1 -1
  84. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.js +1 -1
  85. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.js.map +1 -1
  86. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-7.js +1 -1
  87. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-7.js.map +1 -1
  88. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.d.ts +3 -0
  89. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.d.ts.map +1 -0
  90. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +14 -0
  91. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js.map +1 -0
  92. package/dist/core/extensions/builtin/prompt-preset/presets.d.ts.map +1 -1
  93. package/dist/core/extensions/builtin/prompt-preset/presets.js +13 -0
  94. package/dist/core/extensions/builtin/prompt-preset/presets.js.map +1 -1
  95. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  96. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts.map +1 -1
  97. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  98. package/dist/core/extensions/builtin/prompt-preset/settings.js.map +1 -1
  99. package/dist/core/extensions/builtin/webfetch/webfetch/renderers.d.ts.map +1 -1
  100. package/dist/core/extensions/builtin/webfetch/webfetch/renderers.js +2 -1
  101. package/dist/core/extensions/builtin/webfetch/webfetch/renderers.js.map +1 -1
  102. package/dist/core/extensions/builtin/webfetch/webfetch/tool.d.ts +27 -0
  103. package/dist/core/extensions/builtin/webfetch/webfetch/tool.d.ts.map +1 -1
  104. package/dist/core/extensions/builtin/webfetch/webfetch/tool.js +60 -1
  105. package/dist/core/extensions/builtin/webfetch/webfetch/tool.js.map +1 -1
  106. package/dist/core/extensions/types.d.ts +5 -0
  107. package/dist/core/extensions/types.d.ts.map +1 -1
  108. package/dist/core/extensions/types.js.map +1 -1
  109. package/dist/core/model-config.d.ts +5 -0
  110. package/dist/core/model-config.d.ts.map +1 -1
  111. package/dist/core/model-config.js +1 -0
  112. package/dist/core/model-config.js.map +1 -1
  113. package/dist/modes/interactive/components/tool-execution-renderer.d.ts.map +1 -1
  114. package/dist/modes/interactive/components/tool-execution-renderer.js +1 -0
  115. package/dist/modes/interactive/components/tool-execution-renderer.js.map +1 -1
  116. package/docs/AGENTS.md +70 -0
  117. package/docs/mcp.md +5 -5
  118. package/docs/settings.md +1 -1
  119. package/examples/AGENTS.md +3 -0
  120. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  121. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  122. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  123. package/examples/extensions/gondolin/package-lock.json +2 -2
  124. package/examples/extensions/gondolin/package.json +1 -1
  125. package/examples/extensions/sandbox/package-lock.json +2 -2
  126. package/examples/extensions/sandbox/package.json +1 -1
  127. package/examples/extensions/with-deps/package-lock.json +2 -2
  128. package/examples/extensions/with-deps/package.json +1 -1
  129. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +49 -0
  130. package/node_modules/@code-yeongyu/senpi-codemode/package.json +3 -3
  131. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +59 -19
  132. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +68 -2
  133. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +6 -0
  134. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +6 -0
  135. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  136. package/node_modules/@earendil-works/pi-ai/README.md +1 -1
  137. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  138. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +58 -7
  139. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  140. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts +2 -1
  141. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  142. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +25 -1
  143. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  144. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +70 -131
  145. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  146. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.d.ts +26 -0
  147. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.d.ts.map +1 -1
  148. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.js +6 -0
  149. package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.models.js.map +1 -1
  150. package/node_modules/@earendil-works/pi-ai/dist/providers/moonshotai-cn.models.js +3 -3
  151. package/node_modules/@earendil-works/pi-ai/dist/providers/moonshotai-cn.models.js.map +1 -1
  152. package/node_modules/@earendil-works/pi-ai/dist/providers/moonshotai.models.js +3 -3
  153. package/node_modules/@earendil-works/pi-ai/dist/providers/moonshotai.models.js.map +1 -1
  154. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.d.ts +44 -0
  155. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.d.ts.map +1 -1
  156. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js +36 -0
  157. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js.map +1 -1
  158. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts +0 -21
  159. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts.map +1 -1
  160. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +20 -38
  161. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
  162. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.models.d.ts +0 -110
  163. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.models.d.ts.map +1 -1
  164. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.models.js +0 -90
  165. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.models.js.map +1 -1
  166. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +14 -0
  167. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  168. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  169. package/node_modules/@earendil-works/pi-ai/dist/utils/tool-schema-compat.d.ts +19 -0
  170. package/node_modules/@earendil-works/pi-ai/dist/utils/tool-schema-compat.d.ts.map +1 -0
  171. package/node_modules/@earendil-works/pi-ai/dist/utils/tool-schema-compat.js +171 -0
  172. package/node_modules/@earendil-works/pi-ai/dist/utils/tool-schema-compat.js.map +1 -0
  173. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  174. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  175. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  176. package/package.json +6 -6
@@ -1,3 +1,4 @@
1
+ import * as os from "node:os";
1
2
  import type { ExtensionContext } from "@code-yeongyu/senpi";
2
3
  import type { KernelToHostMessage } from "./bridge/protocol.ts";
3
4
  import type { AgentExecuteTool } from "./bridges/agent-bridge.ts";
@@ -35,6 +36,8 @@ const SESSION_LIFECYCLE_EVENTS = [
35
36
 
36
37
  type SessionLifecycleEvent = (typeof SESSION_LIFECYCLE_EVENTS)[number];
37
38
 
39
+ type CodemodeEvent = SessionLifecycleEvent | "model_select";
40
+
38
41
  type TrackedExecution = {
39
42
  readonly promise: Promise<unknown>;
40
43
  readonly controller: AbortController;
@@ -51,7 +54,7 @@ type SessionRuntime = {
51
54
 
52
55
  export interface CodemodeExtensionAPI {
53
56
  registerTool(tool: ReturnType<typeof createEvalTool>): void;
54
- on(event: SessionLifecycleEvent, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void): void;
57
+ on(event: CodemodeEvent, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void): void;
55
58
  executeTool: AgentExecuteTool;
56
59
  getActiveTools(): string[];
57
60
  }
@@ -75,6 +78,32 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
75
78
  const manager = new SessionManagerProxy();
76
79
  const complete = options.complete ?? ((request, ctx) => createCompletionHandler()(ctx)(request));
77
80
  const renderers = { renderCall: renderEvalCall, renderResult: renderEvalResult };
81
+ let activeRuntime: SessionRuntime | undefined;
82
+ let activeModelId: string | undefined;
83
+ const registerEvalForRuntime = (runtime: SessionRuntime, modelId: string | undefined): void => {
84
+ pi.registerTool(
85
+ createEvalTool({
86
+ enabledLanguages: runtime.enabledLanguages,
87
+ kernelManager: manager,
88
+ cellTimeoutSeconds: runtime.settings.cellTimeoutSeconds,
89
+ executeTool: runtime.executeTool,
90
+ complete,
91
+ settings: runtime.settings,
92
+ artifactsDir: runtime.artifactsDir,
93
+ executionTracker: manager,
94
+ renderers,
95
+ spawns: runtime.spawns,
96
+ spawnDefaultAgent: runtime.settings.taskTools.task,
97
+ hostLine: hostLine(),
98
+ ...(modelId === undefined ? {} : { modelId }),
99
+ }),
100
+ );
101
+ };
102
+ const dropRuntime = async (): Promise<void> => {
103
+ activeRuntime = undefined;
104
+ activeModelId = undefined;
105
+ await manager.dispose();
106
+ };
78
107
  pi.registerTool(
79
108
  createEvalTool({
80
109
  enabledLanguages: { py: true, js: true, rb: true, jl: true },
@@ -85,6 +114,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
85
114
  settings: defaultCodemodeSettings,
86
115
  executionTracker: manager,
87
116
  renderers,
117
+ hostLine: hostLine(),
88
118
  }),
89
119
  );
90
120
 
@@ -92,25 +122,35 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
92
122
  const generation = manager.beginReplacement();
93
123
  const runtime = await createRuntime(pi, ctx, event, complete, options);
94
124
  if (!(await manager.replace(generation, runtime.manager))) return;
95
- pi.registerTool(
96
- createEvalTool({
97
- enabledLanguages: runtime.enabledLanguages,
98
- kernelManager: manager,
99
- cellTimeoutSeconds: runtime.settings.cellTimeoutSeconds,
100
- executeTool: runtime.executeTool,
101
- complete,
102
- settings: runtime.settings,
103
- artifactsDir: runtime.artifactsDir,
104
- executionTracker: manager,
105
- renderers,
106
- spawns: runtime.spawns,
107
- spawnDefaultAgent: runtime.settings.taskTools.task,
108
- }),
109
- );
125
+ activeRuntime = runtime;
126
+ activeModelId = ctx.model?.id;
127
+ registerEvalForRuntime(runtime, activeModelId);
128
+ });
129
+ pi.on("session_shutdown", async () => dropRuntime());
130
+ pi.on("session_before_switch", async () => dropRuntime());
131
+ pi.on("session_before_fork", async () => dropRuntime());
132
+ pi.on("model_select", async (event) => {
133
+ const runtime = activeRuntime;
134
+ if (runtime === undefined) return;
135
+ const modelId = modelIdFrom(event);
136
+ if (modelId === undefined || modelId === activeModelId) return;
137
+ activeModelId = modelId;
138
+ registerEvalForRuntime(runtime, modelId);
110
139
  });
111
- pi.on("session_shutdown", async () => manager.dispose());
112
- pi.on("session_before_switch", async () => manager.dispose());
113
- pi.on("session_before_fork", async () => manager.dispose());
140
+ }
141
+
142
+ function hostLine(): string {
143
+ const cpu = os.cpus()[0]?.model?.trim();
144
+ return [`${os.platform()} ${os.arch()}`, cpu, `${os.availableParallelism()} cores`]
145
+ .filter((part): part is string => !!part)
146
+ .join(" \u00b7 ");
147
+ }
148
+
149
+ function modelIdFrom(event: unknown): string | undefined {
150
+ if (typeof event !== "object" || event === null || !("model" in event)) return undefined;
151
+ const model = event.model;
152
+ if (typeof model !== "object" || model === null || !("id" in model)) return undefined;
153
+ return typeof model.id === "string" ? model.id : undefined;
114
154
  }
115
155
 
116
156
  class SessionManagerProxy implements CodemodeSessionManager, EvalExecutionTracker {
@@ -14,6 +14,35 @@ export interface EvalPromptParts {
14
14
  export interface EvalPromptOptions {
15
15
  readonly spawns: boolean;
16
16
  readonly spawnDefaultAgent?: string;
17
+ /** Active model id; selects the emphasis dialect of the batching guidance. */
18
+ readonly modelId?: string;
19
+ /** Preformatted host line (e.g. "darwin arm64 · Apple M5 Max · 18 cores"); enables the host-sizing note. */
20
+ readonly hostLine?: string;
21
+ }
22
+
23
+ /** Prompt dialect for the eval-first batching emphasis. */
24
+ export type EvalEmphasisStyle = "default" | "claude" | "codex" | "kimi";
25
+
26
+ const CLAUDE_MODEL_RE = /(^|[/.:])claude[-.]/i;
27
+ const GLM_MODEL_RE = /(^|[/.:@-])glm[-.]?\d/i;
28
+ const KIMI_MODEL_RE = /(^|[/.:])kimi[-.]/i;
29
+ const OPENAI_MODEL_RE = /(^|[/.:])(gpt|chatgpt|codex)[-.]|(^|[/.:])o[134](?:[-.]|$)/i;
30
+
31
+ /**
32
+ * Selects the eval-first batching dialect for a model id:
33
+ * - `claude`: Claude/GLM — direct imperatives; both are steered most reliably
34
+ * by explicit tagged directives (GLM prompting guidance routes to Claude's).
35
+ * - `codex`: OpenAI reasoning families — terse bounded rules, no emphasis spam.
36
+ * - `kimi`: Kimi K-series — positive operational constraints; all-caps NEVER
37
+ * directives make K2.x overthink instead of comply.
38
+ * - `default`: everything else (and no model) — maximum-emphasis fallback.
39
+ */
40
+ export function evalEmphasisStyle(modelId: string | undefined): EvalEmphasisStyle {
41
+ if (!modelId) return "default";
42
+ if (CLAUDE_MODEL_RE.test(modelId) || GLM_MODEL_RE.test(modelId)) return "claude";
43
+ if (KIMI_MODEL_RE.test(modelId)) return "kimi";
44
+ if (OPENAI_MODEL_RE.test(modelId)) return "codex";
45
+ return "default";
17
46
  }
18
47
 
19
48
  type ContextValue = string | boolean;
@@ -57,7 +86,24 @@ const EVAL_PROMPT_TEMPLATE = `Run one step of code in a persistent kernel.
57
86
 
58
87
  Work incrementally: imports in one call, define in the next, test, then use — each its own eval call. Re-run setup ONLY after \`reset\`, a kernel crash, or a \`NameError\`/\`ReferenceError\` proving the state is gone.
59
88
 
60
- **Batch through eval.** One cell replaces many one-shot tool calls: loop or comprehend over file sets with \`read()\`/stdlib instead of reading files one call at a time, post-process \`tool.<name>()\` results programmatically, and fan independent calls through \`parallel(thunks)\` within the cell — never one call per turn.
89
+ {{#if styleClaude}}<eval_first_batching>
90
+ \`eval\` is your default execution surface: if a step needs more than one tool call, write ONE cell that performs the whole step — never issue the calls one at a time.
91
+ - Enumerate every lookup the step needs, then run all independent ones simultaneously with \`parallel(thunks)\` inside the cell; keep calls sequential only when one result feeds the next.
92
+ - Write real code around the calls: loop or comprehend over file sets with \`read()\`/stdlib, branch per case, and wrap risky calls in try/except so one failure degrades only its item — recover or retry inside the cell, keep the batch alive.
93
+ - Post-process \`tool.<name>()\` results programmatically and return distilled facts, not raw dumps.
94
+ </eval_first_batching>{{/if}}{{#if styleCodex}}Route multi-call steps through eval: one cell per step, independent lookups dispatched together via \`parallel(thunks)\`; keep work sequential only when one result determines the next action.
95
+ - Loop or comprehend over file sets with \`read()\`/stdlib instead of reading files one call at a time; post-process \`tool.<name>()\` results programmatically.
96
+ - Wrap failable calls in try/except inside the cell; a failed item degrades only itself. After two distinct failed strategies for the same fact, fall back to direct tool calls.
97
+ - Reduce large results in-kernel to the facts the task needs before returning.{{/if}}{{#if styleKimi}}Treat eval as the standard way to execute any step involving several tool calls: write one cell that performs the whole step.
98
+ - When lookups are independent, run them together with \`parallel(thunks)\` in that cell; when one result feeds the next, keep them sequential inside the same cell.
99
+ - Loop or comprehend over file sets with \`read()\`/stdlib, post-process \`tool.<name>()\` results programmatically, and put try/except around each risky call so the rest of the batch completes.
100
+ - Filter and aggregate results in the kernel, then return the distilled facts.{{/if}}{{#if styleDefault}}**EVAL IS YOUR PRIMARY EXECUTION SURFACE.** Any step that needs MORE THAN ONE tool call MUST be written as ONE cell — NEVER as a chain of single tool calls.
101
+ - **PLAN THE WHOLE STEP, THEN BATCH IT.** Enumerate every read/search/lookup the step needs and dispatch ALL independent ones through \`parallel(thunks)\` in one cell.
102
+ - **WRITE REAL CODE, NOT CALL LISTS.** Loop or comprehend over file sets with \`read()\`/stdlib, branch \`if\`/\`else\` per case, post-process \`tool.<name>()\` results programmatically, and wrap EVERY risky call in try/except so ONE failure NEVER kills the batch.
103
+ - **DISTILL IN-KERNEL.** Filter, diff, and aggregate in code before returning; return facts, NOT dumps.{{/if}}
104
+ {{#if hostLine}}
105
+ Host: {{hostLine}} — cells execute here. Size \`parallel(thunks)\` pools to its cores; \`tool.<name>()\` shell commands must fit this platform, even when the code you are writing targets another machine.
106
+ {{/if}}
61
107
 
62
108
  Fields:
63
109
 
@@ -129,6 +175,7 @@ export function buildEvalPrompt(
129
175
  throw new Error("no kernels enabled for eval prompt");
130
176
  }
131
177
  const spawnDefaultAgent = options.spawnDefaultAgent ?? "task";
178
+ const style = evalEmphasisStyle(options.modelId);
132
179
  const context: Context = {
133
180
  py: enabled.py,
134
181
  js: enabled.js,
@@ -136,6 +183,11 @@ export function buildEvalPrompt(
136
183
  jl: enabled.jl,
137
184
  spawns: options.spawns,
138
185
  spawnDefaultAgent,
186
+ styleClaude: style === "claude",
187
+ styleCodex: style === "codex",
188
+ styleKimi: style === "kimi",
189
+ styleDefault: style === "default",
190
+ hostLine: options.hostLine ?? "",
139
191
  };
140
192
  const examples = REUSE_CHAIN_EXAMPLES.filter((example) => enabled[example.language])
141
193
  .map((example) => {
@@ -155,12 +207,26 @@ export function buildEvalPrompt(
155
207
  description,
156
208
  promptSnippet: "Run one incremental code cell in a persistent language kernel.",
157
209
  promptGuidelines: [
158
- "Use eval for incremental code execution when a persistent JS, Python, Ruby, or Julia kernel is available.",
210
+ BATCHING_GUIDELINES[style],
159
211
  "Use eval reset only when a language kernel must be wiped; reset is scoped to the selected language.",
160
212
  ],
161
213
  };
162
214
  }
163
215
 
216
+ /**
217
+ * System-prompt guideline per emphasis dialect. The default dialect carries
218
+ * maximum emphasis so unmapped models still batch through eval; the others are
219
+ * tuned to what steers that family reliably.
220
+ */
221
+ const BATCHING_GUIDELINES: Record<EvalEmphasisStyle, string> = {
222
+ default:
223
+ "**EVAL FIRST.** Any step needing MORE THAN ONE tool call MUST be ONE eval cell: run independent calls in parallel, wrap risky calls in try/except, and return distilled facts — NEVER a chain of single tool calls.",
224
+ claude:
225
+ "Prefer eval for any step needing more than one tool call: one cell that runs independent calls in parallel, handles per-call failures in code, and returns distilled facts.",
226
+ codex: "Route multi-call steps through eval: one cell per step, independent calls dispatched in parallel; fall back to direct tool calls when one call is sufficient or each result changes the next decision.",
227
+ kimi: "Treat eval as the standard way to execute multi-call steps: one cell that runs independent calls in parallel, handles failures per item, and returns distilled facts.",
228
+ };
229
+
164
230
  function renderTemplate(template: string, context: Context): string {
165
231
  let index = 0;
166
232
  const [rendered, nextIndex] = renderUntil(template, context, index, []);
@@ -39,6 +39,10 @@ export interface CreateEvalToolOptions {
39
39
  readonly spawns?: boolean;
40
40
  /** Default agent name surfaced in the agent() helper docs when spawns are enabled. */
41
41
  readonly spawnDefaultAgent?: string;
42
+ /** Active model id; selects the emphasis dialect of the eval prompt. */
43
+ readonly modelId?: string;
44
+ /** Preformatted host line rendered into the prompt's host-sizing note. */
45
+ readonly hostLine?: string;
42
46
  }
43
47
 
44
48
  interface EvalCellInvocation {
@@ -152,6 +156,8 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
152
156
  const prompt = buildEvalPrompt(options.enabledLanguages, {
153
157
  spawns: options.spawns ?? false,
154
158
  ...(options.spawnDefaultAgent === undefined ? {} : { spawnDefaultAgent: options.spawnDefaultAgent }),
159
+ ...(options.modelId === undefined ? {} : { modelId: options.modelId }),
160
+ ...(options.hostLine === undefined ? {} : { hostLine: options.hostLine }),
155
161
  });
156
162
  const languages = enabledLanguageList(options.enabledLanguages);
157
163
  return {
@@ -725,6 +725,12 @@ export function renderEvalCall(
725
725
  context: RenderContext,
726
726
  ): EvalRenderComponent {
727
727
  const component = componentFor(context);
728
+ if (context.hasResult === true) {
729
+ // The result renderer owns the full pending -> running -> done frame once a result exists.
730
+ // Rendering the call frame too would stack a duplicate box, so yield to it here.
731
+ component.setBlocks([]);
732
+ return component;
733
+ }
728
734
  if (theme === undefined && context.spinnerFrame === undefined) {
729
735
  const title = args.title === undefined ? "" : ` ${args.title}`;
730
736
  const reset = args.reset === true ? " reset" : "";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@earendil-works/pi-agent-core",
3
3
  "private": true,
4
- "version": "2026.7.16",
4
+ "version": "2026.7.17-3",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -30,7 +30,7 @@
30
30
  "prepublishOnly": "npm run clean && npm run build"
31
31
  },
32
32
  "dependencies": {
33
- "@earendil-works/pi-ai": "^2026.7.16",
33
+ "@earendil-works/pi-ai": "^2026.7.17-3",
34
34
  "ignore": "7.0.5",
35
35
  "typebox": "1.1.38",
36
36
  "yaml": "2.9.0"
@@ -747,7 +747,7 @@ Many models support thinking/reasoning capabilities where they can show their in
747
747
  const model = models.getModel('anthropic', 'claude-sonnet-4-5')!;
748
748
  // or models.getModel('openai', 'gpt-5-mini');
749
749
  // or models.getModel('google', 'gemini-2.5-flash');
750
- // or models.getModel('xai', 'grok-code-fast-1');
750
+ // or models.getModel('xai', 'grok-4.5');
751
751
 
752
752
  // Check if model supports reasoning
753
753
  if (model.reasoning) {
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAU1C,OAAO,KAAK,EAWX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAgKrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AAuDhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAghBD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CAsTzE,CAAC;AAoEF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CA2ClF,CAAC"}
1
+ {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAC;AAU1C,OAAO,KAAK,EAWX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAgKrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA6DhE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAuiBD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CAwUzE,CAAC;AAoEF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CA2ClF,CAAC"}
@@ -152,6 +152,12 @@ function getAnthropicCompat(model) {
152
152
  supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? !CLAUDE_FABLE_OR_MYTHOS_MODEL_ID.test(model.id),
153
153
  allowEmptySignature: model.compat?.allowEmptySignature ?? false,
154
154
  supportsToolReferences: model.compat?.supportsToolReferences ?? defaultSupportsToolReferences(model),
155
+ // Default: first-party Anthropic only. Anthropic-compatible providers
156
+ // (kimi-coding, fireworks, copilot, gateways) may execute the server-side
157
+ // search but reject the replayed server_tool_use / web_search_tool_result
158
+ // blocks on the next request (kimi-coding 400s with `tool_call_id is not
159
+ // found`).
160
+ supportsWebSearch: model.compat?.supportsWebSearch ?? isAnthropicApiBaseUrl(model.baseUrl),
155
161
  };
156
162
  }
157
163
  /**
@@ -227,6 +233,23 @@ function lastAnthropicFallbackBoundary(content) {
227
233
  function isAnthropicServerToolUseBlock(raw) {
228
234
  return isRecord(raw) && raw.type === "server_tool_use" && typeof raw.id === "string";
229
235
  }
236
+ // Only tool_use-shaped provider-native blocks (server_tool_use, mcp_tool_use)
237
+ // stream their input via input_json_delta. Result-shaped blocks must replay
238
+ // byte-for-byte (encrypted_content), so never merge an `input` into them.
239
+ function isProviderNativeToolUseBlock(raw) {
240
+ return isRecord(raw) && (raw.type === "server_tool_use" || raw.type === "mcp_tool_use");
241
+ }
242
+ // Endpoints without `supportsWebSearch` reject replayed web-search server-tool
243
+ // blocks (kimi-coding 400s with `tool_call_id is not found`), wedging every
244
+ // subsequent request of the session. Dropping the pair loses the searched
245
+ // context but keeps the conversation usable.
246
+ function isAnthropicWebSearchReplayBlock(raw) {
247
+ if (!isRecord(raw))
248
+ return false;
249
+ if (raw.type === "web_search_tool_result")
250
+ return true;
251
+ return raw.type === "server_tool_use" && raw.name === "web_search";
252
+ }
230
253
  // tool_use ids referenced by server-tool result blocks in content[0, boundary).
231
254
  // A pre-boundary `server_tool_use` whose id is absent here is unpaired — the
232
255
  // fallback interrupted the declined attempt before its result arrived — so
@@ -339,27 +362,28 @@ function rejectsComputerUseBeta(model) {
339
362
  return ((model.provider === "cloudflare-ai-gateway" && model.baseUrl.includes("anthropic")) ||
340
363
  matchesModelMarker(model, UNSUPPORTED_NATIVE_COMPUTER_TOOL_MODEL_MARKERS));
341
364
  }
365
+ function isAnthropicWebSearchToolType(toolType) {
366
+ return toolType.startsWith("web_search_");
367
+ }
342
368
  function sanitizeUnsupportedNativeTools(model, params) {
343
369
  const payload = params;
344
370
  const headers = stringRecord(payload.headers);
345
371
  const headerSanitization = rejectsComputerUseBeta(model)
346
372
  ? removeComputerUseBetaHeader(headers)
347
373
  : { changed: false };
374
+ const rejectsNativeWebSearch = !getAnthropicCompat(model).supportsWebSearch;
348
375
  const tools = payload.tools;
349
376
  const sanitized = { ...payload };
350
377
  let changed = false;
351
- const removedToolNames = new Set();
352
378
  if (Array.isArray(tools)) {
353
379
  const supportedTools = [];
354
380
  for (const tool of tools) {
355
381
  const hookTool = tool;
356
382
  if (isRecord(hookTool) &&
357
383
  typeof hookTool.type === "string" &&
358
- rejectsNativeComputerTool(model, hookTool.type)) {
384
+ (rejectsNativeComputerTool(model, hookTool.type) ||
385
+ (rejectsNativeWebSearch && isAnthropicWebSearchToolType(hookTool.type)))) {
359
386
  changed = true;
360
- if (typeof hookTool.name === "string") {
361
- removedToolNames.add(hookTool.name);
362
- }
363
387
  continue;
364
388
  }
365
389
  supportedTools.push(tool);
@@ -384,7 +408,10 @@ function sanitizeUnsupportedNativeTools(model, params) {
384
408
  }
385
409
  if (changed && isRecord(sanitized.tool_choice)) {
386
410
  const toolChoiceName = sanitized.tool_choice.name;
387
- const shouldRemoveToolChoice = (typeof toolChoiceName === "string" && removedToolNames.has(toolChoiceName)) || sanitized.tools === undefined;
411
+ const hasSelectedTool = typeof toolChoiceName === "string" &&
412
+ Array.isArray(sanitized.tools) &&
413
+ sanitized.tools.some((tool) => isRecord(tool) && tool.name === toolChoiceName);
414
+ const shouldRemoveToolChoice = sanitized.tools === undefined || (typeof toolChoiceName === "string" && !hasSelectedTool);
388
415
  if (shouldRemoveToolChoice) {
389
416
  delete sanitized.tool_choice;
390
417
  }
@@ -764,6 +791,12 @@ export const stream = (model, context, options) => {
764
791
  partial: output,
765
792
  });
766
793
  }
794
+ else if (block && block.type === "providerNative" && isProviderNativeToolUseBlock(block.raw)) {
795
+ // Server-side tool blocks (server_tool_use) stream their input
796
+ // the same way tool_use does; the block captured at
797
+ // content_block_start still has `input: {}`.
798
+ block.partialJson = (block.partialJson ?? "") + event.delta.partial_json;
799
+ }
767
800
  }
768
801
  else if (event.delta.type === "signature_delta") {
769
802
  const index = blocks.findIndex((b) => b.index === event.index);
@@ -807,6 +840,13 @@ export const stream = (model, context, options) => {
807
840
  partial: output,
808
841
  });
809
842
  }
843
+ else if (block.type === "providerNative") {
844
+ const partialJson = block.partialJson;
845
+ delete block.partialJson;
846
+ if (partialJson !== undefined && isRecord(block.raw)) {
847
+ block.raw = { ...block.raw, input: parseStreamingJson(partialJson) };
848
+ }
849
+ }
810
850
  }
811
851
  }
812
852
  else if (event.type === "message_delta") {
@@ -859,6 +899,13 @@ export const stream = (model, context, options) => {
859
899
  catch (error) {
860
900
  for (const block of output.content) {
861
901
  delete block.index;
902
+ // An aborted stream never reaches content_block_stop; keep whatever
903
+ // provider-native input accumulated, mirroring toolCall's partial
904
+ // arguments.
905
+ const scratch = block.partialJson;
906
+ if (block.type === "providerNative" && scratch !== undefined && isRecord(block.raw)) {
907
+ block.raw = { ...block.raw, input: parseStreamingJson(scratch) };
908
+ }
862
909
  // partialJson is only a streaming scratch buffer; never persist it.
863
910
  delete block.partialJson;
864
911
  }
@@ -1199,6 +1246,7 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
1199
1246
  // Tool calls from a declined pre-fallback attempt are dropped from their
1200
1247
  // assistant turn below; drop their tool_results in lockstep so none dangle.
1201
1248
  const discardedFallbackToolCallIds = collectDiscardedFallbackToolCallIds(transformedMessages, model);
1249
+ const rejectsNativeWebSearchReplay = !getAnthropicCompat(model).supportsWebSearch;
1202
1250
  for (let i = 0; i < transformedMessages.length; i++) {
1203
1251
  const msg = transformedMessages[i];
1204
1252
  if (msg.role === "user") {
@@ -1320,8 +1368,11 @@ function convertMessages(transformedMessages, model, isOAuthToken, cacheControl,
1320
1368
  });
1321
1369
  }
1322
1370
  else if (block.type === "providerNative") {
1323
- if (isSameModel && isReplayableAnthropicProviderNativeBlock(block.raw))
1371
+ if (isSameModel &&
1372
+ isReplayableAnthropicProviderNativeBlock(block.raw) &&
1373
+ !(rejectsNativeWebSearchReplay && isAnthropicWebSearchReplayBlock(block.raw))) {
1324
1374
  blocks.push(block.raw);
1375
+ }
1325
1376
  }
1326
1377
  }
1327
1378
  if (blocks.length === 0)