@albalink/agent 1.0.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.
Files changed (67) hide show
  1. package/README.md +91 -0
  2. package/bin/albacli +31 -0
  3. package/dist/api/ExtensionAPI.d.ts +103 -0
  4. package/dist/api/ExtensionAPI.js +15 -0
  5. package/dist/api/Registry.d.ts +54 -0
  6. package/dist/api/Registry.js +103 -0
  7. package/dist/cli.d.ts +6 -0
  8. package/dist/cli.js +230 -0
  9. package/dist/index.d.ts +41 -0
  10. package/dist/index.js +41 -0
  11. package/dist/loader.d.ts +16 -0
  12. package/dist/loader.js +89 -0
  13. package/dist/mcp/entry.d.ts +2 -0
  14. package/dist/mcp/entry.js +71 -0
  15. package/dist/mcp/server.d.ts +31 -0
  16. package/dist/mcp/server.js +128 -0
  17. package/dist/models/CostTracker.d.ts +67 -0
  18. package/dist/models/CostTracker.js +151 -0
  19. package/dist/models/ModelRegistry.d.ts +159 -0
  20. package/dist/models/ModelRegistry.js +500 -0
  21. package/dist/models/index.d.ts +5 -0
  22. package/dist/models/index.js +3 -0
  23. package/dist/runner/AgentRunner.d.ts +88 -0
  24. package/dist/runner/AgentRunner.js +473 -0
  25. package/dist/runner/ModelClient.d.ts +97 -0
  26. package/dist/runner/ModelClient.js +350 -0
  27. package/dist/runner/SwarmRouter.d.ts +64 -0
  28. package/dist/runner/SwarmRouter.js +216 -0
  29. package/dist/runner/ToolDispatcher.d.ts +29 -0
  30. package/dist/runner/ToolDispatcher.js +168 -0
  31. package/dist/scheduler/AgentScheduler.d.ts +119 -0
  32. package/dist/scheduler/AgentScheduler.js +263 -0
  33. package/dist/server-entry.d.ts +11 -0
  34. package/dist/server-entry.js +15 -0
  35. package/dist/session/ContextStore.d.ts +96 -0
  36. package/dist/session/ContextStore.js +207 -0
  37. package/dist/session/GoalManager.d.ts +101 -0
  38. package/dist/session/GoalManager.js +167 -0
  39. package/dist/session/MemoryStore.d.ts +48 -0
  40. package/dist/session/MemoryStore.js +167 -0
  41. package/dist/session/SessionManager.d.ts +64 -0
  42. package/dist/session/SessionManager.js +193 -0
  43. package/dist/telemetry/Tracer.d.ts +48 -0
  44. package/dist/telemetry/Tracer.js +102 -0
  45. package/dist/tools/MarketSentiment.d.ts +166 -0
  46. package/dist/tools/MarketSentiment.js +209 -0
  47. package/dist/tools/NewsSentiment.d.ts +67 -0
  48. package/dist/tools/NewsSentiment.js +226 -0
  49. package/dist/tools/PriceFeed.d.ts +105 -0
  50. package/dist/tools/PriceFeed.js +282 -0
  51. package/dist/tools/TechnicalAnalysis.d.ts +110 -0
  52. package/dist/tools/TechnicalAnalysis.js +357 -0
  53. package/dist/tools/index.d.ts +7 -0
  54. package/dist/tools/index.js +4 -0
  55. package/dist/tui/App.d.ts +17 -0
  56. package/dist/tui/App.js +225 -0
  57. package/dist/tui/ModelSelector.d.ts +12 -0
  58. package/dist/tui/ModelSelector.js +87 -0
  59. package/dist/tui/REPL.d.ts +24 -0
  60. package/dist/tui/REPL.js +51 -0
  61. package/dist/tui/StatusBar.d.ts +14 -0
  62. package/dist/tui/StatusBar.js +14 -0
  63. package/dist/tui/theme.d.ts +30 -0
  64. package/dist/tui/theme.js +41 -0
  65. package/dist/util/safeLog.d.ts +3 -0
  66. package/dist/util/safeLog.js +21 -0
  67. package/package.json +67 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * ToolDispatcher — executes tool calls from the model response.
