@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.
Files changed (90) hide show
  1. package/README.md +9 -9
  2. package/README.npm.md +212 -0
  3. package/bin/jellyos-mcp +26 -0
  4. package/dist/api/ExtensionAPI.d.ts +6 -0
  5. package/dist/api/Registry.js +3 -1
  6. package/dist/cli.js +117 -42
  7. package/dist/index.d.ts +24 -1
  8. package/dist/index.js +19 -2
  9. package/dist/mcp/entry.d.ts +2 -0
  10. package/dist/mcp/entry.js +71 -0
  11. package/dist/mcp/server.d.ts +31 -0
  12. package/dist/mcp/server.js +128 -0
  13. package/dist/models/CostTracker.d.ts +66 -0
  14. package/dist/models/CostTracker.js +148 -0
  15. package/dist/models/ModelRegistry.d.ts +157 -0
  16. package/dist/models/ModelRegistry.js +496 -0
  17. package/dist/models/index.d.ts +5 -0
  18. package/dist/models/index.js +3 -0
  19. package/dist/runner/AgentRunner.d.ts +23 -2
  20. package/dist/runner/AgentRunner.js +264 -24
  21. package/dist/runner/ModelClient.d.ts +26 -6
  22. package/dist/runner/ModelClient.js +147 -28
  23. package/dist/runner/SwarmRouter.d.ts +10 -7
  24. package/dist/runner/SwarmRouter.js +85 -28
  25. package/dist/runner/ToolDispatcher.d.ts +10 -0
  26. package/dist/runner/ToolDispatcher.js +106 -2
  27. package/dist/scheduler/AgentScheduler.d.ts +118 -0
  28. package/dist/scheduler/AgentScheduler.js +253 -0
  29. package/dist/session/ContextStore.d.ts +96 -0
  30. package/dist/session/ContextStore.js +207 -0
  31. package/dist/session/GoalManager.d.ts +101 -0
  32. package/dist/session/GoalManager.js +167 -0
  33. package/dist/session/MemoryStore.d.ts +48 -0
  34. package/dist/session/MemoryStore.js +166 -0
  35. package/dist/session/SessionManager.d.ts +45 -4
  36. package/dist/session/SessionManager.js +151 -8
  37. package/dist/telemetry/Tracer.d.ts +48 -0
  38. package/dist/telemetry/Tracer.js +102 -0
  39. package/dist/tests/ContextStore.test.d.ts +2 -0
  40. package/dist/tests/ContextStore.test.js +74 -0
  41. package/dist/tests/ModelRegistry.test.d.ts +2 -0
  42. package/dist/tests/ModelRegistry.test.js +69 -0
  43. package/dist/tests/SessionManager.test.d.ts +2 -0
  44. package/dist/tests/SessionManager.test.js +108 -0
  45. package/dist/tests/TechnicalAnalysis.test.d.ts +2 -0
  46. package/dist/tests/TechnicalAnalysis.test.js +109 -0
  47. package/dist/tools/MarketSentiment.d.ts +166 -0
  48. package/dist/tools/MarketSentiment.js +209 -0
  49. package/dist/tools/NewsSentiment.d.ts +67 -0
  50. package/dist/tools/NewsSentiment.js +226 -0
  51. package/dist/tools/PriceFeed.d.ts +105 -0
  52. package/dist/tools/PriceFeed.js +282 -0
  53. package/dist/tools/TechnicalAnalysis.d.ts +110 -0
  54. package/dist/tools/TechnicalAnalysis.js +357 -0
  55. package/dist/tools/index.d.ts +7 -0
  56. package/dist/tools/index.js +4 -0
  57. package/dist/tui/App.d.ts +7 -5
  58. package/dist/tui/App.js +350 -65
  59. package/dist/tui/REPL.d.ts +2 -1
  60. package/dist/tui/REPL.js +11 -6
  61. package/dist/tui/StatusBar.js +1 -1
  62. package/package.json +9 -4
  63. package/dist/api/ExtensionAPI.d.ts.map +0 -1
  64. package/dist/api/ExtensionAPI.js.map +0 -1
  65. package/dist/api/Registry.d.ts.map +0 -1
  66. package/dist/api/Registry.js.map +0 -1
  67. package/dist/cli.d.ts.map +0 -1
  68. package/dist/cli.js.map +0 -1
  69. package/dist/index.d.ts.map +0 -1
  70. package/dist/index.js.map +0 -1
  71. package/dist/loader.d.ts.map +0 -1
  72. package/dist/loader.js.map +0 -1
  73. package/dist/runner/AgentRunner.d.ts.map +0 -1
  74. package/dist/runner/AgentRunner.js.map +0 -1
  75. package/dist/runner/ModelClient.d.ts.map +0 -1
  76. package/dist/runner/ModelClient.js.map +0 -1
  77. package/dist/runner/SwarmRouter.d.ts.map +0 -1
  78. package/dist/runner/SwarmRouter.js.map +0 -1
  79. package/dist/runner/ToolDispatcher.d.ts.map +0 -1
  80. package/dist/runner/ToolDispatcher.js.map +0 -1
  81. package/dist/session/SessionManager.d.ts.map +0 -1
  82. package/dist/session/SessionManager.js.map +0 -1
  83. package/dist/tui/App.d.ts.map +0 -1
  84. package/dist/tui/App.js.map +0 -1
  85. package/dist/tui/REPL.d.ts.map +0 -1
  86. package/dist/tui/REPL.js.map +0 -1
  87. package/dist/tui/StatusBar.d.ts.map +0 -1
  88. package/dist/tui/StatusBar.js.map +0 -1
  89. package/dist/tui/theme.d.ts.map +0 -1
  90. package/dist/tui/theme.js.map +0 -1
