joonecli 0.1.1 → 0.2.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 (235) hide show
  1. package/dist/__tests__/config.test.js +1 -0
  2. package/dist/__tests__/config.test.js.map +1 -1
  3. package/dist/__tests__/installHostDeps.test.js +45 -0
  4. package/dist/__tests__/installHostDeps.test.js.map +1 -0
  5. package/dist/__tests__/whitelistedBackend.test.js +18 -0
  6. package/dist/__tests__/whitelistedBackend.test.js.map +1 -0
  7. package/dist/cli/config.d.ts +2 -0
  8. package/dist/cli/config.js +1 -0
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/index.js +84 -97
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/commands/builtinCommands.js +6 -6
  13. package/dist/commands/builtinCommands.js.map +1 -1
  14. package/dist/commands/commandRegistry.d.ts +3 -1
  15. package/dist/commands/commandRegistry.js.map +1 -1
  16. package/dist/core/agentLoop.d.ts +11 -28
  17. package/dist/core/agentLoop.js +68 -229
  18. package/dist/core/agentLoop.js.map +1 -1
  19. package/dist/core/compactor.js +2 -2
  20. package/dist/core/compactor.js.map +1 -1
  21. package/dist/core/contextGuard.d.ts +5 -0
  22. package/dist/core/contextGuard.js +30 -3
  23. package/dist/core/contextGuard.js.map +1 -1
  24. package/dist/core/events.d.ts +45 -0
  25. package/dist/core/events.js +8 -0
  26. package/dist/core/events.js.map +1 -0
  27. package/dist/core/promptBuilder.js.map +1 -1
  28. package/dist/core/sessionStore.js +3 -2
  29. package/dist/core/sessionStore.js.map +1 -1
  30. package/dist/core/tokenCounter.d.ts +8 -1
  31. package/dist/core/tokenCounter.js +28 -0
  32. package/dist/core/tokenCounter.js.map +1 -1
  33. package/dist/hitl/bridge.js +1 -27
  34. package/dist/hitl/bridge.js.map +1 -1
  35. package/dist/middleware/loopDetection.d.ts +7 -23
  36. package/dist/middleware/loopDetection.js +38 -42
  37. package/dist/middleware/loopDetection.js.map +1 -1
  38. package/dist/sandbox/whitelistedBackend.d.ts +5 -0
  39. package/dist/sandbox/whitelistedBackend.js +27 -0
  40. package/dist/sandbox/whitelistedBackend.js.map +1 -0
  41. package/dist/tools/askUser.d.ts +12 -3
  42. package/dist/tools/askUser.js +16 -28
  43. package/dist/tools/askUser.js.map +1 -1
  44. package/dist/tools/bashTool.d.ts +11 -0
  45. package/dist/tools/bashTool.js +51 -0
  46. package/dist/tools/bashTool.js.map +1 -0
  47. package/dist/tools/index.d.ts +15 -27
  48. package/dist/tools/index.js +9 -181
  49. package/dist/tools/index.js.map +1 -1
  50. package/dist/tools/installHostDeps.d.ts +8 -0
  51. package/dist/tools/installHostDeps.js +44 -0
  52. package/dist/tools/installHostDeps.js.map +1 -0
  53. package/dist/tracing/sessionTracer.d.ts +1 -0
  54. package/dist/tracing/sessionTracer.js +4 -1
  55. package/dist/tracing/sessionTracer.js.map +1 -1
  56. package/dist/ui/App.js +116 -55
  57. package/dist/ui/App.js.map +1 -1
  58. package/dist/ui/components/ActionLog.d.ts +7 -0
  59. package/dist/ui/components/ActionLog.js +63 -0
  60. package/dist/ui/components/ActionLog.js.map +1 -0
  61. package/dist/ui/components/FileBrowser.d.ts +2 -0
  62. package/dist/ui/components/FileBrowser.js +41 -0
  63. package/dist/ui/components/FileBrowser.js.map +1 -0
  64. package/dist/ui/components/MessageBubble.js +1 -1
  65. package/dist/ui/components/MessageBubble.js.map +1 -1
  66. package/package.json +8 -5
  67. package/AGENTS.md +0 -56
  68. package/Handover.md +0 -115
  69. package/PROGRESS.md +0 -160
  70. package/dist/__tests__/m55.test.js +0 -160
  71. package/dist/__tests__/m55.test.js.map +0 -1
  72. package/dist/__tests__/middleware.test.js +0 -169
  73. package/dist/__tests__/middleware.test.js.map +0 -1
  74. package/dist/__tests__/optimizations.test.d.ts +0 -1
  75. package/dist/__tests__/optimizations.test.js +0 -136
  76. package/dist/__tests__/optimizations.test.js.map +0 -1
  77. package/dist/__tests__/security.test.d.ts +0 -1
  78. package/dist/__tests__/security.test.js +0 -86
  79. package/dist/__tests__/security.test.js.map +0 -1
  80. package/dist/__tests__/streaming.test.d.ts +0 -1
  81. package/dist/__tests__/streaming.test.js +0 -71
  82. package/dist/__tests__/streaming.test.js.map +0 -1
  83. package/dist/__tests__/toolRouter.test.d.ts +0 -1
  84. package/dist/__tests__/toolRouter.test.js +0 -37
  85. package/dist/__tests__/toolRouter.test.js.map +0 -1
  86. package/dist/__tests__/tools.test.d.ts +0 -1
  87. package/dist/__tests__/tools.test.js +0 -112
  88. package/dist/__tests__/tools.test.js.map +0 -1
  89. package/dist/core/subAgent.d.ts +0 -56
  90. package/dist/core/subAgent.js +0 -240
  91. package/dist/core/subAgent.js.map +0 -1
  92. package/dist/debug_google.d.ts +0 -1
  93. package/dist/debug_google.js +0 -23
  94. package/dist/debug_google.js.map +0 -1
  95. package/dist/middleware/commandSanitizer.d.ts +0 -18
  96. package/dist/middleware/commandSanitizer.js +0 -50
  97. package/dist/middleware/commandSanitizer.js.map +0 -1
  98. package/dist/middleware/permission.d.ts +0 -17
  99. package/dist/middleware/permission.js +0 -59
  100. package/dist/middleware/permission.js.map +0 -1
  101. package/dist/middleware/pipeline.d.ts +0 -31
  102. package/dist/middleware/pipeline.js +0 -62
  103. package/dist/middleware/pipeline.js.map +0 -1
  104. package/dist/middleware/preCompletion.d.ts +0 -29
  105. package/dist/middleware/preCompletion.js +0 -82
  106. package/dist/middleware/preCompletion.js.map +0 -1
  107. package/dist/middleware/types.d.ts +0 -40
  108. package/dist/middleware/types.js +0 -8
  109. package/dist/middleware/types.js.map +0 -1
  110. package/dist/skills/loader.d.ts +0 -55
  111. package/dist/skills/loader.js +0 -132
  112. package/dist/skills/loader.js.map +0 -1
  113. package/dist/skills/tools.d.ts +0 -5
  114. package/dist/skills/tools.js +0 -78
  115. package/dist/skills/tools.js.map +0 -1
  116. package/dist/test_cache.d.ts +0 -1
  117. package/dist/test_cache.js +0 -55
  118. package/dist/test_cache.js.map +0 -1
  119. package/dist/test_google.d.ts +0 -1
  120. package/dist/test_google.js +0 -36
  121. package/dist/test_google.js.map +0 -1
  122. package/dist/tools/browser.d.ts +0 -19
  123. package/dist/tools/browser.js +0 -111
  124. package/dist/tools/browser.js.map +0 -1
  125. package/dist/tools/registry.d.ts +0 -31
  126. package/dist/tools/registry.js +0 -168
  127. package/dist/tools/registry.js.map +0 -1
  128. package/dist/tools/router.d.ts +0 -34
  129. package/dist/tools/router.js +0 -75
  130. package/dist/tools/router.js.map +0 -1
  131. package/dist/tools/security.d.ts +0 -28
  132. package/dist/tools/security.js +0 -183
  133. package/dist/tools/security.js.map +0 -1
  134. package/dist/tools/spawnAgent.d.ts +0 -19
  135. package/dist/tools/spawnAgent.js +0 -130
  136. package/dist/tools/spawnAgent.js.map +0 -1
  137. package/dist/tools/webSearch.d.ts +0 -6
  138. package/dist/tools/webSearch.js +0 -120
  139. package/dist/tools/webSearch.js.map +0 -1
  140. package/docs/01_insights_and_patterns.md +0 -27
  141. package/docs/02_edge_cases_and_mitigations.md +0 -143
  142. package/docs/03_initial_implementation_plan.md +0 -66
  143. package/docs/04_tech_stack_proposal.md +0 -20
  144. package/docs/05_prd.md +0 -87
  145. package/docs/06_user_stories.md +0 -72
  146. package/docs/07_system_architecture.md +0 -138
  147. package/docs/08_roadmap.md +0 -200
  148. package/e2b/Dockerfile +0 -26
  149. package/src/__tests__/bootstrap.test.ts +0 -111
  150. package/src/__tests__/config.test.ts +0 -97
  151. package/src/__tests__/m55.test.ts +0 -238
  152. package/src/__tests__/middleware.test.ts +0 -219
  153. package/src/__tests__/modelFactory.test.ts +0 -63
  154. package/src/__tests__/optimizations.test.ts +0 -201
  155. package/src/__tests__/promptBuilder.test.ts +0 -141
  156. package/src/__tests__/sandbox.test.ts +0 -102
  157. package/src/__tests__/security.test.ts +0 -122
  158. package/src/__tests__/streaming.test.ts +0 -82
  159. package/src/__tests__/toolRouter.test.ts +0 -52
  160. package/src/__tests__/tools.test.ts +0 -146
  161. package/src/__tests__/tracing.test.ts +0 -196
  162. package/src/agents/agentRegistry.ts +0 -69
  163. package/src/agents/agentSpec.ts +0 -67
  164. package/src/agents/builtinAgents.ts +0 -142
  165. package/src/cli/config.ts +0 -124
  166. package/src/cli/index.ts +0 -742
  167. package/src/cli/modelFactory.ts +0 -174
  168. package/src/cli/postinstall.ts +0 -28
  169. package/src/cli/providers.ts +0 -107
  170. package/src/commands/builtinCommands.ts +0 -293
  171. package/src/commands/commandRegistry.ts +0 -194
  172. package/src/core/agentLoop.d.ts.map +0 -1
  173. package/src/core/agentLoop.ts +0 -312
  174. package/src/core/autoSave.ts +0 -95
  175. package/src/core/compactor.ts +0 -252
  176. package/src/core/contextGuard.ts +0 -129
  177. package/src/core/errors.ts +0 -202
  178. package/src/core/promptBuilder.d.ts.map +0 -1
  179. package/src/core/promptBuilder.ts +0 -139
  180. package/src/core/reasoningRouter.ts +0 -121
  181. package/src/core/retry.ts +0 -75
  182. package/src/core/sessionResumer.ts +0 -90
  183. package/src/core/sessionStore.ts +0 -216
  184. package/src/core/subAgent.ts +0 -339
  185. package/src/core/tokenCounter.ts +0 -64
  186. package/src/evals/dataset.ts +0 -67
  187. package/src/evals/evaluator.ts +0 -81
  188. package/src/hitl/bridge.ts +0 -160
  189. package/src/middleware/commandSanitizer.ts +0 -60
  190. package/src/middleware/loopDetection.ts +0 -63
  191. package/src/middleware/permission.ts +0 -72
  192. package/src/middleware/pipeline.ts +0 -75
  193. package/src/middleware/preCompletion.ts +0 -94
  194. package/src/middleware/types.ts +0 -45
  195. package/src/sandbox/bootstrap.ts +0 -121
  196. package/src/sandbox/manager.ts +0 -239
  197. package/src/sandbox/sync.ts +0 -157
  198. package/src/skills/loader.ts +0 -143
  199. package/src/skills/tools.ts +0 -99
  200. package/src/skills/types.ts +0 -13
  201. package/src/test_cache.ts +0 -72
  202. package/src/tools/askUser.ts +0 -47
  203. package/src/tools/browser.ts +0 -137
  204. package/src/tools/index.d.ts.map +0 -1
  205. package/src/tools/index.ts +0 -237
  206. package/src/tools/registry.ts +0 -198
  207. package/src/tools/router.ts +0 -78
  208. package/src/tools/security.ts +0 -220
  209. package/src/tools/spawnAgent.ts +0 -158
  210. package/src/tools/webSearch.ts +0 -142
  211. package/src/tracing/analyzer.ts +0 -265
  212. package/src/tracing/langsmith.ts +0 -63
  213. package/src/tracing/sessionTracer.ts +0 -202
  214. package/src/tracing/types.ts +0 -49
  215. package/src/types/valyu.d.ts +0 -37
  216. package/src/ui/App.tsx +0 -404
  217. package/src/ui/components/HITLPrompt.tsx +0 -119
  218. package/src/ui/components/Header.tsx +0 -51
  219. package/src/ui/components/MessageBubble.tsx +0 -46
  220. package/src/ui/components/StatusBar.tsx +0 -138
  221. package/src/ui/components/StreamingText.tsx +0 -48
  222. package/src/ui/components/ToolCallPanel.tsx +0 -80
  223. package/tests/commands/commands.test.ts +0 -356
  224. package/tests/core/compactor.test.ts +0 -217
  225. package/tests/core/retryAndErrors.test.ts +0 -164
  226. package/tests/core/sessionResumer.test.ts +0 -95
  227. package/tests/core/sessionStore.test.ts +0 -84
  228. package/tests/core/stability.test.ts +0 -165
  229. package/tests/core/subAgent.test.ts +0 -238
  230. package/tests/hitl/hitlBridge.test.ts +0 -115
  231. package/tsconfig.json +0 -16
  232. package/vitest.config.ts +0 -10
  233. package/vitest.out +0 -48
  234. /package/dist/__tests__/{m55.test.d.ts → installHostDeps.test.d.ts} +0 -0
  235. /package/dist/__tests__/{middleware.test.d.ts → whitelistedBackend.test.d.ts} +0 -0
