Skip to content

Commit f6c9496

Browse files
committed
fix 02agent
1 parent b5e7a3b commit f6c9496

2 files changed

Lines changed: 230 additions & 0 deletions

File tree

src/addons/addons/02agent/tools.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
repairListVariableValues,
88
replaceBlocksRangeByUCF,
99
replaceScriptByUCF,
10+
replaceTargetScriptsByUCFSections,
1011
} from "./workspaceRangeTools";
1112
import { setGetBlockInfoTool, setRuntime } from "./converter";
1213
import scratchBlocksCatalog from "./scratch_blocks.json";
@@ -1883,6 +1884,103 @@ export class AITools {
18831884
return { errors, warnings };
18841885
}
18851886

1887+
private _getScriptStructureFingerprint(section: VirtualScriptSection) {
1888+
const blocks = ucfToScratch(normalizeModelUCF(section.code), {
1889+
runtime: this.vm.runtime,
1890+
includeComments: true,
1891+
});
1892+
const opcodeCounts: Record<string, number> = {};
1893+
const definedProcedures: string[] = [];
1894+
const calledProcedures: string[] = [];
1895+
1896+
blocks.forEach((block: any) => {
1897+
const opcode = String(block?.opcode || "");
1898+
opcodeCounts[opcode] = (opcodeCounts[opcode] || 0) + 1;
1899+
const proccode = block?.mutation?.proccode;
1900+
if (opcode === "procedures_prototype" && proccode) {
1901+
definedProcedures.push(normalizeProcedureSignature(proccode));
1902+
} else if (opcode === "procedures_call" && proccode) {
1903+
calledProcedures.push(normalizeProcedureSignature(proccode));
1904+
}
1905+
});
1906+
1907+
return {
1908+
blockCount: blocks.length,
1909+
topOpcodes: blocks.filter((block: any) => block?.topLevel).map((block: any) => block.opcode).sort(),
1910+
opcodeCounts,
1911+
definedProcedures: definedProcedures.sort(),
1912+
calledProcedures: calledProcedures.sort(),
1913+
};
1914+
}
1915+
1916+
private _verifyTargetSyncResult(entry: VirtualFileEntry, expectedContent: string) {
1917+
const currentEntry = this._getVirtualFile(entry.path);
1918+
if (!currentEntry || currentEntry.kind !== "target") {
1919+
return {
1920+
success: false,
1921+
error: `Could not read back virtual target file after sync: ${entry.path}`,
1922+
};
1923+
}
1924+
1925+
const expectedSections = extractVirtualScriptSections(expectedContent);
1926+
const actualSections = extractVirtualScriptSections(currentEntry.content);
1927+
const expectedIds = expectedSections.map((section) => section.scriptId);
1928+
const actualIds = actualSections.map((section) => section.scriptId);
1929+
const missingScriptIds = expectedIds.filter((scriptId) => !actualIds.includes(scriptId));
1930+
const extraScriptIds = actualIds.filter((scriptId) => !expectedIds.includes(scriptId));
1931+
1932+
if (missingScriptIds.length || extraScriptIds.length || expectedSections.length !== actualSections.length) {
1933+
return {
1934+
success: false,
1935+
error: "VM readback script set does not match patched virtual file.",
1936+
expectedScriptIds: expectedIds,
1937+
actualScriptIds: actualIds,
1938+
missingScriptIds,
1939+
extraScriptIds,
1940+
};
1941+
}
1942+
1943+
const actualById = new Map(actualSections.map((section) => [section.scriptId, section]));
1944+
const mismatches: any[] = [];
1945+
1946+
expectedSections.forEach((expectedSection) => {
1947+
const actualSection = actualById.get(expectedSection.scriptId);
1948+
if (!actualSection) return;
1949+
if (expectedSection.normalizedCode === actualSection.normalizedCode) return;
1950+
1951+
try {
1952+
const expectedFingerprint = this._getScriptStructureFingerprint(expectedSection);
1953+
const actualFingerprint = this._getScriptStructureFingerprint(actualSection);
1954+
if (JSON.stringify(expectedFingerprint) !== JSON.stringify(actualFingerprint)) {
1955+
mismatches.push({
1956+
scriptId: expectedSection.scriptId,
1957+
expected: expectedFingerprint,
1958+
actual: actualFingerprint,
1959+
});
1960+
}
1961+
} catch (error) {
1962+
mismatches.push({
1963+
scriptId: expectedSection.scriptId,
1964+
error: error instanceof Error ? error.message : String(error),
1965+
});
1966+
}
1967+
});
1968+
1969+
if (mismatches.length) {
1970+
return {
1971+
success: false,
1972+
error: "VM readback script structure does not match patched virtual file.",
1973+
mismatches,
1974+
};
1975+
}
1976+
1977+
return {
1978+
success: true,
1979+
scriptCount: actualSections.length,
1980+
scriptIds: actualIds,
1981+
};
1982+
}
1983+
18861984
private _validateVirtualTargetFile(entry: VirtualFileEntry, content: string) {
18871985
const sections = extractVirtualScriptSections(content);
18881986
const diagnostics: any = {
@@ -2135,6 +2233,38 @@ export class AITools {
21352233
}
21362234
}
21372235

2236+
if (operations.length > 0) {
2237+
const result: any = await replaceTargetScriptsByUCFSections(
2238+
this.vm,
2239+
window.Blockly.getMainWorkspace() as Blockly.WorkspaceSvg,
2240+
entry.targetId || "",
2241+
newSections.map((section) => ({ scriptId: section.scriptId, code: section.code })),
2242+
{
2243+
includeComments: true,
2244+
},
2245+
);
2246+
if (!result.success) {
2247+
throw new Error(this._formatSyncFailure("Failed to sync target", entry.path, result));
2248+
}
2249+
2250+
const verification = this._verifyTargetSyncResult(entry, newContent);
2251+
if (!verification.success) {
2252+
throw new Error(`Target sync verification failed for ${entry.path}: ${verification.error} ${JSON.stringify(verification).slice(0, 1200)}`);
2253+
}
2254+
2255+
return {
2256+
path: entry.path,
2257+
targetId: entry.targetId,
2258+
operationCount: operations.length,
2259+
operations: operations.map((operation) => ({
2260+
type: operation.type,
2261+
scriptId: operation.section?.scriptId || operation.oldSection?.scriptId,
2262+
result,
2263+
})),
2264+
verification,
2265+
};
2266+
}
2267+
21382268
const results = [];
21392269
for (const operation of operations) {
21402270
if (operation.type === "replace") {
@@ -4834,6 +4964,12 @@ export class AITools {
48344964
this._moveMenuInputsToFields(result);
48354965
this._dedupeFieldAndInputNames(result);
48364966
this._normalizeArgumentReporterInfo(resolvedOpcode, result);
4967+
if (!result.found) {
4968+
const triedText = resolvedOpcode !== requestedOpcode ? ` (also tried "${resolvedOpcode}")` : "";
4969+
throw new Error(
4970+
`Unknown Scratch block opcode "${requestedOpcode}"${triedText}. Use searchBlocks or getBlockHelp to find the exact DSL call/opcode before applying a patch.`,
4971+
);
4972+
}
48374973

48384974
if (!result.found) {
48394975
if (resolvedOpcode !== requestedOpcode) {

src/addons/addons/02agent/workspaceRangeTools.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,100 @@ export const replaceScriptByUCF = async (
896896
};
897897
};
898898

899+
export const replaceTargetScriptsByUCFSections = async (
900+
vm: PluginContext["vm"],
901+
_workspace: Blockly.WorkspaceSvg,
902+
targetId: string,
903+
sections: Array<{ scriptId: string; code: string }>,
904+
options: { includeComments?: boolean } = {},
905+
) => {
906+
const target = targetId ? vm.runtime.getTargetById(targetId) : vm.editingTarget;
907+
if (!target) {
908+
return buildFailureResult("Target not found", "resolve_target", { targetId });
909+
}
910+
911+
const workspace = _workspace || (window.Blockly.getMainWorkspace() as Blockly.WorkspaceSvg);
912+
const existingTopLevelIds = Object.values(target.blocks?._blocks || {})
913+
.filter((block: any) => block?.topLevel && !block?.parent)
914+
.map((block: any) => block.id);
915+
const seenTopLevelIds = new Set<string>();
916+
const parsedScripts: Array<{ scriptId: string; blocksState: any[]; topLevelBlockId: string }> = [];
917+
918+
try {
919+
for (const section of sections) {
920+
const blocksState = ucfToScratch(normalizeModelUCF(section.code), {
921+
runtime: vm.runtime,
922+
includeComments: options.includeComments === true,
923+
});
924+
if (!blocksState.length) {
925+
return buildFailureResult("Script UCF produced no blocks", "parse_target_script", {
926+
targetId: target.id,
927+
scriptId: section.scriptId,
928+
});
929+
}
930+
931+
const topLevelBlocks = blocksState.filter((blockState) => blockState.topLevel);
932+
if (topLevelBlocks.length !== 1) {
933+
return buildFailureResult("Each script section must produce exactly one top-level stack", "validate_target_script_topology", {
934+
targetId: target.id,
935+
scriptId: section.scriptId,
936+
topLevelBlockCount: topLevelBlocks.length,
937+
});
938+
}
939+
940+
const topLevelBlock = topLevelBlocks[0];
941+
if (section.scriptId && topLevelBlock.id !== section.scriptId) {
942+
const oldTopLevelId = topLevelBlock.id;
943+
if (seenTopLevelIds.has(section.scriptId)) {
944+
return buildFailureResult("Duplicate script id in target file", "validate_target_script_ids", {
945+
targetId: target.id,
946+
scriptId: section.scriptId,
947+
});
948+
}
949+
topLevelBlock.id = section.scriptId;
950+
blocksState.forEach((blockState) => {
951+
if (blockState.next === oldTopLevelId) blockState.next = section.scriptId;
952+
if (blockState.parent === oldTopLevelId) blockState.parent = section.scriptId;
953+
Object.values(blockState.inputs || {}).forEach((input: any) => {
954+
if (input.block === oldTopLevelId) input.block = section.scriptId;
955+
if (input.shadow === oldTopLevelId) input.shadow = section.scriptId;
956+
});
957+
});
958+
}
959+
960+
seenTopLevelIds.add(topLevelBlock.id);
961+
resolveVariableReferences(vm, workspace, blocksState);
962+
parsedScripts.push({ scriptId: section.scriptId, blocksState, topLevelBlockId: topLevelBlock.id });
963+
}
964+
965+
setBlocklyEventGroup(true);
966+
const removed = existingTopLevelIds.map((topBlockId) => removeRuntimeScriptBlocks(target, topBlockId));
967+
const inserted = parsedScripts.map((script) => insertRuntimeBlocks(target, script.blocksState));
968+
repairListVariableValues(vm, target.id);
969+
const refreshError = await refreshWorkspaceAfterRuntimeWrite(vm, target);
970+
971+
return {
972+
success: true,
973+
syncMode: "vm-direct-target",
974+
targetId: target.id,
975+
scriptCount: parsedScripts.length,
976+
removedScriptIds: existingTopLevelIds,
977+
insertedScriptIds: parsedScripts.map((script) => script.topLevelBlockId),
978+
removedBlockCount: removed.reduce((sum, item) => sum + item.removedBlockIds.length, 0),
979+
insertedBlockCount: inserted.reduce((sum, item) => sum + item.insertedBlockIds.length, 0),
980+
commentCount: inserted.reduce((sum, item) => sum + item.insertedCommentIds.length, 0),
981+
workspaceRefreshWarning: refreshError || undefined,
982+
};
983+
} catch (error) {
984+
return buildFailureResult(error instanceof Error ? error.message : "Failed to replace target scripts", "exception", {
985+
targetId: target.id,
986+
parsedScriptCount: parsedScripts.length,
987+
});
988+
} finally {
989+
setBlocklyEventGroup(false);
990+
}
991+
};
992+
899993
export const insertScriptByUCF = async (
900994
vm: PluginContext["vm"],
901995
_workspace: Blockly.WorkspaceSvg,

0 commit comments

Comments
 (0)