@sheepbun/yips 0.1.1

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 (99) hide show
  1. package/dist/agent/commands/command-catalog.js +243 -0
  2. package/dist/agent/commands/commands.js +418 -0
  3. package/dist/agent/conductor.js +118 -0
  4. package/dist/agent/context/code-context.js +68 -0
  5. package/dist/agent/context/memory-store.js +159 -0
  6. package/dist/agent/context/session-store.js +211 -0
  7. package/dist/agent/protocol/tool-protocol.js +160 -0
  8. package/dist/agent/skills/skills.js +327 -0
  9. package/dist/agent/tools/tool-executor.js +415 -0
  10. package/dist/agent/tools/tool-safety.js +52 -0
  11. package/dist/app/index.js +35 -0
  12. package/dist/app/repl.js +105 -0
  13. package/dist/app/update-check.js +132 -0
  14. package/dist/app/version.js +51 -0
  15. package/dist/code-context.js +68 -0
  16. package/dist/colors.js +204 -0
  17. package/dist/command-catalog.js +242 -0
  18. package/dist/commands.js +350 -0
  19. package/dist/conductor.js +94 -0
  20. package/dist/config/config.js +335 -0
  21. package/dist/config/hooks.js +187 -0
  22. package/dist/config.js +335 -0
  23. package/dist/downloader-state.js +302 -0
  24. package/dist/downloader-ui.js +289 -0
  25. package/dist/gateway/adapters/discord.js +108 -0
  26. package/dist/gateway/adapters/formatting.js +96 -0
  27. package/dist/gateway/adapters/telegram.js +106 -0
  28. package/dist/gateway/adapters/types.js +2 -0
  29. package/dist/gateway/adapters/whatsapp.js +124 -0
  30. package/dist/gateway/auth-policy.js +66 -0
  31. package/dist/gateway/core.js +87 -0
  32. package/dist/gateway/headless-conductor.js +328 -0
  33. package/dist/gateway/message-router.js +23 -0
  34. package/dist/gateway/rate-limiter.js +48 -0
  35. package/dist/gateway/runtime/backend-policy.js +18 -0
  36. package/dist/gateway/runtime/discord-bot.js +104 -0
  37. package/dist/gateway/runtime/discord-main.js +69 -0
  38. package/dist/gateway/session-manager.js +77 -0
  39. package/dist/gateway/types.js +2 -0
  40. package/dist/hardware.js +92 -0
  41. package/dist/hooks.js +187 -0
  42. package/dist/index.js +34 -0
  43. package/dist/input-engine.js +250 -0
  44. package/dist/llama-client.js +227 -0
  45. package/dist/llama-server.js +620 -0
  46. package/dist/llm/llama-client.js +227 -0
  47. package/dist/llm/llama-server.js +620 -0
  48. package/dist/llm/token-counter.js +47 -0
  49. package/dist/memory-store.js +159 -0
  50. package/dist/messages.js +59 -0
  51. package/dist/model-downloader.js +382 -0
  52. package/dist/model-manager-state.js +118 -0
  53. package/dist/model-manager-ui.js +194 -0
  54. package/dist/model-manager.js +190 -0
  55. package/dist/models/hardware.js +92 -0
  56. package/dist/models/model-downloader.js +382 -0
  57. package/dist/models/model-manager.js +190 -0
  58. package/dist/prompt-box.js +78 -0
  59. package/dist/prompt-composer.js +498 -0
  60. package/dist/repl.js +105 -0
  61. package/dist/session-store.js +211 -0
  62. package/dist/spinner.js +76 -0
  63. package/dist/title-box.js +388 -0
  64. package/dist/token-counter.js +47 -0
  65. package/dist/tool-executor.js +415 -0
  66. package/dist/tool-protocol.js +121 -0
  67. package/dist/tool-safety.js +52 -0
  68. package/dist/tui/app.js +2553 -0
  69. package/dist/tui/startup.js +56 -0
  70. package/dist/tui-input-routing.js +53 -0
  71. package/dist/tui.js +51 -0
  72. package/dist/types/app-types.js +2 -0
  73. package/dist/types.js +2 -0
  74. package/dist/ui/colors.js +204 -0
  75. package/dist/ui/downloader/downloader-state.js +302 -0
  76. package/dist/ui/downloader/downloader-ui.js +289 -0
  77. package/dist/ui/input/input-engine.js +250 -0
  78. package/dist/ui/input/tui-input-routing.js +53 -0
  79. package/dist/ui/input/vt-session.js +168 -0
  80. package/dist/ui/messages.js +59 -0
  81. package/dist/ui/model-manager/model-manager-state.js +118 -0
  82. package/dist/ui/model-manager/model-manager-ui.js +194 -0
  83. package/dist/ui/prompt/prompt-box.js +78 -0
  84. package/dist/ui/prompt/prompt-composer.js +498 -0
  85. package/dist/ui/spinner.js +76 -0
  86. package/dist/ui/title-box.js +388 -0
  87. package/dist/ui/tui/app.js +6 -0
  88. package/dist/ui/tui/autocomplete.js +85 -0
  89. package/dist/ui/tui/constants.js +18 -0
  90. package/dist/ui/tui/history.js +29 -0
  91. package/dist/ui/tui/layout.js +341 -0
  92. package/dist/ui/tui/runtime-core.js +2584 -0
  93. package/dist/ui/tui/runtime-utils.js +53 -0
  94. package/dist/ui/tui/start-tui.js +54 -0
  95. package/dist/ui/tui/startup.js +56 -0
  96. package/dist/version.js +51 -0
  97. package/dist/vt-session.js +168 -0
  98. package/install.sh +457 -0
  99. package/package.json +128 -0
