akanjs 3.0.0-beta.2 → 3.0.0-beta.4
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/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 +7 -3
- package/service/predefinedAdaptor/anthropicLlm.ts +23 -2
- package/service/predefinedAdaptor/openaiDialect.ts +27 -7
- package/types/service/predefinedAdaptor/anthropicLlm.d.ts +13 -0
- package/types/service/predefinedAdaptor/openaiDialect.d.ts +7 -0
- package/types/ui/Agent/Chat.d.ts +5 -0
- package/types/ui/index.d.ts +1 -1
- package/types/vendor/use-agentic/Transcript.d.ts +2 -1
- package/ui/Agent/Attach.tsx +2 -1
- package/ui/Agent/Chat.tsx +5 -0
- package/ui/index.ts +1 -0
- package/vendor/use-agentic/AgentSession.ts +3 -0
- package/vendor/use-agentic/Transcript.ts +2 -2
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/service/agent.service.ts
CHANGED
|
@@ -98,14 +98,18 @@ export class AgentService extends serve("agent" as const, ({ plug }) => ({
|
|
|
98
98
|
private static isReadable(attachment: AgentWireAttachment, accepts: LlmAccepts): boolean {
|
|
99
99
|
if (attachment.text) return true;
|
|
100
100
|
if (!attachment.data && !attachment.url) return false;
|
|
101
|
+
|
|
102
|
+
if (typeof attachment.mimeType !== "string") return false;
|
|
101
103
|
return attachment.mimeType.startsWith("image/") ? !!accepts.image : !!accepts.document;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
private static note(attachment: AgentWireAttachment): string {
|
|
105
107
|
const why =
|
|
106
|
-
attachment.data
|
|
107
|
-
? "
|
|
108
|
-
:
|
|
108
|
+
!attachment.data && !attachment.url
|
|
109
|
+
? "its content is no longer available, as a reloaded conversation keeps the name and not the bytes"
|
|
110
|
+
: typeof attachment.mimeType === "string"
|
|
111
|
+
? "this model cannot read that type"
|
|
112
|
+
: "it names no type it could be read as";
|
|
109
113
|
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.]`;
|
|
110
114
|
}
|
|
111
115
|
}
|
|
@@ -58,6 +58,18 @@ export class AnthropicLlm
|
|
|
58
58
|
*/
|
|
59
59
|
static readonly defaultMaxTokens = 8192;
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The four the API's image block reads. An exact set rather than an `image/*` prefix, because by the time an
|
|
63
|
+
* attachment reaches here `accepts.image` has already carried it past `AgentService.readable`: a phone's
|
|
64
|
+
* `image/heic` — the iPhone camera default, so the likeliest non-canonical image an app sees — arrives as bytes,
|
|
65
|
+
* becomes a block the API refuses, and takes the **whole turn** down on a 400 rather than going unread.
|
|
66
|
+
*
|
|
67
|
+
* The app cannot gate it either: `AttachReader` answers `null` for "not mine", which falls through to the
|
|
68
|
+
* built-in reader that base64s any `image/*`, so there is no way for a reader to refuse one. The check belongs
|
|
69
|
+
* where the block vocabulary is known, which is here.
|
|
70
|
+
*/
|
|
71
|
+
static readonly imageTypes = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
72
|
+
|
|
61
73
|
get #host() {
|
|
62
74
|
return this.llmOption.host ?? "https://api.anthropic.com/v1";
|
|
63
75
|
}
|
|
@@ -266,9 +278,13 @@ export class AnthropicLlm
|
|
|
266
278
|
];
|
|
267
279
|
const source = AnthropicLlm.sourceOf(attachment);
|
|
268
280
|
if (!source) return [];
|
|
269
|
-
|
|
281
|
+
|
|
282
|
+
const mimeType = attachment.mimeType.split(";")[0].trim().toLowerCase();
|
|
283
|
+
if (accepts?.image && AnthropicLlm.imageTypes.has(mimeType))
|
|
284
|
+
return [{ type: "image", source: AnthropicLlm.typed(source, mimeType) }];
|
|
270
285
|
|
|
271
|
-
if (accepts?.document &&
|
|
286
|
+
if (accepts?.document && mimeType === "application/pdf")
|
|
287
|
+
return [{ type: "document", source: AnthropicLlm.typed(source, mimeType) }];
|
|
272
288
|
notes.push(`[Attachment not read: ${attachment.name} (${attachment.mimeType}) — this API has no block for it.]`);
|
|
273
289
|
return [];
|
|
274
290
|
});
|
|
@@ -276,6 +292,11 @@ export class AnthropicLlm
|
|
|
276
292
|
return [...(text ? [{ type: "text" as const, text }] : []), ...blocks];
|
|
277
293
|
}
|
|
278
294
|
|
|
295
|
+
/** The block's `media_type` is the essence, not whatever parameters the browser attached to it. */
|
|
296
|
+
static typed(source: AnthropicSource, mimeType: string): AnthropicSource {
|
|
297
|
+
return source.type === "base64" ? { ...source, media_type: mimeType } : source;
|
|
298
|
+
}
|
|
299
|
+
|
|
279
300
|
static sourceOf(attachment: AgentWireAttachment): AnthropicSource | null {
|
|
280
301
|
if (attachment.url) return { type: "url", url: attachment.url };
|
|
281
302
|
if (attachment.data) return { type: "base64", media_type: attachment.mimeType, data: attachment.data };
|
|
@@ -34,6 +34,14 @@ export interface OpenaiMessage {
|
|
|
34
34
|
* read to a note in the text.
|
|
35
35
|
*/
|
|
36
36
|
export class OpenaiDialect {
|
|
37
|
+
/**
|
|
38
|
+
* The types this dialect's image part reads. Exact rather than an `image/*` prefix and declared apart from
|
|
39
|
+
* Anthropic's identical-looking set, because the two are each a provider's own list and only happen to agree:
|
|
40
|
+
* an unsupported one passed through is a refused *request*, not an unread attachment, so the safe direction is
|
|
41
|
+
* to name what is known to work and note the rest.
|
|
42
|
+
*/
|
|
43
|
+
static readonly imageTypes = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
44
|
+
|
|
37
45
|
static requestBody(
|
|
38
46
|
model: string,
|
|
39
47
|
request: LlmTurnRequest,
|
|
@@ -118,16 +126,28 @@ export class OpenaiDialect {
|
|
|
118
126
|
*/
|
|
119
127
|
static userContent(message: AgentWireMessage, accepts?: LlmAccepts): string | OpenaiContentPart[] {
|
|
120
128
|
const attachments = message.attachments ?? [];
|
|
129
|
+
const notes: string[] = [];
|
|
121
130
|
const blocks = attachments.flatMap((attachment) =>
|
|
122
131
|
attachment.text ? [`--- attachment: ${attachment.name} (${attachment.mimeType}) ---\n${attachment.text}`] : [],
|
|
123
132
|
);
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
const images = !accepts?.image
|
|
134
|
+
? []
|
|
135
|
+
: attachments.flatMap((attachment) => {
|
|
136
|
+
if (attachment.text) return [];
|
|
137
|
+
|
|
138
|
+
const mimeType = attachment.mimeType.split(";")[0].trim().toLowerCase();
|
|
139
|
+
|
|
140
|
+
if (!mimeType.startsWith("image/")) return [];
|
|
141
|
+
if (!OpenaiDialect.imageTypes.has(mimeType)) {
|
|
142
|
+
notes.push(
|
|
143
|
+
`[Attachment not read: ${attachment.name} (${attachment.mimeType}) — this API reads no image of that type.]`,
|
|
144
|
+
);
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
const url = attachment.url ?? (attachment.data ? `data:${mimeType};base64,${attachment.data}` : "");
|
|
148
|
+
return url ? [{ type: "image_url" as const, image_url: { url } }] : [];
|
|
149
|
+
});
|
|
150
|
+
const text = [message.text, ...blocks, ...notes].filter(Boolean).join("\n\n");
|
|
131
151
|
if (!images.length) return text;
|
|
132
152
|
return [...(text ? [{ type: "text" as const, text }] : []), ...images];
|
|
133
153
|
}
|
|
@@ -64,6 +64,17 @@ export declare class AnthropicLlm extends AnthropicLlm_base implements LlmAdapto
|
|
|
64
64
|
* model refusing, so it is `option.setLlm({ maxTokens })` and not a constant.
|
|
65
65
|
*/
|
|
66
66
|
static readonly defaultMaxTokens = 8192;
|
|
67
|
+
/**
|
|
68
|
+
* The four the API's image block reads. An exact set rather than an `image/*` prefix, because by the time an
|
|
69
|
+
* attachment reaches here `accepts.image` has already carried it past `AgentService.readable`: a phone's
|
|
70
|
+
* `image/heic` — the iPhone camera default, so the likeliest non-canonical image an app sees — arrives as bytes,
|
|
71
|
+
* becomes a block the API refuses, and takes the **whole turn** down on a 400 rather than going unread.
|
|
72
|
+
*
|
|
73
|
+
* The app cannot gate it either: `AttachReader` answers `null` for "not mine", which falls through to the
|
|
74
|
+
* built-in reader that base64s any `image/*`, so there is no way for a reader to refuse one. The check belongs
|
|
75
|
+
* where the block vocabulary is known, which is here.
|
|
76
|
+
*/
|
|
77
|
+
static readonly imageTypes: Set<string>;
|
|
67
78
|
/** What the API's blocks carry. A model of the family that reads neither takes the `accepts` override. */
|
|
68
79
|
get accepts(): LlmAccepts;
|
|
69
80
|
chat(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<LlmTurnAnswer | null>;
|
|
@@ -96,6 +107,8 @@ export declare class AnthropicLlm extends AnthropicLlm_base implements LlmAdapto
|
|
|
96
107
|
static providerMessages(messages: AgentWireMessage[], accepts?: LlmAccepts): AnthropicMessage[];
|
|
97
108
|
static providerMessage(message: AgentWireMessage, accepts?: LlmAccepts): AnthropicMessage;
|
|
98
109
|
static userContent(message: AgentWireMessage, accepts?: LlmAccepts): AnthropicBlock[];
|
|
110
|
+
/** The block's `media_type` is the essence, not whatever parameters the browser attached to it. */
|
|
111
|
+
static typed(source: AnthropicSource, mimeType: string): AnthropicSource;
|
|
99
112
|
static sourceOf(attachment: AgentWireAttachment): AnthropicSource | null;
|
|
100
113
|
static turnAnswer(answer: AnthropicAnswer): LlmTurnAnswer;
|
|
101
114
|
/** The ceiling wins over the calls that did arrive — see `OpenaiDialect.stopOf` for why. */
|
|
@@ -47,6 +47,13 @@ export interface OpenaiMessage {
|
|
|
47
47
|
* read to a note in the text.
|
|
48
48
|
*/
|
|
49
49
|
export declare class OpenaiDialect {
|
|
50
|
+
/**
|
|
51
|
+
* The types this dialect's image part reads. Exact rather than an `image/*` prefix and declared apart from
|
|
52
|
+
* Anthropic's identical-looking set, because the two are each a provider's own list and only happen to agree:
|
|
53
|
+
* an unsupported one passed through is a refused *request*, not an unread attachment, so the safe direction is
|
|
54
|
+
* to name what is known to work and note the rest.
|
|
55
|
+
*/
|
|
56
|
+
static readonly imageTypes: Set<string>;
|
|
50
57
|
static requestBody(model: string, request: LlmTurnRequest, { accepts, stream }?: {
|
|
51
58
|
accepts?: LlmAccepts;
|
|
52
59
|
stream?: boolean;
|
package/types/ui/Agent/Chat.d.ts
CHANGED
|
@@ -70,6 +70,11 @@ export interface ChatProps {
|
|
|
70
70
|
* (images as bytes, text as text). This is where an app puts what needs a parser — a PDF's text, a spreadsheet's
|
|
71
71
|
* cells — since the framework carries attachments but depends on nothing that can extract one. It runs before
|
|
72
72
|
* the built-in, so it can also replace how an image is prepared.
|
|
73
|
+
*
|
|
74
|
+
* **A `url` is handed to the provider as the address it will fetch**, so answer `data` whenever the provider
|
|
75
|
+
* cannot reach it. A reader that uploads is the natural place to get this wrong: the default storage backend
|
|
76
|
+
* serves a path only this app can resolve, and a model handed one answers about a picture it never saw, with
|
|
77
|
+
* nothing anywhere reporting a failure.
|
|
73
78
|
*/
|
|
74
79
|
attach?: AttachReader;
|
|
75
80
|
/**
|
package/types/ui/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AgentProvider, type AgentProviderProps, type AgentRunner, AgentSession, type AgentSessionOptions, type ChatMessage, type CompactOptions, type ContextBlock, httpRunner, type PublishedTool, type RunnerEvent, type RunnerRequest, SessionContext, type SessionHistory, type SurfaceView, useAgent, } from "../vendor/use-agentic.d.ts";
|
|
1
|
+
export { AgentProvider, type AgentProviderProps, type AgentRunner, AgentSession, type AgentSessionOptions, type ChatMessage, type CompactOptions, type ContextBlock, httpRunner, type MessageAttachment, type PublishedTool, type RunnerEvent, type RunnerRequest, SessionContext, type SessionHistory, type SurfaceView, useAgent, } from "../vendor/use-agentic.d.ts";
|
|
2
2
|
export { Agent } from "./Agent.d.ts";
|
|
3
3
|
export { type ApprovalProps, DefaultApproval } from "./Agent/Approval.d.ts";
|
|
4
4
|
export { type AgentSessionSetup, agentSessionOf } from "./Agent/agentSessionOf.d.ts";
|
|
@@ -10,9 +10,10 @@ import type { ChatMessage } from "./types.d.ts";
|
|
|
10
10
|
* transcript is assembled rather than where each hole is made.
|
|
11
11
|
*/
|
|
12
12
|
export declare class Transcript {
|
|
13
|
-
#private;
|
|
14
13
|
static readonly unanswered = "The turn was stopped before this call ran.";
|
|
15
14
|
/** What one turn posts: no host-only message, no unanswered call, no result answering a call nobody sees. */
|
|
16
15
|
static wire(messages: readonly ChatMessage[]): ChatMessage[];
|
|
17
16
|
static sanitize(messages: readonly ChatMessage[]): ChatMessage[];
|
|
17
|
+
/** An empty assistant message is a draft a reload or an abort caught before it said anything. */
|
|
18
|
+
static carries(message: ChatMessage): boolean;
|
|
18
19
|
}
|
package/ui/Agent/Attach.tsx
CHANGED
|
@@ -40,6 +40,7 @@ export const Attach = ({ className, label, onPick }: AttachProps) => {
|
|
|
40
40
|
|
|
41
41
|
interface ChipsProps {
|
|
42
42
|
className?: string;
|
|
43
|
+
|
|
43
44
|
attachments: readonly MessageAttachment[];
|
|
44
45
|
/** Omitted for a sent message: what is already on the wire cannot be taken back. */
|
|
45
46
|
onRemove?: (index: number) => void;
|
|
@@ -56,7 +57,7 @@ export const Chips = ({ className, attachments, onRemove, removeLabel, pending =
|
|
|
56
57
|
className="flex items-center gap-1 rounded-field bg-muted px-2 py-0.5 text-xs"
|
|
57
58
|
key={`${attachment.name}-${idx}`}
|
|
58
59
|
>
|
|
59
|
-
{attachment.data && attachment.mimeType
|
|
60
|
+
{attachment.data && attachment.mimeType?.startsWith("image/") ? (
|
|
60
61
|
<img
|
|
61
62
|
alt={attachment.name}
|
|
62
63
|
className="size-6 rounded-field object-cover"
|
package/ui/Agent/Chat.tsx
CHANGED
|
@@ -109,6 +109,11 @@ export interface ChatProps {
|
|
|
109
109
|
* (images as bytes, text as text). This is where an app puts what needs a parser — a PDF's text, a spreadsheet's
|
|
110
110
|
* cells — since the framework carries attachments but depends on nothing that can extract one. It runs before
|
|
111
111
|
* the built-in, so it can also replace how an image is prepared.
|
|
112
|
+
*
|
|
113
|
+
* **A `url` is handed to the provider as the address it will fetch**, so answer `data` whenever the provider
|
|
114
|
+
* cannot reach it. A reader that uploads is the natural place to get this wrong: the default storage backend
|
|
115
|
+
* serves a path only this app can resolve, and a model handed one answers about a picture it never saw, with
|
|
116
|
+
* nothing anywhere reporting a failure.
|
|
112
117
|
*/
|
|
113
118
|
attach?: AttachReader;
|
|
114
119
|
/**
|
package/ui/index.ts
CHANGED
|
@@ -293,6 +293,9 @@ export class AgentSession {
|
|
|
293
293
|
this.#pending = null;
|
|
294
294
|
this.#question = null;
|
|
295
295
|
this.#progress = null;
|
|
296
|
+
|
|
297
|
+
const draft = this.#messages[this.#messages.length - 1];
|
|
298
|
+
if (draft?.role === "assistant" && !Transcript.carries(draft)) this.#messages = this.#messages.slice(0, -1);
|
|
296
299
|
this.#notify();
|
|
297
300
|
}
|
|
298
301
|
}
|
|
@@ -28,7 +28,7 @@ export class Transcript {
|
|
|
28
28
|
if (results.length) kept.push({ ...message, toolResults: results });
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
|
-
if (message.role === "assistant" && !Transcript
|
|
31
|
+
if (message.role === "assistant" && !Transcript.carries(message)) continue;
|
|
32
32
|
kept.push(message);
|
|
33
33
|
const calls = message.toolCalls ?? [];
|
|
34
34
|
for (const call of calls) called.add(call.id);
|
|
@@ -44,7 +44,7 @@ export class Transcript {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/** An empty assistant message is a draft a reload or an abort caught before it said anything. */
|
|
47
|
-
static
|
|
47
|
+
static carries(message: ChatMessage) {
|
|
48
48
|
return !!message.text || !!message.error || !!message.toolCalls?.length || !!message.attachments?.length;
|
|
49
49
|
}
|
|
50
50
|
}
|