@deepwhale/coding-agent 1.0.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.
Files changed (138) hide show
  1. package/bin/deepwhale.js +299 -0
  2. package/dist/agent/agent-compaction.d.ts +74 -0
  3. package/dist/agent/agent-compaction.d.ts.map +1 -0
  4. package/dist/agent/agent-compaction.js +145 -0
  5. package/dist/agent/agent-compaction.js.map +1 -0
  6. package/dist/agent/index.d.ts +16 -0
  7. package/dist/agent/index.d.ts.map +1 -0
  8. package/dist/agent/index.js +17 -0
  9. package/dist/agent/index.js.map +1 -0
  10. package/dist/agent/session-adapter.d.ts +177 -0
  11. package/dist/agent/session-adapter.d.ts.map +1 -0
  12. package/dist/agent/session-adapter.js +340 -0
  13. package/dist/agent/session-adapter.js.map +1 -0
  14. package/dist/agent/tool-loop.d.ts +123 -0
  15. package/dist/agent/tool-loop.d.ts.map +1 -0
  16. package/dist/agent/tool-loop.js +425 -0
  17. package/dist/agent/tool-loop.js.map +1 -0
  18. package/dist/env/load-project-env.d.ts +40 -0
  19. package/dist/env/load-project-env.d.ts.map +1 -0
  20. package/dist/env/load-project-env.js +80 -0
  21. package/dist/env/load-project-env.js.map +1 -0
  22. package/dist/index.d.ts +25 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +24 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/llm-factory.d.ts +47 -0
  27. package/dist/llm-factory.d.ts.map +1 -0
  28. package/dist/llm-factory.js +88 -0
  29. package/dist/llm-factory.js.map +1 -0
  30. package/dist/modes/index.d.ts +14 -0
  31. package/dist/modes/index.d.ts.map +1 -0
  32. package/dist/modes/index.js +14 -0
  33. package/dist/modes/index.js.map +1 -0
  34. package/dist/modes/print.d.ts +50 -0
  35. package/dist/modes/print.d.ts.map +1 -0
  36. package/dist/modes/print.js +236 -0
  37. package/dist/modes/print.js.map +1 -0
  38. package/dist/modes/rpc.d.ts +52 -0
  39. package/dist/modes/rpc.d.ts.map +1 -0
  40. package/dist/modes/rpc.js +316 -0
  41. package/dist/modes/rpc.js.map +1 -0
  42. package/dist/modes/tui.d.ts +54 -0
  43. package/dist/modes/tui.d.ts.map +1 -0
  44. package/dist/modes/tui.js +361 -0
  45. package/dist/modes/tui.js.map +1 -0
  46. package/dist/policy/args-digest.d.ts +13 -0
  47. package/dist/policy/args-digest.d.ts.map +1 -0
  48. package/dist/policy/args-digest.js +29 -0
  49. package/dist/policy/args-digest.js.map +1 -0
  50. package/dist/policy/chain.d.ts +19 -0
  51. package/dist/policy/chain.d.ts.map +1 -0
  52. package/dist/policy/chain.js +24 -0
  53. package/dist/policy/chain.js.map +1 -0
  54. package/dist/policy/sanitize-reason.d.ts +11 -0
  55. package/dist/policy/sanitize-reason.d.ts.map +1 -0
  56. package/dist/policy/sanitize-reason.js +24 -0
  57. package/dist/policy/sanitize-reason.js.map +1 -0
  58. package/dist/policy/static-rules.d.ts +32 -0
  59. package/dist/policy/static-rules.d.ts.map +1 -0
  60. package/dist/policy/static-rules.js +106 -0
  61. package/dist/policy/static-rules.js.map +1 -0
  62. package/dist/policy/types.d.ts +56 -0
  63. package/dist/policy/types.d.ts.map +1 -0
  64. package/dist/policy/types.js +13 -0
  65. package/dist/policy/types.js.map +1 -0
  66. package/dist/repl/repl-confirm.d.ts +49 -0
  67. package/dist/repl/repl-confirm.d.ts.map +1 -0
  68. package/dist/repl/repl-confirm.js +88 -0
  69. package/dist/repl/repl-confirm.js.map +1 -0
  70. package/dist/repl.d.ts +126 -0
  71. package/dist/repl.d.ts.map +1 -0
  72. package/dist/repl.js +734 -0
  73. package/dist/repl.js.map +1 -0
  74. package/dist/sandbox/docker-runner.d.ts +147 -0
  75. package/dist/sandbox/docker-runner.d.ts.map +1 -0
  76. package/dist/sandbox/docker-runner.js +426 -0
  77. package/dist/sandbox/docker-runner.js.map +1 -0
  78. package/dist/sandbox/env-gate.d.ts +28 -0
  79. package/dist/sandbox/env-gate.d.ts.map +1 -0
  80. package/dist/sandbox/env-gate.js +65 -0
  81. package/dist/sandbox/env-gate.js.map +1 -0
  82. package/dist/sandbox/local-runner.d.ts +29 -0
  83. package/dist/sandbox/local-runner.d.ts.map +1 -0
  84. package/dist/sandbox/local-runner.js +79 -0
  85. package/dist/sandbox/local-runner.js.map +1 -0
  86. package/dist/sandbox/types.d.ts +80 -0
  87. package/dist/sandbox/types.d.ts.map +1 -0
  88. package/dist/sandbox/types.js +25 -0
  89. package/dist/sandbox/types.js.map +1 -0
  90. package/dist/tools/bash.d.ts +35 -0
  91. package/dist/tools/bash.d.ts.map +1 -0
  92. package/dist/tools/bash.js +233 -0
  93. package/dist/tools/bash.js.map +1 -0
  94. package/dist/tools/edit-file.d.ts +22 -0
  95. package/dist/tools/edit-file.d.ts.map +1 -0
  96. package/dist/tools/edit-file.js +79 -0
  97. package/dist/tools/edit-file.js.map +1 -0
  98. package/dist/tools/find.d.ts +21 -0
  99. package/dist/tools/find.d.ts.map +1 -0
  100. package/dist/tools/find.js +168 -0
  101. package/dist/tools/find.js.map +1 -0
  102. package/dist/tools/grep.d.ts +19 -0
  103. package/dist/tools/grep.d.ts.map +1 -0
  104. package/dist/tools/grep.js +170 -0
  105. package/dist/tools/grep.js.map +1 -0
  106. package/dist/tools/index.d.ts +10 -0
  107. package/dist/tools/index.d.ts.map +1 -0
  108. package/dist/tools/index.js +10 -0
  109. package/dist/tools/index.js.map +1 -0
  110. package/dist/tools/read-file.d.ts +18 -0
  111. package/dist/tools/read-file.d.ts.map +1 -0
  112. package/dist/tools/read-file.js +52 -0
  113. package/dist/tools/read-file.js.map +1 -0
  114. package/dist/tools/registry.d.ts +39 -0
  115. package/dist/tools/registry.d.ts.map +1 -0
  116. package/dist/tools/registry.js +67 -0
  117. package/dist/tools/registry.js.map +1 -0
  118. package/dist/tools/write-file.d.ts +18 -0
  119. package/dist/tools/write-file.d.ts.map +1 -0
  120. package/dist/tools/write-file.js +47 -0
  121. package/dist/tools/write-file.js.map +1 -0
  122. package/dist/types.d.ts +89 -0
  123. package/dist/types.d.ts.map +1 -0
  124. package/dist/types.js +5 -0
  125. package/dist/types.js.map +1 -0
  126. package/dist/verify/format-report.d.ts +57 -0
  127. package/dist/verify/format-report.d.ts.map +1 -0
  128. package/dist/verify/format-report.js +128 -0
  129. package/dist/verify/format-report.js.map +1 -0
  130. package/dist/verify/index.d.ts +8 -0
  131. package/dist/verify/index.d.ts.map +1 -0
  132. package/dist/verify/index.js +8 -0
  133. package/dist/verify/index.js.map +1 -0
  134. package/dist/verify/verify-runner.d.ts +125 -0
  135. package/dist/verify/verify-runner.d.ts.map +1 -0
  136. package/dist/verify/verify-runner.js +524 -0
  137. package/dist/verify/verify-runner.js.map +1 -0
  138. package/package.json +30 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Session ↔ Tool Loop 适配器(Sprint 1a)
