akanjs 3.0.0-alpha.38 → 3.0.0-alpha.39
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/dictionary/base.dictionary.ts +4 -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 +1 -1
- package/service/agent.service.ts +48 -2
- package/service/predefinedAdaptor/deepseekLlm.ts +13 -1
- package/service/predefinedAdaptor/llm.adaptor.ts +31 -0
- package/store/agent/AgentPrompts.ts +20 -2
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/service/agent.service.d.ts +13 -1
- package/types/service/predefinedAdaptor/deepseekLlm.d.ts +6 -0
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +29 -0
- package/types/ui/Agent/Attach.d.ts +16 -0
- package/types/ui/Agent/Chat.d.ts +9 -1
- package/types/ui/Agent/attachment.d.ts +23 -0
- package/types/ui/Dropdown.d.ts +3 -1
- package/types/ui/index.d.ts +1 -0
- package/types/vendor/use-agentic/types.d.ts +23 -0
- package/ui/Agent/Attach.tsx +77 -0
- package/ui/Agent/Bubble.tsx +6 -7
- package/ui/Agent/Chat.tsx +84 -32
- package/ui/Agent/ChatCommands.ts +2 -0
- package/ui/Agent/attachment.ts +53 -0
- package/ui/Agent/sessionHistory.ts +20 -1
- package/ui/Dropdown.tsx +37 -7
- package/ui/index.ts +1 -0
- package/vendor/use-agentic/AgentSession.ts +3 -1
- package/vendor/use-agentic/WIRE.md +18 -0
- package/vendor/use-agentic/types.ts +24 -0
|
@@ -54,6 +54,10 @@ export const baseDictionary = serviceDictionary(["en", "ko"])
|
|
|
54
54
|
agentClear: ["Clear conversation", "대화 비우기"],
|
|
55
55
|
agentQuestion: ["The agent needs your decision", "에이전트가 결정을 요청합니다"],
|
|
56
56
|
agentAnswer: ["Type your answer...", "답변을 입력하세요..."],
|
|
57
|
+
agentAttach: ["Attach a file", "파일 첨부"],
|
|
58
|
+
agentAttachRemove: ["Remove attachment", "첨부 제거"],
|
|
59
|
+
agentAttachTooLarge: ["{name} is too large to attach.", "{name}은(는) 용량이 너무 커서 첨부할 수 없습니다."],
|
|
60
|
+
agentAttachUnsupported: ["{name} cannot be attached here.", "{name}은(는) 여기에 첨부할 수 없습니다."],
|
|
57
61
|
agentContinue: ["This is taking a while. Keep going?", "시간이 걸리고 있습니다. 계속할까요?"],
|
|
58
62
|
agentKeepGoing: ["Keep going", "계속하기"],
|
|
59
63
|
agentCmdNew: ["Start a new conversation", "새 대화 시작"],
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/service/agent.service.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Err } from "akanjs/dictionary";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
AgentWireAttachment,
|
|
4
|
+
AgentWireMessage,
|
|
5
|
+
LlmAccepts,
|
|
6
|
+
LlmTurnRequest,
|
|
7
|
+
} from "./predefinedAdaptor/llm.adaptor";
|
|
3
8
|
import { LlmAdaptorRole } from "./predefinedAdaptor/role.adaptor";
|
|
4
9
|
import { serve } from "./serve";
|
|
5
10
|
|
|
@@ -7,8 +12,49 @@ export class AgentService extends serve("agent" as const, ({ plug }) => ({
|
|
|
7
12
|
llm: plug(LlmAdaptorRole),
|
|
8
13
|
})) {
|
|
9
14
|
async runTurn(request: LlmTurnRequest, onDelta?: (delta: string) => void) {
|
|
10
|
-
const answer = await this.llm.chat(request, onDelta);
|
|
15
|
+
const answer = await this.llm.chat(AgentService.readable(request, this.llm.accepts), onDelta);
|
|
11
16
|
if (!answer) throw new Err("agent.error.llmUnavailable");
|
|
12
17
|
return { text: answer.text ?? "", toolCalls: answer.toolCalls ?? [], stop: answer.stop };
|
|
13
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Replaces every attachment the provider cannot read with a note naming it, so no adaptor has to think about
|
|
22
|
+
* attachments it does not support and none can lose one quietly. The model has to be *told*, not merely spared:
|
|
23
|
+
* a file that vanishes on the way in is one it answers about from the filename, confidently and wrongly.
|
|
24
|
+
*
|
|
25
|
+
* The note rides in the message text because that is the one field every provider mapping already reads.
|
|
26
|
+
*/
|
|
27
|
+
static readable(request: LlmTurnRequest, accepts: LlmAccepts | undefined): LlmTurnRequest {
|
|
28
|
+
if (!request.messages.some((message) => message.attachments?.length)) return request;
|
|
29
|
+
const messages = request.messages.map((message) => AgentService.readableMessage(message, accepts ?? {}));
|
|
30
|
+
return { ...request, messages };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private static readableMessage(message: AgentWireMessage, accepts: LlmAccepts): AgentWireMessage {
|
|
34
|
+
const { attachments = [], ...rest } = message;
|
|
35
|
+
if (!attachments.length) return message;
|
|
36
|
+
const kept = attachments.filter((attachment) => AgentService.isReadable(attachment, accepts));
|
|
37
|
+
if (kept.length === attachments.length) return message;
|
|
38
|
+
const notes = attachments.filter((attachment) => !kept.includes(attachment)).map(AgentService.note);
|
|
39
|
+
return {
|
|
40
|
+
...rest,
|
|
41
|
+
...(kept.length ? { attachments: kept } : {}),
|
|
42
|
+
text: [message.text, ...notes].filter(Boolean).join("\n\n"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Extracted text is readable by every model there is; bytes and links need the provider to say so. */
|
|
47
|
+
private static isReadable(attachment: AgentWireAttachment, accepts: LlmAccepts): boolean {
|
|
48
|
+
if (attachment.text) return true;
|
|
49
|
+
if (!attachment.data && !attachment.url) return false;
|
|
50
|
+
return attachment.mimeType.startsWith("image/") ? !!accepts.image : !!accepts.document;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private static note(attachment: AgentWireAttachment): string {
|
|
54
|
+
const why =
|
|
55
|
+
attachment.data || attachment.url
|
|
56
|
+
? "this model cannot read that type"
|
|
57
|
+
: "its content is no longer available, as a reloaded conversation keeps the name and not the bytes";
|
|
58
|
+
return `[Attachment not read: ${attachment.name} (${attachment.mimeType}) — ${why}. Tell the user it was not read instead of guessing what it holds, and ask for the text if the answer needs it.]`;
|
|
59
|
+
}
|
|
14
60
|
}
|
|
@@ -201,7 +201,19 @@ export class DeepseekLlm
|
|
|
201
201
|
: {}),
|
|
202
202
|
},
|
|
203
203
|
];
|
|
204
|
-
return [{ role: "user" as const, content: message
|
|
204
|
+
return [{ role: "user" as const, content: DeepseekLlm.userContent(message) }];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `accepts` is left undeclared, so by the time an attachment reaches here `AgentService.readable` has reduced it
|
|
209
|
+
* to its text and turned everything else into a note. Each block is labelled because a model handed two
|
|
210
|
+
* unlabelled documents can no longer cite either one.
|
|
211
|
+
*/
|
|
212
|
+
static userContent(message: AgentWireMessage): string {
|
|
213
|
+
const blocks = (message.attachments ?? []).flatMap((attachment) =>
|
|
214
|
+
attachment.text ? [`--- attachment: ${attachment.name} (${attachment.mimeType}) ---\n${attachment.text}`] : [],
|
|
215
|
+
);
|
|
216
|
+
return [message.text, ...blocks].filter(Boolean).join("\n\n");
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
static turnAnswer(answer: DeepseekAnswer): LlmTurnAnswer {
|
|
@@ -12,6 +12,20 @@ export interface AgentWireToolResult {
|
|
|
12
12
|
error?: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A file the caller attached to one message. Exactly one carrier reaches the model — `data` as inlined bytes, `url`
|
|
17
|
+
* as something the provider fetches, `text` as content already extracted — and which of them a given provider can
|
|
18
|
+
* read is what `LlmAccepts` answers.
|
|
19
|
+
*/
|
|
20
|
+
export interface AgentWireAttachment {
|
|
21
|
+
name: string;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
/** Base64, with no `data:` prefix. */
|
|
24
|
+
data?: string;
|
|
25
|
+
url?: string;
|
|
26
|
+
text?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
/**
|
|
16
30
|
* One transcript message of the in-page agent wire (`use-agentic`'s WIRE.md), typed at both ends independently —
|
|
17
31
|
* the wire is the contract, so the server never imports the client package.
|
|
@@ -19,6 +33,7 @@ export interface AgentWireToolResult {
|
|
|
19
33
|
export interface AgentWireMessage {
|
|
20
34
|
role: "user" | "assistant" | "tool";
|
|
21
35
|
text?: string;
|
|
36
|
+
attachments?: AgentWireAttachment[];
|
|
22
37
|
toolCalls?: AgentWireToolCall[];
|
|
23
38
|
toolResults?: AgentWireToolResult[];
|
|
24
39
|
error?: string;
|
|
@@ -62,6 +77,22 @@ export interface LlmAdaptor {
|
|
|
62
77
|
* answer. An adapter may ignore it — the caller treats zero reported deltas as "answered whole".
|
|
63
78
|
*/
|
|
64
79
|
chat(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<LlmTurnAnswer | null>;
|
|
80
|
+
/** Which attachment carriers this provider's model can read. Omitted means text only. */
|
|
81
|
+
readonly accepts?: LlmAccepts;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* What an adaptor's model reads beyond text. Declared rather than defaulted to true, because the failure of
|
|
86
|
+
* guessing wrong is the worst one available: a provider handed bytes it cannot decode either rejects the whole
|
|
87
|
+
* turn or accepts it having seen nothing, and the model then answers confidently about a file it never read.
|
|
88
|
+
* `AgentService` degrades what is not accepted into a note the model can repeat back, so a text-only provider
|
|
89
|
+
* needs no attachment code at all — which is every provider until somebody swaps one in for vision.
|
|
90
|
+
*/
|
|
91
|
+
export interface LlmAccepts {
|
|
92
|
+
/** Inlined or linked image bytes. */
|
|
93
|
+
image?: boolean;
|
|
94
|
+
/** Non-image bytes handed over whole — a PDF the model parses itself. */
|
|
95
|
+
document?: boolean;
|
|
65
96
|
}
|
|
66
97
|
|
|
67
98
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Translator } from "akanjs/client";
|
|
2
2
|
import { parseAkanI18nEnv } from "akanjs/common";
|
|
3
3
|
import { FetchClient } from "akanjs/fetch";
|
|
4
|
-
import type { PromptContent, PromptResult, SerializedSignal } from "akanjs/signal";
|
|
4
|
+
import type { PromptContent, PromptMessage, PromptResult, SerializedSignal } from "akanjs/signal";
|
|
5
5
|
import type { ChatMessage } from "../../vendor/use-agentic";
|
|
6
6
|
|
|
7
7
|
export interface AgentPrompt {
|
|
@@ -59,7 +59,25 @@ export class AgentPrompts {
|
|
|
59
59
|
/** The messages a prompt returns become the user's turn, the way an MCP client sends a `prompts/get` result. */
|
|
60
60
|
static messagesOf(result: PromptResult): ChatMessage[] {
|
|
61
61
|
if (typeof result === "string") return [{ role: "user", text: result }];
|
|
62
|
-
return result.map((message) =>
|
|
62
|
+
return result.map((message) => AgentPrompts.#messageOf(message));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A binary block becomes an attachment. It used to become the string `[image]`, which a model reads as having
|
|
67
|
+
* been shown a picture — so a prompt built with `Msg.imageOf` produced confident answers about bytes that never
|
|
68
|
+
* left the server. The other block types are text already and stay text.
|
|
69
|
+
*/
|
|
70
|
+
static #messageOf(message: PromptMessage): ChatMessage {
|
|
71
|
+
const { role, content } = message;
|
|
72
|
+
if (content.type !== "image" && content.type !== "audio") return { role, text: AgentPrompts.textOf(content) };
|
|
73
|
+
const name = AgentPrompts.#binaryName(content.mimeType);
|
|
74
|
+
return { role, attachments: [{ name, mimeType: content.mimeType, data: content.data }] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `Msg.image` carries no filename — the protocol has nowhere to put one — so the type is the label. */
|
|
78
|
+
static #binaryName(mimeType: string) {
|
|
79
|
+
const [kind, subtype] = mimeType.split("/");
|
|
80
|
+
return subtype ? `${kind}.${subtype.split("+")[0]}` : mimeType;
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
static textOf(content: PromptContent): string {
|
|
@@ -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" | "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" | "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,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" | "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" | "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>;
|
|
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" | "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" | "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" | "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" | "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" | "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" | "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" | "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" | "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" | "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" | "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" | "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" | "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" | "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" | "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";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LlmTurnRequest } from "./predefinedAdaptor/llm.adaptor";
|
|
1
|
+
import type { LlmAccepts, LlmTurnRequest } from "./predefinedAdaptor/llm.adaptor";
|
|
2
2
|
declare const AgentService_base: import("./serve.d.ts").ServiceCls<"agent", {}, {
|
|
3
3
|
llm: import("./injectInfo.d.ts").InjectInfo<"plug", import("./predefinedAdaptor.d.ts").LlmAdaptor, never, never>;
|
|
4
4
|
}>;
|
|
@@ -8,5 +8,17 @@ export declare class AgentService extends AgentService_base {
|
|
|
8
8
|
toolCalls: import("./predefinedAdaptor.d.ts").AgentWireToolCall[];
|
|
9
9
|
stop: "end" | "toolUse";
|
|
10
10
|
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Replaces every attachment the provider cannot read with a note naming it, so no adaptor has to think about
|
|
13
|
+
* attachments it does not support and none can lose one quietly. The model has to be *told*, not merely spared:
|
|
14
|
+
* a file that vanishes on the way in is one it answers about from the filename, confidently and wrongly.
|
|
15
|
+
*
|
|
16
|
+
* The note rides in the message text because that is the one field every provider mapping already reads.
|
|
17
|
+
*/
|
|
18
|
+
static readable(request: LlmTurnRequest, accepts: LlmAccepts | undefined): LlmTurnRequest;
|
|
19
|
+
private static readableMessage;
|
|
20
|
+
/** Extracted text is readable by every model there is; bytes and links need the provider to say so. */
|
|
21
|
+
private static isReadable;
|
|
22
|
+
private static note;
|
|
11
23
|
}
|
|
12
24
|
export {};
|
|
@@ -57,6 +57,12 @@ export declare class DeepseekLlm extends DeepseekLlm_base implements LlmAdaptor
|
|
|
57
57
|
/** Context rides below the instructions framed as data — screen state must never read as directives. */
|
|
58
58
|
static systemPrompt({ instructions, context }: LlmTurnRequest): string;
|
|
59
59
|
static providerMessages(message: AgentWireMessage): DeepseekMessage[];
|
|
60
|
+
/**
|
|
61
|
+
* `accepts` is left undeclared, so by the time an attachment reaches here `AgentService.readable` has reduced it
|
|
62
|
+
* to its text and turned everything else into a note. Each block is labelled because a model handed two
|
|
63
|
+
* unlabelled documents can no longer cite either one.
|
|
64
|
+
*/
|
|
65
|
+
static userContent(message: AgentWireMessage): string;
|
|
60
66
|
static turnAnswer(answer: DeepseekAnswer): LlmTurnAnswer;
|
|
61
67
|
/** The provider sends arguments as a JSON string; an unparsable one becomes an empty call rather than a crash. */
|
|
62
68
|
static parsedArgs(raw: string | undefined): Record<string, unknown>;
|
|
@@ -10,6 +10,19 @@ export interface AgentWireToolResult {
|
|
|
10
10
|
changes?: unknown[];
|
|
11
11
|
error?: string;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* A file the caller attached to one message. Exactly one carrier reaches the model — `data` as inlined bytes, `url`
|
|
15
|
+
* as something the provider fetches, `text` as content already extracted — and which of them a given provider can
|
|
16
|
+
* read is what `LlmAccepts` answers.
|
|
17
|
+
*/
|
|
18
|
+
export interface AgentWireAttachment {
|
|
19
|
+
name: string;
|
|
20
|
+
mimeType: string;
|
|
21
|
+
/** Base64, with no `data:` prefix. */
|
|
22
|
+
data?: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
text?: string;
|
|
25
|
+
}
|
|
13
26
|
/**
|
|
14
27
|
* One transcript message of the in-page agent wire (`use-agentic`'s WIRE.md), typed at both ends independently —
|
|
15
28
|
* the wire is the contract, so the server never imports the client package.
|
|
@@ -17,6 +30,7 @@ export interface AgentWireToolResult {
|
|
|
17
30
|
export interface AgentWireMessage {
|
|
18
31
|
role: "user" | "assistant" | "tool";
|
|
19
32
|
text?: string;
|
|
33
|
+
attachments?: AgentWireAttachment[];
|
|
20
34
|
toolCalls?: AgentWireToolCall[];
|
|
21
35
|
toolResults?: AgentWireToolResult[];
|
|
22
36
|
error?: string;
|
|
@@ -55,6 +69,21 @@ export interface LlmAdaptor {
|
|
|
55
69
|
* answer. An adapter may ignore it — the caller treats zero reported deltas as "answered whole".
|
|
56
70
|
*/
|
|
57
71
|
chat(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<LlmTurnAnswer | null>;
|
|
72
|
+
/** Which attachment carriers this provider's model can read. Omitted means text only. */
|
|
73
|
+
readonly accepts?: LlmAccepts;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* What an adaptor's model reads beyond text. Declared rather than defaulted to true, because the failure of
|
|
77
|
+
* guessing wrong is the worst one available: a provider handed bytes it cannot decode either rejects the whole
|
|
78
|
+
* turn or accepts it having seen nothing, and the model then answers confidently about a file it never read.
|
|
79
|
+
* `AgentService` degrades what is not accepted into a note the model can repeat back, so a text-only provider
|
|
80
|
+
* needs no attachment code at all — which is every provider until somebody swaps one in for vision.
|
|
81
|
+
*/
|
|
82
|
+
export interface LlmAccepts {
|
|
83
|
+
/** Inlined or linked image bytes. */
|
|
84
|
+
image?: boolean;
|
|
85
|
+
/** Non-image bytes handed over whole — a PDF the model parses itself. */
|
|
86
|
+
document?: boolean;
|
|
58
87
|
}
|
|
59
88
|
/**
|
|
60
89
|
* Settings for whichever adaptor fills `LlmAdaptorRole`, registered with `option.setLlm(...)` and injected as the
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MessageAttachment } from "../../vendor/use-agentic.d.ts";
|
|
2
|
+
interface AttachProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
label: string;
|
|
5
|
+
onPick: (files: File[]) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare const Attach: ({ className, label, onPick }: AttachProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
interface ChipsProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
attachments: readonly MessageAttachment[];
|
|
11
|
+
/** Omitted for a sent message: what is already on the wire cannot be taken back. */
|
|
12
|
+
onRemove?: (index: number) => void;
|
|
13
|
+
removeLabel?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const Chips: ({ className, attachments, onRemove, removeLabel }: ChipsProps) => import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
package/types/ui/Agent/Chat.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type AgentRunner } from "../../vendor/use-agentic.d.ts";
|
|
3
|
+
import { type AttachReader } from "./attachment.d.ts";
|
|
3
4
|
import { type PersistOption } from "./sessionHistory.d.ts";
|
|
4
5
|
export interface ChatProps {
|
|
5
6
|
className?: string;
|
|
@@ -14,6 +15,13 @@ export interface ChatProps {
|
|
|
14
15
|
persist?: PersistOption;
|
|
15
16
|
/** Renders in the page flow instead of floating above it — a zone chat that lives inside its own section. */
|
|
16
17
|
inline?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Reads a file the user attached into an attachment, or answers `null` to leave it to the built-in reader
|
|
20
|
+
* (images as bytes, text as text). This is where an app puts what needs a parser — a PDF's text, a spreadsheet's
|
|
21
|
+
* cells — since the framework carries attachments but depends on nothing that can extract one. It runs before
|
|
22
|
+
* the built-in, so it can also replace how an image is prepared.
|
|
23
|
+
*/
|
|
24
|
+
attach?: AttachReader;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* The user-facing half of the in-page agent: one floating chat wired to the same surface the dock inspects.
|
|
@@ -22,6 +30,6 @@ export interface ChatProps {
|
|
|
22
30
|
* no history). An enclosing AgentProvider's session wins, which is how an app isolates a surface or swaps the
|
|
23
31
|
* loop while keeping this UI.
|
|
24
32
|
*/
|
|
25
|
-
export declare const DefaultChat: ({ className, title, instructions, runner, maxTurns, defaultOpen, persist, inline, }: ChatProps) => ReactNode;
|
|
33
|
+
export declare const DefaultChat: ({ className, title, instructions, runner, maxTurns, defaultOpen, persist, inline, attach, }: ChatProps) => ReactNode;
|
|
26
34
|
declare const _default: import("react").ComponentType<ChatProps>;
|
|
27
35
|
export default _default;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MessageAttachment } from "../../vendor/use-agentic.d.ts";
|
|
2
|
+
/** Turns one picked file into an attachment, or `null` to say this reader does not handle that file. */
|
|
3
|
+
export type AttachReader = (file: File) => Promise<MessageAttachment | null>;
|
|
4
|
+
/**
|
|
5
|
+
* Per-file ceiling for the composer. The bytes ride inside one turn's JSON request, so a file past this is not a
|
|
6
|
+
* slow attachment — it is a request the relay and the provider both refuse, and refusing it here is the only place
|
|
7
|
+
* the user learns which file it was.
|
|
8
|
+
*/
|
|
9
|
+
export declare const maxAttachmentBytes: number;
|
|
10
|
+
export type AttachFailure = "tooLarge" | "unsupported";
|
|
11
|
+
export declare class Attachment {
|
|
12
|
+
#private;
|
|
13
|
+
/**
|
|
14
|
+
* Reads what a browser can read on its own — an image as bytes, a text-ish file as text — and hands everything
|
|
15
|
+
* else to the app's own reader. That split is the layer boundary: a PDF needs a parser, so extracting one is the
|
|
16
|
+
* app's business (`attach`), while carrying the result is the framework's.
|
|
17
|
+
*
|
|
18
|
+
* The app's reader runs first so it can also replace the built-in handling, which is what downscaling an image
|
|
19
|
+
* before it costs a megabyte of prompt looks like.
|
|
20
|
+
*/
|
|
21
|
+
static read(file: File, attach?: AttachReader): Promise<MessageAttachment | AttachFailure>;
|
|
22
|
+
static failure(value: MessageAttachment | AttachFailure): value is AttachFailure;
|
|
23
|
+
}
|
package/types/ui/Dropdown.d.ts
CHANGED
|
@@ -14,8 +14,10 @@ export interface DropdownProps {
|
|
|
14
14
|
dropdownClassName?: string;
|
|
15
15
|
/** Trigger edge the menu lines up with. Position is computed, so a `left-0` class cannot do this. */
|
|
16
16
|
align?: "start" | "end";
|
|
17
|
+
/** Names this dropdown for the in-page agent. Without it the menu publishes nothing — two on one screen would share a name. */
|
|
18
|
+
namespace?: string;
|
|
17
19
|
}
|
|
18
|
-
export declare const DefaultDropdown: ({ value, content, className, buttonClassName, dropdownClassName, align, }: DropdownProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export declare const DefaultDropdown: ({ value, content, className, buttonClassName, dropdownClassName, align, namespace, }: DropdownProps) => import("react/jsx-runtime").JSX.Element;
|
|
19
21
|
/**
|
|
20
22
|
* Dropdown. Resolves to a route-scoped override when a `page/**\/_overrides.tsx`
|
|
21
23
|
* in the route's ancestry declares one, otherwise renders {@link DefaultDropdown}.
|
package/types/ui/index.d.ts
CHANGED
|
@@ -101,9 +101,32 @@ export interface ToolCallResult {
|
|
|
101
101
|
changes?: ResourceDiff[];
|
|
102
102
|
error?: string;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* A file the user handed the conversation rather than the screen — which is why it rides a message instead of a
|
|
106
|
+
* tool, the same reason `askUser` belongs to the session and not to the surface.
|
|
107
|
+
*
|
|
108
|
+
* Three carriers, one of which every attachment must have: `data` inlines the bytes, `url` points at something the
|
|
109
|
+
* provider can fetch, and `text` is content somebody already extracted — the only form a text-only model can read.
|
|
110
|
+
* They mirror what the server's own `Msg.image` / `Msg.link` / `Msg.resource` builders produce, so a prompt's
|
|
111
|
+
* attachment and a user's are the same thing on the wire.
|
|
112
|
+
*
|
|
113
|
+
* Whether a given carrier reaches the model is the provider's answer, not this type's: a backend drops what its
|
|
114
|
+
* model cannot read and says so in the transcript, because a silently dropped file is one the model then
|
|
115
|
+
* hallucinates about.
|
|
116
|
+
*/
|
|
117
|
+
export interface MessageAttachment {
|
|
118
|
+
name: string;
|
|
119
|
+
mimeType: string;
|
|
120
|
+
/** Base64, with no `data:` prefix. */
|
|
121
|
+
data?: string;
|
|
122
|
+
url?: string;
|
|
123
|
+
text?: string;
|
|
124
|
+
}
|
|
104
125
|
export interface ChatMessage {
|
|
105
126
|
role: ChatRole;
|
|
106
127
|
text?: string;
|
|
128
|
+
/** Files the message carries. Content, not instructions — a backend frames them the way it frames context. */
|
|
129
|
+
attachments?: MessageAttachment[];
|
|
107
130
|
toolCalls?: ToolCallRequest[];
|
|
108
131
|
toolResults?: ToolCallResult[];
|
|
109
132
|
/** A failed or capped turn, recorded in the transcript rather than thrown past it. */
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "akanjs/client";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import { AiOutlineClose, AiOutlinePaperClip } from "react-icons/ai";
|
|
5
|
+
import type { MessageAttachment } from "../../vendor/use-agentic";
|
|
6
|
+
|
|
7
|
+
interface AttachProps {
|
|
8
|
+
className?: string;
|
|
9
|
+
label: string;
|
|
10
|
+
onPick: (files: File[]) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Attach = ({ className, label, onPick }: AttachProps) => {
|
|
14
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<button
|
|
18
|
+
aria-label={label}
|
|
19
|
+
className={cn("shrink-0 text-foreground/50 hover:text-foreground", className)}
|
|
20
|
+
onClick={() => ref.current?.click()}
|
|
21
|
+
title={label}
|
|
22
|
+
type="button"
|
|
23
|
+
>
|
|
24
|
+
<AiOutlinePaperClip />
|
|
25
|
+
</button>
|
|
26
|
+
<input
|
|
27
|
+
className="hidden"
|
|
28
|
+
multiple
|
|
29
|
+
onChange={(event) => {
|
|
30
|
+
onPick([...(event.target.files ?? [])]);
|
|
31
|
+
|
|
32
|
+
event.target.value = "";
|
|
33
|
+
}}
|
|
34
|
+
ref={ref}
|
|
35
|
+
type="file"
|
|
36
|
+
/>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface ChipsProps {
|
|
42
|
+
className?: string;
|
|
43
|
+
attachments: readonly MessageAttachment[];
|
|
44
|
+
/** Omitted for a sent message: what is already on the wire cannot be taken back. */
|
|
45
|
+
onRemove?: (index: number) => void;
|
|
46
|
+
removeLabel?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const Chips = ({ className, attachments, onRemove, removeLabel }: ChipsProps) => (
|
|
50
|
+
<div className={cn("flex flex-wrap gap-1", className)}>
|
|
51
|
+
{attachments.map((attachment, idx) => (
|
|
52
|
+
<span
|
|
53
|
+
className="flex items-center gap-1 rounded-field bg-muted px-2 py-0.5 text-xs"
|
|
54
|
+
key={`${attachment.name}-${idx}`}
|
|
55
|
+
>
|
|
56
|
+
{attachment.data && attachment.mimeType.startsWith("image/") ? (
|
|
57
|
+
<img
|
|
58
|
+
alt={attachment.name}
|
|
59
|
+
className="size-6 rounded-field object-cover"
|
|
60
|
+
src={`data:${attachment.mimeType};base64,${attachment.data}`}
|
|
61
|
+
/>
|
|
62
|
+
) : null}
|
|
63
|
+
<span className="max-w-32 truncate">{attachment.name}</span>
|
|
64
|
+
{onRemove ? (
|
|
65
|
+
<button
|
|
66
|
+
aria-label={removeLabel}
|
|
67
|
+
className="text-foreground/40 hover:text-foreground"
|
|
68
|
+
onClick={() => onRemove(idx)}
|
|
69
|
+
type="button"
|
|
70
|
+
>
|
|
71
|
+
<AiOutlineClose />
|
|
72
|
+
</button>
|
|
73
|
+
) : null}
|
|
74
|
+
</span>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
package/ui/Agent/Bubble.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "akanjs/client";
|
|
3
3
|
import { type AgentProgressReport, AgentSession, type ChatMessage, type ToolCallResult } from "../../vendor/use-agentic";
|
|
4
|
+
import { Chips } from "./Attach";
|
|
4
5
|
import Markdown from "./Markdown";
|
|
5
6
|
|
|
6
7
|
interface BubbleProps {
|
|
@@ -90,13 +91,11 @@ export default function Bubble({ className, message, progress, results }: Bubble
|
|
|
90
91
|
);
|
|
91
92
|
if (message.role === "user")
|
|
92
93
|
return (
|
|
93
|
-
<div
|
|
94
|
-
className=
|
|
95
|
-
|
|
96
|
-
className
|
|
97
|
-
)}
|
|
98
|
-
>
|
|
99
|
-
{message.text}
|
|
94
|
+
<div className={cn("flex max-w-[85%] flex-col items-end gap-1 self-end", className)}>
|
|
95
|
+
{message.attachments?.length ? <Chips attachments={message.attachments} className="justify-end" /> : null}
|
|
96
|
+
{message.text ? (
|
|
97
|
+
<p className="whitespace-pre-wrap rounded-box bg-primary/10 px-3 py-2 text-sm">{message.text}</p>
|
|
98
|
+
) : null}
|
|
100
99
|
</div>
|
|
101
100
|
);
|
|
102
101
|
const isDrafting = !message.text && !message.toolCalls?.length && !message.error;
|
package/ui/Agent/Chat.tsx
CHANGED
|
@@ -5,11 +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, SessionContext } from "../../vendor/use-agentic";
|
|
8
|
+
import { type AgentRunner, AgentSession, type MessageAttachment, SessionContext } from "../../vendor/use-agentic";
|
|
9
9
|
import { Button } from "../Button";
|
|
10
10
|
import { inputRecipe } from "../recipe";
|
|
11
11
|
import { createOverridable } from "../UiOverride";
|
|
12
12
|
import Approval from "./Approval";
|
|
13
|
+
import { Attach, Chips } from "./Attach";
|
|
14
|
+
import { Attachment, type AttachReader } from "./attachment";
|
|
13
15
|
import Bubble from "./Bubble";
|
|
14
16
|
import { type ChatCommand, ChatCommands } from "./ChatCommands";
|
|
15
17
|
import { fetchRunner } from "./fetchRunner";
|
|
@@ -30,6 +32,13 @@ export interface ChatProps {
|
|
|
30
32
|
persist?: PersistOption;
|
|
31
33
|
/** Renders in the page flow instead of floating above it — a zone chat that lives inside its own section. */
|
|
32
34
|
inline?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Reads a file the user attached into an attachment, or answers `null` to leave it to the built-in reader
|
|
37
|
+
* (images as bytes, text as text). This is where an app puts what needs a parser — a PDF's text, a spreadsheet's
|
|
38
|
+
* cells — since the framework carries attachments but depends on nothing that can extract one. It runs before
|
|
39
|
+
* the built-in, so it can also replace how an image is prepared.
|
|
40
|
+
*/
|
|
41
|
+
attach?: AttachReader;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
const isApplePlatform = () => /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
|
@@ -52,6 +61,7 @@ export const DefaultChat = ({
|
|
|
52
61
|
defaultOpen = false,
|
|
53
62
|
persist,
|
|
54
63
|
inline = false,
|
|
64
|
+
attach,
|
|
55
65
|
}: ChatProps) => {
|
|
56
66
|
const { l } = usePage();
|
|
57
67
|
const provided = useContext(SessionContext);
|
|
@@ -76,6 +86,7 @@ export const DefaultChat = ({
|
|
|
76
86
|
);
|
|
77
87
|
const [open, setOpen] = useState(defaultOpen);
|
|
78
88
|
const [draft, setDraft] = useState("");
|
|
89
|
+
const [attached, setAttached] = useState<MessageAttachment[]>([]);
|
|
79
90
|
const sent = useRef<string[]>([]);
|
|
80
91
|
const stashed = useRef("");
|
|
81
92
|
const [recall, setRecall] = useState(0);
|
|
@@ -128,6 +139,21 @@ export const DefaultChat = ({
|
|
|
128
139
|
session.report(`/${prompt.name} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
140
|
}
|
|
130
141
|
};
|
|
142
|
+
/** Staged one at a time so one unreadable file names itself instead of failing the whole drop silently. */
|
|
143
|
+
const attachFiles = async (files: File[]) => {
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
try {
|
|
146
|
+
const read = await Attachment.read(file, attach);
|
|
147
|
+
if (!Attachment.failure(read)) setAttached((current) => [...current, read]);
|
|
148
|
+
else
|
|
149
|
+
session.note(
|
|
150
|
+
l(read === "tooLarge" ? "base.agentAttachTooLarge" : "base.agentAttachUnsupported", { name: file.name }),
|
|
151
|
+
);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
session.report(`${file.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
131
157
|
const remember = (text: string) => {
|
|
132
158
|
if (sent.current[sent.current.length - 1] !== text) sent.current = [...sent.current, text].slice(-30);
|
|
133
159
|
setRecall(0);
|
|
@@ -157,10 +183,10 @@ export const DefaultChat = ({
|
|
|
157
183
|
};
|
|
158
184
|
const send = () => {
|
|
159
185
|
const text = draft.trim();
|
|
160
|
-
if (!text) return;
|
|
186
|
+
if (!text && !attached.length) return;
|
|
161
187
|
|
|
162
188
|
const question = session.pendingQuestion;
|
|
163
|
-
if (question) {
|
|
189
|
+
if (question && text) {
|
|
164
190
|
setDraft("");
|
|
165
191
|
question.answer(question.multiple ? [text] : text);
|
|
166
192
|
return;
|
|
@@ -175,12 +201,17 @@ export const DefaultChat = ({
|
|
|
175
201
|
if (session.isRunning) return;
|
|
176
202
|
const prompt = command ? prompts.current?.find(command.name) : null;
|
|
177
203
|
setDraft("");
|
|
178
|
-
remember(text);
|
|
204
|
+
if (text) remember(text);
|
|
179
205
|
if (command && prompt) {
|
|
180
206
|
void runPrompt(prompt, command.args);
|
|
181
207
|
return;
|
|
182
208
|
}
|
|
183
|
-
|
|
209
|
+
if (!attached.length) {
|
|
210
|
+
void session.send(text);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
setAttached([]);
|
|
214
|
+
void session.send([{ role: "user", ...(text ? { text } : {}), attachments: attached }]);
|
|
184
215
|
};
|
|
185
216
|
const query = /^\/[A-Za-z0-9_-]*$/.test(draft) ? draft : "";
|
|
186
217
|
const commandMenu = query ? ChatCommands.list(l).filter((command) => `/${command.name}`.startsWith(query)) : [];
|
|
@@ -241,6 +272,11 @@ export const DefaultChat = ({
|
|
|
241
272
|
!inline && floatingLayer,
|
|
242
273
|
className,
|
|
243
274
|
)}
|
|
275
|
+
onDragOver={(event) => event.preventDefault()}
|
|
276
|
+
onDrop={(event) => {
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
void attachFiles([...event.dataTransfer.files]);
|
|
279
|
+
}}
|
|
244
280
|
>
|
|
245
281
|
<header className="flex items-center gap-2 border-foreground/5 border-b px-4 py-3">
|
|
246
282
|
<span className="font-semibold text-sm">{title ?? l("base.agent")}</span>
|
|
@@ -278,34 +314,50 @@ export const DefaultChat = ({
|
|
|
278
314
|
<Question key={session.pendingQuestion.callId} question={session.pendingQuestion} />
|
|
279
315
|
) : null}
|
|
280
316
|
<Menu commands={commandMenu} onCommand={runCommand} onPrompt={pick} prompts={promptMenu} />
|
|
281
|
-
<div className="flex
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
<div className="flex flex-col gap-2 border-foreground/5 border-t p-3">
|
|
318
|
+
{attached.length ? (
|
|
319
|
+
<Chips
|
|
320
|
+
attachments={attached}
|
|
321
|
+
onRemove={(idx) => setAttached((current) => current.filter((_, at) => at !== idx))}
|
|
322
|
+
removeLabel={l("base.agentAttachRemove")}
|
|
323
|
+
/>
|
|
324
|
+
) : null}
|
|
325
|
+
<div className="flex items-center gap-2">
|
|
326
|
+
<Attach label={l("base.agentAttach")} onPick={(files) => void attachFiles(files)} />
|
|
327
|
+
<input
|
|
328
|
+
className={inputRecipe({ size: "sm" }, "flex-1")}
|
|
329
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
330
|
+
ref={inputRef}
|
|
331
|
+
onKeyDown={(event) => {
|
|
332
|
+
|
|
333
|
+
if ((event.key === "ArrowUp" || event.key === "ArrowDown") && sent.current.length) {
|
|
334
|
+
event.preventDefault();
|
|
335
|
+
step(event.key === "ArrowUp" ? 1 : -1);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
|
|
289
339
|
event.preventDefault();
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
{
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
{
|
|
307
|
-
|
|
308
|
-
|
|
340
|
+
send();
|
|
341
|
+
}}
|
|
342
|
+
onPaste={(event) => {
|
|
343
|
+
const files = [...event.clipboardData.files];
|
|
344
|
+
if (!files.length) return;
|
|
345
|
+
event.preventDefault();
|
|
346
|
+
void attachFiles(files);
|
|
347
|
+
}}
|
|
348
|
+
placeholder={session.pendingQuestion ? l("base.agentAnswer") : l("base.agentPlaceholder")}
|
|
349
|
+
value={draft}
|
|
350
|
+
/>
|
|
351
|
+
{session.isRunning && !session.pendingQuestion ? (
|
|
352
|
+
<Button onClick={session.abort} size="sm" variant="outline">
|
|
353
|
+
{l("base.stop")}
|
|
354
|
+
</Button>
|
|
355
|
+
) : (
|
|
356
|
+
<Button disabled={!draft.trim() && !attached.length} onClick={send} size="sm">
|
|
357
|
+
{l("base.send")}
|
|
358
|
+
</Button>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
309
361
|
</div>
|
|
310
362
|
</aside>,
|
|
311
363
|
);
|
package/ui/Agent/ChatCommands.ts
CHANGED
|
@@ -72,6 +72,8 @@ export class ChatCommands {
|
|
|
72
72
|
if (message.local) continue;
|
|
73
73
|
lines.push(`**${message.role}**`);
|
|
74
74
|
if (message.text) lines.push(message.text);
|
|
75
|
+
for (const attachment of message.attachments ?? [])
|
|
76
|
+
lines.push(`- attached \`${attachment.name}\` (${attachment.mimeType})`);
|
|
75
77
|
for (const call of message.toolCalls ?? []) lines.push(`- call \`${call.name}\` ${JSON.stringify(call.args)}`);
|
|
76
78
|
for (const result of message.toolResults ?? [])
|
|
77
79
|
lines.push(
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MessageAttachment } from "../../vendor/use-agentic";
|
|
2
|
+
|
|
3
|
+
/** Turns one picked file into an attachment, or `null` to say this reader does not handle that file. */
|
|
4
|
+
export type AttachReader = (file: File) => Promise<MessageAttachment | null>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-file ceiling for the composer. The bytes ride inside one turn's JSON request, so a file past this is not a
|
|
8
|
+
* slow attachment — it is a request the relay and the provider both refuse, and refusing it here is the only place
|
|
9
|
+
* the user learns which file it was.
|
|
10
|
+
*/
|
|
11
|
+
export const maxAttachmentBytes = 4 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
export type AttachFailure = "tooLarge" | "unsupported";
|
|
14
|
+
|
|
15
|
+
const textMimes = new Set(["application/json", "application/xml", "application/x-yaml", "application/yaml"]);
|
|
16
|
+
|
|
17
|
+
export class Attachment {
|
|
18
|
+
/**
|
|
19
|
+
* Reads what a browser can read on its own — an image as bytes, a text-ish file as text — and hands everything
|
|
20
|
+
* else to the app's own reader. That split is the layer boundary: a PDF needs a parser, so extracting one is the
|
|
21
|
+
* app's business (`attach`), while carrying the result is the framework's.
|
|
22
|
+
*
|
|
23
|
+
* The app's reader runs first so it can also replace the built-in handling, which is what downscaling an image
|
|
24
|
+
* before it costs a megabyte of prompt looks like.
|
|
25
|
+
*/
|
|
26
|
+
static async read(file: File, attach?: AttachReader): Promise<MessageAttachment | AttachFailure> {
|
|
27
|
+
if (file.size > maxAttachmentBytes) return "tooLarge";
|
|
28
|
+
const injected = await attach?.(file);
|
|
29
|
+
if (injected) return injected;
|
|
30
|
+
|
|
31
|
+
const mimeType = file.type.split(";")[0].trim().toLowerCase();
|
|
32
|
+
if (mimeType.startsWith("image/")) return { name: file.name, mimeType, data: await Attachment.#base64(file) };
|
|
33
|
+
if (mimeType.startsWith("text/") || textMimes.has(mimeType))
|
|
34
|
+
return { name: file.name, mimeType: mimeType || "text/plain", text: await file.text() };
|
|
35
|
+
return "unsupported";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static failure(value: MessageAttachment | AttachFailure): value is AttachFailure {
|
|
39
|
+
return typeof value === "string";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Chunked because `String.fromCharCode(...bytes)` spreads one argument per byte, and a megabyte of them is a
|
|
44
|
+
* RangeError rather than a slow call.
|
|
45
|
+
*/
|
|
46
|
+
static async #base64(file: File): Promise<string> {
|
|
47
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
48
|
+
const chunk = 0x8000;
|
|
49
|
+
let binary = "";
|
|
50
|
+
for (let at = 0; at < bytes.length; at += chunk) binary += String.fromCharCode(...bytes.subarray(at, at + chunk));
|
|
51
|
+
return btoa(binary);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -3,6 +3,25 @@ import type { ChatMessage, SessionHistory } from "../../vendor/use-agentic";
|
|
|
3
3
|
|
|
4
4
|
export type PersistOption = boolean | { storage?: "session" | "local"; key?: string };
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Attachment content never reaches storage. Web storage is a few megabytes per origin, one screenshot fills a
|
|
8
|
+
* chunk of it, and `AgentSession` swallows a failed save — so persisting the bytes would quietly stop persisting
|
|
9
|
+
* the transcript itself. The name and type stay so a restored conversation still reads as what happened, and a
|
|
10
|
+
* `url` stays because a pointer is not content; the server then tells the model the content is gone rather than
|
|
11
|
+
* letting it answer from the filename.
|
|
12
|
+
*/
|
|
13
|
+
const withoutContent = (message: ChatMessage): ChatMessage =>
|
|
14
|
+
message.attachments?.length
|
|
15
|
+
? {
|
|
16
|
+
...message,
|
|
17
|
+
attachments: message.attachments.map(({ name, mimeType, url }) => ({
|
|
18
|
+
name,
|
|
19
|
+
mimeType,
|
|
20
|
+
...(url ? { url } : {}),
|
|
21
|
+
})),
|
|
22
|
+
}
|
|
23
|
+
: message;
|
|
24
|
+
|
|
6
25
|
/**
|
|
7
26
|
* Maps the `persist` prop onto a `SessionHistory` over web storage. Session storage is the default on purpose:
|
|
8
27
|
* surviving a refresh is the whole ask, and a transcript that dies with the tab never lingers on a shared machine
|
|
@@ -24,7 +43,7 @@ export const sessionHistoryOf = (persist: PersistOption | undefined, pathKey = "
|
|
|
24
43
|
return parsed.v === version && Array.isArray(parsed.messages) ? parsed.messages : null;
|
|
25
44
|
},
|
|
26
45
|
save: (messages) => {
|
|
27
|
-
storage.setItem(key, JSON.stringify({ v: version, messages: messages.slice(-cap) }));
|
|
46
|
+
storage.setItem(key, JSON.stringify({ v: version, messages: messages.slice(-cap).map(withoutContent) }));
|
|
28
47
|
},
|
|
29
48
|
clear: () => {
|
|
30
49
|
storage.removeItem(key);
|
package/ui/Dropdown.tsx
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "akanjs/client";
|
|
3
|
+
import { capitalize } from "akanjs/common";
|
|
4
|
+
import { st } from "akanjs/store";
|
|
3
5
|
import { type ReactNode, useEffect, useId, useRef, useState } from "react";
|
|
4
6
|
import { createPortal } from "react-dom";
|
|
5
7
|
|
|
8
|
+
import { agentAttrs } from "./agentAttrs";
|
|
6
9
|
import { buttonRecipe } from "./Button";
|
|
7
10
|
import {
|
|
8
11
|
isOwnOverlayClick,
|
|
@@ -32,6 +35,8 @@ export interface DropdownProps {
|
|
|
32
35
|
dropdownClassName?: string;
|
|
33
36
|
/** Trigger edge the menu lines up with. Position is computed, so a `left-0` class cannot do this. */
|
|
34
37
|
align?: "start" | "end";
|
|
38
|
+
/** Names this dropdown for the in-page agent. Without it the menu publishes nothing — two on one screen would share a name. */
|
|
39
|
+
namespace?: string;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
export const DefaultDropdown = ({
|
|
@@ -41,9 +46,11 @@ export const DefaultDropdown = ({
|
|
|
41
46
|
buttonClassName,
|
|
42
47
|
dropdownClassName,
|
|
43
48
|
align = "end",
|
|
49
|
+
namespace,
|
|
44
50
|
}: DropdownProps) => {
|
|
45
51
|
const [opened, setOpened] = useState(false);
|
|
46
|
-
|
|
52
|
+
|
|
53
|
+
const [portal, setPortal] = useState<HTMLElement | null>(null);
|
|
47
54
|
const ref = useRef<HTMLDivElement>(null);
|
|
48
55
|
const menuRef = useRef<HTMLUListElement>(null);
|
|
49
56
|
const scope = useOverlayScope(useId());
|
|
@@ -52,6 +59,29 @@ export const DefaultDropdown = ({
|
|
|
52
59
|
|
|
53
60
|
const recipe = useUiRecipe("button") ?? buttonRecipe;
|
|
54
61
|
const position = useOverlayPosition({ opened, triggerRef: ref, panelRef: menuRef, align });
|
|
62
|
+
const suffix = namespace ? capitalize(namespace) : "";
|
|
63
|
+
st.expose(namespace ? `dropdownIn${suffix}` : null, opened, { desc: "Whether this dropdown menu is showing." });
|
|
64
|
+
const openDropdown = st
|
|
65
|
+
.tool(namespace ? `openDropdownIn${suffix}` : null, {
|
|
66
|
+
desc: `Open the ${namespace ?? ""} dropdown menu.`,
|
|
67
|
+
effect: "state",
|
|
68
|
+
})
|
|
69
|
+
.exec(() => {
|
|
70
|
+
setOpened(true);
|
|
71
|
+
});
|
|
72
|
+
const closeDropdown = st
|
|
73
|
+
.tool(namespace ? `closeDropdownIn${suffix}` : null, {
|
|
74
|
+
desc: `Close the ${namespace ?? ""} dropdown menu.`,
|
|
75
|
+
effect: "state",
|
|
76
|
+
})
|
|
77
|
+
.exec(() => {
|
|
78
|
+
setOpened(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const toggle = opened ? closeDropdown : openDropdown;
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
setPortal(document.body);
|
|
84
|
+
}, []);
|
|
55
85
|
useEffect(() => {
|
|
56
86
|
if (!opened) return;
|
|
57
87
|
const onMouseDown = (e: MouseEvent) => {
|
|
@@ -102,15 +132,15 @@ export const DefaultDropdown = ({
|
|
|
102
132
|
aria-haspopup="menu"
|
|
103
133
|
aria-expanded={opened}
|
|
104
134
|
className={recipe({ variant: "ghost" }, ["flex", buttonClassName])}
|
|
105
|
-
onClick={
|
|
106
|
-
|
|
107
|
-
setOpened((o) => !o);
|
|
108
|
-
}}
|
|
135
|
+
onClick={toggle}
|
|
136
|
+
{...agentAttrs(toggle)}
|
|
109
137
|
>
|
|
110
138
|
{value}
|
|
111
139
|
</button>
|
|
112
|
-
{/*
|
|
113
|
-
|
|
140
|
+
{/* Mounted from the first render and hidden while closed: a menu item declares its tool on mount, so an
|
|
141
|
+
unmounted menu publishes nothing an agent could find — and unmounting an open one takes any overlay
|
|
142
|
+
a menu item opened down with it. */}
|
|
143
|
+
{portal ? createPortal(menu, portal) : null}
|
|
114
144
|
</div>
|
|
115
145
|
);
|
|
116
146
|
};
|
package/ui/index.ts
CHANGED
|
@@ -237,7 +237,9 @@ export class AgentSession {
|
|
|
237
237
|
*/
|
|
238
238
|
retry = async (): Promise<boolean> => {
|
|
239
239
|
if (this.#active) return false;
|
|
240
|
-
const at = this.#messages.findLastIndex(
|
|
240
|
+
const at = this.#messages.findLastIndex(
|
|
241
|
+
(message) => message.role === "user" && (!!message.text || !!message.attachments?.length),
|
|
242
|
+
);
|
|
241
243
|
if (at < 0) return false;
|
|
242
244
|
const again = this.#messages[at];
|
|
243
245
|
this.#messages = this.#messages.slice(0, at);
|
|
@@ -33,6 +33,24 @@ tools, and the loop stay in the client; the server serves exactly one stateless
|
|
|
33
33
|
- `context` blocks are host vocabulary. The server forwards them to the model as data, framed as data.
|
|
34
34
|
- Everything is JSON-serializable by contract; a tool whose `result` is not is the tool's bug.
|
|
35
35
|
|
|
36
|
+
## Attachments
|
|
37
|
+
|
|
38
|
+
A message may carry files in `attachments` (`MessageAttachment`, `types.ts`) — one of `data` (base64 bytes), `url`,
|
|
39
|
+
or `text` (already-extracted content), plus `name` and `mimeType`:
|
|
40
|
+
|
|
41
|
+
```jsonc
|
|
42
|
+
{ "role": "user", "text": "What does this chart say?",
|
|
43
|
+
"attachments": [{ "name": "q3.png", "mimeType": "image/png", "data": "iVBORw0KG…" }] }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
They are content, not instructions, and the server frames them the way it frames `context`. **A backend must not
|
|
47
|
+
silently drop one its model cannot read** — a file the model never saw is a file it invents an answer about. Replace
|
|
48
|
+
it with a note in the message text saying which file was not read and why, so the model can say so and ask for
|
|
49
|
+
another form. `text` is readable by every provider by definition, which is what makes an extracted PDF work against
|
|
50
|
+
a text-only model.
|
|
51
|
+
|
|
52
|
+
Nothing here is stored: the wire carries the bytes for exactly one turn's request.
|
|
53
|
+
|
|
36
54
|
## Response
|
|
37
55
|
|
|
38
56
|
`200` with a single JSON object:
|
|
@@ -119,9 +119,33 @@ export interface ToolCallResult {
|
|
|
119
119
|
error?: string;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A file the user handed the conversation rather than the screen — which is why it rides a message instead of a
|
|
124
|
+
* tool, the same reason `askUser` belongs to the session and not to the surface.
|
|
125
|
+
*
|
|
126
|
+
* Three carriers, one of which every attachment must have: `data` inlines the bytes, `url` points at something the
|
|
127
|
+
* provider can fetch, and `text` is content somebody already extracted — the only form a text-only model can read.
|
|
128
|
+
* They mirror what the server's own `Msg.image` / `Msg.link` / `Msg.resource` builders produce, so a prompt's
|
|
129
|
+
* attachment and a user's are the same thing on the wire.
|
|
130
|
+
*
|
|
131
|
+
* Whether a given carrier reaches the model is the provider's answer, not this type's: a backend drops what its
|
|
132
|
+
* model cannot read and says so in the transcript, because a silently dropped file is one the model then
|
|
133
|
+
* hallucinates about.
|
|
134
|
+
*/
|
|
135
|
+
export interface MessageAttachment {
|
|
136
|
+
name: string;
|
|
137
|
+
mimeType: string;
|
|
138
|
+
/** Base64, with no `data:` prefix. */
|
|
139
|
+
data?: string;
|
|
140
|
+
url?: string;
|
|
141
|
+
text?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
122
144
|
export interface ChatMessage {
|
|
123
145
|
role: ChatRole;
|
|
124
146
|
text?: string;
|
|
147
|
+
/** Files the message carries. Content, not instructions — a backend frames them the way it frames context. */
|
|
148
|
+
attachments?: MessageAttachment[];
|
|
125
149
|
toolCalls?: ToolCallRequest[];
|
|
126
150
|
toolResults?: ToolCallResult[];
|
|
127
151
|
/** A failed or capped turn, recorded in the transcript rather than thrown past it. */
|