akanjs 3.0.0-beta.14 → 3.0.0-beta.15

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.
Files changed (58) hide show
  1. package/common/CodeAgentClient.ts +102 -0
  2. package/common/CodeTranscript.ts +337 -0
  3. package/common/codeAgentProfile.ts +175 -0
  4. package/common/codeAgentWire.ts +409 -0
  5. package/common/index.ts +51 -0
  6. package/common/markdownSpans.ts +57 -0
  7. package/dictionary/base.dictionary.ts +1 -0
  8. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  9. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  10. package/package.json +3 -1
  11. package/store/agentic/StToolBuilder.ts +54 -0
  12. package/store/agentic/attachAgentic.ts +2 -1
  13. package/store/agentic/useFormTools.ts +1 -1
  14. package/types/common/CodeAgentClient.d.ts +26 -0
  15. package/types/common/CodeTranscript.d.ts +99 -0
  16. package/types/common/codeAgentProfile.d.ts +111 -0
  17. package/types/common/codeAgentWire.d.ts +388 -0
  18. package/types/common/index.d.ts +7 -0
  19. package/types/common/markdownSpans.d.ts +40 -0
  20. package/types/dictionary/base.dictionary.d.ts +1 -1
  21. package/types/dictionary/dictionary.d.ts +8 -8
  22. package/types/store/agentic/StToolBuilder.d.ts +22 -0
  23. package/types/store/agentic/attachAgentic.d.ts +2 -1
  24. package/types/store/baseSt.d.ts +2 -2
  25. package/types/ui/Agent/Chat.d.ts +7 -1
  26. package/types/ui/Agent/Composer.d.ts +17 -1
  27. package/types/ui/Agent/MentionNode.d.ts +26 -0
  28. package/types/ui/Agent/RichInput.d.ts +22 -0
  29. package/types/ui/Agent/ToolCard.d.ts +16 -0
  30. package/types/ui/Agent/markdownSpans.d.ts +1 -0
  31. package/types/ui/Agent/mentionDraft.d.ts +19 -0
  32. package/types/ui/Agent/useChatReferences.d.ts +3 -2
  33. package/types/ui/UiOverride/context.d.ts +2 -0
  34. package/types/ui/index.d.ts +2 -1
  35. package/types/ui/recipe/badgeRecipe.d.ts +2 -2
  36. package/types/ui/recipe/buttonRecipe.d.ts +2 -2
  37. package/types/vendor/use-agentic/AgentSession.d.ts +15 -1
  38. package/types/vendor/use-agentic/ToolRunner.d.ts +21 -1
  39. package/types/vendor/use-agentic/types.d.ts +31 -1
  40. package/ui/Agent/Chat.tsx +24 -7
  41. package/ui/Agent/Composer.tsx +73 -21
  42. package/ui/Agent/Markdown.tsx +2 -2
  43. package/ui/Agent/MentionNode.ts +62 -0
  44. package/ui/Agent/RichInput.tsx +154 -0
  45. package/ui/Agent/ToolCard.tsx +39 -0
  46. package/ui/Agent/markdownSpans.tsx +26 -36
  47. package/ui/Agent/mentionDraft.ts +101 -0
  48. package/ui/Agent/useChatReferences.ts +5 -8
  49. package/ui/UiOverride/context.ts +2 -0
  50. package/ui/index.ts +2 -1
  51. package/vendor/use-agentic/AgentSession.ts +45 -1
  52. package/vendor/use-agentic/AgenticSurface.ts +2 -0
  53. package/vendor/use-agentic/ToolRunner.ts +55 -1
  54. package/vendor/use-agentic/types.ts +36 -1
  55. /package/{ui/Agent → common}/markdownBlocks.ts +0 -0
  56. /package/{ui/Agent → common}/markdownTable.ts +0 -0
  57. /package/types/{ui/Agent → common}/markdownBlocks.d.ts +0 -0
  58. /package/types/{ui/Agent → common}/markdownTable.d.ts +0 -0
