@triggerlink/sdk 0.6.0 → 0.6.2
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 +9 -0
- package/dist/agent.d.ts +28 -1
- package/dist/agent.js +50 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,7 @@ const researcher = createAgent({
|
|
|
123
123
|
system: "You are a research assistant. Answer concisely.",
|
|
124
124
|
tools: { search: searchKb },
|
|
125
125
|
maxIterations: 10, // safety cap; the run fails when exceeded
|
|
126
|
+
maxOutputTokens: 1024, // per-LLM-call output token cap (optional)
|
|
126
127
|
});
|
|
127
128
|
|
|
128
129
|
const answerQuestion = createFunction(
|
|
@@ -179,6 +180,14 @@ Notes:
|
|
|
179
180
|
persistence, e.g. to strip secrets or PII from memos. It must be deterministic and
|
|
180
181
|
replay-safe — the memo is what the model sees of its own prior turns after a crash-resume:
|
|
181
182
|
`redact: (output, ctx) => ...` with `ctx = { kind: "llm" | "tool", iteration, toolName? }`.
|
|
183
|
+
- **`prepareMessages` hook** (optional): synchronous history transform applied before each
|
|
184
|
+
`generateText` call — the typical use is context compaction/trimming. The returned list
|
|
185
|
+
becomes the official history for later turns (and for `AgentResult.output`), while
|
|
186
|
+
`result.toolCalls` stays the complete record. It receives
|
|
187
|
+
`{ messages, iteration, previousUsage }` and must be pure and deterministic — it re-runs
|
|
188
|
+
on crash-resume replays — and must keep every assistant tool-call paired with its
|
|
189
|
+
tool-result (the SDK validates the returned list and throws otherwise):
|
|
190
|
+
`prepareMessages: ({ messages }) => messages.slice(-8)`.
|
|
182
191
|
- **Constraints**: same as any function — a single LLM call must finish within the platform
|
|
183
192
|
callback timeout (5 minutes by default); two different agents in one function must have
|
|
184
193
|
different `name`s; changing the tool set or loop structure between retries of the same run
|
package/dist/agent.d.ts
CHANGED
|
@@ -37,6 +37,28 @@ export interface RedactCtx {
|
|
|
37
37
|
* responseMessages)不得破坏,否则抛错(见 assertLlmMemoShape)。
|
|
38
38
|
*/
|
|
39
39
|
export type RedactHook = (output: unknown, ctx: RedactCtx) => unknown;
|
|
40
|
+
/** prepareMessages 钩子的上下文(§5.8)。 */
|
|
41
|
+
export interface PrepareMessagesCtx {
|
|
42
|
+
/** 当前对话历史(本轮之前的全部 user/assistant/tool 消息) */
|
|
43
|
+
messages: readonly TextMessage[];
|
|
44
|
+
/** Agent 循环迭代号(0 起) */
|
|
45
|
+
iteration: number;
|
|
46
|
+
/** 上一轮 LLM 调用的 token 用量;首轮为 undefined */
|
|
47
|
+
previousUsage?: {
|
|
48
|
+
inputTokens?: number;
|
|
49
|
+
outputTokens?: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 调用前历史变换钩子:在每轮 generateText 之前同步调用,返回值替换对话历史——
|
|
54
|
+
* 既喂给本轮 LLM,也成为后续轮次的正式历史(典型用途:上下文压缩/裁剪)。
|
|
55
|
+
* 必须是纯函数:同步、不执行 I/O、确定(同输入同输出)——恢复重放时历史由 memo
|
|
56
|
+
* 重建后本钩子会重新应用,非确定性会使重建的历史漂移。只改消息内容,不改 step
|
|
57
|
+
* 序列,memo 键不受影响。返回的序列必须保持 assistant tool-call 与 tool-result
|
|
58
|
+
* 成组(每个 tool-call 有对应 tool-result,反之亦然),否则抛错
|
|
59
|
+
* (见 assertPreparedMessagesShape)。
|
|
60
|
+
*/
|
|
61
|
+
export type PrepareMessagesHook = (ctx: PrepareMessagesCtx) => TextMessage[];
|
|
40
62
|
/** 单轮 LLM 调用的结果(lifecycle.onResponse 的入参)。 */
|
|
41
63
|
export interface AgentIterationResult {
|
|
42
64
|
/** 本轮 assistant 文本 */
|
|
@@ -76,6 +98,10 @@ export interface AgentOpts {
|
|
|
76
98
|
tools?: Record<string, AgentTool<any, any>>;
|
|
77
99
|
/** 迭代上限(一次迭代 = 一次 LLM 调用 + 其全部工具执行),默认 10;超限抛错使 run 失败 */
|
|
78
100
|
maxIterations?: number;
|
|
101
|
+
/** 每次 LLM 调用的最大输出 token 数,透传给 generateText;不设则由模型/provider 决定 */
|
|
102
|
+
maxOutputTokens?: number;
|
|
103
|
+
/** 每轮 generateText 前的同步历史变换(§5.8);返回值成为后续轮次的正式历史 */
|
|
104
|
+
prepareMessages?: PrepareMessagesHook;
|
|
79
105
|
redact?: RedactHook;
|
|
80
106
|
lifecycle?: AgentLifecycle;
|
|
81
107
|
}
|
|
@@ -98,7 +124,8 @@ export interface AgentResult {
|
|
|
98
124
|
/**
|
|
99
125
|
* 完整对话历史(user 输入 + 各轮 assistant 消息 + 工具结果),字段名与 AgentKit
|
|
100
126
|
* 的 result.output 对齐——可自行 findLastIndex 等遍历(元素含 role/content)。
|
|
101
|
-
* 恢复重放时由 memo 原样重建;启用 redact
|
|
127
|
+
* 恢复重放时由 memo 原样重建;启用 redact 时历史内容即脱敏后内容;启用
|
|
128
|
+
* prepareMessages 时为裁剪后的正式历史(toolCalls 记录不受影响,始终完整)。
|
|
102
129
|
*/
|
|
103
130
|
output: TextMessage[];
|
|
104
131
|
}
|
package/dist/agent.js
CHANGED
|
@@ -52,6 +52,37 @@ function assertLlmMemoShape(m, name) {
|
|
|
52
52
|
throw new Error(`agent "${name}": llm step memo has a damaged structure (must keep text/toolCalls/responseMessages/usage) — check the redact hook`);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* prepareMessages 返回值的结构校验(§5.8):非空、角色合法、assistant tool-call 与
|
|
57
|
+
* tool-result 成组。坏历史会让下一轮 generateText 以更难懂的方式失败,此处 fail loud
|
|
58
|
+
* 并指名钩子。
|
|
59
|
+
*/
|
|
60
|
+
function assertPreparedMessagesShape(msgs, name) {
|
|
61
|
+
const bad = (why) => new Error(`agent "${name}": prepareMessages returned an invalid message list (${why})`);
|
|
62
|
+
if (!Array.isArray(msgs) || msgs.length === 0)
|
|
63
|
+
throw bad("must be a non-empty array");
|
|
64
|
+
const pending = new Set(); // 已出现、尚未配对 tool-result 的 tool-call id
|
|
65
|
+
for (const m of msgs) {
|
|
66
|
+
if (!m || (m.role !== "user" && m.role !== "assistant" && m.role !== "tool")) {
|
|
67
|
+
throw bad('roles must be "user" | "assistant" | "tool" (system lives in AgentOpts.system)');
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(m.content))
|
|
70
|
+
continue; // 字符串 content 无 parts,无可配对项
|
|
71
|
+
for (const p of m.content) {
|
|
72
|
+
if (m.role === "assistant" && p?.type === "tool-call" && typeof p.toolCallId === "string") {
|
|
73
|
+
pending.add(p.toolCallId);
|
|
74
|
+
}
|
|
75
|
+
if (m.role === "tool" && p?.type === "tool-result" && typeof p.toolCallId === "string") {
|
|
76
|
+
if (!pending.delete(p.toolCallId)) {
|
|
77
|
+
throw bad(`tool-result "${p.toolCallId}" has no preceding assistant tool-call`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (pending.size > 0) {
|
|
83
|
+
throw bad(`assistant tool-call(s) without tool-result: ${[...pending].join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
55
86
|
// 同一函数调用(每次回调都是一个新 StepTool 实例)内 agent 名 → 实例的登记簿,
|
|
56
87
|
// 用于拒绝两个不同 Agent 共用 name 造成的 memo 键前缀冲突(§5.2)。
|
|
57
88
|
// 同一个 Agent 实例多次 run(如循环里)是合法的:ExecCtx 序号机制保证 memo 键确定。
|
|
@@ -67,6 +98,12 @@ export function createAgent(opts) {
|
|
|
67
98
|
if (!Number.isInteger(maxIterations) || maxIterations < 1) {
|
|
68
99
|
throw new Error("createAgent: maxIterations must be a positive integer");
|
|
69
100
|
}
|
|
101
|
+
if (opts.maxOutputTokens !== undefined) {
|
|
102
|
+
if (!Number.isInteger(opts.maxOutputTokens) || opts.maxOutputTokens < 1) {
|
|
103
|
+
throw new Error("createAgent: maxOutputTokens must be a positive integer");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const maxOutputTokens = opts.maxOutputTokens;
|
|
70
107
|
const toolDefs = opts.tools ?? {};
|
|
71
108
|
// 以 schema-only 方式把工具交给 AI SDK(不传 execute):
|
|
72
109
|
// 模型的 tool call 原样返回不执行,"决策"与"执行"之间就是我们的 step 边界(§4.1)。
|
|
@@ -88,20 +125,31 @@ export function createAgent(opts) {
|
|
|
88
125
|
}
|
|
89
126
|
registry.set(agent.name, agent);
|
|
90
127
|
const redact = opts.redact;
|
|
128
|
+
const prepareMessages = opts.prepareMessages;
|
|
91
129
|
const llmStepId = `agent/${agent.name}`;
|
|
92
|
-
|
|
130
|
+
let messages = [{ role: "user", content: input }];
|
|
131
|
+
let prevUsage;
|
|
93
132
|
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
94
133
|
const toolCalls = [];
|
|
95
134
|
for (let i = 0;; i++) {
|
|
96
135
|
if (i >= maxIterations) {
|
|
97
136
|
throw new Error(`agent "${agent.name}": maxIterations (${maxIterations}) exceeded`);
|
|
98
137
|
}
|
|
138
|
+
// prepareMessages:同步、确定的调用前历史变换(§5.8)。在历史由 memo 重建之后、
|
|
139
|
+
// 本轮响应追加之前应用,因此恢复重放走同一代码路径并产出相同历史;
|
|
140
|
+
// 只改消息内容,不改 step 序列,memo 键不受影响。
|
|
141
|
+
if (prepareMessages) {
|
|
142
|
+
const prepared = prepareMessages({ messages, iteration: i, previousUsage: prevUsage });
|
|
143
|
+
assertPreparedMessagesShape(prepared, agent.name);
|
|
144
|
+
messages = prepared;
|
|
145
|
+
}
|
|
99
146
|
const llmMemo = await step.run(llmStepId, async () => {
|
|
100
147
|
const res = await generateText({
|
|
101
148
|
model: opts.model,
|
|
102
149
|
system: opts.system,
|
|
103
150
|
messages,
|
|
104
151
|
tools: aiTools,
|
|
152
|
+
maxOutputTokens,
|
|
105
153
|
});
|
|
106
154
|
const memo = {
|
|
107
155
|
text: res.text,
|
|
@@ -131,6 +179,7 @@ export function createAgent(opts) {
|
|
|
131
179
|
assertLlmMemoShape(llmMemo, agent.name); // memo 命中路径同样校验(防御脏数据)
|
|
132
180
|
usage.inputTokens += llmMemo.usage.inputTokens ?? 0;
|
|
133
181
|
usage.outputTokens += llmMemo.usage.outputTokens ?? 0;
|
|
182
|
+
prevUsage = llmMemo.usage; // memo 命中路径同样赋值:重放时 prepareMessages 入参一致
|
|
134
183
|
messages.push(...llmMemo.responseMessages);
|
|
135
184
|
if (llmMemo.toolCalls.length === 0) {
|
|
136
185
|
return { text: llmMemo.text, iterations: i + 1, usage, toolCalls, output: messages };
|