@jellyos/agent 0.1.4 → 0.1.6
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.npm.md +212 -0
- package/bin/jellyos-mcp +26 -0
- package/dist/api/ExtensionAPI.d.ts +11 -0
- package/dist/cli.js +127 -49
- package/dist/index.d.ts +15 -2
- package/dist/index.js +13 -3
- package/dist/loader.d.ts +2 -9
- package/dist/loader.js +2 -1
- 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/ModelRegistry.d.ts +12 -1
- package/dist/models/ModelRegistry.js +105 -9
- package/dist/runner/AgentRunner.d.ts +19 -2
- package/dist/runner/AgentRunner.js +247 -17
- package/dist/runner/ModelClient.d.ts +10 -1
- package/dist/runner/ModelClient.js +79 -6
- package/dist/runner/SwarmRouter.d.ts +6 -6
- package/dist/runner/SwarmRouter.js +73 -24
- 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/tools/MarketSentiment.d.ts +166 -0
- package/dist/tools/MarketSentiment.js +209 -0
- package/dist/tools/NewsSentiment.js +40 -13
- package/dist/tools/PriceFeed.d.ts +2 -0
- package/dist/tools/PriceFeed.js +79 -27
- package/dist/tools/TechnicalAnalysis.d.ts +37 -0
- package/dist/tools/TechnicalAnalysis.js +85 -0
- package/dist/tui/App.d.ts +4 -3
- package/dist/tui/App.js +346 -119
- package/dist/tui/ModelSelector.d.ts +22 -0
- package/dist/tui/ModelSelector.js +86 -0
- package/dist/tui/REPL.d.ts +2 -1
- package/dist/tui/REPL.js +11 -6
- package/package.json +10 -6
- 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/models/CostTracker.d.ts.map +0 -1
- package/dist/models/CostTracker.js.map +0 -1
- package/dist/models/ModelRegistry.d.ts.map +0 -1
- package/dist/models/ModelRegistry.js.map +0 -1
- package/dist/models/index.d.ts.map +0 -1
- package/dist/models/index.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/tools/NewsSentiment.d.ts.map +0 -1
- package/dist/tools/NewsSentiment.js.map +0 -1
- package/dist/tools/PriceFeed.d.ts.map +0 -1
- package/dist/tools/PriceFeed.js.map +0 -1
- package/dist/tools/TechnicalAnalysis.d.ts.map +0 -1
- package/dist/tools/TechnicalAnalysis.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryStore — persistent long-term memory using Node 22+ built-in SQLite.
|
|
3
|
+
* (#7 — cross-session memory)
|
|
4
|
+
*
|
|
5
|
+
* Stores all conversation messages, searchable by keyword and session.
|
|
6
|
+
* Injected into system prompt at session_start to give the agent awareness
|
|
7
|
+
* of past decisions, prices seen, strategies discussed.
|
|
8
|
+
*
|
|
9
|
+
* Uses node:sqlite (experimental in Node 22, stable in Node 24) — zero deps.
|
|
10
|
+
*/
|
|
11
|
+
import { mkdirSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
|
|
15
|
+
export class MemoryStore {
|
|
16
|
+
db = null;
|
|
17
|
+
available = false;
|
|
18
|
+
constructor() {
|
|
19
|
+
this.init();
|
|
20
|
+
}
|
|
21
|
+
init() {
|
|
22
|
+
try {
|
|
23
|
+
// node:sqlite is available in Node 22+ (experimental) and Node 24 (stable)
|
|
24
|
+
// We import it dynamically to avoid a hard crash on older Node versions
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
26
|
+
const sqlite = require("node:sqlite");
|
|
27
|
+
mkdirSync(JELLY_HOME, { recursive: true });
|
|
28
|
+
this.db = new sqlite.DatabaseSync(join(JELLY_HOME, "memory.db"));
|
|
29
|
+
this.db.exec(`
|
|
30
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
sessionId TEXT NOT NULL,
|
|
33
|
+
role TEXT NOT NULL,
|
|
34
|
+
content TEXT NOT NULL,
|
|
35
|
+
tokens INTEGER DEFAULT 0,
|
|
36
|
+
ts INTEGER NOT NULL,
|
|
37
|
+
tags TEXT DEFAULT '[]'
|
|
38
|
+
);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_session ON messages(sessionId);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_ts ON messages(ts);
|
|
41
|
+
-- Prune old entries automatically (keep last 10k messages)
|
|
42
|
+
CREATE TRIGGER IF NOT EXISTS prune_old_messages
|
|
43
|
+
AFTER INSERT ON messages
|
|
44
|
+
WHEN (SELECT COUNT(*) FROM messages) > 10000
|
|
45
|
+
BEGIN
|
|
46
|
+
DELETE FROM messages WHERE id IN (
|
|
47
|
+
SELECT id FROM messages ORDER BY ts ASC LIMIT 500
|
|
48
|
+
);
|
|
49
|
+
END;
|
|
50
|
+
`);
|
|
51
|
+
this.available = true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// SQLite not available (Node <22) — memory store degrades gracefully
|
|
55
|
+
this.available = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
get isAvailable() { return this.available; }
|
|
59
|
+
// ── Write ──────────────────────────────────────────────────────────────────
|
|
60
|
+
save(sessionId, role, content, tags = []) {
|
|
61
|
+
if (!this.available)
|
|
62
|
+
return;
|
|
63
|
+
try {
|
|
64
|
+
const tokens = Math.ceil(content.length / 4);
|
|
65
|
+
// Cap content at 2KB per entry to keep DB lean
|
|
66
|
+
const capped = content.length > 2000 ? content.slice(0, 2000) + "…" : content;
|
|
67
|
+
this.db.prepare(`INSERT INTO messages (sessionId, role, content, tokens, ts, tags)
|
|
68
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, role, capped, tokens, Date.now(), JSON.stringify(tags));
|
|
69
|
+
}
|
|
70
|
+
catch { /* best effort */ }
|
|
71
|
+
}
|
|
72
|
+
// ── Read ───────────────────────────────────────────────────────────────────
|
|
73
|
+
/** Keyword search across all sessions */
|
|
74
|
+
search(query, limit = 8) {
|
|
75
|
+
if (!this.available)
|
|
76
|
+
return [];
|
|
77
|
+
try {
|
|
78
|
+
const rows = this.db.prepare(`SELECT * FROM messages WHERE content LIKE ? ORDER BY ts DESC LIMIT ?`).all(`%${query}%`, limit);
|
|
79
|
+
return rows.map((r) => ({
|
|
80
|
+
...r,
|
|
81
|
+
tags: JSON.parse(r["tags"] ?? "[]"),
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Get summaries of recent sessions (for system prompt injection) */
|
|
89
|
+
getRecentSessions(limit = 5) {
|
|
90
|
+
if (!this.available)
|
|
91
|
+
return [];
|
|
92
|
+
try {
|
|
93
|
+
const rows = this.db.prepare(`
|
|
94
|
+
SELECT
|
|
95
|
+
sessionId,
|
|
96
|
+
COUNT(*) as msgCount,
|
|
97
|
+
MAX(ts) as ts,
|
|
98
|
+
GROUP_CONCAT(
|
|
99
|
+
CASE WHEN role = 'user' THEN SUBSTR(content, 1, 120)
|
|
100
|
+
WHEN role = 'assistant' THEN SUBSTR(content, 1, 120)
|
|
101
|
+
END,
|
|
102
|
+
' | '
|
|
103
|
+
) as summary
|
|
104
|
+
FROM messages
|
|
105
|
+
GROUP BY sessionId
|
|
106
|
+
ORDER BY ts DESC
|
|
107
|
+
LIMIT ?
|
|
108
|
+
`).all(limit);
|
|
109
|
+
return rows.map((r) => ({
|
|
110
|
+
sessionId: r["sessionId"],
|
|
111
|
+
summary: (r["summary"] ?? "").slice(0, 400),
|
|
112
|
+
msgCount: r["msgCount"],
|
|
113
|
+
ts: r["ts"],
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Get all messages for a specific session */
|
|
121
|
+
getSession(sessionId) {
|
|
122
|
+
if (!this.available)
|
|
123
|
+
return [];
|
|
124
|
+
try {
|
|
125
|
+
const rows = this.db.prepare(`SELECT * FROM messages WHERE sessionId = ? ORDER BY ts ASC`).all(sessionId);
|
|
126
|
+
return rows.map((r) => ({
|
|
127
|
+
...r,
|
|
128
|
+
tags: JSON.parse(r["tags"] ?? "[]"),
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Build a short memory context block for system prompt injection */
|
|
136
|
+
buildContextBlock(currentSessionId) {
|
|
137
|
+
if (!this.available)
|
|
138
|
+
return "";
|
|
139
|
+
const recent = this.getRecentSessions(3).filter(s => s.sessionId !== currentSessionId);
|
|
140
|
+
if (recent.length === 0)
|
|
141
|
+
return "";
|
|
142
|
+
const lines = ["\n## Memory from Past Sessions"];
|
|
143
|
+
for (const s of recent) {
|
|
144
|
+
const ago = Math.round((Date.now() - s.ts) / 60_000);
|
|
145
|
+
const agoStr = ago < 60 ? `${ago}m ago` : ago < 1440 ? `${Math.round(ago / 60)}h ago` : `${Math.round(ago / 1440)}d ago`;
|
|
146
|
+
lines.push(`- [${agoStr}] ${s.summary.slice(0, 200)}`);
|
|
147
|
+
}
|
|
148
|
+
lines.push("");
|
|
149
|
+
return lines.join("\n");
|
|
150
|
+
}
|
|
151
|
+
getStats() {
|
|
152
|
+
if (!this.available)
|
|
153
|
+
return { totalMessages: 0, totalSessions: 0 };
|
|
154
|
+
try {
|
|
155
|
+
const { count } = this.db.prepare(`SELECT COUNT(*) as count FROM messages`).get();
|
|
156
|
+
const { sessions } = this.db.prepare(`SELECT COUNT(DISTINCT sessionId) as sessions FROM messages`).get();
|
|
157
|
+
return { totalMessages: count, totalSessions: sessions };
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return { totalMessages: 0, totalSessions: 0 };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Singleton */
|
|
165
|
+
export const memoryStore = new MemoryStore();
|
|
166
|
+
//# sourceMappingURL=MemoryStore.js.map
|
|
@@ -1,10 +1,22 @@
|
|
|
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
|
import type { Message } from "../runner/ModelClient.js";
|
|
13
|
+
/** Context pressure levels for UI display and swarm gating */
|
|
14
|
+
export interface ContextPressure {
|
|
15
|
+
chars: number;
|
|
16
|
+
pct: number;
|
|
17
|
+
level: "green" | "yellow" | "red" | "critical";
|
|
18
|
+
turboReady: boolean;
|
|
19
|
+
}
|
|
8
20
|
export declare class SessionManager {
|
|
9
21
|
private history;
|
|
10
22
|
private systemPrompt;
|
|
@@ -14,10 +26,39 @@ export declare class SessionManager {
|
|
|
14
26
|
addMessages(msgs: Message[]): void;
|
|
15
27
|
/** Returns messages ready to send to the model — system prompt prepended */
|
|
16
28
|
getMessages(): Message[];
|
|
17
|
-
/** Full raw history (no system) */
|
|
29
|
+
/** Full raw history (no system prompt) */
|
|
18
30
|
getHistory(): Message[];
|
|
19
31
|
clear(): void;
|
|
32
|
+
charCount(): number;
|
|
33
|
+
getContextPressure(): ContextPressure;
|
|
20
34
|
private buildSystemContent;
|
|
21
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Split history into atomic turns.
|
|
37
|
+
* A turn = [user_msg, assistant_msg, ...tool_result_msgs]
|
|
38
|
+
* Keeping turns atomic prevents orphaned tool_call_id errors.
|
|
39
|
+
*/
|
|
40
|
+
private splitIntoTurns;
|
|
41
|
+
/**
|
|
42
|
+
* Tier 1: Trim long tool results in-place. No messages dropped.
|
|
43
|
+
* Replaces content >TOOL_RESULT_CAP chars with a 1-line summary.
|
|
44
|
+
*/
|
|
45
|
+
private trimToolResults;
|
|
46
|
+
/**
|
|
47
|
+
* Tier 2: Drop oldest turns, but ANCHOR high-value messages (prices, signals,
|
|
48
|
+
* decisions) so they survive compaction. (#36 semantic anchor compaction)
|
|
49
|
+
*/
|
|
50
|
+
private dropOldTurns;
|
|
51
|
+
/** #36: Score a message's semantic importance (0-10) */
|
|
52
|
+
private messageWeight;
|
|
53
|
+
maybeCompact(): void;
|
|
54
|
+
/** Force compaction */
|
|
55
|
+
forceCompact(): void;
|
|
56
|
+
/**
|
|
57
|
+
* #32: Tier-2 summarization via a cheap model.
|
|
58
|
+
* Compresses the oldest 40% of turns into a single summary message.
|
|
59
|
+
* Call this when pct is between 70-90 to reclaim space gracefully.
|
|
60
|
+
* @param summarizeCallback — provided by AgentRunner which has model access
|
|
61
|
+
*/
|
|
62
|
+
summarizeOldTurns(summarizeCallback: (messages: Message[]) => Promise<string>): Promise<void>;
|
|
22
63
|
}
|
|
23
64
|
//# sourceMappingURL=SessionManager.d.ts.map
|
|
@@ -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
|