@dynamicagents/plugins 0.11.1 → 0.13.0

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 (134) hide show
  1. package/README.md +59 -79
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +53 -0
  5. package/dist/claude-code/cards.js +272 -0
  6. package/dist/claude-code/config.d.ts +73 -31
  7. package/dist/claude-code/config.js +0 -2
  8. package/dist/claude-code/credentials.d.ts +21 -9
  9. package/dist/claude-code/credentials.js +43 -9
  10. package/dist/claude-code/egress.d.ts +15 -14
  11. package/dist/claude-code/egress.js +51 -14
  12. package/dist/claude-code/events.d.ts +153 -9
  13. package/dist/claude-code/events.js +289 -58
  14. package/dist/claude-code/github.d.ts +39 -0
  15. package/dist/claude-code/github.js +82 -0
  16. package/dist/claude-code/index.d.ts +24 -80
  17. package/dist/claude-code/index.js +19 -234
  18. package/dist/claude-code/model.d.ts +152 -0
  19. package/dist/claude-code/model.js +342 -0
  20. package/dist/claude-code/run.d.ts +283 -92
  21. package/dist/claude-code/run.js +620 -114
  22. package/dist/claude-code/session.d.ts +131 -0
  23. package/dist/claude-code/session.js +285 -0
  24. package/dist/claude-code/spec.d.ts +46 -0
  25. package/dist/claude-code/spec.js +83 -0
  26. package/dist/claude-code/verified.d.ts +15 -0
  27. package/dist/claude-code/verified.js +15 -0
  28. package/dist/computer/context.d.ts +19 -0
  29. package/dist/computer/context.js +222 -0
  30. package/dist/computer/file-lock.d.ts +5 -0
  31. package/dist/computer/file-lock.js +40 -0
  32. package/dist/computer/gate.d.ts +12 -12
  33. package/dist/computer/gate.js +12 -12
  34. package/dist/computer/index.d.ts +57 -255
  35. package/dist/computer/index.js +98 -1039
  36. package/dist/computer/paths.d.ts +47 -22
  37. package/dist/computer/paths.js +64 -49
  38. package/dist/computer/proxy.d.ts +45 -0
  39. package/dist/computer/proxy.js +234 -0
  40. package/dist/computer/render.d.ts +27 -38
  41. package/dist/computer/render.js +32 -65
  42. package/dist/computer/tools-exec.d.ts +3 -0
  43. package/dist/computer/tools-exec.js +268 -0
  44. package/dist/computer/tools-file.d.ts +12 -0
  45. package/dist/computer/tools-file.js +64 -0
  46. package/dist/computer/tools-grep.d.ts +3 -0
  47. package/dist/computer/tools-grep.js +129 -0
  48. package/dist/repo/checkout.d.ts +35 -6
  49. package/dist/repo/checkout.js +64 -6
  50. package/dist/repo/context.d.ts +135 -0
  51. package/dist/repo/context.js +582 -0
  52. package/dist/repo/index.d.ts +79 -57
  53. package/dist/repo/index.js +118 -1112
  54. package/dist/repo/tools-checks.d.ts +3 -0
  55. package/dist/repo/tools-checks.js +112 -0
  56. package/dist/repo/tools-clone.d.ts +4 -0
  57. package/dist/repo/tools-clone.js +192 -0
  58. package/dist/repo/tools-pr.d.ts +14 -0
  59. package/dist/repo/tools-pr.js +196 -0
  60. package/dist/repo/tools-review.d.ts +12 -0
  61. package/dist/repo/tools-review.js +393 -0
  62. package/dist/repo/tools-worktree.d.ts +4 -0
  63. package/dist/repo/tools-worktree.js +305 -0
  64. package/dist/repo/tools-worktrees.d.ts +12 -0
  65. package/dist/repo/tools-worktrees.js +48 -0
  66. package/dist/repo/url.d.ts +2 -2
  67. package/dist/repo/url.js +7 -7
  68. package/dist/scratch/index.d.ts +4 -4
  69. package/dist/scratch/index.js +12 -11
  70. package/dist/{computer → workspace}/advisory.d.ts +33 -20
  71. package/dist/{computer → workspace}/advisory.js +32 -7
  72. package/dist/workspace/ca-trust.d.ts +95 -0
  73. package/dist/workspace/ca-trust.js +312 -0
  74. package/dist/workspace/container-deps.d.ts +54 -0
  75. package/dist/workspace/container-deps.js +224 -0
  76. package/dist/workspace/container-fault.d.ts +38 -0
  77. package/dist/workspace/container-fault.js +73 -0
  78. package/dist/workspace/exec.d.ts +94 -0
  79. package/dist/workspace/exec.js +153 -0
  80. package/dist/workspace/format.d.ts +36 -0
  81. package/dist/workspace/format.js +57 -0
  82. package/dist/workspace/git-host.d.ts +79 -0
  83. package/dist/workspace/git-host.js +226 -0
  84. package/dist/workspace/git-identity.d.ts +57 -0
  85. package/dist/workspace/git-identity.js +147 -0
  86. package/dist/workspace/index.d.ts +43 -47
  87. package/dist/workspace/index.js +52 -112
  88. package/dist/workspace/install-job.d.ts +121 -0
  89. package/dist/workspace/install-job.js +866 -0
  90. package/dist/{computer → workspace}/install.d.ts +15 -19
  91. package/dist/{computer → workspace}/install.js +8 -11
  92. package/dist/workspace/object.d.ts +502 -0
  93. package/dist/workspace/object.js +1427 -0
  94. package/dist/workspace/open.d.ts +95 -0
  95. package/dist/workspace/open.js +63 -0
  96. package/dist/workspace/read.d.ts +16 -0
  97. package/dist/workspace/read.js +22 -0
  98. package/dist/workspace/shell.d.ts +45 -0
  99. package/dist/workspace/shell.js +80 -0
  100. package/dist/workspace/sync.d.ts +111 -0
  101. package/dist/workspace/sync.js +124 -0
  102. package/dist/workspace/wake.d.ts +19 -0
  103. package/dist/workspace/wake.js +10 -0
  104. package/package.json +28 -42
  105. package/dist/arc-agi/analysis.d.ts +0 -336
  106. package/dist/arc-agi/analysis.js +0 -829
  107. package/dist/arc-agi/client.d.ts +0 -82
  108. package/dist/arc-agi/client.js +0 -113
  109. package/dist/arc-agi/game-tools.d.ts +0 -25
  110. package/dist/arc-agi/game-tools.js +0 -26
  111. package/dist/arc-agi/index.d.ts +0 -42
  112. package/dist/arc-agi/index.js +0 -175
  113. package/dist/arc-agi/main-agent.d.ts +0 -27
  114. package/dist/arc-agi/main-agent.js +0 -65
  115. package/dist/arc-agi/recipe.d.ts +0 -51
  116. package/dist/arc-agi/recipe.js +0 -100
  117. package/dist/arc-agi/scorecard.d.ts +0 -118
  118. package/dist/arc-agi/scorecard.js +0 -163
  119. package/dist/arc-agi/soul.d.ts +0 -40
  120. package/dist/arc-agi/soul.js +0 -90
  121. package/dist/arc-agi/store.d.ts +0 -154
  122. package/dist/arc-agi/store.js +0 -216
  123. package/dist/arc-agi/tools.d.ts +0 -36
  124. package/dist/arc-agi/tools.js +0 -576
  125. package/dist/arc-agi/types.d.ts +0 -212
  126. package/dist/arc-agi/types.js +0 -19
  127. package/dist/claude-code/recipe.d.ts +0 -53
  128. package/dist/claude-code/recipe.js +0 -100
  129. package/dist/computer/read.d.ts +0 -137
  130. package/dist/computer/read.js +0 -203
  131. package/dist/recall/index.d.ts +0 -99
  132. package/dist/recall/index.js +0 -171
  133. package/dist/triage/index.d.ts +0 -355
  134. package/dist/triage/index.js +0 -302
