@akanjs/devkit 3.0.0-beta.9 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- 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 +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- 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 +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- 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 +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- 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/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- 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/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- 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/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- 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 +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- 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/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AgentSession,
|
|
3
|
+
createAgentSession,
|
|
4
|
+
DefaultResourceLoader,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type InlineExtension,
|
|
7
|
+
type ToolDefinition,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import {
|
|
10
|
+
type CodeAgentAnswer,
|
|
11
|
+
type CodeAgentEffort,
|
|
12
|
+
type CodeAgentEvent,
|
|
13
|
+
type CodeAgentEventBody,
|
|
14
|
+
type CodeAgentImage,
|
|
15
|
+
type CodeAgentMcpStatus,
|
|
16
|
+
type CodeAgentProfile,
|
|
17
|
+
type CodeAgentProviderInfo,
|
|
18
|
+
type CodeAgentQuestion,
|
|
19
|
+
type CodeAgentSessionInfo,
|
|
20
|
+
type CodeAgentState,
|
|
21
|
+
codeAgentClip,
|
|
22
|
+
codeAgentLabelChars,
|
|
23
|
+
codeAgentRenderAnswer,
|
|
24
|
+
codeAgentSessionName,
|
|
25
|
+
} from "akanjs/common";
|
|
26
|
+
import type { Workspace } from "../../commandDecorators";
|
|
27
|
+
import { AkanCodePlugins } from "../tools/AkanCodePlugins";
|
|
28
|
+
import { AkanCodeServices } from "./AkanCodeServices";
|
|
29
|
+
import {
|
|
30
|
+
akanCodeModel,
|
|
31
|
+
akanCodeModelSupportsImages,
|
|
32
|
+
akanCodeModelWarnings,
|
|
33
|
+
type CodeAgentModelRef,
|
|
34
|
+
} from "./akanCodeModel";
|
|
35
|
+
import { akanCodePaths } from "./akanCodePaths";
|
|
36
|
+
import { CodeAgentAsks } from "./CodeAgentAsks";
|
|
37
|
+
import { CodeAgentEventMapper } from "./CodeAgentEventMapper";
|
|
38
|
+
import { CodeAgentGate } from "./CodeAgentGate";
|
|
39
|
+
import { CodeAgentProxy } from "./CodeAgentProxy";
|
|
40
|
+
import { CodeAgentSuspended } from "./CodeAgentSuspended";
|
|
41
|
+
import { CodeAgentUi } from "./CodeAgentUi";
|
|
42
|
+
import { CodeMailbox } from "./CodeMailbox";
|
|
43
|
+
import { CodeSessionFork } from "./CodeSessionFork";
|
|
44
|
+
import { CodeSessionIndex } from "./CodeSessionIndex";
|
|
45
|
+
import type { SubagentSpend } from "./SubagentPool";
|
|
46
|
+
|
|
47
|
+
// The engine's run-mode type is not re-exported from its package entry, so the literal set is restated.
|
|
48
|
+
export type CodeAgentHostMode = "tui" | "rpc" | "json" | "print";
|
|
49
|
+
|
|
50
|
+
export interface CodeAgentOptions {
|
|
51
|
+
workspace: Workspace;
|
|
52
|
+
cwd?: string;
|
|
53
|
+
profile: CodeAgentProfile;
|
|
54
|
+
model?: CodeAgentModelRef;
|
|
55
|
+
/** Apps whose dev server and preview the turn-end feedback loop watches. */
|
|
56
|
+
apps?: string[];
|
|
57
|
+
extensions?: InlineExtension[];
|
|
58
|
+
customTools?: ToolDefinition[];
|
|
59
|
+
/** `print` for a one-shot stream, `rpc` when a host drives this process over stdio. */
|
|
60
|
+
mode?: CodeAgentHostMode;
|
|
61
|
+
/** How many `task` tools deep this agent already is. */
|
|
62
|
+
depth?: number;
|
|
63
|
+
/** The tally (tokens spent, sub-agents running) of the sub-agent tree whose `task` opened this agent. */
|
|
64
|
+
subagentSpend?: SubagentSpend;
|
|
65
|
+
/** Id of a stored session to continue instead of opening a new one. */
|
|
66
|
+
resume?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class CodeAgent {
|
|
70
|
+
readonly #profile: CodeAgentProfile;
|
|
71
|
+
readonly #gate: CodeAgentGate;
|
|
72
|
+
readonly #mapper = new CodeAgentEventMapper();
|
|
73
|
+
readonly #asks = new CodeAgentAsks();
|
|
74
|
+
#suspended = new CodeAgentSuspended(null);
|
|
75
|
+
readonly #listeners = new Set<(event: CodeAgentEvent) => void>();
|
|
76
|
+
#session: AgentSession | undefined;
|
|
77
|
+
#disposePlugins: (() => void) | undefined;
|
|
78
|
+
#seq = 0;
|
|
79
|
+
#disposed = false;
|
|
80
|
+
#sessionDir: string | undefined;
|
|
81
|
+
#mailbox: CodeMailbox | undefined;
|
|
82
|
+
#mcp: CodeAgentMcpStatus[] = [];
|
|
83
|
+
#workspaceRoot = "";
|
|
84
|
+
// Cleared at `idle`: an earlier turn is in the transcript; only the turn in flight exists nowhere else.
|
|
85
|
+
readonly #replay: CodeAgentEvent[] = [];
|
|
86
|
+
#replayFrom = 0;
|
|
87
|
+
#lastContextTokens: number | undefined;
|
|
88
|
+
|
|
89
|
+
private constructor(profile: CodeAgentProfile) {
|
|
90
|
+
this.#profile = profile;
|
|
91
|
+
this.#gate = new CodeAgentGate(profile);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static async create(options: CodeAgentOptions) {
|
|
95
|
+
const workspaceRoot = options.workspace.workspaceRoot;
|
|
96
|
+
const cwd = options.cwd ?? workspaceRoot;
|
|
97
|
+
const agent = new CodeAgent(options.profile);
|
|
98
|
+
const settingsManager = AkanCodeServices.settings();
|
|
99
|
+
const modelRuntime = await AkanCodeServices.runtime(workspaceRoot, CodeAgentProxy.fromEnv(options.profile));
|
|
100
|
+
const model = await akanCodeModel(modelRuntime, options.model);
|
|
101
|
+
const compaction = settingsManager.getCompactionSettings(model);
|
|
102
|
+
const modelWarnings = akanCodeModelWarnings(model, compaction.reserveTokens + compaction.keepRecentTokens);
|
|
103
|
+
|
|
104
|
+
const akan = await AkanCodePlugins.build({
|
|
105
|
+
workspace: options.workspace,
|
|
106
|
+
cwd,
|
|
107
|
+
profile: options.profile,
|
|
108
|
+
apps: options.apps ?? [],
|
|
109
|
+
canSeeImages: akanCodeModelSupportsImages(model),
|
|
110
|
+
depth: options.depth ?? 0,
|
|
111
|
+
...(options.subagentSpend ? { subagentSpend: options.subagentSpend } : {}),
|
|
112
|
+
currentSessionId: () => agent.sessionId,
|
|
113
|
+
mailbox: () => agent.#mailbox,
|
|
114
|
+
onNotice: (message) => agent.#emit({ type: "notice", level: "warning", message }),
|
|
115
|
+
onAgents: (agents) => agent.#emit({ type: "subagent", agents }),
|
|
116
|
+
onAsk: (question) => agent.ask(question),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
120
|
+
cwd,
|
|
121
|
+
agentDir: akanCodePaths.globalDir(),
|
|
122
|
+
settingsManager,
|
|
123
|
+
...AkanCodeServices.resourceOptions({
|
|
124
|
+
workspaceRoot,
|
|
125
|
+
cwd,
|
|
126
|
+
profile: options.profile,
|
|
127
|
+
extensions: [agent.#gateExtension(), ...akan.extensions, ...(options.extensions ?? [])],
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
await resourceLoader.reload();
|
|
131
|
+
|
|
132
|
+
const { session } = await createAgentSession({
|
|
133
|
+
cwd,
|
|
134
|
+
agentDir: akanCodePaths.globalDir(),
|
|
135
|
+
modelRuntime,
|
|
136
|
+
settingsManager,
|
|
137
|
+
resourceLoader,
|
|
138
|
+
model,
|
|
139
|
+
sessionManager: AkanCodeServices.sessions(workspaceRoot, cwd, options.profile, options.resume),
|
|
140
|
+
noTools: "builtin",
|
|
141
|
+
tools: [...options.profile.tools.builtin, ...akan.toolNames],
|
|
142
|
+
customTools: options.customTools,
|
|
143
|
+
});
|
|
144
|
+
agent.#disposePlugins = akan.dispose;
|
|
145
|
+
agent.#mcp = akan.mcp;
|
|
146
|
+
agent.#workspaceRoot = workspaceRoot;
|
|
147
|
+
if (options.profile.session.store !== "memory") agent.#sessionDir = akanCodePaths.sessionsDir(workspaceRoot);
|
|
148
|
+
await agent.#attach(session, options.mode ?? "print");
|
|
149
|
+
agent.#suspended = new CodeAgentSuspended(CodeAgentSuspended.fileOf(agent.#sessionDir, agent.sessionId));
|
|
150
|
+
if (agent.#sessionDir && options.profile.session.store === "file")
|
|
151
|
+
agent.#openMail(akanCodePaths.mailDir(workspaceRoot), cwd);
|
|
152
|
+
for (const message of modelWarnings) agent.#emit({ type: "notice", level: "warning", message });
|
|
153
|
+
return agent;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get profile() {
|
|
157
|
+
return this.#profile;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
get canSeeImages() {
|
|
161
|
+
const model = this.#session?.model;
|
|
162
|
+
return model ? akanCodeModelSupportsImages(model) : false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The workspace's stored sessions, newest first. Empty on a profile that keeps none. */
|
|
166
|
+
sessions() {
|
|
167
|
+
return this.#sessionDir ? CodeSessionIndex.list(this.#sessionDir) : [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The repo, which is not `profile.paths.root` — that one is narrowed to an app by `akan code --app`. */
|
|
171
|
+
get workspaceRoot() {
|
|
172
|
+
return this.#workspaceRoot;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
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.
|
|
178
|
+
*/
|
|
179
|
+
mcpServers(): CodeAgentMcpStatus[] {
|
|
180
|
+
return this.#mcp;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The other `akan code` sessions running in this workspace right now. */
|
|
184
|
+
peers() {
|
|
185
|
+
return this.#mailbox?.peers() ?? [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The peer receives the text as a prompt of its own, queued as a follow-up when it is mid-turn. */
|
|
189
|
+
send(target: string, text: string) {
|
|
190
|
+
const mailbox = this.#mailbox;
|
|
191
|
+
if (!mailbox) throw new Error("This profile keeps no shared mailbox, so it has no peers.");
|
|
192
|
+
const peer = mailbox.find(target);
|
|
193
|
+
if (!peer) throw new Error(`No live session matches "${target}". Ask for the list first.`);
|
|
194
|
+
mailbox.send(peer.id, text);
|
|
195
|
+
return peer;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#openMail(dir: string, cwd: string) {
|
|
199
|
+
const session = this.#session;
|
|
200
|
+
if (!session) return;
|
|
201
|
+
this.#mailbox = new CodeMailbox(dir, session.sessionId, session.sessionName ?? session.sessionId.slice(0, 8));
|
|
202
|
+
this.#mailbox.open(cwd, (mail) => {
|
|
203
|
+
const from = `${mail.fromName} (${mail.from.slice(0, 8)})`;
|
|
204
|
+
this.#emit({ type: "notice", level: "info", message: `message from ${from}` });
|
|
205
|
+
void this.#prompt(`Message from another session — ${from}:\n${mail.text}`).catch((error: unknown) =>
|
|
206
|
+
this.#emit({ type: "notice", level: "error", message: String(error) }),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
get sessionId() {
|
|
212
|
+
return this.#session?.sessionId ?? "";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get isStreaming() {
|
|
216
|
+
return this.#session?.isStreaming ?? false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get tokensUsed() {
|
|
220
|
+
return this.#session?.getSessionStats().tokens.total ?? 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
get info(): CodeAgentSessionInfo {
|
|
224
|
+
const session = this.#require();
|
|
225
|
+
const model = session.model;
|
|
226
|
+
return {
|
|
227
|
+
sessionId: session.sessionId,
|
|
228
|
+
cwd: this.#profile.paths.root,
|
|
229
|
+
profile: this.#profile.name,
|
|
230
|
+
model: model ? { provider: model.provider, id: model.id, name: model.name } : undefined,
|
|
231
|
+
tools: session.getActiveToolNames(),
|
|
232
|
+
contextTokens: model?.contextWindow,
|
|
233
|
+
// Not `off`: that is a setting of a reasoning model, not the absence of reasoning.
|
|
234
|
+
effort: session.supportsThinking() ? session.thinkingLevel : undefined,
|
|
235
|
+
name: session.sessionName,
|
|
236
|
+
interaction: this.#profile.interaction,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** A `session` frame minted outside this counter would carry `seq: 0`, which every client drops. */
|
|
241
|
+
announce() {
|
|
242
|
+
this.#emit({ type: "session", info: this.info });
|
|
243
|
+
this.#restore();
|
|
244
|
+
//* A resumed worker says again what it is still waiting on, or a host that reattached has no card to answer.
|
|
245
|
+
const { question, approvals } = this.#suspended;
|
|
246
|
+
if (question) this.#emit({ type: "question", question });
|
|
247
|
+
for (const request of approvals) this.#emit({ type: "approval", request });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Prose only: a past turn's tool calls would redraw rows nobody waits on, against a tree that has moved since.
|
|
251
|
+
#restore() {
|
|
252
|
+
const session = this.#session;
|
|
253
|
+
if (!session) return;
|
|
254
|
+
const turnId = `restored:${session.sessionId}`;
|
|
255
|
+
for (const message of session.messages) {
|
|
256
|
+
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
257
|
+
const text = CodeAgent.#textOf(message.content);
|
|
258
|
+
if (text) this.#emit({ type: "message", turnId, role: message.role, text });
|
|
259
|
+
}
|
|
260
|
+
this.#emitContextNow();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static #textOf(content: unknown) {
|
|
264
|
+
if (typeof content === "string") return content.trim();
|
|
265
|
+
if (!Array.isArray(content)) return "";
|
|
266
|
+
return content
|
|
267
|
+
.map((part) => (typeof part === "object" && part && "text" in part ? String(part.text) : ""))
|
|
268
|
+
.join("")
|
|
269
|
+
.trim();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
on(listener: (event: CodeAgentEvent) => void) {
|
|
273
|
+
this.#listeners.add(listener);
|
|
274
|
+
return () => this.#listeners.delete(listener);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** One turn's worth of frames is kept for reconnection; a client further behind reloads the transcript. */
|
|
278
|
+
static readonly replayLimit = 2_000;
|
|
279
|
+
|
|
280
|
+
/** `frames` only when `sinceSeq` is given; a client behind `replayFrom` has to reload the transcript. */
|
|
281
|
+
async state({ sinceSeq }: { sinceSeq?: number } = {}): Promise<CodeAgentState> {
|
|
282
|
+
const base = { info: this.info, streaming: this.isStreaming, replayFrom: this.#replayFrom };
|
|
283
|
+
if (sinceSeq === undefined) return base;
|
|
284
|
+
return { ...base, frames: this.#replay.filter((event) => event.seq > sinceSeq) };
|
|
285
|
+
}
|
|
286
|
+
|
|
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[]) {
|
|
293
|
+
const session = this.#require();
|
|
294
|
+
// From the first prompt, not the answer, so the session is not nameless while it is worth watching.
|
|
295
|
+
if (!session.sessionName && message.trim()) this.setName(codeAgentSessionName(message));
|
|
296
|
+
const attachments = await CodeAgent.#attachments(images);
|
|
297
|
+
const options = attachments.length ? { images: attachments } : {};
|
|
298
|
+
if (session.isStreaming) return await session.prompt(message, { ...options, streamingBehavior: "followUp" });
|
|
299
|
+
await session.prompt(message, { ...options, source: "extension" });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async abort() {
|
|
303
|
+
// The engine finalizes an interrupted message as an ordinary stop, so the abort is noted here.
|
|
304
|
+
this.#mapper.noteOutcome("aborted");
|
|
305
|
+
this.#asks.clear();
|
|
306
|
+
await this.#require().abort();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Not via `prompt`: that opens a turn with the question slot still filled, which the engine does not stop.
|
|
310
|
+
async answer(questionId: string, answer: CodeAgentAnswer) {
|
|
311
|
+
const question = this.#asks.questionOf(questionId);
|
|
312
|
+
const rendered = question ? codeAgentRenderAnswer(question, answer) : (answer.text ?? "");
|
|
313
|
+
if (this.#asks.answer(questionId, rendered)) {
|
|
314
|
+
this.#emit({ type: "question_resolved", questionId, answer, rendered });
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (this.#profile.interaction.question !== "suspend") return false;
|
|
318
|
+
const suspended = this.#suspended.takeQuestion(questionId);
|
|
319
|
+
if (!suspended) return false;
|
|
320
|
+
const suspendedRendered = codeAgentRenderAnswer(suspended, answer);
|
|
321
|
+
this.#emit({ type: "question_resolved", questionId, answer, rendered: suspendedRendered });
|
|
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.`);
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async approve(approvalId: string, approved: boolean) {
|
|
328
|
+
if (this.#asks.resolveApproval(approvalId, approved)) {
|
|
329
|
+
this.#emit({ type: "approval_resolved", approvalId, approved });
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
if (this.#profile.interaction.approval !== "suspend") return false;
|
|
333
|
+
if (!this.#suspended.takeApproval(approvalId)) return false;
|
|
334
|
+
this.#emit({ type: "approval_resolved", approvalId, approved });
|
|
335
|
+
if (approved) await this.#prompt("The user approved the pending action. Retry it and continue.");
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async compact(instructions?: string) {
|
|
340
|
+
await this.#require().compact(instructions);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Renames the session and says so on the wire, because a host draws the name from the session frame. */
|
|
344
|
+
setName(name: string) {
|
|
345
|
+
const clean = name.trim();
|
|
346
|
+
if (!clean) return;
|
|
347
|
+
this.#require().setSessionName(clean);
|
|
348
|
+
this.#mailbox?.rename(clean, this.#profile.paths.root);
|
|
349
|
+
this.#emit({ type: "session", info: this.info });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Copies the session file, not memory, under a new id: the file is what a resume replays. */
|
|
353
|
+
fork(name?: string) {
|
|
354
|
+
if (!this.#sessionDir) throw new Error("This profile keeps no sessions on disk, so there is nothing to fork");
|
|
355
|
+
return CodeSessionFork.fork(this.#sessionDir, this.sessionId, name);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The reasoning levels this model offers, clamped by the engine to what the provider actually serves. */
|
|
359
|
+
efforts(): CodeAgentEffort[] {
|
|
360
|
+
const session = this.#session;
|
|
361
|
+
if (!session?.supportsThinking()) return [];
|
|
362
|
+
return session.getAvailableThinkingLevels();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
setEffort(effort: CodeAgentEffort) {
|
|
366
|
+
const session = this.#require();
|
|
367
|
+
if (!session.supportsThinking()) throw new Error(`${session.model?.id ?? "this model"} does no reasoning`);
|
|
368
|
+
// The engine clamps rather than refuses, so the frame that follows is the level that took, not the one asked.
|
|
369
|
+
session.setThinkingLevel(effort);
|
|
370
|
+
this.#emit({ type: "session", info: this.info });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Every registered provider, keyed or not — `getAvailable()` would list only what is already reachable. */
|
|
374
|
+
catalogue(): CodeAgentProviderInfo[] {
|
|
375
|
+
const runtime = this.#session?.modelRuntime;
|
|
376
|
+
if (!runtime) return [];
|
|
377
|
+
const current = this.#session?.model;
|
|
378
|
+
return runtime
|
|
379
|
+
.getProviders()
|
|
380
|
+
.map((provider) => ({
|
|
381
|
+
id: provider.id,
|
|
382
|
+
name: provider.name ?? provider.id,
|
|
383
|
+
authorized: runtime.hasConfiguredAuth(provider.id),
|
|
384
|
+
models: (runtime.getModels(provider.id) ?? []).map((model) => ({
|
|
385
|
+
id: model.id,
|
|
386
|
+
name: model.name,
|
|
387
|
+
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
|
|
388
|
+
current: model.provider === current?.provider && model.id === current?.id,
|
|
389
|
+
})),
|
|
390
|
+
}))
|
|
391
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async setModel(ref: CodeAgentModelRef) {
|
|
395
|
+
const session = this.#require();
|
|
396
|
+
const model = await akanCodeModel(session.modelRuntime, ref);
|
|
397
|
+
if (!model) throw new Error(`Unknown model: ${ref.provider}/${ref.id}`);
|
|
398
|
+
await session.setModel(model);
|
|
399
|
+
this.#emit({ type: "session", info: this.info });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async waitForIdle() {
|
|
403
|
+
await this.#require().waitForIdle();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
dispose() {
|
|
407
|
+
this.#mailbox?.close();
|
|
408
|
+
if (this.#disposed) return;
|
|
409
|
+
this.#disposed = true;
|
|
410
|
+
this.#asks.clear();
|
|
411
|
+
this.#listeners.clear();
|
|
412
|
+
this.#disposePlugins?.();
|
|
413
|
+
this.#session?.dispose();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async #attach(session: AgentSession, mode: CodeAgentHostMode) {
|
|
417
|
+
this.#session = session;
|
|
418
|
+
session.subscribe((event) => {
|
|
419
|
+
for (const body of this.#mapper.map(event)) this.#emit(body);
|
|
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();
|
|
422
|
+
});
|
|
423
|
+
await session.bindExtensions({
|
|
424
|
+
mode,
|
|
425
|
+
uiContext: this.#ui().context(),
|
|
426
|
+
abortHandler: () => void session.abort(),
|
|
427
|
+
onError: (error: unknown) => this.#emit({ type: "error", message: String(error), fatal: false }),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** A path is read here rather than by the host, so a browser and a terminal hand the core the same thing. */
|
|
432
|
+
static async #attachments(images: CodeAgentImage[] | undefined) {
|
|
433
|
+
if (!images?.length) return [];
|
|
434
|
+
return await Promise.all(
|
|
435
|
+
images.map(async (image) => {
|
|
436
|
+
if ("data" in image) return { type: "image" as const, data: image.data, mimeType: image.mime };
|
|
437
|
+
const file = Bun.file(image.path);
|
|
438
|
+
return {
|
|
439
|
+
type: "image" as const,
|
|
440
|
+
data: Buffer.from(await file.arrayBuffer()).toString("base64"),
|
|
441
|
+
mimeType: file.type || "image/png",
|
|
442
|
+
};
|
|
443
|
+
}),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Also on resume, or a session reopened at 80% shows no usage until its next turn ends.
|
|
448
|
+
#emitContextNow() {
|
|
449
|
+
const usage = this.#session?.getContextUsage();
|
|
450
|
+
// `== null` on purpose: typed `number | null`, but `undefined` must not emit `used: undefined`.
|
|
451
|
+
if (usage?.tokens == null) return;
|
|
452
|
+
if (usage.tokens === this.#lastContextTokens) return;
|
|
453
|
+
this.#lastContextTokens = usage.tokens;
|
|
454
|
+
this.#emit({ type: "context", used: usage.tokens, max: usage.contextWindow || undefined });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#require() {
|
|
458
|
+
if (!this.#session) throw new Error("CodeAgent is not started");
|
|
459
|
+
return this.#session;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#emit(body: CodeAgentEventBody) {
|
|
463
|
+
this.#seq += 1;
|
|
464
|
+
const event = { ...body, seq: this.#seq } as CodeAgentEvent;
|
|
465
|
+
this.#remember(event);
|
|
466
|
+
for (const listener of this.#listeners) listener(event);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#remember(event: CodeAgentEvent) {
|
|
470
|
+
if (event.type === "idle") {
|
|
471
|
+
this.#replay.length = 0;
|
|
472
|
+
this.#replayFrom = event.seq;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
this.#replay.push(event);
|
|
476
|
+
while (this.#replay.length > CodeAgent.replayLimit) {
|
|
477
|
+
const dropped = this.#replay.shift();
|
|
478
|
+
this.#replayFrom = dropped?.seq ?? this.#replayFrom;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
#gateExtension(): InlineExtension {
|
|
483
|
+
return {
|
|
484
|
+
name: "akan-profile-gate",
|
|
485
|
+
factory: (pi: ExtensionAPI) => {
|
|
486
|
+
pi.on("tool_call", async (event) => {
|
|
487
|
+
const verdict = this.#gate.verdict(event.toolName, event.input);
|
|
488
|
+
if (verdict.block) return this.#refuse(event.toolCallId, event.toolName, event.input, verdict.block);
|
|
489
|
+
if (!verdict.approval) return undefined;
|
|
490
|
+
const approved = await this.#requestApproval(event.toolCallId, event.toolName, event.input, verdict.approval);
|
|
491
|
+
if (approved) return undefined;
|
|
492
|
+
return this.#refuse(event.toolCallId, event.toolName, event.input, "The user declined this action.");
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#refuse(toolCallId: string, toolName: string, args: unknown, reason: string) {
|
|
499
|
+
for (const body of this.#mapper.markBlocked(toolCallId, toolName, args, reason)) this.#emit(body);
|
|
500
|
+
return { block: true, reason };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async #requestApproval(toolCallId: string, name: string, args: unknown, summary: string) {
|
|
504
|
+
// Nobody to ask means nobody to refuse: a pod would otherwise deny every write it was created to make.
|
|
505
|
+
if (!this.#profile.ui.canPrompt) return true;
|
|
506
|
+
const approvalId = this.#asks.nextId("a");
|
|
507
|
+
const request = {
|
|
508
|
+
approvalId,
|
|
509
|
+
toolCallId,
|
|
510
|
+
name,
|
|
511
|
+
summary: codeAgentClip(summary, codeAgentLabelChars),
|
|
512
|
+
policy: this.#profile.approval,
|
|
513
|
+
};
|
|
514
|
+
this.#emit({ type: "approval", request });
|
|
515
|
+
if (this.#profile.interaction.approval === "suspend") {
|
|
516
|
+
this.#suspended.addApproval(request);
|
|
517
|
+
this.#mapper.noteOutcome("awaiting");
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
return await this.#asks.openApproval(approvalId);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** The rendered answer; `undefined` when skipped, suspended, or nobody can be prompted. */
|
|
524
|
+
async ask(spec: Omit<CodeAgentQuestion, "questionId">) {
|
|
525
|
+
const question: CodeAgentQuestion = { questionId: this.#asks.nextId("q"), ...spec };
|
|
526
|
+
if (!this.#profile.ui.canPrompt) {
|
|
527
|
+
this.#emit({ type: "question_skipped", question, reason: "no-host" });
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
this.#emit({ type: "question", question });
|
|
531
|
+
if (this.#profile.interaction.question === "suspend") {
|
|
532
|
+
this.#suspended.setQuestion(question);
|
|
533
|
+
this.#mapper.noteOutcome("awaiting");
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
return (await this.#asks.openQuestion(question)) || undefined;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
#ui() {
|
|
540
|
+
return new CodeAgentUi({
|
|
541
|
+
ask: async (prompt, kind, choices) =>
|
|
542
|
+
await this.ask({
|
|
543
|
+
prompt,
|
|
544
|
+
kind,
|
|
545
|
+
...(choices ? { options: choices.map((label) => ({ key: label, label })) } : {}),
|
|
546
|
+
freeText: kind === "text",
|
|
547
|
+
}),
|
|
548
|
+
notify: (level, message) => this.#emit({ type: "notice", level, message }),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { CodeAgentQuestion } from "akanjs/common";
|
|
2
|
+
|
|
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.
|
|
5
|
+
export class CodeAgentAsks {
|
|
6
|
+
#question: { question: CodeAgentQuestion; deferred: PromiseWithResolvers<string> } | undefined;
|
|
7
|
+
readonly #approvals = new Map<string, PromiseWithResolvers<boolean>>();
|
|
8
|
+
#nextId = 0;
|
|
9
|
+
//* Ids outlive the process on a suspending session, so a resumed worker must not hand out `q1` a second time.
|
|
10
|
+
readonly #epoch = Date.now().toString(36);
|
|
11
|
+
|
|
12
|
+
get pendingQuestionId() {
|
|
13
|
+
return this.#question?.question.questionId;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
questionOf(questionId: string) {
|
|
17
|
+
return this.#question?.question.questionId === questionId ? this.#question.question : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get hasPendingApproval() {
|
|
21
|
+
return this.#approvals.size > 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
nextId(prefix: string) {
|
|
25
|
+
this.#nextId += 1;
|
|
26
|
+
return `${prefix}${this.#epoch}.${this.#nextId}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
openQuestion(question: CodeAgentQuestion) {
|
|
30
|
+
// Replaces a pending question: its turn is gone, and leaving it unresolved would strand its caller.
|
|
31
|
+
this.#question?.deferred.resolve("");
|
|
32
|
+
const deferred = Promise.withResolvers<string>();
|
|
33
|
+
this.#question = { question, deferred };
|
|
34
|
+
return deferred.promise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
answer(id: string, text: string) {
|
|
38
|
+
if (this.#question?.question.questionId !== id) return false;
|
|
39
|
+
const { deferred } = this.#question;
|
|
40
|
+
this.#question = undefined;
|
|
41
|
+
deferred.resolve(text);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
openApproval(id: string) {
|
|
46
|
+
const deferred = Promise.withResolvers<boolean>();
|
|
47
|
+
this.#approvals.set(id, deferred);
|
|
48
|
+
return deferred.promise;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
resolveApproval(id: string, approved: boolean) {
|
|
52
|
+
const deferred = this.#approvals.get(id);
|
|
53
|
+
if (!deferred) return false;
|
|
54
|
+
this.#approvals.delete(id);
|
|
55
|
+
deferred.resolve(approved);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Every pending ask dies with its turn — an approval outlives nothing, and a question is reopened on resume. */
|
|
60
|
+
clear() {
|
|
61
|
+
this.#question?.deferred.resolve("");
|
|
62
|
+
this.#question = undefined;
|
|
63
|
+
for (const deferred of this.#approvals.values()) deferred.resolve(false);
|
|
64
|
+
this.#approvals.clear();
|
|
65
|
+
}
|
|
66
|
+
}
|