joonecli 0.1.1 → 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.
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/builtinCommands.js +6 -6
- package/dist/commands/builtinCommands.js.map +1 -1
- package/dist/commands/commandRegistry.d.ts +3 -1
- package/dist/commands/commandRegistry.js.map +1 -1
- package/dist/core/agentLoop.d.ts +3 -1
- package/dist/core/agentLoop.js +17 -7
- package/dist/core/agentLoop.js.map +1 -1
- package/dist/core/compactor.js +2 -2
- package/dist/core/compactor.js.map +1 -1
- package/dist/core/contextGuard.d.ts +5 -0
- package/dist/core/contextGuard.js +30 -3
- package/dist/core/contextGuard.js.map +1 -1
- package/dist/core/events.d.ts +45 -0
- package/dist/core/events.js +8 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/sessionStore.js +3 -2
- package/dist/core/sessionStore.js.map +1 -1
- package/dist/core/subAgent.js +2 -2
- package/dist/core/subAgent.js.map +1 -1
- package/dist/core/tokenCounter.d.ts +8 -1
- package/dist/core/tokenCounter.js +28 -0
- package/dist/core/tokenCounter.js.map +1 -1
- package/dist/middleware/permission.js +1 -0
- package/dist/middleware/permission.js.map +1 -1
- package/dist/tools/browser.js +4 -1
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +11 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/installHostDeps.d.ts +2 -0
- package/dist/tools/installHostDeps.js +37 -0
- package/dist/tools/installHostDeps.js.map +1 -0
- package/dist/tools/router.js +1 -0
- package/dist/tools/router.js.map +1 -1
- package/dist/tools/spawnAgent.js +3 -1
- package/dist/tools/spawnAgent.js.map +1 -1
- package/dist/tracing/sessionTracer.d.ts +1 -0
- package/dist/tracing/sessionTracer.js +4 -1
- package/dist/tracing/sessionTracer.js.map +1 -1
- package/dist/ui/App.js +6 -1
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ActionLog.d.ts +7 -0
- package/dist/ui/components/ActionLog.js +63 -0
- package/dist/ui/components/ActionLog.js.map +1 -0
- package/dist/ui/components/FileBrowser.d.ts +2 -0
- package/dist/ui/components/FileBrowser.js +41 -0
- package/dist/ui/components/FileBrowser.js.map +1 -0
- package/package.json +3 -5
- package/AGENTS.md +0 -56
- package/Handover.md +0 -115
- package/PROGRESS.md +0 -160
- package/docs/01_insights_and_patterns.md +0 -27
- package/docs/02_edge_cases_and_mitigations.md +0 -143
- package/docs/03_initial_implementation_plan.md +0 -66
- package/docs/04_tech_stack_proposal.md +0 -20
- package/docs/05_prd.md +0 -87
- package/docs/06_user_stories.md +0 -72
- package/docs/07_system_architecture.md +0 -138
- package/docs/08_roadmap.md +0 -200
- package/e2b/Dockerfile +0 -26
- package/src/__tests__/bootstrap.test.ts +0 -111
- package/src/__tests__/config.test.ts +0 -97
- package/src/__tests__/m55.test.ts +0 -238
- package/src/__tests__/middleware.test.ts +0 -219
- package/src/__tests__/modelFactory.test.ts +0 -63
- package/src/__tests__/optimizations.test.ts +0 -201
- package/src/__tests__/promptBuilder.test.ts +0 -141
- package/src/__tests__/sandbox.test.ts +0 -102
- package/src/__tests__/security.test.ts +0 -122
- package/src/__tests__/streaming.test.ts +0 -82
- package/src/__tests__/toolRouter.test.ts +0 -52
- package/src/__tests__/tools.test.ts +0 -146
- package/src/__tests__/tracing.test.ts +0 -196
- package/src/agents/agentRegistry.ts +0 -69
- package/src/agents/agentSpec.ts +0 -67
- package/src/agents/builtinAgents.ts +0 -142
- package/src/cli/config.ts +0 -124
- package/src/cli/index.ts +0 -742
- package/src/cli/modelFactory.ts +0 -174
- package/src/cli/postinstall.ts +0 -28
- package/src/cli/providers.ts +0 -107
- package/src/commands/builtinCommands.ts +0 -293
- package/src/commands/commandRegistry.ts +0 -194
- package/src/core/agentLoop.d.ts.map +0 -1
- package/src/core/agentLoop.ts +0 -312
- package/src/core/autoSave.ts +0 -95
- package/src/core/compactor.ts +0 -252
- package/src/core/contextGuard.ts +0 -129
- package/src/core/errors.ts +0 -202
- package/src/core/promptBuilder.d.ts.map +0 -1
- package/src/core/promptBuilder.ts +0 -139
- package/src/core/reasoningRouter.ts +0 -121
- package/src/core/retry.ts +0 -75
- package/src/core/sessionResumer.ts +0 -90
- package/src/core/sessionStore.ts +0 -216
- package/src/core/subAgent.ts +0 -339
- package/src/core/tokenCounter.ts +0 -64
- package/src/evals/dataset.ts +0 -67
- package/src/evals/evaluator.ts +0 -81
- package/src/hitl/bridge.ts +0 -160
- package/src/middleware/commandSanitizer.ts +0 -60
- package/src/middleware/loopDetection.ts +0 -63
- package/src/middleware/permission.ts +0 -72
- package/src/middleware/pipeline.ts +0 -75
- package/src/middleware/preCompletion.ts +0 -94
- package/src/middleware/types.ts +0 -45
- package/src/sandbox/bootstrap.ts +0 -121
- package/src/sandbox/manager.ts +0 -239
- package/src/sandbox/sync.ts +0 -157
- package/src/skills/loader.ts +0 -143
- package/src/skills/tools.ts +0 -99
- package/src/skills/types.ts +0 -13
- package/src/test_cache.ts +0 -72
- package/src/tools/askUser.ts +0 -47
- package/src/tools/browser.ts +0 -137
- package/src/tools/index.d.ts.map +0 -1
- package/src/tools/index.ts +0 -237
- package/src/tools/registry.ts +0 -198
- package/src/tools/router.ts +0 -78
- package/src/tools/security.ts +0 -220
- package/src/tools/spawnAgent.ts +0 -158
- package/src/tools/webSearch.ts +0 -142
- package/src/tracing/analyzer.ts +0 -265
- package/src/tracing/langsmith.ts +0 -63
- package/src/tracing/sessionTracer.ts +0 -202
- package/src/tracing/types.ts +0 -49
- package/src/types/valyu.d.ts +0 -37
- package/src/ui/App.tsx +0 -404
- package/src/ui/components/HITLPrompt.tsx +0 -119
- package/src/ui/components/Header.tsx +0 -51
- package/src/ui/components/MessageBubble.tsx +0 -46
- package/src/ui/components/StatusBar.tsx +0 -138
- package/src/ui/components/StreamingText.tsx +0 -48
- package/src/ui/components/ToolCallPanel.tsx +0 -80
- package/tests/commands/commands.test.ts +0 -356
- package/tests/core/compactor.test.ts +0 -217
- package/tests/core/retryAndErrors.test.ts +0 -164
- package/tests/core/sessionResumer.test.ts +0 -95
- package/tests/core/sessionStore.test.ts +0 -84
- package/tests/core/stability.test.ts +0 -165
- package/tests/core/subAgent.test.ts +0 -238
- package/tests/hitl/hitlBridge.test.ts +0 -115
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -10
- package/vitest.out +0 -48
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
ConversationCompactor,
|
|
4
|
-
COMPACT_SYSTEM_PROMPT,
|
|
5
|
-
createHandoffPrompt,
|
|
6
|
-
FAST_MODEL_DEFAULTS,
|
|
7
|
-
resolveFastModel,
|
|
8
|
-
} from "../../src/core/compactor.js";
|
|
9
|
-
import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from "@langchain/core/messages";
|
|
10
|
-
import { CacheOptimizedPromptBuilder } from "../../src/core/promptBuilder.js";
|
|
11
|
-
|
|
12
|
-
// ─── Mock LLM ───────────────────────────────────────────────────────────────────
|
|
13
|
-
|
|
14
|
-
function createMockLLM(response: string = "## Summary\nMocked summary.") {
|
|
15
|
-
return {
|
|
16
|
-
invoke: vi.fn().mockResolvedValue(new AIMessage(response)),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function createFailingLLM() {
|
|
21
|
-
return {
|
|
22
|
-
invoke: vi.fn().mockRejectedValue(new Error("LLM call failed")),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ─── Test History ───────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
function makeHistory(count: number) {
|
|
29
|
-
const msgs = [];
|
|
30
|
-
for (let i = 0; i < count; i++) {
|
|
31
|
-
if (i % 2 === 0) {
|
|
32
|
-
msgs.push(new HumanMessage(`User message ${i}`));
|
|
33
|
-
} else {
|
|
34
|
-
msgs.push(new AIMessage(`Agent response ${i}`));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return msgs;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ─── FAST_MODEL_DEFAULTS ────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
describe("FAST_MODEL_DEFAULTS", () => {
|
|
43
|
-
it("maps all major providers to fast models", () => {
|
|
44
|
-
expect(FAST_MODEL_DEFAULTS["anthropic"]).toBe("claude-3-haiku-20240307");
|
|
45
|
-
expect(FAST_MODEL_DEFAULTS["openai"]).toBe("gpt-4o-mini");
|
|
46
|
-
expect(FAST_MODEL_DEFAULTS["google"]).toBe("gemini-2.5-flash");
|
|
47
|
-
expect(FAST_MODEL_DEFAULTS["mistral"]).toBe("mistral-small-latest");
|
|
48
|
-
expect(FAST_MODEL_DEFAULTS["groq"]).toBe("mixtral-8x7b-32768");
|
|
49
|
-
expect(FAST_MODEL_DEFAULTS["deepseek"]).toBe("deepseek-chat");
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("resolveFastModel", () => {
|
|
54
|
-
it("returns override when provided", () => {
|
|
55
|
-
expect(resolveFastModel("anthropic", "claude-sonnet-4", "my-custom-model")).toBe("my-custom-model");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("returns provider default when no override", () => {
|
|
59
|
-
expect(resolveFastModel("anthropic", "claude-sonnet-4")).toBe("claude-3-haiku-20240307");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("falls back to main model for unknown provider", () => {
|
|
63
|
-
expect(resolveFastModel("unknown-provider", "main-model")).toBe("main-model");
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// ─── Compact Prompt ─────────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
describe("COMPACT_SYSTEM_PROMPT", () => {
|
|
70
|
-
it("instructs preservation of file paths", () => {
|
|
71
|
-
expect(COMPACT_SYSTEM_PROMPT).toContain("File paths");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("instructs preservation of tool calls", () => {
|
|
75
|
-
expect(COMPACT_SYSTEM_PROMPT).toContain("Tool calls");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("instructs structured markdown format", () => {
|
|
79
|
-
expect(COMPACT_SYSTEM_PROMPT).toContain("structured markdown");
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// ─── Handoff Prompt ─────────────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
describe("createHandoffPrompt", () => {
|
|
86
|
-
it("includes the timestamp", () => {
|
|
87
|
-
const prompt = createHandoffPrompt("2026-03-06T15:00:00Z");
|
|
88
|
-
expect(prompt).toContain("2026-03-06T15:00:00Z");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("includes CONTEXT HANDOFF marker", () => {
|
|
92
|
-
const prompt = createHandoffPrompt("now");
|
|
93
|
-
expect(prompt).toContain("[CONTEXT HANDOFF]");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("instructs not to redo work", () => {
|
|
97
|
-
const prompt = createHandoffPrompt("now");
|
|
98
|
-
expect(prompt).toContain("do NOT redo work");
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// ─── ConversationCompactor ──────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
describe("ConversationCompactor", () => {
|
|
105
|
-
let compactor: ConversationCompactor;
|
|
106
|
-
|
|
107
|
-
beforeEach(() => {
|
|
108
|
-
compactor = new ConversationCompactor();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("skips compaction when history is shorter than keepLastN", async () => {
|
|
112
|
-
const history = makeHistory(4);
|
|
113
|
-
const llm = createMockLLM();
|
|
114
|
-
|
|
115
|
-
const result = await compactor.compact(history, llm as any, { keepLastN: 8 });
|
|
116
|
-
|
|
117
|
-
expect(result.evictedCount).toBe(0);
|
|
118
|
-
expect(result.compactedHistory).toEqual(history);
|
|
119
|
-
expect(result.llmUsed).toBe(false);
|
|
120
|
-
expect(llm.invoke).not.toHaveBeenCalled();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("compacts with LLM and preserves recent messages", async () => {
|
|
124
|
-
const history = makeHistory(20);
|
|
125
|
-
const llm = createMockLLM("## Files Modified\n- test.ts\n## Current State\nIn progress.");
|
|
126
|
-
|
|
127
|
-
const result = await compactor.compact(history, llm as any, { keepLastN: 8 });
|
|
128
|
-
|
|
129
|
-
expect(result.evictedCount).toBe(12);
|
|
130
|
-
expect(result.llmUsed).toBe(true);
|
|
131
|
-
expect(llm.invoke).toHaveBeenCalledTimes(1);
|
|
132
|
-
|
|
133
|
-
// Should have: summary + handoff + 8 recent messages = 10 total
|
|
134
|
-
expect(result.compactedHistory.length).toBe(10);
|
|
135
|
-
|
|
136
|
-
// First message should be the compacted summary
|
|
137
|
-
const summaryMsg = result.compactedHistory[0];
|
|
138
|
-
expect(summaryMsg._getType()).toBe("human");
|
|
139
|
-
expect(typeof summaryMsg.content === "string" && summaryMsg.content).toContain("COMPACTED CONVERSATION SUMMARY");
|
|
140
|
-
|
|
141
|
-
// Second message should be the handoff
|
|
142
|
-
const handoffMsg = result.compactedHistory[1];
|
|
143
|
-
expect(handoffMsg._getType()).toBe("human");
|
|
144
|
-
expect(typeof handoffMsg.content === "string" && handoffMsg.content).toContain("CONTEXT HANDOFF");
|
|
145
|
-
|
|
146
|
-
// Remaining should be the last 8 messages from original history
|
|
147
|
-
const recent = result.compactedHistory.slice(2);
|
|
148
|
-
expect(recent).toEqual(history.slice(-8));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("invokes LLM with compact prompt and evicted messages", async () => {
|
|
152
|
-
const history = makeHistory(12);
|
|
153
|
-
const llm = createMockLLM();
|
|
154
|
-
|
|
155
|
-
await compactor.compact(history, llm as any, { keepLastN: 4 });
|
|
156
|
-
|
|
157
|
-
const invokeCall = llm.invoke.mock.calls[0][0];
|
|
158
|
-
expect(invokeCall.length).toBe(2); // System + Human
|
|
159
|
-
|
|
160
|
-
const systemMsg = invokeCall[0];
|
|
161
|
-
const humanMsg = invokeCall[1];
|
|
162
|
-
|
|
163
|
-
// System msg should contain compact prompt
|
|
164
|
-
expect(typeof systemMsg.content === "string" && systemMsg.content).toContain("conversation summarizer");
|
|
165
|
-
|
|
166
|
-
// Human msg should contain evicted messages in readable format
|
|
167
|
-
expect(typeof humanMsg.content === "string" && humanMsg.content).toContain("User message 0");
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("falls back to string-based compaction when LLM fails", async () => {
|
|
171
|
-
const history = makeHistory(20);
|
|
172
|
-
const llm = createFailingLLM();
|
|
173
|
-
|
|
174
|
-
const result = await compactor.compact(history, llm as any, { keepLastN: 8 });
|
|
175
|
-
|
|
176
|
-
expect(result.evictedCount).toBe(12);
|
|
177
|
-
expect(result.llmUsed).toBe(false);
|
|
178
|
-
|
|
179
|
-
// Should still have summary + handoff + 8 recent = 10 messages
|
|
180
|
-
expect(result.compactedHistory.length).toBe(10);
|
|
181
|
-
|
|
182
|
-
// Summary should mention fallback
|
|
183
|
-
const summaryMsg = result.compactedHistory[0];
|
|
184
|
-
// We didn't change what the text content says, just that it's a HumanMessage now
|
|
185
|
-
expect(summaryMsg._getType()).toBe("human");
|
|
186
|
-
expect(typeof summaryMsg.content === "string" && summaryMsg.content).toContain("Fallback Compaction");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("handles token reduction properly", async () => {
|
|
190
|
-
// Use a large history to ensure meaningful token reduction
|
|
191
|
-
const history = makeHistory(40);
|
|
192
|
-
const llm = createMockLLM("Short summary.");
|
|
193
|
-
|
|
194
|
-
const result = await compactor.compact(history, llm as any, { keepLastN: 8 });
|
|
195
|
-
|
|
196
|
-
// The compacted history (summary + handoff + 8 recent) should use fewer tokens
|
|
197
|
-
// than the original 40 messages
|
|
198
|
-
expect(result.tokensAfter).toBeLessThan(result.tokensBefore);
|
|
199
|
-
expect(result.evictedCount).toBe(32);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// ─── Integration with PromptBuilder ─────────────────────────────────────────────
|
|
204
|
-
|
|
205
|
-
describe("CacheOptimizedPromptBuilder.compactHistoryWithLLM", () => {
|
|
206
|
-
it("delegates to ConversationCompactor", async () => {
|
|
207
|
-
const builder = new CacheOptimizedPromptBuilder();
|
|
208
|
-
const history = makeHistory(20);
|
|
209
|
-
const llm = createMockLLM();
|
|
210
|
-
|
|
211
|
-
const result = await builder.compactHistoryWithLLM(history, llm as any, 8);
|
|
212
|
-
|
|
213
|
-
expect(result.evictedCount).toBe(12);
|
|
214
|
-
expect(result.llmUsed).toBe(true);
|
|
215
|
-
expect(result.compactedHistory.length).toBe(10);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { retryWithBackoff } from "../../src/core/retry.js";
|
|
3
|
-
import { JooneError, LLMApiError, ToolExecutionError, SandboxError, wrapLLMError } from "../../src/core/errors.js";
|
|
4
|
-
|
|
5
|
-
// ─── retryWithBackoff ─────────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
describe("retryWithBackoff", () => {
|
|
8
|
-
it("should return immediately on success", async () => {
|
|
9
|
-
const result = await retryWithBackoff(async () => "ok");
|
|
10
|
-
expect(result).toBe("ok");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("should retry retryable JooneErrors up to maxRetries", async () => {
|
|
14
|
-
let attempts = 0;
|
|
15
|
-
const fn = async () => {
|
|
16
|
-
attempts++;
|
|
17
|
-
if (attempts < 3) {
|
|
18
|
-
throw new JooneError("transient", { category: "network", retryable: true });
|
|
19
|
-
}
|
|
20
|
-
return "recovered";
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const result = await retryWithBackoff(fn, { maxRetries: 3, initialDelayMs: 10, maxJitterMs: 5 });
|
|
24
|
-
expect(result).toBe("recovered");
|
|
25
|
-
expect(attempts).toBe(3);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("should throw immediately on non-retryable JooneError", async () => {
|
|
29
|
-
const fn = async () => {
|
|
30
|
-
throw new JooneError("auth failure", { category: "config", retryable: false });
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
await expect(retryWithBackoff(fn, { maxRetries: 3, initialDelayMs: 10 }))
|
|
34
|
-
.rejects.toThrow("auth failure");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("should throw immediately on raw non-JooneError", async () => {
|
|
38
|
-
const fn = async () => {
|
|
39
|
-
throw new Error("random crash");
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
await expect(retryWithBackoff(fn, { maxRetries: 3, initialDelayMs: 10 }))
|
|
43
|
-
.rejects.toThrow("random crash");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("should call onRetry callback before each retry", async () => {
|
|
47
|
-
const retries: number[] = [];
|
|
48
|
-
let attempts = 0;
|
|
49
|
-
|
|
50
|
-
const fn = async () => {
|
|
51
|
-
attempts++;
|
|
52
|
-
if (attempts <= 2) {
|
|
53
|
-
throw new JooneError("transient", { category: "network", retryable: true });
|
|
54
|
-
}
|
|
55
|
-
return "ok";
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
await retryWithBackoff(fn, {
|
|
59
|
-
maxRetries: 3,
|
|
60
|
-
initialDelayMs: 10,
|
|
61
|
-
maxJitterMs: 0,
|
|
62
|
-
onRetry: (attempt) => retries.push(attempt),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
expect(retries).toEqual([1, 2]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("should throw the last error after all retries exhausted", async () => {
|
|
69
|
-
const fn = async () => {
|
|
70
|
-
throw new JooneError("always fails", { category: "llm_api", retryable: true });
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
await expect(retryWithBackoff(fn, { maxRetries: 2, initialDelayMs: 10, maxJitterMs: 0 }))
|
|
74
|
-
.rejects.toThrow("always fails");
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// ─── JooneError hierarchy ────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
describe("JooneError hierarchy", () => {
|
|
81
|
-
it("LLMApiError produces a rate-limit recovery hint for 429", () => {
|
|
82
|
-
const err = new LLMApiError("Too Many Requests", {
|
|
83
|
-
statusCode: 429,
|
|
84
|
-
provider: "anthropic",
|
|
85
|
-
retryable: true,
|
|
86
|
-
headers: { "retry-after": "30" },
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(err.retryable).toBe(true);
|
|
90
|
-
expect(err.category).toBe("llm_api");
|
|
91
|
-
expect(err.toRecoveryHint()).toContain("RATE LIMITED");
|
|
92
|
-
expect(err.toRecoveryHint()).toContain("30 seconds");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("LLMApiError produces a fatal hint for 401", () => {
|
|
96
|
-
const err = new LLMApiError("Unauthorized", {
|
|
97
|
-
statusCode: 401,
|
|
98
|
-
provider: "openai",
|
|
99
|
-
retryable: false,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(err.retryable).toBe(false);
|
|
103
|
-
expect(err.toRecoveryHint()).toContain("AUTH FAILURE");
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("ToolExecutionError wraps tool metadata", () => {
|
|
107
|
-
const err = new ToolExecutionError("file not found", {
|
|
108
|
-
toolName: "read_file",
|
|
109
|
-
args: { path: "/foo.txt" },
|
|
110
|
-
retryable: false,
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
expect(err.toolName).toBe("read_file");
|
|
114
|
-
expect(err.toRecoveryHint()).toContain('read_file');
|
|
115
|
-
expect(err.toRecoveryHint()).toContain("file not found");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("SandboxError produces a sandbox-specific hint", () => {
|
|
119
|
-
const err = new SandboxError("container died", {
|
|
120
|
-
sandboxProvider: "e2b",
|
|
121
|
-
retryable: true,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
expect(err.retryable).toBe(true);
|
|
125
|
-
expect(err.toRecoveryHint()).toContain("e2b sandbox");
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// ─── wrapLLMError ─────────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
describe("wrapLLMError", () => {
|
|
132
|
-
it("marks 429 as retryable", () => {
|
|
133
|
-
const raw = Object.assign(new Error("rate limited"), { status: 429 });
|
|
134
|
-
const wrapped = wrapLLMError(raw, "anthropic");
|
|
135
|
-
|
|
136
|
-
expect(wrapped).toBeInstanceOf(LLMApiError);
|
|
137
|
-
expect(wrapped.retryable).toBe(true);
|
|
138
|
-
expect(wrapped.statusCode).toBe(429);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("marks 401 as non-retryable", () => {
|
|
142
|
-
const raw = Object.assign(new Error("bad key"), { status: 401 });
|
|
143
|
-
const wrapped = wrapLLMError(raw, "openai");
|
|
144
|
-
|
|
145
|
-
expect(wrapped.retryable).toBe(false);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("marks ECONNRESET as retryable", () => {
|
|
149
|
-
const raw = Object.assign(new Error("connection reset"), { code: "ECONNRESET" });
|
|
150
|
-
const wrapped = wrapLLMError(raw, "google");
|
|
151
|
-
|
|
152
|
-
expect(wrapped.retryable).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("passes through an existing LLMApiError unchanged", () => {
|
|
156
|
-
const original = new LLMApiError("already wrapped", {
|
|
157
|
-
statusCode: 500,
|
|
158
|
-
provider: "test",
|
|
159
|
-
retryable: true,
|
|
160
|
-
});
|
|
161
|
-
const result = wrapLLMError(original, "ignored");
|
|
162
|
-
expect(result).toBe(original);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { SessionResumer } from "../../src/core/sessionResumer.js";
|
|
3
|
-
import { AIMessage, HumanMessage, ToolMessage, SystemMessage } from "@langchain/core/messages";
|
|
4
|
-
import { ContextState } from "../../src/core/promptBuilder.js";
|
|
5
|
-
import { SessionStatePayload } from "../../src/core/sessionStore.js";
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
|
-
|
|
8
|
-
// Mock the Node FS module so we can simulate mtime drifts
|
|
9
|
-
vi.mock("node:fs", async (importOriginal) => {
|
|
10
|
-
const actual = await importOriginal<typeof import("node:fs")>();
|
|
11
|
-
return {
|
|
12
|
-
...actual,
|
|
13
|
-
existsSync: vi.fn(),
|
|
14
|
-
statSync: vi.fn(),
|
|
15
|
-
};
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe("SessionResumer", () => {
|
|
19
|
-
const resumer = new SessionResumer("/mock/workspace");
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("should detect drift on files where mtime > lastSavedAt", () => {
|
|
26
|
-
const state: ContextState = {
|
|
27
|
-
globalSystemInstructions: "",
|
|
28
|
-
projectMemory: "",
|
|
29
|
-
sessionContext: "",
|
|
30
|
-
conversationHistory: [
|
|
31
|
-
new AIMessage({
|
|
32
|
-
content: "Let me modify file_a.ts and read file_b.ts",
|
|
33
|
-
tool_calls: [
|
|
34
|
-
{ id: "1", name: "write_file", args: { path: "file_a.ts", content: "..." }, type: "tool_call" },
|
|
35
|
-
{ id: "2", name: "read_file", args: { path: "file_b.ts" }, type: "tool_call" },
|
|
36
|
-
{ id: "3", name: "bash", args: { command: "ls" }, type: "tool_call" } // Not a file target
|
|
37
|
-
]
|
|
38
|
-
})
|
|
39
|
-
]
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const lastSavedAt = 1000;
|
|
43
|
-
|
|
44
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
45
|
-
vi.mocked(fs.statSync).mockImplementation((pathStr) => {
|
|
46
|
-
if (pathStr.toString().includes("file_a.ts")) {
|
|
47
|
-
return { mtimeMs: 2000 } as fs.Stats; // Drifted!
|
|
48
|
-
}
|
|
49
|
-
if (pathStr.toString().includes("file_b.ts")) {
|
|
50
|
-
return { mtimeMs: 500 } as fs.Stats; // Safe
|
|
51
|
-
}
|
|
52
|
-
return { mtimeMs: 0 } as fs.Stats;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const driftedFiles = resumer.detectFileDrift(state, lastSavedAt);
|
|
56
|
-
expect(driftedFiles).toContain("file_a.ts");
|
|
57
|
-
expect(driftedFiles).not.toContain("file_b.ts");
|
|
58
|
-
expect(driftedFiles.length).toBe(1);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("should inject the wakeup prompt into the state", () => {
|
|
62
|
-
const payload: SessionStatePayload = {
|
|
63
|
-
header: {
|
|
64
|
-
sessionId: "123",
|
|
65
|
-
startedAt: 0,
|
|
66
|
-
lastSavedAt: 1000,
|
|
67
|
-
provider: "test",
|
|
68
|
-
model: "test",
|
|
69
|
-
description: ""
|
|
70
|
-
},
|
|
71
|
-
state: {
|
|
72
|
-
globalSystemInstructions: "sys",
|
|
73
|
-
projectMemory: "mem",
|
|
74
|
-
sessionContext: "ctx",
|
|
75
|
-
conversationHistory: [new HumanMessage("hello")]
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
vi.mocked(fs.existsSync).mockReturnValue(false); // Simulate no drifted files
|
|
80
|
-
|
|
81
|
-
const resumedState = resumer.prepareForResume(payload);
|
|
82
|
-
|
|
83
|
-
const lastMsg = resumedState.conversationHistory[resumedState.conversationHistory.length - 1];
|
|
84
|
-
expect(lastMsg).toBeInstanceOf(HumanMessage);
|
|
85
|
-
|
|
86
|
-
const content = textContent(lastMsg.content);
|
|
87
|
-
expect(content).toContain("[SYSTEM NOTIFICATION: SESSION RESUMED]");
|
|
88
|
-
expect(content).toContain("No files in your active context appear to have been edited");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
function textContent(c: string | any[]): string {
|
|
92
|
-
if (typeof c === "string") return c;
|
|
93
|
-
return c.map(part => part.text).join("");
|
|
94
|
-
}
|
|
95
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { SessionStore } from "../../src/core/sessionStore.js";
|
|
3
|
-
import { HumanMessage, AIMessage, ToolMessage, SystemMessage } from "@langchain/core/messages";
|
|
4
|
-
import * as fs from "node:fs";
|
|
5
|
-
import * as path from "node:path";
|
|
6
|
-
import * as os from "node:os";
|
|
7
|
-
|
|
8
|
-
const SESSIONS_DIR = path.join(os.homedir(), ".joone", "sessions");
|
|
9
|
-
|
|
10
|
-
describe("SessionStore", () => {
|
|
11
|
-
let store: SessionStore;
|
|
12
|
-
const testSessionId = `test-session-${Date.now()}`;
|
|
13
|
-
const testFilePath = path.join(SESSIONS_DIR, `${testSessionId}.jsonl`);
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
store = new SessionStore();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
if (fs.existsSync(testFilePath)) {
|
|
21
|
-
fs.unlinkSync(testFilePath);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("should serialize and deserialize a complex conversation history perfectly", async () => {
|
|
26
|
-
const initialState = {
|
|
27
|
-
globalSystemInstructions: "You are a test agent.",
|
|
28
|
-
projectMemory: "Project X",
|
|
29
|
-
sessionContext: "Windows 11",
|
|
30
|
-
conversationHistory: [
|
|
31
|
-
new SystemMessage("Injected dynamic system rules."),
|
|
32
|
-
new HumanMessage("Hello agent!"),
|
|
33
|
-
new AIMessage({
|
|
34
|
-
content: "Let me use a tool.",
|
|
35
|
-
tool_calls: [{ id: "call_abc123", name: "bash", args: { command: "ls" }, type: "tool_call" }]
|
|
36
|
-
}),
|
|
37
|
-
new ToolMessage({
|
|
38
|
-
tool_call_id: "call_abc123",
|
|
39
|
-
content: "file1.txt\nfile2.txt"
|
|
40
|
-
})
|
|
41
|
-
]
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// 1. Save it
|
|
45
|
-
await store.saveSession(testSessionId, initialState, "anthropic", "claude-3-opus");
|
|
46
|
-
|
|
47
|
-
// Verify file exists
|
|
48
|
-
expect(fs.existsSync(testFilePath)).toBe(true);
|
|
49
|
-
|
|
50
|
-
// 2. Load it back
|
|
51
|
-
const loaded = await store.loadSession(testSessionId);
|
|
52
|
-
|
|
53
|
-
// Verify Header & Metadata
|
|
54
|
-
expect(loaded.header.sessionId).toBe(testSessionId);
|
|
55
|
-
expect(loaded.header.provider).toBe("anthropic");
|
|
56
|
-
expect(loaded.header.model).toBe("claude-3-opus");
|
|
57
|
-
expect(loaded.header.description).toBe("Hello agent!...");
|
|
58
|
-
|
|
59
|
-
// Verify State Primitives
|
|
60
|
-
expect(loaded.state.globalSystemInstructions).toBe("You are a test agent.");
|
|
61
|
-
expect(loaded.state.projectMemory).toBe("Project X");
|
|
62
|
-
expect(loaded.state.sessionContext).toBe("Windows 11");
|
|
63
|
-
|
|
64
|
-
// Verify LangChain Objects Extracted correctly
|
|
65
|
-
const history = loaded.state.conversationHistory;
|
|
66
|
-
expect(history.length).toBe(4);
|
|
67
|
-
|
|
68
|
-
expect(history[0]).toBeInstanceOf(HumanMessage);
|
|
69
|
-
expect(history[0].content).toBe("<system-reminder>\nInjected dynamic system rules.\n</system-reminder>");
|
|
70
|
-
|
|
71
|
-
expect(history[1]).toBeInstanceOf(HumanMessage);
|
|
72
|
-
expect(history[1].content).toBe("Hello agent!");
|
|
73
|
-
|
|
74
|
-
expect(history[2]).toBeInstanceOf(AIMessage);
|
|
75
|
-
const aiMsg = history[2] as AIMessage;
|
|
76
|
-
expect(aiMsg.content).toBe("Let me use a tool.");
|
|
77
|
-
expect(aiMsg.tool_calls![0].id).toBe("call_abc123");
|
|
78
|
-
expect(aiMsg.tool_calls![0].name).toBe("bash");
|
|
79
|
-
|
|
80
|
-
expect(history[3]).toBeInstanceOf(ToolMessage);
|
|
81
|
-
expect((history[3] as ToolMessage).tool_call_id).toBe("call_abc123");
|
|
82
|
-
expect(history[3].content).toBe("file1.txt\nfile2.txt");
|
|
83
|
-
});
|
|
84
|
-
});
|