@@ -1,576 +0,0 @@
1
- import { tool } from "ai";
2
- import { z } from "zod";
3
- import { runtimeAs } from "@dynamicagents/core/subtasks";
4
- import { makeArcClient } from "./client.js";
5
- import { colorHistogram, colorName, describeBox, describeCell, diffGrids, diffShapes, lastGrid, parseGrid, renderGrid, renderRegion, renderShapeDelta, renderShapes, renderTravel, serializeGrid, trackTravel } from "./analysis.js";
6
- import { ARC_SESSION_PATH } from "./types.js";
7
- /**
8
- * The `arc-game` tool family: act / inspect against the ARC-AGI-3 REST API. All
9
- * durable session state (cookies, guid, frames, metrics) lives in the workspace
10
- * at {@link ARC_SESSION_PATH}, so the run resumes across chunks and isolate
11
- * eviction. The API key is closed over from the plugin's config, never model
12
- * input.
13
- *
14
- * This family **plays**; it does not manage scorecards. The game arrives as a
15
- * validated Subtask param and the card as parent-resolved runtime (leased by
16
- * {@link file://./scorecard.ts}), so nothing here ever opens, closes, or chooses
17
- * a card.
18
- *
19
- * **Nothing here ever RESETs.** The play is opened once per (card, game) by the
20
- * parent (`resolvePlay`) and arrives as `runtime.guid`; this family only joins
21
- * it. That is the whole point: a guid is mintable only by RESET, and a second
22
- * RESET is a second run on the card, so a family that could reset would be able
23
- * to throw away a play by retrying it. Instead a WIN or GAME_OVER is the result
24
- * this card recorded for this game, and the model reports it.
25
- *
26
- * The session file is a local cache of that play, not its identity — the guid in
27
- * `runtime` is. A subagent that lost its workspace, or a re-dispatched Subtask,
28
- * rebuilds the file around the same guid and plays on.
29
- *
30
- * Transient ARC faults surface to the model as tool errors (the SDK captures a
31
- * throwing tool as an in-band `tool-error` result, not a rejected generation);
32
- * the soul tells the model to give up and report after repeated failures, and the
33
- * turn budget bounds it.
34
- */
35
- /**
36
- * This family's key in a Recipe's `toolFamilies`. Exported because it is also
37
- * what tells the parent a Subtask needs a scorecard leased before it can run —
38
- * see `resolveRuntime` in the ReactiveAgent DO.
39
- */
40
- export const ARC_GAME_FAMILY = "arc-game";
41
- export function buildArcGameTools(apiKey, ctx) {
42
- const { workspace, emitProgress, params } = ctx;
43
- // Closed over from the plugin's config, never `ctx.env`. The predecessor
44
- // passed the Worker `env` to every tool family, which a published package
45
- // cannot do — `Env` is the ambient interface `wrangler types` generates into a
46
- // *consumer's* app and does not exist here. Config-at-instantiation is also
47
- // the only thing that works on Workers at all, where `env` has no module scope.
48
- const runtime = runtimeAs(ctx.runtime);
49
- // All settled before the model runs — it cannot pick a different game, and it
50
- // cannot see, choose, or name a card or a play at all. The game is the type's
51
- // declared param; the card and the guid are what the parent resolved for this
52
- // chunk, and the guid in particular is the play it must join rather than open.
53
- const gameId = params.game_id;
54
- const cardId = runtime.cardId;
55
- const guid = runtime.guid;
56
- const load = () => workspace.readJson(ARC_SESSION_PATH);
57
- const save = (s) => workspace.writeJson(ARC_SESSION_PATH, s);
58
- /** The stored board as a grid, or null when no frame has been recorded yet. */
59
- const board = (s) => s.lastGridHex === null ? null : parseGrid(s.lastGridHex);
60
- /**
61
- * The session for the play in `runtime`, from the workspace or rebuilt around
62
- * the resolved guid. Never opens a play — there is nothing here that could.
63
- *
64
- * The rebuilt case is a real one (fresh workspace, re-dispatched Subtask), and
65
- * it is why {@link ArcSession.availableActions} may be empty: the parent's
66
- * opening frame is handed over only to whoever opened the play, and no ARC
67
- * endpoint reads a board back. An empty list means *unknown*, not *none* — the
68
- * first ACTION returns a full frame and fills it in.
69
- *
70
- * **Once the file exists it is the play**, and a later chunk's lease cannot
71
- * pull it onto a different card or a different guid. A rollover — the reuse
72
- * window elapsing between two chunk starts, so an ARC outage or an exhausted
73
- * step retry rather than anything a play did — leases a fresh card, and a fresh
74
- * card has no recorded guid, so the parent RESETs. `runtime.guid` then names a
75
- * play with nothing in it while this file names the play holding every level
76
- * reached. Joining the new one would throw the real one away, so the divergence
77
- * is logged and not acted on — and logged rather than silent because both of its
78
- * residuals are outside this family's reach:
79
- *
80
- * - the parent's RESET already recorded a second, empty run on the new card, and
81
- * - `enrichResult` reads the score from the *leased* card, so the report
82
- * describes that empty run instead of this play.
83
- */
84
- const resume = async () => {
85
- const stored = await load();
86
- if (stored) {
87
- if (guid !== undefined && stored.guid !== guid) {
88
- console.warn("[arc-game] play/lease divergence", {
89
- gameId,
90
- playing: stored.guid,
91
- resolved: guid,
92
- playCard: stored.cardId,
93
- leaseCard: cardId
94
- });
95
- }
96
- return stored;
97
- }
98
- if (guid === undefined || cardId === undefined) {
99
- // Only reachable if a Subtask ran this family without the parent resolving
100
- // a play — `buildRecipeTools` gates the family on the card and
101
- // `resolveRuntime` settles both together. Nothing here can recover:
102
- // minting a guid is exactly the power this family does not have.
103
- const missing = [
104
- guid === undefined ? "runtime.guid" : null,
105
- cardId === undefined ? "runtime.cardId" : null
106
- ].filter((field) => field !== null);
107
- throw new Error(`arc-game: no play was resolved for this subtask (missing ${missing.join(" and ")})`);
108
- }
109
- const session = joinSession(gameId, cardId, guid, runtime);
110
- await save(session);
111
- return session;
112
- };
113
- /**
114
- * True once this chunk has told the model where it is.
115
- *
116
- * The tool family is rebuilt per chunk (`buildRecipeTools` in the subagent
117
- * facet), so this closure variable *is* the chunk boundary — and a chunk
118
- * boundary is exactly where the model's context window was trimmed.
119
- */
120
- let oriented = false;
121
- /**
122
- * The last `arc_inspect` this chunk served — its arguments and the board they
123
- * were rendered from. A closure variable for the same reason `oriented` is one:
124
- * it must reset exactly where the model's context was trimmed.
125
- */
126
- let lastView = null;
127
- /**
128
- * The orientation to prepend to this chunk's first tool result, and "" after.
129
- *
130
- * Completing a level emits progress, and progress ends the chunk, so the model
131
- * resumes every new level with its recent turns trimmed away. Leading its next
132
- * result with the board costs no game action and no extra turn — it rides on a
133
- * call it was making anyway — where re-inspecting to get its bearings costs a
134
- * turn out of the run's budget, once per level.
135
- *
136
- * `withBoard` is false for `arc_inspect`, whose view already *is* the board;
137
- * repeating it as shapes would say the same thing twice.
138
- */
139
- const orient = (session, withBoard) => {
140
- if (oriented)
141
- return "";
142
- oriented = true;
143
- return withBoard
144
- ? `${describeState(session, `Playing ${gameId}.`)}\n`
145
- : `Playing ${gameId}. ${stateLine(session)}\n`;
146
- };
147
- const tools = {
148
- arc_act: tool({
149
- description: "Take one or more actions in the current game, in order. Only use actions listed in the latest available_actions. Returns one line per step naming what that action changed — which shapes moved and how far, which were repainted or appeared, and `0 cells changed` when the game refused it outright — then the net effect over the batch and the new level, state and available_actions. Batch the whole path you intend to walk: a batch runs to the end whatever happens, so the report tells you exactly which steps did nothing rather than stopping. Every step is a real action counted against the game's baseline, so batch a route you have reason to believe in, not a guess.",
150
- inputSchema: z.object({
151
- steps: z
152
- .array(z.object({
153
- action: z
154
- .number()
155
- .int()
156
- .min(1)
157
- .max(7)
158
- .describe("1=up 2=down 3=left 4=right 5=interact 6=click(x,y) 7=undo"),
159
- x: z
160
- .number()
161
- .int()
162
- .min(0)
163
- .max(63)
164
- .optional()
165
- .describe("column, action 6 only"),
166
- y: z
167
- .number()
168
- .int()
169
- .min(0)
170
- .max(63)
171
- .optional()
172
- .describe("row, action 6 only")
173
- }))
174
- .min(1)
175
- .max(MAX_STEPS)
176
- .describe(`the actions to take in order, 1–${MAX_STEPS}; each is sent separately and reported separately`),
177
- note: z
178
- .string()
179
- .max(2000)
180
- .optional()
181
- .describe("brief reasoning for this move or sequence")
182
- }),
183
- execute: async ({ steps, note }, { abortSignal }) => {
184
- // The call's own signal, not the execution's: it fires at this call's time
185
- // limit as well as on a cancel, and past that limit core has stopped
186
- // waiting — a batch still sending would spend moves on the card that the
187
- // model is told never finished, and it may send them again. A caller
188
- // with no loop hands `execute` no signal, and then a cancel is all there
189
- // is to stop at.
190
- const signal = abortSignal ?? ctx.signal;
191
- const client = makeArcClient(apiKey, { signal });
192
- const stoppedBy = () => signal?.reason?.name ===
193
- "TimeoutError"
194
- ? "this call reached its time limit"
195
- : "this call was cancelled";
196
- // Acting immediately is right — the results below carry the board — but
197
- // this chunk's orientation goes out first, so the model is never acting
198
- // on a board it can no longer see.
199
- const session = await resume();
200
- const opening = orient(session, true);
201
- if (session.state === "WIN" || session.state === "GAME_OVER") {
202
- return (opening +
203
- `This play is over (state=${session.state}). ` +
204
- "Write your final report.");
205
- }
206
- // Reconcile a possibly-interrupted prior action (crash between send and record).
207
- let recovery = "";
208
- if (session.pendingAction) {
209
- recovery =
210
- "\n(A previous action may have been interrupted and not recorded; the board may have advanced.)";
211
- session.pendingAction = null;
212
- }
213
- // Detail per step is budgeted across the sequence, so a long batch cannot
214
- // flood the context: a single step shows plenty, eight show a little each.
215
- const cellCap = Math.max(3, Math.floor(24 / steps.length));
216
- const trace = [];
217
- let sent = 0;
218
- let stopped = null;
219
- // Where the batch left each shape — keyed by where that shape currently
220
- // is, see {@link trackTravel} — plus counts of the steps that did little.
221
- // All three exist for the net line: a model reading five per-step lines
222
- // should not have to add up its own position, and "4 of 5 steps did
223
- // nothing" is the sentence that would have told this play it was walking
224
- // into a wall.
225
- //
226
- // The two idle counts are kept apart because only one of them is evidence
227
- // of a refusal. `noChange` is the cell diff's own verdict and cannot be
228
- // argued with; `noMove` is a board that changed with nothing travelling
229
- // across it, which is a wall in a game you steer and an ordinary move in a
230
- // game you click. Folding the second into the first told a logged `ft09`
231
- // play that five clicks were "every one of them refused or blocked" when
232
- // four of them had each toggled a 36-cell block.
233
- const travels = new Map();
234
- let noChange = 0;
235
- let noMove = 0;
236
- for (const [index, step] of steps.entries()) {
237
- const { action, x, y } = step;
238
- // Guard before sending: these are requests the API would reject or that
239
- // cannot mean anything, so spending an action on them is pure waste.
240
- //
241
- // An empty list is *unknown*, not *none* — a session rebuilt around a
242
- // resolved guid has not seen a frame yet. Guessing "nothing is legal"
243
- // there would deadlock the play, so the API is left to judge, and its
244
- // response fills the list in for every step after this one.
245
- if (session.availableActions.length > 0 &&
246
- !session.availableActions.includes(action)) {
247
- stopped =
248
- `${actionName(action)} is not available ` +
249
- `(available: ${namedActions(session.availableActions)})`;
250
- }
251
- else if (action === 6 && (x === undefined || y === undefined)) {
252
- stopped = "click (action 6) requires x and y (0–63)";
253
- }
254
- if (stopped) {
255
- trace.push(remaining(steps.length, index, stopped));
256
- break;
257
- }
258
- // Write-ahead intent, then send, then record — the crash window is
259
- // between, and it stays per-step so a crash mid-sequence is recoverable
260
- // exactly as a crash mid-action always was.
261
- session.pendingAction = { action, x, y };
262
- await save(session);
263
- // The signal is checked here, with the intent already on disk, because this
264
- // is the last moment before the request: a check ahead of the write would
265
- // miss a cancel that lands during it. A request on an aborted signal is
266
- // never sent, so the intent is taken back rather than left for the next
267
- // chunk to warn about a move that never left. The batch ends the ordinary
268
- // way, with every step already sent recorded.
269
- if (signal?.aborted) {
270
- session.pendingAction = null;
271
- await save(session);
272
- trace.push(remaining(steps.length, index, stoppedBy()));
273
- break;
274
- }
275
- const { frame, cookies } = await client.act({ action, gameId: session.gameId, guid: session.guid, x, y, note }, session.cookies);
276
- // Diff against the board as it was immediately BEFORE this step, so each
277
- // line attributes its change to the one action that caused it. That
278
- // attribution is what makes batching safe: without it the model would see
279
- // that the board changed but not which action changed it.
280
- //
281
- // Both diffs, always: the cell diff is the ground truth for "did anything
282
- // change at all", and the shape delta is the only one of the two that can
283
- // say a *wall* was hit on a board where something else ticks every turn.
284
- const before = board(session);
285
- const next = lastGrid(frame.frame);
286
- const diff = diffGrids(before, next, cellCap);
287
- const delta = diffShapes(before, next);
288
- const prevLevel = session.levelsCompleted;
289
- // `changed < 0` is the first frame — nothing to compare against, so it
290
- // is neither idle nor active.
291
- if (diff.changed === 0)
292
- noChange++;
293
- else if (diff.changed > 0 && delta?.moved.length === 0)
294
- noMove++;
295
- if (delta)
296
- trackTravel(travels, delta.moved);
297
- session.cookies = cookies;
298
- session.guid = frame.guid;
299
- session.state = frame.state;
300
- session.levelsCompleted = frame.levels_completed;
301
- session.availableActions = frame.available_actions;
302
- if (frame.win_levels)
303
- session.winLevels = frame.win_levels;
304
- session.lastGridHex = serializeGrid(next);
305
- session.actionsSent++;
306
- session.pendingAction = null;
307
- sent++;
308
- if (frame.levels_completed > prevLevel &&
309
- !session.levelsReported.includes(frame.levels_completed)) {
310
- session.levelsReported.push(frame.levels_completed);
311
- emitProgress({
312
- // One play per execution, so the level alone identifies the note;
313
- // the gatekeeper dedupes on this key.
314
- key: `arc:${session.gameId}:level:${frame.levels_completed}`,
315
- text: `ARC ${session.gameId}: reached level ${frame.levels_completed}` +
316
- `${session.winLevels ? `/${session.winLevels}` : ""} ` +
317
- `(${session.actionsSent} actions).`
318
- });
319
- }
320
- await save(session);
321
- trace.push(`${index + 1}. ${actionName(action, x, y)} → ${renderEffect(diff, delta)}`);
322
- // The play ended mid-sequence: anything after this would act on a
323
- // finished game.
324
- if (session.state === "WIN" || session.state === "GAME_OVER") {
325
- if (index + 1 < steps.length) {
326
- trace.push(remaining(steps.length, index + 1, `state became ${session.state}`));
327
- }
328
- break;
329
- }
330
- }
331
- const terminal = session.state === "WIN" || session.state === "GAME_OVER"
332
- ? "\nThis play is over — write your final report."
333
- : "";
334
- const header = steps.length === 1
335
- ? ""
336
- : `${steps.length} steps requested, ${sent} sent.\n`;
337
- return (opening +
338
- header +
339
- trace.join("\n") +
340
- renderNet(travels, sent, noChange, noMove) +
341
- "\n" +
342
- stateLine(session) +
343
- terminal +
344
- recovery);
345
- }
346
- }),
347
- arc_inspect: tool({
348
- description: "Look at the current board without taking a game action. Views: 'shapes' (every colored region with its row/column box, and for large sparse ones — walls, floors, frames — the columns they occupy in each band of rows; this is the map, and usually what you want), 'grid' (the whole 64×64 board, one character per cell with a legend and a column ruler; identical rows collapse, so it is cheaper than it sounds), 'region' (a labeled, ruled square around x,y — for fine detail, not for mapping), 'histogram' (how many cells of each color).",
349
- inputSchema: z.object({
350
- view: z.enum(["grid", "region", "histogram", "shapes"]),
351
- x: z.number().int().min(0).max(63).optional(),
352
- y: z.number().int().min(0).max(63).optional(),
353
- radius: z.number().int().min(1).max(20).optional()
354
- }),
355
- execute: async ({ view, x, y, radius }) => {
356
- const session = await resume();
357
- const grid = board(session);
358
- // Ahead of `orient`, so a chunk that opens on a boardless inspect has not
359
- // spent its one orientation on a message that cannot carry it: this
360
- // reply already says the board is unknown, and the next call — the first
361
- // `arc_act`, or an inspect once a frame has landed — orients instead.
362
- if (grid === null) {
363
- return ("No board received yet for this play — the frame arrives with your " +
364
- "first `arc_act`, so take one action and inspect after it.");
365
- }
366
- const opening = orient(session, false);
367
- // The same view of a board nothing has touched, again. Redrawing it
368
- // re-answers a question already answered and teaches nothing, so say what
369
- // is actually true instead. Scoped to this chunk
370
- // by `lastView` being a closure variable: across a chunk boundary the
371
- // earlier render has been trimmed out of context, and pointing at a view
372
- // the model can no longer see would be worse than repeating it.
373
- const key = JSON.stringify({ view, x, y, radius });
374
- if (lastView !== null &&
375
- lastView.key === key &&
376
- lastView.gridHex === session.lastGridHex) {
377
- return (opening +
378
- `Unchanged since your last \`${view}\` view — not one cell of the ` +
379
- "board has changed since, so what you saw then still holds. Act on " +
380
- "it rather than looking again.");
381
- }
382
- lastView = { key, gridHex: session.lastGridHex };
383
- const rendered = (() => {
384
- switch (view) {
385
- case "grid":
386
- return renderGrid(grid);
387
- case "region":
388
- if (x === undefined || y === undefined)
389
- return "region view needs x and y.";
390
- return renderRegion(grid, y, x, radius);
391
- case "histogram":
392
- return colorHistogram(grid)
393
- .map((h) => `${colorName(h.color)}: ${h.count} cells`)
394
- .join("\n");
395
- case "shapes":
396
- return renderShapes(grid);
397
- }
398
- })();
399
- return opening + rendered;
400
- }
401
- })
402
- };
403
- // No `abort` hook: the only external state this family used to hold was the
404
- // scorecard, and nothing closes one any more — the API retires an idle card on
405
- // its own. A play left unfinished is simply a run the card records as
406
- // incomplete.
407
- return { tools };
408
- }
409
- /**
410
- * A session file for the resolved play, built from whatever the parent could
411
- * supply.
412
- *
413
- * With `runtime.frame` — this chunk is the one that opened the play — the state
414
- * is complete and indistinguishable from what a local RESET used to produce.
415
- * Without it the play already existed, so everything a frame carries is unknown:
416
- * empty `availableActions` and a null board, both filled in by the first ACTION.
417
- * Only `guid` matters for correctness, and it always comes from the parent.
418
- */
419
- function joinSession(gameId, cardId, guid, runtime) {
420
- const frame = runtime.frame;
421
- return {
422
- cardId,
423
- gameId,
424
- guid,
425
- cookies: runtime.cookies ?? {},
426
- winLevels: frame?.win_levels ?? 0,
427
- levelsCompleted: frame?.levels_completed ?? 0,
428
- state: frame?.state ?? "NOT_FINISHED",
429
- availableActions: frame?.available_actions ?? [],
430
- actionsSent: 0,
431
- levelsReported: [],
432
- lastGridHex: frame ? serializeGrid(lastGrid(frame.frame)) : null,
433
- pendingAction: null
434
- };
435
- }
436
- /** Human names for the seven actions, so results read as moves not opcodes. */
437
- const ACTION_NAMES = {
438
- 1: "up",
439
- 2: "down",
440
- 3: "left",
441
- 4: "right",
442
- 5: "interact",
443
- 6: "click",
444
- 7: "undo"
445
- };
446
- /** How many actions one `arc_act` call may carry. */
447
- const MAX_STEPS = 8;
448
- function actionName(action, x, y) {
449
- const name = ACTION_NAMES[action] ?? `action ${action}`;
450
- return action === 6 && x !== undefined && y !== undefined
451
- ? `${name}(${x},${y})`
452
- : name;
453
- }
454
- /**
455
- * `3=left, 6=click` — the legal actions as number **and** name.
456
- *
457
- * The number alone is ambiguous in exactly the case that matters most. A
458
- * click-only game offers `[6]`, and `available actions: 6` reads as *six actions
459
- * are available* at least as naturally as it reads as *action 6 is available* —
460
- * one of the two lines every result of a logged `ft09` play ended on. Naming them
461
- * costs a few tokens and cannot be misread, and the number stays because the
462
- * number is what `arc_act` takes.
463
- */
464
- function namedActions(actions) {
465
- if (actions.length === 0)
466
- return "none";
467
- return actions
468
- .map((action) => {
469
- const name = ACTION_NAMES[action];
470
- return name === undefined ? String(action) : `${action}=${name}`;
471
- })
472
- .join(", ");
473
- }
474
- /** "5-8. not sent: <why>" — an aborted tail, named so it is never assumed to have run. */
475
- function remaining(total, from, why) {
476
- const label = from + 1 === total ? `${total}.` : `${from + 1}-${total}.`;
477
- return `${label} not sent: ${why}`;
478
- }
479
- /**
480
- * What one action did, in the most informative register the frames support.
481
- *
482
- * Shapes first, because "nothing moved" and "the orange 5×5 went right 5" are the
483
- * two answers a player needs and neither is recoverable from a cell count. The
484
- * cell diff takes over whenever the shape view declines — no pairing at all, or a
485
- * repaint of everything — so a board this heuristic cannot read is described as
486
- * fully as it always was, never as a confident "nothing moved".
487
- */
488
- function renderEffect(diff, delta) {
489
- if (diff.changed < 0)
490
- return "first frame";
491
- // Nothing changed anywhere, counters included: worth saying in its own words,
492
- // since it means the game refused the action outright rather than absorbing it.
493
- if (diff.changed === 0)
494
- return "0 cells changed (no effect at all)";
495
- const shapes = delta === null ? null : renderShapeDelta(delta);
496
- return shapes ?? renderDiff(diff);
497
- }
498
- /**
499
- * Where a whole batch left things: net travel per shape, then how many of its
500
- * steps did nothing and how many moved nothing.
501
- *
502
- * The idle counts are what this rendering exists for. A batch is not stopped when
503
- * a step does nothing — different games mean different things by it, and guessing
504
- * would throw away legitimate sequences — so the batch runs to the end and is
505
- * *told on*. "4 of 5 steps changed not one cell" is a sentence a model cannot
506
- * misread; four consecutive fuel-bar diffs, as these logs show, is one it can.
507
- *
508
- * They are reported as two separate sentences because they carry different
509
- * weight. A step that changed not one cell was refused, and this line says so. A
510
- * step that changed the board without moving a shape is stated as exactly that
511
- * and no more — reading it as a wall is a judgement about the game's rules, which
512
- * belongs to the model and the soul, not to a renderer that has never been told
513
- * whether this game moves anything at all.
514
- */
515
- function renderNet(travels, sent, noChange, noMove) {
516
- if (sent < 2)
517
- return "";
518
- const parts = [...travels.values()]
519
- .sort((a, b) => b.shape.size - a.shape.size)
520
- .slice(0, NET_SHAPES)
521
- .map(renderTravel);
522
- const moved = parts.length === 0 ? "no shape travelled" : `net: ${parts.join("; ")}`;
523
- const idle = [];
524
- if (noChange > 0) {
525
- idle.push(`${noChange} of ${sent} step(s) changed not one cell` +
526
- (noChange === sent ? " (every one of them was refused or blocked)" : ""));
527
- }
528
- if (noMove > 0) {
529
- idle.push(`${noMove} of ${sent} step(s) changed the board without moving a shape`);
530
- }
531
- return `\n${moved}${idle.length === 0 ? "" : ` — ${idle.join("; ")}`}.`;
532
- }
533
- /** How many shapes the net line names before it stops. */
534
- const NET_SHAPES = 3;
535
- /**
536
- * What one action did as cells: how much changed, where, and which colors.
537
- *
538
- * The fallback register — see {@link renderEffect}, which owns the "first frame"
539
- * and "nothing changed" cases so this one only ever describes a real change.
540
- */
541
- function renderDiff(diff) {
542
- const where = diff.box === null ? "" : `, ${describeBox(diff.box)}`;
543
- const examples = diff.cells.length === 0
544
- ? ""
545
- : ` (${diff.cells.length < diff.changed ? "e.g. " : ""}` +
546
- `${diff.cells.map(describeCell).join(", ")})`;
547
- return `${diff.changed} cells changed${where}${examples}`;
548
- }
549
- /**
550
- * Level / state / legal-actions — the board-independent status, one line.
551
- *
552
- * A session that joined a play without a frame knows none of it, and saying
553
- * `level 0 | state NOT_FINISHED | available actions: none` there would state
554
- * three placeholders as facts — the last of which reads as "you may do nothing".
555
- * Such a session is reported as what it is instead.
556
- */
557
- function stateLine(session) {
558
- if (session.availableActions.length === 0 && session.lastGridHex === null) {
559
- return "level, state and available actions arrive with your next action's result";
560
- }
561
- return [
562
- `level ${session.levelsCompleted}${session.winLevels ? `/${session.winLevels}` : ""}`,
563
- `state ${session.state}`,
564
- `available actions: ${namedActions(session.availableActions)}`
565
- ].join(" | ");
566
- }
567
- /**
568
- * Full summary used when `arc_act` joins the play, including where every shape
569
- * is. Orienting the model here is what keeps joining free: it never costs a turn
570
- * of its own, and the model is not left acting blind.
571
- */
572
- function describeState(session, prefix) {
573
- const grid = session.lastGridHex === null ? null : parseGrid(session.lastGridHex);
574
- const shapes = grid === null ? "" : `\nBoard:\n${renderShapes(grid)}`;
575
- return `${prefix} ${stateLine(session)}${shapes}`;
576
- }