@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
|
@@ -18,60 +18,38 @@ export interface AkanCodeServicesOptions {
|
|
|
18
18
|
sessionId?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
* Assembles the engine's injectable services from akan's own storage.
|
|
23
|
-
*
|
|
24
|
-
* Every one of the four is passed explicitly. Leave any of them out and the engine falls back to `~/.pi/` —
|
|
25
|
-
* measured: with all four injected it writes nothing outside the paths named here.
|
|
26
|
-
*/
|
|
21
|
+
// All four engine services are injected explicitly: any one left out makes the engine fall back to `~/.pi/`.
|
|
27
22
|
export class AkanCodeServices {
|
|
28
23
|
static settings() {
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// `enableInstallTelemetry` defaults to true and attaches pi-branded headers to OpenRouter, NVIDIA and
|
|
32
|
-
// Cloudflare requests, so it is turned off here rather than per host — a host that changes provider later
|
|
33
|
-
// would otherwise turn it back on without meaning to.
|
|
24
|
+
// In memory on purpose: file settings would be a second configuration surface beside the profile.
|
|
25
|
+
// `enableInstallTelemetry` defaults to true and adds pi-branded headers to OpenRouter/NVIDIA/Cloudflare calls.
|
|
34
26
|
return SettingsManager.inMemory({
|
|
35
27
|
enableInstallTelemetry: false,
|
|
36
28
|
enableAnalytics: false,
|
|
37
29
|
quietStartup: true,
|
|
38
|
-
// Twice the engine's 16,384
|
|
39
|
-
//
|
|
40
|
-
// reasoning model stops mid-thought there, and the recovery re-sends a near-full window to retry the turn.
|
|
30
|
+
// Twice the engine's 16,384: at 16k a reasoning model stops mid-thought at the compaction threshold,
|
|
31
|
+
// and the recovery re-sends a near-full window to retry the turn.
|
|
41
32
|
compaction: { reserveTokens: 32_768 },
|
|
42
33
|
});
|
|
43
34
|
}
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
* The model catalogue and the credentials behind it, both under `~/.akan/`.
|
|
47
|
-
*
|
|
48
|
-
* `authPath` is the whole of what akan's own credential file used to need a storage backend for: the engine
|
|
49
|
-
* locks and rewrites that file itself, so a second `akan code` refreshing an OAuth token at the same moment
|
|
50
|
-
* cannot clobber the first. Nothing lands in `~/.pi/`, and a user who removes akan takes their keys with them.
|
|
51
|
-
*/
|
|
36
|
+
// The engine locks and rewrites `authPath` itself, so two `akan code` refreshing a token at once cannot clobber it.
|
|
52
37
|
static async runtime(workspaceRoot: string, proxy: CodeAgentProxy | null = null) {
|
|
53
38
|
mkdirSync(akanCodePaths.globalDir(), { recursive: true, mode: 0o700 });
|
|
54
|
-
if (proxy) {
|
|
55
|
-
//* Behind a proxy neither `auth.json` nor an env key may reach a request, so credentials live in a throwaway
|
|
56
|
-
//* file and the only key the engine ever sends is the proxy token.
|
|
57
|
-
const runtime = await ModelRuntime.create({
|
|
58
|
-
authPath: path.join(mkdtempSync(path.join(tmpdir(), "akan-code-proxy-")), "auth.json"),
|
|
59
|
-
modelsPath: akanCodePaths.modelsFile(),
|
|
60
|
-
});
|
|
61
|
-
await proxy.apply(runtime);
|
|
62
|
-
return runtime;
|
|
63
|
-
}
|
|
64
39
|
const runtime = await ModelRuntime.create({
|
|
65
|
-
|
|
40
|
+
//* Behind a proxy no real key may reach a request, so credentials live in a throwaway file.
|
|
41
|
+
authPath: proxy
|
|
42
|
+
? path.join(mkdtempSync(path.join(tmpdir(), "akan-code-proxy-")), "auth.json")
|
|
43
|
+
: akanCodePaths.authFile(),
|
|
66
44
|
modelsPath: akanCodePaths.modelsFile(),
|
|
67
45
|
});
|
|
68
|
-
await
|
|
46
|
+
if (proxy) await proxy.apply(runtime);
|
|
47
|
+
else await AkanEnvKeys.apply(runtime, workspaceRoot);
|
|
69
48
|
return runtime;
|
|
70
49
|
}
|
|
71
50
|
|
|
72
51
|
static sessions(workspaceRoot: string, cwd: string, profile: CodeAgentProfile, resume?: string) {
|
|
73
|
-
//* `remote` has no store of its own yet, so it keeps a file session
|
|
74
|
-
//* and its suspended asks and resume need the file more than a laptop does.
|
|
52
|
+
//* `remote` has no store of its own yet, so it keeps a file session: a pod worker's suspended asks need it.
|
|
75
53
|
if (profile.session.store === "memory") return SessionManager.inMemory(cwd);
|
|
76
54
|
const dir = akanCodePaths.sessionsDir(workspaceRoot);
|
|
77
55
|
mkdirSync(dir, { recursive: true });
|
|
@@ -84,15 +62,7 @@ export class AkanCodeServices {
|
|
|
84
62
|
return manager;
|
|
85
63
|
}
|
|
86
64
|
|
|
87
|
-
|
|
88
|
-
* The akan skill set, then the person's own, then the workspace's.
|
|
89
|
-
*
|
|
90
|
-
* `~/.akan/code/skills` is read because a skill somebody wrote for themselves is theirs, not one checkout's,
|
|
91
|
-
* and having to copy it into every repo is the same friction that makes people stop writing them. It is
|
|
92
|
-
* text the model may read, never code that runs — which is why extensions stay workspace-only below.
|
|
93
|
-
*
|
|
94
|
-
* A path that does not exist is a reported load error rather than a skip, so each is checked first.
|
|
95
|
-
*/
|
|
65
|
+
// Skills are text, never code that runs, so unlike extensions they may come from the home directory too.
|
|
96
66
|
static #skillPaths(profile: CodeAgentProfile, workspaceRoot: string) {
|
|
97
67
|
if (!profile.context.skills) return [];
|
|
98
68
|
const dirs = [
|
|
@@ -103,28 +73,16 @@ export class AkanCodeServices {
|
|
|
103
73
|
return dirs.filter((dir): dir is string => !!dir && existsSync(dir));
|
|
104
74
|
}
|
|
105
75
|
|
|
106
|
-
/**
|
|
107
|
-
* What the engine's resource loader may discover. Extensions come from the workspace and nowhere else: a
|
|
108
|
-
* globally installed extension is code that runs in every checkout, so an agent would behave differently in
|
|
109
|
-
* two clones of the same repo for reasons nothing in the repo explains. Skills are text and are read from
|
|
110
|
-
* the home directory too — see `#skillPaths`.
|
|
111
|
-
*/
|
|
112
76
|
static resourceOptions(options: AkanCodeServicesOptions) {
|
|
113
77
|
// A path that does not exist is reported as a load error rather than skipped, and these two are optional.
|
|
114
78
|
const present = (dir: string) => (existsSync(dir) ? [dir] : []);
|
|
115
79
|
return {
|
|
116
80
|
noExtensions: true,
|
|
117
|
-
//
|
|
118
|
-
// checkout's own `.pi/`. Explicit `additionalSkillPaths` are merged either way, so this is the switch
|
|
119
|
-
// that keeps a skill in whoever's home directory from changing how the agent works here.
|
|
81
|
+
// Disables only the engine's default skill locations (`~/.pi/skills`, `.pi/`); `additionalSkillPaths` merge.
|
|
120
82
|
noSkills: true,
|
|
121
83
|
noPromptTemplates: true,
|
|
122
84
|
noThemes: true,
|
|
123
85
|
noContextFiles: !options.profile.context.projectFiles,
|
|
124
|
-
// The engine takes one context file per directory and looks at no editor's own rules, so a repo whose
|
|
125
|
-
// conventions live in a `CLAUDE.md` or under `.cursor/rules` hands this agent less than it hands the
|
|
126
|
-
// editor beside it — see {@link AkanContextFiles}. Gated on the same flag, so a profile that wants no
|
|
127
|
-
// project context still gets none.
|
|
128
86
|
agentsFilesOverride: ({ agentsFiles }: { agentsFiles: AkanContextFile[] }) => ({
|
|
129
87
|
agentsFiles: options.profile.context.projectFiles
|
|
130
88
|
? AkanContextFiles.extend(agentsFiles, { workspaceRoot: options.workspaceRoot, cwd: options.cwd })
|
|
@@ -11,25 +11,15 @@ export interface AkanContextFilesOptions {
|
|
|
11
11
|
cwd: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*
|
|
17
|
-
* It walks the ancestors of `cwd` and takes **one** file per directory, first match of
|
|
18
|
-
* `AGENTS.override.md · AGENTS.md · AGENTS.MD · CLAUDE.md · CLAUDE.MD` — so a repo carrying both an `AGENTS.md`
|
|
19
|
-
* and a `CLAUDE.md` has the second one silently dropped, and a `.cursor/rules` tree is never looked at. Both
|
|
20
|
-
* are files a developer wrote expecting an agent to obey them, and an agent that reads one editor's copy and
|
|
21
|
-
* not another's follows rules that disagree with the repo it is in.
|
|
22
|
-
*
|
|
23
|
-
* Added rather than replaced: the base list keeps its order and its precedence, and these land after it.
|
|
24
|
-
*/
|
|
14
|
+
// The engine takes only the first of `AGENTS.override.md · AGENTS.md · CLAUDE.md` per directory and never reads
|
|
15
|
+
// `.cursor/rules`; this appends what it drops, after the engine's own list.
|
|
25
16
|
export class AkanContextFiles {
|
|
26
17
|
/** Cursor's frontmatter block, which is metadata for its own rule picker and not instruction for a model. */
|
|
27
18
|
static readonly #frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
28
19
|
|
|
29
20
|
static extend(base: AkanContextFile[], options: AkanContextFilesOptions): AkanContextFile[] {
|
|
30
21
|
const seen = new Set(base.map((file) => file.path));
|
|
31
|
-
// Keyed by content
|
|
32
|
-
// to the other or a one-line pointer at it — and the same guide twice is the same guide twice.
|
|
22
|
+
// Keyed by content too: the usual way to carry both names is a symlink or a one-line pointer.
|
|
33
23
|
const contents = new Set(base.map((file) => file.content.trim()));
|
|
34
24
|
const added: AkanContextFile[] = [];
|
|
35
25
|
for (const file of [...AkanContextFiles.#claude(base), ...AkanContextFiles.#cursor(options)]) {
|
|
@@ -41,12 +31,7 @@ export class AkanContextFiles {
|
|
|
41
31
|
return [...base, ...added];
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
* The `CLAUDE.md` beside an `AGENTS.md` the engine already took.
|
|
46
|
-
*
|
|
47
|
-
* Not beside an `AGENTS.override.md`: that name means "ignore what this directory would otherwise say", so
|
|
48
|
-
* reinstating the file it was written to displace is the one case where loading both is wrong.
|
|
49
|
-
*/
|
|
34
|
+
// Not beside an `AGENTS.override.md`: that file exists to displace the others in its directory.
|
|
50
35
|
static #claude(base: AkanContextFile[]) {
|
|
51
36
|
const files: AkanContextFile[] = [];
|
|
52
37
|
for (const file of base) {
|
|
@@ -60,14 +45,7 @@ export class AkanContextFiles {
|
|
|
60
45
|
return files;
|
|
61
46
|
}
|
|
62
47
|
|
|
63
|
-
|
|
64
|
-
* Cursor's always-on rules, from the workspace root and from the session's own directory.
|
|
65
|
-
*
|
|
66
|
-
* Only `alwaysApply: true` is taken. Cursor's other three kinds are conditional — `globs` attaches a rule to
|
|
67
|
-
* the files it names, a bare `description` offers it for the model to ask for, and neither means the rule is
|
|
68
|
-
* loaded manually — so putting them in a system prompt applies rules their author scoped away. A file with
|
|
69
|
-
* no frontmatter at all is the `.cursorrules` shape, which was never conditional.
|
|
70
|
-
*/
|
|
48
|
+
// Only `alwaysApply: true` or frontmatter-less rules: Cursor's `globs`/`description`/manual kinds are scoped.
|
|
71
49
|
static #cursor(options: AkanContextFilesOptions) {
|
|
72
50
|
const files: AkanContextFile[] = [];
|
|
73
51
|
const roots = [...new Set([options.workspaceRoot, options.cwd])];
|
|
@@ -84,7 +62,6 @@ export class AkanContextFiles {
|
|
|
84
62
|
return files;
|
|
85
63
|
}
|
|
86
64
|
|
|
87
|
-
/** The rule body when the rule applies unconditionally, and nothing when its author scoped it. */
|
|
88
65
|
static #applied(raw: string) {
|
|
89
66
|
const match = AkanContextFiles.#frontmatter.exec(raw);
|
|
90
67
|
if (!match) return raw;
|
|
@@ -42,11 +42,9 @@ import { CodeAgentUi } from "./CodeAgentUi";
|
|
|
42
42
|
import { CodeMailbox } from "./CodeMailbox";
|
|
43
43
|
import { CodeSessionFork } from "./CodeSessionFork";
|
|
44
44
|
import { CodeSessionIndex } from "./CodeSessionIndex";
|
|
45
|
+
import type { SubagentSpend } from "./SubagentPool";
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
* What the engine calls a run mode. It is not re-exported from the package entry, so the literal set is
|
|
48
|
-
* restated here; it is the whole type, and a mismatch is a type error at the one call site that uses it.
|
|
49
|
-
*/
|
|
47
|
+
// The engine's run-mode type is not re-exported from its package entry, so the literal set is restated.
|
|
50
48
|
export type CodeAgentHostMode = "tui" | "rpc" | "json" | "print";
|
|
51
49
|
|
|
52
50
|
export interface CodeAgentOptions {
|
|
@@ -56,23 +54,18 @@ export interface CodeAgentOptions {
|
|
|
56
54
|
model?: CodeAgentModelRef;
|
|
57
55
|
/** Apps whose dev server and preview the turn-end feedback loop watches. */
|
|
58
56
|
apps?: string[];
|
|
59
|
-
/** Extra capability packs, layered on top of the profile gate and the akan pack. */
|
|
60
57
|
extensions?: InlineExtension[];
|
|
61
58
|
customTools?: ToolDefinition[];
|
|
62
59
|
/** `print` for a one-shot stream, `rpc` when a host drives this process over stdio. */
|
|
63
60
|
mode?: CodeAgentHostMode;
|
|
64
|
-
/** How many `task` tools deep this agent already is.
|
|
61
|
+
/** How many `task` tools deep this agent already is. */
|
|
65
62
|
depth?: number;
|
|
63
|
+
/** The tally (tokens spent, sub-agents running) of the sub-agent tree whose `task` opened this agent. */
|
|
64
|
+
subagentSpend?: SubagentSpend;
|
|
66
65
|
/** Id of a stored session to continue instead of opening a new one. */
|
|
67
66
|
resume?: string;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
/**
|
|
71
|
-
* The code agent core: one engine session, gated by a profile, emitting the akan wire.
|
|
72
|
-
*
|
|
73
|
-
* Nothing here knows what a host is. A host receives events, sends commands and injects a profile — which is
|
|
74
|
-
* what lets one core serve a terminal, an RPC child and a browser without three copies of the loop.
|
|
75
|
-
*/
|
|
76
69
|
export class CodeAgent {
|
|
77
70
|
readonly #profile: CodeAgentProfile;
|
|
78
71
|
readonly #gate: CodeAgentGate;
|
|
@@ -88,12 +81,7 @@ export class CodeAgent {
|
|
|
88
81
|
#mailbox: CodeMailbox | undefined;
|
|
89
82
|
#mcp: CodeAgentMcpStatus[] = [];
|
|
90
83
|
#workspaceRoot = "";
|
|
91
|
-
|
|
92
|
-
* The frames of the turn in flight, so a host that reconnects mid-turn can be handed what it missed.
|
|
93
|
-
*
|
|
94
|
-
* Bounded and cleared at `idle`: the completed messages of an earlier turn are in the transcript, and the
|
|
95
|
-
* only thing that exists nowhere else is the bubble still being written.
|
|
96
|
-
*/
|
|
84
|
+
// Cleared at `idle`: an earlier turn is in the transcript; only the turn in flight exists nowhere else.
|
|
97
85
|
readonly #replay: CodeAgentEvent[] = [];
|
|
98
86
|
#replayFrom = 0;
|
|
99
87
|
#lastContextTokens: number | undefined;
|
|
@@ -120,6 +108,7 @@ export class CodeAgent {
|
|
|
120
108
|
apps: options.apps ?? [],
|
|
121
109
|
canSeeImages: akanCodeModelSupportsImages(model),
|
|
122
110
|
depth: options.depth ?? 0,
|
|
111
|
+
...(options.subagentSpend ? { subagentSpend: options.subagentSpend } : {}),
|
|
123
112
|
currentSessionId: () => agent.sessionId,
|
|
124
113
|
mailbox: () => agent.#mailbox,
|
|
125
114
|
onNotice: (message) => agent.#emit({ type: "notice", level: "warning", message }),
|
|
@@ -168,7 +157,6 @@ export class CodeAgent {
|
|
|
168
157
|
return this.#profile;
|
|
169
158
|
}
|
|
170
159
|
|
|
171
|
-
/** Whether the model can be handed a picture at all, which decides whether offering to attach one is honest. */
|
|
172
160
|
get canSeeImages() {
|
|
173
161
|
const model = this.#session?.model;
|
|
174
162
|
return model ? akanCodeModelSupportsImages(model) : false;
|
|
@@ -185,10 +173,8 @@ export class CodeAgent {
|
|
|
185
173
|
}
|
|
186
174
|
|
|
187
175
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* Read from the assembly rather than from the live session: the tool allowlist is fixed when the session is
|
|
191
|
-
* created, so a server declared afterwards is not one this agent can reach however the file now reads.
|
|
176
|
+
* As assembled at creation: the tool allowlist is fixed then, so a server declared later is unreachable. A
|
|
177
|
+
* server's `auth` still turns `required` when a call's token cannot be renewed, until a reload.
|
|
192
178
|
*/
|
|
193
179
|
mcpServers(): CodeAgentMcpStatus[] {
|
|
194
180
|
return this.#mcp;
|
|
@@ -199,12 +185,7 @@ export class CodeAgent {
|
|
|
199
185
|
return this.#mailbox?.peers() ?? [];
|
|
200
186
|
}
|
|
201
187
|
|
|
202
|
-
/**
|
|
203
|
-
* Hands a message to another session, which receives it as a prompt of its own.
|
|
204
|
-
*
|
|
205
|
-
* It is a prompt rather than a note on its screen because the point of reaching another session is to set
|
|
206
|
-
* it working — and it goes in as a follow-up, so a peer in the middle of a turn finishes that turn first.
|
|
207
|
-
*/
|
|
188
|
+
/** The peer receives the text as a prompt of its own, queued as a follow-up when it is mid-turn. */
|
|
208
189
|
send(target: string, text: string) {
|
|
209
190
|
const mailbox = this.#mailbox;
|
|
210
191
|
if (!mailbox) throw new Error("This profile keeps no shared mailbox, so it has no peers.");
|
|
@@ -221,7 +202,7 @@ export class CodeAgent {
|
|
|
221
202
|
this.#mailbox.open(cwd, (mail) => {
|
|
222
203
|
const from = `${mail.fromName} (${mail.from.slice(0, 8)})`;
|
|
223
204
|
this.#emit({ type: "notice", level: "info", message: `message from ${from}` });
|
|
224
|
-
void this
|
|
205
|
+
void this.#prompt(`Message from another session — ${from}:\n${mail.text}`).catch((error: unknown) =>
|
|
225
206
|
this.#emit({ type: "notice", level: "error", message: String(error) }),
|
|
226
207
|
);
|
|
227
208
|
});
|
|
@@ -235,7 +216,6 @@ export class CodeAgent {
|
|
|
235
216
|
return this.#session?.isStreaming ?? false;
|
|
236
217
|
}
|
|
237
218
|
|
|
238
|
-
/** Total tokens this session has spent, which is what a sub-agent budget is drawn down against. */
|
|
239
219
|
get tokensUsed() {
|
|
240
220
|
return this.#session?.getSessionStats().tokens.total ?? 0;
|
|
241
221
|
}
|
|
@@ -250,19 +230,14 @@ export class CodeAgent {
|
|
|
250
230
|
model: model ? { provider: model.provider, id: model.id, name: model.name } : undefined,
|
|
251
231
|
tools: session.getActiveToolNames(),
|
|
252
232
|
contextTokens: model?.contextWindow,
|
|
253
|
-
//
|
|
254
|
-
// capability and the other is a setting, so they cannot share a spelling.
|
|
233
|
+
// Not `off`: that is a setting of a reasoning model, not the absence of reasoning.
|
|
255
234
|
effort: session.supportsThinking() ? session.thinkingLevel : undefined,
|
|
256
235
|
name: session.sessionName,
|
|
257
236
|
interaction: this.#profile.interaction,
|
|
258
237
|
};
|
|
259
238
|
}
|
|
260
239
|
|
|
261
|
-
/**
|
|
262
|
-
* Emits the opening `session` frame through the same counter as everything else.
|
|
263
|
-
*
|
|
264
|
-
* A frame minted outside it would carry `seq: 0`, which every client drops — their watermark starts there.
|
|
265
|
-
*/
|
|
240
|
+
/** A `session` frame minted outside this counter would carry `seq: 0`, which every client drops. */
|
|
266
241
|
announce() {
|
|
267
242
|
this.#emit({ type: "session", info: this.info });
|
|
268
243
|
this.#restore();
|
|
@@ -272,16 +247,7 @@ export class CodeAgent {
|
|
|
272
247
|
for (const request of approvals) this.#emit({ type: "approval", request });
|
|
273
248
|
}
|
|
274
249
|
|
|
275
|
-
|
|
276
|
-
* Replays a resumed session's conversation onto the wire.
|
|
277
|
-
*
|
|
278
|
-
* The model gets its context back from the session file; a host gets nothing, because the wire only carries
|
|
279
|
-
* what happens live. Without this a resumed session is a blank screen in front of an agent that remembers
|
|
280
|
-
* everything — which reads as a resume that failed.
|
|
281
|
-
*
|
|
282
|
-
* Prose only: the tool traffic of a past turn is in the file, but replaying it would redraw calls nobody is
|
|
283
|
-
* waiting on, against a working tree that has moved since.
|
|
284
|
-
*/
|
|
250
|
+
// Prose only: a past turn's tool calls would redraw rows nobody waits on, against a tree that has moved since.
|
|
285
251
|
#restore() {
|
|
286
252
|
const session = this.#session;
|
|
287
253
|
if (!session) return;
|
|
@@ -311,12 +277,7 @@ export class CodeAgent {
|
|
|
311
277
|
/** One turn's worth of frames is kept for reconnection; a client further behind reloads the transcript. */
|
|
312
278
|
static readonly replayLimit = 2_000;
|
|
313
279
|
|
|
314
|
-
/**
|
|
315
|
-
* Current state, and the frames after `sinceSeq` when a reconnecting host asks for them.
|
|
316
|
-
*
|
|
317
|
-
* `replayFrom` says how far back the buffer reaches. A client behind it cannot be caught up frame by frame
|
|
318
|
-
* and has to reload — saying so is better than silently handing it a gap.
|
|
319
|
-
*/
|
|
280
|
+
/** `frames` only when `sinceSeq` is given; a client behind `replayFrom` has to reload the transcript. */
|
|
320
281
|
async state({ sinceSeq }: { sinceSeq?: number } = {}): Promise<CodeAgentState> {
|
|
321
282
|
const base = { info: this.info, streaming: this.isStreaming, replayFrom: this.#replayFrom };
|
|
322
283
|
if (sinceSeq === undefined) return base;
|
|
@@ -324,9 +285,13 @@ export class CodeAgent {
|
|
|
324
285
|
}
|
|
325
286
|
|
|
326
287
|
async prompt(message: string, images?: CodeAgentImage[]) {
|
|
288
|
+
this.#suspended.clear();
|
|
289
|
+
await this.#prompt(message, images);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async #prompt(message: string, images?: CodeAgentImage[]) {
|
|
327
293
|
const session = this.#require();
|
|
328
|
-
//
|
|
329
|
-
// turn to end would only mean the session is nameless for exactly as long as it is interesting to watch.
|
|
294
|
+
// From the first prompt, not the answer, so the session is not nameless while it is worth watching.
|
|
330
295
|
if (!session.sessionName && message.trim()) this.setName(codeAgentSessionName(message));
|
|
331
296
|
const attachments = await CodeAgent.#attachments(images);
|
|
332
297
|
const options = attachments.length ? { images: attachments } : {};
|
|
@@ -335,17 +300,13 @@ export class CodeAgent {
|
|
|
335
300
|
}
|
|
336
301
|
|
|
337
302
|
async abort() {
|
|
338
|
-
// The engine finalizes an interrupted message as an ordinary stop, so the
|
|
339
|
-
// an abort is the one that asked for it.
|
|
303
|
+
// The engine finalizes an interrupted message as an ordinary stop, so the abort is noted here.
|
|
340
304
|
this.#mapper.noteOutcome("aborted");
|
|
341
305
|
this.#asks.clear();
|
|
342
306
|
await this.#require().abort();
|
|
343
307
|
}
|
|
344
308
|
|
|
345
|
-
|
|
346
|
-
* Answering is not prompting. Routing an answer through `prompt` would open a turn while the question slot is
|
|
347
|
-
* still filled, leaving a card on screen that still looks clickable — the library does not stop that.
|
|
348
|
-
*/
|
|
309
|
+
// Not via `prompt`: that opens a turn with the question slot still filled, which the engine does not stop.
|
|
349
310
|
async answer(questionId: string, answer: CodeAgentAnswer) {
|
|
350
311
|
const question = this.#asks.questionOf(questionId);
|
|
351
312
|
const rendered = question ? codeAgentRenderAnswer(question, answer) : (answer.text ?? "");
|
|
@@ -358,9 +319,8 @@ export class CodeAgent {
|
|
|
358
319
|
if (!suspended) return false;
|
|
359
320
|
const suspendedRendered = codeAgentRenderAnswer(suspended, answer);
|
|
360
321
|
this.#emit({ type: "question_resolved", questionId, answer, rendered: suspendedRendered });
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
await this.prompt(`The user answered: ${suspendedRendered}\nContinue the task.`);
|
|
322
|
+
// As prose: compaction and the next turn read message content only, so a structured-only answer is lost.
|
|
323
|
+
await this.#prompt(`The user answered: ${suspendedRendered}\nContinue the task.`);
|
|
364
324
|
return true;
|
|
365
325
|
}
|
|
366
326
|
|
|
@@ -372,7 +332,7 @@ export class CodeAgent {
|
|
|
372
332
|
if (this.#profile.interaction.approval !== "suspend") return false;
|
|
373
333
|
if (!this.#suspended.takeApproval(approvalId)) return false;
|
|
374
334
|
this.#emit({ type: "approval_resolved", approvalId, approved });
|
|
375
|
-
if (approved) await this
|
|
335
|
+
if (approved) await this.#prompt("The user approved the pending action. Retry it and continue.");
|
|
376
336
|
return true;
|
|
377
337
|
}
|
|
378
338
|
|
|
@@ -389,12 +349,7 @@ export class CodeAgent {
|
|
|
389
349
|
this.#emit({ type: "session", info: this.info });
|
|
390
350
|
}
|
|
391
351
|
|
|
392
|
-
/**
|
|
393
|
-
* Copies this session under a new id, so the conversation can be continued two ways.
|
|
394
|
-
*
|
|
395
|
-
* The copy is taken off disk rather than out of memory: the file is what a resume replays, so forking it is
|
|
396
|
-
* the only version of "the same conversation" that both agents will actually agree on.
|
|
397
|
-
*/
|
|
352
|
+
/** Copies the session file, not memory, under a new id: the file is what a resume replays. */
|
|
398
353
|
fork(name?: string) {
|
|
399
354
|
if (!this.#sessionDir) throw new Error("This profile keeps no sessions on disk, so there is nothing to fork");
|
|
400
355
|
return CodeSessionFork.fork(this.#sessionDir, this.sessionId, name);
|
|
@@ -415,13 +370,7 @@ export class CodeAgent {
|
|
|
415
370
|
this.#emit({ type: "session", info: this.info });
|
|
416
371
|
}
|
|
417
372
|
|
|
418
|
-
/**
|
|
419
|
-
* The model catalogue, as the two questions a person actually asks of it.
|
|
420
|
-
*
|
|
421
|
-
* Built from the registry rather than `getAvailable()` so it needs no await and can answer about a provider
|
|
422
|
-
* nobody has a key for: "what else could I run" is most of why the list is opened, and a list of only what
|
|
423
|
-
* is already reachable cannot answer it.
|
|
424
|
-
*/
|
|
373
|
+
/** Every registered provider, keyed or not — `getAvailable()` would list only what is already reachable. */
|
|
425
374
|
catalogue(): CodeAgentProviderInfo[] {
|
|
426
375
|
const runtime = this.#session?.modelRuntime;
|
|
427
376
|
if (!runtime) return [];
|
|
@@ -468,7 +417,8 @@ export class CodeAgent {
|
|
|
468
417
|
this.#session = session;
|
|
469
418
|
session.subscribe((event) => {
|
|
470
419
|
for (const body of this.#mapper.map(event)) this.#emit(body);
|
|
471
|
-
|
|
420
|
+
// Per turn, not per delta: the count only moves when a provider response lands.
|
|
421
|
+
if (event.type === "agent_end" || event.type === "compaction_end") this.#emitContextNow();
|
|
472
422
|
});
|
|
473
423
|
await session.bindExtensions({
|
|
474
424
|
mode,
|
|
@@ -494,29 +444,10 @@ export class CodeAgent {
|
|
|
494
444
|
);
|
|
495
445
|
}
|
|
496
446
|
|
|
497
|
-
|
|
498
|
-
* Reports how full the window is, once per turn and after a compaction.
|
|
499
|
-
*
|
|
500
|
-
* Per turn rather than per delta: the number moves only when a provider response lands, and a frame per
|
|
501
|
-
* token delta would be thousands of frames saying the same thing. `tokens` is null right after a compaction
|
|
502
|
-
* and before the next response — that is "unknown", not zero, so nothing is emitted for it.
|
|
503
|
-
*/
|
|
504
|
-
#emitContextUsage(eventType: string) {
|
|
505
|
-
if (eventType !== "agent_end" && eventType !== "compaction_end") return;
|
|
506
|
-
this.#emitContextNow();
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* The same frame, on demand, for the one moment the window is already full and no turn has run.
|
|
511
|
-
*
|
|
512
|
-
* A resumed session restores a conversation the model is carrying and the host has never seen, so without
|
|
513
|
-
* this the status line offers the declared window and no share of it until the next turn ends — a session
|
|
514
|
-
* reopened at 80% looks identical to one reopened empty.
|
|
515
|
-
*/
|
|
447
|
+
// Also on resume, or a session reopened at 80% shows no usage until its next turn ends.
|
|
516
448
|
#emitContextNow() {
|
|
517
449
|
const usage = this.#session?.getContextUsage();
|
|
518
|
-
// `== null
|
|
519
|
-
// past an `undefined` the way the mirror of this bug does — which would emit `used: undefined`.
|
|
450
|
+
// `== null` on purpose: typed `number | null`, but `undefined` must not emit `used: undefined`.
|
|
520
451
|
if (usage?.tokens == null) return;
|
|
521
452
|
if (usage.tokens === this.#lastContextTokens) return;
|
|
522
453
|
this.#lastContextTokens = usage.tokens;
|
|
@@ -589,13 +520,7 @@ export class CodeAgent {
|
|
|
589
520
|
return await this.#asks.openApproval(approvalId);
|
|
590
521
|
}
|
|
591
522
|
|
|
592
|
-
/**
|
|
593
|
-
* Puts one question on the wire and waits for its answer, rendered as the labels a person chose.
|
|
594
|
-
*
|
|
595
|
-
* The single way anything asks: the engine's own UI port and the `ask_user` tool both land here, so the
|
|
596
|
-
* suspend path, the profile's refusal to prompt at all, and the id the host answers against cannot disagree
|
|
597
|
-
* between them. An empty answer is a skip, which is a real answer and not a failure.
|
|
598
|
-
*/
|
|
523
|
+
/** The rendered answer; `undefined` when skipped, suspended, or nobody can be prompted. */
|
|
599
524
|
async ask(spec: Omit<CodeAgentQuestion, "questionId">) {
|
|
600
525
|
const question: CodeAgentQuestion = { questionId: this.#asks.nextId("q"), ...spec };
|
|
601
526
|
if (!this.#profile.ui.canPrompt) {
|
|
@@ -1,23 +1,10 @@
|
|
|
1
1
|
import type { CodeAgentQuestion } from "akanjs/common";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
promise: Promise<T>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Holds what the agent is waiting on a person for.
|
|
10
|
-
*
|
|
11
|
-
* A question is a **single slot**: asking one ends the turn, so a second cannot exist. An approval is a
|
|
12
|
-
* **queue**: "may I write this file" naturally arrives twice in a row while a turn is still running, and a
|
|
13
|
-
* single slot would drop the second silently.
|
|
14
|
-
*
|
|
15
|
-
* Idempotency is by id. Two browser tabs, or answering after a refresh, both produce a second answer for an id
|
|
16
|
-
* that is already resolved — which must be ignored rather than applied to whatever is pending now.
|
|
17
|
-
*/
|
|
3
|
+
// A question is a single slot (asking ends the turn); approvals queue, since two can arrive within one turn.
|
|
4
|
+
// Idempotent by id: a second tab, or an answer after a refresh, for an already resolved id is ignored.
|
|
18
5
|
export class CodeAgentAsks {
|
|
19
|
-
#question: { question: CodeAgentQuestion; deferred:
|
|
20
|
-
readonly #approvals = new Map<string,
|
|
6
|
+
#question: { question: CodeAgentQuestion; deferred: PromiseWithResolvers<string> } | undefined;
|
|
7
|
+
readonly #approvals = new Map<string, PromiseWithResolvers<boolean>>();
|
|
21
8
|
#nextId = 0;
|
|
22
9
|
//* Ids outlive the process on a suspending session, so a resumed worker must not hand out `q1` a second time.
|
|
23
10
|
readonly #epoch = Date.now().toString(36);
|
|
@@ -26,7 +13,6 @@ export class CodeAgentAsks {
|
|
|
26
13
|
return this.#question?.question.questionId;
|
|
27
14
|
}
|
|
28
15
|
|
|
29
|
-
/** The open question, so an answer can be rendered against the options it was asked with. */
|
|
30
16
|
questionOf(questionId: string) {
|
|
31
17
|
return this.#question?.question.questionId === questionId ? this.#question.question : undefined;
|
|
32
18
|
}
|
|
@@ -41,10 +27,9 @@ export class CodeAgentAsks {
|
|
|
41
27
|
}
|
|
42
28
|
|
|
43
29
|
openQuestion(question: CodeAgentQuestion) {
|
|
44
|
-
//
|
|
45
|
-
// receive the old answer, and leaving it would strand the caller forever.
|
|
30
|
+
// Replaces a pending question: its turn is gone, and leaving it unresolved would strand its caller.
|
|
46
31
|
this.#question?.deferred.resolve("");
|
|
47
|
-
const deferred =
|
|
32
|
+
const deferred = Promise.withResolvers<string>();
|
|
48
33
|
this.#question = { question, deferred };
|
|
49
34
|
return deferred.promise;
|
|
50
35
|
}
|
|
@@ -58,7 +43,7 @@ export class CodeAgentAsks {
|
|
|
58
43
|
}
|
|
59
44
|
|
|
60
45
|
openApproval(id: string) {
|
|
61
|
-
const deferred =
|
|
46
|
+
const deferred = Promise.withResolvers<boolean>();
|
|
62
47
|
this.#approvals.set(id, deferred);
|
|
63
48
|
return deferred.promise;
|
|
64
49
|
}
|
|
@@ -78,12 +63,4 @@ export class CodeAgentAsks {
|
|
|
78
63
|
for (const deferred of this.#approvals.values()) deferred.resolve(false);
|
|
79
64
|
this.#approvals.clear();
|
|
80
65
|
}
|
|
81
|
-
|
|
82
|
-
static #defer<T>(): Deferred<T> {
|
|
83
|
-
let resolve: (value: T) => void = () => {};
|
|
84
|
-
const promise = new Promise<T>((r) => {
|
|
85
|
-
resolve = r;
|
|
86
|
-
});
|
|
87
|
-
return { resolve, promise };
|
|
88
|
-
}
|
|
89
66
|
}
|