@hasna/todos 0.10.21 → 0.11.1

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/dist/mcp/index.js CHANGED
@@ -4,37 +4,27 @@ var __defProp = Object.defineProperty;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- function __accessProp(key) {
8
- return this[key];
9
- }
7
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
10
8
  var __toCommonJS = (from) => {
11
- var entry = (__moduleCache ??= new WeakMap).get(from), desc;
9
+ var entry = __moduleCache.get(from), desc;
12
10
  if (entry)
13
11
  return entry;
14
12
  entry = __defProp({}, "__esModule", { value: true });
15
- if (from && typeof from === "object" || typeof from === "function") {
16
- for (var key of __getOwnPropNames(from))
17
- if (!__hasOwnProp.call(entry, key))
18
- __defProp(entry, key, {
19
- get: __accessProp.bind(from, key),
20
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
- });
22
- }
13
+ if (from && typeof from === "object" || typeof from === "function")
14
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
15
+ get: () => from[key],
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ }));
23
18
  __moduleCache.set(from, entry);
24
19
  return entry;
25
20
  };
26
- var __moduleCache;
27
- var __returnValue = (v) => v;
28
- function __exportSetter(name, newValue) {
29
- this[name] = __returnValue.bind(null, newValue);
30
- }
31
21
  var __export = (target, all) => {
32
22
  for (var name in all)
33
23
  __defProp(target, name, {
34
24
  get: all[name],
35
25
  enumerable: true,
36
26
  configurable: true,
37
- set: __exportSetter.bind(all, name)
27
+ set: (newValue) => all[name] = () => newValue
38
28
  });
39
29
  };