@@ -0,0 +1,102 @@
1
+ import {
2
+ type CodeAgentCommand,
3
+ type CodeAgentEvent,
4
+ type CodeAgentFrame,
5
+ type CodeAgentRequest,
6
+ isCodeAgentReply,
7
+ } from "./codeAgentWire";
8
+
9
+ export interface CodeAgentTransport {
10
+ send(line: string): void | Promise<void>;
11
+ /** Called once with a handler that receives whole lines. */
12
+ onLine(handler: (line: string) => void): void;
13
+ close?(): void | Promise<void>;
14
+ }
15
+
16
+ interface PendingReply {
17
+ resolve: (data: unknown) => void;
18
+ reject: (error: Error) => void;
19
+ }
20
+
21
+ /**
22
+ * Speaks the code-agent wire over any line transport — a child process's stdio, a websocket, an in-process pair.
23
+ *
24
+ * It has no dependencies because the browser holds one too. Everything host-specific (spawning, reconnecting,
25
+ * authenticating) belongs to whoever builds the transport.
26
+ */
27
+ export class CodeAgentClient {
28
+ readonly #transport: CodeAgentTransport;
29
+ readonly #pending = new Map<string, PendingReply>();
30
+ readonly #listeners = new Set<(event: CodeAgentEvent) => void>();
31
+ #nextId = 0;
32
+ #lastSeq = 0;
33
+ #buffer = "";
34
+
35
+ constructor(transport: CodeAgentTransport) {
36
+ this.#transport = transport;
37
+ this.#transport.onLine((line) => this.#receive(line));
38
+ }
39
+
40
+ /** The highest sequence accepted. A reconnecting host replays from here. */
41
+ get lastSeq() {
42
+ return this.#lastSeq;
43
+ }
44
+
45
+ /** A new session restarts the sequence, so the watermark has to go with it or every frame reads as a duplicate. */
46
+ resetSeq() {
47
+ this.#lastSeq = 0;
48
+ }
49
+
50
+ on(listener: (event: CodeAgentEvent) => void) {
51
+ this.#listeners.add(listener);
52
+ return () => this.#listeners.delete(listener);
53
+ }
54
+
55
+ async send(command: CodeAgentCommand): Promise<unknown> {
56
+ this.#nextId += 1;
57
+ const id = `c${this.#nextId}`;
58
+ const request: CodeAgentRequest = { id, command };
59
+ const promise = new Promise<unknown>((resolve, reject) => this.#pending.set(id, { resolve, reject }));
60
+ await this.#transport.send(`${JSON.stringify(request)}\n`);
61
+ return await promise;
62
+ }
63
+
64
+ async close() {
65
+ for (const pending of this.#pending.values()) pending.reject(new Error("code agent transport closed"));
66
+ this.#pending.clear();
67
+ await this.#transport.close?.();
68
+ }
69
+
70
+ /** Feed raw chunks when the transport is byte-oriented rather than line-oriented. */
71
+ push(chunk: string) {
72
+ this.#buffer += chunk;
73
+ let index = this.#buffer.indexOf("\n");
74
+ while (index >= 0) {
75
+ const line = this.#buffer.slice(0, index);
76
+ this.#buffer = this.#buffer.slice(index + 1);
77
+ if (line.trim()) this.#receive(line);
78
+ index = this.#buffer.indexOf("\n");
79
+ }
80
+ }
81
+
82
+ #receive(line: string) {
83
+ let frame: CodeAgentFrame;
84
+ try {
85
+ frame = JSON.parse(line) as CodeAgentFrame;
86
+ } catch {
87
+ return;
88
+ }
89
+ if (isCodeAgentReply(frame)) {
90
+ const pending = this.#pending.get(frame.id);
91
+ if (!pending) return;
92
+ this.#pending.delete(frame.id);
93
+ if (frame.ok) pending.resolve(frame.data);
94
+ else pending.reject(new Error(frame.error ?? "code agent command failed"));
95
+ return;
96
+ }
97
+ if (frame.type !== "event") return;
98
+ if (frame.event.seq <= this.#lastSeq) return;
99
+ this.#lastSeq = frame.event.seq;
100
+ for (const listener of this.#listeners) listener(frame.event);
101
+ }
102
+ }
@@ -0,0 +1,337 @@
1
+ import type {
2
+ CodeAgentApprovalRequest,
3
+ CodeAgentEvent,
4
+ CodeAgentQuestion,
5
+ CodeAgentSessionInfo,
6
+ CodeAgentStopReason,
7
+ CodeAgentToolOutcome,
8
+ CodeAgentToolSummary,
9
+ } from "akanjs/common";
10
+
11
+ export type CodeTranscriptPart =
12
+ | { kind: "user"; id: string; text: string; images?: number }
13
+ | { kind: "assistant"; id: string; text: string; streaming: boolean; truncated: boolean }
14
+ | { kind: "thinking"; id: string; text: string }
15
+ | {
16
+ kind: "tool";
17
+ id: string;
18
+ tool: CodeAgentToolSummary;
19
+ outcome?: CodeAgentToolOutcome;
20
+ output?: string;
21
+ progress?: string;
22
+ }
23
+ | { kind: "notice"; id: string; level: "info" | "warning" | "error"; text: string }
24
+ | { kind: "question"; id: string; question: CodeAgentQuestion; rendered?: string }
25
+ | { kind: "approval"; id: string; request: CodeAgentApprovalRequest; approved?: boolean }
26
+ | { kind: "host"; id: string; hostKind: string; text: string };
27
+
28
+ /**
29
+ * Folds the akan wire into what a screen shows.
30
+ *
31
+ * It reads the contract and nothing else, so the terminal and a browser can share it — and so anything it
32
+ * cannot render is a hole in the contract rather than a missing feature of one host.
33
+ *
34
+ * ⚠️ **Tool and host rows are upserted by id, and the first row of a turn is index `0`.** A truthy check on the
35
+ * looked-up index sends that first `tool_end` down the append path, and the same call ends up on screen twice.
36
+ * `undefined` is the only absence here; `== null` would be wrong for the same reason.
37
+ */
38
+ export class CodeTranscript {
39
+ readonly #parts: CodeTranscriptPart[] = [];
40
+ readonly #indexById = new Map<string, number>();
41
+ #info: CodeAgentSessionInfo | undefined;
42
+ #context: { used: number; max: number | undefined } | undefined;
43
+ #streaming = false;
44
+ #compacting = false;
45
+ #question: CodeAgentQuestion | undefined;
46
+ #approval: CodeAgentApprovalRequest | undefined;
47
+ #stopReason: CodeAgentStopReason | undefined;
48
+ #queue = { steering: 0, followUp: 0 };
49
+ #openAssistant: string | undefined;
50
+ #openThinking: string | undefined;
51
+ #nextId = 0;
52
+ #revision = 0;
53
+
54
+ get parts(): readonly CodeTranscriptPart[] {
55
+ return this.#parts;
56
+ }
57
+
58
+ get info() {
59
+ return this.#info;
60
+ }
61
+
62
+ /**
63
+ * The one line that says what this session is, derived here rather than by each host.
64
+ *
65
+ * The declared context window is on it deliberately: a model descriptor that is wrong but self-consistent —
66
+ * a 65k window declared for a provider that serves 1M — passes every programmatic check there is, and the
67
+ * only thing that catches it is a person reading the number.
68
+ */
69
+ get headline() {
70
+ if (!this.#info) return "starting…";
71
+ return [
72
+ this.#info.model?.name ?? "no model",
73
+ this.#info.contextTokens ? `${Math.round(this.#info.contextTokens / 1000)}k ctx` : "unknown ctx",
74
+ this.#info.profile,
75
+ this.#info.sessionId.slice(0, 8),
76
+ ].join(" · ");
77
+ }
78
+
79
+ get context() {
80
+ return this.#context;
81
+ }
82
+
83
+ get streaming() {
84
+ return this.#streaming;
85
+ }
86
+
87
+ get compacting() {
88
+ return this.#compacting;
89
+ }
90
+
91
+ get question() {
92
+ return this.#question;
93
+ }
94
+
95
+ get approval() {
96
+ return this.#approval;
97
+ }
98
+
99
+ get stopReason() {
100
+ return this.#stopReason;
101
+ }
102
+
103
+ get queue() {
104
+ return this.#queue;
105
+ }
106
+
107
+ /** Bumped on every applied event, so a view can tell "changed" from "same" without comparing arrays. */
108
+ get revision() {
109
+ return this.#revision;
110
+ }
111
+
112
+ /**
113
+ * Drops every row, keeping what the session *is*.
114
+ *
115
+ * Only the screen: the model's own window is untouched, so a cleared transcript and a fresh conversation are
116
+ * two different things and a host that offers this has to say which one it did.
117
+ */
118
+ clear() {
119
+ this.#revision += 1;
120
+ this.#parts.length = 0;
121
+ this.#indexById.clear();
122
+ this.#openAssistant = undefined;
123
+ this.#openThinking = undefined;
124
+ }
125
+
126
+ /**
127
+ * A locally typed prompt, shown before the engine echoes it back as a `message`.
128
+ *
129
+ * Attachments ride beside the text rather than in it: the engine's echo carries the words only, and the
130
+ * two rows are matched by text to keep one bubble.
131
+ */
132
+ echo(text: string, images = 0) {
133
+ this.#revision += 1;
134
+ this.#push({ kind: "user", id: this.#id(), text, ...(images ? { images } : {}) });
135
+ }
136
+
137
+ /** Something the host has to say — a slash-command answer, a key hint — in the same column as the rest. */
138
+ note(level: "info" | "warning" | "error", text: string) {
139
+ this.#revision += 1;
140
+ this.#notice(level, text);
141
+ }
142
+
143
+ apply(event: CodeAgentEvent) {
144
+ this.#revision += 1;
145
+ switch (event.type) {
146
+ case "session":
147
+ this.#info = event.info;
148
+ return;
149
+ case "turn_start":
150
+ this.#streaming = true;
151
+ this.#stopReason = undefined;
152
+ return;
153
+ case "turn_end":
154
+ this.#streaming = false;
155
+ this.#stopReason = event.stopReason;
156
+ this.#closeStreams(event.stopReason === "truncated");
157
+ return;
158
+ case "text_delta":
159
+ return this.#appendAssistant(event.text);
160
+ case "thinking_delta":
161
+ return this.#appendThinking(event.text);
162
+ case "message":
163
+ return this.#message(event.role, event.text);
164
+ case "tool_start":
165
+
166
+ this.#closeStreams(false);
167
+ return this.#upsertTool(event.tool, {});
168
+ case "tool_progress":
169
+ return this.#progress(event.toolCallId, event.text);
170
+ case "tool_end":
171
+ return this.#upsertTool(event.tool, { outcome: event.outcome, output: event.output });
172
+ case "question":
173
+ this.#question = event.question;
174
+ return this.#push({ kind: "question", id: event.question.questionId, question: event.question });
175
+ case "question_resolved":
176
+ if (this.#question?.questionId === event.questionId) this.#question = undefined;
177
+ return this.#patch(event.questionId, (part) => {
178
+ if (part.kind === "question") part.rendered = event.rendered;
179
+ });
180
+ case "approval":
181
+ this.#approval = event.request;
182
+ return this.#push({ kind: "approval", id: event.request.approvalId, request: event.request });
183
+ case "approval_resolved":
184
+ if (this.#approval?.approvalId === event.approvalId) this.#approval = undefined;
185
+ return this.#patch(event.approvalId, (part) => {
186
+ if (part.kind === "approval") part.approved = event.approved;
187
+ });
188
+ case "context":
189
+ this.#context = { used: event.used, max: event.max };
190
+ return;
191
+ case "compaction":
192
+ this.#compacting = event.phase === "start";
193
+ if (event.phase === "end") this.#notice("info", `Compacted the conversation (${event.reason}).`);
194
+ return;
195
+ case "retry":
196
+ return this.#notice(
197
+ "warning",
198
+ `Retrying (${event.attempt}/${event.maxAttempts}) in ${event.delayMs}ms — ${event.message}`,
199
+ );
200
+ case "queue":
201
+ this.#queue = { steering: event.steering.length, followUp: event.followUp.length };
202
+ return;
203
+ case "notice":
204
+ return this.#notice(event.level, event.message);
205
+ case "error":
206
+ return this.#notice("error", event.message);
207
+ case "idle":
208
+ this.#streaming = false;
209
+ this.#queue = { steering: 0, followUp: 0 };
210
+ this.#closeStreams(false);
211
+ return;
212
+ case "host":
213
+ return this.#host(event.kind, event.id, event.payload);
214
+ default:
215
+ return;
216
+ }
217
+ }
218
+
219
+ #id() {
220
+ this.#nextId += 1;
221
+ return `p${this.#nextId}`;
222
+ }
223
+
224
+ #push(part: CodeTranscriptPart) {
225
+ this.#indexById.set(part.id, this.#parts.length);
226
+ this.#parts.push(part);
227
+ }
228
+
229
+ #patch(id: string, mutate: (part: CodeTranscriptPart) => void) {
230
+ const index = this.#indexById.get(id);
231
+ if (index === undefined) return;
232
+ const part = this.#parts[index];
233
+ if (part) mutate(part);
234
+ }
235
+
236
+ #notice(level: "info" | "warning" | "error", text: string) {
237
+ this.#push({ kind: "notice", id: this.#id(), level, text });
238
+ }
239
+
240
+ #appendAssistant(text: string) {
241
+ if (this.#openAssistant === undefined) {
242
+ const id = this.#id();
243
+ this.#openAssistant = id;
244
+ this.#push({ kind: "assistant", id, text, streaming: true, truncated: false });
245
+ return;
246
+ }
247
+ this.#patch(this.#openAssistant, (part) => {
248
+ if (part.kind === "assistant") part.text += text;
249
+ });
250
+ }
251
+
252
+ #appendThinking(text: string) {
253
+ if (this.#openThinking === undefined) {
254
+ const id = this.#id();
255
+ this.#openThinking = id;
256
+ this.#push({ kind: "thinking", id, text });
257
+ return;
258
+ }
259
+ this.#patch(this.#openThinking, (part) => {
260
+ if (part.kind === "thinking") part.text += text;
261
+ });
262
+ }
263
+
264
+ /**
265
+ * The final message wins over the deltas that built it.
266
+ *
267
+ * A retried request streams its first attempt's tokens too, so a bubble assembled from deltas alone shows
268
+ * the abandoned answer followed by the real one.
269
+ */
270
+ #message(role: "user" | "assistant", text: string) {
271
+ if (role === "user") {
272
+
273
+ const last = this.#parts.at(-1);
274
+ if (last?.kind === "user" && last.text === text) return;
275
+ this.#push({ kind: "user", id: this.#id(), text });
276
+ return;
277
+ }
278
+ if (this.#openAssistant !== undefined) {
279
+ const open = this.#openAssistant;
280
+ this.#openAssistant = undefined;
281
+ this.#patch(open, (part) => {
282
+ if (part.kind !== "assistant") return;
283
+ part.text = text;
284
+ part.streaming = false;
285
+ });
286
+ return;
287
+ }
288
+ this.#push({ kind: "assistant", id: this.#id(), text, streaming: false, truncated: false });
289
+ }
290
+
291
+ #closeStreams(truncated: boolean) {
292
+ this.#openThinking = undefined;
293
+ const open = this.#openAssistant;
294
+ this.#openAssistant = undefined;
295
+ if (open === undefined) return;
296
+ this.#patch(open, (part) => {
297
+ if (part.kind !== "assistant") return;
298
+ part.streaming = false;
299
+ part.truncated = truncated;
300
+ });
301
+ }
302
+
303
+ #upsertTool(tool: CodeAgentToolSummary, result: { outcome?: CodeAgentToolOutcome; output?: string }) {
304
+ const index = this.#indexById.get(tool.toolCallId);
305
+ if (index === undefined) {
306
+ this.#push({ kind: "tool", id: tool.toolCallId, tool, ...result });
307
+ return;
308
+ }
309
+ const part = this.#parts[index];
310
+ if (part?.kind !== "tool") return;
311
+
312
+ part.tool = tool;
313
+ if (result.outcome) part.outcome = result.outcome;
314
+ if (result.output !== undefined) part.output = result.output;
315
+ }
316
+
317
+ #progress(toolCallId: string, text: string) {
318
+ this.#patch(toolCallId, (part) => {
319
+ if (part.kind === "tool") part.progress = text;
320
+ });
321
+ }
322
+
323
+ #host(hostKind: string, id: string | undefined, payload: unknown) {
324
+ const text = typeof payload === "string" ? payload : JSON.stringify(payload);
325
+ if (id === undefined) {
326
+ this.#push({ kind: "host", id: this.#id(), hostKind, text });
327
+ return;
328
+ }
329
+ const index = this.#indexById.get(id);
330
+ if (index === undefined) {
331
+ this.#push({ kind: "host", id, hostKind, text });
332
+ return;
333
+ }
334
+ const part = this.#parts[index];
335
+ if (part?.kind === "host") part.text = text;
336
+ }
337
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * What a code agent is allowed to be, as one value.
3
+ *
4
+ * The profile is read by both the host and the core, so it lives here rather than in the CLI. Most of it is
5
+ * applied at **assembly time** — a tool outside `tools` is never constructed, so the model cannot call it and
6
+ * it costs no prompt tokens either. The runtime hooks (`approval`, `paths`, `limits`) are the second gate, for
7
+ * the things assembly cannot decide in advance.
8
+ */
9
+
10
+ export type CodeAgentBuiltinTool = "read" | "write" | "edit" | "ls" | "grep" | "find" | "bash";
11
+
12
+ /** `never` trusts the environment, `all` trusts nothing. The middle two are the useful ones. */
13
+ export type CodeAgentApprovalPolicy = "never" | "writes" | "commands" | "all";
14
+
15
+ /**
16
+ * How the core behaves while a human is being asked something.
17
+ *
18
+ * `await` holds the turn open until the answer arrives. `suspend` resolves the request with a sentinel, ends the
19
+ * turn, and expects the host to reopen one carrying the answer — which is the only form that survives a process
20
+ * restart and does not hold a shared workspace for the minutes or days a person may take to answer.
21
+ */
22
+ export type CodeAgentInteractionMode = "await" | "suspend";
23
+
24
+ export interface CodeAgentMcpServerRef {
25
+ name: string;
26
+ transport: "stdio" | "http";
27
+ /** stdio: the executable and its argv. http: the endpoint. */
28
+ command?: string;
29
+ args?: string[];
30
+ url?: string;
31
+ env?: Record<string, string>;
32
+ }
33
+
34
+ export interface CodeAgentSubagentBudget {
35
+ maxDepth: number;
36
+ maxConcurrent: number;
37
+ /** Tokens a whole subagent tree may spend before the `task` tool refuses to open another. */
38
+ budget: number;
39
+ }
40
+
41
+ export interface CodeAgentProfile {
42
+ name: string;
43
+ tools: {
44
+ builtin: CodeAgentBuiltinTool[];
45
+ /** Workflow, context and self-verification tools built on devkit. */
46
+ akan: boolean;
47
+ /**
48
+ * `"off"` reaches no server at all. An array turns discovery on: the workspace's `.akan/code/mcp.json`
49
+ * plus whatever the array names, so the common case is an empty array.
50
+ */
51
+ mcp: "off" | CodeAgentMcpServerRef[];
52
+ subagent: false | CodeAgentSubagentBudget;
53
+ web: { fetch: boolean; search: boolean };
54
+ };
55
+ approval: CodeAgentApprovalPolicy;
56
+ paths: { root: string; allow?: string[]; deny?: string[] };
57
+ session: { store: "file" | "memory" | "remote"; crossSession: boolean };
58
+ interaction: { question: CodeAgentInteractionMode; approval: CodeAgentInteractionMode };
59
+ network: { allowHosts?: string[]; proxyBaseUrl?: string };
60
+ /** Whether the host can answer a question at all. A pod has nobody in front of it. */
61
+ ui: { canPrompt: boolean };
62
+ limits: {
63
+ turnMs: number;
64
+ toolOutputBytes: number;
65
+ contextTokens: number;
66
+ /** How many times one turn-end plugin may reopen a turn with the same finding before it gives up. */
67
+ feedback: number;
68
+ };
69
+ context: {
70
+ /** The repo's `AGENTS.md` is ~26k tokens; a read-only reviewer does not need it. */
71
+ projectFiles: boolean;
72
+ /**
73
+ * The akan skill set — the scaffolding chain, the store surface, the validation loop.
74
+ *
75
+ * Only each skill's one-line description sits in the window; the body is read when a task matches. That
76
+ * makes it the cheapest context of the three, and the most valuable to a profile carrying no `AGENTS.md`.
77
+ */
78
+ skills: boolean;
79
+ };
80
+ }
81
+
82
+ const allBuiltins: CodeAgentBuiltinTool[] = ["read", "write", "edit", "ls", "grep", "find", "bash"];
83
+ /** The builtins that cannot change anything, which is what a profile is narrowed to when it must not. */
84
+ export const codeAgentReadOnlyBuiltins: CodeAgentBuiltinTool[] = ["read", "ls", "grep", "find"];
85
+
86
+ const baseLimits = { turnMs: 900_000, toolOutputBytes: 200_000, contextTokens: 0, feedback: 2 };
87
+
88
+ /** Paths no profile may read, whatever its allowlist says. */
89
+ export const codeAgentDeniedPaths = ["**/.env", "**/.env.*", "**/secrets/**", "**/*.pem", "**/*.key"];
90
+
91
+ export const codeAgentPresets = {
92
+ local: (root: string): CodeAgentProfile => ({
93
+ name: "local",
94
+ tools: {
95
+ builtin: allBuiltins,
96
+ akan: true,
97
+ mcp: [],
98
+ subagent: { maxDepth: 2, maxConcurrent: 3, budget: 200_000 },
99
+ web: { fetch: true, search: true },
100
+ },
101
+ approval: "never",
102
+ paths: { root, deny: codeAgentDeniedPaths },
103
+ session: { store: "file", crossSession: true },
104
+ interaction: { question: "await", approval: "await" },
105
+ network: {},
106
+ ui: { canPrompt: true },
107
+ limits: baseLimits,
108
+ context: { projectFiles: true, skills: true },
109
+ }),
110
+ /**
111
+ * An isolated container. Everything is on because the container is the boundary, but nobody is watching it:
112
+ * a question that waits for an answer would hang the pod, so both interactions suspend. MCP is off because
113
+ * a pod's egress goes through a proxy and a stdio server started inside it is a process nobody vetted.
114
+ */
115
+ pod: (root: string): CodeAgentProfile => ({
116
+ name: "pod",
117
+ tools: {
118
+ builtin: allBuiltins,
119
+ akan: true,
120
+ mcp: "off",
121
+ subagent: { maxDepth: 2, maxConcurrent: 3, budget: 200_000 },
122
+ web: { fetch: true, search: true },
123
+ },
124
+ approval: "never",
125
+ paths: { root, deny: codeAgentDeniedPaths },
126
+ session: { store: "remote", crossSession: true },
127
+ interaction: { question: "suspend", approval: "suspend" },
128
+ network: {},
129
+ ui: { canPrompt: false },
130
+ limits: baseLimits,
131
+ context: { projectFiles: true, skills: true },
132
+ }),
133
+ /** A reviewer reads the repo and nothing else, so it reaches no external service. */
134
+ review: (root: string): CodeAgentProfile => ({
135
+ name: "review",
136
+ tools: {
137
+ builtin: codeAgentReadOnlyBuiltins,
138
+ akan: true,
139
+ mcp: "off",
140
+ subagent: { maxDepth: 1, maxConcurrent: 4, budget: 120_000 },
141
+ web: { fetch: false, search: false },
142
+ },
143
+ approval: "never",
144
+ paths: { root, deny: codeAgentDeniedPaths },
145
+ session: { store: "memory", crossSession: false },
146
+ interaction: { question: "await", approval: "await" },
147
+ network: {},
148
+ ui: { canPrompt: true },
149
+ limits: baseLimits,
150
+ context: { projectFiles: false, skills: true },
151
+ }),
152
+ /** Isolation and approval are different axes: a pod is safe and its user may still want to be asked. */
153
+ web: (root: string): CodeAgentProfile => ({
154
+ name: "web",
155
+ tools: {
156
+ builtin: allBuiltins,
157
+ akan: true,
158
+ mcp: [],
159
+ subagent: { maxDepth: 2, maxConcurrent: 3, budget: 200_000 },
160
+ web: { fetch: true, search: true },
161
+ },
162
+ approval: "writes",
163
+ paths: { root, deny: codeAgentDeniedPaths },
164
+ session: { store: "file", crossSession: true },
165
+ interaction: { question: "suspend", approval: "suspend" },
166
+ network: {},
167
+ ui: { canPrompt: true },
168
+ limits: baseLimits,
169
+ context: { projectFiles: true, skills: true },
170
+ }),
171
+ } as const;
172
+
173
+ export type CodeAgentPresetName = keyof typeof codeAgentPresets;
174
+
175
+ export const isCodeAgentPresetName = (name: string): name is CodeAgentPresetName => name in codeAgentPresets;