pi-codemcp 1.2.2 → 1.3.0

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.
@@ -24,6 +24,7 @@ from .json_types import (
24
24
  JsonValue,
25
25
  )
26
26
  from .models import SearchDetail, ToolSchemaView
27
+ from .sandbox_api import STUB_PRELUDE
27
28
 
28
29
  if TYPE_CHECKING:
29
30
  from collections.abc import Iterable
@@ -32,15 +33,6 @@ if TYPE_CHECKING:
32
33
 
33
34
  SEARCH_SCORE_CUTOFF = 20.0
34
35
  MIN_PLURAL_TOKEN_LENGTH = 4
35
- STUB_IMPORTS = "from typing import Literal, Never, NotRequired, TypeAlias, TypedDict"
36
- JSON_TYPE_STUBS = (
37
- "JsonScalar: TypeAlias = bool | int | float | str | None",
38
- 'JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]',
39
- )
40
- INSPECT_JSON_STUB = (
41
- "def inspect_json(value: JsonValue, *, samples: int = 2, max_depth: int = 3) -> JsonValue: ..."
42
- )
43
- STUB_PRELUDE = "\n\n".join([STUB_IMPORTS, *JSON_TYPE_STUBS, INSPECT_JSON_STUB])
44
36
 
45
37
 
46
38
  class ToolSpec(BaseModel):
