devez-vibe 1.2.4 β 1.2.6
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/bin/dvz.exe +0 -0
- package/bridge/claude-agent-sdk-bridge.mjs +603 -160
- package/package.json +1 -1
package/bin/dvz.exe
CHANGED
|
Binary file
|
|
@@ -435,23 +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
|
-
|
|
438
|
+
tools: new Map(),
|
|
439
|
+
tasks: new Map(),
|
|
440
|
+
subagents: new Map(),
|
|
441
|
+
knownSubagents: new Map(),
|
|
442
|
+
lastContextUsage: null,
|
|
442
443
|
lastContextWindow: 0,
|
|
443
444
|
};
|
|
444
445
|
const agentQuery = await startAgentQuery(queue, makeOptions(params, id, resumeId));
|
|
445
446
|
session.query = agentQuery;
|
|
446
447
|
sessions.set(id, session);
|
|
447
|
-
const consumer = consume(session).catch((error) => {
|
|
448
|
+
const consumer = consume(session).catch((error) => {
|
|
448
449
|
notify("error", {
|
|
449
450
|
threadId: id,
|
|
450
451
|
provider: "Claude",
|
|
451
452
|
error: { message: error instanceof Error ? error.message : String(error) },
|
|
452
|
-
willRetry: false,
|
|
453
|
-
});
|
|
454
|
-
|
|
453
|
+
willRetry: false,
|
|
454
|
+
});
|
|
455
|
+
clearSubagents(session);
|
|
456
|
+
if (session.turn) finishTurn(session, error);
|
|
455
457
|
});
|
|
456
458
|
session.consumer = consumer;
|
|
457
459
|
let initialization;
|
|
@@ -495,9 +497,87 @@ function emitItem(session, phase, item) {
|
|
|
495
497
|
notify(`item/${phase}`, { threadId: session.id, turnId: session.turn?.id, item });
|
|
496
498
|
}
|
|
497
499
|
|
|
498
|
-
function emitDelta(session, method, itemId, delta) {
|
|
499
|
-
notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
|
|
500
|
-
}
|
|
500
|
+
function emitDelta(session, method, itemId, delta) {
|
|
501
|
+
notify(method, { threadId: session.id, turnId: session.turn?.id, itemId, delta, provider: "Claude" });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Windows rounds larger timer delays up to the next scheduler slice. Ten
|
|
505
|
+
// milliseconds lands near one terminal frame instead of visibly stepping at ~30ms.
|
|
506
|
+
const SMOOTH_TEXT_INTERVAL_MS = 10;
|
|
507
|
+
const SMOOTH_TEXT_TARGET_FRAMES = 8;
|
|
508
|
+
const SMOOTH_TEXT_MAX_GRAPHEMES = 24;
|
|
509
|
+
const graphemeSegmenter = typeof Intl.Segmenter === "function"
|
|
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);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Claude can deliver a whole phrase in one SDK event. Drain roughly one visual
|
|
520
|
+
// frame's share at a time, catching up quickly when a large backlog arrives.
|
|
521
|
+
function takeSmoothTextChunk(text) {
|
|
522
|
+
const graphemes = splitGraphemes(text);
|
|
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
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
class SmoothTextStream {
|
|
534
|
+
constructor(emit, intervalMs = SMOOTH_TEXT_INTERVAL_MS) {
|
|
535
|
+
this.emit = emit;
|
|
536
|
+
this.intervalMs = intervalMs;
|
|
537
|
+
this.pending = "";
|
|
538
|
+
this.timer = null;
|
|
539
|
+
this.waiters = [];
|
|
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();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function flushSmoothStreams(session) {
|
|
579
|
+
for (const block of session.streamBlocks.values()) block.smooth?.flush();
|
|
580
|
+
}
|
|
501
581
|
|
|
502
582
|
function tokenBreakdown(usage) {
|
|
503
583
|
if (!usage) return null;
|
|
@@ -545,41 +625,44 @@ function historyTokenUsage(messages, models, model) {
|
|
|
545
625
|
};
|
|
546
626
|
}
|
|
547
627
|
|
|
548
|
-
function processStreamEvent(session, message) {
|
|
549
|
-
if (!session.turn || message.parent_tool_use_id) return;
|
|
550
|
-
const event = message.event || {};
|
|
551
|
-
if (event.type === "message_start")
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
+
}
|
|
635
|
+
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);
|
|
556
639
|
const item = block.type === "text"
|
|
557
640
|
? { id, type: "agentMessage", text: "", provider: "Claude" }
|
|
558
641
|
: { id, type: "reasoning", summary: [] };
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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, { id, type: block.type, text: "", smooth });
|
|
646
|
+
emitItem(session, "started", item);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
563
649
|
if (event.type === "content_block_delta") {
|
|
564
650
|
const current = session.streamBlocks.get(event.index);
|
|
565
651
|
if (!current) return;
|
|
566
|
-
const delta = event.delta?.text || event.delta?.thinking || "";
|
|
567
|
-
if (!delta) return;
|
|
568
|
-
current.text += delta;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
);
|
|
575
|
-
return;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const item = current.type === "text"
|
|
581
|
-
? { id: current.id, type: "agentMessage", text: current.text, provider: "Claude" }
|
|
582
|
-
: { id: current.id, type: "reasoning", summary: [current.text] };
|
|
652
|
+
const delta = event.delta?.text || event.delta?.thinking || "";
|
|
653
|
+
if (!delta) return;
|
|
654
|
+
current.text += delta;
|
|
655
|
+
if (current.smooth) current.smooth.push(delta);
|
|
656
|
+
else emitDelta(session, "item/reasoning/summaryTextDelta", current.id, delta);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (event.type === "content_block_stop") {
|
|
660
|
+
const current = session.streamBlocks.get(event.index);
|
|
661
|
+
if (!current) return;
|
|
662
|
+
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] };
|
|
583
666
|
emitItem(session, "completed", item);
|
|
584
667
|
session.streamBlocks.delete(event.index);
|
|
585
668
|
}
|
|
@@ -668,34 +751,52 @@ function fileChanges(name, input) {
|
|
|
668
751
|
return [{ path, kind: { type: name === "Write" ? "add" : "update" }, diff: `@@ -0,0 +1 @@\n${additions}` }];
|
|
669
752
|
}
|
|
670
753
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
+
if (input.status) task.status = input.status;
|
|
770
|
+
task.turnId = turnId;
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// TaskListμλ μμ κ³νκΉμ§ ν¨κ» λ€μ΄μ¬ μ μλ€. λ§μ§λ§μΌλ‘ λ²νΈκ° 1λΆν°
|
|
775
|
+
// μμν λ¬Άμλ§ νμ¬ κ³νμΌλ‘ μΌλ, λ²νΈκ° μλ λͺ©λ‘μ μμ€ μμ΄ κ·Έλλ‘ λλ€.
|
|
776
|
+
function latestTaskPlan(tasks) {
|
|
777
|
+
const entries = [...tasks.entries()];
|
|
778
|
+
let start = 0;
|
|
779
|
+
for (let index = 0; index < entries.length; index++) {
|
|
780
|
+
if (numberedTaskIndex(entries[index][1].subject) === 1) start = index;
|
|
781
|
+
}
|
|
782
|
+
return new Map(entries.slice(start));
|
|
783
|
+
}
|
|
678
784
|
|
|
679
785
|
function updatePlanFromToolUse(session, name, toolUseId, input) {
|
|
680
|
-
const turnId = session.turn?.id;
|
|
681
|
-
if (name === "TaskCreate") {
|
|
682
|
-
|
|
683
|
-
session.tasks.set(`pending:${toolUseId}`, {
|
|
684
|
-
id: `pending:${toolUseId}`,
|
|
685
|
-
subject: input.subject || input.description || "μμ
",
|
|
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 || "μμ
",
|
|
686
792
|
status: "pending",
|
|
687
|
-
turnId,
|
|
688
|
-
});
|
|
689
|
-
} else if (name === "TaskUpdate") {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
task.turnId = turnId;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
emitPlan(session);
|
|
698
|
-
}
|
|
793
|
+
turnId,
|
|
794
|
+
});
|
|
795
|
+
} else if (name === "TaskUpdate") {
|
|
796
|
+
applyTaskUpdate(session.tasks, input, turnId);
|
|
797
|
+
}
|
|
798
|
+
emitPlan(session);
|
|
799
|
+
}
|
|
699
800
|
|
|
700
801
|
function updatePlanFromToolResult(session, pending, message) {
|
|
701
802
|
const value = message.tool_use_result;
|
|
@@ -723,14 +824,10 @@ function updatePlanFromToolResult(session, pending, message) {
|
|
|
723
824
|
turnId: previous.get(id)?.turnId ?? turnId,
|
|
724
825
|
});
|
|
725
826
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
if (session.tasks.size === 0) session.tasks = listed;
|
|
731
|
-
emitPlan(session);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
827
|
+
session.tasks = latestTaskPlan(session.tasks);
|
|
828
|
+
emitPlan(session);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
734
831
|
|
|
735
832
|
function taskCreatedResult(structured, content) {
|
|
736
833
|
const created = structured?.task || structured;
|
|
@@ -771,22 +868,78 @@ function numberedTaskSubject(subject, index) {
|
|
|
771
868
|
// κ·Έ IDλ‘ λ¬Άμ΄ λλ©΄ μ§κΈ μ΄λ€ μμ΄μ νΈκ° λ¬΄μ¨ λꡬλ₯Ό λ리λμ§ κ·Έλλ‘ λ³΅μν μ μλ€.
|
|
772
869
|
const SUBAGENT_TOOLS = ["Agent", "Task"];
|
|
773
870
|
|
|
774
|
-
function startSubagent(session, block) {
|
|
775
|
-
const input = block.input || {};
|
|
776
|
-
session.subagents.set(block.id, {
|
|
777
|
-
id: block.id,
|
|
778
|
-
|
|
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),
|
|
779
878
|
description: firstLine(input.description || input.prompt || "", 120),
|
|
780
879
|
tool: "",
|
|
781
880
|
startedAt: Date.now(),
|
|
782
881
|
});
|
|
783
|
-
emitSubagents(session);
|
|
784
|
-
}
|
|
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
|
+
}
|
|
785
938
|
|
|
786
939
|
// μλΈμμ΄μ νΈκ° μ€μ λ‘ λ¬΄μμ νλμ§λ μμ λ©μμ§μλ§ λ¨λλ€. μ΄λμ© κΈ°λ‘μ μ¬κΈ°μ
|
|
787
940
|
// ν μ€μ© νλ €λ³΄λ΄κ³ , λͺ©λ‘ νμ μΈ νμ¬ λκ΅¬λ§ λ°λ‘ κ°±μ νλ€.
|
|
788
|
-
function recordSubagentMessage(session, message) {
|
|
789
|
-
const running = session
|
|
941
|
+
function recordSubagentMessage(session, message) {
|
|
942
|
+
const running = findSubagent(session, message.parent_tool_use_id);
|
|
790
943
|
if (!running) return;
|
|
791
944
|
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
792
945
|
let toolChanged = false;
|
|
@@ -807,8 +960,8 @@ function recordSubagentMessage(session, message) {
|
|
|
807
960
|
if (toolChanged) emitSubagents(session);
|
|
808
961
|
}
|
|
809
962
|
|
|
810
|
-
function recordSubagentResult(session, message) {
|
|
811
|
-
const running = session
|
|
963
|
+
function recordSubagentResult(session, message) {
|
|
964
|
+
const running = findSubagent(session, message.parent_tool_use_id);
|
|
812
965
|
if (!running) return;
|
|
813
966
|
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
814
967
|
for (const block of content) {
|
|
@@ -844,13 +997,32 @@ function subagentToolLabel(block) {
|
|
|
844
997
|
return text ? `${name}(${text})` : name;
|
|
845
998
|
}
|
|
846
999
|
|
|
847
|
-
function finishSubagent(session, toolUseId) {
|
|
848
|
-
if (!session.subagents.delete(toolUseId)) return;
|
|
849
|
-
emitSubagents(session);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function
|
|
853
|
-
if (!
|
|
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;
|
|
854
1026
|
session.subagents.clear();
|
|
855
1027
|
emitSubagents(session);
|
|
856
1028
|
}
|
|
@@ -870,22 +1042,87 @@ function emitSubagents(session) {
|
|
|
870
1042
|
tool: agent.tool,
|
|
871
1043
|
elapsedMs: Date.now() - agent.startedAt,
|
|
872
1044
|
})),
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function
|
|
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) {
|
|
877
1101
|
// μμ tool_resultμ tool_use_idλ λΆλͺ¨ μΈμ
μ κ²κ³Ό λ€λ₯Έ 곡κ°μ΄λ―λ‘, λΆλͺ¨ νλ¦μ
|
|
878
1102
|
// μμ΄κΈ° μ μ μλΈμμ΄μ νΈ κΈ°λ‘μΌλ‘ 보λΈλ€.
|
|
879
1103
|
if (message.parent_tool_use_id) {
|
|
880
|
-
recordSubagentResult(session, message);
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
if (
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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;
|
|
889
1126
|
pending.toolUseId = block.tool_use_id;
|
|
890
1127
|
if (pending.suppressed) {
|
|
891
1128
|
updatePlanFromToolResult(session, pending, message);
|
|
@@ -972,9 +1209,10 @@ async function runPendingPrompt(session) {
|
|
|
972
1209
|
}
|
|
973
1210
|
}
|
|
974
1211
|
|
|
975
|
-
function finishTurn(session, error, durationMs) {
|
|
976
|
-
if (!session.turn) return;
|
|
977
|
-
|
|
1212
|
+
function finishTurn(session, error, durationMs) {
|
|
1213
|
+
if (!session.turn) return;
|
|
1214
|
+
flushSmoothStreams(session);
|
|
1215
|
+
clearForegroundSubagents(session);
|
|
978
1216
|
const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
|
|
979
1217
|
if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
|
|
980
1218
|
if (durationMs != null) turn.durationMs = durationMs;
|
|
@@ -990,7 +1228,7 @@ async function consume(session) {
|
|
|
990
1228
|
if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
|
|
991
1229
|
if (session.turn) session.turn.sawStreamText = true;
|
|
992
1230
|
}
|
|
993
|
-
processStreamEvent(session, message);
|
|
1231
|
+
await processStreamEvent(session, message);
|
|
994
1232
|
} else if (message.type === "assistant") processAssistant(session, message);
|
|
995
1233
|
else if (message.type === "user") processUser(session, message);
|
|
996
1234
|
else if (message.type === "result") await processResult(session, message);
|
|
@@ -1060,7 +1298,7 @@ async function startPrompt(params) {
|
|
|
1060
1298
|
return runPrompt(session, params);
|
|
1061
1299
|
}
|
|
1062
1300
|
|
|
1063
|
-
async function runPrompt(session, params) {
|
|
1301
|
+
async function runPrompt(session, params) {
|
|
1064
1302
|
const id = session.id;
|
|
1065
1303
|
if (params.model) {
|
|
1066
1304
|
const model = stripClaudeModel(params.model);
|
|
@@ -1073,10 +1311,7 @@ async function runPrompt(session, params) {
|
|
|
1073
1311
|
}
|
|
1074
1312
|
session.effort = effort;
|
|
1075
1313
|
await applyPermissionMode(session, params.permissionMode);
|
|
1076
|
-
const turnId =
|
|
1077
|
-
session.turn = { id: turnId, sawStreamText: false };
|
|
1078
|
-
session.lastContextUsage = null;
|
|
1079
|
-
notify("turn/started", { threadId: id, turn: { id: turnId } });
|
|
1314
|
+
const turnId = beginTurn(session);
|
|
1080
1315
|
session.queue.push({
|
|
1081
1316
|
type: "user",
|
|
1082
1317
|
message: { role: "user", content: await inputContent(params.input, params.handoffContext) },
|
|
@@ -1084,8 +1319,18 @@ async function runPrompt(session, params) {
|
|
|
1084
1319
|
session_id: id,
|
|
1085
1320
|
origin: { kind: "human" },
|
|
1086
1321
|
});
|
|
1087
|
-
return { turn: { id: turnId } };
|
|
1088
|
-
}
|
|
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) {
|
|
1328
|
+
const turnId = `claude-turn-${session.turnSequence++}-${randomUUID()}`;
|
|
1329
|
+
session.turn = { id: turnId, sawStreamText: false };
|
|
1330
|
+
session.lastContextUsage = null;
|
|
1331
|
+
notify("turn/started", { threadId: session.id, turn: { id: turnId } });
|
|
1332
|
+
return turnId;
|
|
1333
|
+
}
|
|
1089
1334
|
|
|
1090
1335
|
function contentBlocks(message) {
|
|
1091
1336
|
const content = message?.content;
|
|
@@ -1110,7 +1355,7 @@ function isInternalHistoryText(message, text) {
|
|
|
1110
1355
|
].includes(tag);
|
|
1111
1356
|
}
|
|
1112
1357
|
|
|
1113
|
-
function
|
|
1358
|
+
function historyState(messages) {
|
|
1114
1359
|
const turns = [];
|
|
1115
1360
|
let turn = null;
|
|
1116
1361
|
const tools = new Map();
|
|
@@ -1146,15 +1391,14 @@ function historyTurns(messages) {
|
|
|
1146
1391
|
if (block.type === "text") turn.items.push({ id: `${message.uuid}-text`, type: "agentMessage", text: block.text || "", provider: "Claude" });
|
|
1147
1392
|
else if (block.type === "thinking") turn.items.push({ id: `${message.uuid}-thinking`, type: "reasoning", summary: [block.thinking || ""] });
|
|
1148
1393
|
else if (block.type === "tool_use") {
|
|
1149
|
-
const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
|
|
1150
|
-
tools.set(block.id, pending);
|
|
1151
|
-
if (block.name === "TaskCreate") {
|
|
1152
|
-
|
|
1153
|
-
tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "μμ
", status: "pending", turnId: turn.id });
|
|
1154
|
-
} else if (block.name === "TaskUpdate") {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
} else if (!["TaskList", "AskUserQuestion"].includes(block.name)) turn.items.push(pending.item);
|
|
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);
|
|
1158
1402
|
}
|
|
1159
1403
|
}
|
|
1160
1404
|
} else if (message.type === "user") {
|
|
@@ -1171,12 +1415,12 @@ function historyTurns(messages) {
|
|
|
1171
1415
|
tasks.set(temporary.id, temporary);
|
|
1172
1416
|
}
|
|
1173
1417
|
} else if (pending.name === "TaskList" && Array.isArray(message.tool_use_result?.tasks)) {
|
|
1174
|
-
const known = new Map(tasks);
|
|
1175
|
-
tasks.clear();
|
|
1176
|
-
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 });
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
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);
|
|
1180
1424
|
} else if (pending.item) {
|
|
1181
1425
|
const output = toolOutput(block.content, message.tool_use_result);
|
|
1182
1426
|
Object.assign(pending.item, pending.item.type === "commandExecution"
|
|
@@ -1192,10 +1436,17 @@ function historyTurns(messages) {
|
|
|
1192
1436
|
const text = [...tasks.values()].map((task, index) => `${task.status === "completed" ? "β" : task.status === "in_progress" ? "βΈ" : "β‘"} ${numberedTaskSubject(task.subject, index)}`).join("\n");
|
|
1193
1437
|
turn.items.push({ id: "claude-plan-latest", type: "plan", text });
|
|
1194
1438
|
}
|
|
1195
|
-
return
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
+
}
|
|
1199
1450
|
|
|
1200
1451
|
async function dispatch(method, params = {}) {
|
|
1201
1452
|
if (method === "model/list") return loadModelCatalog(params);
|
|
@@ -1220,10 +1471,11 @@ async function dispatch(method, params = {}) {
|
|
|
1220
1471
|
}
|
|
1221
1472
|
if (method === "session/resume") {
|
|
1222
1473
|
const id = liveSessionId(params.sessionId);
|
|
1223
|
-
const existing = sessions.get(id);
|
|
1224
|
-
if (existing) {
|
|
1225
|
-
const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
|
|
1226
|
-
|
|
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 {
|
|
1227
1479
|
id,
|
|
1228
1480
|
thread: { id, turns: [] },
|
|
1229
1481
|
initialTurnsPage: { data: [], nextCursor: null },
|
|
@@ -1247,10 +1499,11 @@ async function dispatch(method, params = {}) {
|
|
|
1247
1499
|
model: params.model || lastModel || params.fallbackModel,
|
|
1248
1500
|
effort: params.effort || params.fallbackEffort,
|
|
1249
1501
|
}, id);
|
|
1250
|
-
const tokenUsage = historyTokenUsage(messages, session.models, session.model);
|
|
1251
|
-
// Seed the live session so the next turn keeps reporting a full context.
|
|
1252
|
-
session.lastContextUsage = tokenUsage?.last || null;
|
|
1253
|
-
session.lastContextWindow = tokenUsage?.modelContextWindow || 0;
|
|
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;
|
|
1254
1507
|
return {
|
|
1255
1508
|
id,
|
|
1256
1509
|
thread: { id, turns: [] },
|
|
@@ -1352,7 +1605,7 @@ async function dispatch(method, params = {}) {
|
|
|
1352
1605
|
throw new Error(`μ§μνμ§ μλ Claude λΈλ¦¬μ§ λ©μλ: ${method}`);
|
|
1353
1606
|
}
|
|
1354
1607
|
|
|
1355
|
-
function runSelfTest() {
|
|
1608
|
+
async function runSelfTest() {
|
|
1356
1609
|
const user = (uuid, text) => ({
|
|
1357
1610
|
type: "user",
|
|
1358
1611
|
uuid,
|
|
@@ -1383,12 +1636,70 @@ function runSelfTest() {
|
|
|
1383
1636
|
["say hello", "claude:claude-sonnet-5"],
|
|
1384
1637
|
["hay zzz", "claude:claude-haiku-4-5-20251001"],
|
|
1385
1638
|
];
|
|
1386
|
-
if (turns.length !== expected.length
|
|
1387
|
-
|| prompts.some((prompt, index) => prompt?.content?.[0]?.text !== expected[index][0]
|
|
1388
|
-
|| prompt.model !== expected[index][1])) {
|
|
1389
|
-
throw new Error(`Claude history self-test failed: ${JSON.stringify(turns)}`);
|
|
1390
|
-
}
|
|
1391
|
-
const
|
|
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 mixedPlans = new Map([
|
|
1691
|
+
["old-1", { subject: "1. μ΄μ μμ
", status: "completed" }],
|
|
1692
|
+
["old-2", { subject: "2. μ΄μ κ²μ¦", status: "completed" }],
|
|
1693
|
+
...[...restoredTasks],
|
|
1694
|
+
]);
|
|
1695
|
+
if ([...latestTaskPlan(mixedPlans).keys()].join(",") !== "25,26,27,28,29,30") {
|
|
1696
|
+
throw new Error(`Claude latest task plan self-test failed: ${JSON.stringify([...mixedPlans])}`);
|
|
1697
|
+
}
|
|
1698
|
+
prepareTaskPlanForCreate(restoredTasks, "7. μΆκ° μμ
");
|
|
1699
|
+
if (restoredTasks.size !== 6) throw new Error("Claude appended task unexpectedly reset the plan");
|
|
1700
|
+
prepareTaskPlanForCreate(restoredTasks, "1. μ μμ
");
|
|
1701
|
+
if (restoredTasks.size !== 0) throw new Error("Claude new task plan did not reset the previous plan");
|
|
1702
|
+
const usage = tokenBreakdown({
|
|
1392
1703
|
input_tokens: 2,
|
|
1393
1704
|
cache_read_input_tokens: 68_000,
|
|
1394
1705
|
cache_creation_input_tokens: 500,
|
|
@@ -1402,16 +1713,148 @@ function runSelfTest() {
|
|
|
1402
1713
|
catalogEntry({ value: "sonnet", resolvedModel: "claude-sonnet-5" }, "").contextWindow,
|
|
1403
1714
|
catalogEntry({ value: "haiku", resolvedModel: "x", contextWindow: 300_000 }, "").contextWindow,
|
|
1404
1715
|
];
|
|
1405
|
-
if (windows.join(",") !== "1000000,200000,300000") {
|
|
1406
|
-
throw new Error(`Claude context window self-test failed: ${windows.join(",")}`);
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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)}`);
|
|
1737
|
+
}
|
|
1738
|
+
const lifecycleSession = {
|
|
1739
|
+
id: "self-test-session",
|
|
1740
|
+
turn: { id: "parent-turn", sawStreamText: false },
|
|
1741
|
+
turnSequence: 1,
|
|
1742
|
+
streamBlocks: new Map(),
|
|
1743
|
+
tools: new Map([[
|
|
1744
|
+
"toolu_1",
|
|
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
|
+
]]),
|
|
1763
|
+
knownSubagents: new Map(),
|
|
1764
|
+
lastContextUsage: null,
|
|
1765
|
+
};
|
|
1766
|
+
const captured = [];
|
|
1767
|
+
const stdoutWrite = process.stdout.write;
|
|
1768
|
+
process.stdout.write = (chunk) => {
|
|
1769
|
+
captured.push(String(chunk));
|
|
1770
|
+
return true;
|
|
1771
|
+
};
|
|
1772
|
+
try {
|
|
1773
|
+
processUser(lifecycleSession, {
|
|
1774
|
+
message: { content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "launched" }] },
|
|
1775
|
+
tool_use_result: { isAsync: true, status: "async_launched", agentId: "agent-1" },
|
|
1776
|
+
});
|
|
1777
|
+
finishTurn(lifecycleSession, null, 1);
|
|
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>` },
|
|
1787
|
+
});
|
|
1788
|
+
if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
|
|
1789
|
+
throw new Error("Claude task notification did not finish the agent in an automatic turn");
|
|
1790
|
+
}
|
|
1791
|
+
finishTurn(lifecycleSession, null, 1);
|
|
1792
|
+
|
|
1793
|
+
beginTurn(lifecycleSession);
|
|
1794
|
+
lifecycleSession.tools.set("toolu_2", {
|
|
1795
|
+
name: "SendMessage",
|
|
1796
|
+
input: { to: "agent-1", summary: "Continue inspection" },
|
|
1797
|
+
item: { id: "toolu_2", type: "dynamicToolCall", tool: "SendMessage", arguments: {} },
|
|
1798
|
+
});
|
|
1799
|
+
processUser(lifecycleSession, {
|
|
1800
|
+
message: { content: [{ type: "tool_result", tool_use_id: "toolu_2", content: "resumed" }] },
|
|
1801
|
+
tool_use_result: { success: true, resumedAgentId: "agent-1" },
|
|
1802
|
+
});
|
|
1803
|
+
const resumed = lifecycleSession.subagents.get("toolu_2");
|
|
1804
|
+
if (!resumed?.background || resumed.taskId !== "agent-1" || resumed.name !== "Explore") {
|
|
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>` },
|
|
1814
|
+
});
|
|
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
|
+
} finally {
|
|
1820
|
+
process.stdout.write = stdoutWrite;
|
|
1821
|
+
}
|
|
1822
|
+
const lifecycleEvents = captured
|
|
1823
|
+
.join("")
|
|
1824
|
+
.trim()
|
|
1825
|
+
.split("\n")
|
|
1826
|
+
.filter(Boolean)
|
|
1827
|
+
.map((line) => JSON.parse(line));
|
|
1828
|
+
const lifecycleMethods = lifecycleEvents.map((event) => event.method);
|
|
1829
|
+
if (!lifecycleMethods.includes("turn/subagents/updated")
|
|
1830
|
+
|| lifecycleMethods.filter((method) => method === "turn/started").length < 3
|
|
1831
|
+
|| !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
|
|
1832
|
+
&& event.params?.line?.kind === "error")) {
|
|
1833
|
+
throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
|
|
1834
|
+
}
|
|
1835
|
+
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
|
+
}
|
|
1415
1858
|
|
|
1416
1859
|
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
1417
1860
|
lines.on("line", async (line) => {
|