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