akanjs 3.0.0-beta.13 → 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 (74) 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/agent.dictionary.ts +4 -11
  8. package/dictionary/base.dictionary.ts +1 -0
  9. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  10. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  11. package/package.json +3 -1
  12. package/server/akanOption.ts +9 -2
  13. package/server/di/predefinedAdaptor.ts +2 -2
  14. package/service/predefinedAdaptor/anthropicLlm.ts +6 -4
  15. package/service/predefinedAdaptor/index.ts +0 -1
  16. package/service/predefinedAdaptor/llm.adaptor.ts +21 -1
  17. package/service/predefinedAdaptor/openaiLlm.ts +37 -18
  18. package/store/agentic/StToolBuilder.ts +54 -0
  19. package/store/agentic/attachAgentic.ts +2 -1
  20. package/store/agentic/useFormTools.ts +1 -1
  21. package/types/common/CodeAgentClient.d.ts +26 -0
  22. package/types/common/CodeTranscript.d.ts +99 -0
  23. package/types/common/codeAgentProfile.d.ts +111 -0
  24. package/types/common/codeAgentWire.d.ts +388 -0
  25. package/types/common/index.d.ts +7 -0
  26. package/types/common/markdownSpans.d.ts +40 -0
  27. package/types/dictionary/agent.dictionary.d.ts +1 -1
  28. package/types/dictionary/base.dictionary.d.ts +1 -1
  29. package/types/dictionary/dictionary.d.ts +9 -9
  30. package/types/server/akanOption.d.ts +9 -2
  31. package/types/server/di/predefinedAdaptor.d.ts +2 -2
  32. package/types/service/predefinedAdaptor/anthropicLlm.d.ts +1 -1
  33. package/types/service/predefinedAdaptor/index.d.ts +0 -1
  34. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +14 -1
  35. package/types/service/predefinedAdaptor/openaiLlm.d.ts +20 -11
  36. package/types/store/agentic/StToolBuilder.d.ts +22 -0
  37. package/types/store/agentic/attachAgentic.d.ts +2 -1
  38. package/types/store/baseSt.d.ts +2 -2
  39. package/types/ui/Agent/Chat.d.ts +7 -1
  40. package/types/ui/Agent/Composer.d.ts +17 -1
  41. package/types/ui/Agent/MentionNode.d.ts +26 -0
  42. package/types/ui/Agent/RichInput.d.ts +22 -0
  43. package/types/ui/Agent/ToolCard.d.ts +16 -0
  44. package/types/ui/Agent/markdownSpans.d.ts +1 -0
  45. package/types/ui/Agent/mentionDraft.d.ts +19 -0
  46. package/types/ui/Agent/useChatReferences.d.ts +3 -2
  47. package/types/ui/UiOverride/context.d.ts +2 -0
  48. package/types/ui/index.d.ts +2 -1
  49. package/types/ui/recipe/badgeRecipe.d.ts +2 -2
  50. package/types/ui/recipe/buttonRecipe.d.ts +2 -2
  51. package/types/vendor/use-agentic/AgentSession.d.ts +15 -1
  52. package/types/vendor/use-agentic/ToolRunner.d.ts +21 -1
  53. package/types/vendor/use-agentic/types.d.ts +31 -1
  54. package/ui/Agent/Chat.tsx +24 -7
  55. package/ui/Agent/Composer.tsx +73 -21
  56. package/ui/Agent/Markdown.tsx +2 -2
  57. package/ui/Agent/MentionNode.ts +62 -0
  58. package/ui/Agent/RichInput.tsx +154 -0
  59. package/ui/Agent/ToolCard.tsx +39 -0
  60. package/ui/Agent/markdownSpans.tsx +26 -36
  61. package/ui/Agent/mentionDraft.ts +101 -0
  62. package/ui/Agent/useChatReferences.ts +5 -8
  63. package/ui/UiOverride/context.ts +2 -0
  64. package/ui/index.ts +2 -1
  65. package/vendor/use-agentic/AgentSession.ts +45 -1
  66. package/vendor/use-agentic/AgenticSurface.ts +2 -0
  67. package/vendor/use-agentic/ToolRunner.ts +55 -1
  68. package/vendor/use-agentic/types.ts +36 -1
  69. package/service/predefinedAdaptor/deepseekLlm.ts +0 -82
  70. package/types/service/predefinedAdaptor/deepseekLlm.d.ts +0 -19
  71. /package/{ui/Agent → common}/markdownBlocks.ts +0 -0
  72. /package/{ui/Agent → common}/markdownTable.ts +0 -0
  73. /package/types/{ui/Agent → common}/markdownBlocks.d.ts +0 -0
  74. /package/types/{ui/Agent → common}/markdownTable.d.ts +0 -0
