akanjs 3.0.0-beta.1 → 3.0.0-beta.2
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 +9 -3
- package/service/predefinedAdaptor/llm.adaptor.ts +7 -1
- package/service/predefinedAdaptor/openaiDialect.ts +11 -2
- 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 +2 -0
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +7 -1
- package/types/service/predefinedAdaptor/openaiDialect.d.ts +5 -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/vendor/use-agentic/types.d.ts +7 -1
- 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
|
@@ -101,7 +101,7 @@ export class AnthropicLlm
|
|
|
101
101
|
* so it is one line to diagnose rather than a model that appears to have refused.
|
|
102
102
|
*/
|
|
103
103
|
#reported(answer: LlmTurnAnswer): LlmTurnAnswer {
|
|
104
|
-
if (!answer.text && !answer.toolCalls?.length)
|
|
104
|
+
if (!answer.text && !answer.toolCalls?.length && answer.stop !== "length")
|
|
105
105
|
this.logger.warn(
|
|
106
106
|
`Anthropic answered with no text and no tool call. If this repeats, raise option.setLlm({ maxTokens }) — currently ${this.llmOption.maxTokens ?? AnthropicLlm.defaultMaxTokens}.`,
|
|
107
107
|
);
|
|
@@ -294,10 +294,16 @@ export class AnthropicLlm
|
|
|
294
294
|
return {
|
|
295
295
|
...(text ? { text } : {}),
|
|
296
296
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
297
|
-
stop: answer.stop_reason
|
|
297
|
+
stop: AnthropicLlm.stopOf(answer.stop_reason, toolCalls.length),
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/** The ceiling wins over the calls that did arrive — see `OpenaiDialect.stopOf` for why. */
|
|
302
|
+
static stopOf(reason: string | null | undefined, calls: number): LlmTurnAnswer["stop"] {
|
|
303
|
+
if (reason === "max_tokens") return "length";
|
|
304
|
+
return reason === "tool_use" || calls ? "toolUse" : "end";
|
|
305
|
+
}
|
|
306
|
+
|
|
301
307
|
/**
|
|
302
308
|
* The API streams named SSE events rather than one chunk shape. A tool call opens as `content_block_start`
|
|
303
309
|
* carrying its id and name and then arrives as `input_json_delta` fragments of a JSON string, so it is assembled
|
|
@@ -359,7 +365,7 @@ export class AnthropicLlm
|
|
|
359
365
|
return {
|
|
360
366
|
...(text ? { text } : {}),
|
|
361
367
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
362
|
-
stop: stopReason
|
|
368
|
+
stop: AnthropicLlm.stopOf(stopReason, toolCalls.length),
|
|
363
369
|
};
|
|
364
370
|
}
|
|
365
371
|
|
|
@@ -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
|
/**
|
|
@@ -193,10 +193,19 @@ export class OpenaiDialect {
|
|
|
193
193
|
return {
|
|
194
194
|
...(text ? { text } : {}),
|
|
195
195
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
196
|
-
stop: finish
|
|
196
|
+
stop: OpenaiDialect.stopOf(finish, toolCalls.length),
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/**
|
|
201
|
+
* The ceiling wins over the calls that did arrive. A turn the provider cut short is one whose last call may be
|
|
202
|
+
* missing, so running the batch it did finish is acting on half an intention.
|
|
203
|
+
*/
|
|
204
|
+
static stopOf(finish: string | null | undefined, calls: number): LlmTurnAnswer["stop"] {
|
|
205
|
+
if (finish === "length") return "length";
|
|
206
|
+
return finish === "tool_calls" || calls ? "toolUse" : "end";
|
|
207
|
+
}
|
|
208
|
+
|
|
200
209
|
static turnAnswer(answer: OpenaiAnswer): LlmTurnAnswer {
|
|
201
210
|
const choice = answer.choices?.[0];
|
|
202
211
|
const toolCalls = (choice?.message?.tool_calls ?? []).flatMap((call) => {
|
|
@@ -206,7 +215,7 @@ export class OpenaiDialect {
|
|
|
206
215
|
return {
|
|
207
216
|
...(choice?.message?.content ? { text: choice.message.content } : {}),
|
|
208
217
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
209
|
-
stop: choice?.finish_reason
|
|
218
|
+
stop: OpenaiDialect.stopOf(choice?.finish_reason, toolCalls.length),
|
|
210
219
|
};
|
|
211
220
|
}
|
|
212
221
|
|
|
@@ -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
|
|
@@ -98,6 +98,8 @@ export declare class AnthropicLlm extends AnthropicLlm_base implements LlmAdapto
|
|
|
98
98
|
static userContent(message: AgentWireMessage, accepts?: LlmAccepts): AnthropicBlock[];
|
|
99
99
|
static sourceOf(attachment: AgentWireAttachment): AnthropicSource | null;
|
|
100
100
|
static turnAnswer(answer: AnthropicAnswer): LlmTurnAnswer;
|
|
101
|
+
/** The ceiling wins over the calls that did arrive — see `OpenaiDialect.stopOf` for why. */
|
|
102
|
+
static stopOf(reason: string | null | undefined, calls: number): LlmTurnAnswer["stop"];
|
|
101
103
|
/**
|
|
102
104
|
* The API streams named SSE events rather than one chunk shape. A tool call opens as `content_block_start`
|
|
103
105
|
* 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
|
|
@@ -79,6 +79,11 @@ export declare class OpenaiDialect {
|
|
|
79
79
|
* by index and parsed once at the end; only assistant text is worth reporting as it arrives.
|
|
80
80
|
*/
|
|
81
81
|
static consumeStream(body: ReadableStream<Uint8Array>, onDelta: (delta: string) => void): Promise<LlmTurnAnswer>;
|
|
82
|
+
/**
|
|
83
|
+
* The ceiling wins over the calls that did arrive. A turn the provider cut short is one whose last call may be
|
|
84
|
+
* missing, so running the batch it did finish is acting on half an intention.
|
|
85
|
+
*/
|
|
86
|
+
static stopOf(finish: string | null | undefined, calls: number): LlmTurnAnswer["stop"];
|
|
82
87
|
static turnAnswer(answer: OpenaiAnswer): LlmTurnAnswer;
|
|
83
88
|
/** The provider sends arguments as a JSON string; an unparsable one becomes an empty call rather than a crash. */
|
|
84
89
|
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`
|
|
@@ -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
|
|
@@ -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.
|