akanjs 3.0.0-alpha.41 → 3.0.0-alpha.42

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.
@@ -21,11 +21,11 @@ export const agentDictionary = serviceDictionary(["en", "ko"])
21
21
  }))
22
22
  .error({
23
23
  llmUnavailable: [
24
- "The agent model is unavailable — no LLM adaptor answered this turn",
25
- "에이전트 모델을 사용할 수 없습니다. 이번 턴에 응답한 LLM 어댑터가 없습니다",
24
+ "The agent is unavailable — this app has no language model configured",
25
+ "에이전트를 사용할 수 없습니다. 앱에 언어 모델이 설정되어 있지 않습니다",
26
26
  ],
27
27
  deepseekRequestFailed: [
28
- "DeepSeek request failed with status {status}",
29
- "DeepSeek 요청이 실패했습니다 (status {status})",
28
+ "DeepSeek refused this turn with status {status}. Reason: {reason}",
29
+ "DeepSeek 이번 턴을 거절했습니다 (status {status}). 사유: {reason}",
30
30
  ],
31
31
  });
@@ -64,6 +64,7 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
64
64
  agentKeepGoing: ["Keep going", "계속하기"],
65
65
  agentCmdNew: ["Start a new conversation", "새 대화 시작"],
66
66
  agentCmdRetry: ["Send the last message again", "마지막 메시지 다시 보내기"],
67
+ agentCmdCompact: ["Summarize the conversation so far", "지금까지의 대화 요약하기"],
67
68
  agentCmdCopy: ["Copy this conversation", "이 대화 복사"],
68
69
  agentCmdHelp: ["What you can do here", "여기서 할 수 있는 것"],
69
70
  agentCmdTools: ["List this screen's tools", "이 화면의 툴 목록"],
@@ -76,6 +77,9 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
76
77
  "에이전트는 혼자 결정하면 안 되는 일을 하기 전에 물어보며, 중지는 진행 중인 턴을 끝냅니다.",
77
78
  ],
78
79
  agentNothingToRetry: ["There is no message to send again.", "다시 보낼 메시지가 없습니다."],
80
+ agentNothingToCompact: ["There is nothing to summarize yet.", "아직 요약할 대화가 없습니다."],
81
+ agentCompacted: ["Summarized the conversation so far.", "지금까지의 대화를 요약했습니다."],
82
+ agentSummary: ["Summary of the earlier conversation", "이전 대화 요약"],
79
83
  agentBusy: ["A turn is still running. Stop it first.", "진행 중인 턴이 있습니다. 먼저 중지하세요."],
80
84
  agentCopied: ["Conversation copied to the clipboard.", "대화를 클립보드에 복사했습니다."],
81
85
  agentCopyFailed: ["Could not reach the clipboard.", "클립보드에 접근할 수 없습니다."],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.41",
3
+ "version": "3.0.0-alpha.42",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -54,8 +54,9 @@ export class DeepseekLlm
54
54
  const body = await this.#apiStream("/chat/completions", DeepseekLlm.requestBody(this.#model, request, true));
55
55
  return await DeepseekLlm.consumeStream(body, onDelta);
56
56
  } catch (error) {
57
+
57
58
  this.logger.error(`DeepSeek turn failed: ${error instanceof Error ? error.message : String(error)}`);
58
- return null;
59
+ throw error;
59
60
  }
60
61
  }
61
62
 
@@ -67,7 +68,7 @@ export class DeepseekLlm
67
68
 
68
69
  signal: AbortSignal.timeout(120_000),
69
70
  });
70
- if (!response.ok) throw new Err("agent.error.deepseekRequestFailed", { status: String(response.status) });
71
+ if (!response.ok) throw await DeepseekLlm.refusal(response);
71
72
  return (await response.json()) as T;
72
73
  }
73
74
 
@@ -78,11 +79,31 @@ export class DeepseekLlm
78
79
  body: JSON.stringify(body),
79
80
  signal: AbortSignal.timeout(120_000),
80
81
  });
81
- if (!response.ok || !response.body)
82
- throw new Err("agent.error.deepseekRequestFailed", { status: String(response.status) });
82
+ if (!response.ok || !response.body) throw await DeepseekLlm.refusal(response);
83
83
  return response.body;
84
84
  }
85
85
 
86
+ /**
87
+ * The dialect answers a refusal as `{ error: { message } }`, and that sentence is the useful half — a request
88
+ * past the context window says exactly which limit it passed. Carried on the `Err` so the chat can print it.
89
+ */
90
+ static async refusal(response: Response): Promise<Error> {
91
+ return new Err("agent.error.deepseekRequestFailed", {
92
+ status: String(response.status),
93
+ reason: await DeepseekLlm.reasonOf(response),
94
+ });
95
+ }
96
+
97
+ static async reasonOf(response: Response): Promise<string> {
98
+ try {
99
+ const body = (await response.json()) as { error?: { message?: unknown } | string };
100
+ const message = typeof body.error === "string" ? body.error : body.error?.message;
101
+ if (typeof message === "string" && message) return message;
102
+ } catch {
103
+ }
104
+ return response.statusText || "no reason given";
105
+ }
106
+
86
107
  /**
87
108
  * The dialect streams `data: {chunk}` SSE lines ending with `data: [DONE]`. Tool calls arrive fragmented — the
88
109
  * first fragment of an index carries id/name, later ones append to the arguments string — so they are assembled
@@ -175,6 +196,14 @@ export class DeepseekLlm
175
196
  }
176
197
 
177
198
  static providerMessages(message: AgentWireMessage): DeepseekMessage[] {
199
+
200
+ if (message.summary)
201
+ return [
202
+ {
203
+ role: "system" as const,
204
+ content: `Summary of the earlier conversation, standing in for the messages it replaced:\n\n${message.text ?? ""}`,
205
+ },
206
+ ];
178
207
  if (message.role === "tool")
179
208
  return (message.toolResults ?? []).map((result) => ({
180
209
  role: "tool" as const,
@@ -37,6 +37,11 @@ export interface AgentWireMessage {
37
37
  toolCalls?: AgentWireToolCall[];
38
38
  toolResults?: AgentWireToolResult[];
39
39
  error?: string;
40
+ /**
41
+ * Stands in for the messages the client's own compaction replaced. It arrives with the user's role because the
42
+ * wire has no other, but it is history rather than an ask, so a provider mapping frames it as one.
43
+ */
44
+ summary?: boolean;
40
45
  }
