devez-vibe 1.2.6 → 1.2.8
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/LICENSE +33 -33
- package/bin/dvz.exe +0 -0
- package/bridge/claude-agent-sdk-bridge.mjs +796 -578
- package/package.json +41 -41
|
@@ -435,25 +435,25 @@ async function createSession(params, resumeId) {
|
|
|
435
435
|
turnSequence: 1,
|
|
436
436
|
itemSequence: 1,
|
|
437
437
|
streamBlocks: new Map(),
|
|
438
|
-
tools: new Map(),
|
|
439
|
-
tasks: new Map(),
|
|
440
|
-
subagents: new Map(),
|
|
441
|
-
knownSubagents: new Map(),
|
|
442
|
-
lastContextUsage: null,
|
|
438
|
+
tools: new Map(),
|
|
439
|
+
tasks: new Map(),
|
|
440
|
+
subagents: new Map(),
|
|
441
|
+
knownSubagents: new Map(),
|
|
442
|
+
lastContextUsage: null,
|
|
443
443
|
lastContextWindow: 0,
|
|
444
444
|
};
|
|
445
445
|
const agentQuery = await startAgentQuery(queue, makeOptions(params, id, resumeId));
|
|
446
446
|
session.query = agentQuery;
|
|
447
447
|
sessions.set(id, session);
|
|
448
|
-
const consumer = consume(session).catch((error) => {
|
|
448
|
+
const consumer = consume(session).catch((error) => {
|
|
449
449
|
notify("error", {
|
|
450
450
|
threadId: id,
|
|
451
451
|
provider: "Claude",
|
|
452
452
|
error: { message: error instanceof Error ? error.message : String(error) },
|
|
453
|
-
willRetry: false,
|
|
454
|
-
});
|
|
455
|
-
clearSubagents(session);
|
|
456
|
-
if (session.turn) finishTurn(session, error);
|
|
453
|
+
willRetry: false,
|
|
454
|
+
});
|
|
455
|
+
clearSubagents(session);
|
|
456
|
+
if (session.turn) finishTurn(session, error);
|
|
457
457
|
});
|
|
458
458
|
session.consumer = consumer;
|
|
459
459
|
let initialization;
|
|
@@ -501,84 +501,123 @@ function emitDelta(session, method, itemId, delta) {
|
|
|
501
501
|
notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
511
|
-
: null;
|
|
512
|
-
|
|
513
|
-
function splitGraphemes(text) {
|
|
514
|
-
return graphemeSegmenter
|
|
515
|
-
? Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment)
|
|
516
|
-
: Array.from(text);
|
|
504
|
+
function isKoreanPrompt(input) {
|
|
505
|
+
const prompt = (Array.isArray(input) ? input : [])
|
|
506
|
+
.filter((item) => item?.type === "text")
|
|
507
|
+
.map((item) => String(item.text || ""))
|
|
508
|
+
.join("\n");
|
|
509
|
+
return /[\uac00-\ud7a3]/.test(prompt);
|
|
517
510
|
}
|
|
518
511
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const size = Math.min(
|
|
524
|
-
SMOOTH_TEXT_MAX_GRAPHEMES,
|
|
525
|
-
Math.max(1, Math.ceil(graphemes.length / SMOOTH_TEXT_TARGET_FRAMES)),
|
|
526
|
-
);
|
|
527
|
-
return {
|
|
528
|
-
chunk: graphemes.slice(0, size).join(""),
|
|
529
|
-
rest: graphemes.slice(size).join(""),
|
|
530
|
-
};
|
|
512
|
+
function openingNotice(input) {
|
|
513
|
+
return isKoreanPrompt(input)
|
|
514
|
+
? "요청 내용을 확인하고 필요한 작업을 진행하겠습니다."
|
|
515
|
+
: "I’ll review the request and proceed with the necessary work.";
|
|
531
516
|
}
|
|
532
517
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
push(text) {
|
|
543
|
-
this.pending += text;
|
|
544
|
-
if (this.timer == null) this.drain();
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
drain() {
|
|
548
|
-
if (!this.pending) {
|
|
549
|
-
this.timer = null;
|
|
550
|
-
for (const resolve of this.waiters.splice(0)) resolve();
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
const { chunk, rest } = takeSmoothTextChunk(this.pending);
|
|
554
|
-
this.pending = rest;
|
|
555
|
-
this.emit(chunk);
|
|
556
|
-
if (this.pending) {
|
|
557
|
-
this.timer = setTimeout(() => this.drain(), this.intervalMs);
|
|
558
|
-
} else {
|
|
559
|
-
this.timer = null;
|
|
560
|
-
for (const resolve of this.waiters.splice(0)) resolve();
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
finish() {
|
|
565
|
-
if (!this.pending && this.timer == null) return Promise.resolve();
|
|
566
|
-
return new Promise((resolve) => this.waiters.push(resolve));
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
flush() {
|
|
570
|
-
if (this.timer != null) clearTimeout(this.timer);
|
|
571
|
-
this.timer = null;
|
|
572
|
-
if (this.pending) this.emit(this.pending);
|
|
573
|
-
this.pending = "";
|
|
574
|
-
for (const resolve of this.waiters.splice(0)) resolve();
|
|
518
|
+
function normalizeProgressText(turn, text) {
|
|
519
|
+
const value = String(text || "");
|
|
520
|
+
const trimmed = value.trim();
|
|
521
|
+
if (turn?.koreanRequest
|
|
522
|
+
&& trimmed.length <= 160
|
|
523
|
+
&& !/[\uac00-\ud7a3]/.test(trimmed)
|
|
524
|
+
&& /^Now\b[^\r\n]*[.!?]?$/i.test(trimmed)) {
|
|
525
|
+
return "다음 부분을 이어서 확인하겠습니다.";
|
|
575
526
|
}
|
|
527
|
+
return value;
|
|
576
528
|
}
|
|
577
529
|
|
|
578
|
-
|
|
579
|
-
|
|
530
|
+
// Claude can answer with a tool_use as its first and only content block even
|
|
531
|
+
// when the prompt asks for an opening update. Keep the visible contract stable
|
|
532
|
+
// without duplicating a real model-written update.
|
|
533
|
+
function emitOpeningNotice(session) {
|
|
534
|
+
if (!session.turn || session.turn.openingNoticeEmitted) return;
|
|
535
|
+
const text = session.turn.openingNotice;
|
|
536
|
+
const id = nextItemId(session, "opening");
|
|
537
|
+
const item = { id, type: "agentMessage", text, provider: "Claude" };
|
|
538
|
+
emitItem(session, "started", item);
|
|
539
|
+
emitItem(session, "completed", item);
|
|
540
|
+
session.turn.openingNoticeEmitted = true;
|
|
580
541
|
}
|
|
581
542
|
|
|
543
|
+
// Windows rounds larger timer delays up to the next scheduler slice. Ten
|
|
544
|
+
// milliseconds lands near one terminal frame instead of visibly stepping at ~30ms.
|
|
545
|
+
const SMOOTH_TEXT_INTERVAL_MS = 10;
|
|
546
|
+
const SMOOTH_TEXT_TARGET_FRAMES = 8;
|
|
547
|
+
const SMOOTH_TEXT_MAX_GRAPHEMES = 24;
|
|
548
|
+
const graphemeSegmenter = typeof Intl.Segmenter === "function"
|
|
549
|
+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
550
|
+
: null;
|
|
551
|
+
|
|
552
|
+
function splitGraphemes(text) {
|
|
553
|
+
return graphemeSegmenter
|
|
554
|
+
? Array.from(graphemeSegmenter.segment(text), ({ segment }) => segment)
|
|
555
|
+
: Array.from(text);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Claude can deliver a whole phrase in one SDK event. Drain roughly one visual
|
|
559
|
+
// frame's share at a time, catching up quickly when a large backlog arrives.
|
|
560
|
+
function takeSmoothTextChunk(text) {
|
|
561
|
+
const graphemes = splitGraphemes(text);
|
|
562
|
+
const size = Math.min(
|
|
563
|
+
SMOOTH_TEXT_MAX_GRAPHEMES,
|
|
564
|
+
Math.max(1, Math.ceil(graphemes.length / SMOOTH_TEXT_TARGET_FRAMES)),
|
|
565
|
+
);
|
|
566
|
+
return {
|
|
567
|
+
chunk: graphemes.slice(0, size).join(""),
|
|
568
|
+
rest: graphemes.slice(size).join(""),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
class SmoothTextStream {
|
|
573
|
+
constructor(emit, intervalMs = SMOOTH_TEXT_INTERVAL_MS) {
|
|
574
|
+
this.emit = emit;
|
|
575
|
+
this.intervalMs = intervalMs;
|
|
576
|
+
this.pending = "";
|
|
577
|
+
this.timer = null;
|
|
578
|
+
this.waiters = [];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
push(text) {
|
|
582
|
+
this.pending += text;
|
|
583
|
+
if (this.timer == null) this.drain();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
drain() {
|
|
587
|
+
if (!this.pending) {
|
|
588
|
+
this.timer = null;
|
|
589
|
+
for (const resolve of this.waiters.splice(0)) resolve();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const { chunk, rest } = takeSmoothTextChunk(this.pending);
|
|
593
|
+
this.pending = rest;
|
|
594
|
+
this.emit(chunk);
|
|
595
|
+
if (this.pending) {
|
|
596
|
+
this.timer = setTimeout(() => this.drain(), this.intervalMs);
|
|
597
|
+
} else {
|
|
598
|
+
this.timer = null;
|
|
599
|
+
for (const resolve of this.waiters.splice(0)) resolve();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
finish() {
|
|
604
|
+
if (!this.pending && this.timer == null) return Promise.resolve();
|
|
605
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
flush() {
|
|
609
|
+
if (this.timer != null) clearTimeout(this.timer);
|
|
610
|
+
this.timer = null;
|
|
611
|
+
if (this.pending) this.emit(this.pending);
|
|
612
|
+
this.pending = "";
|
|
613
|
+
for (const resolve of this.waiters.splice(0)) resolve();
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function flushSmoothStreams(session) {
|
|
618
|
+
for (const block of session.streamBlocks.values()) block.smooth?.flush();
|
|
619
|
+
}
|
|
620
|
+
|
|
582
621
|
function tokenBreakdown(usage) {
|
|
583
622
|
if (!usage) return null;
|
|
584
623
|
const input = Number(usage.input_tokens ?? usage.inputTokens ?? 0);
|
|
@@ -625,44 +664,74 @@ function historyTokenUsage(messages, models, model) {
|
|
|
625
664
|
};
|
|
626
665
|
}
|
|
627
666
|
|
|
628
|
-
async function processStreamEvent(session, message) {
|
|
629
|
-
if (!session.turn || message.parent_tool_use_id) return;
|
|
630
|
-
const event = message.event || {};
|
|
631
|
-
if (event.type === "message_start") {
|
|
632
|
-
flushSmoothStreams(session);
|
|
633
|
-
session.streamBlocks.clear();
|
|
634
|
-
}
|
|
667
|
+
async function processStreamEvent(session, message) {
|
|
668
|
+
if (!session.turn || message.parent_tool_use_id) return;
|
|
669
|
+
const event = message.event || {};
|
|
670
|
+
if (event.type === "message_start") {
|
|
671
|
+
flushSmoothStreams(session);
|
|
672
|
+
session.streamBlocks.clear();
|
|
673
|
+
}
|
|
635
674
|
if (event.type === "content_block_start") {
|
|
636
|
-
const block = event.content_block || {};
|
|
637
|
-
if (block.type !== "text" && block.type !== "thinking") return;
|
|
638
|
-
const id = nextItemId(session, block.type);
|
|
675
|
+
const block = event.content_block || {};
|
|
676
|
+
if (block.type !== "text" && block.type !== "thinking") return;
|
|
677
|
+
const id = nextItemId(session, block.type);
|
|
639
678
|
const item = block.type === "text"
|
|
640
679
|
? { id, type: "agentMessage", text: "", provider: "Claude" }
|
|
641
680
|
: { id, type: "reasoning", summary: [] };
|
|
642
|
-
const smooth = block.type === "text"
|
|
643
|
-
? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
|
|
644
|
-
: null;
|
|
645
|
-
session.streamBlocks.set(event.index, {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
681
|
+
const smooth = block.type === "text"
|
|
682
|
+
? new SmoothTextStream((delta) => emitDelta(session, "item/agentMessage/delta", id, delta))
|
|
683
|
+
: null;
|
|
684
|
+
session.streamBlocks.set(event.index, {
|
|
685
|
+
id,
|
|
686
|
+
type: block.type,
|
|
687
|
+
text: "",
|
|
688
|
+
smooth,
|
|
689
|
+
languagePending: block.type === "text" && session.turn.koreanRequest ? "" : null,
|
|
690
|
+
holdEnglishProgress: false,
|
|
691
|
+
});
|
|
692
|
+
emitItem(session, "started", item);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (event.type === "content_block_delta") {
|
|
650
696
|
const current = session.streamBlocks.get(event.index);
|
|
651
697
|
if (!current) return;
|
|
652
698
|
const delta = event.delta?.text || event.delta?.thinking || "";
|
|
653
699
|
if (!delta) return;
|
|
654
700
|
current.text += delta;
|
|
655
|
-
if (current.
|
|
656
|
-
|
|
701
|
+
if (current.type === "text") session.turn.sawVisibleText = true;
|
|
702
|
+
if (!current.smooth) {
|
|
703
|
+
emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (current.languagePending != null) {
|
|
707
|
+
current.languagePending += delta;
|
|
708
|
+
const probe = current.languagePending.trimStart();
|
|
709
|
+
const lower = probe.toLowerCase();
|
|
710
|
+
if (!current.holdEnglishProgress && "now".startsWith(lower)) return;
|
|
711
|
+
if (/^now(?:\s|$)/i.test(probe)) {
|
|
712
|
+
current.holdEnglishProgress = true;
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
current.smooth.push(current.languagePending);
|
|
716
|
+
current.languagePending = null;
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
current.smooth.push(delta);
|
|
657
720
|
return;
|
|
658
721
|
}
|
|
659
|
-
if (event.type === "content_block_stop") {
|
|
722
|
+
if (event.type === "content_block_stop") {
|
|
660
723
|
const current = session.streamBlocks.get(event.index);
|
|
661
724
|
if (!current) return;
|
|
725
|
+
if (current.languagePending != null) {
|
|
726
|
+
const visible = normalizeProgressText(session.turn, current.languagePending);
|
|
727
|
+
current.text = visible;
|
|
728
|
+
current.smooth?.push(visible);
|
|
729
|
+
current.languagePending = null;
|
|
730
|
+
}
|
|
662
731
|
await current.smooth?.finish();
|
|
663
|
-
const item = current.type === "text"
|
|
664
|
-
? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
|
|
665
|
-
: { id: current.id, type: "reasoning", summary: [current.text] };
|
|
732
|
+
const item = current.type === "text"
|
|
733
|
+
? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
|
|
734
|
+
: { id: current.id, type: "reasoning", summary: [current.text] };
|
|
666
735
|
emitItem(session, "completed", item);
|
|
667
736
|
session.streamBlocks.delete(event.index);
|
|
668
737
|
}
|
|
@@ -684,22 +753,29 @@ function processAssistant(session, message) {
|
|
|
684
753
|
message.message?.model,
|
|
685
754
|
session.model,
|
|
686
755
|
);
|
|
687
|
-
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
756
|
+
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
757
|
+
const hasToolUse = content.some((block) => block.type === "tool_use");
|
|
758
|
+
const hasVisibleText = session.turn.sawVisibleText || content.some(
|
|
759
|
+
(block) => block.type === "text" && String(block.text || "").trim(),
|
|
760
|
+
);
|
|
761
|
+
// Without partial SDK events, replay completed text before tool items so the
|
|
762
|
+
// visible order still matches the assistant content order.
|
|
763
|
+
if (!session.streamBlocks.size && !session.turn.sawStreamText) {
|
|
764
|
+
for (const block of content) {
|
|
765
|
+
if (block.type !== "text" && block.type !== "thinking") continue;
|
|
766
|
+
const id = nextItemId(session, block.type);
|
|
767
|
+
const item = block.type === "text"
|
|
768
|
+
? { id, type: "agentMessage", text: normalizeProgressText(session.turn, block.text), provider: "Claude" }
|
|
769
|
+
: { id, type: "reasoning", summary: [block.thinking || ""] };
|
|
770
|
+
emitItem(session, "started", item);
|
|
771
|
+
emitItem(session, "completed", item);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (hasToolUse && !hasVisibleText) emitOpeningNotice(session);
|
|
775
|
+
for (const block of content) {
|
|
776
|
+
if (block.type === "tool_use") processToolUse(session, block);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
703
779
|
|
|
704
780
|
function processToolUse(session, block) {
|
|
705
781
|
const name = block.name || "Tool";
|
|
@@ -751,52 +827,89 @@ function fileChanges(name, input) {
|
|
|
751
827
|
return [{ path, kind: { type: name === "Write" ? "add" : "update" }, diff: `@@ -0,0 +1 @@\n${additions}` }];
|
|
752
828
|
}
|
|
753
829
|
|
|
754
|
-
function numberedTaskIndex(subject) {
|
|
755
|
-
const match = String(subject || "").trim().match(/^(\d+)[.)]\s+/);
|
|
756
|
-
return match ? Number(match[1]) : null;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Claude의 Task id는 세션 전체에서 누적된다. 제목 번호가 다시 1부터
|
|
760
|
-
// 시작할 때만 새 계획이며, 3·4번만 갱신되는 턴은 기존 1~6번을 지킨다.
|
|
761
|
-
function prepareTaskPlanForCreate(tasks, subject) {
|
|
762
|
-
if (numberedTaskIndex(subject) === 1) tasks.clear();
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function applyTaskUpdate(tasks, input, turnId) {
|
|
766
|
-
const task = tasks.get(String(input.taskId));
|
|
767
|
-
if (!task) return false;
|
|
768
|
-
if (input.subject) task.subject = input.subject;
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
//
|
|
775
|
-
//
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
830
|
+
function numberedTaskIndex(subject) {
|
|
831
|
+
const match = String(subject || "").trim().match(/^(\d+)[.)]\s+/);
|
|
832
|
+
return match ? Number(match[1]) : null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Claude의 Task id는 세션 전체에서 누적된다. 제목 번호가 다시 1부터
|
|
836
|
+
// 시작할 때만 새 계획이며, 3·4번만 갱신되는 턴은 기존 1~6번을 지킨다.
|
|
837
|
+
function prepareTaskPlanForCreate(tasks, subject) {
|
|
838
|
+
if (numberedTaskIndex(subject) === 1) tasks.clear();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function applyTaskUpdate(tasks, input, turnId, onIntermediate) {
|
|
842
|
+
const task = tasks.get(String(input.taskId));
|
|
843
|
+
if (!task) return false;
|
|
844
|
+
if (input.subject) task.subject = input.subject;
|
|
845
|
+
const status = input.status;
|
|
846
|
+
if (status === "in_progress" || status === "completed") {
|
|
847
|
+
const entries = [...tasks.values()];
|
|
848
|
+
const targetIndex = entries.indexOf(task);
|
|
849
|
+
|
|
850
|
+
// Claude occasionally closes a later pending task at the end of a turn
|
|
851
|
+
// without ever starting it. Keep the visible plan truthful and sequential:
|
|
852
|
+
// every skipped predecessor and the target itself pass through in_progress.
|
|
853
|
+
for (let index = 0; index < targetIndex; index++) {
|
|
854
|
+
const previous = entries[index];
|
|
855
|
+
if (previous.status === "completed") continue;
|
|
856
|
+
if (previous.status !== "in_progress") {
|
|
857
|
+
previous.status = "in_progress";
|
|
858
|
+
previous.turnId = turnId;
|
|
859
|
+
onIntermediate?.();
|
|
860
|
+
}
|
|
861
|
+
previous.status = "completed";
|
|
862
|
+
previous.turnId = turnId;
|
|
863
|
+
onIntermediate?.();
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
for (let index = 0; index < entries.length; index++) {
|
|
867
|
+
const other = entries[index];
|
|
868
|
+
if (other === task || other.status !== "in_progress") continue;
|
|
869
|
+
other.status = index < targetIndex ? "completed" : "pending";
|
|
870
|
+
other.turnId = turnId;
|
|
871
|
+
onIntermediate?.();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (status === "completed" && task.status !== "in_progress" && task.status !== "completed") {
|
|
875
|
+
task.status = "in_progress";
|
|
876
|
+
task.turnId = turnId;
|
|
877
|
+
onIntermediate?.();
|
|
878
|
+
}
|
|
879
|
+
task.status = status;
|
|
880
|
+
} else if (status) {
|
|
881
|
+
task.status = status;
|
|
882
|
+
}
|
|
883
|
+
task.turnId = turnId;
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// TaskList에는 예전 계획까지 함께 들어올 수 있다. 마지막으로 번호가 1부터
|
|
888
|
+
// 시작한 묶음만 현재 계획으로 삼되, 번호가 없는 목록은 손실 없이 그대로 둔다.
|
|
889
|
+
function latestTaskPlan(tasks) {
|
|
890
|
+
const entries = [...tasks.entries()];
|
|
891
|
+
let start = 0;
|
|
892
|
+
for (let index = 0; index < entries.length; index++) {
|
|
893
|
+
if (numberedTaskIndex(entries[index][1].subject) === 1) start = index;
|
|
894
|
+
}
|
|
895
|
+
return new Map(entries.slice(start));
|
|
896
|
+
}
|
|
784
897
|
|
|
785
898
|
function updatePlanFromToolUse(session, name, toolUseId, input) {
|
|
786
|
-
const turnId = session.turn?.id;
|
|
787
|
-
if (name === "TaskCreate") {
|
|
788
|
-
prepareTaskPlanForCreate(session.tasks, input.subject);
|
|
789
|
-
session.tasks.set(`pending:${toolUseId}`, {
|
|
790
|
-
id: `pending:${toolUseId}`,
|
|
791
|
-
subject: input.subject || input.description || "작업",
|
|
899
|
+
const turnId = session.turn?.id;
|
|
900
|
+
if (name === "TaskCreate") {
|
|
901
|
+
prepareTaskPlanForCreate(session.tasks, input.subject);
|
|
902
|
+
session.tasks.set(`pending:${toolUseId}`, {
|
|
903
|
+
id: `pending:${toolUseId}`,
|
|
904
|
+
subject: input.subject || input.description || "작업",
|
|
792
905
|
status: "pending",
|
|
793
|
-
turnId,
|
|
794
|
-
});
|
|
795
|
-
} else if (name === "TaskUpdate") {
|
|
796
|
-
applyTaskUpdate(session.tasks, input, turnId);
|
|
797
|
-
}
|
|
798
|
-
emitPlan(session);
|
|
799
|
-
}
|
|
906
|
+
turnId,
|
|
907
|
+
});
|
|
908
|
+
} else if (name === "TaskUpdate") {
|
|
909
|
+
applyTaskUpdate(session.tasks, input, turnId, () => emitPlan(session));
|
|
910
|
+
}
|
|
911
|
+
emitPlan(session);
|
|
912
|
+
}
|
|
800
913
|
|
|
801
914
|
function updatePlanFromToolResult(session, pending, message) {
|
|
802
915
|
const value = message.tool_use_result;
|
|
@@ -824,10 +937,10 @@ function updatePlanFromToolResult(session, pending, message) {
|
|
|
824
937
|
turnId: previous.get(id)?.turnId ?? turnId,
|
|
825
938
|
});
|
|
826
939
|
}
|
|
827
|
-
session.tasks = latestTaskPlan(session.tasks);
|
|
828
|
-
emitPlan(session);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
940
|
+
session.tasks = latestTaskPlan(session.tasks);
|
|
941
|
+
emitPlan(session);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
831
944
|
|
|
832
945
|
function taskCreatedResult(structured, content) {
|
|
833
946
|
const created = structured?.task || structured;
|
|
@@ -868,78 +981,78 @@ function numberedTaskSubject(subject, index) {
|
|
|
868
981
|
// 그 ID로 묶어 두면 지금 어떤 에이전트가 무슨 도구를 돌리는지 그대로 복원할 수 있다.
|
|
869
982
|
const SUBAGENT_TOOLS = ["Agent", "Task"];
|
|
870
983
|
|
|
871
|
-
function startSubagent(session, block) {
|
|
872
|
-
const input = block.input || {};
|
|
873
|
-
session.subagents.set(block.id, {
|
|
874
|
-
id: block.id,
|
|
875
|
-
taskId: "",
|
|
876
|
-
background: false,
|
|
877
|
-
name: firstLine(input.subagent_type || input.agentType || "agent", 40),
|
|
984
|
+
function startSubagent(session, block) {
|
|
985
|
+
const input = block.input || {};
|
|
986
|
+
session.subagents.set(block.id, {
|
|
987
|
+
id: block.id,
|
|
988
|
+
taskId: "",
|
|
989
|
+
background: false,
|
|
990
|
+
name: firstLine(input.subagent_type || input.agentType || "agent", 40),
|
|
878
991
|
description: firstLine(input.description || input.prompt || "", 120),
|
|
879
992
|
tool: "",
|
|
880
993
|
startedAt: Date.now(),
|
|
881
994
|
});
|
|
882
|
-
emitSubagents(session);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
function isBackgroundSubagentResult(result) {
|
|
886
|
-
return result?.isAsync === true || result?.status === "async_launched";
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Claude Code treats an async Agent result as a launch receipt. The agent remains
|
|
890
|
-
// live until its later task-notification names the same task or tool-use id.
|
|
891
|
-
function keepBackgroundSubagent(session, toolUseId, result) {
|
|
892
|
-
if (!isBackgroundSubagentResult(result)) return false;
|
|
893
|
-
const running = session.subagents.get(toolUseId);
|
|
894
|
-
if (!running) return false;
|
|
895
|
-
running.background = true;
|
|
896
|
-
running.taskId = firstLine(result?.agentId || result?.taskId || "", 80);
|
|
897
|
-
if (running.taskId) {
|
|
898
|
-
session.knownSubagents.set(running.taskId, {
|
|
899
|
-
name: running.name,
|
|
900
|
-
description: running.description,
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
return true;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// SendMessage can wake a completed agent from its transcript. Its new parent
|
|
907
|
-
// tool-use id owns this run, while the stable agent id lets later notifications
|
|
908
|
-
// and further resumes recover the original label.
|
|
909
|
-
function resumeBackgroundSubagent(session, toolUseId, pending, result) {
|
|
910
|
-
const taskId = firstLine(result?.resumedAgentId || "", 80);
|
|
911
|
-
if (!taskId) return false;
|
|
912
|
-
const existing = [...session.subagents.values()].find((agent) => agent.taskId === taskId);
|
|
913
|
-
if (existing) return true;
|
|
914
|
-
const known = session.knownSubagents.get(taskId);
|
|
915
|
-
const input = pending?.input || {};
|
|
916
|
-
const running = {
|
|
917
|
-
id: toolUseId,
|
|
918
|
-
taskId,
|
|
919
|
-
background: true,
|
|
920
|
-
name: known?.name || "agent",
|
|
921
|
-
description: known?.description || firstLine(input.summary || input.message || "", 120),
|
|
922
|
-
tool: "",
|
|
923
|
-
startedAt: Date.now(),
|
|
924
|
-
};
|
|
925
|
-
session.subagents.set(toolUseId, running);
|
|
926
|
-
session.knownSubagents.set(taskId, {
|
|
927
|
-
name: running.name,
|
|
928
|
-
description: running.description,
|
|
929
|
-
});
|
|
930
|
-
emitSubagents(session);
|
|
931
|
-
return true;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function findSubagent(session, id) {
|
|
935
|
-
return session.subagents.get(id)
|
|
936
|
-
|| [...session.subagents.values()].find((agent) => agent.taskId === id);
|
|
937
|
-
}
|
|
995
|
+
emitSubagents(session);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function isBackgroundSubagentResult(result) {
|
|
999
|
+
return result?.isAsync === true || result?.status === "async_launched";
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Claude Code treats an async Agent result as a launch receipt. The agent remains
|
|
1003
|
+
// live until its later task-notification names the same task or tool-use id.
|
|
1004
|
+
function keepBackgroundSubagent(session, toolUseId, result) {
|
|
1005
|
+
if (!isBackgroundSubagentResult(result)) return false;
|
|
1006
|
+
const running = session.subagents.get(toolUseId);
|
|
1007
|
+
if (!running) return false;
|
|
1008
|
+
running.background = true;
|
|
1009
|
+
running.taskId = firstLine(result?.agentId || result?.taskId || "", 80);
|
|
1010
|
+
if (running.taskId) {
|
|
1011
|
+
session.knownSubagents.set(running.taskId, {
|
|
1012
|
+
name: running.name,
|
|
1013
|
+
description: running.description,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// SendMessage can wake a completed agent from its transcript. Its new parent
|
|
1020
|
+
// tool-use id owns this run, while the stable agent id lets later notifications
|
|
1021
|
+
// and further resumes recover the original label.
|
|
1022
|
+
function resumeBackgroundSubagent(session, toolUseId, pending, result) {
|
|
1023
|
+
const taskId = firstLine(result?.resumedAgentId || "", 80);
|
|
1024
|
+
if (!taskId) return false;
|
|
1025
|
+
const existing = [...session.subagents.values()].find((agent) => agent.taskId === taskId);
|
|
1026
|
+
if (existing) return true;
|
|
1027
|
+
const known = session.knownSubagents.get(taskId);
|
|
1028
|
+
const input = pending?.input || {};
|
|
1029
|
+
const running = {
|
|
1030
|
+
id: toolUseId,
|
|
1031
|
+
taskId,
|
|
1032
|
+
background: true,
|
|
1033
|
+
name: known?.name || "agent",
|
|
1034
|
+
description: known?.description || firstLine(input.summary || input.message || "", 120),
|
|
1035
|
+
tool: "",
|
|
1036
|
+
startedAt: Date.now(),
|
|
1037
|
+
};
|
|
1038
|
+
session.subagents.set(toolUseId, running);
|
|
1039
|
+
session.knownSubagents.set(taskId, {
|
|
1040
|
+
name: running.name,
|
|
1041
|
+
description: running.description,
|
|
1042
|
+
});
|
|
1043
|
+
emitSubagents(session);
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function findSubagent(session, id) {
|
|
1048
|
+
return session.subagents.get(id)
|
|
1049
|
+
|| [...session.subagents.values()].find((agent) => agent.taskId === id);
|
|
1050
|
+
}
|
|
938
1051
|
|
|
939
1052
|
// 서브에이전트가 실제로 무엇을 했는지는 자식 메시지에만 남는다. 열람용 기록은 여기서
|
|
940
1053
|
// 한 줄씩 흘려보내고, 목록 행에 쓸 현재 도구만 따로 갱신한다.
|
|
941
|
-
function recordSubagentMessage(session, message) {
|
|
942
|
-
const running = findSubagent(session, message.parent_tool_use_id);
|
|
1054
|
+
function recordSubagentMessage(session, message) {
|
|
1055
|
+
const running = findSubagent(session, message.parent_tool_use_id);
|
|
943
1056
|
if (!running) return;
|
|
944
1057
|
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
945
1058
|
let toolChanged = false;
|
|
@@ -960,8 +1073,8 @@ function recordSubagentMessage(session, message) {
|
|
|
960
1073
|
if (toolChanged) emitSubagents(session);
|
|
961
1074
|
}
|
|
962
1075
|
|
|
963
|
-
function recordSubagentResult(session, message) {
|
|
964
|
-
const running = findSubagent(session, message.parent_tool_use_id);
|
|
1076
|
+
function recordSubagentResult(session, message) {
|
|
1077
|
+
const running = findSubagent(session, message.parent_tool_use_id);
|
|
965
1078
|
if (!running) return;
|
|
966
1079
|
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
967
1080
|
for (const block of content) {
|
|
@@ -997,32 +1110,32 @@ function subagentToolLabel(block) {
|
|
|
997
1110
|
return text ? `${name}(${text})` : name;
|
|
998
1111
|
}
|
|
999
1112
|
|
|
1000
|
-
function finishSubagent(session, toolUseId) {
|
|
1001
|
-
if (!session.subagents.delete(toolUseId)) return;
|
|
1002
|
-
emitSubagents(session);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
function finishSubagentTask(session, taskId) {
|
|
1006
|
-
if (!taskId) return false;
|
|
1007
|
-
const entry = [...session.subagents.entries()].find(([, agent]) => agent.taskId === taskId);
|
|
1008
|
-
if (!entry) return false;
|
|
1009
|
-
session.subagents.delete(entry[0]);
|
|
1010
|
-
emitSubagents(session);
|
|
1011
|
-
return true;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function clearForegroundSubagents(session) {
|
|
1015
|
-
let changed = false;
|
|
1016
|
-
for (const [id, agent] of session.subagents) {
|
|
1017
|
-
if (agent.background) continue;
|
|
1018
|
-
session.subagents.delete(id);
|
|
1019
|
-
changed = true;
|
|
1020
|
-
}
|
|
1021
|
-
if (changed) emitSubagents(session);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function clearSubagents(session) {
|
|
1025
|
-
if (!session.subagents.size) return;
|
|
1113
|
+
function finishSubagent(session, toolUseId) {
|
|
1114
|
+
if (!session.subagents.delete(toolUseId)) return;
|
|
1115
|
+
emitSubagents(session);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function finishSubagentTask(session, taskId) {
|
|
1119
|
+
if (!taskId) return false;
|
|
1120
|
+
const entry = [...session.subagents.entries()].find(([, agent]) => agent.taskId === taskId);
|
|
1121
|
+
if (!entry) return false;
|
|
1122
|
+
session.subagents.delete(entry[0]);
|
|
1123
|
+
emitSubagents(session);
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function clearForegroundSubagents(session) {
|
|
1128
|
+
let changed = false;
|
|
1129
|
+
for (const [id, agent] of session.subagents) {
|
|
1130
|
+
if (agent.background) continue;
|
|
1131
|
+
session.subagents.delete(id);
|
|
1132
|
+
changed = true;
|
|
1133
|
+
}
|
|
1134
|
+
if (changed) emitSubagents(session);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function clearSubagents(session) {
|
|
1138
|
+
if (!session.subagents.size) return;
|
|
1026
1139
|
session.subagents.clear();
|
|
1027
1140
|
emitSubagents(session);
|
|
1028
1141
|
}
|
|
@@ -1042,87 +1155,87 @@ function emitSubagents(session) {
|
|
|
1042
1155
|
tool: agent.tool,
|
|
1043
1156
|
elapsedMs: Date.now() - agent.startedAt,
|
|
1044
1157
|
})),
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
function messageTextParts(message) {
|
|
1049
|
-
const content = message.message?.content;
|
|
1050
|
-
if (typeof content === "string") return [content];
|
|
1051
|
-
if (!Array.isArray(content)) return [];
|
|
1052
|
-
return content
|
|
1053
|
-
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
1054
|
-
.map((block) => block.text);
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
function notificationTag(body, name) {
|
|
1058
|
-
const match = body.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
|
|
1059
|
-
return match?.[1]?.trim() || "";
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function taskNotifications(message) {
|
|
1063
|
-
if (message.origin?.kind !== "task-notification"
|
|
1064
|
-
&& !messageTextParts(message).some((text) => text.includes("<task-notification>"))) {
|
|
1065
|
-
return [];
|
|
1066
|
-
}
|
|
1067
|
-
const notifications = [];
|
|
1068
|
-
for (const text of messageTextParts(message)) {
|
|
1069
|
-
for (const match of text.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)) {
|
|
1070
|
-
const body = match[1];
|
|
1071
|
-
notifications.push({
|
|
1072
|
-
taskId: notificationTag(body, "task-id"),
|
|
1073
|
-
toolUseId: notificationTag(body, "tool-use-id"),
|
|
1074
|
-
status: notificationTag(body, "status"),
|
|
1075
|
-
summary: notificationTag(body, "summary"),
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
return notifications;
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function finishNotifiedSubagents(session, notifications) {
|
|
1083
|
-
for (const notification of notifications) {
|
|
1084
|
-
const byToolUse = notification.toolUseId
|
|
1085
|
-
? session.subagents.get(notification.toolUseId)
|
|
1086
|
-
: null;
|
|
1087
|
-
const running = byToolUse || (notification.taskId
|
|
1088
|
-
? [...session.subagents.values()].find((agent) => agent.taskId === notification.taskId)
|
|
1089
|
-
: null);
|
|
1090
|
-
if (!running) continue;
|
|
1091
|
-
emitSubagentLine(session, running.id, {
|
|
1092
|
-
kind: notification.status === "completed" ? "result" : "error",
|
|
1093
|
-
text: notification.summary || notification.status || "완료됨",
|
|
1094
|
-
});
|
|
1095
|
-
session.subagents.delete(running.id);
|
|
1096
|
-
emitSubagents(session);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function processUser(session, message) {
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function messageTextParts(message) {
|
|
1162
|
+
const content = message.message?.content;
|
|
1163
|
+
if (typeof content === "string") return [content];
|
|
1164
|
+
if (!Array.isArray(content)) return [];
|
|
1165
|
+
return content
|
|
1166
|
+
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
1167
|
+
.map((block) => block.text);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function notificationTag(body, name) {
|
|
1171
|
+
const match = body.match(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`));
|
|
1172
|
+
return match?.[1]?.trim() || "";
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function taskNotifications(message) {
|
|
1176
|
+
if (message.origin?.kind !== "task-notification"
|
|
1177
|
+
&& !messageTextParts(message).some((text) => text.includes("<task-notification>"))) {
|
|
1178
|
+
return [];
|
|
1179
|
+
}
|
|
1180
|
+
const notifications = [];
|
|
1181
|
+
for (const text of messageTextParts(message)) {
|
|
1182
|
+
for (const match of text.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)) {
|
|
1183
|
+
const body = match[1];
|
|
1184
|
+
notifications.push({
|
|
1185
|
+
taskId: notificationTag(body, "task-id"),
|
|
1186
|
+
toolUseId: notificationTag(body, "tool-use-id"),
|
|
1187
|
+
status: notificationTag(body, "status"),
|
|
1188
|
+
summary: notificationTag(body, "summary"),
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return notifications;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function finishNotifiedSubagents(session, notifications) {
|
|
1196
|
+
for (const notification of notifications) {
|
|
1197
|
+
const byToolUse = notification.toolUseId
|
|
1198
|
+
? session.subagents.get(notification.toolUseId)
|
|
1199
|
+
: null;
|
|
1200
|
+
const running = byToolUse || (notification.taskId
|
|
1201
|
+
? [...session.subagents.values()].find((agent) => agent.taskId === notification.taskId)
|
|
1202
|
+
: null);
|
|
1203
|
+
if (!running) continue;
|
|
1204
|
+
emitSubagentLine(session, running.id, {
|
|
1205
|
+
kind: notification.status === "completed" ? "result" : "error",
|
|
1206
|
+
text: notification.summary || notification.status || "완료됨",
|
|
1207
|
+
});
|
|
1208
|
+
session.subagents.delete(running.id);
|
|
1209
|
+
emitSubagents(session);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function processUser(session, message) {
|
|
1101
1214
|
// 자식 tool_result의 tool_use_id는 부모 세션의 것과 다른 공간이므로, 부모 흐름에
|
|
1102
1215
|
// 섞이기 전에 서브에이전트 기록으로 보낸다.
|
|
1103
1216
|
if (message.parent_tool_use_id) {
|
|
1104
|
-
recordSubagentResult(session, message);
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
const notifications = taskNotifications(message);
|
|
1108
|
-
if (notifications.length) {
|
|
1109
|
-
if (!session.turn) beginTurn(session);
|
|
1110
|
-
finishNotifiedSubagents(session, notifications);
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
1114
|
-
for (const block of content) {
|
|
1115
|
-
if (block.type !== "tool_result") continue;
|
|
1116
|
-
const pending = session.tools.get(block.tool_use_id);
|
|
1117
|
-
const staysInBackground = SUBAGENT_TOOLS.includes(pending?.name)
|
|
1118
|
-
&& keepBackgroundSubagent(session, block.tool_use_id, message.tool_use_result);
|
|
1119
|
-
if (!staysInBackground) finishSubagent(session, block.tool_use_id);
|
|
1120
|
-
if (pending?.name === "SendMessage") {
|
|
1121
|
-
resumeBackgroundSubagent(session, block.tool_use_id, pending, message.tool_use_result);
|
|
1122
|
-
} else if (pending?.name === "TaskStop" && message.tool_use_result?.success !== false) {
|
|
1123
|
-
finishSubagentTask(session, firstLine(pending.input?.task_id || "", 80));
|
|
1124
|
-
}
|
|
1125
|
-
if (!pending) continue;
|
|
1217
|
+
recordSubagentResult(session, message);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
const notifications = taskNotifications(message);
|
|
1221
|
+
if (notifications.length) {
|
|
1222
|
+
if (!session.turn) beginTurn(session);
|
|
1223
|
+
finishNotifiedSubagents(session, notifications);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
1227
|
+
for (const block of content) {
|
|
1228
|
+
if (block.type !== "tool_result") continue;
|
|
1229
|
+
const pending = session.tools.get(block.tool_use_id);
|
|
1230
|
+
const staysInBackground = SUBAGENT_TOOLS.includes(pending?.name)
|
|
1231
|
+
&& keepBackgroundSubagent(session, block.tool_use_id, message.tool_use_result);
|
|
1232
|
+
if (!staysInBackground) finishSubagent(session, block.tool_use_id);
|
|
1233
|
+
if (pending?.name === "SendMessage") {
|
|
1234
|
+
resumeBackgroundSubagent(session, block.tool_use_id, pending, message.tool_use_result);
|
|
1235
|
+
} else if (pending?.name === "TaskStop" && message.tool_use_result?.success !== false) {
|
|
1236
|
+
finishSubagentTask(session, firstLine(pending.input?.task_id || "", 80));
|
|
1237
|
+
}
|
|
1238
|
+
if (!pending) continue;
|
|
1126
1239
|
pending.toolUseId = block.tool_use_id;
|
|
1127
1240
|
if (pending.suppressed) {
|
|
1128
1241
|
updatePlanFromToolResult(session, pending, message);
|
|
@@ -1209,10 +1322,10 @@ async function runPendingPrompt(session) {
|
|
|
1209
1322
|
}
|
|
1210
1323
|
}
|
|
1211
1324
|
|
|
1212
|
-
function finishTurn(session, error, durationMs) {
|
|
1213
|
-
if (!session.turn) return;
|
|
1214
|
-
flushSmoothStreams(session);
|
|
1215
|
-
clearForegroundSubagents(session);
|
|
1325
|
+
function finishTurn(session, error, durationMs) {
|
|
1326
|
+
if (!session.turn) return;
|
|
1327
|
+
flushSmoothStreams(session);
|
|
1328
|
+
clearForegroundSubagents(session);
|
|
1216
1329
|
const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
|
|
1217
1330
|
if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
|
|
1218
1331
|
if (durationMs != null) turn.durationMs = durationMs;
|
|
@@ -1228,7 +1341,7 @@ async function consume(session) {
|
|
|
1228
1341
|
if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
|
|
1229
1342
|
if (session.turn) session.turn.sawStreamText = true;
|
|
1230
1343
|
}
|
|
1231
|
-
await processStreamEvent(session, message);
|
|
1344
|
+
await processStreamEvent(session, message);
|
|
1232
1345
|
} else if (message.type === "assistant") processAssistant(session, message);
|
|
1233
1346
|
else if (message.type === "user") processUser(session, message);
|
|
1234
1347
|
else if (message.type === "result") await processResult(session, message);
|
|
@@ -1309,28 +1422,36 @@ async function runPrompt(session, params) {
|
|
|
1309
1422
|
if (effort) {
|
|
1310
1423
|
await session.query.applyFlagSettings({ effortLevel: effort });
|
|
1311
1424
|
}
|
|
1312
|
-
session.effort = effort;
|
|
1313
|
-
await applyPermissionMode(session, params.permissionMode);
|
|
1314
|
-
const
|
|
1315
|
-
session.
|
|
1316
|
-
|
|
1317
|
-
|
|
1425
|
+
session.effort = effort;
|
|
1426
|
+
await applyPermissionMode(session, params.permissionMode);
|
|
1427
|
+
const content = await inputContent(params.input, params.handoffContext);
|
|
1428
|
+
const turnId = beginTurn(session, params.input);
|
|
1429
|
+
session.queue.push({
|
|
1430
|
+
type: "user",
|
|
1431
|
+
message: { role: "user", content },
|
|
1318
1432
|
parent_tool_use_id: null,
|
|
1319
1433
|
session_id: id,
|
|
1320
1434
|
origin: { kind: "human" },
|
|
1321
1435
|
});
|
|
1322
|
-
return { turn: { id: turnId } };
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
// A background task notification is an internal user message that starts its
|
|
1326
|
-
// own Claude response even though the host did not submit a new prompt.
|
|
1327
|
-
function beginTurn(session) {
|
|
1436
|
+
return { turn: { id: turnId } };
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// A background task notification is an internal user message that starts its
|
|
1440
|
+
// own Claude response even though the host did not submit a new prompt.
|
|
1441
|
+
function beginTurn(session, input = []) {
|
|
1328
1442
|
const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
|
|
1329
|
-
session.turn = {
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1443
|
+
session.turn = {
|
|
1444
|
+
id: turnId,
|
|
1445
|
+
sawStreamText: false,
|
|
1446
|
+
sawVisibleText: false,
|
|
1447
|
+
koreanRequest: isKoreanPrompt(input),
|
|
1448
|
+
openingNotice: openingNotice(input),
|
|
1449
|
+
openingNoticeEmitted: false,
|
|
1450
|
+
};
|
|
1451
|
+
session.lastContextUsage = null;
|
|
1452
|
+
notify("turn/started", { threadId: session.id, turn: { id: turnId } });
|
|
1453
|
+
return turnId;
|
|
1454
|
+
}
|
|
1334
1455
|
|
|
1335
1456
|
function contentBlocks(message) {
|
|
1336
1457
|
const content = message?.content;
|
|
@@ -1355,7 +1476,7 @@ function isInternalHistoryText(message, text) {
|
|
|
1355
1476
|
].includes(tag);
|
|
1356
1477
|
}
|
|
1357
1478
|
|
|
1358
|
-
function historyState(messages) {
|
|
1479
|
+
function historyState(messages) {
|
|
1359
1480
|
const turns = [];
|
|
1360
1481
|
let turn = null;
|
|
1361
1482
|
const tools = new Map();
|
|
@@ -1391,14 +1512,14 @@ function historyState(messages) {
|
|
|
1391
1512
|
if (block.type === "text") turn.items.push({ id: `${message.uuid}-text`, type: "agentMessage", text: block.text || "", provider: "Claude" });
|
|
1392
1513
|
else if (block.type === "thinking") turn.items.push({ id: `${message.uuid}-thinking`, type: "reasoning", summary: [block.thinking || ""] });
|
|
1393
1514
|
else if (block.type === "tool_use") {
|
|
1394
|
-
const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
|
|
1395
|
-
tools.set(block.id, pending);
|
|
1396
|
-
if (block.name === "TaskCreate") {
|
|
1397
|
-
prepareTaskPlanForCreate(tasks, block.input?.subject);
|
|
1398
|
-
tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "작업", status: "pending", turnId: turn.id });
|
|
1399
|
-
} else if (block.name === "TaskUpdate") {
|
|
1400
|
-
applyTaskUpdate(tasks, block.input || {}, turn.id);
|
|
1401
|
-
} else if (!["TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
|
|
1515
|
+
const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
|
|
1516
|
+
tools.set(block.id, pending);
|
|
1517
|
+
if (block.name === "TaskCreate") {
|
|
1518
|
+
prepareTaskPlanForCreate(tasks, block.input?.subject);
|
|
1519
|
+
tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "작업", status: "pending", turnId: turn.id });
|
|
1520
|
+
} else if (block.name === "TaskUpdate") {
|
|
1521
|
+
applyTaskUpdate(tasks, block.input || {}, turn.id);
|
|
1522
|
+
} else if (!["TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
|
|
1402
1523
|
}
|
|
1403
1524
|
}
|
|
1404
1525
|
} else if (message.type === "user") {
|
|
@@ -1415,12 +1536,12 @@ function historyState(messages) {
|
|
|
1415
1536
|
tasks.set(temporary.id, temporary);
|
|
1416
1537
|
}
|
|
1417
1538
|
} else if (pending.name === "TaskList" && Array.isArray(message.tool_use_result?.tasks)) {
|
|
1418
|
-
const known = new Map(tasks);
|
|
1419
|
-
tasks.clear();
|
|
1420
|
-
for (const task of message.tool_use_result.tasks) tasks.set(String(task.id), { id: String(task.id), subject: task.subject || "작업", status: task.status || "pending", turnId: known.get(String(task.id))?.turnId ?? turn.id });
|
|
1421
|
-
const current = latestTaskPlan(tasks);
|
|
1422
|
-
tasks.clear();
|
|
1423
|
-
for (const [id, task] of current) tasks.set(id, task);
|
|
1539
|
+
const known = new Map(tasks);
|
|
1540
|
+
tasks.clear();
|
|
1541
|
+
for (const task of message.tool_use_result.tasks) tasks.set(String(task.id), { id: String(task.id), subject: task.subject || "작업", status: task.status || "pending", turnId: known.get(String(task.id))?.turnId ?? turn.id });
|
|
1542
|
+
const current = latestTaskPlan(tasks);
|
|
1543
|
+
tasks.clear();
|
|
1544
|
+
for (const [id, task] of current) tasks.set(id, task);
|
|
1424
1545
|
} else if (pending.item) {
|
|
1425
1546
|
const output = toolOutput(block.content, message.tool_use_result);
|
|
1426
1547
|
Object.assign(pending.item, pending.item.type === "commandExecution"
|
|
@@ -1436,17 +1557,17 @@ function historyState(messages) {
|
|
|
1436
1557
|
const text = [...tasks.values()].map((task, index) => `${task.status === "completed" ? "✓" : task.status === "in_progress" ? "▸" : "□"} ${numberedTaskSubject(task.subject, index)}`).join("\n");
|
|
1437
1558
|
turn.items.push({ id: "claude-plan-latest", type: "plan", text });
|
|
1438
1559
|
}
|
|
1439
|
-
return {
|
|
1440
|
-
tasks,
|
|
1441
|
-
turns: turns
|
|
1442
|
-
.filter((candidate) => !candidate.synthetic)
|
|
1443
|
-
.map(({ synthetic: _, ...candidate }) => candidate),
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
function historyTurns(messages) {
|
|
1448
|
-
return historyState(messages).turns;
|
|
1449
|
-
}
|
|
1560
|
+
return {
|
|
1561
|
+
tasks,
|
|
1562
|
+
turns: turns
|
|
1563
|
+
.filter((candidate) => !candidate.synthetic)
|
|
1564
|
+
.map(({ synthetic: _, ...candidate }) => candidate),
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function historyTurns(messages) {
|
|
1569
|
+
return historyState(messages).turns;
|
|
1570
|
+
}
|
|
1450
1571
|
|
|
1451
1572
|
async function dispatch(method, params = {}) {
|
|
1452
1573
|
if (method === "model/list") return loadModelCatalog(params);
|
|
@@ -1471,11 +1592,11 @@ async function dispatch(method, params = {}) {
|
|
|
1471
1592
|
}
|
|
1472
1593
|
if (method === "session/resume") {
|
|
1473
1594
|
const id = liveSessionId(params.sessionId);
|
|
1474
|
-
const existing = sessions.get(id);
|
|
1475
|
-
if (existing) {
|
|
1476
|
-
const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
|
|
1477
|
-
if (!existing.tasks.size) existing.tasks = historyState(messages).tasks;
|
|
1478
|
-
return {
|
|
1595
|
+
const existing = sessions.get(id);
|
|
1596
|
+
if (existing) {
|
|
1597
|
+
const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
|
|
1598
|
+
if (!existing.tasks.size) existing.tasks = historyState(messages).tasks;
|
|
1599
|
+
return {
|
|
1479
1600
|
id,
|
|
1480
1601
|
thread: { id, turns: [] },
|
|
1481
1602
|
initialTurnsPage: { data: [], nextCursor: null },
|
|
@@ -1499,11 +1620,11 @@ async function dispatch(method, params = {}) {
|
|
|
1499
1620
|
model: params.model || lastModel || params.fallbackModel,
|
|
1500
1621
|
effort: params.effort || params.fallbackEffort,
|
|
1501
1622
|
}, id);
|
|
1502
|
-
const tokenUsage = historyTokenUsage(messages, session.models, session.model);
|
|
1503
|
-
// Seed the live session so the next turn keeps reporting a full context.
|
|
1504
|
-
session.lastContextUsage = tokenUsage?.last || null;
|
|
1505
|
-
session.lastContextWindow = tokenUsage?.modelContextWindow || 0;
|
|
1506
|
-
session.tasks = historyState(messages).tasks;
|
|
1623
|
+
const tokenUsage = historyTokenUsage(messages, session.models, session.model);
|
|
1624
|
+
// Seed the live session so the next turn keeps reporting a full context.
|
|
1625
|
+
session.lastContextUsage = tokenUsage?.last || null;
|
|
1626
|
+
session.lastContextWindow = tokenUsage?.modelContextWindow || 0;
|
|
1627
|
+
session.tasks = historyState(messages).tasks;
|
|
1507
1628
|
return {
|
|
1508
1629
|
id,
|
|
1509
1630
|
thread: { id, turns: [] },
|
|
@@ -1605,7 +1726,7 @@ async function dispatch(method, params = {}) {
|
|
|
1605
1726
|
throw new Error(`지원하지 않는 Claude 브리지 메서드: ${method}`);
|
|
1606
1727
|
}
|
|
1607
1728
|
|
|
1608
|
-
async function runSelfTest() {
|
|
1729
|
+
async function runSelfTest() {
|
|
1609
1730
|
const user = (uuid, text) => ({
|
|
1610
1731
|
type: "user",
|
|
1611
1732
|
uuid,
|
|
@@ -1636,70 +1757,89 @@ async function runSelfTest() {
|
|
|
1636
1757
|
["say hello", "claude:claude-sonnet-5"],
|
|
1637
1758
|
["hay zzz", "claude:claude-haiku-4-5-20251001"],
|
|
1638
1759
|
];
|
|
1639
|
-
if (turns.length !== expected.length
|
|
1640
|
-
|| prompts.some((prompt, index) => prompt?.content?.[0]?.text !== expected[index][0]
|
|
1641
|
-
|| prompt.model !== expected[index][1])) {
|
|
1642
|
-
throw new Error(`Claude history self-test failed: ${JSON.stringify(turns)}`);
|
|
1643
|
-
}
|
|
1644
|
-
const taskUse = (uuid, id, name, input) => ({
|
|
1645
|
-
type: "assistant",
|
|
1646
|
-
uuid,
|
|
1647
|
-
message: {
|
|
1648
|
-
role: "assistant",
|
|
1649
|
-
model: "claude-opus-5",
|
|
1650
|
-
content: [{ type: "tool_use", id, name, input }],
|
|
1651
|
-
},
|
|
1652
|
-
});
|
|
1653
|
-
const taskResult = (uuid, id, toolUseResult, content) => ({
|
|
1654
|
-
type: "user",
|
|
1655
|
-
uuid,
|
|
1656
|
-
message: { role: "user", content: [{ type: "tool_result", tool_use_id: id, content }] },
|
|
1657
|
-
tool_use_result: toolUseResult,
|
|
1658
|
-
});
|
|
1659
|
-
const taskMessages = [user("plan", "작업을 진행해")];
|
|
1660
|
-
for (let index = 1; index <= 6; index++) {
|
|
1661
|
-
const id = String(24 + index);
|
|
1662
|
-
const toolId = `create-${id}`;
|
|
1663
|
-
const subject = `${index}. 작업 ${index}`;
|
|
1664
|
-
taskMessages.push(
|
|
1665
|
-
taskUse(`create-use-${id}`, toolId, "TaskCreate", { subject }),
|
|
1666
|
-
taskResult(`create-result-${id}`, toolId, { task: { id, subject } }, `Task #${id} created successfully: ${subject}`),
|
|
1667
|
-
);
|
|
1668
|
-
}
|
|
1669
|
-
taskMessages.push(
|
|
1670
|
-
taskUse("update-25-start", "update-25-start", "TaskUpdate", { taskId: "25", status: "in_progress" }),
|
|
1671
|
-
taskUse("update-25-done", "update-25-done", "TaskUpdate", { taskId: "25", status: "completed" }),
|
|
1672
|
-
taskUse("update-26-start", "update-26-start", "TaskUpdate", { taskId: "26", status: "in_progress" }),
|
|
1673
|
-
);
|
|
1674
|
-
const restoredTasks = historyState(taskMessages).tasks;
|
|
1675
|
-
if ([...restoredTasks.keys()].join(",") !== "25,26,27,28,29,30"
|
|
1676
|
-
|| restoredTasks.get("25")?.status !== "completed"
|
|
1677
|
-
|| restoredTasks.get("26")?.status !== "in_progress"
|
|
1678
|
-
|| [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
|
|
1679
|
-
throw new Error(`Claude task resume self-test failed: ${JSON.stringify([...restoredTasks])}`);
|
|
1680
|
-
}
|
|
1681
|
-
applyTaskUpdate(restoredTasks, { taskId: "26", status: "completed" }, "resumed-turn");
|
|
1682
|
-
applyTaskUpdate(restoredTasks, { taskId: "27", status: "in_progress" }, "resumed-turn");
|
|
1683
|
-
if (restoredTasks.size !== 6
|
|
1684
|
-
|| restoredTasks.get("25")?.status !== "completed"
|
|
1685
|
-
|| restoredTasks.get("26")?.status !== "completed"
|
|
1686
|
-
|| restoredTasks.get("27")?.status !== "in_progress"
|
|
1687
|
-
|| [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
|
|
1688
|
-
throw new Error(`Claude sequential task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
|
|
1689
|
-
}
|
|
1690
|
-
const
|
|
1691
|
-
["
|
|
1692
|
-
["
|
|
1693
|
-
|
|
1694
|
-
]);
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
if (
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1760
|
+
if (turns.length !== expected.length
|
|
1761
|
+
|| prompts.some((prompt, index) => prompt?.content?.[0]?.text !== expected[index][0]
|
|
1762
|
+
|| prompt.model !== expected[index][1])) {
|
|
1763
|
+
throw new Error(`Claude history self-test failed: ${JSON.stringify(turns)}`);
|
|
1764
|
+
}
|
|
1765
|
+
const taskUse = (uuid, id, name, input) => ({
|
|
1766
|
+
type: "assistant",
|
|
1767
|
+
uuid,
|
|
1768
|
+
message: {
|
|
1769
|
+
role: "assistant",
|
|
1770
|
+
model: "claude-opus-5",
|
|
1771
|
+
content: [{ type: "tool_use", id, name, input }],
|
|
1772
|
+
},
|
|
1773
|
+
});
|
|
1774
|
+
const taskResult = (uuid, id, toolUseResult, content) => ({
|
|
1775
|
+
type: "user",
|
|
1776
|
+
uuid,
|
|
1777
|
+
message: { role: "user", content: [{ type: "tool_result", tool_use_id: id, content }] },
|
|
1778
|
+
tool_use_result: toolUseResult,
|
|
1779
|
+
});
|
|
1780
|
+
const taskMessages = [user("plan", "작업을 진행해")];
|
|
1781
|
+
for (let index = 1; index <= 6; index++) {
|
|
1782
|
+
const id = String(24 + index);
|
|
1783
|
+
const toolId = `create-${id}`;
|
|
1784
|
+
const subject = `${index}. 작업 ${index}`;
|
|
1785
|
+
taskMessages.push(
|
|
1786
|
+
taskUse(`create-use-${id}`, toolId, "TaskCreate", { subject }),
|
|
1787
|
+
taskResult(`create-result-${id}`, toolId, { task: { id, subject } }, `Task #${id} created successfully: ${subject}`),
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
taskMessages.push(
|
|
1791
|
+
taskUse("update-25-start", "update-25-start", "TaskUpdate", { taskId: "25", status: "in_progress" }),
|
|
1792
|
+
taskUse("update-25-done", "update-25-done", "TaskUpdate", { taskId: "25", status: "completed" }),
|
|
1793
|
+
taskUse("update-26-start", "update-26-start", "TaskUpdate", { taskId: "26", status: "in_progress" }),
|
|
1794
|
+
);
|
|
1795
|
+
const restoredTasks = historyState(taskMessages).tasks;
|
|
1796
|
+
if ([...restoredTasks.keys()].join(",") !== "25,26,27,28,29,30"
|
|
1797
|
+
|| restoredTasks.get("25")?.status !== "completed"
|
|
1798
|
+
|| restoredTasks.get("26")?.status !== "in_progress"
|
|
1799
|
+
|| [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
|
|
1800
|
+
throw new Error(`Claude task resume self-test failed: ${JSON.stringify([...restoredTasks])}`);
|
|
1801
|
+
}
|
|
1802
|
+
applyTaskUpdate(restoredTasks, { taskId: "26", status: "completed" }, "resumed-turn");
|
|
1803
|
+
applyTaskUpdate(restoredTasks, { taskId: "27", status: "in_progress" }, "resumed-turn");
|
|
1804
|
+
if (restoredTasks.size !== 6
|
|
1805
|
+
|| restoredTasks.get("25")?.status !== "completed"
|
|
1806
|
+
|| restoredTasks.get("26")?.status !== "completed"
|
|
1807
|
+
|| restoredTasks.get("27")?.status !== "in_progress"
|
|
1808
|
+
|| [...restoredTasks.values()].filter((task) => task.status === "in_progress").length !== 1) {
|
|
1809
|
+
throw new Error(`Claude sequential task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
|
|
1810
|
+
}
|
|
1811
|
+
const skippedTasks = new Map([
|
|
1812
|
+
["1", { id: "1", subject: "1. 조사", status: "pending" }],
|
|
1813
|
+
["2", { id: "2", subject: "2. 분석", status: "pending" }],
|
|
1814
|
+
["3", { id: "3", subject: "3. 검증", status: "pending" }],
|
|
1815
|
+
]);
|
|
1816
|
+
const transitions = [];
|
|
1817
|
+
const snapshot = () => transitions.push([...skippedTasks.values()].map((task) => task.status).join(","));
|
|
1818
|
+
applyTaskUpdate(skippedTasks, { taskId: "3", status: "completed" }, "turn", snapshot);
|
|
1819
|
+
snapshot();
|
|
1820
|
+
if (transitions.join("|") !== [
|
|
1821
|
+
"in_progress,pending,pending",
|
|
1822
|
+
"completed,pending,pending",
|
|
1823
|
+
"completed,in_progress,pending",
|
|
1824
|
+
"completed,completed,pending",
|
|
1825
|
+
"completed,completed,in_progress",
|
|
1826
|
+
"completed,completed,completed",
|
|
1827
|
+
].join("|")) {
|
|
1828
|
+
throw new Error(`Claude skipped task transition self-test failed: ${transitions.join("|")}`);
|
|
1829
|
+
}
|
|
1830
|
+
const mixedPlans = new Map([
|
|
1831
|
+
["old-1", { subject: "1. 이전 작업", status: "completed" }],
|
|
1832
|
+
["old-2", { subject: "2. 이전 검증", status: "completed" }],
|
|
1833
|
+
...[...restoredTasks],
|
|
1834
|
+
]);
|
|
1835
|
+
if ([...latestTaskPlan(mixedPlans).keys()].join(",") !== "25,26,27,28,29,30") {
|
|
1836
|
+
throw new Error(`Claude latest task plan self-test failed: ${JSON.stringify([...mixedPlans])}`);
|
|
1837
|
+
}
|
|
1838
|
+
prepareTaskPlanForCreate(restoredTasks, "7. 추가 작업");
|
|
1839
|
+
if (restoredTasks.size !== 6) throw new Error("Claude appended task unexpectedly reset the plan");
|
|
1840
|
+
prepareTaskPlanForCreate(restoredTasks, "1. 새 작업");
|
|
1841
|
+
if (restoredTasks.size !== 0) throw new Error("Claude new task plan did not reset the previous plan");
|
|
1842
|
+
const usage = tokenBreakdown({
|
|
1703
1843
|
input_tokens: 2,
|
|
1704
1844
|
cache_read_input_tokens: 68_000,
|
|
1705
1845
|
cache_creation_input_tokens: 500,
|
|
@@ -1713,148 +1853,226 @@ async function runSelfTest() {
|
|
|
1713
1853
|
catalogEntry({ value: "sonnet", resolvedModel: "claude-sonnet-5" }, "").contextWindow,
|
|
1714
1854
|
catalogEntry({ value: "haiku", resolvedModel: "x", contextWindow: 300_000 }, "").contextWindow,
|
|
1715
1855
|
];
|
|
1716
|
-
if (windows.join(",") !== "1000000,200000,300000") {
|
|
1717
|
-
throw new Error(`Claude context window self-test failed: ${windows.join(",")}`);
|
|
1718
|
-
}
|
|
1719
|
-
const notification = taskNotifications({
|
|
1720
|
-
origin: { kind: "task-notification" },
|
|
1721
|
-
message: {
|
|
1722
|
-
content: `<task-notification>
|
|
1723
|
-
<task-id>agent-1</task-id>
|
|
1724
|
-
<tool-use-id>toolu_1</tool-use-id>
|
|
1725
|
-
<status>completed</status>
|
|
1726
|
-
<summary>Agent "Explore" finished</summary>
|
|
1727
|
-
<result>done</result>
|
|
1728
|
-
</task-notification>`,
|
|
1729
|
-
},
|
|
1730
|
-
});
|
|
1731
|
-
if (notification.length !== 1
|
|
1732
|
-
|| notification[0].taskId !== "agent-1"
|
|
1733
|
-
|| notification[0].toolUseId !== "toolu_1"
|
|
1734
|
-
|| notification[0].status !== "completed"
|
|
1735
|
-
|| notification[0].summary !== 'Agent "Explore" finished') {
|
|
1736
|
-
throw new Error(`Claude task notification self-test failed: ${JSON.stringify(notification)}`);
|
|
1856
|
+
if (windows.join(",") !== "1000000,200000,300000") {
|
|
1857
|
+
throw new Error(`Claude context window self-test failed: ${windows.join(",")}`);
|
|
1858
|
+
}
|
|
1859
|
+
const notification = taskNotifications({
|
|
1860
|
+
origin: { kind: "task-notification" },
|
|
1861
|
+
message: {
|
|
1862
|
+
content: `<task-notification>
|
|
1863
|
+
<task-id>agent-1</task-id>
|
|
1864
|
+
<tool-use-id>toolu_1</tool-use-id>
|
|
1865
|
+
<status>completed</status>
|
|
1866
|
+
<summary>Agent "Explore" finished</summary>
|
|
1867
|
+
<result>done</result>
|
|
1868
|
+
</task-notification>`,
|
|
1869
|
+
},
|
|
1870
|
+
});
|
|
1871
|
+
if (notification.length !== 1
|
|
1872
|
+
|| notification[0].taskId !== "agent-1"
|
|
1873
|
+
|| notification[0].toolUseId !== "toolu_1"
|
|
1874
|
+
|| notification[0].status !== "completed"
|
|
1875
|
+
|| notification[0].summary !== 'Agent "Explore" finished') {
|
|
1876
|
+
throw new Error(`Claude task notification self-test failed: ${JSON.stringify(notification)}`);
|
|
1877
|
+
}
|
|
1878
|
+
const lifecycleSession = {
|
|
1879
|
+
id: "self-test-session",
|
|
1880
|
+
turn: { id: "parent-turn", sawStreamText: false },
|
|
1881
|
+
turnSequence: 1,
|
|
1882
|
+
streamBlocks: new Map(),
|
|
1883
|
+
tools: new Map([[
|
|
1884
|
+
"toolu_1",
|
|
1885
|
+
{
|
|
1886
|
+
name: "Agent",
|
|
1887
|
+
input: { subagent_type: "Explore", description: "Inspect files" },
|
|
1888
|
+
item: { id: "toolu_1", type: "collabAgentToolCall", tool: { name: "Agent", arguments: {} } },
|
|
1889
|
+
},
|
|
1890
|
+
]]),
|
|
1891
|
+
subagents: new Map([[
|
|
1892
|
+
"toolu_1",
|
|
1893
|
+
{
|
|
1894
|
+
id: "toolu_1",
|
|
1895
|
+
taskId: "",
|
|
1896
|
+
background: false,
|
|
1897
|
+
name: "Explore",
|
|
1898
|
+
description: "Inspect files",
|
|
1899
|
+
tool: "",
|
|
1900
|
+
startedAt: Date.now(),
|
|
1901
|
+
},
|
|
1902
|
+
]]),
|
|
1903
|
+
knownSubagents: new Map(),
|
|
1904
|
+
lastContextUsage: null,
|
|
1905
|
+
};
|
|
1906
|
+
const captured = [];
|
|
1907
|
+
const stdoutWrite = process.stdout.write;
|
|
1908
|
+
process.stdout.write = (chunk) => {
|
|
1909
|
+
captured.push(String(chunk));
|
|
1910
|
+
return true;
|
|
1911
|
+
};
|
|
1912
|
+
try {
|
|
1913
|
+
processUser(lifecycleSession, {
|
|
1914
|
+
message: { content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "launched" }] },
|
|
1915
|
+
tool_use_result: { isAsync: true, status: "async_launched", agentId: "agent-1" },
|
|
1916
|
+
});
|
|
1917
|
+
finishTurn(lifecycleSession, null, 1);
|
|
1918
|
+
if (!lifecycleSession.subagents.has("toolu_1") || lifecycleSession.turn !== null) {
|
|
1919
|
+
throw new Error("Claude background subagent did not survive its parent turn");
|
|
1920
|
+
}
|
|
1921
|
+
processUser(lifecycleSession, {
|
|
1922
|
+
origin: { kind: "task-notification" },
|
|
1923
|
+
message: { content: notification[0] && `<task-notification>
|
|
1924
|
+
<task-id>agent-1</task-id><tool-use-id>toolu_1</tool-use-id>
|
|
1925
|
+
<status>completed</status><summary>Agent finished</summary>
|
|
1926
|
+
</task-notification>` },
|
|
1927
|
+
});
|
|
1928
|
+
if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
|
|
1929
|
+
throw new Error("Claude task notification did not finish the agent in an automatic turn");
|
|
1930
|
+
}
|
|
1931
|
+
finishTurn(lifecycleSession, null, 1);
|
|
1932
|
+
|
|
1933
|
+
beginTurn(lifecycleSession);
|
|
1934
|
+
lifecycleSession.tools.set("toolu_2", {
|
|
1935
|
+
name: "SendMessage",
|
|
1936
|
+
input: { to: "agent-1", summary: "Continue inspection" },
|
|
1937
|
+
item: { id: "toolu_2", type: "dynamicToolCall", tool: "SendMessage", arguments: {} },
|
|
1938
|
+
});
|
|
1939
|
+
processUser(lifecycleSession, {
|
|
1940
|
+
message: { content: [{ type: "tool_result", tool_use_id: "toolu_2", content: "resumed" }] },
|
|
1941
|
+
tool_use_result: { success: true, resumedAgentId: "agent-1" },
|
|
1942
|
+
});
|
|
1943
|
+
const resumed = lifecycleSession.subagents.get("toolu_2");
|
|
1944
|
+
if (!resumed?.background || resumed.taskId !== "agent-1" || resumed.name !== "Explore") {
|
|
1945
|
+
throw new Error(`Claude resumed subagent self-test failed: ${JSON.stringify(resumed)}`);
|
|
1946
|
+
}
|
|
1947
|
+
finishTurn(lifecycleSession, null, 1);
|
|
1948
|
+
processUser(lifecycleSession, {
|
|
1949
|
+
origin: { kind: "task-notification" },
|
|
1950
|
+
message: { content: `<task-notification>
|
|
1951
|
+
<task-id>agent-1</task-id><tool-use-id>toolu_2</tool-use-id>
|
|
1952
|
+
<status>failed</status><summary>Agent failed</summary>
|
|
1953
|
+
</task-notification>` },
|
|
1954
|
+
});
|
|
1955
|
+
if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
|
|
1956
|
+
throw new Error("Claude failed task notification did not finish the resumed agent");
|
|
1957
|
+
}
|
|
1958
|
+
finishTurn(lifecycleSession, null, 1);
|
|
1959
|
+
} finally {
|
|
1960
|
+
process.stdout.write = stdoutWrite;
|
|
1961
|
+
}
|
|
1962
|
+
const lifecycleEvents = captured
|
|
1963
|
+
.join("")
|
|
1964
|
+
.trim()
|
|
1965
|
+
.split("\n")
|
|
1966
|
+
.filter(Boolean)
|
|
1967
|
+
.map((line) => JSON.parse(line));
|
|
1968
|
+
const lifecycleMethods = lifecycleEvents.map((event) => event.method);
|
|
1969
|
+
if (!lifecycleMethods.includes("turn/subagents/updated")
|
|
1970
|
+
|| lifecycleMethods.filter((method) => method === "turn/started").length < 3
|
|
1971
|
+
|| !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
|
|
1972
|
+
&& event.params?.line?.kind === "error")) {
|
|
1973
|
+
throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
|
|
1737
1974
|
}
|
|
1738
|
-
const
|
|
1739
|
-
id: "self-test
|
|
1740
|
-
|
|
1975
|
+
const openingSession = {
|
|
1976
|
+
id: "opening-self-test",
|
|
1977
|
+
model: "claude:default",
|
|
1978
|
+
models: [],
|
|
1979
|
+
turn: null,
|
|
1741
1980
|
turnSequence: 1,
|
|
1981
|
+
itemSequence: 1,
|
|
1742
1982
|
streamBlocks: new Map(),
|
|
1743
|
-
tools: new Map(
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
name: "Agent",
|
|
1747
|
-
input: { subagent_type: "Explore", description: "Inspect files" },
|
|
1748
|
-
item: { id: "toolu_1", type: "collabAgentToolCall", tool: { name: "Agent", arguments: {} } },
|
|
1749
|
-
},
|
|
1750
|
-
]]),
|
|
1751
|
-
subagents: new Map([[
|
|
1752
|
-
"toolu_1",
|
|
1753
|
-
{
|
|
1754
|
-
id: "toolu_1",
|
|
1755
|
-
taskId: "",
|
|
1756
|
-
background: false,
|
|
1757
|
-
name: "Explore",
|
|
1758
|
-
description: "Inspect files",
|
|
1759
|
-
tool: "",
|
|
1760
|
-
startedAt: Date.now(),
|
|
1761
|
-
},
|
|
1762
|
-
]]),
|
|
1983
|
+
tools: new Map(),
|
|
1984
|
+
tasks: new Map(),
|
|
1985
|
+
subagents: new Map(),
|
|
1763
1986
|
knownSubagents: new Map(),
|
|
1764
1987
|
lastContextUsage: null,
|
|
1988
|
+
lastContextWindow: 0,
|
|
1765
1989
|
};
|
|
1766
|
-
const
|
|
1767
|
-
const stdoutWrite = process.stdout.write;
|
|
1990
|
+
const openingCaptured = [];
|
|
1768
1991
|
process.stdout.write = (chunk) => {
|
|
1769
|
-
|
|
1992
|
+
openingCaptured.push(String(chunk));
|
|
1770
1993
|
return true;
|
|
1771
1994
|
};
|
|
1772
1995
|
try {
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
if (!lifecycleSession.subagents.has("toolu_1") || lifecycleSession.turn !== null) {
|
|
1779
|
-
throw new Error("Claude background subagent did not survive its parent turn");
|
|
1780
|
-
}
|
|
1781
|
-
processUser(lifecycleSession, {
|
|
1782
|
-
origin: { kind: "task-notification" },
|
|
1783
|
-
message: { content: notification[0] && `<task-notification>
|
|
1784
|
-
<task-id>agent-1</task-id><tool-use-id>toolu_1</tool-use-id>
|
|
1785
|
-
<status>completed</status><summary>Agent finished</summary>
|
|
1786
|
-
</task-notification>` },
|
|
1996
|
+
beginTurn(openingSession, [{ type: "text", text: "provider 메뉴를 수정해" }]);
|
|
1997
|
+
processAssistant(openingSession, {
|
|
1998
|
+
message: {
|
|
1999
|
+
content: [{ type: "tool_use", id: "read-1", name: "Read", input: { file_path: "src/main.rs" } }],
|
|
2000
|
+
},
|
|
1787
2001
|
});
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2002
|
+
} finally {
|
|
2003
|
+
process.stdout.write = stdoutWrite;
|
|
2004
|
+
}
|
|
2005
|
+
const openingEvents = openingCaptured
|
|
2006
|
+
.join("")
|
|
2007
|
+
.trim()
|
|
2008
|
+
.split("\n")
|
|
2009
|
+
.filter(Boolean)
|
|
2010
|
+
.map((line) => JSON.parse(line));
|
|
2011
|
+
const openingMessageIndex = openingEvents.findIndex((event) =>
|
|
2012
|
+
event.method === "item/completed"
|
|
2013
|
+
&& event.params?.item?.type === "agentMessage"
|
|
2014
|
+
&& event.params.item.text === "요청 내용을 확인하고 필요한 작업을 진행하겠습니다.");
|
|
2015
|
+
const openingToolIndex = openingEvents.findIndex((event) =>
|
|
2016
|
+
event.method === "item/started" && event.params?.item?.type === "dynamicToolCall");
|
|
2017
|
+
if (openingMessageIndex < 0 || openingToolIndex < 0 || openingMessageIndex > openingToolIndex) {
|
|
2018
|
+
throw new Error(`Claude opening notice order self-test failed: ${JSON.stringify(openingEvents)}`);
|
|
2019
|
+
}
|
|
2020
|
+
const languageCaptured = [];
|
|
2021
|
+
process.stdout.write = (chunk) => {
|
|
2022
|
+
languageCaptured.push(String(chunk));
|
|
2023
|
+
return true;
|
|
2024
|
+
};
|
|
2025
|
+
try {
|
|
2026
|
+
openingSession.streamBlocks.clear();
|
|
2027
|
+
await processStreamEvent(openingSession, {
|
|
2028
|
+
event: { type: "content_block_start", index: 0, content_block: { type: "text" } },
|
|
1798
2029
|
});
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
tool_use_result: { success: true, resumedAgentId: "agent-1" },
|
|
2030
|
+
await processStreamEvent(openingSession, {
|
|
2031
|
+
event: { type: "content_block_delta", index: 0, delta: { text: "Now the tile view logic." } },
|
|
1802
2032
|
});
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
throw new Error(`Claude resumed subagent self-test failed: ${JSON.stringify(resumed)}`);
|
|
1806
|
-
}
|
|
1807
|
-
finishTurn(lifecycleSession, null, 1);
|
|
1808
|
-
processUser(lifecycleSession, {
|
|
1809
|
-
origin: { kind: "task-notification" },
|
|
1810
|
-
message: { content: `<task-notification>
|
|
1811
|
-
<task-id>agent-1</task-id><tool-use-id>toolu_2</tool-use-id>
|
|
1812
|
-
<status>failed</status><summary>Agent failed</summary>
|
|
1813
|
-
</task-notification>` },
|
|
2033
|
+
await processStreamEvent(openingSession, {
|
|
2034
|
+
event: { type: "content_block_stop", index: 0 },
|
|
1814
2035
|
});
|
|
1815
|
-
if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
|
|
1816
|
-
throw new Error("Claude failed task notification did not finish the resumed agent");
|
|
1817
|
-
}
|
|
1818
|
-
finishTurn(lifecycleSession, null, 1);
|
|
1819
2036
|
} finally {
|
|
1820
2037
|
process.stdout.write = stdoutWrite;
|
|
1821
2038
|
}
|
|
1822
|
-
const
|
|
2039
|
+
const languageEvents = languageCaptured
|
|
1823
2040
|
.join("")
|
|
1824
2041
|
.trim()
|
|
1825
2042
|
.split("\n")
|
|
1826
2043
|
.filter(Boolean)
|
|
1827
2044
|
.map((line) => JSON.parse(line));
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2045
|
+
const visibleLanguage = languageEvents
|
|
2046
|
+
.filter((event) => event.method === "item/agentMessage/delta")
|
|
2047
|
+
.map((event) => event.params?.delta || "")
|
|
2048
|
+
.join("");
|
|
2049
|
+
if (visibleLanguage !== "다음 부분을 이어서 확인하겠습니다."
|
|
2050
|
+
|| languageEvents.some((event) => JSON.stringify(event).includes("Now the tile view logic."))) {
|
|
2051
|
+
throw new Error(`Claude Korean progress normalization self-test failed: ${JSON.stringify(languageEvents)}`);
|
|
1834
2052
|
}
|
|
1835
2053
|
const smoothText = "Claude가 👨👩👧👦 한 문장을 한꺼번에 보내도 부드럽게 표시합니다.";
|
|
1836
|
-
const emitted = [];
|
|
1837
|
-
const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
|
|
1838
|
-
smooth.push(smoothText);
|
|
1839
|
-
await smooth.finish();
|
|
1840
|
-
if (emitted.length < 2 || emitted.join("") !== smoothText) {
|
|
1841
|
-
throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
|
|
1842
|
-
}
|
|
1843
|
-
const flushed = [];
|
|
1844
|
-
const interrupted = new SmoothTextStream((chunk) => flushed.push(chunk), 1000);
|
|
1845
|
-
interrupted.push(smoothText);
|
|
1846
|
-
interrupted.flush();
|
|
1847
|
-
await interrupted.finish();
|
|
1848
|
-
if (flushed.join("") !== smoothText) {
|
|
1849
|
-
throw new Error(`Claude smooth stream flush self-test failed: ${JSON.stringify(flushed)}`);
|
|
1850
|
-
}
|
|
1851
|
-
process.stdout.write("Claude bridge self-test passed\n");
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
if (process.argv.includes("--self-test")) {
|
|
1855
|
-
await runSelfTest();
|
|
1856
|
-
process.exit(0);
|
|
1857
|
-
}
|
|
2054
|
+
const emitted = [];
|
|
2055
|
+
const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
|
|
2056
|
+
smooth.push(smoothText);
|
|
2057
|
+
await smooth.finish();
|
|
2058
|
+
if (emitted.length < 2 || emitted.join("") !== smoothText) {
|
|
2059
|
+
throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
|
|
2060
|
+
}
|
|
2061
|
+
const flushed = [];
|
|
2062
|
+
const interrupted = new SmoothTextStream((chunk) => flushed.push(chunk), 1000);
|
|
2063
|
+
interrupted.push(smoothText);
|
|
2064
|
+
interrupted.flush();
|
|
2065
|
+
await interrupted.finish();
|
|
2066
|
+
if (flushed.join("") !== smoothText) {
|
|
2067
|
+
throw new Error(`Claude smooth stream flush self-test failed: ${JSON.stringify(flushed)}`);
|
|
2068
|
+
}
|
|
2069
|
+
process.stdout.write("Claude bridge self-test passed\n");
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
if (process.argv.includes("--self-test")) {
|
|
2073
|
+
await runSelfTest();
|
|
2074
|
+
process.exit(0);
|
|
2075
|
+
}
|
|
1858
2076
|
|
|
1859
2077
|
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
1860
2078
|
lines.on("line", async (line) => {
|