@kaleidorg/mind 0.5.1 → 0.6.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 (104) hide show
  1. package/dist/autonomy/index.d.ts +21 -0
  2. package/dist/autonomy/index.d.ts.map +1 -0
  3. package/dist/autonomy/index.js +16 -0
  4. package/dist/autonomy/index.js.map +1 -0
  5. package/dist/autonomy/prompt.d.ts +21 -0
  6. package/dist/autonomy/prompt.d.ts.map +1 -0
  7. package/dist/autonomy/prompt.js +37 -0
  8. package/dist/autonomy/prompt.js.map +1 -0
  9. package/dist/autonomy/risk.d.ts +53 -0
  10. package/dist/autonomy/risk.d.ts.map +1 -0
  11. package/dist/autonomy/risk.js +74 -0
  12. package/dist/autonomy/risk.js.map +1 -0
  13. package/dist/autonomy/run-state.d.ts +39 -0
  14. package/dist/autonomy/run-state.d.ts.map +1 -0
  15. package/dist/autonomy/run-state.js +118 -0
  16. package/dist/autonomy/run-state.js.map +1 -0
  17. package/dist/autonomy/scheduler.d.ts +18 -0
  18. package/dist/autonomy/scheduler.d.ts.map +1 -0
  19. package/dist/autonomy/scheduler.js +113 -0
  20. package/dist/autonomy/scheduler.js.map +1 -0
  21. package/dist/autonomy/task-store.d.ts +44 -0
  22. package/dist/autonomy/task-store.d.ts.map +1 -0
  23. package/dist/autonomy/task-store.js +139 -0
  24. package/dist/autonomy/task-store.js.map +1 -0
  25. package/dist/autonomy/types.d.ts +164 -0
  26. package/dist/autonomy/types.d.ts.map +1 -0
  27. package/dist/autonomy/types.js +20 -0
  28. package/dist/autonomy/types.js.map +1 -0
  29. package/dist/funnel.d.ts.map +1 -1
  30. package/dist/funnel.js +12 -0
  31. package/dist/funnel.js.map +1 -1
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/knowledge/bitcoin-copilot.js +2 -2
  37. package/dist/knowledge/bitcoin-copilot.js.map +1 -1
  38. package/dist/qvac/index.d.ts +1 -1
  39. package/dist/qvac/index.d.ts.map +1 -1
  40. package/dist/qvac/index.js.map +1 -1
  41. package/dist/qvac/parse.d.ts +18 -0
  42. package/dist/qvac/parse.d.ts.map +1 -1
  43. package/dist/qvac/parse.js +1 -0
  44. package/dist/qvac/parse.js.map +1 -1
  45. package/dist/qvac/provider.d.ts +16 -0
  46. package/dist/qvac/provider.d.ts.map +1 -1
  47. package/dist/qvac/provider.js +17 -1
  48. package/dist/qvac/provider.js.map +1 -1
  49. package/dist/qvac/stream.d.ts +16 -0
  50. package/dist/qvac/stream.d.ts.map +1 -1
  51. package/dist/qvac/stream.js +21 -1
  52. package/dist/qvac/stream.js.map +1 -1
  53. package/dist/recipe/buy-asset-channel.d.ts +1 -1
  54. package/dist/recipe/buy-asset-channel.d.ts.map +1 -1
  55. package/dist/recipe/buy-asset-channel.js +4 -3
  56. package/dist/recipe/buy-asset-channel.js.map +1 -1
  57. package/dist/recipe/kaleidoswap-atomic.d.ts +1 -1
  58. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
  59. package/dist/recipe/kaleidoswap-atomic.js +5 -4
  60. package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
  61. package/dist/recipe/runner.d.ts.map +1 -1
  62. package/dist/recipe/runner.js +38 -0
  63. package/dist/recipe/runner.js.map +1 -1
  64. package/dist/tools/mcp.d.ts +19 -0
  65. package/dist/tools/mcp.d.ts.map +1 -1
  66. package/dist/tools/mcp.js +51 -9
  67. package/dist/tools/mcp.js.map +1 -1
  68. package/package.json +2 -1
  69. package/skills/channel-manager/SKILL.md +59 -0
  70. package/skills/dca/SKILL.md +48 -0
  71. package/skills/kaleido-lsps/SKILL.md +12 -12
  72. package/skills/kaleido-trading/SKILL.md +1 -1
  73. package/skills/liquidity-optimizer/SKILL.md +91 -0
  74. package/skills/merchant-finder/SKILL.md +1 -1
  75. package/skills/portfolio-manager/SKILL.md +67 -0
  76. package/skills/rgb-lightning-node/SKILL.md +3 -3
  77. package/skills/wallet-assistant/SKILL.md +1 -1
  78. package/src/autonomy/autonomy.test.ts +348 -0
  79. package/src/autonomy/index.ts +50 -0
  80. package/src/autonomy/prompt.ts +48 -0
  81. package/src/autonomy/risk.ts +139 -0
  82. package/src/autonomy/run-state.ts +144 -0
  83. package/src/autonomy/scheduler.ts +120 -0
  84. package/src/autonomy/task-store.ts +167 -0
  85. package/src/autonomy/types.ts +186 -0
  86. package/src/funnel.mind.test.ts +390 -0
  87. package/src/funnel.ts +14 -0
  88. package/src/index.ts +41 -0
  89. package/src/knowledge/bitcoin-copilot.ts +2 -2
  90. package/src/qvac/index.ts +1 -0
  91. package/src/qvac/parse.ts +20 -0
  92. package/src/qvac/provider.test.ts +17 -0
  93. package/src/qvac/provider.ts +37 -1
  94. package/src/qvac/stream.test.ts +25 -0
  95. package/src/qvac/stream.ts +38 -1
  96. package/src/recipe/buy-asset-channel.test.ts +5 -0
  97. package/src/recipe/buy-asset-channel.ts +6 -3
  98. package/src/recipe/kaleidoswap-atomic.test.ts +3 -3
  99. package/src/recipe/kaleidoswap-atomic.ts +5 -4
  100. package/src/recipe/recipe.test.ts +16 -0
  101. package/src/recipe/runner.ts +41 -0
  102. package/src/tools/mcp.live.test.ts +116 -0
  103. package/src/tools/mcp.parse.test.ts +37 -0
  104. package/src/tools/mcp.ts +55 -9