41
46
 
42
47
  export interface AgentWireTool {
@@ -68,8 +73,12 @@ export interface LlmTurnAnswer {
68
73
  /**
69
74
  * The provider seam for one stateless agent turn: the whole transcript in, one assistant answer out. The server
70
75
  * relays — it never executes a client tool — so this is the only surface a provider integration fills. An
71
- * implementation is an `adapt()` class in a lib's `srvkit/` and follows the adapter convention: failures are
72
- * logged and answered as `null`, and the calling service decides what that means.
76
+ * implementation is an `adapt()` class in a lib's `srvkit/`.
77
+ *
78
+ * `null` means this provider is not configured, and the caller turns it into the one sentence that says so. A
79
+ * failure the provider explained is logged and **thrown** instead, as an `Err` whose text the chat prints: a
80
+ * refused turn and an unconfigured app are different things to be told, and collapsing both into `null` left a
81
+ * user reading "no model is configured" about a conversation that had merely outgrown the context window.
73
82
  */
74
83
  export interface LlmAdaptor {
75
84
  /**
@@ -16,6 +16,18 @@ export class AgentTurnStream {
16
16
  return !!request.headers.get("accept")?.includes("text/event-stream");
17
17
  }
18
18
 
19
+ /**
20
+ * A domain `Err` carries its dictionary key as the message and the values its text interpolates as `data`, so
21
+ * both travel: the key alone would reach the chat as `agent.error.…` with its placeholders unfilled.
22
+ */
23
+ static failure(error: unknown): { message: string; data?: Record<string, string | number> } {
24
+ const message = error instanceof Error ? error.message : String(error);
25
+ const data = (error as { data?: unknown } | null)?.data;
26
+ return data && typeof data === "object" && !Array.isArray(data)
27
+ ? { message, data: data as Record<string, string | number> }
28
+ : { message };
29
+ }
30
+
19
31
  static response(run: (onDelta: (delta: string) => void) => Promise<StreamedTurn>): Response {
20
32
  const encoder = new TextEncoder();
21
33
  const stream = new ReadableStream<Uint8Array>({
@@ -35,7 +47,7 @@ export class AgentTurnStream {
35
47
  send({ type: "done", stop: turn.stop === "toolUse" || toolCalls.length ? "toolUse" : "end" });
36
48
  } catch (error) {
37
49
 
38
- send({ type: "error", message: error instanceof Error ? error.message : String(error) });
50
+ send({ type: "error", ...AgentTurnStream.failure(error) });
39
51
  } finally {
40
52
  controller.close();
41
53
  }
@@ -1 +1 @@
1
- export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing", never, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view">;
1
+ export declare const baseDictionary: import("./dictInfo.d.ts").ServiceDictInfo<[string, string], "ping" | "pingBody" | "pingParam" | "pingQuery" | "wsPing" | "pubsubPing", never, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view">;
@@ -1,13 +1,13 @@
1
1
  import type { AgentEndpoint, AgentTurn, BaseEndpoint } from "akanjs/signal";
2
2
  export declare const dictionary: {
3
- base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view">, never>;
3
+ base: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view">, never>;
4
4
  agentTurn: import("./locale.d.ts").DictModule<import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc`, never>;
5
5
  agent: import("./locale.d.ts").DictModule<import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">;
6
6
  };
7
- export declare const Err: import("./trans.d.ts").ErrConstructor<"agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, data?: import("./trans.d.ts").TranslationData) => string, msg: {
8
- info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
9
- success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
10
- error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
11
- warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
12
- loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
13
- }, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, __Error_Key__: "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed";
7
+ export declare const Err: import("./trans.d.ts").ErrConstructor<"agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed">, translate: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja", key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, data?: import("./trans.d.ts").TranslationData) => string, msg: {
8
+ info: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
9
+ success: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
10
+ error: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
11
+ warning: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
12
+ loading: (key: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, option?: import("./trans.d.ts").TransMessageOption) => void;
13
+ }, getDictionary: (lang: "en" | "ko" | "zhChs" | "zhCht" | "ja") => object, getAllDictionary: () => import("./trans.d.ts").RootDictionary, __Dict_Key__: import("./locale.d.ts").ServiceTranslatorKey<"base", BaseEndpoint, "error" | "remove" | "save" | "refresh" | "password" | "stop" | "send" | "skip" | "agent" | "ok" | "connecting" | "new" | "somethingWrong" | "connected" | "serverDisconnected" | "refreshing" | "tryReconnecting" | "serverHasProblem" | "checkServerStatus" | "success" | "failed" | "processing" | "processed" | "noData" | "invalidValueError" | "emailInvalidError" | "phoneInvalidError" | "cancel" | "unauthorized" | "agentIntro" | "agentPlaceholder" | "agentClear" | "agentQuestion" | "agentAnswer" | "agentListen" | "agentVoiceFailed" | "agentAttach" | "agentAttachRemove" | "agentAttachTooLarge" | "agentAttachUnsupported" | "agentContinue" | "agentKeepGoing" | "agentCmdNew" | "agentCmdRetry" | "agentCmdCompact" | "agentCmdCopy" | "agentCmdHelp" | "agentCmdTools" | "agentHelpIntro" | "agentHelpNote" | "agentNothingToRetry" | "agentNothingToCompact" | "agentCompacted" | "agentSummary" | "agentBusy" | "agentCopied" | "agentCopyFailed" | "agentToolsHead" | "agentToolsState" | "approve" | "decline" | "confirmClose" | "textTooShortError" | "textTooLongError" | "selectTooShortError" | "selectTooLongError" | "numberTooSmallError" | "numberTooBigError" | "passwordNotMatchError" | "selectDateError" | "priceUnit" | "passwordConfirm" | "noOptions" | "addModel" | "createModel" | "createSuccess" | "updateModel" | "removeModel" | "updateSuccess" | "removeSuccess" | "sureToRemove" | "irreversibleOps" | "typeNameToRemove" | "yesRemove" | "removeMsg" | "confirmMsg" | "perPage" | "cardView" | "tableView" | "exportCsv" | "exportJson" | "actions" | "edit" | "view"> | import("./locale.d.ts").ScalarTranslatorKey<"agentTurn", AgentTurn, never> | `agentStop.${string}` | `agentStop.${string}.desc` | import("./locale.d.ts").ServiceTranslatorKey<"agent", AgentEndpoint, never>, __Error_Key__: "agent.error.llmUnavailable" | "agent.error.deepseekRequestFailed";
@@ -34,6 +34,12 @@ declare const DeepseekLlm_base: import("..").AdaptorCls<{}, {
34
34
  export declare class DeepseekLlm extends DeepseekLlm_base implements LlmAdaptor {
35
35
  #private;
36
36
  chat(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<LlmTurnAnswer | null>;
37
+ /**
38
+ * The dialect answers a refusal as `{ error: { message } }`, and that sentence is the useful half — a request
39
+ * past the context window says exactly which limit it passed. Carried on the `Err` so the chat can print it.
40
+ */
41
+ static refusal(response: Response): Promise<Error>;
42
+ static reasonOf(response: Response): Promise<string>;
37
43
  /**
38
44
  * The dialect streams `data: {chunk}` SSE lines ending with `data: [DONE]`. Tool calls arrive fragmented — the
39
45
  * first fragment of an index carries id/name, later ones append to the arguments string — so they are assembled
@@ -34,6 +34,11 @@ export interface AgentWireMessage {
34
34
  toolCalls?: AgentWireToolCall[];
35
35
  toolResults?: AgentWireToolResult[];
36
36
  error?: string;
37
+ /**
38
+ * Stands in for the messages the client's own compaction replaced. It arrives with the user's role because the
39
+ * wire has no other, but it is history rather than an ask, so a provider mapping frames it as one.
40
+ */
41
+ summary?: boolean;
37
42
  }
38
43
  export interface AgentWireTool {
39
44
  name: string;
@@ -60,8 +65,12 @@ export interface LlmTurnAnswer {
60
65
  /**
61
66
  * The provider seam for one stateless agent turn: the whole transcript in, one assistant answer out. The server
62
67
  * relays — it never executes a client tool — so this is the only surface a provider integration fills. An
63
- * implementation is an `adapt()` class in a lib's `srvkit/` and follows the adapter convention: failures are
64
- * logged and answered as `null`, and the calling service decides what that means.
68
+ * implementation is an `adapt()` class in a lib's `srvkit/`.
69
+ *
70
+ * `null` means this provider is not configured, and the caller turns it into the one sentence that says so. A
71
+ * failure the provider explained is logged and **thrown** instead, as an `Err` whose text the chat prints: a
72
+ * refused turn and an unconfigured app are different things to be told, and collapsing both into `null` left a
73
+ * user reading "no model is configured" about a conversation that had merely outgrown the context window.
65
74
  */
66
75
  export interface LlmAdaptor {
67
76
  /**
@@ -11,6 +11,14 @@ interface StreamedTurn {
11
11
  */
12
12
  export declare class AgentTurnStream {
13
13
  static wants(request: Bun.BunRequest): boolean;
14
+ /**
15
+ * A domain `Err` carries its dictionary key as the message and the values its text interpolates as `data`, so
16
+ * both travel: the key alone would reach the chat as `agent.error.…` with its placeholders unfilled.
17
+ */
18
+ static failure(error: unknown): {
19
+ message: string;
20
+ data?: Record<string, string | number>;
21
+ };
14
22
  static response(run: (onDelta: (delta: string) => void) => Promise<StreamedTurn>): Response;
15
23
  }
16
24
  export {};
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from "react";
2
- import { type AgentRunner } from "../../vendor/use-agentic.d.ts";
2
+ import { type AgentRunner, type CompactOptions } from "../../vendor/use-agentic.d.ts";
3
3
  import { type AttachReader } from "./attachment.d.ts";
4
4
  import { type PersistOption } from "./sessionHistory.d.ts";
5
5
  import { type VoiceEngine } from "./voice.d.ts";
@@ -11,6 +11,11 @@ export interface ChatProps {
11
11
  /** Swap the transport; the default drives the app's `runAgentTurn` endpoint. */
12
12
  runner?: AgentRunner;
13
13
  maxTurns?: number;
14
+ /**
15
+ * When the conversation summarizes itself to stay inside the model's window — `at` estimated tokens, `keep`
16
+ * messages left verbatim below the summary. Tune it per provider; `{ at: 0 }` turns it off.
17
+ */
18
+ compact?: CompactOptions;
14
19
  defaultOpen?: boolean;
15
20
  /** Keeps the transcript across reloads — sessionStorage by default, `{ storage: "local" }` to outlive the tab. */
16
21
  persist?: PersistOption;
@@ -37,6 +42,6 @@ export interface ChatProps {
37
42
  * no history). An enclosing AgentProvider's session wins, which is how an app isolates a surface or swaps the
38
43
  * loop while keeping this UI.
39
44
  */
40
- export declare const DefaultChat: ({ className, title, instructions, runner, maxTurns, defaultOpen, persist, inline, attach, voice, }: ChatProps) => ReactNode;
45
+ export declare const DefaultChat: ({ className, title, instructions, runner, maxTurns, compact, defaultOpen, persist, inline, attach, voice, }: ChatProps) => ReactNode;
41
46
  declare const _default: import("react").ComponentType<ChatProps>;
42
47
  export default _default;
@@ -1,4 +1,5 @@
1
1
  import { type AgentProgressReport } from "./AgentProgress.d.ts";
2
+ import { type CompactOptions } from "./Compaction.d.ts";
2
3
  import type { AgentRunner, ChatMessage, ContextBlock, PublishedTool, SurfaceView } from "./types.d.ts";
3
4
  export interface PendingApproval {
4
5
  callId: string;
@@ -47,6 +48,11 @@ export interface AgentSessionOptions {
47
48
  question: string;
48
49
  keep: string;
49
50
  };
51
+ /**
52
+ * Keeps a long conversation inside the model's window: past `at` estimated tokens the history above the last
53
+ * few messages is replaced by one summary of itself, before the turn that would have overflowed is sent.
54
+ */
55
+ compact?: CompactOptions;
50
56
  }
51
57
  /**
52
58
  * The client-side conversation loop: send → model turn → tool calls → approval gate → execute → report diffs → next
@@ -93,6 +99,16 @@ export declare class AgentSession {
93
99
  * only way back. Only the trailing message is replayed, so a prompt's own preamble stays where it is.
94
100
  */
95
101
  retry: () => Promise<boolean>;
102
+ /**
103
+ * Replaces the history with one summary of itself and returns whether anything was replaced. `keep` leaves that
104
+ * many trailing messages verbatim; the command that a user types keeps none, because a summary of everything is
105
+ * what they asked for. A turn in flight refuses — the transcript it is appending to is not one to rewrite.
106
+ */
107
+ compact: ({ keep }?: {
108
+ keep?: number;
109
+ }) => Promise<boolean>;
110
+ /** What the transcript is estimated to cost the next turn, in tokens — the number auto-compaction watches. */
111
+ get tokens(): number;
96
112
  /** Records a host-side failure (a prompt fetch, an upload) in the transcript, where every other failure lands. */
97
113
  report: (error: string) => void;
98
114
  /** A line the host wrote — a command's own output. Rendered in the transcript, withheld from the model. */
@@ -0,0 +1,45 @@
1
+ import type { ChatMessage } from "./types.d.ts";
2
+ export interface CompactOptions {
3
+ /** Estimated transcript tokens above which a turn summarizes its own history first. `0` never compacts. */
4
+ at?: number;
5
+ /** Messages left verbatim below the summary. The cut rises to the nearest user message above them. */
6
+ keep?: number;
7
+ /** Produces the summary from the digest. Default: one tool-less turn through the session's own runner. */
8
+ summarize?: (digest: string, signal: AbortSignal) => Promise<string>;
9
+ }
10
+ /**
11
+ * Turns the part of a transcript that no longer fits into one message standing in for it. The loop runs in the
12
+ * browser and the relay is stateless, so nothing else is keeping the conversation inside the model's window: an
13
+ * uncompacted chat simply grows until the provider refuses the request.
14
+ */
15
+ export declare class Compaction {
16
+ #private;
17
+ /**
18
+ * Deliberately well under the smallest window a provider is likely to have: what a conversation loses to an
19
+ * early summary is detail, and what it loses to a late one is the conversation. The tools, the screen context
20
+ * and the system prompt ride on top of the transcript on every turn, and none of them is compactable.
21
+ */
22
+ static readonly defaults: {
23
+ at: number;
24
+ keep: number;
25
+ };
26
+ static readonly instruction: string;
27
+ /**
28
+ * Four characters per token, counted over the JSON the turn actually posts. A rough estimate on purpose: this
29
+ * places a threshold, and shipping a per-provider tokenizer to the browser to place it more exactly would cost
30
+ * more than the slack the estimate leaves.
31
+ */
32
+ static tokensOf(messages: readonly ChatMessage[]): number;
33
+ /**
34
+ * Where the kept half starts, or `-1` when nothing can be cut. Only a user message opens one: everything before
35
+ * it is settled, so the kept half never begins with a `tool` result whose call was summarized away — a shape
36
+ * every provider dialect rejects. `keep: 0` summarizes the whole transcript, which is what the command does.
37
+ */
38
+ static cutAt(messages: readonly ChatMessage[], keep: number): number;
39
+ /**
40
+ * The messages as one bounded block of text. Bounded is the point: the transcript being summarized is the one
41
+ * that no longer fits, so feeding it back verbatim would fail exactly where compaction is needed most.
42
+ */
43
+ static digest(messages: readonly ChatMessage[], budget?: number): string;
44
+ static message(summary: string): ChatMessage;
45
+ }
@@ -5,6 +5,7 @@ export * from "./AgentProgress.d.ts";
5
5
  export * from "./AgentProvider.d.ts";
6
6
  export * from "./AgentScope.d.ts";
7
7
  export * from "./AgentSession.d.ts";
8
+ export * from "./Compaction.d.ts";
8
9
  export * from "./httpRunner.d.ts";
9
10
  export * from "./surfaceContext.d.ts";
10
11
  export * from "./types.d.ts";
@@ -136,6 +136,12 @@ export interface ChatMessage {
136
136
  * history the model reads, which would take it for something it had said itself.
137
137
  */
138
138
  local?: boolean;
139
+ /**
140
+ * Stands in for the messages compaction replaced. It rides the wire like any other message — it is what the
141
+ * model now remembers of them — but it is not something the user said, so a backend frames it as a summary and
142
+ * a host renders it as one.
143
+ */
144
+ summary?: boolean;
139
145
  }
140
146
  /** One block of screen context the host assembles per turn. `kind` is the host's vocabulary; the wire forwards it verbatim. */
141
147
  export interface ContextBlock {
@@ -153,9 +159,15 @@ export type RunnerEvent = {
153
159
  } | {
154
160
  type: "done";
155
161
  stop: "end" | "toolUse";
156
- } | {
162
+ }
163
+ /**
164
+ * `data` accompanies a message that is a code rather than a sentence — the values whoever resolves the code
165
+ * interpolates into its text. A host that does not know the code shows the message as it stands.
166
+ */
167
+ | {
157
168
  type: "error";
158
169
  message: string;
170
+ data?: Record<string, string | number>;
159
171
  };
160
172
  export interface RunnerRequest {
161
173
  messages: ChatMessage[];
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { cn } from "akanjs/client";
2
+ import { cn, usePage } from "akanjs/client";
3
3
  import { type AgentProgressReport, AgentSession, type ChatMessage, type ToolCallResult } from "../../vendor/use-agentic";
4
4
  import { Chips } from "./Attach";
5
5
  import Markdown from "./Markdown";
@@ -81,6 +81,15 @@ const Ask = ({ args, result }: AskProps) => {
81
81
  };
82
82
 
83
83
  export default function Bubble({ className, message, progress, results }: BubbleProps) {
84
+ const { l } = usePage();
85
+
86
+ if (message.summary)
87
+ return (
88
+ <details className={cn("rounded-box border border-border bg-muted/60 px-3 py-2", className)}>
89
+ <summary className="cursor-pointer text-foreground/50 text-xs">{l("base.agentSummary")}</summary>
90
+ <p className="mt-2 whitespace-pre-wrap text-foreground/70 text-xs">{message.text}</p>
91
+ </details>
92
+ );
84
93
  if (message.role === "tool")
85
94
  return (
86
95
  <div className={cn("flex flex-col gap-1", className)}>
package/ui/Agent/Chat.tsx CHANGED
@@ -5,7 +5,13 @@ import { AgentContext, type AgentPrompt, AgentPrompts, ensureStoreSurface, Scree
5
5
  import { type ReactNode, useContext, useEffect, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { createPortal } from "react-dom";
7
7
  import { AiOutlineClear, AiOutlineClose } from "react-icons/ai";
8
- import { type AgentRunner, AgentSession, type MessageAttachment, SessionContext } from "../../vendor/use-agentic";
8
+ import {
9
+ type AgentRunner,
10
+ AgentSession,
11
+ type CompactOptions,
12
+ type MessageAttachment,
13
+ SessionContext,
14
+ } from "../../vendor/use-agentic";
9
15
  import { Button } from "../Button";
10
16
  import { inputRecipe } from "../recipe";
11
17
  import { createOverridable } from "../UiOverride";
@@ -29,6 +35,11 @@ export interface ChatProps {
29
35
  /** Swap the transport; the default drives the app's `runAgentTurn` endpoint. */
30
36
  runner?: AgentRunner;
31
37
  maxTurns?: number;
38
+ /**
39
+ * When the conversation summarizes itself to stay inside the model's window — `at` estimated tokens, `keep`
40
+ * messages left verbatim below the summary. Tune it per provider; `{ at: 0 }` turns it off.
41
+ */
42
+ compact?: CompactOptions;
32
43
  defaultOpen?: boolean;
33
44
  /** Keeps the transcript across reloads — sessionStorage by default, `{ storage: "local" }` to outlive the tab. */
34
45
  persist?: PersistOption;
@@ -66,6 +77,7 @@ export const DefaultChat = ({
66
77
  instructions,
67
78
  runner,
68
79
  maxTurns,
80
+ compact,
69
81
  defaultOpen = false,
70
82
  persist,
71
83
  inline = false,
@@ -83,6 +95,7 @@ export const DefaultChat = ({
83
95
  continueAsk: { question: l("base.agentContinue"), keep: l("base.agentKeepGoing") },
84
96
  ...(instructions ? { instructions } : {}),
85
97
  ...(maxTurns ? { maxTurns } : {}),
98
+ ...(compact ? { compact } : {}),
86
99
  ...(persist ? { history: sessionHistoryOf(persist) } : {}),
87
100
  });
88
101
  const session = held.current;
@@ -42,6 +42,15 @@ export class ChatCommands {
42
42
  else if (!(await session.retry())) session.note(t("base.agentNothingToRetry"));
43
43
  },
44
44
  },
45
+ {
46
+ name: "compact",
47
+ description: l("base.agentCmdCompact"),
48
+ run: async ({ session, l: t }) => {
49
+ if (session.isRunning) session.note(t("base.agentBusy"));
50
+ else if (await session.compact()) session.note(t("base.agentCompacted"));
51
+ else session.note(t("base.agentNothingToCompact"));
52
+ },
53
+ },
45
54
  { name: "copy", description: l("base.agentCmdCopy"), run: (context) => ChatCommands.#copy(context) },
46
55
  { name: "help", description: l("base.agentCmdHelp"), run: (context) => ChatCommands.#help(context) },
47
56
  { name: "tools", description: l("base.agentCmdTools"), run: (context) => ChatCommands.#tools(context) },
@@ -70,7 +79,8 @@ export class ChatCommands {
70
79
  const lines = [`# Agent conversation`, [where, new Date().toISOString()].filter(Boolean).join(" · "), ""];
71
80
  for (const message of messages) {
72
81
  if (message.local) continue;
73
- lines.push(`**${message.role}**`);
82
+
83
+ lines.push(`**${message.summary ? "summary" : message.role}**`);
74
84
  if (message.text) lines.push(message.text);
75
85
  for (const attachment of message.attachments ?? [])
76
86
  lines.push(`- attached \`${attachment.name}\` (${attachment.mimeType})`);
@@ -1,6 +1,20 @@
1
1
  import { getEnv } from "akanjs/base";
2
- import { fetch } from "akanjs/client";
3
- import { type AgentRunner, httpRunner } from "../../vendor/use-agentic";
2
+ import { fetch, Translator } from "akanjs/client";
3
+ import { type AgentRunner, httpRunner, type RunnerEvent } from "../../vendor/use-agentic";
4
+
5
+ /** `<refName>.error.<key>` — the shape a domain `Err` puts on the wire, its dictionary text being the message. */
6
+ const errorKey = /^[a-zA-Z][A-Za-z0-9]*\.error\.[A-Za-z0-9_]+$/;
7
+
8
+ /**
9
+ * A server `Err` travels as its key, because the endpoint has no language to resolve it in — the chat does, so
10
+ * the resolving happens here, one step before the transcript. Anything else is already a sentence somebody wrote,
11
+ * and a key with no entry stays as it is rather than becoming a worse sentence.
12
+ */
13
+ const readable = (event: RunnerEvent): RunnerEvent => {
14
+ if (event.type !== "error" || !errorKey.test(event.message)) return event;
15
+ const text = Translator.translateByLocale(Translator.getActiveLocale() ?? "en", event.message, event.data);
16
+ return text === event.message ? event : { type: "error", message: text };
17
+ };
4
18
 
5
19
  /**
6
20
  * Runs each assistant turn against the app's own `runAgentTurn` route — service signals mount unprefixed, so the
@@ -25,6 +39,6 @@ export const fetchRunner = (options: { fetcher?: typeof globalThis.fetch } = {})
25
39
  },
26
40
  ...(options.fetcher ? { fetcher: options.fetcher } : {}),
27
41
  });
28
- yield* runner.run(request);
42
+ for await (const event of runner.run(request)) yield readable(event);
29
43
  },
30
44
  });
package/ui/Load/Units.tsx CHANGED
@@ -113,7 +113,6 @@ function Render<RefName extends string, Light extends { id: string }>({
113
113
  const initModelObjInsight = (init as any)[names.modelObjInsight] as BaseInsight;
114
114
  const initLimitOfModel = (init as any)[names.limitOfModel] as number;
115
115
  const initPageOfModel = (init as any)[names.pageOfModel] as number;
116
- const modelStaleAt = storeUse[namesOfSlice.modelStaleAt]() as Date;
117
116
 
118
117
  const useCache =
119
118
  !modelListLoading &&
@@ -152,11 +151,12 @@ function Render<RefName extends string, Light extends { id: string }>({
152
151
  }, []);
153
152
 
154
153
  useEffect(() => {
154
+ const modelStaleAt = storeGet<Date>()[namesOfSlice.modelStaleAt];
155
155
  const staleThreshold = Math.max(modelStaleAt.getTime(), staleTime === undefined ? 0 : Date.now() - staleTime);
156
156
  if (storeGet<Date>()[namesOfSlice.modelInitAt].getTime() >= staleThreshold) return;
157
157
  if (storeGet<boolean>()[namesOfSlice.modelListLoading]) return;
158
158
  void storeDo[namesOfSlice.refreshModel]({ invalidate: true });
159
- }, [modelStaleAt]);
159
+ }, []);
160
160
 
161
161
  const modelInsight = storeUse[namesOfSlice.modelInsight]() as BaseInsight;
162
162
  const limitOfModel = storeUse[namesOfSlice.limitOfModel]() as number;
@@ -1,5 +1,6 @@
1
1
  import { AgentAbort } from "./AgentAbort";
2
2
  import { AgentProgress, type AgentProgressReport } from "./AgentProgress";
3
+ import { Compaction, type CompactOptions } from "./Compaction";
3
4
  import type {
4
5
  AgentRunner,
5
6
  ChatMessage,
@@ -59,6 +60,11 @@ export interface AgentSessionOptions {
59
60
  * renders no `pendingQuestion` would otherwise wait forever for an answer nobody can give.
60
61
  */
61
62
  continueAsk?: { question: string; keep: string };
63
+ /**
64
+ * Keeps a long conversation inside the model's window: past `at` estimated tokens the history above the last
65
+ * few messages is replaced by one summary of itself, before the turn that would have overflowed is sent.
66
+ */
67
+ compact?: CompactOptions;
62
68
  }
63
69
 
64
70
  /**
@@ -107,6 +113,8 @@ export class AgentSession {
107
113
  #version = 0;
108
114
  #listeners = new Set<() => void>();
109
115
  #saveTimer: ReturnType<typeof setTimeout> | null = null;
116
+ /** Size below which auto-compaction stays out of the way, raised when a summary failed to shrink anything. */
117
+ #compactFloor = 0;
110
118
 
111
119
  constructor(surface: SurfaceView, runner: AgentRunner, options: AgentSessionOptions = {}) {
112
120
  this.#surface = surface;
@@ -181,6 +189,7 @@ export class AgentSession {
181
189
  this.#append({ role: "user", text: answer });
182
190
  budget = turn + maxTurns;
183
191
  }
192
+ await this.#autoCompact(controller.signal);
184
193
  const { toolCalls, stop } = await this.#assistantTurn(controller.signal);
185
194
  if (controller.signal.aborted || stop !== "toolUse" || !toolCalls.length) return;
186
195
  const toolResults: ToolCallResult[] = [];
@@ -218,6 +227,7 @@ export class AgentSession {
218
227
  await this.#active;
219
228
  }
220
229
  this.#messages = [];
230
+ this.#compactFloor = 0;
221
231
 
222
232
  if (this.#saveTimer) {
223
233
  clearTimeout(this.#saveTimer);
@@ -239,7 +249,8 @@ export class AgentSession {
239
249
  retry = async (): Promise<boolean> => {
240
250
  if (this.#active) return false;
241
251
  const at = this.#messages.findLastIndex(
242
- (message) => message.role === "user" && (!!message.text || !!message.attachments?.length),
252
+
253
+ (message) => message.role === "user" && !message.summary && (!!message.text || !!message.attachments?.length),
243
254
  );
244
255
  if (at < 0) return false;
245
256
  const again = this.#messages[at];
@@ -248,6 +259,43 @@ export class AgentSession {
248
259
  return true;
249
260
  };
250
261
 
262
+ /**
263
+ * Replaces the history with one summary of itself and returns whether anything was replaced. `keep` leaves that
264
+ * many trailing messages verbatim; the command that a user types keeps none, because a summary of everything is
265
+ * what they asked for. A turn in flight refuses — the transcript it is appending to is not one to rewrite.
266
+ */
267
+ compact = async ({ keep = 0 }: { keep?: number } = {}): Promise<boolean> => {
268
+ if (this.#running) return false;
269
+ const controller = new AbortController();
270
+ this.#controller = controller;
271
+ this.#running = true;
272
+
273
+ this.#notify();
274
+ const run = this.#compact(keep, controller.signal);
275
+
276
+ this.#active = run.then(
277
+ () => undefined,
278
+ () => undefined,
279
+ );
280
+ try {
281
+ return await run;
282
+ } catch (error) {
283
+
284
+ if (controller.signal.aborted) return false;
285
+ throw error;
286
+ } finally {
287
+ this.#running = false;
288
+ this.#controller = null;
289
+ this.#active = null;
290
+ this.#notify();
291
+ }
292
+ };
293
+
294
+ /** What the transcript is estimated to cost the next turn, in tokens — the number auto-compaction watches. */
295
+ get tokens() {
296
+ return Compaction.tokensOf(this.#messages);
297
+ }
298
+
251
299
  /** Records a host-side failure (a prompt fetch, an upload) in the transcript, where every other failure lands. */
252
300
  report = (error: string) => {
253
301
  this.#append({ role: "assistant", error });
@@ -258,6 +306,53 @@ export class AgentSession {
258
306
  this.#append({ role: "assistant", text, local: true });
259
307
  };
260
308
 
309
+ async #compact(keep: number, signal: AbortSignal): Promise<boolean> {
310
+ const at = Compaction.cutAt(this.#messages, keep);
311
+ if (at <= 0) return false;
312
+ const summary = (await this.#summarize(Compaction.digest(this.#messages.slice(0, at)), signal)).trim();
313
+ if (!summary || signal.aborted) return false;
314
+ this.#messages = [Compaction.message(summary), ...this.#messages.slice(at)];
315
+ this.#notify();
316
+ return true;
317
+ }
318
+
319
+ /**
320
+ * Runs before the turn that would have overflowed rather than after it fails: the provider answers a request
321
+ * that is too long with a refusal, not with a shorter answer, so there is nothing to recover from afterwards.
322
+ * Best effort — a summary that cannot be produced leaves the transcript as it stands and the turn goes out as
323
+ * it would have, since it may well still fit.
324
+ */
325
+ async #autoCompact(signal: AbortSignal) {
326
+ const { at = Compaction.defaults.at, keep = Compaction.defaults.keep } = this.#options.compact ?? {};
327
+ if (!at || Compaction.tokensOf(this.#messages) < Math.max(at, this.#compactFloor)) return;
328
+ try {
329
+ await this.#compact(keep, signal);
330
+ } catch (error) {
331
+ console.warn(`[use-agentic] compaction failed: ${error instanceof Error ? error.message : String(error)}`);
332
+ }
333
+ const after = Compaction.tokensOf(this.#messages);
334
+
335
+ this.#compactFloor = after < at ? 0 : after + at;
336
+ }
337
+
338
+ /** No tools and no screen context: this turn summarizes the conversation, and must not act on it. */
339
+ async #summarize(digest: string, signal: AbortSignal): Promise<string> {
340
+ const custom = this.#options.compact?.summarize;
341
+ if (custom) return await custom(digest, signal);
342
+ let text = "";
343
+ for await (const event of this.#runner.run({
344
+ messages: [{ role: "user", text: digest }],
345
+ tools: [],
346
+ context: [],
347
+ instructions: Compaction.instruction,
348
+ signal,
349
+ })) {
350
+ if (event.type === "text") text += event.delta;
351
+ else if (event.type === "error") throw new Error(event.message);
352
+ }
353
+ return text;
354
+ }
355
+
261
356
  async #assistantTurn(signal: AbortSignal): Promise<{ toolCalls: ToolCallRequest[]; stop: "end" | "toolUse" }> {
262
357
  const { tools, guides } = this.#surface.snapshot();
263
358
  const instructions = [this.#options.instructions, ...guides].filter(Boolean).join("\n\n");
@@ -0,0 +1,110 @@
1
+ import type { ChatMessage } from "./types";
2
+
3
+ export interface CompactOptions {
4
+ /** Estimated transcript tokens above which a turn summarizes its own history first. `0` never compacts. */
5
+ at?: number;
6
+ /** Messages left verbatim below the summary. The cut rises to the nearest user message above them. */
7
+ keep?: number;
8
+ /** Produces the summary from the digest. Default: one tool-less turn through the session's own runner. */
9
+ summarize?: (digest: string, signal: AbortSignal) => Promise<string>;
10
+ }
11
+
12
+ /**
13
+ * Turns the part of a transcript that no longer fits into one message standing in for it. The loop runs in the
14
+ * browser and the relay is stateless, so nothing else is keeping the conversation inside the model's window: an
15
+ * uncompacted chat simply grows until the provider refuses the request.
16
+ */
17
+ export class Compaction {
18
+ /**
19
+ * Deliberately well under the smallest window a provider is likely to have: what a conversation loses to an
20
+ * early summary is detail, and what it loses to a late one is the conversation. The tools, the screen context
21
+ * and the system prompt ride on top of the transcript on every turn, and none of them is compactable.
22
+ */
23
+ static readonly defaults = { at: 24_000, keep: 6 };
24
+
25
+ static readonly instruction =
26
+ "Summarize the conversation below so you can carry it on with the summary in place of the messages themselves. " +
27
+ "Keep what the user is trying to do, the decisions taken, the facts and tool results that still matter, and " +
28
+ "anything left unfinished. Drop pleasantries and anything already superseded. Write compact notes, not prose, " +
29
+ "and write nothing but the summary itself.";
30
+
31
+ /**
32
+ * Four characters per token, counted over the JSON the turn actually posts. A rough estimate on purpose: this
33
+ * places a threshold, and shipping a per-provider tokenizer to the browser to place it more exactly would cost
34
+ * more than the slack the estimate leaves.
35
+ */
36
+ static tokensOf(messages: readonly ChatMessage[]): number {
37
+ let chars = 0;
38
+ for (const message of messages) if (!message.local) chars += JSON.stringify(message).length;
39
+ return Math.ceil(chars / 4);
40
+ }
41
+
42
+ /**
43
+ * Where the kept half starts, or `-1` when nothing can be cut. Only a user message opens one: everything before
44
+ * it is settled, so the kept half never begins with a `tool` result whose call was summarized away — a shape
45
+ * every provider dialect rejects. `keep: 0` summarizes the whole transcript, which is what the command does.
46
+ */
47
+ static cutAt(messages: readonly ChatMessage[], keep: number): number {
48
+ if (!messages.length) return -1;
49
+ if (keep <= 0) return messages.length;
50
+ const target = messages.length - keep;
51
+ if (target <= 0) return -1;
52
+ for (let at = target; at < messages.length; at += 1) if (messages[at].role === "user") return at;
53
+ return -1;
54
+ }
55
+
56
+ /**
57
+ * The messages as one bounded block of text. Bounded is the point: the transcript being summarized is the one
58
+ * that no longer fits, so feeding it back verbatim would fail exactly where compaction is needed most.
59
+ */
60
+ static digest(messages: readonly ChatMessage[], budget = 12_000): string {
61
+ const lines = messages.filter((message) => !message.local).map((message) => Compaction.#line(message));
62
+ if (lines.reduce((sum, line) => sum + line.length + 1, 0) <= budget) return lines.join("\n");
63
+
64
+ const head: string[] = [];
65
+ const tail: string[] = [];
66
+ let used = 0;
67
+ let low = 0;
68
+ let high = lines.length - 1;
69
+ let fromHead = true;
70
+ while (low <= high) {
71
+ const line = lines[fromHead ? low : high];
72
+ if (used + line.length + 1 > budget) break;
73
+ used += line.length + 1;
74
+ if (fromHead) {
75
+ head.push(line);
76
+ low += 1;
77
+ } else {
78
+ tail.unshift(line);
79
+ high -= 1;
80
+ }
81
+ fromHead = !fromHead;
82
+ }
83
+ return [...head, `[... ${high - low + 1} messages omitted ...]`, ...tail].join("\n");
84
+ }
85
+
86
+ static message(summary: string): ChatMessage {
87
+ return { role: "user", text: summary, summary: true };
88
+ }
89
+
90
+ static #line(message: ChatMessage): string {
91
+ const parts: string[] = [];
92
+ if (message.text) parts.push(Compaction.#clip(message.text, 1200));
93
+ for (const attachment of message.attachments ?? []) parts.push(`[attached ${attachment.name}]`);
94
+ for (const call of message.toolCalls ?? [])
95
+ parts.push(`[called ${call.name} ${Compaction.#clip(JSON.stringify(call.args), 200)}]`);
96
+ for (const result of message.toolResults ?? [])
97
+ parts.push(
98
+ `[${result.error ? "failed" : "result"} ${result.name}: ${Compaction.#clip(
99
+ result.error ?? JSON.stringify(result.result ?? null),
100
+ 400,
101
+ )}]`,
102
+ );
103
+ if (message.error) parts.push(`[turn failed: ${message.error}]`);
104
+ return `${message.role}: ${parts.join(" ")}`.trimEnd();
105
+ }
106
+
107
+ static #clip(text: string, max: number): string {
108
+ return text.length <= max ? text : `${text.slice(0, max)}...`;
109
+ }
110
+ }
@@ -30,6 +30,8 @@ tools, and the loop stay in the client; the server serves exactly one stateless
30
30
 
31
31
  - `messages` is the whole transcript in `ChatMessage` shape (`types.ts`). The server maps it to its provider's
32
32
  format and maps the answer back; it never executes a tool — `tools` is a schema catalogue, not an offer.
33
+ - A message flagged `"summary": true` stands in for the earlier messages a client-side compaction replaced. It is
34
+ history, not something the user said, so a backend that can frame it as one — a system message — should.
33
35
  - `context` blocks are host vocabulary. The server forwards them to the model as data, framed as data.
34
36
  - Everything is JSON-serializable by contract; a tool whose `result` is not is the tool's bug.
35
37
 
@@ -64,7 +66,10 @@ Nothing here is stored: the wire carries the bytes for exactly one turn's reques
64
66
  ```
65
67
 
66
68
  Any non-2xx status is surfaced to the session as one error event and ends the turn; a string `message` or
67
- `error` field in a JSON body is quoted in that event, and any other body is not interpreted.
69
+ `error` field in a JSON body becomes that event's message verbatim, and any other body is not interpreted. A
70
+ message that is a code rather than a sentence may be accompanied by a flat `data` object of strings and numbers —
71
+ the values whoever resolves the code interpolates into its text; a host that does not know the code shows the
72
+ message as it stands.
68
73
 
69
74
  ## Streaming
70
75
 
@@ -64,12 +64,28 @@ async function* streamedEvents(body: ReadableStream<Uint8Array>, signal: AbortSi
64
64
  if (!ended) yield { type: "error", message: "The turn stream ended without a done event." };
65
65
  }
66
66
 
67
- const turnErrorMessage = async (response: Response): Promise<string> => {
68
- const fallback = `Agent turn failed: ${response.status}`;
67
+ /** Only a flat record of scalars is forwarded: what a coded message interpolates, never a nested payload. */
68
+ const interpolations = (value: unknown): Record<string, string | number> | undefined => {
69
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
70
+ const entries = Object.entries(value).filter(
71
+ (entry): entry is [string, string | number] => typeof entry[1] === "string" || typeof entry[1] === "number",
72
+ );
73
+ return entries.length ? Object.fromEntries(entries) : undefined;
74
+ };
75
+
76
+ /**
77
+ * The server's own message travels verbatim — it was written for whoever is reading the chat, and a prefix in
78
+ * front of it both reads as two sentences and hides a coded message from the host that would have resolved it.
79
+ * The status stands in only when the body says nothing.
80
+ */
81
+ const turnError = async (response: Response): Promise<RunnerEvent> => {
82
+ const fallback = { type: "error", message: `Agent turn failed: ${response.status}` } as const;
69
83
  try {
70
- const body = (await response.json()) as { message?: unknown; error?: unknown };
84
+ const body = (await response.json()) as { message?: unknown; error?: unknown; data?: unknown };
71
85
  const message = typeof body.message === "string" ? body.message : typeof body.error === "string" ? body.error : "";
72
- return message ? `Agent turn failed: ${message}` : fallback;
86
+ if (!message) return fallback;
87
+ const data = interpolations(body.data);
88
+ return { type: "error", message, ...(data ? { data } : {}) };
73
89
  } catch {
74
90
  return fallback;
75
91
  }
@@ -100,7 +116,7 @@ export const httpRunner = ({ url, headers, fetcher }: HttpRunnerOptions): AgentR
100
116
  signal: request.signal,
101
117
  });
102
118
  if (!response.ok) {
103
- yield { type: "error", message: await turnErrorMessage(response) };
119
+ yield await turnError(response);
104
120
  return;
105
121
  }
106
122
  if (response.headers.get("content-type")?.includes("text/event-stream") && response.body) {
@@ -6,6 +6,7 @@ export * from "./AgentProgress";
6
6
  export * from "./AgentProvider";
7
7
  export * from "./AgentScope";
8
8
  export * from "./AgentSession";
9
+ export * from "./Compaction";
9
10
  export * from "./httpRunner";
10
11
  export * from "./surfaceContext";
11
12
  export * from "./types";
@@ -155,6 +155,12 @@ export interface ChatMessage {
155
155
  * history the model reads, which would take it for something it had said itself.
156
156
  */
157
157
  local?: boolean;
158
+ /**
159
+ * Stands in for the messages compaction replaced. It rides the wire like any other message — it is what the
160
+ * model now remembers of them — but it is not something the user said, so a backend frames it as a summary and
161
+ * a host renders it as one.
162
+ */
163
+ summary?: boolean;
158
164
  }
159
165
 
160
166
  /** One block of screen context the host assembles per turn. `kind` is the host's vocabulary; the wire forwards it verbatim. */
@@ -167,7 +173,11 @@ export type RunnerEvent =
167
173
  | { type: "text"; delta: string }
168
174
  | { type: "toolCall"; id: string; name: string; args: Record<string, unknown> }
169
175
  | { type: "done"; stop: "end" | "toolUse" }
170
- | { type: "error"; message: string };
176
+ /**
177
+ * `data` accompanies a message that is a code rather than a sentence — the values whoever resolves the code
178
+ * interpolates into its text. A host that does not know the code shows the message as it stands.
179
+ */
180
+ | { type: "error"; message: string; data?: Record<string, string | number> };
171
181
 
172
182
  export interface RunnerRequest {
173
183
  messages: ChatMessage[];