devez-vibe 1.2.3 β 1.2.5
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 -155
- 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,11 +824,10 @@ function updatePlanFromToolResult(session, pending, message) {
|
|
|
723
824
|
turnId: previous.get(id)?.turnId ?? turnId,
|
|
724
825
|
});
|
|
725
826
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
}
|
|
827
|
+
session.tasks = latestTaskPlan(session.tasks);
|
|
828
|
+
emitPlan(session);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
731
831
|
|
|
732
832
|
function taskCreatedResult(structured, content) {
|
|
733
833
|
const created = structured?.task || structured;
|
|
@@ -745,6 +845,8 @@ function planStatus(status) {
|
|
|
745
845
|
}
|
|
746
846
|
|
|
747
847
|
function emitPlan(session) {
|
|
848
|
+
// λΉ κ³νμ 보λ΄λ©΄ νλ©΄μ κ³ν μΉ΄λκ° μ¬λΌμ§λ€. 보μ¬μ€ μμ
μ΄ μμ λλ λ§μ§λ§ κ³νμ κ·Έλλ‘ λλ€.
|
|
849
|
+
if (session.tasks.size === 0) return;
|
|
748
850
|
notify("turn/plan/updated", {
|
|
749
851
|
threadId: session.id,
|
|
750
852
|
turnId: session.turn?.id,
|
|
@@ -766,22 +868,78 @@ function numberedTaskSubject(subject, index) {
|
|
|
766
868
|
// κ·Έ IDλ‘ λ¬Άμ΄ λλ©΄ μ§κΈ μ΄λ€ μμ΄μ νΈκ° λ¬΄μ¨ λꡬλ₯Ό λ리λμ§ κ·Έλλ‘ λ³΅μν μ μλ€.
|
|
767
869
|
const SUBAGENT_TOOLS = ["Agent", "Task"];
|
|
768
870
|
|
|
769
|
-
function startSubagent(session, block) {
|
|
770
|
-
const input = block.input || {};
|
|
771
|
-
session.subagents.set(block.id, {
|
|
772
|
-
id: block.id,
|
|
773
|
-
|
|
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),
|
|
774
878
|
description: firstLine(input.description || input.prompt || "", 120),
|
|
775
879
|
tool: "",
|
|
776
880
|
startedAt: Date.now(),
|
|
777
881
|
});
|
|
778
|
-
emitSubagents(session);
|
|
779
|
-
}
|
|
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
|
+
}
|
|
780
938
|
|
|
781
939
|
// μλΈμμ΄μ νΈκ° μ€μ λ‘ λ¬΄μμ νλμ§λ μμ λ©μμ§μλ§ λ¨λλ€. μ΄λμ© κΈ°λ‘μ μ¬κΈ°μ
|
|
782
940
|
// ν μ€μ© νλ €λ³΄λ΄κ³ , λͺ©λ‘ νμ μΈ νμ¬ λκ΅¬λ§ λ°λ‘ κ°±μ νλ€.
|
|
783
|
-
function recordSubagentMessage(session, message) {
|
|
784
|
-
const running = session
|
|
941
|
+
function recordSubagentMessage(session, message) {
|
|
942
|
+
const running = findSubagent(session, message.parent_tool_use_id);
|
|
785
943
|
if (!running) return;
|
|
786
944
|
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
787
945
|
let toolChanged = false;
|
|
@@ -802,8 +960,8 @@ function recordSubagentMessage(session, message) {
|
|
|
802
960
|
if (toolChanged) emitSubagents(session);
|
|
803
961
|
}
|
|
804
962
|
|
|
805
|
-
function recordSubagentResult(session, message) {
|
|
806
|
-
const running = session
|
|
963
|
+
function recordSubagentResult(session, message) {
|
|
964
|
+
const running = findSubagent(session, message.parent_tool_use_id);
|
|
807
965
|
if (!running) return;
|
|
808
966
|
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
809
967
|
for (const block of content) {
|
|
@@ -839,13 +997,32 @@ function subagentToolLabel(block) {
|
|
|
839
997
|
return text ? `${name}(${text})` : name;
|
|
840
998
|
}
|
|
841
999
|
|
|
842
|
-
function finishSubagent(session, toolUseId) {
|
|
843
|
-
if (!session.subagents.delete(toolUseId)) return;
|
|
844
|
-
emitSubagents(session);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function
|
|
848
|
-
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;
|
|
849
1026
|
session.subagents.clear();
|
|
850
1027
|
emitSubagents(session);
|
|
851
1028
|
}
|
|
@@ -865,22 +1042,87 @@ function emitSubagents(session) {
|
|
|
865
1042
|
tool: agent.tool,
|
|
866
1043
|
elapsedMs: Date.now() - agent.startedAt,
|
|
867
1044
|
})),
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
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) {
|
|
872
1101
|
// μμ tool_resultμ tool_use_idλ λΆλͺ¨ μΈμ
μ κ²κ³Ό λ€λ₯Έ 곡κ°μ΄λ―λ‘, λΆλͺ¨ νλ¦μ
|
|
873
1102
|
// μμ΄κΈ° μ μ μλΈμμ΄μ νΈ κΈ°λ‘μΌλ‘ 보λΈλ€.
|
|
874
1103
|
if (message.parent_tool_use_id) {
|
|
875
|
-
recordSubagentResult(session, message);
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
if (
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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;
|
|
884
1126
|
pending.toolUseId = block.tool_use_id;
|
|
885
1127
|
if (pending.suppressed) {
|
|
886
1128
|
updatePlanFromToolResult(session, pending, message);
|
|
@@ -967,9 +1209,10 @@ async function runPendingPrompt(session) {
|
|
|
967
1209
|
}
|
|
968
1210
|
}
|
|
969
1211
|
|
|
970
|
-
function finishTurn(session, error, durationMs) {
|
|
971
|
-
if (!session.turn) return;
|
|
972
|
-
|
|
1212
|
+
function finishTurn(session, error, durationMs) {
|
|
1213
|
+
if (!session.turn) return;
|
|
1214
|
+
flushSmoothStreams(session);
|
|
1215
|
+
clearForegroundSubagents(session);
|
|
973
1216
|
const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
|
|
974
1217
|
if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
|
|
975
1218
|
if (durationMs != null) turn.durationMs = durationMs;
|
|
@@ -985,7 +1228,7 @@ async function consume(session) {
|
|
|
985
1228
|
if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
|
|
986
1229
|
if (session.turn) session.turn.sawStreamText = true;
|
|
987
1230
|
}
|
|
988
|
-
processStreamEvent(session, message);
|
|
1231
|
+
await processStreamEvent(session, message);
|
|
989
1232
|
} else if (message.type === "assistant") processAssistant(session, message);
|
|
990
1233
|
else if (message.type === "user") processUser(session, message);
|
|
991
1234
|
else if (message.type === "result") await processResult(session, message);
|
|
@@ -1055,7 +1298,7 @@ async function startPrompt(params) {
|
|
|
1055
1298
|
return runPrompt(session, params);
|
|
1056
1299
|
}
|
|
1057
1300
|
|
|
1058
|
-
async function runPrompt(session, params) {
|
|
1301
|
+
async function runPrompt(session, params) {
|
|
1059
1302
|
const id = session.id;
|
|
1060
1303
|
if (params.model) {
|
|
1061
1304
|
const model = stripClaudeModel(params.model);
|
|
@@ -1068,10 +1311,7 @@ async function runPrompt(session, params) {
|
|
|
1068
1311
|
}
|
|
1069
1312
|
session.effort = effort;
|
|
1070
1313
|
await applyPermissionMode(session, params.permissionMode);
|
|
1071
|
-
const turnId =
|
|
1072
|
-
session.turn = { id: turnId, sawStreamText: false };
|
|
1073
|
-
session.lastContextUsage = null;
|
|
1074
|
-
notify("turn/started", { threadId: id, turn: { id: turnId } });
|
|
1314
|
+
const turnId = beginTurn(session);
|
|
1075
1315
|
session.queue.push({
|
|
1076
1316
|
type: "user",
|
|
1077
1317
|
message: { role: "user", content: await inputContent(params.input, params.handoffContext) },
|
|
@@ -1079,8 +1319,18 @@ async function runPrompt(session, params) {
|
|
|
1079
1319
|
session_id: id,
|
|
1080
1320
|
origin: { kind: "human" },
|
|
1081
1321
|
});
|
|
1082
|
-
return { turn: { id: turnId } };
|
|
1083
|
-
}
|
|
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
|
+
}
|
|
1084
1334
|
|
|
1085
1335
|
function contentBlocks(message) {
|
|
1086
1336
|
const content = message?.content;
|
|
@@ -1105,7 +1355,7 @@ function isInternalHistoryText(message, text) {
|
|
|
1105
1355
|
].includes(tag);
|
|
1106
1356
|
}
|
|
1107
1357
|
|
|
1108
|
-
function
|
|
1358
|
+
function historyState(messages) {
|
|
1109
1359
|
const turns = [];
|
|
1110
1360
|
let turn = null;
|
|
1111
1361
|
const tools = new Map();
|
|
@@ -1141,15 +1391,14 @@ function historyTurns(messages) {
|
|
|
1141
1391
|
if (block.type === "text") turn.items.push({ id: `${message.uuid}-text`, type: "agentMessage", text: block.text || "", provider: "Claude" });
|
|
1142
1392
|
else if (block.type === "thinking") turn.items.push({ id: `${message.uuid}-thinking`, type: "reasoning", summary: [block.thinking || ""] });
|
|
1143
1393
|
else if (block.type === "tool_use") {
|
|
1144
|
-
const pending = { name: block.name, input: block.input || {}, item: toolItem({}, block.id, block.name, block.input || {}) };
|
|
1145
|
-
tools.set(block.id, pending);
|
|
1146
|
-
if (block.name === "TaskCreate") {
|
|
1147
|
-
|
|
1148
|
-
tasks.set(`pending:${block.id}`, { id: `pending:${block.id}`, subject: block.input?.subject || "μμ
", status: "pending", turnId: turn.id });
|
|
1149
|
-
} else if (block.name === "TaskUpdate") {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
} 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);
|
|
1153
1402
|
}
|
|
1154
1403
|
}
|
|
1155
1404
|
} else if (message.type === "user") {
|
|
@@ -1166,10 +1415,12 @@ function historyTurns(messages) {
|
|
|
1166
1415
|
tasks.set(temporary.id, temporary);
|
|
1167
1416
|
}
|
|
1168
1417
|
} else if (pending.name === "TaskList" && Array.isArray(message.tool_use_result?.tasks)) {
|
|
1169
|
-
const known = new Map(tasks);
|
|
1170
|
-
tasks.clear();
|
|
1171
|
-
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 });
|
|
1172
|
-
|
|
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);
|
|
1173
1424
|
} else if (pending.item) {
|
|
1174
1425
|
const output = toolOutput(block.content, message.tool_use_result);
|
|
1175
1426
|
Object.assign(pending.item, pending.item.type === "commandExecution"
|
|
@@ -1185,10 +1436,17 @@ function historyTurns(messages) {
|
|
|
1185
1436
|
const text = [...tasks.values()].map((task, index) => `${task.status === "completed" ? "β" : task.status === "in_progress" ? "βΈ" : "β‘"} ${numberedTaskSubject(task.subject, index)}`).join("\n");
|
|
1186
1437
|
turn.items.push({ id: "claude-plan-latest", type: "plan", text });
|
|
1187
1438
|
}
|
|
1188
|
-
return
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
+
}
|
|
1192
1450
|
|
|
1193
1451
|
async function dispatch(method, params = {}) {
|
|
1194
1452
|
if (method === "model/list") return loadModelCatalog(params);
|
|
@@ -1213,10 +1471,11 @@ async function dispatch(method, params = {}) {
|
|
|
1213
1471
|
}
|
|
1214
1472
|
if (method === "session/resume") {
|
|
1215
1473
|
const id = liveSessionId(params.sessionId);
|
|
1216
|
-
const existing = sessions.get(id);
|
|
1217
|
-
if (existing) {
|
|
1218
|
-
const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
|
|
1219
|
-
|
|
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 {
|
|
1220
1479
|
id,
|
|
1221
1480
|
thread: { id, turns: [] },
|
|
1222
1481
|
initialTurnsPage: { data: [], nextCursor: null },
|
|
@@ -1240,10 +1499,11 @@ async function dispatch(method, params = {}) {
|
|
|
1240
1499
|
model: params.model || lastModel || params.fallbackModel,
|
|
1241
1500
|
effort: params.effort || params.fallbackEffort,
|
|
1242
1501
|
}, id);
|
|
1243
|
-
const tokenUsage = historyTokenUsage(messages, session.models, session.model);
|
|
1244
|
-
// Seed the live session so the next turn keeps reporting a full context.
|
|
1245
|
-
session.lastContextUsage = tokenUsage?.last || null;
|
|
1246
|
-
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;
|
|
1247
1507
|
return {
|
|
1248
1508
|
id,
|
|
1249
1509
|
thread: { id, turns: [] },
|
|
@@ -1345,7 +1605,7 @@ async function dispatch(method, params = {}) {
|
|
|
1345
1605
|
throw new Error(`μ§μνμ§ μλ Claude λΈλ¦¬μ§ λ©μλ: ${method}`);
|
|
1346
1606
|
}
|
|
1347
1607
|
|
|
1348
|
-
function runSelfTest() {
|
|
1608
|
+
async function runSelfTest() {
|
|
1349
1609
|
const user = (uuid, text) => ({
|
|
1350
1610
|
type: "user",
|
|
1351
1611
|
uuid,
|
|
@@ -1376,12 +1636,68 @@ function runSelfTest() {
|
|
|
1376
1636
|
["say hello", "claude:claude-sonnet-5"],
|
|
1377
1637
|
["hay zzz", "claude:claude-haiku-4-5-20251001"],
|
|
1378
1638
|
];
|
|
1379
|
-
if (turns.length !== expected.length
|
|
1380
|
-
|| prompts.some((prompt, index) => prompt?.content?.[0]?.text !== expected[index][0]
|
|
1381
|
-
|| prompt.model !== expected[index][1])) {
|
|
1382
|
-
throw new Error(`Claude history self-test failed: ${JSON.stringify(turns)}`);
|
|
1383
|
-
}
|
|
1384
|
-
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
|
+
for (const id of ["27", "28"]) {
|
|
1670
|
+
taskMessages.push(taskUse(`update-${id}`, `update-${id}`, "TaskUpdate", { taskId: id, status: "in_progress" }));
|
|
1671
|
+
}
|
|
1672
|
+
const restoredTasks = historyState(taskMessages).tasks;
|
|
1673
|
+
if ([...restoredTasks.keys()].join(",") !== "25,26,27,28,29,30"
|
|
1674
|
+
|| restoredTasks.get("25")?.status !== "pending"
|
|
1675
|
+
|| restoredTasks.get("27")?.status !== "in_progress"
|
|
1676
|
+
|| restoredTasks.get("28")?.status !== "in_progress") {
|
|
1677
|
+
throw new Error(`Claude task resume self-test failed: ${JSON.stringify([...restoredTasks])}`);
|
|
1678
|
+
}
|
|
1679
|
+
applyTaskUpdate(restoredTasks, { taskId: "27", status: "completed" }, "resumed-turn");
|
|
1680
|
+
applyTaskUpdate(restoredTasks, { taskId: "28", status: "completed" }, "resumed-turn");
|
|
1681
|
+
if (restoredTasks.size !== 6
|
|
1682
|
+
|| restoredTasks.get("25")?.status !== "pending"
|
|
1683
|
+
|| restoredTasks.get("26")?.status !== "pending"
|
|
1684
|
+
|| restoredTasks.get("27")?.status !== "completed"
|
|
1685
|
+
|| restoredTasks.get("28")?.status !== "completed") {
|
|
1686
|
+
throw new Error(`Claude middle task update self-test failed: ${JSON.stringify([...restoredTasks])}`);
|
|
1687
|
+
}
|
|
1688
|
+
const mixedPlans = new Map([
|
|
1689
|
+
["old-1", { subject: "1. μ΄μ μμ
", status: "completed" }],
|
|
1690
|
+
["old-2", { subject: "2. μ΄μ κ²μ¦", status: "completed" }],
|
|
1691
|
+
...[...restoredTasks],
|
|
1692
|
+
]);
|
|
1693
|
+
if ([...latestTaskPlan(mixedPlans).keys()].join(",") !== "25,26,27,28,29,30") {
|
|
1694
|
+
throw new Error(`Claude latest task plan self-test failed: ${JSON.stringify([...mixedPlans])}`);
|
|
1695
|
+
}
|
|
1696
|
+
prepareTaskPlanForCreate(restoredTasks, "7. μΆκ° μμ
");
|
|
1697
|
+
if (restoredTasks.size !== 6) throw new Error("Claude appended task unexpectedly reset the plan");
|
|
1698
|
+
prepareTaskPlanForCreate(restoredTasks, "1. μ μμ
");
|
|
1699
|
+
if (restoredTasks.size !== 0) throw new Error("Claude new task plan did not reset the previous plan");
|
|
1700
|
+
const usage = tokenBreakdown({
|
|
1385
1701
|
input_tokens: 2,
|
|
1386
1702
|
cache_read_input_tokens: 68_000,
|
|
1387
1703
|
cache_creation_input_tokens: 500,
|
|
@@ -1395,16 +1711,148 @@ function runSelfTest() {
|
|
|
1395
1711
|
catalogEntry({ value: "sonnet", resolvedModel: "claude-sonnet-5" }, "").contextWindow,
|
|
1396
1712
|
catalogEntry({ value: "haiku", resolvedModel: "x", contextWindow: 300_000 }, "").contextWindow,
|
|
1397
1713
|
];
|
|
1398
|
-
if (windows.join(",") !== "1000000,200000,300000") {
|
|
1399
|
-
throw new Error(`Claude context window self-test failed: ${windows.join(",")}`);
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1714
|
+
if (windows.join(",") !== "1000000,200000,300000") {
|
|
1715
|
+
throw new Error(`Claude context window self-test failed: ${windows.join(",")}`);
|
|
1716
|
+
}
|
|
1717
|
+
const notification = taskNotifications({
|
|
1718
|
+
origin: { kind: "task-notification" },
|
|
1719
|
+
message: {
|
|
1720
|
+
content: `<task-notification>
|
|
1721
|
+
<task-id>agent-1</task-id>
|
|
1722
|
+
<tool-use-id>toolu_1</tool-use-id>
|
|
1723
|
+
<status>completed</status>
|
|
1724
|
+
<summary>Agent "Explore" finished</summary>
|
|
1725
|
+
<result>done</result>
|
|
1726
|
+
</task-notification>`,
|
|
1727
|
+
},
|
|
1728
|
+
});
|
|
1729
|
+
if (notification.length !== 1
|
|
1730
|
+
|| notification[0].taskId !== "agent-1"
|
|
1731
|
+
|| notification[0].toolUseId !== "toolu_1"
|
|
1732
|
+
|| notification[0].status !== "completed"
|
|
1733
|
+
|| notification[0].summary !== 'Agent "Explore" finished') {
|
|
1734
|
+
throw new Error(`Claude task notification self-test failed: ${JSON.stringify(notification)}`);
|
|
1735
|
+
}
|
|
1736
|
+
const lifecycleSession = {
|
|
1737
|
+
id: "self-test-session",
|
|
1738
|
+
turn: { id: "parent-turn", sawStreamText: false },
|
|
1739
|
+
turnSequence: 1,
|
|
1740
|
+
streamBlocks: new Map(),
|
|
1741
|
+
tools: new Map([[
|
|
1742
|
+
"toolu_1",
|
|
1743
|
+
{
|
|
1744
|
+
name: "Agent",
|
|
1745
|
+
input: { subagent_type: "Explore", description: "Inspect files" },
|
|
1746
|
+
item: { id: "toolu_1", type: "collabAgentToolCall", tool: { name: "Agent", arguments: {} } },
|
|
1747
|
+
},
|
|
1748
|
+
]]),
|
|
1749
|
+
subagents: new Map([[
|
|
1750
|
+
"toolu_1",
|
|
1751
|
+
{
|
|
1752
|
+
id: "toolu_1",
|
|
1753
|
+
taskId: "",
|
|
1754
|
+
background: false,
|
|
1755
|
+
name: "Explore",
|
|
1756
|
+
description: "Inspect files",
|
|
1757
|
+
tool: "",
|
|
1758
|
+
startedAt: Date.now(),
|
|
1759
|
+
},
|
|
1760
|
+
]]),
|
|
1761
|
+
knownSubagents: new Map(),
|
|
1762
|
+
lastContextUsage: null,
|
|
1763
|
+
};
|
|
1764
|
+
const captured = [];
|
|
1765
|
+
const stdoutWrite = process.stdout.write;
|
|
1766
|
+
process.stdout.write = (chunk) => {
|
|
1767
|
+
captured.push(String(chunk));
|
|
1768
|
+
return true;
|
|
1769
|
+
};
|
|
1770
|
+
try {
|
|
1771
|
+
processUser(lifecycleSession, {
|
|
1772
|
+
message: { content: [{ type: "tool_result", tool_use_id: "toolu_1", content: "launched" }] },
|
|
1773
|
+
tool_use_result: { isAsync: true, status: "async_launched", agentId: "agent-1" },
|
|
1774
|
+
});
|
|
1775
|
+
finishTurn(lifecycleSession, null, 1);
|
|
1776
|
+
if (!lifecycleSession.subagents.has("toolu_1") || lifecycleSession.turn !== null) {
|
|
1777
|
+
throw new Error("Claude background subagent did not survive its parent turn");
|
|
1778
|
+
}
|
|
1779
|
+
processUser(lifecycleSession, {
|
|
1780
|
+
origin: { kind: "task-notification" },
|
|
1781
|
+
message: { content: notification[0] && `<task-notification>
|
|
1782
|
+
<task-id>agent-1</task-id><tool-use-id>toolu_1</tool-use-id>
|
|
1783
|
+
<status>completed</status><summary>Agent finished</summary>
|
|
1784
|
+
</task-notification>` },
|
|
1785
|
+
});
|
|
1786
|
+
if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
|
|
1787
|
+
throw new Error("Claude task notification did not finish the agent in an automatic turn");
|
|
1788
|
+
}
|
|
1789
|
+
finishTurn(lifecycleSession, null, 1);
|
|
1790
|
+
|
|
1791
|
+
beginTurn(lifecycleSession);
|
|
1792
|
+
lifecycleSession.tools.set("toolu_2", {
|
|
1793
|
+
name: "SendMessage",
|
|
1794
|
+
input: { to: "agent-1", summary: "Continue inspection" },
|
|
1795
|
+
item: { id: "toolu_2", type: "dynamicToolCall", tool: "SendMessage", arguments: {} },
|
|
1796
|
+
});
|
|
1797
|
+
processUser(lifecycleSession, {
|
|
1798
|
+
message: { content: [{ type: "tool_result", tool_use_id: "toolu_2", content: "resumed" }] },
|
|
1799
|
+
tool_use_result: { success: true, resumedAgentId: "agent-1" },
|
|
1800
|
+
});
|
|
1801
|
+
const resumed = lifecycleSession.subagents.get("toolu_2");
|
|
1802
|
+
if (!resumed?.background || resumed.taskId !== "agent-1" || resumed.name !== "Explore") {
|
|
1803
|
+
throw new Error(`Claude resumed subagent self-test failed: ${JSON.stringify(resumed)}`);
|
|
1804
|
+
}
|
|
1805
|
+
finishTurn(lifecycleSession, null, 1);
|
|
1806
|
+
processUser(lifecycleSession, {
|
|
1807
|
+
origin: { kind: "task-notification" },
|
|
1808
|
+
message: { content: `<task-notification>
|
|
1809
|
+
<task-id>agent-1</task-id><tool-use-id>toolu_2</tool-use-id>
|
|
1810
|
+
<status>failed</status><summary>Agent failed</summary>
|
|
1811
|
+
</task-notification>` },
|
|
1812
|
+
});
|
|
1813
|
+
if (lifecycleSession.subagents.size !== 0 || lifecycleSession.turn === null) {
|
|
1814
|
+
throw new Error("Claude failed task notification did not finish the resumed agent");
|
|
1815
|
+
}
|
|
1816
|
+
finishTurn(lifecycleSession, null, 1);
|
|
1817
|
+
} finally {
|
|
1818
|
+
process.stdout.write = stdoutWrite;
|
|
1819
|
+
}
|
|
1820
|
+
const lifecycleEvents = captured
|
|
1821
|
+
.join("")
|
|
1822
|
+
.trim()
|
|
1823
|
+
.split("\n")
|
|
1824
|
+
.filter(Boolean)
|
|
1825
|
+
.map((line) => JSON.parse(line));
|
|
1826
|
+
const lifecycleMethods = lifecycleEvents.map((event) => event.method);
|
|
1827
|
+
if (!lifecycleMethods.includes("turn/subagents/updated")
|
|
1828
|
+
|| lifecycleMethods.filter((method) => method === "turn/started").length < 3
|
|
1829
|
+
|| !lifecycleEvents.some((event) => event.method === "turn/subagent/line"
|
|
1830
|
+
&& event.params?.line?.kind === "error")) {
|
|
1831
|
+
throw new Error(`Claude subagent lifecycle events self-test failed: ${lifecycleMethods}`);
|
|
1832
|
+
}
|
|
1833
|
+
const smoothText = "Claudeκ° π¨βπ©βπ§βπ¦ ν λ¬Έμ₯μ νκΊΌλ²μ 보λ΄λ λΆλλ½κ² νμν©λλ€.";
|
|
1834
|
+
const emitted = [];
|
|
1835
|
+
const smooth = new SmoothTextStream((chunk) => emitted.push(chunk), 0);
|
|
1836
|
+
smooth.push(smoothText);
|
|
1837
|
+
await smooth.finish();
|
|
1838
|
+
if (emitted.length < 2 || emitted.join("") !== smoothText) {
|
|
1839
|
+
throw new Error(`Claude smooth stream self-test failed: ${JSON.stringify(emitted)}`);
|
|
1840
|
+
}
|
|
1841
|
+
const flushed = [];
|
|
1842
|
+
const interrupted = new SmoothTextStream((chunk) => flushed.push(chunk), 1000);
|
|
1843
|
+
interrupted.push(smoothText);
|
|
1844
|
+
interrupted.flush();
|
|
1845
|
+
await interrupted.finish();
|
|
1846
|
+
if (flushed.join("") !== smoothText) {
|
|
1847
|
+
throw new Error(`Claude smooth stream flush self-test failed: ${JSON.stringify(flushed)}`);
|
|
1848
|
+
}
|
|
1849
|
+
process.stdout.write("Claude bridge self-test passed\n");
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
if (process.argv.includes("--self-test")) {
|
|
1853
|
+
await runSelfTest();
|
|
1854
|
+
process.exit(0);
|
|
1855
|
+
}
|
|
1408
1856
|
|
|
1409
1857
|
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
1410
1858
|
lines.on("line", async (line) => {
|