@dynamicagents/plugins 0.13.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/dist/claude-code/cards.d.ts +2 -0
- package/dist/claude-code/cards.js +11 -0
- package/dist/claude-code/model.js +26 -2
- package/dist/claude-code/run.d.ts +21 -4
- package/dist/claude-code/run.js +51 -39
- package/dist/claude-code/session.js +4 -1
- package/dist/claude-code/verified.d.ts +1 -1
- package/dist/claude-code/verified.js +1 -1
- package/dist/workspace/container-deps.d.ts +4 -0
- package/dist/workspace/container-deps.js +26 -1
- package/dist/workspace/deps-snapshot.d.ts +110 -0
- package/dist/workspace/deps-snapshot.js +424 -0
- package/dist/workspace/index.d.ts +2 -1
- package/dist/workspace/install-job.d.ts +49 -0
- package/dist/workspace/install-job.js +75 -7
- package/dist/workspace/install.d.ts +9 -0
- package/dist/workspace/install.js +21 -3
- package/dist/workspace/object.d.ts +24 -4
- package/dist/workspace/object.js +130 -16
- package/dist/workspace/wake.d.ts +2 -0
- package/package.json +11 -11
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DurableObject } from "cloudflare:workers";
|
|
2
2
|
import { type WorkspaceEgressPolicy, type WorkspaceStub } from "@cloudflare/computer";
|
|
3
3
|
import { ContainerBackend, type ContainerInstanceSize } from "@cloudflare/computer/backends/container";
|
|
4
|
+
import { type DepsSnapshotRecord } from "./deps-snapshot.js";
|
|
4
5
|
import { type WorkspaceAdvisory } from "./advisory.js";
|
|
5
6
|
import type { InstallPlan, InstallState } from "./install.js";
|
|
6
7
|
import type { RepoGitResult } from "../repo/index.js";
|
|
@@ -49,6 +50,7 @@ import type { RepoGitResult } from "../repo/index.js";
|
|
|
49
50
|
* and each carries the reasoning for its own decisions:
|
|
50
51
|
*
|
|
51
52
|
* - `./install-job.ts` — when a dependency install runs, and every guard on it.
|
|
53
|
+
* - `./deps-snapshot.ts` — starting a container from one that already installed.
|
|
52
54
|
* - `./sync.ts` — the host's half of syncing, which nothing else will do.
|
|
53
55
|
* - `./ca-trust.ts` — making a container able to speak TLS.
|
|
54
56
|
* - `./git-identity.ts` — who a container's commits belong to.
|
|
@@ -337,15 +339,33 @@ export declare abstract class WorkspaceObjectBase<TEnv extends Cloudflare.Env =
|
|
|
337
339
|
dir: string;
|
|
338
340
|
repo?: string;
|
|
339
341
|
}): Promise<InstallState>;
|
|
342
|
+
/**
|
|
343
|
+
* This workspace's dependency snapshot, for another of the same class to start
|
|
344
|
+
* from — see {@link seedDepsSnapshot}. Starts no container and moves no
|
|
345
|
+
* deadline.
|
|
346
|
+
*/
|
|
347
|
+
depsSnapshot(): Promise<DepsSnapshotRecord | undefined>;
|
|
348
|
+
/**
|
|
349
|
+
* Start this workspace's next container from `record`, another workspace's
|
|
350
|
+
* dependency snapshot, when it fits the checkout here — a worktree at the same
|
|
351
|
+
* path as the checkout it was cut from. Call it after the checkout and before
|
|
352
|
+
* the first command.
|
|
353
|
+
*
|
|
354
|
+
* A snapshot is that workspace's whole disk, so offer one only between
|
|
355
|
+
* workspaces of the same caller.
|
|
356
|
+
*/
|
|
357
|
+
seedDepsSnapshot(record: DepsSnapshotRecord): Promise<{
|
|
358
|
+
seeded: boolean;
|
|
359
|
+
reason?: string;
|
|
360
|
+
}>;
|
|
340
361
|
/**
|
|
341
362
|
* Stop the container, and **keep everything on disk**.
|
|
342
363
|
*
|
|
343
364
|
* The counterpart to {@link reclaimIfIdle}, and the difference is the whole
|
|
344
365
|
* reason both exist. A reclaim is for a workspace nobody wants again: it empties
|
|
345
|
-
* storage, so the checkout and the dependency
|
|
346
|
-
* for a workspace that will be worked in again — the next task on the
|
|
347
|
-
* repository
|
|
348
|
-
* design is arranged around.
|
|
366
|
+
* storage, so the checkout and the dependency snapshot go with the container.
|
|
367
|
+
* This is for a workspace that will be worked in again — the next task on the
|
|
368
|
+
* same repository skips the clone, and the install by restoring the snapshot.
|
|
349
369
|
*
|
|
350
370
|
* **Why a caller needs this at all.** The idle deadline would eventually do it,
|
|
351
371
|
* but it cannot be tuned down to meet a cost target: `containerIdleMs` must
|
package/dist/workspace/object.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createGitClient } from "@cloudflare/computer/git";
|
|
|
9
9
|
import { createCloudflareObserver } from "@cloudflare/computer/observe/cloudflare";
|
|
10
10
|
import { ContainerTrust } from "./ca-trust.js";
|
|
11
11
|
import { ContainerDeps } from "./container-deps.js";
|
|
12
|
+
import { DepsSnapshot } from "./deps-snapshot.js";
|
|
12
13
|
import { ContainerGitIdentity } from "./git-identity.js";
|
|
13
14
|
import { InstallJob } from "./install-job.js";
|
|
14
15
|
import { WorkspaceGitHost } from "./git-host.js";
|
|
@@ -62,6 +63,7 @@ import { pathExists } from "./read.js";
|
|
|
62
63
|
* and each carries the reasoning for its own decisions:
|
|
63
64
|
*
|
|
64
65
|
* - `./install-job.ts` — when a dependency install runs, and every guard on it.
|
|
66
|
+
* - `./deps-snapshot.ts` — starting a container from one that already installed.
|
|
65
67
|
* - `./sync.ts` — the host's half of syncing, which nothing else will do.
|
|
66
68
|
* - `./ca-trust.ts` — making a container able to speak TLS.
|
|
67
69
|
* - `./git-identity.ts` — who a container's commits belong to.
|
|
@@ -135,6 +137,8 @@ const HOLDS_KEY = "container-holds";
|
|
|
135
137
|
const RELEASE_ASKED_KEY = "container-release-asked";
|
|
136
138
|
/** Where the container-warm wake keeps its current schedule id. */
|
|
137
139
|
const CONTAINER_WARM_ID = "container-warm-id";
|
|
140
|
+
/** Where the dependency-snapshot wake keeps its current schedule id. */
|
|
141
|
+
const DEPS_SNAPSHOT_ID = "deps-snapshot-id";
|
|
138
142
|
/**
|
|
139
143
|
* How long a container stays up after the last command **started**.
|
|
140
144
|
*
|
|
@@ -301,6 +305,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
301
305
|
idleReclaim: () => this.#onIdleReclaim(),
|
|
302
306
|
containerIdle: (payload) => this.#onContainerIdle(payload),
|
|
303
307
|
containerWarm: () => this.#onContainerWarm(),
|
|
308
|
+
depsSnapshot: () => this.#onDepsSnapshot(),
|
|
304
309
|
syncRetry: (payload) => this.#onSyncDrain(payload)
|
|
305
310
|
},
|
|
306
311
|
onError: (err) => {
|
|
@@ -337,6 +342,18 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
337
342
|
key: CONTAINER_WARM_ID,
|
|
338
343
|
callback: "containerWarm"
|
|
339
344
|
});
|
|
345
|
+
/**
|
|
346
|
+
* Snapshot a container that just installed — see `./deps-snapshot.ts`.
|
|
347
|
+
*
|
|
348
|
+
* From the alarm rather than the install's drain, which a model turn's
|
|
349
|
+
* `waitUntil` may own.
|
|
350
|
+
*/
|
|
351
|
+
#snapshotDue = namedDeadline({
|
|
352
|
+
storage: this.ctx.storage,
|
|
353
|
+
scheduler: this.#wake.scheduler,
|
|
354
|
+
key: DEPS_SNAPSHOT_ID,
|
|
355
|
+
callback: "depsSnapshot"
|
|
356
|
+
});
|
|
340
357
|
/**
|
|
341
358
|
* Come back and move the next block of an outstanding pull.
|
|
342
359
|
*
|
|
@@ -369,6 +386,19 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
369
386
|
ready: () => this.#ready(),
|
|
370
387
|
waitUntil: (promise) => this.ctx.waitUntil(promise),
|
|
371
388
|
containerGone: () => this.#containerGone(),
|
|
389
|
+
installedRecords: (tree) => this.#snapshot.dueEntries(tree),
|
|
390
|
+
installed: async () => void (await this.#snapshotDue.set(new Date())),
|
|
391
|
+
tag: () => this.#tag,
|
|
392
|
+
id: () => this.ctx.id.toString()
|
|
393
|
+
});
|
|
394
|
+
/** The next container's dependency trees — see `./deps-snapshot.ts`. */
|
|
395
|
+
#snapshot = new DepsSnapshot({
|
|
396
|
+
storage: this.ctx.storage,
|
|
397
|
+
container: () => this.ctx.container,
|
|
398
|
+
image: () => this.#image,
|
|
399
|
+
workspace: () => this.#workspace,
|
|
400
|
+
rootFingerprint: (dir) => this.#install.rootFingerprint(dir),
|
|
401
|
+
tree: () => this.#install.tree(),
|
|
372
402
|
tag: () => this.#tag,
|
|
373
403
|
id: () => this.ctx.id.toString()
|
|
374
404
|
});
|
|
@@ -426,13 +456,21 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
426
456
|
* The binding name, the egress policy and the instance size are the
|
|
427
457
|
* subclass's — see {@link WorkspaceObjectConfig}, which carries the warnings
|
|
428
458
|
* on each.
|
|
459
|
+
*
|
|
460
|
+
* `launch` is how a start restores a dependency snapshot instead of booting
|
|
461
|
+
* the image — see `./deps-snapshot.ts`. Never in the env: the env is part of
|
|
462
|
+
* what a running container is matched against.
|
|
429
463
|
*/
|
|
430
464
|
#backendMemo;
|
|
431
465
|
get backend() {
|
|
432
466
|
return (this.#backendMemo ??= this.#newBackend());
|
|
433
467
|
}
|
|
468
|
+
/** The image a start boots, as the deploy prepared it. */
|
|
469
|
+
get #image() {
|
|
470
|
+
return this.ctx.container?.images?.[IMAGE_NAME];
|
|
471
|
+
}
|
|
434
472
|
#newBackend() {
|
|
435
|
-
const image = this
|
|
473
|
+
const image = this.#image;
|
|
436
474
|
return new ContainerBackend({
|
|
437
475
|
container: () => this,
|
|
438
476
|
workspace: {
|
|
@@ -442,7 +480,8 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
442
480
|
egress: this.#cfg.egress,
|
|
443
481
|
name: IMAGE_NAME,
|
|
444
482
|
instance: this.#cfg.instance,
|
|
445
|
-
containerEnv: image === undefined ? {} : { [IMAGE_ENV]: image }
|
|
483
|
+
containerEnv: image === undefined ? {} : { [IMAGE_ENV]: image },
|
|
484
|
+
launch: this.#snapshot.launch
|
|
446
485
|
});
|
|
447
486
|
}
|
|
448
487
|
#workspaceMemo;
|
|
@@ -486,6 +525,7 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
486
525
|
this.#gitIdentity.forget();
|
|
487
526
|
this.#deps.forget();
|
|
488
527
|
await this.#install.containerGone();
|
|
528
|
+
await this.#snapshot.forgetDue();
|
|
489
529
|
}
|
|
490
530
|
/**
|
|
491
531
|
* Set once {@link reclaimIfIdle} has emptied storage, until the alarm it armed
|
|
@@ -538,19 +578,53 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
538
578
|
});
|
|
539
579
|
return this.#readying;
|
|
540
580
|
}
|
|
541
|
-
/**
|
|
581
|
+
/**
|
|
582
|
+
* One start, run for whoever got there first — see {@link #ready}.
|
|
583
|
+
*
|
|
584
|
+
* A cold start restores the dependency snapshot when one fits, and the marker
|
|
585
|
+
* the setup reads decides whether what came up is adopted as installed — see
|
|
586
|
+
* `./deps-snapshot.ts`.
|
|
587
|
+
*/
|
|
542
588
|
async #readyNow() {
|
|
543
589
|
// Read **before** `ready()`, which starts a stopped container.
|
|
544
|
-
|
|
590
|
+
let restoring;
|
|
591
|
+
if (!this.ctx.container?.running) {
|
|
545
592
|
await this.#containerGone();
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
await this.#
|
|
593
|
+
restoring = await this.#snapshot.arm(() => this.#expectedTree());
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
await this.#workspace.ready();
|
|
597
|
+
// Before the first exec, which pushes the whole tree into a new container.
|
|
598
|
+
await this.#purgeSyncedTrees();
|
|
599
|
+
await this.#trust.ensure();
|
|
600
|
+
await this.#gitIdentity.ensure();
|
|
601
|
+
const found = await this.#deps.ensure(restoring?.dir ?? (await this.#install.dir()));
|
|
602
|
+
if (found)
|
|
603
|
+
await this.#install.reconcile(found.marker);
|
|
604
|
+
if (restoring &&
|
|
605
|
+
(await this.#snapshot.restored(restoring, found?.marker)))
|
|
606
|
+
await this.#install.adopt(restoring);
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
this.#snapshot.disarm();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* The install a cold start would owe: where the last one ran, or the checkout
|
|
614
|
+
* when none has — a worktree seeded before its first install.
|
|
615
|
+
*/
|
|
616
|
+
async #expectedTree() {
|
|
617
|
+
const target = (await this.#install.target()) ?? (await this.#checkoutTarget());
|
|
618
|
+
if (!target)
|
|
619
|
+
return undefined;
|
|
620
|
+
const fingerprint = await this.#install.fingerprintFor(target);
|
|
621
|
+
return fingerprint ? { dir: target.dir, fingerprint } : undefined;
|
|
622
|
+
}
|
|
623
|
+
async #checkoutTarget() {
|
|
624
|
+
const record = await this.ctx.storage.get(CHECKOUT_KEY);
|
|
625
|
+
if (!record?.dir)
|
|
626
|
+
return undefined;
|
|
627
|
+
return { dir: record.dir, ...(record.repo ? { repo: record.repo } : {}) };
|
|
554
628
|
}
|
|
555
629
|
/**
|
|
556
630
|
* Delete every `node_modules` that was synced into this object, once.
|
|
@@ -834,6 +908,26 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
834
908
|
async startInstall(req) {
|
|
835
909
|
return await this.#install.start(req);
|
|
836
910
|
}
|
|
911
|
+
/**
|
|
912
|
+
* This workspace's dependency snapshot, for another of the same class to start
|
|
913
|
+
* from — see {@link seedDepsSnapshot}. Starts no container and moves no
|
|
914
|
+
* deadline.
|
|
915
|
+
*/
|
|
916
|
+
async depsSnapshot() {
|
|
917
|
+
return await this.#snapshot.get();
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Start this workspace's next container from `record`, another workspace's
|
|
921
|
+
* dependency snapshot, when it fits the checkout here — a worktree at the same
|
|
922
|
+
* path as the checkout it was cut from. Call it after the checkout and before
|
|
923
|
+
* the first command.
|
|
924
|
+
*
|
|
925
|
+
* A snapshot is that workspace's whole disk, so offer one only between
|
|
926
|
+
* workspaces of the same caller.
|
|
927
|
+
*/
|
|
928
|
+
async seedDepsSnapshot(record) {
|
|
929
|
+
return await this.#snapshot.seed(record, () => this.#expectedTree());
|
|
930
|
+
}
|
|
837
931
|
// --- lifecycle ---------------------------------------------------------------
|
|
838
932
|
/**
|
|
839
933
|
* Mark this workspace as in use, and push its reclamation back.
|
|
@@ -868,10 +962,9 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
868
962
|
*
|
|
869
963
|
* The counterpart to {@link reclaimIfIdle}, and the difference is the whole
|
|
870
964
|
* reason both exist. A reclaim is for a workspace nobody wants again: it empties
|
|
871
|
-
* storage, so the checkout and the dependency
|
|
872
|
-
* for a workspace that will be worked in again — the next task on the
|
|
873
|
-
* repository
|
|
874
|
-
* design is arranged around.
|
|
965
|
+
* storage, so the checkout and the dependency snapshot go with the container.
|
|
966
|
+
* This is for a workspace that will be worked in again — the next task on the
|
|
967
|
+
* same repository skips the clone, and the install by restoring the snapshot.
|
|
875
968
|
*
|
|
876
969
|
* **Why a caller needs this at all.** The idle deadline would eventually do it,
|
|
877
970
|
* but it cannot be tuned down to meet a cost target: `containerIdleMs` must
|
|
@@ -1041,6 +1134,12 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
1041
1134
|
reason,
|
|
1042
1135
|
running: this.ctx.container?.running ?? false
|
|
1043
1136
|
});
|
|
1137
|
+
// An install's snapshot the wake has not taken yet would go with the disk
|
|
1138
|
+
// it is of, so it is taken here first. Not for a reclaim, which keeps
|
|
1139
|
+
// nothing. Either way, one in flight needs the container running.
|
|
1140
|
+
if (reason === "idle")
|
|
1141
|
+
await this.#snapshot.takeDue();
|
|
1142
|
+
await this.#snapshot.settled();
|
|
1044
1143
|
// Hang up the session to computerd first: a destroy that cuts it live
|
|
1045
1144
|
// surfaces as an uncaught `Network connection lost`. The workspace
|
|
1046
1145
|
// reconnects on its next use, and `close()` swallows its own failures.
|
|
@@ -1305,6 +1404,21 @@ export class WorkspaceObjectBase extends WorkspaceContainerBase {
|
|
|
1305
1404
|
await this.#sync.arm((payload?.attempt ?? 0) + 1);
|
|
1306
1405
|
}
|
|
1307
1406
|
}
|
|
1407
|
+
/** An install finished in this container — see `./deps-snapshot.ts`. */
|
|
1408
|
+
async #onDepsSnapshot() {
|
|
1409
|
+
// The record is read from this object's storage — each root's inputs, and
|
|
1410
|
+
// which roots there are — so the install's own pull has to have landed: it
|
|
1411
|
+
// carries the submodules a bootstrap cloned. One still moving comes back;
|
|
1412
|
+
// one that failed is left to the stop, which drains before it takes.
|
|
1413
|
+
const synced = await this.#sync.drain(SYNC_DRAIN_BUDGET_MS);
|
|
1414
|
+
if (synced === "incomplete") {
|
|
1415
|
+
await this.#snapshotDue.set(new Date(Date.now() + SYNC_DRAIN_RESUME_MS));
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (synced === "failed")
|
|
1419
|
+
return;
|
|
1420
|
+
await this.#snapshot.takeDue();
|
|
1421
|
+
}
|
|
1308
1422
|
/** The idle-reclaim deadline came due. */
|
|
1309
1423
|
async #onIdleReclaim() {
|
|
1310
1424
|
const { reclaimed, idleMs } = await this.reclaimIfIdle();
|
package/dist/workspace/wake.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type WorkspaceWakeHandlers = {
|
|
|
14
14
|
idleReclaim: () => Promise<void>;
|
|
15
15
|
/** Start a cold container before a command needs it — see `./object.ts`. */
|
|
16
16
|
containerWarm: () => Promise<void>;
|
|
17
|
+
/** Snapshot a container that just installed — see `./deps-snapshot.ts`. */
|
|
18
|
+
depsSnapshot: () => Promise<void>;
|
|
17
19
|
containerIdle: (payload?: SyncDrainIntent) => Promise<void>;
|
|
18
20
|
syncRetry: (payload?: SyncDrainIntent) => Promise<void>;
|
|
19
21
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamicagents/plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Optional, composable capabilities for a Dynamic Agent: browser rendering, a Linux container, git and pull requests, a scratchpad, and Claude Code sessions. One subpath per plugin, so a bundle grows only with what it imports.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
"prepare": "husky || true && npm run build"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@cloudflare/computer": "^0.
|
|
79
|
-
"@cloudflare/think": ">=0.20.
|
|
80
|
-
"@dynamicagents/core": ">=0.
|
|
81
|
-
"@platformatic/vfs": "^0.
|
|
82
|
-
"agents": ">=0.
|
|
78
|
+
"@cloudflare/computer": "^0.5.0",
|
|
79
|
+
"@cloudflare/think": ">=0.20.2 <0.21.0",
|
|
80
|
+
"@dynamicagents/core": ">=0.14.0 <0.15.0",
|
|
81
|
+
"@platformatic/vfs": "^0.5.0",
|
|
82
|
+
"agents": ">=0.28.0 <0.29.0",
|
|
83
83
|
"ai": "^7.0.88",
|
|
84
84
|
"zod": "^4.4.3"
|
|
85
85
|
},
|
|
@@ -92,13 +92,13 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@cloudflare/computer": "^0.
|
|
96
|
-
"@cloudflare/think": "^0.20.
|
|
95
|
+
"@cloudflare/computer": "^0.5.0",
|
|
96
|
+
"@cloudflare/think": "^0.20.2",
|
|
97
97
|
"@cloudflare/vitest-plugin": "^1.1.9",
|
|
98
|
-
"@dynamicagents/core": "^0.
|
|
99
|
-
"@platformatic/vfs": "^0.
|
|
98
|
+
"@dynamicagents/core": "^0.14.0",
|
|
99
|
+
"@platformatic/vfs": "^0.5.0",
|
|
100
100
|
"@types/node": "^26.5.1",
|
|
101
|
-
"agents": "^0.
|
|
101
|
+
"agents": "^0.28.0",
|
|
102
102
|
"ai": "^7.0.101",
|
|
103
103
|
"eslint": "^10.10.0",
|
|
104
104
|
"husky": "^9.1.7",
|