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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
timeout-minutes: 20
strategy:
matrix:
node-version: ['20', '22']
node-version: ['22']

steps:
- name: Checkout repository
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ pnpm add @blade-ai/agent-sdk

已发布包面向 npm 分发;这个仓库本身使用 `pnpm` 进行依赖安装、构建、测试、发布和文档开发。

> **ESM-only**:本包为纯 ESM(`"type": "module"`),仅通过 `import` 使用,不支持 CommonJS `require()`(否则会报 `ERR_PACKAGE_PATH_NOT_EXPORTED`)。请确保项目为 ESM(package.json 设 `"type": "module"`)或使用支持 ESM 的运行时/打包器。

## 快速开始

```ts
Expand Down
71 changes: 59 additions & 12 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ SDK 提供三种方式创建自定义工具,从简单到完整:
最简单的工具定义方式,原样返回传入的定义。适合直接传给 `SessionOptions.tools`。

```ts
import { defineTool } from '@blade-ai/agent-sdk';
import { defineTool, ToolKind } from '@blade-ai/agent-sdk';

const searchTool = defineTool({
name: 'SearchDocs',
Expand All @@ -26,30 +26,35 @@ const searchTool = defineTool({
},
required: ['query'],
},
kind: 'readonly',
kind: ToolKind.ReadOnly,
execute: async (params) => {
const results = await searchDocuments(params.query, params.limit ?? 10);
return {
success: true,
llmContent: JSON.stringify(results),
displayContent: `找到 ${results.length} 条结果`,
// 可选:结构化数据(必须是 JSON 值),供调用方消费
data: { count: results.length },
};
},
});
```

::: tip
`kind` 使用 `ToolKind` 枚举(`ToolKind.ReadOnly` / `ToolKind.Write` / `ToolKind.Execute`),从 `@blade-ai/agent-sdk` 导入。TypeScript 下不接受裸字符串字面量。
:::

## createTool

使用 Zod Schema 的工厂函数,提供完整的类型推断和运行时验证。

```ts
import { z } from 'zod';
import { createTool } from '@blade-ai/agent-sdk';
import { createTool, ToolKind } from '@blade-ai/agent-sdk';

