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