@@ -0,0 +1,144 @@
1
+ /**
2
+ * TaskRunLog — what each task did, when, and what it cost. Port of kaleidoagent's
3
+ * agent-state.ts (LoopStats + recent-runs + cumulative cost), generalized and
4
+ * decoupled from the HTTP status server. This is the "memory of the tasks": the
5
+ * record the UI reads to show "rebalance last ran 2h ago, cost $0.003".
6
+ *
7
+ * In-memory with optional injected persistence. Pure TS, zero deps.
8
+ */
9
+
10
+ import type {
11
+ RunLogIO,
12
+ RunLogSnapshot,
13
+ TaskRunCost,
14
+ TaskRunRecord,
15
+ TaskStats,
16
+ } from './types.js';
17
+
18
+ const ZERO_COST: TaskRunCost = { usd: 0, inputTokens: 0, outputTokens: 0 };
19
+ const DEFAULT_MAX_RECENT = 20;
20
+ const TEXT_CAP = 800;
21
+
22
+ export interface RunLogOptions {
23
+ /** Persistence (load on first use, save on writes). Omit for ephemeral logs. */
24
+ io?: RunLogIO;
25
+ /** How many recent runs to retain. Default 20. */
26
+ maxRecent?: number;
27
+ /** Clock — injectable for deterministic tests. */
28
+ now?: () => number;
29
+ }
30
+
31
+ export class TaskRunLog {
32
+ private stats: Record<string, TaskStats> = {};
33
+ private recentRuns: TaskRunRecord[] = [];
34
+ private cumulative: TaskRunCost = { ...ZERO_COST };
35
+ private hydrated = false;
36
+ private readonly io?: RunLogIO;
37
+ private readonly maxRecent: number;
38
+ private readonly now: () => number;
39
+
40
+ constructor(opts: RunLogOptions = {}) {
41
+ this.io = opts.io;
42
+ this.maxRecent = opts.maxRecent ?? DEFAULT_MAX_RECENT;
43
+ this.now = opts.now ?? (() => Date.now());
44
+ }
45
+
46
+ private async hydrate(): Promise<void> {
47
+ if (this.hydrated) return;
48
+ this.hydrated = true;
49
+ if (this.io) {
50
+ try {
51
+ const snap = await this.io.load();
52
+ if (snap) {
53
+ this.stats = snap.stats ?? {};
54
+ this.recentRuns = snap.recent ?? [];
55
+ this.cumulative = snap.cumulative ?? { ...ZERO_COST };
56
+ }
57
+ } catch {
58
+ /* start fresh on a corrupt/absent snapshot */
59
+ }
60
+ }
61
+ }
62
+
63
+ private async persist(): Promise<void> {
64
+ if (this.io) await this.io.save(this.snapshotSync());
65
+ }
66
+
67
+ private ensure(taskId: string): TaskStats {
68
+ const existing = this.stats[taskId];
69
+ if (existing) return existing;
70
+ const fresh: TaskStats = {
71
+ runs: 0,
72
+ errors: 0,
73
+ lastRunAt: null,
74
+ lastDurationMs: null,
75
+ lastToolCalls: null,
76
+ lastError: null,
77
+ lastText: null,
78
+ };
79
+ this.stats[taskId] = fresh;
80
+ return fresh;
81
+ }
82
+
83
+ /** Record a completed run (success or failure). */
84
+ async record(record: TaskRunRecord): Promise<void> {
85
+ await this.hydrate();
86
+ const s = this.ensure(record.taskId);
87
+ s.runs += 1;
88
+ s.lastRunAt = record.startedAt;
89
+ s.lastDurationMs = record.durationMs;
90
+ s.lastToolCalls = record.toolCalls;
91
+ if (record.ok) {
92
+ s.lastError = null;
93
+ s.lastText = record.text.slice(0, TEXT_CAP);
94
+ } else {
95
+ s.errors += 1;
96
+ s.lastError = record.error;
97
+ }
98
+
99
+ this.cumulative = {
100
+ usd: this.cumulative.usd + record.cost.usd,
101
+ inputTokens: this.cumulative.inputTokens + record.cost.inputTokens,
102
+ outputTokens: this.cumulative.outputTokens + record.cost.outputTokens,
103
+ };
104
+
105
+ this.recentRuns.unshift({ ...record, text: record.text.slice(0, TEXT_CAP) });
106
+ if (this.recentRuns.length > this.maxRecent) {
107
+ this.recentRuns.length = this.maxRecent;
108
+ }
109
+ await this.persist();
110
+ }
111
+
112
+ async statsFor(taskId: string): Promise<TaskStats | null> {
113
+ await this.hydrate();
114
+ return this.stats[taskId] ?? null;
115
+ }
116
+
117
+ async allStats(): Promise<Record<string, TaskStats>> {
118
+ await this.hydrate();
119
+ return { ...this.stats };
120
+ }
121
+
122
+ async recent(limit?: number): Promise<TaskRunRecord[]> {
123
+ await this.hydrate();
124
+ return this.recentRuns.slice(0, limit ?? this.recentRuns.length);
125
+ }
126
+
127
+ async totalCost(): Promise<TaskRunCost> {
128
+ await this.hydrate();
129
+ return { ...this.cumulative };
130
+ }
131
+
132
+ async snapshot(): Promise<RunLogSnapshot> {
133
+ await this.hydrate();
134
+ return this.snapshotSync();
135
+ }
136
+
137
+ private snapshotSync(): RunLogSnapshot {
138
+ return {
139
+ stats: { ...this.stats },
140
+ recent: [...this.recentRuns],
141
+ cumulative: { ...this.cumulative },
142
+ };
143
+ }
144
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Scheduler — fires due tasks on their interval. Replaces the nanobot cron
3
+ * engine kaleidoagent relied on; here it's pure TS with injectable clock +
4
+ * timers so it's deterministically testable and runs anywhere (no cron daemon).
5
+ *
6
+ * Semantics:
7
+ * - A task is "due" when enabled, scheduleSec > 0, and at least scheduleSec
8
+ * has elapsed since its last run (or its creation, if never run). A fresh
9
+ * task therefore waits one full interval before its first auto-run — unless
10
+ * runOnStartup is set, in which case start() runs it immediately.
11
+ * - Runs are serial by default (concurrency 1) — safest for a wallet. A task
12
+ * never runs concurrently with itself.
13
+ * - lastRunAt is stamped at the START of a run, so a slow run doesn't shorten
14
+ * the next interval, and a failing run still advances (no hot-loop).
15
+ */
16
+
17
+ import type {
18
+ AgentTask,
19
+ SchedulerOptions,
20
+ TaskRunOutcome,
21
+ TaskScheduler,
22
+ TimerHandle,
23
+ } from './types.js';
24
+
25
+ export function createTaskScheduler(opts: SchedulerOptions): TaskScheduler {
26
+ const { store, run } = opts;
27
+ const now = opts.now ?? (() => Date.now());
28
+ const log = opts.log ?? (() => {});
29
+ const tickMs = opts.tickMs ?? 30_000;
30
+ const concurrency = Math.max(1, opts.concurrency ?? 1);
31
+ const setTimer = opts.setTimer ?? ((fn, ms) => setInterval(fn, ms) as unknown as TimerHandle);
32
+ const clearTimer = opts.clearTimer ?? ((h) => clearInterval(h as ReturnType<typeof setInterval>));
33
+
34
+ const active = new Set<string>();
35
+ let running = false;
36
+ let timer: TimerHandle | undefined;
37
+
38
+ function isDue(task: AgentTask, ref: number): boolean {
39
+ if (!task.enabled || task.scheduleSec <= 0) return false;
40
+ const since = ref - (task.lastRunAt ?? task.createdAt);
41
+ return since >= task.scheduleSec * 1000;
42
+ }
43
+
44
+ /** Run one task, stamping lastRunAt and reporting the outcome. */
45
+ async function runOne(task: AgentTask): Promise<TaskRunOutcome> {
46
+ active.add(task.id);
47
+ const started = now();
48
+ // Stamp the start time first so a crash mid-run still advances the interval.
49
+ await store.update(task.id, { lastRunAt: started });
50
+ let outcome: TaskRunOutcome;
51
+ try {
52
+ outcome = await run(task);
53
+ } catch (e) {
54
+ outcome = { ok: false, error: (e as Error)?.message ?? String(e) };
55
+ } finally {
56
+ active.delete(task.id);
57
+ }
58
+ const durationMs = now() - started;
59
+ log(`ran ${task.id} ok=${outcome.ok} ${durationMs}ms`);
60
+ opts.onOutcome?.(task, outcome, durationMs);
61
+ return outcome;
62
+ }
63
+
64
+ async function tick(): Promise<void> {
65
+ const tasks = await store.list();
66
+ const ref = now();
67
+ const launched: Promise<unknown>[] = [];
68
+ for (const task of tasks) {
69
+ if (active.size >= concurrency) break;
70
+ if (active.has(task.id) || !isDue(task, ref)) continue;
71
+ // active.add (inside runOne) runs synchronously, so the size guard stays
72
+ // accurate as we launch up to `concurrency` runs that overlap each other;
73
+ // we await them so `tick()` resolves only once this batch is done.
74
+ launched.push(runOne(task));
75
+ }
76
+ await Promise.all(launched);
77
+ }
78
+
79
+ async function startupPass(): Promise<void> {
80
+ const tasks = await store.list();
81
+ for (const task of tasks) {
82
+ if (task.enabled && task.runOnStartup && !active.has(task.id)) {
83
+ // Serial on startup — predictable ordering, no thundering herd.
84
+ await runOne(task);
85
+ }
86
+ }
87
+ }
88
+
89
+ return {
90
+ start(): void {
91
+ if (running) return;
92
+ running = true;
93
+ timer = setTimer(() => {
94
+ void tick();
95
+ }, tickMs);
96
+ log(`scheduler started (tick=${tickMs}ms, concurrency=${concurrency})`);
97
+ void startupPass();
98
+ },
99
+ stop(): void {
100
+ running = false;
101
+ if (timer !== undefined) {
102
+ clearTimer(timer);
103
+ timer = undefined;
104
+ }
105
+ log('scheduler stopped');
106
+ },
107
+ tick,
108
+ async runNow(id: string): Promise<TaskRunOutcome | null> {
109
+ const task = await store.get(id);
110
+ if (!task || active.has(id)) return null;
111
+ return runOne(task);
112
+ },
113
+ active(): string[] {
114
+ return [...active];
115
+ },
116
+ isRunning(): boolean {
117
+ return running;
118
+ },
119
+ };
120
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * TaskStore implementation — in-memory, with optional injected persistence.
3
+ * Pure TS, zero deps. Port of kaleidoagent's tasks-store.ts, but storage-agnostic
4
+ * (host injects IO: fs on a Node sidecar, SQLite on the desktop, AsyncStorage on RN).
5
+ *
6
+ * const store = new InMemoryTaskStore(); // ephemeral
7
+ * const store = new InMemoryTaskStore({ io }); // persisted
8
+ */
9
+
10
+ import {
11
+ ZERO_ALLOCATION,
12
+ type AgentTask,
13
+ type NewTask,
14
+ type TaskSeed,
15
+ type TaskStore,
16
+ type TaskStoreIO,
17
+ } from './types.js';
18
+
19
+ export interface TaskStoreOptions {
20
+ /** Persistence (load on first use, save on writes). Omit for ephemeral tasks. */
21
+ io?: TaskStoreIO;
22
+ /** Clock — injectable for deterministic tests. */
23
+ now?: () => number;
24
+ }
25
+
26
+ export class InMemoryTaskStore implements TaskStore {
27
+ private tasks: AgentTask[] = [];
28
+ private hydrated = false;
29
+ private counter = 0;
30
+ private readonly io?: TaskStoreIO;
31
+ private readonly now: () => number;
32
+
33
+ constructor(opts: TaskStoreOptions = {}) {
34
+ this.io = opts.io;
35
+ this.now = opts.now ?? (() => Date.now());
36
+ }
37
+
38
+ private async hydrate(): Promise<void> {
39
+ if (this.hydrated) return;
40
+ this.hydrated = true;
41
+ if (this.io) {
42
+ try {
43
+ this.tasks = await this.io.load();
44
+ this.counter = this.tasks.length;
45
+ } catch {
46
+ this.tasks = [];
47
+ }
48
+ }
49
+ }
50
+
51
+ private async persist(): Promise<void> {
52
+ if (this.io) await this.io.save(this.tasks);
53
+ }
54
+
55
+ async list(): Promise<AgentTask[]> {
56
+ await this.hydrate();
57
+ return [...this.tasks];
58
+ }
59
+
60
+ async get(id: string): Promise<AgentTask | null> {
61
+ await this.hydrate();
62
+ return this.tasks.find((t) => t.id === id) ?? null;
63
+ }
64
+
65
+ async create(input: NewTask): Promise<AgentTask> {
66
+ await this.hydrate();
67
+ const task = this.materialize(input);
68
+ this.tasks.push(task);
69
+ await this.persist();
70
+ return task;
71
+ }
72
+
73
+ async update(
74
+ id: string,
75
+ patch: Partial<Omit<AgentTask, 'id' | 'createdAt'>>,
76
+ ): Promise<AgentTask | null> {
77
+ await this.hydrate();
78
+ const existing = this.tasks.find((t) => t.id === id);
79
+ if (!existing) return null;
80
+ // id + createdAt are immutable; strip them defensively even if cast in.
81
+ const { id: _id, createdAt: _createdAt, ...safe } = patch as Partial<AgentTask>;
82
+ const updated: AgentTask = { ...existing, ...safe };
83
+ this.tasks = this.tasks.map((t) => (t.id === id ? updated : t));
84
+ await this.persist();
85
+ return updated;
86
+ }
87
+
88
+ async remove(id: string): Promise<boolean> {
89
+ await this.hydrate();
90
+ const before = this.tasks.length;
91
+ this.tasks = this.tasks.filter((t) => t.id !== id);
92
+ const removed = this.tasks.length < before;
93
+ if (removed) await this.persist();
94
+ return removed;
95
+ }
96
+
97
+ async seedDefaults(seeds: TaskSeed[]): Promise<AgentTask[]> {
98
+ await this.hydrate();
99
+ const present = new Set(this.tasks.map((t) => t.id));
100
+ const added = seeds
101
+ .filter((s) => !present.has(s.id))
102
+ .map((s) => this.materialize(s));
103
+ if (added.length) {
104
+ // Seeds first, so the default loops sort to the top of the list.
105
+ this.tasks = [...added, ...this.tasks];
106
+ await this.persist();
107
+ }
108
+ return added;
109
+ }
110
+
111
+ /** Fill defaults + assign id/createdAt for a new or seed task. */
112
+ private materialize(input: NewTask): AgentTask {
113
+ return {
114
+ id: input.id ?? `task_${this.now()}_${++this.counter}`,
115
+ name: input.name,
116
+ description: input.description,
117
+ skill: input.skill,
118
+ scheduleSec: input.scheduleSec,
119
+ runOnStartup: input.runOnStartup ?? false,
120
+ allocation: input.allocation ?? { ...ZERO_ALLOCATION },
121
+ enabled: input.enabled,
122
+ createdAt: input.createdAt ?? this.now(),
123
+ lastRunAt: input.lastRunAt ?? null,
124
+ };
125
+ }
126
+ }
127
+
128
+ /**
129
+ * The three default loops nanobot seeded — ready to pass to `seedDefaults`.
130
+ * Disabled by default: a wallet agent must be turned on deliberately, never
131
+ * auto-arm itself on first launch.
132
+ */
133
+ export function defaultTaskSeeds(opts: {
134
+ heartbeatSec?: number;
135
+ rebalanceSec?: number;
136
+ dailySummarySec?: number;
137
+ } = {}): TaskSeed[] {
138
+ return [
139
+ {
140
+ id: 'heartbeat',
141
+ name: 'Heartbeat',
142
+ description: 'Node health check, channel audit, RGB transfer flush',
143
+ skill: 'channel-manager',
144
+ scheduleSec: opts.heartbeatSec ?? 300,
145
+ runOnStartup: true,
146
+ enabled: false,
147
+ },
148
+ {
149
+ id: 'rebalance',
150
+ name: 'Portfolio Rebalance',
151
+ description: 'Detect allocation drift and execute rebalancing swaps',
152
+ skill: 'portfolio-manager',
153
+ scheduleSec: opts.rebalanceSec ?? 3600,
154
+ runOnStartup: false,
155
+ enabled: false,
156
+ },
157
+ {
158
+ id: 'daily_summary',
159
+ name: 'Daily Summary',
160
+ description: 'Full portfolio snapshot and market report',
161
+ skill: 'portfolio-manager',
162
+ scheduleSec: opts.dailySummarySec ?? 86_400,
163
+ runOnStartup: false,
164
+ enabled: false,
165
+ },
166
+ ];
167
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Autonomy — the agent's task brain.
3
+ *
4
+ * This is the half of "the agent's memory" the {@link ../memory/types MemoryStore}
5
+ * (soul + facts) does NOT cover: the *operational* state nanobot kept across
6
+ * `tasks.json` + `cron/jobs.json` + its run history. Lifted into core so every
7
+ * host (desktop sidecar, agent, cli) runs the same autonomous loop — storage
8
+ * and timers are injected (fs/SQLite on Node, AsyncStorage on RN), the logic is
9
+ * pure TS with zero runtime deps.
10
+ *
11
+ * Three pieces:
12
+ * - TaskStore — the registry of scheduled/manual tasks (was tasks-store.ts)
13
+ * - TaskRunLog — what each task did, when, and what it cost (was agent-state.ts)
14
+ * - Scheduler — fires due tasks on their interval (was nanobot cron)
15
+ *
16
+ * Spend safety lives alongside in {@link ./risk}.
17
+ */
18
+
19
+ /** Capital earmarked for a task — isolates how much a single task may touch. */
20
+ export interface TaskAllocation {
21
+ /** Satoshis of BTC the task may spend. */
22
+ btcSat: number;
23
+ /** USDT (display units) the task may spend. */
24
+ usdt: number;
25
+ /** XAUT (display units) the task may spend. */
26
+ xaut: number;
27
+ }
28
+
29
+ /** A zero allocation — the default for read-only / monitoring tasks. */
30
+ export const ZERO_ALLOCATION: TaskAllocation = { btcSat: 0, usdt: 0, xaut: 0 };
31
+
32
+ /**
33
+ * A scheduled (or manual) autonomous task — the unit nanobot stored in
34
+ * `tasks.json`. A task names a skill to run on an interval, with an optional
35
+ * capital budget and an enable switch.
36
+ */
37
+ export interface AgentTask {
38
+ id: string;
39
+ name: string;
40
+ description: string;
41
+ /** Skill that scopes the run, e.g. 'portfolio-manager'. */
42
+ skill: string;
43
+ /** Seconds between runs. 0 = manual-only (never auto-fires). */
44
+ scheduleSec: number;
45
+ /** Run once immediately when the scheduler starts. */
46
+ runOnStartup: boolean;
47
+ /** Capital this task is allowed to move. */
48
+ allocation: TaskAllocation;
49
+ enabled: boolean;
50
+ /** Epoch ms. */
51
+ createdAt: number;
52
+ /** Epoch ms of the last run, or null if never run. */
53
+ lastRunAt: number | null;
54
+ }
55
+
56
+ /**
57
+ * What `create` accepts — id/createdAt/lastRunAt are filled by the store, and
58
+ * allocation/runOnStartup default when omitted.
59
+ */
60
+ export type NewTask = Omit<
61
+ AgentTask,
62
+ 'id' | 'createdAt' | 'lastRunAt' | 'allocation' | 'runOnStartup'
63
+ > &
64
+ Partial<Pick<AgentTask, 'id' | 'createdAt' | 'lastRunAt' | 'allocation' | 'runOnStartup'>>;
65
+
66
+ /** A default/seed task — carries a stable id so seeding is idempotent. */
67
+ export type TaskSeed = NewTask & { id: string };
68
+
69
+ /** The task registry. Mirrors {@link ../memory/types.MemoryStore}'s shape. */
70
+ export interface TaskStore {
71
+ list(): Promise<AgentTask[]>;
72
+ get(id: string): Promise<AgentTask | null>;
73
+ create(input: NewTask): Promise<AgentTask>;
74
+ /** Patch a task. id/createdAt are immutable. Returns null if not found. */
75
+ update(id: string, patch: Partial<Omit<AgentTask, 'id' | 'createdAt'>>): Promise<AgentTask | null>;
76
+ remove(id: string): Promise<boolean>;
77
+ /** Insert any seed whose id isn't already present. Returns the ones added. */
78
+ seedDefaults(seeds: TaskSeed[]): Promise<AgentTask[]>;
79
+ }
80
+
81
+ /** Injected persistence — load once, save on every mutation. */
82
+ export interface TaskStoreIO {
83
+ load(): Promise<AgentTask[]>;
84
+ save(tasks: AgentTask[]): Promise<void>;
85
+ }
86
+
87
+ // ── Run history ────────────────────────────────────────────────────────────
88
+
89
+ /** Token + dollar cost of a single run. */
90
+ export interface TaskRunCost {
91
+ usd: number;
92
+ inputTokens: number;
93
+ outputTokens: number;
94
+ }
95
+
96
+ /** Aggregated stats for one task across all its runs. */
97
+ export interface TaskStats {
98
+ runs: number;
99
+ errors: number;
100
+ lastRunAt: number | null;
101
+ lastDurationMs: number | null;
102
+ lastToolCalls: number | null;
103
+ lastError: string | null;
104
+ /** First ~800 chars of the last final response. */
105
+ lastText: string | null;
106
+ }
107
+
108
+ /** One completed run, newest-first in the recent ring buffer. */
109
+ export interface TaskRunRecord {
110
+ taskId: string;
111
+ taskName: string;
112
+ /** Epoch ms the run started. */
113
+ startedAt: number;
114
+ durationMs: number;
115
+ toolCalls: number;
116
+ ok: boolean;
117
+ error: string | null;
118
+ /** Final response text (truncated by the log). */
119
+ text: string;
120
+ cost: TaskRunCost;
121
+ }
122
+
123
+ /** A serializable point-in-time view of the run log, for persistence. */
124
+ export interface RunLogSnapshot {
125
+ stats: Record<string, TaskStats>;
126
+ recent: TaskRunRecord[];
127
+ cumulative: TaskRunCost;
128
+ }
129
+
130
+ /** Injected persistence for the run log. */
131
+ export interface RunLogIO {
132
+ load(): Promise<RunLogSnapshot | null>;
133
+ save(snapshot: RunLogSnapshot): Promise<void>;
134
+ }
135
+
136
+ // ── Scheduler ──────────────────────────────────────────────────────────────
137
+
138
+ /** The result of running a task once. The host's `run` callback returns this. */
139
+ export interface TaskRunOutcome {
140
+ ok: boolean;
141
+ text?: string;
142
+ toolCalls?: number;
143
+ error?: string;
144
+ cost?: Partial<TaskRunCost>;
145
+ }
146
+
147
+ /** Host-provided runner — typically wraps `Funnel.runTurn(buildTaskPrompt(task))`. */
148
+ export type RunTask = (task: AgentTask) => Promise<TaskRunOutcome>;
149
+
150
+ /** Opaque timer handle so the scheduler stays platform-agnostic. */
151
+ export type TimerHandle = unknown;
152
+
153
+ export interface SchedulerOptions {
154
+ store: TaskStore;
155
+ /** Runs a task and resolves with its outcome. Errors are caught by the scheduler. */
156
+ run: RunTask;
157
+ /** Notified after every run (success or error) — wire to a {@link TaskRunLog}. */
158
+ onOutcome?: (task: AgentTask, outcome: TaskRunOutcome, durationMs: number) => void;
159
+ /** Diagnostics sink. Default: silent. */
160
+ log?: (msg: string) => void;
161
+ /** Injectable clock. Default: Date.now. */
162
+ now?: () => number;
163
+ /** How often `start()` evaluates due tasks, ms. Default 30_000. */
164
+ tickMs?: number;
165
+ /** Max tasks running at once. Default 1 (serial — safest for a wallet). */
166
+ concurrency?: number;
167
+ /** Injectable timer. Default: setInterval. */
168
+ setTimer?: (fn: () => void, ms: number) => TimerHandle;
169
+ /** Injectable timer-clear. Default: clearInterval. */
170
+ clearTimer?: (handle: TimerHandle) => void;
171
+ }
172
+
173
+ export interface TaskScheduler {
174
+ /** Begin firing due tasks; runs `runOnStartup` tasks immediately. Idempotent. */
175
+ start(): void;
176
+ /** Stop firing. In-flight runs finish; no new ones start. */
177
+ stop(): void;
178
+ /** Evaluate all tasks once and run those that are due. Safe to call directly (tests). */
179
+ tick(): Promise<void>;
180
+ /** Force-run a task now regardless of schedule/enabled. Null if unknown/already running. */
181
+ runNow(id: string): Promise<TaskRunOutcome | null>;
182
+ /** Ids of tasks currently running. */
183
+ active(): string[];
184
+ /** Whether `start()` is in effect. */
185
+ isRunning(): boolean;
186
+ }