@dynamicagents/plugins 0.11.0 → 0.12.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 +12 -13
- package/dist/arc-agi/analysis.d.ts +16 -30
- package/dist/arc-agi/analysis.js +20 -47
- package/dist/arc-agi/client.d.ts +3 -2
- package/dist/arc-agi/game-tools.d.ts +5 -6
- package/dist/arc-agi/index.d.ts +6 -8
- package/dist/arc-agi/index.js +3 -3
- package/dist/arc-agi/main-agent.d.ts +5 -8
- package/dist/arc-agi/main-agent.js +5 -8
- package/dist/arc-agi/recipe.d.ts +17 -20
- package/dist/arc-agi/recipe.js +17 -20
- package/dist/arc-agi/soul.d.ts +16 -27
- package/dist/arc-agi/soul.js +16 -27
- package/dist/arc-agi/store.d.ts +4 -5
- package/dist/arc-agi/store.js +4 -5
- package/dist/arc-agi/tools.d.ts +1 -1
- package/dist/arc-agi/tools.js +26 -19
- package/dist/arc-agi/types.d.ts +6 -7
- package/dist/claude-code/config.d.ts +88 -11
- package/dist/claude-code/config.js +11 -2
- package/dist/claude-code/credentials.d.ts +21 -9
- package/dist/claude-code/credentials.js +43 -9
- package/dist/claude-code/egress.d.ts +7 -13
- package/dist/claude-code/egress.js +9 -10
- package/dist/claude-code/events.d.ts +47 -0
- package/dist/claude-code/events.js +96 -10
- package/dist/claude-code/index.d.ts +31 -13
- package/dist/claude-code/index.js +62 -15
- package/dist/claude-code/run.d.ts +135 -41
- package/dist/claude-code/run.js +366 -61
- package/dist/computer/advisory.d.ts +31 -18
- package/dist/computer/advisory.js +30 -5
- package/dist/computer/context.d.ts +23 -0
- package/dist/computer/context.js +225 -0
- package/dist/computer/file-lock.d.ts +5 -0
- package/dist/computer/file-lock.js +39 -0
- package/dist/computer/gate.d.ts +8 -8
- package/dist/computer/gate.js +7 -7
- package/dist/computer/host/ca-trust.d.ts +88 -0
- package/dist/computer/host/ca-trust.js +303 -0
- package/dist/computer/host/container-deps.d.ts +54 -0
- package/dist/computer/host/container-deps.js +224 -0
- package/dist/computer/host/container-fault.d.ts +38 -0
- package/dist/computer/host/container-fault.js +58 -0
- package/dist/computer/host/git-host.d.ts +62 -0
- package/dist/computer/host/git-host.js +156 -0
- package/dist/computer/host/git-identity.d.ts +57 -0
- package/dist/computer/host/git-identity.js +147 -0
- package/dist/computer/host/install-job.d.ts +121 -0
- package/dist/computer/host/install-job.js +766 -0
- package/dist/computer/host/sync.d.ts +111 -0
- package/dist/computer/host/sync.js +124 -0
- package/dist/computer/host/wake.d.ts +19 -0
- package/dist/computer/host/wake.js +10 -0
- package/dist/computer/host/workspace.d.ts +424 -0
- package/dist/computer/host/workspace.js +1258 -0
- package/dist/computer/index.d.ts +70 -50
- package/dist/computer/index.js +79 -767
- package/dist/computer/install.d.ts +8 -12
- package/dist/computer/install.js +6 -9
- package/dist/computer/paths.d.ts +21 -16
- package/dist/computer/paths.js +40 -38
- package/dist/computer/read.d.ts +18 -22
- package/dist/computer/read.js +24 -27
- package/dist/computer/render.d.ts +28 -0
- package/dist/computer/render.js +34 -1
- package/dist/computer/shell.d.ts +45 -0
- package/dist/computer/shell.js +80 -0
- package/dist/computer/tools-exec.d.ts +3 -0
- package/dist/computer/tools-exec.js +269 -0
- package/dist/computer/tools-file.d.ts +4 -0
- package/dist/computer/tools-file.js +124 -0
- package/dist/computer/tools-find.d.ts +3 -0
- package/dist/computer/tools-find.js +216 -0
- package/dist/recall/index.d.ts +9 -3
- package/dist/recall/index.js +22 -12
- package/dist/repo/checkout.d.ts +34 -4
- package/dist/repo/checkout.js +36 -1
- package/dist/repo/context.d.ts +109 -0
- package/dist/repo/context.js +530 -0
- package/dist/repo/index.d.ts +6 -29
- package/dist/repo/index.js +17 -1027
- package/dist/repo/tools-clone.d.ts +4 -0
- package/dist/repo/tools-clone.js +192 -0
- package/dist/repo/tools-forge.d.ts +4 -0
- package/dist/repo/tools-forge.js +179 -0
- package/dist/repo/tools-review.d.ts +3 -0
- package/dist/repo/tools-review.js +373 -0
- package/dist/repo/tools-worktree.d.ts +4 -0
- package/dist/repo/tools-worktree.js +221 -0
- package/dist/triage/index.d.ts +6 -1
- package/dist/triage/index.js +32 -23
- package/dist/workspace/index.d.ts +3 -3
- package/dist/workspace/index.js +3 -3
- package/package.json +21 -15
package/dist/arc-agi/store.js
CHANGED
|
@@ -16,11 +16,10 @@ import { z } from "zod";
|
|
|
16
16
|
*
|
|
17
17
|
* A plugin must never run drizzle's durable-sqlite **migrator**: it keeps one
|
|
18
18
|
* flat integer journal and one global `__drizzle_migrations` table, which two
|
|
19
|
-
* independently-versioned packages cannot share
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* correct diff at all.
|
|
19
|
+
* independently-versioned packages cannot share: two agents consuming the same
|
|
20
|
+
* module fork that journal at the first index they disagree on. And
|
|
21
|
+
* `drizzle-kit generate` diffs against a snapshot in a single output directory,
|
|
22
|
+
* so a plugin shipping from its own repo cannot produce a correct diff at all.
|
|
24
23
|
*
|
|
25
24
|
* None of that touches the **query builder**, which is a typed wrapper over the
|
|
26
25
|
* same `DurableObjectStorage` holding no journal and no connection state. So the
|
package/dist/arc-agi/tools.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ import type { ToolFamilyContext, RecipeToolSet } from "@dynamicagents/core";
|
|
|
30
30
|
/**
|
|
31
31
|
* This family's key in a Recipe's `toolFamilies`. Exported because it is also
|
|
32
32
|
* what tells the parent a Subtask needs a scorecard leased before it can run —
|
|
33
|
-
* see `resolveRuntime` in
|
|
33
|
+
* see `resolveRuntime` in `./index.ts`.
|
|
34
34
|
*/
|
|
35
35
|
export declare const ARC_GAME_FAMILY = "arc-game";
|
|
36
36
|
export declare function buildArcGameTools(apiKey: string, ctx: ToolFamilyContext): RecipeToolSet;
|
package/dist/arc-agi/tools.js
CHANGED
|
@@ -35,19 +35,15 @@ import { ARC_SESSION_PATH } from "./types.js";
|
|
|
35
35
|
/**
|
|
36
36
|
* This family's key in a Recipe's `toolFamilies`. Exported because it is also
|
|
37
37
|
* what tells the parent a Subtask needs a scorecard leased before it can run —
|
|
38
|
-
* see `resolveRuntime` in
|
|
38
|
+
* see `resolveRuntime` in `./index.ts`.
|
|
39
39
|
*/
|
|
40
40
|
export const ARC_GAME_FAMILY = "arc-game";
|
|
41
41
|
export function buildArcGameTools(apiKey, ctx) {
|
|
42
42
|
const { workspace, emitProgress, params } = ctx;
|
|
43
|
-
// Closed over from the plugin's config, never `ctx.env
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// the only thing that works on Workers at all, where `env` has no module scope.
|
|
48
|
-
// The execution's signal goes in the same way, so a cancel reaches a request or
|
|
49
|
-
// a backoff without every call having to carry it.
|
|
50
|
-
const client = makeArcClient(apiKey, { signal: ctx.signal });
|
|
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.
|
|
51
47
|
const runtime = runtimeAs(ctx.runtime);
|
|
52
48
|
// All settled before the model runs — it cannot pick a different game, and it
|
|
53
49
|
// cannot see, choose, or name a card or a play at all. The game is the type's
|
|
@@ -183,7 +179,19 @@ export function buildArcGameTools(apiKey, ctx) {
|
|
|
183
179
|
.optional()
|
|
184
180
|
.describe("brief reasoning for this move or sequence")
|
|
185
181
|
}),
|
|
186
|
-
execute: async ({ steps, note }) => {
|
|
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";
|
|
187
195
|
// Acting immediately is right — the results below carry the board — but
|
|
188
196
|
// this chunk's orientation goes out first, so the model is never acting
|
|
189
197
|
// on a board it can no longer see.
|
|
@@ -251,16 +259,16 @@ export function buildArcGameTools(apiKey, ctx) {
|
|
|
251
259
|
// exactly as a crash mid-action always was.
|
|
252
260
|
session.pendingAction = { action, x, y };
|
|
253
261
|
await save(session);
|
|
254
|
-
//
|
|
262
|
+
// The signal is checked here, with the intent already on disk, because this
|
|
255
263
|
// is the last moment before the request: a check ahead of the write would
|
|
256
264
|
// miss a cancel that lands during it. A request on an aborted signal is
|
|
257
265
|
// never sent, so the intent is taken back rather than left for the next
|
|
258
266
|
// chunk to warn about a move that never left. The batch ends the ordinary
|
|
259
267
|
// way, with every step already sent recorded.
|
|
260
|
-
if (
|
|
268
|
+
if (signal?.aborted) {
|
|
261
269
|
session.pendingAction = null;
|
|
262
270
|
await save(session);
|
|
263
|
-
trace.push(remaining(steps.length, index,
|
|
271
|
+
trace.push(remaining(steps.length, index, stoppedBy()));
|
|
264
272
|
break;
|
|
265
273
|
}
|
|
266
274
|
const { frame, cookies } = await client.act({ action, gameId: session.gameId, guid: session.guid, x, y, note }, session.cookies);
|
|
@@ -391,10 +399,9 @@ export function buildArcGameTools(apiKey, ctx) {
|
|
|
391
399
|
}
|
|
392
400
|
})
|
|
393
401
|
};
|
|
394
|
-
// No `abort` hook:
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
// incomplete.
|
|
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.
|
|
398
405
|
return { tools };
|
|
399
406
|
}
|
|
400
407
|
/**
|
|
@@ -402,8 +409,8 @@ export function buildArcGameTools(apiKey, ctx) {
|
|
|
402
409
|
* supply.
|
|
403
410
|
*
|
|
404
411
|
* With `runtime.frame` — this chunk is the one that opened the play — the state
|
|
405
|
-
* is complete
|
|
406
|
-
*
|
|
412
|
+
* is complete. Without it the play already existed, so everything a frame
|
|
413
|
+
* carries is unknown:
|
|
407
414
|
* empty `availableActions` and a null board, both filled in by the first ACTION.
|
|
408
415
|
* Only `guid` matters for correctness, and it always comes from the parent.
|
|
409
416
|
*/
|
package/dist/arc-agi/types.d.ts
CHANGED
|
@@ -188,13 +188,12 @@ export declare const ARC_SESSION_PATH = "arc/session.json";
|
|
|
188
188
|
* This plugin's slice of a Subtask's runtime bag — what the parent resolves for
|
|
189
189
|
* an execution and no model could supply.
|
|
190
190
|
*
|
|
191
|
-
* Core's `SubtaskRuntime` is an open `Record<string, unknown>`, and
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* this one module.
|
|
191
|
+
* Core's `SubtaskRuntime` is an open `Record<string, unknown>`, and this slice is
|
|
192
|
+
* why: declared in core's own delegation types instead, core would import a
|
|
193
|
+
* domain's vocabulary and every unrelated agent would carry it in its bundle.
|
|
194
|
+
* The plugin that writes this slice is the plugin that reads it, so narrow it
|
|
195
|
+
* with `runtimeAs<ArcRuntime>` at the boundary and keep the cast's blast radius
|
|
196
|
+
* to this one module.
|
|
198
197
|
*/
|
|
199
198
|
export type ArcRuntime = {
|
|
200
199
|
/** The scorecard leased for this chunk. */
|
|
@@ -25,6 +25,21 @@ export type PermissionMode = "default" | "acceptEdits" | "bypassPermissions" | "
|
|
|
25
25
|
* {@link ClaudeCodeConfig.permissionMode}.
|
|
26
26
|
*/
|
|
27
27
|
export declare const DEFAULT_PERMISSION_MODE: PermissionMode;
|
|
28
|
+
/**
|
|
29
|
+
* The effort levels `claude --effort` accepts.
|
|
30
|
+
*
|
|
31
|
+
* Spelled out for a sharper reason than {@link PermissionMode}: an unrecognised
|
|
32
|
+
* permission mode exits 1, but an unrecognised effort is **warned about on
|
|
33
|
+
* stderr and then ignored**, leaving the session at the model's default. A typo
|
|
34
|
+
* here does not fail, it quietly buys nothing — and stderr from a headless
|
|
35
|
+
* session reaches an operator far less reliably than a type error reaches a
|
|
36
|
+
* developer.
|
|
37
|
+
*
|
|
38
|
+
* A model that does not carry a level is the same silence: the CLI clamps down
|
|
39
|
+
* to `high` rather than refusing, so asking for more than a model offers is
|
|
40
|
+
* indistinguishable from asking for `high`.
|
|
41
|
+
*/
|
|
42
|
+
export type EffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
|
|
28
43
|
export interface ClaudeCodeConfig {
|
|
29
44
|
/**
|
|
30
45
|
* The credential pool, in priority order — index 0 is tried first.
|
|
@@ -61,14 +76,28 @@ export interface ClaudeCodeConfig {
|
|
|
61
76
|
/** Which model the session runs. Unset, Claude Code picks its own default. */
|
|
62
77
|
model?: string;
|
|
63
78
|
/**
|
|
64
|
-
*
|
|
79
|
+
* How hard the session's model thinks, per turn.
|
|
80
|
+
*
|
|
81
|
+
* Unset, the model's own default applies, which is `high` for the frontier
|
|
82
|
+
* models this plugin exists to reach. Raising it buys depth at a multiple of
|
|
83
|
+
* the spend, per turn — so the level decides how many sessions a credential
|
|
84
|
+
* holds, not just how well one thinks. The README's Costs section carries the
|
|
85
|
+
* multiples and the sizing they imply.
|
|
65
86
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
87
|
+
* Not to be confused with the host round loop's own reasoning effort, which
|
|
88
|
+
* belongs to a different model on the Worker side. This one reaches only the
|
|
89
|
+
* CLI in the container.
|
|
90
|
+
*/
|
|
91
|
+
effort?: EffortLevel;
|
|
92
|
+
/**
|
|
93
|
+
* Caps on Claude Code's own subagent tree.
|
|
94
|
+
*
|
|
95
|
+
* Advisory, and the distinction is the reason there is no turn ceiling beside
|
|
96
|
+
* them — see the README on what this package deliberately does not do. That
|
|
97
|
+
* tree is invisible to Dynamic Agents' scheduler, so it multiplies whatever
|
|
98
|
+
* these say; {@link timeoutMs} is the ceiling that actually holds, because the
|
|
99
|
+
* container runtime enforces it.
|
|
69
100
|
*/
|
|
70
|
-
maxTurns?: number;
|
|
71
|
-
/** Caps on Claude Code's own subagent tree. Advisory, like `maxTurns`. */
|
|
72
101
|
maxSubagentDepth?: number;
|
|
73
102
|
maxConcurrentSubagents?: number;
|
|
74
103
|
/**
|
|
@@ -116,10 +145,33 @@ export interface ClaudeCodeConfig {
|
|
|
116
145
|
/**
|
|
117
146
|
* How long one chunk blocks before checkpointing and yielding.
|
|
118
147
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
148
|
+
* **A read deadline on the session's stdout stream, not a halt.** Nothing is
|
|
149
|
+
* stopped and nothing is destroyed when it expires: the host detaches, the
|
|
150
|
+
* session process inside the container never notices, and the next chunk
|
|
151
|
+
* re-attaches to the live session from the cursor. So the window is bounded
|
|
152
|
+
* against `STEP_TIMEOUT_MS` rather than against core's `CHUNK_SOFT_MS` —
|
|
153
|
+
* there is no in-flight turn to overrun, which is what sizes the recipe
|
|
154
|
+
* path's budget.
|
|
155
|
+
*
|
|
156
|
+
* What prices the headroom instead is the post-exit filesystem pull, which is
|
|
157
|
+
* deliberately unbounded because it carries whatever the session wrote,
|
|
158
|
+
* dependency trees included, and can outlast the window. So no value here can
|
|
159
|
+
* guarantee the chunk fits inside the step timeout: what is left over is
|
|
160
|
+
* sized for an *expected* pull, plus the sub-second unwind of yielding and
|
|
161
|
+
* the step's own bookkeeping, and a large enough pull overruns it whatever
|
|
162
|
+
* this is set to. See {@link file://./run.ts DrainOptions.windowMs}.
|
|
163
|
+
*
|
|
164
|
+
* **Overrunning is the cheaper failure, which is why the default is
|
|
165
|
+
* generous.** The step is killed and retried; the filesystem sync resumes
|
|
166
|
+
* from the blocks it has already committed rather than starting over, and the
|
|
167
|
+
* retry re-attaches to the session from the cursor — so an overrun costs a
|
|
168
|
+
* retry and some replayed notes, never a session's edits. An undersized
|
|
169
|
+
* window costs a chunk boundary on *every* session instead — a checkpoint, a
|
|
170
|
+
* fresh step, a re-hydrated subagent and a re-attach that replays the tail —
|
|
171
|
+
* paid whether or not the pull was ever going to be large.
|
|
172
|
+
*
|
|
173
|
+
* It must also be long enough that a session does not exhaust its branch's
|
|
174
|
+
* chunk allowance while it is still thinking.
|
|
123
175
|
*/
|
|
124
176
|
windowMs?: number;
|
|
125
177
|
/**
|
|
@@ -133,8 +185,33 @@ export interface ClaudeCodeConfig {
|
|
|
133
185
|
timeoutMs?: number;
|
|
134
186
|
/** Extra environment for the session. Never secrets — see the README. */
|
|
135
187
|
env?: Record<string, string>;
|
|
188
|
+
/**
|
|
189
|
+
* Who the session's own git commits are attributed to.
|
|
190
|
+
*
|
|
191
|
+
* A session has a shell and a checkout, so it commits — and it amends,
|
|
192
|
+
* rebases and cherry-picks, none of which a host can intercept the way it
|
|
193
|
+
* names an identity on a commit it makes itself. Left unset, every one of
|
|
194
|
+
* those falls through to whatever config the checkout happens to carry, which
|
|
195
|
+
* is a value frozen whenever that checkout was created.
|
|
196
|
+
*
|
|
197
|
+
* See {@link file://./run.ts buildLaunch} for why this becomes an environment
|
|
198
|
+
* rather than a `git config`.
|
|
199
|
+
*/
|
|
200
|
+
author?: {
|
|
201
|
+
name: string;
|
|
202
|
+
email: string;
|
|
203
|
+
};
|
|
136
204
|
}
|
|
137
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Twenty minutes against a 30-minute step timeout, which leaves ~10 minutes for
|
|
207
|
+
* the sub-second unwind of yielding the window and for the post-exit filesystem
|
|
208
|
+
* pull. An expected pull fits there; a pull that carries an install's
|
|
209
|
+
* dependency tree may not, and nothing here promises it will — that pull is
|
|
210
|
+
* unbounded by design. Sized for the expected case deliberately: the pull that
|
|
211
|
+
* overruns is retried and resumes, while a shorter window would buy that back
|
|
212
|
+
* by paying for extra chunk boundaries on every session. See
|
|
213
|
+
* {@link ClaudeCodeConfig.windowMs}.
|
|
214
|
+
*/
|
|
138
215
|
export declare const DEFAULT_WINDOW_MS: number;
|
|
139
216
|
/** Fifteen minutes, well under the coder workspace's 20-minute idle timer. */
|
|
140
217
|
export declare const DEFAULT_TIMEOUT_MS: number;
|
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
* {@link ClaudeCodeConfig.permissionMode}.
|
|
8
8
|
*/
|
|
9
9
|
export const DEFAULT_PERMISSION_MODE = "bypassPermissions";
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Twenty minutes against a 30-minute step timeout, which leaves ~10 minutes for
|
|
12
|
+
* the sub-second unwind of yielding the window and for the post-exit filesystem
|
|
13
|
+
* pull. An expected pull fits there; a pull that carries an install's
|
|
14
|
+
* dependency tree may not, and nothing here promises it will — that pull is
|
|
15
|
+
* unbounded by design. Sized for the expected case deliberately: the pull that
|
|
16
|
+
* overruns is retried and resumes, while a shorter window would buy that back
|
|
17
|
+
* by paying for extra chunk boundaries on every session. See
|
|
18
|
+
* {@link ClaudeCodeConfig.windowMs}.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_WINDOW_MS = 20 * 60_000;
|
|
12
21
|
/** Fifteen minutes, well under the coder workspace's 20-minute idle timer. */
|
|
13
22
|
export const DEFAULT_TIMEOUT_MS = 15 * 60_000;
|
|
@@ -5,15 +5,12 @@
|
|
|
5
5
|
* ## Why a pool rather than a budget
|
|
6
6
|
*
|
|
7
7
|
* A Claude subscription has two limits that matter here: a rolling 5-hour
|
|
8
|
-
* session bucket and a weekly one. Neither is readable,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* terminal `result` event), so it was never a cap on spend at all — only a gate
|
|
15
|
-
* on starting. Meanwhile the bucket itself says so, precisely, the moment it is
|
|
16
|
-
* empty: Anthropic answers `429`.
|
|
8
|
+
* session bucket and a weekly one. Neither is readable, so staying under them by
|
|
9
|
+
* *estimating* spend is the wrong instrument twice over — the estimate is a
|
|
10
|
+
* guess about a bucket nobody can see, and it only moves when a run *ends*,
|
|
11
|
+
* since usage is learned from the terminal `result` event. It is never a cap on
|
|
12
|
+
* spend, only a gate on starting. The bucket itself says so precisely the moment
|
|
13
|
+
* it is empty: Anthropic answers `429`.
|
|
17
14
|
*
|
|
18
15
|
* So this module does not predict. It **detects and routes around**: the
|
|
19
16
|
* credential becomes an ordered pool, the gateway uses the first usable entry,
|
|
@@ -154,6 +151,21 @@ export type Refusal = {
|
|
|
154
151
|
kind: "transient";
|
|
155
152
|
retryAfterMs: number;
|
|
156
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* Read the client's own bucket reading as a verdict on the credential in use.
|
|
156
|
+
*
|
|
157
|
+
* `undefined` means "nothing to act on", which today is every reading — see
|
|
158
|
+
* {@link RATE_LIMIT_SPENT}. Otherwise the epoch-milliseconds the bucket refills,
|
|
159
|
+
* ready for {@link CredentialPool.spend}.
|
|
160
|
+
*
|
|
161
|
+
* Here rather than beside the parser because this is the same question
|
|
162
|
+
* {@link readRefusal} answers about a response: is this credential still worth
|
|
163
|
+
* using? The parser's job is to say what the line contained.
|
|
164
|
+
*/
|
|
165
|
+
export declare function readRateLimitEvent(info: {
|
|
166
|
+
status: string;
|
|
167
|
+
resetsAt?: number;
|
|
168
|
+
}): number | undefined;
|
|
157
169
|
/**
|
|
158
170
|
* Read a response as a verdict on the credential that produced it.
|
|
159
171
|
*
|
|
@@ -5,15 +5,12 @@
|
|
|
5
5
|
* ## Why a pool rather than a budget
|
|
6
6
|
*
|
|
7
7
|
* A Claude subscription has two limits that matter here: a rolling 5-hour
|
|
8
|
-
* session bucket and a weekly one. Neither is readable,
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* terminal `result` event), so it was never a cap on spend at all — only a gate
|
|
15
|
-
* on starting. Meanwhile the bucket itself says so, precisely, the moment it is
|
|
16
|
-
* empty: Anthropic answers `429`.
|
|
8
|
+
* session bucket and a weekly one. Neither is readable, so staying under them by
|
|
9
|
+
* *estimating* spend is the wrong instrument twice over — the estimate is a
|
|
10
|
+
* guess about a bucket nobody can see, and it only moves when a run *ends*,
|
|
11
|
+
* since usage is learned from the terminal `result` event. It is never a cap on
|
|
12
|
+
* spend, only a gate on starting. The bucket itself says so precisely the moment
|
|
13
|
+
* it is empty: Anthropic answers `429`.
|
|
17
14
|
*
|
|
18
15
|
* So this module does not predict. It **detects and routes around**: the
|
|
19
16
|
* credential becomes an ordered pool, the gateway uses the first usable entry,
|
|
@@ -243,6 +240,43 @@ function resetAtFrom(value) {
|
|
|
243
240
|
const parsed = Date.parse(trimmed);
|
|
244
241
|
return Number.isNaN(parsed) ? undefined : parsed;
|
|
245
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* The `rate_limit_event` statuses that mean the bucket is empty.
|
|
245
|
+
*
|
|
246
|
+
* **Empty, and that is the finding rather than a placeholder.** The client
|
|
247
|
+
* announces its bucket on every session and the only status ever observed is
|
|
248
|
+
* {@link file://./events.ts RATE_LIMIT_OK} — so there is no known value here to
|
|
249
|
+
* put in, and putting in a guess is the expensive half of the asymmetry
|
|
250
|
+
* {@link readRefusal} is built around: reading a healthy bucket as exhausted
|
|
251
|
+
* retires a working credential for hours, and with a pool of one that is an
|
|
252
|
+
* outage. The same reasoning that leaves a 403 unclassified leaves this set
|
|
253
|
+
* empty.
|
|
254
|
+
*
|
|
255
|
+
* What fills it is the log. `run.ts` reports every change of reading, and a
|
|
256
|
+
* status that is not `allowed` is also surfaced as a progress note, so the first
|
|
257
|
+
* real one arrives named — at which point it goes in here and the pool starts
|
|
258
|
+
* rotating *before* Anthropic refuses a request rather than because it did.
|
|
259
|
+
*/
|
|
260
|
+
const RATE_LIMIT_SPENT = new Set();
|
|
261
|
+
/**
|
|
262
|
+
* Read the client's own bucket reading as a verdict on the credential in use.
|
|
263
|
+
*
|
|
264
|
+
* `undefined` means "nothing to act on", which today is every reading — see
|
|
265
|
+
* {@link RATE_LIMIT_SPENT}. Otherwise the epoch-milliseconds the bucket refills,
|
|
266
|
+
* ready for {@link CredentialPool.spend}.
|
|
267
|
+
*
|
|
268
|
+
* Here rather than beside the parser because this is the same question
|
|
269
|
+
* {@link readRefusal} answers about a response: is this credential still worth
|
|
270
|
+
* using? The parser's job is to say what the line contained.
|
|
271
|
+
*/
|
|
272
|
+
export function readRateLimitEvent(info) {
|
|
273
|
+
if (!RATE_LIMIT_SPENT.has(info.status))
|
|
274
|
+
return undefined;
|
|
275
|
+
// Seconds on the wire. A value passed through unconverted lands in 1970, and a
|
|
276
|
+
// credential marked spent until then reads as usable — the failure would be
|
|
277
|
+
// this function doing nothing, silently.
|
|
278
|
+
return info.resetsAt === undefined ? undefined : info.resetsAt * 1000;
|
|
279
|
+
}
|
|
246
280
|
/**
|
|
247
281
|
* Read a response as a verdict on the credential that produced it.
|
|
248
282
|
*
|
|
@@ -33,10 +33,9 @@ import { type CredentialStore } from "./credentials.js";
|
|
|
33
33
|
*
|
|
34
34
|
* ## What this deliberately does not do
|
|
35
35
|
*
|
|
36
|
-
* It does not meter
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* itself announces the answer exactly — see the docblock in `credentials.ts`.
|
|
36
|
+
* It does not meter, and a budget gate must not come back: an estimate of spend
|
|
37
|
+
* is a guess about a bucket nobody can read, moves only once per run, and the
|
|
38
|
+
* bucket announces the answer exactly — see the docblock in `credentials.ts`.
|
|
40
39
|
*
|
|
41
40
|
* And it does not, by default, **bound exfiltration**. The container holds the
|
|
42
41
|
* checkout, and with no restriction configured it can send it anywhere. That is
|
|
@@ -49,16 +48,11 @@ import { type CredentialStore } from "./credentials.js";
|
|
|
49
48
|
export declare const ANTHROPIC_HOST = "api.anthropic.com";
|
|
50
49
|
export interface EgressConfig {
|
|
51
50
|
/**
|
|
52
|
-
* The credential pool
|
|
51
|
+
* The credential pool — see {@link file://./config.ts ClaudeCodeConfig.credentials}.
|
|
53
52
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* swap happens here.
|
|
58
|
-
*
|
|
59
|
-
* An array of one is entirely valid and behaves as 0.5.0's single credential
|
|
60
|
-
* did — used until its bucket empties, after which requests are refused with
|
|
61
|
-
* the reset time.
|
|
53
|
+
* Called per request, so it must be cheap. This is the one place a real
|
|
54
|
+
* credential is read: the session is launched with `CREDENTIAL_PLACEHOLDER`
|
|
55
|
+
* and the swap happens here.
|
|
62
56
|
*/
|
|
63
57
|
credentials: () => readonly string[];
|
|
64
58
|
/**
|
|
@@ -33,10 +33,9 @@ import { credentialPool, readRefusal } from "./credentials.js";
|
|
|
33
33
|
*
|
|
34
34
|
* ## What this deliberately does not do
|
|
35
35
|
*
|
|
36
|
-
* It does not meter
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* itself announces the answer exactly — see the docblock in `credentials.ts`.
|
|
36
|
+
* It does not meter, and a budget gate must not come back: an estimate of spend
|
|
37
|
+
* is a guess about a bucket nobody can read, moves only once per run, and the
|
|
38
|
+
* bucket announces the answer exactly — see the docblock in `credentials.ts`.
|
|
40
39
|
*
|
|
41
40
|
* And it does not, by default, **bound exfiltration**. The container holds the
|
|
42
41
|
* checkout, and with no restriction configured it can send it anywhere. That is
|
|
@@ -241,8 +240,8 @@ export function claudeCodeEgress(config) {
|
|
|
241
240
|
* The swap, and it is the whole point of the file.
|
|
242
241
|
*
|
|
243
242
|
* `authorization` is set and `x-api-key` removed because that is the shape
|
|
244
|
-
* the sanctioned client sends
|
|
245
|
-
*
|
|
243
|
+
* the sanctioned client sends: `authorization: Bearer …` with no
|
|
244
|
+
* `x-api-key` at all. Everything else is forwarded
|
|
246
245
|
* untouched: the `anthropic-beta` list (which carries
|
|
247
246
|
* `claude-code-20250219` and `oauth-2025-04-20`, almost certainly part of
|
|
248
247
|
* what marks the request as coming from the client) and the `user-agent`
|
|
@@ -278,10 +277,10 @@ export function claudeCodeEgress(config) {
|
|
|
278
277
|
/**
|
|
279
278
|
* Capture the whole thing, every time.
|
|
280
279
|
*
|
|
281
|
-
* **No genuine subscription-exhaustion `429` has
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
280
|
+
* **No genuine subscription-exhaustion `429` has been observed through this
|
|
281
|
+
* path** — a raw-API refusal is a different response — so the rules in
|
|
282
|
+
* `readRefusal` are inferences from the documented API rate limits. This log
|
|
283
|
+
* is the instrument that replaces
|
|
285
284
|
* them with a fact, and it costs nothing: these statuses are rare, and the
|
|
286
285
|
* body of one is a few hundred bytes.
|
|
287
286
|
*/
|
|
@@ -32,10 +32,46 @@ export interface ClaudeCodeUsage {
|
|
|
32
32
|
cacheRead: number;
|
|
33
33
|
cacheWrite: number;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* What the client knows about the subscription bucket it is drawing on.
|
|
37
|
+
*
|
|
38
|
+
* The one thing on this stream that is about the *deployment* rather than the
|
|
39
|
+
* run. This plugin's entry point describes the 5-hour and weekly limits as
|
|
40
|
+
* routed around rather than predicted, because the gateway only learns a bucket
|
|
41
|
+
* is empty when Anthropic refuses a request — this is the client saying so in
|
|
42
|
+
* advance, on every session.
|
|
43
|
+
*
|
|
44
|
+
* `resetsAt` is **seconds**, not milliseconds, which is the one field here it is
|
|
45
|
+
* possible to get silently wrong: a value handed to `Date` unconverted lands in
|
|
46
|
+
* January 1970, and a credential marked spent until then reads as usable.
|
|
47
|
+
*/
|
|
48
|
+
export interface RateLimitInfo {
|
|
49
|
+
/**
|
|
50
|
+
* {@link RATE_LIMIT_OK} on a healthy bucket. Any other value is
|
|
51
|
+
* **unclassified** — no consumer should read one as exhaustion.
|
|
52
|
+
*/
|
|
53
|
+
status: string;
|
|
54
|
+
/** Unix **seconds** at which this bucket refills. */
|
|
55
|
+
resetsAt?: number;
|
|
56
|
+
/** Which bucket — `five_hour`, and the weekly one. */
|
|
57
|
+
rateLimitType?: string;
|
|
58
|
+
/** Whether spend beyond the bucket is permitted, and why not. */
|
|
59
|
+
overageStatus?: string;
|
|
60
|
+
overageDisabledReason?: string;
|
|
61
|
+
}
|
|
35
62
|
export type ClaudeCodeEvent = {
|
|
36
63
|
kind: "init";
|
|
37
64
|
sessionId: string;
|
|
38
65
|
model?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The subscription bucket, as the client sees it. Carried rather than acted
|
|
69
|
+
* on here — {@link describe} decides what a reader is told, and the caller
|
|
70
|
+
* decides what the credential pool is told.
|
|
71
|
+
*/
|
|
72
|
+
| {
|
|
73
|
+
kind: "rateLimit";
|
|
74
|
+
info: RateLimitInfo;
|
|
39
75
|
} | {
|
|
40
76
|
kind: "assistant";
|
|
41
77
|
text: string;
|
|
@@ -104,6 +140,17 @@ export interface ParsedStream {
|
|
|
104
140
|
* `result` and `init` are never nested and are read unconditionally.
|
|
105
141
|
*/
|
|
106
142
|
export declare function parseStream(buffer: string): ParsedStream;
|
|
143
|
+
/**
|
|
144
|
+
* The status a healthy bucket reports, and the only one ever observed.
|
|
145
|
+
*
|
|
146
|
+
* **Every other value is unclassified, not a refusal.** Nothing here knows what
|
|
147
|
+
* a non-`allowed` status means, and the one place that decides whether a
|
|
148
|
+
* credential is spent — `readRateLimitEvent` in this package's `credentials.ts`
|
|
149
|
+
* — recognises none of them, deliberately: retiring a working credential on a
|
|
150
|
+
* guess is the expensive half of that trade. What an unrecognised status earns
|
|
151
|
+
* is a note and a log, so somebody can name it.
|
|
152
|
+
*/
|
|
153
|
+
export declare const RATE_LIMIT_OK = "allowed";
|
|
107
154
|
/**
|
|
108
155
|
* Turn events into progress notes the parent can post.
|
|
109
156
|
*
|