@@ -0,0 +1,118 @@
1
+ /**
2
+ * AgentScheduler — autonomous task scheduling. (#11)
3
+ *
4
+ * Enables the agent to act without user input:
5
+ * - Cron-style recurring tasks ("every 15 minutes, check BTC funding rates")
6
+ * - Price triggers ("when ETH drops below $2000, alert me")
7
+ * - One-shot future tasks ("in 30 minutes, summarize market conditions")
8
+ *
9
+ * Tasks are persisted to ~/.jelly/schedule.json and survive restarts.
10
+ * The scheduler polls every 60s and fires tasks via the provided callback.
11
+ *
12
+ * Usage:
13
+ * scheduler.addTask({ name: "BTC check", prompt: "check BTC price and RSI", cron: "@every_15m" });
14
+ * scheduler.start((task) => runner.run(task.prompt));
15
+ */
16
+ import { type Static } from "@sinclair/typebox";
17
+ export interface PriceTrigger {
18
+ symbol: string;
19
+ above?: number;
20
+ below?: number;
21
+ /** Percent change threshold (e.g. 5 = 5% move either direction) */
22
+ changePct?: number;
23
+ }
24
+ export interface ScheduledTask {
25
+ id: string;
26
+ name: string;
27
+ prompt: string;
28
+ /** Cron expression e.g. "@every_15m" or "0 * * * *" (hourly).
29
+ * Shorthand: @every_5m @every_15m @every_30m @every_1h @every_6h @hourly @daily
30
+ */
31
+ cron?: string;
32
+ /** Price-based trigger */
33
+ trigger?: PriceTrigger;
34
+ /** One-shot run time (epoch ms) */
35
+ runAt?: number;
36
+ /** If true, disable after first run */
37
+ runOnce: boolean;
38
+ enabled: boolean;
39
+ createdAt: number;
40
+ lastRun?: number;
41
+ runCount: number;
42
+ }
43
+ export declare class AgentScheduler {
44
+ private tasks;
45
+ private timer?;
46
+ private lastTickMinute;
47
+ constructor();
48
+ private load;
49
+ private save;
50
+ start(onTrigger: (task: ScheduledTask) => void): void;
51
+ stop(): void;
52
+ private tick;
53
+ private checkPriceTrigger;
54
+ addTask(task: Omit<ScheduledTask, "id" | "createdAt" | "runCount">): ScheduledTask;
55
+ removeTask(id: string): boolean;
56
+ enableTask(id: string, enabled: boolean): boolean;
57
+ listTasks(): ScheduledTask[];
58
+ getTask(id: string): ScheduledTask | undefined;
59
+ readonly addTaskParams: import("@sinclair/typebox").TObject<{
60
+ name: import("@sinclair/typebox").TString;
61
+ prompt: import("@sinclair/typebox").TString;
62
+ cron: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
63
+ trigger_symbol: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
64
+ trigger_above: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
65
+ trigger_below: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
66
+ trigger_change_pct: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
67
+ run_in_minutes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
68
+ run_once: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
69
+ }>;
70
+ addTaskTool(_id: string, params: Static<typeof this.addTaskParams>): Promise<{
71
+ content: {
72
+ type: "text";
73
+ text: string;
74
+ }[];
75
+ details: {
76
+ taskId: string;
77
+ task: ScheduledTask;
78
+ };
79
+ }>;
80
+ readonly listTasksParams: import("@sinclair/typebox").TObject<{
81
+ enabled_only: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
82
+ }>;
83
+ listTasksTool(_id: string, params: Static<typeof this.listTasksParams>): Promise<{
84
+ content: {
85
+ type: "text";
86
+ text: string;
87
+ }[];
88
+ details: {
89
+ count?: undefined;
90
+ tasks?: undefined;
91
+ };
92
+ } | {
93
+ content: {
94
+ type: "text";
95
+ text: string;
96
+ }[];
97
+ details: {
98
+ count: number;
99
+ tasks: ScheduledTask[];
100
+ };
101
+ }>;
102
+ readonly removeTaskParams: import("@sinclair/typebox").TObject<{
103
+ id: import("@sinclair/typebox").TString;
104
+ }>;
105
+ removeTaskTool(_id: string, params: Static<typeof this.removeTaskParams>): Promise<{
106
+ content: {
107
+ type: "text";
108
+ text: string;
109
+ }[];
110
+ details: {
111
+ taskId: string;
112
+ success: boolean;
113
+ };
114
+ }>;
115
+ }
116
+ /** Singleton */
117
+ export declare const agentScheduler: AgentScheduler;
118
+ //# sourceMappingURL=AgentScheduler.d.ts.map
@@ -0,0 +1,253 @@
1
+ /**
2
+ * AgentScheduler — autonomous task scheduling. (#11)
3
+ *
4
+ * Enables the agent to act without user input:
5
+ * - Cron-style recurring tasks ("every 15 minutes, check BTC funding rates")
6
+ * - Price triggers ("when ETH drops below $2000, alert me")
7
+ * - One-shot future tasks ("in 30 minutes, summarize market conditions")
8
+ *
9
+ * Tasks are persisted to ~/.jelly/schedule.json and survive restarts.
10
+ * The scheduler polls every 60s and fires tasks via the provided callback.
11
+ *
12
+ * Usage:
13
+ * scheduler.addTask({ name: "BTC check", prompt: "check BTC price and RSI", cron: "@every_15m" });
14
+ * scheduler.start((task) => runner.run(task.prompt));
15
+ */
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { homedir } from "node:os";
19
+ import { randomUUID } from "node:crypto";
20
+ import { priceFeed } from "../tools/PriceFeed.js";
21
+ import { Type } from "@sinclair/typebox";
22
+ const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
23
+ const SCHEDULE_FILE = join(JELLY_HOME, "schedule.json");
24
+ // ── Cron parser (minimal subset) ──────────────────────────────────────────────
25
+ const SHORTHAND_MAP = {
26
+ "@hourly": "0 * * * *",
27
+ "@daily": "0 0 * * *",
28
+ "@every_5m": "*/5 * * * *",
29
+ "@every_15m": "*/15 * * * *",
30
+ "@every_30m": "*/30 * * * *",
31
+ "@every_1h": "0 * * * *",
32
+ "@every_6h": "0 */6 * * *",
33
+ "@every_12h": "0 */12 * * *",
34
+ };
35
+ function parseCron(expr) {
36
+ const resolved = SHORTHAND_MAP[expr] ?? expr;
37
+ const parts = resolved.trim().split(/\s+/);
38
+ if (parts.length < 5)
39
+ return () => false;
40
+ const [minuteExpr, hourExpr] = parts;
41
+ function matchField(expr, value) {
42
+ if (expr === "*")
43
+ return true;
44
+ if (expr.startsWith("*/")) {
45
+ const step = parseInt(expr.slice(2));
46
+ return !isNaN(step) && value % step === 0;
47
+ }
48
+ const num = parseInt(expr);
49
+ return !isNaN(num) && num === value;
50
+ }
51
+ return (now) => matchField(minuteExpr, now.getMinutes()) &&
52
+ matchField(hourExpr, now.getHours());
53
+ }
54
+ // ── AgentScheduler ─────────────────────────────────────────────────────────────
55
+ export class AgentScheduler {
56
+ tasks = [];
57
+ timer;
58
+ lastTickMinute = -1;
59
+ constructor() {
60
+ this.load();
61
+ }
62
+ // ── Persistence ────────────────────────────────────────────────────────────
63
+ load() {
64
+ try {
65
+ if (!existsSync(SCHEDULE_FILE))
66
+ return;
67
+ const raw = JSON.parse(readFileSync(SCHEDULE_FILE, "utf-8"));
68
+ if (Array.isArray(raw))
69
+ this.tasks = raw;
70
+ }
71
+ catch { /* start fresh */ }
72
+ }
73
+ save() {
74
+ try {
75
+ mkdirSync(JELLY_HOME, { recursive: true });
76
+ writeFileSync(SCHEDULE_FILE, JSON.stringify(this.tasks, null, 2), "utf-8");
77
+ }
78
+ catch { /* best effort */ }
79
+ }
80
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
81
+ start(onTrigger) {
82
+ if (this.timer)
83
+ return;
84
+ // Poll every 60 seconds — aligned to minute boundaries
85
+ this.timer = setInterval(() => this.tick(onTrigger), 60_000);
86
+ // Run once immediately on start to catch any missed tasks
87
+ this.tick(onTrigger);
88
+ }
89
+ stop() {
90
+ if (this.timer) {
91
+ clearInterval(this.timer);
92
+ this.timer = undefined;
93
+ }
94
+ }
95
+ tick(onTrigger) {
96
+ const now = new Date();
97
+ const minute = now.getMinutes();
98
+ // Prevent double-firing within the same minute
99
+ if (minute === this.lastTickMinute)
100
+ return;
101
+ this.lastTickMinute = minute;
102
+ for (const task of this.tasks) {
103
+ if (!task.enabled)
104
+ continue;
105
+ let shouldRun = false;
106
+ // One-shot: run at specific time
107
+ if (task.runAt && !task.lastRun) {
108
+ shouldRun = Date.now() >= task.runAt;
109
+ }
110
+ // Cron schedule
111
+ if (task.cron && !shouldRun) {
112
+ const matches = parseCron(task.cron);
113
+ shouldRun = matches(now);
114
+ // Prevent re-firing within same minute
115
+ if (shouldRun && task.lastRun && Date.now() - task.lastRun < 58_000) {
116
+ shouldRun = false;
117
+ }
118
+ }
119
+ // Price trigger
120
+ if (task.trigger && !shouldRun) {
121
+ shouldRun = this.checkPriceTrigger(task.trigger);
122
+ }
123
+ if (shouldRun) {
124
+ task.lastRun = Date.now();
125
+ task.runCount = (task.runCount ?? 0) + 1;
126
+ if (task.runOnce)
127
+ task.enabled = false;
128
+ this.save();
129
+ onTrigger(task);
130
+ }
131
+ }
132
+ }
133
+ checkPriceTrigger(trigger) {
134
+ const tick = priceFeed.get(trigger.symbol.toLowerCase());
135
+ if (!tick)
136
+ return false;
137
+ if (trigger.above !== undefined && tick.price >= trigger.above)
138
+ return true;
139
+ if (trigger.below !== undefined && tick.price <= trigger.below)
140
+ return true;
141
+ if (trigger.changePct !== undefined && Math.abs(tick.change24h) >= trigger.changePct)
142
+ return true;
143
+ return false;
144
+ }
145
+ // ── Task CRUD ──────────────────────────────────────────────────────────────
146
+ addTask(task) {
147
+ const full = {
148
+ ...task,
149
+ id: randomUUID().slice(0, 8),
150
+ createdAt: Date.now(),
151
+ runCount: 0,
152
+ };
153
+ this.tasks.push(full);
154
+ this.save();
155
+ return full;
156
+ }
157
+ removeTask(id) {
158
+ const before = this.tasks.length;
159
+ this.tasks = this.tasks.filter(t => t.id !== id);
160
+ if (this.tasks.length < before) {
161
+ this.save();
162
+ return true;
163
+ }
164
+ return false;
165
+ }
166
+ enableTask(id, enabled) {
167
+ const t = this.tasks.find(t => t.id === id);
168
+ if (!t)
169
+ return false;
170
+ t.enabled = enabled;
171
+ this.save();
172
+ return true;
173
+ }
174
+ listTasks() { return [...this.tasks]; }
175
+ getTask(id) {
176
+ return this.tasks.find(t => t.id === id);
177
+ }
178
+ // ── Tools ──────────────────────────────────────────────────────────────────
179
+ addTaskParams = Type.Object({
180
+ name: Type.String({ description: "Task name" }),
181
+ prompt: Type.String({ description: "The message the agent will run" }),
182
+ cron: Type.Optional(Type.String({
183
+ description: "Cron schedule: '*/15 * * * *' or shorthand @every_15m @hourly @daily",
184
+ })),
185
+ trigger_symbol: Type.Optional(Type.String({ description: "Symbol for price trigger e.g. BTC" })),
186
+ trigger_above: Type.Optional(Type.Number({ description: "Fire when price goes above this" })),
187
+ trigger_below: Type.Optional(Type.Number({ description: "Fire when price goes below this" })),
188
+ trigger_change_pct: Type.Optional(Type.Number({ description: "Fire when 24h change exceeds this %" })),
189
+ run_in_minutes: Type.Optional(Type.Number({ description: "One-shot: run after N minutes" })),
190
+ run_once: Type.Optional(Type.Boolean({ description: "Disable after first run (default: false)" })),
191
+ });
192
+ async addTaskTool(_id, params) {
193
+ let trigger;
194
+ if (params.trigger_symbol) {
195
+ trigger = {
196
+ symbol: params.trigger_symbol,
197
+ above: params.trigger_above,
198
+ below: params.trigger_below,
199
+ changePct: params.trigger_change_pct,
200
+ };
201
+ }
202
+ const task = this.addTask({
203
+ name: params.name,
204
+ prompt: params.prompt,
205
+ cron: params.cron,
206
+ trigger,
207
+ runAt: params.run_in_minutes ? Date.now() + params.run_in_minutes * 60_000 : undefined,
208
+ runOnce: params.run_once ?? false,
209
+ enabled: true,
210
+ });
211
+ const desc = [
212
+ params.cron ? `cron: ${params.cron}` : "",
213
+ trigger ? `trigger: ${params.trigger_symbol} ${trigger.above ? `>$${trigger.above}` : ""} ${trigger.below ? `<$${trigger.below}` : ""}` : "",
214
+ params.run_in_minutes ? `runs in ${params.run_in_minutes}m` : "",
215
+ ].filter(Boolean).join(", ");
216
+ return {
217
+ content: [{ type: "text", text: `Scheduled: [${task.id}] ${task.name}${desc ? ` (${desc})` : ""}` }],
218
+ details: { taskId: task.id, task },
219
+ };
220
+ }
221
+ listTasksParams = Type.Object({
222
+ enabled_only: Type.Optional(Type.Boolean({ description: "Only show enabled tasks (default: false)" })),
223
+ });
224
+ async listTasksTool(_id, params) {
225
+ const tasks = params.enabled_only ? this.tasks.filter(t => t.enabled) : this.tasks;
226
+ if (tasks.length === 0) {
227
+ return { content: [{ type: "text", text: "No scheduled tasks." }], details: {} };
228
+ }
229
+ const lines = tasks.map(t => {
230
+ const icon = t.enabled ? "🟢" : "⚪";
231
+ const schedule = t.cron ?? (t.trigger ? `price trigger ${t.trigger.symbol}` : t.runAt ? `at ${new Date(t.runAt).toLocaleTimeString()}` : "manual");
232
+ const runs = t.runCount > 0 ? ` (${t.runCount} runs)` : "";
233
+ return `${icon} [${t.id}] ${t.name} — ${schedule}${runs}`;
234
+ });
235
+ return {
236
+ content: [{ type: "text", text: `Scheduled tasks (${tasks.length}):\n${lines.join("\n")}` }],
237
+ details: { count: tasks.length, tasks },
238
+ };
239
+ }
240
+ removeTaskParams = Type.Object({
241
+ id: Type.String({ description: "Task ID to remove" }),
242
+ });
243
+ async removeTaskTool(_id, params) {
244
+ const ok = this.removeTask(params.id);
245
+ return {
246
+ content: [{ type: "text", text: ok ? `Task ${params.id} removed.` : `Task ${params.id} not found.` }],
247
+ details: { taskId: params.id, success: ok },
248
+ };
249
+ }
250
+ }
251
+ /** Singleton */
252
+ export const agentScheduler = new AgentScheduler();
253
+ //# sourceMappingURL=AgentScheduler.js.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ContextStore — Ephemeral task context folders. (#31, #39)
3
+ *
4
+ * When an agent task takes >2 tool rounds, a ~/.jelly/tasks/<id>/context.md
5
+ * file is created. Intermediate tool results are appended there instead of
6
+ * bloating the message history. The model gets a compact file reference
7
+ * instead of 10KB of raw JSON. The folder auto-deletes on task completion
8
+ * unless the user marked it /keep.
9
+ *
10
+ * This is the primary mechanism that allows turbo/max mode to stay within
11
+ * context budget even on complex multi-step research tasks.
12
+ */
13
+ export interface TaskContext {
14
+ taskId: string;
15
+ taskDir: string;
16
+ contextMd: string;
17
+ createdAt: number;
18
+ title: string;
19
+ keep: boolean;
20
+ findings: number;
21
+ }
22
+ export declare class ContextStore {
23
+ private activeTasks;
24
+ constructor();
25
+ /** Open a new ephemeral task folder. Returns the TaskContext. */
26
+ openTask(title: string, keep?: boolean): TaskContext;
27
+ /** Append a finding / tool result to the task's context.md */
28
+ appendFinding(taskId: string, section: string, content: string): void;
29
+ /**
30
+ * Get a compact reference string for injection into the model context.
31
+ * Returns something like:
32
+ * "[Task ctx: ~/.jelly/tasks/.../context.md — 4 findings, 3.2KB. Use read_task_context("abc123")]"
33
+ */
34
+ getReference(taskId: string): string;
35
+ /**
36
+ * Mark a task complete and optionally delete its folder.
37
+ * Deletion is deferred 5 seconds to allow model to read final state.
38
+ */
39
+ closeTask(taskId: string): void;
40
+ /** Permanently keep a task folder (user called /keep <taskId>) */
41
+ keepTask(taskId: string): boolean;
42
+ getActiveTasks(): TaskContext[];
43
+ getTask(taskId: string): TaskContext | undefined;
44
+ readContextTool(_id: string, { taskId }: {
45
+ taskId: string;
46
+ }): Promise<{
47
+ content: {
48
+ type: "text";
49
+ text: string;
50
+ }[];
51
+ details: {
52
+ taskId: string;
53
+ path: string;
54
+ sizeBytes: number;
55
+ status: string;
56
+ findings?: undefined;
57
+ };
58
+ } | {
59
+ content: {
60
+ type: "text";
61
+ text: string;
62
+ }[];
63
+ details: {
64
+ taskId?: undefined;
65
+ path?: undefined;
66
+ sizeBytes?: undefined;
67
+ status?: undefined;
68
+ findings?: undefined;
69
+ };
70
+ } | {
71
+ content: {
72
+ type: "text";
73
+ text: string;
74
+ }[];
75
+ details: {
76
+ taskId: string;
77
+ path: string;
78
+ sizeBytes: number;
79
+ findings: number;
80
+ status: string;
81
+ };
82
+ }>;
83
+ listTasksTool(): Promise<{
84
+ content: {
85
+ type: "text";
86
+ text: string;
87
+ }[];
88
+ details: {
89
+ activeTasks: number;
90
+ completedOnDisk: number;
91
+ };
92
+ }>;
93
+ }
94
+ /** Singleton — one store per process */
95
+ export declare const contextStore: ContextStore;
96
+ //# sourceMappingURL=ContextStore.d.ts.map
@@ -0,0 +1,207 @@
1
+ /**
2
+ * ContextStore — Ephemeral task context folders. (#31, #39)
3
+ *
4
+ * When an agent task takes >2 tool rounds, a ~/.jelly/tasks/<id>/context.md
5
+ * file is created. Intermediate tool results are appended there instead of
6
+ * bloating the message history. The model gets a compact file reference
7
+ * instead of 10KB of raw JSON. The folder auto-deletes on task completion
8
+ * unless the user marked it /keep.
9
+ *
10
+ * This is the primary mechanism that allows turbo/max mode to stay within
11
+ * context budget even on complex multi-step research tasks.
12
+ */
13
+ import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, readdirSync, } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { homedir } from "node:os";
16
+ import { randomUUID } from "node:crypto";
17
+ const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
18
+ const TASKS_DIR = join(JELLY_HOME, "tasks");
19
+ export class ContextStore {
20
+ activeTasks = new Map();
21
+ constructor() {
22
+ mkdirSync(TASKS_DIR, { recursive: true });
23
+ }
24
+ // ── Task lifecycle ─────────────────────────────────────────────────────────
25
+ /** Open a new ephemeral task folder. Returns the TaskContext. */
26
+ openTask(title, keep = false) {
27
+ const taskId = randomUUID().slice(0, 8);
28
+ const taskDir = join(TASKS_DIR, `${Date.now()}-${taskId}`);
29
+ const contextMd = join(taskDir, "context.md");
30
+ mkdirSync(taskDir, { recursive: true });
31
+ writeFileSync(contextMd, [
32
+ `# Task: ${title.slice(0, 120)}`,
33
+ `**ID:** ${taskId}`,
34
+ `**Started:** ${new Date().toISOString()}`,
35
+ `**Status:** in_progress`,
36
+ ``,
37
+ `## Findings`,
38
+ ``,
39
+ ].join("\n"), "utf-8");
40
+ const ctx = {
41
+ taskId, taskDir, contextMd,
42
+ createdAt: Date.now(), title, keep, findings: 0,
43
+ };
44
+ this.activeTasks.set(taskId, ctx);
45
+ return ctx;
46
+ }
47
+ /** Append a finding / tool result to the task's context.md */
48
+ appendFinding(taskId, section, content) {
49
+ const ctx = this.activeTasks.get(taskId);
50
+ if (!ctx || !existsSync(ctx.contextMd))
51
+ return;
52
+ // Cap individual entries at 3KB to prevent runaway growth
53
+ const cappedContent = content.length > 3000
54
+ ? content.slice(0, 3000) + `\n\n…[truncated ${content.length - 3000} chars]`
55
+ : content;
56
+ const entry = [
57
+ `### ${section}`,
58
+ `_${new Date().toLocaleTimeString()}_`,
59
+ ``,
60
+ cappedContent,
61
+ ``,
62
+ `---`,
63
+ ``,
64
+ ].join("\n");
65
+ const existing = readFileSync(ctx.contextMd, "utf-8");
66
+ writeFileSync(ctx.contextMd, existing + entry, "utf-8");
67
+ ctx.findings++;
68
+ }
69
+ /**
70
+ * Get a compact reference string for injection into the model context.
71
+ * Returns something like:
72
+ * "[Task ctx: ~/.jelly/tasks/.../context.md — 4 findings, 3.2KB. Use read_task_context("abc123")]"
73
+ */
74
+ getReference(taskId) {
75
+ const ctx = this.activeTasks.get(taskId);
76
+ if (!ctx || !existsSync(ctx.contextMd))
77
+ return "";
78
+ const content = readFileSync(ctx.contextMd, "utf-8");
79
+ const sizeKB = (content.length / 1024).toFixed(1);
80
+ return `[Task context saved: ${ctx.contextMd} — ${ctx.findings} findings, ${sizeKB}KB. ` +
81
+ `To read back: use tool read_task_context with taskId="${taskId}"]`;
82
+ }
83
+ /**
84
+ * Mark a task complete and optionally delete its folder.
85
+ * Deletion is deferred 5 seconds to allow model to read final state.
86
+ */
87
+ closeTask(taskId) {
88
+ const ctx = this.activeTasks.get(taskId);
89
+ if (!ctx)
90
+ return;
91
+ // Update status in the file
92
+ if (existsSync(ctx.contextMd)) {
93
+ try {
94
+ const content = readFileSync(ctx.contextMd, "utf-8");
95
+ writeFileSync(ctx.contextMd, content
96
+ .replace("**Status:** in_progress", `**Status:** completed\n**Completed:** ${new Date().toISOString()}`), "utf-8");
97
+ }
98
+ catch { /* best effort */ }
99
+ }
100
+ if (!ctx.keep) {
101
+ // Deferred delete — give agent 5s to read final context if needed
102
+ setTimeout(() => {
103
+ try {
104
+ rmSync(ctx.taskDir, { recursive: true, force: true });
105
+ }
106
+ catch { /* ignore */ }
107
+ }, 5_000);
108
+ }
109
+ this.activeTasks.delete(taskId);
110
+ }
111
+ /** Permanently keep a task folder (user called /keep <taskId>) */
112
+ keepTask(taskId) {
113
+ const ctx = this.activeTasks.get(taskId);
114
+ if (!ctx)
115
+ return false;
116
+ ctx.keep = true;
117
+ return true;
118
+ }
119
+ getActiveTasks() {
120
+ return [...this.activeTasks.values()];
121
+ }
122
+ getTask(taskId) {
123
+ return this.activeTasks.get(taskId);
124
+ }
125
+ // ── Tool: read_task_context ────────────────────────────────────────────────
126
+ async readContextTool(_id, { taskId }) {
127
+ const ctx = this.activeTasks.get(taskId);
128
+ if (!ctx) {
129
+ // Try to find a completed task folder on disk by taskId suffix
130
+ const dirs = existsSync(TASKS_DIR) ? readdirSync(TASKS_DIR) : [];
131
+ const dir = dirs.find(d => d.endsWith(`-${taskId}`));
132
+ if (dir) {
133
+ const mdPath = join(TASKS_DIR, dir, "context.md");
134
+ if (existsSync(mdPath)) {
135
+ const content = readFileSync(mdPath, "utf-8");
136
+ return {
137
+ content: [{ type: "text", text: content }],
138
+ details: { taskId, path: mdPath, sizeBytes: content.length, status: "archived" },
139
+ };
140
+ }
141
+ }
142
+ return {
143
+ content: [{ type: "text", text: `Task "${taskId}" not found. It may have been deleted after completion.` }],
144
+ details: {},
145
+ };
146
+ }
147
+ if (!existsSync(ctx.contextMd)) {
148
+ return {
149
+ content: [{ type: "text", text: `Context file missing for task "${taskId}".` }],
150
+ details: {},
151
+ };
152
+ }
153
+ const content = readFileSync(ctx.contextMd, "utf-8");
154
+ return {
155
+ content: [{ type: "text", text: content }],
156
+ details: {
157
+ taskId,
158
+ path: ctx.contextMd,
159
+ sizeBytes: content.length,
160
+ findings: ctx.findings,
161
+ status: "active",
162
+ },
163
+ };
164
+ }
165
+ // ── Tool: list_tasks ───────────────────────────────────────────────────────
166
+ async listTasksTool() {
167
+ const active = this.getActiveTasks();
168
+ // Also scan disk for recent completed tasks
169
+ const onDisk = [];
170
+ if (existsSync(TASKS_DIR)) {
171
+ const dirs = readdirSync(TASKS_DIR).slice(-10); // last 10
172
+ for (const d of dirs) {
173
+ const mdPath = join(TASKS_DIR, d, "context.md");
174
+ if (existsSync(mdPath)) {
175
+ const size = (readFileSync(mdPath, "utf-8").length / 1024).toFixed(1);
176
+ const id = d.split("-").pop() ?? d;
177
+ if (!this.activeTasks.has(id))
178
+ onDisk.push({ dir: d, id, sizeKB: size });
179
+ }
180
+ }
181
+ }
182
+ const lines = [];
183
+ if (active.length > 0) {
184
+ lines.push(`Active tasks (${active.length}):`);
185
+ for (const t of active) {
186
+ const mdContent = existsSync(t.contextMd) ? readFileSync(t.contextMd, "utf-8") : "";
187
+ const sizeKB = (mdContent.length / 1024).toFixed(1);
188
+ lines.push(` 📁 [${t.taskId}] ${t.title.slice(0, 50)} — ${t.findings} findings, ${sizeKB}KB`);
189
+ }
190
+ }
191
+ if (onDisk.length > 0) {
192
+ lines.push(`\nRecent completed tasks:`);
193
+ for (const t of onDisk) {
194
+ lines.push(` 📄 [${t.id}] ${t.dir.slice(14, 64)} — ${t.sizeKB}KB`);
195
+ }
196
+ }
197
+ if (lines.length === 0)
198
+ lines.push("No active or recent task contexts.");
199
+ return {
200
+ content: [{ type: "text", text: lines.join("\n") }],
201
+ details: { activeTasks: active.length, completedOnDisk: onDisk.length },
202
+ };
203
+ }
204
+ }
205
+ /** Singleton — one store per process */
206
+ export const contextStore = new ContextStore();
207
+ //# sourceMappingURL=ContextStore.js.map