@triggerlink/sdk 0.6.1 → 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 CHANGED
@@ -180,6 +180,14 @@ Notes:
180
180
  persistence, e.g. to strip secrets or PII from memos. It must be deterministic and
181
181
  replay-safe — the memo is what the model sees of its own prior turns after a crash-resume:
182
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)`.
183
191
  - **Constraints**: same as any function — a single LLM call must finish within the platform
184
192
  callback timeout (5 minutes by default); two different agents in one function must have
185
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 文本 */
@@ -78,6 +100,8 @@ export interface AgentOpts {
78
100
  maxIterations?: number;
79
101
  /** 每次 LLM 调用的最大输出 token 数,透传给 generateText;不设则由模型/provider 决定 */
80
102
  maxOutputTokens?: number;
103
+ /** 每轮 generateText 前的同步历史变换(§5.8);返回值成为后续轮次的正式历史 */
104
+ prepareMessages?: PrepareMessagesHook;
81
105
  redact?: RedactHook;
82
106
  lifecycle?: AgentLifecycle;
83
107
  }
@@ -100,7 +124,8 @@ export interface AgentResult {
100
124
  /**
101
125
  * 完整对话历史(user 输入 + 各轮 assistant 消息 + 工具结果),字段名与 AgentKit
102
126
  * 的 result.output 对齐——可自行 findLastIndex 等遍历(元素含 role/content)。
103
- * 恢复重放时由 memo 原样重建;启用 redact 时历史内容即脱敏后内容。
127
+ * 恢复重放时由 memo 原样重建;启用 redact 时历史内容即脱敏后内容;启用
128
+ * prepareMessages 时为裁剪后的正式历史(toolCalls 记录不受影响,始终完整)。
104
129
  */
105
130
  output: TextMessage[];
106
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 键确定。
@@ -94,14 +125,24 @@ export function createAgent(opts) {
94
125
  }
95
126
  registry.set(agent.name, agent);
96
127
  const redact = opts.redact;
128
+ const prepareMessages = opts.prepareMessages;
97
129
  const llmStepId = `agent/${agent.name}`;
98
- const messages = [{ role: "user", content: input }];
130
+ let messages = [{ role: "user", content: input }];
131
+ let prevUsage;
99
132
  const usage = { inputTokens: 0, outputTokens: 0 };
100
133
  const toolCalls = [];
101
134
  for (let i = 0;; i++) {
102
135
  if (i >= maxIterations) {
103
136
  throw new Error(`agent "${agent.name}": maxIterations (${maxIterations}) exceeded`);
104
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
+ }
105
146
  const llmMemo = await step.run(llmStepId, async () => {
106
147
  const res = await generateText({
107
148
  model: opts.model,
@@ -138,6 +179,7 @@ export function createAgent(opts) {
138
179
  assertLlmMemoShape(llmMemo, agent.name); // memo 命中路径同样校验(防御脏数据)
139
180
  usage.inputTokens += llmMemo.usage.inputTokens ?? 0;
140
181
  usage.outputTokens += llmMemo.usage.outputTokens ?? 0;
182
+ prevUsage = llmMemo.usage; // memo 命中路径同样赋值:重放时 prepareMessages 入参一致
141
183
  messages.push(...llmMemo.responseMessages);
142
184
  if (llmMemo.toolCalls.length === 0) {
143
185
  return { text: llmMemo.text, iterations: i + 1, usage, toolCalls, output: messages };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triggerlink/sdk",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "TriggerLink TypeScript SDK: durable, crash-recoverable functions for Next.js / Node.js",
5
5
  "keywords": [
6
6
  "triggerlink",