joonecli 0.1.0 → 0.2.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 (184) hide show
  1. package/README.md +12 -12
  2. package/dist/__tests__/optimizations.test.js.map +1 -1
  3. package/dist/__tests__/promptBuilder.test.js +14 -20
  4. package/dist/__tests__/promptBuilder.test.js.map +1 -1
  5. package/dist/agents/agentRegistry.d.ts +37 -0
  6. package/dist/agents/agentRegistry.js +58 -0
  7. package/dist/agents/agentRegistry.js.map +1 -0
  8. package/dist/agents/agentSpec.d.ts +54 -0
  9. package/dist/agents/agentSpec.js +9 -0
  10. package/dist/agents/agentSpec.js.map +1 -0
  11. package/dist/agents/builtinAgents.d.ts +20 -0
  12. package/{src/agents/builtinAgents.ts → dist/agents/builtinAgents.js} +84 -101
  13. package/dist/agents/builtinAgents.js.map +1 -0
  14. package/dist/cli/config.d.ts +4 -0
  15. package/dist/cli/config.js.map +1 -1
  16. package/dist/cli/index.js +29 -2
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/postinstall.d.ts +2 -0
  19. package/dist/cli/postinstall.js +25 -0
  20. package/dist/cli/postinstall.js.map +1 -0
  21. package/dist/commands/builtinCommands.d.ts +21 -0
  22. package/dist/commands/builtinCommands.js +241 -0
  23. package/dist/commands/builtinCommands.js.map +1 -0
  24. package/dist/commands/commandRegistry.d.ts +92 -0
  25. package/dist/commands/commandRegistry.js +128 -0
  26. package/dist/commands/commandRegistry.js.map +1 -0
  27. package/dist/core/agentLoop.d.ts +7 -2
  28. package/dist/core/agentLoop.js +35 -13
  29. package/dist/core/agentLoop.js.map +1 -1
  30. package/dist/core/autoSave.d.ts +41 -0
  31. package/dist/core/autoSave.js +69 -0
  32. package/dist/core/autoSave.js.map +1 -0
  33. package/dist/core/compactor.d.ts +66 -0
  34. package/dist/core/compactor.js +170 -0
  35. package/dist/core/compactor.js.map +1 -0
  36. package/dist/core/contextGuard.d.ts +38 -0
  37. package/dist/core/contextGuard.js +122 -0
  38. package/dist/core/contextGuard.js.map +1 -0
  39. package/dist/core/events.d.ts +45 -0
  40. package/dist/core/events.js +8 -0
  41. package/dist/core/events.js.map +1 -0
  42. package/dist/core/promptBuilder.d.ts +16 -1
  43. package/dist/core/promptBuilder.js +27 -14
  44. package/dist/core/promptBuilder.js.map +1 -1
  45. package/dist/core/sessionResumer.js +3 -3
  46. package/dist/core/sessionResumer.js.map +1 -1
  47. package/dist/core/sessionStore.js +3 -2
  48. package/dist/core/sessionStore.js.map +1 -1
  49. package/dist/core/subAgent.d.ts +56 -0
  50. package/dist/core/subAgent.js +240 -0
  51. package/dist/core/subAgent.js.map +1 -0
  52. package/dist/core/tokenCounter.d.ts +8 -1
  53. package/dist/core/tokenCounter.js +28 -0
  54. package/dist/core/tokenCounter.js.map +1 -1
  55. package/dist/debug_google.d.ts +1 -0
  56. package/dist/debug_google.js +23 -0
  57. package/dist/debug_google.js.map +1 -0
  58. package/dist/middleware/permission.js +1 -0
  59. package/dist/middleware/permission.js.map +1 -1
  60. package/dist/test_google.d.ts +1 -0
  61. package/dist/test_google.js +32 -89
  62. package/dist/test_google.js.map +1 -0
  63. package/dist/tools/browser.js +4 -1
  64. package/dist/tools/browser.js.map +1 -1
  65. package/dist/tools/index.d.ts +2 -1
  66. package/dist/tools/index.js +11 -3
  67. package/dist/tools/index.js.map +1 -1
  68. package/dist/tools/installHostDeps.d.ts +2 -0
  69. package/dist/tools/installHostDeps.js +37 -0
  70. package/dist/tools/installHostDeps.js.map +1 -0
  71. package/dist/tools/router.js +3 -0
  72. package/dist/tools/router.js.map +1 -1
  73. package/dist/tools/spawnAgent.d.ts +19 -0
  74. package/dist/tools/spawnAgent.js +132 -0
  75. package/dist/tools/spawnAgent.js.map +1 -0
  76. package/dist/tracing/sessionTracer.d.ts +1 -0
  77. package/dist/tracing/sessionTracer.js +4 -1
  78. package/dist/tracing/sessionTracer.js.map +1 -1
  79. package/dist/ui/App.js +94 -6
  80. package/dist/ui/App.js.map +1 -1
  81. package/dist/ui/components/ActionLog.d.ts +7 -0
  82. package/dist/ui/components/ActionLog.js +63 -0
  83. package/dist/ui/components/ActionLog.js.map +1 -0
  84. package/dist/ui/components/FileBrowser.d.ts +2 -0
  85. package/dist/ui/components/FileBrowser.js +41 -0
  86. package/dist/ui/components/FileBrowser.js.map +1 -0
  87. package/package.json +5 -6
  88. package/AGENTS.md +0 -56
  89. package/Handover.md +0 -115
  90. package/PROGRESS.md +0 -160
  91. package/docs/01_insights_and_patterns.md +0 -27
  92. package/docs/02_edge_cases_and_mitigations.md +0 -143
  93. package/docs/03_initial_implementation_plan.md +0 -66
  94. package/docs/04_tech_stack_proposal.md +0 -20
  95. package/docs/05_prd.md +0 -87
  96. package/docs/06_user_stories.md +0 -72
  97. package/docs/07_system_architecture.md +0 -138
  98. package/docs/08_roadmap.md +0 -200
  99. package/e2b/Dockerfile +0 -26
  100. package/src/__tests__/bootstrap.test.ts +0 -111
  101. package/src/__tests__/config.test.ts +0 -97
  102. package/src/__tests__/m55.test.ts +0 -238
  103. package/src/__tests__/middleware.test.ts +0 -219
  104. package/src/__tests__/modelFactory.test.ts +0 -63
  105. package/src/__tests__/optimizations.test.ts +0 -201
  106. package/src/__tests__/promptBuilder.test.ts +0 -141
  107. package/src/__tests__/sandbox.test.ts +0 -102
  108. package/src/__tests__/security.test.ts +0 -122
  109. package/src/__tests__/streaming.test.ts +0 -82
  110. package/src/__tests__/toolRouter.test.ts +0 -52
  111. package/src/__tests__/tools.test.ts +0 -146
  112. package/src/__tests__/tracing.test.ts +0 -196
  113. package/src/agents/agentRegistry.ts +0 -69
  114. package/src/agents/agentSpec.ts +0 -67
  115. package/src/cli/config.ts +0 -124
  116. package/src/cli/index.ts +0 -730
  117. package/src/cli/modelFactory.ts +0 -174
  118. package/src/cli/providers.ts +0 -107
  119. package/src/commands/builtinCommands.ts +0 -293
  120. package/src/commands/commandRegistry.ts +0 -194
  121. package/src/core/agentLoop.d.ts.map +0 -1
  122. package/src/core/agentLoop.ts +0 -312
  123. package/src/core/autoSave.ts +0 -95
  124. package/src/core/compactor.ts +0 -252
  125. package/src/core/contextGuard.ts +0 -129
  126. package/src/core/errors.ts +0 -202
  127. package/src/core/promptBuilder.d.ts.map +0 -1
  128. package/src/core/promptBuilder.ts +0 -139
  129. package/src/core/reasoningRouter.ts +0 -121
  130. package/src/core/retry.ts +0 -75
  131. package/src/core/sessionResumer.ts +0 -90
  132. package/src/core/sessionStore.ts +0 -215
  133. package/src/core/subAgent.ts +0 -339
  134. package/src/core/tokenCounter.ts +0 -64
  135. package/src/evals/dataset.ts +0 -67
  136. package/src/evals/evaluator.ts +0 -81
  137. package/src/hitl/bridge.ts +0 -160
  138. package/src/middleware/commandSanitizer.ts +0 -60
  139. package/src/middleware/loopDetection.ts +0 -63
  140. package/src/middleware/permission.ts +0 -72
  141. package/src/middleware/pipeline.ts +0 -75
  142. package/src/middleware/preCompletion.ts +0 -94
  143. package/src/middleware/types.ts +0 -45
  144. package/src/sandbox/bootstrap.ts +0 -121
  145. package/src/sandbox/manager.ts +0 -239
  146. package/src/sandbox/sync.ts +0 -157
  147. package/src/skills/loader.ts +0 -143
  148. package/src/skills/tools.ts +0 -99
  149. package/src/skills/types.ts +0 -13
  150. package/src/test_cache.ts +0 -72
  151. package/src/test_google.js +0 -40
  152. package/src/test_google.ts +0 -40
  153. package/src/tools/askUser.ts +0 -47
  154. package/src/tools/browser.ts +0 -137
  155. package/src/tools/index.d.ts.map +0 -1
  156. package/src/tools/index.ts +0 -237
  157. package/src/tools/registry.ts +0 -198
  158. package/src/tools/router.ts +0 -78
  159. package/src/tools/security.ts +0 -220
  160. package/src/tools/spawnAgent.ts +0 -158
  161. package/src/tools/webSearch.ts +0 -142
  162. package/src/tracing/analyzer.ts +0 -265
  163. package/src/tracing/langsmith.ts +0 -63
  164. package/src/tracing/sessionTracer.ts +0 -202
  165. package/src/tracing/types.ts +0 -49
  166. package/src/types/valyu.d.ts +0 -37
  167. package/src/ui/App.tsx +0 -404
  168. package/src/ui/components/HITLPrompt.tsx +0 -119
  169. package/src/ui/components/Header.tsx +0 -51
  170. package/src/ui/components/MessageBubble.tsx +0 -46
  171. package/src/ui/components/StatusBar.tsx +0 -138
  172. package/src/ui/components/StreamingText.tsx +0 -48
  173. package/src/ui/components/ToolCallPanel.tsx +0 -80
  174. package/tests/commands/commands.test.ts +0 -356
  175. package/tests/core/compactor.test.ts +0 -217
  176. package/tests/core/retryAndErrors.test.ts +0 -164
  177. package/tests/core/sessionResumer.test.ts +0 -95
  178. package/tests/core/sessionStore.test.ts +0 -84
  179. package/tests/core/stability.test.ts +0 -165
  180. package/tests/core/subAgent.test.ts +0 -238
  181. package/tests/hitl/hitlBridge.test.ts +0 -115
  182. package/tsconfig.json +0 -16
  183. package/vitest.config.ts +0 -10
  184. package/vitest.out +0 -48
@@ -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
- }