@vietor/agent-core 0.7.4 → 0.7.6
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/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/session.js +1 -0
- package/dist/runtime/sub-agent-runner.d.ts +1 -0
- package/dist/runtime/sub-agent-runner.js +1 -0
- package/dist/tools/glob.js +3 -3
- package/dist/tools/grep.js +4 -4
- package/dist/tools/sub-agent.js +14 -2
- package/dist/util/async.js +1 -1
- package/dist/util/file.d.ts +4 -1
- package/dist/util/file.js +7 -3
- 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/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 };
|
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;
|
|
@@ -19,6 +19,7 @@ export function createSubAgentRunner(opts) {
|
|
|
19
19
|
contextLimit: opts.contextLimit,
|
|
20
20
|
});
|
|
21
21
|
const status = await subAgent.run(task, undefined, signal);
|
|
22
|
+
opts.onUsage?.(subAgent.usage.cacheInputTokens, subAgent.usage.missInputTokens, subAgent.usage.outputTokens);
|
|
22
23
|
const reply = conversation.lastAssistantText() || `(sub-agent produced no final text; status ${status})`;
|
|
23
24
|
const messages = status !== "ok" ? conversation.export() : [];
|
|
24
25
|
return { status, reply, messages };
|
package/dist/tools/glob.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatRipgrepOutput, ripgrepResultSummary, runRipgrepLines } from "../util/ripgrep.js";
|
|
2
2
|
import { NO_MATCHES } from "../util/constants.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveSearchPath } from "../util/file.js";
|
|
4
4
|
const DESCRIPTION = "List files under a directory, optionally filtered by a glob pattern (e.g. **/*.ts). Skips node_modules and .git.";
|
|
5
5
|
export const globTool = {
|
|
6
6
|
name: "Glob",
|
|
@@ -15,12 +15,12 @@ export const globTool = {
|
|
|
15
15
|
required: [],
|
|
16
16
|
},
|
|
17
17
|
async execute(args, ctx) {
|
|
18
|
-
const cwd =
|
|
18
|
+
const { cwd, target } = resolveSearchPath(args, ctx.cwd);
|
|
19
19
|
const rgArgs = ["--files"];
|
|
20
20
|
const pattern = args.pattern;
|
|
21
21
|
if (pattern)
|
|
22
22
|
rgArgs.push("-g", pattern);
|
|
23
|
-
rgArgs.push(
|
|
23
|
+
rgArgs.push(target);
|
|
24
24
|
const { lines, truncated } = await runRipgrepLines(rgArgs, cwd, ctx.signal);
|
|
25
25
|
return { content: formatRipgrepOutput(lines, truncated, NO_MATCHES) };
|
|
26
26
|
},
|
package/dist/tools/grep.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatRipgrepOutput, ripgrepResultSummary, runRipgrepLines } from "../util/ripgrep.js";
|
|
2
2
|
import { DEFAULT_GREP_LIMIT, NO_MATCHES } from "../util/constants.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveSearchPath } from "../util/file.js";
|
|
4
4
|
const DESCRIPTION = `Search file contents recursively for a regex pattern (RE2 syntax). Skips node_modules and .git. Returns path:line:content, capped at ${DEFAULT_GREP_LIMIT} lines. For large codebases, use output_mode=files_with_matches first, or narrow with glob/type, or raise head_limit.`;
|
|
5
5
|
export const grepTool = {
|
|
6
6
|
name: "Grep",
|
|
@@ -10,7 +10,7 @@ export const grepTool = {
|
|
|
10
10
|
type: "object",
|
|
11
11
|
properties: {
|
|
12
12
|
pattern: { type: "string" },
|
|
13
|
-
path: { type: "string", description: "
|
|
13
|
+
path: { type: "string", description: "file or directory, defaults to cwd" },
|
|
14
14
|
glob: { type: "string", description: "filter files, e.g. *.ts" },
|
|
15
15
|
type: { type: "string", description: "file type, e.g. ts, js, py" },
|
|
16
16
|
output_mode: { type: "string", enum: ["content", "files_with_matches", "count"], description: "defaults to content" },
|
|
@@ -25,7 +25,7 @@ export const grepTool = {
|
|
|
25
25
|
required: ["pattern"],
|
|
26
26
|
},
|
|
27
27
|
async execute(args, ctx) {
|
|
28
|
-
const cwd =
|
|
28
|
+
const { cwd, target } = resolveSearchPath(args, ctx.cwd);
|
|
29
29
|
const rgArgs = ["--line-number", "--with-filename", "--no-heading"];
|
|
30
30
|
if (args.ignore_case)
|
|
31
31
|
rgArgs.push("-i");
|
|
@@ -56,7 +56,7 @@ export const grepTool = {
|
|
|
56
56
|
rgArgs.push("-c");
|
|
57
57
|
else
|
|
58
58
|
rgArgs.push("-m", String(headLimit));
|
|
59
|
-
rgArgs.push("--", args.pattern,
|
|
59
|
+
rgArgs.push("--", args.pattern, target);
|
|
60
60
|
const { lines, truncated } = await runRipgrepLines(rgArgs, cwd, ctx.signal, headLimit);
|
|
61
61
|
return { content: formatRipgrepOutput(lines, truncated, NO_MATCHES) };
|
|
62
62
|
},
|
package/dist/tools/sub-agent.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { NOT_EXECUTED_PREFIX } from "../util/constants.js";
|
|
2
|
+
import { summarizeText } from "../util/text.js";
|
|
2
3
|
import { toolError } from "./types.js";
|
|
4
|
+
const MAX_LABEL_LENGTH = 50;
|
|
3
5
|
export const SUB_AGENT_GUIDANCE = '- Consider delegating to the SubAgent tool when the task matches an agent type, when you have independent work to run in parallel, or when answering would mean reading across several files — delegate and keep the conclusion, not the file dumps. type: "explore" — read-only search agent for broad fan-out searches (state the search breadth in the task); type: "plan" — software architect producing implementation plans. For a single-fact lookup where you already know the file, symbol, or value, search directly. Once you have delegated a search, do not also run it yourself — wait for the result. Issue at most 2 SubAgent calls per turn; multiple calls in the same turn run concurrently. Sub-agents are read-only and return only their final report, not intermediate steps — verify important results yourself. For large workloads with many independent items that would exceed the turn budget, split the items into chunks sized so each sub-agent can complete its chunk within its own loop budget, delegate one SubAgent per chunk, and run the remaining chunks in the following turns as results return. Instruct each sub-agent to report results per item in structured lines so you can consolidate.';
|
|
4
6
|
const EXPLORE_PROMPT = [
|
|
5
7
|
"You are the Explore sub-agent — a read-only search agent for broad fan-out searches. Use it when answering means sweeping many files, directories, or naming conventions and the parent needs only the conclusion, not the file dumps. You read excerpts rather than whole files, so you locate code — you do not review or audit it. You are read-only: you must not modify any files.",
|
|
@@ -51,11 +53,21 @@ export function createSubAgentTool(deps) {
|
|
|
51
53
|
enum: SUB_AGENT_DEFS.map((d) => d.type),
|
|
52
54
|
description: 'The sub-agent type to invoke: "explore" (read-only fan-out search) or "plan" (implementation plan).',
|
|
53
55
|
},
|
|
56
|
+
label: {
|
|
57
|
+
type: "string",
|
|
58
|
+
maxLength: MAX_LABEL_LENGTH,
|
|
59
|
+
description: `Short label (max ${MAX_LABEL_LENGTH} characters) for this sub-agent run, shown in the UI.`,
|
|
60
|
+
},
|
|
54
61
|
task: { type: "string", description: "The task or question for the sub-agent, as a self-contained description." },
|
|
55
62
|
},
|
|
56
63
|
required: ["type", "task"],
|
|
57
64
|
},
|
|
58
|
-
|
|
65
|
+
summarizeArgs: (args) => {
|
|
66
|
+
const type = args.type;
|
|
67
|
+
const label = typeof args.label === "string" ? summarizeText(args.label, MAX_LABEL_LENGTH) : "";
|
|
68
|
+
const def = SUB_AGENT_DEFS.find((d) => d.type === type);
|
|
69
|
+
return (def?.name || type) + (label ? ` ${label}` : "");
|
|
70
|
+
},
|
|
59
71
|
async execute(args, ctx) {
|
|
60
72
|
const type = args.type;
|
|
61
73
|
const task = (args.task ?? "").trim();
|
|
@@ -80,7 +92,7 @@ export function createSubAgentTool(deps) {
|
|
|
80
92
|
}
|
|
81
93
|
}
|
|
82
94
|
const suffix = stallReason ? ` ${stallReason}` : "";
|
|
83
|
-
return { content: `Sub-agent "${
|
|
95
|
+
return { content: `Sub-agent "${def.name}" ended with status ${status}.${suffix}\n\n${reply}`, isError: true };
|
|
84
96
|
},
|
|
85
97
|
};
|
|
86
98
|
}
|
package/dist/util/async.js
CHANGED
|
@@ -11,7 +11,7 @@ export function isAbortError(e) {
|
|
|
11
11
|
return name === "AbortError" || name === "APIUserAbortError";
|
|
12
12
|
}
|
|
13
13
|
export function backoffDelay(attempt) {
|
|
14
|
-
return
|
|
14
|
+
return Math.min(2000 * 2 ** attempt, 60_000);
|
|
15
15
|
}
|
|
16
16
|
export async function withRetry(fn, opts) {
|
|
17
17
|
for (let attempt = 0;; attempt++) {
|
package/dist/util/file.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export declare function tryReadFileText(path: string): string | undefined;
|
|
2
2
|
export declare function resolveRequiredPath(args: Record<string, unknown>, cwd: string): string;
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function resolveSearchPath(args: Record<string, unknown>, cwd: string): {
|
|
4
|
+
cwd: string;
|
|
5
|
+
target: string;
|
|
6
|
+
};
|
package/dist/util/file.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
export function tryReadFileText(path) {
|
|
4
4
|
if (existsSync(path)) {
|
|
@@ -14,6 +14,10 @@ export function resolveRequiredPath(args, cwd) {
|
|
|
14
14
|
throw new Error("path is required");
|
|
15
15
|
return resolve(cwd, path);
|
|
16
16
|
}
|
|
17
|
-
export function
|
|
18
|
-
|
|
17
|
+
export function resolveSearchPath(args, cwd) {
|
|
18
|
+
const path = resolve(cwd, args.path || "");
|
|
19
|
+
if (existsSync(path) && !statSync(path).isDirectory()) {
|
|
20
|
+
return { cwd, target: path };
|
|
21
|
+
}
|
|
22
|
+
return { cwd: path, target: "." };
|
|
19
23
|
}
|