package/src/chains.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { existsSync, readdirSync, readFileSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { type ExtensionAPI, highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
@@ -67,6 +68,10 @@ interface LoadedChains {
67
68
 
68
69
  const CHAIN_NAME = /^[a-z][a-z0-9_]{0,63}$/;
69
70
 
71
+ export function newCodeMcpTraceId(source: string): string {
72
+ return `${source}:${randomUUID()}`;
73
+ }
74
+
70
75
  export class SavedChainManager {
71
76
  readonly startupErrors: string[] = [];
72
77
  private readonly manifests = new Map<string, ScopedSavedChain>();
@@ -94,7 +99,11 @@ export class SavedChainManager {
94
99
  }
95
100
  }
96
101
 
97
- async save(input: SaveChainInput, signal?: AbortSignal): Promise<SavedChainView> {
102
+ async save(
103
+ input: SaveChainInput,
104
+ traceId: string,
105
+ signal?: AbortSignal,
106
+ ): Promise<SavedChainView> {
98
107
  assertChainName(input.name);
99
108
  this.assertToolNameAvailable(input.name);
100
109
  const result = await this.lifecycle.request(
@@ -106,6 +115,7 @@ export class SavedChainManager {
106
115
  code: input.code,
107
116
  input_schema: input.inputSchema,
108
117
  output_schema: input.outputSchema,
118
+ trace_id: traceId,
109
119
  },
110
120
  signal,
111
121
  );
@@ -116,8 +126,8 @@ export class SavedChainManager {
116
126
  return view;
117
127
  }
118
128
 
119
- async list(signal?: AbortSignal): Promise<SavedChainView[]> {
120
- const result = await this.lifecycle.request("list_chains", {}, signal);
129
+ async list(traceId: string, signal?: AbortSignal): Promise<SavedChainView[]> {
130
+ const result = await this.lifecycle.request("list_chains", { trace_id: traceId }, signal);
121
131
  const views = parseViewList(result.chains, "list_chains.chains");
122
132
  this.synchronizeViews(views);
123
133
  return views;
@@ -127,11 +137,12 @@ export class SavedChainManager {
127
137
  name: string,
128
138
  scope: ChainScope,
129
139
  enabled: boolean,
140
+ traceId: string,
130
141
  signal?: AbortSignal,
131
142
  ): Promise<SavedChainView> {
132
143
  const result = await this.lifecycle.request(
133
144
  "set_chain_enabled",
134
- { name, scope, enabled },
145
+ { name, scope, enabled, trace_id: traceId },
135
146
  signal,
136
147
  );
137
148
  const view = parseSavedChainView(result, "set_chain_enabled");
@@ -140,16 +151,34 @@ export class SavedChainManager {
140
151
  return view;
141
152
  }
142
153
 
143
- async revalidate(name: string, scope: ChainScope, signal?: AbortSignal): Promise<SavedChainView> {
144
- const result = await this.lifecycle.request("revalidate_chain", { name, scope }, signal);
154
+ async revalidate(
155
+ name: string,
156
+ scope: ChainScope,
157
+ traceId: string,
158
+ signal?: AbortSignal,
159
+ ): Promise<SavedChainView> {
160
+ const result = await this.lifecycle.request(
161
+ "revalidate_chain",
162
+ { name, scope, trace_id: traceId },
163
+ signal,
164
+ );
145
165
  const view = parseSavedChainView(result, "revalidate_chain");
146
166
  this.upsertView(view);
147
167
  this.refreshNativeTools();
148
168
  return view;
149
169
  }
150
170
 
151
- async delete(name: string, scope: ChainScope, signal?: AbortSignal): Promise<SavedChainView[]> {
152
- const result = await this.lifecycle.request("delete_chain", { name, scope }, signal);
171
+ async delete(
172
+ name: string,
173
+ scope: ChainScope,
174
+ traceId: string,
175
+ signal?: AbortSignal,
176
+ ): Promise<SavedChainView[]> {
177
+ const result = await this.lifecycle.request(
178
+ "delete_chain",
179
+ { name, scope, trace_id: traceId },
180
+ signal,
181
+ );
153
182
  const views = parseViewList(result.chains, "delete_chain.chains");
154
183
  this.synchronizeViews(views);
155
184
  return views;
@@ -163,7 +192,7 @@ export class SavedChainManager {
163
192
  label: chain.name,
164
193
  description: chain.description,
165
194
  parameters: chain.inputSchema,
166
- async execute(_toolCallId, params, signal, onUpdate) {
195
+ async execute(toolCallId, params, signal, onUpdate) {
167
196
  onUpdate?.({
168
197
  content: [{ type: "text", text: `Running saved MCP chain ${chain.name}...` }],
169
198
  details: undefined,
@@ -171,13 +200,24 @@ export class SavedChainManager {
171
200
  const arguments_ = requireRecord(params, `${chain.name} arguments`);
172
201
  const result = await manager.lifecycle.request(
173
202
  "execute_chain",
174
- { name: chain.name, arguments: arguments_ },
203
+ { name: chain.name, arguments: arguments_, trace_id: toolCallId },
175
204
  signal,
176
205
  );
177
206
  if (result.ok !== true) {
178
- const error =
179
- typeof result.error === "string" ? result.error : `Saved chain ${chain.name} failed`;
180
- throw new Error(error);
207
+ throw new Error(
208
+ JSON.stringify({
209
+ failure_stage: result.failure_stage,
210
+ error:
211
+ typeof result.error === "string"
212
+ ? result.error
213
+ : `Saved chain ${chain.name} failed`,
214
+ failure: result.failure,
215
+ result_ref: result.result_ref,
216
+ expires_in_seconds: result.expires_in_seconds,
217
+ calls_made: result.calls_made,
218
+ chain_calls: result.chain_calls,
219
+ }),
220
+ );
181
221
  }
182
222
  const settings = manager.lifecycle.loadSettings();
183
223
  const output = formatCodeMcpOutput(result.result, {
package/src/mcp-client.ts CHANGED
@@ -99,6 +99,7 @@ export class SidecarClient {
99
99
  ...definedProcessEnvironment(),
100
100
  ...(options.environment ?? {}),
101
101
  PI_CODEMCP_AGENT_DIR: this.agentDir,
102
+ PI_CODEMCP_PACKAGE_VERSION: this.packageVersion,
102
103
  ...(this.projectChainsDirectory === undefined
103
104
  ? {}
104
105
  : { PI_CODEMCP_PROJECT_CHAINS_DIR: this.projectChainsDirectory }),
package/src/modal.ts CHANGED
@@ -72,6 +72,7 @@ export interface StatsModalState {
72
72
  p95Ms: number;
73
73
  maxMs: number;
74
74
  }>;
75
+ outcomes: Array<{ name: string; count: number }>;
75
76
  failures: Array<{ stage: string; count: number }>;
76
77
  upstreamOutputBytes: number;
77
78
  cacheHits: number;
@@ -258,6 +259,10 @@ export function statsStateFromSnapshot(snapshot: Record<string, unknown>): Stats
258
259
  },
259
260
  ];
260
261
  });
262
+ const rawOutcomes = isRecord(snapshot.outcomes) ? snapshot.outcomes : {};
263
+ const outcomes = Object.entries(rawOutcomes)
264
+ .flatMap(([name, count]) => (typeof count === "number" && count >= 0 ? [{ name, count }] : []))
265
+ .sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
261
266
  const rawFailures = isRecord(snapshot.failures) ? snapshot.failures : {};
262
267
  const failures = Object.entries(rawFailures)
263
268
  .flatMap(([stage, count]) =>
@@ -278,6 +283,7 @@ export function statsStateFromSnapshot(snapshot: Record<string, unknown>): Stats
278
283
  recent,
279
284
  operations,
280
285
  phases,
286
+ outcomes,
281
287
  failures,
282
288
  upstreamOutputBytes,
283
289
  cacheHits: numberField(cache, "hits"),
@@ -755,6 +761,11 @@ class ServerManagerModal implements Component, Focusable {
755
761
  ),
756
762
  );
757
763
  }
764
+ lines.push("", this.theme.fg("dim", this.theme.bold("OUTCOMES")));
765
+ if (stats.outcomes.length === 0) lines.push(this.theme.fg("muted", "No outcomes yet"));
766
+ for (const outcome of stats.outcomes) {
767
+ lines.push(`${outcome.name.padEnd(22)} ${outcome.count.toLocaleString().padStart(8)}`);
768
+ }
758
769
  lines.push("", this.theme.fg("dim", this.theme.bold("FAILURE STAGES")));
759
770
  if (stats.failures.length === 0) lines.push(this.theme.fg("muted", "No failures yet"));
760
771
  for (const failure of stats.failures) {
package/src/prompts.ts CHANGED
@@ -8,10 +8,10 @@ export const INSPECT_PROMPT_GUIDELINES = [
8
8
  ] as const;
9
9
 
10
10
  export const EXECUTE_PROMPT_GUIDELINES = [
11
- "Use programmatic execution for a bounded workflow when code can deterministically filter, join, aggregate, deduplicate, validate, or reduce intermediate results.",
11
+ "Use bounded execution to deterministically filter, aggregate, sample, join, or reduce upstream data in the sandbox; do not return raw payloads.",
12
12
  "Keep a model turn between calls when an intermediate result changes the semantic decision or user approval is required.",
13
- "Return the smallest result that answers the request; oversized results fail explicitly with bounded structural inspection data.",
14
- "SDK facades are prebound globals and must not be imported. Import supported stdlib normally (for example, `import asyncio`); class declarations and `__import__` are unsupported.",
13
+ "Return the smallest answer. If an oversized result provides result_ref, refine it via inputRef instead of repeating upstream calls.",
14
+ "SDK facades are prebound globals and must not be imported. Use `import asyncio` for `asyncio.gather`; other imports, classes, `asyncio.create_task`, and `__import__` are unsupported.",
15
15
  ] as const;
16
16
 
17
17
  export const SAVE_CHAIN_PROMPT_GUIDELINES = [
package/src/tools.ts CHANGED
@@ -144,6 +144,12 @@ const ExecuteParameters = Type.Object({
144
144
  description:
145
145
  "Sandboxed Python body. Call typed SDK methods such as await linear.list_issues(arguments) and return a compact final value.",
146
146
  }),
147
+ inputRef: Type.Optional(
148
+ Type.String({
149
+ minLength: 1,
150
+ description: "Opaque retained-result reference exposed to sandbox code as input",
151
+ }),
152
+ ),
147
153
  });
148
154
 
149
155
  export function registerCodeMcpTools(
@@ -159,7 +165,7 @@ export function registerCodeMcpTools(
159
165
  promptSnippet: "Discover compact MCP capabilities or inventory",
160
166
  promptGuidelines: [...SEARCH_PROMPT_GUIDELINES],
161
167
  parameters: SearchParameters,
162
- async execute(_toolCallId, params, signal, onUpdate) {
168
+ async execute(toolCallId, params, signal, onUpdate) {
163
169
  onUpdate?.({
164
170
  content: [{ type: "text", text: "Searching MCP tools..." }],
165
171
  details: undefined,
@@ -173,6 +179,7 @@ export function registerCodeMcpTools(
173
179
  limit: params.limit ?? 5,
174
180
  cursor: params.cursor ?? 0,
175
181
  ...(params.server === undefined ? {} : { server: params.server }),
182
+ trace_id: toolCallId,
176
183
  },
177
184
  signal,
178
185
  );
@@ -247,12 +254,16 @@ export function registerCodeMcpTools(
247
254
  promptSnippet: "Load exact typed contracts for selected MCP calls",
248
255
  promptGuidelines: [...INSPECT_PROMPT_GUIDELINES],
249
256
  parameters: InspectParameters,
250
- async execute(_toolCallId, params, signal, onUpdate) {
257
+ async execute(toolCallId, params, signal, onUpdate) {
251
258
  onUpdate?.({
252
259
  content: [{ type: "text", text: "Inspecting MCP tool contracts..." }],
253
260
  details: undefined,
254
261
  });
255
- const result = await lifecycle.request("inspect", { calls: params.calls }, signal);
262
+ const result = await lifecycle.request(
263
+ "inspect",
264
+ { calls: params.calls, trace_id: toolCallId },
265
+ signal,
266
+ );
256
267
  const output = formatCodeMcpOutput(result, outputLimits(lifecycle));
257
268
  const results = Array.isArray(result.results) ? result.results : [];
258
269
  return {
@@ -292,23 +303,34 @@ export function registerCodeMcpTools(
292
303
  name: "codemcp_execute",
293
304
  label: "MCP Execute",
294
305
  description:
295
- "Type-check and execute one bounded sandboxed Python MCP call graph. Use it when code can deterministically process intermediate results; preserve a model turn for semantic decisions or approvals. The sandbox has no host filesystem, environment, network, or subprocess access. Oversized results fail with bounded shape, size, and sample diagnostics.",
306
+ "Type-check and execute one bounded sandboxed Python MCP call graph. Filter, aggregate, or sample upstream data inside the sandbox instead of returning raw payloads; an optional inputRef exposes a retained oversized value as input without repeating upstream calls. Preserve a model turn for semantic decisions or approvals. The sandbox has no host filesystem, environment, network, or subprocess access. Oversized results fail with bounded shape, size, and sample diagnostics.",
296
307
  promptSnippet: "Run a bounded typed MCP workflow and return a compact result",
297
308
  promptGuidelines: [...EXECUTE_PROMPT_GUIDELINES],
298
309
  parameters: ExecuteParameters,
299
- async execute(_toolCallId, params, signal, onUpdate) {
310
+ async execute(toolCallId, params, signal, onUpdate) {
300
311
  onUpdate?.({
301
312
  content: [{ type: "text", text: "Type-checking MCP chain..." }],
302
313
  details: undefined,
303
314
  });
304
- const result = await lifecycle.request("execute", { code: params.code }, signal);
315
+ const result = await lifecycle.request(
316
+ "execute",
317
+ {
318
+ code: params.code,
319
+ trace_id: toolCallId,
320
+ ...(params.inputRef === undefined ? {} : { input_ref: params.inputRef }),
321
+ },
322
+ signal,
323
+ );
305
324
  const ok = result.ok === true;
306
325
  const modelValue = ok
307
326
  ? result.result
308
327
  : {
309
328
  failure_stage: result.failure_stage,
310
329
  error: result.error,
330
+ failure: result.failure,
311
331
  shape: result.shape,
332
+ result_ref: result.result_ref,
333
+ expires_in_seconds: result.expires_in_seconds,
312
334
  calls_made: result.calls_made,
313
335
  chain_calls: result.chain_calls,
314
336
  };
@@ -365,7 +387,7 @@ export function registerCodeMcpTools(
365
387
  promptSnippet: "Save a repeated MCP execution as a typed reusable native tool",
366
388
  promptGuidelines: [...SAVE_CHAIN_PROMPT_GUIDELINES],
367
389
  parameters: SaveChainParameters,
368
- async execute(_toolCallId, params, signal, onUpdate) {
390
+ async execute(toolCallId, params, signal, onUpdate) {
369
391
  const scope = requireChainScope(params.scope ?? "project");
370
392
  if (scope === "project" && lifecycle.projectChainsPath === undefined) {
371
393
  throw new Error(
@@ -385,6 +407,7 @@ export function registerCodeMcpTools(
385
407
  inputSchema: params.inputSchema,
386
408
  outputSchema: params.outputSchema,
387
409
  },
410
+ toolCallId,
388
411
  signal,
389
412
  );
390
413
  const result = {
@@ -455,11 +478,11 @@ export function registerCodeMcpTools(
455
478
  promptSnippet: "List or explicitly manage saved MCP chains",
456
479
  promptGuidelines: [...MANAGE_CHAIN_PROMPT_GUIDELINES],
457
480
  parameters: ManageChainsParameters,
458
- async execute(_toolCallId, params, signal, onUpdate) {
481
+ async execute(toolCallId, params, signal, onUpdate) {
459
482
  const action = params.action;
460
483
  let views: SavedChainView[];
461
484
  if (action === "list") {
462
- views = await chains.list(signal);
485
+ views = await chains.list(toolCallId, signal);
463
486
  } else {
464
487
  if (params.confirmedByUser !== true) {
465
488
  throw new Error(`${action} requires confirmedByUser=true after explicit user approval`);
@@ -476,13 +499,13 @@ export function registerCodeMcpTools(
476
499
  details: undefined,
477
500
  });
478
501
  if (action === "enable" || action === "disable") {
479
- await chains.setEnabled(params.name, scope, action === "enable", signal);
480
- views = await chains.list(signal);
502
+ await chains.setEnabled(params.name, scope, action === "enable", toolCallId, signal);
503
+ views = await chains.list(toolCallId, signal);
481
504
  } else if (action === "revalidate") {
482
- await chains.revalidate(params.name, scope, signal);
483
- views = await chains.list(signal);
505
+ await chains.revalidate(params.name, scope, toolCallId, signal);
506
+ views = await chains.list(toolCallId, signal);
484
507
  } else {
485
- views = await chains.delete(params.name, scope, signal);
508
+ views = await chains.delete(params.name, scope, toolCallId, signal);
486
509
  }
487
510
  }
488
511
  const result = {