@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.
Files changed (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +137 -0
  14. package/skills/c64-docs/references/page-types.md +113 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +193 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +89 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -60
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +63 -0
  81. package/src/host-client/image.ts +66 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +203 -32
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +137 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -8
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. 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=@henols/c64-re-tools@latest c64-re-tools-mcp
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 latest version.
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 for the life of the process (1 MCP = 1 session = 1 VICE).
49
- * It opens at start; if that fails before a session existed (for example the
50
- * host was not running yet), the next tool call tries again. Once a session
51
- * existed, its loss is final and reported by the session itself. Close stops
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
- opening = (async () => ViceSessionClient.open({ videoStandard: videoStandard(process.env), signal: stop.signal }))();
61
- void opening.then(() => trace().event("session.opened", { ms: elapsedMs(started) }), () => {});
62
- opening.catch((error: unknown) => {
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 opening;
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 { ViceSessionClient } from "../host-client/vice-session.ts";
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.message }) }] };
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
- const output = await tool.run(parsed.data, await session());
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 {
@@ -3,9 +3,13 @@
3
3
 
4
4
  import { z } from "zod";
5
5
 
6
- import { formatC64Address } from "../../c64.ts";
7
- import { CHECKPOINT_ACTIONS, MAX_BACKTRACE, MAX_HISTORY, MAX_MEMMAP_RANGES, MAX_PROFILE, MAX_WATCH_SIZE, SPACES, TIMING_ACTIONS, WATCH_ACCESS, type Breakpoint, type Watchpoint } from "../../protocol/vice.ts";
8
- import { AddressInput, AddressOutput, Byte, ConditionInput, ConditionOutput, defineTool, HexData, requireFields, showCondition, SpaceInput, toCondition } from "../server.ts";
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. Actions remove, enable and disable take that id. Action list gives all breakpoints. Each breakpoint shows its condition, when it has one. " +
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. Actions remove, enable and disable take that id. Action list gives all watchpoints. Each watchpoint shows its condition, when it has one.",
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. This does not change the run state of the machine.",
128
- inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_HISTORY).default(50), space: SpaceInput }).strict(),
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: true,
221
+ readOnly: false,
145
222
  async run(input, session) {
146
- const { entries } = await session.cpuHistory(input);
147
- return { entries: entries.map((entry) => ({ ...entry, address: formatC64Address(entry.address) })) };
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 PAL frame has 19656 cycles and an NTSC frame 17095.",
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
- "Get the routines where the C64 CPU spent the most cycles since the session started. " +
191
- `limit is the maximum number of routines (1 to ${MAX_PROFILE}, default 20). ` +
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.object({ limit: z.number().int().min(1).max(MAX_PROFILE).default(20) }).strict(),
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: true,
294
+ readOnly: false,
200
295
  async run(input, session) {
201
- const { entries } = await session.profile(input.limit);
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 only. Default $0000"),
218
- end: AddressInput.optional().describe("read only. Default $ffff"),
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
- if (input.action === "clear") {
229
- requireFields("clear", { start: input.start, end: input.end, maxRanges: input.maxRanges }, []);
363
+ const { action, ...fields } = input;
364
+ if (action === "clear") {
365
+ requireFields("clear", fields, []);
230
366
  return session.memmap({ action: "clear" });
231
367
  }
232
- const result = await session.memmap({ action: "read", start: input.start ?? 0x0000, end: input.end ?? 0xffff, maxRanges: input.maxRanges ?? 256 });
233
- return { ranges: result.ranges.map((range) => ({ ...range, start: formatC64Address(range.start), end: formatC64Address(range.end) })) };
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 or watchpoint can stop it before that. advancedFrames tells how many frames ran. " +
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({ reached: z.boolean(), stopReason: z.enum(STOP_REASONS), state: z.enum(RUN_STATES), pc: AddressOutput }),
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
- return { ...result, pc: formatC64Address(result.pc) };
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