3
+ * Looks up tool by name in the Registry, validates params, runs execute().
4
+ */
5
+ import { Value } from "@sinclair/typebox/value";
6
+ /**
7
+ * Attempt to repair common JSON errors from model output.
8
+ * Handles trailing commas, single quotes, unquoted keys.
9
+ * Returns original string if repair doesn't help.
10
+ */
11
+ function repairJson(raw) {
12
+ try {
13
+ JSON.parse(raw);
14
+ return raw;
15
+ }
16
+ catch { /* fall through to repair */ }
17
+ const repaired = raw
18
+ .replace(/,\s*}/g, "}")
19
+ .replace(/,\s*]/g, "]")
20
+ .replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3') // unquoted keys
21
+ .replace(/:\s*'([^']*)'/g, ': "$1"'); // single-quoted values
22
+ try {
23
+ JSON.parse(repaired);
24
+ return repaired;
25
+ }
26
+ catch {
27
+ return raw;
28
+ }
29
+ }
30
+ const TOOL_TIMEOUT_MS = 30_000;
31
+ const CIRCUIT_OPEN_MS = 300_000;
32
+ const CIRCUIT_THRESHOLD = 3;
33
+ // #40: Estimated output sizes per tool (chars) for pre-dispatch budget forecasting
34
+ const TOOL_OUTPUT_ESTIMATES = {
35
+ get_candles: 8_000, // 100 OHLCV + TA = ~8KB
36
+ analyze_ta: 2_000,
37
+ get_prices: 500,
38
+ get_top_movers: 800,
39
+ get_market_overview: 1_000,
40
+ get_news: 4_000,
41
+ get_fear_greed: 400,
42
+ get_funding_rates: 600,
43
+ get_btc_mempool: 400,
44
+ get_defi_tvl: 2_000,
45
+ get_solana_stats: 300,
46
+ list_models: 3_000,
47
+ list_tasks: 500,
48
+ read_task_context: 6_000,
49
+ cost_report: 400,
50
+ list_goals: 600,
51
+ model_summary: 400,
52
+ _default: 2_000,
53
+ };
54
+ /** #40: Estimate chars that will be added to context by dispatching these calls */
55
+ export function forecastContextGrowth(calls) {
56
+ return calls.reduce((sum, tc) => {
57
+ const est = TOOL_OUTPUT_ESTIMATES[tc.function.name] ?? TOOL_OUTPUT_ESTIMATES["_default"];
58
+ return sum + est;
59
+ }, 0);
60
+ }
61
+ export class ToolDispatcher {
62
+ registry;
63
+ failureCounts = new Map();
64
+ openCircuits = new Map(); // toolName → openUntil timestamp
65
+ constructor(registry) {
66
+ this.registry = registry;
67
+ }
68
+ async dispatch(calls) {
69
+ return Promise.all(calls.map(tc => this.execute(tc)));
70
+ }
71
+ async execute(tc) {
72
+ const toolName = tc.function.name;
73
+ // #6: Circuit breaker — fast-fail if tool has been consistently broken
74
+ const openUntil = this.openCircuits.get(toolName) ?? 0;
75
+ if (Date.now() < openUntil) {
76
+ const remainMs = Math.ceil((openUntil - Date.now()) / 1000);
77
+ return {
78
+ tool_call_id: tc.id,
79
+ name: toolName,
80
+ content: `Tool "${toolName}" is temporarily unavailable (circuit open for ${remainMs}s after repeated failures). Use a different approach or try again later.`,
81
+ isError: true,
82
+ };
83
+ }
84
+ try {
85
+ const result = await this.executeWithTimeout(tc);
86
+ // Reset failure count on success
87
+ this.failureCounts.delete(toolName);
88
+ return result;
89
+ }
90
+ catch (e) {
91
+ const errMsg = e instanceof Error ? e.message : String(e);
92
+ const failures = (this.failureCounts.get(toolName) ?? 0) + 1;
93
+ this.failureCounts.set(toolName, failures);
94
+ if (failures >= CIRCUIT_THRESHOLD) {
95
+ this.openCircuits.set(toolName, Date.now() + CIRCUIT_OPEN_MS);
96
+ this.failureCounts.delete(toolName);
97
+ return {
98
+ tool_call_id: tc.id,
99
+ name: toolName,
100
+ content: `Tool "${toolName}" failed ${CIRCUIT_THRESHOLD} times in a row. Circuit opened for 5 minutes. Error: ${errMsg}`,
101
+ isError: true,
102
+ };
103
+ }
104
+ return {
105
+ tool_call_id: tc.id,
106
+ name: toolName,
107
+ content: `Tool error (failure ${failures}/${CIRCUIT_THRESHOLD}): ${errMsg}`,
108
+ isError: true,
109
+ };
110
+ }
111
+ }
112
+ async executeWithTimeout(tc) {
113
+ // Race tool execution against a hard timeout
114
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool "${tc.function.name}" timed out after ${TOOL_TIMEOUT_MS / 1000}s`)), TOOL_TIMEOUT_MS));
115
+ return Promise.race([this.executeInner(tc), timeoutPromise]);
116
+ }
117
+ async executeInner(tc) {
118
+ const tool = this.registry.getTool(tc.function.name);
119
+ if (!tool) {
120
+ return {
121
+ tool_call_id: tc.id,
122
+ name: tc.function.name,
123
+ content: `Unknown tool: ${tc.function.name}`,
124
+ isError: true,
125
+ };
126
+ }
127
+ let params;
128
+ try {
129
+ // #8: attempt JSON repair before hard-failing on malformed model output
130
+ params = JSON.parse(repairJson(tc.function.arguments || "{}"));
131
+ }
132
+ catch {
133
+ return {
134
+ tool_call_id: tc.id,
135
+ name: tc.function.name,
136
+ content: `Invalid JSON arguments (repair failed): ${tc.function.arguments}`,
137
+ isError: true,
138
+ };
139
+ }
140
+ // Validate params against the tool's TypeBox schema
141
+ if (!Value.Check(tool.parameters, params)) {
142
+ const errors = [...Value.Errors(tool.parameters, params)]
143
+ .slice(0, 3)
144
+ .map(e => `${e.path}: ${e.message}`)
145
+ .join("; ");
146
+ return {
147
+ tool_call_id: tc.id,
148
+ name: tc.function.name,
149
+ content: `Parameter validation failed: ${errors}`,
150
+ isError: true,
151
+ };
152
+ }
153
+ try {
154
+ const result = await tool.execute(tc.id, params);
155
+ const content = result.content.map(c => c.text).join("\n");
156
+ return { tool_call_id: tc.id, name: tc.function.name, content, isError: false };
157
+ }
158
+ catch (e) {
159
+ return {
160
+ tool_call_id: tc.id,
161
+ name: tc.function.name,
162
+ content: `Tool error: ${e?.message ?? String(e)}`,
163
+ isError: true,
164
+ };
165
+ }
166
+ }
167
+ }
168
+ //# sourceMappingURL=ToolDispatcher.js.map
@@ -0,0 +1,119 @@
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 ~/.alba/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
+ private _priceListener?;
48
+ constructor();
49
+ private load;
50
+ private save;
51
+ start(onTrigger: (task: ScheduledTask) => void): void;
52
+ stop(): void;
53
+ private tick;
54
+ private checkPriceTrigger;
55
+ addTask(task: Omit<ScheduledTask, "id" | "createdAt" | "runCount">): ScheduledTask;
56
+ removeTask(id: string): boolean;
57
+ enableTask(id: string, enabled: boolean): boolean;
58
+ listTasks(): ScheduledTask[];
59
+ getTask(id: string): ScheduledTask | undefined;
60
+ readonly addTaskParams: import("@sinclair/typebox").TObject<{
61
+ name: import("@sinclair/typebox").TString;
62
+ prompt: import("@sinclair/typebox").TString;
63
+ cron: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
64
+ trigger_symbol: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
65
+ trigger_above: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
66
+ trigger_below: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
67
+ trigger_change_pct: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
68
+ run_in_minutes: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
69
+ run_once: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
70
+ }>;
71
+ addTaskTool(_id: string, params: Static<typeof this.addTaskParams>): Promise<{
72
+ content: {
73
+ type: "text";
74
+ text: string;
75
+ }[];
76
+ details: {
77
+ taskId: string;
78
+ task: ScheduledTask;
79
+ };
80
+ }>;
81
+ readonly listTasksParams: import("@sinclair/typebox").TObject<{
82
+ enabled_only: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
83
+ }>;
84
+ listTasksTool(_id: string, params: Static<typeof this.listTasksParams>): Promise<{
85
+ content: {
86
+ type: "text";
87
+ text: string;
88
+ }[];
89
+ details: {
90
+ count?: undefined;
91
+ tasks?: undefined;
92
+ };
93
+ } | {
94
+ content: {
95
+ type: "text";
96
+ text: string;
97
+ }[];
98
+ details: {
99
+ count: number;
100
+ tasks: ScheduledTask[];
101
+ };
102
+ }>;
103
+ readonly removeTaskParams: import("@sinclair/typebox").TObject<{
104
+ id: import("@sinclair/typebox").TString;
105
+ }>;
106
+ removeTaskTool(_id: string, params: Static<typeof this.removeTaskParams>): Promise<{
107
+ content: {
108
+ type: "text";
109
+ text: string;
110
+ }[];
111
+ details: {
112
+ taskId: string;
113
+ success: boolean;
114
+ };
115
+ }>;
116
+ }
117
+ /** Singleton */
118
+ export declare const agentScheduler: AgentScheduler;
119
+ //# sourceMappingURL=AgentScheduler.d.ts.map
@@ -0,0 +1,263 @@
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 ~/.alba/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 ALBA_HOME = process.env.ALBA_HOME ?? join(homedir(), ".alba");
23
+ const SCHEDULE_FILE = join(ALBA_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
+ _priceListener;
60
+ constructor() {
61
+ this.load();
62
+ }
63
+ // ── Persistence ────────────────────────────────────────────────────────────
64
+ load() {
65
+ try {
66
+ if (!existsSync(SCHEDULE_FILE))
67
+ return;
68
+ const raw = JSON.parse(readFileSync(SCHEDULE_FILE, "utf-8"));
69
+ if (Array.isArray(raw))
70
+ this.tasks = raw;
71
+ }
72
+ catch { /* start fresh */ }
73
+ }
74
+ save() {
75
+ try {
76
+ mkdirSync(ALBA_HOME, { recursive: true });
77
+ writeFileSync(SCHEDULE_FILE, JSON.stringify(this.tasks, null, 2), "utf-8");
78
+ }
79
+ catch { /* best effort */ }
80
+ }
81
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
82
+ start(onTrigger) {
83
+ if (this.timer)
84
+ return;
85
+ // Poll every 60 seconds — aligned to minute boundaries
86
+ this.timer = setInterval(() => this.tick(onTrigger), 60_000);
87
+ // Run once immediately on start to catch any missed tasks
88
+ this.tick(onTrigger);
89
+ // #18: Also check triggers on every price update (not just poll) for low-latency
90
+ if (!this._priceListener) {
91
+ this._priceListener = () => this.tick(onTrigger);
92
+ priceFeed.on?.("prices", this._priceListener);
93
+ }
94
+ }
95
+ stop() {
96
+ if (this.timer) {
97
+ clearInterval(this.timer);
98
+ this.timer = undefined;
99
+ }
100
+ if (this._priceListener) {
101
+ priceFeed.off?.("prices", this._priceListener);
102
+ this._priceListener = undefined;
103
+ }
104
+ }
105
+ tick(onTrigger) {
106
+ const now = new Date();
107
+ const minute = now.getMinutes();
108
+ // Prevent double-firing within the same minute
109
+ if (minute === this.lastTickMinute)
110
+ return;
111
+ this.lastTickMinute = minute;
112
+ for (const task of this.tasks) {
113
+ if (!task.enabled)
114
+ continue;
115
+ let shouldRun = false;
116
+ // One-shot: run at specific time
117
+ if (task.runAt && !task.lastRun) {
118
+ shouldRun = Date.now() >= task.runAt;
119
+ }
120
+ // Cron schedule
121
+ if (task.cron && !shouldRun) {
122
+ const matches = parseCron(task.cron);
123
+ shouldRun = matches(now);
124
+ // Prevent re-firing within same minute
125
+ if (shouldRun && task.lastRun && Date.now() - task.lastRun < 58_000) {
126
+ shouldRun = false;
127
+ }
128
+ }
129
+ // Price trigger
130
+ if (task.trigger && !shouldRun) {
131
+ shouldRun = this.checkPriceTrigger(task.trigger);
132
+ }
133
+ if (shouldRun) {
134
+ task.lastRun = Date.now();
135
+ task.runCount = (task.runCount ?? 0) + 1;
136
+ if (task.runOnce)
137
+ task.enabled = false;
138
+ this.save();
139
+ onTrigger(task);
140
+ }
141
+ }
142
+ }
143
+ checkPriceTrigger(trigger) {
144
+ const tick = priceFeed.get(trigger.symbol.toLowerCase());
145
+ if (!tick)
146
+ return false;
147
+ if (trigger.above !== undefined && tick.price >= trigger.above)
148
+ return true;
149
+ if (trigger.below !== undefined && tick.price <= trigger.below)
150
+ return true;
151
+ if (trigger.changePct !== undefined && Math.abs(tick.change24h) >= trigger.changePct)
152
+ return true;
153
+ return false;
154
+ }
155
+ // ── Task CRUD ──────────────────────────────────────────────────────────────
156
+ addTask(task) {
157
+ const full = {
158
+ ...task,
159
+ id: randomUUID().slice(0, 8),
160
+ createdAt: Date.now(),
161
+ runCount: 0,
162
+ };
163
+ this.tasks.push(full);
164
+ this.save();
165
+ return full;
166
+ }
167
+ removeTask(id) {
168
+ const before = this.tasks.length;
169
+ this.tasks = this.tasks.filter(t => t.id !== id);
170
+ if (this.tasks.length < before) {
171
+ this.save();
172
+ return true;
173
+ }
174
+ return false;
175
+ }
176
+ enableTask(id, enabled) {
177
+ const t = this.tasks.find(t => t.id === id);
178
+ if (!t)
179
+ return false;
180
+ t.enabled = enabled;
181
+ this.save();
182
+ return true;
183
+ }
184
+ listTasks() { return [...this.tasks]; }
185
+ getTask(id) {
186
+ return this.tasks.find(t => t.id === id);
187
+ }
188
+ // ── Tools ──────────────────────────────────────────────────────────────────
189
+ addTaskParams = Type.Object({
190
+ name: Type.String({ description: "Task name" }),
191
+ prompt: Type.String({ description: "The message the agent will run" }),
192
+ cron: Type.Optional(Type.String({
193
+ description: "Cron schedule: '*/15 * * * *' or shorthand @every_15m @hourly @daily",
194
+ })),
195
+ trigger_symbol: Type.Optional(Type.String({ description: "Symbol for price trigger e.g. BTC" })),
196
+ trigger_above: Type.Optional(Type.Number({ description: "Fire when price goes above this" })),
197
+ trigger_below: Type.Optional(Type.Number({ description: "Fire when price goes below this" })),
198
+ trigger_change_pct: Type.Optional(Type.Number({ description: "Fire when 24h change exceeds this %" })),
199
+ run_in_minutes: Type.Optional(Type.Number({ description: "One-shot: run after N minutes" })),
200
+ run_once: Type.Optional(Type.Boolean({ description: "Disable after first run (default: false)" })),
201
+ });
202
+ async addTaskTool(_id, params) {
203
+ let trigger;
204
+ if (params.trigger_symbol) {
205
+ trigger = {
206
+ symbol: params.trigger_symbol,
207
+ above: params.trigger_above,
208
+ below: params.trigger_below,
209
+ changePct: params.trigger_change_pct,
210
+ };
211
+ }
212
+ const task = this.addTask({
213
+ name: params.name,
214
+ prompt: params.prompt,
215
+ cron: params.cron,
216
+ trigger,
217
+ runAt: params.run_in_minutes ? Date.now() + params.run_in_minutes * 60_000 : undefined,
218
+ runOnce: params.run_once ?? false,
219
+ enabled: true,
220
+ });
221
+ const desc = [
222
+ params.cron ? `cron: ${params.cron}` : "",
223
+ trigger ? `trigger: ${params.trigger_symbol} ${trigger.above ? `>$${trigger.above}` : ""} ${trigger.below ? `<$${trigger.below}` : ""}` : "",
224
+ params.run_in_minutes ? `runs in ${params.run_in_minutes}m` : "",
225
+ ].filter(Boolean).join(", ");
226
+ return {
227
+ content: [{ type: "text", text: `Scheduled: [${task.id}] ${task.name}${desc ? ` (${desc})` : ""}` }],
228
+ details: { taskId: task.id, task },
229
+ };
230
+ }
231
+ listTasksParams = Type.Object({
232
+ enabled_only: Type.Optional(Type.Boolean({ description: "Only show enabled tasks (default: false)" })),
233
+ });
234
+ async listTasksTool(_id, params) {
235
+ const tasks = params.enabled_only ? this.tasks.filter(t => t.enabled) : this.tasks;
236
+ if (tasks.length === 0) {
237
+ return { content: [{ type: "text", text: "No scheduled tasks." }], details: {} };
238
+ }
239
+ const lines = tasks.map(t => {
240
+ const icon = t.enabled ? "🟢" : "⚪";
241
+ const schedule = t.cron ?? (t.trigger ? `price trigger ${t.trigger.symbol}` : t.runAt ? `at ${new Date(t.runAt).toLocaleTimeString()}` : "manual");
242
+ const runs = t.runCount > 0 ? ` (${t.runCount} runs)` : "";
243
+ return `${icon} [${t.id}] ${t.name} — ${schedule}${runs}`;
244
+ });
245
+ return {
246
+ content: [{ type: "text", text: `Scheduled tasks (${tasks.length}):\n${lines.join("\n")}` }],
247
+ details: { count: tasks.length, tasks },
248
+ };
249
+ }
250
+ removeTaskParams = Type.Object({
251
+ id: Type.String({ description: "Task ID to remove" }),
252
+ });
253
+ async removeTaskTool(_id, params) {
254
+ const ok = this.removeTask(params.id);
255
+ return {
256
+ content: [{ type: "text", text: ok ? `Task ${params.id} removed.` : `Task ${params.id} not found.` }],
257
+ details: { taskId: params.id, success: ok },
258
+ };
259
+ }
260
+ }
261
+ /** Singleton */
262
+ export const agentScheduler = new AgentScheduler();
263
+ //# sourceMappingURL=AgentScheduler.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * server-entry.ts — Lean entry point for the dashboard server.
3
+ * Exports only what app/server/server.js needs, avoiding Ink/React/React DOM deps.
4
+ *
5
+ * Import from: import { ModelClient, resolveModelConfig, PriceFeed, NewsFeed, SwarmRouter } from "@albalink/agent";
6
+ */
7
+ export { ModelClient, resolveModelConfig } from "./runner/ModelClient.js";
8
+ export { PriceFeed, priceFeed } from "./tools/PriceFeed.js";
9
+ export { NewsFeed, newsFeed } from "./tools/NewsSentiment.js";
10
+ export { SwarmRouter, scoreComplexity } from "./runner/SwarmRouter.js";
11
+ //# sourceMappingURL=server-entry.d.ts.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * server-entry.ts — Lean entry point for the dashboard server.
3
+ * Exports only what app/server/server.js needs, avoiding Ink/React/React DOM deps.
4
+ *
5
+ * Import from: import { ModelClient, resolveModelConfig, PriceFeed, NewsFeed, SwarmRouter } from "@albalink/agent";
6
+ */
7
+ // Model client — used by server for OpenAI-compatible chat completions
8
+ export { ModelClient, resolveModelConfig } from "./runner/ModelClient.js";
9
+ // Price feed singleton — real-time crypto prices
10
+ export { PriceFeed, priceFeed } from "./tools/PriceFeed.js";
11
+ // News feed singleton — crypto news sentiment
12
+ export { NewsFeed, newsFeed } from "./tools/NewsSentiment.js";
13
+ // SwarmRouter — sub-agent orchestration
14
+ export { SwarmRouter, scoreComplexity } from "./runner/SwarmRouter.js";
15
+ //# sourceMappingURL=server-entry.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 ~/.alba/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: ~/.alba/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