@triggerlink/sdk 0.3.2 → 0.4.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 +64 -1
- package/dist/agent.d.ts +53 -0
- package/dist/agent.js +119 -0
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/package.json +20 -2
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Lets Next.js / Node.js applications integrate with the TriggerLink platform using an
|
|
4
4
|
Inngest-style DX. See the protocol spec at
|
|
5
5
|
[`docs/protocol.md`](../docs/protocol.md). Currently supports the `step.run`,
|
|
6
|
-
`step.sleep` / `step.sleepUntil`, and `step.sendEvent` primitives
|
|
6
|
+
`step.sleep` / `step.sleepUntil`, and `step.sendEvent` primitives, plus a native
|
|
7
|
+
AI agent primitive via the `@triggerlink/sdk/agent` subpath.
|
|
7
8
|
|
|
8
9
|
## Integrating with Next.js (App Router)
|
|
9
10
|
|
|
@@ -62,6 +63,68 @@ Note: for local development, point the serve URL at `http://localhost:3000/api/t
|
|
|
62
63
|
- The step call sequence must be deterministic: branches/loops may only depend on event data and the outputs of completed steps;
|
|
63
64
|
- A single step's duration must be shorter than both the deployment platform's function limit and the platform callback timeout (5 minutes by default).
|
|
64
65
|
|
|
66
|
+
## AI Agents (`@triggerlink/sdk/agent`)
|
|
67
|
+
|
|
68
|
+
A native agent primitive: a single agent (system prompt + tools + model) whose
|
|
69
|
+
LLM/tool loop is decomposed into ordinary durable steps. **Each LLM call and each
|
|
70
|
+
tool execution is individually memoized** — on crash recovery, completed calls are
|
|
71
|
+
injected from memo and only the failed call re-runs (no re-billed LLM tokens).
|
|
72
|
+
Built on the [Vercel AI SDK](https://github.com/vercel/ai) for multi-provider
|
|
73
|
+
support; design details in [`docs/agent-design.md`](../docs/agent-design.md).
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install @triggerlink/sdk ai zod @ai-sdk/anthropic # or another AI SDK provider
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { createFunction } from "@triggerlink/sdk";
|
|
81
|
+
import { createAgent } from "@triggerlink/sdk/agent"; // subpath import, not the main entry
|
|
82
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
83
|
+
import { z } from "zod";
|
|
84
|
+
|
|
85
|
+
const researcher = createAgent({
|
|
86
|
+
name: "researcher", // stable ID, used in memo keys — do not rename casually
|
|
87
|
+
model: anthropic("claude-sonnet-4-5"), // any AI SDK LanguageModel
|
|
88
|
+
system: "You are a research assistant. Answer concisely.",
|
|
89
|
+
tools: {
|
|
90
|
+
search: {
|
|
91
|
+
description: "Search the knowledge base",
|
|
92
|
+
parameters: z.object({ query: z.string() }),
|
|
93
|
+
handler: async ({ query }) => kb.search(query),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
maxIterations: 10, // safety cap; the run fails when exceeded
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const answerQuestion = createFunction(
|
|
100
|
+
{ id: "answer-question", event: "question/asked" },
|
|
101
|
+
async ({ event, step }) => {
|
|
102
|
+
const { question } = event.data as { question: string };
|
|
103
|
+
const result = await researcher.run(step, question); // each LLM/tool call is a durable step
|
|
104
|
+
|
|
105
|
+
// Function code is the router: chain agents, branch, or fan out — no extra abstraction
|
|
106
|
+
await step.sendEvent("notify", { name: "question/answered", data: { answer: result.text } });
|
|
107
|
+
return result;
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Notes:
|
|
113
|
+
|
|
114
|
+
- **Durability granularity**: an agent run with *L* LLM calls and *T* tool executions costs
|
|
115
|
+
*L + T* platform callbacks (one per step). Each step appears in the dashboard run detail
|
|
116
|
+
with its output — per-call tracing and token usage for free.
|
|
117
|
+
- **Multi-tool responses** are executed sequentially in array order, one step each.
|
|
118
|
+
- **`redact` hook** (optional): transforms each step's output inside `step.run` before
|
|
119
|
+
persistence, e.g. to strip secrets or PII from memos. It must be deterministic and
|
|
120
|
+
replay-safe — the memo is what the model sees of its own prior turns after a crash-resume:
|
|
121
|
+
`redact: (output, ctx) => ...` with `ctx = { kind: "llm" | "tool", iteration, toolName? }`.
|
|
122
|
+
- **Constraints**: same as any function — a single LLM call must finish within the platform
|
|
123
|
+
callback timeout (5 minutes by default); two different agents in one function must have
|
|
124
|
+
different `name`s; changing the tool set or loop structure between retries of the same run
|
|
125
|
+
can misalign memo keys (changing prompt text is safe).
|
|
126
|
+
- The `ai` and `zod` packages are optional peer dependencies — only agent users install them.
|
|
127
|
+
|
|
65
128
|
## Sending events (any TS code, modeled after Inngest's `inngest.send`)
|
|
66
129
|
|
|
67
130
|
```ts
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type LanguageModel } from "ai";
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
|
+
import type { StepTool } from "./step.js";
|
|
4
|
+
/** Agent 工具定义。parameters 为 zod schema;handler 入参是 schema parse 后的值。 */
|
|
5
|
+
export interface AgentTool<P = unknown, R = unknown> {
|
|
6
|
+
description: string;
|
|
7
|
+
parameters: ZodType<P>;
|
|
8
|
+
handler: (params: P) => Promise<R> | R;
|
|
9
|
+
}
|
|
10
|
+
/** redact 钩子的上下文(§5.7)。 */
|
|
11
|
+
export interface RedactCtx {
|
|
12
|
+
/** 产生输出的 step 类型 */
|
|
13
|
+
kind: "llm" | "tool";
|
|
14
|
+
/** Agent 循环迭代号(0 起) */
|
|
15
|
+
iteration: number;
|
|
16
|
+
/** kind === "tool" 时的工具名 */
|
|
17
|
+
toolName?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 输出脱敏钩子:在 step.run 内部、输出被持久化之前调用。
|
|
21
|
+
* 必须 replay-safe 且确定(同输入同输出)——memo 是恢复时重建对话历史的唯一来源,
|
|
22
|
+
* 删改的字段会原样出现在恢复后的后续 LLM 调用里。llm 输出的结构(text/toolCalls/
|
|
23
|
+
* responseMessages)不得破坏,否则抛错(见 assertLlmMemoShape)。
|
|
24
|
+
*/
|
|
25
|
+
export type RedactHook = (output: unknown, ctx: RedactCtx) => unknown;
|
|
26
|
+
export interface AgentOpts {
|
|
27
|
+
/** 稳定标识,用作 memo 键前缀(agent/<name>/...);同一函数内不同 Agent 必须不同名(§5.2) */
|
|
28
|
+
name: string;
|
|
29
|
+
/** AI SDK 的 LanguageModel(用户自带 provider 包,如 @ai-sdk/anthropic) */
|
|
30
|
+
model: LanguageModel;
|
|
31
|
+
system?: string;
|
|
32
|
+
tools?: Record<string, AgentTool>;
|
|
33
|
+
/** 迭代上限(一次迭代 = 一次 LLM 调用 + 其全部工具执行),默认 10;超限抛错使 run 失败 */
|
|
34
|
+
maxIterations?: number;
|
|
35
|
+
redact?: RedactHook;
|
|
36
|
+
}
|
|
37
|
+
export interface AgentResult {
|
|
38
|
+
/** 最终一条 assistant 文本 */
|
|
39
|
+
text: string;
|
|
40
|
+
/** 实际执行的迭代次数 */
|
|
41
|
+
iterations: number;
|
|
42
|
+
/** 各迭代 token 用量合计(memo 命中时也照常累计) */
|
|
43
|
+
usage: {
|
|
44
|
+
inputTokens: number;
|
|
45
|
+
outputTokens: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface Agent {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
run(step: StepTool, input: string): Promise<AgentResult>;
|
|
51
|
+
}
|
|
52
|
+
/** 定义一个 Agent:system prompt + tools + 模型,循环直到模型不再调用工具。 */
|
|
53
|
+
export declare function createAgent(opts: AgentOpts): Agent;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Agent 原语:把单 Agent 的 LLM/tool 循环分解为 durable step(设计文档 docs/agent-design.md)。
|
|
2
|
+
// 每次 LLM 调用与每次工具执行各自 memo 化;崩溃恢复时从 memo 重建对话历史,只重跑失败的调用。
|
|
3
|
+
// 本模块只通过子路径 @triggerlink/sdk/agent 导出——ai/zod 是 optional peer 依赖,
|
|
4
|
+
// 主入口 index.ts 不得 import 本模块(§8.1),否则未装 ai 的普通用户会在 import 时崩溃。
|
|
5
|
+
import { generateText, tool, } from "ai";
|
|
6
|
+
/** redact 钩子可能破坏 llm memo 结构;此处 fail loud,不让坏 memo 落库或参与历史重建(§5.7)。 */
|
|
7
|
+
function assertLlmMemoShape(m, name) {
|
|
8
|
+
const o = m;
|
|
9
|
+
if (!o ||
|
|
10
|
+
typeof o.text !== "string" ||
|
|
11
|
+
!Array.isArray(o.toolCalls) ||
|
|
12
|
+
!Array.isArray(o.responseMessages) ||
|
|
13
|
+
!o.usage ||
|
|
14
|
+
typeof o.usage !== "object") {
|
|
15
|
+
throw new Error(`agent "${name}": llm step memo has a damaged structure (must keep text/toolCalls/responseMessages/usage) — check the redact hook`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// 同一函数调用(每次回调都是一个新 StepTool 实例)内 agent 名 → 实例的登记簿,
|
|
19
|
+
// 用于拒绝两个不同 Agent 共用 name 造成的 memo 键前缀冲突(§5.2)。
|
|
20
|
+
// 同一个 Agent 实例多次 run(如循环里)是合法的:ExecCtx 序号机制保证 memo 键确定。
|
|
21
|
+
const claimedNames = new WeakMap();
|
|
22
|
+
/** 定义一个 Agent:system prompt + tools + 模型,循环直到模型不再调用工具。 */
|
|
23
|
+
export function createAgent(opts) {
|
|
24
|
+
if (!opts.name || opts.name.includes("/")) {
|
|
25
|
+
throw new Error('createAgent: name is required and must not contain "/"');
|
|
26
|
+
}
|
|
27
|
+
if (!opts.model)
|
|
28
|
+
throw new Error("createAgent: model is required");
|
|
29
|
+
const maxIterations = opts.maxIterations ?? 10;
|
|
30
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 1) {
|
|
31
|
+
throw new Error("createAgent: maxIterations must be a positive integer");
|
|
32
|
+
}
|
|
33
|
+
const toolDefs = opts.tools ?? {};
|
|
34
|
+
// 以 schema-only 方式把工具交给 AI SDK(不传 execute):
|
|
35
|
+
// 模型的 tool call 原样返回不执行,"决策"与"执行"之间就是我们的 step 边界(§4.1)。
|
|
36
|
+
const aiTools = {};
|
|
37
|
+
for (const [toolName, def] of Object.entries(toolDefs)) {
|
|
38
|
+
aiTools[toolName] = tool({ description: def.description, inputSchema: def.parameters });
|
|
39
|
+
}
|
|
40
|
+
const agent = {
|
|
41
|
+
name: opts.name,
|
|
42
|
+
async run(step, input) {
|
|
43
|
+
let registry = claimedNames.get(step);
|
|
44
|
+
if (!registry) {
|
|
45
|
+
registry = new Map();
|
|
46
|
+
claimedNames.set(step, registry);
|
|
47
|
+
}
|
|
48
|
+
const claimed = registry.get(agent.name);
|
|
49
|
+
if (claimed && claimed !== agent) {
|
|
50
|
+
throw new Error(`agent "${agent.name}": another agent with the same name already ran in this function; names must be unique per function (memo key prefix collision)`);
|
|
51
|
+
}
|
|
52
|
+
registry.set(agent.name, agent);
|
|
53
|
+
const redact = opts.redact;
|
|
54
|
+
const llmStepId = `agent/${agent.name}/llm`;
|
|
55
|
+
const toolStepId = `agent/${agent.name}/tool`;
|
|
56
|
+
const messages = [{ role: "user", content: input }];
|
|
57
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
58
|
+
for (let i = 0;; i++) {
|
|
59
|
+
if (i >= maxIterations) {
|
|
60
|
+
throw new Error(`agent "${agent.name}": maxIterations (${maxIterations}) exceeded`);
|
|
61
|
+
}
|
|
62
|
+
const llmMemo = await step.run(llmStepId, async () => {
|
|
63
|
+
const res = await generateText({
|
|
64
|
+
model: opts.model,
|
|
65
|
+
system: opts.system,
|
|
66
|
+
messages,
|
|
67
|
+
tools: aiTools,
|
|
68
|
+
});
|
|
69
|
+
const memo = {
|
|
70
|
+
text: res.text,
|
|
71
|
+
toolCalls: res.toolCalls.map((c) => ({
|
|
72
|
+
toolCallId: c.toolCallId,
|
|
73
|
+
toolName: c.toolName,
|
|
74
|
+
input: c.input,
|
|
75
|
+
})),
|
|
76
|
+
responseMessages: res.responseMessages,
|
|
77
|
+
usage: { inputTokens: res.usage.inputTokens, outputTokens: res.usage.outputTokens },
|
|
78
|
+
};
|
|
79
|
+
const out = redact ? redact(memo, { kind: "llm", iteration: i }) : memo;
|
|
80
|
+
assertLlmMemoShape(out, agent.name); // 落库前拦截坏结构
|
|
81
|
+
return out;
|
|
82
|
+
});
|
|
83
|
+
assertLlmMemoShape(llmMemo, agent.name); // memo 命中路径同样校验(防御脏数据)
|
|
84
|
+
usage.inputTokens += llmMemo.usage.inputTokens ?? 0;
|
|
85
|
+
usage.outputTokens += llmMemo.usage.outputTokens ?? 0;
|
|
86
|
+
messages.push(...llmMemo.responseMessages);
|
|
87
|
+
if (llmMemo.toolCalls.length === 0) {
|
|
88
|
+
return { text: llmMemo.text, iterations: i + 1, usage };
|
|
89
|
+
}
|
|
90
|
+
// 并行 tool call 顺序执行(数组序),每个一个 durable step(§5.3)
|
|
91
|
+
for (const call of llmMemo.toolCalls) {
|
|
92
|
+
const def = toolDefs[call.toolName];
|
|
93
|
+
if (!def) {
|
|
94
|
+
throw new Error(`agent "${agent.name}": model called unknown tool "${call.toolName}"`);
|
|
95
|
+
}
|
|
96
|
+
const output = await step.run(toolStepId, async () => {
|
|
97
|
+
const out = await def.handler(def.parameters.parse(call.input));
|
|
98
|
+
return redact
|
|
99
|
+
? redact(out, { kind: "tool", iteration: i, toolName: call.toolName })
|
|
100
|
+
: out;
|
|
101
|
+
});
|
|
102
|
+
const toolMsg = {
|
|
103
|
+
role: "tool",
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "tool-result",
|
|
107
|
+
toolCallId: call.toolCallId,
|
|
108
|
+
toolName: call.toolName,
|
|
109
|
+
output: { type: "json", value: (output ?? null) },
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
messages.push(toolMsg);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
return agent;
|
|
119
|
+
}
|
package/dist/serve.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Client } from "./client.js";
|
|
2
2
|
import type { TriggerFunction } from "./function.js";
|
|
3
|
-
export declare const sdkVersion = "triggerlink-ts/0.
|
|
3
|
+
export declare const sdkVersion = "triggerlink-ts/0.4.0";
|
|
4
4
|
export declare const SIGNATURE_HEADER = "x-triggerlink-signature";
|
|
5
5
|
export interface ServeOptions {
|
|
6
6
|
client: Client;
|
package/dist/serve.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ExecCtx, StepInterrupt } from "./execx.js";
|
|
2
2
|
import { createStepTool, errMessage } from "./step.js";
|
|
3
3
|
import { verifySignature } from "./sign.js";
|
|
4
|
-
export const sdkVersion = "triggerlink-ts/0.
|
|
4
|
+
export const sdkVersion = "triggerlink-ts/0.4.0";
|
|
5
5
|
export const SIGNATURE_HEADER = "x-triggerlink-signature";
|
|
6
6
|
const MAX_BODY = 10 << 20; // 10 MB,与平台一致
|
|
7
7
|
function json(data, status = 200) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triggerlink/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "TriggerLink TypeScript SDK: durable, crash-recoverable functions for Next.js / Node.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"triggerlink",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
".": {
|
|
19
19
|
"types": "./dist/index.d.ts",
|
|
20
20
|
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./agent": {
|
|
23
|
+
"types": "./dist/agent.d.ts",
|
|
24
|
+
"import": "./dist/agent.js"
|
|
21
25
|
}
|
|
22
26
|
},
|
|
23
27
|
"files": [
|
|
@@ -39,6 +43,20 @@
|
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
41
45
|
"@types/node": "^22.0.0",
|
|
42
|
-
"
|
|
46
|
+
"ai": "^7.0.66",
|
|
47
|
+
"typescript": "^5.5.0",
|
|
48
|
+
"zod": "^4.4.3"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"ai": "^7.0.0",
|
|
52
|
+
"zod": "^3.25.76 || ^4.1.8"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"ai": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"zod": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
}
|