pi-codemcp 1.4.0 → 1.5.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.
- package/README.md +92 -196
- package/extensions/index.ts +63 -5
- package/package.json +3 -2
- package/sidecar/executor.py +14 -1
- package/sidecar/gateway.py +29 -5
- package/sidecar/settings.py +7 -0
- package/src/execution-rendering.ts +16 -0
- package/src/jev-router.ts +323 -0
- package/src/lifecycle.ts +8 -2
- package/src/mcp-client.ts +15 -1
- package/src/modal.ts +28 -5
- package/src/settings.ts +24 -4
- package/src/tools.ts +132 -0
package/sidecar/gateway.py
CHANGED
|
@@ -11,7 +11,7 @@ from contextlib import AsyncExitStack, asynccontextmanager, suppress
|
|
|
11
11
|
from typing import TYPE_CHECKING, Literal, NamedTuple, Protocol, cast
|
|
12
12
|
|
|
13
13
|
import pydantic_monty
|
|
14
|
-
from fastmcp import Client, FastMCP
|
|
14
|
+
from fastmcp import Client, Context, FastMCP
|
|
15
15
|
from fastmcp.mcp_config import RemoteMCPServer, StdioMCPServer
|
|
16
16
|
from pydantic_core import to_json
|
|
17
17
|
from rapidfuzz import fuzz, process
|
|
@@ -33,6 +33,7 @@ from .executor import (
|
|
|
33
33
|
ExecutionFailureInfo,
|
|
34
34
|
ExecutionResponse,
|
|
35
35
|
MontyExecutor,
|
|
36
|
+
ProgressReporter,
|
|
36
37
|
)
|
|
37
38
|
from .mcp_config import NormalizedConfig, load_mcp_json, normalize_mcp_config
|
|
38
39
|
from .models import (
|
|
@@ -601,15 +602,17 @@ class GatewayRuntime:
|
|
|
601
602
|
code: str,
|
|
602
603
|
trace_id: str,
|
|
603
604
|
input_ref: str | None = None,
|
|
605
|
+
progress: ProgressReporter | None = None,
|
|
604
606
|
) -> ExecutionResponse:
|
|
605
607
|
async with self._execute_lock:
|
|
606
|
-
return await self._execute(code, trace_id, input_ref)
|
|
608
|
+
return await self._execute(code, trace_id, input_ref, progress)
|
|
607
609
|
|
|
608
610
|
async def edit_execute(
|
|
609
611
|
self,
|
|
610
612
|
old_text: str,
|
|
611
613
|
new_text: str,
|
|
612
614
|
trace_id: str,
|
|
615
|
+
progress: ProgressReporter | None = None,
|
|
613
616
|
) -> ExecutionResponse:
|
|
614
617
|
async with self._execute_lock:
|
|
615
618
|
previous = self._last_execution
|
|
@@ -625,13 +628,14 @@ class GatewayRuntime:
|
|
|
625
628
|
f"old_text must match the previous code exactly once; found {matches} matches"
|
|
626
629
|
)
|
|
627
630
|
code = previous.code.replace(old_text, new_text, 1)
|
|
628
|
-
return await self._execute(code, trace_id, previous.input_ref)
|
|
631
|
+
return await self._execute(code, trace_id, previous.input_ref, progress)
|
|
629
632
|
|
|
630
633
|
async def _execute(
|
|
631
634
|
self,
|
|
632
635
|
code: str,
|
|
633
636
|
trace_id: str,
|
|
634
637
|
input_ref: str | None,
|
|
638
|
+
progress: ProgressReporter | None = None,
|
|
635
639
|
) -> ExecutionResponse:
|
|
636
640
|
self._last_execution = None
|
|
637
641
|
started = time.perf_counter()
|
|
@@ -682,6 +686,7 @@ class GatewayRuntime:
|
|
|
682
686
|
self._dispatch,
|
|
683
687
|
input_value=input_value,
|
|
684
688
|
retain_result=self.refinement_cache.retain,
|
|
689
|
+
progress=progress,
|
|
685
690
|
)
|
|
686
691
|
except BaseException as error:
|
|
687
692
|
cancelled = isinstance(error, asyncio.CancelledError)
|
|
@@ -1615,6 +1620,13 @@ def _require_runtime() -> GatewayRuntime:
|
|
|
1615
1620
|
return _runtime_state.runtime
|
|
1616
1621
|
|
|
1617
1622
|
|
|
1623
|
+
def _progress_reporter(ctx: Context) -> ProgressReporter:
|
|
1624
|
+
async def report(total_calls: int, message: str) -> None:
|
|
1625
|
+
await ctx.report_progress(progress=float(total_calls), message=message)
|
|
1626
|
+
|
|
1627
|
+
return report
|
|
1628
|
+
|
|
1629
|
+
|
|
1618
1630
|
def configure_runtime_paths(paths: RuntimePaths | None) -> None:
|
|
1619
1631
|
_runtime_state.paths = paths
|
|
1620
1632
|
|
|
@@ -1714,10 +1726,16 @@ async def set_chain_enabled(
|
|
|
1714
1726
|
async def execute(
|
|
1715
1727
|
trace_id: str,
|
|
1716
1728
|
code: str,
|
|
1729
|
+
ctx: Context,
|
|
1717
1730
|
input_ref: str | None = None,
|
|
1718
1731
|
) -> ExecutionResponse:
|
|
1719
1732
|
"""Type-check and run one sandboxed Python MCP SDK chain."""
|
|
1720
|
-
return await _require_runtime().execute(
|
|
1733
|
+
return await _require_runtime().execute(
|
|
1734
|
+
code,
|
|
1735
|
+
trace_id,
|
|
1736
|
+
input_ref,
|
|
1737
|
+
_progress_reporter(ctx),
|
|
1738
|
+
)
|
|
1721
1739
|
|
|
1722
1740
|
|
|
1723
1741
|
@mcp.tool
|
|
@@ -1725,9 +1743,15 @@ async def edit_execute(
|
|
|
1725
1743
|
trace_id: str,
|
|
1726
1744
|
old_text: str,
|
|
1727
1745
|
new_text: str,
|
|
1746
|
+
ctx: Context,
|
|
1728
1747
|
) -> ExecutionResponse:
|
|
1729
1748
|
"""Patch and rerun the last sandbox execution."""
|
|
1730
|
-
return await _require_runtime().edit_execute(
|
|
1749
|
+
return await _require_runtime().edit_execute(
|
|
1750
|
+
old_text,
|
|
1751
|
+
new_text,
|
|
1752
|
+
trace_id,
|
|
1753
|
+
_progress_reporter(ctx),
|
|
1754
|
+
)
|
|
1731
1755
|
|
|
1732
1756
|
|
|
1733
1757
|
@mcp.tool
|
package/sidecar/settings.py
CHANGED
|
@@ -26,6 +26,7 @@ class CodeMcpSettings(BaseModel):
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
version: Literal[2] = 2
|
|
29
|
+
jev_enabled: bool = False
|
|
29
30
|
background_warmup: bool = True
|
|
30
31
|
cache_ttl_hours: int = Field(default=24, ge=0, le=720)
|
|
31
32
|
execution_timeout_seconds: int = Field(default=30, ge=1, le=300)
|
|
@@ -60,4 +61,10 @@ def load_settings(path: Path) -> CodeMcpSettings:
|
|
|
60
61
|
if isinstance(value, dict) and value.get("version", 1) == 1:
|
|
61
62
|
value = {key: item for key, item in value.items() if key != "outputLineLimit"}
|
|
62
63
|
value["version"] = 2
|
|
64
|
+
if isinstance(value, dict) and "discoveryMode" in value:
|
|
65
|
+
mode = value.pop("discoveryMode")
|
|
66
|
+
if not isinstance(mode, str) or mode not in {"search", "jev"}:
|
|
67
|
+
value["discoveryMode"] = mode
|
|
68
|
+
else:
|
|
69
|
+
value.setdefault("jevEnabled", mode == "jev")
|
|
63
70
|
return CodeMcpSettings.model_validate(value)
|
|
@@ -16,6 +16,12 @@ interface RenderResult {
|
|
|
16
16
|
details?: unknown;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Details sent with partial updates while an execution is running. */
|
|
20
|
+
export interface ExecutionProgressDetails {
|
|
21
|
+
callsMade: number;
|
|
22
|
+
currentCall?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
interface ExecutionRendererOptions {
|
|
20
26
|
partialText?: string;
|
|
21
27
|
expandDescription?: string;
|
|
@@ -28,6 +34,16 @@ export function renderExecutionResult(
|
|
|
28
34
|
options: ExecutionRendererOptions = {},
|
|
29
35
|
): Text {
|
|
30
36
|
if (state.isPartial) {
|
|
37
|
+
const progress = result.details as ExecutionProgressDetails | undefined;
|
|
38
|
+
if (typeof progress?.callsMade === "number") {
|
|
39
|
+
const calls = progress.callsMade;
|
|
40
|
+
const current = progress.currentCall ? ` · ${progress.currentCall}` : "";
|
|
41
|
+
return new Text(
|
|
42
|
+
`\n${theme.fg("warning", `Executing · ${calls} MCP ${calls === 1 ? "call" : "calls"}${current}...`)}`,
|
|
43
|
+
0,
|
|
44
|
+
0,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
31
47
|
return new Text(
|
|
32
48
|
`\n${theme.fg("warning", options.partialText ?? "Preflight check, then execution...")}`,
|
|
33
49
|
0,
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { choice, noul, type Questions, type TypeSafeClient } from "@typesafe-ai/sdk";
|
|
3
|
+
import type { CodeMcpLifecycle } from "./lifecycle.js";
|
|
4
|
+
|
|
5
|
+
const TOOL_RELEVANCE_THRESHOLD = 0.65;
|
|
6
|
+
const NOUL_THRESHOLD = 0.5;
|
|
7
|
+
// ponytail: bound injected schemas; raise only if routing evals show recall loss.
|
|
8
|
+
const MAX_SELECTED_TOOLS = 8;
|
|
9
|
+
const CATALOG_PAGE_SIZE = 20;
|
|
10
|
+
const JEV_CHUNK_SIZE = 40;
|
|
11
|
+
|
|
12
|
+
type ToolRole = "source" | "enrichment" | "sink" | "standalone" | "unspecified";
|
|
13
|
+
const TOOL_ROLE_BY_CHOICE: Readonly<Record<string, ToolRole>> = {
|
|
14
|
+
source: "source",
|
|
15
|
+
enrichment: "enrichment",
|
|
16
|
+
sink: "sink",
|
|
17
|
+
standalone: "standalone",
|
|
18
|
+
irrelevant: "unspecified",
|
|
19
|
+
};
|
|
20
|
+
type WorkflowShape = "single_call" | "parallel" | "pipeline" | "mixed";
|
|
21
|
+
|
|
22
|
+
interface CatalogTool {
|
|
23
|
+
call: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface IndexedTool {
|
|
28
|
+
index: number;
|
|
29
|
+
tool: CatalogTool;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ChunkResult {
|
|
33
|
+
tools: JevSelectedTool[];
|
|
34
|
+
needsAnyTool?: number;
|
|
35
|
+
needsCheckpoint?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface JevSelectedTool extends CatalogTool {
|
|
39
|
+
relevance: number;
|
|
40
|
+
role: ToolRole;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface JevRoute {
|
|
44
|
+
prompt: string;
|
|
45
|
+
selected: JevSelectedTool[];
|
|
46
|
+
needsAnyTool: number;
|
|
47
|
+
workflowShape: WorkflowShape;
|
|
48
|
+
needsCheckpoint: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class JevRouter {
|
|
52
|
+
constructor(
|
|
53
|
+
private readonly lifecycle: Pick<CodeMcpLifecycle, "request">,
|
|
54
|
+
private readonly client: TypeSafeClient,
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
async route(task: string, recentContext = "", signal?: AbortSignal): Promise<JevRoute> {
|
|
58
|
+
const catalog = await this.loadCatalog(signal);
|
|
59
|
+
if (catalog.length === 0) return emptyRoute();
|
|
60
|
+
|
|
61
|
+
const chunks = chunk(
|
|
62
|
+
catalog.map((tool, index) => ({ tool, index })),
|
|
63
|
+
JEV_CHUNK_SIZE,
|
|
64
|
+
);
|
|
65
|
+
const results = await Promise.all(
|
|
66
|
+
chunks.map((tools, index) =>
|
|
67
|
+
this.evaluateChunk(task, recentContext, tools, index === 0, signal),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
const needsAnyTool = results[0]?.needsAnyTool ?? 0;
|
|
71
|
+
const needsCheckpoint = results[0]?.needsCheckpoint ?? 0;
|
|
72
|
+
const ranked = results
|
|
73
|
+
.flatMap((result) => result.tools)
|
|
74
|
+
.sort(
|
|
75
|
+
(left, right) => right.relevance - left.relevance || left.call.localeCompare(right.call),
|
|
76
|
+
);
|
|
77
|
+
const selected =
|
|
78
|
+
needsAnyTool >= NOUL_THRESHOLD
|
|
79
|
+
? ranked
|
|
80
|
+
.filter((tool) => tool.relevance >= TOOL_RELEVANCE_THRESHOLD)
|
|
81
|
+
.slice(0, MAX_SELECTED_TOOLS)
|
|
82
|
+
: [];
|
|
83
|
+
const workflowShape = workflowShapeFor(selected);
|
|
84
|
+
if (selected.length === 0) {
|
|
85
|
+
return { ...emptyRoute(), needsAnyTool, needsCheckpoint, workflowShape };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const inspection = await this.lifecycle.request(
|
|
89
|
+
"inspect",
|
|
90
|
+
{
|
|
91
|
+
calls: selected.map((tool) => tool.call),
|
|
92
|
+
trace_id: `jev-${randomUUID()}`,
|
|
93
|
+
},
|
|
94
|
+
signal,
|
|
95
|
+
);
|
|
96
|
+
const prelude = typeof inspection.prelude === "string" ? inspection.prelude : "";
|
|
97
|
+
const stubs = Array.isArray(inspection.results)
|
|
98
|
+
? inspection.results.flatMap((item) => {
|
|
99
|
+
if (!isRecord(item) || typeof item.stub !== "string") return [];
|
|
100
|
+
return [item.stub];
|
|
101
|
+
})
|
|
102
|
+
: [];
|
|
103
|
+
if (stubs.length !== selected.length) {
|
|
104
|
+
throw new Error("CodeMCP inspect returned incomplete Jev-selected contracts");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
prompt: jevPrompt(
|
|
109
|
+
selected,
|
|
110
|
+
workflowShape,
|
|
111
|
+
needsCheckpoint,
|
|
112
|
+
[prelude, ...stubs].filter(Boolean).join("\n\n"),
|
|
113
|
+
),
|
|
114
|
+
selected,
|
|
115
|
+
needsAnyTool,
|
|
116
|
+
workflowShape,
|
|
117
|
+
needsCheckpoint,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async evaluateChunk(
|
|
122
|
+
task: string,
|
|
123
|
+
recentContext: string,
|
|
124
|
+
tools: IndexedTool[],
|
|
125
|
+
includeTaskQuestions: boolean,
|
|
126
|
+
signal?: AbortSignal,
|
|
127
|
+
): Promise<ChunkResult> {
|
|
128
|
+
const questions: Questions = {};
|
|
129
|
+
if (includeTaskQuestions) {
|
|
130
|
+
questions.needs_any_tool = noul(
|
|
131
|
+
"Does the current task (the agent's routing intent) require at least one configured external-service or saved-workflow tool? Use the original user request as context, not as a requirement that every step be explicitly named.",
|
|
132
|
+
{
|
|
133
|
+
true: "The request needs current, private, or external state, or asks for an external action.",
|
|
134
|
+
false: "Explanation, reasoning, or local coding tools can fully satisfy the request.",
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
questions.needs_checkpoint = noul(
|
|
138
|
+
"For the current task, must the agent inspect an intermediate result, make a semantic decision, or obtain user approval before the next external call?",
|
|
139
|
+
{
|
|
140
|
+
true: "A model or user decision is required between tool stages.",
|
|
141
|
+
false: "One deterministic CodeMCP program can safely run the complete workflow.",
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
for (const { index, tool } of tools) {
|
|
146
|
+
const toolDescription = {
|
|
147
|
+
call: tool.call,
|
|
148
|
+
description: tool.description ?? tool.call,
|
|
149
|
+
};
|
|
150
|
+
questions[`tool_${index}`] = noul(
|
|
151
|
+
{
|
|
152
|
+
question:
|
|
153
|
+
"Is this tool needed for the current task (the agent's routing intent), including prerequisite discovery or diagnostic calls? The user need not explicitly name each step.",
|
|
154
|
+
tool: toolDescription,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
true: "The task needs this capability directly or as a prerequisite, such as finding a datasource before querying logs.",
|
|
158
|
+
false: "The tool is unrelated, redundant, optional, or merely adjacent.",
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
questions[`role_${index}`] = choice(
|
|
162
|
+
{
|
|
163
|
+
question: "What role should this tool have in the minimal workflow for the current task?",
|
|
164
|
+
tool: toolDescription,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
source: "Retrieves initial data.",
|
|
168
|
+
enrichment: "Retrieves data using an earlier result.",
|
|
169
|
+
sink: "Performs a downstream action using earlier results.",
|
|
170
|
+
standalone: "Independently completes one requested action.",
|
|
171
|
+
irrelevant: "Should not be used for this task.",
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const response = await this.client.systemOne(
|
|
177
|
+
{
|
|
178
|
+
state: {
|
|
179
|
+
task,
|
|
180
|
+
...(recentContext ? { recent_context: recentContext } : {}),
|
|
181
|
+
},
|
|
182
|
+
questions,
|
|
183
|
+
},
|
|
184
|
+
signal ? { signal } : undefined,
|
|
185
|
+
);
|
|
186
|
+
return {
|
|
187
|
+
tools: tools.map(({ index, tool }) => ({
|
|
188
|
+
...tool,
|
|
189
|
+
relevance: noulValue(response.answers[`tool_${index}`]),
|
|
190
|
+
role: toolRoleValue(response.answers[`role_${index}`]),
|
|
191
|
+
})),
|
|
192
|
+
...(includeTaskQuestions
|
|
193
|
+
? {
|
|
194
|
+
needsAnyTool: noulValue(response.answers.needs_any_tool),
|
|
195
|
+
needsCheckpoint: noulValue(response.answers.needs_checkpoint),
|
|
196
|
+
}
|
|
197
|
+
: {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async loadCatalog(signal?: AbortSignal): Promise<CatalogTool[]> {
|
|
202
|
+
const tools: CatalogTool[] = [];
|
|
203
|
+
let cursor = 0;
|
|
204
|
+
while (true) {
|
|
205
|
+
const page = await this.lifecycle.request(
|
|
206
|
+
"search",
|
|
207
|
+
{
|
|
208
|
+
mode: "inventory",
|
|
209
|
+
detail: "names",
|
|
210
|
+
limit: CATALOG_PAGE_SIZE,
|
|
211
|
+
cursor,
|
|
212
|
+
trace_id: `jev-${randomUUID()}`,
|
|
213
|
+
},
|
|
214
|
+
signal,
|
|
215
|
+
);
|
|
216
|
+
if (Array.isArray(page.results)) {
|
|
217
|
+
for (const item of page.results) {
|
|
218
|
+
if (!isRecord(item) || typeof item.call !== "string") continue;
|
|
219
|
+
tools.push({
|
|
220
|
+
call: item.call,
|
|
221
|
+
...(typeof item.description === "string" ? { description: item.description } : {}),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (typeof page.next_cursor !== "number") break;
|
|
226
|
+
if (page.next_cursor <= cursor) throw new Error("CodeMCP inventory cursor did not advance");
|
|
227
|
+
cursor = page.next_cursor;
|
|
228
|
+
}
|
|
229
|
+
return tools;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function emptyRoute(): JevRoute {
|
|
234
|
+
return {
|
|
235
|
+
prompt: "Jev found no configured MCP tool relevant to this request.",
|
|
236
|
+
selected: [],
|
|
237
|
+
needsAnyTool: 0,
|
|
238
|
+
workflowShape: "single_call",
|
|
239
|
+
needsCheckpoint: 0,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function workflowShapeFor(selected: JevSelectedTool[]): WorkflowShape {
|
|
244
|
+
if (selected.length <= 1) return "single_call";
|
|
245
|
+
const independent = selected.filter(
|
|
246
|
+
(tool) => tool.role === "source" || tool.role === "standalone",
|
|
247
|
+
).length;
|
|
248
|
+
const downstream = selected.some((tool) => tool.role === "enrichment" || tool.role === "sink");
|
|
249
|
+
if (!downstream) return "parallel";
|
|
250
|
+
return independent > 1 ? "mixed" : "pipeline";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function jevPrompt(
|
|
254
|
+
selected: JevSelectedTool[],
|
|
255
|
+
workflowShape: WorkflowShape,
|
|
256
|
+
needsCheckpoint: number,
|
|
257
|
+
contracts: string,
|
|
258
|
+
): string {
|
|
259
|
+
const roles = selected.map((tool) => `- ${tool.call}: ${tool.role}`).join("\n");
|
|
260
|
+
const execution =
|
|
261
|
+
needsCheckpoint >= NOUL_THRESHOLD
|
|
262
|
+
? "Run only the first stage in codemcp_execute, return compact decision data, then preserve a model/user checkpoint before downstream calls."
|
|
263
|
+
: workflowInstruction(workflowShape);
|
|
264
|
+
return [
|
|
265
|
+
`Jev selected these MCP calls:\n${roles}`,
|
|
266
|
+
`Composition: ${workflowShape}.`,
|
|
267
|
+
`Execution recommendation: ${execution}`,
|
|
268
|
+
"Use these exact typed SDK contracts:",
|
|
269
|
+
"```python",
|
|
270
|
+
contracts,
|
|
271
|
+
"```",
|
|
272
|
+
].join("\n\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function workflowInstruction(shape: WorkflowShape): string {
|
|
276
|
+
switch (shape) {
|
|
277
|
+
case "single_call":
|
|
278
|
+
return "Write and run one minimal codemcp_execute program using the selected call.";
|
|
279
|
+
case "parallel":
|
|
280
|
+
return "Write and run one codemcp_execute program using asyncio.gather for independent calls, then combine their results locally.";
|
|
281
|
+
case "pipeline":
|
|
282
|
+
return "Write and run one codemcp_execute program that passes earlier outputs into dependent calls.";
|
|
283
|
+
case "mixed":
|
|
284
|
+
return "Write and run one codemcp_execute program that gathers independent source calls, then feeds their results into downstream calls.";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function toolRoleValue(answer: unknown): ToolRole {
|
|
289
|
+
const role = TOOL_ROLE_BY_CHOICE[choiceValue(answer)];
|
|
290
|
+
if (!role) throw new Error("TypeSafe returned an invalid Jev tool role");
|
|
291
|
+
return role;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function choiceValue(answer: unknown): string {
|
|
295
|
+
if (!isRecord(answer) || answer.type !== "choice" || typeof answer.choice !== "string") {
|
|
296
|
+
throw new Error("TypeSafe returned an invalid Jev routing choice");
|
|
297
|
+
}
|
|
298
|
+
return answer.choice;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function noulValue(answer: unknown): number {
|
|
302
|
+
if (
|
|
303
|
+
!isRecord(answer) ||
|
|
304
|
+
answer.type !== "noul" ||
|
|
305
|
+
typeof answer.noul !== "number" ||
|
|
306
|
+
!Number.isFinite(answer.noul)
|
|
307
|
+
) {
|
|
308
|
+
throw new Error("TypeSafe returned an invalid Jev routing answer");
|
|
309
|
+
}
|
|
310
|
+
return answer.noul;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function chunk<T>(values: T[], size: number): T[][] {
|
|
314
|
+
const chunks: T[][] = [];
|
|
315
|
+
for (let index = 0; index < values.length; index += size) {
|
|
316
|
+
chunks.push(values.slice(index, index + size));
|
|
317
|
+
}
|
|
318
|
+
return chunks;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
322
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
323
|
+
}
|
package/src/lifecycle.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
SidecarClient,
|
|
3
|
+
type SidecarClientOptions,
|
|
4
|
+
type SidecarProgressCallback,
|
|
5
|
+
type SidecarToolName,
|
|
6
|
+
} from "./mcp-client.js";
|
|
2
7
|
import { type CodeMcpSettings, loadCodeMcpSettings } from "./settings.js";
|
|
3
8
|
|
|
4
9
|
export class CodeMcpLifecycle {
|
|
@@ -37,9 +42,10 @@ export class CodeMcpLifecycle {
|
|
|
37
42
|
name: SidecarToolName,
|
|
38
43
|
args: Record<string, unknown>,
|
|
39
44
|
signal?: AbortSignal,
|
|
45
|
+
onProgress?: SidecarProgressCallback,
|
|
40
46
|
): Promise<Record<string, unknown>> {
|
|
41
47
|
await this.reloadBarrier;
|
|
42
|
-
return this.sidecar.call(name, args, signal);
|
|
48
|
+
return this.sidecar.call(name, args, signal, onProgress);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
async warmup(): Promise<void> {
|
package/src/mcp-client.ts
CHANGED
|
@@ -66,6 +66,14 @@ export function sidecarToolTimeoutMs(
|
|
|
66
66
|
return DEFAULT_TOOL_TIMEOUT_MS;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
export interface SidecarProgress {
|
|
70
|
+
progress: number;
|
|
71
|
+
total?: number | undefined;
|
|
72
|
+
message?: string | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type SidecarProgressCallback = (progress: SidecarProgress) => void;
|
|
76
|
+
|
|
69
77
|
export interface SidecarClientOptions {
|
|
70
78
|
packageRoot?: string;
|
|
71
79
|
agentDir?: string;
|
|
@@ -153,7 +161,12 @@ export class SidecarClient {
|
|
|
153
161
|
return this.client !== undefined && this.transport?.pid !== null;
|
|
154
162
|
}
|
|
155
163
|
|
|
156
|
-
async call(
|
|
164
|
+
async call(
|
|
165
|
+
name: SidecarToolName,
|
|
166
|
+
args: JsonObject,
|
|
167
|
+
signal?: AbortSignal,
|
|
168
|
+
onProgress?: SidecarProgressCallback,
|
|
169
|
+
): Promise<JsonObject> {
|
|
157
170
|
await this.ensureStarted(signal);
|
|
158
171
|
const client = this.client;
|
|
159
172
|
if (!client) throw new Error("Sidecar client failed to initialize");
|
|
@@ -165,6 +178,7 @@ export class SidecarClient {
|
|
|
165
178
|
const result = await client.callTool({ name, arguments: args }, undefined, {
|
|
166
179
|
timeout,
|
|
167
180
|
...(signal === undefined ? {} : { signal }),
|
|
181
|
+
...(onProgress === undefined ? {} : { onprogress: onProgress }),
|
|
168
182
|
});
|
|
169
183
|
|
|
170
184
|
if (result.isError) {
|
package/src/modal.ts
CHANGED
|
@@ -13,7 +13,12 @@ import {
|
|
|
13
13
|
} from "@earendil-works/pi-tui";
|
|
14
14
|
import type { ChainScope, SavedChainView } from "./chains.js";
|
|
15
15
|
import { summarizeError } from "./errors.js";
|
|
16
|
-
import
|
|
16
|
+
import {
|
|
17
|
+
type CodeMcpSettings,
|
|
18
|
+
type EditableSettingKey,
|
|
19
|
+
type EditableSettingValue,
|
|
20
|
+
setEditableSetting,
|
|
21
|
+
} from "./settings.js";
|
|
17
22
|
|
|
18
23
|
export interface ToolModalState {
|
|
19
24
|
name: string;
|
|
@@ -136,13 +141,22 @@ const OVERLAY_OPTIONS = {
|
|
|
136
141
|
minWidth: 72,
|
|
137
142
|
maxHeight: "85%",
|
|
138
143
|
} as const;
|
|
139
|
-
|
|
140
144
|
const PROBLEM_REPORT_LABEL = "Extension is broken!";
|
|
141
145
|
const PROBLEM_REPORT_DESCRIPTION =
|
|
142
146
|
"Well, that sucks. With this button you can ask the agent to describe the problem and prepare a GitHub issue for review. The goal is to make pi-codemcp usable for everyone, don't be lazy - submit an issue. Don't worry, you will see all prompts, this is a transparent process.";
|
|
143
147
|
const PROBLEM_REPORT_SHORTCUT = "Report issue: R";
|
|
144
148
|
|
|
145
149
|
const SETTING_DEFINITIONS: SettingDefinition[] = [
|
|
150
|
+
{
|
|
151
|
+
key: "jevEnabled",
|
|
152
|
+
label: "Enable Jev",
|
|
153
|
+
description:
|
|
154
|
+
"Let the agent call Jev on demand to select and compose relevant MCP contracts instead of local search. Requires TYPESAFE_API_KEY and sends routed tasks and enabled tool descriptions to TypeSafe.",
|
|
155
|
+
choices: [
|
|
156
|
+
{ value: false, label: "false" },
|
|
157
|
+
{ value: true, label: "true" },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
146
160
|
{
|
|
147
161
|
key: "backgroundWarmup",
|
|
148
162
|
label: "Background warmup",
|
|
@@ -327,6 +341,7 @@ class ServerManagerModal implements Component, Focusable {
|
|
|
327
341
|
private selectedToolIndex = 0;
|
|
328
342
|
private selectedChainIndex = 0;
|
|
329
343
|
private selectedSettingIndex = 0;
|
|
344
|
+
private settingBusy = false;
|
|
330
345
|
private settingsError: string | undefined;
|
|
331
346
|
private _focused = false;
|
|
332
347
|
|
|
@@ -1005,8 +1020,9 @@ class ServerManagerModal implements Component, Focusable {
|
|
|
1005
1020
|
|
|
1006
1021
|
private cycleSelectedSetting(direction: -1 | 1): void {
|
|
1007
1022
|
const definition = SETTING_DEFINITIONS[this.selectedSettingIndex];
|
|
1008
|
-
if (!definition) return;
|
|
1009
|
-
const
|
|
1023
|
+
if (!definition || this.settingBusy) return;
|
|
1024
|
+
const previous = this.options.settings;
|
|
1025
|
+
const current = previous[definition.key];
|
|
1010
1026
|
const currentIndex = Math.max(
|
|
1011
1027
|
0,
|
|
1012
1028
|
definition.choices.findIndex((choice) => choice.value === current),
|
|
@@ -1014,16 +1030,23 @@ class ServerManagerModal implements Component, Focusable {
|
|
|
1014
1030
|
const choice =
|
|
1015
1031
|
definition.choices[cycleIndex(currentIndex, direction, definition.choices.length)];
|
|
1016
1032
|
if (!choice) return;
|
|
1033
|
+
this.settingBusy = true;
|
|
1017
1034
|
this.settingsError = undefined;
|
|
1035
|
+
this.options.settings = setEditableSetting(previous, definition.key, choice.value);
|
|
1036
|
+
this.requestRender();
|
|
1018
1037
|
void this.options
|
|
1019
1038
|
.onSetSetting(definition.key, choice.value)
|
|
1020
1039
|
.then((settings) => {
|
|
1021
1040
|
this.options.settings = settings;
|
|
1022
1041
|
})
|
|
1023
1042
|
.catch((error: unknown) => {
|
|
1043
|
+
this.options.settings = previous;
|
|
1024
1044
|
this.settingsError = summarizeError(error);
|
|
1025
1045
|
})
|
|
1026
|
-
.finally(() =>
|
|
1046
|
+
.finally(() => {
|
|
1047
|
+
this.settingBusy = false;
|
|
1048
|
+
this.requestRender();
|
|
1049
|
+
});
|
|
1027
1050
|
}
|
|
1028
1051
|
|
|
1029
1052
|
private focusProblemReport(): void {
|