3
+ *
4
+ * 把 tool loop 的 ToolLoopStep 事件 → SessionEvent JSONL 持久化,
5
+ * 同时支持从已有 JSONL 重建 messages 列表(让 LLM 看到"上次聊到哪")。
6
+ *
7
+ * Sprint 1a 范围(极简):
8
+ * - assistant step → 'assistant' event(保留 tool_calls)
9
+ * - tool step → 'tool' event(保留 tool_call_id + name + result)
10
+ * - 不持久化 user 消息(REPL 层自己 append 'user')
11
+ * - 不持久化 limit/error 事件(runtime 状态,不进 LLM context)
12
+ * - 不做 compaction、加密、压缩、分片(v1.5+)
13
+ *
14
+ * 重建 messages 规则(让 LLM 续聊):
15
+ * - 遍历 events,遇到 user/assistant/tool 都 push 成 ChatMessage
16
+ * - tool 消息需保留 tool_call_id
17
+ * - assistant 消息需保留 tool_calls
18
+ * - 系统提示由 caller 单独组装(repl.ts 拼),不存 JSONL
19
+ *
20
+ * @module @deepwhale/coding-agent/session-adapter
21
+ */
22
+ import type { ChatMessage } from '@deepwhale/llm';
23
+ import type { SessionEvent, SessionWriter, SessionReader } from '@deepwhale/core';
24
+ import type { ToolLoopStep } from './tool-loop.js';
25
+ /**
26
+ * 把 tool loop step 翻译成 SessionEvent。
27
+ *
28
+ * 只翻译能放进 LLM context 的 step:
29
+ * - 'assistant' → { kind: 'assistant', ts, content, tool_calls }
30
+ * - 'tool' → { kind: 'tool', ts, tool_call_id, name, result }
31
+ *
32
+ * limit/error 是 runtime 状态,**不**持久化(避免重启时把它们当 LLM context 喂回)。
33
+ */
34
+ export declare function toolLoopStepToSessionEvent(step: ToolLoopStep): SessionEvent | null;
35
+ /**
36
+ * 把 SessionEvent 列表重建为 LLM 的 ChatMessage 列表(用于 LLM 续聊)。
37
+ *
38
+ * Sprint 1c P2 修复: 过滤 dangling tool_call transcript.
39
+ *
40
+ * 背景: 二次启动恢复时, JSONL 里可能存在 "assistant(tool_calls=[c2]) 但
41
+ * tool(c2) 没落盘" 的孤立 assistant (crash 写完 assistant 还没写 tool result
42
+ * 就被杀). 旧实现直接 push 这个 assistant → LLM continuation 看到无对应
43
+ * tool result 的 tool_call, 形成非法 transcript, OpenAI API 拒收.
44
+ *
45
+ * 修复规则 (按 user 拍板 2026-06-04):
46
+ * 1. assistant(tool_calls): 只有在下一个 user/assistant 前, 所有
47
+ * tool_call_id 都有对应 tool event, 才保留 (整个 assistant message)
48
+ * 2. tool: 只有它的 tool_call_id 属于"当前未结算的 assistant tool_calls",
49
+ * 才保留; 否则丢 (孤儿)
50
+ * 3. 普通 assistant(content) / user 不受影响
51
+ * 4. **不改写 JSONL events**, 只在重建 messages 时过滤 — 后续补 tool
52
+ * result 后完整 tool_call 组自然重新合法化 (Sprint 1c P2 spec)
53
+ *
54
+ * 实现: 延迟 push 模式 — assistant(tool_calls) 进入 buffer, tool events
55
+ * 配对删除 pending. user/assistant 边界触发 buffer 结算: pending 非空
56
+ * → 整体 roll back (不 push); pending 空 → push buffer.
57
+ *
58
+ * Sprint 1c-revive-2-D-5+ reload 修复 (review P1, 2026-06-04):
59
+ * - JSONL 中的 'compaction' event 必须 **replay** 到 LLM context, 否则
60
+ * reload 后 messages 会从原始 user/assistant/tool events 重建, 旧
61
+ * compacted head 重新出现, 上下文不被压缩 (内存压缩成功但 reload
62
+ * 失效 = P1).
63
+ * - replay 协议: 见到 'compaction' event 时, 把当前累积 messages 的
64
+ * `replaced_range[0..replaced_range[1])` 段 (按 JSONL 累积 index)
65
+ * 替换为 1 条 system summary.
66
+ * - index 空间 (拍板 2026-06-04 review): compact() 入参 messages 来自
67
+ * caller 的 working 列表. REPL 路径下 working = loadSession() 返回
68
+ * 的 JSONL 累积 (纯 user/assistant/tool) + REPL startup 拼的 system
69
+ * prompt. 拍板 compact() 只对 working[0..end] 操作, 不含外部 system
70
+ * prompt, 所以 replaced_range 索引的就是 "JSONL 累积 messages" 的
71
+ * index. reload 时 messages 仍按 JSONL 累积重建, replaced_range 同
72
+ * index 空间, 不偏移. (多个 'compaction' event 串行: 第 1 次 applied
73
+ * 后累积 messages = 1 summary + N tail, 第 2 次 compact 入参就是
74
+ * reload 后的累积, index 重新从 0 计 — protocol 自洽.)
75
+ * - 'compaction_paused' event: 不入 messages. caller 决定是否 reset
76
+ * latch; UI/footer 可读 paused event 显式提示.
77
+ */
78
+ export declare function sessionEventsToMessages(events: ReadonlyArray<SessionEvent>): ChatMessage[];
79
+ /**
80
+ * 写一个 user event 到 session。
81
+ * Sprint 1a 简化:user 消息也走 SessionWriter(统一审计)。
82
+ */
83
+ export declare function appendUserEvent(writer: SessionWriter, content: string, ts?: number): Promise<void>;
84
+ /**
85
+ * 写一条 'compaction' event 到 session (Sprint 1c-revive-2-D-5-1 拍板).
86
+ *
87
+ * 用途: agent-compaction.ts 在 runCompactionWithLatch 返 kind='ok' 时调,
88
+ * 把 summary 拍板落盘 (供 reload 重建 messages 时知道哪段被总结过).
89
+ *
90
+ * 拍板: SessionReader 读到 kind='compaction' 时**不**重放进 LLM context
91
+ * (跟 sessionEventsToMessages L132 'system' 跳过 一致 — compaction event 是 metadata,
92
+ * 不是 LLM 看到的对话轮次).
93
+ */
94
+ export declare function appendCompactionEvent(writer: SessionWriter, summary: string, replacedRange: readonly [number, number], meta?: Record<string, unknown>, ts?: number): Promise<void>;
95
+ /**
96
+ * 写一条 'compaction_paused' event 到 session (Sprint 1c-revive-2-D-5-2 拍板).
97
+ *
98
+ * 用途: agent-compaction.ts 在 CompactionState latch 触发时调,
99
+ * 记录"自动暂停, 防 death loop" 拍板 (供 reload 时 caller 知道状态).
100
+ *
101
+ * 拍板: SessionReader 读到 kind='compaction_paused' 时**不**重放进 LLM context
102
+ * (caller 该决定是否 reset CompactionState / 改 summaryFn / 改 config).
103
+ */
104
+ export declare function appendCompactionPausedEvent(writer: SessionWriter, consecutiveFailures: number, reason: string, lastError: string, meta?: Record<string, unknown>, ts?: number): Promise<void>;
105
+ /**
106
+ * 把 tool loop 跑完后产出的 steps 全部落盘(assistant + tool)。
107
+ * limit/error 跳过(toSessionEvent 返回 null 时不 append)。
108
+ */
109
+ export declare function persistToolLoopSteps(writer: SessionWriter, steps: ReadonlyArray<ToolLoopStep>): Promise<void>;
110
+ /**
111
+ * 加载已有 session 并重建 messages。
112
+ *
113
+ * Sprint 1b: 内部自动调 reader.truncate() 把 partial last line 清掉。
114
+ * 之前 caller 必须自己记得调, Sprint 1a 全部漏调 → partial line 累积,
115
+ * 下次 append 拼坏 JSON。Sprint 1b 闭环在 adapter 里, 3 个 mode (repl/print/rpc) 自动受益。
116
+ *
117
+ * 行为契约:
118
+ * - 加载完整 events + 重建 messages
119
+ * - 若文件末尾有 partial line(崩溃恢复), 自动 truncate
120
+ * - truncate 失败不抛(不阻塞 agent 启动, 跟 Sprint 1a 容错语义一致)
121
+ *
122
+ * Sprint 1a 简化:返回 (events, messages) 两份数据, caller 决定要不要 ignore events。
123
+ */
124
+ export declare function loadSession(reader: SessionReader): Promise<{
125
+ events: ReadonlyArray<SessionEvent>;
126
+ messages: ChatMessage[];
127
+ }>;
128
+ /**
129
+ * 写一个 'verification' event 到 session (Sprint 1c-revive-2-D-11-3, 2026-06-04).
130
+ *
131
+ * 调用场景: `deepwhale --verify` 或 REPL `/verify` 跑完, 把 VerificationReport
132
+ * 摘要写 1 条到 session JSONL. 跟 `appendUserEvent` / `appendCompactionEvent` 模式一致.
133
+ *
134
+ * 字段 (跟 core/src/session/jsonl.ts 'verification' union 一致):
135
+ * - status: 整体结果 passed / failed
136
+ * - durationMs: 整体耗时
137
+ * - command_count: 跑的 step 数
138
+ * - failed_count: 失败 step 数
139
+ * - summary: 人类可读 summary (来自 formatter.buildSummaryAndNext)
140
+ * - meta: 可选扩展 (e.g. log file path, git sha)
141
+ *
142
+ * 不变量 (跟其它 event 写入一致):
143
+ * - ts 默认 Date.now(), 单测可注入
144
+ * - 走 SessionWriter.append → fsync 串行化, 顺序保证
145
+ * - 'verification' 是 metadata, reload session 时 sessionEventsToMessages 跳过
146
+ * (跟 compaction_paused 同语义), 不污染 LLM 看到的 messages
147
+ */
148
+ export declare function appendVerificationEvent(writer: SessionWriter, args: {
149
+ status: 'passed' | 'failed';
150
+ durationMs: number;
151
+ commandCount: number;
152
+ failedCount: number;
153
+ summary: string;
154
+ meta?: Record<string, unknown>;
155
+ ts?: number;
156
+ }): Promise<void>;
157
+ /**
158
+ * 写一条 'policy_decision' event (Sprint 1c-revive-3-D-13, 2026-06-05).
159
+ * 拍板 (用户 2026-06-05): 'allow' 不调本函数 (避免 JSONL 刷爆). 只对
160
+ * 'deny' / 'require_confirmation' / 'user_approved' / 'user_denied' 4 个
161
+ * 终态落盘.
162
+ *
163
+ * 字段拍板:
164
+ * - tool_call_id: 跟后续 'tool' event 配对 (reload 时 audit trace 完整)
165
+ * - argsDigest: sha256:<12hex>, 不存原始 args
166
+ * - reason: 已过 sanitize (长度 / 换行 / NUL, 拍板红线: 不存 secret)
167
+ */
168
+ export declare function appendPolicyDecisionEvent(writer: SessionWriter, args: {
169
+ tool_call_id: string;
170
+ name: string;
171
+ decision: 'deny' | 'require_confirmation' | 'user_approved' | 'user_denied';
172
+ argsDigest: string;
173
+ reason?: string;
174
+ meta?: Record<string, unknown>;
175
+ ts?: number;
176
+ }): Promise<void>;
177
+ //# sourceMappingURL=session-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-adapter.d.ts","sourceRoot":"","sources":["../../src/agent/session-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAY,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,GAAG,IAAI,CA2BlF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,WAAW,EAAE,CA+F1F;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,EACf,EAAE,GAAE,MAAmB,GACtB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,EACxC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,EAAE,GAAE,MAAmB,GACtB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;;;;GAQG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,aAAa,EACrB,mBAAmB,EAAE,MAAM,EAC3B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,EAAE,GAAE,MAAmB,GACtB,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,aAAa,CAAC,YAAY,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC;IAAE,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAAC,QAAQ,EAAE,WAAW,EAAE,CAAA;CAAE,CAAC,CAS3E;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE;IACJ,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GACA,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE;IACJ,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,sBAAsB,GAAG,eAAe,GAAG,aAAa,CAAC;IAC5E,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GACA,OAAO,CAAC,IAAI,CAAC,CAWf"}
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Session ↔ Tool Loop 适配器(Sprint 1a)
3
+ *
4
+ * 把 tool loop 的 ToolLoopStep 事件 → SessionEvent JSONL 持久化,
5
+ * 同时支持从已有 JSONL 重建 messages 列表(让 LLM 看到"上次聊到哪")。
6
+ *
7
+ * Sprint 1a 范围(极简):
8
+ * - assistant step → 'assistant' event(保留 tool_calls)
9
+ * - tool step → 'tool' event(保留 tool_call_id + name + result)
10
+ * - 不持久化 user 消息(REPL 层自己 append 'user')
11
+ * - 不持久化 limit/error 事件(runtime 状态,不进 LLM context)
12
+ * - 不做 compaction、加密、压缩、分片(v1.5+)
13
+ *
14
+ * 重建 messages 规则(让 LLM 续聊):
15
+ * - 遍历 events,遇到 user/assistant/tool 都 push 成 ChatMessage
16
+ * - tool 消息需保留 tool_call_id
17
+ * - assistant 消息需保留 tool_calls
18
+ * - 系统提示由 caller 单独组装(repl.ts 拼),不存 JSONL
19
+ *
20
+ * @module @deepwhale/coding-agent/session-adapter
21
+ */
22
+ /**
23
+ * 把 tool loop step 翻译成 SessionEvent。
24
+ *
25
+ * 只翻译能放进 LLM context 的 step:
26
+ * - 'assistant' → { kind: 'assistant', ts, content, tool_calls }
27
+ * - 'tool' → { kind: 'tool', ts, tool_call_id, name, result }
28
+ *
29
+ * limit/error 是 runtime 状态,**不**持久化(避免重启时把它们当 LLM context 喂回)。
30
+ */
31
+ export function toolLoopStepToSessionEvent(step) {
32
+ if (step.kind === 'assistant') {
33
+ return {
34
+ kind: 'assistant',
35
+ ts: step.ts,
36
+ content: step.message.content,
37
+ ...(step.message.tool_calls ? { tool_calls: [...step.message.tool_calls] } : {}),
38
+ };
39
+ }
40
+ if (step.kind === 'tool') {
41
+ return {
42
+ kind: 'tool',
43
+ ts: step.ts,
44
+ tool_call_id: step.tool_call.id,
45
+ name: step.tool_call.name,
46
+ result: {
47
+ success: step.result.success,
48
+ content: step.result.content,
49
+ ...(step.result.success === false && step.result.error !== undefined
50
+ ? { error: step.result.error }
51
+ : {}),
52
+ },
53
+ duration_ms: step.duration_ms,
54
+ };
55
+ }
56
+ // 'limit' / 'error' 不持久化
57
+ return null;
58
+ }
59
+ /**
60
+ * 把 SessionEvent 列表重建为 LLM 的 ChatMessage 列表(用于 LLM 续聊)。
61
+ *
62
+ * Sprint 1c P2 修复: 过滤 dangling tool_call transcript.
63
+ *
64
+ * 背景: 二次启动恢复时, JSONL 里可能存在 "assistant(tool_calls=[c2]) 但
65
+ * tool(c2) 没落盘" 的孤立 assistant (crash 写完 assistant 还没写 tool result
66
+ * 就被杀). 旧实现直接 push 这个 assistant → LLM continuation 看到无对应
67
+ * tool result 的 tool_call, 形成非法 transcript, OpenAI API 拒收.
68
+ *
69
+ * 修复规则 (按 user 拍板 2026-06-04):
70
+ * 1. assistant(tool_calls): 只有在下一个 user/assistant 前, 所有
71
+ * tool_call_id 都有对应 tool event, 才保留 (整个 assistant message)
72
+ * 2. tool: 只有它的 tool_call_id 属于"当前未结算的 assistant tool_calls",
73
+ * 才保留; 否则丢 (孤儿)
74
+ * 3. 普通 assistant(content) / user 不受影响
75
+ * 4. **不改写 JSONL events**, 只在重建 messages 时过滤 — 后续补 tool
76
+ * result 后完整 tool_call 组自然重新合法化 (Sprint 1c P2 spec)
77
+ *
78
+ * 实现: 延迟 push 模式 — assistant(tool_calls) 进入 buffer, tool events
79
+ * 配对删除 pending. user/assistant 边界触发 buffer 结算: pending 非空
80
+ * → 整体 roll back (不 push); pending 空 → push buffer.
81
+ *
82
+ * Sprint 1c-revive-2-D-5+ reload 修复 (review P1, 2026-06-04):
83
+ * - JSONL 中的 'compaction' event 必须 **replay** 到 LLM context, 否则
84
+ * reload 后 messages 会从原始 user/assistant/tool events 重建, 旧
85
+ * compacted head 重新出现, 上下文不被压缩 (内存压缩成功但 reload
86
+ * 失效 = P1).
87
+ * - replay 协议: 见到 'compaction' event 时, 把当前累积 messages 的
88
+ * `replaced_range[0..replaced_range[1])` 段 (按 JSONL 累积 index)
89
+ * 替换为 1 条 system summary.
90
+ * - index 空间 (拍板 2026-06-04 review): compact() 入参 messages 来自
91
+ * caller 的 working 列表. REPL 路径下 working = loadSession() 返回
92
+ * 的 JSONL 累积 (纯 user/assistant/tool) + REPL startup 拼的 system
93
+ * prompt. 拍板 compact() 只对 working[0..end] 操作, 不含外部 system
94
+ * prompt, 所以 replaced_range 索引的就是 "JSONL 累积 messages" 的
95
+ * index. reload 时 messages 仍按 JSONL 累积重建, replaced_range 同
96
+ * index 空间, 不偏移. (多个 'compaction' event 串行: 第 1 次 applied
97
+ * 后累积 messages = 1 summary + N tail, 第 2 次 compact 入参就是
98
+ * reload 后的累积, index 重新从 0 计 — protocol 自洽.)
99
+ * - 'compaction_paused' event: 不入 messages. caller 决定是否 reset
100
+ * latch; UI/footer 可读 paused event 显式提示.
101
+ */
102
+ export function sessionEventsToMessages(events) {
103
+ // Sprint 1c P2 修复: 过滤 dangling tool_call transcript. 延迟 push 模式
104
+ // — assistant(tool_calls) 进入 buffer, tool events 配对删除 pending.
105
+ // user/assistant 边界触发 flushBuffer 结算: pending 非空 → 整体 roll
106
+ // back; pending 空 → push buffer.
107
+ const out = [];
108
+ let buffer = [];
109
+ let pendingToolCalls = new Set();
110
+ const flushBuffer = () => {
111
+ if (pendingToolCalls.size > 0) {
112
+ // 上一个 assistant(tool_calls) 未完成 (user/assistant 边界), 整个 roll back
113
+ buffer = [];
114
+ }
115
+ else {
116
+ out.push(...buffer);
117
+ buffer = [];
118
+ }
119
+ pendingToolCalls = new Set();
120
+ };
121
+ for (const ev of events) {
122
+ if (ev.kind === 'user') {
123
+ flushBuffer();
124
+ out.push({ role: 'user', content: ev.content });
125
+ }
126
+ else if (ev.kind === 'assistant') {
127
+ flushBuffer();
128
+ if (ev.tool_calls && ev.tool_calls.length > 0) {
129
+ // 进入延迟 push 模式
130
+ pendingToolCalls = new Set(ev.tool_calls.map((tc) => tc.id));
131
+ const msg = { role: 'assistant', content: ev.content };
132
+ msg.tool_calls = [...ev.tool_calls];
133
+ buffer.push(msg);
134
+ }
135
+ else {
136
+ // 普通 assistant(content) 无 tool_calls, 立即 commit
137
+ out.push({ role: 'assistant', content: ev.content });
138
+ }
139
+ }
140
+ else if (ev.kind === 'tool') {
141
+ if (pendingToolCalls.has(ev.tool_call_id)) {
142
+ pendingToolCalls.delete(ev.tool_call_id);
143
+ buffer.push({
144
+ role: 'tool',
145
+ content: ev.result.content,
146
+ tool_call_id: ev.tool_call_id,
147
+ name: ev.name,
148
+ });
149
+ }
150
+ // 孤儿 tool (没匹配 assistant tool_call): 丢 — 不会出现无主 tool message
151
+ }
152
+ else if (ev.kind === 'compaction') {
153
+ // Sprint 1c-revive-2-D-5+ (review P1 修复, 2026-06-04): replay 到 messages.
154
+ // 协议 (see header comment L88-L102 for index space):
155
+ // 1. flushBuffer first, to flush any un-settled assistant(tool_calls)
156
+ // and prevent a dangling transcript (tool_call mid-way + compaction
157
+ // event = invalid half-transcript).
158
+ // 2. replaced_range is the JSONL-accumulated index space, equal to
159
+ // the current out accumulation.
160
+ // 3. Splice out[start..end) and insert 1 system summary at position
161
+ // start.
162
+ // 容错 (safe-fail):
163
+ // - start > out.length: skip (replaced_range is the trailing-tail
164
+ // index, already covered by a prior compaction; this event
165
+ // shouldn't occur unless JSONL was hand-edited, we take the
166
+ // safe path).
167
+ // - end > out.length but start <= out.length: splice out the
168
+ // remaining tail and insert summary at position start (a new
169
+ // user event may have pushed the tail off the end).
170
+ // - start < 0 or end < start: skip (corrupt event).
171
+ // 'compaction_paused' event: does NOT enter messages. The caller
172
+ // decides whether to reset the latch; UI/footer reads paused event
173
+ // explicitly for status display.
174
+ flushBuffer();
175
+ const [start, end] = ev.replaced_range;
176
+ if (start < 0 || end < start)
177
+ continue; // 损坏 event
178
+ if (start > out.length)
179
+ continue; // out 没积累到, 跳过 (见上)
180
+ const removeCount = Math.min(end - start, out.length - start);
181
+ out.splice(start, removeCount, {
182
+ role: 'system',
183
+ content: `[Session compaction summary]\n${ev.summary}`,
184
+ });
185
+ }
186
+ // 'system' / 'compaction_paused' / 'verification' / 'policy_decision' 跳过 — 4 种都是 metadata, 不进 LLM context:
187
+ // - 'system' caller 决定要不要用
188
+ // - 'compaction_paused' UI/footer 显式读
189
+ // - 'verification' (Sprint 1c-revive-2-D-11-3, 2026-06-04) audit log / viewer 显式读
190
+ // reload session 时验证历史不污染 LLM 看到的 messages 列表
191
+ // - 'policy_decision' (Sprint 1c-revive-3-D-13, 2026-06-05) tool policy 决策审计, 同 verification
192
+ // 不进 LLM context. 'allow' 不写 (避免 JSONL 刷爆), 只 deny / require_confirmation /
193
+ // user_approved / user_denied 4 个终态落盘, 跟 verification 同 metadata 拍板.
194
+ }
195
+ // EOF: 调 flushBuffer 而非硬清空.
196
+ // 修复: 旧实现 `buffer = []` 把"assistant(tool_calls) → tool 已配对完成但
197
+ // final assistant 还没落盘" 的合法 transcript 也丢了 — 这种 crash 真实存在
198
+ // (工具结果已 fsync, 进程在生成最终回答前被杀, LLM 续聊需要看到 tool result
199
+ // 才能继续生成). 现在 EOF 走 flushBuffer, pending 空时正常 push buffer.
200
+ flushBuffer();
201
+ return out;
202
+ }
203
+ /**
204
+ * 写一个 user event 到 session。
205
+ * Sprint 1a 简化:user 消息也走 SessionWriter(统一审计)。
206
+ */
207
+ export async function appendUserEvent(writer, content, ts = Date.now()) {
208
+ await writer.append({ kind: 'user', ts, content });
209
+ }
210
+ /**
211
+ * 写一条 'compaction' event 到 session (Sprint 1c-revive-2-D-5-1 拍板).
212
+ *
213
+ * 用途: agent-compaction.ts 在 runCompactionWithLatch 返 kind='ok' 时调,
214
+ * 把 summary 拍板落盘 (供 reload 重建 messages 时知道哪段被总结过).
215
+ *
216
+ * 拍板: SessionReader 读到 kind='compaction' 时**不**重放进 LLM context
217
+ * (跟 sessionEventsToMessages L132 'system' 跳过 一致 — compaction event 是 metadata,
218
+ * 不是 LLM 看到的对话轮次).
219
+ */
220
+ export async function appendCompactionEvent(writer, summary, replacedRange, meta, ts = Date.now()) {
221
+ await writer.append({
222
+ kind: 'compaction',
223
+ ts,
224
+ summary,
225
+ replaced_range: replacedRange,
226
+ ...(meta !== undefined ? { meta } : {}),
227
+ });
228
+ }
229
+ /**
230
+ * 写一条 'compaction_paused' event 到 session (Sprint 1c-revive-2-D-5-2 拍板).
231
+ *
232
+ * 用途: agent-compaction.ts 在 CompactionState latch 触发时调,
233
+ * 记录"自动暂停, 防 death loop" 拍板 (供 reload 时 caller 知道状态).
234
+ *
235
+ * 拍板: SessionReader 读到 kind='compaction_paused' 时**不**重放进 LLM context
236
+ * (caller 该决定是否 reset CompactionState / 改 summaryFn / 改 config).
237
+ */
238
+ export async function appendCompactionPausedEvent(writer, consecutiveFailures, reason, lastError, meta, ts = Date.now()) {
239
+ await writer.append({
240
+ kind: 'compaction_paused',
241
+ ts,
242
+ consecutive_failures: consecutiveFailures,
243
+ reason,
244
+ last_error: lastError,
245
+ ...(meta !== undefined ? { meta } : {}),
246
+ });
247
+ }
248
+ /**
249
+ * 把 tool loop 跑完后产出的 steps 全部落盘(assistant + tool)。
250
+ * limit/error 跳过(toSessionEvent 返回 null 时不 append)。
251
+ */
252
+ export async function persistToolLoopSteps(writer, steps) {
253
+ for (const step of steps) {
254
+ const ev = toolLoopStepToSessionEvent(step);
255
+ if (ev !== null) {
256
+ await writer.append(ev);
257
+ }
258
+ }
259
+ }
260
+ /**
261
+ * 加载已有 session 并重建 messages。
262
+ *
263
+ * Sprint 1b: 内部自动调 reader.truncate() 把 partial last line 清掉。
264
+ * 之前 caller 必须自己记得调, Sprint 1a 全部漏调 → partial line 累积,
265
+ * 下次 append 拼坏 JSON。Sprint 1b 闭环在 adapter 里, 3 个 mode (repl/print/rpc) 自动受益。
266
+ *
267
+ * 行为契约:
268
+ * - 加载完整 events + 重建 messages
269
+ * - 若文件末尾有 partial line(崩溃恢复), 自动 truncate
270
+ * - truncate 失败不抛(不阻塞 agent 启动, 跟 Sprint 1a 容错语义一致)
271
+ *
272
+ * Sprint 1a 简化:返回 (events, messages) 两份数据, caller 决定要不要 ignore events。
273
+ */
274
+ export async function loadSession(reader) {
275
+ const events = await reader.readAll();
276
+ // Sprint 1b: 闭环 truncate, 防止 partial line 累积污染下次 append
277
+ try {
278
+ await reader.truncate();
279
+ }
280
+ catch {
281
+ // truncate 失败不阻塞启动(可能是权限/磁盘满等, 但 events 已读到内存)
282
+ }
283
+ return { events, messages: sessionEventsToMessages(events) };
284
+ }
285
+ /**
286
+ * 写一个 'verification' event 到 session (Sprint 1c-revive-2-D-11-3, 2026-06-04).
287
+ *
288
+ * 调用场景: `deepwhale --verify` 或 REPL `/verify` 跑完, 把 VerificationReport
289
+ * 摘要写 1 条到 session JSONL. 跟 `appendUserEvent` / `appendCompactionEvent` 模式一致.
290
+ *
291
+ * 字段 (跟 core/src/session/jsonl.ts 'verification' union 一致):
292
+ * - status: 整体结果 passed / failed
293
+ * - durationMs: 整体耗时
294
+ * - command_count: 跑的 step 数
295
+ * - failed_count: 失败 step 数
296
+ * - summary: 人类可读 summary (来自 formatter.buildSummaryAndNext)
297
+ * - meta: 可选扩展 (e.g. log file path, git sha)
298
+ *
299
+ * 不变量 (跟其它 event 写入一致):
300
+ * - ts 默认 Date.now(), 单测可注入
301
+ * - 走 SessionWriter.append → fsync 串行化, 顺序保证
302
+ * - 'verification' 是 metadata, reload session 时 sessionEventsToMessages 跳过
303
+ * (跟 compaction_paused 同语义), 不污染 LLM 看到的 messages
304
+ */
305
+ export async function appendVerificationEvent(writer, args) {
306
+ await writer.append({
307
+ kind: 'verification',
308
+ ts: args.ts ?? Date.now(),
309
+ status: args.status,
310
+ durationMs: args.durationMs,
311
+ command_count: args.commandCount,
312
+ failed_count: args.failedCount,
313
+ summary: args.summary,
314
+ ...(args.meta !== undefined ? { meta: args.meta } : {}),
315
+ });
316
+ }
317
+ /**
318
+ * 写一条 'policy_decision' event (Sprint 1c-revive-3-D-13, 2026-06-05).
319
+ * 拍板 (用户 2026-06-05): 'allow' 不调本函数 (避免 JSONL 刷爆). 只对
320
+ * 'deny' / 'require_confirmation' / 'user_approved' / 'user_denied' 4 个
321
+ * 终态落盘.
322
+ *
323
+ * 字段拍板:
324
+ * - tool_call_id: 跟后续 'tool' event 配对 (reload 时 audit trace 完整)
325
+ * - argsDigest: sha256:<12hex>, 不存原始 args
326
+ * - reason: 已过 sanitize (长度 / 换行 / NUL, 拍板红线: 不存 secret)
327
+ */
328
+ export async function appendPolicyDecisionEvent(writer, args) {
329
+ await writer.append({
330
+ kind: 'policy_decision',
331
+ ts: args.ts ?? Date.now(),
332
+ tool_call_id: args.tool_call_id,
333
+ name: args.name,
334
+ decision: args.decision,
335
+ argsDigest: args.argsDigest,
336
+ ...(args.reason !== undefined ? { reason: args.reason } : {}),
337
+ ...(args.meta !== undefined ? { meta: args.meta } : {}),
338
+ });
339
+ }
340
+ //# sourceMappingURL=session-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-adapter.js","sourceRoot":"","sources":["../../src/agent/session-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Tool Loop — minimal agent loop (Sprint 1a)
3
+ *
4
+ * 协议(Codex/oh-my-pi 借鉴):
5
+ * 1. 把 system + history 喂给 LLM
6
+ * 2. 如果 LLM 返回 tool_calls:
7
+ * - 用 ToolRegistry 调工具(schema 校验 Sprint 1b 加)
8
+ * - 把结果作为 tool 消息 push 回 messages
9
+ * - 回到 (1)
10
+ * 3. 如果 LLM 返回 content(finish_reason='stop')→ 终结
11
+ * 4. 如果 max_steps 触顶 → 抛 ToolLoopLimitError
12
+ *
13
+ * Sprint 1a 范围(极简):
14
+ * - 同步(不并发 tool_calls;DeepSeek V4 Flash 一次只调 1 个常见)
15
+ * - 不做 budget cap(v2.0)
16
+ * - 不做 schema 校验(trust LLM 输出,argparse 失败时 tool 返回 error 即可)
17
+ * - 不做 plan mode(v2.5)
18
+ * - 不做 cost accounting(1b 加)
19
+ * - 流式:可选 onChunk 回调(REPL 用)
20
+ *
21
+ * 错误处理:
22
+ * - tool 自身失败 → tool 消息 content 是 error,loop 继续
23
+ * - LLM 失败 → 抛 LLMError(caller 决定是否 retry/终止)
24
+ * - max_steps 触顶 → 抛 ToolLoopLimitError(caller 决定是否放弃)
25
+ *
26
+ * @module @deepwhale/coding-agent/agent
27
+ */
28
+ import type { ChatMessage, ChatResult, LLMClient, ToolCall } from '@deepwhale/llm';
29
+ import type { ToolResult } from '../types.js';
30
+ import type { ToolRegistry } from '../tools/registry.js';
31
+ import type { ToolPolicy } from '../policy/types.js';
32
+ import type { SessionWriter } from '@deepwhale/core';
33
+ /** Sprint 1a 默认:跟 LLM 来回 5 轮(够用 coding agent 短任务,长任务 caller 调高)。 */
34
+ export declare const TOOL_LOOP_DEFAULT_MAX_STEPS = 5;
35
+ export interface ToolLoopOptions {
36
+ /** 注入工具注册表(默认 createDefaultRegistry())。 */
37
+ registry?: ToolRegistry;
38
+ /** 上限:单次 loop LLM call 次数(含 tool_calls 触发的回传)。默认 5。 */
39
+ maxSteps?: number;
40
+ /** 给所有 tool 调用的 timeout ms。Sprint 1a 不支持单 tool 自定义 timeout。 */
41
+ toolTimeoutMs?: number;
42
+ /** 流式 chunk 回调(null/undefined = 不流式)。REPL 接 onChunk 实时打印。 */
43
+ onChunk?: (chunk: {
44
+ content?: string;
45
+ tool_calls?: ReadonlyArray<ToolCall>;
46
+ }) => void;
47
+ /** 外部 abort signal(Ctrl-C / session 结束)。 */
48
+ signal?: AbortSignal;
49
+ /**
50
+ * Sprint 1c-revive-3-D-13 (2026-06-05): tool call policy.
51
+ * 默认 staticToolPolicy. 显式传 null = 不检查 (单测用).
52
+ * 拍板: 'allow' 不写 session, 只有 deny / require_confirmation 落 policy_decision.
53
+ */
54
+ policy?: ToolPolicy | null;
55
+ /** Sprint 1c-revive-3-D-13: 模式是否可交互 (REPL = true, print/rpc 默认 = false). */
56
+ isInteractive?: boolean;
57
+ /** Sprint 1c-revive-3-D-13: --yes 标志. yes=true bypass require_confirmation, 不 bypass deny. */
58
+ yes?: boolean;
59
+ /** Sprint 1c-revive-3-D-13: session writer 注入 (写 policy_decision event 用). */
60
+ writer?: SessionWriter | null;
61
+ }
62
+ export type ToolLoopStep = {
63
+ kind: 'assistant';
64
+ ts: number;
65
+ message: ChatMessage;
66
+ result: ChatResult;
67
+ } | {
68
+ kind: 'tool';
69
+ ts: number;
70
+ tool_call: ToolCall;
71
+ result: ToolResult;
72
+ duration_ms: number;
73
+ } | {
74
+ kind: 'limit';
75
+ ts: number;
76
+ steps: number;
77
+ lastResult: ChatResult;
78
+ } | {
79
+ kind: 'error';
80
+ ts: number;
81
+ error: Error;
82
+ };
83
+ export interface ToolLoopResult {
84
+ /** 全部 messages(含 tool_calls 步骤),caller 用来继续下一轮。 */
85
+ messages: ChatMessage[];
86
+ /** 最后一次 assistant ChatResult(content + tool_calls + usage)。 */
87
+ final: ChatResult;
88
+ /**
89
+ * 完整 step 序列(含 tool 调用、limit、error),caller 用来回放/审计/持久化。
90
+ * Sprint 1a 极简:caller 通过 runToolLoop 后读取 .steps 自己落盘。
91
+ * Sprint 1b 加 onStep 实时回调(避免 caller 等跑完才落)。
92
+ */
93
+ steps: ToolLoopStep[];
94
+ }
95
+ /**
96
+ * 错误:maxSteps 触顶、LLM 一直在调工具不收敛。
97
+ * caller 通常应当:缩短 prompt / 拆任务 / 主动放弃。
98
+ */
99
+ export declare class ToolLoopLimitError extends Error {
100
+ readonly steps: number;
101
+ readonly lastResult: ChatResult;
102
+ readonly name: "ToolLoopLimitError";
103
+ readonly isToolLoopError: true;
104
+ constructor(steps: number, lastResult: ChatResult);
105
+ }
106
+ /** 类型守卫。 */
107
+ export declare function isToolLoopError(err: unknown): err is ToolLoopLimitError;
108
+ /**
109
+ * 执行一轮 tool loop。
110
+ *
111
+ * 行为契约:
112
+ * - 不修改输入 messages(immutable),返回**新**数组(带 tool 步骤)
113
+ * - 任何 LLM 调用失败 → 抛 LLMError(ToolLoopStep.error 事件也写入 steps)
114
+ * - tool 自身失败 → 不抛,包成 tool 消息的 error content 继续
115
+ * - maxSteps 触顶 → 抛 ToolLoopLimitError(steps 同时写入)
116
+ * - 外部 abort → 抛带 cause 的 LLMUnknownError(包装 AbortError)
117
+ *
118
+ * Sprint 1a 限制:
119
+ * - 只支持非流式调用时 onChunk=null;有 onChunk 时走 stream()。
120
+ * - onChunk 触发的内容**也**写入最终 final.content(assembled)
121
+ */
122
+ export declare function runToolLoop(client: LLMClient, messages: ReadonlyArray<ChatMessage>, options?: ToolLoopOptions): Promise<ToolLoopResult>;
123
+ //# sourceMappingURL=tool-loop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-loop.d.ts","sourceRoot":"","sources":["../../src/agent/tool-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAiB,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAGlG,OAAO,KAAK,EAAQ,UAAU,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAMrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAErD,oEAAoE;AACpE,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C,MAAM,WAAW,eAAe;IAC9B,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IACtF,4CAA4C;IAC5C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC3B,4EAA4E;IAC5E,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,8FAA8F;IAC9F,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,8EAA8E;IAC9E,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,UAAU,CAAA;CAAE,GAC3E;IACE,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,QAAQ,CAAC;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB,GACD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAEhD,MAAM,WAAW,cAAc;IAC7B,mDAAmD;IACnD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,+DAA+D;IAC/D,KAAK,EAAE,UAAU,CAAC;IAClB;;;;OAIG;IACH,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;aAIzB,KAAK,EAAE,MAAM;aACb,UAAU,EAAE,UAAU;IAJxC,SAAkB,IAAI,EAAG,oBAAoB,CAAU;IACvD,QAAQ,CAAC,eAAe,EAAG,IAAI,CAAU;gBAEvB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,UAAU;CAIzC;AAED,YAAY;AACZ,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,kBAAkB,CAEvE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,aAAa,CAAC,WAAW,CAAC,EACpC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAqFzB"}