@akanjs/cli 3.1.0-rc.11 → 3.1.0-rc.13
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/.build-stamp +1 -1
- package/{CodeAgent-rkjyxgec.js → CodeAgent-n3tzjm91.js} +2 -2
- package/{CodeAgentRpcHost-p7h6n2hp.js → CodeAgentRpcHost-9g27hmer.js} +1 -1
- package/{CodeTui-h7pyj6xa.js → CodeTui-fnm9ft4d.js} +47 -4
- package/akanCode.js +7 -5
- package/{code.command-0ad6gw2a.js → code.command-130bdmhe.js} +23 -10
- package/guidelines/conventions/conventions.instruction.md +3 -1
- package/guidelines/fieldRule/fieldRule.instruction.md +12 -0
- package/guidelines/queryRule/queryRule.instruction.md +3 -1
- package/{index-8fp3b47h.js → index-1djcjcre.js} +256 -44
- package/{index-wa5jbxj1.js → index-2mrn1mrg.js} +1 -0
- package/{index-d2242ksy.js → index-at7bz0xq.js} +3 -0
- package/{index-20bxxyvn.js → index-dye8pnxx.js} +40 -2
- package/index.js +1 -1
- package/package.json +2 -2
- package/skills/akan-code/SKILL.md +4 -0
- package/skills/akan-store/SKILL.md +2 -1
package/.build-stamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
7bcd4fe279524ed2759e02783eb078a66b8ee0e6acee19a817c2f751ecba695a
|
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
SubagentPool2,
|
|
4
4
|
McpServerConfig,
|
|
5
5
|
McpTokenStore,
|
|
6
|
-
McpSignIn
|
|
7
|
-
|
|
6
|
+
McpSignIn,
|
|
7
|
+
AkanCodeCompaction2
|
|
8
|
+
} from "./index-dye8pnxx.js";
|
|
8
9
|
import {
|
|
9
10
|
CodeTuiLines2,
|
|
10
11
|
CodeTuiAgents2,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
CodeTuiFiles2,
|
|
15
16
|
CodeTuiMarkdown2,
|
|
16
17
|
CodeTuiParts2
|
|
17
|
-
} from "./index-
|
|
18
|
+
} from "./index-2mrn1mrg.js";
|
|
18
19
|
import {
|
|
19
20
|
openBrowser
|
|
20
21
|
} from "./index-9dp031by.js";
|
|
@@ -345,6 +346,34 @@ var CodeTuiApp = ({ actions }) => {
|
|
|
345
346
|
}, undefined, true, undefined, this);
|
|
346
347
|
};
|
|
347
348
|
|
|
349
|
+
// pkgs/@akanjs/cli/code/CodeTuiAutoCompact.ts
|
|
350
|
+
class CodeTuiAutoCompact {
|
|
351
|
+
static parse(argument) {
|
|
352
|
+
const percent = Number(argument.trim().replace(/%$/, ""));
|
|
353
|
+
return argument.trim() && AkanCodeCompaction2.isStep(percent) ? percent : undefined;
|
|
354
|
+
}
|
|
355
|
+
static describe(info) {
|
|
356
|
+
const percent = info?.autoCompactAt;
|
|
357
|
+
const label = `autocompact ${percent ? `${percent}%` : "default"}`;
|
|
358
|
+
const window = info?.contextTokens;
|
|
359
|
+
if (!window)
|
|
360
|
+
return label;
|
|
361
|
+
const threshold = AkanCodeCompaction2.thresholdOf(window, percent);
|
|
362
|
+
const k = (tokens) => `${Math.round(tokens / 1000)}k`;
|
|
363
|
+
const capped = percent && threshold < Math.floor(window * percent / 100) ? `, the latest that still leaves ${k(window - threshold)} for the reply` : "";
|
|
364
|
+
return `${label} \xB7 compacts above ${k(threshold)} of ${k(window)}${capped}`;
|
|
365
|
+
}
|
|
366
|
+
static usage(info) {
|
|
367
|
+
return [
|
|
368
|
+
"Usage: /autocompact <percent>",
|
|
369
|
+
"",
|
|
370
|
+
`now: ${CodeTuiAutoCompact.describe(info)}`,
|
|
371
|
+
`${AkanCodeCompaction2.steps.join(" \xB7 ")} \u2014 100 is the default`
|
|
372
|
+
].join(`
|
|
373
|
+
`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
348
377
|
// pkgs/@akanjs/cli/code/CodeTuiMcp.ts
|
|
349
378
|
class CodeTuiMcp {
|
|
350
379
|
static namePattern = /^[a-zA-Z0-9_-]+$/;
|
|
@@ -897,7 +926,8 @@ class CodeTui {
|
|
|
897
926
|
const context = this.#transcript.context;
|
|
898
927
|
const window = info.contextTokens ? `${Math.round(info.contextTokens / 1000)}k` : "";
|
|
899
928
|
const used = context?.max ? `${Math.round(context.used / context.max * 100)}% of ${window}` : "";
|
|
900
|
-
|
|
929
|
+
const compactsAt = info.autoCompactAt ? `compacts at ${info.autoCompactAt}%` : "";
|
|
930
|
+
return [info.model?.name ?? "no model", used || window && `${window} ctx`, compactsAt, info.profile, info.effort].filter((part) => !!part).join(" \xB7 ");
|
|
901
931
|
}
|
|
902
932
|
#status() {
|
|
903
933
|
if (this.#transcript.compacting)
|
|
@@ -1248,6 +1278,8 @@ class CodeTui {
|
|
|
1248
1278
|
return this.#say("screen cleared \xB7 the model still remembers \xB7 /compact shortens what it carries");
|
|
1249
1279
|
case "compact":
|
|
1250
1280
|
return void this.#agent.compact(argument || undefined).catch((error) => this.#fail(error));
|
|
1281
|
+
case "autocompact":
|
|
1282
|
+
return this.#setAutoCompact(argument);
|
|
1251
1283
|
case "thinking":
|
|
1252
1284
|
this.#thinking = !this.#thinking;
|
|
1253
1285
|
return this.#say(this.#thinking ? "showing the model's reasoning" : "reasoning folded back to one line");
|
|
@@ -1410,6 +1442,17 @@ ${levels.join(" \xB7 ")}`);
|
|
|
1410
1442
|
this.#fail(error);
|
|
1411
1443
|
}
|
|
1412
1444
|
}
|
|
1445
|
+
#setAutoCompact(argument) {
|
|
1446
|
+
const percent = CodeTuiAutoCompact.parse(argument);
|
|
1447
|
+
if (percent === undefined)
|
|
1448
|
+
return this.#open("autocompact", CodeTuiAutoCompact.usage(this.#transcript.info));
|
|
1449
|
+
try {
|
|
1450
|
+
this.#agent.setAutoCompactAt(percent);
|
|
1451
|
+
this.#say(CodeTuiAutoCompact.describe(this.#transcript.info));
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
this.#fail(error);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1413
1456
|
#setModel(argument) {
|
|
1414
1457
|
const wanted = argument.trim();
|
|
1415
1458
|
const [provider, ...rest] = wanted.split("/");
|
package/akanCode.js
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
CodeMailbox2,
|
|
29
29
|
CodeSessionFork2,
|
|
30
30
|
CodeAgent2
|
|
31
|
-
} from "./index-
|
|
31
|
+
} from "./index-1djcjcre.js";
|
|
32
32
|
import {
|
|
33
33
|
akanCodeDefaultModel2,
|
|
34
34
|
akanCodeModel2,
|
|
@@ -38,12 +38,13 @@ import {
|
|
|
38
38
|
} from "./index-c6eggbee.js";
|
|
39
39
|
import {
|
|
40
40
|
SubagentPool2,
|
|
41
|
-
akanCodePaths2
|
|
42
|
-
|
|
41
|
+
akanCodePaths2,
|
|
42
|
+
AkanCodeCompaction2
|
|
43
|
+
} from "./index-dye8pnxx.js";
|
|
43
44
|
import"./index-7md2g9vw.js";
|
|
44
45
|
import {
|
|
45
46
|
CodeAgentRpcHost2
|
|
46
|
-
} from "./index-
|
|
47
|
+
} from "./index-at7bz0xq.js";
|
|
47
48
|
import {
|
|
48
49
|
CodeTuiLines2,
|
|
49
50
|
CodeTuiAgents2,
|
|
@@ -53,9 +54,10 @@ import {
|
|
|
53
54
|
CodeTuiFiles2,
|
|
54
55
|
CodeTuiMarkdown2,
|
|
55
56
|
CodeTuiParts2
|
|
56
|
-
} from "./index-
|
|
57
|
+
} from "./index-2mrn1mrg.js";
|
|
57
58
|
export {
|
|
58
59
|
AkanCli2 as AkanCli,
|
|
60
|
+
AkanCodeCompaction2 as AkanCodeCompaction,
|
|
59
61
|
AkanCodePlugins2 as AkanCodePlugins,
|
|
60
62
|
AkanCodeServices2 as AkanCodeServices,
|
|
61
63
|
AkanEditScope2 as AkanEditScope,
|
|
@@ -19,8 +19,8 @@ class CodeRunner extends runner("code") {
|
|
|
19
19
|
async serve(options, listen) {
|
|
20
20
|
await import("./consoleToStderr-h5bryj94.js");
|
|
21
21
|
const [{ CodeAgent }, { CodeAgentRpcHost }, { CodeAgentRpcListener }] = await Promise.all([
|
|
22
|
-
import("./CodeAgent-
|
|
23
|
-
import("./CodeAgentRpcHost-
|
|
22
|
+
import("./CodeAgent-n3tzjm91.js"),
|
|
23
|
+
import("./CodeAgentRpcHost-9g27hmer.js"),
|
|
24
24
|
import("./CodeAgentRpcListener-7p7z5dd7.js")
|
|
25
25
|
]);
|
|
26
26
|
const address = listen ? CodeAgentRpcListener.parse(listen) : null;
|
|
@@ -34,21 +34,28 @@ class CodeRunner extends runner("code") {
|
|
|
34
34
|
}
|
|
35
35
|
async tui(options, seed) {
|
|
36
36
|
const [{ CodeAgent }, { CodeTui }] = await Promise.all([
|
|
37
|
-
import("./CodeAgent-
|
|
38
|
-
import("./CodeTui-
|
|
37
|
+
import("./CodeAgent-n3tzjm91.js"),
|
|
38
|
+
import("./CodeTui-fnm9ft4d.js")
|
|
39
39
|
]);
|
|
40
40
|
const agentOptions = await CodeRunner.#agentOptions(options, false);
|
|
41
41
|
let resume = options.resume;
|
|
42
|
+
let autoCompactAt = agentOptions.autoCompactAt;
|
|
42
43
|
let prompt = seed;
|
|
43
44
|
let notice;
|
|
44
45
|
for (;; ) {
|
|
45
|
-
const agent = await CodeAgent.create({
|
|
46
|
+
const agent = await CodeAgent.create({
|
|
47
|
+
...agentOptions,
|
|
48
|
+
mode: "tui",
|
|
49
|
+
...resume ? { resume } : {},
|
|
50
|
+
...autoCompactAt ? { autoCompactAt } : {}
|
|
51
|
+
});
|
|
46
52
|
let next;
|
|
47
53
|
try {
|
|
48
54
|
next = await new CodeTui(agent, {
|
|
49
55
|
thinking: options.thinking,
|
|
50
56
|
...notice ? { notice } : {}
|
|
51
57
|
}).run(prompt);
|
|
58
|
+
autoCompactAt = agent.info.autoCompactAt;
|
|
52
59
|
} finally {
|
|
53
60
|
agent.dispose();
|
|
54
61
|
}
|
|
@@ -60,7 +67,7 @@ class CodeRunner extends runner("code") {
|
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
69
|
async run(prompt, options) {
|
|
63
|
-
const { CodeAgent } = await import("./CodeAgent-
|
|
70
|
+
const { CodeAgent } = await import("./CodeAgent-n3tzjm91.js");
|
|
64
71
|
const agent = await CodeAgent.create(await CodeRunner.#agentOptions(options, false));
|
|
65
72
|
const printer = new CodeAgentStreamPrinter2({ json: options.json, thinking: options.thinking });
|
|
66
73
|
agent.on((event) => printer.print(event));
|
|
@@ -80,7 +87,8 @@ class CodeRunner extends runner("code") {
|
|
|
80
87
|
cwd: profile.paths.root,
|
|
81
88
|
profile,
|
|
82
89
|
apps: options.app ? [options.app] : await options.workspace.getApps(),
|
|
83
|
-
model: CodeRunner.modelOf(options.model)
|
|
90
|
+
model: CodeRunner.modelOf(options.model),
|
|
91
|
+
...options.compactAt !== undefined ? { autoCompactAt: options.compactAt } : {}
|
|
84
92
|
};
|
|
85
93
|
}
|
|
86
94
|
static profileOf(options, { hostAttached }) {
|
|
@@ -116,6 +124,7 @@ class CodeScript extends script("code", [CodeRunner]) {
|
|
|
116
124
|
...options.app ? { app: options.app } : {},
|
|
117
125
|
profile: options.profile,
|
|
118
126
|
...options.model ? { model: options.model } : {},
|
|
127
|
+
...options.compactAt !== null ? { compactAt: options.compactAt } : {},
|
|
119
128
|
json: options.json,
|
|
120
129
|
thinking: options.thinking,
|
|
121
130
|
...options.resume ? { resume: options.resume } : {}
|
|
@@ -135,7 +144,11 @@ class CodeScript extends script("code", [CodeRunner]) {
|
|
|
135
144
|
|
|
136
145
|
// pkgs/@akanjs/cli/code/code.command.ts
|
|
137
146
|
class CodeCommand extends command("code", [CodeScript], ({ public: target }) => ({
|
|
138
|
-
code: target({ desc: "Run the akan coding agent on a prompt" }).arg("prompt", String, { desc: "what the agent should do", nullable: true }).option("app", String, { desc: "narrow the agent to one app instead of the whole repo", nullable: true }).option("profile", String, { desc: "local, pod, review, or web", default: "local" }).option("model", String, { desc: "<provider>/<id>, e.g. deepseek/deepseek-flash", nullable: true }).option("
|
|
147
|
+
code: target({ desc: "Run the akan coding agent on a prompt" }).arg("prompt", String, { desc: "what the agent should do", nullable: true }).option("app", String, { desc: "narrow the agent to one app instead of the whole repo", nullable: true }).option("profile", String, { desc: "local, pod, review, or web", default: "local" }).option("model", String, { desc: "<provider>/<id>, e.g. deepseek/deepseek-flash", nullable: true }).option("compactAt", Number, {
|
|
148
|
+
desc: "compact once the context window is this full, in percent: 10, 20, \u2026 100",
|
|
149
|
+
flag: "c",
|
|
150
|
+
nullable: true
|
|
151
|
+
}).option("json", Boolean, { desc: "print one event per line as JSON", default: false }).option("thinking", Boolean, { desc: "print the model's reasoning", default: false }).option("rpc", Boolean, { desc: "serve the agent over stdio for another process to drive", default: false }).option("rpcListen", String, {
|
|
139
152
|
desc: "serve the RPC wire on unix:<path> or tcp:[host:]<port> instead of stdio",
|
|
140
153
|
flag: "L",
|
|
141
154
|
nullable: true
|
|
@@ -143,8 +156,8 @@ class CodeCommand extends command("code", [CodeScript], ({ public: target }) =>
|
|
|
143
156
|
desc: "open the full-screen session even when a prompt is given",
|
|
144
157
|
flag: "i",
|
|
145
158
|
default: false
|
|
146
|
-
}).with(Workspace).exec(async function(prompt, app, profile, model, json, thinking, rpc, rpcListen, resume, interactive, workspace) {
|
|
147
|
-
const options = { app, profile, model, json, thinking, rpc, rpcListen, interactive, resume };
|
|
159
|
+
}).with(Workspace).exec(async function(prompt, app, profile, model, compactAt, json, thinking, rpc, rpcListen, resume, interactive, workspace) {
|
|
160
|
+
const options = { app, profile, model, compactAt, json, thinking, rpc, rpcListen, interactive, resume };
|
|
148
161
|
await this.codeScript.run(workspace, prompt ?? "", options);
|
|
149
162
|
})
|
|
150
163
|
})) {
|
|
@@ -445,7 +445,9 @@ orchestration in `service`, client state in `store`; `Template` is forms, `Unit`
|
|
|
445
445
|
- Import `Int`, `Float`, `ID`, `Any`, `Binary`, `Upload`, `ImageResult`, `enumOf`, `dayjs` from `akanjs/base`;
|
|
446
446
|
`String` / `Boolean` / `Date` are globals. No `JSON` scalar (use `Any`), no `Number`, and `Binary` / `Upload` are
|
|
447
447
|
never fields — a file is `field(File).optional()` / `field([File])`, uploaded by the generated
|
|
448
|
-
`upload<Field>On<Model
|
|
448
|
+
`upload<Field>On<Model>` (which `Field.Img` / `Files` and siblings use when handed the generated setter); it marks
|
|
449
|
+
the field in `<model>FormUploading` while the request runs and drops a result whose form was set whole meanwhile
|
|
450
|
+
(or, on a single field, re-picked) — gate a hand-written submit on it.
|
|
449
451
|
- Defaults: a literal for scalars, a thunk for anything constructed (`default: () => dayjs()`). Arrays
|
|
450
452
|
`field([T])`; optional `.optional()`.
|
|
451
453
|
- **`field.hidden` / `field.secret` read `null` on the client while the type declares them** — guard with `??` or
|
|
@@ -64,6 +64,18 @@ Use this whenever generating constants or scalar constants. It defines the curre
|
|
|
64
64
|
field as a relation to `File`: `image: field(File).optional()` for one, `images: field([File])` for many.
|
|
65
65
|
- The store then auto-generates an `upload<Field>On<Model>(fileList)` action that calls the framework upload
|
|
66
66
|
mutation and polls file status until it leaves `"uploading"` (`pkgs/akanjs/store/action.ts`).
|
|
67
|
+
- **An upload is scoped to the form that started it.** `Field.Img` / `Imgs` / `File` / `Files` handed the generated
|
|
68
|
+
`set<Field>On<Model>` upload through `upload<Field>On<Model>` (any other `onChange` keeps the plain slice upload).
|
|
69
|
+
While the request runs, `<model>FormUploading` holds a ticket under the field's name (`{}` when idle);
|
|
70
|
+
`check<Model>Submitable`, `Model.EditModal` and `Model.ViewEditModal` keep submit disabled, and
|
|
71
|
+
`create` / `update<Model>InForm` (so `submit<Model>`) save nothing. A written result runs `_postSet<Field>` like
|
|
72
|
+
any setter. The result is dropped when the form was set whole meanwhile (`new` / `edit` / `reset`, a save, a
|
|
73
|
+
draft restore, `set<Model>Form`, an edit shell's hydration — the store clears the key on every
|
|
74
|
+
`set({ <model>Form })` — or another record's id) or, on a single field, when a newer upload or a
|
|
75
|
+
`set<Field>On<Model>` superseded it; an array field inserts into the list as it is when the result lands. A form
|
|
76
|
+
replaced inside an immer updater (`set((state) => { state.<model>Form = … })`) is not seen, so its uploads stay
|
|
77
|
+
valid. A hand-written submit reads `Object.keys(st.use.<model>FormUploading()).length` rather than tracking uploads
|
|
78
|
+
itself.
|
|
67
79
|
- Storage is wired through the `StorageAdaptor` DI role (default `BlobStorage`,
|
|
68
80
|
`pkgs/akanjs/service/predefinedAdaptor/storage.adaptor.ts`); the reference implementation is the `file` module.
|
|
69
81
|
Do not hand-roll data-URL fallbacks. `{ cascade: "removeRef" }` on the field removes the file with its owner
|
|
@@ -221,7 +221,9 @@ shape, so `cascade` never means "related" — it means one of exactly these:
|
|
|
221
221
|
and an unmounted `removeRef` target or `removeWith` owner fails the boot rather than the first removal. An
|
|
222
222
|
unmounted `refPath` candidate only warns, since that list spans optional modules by design.
|
|
223
223
|
- **Nothing checks whether another document still references the same target.** `File` in particular is deduped
|
|
224
|
-
by `origin`, so two parents can share one row; `removeRef` claims the field owns its target exclusively.
|
|
224
|
+
by `origin`, so two parents can share one row; `removeRef` claims the field owns its target exclusively. Removing
|
|
225
|
+
the first parent removes the shared row; the second parent's cascade later skips it, since a live-row lookup per
|
|
226
|
+
edge (200 ids at a time) drops a target that is already gone, and still runs the rest of its edges.
|
|
225
227
|
- **A reference no cascade removes keeps pointing at the removed row, and a response reads it live-only**: a list
|
|
226
228
|
leaves the target out, an optional field reads `null`, and a required one answers 404
|
|
227
229
|
`base.error.relationNotFound` naming the model and the field path. Make a reference whose target can go first
|
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
SubagentPool2,
|
|
4
4
|
akanCodePaths2,
|
|
5
5
|
McpServerConfig,
|
|
6
|
-
McpSignIn
|
|
7
|
-
|
|
6
|
+
McpSignIn,
|
|
7
|
+
AkanCodeCompaction2
|
|
8
|
+
} from "./index-dye8pnxx.js";
|
|
8
9
|
import {
|
|
9
10
|
stripAnsi
|
|
10
11
|
} from "./index-7md2g9vw.js";
|
|
@@ -254,7 +255,7 @@ class AkanCodeServices2 {
|
|
|
254
255
|
enableInstallTelemetry: false,
|
|
255
256
|
enableAnalytics: false,
|
|
256
257
|
quietStartup: true,
|
|
257
|
-
compaction: { reserveTokens:
|
|
258
|
+
compaction: { reserveTokens: AkanCodeCompaction2.reserveFloor }
|
|
258
259
|
});
|
|
259
260
|
}
|
|
260
261
|
static async runtime(workspaceRoot, proxy = null) {
|
|
@@ -318,7 +319,7 @@ import {
|
|
|
318
319
|
DefaultResourceLoader
|
|
319
320
|
} from "@earendil-works/pi-coding-agent";
|
|
320
321
|
import {
|
|
321
|
-
codeAgentClip as
|
|
322
|
+
codeAgentClip as codeAgentClip6,
|
|
322
323
|
codeAgentLabelChars as codeAgentLabelChars2,
|
|
323
324
|
codeAgentRenderAnswer,
|
|
324
325
|
codeAgentSessionName as codeAgentSessionName2
|
|
@@ -1395,11 +1396,120 @@ class SessionToolPack2 {
|
|
|
1395
1396
|
}
|
|
1396
1397
|
|
|
1397
1398
|
// pkgs/@akanjs/devkit/codeAgent/tools/WebToolPack.ts
|
|
1398
|
-
import {
|
|
1399
|
+
import { isIP } from "net";
|
|
1400
|
+
import { codeAgentClip as codeAgentClip4 } from "akanjs/common";
|
|
1399
1401
|
import { Type as Type6 } from "typebox";
|
|
1400
1402
|
|
|
1403
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/DeepSeekWeb.ts
|
|
1404
|
+
import { codeAgentClip as codeAgentClip3 } from "akanjs/common";
|
|
1405
|
+
|
|
1406
|
+
class DeepSeekWeb {
|
|
1407
|
+
static baseUrl = "https://api.deepseek.com/v1/tools";
|
|
1408
|
+
static maxQueryChars = 128;
|
|
1409
|
+
static summaryChars = 600;
|
|
1410
|
+
static #retryable = {
|
|
1411
|
+
search: [408, 429, 500, 502, 503, 504],
|
|
1412
|
+
fetch: [429]
|
|
1413
|
+
};
|
|
1414
|
+
static #attempts = 3;
|
|
1415
|
+
#key;
|
|
1416
|
+
#baseUrl;
|
|
1417
|
+
constructor(key, baseUrl = DeepSeekWeb.baseUrl) {
|
|
1418
|
+
this.#key = key;
|
|
1419
|
+
this.#baseUrl = baseUrl;
|
|
1420
|
+
}
|
|
1421
|
+
static async fromRuntime(runtime, proxied) {
|
|
1422
|
+
if (proxied)
|
|
1423
|
+
return;
|
|
1424
|
+
const key = (await runtime.getAuth("deepseek").catch(() => {
|
|
1425
|
+
return;
|
|
1426
|
+
}))?.auth.apiKey;
|
|
1427
|
+
return key ? new DeepSeekWeb(key) : undefined;
|
|
1428
|
+
}
|
|
1429
|
+
async search(query, count, signal) {
|
|
1430
|
+
if ([...query].length > DeepSeekWeb.maxQueryChars)
|
|
1431
|
+
throw new Error(`DeepSeek search takes at most ${DeepSeekWeb.maxQueryChars} characters; shorten the query.`);
|
|
1432
|
+
const body = await this.#call("search", { query, count }, 20000, signal);
|
|
1433
|
+
const byUrl = new Map;
|
|
1434
|
+
for (const result of body.results ?? []) {
|
|
1435
|
+
const url = DeepSeekWeb.#cleanUrl(result.url);
|
|
1436
|
+
if (!byUrl.has(url))
|
|
1437
|
+
byUrl.set(url, { ...result, url });
|
|
1438
|
+
}
|
|
1439
|
+
return [...byUrl.values()].map((result) => {
|
|
1440
|
+
const date = result.date ? ` \xB7 ${result.date.slice(0, 10)}` : "";
|
|
1441
|
+
const summary = DeepSeekWeb.unescape(result.summary || result.snippet || "").replace(/\s+/g, " ").trim();
|
|
1442
|
+
return `- ${result.title}
|
|
1443
|
+
${result.url}${date}
|
|
1444
|
+
${codeAgentClip3(summary, DeepSeekWeb.summaryChars)}`;
|
|
1445
|
+
}).join(`
|
|
1446
|
+
`);
|
|
1447
|
+
}
|
|
1448
|
+
async fetch(url, signal) {
|
|
1449
|
+
const page = await this.#call("fetch", { url }, 30000, signal);
|
|
1450
|
+
return { title: page.title ?? "", content: DeepSeekWeb.unescape(page.content ?? "") };
|
|
1451
|
+
}
|
|
1452
|
+
static unescape(markdown) {
|
|
1453
|
+
const escaped = markdown.match(/\\[!-/:-@[-`{-~]/g)?.length ?? 0;
|
|
1454
|
+
const punctuation = markdown.match(/[!-/:-@[-`{-~]/g)?.length ?? 0;
|
|
1455
|
+
if (!escaped || escaped < (punctuation - escaped) / 2)
|
|
1456
|
+
return markdown;
|
|
1457
|
+
return markdown.replace(/\\([!-/:-@[-`{-~])/g, "$1");
|
|
1458
|
+
}
|
|
1459
|
+
async#call(tool, body, timeoutMs, signal) {
|
|
1460
|
+
for (let attempt = 1;; attempt++) {
|
|
1461
|
+
const response = await fetch(`${this.#baseUrl}/${tool}`, {
|
|
1462
|
+
method: "POST",
|
|
1463
|
+
headers: { authorization: `Bearer ${this.#key}`, "content-type": "application/json" },
|
|
1464
|
+
body: JSON.stringify(body),
|
|
1465
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs)
|
|
1466
|
+
});
|
|
1467
|
+
if (response.ok)
|
|
1468
|
+
return await response.json();
|
|
1469
|
+
if (!DeepSeekWeb.#retryable[tool].includes(response.status) || attempt >= DeepSeekWeb.#attempts) {
|
|
1470
|
+
const error = await response.json().catch(() => {
|
|
1471
|
+
return;
|
|
1472
|
+
});
|
|
1473
|
+
throw new Error(`DeepSeek ${tool} answered ${response.status}: ${error?.error?.message ?? response.statusText}`);
|
|
1474
|
+
}
|
|
1475
|
+
await DeepSeekWeb.#wait(DeepSeekWeb.#delayOf(response, attempt), signal);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
static #delayOf(response, attempt) {
|
|
1479
|
+
const retryAfter = Number(response.headers.get("retry-after"));
|
|
1480
|
+
return Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 5) * 1000 : 1000 * 2 ** (attempt - 1);
|
|
1481
|
+
}
|
|
1482
|
+
static #wait(ms, signal) {
|
|
1483
|
+
return new Promise((resolve) => {
|
|
1484
|
+
const timer = setTimeout(resolve, ms);
|
|
1485
|
+
signal?.addEventListener("abort", () => {
|
|
1486
|
+
clearTimeout(timer);
|
|
1487
|
+
resolve();
|
|
1488
|
+
}, { once: true });
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
static #cleanUrl(url) {
|
|
1492
|
+
try {
|
|
1493
|
+
const parsed = new URL(url);
|
|
1494
|
+
for (const name of [...parsed.searchParams.keys()])
|
|
1495
|
+
if (name.startsWith("utm_"))
|
|
1496
|
+
parsed.searchParams.delete(name);
|
|
1497
|
+
if (/^#\d+$/.test(parsed.hash))
|
|
1498
|
+
parsed.hash = "";
|
|
1499
|
+
return parsed.toString();
|
|
1500
|
+
} catch {
|
|
1501
|
+
return url;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/WebToolPack.ts
|
|
1401
1507
|
class WebToolPack2 {
|
|
1508
|
+
static pageChars = 1e4;
|
|
1509
|
+
static maxResults = 10;
|
|
1510
|
+
static #rememberedPages = 16;
|
|
1402
1511
|
#options;
|
|
1512
|
+
#pages = new Map;
|
|
1403
1513
|
constructor(options) {
|
|
1404
1514
|
this.#options = options;
|
|
1405
1515
|
}
|
|
@@ -1438,7 +1548,7 @@ class WebToolPack2 {
|
|
|
1438
1548
|
const names = [];
|
|
1439
1549
|
if (this.#options.profile.tools.web.fetch)
|
|
1440
1550
|
names.push("web_fetch");
|
|
1441
|
-
if (this.#options.profile.tools.web.search &&
|
|
1551
|
+
if (this.#options.profile.tools.web.search && this.#searcher())
|
|
1442
1552
|
names.push("web_search");
|
|
1443
1553
|
return names;
|
|
1444
1554
|
}
|
|
@@ -1450,58 +1560,124 @@ class WebToolPack2 {
|
|
|
1450
1560
|
#register(pi) {
|
|
1451
1561
|
if (this.#options.profile.tools.web.fetch)
|
|
1452
1562
|
this.#registerFetch(pi);
|
|
1453
|
-
const
|
|
1454
|
-
if (this.#options.profile.tools.web.search &&
|
|
1455
|
-
this.#registerSearch(pi,
|
|
1563
|
+
const searcher = this.#searcher();
|
|
1564
|
+
if (this.#options.profile.tools.web.search && searcher)
|
|
1565
|
+
this.#registerSearch(pi, searcher);
|
|
1456
1566
|
}
|
|
1457
1567
|
#registerFetch(pi) {
|
|
1458
1568
|
const allowHosts = this.#options.profile.network.allowHosts;
|
|
1459
1569
|
pi.registerTool({
|
|
1460
1570
|
name: "web_fetch",
|
|
1461
1571
|
label: "Fetch",
|
|
1462
|
-
description:
|
|
1572
|
+
description: `Fetch a URL and return its readable text, ${WebToolPack2.pageChars} characters at a time. A longer page ends with the offset that continues it.`,
|
|
1463
1573
|
promptSnippet: "web_fetch: fetch a URL and return its readable text",
|
|
1464
|
-
parameters: Type6.Object({
|
|
1465
|
-
|
|
1574
|
+
parameters: Type6.Object({
|
|
1575
|
+
url: Type6.String({ description: "Absolute http(s) URL." }),
|
|
1576
|
+
offset: Type6.Optional(Type6.Number({ description: "Character offset a previous call named, to read on." }))
|
|
1577
|
+
}),
|
|
1578
|
+
execute: async (_id, params, signal) => {
|
|
1466
1579
|
const refusal = WebToolPack2.#refuseHost(params.url, allowHosts);
|
|
1467
1580
|
if (refusal)
|
|
1468
1581
|
return { content: [{ type: "text", text: refusal }], details: undefined, isError: true };
|
|
1469
|
-
const
|
|
1470
|
-
const
|
|
1582
|
+
const offset = Math.max(0, Math.floor(params.offset ?? 0));
|
|
1583
|
+
const cached = offset ? this.#pages.get(params.url) : undefined;
|
|
1584
|
+
const page = cached ?? await this.#read(params.url, signal);
|
|
1585
|
+
if (page.ok)
|
|
1586
|
+
this.#remember(params.url, page);
|
|
1471
1587
|
return {
|
|
1472
|
-
content: [
|
|
1473
|
-
{ type: "text", text: `${response.status} ${params.url}
|
|
1474
|
-
|
|
1475
|
-
${codeAgentClip3(text, codeAgentOutputChars3)}` }
|
|
1476
|
-
],
|
|
1588
|
+
content: [{ type: "text", text: WebToolPack2.#window(params.url, page, offset) }],
|
|
1477
1589
|
details: undefined,
|
|
1478
|
-
isError: !
|
|
1590
|
+
isError: !page.ok
|
|
1479
1591
|
};
|
|
1480
1592
|
}
|
|
1481
1593
|
});
|
|
1482
1594
|
}
|
|
1483
|
-
#registerSearch(pi,
|
|
1595
|
+
#registerSearch(pi, searcher) {
|
|
1596
|
+
const hint = searcher.name === "deepseek" ? ` Keep the query within ${DeepSeekWeb.maxQueryChars} characters; site:, -site: and quoted phrases work.` : "";
|
|
1484
1597
|
pi.registerTool({
|
|
1485
1598
|
name: "web_search",
|
|
1486
1599
|
label: "Search",
|
|
1487
|
-
description: `Search the web (${
|
|
1600
|
+
description: `Search the web (${searcher.name}) and return the top results as title, url and summary.${hint}`,
|
|
1488
1601
|
promptSnippet: "web_search: search the web and return titles, urls and snippets",
|
|
1489
1602
|
parameters: Type6.Object({
|
|
1490
1603
|
query: Type6.String(),
|
|
1491
|
-
count: Type6.Optional(Type6.Number({ description:
|
|
1604
|
+
count: Type6.Optional(Type6.Number({ description: `How many results, default 5, at most ${WebToolPack2.maxResults}.` }))
|
|
1492
1605
|
}),
|
|
1493
|
-
execute: async (_id, params) => {
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1606
|
+
execute: async (_id, params, signal) => {
|
|
1607
|
+
const count = Math.min(Math.max(Math.round(params.count ?? 5), 1), WebToolPack2.maxResults);
|
|
1608
|
+
try {
|
|
1609
|
+
const results = await searcher.search(params.query, count, signal);
|
|
1610
|
+
return {
|
|
1611
|
+
content: [{ type: "text", text: codeAgentClip4(results || "(no results)", WebToolPack2.pageChars) }],
|
|
1612
|
+
details: undefined
|
|
1613
|
+
};
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1616
|
+
return { content: [{ type: "text", text: message }], details: undefined, isError: true };
|
|
1617
|
+
}
|
|
1500
1618
|
}
|
|
1501
1619
|
});
|
|
1502
1620
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1621
|
+
#searcher() {
|
|
1622
|
+
const provider = WebToolPack2.providers.find((entry) => !!process.env[entry.envKey]);
|
|
1623
|
+
if (provider)
|
|
1624
|
+
return {
|
|
1625
|
+
name: provider.name,
|
|
1626
|
+
search: (query, count) => provider.search(process.env[provider.envKey] ?? "", query, count)
|
|
1627
|
+
};
|
|
1628
|
+
const deepseek = this.#options.deepseek;
|
|
1629
|
+
return deepseek ? { name: "deepseek", search: (query, count, signal) => deepseek.search(query, count, signal) } : undefined;
|
|
1630
|
+
}
|
|
1631
|
+
async#read(url, signal) {
|
|
1632
|
+
const deepseek = this.#options.deepseek;
|
|
1633
|
+
if (deepseek && !WebToolPack2.#isLocal(new URL(url).hostname)) {
|
|
1634
|
+
try {
|
|
1635
|
+
const page = await deepseek.fetch(url, signal);
|
|
1636
|
+
return { status: 200, ok: true, title: page.title, text: page.content };
|
|
1637
|
+
} catch (error) {
|
|
1638
|
+
if (signal?.aborted)
|
|
1639
|
+
throw error;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const response = await fetch(url, {
|
|
1643
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(20000)]) : AbortSignal.timeout(20000),
|
|
1644
|
+
redirect: "follow"
|
|
1645
|
+
});
|
|
1646
|
+
return { status: response.status, ok: response.ok, title: "", text: WebToolPack2.#readable(await response.text()) };
|
|
1647
|
+
}
|
|
1648
|
+
#remember(url, page) {
|
|
1649
|
+
this.#pages.delete(url);
|
|
1650
|
+
this.#pages.set(url, page);
|
|
1651
|
+
const oldest = this.#pages.keys().next().value;
|
|
1652
|
+
if (this.#pages.size > WebToolPack2.#rememberedPages && oldest)
|
|
1653
|
+
this.#pages.delete(oldest);
|
|
1654
|
+
}
|
|
1655
|
+
static #window(url, page, offset) {
|
|
1656
|
+
const head = [`${page.status} ${url}`, page.title].filter(Boolean).join(`
|
|
1657
|
+
`);
|
|
1658
|
+
const total = page.text.length;
|
|
1659
|
+
if (offset && offset >= total)
|
|
1660
|
+
return `${head}
|
|
1661
|
+
|
|
1662
|
+
Offset ${offset} is past the end; the page has ${total} characters.`;
|
|
1663
|
+
const end = Math.min(offset + WebToolPack2.pageChars, total);
|
|
1664
|
+
const rest = end < total ? `
|
|
1665
|
+
|
|
1666
|
+
[characters ${offset}\u2013${end} of ${total}; offset ${end} reads on]` : "";
|
|
1667
|
+
return `${head}
|
|
1668
|
+
|
|
1669
|
+
${page.text.slice(offset, end)}${rest}`;
|
|
1670
|
+
}
|
|
1671
|
+
static #isLocal(hostname) {
|
|
1672
|
+
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
1673
|
+
const family = isIP(host);
|
|
1674
|
+
if (family === 6)
|
|
1675
|
+
return host === "::1" || /^(f[cd]|fe[89ab])/.test(host);
|
|
1676
|
+
if (family === 4) {
|
|
1677
|
+
const [a = 0, b = 0] = host.split(".").map(Number);
|
|
1678
|
+
return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
1679
|
+
}
|
|
1680
|
+
return !host.includes(".") || /\.(localhost|local|internal|lan|home\.arpa)$/.test(host);
|
|
1505
1681
|
}
|
|
1506
1682
|
static #refuseHost(url, allowHosts) {
|
|
1507
1683
|
let parsed;
|
|
@@ -1533,7 +1709,7 @@ class AkanCodePlugins2 {
|
|
|
1533
1709
|
});
|
|
1534
1710
|
const dispose = () => mcp?.close();
|
|
1535
1711
|
const mcpStatus = mcp?.status ?? [];
|
|
1536
|
-
const web = new WebToolPack2({ profile: options.profile });
|
|
1712
|
+
const web = new WebToolPack2({ profile: options.profile, deepseek: options.deepseek });
|
|
1537
1713
|
const sessions = new SessionToolPack2({
|
|
1538
1714
|
workspaceRoot: options.workspace.workspaceRoot,
|
|
1539
1715
|
cwd: options.cwd,
|
|
@@ -1668,9 +1844,9 @@ class CodeAgentAsks2 {
|
|
|
1668
1844
|
|
|
1669
1845
|
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentEventMapper.ts
|
|
1670
1846
|
import {
|
|
1671
|
-
codeAgentClip as
|
|
1847
|
+
codeAgentClip as codeAgentClip5,
|
|
1672
1848
|
codeAgentLabelChars,
|
|
1673
|
-
codeAgentOutputChars as
|
|
1849
|
+
codeAgentOutputChars as codeAgentOutputChars3
|
|
1674
1850
|
} from "akanjs/common";
|
|
1675
1851
|
|
|
1676
1852
|
// pkgs/@akanjs/devkit/codeAgent/agent/stringArg.ts
|
|
@@ -1730,7 +1906,7 @@ class CodeAgentEventMapper2 {
|
|
|
1730
1906
|
type: "tool_progress",
|
|
1731
1907
|
turnId: this.#turnId,
|
|
1732
1908
|
toolCallId: event.toolCallId,
|
|
1733
|
-
text:
|
|
1909
|
+
text: codeAgentClip5(CodeAgentEventMapper2.#renderToolText(event.partialResult), codeAgentLabelChars)
|
|
1734
1910
|
}
|
|
1735
1911
|
];
|
|
1736
1912
|
case "tool_execution_end":
|
|
@@ -1801,8 +1977,8 @@ class CodeAgentEventMapper2 {
|
|
|
1801
1977
|
turnId: this.#turnId,
|
|
1802
1978
|
tool,
|
|
1803
1979
|
outcome: reason ? "blocked" : event.isError ? "error" : "ok",
|
|
1804
|
-
output:
|
|
1805
|
-
truncated: full.length >
|
|
1980
|
+
output: codeAgentClip5(full, codeAgentOutputChars3),
|
|
1981
|
+
truncated: full.length > codeAgentOutputChars3
|
|
1806
1982
|
};
|
|
1807
1983
|
}
|
|
1808
1984
|
noteOutcome(reason) {
|
|
@@ -1831,7 +2007,7 @@ class CodeAgentEventMapper2 {
|
|
|
1831
2007
|
name,
|
|
1832
2008
|
op: toolOps[name] ?? "other",
|
|
1833
2009
|
...path ? { path } : {},
|
|
1834
|
-
title:
|
|
2010
|
+
title: codeAgentClip5(`${name}${CodeAgentEventMapper2.#renderArgs(args)}`, codeAgentLabelChars)
|
|
1835
2011
|
};
|
|
1836
2012
|
}
|
|
1837
2013
|
#mapDelta(event) {
|
|
@@ -1858,7 +2034,7 @@ class CodeAgentEventMapper2 {
|
|
|
1858
2034
|
if (!entries.length)
|
|
1859
2035
|
return "";
|
|
1860
2036
|
const rendered = entries.map(([key, value]) => `${key}=${CodeAgentEventMapper2.#renderArgValue(value)}`).join(" ");
|
|
1861
|
-
return `(${
|
|
2037
|
+
return `(${codeAgentClip5(rendered, 120)})`;
|
|
1862
2038
|
}
|
|
1863
2039
|
static #renderArgValue(value) {
|
|
1864
2040
|
if (Array.isArray(value))
|
|
@@ -1867,7 +2043,7 @@ class CodeAgentEventMapper2 {
|
|
|
1867
2043
|
const keys = Object.keys(value).length;
|
|
1868
2044
|
return `{${keys} key${keys === 1 ? "" : "s"}}`;
|
|
1869
2045
|
}
|
|
1870
|
-
return
|
|
2046
|
+
return codeAgentClip5(String(value), 60);
|
|
1871
2047
|
}
|
|
1872
2048
|
static #renderContent(content) {
|
|
1873
2049
|
if (typeof content === "string")
|
|
@@ -2308,6 +2484,7 @@ class CodeAgent2 {
|
|
|
2308
2484
|
#replay = [];
|
|
2309
2485
|
#replayFrom = 0;
|
|
2310
2486
|
#lastContextTokens;
|
|
2487
|
+
#autoCompactAt;
|
|
2311
2488
|
constructor(profile) {
|
|
2312
2489
|
this.#profile = profile;
|
|
2313
2490
|
this.#gate = new CodeAgentGate2(profile);
|
|
@@ -2316,8 +2493,10 @@ class CodeAgent2 {
|
|
|
2316
2493
|
const workspaceRoot = options.workspace.workspaceRoot;
|
|
2317
2494
|
const cwd = options.cwd ?? workspaceRoot;
|
|
2318
2495
|
const agent = new CodeAgent2(options.profile);
|
|
2496
|
+
const autoCompactAt = AkanCodeCompaction2.normalize(options.autoCompactAt);
|
|
2319
2497
|
const settingsManager = AkanCodeServices2.settings();
|
|
2320
|
-
const
|
|
2498
|
+
const proxy = CodeAgentProxy.fromEnv(options.profile);
|
|
2499
|
+
const modelRuntime = await AkanCodeServices2.runtime(workspaceRoot, proxy);
|
|
2321
2500
|
const model = await akanCodeModel2(modelRuntime, options.model);
|
|
2322
2501
|
const compaction = settingsManager.getCompactionSettings(model);
|
|
2323
2502
|
const modelWarnings = akanCodeModelWarnings2(model, compaction.reserveTokens + compaction.keepRecentTokens);
|
|
@@ -2327,6 +2506,7 @@ class CodeAgent2 {
|
|
|
2327
2506
|
profile: options.profile,
|
|
2328
2507
|
apps: options.apps ?? [],
|
|
2329
2508
|
canSeeImages: akanCodeModelSupportsImages2(model),
|
|
2509
|
+
deepseek: await DeepSeekWeb.fromRuntime(modelRuntime, !!proxy),
|
|
2330
2510
|
depth: options.depth ?? 0,
|
|
2331
2511
|
...options.subagentSpend ? { subagentSpend: options.subagentSpend } : {},
|
|
2332
2512
|
currentSessionId: () => agent.sessionId,
|
|
@@ -2365,10 +2545,11 @@ class CodeAgent2 {
|
|
|
2365
2545
|
if (options.profile.session.store !== "memory")
|
|
2366
2546
|
agent.#sessionDir = akanCodePaths2.sessionsDir(workspaceRoot);
|
|
2367
2547
|
await agent.#attach(session, options.mode ?? "print");
|
|
2548
|
+
const autoCompactWarning = agent.#fitAutoCompact(autoCompactAt);
|
|
2368
2549
|
agent.#suspended = new CodeAgentSuspended(CodeAgentSuspended.fileOf(agent.#sessionDir, agent.sessionId));
|
|
2369
2550
|
if (agent.#sessionDir && options.profile.session.store === "file")
|
|
2370
2551
|
agent.#openMail(akanCodePaths2.mailDir(workspaceRoot), cwd);
|
|
2371
|
-
for (const message of modelWarnings)
|
|
2552
|
+
for (const message of [...modelWarnings, ...autoCompactWarning])
|
|
2372
2553
|
agent.#emit({ type: "notice", level: "warning", message });
|
|
2373
2554
|
return agent;
|
|
2374
2555
|
}
|
|
@@ -2433,6 +2614,7 @@ ${mail.text}`).catch((error) => this.#emit({ type: "notice", level: "error", mes
|
|
|
2433
2614
|
tools: session.getActiveToolNames(),
|
|
2434
2615
|
contextTokens: model?.contextWindow,
|
|
2435
2616
|
effort: session.supportsThinking() ? session.thinkingLevel : undefined,
|
|
2617
|
+
autoCompactAt: this.#autoCompactAt,
|
|
2436
2618
|
name: session.sessionName,
|
|
2437
2619
|
interaction: this.#profile.interaction
|
|
2438
2620
|
};
|
|
@@ -2581,8 +2763,38 @@ Continue the task.`);
|
|
|
2581
2763
|
if (!model)
|
|
2582
2764
|
throw new Error(`Unknown model: ${ref.provider}/${ref.id}`);
|
|
2583
2765
|
await session.setModel(model);
|
|
2766
|
+
for (const message of this.#fitAutoCompact(this.#autoCompactAt))
|
|
2767
|
+
this.#emit({ type: "notice", level: "warning", message });
|
|
2768
|
+
this.#emit({ type: "session", info: this.info });
|
|
2769
|
+
}
|
|
2770
|
+
setAutoCompactAt(percent) {
|
|
2771
|
+
const wanted = AkanCodeCompaction2.normalize(percent);
|
|
2772
|
+
const problem = this.#autoCompactProblem(wanted);
|
|
2773
|
+
if (problem)
|
|
2774
|
+
throw new Error(problem);
|
|
2775
|
+
this.#autoCompactAt = wanted;
|
|
2776
|
+
this.#applyAutoCompact();
|
|
2584
2777
|
this.#emit({ type: "session", info: this.info });
|
|
2585
2778
|
}
|
|
2779
|
+
#fitAutoCompact(percent) {
|
|
2780
|
+
const problem = this.#autoCompactProblem(percent);
|
|
2781
|
+
this.#autoCompactAt = problem ? undefined : percent;
|
|
2782
|
+
this.#applyAutoCompact();
|
|
2783
|
+
return problem ? [`${problem} Autocompact is back to the default.`] : [];
|
|
2784
|
+
}
|
|
2785
|
+
#autoCompactProblem(percent) {
|
|
2786
|
+
const session = this.#require();
|
|
2787
|
+
const model = session.model;
|
|
2788
|
+
if (!model || !percent)
|
|
2789
|
+
return;
|
|
2790
|
+
const { keepRecentTokens } = session.settingsManager.getCompactionSettings(model);
|
|
2791
|
+
return AkanCodeCompaction2.problemOf(model, percent, keepRecentTokens);
|
|
2792
|
+
}
|
|
2793
|
+
#applyAutoCompact() {
|
|
2794
|
+
const session = this.#require();
|
|
2795
|
+
if (session.model)
|
|
2796
|
+
AkanCodeCompaction2.apply(session.settingsManager, session.model, this.#autoCompactAt);
|
|
2797
|
+
}
|
|
2586
2798
|
async waitForIdle() {
|
|
2587
2799
|
await this.#require().waitForIdle();
|
|
2588
2800
|
}
|
|
@@ -2689,7 +2901,7 @@ Continue the task.`);
|
|
|
2689
2901
|
approvalId,
|
|
2690
2902
|
toolCallId,
|
|
2691
2903
|
name,
|
|
2692
|
-
summary:
|
|
2904
|
+
summary: codeAgentClip6(summary, codeAgentLabelChars2),
|
|
2693
2905
|
policy: this.#profile.approval
|
|
2694
2906
|
};
|
|
2695
2907
|
this.#emit({ type: "approval", request });
|
|
@@ -250,6 +250,7 @@ class CodeTuiCommands2 {
|
|
|
250
250
|
{ name: "effort", desc: "how hard the model thinks before it answers", arg: "<level>" },
|
|
251
251
|
{ name: "thinking", desc: "show the model's reasoning, or fold it away again" },
|
|
252
252
|
{ name: "compact", desc: "summarise the conversation to free the window", arg: "[notes]" },
|
|
253
|
+
{ name: "autocompact", desc: "how full the window gets before it is summarised", arg: "[<percent>]" },
|
|
253
254
|
{ name: "abort", desc: "stop the running turn" },
|
|
254
255
|
{ name: "clear", desc: "wipe the screen; the model keeps the conversation" },
|
|
255
256
|
{ name: "name", desc: "name this session", arg: "<text>" },
|
|
@@ -77,6 +77,9 @@ class CodeAgentRpcHost2 {
|
|
|
77
77
|
case "set_model":
|
|
78
78
|
await this.#agent.setModel({ provider: command.provider, id: command.modelId });
|
|
79
79
|
return await this.#agent.state();
|
|
80
|
+
case "set_auto_compact":
|
|
81
|
+
this.#agent.setAutoCompactAt(command.percent);
|
|
82
|
+
return await this.#agent.state();
|
|
80
83
|
case "get_state":
|
|
81
84
|
return await this.#agent.state(command.sinceSeq === undefined ? {} : { sinceSeq: command.sinceSeq });
|
|
82
85
|
case "shutdown":
|
|
@@ -3,6 +3,44 @@ import {
|
|
|
3
3
|
ConfigLock
|
|
4
4
|
} from "./index-bqbf654f.js";
|
|
5
5
|
|
|
6
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/AkanCodeCompaction.ts
|
|
7
|
+
class AkanCodeCompaction2 {
|
|
8
|
+
static reserveFloor = 32768;
|
|
9
|
+
static steps = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100];
|
|
10
|
+
static isStep(percent) {
|
|
11
|
+
return AkanCodeCompaction2.steps.some((step) => step === percent);
|
|
12
|
+
}
|
|
13
|
+
static normalize(percent) {
|
|
14
|
+
if (percent === undefined)
|
|
15
|
+
return;
|
|
16
|
+
if (!AkanCodeCompaction2.isStep(percent))
|
|
17
|
+
throw new Error(`Autocompact takes a percent in steps of 10: ${AkanCodeCompaction2.steps.join(", ")}.`);
|
|
18
|
+
return percent === 100 ? undefined : percent;
|
|
19
|
+
}
|
|
20
|
+
static thresholdOf(contextWindow, percent) {
|
|
21
|
+
const share = percent ? Math.floor(contextWindow * percent / 100) : contextWindow;
|
|
22
|
+
return Math.min(share, contextWindow - AkanCodeCompaction2.reserveFloor);
|
|
23
|
+
}
|
|
24
|
+
static apply(settings, model, percent) {
|
|
25
|
+
const threshold = AkanCodeCompaction2.thresholdOf(model.contextWindow, percent);
|
|
26
|
+
settings.applyOverrides({
|
|
27
|
+
compaction: {
|
|
28
|
+
modelOverrides: { [`${model.provider}/${model.id}`]: { reserveTokens: model.contextWindow - threshold } }
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
static problemOf(model, percent, keepRecentTokens) {
|
|
33
|
+
const name = `${model.provider}/${model.id}`;
|
|
34
|
+
if (model.contextWindow <= 0)
|
|
35
|
+
return `${name} declares no context window, so it has no share to compact at.`;
|
|
36
|
+
const threshold = AkanCodeCompaction2.thresholdOf(model.contextWindow, percent);
|
|
37
|
+
if (threshold >= keepRecentTokens)
|
|
38
|
+
return;
|
|
39
|
+
const k = (tokens) => `${Math.round(tokens / 1000)}k`;
|
|
40
|
+
return `Compacting at ${percent}% of the ${k(model.contextWindow)} window of ${name} starts at ${k(threshold)}, under the ${k(keepRecentTokens)} of recent turns every compaction keeps, so every turn would compact again.`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
6
44
|
// pkgs/@akanjs/devkit/codeAgent/agent/akanCodePaths.ts
|
|
7
45
|
import { existsSync } from "fs";
|
|
8
46
|
import { homedir } from "os";
|
|
@@ -141,7 +179,7 @@ class SubagentPool2 {
|
|
|
141
179
|
}
|
|
142
180
|
}
|
|
143
181
|
async#run(id, kind, prompt, signal) {
|
|
144
|
-
const { CodeAgent } = await import("./CodeAgent-
|
|
182
|
+
const { CodeAgent } = await import("./CodeAgent-n3tzjm91.js");
|
|
145
183
|
const budget = this.#options.parent.tools.subagent;
|
|
146
184
|
const child = SubagentPool2.childOf(this.#options.parent, kind);
|
|
147
185
|
const agent = await CodeAgent.create({
|
|
@@ -697,4 +735,4 @@ ${stored.refreshToken}`;
|
|
|
697
735
|
}
|
|
698
736
|
}
|
|
699
737
|
|
|
700
|
-
export { SubagentPool2, akanCodePaths2, McpServerConfig, McpTokenStore, McpSignIn };
|
|
738
|
+
export { SubagentPool2, akanCodePaths2, McpServerConfig, McpTokenStore, McpSignIn, AkanCodeCompaction2 };
|
package/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import path from "path";
|
|
|
15
15
|
var commandModules = {
|
|
16
16
|
workspace: async () => (await import("./workspace.command-6qfgq4b4.js")).WorkspaceCommand,
|
|
17
17
|
agent: async () => (await import("./agent.command-3qxf4xcj.js")).AgentCommand,
|
|
18
|
-
code: async () => (await import("./code.command-
|
|
18
|
+
code: async () => (await import("./code.command-130bdmhe.js")).CodeCommand,
|
|
19
19
|
application: async () => (await import("./application.command-m1kcd7jf.js")).ApplicationCommand,
|
|
20
20
|
library: async () => (await import("./library.command-erp2f56n.js")).LibraryCommand,
|
|
21
21
|
localRegistry: async () => (await import("./localRegistry.command-y176p558.js")).LocalRegistryCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/cli",
|
|
3
|
-
"version": "3.1.0-rc.
|
|
3
|
+
"version": "3.1.0-rc.13",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@earendil-works/pi-tui": "1.3.0",
|
|
40
40
|
"@inquirer/prompts": "^8.7.3",
|
|
41
41
|
"@tailwindcss/node": "^4.3.3",
|
|
42
|
-
"akanjs": "3.1.0-rc.
|
|
42
|
+
"akanjs": "3.1.0-rc.13",
|
|
43
43
|
"chalk": "^6.0.1",
|
|
44
44
|
"commander": "^15.0.0",
|
|
45
45
|
"dayjs": "^1.11.23",
|
|
@@ -78,6 +78,9 @@ Stored under `.akan/code/sessions` — project history, so a second checkout of
|
|
|
78
78
|
- `/peers` lists other `akan code` sessions running in this workspace; `/msg <peer> <text>` hands one a
|
|
79
79
|
message.
|
|
80
80
|
- `/clear` wipes the screen and keeps the conversation; `/compact [notes]` shortens what the model carries.
|
|
81
|
+
- `/autocompact <percent>` compacts on its own once the window is that full, in steps of 10 (`/autocompact 60`);
|
|
82
|
+
`100` is the default, which waits until 32k remain for the reply. `akan code --compact-at 60` (`-c`) starts
|
|
83
|
+
with it, and a share under the 20k every compaction keeps is refused.
|
|
81
84
|
|
|
82
85
|
## Model, effort, keys
|
|
83
86
|
|
|
@@ -119,6 +122,7 @@ akan code -i "<prompt>" open the session with the prompt already sent
|
|
|
119
122
|
--app <name> narrow the agent to one app
|
|
120
123
|
--profile local|pod|review|web what it may do (default local)
|
|
121
124
|
--model <provider>/<id> --thinking, --json, --rpc, --resume <id>
|
|
125
|
+
--compact-at <percent> compact once the window is this full (10, 20, … 100)
|
|
122
126
|
```
|
|
123
127
|
|
|
124
128
|
`review` reads and never writes, loads no project context files, and keeps no session on disk. `pod` has
|
|
@@ -26,7 +26,8 @@ For a model `comment` (`<Slice>` is a signal slice suffix, e.g. `InPublic`):
|
|
|
26
26
|
|
|
27
27
|
## Generated state — `st.use.*`
|
|
28
28
|
|
|
29
|
-
`comment`, `commentLoading`, `commentForm`, `commentFormLoading`, `
|
|
29
|
+
`comment`, `commentLoading`, `commentForm`, `commentFormLoading`, `commentFormUploading` (uploads in flight, per
|
|
30
|
+
`File` field — non-empty means do not submit), `commentSubmit`, `commentModal`,
|
|
30
31
|
`commentViewAt`, `commentOperation`; per slice `commentList<Slice>`, `commentListLoading<Slice>`,
|
|
31
32
|
`commentInitList<Slice>`, `commentSelection<Slice>`, `commentInsight<Slice>`, `pageOfComment<Slice>`,
|
|
32
33
|
`lastPageOfComment<Slice>`, `hasMoreOfComment<Slice>`, `limitOfComment<Slice>`, `sortOfComment<Slice>`.
|