@jellyos/agent 0.1.3 → 0.1.5
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 +9 -9
- package/README.npm.md +212 -0
- package/bin/jellyos-mcp +26 -0
- package/dist/api/ExtensionAPI.d.ts +6 -0
- package/dist/api/Registry.js +3 -1
- package/dist/cli.js +117 -42
- package/dist/index.d.ts +24 -1
- package/dist/index.js +19 -2
- package/dist/mcp/entry.d.ts +2 -0
- package/dist/mcp/entry.js +71 -0
- package/dist/mcp/server.d.ts +31 -0
- package/dist/mcp/server.js +128 -0
- package/dist/models/CostTracker.d.ts +66 -0
- package/dist/models/CostTracker.js +148 -0
- package/dist/models/ModelRegistry.d.ts +157 -0
- package/dist/models/ModelRegistry.js +496 -0
- package/dist/models/index.d.ts +5 -0
- package/dist/models/index.js +3 -0
- package/dist/runner/AgentRunner.d.ts +23 -2
- package/dist/runner/AgentRunner.js +264 -24
- package/dist/runner/ModelClient.d.ts +26 -6
- package/dist/runner/ModelClient.js +147 -28
- package/dist/runner/SwarmRouter.d.ts +10 -7
- package/dist/runner/SwarmRouter.js +85 -28
- package/dist/runner/ToolDispatcher.d.ts +10 -0
- package/dist/runner/ToolDispatcher.js +106 -2
- package/dist/scheduler/AgentScheduler.d.ts +118 -0
- package/dist/scheduler/AgentScheduler.js +253 -0
- package/dist/session/ContextStore.d.ts +96 -0
- package/dist/session/ContextStore.js +207 -0
- package/dist/session/GoalManager.d.ts +101 -0
- package/dist/session/GoalManager.js +167 -0
- package/dist/session/MemoryStore.d.ts +48 -0
- package/dist/session/MemoryStore.js +166 -0
- package/dist/session/SessionManager.d.ts +45 -4
- package/dist/session/SessionManager.js +151 -8
- package/dist/telemetry/Tracer.d.ts +48 -0
- package/dist/telemetry/Tracer.js +102 -0
- package/dist/tests/ContextStore.test.d.ts +2 -0
- package/dist/tests/ContextStore.test.js +74 -0
- package/dist/tests/ModelRegistry.test.d.ts +2 -0
- package/dist/tests/ModelRegistry.test.js +69 -0
- package/dist/tests/SessionManager.test.d.ts +2 -0
- package/dist/tests/SessionManager.test.js +108 -0
- package/dist/tests/TechnicalAnalysis.test.d.ts +2 -0
- package/dist/tests/TechnicalAnalysis.test.js +109 -0
- package/dist/tools/MarketSentiment.d.ts +166 -0
- package/dist/tools/MarketSentiment.js +209 -0
- package/dist/tools/NewsSentiment.d.ts +67 -0
- package/dist/tools/NewsSentiment.js +226 -0
- package/dist/tools/PriceFeed.d.ts +105 -0
- package/dist/tools/PriceFeed.js +282 -0
- package/dist/tools/TechnicalAnalysis.d.ts +110 -0
- package/dist/tools/TechnicalAnalysis.js +357 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +4 -0
- package/dist/tui/App.d.ts +7 -5
- package/dist/tui/App.js +350 -65
- package/dist/tui/REPL.d.ts +2 -1
- package/dist/tui/REPL.js +11 -6
- package/dist/tui/StatusBar.js +1 -1
- package/package.json +9 -4
- package/dist/api/ExtensionAPI.d.ts.map +0 -1
- package/dist/api/ExtensionAPI.js.map +0 -1
- package/dist/api/Registry.d.ts.map +0 -1
- package/dist/api/Registry.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js.map +0 -1
- package/dist/runner/AgentRunner.d.ts.map +0 -1
- package/dist/runner/AgentRunner.js.map +0 -1
- package/dist/runner/ModelClient.d.ts.map +0 -1
- package/dist/runner/ModelClient.js.map +0 -1
- package/dist/runner/SwarmRouter.d.ts.map +0 -1
- package/dist/runner/SwarmRouter.js.map +0 -1
- package/dist/runner/ToolDispatcher.d.ts.map +0 -1
- package/dist/runner/ToolDispatcher.js.map +0 -1
- package/dist/session/SessionManager.d.ts.map +0 -1
- package/dist/session/SessionManager.js.map +0 -1
- package/dist/tui/App.d.ts.map +0 -1
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/REPL.d.ts.map +0 -1
- package/dist/tui/REPL.js.map +0 -1
- package/dist/tui/StatusBar.d.ts.map +0 -1
- package/dist/tui/StatusBar.js.map +0 -1
- package/dist/tui/theme.d.ts.map +0 -1
- package/dist/tui/theme.js.map +0 -1
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SessionManager — manages conversation history, compaction, and persistence.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Compaction is done in atomic turn pairs so tool_call_id references are
|
|
5
|
+
* never orphaned (which causes 400 errors from every provider).
|
|
6
|
+
*
|
|
7
|
+
* Three compaction tiers:
|
|
8
|
+
* Tier 1 (>70%): trim long tool results to 1-line summaries
|
|
9
|
+
* Tier 2 (>85%): drop oldest complete turns (atomic pairs only)
|
|
10
|
+
* Tier 3 (>95%): hard reset keeping only last KEEP_TURNS turns
|
|
6
11
|
*/
|
|
7
12
|
const MAX_HISTORY_CHARS = 80_000; // ~20k tokens rough estimate
|
|
8
|
-
const
|
|
13
|
+
const KEEP_TURNS = 8; // complete turns to keep on hard reset
|
|
14
|
+
const TOOL_RESULT_CAP = 500; // trim tool results longer than this
|
|
9
15
|
export class SessionManager {
|
|
10
16
|
history = [];
|
|
11
17
|
systemPrompt = "";
|
|
@@ -29,22 +35,159 @@ export class SessionManager {
|
|
|
29
35
|
const sys = { role: "system", content: this.buildSystemContent() };
|
|
30
36
|
return [sys, ...this.history];
|
|
31
37
|
}
|
|
32
|
-
/** Full raw history (no system) */
|
|
38
|
+
/** Full raw history (no system prompt) */
|
|
33
39
|
getHistory() {
|
|
34
40
|
return [...this.history];
|
|
35
41
|
}
|
|
36
42
|
clear() {
|
|
37
43
|
this.history = [];
|
|
38
44
|
}
|
|
45
|
+
charCount() {
|
|
46
|
+
return this.history.reduce((n, m) => n + (typeof m.content === "string" ? m.content.length : 0), 0);
|
|
47
|
+
}
|
|
48
|
+
getContextPressure() {
|
|
49
|
+
const chars = this.charCount();
|
|
50
|
+
const pct = Math.min(100, Math.round(chars / MAX_HISTORY_CHARS * 100));
|
|
51
|
+
return {
|
|
52
|
+
chars,
|
|
53
|
+
pct,
|
|
54
|
+
level: pct < 50 ? "green" : pct < 70 ? "yellow" : pct < 90 ? "red" : "critical",
|
|
55
|
+
turboReady: pct < 70, // keep 30% free for swarm sub-agent outputs
|
|
56
|
+
};
|
|
57
|
+
}
|
|
39
58
|
buildSystemContent() {
|
|
40
59
|
return this.systemPrompt || "You are JellyOS, an AI trading agent.";
|
|
41
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Split history into atomic turns.
|
|
63
|
+
* A turn = [user_msg, assistant_msg, ...tool_result_msgs]
|
|
64
|
+
* Keeping turns atomic prevents orphaned tool_call_id errors.
|
|
65
|
+
*/
|
|
66
|
+
splitIntoTurns() {
|
|
67
|
+
const turns = [];
|
|
68
|
+
let current = [];
|
|
69
|
+
for (const msg of this.history) {
|
|
70
|
+
// New user message starts a new turn (unless history starts with assistant)
|
|
71
|
+
if (msg.role === "user" && current.length > 0) {
|
|
72
|
+
turns.push(current);
|
|
73
|
+
current = [];
|
|
74
|
+
}
|
|
75
|
+
current.push(msg);
|
|
76
|
+
}
|
|
77
|
+
if (current.length > 0)
|
|
78
|
+
turns.push(current);
|
|
79
|
+
return turns;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Tier 1: Trim long tool results in-place. No messages dropped.
|
|
83
|
+
* Replaces content >TOOL_RESULT_CAP chars with a 1-line summary.
|
|
84
|
+
*/
|
|
85
|
+
trimToolResults() {
|
|
86
|
+
for (const msg of this.history) {
|
|
87
|
+
if (msg.role === "tool" && typeof msg.content === "string" &&
|
|
88
|
+
msg.content.length > TOOL_RESULT_CAP) {
|
|
89
|
+
const firstLine = msg.content.split("\n")[0] ?? msg.content.slice(0, 120);
|
|
90
|
+
msg.content = `${firstLine} … [trimmed, ${msg.content.length} chars]`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Tier 2: Drop oldest turns, but ANCHOR high-value messages (prices, signals,
|
|
96
|
+
* decisions) so they survive compaction. (#36 semantic anchor compaction)
|
|
97
|
+
*/
|
|
98
|
+
dropOldTurns() {
|
|
99
|
+
const turns = this.splitIntoTurns();
|
|
100
|
+
if (turns.length <= KEEP_TURNS)
|
|
101
|
+
return;
|
|
102
|
+
const recentTurns = turns.slice(-KEEP_TURNS);
|
|
103
|
+
const olderTurns = turns.slice(0, -KEEP_TURNS);
|
|
104
|
+
// #36: From older turns, extract high-weight messages as anchors
|
|
105
|
+
const anchors = [];
|
|
106
|
+
for (const turn of olderTurns) {
|
|
107
|
+
for (const msg of turn) {
|
|
108
|
+
if (this.messageWeight(msg) >= 7)
|
|
109
|
+
anchors.push(msg);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Prepend anchors (deduped) before the recent window
|
|
113
|
+
const anchorIds = new Set(anchors.map(m => `${m.role}:${String(m.content).slice(0, 50)}`));
|
|
114
|
+
const recentFlat = recentTurns.flat();
|
|
115
|
+
// Remove any anchors already in recent window
|
|
116
|
+
const recentIds = new Set(recentFlat.map(m => `${m.role}:${String(m.content).slice(0, 50)}`));
|
|
117
|
+
const dedupedAnchors = anchors.filter(m => !recentIds.has(`${m.role}:${String(m.content).slice(0, 50)}`));
|
|
118
|
+
this.history = [...dedupedAnchors, ...recentFlat];
|
|
119
|
+
void anchorIds; // suppress unused warning
|
|
120
|
+
}
|
|
121
|
+
/** #36: Score a message's semantic importance (0-10) */
|
|
122
|
+
messageWeight(msg) {
|
|
123
|
+
if (msg.role === "system")
|
|
124
|
+
return 10;
|
|
125
|
+
const content = typeof msg.content === "string" ? msg.content.toLowerCase() : "";
|
|
126
|
+
if (msg.role === "tool") {
|
|
127
|
+
// High value: price data, signals, positions, goals
|
|
128
|
+
if (/\$[\d,]+|\d+%|signal|buy|sell|position|goal|rsi|macd/.test(content))
|
|
129
|
+
return 8;
|
|
130
|
+
if (/news|sentiment|bullish|bearish|tvl|funding/.test(content))
|
|
131
|
+
return 6;
|
|
132
|
+
return 2; // generic tool result — expendable
|
|
133
|
+
}
|
|
134
|
+
if (msg.role === "user")
|
|
135
|
+
return 3;
|
|
136
|
+
if (msg.role === "assistant") {
|
|
137
|
+
// Keep assistant messages with decisions/recommendations
|
|
138
|
+
if (/recommend|suggest|should|will|plan|strategy|target|stop/.test(content))
|
|
139
|
+
return 7;
|
|
140
|
+
return 4;
|
|
141
|
+
}
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
42
144
|
maybeCompact() {
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
145
|
+
const pct = this.getContextPressure().pct;
|
|
146
|
+
if (pct < 70)
|
|
147
|
+
return;
|
|
148
|
+
if (pct < 85) {
|
|
149
|
+
this.trimToolResults();
|
|
45
150
|
return;
|
|
46
|
-
//
|
|
47
|
-
|
|
151
|
+
} // Tier 1: trim verbose tool results
|
|
152
|
+
if (pct < 95) {
|
|
153
|
+
this.trimToolResults();
|
|
154
|
+
this.dropOldTurns();
|
|
155
|
+
return;
|
|
156
|
+
} // Tier 2: drop turns
|
|
157
|
+
// Tier 3: hard reset — emergency
|
|
158
|
+
this.trimToolResults();
|
|
159
|
+
this.dropOldTurns();
|
|
160
|
+
if (this.getContextPressure().pct >= 95) {
|
|
161
|
+
const turns = this.splitIntoTurns();
|
|
162
|
+
this.history = turns.slice(-4).flat();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Force compaction */
|
|
166
|
+
forceCompact() {
|
|
167
|
+
this.trimToolResults();
|
|
168
|
+
this.dropOldTurns();
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* #32: Tier-2 summarization via a cheap model.
|
|
172
|
+
* Compresses the oldest 40% of turns into a single summary message.
|
|
173
|
+
* Call this when pct is between 70-90 to reclaim space gracefully.
|
|
174
|
+
* @param summarizeCallback — provided by AgentRunner which has model access
|
|
175
|
+
*/
|
|
176
|
+
async summarizeOldTurns(summarizeCallback) {
|
|
177
|
+
const turns = this.splitIntoTurns();
|
|
178
|
+
if (turns.length < 4)
|
|
179
|
+
return; // not enough history to summarize
|
|
180
|
+
const splitAt = Math.floor(turns.length * 0.4);
|
|
181
|
+
const toSummarize = turns.slice(0, splitAt).flat();
|
|
182
|
+
const toKeep = turns.slice(splitAt).flat();
|
|
183
|
+
const summary = await summarizeCallback(toSummarize);
|
|
184
|
+
this.history = [
|
|
185
|
+
{
|
|
186
|
+
role: "system",
|
|
187
|
+
content: `[CONVERSATION SUMMARY — ${toSummarize.length} earlier messages compressed]\n${summary}`,
|
|
188
|
+
},
|
|
189
|
+
...toKeep,
|
|
190
|
+
];
|
|
48
191
|
}
|
|
49
192
|
}
|
|
50
193
|
//# sourceMappingURL=SessionManager.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracer — lightweight JSONL span tracing for agent observability. (#30)
|
|
3
|
+
*
|
|
4
|
+
* Zero external dependencies. Writes structured traces to ~/.jelly/traces.jsonl.
|
|
5
|
+
* Each trace covers one user turn and records all model calls, tool dispatches,
|
|
6
|
+
* and swarm sub-tasks with timing and status.
|
|
7
|
+
*
|
|
8
|
+
* View traces: cat ~/.jelly/traces.jsonl | jq
|
|
9
|
+
* Last 5 traces: tail -5 ~/.jelly/traces.jsonl | jq
|
|
10
|
+
* /traces command in REPL shows a formatted summary.
|
|
11
|
+
*/
|
|
12
|
+
export interface Span {
|
|
13
|
+
spanId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
startMs: number;
|
|
16
|
+
durationMs?: number;
|
|
17
|
+
status: "ok" | "error" | "aborted";
|
|
18
|
+
attrs: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface Trace {
|
|
21
|
+
traceId: string;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
prompt: string;
|
|
24
|
+
startMs: number;
|
|
25
|
+
durationMs?: number;
|
|
26
|
+
spans: Span[];
|
|
27
|
+
totalCost?: number;
|
|
28
|
+
modelUsed?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare class Tracer {
|
|
31
|
+
/** @internal exposed for AgentRunner span lookup */
|
|
32
|
+
readonly trace: Trace;
|
|
33
|
+
constructor(sessionId: string, prompt: string);
|
|
34
|
+
/** Start a named span, returns spanId */
|
|
35
|
+
startSpan(name: string, attrs?: Record<string, unknown>): string;
|
|
36
|
+
/** End a span by ID */
|
|
37
|
+
endSpan(spanId: string, status?: "ok" | "error" | "aborted", attrs?: Record<string, unknown>): void;
|
|
38
|
+
/** Record model call details */
|
|
39
|
+
recordModel(spanId: string, model: string, promptTokens: number, completionTokens: number, costNano: number): void;
|
|
40
|
+
/** Flush trace to disk */
|
|
41
|
+
flush(status?: "ok" | "error" | "aborted"): void;
|
|
42
|
+
get traceId(): string;
|
|
43
|
+
/** Read last N traces from disk for /traces command */
|
|
44
|
+
static readRecent(limit?: number): Trace[];
|
|
45
|
+
/** Format traces for REPL display */
|
|
46
|
+
static formatSummary(traces: Trace[]): string;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=Tracer.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracer — lightweight JSONL span tracing for agent observability. (#30)
|
|
3
|
+
*
|
|
4
|
+
* Zero external dependencies. Writes structured traces to ~/.jelly/traces.jsonl.
|
|
5
|
+
* Each trace covers one user turn and records all model calls, tool dispatches,
|
|
6
|
+
* and swarm sub-tasks with timing and status.
|
|
7
|
+
*
|
|
8
|
+
* View traces: cat ~/.jelly/traces.jsonl | jq
|
|
9
|
+
* Last 5 traces: tail -5 ~/.jelly/traces.jsonl | jq
|
|
10
|
+
* /traces command in REPL shows a formatted summary.
|
|
11
|
+
*/
|
|
12
|
+
import { appendFileSync, existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
|
|
17
|
+
const TRACES_FILE = join(JELLY_HOME, "traces.jsonl");
|
|
18
|
+
export class Tracer {
|
|
19
|
+
/** @internal exposed for AgentRunner span lookup */
|
|
20
|
+
trace;
|
|
21
|
+
constructor(sessionId, prompt) {
|
|
22
|
+
this.trace = {
|
|
23
|
+
traceId: randomUUID().slice(0, 12),
|
|
24
|
+
sessionId,
|
|
25
|
+
prompt: prompt.slice(0, 200),
|
|
26
|
+
startMs: Date.now(),
|
|
27
|
+
spans: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Start a named span, returns spanId */
|
|
31
|
+
startSpan(name, attrs = {}) {
|
|
32
|
+
const spanId = randomUUID().slice(0, 8);
|
|
33
|
+
this.trace.spans.push({ spanId, name, startMs: Date.now(), status: "ok", attrs });
|
|
34
|
+
return spanId;
|
|
35
|
+
}
|
|
36
|
+
/** End a span by ID */
|
|
37
|
+
endSpan(spanId, status = "ok", attrs = {}) {
|
|
38
|
+
const span = this.trace.spans.find(s => s.spanId === spanId);
|
|
39
|
+
if (!span)
|
|
40
|
+
return;
|
|
41
|
+
span.durationMs = Date.now() - span.startMs;
|
|
42
|
+
span.status = status;
|
|
43
|
+
Object.assign(span.attrs, attrs);
|
|
44
|
+
}
|
|
45
|
+
/** Record model call details */
|
|
46
|
+
recordModel(spanId, model, promptTokens, completionTokens, costNano) {
|
|
47
|
+
this.endSpan(spanId, "ok", { model, promptTokens, completionTokens, costNano });
|
|
48
|
+
this.trace.modelUsed = model;
|
|
49
|
+
this.trace.totalCost = (this.trace.totalCost ?? 0) + costNano;
|
|
50
|
+
}
|
|
51
|
+
/** Flush trace to disk */
|
|
52
|
+
flush(status = "ok") {
|
|
53
|
+
this.trace.durationMs = Date.now() - this.trace.startMs;
|
|
54
|
+
try {
|
|
55
|
+
mkdirSync(JELLY_HOME, { recursive: true });
|
|
56
|
+
appendFileSync(TRACES_FILE, JSON.stringify({ ...this.trace, status, ts: Date.now() }) + "\n", "utf-8");
|
|
57
|
+
}
|
|
58
|
+
catch { /* best effort — tracing should never crash the agent */ }
|
|
59
|
+
}
|
|
60
|
+
get traceId() { return this.trace.traceId; }
|
|
61
|
+
// ── Static helpers ────────────────────────────────────────────────────────
|
|
62
|
+
/** Read last N traces from disk for /traces command */
|
|
63
|
+
static readRecent(limit = 5) {
|
|
64
|
+
try {
|
|
65
|
+
if (!existsSync(TRACES_FILE))
|
|
66
|
+
return [];
|
|
67
|
+
const lines = readFileSync(TRACES_FILE, "utf-8")
|
|
68
|
+
.trim().split("\n").filter(Boolean);
|
|
69
|
+
return lines.slice(-limit).map(l => JSON.parse(l)).reverse();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Format traces for REPL display */
|
|
76
|
+
static formatSummary(traces) {
|
|
77
|
+
if (traces.length === 0)
|
|
78
|
+
return "No traces recorded yet.";
|
|
79
|
+
return traces.map(t => {
|
|
80
|
+
const duration = t.durationMs ? `${(t.durationMs / 1000).toFixed(1)}s` : "?s";
|
|
81
|
+
const model = t.modelUsed?.split("/").pop()?.slice(0, 20) ?? "?";
|
|
82
|
+
const spans = t.spans.length;
|
|
83
|
+
const tools = t.spans.filter(s => s.name.startsWith("tool:")).length;
|
|
84
|
+
const cost = t.totalCost ? `$${(t.totalCost / 1_000_000_000).toFixed(4)}` : "?";
|
|
85
|
+
const prompt = t.prompt.slice(0, 60);
|
|
86
|
+
const errors = t.spans.filter(s => s.status === "error").length;
|
|
87
|
+
const lines = [
|
|
88
|
+
`[${t.traceId}] ${new Date(t.startMs).toLocaleTimeString()} — ${duration} · ${model} · ${cost}`,
|
|
89
|
+
` Prompt: "${prompt}"`,
|
|
90
|
+
` Spans: ${spans} total, ${tools} tool calls${errors > 0 ? `, ${errors} errors` : ""}`,
|
|
91
|
+
];
|
|
92
|
+
// Show tool spans
|
|
93
|
+
for (const span of t.spans.filter(s => s.name.startsWith("tool:"))) {
|
|
94
|
+
const icon = span.status === "ok" ? "✓" : "✗";
|
|
95
|
+
const dur = span.durationMs ? `${span.durationMs}ms` : "?";
|
|
96
|
+
lines.push(` ${icon} ${span.name.slice(5)} (${dur})`);
|
|
97
|
+
}
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}).join("\n\n");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=Tracer.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextStore tests — task lifecycle, findings, auto-delete.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
5
|
+
import { ContextStore } from "../session/ContextStore.js";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
// Use a temp dir for tests
|
|
8
|
+
process.env.JELLYOS_HOME = `/tmp/jellyos-test-${Date.now()}`;
|
|
9
|
+
describe("ContextStore", () => {
|
|
10
|
+
let store;
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
store = new ContextStore(); // fresh store
|
|
13
|
+
});
|
|
14
|
+
it("openTask creates a context.md file", () => {
|
|
15
|
+
store = new ContextStore();
|
|
16
|
+
const ctx = store.openTask("Test task");
|
|
17
|
+
expect(existsSync(ctx.contextMd)).toBe(true);
|
|
18
|
+
expect(ctx.taskId.length).toBeGreaterThan(0);
|
|
19
|
+
expect(ctx.findings).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
it("appendFinding increments findings count", () => {
|
|
22
|
+
store = new ContextStore();
|
|
23
|
+
const ctx = store.openTask("Test");
|
|
24
|
+
store.appendFinding(ctx.taskId, "Price Data", "BTC: $70,000");
|
|
25
|
+
store.appendFinding(ctx.taskId, "RSI", "RSI: 67 bullish");
|
|
26
|
+
expect(store.getTask(ctx.taskId)?.findings).toBe(2);
|
|
27
|
+
});
|
|
28
|
+
it("getReference returns path + count info", () => {
|
|
29
|
+
store = new ContextStore();
|
|
30
|
+
const ctx = store.openTask("Analysis");
|
|
31
|
+
store.appendFinding(ctx.taskId, "Data", "some findings");
|
|
32
|
+
const ref = store.getReference(ctx.taskId);
|
|
33
|
+
expect(ref).toContain("context.md");
|
|
34
|
+
expect(ref).toContain("1 findings");
|
|
35
|
+
expect(ref).toContain(ctx.taskId);
|
|
36
|
+
});
|
|
37
|
+
it("getActiveTasks returns open tasks", () => {
|
|
38
|
+
store = new ContextStore();
|
|
39
|
+
const t1 = store.openTask("Task 1");
|
|
40
|
+
const t2 = store.openTask("Task 2");
|
|
41
|
+
const active = store.getActiveTasks();
|
|
42
|
+
expect(active.map(t => t.taskId)).toContain(t1.taskId);
|
|
43
|
+
expect(active.map(t => t.taskId)).toContain(t2.taskId);
|
|
44
|
+
});
|
|
45
|
+
it("closeTask removes from active tasks", () => {
|
|
46
|
+
store = new ContextStore();
|
|
47
|
+
const ctx = store.openTask("Temp task");
|
|
48
|
+
store.closeTask(ctx.taskId);
|
|
49
|
+
expect(store.getTask(ctx.taskId)).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
it("keepTask prevents deletion", () => {
|
|
52
|
+
store = new ContextStore();
|
|
53
|
+
const ctx = store.openTask("Important");
|
|
54
|
+
store.keepTask(ctx.taskId);
|
|
55
|
+
expect(store.getTask(ctx.taskId)?.keep).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it("readContextTool returns file contents", async () => {
|
|
58
|
+
store = new ContextStore();
|
|
59
|
+
const ctx = store.openTask("Read test");
|
|
60
|
+
store.appendFinding(ctx.taskId, "Finding", "ETH price is $3000");
|
|
61
|
+
const result = await store.readContextTool("", { taskId: ctx.taskId });
|
|
62
|
+
expect(result.content[0]?.text).toContain("ETH price is $3000");
|
|
63
|
+
});
|
|
64
|
+
it("caps individual findings at 3000 chars", () => {
|
|
65
|
+
store = new ContextStore();
|
|
66
|
+
const ctx = store.openTask("Big task");
|
|
67
|
+
const bigContent = "x".repeat(5000);
|
|
68
|
+
store.appendFinding(ctx.taskId, "Big finding", bigContent);
|
|
69
|
+
// Should not throw and should be capped
|
|
70
|
+
const result = store.getReference(ctx.taskId);
|
|
71
|
+
expect(result).toBeTruthy();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=ContextStore.test.js.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelRegistry tests — tier classification + temperature profiles.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { classifyModel } from "../models/ModelRegistry.js";
|
|
6
|
+
function makeModel(id, promptPrice = "0.000005") {
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
name: id,
|
|
10
|
+
created: Date.now(),
|
|
11
|
+
description: "",
|
|
12
|
+
context_length: 128_000,
|
|
13
|
+
architecture: { modality: "text", tokenizer: "cl100k", instruct_type: null },
|
|
14
|
+
pricing: { prompt: promptPrice, completion: "0.000015" },
|
|
15
|
+
top_provider: { context_length: 128_000, max_completion_tokens: 4096, is_moderated: false },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe("classifyModel() — 2025 models", () => {
|
|
19
|
+
// Orchestrator tier
|
|
20
|
+
it("classifies claude-opus-4.7 as orchestrator", () => {
|
|
21
|
+
expect(classifyModel(makeModel("anthropic/claude-opus-4.7"))).toBe("orchestrator");
|
|
22
|
+
});
|
|
23
|
+
it("classifies claude-opus-4.6 as orchestrator", () => {
|
|
24
|
+
expect(classifyModel(makeModel("anthropic/claude-opus-4.6"))).toBe("orchestrator");
|
|
25
|
+
});
|
|
26
|
+
it("classifies gpt-5.5 as orchestrator", () => {
|
|
27
|
+
expect(classifyModel(makeModel("openai/gpt-5.5"))).toBe("orchestrator");
|
|
28
|
+
});
|
|
29
|
+
it("classifies gpt-5.5-pro as orchestrator", () => {
|
|
30
|
+
expect(classifyModel(makeModel("openai/gpt-5.5-pro"))).toBe("orchestrator");
|
|
31
|
+
});
|
|
32
|
+
it("classifies gemini-3.1-pro as orchestrator", () => {
|
|
33
|
+
expect(classifyModel(makeModel("google/gemini-3.1-pro-preview"))).toBe("orchestrator");
|
|
34
|
+
});
|
|
35
|
+
it("classifies deepseek-v4-pro as orchestrator", () => {
|
|
36
|
+
expect(classifyModel(makeModel("deepseek/deepseek-v4-pro"))).toBe("orchestrator");
|
|
37
|
+
});
|
|
38
|
+
it("classifies grok-4.0 as orchestrator", () => {
|
|
39
|
+
expect(classifyModel(makeModel("x-ai/grok-4.0"))).toBe("orchestrator");
|
|
40
|
+
});
|
|
41
|
+
// Analyst tier
|
|
42
|
+
it("classifies claude-sonnet-4.6 as analyst", () => {
|
|
43
|
+
expect(classifyModel(makeModel("anthropic/claude-sonnet-4.6"))).toBe("analyst");
|
|
44
|
+
});
|
|
45
|
+
it("classifies claude-sonnet-4.5 as analyst", () => {
|
|
46
|
+
expect(classifyModel(makeModel("anthropic/claude-sonnet-4.5"))).toBe("analyst");
|
|
47
|
+
});
|
|
48
|
+
it("classifies gemini-3.5-flash as analyst", () => {
|
|
49
|
+
expect(classifyModel(makeModel("google/gemini-3.5-flash"))).toBe("analyst");
|
|
50
|
+
});
|
|
51
|
+
it("classifies deepseek-v4 (non-pro) as analyst", () => {
|
|
52
|
+
expect(classifyModel(makeModel("deepseek/deepseek-v4-flash"))).toBe("analyst");
|
|
53
|
+
});
|
|
54
|
+
// Free tier
|
|
55
|
+
it("classifies :free models as free", () => {
|
|
56
|
+
expect(classifyModel(makeModel("deepseek/deepseek-v4-flash:free", "0"))).toBe("free");
|
|
57
|
+
});
|
|
58
|
+
it("classifies free-priced models as free", () => {
|
|
59
|
+
expect(classifyModel(makeModel("meta-llama/llama-3.1-8b-instruct:free", "0"))).toBe("free");
|
|
60
|
+
});
|
|
61
|
+
// Worker fallback
|
|
62
|
+
it("classifies unknown model as worker", () => {
|
|
63
|
+
expect(classifyModel(makeModel("some-unknown/model-v1"))).toBe("worker");
|
|
64
|
+
});
|
|
65
|
+
it("classifies small llama as worker", () => {
|
|
66
|
+
expect(classifyModel(makeModel("meta-llama/llama-3.1-8b-instruct", "0.0002"))).toBe("worker");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=ModelRegistry.test.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionManager tests — atomic compaction, context pressure, semantic anchors.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
5
|
+
import { SessionManager } from "../session/SessionManager.js";
|
|
6
|
+
function makeUserTurn(userContent, assistantContent) {
|
|
7
|
+
return [
|
|
8
|
+
{ role: "user", content: userContent },
|
|
9
|
+
{ role: "assistant", content: assistantContent },
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
function makeToolTurn(userContent, toolContent) {
|
|
13
|
+
const tc_id = `call_${Math.random().toString(36).slice(2)}`;
|
|
14
|
+
return [
|
|
15
|
+
{ role: "user", content: userContent },
|
|
16
|
+
{ role: "assistant", content: null, tool_calls: [{ id: tc_id, type: "function", function: { name: "get_prices", arguments: "{}" } }] },
|
|
17
|
+
{ role: "tool", content: toolContent, name: "get_prices", tool_call_id: tc_id },
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
describe("SessionManager", () => {
|
|
21
|
+
let session;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
session = new SessionManager();
|
|
24
|
+
session.setSystemPrompt("You are JellyOS.");
|
|
25
|
+
});
|
|
26
|
+
it("getMessages() prepends system prompt", () => {
|
|
27
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
28
|
+
const msgs = session.getMessages();
|
|
29
|
+
expect(msgs[0]?.role).toBe("system");
|
|
30
|
+
expect(msgs[0]?.content).toContain("JellyOS");
|
|
31
|
+
expect(msgs.length).toBe(2);
|
|
32
|
+
});
|
|
33
|
+
it("charCount() counts history chars", () => {
|
|
34
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
35
|
+
expect(session.charCount()).toBe(5);
|
|
36
|
+
});
|
|
37
|
+
it("getContextPressure() returns green for small history", () => {
|
|
38
|
+
session.addMessage({ role: "user", content: "hi" });
|
|
39
|
+
const p = session.getContextPressure();
|
|
40
|
+
expect(p.level).toBe("green");
|
|
41
|
+
expect(p.turboReady).toBe(true);
|
|
42
|
+
expect(p.pct).toBeLessThan(50);
|
|
43
|
+
});
|
|
44
|
+
it("getContextPressure() reports yellow at 60%", () => {
|
|
45
|
+
// Add ~48KB of content (60% of 80KB)
|
|
46
|
+
const bigMsg = "x".repeat(48_000);
|
|
47
|
+
session.addMessage({ role: "user", content: bigMsg });
|
|
48
|
+
const p = session.getContextPressure();
|
|
49
|
+
expect(p.pct).toBeGreaterThanOrEqual(50);
|
|
50
|
+
});
|
|
51
|
+
it("turboReady is false when context >70%", () => {
|
|
52
|
+
const bigMsg = "x".repeat(60_000);
|
|
53
|
+
session.addMessage({ role: "user", content: bigMsg });
|
|
54
|
+
const p = session.getContextPressure();
|
|
55
|
+
expect(p.turboReady).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
it("compaction never orphans tool_call_id pairs", () => {
|
|
58
|
+
// Add enough turns to trigger compaction
|
|
59
|
+
for (let i = 0; i < 20; i++) {
|
|
60
|
+
for (const msg of makeToolTurn(`question ${i}`, `tool result ${i} — ${"data".repeat(200)}`)) {
|
|
61
|
+
session.addMessage(msg);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const history = session.getHistory();
|
|
65
|
+
// Find all assistant messages with tool_calls
|
|
66
|
+
const toolCallMsgs = history.filter(m => m.tool_calls && m.tool_calls.length > 0);
|
|
67
|
+
for (const tcMsg of toolCallMsgs) {
|
|
68
|
+
for (const tc of tcMsg.tool_calls) {
|
|
69
|
+
// Every tool_call_id must have a corresponding tool result
|
|
70
|
+
const hasResult = history.some(m => m.role === "tool" && m.tool_call_id === tc.id);
|
|
71
|
+
expect(hasResult).toBe(true);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
it("forceCompact() reduces history", () => {
|
|
76
|
+
for (let i = 0; i < 30; i++) {
|
|
77
|
+
for (const msg of makeUserTurn(`q${i}`, `a${i}`)) {
|
|
78
|
+
session.addMessage(msg);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const before = session.getHistory().length;
|
|
82
|
+
session.forceCompact();
|
|
83
|
+
const after = session.getHistory().length;
|
|
84
|
+
expect(after).toBeLessThanOrEqual(before);
|
|
85
|
+
});
|
|
86
|
+
it("clear() empties history", () => {
|
|
87
|
+
session.addMessage({ role: "user", content: "hello" });
|
|
88
|
+
session.clear();
|
|
89
|
+
expect(session.getHistory().length).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("SessionManager — SwarmRouter integration", () => {
|
|
93
|
+
it("scoreComplexity returns low score for simple price check", async () => {
|
|
94
|
+
const { scoreComplexity } = await import("../runner/SwarmRouter.js");
|
|
95
|
+
expect(scoreComplexity("what is the price of ETH")).toBeLessThan(40);
|
|
96
|
+
});
|
|
97
|
+
it("scoreComplexity returns high score for complex multi-chain analysis", async () => {
|
|
98
|
+
const { scoreComplexity } = await import("../runner/SwarmRouter.js");
|
|
99
|
+
const score = scoreComplexity("analyze ETH and BTC then compare their RSI and predict which will pump first");
|
|
100
|
+
expect(score).toBeGreaterThanOrEqual(40);
|
|
101
|
+
});
|
|
102
|
+
it("decomposeHeuristic splits on conjunctions", async () => {
|
|
103
|
+
const { decomposeHeuristic } = await import("../runner/SwarmRouter.js");
|
|
104
|
+
const tasks = decomposeHeuristic("analyze ETH and check BTC mempool and get SOL TPS", 5);
|
|
105
|
+
expect(tasks.length).toBeGreaterThanOrEqual(2);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=SessionManager.test.js.map
|