@@ -0,0 +1,350 @@
1
+ "use strict";
2
+ /** Slash command registry and dispatch system. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CommandRegistry = void 0;
5
+ exports.parseCommand = parseCommand;
6
+ exports.createDefaultRegistry = createDefaultRegistry;
7
+ const command_catalog_1 = require("./command-catalog");
8
+ const config_1 = require("./config");
9
+ const memory_store_1 = require("./memory-store");
10
+ const model_manager_1 = require("./model-manager");
11
+ const model_downloader_1 = require("./model-downloader");
12
+ function isGenericDescription(description) {
13
+ const trimmed = description.trim();
14
+ return trimmed.length === 0 || trimmed === "Command";
15
+ }
16
+ class CommandRegistry {
17
+ commands = new Map();
18
+ descriptors = new Map();
19
+ constructor(initialDescriptors = []) {
20
+ for (const descriptor of initialDescriptors) {
21
+ const name = descriptor.name.toLowerCase();
22
+ this.descriptors.set(name, { ...descriptor, name });
23
+ }
24
+ }
25
+ register(name, handler, description, kind = "builtin") {
26
+ const normalizedName = name.toLowerCase();
27
+ const existing = this.descriptors.get(normalizedName);
28
+ const mergedDescription = existing && !isGenericDescription(existing.description) ? existing.description : description;
29
+ this.commands.set(normalizedName, {
30
+ name: normalizedName,
31
+ description: mergedDescription,
32
+ handler
33
+ });
34
+ this.descriptors.set(normalizedName, {
35
+ name: normalizedName,
36
+ description: mergedDescription,
37
+ kind: existing?.kind ?? kind,
38
+ implemented: true
39
+ });
40
+ }
41
+ async dispatch(name, args, context) {
42
+ const command = this.commands.get(name.toLowerCase());
43
+ if (command) {
44
+ return await command.handler(args, context);
45
+ }
46
+ if (this.descriptors.has(name.toLowerCase())) {
47
+ return {
48
+ output: `Command /${name} is recognized but not implemented in this TypeScript rewrite yet. ` +
49
+ "Type /help to see implemented commands.",
50
+ action: "continue"
51
+ };
52
+ }
53
+ return {
54
+ output: `Unknown command: /${name}. Type /help for help.`,
55
+ action: "continue"
56
+ };
57
+ }
58
+ getHelp() {
59
+ const commands = this.listCommands();
60
+ const implemented = commands.filter((command) => command.implemented);
61
+ const planned = commands.filter((command) => !command.implemented);
62
+ const lines = ["Available commands:"];
63
+ lines.push("Implemented:");
64
+ for (const cmd of implemented) {
65
+ lines.push(` /${cmd.name} - ${cmd.description}`);
66
+ }
67
+ if (planned.length > 0) {
68
+ lines.push("");
69
+ lines.push("Recognized (not implemented in this rewrite yet):");
70
+ for (const cmd of planned) {
71
+ lines.push(` /${cmd.name} - ${cmd.description}`);
72
+ }
73
+ }
74
+ return lines.join("\n");
75
+ }
76
+ has(name) {
77
+ return this.descriptors.has(name.toLowerCase());
78
+ }
79
+ getNames() {
80
+ return this.listCommands().map((command) => command.name);
81
+ }
82
+ listCommands() {
83
+ return [...this.descriptors.values()].sort((left, right) => left.name.localeCompare(right.name));
84
+ }
85
+ getAutocompleteCommands() {
86
+ return this.getNames().map((name) => `/${name}`);
87
+ }
88
+ }
89
+ exports.CommandRegistry = CommandRegistry;
90
+ function splitCommandArgs(input) {
91
+ const matches = input.match(/"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|[^\s]+/gu);
92
+ if (!matches) {
93
+ return [];
94
+ }
95
+ return matches.map((token) => {
96
+ if ((token.startsWith('"') && token.endsWith('"')) ||
97
+ (token.startsWith("'") && token.endsWith("'"))) {
98
+ return token.slice(1, -1);
99
+ }
100
+ return token;
101
+ });
102
+ }
103
+ function parseTokenCountArg(input) {
104
+ const trimmed = input.trim().toLowerCase();
105
+ if (trimmed.length === 0) {
106
+ return null;
107
+ }
108
+ const match = trimmed.match(/^(\d+)(k)?$/u);
109
+ if (!match) {
110
+ return null;
111
+ }
112
+ const raw = Number(match[1]);
113
+ if (!Number.isInteger(raw) || raw <= 0) {
114
+ return null;
115
+ }
116
+ const multiplier = match[2] === "k" ? 1000 : 1;
117
+ return raw * multiplier;
118
+ }
119
+ function parseCommand(input) {
120
+ const trimmed = input.trim();
121
+ if (!trimmed.startsWith("/"))
122
+ return null;
123
+ const spaceIndex = trimmed.indexOf(" ");
124
+ if (spaceIndex === -1) {
125
+ return { command: trimmed.slice(1).toLowerCase(), args: "" };
126
+ }
127
+ return {
128
+ command: trimmed.slice(1, spaceIndex).toLowerCase(),
129
+ args: trimmed.slice(spaceIndex + 1).trim()
130
+ };
131
+ }
132
+ function createDefaultRegistry() {
133
+ const registry = new CommandRegistry((0, command_catalog_1.loadCommandCatalog)());
134
+ const downloadUsage = [
135
+ "Model downloader:",
136
+ " /download Open interactive downloader",
137
+ " /download <hf_url> Download directly from hf.co/huggingface URL",
138
+ " /dl ... Alias for /download"
139
+ ].join("\n");
140
+ const handleDownload = async (args) => {
141
+ const trimmed = args.trim();
142
+ const tokens = trimmed.length > 0 ? trimmed.split(/\s+/u) : [];
143
+ try {
144
+ if (tokens.length === 0) {
145
+ return {
146
+ action: "continue",
147
+ uiAction: { type: "open-downloader" }
148
+ };
149
+ }
150
+ const inputArg = tokens.join(" ");
151
+ if (inputArg.toLowerCase() === "help") {
152
+ return { output: downloadUsage, action: "continue" };
153
+ }
154
+ if (!(0, model_downloader_1.isHfDownloadUrl)(inputArg)) {
155
+ return {
156
+ output: `Invalid /download argument. Only direct Hugging Face URLs are supported.\n\n${downloadUsage}`,
157
+ action: "continue"
158
+ };
159
+ }
160
+ const parsed = (0, model_downloader_1.parseHfDownloadUrl)(inputArg);
161
+ const result = await (0, model_downloader_1.downloadModelFile)(parsed);
162
+ const modelsDir = (0, model_downloader_1.resolveDefaultModelsDir)();
163
+ return {
164
+ output: `Downloaded ${parsed.filename} from ${parsed.repoId}.\n` +
165
+ `Saved to: ${result.localPath}\n` +
166
+ `Models dir: ${modelsDir}\n` +
167
+ `Use with: /model ${parsed.repoId}/${parsed.filename}`,
168
+ action: "continue"
169
+ };
170
+ }
171
+ catch (error) {
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ return {
174
+ output: `Download command failed: ${message}`,
175
+ action: "continue"
176
+ };
177
+ }
178
+ };
179
+ const memorizeUsage = [
180
+ "Memory commands:",
181
+ " /memorize <fact> Save a memory",
182
+ " /memorize list [limit] List recent memories (default 10)",
183
+ " /memorize read <memory_id> Read a saved memory",
184
+ " /memorize help Show this help"
185
+ ].join("\n");
186
+ registry.register("help", () => ({
187
+ output: registry.getHelp(),
188
+ action: "continue"
189
+ }), "Show this help");
190
+ registry.register("exit", () => ({ output: "Goodbye.", action: "exit" }), "Exit Yips");
191
+ registry.register("quit", () => ({ output: "Goodbye.", action: "exit" }), "Exit Yips");
192
+ registry.register("restart", () => ({ output: "Restarting Yips.", action: "restart" }), "Restart Yips");
193
+ registry.register("clear", () => ({ action: "clear" }), "Clear the screen");
194
+ registry.register("new", () => ({ action: "clear" }), "Start a new conversation");
195
+ registry.register("model", async (args, context) => {
196
+ try {
197
+ const trimmed = args.trim();
198
+ if (trimmed.length === 0) {
199
+ return { action: "continue", uiAction: { type: "open-model-manager" } };
200
+ }
201
+ let selectedModel = trimmed;
202
+ const localModels = await (0, model_manager_1.listLocalModels)({ nicknames: context.config.nicknames });
203
+ const matched = (0, model_manager_1.findMatchingModel)(localModels, trimmed);
204
+ if (matched) {
205
+ selectedModel = matched.id;
206
+ }
207
+ context.config.backend = "llamacpp";
208
+ context.config.model = selectedModel;
209
+ await (0, config_1.saveConfig)(context.config);
210
+ const matchSuffix = matched ? ` (matched from '${trimmed}')` : " (free-form fallback)";
211
+ return { output: `Model set to: ${selectedModel}${matchSuffix}`, action: "continue" };
212
+ }
213
+ catch (error) {
214
+ const message = error instanceof Error ? error.message : String(error);
215
+ return {
216
+ output: `Model command failed: ${message}`,
217
+ action: "continue"
218
+ };
219
+ }
220
+ }, "View or set the current model");
221
+ registry.register("sessions", async () => {
222
+ return { action: "continue", uiAction: { type: "open-sessions" } };
223
+ }, "Interactively select and load a session");
224
+ registry.register("vt", async () => ({ action: "continue", uiAction: { type: "open-vt" } }), "Open Virtual Terminal");
225
+ registry.register("stream", (_args, context) => {
226
+ context.config.streaming = !context.config.streaming;
227
+ const state = context.config.streaming ? "enabled" : "disabled";
228
+ return { output: `Streaming ${state}.`, action: "continue" };
229
+ }, "Toggle streaming mode");
230
+ registry.register("verbose", (_args, context) => {
231
+ context.config.verbose = !context.config.verbose;
232
+ const state = context.config.verbose ? "enabled" : "disabled";
233
+ return { output: `Verbose mode ${state}.`, action: "continue" };
234
+ }, "Toggle verbose mode");
235
+ registry.register("tokens", async (args, context) => {
236
+ const trimmed = args.trim();
237
+ if (trimmed.length === 0) {
238
+ if (context.config.tokensMode === "auto") {
239
+ return { output: "Tokens mode: auto (dynamic).", action: "continue" };
240
+ }
241
+ return {
242
+ output: `Tokens mode: manual (${context.config.tokensManualMax}).`,
243
+ action: "continue"
244
+ };
245
+ }
246
+ if (trimmed.toLowerCase() === "auto") {
247
+ context.config.tokensMode = "auto";
248
+ await (0, config_1.saveConfig)(context.config);
249
+ return {
250
+ output: "Token limit mode set to auto.",
251
+ action: "continue"
252
+ };
253
+ }
254
+ const parsed = parseTokenCountArg(trimmed);
255
+ if (parsed === null) {
256
+ return {
257
+ output: "Usage: /tokens auto | /tokens <positive_number|numberk>",
258
+ action: "continue"
259
+ };
260
+ }
261
+ context.config.tokensMode = "manual";
262
+ context.config.tokensManualMax = parsed;
263
+ await (0, config_1.saveConfig)(context.config);
264
+ return {
265
+ output: `Token limit set to ${parsed} (manual).`,
266
+ action: "continue"
267
+ };
268
+ }, "Show or set token counter mode and max");
269
+ registry.register("download", (args) => handleDownload(args), "Open the model downloader");
270
+ registry.register("dl", (args) => handleDownload(args), "Alias for /download");
271
+ registry.register("memorize", async (args) => {
272
+ const trimmed = args.trim();
273
+ if (trimmed.length === 0 || trimmed.toLowerCase() === "help") {
274
+ return { output: memorizeUsage, action: "continue" };
275
+ }
276
+ const tokens = trimmed.split(/\s+/u);
277
+ const subcommand = (tokens[0] ?? "").toLowerCase();
278
+ try {
279
+ if (subcommand === "list") {
280
+ const limitRaw = tokens[1] ?? "10";
281
+ const limit = Number.parseInt(limitRaw, 10);
282
+ if (!Number.isInteger(limit) || limit <= 0) {
283
+ return { output: "Usage: /memorize list [positive_limit]", action: "continue" };
284
+ }
285
+ const memories = await (0, memory_store_1.listMemories)(limit);
286
+ if (memories.length === 0) {
287
+ return { output: "No saved memories yet.", action: "continue" };
288
+ }
289
+ const lines = ["Saved memories:"];
290
+ for (const memory of memories) {
291
+ lines.push(`- ${memory.id}: ${memory.preview}`);
292
+ }
293
+ return { output: lines.join("\n"), action: "continue" };
294
+ }
295
+ if (subcommand === "read") {
296
+ const memoryId = tokens.slice(1).join(" ").trim();
297
+ if (memoryId.length === 0) {
298
+ return { output: "Usage: /memorize read <memory_id>", action: "continue" };
299
+ }
300
+ const memory = await (0, memory_store_1.readMemory)(memoryId);
301
+ return {
302
+ output: [`Memory ${memory.id}:`, memory.content].join("\n\n"),
303
+ action: "continue"
304
+ };
305
+ }
306
+ const saved = await (0, memory_store_1.saveMemory)(trimmed);
307
+ return {
308
+ output: `Saved memory: ${saved.id}`,
309
+ action: "continue"
310
+ };
311
+ }
312
+ catch (error) {
313
+ const message = error instanceof Error ? error.message : String(error);
314
+ return { output: `Memorize command failed: ${message}`, action: "continue" };
315
+ }
316
+ }, "Save, list, and read long-term memories");
317
+ registry.register("nick", async (args, context) => {
318
+ try {
319
+ const tokens = splitCommandArgs(args.trim());
320
+ if (tokens.length < 2) {
321
+ return {
322
+ output: "Usage: /nick <model_name_or_filename> <nickname>",
323
+ action: "continue"
324
+ };
325
+ }
326
+ const [target, ...nicknameTokens] = tokens;
327
+ const nickname = nicknameTokens.join(" ").trim();
328
+ if (!target || nickname.length === 0) {
329
+ return {
330
+ output: "Usage: /nick <model_name_or_filename> <nickname>",
331
+ action: "continue"
332
+ };
333
+ }
334
+ context.config.nicknames[target] = nickname;
335
+ await (0, config_1.saveConfig)(context.config);
336
+ return {
337
+ output: `Nickname set: ${target} -> ${nickname}`,
338
+ action: "continue"
339
+ };
340
+ }
341
+ catch (error) {
342
+ const message = error instanceof Error ? error.message : String(error);
343
+ return {
344
+ output: `Nick command failed: ${message}`,
345
+ action: "continue"
346
+ };
347
+ }
348
+ }, "Set a custom nickname for a model");
349
+ return registry;
350
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runConductorTurn = runConductorTurn;
4
+ const tool_protocol_1 = require("./tool-protocol");
5
+ const TOOL_FAILURE_STATUSES = new Set(["error", "denied", "timeout"]);
6
+ const CONSECUTIVE_TOOL_FAILURE_PIVOT_THRESHOLD = 2;
7
+ function shouldCountAsFailureRound(toolResults) {
8
+ if (toolResults.length === 0) {
9
+ return false;
10
+ }
11
+ return toolResults.every((result) => TOOL_FAILURE_STATUSES.has(result.status));
12
+ }
13
+ async function runConductorTurn(dependencies) {
14
+ const maxRounds = dependencies.maxRounds ?? 6;
15
+ let rounds = 0;
16
+ let finished = false;
17
+ let latestOutputTokensPerSecond = null;
18
+ let usedTokensExact = null;
19
+ let consecutiveFailureRounds = 0;
20
+ while (!finished && rounds < maxRounds) {
21
+ rounds += 1;
22
+ const reply = await dependencies.requestAssistant();
23
+ const parsed = (0, tool_protocol_1.parseToolProtocol)(reply.text);
24
+ const assistantText = parsed.assistantText.trim();
25
+ if (assistantText.length > 0) {
26
+ dependencies.onAssistantText(assistantText, reply.rendered);
27
+ dependencies.history.push({ role: "assistant", content: assistantText });
28
+ }
29
+ const completionTokens = typeof reply.completionTokens === "number" && reply.completionTokens > 0
30
+ ? reply.completionTokens
31
+ : dependencies.estimateCompletionTokens(reply.text);
32
+ latestOutputTokensPerSecond = dependencies.computeTokensPerSecond(completionTokens, reply.generationDurationMs ?? 0);
33
+ usedTokensExact =
34
+ typeof reply.totalTokens === "number" && reply.totalTokens >= 0
35
+ ? reply.totalTokens
36
+ : dependencies.estimateHistoryTokens(dependencies.history);
37
+ if (parsed.toolCalls.length === 0 && parsed.subagentCalls.length === 0) {
38
+ finished = true;
39
+ break;
40
+ }
41
+ if (parsed.toolCalls.length > 0) {
42
+ const toolResults = await dependencies.executeToolCalls(parsed.toolCalls);
43
+ dependencies.history.push({
44
+ role: "system",
45
+ content: `Tool results: ${JSON.stringify(toolResults)}`
46
+ });
47
+ if (shouldCountAsFailureRound(toolResults)) {
48
+ consecutiveFailureRounds += 1;
49
+ if (consecutiveFailureRounds >= CONSECUTIVE_TOOL_FAILURE_PIVOT_THRESHOLD) {
50
+ dependencies.history.push({
51
+ role: "system",
52
+ content: "Automatic pivot: consecutive tool failures detected. Try a different approach, use different tools, or ask the user for clarification."
53
+ });
54
+ dependencies.onWarning("Consecutive tool failures detected. Attempting an alternative approach.");
55
+ consecutiveFailureRounds = 0;
56
+ }
57
+ }
58
+ else {
59
+ consecutiveFailureRounds = 0;
60
+ }
61
+ }
62
+ if (parsed.subagentCalls.length > 0) {
63
+ if (!dependencies.executeSubagentCalls) {
64
+ dependencies.onWarning("Subagent delegation requested, but no subagent runner is configured.");
65
+ const fallbackResults = parsed.subagentCalls.map((call) => ({
66
+ callId: call.id,
67
+ status: "error",
68
+ output: "Subagent delegation is unavailable in this runtime."
69
+ }));
70
+ dependencies.history.push({
71
+ role: "system",
72
+ content: `Subagent results: ${JSON.stringify(fallbackResults)}`
73
+ });
74
+ }
75
+ else {
76
+ const subagentResults = await dependencies.executeSubagentCalls(parsed.subagentCalls);
77
+ dependencies.history.push({
78
+ role: "system",
79
+ content: `Subagent results: ${JSON.stringify(subagentResults)}`
80
+ });
81
+ }
82
+ }
83
+ dependencies.onRoundComplete?.();
84
+ }
85
+ if (!finished) {
86
+ dependencies.onWarning("Stopped tool chaining after max depth (6 rounds).");
87
+ }
88
+ return {
89
+ finished,
90
+ rounds,
91
+ latestOutputTokensPerSecond,
92
+ usedTokensExact
93
+ };
94
+ }