heyio 0.2.1 → 0.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.
- package/dist/api/server.js +16 -1
- package/dist/copilot/agents.js +127 -16
- package/dist/copilot/cron.js +136 -0
- package/dist/copilot/event-summary.js +286 -0
- package/dist/copilot/io-scheduler.js +132 -0
- package/dist/copilot/orchestrator.js +1 -0
- package/dist/copilot/review-backfill.js +57 -0
- package/dist/copilot/scheduler.js +171 -0
- package/dist/copilot/system-message.js +27 -2
- package/dist/copilot/tools.js +324 -6
- package/dist/daemon.js +31 -1
- package/dist/store/db.js +27 -0
- package/dist/store/io-schedules.js +63 -0
- package/dist/store/schedules.js +83 -0
- package/dist/store/squads.js +21 -0
- package/dist/tui/index.js +62 -2
- package/package.json +1 -1
- package/web-dist/assets/index-BWGQix5_.css +1 -0
- package/web-dist/assets/index-BksyB2za.js +74 -0
- package/web-dist/index.html +2 -2
- package/web-dist/assets/index-B6FXWKsy.js +0 -74
- package/web-dist/assets/index-CXTrW8OO.css +0 -1
package/dist/api/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { config } from "../config.js";
|
|
|
6
6
|
import { listSkills } from "../copilot/skills.js";
|
|
7
7
|
import { listSquads, createSquad, listSquadAgents } from "../store/squads.js";
|
|
8
8
|
import { getAgentInfo, cancelAgentTask, getTaskEvents, subscribeToTaskEvents } from "../copilot/agents.js";
|
|
9
|
+
import { summarize, summarizeEvent } from "../copilot/event-summary.js";
|
|
9
10
|
import { abortOrchestrator } from "../copilot/orchestrator.js";
|
|
10
11
|
import { getActiveTasks, getTask, listRecentTasks } from "../store/tasks.js";
|
|
11
12
|
import { IO_VERSION } from "../paths.js";
|
|
@@ -153,6 +154,18 @@ export async function startApiServer() {
|
|
|
153
154
|
res.status(500).json({ error: "Failed to fetch task" });
|
|
154
155
|
}
|
|
155
156
|
});
|
|
157
|
+
api.get("/tasks/:taskId/activity", (req, res) => {
|
|
158
|
+
const taskId = Array.isArray(req.params.taskId) ? req.params.taskId[0] : req.params.taskId;
|
|
159
|
+
try {
|
|
160
|
+
const events = getTaskEvents(taskId);
|
|
161
|
+
const activity = summarize(events);
|
|
162
|
+
res.json({ taskId, activity });
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
console.error("Error building task activity:", e);
|
|
166
|
+
res.status(500).json({ error: "Failed to build task activity" });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
156
169
|
api.get("/tasks/:taskId/events", (req, res) => {
|
|
157
170
|
const taskId = Array.isArray(req.params.taskId) ? req.params.taskId[0] : req.params.taskId;
|
|
158
171
|
res.setHeader("Content-Type", "text/event-stream");
|
|
@@ -162,7 +175,9 @@ export async function startApiServer() {
|
|
|
162
175
|
res.flushHeaders();
|
|
163
176
|
const send = (ev) => {
|
|
164
177
|
try {
|
|
165
|
-
|
|
178
|
+
const summary = summarizeEvent(ev);
|
|
179
|
+
const payload = { ...ev, summary };
|
|
180
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
166
181
|
}
|
|
167
182
|
catch {
|
|
168
183
|
// client likely disconnected; cleanup happens on req.close
|
package/dist/copilot/agents.js
CHANGED
|
@@ -48,6 +48,7 @@ export function getAgentInfo() {
|
|
|
48
48
|
status: agent?.status === "working" ? "working" : currentTask ? "working" : "idle",
|
|
49
49
|
currentTask,
|
|
50
50
|
currentTaskId,
|
|
51
|
+
model: agentSessionModels.get(key),
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
else {
|
|
@@ -60,6 +61,7 @@ export function getAgentInfo() {
|
|
|
60
61
|
status: currentTask ? "working" : squad?.status === "error" ? "error" : "idle",
|
|
61
62
|
currentTask,
|
|
62
63
|
currentTaskId,
|
|
64
|
+
model: agentSessionModels.get(key),
|
|
63
65
|
});
|
|
64
66
|
}
|
|
65
67
|
}
|
|
@@ -238,21 +240,38 @@ async function getOrCreateAgentSession(squadSlug, agent, taskDescription) {
|
|
|
238
240
|
const tierRank = { high: 3, medium: 2, low: 1 };
|
|
239
241
|
const effectiveTier = tierRank[taskTier] >= tierRank[agentTier] ? taskTier : agentTier;
|
|
240
242
|
const model = getModelForTier(effectiveTier);
|
|
241
|
-
// If we have a cached session, check if the model matches
|
|
243
|
+
// If we have a cached session, check if the model matches AND the agent
|
|
244
|
+
// hasn't been left in an error state by a previous task. If either is off,
|
|
245
|
+
// destroy and recreate. Reusing a session whose underlying SDK process has
|
|
246
|
+
// been throwing is how Panthro got "stuck" in error after the issue #42
|
|
247
|
+
// delegation timeout (issue #55).
|
|
242
248
|
const existing = agentSessions.get(key);
|
|
243
249
|
if (existing) {
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
const fresh = getSquadAgent(squadSlug, agent.character_name);
|
|
251
|
+
const persistedStatus = fresh?.status ?? agent.status;
|
|
252
|
+
if (persistedStatus === "error") {
|
|
253
|
+
console.error(`[io] Agent ${agent.character_name}: previous session ended in error — discarding cached session and recreating`);
|
|
254
|
+
try {
|
|
255
|
+
await existing.destroy();
|
|
256
|
+
}
|
|
257
|
+
catch { /* best-effort */ }
|
|
258
|
+
agentSessions.delete(key);
|
|
259
|
+
agentSessionModels.delete(key);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// Sessions don't expose their model, so track it separately
|
|
263
|
+
const cachedModel = agentSessionModels.get(key);
|
|
264
|
+
if (cachedModel === model)
|
|
265
|
+
return existing;
|
|
266
|
+
// Model changed — destroy old session for the upgraded model
|
|
267
|
+
console.error(`[io] Agent ${agent.character_name}: upgrading model ${cachedModel} → ${model} for task complexity`);
|
|
268
|
+
try {
|
|
269
|
+
await existing.destroy();
|
|
270
|
+
}
|
|
271
|
+
catch { /* best-effort */ }
|
|
272
|
+
agentSessions.delete(key);
|
|
273
|
+
agentSessionModels.delete(key);
|
|
252
274
|
}
|
|
253
|
-
catch { /* best-effort */ }
|
|
254
|
-
agentSessions.delete(key);
|
|
255
|
-
agentSessionModels.delete(key);
|
|
256
275
|
}
|
|
257
276
|
const squad = getSquad(squadSlug);
|
|
258
277
|
const client = await getClient();
|
|
@@ -571,6 +590,69 @@ function walkDirectory(dir, maxDepth = 3, depth = 0) {
|
|
|
571
590
|
}
|
|
572
591
|
return results;
|
|
573
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* Parse APPROVED/REJECTED verdict from a reviewer's free-form response.
|
|
595
|
+
*
|
|
596
|
+
* Robust to common formatting variants:
|
|
597
|
+
* - Leading blank lines or markdown headers (e.g. "## Review\n\nAPPROVED")
|
|
598
|
+
* - Markdown emphasis (e.g. "**APPROVED**")
|
|
599
|
+
* - Verdict appearing only later in the response
|
|
600
|
+
* - Both tokens appearing in the same line ("I almost said REJECTED but APPROVED")
|
|
601
|
+
*
|
|
602
|
+
* Strategy:
|
|
603
|
+
* 1. Strip markdown noise.
|
|
604
|
+
* 2. Look at the first 10 non-empty lines for a *line-leading* verdict.
|
|
605
|
+
* 3. Fall back to the first occurrence of either token anywhere in the body.
|
|
606
|
+
* 4. If neither token appears, treat as REJECTED (conservative).
|
|
607
|
+
*/
|
|
608
|
+
export function parseReviewVerdict(content) {
|
|
609
|
+
if (!content)
|
|
610
|
+
return false;
|
|
611
|
+
const stripped = content.replace(/[*_`#>]/g, "");
|
|
612
|
+
const lines = stripped
|
|
613
|
+
.split(/\r?\n/)
|
|
614
|
+
.map((l) => l.trim())
|
|
615
|
+
.filter(Boolean)
|
|
616
|
+
.slice(0, 10);
|
|
617
|
+
for (const line of lines) {
|
|
618
|
+
const lead = line
|
|
619
|
+
.toUpperCase()
|
|
620
|
+
.match(/^[^A-Z]*\b(APPROVED|REJECTED)\b/);
|
|
621
|
+
if (lead)
|
|
622
|
+
return lead[1] === "APPROVED";
|
|
623
|
+
}
|
|
624
|
+
const upper = stripped.toUpperCase();
|
|
625
|
+
const a = upper.search(/\bAPPROVED\b/);
|
|
626
|
+
const r = upper.search(/\bREJECTED\b/);
|
|
627
|
+
if (a === -1 && r === -1)
|
|
628
|
+
return false;
|
|
629
|
+
if (a === -1)
|
|
630
|
+
return false;
|
|
631
|
+
if (r === -1)
|
|
632
|
+
return true;
|
|
633
|
+
return a < r;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Return the reviewer's prose comments with any leading verdict line stripped.
|
|
637
|
+
* Preserves the original formatting (no upper-casing, no markdown stripping).
|
|
638
|
+
*/
|
|
639
|
+
export function stripLeadingVerdictLine(content) {
|
|
640
|
+
if (!content)
|
|
641
|
+
return "";
|
|
642
|
+
const lines = content.split(/\r?\n/);
|
|
643
|
+
let i = 0;
|
|
644
|
+
while (i < lines.length && lines[i].trim() === "")
|
|
645
|
+
i++;
|
|
646
|
+
if (i < lines.length) {
|
|
647
|
+
const probe = lines[i]
|
|
648
|
+
.replace(/[*_`#>]/g, "")
|
|
649
|
+
.trim()
|
|
650
|
+
.toUpperCase();
|
|
651
|
+
if (/^(APPROVED|REJECTED)\b/.test(probe))
|
|
652
|
+
i++;
|
|
653
|
+
}
|
|
654
|
+
return lines.slice(i).join("\n").trim();
|
|
655
|
+
}
|
|
574
656
|
/**
|
|
575
657
|
* Run a peer review phase after a task completes. Every other agent on the
|
|
576
658
|
* squad reviews the work and votes APPROVED / REJECTED. QA agents
|
|
@@ -605,10 +687,8 @@ Review the work. Respond with:
|
|
|
605
687
|
const session = await getOrCreateAgentSession(squadSlug, reviewer, `Peer review of task ${taskId}`);
|
|
606
688
|
const response = await session.sendAndWait({ prompt: reviewPrompt }, 300_000);
|
|
607
689
|
const content = response?.data?.content ?? "";
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
const approved = firstLine.includes("APPROVED") && !firstLine.includes("REJECTED");
|
|
611
|
-
const comments = lines.slice(1).join("\n").trim() || null;
|
|
690
|
+
const approved = parseReviewVerdict(content);
|
|
691
|
+
const comments = stripLeadingVerdictLine(content) || null;
|
|
612
692
|
createReview(taskId, squadSlug, reviewer.character_name, approved, comments ?? undefined);
|
|
613
693
|
recordTaskEvent(taskId, {
|
|
614
694
|
ts: Date.now(),
|
|
@@ -616,6 +696,7 @@ Review the work. Respond with:
|
|
|
616
696
|
data: {
|
|
617
697
|
reviewer: reviewer.character_name,
|
|
618
698
|
is_qa: reviewer.is_qa === 1,
|
|
699
|
+
is_lead: reviewer.is_lead === 1,
|
|
619
700
|
approved,
|
|
620
701
|
comments,
|
|
621
702
|
},
|
|
@@ -623,6 +704,7 @@ Review the work. Respond with:
|
|
|
623
704
|
reviews.push({
|
|
624
705
|
reviewer: reviewer.character_name,
|
|
625
706
|
is_qa: reviewer.is_qa === 1,
|
|
707
|
+
is_lead: reviewer.is_lead === 1,
|
|
626
708
|
approved,
|
|
627
709
|
comments: comments ?? "",
|
|
628
710
|
});
|
|
@@ -637,7 +719,24 @@ Review the work. Respond with:
|
|
|
637
719
|
});
|
|
638
720
|
}
|
|
639
721
|
}
|
|
722
|
+
const hasQaReviewers = reviews.some((r) => r.is_qa);
|
|
723
|
+
const hasLeadReviewer = reviews.some((r) => r.is_lead);
|
|
640
724
|
const qaRejection = reviews.find((r) => r.is_qa && !r.approved);
|
|
725
|
+
// Team lead has implicit veto power equivalent to a QA reviewer. If the lead
|
|
726
|
+
// is also a QA agent the qaRejection branch already covers it; this catches
|
|
727
|
+
// the lead-but-not-QA case.
|
|
728
|
+
const leadRejection = reviews.find((r) => r.is_lead && !r.is_qa && !r.approved);
|
|
729
|
+
const advisoryRejections = reviews.filter((r) => !r.is_qa && !r.is_lead && !r.approved);
|
|
730
|
+
if (!hasQaReviewers && !hasLeadReviewer && advisoryRejections.length > 0) {
|
|
731
|
+
recordTaskEvent(taskId, {
|
|
732
|
+
ts: Date.now(),
|
|
733
|
+
type: "task.review_advisory",
|
|
734
|
+
data: {
|
|
735
|
+
reason: "No QA reviewers or team lead designated; rejections are advisory and do not block promotion.",
|
|
736
|
+
rejectedBy: advisoryRejections.map((r) => r.reviewer),
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
}
|
|
641
740
|
const prMatch = taskResult.match(/https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)/);
|
|
642
741
|
if (qaRejection) {
|
|
643
742
|
recordTaskEvent(taskId, {
|
|
@@ -651,6 +750,18 @@ Review the work. Respond with:
|
|
|
651
750
|
});
|
|
652
751
|
return;
|
|
653
752
|
}
|
|
753
|
+
if (leadRejection) {
|
|
754
|
+
recordTaskEvent(taskId, {
|
|
755
|
+
ts: Date.now(),
|
|
756
|
+
type: "task.review_complete",
|
|
757
|
+
data: {
|
|
758
|
+
promoted: false,
|
|
759
|
+
reason: `Lead veto from ${leadRejection.reviewer}`,
|
|
760
|
+
prUrl: prMatch ? prMatch[0] : null,
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
654
765
|
if (!prMatch) {
|
|
655
766
|
recordTaskEvent(taskId, {
|
|
656
767
|
ts: Date.now(),
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Minimal standard 5-field cron parser + next-run calculator.
|
|
2
|
+
//
|
|
3
|
+
// Fields (in order): minute, hour, day-of-month, month, day-of-week.
|
|
4
|
+
// Supported syntax per field:
|
|
5
|
+
// * — all values
|
|
6
|
+
// N — single value
|
|
7
|
+
// A,B,C — list
|
|
8
|
+
// A-B — range
|
|
9
|
+
// A-B/N or */N — step (A-B/N restricts to range; */N applies to full range)
|
|
10
|
+
//
|
|
11
|
+
// Day-of-week: 0 or 7 = Sunday, 1 = Monday … (standard Unix cron).
|
|
12
|
+
//
|
|
13
|
+
// Matching semantics: when both day-of-month and day-of-week are restricted
|
|
14
|
+
// (i.e. neither is "*"), Vixie cron uses an OR between them — we follow that.
|
|
15
|
+
const FIELD_RANGES = [
|
|
16
|
+
[0, 59],
|
|
17
|
+
[0, 23],
|
|
18
|
+
[1, 31],
|
|
19
|
+
[1, 12],
|
|
20
|
+
[0, 7],
|
|
21
|
+
];
|
|
22
|
+
function parseField(raw, [min, max]) {
|
|
23
|
+
const out = new Set();
|
|
24
|
+
for (const part of raw.split(",")) {
|
|
25
|
+
const stepMatch = part.match(/^(.+?)\/(\d+)$/);
|
|
26
|
+
const body = stepMatch ? stepMatch[1] : part;
|
|
27
|
+
const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
|
|
28
|
+
if (!(step >= 1))
|
|
29
|
+
throw new Error(`Invalid step in cron field: "${part}"`);
|
|
30
|
+
let lo, hi;
|
|
31
|
+
if (body === "*") {
|
|
32
|
+
lo = min;
|
|
33
|
+
hi = max;
|
|
34
|
+
}
|
|
35
|
+
else if (body.includes("-")) {
|
|
36
|
+
const [a, b] = body.split("-").map((n) => parseInt(n, 10));
|
|
37
|
+
if (Number.isNaN(a) || Number.isNaN(b)) {
|
|
38
|
+
throw new Error(`Invalid range in cron field: "${part}"`);
|
|
39
|
+
}
|
|
40
|
+
lo = a;
|
|
41
|
+
hi = b;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const v = parseInt(body, 10);
|
|
45
|
+
if (Number.isNaN(v))
|
|
46
|
+
throw new Error(`Invalid cron field value: "${part}"`);
|
|
47
|
+
lo = v;
|
|
48
|
+
hi = v;
|
|
49
|
+
}
|
|
50
|
+
if (lo < min || hi > max || lo > hi) {
|
|
51
|
+
throw new Error(`Cron field out of range [${min}-${max}]: "${part}"`);
|
|
52
|
+
}
|
|
53
|
+
for (let v = lo; v <= hi; v += step)
|
|
54
|
+
out.add(v);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
export function parseCron(expr) {
|
|
59
|
+
const trimmed = expr.trim().replace(/\s+/g, " ");
|
|
60
|
+
const parts = trimmed.split(" ");
|
|
61
|
+
if (parts.length !== 5) {
|
|
62
|
+
throw new Error(`Cron expression must have 5 fields (minute hour dom month dow), got ${parts.length}: "${expr}"`);
|
|
63
|
+
}
|
|
64
|
+
const [mRaw, hRaw, domRaw, monRaw, dowRaw] = parts;
|
|
65
|
+
const minutes = parseField(mRaw, FIELD_RANGES[0]);
|
|
66
|
+
const hours = parseField(hRaw, FIELD_RANGES[1]);
|
|
67
|
+
const doms = parseField(domRaw, FIELD_RANGES[2]);
|
|
68
|
+
const months = parseField(monRaw, FIELD_RANGES[3]);
|
|
69
|
+
const dowsRaw = parseField(dowRaw, FIELD_RANGES[4]);
|
|
70
|
+
const dows = new Set();
|
|
71
|
+
for (const v of dowsRaw)
|
|
72
|
+
dows.add(v === 7 ? 0 : v);
|
|
73
|
+
return {
|
|
74
|
+
minutes,
|
|
75
|
+
hours,
|
|
76
|
+
doms,
|
|
77
|
+
months,
|
|
78
|
+
dows,
|
|
79
|
+
domStar: domRaw === "*",
|
|
80
|
+
dowStar: dowRaw === "*",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Return the next Date strictly after `after` that matches the cron expression.
|
|
85
|
+
* Iterates minute-by-minute with month/day fast-forwarding. Capped at ~5
|
|
86
|
+
* years lookahead to guard against unsatisfiable expressions.
|
|
87
|
+
*/
|
|
88
|
+
export function nextRun(expr, after = new Date()) {
|
|
89
|
+
const c = typeof expr === "string" ? parseCron(expr) : expr;
|
|
90
|
+
const cursor = new Date(after.getTime());
|
|
91
|
+
cursor.setSeconds(0, 0);
|
|
92
|
+
cursor.setMinutes(cursor.getMinutes() + 1);
|
|
93
|
+
const limit = new Date(after.getTime() + 5 * 366 * 24 * 60 * 60 * 1000);
|
|
94
|
+
while (cursor <= limit) {
|
|
95
|
+
const month = cursor.getMonth() + 1;
|
|
96
|
+
if (!c.months.has(month)) {
|
|
97
|
+
cursor.setMonth(cursor.getMonth() + 1, 1);
|
|
98
|
+
cursor.setHours(0, 0, 0, 0);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const dom = cursor.getDate();
|
|
102
|
+
const dow = cursor.getDay();
|
|
103
|
+
const dayOk = c.domStar && c.dowStar
|
|
104
|
+
? true
|
|
105
|
+
: c.domStar
|
|
106
|
+
? c.dows.has(dow)
|
|
107
|
+
: c.dowStar
|
|
108
|
+
? c.doms.has(dom)
|
|
109
|
+
: c.doms.has(dom) || c.dows.has(dow);
|
|
110
|
+
if (!dayOk) {
|
|
111
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
112
|
+
cursor.setHours(0, 0, 0, 0);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!c.hours.has(cursor.getHours())) {
|
|
116
|
+
cursor.setHours(cursor.getHours() + 1, 0, 0, 0);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!c.minutes.has(cursor.getMinutes())) {
|
|
120
|
+
cursor.setMinutes(cursor.getMinutes() + 1, 0, 0);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
return cursor;
|
|
124
|
+
}
|
|
125
|
+
throw new Error(`No next run found within 5 years for cron expression`);
|
|
126
|
+
}
|
|
127
|
+
export function validateCron(expr) {
|
|
128
|
+
try {
|
|
129
|
+
const next = nextRun(expr);
|
|
130
|
+
return { ok: true, next };
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=cron.js.map
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Tiered activity log normalizer.
|
|
2
|
+
//
|
|
3
|
+
// Converts raw TaskStreamEvent records (forwarded from the Copilot SDK
|
|
4
|
+
// session) into "activity entries" that present a clean human-readable
|
|
5
|
+
// summary. Each entry preserves the underlying raw payload for drill-down so
|
|
6
|
+
// no information is lost.
|
|
7
|
+
//
|
|
8
|
+
// Used by:
|
|
9
|
+
// - GET /tasks/:taskId/activity (full collapsed list)
|
|
10
|
+
// - the SSE per-event payload (single-event summary attached alongside raw)
|
|
11
|
+
// - the TUI /activity command
|
|
12
|
+
const TOOL_VERBS = {
|
|
13
|
+
bash: "Ran shell command",
|
|
14
|
+
shell: "Ran shell command",
|
|
15
|
+
read_file: "Read file",
|
|
16
|
+
view: "Read file",
|
|
17
|
+
create: "Created file",
|
|
18
|
+
edit: "Edited file",
|
|
19
|
+
str_replace_editor: "Edited file",
|
|
20
|
+
grep: "Searched code",
|
|
21
|
+
glob: "Searched filenames",
|
|
22
|
+
web_fetch: "Fetched URL",
|
|
23
|
+
github: "Called GitHub API",
|
|
24
|
+
delegate_to_teammate: "Delegated to teammate",
|
|
25
|
+
squad_delegate: "Delegated to squad",
|
|
26
|
+
squad_status: "Listed squads",
|
|
27
|
+
squad_agents: "Listed squad agents",
|
|
28
|
+
squad_set_lead: "Set squad lead",
|
|
29
|
+
squad_set_qa: "Set squad QA",
|
|
30
|
+
squad_task_status: "Checked task status",
|
|
31
|
+
squad_task_reviews: "Read task reviews",
|
|
32
|
+
squad_log_decision: "Logged squad decision",
|
|
33
|
+
squad_schedule_create: "Created squad schedule",
|
|
34
|
+
squad_schedule_list: "Listed squad schedules",
|
|
35
|
+
squad_schedule_run_now: "Fired squad schedule",
|
|
36
|
+
wiki_read: "Read wiki page",
|
|
37
|
+
wiki_write: "Wrote wiki page",
|
|
38
|
+
wiki_search: "Searched wiki",
|
|
39
|
+
wiki_list: "Listed wiki pages",
|
|
40
|
+
skill_list: "Listed skills",
|
|
41
|
+
skill_install: "Installed skill",
|
|
42
|
+
skill_remove: "Removed skill",
|
|
43
|
+
skill_search: "Searched skills registry",
|
|
44
|
+
config_update: "Updated config",
|
|
45
|
+
check_update: "Checked for IO update",
|
|
46
|
+
file_ops: "File operation",
|
|
47
|
+
};
|
|
48
|
+
function trunc(s, n) {
|
|
49
|
+
if (!s)
|
|
50
|
+
return "";
|
|
51
|
+
const flat = s.replace(/\s+/g, " ").trim();
|
|
52
|
+
return flat.length > n ? flat.slice(0, n - 1) + "…" : flat;
|
|
53
|
+
}
|
|
54
|
+
function pickArgSummary(toolName, args) {
|
|
55
|
+
if (!args)
|
|
56
|
+
return "";
|
|
57
|
+
const a = args;
|
|
58
|
+
switch (toolName) {
|
|
59
|
+
case "bash":
|
|
60
|
+
case "shell": {
|
|
61
|
+
const cmd = typeof a.command === "string" ? a.command : "";
|
|
62
|
+
return trunc(cmd, 80);
|
|
63
|
+
}
|
|
64
|
+
case "read_file":
|
|
65
|
+
case "view":
|
|
66
|
+
case "create":
|
|
67
|
+
case "edit":
|
|
68
|
+
case "str_replace_editor": {
|
|
69
|
+
const p = typeof a.path === "string" ? a.path : (typeof a.file_path === "string" ? a.file_path : "");
|
|
70
|
+
return p;
|
|
71
|
+
}
|
|
72
|
+
case "grep": {
|
|
73
|
+
const pat = typeof a.pattern === "string" ? `"${trunc(a.pattern, 40)}"` : "";
|
|
74
|
+
const paths = Array.isArray(a.paths) ? a.paths.join(", ") : (typeof a.paths === "string" ? a.paths : "");
|
|
75
|
+
return [pat, paths].filter(Boolean).join(" in ");
|
|
76
|
+
}
|
|
77
|
+
case "glob": {
|
|
78
|
+
return typeof a.pattern === "string" ? a.pattern : "";
|
|
79
|
+
}
|
|
80
|
+
case "web_fetch": {
|
|
81
|
+
return typeof a.url === "string" ? a.url : "";
|
|
82
|
+
}
|
|
83
|
+
case "delegate_to_teammate": {
|
|
84
|
+
const teammate = typeof a.teammate === "string" ? a.teammate : "";
|
|
85
|
+
const task = typeof a.task === "string" ? trunc(a.task, 60) : "";
|
|
86
|
+
return [teammate, task].filter(Boolean).join(": ");
|
|
87
|
+
}
|
|
88
|
+
case "squad_delegate": {
|
|
89
|
+
const slug = typeof a.slug === "string" ? a.slug : "";
|
|
90
|
+
const agent = typeof a.agent === "string" ? ` → ${a.agent}` : "";
|
|
91
|
+
const task = typeof a.task === "string" ? trunc(a.task, 50) : "";
|
|
92
|
+
return `${slug}${agent}${task ? `: ${task}` : ""}`;
|
|
93
|
+
}
|
|
94
|
+
case "wiki_read":
|
|
95
|
+
case "wiki_write":
|
|
96
|
+
case "wiki_delete": {
|
|
97
|
+
return typeof a.path === "string" ? a.path : "";
|
|
98
|
+
}
|
|
99
|
+
case "wiki_search":
|
|
100
|
+
case "skill_search": {
|
|
101
|
+
return typeof a.query === "string" ? `"${trunc(a.query, 40)}"` : "";
|
|
102
|
+
}
|
|
103
|
+
case "github": {
|
|
104
|
+
const ep = typeof a.endpoint === "string" ? a.endpoint : "";
|
|
105
|
+
const method = typeof a.method === "string" ? a.method : "";
|
|
106
|
+
return [method, ep].filter(Boolean).join(" ");
|
|
107
|
+
}
|
|
108
|
+
case "file_ops": {
|
|
109
|
+
const op = typeof a.operation === "string" ? a.operation : "";
|
|
110
|
+
const p = typeof a.path === "string" ? a.path : "";
|
|
111
|
+
return [op, p].filter(Boolean).join(" ");
|
|
112
|
+
}
|
|
113
|
+
default: {
|
|
114
|
+
// Best-effort: stringify the first arg value.
|
|
115
|
+
const k = Object.keys(a)[0];
|
|
116
|
+
if (!k)
|
|
117
|
+
return "";
|
|
118
|
+
const v = a[k];
|
|
119
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
120
|
+
return `${k}=${trunc(s ?? "", 50)}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Summarize a single TaskStreamEvent. */
|
|
125
|
+
export function summarizeEvent(ev) {
|
|
126
|
+
const data = (ev.data ?? {});
|
|
127
|
+
const base = { ts: ev.ts, rawType: ev.type, raw: ev.data };
|
|
128
|
+
switch (ev.type) {
|
|
129
|
+
case "assistant.intent": {
|
|
130
|
+
const intent = typeof data.intent === "string" ? data.intent : "";
|
|
131
|
+
return { ...base, kind: "reasoning", icon: "🧠", summary: intent || "(intent)" };
|
|
132
|
+
}
|
|
133
|
+
case "assistant.reasoning": {
|
|
134
|
+
const content = typeof data.content === "string" ? data.content : "";
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
kind: "reasoning",
|
|
138
|
+
icon: "🧠",
|
|
139
|
+
summary: trunc(content, 140) || "(reasoning)",
|
|
140
|
+
detail: content,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
case "assistant.message": {
|
|
144
|
+
const content = typeof data.content === "string" ? data.content : "";
|
|
145
|
+
return {
|
|
146
|
+
...base,
|
|
147
|
+
kind: "message",
|
|
148
|
+
icon: "💬",
|
|
149
|
+
summary: trunc(content, 200) || "(empty message)",
|
|
150
|
+
detail: content,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
case "assistant.turn_start":
|
|
154
|
+
return { ...base, kind: "system", icon: "▶️", summary: "Turn started" };
|
|
155
|
+
case "assistant.turn_end":
|
|
156
|
+
return { ...base, kind: "system", icon: "⏹️", summary: "Turn ended" };
|
|
157
|
+
case "tool.execution_start": {
|
|
158
|
+
const name = typeof data.toolName === "string" ? data.toolName : "tool";
|
|
159
|
+
const verb = TOOL_VERBS[name] ?? `Used ${name}`;
|
|
160
|
+
const args = pickArgSummary(name, data.arguments);
|
|
161
|
+
return {
|
|
162
|
+
...base,
|
|
163
|
+
kind: "tool",
|
|
164
|
+
icon: "🔧",
|
|
165
|
+
summary: args ? `${verb} — ${args}` : verb,
|
|
166
|
+
toolCallId: typeof data.toolCallId === "string" ? data.toolCallId : undefined,
|
|
167
|
+
status: "pending",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
case "tool.execution_complete": {
|
|
171
|
+
const success = data.success === true;
|
|
172
|
+
const result = (data.result ?? {});
|
|
173
|
+
const content = typeof result.content === "string" ? result.content : "";
|
|
174
|
+
return {
|
|
175
|
+
...base,
|
|
176
|
+
kind: "tool",
|
|
177
|
+
icon: success ? "✅" : "❌",
|
|
178
|
+
summary: success ? "Tool completed" : "Tool failed",
|
|
179
|
+
detail: trunc(content, 300),
|
|
180
|
+
toolCallId: typeof data.toolCallId === "string" ? data.toolCallId : undefined,
|
|
181
|
+
status: success ? "success" : "error",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
case "tool.execution_progress": {
|
|
185
|
+
const message = typeof data.message === "string" ? data.message : "";
|
|
186
|
+
return {
|
|
187
|
+
...base,
|
|
188
|
+
kind: "tool",
|
|
189
|
+
icon: "⏳",
|
|
190
|
+
summary: trunc(message, 140) || "Tool progress",
|
|
191
|
+
toolCallId: typeof data.toolCallId === "string" ? data.toolCallId : undefined,
|
|
192
|
+
status: "pending",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
case "session.error": {
|
|
196
|
+
const msg = typeof data.message === "string" ? data.message : (typeof data.error === "string" ? data.error : "");
|
|
197
|
+
return { ...base, kind: "outcome", icon: "❌", summary: `Error: ${trunc(msg, 160) || "session error"}` };
|
|
198
|
+
}
|
|
199
|
+
case "session.warning": {
|
|
200
|
+
const msg = typeof data.message === "string" ? data.message : "";
|
|
201
|
+
return { ...base, kind: "outcome", icon: "⚠️", summary: `Warning: ${trunc(msg, 160) || "session warning"}` };
|
|
202
|
+
}
|
|
203
|
+
case "task.done": {
|
|
204
|
+
const r = typeof data.result === "string" ? data.result : "";
|
|
205
|
+
return { ...base, kind: "outcome", icon: "✅", summary: "Task completed", detail: trunc(r, 300) };
|
|
206
|
+
}
|
|
207
|
+
case "task.failed": {
|
|
208
|
+
const e = typeof data.error === "string" ? data.error : "";
|
|
209
|
+
return { ...base, kind: "outcome", icon: "❌", summary: `Task failed: ${trunc(e, 160)}` };
|
|
210
|
+
}
|
|
211
|
+
case "task.cancelled":
|
|
212
|
+
return { ...base, kind: "outcome", icon: "🛑", summary: "Task cancelled" };
|
|
213
|
+
case "task.review":
|
|
214
|
+
return {
|
|
215
|
+
...base,
|
|
216
|
+
kind: "outcome",
|
|
217
|
+
icon: data.approved ? "👍" : "👎",
|
|
218
|
+
summary: `Review by ${data.reviewer ?? "?"}: ${data.approved ? "APPROVED" : "REJECTED"}${data.is_qa ? " (QA)" : ""}`,
|
|
219
|
+
detail: typeof data.comments === "string" ? data.comments : undefined,
|
|
220
|
+
};
|
|
221
|
+
case "task.review_complete":
|
|
222
|
+
return {
|
|
223
|
+
...base,
|
|
224
|
+
kind: "outcome",
|
|
225
|
+
icon: data.promoted ? "🚀" : "ℹ️",
|
|
226
|
+
summary: data.promoted
|
|
227
|
+
? `Promoted PR: ${data.prUrl ?? ""}`
|
|
228
|
+
: `Review complete: ${typeof data.reason === "string" ? data.reason : ""}`,
|
|
229
|
+
};
|
|
230
|
+
case "task.review_advisory":
|
|
231
|
+
return {
|
|
232
|
+
...base,
|
|
233
|
+
kind: "outcome",
|
|
234
|
+
icon: "ℹ️",
|
|
235
|
+
summary: typeof data.reason === "string" ? data.reason : "Advisory review note",
|
|
236
|
+
};
|
|
237
|
+
case "task.review_error": {
|
|
238
|
+
const e = typeof data.error === "string" ? data.error : "";
|
|
239
|
+
return { ...base, kind: "outcome", icon: "❌", summary: `Review error: ${trunc(e, 160)}` };
|
|
240
|
+
}
|
|
241
|
+
default:
|
|
242
|
+
return { ...base, kind: "system", icon: "•", summary: ev.type };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Collapse a stream of events into the activity log. Tool start + complete
|
|
247
|
+
* pairs are merged into one entry (success/error and detail attached).
|
|
248
|
+
* Streaming deltas (assistant.message_delta, assistant.reasoning_delta,
|
|
249
|
+
* tool.execution_partial_result) are dropped — the "complete" sibling event
|
|
250
|
+
* is what we summarize.
|
|
251
|
+
*/
|
|
252
|
+
export function summarize(events) {
|
|
253
|
+
const out = [];
|
|
254
|
+
const toolIndex = new Map(); // toolCallId → index into out
|
|
255
|
+
for (const ev of events) {
|
|
256
|
+
if (ev.type === "assistant.message_delta" ||
|
|
257
|
+
ev.type === "assistant.reasoning_delta" ||
|
|
258
|
+
ev.type === "tool.execution_partial_result") {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const entry = summarizeEvent(ev);
|
|
262
|
+
if (entry.kind === "tool" && entry.toolCallId) {
|
|
263
|
+
const existing = toolIndex.get(entry.toolCallId);
|
|
264
|
+
if (existing != null && entry.status && entry.status !== "pending") {
|
|
265
|
+
// Merge: keep the verb from the start event, add status/detail/icon from completion.
|
|
266
|
+
const prior = out[existing];
|
|
267
|
+
out[existing] = {
|
|
268
|
+
...prior,
|
|
269
|
+
icon: entry.icon,
|
|
270
|
+
status: entry.status,
|
|
271
|
+
detail: entry.detail ?? prior.detail,
|
|
272
|
+
// Append a brief outcome marker if the start summary is the only descriptor.
|
|
273
|
+
summary: prior.summary,
|
|
274
|
+
// Keep the completion event raw available for drill-down by stashing on detail when no detail present
|
|
275
|
+
raw: { start: prior.raw, complete: entry.raw },
|
|
276
|
+
rawType: `${prior.rawType}+${entry.rawType}`,
|
|
277
|
+
};
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
toolIndex.set(entry.toolCallId, out.length);
|
|
281
|
+
}
|
|
282
|
+
out.push(entry);
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=event-summary.js.map
|