const deployTool = createTool({
name: 'Deploy',
displayName: 'Deploy',
kind: 'execute',
kind: ToolKind.Execute,
description: {
short: '部署应用到指定环境',
long: '支持 staging 和 production 环境的自动部署',
Expand All @@ -64,7 +69,6 @@ const deployTool = createTool({
return {
success: true,
llmContent: `已部署 v${params.version} 到 ${params.environment}`,
displayContent: `✅ 部署成功: v${params.version} → ${params.environment}`,
};
},
});
Expand Down Expand Up @@ -194,17 +198,60 @@ interface ToolDescription {

### ToolResult

`ToolResult` 是成功与失败两种结果的判别联合:

```ts
interface ToolResult {
success: boolean;
type ToolResult = ToolSuccessResult | ToolFailureResult;

interface ToolSuccessResult {
success: true;
llmContent: string | object; // 返回给 LLM 的内容
data?: JsonValue; // 可选:结构化数据(必须是 JSON 值)
metadata?: ToolResultMetadata;
}

interface ToolFailureResult {
success: false;
llmContent: string | object;
displayContent: string;
error?: ToolError;
metadata?: Record<string, unknown>;
error: ToolError; // 失败时必填
metadata?: ToolResultMetadata;
}
```

`llmContent` 和 `displayContent` 分离设计让你可以给 LLM 提供结构化数据,同时给用户展示可读的摘要。
`llmContent` 是给 LLM 消费的内容;如需返回结构化数据用 `data`(其类型为 `JsonValue`,因此自定义对象数组等需保证可 JSON 序列化)。失败时 `success: false` 且必须带 `error`。

::: warning data 必须可 JSON 序列化
`data` 会被序列化落盘(结果产物存储),因此其类型约束为 `JsonValue`。若你的领域类型是 `interface`(无索引签名),赋给 `data` 时可能报「缺少索引签名」。解决办法是让该类型满足 `JsonValue`(字段均为 JSON 值),不要用 `as unknown` 强绕过——那会把「运行时序列化失败」的风险藏起来。
:::

### 为工具参数与 data 提供类型

`defineTool` 支持两个可选泛型:`TParams`(参数类型)与 `TData`(`data` 字段类型,须 `extends JsonValue`)。指定后 `execute` 的 `params` 与返回的 `data` 都会得到精确类型,无需在 `execute` 内部做 `as` 断言:

```ts
import { defineTool, ToolKind } from '@blade-ai/agent-sdk';

const tool = defineTool<{ query: string; limit?: number }, { count: number }>({
name: 'SearchDocs',
description: '搜索文档库',
kind: ToolKind.ReadOnly,
parameters: {
type: 'object',
properties: {
query: { type: 'string' },
limit: { type: 'number' },
},
required: ['query'],
},
execute: async (params) => {
// params.query: string, params.limit?: number —— 无需 cast
const results = await searchDocuments(params.query, params.limit ?? 10);
return { success: true, llmContent: JSON.stringify(results), data: { count: results.length } };
},
});
```

带具体 `TParams` 的工具可以直接放进 `SessionOptions.tools`,无需断言。

### ExecutionContext

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "Blade AI Agent SDK",
"type": "module",
"engines": {
"node": ">=20.0.0"
"node": ">=22.14.0"
},
"packageManager": "pnpm@10.6.0",
"main": "./dist/index.js",
Expand Down
21 changes: 21 additions & 0 deletions scripts/__tests__/node-version-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

import { describe, expect, it } from 'vitest';
import { parse } from 'yaml';

describe('Node.js version policy', () => {
it('runs CI verification only on the supported Node.js release line', () => {
const workflow = parse(
readFileSync(resolve('.github/workflows/ci.yml'), 'utf8'),
);

expect(workflow.jobs.verify.strategy.matrix['node-version']).toEqual(['22']);
});

it('advertises the same runtime floor in package metadata', () => {
const packageJson = JSON.parse(readFileSync(resolve('package.json'), 'utf8'));

expect(packageJson.engines.node).toBe('>=22.14.0');
});
});
12 changes: 9 additions & 3 deletions src/__tests__/deepseek-agent.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
PermissionMode,
prompt,
type StreamMessage,
ToolErrorType,
} from '../index.js';

// ─── 配置 ─────────────────────────────────────────────────
Expand Down Expand Up @@ -219,7 +220,11 @@ describeDeepSeek('2. Thinking + Tool Use 组合场景', () => {
}
return {
success: false as const,
error: `API Error: endpoint "${params.endpoint}" returned 503 Service Unavailable`,
llmContent: `API Error: endpoint "${params.endpoint}" returned 503 Service Unavailable`,
error: {
type: ToolErrorType.EXECUTION_ERROR,
message: `API Error: endpoint "${params.endpoint}" returned 503 Service Unavailable`,
},
};
},
});
Expand Down Expand Up @@ -412,7 +417,8 @@ describeDeepSeek('5. 结构化输出兼容性', () => {
// 验证输出是合法 JSON
const jsonMatch = res.result.match(/\{[\s\S]*\}/);
expect(jsonMatch).not.toBeNull();
const parsed = JSON.parse(jsonMatch![0]);
if (!jsonMatch) throw new Error('Expected JSON object in response');
const parsed = JSON.parse(jsonMatch[0]);
expect(parsed.languages).toBeDefined();
expect(parsed.languages.length).toBeGreaterThanOrEqual(3);
expect(parsed.languages[0].name).toBeDefined();
Expand Down Expand Up @@ -509,7 +515,7 @@ describeDeepSeek('7. 并发与边界情况', () => {

let aborted = false;
try {
for await (const msg of session.stream({ includeThinking: true })) {
for await (const _msg of session.stream({ includeThinking: true })) {
chunkCount++;
if (chunkCount > 200) break; // 安全阀
}
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ export { ToolKind } from './tools/types/ToolKind.js';
export { AgentId, MessageId, SessionId, ToolUseId } from './types/branded.js';
// --- Constants & types ---
export type {
JsonObject,
JsonValue,
McpServerConfig,
OutputFormat,
SandboxSettings,
Expand Down
Loading
Loading