@vietor/agent-core 0.7.3 → 0.7.5
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/README.md +6 -3
- package/dist/create-session.js +2 -2
- package/dist/llm/anthropic.js +3 -2
- package/dist/llm/completions.js +2 -1
- package/dist/llm/responses.js +2 -1
- package/dist/llm/types.d.ts +1 -1
- package/dist/runtime/agent.d.ts +5 -2
- package/dist/runtime/agent.js +15 -6
- package/dist/runtime/events.d.ts +2 -1
- package/dist/runtime/events.js +1 -1
- package/dist/runtime/prompts.d.ts +1 -0
- package/dist/runtime/prompts.js +4 -1
- package/dist/runtime/session-messages.d.ts +1 -0
- package/dist/runtime/session-messages.js +3 -0
- package/dist/runtime/session.js +1 -0
- package/dist/runtime/sub-agent-runner.d.ts +1 -0
- package/dist/runtime/sub-agent-runner.js +6 -5
- package/dist/tools/sub-agent.js +10 -5
- package/dist/tools/todo-write.d.ts +1 -1
- package/dist/tools/todo-write.js +9 -2
- package/dist/util/net.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,8 +46,10 @@ const session = await createSession({
|
|
|
46
46
|
|
|
47
47
|
session.onEvent((e) => {
|
|
48
48
|
if (e.type === "assistant_delta") process.stdout.write(e.text);
|
|
49
|
-
else if (e.type === "run_metrics")
|
|
50
|
-
|
|
49
|
+
else if (e.type === "run_metrics") {
|
|
50
|
+
const inputTokens = e.cacheInputTokens + e.missInputTokens;
|
|
51
|
+
console.log(`tokens: ${inputTokens} input / ${e.outputTokens} output`);
|
|
52
|
+
}
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
const result = await session.prompt("What files are in the current directory?");
|
|
@@ -219,7 +221,8 @@ interface RunMetrics {
|
|
|
219
221
|
elapsed: number; // seconds since the current prompt started
|
|
220
222
|
thinkingElapsed: number; // seconds before the first assistant text token (incl. thinking/tools)
|
|
221
223
|
replyElapsed: number; // seconds after the first assistant text token (incl. later tool rounds)
|
|
222
|
-
|
|
224
|
+
cacheInputTokens: number; // cumulative cached input (prompt) tokens for the current run
|
|
225
|
+
missInputTokens: number; // cumulative non-cached input (prompt) tokens for the current run
|
|
223
226
|
outputTokens: number; // cumulative output (completion) tokens for the current run
|
|
224
227
|
}
|
|
225
228
|
```
|
package/dist/create-session.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createLLM } from "./llm/client.js";
|
|
|
2
2
|
import { Session } from "./runtime/session.js";
|
|
3
3
|
import { ToolRegistry } from "./tools/registry.js";
|
|
4
4
|
import { MCPServerManager } from "./mcp/manager.js";
|
|
5
|
-
import {
|
|
5
|
+
import { renderToolUsePrompt } from "./runtime/prompts.js";
|
|
6
6
|
import { DEFAULT_MAX_TURNS } from "./util/constants.js";
|
|
7
7
|
import { TODO_WRITE_GUIDANCE } from "./tools/todo-write.js";
|
|
8
8
|
import { ASK_USER_GUIDANCE } from "./tools/ask-user.js";
|
|
@@ -13,7 +13,7 @@ function contextLimitFor(maxInputTokens) {
|
|
|
13
13
|
}
|
|
14
14
|
function buildSystemPrompt(base, skills, builtInTools, maxTurns) {
|
|
15
15
|
const parts = [base];
|
|
16
|
-
const toolUseLines = [
|
|
16
|
+
const toolUseLines = [renderToolUsePrompt(maxTurns)];
|
|
17
17
|
if (typeof builtInTools === "object") {
|
|
18
18
|
if (builtInTools.todoWrite)
|
|
19
19
|
toolUseLines.push(TODO_WRITE_GUIDANCE);
|
package/dist/llm/anthropic.js
CHANGED
|
@@ -44,7 +44,7 @@ export class AnthropicAdapter extends BaseAdapter {
|
|
|
44
44
|
const stream = this.client.messages.stream(params, { signal: opts.signal });
|
|
45
45
|
if (opts.onUsage)
|
|
46
46
|
stream.on("streamEvent", (e) => { if (e.type === "message_start")
|
|
47
|
-
opts.onUsage(e.message.usage.input_tokens, 0); });
|
|
47
|
+
opts.onUsage(0, e.message.usage.input_tokens, 0); });
|
|
48
48
|
if (opts.onDelta)
|
|
49
49
|
stream.on("text", (delta) => opts.onDelta(delta));
|
|
50
50
|
if (opts.onThinking)
|
|
@@ -53,7 +53,8 @@ export class AnthropicAdapter extends BaseAdapter {
|
|
|
53
53
|
stream.on("contentBlock", (block) => { if (block.type === "tool_use")
|
|
54
54
|
opts.onToolCall(); });
|
|
55
55
|
const final = await stream.finalMessage();
|
|
56
|
-
|
|
56
|
+
const cacheTokens = (final.usage.cache_read_input_tokens ?? 0) + (final.usage.cache_creation_input_tokens ?? 0);
|
|
57
|
+
opts.onUsage?.(cacheTokens, final.usage.input_tokens, final.usage.output_tokens);
|
|
57
58
|
const thinking = [];
|
|
58
59
|
let text = "";
|
|
59
60
|
const toolCalls = [];
|
package/dist/llm/completions.js
CHANGED
|
@@ -31,7 +31,8 @@ export class CompletionsAdapter extends BaseAdapter {
|
|
|
31
31
|
const stream = await this.client.chat.completions.create(params, { signal });
|
|
32
32
|
for await (const chunk of stream) {
|
|
33
33
|
if (chunk.usage) {
|
|
34
|
-
|
|
34
|
+
const cached = chunk.usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
35
|
+
onUsage?.(cached, chunk.usage.prompt_tokens ?? 0, chunk.usage.completion_tokens ?? 0);
|
|
35
36
|
}
|
|
36
37
|
const delta = chunk.choices[0]?.delta;
|
|
37
38
|
if (!delta)
|
package/dist/llm/responses.js
CHANGED
|
@@ -59,7 +59,8 @@ export class ResponsesAdapter extends BaseAdapter {
|
|
|
59
59
|
throw new Error(`Responses API error: ${detail}`);
|
|
60
60
|
}
|
|
61
61
|
if (finalResponse.usage) {
|
|
62
|
-
|
|
62
|
+
const cacheTokens = finalResponse.usage.input_tokens_details.cached_tokens;
|
|
63
|
+
onUsage?.(cacheTokens, finalResponse.usage.input_tokens, finalResponse.usage.output_tokens);
|
|
63
64
|
}
|
|
64
65
|
const textParts = [];
|
|
65
66
|
const toolCalls = [];
|
package/dist/llm/types.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface ChatOptions {
|
|
|
23
23
|
onDelta?: (text: string) => void;
|
|
24
24
|
onThinking?: (text: string) => void;
|
|
25
25
|
onRetry?: (attempt: number, max: number, error: unknown) => void;
|
|
26
|
-
onUsage?: (
|
|
26
|
+
onUsage?: (cacheInputTokens: number, missInputTokens: number, outputTokens: number) => void;
|
|
27
27
|
onToolCall?: () => void;
|
|
28
28
|
thinking?: boolean;
|
|
29
29
|
signal?: AbortSignal;
|
package/dist/runtime/agent.d.ts
CHANGED
|
@@ -31,15 +31,18 @@ export declare class Agent {
|
|
|
31
31
|
private todoSnapshot;
|
|
32
32
|
private resolveSkill?;
|
|
33
33
|
private onCompact?;
|
|
34
|
-
private
|
|
34
|
+
private cacheInputTokens;
|
|
35
|
+
private missInputTokens;
|
|
35
36
|
private outputTokens;
|
|
36
37
|
constructor(opts: AgentOptions);
|
|
37
38
|
get contextTokens(): number;
|
|
38
39
|
get usage(): {
|
|
39
|
-
|
|
40
|
+
cacheInputTokens: number;
|
|
41
|
+
missInputTokens: number;
|
|
40
42
|
outputTokens: number;
|
|
41
43
|
};
|
|
42
44
|
resetUsage(): void;
|
|
45
|
+
addUsage(cacheInputTokens: number, missInputTokens: number, outputTokens: number): void;
|
|
43
46
|
get model(): string;
|
|
44
47
|
get thinkingEffort(): import("../llm/types.js").LLMThinkingEffort;
|
|
45
48
|
clear(): void;
|
package/dist/runtime/agent.js
CHANGED
|
@@ -18,7 +18,8 @@ export class Agent {
|
|
|
18
18
|
todoSnapshot = [];
|
|
19
19
|
resolveSkill;
|
|
20
20
|
onCompact;
|
|
21
|
-
|
|
21
|
+
cacheInputTokens = 0;
|
|
22
|
+
missInputTokens = 0;
|
|
22
23
|
outputTokens = 0;
|
|
23
24
|
constructor(opts) {
|
|
24
25
|
this.llm = opts.llm;
|
|
@@ -37,12 +38,18 @@ export class Agent {
|
|
|
37
38
|
return this.conversation.getEstimatedTokens();
|
|
38
39
|
}
|
|
39
40
|
get usage() {
|
|
40
|
-
return {
|
|
41
|
+
return { cacheInputTokens: this.cacheInputTokens, missInputTokens: this.missInputTokens, outputTokens: this.outputTokens };
|
|
41
42
|
}
|
|
42
43
|
resetUsage() {
|
|
43
|
-
this.
|
|
44
|
+
this.cacheInputTokens = 0;
|
|
45
|
+
this.missInputTokens = 0;
|
|
44
46
|
this.outputTokens = 0;
|
|
45
47
|
}
|
|
48
|
+
addUsage(cacheInputTokens, missInputTokens, outputTokens) {
|
|
49
|
+
this.cacheInputTokens += cacheInputTokens;
|
|
50
|
+
this.missInputTokens += missInputTokens;
|
|
51
|
+
this.outputTokens += outputTokens;
|
|
52
|
+
}
|
|
46
53
|
get model() {
|
|
47
54
|
return this.llm.model;
|
|
48
55
|
}
|
|
@@ -207,6 +214,7 @@ export class Agent {
|
|
|
207
214
|
}
|
|
208
215
|
async chatOnce(opts, onAbort) {
|
|
209
216
|
try {
|
|
217
|
+
let usage;
|
|
210
218
|
const message = await withAbort(this.llm.chat({
|
|
211
219
|
messages: opts.messages,
|
|
212
220
|
tools: opts.tools,
|
|
@@ -214,12 +222,13 @@ export class Agent {
|
|
|
214
222
|
onDelta: (text) => opts.onEvent?.({ type: "assistant_delta", text }),
|
|
215
223
|
onThinking: (text) => opts.onEvent?.({ type: "thinking_delta", text }),
|
|
216
224
|
onRetry: (attempt, max, error) => opts.onEvent?.({ type: "retry", attempt, max, reason: toErrorMessage(error) }),
|
|
217
|
-
onUsage: (
|
|
218
|
-
|
|
219
|
-
this.outputTokens = outputTokens;
|
|
225
|
+
onUsage: (cacheInputTokens, missInputTokens, outputTokens) => {
|
|
226
|
+
usage = { cacheInputTokens, missInputTokens, outputTokens };
|
|
220
227
|
},
|
|
221
228
|
signal: opts.signal,
|
|
222
229
|
}), opts.signal);
|
|
230
|
+
if (usage)
|
|
231
|
+
this.addUsage(usage.cacheInputTokens, usage.missInputTokens, usage.outputTokens);
|
|
223
232
|
return { ok: true, message };
|
|
224
233
|
}
|
|
225
234
|
catch (e) {
|
package/dist/runtime/events.d.ts
CHANGED
package/dist/runtime/events.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const INITIAL_RUN_METRICS = { running: false, elapsed: 0, thinkingElapsed: 0, replyElapsed: 0,
|
|
1
|
+
export const INITIAL_RUN_METRICS = { running: false, elapsed: 0, thinkingElapsed: 0, replyElapsed: 0, cacheInputTokens: 0, missInputTokens: 0, outputTokens: 0 };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Todo } from "../tools/types.js";
|
|
2
2
|
export declare const TOOL_USE_PROMPT: string;
|
|
3
|
+
export declare function renderToolUsePrompt(maxTurns: number): string;
|
|
3
4
|
export declare const COMPACT_PROMPT: string;
|
|
4
5
|
export declare function renderTodoReminder(todos: readonly Todo[]): string;
|
|
5
6
|
export declare function renderIncompleteTodoNudge(todos: readonly Todo[]): string;
|
package/dist/runtime/prompts.js
CHANGED
|
@@ -8,6 +8,9 @@ export const TOOL_USE_PROMPT = [
|
|
|
8
8
|
"- For file operations (read/write/edit/glob/grep) and fetching URLs, use the dedicated tool. Fall back to Shell only when no dedicated tool covers the task and Shell is available. A runtime error does not make Shell the fallback; do not retry that same operation through Shell.",
|
|
9
9
|
"- If a tool call fails, read the error, adjust the arguments or approach, and continue; do not repeat the identical call and do not abandon the task over a single failure.",
|
|
10
10
|
].join("\n");
|
|
11
|
+
export function renderToolUsePrompt(maxTurns) {
|
|
12
|
+
return [TOOL_USE_PROMPT, `- Turn budget: ${maxTurns} tool-calling turns per run.`].join("\n");
|
|
13
|
+
}
|
|
11
14
|
export const COMPACT_PROMPT = [
|
|
12
15
|
"Summarize the conversation above for context continuation. Preserve:\n",
|
|
13
16
|
"1. Primary goal, sub-goals, constraints, acceptance criteria.\n",
|
|
@@ -34,7 +37,7 @@ export function renderTodoReminder(todos) {
|
|
|
34
37
|
const focusLine = focus ? ` Current focus: ${focus.content}` : "";
|
|
35
38
|
const incomplete = todos.filter(t => t.status !== "completed");
|
|
36
39
|
const warning = incomplete.length > 0
|
|
37
|
-
? ` ${incomplete.length} incomplete. You MUST complete EVERY task before your final text-only response
|
|
40
|
+
? ` ${incomplete.length} incomplete. You MUST complete EVERY task before your final text-only response. Mark them complete via TodoWrite as they finish; the final update may go in the same turn as your last tool call.`
|
|
38
41
|
: "";
|
|
39
42
|
return `<system-reminder>Tasks: ${items.join(" | ")}${focusLine}${warning}</system-reminder>`;
|
|
40
43
|
}
|
|
@@ -30,6 +30,7 @@ export declare class SessionMessages {
|
|
|
30
30
|
add(msg: SessionMessage): void;
|
|
31
31
|
toLLM(): LLMMessage[];
|
|
32
32
|
export(): SessionMessage[];
|
|
33
|
+
lastAssistantText(): string;
|
|
33
34
|
import(messages: SessionMessage[]): void;
|
|
34
35
|
normalizeInterruptedToolCalls(): void;
|
|
35
36
|
clear(): void;
|
|
@@ -86,6 +86,9 @@ export class SessionMessages {
|
|
|
86
86
|
export() {
|
|
87
87
|
return this.messages.slice();
|
|
88
88
|
}
|
|
89
|
+
lastAssistantText() {
|
|
90
|
+
return lastAssistantText(this.messages);
|
|
91
|
+
}
|
|
89
92
|
import(messages) {
|
|
90
93
|
this.resetMessages(messages.slice(), messages.reduce((sum, m) => sum + estimateTokens(messageText(m)), 0));
|
|
91
94
|
this.normalizeInterruptedToolCalls();
|
package/dist/runtime/session.js
CHANGED
|
@@ -211,6 +211,7 @@ export class Session {
|
|
|
211
211
|
maxTurns: deps.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
212
212
|
stallThreshold: deps.stallThreshold ?? DEFAULT_STALL_THRESHOLD,
|
|
213
213
|
contextLimit: deps.contextLimit,
|
|
214
|
+
onUsage: (cacheInputTokens, missInputTokens, outputTokens) => this.agent.addUsage(cacheInputTokens, missInputTokens, outputTokens),
|
|
214
215
|
})(systemPrompt, task, signal),
|
|
215
216
|
},
|
|
216
217
|
});
|
|
@@ -9,6 +9,7 @@ export interface SubAgentRunOptions {
|
|
|
9
9
|
maxTurns: number;
|
|
10
10
|
stallThreshold: number;
|
|
11
11
|
contextLimit: number;
|
|
12
|
+
onUsage?: (cacheInputTokens: number, missInputTokens: number, outputTokens: number) => void;
|
|
12
13
|
}
|
|
13
14
|
export interface SubAgentRunResult {
|
|
14
15
|
status: RunStatus;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { SessionMessages
|
|
1
|
+
import { SessionMessages } from "./session-messages.js";
|
|
2
2
|
import { Agent } from "./agent.js";
|
|
3
|
-
import {
|
|
3
|
+
import { renderToolUsePrompt } from "./prompts.js";
|
|
4
4
|
import { ToolRegistry } from "../tools/registry.js";
|
|
5
5
|
export function createSubAgentRunner(opts) {
|
|
6
6
|
return async (systemPrompt, task, signal) => {
|
|
7
|
-
const conversation = new SessionMessages([systemPrompt,
|
|
7
|
+
const conversation = new SessionMessages([systemPrompt, renderToolUsePrompt(opts.maxTurns)].join("\n\n"));
|
|
8
8
|
const subTools = new ToolRegistry();
|
|
9
9
|
subTools.registerAll(opts.tools.filter((t) => t.readOnly === true));
|
|
10
10
|
const subAgent = new Agent({
|
|
@@ -19,8 +19,9 @@ export function createSubAgentRunner(opts) {
|
|
|
19
19
|
contextLimit: opts.contextLimit,
|
|
20
20
|
});
|
|
21
21
|
const status = await subAgent.run(task, undefined, signal);
|
|
22
|
-
|
|
23
|
-
const reply = lastAssistantText(
|
|
22
|
+
opts.onUsage?.(subAgent.usage.cacheInputTokens, subAgent.usage.missInputTokens, subAgent.usage.outputTokens);
|
|
23
|
+
const reply = conversation.lastAssistantText() || `(sub-agent produced no final text; status ${status})`;
|
|
24
|
+
const messages = status !== "ok" ? conversation.export() : [];
|
|
24
25
|
return { status, reply, messages };
|
|
25
26
|
};
|
|
26
27
|
}
|
package/dist/tools/sub-agent.js
CHANGED
|
@@ -69,11 +69,16 @@ export function createSubAgentTool(deps) {
|
|
|
69
69
|
const { status, reply, messages } = await deps.runSubAgent(def.systemPrompt, task, ctx.signal);
|
|
70
70
|
if (status === "ok")
|
|
71
71
|
return { content: reply };
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
let stallReason;
|
|
73
|
+
if (status === "stalled") {
|
|
74
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
75
|
+
const content = messages[i].content;
|
|
76
|
+
if (typeof content === "string" && content.startsWith(NOT_EXECUTED_PREFIX)) {
|
|
77
|
+
stallReason = content;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
77
82
|
const suffix = stallReason ? ` ${stallReason}` : "";
|
|
78
83
|
return { content: `Sub-agent "${type}" ended with status ${status}.${suffix}\n\n${reply}`, isError: true };
|
|
79
84
|
},
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Tool, Todo } from "./types.js";
|
|
2
|
-
export declare const TODO_WRITE_GUIDANCE = "- For multi-step tasks (
|
|
2
|
+
export declare const TODO_WRITE_GUIDANCE = "- For multi-step tasks (5+ steps), you MUST use TodoWrite: create the task list first, then update statuses as tasks complete. Never execute a 5+ step task without a TodoWrite task list.";
|
|
3
3
|
export declare function createTodoWriteTool(setTodos: (todos: Todo[]) => void): Tool;
|
package/dist/tools/todo-write.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { toolError } from "./types.js";
|
|
2
|
-
export const TODO_WRITE_GUIDANCE = "- For multi-step tasks (
|
|
2
|
+
export const TODO_WRITE_GUIDANCE = "- For multi-step tasks (5+ steps), you MUST use TodoWrite: create the task list first, then update statuses as tasks complete. Never execute a 5+ step task without a TodoWrite task list.";
|
|
3
3
|
const STATUSES = ["pending", "inProgress", "completed"];
|
|
4
|
-
const DESCRIPTION = "Manage the task list for tasks with
|
|
4
|
+
const DESCRIPTION = "Manage the task list for tasks with 5+ steps. Pass the FULL list each call; it replaces the previous list. Keep one inProgress at a time. status: pending, inProgress, completed.";
|
|
5
5
|
function parseTodos(args) {
|
|
6
6
|
const raw = args.todos;
|
|
7
7
|
if (!Array.isArray(raw)) {
|
|
@@ -32,6 +32,13 @@ function parseTodos(args) {
|
|
|
32
32
|
}
|
|
33
33
|
todos.push({ content, status });
|
|
34
34
|
}
|
|
35
|
+
if (!seenInProgress) {
|
|
36
|
+
const firstPending = todos.find((t) => t.status === "pending");
|
|
37
|
+
if (firstPending) {
|
|
38
|
+
firstPending.status = "inProgress";
|
|
39
|
+
normalized++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
35
42
|
return { todos, done, normalized };
|
|
36
43
|
}
|
|
37
44
|
export function createTodoWriteTool(setTodos) {
|
package/dist/util/net.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProxyAgent } from "undici";
|
|
1
|
+
import { fetch, ProxyAgent } from "undici";
|
|
2
2
|
let proxyAgent;
|
|
3
3
|
let noProxyList;
|
|
4
4
|
let proxyInitialized = false;
|
|
@@ -11,11 +11,12 @@ function ensureProxy() {
|
|
|
11
11
|
proxyAgent = proxyUrl ? new ProxyAgent(proxyUrl) : null;
|
|
12
12
|
noProxyList = noProxy?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13
13
|
}
|
|
14
|
+
const undiciFetch = fetch;
|
|
14
15
|
export async function netFetch(input, init) {
|
|
15
16
|
if (!proxyInitialized)
|
|
16
17
|
ensureProxy();
|
|
17
18
|
if (!proxyAgent)
|
|
18
|
-
return
|
|
19
|
+
return undiciFetch(input, init);
|
|
19
20
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
20
21
|
if (noProxyList) {
|
|
21
22
|
const hostname = new URL(url).hostname;
|
|
@@ -25,7 +26,7 @@ export async function netFetch(input, init) {
|
|
|
25
26
|
return hostname === p || hostname.endsWith("." + p);
|
|
26
27
|
});
|
|
27
28
|
if (shouldBypass)
|
|
28
|
-
return
|
|
29
|
+
return undiciFetch(input, init);
|
|
29
30
|
}
|
|
30
|
-
return
|
|
31
|
+
return undiciFetch(input, { ...init, dispatcher: proxyAgent });
|
|
31
32
|
}
|