@keo-ai/axiom 0.1.8 → 0.2.0

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 CHANGED
@@ -297,8 +297,12 @@ for await (const chunk of stream) {
297
297
  case 'turn_start':
298
298
  console.log(`\n--- Turn ${chunk.turn} ---`);
299
299
  break;
300
+ case 'reasoning':
301
+ // 实时流式输出模型的 reasoning content(思考过程)
302
+ process.stdout.write(chunk.delta);
303
+ break;
300
304
  case 'content':
301
- // 实时流式输出 content(支持 reasoning 内容)
305
+ // 实时流式输出 content(正式回复内容)
302
306
  process.stdout.write(chunk.delta);
303
307
  break;
304
308
  case 'tool_call':
@@ -341,7 +345,8 @@ console.log('Turns:', result!.turns);
341
345
  | 事件 | 触发时机 | 包含字段 |
342
346
  |---|---|---|
343
347
  | `turn_start` | 新一轮开始 | `turn` |
344
- | `content` | LLM 输出 content 增量 | `delta`, `turn` |
348
+ | `reasoning` | LLM 输出 reasoning content(推理过程)增量 | `delta`, `turn` |
349
+ | `content` | LLM 输出 content(正式回复)增量 | `delta`, `turn` |
345
350
  | `tool_call` | LLM 决定调用 tool(流结束、完整 tool_calls 解析完成) | `toolCalls`, `turn` |
346
351
  | `tool_result` | tool 执行完成 | `callId`, `toolName`, `content`, `status`, `turn` |
347
352
  | `turn_end` | 一轮结束(tool 全部执行完或 content 直接返回) | `turn` |
@@ -351,7 +356,7 @@ console.log('Turns:', result!.turns);
351
356
  | 场景 | 推荐方式 |
352
357
  |---|---|
353
358
  | 需要实时展示 LLM 输出(打字机效果) | `runLoopStream` |
354
- | 需要展示模型 reasoning 过程 | `runLoopStream`(content 增量中包含 reasoning |
359
+ | 需要实时展示模型 reasoning(思考)过程 | `runLoopStream`(通过 `reasoning` 事件独立透出) |
355
360
  | 后台静默执行,只关心最终结果 | `runLoop` |
356
361
  | 低延迟、简单场景 | `runLoop` |
357
362
 
@@ -406,6 +411,42 @@ console.log(result.harness);
406
411
 
407
412
  **并行隔离原则**:本轮并行执行的多个 tool,各自收到的 Harness 是"本轮并行开始前"的快照,互相看不到同轮其他正在执行的 tool。
408
413
 
414
+ ### Tool 结果分流(ToolResult)
415
+
416
+ 当 tool 返回大量结构化数据时,直接 `JSON.stringify` 给 LLM 会导致模型重复复述。此时可以用 `ToolResult` 把结果分成两份:
417
+
418
+ ```ts
419
+ const recommendTool = {
420
+ name: 'recommend_conferences',
421
+ description: 'Recommend academic conferences',
422
+ parameters: { /* ... */ },
423
+ execute: async (args) => {
424
+ const raw = await recommendConferences(args);
425
+ return {
426
+ forLLM: `找到 ${raw.results.length} 个相关会议:${raw.results.map(r => r.name).join('、')}`,
427
+ forFrontend: raw,
428
+ };
429
+ },
430
+ };
431
+ ```
432
+
433
+ | 字段 | 去向 | 说明 |
434
+ |---|---|---|
435
+ | `forLLM` | `messages[]` → LLM 上下文 | 精简摘要,建议用自然语言 |
436
+ | `forFrontend` | `tool_result` SSE 事件 | 完整原始数据,前端渲染用 |
437
+
438
+ 流式消费时前端可以拿到完整数据:
439
+
440
+ ```ts
441
+ for await (const chunk of stream) {
442
+ if (chunk.type === 'tool_result' && chunk.frontendData) {
443
+ renderCard(chunk.frontendData); // 前端拿到完整 JSON 渲染卡片
444
+ }
445
+ }
446
+ ```
447
+
448
+ **向后兼容**:返回普通值(非 `ToolResult`)时行为完全不变,仍按原有逻辑 `JSON.stringify` 后透给 LLM。
449
+
409
450
  ### Tool 审批拦截
410
451
 
411
452
  对于敏感操作(转账、删除数据等),可以给 tool 配置 `approval`,Loop 会在执行前暂停并返回 `pendingApproval`:
@@ -640,6 +681,19 @@ interface LoopResult {
640
681
  }
641
682
  ```
642
683
 
684
+ **Message 中的 reasoningContent**:当使用支持推理的模型(如 DeepSeek-R1、QwQ)时,`messages` 中 `role: 'assistant'` 的条目会包含 `reasoningContent` 字段,记录该轮模型的完整推理过程。该字段会自动保留在对话上下文中,供多轮对话回传。
685
+
686
+ ```ts
687
+ const result = await FunctionCallLoop.runLoop(config);
688
+
689
+ for (const msg of result.messages) {
690
+ if (msg.role === 'assistant' && msg.reasoningContent) {
691
+ console.log('[Reasoning]', msg.reasoningContent);
692
+ console.log('[Content]', msg.content);
693
+ }
694
+ }
695
+ ```
696
+
643
697
  ### 错误处理
644
698
 
645
699
  同其他模块:**直接抛异常**。常见场景:
@@ -21,6 +21,7 @@ export declare function compressMessages(config: LoopConfig, messages: Message[]
21
21
  export type ExecutionResult = {
22
22
  toolCallId: string;
23
23
  content: string;
24
+ frontendData?: unknown;
24
25
  record: HarnessRecord;
25
26
  events: LoopEvent[];
26
27
  pendingApproval?: {
@@ -92,6 +92,7 @@ function runLoop(config) {
92
92
  messages.push({
93
93
  role: 'assistant',
94
94
  content: (_f = llmResponse.content) !== null && _f !== void 0 ? _f : '',
95
+ reasoningContent: llmResponse.reasoningContent,
95
96
  });
96
97
  return {
97
98
  messages,
@@ -104,6 +105,7 @@ function runLoop(config) {
104
105
  messages.push({
105
106
  role: 'assistant',
106
107
  content: (_g = llmResponse.content) !== null && _g !== void 0 ? _g : '',
108
+ reasoningContent: llmResponse.reasoningContent,
107
109
  tool_calls: llmResponse.tool_calls,
108
110
  });
109
111
  // 第七步:并行执行 tool
@@ -261,6 +263,12 @@ function compressMessages(config, messages, _turn) {
261
263
  }
262
264
  });
263
265
  }
266
+ function isToolResult(value) {
267
+ return (typeof value === 'object' &&
268
+ value !== null &&
269
+ 'forLLM' in value &&
270
+ typeof value.forLLM === 'string');
271
+ }
264
272
  /**
265
273
  * 并行执行 tool。
266
274
  */
@@ -365,14 +373,27 @@ function executeToolsInParallel(config, harness, toolCalls, visibleToolNames, ha
365
373
  error: finalRecord.error,
366
374
  turn,
367
375
  };
368
- const content = status === 'success'
369
- ? typeof result === 'object' && result !== null
370
- ? JSON.stringify(result)
371
- : String(result !== null && result !== void 0 ? result : '')
372
- : error !== null && error !== void 0 ? error : 'Execution failed';
376
+ let content;
377
+ let frontendData;
378
+ if (status === 'success') {
379
+ if (isToolResult(result)) {
380
+ content = result.forLLM;
381
+ frontendData = result.forFrontend;
382
+ }
383
+ else {
384
+ content =
385
+ typeof result === 'object' && result !== null
386
+ ? JSON.stringify(result)
387
+ : String(result !== null && result !== void 0 ? result : '');
388
+ }
389
+ }
390
+ else {
391
+ content = error !== null && error !== void 0 ? error : 'Execution failed';
392
+ }
373
393
  return {
374
394
  toolCallId: callId,
375
395
  content,
396
+ frontendData,
376
397
  record: finalRecord,
377
398
  events: [endEvent],
378
399
  pendingApproval: typeof result === 'object' &&
@@ -14,6 +14,7 @@ export interface ProviderOptions {
14
14
  export declare function createLLMCaller(options: ProviderOptions): {
15
15
  call(messages: ReadonlyArray<Message>, tools: ReadonlyArray<ToolDefinition>, callOptions?: LLMCallOptions): Promise<{
16
16
  readonly content: string | null;
17
+ readonly reasoningContent?: string;
17
18
  readonly tool_calls?: ReadonlyArray<ToolCall>;
18
19
  }>;
19
20
  /**
@@ -31,6 +31,7 @@ function toOpenAIMessages(messages) {
31
31
  return ({
32
32
  role: m.role,
33
33
  content: m.content,
34
+ reasoning_content: m.reasoningContent,
34
35
  tool_calls: (_a = m.tool_calls) === null || _a === void 0 ? void 0 : _a.map((tc) => ({
35
36
  id: tc.id,
36
37
  type: tc.type,
@@ -80,6 +81,7 @@ function createLLMCaller(options) {
80
81
  const choice = data.choices[0];
81
82
  return {
82
83
  content: choice.message.content,
84
+ reasoningContent: choice.message.reasoning_content,
83
85
  tool_calls: (_b = choice.message.tool_calls) === null || _b === void 0 ? void 0 : _b.map((tc) => ({
84
86
  id: tc.id,
85
87
  type: tc.type,
@@ -137,6 +139,7 @@ function createLLMCaller(options) {
137
139
  // 用于增量收集 tool_calls
138
140
  const toolCallAccumulators = [];
139
141
  let fullContent = '';
142
+ let fullReasoningContent = '';
140
143
  let hasYieldedFinish = false;
141
144
  try {
142
145
  while (true) {
@@ -164,6 +167,11 @@ function createLLMCaller(options) {
164
167
  if (!choice)
165
168
  continue;
166
169
  const delta = choice.delta;
170
+ // 收集 reasoning_content 增量
171
+ if (delta.reasoning_content) {
172
+ fullReasoningContent += delta.reasoning_content;
173
+ yield yield __await({ type: 'reasoning', delta: delta.reasoning_content });
174
+ }
167
175
  // 收集 content 增量
168
176
  if (delta.content) {
169
177
  fullContent += delta.content;
@@ -209,6 +217,7 @@ function createLLMCaller(options) {
209
217
  yield yield __await({
210
218
  type: 'finish',
211
219
  content: fullContent || null,
220
+ reasoningContent: fullReasoningContent || undefined,
212
221
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
213
222
  });
214
223
  hasYieldedFinish = true;
@@ -233,6 +242,7 @@ function createLLMCaller(options) {
233
242
  yield yield __await({
234
243
  type: 'finish',
235
244
  content: fullContent || null,
245
+ reasoningContent: fullReasoningContent || undefined,
236
246
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
237
247
  });
238
248
  }
@@ -54,7 +54,7 @@ function buildInitialMessages(config) {
54
54
  function runLoopStream(config) {
55
55
  return __asyncGenerator(this, arguments, function* runLoopStream_1() {
56
56
  var _a, e_1, _b, _c;
57
- var _d, _e, _f, _g, _h, _j;
57
+ var _d, _e, _f, _g, _h, _j, _k;
58
58
  const llmCaller = (_d = config.llmCaller) !== null && _d !== void 0 ? _d : (0, provider_1.createLLMCaller)({
59
59
  apiKey: (_e = process.env.BAILIAN_API_KEY) !== null && _e !== void 0 ? _e : '',
60
60
  baseUrl: (_f = process.env.BAILIAN_BASE_URL) !== null && _f !== void 0 ? _f : DEFAULT_BAILIAN_BASE_URL,
@@ -108,6 +108,7 @@ function runLoopStream(config) {
108
108
  const messagesForLLM = yield __await((0, loop_1.compressMessages)(config, messages, turn));
109
109
  // 第五步:流式调用 LLM
110
110
  let fullContent = '';
111
+ let fullReasoningContent = '';
111
112
  let finishChunk = null;
112
113
  const stream = llmCaller.stream(messagesForLLM, toolDefinitions, {
113
114
  model: config.model,
@@ -117,14 +118,18 @@ function runLoopStream(config) {
117
118
  reasoningEffort: config.reasoningEffort,
118
119
  });
119
120
  try {
120
- for (var _k = true, stream_1 = (e_1 = void 0, __asyncValues(stream)), stream_1_1; stream_1_1 = yield __await(stream_1.next()), _a = stream_1_1.done, !_a; _k = true) {
121
+ for (var _l = true, stream_1 = (e_1 = void 0, __asyncValues(stream)), stream_1_1; stream_1_1 = yield __await(stream_1.next()), _a = stream_1_1.done, !_a; _l = true) {
121
122
  _c = stream_1_1.value;
122
- _k = false;
123
+ _l = false;
123
124
  const chunk = _c;
124
125
  if (chunk.type === 'content') {
125
126
  fullContent += chunk.delta;
126
127
  yield yield __await({ type: 'content', delta: chunk.delta, turn });
127
128
  }
129
+ else if (chunk.type === 'reasoning') {
130
+ fullReasoningContent += chunk.delta;
131
+ yield yield __await({ type: 'reasoning', delta: chunk.delta, turn });
132
+ }
128
133
  else if (chunk.type === 'finish') {
129
134
  finishChunk = chunk;
130
135
  }
@@ -133,7 +138,7 @@ function runLoopStream(config) {
133
138
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
134
139
  finally {
135
140
  try {
136
- if (!_k && !_a && (_b = stream_1.return)) yield __await(_b.call(stream_1));
141
+ if (!_l && !_a && (_b = stream_1.return)) yield __await(_b.call(stream_1));
137
142
  }
138
143
  finally { if (e_1) throw e_1.error; }
139
144
  }
@@ -142,11 +147,13 @@ function runLoopStream(config) {
142
147
  }
143
148
  // 使用 finish chunk 中的 content(优先级更高,可能包含完整格式化内容)
144
149
  const assistantContent = (_j = finishChunk.content) !== null && _j !== void 0 ? _j : fullContent;
150
+ const assistantReasoningContent = (_k = finishChunk.reasoningContent) !== null && _k !== void 0 ? _k : (fullReasoningContent || undefined);
145
151
  // 第六步:检查 LLM 回复
146
152
  if (!finishChunk.tool_calls || finishChunk.tool_calls.length === 0) {
147
153
  messages.push({
148
154
  role: 'assistant',
149
155
  content: assistantContent,
156
+ reasoningContent: assistantReasoningContent,
150
157
  });
151
158
  yield yield __await({ type: 'turn_end', turn });
152
159
  return yield __await({
@@ -165,6 +172,7 @@ function runLoopStream(config) {
165
172
  messages.push({
166
173
  role: 'assistant',
167
174
  content: assistantContent,
175
+ reasoningContent: assistantReasoningContent,
168
176
  tool_calls: finishChunk.tool_calls,
169
177
  });
170
178
  // 第七步:并行执行 tool
@@ -183,6 +191,7 @@ function runLoopStream(config) {
183
191
  callId: result.toolCallId,
184
192
  toolName: result.record.toolName,
185
193
  content: result.content,
194
+ frontendData: result.frontendData,
186
195
  status: result.record.status,
187
196
  });
188
197
  }
@@ -5,6 +5,7 @@
5
5
  export interface Message {
6
6
  readonly role: 'system' | 'user' | 'assistant' | 'tool';
7
7
  readonly content: string;
8
+ readonly reasoningContent?: string;
8
9
  readonly tool_calls?: ReadonlyArray<ToolCall>;
9
10
  readonly tool_call_id?: string;
10
11
  }
@@ -106,6 +107,15 @@ export interface ApprovalConfig {
106
107
  approved?: boolean;
107
108
  }>;
108
109
  }
110
+ /**
111
+ * Tool 执行结果的分流包装。
112
+ * 当 execute 返回此类型时,forLLM 进入 messages[] 给 LLM 上下文,
113
+ * forFrontend 通过 SSE tool_result 事件透给前端渲染。
114
+ */
115
+ export interface ToolResult {
116
+ readonly forLLM: string;
117
+ readonly forFrontend?: unknown;
118
+ }
109
119
  /**
110
120
  * 注册到 Loop 中的 Tool。
111
121
  */
@@ -114,7 +124,7 @@ export interface Tool {
114
124
  readonly description: string;
115
125
  readonly parameters: unknown;
116
126
  readonly discover?: (harness: ReadonlyArray<HarnessRecord>, metadata: unknown) => ToolDiscoverResult | Promise<ToolDiscoverResult>;
117
- readonly execute: (args: unknown, context: ToolExecuteContext) => unknown | Promise<unknown>;
127
+ readonly execute: (args: unknown, context: ToolExecuteContext) => unknown | ToolResult | Promise<unknown | ToolResult>;
118
128
  readonly approval?: ApprovalConfig;
119
129
  }
120
130
  /**
@@ -150,9 +160,13 @@ export interface LLMCallOptions {
150
160
  export type LLMStreamChunk = {
151
161
  readonly type: 'content';
152
162
  readonly delta: string;
163
+ } | {
164
+ readonly type: 'reasoning';
165
+ readonly delta: string;
153
166
  } | {
154
167
  readonly type: 'finish';
155
168
  readonly content: string | null;
169
+ readonly reasoningContent?: string;
156
170
  readonly tool_calls?: ReadonlyArray<ToolCall>;
157
171
  };
158
172
  /**
@@ -161,6 +175,7 @@ export type LLMStreamChunk = {
161
175
  export interface LLMCaller {
162
176
  readonly call: (messages: ReadonlyArray<Message>, tools: ReadonlyArray<ToolDefinition>, options?: LLMCallOptions) => Promise<{
163
177
  readonly content: string | null;
178
+ readonly reasoningContent?: string;
164
179
  readonly tool_calls?: ReadonlyArray<ToolCall>;
165
180
  }>;
166
181
  readonly stream?: (messages: ReadonlyArray<Message>, tools: ReadonlyArray<ToolDefinition>, options?: LLMCallOptions) => AsyncGenerator<LLMStreamChunk, void, unknown>;
@@ -225,6 +240,10 @@ export type LoopStreamChunk = {
225
240
  readonly type: 'content';
226
241
  readonly delta: string;
227
242
  readonly turn: number;
243
+ } | {
244
+ readonly type: 'reasoning';
245
+ readonly delta: string;
246
+ readonly turn: number;
228
247
  } | {
229
248
  readonly type: 'tool_call';
230
249
  readonly turn: number;
@@ -235,6 +254,7 @@ export type LoopStreamChunk = {
235
254
  readonly callId: string;
236
255
  readonly toolName: string;
237
256
  readonly content: string;
257
+ readonly frontendData?: unknown;
238
258
  readonly status: HarnessRecordStatus;
239
259
  } | {
240
260
  readonly type: 'turn_end';
@@ -80,6 +80,7 @@ class BailianProvider {
80
80
  const choice = data.choices[0];
81
81
  return {
82
82
  content: choice.message.content,
83
+ reasoningContent: choice.message.reasoning_content,
83
84
  usage: data.usage
84
85
  ? {
85
86
  promptTokens: data.usage.prompt_tokens,
@@ -152,6 +153,9 @@ class BailianProvider {
152
153
  if (!choice)
153
154
  continue;
154
155
  const delta = choice.delta;
156
+ if (delta.reasoning_content) {
157
+ yield yield __await({ type: 'reasoning', delta: delta.reasoning_content });
158
+ }
155
159
  if (delta.content) {
156
160
  yield yield __await({ type: 'content', delta: delta.content });
157
161
  }
@@ -5,6 +5,7 @@
5
5
  export interface OpenAIChatMessage {
6
6
  role: string;
7
7
  content: string | null;
8
+ reasoning_content?: string;
8
9
  tool_calls?: Array<{
9
10
  id: string;
10
11
  type: 'function';
@@ -42,6 +43,7 @@ export interface OpenAIChatResponse {
42
43
  choices: Array<{
43
44
  message: {
44
45
  content: string | null;
46
+ reasoning_content?: string;
45
47
  tool_calls?: Array<{
46
48
  id: string;
47
49
  type: 'function';
@@ -22,6 +22,7 @@ export interface LLMRequest {
22
22
  */
23
23
  export interface LLMResponse {
24
24
  readonly content: string | null;
25
+ readonly reasoningContent?: string;
25
26
  readonly usage?: {
26
27
  readonly promptTokens: number;
27
28
  readonly completionTokens: number;
@@ -35,6 +36,9 @@ export interface LLMResponse {
35
36
  export type StreamChunk = {
36
37
  readonly type: 'content';
37
38
  readonly delta: string;
39
+ } | {
40
+ readonly type: 'reasoning';
41
+ readonly delta: string;
38
42
  } | {
39
43
  readonly type: 'finish';
40
44
  usage?: LLMResponse['usage'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keo-ai/axiom",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "基于 LLM 的预测与推理库,支持多 Provider 切换",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",