devez-vibe 0.1.48 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dvz.exe +0 -0
- package/bridge/claude-agent-sdk-bridge.mjs +175 -8
- package/package.json +1 -1
package/bin/dvz.exe
CHANGED
|
Binary file
|
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
|
|
15
15
|
const VERSION = process.env.DEVEZ_VIBE_VERSION || "dev";
|
|
16
16
|
const sessions = new Map();
|
|
17
|
+
// The id this bridge proposes is not always the id the CLI persists the
|
|
18
|
+
// transcript under, so a session can be renamed mid-flight. Old id → live id,
|
|
19
|
+
// which keeps ids the host already handed out (or wrote to disk) resolvable.
|
|
20
|
+
const sessionAliases = new Map();
|
|
17
21
|
const pendingHostRequests = new Map();
|
|
18
22
|
const modelCatalogs = new Map();
|
|
19
23
|
let nextHostRequest = 1;
|
|
@@ -215,6 +219,44 @@ function rawSession(id) {
|
|
|
215
219
|
return id.startsWith("claude:") ? id.slice("claude:".length) : id;
|
|
216
220
|
}
|
|
217
221
|
|
|
222
|
+
/** Follows the rename chain from an id the host still remembers to the live one. */
|
|
223
|
+
function liveSessionId(id) {
|
|
224
|
+
let current = rawSession(String(id ?? ""));
|
|
225
|
+
const seen = new Set();
|
|
226
|
+
while (sessionAliases.has(current) && !seen.has(current)) {
|
|
227
|
+
seen.add(current);
|
|
228
|
+
current = sessionAliases.get(current);
|
|
229
|
+
}
|
|
230
|
+
return current;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function lookupSession(id) {
|
|
234
|
+
return sessions.get(liveSessionId(id));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Binds the session to the id the CLI actually persists under. `options.sessionId`
|
|
239
|
+
* is a request, not a guarantee: a session that gets rotated (a pre-warmed process
|
|
240
|
+
* the first turn does not reuse, a resume the CLI declines) writes its transcript
|
|
241
|
+
* under a different uuid, and everything downstream — `session/resume`,
|
|
242
|
+
* `session/history`, DevezCode's `-r` on the next launch — keys off the persisted
|
|
243
|
+
* id. Adopting it here and telling the host is what keeps a Claude-backed session
|
|
244
|
+
* resumable at all.
|
|
245
|
+
*/
|
|
246
|
+
function adoptSessionId(session, incoming) {
|
|
247
|
+
const real = rawSession(String(incoming ?? ""));
|
|
248
|
+
if (!real || real === session.id) return;
|
|
249
|
+
const previous = session.id;
|
|
250
|
+
sessions.delete(previous);
|
|
251
|
+
sessionAliases.set(previous, real);
|
|
252
|
+
session.id = real;
|
|
253
|
+
sessions.set(real, session);
|
|
254
|
+
notify("thread/rebound", {
|
|
255
|
+
threadId: visibleSession(previous),
|
|
256
|
+
newThreadId: visibleSession(real),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
218
260
|
function makeOptions(params, sessionId, resume) {
|
|
219
261
|
const options = {
|
|
220
262
|
cwd: params.cwd || process.cwd(),
|
|
@@ -341,6 +383,7 @@ async function createSession(params, resumeId) {
|
|
|
341
383
|
streamBlocks: new Map(),
|
|
342
384
|
tools: new Map(),
|
|
343
385
|
tasks: new Map(),
|
|
386
|
+
subagents: new Map(),
|
|
344
387
|
lastContextUsage: null,
|
|
345
388
|
lastContextWindow: 0,
|
|
346
389
|
};
|
|
@@ -482,7 +525,11 @@ function processStreamEvent(session, message) {
|
|
|
482
525
|
}
|
|
483
526
|
|
|
484
527
|
function processAssistant(session, message) {
|
|
485
|
-
if (!session.turn
|
|
528
|
+
if (!session.turn) return;
|
|
529
|
+
if (message.parent_tool_use_id) {
|
|
530
|
+
recordSubagentMessage(session, message);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
486
533
|
session.lastContextUsage = tokenBreakdown(message.message?.usage);
|
|
487
534
|
const capabilities = modelCapabilities(
|
|
488
535
|
session.models,
|
|
@@ -523,6 +570,7 @@ function processToolUse(session, block) {
|
|
|
523
570
|
const item = toolItem(session, block.id, name, input);
|
|
524
571
|
session.tools.set(block.id, { name, input, item });
|
|
525
572
|
emitItem(session, "started", item);
|
|
573
|
+
if (SUBAGENT_TOOLS.includes(name)) startSubagent(session, block);
|
|
526
574
|
}
|
|
527
575
|
|
|
528
576
|
function toolItem(session, id, name, input) {
|
|
@@ -643,10 +691,123 @@ function numberedTaskSubject(subject, index) {
|
|
|
643
691
|
return /^\d+\.\s/.test(text) ? text : `${index + 1}. ${text}`;
|
|
644
692
|
}
|
|
645
693
|
|
|
694
|
+
// 서브에이전트는 자기 메시지를 부모 Task 툴콜의 `parent_tool_use_id`와 함께 흘려보낸다.
|
|
695
|
+
// 그 ID로 묶어 두면 지금 어떤 에이전트가 무슨 도구를 돌리는지 그대로 복원할 수 있다.
|
|
696
|
+
const SUBAGENT_TOOLS = ["Agent", "Task"];
|
|
697
|
+
|
|
698
|
+
function startSubagent(session, block) {
|
|
699
|
+
const input = block.input || {};
|
|
700
|
+
session.subagents.set(block.id, {
|
|
701
|
+
id: block.id,
|
|
702
|
+
name: firstLine(input.subagent_type || input.agentType || "agent", 40),
|
|
703
|
+
description: firstLine(input.description || input.prompt || "", 120),
|
|
704
|
+
tool: "",
|
|
705
|
+
startedAt: Date.now(),
|
|
706
|
+
});
|
|
707
|
+
emitSubagents(session);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 서브에이전트가 실제로 무엇을 했는지는 자식 메시지에만 남는다. 열람용 기록은 여기서
|
|
711
|
+
// 한 줄씩 흘려보내고, 목록 행에 쓸 현재 도구만 따로 갱신한다.
|
|
712
|
+
function recordSubagentMessage(session, message) {
|
|
713
|
+
const running = session.subagents.get(message.parent_tool_use_id);
|
|
714
|
+
if (!running) return;
|
|
715
|
+
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
716
|
+
let toolChanged = false;
|
|
717
|
+
for (const block of content) {
|
|
718
|
+
if (block.type === "text") {
|
|
719
|
+
const text = String(block.text || "").trim();
|
|
720
|
+
if (text) emitSubagentLine(session, running.id, { kind: "text", text });
|
|
721
|
+
} else if (block.type === "tool_use") {
|
|
722
|
+
running.tool = subagentToolLabel(block);
|
|
723
|
+
toolChanged = true;
|
|
724
|
+
emitSubagentLine(session, running.id, {
|
|
725
|
+
kind: "tool",
|
|
726
|
+
text: running.tool,
|
|
727
|
+
toolUseId: block.id,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (toolChanged) emitSubagents(session);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function recordSubagentResult(session, message) {
|
|
735
|
+
const running = session.subagents.get(message.parent_tool_use_id);
|
|
736
|
+
if (!running) return;
|
|
737
|
+
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
738
|
+
for (const block of content) {
|
|
739
|
+
if (block.type !== "tool_result") continue;
|
|
740
|
+
emitSubagentLine(session, running.id, {
|
|
741
|
+
kind: block.is_error ? "error" : "result",
|
|
742
|
+
text: firstLine(toolOutput(block.content, message.tool_use_result), 200),
|
|
743
|
+
toolUseId: block.tool_use_id,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function emitSubagentLine(session, parentToolUseId, line) {
|
|
749
|
+
notify("turn/subagent/line", {
|
|
750
|
+
threadId: session.id,
|
|
751
|
+
turnId: session.turn?.id,
|
|
752
|
+
parentToolUseId,
|
|
753
|
+
line,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function subagentToolLabel(block) {
|
|
758
|
+
const name = block.name || "Tool";
|
|
759
|
+
const input = block.input || {};
|
|
760
|
+
const detail = input.command
|
|
761
|
+
?? input.pattern
|
|
762
|
+
?? input.file_path
|
|
763
|
+
?? input.description
|
|
764
|
+
?? input.query
|
|
765
|
+
?? input.url
|
|
766
|
+
?? "";
|
|
767
|
+
const text = firstLine(detail, 60);
|
|
768
|
+
return text ? `${name}(${text})` : name;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function finishSubagent(session, toolUseId) {
|
|
772
|
+
if (!session.subagents.delete(toolUseId)) return;
|
|
773
|
+
emitSubagents(session);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function clearSubagents(session) {
|
|
777
|
+
if (!session.subagents.size) return;
|
|
778
|
+
session.subagents.clear();
|
|
779
|
+
emitSubagents(session);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function firstLine(value, limit) {
|
|
783
|
+
return String(value ?? "").split("\n")[0].trim().slice(0, limit);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function emitSubagents(session) {
|
|
787
|
+
notify("turn/subagents/updated", {
|
|
788
|
+
threadId: session.id,
|
|
789
|
+
turnId: session.turn?.id,
|
|
790
|
+
subagents: [...session.subagents.values()].map((agent) => ({
|
|
791
|
+
id: agent.id,
|
|
792
|
+
name: agent.name,
|
|
793
|
+
description: agent.description,
|
|
794
|
+
tool: agent.tool,
|
|
795
|
+
elapsedMs: Date.now() - agent.startedAt,
|
|
796
|
+
})),
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
646
800
|
function processUser(session, message) {
|
|
801
|
+
// 자식 tool_result의 tool_use_id는 부모 세션의 것과 다른 공간이므로, 부모 흐름에
|
|
802
|
+
// 섞이기 전에 서브에이전트 기록으로 보낸다.
|
|
803
|
+
if (message.parent_tool_use_id) {
|
|
804
|
+
recordSubagentResult(session, message);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
647
807
|
const content = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
648
808
|
for (const block of content) {
|
|
649
809
|
if (block.type !== "tool_result") continue;
|
|
810
|
+
finishSubagent(session, block.tool_use_id);
|
|
650
811
|
const pending = session.tools.get(block.tool_use_id);
|
|
651
812
|
if (!pending) continue;
|
|
652
813
|
pending.toolUseId = block.tool_use_id;
|
|
@@ -735,6 +896,7 @@ async function runPendingPrompt(session) {
|
|
|
735
896
|
|
|
736
897
|
function finishTurn(session, error, durationMs) {
|
|
737
898
|
if (!session.turn) return;
|
|
899
|
+
clearSubagents(session);
|
|
738
900
|
const turn = { id: session.turn.id, status: error ? "failed" : "completed" };
|
|
739
901
|
if (error) turn.error = { message: error instanceof Error ? error.message : error.message || String(error) };
|
|
740
902
|
if (durationMs != null) turn.durationMs = durationMs;
|
|
@@ -745,6 +907,7 @@ function finishTurn(session, error, durationMs) {
|
|
|
745
907
|
|
|
746
908
|
async function consume(session) {
|
|
747
909
|
for await (const message of session.query) {
|
|
910
|
+
adoptSessionId(session, message.session_id);
|
|
748
911
|
if (message.type === "stream_event") {
|
|
749
912
|
if (message.event?.type === "content_block_delta" && (message.event?.delta?.text || message.event?.delta?.thinking)) {
|
|
750
913
|
if (session.turn) session.turn.sawStreamText = true;
|
|
@@ -807,7 +970,7 @@ async function inputContent(input, handoffContext) {
|
|
|
807
970
|
}
|
|
808
971
|
|
|
809
972
|
async function startPrompt(params) {
|
|
810
|
-
const id =
|
|
973
|
+
const id = liveSessionId(params.sessionId);
|
|
811
974
|
const session = sessions.get(id);
|
|
812
975
|
if (!session) throw new Error(`Claude 세션을 찾을 수 없습니다: ${id}`);
|
|
813
976
|
// Claude runs one turn at a time, so extra input waits its turn instead of
|
|
@@ -966,7 +1129,7 @@ async function dispatch(method, params = {}) {
|
|
|
966
1129
|
};
|
|
967
1130
|
}
|
|
968
1131
|
if (method === "session/resume") {
|
|
969
|
-
const id =
|
|
1132
|
+
const id = liveSessionId(params.sessionId);
|
|
970
1133
|
const existing = sessions.get(id);
|
|
971
1134
|
if (existing) {
|
|
972
1135
|
const messages = await getSessionMessages(id, { dir: existing.cwd, includeSystemMessages: true });
|
|
@@ -1022,13 +1185,13 @@ async function dispatch(method, params = {}) {
|
|
|
1022
1185
|
};
|
|
1023
1186
|
}
|
|
1024
1187
|
if (method === "session/history") {
|
|
1025
|
-
const id =
|
|
1188
|
+
const id = liveSessionId(params.sessionId);
|
|
1026
1189
|
const messages = await getSessionMessages(id, { dir: params.cwd, includeSystemMessages: true });
|
|
1027
1190
|
return { data: historyTurns(messages), nextCursor: null };
|
|
1028
1191
|
}
|
|
1029
1192
|
if (method === "session/prompt") return startPrompt(params);
|
|
1030
1193
|
if (method === "session/interrupt") {
|
|
1031
|
-
const session =
|
|
1194
|
+
const session = lookupSession(params.sessionId);
|
|
1032
1195
|
// Stopping the run drops what was waiting behind it too, so nothing the user
|
|
1033
1196
|
// just cancelled starts on its own afterwards.
|
|
1034
1197
|
if (session) session.pendingPrompts.length = 0;
|
|
@@ -1048,7 +1211,7 @@ async function dispatch(method, params = {}) {
|
|
|
1048
1211
|
return startPrompt({ ...params, input: [{ type: "text", text: "/compact" }] });
|
|
1049
1212
|
}
|
|
1050
1213
|
if (method === "session/fork") {
|
|
1051
|
-
const source =
|
|
1214
|
+
const source = liveSessionId(params.sessionId);
|
|
1052
1215
|
const forked = await forkSession(source, { dir: params.cwd });
|
|
1053
1216
|
const id = forked.sessionId || forked;
|
|
1054
1217
|
const { session, account, usage } = await createSession(params, id);
|
|
@@ -1063,17 +1226,20 @@ async function dispatch(method, params = {}) {
|
|
|
1063
1226
|
};
|
|
1064
1227
|
}
|
|
1065
1228
|
if (method === "session/close") {
|
|
1066
|
-
const session =
|
|
1229
|
+
const session = lookupSession(params.sessionId);
|
|
1067
1230
|
if (session) {
|
|
1068
1231
|
session.queue.close();
|
|
1069
1232
|
session.query.close();
|
|
1070
1233
|
sessions.delete(session.id);
|
|
1234
|
+
for (const [from, to] of sessionAliases) {
|
|
1235
|
+
if (to === session.id) sessionAliases.delete(from);
|
|
1236
|
+
}
|
|
1071
1237
|
if (params.delete) await deleteSession(session.id, { dir: session.cwd });
|
|
1072
1238
|
}
|
|
1073
1239
|
return {};
|
|
1074
1240
|
}
|
|
1075
1241
|
if (method === "account/usage") {
|
|
1076
|
-
const session =
|
|
1242
|
+
const session = lookupSession(params.sessionId) || [...sessions.values()][0];
|
|
1077
1243
|
if (!session) return { account: null, usage: null };
|
|
1078
1244
|
return { account: await safeAccount(session.query), usage: await safeUsage(session.query) };
|
|
1079
1245
|
}
|
|
@@ -1083,6 +1249,7 @@ async function dispatch(method, params = {}) {
|
|
|
1083
1249
|
session.query.close();
|
|
1084
1250
|
}
|
|
1085
1251
|
sessions.clear();
|
|
1252
|
+
sessionAliases.clear();
|
|
1086
1253
|
return {};
|
|
1087
1254
|
}
|
|
1088
1255
|
throw new Error(`지원하지 않는 Claude 브리지 메서드: ${method}`);
|