40
30
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -392,6 +382,15 @@ function ensureSchema(db) {
392
382
  ensureColumn("tasks", "assigned_by", "TEXT");
393
383
  ensureColumn("tasks", "assigned_from_project", "TEXT");
394
384
  ensureColumn("tasks", "started_at", "TEXT");
385
+ ensureColumn("tasks", "task_type", "TEXT");
386
+ ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
387
+ ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
388
+ ensureColumn("tasks", "delegated_from", "TEXT");
389
+ ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
390
+ ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
391
+ ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
392
+ ensureColumn("tasks", "retry_after", "TEXT");
393
+ ensureColumn("tasks", "sla_minutes", "INTEGER");
395
394
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
396
395
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
397
396
  ensureColumn("agents", "reports_to", "TEXT");
@@ -1005,6 +1004,58 @@ var init_database = __esm(() => {
1005
1004
  `
1006
1005
  ALTER TABLE tasks ADD COLUMN task_type TEXT;
1007
1006
  CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
1007
+ ALTER TABLE tasks ADD COLUMN cost_tokens INTEGER DEFAULT 0;
1008
+ ALTER TABLE tasks ADD COLUMN cost_usd REAL DEFAULT 0;
1009
+ ALTER TABLE tasks ADD COLUMN delegated_from TEXT;
1010
+ ALTER TABLE tasks ADD COLUMN delegation_depth INTEGER DEFAULT 0;
1011
+ ALTER TABLE tasks ADD COLUMN retry_count INTEGER DEFAULT 0;
1012
+ ALTER TABLE tasks ADD COLUMN max_retries INTEGER DEFAULT 3;
1013
+ ALTER TABLE tasks ADD COLUMN retry_after TEXT;
1014
+ ALTER TABLE tasks ADD COLUMN sla_minutes INTEGER;
1015
+
1016
+ CREATE TABLE IF NOT EXISTS task_traces (
1017
+ id TEXT PRIMARY KEY,
1018
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1019
+ agent_id TEXT,
1020
+ trace_type TEXT NOT NULL CHECK(trace_type IN ('tool_call','llm_call','error','handoff','custom')),
1021
+ name TEXT,
1022
+ input_summary TEXT,
1023
+ output_summary TEXT,
1024
+ duration_ms INTEGER,
1025
+ tokens INTEGER,
1026
+ cost_usd REAL,
1027
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1028
+ );
1029
+ CREATE INDEX IF NOT EXISTS idx_task_traces_task ON task_traces(task_id);
1030
+ CREATE INDEX IF NOT EXISTS idx_task_traces_agent ON task_traces(agent_id);
1031
+
1032
+ CREATE TABLE IF NOT EXISTS context_snapshots (
1033
+ id TEXT PRIMARY KEY,
1034
+ agent_id TEXT,
1035
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1036
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1037
+ snapshot_type TEXT NOT NULL CHECK(snapshot_type IN ('interrupt','complete','handoff','checkpoint')),
1038
+ plan_summary TEXT,
1039
+ files_open TEXT DEFAULT '[]',
1040
+ attempts TEXT DEFAULT '[]',
1041
+ blockers TEXT DEFAULT '[]',
1042
+ next_steps TEXT,
1043
+ metadata TEXT DEFAULT '{}',
1044
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1045
+ );
1046
+ CREATE INDEX IF NOT EXISTS idx_snapshots_agent ON context_snapshots(agent_id);
1047
+ CREATE INDEX IF NOT EXISTS idx_snapshots_task ON context_snapshots(task_id);
1048
+
1049
+ CREATE TABLE IF NOT EXISTS agent_budgets (
1050
+ agent_id TEXT PRIMARY KEY,
1051
+ max_concurrent INTEGER DEFAULT 5,
1052
+ max_cost_usd REAL,
1053
+ max_task_minutes INTEGER,
1054
+ period_hours INTEGER DEFAULT 24,
1055
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1056
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1057
+ );
1058
+
1008
1059
  INSERT OR IGNORE INTO _migrations (id) VALUES (35);
1009
1060
  `
1010
1061
  ];
@@ -1601,12 +1652,14 @@ var exports_tasks = {};
1601
1652
  __export(exports_tasks, {
1602
1653
  updateTask: () => updateTask,
1603
1654
  unlockTask: () => unlockTask,
1655
+ stealTask: () => stealTask,
1604
1656
  startTask: () => startTask,
1605
1657
  setTaskStatus: () => setTaskStatus,
1606
1658
  setTaskPriority: () => setTaskPriority,
1607
1659
  removeDependency: () => removeDependency,
1608
1660
  redistributeStaleTasks: () => redistributeStaleTasks,
1609
1661
  moveTask: () => moveTask,
1662
+ logCost: () => logCost,
1610
1663
  lockTask: () => lockTask,
1611
1664
  listTasks: () => listTasks,
1612
1665
  getTasksChangedSince: () => getTasksChangedSince,
@@ -1629,6 +1682,7 @@ __export(exports_tasks, {
1629
1682
  countTasks: () => countTasks,
1630
1683
  completeTask: () => completeTask,
1631
1684
  cloneTask: () => cloneTask,
1685
+ claimOrSteal: () => claimOrSteal,
1632
1686
  claimNextTask: () => claimNextTask,
1633
1687
  bulkUpdateTasks: () => bulkUpdateTasks,
1634
1688
  bulkCreateTasks: () => bulkCreateTasks,
@@ -2110,6 +2164,20 @@ function completeTask(id, agentId, db, options) {
2110
2164
  if (spawnedFromTemplate) {
2111
2165
  meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
2112
2166
  }
2167
+ const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
2168
+ JOIN task_dependencies td ON td.task_id = t.id
2169
+ WHERE td.depends_on = ? AND t.status = 'pending'
2170
+ AND NOT EXISTS (
2171
+ SELECT 1 FROM task_dependencies td2
2172
+ JOIN tasks dep2 ON dep2.id = td2.depends_on
2173
+ WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
2174
+ )`).all(id, id);
2175
+ if (unblockedDeps.length > 0) {
2176
+ meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
2177
+ for (const dep of unblockedDeps) {
2178
+ dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
2179
+ }
2180
+ }
2113
2181
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
2114
2182
  }
2115
2183
  function lockTask(id, agentId, db) {
@@ -2406,24 +2474,36 @@ function failTask(id, agentId, reason, options, db) {
2406
2474
  };
2407
2475
  let retryTask;
2408
2476
  if (options?.retry) {
2409
- let title = task.title;
2410
- if (task.short_id && title.startsWith(task.short_id + ": ")) {
2411
- title = title.slice(task.short_id.length + 2);
2477
+ const retryCount = (task.retry_count || 0) + 1;
2478
+ const maxRetries = task.max_retries || 3;
2479
+ if (retryCount > maxRetries) {
2480
+ d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
2481
+ JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
2482
+ id
2483
+ ]);
2484
+ } else {
2485
+ const backoffMinutes = Math.pow(5, retryCount - 1);
2486
+ const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
2487
+ let title = task.title;
2488
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
2489
+ title = title.slice(task.short_id.length + 2);
2490
+ }
2491
+ retryTask = createTask({
2492
+ title,
2493
+ description: task.description ?? undefined,
2494
+ priority: task.priority,
2495
+ project_id: task.project_id ?? undefined,
2496
+ task_list_id: task.task_list_id ?? undefined,
2497
+ plan_id: task.plan_id ?? undefined,
2498
+ assigned_to: task.assigned_to ?? undefined,
2499
+ tags: task.tags,
2500
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
2501
+ estimated_minutes: task.estimated_minutes ?? undefined,
2502
+ recurrence_rule: task.recurrence_rule ?? undefined,
2503
+ due_at: retryAfter
2504
+ }, d);
2505
+ d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
2412
2506
  }
2413
- retryTask = createTask({
2414
- title,
2415
- description: task.description ?? undefined,
2416
- priority: task.priority,
2417
- project_id: task.project_id ?? undefined,
2418
- task_list_id: task.task_list_id ?? undefined,
2419
- plan_id: task.plan_id ?? undefined,
2420
- assigned_to: task.assigned_to ?? undefined,
2421
- tags: task.tags,
2422
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
2423
- estimated_minutes: task.estimated_minutes ?? undefined,
2424
- recurrence_rule: task.recurrence_rule ?? undefined,
2425
- due_at: options.retry_after || task.due_at || undefined
2426
- }, d);
2427
2507
  }
2428
2508
  return { task: failedTask, retryTask };
2429
2509
  }
@@ -2447,6 +2527,40 @@ function getStaleTasks(staleMinutes = 30, filters, db) {
2447
2527
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
2448
2528
  return rows.map(rowToTask);
2449
2529
  }
2530
+ function logCost(taskId, tokens, usd, db) {
2531
+ const d = db || getDatabase();
2532
+ d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
2533
+ }
2534
+ function stealTask(agentId, opts, db) {
2535
+ const d = db || getDatabase();
2536
+ const staleMinutes = opts?.stale_minutes ?? 30;
2537
+ const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
2538
+ if (staleTasks.length === 0)
2539
+ return null;
2540
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
2541
+ staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
2542
+ const target = staleTasks[0];
2543
+ const timestamp = now();
2544
+ d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ?`, [agentId, agentId, timestamp, timestamp, target.id]);
2545
+ logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
2546
+ dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
2547
+ return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
2548
+ }
2549
+ function claimOrSteal(agentId, filters, db) {
2550
+ const d = db || getDatabase();
2551
+ const tx = d.transaction(() => {
2552
+ const next = getNextTask(agentId, filters, d);
2553
+ if (next) {
2554
+ const started = startTask(next.id, agentId, d);
2555
+ return { task: started, stolen: false };
2556
+ }
2557
+ const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
2558
+ if (stolen)
2559
+ return { task: stolen, stolen: true };
2560
+ return null;
2561
+ });
2562
+ return tx();
2563
+ }
2450
2564
  function getStatus(filters, agentId, options, db) {
2451
2565
  const d = db || getDatabase();
2452
2566
  const pending = countTasks({ ...filters, status: "pending" }, d);
@@ -3042,6 +3156,219 @@ var init_task_commits = __esm(() => {
3042
3156
  init_database();
3043
3157
  });
3044
3158
 
3159
+ // src/db/traces.ts
3160
+ var exports_traces = {};
3161
+ __export(exports_traces, {
3162
+ logTrace: () => logTrace,
3163
+ getTraceStats: () => getTraceStats,
3164
+ getTaskTraces: () => getTaskTraces
3165
+ });
3166
+ function logTrace(input, db) {
3167
+ const d = db || getDatabase();
3168
+ const id = uuid();
3169
+ const timestamp = now();
3170
+ d.run(`INSERT INTO task_traces (id, task_id, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd, created_at)
3171
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3172
+ id,
3173
+ input.task_id,
3174
+ input.agent_id || null,
3175
+ input.trace_type,
3176
+ input.name || null,
3177
+ input.input_summary || null,
3178
+ input.output_summary || null,
3179
+ input.duration_ms ?? null,
3180
+ input.tokens ?? null,
3181
+ input.cost_usd ?? null,
3182
+ timestamp
3183
+ ]);
3184
+ return {
3185
+ id,
3186
+ task_id: input.task_id,
3187
+ agent_id: input.agent_id || null,
3188
+ trace_type: input.trace_type,
3189
+ name: input.name || null,
3190
+ input_summary: input.input_summary || null,
3191
+ output_summary: input.output_summary || null,
3192
+ duration_ms: input.duration_ms ?? null,
3193
+ tokens: input.tokens ?? null,
3194
+ cost_usd: input.cost_usd ?? null,
3195
+ created_at: timestamp
3196
+ };
3197
+ }
3198
+ function getTaskTraces(taskId, db) {
3199
+ const d = db || getDatabase();
3200
+ return d.query("SELECT * FROM task_traces WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
3201
+ }
3202
+ function getTraceStats(taskId, db) {
3203
+ const d = db || getDatabase();
3204
+ const row = d.query(`SELECT COUNT(*) as total,
3205
+ SUM(CASE WHEN trace_type = 'tool_call' THEN 1 ELSE 0 END) as tool_calls,
3206
+ SUM(CASE WHEN trace_type = 'llm_call' THEN 1 ELSE 0 END) as llm_calls,
3207
+ SUM(CASE WHEN trace_type = 'error' THEN 1 ELSE 0 END) as errors,
3208
+ COALESCE(SUM(tokens), 0) as total_tokens,
3209
+ COALESCE(SUM(cost_usd), 0) as total_cost_usd,
3210
+ COALESCE(SUM(duration_ms), 0) as total_duration_ms
3211
+ FROM task_traces WHERE task_id = ?`).get(taskId);
3212
+ return row;
3213
+ }
3214
+ var init_traces = __esm(() => {
3215
+ init_database();
3216
+ });
3217
+
3218
+ // src/db/snapshots.ts
3219
+ var exports_snapshots = {};
3220
+ __export(exports_snapshots, {
3221
+ saveSnapshot: () => saveSnapshot,
3222
+ listSnapshots: () => listSnapshots,
3223
+ getLatestSnapshot: () => getLatestSnapshot
3224
+ });
3225
+ function rowToSnapshot(row) {
3226
+ return {
3227
+ ...row,
3228
+ snapshot_type: row.snapshot_type,
3229
+ files_open: JSON.parse(row.files_open || "[]"),
3230
+ attempts: JSON.parse(row.attempts || "[]"),
3231
+ blockers: JSON.parse(row.blockers || "[]"),
3232
+ metadata: JSON.parse(row.metadata || "{}")
3233
+ };
3234
+ }
3235
+ function saveSnapshot(input, db) {
3236
+ const d = db || getDatabase();
3237
+ const id = uuid();
3238
+ const timestamp = now();
3239
+ d.run(`INSERT INTO context_snapshots (id, agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps, metadata, created_at)
3240
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3241
+ id,
3242
+ input.agent_id || null,
3243
+ input.task_id || null,
3244
+ input.project_id || null,
3245
+ input.snapshot_type,
3246
+ input.plan_summary || null,
3247
+ JSON.stringify(input.files_open || []),
3248
+ JSON.stringify(input.attempts || []),
3249
+ JSON.stringify(input.blockers || []),
3250
+ input.next_steps || null,
3251
+ JSON.stringify(input.metadata || {}),
3252
+ timestamp
3253
+ ]);
3254
+ return {
3255
+ id,
3256
+ agent_id: input.agent_id || null,
3257
+ task_id: input.task_id || null,
3258
+ project_id: input.project_id || null,
3259
+ snapshot_type: input.snapshot_type,
3260
+ plan_summary: input.plan_summary || null,
3261
+ files_open: input.files_open || [],
3262
+ attempts: input.attempts || [],
3263
+ blockers: input.blockers || [],
3264
+ next_steps: input.next_steps || null,
3265
+ metadata: input.metadata || {},
3266
+ created_at: timestamp
3267
+ };
3268
+ }
3269
+ function getLatestSnapshot(agentId, taskId, db) {
3270
+ const d = db || getDatabase();
3271
+ const conditions = [];
3272
+ const params = [];
3273
+ if (agentId) {
3274
+ conditions.push("agent_id = ?");
3275
+ params.push(agentId);
3276
+ }
3277
+ if (taskId) {
3278
+ conditions.push("task_id = ?");
3279
+ params.push(taskId);
3280
+ }
3281
+ if (conditions.length === 0)
3282
+ return null;
3283
+ const where = conditions.join(" AND ");
3284
+ const row = d.query(`SELECT * FROM context_snapshots WHERE ${where} ORDER BY created_at DESC LIMIT 1`).get(...params);
3285
+ return row ? rowToSnapshot(row) : null;
3286
+ }
3287
+ function listSnapshots(opts, db) {
3288
+ const d = db || getDatabase();
3289
+ const conditions = [];
3290
+ const params = [];
3291
+ if (opts.agent_id) {
3292
+ conditions.push("agent_id = ?");
3293
+ params.push(opts.agent_id);
3294
+ }
3295
+ if (opts.task_id) {
3296
+ conditions.push("task_id = ?");
3297
+ params.push(opts.task_id);
3298
+ }
3299
+ if (opts.project_id) {
3300
+ conditions.push("project_id = ?");
3301
+ params.push(opts.project_id);
3302
+ }
3303
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3304
+ const limit = opts.limit || 20;
3305
+ params.push(limit);
3306
+ return d.query(`SELECT * FROM context_snapshots ${where} ORDER BY created_at DESC LIMIT ?`).all(...params).map(rowToSnapshot);
3307
+ }
3308
+ var init_snapshots = __esm(() => {
3309
+ init_database();
3310
+ });
3311
+
3312
+ // src/db/budgets.ts
3313
+ var exports_budgets = {};
3314
+ __export(exports_budgets, {
3315
+ setBudget: () => setBudget,
3316
+ getBudget: () => getBudget,
3317
+ checkBudget: () => checkBudget
3318
+ });
3319
+ function setBudget(agentId, opts, db) {
3320
+ const d = db || getDatabase();
3321
+ const timestamp = now();
3322
+ d.run(`INSERT INTO agent_budgets (agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours, created_at, updated_at)
3323
+ VALUES (?, ?, ?, ?, ?, ?, ?)
3324
+ ON CONFLICT(agent_id) DO UPDATE SET
3325
+ max_concurrent = COALESCE(?, max_concurrent),
3326
+ max_cost_usd = COALESCE(?, max_cost_usd),
3327
+ max_task_minutes = COALESCE(?, max_task_minutes),
3328
+ period_hours = COALESCE(?, period_hours),
3329
+ updated_at = ?`, [
3330
+ agentId,
3331
+ opts.max_concurrent ?? 5,
3332
+ opts.max_cost_usd ?? null,
3333
+ opts.max_task_minutes ?? null,
3334
+ opts.period_hours ?? 24,
3335
+ timestamp,
3336
+ timestamp,
3337
+ opts.max_concurrent ?? null,
3338
+ opts.max_cost_usd ?? null,
3339
+ opts.max_task_minutes ?? null,
3340
+ opts.period_hours ?? null,
3341
+ timestamp
3342
+ ]);
3343
+ return getBudget(agentId, d);
3344
+ }
3345
+ function getBudget(agentId, db) {
3346
+ const d = db || getDatabase();
3347
+ return d.query("SELECT * FROM agent_budgets WHERE agent_id = ?").get(agentId);
3348
+ }
3349
+ function checkBudget(agentId, db) {
3350
+ const d = db || getDatabase();
3351
+ const budget = getBudget(agentId, d);
3352
+ if (!budget)
3353
+ return { allowed: true, current_concurrent: 0, max_concurrent: 999 };
3354
+ const concurrent = countTasks({ status: "in_progress", assigned_to: agentId }, d);
3355
+ if (concurrent >= budget.max_concurrent) {
3356
+ return { allowed: false, reason: `Concurrent limit reached (${concurrent}/${budget.max_concurrent})`, current_concurrent: concurrent, max_concurrent: budget.max_concurrent };
3357
+ }
3358
+ if (budget.max_cost_usd != null) {
3359
+ const periodStart = new Date(Date.now() - budget.period_hours * 60 * 60 * 1000).toISOString();
3360
+ const costRow = d.query("SELECT COALESCE(SUM(cost_usd), 0) as total FROM tasks WHERE (assigned_to = ? OR agent_id = ?) AND updated_at > ?").get(agentId, agentId, periodStart);
3361
+ if (costRow.total >= budget.max_cost_usd) {
3362
+ return { allowed: false, reason: `Cost limit reached ($${costRow.total.toFixed(2)}/$${budget.max_cost_usd.toFixed(2)} in ${budget.period_hours}h)`, current_concurrent: concurrent, max_concurrent: budget.max_concurrent, current_cost_usd: costRow.total, max_cost_usd: budget.max_cost_usd };
3363
+ }
3364
+ }
3365
+ return { allowed: true, current_concurrent: concurrent, max_concurrent: budget.max_concurrent };
3366
+ }
3367
+ var init_budgets = __esm(() => {
3368
+ init_database();
3369
+ init_tasks();
3370
+ });
3371
+
3045
3372
  // src/lib/github.ts
3046
3373
  var exports_github = {};
3047
3374
  __export(exports_github, {
@@ -10464,6 +10791,33 @@ ID: ${updated.id}`
10464
10791
  }
10465
10792
  });
10466
10793
  }
10794
+ if (shouldRegisterTool("update_agent")) {
10795
+ server.tool("update_agent", "Update an agent's description, role, title, or other metadata. Resolve by id or name.", {
10796
+ id: exports_external.string().optional(),
10797
+ name: exports_external.string().optional(),
10798
+ description: exports_external.string().optional(),
10799
+ role: exports_external.string().optional(),
10800
+ title: exports_external.string().optional(),
10801
+ level: exports_external.string().optional(),
10802
+ capabilities: exports_external.array(exports_external.string()).optional(),
10803
+ permissions: exports_external.array(exports_external.string()).optional(),
10804
+ metadata: exports_external.record(exports_external.unknown()).optional()
10805
+ }, async ({ id, name, ...updates }) => {
10806
+ try {
10807
+ if (!id && !name) {
10808
+ return { content: [{ type: "text", text: "Provide either id or name." }], isError: true };
10809
+ }
10810
+ const agent = id ? getAgent(id) : getAgentByName(name);
10811
+ if (!agent) {
10812
+ return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
10813
+ }
10814
+ const updated = updateAgent(agent.id, updates);
10815
+ return { content: [{ type: "text", text: `Agent updated: ${updated.name} (${updated.id.slice(0, 8)})` }] };
10816
+ } catch (e) {
10817
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
10818
+ }
10819
+ });
10820
+ }
10467
10821
  if (shouldRegisterTool("delete_agent")) {
10468
10822
  server.tool("delete_agent", "Archive an agent (soft delete). The agent is hidden from list_agents but preserved for task history. Use unarchive_agent to restore. Resolve by id or name.", {
10469
10823
  id: exports_external.string().optional(),
@@ -10822,6 +11176,135 @@ Blocked:`);
10822
11176
  }
10823
11177
  });
10824
11178
  }
11179
+ if (shouldRegisterTool("log_cost")) {
11180
+ server.tool("log_cost", "Log token usage and cost to a task. Accumulates \u2014 call after each LLM invocation.", { task_id: exports_external.string(), tokens: exports_external.number().describe("Token count"), usd: exports_external.number().describe("Cost in USD") }, async ({ task_id, tokens, usd }) => {
11181
+ try {
11182
+ const { logCost: logCost2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
11183
+ const resolvedId = resolveId(task_id, "tasks");
11184
+ logCost2(resolvedId, tokens, usd);
11185
+ return { content: [{ type: "text", text: `Logged ${tokens} tokens ($${usd.toFixed(4)}) to ${task_id}` }] };
11186
+ } catch (e) {
11187
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11188
+ }
11189
+ });
11190
+ }
11191
+ if (shouldRegisterTool("log_trace")) {
11192
+ server.tool("log_trace", "Log a trace entry (tool call, LLM call, error, handoff) to a task for observability.", {
11193
+ task_id: exports_external.string(),
11194
+ agent_id: exports_external.string().optional(),
11195
+ trace_type: exports_external.enum(["tool_call", "llm_call", "error", "handoff", "custom"]),
11196
+ name: exports_external.string().optional(),
11197
+ input_summary: exports_external.string().optional(),
11198
+ output_summary: exports_external.string().optional(),
11199
+ duration_ms: exports_external.number().optional(),
11200
+ tokens: exports_external.number().optional(),
11201
+ cost_usd: exports_external.number().optional()
11202
+ }, async ({ task_id, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd }) => {
11203
+ try {
11204
+ const { logTrace: logTrace2 } = await Promise.resolve().then(() => (init_traces(), exports_traces));
11205
+ const resolvedId = resolveId(task_id, "tasks");
11206
+ const trace = logTrace2({ task_id: resolvedId, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd });
11207
+ return { content: [{ type: "text", text: `Trace logged: ${trace.id} [${trace_type}] ${name || ""}` }] };
11208
+ } catch (e) {
11209
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11210
+ }
11211
+ });
11212
+ }
11213
+ if (shouldRegisterTool("get_traces")) {
11214
+ server.tool("get_traces", "Get execution traces for a task \u2014 tool calls, LLM invocations, errors, handoffs.", { task_id: exports_external.string() }, async ({ task_id }) => {
11215
+ try {
11216
+ const { getTaskTraces: getTaskTraces2, getTraceStats: getTraceStats2 } = await Promise.resolve().then(() => (init_traces(), exports_traces));
11217
+ const resolvedId = resolveId(task_id, "tasks");
11218
+ const traces = getTaskTraces2(resolvedId);
11219
+ const stats = getTraceStats2(resolvedId);
11220
+ const lines = [`Traces for ${task_id}: ${stats.total} total (${stats.tool_calls} tools, ${stats.llm_calls} LLM, ${stats.errors} errors) | ${stats.total_tokens} tokens | $${stats.total_cost_usd.toFixed(4)} | ${stats.total_duration_ms}ms`];
11221
+ for (const t of traces.slice(0, 20)) {
11222
+ lines.push(` ${t.created_at} [${t.trace_type}] ${t.name || ""} ${t.tokens ? t.tokens + "tok" : ""} ${t.duration_ms ? t.duration_ms + "ms" : ""}`);
11223
+ }
11224
+ return { content: [{ type: "text", text: lines.join(`
11225
+ `) }] };
11226
+ } catch (e) {
11227
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11228
+ }
11229
+ });
11230
+ }
11231
+ if (shouldRegisterTool("save_snapshot")) {
11232
+ server.tool("save_snapshot", "Save a structured context snapshot \u2014 what you were working on, what files are open, what was tried, blockers, next steps. Call on session end or before handoff.", {
11233
+ agent_id: exports_external.string().optional(),
11234
+ task_id: exports_external.string().optional(),
11235
+ project_id: exports_external.string().optional(),
11236
+ snapshot_type: exports_external.enum(["interrupt", "complete", "handoff", "checkpoint"]),
11237
+ plan_summary: exports_external.string().optional(),
11238
+ files_open: exports_external.array(exports_external.string()).optional(),
11239
+ attempts: exports_external.array(exports_external.string()).optional(),
11240
+ blockers: exports_external.array(exports_external.string()).optional(),
11241
+ next_steps: exports_external.string().optional()
11242
+ }, async ({ agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps }) => {
11243
+ try {
11244
+ const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshots(), exports_snapshots));
11245
+ const snap = saveSnapshot2({ agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps });
11246
+ return { content: [{ type: "text", text: `Snapshot saved: ${snap.id} [${snapshot_type}]${plan_summary ? " \u2014 " + plan_summary.slice(0, 80) : ""}` }] };
11247
+ } catch (e) {
11248
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11249
+ }
11250
+ });
11251
+ }
11252
+ if (shouldRegisterTool("get_snapshot")) {
11253
+ server.tool("get_snapshot", "Get the latest context snapshot for an agent or task \u2014 use to resume work after interruption.", { agent_id: exports_external.string().optional(), task_id: exports_external.string().optional() }, async ({ agent_id, task_id }) => {
11254
+ try {
11255
+ const { getLatestSnapshot: getLatestSnapshot2 } = await Promise.resolve().then(() => (init_snapshots(), exports_snapshots));
11256
+ const snap = getLatestSnapshot2(agent_id, task_id);
11257
+ if (!snap)
11258
+ return { content: [{ type: "text", text: "No snapshot found." }] };
11259
+ const lines = [`Snapshot [${snap.snapshot_type}] from ${snap.created_at}`];
11260
+ if (snap.plan_summary)
11261
+ lines.push(`Plan: ${snap.plan_summary}`);
11262
+ if (snap.files_open.length > 0)
11263
+ lines.push(`Files: ${snap.files_open.join(", ")}`);
11264
+ if (snap.blockers.length > 0)
11265
+ lines.push(`Blockers: ${snap.blockers.join(", ")}`);
11266
+ if (snap.next_steps)
11267
+ lines.push(`Next: ${snap.next_steps}`);
11268
+ if (snap.attempts.length > 0)
11269
+ lines.push(`Attempts: ${snap.attempts.join("; ")}`);
11270
+ return { content: [{ type: "text", text: lines.join(`
11271
+ `) }] };
11272
+ } catch (e) {
11273
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11274
+ }
11275
+ });
11276
+ }
11277
+ if (shouldRegisterTool("set_budget")) {
11278
+ server.tool("set_budget", "Set execution budget for an agent \u2014 max concurrent tasks, cost limit, time limit per period.", {
11279
+ agent_id: exports_external.string(),
11280
+ max_concurrent: exports_external.number().optional(),
11281
+ max_cost_usd: exports_external.number().optional(),
11282
+ max_task_minutes: exports_external.number().optional(),
11283
+ period_hours: exports_external.number().optional()
11284
+ }, async ({ agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours }) => {
11285
+ try {
11286
+ const { setBudget: setBudget2 } = await Promise.resolve().then(() => (init_budgets(), exports_budgets));
11287
+ const budget = setBudget2(agent_id, { max_concurrent, max_cost_usd, max_task_minutes, period_hours });
11288
+ return { content: [{ type: "text", text: `Budget set for ${agent_id}: max ${budget.max_concurrent} concurrent, $${budget.max_cost_usd ?? "\u221E"} cost limit, ${budget.period_hours}h period` }] };
11289
+ } catch (e) {
11290
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11291
+ }
11292
+ });
11293
+ }
11294
+ if (shouldRegisterTool("check_budget")) {
11295
+ server.tool("check_budget", "Check if an agent is within their execution budget (concurrent tasks, cost, time).", { agent_id: exports_external.string() }, async ({ agent_id }) => {
11296
+ try {
11297
+ const { checkBudget: checkBudget2 } = await Promise.resolve().then(() => (init_budgets(), exports_budgets));
11298
+ const result = checkBudget2(agent_id);
11299
+ if (result.allowed) {
11300
+ return { content: [{ type: "text", text: `Budget OK: ${result.current_concurrent}/${result.max_concurrent} concurrent tasks` }] };
11301
+ }
11302
+ return { content: [{ type: "text", text: `BUDGET EXCEEDED: ${result.reason}` }], isError: true };
11303
+ } catch (e) {
11304
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11305
+ }
11306
+ });
11307
+ }
10825
11308
  if (shouldRegisterTool("import_github_issue")) {
10826
11309
  server.tool("import_github_issue", "Import a GitHub issue as a task. Requires gh CLI installed and authenticated.", {
10827
11310
  url: exports_external.string().describe("GitHub issue URL (e.g. https://github.com/owner/repo/issues/42)"),
@@ -11532,6 +12015,56 @@ ${formatTaskDetail(task, 300)}` }] };
11532
12015
  }
11533
12016
  });
11534
12017
  }
12018
+ if (shouldRegisterTool("steal_task")) {
12019
+ server.tool("steal_task", "Work-stealing: take the highest-priority stale in_progress task from another agent and reassign it to you.", {
12020
+ agent_id: exports_external.string().describe("Your agent ID"),
12021
+ stale_minutes: exports_external.number().optional().describe("How long a task must be stale before stealing (default: 30)"),
12022
+ project_id: exports_external.string().optional(),
12023
+ task_list_id: exports_external.string().optional()
12024
+ }, async ({ agent_id, stale_minutes, project_id, task_list_id }) => {
12025
+ try {
12026
+ const task = stealTask(agent_id, { stale_minutes, project_id, task_list_id });
12027
+ if (!task)
12028
+ return { content: [{ type: "text", text: "No stale tasks available to steal." }] };
12029
+ return { content: [{ type: "text", text: `Stolen: ${formatTask(task)}
12030
+ Previous owner: ${task.metadata?._stolen_from || "unknown"}` }] };
12031
+ } catch (e) {
12032
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12033
+ }
12034
+ });
12035
+ }
12036
+ if (shouldRegisterTool("claim_or_steal")) {
12037
+ server.tool("claim_or_steal", "Try to claim a pending task first; if none available, steal from a stale agent. Best single call for getting work.", {
12038
+ agent_id: exports_external.string(),
12039
+ project_id: exports_external.string().optional(),
12040
+ task_list_id: exports_external.string().optional(),
12041
+ plan_id: exports_external.string().optional(),
12042
+ tags: exports_external.array(exports_external.string()).optional(),
12043
+ stale_minutes: exports_external.number().optional().describe("Stale threshold for work-stealing fallback (default: 30)")
12044
+ }, async ({ agent_id, project_id, task_list_id, plan_id, tags, stale_minutes }) => {
12045
+ try {
12046
+ const filters = {};
12047
+ if (project_id)
12048
+ filters.project_id = resolveId(project_id, "projects");
12049
+ if (task_list_id)
12050
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
12051
+ if (plan_id)
12052
+ filters.plan_id = resolveId(plan_id, "plans");
12053
+ if (tags)
12054
+ filters.tags = tags;
12055
+ if (stale_minutes)
12056
+ filters.stale_minutes = stale_minutes;
12057
+ const result = claimOrSteal(agent_id, Object.keys(filters).length > 0 ? filters : undefined);
12058
+ if (!result)
12059
+ return { content: [{ type: "text", text: "No tasks available to claim or steal." }] };
12060
+ const prefix = result.stolen ? "Stolen" : "Claimed";
12061
+ return { content: [{ type: "text", text: `${prefix}: ${formatTask(result.task)}
12062
+ ${formatTaskDetail(result.task, 300)}` }] };
12063
+ } catch (e) {
12064
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12065
+ }
12066
+ });
12067
+ }
11535
12068
  if (shouldRegisterTool("get_stale_tasks")) {
11536
12069
  server.tool("get_stale_tasks", "Find stale in_progress tasks with no recent activity.", {
11537
12070
  stale_minutes: exports_external.number().optional(),