heyio 1.2.4 → 1.4.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 (69) hide show
  1. package/dist/api/server.js +289 -12
  2. package/dist/config.js +6 -0
  3. package/dist/copilot/agents.js +100 -5
  4. package/dist/copilot/ceremonies.js +12 -2
  5. package/dist/copilot/io-scheduler.js +9 -1
  6. package/dist/copilot/orchestrator.js +4 -0
  7. package/dist/copilot/scheduler.js +7 -2
  8. package/dist/copilot/skills.js +138 -6
  9. package/dist/copilot/squad-tools.js +102 -0
  10. package/dist/copilot/system-message.js +2 -1
  11. package/dist/copilot/token-tracker.js +89 -0
  12. package/dist/copilot/tools.js +27 -5
  13. package/dist/copilot/trigger-schedule.js +31 -0
  14. package/dist/paths.js +1 -0
  15. package/dist/store/agent-events.js +19 -0
  16. package/dist/store/audit-log.js +71 -0
  17. package/dist/store/conversations.js +150 -0
  18. package/dist/store/db.js +111 -0
  19. package/dist/store/schedules.js +9 -1
  20. package/dist/store/squad-colors.js +23 -0
  21. package/dist/store/squads.js +6 -1
  22. package/dist/store/tasks.js +43 -0
  23. package/dist/store/token-usage.js +94 -0
  24. package/dist/wiki/backlinks.js +51 -0
  25. package/dist/wiki/fs.js +63 -1
  26. package/dist/wiki/search.js +13 -2
  27. package/package.json +1 -1
  28. package/web-dist/assets/AuditLogView-DqxVzjd_.js +6 -0
  29. package/web-dist/assets/ChatView-BBopM_A3.js +1 -0
  30. package/web-dist/assets/FeedView-Bo4p1stx.js +6 -0
  31. package/web-dist/assets/HistoryView-ChTuQvXr.js +1 -0
  32. package/web-dist/assets/LoginView-AnOP3Mau.js +1 -0
  33. package/web-dist/assets/McpView-DPcihjuB.js +1 -0
  34. package/web-dist/assets/SchedulesView-B2o3vMm-.js +6 -0
  35. package/web-dist/assets/SettingsView-rtMUmH43.js +1 -0
  36. package/web-dist/assets/SkillsView-D_NHLk7C.js +15 -0
  37. package/web-dist/assets/SquadDetailView-BKXLWvwn.js +26 -0
  38. package/web-dist/assets/SquadHealthView-CVJiAgVW.js +11 -0
  39. package/web-dist/assets/SquadsView-fammrB7r.js +6 -0
  40. package/web-dist/assets/UsageView-Cy5Mbprb.js +16 -0
  41. package/web-dist/assets/WikiView-B5TOMnOg.js +36 -0
  42. package/web-dist/assets/arrow-left-CGMB1w_A.js +6 -0
  43. package/web-dist/assets/git-branch-C_Hu39uh.js +6 -0
  44. package/web-dist/assets/index-CQ_szaoT.css +1 -0
  45. package/web-dist/assets/index-CiZnRvN4.js +253 -0
  46. package/web-dist/assets/{plus-Cvp1w2CO.js → plus-DIBAaEMT.js} +1 -1
  47. package/web-dist/assets/{x-O3fBd1Cr.js → save-Chqlu7QA.js} +2 -7
  48. package/web-dist/assets/search-Cl8HcIsG.js +6 -0
  49. package/web-dist/assets/squad-colors-B8B_Y-lz.js +1 -0
  50. package/web-dist/assets/{trash-2-Cr3vrmL5.js → trash-2-CQSzbVIr.js} +1 -1
  51. package/web-dist/assets/triangle-alert-C1OjMvP5.js +6 -0
  52. package/web-dist/assets/x-DThJHYFm.js +6 -0
  53. package/web-dist/favicon.svg +9 -3
  54. package/web-dist/index.html +2 -2
  55. package/web-dist/logo.svg +10 -0
  56. package/web-dist/assets/ChatView-mZaaw3pd.js +0 -11
  57. package/web-dist/assets/FeedView-BHacQwXQ.js +0 -6
  58. package/web-dist/assets/LoginView-B6aSD9II.js +0 -1
  59. package/web-dist/assets/MarkdownContent.vue_vue_type_script_setup_true_lang-CEo_ckIb.js +0 -56
  60. package/web-dist/assets/McpView-BAVRUHIE.js +0 -1
  61. package/web-dist/assets/SchedulesView-dOd1SQiP.js +0 -1
  62. package/web-dist/assets/SettingsView-CCDeEsVg.js +0 -1
  63. package/web-dist/assets/SkillsView-gCfQ35FQ.js +0 -1
  64. package/web-dist/assets/SquadDetailView-CQhFfZTc.js +0 -21
  65. package/web-dist/assets/SquadsView-CZFxtOao.js +0 -6
  66. package/web-dist/assets/WikiView-B0cuUFfm.js +0 -26
  67. package/web-dist/assets/api-DdW5uOZf.js +0 -1
  68. package/web-dist/assets/index-BQdXxKfc.js +0 -138
  69. package/web-dist/assets/index-BbSJ0cfF.css +0 -1
