@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
|
@@ -193,12 +193,83 @@ export function parseStream(buffer) {
|
|
|
193
193
|
events.push({ kind: "result", result: readResult(event) });
|
|
194
194
|
break;
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* A top-level `type`, not a `system` subtype, so it is handled here and
|
|
198
|
+
* not in the subtype switch above.
|
|
199
|
+
*
|
|
200
|
+
* The placement is load-bearing rather than tidy: everything this switch
|
|
201
|
+
* does not name falls to `default`, which counts it as an unrecognised
|
|
202
|
+
* line and warns. A recognised shape parsed in the wrong place is
|
|
203
|
+
* therefore indistinguishable from a schema that moved — the drain reports
|
|
204
|
+
* a steady `skipped` on every session, and the reading it is dropping is
|
|
205
|
+
* the only free view of the subscription bucket anything here gets.
|
|
206
|
+
*/
|
|
207
|
+
case "rate_limit_event": {
|
|
208
|
+
const info = readRateLimit(event);
|
|
209
|
+
if (!info) {
|
|
210
|
+
skip(trimmed);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
events.push({ kind: "rateLimit", info });
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
196
216
|
default:
|
|
197
217
|
skip(trimmed);
|
|
198
218
|
}
|
|
199
219
|
}
|
|
200
220
|
return { events, carry, skipped, nested, ...(sample ? { sample } : {}) };
|
|
201
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Read a `rate_limit_event` line, or nothing when it carries no status.
|
|
224
|
+
*
|
|
225
|
+
* `status` is the only required field: it is what decides whether anything is
|
|
226
|
+
* wrong, and a line without it says nothing this package can use — so it is
|
|
227
|
+
* skipped and sampled like any other unrecognised shape, rather than recorded as
|
|
228
|
+
* a bucket in an unknown state.
|
|
229
|
+
*/
|
|
230
|
+
function readRateLimit(event) {
|
|
231
|
+
const info = asRecord(event.rate_limit_info);
|
|
232
|
+
const status = info && str(info.status);
|
|
233
|
+
if (!info || !status)
|
|
234
|
+
return undefined;
|
|
235
|
+
const resetsAt = info.resetsAt;
|
|
236
|
+
return {
|
|
237
|
+
status,
|
|
238
|
+
// Finite is not enough. `1e308` is finite, and seconds past the `Date` range
|
|
239
|
+
// make `toISOString()` throw — inside `describe`, inside the drain's parse
|
|
240
|
+
// loop, which would take a whole session down over one malformed vendor
|
|
241
|
+
// field. The contract everywhere else here is to drop a bad line, not to
|
|
242
|
+
// raise, so the check is "does this name a moment" rather than "is this a
|
|
243
|
+
// number".
|
|
244
|
+
...(typeof resetsAt === "number" && isRealDate(resetsAt * 1000)
|
|
245
|
+
? { resetsAt }
|
|
246
|
+
: {}),
|
|
247
|
+
...(str(info.rateLimitType)
|
|
248
|
+
? { rateLimitType: str(info.rateLimitType) }
|
|
249
|
+
: {}),
|
|
250
|
+
...(str(info.overageStatus)
|
|
251
|
+
? { overageStatus: str(info.overageStatus) }
|
|
252
|
+
: {}),
|
|
253
|
+
...(str(info.overageDisabledReason)
|
|
254
|
+
? { overageDisabledReason: str(info.overageDisabledReason) }
|
|
255
|
+
: {})
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** Whether a millisecond value is one `Date` can actually represent. */
|
|
259
|
+
function isRealDate(ms) {
|
|
260
|
+
return Number.isFinite(ms) && !Number.isNaN(new Date(ms).getTime());
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The status a healthy bucket reports, and the only one ever observed.
|
|
264
|
+
*
|
|
265
|
+
* **Every other value is unclassified, not a refusal.** Nothing here knows what
|
|
266
|
+
* a non-`allowed` status means, and the one place that decides whether a
|
|
267
|
+
* credential is spent — `readRateLimitEvent` in this package's `credentials.ts`
|
|
268
|
+
* — recognises none of them, deliberately: retiring a working credential on a
|
|
269
|
+
* guess is the expensive half of that trade. What an unrecognised status earns
|
|
270
|
+
* is a note and a log, so somebody can name it.
|
|
271
|
+
*/
|
|
272
|
+
export const RATE_LIMIT_OK = "allowed";
|
|
202
273
|
/** Read a `result` line. Total by construction — a missing field reads as zero. */
|
|
203
274
|
function readResult(event) {
|
|
204
275
|
const apiErrorStatus = event.api_error_status;
|
|
@@ -222,15 +293,11 @@ function readResult(event) {
|
|
|
222
293
|
permissionDenials: denials
|
|
223
294
|
};
|
|
224
295
|
}
|
|
225
|
-
/** How much of one progress note the parent is shown. */
|
|
226
|
-
const PROGRESS_MAX_CHARS = 240;
|
|
227
296
|
/**
|
|
228
297
|
* How much of an unrecognised line is kept for the logs.
|
|
229
298
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* whether a schema moved. The discriminating part of a stream-json line is its
|
|
233
|
-
* `type` and `subtype`, both of which are at the front.
|
|
299
|
+
* Read by a person deciding whether a schema moved, and the discriminating part
|
|
300
|
+
* of a stream-json line is its `type` and `subtype`, both at the front.
|
|
234
301
|
*/
|
|
235
302
|
const SAMPLE_MAX_CHARS = 200;
|
|
236
303
|
/**
|
|
@@ -273,8 +340,10 @@ function describe(event) {
|
|
|
273
340
|
// Deliberately narrower than silencing the whole channel: `denied` and
|
|
274
341
|
// `retry` below stay, because they are the only evidence of a session that
|
|
275
342
|
// is being refused or throttled.
|
|
343
|
+
//
|
|
344
|
+
// Never clipped. A plan or a report cut mid-sentence reads as complete.
|
|
276
345
|
case "assistant":
|
|
277
|
-
return event.text
|
|
346
|
+
return event.text || undefined;
|
|
278
347
|
case "denied":
|
|
279
348
|
// The one progress note that is more useful than the session's own
|
|
280
349
|
// account of itself. A refused tool call is invisible in the transcript —
|
|
@@ -282,12 +351,29 @@ function describe(event) {
|
|
|
282
351
|
// parent sees a subagent being resourceful and never learns it was fenced
|
|
283
352
|
// in. Named tool first, because that is what distinguishes a broken
|
|
284
353
|
// configuration from a deny rule doing its job.
|
|
285
|
-
return `permission denied${event.tool ? ` for ${event.tool}` : ""}: ${
|
|
354
|
+
return `permission denied${event.tool ? ` for ${event.tool}` : ""}: ${event.reason}`;
|
|
286
355
|
case "retry":
|
|
287
356
|
// Surfaced deliberately. This is what a budget refusal from the egress
|
|
288
357
|
// gateway looks like from inside the container, and a run that ends
|
|
289
358
|
// shortly afterwards is explained by it.
|
|
290
|
-
return `retrying the model call: ${
|
|
359
|
+
return `retrying the model call: ${event.detail}`;
|
|
360
|
+
case "rateLimit":
|
|
361
|
+
/**
|
|
362
|
+
* Silent while the bucket is fine, which is every line of a healthy
|
|
363
|
+
* session — one note per `rate_limit_event` would be the noisiest thing on
|
|
364
|
+
* the stream and would say "allowed" each time.
|
|
365
|
+
*
|
|
366
|
+
* The test is inverted deliberately: an unrecognised status produces a
|
|
367
|
+
* note rather than silence. The statuses are a vendor's and only
|
|
368
|
+
* {@link RATE_LIMIT_OK} has been seen, so a new one meaning "nearly out"
|
|
369
|
+
* should surface as prose somebody reads, not be assumed benign.
|
|
370
|
+
*/
|
|
371
|
+
return event.info.status === RATE_LIMIT_OK
|
|
372
|
+
? undefined
|
|
373
|
+
: `the ${event.info.rateLimitType ?? "subscription"} limit reports "${event.info.status}"` +
|
|
374
|
+
(event.info.resetsAt === undefined
|
|
375
|
+
? ""
|
|
376
|
+
: ` until ${new Date(event.info.resetsAt * 1000).toISOString()}`);
|
|
291
377
|
// `init` names a session id nobody outside this package can use, and
|
|
292
378
|
// `result` is the terminal outcome the caller reports itself.
|
|
293
379
|
case "init":
|
|
@@ -295,6 +381,6 @@ function describe(event) {
|
|
|
295
381
|
return undefined;
|
|
296
382
|
}
|
|
297
383
|
}
|
|
298
|
-
function clip(text, max
|
|
384
|
+
function clip(text, max) {
|
|
299
385
|
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
300
386
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentPlugin } from "@dynamicagents/core";
|
|
2
2
|
import type { CredentialStore, Lead } from "./credentials.js";
|
|
3
|
-
import { type DrainCursor, type DrainOutcome, type SessionRuntime } from "./run.js";
|
|
3
|
+
import { type DrainCursor, type DrainOutcome, type DrainOptions, type SessionRuntime } from "./run.js";
|
|
4
4
|
import { type ClaudeCodeConfig } from "./config.js";
|
|
5
5
|
/**
|
|
6
6
|
* `@dynamicagents/plugins/claude-code` — subtasks that run the Claude Code CLI.
|
|
@@ -36,10 +36,9 @@ import { type ClaudeCodeConfig } from "./config.js";
|
|
|
36
36
|
*
|
|
37
37
|
* ## The 5-hour and weekly limits are routed around, not predicted
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* a cap on spend, only a gate on starting.
|
|
39
|
+
* **No budget gate, and one must not come back.** An estimate of spend is a
|
|
40
|
+
* guess about a bucket nobody can read, and it only moves when a run *ends* — so
|
|
41
|
+
* it caps nothing, it only refuses to start.
|
|
43
42
|
*
|
|
44
43
|
* The gateway sees Anthropic's actual response, which is the one place the
|
|
45
44
|
* bucket announces itself. So the credential is a **pool**: the first usable
|
|
@@ -71,9 +70,28 @@ export declare function claudeCodeSession(config: ClaudeCodeConfig): {
|
|
|
71
70
|
* two sessions in one workspace under one id would spawn over each other —
|
|
72
71
|
* see {@link execIdFor}.
|
|
73
72
|
*/
|
|
74
|
-
start(runtime: SessionRuntime, subtaskId: string | number, prompt: string, dir: string): Promise<DrainOutcome>;
|
|
75
|
-
/**
|
|
76
|
-
|
|
73
|
+
start(runtime: SessionRuntime, subtaskId: string | number, prompt: string, dir: string, sinks?: Pick<DrainOptions, "onProgress" | "onCheckpoint">): Promise<DrainOutcome>;
|
|
74
|
+
/**
|
|
75
|
+
* Re-attach to a running session and drain one more window.
|
|
76
|
+
*
|
|
77
|
+
* **A session whose container was replaced is reported, not thrown.** The
|
|
78
|
+
* attachment is the first thing to notice a replacement, and the caller is a
|
|
79
|
+
* chunk loop with a cursor it will happily keep presenting: a throw here
|
|
80
|
+
* retries the same dead id until the chunk allowance runs out, and the run
|
|
81
|
+
* ends on a stack trace that names neither the session nor the cause. A
|
|
82
|
+
* terminal outcome ends it at the first attempt instead, with the exit code
|
|
83
|
+
* the runtime uses for a killed process and an explanation on `stderr`,
|
|
84
|
+
* which is exactly the channel a session that died without a result line
|
|
85
|
+
* already reports through.
|
|
86
|
+
*
|
|
87
|
+
* **What it must not say is that starting over is safe.** A session is an
|
|
88
|
+
* agent: by the time its container went, it may have written files that
|
|
89
|
+
* reached the workspace, committed, pushed, or called something outside
|
|
90
|
+
* altogether. None of that is visible from here — only that the attachment
|
|
91
|
+
* is gone — so the honest report is what is known plus where to look, and
|
|
92
|
+
* the decision belongs to whoever can read the durable state.
|
|
93
|
+
*/
|
|
94
|
+
resume(runtime: SessionRuntime, cursor: DrainCursor, sinks?: Pick<DrainOptions, "onProgress" | "onCheckpoint">): Promise<DrainOutcome>;
|
|
77
95
|
/** Stop a session — `SIGTERM`, so its own process tree goes with it. */
|
|
78
96
|
stop(runtime: SessionRuntime, subtaskId: string | number): Promise<void>;
|
|
79
97
|
/**
|
|
@@ -116,12 +134,12 @@ export type ClaudeCodeSession = ReturnType<typeof claudeCodeSession>;
|
|
|
116
134
|
export declare function claudeCode(config: ClaudeCodeConfig): AgentPlugin;
|
|
117
135
|
export { ANTHROPIC_HOST, claudeCodeEgress } from "./egress.js";
|
|
118
136
|
export type { EgressConfig } from "./egress.js";
|
|
119
|
-
export { credentialPool, readRefusal } from "./credentials.js";
|
|
137
|
+
export { credentialPool, readRefusal, readRateLimitEvent } from "./credentials.js";
|
|
120
138
|
export type { CredentialPool, CredentialPoolConfig, CredentialState, CredentialStore, Lead, Refusal } from "./credentials.js";
|
|
121
139
|
export { buildLaunch, CLAUDE_EXEC_PREFIX, CREDENTIAL_PLACEHOLDER, execIdFor, freshCursor } from "./run.js";
|
|
122
|
-
export type { DrainCursor, DrainOutcome, Launch, LaunchOptions, SessionRuntime } from "./run.js";
|
|
123
|
-
export { parseStream, toProgress } from "./events.js";
|
|
124
|
-
export type { ClaudeCodeEvent, ClaudeCodeResult, ClaudeCodeUsage } from "./events.js";
|
|
140
|
+
export type { DrainCursor, DrainOptions, DrainOutcome, Launch, LaunchOptions, SessionRuntime } from "./run.js";
|
|
141
|
+
export { parseStream, toProgress, RATE_LIMIT_OK } from "./events.js";
|
|
142
|
+
export type { ClaudeCodeEvent, ClaudeCodeResult, ClaudeCodeUsage, RateLimitInfo } from "./events.js";
|
|
125
143
|
export { CLAUDE_CODE_CAPABILITY, CLAUDE_CODE_RECIPE, CLAUDE_CODE_SPEC, CLAUDE_CODE_TYPE, WORKSPACE_RUNTIME_KEY } from "./recipe.js";
|
|
126
144
|
export { DEFAULT_PERMISSION_MODE, DEFAULT_TIMEOUT_MS, DEFAULT_WINDOW_MS } from "./config.js";
|
|
127
|
-
export type { ClaudeCodeConfig, PermissionMode } from "./config.js";
|
|
145
|
+
export type { ClaudeCodeConfig, EffortLevel, PermissionMode } from "./config.js";
|
|
@@ -53,7 +53,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
53
53
|
import { definePlugin, PLUGIN_CONTRACT_VERSION } from "@dynamicagents/core";
|
|
54
54
|
import { claudeCodeEgress } from "./egress.js";
|
|
55
55
|
import { credentialPool } from "./credentials.js";
|
|
56
|
-
import { attachRun, drainRun, execIdFor, freshCursor, killRun, startRun } from "./run.js";
|
|
56
|
+
import { attachRun, drainRun, execIdFor, freshCursor, isExecLost, killRun, startRun } from "./run.js";
|
|
57
57
|
import { DEFAULT_TIMEOUT_MS, DEFAULT_WINDOW_MS } from "./config.js";
|
|
58
58
|
import { CLAUDE_CODE_SPEC, CLAUDE_CODE_TYPE, WORKSPACE_RUNTIME_KEY } from "./recipe.js";
|
|
59
59
|
/**
|
|
@@ -90,10 +90,9 @@ import { CLAUDE_CODE_SPEC, CLAUDE_CODE_TYPE, WORKSPACE_RUNTIME_KEY } from "./rec
|
|
|
90
90
|
*
|
|
91
91
|
* ## The 5-hour and weekly limits are routed around, not predicted
|
|
92
92
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* a cap on spend, only a gate on starting.
|
|
93
|
+
* **No budget gate, and one must not come back.** An estimate of spend is a
|
|
94
|
+
* guess about a bucket nobody can read, and it only moves when a run *ends* — so
|
|
95
|
+
* it caps nothing, it only refuses to start.
|
|
97
96
|
*
|
|
98
97
|
* The gateway sees Anthropic's actual response, which is the one place the
|
|
99
98
|
* bucket announces itself. So the credential is a **pool**: the first usable
|
|
@@ -124,7 +123,7 @@ export function claudeCodeSession(config) {
|
|
|
124
123
|
prompt,
|
|
125
124
|
dir,
|
|
126
125
|
...(config.model ? { model: config.model } : {}),
|
|
127
|
-
...(config.
|
|
126
|
+
...(config.effort ? { effort: config.effort } : {}),
|
|
128
127
|
...(config.maxSubagentDepth === undefined
|
|
129
128
|
? {}
|
|
130
129
|
: { maxSubagentDepth: config.maxSubagentDepth }),
|
|
@@ -134,7 +133,8 @@ export function claudeCodeSession(config) {
|
|
|
134
133
|
// Forwarded only when set, so `buildLaunch` owns the default in one place
|
|
135
134
|
// rather than this line resolving it and the flag being written twice.
|
|
136
135
|
...(config.permissionMode ? { permissionMode: config.permissionMode } : {}),
|
|
137
|
-
...(config.env ? { env: config.env } : {})
|
|
136
|
+
...(config.env ? { env: config.env } : {}),
|
|
137
|
+
...(config.author ? { author: config.author } : {})
|
|
138
138
|
});
|
|
139
139
|
return {
|
|
140
140
|
/**
|
|
@@ -144,7 +144,7 @@ export function claudeCodeSession(config) {
|
|
|
144
144
|
* two sessions in one workspace under one id would spawn over each other —
|
|
145
145
|
* see {@link execIdFor}.
|
|
146
146
|
*/
|
|
147
|
-
async start(runtime, subtaskId, prompt, dir) {
|
|
147
|
+
async start(runtime, subtaskId, prompt, dir, sinks = {}) {
|
|
148
148
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
149
149
|
try {
|
|
150
150
|
const execId = execIdFor(subtaskId);
|
|
@@ -154,7 +154,10 @@ export function claudeCodeSession(config) {
|
|
|
154
154
|
execId,
|
|
155
155
|
timeoutMs
|
|
156
156
|
}), false);
|
|
157
|
-
return await drainRun(handle, freshCursor(execId), {
|
|
157
|
+
return await drainRun(handle, freshCursor(execId), {
|
|
158
|
+
windowMs,
|
|
159
|
+
...sinks
|
|
160
|
+
});
|
|
158
161
|
}
|
|
159
162
|
catch (e_1) {
|
|
160
163
|
env_1.error = e_1;
|
|
@@ -164,12 +167,56 @@ export function claudeCodeSession(config) {
|
|
|
164
167
|
__disposeResources(env_1);
|
|
165
168
|
}
|
|
166
169
|
},
|
|
167
|
-
/**
|
|
168
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Re-attach to a running session and drain one more window.
|
|
172
|
+
*
|
|
173
|
+
* **A session whose container was replaced is reported, not thrown.** The
|
|
174
|
+
* attachment is the first thing to notice a replacement, and the caller is a
|
|
175
|
+
* chunk loop with a cursor it will happily keep presenting: a throw here
|
|
176
|
+
* retries the same dead id until the chunk allowance runs out, and the run
|
|
177
|
+
* ends on a stack trace that names neither the session nor the cause. A
|
|
178
|
+
* terminal outcome ends it at the first attempt instead, with the exit code
|
|
179
|
+
* the runtime uses for a killed process and an explanation on `stderr`,
|
|
180
|
+
* which is exactly the channel a session that died without a result line
|
|
181
|
+
* already reports through.
|
|
182
|
+
*
|
|
183
|
+
* **What it must not say is that starting over is safe.** A session is an
|
|
184
|
+
* agent: by the time its container went, it may have written files that
|
|
185
|
+
* reached the workspace, committed, pushed, or called something outside
|
|
186
|
+
* altogether. None of that is visible from here — only that the attachment
|
|
187
|
+
* is gone — so the honest report is what is known plus where to look, and
|
|
188
|
+
* the decision belongs to whoever can read the durable state.
|
|
189
|
+
*/
|
|
190
|
+
async resume(runtime, cursor, sinks = {}) {
|
|
169
191
|
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
170
192
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
193
|
+
let handle;
|
|
194
|
+
try {
|
|
195
|
+
handle = await attachRun(runtime, cursor);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
if (!isExecLost(err))
|
|
199
|
+
throw err;
|
|
200
|
+
console.warn("[claude-code] the session's container was replaced", {
|
|
201
|
+
execId: cursor.execId,
|
|
202
|
+
err: String(err)
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
done: true,
|
|
206
|
+
cursor,
|
|
207
|
+
progress: [],
|
|
208
|
+
exitCode: -1,
|
|
209
|
+
stderr: "the container holding this session was replaced, so the session " +
|
|
210
|
+
"was lost before it finished and cannot be re-attached. What it had " +
|
|
211
|
+
"already written to the workspace is still there, and anything it " +
|
|
212
|
+
"did outside the workspace — a commit, a push, a request — has " +
|
|
213
|
+
"already happened. Check the workspace and the branch before " +
|
|
214
|
+
"starting this subtask again, since a rerun repeats from the " +
|
|
215
|
+
"beginning."
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const session = __addDisposableResource(env_2, handle, false);
|
|
219
|
+
return await drainRun(session, cursor, { windowMs, ...sinks });
|
|
173
220
|
}
|
|
174
221
|
catch (e_2) {
|
|
175
222
|
env_2.error = e_2;
|
|
@@ -272,8 +319,8 @@ export function claudeCode(config) {
|
|
|
272
319
|
});
|
|
273
320
|
}
|
|
274
321
|
export { ANTHROPIC_HOST, claudeCodeEgress } from "./egress.js";
|
|
275
|
-
export { credentialPool, readRefusal } from "./credentials.js";
|
|
322
|
+
export { credentialPool, readRefusal, readRateLimitEvent } from "./credentials.js";
|
|
276
323
|
export { buildLaunch, CLAUDE_EXEC_PREFIX, CREDENTIAL_PLACEHOLDER, execIdFor, freshCursor } from "./run.js";
|
|
277
|
-
export { parseStream, toProgress } from "./events.js";
|
|
324
|
+
export { parseStream, toProgress, RATE_LIMIT_OK } from "./events.js";
|
|
278
325
|
export { CLAUDE_CODE_CAPABILITY, CLAUDE_CODE_RECIPE, CLAUDE_CODE_SPEC, CLAUDE_CODE_TYPE, WORKSPACE_RUNTIME_KEY } from "./recipe.js";
|
|
279
326
|
export { DEFAULT_PERMISSION_MODE, DEFAULT_TIMEOUT_MS, DEFAULT_WINDOW_MS } from "./config.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { WorkspaceRuntimeExecHandle, WorkspaceRuntimeExecOptions, WorkspaceRuntimeGetOptions, WorkspaceRuntimeKillOptions } from "@cloudflare/computer";
|
|
2
2
|
import type { ProgressEvent } from "@dynamicagents/core/subtasks";
|
|
3
|
-
import { type ClaudeCodeResult } from "./events.js";
|
|
4
|
-
import { type PermissionMode } from "./config.js";
|
|
3
|
+
import { type ClaudeCodeResult, type RateLimitInfo } from "./events.js";
|
|
4
|
+
import { type EffortLevel, type PermissionMode } from "./config.js";
|
|
5
5
|
/**
|
|
6
6
|
* Launching Claude Code in the workspace container, and draining it in windows.
|
|
7
7
|
*
|
|
@@ -16,9 +16,8 @@ import { type PermissionMode } from "./config.js";
|
|
|
16
16
|
*
|
|
17
17
|
* The run is spawned under an exec id and left running. Each chunk re-attaches,
|
|
18
18
|
* drains for a bounded window, and returns. That shape is not a preference: a
|
|
19
|
-
* drain owned by an RPC that returns in milliseconds gets disposed mid-command
|
|
20
|
-
*
|
|
21
|
-
* `npm ci`.
|
|
19
|
+
* drain owned by an RPC that returns in milliseconds gets disposed mid-command
|
|
20
|
+
* — the same way a dependency install dies halfway through `npm ci`.
|
|
22
21
|
*
|
|
23
22
|
* ## The cursor
|
|
24
23
|
*
|
|
@@ -69,11 +68,10 @@ export declare function execIdFor(subtaskId: string | number): string;
|
|
|
69
68
|
/**
|
|
70
69
|
* What the container is given instead of a credential.
|
|
71
70
|
*
|
|
72
|
-
* Claude Code does not validate it locally
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* environment learns this and nothing else.
|
|
71
|
+
* Claude Code does not validate it locally: a run with this exact value
|
|
72
|
+
* succeeds, and the container only ever sends the placeholder. The egress
|
|
73
|
+
* gateway swaps in the real credential on the way out, so a `postinstall` script
|
|
74
|
+
* that dumps the environment learns this and nothing else.
|
|
77
75
|
*
|
|
78
76
|
* Shaped like a real token deliberately: something that looks obviously fake
|
|
79
77
|
* invites a future reader to "fix" it by putting the real one there.
|
|
@@ -85,16 +83,16 @@ export interface LaunchOptions {
|
|
|
85
83
|
/** Where the checkout is. The session runs with this as its cwd. */
|
|
86
84
|
dir: string;
|
|
87
85
|
model?: string;
|
|
88
|
-
/** Ceiling on the *outer* session's turns. Advisory; the budget gate is not. */
|
|
89
|
-
maxTurns?: number;
|
|
90
86
|
/**
|
|
91
|
-
*
|
|
87
|
+
* How hard the model thinks, per turn. Unset, the model's own default.
|
|
92
88
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
* See {@link file://./config.ts ClaudeCodeConfig.effort} for what a level
|
|
90
|
+
* costs, and why an unrecognised one is worse here than a rejected one.
|
|
91
|
+
*/
|
|
92
|
+
effort?: EffortLevel;
|
|
93
|
+
/**
|
|
94
|
+
* Caps on Claude Code's own subagent tree — advisory, for the reason
|
|
95
|
+
* {@link file://./config.ts ClaudeCodeConfig.maxSubagentDepth} gives.
|
|
98
96
|
*/
|
|
99
97
|
maxSubagentDepth?: number;
|
|
100
98
|
maxConcurrentSubagents?: number;
|
|
@@ -114,6 +112,14 @@ export interface LaunchOptions {
|
|
|
114
112
|
* than trusted to the reader of that sentence.
|
|
115
113
|
*/
|
|
116
114
|
env?: Record<string, string>;
|
|
115
|
+
/**
|
|
116
|
+
* Who this session's commits are attributed to — see
|
|
117
|
+
* {@link file://./config.ts ClaudeCodeConfig.author}.
|
|
118
|
+
*/
|
|
119
|
+
author?: {
|
|
120
|
+
name: string;
|
|
121
|
+
email: string;
|
|
122
|
+
};
|
|
117
123
|
}
|
|
118
124
|
export interface Launch {
|
|
119
125
|
command: string;
|
|
@@ -133,27 +139,20 @@ export interface Launch {
|
|
|
133
139
|
* container is already an arbitrary-code-execution environment by design — the
|
|
134
140
|
* install runs the repo's `postinstall`, the agent runs its test suite. Stripping
|
|
135
141
|
* one door while the others stand open buys nothing and costs the agent its
|
|
136
|
-
* context.
|
|
142
|
+
* context.
|
|
137
143
|
*
|
|
138
|
-
* No `ANTHROPIC_BASE_URL` either
|
|
139
|
-
* the
|
|
140
|
-
*
|
|
141
|
-
*
|
|
144
|
+
* No `ANTHROPIC_BASE_URL` either: `http-gateway` egress intercepts
|
|
145
|
+
* transparently, so the client talks to the real hostname and the gateway sees
|
|
146
|
+
* it. Nothing has to be told to use a proxy, which means nothing in the
|
|
147
|
+
* container can be told *not* to.
|
|
142
148
|
*
|
|
143
149
|
* ## `--permission-mode`, and the root guard behind it
|
|
144
150
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* like considered reluctance rather than a blocked tool. That cost this
|
|
151
|
-
* deployment a day of "the container is fixed but nothing lands".
|
|
152
|
-
*
|
|
153
|
-
* So the mode is passed explicitly, and {@link DEFAULT_PERMISSION_MODE} is
|
|
154
|
-
* `bypassPermissions` — see
|
|
155
|
-
* {@link file://./config.ts ClaudeCodeConfig.permissionMode} for why the
|
|
156
|
-
* permissive value is the correct default here rather than a concession.
|
|
151
|
+
* The mode is always passed explicitly, never left to the CLI's `default` — see
|
|
152
|
+
* {@link file://./config.ts ClaudeCodeConfig.permissionMode} for what an
|
|
153
|
+
* unset mode does to a headless session, and why
|
|
154
|
+
* {@link DEFAULT_PERMISSION_MODE} is the permissive value rather than a
|
|
155
|
+
* concession.
|
|
157
156
|
*
|
|
158
157
|
* **`IS_SANDBOX=1` ships in the same branch as the flag, and separating the two
|
|
159
158
|
* breaks a session harder than passing no flag at all.** The container runs as
|
|
@@ -221,10 +220,22 @@ export type DrainOutcome = {
|
|
|
221
220
|
done: false;
|
|
222
221
|
cursor: DrainCursor;
|
|
223
222
|
progress: ProgressEvent[];
|
|
223
|
+
/**
|
|
224
|
+
* The bucket as the client reported it **during this window**, if it did.
|
|
225
|
+
*
|
|
226
|
+
* Deliberately not carried on the cursor the way `result` is. A caller
|
|
227
|
+
* acts on this against whichever credential is leading *now*, so a reading
|
|
228
|
+
* replayed on every later chunk would let one window's observation retire
|
|
229
|
+
* a credential that was not even in use when it was taken. Absent means
|
|
230
|
+
* "this window learned nothing", which is the honest answer.
|
|
231
|
+
*/
|
|
232
|
+
rateLimit?: RateLimitInfo;
|
|
224
233
|
} | {
|
|
225
234
|
done: true;
|
|
226
235
|
cursor: DrainCursor;
|
|
227
236
|
progress: ProgressEvent[];
|
|
237
|
+
/** The bucket as reported **in this window**. See the `done: false` arm. */
|
|
238
|
+
rateLimit?: RateLimitInfo;
|
|
228
239
|
exitCode: number;
|
|
229
240
|
/** Absent when the process died without ever emitting a `result` line. */
|
|
230
241
|
result?: ClaudeCodeResult;
|
|
@@ -239,14 +250,69 @@ export interface DrainOptions {
|
|
|
239
250
|
/**
|
|
240
251
|
* How long this chunk may block before checkpointing and yielding.
|
|
241
252
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* the
|
|
245
|
-
*
|
|
253
|
+
* **This is the coder path's soft limit**, justified the same way core's
|
|
254
|
+
* `CHUNK_SOFT_MS` is — by the step timeout — but checked as a deadline on
|
|
255
|
+
* reading the session's stdout stream rather than between turns, because a
|
|
256
|
+
* `claude -p` session runs its own loop inside the container and there are no
|
|
257
|
+
* core-visible turns to stop between. The stream is the only synchronisation
|
|
258
|
+
* point there is. {@link file://./config.ts ClaudeCodeConfig.windowMs} holds
|
|
259
|
+
* how the default is sized against that timeout, what the headroom it leaves
|
|
260
|
+
* is for, and why that headroom is an expectation rather than a guarantee.
|
|
261
|
+
*
|
|
262
|
+
* It is also what stops a run burning its whole chunk allowance in seconds: a
|
|
263
|
+
* drain that returned the moment it had nothing to read would exhaust
|
|
264
|
+
* `MAX_CHUNKS_PER_BRANCH` before the session finished thinking.
|
|
265
|
+
*
|
|
266
|
+
* **It is not the reporting interval**, and reading it as one is the mistake
|
|
267
|
+
* {@link DrainOptions.onProgress} exists to remove: a session that finishes
|
|
268
|
+
* inside a single window reaches no boundary at all, so a caller with nothing
|
|
269
|
+
* but the outcome learns everything at once, once the work is over.
|
|
246
270
|
*/
|
|
247
271
|
windowMs: number;
|
|
272
|
+
/**
|
|
273
|
+
* Called with each note as it is parsed, rather than with all of them when the
|
|
274
|
+
* window ends.
|
|
275
|
+
*
|
|
276
|
+
* Notes are still returned on the outcome as well, so a caller that posts from
|
|
277
|
+
* here must drop what it is handed back, or the same note is posted twice. The
|
|
278
|
+
* keys are positional, so the gatekeeper would dedupe it, but paying for the
|
|
279
|
+
* second post to be discarded is not a plan.
|
|
280
|
+
*
|
|
281
|
+
* **Never awaited inside the read loop.** A post is a signed round trip to the
|
|
282
|
+
* gatekeeper — measured at ~700 ms — and awaiting one per note would stall
|
|
283
|
+
* reading the container's stream for as long as the session is talkative.
|
|
284
|
+
* Calls are chained instead, so they stay in order, and the chain is settled
|
|
285
|
+
* before the drain returns.
|
|
286
|
+
*/
|
|
287
|
+
onProgress?: (event: ProgressEvent) => void | Promise<void>;
|
|
288
|
+
/**
|
|
289
|
+
* Called with a cursor that is safe to persist, no more often than
|
|
290
|
+
* {@link CHECKPOINT_MIN_MS}.
|
|
291
|
+
*
|
|
292
|
+
* **Only meaningful alongside `onProgress`, and that coupling is the whole
|
|
293
|
+
* point.** A caller normally commits the cursor after the drain returns,
|
|
294
|
+
* because a cursor written ahead of consuming events would skip events a retry
|
|
295
|
+
* never saw. A cursor offered here names a position whose notes have *already
|
|
296
|
+
* been handed to the sink*, so resuming from it loses nothing a person saw —
|
|
297
|
+
* which is only true because the sink posted them.
|
|
298
|
+
*
|
|
299
|
+
* Without it, a chunk that dies mid-window resumes from the last committed
|
|
300
|
+
* position, which is wherever the previous window ended. One production run
|
|
301
|
+
* lost six and a half minutes of a session that way and re-derived it by
|
|
302
|
+
* replaying the whole stream.
|
|
303
|
+
*/
|
|
304
|
+
onCheckpoint?: (cursor: DrainCursor) => void | Promise<void>;
|
|
248
305
|
now?: () => number;
|
|
249
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Whether a thrown value means the session this cursor names is gone for good.
|
|
309
|
+
*
|
|
310
|
+
* `EEXEC_LOST` is the runtime saying the container that held the execution was
|
|
311
|
+
* replaced. The session died with it, and no attachment will ever find it — so
|
|
312
|
+
* unlike `EEXEC_BUSY`, which is a live session reached the wrong way, there is
|
|
313
|
+
* nothing to recover and retrying only spends chunks discovering that again.
|
|
314
|
+
*/
|
|
315
|
+
export declare function isExecLost(err: unknown): boolean;
|
|
250
316
|
/**
|
|
251
317
|
* Start a session, detached.
|
|
252
318
|
*
|
|
@@ -257,13 +323,41 @@ export interface DrainOptions {
|
|
|
257
323
|
* still running (`EEXEC_BUSY`). Without this the retry throws, every later retry
|
|
258
324
|
* throws the same way, and a perfectly healthy session in the container becomes
|
|
259
325
|
* unreachable.
|
|
326
|
+
*
|
|
327
|
+
* **The sync stays on the default, and `defer` is not an option here.** A
|
|
328
|
+
* deferred exec skips the post-command pull entirely and settles the outcome
|
|
329
|
+
* from the cursor instead — and its `cancel` reads the stream to the end first,
|
|
330
|
+
* so ending a window would block until the session exits rather than yielding
|
|
331
|
+
* the chunk. Both halves are exactly what this drain is built not to do.
|
|
260
332
|
*/
|
|
261
333
|
export declare function startRun(runtime: SessionRuntime, options: LaunchOptions & {
|
|
262
334
|
execId: string;
|
|
263
335
|
timeoutMs: number;
|
|
264
336
|
}): Promise<WorkspaceRuntimeExecHandle<"utf8">>;
|
|
265
|
-
/**
|
|
266
|
-
export
|
|
337
|
+
/** The clock and the wait, injectable so a spec can drive the bound. */
|
|
338
|
+
export interface AttachOptions {
|
|
339
|
+
now?: () => number;
|
|
340
|
+
wait?: (ms: number) => Promise<void>;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Re-attach to a session this isolate did not start.
|
|
344
|
+
*
|
|
345
|
+
* **Waits out a subscriber the previous window has not released yet**, and that
|
|
346
|
+
* wait is the point of this function rather than a refinement of it. The
|
|
347
|
+
* alternative is not "fail fast" — it is spending a Workflow retry to discover a
|
|
348
|
+
* condition that clears on its own in a second, when a chunk step's retries are
|
|
349
|
+
* what a deploy, a severed stub and a network drop have to be covered out of.
|
|
350
|
+
* Core sizes that budget and says why; what matters here is that this must not
|
|
351
|
+
* be charged to it. A deployment lost a twenty-one-minute session exactly that
|
|
352
|
+
* way — consecutive attach races used the retries up, and the redeploy that
|
|
353
|
+
* followed had no attempt left to be retried on. See {@link isExecSubscribed}.
|
|
354
|
+
*
|
|
355
|
+
* Everything else is rethrown on the first look, `EEXEC_LOST` above all: it is
|
|
356
|
+
* the runtime saying the container was replaced, `resume` turns it into a report
|
|
357
|
+
* for the model, and a retry loop would sit on it for {@link ATTACH_MAX_MS}
|
|
358
|
+
* learning nothing.
|
|
359
|
+
*/
|
|
360
|
+
export declare function attachRun(runtime: SessionRuntime, cursor: DrainCursor, options?: AttachOptions): Promise<WorkspaceRuntimeExecHandle<"utf8">>;
|
|
267
361
|
/**
|
|
268
362
|
* Drain a session for one bounded window.
|
|
269
363
|
*
|