@@ -1,194 +0,0 @@
1
- /**
2
- * Slash Command Registry
3
- *
4
- * Intercepts user input starting with "/" in the TUI and routes it to
5
- * registered command handlers. This bypasses the agent loop entirely,
6
- * making slash commands zero-cost (no LLM tokens consumed).
7
- *
8
- * Architecture:
9
- * - Commands are self-contained objects implementing SlashCommand
10
- * - The registry supports aliases (e.g., /m → /model)
11
- * - Unknown commands suggest similar names via Levenshtein distance
12
- */
13
-
14
- import { ContextState } from "../core/promptBuilder.js";
15
- import { ExecutionHarness } from "../core/agentLoop.js";
16
- import { JooneConfig } from "../cli/config.js";
17
-
18
- // ─── Types ──────────────────────────────────────────────────────────────────────
19
-
20
- /**
21
- * Context passed to every slash command handler.
22
- * Provides read/write access to session state, config, and UI.
23
- */
24
- export interface CommandContext {
25
- /** Current Joone configuration. */
26
- config: JooneConfig;
27
- /** Path to the config file (for commands that modify config). */
28
- configPath: string;
29
- /** The execution harness (for commands that need LLM access, e.g., /compact). */
30
- harness: ExecutionHarness;
31
- /** Current conversation/context state. */
32
- contextState: ContextState;
33
- /** Setter for updating context state from a command. */
34
- setContextState: (state: ContextState) => void;
35
- /** Setter for pushing UI messages. */
36
- addSystemMessage: (content: string) => void;
37
- /** Current provider name. */
38
- provider: string;
39
- /** Current model name. */
40
- model: string;
41
- /** Max tokens (context window). */
42
- maxTokens: number;
43
- }
44
-
45
- /**
46
- * A registered slash command.
47
- */
48
- export interface SlashCommand {
49
- /** Primary name (without the leading /). */
50
- name: string;
51
- /** Optional aliases (e.g., ["m"] for /model). */
52
- aliases?: string[];
53
- /** Short description shown in /help. */
54
- description: string;
55
- /** Execute the command. Returns an optional string to display. */
56
- execute: (args: string, context: CommandContext) => Promise<string | void>;
57
- }
58
-
59
- // ─── Levenshtein Distance ───────────────────────────────────────────────────────
60
-
61
- /**
62
- * Computes the Levenshtein edit distance between two strings.
63
- * Used for "did you mean?" suggestions on unknown commands.
64
- */
65
- export function levenshteinDistance(a: string, b: string): number {
66
- const matrix: number[][] = [];
67
-
68
- for (let i = 0; i <= b.length; i++) {
69
- matrix[i] = [i];
70
- }
71
- for (let j = 0; j <= a.length; j++) {
72
- matrix[0][j] = j;
73
- }
74
-
75
- for (let i = 1; i <= b.length; i++) {
76
- for (let j = 1; j <= a.length; j++) {
77
- const cost = b.charAt(i - 1) === a.charAt(j - 1) ? 0 : 1;
78
- matrix[i][j] = Math.min(
79
- matrix[i - 1][j] + 1, // deletion
80
- matrix[i][j - 1] + 1, // insertion
81
- matrix[i - 1][j - 1] + cost // substitution
82
- );
83
- }
84
- }
85
-
86
- return matrix[b.length][a.length];
87
- }
88
-
89
- // ─── Registry ───────────────────────────────────────────────────────────────────
90
-
91
- export class CommandRegistry {
92
- private commands: Map<string, SlashCommand> = new Map();
93
- private aliasMap: Map<string, string> = new Map();
94
-
95
- /**
96
- * Register a slash command. Overwrites if name already exists.
97
- */
98
- register(command: SlashCommand): void {
99
- this.commands.set(command.name, command);
100
-
101
- if (command.aliases) {
102
- for (const alias of command.aliases) {
103
- this.aliasMap.set(alias, command.name);
104
- }
105
- }
106
- }
107
-
108
- /**
109
- * Returns true if the input looks like a slash command.
110
- */
111
- isCommand(input: string): boolean {
112
- return input.trimStart().startsWith("/");
113
- }
114
-
115
- /**
116
- * Parse user input into command name and arguments.
117
- */
118
- private parse(input: string): { name: string; args: string } {
119
- const trimmed = input.trimStart().slice(1); // Remove leading "/"
120
- const spaceIdx = trimmed.indexOf(" ");
121
- if (spaceIdx === -1) {
122
- return { name: trimmed.toLowerCase(), args: "" };
123
- }
124
- return {
125
- name: trimmed.slice(0, spaceIdx).toLowerCase(),
126
- args: trimmed.slice(spaceIdx + 1).trim(),
127
- };
128
- }
129
-
130
- /**
131
- * Execute a slash command from raw user input.
132
- * Returns the command output string, or an error/suggestion message.
133
- */
134
- async execute(input: string, context: CommandContext): Promise<string> {
135
- const { name, args } = this.parse(input);
136
-
137
- // Resolve alias → primary name
138
- const resolvedName = this.aliasMap.get(name) ?? name;
139
- const command = this.commands.get(resolvedName);
140
-
141
- if (command) {
142
- const result = await command.execute(args, context);
143
- return result ?? "";
144
- }
145
-
146
- // Unknown command — find suggestions
147
- const allNames = this.getAllNames();
148
- const suggestions = allNames
149
- .map((n) => ({ name: n, dist: levenshteinDistance(name, n) }))
150
- .filter((s) => s.dist <= 2) // Max 2 edits
151
- .sort((a, b) => a.dist - b.dist)
152
- .slice(0, 3)
153
- .map((s) => `/${s.name}`);
154
-
155
- let msg = `Unknown command: /${name}.`;
156
- if (suggestions.length > 0) {
157
- msg += ` Did you mean: ${suggestions.join(", ")}?`;
158
- }
159
- msg += ` Type /help for all commands.`;
160
- return msg;
161
- }
162
-
163
- /**
164
- * Returns all registered commands.
165
- */
166
- getAll(): SlashCommand[] {
167
- return Array.from(this.commands.values());
168
- }
169
-
170
- /**
171
- * Returns all command names AND aliases (for suggestion matching).
172
- */
173
- getAllNames(): string[] {
174
- const names = Array.from(this.commands.keys());
175
- const aliases = Array.from(this.aliasMap.keys());
176
- return [...names, ...aliases];
177
- }
178
-
179
- /**
180
- * Generates formatted help text for all registered commands.
181
- */
182
- getHelp(): string {
183
- const lines: string[] = ["Available commands:\n"];
184
-
185
- for (const cmd of this.commands.values()) {
186
- const aliases = cmd.aliases?.length
187
- ? ` (${cmd.aliases.map((a) => `/${a}`).join(", ")})`
188
- : "";
189
- lines.push(` /${cmd.name}${aliases} — ${cmd.description}`);
190
- }
191
-
192
- return lines.join("\n");
193
- }
194
- }
@@ -1 +0,0 @@
1
- {"version":3,"file":"agentLoop.d.ts","sourceRoot":"","sources":["agentLoop.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,SAAS,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,EAA+B,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAEhD,qBAAa,gBAAgB;IACzB,OAAO,CAAC,GAAG,CAAgB;IAC3B,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,cAAc,CAAyB;gBAEnC,SAAS,GAAE,MAAqC,EAAE,KAAK,GAAE,oBAAoB,EAAO;IAYhG;;;OAGG;IACU,IAAI,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC;IAW1D;;OAEG;IACU,gBAAgB,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;CAsC9E"}
@@ -1,312 +0,0 @@
1
- import { BaseChatModel } from "@langchain/core/language_models/chat_models";
2
- import { BaseMessage, AIMessage, ToolMessage, AIMessageChunk, HumanMessage } from "@langchain/core/messages";
3
- import { Runnable } from "@langchain/core/runnables";
4
- import { CacheOptimizedPromptBuilder, ContextState } from "./promptBuilder.js";
5
- import { DynamicToolInterface } from "../tools/index.js";
6
- import { MiddlewarePipeline } from "../middleware/pipeline.js";
7
- import { ToolCallContext } from "../middleware/types.js";
8
- import { SessionTracer } from "../tracing/sessionTracer.js";
9
- import { countMessageTokens } from "./tokenCounter.js";
10
- import { SessionStore } from "./sessionStore.js";
11
- import { retryWithBackoff } from "./retry.js";
12
- import { wrapLLMError, JooneError, ToolExecutionError } from "./errors.js";
13
- import { SystemMessage } from "@langchain/core/messages";
14
- import { ContextGuard } from "./contextGuard.js";
15
- import { AutoSave } from "./autoSave.js";
16
-
17
- export interface StreamStepOptions {
18
- /** Called for each text token received from the stream. */
19
- onToken?: (token: string) => void;
20
- }
21
-
22
- export class ExecutionHarness {
23
- private llm: Runnable<any, AIMessageChunk> | BaseChatModel;
24
- private promptBuilder: CacheOptimizedPromptBuilder;
25
- private availableTools: DynamicToolInterface[];
26
- private pipeline: MiddlewarePipeline;
27
- public tracer: SessionTracer;
28
- private sessionStore: SessionStore;
29
- public sessionId: string;
30
- private provider: string;
31
- private model: string;
32
- private contextGuard: ContextGuard;
33
- public autoSave: AutoSave;
34
-
35
- /**
36
- * Initializes the harness with a pre-configured, tool-bound LLM instance.
37
- * This allows swapping between Anthropic Claude, OpenAI GPT-4, Google Gemini, etc.
38
- */
39
- constructor(
40
- boundLlm: Runnable<any, AIMessageChunk> | BaseChatModel,
41
- tools: DynamicToolInterface[] = [],
42
- pipeline?: MiddlewarePipeline,
43
- tracer?: SessionTracer,
44
- provider: string = "unknown",
45
- model: string = "unknown",
46
- sessionId?: string,
47
- maxTokens: number = 4096
48
- ) {
49
- this.llm = boundLlm;
50
- this.promptBuilder = new CacheOptimizedPromptBuilder();
51
- this.availableTools = tools;
52
- this.pipeline = pipeline ?? new MiddlewarePipeline();
53
- this.tracer = tracer ?? new SessionTracer();
54
- this.sessionStore = new SessionStore();
55
- this.sessionId = sessionId ?? this.tracer.getSessionId();
56
- this.provider = provider;
57
- this.model = model;
58
- this.contextGuard = new ContextGuard(this.llm, maxTokens, this.promptBuilder);
59
- this.autoSave = new AutoSave(this.sessionId, this.sessionStore);
60
- }
61
-
62
- /**
63
- * The main execution engine (non-streaming).
64
- * Takes the context state, builds the cache-optimized prompt, and queries the LLM.
65
- */
66
- public async step(state: ContextState): Promise<AIMessage> {
67
- const start = Date.now();
68
-
69
- // ContextGuard: Check capacity before building prompt
70
- const { state: updatedState, metrics } = await this.contextGuard.ensureCapacity(state);
71
- state = updatedState; // Reassign state if compacted
72
-
73
- const messages = this.promptBuilder.buildPrompt(state);
74
-
75
- try {
76
- const response = await retryWithBackoff(
77
- () => this.llm.invoke(messages).catch((e) => { throw wrapLLMError(e, this.provider); }),
78
- {
79
- onRetry: (attempt, error, delay) => {
80
- this.tracer.recordError({ message: `LLM retry #${attempt}: ${error.message} (waiting ${delay}ms)` });
81
- },
82
- }
83
- );
84
-
85
- const promptTokens = countMessageTokens(messages);
86
- const completionTokens = countMessageTokens([response as AIMessage]);
87
-
88
- this.tracer.recordLLMCall({
89
- promptTokens,
90
- completionTokens,
91
- cached: false,
92
- duration: Date.now() - start
93
- });
94
-
95
- await this.autoSave.tick({ config: { provider: this.provider, model: this.model }, state });
96
- return response as AIMessage;
97
- } catch (error: unknown) {
98
- // Self-recovery: inject the error hint and let the agent adapt
99
- if (error instanceof JooneError && error.retryable) {
100
- this.tracer.recordError({ message: `LLM retries exhausted: ${error.message}` });
101
- state.conversationHistory.push(new HumanMessage(`<system-alert>\nSystem recovery hint:\n${error.toRecoveryHint()}\n</system-alert>`));
102
- await this.autoSave.forceSave({ config: { provider: this.provider, model: this.model }, state });
103
- // Return a synthetic AI message so the turn doesn't crash
104
- return new AIMessage(error.toRecoveryHint());
105
- }
106
- throw error; // Fatal (auth, config) — propagate to TUI
107
- }
108
- }
109
-
110
- /**
111
- * Streaming execution engine.
112
- * Streams text tokens via the onToken callback and buffers tool call chunks
113
- * until the full call is received. Returns a complete AIMessage for history.
114
- */
115
- public async streamStep(
116
- state: ContextState,
117
- options: StreamStepOptions
118
- ): Promise<AIMessage> {
119
- const start = Date.now();
120
-
121
- // ContextGuard: Check capacity before building prompt
122
- const { state: updatedState, metrics } = await this.contextGuard.ensureCapacity(state);
123
- state = updatedState;
124
-
125
- const messages = this.promptBuilder.buildPrompt(state);
126
-
127
- try {
128
- const result = await retryWithBackoff(
129
- async () => {
130
- let fullContent = "";
131
- const toolCallBuffers: Map<number, { id: string; name: string; argsJson: string }> = new Map();
132
-
133
- let stream: AsyncIterable<any>;
134
- try {
135
- stream = await (this.llm as any).stream(messages);
136
- } catch (e) {
137
- throw wrapLLMError(e, this.provider);
138
- }
139
-
140
- for await (const chunk of stream) {
141
- if (chunk.content && typeof chunk.content === "string") {
142
- fullContent += chunk.content;
143
- if (options.onToken) {
144
- options.onToken(chunk.content);
145
- }
146
- }
147
-
148
- if (chunk.tool_call_chunks && chunk.tool_call_chunks.length > 0) {
149
- for (const tc of chunk.tool_call_chunks) {
150
- const idx = tc.index ?? 0;
151
- if (!toolCallBuffers.has(idx)) {
152
- toolCallBuffers.set(idx, {
153
- id: tc.id || "",
154
- name: tc.name || "",
155
- argsJson: "",
156
- });
157
- }
158
- const buf = toolCallBuffers.get(idx)!;
159
- if (tc.id) buf.id = tc.id;
160
- if (tc.name) buf.name = tc.name;
161
- if (tc.args) buf.argsJson += tc.args;
162
- }
163
- }
164
- }
165
-
166
- const toolCalls = Array.from(toolCallBuffers.values()).map((buf) => ({
167
- id: buf.id,
168
- name: buf.name,
169
- args: (() => {
170
- try {
171
- return JSON.parse(buf.argsJson || "{}");
172
- } catch {
173
- return { _parseError: true, rawArgs: buf.argsJson };
174
- }
175
- })(),
176
- type: "tool_call" as const,
177
- }));
178
-
179
- return new AIMessage({
180
- content: fullContent,
181
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
182
- });
183
- },
184
- {
185
- onRetry: (attempt, error, delay) => {
186
- this.tracer.recordError({ message: `LLM stream retry #${attempt}: ${error.message} (waiting ${delay}ms)` });
187
- },
188
- }
189
- );
190
-
191
- const promptTokens = countMessageTokens(messages);
192
- const completionTokens = countMessageTokens([result]);
193
-
194
- this.tracer.recordLLMCall({
195
- promptTokens,
196
- completionTokens,
197
- cached: false,
198
- duration: Date.now() - start
199
- });
200
-
201
- await this.autoSave.tick({ config: { provider: this.provider, model: this.model }, state });
202
- return result;
203
- } catch (error: unknown) {
204
- // Self-recovery for streaming
205
- if (error instanceof JooneError && error.retryable) {
206
- this.tracer.recordError({ message: `LLM stream retries exhausted: ${(error as JooneError).message}` });
207
- state.conversationHistory.push(new HumanMessage(`<system-alert>\nSystem recovery hint:\n${(error as JooneError).toRecoveryHint()}\n</system-alert>`));
208
- await this.autoSave.forceSave({ config: { provider: this.provider, model: this.model }, state });
209
- return new AIMessage((error as JooneError).toRecoveryHint());
210
- }
211
- throw error;
212
- }
213
- }
214
-
215
- /**
216
- * Executes tool calls from an AI response, routing through the middleware pipeline.
217
- * Each call passes through all registered before-hooks, then the tool, then after-hooks.
218
- */
219
- public async executeToolCalls(aiMessage: AIMessage, state: ContextState): Promise<(ToolMessage | HumanMessage)[]> {
220
- const results: (ToolMessage | HumanMessage)[] = [];
221
-
222
- if (!aiMessage.tool_calls || aiMessage.tool_calls.length === 0) {
223
- return results;
224
- }
225
-
226
- for (const call of aiMessage.tool_calls) {
227
- // Soft-Fail Edge Case: If the LLM omits the tool_call_id, do not execute the tool.
228
- // Return a HumanMessage prompting correction instead of a malformed ToolMessage.
229
- if (!call.id) {
230
- this.tracer.recordError({ message: `Malformed tool call: Missing ID for ${call.name}`, tool: call.name });
231
- results.push(new HumanMessage(
232
- `You attempted to call the tool '${call.name}', but you did not provide a tool_call_id. ` +
233
- `This is a malformed request. Please try again and ensure you provide a valid ID.`
234
- ));
235
- continue;
236
- }
237
-
238
- const safeCallId = call.id;
239
-
240
- // Handle malformed args from streaming parse errors
241
- if (call.args && call.args._parseError) {
242
- this.tracer.recordError({ message: `Failed to parse tool arguments`, tool: call.name });
243
- results.push(new ToolMessage({
244
- content: `Error: Failed to parse tool arguments. The JSON provided was malformed:\n${call.args.rawArgs}\nPlease correct the JSON format and try again.`,
245
- tool_call_id: safeCallId
246
- }));
247
- continue;
248
- }
249
-
250
- const tool = this.availableTools.find(t => t.name === call.name);
251
- if (!tool) {
252
- this.tracer.recordError({ message: `Tool ${call.name} not found`, tool: call.name });
253
- results.push(new ToolMessage({
254
- content: `Error: Tool ${call.name} not found.`,
255
- tool_call_id: safeCallId
256
- }));
257
- continue;
258
- }
259
-
260
- const ctx: ToolCallContext = {
261
- toolName: call.name,
262
- args: call.args,
263
- callId: safeCallId,
264
- };
265
-
266
- const start = Date.now();
267
- try {
268
- const output = await this.pipeline.run(
269
- ctx,
270
- async (c) => tool.execute(c.args)
271
- );
272
- this.tracer.recordToolCall({
273
- name: call.name,
274
- args: call.args,
275
- result: typeof output === "string" ? output : JSON.stringify(output).substring(0, 100),
276
- duration: Date.now() - start,
277
- success: true
278
- });
279
-
280
- const stringifiedOutput = typeof output === "string" ? output : JSON.stringify(output);
281
- results.push(new ToolMessage({
282
- content: stringifiedOutput,
283
- tool_call_id: safeCallId
284
- }));
285
- } catch (error: any) {
286
- const toolError = new ToolExecutionError(error.message, {
287
- toolName: call.name,
288
- args: call.args,
289
- retryable: false,
290
- cause: error,
291
- });
292
- this.tracer.recordToolCall({
293
- name: call.name,
294
- args: call.args,
295
- duration: Date.now() - start,
296
- success: false
297
- });
298
- this.tracer.recordError({ message: toolError.message, tool: call.name });
299
- results.push(new ToolMessage({
300
- content: toolError.toRecoveryHint(),
301
- tool_call_id: safeCallId
302
- }));
303
- }
304
- }
305
-
306
- // Add the tool results to the state immediately before saving
307
- state.conversationHistory.push(...results);
308
- await this.autoSave.tick({ config: { provider: this.provider, model: this.model }, state });
309
-
310
- return results;
311
- }
312
- }
@@ -1,95 +0,0 @@
1
- /**
2
- * Auto-Save
3
- *
4
- * Periodically saves the session state to disk using atomic writes.
5
- * This prevents data loss if the terminal crashes or the LLM loop hangs.
6
- * Atomic writes ensure the JSONL session file is never corrupted during save.
7
- */
8
-
9
- import { SessionStore } from "./sessionStore.js";
10
- import { ContextState } from "./promptBuilder.js";
11
- import { ActiveToolCall } from "../ui/App.js";
12
-
13
- // We extract just what we need for auto-save from the harness
14
- export interface AutoSaveData {
15
- config: { provider: string; model: string };
16
- state: ContextState;
17
- activeTool?: ActiveToolCall;
18
- }
19
-
20
- export class AutoSave {
21
- private store: SessionStore;
22
- private sessionId: string;
23
- private saveFrequencyTurns: number;
24
- private turnsSinceLastSave = 0;
25
- private lastSaveTime = 0;
26
- private debounceMs: number;
27
- private isSaving = false;
28
-
29
- constructor(
30
- sessionId: string,
31
- store: SessionStore = new SessionStore(),
32
- saveFrequencyTurns = 5,
33
- debounceMs = 10000 // 10 seconds minimum between saves
34
- ) {
35
- this.sessionId = sessionId;
36
- this.store = store;
37
- this.saveFrequencyTurns = saveFrequencyTurns;
38
- this.debounceMs = debounceMs;
39
- }
40
-
41
- /**
42
- * Called at the end of every agent loop turn.
43
- * Periodically triggers an atomic save.
44
- */
45
- async tick(data: AutoSaveData): Promise<boolean> {
46
- this.turnsSinceLastSave++;
47
-
48
- const now = Date.now();
49
- const shouldSave =
50
- this.turnsSinceLastSave >= this.saveFrequencyTurns &&
51
- now - this.lastSaveTime >= this.debounceMs;
52
-
53
- if (shouldSave && !this.isSaving) {
54
- await this.forceSave(data);
55
- return true;
56
- }
57
-
58
- return false;
59
- }
60
-
61
- /**
62
- * Forces an immediate atomic save, e.g., during SIGINT/SIGTERM shutdown.
63
- */
64
- async forceSave(data: AutoSaveData): Promise<void> {
65
- if (this.isSaving) return; // Prevent concurrent overlapping saves
66
- this.isSaving = true;
67
-
68
- try {
69
- // SessionStore.saveSession creates a write stream directly to a new cleanly formatted JSONL payload.
70
- await this.store.saveSession(
71
- this.sessionId,
72
- data.state,
73
- data.config.provider,
74
- data.config.model
75
- );
76
-
77
- this.turnsSinceLastSave = 0;
78
- this.lastSaveTime = Date.now();
79
- } catch (err: any) {
80
- // We explicitly swallow auto-save errors so they don't crash the agent loop.
81
- // E2B Sandboxes and other critical operations take precedence.
82
- console.error(`\n[AutoSave Failed] ${err.message}`);
83
- } finally {
84
- this.isSaving = false;
85
- }
86
- }
87
-
88
- /**
89
- * Resets the counter.
90
- */
91
- resetTimer(): void {
92
- this.turnsSinceLastSave = 0;
93
- this.lastSaveTime = Date.now();
94
- }
95
- }