@@ -6,14 +6,21 @@ import { loadConfig, saveConfig } from "../config.js";
6
6
  import { createAuthMiddleware } from "./auth.js";
7
7
  import { sendToOrchestrator } from "../copilot/orchestrator.js";
8
8
  import { listSquads, getSquad, getAgentsForSquad } from "../store/squads.js";
9
- import { getTasksForSquad } from "../store/tasks.js";
10
- import { getInstancesForSquad } from "../store/instances.js";
9
+ import { getTasksForSquad, getSquadTaskMetrics } from "../store/tasks.js";
10
+ import { getInstancesForSquad, destroyInstance } from "../store/instances.js";
11
+ import { getAgentEvents } from "../store/agent-events.js";
12
+ import { getAuditLog, countAuditLog } from "../store/audit-log.js";
11
13
  import { getFeedItems, markFeedItemRead, deleteFeedItem, getUnreadCount, } from "../store/feed.js";
12
14
  import { listSchedules, createSchedule, deleteSchedule, toggleSchedule } from "../store/schedules.js";
15
+ import { triggerSchedule } from "../copilot/trigger-schedule.js";
13
16
  import { listServers, toggleMcpServer, addMcpServer, removeMcpServer } from "../mcp/index.js";
14
- import { listSkills, addSkill, removeSkill, getSkillContent, updateSkillContent } from "../copilot/skills.js";
15
- import { readPage, writePage, deletePage, listPages } from "../wiki/fs.js";
17
+ import { listSkills, addSkill, createSkill, removeSkill, getSkillContent, updateSkillContent, discoverSkills, installFromSource, fetchRemoteSkillPreview } from "../copilot/skills.js";
18
+ import { readPage, writePage, deletePage, listPages, listTemplates, readTemplate, writeTemplate, deleteTemplate } from "../wiki/fs.js";
16
19
  import { searchPages } from "../wiki/search.js";
20
+ import { getBacklinks } from "../wiki/backlinks.js";
21
+ import { saveMessage, getConversation, listConversations, searchConversations, deleteConversation, } from "../store/conversations.js";
22
+ import { getTokenUsageSummary, getTokenUsageBySquad, getTokenUsageByAgent, getDailyTokenUsage, } from "../store/token-usage.js";
23
+ import { DEFAULT_MODEL_PRICING } from "../copilot/token-tracker.js";
17
24
  import { randomUUID } from "node:crypto";
18
25
  const __filename = fileURLToPath(import.meta.url);
19
26
  const __dirname = dirname(__filename);
@@ -57,22 +64,94 @@ export async function startApiServer(config) {
57
64
  });
58
65
  // --- Chat ---
59
66
  app.post("/api/message", async (req, res) => {
60
- const { prompt } = req.body;
67
+ const { prompt, conversationId: clientConvId } = req.body;
61
68
  if (!prompt || typeof prompt !== "string") {
62
69
  res.status(400).json({ error: "prompt is required" });
63
70
  return;
64
71
  }
72
+ const conversationId = (typeof clientConvId === "string" && clientConvId) ? clientConvId : randomUUID();
73
+ // Persist the user message
74
+ saveMessage(conversationId, "user", prompt, "web");
65
75
  // Stream response via SSE, send final to HTTP response
66
76
  await sendToOrchestrator(prompt, "web", (content, done) => {
67
77
  broadcast("message_delta", { content, done });
68
78
  if (done) {
69
- res.json({ content });
79
+ // Persist the assistant response
80
+ saveMessage(conversationId, "assistant", content, "web");
81
+ res.json({ content, conversationId });
70
82
  }
71
83
  });
72
84
  });
85
+ // --- History ---
86
+ app.get("/api/history", (req, res) => {
87
+ const q = req.query.q;
88
+ const from = req.query.from;
89
+ const to = req.query.to;
90
+ const limit = parseInt(req.query.limit) || 50;
91
+ const offset = parseInt(req.query.offset) || 0;
92
+ if (q) {
93
+ res.json(searchConversations(q, { limit, offset, from, to }));
94
+ }
95
+ else {
96
+ res.json(listConversations({ limit, offset, from, to }));
97
+ }
98
+ });
99
+ app.get("/api/history/:id", (req, res) => {
100
+ const messages = getConversation(req.params.id);
101
+ if (messages.length === 0) {
102
+ res.status(404).json({ error: "Conversation not found" });
103
+ return;
104
+ }
105
+ res.json(messages);
106
+ });
107
+ app.delete("/api/history/:id", (req, res) => {
108
+ deleteConversation(req.params.id);
109
+ res.json({ ok: true });
110
+ });
73
111
  // --- Squads ---
