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