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.
- package/common/CodeAgentClient.ts +102 -0
- package/common/CodeTranscript.ts +337 -0
- package/common/codeAgentProfile.ts +175 -0
- package/common/codeAgentWire.ts +409 -0
- package/common/index.ts +51 -0
- package/common/markdownSpans.ts +57 -0
- package/dictionary/agent.dictionary.ts +4 -11
- package/dictionary/base.dictionary.ts +1 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +3 -1
- package/server/akanOption.ts +9 -2
- package/server/di/predefinedAdaptor.ts +2 -2
- package/service/predefinedAdaptor/anthropicLlm.ts +6 -4
- package/service/predefinedAdaptor/index.ts +0 -1
- package/service/predefinedAdaptor/llm.adaptor.ts +21 -1
- package/service/predefinedAdaptor/openaiLlm.ts +37 -18
- package/store/agentic/StToolBuilder.ts +54 -0
- package/store/agentic/attachAgentic.ts +2 -1
- package/store/agentic/useFormTools.ts +1 -1
- package/types/common/CodeAgentClient.d.ts +26 -0
- package/types/common/CodeTranscript.d.ts +99 -0
- package/types/common/codeAgentProfile.d.ts +111 -0
- package/types/common/codeAgentWire.d.ts +388 -0
- package/types/common/index.d.ts +7 -0
- package/types/common/markdownSpans.d.ts +40 -0
- package/types/dictionary/agent.dictionary.d.ts +1 -1
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +9 -9
- package/types/server/akanOption.d.ts +9 -2
- package/types/server/di/predefinedAdaptor.d.ts +2 -2
- package/types/service/predefinedAdaptor/anthropicLlm.d.ts +1 -1
- package/types/service/predefinedAdaptor/index.d.ts +0 -1
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +14 -1
- package/types/service/predefinedAdaptor/openaiLlm.d.ts +20 -11
- package/types/store/agentic/StToolBuilder.d.ts +22 -0
- package/types/store/agentic/attachAgentic.d.ts +2 -1
- package/types/store/baseSt.d.ts +2 -2
- package/types/ui/Agent/Chat.d.ts +7 -1
- package/types/ui/Agent/Composer.d.ts +17 -1
- package/types/ui/Agent/MentionNode.d.ts +26 -0
- package/types/ui/Agent/RichInput.d.ts +22 -0
- package/types/ui/Agent/ToolCard.d.ts +16 -0
- package/types/ui/Agent/markdownSpans.d.ts +1 -0
- package/types/ui/Agent/mentionDraft.d.ts +19 -0
- package/types/ui/Agent/useChatReferences.d.ts +3 -2
- package/types/ui/UiOverride/context.d.ts +2 -0
- package/types/ui/index.d.ts +2 -1
- package/types/ui/recipe/badgeRecipe.d.ts +2 -2
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/types/vendor/use-agentic/AgentSession.d.ts +15 -1
- package/types/vendor/use-agentic/ToolRunner.d.ts +21 -1
- package/types/vendor/use-agentic/types.d.ts +31 -1
- package/ui/Agent/Chat.tsx +24 -7
- package/ui/Agent/Composer.tsx +73 -21
- package/ui/Agent/Markdown.tsx +2 -2
- package/ui/Agent/MentionNode.ts +62 -0
- package/ui/Agent/RichInput.tsx +154 -0
- package/ui/Agent/ToolCard.tsx +39 -0
- package/ui/Agent/markdownSpans.tsx +26 -36
- package/ui/Agent/mentionDraft.ts +101 -0
- package/ui/Agent/useChatReferences.ts +5 -8
- package/ui/UiOverride/context.ts +2 -0
- package/ui/index.ts +2 -1
- package/vendor/use-agentic/AgentSession.ts +45 -1
- package/vendor/use-agentic/AgenticSurface.ts +2 -0
- package/vendor/use-agentic/ToolRunner.ts +55 -1
- package/vendor/use-agentic/types.ts +36 -1
- package/service/predefinedAdaptor/deepseekLlm.ts +0 -82
- package/types/service/predefinedAdaptor/deepseekLlm.d.ts +0 -19
- /package/{ui/Agent → common}/markdownBlocks.ts +0 -0
- /package/{ui/Agent → common}/markdownTable.ts +0 -0
- /package/types/{ui/Agent → common}/markdownBlocks.d.ts +0 -0
- /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
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
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.", "마이크를 사용할 수 없습니다."],
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akanjs",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
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
|
},
|
package/server/akanOption.ts
CHANGED
|
@@ -73,8 +73,15 @@ export class AkanOption<Env extends BackendEnv = BackendEnv> {
|
|
|
73
73
|
this.#crossSite = crossSite;
|
|
74
74
|
return this;
|
|
75
75
|
}
|
|
76
|
-
/**
|
|
77
|
-
|
|
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:
|
|
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.
|
|
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
|
});
|