@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
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import { initCard, resultCard, thinkingCard, toolCallCard, toolResultCard } from "./cards.js";
|
|
2
|
+
/**
|
|
3
|
+
* How far a thinking line's arrival may be from its own `timestamp` for the
|
|
4
|
+
* block to be timed.
|
|
5
|
+
*
|
|
6
|
+
* Both ends of a block are timed by when they arrive, which is only the truth
|
|
7
|
+
* while the drain reads live. A drain that re-attached after a cut is handed
|
|
8
|
+
* what was printed meanwhile in one burst, and its arrival times are the
|
|
9
|
+
* replay's — so a block whose line arrived far from when the CLI stamped it
|
|
10
|
+
* reads "Thought", with no time. Locally a line arrives 5–31 ms after its
|
|
11
|
+
* stamp; a re-attach has been measured at about a second. A container clock
|
|
12
|
+
* this far from the Worker's degrades every block to no time, rather than to a
|
|
13
|
+
* wrong one.
|
|
14
|
+
*/
|
|
15
|
+
export const LIVE_SLACK_MS = 5_000;
|
|
1
16
|
const EMPTY_USAGE = {
|
|
2
17
|
input: 0,
|
|
3
18
|
output: 0,
|
|
@@ -27,29 +42,86 @@ function readUsage(raw) {
|
|
|
27
42
|
};
|
|
28
43
|
}
|
|
29
44
|
/**
|
|
30
|
-
*
|
|
45
|
+
* An `assistant` message as events, one per block, in the order it said them.
|
|
31
46
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
47
|
+
* Per block rather than flattened, because the blocks are different things to
|
|
48
|
+
* a reader: prose, the model's reasoning, and each tool it called. A message
|
|
49
|
+
* that only calls tools is as much progress as one that narrates.
|
|
35
50
|
*/
|
|
36
|
-
function readAssistant(message) {
|
|
51
|
+
function readAssistant(message, thought) {
|
|
52
|
+
const messageId = str(message.id);
|
|
37
53
|
const content = Array.isArray(message.content)
|
|
38
54
|
? message.content
|
|
39
55
|
: [];
|
|
40
|
-
const
|
|
41
|
-
const tools = [];
|
|
56
|
+
const events = [];
|
|
42
57
|
for (const block of content) {
|
|
43
58
|
if (!asRecord(block))
|
|
44
59
|
continue;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
if (block.type === "text") {
|
|
61
|
+
// Trimmed only to ask whether it is empty: indentation is a code block's
|
|
62
|
+
// and trailing spaces a hard break's, so the event keeps it verbatim.
|
|
63
|
+
const text = str(block.text);
|
|
64
|
+
if (text?.trim())
|
|
65
|
+
events.push({ kind: "text", text });
|
|
66
|
+
}
|
|
67
|
+
else if (block.type === "thinking") {
|
|
68
|
+
events.push({
|
|
69
|
+
kind: "thinking",
|
|
70
|
+
...(messageId ? { messageId } : {}),
|
|
71
|
+
...(thought === undefined ? {} : { durationMs: thought })
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else if (block.type === "tool_use") {
|
|
75
|
+
const id = str(block.id);
|
|
76
|
+
const name = str(block.name);
|
|
77
|
+
if (id && name)
|
|
78
|
+
events.push({ kind: "toolUse", id, name, input: block.input });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return events;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A `user` line's tool results. Anything else a user line carries — the
|
|
85
|
+
* prompt echoed back, a hook's note — is not a tool's answer and says nothing.
|
|
86
|
+
*/
|
|
87
|
+
function readToolResults(event, message) {
|
|
88
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
89
|
+
const results = content.filter((block) => asRecord(block)?.type === "tool_result");
|
|
90
|
+
const structured = results.length === 1 ? event.tool_use_result : undefined;
|
|
91
|
+
const events = [];
|
|
92
|
+
for (const block of results) {
|
|
93
|
+
const id = str(block.tool_use_id);
|
|
94
|
+
if (!id)
|
|
95
|
+
continue;
|
|
96
|
+
events.push({
|
|
97
|
+
kind: "toolResult",
|
|
98
|
+
id,
|
|
99
|
+
isError: block.is_error === true,
|
|
100
|
+
content: resultText(block.content),
|
|
101
|
+
...(structured === undefined || structured === null ? {} : { structured })
|
|
102
|
+
});
|
|
51
103
|
}
|
|
52
|
-
return
|
|
104
|
+
return events;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A `tool_result` block's content as text. It is a string, or a list of
|
|
108
|
+
* blocks of which only the text ones are readable here — an image a tool
|
|
109
|
+
* returned is named, not carried.
|
|
110
|
+
*/
|
|
111
|
+
function resultText(content) {
|
|
112
|
+
if (typeof content === "string")
|
|
113
|
+
return content;
|
|
114
|
+
if (!Array.isArray(content))
|
|
115
|
+
return "";
|
|
116
|
+
return content
|
|
117
|
+
.map((block) => {
|
|
118
|
+
const record = asRecord(block);
|
|
119
|
+
if (record?.type === "text")
|
|
120
|
+
return str(record.text) ?? "";
|
|
121
|
+
return record?.type ? `[${String(record.type)}]` : "";
|
|
122
|
+
})
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join("\n");
|
|
53
125
|
}
|
|
54
126
|
/**
|
|
55
127
|
* Read a `system`/`permission_denied` line into something worth reporting.
|
|
@@ -87,8 +159,13 @@ function readDenial(event) {
|
|
|
87
159
|
* the filter.
|
|
88
160
|
*
|
|
89
161
|
* `result` and `init` are never nested and are read unconditionally.
|
|
162
|
+
*
|
|
163
|
+
* `clock` is when this read arrived, and the pending thinking block's start
|
|
164
|
+
* from the previous read — see {@link ParsedStream.thinkingFrom}. Without it no
|
|
165
|
+
* block is timed.
|
|
90
166
|
*/
|
|
91
|
-
export function parseStream(buffer) {
|
|
167
|
+
export function parseStream(buffer, clock) {
|
|
168
|
+
let thinkingFrom = clock?.thinkingFrom;
|
|
92
169
|
const events = [];
|
|
93
170
|
let skipped = 0;
|
|
94
171
|
let nested = 0;
|
|
@@ -131,6 +208,7 @@ export function parseStream(buffer) {
|
|
|
131
208
|
nested++;
|
|
132
209
|
continue;
|
|
133
210
|
}
|
|
211
|
+
const at = readTime(event.timestamp);
|
|
134
212
|
switch (event.type) {
|
|
135
213
|
case "system": {
|
|
136
214
|
if (event.subtype === "init") {
|
|
@@ -140,10 +218,14 @@ export function parseStream(buffer) {
|
|
|
140
218
|
break;
|
|
141
219
|
}
|
|
142
220
|
const model = str(event.model);
|
|
221
|
+
const cwd = str(event.cwd);
|
|
222
|
+
const permissionMode = str(event.permissionMode);
|
|
143
223
|
events.push({
|
|
144
224
|
kind: "init",
|
|
145
225
|
sessionId,
|
|
146
|
-
...(model ? { model } : {})
|
|
226
|
+
...(model ? { model } : {}),
|
|
227
|
+
...(cwd ? { cwd } : {}),
|
|
228
|
+
...(permissionMode ? { permissionMode } : {})
|
|
147
229
|
});
|
|
148
230
|
break;
|
|
149
231
|
}
|
|
@@ -151,6 +233,11 @@ export function parseStream(buffer) {
|
|
|
151
233
|
events.push(readDenial(event));
|
|
152
234
|
break;
|
|
153
235
|
}
|
|
236
|
+
if (event.subtype === "thinking_tokens") {
|
|
237
|
+
if (clock)
|
|
238
|
+
thinkingFrom ??= clock.now;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
154
241
|
if (event.subtype === "api_retry") {
|
|
155
242
|
events.push({
|
|
156
243
|
kind: "retry",
|
|
@@ -176,19 +263,25 @@ export function parseStream(buffer) {
|
|
|
176
263
|
skip(trimmed);
|
|
177
264
|
break;
|
|
178
265
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
266
|
+
// Ended by its own line's arrival, and trusted only on a live read.
|
|
267
|
+
const thought = clock !== undefined &&
|
|
268
|
+
thinkingFrom !== undefined &&
|
|
269
|
+
at !== undefined &&
|
|
270
|
+
Math.abs(clock.now - at) <= LIVE_SLACK_MS
|
|
271
|
+
? clock.now - thinkingFrom
|
|
272
|
+
: undefined;
|
|
273
|
+
events.push(...readAssistant(message, thought));
|
|
183
274
|
break;
|
|
184
275
|
}
|
|
185
276
|
// `user` messages on this stream are tool *results* being fed back to the
|
|
186
|
-
// model, not anything a human said
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
277
|
+
// model, not anything a human said — each one completes the card its call
|
|
278
|
+
// opened.
|
|
279
|
+
case "user": {
|
|
280
|
+
const message = asRecord(event.message);
|
|
281
|
+
if (message)
|
|
282
|
+
events.push(...readToolResults(event, message));
|
|
191
283
|
break;
|
|
284
|
+
}
|
|
192
285
|
case "result": {
|
|
193
286
|
events.push({ kind: "result", result: readResult(event) });
|
|
194
287
|
break;
|
|
@@ -216,8 +309,17 @@ export function parseStream(buffer) {
|
|
|
216
309
|
default:
|
|
217
310
|
skip(trimmed);
|
|
218
311
|
}
|
|
312
|
+
if (at !== undefined)
|
|
313
|
+
thinkingFrom = undefined;
|
|
219
314
|
}
|
|
220
|
-
return {
|
|
315
|
+
return {
|
|
316
|
+
events,
|
|
317
|
+
carry,
|
|
318
|
+
skipped,
|
|
319
|
+
nested,
|
|
320
|
+
...(sample ? { sample } : {}),
|
|
321
|
+
...(thinkingFrom === undefined ? {} : { thinkingFrom })
|
|
322
|
+
};
|
|
221
323
|
}
|
|
222
324
|
/**
|
|
223
325
|
* Read a `rate_limit_event` line, or nothing when it carries no status.
|
|
@@ -255,6 +357,11 @@ function readRateLimit(event) {
|
|
|
255
357
|
: {})
|
|
256
358
|
};
|
|
257
359
|
}
|
|
360
|
+
/** A line's `timestamp`, in epoch milliseconds, or `undefined`. */
|
|
361
|
+
function readTime(value) {
|
|
362
|
+
const ms = typeof value === "string" ? Date.parse(value) : NaN;
|
|
363
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
364
|
+
}
|
|
258
365
|
/** Whether a millisecond value is one `Date` can actually represent. */
|
|
259
366
|
function isRealDate(ms) {
|
|
260
367
|
return Number.isFinite(ms) && !Number.isNaN(new Date(ms).getTime());
|
|
@@ -279,6 +386,7 @@ function readResult(event) {
|
|
|
279
386
|
: 0;
|
|
280
387
|
const sessionId = str(event.session_id);
|
|
281
388
|
const spawned = stats ? num(stats.spawned) : undefined;
|
|
389
|
+
const errors = readErrors(event.errors);
|
|
282
390
|
return {
|
|
283
391
|
subtype: str(event.subtype) ?? "unknown",
|
|
284
392
|
isError: event.is_error === true,
|
|
@@ -290,9 +398,40 @@ function readResult(event) {
|
|
|
290
398
|
costUsd: num(event.total_cost_usd),
|
|
291
399
|
usage: readUsage(event.usage),
|
|
292
400
|
...(spawned === undefined ? {} : { subagentsSpawned: spawned }),
|
|
293
|
-
permissionDenials: denials
|
|
401
|
+
permissionDenials: denials,
|
|
402
|
+
...(errors.length > 0 ? { errors } : {}),
|
|
403
|
+
...(event.structured_output === undefined ||
|
|
404
|
+
event.structured_output === null
|
|
405
|
+
? {}
|
|
406
|
+
: { structured: event.structured_output })
|
|
294
407
|
};
|
|
295
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* A result line's `errors`, as sentences.
|
|
411
|
+
*
|
|
412
|
+
* **Anything that is not a string is stringified rather than dropped**, which is
|
|
413
|
+
* the whole of the care this needs: the field is a vendor's, an entry carrying a
|
|
414
|
+
* code and a message is a perfectly likely shape for it, and on the run this
|
|
415
|
+
* exists to explain — a resume with nowhere to resume from — it is the only
|
|
416
|
+
* account of what happened. Dropping an unexpected entry would report that run
|
|
417
|
+
* as having failed for no stated reason.
|
|
418
|
+
*/
|
|
419
|
+
function readErrors(raw) {
|
|
420
|
+
if (!Array.isArray(raw))
|
|
421
|
+
return [];
|
|
422
|
+
return raw
|
|
423
|
+
.map((entry) => (typeof entry === "string" ? entry : safeJson(entry)))
|
|
424
|
+
.filter((entry) => entry.length > 0);
|
|
425
|
+
}
|
|
426
|
+
/** `value` as JSON, or as whatever `String` makes of it when it has no JSON. */
|
|
427
|
+
function safeJson(value) {
|
|
428
|
+
try {
|
|
429
|
+
return JSON.stringify(value) ?? String(value);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
return String(value);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
296
435
|
/**
|
|
297
436
|
* How much of an unrecognised line is kept for the logs.
|
|
298
437
|
*
|
|
@@ -301,14 +440,15 @@ function readResult(event) {
|
|
|
301
440
|
*/
|
|
302
441
|
const SAMPLE_MAX_CHARS = 200;
|
|
303
442
|
/**
|
|
304
|
-
* Turn events into progress notes the parent
|
|
443
|
+
* Turn events into progress notes for the parent's transcript.
|
|
305
444
|
*
|
|
306
445
|
* **`from` is the number of notes already emitted for this run, and the key is
|
|
307
446
|
* derived from it.** Not from the note's content, not from a clock, and not from
|
|
308
447
|
* the Claude Code `uuid` — from position alone.
|
|
309
448
|
*
|
|
310
|
-
* The reason is replay. A
|
|
311
|
-
*
|
|
449
|
+
* The reason is replay. A drain that dies mid-stream is resumed from the last
|
|
450
|
+
* stored sequence, whatever arrived after it is read again, and the same
|
|
451
|
+
* assistant turn is parsed twice. A
|
|
312
452
|
* content-derived key would make two identical notes collide *by luck* (and two
|
|
313
453
|
* genuinely repeated notes — "Running tests" twice — collide wrongly). A
|
|
314
454
|
* clock-derived key would never collide, so every replay would re-post
|
|
@@ -319,31 +459,30 @@ export function toProgress(events, from) {
|
|
|
319
459
|
const out = [];
|
|
320
460
|
let n = from;
|
|
321
461
|
for (const event of events) {
|
|
322
|
-
const
|
|
323
|
-
if (!
|
|
462
|
+
const note = describe(event);
|
|
463
|
+
if (!note)
|
|
324
464
|
continue;
|
|
325
|
-
out.push({ key: `claude:${n}`,
|
|
465
|
+
out.push({ key: `claude:${n}`, ...note });
|
|
326
466
|
n++;
|
|
327
467
|
}
|
|
328
468
|
return out;
|
|
329
469
|
}
|
|
330
|
-
/** One
|
|
470
|
+
/** One note, or nothing when the event is not worth one. */
|
|
331
471
|
function describe(event) {
|
|
332
472
|
switch (event.kind) {
|
|
333
|
-
// Tool calls are not narration. A turn that only called tools said nothing
|
|
334
|
-
// the reader can act on, and it cost one Slack message per Bash/Read/Edit —
|
|
335
|
-
// nine of them for a one-line README edit. The tool names added nothing to
|
|
336
|
-
// the turns that *did* have text either, so the prefix goes with them.
|
|
337
|
-
//
|
|
338
|
-
// Same policy the in-process agents already apply in the gatekeeper
|
|
339
|
-
// (src/agents/shared/loop.ts: "Tool-only steps stay silent in Slack").
|
|
340
|
-
// Deliberately narrower than silencing the whole channel: `denied` and
|
|
341
|
-
// `retry` below stay, because they are the only evidence of a session that
|
|
342
|
-
// is being refused or throttled.
|
|
343
|
-
//
|
|
344
473
|
// Never clipped. A plan or a report cut mid-sentence reads as complete.
|
|
345
|
-
case "
|
|
346
|
-
return event.text
|
|
474
|
+
case "text":
|
|
475
|
+
return { text: event.text };
|
|
476
|
+
// Tool activity is a card, and a card never reaches the thread — core's
|
|
477
|
+
// `transcribeNote` keeps it on the transcript — so a session's every Bash,
|
|
478
|
+
// Read and Edit is shown without one message each in front of the person.
|
|
479
|
+
// `./cards.ts` holds how each tool reads.
|
|
480
|
+
case "toolUse":
|
|
481
|
+
return toolCallCard(event.id, event.name, event.input);
|
|
482
|
+
case "toolResult":
|
|
483
|
+
return toolResultCard(event);
|
|
484
|
+
case "thinking":
|
|
485
|
+
return thinkingCard(event.durationMs, event.messageId);
|
|
347
486
|
case "denied":
|
|
348
487
|
// The one progress note that is more useful than the session's own
|
|
349
488
|
// account of itself. A refused tool call is invisible in the transcript —
|
|
@@ -351,12 +490,14 @@ function describe(event) {
|
|
|
351
490
|
// parent sees a subagent being resourceful and never learns it was fenced
|
|
352
491
|
// in. Named tool first, because that is what distinguishes a broken
|
|
353
492
|
// configuration from a deny rule doing its job.
|
|
354
|
-
return
|
|
493
|
+
return {
|
|
494
|
+
text: `permission denied${event.tool ? ` for ${event.tool}` : ""}: ${event.reason}`
|
|
495
|
+
};
|
|
355
496
|
case "retry":
|
|
356
497
|
// Surfaced deliberately. This is what a budget refusal from the egress
|
|
357
498
|
// gateway looks like from inside the container, and a run that ends
|
|
358
499
|
// shortly afterwards is explained by it.
|
|
359
|
-
return `retrying the model call: ${event.detail}
|
|
500
|
+
return { text: `retrying the model call: ${event.detail}` };
|
|
360
501
|
case "rateLimit":
|
|
361
502
|
/**
|
|
362
503
|
* Silent while the bucket is fine, which is every line of a healthy
|
|
@@ -370,15 +511,19 @@ function describe(event) {
|
|
|
370
511
|
*/
|
|
371
512
|
return event.info.status === RATE_LIMIT_OK
|
|
372
513
|
? undefined
|
|
373
|
-
:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
514
|
+
: {
|
|
515
|
+
text: `the ${event.info.rateLimitType ?? "subscription"} limit reports "${event.info.status}"` +
|
|
516
|
+
(event.info.resetsAt === undefined
|
|
517
|
+
? ""
|
|
518
|
+
: ` until ${new Date(event.info.resetsAt * 1000).toISOString()}`)
|
|
519
|
+
};
|
|
520
|
+
// `init`'s session id is a handle as well as news: it reaches the host on
|
|
521
|
+
// the drain's cursor — see `DrainCursor.sessionId` in ./run.ts — where
|
|
522
|
+
// something can record it and resume with it.
|
|
379
523
|
case "init":
|
|
524
|
+
return initCard(event);
|
|
380
525
|
case "result":
|
|
381
|
-
return
|
|
526
|
+
return resultCard(event.result);
|
|
382
527
|
}
|
|
383
528
|
}
|
|
384
529
|
function clip(text, max) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub access for a session, with the token on the Worker side.
|
|
3
|
+
*
|
|
4
|
+
* The session is launched with {@link GITHUB_TOKEN_PLACEHOLDER} as `GH_TOKEN`,
|
|
5
|
+
* and git is given a credential helper that answers with it. The egress gateway swaps
|
|
6
|
+
* the placeholder for the real token on {@link GITHUB_HOSTS} only, so a process
|
|
7
|
+
* in the container can *use* GitHub as the deployment's account and can never
|
|
8
|
+
* *read* the token — the same containment as the Anthropic credential.
|
|
9
|
+
*
|
|
10
|
+
* Keyed on the placeholder, unlike the Anthropic swap: a GitHub request that
|
|
11
|
+
* did not ask for a credential stays anonymous, as it would be anywhere else.
|
|
12
|
+
*/
|
|
13
|
+
/** The hosts the token is ever sent to: git over HTTPS, and the API. */
|
|
14
|
+
export declare const GITHUB_HOSTS: ReadonlySet<string>;
|
|
15
|
+
/**
|
|
16
|
+
* What `GH_TOKEN` and git's credential helper hold. Shaped like a classic
|
|
17
|
+
* token, so nothing that checks the shape refuses it before sending it.
|
|
18
|
+
*/
|
|
19
|
+
export declare const GITHUB_TOKEN_PLACEHOLDER: string;
|
|
20
|
+
/**
|
|
21
|
+
* The session's GitHub environment. A credential helper for `https://github.com`
|
|
22
|
+
* rather than a token in the remote URL, so the checkout's `.git/config` holds
|
|
23
|
+
* nothing and every git the session starts presents the placeholder.
|
|
24
|
+
*/
|
|
25
|
+
export declare function githubEnv(): Record<string, string>;
|
|
26
|
+
/** The keys {@link githubEnv} owns, which a host's `env` may not set beside it. */
|
|
27
|
+
export declare const GITHUB_ENV_KEYS: readonly string[];
|
|
28
|
+
/** How an authorization header carried the placeholder, if it did. */
|
|
29
|
+
type Presented = "bearer" | "basic";
|
|
30
|
+
/**
|
|
31
|
+
* Whether `value` presents the placeholder: `Bearer`/`token` as `gh` sends it,
|
|
32
|
+
* or Basic `user:placeholder` as git does after a `401`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function presentsPlaceholder(value: string | null): Presented | undefined;
|
|
35
|
+
/** The real credential, in the scheme the placeholder came in. */
|
|
36
|
+
export declare function githubAuthorization(presented: Presented, token: string): string;
|
|
37
|
+
/** A refusal in GitHub's error shape, which `gh` and git both print. */
|
|
38
|
+
export declare function githubError(message: string, status: number): Response;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub access for a session, with the token on the Worker side.
|
|
3
|
+
*
|
|
4
|
+
* The session is launched with {@link GITHUB_TOKEN_PLACEHOLDER} as `GH_TOKEN`,
|
|
5
|
+
* and git is given a credential helper that answers with it. The egress gateway swaps
|
|
6
|
+
* the placeholder for the real token on {@link GITHUB_HOSTS} only, so a process
|
|
7
|
+
* in the container can *use* GitHub as the deployment's account and can never
|
|
8
|
+
* *read* the token — the same containment as the Anthropic credential.
|
|
9
|
+
*
|
|
10
|
+
* Keyed on the placeholder, unlike the Anthropic swap: a GitHub request that
|
|
11
|
+
* did not ask for a credential stays anonymous, as it would be anywhere else.
|
|
12
|
+
*/
|
|
13
|
+
/** The hosts the token is ever sent to: git over HTTPS, and the API. */
|
|
14
|
+
export const GITHUB_HOSTS = new Set([
|
|
15
|
+
"github.com",
|
|
16
|
+
"api.github.com"
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* What `GH_TOKEN` and git's credential helper hold. Shaped like a classic
|
|
20
|
+
* token, so nothing that checks the shape refuses it before sending it.
|
|
21
|
+
*/
|
|
22
|
+
export const GITHUB_TOKEN_PLACEHOLDER = "ghp_" + "0".repeat(36);
|
|
23
|
+
/** The username GitHub expects beside a token in git's Basic auth. */
|
|
24
|
+
const GIT_USER = "x-access-token";
|
|
25
|
+
/**
|
|
26
|
+
* The session's GitHub environment. A credential helper for `https://github.com`
|
|
27
|
+
* rather than a token in the remote URL, so the checkout's `.git/config` holds
|
|
28
|
+
* nothing and every git the session starts presents the placeholder.
|
|
29
|
+
*/
|
|
30
|
+
export function githubEnv() {
|
|
31
|
+
return {
|
|
32
|
+
GH_TOKEN: GITHUB_TOKEN_PLACEHOLDER,
|
|
33
|
+
GIT_CONFIG_COUNT: "1",
|
|
34
|
+
GIT_CONFIG_KEY_0: "credential.https://github.com.helper",
|
|
35
|
+
// Answers `get` only; `store` and `erase` are no-ops that still exit 0.
|
|
36
|
+
GIT_CONFIG_VALUE_0: `!f() { if test "$1" = get; then printf 'username=${GIT_USER}\\npassword=%s\\n' "$GH_TOKEN"; fi; }; f`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** The keys {@link githubEnv} owns, which a host's `env` may not set beside it. */
|
|
40
|
+
export const GITHUB_ENV_KEYS = Object.keys(githubEnv());
|
|
41
|
+
/**
|
|
42
|
+
* Whether `value` presents the placeholder: `Bearer`/`token` as `gh` sends it,
|
|
43
|
+
* or Basic `user:placeholder` as git does after a `401`.
|
|
44
|
+
*/
|
|
45
|
+
export function presentsPlaceholder(value) {
|
|
46
|
+
if (!value)
|
|
47
|
+
return undefined;
|
|
48
|
+
const [scheme = "", credential = ""] = value.trim().split(/\s+/, 2);
|
|
49
|
+
switch (scheme.toLowerCase()) {
|
|
50
|
+
case "bearer":
|
|
51
|
+
case "token":
|
|
52
|
+
return credential === GITHUB_TOKEN_PLACEHOLDER ? "bearer" : undefined;
|
|
53
|
+
case "basic": {
|
|
54
|
+
let decoded;
|
|
55
|
+
try {
|
|
56
|
+
decoded = atob(credential);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const password = decoded.slice(decoded.indexOf(":") + 1);
|
|
62
|
+
return decoded.includes(":") && password === GITHUB_TOKEN_PLACEHOLDER
|
|
63
|
+
? "basic"
|
|
64
|
+
: undefined;
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** The real credential, in the scheme the placeholder came in. */
|
|
71
|
+
export function githubAuthorization(presented, token) {
|
|
72
|
+
return presented === "basic"
|
|
73
|
+
? `Basic ${btoa(`${GIT_USER}:${token}`)}`
|
|
74
|
+
: `Bearer ${token}`;
|
|
75
|
+
}
|
|
76
|
+
/** A refusal in GitHub's error shape, which `gh` and git both print. */
|
|
77
|
+
export function githubError(message, status) {
|
|
78
|
+
return new Response(JSON.stringify({ message }), {
|
|
79
|
+
status,
|
|
80
|
+
headers: { "content-type": "application/json" }
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import type { AgentPlugin } from "@dynamicagents/core";
|
|
2
|
-
import type { CredentialStore, Lead } from "./credentials.js";
|
|
3
|
-
import { type DrainCursor, type DrainOutcome, type DrainOptions, type SessionRuntime } from "./run.js";
|
|
4
|
-
import { type ClaudeCodeConfig } from "./config.js";
|
|
5
1
|
/**
|
|
6
|
-
* `@dynamicagents/plugins/claude-code` —
|
|
2
|
+
* `@dynamicagents/plugins/claude-code` — sub-agents that run the Claude Code CLI.
|
|
7
3
|
*
|
|
8
4
|
* ## Why this exists
|
|
9
5
|
*
|
|
@@ -16,11 +12,12 @@ import { type ClaudeCodeConfig } from "./config.js";
|
|
|
16
12
|
*
|
|
17
13
|
* ## What it contributes, and what it does not
|
|
18
14
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
15
|
+
* **A writer's sub-agent spec, a model, and no plugin.** Claude Code brings its own
|
|
16
|
+
* tools, its own loop and its own context management, so there is nothing for
|
|
17
|
+
* an agent loop to drive: the session *is* the sub-agent's model —
|
|
18
|
+
* {@link claudeCodeModel} — and Think's recovery drives that. A host binds
|
|
19
|
+
* {@link CLAUDE_CODE_AGENT} to a `SubAgent` class whose `getModel()` returns
|
|
20
|
+
* one, and adds the `prepare` and `settle` that claim and release a workspace.
|
|
24
21
|
*
|
|
25
22
|
* ## The credential never enters the container
|
|
26
23
|
*
|
|
@@ -55,91 +52,20 @@ import { type ClaudeCodeConfig } from "./config.js";
|
|
|
55
52
|
* policy is `{ mode: "http-gateway" }` and which supplies a
|
|
56
53
|
* {@link CredentialStore}. See the README.
|
|
57
54
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* and a fresh isolate picks up exactly where the last one stopped.
|
|
64
|
-
*/
|
|
65
|
-
export declare function claudeCodeSession(config: ClaudeCodeConfig): {
|
|
66
|
-
/**
|
|
67
|
-
* Spawn a session for one subtask and drain its first window.
|
|
68
|
-
*
|
|
69
|
-
* `subtaskId` namespaces the exec id. Subtasks are a concurrent fan-out, so
|
|
70
|
-
* two sessions in one workspace under one id would spawn over each other —
|
|
71
|
-
* see {@link execIdFor}.
|
|
72
|
-
*/
|
|
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>;
|
|
95
|
-
/** Stop a session — `SIGTERM`, so its own process tree goes with it. */
|
|
96
|
-
stop(runtime: SessionRuntime, subtaskId: string | number): Promise<void>;
|
|
97
|
-
/**
|
|
98
|
-
* Is any credential usable right now, and if not, when?
|
|
99
|
-
*
|
|
100
|
-
* The same question {@link egress} answers per request, asked ahead of time
|
|
101
|
-
* so a host can decline to start a session it cannot pay for. That is not a
|
|
102
|
-
* micro-optimisation: an invocation carries an 18.7-27k-token cached prefix
|
|
103
|
-
* before it does anything, so starting one and letting the gateway refuse
|
|
104
|
-
* its first model call costs a container start and that prefix to learn what
|
|
105
|
-
* this returns for free — and reports it as a failed run rather than as a
|
|
106
|
-
* rate limit with a time on it.
|
|
107
|
-
*
|
|
108
|
-
* Reads only. Nothing here marks anything spent.
|
|
109
|
-
*/
|
|
110
|
-
credentials: (store: CredentialStore) => Promise<Lead>;
|
|
111
|
-
/**
|
|
112
|
-
* The `Fetcher` the workspace object installs as its egress policy.
|
|
113
|
-
*
|
|
114
|
-
* `store` is an **argument rather than a config field**, and that is the
|
|
115
|
-
* point: the credential pool's state belongs to whichever object has
|
|
116
|
-
* storage, and this same config object is also held by the parent's plugin
|
|
117
|
-
* list and by the subagent facet, neither of which has any. Making it a
|
|
118
|
-
* field would force both of them to invent one.
|
|
119
|
-
*/
|
|
120
|
-
egress: (store: CredentialStore) => {
|
|
121
|
-
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
122
|
-
connect(address: SocketAddress | string, options?: SocketOptions): Socket;
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
export type ClaudeCodeSession = ReturnType<typeof claudeCodeSession>;
|
|
126
|
-
/**
|
|
127
|
-
* The plugin.
|
|
128
|
-
*
|
|
129
|
-
* `subtaskType` and nothing else: no `toolFamilies`, because Claude Code's tools
|
|
130
|
-
* are its own, and no `capability` on the plugin, because a plugin declaring a
|
|
131
|
-
* subtask type puts its capability block on the **type** — declaring both makes
|
|
132
|
-
* the main agent read the same advice twice per round.
|
|
133
|
-
*/
|
|
134
|
-
export declare function claudeCode(config: ClaudeCodeConfig): AgentPlugin;
|
|
55
|
+
export { claudeCodeModel } from "./model.js";
|
|
56
|
+
export type { ClaudeCodeModelOptions, SessionEnd, SessionOutcome, SessionRecord, SessionWorkspace } from "./model.js";
|
|
57
|
+
export { claudeCodeSession } from "./session.js";
|
|
58
|
+
export type { ClaudeCodeSession, ResumeSession, StartSession } from "./session.js";
|
|
59
|
+
export { CLAUDE_CODE_AGENT, CLAUDE_CODE_WRITE_INPUT, type ClaudeCodeInput } from "./spec.js";
|
|
135
60
|
export { ANTHROPIC_HOST, claudeCodeEgress } from "./egress.js";
|
|
61
|
+
export { VERIFIED_CLAUDE_CODE_VERSION } from "./verified.js";
|
|
136
62
|
export type { EgressConfig } from "./egress.js";
|
|
63
|
+
export { GITHUB_HOSTS, GITHUB_TOKEN_PLACEHOLDER } from "./github.js";
|
|
137
64
|
export { credentialPool, readRefusal, readRateLimitEvent } from "./credentials.js";
|
|
138
65
|
export type { CredentialPool, CredentialPoolConfig, CredentialState, CredentialStore, Lead, Refusal } from "./credentials.js";
|
|
139
|
-
export { buildLaunch, CLAUDE_EXEC_PREFIX, CREDENTIAL_PLACEHOLDER, execIdFor, freshCursor } from "./run.js";
|
|
66
|
+
export { buildLaunch, CLAUDE_EXEC_PREFIX, CREDENTIAL_PLACEHOLDER, execIdFor, followUpExecIdFor, freshCursor } from "./run.js";
|
|
140
67
|
export type { DrainCursor, DrainOptions, DrainOutcome, Launch, LaunchOptions, SessionRuntime } from "./run.js";
|
|
141
68
|
export { parseStream, toProgress, RATE_LIMIT_OK } from "./events.js";
|
|
142
69
|
export type { ClaudeCodeEvent, ClaudeCodeResult, ClaudeCodeUsage, RateLimitInfo } from "./events.js";
|
|
143
|
-
export {
|
|
144
|
-
export { DEFAULT_PERMISSION_MODE, DEFAULT_TIMEOUT_MS, DEFAULT_WINDOW_MS } from "./config.js";
|
|
70
|
+
export { DEFAULT_PERMISSION_MODE, DEFAULT_TIMEOUT_MS } from "./config.js";
|
|
145
71
|
export type { ClaudeCodeConfig, EffortLevel, PermissionMode } from "./config.js";
|