@dynamicagents/plugins 0.10.3 → 0.11.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 +4 -3
- package/dist/arc-agi/client.d.ts +16 -0
- package/dist/arc-agi/client.js +14 -3
- package/dist/arc-agi/tools.js +15 -1
- package/dist/computer/index.js +95 -10
- package/dist/recall/index.js +1 -4
- package/dist/repo/checkout.d.ts +1 -1
- package/dist/repo/index.d.ts +14 -1
- package/dist/repo/index.js +62 -2
- package/dist/triage/index.js +1 -4
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -136,8 +136,9 @@ npm run verify:exports
|
|
|
136
136
|
|
|
137
137
|
### Working against an unpublished core
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
A plain `npm install` resolves `@dynamicagents/core` from whatever this branch's
|
|
140
|
+
`package.json` declares — `AGENTS.md` says which — and does it right over a local build
|
|
141
|
+
you are testing against.
|
|
141
142
|
|
|
142
143
|
```bash
|
|
143
144
|
npm run link:local # npm pack + tarball install from ../core
|
|
@@ -150,7 +151,7 @@ repo's source for a contract change sitting uninstalled one directory away.
|
|
|
150
151
|
`npm pack` + tarball, deliberately — not `npm link`, which symlinks the checkout and
|
|
151
152
|
gives it its own copy of every peer. Two copies of `agents` in one bundle breaks the
|
|
152
153
|
`Session` types and every `instanceof`, at runtime rather than at the type level.
|
|
153
|
-
Nothing is written to `package.json`, so CI
|
|
154
|
+
Nothing is written to `package.json`, so CI never builds against a local checkout.
|
|
154
155
|
|
|
155
156
|
`test/arc-agi/recorded.spec.ts` drives the **real** ARC API and replays a committed
|
|
156
157
|
cassette, so it needs no key either. Re-record it against the live API with:
|
package/dist/arc-agi/client.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ import { type CookieJar, type FrameResponse, type GameInfo, type ScorecardSummar
|
|
|
14
14
|
* exponential backoff. Exhausted retries and 5xx throw (transient — the
|
|
15
15
|
* Workflow step retries and the runner resumes from its checkpoint); a 401
|
|
16
16
|
* throws a tagged deterministic error (bad key — a terminal failure).
|
|
17
|
+
* - **Cancellation**: a client built with a `signal` stops at it — the `fetch` and
|
|
18
|
+
* the backoff between retries alike. `Retry-After` is honoured up to
|
|
19
|
+
* {@link MAX_RETRY_AFTER_MS} and no further: the header is the server's to set,
|
|
20
|
+
* and an uncapped one parks a tool for as long as the server likes.
|
|
17
21
|
*/
|
|
18
22
|
export interface ArcClient {
|
|
19
23
|
listGames(cookies: CookieJar): Promise<{
|
|
@@ -53,6 +57,12 @@ export interface ArcClient {
|
|
|
53
57
|
cookies: CookieJar;
|
|
54
58
|
}>;
|
|
55
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The longest a `Retry-After` is waited out. A minute covers the API's own
|
|
62
|
+
* per-minute rate window; past that the header is either hostile or describing an
|
|
63
|
+
* outage, and a retry loop is the wrong place to sit through either.
|
|
64
|
+
*/
|
|
65
|
+
export declare const MAX_RETRY_AFTER_MS = 60000;
|
|
56
66
|
/** Message prefix of the deterministic (non-retryable) auth error. */
|
|
57
67
|
export declare const ARC_AUTH_ERROR = "arc-client: unauthorized";
|
|
58
68
|
export interface ArcClientOptions {
|
|
@@ -61,5 +71,11 @@ export interface ArcClientOptions {
|
|
|
61
71
|
sleep?: (ms: number) => Promise<void>;
|
|
62
72
|
/** Max attempts per request before a 429/5xx becomes a thrown transient fault. */
|
|
63
73
|
maxAttempts?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Stops every request this client makes, and every wait between them. Taken at
|
|
76
|
+
* construction, because a client built for one tool surface stops on that
|
|
77
|
+
* surface's signal — core's `ToolFamilyContext.signal` for a subagent's.
|
|
78
|
+
*/
|
|
79
|
+
signal?: AbortSignal;
|
|
64
80
|
}
|
|
65
81
|
export declare function makeArcClient(apiKey: string, options?: ArcClientOptions): ArcClient;
|
package/dist/arc-agi/client.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { withAbort } from "@dynamicagents/core";
|
|
1
2
|
import { ARC_BASE_URL } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The longest a `Retry-After` is waited out. A minute covers the API's own
|
|
5
|
+
* per-minute rate window; past that the header is either hostile or describing an
|
|
6
|
+
* outage, and a retry loop is the wrong place to sit through either.
|
|
7
|
+
*/
|
|
8
|
+
export const MAX_RETRY_AFTER_MS = 60_000;
|
|
2
9
|
/** Message prefix of the deterministic (non-retryable) auth error. */
|
|
3
10
|
export const ARC_AUTH_ERROR = "arc-client: unauthorized";
|
|
4
11
|
const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -23,6 +30,7 @@ export function makeArcClient(apiKey, options = {}) {
|
|
|
23
30
|
const doFetch = options.fetchFn ?? fetch;
|
|
24
31
|
const sleep = options.sleep ?? realSleep;
|
|
25
32
|
const maxAttempts = options.maxAttempts ?? 4;
|
|
33
|
+
const signal = options.signal;
|
|
26
34
|
async function request(path, init, cookies) {
|
|
27
35
|
const headers = {
|
|
28
36
|
"X-API-Key": apiKey,
|
|
@@ -39,7 +47,8 @@ export function makeArcClient(apiKey, options = {}) {
|
|
|
39
47
|
const res = await doFetch(`${ARC_BASE_URL}${path}`, {
|
|
40
48
|
method: init.method,
|
|
41
49
|
headers,
|
|
42
|
-
body: init.body === undefined ? undefined : JSON.stringify(init.body)
|
|
50
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
51
|
+
...(signal ? { signal } : {})
|
|
43
52
|
});
|
|
44
53
|
if (res.ok) {
|
|
45
54
|
const nextCookies = mergeCookies(cookies, res);
|
|
@@ -56,9 +65,11 @@ export function makeArcClient(apiKey, options = {}) {
|
|
|
56
65
|
}
|
|
57
66
|
const retryAfter = Number(res.headers.get("retry-after"));
|
|
58
67
|
const backoffMs = Number.isFinite(retryAfter) && retryAfter > 0
|
|
59
|
-
? retryAfter * 1000
|
|
68
|
+
? Math.min(retryAfter * 1000, MAX_RETRY_AFTER_MS)
|
|
60
69
|
: Math.min(1000 * 2 ** (attempt - 1), 8000);
|
|
61
|
-
|
|
70
|
+
// The wait between attempts is the longest thing this loop does, so it is
|
|
71
|
+
// the part a cancel most needs to reach.
|
|
72
|
+
await withAbort(signal, sleep(backoffMs));
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
75
|
return {
|
package/dist/arc-agi/tools.js
CHANGED
|
@@ -45,7 +45,9 @@ export function buildArcGameTools(apiKey, ctx) {
|
|
|
45
45
|
// cannot do — `Env` is the ambient interface `wrangler types` generates into a
|
|
46
46
|
// *consumer's* app and does not exist here. Config-at-instantiation is also
|
|
47
47
|
// the only thing that works on Workers at all, where `env` has no module scope.
|
|
48
|
-
|
|
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 });
|
|
49
51
|
const runtime = runtimeAs(ctx.runtime);
|
|
50
52
|
// All settled before the model runs — it cannot pick a different game, and it
|
|
51
53
|
// cannot see, choose, or name a card or a play at all. The game is the type's
|
|
@@ -249,6 +251,18 @@ export function buildArcGameTools(apiKey, ctx) {
|
|
|
249
251
|
// exactly as a crash mid-action always was.
|
|
250
252
|
session.pendingAction = { action, x, y };
|
|
251
253
|
await save(session);
|
|
254
|
+
// A cancel is checked here, with the intent already on disk, because this
|
|
255
|
+
// is the last moment before the request: a check ahead of the write would
|
|
256
|
+
// miss a cancel that lands during it. A request on an aborted signal is
|
|
257
|
+
// never sent, so the intent is taken back rather than left for the next
|
|
258
|
+
// chunk to warn about a move that never left. The batch ends the ordinary
|
|
259
|
+
// way, with every step already sent recorded.
|
|
260
|
+
if (ctx.signal?.aborted) {
|
|
261
|
+
session.pendingAction = null;
|
|
262
|
+
await save(session);
|
|
263
|
+
trace.push(remaining(steps.length, index, "this call was cancelled"));
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
252
266
|
const { frame, cookies } = await client.act({ action, gameId: session.gameId, guid: session.guid, x, y, note }, session.cookies);
|
|
253
267
|
// Diff against the board as it was immediately BEFORE this step, so each
|
|
254
268
|
// line attributes its change to the one action that caused it. That
|
package/dist/computer/index.js
CHANGED
|
@@ -52,7 +52,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
52
52
|
});
|
|
53
53
|
import { tool } from "ai";
|
|
54
54
|
import { z } from "zod";
|
|
55
|
-
import { definePlugin } from "@dynamicagents/core";
|
|
55
|
+
import { definePlugin, withAbort } from "@dynamicagents/core";
|
|
56
56
|
import { getWorkspace, shellQuote } from "@cloudflare/computer";
|
|
57
57
|
import { guardPath } from "./paths.js";
|
|
58
58
|
import { cancelledNote, humanBytes, packBlocks, renderGrepMatches, renderResult } from "./render.js";
|
|
@@ -290,6 +290,31 @@ export function withShell(command, shell) {
|
|
|
290
290
|
export function withShellTranscript(command, shell) {
|
|
291
291
|
return shell ? `${wrapped(command, shell)} 2>&1` : command;
|
|
292
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Wait for a disposable resource without outliving `signal`.
|
|
295
|
+
*
|
|
296
|
+
* `withAbort` can abandon the wait but not the resource, and one that arrives
|
|
297
|
+
* after its caller has given up has nobody left to dispose it. So it is released
|
|
298
|
+
* on arrival instead — disposed by default, or handed to `release` where
|
|
299
|
+
* disposing alone would leave work running.
|
|
300
|
+
*/
|
|
301
|
+
function acquire(signal, opening, release = (resource) => resource[Symbol.dispose]()) {
|
|
302
|
+
return withAbort(signal, opening, () => {
|
|
303
|
+
void opening.then(release).catch(() => { });
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* How an exec handle that arrived too late is released. Disposing only detaches
|
|
308
|
+
* this side, so the process is killed first.
|
|
309
|
+
*/
|
|
310
|
+
async function killLate(handle) {
|
|
311
|
+
try {
|
|
312
|
+
await handle.kill("SIGTERM");
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
handle[Symbol.dispose]();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
293
318
|
export function buildComputerTools(workspace, config, advisories) {
|
|
294
319
|
const cwd = config.cwd ?? DEFAULT_CWD;
|
|
295
320
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -311,7 +336,7 @@ export function buildComputerTools(workspace, config, advisories) {
|
|
|
311
336
|
* it was dropping. The cost is one same-colo RPC on commands that previously
|
|
312
337
|
* skipped it, against a class of silent data loss.
|
|
313
338
|
*/
|
|
314
|
-
const awaitAdvisories = async (command) => {
|
|
339
|
+
const awaitAdvisories = async (command, signal) => {
|
|
315
340
|
if (!advisories)
|
|
316
341
|
return {};
|
|
317
342
|
// Fails **open**, and that belongs here rather than at the call site: this
|
|
@@ -321,9 +346,14 @@ export function buildComputerTools(workspace, config, advisories) {
|
|
|
321
346
|
// subagent.
|
|
322
347
|
const read = async () => {
|
|
323
348
|
try {
|
|
324
|
-
|
|
349
|
+
signal?.throwIfAborted();
|
|
350
|
+
return await withAbort(signal, advisories());
|
|
325
351
|
}
|
|
326
|
-
catch {
|
|
352
|
+
catch (err) {
|
|
353
|
+
// Open on a failed read, never on a cancel: failing open here would go on
|
|
354
|
+
// to run the command the caller just gave up on.
|
|
355
|
+
if (signal?.aborted)
|
|
356
|
+
throw err;
|
|
327
357
|
return [];
|
|
328
358
|
}
|
|
329
359
|
};
|
|
@@ -338,7 +368,9 @@ export function buildComputerTools(workspace, config, advisories) {
|
|
|
338
368
|
// the outcome.
|
|
339
369
|
while (execGate(current, command).block && Date.now() < deadline) {
|
|
340
370
|
const wait = Math.min(INSTALL_POLL_MS, deadline - Date.now());
|
|
341
|
-
|
|
371
|
+
// Throws on a cancel rather than returning the verdict so far: a gate that
|
|
372
|
+
// outlived its caller would otherwise go on to run the command it was holding.
|
|
373
|
+
await withAbort(signal, new Promise((resolve) => setTimeout(resolve, wait)));
|
|
342
374
|
current = await read();
|
|
343
375
|
}
|
|
344
376
|
return execGate(current, command);
|
|
@@ -444,7 +476,7 @@ export function buildComputerTools(workspace, config, advisories) {
|
|
|
444
476
|
.optional()
|
|
445
477
|
.describe(`Working directory (default: ${cwd})`)
|
|
446
478
|
}),
|
|
447
|
-
execute: async ({ command, cwd: overrideCwd }) => {
|
|
479
|
+
execute: async ({ command, cwd: overrideCwd }, { abortSignal }) => {
|
|
448
480
|
/**
|
|
449
481
|
* One line per command, and it is the only view of where a task's wall
|
|
450
482
|
* clock actually goes.
|
|
@@ -462,13 +494,50 @@ export function buildComputerTools(workspace, config, advisories) {
|
|
|
462
494
|
* indistinguishable from the outside and want opposite fixes.
|
|
463
495
|
*/
|
|
464
496
|
const startedAtMs = Date.now();
|
|
497
|
+
/**
|
|
498
|
+
* What the model is told when this call stops before the command
|
|
499
|
+
* finishes. Said plainly, because the likeliest next move after a bare
|
|
500
|
+
* error is the same command again — and whether that is right depends on
|
|
501
|
+
* where the call stopped. A command that outran the limit once will again;
|
|
502
|
+
* one that never started because the workspace did not answer may not.
|
|
503
|
+
*
|
|
504
|
+
* `sb_exec` reads its call's signal rather than leaving the wait to core,
|
|
505
|
+
* and this is why: core's abandonment can say only that the command may
|
|
506
|
+
* still be running. Core's `TOOL_CALL_GRACE_MS` is the window this answer
|
|
507
|
+
* has to arrive in, and it covers sending the kill below.
|
|
508
|
+
*/
|
|
509
|
+
const stopped = (gateMs, started) => {
|
|
510
|
+
const timedOut = abortSignal?.reason?.name ===
|
|
511
|
+
"TimeoutError";
|
|
512
|
+
console.info("[computer] sb_exec stopped", {
|
|
513
|
+
command,
|
|
514
|
+
gateMs,
|
|
515
|
+
durationMs: Date.now() - startedAtMs - gateMs,
|
|
516
|
+
started,
|
|
517
|
+
reason: timedOut ? "time limit" : "cancelled"
|
|
518
|
+
});
|
|
519
|
+
if (!timedOut)
|
|
520
|
+
return "the command was stopped because this call was cancelled. Anything it changed before then is still changed.";
|
|
521
|
+
return started
|
|
522
|
+
? "the command was stopped: it ran past this call's time limit. Anything it changed before then is still changed. Try something narrower."
|
|
523
|
+
: "the command did not run: this call reached its time limit while the workspace was still getting ready, so nothing was changed. Try again; if it happens again, the workspace is not responding.";
|
|
524
|
+
};
|
|
465
525
|
// Only `sb_exec` waits on an install. The file tools read and write
|
|
466
526
|
// source, which is in the workspace and unaffected by an install in
|
|
467
527
|
// flight — blocking them would stop the subagent doing the reading it
|
|
468
528
|
// could usefully do while it waits. They do consult `writeGate`, which
|
|
469
529
|
// is a different question: not "is the tree ready" but "does a write
|
|
470
530
|
// survive at all".
|
|
471
|
-
|
|
531
|
+
let gate;
|
|
532
|
+
try {
|
|
533
|
+
gate = await awaitAdvisories(command, abortSignal);
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
// Only a cancel gets out of the gate; a failed advisory read opens it.
|
|
537
|
+
if (abortSignal?.aborted)
|
|
538
|
+
return stopped(Date.now() - startedAtMs, false);
|
|
539
|
+
throw err;
|
|
540
|
+
}
|
|
472
541
|
const gateMsWaited = Date.now() - startedAtMs;
|
|
473
542
|
if (gate.block) {
|
|
474
543
|
console.info("[computer] sb_exec blocked by a workspace advisory", {
|
|
@@ -481,10 +550,15 @@ export function buildComputerTools(workspace, config, advisories) {
|
|
|
481
550
|
// is context for the output, not a substitute for it — which is why it
|
|
482
551
|
// is carried through the catch as well.
|
|
483
552
|
const note = (body) => gate.warn ? `${gate.warn}\n\n${body}` : body;
|
|
553
|
+
// Set once the exec is sent. See `stopped`.
|
|
554
|
+
let started = false;
|
|
484
555
|
try {
|
|
485
556
|
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
486
557
|
try {
|
|
487
|
-
|
|
558
|
+
// A workspace that stops answering would hold the call before any of the
|
|
559
|
+
// cancellation below is reached.
|
|
560
|
+
abortSignal?.throwIfAborted();
|
|
561
|
+
const ws = __addDisposableResource(env_2, await acquire(abortSignal, workspace()), false);
|
|
488
562
|
// Read once per command. This is the *host's* thunk, so calling it
|
|
489
563
|
// twice in the construction of one command's options is two chances to
|
|
490
564
|
// disagree — the check and the value would come from different reads.
|
|
@@ -497,8 +571,17 @@ export function buildComputerTools(workspace, config, advisories) {
|
|
|
497
571
|
};
|
|
498
572
|
// Transcript, not two streams: this result goes to a model, which
|
|
499
573
|
// reads it as a terminal session rather than parsing it.
|
|
500
|
-
|
|
501
|
-
|
|
574
|
+
// Not started at all on a signal that has already fired: a process that
|
|
575
|
+
// is killed on the next line still ran long enough to change something.
|
|
576
|
+
abortSignal?.throwIfAborted();
|
|
577
|
+
// From here a stop cannot promise nothing ran: the exec may reach the
|
|
578
|
+
// container before its handle reaches this side.
|
|
579
|
+
started = true;
|
|
580
|
+
const handle = __addDisposableResource(env_2, await acquire(abortSignal, ws.runtime.exec(withShellTranscript(command, config.shell), options), killLate), false);
|
|
581
|
+
// The runtime takes no signal, so stopping the wait and stopping the
|
|
582
|
+
// process are two acts. Disposing the handle is neither — it releases
|
|
583
|
+
// this side's attachment and leaves the command running.
|
|
584
|
+
const result = await withAbort(abortSignal, handle.result(), () => handle.kill("SIGTERM"));
|
|
502
585
|
console.info("[computer] sb_exec", {
|
|
503
586
|
command,
|
|
504
587
|
exitCode: result.exitCode,
|
|
@@ -520,6 +603,8 @@ export function buildComputerTools(workspace, config, advisories) {
|
|
|
520
603
|
}
|
|
521
604
|
}
|
|
522
605
|
catch (err) {
|
|
606
|
+
if (abortSignal?.aborted)
|
|
607
|
+
return note(stopped(gateMsWaited, started));
|
|
523
608
|
const lost = execLostNote(err);
|
|
524
609
|
console.warn("[computer] sb_exec failed", {
|
|
525
610
|
command,
|
package/dist/recall/index.js
CHANGED
|
@@ -119,10 +119,7 @@ export function recall(config) {
|
|
|
119
119
|
});
|
|
120
120
|
const { embeddings } = await embedMany({
|
|
121
121
|
model: provider.embedding(embeddingModelId),
|
|
122
|
-
values: texts
|
|
123
|
-
// Failures surface to the caller; archival is best-effort and the SDK's
|
|
124
|
-
// backoff would only add latency in front of a compaction.
|
|
125
|
-
maxRetries: 0
|
|
122
|
+
values: texts
|
|
126
123
|
});
|
|
127
124
|
return embeddings;
|
|
128
125
|
});
|
package/dist/repo/checkout.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface GitAnswer {
|
|
|
34
34
|
stderr: string;
|
|
35
35
|
unreachable?: true;
|
|
36
36
|
}
|
|
37
|
-
/** The two runners `refreshCheckout` borrows from `
|
|
37
|
+
/** The two runners `refreshCheckout` borrows from `repoSurface`. */
|
|
38
38
|
export interface GitRunners {
|
|
39
39
|
plain: (args: string, cwd: string, vars?: Record<string, string>) => Promise<GitAnswer>;
|
|
40
40
|
/**
|
package/dist/repo/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ToolSet } from "ai";
|
|
2
|
-
import type { AgentPlugin } from "@dynamicagents/core";
|
|
2
|
+
import type { AgentPlugin, MainAgentToolApproval } from "@dynamicagents/core";
|
|
3
3
|
/**
|
|
4
4
|
* The URL parsing, re-exported from the one entry point.
|
|
5
5
|
*
|
|
@@ -311,4 +311,17 @@ export declare function truncateOutput(text: string, max: number): string;
|
|
|
311
311
|
export declare function buildRepoTools(config: RepoConfig,
|
|
312
312
|
/** Forwarded to every `exec`; see {@link RepoExec}'s `runtime` option. */
|
|
313
313
|
runtime?: unknown): ToolSet;
|
|
314
|
+
/**
|
|
315
|
+
* The repo tools a person approves before they run: opening a pull request, and
|
|
316
|
+
* nothing else. Pushing a work branch and commenting run without asking.
|
|
317
|
+
*
|
|
318
|
+
* The reason is what the person reads beside Approve and Reject, so it says what
|
|
319
|
+
* the call will do in their terms: which repository, from which branch into
|
|
320
|
+
* which, and under what title. The repository is the checkout's origin, read
|
|
321
|
+
* through the same lookup the tool runs, so a directory named after one
|
|
322
|
+
* repository cannot present a pull request on another. The input has already
|
|
323
|
+
* passed the tool's own schema. What a rule is, and whose calls it covers, is
|
|
324
|
+
* core's to say — see `AgentPlugin.mainAgentToolApproval`.
|
|
325
|
+
*/
|
|
326
|
+
export declare function repoToolApproval(config: RepoConfig): MainAgentToolApproval;
|
|
314
327
|
export declare function repo(config: RepoConfig): AgentPlugin;
|
package/dist/repo/index.js
CHANGED
|
@@ -157,7 +157,7 @@ function unreachableNote(err) {
|
|
|
157
157
|
* `repo_push` race: the commit fails, the push succeeds against the previous
|
|
158
158
|
* state, and the round reports work that never landed.
|
|
159
159
|
*
|
|
160
|
-
* Held here rather than in {@link
|
|
160
|
+
* Held here rather than in {@link repoSurface}'s closure, because that closure
|
|
161
161
|
* is too short-lived to be the boundary. Core rebuilds `mainAgentTools` every
|
|
162
162
|
* turn and gives each subagent execution its own tool family, so a per-call queue
|
|
163
163
|
* leaves a subagent racing its parent over one checkout — the same collision, one
|
|
@@ -196,6 +196,14 @@ const gitQueues = new WeakMap();
|
|
|
196
196
|
export function buildRepoTools(config,
|
|
197
197
|
/** Forwarded to every `exec`; see {@link RepoExec}'s `runtime` option. */
|
|
198
198
|
runtime) {
|
|
199
|
+
return repoSurface(config, runtime).tools;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The repo tools, and the approval rule for the one a person approves — one
|
|
203
|
+
* closure, so the rule reads the checkout through the same `origin` the tool it
|
|
204
|
+
* gates runs against. See {@link repoToolApproval}.
|
|
205
|
+
*/
|
|
206
|
+
function repoSurface(config, runtime) {
|
|
199
207
|
const workdir = config.workdir ?? DEFAULT_WORKDIR;
|
|
200
208
|
const apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
201
209
|
const author = config.author ?? DEFAULT_AUTHOR;
|
|
@@ -502,7 +510,7 @@ runtime) {
|
|
|
502
510
|
// `.git/config`.
|
|
503
511
|
return { remote: { host: location.host, url: location.url } };
|
|
504
512
|
};
|
|
505
|
-
|
|
513
|
+
const tools = {
|
|
506
514
|
repo_clone: tool({
|
|
507
515
|
description: "Clone a git repository into the workspace. Returns the checkout path and the branch you landed on. If the repository is already checked out from an earlier task, it is fetched and reset to the remote instead — unless it has uncommitted changes, which are left alone for you to inspect.",
|
|
508
516
|
inputSchema: z.object({
|
|
@@ -1044,11 +1052,63 @@ runtime) {
|
|
|
1044
1052
|
}
|
|
1045
1053
|
})
|
|
1046
1054
|
};
|
|
1055
|
+
/**
|
|
1056
|
+
* Where a pull request is opened, as the person approving it reads it: the
|
|
1057
|
+
* host, owner and repository of the checkout's origin — what the tool resolves
|
|
1058
|
+
* when it runs. Never the directory's name, which says nothing about where
|
|
1059
|
+
* `.git/config` points. A checkout with no readable origin on an allowed host is
|
|
1060
|
+
* named by its path, and the tool refuses it when it runs.
|
|
1061
|
+
*/
|
|
1062
|
+
const destination = async (dir) => {
|
|
1063
|
+
const { remote } = await origin(dir);
|
|
1064
|
+
const parsed = remote ? parseRepo(remote.url, allowedHosts) : undefined;
|
|
1065
|
+
return remote && parsed
|
|
1066
|
+
? `${remote.host}/${parsed.owner}/${parsed.repo}`
|
|
1067
|
+
: `the checkout at ${approvalField(dir)}, whose origin could not be read`;
|
|
1068
|
+
};
|
|
1069
|
+
const approval = {
|
|
1070
|
+
repo_open_pr: async (input) => {
|
|
1071
|
+
const { dir, head, base, title } = input;
|
|
1072
|
+
return {
|
|
1073
|
+
type: "user-approval",
|
|
1074
|
+
reason: `Open a pull request on ${await destination(dir)} from \`${approvalField(head)}\` into \`${approvalField(base)}\`: ${approvalField(title)}`
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
return { tools, approval };
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* The longest branch, title or path a person is shown in full before approving.
|
|
1082
|
+
* Every model-supplied field in a reason is cut to it, so no one input can bury
|
|
1083
|
+
* the rest of what they are deciding.
|
|
1084
|
+
*/
|
|
1085
|
+
const APPROVAL_FIELD_CHARS = 200;
|
|
1086
|
+
/** One model-supplied field of an approval reason, cut to {@link APPROVAL_FIELD_CHARS}. */
|
|
1087
|
+
function approvalField(text) {
|
|
1088
|
+
return text.length <= APPROVAL_FIELD_CHARS
|
|
1089
|
+
? text
|
|
1090
|
+
: `${text.slice(0, APPROVAL_FIELD_CHARS - 1)}…`;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* The repo tools a person approves before they run: opening a pull request, and
|
|
1094
|
+
* nothing else. Pushing a work branch and commenting run without asking.
|
|
1095
|
+
*
|
|
1096
|
+
* The reason is what the person reads beside Approve and Reject, so it says what
|
|
1097
|
+
* the call will do in their terms: which repository, from which branch into
|
|
1098
|
+
* which, and under what title. The repository is the checkout's origin, read
|
|
1099
|
+
* through the same lookup the tool runs, so a directory named after one
|
|
1100
|
+
* repository cannot present a pull request on another. The input has already
|
|
1101
|
+
* passed the tool's own schema. What a rule is, and whose calls it covers, is
|
|
1102
|
+
* core's to say — see `AgentPlugin.mainAgentToolApproval`.
|
|
1103
|
+
*/
|
|
1104
|
+
export function repoToolApproval(config) {
|
|
1105
|
+
return repoSurface(config).approval;
|
|
1047
1106
|
}
|
|
1048
1107
|
export function repo(config) {
|
|
1049
1108
|
return definePlugin({
|
|
1050
1109
|
key: "repo",
|
|
1051
1110
|
mainAgentTools: () => buildRepoTools(config),
|
|
1111
|
+
mainAgentToolApproval: () => repoToolApproval(config),
|
|
1052
1112
|
// The runtime state goes through to `exec` untouched, so a delegated
|
|
1053
1113
|
// subtask's git commands run in the same container its parent cloned into.
|
|
1054
1114
|
toolFamilies: {
|
package/dist/triage/index.js
CHANGED
|
@@ -144,10 +144,7 @@ export async function shouldReply(model, history, options = {}) {
|
|
|
144
144
|
const { output } = await generateText({
|
|
145
145
|
model,
|
|
146
146
|
output: Output.object({ schema: triageSchema }),
|
|
147
|
-
prompt: buildTriagePrompt(history, options)
|
|
148
|
-
// Consistent with the rest of the runtime: we handle failure ourselves
|
|
149
|
-
// (here, by failing open), so SDK backoff would only delay the turn.
|
|
150
|
-
maxRetries: 0
|
|
147
|
+
prompt: buildTriagePrompt(history, options)
|
|
151
148
|
});
|
|
152
149
|
// Log the whole verdict, not just the outcome. The booleans are the
|
|
153
150
|
// reasoning — a correct verdict reached through wrong booleans means the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamicagents/plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Optional, composable capabilities for a Dynamic Agent: ARC-AGI-3, browser rendering, a Linux container, git and pull requests, workspace, episodic recall, and pre-turn triage. One subpath per plugin, so a bundle grows only with what it imports.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -85,12 +85,12 @@
|
|
|
85
85
|
"verify:peer-ranges": "node scripts/verify-peer-ranges.mjs",
|
|
86
86
|
"prepack": "npm run build && npm run verify:exports",
|
|
87
87
|
"prepublishOnly": "npm run check && npm test && npm run verify:exports",
|
|
88
|
-
"prepare": "husky"
|
|
88
|
+
"prepare": "husky || true && npm run build"
|
|
89
89
|
},
|
|
90
90
|
"peerDependencies": {
|
|
91
91
|
"@cloudflare/computer": "^0.2.1",
|
|
92
92
|
"@cloudflare/shell": "^0.4.3",
|
|
93
|
-
"@dynamicagents/core": ">=0.
|
|
93
|
+
"@dynamicagents/core": ">=0.11.0 <0.12.0",
|
|
94
94
|
"agents": ">=0.21.0 <0.23.0",
|
|
95
95
|
"ai": "^7.0.77",
|
|
96
96
|
"drizzle-orm": ">=0.45.2",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"@cloudflare/computer": "^0.2.1",
|
|
110
110
|
"@cloudflare/shell": "0.4.3",
|
|
111
111
|
"@cloudflare/vitest-plugin": "^1.1.5",
|
|
112
|
-
"@dynamicagents/core": "^0.
|
|
112
|
+
"@dynamicagents/core": "^0.11.0",
|
|
113
113
|
"@types/node": "^26.5.0",
|
|
114
114
|
"agents": ">=0.21.0 <0.23.0",
|
|
115
115
|
"ai": "^7.0.93",
|