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.
- package/README.md +5 -2
- package/extensions/index.ts +12 -8
- package/package.json +1 -1
- package/sidecar/cli.py +27 -7
- package/sidecar/executor.py +207 -35
- package/sidecar/gateway.py +470 -115
- package/sidecar/refinement_cache.py +154 -0
- package/sidecar/sandbox_api.py +44 -0
- package/sidecar/stats.py +801 -194
- package/sidecar/tool_catalog.py +1 -9
- package/src/chains.ts +53 -13
- package/src/mcp-client.ts +1 -0
- package/src/modal.ts +11 -0
- package/src/prompts.ts +3 -3
- package/src/tools.ts +37 -14
package/sidecar/tool_catalog.py
CHANGED
|
@@ -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(
|
|
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(
|
|
144
|
-
|
|
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(
|
|
152
|
-
|
|
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(
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
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
|
|
14
|
-
"SDK facades are prebound globals and must not be imported.
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 = {
|