@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.20
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/README.md +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +137 -0
- package/skills/c64-docs/references/page-types.md +113 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +193 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +89 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -60
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +203 -32
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +137 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -8
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
package/src/mcp/main.ts
CHANGED
|
@@ -13,25 +13,23 @@ import { machineTools } from "./tools/machine.ts";
|
|
|
13
13
|
import { mediaTools } from "./tools/media.ts";
|
|
14
14
|
import { memoryTools } from "./tools/memory.ts";
|
|
15
15
|
import { videoTools } from "./tools/video.ts";
|
|
16
|
+
import { PACKAGE } from "../package.ts";
|
|
16
17
|
|
|
17
18
|
const HELP = `c64-re-tools-mcp
|
|
18
19
|
|
|
19
20
|
Usage:
|
|
20
|
-
npx -y --package
|
|
21
|
+
npx -y --package=${PACKAGE.name}@${PACKAGE.tag} c64-re-tools-mcp
|
|
21
22
|
c64-re-tools-mcp --help
|
|
22
23
|
|
|
23
24
|
Runs the stateful C64 MCP server on stdio. Each c64-re-tools-mcp process owns
|
|
24
25
|
one C64 emulator, started by the Host Runtime (c64-re-tools-host) when this
|
|
25
26
|
server starts and stopped when it exits. c64-re-tools install declares it for
|
|
26
|
-
the agent, through npx with the
|
|
27
|
+
the agent, through npx with the newest version of its channel.
|
|
27
28
|
|
|
28
29
|
Environment:
|
|
29
30
|
C64RT_VIDEO pal (default) or ntsc; fixed for the life of the process.
|
|
30
31
|
C64RT_HOST host:port of the Host Runtime when it is not on 127.0.0.1:6464,
|
|
31
|
-
host.docker.internal:6464 or host.containers.internal:6464
|
|
32
|
-
C64RT_HOST_TOKEN
|
|
33
|
-
the shared secret of a Host Runtime that listens on an address
|
|
34
|
-
that is not loopback. Use the same value as the Host Runtime.`;
|
|
32
|
+
host.docker.internal:6464 or host.containers.internal:6464.`;
|
|
35
33
|
|
|
36
34
|
function log(line: string): void {
|
|
37
35
|
process.stderr.write(`c64-re-tools-mcp: ${line}\n`);
|
|
@@ -45,32 +43,59 @@ function videoStandard(env: NodeJS.ProcessEnv): VideoStandard {
|
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
/**
|
|
48
|
-
* One session
|
|
49
|
-
*
|
|
50
|
-
* host
|
|
51
|
-
*
|
|
46
|
+
* One session at a time (1 MCP = 1 session = 1 VICE). It opens at start; if
|
|
47
|
+
* that fails (for example the host was not running yet), the next tool call
|
|
48
|
+
* tries again. When the host connection is lost, the next call opens a new
|
|
49
|
+
* session (D57). A lost session that the emulator answered first gives its
|
|
50
|
+
* machine-state-lost once, unless a pending call got it already. Close stops
|
|
52
51
|
* an open that is still waiting for the host at once.
|
|
53
52
|
*/
|
|
54
53
|
function sessionSource(): { get: SessionSource; close(): Promise<void> } {
|
|
55
54
|
let opening: Promise<ViceSessionClient> | undefined;
|
|
55
|
+
let lostNotice: WireFailure | undefined;
|
|
56
56
|
let closed = false;
|
|
57
57
|
const stop = new AbortController();
|
|
58
58
|
const open = () => {
|
|
59
59
|
const started = performance.now();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const current = (async () => ViceSessionClient.open({ videoStandard: videoStandard(process.env), signal: stop.signal }))();
|
|
61
|
+
opening = current;
|
|
62
|
+
void current.then(
|
|
63
|
+
(client) => {
|
|
64
|
+
trace().event("session.opened", { ms: elapsedMs(started) });
|
|
65
|
+
void client.closed.then(() => drop(current, client));
|
|
66
|
+
},
|
|
67
|
+
() => {},
|
|
68
|
+
);
|
|
69
|
+
current.catch((error: unknown) => {
|
|
63
70
|
log(`session not opened: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
|
-
opening = undefined;
|
|
71
|
+
if (opening === current) opening = undefined;
|
|
65
72
|
});
|
|
66
|
-
return
|
|
73
|
+
return current;
|
|
74
|
+
};
|
|
75
|
+
/** Forgets a lost session, once; the next call opens a new one. */
|
|
76
|
+
const drop = (current: Promise<ViceSessionClient>, client: ViceSessionClient) => {
|
|
77
|
+
if (closed || opening !== current) return;
|
|
78
|
+
log("the connection to the host runtime was lost; the next call opens a new session");
|
|
79
|
+
opening = undefined;
|
|
80
|
+
if (client.answered) lostNotice = client.unreportedLoss;
|
|
67
81
|
};
|
|
68
82
|
open();
|
|
83
|
+
const get = async (): Promise<ViceSessionClient> => {
|
|
84
|
+
if (closed) throw new WireFailure("machine-unavailable", "The C64 session is shutting down.");
|
|
85
|
+
if (lostNotice !== undefined) {
|
|
86
|
+
const notice = lostNotice;
|
|
87
|
+
lostNotice = undefined;
|
|
88
|
+
throw notice;
|
|
89
|
+
}
|
|
90
|
+
const current = opening ?? open();
|
|
91
|
+
const client = await current;
|
|
92
|
+
if (!client.lost) return client;
|
|
93
|
+
// The loss is known here before the close event reaches drop.
|
|
94
|
+
drop(current, client);
|
|
95
|
+
return get();
|
|
96
|
+
};
|
|
69
97
|
return {
|
|
70
|
-
get
|
|
71
|
-
if (closed) return Promise.reject(new WireFailure("machine-unavailable", "The C64 session is shutting down."));
|
|
72
|
-
return opening ?? open();
|
|
73
|
-
},
|
|
98
|
+
get,
|
|
74
99
|
async close() {
|
|
75
100
|
closed = true;
|
|
76
101
|
stop.abort();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Tool results written to a project file instead of the context (D39). The
|
|
2
|
+
// MCP server runs on the agent's machine, so it writes the file itself; the
|
|
3
|
+
// host never sees the path. The project is the folder that `project` names,
|
|
4
|
+
// or the one that the D67 rules find from the folder the agent started in (D68).
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
import { projectFolder, writeProjectFile } from "../host-client/transfer.ts";
|
|
11
|
+
|
|
12
|
+
export const OutInput = z.string().min(1).describe("write the result to this file, relative to the project folder, in place of returning it. An existing file is overwritten");
|
|
13
|
+
|
|
14
|
+
/** The project of a tool with a path (D67, D68). */
|
|
15
|
+
export const ProjectInput = z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1)
|
|
18
|
+
.describe("the project folder, relative to the folder where the agent started. Give it when that folder is not in a project and has projects below it. The paths of this call are relative to the project folder");
|
|
19
|
+
|
|
20
|
+
/** What a result says in place of the bytes it wrote. */
|
|
21
|
+
export interface Written {
|
|
22
|
+
out: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
sha256: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The fields of a result with out, optional in an output schema that also has the inline form. */
|
|
28
|
+
export const WrittenOutput = {
|
|
29
|
+
out: z.string().optional(),
|
|
30
|
+
bytes: z.number().int().min(0).optional(),
|
|
31
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Writes `data` to the file `path` of the project that `project` names (or the D67 lookup finds), and gives what the result shows in its place. */
|
|
35
|
+
export function writeOut(path: string, data: Uint8Array | string, project: string | undefined): Written {
|
|
36
|
+
const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
37
|
+
writeProjectFile(path, bytes, { what: "out", root: projectFolder(project) });
|
|
38
|
+
return { out: path, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
39
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -8,8 +8,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema, type CallToolResult, typ
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
|
|
10
10
|
import { formatC64Address, parseC64Address } from "../c64.ts";
|
|
11
|
-
import type
|
|
12
|
-
import { WireFailure, type WireError } from "../protocol/messages.ts";
|
|
11
|
+
import { LostBeforeAnswer, type ViceSessionClient } from "../host-client/vice-session.ts";
|
|
12
|
+
import { shownMessage, WireFailure, type WireError } from "../protocol/messages.ts";
|
|
13
13
|
import { COMPARISONS, CONDITION_REGISTERS, MEMORY_VIEWS, SPACES, type Condition } from "../protocol/vice.ts";
|
|
14
14
|
import { elapsedMs, trace, type TraceFields } from "../trace.ts";
|
|
15
15
|
|
|
@@ -38,7 +38,10 @@ export type ViceSessionApi = Pick<
|
|
|
38
38
|
| "screenBaselines"
|
|
39
39
|
| "screenDiscard"
|
|
40
40
|
| "snapshot"
|
|
41
|
+
| "snapshotExport"
|
|
42
|
+
| "snapshotRestore"
|
|
41
43
|
| "cpuHistory"
|
|
44
|
+
| "trace"
|
|
42
45
|
| "backtrace"
|
|
43
46
|
| "timing"
|
|
44
47
|
| "profile"
|
|
@@ -206,7 +209,7 @@ export function toolListing(tool: ToolDefinition): McpTool {
|
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
function errorResult(error: WireError): CallToolResult {
|
|
209
|
-
return { isError: true, content: [{ type: "text", text: JSON.stringify({ code: error.code, message: error
|
|
212
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ code: error.code, message: shownMessage(error) }) }] };
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
/** "address: Invalid string: must match ..." style messages, one sentence per problem. */
|
|
@@ -259,7 +262,14 @@ async function runCall(
|
|
|
259
262
|
const parsed = tool.inputSchema.safeParse(args ?? {});
|
|
260
263
|
if (!parsed.success) return errorResult({ code: "invalid-input", message: describeIssues(parsed.error) });
|
|
261
264
|
try {
|
|
262
|
-
|
|
265
|
+
let output: unknown;
|
|
266
|
+
try {
|
|
267
|
+
output = await tool.run(parsed.data, await session());
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// A session that the emulator never answered had no machine state: the call goes again on a new session (D57).
|
|
270
|
+
if (!(error instanceof LostBeforeAnswer)) throw error;
|
|
271
|
+
output = await tool.run(parsed.data, await session());
|
|
272
|
+
}
|
|
263
273
|
// The result goes out in the shape that the tool listing advertises: keys
|
|
264
274
|
// that the output schema does not name are removed, and a result that does
|
|
265
275
|
// not fit the schema is an internal failure.
|
|
@@ -291,6 +301,7 @@ const INSTRUCTIONS =
|
|
|
291
301
|
"These tools control one live Commodore 64 emulator that belongs to this session. " +
|
|
292
302
|
"Addresses are four hex digits after a dollar sign, for example $c000. " +
|
|
293
303
|
"Memory data is lowercase hex without separators. " +
|
|
304
|
+
"Other register values and chip values are decimal numbers. " +
|
|
294
305
|
"A read does not change the run state of the machine.";
|
|
295
306
|
|
|
296
307
|
export function createMcpServer(options: { tools: readonly ToolDefinition[]; session: SessionSource; log?: (line: string) => void }): Server {
|
package/src/mcp/tools/debug.ts
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
|
-
import { formatC64Address } from "../../c64.ts";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { COVERAGE, executedRamCode, EXECUTE, formatC64Address, READ, WRITE, type CodeRange } from "../../c64.ts";
|
|
7
|
+
import { WireFailure } from "../../protocol/messages.ts";
|
|
8
|
+
import { CHECKPOINT_ACTIONS, MAX_BACKTRACE, MAX_HISTORY, MAX_TRACE_FRAMES, MAX_TRACE_HITS, MAX_TRACE_POINTS, TRACE_KINDS, TRACE_ENDS, MAX_MEMMAP_RANGES, MAX_MEMORY_READ, MAX_PROFILE, MAX_WATCH_SIZE, SPACES, TIMING_ACTIONS, WATCH_ACCESS, type Breakpoint, type Watchpoint } from "../../protocol/vice.ts";
|
|
9
|
+
import { readCoverage } from "../../host-client/coverage.ts";
|
|
10
|
+
import { OutInput, ProjectInput, writeOut, WrittenOutput } from "../output.ts";
|
|
11
|
+
import { AddressInput, AddressOutput, Byte, ConditionInput, ConditionOutput, defineTool, HexData, requireFields, showCondition, SpaceInput, toCondition, type ViceSessionApi } from "../server.ts";
|
|
12
|
+
import { spaced } from "./memory.ts";
|
|
9
13
|
|
|
10
14
|
/** No default here: a filled-in default would count as a field the other actions refuse. */
|
|
11
15
|
const OptionalSpace = z.enum(SPACES).optional().describe("add only: c64 (default) or drive8, the 1541 disk drive CPU");
|
|
@@ -37,7 +41,8 @@ export const c64Breakpoint = defineTool({
|
|
|
37
41
|
description:
|
|
38
42
|
"Manage breakpoints. A breakpoint stops the CPU when it is about to execute the instruction at an address. " +
|
|
39
43
|
"Action add sets one at address in space c64 or drive8. With a condition, it stops only when the condition is true. " +
|
|
40
|
-
"add returns the new id.
|
|
44
|
+
"add returns the new id. Breakpoints and watchpoints take their ids from one counter, so the ids of one kind can have gaps. " +
|
|
45
|
+
"Actions remove, enable and disable take that id. remove returns removed true and the id. Action list gives all breakpoints. Each breakpoint shows its condition, when it has one. " +
|
|
41
46
|
"Breakpoints stay through a reset.",
|
|
42
47
|
inputSchema: z
|
|
43
48
|
.object({
|
|
@@ -48,7 +53,7 @@ export const c64Breakpoint = defineTool({
|
|
|
48
53
|
id: Id.optional().describe("remove, enable and disable only"),
|
|
49
54
|
})
|
|
50
55
|
.strict(),
|
|
51
|
-
outputSchema: BreakpointOutput.partial().extend({ breakpoints: z.array(BreakpointOutput).optional() }),
|
|
56
|
+
outputSchema: BreakpointOutput.partial().extend({ removed: z.literal(true).optional(), breakpoints: z.array(BreakpointOutput).optional() }),
|
|
52
57
|
readOnly: false,
|
|
53
58
|
async run(input, session) {
|
|
54
59
|
const { action, ...fields } = input;
|
|
@@ -59,6 +64,7 @@ export const c64Breakpoint = defineTool({
|
|
|
59
64
|
}
|
|
60
65
|
if (action !== "add") {
|
|
61
66
|
requireFields(action, fields, ["id"], ["id"]);
|
|
67
|
+
if (action === "remove") return session.breakpoint({ action, id: fields.id! });
|
|
62
68
|
return showBreakpoint(await session.breakpoint({ action, id: fields.id! }));
|
|
63
69
|
}
|
|
64
70
|
requireFields(action, fields, ["address", "space", "condition"], ["address"]);
|
|
@@ -79,7 +85,8 @@ export const c64Watchpoint = defineTool({
|
|
|
79
85
|
"Manage watchpoints. A watchpoint stops the CPU when an instruction reads or writes memory in a range. " +
|
|
80
86
|
`Action add sets one at address for size bytes (1 to ${MAX_WATCH_SIZE}, default 1), with access read, write or read-write. ` +
|
|
81
87
|
"With a condition, it stops only when the condition is true. For a write, a memory condition sees the memory after the write. " +
|
|
82
|
-
"add returns the new id.
|
|
88
|
+
"add returns the new id. Breakpoints and watchpoints take their ids from one counter, so the ids of one kind can have gaps. " +
|
|
89
|
+
"Actions remove, enable and disable take that id. remove returns removed true and the id. Action list gives all watchpoints. Each watchpoint shows its condition, when it has one.",
|
|
83
90
|
inputSchema: z
|
|
84
91
|
.object({
|
|
85
92
|
action: z.enum(CHECKPOINT_ACTIONS),
|
|
@@ -91,7 +98,7 @@ export const c64Watchpoint = defineTool({
|
|
|
91
98
|
id: Id.optional().describe("remove, enable and disable only"),
|
|
92
99
|
})
|
|
93
100
|
.strict(),
|
|
94
|
-
outputSchema: WatchpointOutput.partial().extend({ watchpoints: z.array(WatchpointOutput).optional() }),
|
|
101
|
+
outputSchema: WatchpointOutput.partial().extend({ removed: z.literal(true).optional(), watchpoints: z.array(WatchpointOutput).optional() }),
|
|
95
102
|
readOnly: false,
|
|
96
103
|
async run(input, session) {
|
|
97
104
|
const { action, ...fields } = input;
|
|
@@ -102,6 +109,7 @@ export const c64Watchpoint = defineTool({
|
|
|
102
109
|
}
|
|
103
110
|
if (action !== "add") {
|
|
104
111
|
requireFields(action, fields, ["id"], ["id"]);
|
|
112
|
+
if (action === "remove") return session.watchpoint({ action, id: fields.id! });
|
|
105
113
|
return showWatchpoint(await session.watchpoint({ action, id: fields.id! }));
|
|
106
114
|
}
|
|
107
115
|
requireFields(action, fields, ["address", "size", "access", "space", "condition"], ["address", "access"]);
|
|
@@ -123,9 +131,11 @@ export const c64CpuHistory = defineTool({
|
|
|
123
131
|
description:
|
|
124
132
|
"Get the last instructions that the CPU executed, oldest first. " +
|
|
125
133
|
`limit is the number of instructions (1 to ${MAX_HISTORY}, default 50). ` +
|
|
126
|
-
"Each entry gives the address, the bytes, the instruction and the registers a, x, y and sp. " +
|
|
127
|
-
"In space c64, it also gives the raster line and cycle when the instruction started.
|
|
128
|
-
|
|
134
|
+
"Each entry gives the address, the bytes, the instruction and the registers a, x, y and sp before the instruction executed. " +
|
|
135
|
+
"In space c64, it also gives the raster line and cycle when the instruction started. " +
|
|
136
|
+
"With out, it writes the entries as text lines to that project file, and the result gives the file in place of the entries. " +
|
|
137
|
+
"This does not change the run state of the machine.",
|
|
138
|
+
inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_HISTORY).default(50), space: SpaceInput, out: OutInput.optional(), project: ProjectInput.optional() }).strict(),
|
|
129
139
|
outputSchema: z.object({
|
|
130
140
|
entries: z.array(
|
|
131
141
|
z.object({
|
|
@@ -139,12 +149,86 @@ export const c64CpuHistory = defineTool({
|
|
|
139
149
|
rasterLine: z.number().int().min(0).optional(),
|
|
140
150
|
rasterCycle: z.number().int().min(0).optional(),
|
|
141
151
|
}),
|
|
152
|
+
).optional(),
|
|
153
|
+
count: z.number().int().min(0).optional(),
|
|
154
|
+
...WrittenOutput,
|
|
155
|
+
}),
|
|
156
|
+
readOnly: false,
|
|
157
|
+
async run(input, session) {
|
|
158
|
+
const { out, project, ...request } = input;
|
|
159
|
+
if (out === undefined && project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
|
|
160
|
+
const { entries } = await session.cpuHistory(request);
|
|
161
|
+
if (out === undefined) return { entries: entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
|
|
162
|
+
const byte = (value: number) => value.toString(16).padStart(2, "0");
|
|
163
|
+
const lines = entries.map((entry) => {
|
|
164
|
+
const raster = entry.rasterLine === undefined ? "" : ` line ${entry.rasterLine} cycle ${entry.rasterCycle ?? 0}`;
|
|
165
|
+
return `${formatC64Address(entry.address)} ${spaced(entry.bytes).padEnd(9)} ${entry.text.padEnd(16)} a=${byte(entry.a)} x=${byte(entry.x)} y=${byte(entry.y)} sp=${byte(entry.sp)}${raster}`;
|
|
166
|
+
});
|
|
167
|
+
return { count: entries.length, ...writeOut(out, `${lines.join("\n")}\n`, project) };
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/** The hits that a trace result gives inline; out gives all of them. */
|
|
172
|
+
const TRACE_LISTED = 200;
|
|
173
|
+
|
|
174
|
+
export const c64Trace = defineTool({
|
|
175
|
+
name: "c64_trace",
|
|
176
|
+
title: "Trace accesses and instructions",
|
|
177
|
+
description:
|
|
178
|
+
"Run the machine and record each hit of the points. The program does not stop for you. " +
|
|
179
|
+
"A point is exec (an instruction at an address from start to end), read or write (an access from start to end). " +
|
|
180
|
+
`Give 1 to ${MAX_TRACE_POINTS} points. ` +
|
|
181
|
+
"Each hit gives the instruction (pc, bytes, text) and the registers a, x, y and sp before it. " +
|
|
182
|
+
"It also gives the frame from 0, and the raster line and cycle where the instruction started. " +
|
|
183
|
+
"A read or write hit also gives the address that the instruction used, and the byte there after the access. " +
|
|
184
|
+
`The trace ends after frames frames (1 to ${MAX_TRACE_FRAMES}), at maxHits hits (at most ${MAX_TRACE_HITS}), or at a breakpoint, a watchpoint or a JAM. ` +
|
|
185
|
+
`The result lists the first ${TRACE_LISTED} hits. With out, it writes all hits as JSON lines to that project file. ` +
|
|
186
|
+
"Each hit stops the machine for a short time. Thus a point with many hits in each frame makes the trace slow. Use a small range and a maxHits. " +
|
|
187
|
+
"The machine is stopped after the trace.",
|
|
188
|
+
inputSchema: z
|
|
189
|
+
.object({
|
|
190
|
+
points: z.array(z.object({ kind: z.enum(TRACE_KINDS), start: AddressInput, end: AddressInput.optional() }).strict()).min(1).max(MAX_TRACE_POINTS),
|
|
191
|
+
frames: z.number().int().min(1).max(MAX_TRACE_FRAMES),
|
|
192
|
+
maxHits: z.number().int().min(1).max(MAX_TRACE_HITS).default(MAX_TRACE_HITS),
|
|
193
|
+
out: OutInput.optional(),
|
|
194
|
+
project: ProjectInput.optional(),
|
|
195
|
+
})
|
|
196
|
+
.strict(),
|
|
197
|
+
outputSchema: z.object({
|
|
198
|
+
end: z.enum(TRACE_ENDS),
|
|
199
|
+
frames: z.number().int().min(0),
|
|
200
|
+
pc: AddressOutput,
|
|
201
|
+
count: z.number().int().min(0),
|
|
202
|
+
hits: z.array(
|
|
203
|
+
z.object({
|
|
204
|
+
point: z.number().int().min(0),
|
|
205
|
+
pc: AddressOutput,
|
|
206
|
+
bytes: HexData,
|
|
207
|
+
text: z.string(),
|
|
208
|
+
a: Byte,
|
|
209
|
+
x: Byte,
|
|
210
|
+
y: Byte,
|
|
211
|
+
sp: Byte,
|
|
212
|
+
frame: z.number().int().min(0),
|
|
213
|
+
rasterLine: z.number().int().min(0),
|
|
214
|
+
rasterCycle: z.number().int().min(0),
|
|
215
|
+
address: AddressOutput.optional(),
|
|
216
|
+
value: Byte.optional(),
|
|
217
|
+
}),
|
|
142
218
|
),
|
|
219
|
+
...WrittenOutput,
|
|
143
220
|
}),
|
|
144
|
-
readOnly:
|
|
221
|
+
readOnly: false,
|
|
145
222
|
async run(input, session) {
|
|
146
|
-
const {
|
|
147
|
-
|
|
223
|
+
const { out, project, points, ...request } = input;
|
|
224
|
+
if (out === undefined && project !== undefined) throw new WireFailure("invalid-input", "project is used only with out.");
|
|
225
|
+
const ranges = points.map((point) => ({ kind: point.kind, start: point.start, end: point.end ?? point.start }));
|
|
226
|
+
if (ranges.some((point) => point.end < point.start)) throw new WireFailure("invalid-input", "A point end must not be before its start.");
|
|
227
|
+
const traced = await session.trace({ points: ranges, ...request });
|
|
228
|
+
const shown = traced.hits.map(({ address, ...hit }) => ({ ...hit, pc: formatC64Address(hit.pc), ...(address === undefined ? {} : { address: formatC64Address(address) }) }));
|
|
229
|
+
const summary = { end: traced.end, frames: traced.frames, pc: formatC64Address(traced.pc), count: traced.hits.length, hits: shown.slice(0, TRACE_LISTED) };
|
|
230
|
+
if (out === undefined) return summary;
|
|
231
|
+
return { ...summary, ...writeOut(out, `${shown.map((hit) => JSON.stringify(hit)).join("\n")}\n`, project) };
|
|
148
232
|
},
|
|
149
233
|
});
|
|
150
234
|
|
|
@@ -174,7 +258,8 @@ export const c64Timing = defineTool({
|
|
|
174
258
|
title: "Cycle stopwatch",
|
|
175
259
|
description:
|
|
176
260
|
"A stopwatch that counts C64 CPU cycles. Action start sets it to zero. Action read gives the cycles since the last start, " +
|
|
177
|
-
"or since the session started, as a decimal string. A
|
|
261
|
+
"or since the session started, as a decimal string. A reset and c64_autostart start it again from the reset. " +
|
|
262
|
+
"A PAL frame has 19656 cycles and an NTSC frame 17095.",
|
|
178
263
|
inputSchema: z.object({ action: z.enum(TIMING_ACTIONS) }).strict(),
|
|
179
264
|
outputSchema: z.object({ started: z.boolean().optional(), cycles: z.string().regex(/^\d+$/).optional() }),
|
|
180
265
|
readOnly: false,
|
|
@@ -187,51 +272,137 @@ export const c64Profile = defineTool({
|
|
|
187
272
|
name: "c64_profile",
|
|
188
273
|
title: "Cycle profile",
|
|
189
274
|
description:
|
|
190
|
-
"
|
|
191
|
-
|
|
275
|
+
"Action read gets the routines where the C64 CPU spent the most cycles since the session started, the last clear or the last c64_window. " +
|
|
276
|
+
"Action clear sets all counts to zero, so that the next read measures only what runs after it. " +
|
|
277
|
+
"The emulator gives the cycles to the routine that a JSR called last. " +
|
|
278
|
+
"Code that the CPU enters with JMP or a branch counts for that routine. " +
|
|
279
|
+
"After c64_window or a snapshot restore, the emulator can give the cycles to a routine that ran before the restore, for example $fd50. Do not trust the routine addresses then. " +
|
|
280
|
+
`limit is the maximum number of routines for read (1 to ${MAX_PROFILE}, default 20). ` +
|
|
192
281
|
"Each entry gives the routine address, its total cycles with the routines it calls, and its own cycles. " +
|
|
193
282
|
"percent is the part of all cycles that the routine used itself. " +
|
|
194
283
|
"Cycle counts are decimal strings.",
|
|
195
|
-
inputSchema: z
|
|
284
|
+
inputSchema: z
|
|
285
|
+
.object({
|
|
286
|
+
action: z.enum(["read", "clear"]).default("read"),
|
|
287
|
+
limit: z.number().int().min(1).max(MAX_PROFILE).optional().describe("read only. Default 20"),
|
|
288
|
+
})
|
|
289
|
+
.strict(),
|
|
196
290
|
outputSchema: z.object({
|
|
197
|
-
entries: z.array(z.object({ address: AddressOutput, totalCycles: z.string(), selfCycles: z.string(), percent: z.number() })),
|
|
291
|
+
entries: z.array(z.object({ address: AddressOutput, totalCycles: z.string(), selfCycles: z.string(), percent: z.number() })).optional(),
|
|
292
|
+
cleared: z.boolean().optional(),
|
|
198
293
|
}),
|
|
199
|
-
readOnly:
|
|
294
|
+
readOnly: false,
|
|
200
295
|
async run(input, session) {
|
|
201
|
-
const {
|
|
296
|
+
const { action, ...fields } = input;
|
|
297
|
+
if (action === "clear") {
|
|
298
|
+
requireFields("clear", fields, []);
|
|
299
|
+
return session.profile({ action: "clear" });
|
|
300
|
+
}
|
|
301
|
+
const { entries } = await session.profile({ action: "read", limit: fields.limit ?? 20 });
|
|
202
302
|
return { entries: entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
|
|
203
303
|
},
|
|
204
304
|
});
|
|
205
305
|
|
|
306
|
+
/** The most code ranges a memmap summary lists. */
|
|
307
|
+
const MAX_SUMMARY_RANGES = 64;
|
|
308
|
+
|
|
309
|
+
/** Where an access happened: RAM, ROM or I/O. */
|
|
310
|
+
function sources(flags: number): Array<"ram" | "rom" | "io"> {
|
|
311
|
+
const where: Array<"ram" | "rom" | "io"> = [];
|
|
312
|
+
if (flags & (COVERAGE.ramRead | COVERAGE.ramWrite | COVERAGE.ramExecute)) where.push("ram");
|
|
313
|
+
if (flags & (COVERAGE.romRead | COVERAGE.romExecute)) where.push("rom");
|
|
314
|
+
if (flags & (COVERAGE.ioRead | COVERAGE.ioWrite | COVERAGE.ioExecute)) where.push("io");
|
|
315
|
+
return where;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const count = (map: Uint8Array, bit: number) => map.reduce((sum, flags) => sum + ((flags & bit) === 0 ? 0 : 1), 0);
|
|
319
|
+
const RangeOutput = z.object({ start: AddressOutput, end: AddressOutput });
|
|
320
|
+
const showRanges = (ranges: CodeRange[]) => ranges.slice(0, MAX_SUMMARY_RANGES).map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) }));
|
|
321
|
+
|
|
206
322
|
export const c64Memmap = defineTool({
|
|
207
323
|
name: "c64_memmap",
|
|
208
324
|
title: "Memory access map",
|
|
209
325
|
description:
|
|
210
|
-
"Get how the C64 CPU used memory since the session started or since the last clear. " +
|
|
211
|
-
"Action read gives ranges of addresses with the same access (execute, read, write) from start to end. " +
|
|
326
|
+
"Get how the C64 CPU used memory since the session started or since the last clear. RAM, ROM and I/O are apart. " +
|
|
327
|
+
"Action read gives ranges of addresses with the same access (execute, read, write) from start to end. Each range also tells if the access was in RAM, ROM or I/O. " +
|
|
212
328
|
`It gives at most maxRanges ranges (1 to ${MAX_MEMMAP_RANGES}, default 256). Addresses the CPU did not touch are not in the result. ` +
|
|
329
|
+
"If more ranges follow, the result also gives truncated: true and next. Read again with start set to next. " +
|
|
330
|
+
"With out, read writes a coverage map to that project file, one byte for each address from start to end. " +
|
|
331
|
+
"Bits 0 to 2 are RAM read, write and execute. Bits 3 and 4 are ROM read and execute. Bits 5 to 7 are I/O read, write and execute. " +
|
|
332
|
+
"Execute is set only on the first byte of each executed instruction. Thus read gives a new range at each instruction of code. For code, use summary or read with out. " +
|
|
333
|
+
"A read on a byte right after executed code, or at a return address, can be a dummy read that the CPU does not use. " +
|
|
334
|
+
"Action summary gives the counts for RAM, ROM and I/O. It also gives the executed code in RAM with its operand bytes, and the RAM that was written and executed. " +
|
|
335
|
+
`Each list has at most ${MAX_SUMMARY_RANGES} ranges. ` +
|
|
213
336
|
"Action clear sets all access records to zero.",
|
|
214
337
|
inputSchema: z
|
|
215
338
|
.object({
|
|
216
|
-
action: z.enum(["read", "clear"]),
|
|
217
|
-
start: AddressInput.optional().describe("read
|
|
218
|
-
end: AddressInput.optional().describe("read
|
|
219
|
-
maxRanges: z.number().int().min(1).max(MAX_MEMMAP_RANGES).optional().describe("read only. Default 256"),
|
|
339
|
+
action: z.enum(["read", "summary", "clear"]),
|
|
340
|
+
start: AddressInput.optional().describe("read and summary. Default $0000"),
|
|
341
|
+
end: AddressInput.optional().describe("read and summary. Default $ffff"),
|
|
342
|
+
maxRanges: z.number().int().min(1).max(MAX_MEMMAP_RANGES).optional().describe("read without out only. Default 256"),
|
|
343
|
+
out: OutInput.optional().describe("read only: write a coverage map to this project file, one byte for each address. An existing file is overwritten"),
|
|
344
|
+
project: ProjectInput.optional().describe("read with out only: the project folder, relative to the folder where the agent started. Give it when that folder is not in a project and has projects below it"),
|
|
220
345
|
})
|
|
221
346
|
.strict(),
|
|
222
347
|
outputSchema: z.object({
|
|
223
|
-
ranges: z.array(z.object({ start: AddressOutput, end: AddressOutput, execute: z.boolean(), read: z.boolean(), write: z.boolean() })).optional(),
|
|
348
|
+
ranges: z.array(z.object({ start: AddressOutput, end: AddressOutput, execute: z.boolean(), read: z.boolean(), write: z.boolean(), in: z.array(z.enum(["ram", "rom", "io"])) })).optional(),
|
|
349
|
+
truncated: z.boolean().optional(),
|
|
350
|
+
next: AddressOutput.optional(),
|
|
351
|
+
ram: z.object({ executed: z.number().int(), read: z.number().int(), written: z.number().int() }).optional(),
|
|
352
|
+
rom: z.object({ executed: z.number().int(), read: z.number().int() }).optional(),
|
|
353
|
+
io: z.object({ executed: z.number().int(), read: z.number().int(), written: z.number().int() }).optional(),
|
|
354
|
+
code: z.array(RangeOutput).optional(),
|
|
355
|
+
codeTruncated: z.boolean().optional(),
|
|
356
|
+
writtenAndExecuted: z.array(RangeOutput).optional(),
|
|
357
|
+
writtenAndExecutedTruncated: z.boolean().optional(),
|
|
224
358
|
cleared: z.boolean().optional(),
|
|
359
|
+
...WrittenOutput,
|
|
225
360
|
}),
|
|
226
361
|
readOnly: false,
|
|
227
362
|
async run(input, session) {
|
|
228
|
-
|
|
229
|
-
|
|
363
|
+
const { action, ...fields } = input;
|
|
364
|
+
if (action === "clear") {
|
|
365
|
+
requireFields("clear", fields, []);
|
|
230
366
|
return session.memmap({ action: "clear" });
|
|
231
367
|
}
|
|
232
|
-
const
|
|
233
|
-
|
|
368
|
+
const start = fields.start ?? 0x0000;
|
|
369
|
+
const end = fields.end ?? 0xffff;
|
|
370
|
+
if (end < start) throw new WireFailure("invalid-input", "end must not be before start.");
|
|
371
|
+
if (action === "summary") {
|
|
372
|
+
requireFields("summary", fields, ["start", "end"]);
|
|
373
|
+
const map = await readCoverage(session, start, end);
|
|
374
|
+
// The opcodes give the instruction lengths: RAM as it is now.
|
|
375
|
+
const parts: Buffer[] = [];
|
|
376
|
+
for (let at = 0; at < 0x10000; at += MAX_MEMORY_READ) parts.push(Buffer.from((await session.memoryRead({ address: at, size: MAX_MEMORY_READ, space: "c64", view: "ram" })).data, "hex"));
|
|
377
|
+
const memory = Buffer.concat(parts);
|
|
378
|
+
const code = executedRamCode(map, start, memory);
|
|
379
|
+
const written = executedRamCode(map.map((flags) => ((flags & COVERAGE.ramWrite) === 0 ? 0 : flags)), start, memory);
|
|
380
|
+
return {
|
|
381
|
+
ram: { executed: count(map, COVERAGE.ramExecute), read: count(map, COVERAGE.ramRead), written: count(map, COVERAGE.ramWrite) },
|
|
382
|
+
rom: { executed: count(map, COVERAGE.romExecute), read: count(map, COVERAGE.romRead) },
|
|
383
|
+
io: { executed: count(map, COVERAGE.ioExecute), read: count(map, COVERAGE.ioRead), written: count(map, COVERAGE.ioWrite) },
|
|
384
|
+
code: showRanges(code),
|
|
385
|
+
...(code.length > MAX_SUMMARY_RANGES ? { codeTruncated: true } : {}),
|
|
386
|
+
writtenAndExecuted: showRanges(written),
|
|
387
|
+
...(written.length > MAX_SUMMARY_RANGES ? { writtenAndExecutedTruncated: true } : {}),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (fields.out !== undefined) {
|
|
391
|
+
requireFields("read", fields, ["start", "end", "out", "project"]);
|
|
392
|
+
return writeOut(fields.out, await readCoverage(session, start, end), fields.project);
|
|
393
|
+
}
|
|
394
|
+
requireFields("read", fields, ["start", "end", "maxRanges"]);
|
|
395
|
+
const result = await session.memmap({ action: "read", start, end, maxRanges: fields.maxRanges ?? 256 });
|
|
396
|
+
const ranges = result.ranges.map((range) => ({
|
|
397
|
+
start: formatC64Address(range.start),
|
|
398
|
+
end: formatC64Address(range.end),
|
|
399
|
+
execute: (range.flags & EXECUTE) !== 0,
|
|
400
|
+
read: (range.flags & READ) !== 0,
|
|
401
|
+
write: (range.flags & WRITE) !== 0,
|
|
402
|
+
in: sources(range.flags),
|
|
403
|
+
}));
|
|
404
|
+
return result.next === undefined ? { ranges } : { ranges, truncated: true, next: formatC64Address(result.next) };
|
|
234
405
|
},
|
|
235
406
|
});
|
|
236
407
|
|
|
237
|
-
export const debugTools = [c64Breakpoint, c64Watchpoint, c64CpuHistory, c64Backtrace, c64Profile, c64Memmap, c64Timing];
|
|
408
|
+
export const debugTools = [c64Breakpoint, c64Watchpoint, c64CpuHistory, c64Trace, c64Backtrace, c64Profile, c64Memmap, c64Timing];
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import { formatC64Address } from "../../c64.ts";
|
|
6
|
-
import { COMPARISONS, EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_TIMEOUT_FRAMES, RUN_STATES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
|
|
7
|
-
import { AddressInput, AddressOutput, Byte, ConditionInput, defineTool, MemoryViewInput, SpaceInput, toCondition } from "../server.ts";
|
|
6
|
+
import { COMPARISONS, EXECUTION_ACTIONS, MAX_EXECUTION_COUNT, MAX_HISTORY, MAX_TIMEOUT_FRAMES, POINT_STOP_REASONS, RUN_STATES, SPACES, STOP_REASONS, type RunTarget } from "../../protocol/vice.ts";
|
|
7
|
+
import { AddressInput, AddressOutput, Byte, ConditionInput, defineTool, MemoryViewInput, SpaceInput, toCondition, ToolOutput } from "../server.ts";
|
|
8
|
+
import { c64Observe } from "./video.ts";
|
|
8
9
|
|
|
9
10
|
export const c64Execution = defineTool({
|
|
10
11
|
name: "c64_execution",
|
|
@@ -15,7 +16,9 @@ export const c64Execution = defineTool({
|
|
|
15
16
|
"next executes count instructions (default 1) and treats a JSR and its subroutine as one instruction. " +
|
|
16
17
|
"until-return runs until the current subroutine returns (after the next RTS or RTI). " +
|
|
17
18
|
"advance-frames runs exactly count video frames (count is necessary). It stops at the same raster position, count frames later. " +
|
|
18
|
-
"A breakpoint
|
|
19
|
+
"A breakpoint, a watchpoint or a JAM opcode can stop it before that. advancedFrames tells how many frames ran. " +
|
|
20
|
+
"stopReason tells why it stopped: frames (all frames ran), breakpoint, watchpoint or jam. pointId gives the id of the breakpoint or watchpoint. " +
|
|
21
|
+
"To wait for a breakpoint or watchpoint after resume, use advance-frames with a large count. " +
|
|
19
22
|
"step, next, until-return and advance-frames first stop a running CPU. Use count only with step, next and advance-frames. " +
|
|
20
23
|
"step, next and until-return work only in space c64. For the drive, use c64_run_until with an address in space drive8.",
|
|
21
24
|
inputSchema: z
|
|
@@ -36,6 +39,8 @@ export const c64Execution = defineTool({
|
|
|
36
39
|
pc: AddressOutput.optional(),
|
|
37
40
|
executed: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("instructions executed by step or next"),
|
|
38
41
|
advancedFrames: z.number().int().min(0).max(MAX_EXECUTION_COUNT).optional().describe("frames advance-frames ran"),
|
|
42
|
+
stopReason: z.enum(["frames", ...POINT_STOP_REASONS]).optional().describe("advance-frames: why it stopped"),
|
|
43
|
+
pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped advance-frames"),
|
|
39
44
|
}),
|
|
40
45
|
readOnly: false,
|
|
41
46
|
async run(input, session) {
|
|
@@ -46,6 +51,8 @@ export const c64Execution = defineTool({
|
|
|
46
51
|
...(result.pc === undefined ? {} : { pc: formatC64Address(result.pc) }),
|
|
47
52
|
...(result.executed === undefined ? {} : { executed: result.executed }),
|
|
48
53
|
...(result.advancedFrames === undefined ? {} : { advancedFrames: result.advancedFrames }),
|
|
54
|
+
...(result.stopReason === undefined ? {} : { stopReason: result.stopReason }),
|
|
55
|
+
...(result.pointId === undefined ? {} : { pointId: result.pointId }),
|
|
49
56
|
};
|
|
50
57
|
},
|
|
51
58
|
});
|
|
@@ -88,14 +95,32 @@ export const c64RunUntil = defineTool({
|
|
|
88
95
|
"Kind raster stops at the first instruction at or after a raster line and cycle. If the machine is there now, it stops at the next frame's pass. " +
|
|
89
96
|
`timeoutFrames (1 to ${MAX_TIMEOUT_FRAMES}, default 3000) limits the run in video frames. ` +
|
|
90
97
|
"reached tells if the target occurred. stopReason tells why the CPU stopped: target, breakpoint, watchpoint, jam (a JAM opcode) or timeout. " +
|
|
98
|
+
"pointId gives the id of the breakpoint or watchpoint that stopped the CPU. " +
|
|
99
|
+
"space tells which CPU stopped: c64, or drive8 for a stop in the disk drive. pc is the address in that space. " +
|
|
100
|
+
"With observe, the result also gives what c64_observe gives at the stop, in observed. " +
|
|
101
|
+
`With history (1 to ${MAX_HISTORY}), it also gives the last instructions before the stop, as c64_cpu_history gives them. ` +
|
|
102
|
+
"Thus one call runs to a place and reads the machine there. " +
|
|
91
103
|
"The CPU is always stopped after this tool.",
|
|
92
104
|
inputSchema: z
|
|
93
105
|
.object({
|
|
94
106
|
target: RunTargetInput,
|
|
95
107
|
timeoutFrames: z.number().int().min(1).max(MAX_TIMEOUT_FRAMES).default(3000),
|
|
108
|
+
observe: c64Observe.inputSchema.optional().describe("what to read at the stop, as for c64_observe"),
|
|
109
|
+
history: z.number().int().min(1).max(MAX_HISTORY).optional().describe("the number of instructions before the stop to give"),
|
|
96
110
|
})
|
|
97
111
|
.strict(),
|
|
98
|
-
outputSchema: z.object({
|
|
112
|
+
outputSchema: z.object({
|
|
113
|
+
reached: z.boolean(),
|
|
114
|
+
stopReason: z.enum(STOP_REASONS),
|
|
115
|
+
state: z.enum(RUN_STATES),
|
|
116
|
+
pc: AddressOutput,
|
|
117
|
+
space: z.enum(SPACES),
|
|
118
|
+
pointId: z.number().int().min(1).optional().describe("the id of the breakpoint or watchpoint that stopped the CPU"),
|
|
119
|
+
observed: c64Observe.outputSchema.optional(),
|
|
120
|
+
history: z
|
|
121
|
+
.array(z.object({ address: AddressOutput, bytes: z.string(), text: z.string(), a: Byte, x: Byte, y: Byte, sp: Byte, rasterLine: z.number().int().min(0).optional(), rasterCycle: z.number().int().min(0).optional() }))
|
|
122
|
+
.optional(),
|
|
123
|
+
}),
|
|
99
124
|
readOnly: false,
|
|
100
125
|
async run(input, session) {
|
|
101
126
|
const { target: given } = input;
|
|
@@ -109,7 +134,16 @@ export const c64RunUntil = defineTool({
|
|
|
109
134
|
target = given.cycle === undefined ? { kind: "raster", line: given.line } : { kind: "raster", line: given.line, cycle: given.cycle };
|
|
110
135
|
}
|
|
111
136
|
const result = await session.runUntil({ target, timeoutFrames: input.timeoutFrames });
|
|
112
|
-
|
|
137
|
+
const answer = { ...result, pc: formatC64Address(result.pc) };
|
|
138
|
+
const history =
|
|
139
|
+
input.history === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: { history: (await session.cpuHistory({ limit: input.history, space: result.space })).entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
|
|
142
|
+
if (input.observe === undefined) return { ...answer, ...history };
|
|
143
|
+
// The observation at the stop (M23.10): an image of the screen comes with the result as c64_observe gives it.
|
|
144
|
+
const observed = await c64Observe.run(input.observe, session);
|
|
145
|
+
if (observed instanceof ToolOutput) return new ToolOutput({ ...answer, ...history, observed: observed.structured }, observed.content);
|
|
146
|
+
return { ...answer, ...history, observed };
|
|
113
147
|
},
|
|
114
148
|
});
|
|
115
149
|
|