@@ -0,0 +1,409 @@
1
+ import type { CodeAgentApprovalPolicy, CodeAgentInteractionMode, CodeAgentProfile } from "./codeAgentProfile";
2
+
3
+ /**
4
+ * The wire between a code agent core and whatever is driving it — the terminal TUI, the non-interactive stream
5
+ * printer, or a browser over a relay. It is the only thing the two sides share, so it lives here: a browser
6
+ * bundle has to know these types and cannot import the CLI.
7
+ *
8
+ * **These events are ours, not the engine's.** They are a deliberate narrowing of what the underlying agent
9
+ * loop emits, so an engine upgrade stops at the core instead of reaching every host and the web UI. The cost is
10
+ * one mapping function in the core; the alternative is a version bump that breaks three consumers at once.
11
+ *
12
+ * **Nothing here carries a payload proportional to a file.** A `write` call's arguments are the whole new file
13
+ * body and a `read` result is the whole old one; putting either on the wire sends hundreds of KB per edit to a
14
+ * browser and then keeps it there. Labels are clipped to {@link codeAgentLabelChars} and outputs to
15
+ * {@link codeAgentOutputChars}, with `truncated` saying so. The unclipped text lives in the transcript the core
16
+ * owns, which is where a model reads it from anyway.
17
+ *
18
+ * **`seq` belongs to a transport, not to the session end to end.** It is monotonic for the life of one
19
+ * connection, and a relay that merges these frames with its own **re-stamps** them — otherwise a client holds
20
+ * two watermarks and the order between the two streams is undefined. A reconnecting client replays from the
21
+ * last `seq` it saw on *that* transport, and resets to 0 when the session changes, or every frame of the next
22
+ * session reads as a duplicate.
23
+ */
24
+
25
+ export const codeAgentWireVersion = 1;
26
+
27
+ /** One line in a collapsed row. */
28
+ export const codeAgentLabelChars = 200;
29
+ /** A tool result on the wire. The model's own copy is not clipped. */
30
+ export const codeAgentOutputChars = 2_000;
31
+
32
+ /** How hard the model is asked to think before it answers. */
33
+ export type CodeAgentEffort = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
34
+
35
+ export interface CodeAgentSessionInfo {
36
+ sessionId: string;
37
+ cwd: string;
38
+ profile: string;
39
+ model: { provider: string; id: string; name: string } | undefined;
40
+ tools: string[];
41
+ /** Absent when the model reports no window, which is not the same as a window of zero. */
42
+ contextTokens: number | undefined;
43
+ /** Absent when the model does no reasoning at all — which is not the same as reasoning turned `off`. */
44
+ effort: CodeAgentEffort | undefined;
45
+ /** What this session calls itself, once it has been asked something. */
46
+ name: string | undefined;
47
+ /**
48
+ * Whether asking a person ends the turn, read once instead of inferred per question.
49
+ *
50
+ * `await`: the question happens **inside** a turn, the `turnId` does not change, and no `turn_end` is
51
+ * emitted for it. `suspend`: the core emits `turn_end` with `awaiting` right after `question`, and the
52
+ * answer opens a **new** turn with a new `turnId`. A host that treated "question" as a turn boundary in both
53
+ * would either split a turn that never ended or merge two that did.
54
+ */
55
+ interaction: { question: CodeAgentInteractionMode; approval: CodeAgentInteractionMode };
56
+ }
57
+
58
+ /**
59
+ * Why a turn ended.
60
+ *
61
+ * `awaiting` means a person was asked something and the turn was closed to wait for them — it is reported only
62
+ * by a profile whose `interaction` suspends; one that awaits keeps the question inside the turn and never emits
63
+ * it. A host reads {@link CodeAgentSessionInfo.interaction} once to know which shape to expect.
64
+ *
65
+ * **`truncated` is a partial success, not a failure.** The model hit its output cap mid-answer: the text that
66
+ * arrived is real and must be kept, and the useful offer is "continue", not "retry". Folding it into `done` is
67
+ * the expensive mistake — the answer is then stored as complete, and on the next turn the memory window
68
+ * replays it as something the model finished saying, so every later turn reasons from a sentence that stopped
69
+ * in the middle. Folding it into `error` is the other one: it throws away good output and offers a retry that
70
+ * will truncate at the same place.
71
+ *
72
+ * `maxTurns` is a limit on how many times the agent loops, not on how long one message is. Nothing in the core
73
+ * emits it today; it is here for a host that imposes its own ceiling.
74
+ *
75
+ * **Order contract, in force for every profile:**
76
+ * - an assistant `message` for a turn is always emitted **before** that turn's `turn_end`;
77
+ * - that holds for `aborted` and `truncated` too — whatever the model produced before the cut is emitted,
78
+ * because a host that persists on the terminal frame would otherwise store an answer-less turn and the
79
+ * symptom reads as "interrupting loses the reply".
80
+ */
81
+ export type CodeAgentStopReason = "done" | "aborted" | "truncated" | "error" | "awaiting" | "maxTurns";
82
+
83
+ /**
84
+ * `blocked` is not a flavour of `error` — a blocked call never executes, so a host that counts errors, derives
85
+ * a file tree from writes, or retries failures must be able to tell the two apart.
86
+ */
87
+ export type CodeAgentToolOutcome = "ok" | "error" | "blocked";
88
+
89
+ /** What the call does to the workspace, so a host can react without learning every tool name. */
90
+ export type CodeAgentToolOp = "read" | "write" | "delete" | "list" | "search" | "execute" | "other";
91
+
92
+ /**
93
+ * Carried on **both** the start and the end frame.
94
+ *
95
+ * A host folding by `toolCallId` replaces the part on the end frame; a summary present only on start leaves a
96
+ * finished `bash` labelled "bash" with the command gone.
97
+ */
98
+ export interface CodeAgentToolSummary {
99
+ toolCallId: string;
100
+ name: string;
101
+ op: CodeAgentToolOp;
102
+ /**
103
+ * The file this call names, when it names one.
104
+ *
105
+ * ⚠️ **Progress, not a change list.** An edit made through `bash` — `sed -i`, `mv`, a codemod, a script —
106
+ * names no path here, and neither does the sweep of generated barrels a single `akan sync` rewrites, which
107
+ * is most of the files a scaffold touches. A `blocked` call names a path it never wrote. Anything that has
108
+ * to be *correct* about what changed reads `git status`; this field is for showing the row.
109
+ */
110
+ path?: string;
111
+ /** Clipped to {@link codeAgentLabelChars}. */
112
+ title: string;
113
+ }
114
+
115
+ export interface CodeAgentQuestionOption {
116
+ key: string;
117
+ label: string;
118
+ detail?: string;
119
+ /** The one to take when the user says "you decide". */
120
+ recommended?: boolean;
121
+ }
122
+
123
+ /**
124
+ * A question is a conversation turn; an approval is a gate on one tool call. They differ in every property that
125
+ * matters — cardinality, lifetime, whether they survive into the transcript — so they are two events, not one
126
+ * with a discriminator.
127
+ */
128
+ export interface CodeAgentQuestion {
129
+ questionId: string;
130
+ prompt: string;
131
+ kind: "text" | "select" | "confirm";
132
+ options?: CodeAgentQuestionOption[];
133
+ multiSelect?: boolean;
134
+ /** Whether a free-text answer is accepted alongside, or instead of, the options. */
135
+ freeText?: boolean;
136
+ }
137
+
138
+ /**
139
+ * Structured, not a string: a multi-select answer joined into prose cannot be parsed back, so a client that
140
+ * reconnects cannot restore which boxes were ticked. The prose the transcript needs is derived once, by
141
+ * {@link codeAgentRenderAnswer}, rather than by each host separately.
142
+ */
143
+ export interface CodeAgentAnswer {
144
+ keys?: string[];
145
+ text?: string;
146
+ }
147
+
148
+ export interface CodeAgentApprovalRequest {
149
+ approvalId: string;
150
+ toolCallId: string;
151
+ name: string;
152
+ /** What the call will do, rendered and clipped. */
153
+ summary: string;
154
+ policy: CodeAgentApprovalPolicy;
155
+ }
156
+
157
+ export type CodeAgentEventBody =
158
+ | { type: "session"; info: CodeAgentSessionInfo }
159
+ | { type: "turn_start"; turnId: string }
160
+ | { type: "turn_end"; turnId: string; stopReason: CodeAgentStopReason }
161
+ | { type: "text_delta"; turnId: string; text: string }
162
+ | { type: "thinking_delta"; turnId: string; text: string }
163
+ | { type: "message"; turnId: string; role: "user" | "assistant"; text: string }
164
+ | { type: "tool_start"; turnId: string; tool: CodeAgentToolSummary }
165
+ | { type: "tool_progress"; turnId: string; toolCallId: string; text: string }
166
+ | {
167
+ type: "tool_end";
168
+ turnId: string;
169
+ tool: CodeAgentToolSummary;
170
+ outcome: CodeAgentToolOutcome;
171
+ output: string;
172
+ truncated: boolean;
173
+ }
174
+ | { type: "question"; question: CodeAgentQuestion }
175
+ | { type: "question_resolved"; questionId: string; answer: CodeAgentAnswer; rendered: string }
176
+ | { type: "approval"; request: CodeAgentApprovalRequest }
177
+ | { type: "approval_resolved"; approvalId: string; approved: boolean }
178
+ | { type: "context"; used: number; max: number | undefined }
179
+ | { type: "compaction"; phase: "start" | "end"; reason: "manual" | "threshold" | "overflow" }
180
+ | { type: "retry"; attempt: number; maxAttempts: number; delayMs: number; message: string }
181
+ | { type: "queue"; steering: string[]; followUp: string[] }
182
+ | { type: "notice"; level: "info" | "warning" | "error"; message: string }
183
+ | { type: "error"; message: string; fatal: boolean }
184
+ | { type: "idle" }
185
+ /**
186
+ * A frame the **host** made, carried in the core's sequence so it orders against engine frames.
187
+ *
188
+ * `kind` is opaque here: no code in this file or in the core branches on its value. Plan steps, warming and
189
+ * preview lifecycle, deployment progress — all of them belong to whoever runs the agent, and a second channel
190
+ * for them would leave their order against `text_delta` undefined. When `id` is present a client **upserts**
191
+ * by it, because one step emitting pending → running → done must be one row, not three.
192
+ *
193
+ * `persist` is per **frame**, not per `kind`: the same plan step is worth storing when it reports `done` and
194
+ * must not be stored while it is still `pending`, because a step that never started reads later as one that
195
+ * did. It defaults to false — a host that forgets to set it loses a row, which is cheaper than a permanent
196
+ * record of every transient status it ever emitted.
197
+ */
198
+ | { type: "host"; kind: string; id?: string; persist?: boolean; payload: unknown };
199
+
200
+ export type CodeAgentEvent = CodeAgentEventBody & { seq: number };
201
+
202
+ export type CodeAgentEventType = CodeAgentEventBody["type"];
203
+
204
+ /**
205
+ * Whether an event belongs in a stored transcript or only on a live screen.
206
+ *
207
+ * Folding and persisting are different rules, and conflating them is a bug in both directions: a `pending`
208
+ * step written to permanent history reads later as "this was done", and a reconnecting client that replays
209
+ * only persisted frames loses the tool row it was watching.
210
+ *
211
+ * **A transcript is not a memory window.** What a person sees on reopening and what the model is given on the
212
+ * next turn are two questions, and this table answers only the first. The memory window is derived from
213
+ * `message` rows and nothing else — a summariser fed `turn_start`, `turn_end` and `compaction` rows spends its
214
+ * input on bookkeeping. That is also why a question and its answer are rendered into message text rather than
215
+ * left as structure: the next turn reads content, not frames.
216
+ *
217
+ * `host` is the one entry the table cannot settle, because the same `kind` persists in one frame and not in the
218
+ * next. Its row is the default; the frame's own `persist` decides. Use {@link codeAgentShouldPersist}.
219
+ */
220
+ export const codeAgentEventPersistence: { [key in CodeAgentEventType]: "live" | "persist" } = {
221
+ session: "live",
222
+ turn_start: "persist",
223
+ turn_end: "persist",
224
+ text_delta: "live",
225
+ thinking_delta: "live",
226
+ message: "persist",
227
+ tool_start: "live",
228
+ tool_progress: "live",
229
+ tool_end: "persist",
230
+ question: "persist",
231
+ question_resolved: "persist",
232
+ approval: "live",
233
+ approval_resolved: "live",
234
+ context: "live",
235
+ compaction: "persist",
236
+ retry: "live",
237
+ queue: "live",
238
+ notice: "live",
239
+ error: "persist",
240
+ idle: "live",
241
+ host: "live",
242
+ };
243
+
244
+ /**
245
+ * `answer` and `approve` are separate from `prompt` on purpose: answering a pending question is not a new
246
+ * instruction, and a host that routed it through `prompt` would open a turn while the question slot is still
247
+ * occupied — leaving a card on screen that still looks clickable.
248
+ */
249
+ /**
250
+ * A browser holds bytes and a terminal holds a path, so `string[]` would be implemented differently on each
251
+ * side and diverge the first time one of them handed the other its own form.
252
+ */
253
+ export type CodeAgentImage = { path: string } | { data: string; mime: string };
254
+
255
+ /**
256
+ * The name a session takes from the first thing it was asked.
257
+ *
258
+ * Derived from the text rather than written by the model: a session name is worth one glance in a list, and
259
+ * generating one would be a second request standing between the person and their first answer — charged again
260
+ * on every session that is opened and abandoned.
261
+ */
262
+ export const codeAgentSessionName = (text: string, max = 40) => {
263
+ const words = text
264
+ .replace(/```[\s\S]*?```/g, " ")
265
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
266
+ .trim()
267
+ .toLowerCase()
268
+ .split(/\s+/)
269
+ .filter((word) => !!word);
270
+ const name: string[] = [];
271
+ for (const word of words) {
272
+ if (name.length && [...name, word].join("-").length > max) break;
273
+ name.push(word);
274
+ }
275
+ return name.join("-").slice(0, max) || "session";
276
+ };
277
+
278
+ export type CodeAgentCommand =
279
+ | { type: "prompt"; message: string; images?: CodeAgentImage[]; deliverAs?: "steer" | "followUp" }
280
+ | { type: "answer"; questionId: string; answer: CodeAgentAnswer }
281
+ | { type: "approve"; approvalId: string; approved: boolean }
282
+ | { type: "abort" }
283
+ | { type: "compact"; instructions?: string }
284
+ | { type: "set_model"; provider: string; modelId: string }
285
+ | { type: "fork"; entryId: string }
286
+ | { type: "new_session" }
287
+ /**
288
+ * Current state, plus every frame after `sinceSeq` when one is given.
289
+ *
290
+ * A browser that reloads mid-turn needs the frames it missed, not a snapshot: the completed messages survive
291
+ * in the transcript either way, but the `text_delta` and `tool_start` frames of the bubble still being
292
+ * written exist nowhere else. A terminal host never discovers this — its process and its session die
293
+ * together — which is why it is in the contract rather than waiting for the web host to find it.
294
+ */
295
+ | { type: "get_state"; sinceSeq?: number }
296
+ | { type: "shutdown" };
297
+
298
+ export type CodeAgentCommandType = CodeAgentCommand["type"];
299
+
300
+ export interface CodeAgentRequest {
301
+ id: string;
302
+ command: CodeAgentCommand;
303
+ }
304
+
305
+ export interface CodeAgentReply {
306
+ type: "reply";
307
+ id: string;
308
+ ok: boolean;
309
+ error?: string;
310
+ data?: unknown;
311
+ }
312
+
313
+ export type CodeAgentFrame = ({ type: "event" } & { event: CodeAgentEvent }) | CodeAgentReply;
314
+
315
+ export const isCodeAgentReply = (frame: CodeAgentFrame): frame is CodeAgentReply => frame.type === "reply";
316
+
317
+ /** The table, with a `host` frame's own `persist` taking precedence over the default. */
318
+ export const codeAgentShouldPersist = (event: CodeAgentEventBody) =>
319
+ event.type === "host" ? event.persist === true : codeAgentEventPersistence[event.type] === "persist";
320
+
321
+ export const codeAgentClip = (text: string, max: number) => (text.length <= max ? text : `${text.slice(0, max - 1)}…`);
322
+
323
+ /**
324
+ * The prose form of an answer, made once.
325
+ *
326
+ * Compaction and the next turn read `role`/`content` only, so an answer that exists solely as structure is an
327
+ * answer the agent will not remember giving.
328
+ */
329
+ export const codeAgentRenderAnswer = (question: CodeAgentQuestion, answer: CodeAgentAnswer) => {
330
+ const labels = (answer.keys ?? []).map((key) => question.options?.find((option) => option.key === key)?.label ?? key);
331
+ return [labels.join(", "), answer.text].filter(Boolean).join(" — ");
332
+ };
333
+
334
+ const toolOutcomeMark: { [key in CodeAgentToolOutcome]: string } = { ok: "✓", error: "✗", blocked: "⦸" };
335
+
336
+ /** A one-line rendering of an event, shared by the stream printer and the TUI so the two cannot drift. */
337
+ export const codeAgentEventLabel = (event: CodeAgentEventBody): string => {
338
+ switch (event.type) {
339
+ case "session":
340
+
341
+ return [
342
+ `session ${event.info.sessionId}`,
343
+ event.info.model?.name ?? "no model",
344
+ event.info.contextTokens ? `${Math.round(event.info.contextTokens / 1000)}k ctx` : "unknown ctx",
345
+ event.info.profile,
346
+ ].join(" · ");
347
+ case "turn_start":
348
+ return "turn start";
349
+ case "turn_end":
350
+ return event.stopReason === "truncated"
351
+ ? "turn end — the answer was cut off at the model's output limit"
352
+ : `turn end (${event.stopReason})`;
353
+ case "text_delta":
354
+ return event.text;
355
+ case "thinking_delta":
356
+ return event.text;
357
+ case "message":
358
+ return `${event.role}: ${event.text}`;
359
+ case "tool_start":
360
+ return `→ ${event.tool.title}`;
361
+ case "tool_end":
362
+ return `${toolOutcomeMark[event.outcome]} ${event.tool.title}`;
363
+ case "tool_progress":
364
+ return ` ${event.text}`;
365
+ case "question":
366
+ return `? ${event.question.prompt}`;
367
+ case "question_resolved":
368
+ return `= ${event.rendered}`;
369
+ case "approval":
370
+ return `approve? ${event.request.summary}`;
371
+ case "approval_resolved":
372
+ return event.approved ? "approved" : "denied";
373
+ case "context":
374
+ return `context ${event.used}${event.max ? `/${event.max}` : ""}`;
375
+ case "compaction":
376
+ return `compaction ${event.phase} (${event.reason})`;
377
+ case "retry":
378
+ return `retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms — ${event.message}`;
379
+ case "queue":
380
+ return `queued ${event.steering.length} steering, ${event.followUp.length} follow-up`;
381
+ case "notice":
382
+ return `[${event.level}] ${event.message}`;
383
+ case "error":
384
+ return `error: ${event.message}`;
385
+ case "idle":
386
+ return "idle";
387
+ case "host":
388
+ return `${event.kind}${event.id ? ` ${event.id}` : ""}`;
389
+ default:
390
+ return "";
391
+ }
392
+ };
393
+
394
+ export interface CodeAgentState {
395
+ info: CodeAgentSessionInfo;
396
+ streaming: boolean;
397
+ /** Present only when `get_state` asked for a replay. Empty when nothing was missed. */
398
+ frames?: CodeAgentEvent[];
399
+ /** How far back a replay can reach. A client behind this must reload the transcript instead. */
400
+ replayFrom: number;
401
+ }
402
+
403
+ export interface CodeAgentStartOptions {
404
+ cwd: string;
405
+ profile: CodeAgentProfile;
406
+ model?: { provider: string; id: string };
407
+ /** Resume this session instead of opening a new one. */
408
+ sessionId?: string;
409
+ }
package/common/index.ts CHANGED
@@ -7,6 +7,8 @@ export {
7
7
  legacyAuthTokenKey,
8
8
  readAuthToken,
9
9
  } from "./authToken";