74
112
  app.get("/api/squads", (_req, res) => {
75
- res.json(listSquads());
113
+ const data = listSquads();
114
+ const instanceCounts = {};
115
+ for (const squad of data.squads) {
116
+ instanceCounts[squad.id] = getInstancesForSquad(squad.id).length;
117
+ }
118
+ res.json({ ...data, instanceCounts });
119
+ });
120
+ // --- Squad Health Dashboard ---
121
+ app.get("/api/squads/health", (_req, res) => {
122
+ const { squads, agents } = listSquads();
123
+ const health = squads.map((squad) => {
124
+ const squadAgents = agents.filter((a) => a.squad_id === squad.id);
125
+ const instances = getInstancesForSquad(squad.id);
126
+ const metrics = getSquadTaskMetrics(squad.id);
127
+ return {
128
+ id: squad.id,
129
+ name: squad.name,
130
+ universe: squad.universe,
131
+ agentCount: squadAgents.length,
132
+ activeInstanceCount: instances.length,
133
+ activeInstances: instances.map((inst) => ({
134
+ id: inst.id,
135
+ branch: inst.branch,
136
+ lastActivity: inst.last_activity,
137
+ })),
138
+ tasksTotal: metrics.tasksTotal,
139
+ tasksCompleted: metrics.tasksCompleted,
140
+ tasksCompletedRecent: metrics.tasksCompletedRecent,
141
+ tasksPending: metrics.tasksPending,
142
+ tasksInProgress: metrics.tasksInProgress,
143
+ tasksFailed: metrics.tasksFailed,
144
+ avgCycleTimeMinutes: metrics.avgCycleTimeMinutes,
145
+ isStalled: metrics.isStalled,
146
+ recentTasks: metrics.recentTasks.map((t) => ({
147
+ id: t.id,
148
+ description: t.description,
149
+ status: t.status,
150
+ updatedAt: t.updated_at,
151
+ })),
152
+ };
153
+ });
154
+ res.json({ health });
76
155
  });
