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,139 +0,0 @@
1
- import {
2
- BaseMessage,
3
- SystemMessage,
4
- HumanMessage,
5
- AIMessage,
6
- } from "@langchain/core/messages";
7
- import { BaseChatModel } from "@langchain/core/language_models/chat_models";
8
- import { Runnable } from "@langchain/core/runnables";
9
- import { countMessageTokens } from "./tokenCounter.js";
10
- import { ConversationCompactor, CompactionResult } from "./compactor.js";
11
-
12
- export interface ContextState {
13
- globalSystemInstructions: string;
14
- projectMemory: string;
15
- sessionContext: string;
16
- conversationHistory: BaseMessage[];
17
- }
18
-
19
- /**
20
- * CacheOptimizedPromptBuilder
21
- *
22
- * Enforces strict prefix ordering to maximize Claude's Prompt Caching.
23
- *
24
- * Order of Prefix (Static to Dynamic):
25
- * 1. Base System Instructions + Tool Definitions (Globally Cached)
26
- * 2. Project Memory (e.g., CLAUDE.md) (Cached per project)
27
- * 3. Session State (Environment variables) (Cached per session)
28
- * 4. Conversation Messages (Grows iteratively)
29
- */
30
- export class CacheOptimizedPromptBuilder {
31
- /**
32
- * Compiles the full message array for the LLM request.
33
- * The first messages are static, and subsequent ones are dynamic.
34
- */
35
- public buildPrompt(state: ContextState): BaseMessage[] {
36
- // We use SystemMessages for the static prefix.
37
- // In @langchain/anthropic, to use cache_control, we can inject it into the final message of each tier if needed,
38
- // but preserving the exact order of the system prompts is the main requirement.
39
-
40
- const unifiedContent = [
41
- state.globalSystemInstructions,
42
- `--- Project Context ---\n${state.projectMemory}`,
43
- `--- Session Rules ---\n${state.sessionContext}`,
44
- ].join("\n\n");
45
-
46
- const systemMessages: BaseMessage[] = [
47
- new SystemMessage({
48
- content: unifiedContent,
49
- name: "global_context",
50
- }),
51
- ];
52
-
53
- // Combine the static prefix with the dynamic conversation history
54
- return [...systemMessages, ...state.conversationHistory];
55
- }
56
-
57
- /**
58
- * The System Reminder Pattern
59
- * Instead of replacing the System Prompt (which breaks cache),
60
- * use this to inject state updates into the Conversation History.
61
- */
62
- public injectSystemReminder(
63
- history: BaseMessage[],
64
- reminder: string,
65
- ): BaseMessage[] {
66
- const reminderMsg = new HumanMessage({
67
- content: `<system-reminder>\n${reminder}\n</system-reminder>`,
68
- });
69
- return [...history, reminderMsg];
70
- }
71
-
72
- /**
73
- * Cache-Safe Compaction (string-based fallback)
74
- * When history gets too long, we preserve the last N messages (recent context)
75
- * and replace older messages with a summary. The static system prefix is untouched.
76
- *
77
- * @param history - The full conversation history.
78
- * @param summary - A text summary of the older messages.
79
- * @param keepLastN - Number of recent messages to preserve (default: 6).
80
- */
81
- public compactHistory(
82
- history: BaseMessage[],
83
- summary: string,
84
- keepLastN = 6,
85
- ): BaseMessage[] {
86
- if (history.length === 0) {
87
- return history;
88
- }
89
-
90
- // Use HumanMessage formatted as a system update to avoid breaking Google's validation
91
- // and maintaining proper user/assistant flow.
92
- const compactedMessage = new HumanMessage(
93
- `<system-update>\n[The previous conversation history has been compacted.]\nSummary:\n${summary}\n</system-update>`,
94
- );
95
-
96
- // Preserve recent messages for continuity
97
- const recentMessages = history.slice(-keepLastN);
98
-
99
- return [compactedMessage, ...recentMessages];
100
- }
101
-
102
- /**
103
- * LLM-Powered Compaction with Handoff
104
- * Uses a dedicated LLM call to generate a structured summary, then injects
105
- * a handoff prompt to orient the agent. Falls back to string-based compaction
106
- * if the LLM call fails.
107
- *
108
- * @param history - The full conversation history.
109
- * @param llm - The LLM to use for summarization (should be a fast/cheap model).
110
- * @param keepLastN - Number of recent messages to preserve (default: 8).
111
- * @returns CompactionResult with the new history and metrics.
112
- */
113
- public async compactHistoryWithLLM(
114
- history: BaseMessage[],
115
- llm: Runnable | BaseChatModel,
116
- keepLastN = 8,
117
- ): Promise<CompactionResult> {
118
- const compactor = new ConversationCompactor();
119
- return compactor.compact(history, llm, { keepLastN });
120
- }
121
-
122
- /**
123
- * Checks if the conversation should be compacted based on token usage.
124
- *
125
- * @param state - The current context state.
126
- * @param maxTokens - The model's context window.
127
- * @param threshold - Fraction of capacity to trigger (default: 0.8).
128
- */
129
- public shouldCompact(
130
- state: ContextState,
131
- maxTokens: number,
132
- threshold = 0.8,
133
- ): boolean {
134
- const allMessages = this.buildPrompt(state);
135
- const usage = countMessageTokens(allMessages);
136
- return usage >= maxTokens * threshold;
137
- }
138
- }
139
-
@@ -1,121 +0,0 @@
1
- /**
2
- * Reasoning Sandwich — Dynamic Reasoning Router
3
- *
4
- * Adjusts the reasoning intensity per turn:
5
- * - HIGH: Planning, recovery after errors, final verification
6
- * - MEDIUM: Mechanical code writing, tool-heavy turns
7
- *
8
- * We adjust temperature only (not model variant) to preserve prompt cache prefix.
9
- * See docs/02_edge_cases_and_mitigations.md — "The Mid-Session Model Switch."
10
- */
11
-
12
- export enum ReasoningLevel {
13
- HIGH = "high",
14
- MEDIUM = "medium",
15
- }
16
-
17
- export interface ReasoningConfig {
18
- /** Temperature for HIGH reasoning (default: 0). */
19
- highTemp: number;
20
- /** Temperature for MEDIUM reasoning (default: 0.2). */
21
- mediumTemp: number;
22
- /** Number of initial turns that always use HIGH reasoning (default: 2). */
23
- planningTurns: number;
24
- }
25
-
26
- const DEFAULT_CONFIG: ReasoningConfig = {
27
- highTemp: 0,
28
- mediumTemp: 0.2,
29
- planningTurns: 2,
30
- };
31
-
32
- /**
33
- * Tracks turn context and decides the reasoning level for each step.
34
- */
35
- export class ReasoningRouter {
36
- private config: ReasoningConfig;
37
- private turnCount = 0;
38
- private lastTurnHadError = false;
39
- private consecutiveToolTurns = 0;
40
-
41
- constructor(config?: Partial<ReasoningConfig>) {
42
- this.config = { ...DEFAULT_CONFIG, ...config };
43
- }
44
-
45
- /**
46
- * Advances to the next turn and records the outcome of the previous one.
47
- * Call exactly once per turn BEFORE querying the level for the new turn.
48
- *
49
- * @param hasToolCalls - Whether the previous response contained tool calls.
50
- * @param hadError - Whether the previous turn resulted in an error.
51
- */
52
- advanceTurn(hasToolCalls = false, hadError = false): void {
53
- this.turnCount++;
54
- this.lastTurnHadError = hadError;
55
-
56
- if (hadError) {
57
- this.consecutiveToolTurns = 0;
58
- } else if (hasToolCalls) {
59
- this.consecutiveToolTurns++;
60
- } else {
61
- this.consecutiveToolTurns = 0;
62
- }
63
- }
64
-
65
- /**
66
- * Determines the reasoning level for the current turn based on state.
67
- *
68
- * @returns The recommended reasoning level.
69
- */
70
- getLevel(): ReasoningLevel {
71
- // First N turns → always HIGH (planning phase)
72
- if (this.turnCount <= this.config.planningTurns) {
73
- return ReasoningLevel.HIGH;
74
- }
75
-
76
- // Post-error → HIGH (recovery)
77
- if (this.lastTurnHadError) {
78
- return ReasoningLevel.HIGH;
79
- }
80
-
81
- // Tool-heavy turn → MEDIUM (mechanical work)
82
- if (this.consecutiveToolTurns > 0) {
83
- return ReasoningLevel.MEDIUM;
84
- }
85
-
86
- // No tool calls previously (agent is thinking/planning) → HIGH
87
- return ReasoningLevel.HIGH;
88
- }
89
-
90
- /**
91
- * Returns the temperature setting for a given reasoning level.
92
- */
93
- getTemperature(level: ReasoningLevel): number {
94
- return level === ReasoningLevel.HIGH
95
- ? this.config.highTemp
96
- : this.config.mediumTemp;
97
- }
98
-
99
- /**
100
- * Convenience: get the recommended temperature for the current turn.
101
- */
102
- getRecommendedTemperature(): number {
103
- return this.getTemperature(this.getLevel());
104
- }
105
-
106
- /**
107
- * Returns the current turn count.
108
- */
109
- getTurnCount(): number {
110
- return this.turnCount;
111
- }
112
-
113
- /**
114
- * Resets state. For testing or new sessions.
115
- */
116
- reset(): void {
117
- this.turnCount = 0;
118
- this.lastTurnHadError = false;
119
- this.consecutiveToolTurns = 0;
120
- }
121
- }
package/src/core/retry.ts DELETED
@@ -1,75 +0,0 @@
1
- import { JooneError } from "./errors.js";
2
-
3
- export interface RetryOptions {
4
- /** Maximum number of retry attempts. Default: 3. */
5
- maxRetries?: number;
6
- /** Initial delay in milliseconds before the first retry. Default: 1000. */
7
- initialDelayMs?: number;
8
- /** Maximum jitter in milliseconds added/subtracted from each delay. Default: 500. */
9
- maxJitterMs?: number;
10
- /** Optional callback invoked before each retry. */
11
- onRetry?: (attempt: number, error: JooneError, delayMs: number) => void;
12
- }
13
-
14
- /**
15
- * Executes an async function with exponential backoff retry logic.
16
- *
17
- * - Retries only if the caught error is a `JooneError` with `retryable === true`.
18
- * - Non-retryable errors are re-thrown immediately.
19
- * - Raw (non-JooneError) errors are re-thrown immediately.
20
- * - Delays double with each attempt: 1s → 2s → 4s (+ random jitter).
21
- *
22
- * @param fn - The async function to execute.
23
- * @param opts - Retry configuration.
24
- * @returns The result of `fn` on success.
25
- * @throws The last error encountered after all retries are exhausted, or any non-retryable error.
26
- */
27
- export async function retryWithBackoff<T>(
28
- fn: () => Promise<T>,
29
- opts: RetryOptions = {}
30
- ): Promise<T> {
31
- const {
32
- maxRetries = 3,
33
- initialDelayMs = 1000,
34
- maxJitterMs = 500,
35
- onRetry,
36
- } = opts;
37
-
38
- let lastError: JooneError | undefined;
39
-
40
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
41
- try {
42
- return await fn();
43
- } catch (error: unknown) {
44
- // Only retry JooneErrors that are explicitly marked retryable
45
- if (error instanceof JooneError && error.retryable) {
46
- lastError = error;
47
-
48
- if (attempt < maxRetries) {
49
- const baseDelay = initialDelayMs * Math.pow(2, attempt);
50
- const jitter = Math.floor(Math.random() * maxJitterMs * 2) - maxJitterMs;
51
- const delay = Math.max(0, baseDelay + jitter);
52
-
53
- if (onRetry) {
54
- onRetry(attempt + 1, error, delay);
55
- }
56
-
57
- await sleep(delay);
58
- continue;
59
- }
60
- }
61
-
62
- // Non-retryable or non-JooneError: propagate immediately
63
- if (!(error instanceof JooneError) || !error.retryable) {
64
- throw error;
65
- }
66
- }
67
- }
68
-
69
- // All retries exhausted
70
- throw lastError!;
71
- }
72
-
73
- function sleep(ms: number): Promise<void> {
74
- return new Promise((resolve) => setTimeout(resolve, ms));
75
- }
@@ -1,90 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { ContextState } from "./promptBuilder.js";
4
- import { SessionStatePayload } from "./sessionStore.js";
5
- import { AIMessage, HumanMessage, SystemMessage, ToolMessage } from "@langchain/core/messages";
6
-
7
- export class SessionResumer {
8
- private workspaceDir: string;
9
-
10
- constructor(workspaceDir: string) {
11
- this.workspaceDir = workspaceDir;
12
- }
13
-
14
- /**
15
- * Prepares a loaded session state for execution by detecting external drift
16
- * and injecting the Wakeup prompt so the LLM knows it is inside a fresh sandbox.
17
- */
18
- public prepareForResume(payload: SessionStatePayload): ContextState {
19
- const state = { ...payload.state };
20
-
21
- // 1. Detect File Drift
22
- const driftedFiles = this.detectFileDrift(state, payload.header.lastSavedAt);
23
-
24
- // 2. Formulate the Sandbox Amnesia & Drift Wakeup prompt
25
- let wakeupPrompt = `[SYSTEM NOTIFICATION: SESSION RESUMED]\n`;
26
- wakeupPrompt += `You were paused and have just been loaded into a **NEW** execution session. \n`;
27
- wakeupPrompt += `IMPORTANT CONTEXT:\n`;
28
- wakeupPrompt += `- The execution Sandbox is completely fresh. Any background processes, dev servers, or in-memory databases you were running previously are GONE. You must restart them if needed.\n`;
29
-
30
- if (driftedFiles.length > 0) {
31
- wakeupPrompt += `- The following files were mutated externally on the host machine while you were offline:\n`;
32
- for (const file of driftedFiles) {
33
- wakeupPrompt += ` - \`${file}\`\n`;
34
- }
35
- wakeupPrompt += `Before modifying these files using replace_file_content, you MUST re-read them using view_code_item or view_file to understand the external changes.\n`;
36
- } else {
37
- wakeupPrompt += `- No files in your active context appear to have been edited on the host while you were paused.\n`;
38
- }
39
-
40
- // Inject as a Human Message at the very end of the history
41
- // so it acts as an immediate reminder before the next LLM generation.
42
- state.conversationHistory.push(new HumanMessage(`<system-wakeup>\n${wakeupPrompt}\n</system-wakeup>`));
43
-
44
- return state;
45
- }
46
-
47
- /**
48
- * Analyzes the conversation history for any files the agent has interacted with.
49
- * Checks their `mtime` against the session `lastSavedAt`.
50
- * If the file on disk is newer, it has drifted.
51
- */
52
- public detectFileDrift(state: ContextState, lastSavedAt: number): string[] {
53
- const interactedFiles = new Set<string>();
54
-
55
- // We deduce file interaction by looking at what tools were called
56
- for (const msg of state.conversationHistory) {
57
- if (msg instanceof AIMessage && msg.tool_calls) {
58
- for (const call of msg.tool_calls) {
59
- if (call.name === "read_file" || call.name === "write_file" || call.name === "replace_file_content" || call.name === "multi_replace_file_content" || call.name === "view_file") {
60
- let targetPath = "";
61
- if (call.args.path) targetPath = call.args.path;
62
- if (call.args.AbsolutePath) targetPath = call.args.AbsolutePath;
63
- if (call.args.TargetFile) targetPath = call.args.TargetFile;
64
-
65
- if (targetPath) {
66
- interactedFiles.add(targetPath);
67
- }
68
- }
69
- }
70
- }
71
- }
72
-
73
- const drifted: string[] = [];
74
-
75
- for (const file of interactedFiles) {
76
- // Because paths might be absolute or relative, attempt resolution
77
- const absolutePath = path.isAbsolute(file) ? file : path.resolve(this.workspaceDir, file);
78
-
79
- if (fs.existsSync(absolutePath)) {
80
- const stats = fs.statSync(absolutePath);
81
- // If the file's modification time is STRICTLY greater than the save time, it drifted
82
- if (stats.mtimeMs > lastSavedAt) {
83
- drifted.push(file);
84
- }
85
- }
86
- }
87
-
88
- return drifted;
89
- }
90
- }
@@ -1,216 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import * as os from "node:os";
4
- import * as readline from "node:readline";
5
- import {
6
- BaseMessage,
7
- HumanMessage,
8
- AIMessage,
9
- SystemMessage,
10
- ToolMessage
11
- } from "@langchain/core/messages";
12
- import { ContextState } from "./promptBuilder.js";
13
-
14
- // Ensure the sessions directory exists
15
- const SESSIONS_DIR = path.join(os.homedir(), ".joone", "sessions");
16
-
17
- export interface SessionHeader {
18
- sessionId: string;
19
- startedAt: number;
20
- lastSavedAt: number;
21
- provider: string;
22
- model: string;
23
- description: string;
24
- }
25
-
26
- export interface SessionStatePayload {
27
- header: SessionHeader;
28
- state: ContextState;
29
- }
30
-
31
- /**
32
- * Serializes and deserializes agent conversation history to streaming JSONL files.
33
- */
34
- export class SessionStore {
35
- constructor() {
36
- if (!fs.existsSync(SESSIONS_DIR)) {
37
- fs.mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o700 });
38
- }
39
- }
40
-
41
- /**
42
- * Serializes a LangChain BaseMessage instance into a raw JSON object.
43
- */
44
- private serializeMessage(msg: BaseMessage): any {
45
- const base = {
46
- type: msg._getType(),
47
- content: msg.content,
48
- };
49
-
50
- if (msg instanceof AIMessage && msg.tool_calls && msg.tool_calls.length > 0) {
51
- (base as any).tool_calls = msg.tool_calls;
52
- }
53
-
54
- if (msg instanceof ToolMessage) {
55
- (base as any).tool_call_id = msg.tool_call_id;
56
- }
57
-
58
- return base;
59
- }
60
-
61
- /**
62
- * Rehydrates a raw JSON object back into a LangChain BaseMessage class instance.
63
- */
64
- private deserializeMessage(raw: any): BaseMessage {
65
- switch (raw.type) {
66
- case "human":
67
- return new HumanMessage(raw.content);
68
- case "ai":
69
- return new AIMessage({
70
- content: raw.content,
71
- tool_calls: raw.tool_calls || undefined,
72
- });
73
- case "system":
74
- // Remap old saved SystemMessages to HumanMessages to prevent provider index errors
75
- return new HumanMessage(`<system-reminder>\n${raw.content}\n</system-reminder>`);
76
- case "tool":
77
- return new ToolMessage({
78
- content: raw.content,
79
- tool_call_id: raw.tool_call_id,
80
- });
81
- default:
82
- throw new Error(`Unknown message type in session history: ${raw.type}`);
83
- }
84
- }
85
-
86
- /**
87
- * Saves the entire session state cleanly to a .jsonl file, overwriting the previous save.
88
- * We don't append to avoid partial mid-turn corruption. Since we compact the context,
89
- * the file size remains extremely small.
90
- */
91
- public async saveSession(
92
- sessionId: string,
93
- state: ContextState,
94
- provider: string,
95
- model: string
96
- ): Promise<void> {
97
- const filePath = path.join(SESSIONS_DIR, `${sessionId}.jsonl`);
98
-
99
- let description = "Empty session";
100
- if (state.conversationHistory.length > 0) {
101
- const firstMsg = state.conversationHistory.find(m => m instanceof HumanMessage);
102
- if (firstMsg && typeof firstMsg.content === "string") {
103
- description = firstMsg.content.substring(0, 100).replace(/\n/g, " ") + "...";
104
- }
105
- }
106
-
107
- const header: SessionHeader = {
108
- sessionId,
109
- startedAt: fs.existsSync(filePath) ? (await this.loadHeader(sessionId))?.startedAt || Date.now() : Date.now(),
110
- lastSavedAt: Date.now(),
111
- provider,
112
- model,
113
- description,
114
- };
115
-
116
- const writeStream = fs.createWriteStream(filePath, { encoding: "utf8" });
117
-
118
- // Line 1: Header + System State
119
- const payload = {
120
- header,
121
- globalSystemInstructions: state.globalSystemInstructions,
122
- projectMemory: state.projectMemory,
123
- sessionContext: state.sessionContext,
124
- };
125
- writeStream.write(JSON.stringify({ __type: "header", ...payload }) + "\n");
126
-
127
- // Line 2+: conversationHistory messages
128
- for (const msg of state.conversationHistory) {
129
- writeStream.write(JSON.stringify(this.serializeMessage(msg)) + "\n");
130
- }
131
-
132
- await new Promise((resolve) => writeStream.end(resolve));
133
- }
134
-
135
- /**
136
- * Loads the session state from disk.
137
- */
138
- public async loadSession(sessionId: string): Promise<SessionStatePayload> {
139
- const filePath = path.join(SESSIONS_DIR, `${sessionId}.jsonl`);
140
- if (!fs.existsSync(filePath)) {
141
- throw new Error(`Session not found: ${sessionId}`);
142
- }
143
-
144
- const readStream = fs.createReadStream(filePath, { encoding: "utf8" });
145
- const rl = readline.createInterface({ input: readStream, crlfDelay: Infinity });
146
-
147
- let headerData: any = null;
148
- const conversationHistory: BaseMessage[] = [];
149
-
150
- for await (const line of rl) {
151
- if (!line.trim()) continue;
152
- const parsed = JSON.parse(line);
153
-
154
- if (parsed.__type === "header") {
155
- headerData = parsed;
156
- } else {
157
- conversationHistory.push(this.deserializeMessage(parsed));
158
- }
159
- }
160
-
161
- if (!headerData) {
162
- throw new Error(`Malformed session file: Missing header block in ${sessionId}.jsonl`);
163
- }
164
-
165
- return {
166
- header: headerData.header,
167
- state: {
168
- globalSystemInstructions: headerData.globalSystemInstructions,
169
- projectMemory: headerData.projectMemory,
170
- sessionContext: headerData.sessionContext,
171
- conversationHistory,
172
- }
173
- };
174
- }
175
-
176
- /**
177
- * Only reads the first line of the given session file to quickly extract the header.
178
- */
179
- private async loadHeader(sessionId: string): Promise<SessionHeader | null> {
180
- const filePath = path.join(SESSIONS_DIR, `${sessionId}.jsonl`);
181
- if (!fs.existsSync(filePath)) return null;
182
-
183
- const readStream = fs.createReadStream(filePath, { encoding: "utf8" });
184
- const rl = readline.createInterface({ input: readStream, crlfDelay: Infinity });
185
-
186
- for await (const line of rl) {
187
- if (!line.trim()) continue;
188
- const parsed = JSON.parse(line);
189
- if (parsed.__type === "header") {
190
- readStream.close();
191
- return parsed.header as SessionHeader;
192
- }
193
- }
194
- return null;
195
- }
196
-
197
- /**
198
- * Lists all saved sessions, sorted by most recently saved.
199
- */
200
- public async listSessions(): Promise<SessionHeader[]> {
201
- if (!fs.existsSync(SESSIONS_DIR)) return [];
202
-
203
- const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.endsWith(".jsonl"));
204
- const sessions: SessionHeader[] = [];
205
-
206
- for (const file of files) {
207
- const sessionId = file.replace(".jsonl", "");
208
- const header = await this.loadHeader(sessionId);
209
- if (header) {
210
- sessions.push(header);
211
- }
212
- }
213
-
214
- return sessions.sort((a, b) => b.lastSavedAt - a.lastSavedAt);
215
- }
216
- }