march-cli 0.1.45 → 0.1.46
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/cache.mjs +13 -5
- package/src/agent/code-search/engine.mjs +7 -2
- package/src/agent/code-search/retrieval/safetensors.mjs +16 -10
- package/src/agent/code-search/scanner.mjs +11 -5
- package/src/agent/model-payload-dumper.mjs +1 -1
- package/src/agent/runner/payload/provider-payload-transform.mjs +59 -0
- package/src/agent/runner/recall/mid-turn-recall-bridge.mjs +23 -0
- package/src/agent/runner.mjs +28 -27
- package/src/agent/runtime/remote-ui-client.mjs +1 -1
- package/src/agent/runtime/resource/context-resource-loader.mjs +17 -0
- package/src/agent/runtime/runtime-factory.mjs +5 -1
- package/src/agent/runtime/state/runner-state.mjs +10 -3
- package/src/agent/runtime/ui-event-bridge.mjs +2 -2
- package/src/agent/turn/turn-runner.mjs +20 -16
- package/src/cli/fallback-ui.mjs +2 -2
- package/src/cli/repl-loop.mjs +5 -4
- package/src/cli/startup/app-runtime.mjs +2 -3
- package/src/cli/tui/input/mouse-selection-controller.mjs +19 -0
- package/src/cli/tui/output/selectable-copy.mjs +3 -3
- package/src/cli/tui/output-buffer.mjs +18 -0
- package/src/cli/tui/recall-rendering.mjs +30 -8
- package/src/cli/tui/selection/ansi-range.mjs +88 -0
- package/src/cli/tui/selection-screen.mjs +31 -99
- package/src/cli/turn/turn-input-preparer.mjs +9 -2
- package/src/cli/ui.mjs +2 -2
- package/src/context/engine.mjs +13 -3
- package/src/memory/markdown/semantic-preload.mjs +9 -0
- package/src/memory/markdown/semantic-recall.mjs +10 -6
- package/src/memory/markdown-store.mjs +19 -11
- package/src/web-ui/dist/assets/{index-CcbYCcWs.css → index-BG1Pxf1k.css} +1 -1
- package/src/web-ui/dist/assets/{index-CBYbNVgs.js → index-C0xOHlDz.js} +1 -1
- package/src/web-ui/dist/index.html +2 -2
- package/src/web-ui/runtime-host.mjs +15 -3
- package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +9 -6
- package/src/web-ui/src/model.ts +2 -2
- package/src/web-ui/src/runtime/client.ts +1 -1
- package/src/web-ui/src/runtime/runtimeTimeline.ts +2 -2
- package/src/web-ui/src/styles/shell.css +1 -0
- package/src/web-ui/src/timelineAdapter.ts +1 -1
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { chunkFile } from "./chunker.mjs";
|
|
4
|
+
import { readCodeFileContent } from "./scanner.mjs";
|
|
4
5
|
import { Bm25Index } from "./retrieval/bm25.mjs";
|
|
5
6
|
import { describeVectorizer } from "./retrieval/resilient-vectorizer.mjs";
|
|
6
7
|
import { LocalVectorIndex, defaultVectorizer } from "./retrieval/vector.mjs";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_MAX_FILE_ENTRIES = 8_000;
|
|
9
|
-
const DEFAULT_MAX_INDEX_ENTRIES =
|
|
10
|
+
const DEFAULT_MAX_INDEX_ENTRIES = 6;
|
|
10
11
|
|
|
11
12
|
export class CodeSearchIndexCache {
|
|
12
13
|
constructor({
|
|
@@ -25,7 +26,7 @@ export class CodeSearchIndexCache {
|
|
|
25
26
|
this.dirty = false;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
async build(files) {
|
|
29
|
+
async build(files, { includeVector = true } = {}) {
|
|
29
30
|
await this.load();
|
|
30
31
|
const chunks = [];
|
|
31
32
|
let reusedFiles = 0;
|
|
@@ -39,7 +40,9 @@ export class CodeSearchIndexCache {
|
|
|
39
40
|
reusedFiles += 1;
|
|
40
41
|
continue;
|
|
41
42
|
}
|
|
42
|
-
const
|
|
43
|
+
const content = await readCodeFileContent(file.absPath);
|
|
44
|
+
if (content === null) continue;
|
|
45
|
+
const fileChunks = await chunkFile({ ...file, content });
|
|
43
46
|
this.fileChunks.set(key, { signature, chunks: fileChunks });
|
|
44
47
|
this.dirty = true;
|
|
45
48
|
chunks.push(...fileChunks);
|
|
@@ -49,7 +52,7 @@ export class CodeSearchIndexCache {
|
|
|
49
52
|
this.pruneFileCache();
|
|
50
53
|
await this.persist();
|
|
51
54
|
|
|
52
|
-
const indexSignature =
|
|
55
|
+
const indexSignature = this.indexSignature(files, { includeVector });
|
|
53
56
|
const cachedIndex = this.indices.get(indexSignature);
|
|
54
57
|
if (cachedIndex) {
|
|
55
58
|
this.indices.delete(indexSignature);
|
|
@@ -59,13 +62,18 @@ export class CodeSearchIndexCache {
|
|
|
59
62
|
|
|
60
63
|
const index = {
|
|
61
64
|
lexical: new Bm25Index(chunks),
|
|
62
|
-
vector: await LocalVectorIndex.create(chunks, { vectorizer: this.vectorizer }),
|
|
65
|
+
vector: includeVector ? await LocalVectorIndex.create(chunks, { vectorizer: this.vectorizer }) : null,
|
|
63
66
|
};
|
|
64
67
|
this.indices.set(indexSignature, index);
|
|
65
68
|
this.pruneIndexCache();
|
|
66
69
|
return { chunks, index, reusedFiles, indexedFiles, reusedIndex: false, ...describeVectorizer(this.vectorizer) };
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
indexSignature(files, { includeVector }) {
|
|
73
|
+
const vectorKey = includeVector ? this.vectorizer.id : "lexical";
|
|
74
|
+
return [vectorKey, ...files.map(fileSignature)].join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
clear() {
|
|
70
78
|
this.fileChunks.clear();
|
|
71
79
|
this.indices.clear();
|
|
@@ -22,7 +22,7 @@ export async function searchCode(options = {}) {
|
|
|
22
22
|
if (!normalizedQuery && !related_to) return { results: [], stats: { files: 0, chunks: 0 } };
|
|
23
23
|
|
|
24
24
|
const files = await scanCodeFiles({ root, path });
|
|
25
|
-
const built = await activeCache.build(files);
|
|
25
|
+
const built = await activeCache.build(files, { includeVector: needsVectorIndex({ mode, related_to }) });
|
|
26
26
|
const related = related_to ? relatedQuery(built.chunks, related_to, normalizedQuery) : null;
|
|
27
27
|
const queryText = related?.query ?? normalizedQuery;
|
|
28
28
|
const retrieved = await retrieveChunks(built.index, queryText, mode);
|
|
@@ -38,7 +38,7 @@ export async function searchCode(options = {}) {
|
|
|
38
38
|
async function retrieveChunks(index, queryText, mode) {
|
|
39
39
|
const lexical = index.lexical.search(queryText, { limit: RETRIEVAL_LIMIT });
|
|
40
40
|
if (mode === "lexical" || mode === "symbol") return lexical;
|
|
41
|
-
const semantic = await index.vector.search(queryText, { limit: RETRIEVAL_LIMIT });
|
|
41
|
+
const semantic = index.vector ? await index.vector.search(queryText, { limit: RETRIEVAL_LIMIT }) : [];
|
|
42
42
|
if (mode === "semantic") return semantic;
|
|
43
43
|
return rrfFuse([
|
|
44
44
|
{ results: lexical, weight: 1.2 },
|
|
@@ -46,6 +46,11 @@ async function retrieveChunks(index, queryText, mode) {
|
|
|
46
46
|
], { limit: RETRIEVAL_LIMIT });
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function needsVectorIndex({ mode, related_to }) {
|
|
50
|
+
if (related_to) return true;
|
|
51
|
+
return mode !== "lexical" && mode !== "symbol";
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
function resultMode({ related_to, mode }) {
|
|
50
55
|
if (related_to) return "related";
|
|
51
56
|
if (mode === "symbol") return "symbol";
|
|
@@ -16,34 +16,40 @@ export async function readSafetensors(filePath) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function parseSafetensors(buffer) {
|
|
19
|
-
const
|
|
20
|
-
const headerLength = Number(new DataView(
|
|
19
|
+
const source = toByteSource(buffer);
|
|
20
|
+
const headerLength = Number(new DataView(source.buffer, source.byteOffset, HEADER_BYTES).getBigUint64(0, true));
|
|
21
21
|
const headerStart = HEADER_BYTES;
|
|
22
22
|
const headerEnd = headerStart + headerLength;
|
|
23
|
-
const headerJson = new TextDecoder().decode(new Uint8Array(
|
|
23
|
+
const headerJson = new TextDecoder().decode(new Uint8Array(source.buffer, source.byteOffset + headerStart, headerLength));
|
|
24
24
|
const header = JSON.parse(headerJson);
|
|
25
25
|
return {
|
|
26
26
|
names: Object.keys(header).filter((name) => name !== "__metadata__"),
|
|
27
27
|
getTensor(name) {
|
|
28
28
|
const descriptor = header[name];
|
|
29
29
|
if (!descriptor) throw new Error(`Missing safetensors tensor: ${name}`);
|
|
30
|
-
return readTensor(
|
|
30
|
+
return readTensor(source, headerEnd, descriptor);
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function readTensor(
|
|
35
|
+
function readTensor(source, dataStart, descriptor) {
|
|
36
36
|
const reader = DTYPE_READERS[descriptor.dtype];
|
|
37
37
|
if (!reader) throw new Error(`Unsupported safetensors dtype: ${descriptor.dtype}`);
|
|
38
38
|
const [start, end] = descriptor.data_offsets;
|
|
39
|
-
const byteOffset = dataStart + start;
|
|
40
|
-
const
|
|
41
|
-
const
|
|
39
|
+
const byteOffset = source.byteOffset + dataStart + start;
|
|
40
|
+
const byteLength = end - start;
|
|
41
|
+
const length = byteLength / reader.size;
|
|
42
|
+
if (descriptor.dtype === "F32" && byteOffset % Float32Array.BYTES_PER_ELEMENT === 0) {
|
|
43
|
+
return { values: new Float32Array(source.buffer, byteOffset, length), shape: descriptor.shape, dtype: descriptor.dtype };
|
|
44
|
+
}
|
|
45
|
+
const view = new DataView(source.buffer, byteOffset, byteLength);
|
|
42
46
|
const values = new Float32Array(length);
|
|
43
47
|
for (let index = 0; index < length; index += 1) values[index] = reader.read(view, index * reader.size);
|
|
44
48
|
return { values, shape: descriptor.shape, dtype: descriptor.dtype };
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
function
|
|
48
|
-
return buffer
|
|
51
|
+
function toByteSource(buffer) {
|
|
52
|
+
if (buffer instanceof ArrayBuffer) return { buffer, byteOffset: 0, byteLength: buffer.byteLength };
|
|
53
|
+
if (ArrayBuffer.isView(buffer)) return { buffer: buffer.buffer, byteOffset: buffer.byteOffset, byteLength: buffer.byteLength };
|
|
54
|
+
throw new TypeError("safetensors input must be an ArrayBuffer or typed array view");
|
|
49
55
|
}
|
|
@@ -26,13 +26,11 @@ export async function scanCodeFiles({ root, path = ".", maxFiles = DEFAULT_MAX_F
|
|
|
26
26
|
let info;
|
|
27
27
|
try { info = await stat(absPath); } catch { continue; }
|
|
28
28
|
if (!info.isFile() || info.size > maxFileBytes) continue;
|
|
29
|
-
|
|
30
|
-
if (content === null) continue;
|
|
29
|
+
if (await isSymlink(absPath)) continue;
|
|
31
30
|
files.push({
|
|
32
31
|
absPath,
|
|
33
32
|
relPath: relPath.replace(/\\/g, "/"),
|
|
34
33
|
language: languageForPath(relPath),
|
|
35
|
-
content,
|
|
36
34
|
size: info.size,
|
|
37
35
|
mtimeMs: info.mtimeMs,
|
|
38
36
|
});
|
|
@@ -40,6 +38,7 @@ export async function scanCodeFiles({ root, path = ".", maxFiles = DEFAULT_MAX_F
|
|
|
40
38
|
return files;
|
|
41
39
|
}
|
|
42
40
|
|
|
41
|
+
|
|
43
42
|
async function listCandidateFiles(root, base) {
|
|
44
43
|
const fromRipgrep = await listRipgrepFiles(root, base);
|
|
45
44
|
if (fromRipgrep) return fromRipgrep;
|
|
@@ -72,9 +71,8 @@ async function listFilesRecursively(root, dir) {
|
|
|
72
71
|
return files;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
async function
|
|
74
|
+
export async function readCodeFileContent(path) {
|
|
76
75
|
try {
|
|
77
|
-
if ((await lstat(path)).isSymbolicLink()) return null;
|
|
78
76
|
const buffer = await readFile(path);
|
|
79
77
|
if (buffer.includes(0)) return null;
|
|
80
78
|
return buffer.toString("utf8");
|
|
@@ -82,3 +80,11 @@ async function readUtf8Text(path) {
|
|
|
82
80
|
return null;
|
|
83
81
|
}
|
|
84
82
|
}
|
|
83
|
+
|
|
84
|
+
async function isSymlink(path) {
|
|
85
|
+
try {
|
|
86
|
+
return (await lstat(path)).isSymbolicLink();
|
|
87
|
+
} catch {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -14,7 +14,7 @@ export function installModelPayloadDumper(session, modelContextDumper, getKind =
|
|
|
14
14
|
const originalEffectivePayload = replacement === undefined ? payload : replacement;
|
|
15
15
|
const kind = getKind();
|
|
16
16
|
const effectivePayload = typeof transformPayload === "function"
|
|
17
|
-
? transformPayload(originalEffectivePayload, { kind, model })
|
|
17
|
+
? await transformPayload(originalEffectivePayload, { kind, model })
|
|
18
18
|
: originalEffectivePayload;
|
|
19
19
|
onModelPayload?.({
|
|
20
20
|
payload: effectivePayload,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { injectHostedTools } from "../../../provider/hosted-tools.mjs";
|
|
2
|
+
import { replaceProviderContextMessages } from "../../model-payload-dumper.mjs";
|
|
3
|
+
import { applyCodexLargeContextGuardToPayload } from "../codex-large-context-guard.mjs";
|
|
4
|
+
|
|
5
|
+
export function createRunnerProviderPayloadTransform({
|
|
6
|
+
engine,
|
|
7
|
+
sessionBinding,
|
|
8
|
+
hostedTools,
|
|
9
|
+
getCurrentPrompt,
|
|
10
|
+
getContextMode,
|
|
11
|
+
getFastEntry,
|
|
12
|
+
waitForMidTurnRecall = null,
|
|
13
|
+
getMidTurnRecallMessages = null,
|
|
14
|
+
}) {
|
|
15
|
+
let didReplaceProviderContext = false;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
resetTurn() {
|
|
19
|
+
didReplaceProviderContext = false;
|
|
20
|
+
},
|
|
21
|
+
async transform(payload, { kind, model } = {}) {
|
|
22
|
+
if (kind !== "user") return payload;
|
|
23
|
+
const shouldReplaceProviderContext = getContextMode() !== "continueExistingPiTranscript"
|
|
24
|
+
&& !didReplaceProviderContext;
|
|
25
|
+
let nextPayload = payload;
|
|
26
|
+
if (shouldReplaceProviderContext) {
|
|
27
|
+
nextPayload = replaceProviderContextMessages(payload, engine.buildProviderContext(getCurrentPrompt()));
|
|
28
|
+
didReplaceProviderContext = true;
|
|
29
|
+
} else {
|
|
30
|
+
await waitForMidTurnRecall?.();
|
|
31
|
+
nextPayload = appendMissingMidTurnRecallMessages(nextPayload, getMidTurnRecallMessages?.() ?? []);
|
|
32
|
+
}
|
|
33
|
+
nextPayload = injectHostedTools(nextPayload, model, hostedTools);
|
|
34
|
+
nextPayload = applyCodexLargeContextGuardToPayload(nextPayload, { model, session: sessionBinding.get() });
|
|
35
|
+
if (getFastEntry()) nextPayload = { ...nextPayload, service_tier: "priority" };
|
|
36
|
+
return nextPayload;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function appendMissingMidTurnRecallMessages(payload, recallMessages) {
|
|
42
|
+
if (!Array.isArray(payload?.messages) || recallMessages.length === 0) return payload;
|
|
43
|
+
const existingText = payload.messages.map((message) => providerMessageText(message)).join("\n");
|
|
44
|
+
const missing = recallMessages.filter((content) => content && !existingText.includes(content));
|
|
45
|
+
if (missing.length === 0) return payload;
|
|
46
|
+
return {
|
|
47
|
+
...payload,
|
|
48
|
+
messages: [
|
|
49
|
+
...payload.messages,
|
|
50
|
+
...missing.map((content) => ({ role: "user", content })),
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function providerMessageText(message) {
|
|
56
|
+
if (typeof message?.content === "string") return message.content;
|
|
57
|
+
if (Array.isArray(message?.content)) return message.content.map((part) => part?.text ?? "").join("");
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function createMidTurnRecallBridge() {
|
|
2
|
+
const messages = [];
|
|
3
|
+
const tasks = new Set();
|
|
4
|
+
return {
|
|
5
|
+
reset() {
|
|
6
|
+
messages.length = 0;
|
|
7
|
+
tasks.clear();
|
|
8
|
+
},
|
|
9
|
+
track({ content, task } = {}) {
|
|
10
|
+
if (content && !messages.includes(content)) messages.push(content);
|
|
11
|
+
if (!task?.finally) return;
|
|
12
|
+
tasks.add(task);
|
|
13
|
+
task.finally(() => tasks.delete(task));
|
|
14
|
+
},
|
|
15
|
+
async wait() {
|
|
16
|
+
if (tasks.size === 0) return;
|
|
17
|
+
await Promise.allSettled([...tasks]);
|
|
18
|
+
},
|
|
19
|
+
messages() {
|
|
20
|
+
return [...messages];
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
package/src/agent/runner.mjs
CHANGED
|
@@ -5,15 +5,17 @@ import { createMarchLifecycleAdapter } from "../extensions/lifecycle-adapter.mjs
|
|
|
5
5
|
import { syncMarchSessionState } from "../session/state/march-session-sync.mjs";
|
|
6
6
|
import { LspService } from "../lsp/service.mjs";
|
|
7
7
|
import { formatLspServiceEvent } from "../lsp/status-message.mjs";
|
|
8
|
-
import { estimateProviderPayloadTokens, installModelPayloadDumper
|
|
8
|
+
import { estimateProviderPayloadTokens, installModelPayloadDumper } from "./model-payload-dumper.mjs";
|
|
9
9
|
import { resolveInitialModel, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
10
10
|
import { runRunnerCleanup } from "./runner/runner-cleanup.mjs";
|
|
11
11
|
import { createRunnerRuntimeHost } from "./runtime/runner-runtime-host.mjs";
|
|
12
|
+
import { createMarchPiResourceLoader } from "./runtime/resource/context-resource-loader.mjs";
|
|
12
13
|
import { createRuntimeUiBridge } from "./runtime/ui-event-bridge.mjs";
|
|
13
14
|
import { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
14
15
|
import { buildNotificationActivation, installRunnerProcessGuards, notifyTurnEndBestEffort, notifyTurnEndDetached, providerContextToPayload } from "./runner/runner-utils.mjs";
|
|
15
16
|
import { dumpCodexTransportDebug, getCodexTransportDebugSnapshot } from "./runner/codex-transport-debug.mjs";
|
|
16
|
-
import {
|
|
17
|
+
import { createRunnerProviderPayloadTransform } from "./runner/payload/provider-payload-transform.mjs";
|
|
18
|
+
import { createMidTurnRecallBridge } from "./runner/recall/mid-turn-recall-bridge.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";
|
|
@@ -23,13 +25,11 @@ import { beginLoggedTurn } from "./turn/turn-logging.mjs";
|
|
|
23
25
|
import { appendFastVariants, createFastModelEntry, fromFastEntryModel, isFastProvider } from "./runner/fast-model.mjs";
|
|
24
26
|
import { registerSuperGrokProvider } from "../supergrok/provider.mjs";
|
|
25
27
|
import { registerCustomProviders } from "../provider/custom-provider.mjs";
|
|
26
|
-
import { injectHostedTools } from "../provider/hosted-tools.mjs";
|
|
27
28
|
import { createRunnerLifecycle } from "./lifecycle/runner-lifecycle.mjs";
|
|
28
29
|
import { createRunnerProviderQuotaRuntime } from "./runner/provider-quota-runtime.mjs";
|
|
29
30
|
import { appendRunnerTurnHistory, createRunnerHistoryStore } from "../history/runner.mjs";
|
|
30
31
|
export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
|
|
31
|
-
export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
|
|
32
|
-
export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
32
|
+
export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs"; export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
|
|
33
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, syncMarchSessionState: syncMarchSessionStateEnabled = false, syncPiSidecar = syncMarchSessionStateEnabled, 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
34
|
installRunnerProcessGuards();
|
|
35
35
|
if (!useRuntimeHost && extensionPaths.length > 0) throw new Error("--extension requires the default pi runtime host path");
|
|
@@ -43,10 +43,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
43
43
|
if (!selectedModel) throw new Error("No authenticated models available. Run: march provider --config");
|
|
44
44
|
provider = selectedModel.provider;
|
|
45
45
|
modelId = selectedModel.id;
|
|
46
|
-
const settingsManager = SettingsManager.inMemory({
|
|
47
|
-
compaction: { enabled: false },
|
|
48
|
-
retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
|
|
49
|
-
});
|
|
46
|
+
const settingsManager = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000, provider: { timeoutMs: 20000, maxRetries: 3, maxRetryDelayMs: 60000 } } });
|
|
50
47
|
const { ui: runtimeUi, eventBus: runtimeUiEvents, detach: detachRuntimeUi } = createRuntimeUiBridge(ui);
|
|
51
48
|
const lspService = new LspService({ cwd, onEvent: (event) => runtimeUi.status?.(formatLspServiceEvent(event)), onStatusChange: (event) => onLspStatusChange?.(event) });
|
|
52
49
|
const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, profilePaths, remoteMemorySources, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
|
|
@@ -54,11 +51,18 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
54
51
|
const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
|
|
55
52
|
const sessionBinding = createSessionBinding(null);
|
|
56
53
|
let currentModelCallKind = "model", currentTurnId = null, currentPromptForContext = "";
|
|
54
|
+
const midTurnRecallBridge = createMidTurnRecallBridge();
|
|
57
55
|
const lifecycle = createRunnerLifecycle();
|
|
58
|
-
let currentTurnContextMode = "rebuild";
|
|
59
|
-
let
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
let currentTurnContextMode = "rebuild", nextTurnContextMode = "rebuild";
|
|
57
|
+
let lastNotificationResult = null, runtimeHost = null, lifecycleAdapter = null, _currentFastEntry = null;
|
|
58
|
+
const providerPayloadTransform = createRunnerProviderPayloadTransform({
|
|
59
|
+
engine, sessionBinding, hostedTools,
|
|
60
|
+
getCurrentPrompt: () => currentPromptForContext,
|
|
61
|
+
getContextMode: () => currentTurnContextMode,
|
|
62
|
+
getFastEntry: () => _currentFastEntry,
|
|
63
|
+
waitForMidTurnRecall: () => midTurnRecallBridge.wait(),
|
|
64
|
+
getMidTurnRecallMessages: () => midTurnRecallBridge.messages(),
|
|
65
|
+
});
|
|
62
66
|
if (useRuntimeHost) {
|
|
63
67
|
runtimeHost = await createRunnerRuntimeHost({
|
|
64
68
|
cwd, stateRoot, provider, modelId,
|
|
@@ -69,7 +73,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
69
73
|
memoryTools, memoryStore, historyStore, shellRuntime, lspService, mcpTools, webTools,
|
|
70
74
|
lifecycle, extensionPaths, hostedTools,
|
|
71
75
|
onRebind: (session) => {
|
|
72
|
-
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload,
|
|
76
|
+
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, providerPayloadTransform.transform);
|
|
73
77
|
syncEngineSessionState(engine, session);
|
|
74
78
|
},
|
|
75
79
|
createAgentSessionRuntimeImpl,
|
|
@@ -83,13 +87,18 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
83
87
|
authStorage: resolvedAuth, projectMarchDir,
|
|
84
88
|
getCurrentModel: () => sessionBinding.get()?.model ?? selectedModel,
|
|
85
89
|
});
|
|
90
|
+
const resourceLoader = await createMarchPiResourceLoader({
|
|
91
|
+
cwd,
|
|
92
|
+
agentDir: stateRoot,
|
|
93
|
+
settingsManager,
|
|
94
|
+
});
|
|
86
95
|
const { session } = await createAgentSessionImpl({
|
|
87
96
|
cwd, agentDir: stateRoot, ...sessionOptions,
|
|
88
97
|
authStorage: resolvedAuth, modelRegistry,
|
|
89
|
-
sessionManager: resolvedSessionManager, settingsManager,
|
|
98
|
+
sessionManager: resolvedSessionManager, settingsManager, resourceLoader,
|
|
90
99
|
});
|
|
91
100
|
sessionBinding.set(session);
|
|
92
|
-
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload,
|
|
101
|
+
installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, providerPayloadTransform.transform);
|
|
93
102
|
}
|
|
94
103
|
syncEngineSessionState(engine, sessionBinding.get());
|
|
95
104
|
lifecycleAdapter = createMarchLifecycleAdapter({
|
|
@@ -113,6 +122,8 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
113
122
|
currentPromptForContext = prompt;
|
|
114
123
|
const contextMode = nextTurnContextMode;
|
|
115
124
|
currentTurnContextMode = contextMode;
|
|
125
|
+
providerPayloadTransform.resetTurn();
|
|
126
|
+
midTurnRecallBridge.reset();
|
|
116
127
|
nextTurnContextMode = "rebuild";
|
|
117
128
|
lifecycle.clearPendingAction();
|
|
118
129
|
const turnStartedAt = Date.now();
|
|
@@ -129,6 +140,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
129
140
|
autoNameSession,
|
|
130
141
|
contextMode,
|
|
131
142
|
recordHistory: (turn) => appendRunnerTurnHistory({ store: historyStore, turn, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost), modelId: engine.modelId, provider: engine.provider }),
|
|
143
|
+
trackMidTurnRecallInjection: (injection) => midTurnRecallBridge.track(injection),
|
|
132
144
|
});
|
|
133
145
|
notifyTurnEndDetached(turnNotifier, {
|
|
134
146
|
status: "success",
|
|
@@ -256,7 +268,6 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
256
268
|
]);
|
|
257
269
|
},
|
|
258
270
|
};
|
|
259
|
-
return runner;
|
|
260
271
|
function syncCurrentMarchSessionState() {
|
|
261
272
|
return syncMarchSessionState({
|
|
262
273
|
enabled: syncPiSidecar || syncMarchSessionStateEnabled, projectMarchDir, engine,
|
|
@@ -285,14 +296,4 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
|
|
|
285
296
|
});
|
|
286
297
|
onModelPayload?.(event);
|
|
287
298
|
}
|
|
288
|
-
function injectMarchSystemContext(payload, { kind, model } = {}) {
|
|
289
|
-
if (kind !== "user") return payload;
|
|
290
|
-
let nextPayload = currentTurnContextMode === "continueExistingPiTranscript"
|
|
291
|
-
? payload
|
|
292
|
-
: replaceProviderContextMessages(payload, engine.buildProviderContext(currentPromptForContext));
|
|
293
|
-
nextPayload = injectHostedTools(nextPayload, model, hostedTools);
|
|
294
|
-
nextPayload = applyCodexLargeContextGuardToPayload(nextPayload, { model, session: sessionBinding.get() });
|
|
295
|
-
if (_currentFastEntry) nextPayload = { ...nextPayload, service_tier: "priority" };
|
|
296
|
-
return nextPayload;
|
|
297
|
-
}
|
|
298
299
|
}
|
|
@@ -13,7 +13,7 @@ export function createRemoteRuntimeUiClient(peer) {
|
|
|
13
13
|
retryEnd: (event) => peer.notify("uiEvent", { type: "retry_end", ...event }),
|
|
14
14
|
status: (text) => peer.notify("uiEvent", { type: "status", text }),
|
|
15
15
|
debugLines: (lines) => peer.notify("uiEvent", { type: "debug_lines", lines }),
|
|
16
|
-
recall: ({
|
|
16
|
+
recall: ({ hints, report, variant }) => peer.notify("uiEvent", { type: "recall", hints, report, variant }),
|
|
17
17
|
editDiff: (path, diffLines) => peer.notify("uiEvent", { type: "edit_diff", path, diffLines }),
|
|
18
18
|
};
|
|
19
19
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const MARCH_PI_RESOURCE_LOADER_OPTIONS = Object.freeze({
|
|
4
|
+
noContextFiles: true,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export async function createMarchPiResourceLoader({ cwd, agentDir, settingsManager, extraOptions = {} }) {
|
|
8
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
9
|
+
...MARCH_PI_RESOURCE_LOADER_OPTIONS,
|
|
10
|
+
...extraOptions,
|
|
11
|
+
cwd,
|
|
12
|
+
agentDir,
|
|
13
|
+
settingsManager,
|
|
14
|
+
});
|
|
15
|
+
await resourceLoader.reload();
|
|
16
|
+
return resourceLoader;
|
|
17
|
+
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
createAgentSessionFromServices,
|
|
3
3
|
createAgentSessionServices,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { MARCH_PI_RESOURCE_LOADER_OPTIONS } from "./resource/context-resource-loader.mjs";
|
|
5
6
|
|
|
6
7
|
export function createMarchRuntimeFactory({
|
|
7
8
|
agentDir,
|
|
@@ -24,7 +25,10 @@ export function createMarchRuntimeFactory({
|
|
|
24
25
|
authStorage,
|
|
25
26
|
settingsManager,
|
|
26
27
|
modelRegistry,
|
|
27
|
-
resourceLoaderOptions
|
|
28
|
+
resourceLoaderOptions: {
|
|
29
|
+
...MARCH_PI_RESOURCE_LOADER_OPTIONS,
|
|
30
|
+
...resourceLoaderOptions,
|
|
31
|
+
},
|
|
28
32
|
});
|
|
29
33
|
const sessionOptions = await resolveSessionOptions({ cwd, services });
|
|
30
34
|
const result = await createFromServices({
|
|
@@ -13,6 +13,7 @@ export function createRunnerStateSnapshot(runner) {
|
|
|
13
13
|
remoteMemorySources: engine.remoteMemorySources ?? [],
|
|
14
14
|
turns: engine.turns ?? [],
|
|
15
15
|
pendingAssistantRecallHints: engine.peekPendingAssistantRecallHints?.() ?? engine.pendingAssistantRecallHints ?? [],
|
|
16
|
+
pendingAssistantRecallReport: engine.peekPendingAssistantRecallReport?.() ?? engine.pendingAssistantRecallReport ?? null,
|
|
16
17
|
pendingAssistantRecallHintsRendered: engine.hasRenderedPendingAssistantRecallHints?.() ?? engine.pendingAssistantRecallHintsRendered ?? false,
|
|
17
18
|
recentRecallMemoryIds: [...(engine.getRecentRecallMemoryIds?.() ?? [])],
|
|
18
19
|
},
|
|
@@ -41,23 +42,29 @@ export function createRunnerEngineStateFacade({ getState, setState }) {
|
|
|
41
42
|
peekPendingAssistantRecallHints() {
|
|
42
43
|
return engineState(getState()).pendingAssistantRecallHints ?? [];
|
|
43
44
|
},
|
|
45
|
+
peekPendingAssistantRecallReport() {
|
|
46
|
+
return engineState(getState()).pendingAssistantRecallReport ?? null;
|
|
47
|
+
},
|
|
44
48
|
hasRenderedPendingAssistantRecallHints() {
|
|
45
49
|
return Boolean(engineState(getState()).pendingAssistantRecallHintsRendered);
|
|
46
50
|
},
|
|
47
51
|
markPendingAssistantRecallHintsRendered() {
|
|
48
52
|
updateEngineState(getState, setState, (engine) => {
|
|
49
|
-
if ((engine.pendingAssistantRecallHints ?? []).length > 0) {
|
|
53
|
+
if ((engine.pendingAssistantRecallHints ?? []).length > 0 || engine.pendingAssistantRecallReport) {
|
|
50
54
|
engine.pendingAssistantRecallHintsRendered = true;
|
|
51
55
|
}
|
|
52
56
|
});
|
|
53
57
|
},
|
|
54
58
|
takePendingAssistantRecallHints() {
|
|
55
|
-
const
|
|
59
|
+
const state = engineState(getState());
|
|
60
|
+
const hints = state.pendingAssistantRecallHints ?? [];
|
|
61
|
+
const report = state.pendingAssistantRecallReport ?? null;
|
|
56
62
|
updateEngineState(getState, setState, (engine) => {
|
|
57
63
|
engine.pendingAssistantRecallHints = [];
|
|
64
|
+
engine.pendingAssistantRecallReport = null;
|
|
58
65
|
engine.pendingAssistantRecallHintsRendered = false;
|
|
59
66
|
});
|
|
60
|
-
return hints;
|
|
67
|
+
return { hints, report };
|
|
61
68
|
},
|
|
62
69
|
getRecentRecallMemoryIds() {
|
|
63
70
|
return engineState(getState()).recentRecallMemoryIds ?? [];
|
|
@@ -50,7 +50,7 @@ export function createRuntimeUiClient(eventBus) {
|
|
|
50
50
|
retryEnd: (event) => eventBus.emit({ type: "retry_end", ...event }),
|
|
51
51
|
status: (text) => eventBus.emit({ type: "status", text }),
|
|
52
52
|
debugLines: (lines) => eventBus.emit({ type: "debug_lines", lines }),
|
|
53
|
-
recall: ({
|
|
53
|
+
recall: ({ hints, report, variant }) => eventBus.emit({ type: "recall", hints, report, variant }),
|
|
54
54
|
providerQuotaSnapshot: (snapshot) => eventBus.emit({ type: "provider_quota_snapshot", snapshot }),
|
|
55
55
|
editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
|
|
56
56
|
};
|
|
@@ -71,7 +71,7 @@ export function dispatchRuntimeUiEvent(ui, event) {
|
|
|
71
71
|
case "retry_end": return ui.retryEnd?.(pickRetryEnd(event));
|
|
72
72
|
case "status": return ui.status?.(event.text);
|
|
73
73
|
case "debug_lines": return writeDebugLines(ui, event.lines);
|
|
74
|
-
case "recall": return ui.recall?.({ hints: event.hints, report: event.report });
|
|
74
|
+
case "recall": return ui.recall?.({ hints: event.hints, report: event.report, variant: event.variant });
|
|
75
75
|
case "provider_quota_snapshot": return ui.providerQuotaSnapshot?.(event.snapshot);
|
|
76
76
|
case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
|
|
77
77
|
default: return undefined;
|
|
@@ -20,6 +20,7 @@ export async function runRunnerTurn({
|
|
|
20
20
|
autoNameSession,
|
|
21
21
|
contextMode = "rebuild",
|
|
22
22
|
recordHistory = null,
|
|
23
|
+
trackMidTurnRecallInjection = null,
|
|
23
24
|
}) {
|
|
24
25
|
const {
|
|
25
26
|
userRecallHints = [],
|
|
@@ -57,7 +58,8 @@ export async function runRunnerTurn({
|
|
|
57
58
|
}
|
|
58
59
|
handleRunnerSessionEvent(event, { ui, engine, state: turnState });
|
|
59
60
|
if (event.type === "tool_execution_start") {
|
|
60
|
-
const task = flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints });
|
|
61
|
+
const task = flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints, trackMidTurnRecallInjection });
|
|
62
|
+
trackMidTurnRecallInjection?.({ task });
|
|
61
63
|
midTurnRecallTasks.push(task);
|
|
62
64
|
}
|
|
63
65
|
});
|
|
@@ -169,16 +171,16 @@ function throwIfAssistantEndedWithError(turnState) {
|
|
|
169
171
|
|
|
170
172
|
function queueMidTurnRecallHints(session, hints, logger) {
|
|
171
173
|
const content = formatRecallHints(hints);
|
|
172
|
-
if (!content) return;
|
|
173
|
-
const
|
|
174
|
+
if (!content) return null;
|
|
175
|
+
const task = Promise.resolve(session.sendCustomMessage?.({
|
|
174
176
|
customType: "march.recall",
|
|
175
177
|
content,
|
|
176
178
|
display: false,
|
|
177
179
|
details: { type: "recall" },
|
|
178
|
-
}, { deliverAs: "steer" })
|
|
179
|
-
void injected?.catch?.((err) => {
|
|
180
|
+
}, { deliverAs: "steer" })).catch((err) => {
|
|
180
181
|
logger?.debug("memory.mid_turn_recall.inject_failed", { errorMessage: err?.message ?? String(err) });
|
|
181
182
|
});
|
|
183
|
+
return { content, task };
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
function logSessionEvent(logger, event) {
|
|
@@ -207,9 +209,9 @@ function logSessionEvent(logger, event) {
|
|
|
207
209
|
|
|
208
210
|
async function finalizeTurn({ prompt, userMessage, userRecallHints, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentMarchSessionState, autoNameSession, recordHistory }) {
|
|
209
211
|
closeAssistantReply({ ui, state: turnState });
|
|
210
|
-
const
|
|
211
|
-
engine.setPendingAssistantRecallHints?.(
|
|
212
|
-
const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...
|
|
212
|
+
const assistantRecall = await flushAssistantRecall({ memoryStore, engine, turnState });
|
|
213
|
+
engine.setPendingAssistantRecallHints?.(assistantRecall.hints, assistantRecall.report);
|
|
214
|
+
const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecall.hints]);
|
|
213
215
|
|
|
214
216
|
const turn = engine.recordTurn({
|
|
215
217
|
userMessage: userMessage ?? prompt.slice(0, 300),
|
|
@@ -225,22 +227,24 @@ async function finalizeTurn({ prompt, userMessage, userRecallHints, memoryStore,
|
|
|
225
227
|
}
|
|
226
228
|
|
|
227
229
|
async function flushAssistantRecall({ memoryStore, engine, turnState }) {
|
|
228
|
-
if (!memoryStore) return [];
|
|
230
|
+
if (!memoryStore) return { hints: [], report: null };
|
|
229
231
|
const text = assistantRecallDeltaText(turnState);
|
|
230
232
|
advanceAssistantRecallCursor(turnState);
|
|
231
|
-
if (!text.trim()) return [];
|
|
233
|
+
if (!text.trim()) return { hints: [], report: null };
|
|
232
234
|
return await memoryStore.recallForAssistant(text, {
|
|
233
235
|
excludedIds: engine.getRecentRecallMemoryIds?.() ?? [],
|
|
234
236
|
});
|
|
235
237
|
}
|
|
236
238
|
|
|
237
|
-
async function flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints }) {
|
|
239
|
+
async function flushMidTurnAssistantRecall({ memoryStore, engine, turnState, activeSession, ui, logger, midTurnRecallHints, trackMidTurnRecallInjection }) {
|
|
238
240
|
try {
|
|
239
|
-
const hints = await flushAssistantRecall({ memoryStore, engine, turnState });
|
|
240
|
-
if (hints.length
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
const { hints, report } = await flushAssistantRecall({ memoryStore, engine, turnState });
|
|
242
|
+
if (hints.length > 0) {
|
|
243
|
+
midTurnRecallHints.push(...hints);
|
|
244
|
+
const injection = queueMidTurnRecallHints(activeSession, hints, logger);
|
|
245
|
+
if (injection) trackMidTurnRecallInjection?.(injection);
|
|
246
|
+
}
|
|
247
|
+
if (report) ui.recall?.({ hints, report, variant: "assistant" });
|
|
244
248
|
} catch (err) {
|
|
245
249
|
logger?.debug("memory.mid_turn_recall.failed", { errorMessage: err?.message ?? String(err) });
|
|
246
250
|
}
|
package/src/cli/fallback-ui.mjs
CHANGED
|
@@ -109,9 +109,9 @@ export function createPlainUI() {
|
|
|
109
109
|
},
|
|
110
110
|
textDelta: writeText,
|
|
111
111
|
status: (text) => { ensureNewline(); stdout.write(`${brightBlack(`● ${text}`)}\n`); },
|
|
112
|
-
recall: ({ hints, report }) => {
|
|
112
|
+
recall: ({ hints, report, variant }) => {
|
|
113
113
|
ensureNewline();
|
|
114
|
-
for (const line of formatRecallLines(hints, report)) stdout.write(`${brightBlack(line)}\n`);
|
|
114
|
+
for (const line of formatRecallLines(hints, report, { variant })) stdout.write(`${brightBlack(line)}\n`);
|
|
115
115
|
},
|
|
116
116
|
clearOutput: () => {},
|
|
117
117
|
restoreTranscript: () => {},
|