@agent-native/core 0.211.0 → 0.211.1-nightly-20261020213022
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/cli/template-baseline.js +18 -5
- package/dist/cli/template-sync.js +100 -19
- package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
- package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/bounded-task-bundle.d.ts +17 -0
- package/dist/collab/bounded-task-bundle.js +39 -0
- package/dist/collab/bounded-task-source.d.ts +14 -0
- package/dist/collab/bounded-task-source.js +8 -0
- package/dist/collab/bounded-tasks.d.ts +67 -0
- package/dist/collab/bounded-tasks.js +439 -0
- package/dist/collab/index.d.ts +5 -2
- package/dist/collab/index.js +5 -2
- package/dist/collab/live-copy-tasks.d.ts +80 -0
- package/dist/collab/live-copy-tasks.js +330 -0
- package/dist/collab/live-copy.d.ts +188 -0
- package/dist/collab/live-copy.js +415 -0
- package/dist/collab/routes.d.ts +6 -1
- package/dist/collab/routes.js +24 -8
- package/dist/collab/storage.d.ts +53 -1
- package/dist/collab/storage.js +93 -5
- package/dist/collab/ydoc-manager.d.ts +92 -3
- package/dist/collab/ydoc-manager.js +447 -38
- package/dist/db/client.d.ts +6 -0
- package/dist/db/client.js +25 -5
- package/dist/deploy/build.js +2 -0
- package/dist/extensions/actions.js +1 -0
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/mcp/agent-connected.js +5 -1
- package/dist/mcp/build-server.js +39 -10
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/index.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/collab-plugin.d.ts +10 -1
- package/dist/server/collab-plugin.js +89 -20
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/optional-node-builtins.d.ts +1 -0
- package/dist/shared/optional-node-builtins.js +3 -0
- package/dist/tracking/providers.d.ts +1 -0
- package/dist/tracking/providers.js +2 -1
- package/dist/triggers/actions/manage-automation.d.ts +2 -2
- package/dist/vite/bounded-task-plugin.d.ts +22 -0
- package/dist/vite/bounded-task-plugin.js +44 -0
- package/dist/vite/client.js +2 -0
- package/dist/vitest-config.js +3 -0
- package/package.json +4 -3
|
@@ -10,6 +10,7 @@ const GIT_IDENTITY = {
|
|
|
10
10
|
GIT_COMMITTER_NAME: "agent-native",
|
|
11
11
|
GIT_COMMITTER_EMAIL: "noreply@agent-native.com",
|
|
12
12
|
};
|
|
13
|
+
const KEEP_LINE_ENDINGS = ["-c", "core.autocrlf=false", "-c", "core.eol=lf"];
|
|
13
14
|
export function resolveBaselineStore(appDir) {
|
|
14
15
|
const resolved = realpath(path.resolve(appDir));
|
|
15
16
|
const gitDir = findGitDir(resolved);
|
|
@@ -67,10 +68,15 @@ export function writeBaseline(store, sourceDir, slot, meta) {
|
|
|
67
68
|
copyPlainTree(sourceDir, target);
|
|
68
69
|
const indexFile = path.join(stage, "index");
|
|
69
70
|
const env = { ...process.env, ...GIT_IDENTITY, GIT_INDEX_FILE: indexFile };
|
|
70
|
-
git(store, [
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
git(store, [
|
|
72
|
+
...KEEP_LINE_ENDINGS,
|
|
73
|
+
"--work-tree",
|
|
74
|
+
stageRoot,
|
|
75
|
+
"add",
|
|
76
|
+
"-A",
|
|
77
|
+
"--force",
|
|
78
|
+
".",
|
|
79
|
+
], { cwd: stageRoot, env });
|
|
74
80
|
const tree = git(store, ["write-tree"], { cwd: stageRoot, env }).trim();
|
|
75
81
|
const ref = baselineRefName(store, slot);
|
|
76
82
|
const parent = revParse(store, ref);
|
|
@@ -142,7 +148,14 @@ export function materializeBaseline(store, slot) {
|
|
|
142
148
|
return null;
|
|
143
149
|
}
|
|
144
150
|
const tarPath = path.join(out, ".baseline.tar");
|
|
145
|
-
const args = [
|
|
151
|
+
const args = [
|
|
152
|
+
...KEEP_LINE_ENDINGS,
|
|
153
|
+
"archive",
|
|
154
|
+
"--format=tar",
|
|
155
|
+
"-o",
|
|
156
|
+
tarPath,
|
|
157
|
+
ref,
|
|
158
|
+
];
|
|
146
159
|
if (store.prefix)
|
|
147
160
|
args.push("--", store.prefix);
|
|
148
161
|
git(store, args);
|
|
@@ -149,12 +149,13 @@ export function mergeTemplateTrees(appDir, baseDir, theirsDir, options = {}) {
|
|
|
149
149
|
manual: [],
|
|
150
150
|
keptLocal: [],
|
|
151
151
|
};
|
|
152
|
-
const paths = new Set([
|
|
152
|
+
const paths = Array.from(new Set([
|
|
153
153
|
...listMergeableFiles(baseDir),
|
|
154
154
|
...listMergeableFiles(theirsDir),
|
|
155
155
|
...listMergeableFiles(appDir),
|
|
156
|
-
]);
|
|
157
|
-
|
|
156
|
+
])).sort();
|
|
157
|
+
const normalized = crlfNormalizedPaths(appDir, paths);
|
|
158
|
+
for (const rel of paths) {
|
|
158
159
|
const oursPath = path.join(appDir, rel);
|
|
159
160
|
if (isLocalSymlink(oursPath)) {
|
|
160
161
|
result.manual.push(`${rel} (local symlink)`);
|
|
@@ -162,7 +163,9 @@ export function mergeTemplateTrees(appDir, baseDir, theirsDir, options = {}) {
|
|
|
162
163
|
}
|
|
163
164
|
const base = readFileOrNull(path.join(baseDir, rel));
|
|
164
165
|
const theirs = readFileOrNull(path.join(theirsDir, rel));
|
|
165
|
-
const
|
|
166
|
+
const app = readAppFile(oursPath, normalized.has(rel) ? (base ?? theirs) : null);
|
|
167
|
+
const ours = app.content;
|
|
168
|
+
const writeOurs = (content) => writeFile(oursPath, app.crlf ? withCrlf(content) : content);
|
|
166
169
|
if (equalBuffers(base, theirs))
|
|
167
170
|
continue;
|
|
168
171
|
if (theirs === null) {
|
|
@@ -193,7 +196,7 @@ export function mergeTemplateTrees(appDir, baseDir, theirsDir, options = {}) {
|
|
|
193
196
|
continue;
|
|
194
197
|
if (equalBuffers(ours, base)) {
|
|
195
198
|
if (!options.dryRun)
|
|
196
|
-
|
|
199
|
+
writeOurs(theirs);
|
|
197
200
|
result.updated.push(rel);
|
|
198
201
|
continue;
|
|
199
202
|
}
|
|
@@ -206,8 +209,9 @@ export function mergeTemplateTrees(appDir, baseDir, theirsDir, options = {}) {
|
|
|
206
209
|
result.manual.push(`${rel} (merge failed)`);
|
|
207
210
|
continue;
|
|
208
211
|
}
|
|
209
|
-
if (!options.dryRun)
|
|
210
|
-
|
|
212
|
+
if (!options.dryRun && !equalBuffers(merged.content, ours)) {
|
|
213
|
+
writeOurs(merged.content);
|
|
214
|
+
}
|
|
211
215
|
if (merged.conflicts > 0)
|
|
212
216
|
result.conflicted.push(rel);
|
|
213
217
|
else
|
|
@@ -475,19 +479,21 @@ async function statusCommand(target, flags, io) {
|
|
|
475
479
|
try {
|
|
476
480
|
const local = countLocallyModified(target.appDir, baseDir);
|
|
477
481
|
lines.push(` locally modified files: ${local}`);
|
|
478
|
-
const targetRef = flags.to ?? target.provenance.templateRef ?? latest;
|
|
479
|
-
const theirs = await materializeForTarget(target, flags, targetRef);
|
|
480
482
|
try {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
+
const targetRef = flags.to ?? target.provenance.templateRef ?? latest;
|
|
484
|
+
const theirs = await materializeForTarget(target, flags, targetRef);
|
|
485
|
+
try {
|
|
486
|
+
const changed = countUpstreamChanged(baseDir, theirs.dir);
|
|
487
|
+
lines.push(` upstream-changed files: ${changed} (at ${theirs.ref})`);
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
fs.rmSync(theirs.dir, { recursive: true, force: true });
|
|
491
|
+
}
|
|
483
492
|
}
|
|
484
|
-
|
|
485
|
-
|
|
493
|
+
catch (err) {
|
|
494
|
+
lines.push(` upstream-changed files: unavailable (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
|
|
486
495
|
}
|
|
487
496
|
}
|
|
488
|
-
catch (err) {
|
|
489
|
-
lines.push(` upstream-changed files: unavailable (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`);
|
|
490
|
-
}
|
|
491
497
|
finally {
|
|
492
498
|
fs.rmSync(baseDir, { recursive: true, force: true });
|
|
493
499
|
}
|
|
@@ -648,13 +654,19 @@ function backfillProvenance(target, template, materialized) {
|
|
|
648
654
|
shape: target.shape,
|
|
649
655
|
};
|
|
650
656
|
pkg["agent-native"] = agentNative;
|
|
651
|
-
|
|
657
|
+
const json = Buffer.from(`${JSON.stringify(pkg, null, 2)}\n`);
|
|
658
|
+
const app = readAppFile(pkgPath, json);
|
|
659
|
+
if (equalBuffers(app.content, json))
|
|
660
|
+
return;
|
|
661
|
+
fs.writeFileSync(pkgPath, app.crlf ? withCrlf(json) : json);
|
|
652
662
|
}
|
|
653
663
|
function countLocallyModified(appDir, baseDir) {
|
|
654
664
|
let count = 0;
|
|
655
|
-
|
|
665
|
+
const paths = Array.from(listMergeableFiles(baseDir));
|
|
666
|
+
const normalized = crlfNormalizedPaths(appDir, paths);
|
|
667
|
+
for (const rel of paths) {
|
|
656
668
|
const base = readFileOrNull(path.join(baseDir, rel));
|
|
657
|
-
const ours =
|
|
669
|
+
const ours = readAppFile(path.join(appDir, rel), normalized.has(rel) ? base : null).content;
|
|
658
670
|
if (!equalBuffers(base, ours))
|
|
659
671
|
count++;
|
|
660
672
|
}
|
|
@@ -809,6 +821,75 @@ function equalBuffers(a, b) {
|
|
|
809
821
|
return a === b;
|
|
810
822
|
return a.equals(b);
|
|
811
823
|
}
|
|
824
|
+
// CRLF that git normalizes on add is not a local edit.
|
|
825
|
+
function readAppFile(file, template) {
|
|
826
|
+
const content = readFileOrNull(file);
|
|
827
|
+
if (content === null ||
|
|
828
|
+
template === null ||
|
|
829
|
+
template.includes(0x0d) ||
|
|
830
|
+
!isTextBuffer(content) ||
|
|
831
|
+
!isTextBuffer(template)) {
|
|
832
|
+
return { content, crlf: false };
|
|
833
|
+
}
|
|
834
|
+
const text = content.toString("utf-8");
|
|
835
|
+
if (!text.includes("\r\n") || /\r(?!\n)/.test(text)) {
|
|
836
|
+
return { content, crlf: false };
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
content: Buffer.from(text.replace(/\r\n/g, "\n")),
|
|
840
|
+
crlf: !/(^|[^\r])\n/.test(text),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
// Paths whose CRLF git turns into LF on add; anywhere else a CR is content.
|
|
844
|
+
function crlfNormalizedPaths(appDir, rels) {
|
|
845
|
+
const normalized = new Set();
|
|
846
|
+
if (!resolveBaselineStore(appDir).gitDir)
|
|
847
|
+
return normalized;
|
|
848
|
+
const res = spawnSync("git", ["check-attr", "-z", "--stdin", "text", "crlf", "eol"], {
|
|
849
|
+
cwd: appDir,
|
|
850
|
+
input: rels.join("\0"),
|
|
851
|
+
encoding: "utf-8",
|
|
852
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
853
|
+
});
|
|
854
|
+
if (res.status !== 0) {
|
|
855
|
+
throw new Error(`git check-attr failed in ${appDir}: ${res.stderr || res.error?.message}`);
|
|
856
|
+
}
|
|
857
|
+
const config = spawnSync("git", ["config", "--get", "core.autocrlf"], {
|
|
858
|
+
cwd: appDir,
|
|
859
|
+
encoding: "utf-8",
|
|
860
|
+
});
|
|
861
|
+
if (config.status !== 0 && config.status !== 1) {
|
|
862
|
+
throw new Error(`git config core.autocrlf failed in ${appDir}: ${config.stderr || config.error?.message}`);
|
|
863
|
+
}
|
|
864
|
+
const byConfig = ["true", "yes", "on", "1", "input"].includes(config.stdout.trim().toLowerCase());
|
|
865
|
+
const attrs = new Map();
|
|
866
|
+
const fields = res.stdout.split("\0");
|
|
867
|
+
for (let i = 0; i + 2 < fields.length; i += 3) {
|
|
868
|
+
attrs.set(fields[i], {
|
|
869
|
+
...attrs.get(fields[i]),
|
|
870
|
+
[fields[i + 1]]: fields[i + 2],
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
for (const [rel, { text, crlf, eol }] of attrs) {
|
|
874
|
+
const byAttrs = conversion(text) ??
|
|
875
|
+
conversion(crlf) ??
|
|
876
|
+
(eol === "lf" || eol === "crlf" ? true : null);
|
|
877
|
+
if (byAttrs ?? byConfig)
|
|
878
|
+
normalized.add(rel);
|
|
879
|
+
}
|
|
880
|
+
return normalized;
|
|
881
|
+
}
|
|
882
|
+
// A text or crlf attribute as git reads it: convert, keep bytes, or no say.
|
|
883
|
+
function conversion(value) {
|
|
884
|
+
if (value === "set" || value === "auto" || value === "input")
|
|
885
|
+
return true;
|
|
886
|
+
return value === "unset" ? false : null;
|
|
887
|
+
}
|
|
888
|
+
function withCrlf(content) {
|
|
889
|
+
if (!isTextBuffer(content))
|
|
890
|
+
return content;
|
|
891
|
+
return Buffer.from(content.toString("utf-8").replace(/\r?\n/g, "\r\n"));
|
|
892
|
+
}
|
|
812
893
|
function isTextBuffer(buf) {
|
|
813
894
|
if (buf === null)
|
|
814
895
|
return true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const tasks: Record<string, (input: never) => Promise<import("../live-copy-tasks.js").LiveCopyTaskResult>>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { defineLiveCopyTasks } from "../live-copy-tasks.js";
|
|
3
|
+
// A tiny live-copy task module for the live-copy spec: a body is a list of
|
|
4
|
+
// paragraphs, and anything else is refused rather than dropped. Paragraphs
|
|
5
|
+
// and seed sources starting with "!" drive the failure cases.
|
|
6
|
+
function paragraph(text) {
|
|
7
|
+
const element = new Y.XmlElement("paragraph");
|
|
8
|
+
const content = new Y.XmlText();
|
|
9
|
+
content.insert(0, text);
|
|
10
|
+
element.insert(0, [content]);
|
|
11
|
+
return element;
|
|
12
|
+
}
|
|
13
|
+
/** An update whose base this document never had. */
|
|
14
|
+
function orphanUpdate() {
|
|
15
|
+
const elsewhere = new Y.Doc();
|
|
16
|
+
elsewhere.getXmlFragment("default").insert(0, [paragraph("unknown base")]);
|
|
17
|
+
const before = Y.encodeStateVector(elsewhere);
|
|
18
|
+
const fragment = elsewhere.getXmlFragment("default");
|
|
19
|
+
fragment.insert(1, [paragraph("orphan")]);
|
|
20
|
+
return Y.encodeStateAsUpdate(elsewhere, before);
|
|
21
|
+
}
|
|
22
|
+
export const tasks = defineLiveCopyTasks({
|
|
23
|
+
derive(doc, field) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
for (const node of doc.getXmlFragment(field).toArray()) {
|
|
26
|
+
if (!(node instanceof Y.XmlElement) || node.nodeName !== "paragraph") {
|
|
27
|
+
return {
|
|
28
|
+
kind: "refused",
|
|
29
|
+
reason: "unsupported_node",
|
|
30
|
+
detail: node instanceof Y.XmlElement ? node.nodeName : "text",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const text = node
|
|
34
|
+
.toArray()
|
|
35
|
+
.map((child) => child.toString())
|
|
36
|
+
.join("");
|
|
37
|
+
if (text === "!spin") {
|
|
38
|
+
for (;;) {
|
|
39
|
+
// never returns
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (text === "!html")
|
|
43
|
+
return { kind: "html" };
|
|
44
|
+
if (text === "!edit")
|
|
45
|
+
doc.getXmlFragment(field).insert(0, [paragraph("")]);
|
|
46
|
+
if (text === "!park")
|
|
47
|
+
Y.applyUpdate(doc, orphanUpdate());
|
|
48
|
+
lines.push(text);
|
|
49
|
+
}
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
},
|
|
52
|
+
seed(doc, field, source) {
|
|
53
|
+
if (source === "!refuse")
|
|
54
|
+
return { kind: "refused", reason: "too_complex" };
|
|
55
|
+
if (source === "!empty")
|
|
56
|
+
return;
|
|
57
|
+
if (source === "!junk")
|
|
58
|
+
return { kind: "update" };
|
|
59
|
+
if (source === "!stray")
|
|
60
|
+
doc.getText("other").insert(0, "hidden");
|
|
61
|
+
doc.getXmlFragment(field).insert(0, source.split("\n").map(paragraph));
|
|
62
|
+
},
|
|
63
|
+
edits: {
|
|
64
|
+
append(doc, field, input) {
|
|
65
|
+
const fragment = doc.getXmlFragment(field);
|
|
66
|
+
const text = input?.text ?? "";
|
|
67
|
+
fragment.insert(fragment.length, [paragraph(text)]);
|
|
68
|
+
},
|
|
69
|
+
clear(doc, field) {
|
|
70
|
+
const fragment = doc.getXmlFragment(field);
|
|
71
|
+
fragment.delete(0, fragment.length);
|
|
72
|
+
},
|
|
73
|
+
stray(doc) {
|
|
74
|
+
doc.getText("other").insert(0, "hidden");
|
|
75
|
+
},
|
|
76
|
+
unchanged() { },
|
|
77
|
+
refuse() {
|
|
78
|
+
return { kind: "refused", reason: "too_complex" };
|
|
79
|
+
},
|
|
80
|
+
junk() {
|
|
81
|
+
return { kind: "update" };
|
|
82
|
+
},
|
|
83
|
+
dependent(doc) {
|
|
84
|
+
Y.applyUpdate(doc, orphanUpdate());
|
|
85
|
+
},
|
|
86
|
+
dependentAndValid(doc, field) {
|
|
87
|
+
Y.applyUpdate(doc, orphanUpdate());
|
|
88
|
+
const fragment = doc.getXmlFragment(field);
|
|
89
|
+
fragment.insert(fragment.length, [paragraph("valid")]);
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
@@ -38,19 +38,19 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
38
38
|
error: string;
|
|
39
39
|
states?: undefined;
|
|
40
40
|
} | {
|
|
41
|
-
error?: undefined;
|
|
42
41
|
states: {
|
|
43
42
|
clientId: number;
|
|
44
43
|
state: string;
|
|
45
44
|
}[];
|
|
45
|
+
error?: undefined;
|
|
46
46
|
}>>;
|
|
47
47
|
export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
48
48
|
error: string;
|
|
49
49
|
users?: undefined;
|
|
50
50
|
} | {
|
|
51
|
-
error?: undefined;
|
|
52
51
|
users: {
|
|
53
52
|
clientId: number;
|
|
54
53
|
lastSeen: number;
|
|
55
54
|
}[];
|
|
55
|
+
error?: undefined;
|
|
56
56
|
}>>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type BoundedTaskSource } from "./bounded-task-source.js";
|
|
2
|
+
export interface BundleBoundedTaskSourceOptions {
|
|
3
|
+
/** Shown in errors and runner names. Defaults to the entry file's name. */
|
|
4
|
+
name?: string;
|
|
5
|
+
/** Extra esbuild `conditions` for package resolution. */
|
|
6
|
+
conditions?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface BundledBoundedTaskSource extends BoundedTaskSource {
|
|
9
|
+
/** Every file esbuild read, for watch-mode rebuilds. */
|
|
10
|
+
readonly inputs: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Bundles a task module and everything it imports into one script a worker
|
|
14
|
+
* can evaluate. The module must export `tasks`: an object of functions that
|
|
15
|
+
* take plain data and return plain data (or a Promise of it).
|
|
16
|
+
*/
|
|
17
|
+
export declare function bundleBoundedTaskSource(entry: string | URL, options?: BundleBoundedTaskSourceOptions): Promise<BundledBoundedTaskSource>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { BOUNDED_TASK_GLOBAL, } from "./bounded-task-source.js";
|
|
4
|
+
/**
|
|
5
|
+
* Bundles a task module and everything it imports into one script a worker
|
|
6
|
+
* can evaluate. The module must export `tasks`: an object of functions that
|
|
7
|
+
* take plain data and return plain data (or a Promise of it).
|
|
8
|
+
*/
|
|
9
|
+
export async function bundleBoundedTaskSource(entry, options = {}) {
|
|
10
|
+
const entryPath = entry instanceof URL || entry.startsWith("file:")
|
|
11
|
+
? fileURLToPath(entry)
|
|
12
|
+
: path.resolve(entry);
|
|
13
|
+
const esbuild = await import("esbuild");
|
|
14
|
+
const result = await esbuild.build({
|
|
15
|
+
entryPoints: [entryPath],
|
|
16
|
+
bundle: true,
|
|
17
|
+
write: false,
|
|
18
|
+
format: "iife",
|
|
19
|
+
globalName: BOUNDED_TASK_GLOBAL,
|
|
20
|
+
platform: "node",
|
|
21
|
+
target: "node20",
|
|
22
|
+
metafile: true,
|
|
23
|
+
sourcemap: false,
|
|
24
|
+
legalComments: "none",
|
|
25
|
+
logLevel: "silent",
|
|
26
|
+
...(options.conditions ? { conditions: options.conditions } : {}),
|
|
27
|
+
});
|
|
28
|
+
const output = result.outputFiles?.[0];
|
|
29
|
+
if (!output) {
|
|
30
|
+
throw new Error(`esbuild produced no output for bounded task ${entryPath}`);
|
|
31
|
+
}
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
return {
|
|
34
|
+
kind: "bounded-task-source",
|
|
35
|
+
name: options.name ?? path.basename(entryPath).replace(/\.[^.]+$/, ""),
|
|
36
|
+
code: output.text,
|
|
37
|
+
inputs: Object.keys(result.metafile?.inputs ?? {}).map((input) => path.resolve(cwd, input)),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identifier the bundled task module is assigned to. `bundleBoundedTaskSource`
|
|
3
|
+
* emits an IIFE with this global name; the worker host reads it back.
|
|
4
|
+
*/
|
|
5
|
+
export declare const BOUNDED_TASK_GLOBAL = "__agentNativeBoundedTasks";
|
|
6
|
+
/**
|
|
7
|
+
* A self-contained task module, bundled to one script. Produce it with
|
|
8
|
+
* `bundleBoundedTaskSource` (Node, esbuild); never hand-write `code`.
|
|
9
|
+
*/
|
|
10
|
+
export interface BoundedTaskSource {
|
|
11
|
+
readonly kind: "bounded-task-source";
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly code: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The bundler and the vitest base config import this, not bounded-tasks.ts:
|
|
2
|
+
// packages that typecheck @agent-native/core/vitest-config from source load
|
|
3
|
+
// several @types/node versions, and node:worker_threads does not type there.
|
|
4
|
+
/**
|
|
5
|
+
* Identifier the bundled task module is assigned to. `bundleBoundedTaskSource`
|
|
6
|
+
* emits an IIFE with this global name; the worker host reads it back.
|
|
7
|
+
*/
|
|
8
|
+
export const BOUNDED_TASK_GLOBAL = "__agentNativeBoundedTasks";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { BOUNDED_TASK_GLOBAL, type BoundedTaskSource } from "./bounded-task-source.js";
|
|
2
|
+
export { BOUNDED_TASK_GLOBAL, type BoundedTaskSource };
|
|
3
|
+
export type BoundedTaskErrorCode = "unavailable" | "closed" | "queue_full" | "queue_timeout" | "invalid_input" | "input_too_large" | "startup_failed" | "task_unknown" | "task_threw" | "deadline_exceeded" | "memory_exceeded" | "invalid_output" | "output_too_large" | "worker_crashed";
|
|
4
|
+
export declare class BoundedTaskError extends Error {
|
|
5
|
+
readonly code: BoundedTaskErrorCode;
|
|
6
|
+
readonly task: string;
|
|
7
|
+
/** `name` of the error the task threw, for `task_threw`. */
|
|
8
|
+
readonly taskErrorName?: string;
|
|
9
|
+
constructor(code: BoundedTaskErrorCode, task: string, message: string, options?: {
|
|
10
|
+
cause?: unknown;
|
|
11
|
+
taskErrorName?: string;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export interface BoundedTaskRunnerOptions {
|
|
15
|
+
source: BoundedTaskSource;
|
|
16
|
+
/** Wall-clock budget for one task once a worker has it. Default 2000 ms. */
|
|
17
|
+
deadlineMs?: number;
|
|
18
|
+
/** Budget for a fresh worker to evaluate the bundle. Default 10000 ms. */
|
|
19
|
+
startupTimeoutMs?: number;
|
|
20
|
+
/** Workers alive at once; also the number of tasks running at once. Default 1. */
|
|
21
|
+
maxWorkers?: number;
|
|
22
|
+
/** Tasks allowed to wait for a worker. Beyond this, `queue_full`. Default 8. */
|
|
23
|
+
maxQueue?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Longest a task may wait for a busy or terminating worker. A task waiting
|
|
26
|
+
* only for a worker to start is bounded by `startupTimeoutMs` instead.
|
|
27
|
+
* Default `deadlineMs`.
|
|
28
|
+
*/
|
|
29
|
+
queueTimeoutMs?: number;
|
|
30
|
+
/** Strings count two bytes per UTF-16 unit. Default 8 MiB. */
|
|
31
|
+
maxInputBytes?: number;
|
|
32
|
+
/** Checked inside the worker before the result is copied out. Default 16 MiB. */
|
|
33
|
+
maxOutputBytes?: number;
|
|
34
|
+
/** Worker old-generation heap. Default 128 MiB. */
|
|
35
|
+
memoryLimitMb?: number;
|
|
36
|
+
/** Default 4 MiB. */
|
|
37
|
+
stackSizeMb?: number;
|
|
38
|
+
/** An idle worker is stopped after this long. Default 30000 ms. */
|
|
39
|
+
idleTimeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
export interface BoundedTaskRunnerStats {
|
|
42
|
+
workers: number;
|
|
43
|
+
running: number;
|
|
44
|
+
queued: number;
|
|
45
|
+
}
|
|
46
|
+
export interface BoundedTaskRunner {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
run<Output = unknown>(task: string, input: unknown): Promise<Output>;
|
|
49
|
+
stats(): BoundedTaskRunnerStats;
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Size of a structured-clone payload in bytes, or -1 when it holds anything
|
|
54
|
+
* but plain data (or more than 200,000 values). Stops counting once
|
|
55
|
+
* `limit` is passed. The worker host evaluates this function's source, so it
|
|
56
|
+
* must not reference anything outside its own body.
|
|
57
|
+
*/
|
|
58
|
+
export declare function measureBoundedPayload(value: unknown, limit: number): number;
|
|
59
|
+
/**
|
|
60
|
+
* Runs functions from one bundled module in worker threads, each under a
|
|
61
|
+
* terminable deadline and a heap limit, with a fixed number of workers and a
|
|
62
|
+
* bounded wait queue. A task that overruns is stopped by terminating its
|
|
63
|
+
* worker, so synchronous work in the task never blocks the caller's event
|
|
64
|
+
* loop. Where worker threads do not exist (Cloudflare Workers), every run
|
|
65
|
+
* fails with `unavailable`; it never falls back to running inline.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createBoundedTaskRunner(options: BoundedTaskRunnerOptions): BoundedTaskRunner;
|