qlogicagent 0.2.1 → 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.
Files changed (226) hide show
  1. package/README.md +45 -45
  2. package/package.json +56 -42
  3. package/dist/agent/agent.d.ts +0 -43
  4. package/dist/agent/agent.js +0 -113
  5. package/dist/agent/tool-loop.d.ts +0 -64
  6. package/dist/agent/tool-loop.js +0 -575
  7. package/dist/agent/types.d.ts +0 -175
  8. package/dist/agent/types.js +0 -14
  9. package/dist/cli/main.d.ts +0 -11
  10. package/dist/cli/main.js +0 -23
  11. package/dist/cli/stdio-server.d.ts +0 -45
  12. package/dist/cli/stdio-server.js +0 -463
  13. package/dist/config/config.d.ts +0 -17
  14. package/dist/config/config.js +0 -21
  15. package/dist/contracts/hooks.d.ts +0 -120
  16. package/dist/contracts/hooks.js +0 -7
  17. package/dist/contracts/index.d.ts +0 -10
  18. package/dist/contracts/index.js +0 -10
  19. package/dist/contracts/planner.d.ts +0 -35
  20. package/dist/contracts/planner.js +0 -2
  21. package/dist/contracts/skill-candidate.d.ts +0 -63
  22. package/dist/contracts/skill-candidate.js +0 -195
  23. package/dist/contracts/todo.d.ts +0 -14
  24. package/dist/contracts/todo.js +0 -9
  25. package/dist/index.d.ts +0 -13
  26. package/dist/index.js +0 -15
  27. package/dist/llm/builtin-providers.d.ts +0 -10
  28. package/dist/llm/builtin-providers.js +0 -531
  29. package/dist/llm/index.d.ts +0 -15
  30. package/dist/llm/index.js +0 -14
  31. package/dist/llm/llm-client.d.ts +0 -43
  32. package/dist/llm/llm-client.js +0 -67
  33. package/dist/llm/model-catalog.d.ts +0 -53
  34. package/dist/llm/model-catalog.js +0 -191
  35. package/dist/llm/provider-def.d.ts +0 -59
  36. package/dist/llm/provider-def.js +0 -12
  37. package/dist/llm/provider-registry.d.ts +0 -54
  38. package/dist/llm/provider-registry.js +0 -147
  39. package/dist/llm/transport.d.ts +0 -62
  40. package/dist/llm/transport.js +0 -27
  41. package/dist/llm/transports/anthropic-messages.d.ts +0 -31
  42. package/dist/llm/transports/anthropic-messages.js +0 -293
  43. package/dist/llm/transports/openai-chat.d.ts +0 -36
  44. package/dist/llm/transports/openai-chat.js +0 -165
  45. package/dist/orchestration/agent-registry.d.ts +0 -41
  46. package/dist/orchestration/agent-registry.js +0 -116
  47. package/dist/orchestration/approval-aware-tool-plan.d.ts +0 -32
  48. package/dist/orchestration/approval-aware-tool-plan.js +0 -87
  49. package/dist/orchestration/context-compression.d.ts +0 -220
  50. package/dist/orchestration/context-compression.js +0 -583
  51. package/dist/orchestration/conversation-repair.d.ts +0 -61
  52. package/dist/orchestration/conversation-repair.js +0 -429
  53. package/dist/orchestration/curator-scheduler.d.ts +0 -119
  54. package/dist/orchestration/curator-scheduler.js +0 -135
  55. package/dist/orchestration/embedded-failover-policy.d.ts +0 -110
  56. package/dist/orchestration/embedded-failover-policy.js +0 -168
  57. package/dist/orchestration/error-classification.d.ts +0 -12
  58. package/dist/orchestration/error-classification.js +0 -77
  59. package/dist/orchestration/failover-classification.d.ts +0 -8
  60. package/dist/orchestration/failover-classification.js +0 -381
  61. package/dist/orchestration/failover-error.d.ts +0 -33
  62. package/dist/orchestration/failover-error.js +0 -198
  63. package/dist/orchestration/fork-subagent.d.ts +0 -100
  64. package/dist/orchestration/fork-subagent.js +0 -98
  65. package/dist/orchestration/index.d.ts +0 -120
  66. package/dist/orchestration/index.js +0 -267
  67. package/dist/orchestration/memory-flush-policy.d.ts +0 -57
  68. package/dist/orchestration/memory-flush-policy.js +0 -85
  69. package/dist/orchestration/memory-provider.d.ts +0 -14
  70. package/dist/orchestration/memory-provider.js +0 -2
  71. package/dist/orchestration/parallel-tool-calls.d.ts +0 -41
  72. package/dist/orchestration/parallel-tool-calls.js +0 -59
  73. package/dist/orchestration/prompt-cache-strategy.d.ts +0 -126
  74. package/dist/orchestration/prompt-cache-strategy.js +0 -228
  75. package/dist/orchestration/reactive-compact.d.ts +0 -73
  76. package/dist/orchestration/reactive-compact.js +0 -78
  77. package/dist/orchestration/retry-loop.d.ts +0 -22
  78. package/dist/orchestration/retry-loop.js +0 -24
  79. package/dist/orchestration/skill-candidate.d.ts +0 -52
  80. package/dist/orchestration/skill-candidate.js +0 -141
  81. package/dist/orchestration/skill-consolidation.d.ts +0 -123
  82. package/dist/orchestration/skill-consolidation.js +0 -220
  83. package/dist/orchestration/skill-improvement.d.ts +0 -59
  84. package/dist/orchestration/skill-improvement.js +0 -66
  85. package/dist/orchestration/skill-similarity.d.ts +0 -98
  86. package/dist/orchestration/skill-similarity.js +0 -131
  87. package/dist/orchestration/streaming-tool-executor.d.ts +0 -73
  88. package/dist/orchestration/streaming-tool-executor.js +0 -96
  89. package/dist/orchestration/team-orchestration.d.ts +0 -195
  90. package/dist/orchestration/team-orchestration.js +0 -369
  91. package/dist/orchestration/team-tool-loop-wiring.d.ts +0 -92
  92. package/dist/orchestration/team-tool-loop-wiring.js +0 -147
  93. package/dist/orchestration/tool-choice-policy.d.ts +0 -54
  94. package/dist/orchestration/tool-choice-policy.js +0 -164
  95. package/dist/orchestration/tool-loop-state.d.ts +0 -50
  96. package/dist/orchestration/tool-loop-state.js +0 -133
  97. package/dist/orchestration/tool-schema.d.ts +0 -39
  98. package/dist/orchestration/tool-schema.js +0 -297
  99. package/dist/orchestration/transcript-repair.d.ts +0 -42
  100. package/dist/orchestration/transcript-repair.js +0 -426
  101. package/dist/orchestration/turn-loop-guard.d.ts +0 -86
  102. package/dist/orchestration/turn-loop-guard.js +0 -92
  103. package/dist/orchestration/web-browser-policy.d.ts +0 -17
  104. package/dist/orchestration/web-browser-policy.js +0 -39
  105. package/dist/runtime/context-compression.d.ts +0 -61
  106. package/dist/runtime/context-compression.js +0 -274
  107. package/dist/runtime/hook-registry.d.ts +0 -12
  108. package/dist/runtime/hook-registry.js +0 -53
  109. package/dist/runtime/memory-hooks.d.ts +0 -23
  110. package/dist/runtime/memory-hooks.js +0 -65
  111. package/dist/runtime/tool-eligibility.d.ts +0 -59
  112. package/dist/runtime/tool-eligibility.js +0 -111
  113. package/dist/skills/index.d.ts +0 -108
  114. package/dist/skills/index.js +0 -82
  115. package/dist/skills/memory-extractor.d.ts +0 -64
  116. package/dist/skills/memory-extractor.js +0 -173
  117. package/dist/skills/memory-query-tool.d.ts +0 -43
  118. package/dist/skills/memory-query-tool.js +0 -127
  119. package/dist/skills/memory-store.d.ts +0 -66
  120. package/dist/skills/memory-store.js +0 -228
  121. package/dist/skills/memory-tool.d.ts +0 -67
  122. package/dist/skills/memory-tool.js +0 -192
  123. package/dist/skills/portable-tool.d.ts +0 -71
  124. package/dist/skills/portable-tool.js +0 -14
  125. package/dist/skills/qmemory-adapter.d.ts +0 -52
  126. package/dist/skills/qmemory-adapter.js +0 -165
  127. package/dist/skills/skill-frontmatter.d.ts +0 -19
  128. package/dist/skills/skill-frontmatter.js +0 -344
  129. package/dist/skills/skill-guard.d.ts +0 -23
  130. package/dist/skills/skill-guard.js +0 -229
  131. package/dist/skills/skill-loader.d.ts +0 -16
  132. package/dist/skills/skill-loader.js +0 -303
  133. package/dist/skills/skill-source.d.ts +0 -119
  134. package/dist/skills/skill-source.js +0 -126
  135. package/dist/skills/skill-types.d.ts +0 -199
  136. package/dist/skills/skill-types.js +0 -6
  137. package/dist/skills/think-tool.d.ts +0 -16
  138. package/dist/skills/think-tool.js +0 -59
  139. package/dist/skills/todo-tool.d.ts +0 -63
  140. package/dist/skills/todo-tool.js +0 -114
  141. package/dist/skills/tools/agent-tool.d.ts +0 -91
  142. package/dist/skills/tools/agent-tool.js +0 -142
  143. package/dist/skills/tools/apply-patch-tool.d.ts +0 -29
  144. package/dist/skills/tools/apply-patch-tool.js +0 -184
  145. package/dist/skills/tools/ask-user-tool.d.ts +0 -80
  146. package/dist/skills/tools/ask-user-tool.js +0 -121
  147. package/dist/skills/tools/brief-tool.d.ts +0 -74
  148. package/dist/skills/tools/brief-tool.js +0 -95
  149. package/dist/skills/tools/browser-tool.d.ts +0 -114
  150. package/dist/skills/tools/browser-tool.js +0 -155
  151. package/dist/skills/tools/checkpoint-tool.d.ts +0 -66
  152. package/dist/skills/tools/checkpoint-tool.js +0 -102
  153. package/dist/skills/tools/config-tool.d.ts +0 -63
  154. package/dist/skills/tools/config-tool.js +0 -143
  155. package/dist/skills/tools/cron-tool.d.ts +0 -116
  156. package/dist/skills/tools/cron-tool.js +0 -175
  157. package/dist/skills/tools/edit-tool.d.ts +0 -43
  158. package/dist/skills/tools/edit-tool.js +0 -70
  159. package/dist/skills/tools/exec-tool.d.ts +0 -102
  160. package/dist/skills/tools/exec-tool.js +0 -133
  161. package/dist/skills/tools/image-generate-tool.d.ts +0 -62
  162. package/dist/skills/tools/image-generate-tool.js +0 -67
  163. package/dist/skills/tools/instructions-tool.d.ts +0 -103
  164. package/dist/skills/tools/instructions-tool.js +0 -187
  165. package/dist/skills/tools/lsp-tool.d.ts +0 -153
  166. package/dist/skills/tools/lsp-tool.js +0 -227
  167. package/dist/skills/tools/mcp-client-types.d.ts +0 -269
  168. package/dist/skills/tools/mcp-client-types.js +0 -53
  169. package/dist/skills/tools/mcp-tool.d.ts +0 -249
  170. package/dist/skills/tools/mcp-tool.js +0 -503
  171. package/dist/skills/tools/memory-tool.d.ts +0 -74
  172. package/dist/skills/tools/memory-tool.js +0 -88
  173. package/dist/skills/tools/monitor-tool.d.ts +0 -113
  174. package/dist/skills/tools/monitor-tool.js +0 -131
  175. package/dist/skills/tools/music-generate-tool.d.ts +0 -55
  176. package/dist/skills/tools/music-generate-tool.js +0 -62
  177. package/dist/skills/tools/notify-tool.d.ts +0 -53
  178. package/dist/skills/tools/notify-tool.js +0 -62
  179. package/dist/skills/tools/patch-tool.d.ts +0 -45
  180. package/dist/skills/tools/patch-tool.js +0 -505
  181. package/dist/skills/tools/pdf-tool.d.ts +0 -66
  182. package/dist/skills/tools/pdf-tool.js +0 -88
  183. package/dist/skills/tools/plan-mode-tool.d.ts +0 -59
  184. package/dist/skills/tools/plan-mode-tool.js +0 -122
  185. package/dist/skills/tools/read-tool.d.ts +0 -51
  186. package/dist/skills/tools/read-tool.js +0 -84
  187. package/dist/skills/tools/repl-tool.d.ts +0 -70
  188. package/dist/skills/tools/repl-tool.js +0 -69
  189. package/dist/skills/tools/search-tool.d.ts +0 -112
  190. package/dist/skills/tools/search-tool.js +0 -225
  191. package/dist/skills/tools/send-message-tool.d.ts +0 -51
  192. package/dist/skills/tools/send-message-tool.js +0 -76
  193. package/dist/skills/tools/skill-list-tool.d.ts +0 -33
  194. package/dist/skills/tools/skill-list-tool.js +0 -54
  195. package/dist/skills/tools/skill-manage-tool.d.ts +0 -73
  196. package/dist/skills/tools/skill-manage-tool.js +0 -153
  197. package/dist/skills/tools/skill-view-tool.d.ts +0 -37
  198. package/dist/skills/tools/skill-view-tool.js +0 -72
  199. package/dist/skills/tools/sleep-tool.d.ts +0 -49
  200. package/dist/skills/tools/sleep-tool.js +0 -81
  201. package/dist/skills/tools/structured-output-tool.d.ts +0 -116
  202. package/dist/skills/tools/structured-output-tool.js +0 -176
  203. package/dist/skills/tools/task-tool.d.ts +0 -104
  204. package/dist/skills/tools/task-tool.js +0 -161
  205. package/dist/skills/tools/team-tool.d.ts +0 -89
  206. package/dist/skills/tools/team-tool.js +0 -105
  207. package/dist/skills/tools/tool-search-tool.d.ts +0 -51
  208. package/dist/skills/tools/tool-search-tool.js +0 -110
  209. package/dist/skills/tools/tts-tool.d.ts +0 -38
  210. package/dist/skills/tools/tts-tool.js +0 -45
  211. package/dist/skills/tools/video-edit-tool.d.ts +0 -69
  212. package/dist/skills/tools/video-edit-tool.js +0 -74
  213. package/dist/skills/tools/video-generate-tool.d.ts +0 -62
  214. package/dist/skills/tools/video-generate-tool.js +0 -66
  215. package/dist/skills/tools/video-merge-tool.d.ts +0 -105
  216. package/dist/skills/tools/video-merge-tool.js +0 -92
  217. package/dist/skills/tools/video-upscale-tool.d.ts +0 -45
  218. package/dist/skills/tools/video-upscale-tool.js +0 -52
  219. package/dist/skills/tools/web-fetch-tool.d.ts +0 -78
  220. package/dist/skills/tools/web-fetch-tool.js +0 -92
  221. package/dist/skills/tools/web-search-tool.d.ts +0 -57
  222. package/dist/skills/tools/web-search-tool.js +0 -86
  223. package/dist/skills/tools/worktree-tool.d.ts +0 -69
  224. package/dist/skills/tools/worktree-tool.js +0 -147
  225. package/dist/skills/tools/write-tool.d.ts +0 -45
  226. package/dist/skills/tools/write-tool.js +0 -81