77
156
  app.get("/api/squads/:id", (req, res) => {
78
157
  const squad = getSquad(req.params.id);
@@ -85,6 +164,50 @@ export async function startApiServer(config) {
85
164
  const instances = getInstancesForSquad(req.params.id);
86
165
  res.json({ squad, agents, tasks, instances });
87
166
  });
167
+ app.delete("/api/instances/:id", async (req, res) => {
168
+ try {
169
+ await destroyInstance(req.params.id);
170
+ res.json({ ok: true });
171
+ }
172
+ catch (err) {
173
+ const msg = err?.message ?? "Unknown error";
174
+ const status = msg.toLowerCase().includes("not found") ? 404 : 500;
175
+ res.status(status).json({ error: msg });
176
+ }
177
+ });
178
+ // --- Task Events ---
179
+ app.get("/api/tasks/:taskId/events", (req, res) => {
180
+ const events = getAgentEvents(req.params.taskId);
181
+ res.json(events);
182
+ });
183
+ // --- Stop Task ---
184
+ app.post("/api/tasks/:taskId/stop", async (req, res) => {
185
+ try {
186
+ const { stopTask } = await import("../copilot/agents.js");
187
+ await stopTask(req.params.taskId);
188
+ res.json({ ok: true });
189
+ }
190
+ catch (err) {
191
+ const msg = err?.message ?? "Unknown error";
192
+ const isNotRunning = msg.toLowerCase().includes("not currently running") || msg.toLowerCase().includes("already completed");
193
+ res.status(isNotRunning ? 404 : 500).json({ error: msg });
194
+ }
195
+ });
196
+ // --- Audit Log ---
197
+ app.get("/api/audit-log", (req, res) => {
198
+ const squad_id = req.query.squad_id;
199
+ const agent_id = req.query.agent_id;
200
+ const action_type = req.query.action_type;
201
+ const from = req.query.from;
202
+ const to = req.query.to;
203
+ const limit = parseInt(req.query.limit) || 50;
204
+ const offset = parseInt(req.query.offset) || 0;
205
+ const filters = { squad_id, agent_id, action_type, from, to, limit, offset };
206
+ res.json({
207
+ entries: getAuditLog(filters),
208
+ total: countAuditLog(filters),
209
+ });
210
+ });
88
211
  // --- Feed ---
89
212
  app.get("/api/feed", (req, res) => {
90
213
  const unreadOnly = req.query.unread === "true";
@@ -129,14 +252,62 @@ export async function startApiServer(config) {
129
252
  const skills = await listSkills();
130
253
  res.json(skills);
131
254
  });
255
+ app.get("/api/skills/discover", async (req, res) => {
256
+ const source = req.query.source;
257
+ if (source !== "awesome-copilot" && source !== "skillssh") {
258
+ res.status(400).json({ error: "source must be 'awesome-copilot' or 'skillssh'" });
259
+ return;
260
+ }
261
+ const q = req.query.q;
262
+ try {
263
+ const skills = await discoverSkills(source, q);
264
+ res.json(skills);
265
+ }
266
+ catch (err) {
267
+ res.status(502).json({ error: err.message });
268
+ }
269
+ });
270
+ app.get("/api/skills/preview", async (req, res) => {
271
+ const source = req.query.source;
272
+ const slug = req.query.slug;
273
+ if (source !== "awesome-copilot" && source !== "skillssh") {
274
+ res.status(400).json({ error: "source must be 'awesome-copilot' or 'skillssh'" });
275
+ return;
276
+ }
277
+ if (!slug) {
278
+ res.status(400).json({ error: "slug is required" });
279
+ return;
280
+ }
281
+ try {
282
+ const content = await fetchRemoteSkillPreview(source, slug);
283
+ res.json({ content });
284
+ }
285
+ catch (err) {
286
+ res.status(502).json({ error: err.message });
287
+ }
288
+ });
132
289
  app.post("/api/skills", async (req, res) => {
133
290
  try {
134
- const { url } = req.body;
135
- if (!url || typeof url !== "string") {
136
- res.status(400).json({ error: "Missing 'url' in request body" });
291
+ const { url, source, slug, content } = req.body;
292
+ if (source && slug) {
293
+ if (source !== "awesome-copilot" && source !== "skillssh") {
294
+ res.status(400).json({ error: "source must be 'awesome-copilot' or 'skillssh'" });
295
+ return;
296
+ }
297
+ await installFromSource(source, slug);
298
+ }
299
+ else if (url && typeof url === "string") {
300
+ // Git-clone method
301
+ await addSkill(url);
302
+ }
303
+ else if (slug && typeof slug === "string" && content && typeof content === "string") {
304
+ // Direct-creation method
305
+ await createSkill(slug, content);
306
+ }
307
+ else {
308
+ res.status(400).json({ error: "Provide 'url' (git clone), 'source' + 'slug' (community install), or 'slug' + 'content' (direct create)" });
137
309
  return;
138
310
  }
139
- await addSkill(url);
140
311
  res.status(201).json({ ok: true });
141
312
  }
142
313
  catch (err) {
@@ -214,13 +385,66 @@ export async function startApiServer(config) {
214
385
  const results = await searchPages(query);
215
386
  res.json(results);
216
387
  });
388
+ app.get("/api/wiki/backlinks/*path", async (req, res) => {
389
+ const raw = req.params.path;
390
+ const pagePath = Array.isArray(raw) ? raw.join("/") : raw;
391
+ const backlinks = await getBacklinks(pagePath);
392
+ res.json(backlinks);
393
+ });
394
+ // --- Wiki Templates ---
395
+ app.get("/api/wiki/templates/squad", async (_req, res) => {
396
+ const files = await listTemplates();
397
+ res.json(files);
398
+ });
399
+ app.get("/api/wiki/template/squad/*path", async (req, res) => {
400
+ try {
401
+ const raw = req.params.path;
402
+ const templatePath = Array.isArray(raw) ? raw.join("/") : raw;
403
+ const content = await readTemplate(templatePath);
404
+ res.json({ path: templatePath, content });
405
+ }
406
+ catch (err) {
407
+ res.status(404).json({ error: err.message });
408
+ }
409
+ });
410
+ app.put("/api/wiki/template/squad/*path", async (req, res) => {
411
+ const raw = req.params.path;
412
+ const templatePath = Array.isArray(raw) ? raw.join("/") : raw;
413
+ const { content } = req.body;
414
+ await writeTemplate(templatePath, content);
415
+ res.json({ ok: true });
416
+ });
417
+ app.delete("/api/wiki/template/squad/*path", async (req, res) => {
418
+ try {
419
+ const raw = req.params.path;
420
+ const templatePath = Array.isArray(raw) ? raw.join("/") : raw;
421
+ await deleteTemplate(templatePath);
422
+ res.json({ ok: true });
423
+ }
424
+ catch (err) {
425
+ res.status(404).json({ error: err.message });
426
+ }
427
+ });
217
428
  // --- Schedules ---
218
429
  app.get("/api/schedules", (_req, res) => {
219
430
  const type = undefined; // return all
220
431
  res.json(listSchedules(type));
221
432
  });
222
433
  app.post("/api/schedules", (req, res) => {
223
- const schedule = createSchedule(req.body);
434
+ const { type, cron, squad_id, agenda, prompt } = req.body ?? {};
435
+ if (type !== "squad" && type !== "io") {
436
+ res.status(400).json({ error: "type must be 'squad' or 'io'" });
437
+ return;
438
+ }
439
+ if (!cron || typeof cron !== "string") {
440
+ res.status(400).json({ error: "cron is required" });
441
+ return;
442
+ }
443
+ if (!squad_id || typeof squad_id !== "string" || !squad_id.trim()) {
444
+ res.status(400).json({ error: "squad_id is required" });
445
+ return;
446
+ }
447
+ const schedule = createSchedule({ type, cron, squad_id, agenda, prompt });
224
448
  res.json(schedule);
225
449
  });
226
450
  app.put("/api/schedules/:id", (req, res) => {
@@ -230,6 +454,14 @@ export async function startApiServer(config) {
230
454
  }
231
455
  res.json({ ok: true });
232
456
  });
457
+ app.post("/api/schedules/:id/trigger", (req, res) => {
458
+ const schedule = triggerSchedule(req.params.id);
459
+ if (!schedule) {
460
+ res.status(404).json({ error: "Schedule not found" });
461
+ return;
462
+ }
463
+ res.json({ ok: true, schedule });
464
+ });
233
465
  app.delete("/api/schedules/:id", (req, res) => {
234
466
  deleteSchedule(req.params.id);
235
467
  res.json({ ok: true });
@@ -284,6 +516,51 @@ export async function startApiServer(config) {
284
516
  saveConfig(updates);
285
517
  res.json({ ok: true });
286
518
  });
519
+ // --- Token Usage ---
520
+ app.get("/api/token-usage/summary", (req, res) => {
521
+ const since = req.query.since;
522
+ res.json(getTokenUsageSummary({ since }));
523
+ });
524
+ app.get("/api/token-usage/by-squad", (req, res) => {
525
+ const since = req.query.since;
526
+ res.json(getTokenUsageBySquad({ since }));
527
+ });
528
+ app.get("/api/token-usage/by-agent", (req, res) => {
529
+ const since = req.query.since;
530
+ const squadId = req.query.squad_id;
531
+ res.json(getTokenUsageByAgent({ since, squadId }));
532
+ });
533
+ app.get("/api/token-usage/daily", (req, res) => {
534
+ const days = parseInt(req.query.days) || 30;
535
+ res.json(getDailyTokenUsage(days));
536
+ });
537
+ app.get("/api/token-usage/pricing", (_req, res) => {
538
+ const config = loadConfig();
539
+ const merged = { ...DEFAULT_MODEL_PRICING, ...(config.modelPricing ?? {}) };
540
+ res.json(merged);
541
+ });
542
+ app.put("/api/token-usage/pricing", (req, res) => {
543
+ const pricing = req.body;
544
+ if (typeof pricing !== "object" || pricing === null) {
545
+ res.status(400).json({ error: "Expected object body" });
546
+ return;
547
+ }
548
+ saveConfig({ modelPricing: pricing });
549
+ res.json({ ok: true });
550
+ });
551
+ app.get("/api/token-usage/alert-threshold", (_req, res) => {
552
+ const config = loadConfig();
553
+ res.json({ tokenAlertThreshold: config.tokenAlertThreshold ?? null });
554
+ });
555
+ app.put("/api/token-usage/alert-threshold", (req, res) => {
556
+ const { tokenAlertThreshold } = req.body;
557
+ if (tokenAlertThreshold !== null && typeof tokenAlertThreshold !== "number") {
558
+ res.status(400).json({ error: "tokenAlertThreshold must be a number or null" });
559
+ return;
560
+ }
561
+ saveConfig({ tokenAlertThreshold: tokenAlertThreshold ?? undefined });
562
+ res.json({ ok: true });
563
+ });
287
564
  // --- Health (unauthenticated) ---
288
565
  app.get("/health", (_req, res) => {
289
566
  res.json({ status: "ok", version: process.env.npm_package_version ?? "unknown" });
package/dist/config.js CHANGED
@@ -2,6 +2,10 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { PATHS } from "./paths.js";
5
+ const ModelPriceSchema = z.object({
6
+ inputPer1M: z.number(),
7
+ outputPer1M: z.number(),
8
+ });
5
9
  const ConfigSchema = z.object({
6
10
  telegramBotToken: z.string().optional(),
7
11
  authorizedUserId: z.number().optional(),
@@ -17,6 +21,8 @@ const ConfigSchema = z.object({
17
21
  .default("meaningful"),
18
22
  backgroundNotifyTelegram: z.boolean().default(true),
19
23
  watchdogEnabled: z.boolean().default(true),
24
+ modelPricing: z.record(z.string(), ModelPriceSchema).optional(),
25
+ tokenAlertThreshold: z.number().optional(),
20
26
  });
21
27
  let cachedConfig;
22
28
  export function loadConfig() {
@@ -1,15 +1,47 @@
1
1
  import { approveAll } from "@github/copilot-sdk";
2
2
  import { getClient } from "./client.js";
3
- import { getLeadForSquad, getAgentsForSquad, updateAgentStatus } from "../store/squads.js";
4
- import { createTask, updateTaskStatus } from "../store/tasks.js";
3
+ import { getLeadForSquad, getAgentsForSquad, updateAgentStatus, getSquad } from "../store/squads.js";
4
+ import { createTask, updateTaskStatus, getTask } from "../store/tasks.js";
5
5
  import { touchInstanceActivity } from "../store/instances.js";
6
6
  import { selectModel, classifyComplexity } from "./model-router.js";
7
7
  import { postFeedItem } from "../store/feed.js";
8
+ import { attachTokenTracker } from "./token-tracker.js";
9
+ import { addAuditEntry } from "../store/audit-log.js";
10
+ import { addAgentEvent } from "../store/agent-events.js";
11
+ import { createSquadTools } from "./squad-tools.js";
12
+ import { loadSkillDirectories } from "./skills.js";
13
+ import { getMcpServersForSession } from "../mcp/registry.js";
14
+ // Registry of active agent sessions keyed by task ID
15
+ const activeSessions = new Map();
16
+ /**
17
+ * Stop a running agent by task ID. Disconnects the session and marks the task as stopped.
18
+ */
19
+ export async function stopTask(taskId) {
20
+ const session = activeSessions.get(taskId);
21
+ if (!session) {
22
+ throw new Error(`Task is not currently running or has already completed`);
23
+ }
24
+ try {
25
+ await session.disconnect();
26
+ }
27
+ finally {
28
+ activeSessions.delete(taskId);
29
+ }
30
+ updateTaskStatus(taskId, "stopped", "Stopped by user");
31
+ addAgentEvent(taskId, "status", "Task stopped by user", { reason: "user_requested" });
32
+ // Reset agent status to idle
33
+ const task = getTask(taskId);
34
+ if (task?.agent_id) {
35
+ updateAgentStatus(task.agent_id, "idle");
36
+ }
37
+ }
8
38
  export async function delegateTask(squadId, task, instanceId) {
9
39
  const lead = getLeadForSquad(squadId);
10
40
  if (!lead) {
11
41
  throw new Error("Squad has no team lead. Add a lead agent first.");
12
42
  }
43
+ const squad = getSquad(squadId);
44
+ const squadSlug = squad?.slug ?? squadId;
13
45
  const agents = getAgentsForSquad(squadId);
14
46
  const taskRecord = createTask(squadId, task, instanceId, lead.id);
15
47
  // Update lead status
@@ -21,6 +53,8 @@ export async function delegateTask(squadId, task, instanceId) {
21
53
  // Select model based on task complexity
22
54
  const tier = classifyComplexity(task);
23
55
  const model = await selectModel(tier);
56
+ // Audit: task delegated
57
+ addAuditEntry("task_delegated", `Task delegated to ${lead.character_name} (${lead.role_title})`, { task: task.slice(0, 500), model }, { squad_id: squadId, agent_id: lead.id, task_id: taskRecord.id });
24
58
  // Create ephemeral agent session for the lead
25
59
  const client = await getClient();
26
60
  const agentRoster = agents
@@ -58,11 +92,18 @@ ${lead.persona ? `## Personality:\n${lead.persona}` : ""}
58
92
  `;
59
93
  let result;
60
94
  try {
95
+ // Load squad-scoped tools, skills, and MCP servers
96
+ const squadTools = createSquadTools(squadSlug, squadId);
97
+ const skillDirs = await loadSkillDirectories();
98
+ const mcpServers = getMcpServersForSession();
61
99
  const session = await client.createSession({
62
100
  model,
63
101
  streaming: true,
64
102
  workingDirectory: process.cwd(),
65
103
  systemMessage: { content: systemMessage },
104
+ tools: squadTools,
105
+ skillDirectories: skillDirs,
106
+ mcpServers,
66
107
  onPermissionRequest: approveAll,
67
108
  infiniteSessions: {
68
109
  enabled: true,
@@ -70,25 +111,79 @@ ${lead.persona ? `## Personality:\n${lead.persona}` : ""}
70
111
  bufferExhaustionThreshold: 0.95,
71
112
  },
72
113
  });
114
+ // Register session so it can be stopped externally
115
+ activeSessions.set(taskRecord.id, session);
116
+ const flushTokens = attachTokenTracker(session, {
117
+ squadId,
118
+ agentId: lead.id,
119
+ taskId: taskRecord.id,
120
+ });
73
121
  try {
74
- const response = await session.sendAndWait({ prompt: `Task delegated to you:\n\n${task}` }, 600_000);
75
- result = response?.data?.content ?? "Task completed (no response content).";
122
+ // Mark task as in progress and record start event
123
+ updateTaskStatus(taskRecord.id, "in_progress");
124
+ addAgentEvent(taskRecord.id, "status", `Task started by ${lead.character_name}`, {
125
+ agent: lead.character_name,
126
+ role: lead.role_title,
127
+ task,
128
+ });
129
+ // Capture streaming message deltas and broadcast via SSE
130
+ let accumulatedMessage = "";
131
+ const { broadcast } = await import("../api/server.js");
132
+ const unsubscribeDelta = session.on("assistant.message_delta", (event) => {
133
+ const delta = event.data?.deltaContent ?? "";
134
+ if (delta) {
135
+ accumulatedMessage += delta;
136
+ broadcast("agent_event", {
137
+ taskId: taskRecord.id,
138
+ type: "message_delta",
139
+ summary: accumulatedMessage,
140
+ payload: { delta, accumulated: accumulatedMessage },
141
+ });
142
+ }
143
+ });
144
+ try {
145
+ const response = await session.sendAndWait({ prompt: `Task delegated to you:\n\n${task}` }, 600_000);
146
+ result = response?.data?.content ?? "Task completed (no response content).";
147
+ // Record the final message event if we have meaningful content
148
+ if (accumulatedMessage.trim()) {
149
+ addAgentEvent(taskRecord.id, "message", accumulatedMessage, {
150
+ agent: lead.character_name,
151
+ content: accumulatedMessage,
152
+ });
153
+ }
154
+ }
155
+ finally {
156
+ unsubscribeDelta();
157
+ }
76
158
  }
77
159
  finally {
160
+ activeSessions.delete(taskRecord.id);
161
+ flushTokens();
78
162
  await session.disconnect();
79
163
  }
80
164
  }
81
165
  catch (err) {
82
166
  const errMsg = err instanceof Error ? err.message : "Unknown error";
167
+ addAgentEvent(taskRecord.id, "status", `Task failed: ${errMsg}`, { error: errMsg });
83
168
  updateTaskStatus(taskRecord.id, "failed", errMsg);
84
169
  updateAgentStatus(lead.id, "idle");
170
+ // Audit: task failed
171
+ addAuditEntry("task_failed", `Task failed: ${errMsg.slice(0, 200)}`, { error: errMsg }, { squad_id: squadId, agent_id: lead.id, task_id: taskRecord.id });
85
172
  throw err;
86
173
  }
87
174
  // Update task and agent status
88
175
  updateTaskStatus(taskRecord.id, "done", result);
89
176
  updateAgentStatus(lead.id, "idle");
177
+ // Audit: task completed
178
+ addAuditEntry("task_completed", `Task completed by ${lead.character_name}`, { result: result.slice(0, 500) }, { squad_id: squadId, agent_id: lead.id, task_id: taskRecord.id });
179
+ // Record completion event
180
+ addAgentEvent(taskRecord.id, "status", `Task completed by ${lead.character_name}`, {
181
+ agent: lead.character_name,
182
+ result: result.slice(0, 500),
183
+ });
90
184
  // Post to feed
91
- postFeedItem(`squad-${squadId}`, `Task completed by ${lead.character_name}`, result.slice(0, 2000));
185
+ const squadSource = `squad-${squadSlug}`;
186
+ postFeedItem(squadSource, `Task completed by ${lead.character_name}`, result.slice(0, 2000));
92
187
  return result;
93
188
  }
94
189
  //# sourceMappingURL=agents.js.map
@@ -1,8 +1,9 @@
1
1
  import { approveAll } from "@github/copilot-sdk";
2
2
  import { getClient } from "./client.js";
3
- import { getLeadForSquad, getAgentsForSquad } from "../store/squads.js";
3
+ import { getLeadForSquad, getAgentsForSquad, getSquad } from "../store/squads.js";
4
4
  import { selectModel } from "./model-router.js";
5
5
  import { postFeedItem } from "../store/feed.js";
6
+ import { attachTokenTracker } from "./token-tracker.js";
6
7
  function buildFacilitatorPrompt(lead, agents, task) {
7
8
  const roster = agents
8
9
  .filter((a) => !a.is_lead)
@@ -96,6 +97,7 @@ export async function planningMeeting(squadId, task) {
96
97
  systemMessage: { content: buildSpecialistPrompt(agent, task) },
97
98
  onPermissionRequest: approveAll,
98
99
  });
100
+ const flushTokens = attachTokenTracker(session, { squadId, agentId: agent.id });
99
101
  try {
100
102
  const response = await session.sendAndWait({ prompt: "Please provide your planning input for this task." }, 60_000);
101
103
  return {
@@ -105,6 +107,7 @@ export async function planningMeeting(squadId, task) {
105
107
  };
106
108
  }
107
109
  finally {
110
+ flushTokens();
108
111
  await session.disconnect();
109
112
  }
110
113
  }));
@@ -124,6 +127,10 @@ export async function planningMeeting(squadId, task) {
124
127
  systemMessage: { content: buildFacilitatorPrompt(lead, agents, task) },
125
128
  onPermissionRequest: approveAll,
126
129
  });
130
+ const flushFacilitatorTokens = attachTokenTracker(facilitatorSession, {
131
+ squadId,
132
+ agentId: lead.id,
133
+ });
127
134
  let plan;
128
135
  try {
129
136
  const prompt = `Here is the input gathered from your team:\n\n${inputsSummary}\n\nNow synthesize this into a clear, structured action plan.`;
@@ -131,6 +138,7 @@ export async function planningMeeting(squadId, task) {
131
138
  plan = response?.data?.content ?? "Planning meeting completed but no plan was produced.";
132
139
  }
133
140
  finally {
141
+ flushFacilitatorTokens();
134
142
  await facilitatorSession.disconnect();
135
143
  }
136
144
  return {
@@ -143,7 +151,9 @@ export async function squadMeeting(squadId, task, executeAfter) {
143
151
  const summary = `## Planning Meeting Complete\n\n**Participants:** ${result.participants.join(", ")}\n\n${result.plan}`;
144
152
  if (!executeAfter) {
145
153
  // Post to feed and wait for user to trigger execution
146
- postFeedItem(`squad-${squadId}`, "Planning meeting complete — awaiting approval", summary);
154
+ const squad = getSquad(squadId);
155
+ const squadSource = squad ? `squad-${squad.slug}` : `squad-${squadId}`;
156
+ postFeedItem(squadSource, "Planning meeting complete — awaiting approval", summary);
147
157
  return summary;
148
158
  }
149
159
  // Execute: delegate with the plan as additional context
@@ -15,14 +15,22 @@ function checkIoSchedules() {
15
15
  continue;
16
16
  if (!isDue(schedule.cron, schedule.last_run, now))
17
17
  continue;
18
+ if (!schedule.squad_id) {
19
+ console.warn(`[io-scheduler] Schedule ${schedule.id} skipped: missing squad_id.`);
20
+ continue;
21
+ }
18
22
  updateScheduleLastRun(schedule.id);
19
- sendToOrchestrator(schedule.prompt, "io-scheduler", (_text, done) => {
23
+ sendToOrchestrator(buildSquadScopedPrompt(schedule), "io-scheduler", (_text, done) => {
20
24
  if (done) {
21
25
  console.log(`[io-scheduler] Schedule ${schedule.id} completed.`);
22
26
  }
23
27
  });
24
28
  }
25
29
  }
30
+ export function buildSquadScopedPrompt(schedule) {
31
+ const squadId = schedule.squad_id ?? "unknown";
32
+ return `[Squad Schedule] Run for squad ${squadId}. Prompt: ${schedule.prompt}`;
33
+ }
26
34
  function isDue(cron, lastRun, now) {
27
35
  const parts = cron.split(" ");
28
36
  if (parts.length !== 5)
@@ -4,7 +4,9 @@ import { loadConfig } from "../config.js";
4
4
  import { buildSystemMessage } from "./system-message.js";
5
5
  import { createTools } from "./tools.js";
6
6
  import { loadSkillDirectories } from "./skills.js";
7
+ import { getMcpServersForSession } from "../mcp/registry.js";
7
8
  import { resetClient } from "./client.js";
9
+ import { addAuditEntry } from "../store/audit-log.js";
8
10
  let orchestratorSession;
9
11
  let sessionCreatePromise;
10
12
  let healthCheckInterval;
@@ -44,6 +46,7 @@ async function createOrResumeSession(client, opts) {
44
46
  systemMessage: { content: systemMessage },
45
47
  tools,
46
48
  skillDirectories: skillDirs,
49
+ mcpServers: getMcpServersForSession(),
47
50
  onPermissionRequest: approveAll,
48
51
  infiniteSessions: {
49
52
  enabled: true,
@@ -80,6 +83,7 @@ function startHealthCheck(client, opts) {
80
83
  healthCheckInterval.unref();
81
84
  }
82
85
  export async function sendToOrchestrator(prompt, source, callback) {
86
+ addAuditEntry("message_received", `Message from ${source}: ${prompt.slice(0, 200)}`, { source, prompt: prompt.slice(0, 1000) });
83
87
  messageQueue.push({ prompt, source, callback });
84
88
  if (!processing)
85
89
  processQueue();