@mindstudio-ai/remy 0.1.195 → 0.1.196
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/dist/compaction/conversation.md +8 -3
- package/dist/compaction/subagent.md +10 -4
- package/dist/headless.js +528 -488
- package/dist/index.js +50 -9
- package/package.json +1 -1
package/dist/headless.js
CHANGED
|
@@ -647,375 +647,215 @@ async function generateBackgroundAck(params) {
|
|
|
647
647
|
}
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
-
// src/
|
|
651
|
-
import fs5 from "fs";
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
650
|
+
// src/tools/spec/readSpec.ts
|
|
651
|
+
import fs5 from "fs/promises";
|
|
652
|
+
|
|
653
|
+
// src/tools/spec/_helpers.ts
|
|
654
|
+
var HEADING_RE = /^(#{1,6})\s+(.+)$/;
|
|
655
|
+
function parseHeadings(content) {
|
|
656
|
+
const lines = content.split("\n");
|
|
657
|
+
const headings = [];
|
|
658
|
+
for (let i = 0; i < lines.length; i++) {
|
|
659
|
+
const match = lines[i].match(HEADING_RE);
|
|
660
|
+
if (match) {
|
|
661
|
+
headings.push({
|
|
662
|
+
level: match[1].length,
|
|
663
|
+
text: match[2].trim(),
|
|
664
|
+
startLine: i,
|
|
665
|
+
contentStart: i + 1,
|
|
666
|
+
contentEnd: lines.length
|
|
667
|
+
// placeholder — resolved below
|
|
668
|
+
});
|
|
662
669
|
}
|
|
663
|
-
fs5.writeSync(fd, JSON.stringify(entry) + "\n");
|
|
664
|
-
} catch {
|
|
665
670
|
}
|
|
671
|
+
for (let i = 0; i < headings.length; i++) {
|
|
672
|
+
const current = headings[i];
|
|
673
|
+
let end = lines.length;
|
|
674
|
+
for (let j = i + 1; j < headings.length; j++) {
|
|
675
|
+
if (headings[j].level <= current.level) {
|
|
676
|
+
end = headings[j].startLine;
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
current.contentEnd = end;
|
|
681
|
+
}
|
|
682
|
+
return headings;
|
|
666
683
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const conversationMessages = getConversationMessagesForSummary(
|
|
678
|
-
messages,
|
|
679
|
-
endIndex
|
|
680
|
-
);
|
|
681
|
-
if (conversationMessages.length > 0) {
|
|
682
|
-
tasks.push(
|
|
683
|
-
generateSummary(
|
|
684
|
-
apiConfig,
|
|
685
|
-
"conversation",
|
|
686
|
-
CONVERSATION_SUMMARY_PROMPT,
|
|
687
|
-
conversationMessages,
|
|
688
|
-
system,
|
|
689
|
-
tools2,
|
|
690
|
-
model
|
|
691
|
-
).then((text) => {
|
|
692
|
-
if (text) {
|
|
693
|
-
summaries.push({ name: "conversation", text });
|
|
694
|
-
}
|
|
695
|
-
})
|
|
696
|
-
);
|
|
684
|
+
function resolveHeadingPath(content, headingPath) {
|
|
685
|
+
const lines = content.split("\n");
|
|
686
|
+
const headings = parseHeadings(content);
|
|
687
|
+
if (headingPath === "") {
|
|
688
|
+
const firstHeadingLine = headings.length > 0 ? headings[0].startLine : lines.length;
|
|
689
|
+
return {
|
|
690
|
+
startLine: 0,
|
|
691
|
+
contentStart: 0,
|
|
692
|
+
contentEnd: firstHeadingLine
|
|
693
|
+
};
|
|
697
694
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
695
|
+
const segments = headingPath.split(">").map((s) => s.trim());
|
|
696
|
+
let searchStart = 0;
|
|
697
|
+
let searchEnd = lines.length;
|
|
698
|
+
let resolved = null;
|
|
699
|
+
for (let si = 0; si < segments.length; si++) {
|
|
700
|
+
const segment = segments[si].toLowerCase();
|
|
701
|
+
const candidates = headings.filter(
|
|
702
|
+
(h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
|
|
703
703
|
);
|
|
704
|
-
if (
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
tools2,
|
|
713
|
-
model
|
|
714
|
-
).then((text) => {
|
|
715
|
-
if (text) {
|
|
716
|
-
summaries.push({ name, text });
|
|
717
|
-
}
|
|
718
|
-
})
|
|
704
|
+
if (candidates.length === 0) {
|
|
705
|
+
const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
|
|
706
|
+
const searchedPath = segments.slice(0, si + 1).join(" > ");
|
|
707
|
+
throw new Error(
|
|
708
|
+
`Heading not found: "${searchedPath}"
|
|
709
|
+
|
|
710
|
+
Available headings:
|
|
711
|
+
${available || "(none)"}`
|
|
719
712
|
);
|
|
720
713
|
}
|
|
714
|
+
resolved = candidates[0];
|
|
715
|
+
searchStart = resolved.contentStart;
|
|
716
|
+
searchEnd = resolved.contentEnd;
|
|
721
717
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
{
|
|
728
|
-
type: "summary",
|
|
729
|
-
name: s.name,
|
|
730
|
-
text: s.text,
|
|
731
|
-
startedAt: Date.now()
|
|
732
|
-
}
|
|
733
|
-
]
|
|
734
|
-
}));
|
|
735
|
-
log3.info("Compaction complete", { summaries: summaries.length });
|
|
736
|
-
return checkpointMessages;
|
|
718
|
+
return {
|
|
719
|
+
startLine: resolved.startLine,
|
|
720
|
+
contentStart: resolved.contentStart,
|
|
721
|
+
contentEnd: resolved.contentEnd
|
|
722
|
+
};
|
|
737
723
|
}
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const msg = messages[idx - 1];
|
|
742
|
-
if (msg.role === "user" && msg.toolCallId) {
|
|
743
|
-
idx--;
|
|
744
|
-
} else {
|
|
745
|
-
break;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
if (idx < messages.length && idx > 0) {
|
|
749
|
-
const msg = messages[idx - 1];
|
|
750
|
-
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
751
|
-
const hasToolUse = msg.content.some(
|
|
752
|
-
(b) => b.type === "tool"
|
|
753
|
-
);
|
|
754
|
-
if (hasToolUse) {
|
|
755
|
-
idx--;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
724
|
+
function validateSpecPath(filePath) {
|
|
725
|
+
if (!filePath.startsWith("src/")) {
|
|
726
|
+
throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
|
|
758
727
|
}
|
|
759
|
-
return
|
|
728
|
+
return filePath;
|
|
760
729
|
}
|
|
761
|
-
function
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
if (!Array.isArray(msg.content)) {
|
|
766
|
-
continue;
|
|
767
|
-
}
|
|
768
|
-
for (const block of msg.content) {
|
|
769
|
-
if (block.type === "summary" && block.name === "conversation") {
|
|
770
|
-
startIdx = i + 1;
|
|
771
|
-
break;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
if (startIdx > 0) {
|
|
775
|
-
break;
|
|
776
|
-
}
|
|
730
|
+
function getHeadingTree(content) {
|
|
731
|
+
const headings = parseHeadings(content);
|
|
732
|
+
if (headings.length === 0) {
|
|
733
|
+
return "(no headings)";
|
|
777
734
|
}
|
|
778
|
-
return
|
|
735
|
+
return headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
|
|
779
736
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
737
|
+
|
|
738
|
+
// src/tools/spec/readSpec.ts
|
|
739
|
+
var DEFAULT_MAX_LINES = 500;
|
|
740
|
+
var readSpecTool = {
|
|
741
|
+
clearable: true,
|
|
742
|
+
definition: {
|
|
743
|
+
name: "readSpec",
|
|
744
|
+
description: "Read a spec file from src/ with line numbers. Always read a spec file before editing it. Paths are relative to the project root and must start with src/ (e.g., src/app.md, src/interfaces/web.md).",
|
|
745
|
+
inputSchema: {
|
|
746
|
+
type: "object",
|
|
747
|
+
properties: {
|
|
748
|
+
path: {
|
|
749
|
+
type: "string",
|
|
750
|
+
description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
|
|
751
|
+
},
|
|
752
|
+
offset: {
|
|
753
|
+
type: "number",
|
|
754
|
+
description: "Line number to start reading from (1-indexed). Use a negative number to read from the end. Defaults to 1."
|
|
755
|
+
},
|
|
756
|
+
maxLines: {
|
|
757
|
+
type: "number",
|
|
758
|
+
description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
required: ["path"]
|
|
786
762
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
763
|
+
},
|
|
764
|
+
async execute(input) {
|
|
765
|
+
try {
|
|
766
|
+
validateSpecPath(input.path);
|
|
767
|
+
} catch (err) {
|
|
768
|
+
return `Error: ${err.message}`;
|
|
792
769
|
}
|
|
793
|
-
|
|
794
|
-
|
|
770
|
+
try {
|
|
771
|
+
const content = await fs5.readFile(input.path, "utf-8");
|
|
772
|
+
const allLines = content.split("\n");
|
|
773
|
+
const totalLines = allLines.length;
|
|
774
|
+
const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES;
|
|
775
|
+
let startIdx;
|
|
776
|
+
if (input.offset && input.offset < 0) {
|
|
777
|
+
startIdx = Math.max(0, totalLines + input.offset);
|
|
778
|
+
} else {
|
|
779
|
+
startIdx = Math.max(0, (input.offset || 1) - 1);
|
|
780
|
+
}
|
|
781
|
+
const sliced = allLines.slice(startIdx, startIdx + maxLines);
|
|
782
|
+
const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
|
|
783
|
+
let result = numbered;
|
|
784
|
+
const endLine = startIdx + sliced.length;
|
|
785
|
+
const displayStart = startIdx + 1;
|
|
786
|
+
if (endLine < totalLines) {
|
|
787
|
+
result += `
|
|
788
|
+
|
|
789
|
+
(showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
|
|
790
|
+
}
|
|
791
|
+
return result;
|
|
792
|
+
} catch (err) {
|
|
793
|
+
return `Error reading file: ${err.message}`;
|
|
795
794
|
}
|
|
796
795
|
}
|
|
797
|
-
|
|
798
|
-
const collected = [];
|
|
799
|
-
for (let i = startIdx; i < endIndex; i++) {
|
|
800
|
-
const msg = messages[i];
|
|
801
|
-
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
|
802
|
-
continue;
|
|
803
|
-
}
|
|
804
|
-
for (const block of msg.content) {
|
|
805
|
-
if (block.type === "tool" && block.name === subAgentName && block.subAgentMessages?.length) {
|
|
806
|
-
collected.push(...block.subAgentMessages);
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
return collected;
|
|
811
|
-
}
|
|
812
|
-
function serializeForSummary(messages) {
|
|
813
|
-
return messages.map((msg) => {
|
|
814
|
-
if (typeof msg.content === "string") {
|
|
815
|
-
return `[${msg.role}]: ${msg.content}`;
|
|
816
|
-
}
|
|
817
|
-
if (!Array.isArray(msg.content)) {
|
|
818
|
-
return `[${msg.role}]: (empty)`;
|
|
819
|
-
}
|
|
820
|
-
const blocks = msg.content;
|
|
821
|
-
const parts = [];
|
|
822
|
-
for (const block of blocks) {
|
|
823
|
-
if (block.type === "text") {
|
|
824
|
-
parts.push(block.text);
|
|
825
|
-
} else if (block.type === "tool") {
|
|
826
|
-
parts.push(
|
|
827
|
-
`[tool: ${block.name}(${JSON.stringify(block.input).slice(0, 200)})] \u2192 ${(block.result ?? "").slice(0, 500)}`
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
return `[${msg.role}]: ${parts.join("\n")}`;
|
|
832
|
-
}).join("\n\n");
|
|
833
|
-
}
|
|
834
|
-
var CHUNK_CHAR_LIMIT = 24e5;
|
|
835
|
-
async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
|
|
836
|
-
const serialized = serializeForSummary(messagesToSummarize);
|
|
837
|
-
if (!serialized.trim()) {
|
|
838
|
-
return null;
|
|
839
|
-
}
|
|
840
|
-
if (serialized.length > CHUNK_CHAR_LIMIT && messagesToSummarize.length > 1) {
|
|
841
|
-
const mid = Math.floor(messagesToSummarize.length / 2);
|
|
842
|
-
log3.info("Chunking summary", {
|
|
843
|
-
name,
|
|
844
|
-
messageCount: messagesToSummarize.length,
|
|
845
|
-
serializedLength: serialized.length
|
|
846
|
-
});
|
|
847
|
-
const [first, second] = await Promise.all([
|
|
848
|
-
generateSummary(
|
|
849
|
-
apiConfig,
|
|
850
|
-
`${name} [pt1]`,
|
|
851
|
-
compactionPrompt,
|
|
852
|
-
messagesToSummarize.slice(0, mid),
|
|
853
|
-
mainSystem,
|
|
854
|
-
mainTools,
|
|
855
|
-
model
|
|
856
|
-
),
|
|
857
|
-
generateSummary(
|
|
858
|
-
apiConfig,
|
|
859
|
-
`${name} [pt2]`,
|
|
860
|
-
compactionPrompt,
|
|
861
|
-
messagesToSummarize.slice(mid),
|
|
862
|
-
mainSystem,
|
|
863
|
-
mainTools,
|
|
864
|
-
model
|
|
865
|
-
)
|
|
866
|
-
]);
|
|
867
|
-
const parts = [first, second].filter((p) => !!p);
|
|
868
|
-
return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
|
|
869
|
-
}
|
|
870
|
-
log3.info("Generating summary", {
|
|
871
|
-
name,
|
|
872
|
-
messageCount: messagesToSummarize.length,
|
|
873
|
-
cacheReuse: !!mainSystem
|
|
874
|
-
});
|
|
875
|
-
let summaryText = "";
|
|
876
|
-
const useMainCache = !!mainSystem;
|
|
877
|
-
const system = useMainCache ? mainSystem : compactionPrompt;
|
|
878
|
-
const tools2 = [];
|
|
879
|
-
const userContent = useMainCache ? `${compactionPrompt}
|
|
880
|
-
|
|
881
|
-
---
|
|
882
|
-
|
|
883
|
-
Conversation to summarize:
|
|
884
|
-
|
|
885
|
-
${serialized}` : serialized;
|
|
886
|
-
const iterStart = Date.now();
|
|
887
|
-
for await (const event of streamChat({
|
|
888
|
-
...apiConfig,
|
|
889
|
-
model,
|
|
890
|
-
subAgentId: "conversationSummarizer",
|
|
891
|
-
system,
|
|
892
|
-
messages: [{ role: "user", content: userContent }],
|
|
893
|
-
tools: tools2
|
|
894
|
-
})) {
|
|
895
|
-
if (event.type === "text") {
|
|
896
|
-
summaryText += event.text;
|
|
897
|
-
} else if (event.type === "done") {
|
|
898
|
-
recordUsage({
|
|
899
|
-
ts: Date.now(),
|
|
900
|
-
agentName: "conversationSummarizer",
|
|
901
|
-
modelId: event.modelId,
|
|
902
|
-
inputTokens: event.usage.inputTokens,
|
|
903
|
-
outputTokens: event.usage.outputTokens,
|
|
904
|
-
cacheCreationTokens: event.usage.cacheCreationTokens,
|
|
905
|
-
cacheReadTokens: event.usage.cacheReadTokens,
|
|
906
|
-
cost: nanoToDollars(event.cost),
|
|
907
|
-
billingEvents: event.billingEvents,
|
|
908
|
-
durationMs: Date.now() - iterStart,
|
|
909
|
-
toolNames: []
|
|
910
|
-
});
|
|
911
|
-
} else if (event.type === "error") {
|
|
912
|
-
log3.error("Summary generation failed", { name, error: event.error });
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
if (!summaryText.trim()) {
|
|
917
|
-
log3.warn("Empty summary generated", { name });
|
|
918
|
-
return null;
|
|
919
|
-
}
|
|
920
|
-
log3.info("Summary generated", { name, summaryLength: summaryText.length });
|
|
921
|
-
return summaryText.trim();
|
|
922
|
-
}
|
|
796
|
+
};
|
|
923
797
|
|
|
924
|
-
// src/tools/spec/
|
|
798
|
+
// src/tools/spec/writeSpec.ts
|
|
925
799
|
import fs6 from "fs/promises";
|
|
800
|
+
import path4 from "path";
|
|
926
801
|
|
|
927
|
-
// src/tools/
|
|
928
|
-
var
|
|
929
|
-
function
|
|
930
|
-
const
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
headings.push({
|
|
936
|
-
level: match[1].length,
|
|
937
|
-
text: match[2].trim(),
|
|
938
|
-
startLine: i,
|
|
939
|
-
contentStart: i + 1,
|
|
940
|
-
contentEnd: lines.length
|
|
941
|
-
// placeholder — resolved below
|
|
942
|
-
});
|
|
943
|
-
}
|
|
802
|
+
// src/tools/_helpers/diff.ts
|
|
803
|
+
var CONTEXT_LINES = 3;
|
|
804
|
+
function unifiedDiff(filePath, oldText, newText) {
|
|
805
|
+
const oldLines = oldText ? oldText.split("\n") : [];
|
|
806
|
+
const newLines = newText ? newText.split("\n") : [];
|
|
807
|
+
let firstDiff = 0;
|
|
808
|
+
while (firstDiff < oldLines.length && firstDiff < newLines.length && oldLines[firstDiff] === newLines[firstDiff]) {
|
|
809
|
+
firstDiff++;
|
|
944
810
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
end = headings[j].startLine;
|
|
951
|
-
break;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
current.contentEnd = end;
|
|
811
|
+
let oldEnd = oldLines.length - 1;
|
|
812
|
+
let newEnd = newLines.length - 1;
|
|
813
|
+
while (oldEnd > firstDiff && newEnd > firstDiff && oldLines[oldEnd] === newLines[newEnd]) {
|
|
814
|
+
oldEnd--;
|
|
815
|
+
newEnd--;
|
|
955
816
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const lines =
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
};
|
|
817
|
+
const ctxStart = Math.max(0, firstDiff - CONTEXT_LINES);
|
|
818
|
+
const ctxOldEnd = Math.min(oldLines.length - 1, oldEnd + CONTEXT_LINES);
|
|
819
|
+
const ctxNewEnd = Math.min(newLines.length - 1, newEnd + CONTEXT_LINES);
|
|
820
|
+
const lines = [];
|
|
821
|
+
lines.push(`--- ${filePath}`);
|
|
822
|
+
lines.push(`+++ ${filePath}`);
|
|
823
|
+
lines.push(
|
|
824
|
+
`@@ -${ctxStart + 1},${ctxOldEnd - ctxStart + 1} +${ctxStart + 1},${ctxNewEnd - ctxStart + 1} @@`
|
|
825
|
+
);
|
|
826
|
+
for (let i = ctxStart; i < firstDiff; i++) {
|
|
827
|
+
lines.push(` ${oldLines[i]}`);
|
|
968
828
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
let searchEnd = lines.length;
|
|
972
|
-
let resolved = null;
|
|
973
|
-
for (let si = 0; si < segments.length; si++) {
|
|
974
|
-
const segment = segments[si].toLowerCase();
|
|
975
|
-
const candidates = headings.filter(
|
|
976
|
-
(h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
|
|
977
|
-
);
|
|
978
|
-
if (candidates.length === 0) {
|
|
979
|
-
const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
|
|
980
|
-
const searchedPath = segments.slice(0, si + 1).join(" > ");
|
|
981
|
-
throw new Error(
|
|
982
|
-
`Heading not found: "${searchedPath}"
|
|
983
|
-
|
|
984
|
-
Available headings:
|
|
985
|
-
${available || "(none)"}`
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
resolved = candidates[0];
|
|
989
|
-
searchStart = resolved.contentStart;
|
|
990
|
-
searchEnd = resolved.contentEnd;
|
|
829
|
+
for (let i = firstDiff; i <= oldEnd; i++) {
|
|
830
|
+
lines.push(`-${oldLines[i]}`);
|
|
991
831
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
contentStart: resolved.contentStart,
|
|
995
|
-
contentEnd: resolved.contentEnd
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
function validateSpecPath(filePath) {
|
|
999
|
-
if (!filePath.startsWith("src/")) {
|
|
1000
|
-
throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
|
|
832
|
+
for (let i = firstDiff; i <= newEnd; i++) {
|
|
833
|
+
lines.push(`+${newLines[i]}`);
|
|
1001
834
|
}
|
|
1002
|
-
|
|
1003
|
-
}
|
|
1004
|
-
function getHeadingTree(content) {
|
|
1005
|
-
const headings = parseHeadings(content);
|
|
1006
|
-
if (headings.length === 0) {
|
|
1007
|
-
return "(no headings)";
|
|
835
|
+
for (let i = oldEnd + 1; i <= ctxOldEnd; i++) {
|
|
836
|
+
lines.push(` ${oldLines[i]}`);
|
|
1008
837
|
}
|
|
1009
|
-
return
|
|
838
|
+
return lines.join("\n");
|
|
1010
839
|
}
|
|
1011
840
|
|
|
1012
|
-
// src/tools/
|
|
1013
|
-
var
|
|
1014
|
-
|
|
841
|
+
// src/tools/_helpers/fileLock.ts
|
|
842
|
+
var locks = /* @__PURE__ */ new Map();
|
|
843
|
+
function acquireFileLock(filePath) {
|
|
844
|
+
let release;
|
|
845
|
+
const next = new Promise((res) => {
|
|
846
|
+
release = res;
|
|
847
|
+
});
|
|
848
|
+
const wait = locks.get(filePath) ?? Promise.resolve();
|
|
849
|
+
locks.set(filePath, next);
|
|
850
|
+
return wait.then(() => release);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/tools/spec/writeSpec.ts
|
|
854
|
+
var writeSpecTool = {
|
|
1015
855
|
clearable: true,
|
|
1016
856
|
definition: {
|
|
1017
|
-
name: "
|
|
1018
|
-
description: "
|
|
857
|
+
name: "writeSpec",
|
|
858
|
+
description: "Create a new spec file or completely overwrite an existing one in src/. Parent directories are created automatically. Use this for new spec files or full rewrites. For targeted changes to existing specs, use editSpec instead.",
|
|
1019
859
|
inputSchema: {
|
|
1020
860
|
type: "object",
|
|
1021
861
|
properties: {
|
|
@@ -1023,131 +863,17 @@ var readSpecTool = {
|
|
|
1023
863
|
type: "string",
|
|
1024
864
|
description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
|
|
1025
865
|
},
|
|
1026
|
-
|
|
1027
|
-
type: "
|
|
1028
|
-
description: "
|
|
1029
|
-
},
|
|
1030
|
-
maxLines: {
|
|
1031
|
-
type: "number",
|
|
1032
|
-
description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
|
|
866
|
+
content: {
|
|
867
|
+
type: "string",
|
|
868
|
+
description: "The full MSFM markdown content to write."
|
|
1033
869
|
}
|
|
1034
870
|
},
|
|
1035
|
-
required: ["path"]
|
|
1036
|
-
}
|
|
1037
|
-
},
|
|
1038
|
-
async execute(input) {
|
|
1039
|
-
try {
|
|
1040
|
-
validateSpecPath(input.path);
|
|
1041
|
-
} catch (err) {
|
|
1042
|
-
return `Error: ${err.message}`;
|
|
1043
|
-
}
|
|
1044
|
-
try {
|
|
1045
|
-
const content = await fs6.readFile(input.path, "utf-8");
|
|
1046
|
-
const allLines = content.split("\n");
|
|
1047
|
-
const totalLines = allLines.length;
|
|
1048
|
-
const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES;
|
|
1049
|
-
let startIdx;
|
|
1050
|
-
if (input.offset && input.offset < 0) {
|
|
1051
|
-
startIdx = Math.max(0, totalLines + input.offset);
|
|
1052
|
-
} else {
|
|
1053
|
-
startIdx = Math.max(0, (input.offset || 1) - 1);
|
|
1054
|
-
}
|
|
1055
|
-
const sliced = allLines.slice(startIdx, startIdx + maxLines);
|
|
1056
|
-
const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
|
|
1057
|
-
let result = numbered;
|
|
1058
|
-
const endLine = startIdx + sliced.length;
|
|
1059
|
-
const displayStart = startIdx + 1;
|
|
1060
|
-
if (endLine < totalLines) {
|
|
1061
|
-
result += `
|
|
1062
|
-
|
|
1063
|
-
(showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
|
|
1064
|
-
}
|
|
1065
|
-
return result;
|
|
1066
|
-
} catch (err) {
|
|
1067
|
-
return `Error reading file: ${err.message}`;
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
|
-
|
|
1072
|
-
// src/tools/spec/writeSpec.ts
|
|
1073
|
-
import fs7 from "fs/promises";
|
|
1074
|
-
import path4 from "path";
|
|
1075
|
-
|
|
1076
|
-
// src/tools/_helpers/diff.ts
|
|
1077
|
-
var CONTEXT_LINES = 3;
|
|
1078
|
-
function unifiedDiff(filePath, oldText, newText) {
|
|
1079
|
-
const oldLines = oldText ? oldText.split("\n") : [];
|
|
1080
|
-
const newLines = newText ? newText.split("\n") : [];
|
|
1081
|
-
let firstDiff = 0;
|
|
1082
|
-
while (firstDiff < oldLines.length && firstDiff < newLines.length && oldLines[firstDiff] === newLines[firstDiff]) {
|
|
1083
|
-
firstDiff++;
|
|
1084
|
-
}
|
|
1085
|
-
let oldEnd = oldLines.length - 1;
|
|
1086
|
-
let newEnd = newLines.length - 1;
|
|
1087
|
-
while (oldEnd > firstDiff && newEnd > firstDiff && oldLines[oldEnd] === newLines[newEnd]) {
|
|
1088
|
-
oldEnd--;
|
|
1089
|
-
newEnd--;
|
|
1090
|
-
}
|
|
1091
|
-
const ctxStart = Math.max(0, firstDiff - CONTEXT_LINES);
|
|
1092
|
-
const ctxOldEnd = Math.min(oldLines.length - 1, oldEnd + CONTEXT_LINES);
|
|
1093
|
-
const ctxNewEnd = Math.min(newLines.length - 1, newEnd + CONTEXT_LINES);
|
|
1094
|
-
const lines = [];
|
|
1095
|
-
lines.push(`--- ${filePath}`);
|
|
1096
|
-
lines.push(`+++ ${filePath}`);
|
|
1097
|
-
lines.push(
|
|
1098
|
-
`@@ -${ctxStart + 1},${ctxOldEnd - ctxStart + 1} +${ctxStart + 1},${ctxNewEnd - ctxStart + 1} @@`
|
|
1099
|
-
);
|
|
1100
|
-
for (let i = ctxStart; i < firstDiff; i++) {
|
|
1101
|
-
lines.push(` ${oldLines[i]}`);
|
|
1102
|
-
}
|
|
1103
|
-
for (let i = firstDiff; i <= oldEnd; i++) {
|
|
1104
|
-
lines.push(`-${oldLines[i]}`);
|
|
1105
|
-
}
|
|
1106
|
-
for (let i = firstDiff; i <= newEnd; i++) {
|
|
1107
|
-
lines.push(`+${newLines[i]}`);
|
|
1108
|
-
}
|
|
1109
|
-
for (let i = oldEnd + 1; i <= ctxOldEnd; i++) {
|
|
1110
|
-
lines.push(` ${oldLines[i]}`);
|
|
1111
|
-
}
|
|
1112
|
-
return lines.join("\n");
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
// src/tools/_helpers/fileLock.ts
|
|
1116
|
-
var locks = /* @__PURE__ */ new Map();
|
|
1117
|
-
function acquireFileLock(filePath) {
|
|
1118
|
-
let release;
|
|
1119
|
-
const next = new Promise((res) => {
|
|
1120
|
-
release = res;
|
|
1121
|
-
});
|
|
1122
|
-
const wait = locks.get(filePath) ?? Promise.resolve();
|
|
1123
|
-
locks.set(filePath, next);
|
|
1124
|
-
return wait.then(() => release);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// src/tools/spec/writeSpec.ts
|
|
1128
|
-
var writeSpecTool = {
|
|
1129
|
-
clearable: true,
|
|
1130
|
-
definition: {
|
|
1131
|
-
name: "writeSpec",
|
|
1132
|
-
description: "Create a new spec file or completely overwrite an existing one in src/. Parent directories are created automatically. Use this for new spec files or full rewrites. For targeted changes to existing specs, use editSpec instead.",
|
|
1133
|
-
inputSchema: {
|
|
1134
|
-
type: "object",
|
|
1135
|
-
properties: {
|
|
1136
|
-
path: {
|
|
1137
|
-
type: "string",
|
|
1138
|
-
description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
|
|
1139
|
-
},
|
|
1140
|
-
content: {
|
|
1141
|
-
type: "string",
|
|
1142
|
-
description: "The full MSFM markdown content to write."
|
|
1143
|
-
}
|
|
1144
|
-
},
|
|
1145
|
-
required: ["path", "content"]
|
|
871
|
+
required: ["path", "content"]
|
|
1146
872
|
}
|
|
1147
873
|
},
|
|
1148
874
|
streaming: {
|
|
1149
875
|
transform: async (partial) => {
|
|
1150
|
-
const oldContent = await
|
|
876
|
+
const oldContent = await fs6.readFile(partial.path, "utf-8").catch(() => "");
|
|
1151
877
|
const lineCount = partial.content.split("\n").length;
|
|
1152
878
|
return `Writing ${partial.path} (${lineCount} lines)
|
|
1153
879
|
${unifiedDiff(partial.path, oldContent, partial.content)}`;
|
|
@@ -1161,13 +887,13 @@ ${unifiedDiff(partial.path, oldContent, partial.content)}`;
|
|
|
1161
887
|
}
|
|
1162
888
|
const release = await acquireFileLock(input.path);
|
|
1163
889
|
try {
|
|
1164
|
-
await
|
|
890
|
+
await fs6.mkdir(path4.dirname(input.path), { recursive: true });
|
|
1165
891
|
let oldContent = null;
|
|
1166
892
|
try {
|
|
1167
|
-
oldContent = await
|
|
893
|
+
oldContent = await fs6.readFile(input.path, "utf-8");
|
|
1168
894
|
} catch {
|
|
1169
895
|
}
|
|
1170
|
-
await
|
|
896
|
+
await fs6.writeFile(input.path, input.content, "utf-8");
|
|
1171
897
|
const lineCount = input.content.split("\n").length;
|
|
1172
898
|
const label = oldContent !== null ? "Wrote" : "Created";
|
|
1173
899
|
return `${label} ${input.path} (${lineCount} lines)
|
|
@@ -1181,7 +907,7 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
|
|
|
1181
907
|
};
|
|
1182
908
|
|
|
1183
909
|
// src/tools/spec/editSpec.ts
|
|
1184
|
-
import
|
|
910
|
+
import fs7 from "fs/promises";
|
|
1185
911
|
var editSpecTool = {
|
|
1186
912
|
clearable: true,
|
|
1187
913
|
definition: {
|
|
@@ -1231,7 +957,7 @@ var editSpecTool = {
|
|
|
1231
957
|
try {
|
|
1232
958
|
let originalContent;
|
|
1233
959
|
try {
|
|
1234
|
-
originalContent = await
|
|
960
|
+
originalContent = await fs7.readFile(input.path, "utf-8");
|
|
1235
961
|
} catch (err) {
|
|
1236
962
|
return `Error reading file: ${err.message}`;
|
|
1237
963
|
}
|
|
@@ -1287,7 +1013,7 @@ ${tree}`;
|
|
|
1287
1013
|
content = lines.join("\n");
|
|
1288
1014
|
}
|
|
1289
1015
|
try {
|
|
1290
|
-
await
|
|
1016
|
+
await fs7.writeFile(input.path, content, "utf-8");
|
|
1291
1017
|
} catch (err) {
|
|
1292
1018
|
return `Error writing file: ${err.message}`;
|
|
1293
1019
|
}
|
|
@@ -1299,7 +1025,7 @@ ${tree}`;
|
|
|
1299
1025
|
};
|
|
1300
1026
|
|
|
1301
1027
|
// src/tools/spec/listSpecFiles.ts
|
|
1302
|
-
import
|
|
1028
|
+
import fs8 from "fs/promises";
|
|
1303
1029
|
import path5 from "path";
|
|
1304
1030
|
var listSpecFilesTool = {
|
|
1305
1031
|
clearable: false,
|
|
@@ -1329,7 +1055,7 @@ var listSpecFilesTool = {
|
|
|
1329
1055
|
};
|
|
1330
1056
|
async function listRecursive(dir) {
|
|
1331
1057
|
const results = [];
|
|
1332
|
-
const entries = await
|
|
1058
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1333
1059
|
entries.sort((a, b) => {
|
|
1334
1060
|
if (a.isDirectory() && !b.isDirectory()) {
|
|
1335
1061
|
return -1;
|
|
@@ -1375,7 +1101,7 @@ var presentPublishPlanTool = {
|
|
|
1375
1101
|
};
|
|
1376
1102
|
|
|
1377
1103
|
// src/tools/spec/writePlan.ts
|
|
1378
|
-
import
|
|
1104
|
+
import fs9 from "fs/promises";
|
|
1379
1105
|
var PLAN_FILE = ".remy-plan.md";
|
|
1380
1106
|
var writePlanTool = {
|
|
1381
1107
|
clearable: false,
|
|
@@ -1400,13 +1126,13 @@ status: pending
|
|
|
1400
1126
|
---
|
|
1401
1127
|
|
|
1402
1128
|
${content}`;
|
|
1403
|
-
await
|
|
1129
|
+
await fs9.writeFile(PLAN_FILE, file, "utf-8");
|
|
1404
1130
|
return "Plan written to .remy-plan.md. Waiting for user approval.";
|
|
1405
1131
|
}
|
|
1406
1132
|
};
|
|
1407
1133
|
|
|
1408
1134
|
// src/tools/spec/updatePlanStatus.ts
|
|
1409
|
-
import
|
|
1135
|
+
import fs10 from "fs/promises";
|
|
1410
1136
|
var PLAN_FILE2 = ".remy-plan.md";
|
|
1411
1137
|
var updatePlanStatusTool = {
|
|
1412
1138
|
clearable: false,
|
|
@@ -1429,15 +1155,15 @@ var updatePlanStatusTool = {
|
|
|
1429
1155
|
const status = input.status;
|
|
1430
1156
|
let content;
|
|
1431
1157
|
try {
|
|
1432
|
-
content = await
|
|
1158
|
+
content = await fs10.readFile(PLAN_FILE2, "utf-8");
|
|
1433
1159
|
} catch {
|
|
1434
1160
|
return "No plan file found.";
|
|
1435
1161
|
}
|
|
1436
1162
|
if (status === "rejected") {
|
|
1437
|
-
await
|
|
1163
|
+
await fs10.unlink(PLAN_FILE2);
|
|
1438
1164
|
return "Plan rejected and removed.";
|
|
1439
1165
|
}
|
|
1440
|
-
await
|
|
1166
|
+
await fs10.writeFile(
|
|
1441
1167
|
PLAN_FILE2,
|
|
1442
1168
|
content.replace(/^status:\s*\w+/m, `status: ${status}`),
|
|
1443
1169
|
"utf-8"
|
|
@@ -1743,6 +1469,24 @@ var askMindStudioSdkTool = {
|
|
|
1743
1469
|
}
|
|
1744
1470
|
};
|
|
1745
1471
|
|
|
1472
|
+
// src/usageLedger.ts
|
|
1473
|
+
import fs11 from "fs";
|
|
1474
|
+
var LEDGER_FILE = ".logs/usage.ndjson";
|
|
1475
|
+
var fd = null;
|
|
1476
|
+
function nanoToDollars(nano) {
|
|
1477
|
+
return typeof nano === "number" ? nano / 1e9 : void 0;
|
|
1478
|
+
}
|
|
1479
|
+
function recordUsage(entry) {
|
|
1480
|
+
try {
|
|
1481
|
+
if (fd === null) {
|
|
1482
|
+
fs11.mkdirSync(".logs", { recursive: true });
|
|
1483
|
+
fd = fs11.openSync(LEDGER_FILE, "a");
|
|
1484
|
+
}
|
|
1485
|
+
fs11.writeSync(fd, JSON.stringify(entry) + "\n");
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1746
1490
|
// src/subagents/common/runMindstudioCli.ts
|
|
1747
1491
|
function stripFlags(args) {
|
|
1748
1492
|
const out = [];
|
|
@@ -2584,11 +2328,11 @@ var editsFinishedTool = {
|
|
|
2584
2328
|
};
|
|
2585
2329
|
|
|
2586
2330
|
// src/tools/_helpers/sidecar.ts
|
|
2587
|
-
var
|
|
2331
|
+
var log3 = createLogger("sidecar");
|
|
2588
2332
|
var baseUrl = null;
|
|
2589
2333
|
function setSidecarBaseUrl(url) {
|
|
2590
2334
|
baseUrl = url;
|
|
2591
|
-
|
|
2335
|
+
log3.info("Configured", { url });
|
|
2592
2336
|
}
|
|
2593
2337
|
async function sidecarRequest(endpoint, body = {}, options) {
|
|
2594
2338
|
if (!baseUrl) {
|
|
@@ -2603,7 +2347,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
|
|
|
2603
2347
|
signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
|
|
2604
2348
|
});
|
|
2605
2349
|
if (!res.ok) {
|
|
2606
|
-
|
|
2350
|
+
log3.error("Sidecar error", { endpoint, status: res.status });
|
|
2607
2351
|
throw new Error(`Sidecar error: ${res.status}`);
|
|
2608
2352
|
}
|
|
2609
2353
|
const data = await res.json();
|
|
@@ -2616,7 +2360,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
|
|
|
2616
2360
|
if (err.message.startsWith("Sidecar error")) {
|
|
2617
2361
|
throw err;
|
|
2618
2362
|
}
|
|
2619
|
-
|
|
2363
|
+
log3.error("Sidecar connection error", { endpoint, error: err.message });
|
|
2620
2364
|
throw new Error(`Sidecar connection error: ${err.message}`);
|
|
2621
2365
|
}
|
|
2622
2366
|
}
|
|
@@ -2907,7 +2651,7 @@ function acquireBrowserLock() {
|
|
|
2907
2651
|
}
|
|
2908
2652
|
|
|
2909
2653
|
// src/toolRegistry.ts
|
|
2910
|
-
var
|
|
2654
|
+
var log4 = createLogger("tool-registry");
|
|
2911
2655
|
var USER_CANCELLED_RESULT = "[USER CANCELLED] The user manually cancelled this tool. Do not retry it automatically \u2014 wait for the user\u2019s next message for direction.";
|
|
2912
2656
|
var ToolRegistry = class {
|
|
2913
2657
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -2934,7 +2678,7 @@ var ToolRegistry = class {
|
|
|
2934
2678
|
if (!entry) {
|
|
2935
2679
|
return false;
|
|
2936
2680
|
}
|
|
2937
|
-
|
|
2681
|
+
log4.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
|
|
2938
2682
|
entry.abortController.abort(mode);
|
|
2939
2683
|
if (mode === "graceful") {
|
|
2940
2684
|
const partial = entry.getPartialResult?.() ?? "";
|
|
@@ -2967,7 +2711,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
|
|
|
2967
2711
|
if (!entry) {
|
|
2968
2712
|
return false;
|
|
2969
2713
|
}
|
|
2970
|
-
|
|
2714
|
+
log4.info("Tool restarted", { toolCallId: id, name: entry.name });
|
|
2971
2715
|
entry.abortController.abort("restart");
|
|
2972
2716
|
const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
|
|
2973
2717
|
this.onEvent?.({
|
|
@@ -3227,7 +2971,7 @@ ${content}` : attachmentHeader;
|
|
|
3227
2971
|
}
|
|
3228
2972
|
|
|
3229
2973
|
// src/subagents/runner.ts
|
|
3230
|
-
var
|
|
2974
|
+
var log5 = createLogger("sub-agent");
|
|
3231
2975
|
async function runSubAgent(config) {
|
|
3232
2976
|
const {
|
|
3233
2977
|
system,
|
|
@@ -3254,7 +2998,7 @@ async function runSubAgent(config) {
|
|
|
3254
2998
|
const signal = background ? bgAbort.signal : parentSignal;
|
|
3255
2999
|
const agentName = subAgentId || "sub-agent";
|
|
3256
3000
|
const runStart = Date.now();
|
|
3257
|
-
|
|
3001
|
+
log5.info("Sub-agent started", { requestId, parentToolId, agentName });
|
|
3258
3002
|
const emit = (e) => {
|
|
3259
3003
|
onEvent({ ...e, parentToolId });
|
|
3260
3004
|
};
|
|
@@ -3471,7 +3215,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3471
3215
|
...hasArtifacts ? { artifacts } : {}
|
|
3472
3216
|
};
|
|
3473
3217
|
}
|
|
3474
|
-
|
|
3218
|
+
log5.info("Tools executing", {
|
|
3475
3219
|
requestId,
|
|
3476
3220
|
parentToolId,
|
|
3477
3221
|
count: toolCalls.length,
|
|
@@ -3548,7 +3292,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3548
3292
|
run2(tc.input);
|
|
3549
3293
|
const r = await resultPromise;
|
|
3550
3294
|
toolRegistry?.unregister(tc.id);
|
|
3551
|
-
|
|
3295
|
+
log5.info("Tool completed", {
|
|
3552
3296
|
requestId,
|
|
3553
3297
|
parentToolId,
|
|
3554
3298
|
toolCallId: tc.id,
|
|
@@ -3599,7 +3343,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3599
3343
|
const wrapRun = async () => {
|
|
3600
3344
|
try {
|
|
3601
3345
|
const result = await run();
|
|
3602
|
-
|
|
3346
|
+
log5.info("Sub-agent complete", {
|
|
3603
3347
|
requestId,
|
|
3604
3348
|
parentToolId,
|
|
3605
3349
|
agentName,
|
|
@@ -3608,7 +3352,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3608
3352
|
});
|
|
3609
3353
|
return result;
|
|
3610
3354
|
} catch (err) {
|
|
3611
|
-
|
|
3355
|
+
log5.warn("Sub-agent error", {
|
|
3612
3356
|
requestId,
|
|
3613
3357
|
parentToolId,
|
|
3614
3358
|
agentName,
|
|
@@ -3620,7 +3364,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3620
3364
|
if (!background) {
|
|
3621
3365
|
return wrapRun();
|
|
3622
3366
|
}
|
|
3623
|
-
|
|
3367
|
+
log5.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
|
|
3624
3368
|
toolRegistry?.register({
|
|
3625
3369
|
id: parentToolId,
|
|
3626
3370
|
name: agentName,
|
|
@@ -3904,7 +3648,7 @@ function resolveModel(surfaceId, models, fallback) {
|
|
|
3904
3648
|
}
|
|
3905
3649
|
|
|
3906
3650
|
// src/subagents/browserAutomation/index.ts
|
|
3907
|
-
var
|
|
3651
|
+
var log6 = createLogger("browser-automation");
|
|
3908
3652
|
async function runBrowserAutomation(task, context, opts) {
|
|
3909
3653
|
const release = await acquireBrowserLock();
|
|
3910
3654
|
try {
|
|
@@ -4003,7 +3747,7 @@ async function runBrowserAutomation(task, context, opts) {
|
|
|
4003
3747
|
}
|
|
4004
3748
|
}
|
|
4005
3749
|
} catch {
|
|
4006
|
-
|
|
3750
|
+
log6.debug("Failed to parse batch analysis result", {
|
|
4007
3751
|
batchResult
|
|
4008
3752
|
});
|
|
4009
3753
|
}
|
|
@@ -5677,6 +5421,13 @@ var ALL_TOOLS = [
|
|
|
5677
5421
|
var CLEARABLE_TOOLS = new Set(
|
|
5678
5422
|
ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
|
|
5679
5423
|
);
|
|
5424
|
+
var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5425
|
+
"visualDesignExpert",
|
|
5426
|
+
"productVision",
|
|
5427
|
+
"codeSanityCheck",
|
|
5428
|
+
"runAutomatedBrowserTest",
|
|
5429
|
+
"askMindStudioSdk"
|
|
5430
|
+
]);
|
|
5680
5431
|
function getToolDefinitions(_onboardingState) {
|
|
5681
5432
|
return ALL_TOOLS.map((t) => t.definition);
|
|
5682
5433
|
}
|
|
@@ -5691,6 +5442,295 @@ function executeTool(name, input, context) {
|
|
|
5691
5442
|
return tool.execute(input, context);
|
|
5692
5443
|
}
|
|
5693
5444
|
|
|
5445
|
+
// src/compaction/index.ts
|
|
5446
|
+
var log7 = createLogger("compaction");
|
|
5447
|
+
var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
|
|
5448
|
+
var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
|
|
5449
|
+
var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
|
|
5450
|
+
async function compactConversation(messages, apiConfig, system, tools2, model) {
|
|
5451
|
+
const endIndex = findSafeInsertionPoint(messages);
|
|
5452
|
+
const summaries = [];
|
|
5453
|
+
const tasks = [];
|
|
5454
|
+
const conversationMessages = getConversationMessagesForSummary(
|
|
5455
|
+
messages,
|
|
5456
|
+
endIndex
|
|
5457
|
+
);
|
|
5458
|
+
if (conversationMessages.length > 0) {
|
|
5459
|
+
tasks.push(
|
|
5460
|
+
generateSummary(
|
|
5461
|
+
apiConfig,
|
|
5462
|
+
"conversation",
|
|
5463
|
+
CONVERSATION_SUMMARY_PROMPT,
|
|
5464
|
+
conversationMessages,
|
|
5465
|
+
system,
|
|
5466
|
+
tools2,
|
|
5467
|
+
model
|
|
5468
|
+
).then((text) => {
|
|
5469
|
+
if (text) {
|
|
5470
|
+
summaries.push({ name: "conversation", text });
|
|
5471
|
+
}
|
|
5472
|
+
})
|
|
5473
|
+
);
|
|
5474
|
+
}
|
|
5475
|
+
for (const name of SUMMARIZABLE_SUBAGENTS) {
|
|
5476
|
+
const subagentMessages = getSubAgentMessagesForSummary(
|
|
5477
|
+
messages,
|
|
5478
|
+
name,
|
|
5479
|
+
endIndex
|
|
5480
|
+
);
|
|
5481
|
+
if (subagentMessages.length > 0) {
|
|
5482
|
+
tasks.push(
|
|
5483
|
+
generateSummary(
|
|
5484
|
+
apiConfig,
|
|
5485
|
+
name,
|
|
5486
|
+
SUBAGENT_SUMMARY_PROMPT,
|
|
5487
|
+
subagentMessages,
|
|
5488
|
+
system,
|
|
5489
|
+
tools2,
|
|
5490
|
+
model
|
|
5491
|
+
).then((text) => {
|
|
5492
|
+
if (text) {
|
|
5493
|
+
summaries.push({ name, text });
|
|
5494
|
+
}
|
|
5495
|
+
})
|
|
5496
|
+
);
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
await Promise.all(tasks);
|
|
5500
|
+
const checkpointMessages = summaries.map((s) => ({
|
|
5501
|
+
role: "user",
|
|
5502
|
+
hidden: true,
|
|
5503
|
+
content: [
|
|
5504
|
+
{
|
|
5505
|
+
type: "summary",
|
|
5506
|
+
name: s.name,
|
|
5507
|
+
text: s.text,
|
|
5508
|
+
startedAt: Date.now()
|
|
5509
|
+
}
|
|
5510
|
+
]
|
|
5511
|
+
}));
|
|
5512
|
+
log7.info("Compaction complete", { summaries: summaries.length });
|
|
5513
|
+
return checkpointMessages;
|
|
5514
|
+
}
|
|
5515
|
+
function findSafeInsertionPoint(messages) {
|
|
5516
|
+
let idx = messages.length;
|
|
5517
|
+
while (idx > 0) {
|
|
5518
|
+
const msg = messages[idx - 1];
|
|
5519
|
+
if (msg.role === "user" && msg.toolCallId) {
|
|
5520
|
+
idx--;
|
|
5521
|
+
} else {
|
|
5522
|
+
break;
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
if (idx < messages.length && idx > 0) {
|
|
5526
|
+
const msg = messages[idx - 1];
|
|
5527
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
5528
|
+
const hasToolUse = msg.content.some(
|
|
5529
|
+
(b) => b.type === "tool"
|
|
5530
|
+
);
|
|
5531
|
+
if (hasToolUse) {
|
|
5532
|
+
idx--;
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
return idx;
|
|
5537
|
+
}
|
|
5538
|
+
function getConversationMessagesForSummary(messages, endIndex) {
|
|
5539
|
+
let startIdx = 0;
|
|
5540
|
+
for (let i = endIndex - 1; i >= 0; i--) {
|
|
5541
|
+
const msg = messages[i];
|
|
5542
|
+
if (!Array.isArray(msg.content)) {
|
|
5543
|
+
continue;
|
|
5544
|
+
}
|
|
5545
|
+
for (const block of msg.content) {
|
|
5546
|
+
if (block.type === "summary" && block.name === "conversation") {
|
|
5547
|
+
startIdx = i + 1;
|
|
5548
|
+
break;
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
if (startIdx > 0) {
|
|
5552
|
+
break;
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
return messages.slice(startIdx, endIndex);
|
|
5556
|
+
}
|
|
5557
|
+
function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
|
|
5558
|
+
let checkpointIdx = -1;
|
|
5559
|
+
for (let i = endIndex - 1; i >= 0; i--) {
|
|
5560
|
+
const msg = messages[i];
|
|
5561
|
+
if (!Array.isArray(msg.content)) {
|
|
5562
|
+
continue;
|
|
5563
|
+
}
|
|
5564
|
+
for (const block of msg.content) {
|
|
5565
|
+
if (block.type === "summary" && block.name === subAgentName) {
|
|
5566
|
+
checkpointIdx = i;
|
|
5567
|
+
break;
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
if (checkpointIdx !== -1) {
|
|
5571
|
+
break;
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5574
|
+
const startIdx = checkpointIdx !== -1 ? checkpointIdx + 1 : 0;
|
|
5575
|
+
const collected = [];
|
|
5576
|
+
for (let i = startIdx; i < endIndex; i++) {
|
|
5577
|
+
const msg = messages[i];
|
|
5578
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
|
|
5579
|
+
continue;
|
|
5580
|
+
}
|
|
5581
|
+
for (const block of msg.content) {
|
|
5582
|
+
if (block.type === "tool" && block.name === subAgentName && block.subAgentMessages?.length) {
|
|
5583
|
+
collected.push(...block.subAgentMessages);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
return collected;
|
|
5588
|
+
}
|
|
5589
|
+
function serializeForSummary(messages) {
|
|
5590
|
+
const toolNameById = /* @__PURE__ */ new Map();
|
|
5591
|
+
for (const msg of messages) {
|
|
5592
|
+
if (!Array.isArray(msg.content)) {
|
|
5593
|
+
continue;
|
|
5594
|
+
}
|
|
5595
|
+
for (const block of msg.content) {
|
|
5596
|
+
if (block.type === "tool") {
|
|
5597
|
+
toolNameById.set(block.id, block.name);
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
const lines = [];
|
|
5602
|
+
for (const msg of messages) {
|
|
5603
|
+
if (msg.role === "user" && msg.toolCallId) {
|
|
5604
|
+
const toolName = toolNameById.get(msg.toolCallId);
|
|
5605
|
+
if (toolName && SUBAGENT_TOOL_NAMES.has(toolName)) {
|
|
5606
|
+
const content = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.filter(
|
|
5607
|
+
(b) => b.type === "text"
|
|
5608
|
+
).map((b) => b.text).join("\n") : "";
|
|
5609
|
+
if (content.trim()) {
|
|
5610
|
+
lines.push(`[${toolName} result]: ${content}`);
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5613
|
+
continue;
|
|
5614
|
+
}
|
|
5615
|
+
if (typeof msg.content === "string") {
|
|
5616
|
+
if (msg.content.trim()) {
|
|
5617
|
+
lines.push(`[${msg.role}]: ${msg.content}`);
|
|
5618
|
+
}
|
|
5619
|
+
continue;
|
|
5620
|
+
}
|
|
5621
|
+
if (!Array.isArray(msg.content)) {
|
|
5622
|
+
continue;
|
|
5623
|
+
}
|
|
5624
|
+
const blocks = msg.content;
|
|
5625
|
+
const parts = [];
|
|
5626
|
+
let toolCount = 0;
|
|
5627
|
+
for (const block of blocks) {
|
|
5628
|
+
if (block.type === "text") {
|
|
5629
|
+
parts.push(block.text);
|
|
5630
|
+
} else if (block.type === "tool") {
|
|
5631
|
+
toolCount++;
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
if (toolCount > 0) {
|
|
5635
|
+
parts.push(`[used ${toolCount} tool${toolCount === 1 ? "" : "s"}]`);
|
|
5636
|
+
}
|
|
5637
|
+
const body = parts.join("\n").trim();
|
|
5638
|
+
if (body) {
|
|
5639
|
+
lines.push(`[${msg.role}]: ${body}`);
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
return lines.join("\n\n");
|
|
5643
|
+
}
|
|
5644
|
+
var CHUNK_CHAR_LIMIT = 24e5;
|
|
5645
|
+
async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
|
|
5646
|
+
const serialized = serializeForSummary(messagesToSummarize);
|
|
5647
|
+
if (!serialized.trim()) {
|
|
5648
|
+
return null;
|
|
5649
|
+
}
|
|
5650
|
+
if (serialized.length > CHUNK_CHAR_LIMIT && messagesToSummarize.length > 1) {
|
|
5651
|
+
const mid = Math.floor(messagesToSummarize.length / 2);
|
|
5652
|
+
log7.info("Chunking summary", {
|
|
5653
|
+
name,
|
|
5654
|
+
messageCount: messagesToSummarize.length,
|
|
5655
|
+
serializedLength: serialized.length
|
|
5656
|
+
});
|
|
5657
|
+
const [first, second] = await Promise.all([
|
|
5658
|
+
generateSummary(
|
|
5659
|
+
apiConfig,
|
|
5660
|
+
`${name} [pt1]`,
|
|
5661
|
+
compactionPrompt,
|
|
5662
|
+
messagesToSummarize.slice(0, mid),
|
|
5663
|
+
mainSystem,
|
|
5664
|
+
mainTools,
|
|
5665
|
+
model
|
|
5666
|
+
),
|
|
5667
|
+
generateSummary(
|
|
5668
|
+
apiConfig,
|
|
5669
|
+
`${name} [pt2]`,
|
|
5670
|
+
compactionPrompt,
|
|
5671
|
+
messagesToSummarize.slice(mid),
|
|
5672
|
+
mainSystem,
|
|
5673
|
+
mainTools,
|
|
5674
|
+
model
|
|
5675
|
+
)
|
|
5676
|
+
]);
|
|
5677
|
+
const parts = [first, second].filter((p) => !!p);
|
|
5678
|
+
return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
|
|
5679
|
+
}
|
|
5680
|
+
log7.info("Generating summary", {
|
|
5681
|
+
name,
|
|
5682
|
+
messageCount: messagesToSummarize.length,
|
|
5683
|
+
cacheReuse: !!mainSystem
|
|
5684
|
+
});
|
|
5685
|
+
let summaryText = "";
|
|
5686
|
+
const useMainCache = !!mainSystem;
|
|
5687
|
+
const system = useMainCache ? mainSystem : compactionPrompt;
|
|
5688
|
+
const tools2 = [];
|
|
5689
|
+
const userContent = useMainCache ? `${compactionPrompt}
|
|
5690
|
+
|
|
5691
|
+
---
|
|
5692
|
+
|
|
5693
|
+
Conversation to summarize:
|
|
5694
|
+
|
|
5695
|
+
${serialized}` : serialized;
|
|
5696
|
+
const iterStart = Date.now();
|
|
5697
|
+
for await (const event of streamChat({
|
|
5698
|
+
...apiConfig,
|
|
5699
|
+
model,
|
|
5700
|
+
subAgentId: "conversationSummarizer",
|
|
5701
|
+
system,
|
|
5702
|
+
messages: [{ role: "user", content: userContent }],
|
|
5703
|
+
tools: tools2
|
|
5704
|
+
})) {
|
|
5705
|
+
if (event.type === "text") {
|
|
5706
|
+
summaryText += event.text;
|
|
5707
|
+
} else if (event.type === "done") {
|
|
5708
|
+
recordUsage({
|
|
5709
|
+
ts: Date.now(),
|
|
5710
|
+
agentName: "conversationSummarizer",
|
|
5711
|
+
modelId: event.modelId,
|
|
5712
|
+
inputTokens: event.usage.inputTokens,
|
|
5713
|
+
outputTokens: event.usage.outputTokens,
|
|
5714
|
+
cacheCreationTokens: event.usage.cacheCreationTokens,
|
|
5715
|
+
cacheReadTokens: event.usage.cacheReadTokens,
|
|
5716
|
+
cost: nanoToDollars(event.cost),
|
|
5717
|
+
billingEvents: event.billingEvents,
|
|
5718
|
+
durationMs: Date.now() - iterStart,
|
|
5719
|
+
toolNames: []
|
|
5720
|
+
});
|
|
5721
|
+
} else if (event.type === "error") {
|
|
5722
|
+
log7.error("Summary generation failed", { name, error: event.error });
|
|
5723
|
+
return null;
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
if (!summaryText.trim()) {
|
|
5727
|
+
log7.warn("Empty summary generated", { name });
|
|
5728
|
+
return null;
|
|
5729
|
+
}
|
|
5730
|
+
log7.info("Summary generated", { name, summaryLength: summaryText.length });
|
|
5731
|
+
return summaryText.trim();
|
|
5732
|
+
}
|
|
5733
|
+
|
|
5694
5734
|
// src/compaction/trigger.ts
|
|
5695
5735
|
var log8 = createLogger("compaction:trigger");
|
|
5696
5736
|
var pendingSummaries = [];
|