@@ -1,575 +0,0 @@
1
- /**
2
- * Tool loop state machine — faithful port of Hub's semantic-turn-tools.ts.
3
- *
4
- * Replaces Hub infrastructure with DI abstractions:
5
- * - invokeGatewayTool() (WS) → ToolInvoker.invoke()
6
- * - createAdminInferProxyClient().stream() → LLMTransport.stream()
7
- * - SemanticTurnDeps (PG/Redis) → in-memory state
8
- * - sidechain DB depth → in-memory depth counter
9
- * - Hub approval flow → skipped (CLI is local, all tools pre-authorized)
10
- *
11
- * Keeps ALL orchestration strategy calls from Hub:
12
- * - turn-loop-guard (abort, token budget, output escalation)
13
- * - tool-loop-state (advance/settle/recover)
14
- * - parallel/serial tool call scheduling
15
- * - sidechain tool access policy filtering
16
- * - consecutive failure early exit
17
- * - API error recovery
18
- * - message repair
19
- * - skill learning evaluation
20
- *
21
- * Zero imports from express/pg/ioredis/ws.
22
- */
23
- import { accumulateToolCalls } from "../llm/transport.js";
24
- import { advanceToolLoopState, applyToolChoicePolicy, buildAssistantToolCallMessage, buildToolResultMessage, filterToolCallsByAccessPolicy, recoverToolLoopStateFromChatConversation, resolveParallelToolCallScheduling, resolveSidechainToolAccessByType, settleToolLoopState, calculateTokenWarningState, resolveApiErrorRecovery, resolveOutputTokenEscalation, shouldAbortTurn, createTurnLoopGuardState, shouldAttemptReactiveCompact, createReactiveCompactState, classifyError, buildSkillInstruction, } from "../orchestration/index.js";
25
- // ── Budget constants (same as Hub) ───────────────────────────────────────────
26
- const MAX_TOOL_BUDGET_CAP = 100;
27
- const DEFAULT_TOOL_BUDGET = Math.min(Math.max(1, Number(process.env.TOOL_LOOP_DEFAULT_BUDGET) || 25), MAX_TOOL_BUDGET_CAP);
28
- const MAX_CONSECUTIVE_FAILURES = 3;
29
- const MAX_SIDECHAIN_DEPTH = 2;
30
- // ── Helper: resolve budget ───────────────────────────────────────────────────
31
- function resolveToolLoopBudget(requested) {
32
- if (typeof requested === "number" && Number.isFinite(requested) && requested >= 1) {
33
- return Math.min(Math.round(requested), MAX_TOOL_BUDGET_CAP);
34
- }
35
- return DEFAULT_TOOL_BUDGET;
36
- }
37
- // ── Main tool loop ───────────────────────────────────────────────────────────
38
- /**
39
- * Execute the tool loop — faithful port of Hub's orchestrateAgentTurnWithGatewayTools().
40
- *
41
- * Yields TurnEvent stream. The caller (Agent class) wraps this with
42
- * start/end lifecycle and error handling.
43
- *
44
- * Events yielded:
45
- * - delta: streaming text from LLM
46
- * - tool_call: LLM requested a tool call
47
- * - tool_result: tool execution completed
48
- * - skill_instruction: skill learning evaluation result
49
- * - end: turn completed successfully
50
- * - error: unrecoverable error
51
- */
52
- export async function* executeToolLoop(params, transport, toolInvoker, log) {
53
- const { turnId, sessionId, messages: inputMessages, tools, model, apiKey, temperature, hooks, signal, } = params;
54
- const hookCtx = { sessionId, turnId };
55
- // ── Tool eligibility filtering (CC permission-level parity) ────
56
- const { resolveToolEligibility } = await import("../runtime/tool-eligibility.js");
57
- const eligibility = resolveToolEligibility(tools, params.toolEligibilityContext);
58
- const effectiveTools = eligibility.eligibleTools;
59
- // Emit blocked tool events
60
- for (const blocked of eligibility.blockedTools) {
61
- yield { type: "tool_blocked", turnId, callId: "", name: blocked.toolName, reason: "blocked-by-policy" };
62
- }
63
- // ── No tools: single LLM call, no loop ─────────────────────────
64
- if (!effectiveTools.length) {
65
- yield* executeSingleLLMRound(turnId, model, inputMessages, apiKey, temperature, signal, transport, log);
66
- return;
67
- }
68
- // ── Tool loop setup ────────────────────────────────────────────
69
- const toolBudget = resolveToolLoopBudget(params.maxRounds);
70
- const messages = [...inputMessages];
71
- // Recover tool loop state from conversation history
72
- let toolLoopState = recoverToolLoopStateFromChatConversation({
73
- maxRounds: toolBudget,
74
- replayMessages: messages,
75
- }).state;
76
- // Turn-loop-guard config (CC-aligned)
77
- const guardConfig = {
78
- contextWindowTokens: params.contextWindowTokens ?? 128_000,
79
- responseBufferTokens: 13_000,
80
- maxOutputTokens: params.maxOutputTokens ?? 16_384,
81
- abortSignal: signal,
82
- reactiveCompactEnabled: true,
83
- outputEscalationEnabled: true,
84
- };
85
- let guardState = createTurnLoopGuardState(guardConfig);
86
- let reactiveCompactState = createReactiveCompactState();
87
- // In-memory sidechain depth (replaces Hub's PG recursive CTE)
88
- let sidechainStarted = false;
89
- let activeSidechainToolPolicy = null;
90
- const sidechainDepth = (params.parentDepth ?? 0) + 1;
91
- let lastUsage;
92
- let finalText = "";
93
- let consecutiveFailedRounds = 0;
94
- let totalUsage = { prompt: 0, completion: 0 };
95
- // Skill learning metrics
96
- const distinctToolNames = new Set();
97
- let totalToolCallCount = 0;
98
- // ── Main loop ──────────────────────────────────────────────────
99
- for (let round = 0; round < toolBudget; round++) {
100
- // Turn-loop-guard: pre-round abort check (CC-aligned)
101
- if (shouldAbortTurn(guardState, guardConfig)) {
102
- log.info(`turn aborted by guard at round ${round}`);
103
- break;
104
- }
105
- // Turn-loop-guard: token budget pre-check
106
- const tokenWarning = calculateTokenWarningState(guardState, guardConfig);
107
- if (tokenWarning.level === "blocking") {
108
- if (tokenWarning.reason === "prompt_too_long" &&
109
- shouldAttemptReactiveCompact(reactiveCompactState)) {
110
- reactiveCompactState.attemptedThisTurn = true;
111
- guardState.hasAttemptedReactiveCompact = true;
112
- log.info(`token budget blocking (${tokenWarning.reason}), reactive compact needed`);
113
- }
114
- log.info(`token budget blocking (${tokenWarning.reason}), breaking tool loop`);
115
- break;
116
- }
117
- if (tokenWarning.level === "warning") {
118
- log.info(`token budget warning: ${tokenWarning.usagePercent}% used, ${tokenWarning.remainingTokens} remaining`);
119
- }
120
- // Build tool loop request: tool choice policy + state recovery
121
- const toolChoiceResult = applyToolChoicePolicy({
122
- tools: effectiveTools,
123
- toolChoice: params.toolChoice ?? "auto",
124
- });
125
- const repaired = recoverToolLoopStateFromChatConversation({
126
- maxRounds: toolBudget,
127
- replayMessages: messages,
128
- lastStopReason: toolLoopState.lastStopReason,
129
- options: { stopReason: toolLoopState.lastStopReason },
130
- });
131
- const requestMessages = toolChoiceResult.extraSystemPrompt
132
- ? [
133
- { role: "system", content: toolChoiceResult.extraSystemPrompt },
134
- ...repaired.state.replayMessages,
135
- ]
136
- : repaired.state.replayMessages;
137
- if (repaired.recoveryActions.length > 0) {
138
- log.debug(`tool loop recovery: ${repaired.recoveryActions.map((a) => a.detail ?? a.kind).join("; ")}`);
139
- }
140
- log.debug(`round ${round + 1}/${toolBudget}, messages: ${requestMessages.length}`);
141
- // Hook: turn.before_inference
142
- hooks?.invoke("turn.before_inference", { ...hookCtx, model }).catch(() => { });
143
- // ── Streaming LLM round with Hermes delta suppression ───────
144
- let hasToolCalls = false;
145
- const textChunks = [];
146
- const toolCallAccumulator = new Map();
147
- let finishReason = "stop";
148
- let roundUsage;
149
- let streamError = null;
150
- try {
151
- for await (const chunk of transport.stream({
152
- model,
153
- messages: requestMessages,
154
- tools: toolChoiceResult.tools,
155
- toolChoice: toolChoiceResult.normalizedToolChoice ??
156
- "auto",
157
- temperature,
158
- maxTokens: guardState.currentMaxOutputTokens || undefined,
159
- }, apiKey, signal)) {
160
- switch (chunk.type) {
161
- case "delta":
162
- textChunks.push(chunk.text);
163
- // Hermes delta suppression: stop text forwarding once tool_calls detected
164
- if (!hasToolCalls) {
165
- yield { type: "delta", turnId, text: chunk.text };
166
- }
167
- break;
168
- case "tool_call_delta":
169
- hasToolCalls = true;
170
- accumulateToolCalls(toolCallAccumulator, chunk);
171
- break;
172
- case "reasoning_delta":
173
- break;
174
- case "usage":
175
- roundUsage = {
176
- prompt: chunk.promptTokens,
177
- completion: chunk.completionTokens,
178
- reasoning: chunk.reasoningTokens,
179
- };
180
- break;
181
- case "done":
182
- finishReason = chunk.finishReason;
183
- break;
184
- }
185
- }
186
- // Hook: turn.after_inference (success path)
187
- if (!hasToolCalls) {
188
- hooks?.invoke("turn.after_inference", { ...hookCtx, model }).catch(() => { });
189
- }
190
- }
191
- catch (err) {
192
- const message = err instanceof Error ? err.message : String(err);
193
- const status = typeof err?.status === "number"
194
- ? err.status
195
- : undefined;
196
- streamError = { status, message };
197
- }
198
- // Hook: turn.after_inference (error path)
199
- if (streamError) {
200
- hooks?.invoke("turn.after_inference", { ...hookCtx, model, response: { error: streamError.message } }).catch(() => { });
201
- }
202
- // ── Handle LLM errors with turn-loop-guard recovery ────────
203
- if (streamError) {
204
- const recovery = resolveApiErrorRecovery({ status: streamError.status ?? 500, message: streamError.message }, guardState, guardConfig);
205
- if (recovery.action === "reactive_compact" &&
206
- shouldAttemptReactiveCompact(reactiveCompactState)) {
207
- reactiveCompactState.attemptedThisTurn = true;
208
- guardState.hasAttemptedReactiveCompact = true;
209
- yield { type: "recovery", turnId, action: "reactive_compact", detail: `API ${streamError.status ?? 500}: ${streamError.message}` };
210
- }
211
- if (recovery.action === "retry") {
212
- yield { type: "recovery", turnId, action: "retry", detail: recovery.reason };
213
- continue;
214
- }
215
- const category = classifyError(streamError.status, streamError.message);
216
- yield { type: "error", turnId, error: streamError.message, code: category };
217
- return;
218
- }
219
- // Accumulate usage
220
- if (roundUsage) {
221
- totalUsage.prompt += roundUsage.prompt;
222
- totalUsage.completion += roundUsage.completion;
223
- if (roundUsage.reasoning) {
224
- totalUsage.reasoning = (totalUsage.reasoning ?? 0) + roundUsage.reasoning;
225
- }
226
- }
227
- // Turn-loop-guard: update token state from usage feedback
228
- if (roundUsage?.prompt) {
229
- guardState.promptTokens = roundUsage.prompt;
230
- }
231
- // Turn-loop-guard: handle max_tokens truncation → escalate output budget (CC-aligned)
232
- if (finishReason === "length" || finishReason === "max_tokens") {
233
- guardState.consecutiveTruncations += 1;
234
- const modelMaxOutput = params.modelMaxOutputTokens ?? 65_536;
235
- const escalation = resolveOutputTokenEscalation(guardState, guardConfig, modelMaxOutput);
236
- if (escalation.shouldEscalate) {
237
- guardState.currentMaxOutputTokens = escalation.newMax;
238
- yield { type: "recovery", turnId, action: "output_escalation", detail: `${escalation.newMax} tokens` };
239
- }
240
- }
241
- else {
242
- guardState.consecutiveTruncations = 0;
243
- }
244
- lastUsage = roundUsage;
245
- finalText = textChunks.join("");
246
- // Convert accumulated tool call deltas to OpenAiToolCall[]
247
- let toolCalls = [...toolCallAccumulator.values()].map((tc) => ({
248
- id: tc.id || `tc_${turnId}_${round}_${Math.random().toString(36).slice(2, 8)}`,
249
- type: "function",
250
- function: { name: tc.name, arguments: tc.arguments },
251
- }));
252
- // Filter tool_calls against active sidechain policy
253
- if (activeSidechainToolPolicy && toolCalls.length > 0) {
254
- const filterResult = filterToolCallsByAccessPolicy(toolCalls, activeSidechainToolPolicy);
255
- if (filterResult.denied.length > 0) {
256
- log.info(`sidechain policy denied: ${filterResult.denied.map((d) => `${d.toolCall.function.name} (${d.reason})`).join(", ")}`);
257
- }
258
- toolCalls = filterResult.allowed;
259
- }
260
- // ── No tool calls → loop complete ──────────────────────────
261
- if (toolCalls.length === 0) {
262
- toolLoopState = settleToolLoopState(toolLoopState, {
263
- replayMessages: messages,
264
- lastStopReason: "completed",
265
- });
266
- // Sidechain completed event
267
- if (sidechainStarted) {
268
- yield { type: "sidechain_completed", turnId, depth: sidechainDepth, toolCallCount: totalToolCallCount };
269
- }
270
- // Skill learning: evaluate turn for skill instruction
271
- if (totalToolCallCount > 0) {
272
- const skillResult = {
273
- ok: true,
274
- toolCallCount: totalToolCallCount,
275
- distinctToolCount: distinctToolNames.size,
276
- multiStep: totalToolCallCount >= 2,
277
- hasSidechain: sidechainStarted,
278
- feedback: null,
279
- existingSkillName: null,
280
- };
281
- const skillInstruction = buildSkillInstruction(skillResult, {
282
- tools: [...distinctToolNames],
283
- });
284
- if (skillInstruction) {
285
- yield { type: "skill_instruction", turnId, instruction: skillInstruction };
286
- }
287
- }
288
- yield { type: "end", turnId, content: finalText, usage: totalUsage, model };
289
- return;
290
- }
291
- // ── Has tool calls → execute them ──────────────────────────
292
- // Emit tool_call events for the host
293
- for (const tc of toolCalls) {
294
- yield {
295
- type: "tool_call",
296
- turnId,
297
- callId: tc.id,
298
- name: tc.function.name,
299
- arguments: tc.function.arguments,
300
- };
301
- }
302
- // Build assistant message with tool calls
303
- messages.push(buildAssistantToolCallMessage(toolCalls));
304
- // Setup sidechain on first tool calls (in-memory depth tracking)
305
- if (!sidechainStarted) {
306
- if (sidechainDepth > MAX_SIDECHAIN_DEPTH) {
307
- yield {
308
- type: "error",
309
- turnId,
310
- error: `sidechain depth ${sidechainDepth} exceeds max ${MAX_SIDECHAIN_DEPTH}`,
311
- code: "SIDECHAIN_DEPTH_LIMIT",
312
- };
313
- return;
314
- }
315
- sidechainStarted = true;
316
- const sidechainRole = sidechainDepth >= MAX_SIDECHAIN_DEPTH ? "leaf" : "orchestrator";
317
- yield { type: "sidechain_started", turnId, depth: sidechainDepth, role: sidechainRole };
318
- activeSidechainToolPolicy = resolveSidechainToolAccessByType({
319
- type: "planner",
320
- depth: sidechainDepth,
321
- maxDepth: MAX_SIDECHAIN_DEPTH,
322
- role: sidechainDepth >= MAX_SIDECHAIN_DEPTH ? "leaf" : "orchestrator",
323
- toolNames: tools.map((t) => t.function.name),
324
- });
325
- }
326
- // Advance tool loop state
327
- toolLoopState = advanceToolLoopState(toolLoopState, {
328
- replayMessages: messages,
329
- pendingToolCallIds: toolCalls.map((tc) => tc.id),
330
- completedToolCallIds: toolLoopState.completedToolCallIds,
331
- lastStopReason: "tool_calls",
332
- });
333
- // Resolve parallel/serial scheduling
334
- const scheduling = resolveParallelToolCallScheduling({
335
- requestedPreference: undefined,
336
- providerSupportsParallel: true,
337
- toolCalls,
338
- toolCapabilities: effectiveTools.map((t) => ({
339
- name: t.function.name,
340
- requiresApproval: t.meta?.requiresApproval ?? false,
341
- approvalMode: (t.meta?.requiresApproval ? "user-confirm" : "pre-authorized"),
342
- parallelSafe: t.meta?.parallelSafe ?? true,
343
- serialOnly: t.meta?.serialOnly ?? false,
344
- })),
345
- });
346
- // ── Execute tool calls in batches ──────────────────────────
347
- const executedToolCallIds = [];
348
- try {
349
- for (const batch of scheduling.batches) {
350
- if (batch.mode === "parallel") {
351
- const toolCallOpts = { hooks, sessionId };
352
- const batchResults = await Promise.all(batch.calls.map((tc) => executeToolCall(tc, turnId, toolInvoker, signal, log, toolCallOpts)));
353
- for (const r of batchResults) {
354
- if (r.blocked) {
355
- yield { type: "tool_blocked", turnId, callId: r.callId, name: r.toolName, reason: r.blockReason ?? "blocked" };
356
- }
357
- messages.push(r.message);
358
- executedToolCallIds.push(r.callId);
359
- distinctToolNames.add(r.toolName);
360
- totalToolCallCount++;
361
- yield {
362
- type: "tool_result",
363
- turnId,
364
- callId: r.callId,
365
- name: r.toolName,
366
- ok: r.ok,
367
- error: r.error,
368
- };
369
- }
370
- }
371
- else {
372
- for (const tc of batch.calls) {
373
- if (signal?.aborted) {
374
- yield { type: "error", turnId, error: "Turn aborted", code: "ABORTED" };
375
- return;
376
- }
377
- const r = await executeToolCall(tc, turnId, toolInvoker, signal, log, { hooks, sessionId });
378
- if (r.blocked) {
379
- yield { type: "tool_blocked", turnId, callId: r.callId, name: r.toolName, reason: r.blockReason ?? "blocked" };
380
- }
381
- messages.push(r.message);
382
- executedToolCallIds.push(r.callId);
383
- distinctToolNames.add(r.toolName);
384
- totalToolCallCount++;
385
- yield {
386
- type: "tool_result",
387
- turnId,
388
- callId: r.callId,
389
- name: r.toolName,
390
- ok: r.ok,
391
- error: r.error,
392
- };
393
- }
394
- }
395
- }
396
- }
397
- catch (err) {
398
- const message = err instanceof Error ? err.message : String(err);
399
- yield { type: "error", turnId, error: message, code: "TOOL_EXECUTION_ERROR" };
400
- return;
401
- }
402
- // Settle tool loop state
403
- toolLoopState = settleToolLoopState(toolLoopState, {
404
- replayMessages: messages,
405
- completedToolCallIds: [
406
- ...toolLoopState.completedToolCallIds,
407
- ...executedToolCallIds,
408
- ],
409
- lastStopReason: scheduling.mode,
410
- });
411
- // ── Consecutive failure tracking ───────────────────────────
412
- const roundToolResults = messages.slice(-toolCalls.length);
413
- const allFailed = roundToolResults.length > 0 &&
414
- roundToolResults.every((msg) => {
415
- const content = msg?.content;
416
- if (typeof content !== "string")
417
- return false;
418
- try {
419
- return JSON.parse(content)?.ok === false;
420
- }
421
- catch {
422
- return false;
423
- }
424
- });
425
- if (allFailed) {
426
- consecutiveFailedRounds += 1;
427
- if (consecutiveFailedRounds >= MAX_CONSECUTIVE_FAILURES && finalText) {
428
- log.info(`early exit: ${consecutiveFailedRounds} consecutive failed rounds, returning partial response`);
429
- yield { type: "end", turnId, content: finalText, usage: totalUsage };
430
- return;
431
- }
432
- }
433
- else {
434
- consecutiveFailedRounds = 0;
435
- }
436
- }
437
- // ── Budget exhaustion ──────────────────────────────────────────
438
- if (finalText) {
439
- log.info(`tool loop budget exhausted (${toolBudget} rounds), returning partial response`);
440
- // Sidechain completed event (budget exhaustion)
441
- if (sidechainStarted) {
442
- yield { type: "sidechain_completed", turnId, depth: sidechainDepth, toolCallCount: totalToolCallCount };
443
- }
444
- // Skill learning on partial completion
445
- if (totalToolCallCount > 0) {
446
- const skillResult = {
447
- ok: true,
448
- toolCallCount: totalToolCallCount,
449
- distinctToolCount: distinctToolNames.size,
450
- multiStep: totalToolCallCount >= 2,
451
- hasSidechain: sidechainStarted,
452
- feedback: null,
453
- existingSkillName: null,
454
- };
455
- const skillInstruction = buildSkillInstruction(skillResult, {
456
- tools: [...distinctToolNames],
457
- });
458
- if (skillInstruction) {
459
- yield { type: "skill_instruction", turnId, instruction: skillInstruction };
460
- }
461
- }
462
- yield { type: "end", turnId, content: finalText, usage: totalUsage, model };
463
- return;
464
- }
465
- yield {
466
- type: "error",
467
- turnId,
468
- error: `Tool loop exceeded budget (${toolBudget} rounds)`,
469
- code: "TOOL_LOOP_LIMIT",
470
- };
471
- }
472
- async function executeToolCall(toolCall, turnId, toolInvoker, signal, log, options) {
473
- const toolName = toolCall.function.name;
474
- log.debug(`tool_call: ${toolName}`);
475
- const hooks = options?.hooks;
476
- // Hook: tool.before_invoke — can block or mutate input (CC PreToolUse parity)
477
- let effectiveArguments = toolCall.function.arguments;
478
- if (hooks) {
479
- try {
480
- const hookResult = await hooks.invoke("tool.before_invoke", {
481
- sessionId: options?.sessionId ?? "",
482
- turnId,
483
- callId: toolCall.id,
484
- toolName,
485
- arguments: safeParseJsonArgs(toolCall.function.arguments),
486
- });
487
- if (hookResult.action === "abort") {
488
- const reason = hookResult.reason ?? "blocked by policy";
489
- log.info(`tool ${toolName} blocked: ${reason}`);
490
- // Emit approval.requested for blocked tools requiring approval
491
- hooks.invoke("approval.requested", {
492
- sessionId: options?.sessionId ?? "",
493
- turnId,
494
- approvalId: toolCall.id,
495
- callId: toolCall.id,
496
- toolName,
497
- }).catch(() => { });
498
- const message = buildToolResultMessage(toolCall.id, {
499
- ok: false,
500
- error: reason,
501
- });
502
- return { callId: toolCall.id, toolName, ok: false, error: reason, blocked: true, blockReason: reason, message };
503
- }
504
- // CC PreToolUse input mutation: hook can return modified arguments via context
505
- if (hookResult.action === "continue" && hookResult.context?.arguments) {
506
- effectiveArguments = JSON.stringify(hookResult.context.arguments);
507
- log.debug(`tool ${toolName}: input mutated by hook`);
508
- }
509
- }
510
- catch {
511
- // Hook failures are non-blocking — proceed with tool execution
512
- }
513
- }
514
- const invokeResult = await toolInvoker.invoke(turnId, toolName, effectiveArguments, signal);
515
- const ok = !invokeResult.error;
516
- const message = buildToolResultMessage(toolCall.id, {
517
- ok,
518
- payload: invokeResult.result,
519
- error: invokeResult.error,
520
- });
521
- // Hook: tool.after_invoke / tool.invoke_failed (fire-and-forget observability)
522
- hooks?.invoke(ok ? "tool.after_invoke" : "tool.invoke_failed", {
523
- sessionId: options?.sessionId ?? "",
524
- turnId,
525
- callId: toolCall.id,
526
- toolName,
527
- ok,
528
- ...(invokeResult.error ? { error: invokeResult.error } : {}),
529
- }).catch(() => { });
530
- return {
531
- callId: toolCall.id,
532
- toolName,
533
- ok,
534
- error: invokeResult.error,
535
- message,
536
- };
537
- }
538
- function safeParseJsonArgs(raw) {
539
- try {
540
- return JSON.parse(raw);
541
- }
542
- catch {
543
- return undefined;
544
- }
545
- }
546
- // ── Internal: single LLM round (no tools) ───────────────────────────────────
547
- async function* executeSingleLLMRound(turnId, model, messages, apiKey, temperature, signal, transport, log) {
548
- const textChunks = [];
549
- let usage;
550
- log.debug(`single LLM round, messages: ${messages.length}`);
551
- for await (const chunk of transport.stream({ model, messages, temperature }, apiKey, signal)) {
552
- switch (chunk.type) {
553
- case "delta":
554
- textChunks.push(chunk.text);
555
- yield { type: "delta", turnId, text: chunk.text };
556
- break;
557
- case "usage":
558
- usage = {
559
- prompt: chunk.promptTokens,
560
- completion: chunk.completionTokens,
561
- reasoning: chunk.reasoningTokens,
562
- };
563
- break;
564
- case "done":
565
- break;
566
- }
567
- }
568
- yield {
569
- type: "end",
570
- turnId,
571
- content: textChunks.join(""),
572
- usage: usage ?? { prompt: 0, completion: 0 },
573
- model,
574
- };
575
- }