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.
- package/README.md +12 -12
- package/dist/__tests__/optimizations.test.js.map +1 -1
- package/dist/__tests__/promptBuilder.test.js +14 -20
- package/dist/__tests__/promptBuilder.test.js.map +1 -1
- package/dist/agents/agentRegistry.d.ts +37 -0
- package/dist/agents/agentRegistry.js +58 -0
- package/dist/agents/agentRegistry.js.map +1 -0
- package/dist/agents/agentSpec.d.ts +54 -0
- package/dist/agents/agentSpec.js +9 -0
- package/dist/agents/agentSpec.js.map +1 -0
- package/dist/agents/builtinAgents.d.ts +20 -0
- package/{src/agents/builtinAgents.ts → dist/agents/builtinAgents.js} +84 -101
- package/dist/agents/builtinAgents.js.map +1 -0
- package/dist/cli/config.d.ts +4 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/index.js +29 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/postinstall.d.ts +2 -0
- package/dist/cli/postinstall.js +25 -0
- package/dist/cli/postinstall.js.map +1 -0
- package/dist/commands/builtinCommands.d.ts +21 -0
- package/dist/commands/builtinCommands.js +241 -0
- package/dist/commands/builtinCommands.js.map +1 -0
- package/dist/commands/commandRegistry.d.ts +92 -0
- package/dist/commands/commandRegistry.js +128 -0
- package/dist/commands/commandRegistry.js.map +1 -0
- package/dist/core/agentLoop.d.ts +7 -2
- package/dist/core/agentLoop.js +35 -13
- package/dist/core/agentLoop.js.map +1 -1
- package/dist/core/autoSave.d.ts +41 -0
- package/dist/core/autoSave.js +69 -0
- package/dist/core/autoSave.js.map +1 -0
- package/dist/core/compactor.d.ts +66 -0
- package/dist/core/compactor.js +170 -0
- package/dist/core/compactor.js.map +1 -0
- package/dist/core/contextGuard.d.ts +38 -0
- package/dist/core/contextGuard.js +122 -0
- package/dist/core/contextGuard.js.map +1 -0
- 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/promptBuilder.d.ts +16 -1
- package/dist/core/promptBuilder.js +27 -14
- package/dist/core/promptBuilder.js.map +1 -1
- package/dist/core/sessionResumer.js +3 -3
- package/dist/core/sessionResumer.js.map +1 -1
- package/dist/core/sessionStore.js +3 -2
- package/dist/core/sessionStore.js.map +1 -1
- package/dist/core/subAgent.d.ts +56 -0
- package/dist/core/subAgent.js +240 -0
- package/dist/core/subAgent.js.map +1 -0
- 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/debug_google.d.ts +1 -0
- package/dist/debug_google.js +23 -0
- package/dist/debug_google.js.map +1 -0
- package/dist/middleware/permission.js +1 -0
- package/dist/middleware/permission.js.map +1 -1
- package/dist/test_google.d.ts +1 -0
- package/dist/test_google.js +32 -89
- package/dist/test_google.js.map +1 -0
- 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 +3 -0
- package/dist/tools/router.js.map +1 -1
- package/dist/tools/spawnAgent.d.ts +19 -0
- package/dist/tools/spawnAgent.js +132 -0
- package/dist/tools/spawnAgent.js.map +1 -0
- 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 +94 -6
- 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 +5 -6
- 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/cli/config.ts +0 -124
- package/src/cli/index.ts +0 -730
- package/src/cli/modelFactory.ts +0 -174
- 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 -215
- 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/test_google.js +0 -40
- package/src/test_google.ts +0 -40
- 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,52 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { ToolRouter, ToolTarget } from "../tools/router.js";
|
|
3
|
-
|
|
4
|
-
describe("Tool Router", () => {
|
|
5
|
-
let router: ToolRouter;
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
router = new ToolRouter();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
// ─── Test #20: Routes write_file to host ───
|
|
12
|
-
|
|
13
|
-
it("routes write_file to the host", () => {
|
|
14
|
-
expect(router.getTarget("write_file")).toBe(ToolTarget.HOST);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
// ─── Test #21: Routes read_file to host ───
|
|
18
|
-
|
|
19
|
-
it("routes read_file to the host", () => {
|
|
20
|
-
expect(router.getTarget("read_file")).toBe(ToolTarget.HOST);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// ─── Test #22: Routes bash to sandbox ───
|
|
24
|
-
|
|
25
|
-
it("routes bash to the sandbox", () => {
|
|
26
|
-
expect(router.getTarget("bash")).toBe(ToolTarget.SANDBOX);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// ─── Test #23: Routes run_tests to sandbox ───
|
|
30
|
-
|
|
31
|
-
it("routes run_tests to the sandbox", () => {
|
|
32
|
-
expect(router.getTarget("run_tests")).toBe(ToolTarget.SANDBOX);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// ─── Test #24: Routes install_deps to sandbox ───
|
|
36
|
-
|
|
37
|
-
it("routes install_deps to the sandbox", () => {
|
|
38
|
-
expect(router.getTarget("install_deps")).toBe(ToolTarget.SANDBOX);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// ─── Test #25: Routes search_tools to host ───
|
|
42
|
-
|
|
43
|
-
it("routes search_tools to the host", () => {
|
|
44
|
-
expect(router.getTarget("search_tools")).toBe(ToolTarget.HOST);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// ─── Test #26: Unknown tools default to sandbox (safe) ───
|
|
48
|
-
|
|
49
|
-
it("defaults unknown tools to sandbox for safety", () => {
|
|
50
|
-
expect(router.getTarget("unknown_tool")).toBe(ToolTarget.SANDBOX);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import * as os from "node:os";
|
|
5
|
-
import { ReadFileTool, WriteFileTool } from "../tools/index.js";
|
|
6
|
-
|
|
7
|
-
describe("ReadFileTool", () => {
|
|
8
|
-
let tmpDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
tmpDir = fs.mkdtempSync(path.join(process.cwd(), ".joone-tools-test-"));
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
// ─── Test #27: Reads a normal file ───
|
|
19
|
-
|
|
20
|
-
it("reads a small file and returns its content", async () => {
|
|
21
|
-
const filePath = path.join(tmpDir, "hello.txt");
|
|
22
|
-
fs.writeFileSync(filePath, "Hello, world!", "utf-8");
|
|
23
|
-
|
|
24
|
-
const result = await ReadFileTool.execute({ path: filePath });
|
|
25
|
-
|
|
26
|
-
expect(result.content).toBe("Hello, world!");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// ─── Test #28: Returns error for non-existent file ───
|
|
30
|
-
|
|
31
|
-
it("returns an error message for a non-existent file", async () => {
|
|
32
|
-
const result = await ReadFileTool.execute({
|
|
33
|
-
path: path.join(tmpDir, "nope.txt"),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
expect(result.content).toMatch(/not found/i);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// ─── Test #29: File size guardrail rejects files over 512KB ───
|
|
40
|
-
|
|
41
|
-
it("rejects files larger than 512KB with a descriptive error", async () => {
|
|
42
|
-
const filePath = path.join(tmpDir, "big.txt");
|
|
43
|
-
// Create a 600KB file
|
|
44
|
-
const bigContent = "x".repeat(600 * 1024);
|
|
45
|
-
fs.writeFileSync(filePath, bigContent, "utf-8");
|
|
46
|
-
|
|
47
|
-
const result = await ReadFileTool.execute({ path: filePath });
|
|
48
|
-
|
|
49
|
-
expect(result.content).toMatch(/too large/i);
|
|
50
|
-
expect(result.content).toMatch(/512/);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// ─── Test #30: Line range slicing works ───
|
|
54
|
-
|
|
55
|
-
it("returns only the requested line range", async () => {
|
|
56
|
-
const filePath = path.join(tmpDir, "lines.txt");
|
|
57
|
-
const lines = Array.from({ length: 20 }, (_, i) => `Line ${i + 1}`);
|
|
58
|
-
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
59
|
-
|
|
60
|
-
const result = await ReadFileTool.execute({
|
|
61
|
-
path: filePath,
|
|
62
|
-
startLine: 5,
|
|
63
|
-
endLine: 7,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
expect(result.content).toContain("5: Line 5");
|
|
67
|
-
expect(result.content).toContain("6: Line 6");
|
|
68
|
-
expect(result.content).toContain("7: Line 7");
|
|
69
|
-
expect(result.content).not.toContain("4: Line 4");
|
|
70
|
-
expect(result.content).not.toContain("8: Line 8");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// ─── Test #31: Line count guardrail truncates long files ───
|
|
74
|
-
|
|
75
|
-
it("truncates files with more than 2000 lines", async () => {
|
|
76
|
-
const filePath = path.join(tmpDir, "long.txt");
|
|
77
|
-
// Create a file with 2500 short lines (under 512KB)
|
|
78
|
-
const lines = Array.from({ length: 2500 }, (_, i) => `L${i + 1}`);
|
|
79
|
-
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
80
|
-
|
|
81
|
-
const result = await ReadFileTool.execute({ path: filePath });
|
|
82
|
-
|
|
83
|
-
expect(result.content).toMatch(/truncated at 2000 lines/i);
|
|
84
|
-
expect(result.content).toContain("1: L1");
|
|
85
|
-
expect(result.content).toContain("2000: L2000");
|
|
86
|
-
expect(result.content).not.toContain("2001: L2001");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// ─── Test #X: Security Guardrail Blocks Outside Files ───
|
|
90
|
-
|
|
91
|
-
it("blocks reading files outside the project workspace", async () => {
|
|
92
|
-
// Create a file in the OS tmp directory (guaranteed outside project workspace)
|
|
93
|
-
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "joone-outside-"));
|
|
94
|
-
const filePath = path.join(outsideDir, "secret.txt");
|
|
95
|
-
fs.writeFileSync(filePath, "secret token", "utf-8");
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
const result = await ReadFileTool.execute({ path: filePath });
|
|
99
|
-
expect(result.isError).toBe(true);
|
|
100
|
-
expect(result.content).toMatch(/Security Error: Access Denied/i);
|
|
101
|
-
expect(result.content).toMatch(/outside the current project workspace/i);
|
|
102
|
-
} finally {
|
|
103
|
-
fs.rmSync(outsideDir, { recursive: true, force: true });
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe("WriteFileTool", () => {
|
|
109
|
-
let tmpDir: string;
|
|
110
|
-
|
|
111
|
-
beforeEach(() => {
|
|
112
|
-
tmpDir = fs.mkdtempSync(path.join(process.cwd(), ".joone-write-test-"));
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
afterEach(() => {
|
|
116
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ─── Test #32: Writes a file to disk ───
|
|
120
|
-
|
|
121
|
-
it("writes content to a file and confirms", async () => {
|
|
122
|
-
const filePath = path.join(tmpDir, "output.ts");
|
|
123
|
-
|
|
124
|
-
const result = await WriteFileTool.execute({
|
|
125
|
-
path: filePath,
|
|
126
|
-
content: "const x = 42;",
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
expect(result.content).toMatch(/file written/i);
|
|
130
|
-
expect(fs.readFileSync(filePath, "utf-8")).toBe("const x = 42;");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// ─── Test #33: Creates parent directories if needed ───
|
|
134
|
-
|
|
135
|
-
it("creates parent directories if they do not exist", async () => {
|
|
136
|
-
const filePath = path.join(tmpDir, "nested", "deep", "file.ts");
|
|
137
|
-
|
|
138
|
-
const result = await WriteFileTool.execute({
|
|
139
|
-
path: filePath,
|
|
140
|
-
content: "export {}",
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
expect(result.content).toMatch(/file written/i);
|
|
144
|
-
expect(fs.existsSync(filePath)).toBe(true);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import * as os from "node:os";
|
|
5
|
-
import { SessionTracer } from "../tracing/sessionTracer.js";
|
|
6
|
-
import {
|
|
7
|
-
enableLangSmith,
|
|
8
|
-
disableLangSmith,
|
|
9
|
-
isLangSmithEnabled,
|
|
10
|
-
} from "../tracing/langsmith.js";
|
|
11
|
-
import { TraceAnalyzer } from "../tracing/analyzer.js";
|
|
12
|
-
import type { SessionTrace } from "../tracing/types.js";
|
|
13
|
-
|
|
14
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
15
|
-
// 6a: SessionTracer
|
|
16
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
17
|
-
|
|
18
|
-
describe("SessionTracer", () => {
|
|
19
|
-
// ─── Test #83: Records LLM calls and computes totals ───
|
|
20
|
-
|
|
21
|
-
it("records LLM calls and computes token totals", () => {
|
|
22
|
-
const tracer = new SessionTracer("test-session-1");
|
|
23
|
-
|
|
24
|
-
tracer.recordLLMCall({ promptTokens: 500, completionTokens: 100, cached: false, duration: 800 });
|
|
25
|
-
tracer.recordLLMCall({ promptTokens: 400, completionTokens: 150, cached: true, duration: 600 });
|
|
26
|
-
|
|
27
|
-
const summary = tracer.getSummary();
|
|
28
|
-
|
|
29
|
-
expect(summary.promptTokens).toBe(900);
|
|
30
|
-
expect(summary.completionTokens).toBe(250);
|
|
31
|
-
expect(summary.totalTokens).toBe(1150);
|
|
32
|
-
expect(summary.turnCount).toBe(2);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// ─── Test #84: Records tool calls and counts them ───
|
|
36
|
-
|
|
37
|
-
it("records tool calls and counts them", () => {
|
|
38
|
-
const tracer = new SessionTracer("test-session-2");
|
|
39
|
-
|
|
40
|
-
tracer.recordToolCall({ name: "bash", args: { command: "ls" }, duration: 50, success: true });
|
|
41
|
-
tracer.recordToolCall({ name: "write_file", args: { path: "a.ts" }, duration: 30, success: true });
|
|
42
|
-
tracer.recordToolCall({ name: "bash", args: { command: "npm test" }, duration: 200, success: false });
|
|
43
|
-
|
|
44
|
-
const summary = tracer.getSummary();
|
|
45
|
-
|
|
46
|
-
expect(summary.toolCallCount).toBe(3);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// ─── Test #85: Computes cache hit rate correctly ───
|
|
50
|
-
|
|
51
|
-
it("computes cache hit rate correctly", () => {
|
|
52
|
-
const tracer = new SessionTracer("test-session-3");
|
|
53
|
-
|
|
54
|
-
// 3 calls: 2 cached, 1 not
|
|
55
|
-
tracer.recordLLMCall({ promptTokens: 100, completionTokens: 50, cached: true, duration: 100 });
|
|
56
|
-
tracer.recordLLMCall({ promptTokens: 100, completionTokens: 50, cached: true, duration: 100 });
|
|
57
|
-
tracer.recordLLMCall({ promptTokens: 100, completionTokens: 50, cached: false, duration: 100 });
|
|
58
|
-
|
|
59
|
-
const summary = tracer.getSummary();
|
|
60
|
-
|
|
61
|
-
// 200 cached out of 300 total prompt tokens = 66.7%
|
|
62
|
-
expect(summary.cacheHitRate).toBeCloseTo(0.667, 2);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// ─── Test #86: export() returns valid SessionTrace ───
|
|
66
|
-
|
|
67
|
-
it("export() returns a valid SessionTrace", () => {
|
|
68
|
-
const tracer = new SessionTracer("export-test");
|
|
69
|
-
|
|
70
|
-
tracer.recordLLMCall({ promptTokens: 100, completionTokens: 50, cached: true, duration: 200 });
|
|
71
|
-
tracer.recordError({ message: "Timeout", tool: "bash" });
|
|
72
|
-
|
|
73
|
-
const trace = tracer.export();
|
|
74
|
-
|
|
75
|
-
expect(trace.sessionId).toBe("export-test");
|
|
76
|
-
expect(trace.startedAt).toBeGreaterThan(0);
|
|
77
|
-
expect(trace.endedAt).toBeGreaterThanOrEqual(trace.startedAt);
|
|
78
|
-
expect(trace.events).toHaveLength(2);
|
|
79
|
-
expect(trace.summary.turnCount).toBe(1);
|
|
80
|
-
expect(trace.summary.errorCount).toBe(1);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
85
|
-
// 6b: LangSmith Integration
|
|
86
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
87
|
-
|
|
88
|
-
describe("LangSmith Integration", () => {
|
|
89
|
-
afterEach(() => {
|
|
90
|
-
disableLangSmith();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// ─── Test #87: enableLangSmith sets correct env vars ───
|
|
94
|
-
|
|
95
|
-
it("sets the correct environment variables", () => {
|
|
96
|
-
enableLangSmith({ apiKey: "test-key-123", project: "my-project" });
|
|
97
|
-
|
|
98
|
-
expect(process.env.LANGCHAIN_TRACING_V2).toBe("true");
|
|
99
|
-
expect(process.env.LANGCHAIN_API_KEY).toBe("test-key-123");
|
|
100
|
-
expect(process.env.LANGCHAIN_PROJECT).toBe("my-project");
|
|
101
|
-
expect(isLangSmithEnabled()).toBe(true);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
// ─── Test #88: disableLangSmith clears env vars ───
|
|
105
|
-
|
|
106
|
-
it("disableLangSmith clears the environment variables", () => {
|
|
107
|
-
enableLangSmith({ apiKey: "test-key" });
|
|
108
|
-
disableLangSmith();
|
|
109
|
-
|
|
110
|
-
expect(process.env.LANGCHAIN_TRACING_V2).toBeUndefined();
|
|
111
|
-
expect(isLangSmithEnabled()).toBe(false);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
116
|
-
// 6c: TraceAnalyzer
|
|
117
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
118
|
-
|
|
119
|
-
describe("TraceAnalyzer", () => {
|
|
120
|
-
const createTrace = (overrides?: Partial<SessionTrace>): SessionTrace => ({
|
|
121
|
-
sessionId: "test",
|
|
122
|
-
startedAt: Date.now() - 10000,
|
|
123
|
-
endedAt: Date.now(),
|
|
124
|
-
events: [],
|
|
125
|
-
summary: {
|
|
126
|
-
totalTokens: 1000,
|
|
127
|
-
promptTokens: 700,
|
|
128
|
-
completionTokens: 300,
|
|
129
|
-
totalCost: 0.006,
|
|
130
|
-
cacheHitRate: 0.8,
|
|
131
|
-
toolCallCount: 5,
|
|
132
|
-
errorCount: 0,
|
|
133
|
-
totalDuration: 10000,
|
|
134
|
-
turnCount: 5,
|
|
135
|
-
},
|
|
136
|
-
...overrides,
|
|
137
|
-
});
|
|
138
|
-
// ─── Test #89: Detects loop patterns ───
|
|
139
|
-
|
|
140
|
-
it("detects doom-loop patterns in tool calls", () => {
|
|
141
|
-
const trace = createTrace({
|
|
142
|
-
events: [
|
|
143
|
-
{ type: "tool_call", timestamp: 1, data: { name: "bash", args: { command: "ls" } } },
|
|
144
|
-
{ type: "tool_call", timestamp: 2, data: { name: "bash", args: { command: "ls" } } },
|
|
145
|
-
{ type: "tool_call", timestamp: 3, data: { name: "bash", args: { command: "ls" } } },
|
|
146
|
-
],
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const analyzer = new TraceAnalyzer(trace);
|
|
150
|
-
const report = analyzer.analyze();
|
|
151
|
-
|
|
152
|
-
const loopIssues = report.issues.filter((i) => i.category === "loop");
|
|
153
|
-
expect(loopIssues.length).toBeGreaterThan(0);
|
|
154
|
-
expect(loopIssues[0].severity).toBe("critical");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ─── Test #90: Detects cost hotspots ───
|
|
158
|
-
|
|
159
|
-
it("flags turns consuming >20% of total tokens", () => {
|
|
160
|
-
const trace = createTrace({
|
|
161
|
-
summary: {
|
|
162
|
-
...createTrace().summary,
|
|
163
|
-
totalTokens: 1000,
|
|
164
|
-
},
|
|
165
|
-
events: [
|
|
166
|
-
{ type: "llm_call", timestamp: 1, data: { promptTokens: 300, completionTokens: 100, cached: false } },
|
|
167
|
-
{ type: "llm_call", timestamp: 2, data: { promptTokens: 100, completionTokens: 50, cached: true } },
|
|
168
|
-
],
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const analyzer = new TraceAnalyzer(trace);
|
|
172
|
-
const report = analyzer.analyze();
|
|
173
|
-
|
|
174
|
-
const costIssues = report.issues.filter((i) => i.category === "cost");
|
|
175
|
-
expect(costIssues.length).toBeGreaterThan(0);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// ─── Test #91: Warns on low cache hit rate ───
|
|
179
|
-
|
|
180
|
-
it("warns when cache hit rate is below 70%", () => {
|
|
181
|
-
const trace = createTrace({
|
|
182
|
-
summary: {
|
|
183
|
-
...createTrace().summary,
|
|
184
|
-
cacheHitRate: 0.5,
|
|
185
|
-
turnCount: 5,
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
const analyzer = new TraceAnalyzer(trace);
|
|
190
|
-
const report = analyzer.analyze();
|
|
191
|
-
|
|
192
|
-
const cacheIssues = report.issues.filter((i) => i.category === "cache");
|
|
193
|
-
expect(cacheIssues.length).toBe(1);
|
|
194
|
-
expect(cacheIssues[0].message).toContain("50.0%");
|
|
195
|
-
});
|
|
196
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Registry
|
|
3
|
-
*
|
|
4
|
-
* Central registry for named sub-agents. The registry enables:
|
|
5
|
-
* - Decoupled agent development (add agents without touching the main loop)
|
|
6
|
-
* - Prompt injection (registry summary included in the main agent's system prompt)
|
|
7
|
-
* - Lookup by name for the spawn_agent tool
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { AgentSpec } from "./agentSpec.js";
|
|
11
|
-
|
|
12
|
-
export class AgentRegistry {
|
|
13
|
-
private agents: Map<string, AgentSpec> = new Map();
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Register a new agent spec. Overwrites if name already exists.
|
|
17
|
-
*/
|
|
18
|
-
register(spec: AgentSpec): void {
|
|
19
|
-
this.agents.set(spec.name, spec);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Look up an agent by name.
|
|
24
|
-
*/
|
|
25
|
-
get(name: string): AgentSpec | undefined {
|
|
26
|
-
return this.agents.get(name);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Returns all registered agent specs.
|
|
31
|
-
*/
|
|
32
|
-
getAll(): AgentSpec[] {
|
|
33
|
-
return Array.from(this.agents.values());
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Returns all registered agent names.
|
|
38
|
-
*/
|
|
39
|
-
getNames(): string[] {
|
|
40
|
-
return Array.from(this.agents.keys());
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Returns true if an agent with the given name exists.
|
|
45
|
-
*/
|
|
46
|
-
has(name: string): boolean {
|
|
47
|
-
return this.agents.has(name);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Generates a summary of all available agents, formatted for injection
|
|
52
|
-
* into the main agent's system prompt.
|
|
53
|
-
*/
|
|
54
|
-
getSummary(): string {
|
|
55
|
-
if (this.agents.size === 0) {
|
|
56
|
-
return "No sub-agents are currently registered.";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const lines = ["Available sub-agents (use spawn_agent tool to invoke):\n"];
|
|
60
|
-
|
|
61
|
-
for (const spec of this.agents.values()) {
|
|
62
|
-
const tools = spec.tools ? ` [tools: ${spec.tools.join(", ")}]` : " [all tools]";
|
|
63
|
-
const turns = spec.maxTurns ?? 10;
|
|
64
|
-
lines.push(` • ${spec.name}: ${spec.description}${tools} (max ${turns} turns)`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return lines.join("\n");
|
|
68
|
-
}
|
|
69
|
-
}
|
package/src/agents/agentSpec.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent Specification
|
|
3
|
-
*
|
|
4
|
-
* Defines the shape of a sub-agent: its identity, capabilities, constraints,
|
|
5
|
-
* and tools. This enables decoupled agent development — new agents can be
|
|
6
|
-
* added to the registry without modifying the main agent or harness.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Describes a named sub-agent with a purpose-tuned configuration.
|
|
11
|
-
*/
|
|
12
|
-
export interface AgentSpec {
|
|
13
|
-
/** Unique name (e.g., "script_runner", "code_reviewer"). */
|
|
14
|
-
name: string;
|
|
15
|
-
|
|
16
|
-
/** Human-readable description included in the main agent's prompt. */
|
|
17
|
-
description: string;
|
|
18
|
-
|
|
19
|
-
/** Dedicated system prompt for this sub-agent. */
|
|
20
|
-
systemPrompt: string;
|
|
21
|
-
|
|
22
|
-
/** Restrict to specific tool names. If omitted, all main-agent tools are available. */
|
|
23
|
-
tools?: string[];
|
|
24
|
-
|
|
25
|
-
/** Maximum turns before the sub-agent is forcibly stopped (doom-loop protection). Default: 10. */
|
|
26
|
-
maxTurns?: number;
|
|
27
|
-
|
|
28
|
-
/** Override model for this agent (default: FAST_MODEL_DEFAULTS from same provider). */
|
|
29
|
-
model?: string;
|
|
30
|
-
|
|
31
|
-
/** Permission behavior for this agent. */
|
|
32
|
-
permissionMode?: "auto" | "ask_all";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Structured result returned by a sub-agent after completing (or failing) a task.
|
|
37
|
-
* Only this result is injected into the main agent's history — the sub-agent's
|
|
38
|
-
* full conversation is discarded to save context.
|
|
39
|
-
*/
|
|
40
|
-
export interface SubAgentResult {
|
|
41
|
-
/** The agent name from AgentSpec. */
|
|
42
|
-
agentName: string;
|
|
43
|
-
|
|
44
|
-
/** The original task description. */
|
|
45
|
-
taskDescription: string;
|
|
46
|
-
|
|
47
|
-
/** Outcome status. */
|
|
48
|
-
outcome: "success" | "failure" | "partial";
|
|
49
|
-
|
|
50
|
-
/** The final text output from the sub-agent. */
|
|
51
|
-
result: string;
|
|
52
|
-
|
|
53
|
-
/** Files created, modified, or deleted during the sub-task. */
|
|
54
|
-
filesModified: string[];
|
|
55
|
-
|
|
56
|
-
/** Total tool calls executed. */
|
|
57
|
-
toolCallCount: number;
|
|
58
|
-
|
|
59
|
-
/** Approximate token usage. */
|
|
60
|
-
tokenUsage: { prompt: number; completion: number };
|
|
61
|
-
|
|
62
|
-
/** Wall-clock duration in milliseconds. */
|
|
63
|
-
duration: number;
|
|
64
|
-
|
|
65
|
-
/** Number of turns the sub-agent ran. */
|
|
66
|
-
turnsUsed: number;
|
|
67
|
-
}
|
package/src/cli/config.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The shape of the Joone configuration file (~/.joone/config.json).
|
|
6
|
-
*/
|
|
7
|
-
export interface JooneConfig {
|
|
8
|
-
provider: string;
|
|
9
|
-
model: string;
|
|
10
|
-
apiKey?: string;
|
|
11
|
-
maxTokens: number;
|
|
12
|
-
temperature: number;
|
|
13
|
-
streaming: boolean;
|
|
14
|
-
/** E2B sandbox template. If set, uses a pre-baked template (prod). If unset, uses default + lazy install (dev). */
|
|
15
|
-
sandboxTemplate?: string;
|
|
16
|
-
/** E2B API key for sandbox provisioning. */
|
|
17
|
-
e2bApiKey?: string;
|
|
18
|
-
/** OpenSandbox API key for sandbox fallback provisioning. */
|
|
19
|
-
openSandboxApiKey?: string;
|
|
20
|
-
/** OpenSandbox API Domain for fallback. */
|
|
21
|
-
openSandboxDomain?: string;
|
|
22
|
-
/** Gemini API key for SecurityScanTool (Gemini CLI inside sandbox). */
|
|
23
|
-
geminiApiKey?: string;
|
|
24
|
-
/** Valyu API key for web search. */
|
|
25
|
-
valyuApiKey?: string;
|
|
26
|
-
/** LangSmith API key for tracing (optional). */
|
|
27
|
-
langsmithApiKey?: string;
|
|
28
|
-
/** LangSmith project name (optional, default: "joone"). */
|
|
29
|
-
langsmithProject?: string;
|
|
30
|
-
/** Tool permission mode: 'auto' (no prompts), 'ask_dangerous' (prompt for destructive tools), 'ask_all' (prompt for everything). */
|
|
31
|
-
permissionMode?: "auto" | "ask_dangerous" | "ask_all";
|
|
32
|
-
/** Override model for context compaction (default: auto-selected fast model from same provider). */
|
|
33
|
-
compactModel?: string;
|
|
34
|
-
/** Override model for sub-agents (default: auto-selected fast model from same provider). */
|
|
35
|
-
subAgentModel?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Sensible defaults — Anthropic Claude as the default provider.
|
|
40
|
-
*/
|
|
41
|
-
export const DEFAULT_CONFIG: JooneConfig = {
|
|
42
|
-
provider: "anthropic",
|
|
43
|
-
model: "claude-sonnet-4-20250514",
|
|
44
|
-
maxTokens: 4096,
|
|
45
|
-
temperature: 0,
|
|
46
|
-
streaming: true,
|
|
47
|
-
permissionMode: "auto",
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Maps provider names to their expected environment variable for the API key.
|
|
52
|
-
*/
|
|
53
|
-
const PROVIDER_ENV_VARS: Record<string, string> = {
|
|
54
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
55
|
-
openai: "OPENAI_API_KEY",
|
|
56
|
-
google: "GOOGLE_API_KEY",
|
|
57
|
-
mistral: "MISTRAL_API_KEY",
|
|
58
|
-
groq: "GROQ_API_KEY",
|
|
59
|
-
deepseek: "DEEPSEEK_API_KEY",
|
|
60
|
-
fireworks: "FIREWORKS_API_KEY",
|
|
61
|
-
together: "TOGETHER_API_KEY",
|
|
62
|
-
// Ollama (local) doesn't need an API key
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Loads the Joone config from the specified path.
|
|
67
|
-
* Returns DEFAULT_CONFIG if the file does not exist.
|
|
68
|
-
* Falls back to environment variables for API key if not set in config.
|
|
69
|
-
*/
|
|
70
|
-
export function loadConfig(configPath: string): JooneConfig {
|
|
71
|
-
let config: JooneConfig;
|
|
72
|
-
|
|
73
|
-
if (!fs.existsSync(configPath)) {
|
|
74
|
-
config = { ...DEFAULT_CONFIG };
|
|
75
|
-
} else {
|
|
76
|
-
try {
|
|
77
|
-
const raw = fs.readFileSync(configPath, "utf-8");
|
|
78
|
-
const parsed = JSON.parse(raw) as Partial<JooneConfig>;
|
|
79
|
-
config = { ...DEFAULT_CONFIG, ...parsed };
|
|
80
|
-
} catch (err) {
|
|
81
|
-
console.warn(`Warning: Failed to parse config at ${configPath}. Using defaults.`);
|
|
82
|
-
config = { ...DEFAULT_CONFIG };
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// Env var fallback: if apiKey is missing, check the provider's env var
|
|
86
|
-
if (!config.apiKey) {
|
|
87
|
-
const envVar = PROVIDER_ENV_VARS[config.provider];
|
|
88
|
-
if (envVar && process.env[envVar]) {
|
|
89
|
-
config.apiKey = process.env[envVar];
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return config;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Saves the Joone config to the specified path.
|
|
98
|
-
* Creates the parent directory if it doesn't exist.
|
|
99
|
-
* Sets restrictive file permissions (owner-only read/write) for security.
|
|
100
|
-
*/
|
|
101
|
-
export function saveConfig(configPath: string, config: JooneConfig): void {
|
|
102
|
-
const dir = path.dirname(configPath);
|
|
103
|
-
if (!fs.existsSync(dir)) {
|
|
104
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
105
|
-
}
|
|
106
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), {
|
|
107
|
-
encoding: "utf-8",
|
|
108
|
-
mode: 0o600, // Owner read/write only (Linux/macOS)
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// On Unix systems, enforce permissions even if file already existed
|
|
112
|
-
try {
|
|
113
|
-
fs.chmodSync(configPath, 0o600);
|
|
114
|
-
} catch {
|
|
115
|
-
// chmod may fail on Windows — ignore silently
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Returns the expected environment variable name for a provider's API key.
|
|
121
|
-
*/
|
|
122
|
-
export function getProviderEnvVar(provider: string): string | undefined {
|
|
123
|
-
return PROVIDER_ENV_VARS[provider];
|
|
124
|
-
}
|