pum-agent 0.2.11-beta.1 → 0.2.12-beta.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/package.json +1 -1
- package/src/app.tsx +46 -18
- package/src/check-policy.ts +53 -3
- package/src/news-popup.tsx +9 -0
- package/src/news.ts +90 -2
- package/src/replay.ts +1 -0
- package/src/subagents/manager.ts +145 -1
- package/src/subagents/types.ts +5 -0
- package/src/transcript.tsx +1 -1
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -1037,9 +1037,15 @@ export function App({
|
|
|
1037
1037
|
else if (event.type === "main-pending-add") addPending(event.pending);
|
|
1038
1038
|
else if (event.type === "main-pending-resolve") resolvePending(event.id);
|
|
1039
1039
|
else if (event.type === "main-pending-drop") dropPending(event.id);
|
|
1040
|
+
else if (event.type === "news-changed") {
|
|
1041
|
+
const loaded = loadNewsItems(session.sessionFile);
|
|
1042
|
+
newsRef.current = loaded;
|
|
1043
|
+
setNews(loaded);
|
|
1044
|
+
setTx((value) => ({ ...value, lines: tagNewsLines(value.lines, loaded) }));
|
|
1045
|
+
}
|
|
1040
1046
|
setAgentRevision((revision) => revision + 1);
|
|
1041
1047
|
}),
|
|
1042
|
-
[subagentManager],
|
|
1048
|
+
[subagentManager, session],
|
|
1043
1049
|
);
|
|
1044
1050
|
|
|
1045
1051
|
useEffect(() => {
|
|
@@ -1506,29 +1512,47 @@ export function App({
|
|
|
1506
1512
|
const jumpFromNews = (target: "answer" | "prompt") => {
|
|
1507
1513
|
const item = newsRef.current[newsCursorRef.current];
|
|
1508
1514
|
if (!item) return;
|
|
1509
|
-
const
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1515
|
+
const requesterAgentId = item.completion?.requesterAgentId ?? null;
|
|
1516
|
+
const lines = requesterAgentId === null
|
|
1517
|
+
? txRef.current.lines
|
|
1518
|
+
: subagentManager.getAgent(requesterAgentId)?.transcript.lines;
|
|
1519
|
+
if (!lines) return;
|
|
1520
|
+
const completionPromptIndex = item.completion
|
|
1521
|
+
? lines.findIndex((line) =>
|
|
1522
|
+
line.kind === "agent-message" && line.messageId === item.completion?.messageId,
|
|
1523
|
+
)
|
|
1524
|
+
: -1;
|
|
1525
|
+
const answerIndex = item.completion
|
|
1526
|
+
? lines.findIndex((line, index) =>
|
|
1527
|
+
index > completionPromptIndex
|
|
1528
|
+
&& line.kind === "text"
|
|
1529
|
+
&& line.role === "assistant"
|
|
1530
|
+
&& line.text === item.text,
|
|
1531
|
+
)
|
|
1532
|
+
: lines.findIndex((line) =>
|
|
1533
|
+
line.kind === "text" && line.role === "assistant" && line.newsId === item.id,
|
|
1534
|
+
);
|
|
1513
1535
|
let targetIndex = answerIndex;
|
|
1514
1536
|
if (target === "prompt") {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1537
|
+
targetIndex = completionPromptIndex;
|
|
1538
|
+
if (!item.completion) {
|
|
1539
|
+
const promptText = item.prompts?.find((prompt) => !prompt.steer)?.text
|
|
1540
|
+
?? item.prompts?.[0]?.text;
|
|
1541
|
+
if (promptText) {
|
|
1542
|
+
const end = answerIndex >= 0 ? answerIndex : lines.length;
|
|
1543
|
+
for (let index = end - 1; index >= 0; index--) {
|
|
1544
|
+
const line = lines[index];
|
|
1545
|
+
if (line?.kind === "text" && line.role === "user" && line.text === promptText) {
|
|
1546
|
+
targetIndex = index;
|
|
1547
|
+
break;
|
|
1548
|
+
}
|
|
1525
1549
|
}
|
|
1526
1550
|
}
|
|
1527
1551
|
}
|
|
1528
1552
|
}
|
|
1529
1553
|
if (targetIndex < 0) return;
|
|
1530
1554
|
|
|
1531
|
-
if (activeAgentIdRef.current !==
|
|
1555
|
+
if (activeAgentIdRef.current !== requesterAgentId && !selectAgentView(requesterAgentId)) return;
|
|
1532
1556
|
newsOpenRef.current = false;
|
|
1533
1557
|
setNewsOpen(false);
|
|
1534
1558
|
const scrollToTarget = () => {
|
|
@@ -1590,8 +1614,12 @@ export function App({
|
|
|
1590
1614
|
const firstLine = item.text.split("\n")[0] ?? item.text;
|
|
1591
1615
|
const preview = firstLine.length > 240 ? `${firstLine.slice(0, 240)} …` : firstLine;
|
|
1592
1616
|
const quote = `> ${preview}\n\n`;
|
|
1593
|
-
|
|
1594
|
-
|
|
1617
|
+
const requesterAgentId = item.completion?.requesterAgentId ?? null;
|
|
1618
|
+
const targetAgentId = requesterAgentId && subagentManager.getAgent(requesterAgentId)
|
|
1619
|
+
? requesterAgentId
|
|
1620
|
+
: null;
|
|
1621
|
+
viewDrafts.current.set(targetAgentId ?? "main", quote);
|
|
1622
|
+
if (activeAgentIdRef.current !== targetAgentId) selectAgentView(targetAgentId);
|
|
1595
1623
|
else setEditorText(quote);
|
|
1596
1624
|
queueMicrotask(() => {
|
|
1597
1625
|
inputRef.current?.focus();
|
package/src/check-policy.ts
CHANGED
|
@@ -549,6 +549,12 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
|
|
|
549
549
|
let pipeline = 0;
|
|
550
550
|
let annotations = 0;
|
|
551
551
|
let truncated = false;
|
|
552
|
+
let commandStart = true;
|
|
553
|
+
const caseContexts: Array<{
|
|
554
|
+
phase: "subject" | "pattern" | "body";
|
|
555
|
+
parenDepth: number;
|
|
556
|
+
braceDepth: number;
|
|
557
|
+
}> = [];
|
|
552
558
|
|
|
553
559
|
const addAnnotation = <T>(list: T[], value: T) => {
|
|
554
560
|
annotations++;
|
|
@@ -607,9 +613,37 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
|
|
|
607
613
|
index++;
|
|
608
614
|
continue;
|
|
609
615
|
}
|
|
610
|
-
if (char === "'" || char === "\"") { quote = char; index++; continue; }
|
|
611
|
-
|
|
612
|
-
|
|
616
|
+
if (char === "'" || char === "\"") { quote = char; commandStart = false; index++; continue; }
|
|
617
|
+
|
|
618
|
+
const word = command.slice(index).match(/^[A-Za-z_][A-Za-z0-9_]*/)?.[0];
|
|
619
|
+
if (word) {
|
|
620
|
+
const next = command[index + word.length];
|
|
621
|
+
const reservedBoundary = next === undefined || /[\s;&|(){}<>]/.test(next);
|
|
622
|
+
const activeCase = caseContexts.at(-1);
|
|
623
|
+
const atCaseLevel = activeCase?.parenDepth === parenDepth && activeCase.braceDepth === braceDepth;
|
|
624
|
+
if (commandStart && word === "case" && reservedBoundary) {
|
|
625
|
+
caseContexts.push({ phase: "subject", parenDepth, braceDepth });
|
|
626
|
+
} else if (activeCase?.phase === "subject" && atCaseLevel && word === "in" && reservedBoundary) {
|
|
627
|
+
activeCase.phase = "pattern";
|
|
628
|
+
} else if (commandStart && activeCase && atCaseLevel && word === "esac" && reservedBoundary) {
|
|
629
|
+
caseContexts.pop();
|
|
630
|
+
}
|
|
631
|
+
commandStart = commandStart && reservedBoundary && new Set(["do", "then", "else", "elif"]).has(word);
|
|
632
|
+
index += word.length;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (char === "(") { parenDepth++; commandStart = true; index++; continue; }
|
|
637
|
+
if (char === ")") {
|
|
638
|
+
const activeCase = caseContexts.at(-1);
|
|
639
|
+
if (activeCase?.phase === "pattern" && activeCase.parenDepth === parenDepth && activeCase.braceDepth === braceDepth) {
|
|
640
|
+
activeCase.phase = "body";
|
|
641
|
+
commandStart = true;
|
|
642
|
+
} else if (parenDepth === 0) errors.push(`unexpected ) at ${index}`);
|
|
643
|
+
else parenDepth--;
|
|
644
|
+
index++;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
613
647
|
if (char === "{") { braceDepth++; index++; continue; }
|
|
614
648
|
if (char === "}") { if (braceDepth === 0) errors.push(`unexpected } at ${index}`); else braceDepth--; index++; continue; }
|
|
615
649
|
|
|
@@ -632,6 +666,15 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
|
|
|
632
666
|
index = newline + 1;
|
|
633
667
|
continue;
|
|
634
668
|
}
|
|
669
|
+
const activeCase = caseContexts.at(-1);
|
|
670
|
+
const inCasePattern = activeCase?.phase === "pattern"
|
|
671
|
+
&& activeCase.parenDepth === parenDepth
|
|
672
|
+
&& activeCase.braceDepth === braceDepth;
|
|
673
|
+
if (char === "|" && inCasePattern && !command.startsWith("||", index) && !command.startsWith("|&", index)) {
|
|
674
|
+
commandStart = false;
|
|
675
|
+
index++;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
635
678
|
const rawOperator = [";;&", "&&", "||", "|&", ";;", ";&", ";", "|", "&", "\n"].find((item) => command.startsWith(item, index));
|
|
636
679
|
if (rawOperator) {
|
|
637
680
|
const normalized = rawOperator === "\n" ? "newline" : rawOperator as BashOperator["operator"];
|
|
@@ -643,9 +686,15 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
|
|
|
643
686
|
stageStart = index + rawOperator.length;
|
|
644
687
|
operatorBefore = normalized;
|
|
645
688
|
}
|
|
689
|
+
if (activeCase?.phase === "body" && activeCase.parenDepth === parenDepth && activeCase.braceDepth === braceDepth
|
|
690
|
+
&& (rawOperator === ";;" || rawOperator === ";&" || rawOperator === ";;&")) {
|
|
691
|
+
activeCase.phase = "pattern";
|
|
692
|
+
}
|
|
693
|
+
commandStart = true;
|
|
646
694
|
index += rawOperator.length;
|
|
647
695
|
continue;
|
|
648
696
|
}
|
|
697
|
+
if (!/\s/.test(char)) commandStart = false;
|
|
649
698
|
index++;
|
|
650
699
|
}
|
|
651
700
|
addStage(command.length);
|
|
@@ -655,6 +704,7 @@ export function analyzeBashCommand(command: string, requestedLimits?: Partial<Ch
|
|
|
655
704
|
if (backtickStart !== undefined) errors.push(`unterminated backtick substitution at ${backtickStart}`);
|
|
656
705
|
if (parenDepth) errors.push("unbalanced parentheses");
|
|
657
706
|
if (braceDepth) errors.push("unbalanced braces");
|
|
707
|
+
if (caseContexts.length) errors.push("unterminated case statement");
|
|
658
708
|
if (truncated) errors.push("analysis annotation limit exceeded");
|
|
659
709
|
|
|
660
710
|
const stages: BashStage[] = stageRanges.map((range, index) => {
|
package/src/news-popup.tsx
CHANGED
|
@@ -69,6 +69,15 @@ export function NewsPopup({
|
|
|
69
69
|
<box style={{ flexGrow: 1, width: 1 }} />
|
|
70
70
|
<text content={formatAge(current.at)} fg={theme.dim} bg={theme.popupBg} wrapMode="none" />
|
|
71
71
|
</box>
|
|
72
|
+
{current.completion ? (
|
|
73
|
+
<text
|
|
74
|
+
content={`${current.completion.agentName} → ${current.completion.requesterName} · finish_subagent`}
|
|
75
|
+
fg={theme.dim}
|
|
76
|
+
bg={theme.popupBg}
|
|
77
|
+
wrapMode="none"
|
|
78
|
+
style={{ height: 1, flexShrink: 0 }}
|
|
79
|
+
/>
|
|
80
|
+
) : null}
|
|
72
81
|
<box style={{ height: 1, flexShrink: 0 }} />
|
|
73
82
|
<scrollbox
|
|
74
83
|
id="news-scrollbox"
|
package/src/news.ts
CHANGED
|
@@ -8,6 +8,16 @@ export type NewsPrompt = {
|
|
|
8
8
|
steer: boolean;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
export type NewsCompletion = {
|
|
12
|
+
settlementId: string;
|
|
13
|
+
messageId: string;
|
|
14
|
+
agentId: string;
|
|
15
|
+
agentName: string;
|
|
16
|
+
requesterAgentId: string | null;
|
|
17
|
+
requesterName: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
11
21
|
export type NewsItem = {
|
|
12
22
|
/** Stable identifier used to tag the matching transcript line. */
|
|
13
23
|
id: string;
|
|
@@ -21,6 +31,8 @@ export type NewsItem = {
|
|
|
21
31
|
answered: boolean;
|
|
22
32
|
/** User prompt and steers that produced this answer, oldest first. */
|
|
23
33
|
prompts?: NewsPrompt[];
|
|
34
|
+
/** Managed completion identity for a finish_subagent-triggered answer. */
|
|
35
|
+
completion?: NewsCompletion;
|
|
24
36
|
};
|
|
25
37
|
|
|
26
38
|
/** The list never holds more than this many answers. */
|
|
@@ -48,10 +60,23 @@ function isNewsItem(value: unknown): value is NewsItem {
|
|
|
48
60
|
Boolean(prompt) &&
|
|
49
61
|
typeof (prompt as Record<string, unknown>).text === "string" &&
|
|
50
62
|
typeof (prompt as Record<string, unknown>).steer === "boolean",
|
|
51
|
-
)))
|
|
63
|
+
))) &&
|
|
64
|
+
(item.completion === undefined || isNewsCompletion(item.completion))
|
|
52
65
|
);
|
|
53
66
|
}
|
|
54
67
|
|
|
68
|
+
function isNewsCompletion(value: unknown): value is NewsCompletion {
|
|
69
|
+
if (!value || typeof value !== "object") return false;
|
|
70
|
+
const completion = value as Record<string, unknown>;
|
|
71
|
+
return typeof completion.settlementId === "string"
|
|
72
|
+
&& typeof completion.messageId === "string"
|
|
73
|
+
&& typeof completion.agentId === "string"
|
|
74
|
+
&& typeof completion.agentName === "string"
|
|
75
|
+
&& (completion.requesterAgentId === null || typeof completion.requesterAgentId === "string")
|
|
76
|
+
&& typeof completion.requesterName === "string"
|
|
77
|
+
&& typeof completion.summary === "string";
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
/** Load the persisted news list for a session. Never throws. */
|
|
56
81
|
export function loadNewsItems(sessionFile: string | undefined): NewsItem[] {
|
|
57
82
|
if (!sessionFile) return [];
|
|
@@ -81,16 +106,79 @@ export function saveNewsItems(
|
|
|
81
106
|
}
|
|
82
107
|
}
|
|
83
108
|
|
|
109
|
+
export type FinishNewsSettlement = {
|
|
110
|
+
id: string;
|
|
111
|
+
messageId: string;
|
|
112
|
+
agentId: string;
|
|
113
|
+
agentName: string;
|
|
114
|
+
parentAgentId: string | null;
|
|
115
|
+
requesterName: string;
|
|
116
|
+
status: "idle" | "completed" | "failed";
|
|
117
|
+
summary?: string;
|
|
118
|
+
content: string;
|
|
119
|
+
createdAt: number;
|
|
120
|
+
response?: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Project one completed finish settlement into the persisted News model. */
|
|
124
|
+
export function newsItemFromFinishSettlement(
|
|
125
|
+
settlement: FinishNewsSettlement,
|
|
126
|
+
): NewsItem | undefined {
|
|
127
|
+
if (settlement.status !== "completed" || !settlement.response?.trim()) return undefined;
|
|
128
|
+
if (/^(?:ack(?:nowledged)?|got it|noted|ok(?:ay)?|thanks|thank you|understood)[.!\s]*$/i.test(
|
|
129
|
+
settlement.response.trim(),
|
|
130
|
+
)) return undefined;
|
|
131
|
+
return {
|
|
132
|
+
id: `subagent-finish:${settlement.messageId}`,
|
|
133
|
+
text: settlement.response.trim(),
|
|
134
|
+
at: settlement.createdAt,
|
|
135
|
+
read: false,
|
|
136
|
+
answered: false,
|
|
137
|
+
prompts: [{ text: settlement.content, steer: false }],
|
|
138
|
+
completion: {
|
|
139
|
+
settlementId: settlement.id,
|
|
140
|
+
messageId: settlement.messageId,
|
|
141
|
+
agentId: settlement.agentId,
|
|
142
|
+
agentName: settlement.agentName,
|
|
143
|
+
requesterAgentId: settlement.parentAgentId,
|
|
144
|
+
requesterName: settlement.requesterName,
|
|
145
|
+
summary: settlement.summary ?? "",
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Upsert stable News identities while preserving local read and reply state. */
|
|
151
|
+
export function mergeNewsItems(
|
|
152
|
+
existing: readonly NewsItem[],
|
|
153
|
+
incoming: readonly NewsItem[],
|
|
154
|
+
): NewsItem[] {
|
|
155
|
+
const byId = new Map(existing.map((item) => [item.id, item]));
|
|
156
|
+
for (const item of incoming) {
|
|
157
|
+
const current = byId.get(item.id);
|
|
158
|
+
byId.set(item.id, current
|
|
159
|
+
? { ...item, read: current.read, answered: current.answered }
|
|
160
|
+
: item);
|
|
161
|
+
}
|
|
162
|
+
return [...byId.values()]
|
|
163
|
+
.sort((a, b) => b.at - a.at)
|
|
164
|
+
.slice(0, NEWS_CAPACITY);
|
|
165
|
+
}
|
|
166
|
+
|
|
84
167
|
/**
|
|
85
168
|
* Attach news identifiers to replayed assistant lines that exactly match a
|
|
86
169
|
* stored answer. Each stored answer claims the first later unmatched line with
|
|
87
170
|
* the same text, so resumed transcripts keep their circle/checkmark markers.
|
|
88
171
|
*/
|
|
89
|
-
export function tagNewsLines<T>(
|
|
172
|
+
export function tagNewsLines<T>(
|
|
173
|
+
lines: readonly T[],
|
|
174
|
+
items: readonly NewsItem[],
|
|
175
|
+
requesterAgentId: string | null = null,
|
|
176
|
+
): T[] {
|
|
90
177
|
if (items.length === 0 || lines.length === 0) return lines.map((line) => line);
|
|
91
178
|
const claimed = new Set<number>();
|
|
92
179
|
const out = lines.map((line) => line);
|
|
93
180
|
for (const item of items) {
|
|
181
|
+
if ((item.completion?.requesterAgentId ?? null) !== requesterAgentId) continue;
|
|
94
182
|
const maybe = (value: T) =>
|
|
95
183
|
value as unknown as { kind?: unknown; role?: unknown; text?: unknown; newsId?: unknown };
|
|
96
184
|
const index = out.findIndex((line, i) =>
|
package/src/replay.ts
CHANGED
package/src/subagents/manager.ts
CHANGED
|
@@ -19,6 +19,13 @@ import {
|
|
|
19
19
|
usageFromEntries,
|
|
20
20
|
} from "../agent-usage";
|
|
21
21
|
import { replayEntries } from "../replay";
|
|
22
|
+
import {
|
|
23
|
+
loadNewsItems,
|
|
24
|
+
mergeNewsItems,
|
|
25
|
+
newsItemFromFinishSettlement,
|
|
26
|
+
saveNewsItems,
|
|
27
|
+
tagNewsLines,
|
|
28
|
+
} from "../news";
|
|
22
29
|
import { isRejectedToolResult, rejectedToolReason } from "../check-mode";
|
|
23
30
|
import {
|
|
24
31
|
observeSearchCalls,
|
|
@@ -185,6 +192,8 @@ type RuntimeRecord = {
|
|
|
185
192
|
* it to idle and blocking its managed merge.
|
|
186
193
|
*/
|
|
187
194
|
statusBeforeTurn?: SubagentStatus;
|
|
195
|
+
completionMessageIds?: Set<string>;
|
|
196
|
+
completionResponse?: string;
|
|
188
197
|
};
|
|
189
198
|
|
|
190
199
|
type IdleOpenReminderState = {
|
|
@@ -289,6 +298,8 @@ export class SubagentManager {
|
|
|
289
298
|
private mainCwd = process.cwd();
|
|
290
299
|
private parentSessionId = "detached";
|
|
291
300
|
private mainRunning = false;
|
|
301
|
+
private readonly mainCompletionMessageIds = new Set<string>();
|
|
302
|
+
private mainCompletionResponse = "";
|
|
292
303
|
private maxActiveSubagents: number;
|
|
293
304
|
private worktreeQueue: Promise<void> = Promise.resolve();
|
|
294
305
|
private readonly messageTimes = new Map<string, number[]>();
|
|
@@ -365,6 +376,8 @@ export class SubagentManager {
|
|
|
365
376
|
this.mainCwd = cwd;
|
|
366
377
|
this.parentSessionId = sessionId;
|
|
367
378
|
this.mainRunning = false;
|
|
379
|
+
this.mainCompletionMessageIds.clear();
|
|
380
|
+
this.mainCompletionResponse = "";
|
|
368
381
|
this.emit({
|
|
369
382
|
type: "trigger-target",
|
|
370
383
|
sessionId,
|
|
@@ -374,6 +387,7 @@ export class SubagentManager {
|
|
|
374
387
|
});
|
|
375
388
|
|
|
376
389
|
const restored = new Map<string, Omit<SubagentSnapshot, "transcript">>();
|
|
390
|
+
const restoredEntries = new Map<string, readonly any[]>();
|
|
377
391
|
const restoredActivity = new Map<string, { activityGeneration: number; idleNotifiedGeneration: number }>();
|
|
378
392
|
const restoredFinish = new Map<string, string>();
|
|
379
393
|
for (const entry of sessionManager.getEntries()) {
|
|
@@ -421,6 +435,7 @@ export class SubagentManager {
|
|
|
421
435
|
const childManager = (await import("@earendil-works/pi-coding-agent")).SessionManager.open(
|
|
422
436
|
snapshot.sessionFile,
|
|
423
437
|
);
|
|
438
|
+
restoredEntries.set(snapshot.id, childManager.getEntries());
|
|
424
439
|
transcript = {
|
|
425
440
|
lines: replayEntries(childManager.buildContextEntries(), snapshot.worktree.path, true),
|
|
426
441
|
stream: null,
|
|
@@ -463,6 +478,8 @@ export class SubagentManager {
|
|
|
463
478
|
this.persistActivity(this.records.get(snapshot.id)!);
|
|
464
479
|
}
|
|
465
480
|
}
|
|
481
|
+
this.restoreSettlementResponses(sessionManager.getEntries(), restoredEntries);
|
|
482
|
+
this.reconcileFinishNews();
|
|
466
483
|
await this.retrySettlementsForParent(null);
|
|
467
484
|
this.emit();
|
|
468
485
|
}
|
|
@@ -733,7 +750,14 @@ export class SubagentManager {
|
|
|
733
750
|
if (typeof id === "string") {
|
|
734
751
|
this.resolvePending(record, id);
|
|
735
752
|
this.acknowledgeSettlementMessage(id);
|
|
753
|
+
if (message.customType === AGENT_MESSAGE_CUSTOM_TYPE
|
|
754
|
+
&& message.details?.kind === "completion") {
|
|
755
|
+
record.completionMessageIds ??= new Set<string>();
|
|
756
|
+
record.completionMessageIds.add(id);
|
|
757
|
+
}
|
|
736
758
|
}
|
|
759
|
+
} else if (message?.role === "assistant" && record.completionMessageIds?.size) {
|
|
760
|
+
record.completionResponse = "";
|
|
737
761
|
} else if (message?.role === "user") {
|
|
738
762
|
const text = typeof message.content === "string"
|
|
739
763
|
? message.content
|
|
@@ -766,6 +790,9 @@ export class SubagentManager {
|
|
|
766
790
|
const update = event.assistantMessageEvent;
|
|
767
791
|
const kind = update.type === "text_delta" ? "assistant" : update.type === "thinking_delta" ? "thinking" : null;
|
|
768
792
|
if (!kind) return;
|
|
793
|
+
if (kind === "assistant" && record.completionMessageIds?.size) {
|
|
794
|
+
record.completionResponse = (record.completionResponse ?? "") + update.delta;
|
|
795
|
+
}
|
|
769
796
|
this.updateTranscript(record, (value) => {
|
|
770
797
|
if (value.stream?.kind === kind) {
|
|
771
798
|
return { ...value, stream: { kind, text: value.stream.text + update.delta } };
|
|
@@ -833,6 +860,13 @@ export class SubagentManager {
|
|
|
833
860
|
case "agent_settled": {
|
|
834
861
|
this.messageCacheController?.releaseRequester({ kind: "subagent", id: record.snapshot.id });
|
|
835
862
|
this.updateTranscript(record, flushTranscript);
|
|
863
|
+
if (record.completionMessageIds?.size) {
|
|
864
|
+
for (const messageId of record.completionMessageIds) {
|
|
865
|
+
this.recordSettlementResponse(messageId, record.completionResponse ?? "");
|
|
866
|
+
}
|
|
867
|
+
record.completionMessageIds.clear();
|
|
868
|
+
record.completionResponse = "";
|
|
869
|
+
}
|
|
836
870
|
if (record.session) {
|
|
837
871
|
void this.triggerManager?.markTargetSettled(
|
|
838
872
|
record.session.sessionId,
|
|
@@ -1112,6 +1146,13 @@ export class SubagentManager {
|
|
|
1112
1146
|
});
|
|
1113
1147
|
pi.on("agent_settled", () => {
|
|
1114
1148
|
this.mainRunning = false;
|
|
1149
|
+
if (this.mainCompletionMessageIds.size > 0) {
|
|
1150
|
+
for (const messageId of this.mainCompletionMessageIds) {
|
|
1151
|
+
this.recordSettlementResponse(messageId, this.mainCompletionResponse);
|
|
1152
|
+
}
|
|
1153
|
+
this.mainCompletionMessageIds.clear();
|
|
1154
|
+
this.mainCompletionResponse = "";
|
|
1155
|
+
}
|
|
1115
1156
|
this.messageCacheController?.releaseRequester({ kind: "main", id: this.parentSessionId });
|
|
1116
1157
|
void this.triggerManager?.markTargetSettled(this.parentSessionId, null);
|
|
1117
1158
|
this.emit({
|
|
@@ -1126,12 +1167,26 @@ export class SubagentManager {
|
|
|
1126
1167
|
pi.on("message_start", (event) => {
|
|
1127
1168
|
const message = event.message;
|
|
1128
1169
|
this.acceptIdleOpenReminder(null, message);
|
|
1170
|
+
if (message.role === "assistant" && this.mainCompletionMessageIds.size > 0) {
|
|
1171
|
+
this.mainCompletionResponse = "";
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1129
1174
|
if (message.role !== "custom"
|
|
1130
1175
|
|| ![AGENT_MESSAGE_CUSTOM_TYPE, TRIGGER_EVENT_CUSTOM_TYPE].includes(message.customType)) return;
|
|
1131
|
-
const
|
|
1176
|
+
const details = message.details as AgentMessageData | undefined;
|
|
1177
|
+
const id = details?.id;
|
|
1132
1178
|
if (typeof id === "string") {
|
|
1133
1179
|
this.acknowledgeSettlementMessage(id);
|
|
1134
1180
|
this.emit({ type: "main-pending-resolve", id });
|
|
1181
|
+
if (message.customType === AGENT_MESSAGE_CUSTOM_TYPE && details?.kind === "completion") {
|
|
1182
|
+
this.mainCompletionMessageIds.add(id);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
pi.on("message_update", (event) => {
|
|
1187
|
+
const update = event.assistantMessageEvent;
|
|
1188
|
+
if (this.mainCompletionMessageIds.size > 0 && update.type === "text_delta") {
|
|
1189
|
+
this.mainCompletionResponse += update.delta;
|
|
1135
1190
|
}
|
|
1136
1191
|
});
|
|
1137
1192
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -1719,6 +1774,7 @@ export class SubagentManager {
|
|
|
1719
1774
|
sender: data.sender,
|
|
1720
1775
|
recipient: data.recipient,
|
|
1721
1776
|
text: data.text,
|
|
1777
|
+
messageId: data.id,
|
|
1722
1778
|
};
|
|
1723
1779
|
}
|
|
1724
1780
|
|
|
@@ -1893,6 +1949,89 @@ export class SubagentManager {
|
|
|
1893
1949
|
if (!delivered) this.emit({ type: "main-pending-drop", id: pending.id });
|
|
1894
1950
|
}
|
|
1895
1951
|
|
|
1952
|
+
private responseAfterSettlement(entries: readonly any[], messageId: string): string {
|
|
1953
|
+
let active = false;
|
|
1954
|
+
let response = "";
|
|
1955
|
+
for (const entry of entries) {
|
|
1956
|
+
const message = entry?.type === "message"
|
|
1957
|
+
? entry.message
|
|
1958
|
+
: entry?.type === "custom_message"
|
|
1959
|
+
? { ...entry, role: "custom" }
|
|
1960
|
+
: entry;
|
|
1961
|
+
if (message?.role === "custom") {
|
|
1962
|
+
if (message.customType === AGENT_MESSAGE_CUSTOM_TYPE && message.details?.id === messageId) {
|
|
1963
|
+
active = true;
|
|
1964
|
+
}
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
if (!active) continue;
|
|
1968
|
+
if (message?.role === "user") break;
|
|
1969
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
1970
|
+
const text = message.content
|
|
1971
|
+
.filter((block: any) => block?.type === "text" && typeof block.text === "string")
|
|
1972
|
+
.map((block: any) => block.text)
|
|
1973
|
+
.join("")
|
|
1974
|
+
.trim();
|
|
1975
|
+
if (text) response = text;
|
|
1976
|
+
}
|
|
1977
|
+
return response;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
private restoreSettlementResponses(
|
|
1981
|
+
mainEntries: readonly any[],
|
|
1982
|
+
childEntries: ReadonlyMap<string, readonly any[]>,
|
|
1983
|
+
): void {
|
|
1984
|
+
for (const settlement of this.settlements.values()) {
|
|
1985
|
+
if (settlement.status !== "completed" || settlement.response?.trim()) continue;
|
|
1986
|
+
const entries = settlement.parentAgentId === null
|
|
1987
|
+
? mainEntries
|
|
1988
|
+
: childEntries.get(settlement.parentAgentId) ?? [];
|
|
1989
|
+
const response = this.responseAfterSettlement(entries, settlement.messageId);
|
|
1990
|
+
if (response) this.recordSettlementResponse(settlement.messageId, response, false);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
private recordSettlementResponse(messageId: string, response: string, reconcile = true): void {
|
|
1995
|
+
const settlement = [...this.settlements.values()].find((item) => item.messageId === messageId);
|
|
1996
|
+
if (!settlement || settlement.status !== "completed" || !response.trim()) return;
|
|
1997
|
+
if (settlement.response === response.trim()) return;
|
|
1998
|
+
settlement.response = response.trim();
|
|
1999
|
+
settlement.respondedAt = Date.now();
|
|
2000
|
+
this.persist({
|
|
2001
|
+
event: "settlement",
|
|
2002
|
+
id: settlement.agentId,
|
|
2003
|
+
at: settlement.respondedAt,
|
|
2004
|
+
settlement: { ...settlement },
|
|
2005
|
+
});
|
|
2006
|
+
if (reconcile) this.reconcileFinishNews();
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
private reconcileFinishNews(): void {
|
|
2010
|
+
const sessionFile = this.mainSessionManager?.getSessionFile?.();
|
|
2011
|
+
if (!sessionFile) return;
|
|
2012
|
+
const incoming = [...this.settlements.values()]
|
|
2013
|
+
.map((settlement) => newsItemFromFinishSettlement({
|
|
2014
|
+
...settlement,
|
|
2015
|
+
agentName: settlement.agentName
|
|
2016
|
+
?? this.records.get(settlement.agentId)?.snapshot.name
|
|
2017
|
+
?? settlement.agentId,
|
|
2018
|
+
requesterName: settlement.requesterName
|
|
2019
|
+
?? (settlement.parentAgentId
|
|
2020
|
+
? this.records.get(settlement.parentAgentId)?.snapshot.name ?? settlement.parentAgentId
|
|
2021
|
+
: "main"),
|
|
2022
|
+
}))
|
|
2023
|
+
.filter((item): item is NonNullable<typeof item> => item !== undefined);
|
|
2024
|
+
const next = mergeNewsItems(loadNewsItems(sessionFile), incoming);
|
|
2025
|
+
saveNewsItems(sessionFile, next);
|
|
2026
|
+
for (const record of this.records.values()) {
|
|
2027
|
+
record.snapshot.transcript = {
|
|
2028
|
+
...record.snapshot.transcript,
|
|
2029
|
+
lines: tagNewsLines(record.snapshot.transcript.lines, next, record.snapshot.id),
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
this.emit({ type: "news-changed" });
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1896
2035
|
private settlementId(record: RuntimeRecord, status: "idle" | "completed" | "failed"): string {
|
|
1897
2036
|
return `${record.snapshot.id}:${record.activityGeneration}:${status}`;
|
|
1898
2037
|
}
|
|
@@ -1912,11 +2051,16 @@ export class SubagentManager {
|
|
|
1912
2051
|
`worktree: ${record.snapshot.worktree.path}`,
|
|
1913
2052
|
summary ? `summary: ${summary}` : "",
|
|
1914
2053
|
].filter(Boolean).join("\n");
|
|
2054
|
+
const requester = record.snapshot.parentAgentId
|
|
2055
|
+
? this.records.get(record.snapshot.parentAgentId)?.snapshot.name ?? record.snapshot.parentAgentId
|
|
2056
|
+
: "main";
|
|
1915
2057
|
settlement = {
|
|
1916
2058
|
id,
|
|
1917
2059
|
messageId: `settlement-${id}`,
|
|
1918
2060
|
agentId: record.snapshot.id,
|
|
2061
|
+
agentName: record.snapshot.name,
|
|
1919
2062
|
parentAgentId: record.snapshot.parentAgentId,
|
|
2063
|
+
requesterName: requester,
|
|
1920
2064
|
status,
|
|
1921
2065
|
summary,
|
|
1922
2066
|
activityGeneration: record.activityGeneration,
|
package/src/subagents/types.ts
CHANGED
|
@@ -60,9 +60,13 @@ export type SubagentSettlement = {
|
|
|
60
60
|
parentAgentId: string | null;
|
|
61
61
|
status: "idle" | "completed" | "failed";
|
|
62
62
|
summary?: string;
|
|
63
|
+
agentName?: string;
|
|
64
|
+
requesterName?: string;
|
|
63
65
|
activityGeneration: number;
|
|
64
66
|
content: string;
|
|
65
67
|
createdAt: number;
|
|
68
|
+
response?: string;
|
|
69
|
+
respondedAt?: number;
|
|
66
70
|
acknowledgedAt?: number;
|
|
67
71
|
};
|
|
68
72
|
|
|
@@ -96,6 +100,7 @@ export type TriggerEventData = ExternalTriggerEventData & {
|
|
|
96
100
|
|
|
97
101
|
export type SubagentManagerEvent =
|
|
98
102
|
| { type: "changed" }
|
|
103
|
+
| { type: "news-changed" }
|
|
99
104
|
| { type: "main-line"; line: Line }
|
|
100
105
|
| { type: "main-pending-add"; pending: PendingLine }
|
|
101
106
|
| { type: "main-pending-resolve"; id: string }
|
package/src/transcript.tsx
CHANGED
|
@@ -20,7 +20,7 @@ export type Role = "user" | "assistant" | "thinking" | "system" | "error";
|
|
|
20
20
|
export type Line =
|
|
21
21
|
| { kind: "text"; role: Role; text: string; newsId?: string }
|
|
22
22
|
| { kind: "tool"; call: ToolCall }
|
|
23
|
-
| { kind: "agent-message"; sender: string; recipient: string; text: string };
|
|
23
|
+
| { kind: "agent-message"; sender: string; recipient: string; text: string; messageId?: string };
|
|
24
24
|
|
|
25
25
|
export type PendingLine = {
|
|
26
26
|
id: string;
|