march-cli 0.1.36 → 0.1.38
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/code-search/tool.mjs +1 -1
- package/src/agent/runner/runner-utils.mjs +20 -0
- package/src/agent/runner.mjs +16 -18
- package/src/agent/runtime/remote-ui-client.mjs +0 -1
- package/src/agent/runtime/runner-process-client.mjs +7 -0
- package/src/agent/runtime/runner-process-factory.mjs +7 -3
- package/src/agent/runtime/runner-runtime-host.mjs +2 -2
- package/src/agent/runtime/ui-event-bridge.mjs +0 -2
- package/src/agent/session/session-options.mjs +2 -2
- package/src/agent/tools.mjs +5 -23
- package/src/agent/turn/turn-events.mjs +41 -0
- package/src/agent/turn/turn-runner.mjs +5 -2
- package/src/cli/args.mjs +0 -3
- package/src/cli/commands/registry/slash-command-registry.mjs +2 -0
- package/src/cli/fallback-ui.mjs +0 -2
- package/src/cli/input/history-store.mjs +65 -3
- package/src/cli/input/mode-state.mjs +1 -1
- package/src/cli/repl-loop.mjs +75 -25
- package/src/cli/startup/app-runtime.mjs +72 -31
- package/src/cli/startup/create-runtime-runner.mjs +5 -46
- package/src/cli/startup/startup-session.mjs +3 -13
- package/src/cli/tui/input/history-navigation-controller.mjs +56 -0
- package/src/cli/turn/turn-input-preparer.mjs +0 -1
- package/src/cli/ui.mjs +9 -6
- package/src/cli/workspace/command.mjs +147 -0
- package/src/cli/workspace/output-router.mjs +108 -0
- package/src/cli/workspace/project-runtime.mjs +92 -0
- package/src/config/features.mjs +0 -1
- package/src/context/engine.mjs +4 -2
- package/src/context/system-core/base.md +4 -1
- package/src/extensions/lifecycle-adapter.mjs +1 -1
- package/src/history/runner.mjs +11 -0
- package/src/history/store.mjs +129 -0
- package/src/history/tool.mjs +39 -0
- package/src/lsp/client.mjs +12 -5
- package/src/lsp/service.mjs +15 -3
- package/src/main.mjs +5 -2
- package/src/notification/desktop-notifier.mjs +16 -8
- package/src/web-ui/command.mjs +2 -2
- package/src/web-ui/dist/assets/index-BQtl1uQs.css +1 -0
- package/src/web-ui/dist/assets/index-DrlJis_D.js +1845 -0
- package/src/web-ui/dist/index.html +13 -0
- package/src/web-ui/runtime-host.mjs +5 -25
- package/src/web-ui/session-manager.mjs +2 -2
- package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +2 -10
- package/src/web-ui/src/mockData.ts +1 -8
- package/src/web-ui/src/model.ts +0 -2
- package/src/web-ui/src/runtime/client.ts +0 -1
- package/src/web-ui/src/runtime/runtimeTimeline.ts +1 -3
- package/src/web-ui/src/styles/shell.css +1 -2
- package/src/web-ui/src/timelineAdapter.ts +1 -2
- package/src/workspace/project-id.mjs +14 -0
- package/src/workspace/project-registry.mjs +74 -0
- package/src/workspace/session-index.mjs +75 -0
- package/src/workspace/supervisor.mjs +172 -0
- package/src/cli/permissions.mjs +0 -103
- package/src/cli/tui/permission-request-ui.mjs +0 -18
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ export function createCodeSearchTool({ engine, stateRoot = null }) {
|
|
|
13
13
|
return defineTool({
|
|
14
14
|
name: "code_search",
|
|
15
15
|
label: "Code Search",
|
|
16
|
-
description: "Native code
|
|
16
|
+
description: "Native semantic/symbol code search over the workspace. Use it first for unknown entry points, cross-module flows, responsibility boundaries, and related implementations. Use grep/read afterward for exact confirmation before editing or making claims.",
|
|
17
17
|
parameters: Type.Object({
|
|
18
18
|
query: Type.Optional(Type.String({ description: "Natural-language or symbol query" })),
|
|
19
19
|
path: Type.Optional(Type.String({ description: "Relative or absolute workspace path to search; default current workspace" })),
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { installCodexTransportCompression } from "./codex-transport-compression.mjs";
|
|
2
|
+
import { installCodexWebSocketEventDebug } from "./codex-websocket-event-debug.mjs";
|
|
3
|
+
import { installCodexLargeContextGuard } from "./codex-large-context-guard.mjs";
|
|
4
|
+
|
|
5
|
+
export function installRunnerProcessGuards() {
|
|
6
|
+
installCodexLargeContextGuard();
|
|
7
|
+
installCodexTransportCompression();
|
|
8
|
+
installCodexWebSocketEventDebug();
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
export function providerContextToPayload(providerContext) {
|
|
2
12
|
return {
|
|
3
13
|
messages: [
|
|
@@ -22,3 +32,13 @@ export function notifyTurnEndDetached(turnNotifier, event, onResult = () => {})
|
|
|
22
32
|
pending.then(onResult, () => {});
|
|
23
33
|
return pending;
|
|
24
34
|
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
export function buildNotificationActivation({ notificationContext, sessionStats }) {
|
|
38
|
+
if (!notificationContext?.projectId) return null;
|
|
39
|
+
return {
|
|
40
|
+
type: "workspace-session",
|
|
41
|
+
projectId: notificationContext.projectId,
|
|
42
|
+
sessionId: sessionStats?.sessionId ?? null,
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/agent/runner.mjs
CHANGED
|
@@ -11,11 +11,9 @@ import { runRunnerCleanup } from "./runner/runner-cleanup.mjs";
|
|
|
11
11
|
import { createRunnerRuntimeHost } from "./runtime/runner-runtime-host.mjs";
|
|
12
12
|
import { createRuntimeUiBridge } from "./runtime/ui-event-bridge.mjs";
|
|
13
13
|
import { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
14
|
-
import { notifyTurnEndBestEffort, notifyTurnEndDetached, providerContextToPayload } from "./runner/runner-utils.mjs";
|
|
14
|
+
import { buildNotificationActivation, installRunnerProcessGuards, notifyTurnEndBestEffort, notifyTurnEndDetached, providerContextToPayload } from "./runner/runner-utils.mjs";
|
|
15
15
|
import { dumpCodexTransportDebug, getCodexTransportDebugSnapshot } from "./runner/codex-transport-debug.mjs";
|
|
16
|
-
import {
|
|
17
|
-
import { installCodexTransportCompression } from "./runner/codex-transport-compression.mjs";
|
|
18
|
-
import { applyCodexLargeContextGuardToPayload, installCodexLargeContextGuard } from "./runner/codex-large-context-guard.mjs";
|
|
16
|
+
import { applyCodexLargeContextGuardToPayload } from "./runner/codex-large-context-guard.mjs";
|
|
19
17
|
import { resolveRunnerSessionOptions } from "./session/session-options.mjs";
|
|
20
18
|
import { createSessionBinding } from "./session/session-binding.mjs";
|
|
21
19
|
import { maybeAutoNameSession } from "./session/session-auto-name.mjs";
|
|
@@ -28,19 +26,14 @@ import { registerCustomProviders } from "../provider/custom-provider.mjs";
|
|
|
28
26
|
import { injectHostedTools } from "../provider/hosted-tools.mjs";
|
|
29
27
|
import { createRunnerLifecycle } from "./lifecycle/runner-lifecycle.mjs";
|
|
30
28
|
import { createRunnerProviderQuotaRuntime } from "./runner/provider-quota-runtime.mjs";
|
|
29
|
+
import { appendRunnerTurnHistory, createRunnerHistoryStore } from "../history/runner.mjs";
|
|
31
30
|
export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
|
|
32
31
|
export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
33
32
|
export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
34
|
-
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], remoteMemorySources = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (!useRuntimeHost && extensionPaths.length > 0) {
|
|
39
|
-
throw new Error("--extension requires the default pi runtime host path");
|
|
40
|
-
}
|
|
41
|
-
const authConfig = authStorage
|
|
42
|
-
? { authStorage, hasAuth: true }
|
|
43
|
-
: createMarchAuthStorage({ provider: provider ?? "deepseek", providers, cwd });
|
|
33
|
+
export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], remoteMemorySources = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, onLspStatusChange = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {}, notificationContext = null }) {
|
|
34
|
+
installRunnerProcessGuards();
|
|
35
|
+
if (!useRuntimeHost && extensionPaths.length > 0) throw new Error("--extension requires the default pi runtime host path");
|
|
36
|
+
const authConfig = authStorage ? { authStorage, hasAuth: true } : createMarchAuthStorage({ provider: provider ?? "deepseek", providers, cwd });
|
|
44
37
|
if (!authConfig.hasAuth) throw new Error("No providers configured. Run: march provider --config");
|
|
45
38
|
const resolvedAuth = authConfig.authStorage;
|
|
46
39
|
const modelRegistry = ModelRegistry.create(resolvedAuth);
|
|
@@ -55,8 +48,9 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
55
48
|
retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
|
|
56
49
|
});
|
|
57
50
|
const { ui: runtimeUi, eventBus: runtimeUiEvents, detach: detachRuntimeUi } = createRuntimeUiBridge(ui);
|
|
58
|
-
const lspService = new LspService({ cwd, onEvent: (event) => runtimeUi.status?.(formatLspServiceEvent(event)) });
|
|
51
|
+
const lspService = new LspService({ cwd, onEvent: (event) => runtimeUi.status?.(formatLspServiceEvent(event)), onStatusChange: (event) => onLspStatusChange?.(event) });
|
|
59
52
|
const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, profilePaths, remoteMemorySources, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
|
|
53
|
+
const historyStore = createRunnerHistoryStore({ stateRoot, cwd });
|
|
60
54
|
const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
|
|
61
55
|
const sessionBinding = createSessionBinding(null);
|
|
62
56
|
let currentModelCallKind = "model", currentTurnId = null, currentPromptForContext = "";
|
|
@@ -72,8 +66,8 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
72
66
|
providers,
|
|
73
67
|
sessionManager: resolvedSessionManager, sessionBinding, engine, ui: runtimeUi,
|
|
74
68
|
projectMarchDir,
|
|
75
|
-
memoryTools, memoryStore, shellRuntime, lspService, mcpTools, webTools,
|
|
76
|
-
lifecycle,
|
|
69
|
+
memoryTools, memoryStore, historyStore, shellRuntime, lspService, mcpTools, webTools,
|
|
70
|
+
lifecycle, extensionPaths, hostedTools,
|
|
77
71
|
onRebind: (session) => {
|
|
78
72
|
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
|
|
79
73
|
syncEngineSessionState(engine, session);
|
|
@@ -85,7 +79,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
85
79
|
} else {
|
|
86
80
|
const sessionOptions = resolveRunnerSessionOptions({
|
|
87
81
|
cwd, stateRoot, provider, modelId, modelRegistry, engine, ui: runtimeUi,
|
|
88
|
-
memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle,
|
|
82
|
+
memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle,
|
|
89
83
|
authStorage: resolvedAuth, projectMarchDir,
|
|
90
84
|
getCurrentModel: () => sessionBinding.get()?.model ?? selectedModel,
|
|
91
85
|
});
|
|
@@ -134,12 +128,14 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
134
128
|
syncCurrentPiSidecar,
|
|
135
129
|
autoNameSession,
|
|
136
130
|
contextMode,
|
|
131
|
+
recordHistory: (turn) => appendRunnerTurnHistory({ store: historyStore, turn, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost), modelId: engine.modelId, provider: engine.provider }),
|
|
137
132
|
});
|
|
138
133
|
notifyTurnEndDetached(turnNotifier, {
|
|
139
134
|
status: "success",
|
|
140
135
|
sessionName: engine.sessionName,
|
|
141
136
|
draft: result?.draft ?? "",
|
|
142
137
|
durationMs: Date.now() - turnStartedAt,
|
|
138
|
+
activation: buildNotificationActivation({ notificationContext, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost) }),
|
|
143
139
|
}, (notificationResult) => { lastNotificationResult = notificationResult; });
|
|
144
140
|
const lifecycleAction = lifecycle.takePendingAction();
|
|
145
141
|
if (lifecycleAction) result.lifecycleAction = lifecycleAction;
|
|
@@ -151,6 +147,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
151
147
|
sessionName: engine.sessionName,
|
|
152
148
|
errorMessage: err?.message ?? String(err),
|
|
153
149
|
durationMs: Date.now() - turnStartedAt,
|
|
150
|
+
activation: buildNotificationActivation({ notificationContext, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost) }),
|
|
154
151
|
}, (notificationResult) => { lastNotificationResult = notificationResult; });
|
|
155
152
|
turnLog.endError(err);
|
|
156
153
|
throw err;
|
|
@@ -258,6 +255,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
258
255
|
]);
|
|
259
256
|
},
|
|
260
257
|
};
|
|
258
|
+
return runner;
|
|
261
259
|
function syncCurrentPiSidecar() {
|
|
262
260
|
return syncPiSessionSidecar({
|
|
263
261
|
enabled: syncPiSidecar, projectMarchDir, engine,
|
|
@@ -15,6 +15,5 @@ export function createRemoteRuntimeUiClient(peer) {
|
|
|
15
15
|
debugLines: (lines) => peer.notify("uiEvent", { type: "debug_lines", lines }),
|
|
16
16
|
recall: ({ source, hints }) => peer.notify("uiEvent", { type: "recall", source, hints }),
|
|
17
17
|
editDiff: (path, diffLines) => peer.notify("uiEvent", { type: "edit_diff", path, diffLines }),
|
|
18
|
-
requestPermission: (request) => peer.call("uiRequest", { type: "permission_request", ...request }),
|
|
19
18
|
};
|
|
20
19
|
}
|
|
@@ -10,6 +10,8 @@ export async function createRunnerProcessClient({
|
|
|
10
10
|
runnerOptions,
|
|
11
11
|
ui,
|
|
12
12
|
onModelPayload = null,
|
|
13
|
+
onLspStatusChange = null,
|
|
14
|
+
onNotificationActivation = null,
|
|
13
15
|
entry = fileURLToPath(DEFAULT_ENTRY),
|
|
14
16
|
forkImpl = fork,
|
|
15
17
|
timeoutMs = 0,
|
|
@@ -46,6 +48,11 @@ export async function createRunnerProcessClient({
|
|
|
46
48
|
target: {
|
|
47
49
|
...createRuntimeUiEventTarget(ui),
|
|
48
50
|
modelPayload: (event) => onModelPayload?.(event),
|
|
51
|
+
lspStatusChange: async (event) => {
|
|
52
|
+
await active?.runner?.refreshState?.();
|
|
53
|
+
onLspStatusChange?.(event);
|
|
54
|
+
},
|
|
55
|
+
notificationActivation: (activation) => onNotificationActivation?.(activation),
|
|
49
56
|
},
|
|
50
57
|
timeoutMs,
|
|
51
58
|
});
|
|
@@ -8,7 +8,6 @@ import { createMarkdownMemoryTools } from "../../memory/markdown-tools.mjs";
|
|
|
8
8
|
import { normalizeRemoteMemorySources } from "../../memory/remote/config.mjs";
|
|
9
9
|
import { initializeMcp } from "../../mcp/index.mjs";
|
|
10
10
|
import { createWebToolsFromConfig } from "../../web/tools.mjs";
|
|
11
|
-
import { createPermissionController } from "../../cli/permissions.mjs";
|
|
12
11
|
import { resolvePiSessionManager } from "../../session/pi-manager.mjs";
|
|
13
12
|
import { createModelContextDumper } from "../../debug/model-context-dumper.mjs";
|
|
14
13
|
import { createLogger, installProcessLogHandlers } from "../../debug/logger.mjs";
|
|
@@ -23,7 +22,6 @@ const DEFAULT_DEPS = {
|
|
|
23
22
|
createMarkdownMemoryTools,
|
|
24
23
|
initializeMcp,
|
|
25
24
|
createWebToolsFromConfig,
|
|
26
|
-
createPermissionController,
|
|
27
25
|
resolvePiSessionManager,
|
|
28
26
|
createModelContextDumper,
|
|
29
27
|
createMarchAuthStorage,
|
|
@@ -85,14 +83,16 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
|
|
|
85
83
|
maxTurns: options.config?.maxTurns ?? undefined,
|
|
86
84
|
trimBatch: options.config?.trimBatch ?? undefined,
|
|
87
85
|
hostedTools: options.config?.hostedTools,
|
|
88
|
-
|
|
86
|
+
notificationContext: options.notificationContext,
|
|
89
87
|
modelContextDumper: d.createModelContextDumper(options.modelContextDumper ?? { enabled: false }),
|
|
90
88
|
turnNotifier: d.createDesktopTurnNotifier({
|
|
91
89
|
enabled: Boolean(options.config?.notifications?.turnEnd),
|
|
92
90
|
config: options.config?.notifications,
|
|
91
|
+
onActivation: (activation) => d.peer.notify("notificationActivation", activation),
|
|
93
92
|
}),
|
|
94
93
|
logger,
|
|
95
94
|
onModelPayload: (event) => d.peer.notify("modelPayload", pickModelPayloadEvent(event)),
|
|
95
|
+
onLspStatusChange: (event) => d.peer.notify("lspStatusChange", pickLspStatusEvent(event)),
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
const originalDispose = runner.dispose;
|
|
@@ -109,3 +109,7 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
|
|
|
109
109
|
function pickModelPayloadEvent({ estimatedTokens, provider, model, kind, turnId } = {}) {
|
|
110
110
|
return { estimatedTokens, provider, model, kind, turnId };
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
function pickLspStatusEvent({ id, root, status, reason, managed } = {}) {
|
|
114
|
+
return { id, root, status, reason, managed };
|
|
115
|
+
}
|
|
@@ -20,12 +20,12 @@ export async function createRunnerRuntimeHost({
|
|
|
20
20
|
ui,
|
|
21
21
|
projectMarchDir = null,
|
|
22
22
|
memoryTools = [],
|
|
23
|
+
historyStore = null,
|
|
23
24
|
shellRuntime = null,
|
|
24
25
|
lspService = null,
|
|
25
26
|
mcpTools = [],
|
|
26
27
|
webTools = [],
|
|
27
28
|
lifecycle = null,
|
|
28
|
-
permissionController = null,
|
|
29
29
|
extensionPaths = [],
|
|
30
30
|
hostedTools = {},
|
|
31
31
|
onRebind = null,
|
|
@@ -56,12 +56,12 @@ export async function createRunnerRuntimeHost({
|
|
|
56
56
|
engine,
|
|
57
57
|
ui,
|
|
58
58
|
memoryTools,
|
|
59
|
+
historyStore,
|
|
59
60
|
shellRuntime,
|
|
60
61
|
lspService,
|
|
61
62
|
mcpTools,
|
|
62
63
|
webTools,
|
|
63
64
|
lifecycle,
|
|
64
|
-
permissionController,
|
|
65
65
|
authStorage,
|
|
66
66
|
projectMarchDir,
|
|
67
67
|
hostedTools,
|
|
@@ -53,7 +53,6 @@ export function createRuntimeUiClient(eventBus) {
|
|
|
53
53
|
recall: ({ source, hints }) => eventBus.emit({ type: "recall", source, hints }),
|
|
54
54
|
providerQuotaSnapshot: (snapshot) => eventBus.emit({ type: "provider_quota_snapshot", snapshot }),
|
|
55
55
|
editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
|
|
56
|
-
requestPermission: (request) => eventBus.request({ type: "permission_request", ...request }),
|
|
57
56
|
};
|
|
58
57
|
}
|
|
59
58
|
|
|
@@ -75,7 +74,6 @@ export function dispatchRuntimeUiEvent(ui, event) {
|
|
|
75
74
|
case "recall": return ui.recall?.({ source: event.source, hints: event.hints });
|
|
76
75
|
case "provider_quota_snapshot": return ui.providerQuotaSnapshot?.(event.snapshot);
|
|
77
76
|
case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
|
|
78
|
-
case "permission_request": return ui.requestPermission?.({ toolName: event.toolName, params: event.params, category: event.category });
|
|
79
77
|
default: return undefined;
|
|
80
78
|
}
|
|
81
79
|
}
|
|
@@ -10,12 +10,12 @@ export function resolveRunnerSessionOptions({
|
|
|
10
10
|
engine,
|
|
11
11
|
ui,
|
|
12
12
|
memoryTools = [],
|
|
13
|
+
historyStore = null,
|
|
13
14
|
shellRuntime = null,
|
|
14
15
|
lspService = null,
|
|
15
16
|
mcpTools = [],
|
|
16
17
|
webTools = [],
|
|
17
18
|
lifecycle = null,
|
|
18
|
-
permissionController = null,
|
|
19
19
|
authStorage = null,
|
|
20
20
|
projectMarchDir = null,
|
|
21
21
|
stateRoot = null,
|
|
@@ -31,7 +31,7 @@ export function resolveRunnerSessionOptions({
|
|
|
31
31
|
?? (provider && modelId ? getModel(provider, modelId) : null);
|
|
32
32
|
if (!model) throw new Error(`Model not found: ${provider}/${modelId}`);
|
|
33
33
|
|
|
34
|
-
const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle,
|
|
34
|
+
const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
|
|
35
35
|
const customToolNames = customTools.map((tool) => tool.name);
|
|
36
36
|
const tools = [
|
|
37
37
|
...customToolNames.filter((name) => name === "read"),
|
package/src/agent/tools.mjs
CHANGED
|
@@ -7,17 +7,18 @@ import { createReadImageTool } from "./file-tools/read-image-tool.mjs";
|
|
|
7
7
|
import { createSendBinaryTool } from "./output/send-binary-tool.mjs";
|
|
8
8
|
import { createScreenTool } from "./screen-tools/screen-tool.mjs";
|
|
9
9
|
import { createListWindowsTool } from "./screen-tools/list-windows-tool.mjs";
|
|
10
|
-
import { toolText } from "./tool-result.mjs";
|
|
11
10
|
import { createShellTools } from "../shell/tools.mjs";
|
|
12
11
|
import { initImageGen } from "../image-gen/index.mjs";
|
|
13
12
|
import { createSuperGrokTool } from "../supergrok/tool.mjs";
|
|
14
13
|
import { createBrowserTools } from "../browser/tools/index.mjs";
|
|
15
14
|
import { createRuntimeRestartTool } from "./lifecycle/runtime-restart-tool.mjs";
|
|
15
|
+
import { createHistorySearchTool } from "../history/tool.mjs";
|
|
16
16
|
|
|
17
|
-
export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null,
|
|
17
|
+
export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], historyStore = null, shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
|
|
18
18
|
const commandExecTool = createCommandExecTool({ cwd });
|
|
19
19
|
const codeSearchTool = createCodeSearchTool({ engine, stateRoot });
|
|
20
20
|
const contextStatsTool = createContextStatsTool({ engine });
|
|
21
|
+
const historySearchTool = createHistorySearchTool({ store: historyStore });
|
|
21
22
|
const editFileTool = createEditFileTool({ engine, ui, lspService });
|
|
22
23
|
const readFileTool = createReadFileTool({ engine });
|
|
23
24
|
const readImageTool = createReadImageTool({ engine, getCurrentModel });
|
|
@@ -36,6 +37,7 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
|
|
|
36
37
|
commandExecTool,
|
|
37
38
|
editFileTool,
|
|
38
39
|
...createShellTools(shellRuntime),
|
|
40
|
+
...(historySearchTool ? [historySearchTool] : []),
|
|
39
41
|
...memoryTools,
|
|
40
42
|
...mcpTools,
|
|
41
43
|
...webTools,
|
|
@@ -44,25 +46,5 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
|
|
|
44
46
|
...(authStorage ? [createSuperGrokTool({ authStorage, projectMarchDir })] : []),
|
|
45
47
|
...(authStorage ? initImageGen({ authStorage, projectMarchDir }) : []),
|
|
46
48
|
];
|
|
47
|
-
|
|
48
|
-
if (!permissionController) return tools;
|
|
49
|
-
|
|
50
|
-
return tools.map((tool) => {
|
|
51
|
-
const execute = tool.execute;
|
|
52
|
-
if (!execute) return tool;
|
|
53
|
-
const wrapped = async (toolCallId, params, signal, onUpdate) => {
|
|
54
|
-
const decision = await permissionController.requestApproval(
|
|
55
|
-
tool.name,
|
|
56
|
-
params,
|
|
57
|
-
ui.requestPermission
|
|
58
|
-
? (ctx) => ui.requestPermission(ctx)
|
|
59
|
-
: null,
|
|
60
|
-
);
|
|
61
|
-
if (decision.behavior === "deny") {
|
|
62
|
-
return toolText(`Permission denied: ${decision.message}`, { error: true, permissionDenied: true });
|
|
63
|
-
}
|
|
64
|
-
return execute(toolCallId, params, signal, onUpdate);
|
|
65
|
-
};
|
|
66
|
-
return { ...tool, execute: wrapped };
|
|
67
|
-
});
|
|
49
|
+
return tools;
|
|
68
50
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { formatToolStartLine, formatToolSuccessSummary } from "../tool-summary.mjs";
|
|
2
2
|
|
|
3
|
+
const TOOL_ERROR_EXCERPT_LIMIT = 4000;
|
|
4
|
+
|
|
3
5
|
export function createTurnEventState() {
|
|
4
6
|
return {
|
|
5
7
|
draft: "",
|
|
@@ -9,6 +11,7 @@ export function createTurnEventState() {
|
|
|
9
11
|
assistantReplyOpen: false,
|
|
10
12
|
assistantContextParts: [],
|
|
11
13
|
activeToolContextPart: null,
|
|
14
|
+
toolCalls: [],
|
|
12
15
|
};
|
|
13
16
|
}
|
|
14
17
|
|
|
@@ -19,10 +22,12 @@ export function handleRunnerSessionEvent(event, { ui, engine, state }) {
|
|
|
19
22
|
if (event.type === "tool_execution_start") {
|
|
20
23
|
closeAssistantReply({ ui, state });
|
|
21
24
|
appendToolStartContext(state, event.toolName, event.args);
|
|
25
|
+
recordToolStart(state, event.toolName, event.args);
|
|
22
26
|
ui.toolStart(event.toolName, event.args);
|
|
23
27
|
}
|
|
24
28
|
if (event.type === "tool_execution_end") {
|
|
25
29
|
updateToolEndContext(state, event.toolName, event.isError, event.result);
|
|
30
|
+
recordToolEnd(state, event.toolName, event.isError, event.result);
|
|
26
31
|
ui.toolEnd(event.toolName, event.isError, event.result);
|
|
27
32
|
}
|
|
28
33
|
if (event.type === "auto_retry_start") {
|
|
@@ -109,3 +114,39 @@ function updateToolEndContext(state, name, isError, result) {
|
|
|
109
114
|
if (summary && summary !== "done") part.text = `${part.text.trimEnd()} (${summary})\n`;
|
|
110
115
|
state.activeToolContextPart = null;
|
|
111
116
|
}
|
|
117
|
+
|
|
118
|
+
function recordToolStart(state, name, args) {
|
|
119
|
+
state.toolCalls.push({ name, args: cloneJson(args), status: "running" });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function recordToolEnd(state, name, isError, result) {
|
|
123
|
+
const call = [...state.toolCalls].reverse().find((item) => item.name === name && item.status === "running");
|
|
124
|
+
if (!call) return;
|
|
125
|
+
call.status = isError ? "failed" : "success";
|
|
126
|
+
if (!isError) return;
|
|
127
|
+
const output = extractToolOutput(result);
|
|
128
|
+
call.error = {
|
|
129
|
+
message: output.split(/\r?\n/).find(Boolean) ?? "Tool call failed",
|
|
130
|
+
details: cloneJson(result?.details ?? null),
|
|
131
|
+
excerpt: truncate(output, TOOL_ERROR_EXCERPT_LIMIT),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function extractToolOutput(result) {
|
|
136
|
+
const content = result?.content;
|
|
137
|
+
if (!Array.isArray(content)) return typeof result === "string" ? result : "";
|
|
138
|
+
return content.filter((item) => item?.type === "text").map((item) => item.text ?? "").join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function cloneJson(value) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function truncate(text, limit) {
|
|
150
|
+
const value = String(text ?? "");
|
|
151
|
+
return value.length > limit ? `${value.slice(0, limit)}\n[truncated]` : value;
|
|
152
|
+
}
|
|
@@ -17,6 +17,7 @@ export async function runRunnerTurn({
|
|
|
17
17
|
syncCurrentPiSidecar,
|
|
18
18
|
autoNameSession,
|
|
19
19
|
contextMode = "rebuild",
|
|
20
|
+
recordHistory = null,
|
|
20
21
|
}) {
|
|
21
22
|
const {
|
|
22
23
|
userRecallHints = [],
|
|
@@ -80,6 +81,7 @@ export async function runRunnerTurn({
|
|
|
80
81
|
midTurnRecallHints,
|
|
81
82
|
syncCurrentPiSidecar,
|
|
82
83
|
autoNameSession,
|
|
84
|
+
recordHistory,
|
|
83
85
|
});
|
|
84
86
|
return { draft: turnState.draft };
|
|
85
87
|
} finally {
|
|
@@ -127,19 +129,20 @@ function logSessionEvent(logger, event) {
|
|
|
127
129
|
});
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession }) {
|
|
132
|
+
function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession, recordHistory }) {
|
|
131
133
|
closeAssistantReply({ ui, state: turnState });
|
|
132
134
|
const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
|
|
133
135
|
engine.setPendingAssistantRecallHints?.(assistantRecallHints);
|
|
134
136
|
const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecallHints]);
|
|
135
137
|
|
|
136
|
-
engine.recordTurn({
|
|
138
|
+
const turn = engine.recordTurn({
|
|
137
139
|
userMessage: userMessage ?? prompt.slice(0, 300),
|
|
138
140
|
assistantMessage: turnState.draft,
|
|
139
141
|
assistantContext: compactAssistantContext(turnState),
|
|
140
142
|
userRecallHints,
|
|
141
143
|
assistantRecallHints: recordedAssistantRecallHints,
|
|
142
144
|
});
|
|
145
|
+
recordHistory?.({ ...turn, thinking: assistantThinkingText(turnState), toolCalls: turnState.toolCalls });
|
|
143
146
|
|
|
144
147
|
autoNameSession?.();
|
|
145
148
|
syncCurrentPiSidecar();
|
package/src/cli/args.mjs
CHANGED
|
@@ -17,7 +17,6 @@ export function parseCliArgs(argv) {
|
|
|
17
17
|
"pi-runtime-host": { type: "boolean" },
|
|
18
18
|
"shell-runtime": { type: "boolean" },
|
|
19
19
|
"no-shell-runtime": { type: "boolean" },
|
|
20
|
-
"permission-mode": { type: "string" },
|
|
21
20
|
host: { type: "string" },
|
|
22
21
|
port: { type: "string" },
|
|
23
22
|
"api-port": { type: "string" },
|
|
@@ -47,7 +46,6 @@ export function parseCliArgs(argv) {
|
|
|
47
46
|
piSessions: values["pi-sessions"] ?? false,
|
|
48
47
|
piRuntimeHost: values["pi-runtime-host"] ?? false,
|
|
49
48
|
shellRuntime: values["no-shell-runtime"] ? false : true,
|
|
50
|
-
permissionMode: values["permission-mode"] ?? "bypassPermissions",
|
|
51
49
|
host: values.host ?? null,
|
|
52
50
|
port: values.port ?? null,
|
|
53
51
|
apiPort: values["api-port"] ?? null,
|
|
@@ -97,7 +95,6 @@ Options:
|
|
|
97
95
|
--pi-runtime-host Force pi AgentSessionRuntime host path
|
|
98
96
|
--shell-runtime Enable interactive PTY shell tools (default)
|
|
99
97
|
--no-shell-runtime Disable interactive PTY shell tools and shell pane
|
|
100
|
-
--permission-mode <mode> Permission mode: default, bypassPermissions, dontAsk (default: bypassPermissions)
|
|
101
98
|
-e, --extension <path>
|
|
102
99
|
Load a pi extension path in the default runtime host (repeatable)
|
|
103
100
|
--host <host> With memory serve/web, bind host (default: 127.0.0.1)
|
|
@@ -12,6 +12,7 @@ import { handleSessionNameCommand, parseSessionNameCommand } from "../../session
|
|
|
12
12
|
import { handleShellCommand, parseShellCommand } from "../../shell/shell-command.mjs";
|
|
13
13
|
import { handleProviderCommand, parseProviderCommand } from "../provider-command.mjs";
|
|
14
14
|
import { handleModeCommand, parseModeCommand } from "../mode-command.mjs";
|
|
15
|
+
import { WORKSPACE_SLASH_COMMANDS } from "../../workspace/command.mjs";
|
|
15
16
|
|
|
16
17
|
export const SLASH_COMMANDS = [
|
|
17
18
|
exactCommand({
|
|
@@ -94,6 +95,7 @@ export const SLASH_COMMANDS = [
|
|
|
94
95
|
parse: parseThinkingCommand,
|
|
95
96
|
run: async (ctx, command) => writeLines(ctx.ui, await handleThinkingCommand(command, { runner: ctx.runner, ui: ctx.ui })),
|
|
96
97
|
}),
|
|
98
|
+
...WORKSPACE_SLASH_COMMANDS,
|
|
97
99
|
exactCommand({
|
|
98
100
|
name: "status",
|
|
99
101
|
description: "Show runtime status",
|
package/src/cli/fallback-ui.mjs
CHANGED
|
@@ -48,7 +48,6 @@ export function createJsonUI() {
|
|
|
48
48
|
editDiff: (path, diffLines) => {
|
|
49
49
|
stdout.write(JSON.stringify({ type: "edit_diff", path, diff: diffLines }) + "\n");
|
|
50
50
|
},
|
|
51
|
-
requestPermission: async () => true,
|
|
52
51
|
setEscapeHandler: () => {},
|
|
53
52
|
setCtrlCHandler: () => {},
|
|
54
53
|
setShiftTabHandler: () => {},
|
|
@@ -137,7 +136,6 @@ export function createPlainUI() {
|
|
|
137
136
|
else stdout.write(`${dim(` ${d.text}`)}\n`);
|
|
138
137
|
}
|
|
139
138
|
},
|
|
140
|
-
requestPermission: async () => true,
|
|
141
139
|
setEscapeHandler: () => {},
|
|
142
140
|
setCtrlCHandler: () => {},
|
|
143
141
|
setShiftTabHandler: () => {},
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
const HISTORY_VERSION = 1;
|
|
5
5
|
const MAX_HISTORY_ITEMS = 100;
|
|
6
|
+
const LOCK_WAIT_MS = 2000;
|
|
7
|
+
const LOCK_STALE_MS = 5000;
|
|
6
8
|
|
|
7
9
|
export function createInputHistoryStore({ path, maxItems = MAX_HISTORY_ITEMS } = {}) {
|
|
8
10
|
return {
|
|
@@ -20,8 +22,10 @@ export function createInputHistoryStore({ path, maxItems = MAX_HISTORY_ITEMS } =
|
|
|
20
22
|
save(items) {
|
|
21
23
|
if (!path) return;
|
|
22
24
|
const normalized = normalizeItems(items, maxItems);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
withHistoryLock(path, () => {
|
|
26
|
+
const merged = mergeItems(normalized, this.load(), maxItems);
|
|
27
|
+
writeHistoryFile(path, merged);
|
|
28
|
+
});
|
|
25
29
|
},
|
|
26
30
|
};
|
|
27
31
|
}
|
|
@@ -33,3 +37,61 @@ function normalizeItems(items, maxItems) {
|
|
|
33
37
|
.filter(Boolean)
|
|
34
38
|
.slice(0, maxItems);
|
|
35
39
|
}
|
|
40
|
+
|
|
41
|
+
function mergeItems(primaryItems, existingItems, maxItems) {
|
|
42
|
+
const merged = [];
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
for (const item of [...primaryItems, ...existingItems]) {
|
|
45
|
+
if (seen.has(item)) continue;
|
|
46
|
+
seen.add(item);
|
|
47
|
+
merged.push(item);
|
|
48
|
+
if (merged.length >= maxItems) break;
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeHistoryFile(path, items) {
|
|
54
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
55
|
+
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
56
|
+
const payload = `${JSON.stringify({ version: HISTORY_VERSION, items }, null, 2)}\n`;
|
|
57
|
+
writeFileSync(tmpPath, payload, "utf8");
|
|
58
|
+
renameSync(tmpPath, path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function withHistoryLock(path, fn) {
|
|
62
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
63
|
+
const lockPath = `${path}.lock`;
|
|
64
|
+
const fd = acquireLock(lockPath);
|
|
65
|
+
try {
|
|
66
|
+
return fn();
|
|
67
|
+
} finally {
|
|
68
|
+
closeSync(fd);
|
|
69
|
+
try { unlinkSync(lockPath); } catch {}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function acquireLock(lockPath) {
|
|
74
|
+
const start = Date.now();
|
|
75
|
+
while (true) {
|
|
76
|
+
try {
|
|
77
|
+
const fd = openSync(lockPath, "wx");
|
|
78
|
+
writeFileSync(fd, `${process.pid}\n`, "utf8");
|
|
79
|
+
return fd;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err?.code !== "EEXIST") throw err;
|
|
82
|
+
removeStaleLock(lockPath);
|
|
83
|
+
if (Date.now() - start >= LOCK_WAIT_MS) throw err;
|
|
84
|
+
sleepSync(25);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function removeStaleLock(lockPath) {
|
|
90
|
+
try {
|
|
91
|
+
if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) unlinkSync(lockPath);
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sleepSync(ms) {
|
|
96
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
97
|
+
}
|
|
@@ -34,7 +34,7 @@ export function formatModeReminder(mode = MODES.DO) {
|
|
|
34
34
|
"</mode>";
|
|
35
35
|
}
|
|
36
36
|
return "<mode>\n" +
|
|
37
|
-
"You are in do mode. You may implement changes when the user asks for execution, following normal
|
|
37
|
+
"You are in do mode. You may implement changes when the user asks for execution, following normal project rules.\n" +
|
|
38
38
|
"</mode>";
|
|
39
39
|
}
|
|
40
40
|
|