10
+ export { CodeAgentClient, type CodeAgentTransport } from "./CodeAgentClient";
11
+ export { CodeTranscript, type CodeTranscriptPart } from "./CodeTranscript";
10
12
  export { capitalize } from "./capitalize";
11
13
  export { clamp } from "./clamp";
12
14
  export {
@@ -15,6 +17,52 @@ export {
15
17
  forwardedHeaders,
16
18
  normalizeIpAddress,
17
19
  } from "./clientAddress";
20
+ export {
21
+ type CodeAgentApprovalPolicy,
22
+ type CodeAgentBuiltinTool,
23
+ type CodeAgentInteractionMode,
24
+ type CodeAgentMcpServerRef,
25
+ type CodeAgentPresetName,
26
+ type CodeAgentProfile,
27
+ type CodeAgentSubagentBudget,
28
+ codeAgentDeniedPaths,
29
+ codeAgentPresets,
30
+ codeAgentReadOnlyBuiltins,
31
+ isCodeAgentPresetName,
32
+ } from "./codeAgentProfile";
33
+ export {
34
+ type CodeAgentAnswer,
35
+ type CodeAgentApprovalRequest,
36
+ type CodeAgentCommand,
37
+ type CodeAgentCommandType,
38
+ type CodeAgentEffort,
39
+ type CodeAgentEvent,
40
+ type CodeAgentEventBody,
41
+ type CodeAgentEventType,
42
+ type CodeAgentFrame,
43
+ type CodeAgentImage,
44
+ type CodeAgentQuestion,
45
+ type CodeAgentQuestionOption,
46
+ type CodeAgentReply,
47
+ type CodeAgentRequest,
48
+ type CodeAgentSessionInfo,
49
+ type CodeAgentStartOptions,
50
+ type CodeAgentState,
51
+ type CodeAgentStopReason,
52
+ type CodeAgentToolOp,
53
+ type CodeAgentToolOutcome,
54
+ type CodeAgentToolSummary,
55
+ codeAgentClip,
56
+ codeAgentEventLabel,
57
+ codeAgentEventPersistence,
58
+ codeAgentLabelChars,
59
+ codeAgentOutputChars,
60
+ codeAgentRenderAnswer,
61
+ codeAgentSessionName,
62
+ codeAgentShouldPersist,
63
+ codeAgentWireVersion,
64
+ isCodeAgentReply,
65
+ } from "./codeAgentWire";
18
66
  export { deepObjectify } from "./deepObjectify";
19
67
  export type { DynamicRecord } from "./dynamicRecord";
20
68
  export { EventStream, type EventStreamOptions } from "./eventStream";
@@ -66,6 +114,9 @@ export {
66
114
  registerLogContextReader,
67
115
  } from "./logContext";
68
116
  export { lowerlize } from "./lowerlize";
117
+ export { type MarkdownBlock, MarkdownBlocks, type MarkdownItem } from "./markdownBlocks";
118
+ export { type MarkdownSpan, MarkdownSpans } from "./markdownSpans";
119
+ export { type Align, MarkdownTable, type TableBlock } from "./markdownTable";
69
120
  export {
70
121
  isMcpDescribableArg,
71
122
  type McpExposureEndpoint,
@@ -0,0 +1,57 @@
1
+ export type MarkdownSpan =
2
+ | { kind: "text"; text: string }
3
+ | { kind: "code"; text: string }
4
+ | { kind: "strong"; text: string }
5
+ | { kind: "em"; text: string }
6
+ | { kind: "del"; text: string }
7
+ | { kind: "link"; text: string; href: string }
8
+ /** A link whose href was refused, or an image: rendered as its label, by every host. */
9
+ | { kind: "plain"; text: string };
10
+
11
+ const inline =
12
+ /(!?)\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)\)|`([^`]+)`|\*\*([\s\S]+?)\*\*|\*([^*\n]+?)\*|~~([\s\S]+?)~~/g;
13
+
14
+ /**
15
+ * The inline scanner both hosts read: a browser turns these into elements, a terminal into styled text.
16
+ *
17
+ * It is here rather than beside either renderer because the two decisions that matter are not rendering
18
+ * decisions. **A link's scheme is refused for everyone** — this text comes from a model and from tool results
19
+ * carrying stored user input, and React writes a `javascript:` href out as given. And **underscore emphasis is
20
+ * deliberately unmatched**: snake_case is everywhere in this content, and `some_var_name` italicising mid-word
21
+ * reads worse than a literal `_emphasis_` does.
22
+ */
23
+ export class MarkdownSpans {
24
+ static of(text: string): MarkdownSpan[] {
25
+ const spans: MarkdownSpan[] = [];
26
+ let cut = 0;
27
+ for (const match of text.matchAll(inline)) {
28
+ const at = match.index;
29
+ const [, image, label, href, code, strong, em, del] = match;
30
+ if (at > cut) spans.push({ kind: "text", text: text.slice(cut, at) });
31
+ cut = at + match[0].length;
32
+ if (href !== undefined)
33
+ spans.push(
34
+ image || !MarkdownSpans.isSafeHref(href)
35
+ ? { kind: "plain", text: label ?? "" }
36
+ : { kind: "link", text: label ?? "", href },
37
+ );
38
+ else if (code !== undefined) spans.push({ kind: "code", text: code });
39
+ else if (strong !== undefined) spans.push({ kind: "strong", text: strong });
40
+ else if (em !== undefined) spans.push({ kind: "em", text: em });
41
+ else if (del !== undefined) spans.push({ kind: "del", text: del });
42
+ }
43
+ if (cut < text.length) spans.push({ kind: "text", text: text.slice(cut) });
44
+ return spans;
45
+ }
46
+
47
+ static isSafeHref(href: string) {
48
+ return !/^[a-z][a-z0-9+.-]*:/i.test(href) || /^(?:https?|mailto|tel):/i.test(href);
49
+ }
50
+
51
+ /** The text with every marker removed, for a host that has no styling to give — a width measurement, a log. */
52
+ static plain(text: string): string {
53
+ return MarkdownSpans.of(text)
54
+ .map((span) => (span.kind === "link" ? span.text : span.text))
55
+ .join("");
56
+ }
57
+ }
@@ -24,16 +24,9 @@ export const agentDictionary = serviceDictionary(["en", "ko"])
24
24
  "The agent is unavailable — this app has no language model configured",
25
25
  "에이전트를 사용할 수 없습니다. 이 앱에 언어 모델이 설정되어 있지 않습니다",
26
26
  ],
27
- deepseekRequestFailed: [
28
- "DeepSeek refused this turn with status {status}. Reason: {reason}",
29
- "DeepSeek가 이번 턴을 거절했습니다 (status {status}). 사유: {reason}",
30
- ],
31
- openaiRequestFailed: [
32
- "OpenAI refused this turn with status {status}. Reason: {reason}",
33
- "OpenAI가 이번 턴을 거절했습니다 (status {status}). 사유: {reason}",
34
- ],
35
- anthropicRequestFailed: [
36
- "Anthropic refused this turn with status {status}. Reason: {reason}",
37
- "Anthropic이 이번 턴을 거절했습니다 (status {status}). 사유: {reason}",
27
+
28
+ llmRequestFailed: [
29
+ "{provider} refused this turn with status {status}. Reason: {reason}",
30
+ "{provider}가 이번 턴을 거절했습니다 (status {status}). 사유: {reason}",
38
31
  ],
39
32
  });
@@ -75,6 +75,7 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
75
75
  agentPlaceholder: ["Message the agent...", "에이전트에게 메시지..."],
76
76
  agentClear: ["Clear conversation", "대화 비우기"],
77
77
  agentQuestion: ["The agent needs your decision", "에이전트가 결정을 요청합니다"],
78
+ agentToolCard: ["The agent needs you to fill this in", "에이전트가 입력을 요청합니다"],
78
79
  agentAnswer: ["Type your answer...", "답변을 입력하세요..."],
79
80
  agentListen: ["Speak to the agent", "에이전트에게 말하기"],
80
81
  agentVoiceFailed: ["The microphone could not be used.", "마이크를 사용할 수 없습니다."],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-beta.13",
3
+ "version": "3.0.0-beta.15",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -198,8 +198,10 @@
198
198
  }
199
199
  },
200
200
  "dependencies": {
201
+ "@lexical/plain-text": "^0.51.0",
201
202
  "dayjs": "^1.11.20",
202
203
  "immer": "^11.1.8",
204
+ "lexical": "^0.51.0",
203
205
  "tailwind-merge": "^3.3.1",
204
206
  "tailwind-variants": "^3.3.0"
205
207
  },
@@ -73,8 +73,15 @@ export class AkanOption<Env extends BackendEnv = BackendEnv> {
73
73
  this.#crossSite = crossSite;
74
74
  return this;
75
75
  }
76
- /** Settings for whichever adaptor fills `LlmAdaptorRole`, injected into it as the `llmOption` use. */
77
- setLlm(llmOrFn: LlmOption | ((env: Env) => LlmOption)) {
76
+ /**
77
+ * Settings for whichever adaptor fills `LlmAdaptorRole`, injected into it as the `llmOption` use.
78
+ *
79
+ * The argument is generic so that whatever an adaptor needs beyond `LlmOption` travels here too: an adaptor an
80
+ * app or a library wrote declares its own interface extending it, reads it with `use<MyLlmOption>()`, and its
81
+ * region or project id rides the same channel the shipped fields do. Entries merge in mount order with the
82
+ * app's last, so a library may name a host and the app the key.
83
+ */
84
+ setLlm<Option extends LlmOption>(llmOrFn: Option | ((env: Env) => Option)) {
78
85
  if (typeof llmOrFn === "function") this.#getLlms.push(llmOrFn);
79
86
  else this.#getLlms.push(() => llmOrFn);
80
87
  return this;
@@ -10,13 +10,13 @@ import {
10
10
  ConsoleLogger,
11
11
  type DatabaseAdaptor,
12
12
  DatabaseAdaptorRole,
13
- DeepseekLlm,
14
13
  JsonCompressor,
15
14
  LibsqlDatabase,
16
15
  type LlmAdaptor,
17
16
  LlmAdaptorRole,
18
17
  type LoggingAdaptor,
19
18
  LoggingAdaptorRole,
19
+ OpenaiLlm,
20
20
  PostgresDatabase,
21
21
  type QueueAdaptor,
22
22
  QueueAdaptorRole,
@@ -68,7 +68,7 @@ export const predefinedAdaptor = {
68
68
  logging: ConsoleLogger,
69
69
  websocket: SolidPubSub,
70
70
  compress: JsonCompressor,
71
- llm: DeepseekLlm,
71
+ llm: OpenaiLlm,
72
72
  };
73
73
 
74
74
  export const getPredefinedAdaptor = (mode: DatabaseMode = "single"): PredefinedAdaptor => {
@@ -9,6 +9,7 @@ import type {
9
9
  LlmTurnAnswer,
10
10
  LlmTurnRequest,
11
11
  } from "./llm.adaptor";
12
+ import { llmProviderOf } from "./llm.adaptor";
12
13
 
13
14
  type AnthropicSource = { type: "base64"; media_type: string; data: string } | { type: "url"; url: string };
14
15
  type AnthropicBlock =
@@ -136,7 +137,7 @@ export class AnthropicLlm
136
137
 
137
138
  signal: AbortSignal.timeout(120_000),
138
139
  });
139
- if (!response.ok) throw await AnthropicLlm.refusal(response);
140
+ if (!response.ok) throw await AnthropicLlm.refusal(this.#host, response);
140
141
  return (await response.json()) as T;
141
142
  }
142
143
 
@@ -147,12 +148,13 @@ export class AnthropicLlm
147
148
  body: JSON.stringify(body),
148
149
  signal: AbortSignal.timeout(120_000),
149
150
  });
150
- if (!response.ok || !response.body) throw await AnthropicLlm.refusal(response);
151
+ if (!response.ok || !response.body) throw await AnthropicLlm.refusal(this.#host, response);
151
152
  return response.body;
152
153
  }
153
154
 
154
- static async refusal(response: Response): Promise<Error> {
155
- return new Err("agent.error.anthropicRequestFailed", {
155
+ static async refusal(host: string, response: Response): Promise<Error> {
156
+ return new Err("agent.error.llmRequestFailed", {
157
+ provider: llmProviderOf(host),
156
158
  status: String(response.status),
157
159
  reason: await AnthropicLlm.reasonOf(response),
158
160
  });