akanjs 3.0.0-beta.1 → 3.0.0-beta.3
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/agentTurn.dictionary.ts +6 -2
- package/fetch/agentTurn.ts +1 -1
- 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/predefinedAdaptor/anthropicLlm.ts +32 -5
- package/service/predefinedAdaptor/llm.adaptor.ts +7 -1
- package/service/predefinedAdaptor/openaiDialect.ts +38 -9
- package/signal/agentTurnStream.ts +5 -2
- package/types/fetch/agentTurn.d.ts +3 -3
- package/types/service/agent.service.d.ts +1 -1
- package/types/service/predefinedAdaptor/anthropicLlm.d.ts +15 -0
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +7 -1
- package/types/service/predefinedAdaptor/openaiDialect.d.ts +12 -0
- package/types/signal/agent.signal.d.ts +1 -1
- package/types/signal/agentTurn.d.ts +1 -1
- package/types/signal/agentTurnStream.d.ts +1 -1
- package/types/ui/Agent/Chat.d.ts +5 -0
- package/types/vendor/use-agentic/types.d.ts +7 -1
- package/ui/Agent/Chat.tsx +5 -0
- package/vendor/use-agentic/AgentSession.ts +12 -2
- package/vendor/use-agentic/httpRunner.ts +1 -1
- package/vendor/use-agentic/types.ts +8 -1
|
@@ -19,11 +19,15 @@ export const agentTurnDictionary = scalarDictionary(["en", "ko"])
|
|
|
19
19
|
"클라이언트가 실행할 툴 호출 목록 ({ id, name, args })",
|
|
20
20
|
]),
|
|
21
21
|
stop: t(["Stop", "종료 사유"]).desc([
|
|
22
|
-
"Why the turn ended — end,
|
|
23
|
-
"턴이 끝난 이유 — end
|
|
22
|
+
"Why the turn ended — end, toolUse when tool results are awaited, or length when the provider cut it off",
|
|
23
|
+
"턴이 끝난 이유 — end, 툴 결과를 기다리는 toolUse, 프로바이더가 잘라낸 length",
|
|
24
24
|
]),
|
|
25
25
|
}))
|
|
26
26
|
.enum<AgentStop>("agentStop", (t) => ({
|
|
27
27
|
end: t(["End", "종료"]).desc(["The final answer", "최종 응답"]),
|
|
28
28
|
toolUse: t(["Tool Use", "툴 사용"]).desc(["The model awaits tool results", "모델이 툴 결과를 기다린다"]),
|
|
29
|
+
length: t(["Length", "길이 초과"]).desc([
|
|
30
|
+
"The provider's answer ceiling cut the turn off, so it is incomplete",
|
|
31
|
+
"프로바이더의 응답 상한에 걸려 턴이 잘렸다. 미완성이다",
|
|
32
|
+
]),
|
|
29
33
|
}));
|
package/fetch/agentTurn.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Any, enumOf } from "akanjs/base";
|
|
2
2
|
import { ConstantRegistry, via } from "akanjs/constant";
|
|
3
3
|
|
|
4
|
-
export class AgentStop extends enumOf("agentStop", ["end", "toolUse"] as const) {}
|
|
4
|
+
export class AgentStop extends enumOf("agentStop", ["end", "toolUse", "length"] as const) {}
|
|
5
5
|
|
|
6
6
|
export class AgentTurn extends via((field) => ({
|
|
7
7
|
text: field(String, { default: "" }), // the assistant's words; empty when the turn is only tool calls
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -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
|
}
|
|
@@ -101,7 +113,7 @@ export class AnthropicLlm
|
|
|
101
113
|
* so it is one line to diagnose rather than a model that appears to have refused.
|
|
102
114
|
*/
|
|
103
115
|
#reported(answer: LlmTurnAnswer): LlmTurnAnswer {
|
|
104
|
-
if (!answer.text && !answer.toolCalls?.length)
|
|
116
|
+
if (!answer.text && !answer.toolCalls?.length && answer.stop !== "length")
|
|
105
117
|
this.logger.warn(
|
|
106
118
|
`Anthropic answered with no text and no tool call. If this repeats, raise option.setLlm({ maxTokens }) — currently ${this.llmOption.maxTokens ?? AnthropicLlm.defaultMaxTokens}.`,
|
|
107
119
|
);
|
|
@@ -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 };
|
|
@@ -294,10 +315,16 @@ export class AnthropicLlm
|
|
|
294
315
|
return {
|
|
295
316
|
...(text ? { text } : {}),
|
|
296
317
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
297
|
-
stop: answer.stop_reason
|
|
318
|
+
stop: AnthropicLlm.stopOf(answer.stop_reason, toolCalls.length),
|
|
298
319
|
};
|
|
299
320
|
}
|
|
300
321
|
|
|
322
|
+
/** The ceiling wins over the calls that did arrive — see `OpenaiDialect.stopOf` for why. */
|
|
323
|
+
static stopOf(reason: string | null | undefined, calls: number): LlmTurnAnswer["stop"] {
|
|
324
|
+
if (reason === "max_tokens") return "length";
|
|
325
|
+
return reason === "tool_use" || calls ? "toolUse" : "end";
|
|
326
|
+
}
|
|
327
|
+
|
|
301
328
|
/**
|
|
302
329
|
* The API streams named SSE events rather than one chunk shape. A tool call opens as `content_block_start`
|
|
303
330
|
* carrying its id and name and then arrives as `input_json_delta` fragments of a JSON string, so it is assembled
|
|
@@ -359,7 +386,7 @@ export class AnthropicLlm
|
|
|
359
386
|
return {
|
|
360
387
|
...(text ? { text } : {}),
|
|
361
388
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
362
|
-
stop: stopReason
|
|
389
|
+
stop: AnthropicLlm.stopOf(stopReason, toolCalls.length),
|
|
363
390
|
};
|
|
364
391
|
}
|
|
365
392
|
|
|
@@ -68,7 +68,13 @@ export interface LlmTurnRequest {
|
|
|
68
68
|
export interface LlmTurnAnswer {
|
|
69
69
|
text?: string;
|
|
70
70
|
toolCalls?: AgentWireToolCall[];
|
|
71
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Why the turn ended. `"length"` is the provider's ceiling — `finish_reason: "length"`, `stop_reason:
|
|
73
|
+
* "max_tokens"` — and it is distinguished from `"end"` because the two are indistinguishable downstream
|
|
74
|
+
* otherwise: a truncated answer reads as a complete one, and a turn cut off mid tool call carries no complete
|
|
75
|
+
* call at all, so it would end the loop looking exactly like a model that chose to stop.
|
|
76
|
+
*/
|
|
77
|
+
stop: "end" | "toolUse" | "length";
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
/**
|
|
@@ -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
|
}
|
|
@@ -193,10 +213,19 @@ export class OpenaiDialect {
|
|
|
193
213
|
return {
|
|
194
214
|
...(text ? { text } : {}),
|
|
195
215
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
196
|
-
stop: finish
|
|
216
|
+
stop: OpenaiDialect.stopOf(finish, toolCalls.length),
|
|
197
217
|
};
|
|
198
218
|
}
|
|
199
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The ceiling wins over the calls that did arrive. A turn the provider cut short is one whose last call may be
|
|
222
|
+
* missing, so running the batch it did finish is acting on half an intention.
|
|
223
|
+
*/
|
|
224
|
+
static stopOf(finish: string | null | undefined, calls: number): LlmTurnAnswer["stop"] {
|
|
225
|
+
if (finish === "length") return "length";
|
|
226
|
+
return finish === "tool_calls" || calls ? "toolUse" : "end";
|
|
227
|
+
}
|
|
228
|
+
|
|
200
229
|
static turnAnswer(answer: OpenaiAnswer): LlmTurnAnswer {
|
|
201
230
|
const choice = answer.choices?.[0];
|
|
202
231
|
const toolCalls = (choice?.message?.tool_calls ?? []).flatMap((call) => {
|
|
@@ -206,7 +235,7 @@ export class OpenaiDialect {
|
|
|
206
235
|
return {
|
|
207
236
|
...(choice?.message?.content ? { text: choice.message.content } : {}),
|
|
208
237
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
209
|
-
stop: choice?.finish_reason
|
|
238
|
+
stop: OpenaiDialect.stopOf(choice?.finish_reason, toolCalls.length),
|
|
210
239
|
};
|
|
211
240
|
}
|
|
212
241
|
|
|
@@ -3,7 +3,7 @@ import type { AgentWireToolCall } from "akanjs/service";
|
|
|
3
3
|
interface StreamedTurn {
|
|
4
4
|
text?: string;
|
|
5
5
|
toolCalls?: AgentWireToolCall[];
|
|
6
|
-
stop?: "end" | "toolUse";
|
|
6
|
+
stop?: "end" | "toolUse" | "length";
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -44,7 +44,10 @@ export class AgentTurnStream {
|
|
|
44
44
|
if (!streamed && turn.text) send({ type: "text", delta: turn.text });
|
|
45
45
|
const toolCalls = turn.toolCalls ?? [];
|
|
46
46
|
for (const call of toolCalls) send({ type: "toolCall", id: call.id, name: call.name, args: call.args });
|
|
47
|
-
|
|
47
|
+
|
|
48
|
+
const stop =
|
|
49
|
+
turn.stop === "length" ? "length" : turn.stop === "toolUse" || toolCalls.length ? "toolUse" : "end";
|
|
50
|
+
send({ type: "done", stop });
|
|
48
51
|
} catch (error) {
|
|
49
52
|
|
|
50
53
|
send({ type: "error", ...AgentTurnStream.failure(error) });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Any } from "akanjs/base";
|
|
2
|
-
declare const AgentStop_base: import("akanjs/base").EnumInstance<"agentStop", "end" | "toolUse">;
|
|
2
|
+
declare const AgentStop_base: import("akanjs/base").EnumInstance<"agentStop", "length" | "end" | "toolUse">;
|
|
3
3
|
export declare class AgentStop extends AgentStop_base {
|
|
4
4
|
}
|
|
5
5
|
declare const AgentTurn_base: import("akanjs/constant").ConstantCls<import("akanjs/constant").ExtractFieldInfoObject<{
|
|
@@ -466,10 +466,10 @@ export declare class AgentTurn extends AgentTurn_base {
|
|
|
466
466
|
export declare const agentTurnConstant: import("akanjs/constant").ScalarConstantModel<"agentTurn", AgentTurn, import("akanjs/constant").DefaultOf<AgentTurn>, {
|
|
467
467
|
text: string;
|
|
468
468
|
toolCalls: unknown[];
|
|
469
|
-
stop: NonNullable<"end" | "toolUse">;
|
|
469
|
+
stop: NonNullable<"length" | "end" | "toolUse">;
|
|
470
470
|
} & {}, {
|
|
471
471
|
text: string;
|
|
472
472
|
toolCalls: unknown[];
|
|
473
|
-
stop: "end" | "toolUse";
|
|
473
|
+
stop: "length" | "end" | "toolUse";
|
|
474
474
|
} & {}>;
|
|
475
475
|
export {};
|
|
@@ -6,7 +6,7 @@ export declare class AgentService extends AgentService_base {
|
|
|
6
6
|
runTurn(request: LlmTurnRequest, onDelta?: (delta: string) => void): Promise<{
|
|
7
7
|
text: string;
|
|
8
8
|
toolCalls: import("./predefinedAdaptor.d.ts").AgentWireToolCall[];
|
|
9
|
-
stop: "end" | "toolUse";
|
|
9
|
+
stop: "length" | "end" | "toolUse";
|
|
10
10
|
}>;
|
|
11
11
|
/**
|
|
12
12
|
* The framework's half of the system prompt, ahead of whatever the app said so the app's text stays the more
|
|
@@ -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,8 +107,12 @@ 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;
|
|
114
|
+
/** The ceiling wins over the calls that did arrive — see `OpenaiDialect.stopOf` for why. */
|
|
115
|
+
static stopOf(reason: string | null | undefined, calls: number): LlmTurnAnswer["stop"];
|
|
101
116
|
/**
|
|
102
117
|
* The API streams named SSE events rather than one chunk shape. A tool call opens as `content_block_start`
|
|
103
118
|
* carrying its id and name and then arrives as `input_json_delta` fragments of a JSON string, so it is assembled
|
|
@@ -61,7 +61,13 @@ export interface LlmTurnRequest {
|
|
|
61
61
|
export interface LlmTurnAnswer {
|
|
62
62
|
text?: string;
|
|
63
63
|
toolCalls?: AgentWireToolCall[];
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Why the turn ended. `"length"` is the provider's ceiling — `finish_reason: "length"`, `stop_reason:
|
|
66
|
+
* "max_tokens"` — and it is distinguished from `"end"` because the two are indistinguishable downstream
|
|
67
|
+
* otherwise: a truncated answer reads as a complete one, and a turn cut off mid tool call carries no complete
|
|
68
|
+
* call at all, so it would end the loop looking exactly like a model that chose to stop.
|
|
69
|
+
*/
|
|
70
|
+
stop: "end" | "toolUse" | "length";
|
|
65
71
|
}
|
|
66
72
|
/**
|
|
67
73
|
* The provider seam for one stateless agent turn: the whole transcript in, one assistant answer out. The server
|
|
@@ -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;
|
|
@@ -79,6 +86,11 @@ export declare class OpenaiDialect {
|
|
|
79
86
|
* by index and parsed once at the end; only assistant text is worth reporting as it arrives.
|
|
80
87
|
*/
|
|
81
88
|
static consumeStream(body: ReadableStream<Uint8Array>, onDelta: (delta: string) => void): Promise<LlmTurnAnswer>;
|
|
89
|
+
/**
|
|
90
|
+
* The ceiling wins over the calls that did arrive. A turn the provider cut short is one whose last call may be
|
|
91
|
+
* missing, so running the batch it did finish is acting on half an intention.
|
|
92
|
+
*/
|
|
93
|
+
static stopOf(finish: string | null | undefined, calls: number): LlmTurnAnswer["stop"];
|
|
82
94
|
static turnAnswer(answer: OpenaiAnswer): LlmTurnAnswer;
|
|
83
95
|
/** The provider sends arguments as a JSON string; an unparsable one becomes an empty call rather than a crash. */
|
|
84
96
|
static parsedArgs(raw: string | undefined): Record<string, unknown>;
|
|
@@ -12,7 +12,7 @@ declare const AgentEndpoint_base: import("./endpoint.d.ts").EndpointCls<import("
|
|
|
12
12
|
}, ["messages", "tools", "context", "instructions"], [arg: unknown[], arg: unknown[], arg: unknown[], arg?: string | null | undefined], [arg: Bun.BunRequest<string>], [arg: unknown[], arg: unknown[], arg: unknown[], arg: string | undefined], typeof AgentTurn, AgentTurn, Promise<AgentTurn | {
|
|
13
13
|
text: string;
|
|
14
14
|
toolCalls: import("akanjs/service").AgentWireToolCall[];
|
|
15
|
-
stop: "end" | "toolUse";
|
|
15
|
+
stop: "length" | "end" | "toolUse";
|
|
16
16
|
}>, false>;
|
|
17
17
|
}>;
|
|
18
18
|
export declare class AgentEndpoint extends AgentEndpoint_base {
|
|
@@ -2,7 +2,7 @@ export { AgentStop, AgentTurn, agentTurnConstant } from "akanjs/fetch";
|
|
|
2
2
|
declare const AgentTurnDocument_base: import("akanjs/document").DatabaseCls<{
|
|
3
3
|
text: string;
|
|
4
4
|
toolCalls: unknown[];
|
|
5
|
-
stop: NonNullable<"end" | "toolUse">;
|
|
5
|
+
stop: NonNullable<"length" | "end" | "toolUse">;
|
|
6
6
|
} & {}>;
|
|
7
7
|
export declare class AgentTurnDocument extends AgentTurnDocument_base {
|
|
8
8
|
}
|
|
@@ -2,7 +2,7 @@ import type { AgentWireToolCall } from "akanjs/service";
|
|
|
2
2
|
interface StreamedTurn {
|
|
3
3
|
text?: string;
|
|
4
4
|
toolCalls?: AgentWireToolCall[];
|
|
5
|
-
stop?: "end" | "toolUse";
|
|
5
|
+
stop?: "end" | "toolUse" | "length";
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* The streaming half of the agent turn wire (use-agentic WIRE.md): the same endpoint answers `text/event-stream`
|
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
|
/**
|
|
@@ -114,6 +114,12 @@ export interface ToolCallResult {
|
|
|
114
114
|
* model cannot read and says so in the transcript, because a silently dropped file is one the model then
|
|
115
115
|
* hallucinates about.
|
|
116
116
|
*/
|
|
117
|
+
/**
|
|
118
|
+
* Why an assistant turn ended. `length` is the provider's own ceiling rather than the model's choice, so the turn
|
|
119
|
+
* is incomplete — a truncated answer and a turn cut off before its tool call finished both arrive this way, and
|
|
120
|
+
* neither is distinguishable from `end` without it.
|
|
121
|
+
*/
|
|
122
|
+
export type TurnStop = "end" | "toolUse" | "length";
|
|
117
123
|
export interface MessageAttachment {
|
|
118
124
|
name: string;
|
|
119
125
|
mimeType: string;
|
|
@@ -165,7 +171,7 @@ export type RunnerEvent = {
|
|
|
165
171
|
args: Record<string, unknown>;
|
|
166
172
|
} | {
|
|
167
173
|
type: "done";
|
|
168
|
-
stop:
|
|
174
|
+
stop: TurnStop;
|
|
169
175
|
}
|
|
170
176
|
/**
|
|
171
177
|
* `data` accompanies a message that is a code rather than a sentence — the values whoever resolves the code
|
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
|
/**
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
SurfaceView,
|
|
13
13
|
ToolCallRequest,
|
|
14
14
|
ToolCallResult,
|
|
15
|
+
TurnStop,
|
|
15
16
|
} from "./types";
|
|
16
17
|
|
|
17
18
|
export interface PendingApproval {
|
|
@@ -264,6 +265,15 @@ export class AgentSession {
|
|
|
264
265
|
this.#unanswered(toolCalls);
|
|
265
266
|
return;
|
|
266
267
|
}
|
|
268
|
+
|
|
269
|
+
if (stop === "length") {
|
|
270
|
+
|
|
271
|
+
this.#fail(
|
|
272
|
+
"The model ran out of room mid-answer, so this turn is incomplete. Ask again, or raise the answer limit.",
|
|
273
|
+
);
|
|
274
|
+
if (toolCalls.length) this.#unanswered(toolCalls);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
267
277
|
if (stop !== "toolUse" || !toolCalls.length) return;
|
|
268
278
|
const toolResults: ToolCallResult[] = [];
|
|
269
279
|
for (const call of toolCalls)
|
|
@@ -478,7 +488,7 @@ export class AgentSession {
|
|
|
478
488
|
return text;
|
|
479
489
|
}
|
|
480
490
|
|
|
481
|
-
async #assistantTurn(signal: AbortSignal): Promise<{ toolCalls: ToolCallRequest[]; stop:
|
|
491
|
+
async #assistantTurn(signal: AbortSignal): Promise<{ toolCalls: ToolCallRequest[]; stop: TurnStop }> {
|
|
482
492
|
const { tools, guides } = this.#surface.snapshot();
|
|
483
493
|
const instructions = [this.#options.instructions, ...guides].filter(Boolean).join("\n\n");
|
|
484
494
|
const request: RunnerRequest = {
|
|
@@ -493,7 +503,7 @@ export class AgentSession {
|
|
|
493
503
|
this.#append({ role: "assistant" });
|
|
494
504
|
let text = "";
|
|
495
505
|
const toolCalls: ToolCallRequest[] = [];
|
|
496
|
-
let stop:
|
|
506
|
+
let stop: TurnStop = "end";
|
|
497
507
|
for await (const event of this.#runner.run(request)) {
|
|
498
508
|
if (signal.aborted) break;
|
|
499
509
|
if (event.type === "text") {
|
|
@@ -10,7 +10,7 @@ export interface HttpRunnerOptions {
|
|
|
10
10
|
interface TurnAnswer {
|
|
11
11
|
text?: string;
|
|
12
12
|
toolCalls?: ToolCallRequest[];
|
|
13
|
-
stop?: "end" | "toolUse";
|
|
13
|
+
stop?: "end" | "toolUse" | "length";
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const eventTypes = new Set(["text", "toolCall", "done", "error"]);
|
|
@@ -131,6 +131,13 @@ export interface ToolCallResult {
|
|
|
131
131
|
* model cannot read and says so in the transcript, because a silently dropped file is one the model then
|
|
132
132
|
* hallucinates about.
|
|
133
133
|
*/
|
|
134
|
+
/**
|
|
135
|
+
* Why an assistant turn ended. `length` is the provider's own ceiling rather than the model's choice, so the turn
|
|
136
|
+
* is incomplete — a truncated answer and a turn cut off before its tool call finished both arrive this way, and
|
|
137
|
+
* neither is distinguishable from `end` without it.
|
|
138
|
+
*/
|
|
139
|
+
export type TurnStop = "end" | "toolUse" | "length";
|
|
140
|
+
|
|
134
141
|
export interface MessageAttachment {
|
|
135
142
|
name: string;
|
|
136
143
|
mimeType: string;
|
|
@@ -178,7 +185,7 @@ export interface ContextBlock {
|
|
|
178
185
|
export type RunnerEvent =
|
|
179
186
|
| { type: "text"; delta: string }
|
|
180
187
|
| { type: "toolCall"; id: string; name: string; args: Record<string, unknown> }
|
|
181
|
-
| { type: "done"; stop:
|
|
188
|
+
| { type: "done"; stop: TurnStop }
|
|
182
189
|
/**
|
|
183
190
|
* `data` accompanies a message that is a code rather than a sentence — the values whoever resolves the code
|
|
184
191
|
* interpolates into its text. A host that does not know the code shows the message as it stands.
|