@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20
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/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +1 -3
- package/akanApp/akanApp.host.ts +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/devWatchBatch.test.ts +14 -18
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/gritRules.test.ts +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { codeAgentPresets } from "akanjs/common";
|
|
4
|
-
import {
|
|
4
|
+
import { makeCliTempWorkspace, tempRoots, writeText } from "../../testHelpers";
|
|
5
|
+
import { CodeAgent, type CodeAgentOptions } from "../agent/CodeAgent";
|
|
5
6
|
import { SubagentPool } from "../agent/SubagentPool";
|
|
6
7
|
import { DevLogFeedback } from "../feedback/DevLogFeedback";
|
|
7
8
|
import { PreviewView } from "../feedback/PreviewView";
|
|
8
9
|
import { TurnFeedback, type TurnFeedbackSource } from "../feedback/TurnFeedback";
|
|
10
|
+
import { AkanCodePlugins } from "./AkanCodePlugins";
|
|
9
11
|
import { AkanEditScope } from "./AkanEditScope";
|
|
10
12
|
import { AkanEnvKeys } from "./AkanEnvKeys";
|
|
11
13
|
import { AkanVerifier } from "./AkanVerifier";
|
|
@@ -14,11 +16,22 @@ import { McpToolPack } from "./McpToolPack";
|
|
|
14
16
|
import { SessionToolPack } from "./SessionToolPack";
|
|
15
17
|
import { WebToolPack } from "./WebToolPack";
|
|
16
18
|
|
|
17
|
-
const
|
|
19
|
+
const track = tempRoots();
|
|
20
|
+
const tempRoot = async () => track(await makeCliTempWorkspace()).root;
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
type ToolExecute = (
|
|
23
|
+
id: string,
|
|
24
|
+
params: never,
|
|
25
|
+
signal?: AbortSignal,
|
|
26
|
+
) => Promise<{ content: { text: string }[]; isError?: boolean }>;
|
|
27
|
+
|
|
28
|
+
const toolsOf = (extension: unknown) => {
|
|
29
|
+
const tools = new Map<string, ToolExecute>();
|
|
30
|
+
(extension as { factory: (pi: unknown) => void }).factory({
|
|
31
|
+
registerTool: (tool: { name: string; execute: ToolExecute }) => tools.set(tool.name, tool.execute),
|
|
32
|
+
});
|
|
33
|
+
return tools;
|
|
34
|
+
};
|
|
22
35
|
|
|
23
36
|
const source = (key: string, findings: (string | undefined)[]): TurnFeedbackSource => {
|
|
24
37
|
let call = 0;
|
|
@@ -52,8 +65,6 @@ describe("TurnFeedback", () => {
|
|
|
52
65
|
expect(await feedback.collect()).toContain("still broken");
|
|
53
66
|
expect(await feedback.collect()).toContain("still broken");
|
|
54
67
|
expect(await feedback.collect()).toBeUndefined();
|
|
55
|
-
// Every reopen is announced: a turn nobody asked for looks, on the transcript, like an agent that will
|
|
56
|
-
// not stop — more tool calls after the answer and nothing naming who sent it back to work.
|
|
57
68
|
expect(notices[0]).toContain("a reopened the turn");
|
|
58
69
|
expect(notices.at(-1)).toContain("still failing after 2 attempts");
|
|
59
70
|
});
|
|
@@ -136,8 +147,7 @@ describe("AkanVerifier.summarize", () => {
|
|
|
136
147
|
|
|
137
148
|
describe("AkanEditScope", () => {
|
|
138
149
|
test("derives apps, libs, sync need and tsx from a git status diff", async () => {
|
|
139
|
-
const
|
|
140
|
-
tempRoots.push(root);
|
|
150
|
+
const root = await tempRoot();
|
|
141
151
|
await Bun.spawn(["git", "init", "-q"], { cwd: root, stdout: "ignore", stderr: "ignore" }).exited;
|
|
142
152
|
const baseline = await AkanEditScope.baseline(root);
|
|
143
153
|
await writeText(path.join(root, "apps", "demo", "lib", "post", "post.service.ts"), "export {};\n");
|
|
@@ -150,8 +160,7 @@ describe("AkanEditScope", () => {
|
|
|
150
160
|
});
|
|
151
161
|
|
|
152
162
|
test("an unchanged tree implicates no target", async () => {
|
|
153
|
-
const
|
|
154
|
-
tempRoots.push(root);
|
|
163
|
+
const root = await tempRoot();
|
|
155
164
|
await Bun.spawn(["git", "init", "-q"], { cwd: root, stdout: "ignore", stderr: "ignore" }).exited;
|
|
156
165
|
const baseline = await AkanEditScope.baseline(root);
|
|
157
166
|
const scope = await AkanEditScope.since(root, baseline);
|
|
@@ -162,10 +171,6 @@ describe("AkanEditScope", () => {
|
|
|
162
171
|
describe("DevLogFeedback", () => {
|
|
163
172
|
const logPath = (root: string, app: string) => path.join(root, "local", "apps", app, "runtime", "dev.log");
|
|
164
173
|
|
|
165
|
-
/**
|
|
166
|
-
* A turn that wrote nothing cannot have broken the running app, so the source is gated on the working tree
|
|
167
|
-
* moving — which makes a repo with one edit in it the minimum a test has to set up.
|
|
168
|
-
*/
|
|
169
174
|
const armed = async (root: string, feedback: DevLogFeedback) => {
|
|
170
175
|
Bun.spawnSync(["git", "init", "-q"], { cwd: root });
|
|
171
176
|
await feedback.begin();
|
|
@@ -173,8 +178,7 @@ describe("DevLogFeedback", () => {
|
|
|
173
178
|
};
|
|
174
179
|
|
|
175
180
|
test("reports only the errors written after the watermark", async () => {
|
|
176
|
-
const
|
|
177
|
-
tempRoots.push(root);
|
|
181
|
+
const root = await tempRoot();
|
|
178
182
|
await writeText(logPath(root, "demo"), "[demo] boot ok\n");
|
|
179
183
|
const feedback = new DevLogFeedback({ workspaceRoot: root, cwd: root, apps: ["demo"], graceMs: 0 });
|
|
180
184
|
await armed(root, feedback);
|
|
@@ -185,8 +189,7 @@ describe("DevLogFeedback", () => {
|
|
|
185
189
|
});
|
|
186
190
|
|
|
187
191
|
test("stays quiet when the new lines are only debug noise", async () => {
|
|
188
|
-
const
|
|
189
|
-
tempRoots.push(root);
|
|
192
|
+
const root = await tempRoot();
|
|
190
193
|
await writeText(logPath(root, "demo"), "start\n");
|
|
191
194
|
const feedback = new DevLogFeedback({ workspaceRoot: root, cwd: root, apps: ["demo"], graceMs: 0 });
|
|
192
195
|
await armed(root, feedback);
|
|
@@ -195,16 +198,14 @@ describe("DevLogFeedback", () => {
|
|
|
195
198
|
});
|
|
196
199
|
|
|
197
200
|
test("no dev server means nothing to say, not an error", async () => {
|
|
198
|
-
const
|
|
199
|
-
tempRoots.push(root);
|
|
201
|
+
const root = await tempRoot();
|
|
200
202
|
const feedback = new DevLogFeedback({ workspaceRoot: root, cwd: root, apps: ["demo"], graceMs: 0 });
|
|
201
203
|
await armed(root, feedback);
|
|
202
204
|
expect(await feedback.observe()).toBeUndefined();
|
|
203
205
|
});
|
|
204
206
|
|
|
205
207
|
test("a turn that changed nothing is not told about a dev server somebody else is driving", async () => {
|
|
206
|
-
const
|
|
207
|
-
tempRoots.push(root);
|
|
208
|
+
const root = await tempRoot();
|
|
208
209
|
Bun.spawnSync(["git", "init", "-q"], { cwd: root });
|
|
209
210
|
await writeText(logPath(root, "demo"), "[demo] boot ok\n");
|
|
210
211
|
const feedback = new DevLogFeedback({ workspaceRoot: root, cwd: root, apps: ["demo"], graceMs: 0 });
|
|
@@ -214,8 +215,7 @@ describe("DevLogFeedback", () => {
|
|
|
214
215
|
});
|
|
215
216
|
|
|
216
217
|
test("reads the dev server's allocated URL back out of its own log", async () => {
|
|
217
|
-
const
|
|
218
|
-
tempRoots.push(root);
|
|
218
|
+
const root = await tempRoot();
|
|
219
219
|
await writeText(
|
|
220
220
|
logPath(root, "demo"),
|
|
221
221
|
"[demo] demo ready (pid=1) — http://localhost:3000\n[demo] demo ready (pid=2) — http://localhost:8282\n",
|
|
@@ -227,8 +227,7 @@ describe("DevLogFeedback", () => {
|
|
|
227
227
|
|
|
228
228
|
describe("AkanEnvKeys", () => {
|
|
229
229
|
test("maps <PROVIDER>_API_KEY onto the engine's provider id without persisting it", async () => {
|
|
230
|
-
const
|
|
231
|
-
tempRoots.push(root);
|
|
230
|
+
const root = await tempRoot();
|
|
232
231
|
await writeText(path.join(root, ".env"), '# comment\nDEEPSEEK_API_KEY="sk-test"\nSOMETHING_ELSE=1\n');
|
|
233
232
|
const applied: [string, string][] = [];
|
|
234
233
|
const fake = {
|
|
@@ -295,16 +294,11 @@ describe("SubagentPool", () => {
|
|
|
295
294
|
test("a code child keeps the parent's tools, and never more than them", () => {
|
|
296
295
|
const full = SubagentPool.childOf(codeAgentPresets.local("/tmp/akan"), "code");
|
|
297
296
|
expect(full.tools.builtin).toContain("write");
|
|
298
|
-
// A read-only parent stays read-only through the kind that is allowed to write: narrowing only narrows.
|
|
299
297
|
const reader = SubagentPool.childOf(codeAgentPresets.review("/tmp/akan"), "code");
|
|
300
298
|
expect(reader.tools.builtin).not.toContain("write");
|
|
301
299
|
expect(reader.tools.builtin).not.toContain("bash");
|
|
302
300
|
});
|
|
303
301
|
|
|
304
|
-
/**
|
|
305
|
-
* A refused call opens no child, so it must leave no row behind either — a rail that shows a sub-agent the
|
|
306
|
-
* pool never started reads as work in flight, and the budget it was refused over says the opposite.
|
|
307
|
-
*/
|
|
308
302
|
test("a refusal reports no running sub-agent", async () => {
|
|
309
303
|
const parent = codeAgentPresets.local("/tmp/akan");
|
|
310
304
|
const spent = { ...parent, tools: { ...parent.tools, subagent: { maxConcurrent: 0, maxDepth: 2, budget: 0 } } };
|
|
@@ -324,7 +318,202 @@ describe("SubagentPool", () => {
|
|
|
324
318
|
expect(frames).toEqual([]);
|
|
325
319
|
});
|
|
326
320
|
|
|
327
|
-
|
|
321
|
+
const budgeted = (budget: number, maxConcurrent = 3) => {
|
|
322
|
+
const parent = codeAgentPresets.local("/tmp/akan");
|
|
323
|
+
return {
|
|
324
|
+
...parent,
|
|
325
|
+
tools: { ...parent.tools, mcp: "off" as const, subagent: { maxConcurrent, maxDepth: 2, budget } },
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
const openFakeAgents = (tokens: number[]) => {
|
|
329
|
+
const opened: CodeAgentOptions[] = [];
|
|
330
|
+
const spy = spyOn(CodeAgent, "create").mockImplementation(async (options) => {
|
|
331
|
+
opened.push(options);
|
|
332
|
+
const tokensUsed = tokens[opened.length - 1] ?? 0;
|
|
333
|
+
return {
|
|
334
|
+
on: () => {},
|
|
335
|
+
prompt: async () => {},
|
|
336
|
+
waitForIdle: async () => {},
|
|
337
|
+
dispose: () => {},
|
|
338
|
+
tokensUsed,
|
|
339
|
+
} as never;
|
|
340
|
+
});
|
|
341
|
+
return { opened, spy };
|
|
342
|
+
};
|
|
343
|
+
const runTask = async (extension: unknown, id: string, signal?: AbortSignal) =>
|
|
344
|
+
await toolsOf(extension).get("task")?.(id, { description: "d", prompt: "p" } as never, signal);
|
|
345
|
+
const until = async (done: () => boolean) => {
|
|
346
|
+
for (let tries = 0; tries < 500 && !done(); tries += 1) await Bun.sleep(2);
|
|
347
|
+
};
|
|
348
|
+
const poolOf = (child: CodeAgentOptions) =>
|
|
349
|
+
SubagentPool.extensionFor({
|
|
350
|
+
workspace,
|
|
351
|
+
cwd: "/tmp/akan",
|
|
352
|
+
parent: child.profile,
|
|
353
|
+
depth: child.depth ?? 0,
|
|
354
|
+
...(child.subagentSpend ? { spend: child.subagentSpend } : {}),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("every pool of one tree draws on one token budget", async () => {
|
|
358
|
+
const { opened, spy } = openFakeAgents([150, 100]);
|
|
359
|
+
try {
|
|
360
|
+
const root = SubagentPool.extensionFor({ workspace, cwd: "/tmp/akan", parent: budgeted(200), depth: 0 });
|
|
361
|
+
expect((await runTask(root, "c1"))?.isError).toBeUndefined();
|
|
362
|
+
const [child] = opened;
|
|
363
|
+
if (!child) throw new Error("no sub-agent was opened");
|
|
364
|
+
const inner = SubagentPool.extensionFor({
|
|
365
|
+
workspace,
|
|
366
|
+
cwd: "/tmp/akan",
|
|
367
|
+
parent: child.profile,
|
|
368
|
+
depth: child.depth ?? 0,
|
|
369
|
+
...(child.subagentSpend ? { spend: child.subagentSpend } : {}),
|
|
370
|
+
});
|
|
371
|
+
expect((await runTask(inner, "c2"))?.isError).toBeUndefined();
|
|
372
|
+
for (const pool of [root, inner]) {
|
|
373
|
+
const refused = await runTask(pool, "c3");
|
|
374
|
+
expect(refused?.isError).toBe(true);
|
|
375
|
+
expect(refused?.content[0]?.text).toContain("token budget (200) is spent");
|
|
376
|
+
}
|
|
377
|
+
expect(opened).toHaveLength(2);
|
|
378
|
+
} finally {
|
|
379
|
+
spy.mockRestore();
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("the plugins a sub-agent builds hand its pool the tree's tally", async () => {
|
|
384
|
+
const { opened, spy } = openFakeAgents([]);
|
|
385
|
+
try {
|
|
386
|
+
const plugins = await AkanCodePlugins.build({
|
|
387
|
+
workspace,
|
|
388
|
+
cwd: "/tmp/akan",
|
|
389
|
+
profile: budgeted(200),
|
|
390
|
+
apps: [],
|
|
391
|
+
canSeeImages: false,
|
|
392
|
+
depth: 1,
|
|
393
|
+
currentSessionId: () => "",
|
|
394
|
+
mailbox: () => undefined,
|
|
395
|
+
subagentSpend: { tokens: 200, running: 0 },
|
|
396
|
+
});
|
|
397
|
+
const pool = plugins.extensions.find((extension) => extension.name === "akan-subagent");
|
|
398
|
+
const refused = await runTask(pool, "c1");
|
|
399
|
+
expect(refused?.isError).toBe(true);
|
|
400
|
+
expect(opened).toHaveLength(0);
|
|
401
|
+
plugins.dispose();
|
|
402
|
+
} finally {
|
|
403
|
+
spy.mockRestore();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("maxConcurrent counts every running sub-agent of the tree, and a full tree refuses a child's task instead of queueing it", async () => {
|
|
408
|
+
const held = Promise.withResolvers<void>();
|
|
409
|
+
const opened: CodeAgentOptions[] = [];
|
|
410
|
+
const spy = spyOn(CodeAgent, "create").mockImplementation(async (options) => {
|
|
411
|
+
opened.push(options);
|
|
412
|
+
return {
|
|
413
|
+
on: () => {},
|
|
414
|
+
prompt: async () => await held.promise,
|
|
415
|
+
waitForIdle: async () => {},
|
|
416
|
+
dispose: () => {},
|
|
417
|
+
tokensUsed: 0,
|
|
418
|
+
} as never;
|
|
419
|
+
});
|
|
420
|
+
try {
|
|
421
|
+
const root = SubagentPool.extensionFor({
|
|
422
|
+
workspace,
|
|
423
|
+
cwd: "/tmp/akan",
|
|
424
|
+
parent: budgeted(1_000_000, 2),
|
|
425
|
+
depth: 0,
|
|
426
|
+
});
|
|
427
|
+
const running = [runTask(root, "c1"), runTask(root, "c2")];
|
|
428
|
+
await until(() => opened.length === 2);
|
|
429
|
+
const [child] = opened;
|
|
430
|
+
if (!child) throw new Error("no sub-agent was opened");
|
|
431
|
+
const refused = await Promise.race([runTask(poolOf(child), "c3"), Bun.sleep(500).then(() => undefined)]);
|
|
432
|
+
expect(refused?.isError).toBe(true);
|
|
433
|
+
expect(refused?.content[0]?.text).toContain("limit 2 at once");
|
|
434
|
+
expect(refused?.content[0]?.text).not.toContain("Wait");
|
|
435
|
+
expect((await runTask(root, "c4"))?.content[0]?.text).toContain("Wait for one of yours");
|
|
436
|
+
expect(opened).toHaveLength(2);
|
|
437
|
+
held.resolve();
|
|
438
|
+
await Promise.all(running);
|
|
439
|
+
expect((await runTask(poolOf(child), "c5"))?.isError).toBeUndefined();
|
|
440
|
+
expect(opened).toHaveLength(3);
|
|
441
|
+
} finally {
|
|
442
|
+
held.resolve();
|
|
443
|
+
spy.mockRestore();
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("a sub-agent that fails still spends from the tree's budget, once, and the notice says it failed", async () => {
|
|
448
|
+
const opened: CodeAgentOptions[] = [];
|
|
449
|
+
const spy = spyOn(CodeAgent, "create").mockImplementation(async (options) => {
|
|
450
|
+
opened.push(options);
|
|
451
|
+
return {
|
|
452
|
+
on: () => {},
|
|
453
|
+
prompt: async () => {
|
|
454
|
+
throw new Error("provider went away");
|
|
455
|
+
},
|
|
456
|
+
waitForIdle: async () => {},
|
|
457
|
+
dispose: () => {},
|
|
458
|
+
tokensUsed: 150,
|
|
459
|
+
} as never;
|
|
460
|
+
});
|
|
461
|
+
const notices: string[] = [];
|
|
462
|
+
try {
|
|
463
|
+
const root = SubagentPool.extensionFor({
|
|
464
|
+
workspace,
|
|
465
|
+
cwd: "/tmp/akan",
|
|
466
|
+
parent: budgeted(100),
|
|
467
|
+
depth: 0,
|
|
468
|
+
onNotice: (message) => notices.push(message),
|
|
469
|
+
});
|
|
470
|
+
await expect(runTask(root, "c1")).rejects.toThrow("provider went away");
|
|
471
|
+
expect(notices.at(-1)).toBe("explore sub-agent failed, 150 tokens (150 of 100 spent)");
|
|
472
|
+
expect((await runTask(root, "c2"))?.content[0]?.text).toContain("token budget (100) is spent");
|
|
473
|
+
expect(opened).toHaveLength(1);
|
|
474
|
+
} finally {
|
|
475
|
+
spy.mockRestore();
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("a sub-agent its parent stops is counted once, and the notice says it stopped", async () => {
|
|
480
|
+
const prompted = Promise.withResolvers<void>();
|
|
481
|
+
const stopped = Promise.withResolvers<void>();
|
|
482
|
+
const spy = spyOn(CodeAgent, "create").mockImplementation(
|
|
483
|
+
async () =>
|
|
484
|
+
({
|
|
485
|
+
on: () => {},
|
|
486
|
+
prompt: async () => {
|
|
487
|
+
prompted.resolve();
|
|
488
|
+
await stopped.promise;
|
|
489
|
+
},
|
|
490
|
+
waitForIdle: async () => {},
|
|
491
|
+
abort: async () => stopped.resolve(),
|
|
492
|
+
dispose: () => {},
|
|
493
|
+
tokensUsed: 120,
|
|
494
|
+
}) as never,
|
|
495
|
+
);
|
|
496
|
+
const notices: string[] = [];
|
|
497
|
+
try {
|
|
498
|
+
const root = SubagentPool.extensionFor({
|
|
499
|
+
workspace,
|
|
500
|
+
cwd: "/tmp/akan",
|
|
501
|
+
parent: budgeted(1_000),
|
|
502
|
+
depth: 0,
|
|
503
|
+
onNotice: (message) => notices.push(message),
|
|
504
|
+
});
|
|
505
|
+
const parent = new AbortController();
|
|
506
|
+
const task = runTask(root, "c1", parent.signal);
|
|
507
|
+
await prompted.promise;
|
|
508
|
+
parent.abort();
|
|
509
|
+
await task;
|
|
510
|
+
expect(notices.at(-1)).toBe("explore sub-agent stopped, 120 tokens (120 of 1000 spent)");
|
|
511
|
+
} finally {
|
|
512
|
+
stopped.resolve();
|
|
513
|
+
spy.mockRestore();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
328
517
|
test("a child keeps no session and cannot ask a human", () => {
|
|
329
518
|
const child = SubagentPool.childOf(codeAgentPresets.local("/tmp/akan"), "explore");
|
|
330
519
|
expect(child.session).toEqual({ store: "memory", crossSession: false });
|
|
@@ -334,7 +523,6 @@ describe("SubagentPool", () => {
|
|
|
334
523
|
});
|
|
335
524
|
|
|
336
525
|
describe("McpToolPack", () => {
|
|
337
|
-
/** A minimal stdio MCP server, written at test time so the suite carries no extra source file. */
|
|
338
526
|
const serverSource = `
|
|
339
527
|
const reply = (id, result) => process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\\n");
|
|
340
528
|
for await (const line of console) {
|
|
@@ -349,8 +537,7 @@ for await (const line of console) {
|
|
|
349
537
|
`;
|
|
350
538
|
|
|
351
539
|
const withServer = async () => {
|
|
352
|
-
const
|
|
353
|
-
tempRoots.push(root);
|
|
540
|
+
const root = await tempRoot();
|
|
354
541
|
const script = path.join(root, "echo-mcp.ts");
|
|
355
542
|
await writeText(script, serverSource);
|
|
356
543
|
await writeText(
|
|
@@ -383,8 +570,7 @@ for await (const line of console) {
|
|
|
383
570
|
});
|
|
384
571
|
|
|
385
572
|
test("a server that cannot start costs its own tools and nothing else", async () => {
|
|
386
|
-
const
|
|
387
|
-
tempRoots.push(root);
|
|
573
|
+
const root = await tempRoot();
|
|
388
574
|
await writeText(
|
|
389
575
|
path.join(root, ".akan", "code", "mcp.json"),
|
|
390
576
|
JSON.stringify({ mcpServers: { broken: { command: "/nonexistent/binary" } } }),
|
|
@@ -397,8 +583,6 @@ for await (const line of console) {
|
|
|
397
583
|
});
|
|
398
584
|
expect(pack?.toolNames).toEqual([]);
|
|
399
585
|
expect(notices.join(" ")).toContain("broken");
|
|
400
|
-
// The pack survives the failure so `/mcp` can say what went wrong; a pack that dissolved would leave the
|
|
401
|
-
// server indistinguishable from one nobody declared.
|
|
402
586
|
expect(pack?.status).toEqual([
|
|
403
587
|
{ name: "broken", transport: "stdio", target: "/nonexistent/binary", tools: [], error: expect.any(String) },
|
|
404
588
|
]);
|
|
@@ -448,28 +632,16 @@ describe("WebToolPack", () => {
|
|
|
448
632
|
});
|
|
449
633
|
try {
|
|
450
634
|
const profile = codeAgentPresets.local(root);
|
|
451
|
-
const
|
|
452
|
-
const tools = new Map<
|
|
453
|
-
string,
|
|
454
|
-
(id: string, params: never) => Promise<{ content: { text: string }[]; isError?: boolean }>
|
|
455
|
-
>();
|
|
456
|
-
const factory = (pack.extension() as { factory: (pi: unknown) => void }).factory;
|
|
457
|
-
factory({ registerTool: (tool: { name: string; execute: never }) => tools.set(tool.name, tool.execute) });
|
|
458
|
-
const fetchTool = tools.get("web_fetch");
|
|
635
|
+
const fetchTool = toolsOf(new WebToolPack({ profile }).extension()).get("web_fetch");
|
|
459
636
|
const ok = await fetchTool?.("1", { url: `http://localhost:${server.port}/` } as never);
|
|
460
637
|
expect(ok?.content[0]?.text).toContain("Akan");
|
|
461
638
|
expect(ok?.content[0]?.text).toContain("Hello & welcome");
|
|
462
639
|
expect(ok?.content[0]?.text).not.toContain("<h1>");
|
|
463
640
|
|
|
464
641
|
const narrowed = new WebToolPack({ profile: { ...profile, network: { allowHosts: ["docs.akanjs.com"] } } });
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
>();
|
|
469
|
-
(narrowed.extension() as { factory: (pi: unknown) => void }).factory({
|
|
470
|
-
registerTool: (tool: { name: string; execute: never }) => narrowedTools.set(tool.name, tool.execute),
|
|
471
|
-
});
|
|
472
|
-
const refused = await narrowedTools.get("web_fetch")?.("1", { url: `http://localhost:${server.port}/` } as never);
|
|
642
|
+
const refused = await toolsOf(narrowed.extension()).get("web_fetch")?.("1", {
|
|
643
|
+
url: `http://localhost:${server.port}/`,
|
|
644
|
+
} as never);
|
|
473
645
|
expect(refused?.isError).toBe(true);
|
|
474
646
|
expect(refused?.content[0]?.text).toContain("docs.akanjs.com");
|
|
475
647
|
} finally {
|
|
@@ -497,12 +669,8 @@ describe("SessionToolPack", () => {
|
|
|
497
669
|
});
|
|
498
670
|
|
|
499
671
|
test("says so plainly when nothing earlier matches", async () => {
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
const tools = new Map<string, (id: string, params: never) => Promise<{ content: { text: string }[] }>>();
|
|
503
|
-
(new SessionToolPack(options(root)).extension() as { factory: (pi: unknown) => void }).factory({
|
|
504
|
-
registerTool: (tool: { name: string; execute: never }) => tools.set(tool.name, tool.execute),
|
|
505
|
-
});
|
|
672
|
+
const root = await tempRoot();
|
|
673
|
+
const tools = toolsOf(new SessionToolPack(options(root)).extension());
|
|
506
674
|
const result = await tools.get("session_search")?.("1", { query: "tunnel wire" } as never);
|
|
507
675
|
expect(result?.content[0]?.text).toContain("No earlier session");
|
|
508
676
|
});
|
|
@@ -11,7 +11,6 @@ interface RegisteredTool {
|
|
|
11
11
|
) => Promise<{ content: { text?: string }[]; isError?: boolean }>;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/** Runs the pack's extension and hands back the one tool it registered, plus the questions it went on to ask. */
|
|
15
14
|
const mount = (
|
|
16
15
|
profile: CodeAgentProfile,
|
|
17
16
|
answer: (question: Omit<CodeAgentQuestion, "questionId">) => string | undefined,
|
|
@@ -43,7 +42,6 @@ const one = (patch: Record<string, unknown> = {}) => ({
|
|
|
43
42
|
describe("ask_user", () => {
|
|
44
43
|
test("it is published only where somebody can answer", () => {
|
|
45
44
|
expect(mount(local, () => "x").pack.names()).toEqual(["ask_user"]);
|
|
46
|
-
// A pod and a sub-agent both set `canPrompt` false; the tool is never constructed there.
|
|
47
45
|
expect(mount(pod, () => "x").pack.names()).toEqual([]);
|
|
48
46
|
expect(mount(pod, () => "x").tool).toBeUndefined();
|
|
49
47
|
});
|
|
@@ -84,7 +82,6 @@ describe("ask_user", () => {
|
|
|
84
82
|
expect(asked).toHaveLength(AskToolPack.maxQuestions);
|
|
85
83
|
});
|
|
86
84
|
|
|
87
|
-
/** A blank answer reads to a model as "the user wanted nothing", which is the one thing a skip never means. */
|
|
88
85
|
test("a skipped question says so, and says what to do about it", async () => {
|
|
89
86
|
const { tool } = mount(local, () => undefined);
|
|
90
87
|
const result = await tool?.execute("t", { questions: [one()] });
|