heyio 0.2.1 → 0.3.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 +98 -4
- package/dist/copilot/cron.js +136 -0
- package/dist/copilot/event-summary.js +286 -0
- package/dist/copilot/orchestrator.js +1 -0
- package/dist/copilot/scheduler.js +155 -0
- package/dist/copilot/system-message.js +19 -2
- package/dist/copilot/tools.js +211 -6
- package/dist/daemon.js +4 -0
- package/dist/store/db.js +14 -0
- package/dist/store/schedules.js +73 -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
|
}
|
|
@@ -571,6 +573,69 @@ function walkDirectory(dir, maxDepth = 3, depth = 0) {
|
|
|
571
573
|
}
|
|
572
574
|
return results;
|
|
573
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Parse APPROVED/REJECTED verdict from a reviewer's free-form response.
|
|
578
|
+
*
|
|
579
|
+
* Robust to common formatting variants:
|
|
580
|
+
* - Leading blank lines or markdown headers (e.g. "## Review\n\nAPPROVED")
|
|
581
|
+
* - Markdown emphasis (e.g. "**APPROVED**")
|
|
582
|
+
* - Verdict appearing only later in the response
|
|
583
|
+
* - Both tokens appearing in the same line ("I almost said REJECTED but APPROVED")
|
|
584
|
+
*
|
|
585
|
+
* Strategy:
|
|
586
|
+
* 1. Strip markdown noise.
|
|
587
|
+
* 2. Look at the first 10 non-empty lines for a *line-leading* verdict.
|
|
588
|
+
* 3. Fall back to the first occurrence of either token anywhere in the body.
|
|
589
|
+
* 4. If neither token appears, treat as REJECTED (conservative).
|
|
590
|
+
*/
|
|
591
|
+
export function parseReviewVerdict(content) {
|
|
592
|
+
if (!content)
|
|
593
|
+
return false;
|
|
594
|
+
const stripped = content.replace(/[*_`#>]/g, "");
|
|
595
|
+
const lines = stripped
|
|
596
|
+
.split(/\r?\n/)
|
|
597
|
+
.map((l) => l.trim())
|
|
598
|
+
.filter(Boolean)
|
|
599
|
+
.slice(0, 10);
|
|
600
|
+
for (const line of lines) {
|
|
601
|
+
const lead = line
|
|
602
|
+
.toUpperCase()
|
|
603
|
+
.match(/^[^A-Z]*\b(APPROVED|REJECTED)\b/);
|
|
604
|
+
if (lead)
|
|
605
|
+
return lead[1] === "APPROVED";
|
|
606
|
+
}
|
|
607
|
+
const upper = stripped.toUpperCase();
|
|
608
|
+
const a = upper.search(/\bAPPROVED\b/);
|
|
609
|
+
const r = upper.search(/\bREJECTED\b/);
|
|
610
|
+
if (a === -1 && r === -1)
|
|
611
|
+
return false;
|
|
612
|
+
if (a === -1)
|
|
613
|
+
return false;
|
|
614
|
+
if (r === -1)
|
|
615
|
+
return true;
|
|
616
|
+
return a < r;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Return the reviewer's prose comments with any leading verdict line stripped.
|
|
620
|
+
* Preserves the original formatting (no upper-casing, no markdown stripping).
|
|
621
|
+
*/
|
|
622
|
+
export function stripLeadingVerdictLine(content) {
|
|
623
|
+
if (!content)
|
|
624
|
+
return "";
|
|
625
|
+
const lines = content.split(/\r?\n/);
|
|
626
|
+
let i = 0;
|
|
627
|
+
while (i < lines.length && lines[i].trim() === "")
|
|
628
|
+
i++;
|
|
629
|
+
if (i < lines.length) {
|
|
630
|
+
const probe = lines[i]
|
|
631
|
+
.replace(/[*_`#>]/g, "")
|
|
632
|
+
.trim()
|
|
633
|
+
.toUpperCase();
|
|
634
|
+
if (/^(APPROVED|REJECTED)\b/.test(probe))
|
|
635
|
+
i++;
|
|
636
|
+
}
|
|
637
|
+
return lines.slice(i).join("\n").trim();
|
|
638
|
+
}
|
|
574
639
|
/**
|
|
575
640
|
* Run a peer review phase after a task completes. Every other agent on the
|
|
576
641
|
* squad reviews the work and votes APPROVED / REJECTED. QA agents
|
|
@@ -605,10 +670,8 @@ Review the work. Respond with:
|
|
|
605
670
|
const session = await getOrCreateAgentSession(squadSlug, reviewer, `Peer review of task ${taskId}`);
|
|
606
671
|
const response = await session.sendAndWait({ prompt: reviewPrompt }, 300_000);
|
|
607
672
|
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;
|
|
673
|
+
const approved = parseReviewVerdict(content);
|
|
674
|
+
const comments = stripLeadingVerdictLine(content) || null;
|
|
612
675
|
createReview(taskId, squadSlug, reviewer.character_name, approved, comments ?? undefined);
|
|
613
676
|
recordTaskEvent(taskId, {
|
|
614
677
|
ts: Date.now(),
|
|
@@ -616,6 +679,7 @@ Review the work. Respond with:
|
|
|
616
679
|
data: {
|
|
617
680
|
reviewer: reviewer.character_name,
|
|
618
681
|
is_qa: reviewer.is_qa === 1,
|
|
682
|
+
is_lead: reviewer.is_lead === 1,
|
|
619
683
|
approved,
|
|
620
684
|
comments,
|
|
621
685
|
},
|
|
@@ -623,6 +687,7 @@ Review the work. Respond with:
|
|
|
623
687
|
reviews.push({
|
|
624
688
|
reviewer: reviewer.character_name,
|
|
625
689
|
is_qa: reviewer.is_qa === 1,
|
|
690
|
+
is_lead: reviewer.is_lead === 1,
|
|
626
691
|
approved,
|
|
627
692
|
comments: comments ?? "",
|
|
628
693
|
});
|
|
@@ -637,7 +702,24 @@ Review the work. Respond with:
|
|
|
637
702
|
});
|
|
638
703
|
}
|
|
639
704
|
}
|
|
705
|
+
const hasQaReviewers = reviews.some((r) => r.is_qa);
|
|
706
|
+
const hasLeadReviewer = reviews.some((r) => r.is_lead);
|
|
640
707
|
const qaRejection = reviews.find((r) => r.is_qa && !r.approved);
|
|
708
|
+
// Team lead has implicit veto power equivalent to a QA reviewer. If the lead
|
|
709
|
+
// is also a QA agent the qaRejection branch already covers it; this catches
|
|
710
|
+
// the lead-but-not-QA case.
|
|
711
|
+
const leadRejection = reviews.find((r) => r.is_lead && !r.is_qa && !r.approved);
|
|
712
|
+
const advisoryRejections = reviews.filter((r) => !r.is_qa && !r.is_lead && !r.approved);
|
|
713
|
+
if (!hasQaReviewers && !hasLeadReviewer && advisoryRejections.length > 0) {
|
|
714
|
+
recordTaskEvent(taskId, {
|
|
715
|
+
ts: Date.now(),
|
|
716
|
+
type: "task.review_advisory",
|
|
717
|
+
data: {
|
|
718
|
+
reason: "No QA reviewers or team lead designated; rejections are advisory and do not block promotion.",
|
|
719
|
+
rejectedBy: advisoryRejections.map((r) => r.reviewer),
|
|
720
|
+
},
|
|
721
|
+
});
|
|
722
|
+
}
|
|
641
723
|
const prMatch = taskResult.match(/https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)/);
|
|
642
724
|
if (qaRejection) {
|
|
643
725
|
recordTaskEvent(taskId, {
|
|
@@ -651,6 +733,18 @@ Review the work. Respond with:
|
|
|
651
733
|
});
|
|
652
734
|
return;
|
|
653
735
|
}
|
|
736
|
+
if (leadRejection) {
|
|
737
|
+
recordTaskEvent(taskId, {
|
|
738
|
+
ts: Date.now(),
|
|
739
|
+
type: "task.review_complete",
|
|
740
|
+
data: {
|
|
741
|
+
promoted: false,
|
|
742
|
+
reason: `Lead veto from ${leadRejection.reviewer}`,
|
|
743
|
+
prUrl: prMatch ? prMatch[0] : null,
|
|
744
|
+
},
|
|
745
|
+
});
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
654
748
|
if (!prMatch) {
|
|
655
749
|
recordTaskEvent(taskId, {
|
|
656
750
|
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
|