Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stello-ai/core",
"version": "0.10.0",
"version": "0.10.1",
"description": "The first open-source conversation topology engine",
"license": "Apache-2.0",
"author": "Stello Contributors",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('session-runtime adapters', () => {
const parsed = sessionSendResultParser.parse(raw);

expect(parsed.content).toBe('done');
expect(parsed.usage).toEqual({ promptTokens: 10, completionTokens: 5 });
expect(parsed.toolCalls).toEqual([
{ id: 't1', name: 'read_file', args: { path: 'a.ts' } },
]);
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/adapters/session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,13 @@ export const sessionSendResultParser: ToolCallParser = {
name: string;
args: Record<string, unknown>;
}>;
usage?: SessionCompatibleSendResult['usage'];
};

return {
content: parsed.content,
toolCalls: parsed.toolCalls ?? [],
usage: parsed.usage,
};
},
};
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/engine/__tests__/turn-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,40 @@ describe('TurnRunner', () => {
expect(result.toolCallsExecuted).toBe(1);
});

it('聚合 tool loop 内每次 LLM 调用的 usage', async () => {
const session = {
id: 's1',
send: vi
.fn()
.mockResolvedValueOnce(
JSON.stringify({
content: null,
toolCalls: [{ id: '1', name: 'read', args: {} }],
usage: { promptTokens: 10, completionTokens: 2 },
}),
)
.mockResolvedValueOnce(
JSON.stringify({
content: 'done',
toolCalls: [],
usage: { promptTokens: 8, completionTokens: 4 },
}),
),
};
const tools = {
executeTool: vi.fn().mockResolvedValue({ success: true, data: { ok: true } }),
};

const runner = new TurnRunner(parser);
const result = await runner.run(session, 'hello', tools);

expect(result.usage).toEqual({
promptTokens: 18,
completionTokens: 6,
totalTokens: 24,
});
});

it('多个 tool call 在同轮内并行执行,但调用顺序保持输入序', async () => {
const session = {
id: 's1',
Expand Down
32 changes: 32 additions & 0 deletions packages/core/src/engine/turn-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,30 @@ export interface ParsedTurnResponse {
content: string | null;
/** 需要由 Engine 执行的工具调用 */
toolCalls: ToolCall[];
/** 本次 LLM 调用的 token 用量 */
usage?: TurnRunnerUsage;
}

/** 单次或聚合后的 LLM token 用量 */
export interface TurnRunnerUsage {
promptTokens?: number;
completionTokens?: number;
totalTokens?: number;
}

function addOptionalNumbers(a: number | undefined, b: number | undefined): number | undefined {
return a === undefined && b === undefined ? undefined : (a ?? 0) + (b ?? 0);
}

function addUsage(current: TurnRunnerUsage | undefined, next: TurnRunnerUsage | undefined): TurnRunnerUsage | undefined {
if (!next) return current;
const currentTotal = current?.totalTokens ?? ((current?.promptTokens ?? 0) + (current?.completionTokens ?? 0));
const nextTotal = next.totalTokens ?? ((next.promptTokens ?? 0) + (next.completionTokens ?? 0));
return {
promptTokens: addOptionalNumbers(current?.promptTokens, next.promptTokens),
completionTokens: addOptionalNumbers(current?.completionTokens, next.completionTokens),
totalTokens: currentTotal + nextTotal,
};
}

/** Session 调用的运行时选项 */
Expand Down Expand Up @@ -160,6 +184,8 @@ export interface TurnRunnerResult {
toolCallsExecuted: number;
/** 原始最终响应 */
rawResponse: string;
/** 本轮内所有 LLM 调用聚合后的 token 用量 */
usage?: TurnRunnerUsage;
}

/** 流式 tool loop 的执行结果 */
Expand Down Expand Up @@ -197,18 +223,21 @@ export class TurnRunner {
let toolRoundCount = 0;
let toolCallsExecuted = 0;
let lastRawResponse = '';
let usage: TurnRunnerUsage | undefined;

while (true) {
options.signal?.throwIfAborted();
lastRawResponse = await session.send(currentInput, { signal: options.signal });
const parsed = this.parser.parse(lastRawResponse);
usage = addUsage(usage, parsed.usage);

if (parsed.toolCalls.length === 0) {
return {
finalContent: parsed.content,
toolRoundCount,
toolCallsExecuted,
rawResponse: lastRawResponse,
usage,
};
}

Expand Down Expand Up @@ -290,6 +319,7 @@ export class TurnRunner {
let lastRawResponse = await rawResult
options.signal?.throwIfAborted()
let parsed = this.parser.parse(lastRawResponse)
let usage = addUsage(undefined, parsed.usage)

while (parsed.toolCalls.length > 0) {
if (toolRoundCount >= maxToolRounds) {
Expand All @@ -303,13 +333,15 @@ export class TurnRunner {
options.signal?.throwIfAborted()
lastRawResponse = await session.send(JSON.stringify({ toolResults }), { signal: options.signal })
parsed = this.parser.parse(lastRawResponse)
usage = addUsage(usage, parsed.usage)
}

return {
finalContent: parsed.content,
toolRoundCount,
toolCallsExecuted,
rawResponse: lastRawResponse,
usage,
}
}
}