march-cli 0.1.8 → 0.1.10
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/package.json +1 -1
- package/src/agent/editing/lsp-report.mjs +69 -0
- package/src/agent/file-edit-tool.mjs +10 -24
- package/src/agent/model-payload-dumper.mjs +11 -4
- package/src/agent/runner/runner-utils.mjs +18 -0
- package/src/agent/runner.mjs +29 -28
- package/src/agent/runtime/runner-runtime-host.mjs +2 -0
- package/src/agent/turn/turn-logging.mjs +30 -0
- package/src/agent/turn/turn-runner.mjs +40 -0
- package/src/cli/commands/status-command.mjs +45 -0
- package/src/cli/permissions.mjs +1 -1
- package/src/cli/startup/runtime-close.mjs +23 -0
- package/src/cli/status-line-updater.mjs +1 -0
- package/src/cli/tui/tool-rendering.mjs +1 -1
- package/src/config/loader.mjs +28 -1
- package/src/debug/logger.mjs +141 -0
- package/src/lsp/client.mjs +2 -2
- package/src/lsp/diagnostic-store.mjs +5 -2
- package/src/{context/diagnostics.mjs → lsp/diagnostics-format.mjs} +6 -4
- package/src/lsp/managed-node-server.mjs +94 -0
- package/src/lsp/path-match.mjs +10 -0
- package/src/lsp/servers.mjs +97 -21
- package/src/lsp/service.mjs +57 -12
- package/src/lsp/status-message.mjs +9 -0
- package/src/lsp/typescript-project-resolver.mjs +186 -0
- package/src/main.mjs +17 -24
- package/src/platform/spawn-command.mjs +27 -0
- package/src/provider/hosted-tools.mjs +111 -0
- package/src/web/tools.mjs +2 -2
package/package.json
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { formatLspDiagnosticsForPath } from "../../lsp/diagnostics-format.mjs";
|
|
2
|
+
import { sameLspPath } from "../../lsp/path-match.mjs";
|
|
3
|
+
|
|
4
|
+
export async function waitForLspReport({ lspService, path, lspResult, since = Date.now(), timeoutMs = 3000, intervalMs = 150 }) {
|
|
5
|
+
const immediate = formatLspResultMessage(lspResult);
|
|
6
|
+
if (!lspService?.snapshot || !path || lspResult?.status === "unsupported") return immediate;
|
|
7
|
+
if (lspResult?.status === "unavailable" || lspResult?.status === "failed") return immediate;
|
|
8
|
+
|
|
9
|
+
const deadline = Date.now() + timeoutMs;
|
|
10
|
+
for (;;) {
|
|
11
|
+
const snapshot = lspService.snapshot();
|
|
12
|
+
const diagnostics = formatCurrentLspDiagnosticsForPath({ snapshot, path, since });
|
|
13
|
+
if (diagnostics) return diagnostics;
|
|
14
|
+
if (hasCurrentDiagnosticPublish({ snapshot, path, since })) return formatNoLspDiagnostics({ snapshot });
|
|
15
|
+
|
|
16
|
+
const remaining = deadline - Date.now();
|
|
17
|
+
if (remaining <= 0) return formatLatestLspMessage({ snapshot, lspResult });
|
|
18
|
+
await sleep(Math.min(intervalMs, remaining));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatLspResultMessage(result, { timedOut = false } = {}) {
|
|
23
|
+
if (!result || result.status === "unsupported") return "";
|
|
24
|
+
if (result.status === "unavailable" || result.status === "failed") {
|
|
25
|
+
return `<lsp status="${result.status}" server="${result.id ?? "unknown"}">${result.reason ?? "unavailable"}</lsp>`;
|
|
26
|
+
}
|
|
27
|
+
if (result.status === "starting") {
|
|
28
|
+
const detail = timedOut ? "diagnostics still pending; server continues in background" : "diagnostics pending";
|
|
29
|
+
return `<lsp status="starting" server="${result.id}">${detail}</lsp>`;
|
|
30
|
+
}
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatCurrentLspDiagnosticsForPath({ snapshot, path, since }) {
|
|
35
|
+
const diagnostics = formatLspDiagnosticsForPath({ snapshot, path });
|
|
36
|
+
if (!diagnostics) return "";
|
|
37
|
+
if (!Array.isArray(snapshot?.files)) return diagnostics;
|
|
38
|
+
return hasCurrentDiagnosticPublish({ snapshot, path, since }) ? diagnostics : "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasCurrentDiagnosticPublish({ snapshot, path, since }) {
|
|
42
|
+
return (snapshot?.files ?? []).some((file) => sameLspPath(file.path, path) && (file.updatedAt ?? 0) >= since);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatNoLspDiagnostics({ snapshot }) {
|
|
46
|
+
const lines = ["[diagnostics]", "source: lsp"];
|
|
47
|
+
if (snapshot?.status) lines.push(`status: ${snapshot.status}`);
|
|
48
|
+
lines.push("summary: 0 diagnostics");
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatLatestLspMessage({ snapshot, lspResult }) {
|
|
53
|
+
const server = latestServerForResult(snapshot, lspResult);
|
|
54
|
+
if (server?.status === "failed" || server?.status === "unavailable") {
|
|
55
|
+
return formatLspResultMessage({ ...lspResult, ...server });
|
|
56
|
+
}
|
|
57
|
+
if (server?.status === "idle" || server?.status === "ready") return formatNoLspDiagnostics({ snapshot });
|
|
58
|
+
return formatLspResultMessage(lspResult, { timedOut: lspResult?.status === "starting" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function latestServerForResult(snapshot, lspResult) {
|
|
62
|
+
const servers = snapshot?.servers ?? [];
|
|
63
|
+
return servers.find((server) => server.id === lspResult?.id && server.root === lspResult?.root)
|
|
64
|
+
?? servers.find((server) => server.id === lspResult?.id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sleep(ms) {
|
|
68
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
69
|
+
}
|
|
@@ -5,7 +5,7 @@ import { Type } from "typebox";
|
|
|
5
5
|
import { toolText } from "./tool-result.mjs";
|
|
6
6
|
import { applyReplaceTextPatch, applyReplaceRangePatch } from "./editing/diff-apply.mjs";
|
|
7
7
|
import { formatAppliedDiff, formatDiff } from "./editing/diff-format.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import { waitForLspReport } from "./editing/lsp-report.mjs";
|
|
9
9
|
|
|
10
10
|
export { formatDiff } from "./editing/diff-format.mjs";
|
|
11
11
|
|
|
@@ -72,10 +72,11 @@ async function writeFullFile({ absPath, path, content, mode, engine, ui, lspServ
|
|
|
72
72
|
try {
|
|
73
73
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
74
74
|
writeFileSync(absPath, content, "utf8");
|
|
75
|
-
|
|
75
|
+
const lspTouchedAt = Date.now();
|
|
76
|
+
const lspResult = await lspService?.touchFile?.(absPath);
|
|
76
77
|
ui.editDiff(absPath, diffLines);
|
|
77
78
|
|
|
78
|
-
return await toolTextWithDiagnostics(`${mode === WRITE_MODE ? "Wrote" : "Overwrote"} ${path}\n\n${formatAppliedDiff([{ oldText, newText: content, startLine: 1 }])}`, { path: absPath }, { lspService, path: absPath });
|
|
79
|
+
return await toolTextWithDiagnostics(`${mode === WRITE_MODE ? "Wrote" : "Overwrote"} ${path}\n\n${formatAppliedDiff([{ oldText, newText: content, startLine: 1 }])}`, { path: absPath }, { lspService, path: absPath, lspResult, since: lspTouchedAt });
|
|
79
80
|
} catch (err) {
|
|
80
81
|
return toolText(`Error writing ${absPath}: ${err.message}`, { error: true });
|
|
81
82
|
}
|
|
@@ -100,33 +101,18 @@ async function patchFile({ absPath, path, edits, engine, ui, lspService }) {
|
|
|
100
101
|
try {
|
|
101
102
|
mkdirSync(dirname(absPath), { recursive: true });
|
|
102
103
|
writeFileSync(absPath, newContent, "utf8");
|
|
103
|
-
|
|
104
|
+
const lspTouchedAt = Date.now();
|
|
105
|
+
const lspResult = await lspService?.touchFile?.(absPath);
|
|
104
106
|
ui.editDiff(absPath, prepared.edits.flatMap((edit) => formatDiff(edit.oldText, edit.newText, { startLine: edit.startLine })));
|
|
105
|
-
return await toolTextWithDiagnostics(`Edited ${absPath}\n\n${formatAppliedDiff(prepared.edits)}`, { path: absPath, edits: prepared.edits.length }, { lspService, path: absPath });
|
|
107
|
+
return await toolTextWithDiagnostics(`Edited ${absPath}\n\n${formatAppliedDiff(prepared.edits)}`, { path: absPath, edits: prepared.edits.length }, { lspService, path: absPath, lspResult, since: lspTouchedAt });
|
|
106
108
|
} catch (err) {
|
|
107
109
|
return toolText(`Error writing ${absPath}: ${err.message}`, { error: true });
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
async function toolTextWithDiagnostics(text, details, { lspService, path, timeoutMs = 3000, intervalMs = 150 } = {}) {
|
|
112
|
-
const
|
|
113
|
-
return toolText(
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function waitForDiagnosticsForPath({ lspService, path, timeoutMs, intervalMs }) {
|
|
117
|
-
if (!lspService?.snapshot || !path) return "";
|
|
118
|
-
const deadline = Date.now() + timeoutMs;
|
|
119
|
-
for (;;) {
|
|
120
|
-
const diagnostics = buildDiagnosticsForPath({ snapshot: lspService.snapshot(), path });
|
|
121
|
-
if (diagnostics) return diagnostics;
|
|
122
|
-
const remaining = deadline - Date.now();
|
|
123
|
-
if (remaining <= 0) return "";
|
|
124
|
-
await sleep(Math.min(intervalMs, remaining));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function sleep(ms) {
|
|
129
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
113
|
+
async function toolTextWithDiagnostics(text, details, { lspService, path, lspResult, since = Date.now(), timeoutMs = 3000, intervalMs = 150 } = {}) {
|
|
114
|
+
const lspReport = await waitForLspReport({ lspService, path, lspResult, since, timeoutMs, intervalMs });
|
|
115
|
+
return toolText(lspReport ? `${text}\n\n${lspReport}` : text, details);
|
|
130
116
|
}
|
|
131
117
|
|
|
132
118
|
export function preparePatchEdits(content, edits, path = "file") {
|
|
@@ -170,24 +170,31 @@ function formatContentPart(part) {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
function formatToolSummary(tool) {
|
|
173
|
-
const name = tool?.function?.name ?? tool?.name ?? "unnamed_tool";
|
|
173
|
+
const name = tool?.function?.name ?? tool?.name ?? tool?.type ?? googleToolName(tool) ?? "unnamed_tool";
|
|
174
174
|
const description = tool?.function?.description ?? tool?.description ?? "";
|
|
175
175
|
return description ? `${name}: ${description}` : name;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
function extractPayloadTools(payload) {
|
|
179
|
+
|
|
179
180
|
if (!payload || typeof payload !== "object") return null;
|
|
180
181
|
if (Array.isArray(payload.tools)) return payload.tools;
|
|
181
|
-
if (
|
|
182
|
+
if (Array.isArray(payload.config?.tools)) return payload.config.tools;
|
|
183
|
+
if (payload.body && typeof payload.body === "object") return extractPayloadTools(payload.body);
|
|
182
184
|
if (typeof payload.body === "string") {
|
|
183
185
|
try {
|
|
184
|
-
|
|
185
|
-
if (Array.isArray(body.tools)) return body.tools;
|
|
186
|
+
return extractPayloadTools(JSON.parse(payload.body));
|
|
186
187
|
} catch {}
|
|
187
188
|
}
|
|
188
189
|
return null;
|
|
189
190
|
}
|
|
190
191
|
|
|
192
|
+
function googleToolName(tool) {
|
|
193
|
+
if (tool?.googleSearch) return "googleSearch";
|
|
194
|
+
if (tool?.googleSearchRetrieval) return "googleSearchRetrieval";
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
191
198
|
function textChars(value) {
|
|
192
199
|
if (value == null) return 0;
|
|
193
200
|
if (typeof value === "string") return value.length;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function providerContextToPayload(providerContext) {
|
|
2
|
+
return {
|
|
3
|
+
messages: [
|
|
4
|
+
{ role: "system", content: providerContext.system },
|
|
5
|
+
...(providerContext.userMessages ?? []).map((message) => ({ role: "user", content: message.content })),
|
|
6
|
+
],
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function notifyTurnEndBestEffort(turnNotifier, event) {
|
|
11
|
+
if (!turnNotifier?.notifyTurnEnd) return { ok: false, reason: "not-configured", results: [] };
|
|
12
|
+
try {
|
|
13
|
+
return await turnNotifier.notifyTurnEnd(event);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
// Notification must never change turn behavior.
|
|
16
|
+
return { ok: false, reason: err?.message ?? String(err), results: [] };
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/agent/runner.mjs
CHANGED
|
@@ -8,27 +8,29 @@ import { ContextEngine } from "../context/engine.mjs";
|
|
|
8
8
|
import { createMarchLifecycleAdapter } from "../extensions/lifecycle-adapter.mjs";
|
|
9
9
|
import { syncPiSessionSidecar } from "../session/sidecar-sync.mjs";
|
|
10
10
|
import { LspService } from "../lsp/service.mjs";
|
|
11
|
+
import { formatLspServiceEvent } from "../lsp/status-message.mjs";
|
|
11
12
|
import { formatRecallHints } from "../memory/markdown-store.mjs";
|
|
12
13
|
import { appendProviderUserMessage, estimateProviderPayloadTokens, installModelPayloadDumper, replaceProviderContextMessages } from "./model-payload-dumper.mjs";
|
|
13
14
|
import { resolveInitialModel, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
14
15
|
import { runRunnerCleanup } from "./runner/runner-cleanup.mjs";
|
|
15
16
|
import { createRunnerRuntimeHost } from "./runtime/runner-runtime-host.mjs";
|
|
16
17
|
import { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
18
|
+
import { notifyTurnEndBestEffort, providerContextToPayload } from "./runner/runner-utils.mjs";
|
|
17
19
|
import { resolveRunnerSessionOptions } from "./session/session-options.mjs";
|
|
18
20
|
import { createSessionBinding } from "./session/session-binding.mjs";
|
|
19
21
|
import { maybeAutoNameSession } from "./session/session-auto-name.mjs";
|
|
20
22
|
import { MARCH_BASE_TOOL_NAMES } from "./tool-names.mjs";
|
|
21
23
|
import { runRunnerTurn } from "./turn/turn-runner.mjs";
|
|
24
|
+
import { beginLoggedTurn } from "./turn/turn-logging.mjs";
|
|
22
25
|
import { appendFastVariants, createFastModelEntry, fromFastEntryModel, isFastProvider } from "./runner/fast-model.mjs";
|
|
23
26
|
import { registerSuperGrokProvider } from "../supergrok/provider.mjs";
|
|
24
27
|
import { registerCustomProviders } from "../provider/custom-provider.mjs";
|
|
25
|
-
|
|
28
|
+
import { injectHostedTools } from "../provider/hosted-tools.mjs";
|
|
26
29
|
export { MARCH_BASE_TOOL_NAMES };
|
|
27
30
|
export { installModelPayloadDumper } from "./model-payload-dumper.mjs";
|
|
28
31
|
export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
29
32
|
export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
30
|
-
|
|
31
|
-
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, centerMemoryPath = null, memoryStore = null, memoryTools = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null }) {
|
|
33
|
+
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, centerMemoryPath = null, memoryStore = null, memoryTools = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {} }) {
|
|
32
34
|
if (!useRuntimeHost && extensionPaths.length > 0) {
|
|
33
35
|
throw new Error("--extension requires the default pi runtime host path");
|
|
34
36
|
}
|
|
@@ -48,11 +50,11 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
48
50
|
compaction: { enabled: false },
|
|
49
51
|
retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
|
|
50
52
|
});
|
|
51
|
-
const lspService = new LspService({ cwd });
|
|
53
|
+
const lspService = new LspService({ cwd, onEvent: (event) => ui.status?.(formatLspServiceEvent(event)) });
|
|
52
54
|
const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, centerMemoryPath, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
|
|
53
55
|
const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
|
|
54
56
|
const sessionBinding = createSessionBinding(null);
|
|
55
|
-
let currentModelCallKind = "model";
|
|
57
|
+
let currentModelCallKind = "model", currentTurnId = null;
|
|
56
58
|
let currentPromptForContext = "";
|
|
57
59
|
let currentTurnContextMode = "rebuild";
|
|
58
60
|
let nextTurnContextMode = "rebuild";
|
|
@@ -69,9 +71,9 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
69
71
|
sessionManager: resolvedSessionManager, sessionBinding, engine, ui,
|
|
70
72
|
projectMarchDir,
|
|
71
73
|
memoryTools, memoryStore, shellRuntime, lspService, mcpTools, webTools,
|
|
72
|
-
permissionController, extensionPaths,
|
|
74
|
+
permissionController, extensionPaths, hostedTools,
|
|
73
75
|
onRebind: (session) => {
|
|
74
|
-
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind,
|
|
76
|
+
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
|
|
75
77
|
syncEngineSessionState(engine, session);
|
|
76
78
|
},
|
|
77
79
|
createAgentSessionRuntimeImpl,
|
|
@@ -90,7 +92,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
90
92
|
sessionManager: resolvedSessionManager, settingsManager,
|
|
91
93
|
});
|
|
92
94
|
sessionBinding.set(session);
|
|
93
|
-
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind,
|
|
95
|
+
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
|
|
94
96
|
}
|
|
95
97
|
syncEngineSessionState(engine, sessionBinding.get());
|
|
96
98
|
lifecycleAdapter = createMarchLifecycleAdapter({
|
|
@@ -114,11 +116,14 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
114
116
|
nextTurnContextMode = "rebuild";
|
|
115
117
|
pendingMidTurnRecallHints = [];
|
|
116
118
|
const turnStartedAt = Date.now();
|
|
119
|
+
const turnLog = beginLoggedTurn({ logger, engine, modelId, provider, contextMode, userMessage, userRecallHints, startedAt: turnStartedAt }); currentTurnId = turnLog.turnId;
|
|
117
120
|
try {
|
|
118
121
|
const result = await runRunnerTurn({
|
|
119
122
|
prompt, userMessage, options: { userRecallHints, currentProject },
|
|
120
123
|
sessionBinding, engine, ui, projectMarchDir, memoryStore,
|
|
121
124
|
setModelCallKind: (kind) => { currentModelCallKind = kind; },
|
|
125
|
+
logger: turnLog.logger,
|
|
126
|
+
setPhase: turnLog.setPhase,
|
|
122
127
|
onMidTurnRecallHints: (hints) => { pendingMidTurnRecallHints.push(...hints); },
|
|
123
128
|
syncCurrentPiSidecar,
|
|
124
129
|
autoNameSession,
|
|
@@ -130,6 +135,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
130
135
|
draft: result?.draft ?? "",
|
|
131
136
|
durationMs: Date.now() - turnStartedAt,
|
|
132
137
|
});
|
|
138
|
+
turnLog.endSuccess(result);
|
|
133
139
|
return result;
|
|
134
140
|
} catch (err) {
|
|
135
141
|
lastNotificationResult = await notifyTurnEndBestEffort(turnNotifier, {
|
|
@@ -138,8 +144,10 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
138
144
|
errorMessage: err?.message ?? String(err),
|
|
139
145
|
durationMs: Date.now() - turnStartedAt,
|
|
140
146
|
});
|
|
147
|
+
turnLog.endError(err);
|
|
141
148
|
throw err;
|
|
142
149
|
} finally {
|
|
150
|
+
currentTurnId = null;
|
|
143
151
|
currentTurnContextMode = "rebuild";
|
|
144
152
|
}
|
|
145
153
|
},
|
|
@@ -218,6 +226,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
218
226
|
},
|
|
219
227
|
getExtensionDiagnostics() { return runtimeHost?.getDiagnostics?.() ?? []; },
|
|
220
228
|
getExtensionLifecycleState() { return lifecycleAdapter.getState(); },
|
|
229
|
+
getLspStatus() { return lspService.snapshot(); },
|
|
221
230
|
async switchPiSession(sessionPath) {
|
|
222
231
|
if (!runtimeHost) throw new Error("pi runtime host is not enabled");
|
|
223
232
|
nextTurnContextMode = "rebuild";
|
|
@@ -263,11 +272,22 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
263
272
|
},
|
|
264
273
|
});
|
|
265
274
|
}
|
|
266
|
-
function
|
|
275
|
+
function onLoggedModelPayload(event) {
|
|
276
|
+
logger?.event("model.payload", {
|
|
277
|
+
kind: event.kind,
|
|
278
|
+
provider: event.model?.provider,
|
|
279
|
+
model: event.model?.id,
|
|
280
|
+
estimatedTokens: event.estimatedTokens,
|
|
281
|
+
turnId: currentTurnId,
|
|
282
|
+
});
|
|
283
|
+
onModelPayload?.(event);
|
|
284
|
+
}
|
|
285
|
+
function injectMarchSystemContext(payload, { kind, model } = {}) {
|
|
267
286
|
if (kind !== "user") return payload;
|
|
268
287
|
let nextPayload = currentTurnContextMode === "continueExistingPiTranscript"
|
|
269
288
|
? payload
|
|
270
289
|
: replaceProviderContextMessages(payload, engine.buildProviderContext(currentPromptForContext));
|
|
290
|
+
nextPayload = injectHostedTools(nextPayload, model, hostedTools);
|
|
271
291
|
if (_currentFastEntry) nextPayload = { ...nextPayload, service_tier: "priority" };
|
|
272
292
|
if (pendingMidTurnRecallHints.length > 0) {
|
|
273
293
|
nextPayload = appendProviderUserMessage(nextPayload, formatRecallHints("assistant", pendingMidTurnRecallHints));
|
|
@@ -276,22 +296,3 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
276
296
|
return nextPayload;
|
|
277
297
|
}
|
|
278
298
|
}
|
|
279
|
-
|
|
280
|
-
function providerContextToPayload(providerContext) {
|
|
281
|
-
return {
|
|
282
|
-
messages: [
|
|
283
|
-
{ role: "system", content: providerContext.system },
|
|
284
|
-
...(providerContext.userMessages ?? []).map((message) => ({ role: "user", content: message.content })),
|
|
285
|
-
],
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function notifyTurnEndBestEffort(turnNotifier, event) {
|
|
290
|
-
if (!turnNotifier?.notifyTurnEnd) return { ok: false, reason: "not-configured", results: [] };
|
|
291
|
-
try {
|
|
292
|
-
return await turnNotifier.notifyTurnEnd(event);
|
|
293
|
-
} catch (err) {
|
|
294
|
-
// Notification must never change turn behavior.
|
|
295
|
-
return { ok: false, reason: err?.message ?? String(err), results: [] };
|
|
296
|
-
}
|
|
297
|
-
}
|
|
@@ -26,6 +26,7 @@ export async function createRunnerRuntimeHost({
|
|
|
26
26
|
webTools = [],
|
|
27
27
|
permissionController = null,
|
|
28
28
|
extensionPaths = [],
|
|
29
|
+
hostedTools = {},
|
|
29
30
|
onRebind = null,
|
|
30
31
|
createAgentSessionRuntimeImpl = createAgentSessionRuntime,
|
|
31
32
|
createServices,
|
|
@@ -60,6 +61,7 @@ export async function createRunnerRuntimeHost({
|
|
|
60
61
|
permissionController,
|
|
61
62
|
authStorage,
|
|
62
63
|
projectMarchDir,
|
|
64
|
+
hostedTools,
|
|
63
65
|
});
|
|
64
66
|
},
|
|
65
67
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHeartbeat, formatError } from "../../debug/logger.mjs";
|
|
2
|
+
|
|
3
|
+
export function beginLoggedTurn({ logger, engine, modelId, provider, contextMode, userMessage, userRecallHints, startedAt = Date.now() } = {}) {
|
|
4
|
+
const turnId = `${startedAt.toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
5
|
+
let phase = "starting";
|
|
6
|
+
const turnLogger = logger?.child?.({ turnId, sessionName: engine?.sessionName, modelId, provider });
|
|
7
|
+
const heartbeat = createHeartbeat({
|
|
8
|
+
logger: turnLogger,
|
|
9
|
+
event: "turn.heartbeat",
|
|
10
|
+
getFields: () => ({ phase, elapsedMs: Date.now() - startedAt }),
|
|
11
|
+
});
|
|
12
|
+
turnLogger?.event("turn.start", {
|
|
13
|
+
userMessageLength: String(userMessage ?? "").length,
|
|
14
|
+
contextMode,
|
|
15
|
+
userRecallHintCount: userRecallHints?.length ?? 0,
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
turnId,
|
|
19
|
+
logger: turnLogger,
|
|
20
|
+
setPhase(value) { phase = value; },
|
|
21
|
+
endSuccess(result) {
|
|
22
|
+
heartbeat.stop();
|
|
23
|
+
turnLogger?.event("turn.end", { status: "success", durationMs: Date.now() - startedAt, draftLength: result?.draft?.length ?? 0 });
|
|
24
|
+
},
|
|
25
|
+
endError(err) {
|
|
26
|
+
heartbeat.stop();
|
|
27
|
+
turnLogger?.error("turn.error", { durationMs: Date.now() - startedAt, phase, error: formatError(err) });
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -11,6 +11,8 @@ export async function runRunnerTurn({
|
|
|
11
11
|
projectMarchDir,
|
|
12
12
|
memoryStore,
|
|
13
13
|
setModelCallKind,
|
|
14
|
+
logger = null,
|
|
15
|
+
setPhase = null,
|
|
14
16
|
onMidTurnRecallHints,
|
|
15
17
|
syncCurrentPiSidecar,
|
|
16
18
|
autoNameSession,
|
|
@@ -24,8 +26,16 @@ export async function runRunnerTurn({
|
|
|
24
26
|
const turnState = createTurnEventState();
|
|
25
27
|
const midTurnRecallHints = [];
|
|
26
28
|
ui.turnStart();
|
|
29
|
+
setPhase?.("subscribed");
|
|
30
|
+
logger?.event("turn.ui.start");
|
|
27
31
|
|
|
28
32
|
const unsubscribe = activeSession.subscribe((event) => {
|
|
33
|
+
logSessionEvent(logger, event);
|
|
34
|
+
if (event.type === "tool_execution_start") setPhase?.(`tool_running:${event.toolName ?? "unknown"}`);
|
|
35
|
+
if (event.type === "tool_execution_end") setPhase?.("model_streaming");
|
|
36
|
+
if (event.type === "auto_retry_start") setPhase?.("retry_wait");
|
|
37
|
+
if (event.type === "auto_retry_end") setPhase?.("model_streaming");
|
|
38
|
+
if (event.type === "message_update") setPhase?.("model_streaming");
|
|
29
39
|
handleRunnerSessionEvent(event, { ui, engine, state: turnState });
|
|
30
40
|
if (event.type === "tool_execution_start") {
|
|
31
41
|
const hints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
|
|
@@ -42,7 +52,10 @@ export async function runRunnerTurn({
|
|
|
42
52
|
text: userMessage ?? prompt,
|
|
43
53
|
projectMarchDir,
|
|
44
54
|
});
|
|
55
|
+
logger?.event("turn.attachments.resolved", { imageCount: attachmentReferences.images.length });
|
|
45
56
|
setModelCallKind("user");
|
|
57
|
+
setPhase?.("model_request");
|
|
58
|
+
logger?.event("model.prompt.start", { contextMode });
|
|
46
59
|
try {
|
|
47
60
|
if (contextMode === "rebuild") resetPiMessageHistory(activeSession);
|
|
48
61
|
await activeSession.prompt(
|
|
@@ -51,8 +64,10 @@ export async function runRunnerTurn({
|
|
|
51
64
|
);
|
|
52
65
|
} finally {
|
|
53
66
|
setModelCallKind("model");
|
|
67
|
+
logger?.event("model.prompt.end");
|
|
54
68
|
}
|
|
55
69
|
|
|
70
|
+
setPhase?.("finalizing");
|
|
56
71
|
finalizeTurn({
|
|
57
72
|
prompt,
|
|
58
73
|
userMessage,
|
|
@@ -68,11 +83,36 @@ export async function runRunnerTurn({
|
|
|
68
83
|
});
|
|
69
84
|
return { draft: turnState.draft };
|
|
70
85
|
} finally {
|
|
86
|
+
logger?.event("turn.ui.end");
|
|
71
87
|
ui.turnEnd();
|
|
72
88
|
unsubscribe();
|
|
73
89
|
}
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
function logSessionEvent(logger, event) {
|
|
93
|
+
if (!logger) return;
|
|
94
|
+
if (event.type === "message_update") {
|
|
95
|
+
const messageEvent = event.assistantMessageEvent;
|
|
96
|
+
logger.debug("session.event", {
|
|
97
|
+
type: event.type,
|
|
98
|
+
assistantMessageType: messageEvent?.type,
|
|
99
|
+
deltaLength: messageEvent?.delta?.length,
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
logger.event("session.event", {
|
|
104
|
+
type: event.type,
|
|
105
|
+
toolName: event.toolName,
|
|
106
|
+
isError: event.isError,
|
|
107
|
+
attempt: event.attempt,
|
|
108
|
+
maxAttempts: event.maxAttempts,
|
|
109
|
+
delayMs: event.delayMs,
|
|
110
|
+
success: event.success,
|
|
111
|
+
errorMessage: event.errorMessage,
|
|
112
|
+
finalError: event.finalError,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
76
116
|
function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession }) {
|
|
77
117
|
closeAssistantReply({ ui, state: turnState });
|
|
78
118
|
const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
|
|
@@ -31,6 +31,7 @@ export function statusBarLine({
|
|
|
31
31
|
mode = MODES.DO,
|
|
32
32
|
contextTokens = null,
|
|
33
33
|
activity = null,
|
|
34
|
+
lspStatus = null,
|
|
34
35
|
}) {
|
|
35
36
|
return formatStatusBarLine({
|
|
36
37
|
engine: runner.engine,
|
|
@@ -43,6 +44,7 @@ export function statusBarLine({
|
|
|
43
44
|
mode,
|
|
44
45
|
contextTokens,
|
|
45
46
|
activity,
|
|
47
|
+
lspStatus,
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -80,6 +82,7 @@ export function formatStatusBarLine({
|
|
|
80
82
|
mode = MODES.DO,
|
|
81
83
|
contextTokens = null,
|
|
82
84
|
activity = null,
|
|
85
|
+
lspStatus = null,
|
|
83
86
|
}) {
|
|
84
87
|
const model = engine.modelId || "model?";
|
|
85
88
|
const thinking = engine.thinkingLevel || "thinking?";
|
|
@@ -91,6 +94,8 @@ export function formatStatusBarLine({
|
|
|
91
94
|
const modeSegment = `${mode === MODES.DISCUSS ? WARN : OK}${formatModeLabel(mode)}`;
|
|
92
95
|
const runtime = `${C.cyan}${model}${DIM}·${thinking}`;
|
|
93
96
|
const segments = [modeSegment, runtime];
|
|
97
|
+
const lspText = formatLspSegment(lspStatus);
|
|
98
|
+
if (lspText) segments.push(`${C.fg250}${lspText}`);
|
|
94
99
|
const activityText = formatActivitySegment(activity);
|
|
95
100
|
if (activityText) segments.push(`${C.fg250}${activityText}`);
|
|
96
101
|
const compactTokens = formatCompactTokenCount(contextTokens);
|
|
@@ -100,6 +105,41 @@ export function formatStatusBarLine({
|
|
|
100
105
|
return `${inner}${R}`;
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
export function formatLspSegment(lspStatus) {
|
|
109
|
+
if (!lspStatus) return "";
|
|
110
|
+
const servers = lspStatus.servers ?? [];
|
|
111
|
+
const visible = servers.filter((server) => server.id);
|
|
112
|
+
if (visible.length === 0) return "lsp:off";
|
|
113
|
+
const parts = buildLspStatusParts(visible);
|
|
114
|
+
if (parts.length === 0) return "lsp:off";
|
|
115
|
+
return `lsp:${parts.join(",")}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function buildLspStatusParts(servers) {
|
|
119
|
+
const byId = new Map();
|
|
120
|
+
for (const server of servers) {
|
|
121
|
+
const id = shortLspId(server.id);
|
|
122
|
+
byId.set(id, mergeLspStatus(byId.get(id), server.status));
|
|
123
|
+
}
|
|
124
|
+
return [...byId.entries()]
|
|
125
|
+
.map(([id, status]) => `${id}${formatLspStatusMark(status)}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mergeLspStatus(current, next) {
|
|
129
|
+
const rank = { failed: 5, installing: 4, starting: 3, busy: 2, ready: 1, idle: 1, unavailable: 0 };
|
|
130
|
+
if (!current) return next ?? "unavailable";
|
|
131
|
+
const currentRank = rank[current] ?? 0;
|
|
132
|
+
const nextRank = rank[next] ?? 0;
|
|
133
|
+
return nextRank > currentRank ? next : current;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatLspStatusMark(status) {
|
|
137
|
+
if (status === "failed") return "!";
|
|
138
|
+
if (status === "starting" || status === "installing") return "…";
|
|
139
|
+
if (status === "unavailable") return "?";
|
|
140
|
+
return "✓";
|
|
141
|
+
}
|
|
142
|
+
|
|
103
143
|
function formatActivitySegment(activity) {
|
|
104
144
|
if (!activity) return "";
|
|
105
145
|
const label = String(activity.label ?? "").trim();
|
|
@@ -107,6 +147,11 @@ function formatActivitySegment(activity) {
|
|
|
107
147
|
return [frame, label].filter(Boolean).join(" ");
|
|
108
148
|
}
|
|
109
149
|
|
|
150
|
+
function shortLspId(id) {
|
|
151
|
+
if (id === "typescript") return "ts";
|
|
152
|
+
return String(id ?? "?");
|
|
153
|
+
}
|
|
154
|
+
|
|
110
155
|
export function formatCompactTokenCount(tokens) {
|
|
111
156
|
const value = Number(tokens);
|
|
112
157
|
if (!Number.isFinite(value) || value <= 0) return "";
|
package/src/cli/permissions.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const DEFAULT_CATEGORIES = {
|
|
|
40
40
|
terminal_search: PERM.COMMAND_EXEC,
|
|
41
41
|
terminal_read: PERM.COMMAND_EXEC,
|
|
42
42
|
terminal_snapshot: PERM.COMMAND_EXEC,
|
|
43
|
-
|
|
43
|
+
external_web_search: PERM.NETWORK_EXTERNAL,
|
|
44
44
|
web_fetch: PERM.NETWORK_EXTERNAL,
|
|
45
45
|
};
|
|
46
46
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export async function closeMarchRuntime({ runner, memoryStore, ui, logger = null, blankLine = false }) {
|
|
2
|
+
let firstError = null;
|
|
3
|
+
try {
|
|
4
|
+
await runner.dispose();
|
|
5
|
+
} catch (err) {
|
|
6
|
+
firstError ??= err;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
memoryStore.close();
|
|
10
|
+
} catch (err) {
|
|
11
|
+
firstError ??= err;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
if (blankLine) ui.writeln("");
|
|
15
|
+
await ui.close();
|
|
16
|
+
} catch (err) {
|
|
17
|
+
firstError ??= err;
|
|
18
|
+
}
|
|
19
|
+
if (firstError) {
|
|
20
|
+
logger?.error("process.close.error", { error: firstError });
|
|
21
|
+
throw firstError;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -55,7 +55,7 @@ export function formatToolStartLine(name, args = {}) {
|
|
|
55
55
|
if (name === "command_exec") return joinToolParts("◆", name, [compactText(args?.command ?? "")]);
|
|
56
56
|
if (name === "terminal_send") return joinToolParts("◆", name, [args?.shell_id, formatTerminalSendAction(args)]);
|
|
57
57
|
if (name?.startsWith?.("terminal_")) return joinToolParts("◆", name, [args?.shell_id, formatTerminalDetails(args)]);
|
|
58
|
-
if (name === "
|
|
58
|
+
if (name === "external_web_search") return joinToolParts("◆", name, [quoteCompact(args?.query ?? "")]);
|
|
59
59
|
if (name === "web_fetch") return joinToolParts("◆", name, [compactText(args?.url ?? "")]);
|
|
60
60
|
if (name === "context_stats") return joinToolParts("◆", name, []);
|
|
61
61
|
if (name === "read") {
|