loadtoagent 1.0.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/README.ko.md +175 -0
- package/README.md +175 -0
- package/README.zh-CN.md +175 -0
- package/bin/loadtoagent.js +171 -0
- package/docs/ARCHITECTURE.md +30 -0
- package/docs/PROVIDER-CONTRACTS.md +58 -0
- package/docs/assets/loadtoagent-dashboard.png +0 -0
- package/docs/assets/loadtoagent-demo.gif +0 -0
- package/main.js +493 -0
- package/package.json +133 -0
- package/preload.js +75 -0
- package/renderer/app-agent-actions.js +285 -0
- package/renderer/app-bootstrap.js +95 -0
- package/renderer/app-dashboard.js +361 -0
- package/renderer/app-drawer-content.js +203 -0
- package/renderer/app-drawer-data.js +41 -0
- package/renderer/app-drawer.js +183 -0
- package/renderer/app-events-dialogs.js +169 -0
- package/renderer/app-events-filters.js +74 -0
- package/renderer/app-events-navigation.js +96 -0
- package/renderer/app-events-sessions.js +195 -0
- package/renderer/app-events.js +16 -0
- package/renderer/app-graph-layout.js +71 -0
- package/renderer/app-graph-model.js +107 -0
- package/renderer/app-graph-orchestration.js +76 -0
- package/renderer/app-graph-view.js +544 -0
- package/renderer/app-run-modal.js +221 -0
- package/renderer/app-session-render.js +243 -0
- package/renderer/app-tmux-render.js +247 -0
- package/renderer/app.js +608 -0
- package/renderer/fonts/geist-ext.woff2 +0 -0
- package/renderer/fonts/geist.woff2 +0 -0
- package/renderer/i18n-messages.js +355 -0
- package/renderer/i18n.js +122 -0
- package/renderer/index.html +386 -0
- package/renderer/shared.js +26 -0
- package/renderer/styles-agent-map.css +401 -0
- package/renderer/styles-cards.css +600 -0
- package/renderer/styles-collaboration.css +534 -0
- package/renderer/styles-components.css +1293 -0
- package/renderer/styles-onboarding.css +344 -0
- package/renderer/styles-overlays.css +284 -0
- package/renderer/styles-product.css +686 -0
- package/renderer/styles-responsive-product.css +689 -0
- package/renderer/styles-responsive-runtime.css +718 -0
- package/renderer/styles-responsive-shell.css +533 -0
- package/renderer/styles-responsive-workflows.css +778 -0
- package/renderer/styles-run-composer.css +695 -0
- package/renderer/styles-settings.css +606 -0
- package/renderer/styles-terminal.css +1241 -0
- package/renderer/styles-tmux.css +1162 -0
- package/renderer/styles-workflow-map.css +513 -0
- package/renderer/styles-workflows.css +1217 -0
- package/renderer/styles.css +486 -0
- package/renderer/terminal-agent.js +152 -0
- package/renderer/terminal-events.js +226 -0
- package/renderer/terminal-workbench.js +464 -0
- package/renderer/terminal.js +497 -0
- package/src/agentMonitor/claudeParser.js +197 -0
- package/src/agentMonitor/codexCollaboration.js +95 -0
- package/src/agentMonitor/codexParser.js +447 -0
- package/src/agentMonitor/genericParser.js +244 -0
- package/src/agentMonitor/hierarchy.js +248 -0
- package/src/agentMonitor/sessionFiles.js +86 -0
- package/src/agentMonitor.js +591 -0
- package/src/agentRunner.js +465 -0
- package/src/bridgeServer.js +246 -0
- package/src/contracts.js +71 -0
- package/src/diagnostics.js +23 -0
- package/src/ipc/registerAgentIpc.js +12 -0
- package/src/ipc/registerAppIpc.js +20 -0
- package/src/ipc/registerTerminalIpc.js +50 -0
- package/src/ipc/registerTmuxIpc.js +30 -0
- package/src/ipc/registerWorkspaceIpc.js +14 -0
- package/src/monitorWorker.js +273 -0
- package/src/processMonitor.js +394 -0
- package/src/providerRegistry.js +120 -0
- package/src/terminalManager.js +438 -0
- package/src/tmuxController.js +183 -0
- package/src/tmuxMonitor.js +432 -0
- package/src/updateManager.js +339 -0
- package/src/workspaceStore.js +77 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const TOOL_START_PATTERN = /tool_use|tool-call|tool_start/;
|
|
6
|
+
const TOOL_END_PATTERN = /tool_result|tool-result|tool_end/;
|
|
7
|
+
|
|
8
|
+
function createGenericParser(dependencies) {
|
|
9
|
+
const {
|
|
10
|
+
ACTIVE_THRESHOLD_MS,
|
|
11
|
+
STALE_TURN_THRESHOLD_MS,
|
|
12
|
+
addLifecycle,
|
|
13
|
+
addMessage,
|
|
14
|
+
baseSession,
|
|
15
|
+
compactText,
|
|
16
|
+
contextInfo,
|
|
17
|
+
finalizeUsage,
|
|
18
|
+
modelContextWindow,
|
|
19
|
+
readJson,
|
|
20
|
+
readJsonLines,
|
|
21
|
+
settleLifecycle,
|
|
22
|
+
sumUsage,
|
|
23
|
+
timestamp,
|
|
24
|
+
trimSession,
|
|
25
|
+
} = dependencies;
|
|
26
|
+
|
|
27
|
+
function normalizeUsage(raw = {}) {
|
|
28
|
+
const usage = raw.usageMetadata || raw.usage_metadata || raw.usage
|
|
29
|
+
|| raw.stats || raw.tokens || raw;
|
|
30
|
+
return finalizeUsage({
|
|
31
|
+
input: usage.input_tokens || usage.inputTokenCount || usage.prompt_tokens || usage.promptTokenCount,
|
|
32
|
+
cachedInput: usage.cached_input_tokens || usage.cachedContentTokenCount || usage.cached_tokens,
|
|
33
|
+
cacheWrite: usage.cache_creation_input_tokens,
|
|
34
|
+
output: usage.output_tokens || usage.candidatesTokenCount
|
|
35
|
+
|| usage.completion_tokens || usage.response_tokens,
|
|
36
|
+
reasoning: usage.reasoning_tokens || usage.thoughtsTokenCount,
|
|
37
|
+
total: usage.total_tokens || usage.totalTokenCount || usage.total_token_count,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function flattenRows(value, out = [], depth = 0, seen = new Set()) {
|
|
42
|
+
if (depth > 6 || value == null) return out;
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
for (const item of value) flattenRows(item, out, depth + 1, seen);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value !== 'object' || seen.has(value)) return out;
|
|
48
|
+
seen.add(value);
|
|
49
|
+
const role = value.role || value.author || value.sender;
|
|
50
|
+
const deltaText = typeof value.delta === 'string' ? value.delta : '';
|
|
51
|
+
const text = compactText(value.text || value.content || value.message
|
|
52
|
+
|| value.response || value.prompt || deltaText);
|
|
53
|
+
if (role && text) out.push({ value, order: out.length });
|
|
54
|
+
for (const key of ['messages', 'history', 'turns', 'events', 'conversation']) {
|
|
55
|
+
if (value[key]) flattenRows(value[key], out, depth + 1, seen);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readSessionFile(fileInfo) {
|
|
61
|
+
const isJsonl = /\.jsonl$/i.test(fileInfo.file);
|
|
62
|
+
return isJsonl
|
|
63
|
+
? readJsonLines(fileInfo.file)
|
|
64
|
+
: { rows: [readJson(fileInfo.file, {})], truncated: false };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function initializeSession(fileInfo, provider, parsed) {
|
|
68
|
+
const rows = parsed.rows.filter(Boolean);
|
|
69
|
+
const root = rows.length === 1 ? rows[0] : { events: rows };
|
|
70
|
+
const externalId = root.session_id || root.sessionId || root.id
|
|
71
|
+
|| path.basename(fileInfo.file).replace(/\.(jsonl?|ndjson)$/i, '');
|
|
72
|
+
const session = baseSession(provider, externalId, fileInfo.file, fileInfo);
|
|
73
|
+
session.truncated = parsed.truncated;
|
|
74
|
+
session.cwd = root.cwd || root.projectPath || root.project_path || '';
|
|
75
|
+
session.model = root.model || root.modelId || '';
|
|
76
|
+
session.startedAt = timestamp(root.startTime || root.startedAt || root.created_at, session.updatedAt);
|
|
77
|
+
session.parentId = root.parent_session_id ? `${provider}:${root.parent_session_id}` : null;
|
|
78
|
+
session.depth = session.parentId ? 1 : 0;
|
|
79
|
+
return { rows, root, session };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function recordToolStart(session, event) {
|
|
83
|
+
const tool = event.tool_name || event.name || event.tool || 'tool';
|
|
84
|
+
addMessage(session, {
|
|
85
|
+
id: event.id,
|
|
86
|
+
role: 'tool',
|
|
87
|
+
type: 'tool',
|
|
88
|
+
title: tool,
|
|
89
|
+
text: compactText(event.parameters || event.args || event.input, 1000),
|
|
90
|
+
status: 'started',
|
|
91
|
+
timestamp: event.timestamp,
|
|
92
|
+
});
|
|
93
|
+
addLifecycle(session, {
|
|
94
|
+
id: event.id,
|
|
95
|
+
type: 'tool',
|
|
96
|
+
label: tool,
|
|
97
|
+
status: 'running',
|
|
98
|
+
timestamp: event.timestamp,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function recordToolEnd(session, event) {
|
|
103
|
+
const callId = event.tool_call_id || event.tool_use_id || event.id;
|
|
104
|
+
settleLifecycle(session, callId, event.error ? 'failed' : 'done', event.timestamp);
|
|
105
|
+
addLifecycle(session, {
|
|
106
|
+
id: `result:${event.id || event.tool_call_id}`,
|
|
107
|
+
type: 'tool-result',
|
|
108
|
+
label: '도구 완료',
|
|
109
|
+
status: event.error ? 'failed' : 'done',
|
|
110
|
+
timestamp: event.timestamp,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function processEvents(session, events) {
|
|
115
|
+
const state = { running: false, failed: false };
|
|
116
|
+
for (const event of events) {
|
|
117
|
+
const type = String(event.type || event.event || event.kind || '').toLowerCase();
|
|
118
|
+
if (type === 'init') {
|
|
119
|
+
session.model = event.model || session.model;
|
|
120
|
+
session.externalId = event.session_id || event.sessionId || session.externalId;
|
|
121
|
+
addLifecycle(session, {
|
|
122
|
+
id: `init:${event.timestamp || 0}`,
|
|
123
|
+
type: 'session-start',
|
|
124
|
+
label: '세션 시작',
|
|
125
|
+
status: 'done',
|
|
126
|
+
timestamp: event.timestamp,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (TOOL_START_PATTERN.test(type)) {
|
|
130
|
+
recordToolStart(session, event);
|
|
131
|
+
state.running = true;
|
|
132
|
+
}
|
|
133
|
+
if (TOOL_END_PATTERN.test(type)) recordToolEnd(session, event);
|
|
134
|
+
if (type === 'result' || /session_end|completed/.test(type)) state.running = false;
|
|
135
|
+
if (type === 'error' || event.error) state.failed = true;
|
|
136
|
+
const usage = normalizeUsage(event);
|
|
137
|
+
if (usage.total) session.usage = usage;
|
|
138
|
+
}
|
|
139
|
+
return state;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizedMessage(row, session) {
|
|
143
|
+
const item = row.value;
|
|
144
|
+
const eventType = String(item.type || item.event || item.kind || '').toLowerCase();
|
|
145
|
+
if (TOOL_START_PATTERN.test(eventType) || TOOL_END_PATTERN.test(eventType)) return null;
|
|
146
|
+
const rawRole = String(item.role || item.author || item.sender || '').toLowerCase();
|
|
147
|
+
const role = /assistant|model|agent/.test(rawRole)
|
|
148
|
+
? 'assistant'
|
|
149
|
+
: (rawRole === 'user' ? 'user' : 'system');
|
|
150
|
+
const deltaText = typeof item.delta === 'string' ? item.delta : '';
|
|
151
|
+
const text = compactText(item.text || item.content || item.message
|
|
152
|
+
|| item.response || item.prompt || deltaText);
|
|
153
|
+
const id = item.id || item.uuid || '';
|
|
154
|
+
const recordedAt = timestamp(item.timestamp || item.created_at, session.updatedAt);
|
|
155
|
+
const isDelta = item.is_delta === true || item.delta === true
|
|
156
|
+
|| typeof item.delta === 'string'
|
|
157
|
+
|| /(?:^|[_-])delta(?:$|[_-])/.test(eventType);
|
|
158
|
+
return {
|
|
159
|
+
item,
|
|
160
|
+
role,
|
|
161
|
+
text,
|
|
162
|
+
id,
|
|
163
|
+
recordedAt,
|
|
164
|
+
isDelta,
|
|
165
|
+
key: id ? `${role}:${id}` : `${role}:${text}:${recordedAt}`,
|
|
166
|
+
order: row.order,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function processMessages(session, root) {
|
|
171
|
+
const messages = new Map();
|
|
172
|
+
const usages = [];
|
|
173
|
+
let firstUser = '';
|
|
174
|
+
for (const row of flattenRows(root)) {
|
|
175
|
+
const message = normalizedMessage(row, session);
|
|
176
|
+
if (!message) continue;
|
|
177
|
+
const previous = messages.get(message.key);
|
|
178
|
+
const mergedText = previous && message.isDelta
|
|
179
|
+
? `${previous.text}${message.text}`
|
|
180
|
+
: message.text;
|
|
181
|
+
if (!previous || message.isDelta || message.text.length >= previous.text.length) {
|
|
182
|
+
messages.set(message.key, {
|
|
183
|
+
id: message.id,
|
|
184
|
+
role: message.role,
|
|
185
|
+
text: mergedText,
|
|
186
|
+
timestamp: message.recordedAt,
|
|
187
|
+
order: previous ? previous.order : message.order,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (message.role === 'user' && !firstUser) firstUser = message.text;
|
|
191
|
+
const usage = normalizeUsage(message.item);
|
|
192
|
+
if (usage.total) {
|
|
193
|
+
session.turnUsage = usage;
|
|
194
|
+
usages.push(usage);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
[...messages.values()]
|
|
198
|
+
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp) || a.order - b.order)
|
|
199
|
+
.forEach(message => addMessage(session, message));
|
|
200
|
+
return { firstUser, usages };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function finalizeSession(session, provider, root, eventState, messageState, fileInfo) {
|
|
204
|
+
if (!session.usage.total && messageState.usages.length) {
|
|
205
|
+
session.usage = sumUsage(messageState.usages);
|
|
206
|
+
}
|
|
207
|
+
session.title = messageState.firstUser || `${provider === 'gemini' ? 'Gemini' : 'Grok'} 세션`;
|
|
208
|
+
const context = modelContextWindow(provider, session.model, root.context_window || root.contextWindow);
|
|
209
|
+
session.context = contextInfo(session.turnUsage.total || session.usage.total, context);
|
|
210
|
+
const age = Date.now() - fileInfo.mtimeMs;
|
|
211
|
+
session.status = eventState.failed
|
|
212
|
+
? 'failed'
|
|
213
|
+
: ((eventState.running && age < STALE_TURN_THRESHOLD_MS) || age < ACTIVE_THRESHOLD_MS
|
|
214
|
+
? 'running'
|
|
215
|
+
: 'idle');
|
|
216
|
+
session.statusDetail = eventState.failed
|
|
217
|
+
? '오류 발생'
|
|
218
|
+
: (session.status === 'running' ? '실시간 이벤트 수신 중' : '다음 요청 대기');
|
|
219
|
+
session.statusObserved = eventState.running;
|
|
220
|
+
trimSession(session);
|
|
221
|
+
return session;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return function parseGeneric(fileInfo, provider) {
|
|
225
|
+
const parsed = readSessionFile(fileInfo);
|
|
226
|
+
const initialized = initializeSession(fileInfo, provider, parsed);
|
|
227
|
+
if (!initialized.rows.length) return null;
|
|
228
|
+
const events = initialized.rows.length === 1 && Array.isArray(initialized.root.events)
|
|
229
|
+
? initialized.root.events
|
|
230
|
+
: initialized.rows;
|
|
231
|
+
const eventState = processEvents(initialized.session, events);
|
|
232
|
+
const messageState = processMessages(initialized.session, initialized.root);
|
|
233
|
+
return finalizeSession(
|
|
234
|
+
initialized.session,
|
|
235
|
+
provider,
|
|
236
|
+
initialized.root,
|
|
237
|
+
eventState,
|
|
238
|
+
messageState,
|
|
239
|
+
fileInfo,
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = { createGenericParser };
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createHierarchyAttacher(dependencies) {
|
|
4
|
+
const {
|
|
5
|
+
addMessage,
|
|
6
|
+
baseSession,
|
|
7
|
+
collaborationTaskName,
|
|
8
|
+
compactText,
|
|
9
|
+
timestamp,
|
|
10
|
+
trimSession,
|
|
11
|
+
} = dependencies;
|
|
12
|
+
|
|
13
|
+
function indexExistingRelationships(sessions) {
|
|
14
|
+
const byId = new Map(sessions.map(session => [session.id, session]));
|
|
15
|
+
for (const session of sessions) session.childIds = [];
|
|
16
|
+
for (const session of sessions) {
|
|
17
|
+
if (!session.parentId) continue;
|
|
18
|
+
const parent = byId.get(session.parentId);
|
|
19
|
+
if (parent && !parent.childIds.includes(session.id)) parent.childIds.push(session.id);
|
|
20
|
+
}
|
|
21
|
+
return byId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findRecordedChild(sessions, byId, parent, record) {
|
|
25
|
+
if (record.childId && byId.has(record.childId)) return byId.get(record.childId);
|
|
26
|
+
return sessions.find(candidate => candidate.parentId === parent.id
|
|
27
|
+
&& ((record.agentPath && candidate.agentPath === record.agentPath)
|
|
28
|
+
|| (record.taskName && candidate.taskName === record.taskName)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createHistoryChild(parent, record) {
|
|
32
|
+
const externalId = String(record.childId || `${parent.externalId}:spawn:${record.callId}`).replace(/^codex:/, '');
|
|
33
|
+
const child = baseSession('codex', externalId, '', {
|
|
34
|
+
mtimeMs: Date.parse(record.completedAt || record.startedAt || parent.updatedAt) || Date.now(),
|
|
35
|
+
});
|
|
36
|
+
child.id = record.childId || child.id;
|
|
37
|
+
child.parentId = parent.id;
|
|
38
|
+
child.depth = Number(parent.depth || 0) + 1;
|
|
39
|
+
child.agentPath = record.agentPath;
|
|
40
|
+
child.taskName = record.taskName;
|
|
41
|
+
child.agentName = record.agentName || record.taskName || 'subagent';
|
|
42
|
+
child.title = record.assignmentObserved
|
|
43
|
+
? compactText(record.assignment, 180)
|
|
44
|
+
: (record.taskName || '서브에이전트 작업');
|
|
45
|
+
child.sharedGoal = record.sharedGoal || parent.title;
|
|
46
|
+
child.status = record.status === 'completed' ? 'completed' : (record.status === 'running' ? 'running' : 'idle');
|
|
47
|
+
child.statusDetail = child.status === 'completed'
|
|
48
|
+
? '작업 완료 기록'
|
|
49
|
+
: (child.status === 'running' ? '실행 시작 관측' : '상태 기록만 확인됨');
|
|
50
|
+
child.startedAt = timestamp(record.startedAt, parent.startedAt);
|
|
51
|
+
child.updatedAt = timestamp(record.completedAt || record.startedAt, parent.updatedAt);
|
|
52
|
+
child.completedAt = timestamp(record.completedAt, null);
|
|
53
|
+
child.completionObserved = child.status === 'completed';
|
|
54
|
+
child.result = record.result || '';
|
|
55
|
+
child.source = 'collaboration-history';
|
|
56
|
+
child.sourceLabel = 'Codex 협업 이벤트';
|
|
57
|
+
child.clientKind = parent.clientKind;
|
|
58
|
+
child.model = parent.model;
|
|
59
|
+
child.cwd = parent.cwd;
|
|
60
|
+
child.workspace = parent.workspace;
|
|
61
|
+
child.projectless = parent.projectless;
|
|
62
|
+
child.environment = parent.environment;
|
|
63
|
+
if (child.result) {
|
|
64
|
+
addMessage(child, {
|
|
65
|
+
id: `${child.id}:result`,
|
|
66
|
+
role: 'assistant',
|
|
67
|
+
text: child.result,
|
|
68
|
+
timestamp: child.completedAt || child.updatedAt,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
trimSession(child);
|
|
72
|
+
return child;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveChild(sessions, byId, parent, record) {
|
|
76
|
+
const existing = findRecordedChild(sessions, byId, parent, record);
|
|
77
|
+
if (existing) return existing;
|
|
78
|
+
const child = createHistoryChild(parent, record);
|
|
79
|
+
sessions.push(child);
|
|
80
|
+
byId.set(child.id, child);
|
|
81
|
+
return child;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function connectCommunications(collaboration, record, child) {
|
|
85
|
+
for (const communication of collaboration.communications || []) {
|
|
86
|
+
if (communication.childId) continue;
|
|
87
|
+
if ((record.agentPath && (communication.from === record.agentPath || communication.to === record.agentPath))
|
|
88
|
+
|| (record.taskName && communication.taskName === record.taskName)) {
|
|
89
|
+
communication.childId = child.id;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function synchronizeSpawn(parent, collaboration, retainedByTask, record, child) {
|
|
95
|
+
record.childId = child.id;
|
|
96
|
+
record.agentPath = record.agentPath || child.agentPath;
|
|
97
|
+
record.taskName = record.taskName || child.taskName || collaborationTaskName(child.agentPath);
|
|
98
|
+
record.agentName = child.agentName || record.agentName;
|
|
99
|
+
record.result = record.result || child.result || '';
|
|
100
|
+
if (child.status === 'running' || child.status === 'starting') record.status = 'running';
|
|
101
|
+
else if (child.status === 'completed' || child.completionObserved || record.result) record.status = 'completed';
|
|
102
|
+
if (!record.completedAt && child.completedAt) record.completedAt = child.completedAt;
|
|
103
|
+
|
|
104
|
+
const retained = retainedByTask.get(record.taskName);
|
|
105
|
+
record.currentlyRetained = Boolean(retained);
|
|
106
|
+
if (retained && retained.name && (!child.agentName || child.agentName === child.taskName)) {
|
|
107
|
+
child.agentName = retained.name;
|
|
108
|
+
}
|
|
109
|
+
child.taskName = child.taskName || record.taskName;
|
|
110
|
+
child.sharedGoal = child.sharedGoal || record.sharedGoal || parent.title;
|
|
111
|
+
child.delegation = {
|
|
112
|
+
taskName: record.taskName,
|
|
113
|
+
assignment: record.assignment,
|
|
114
|
+
assignmentObserved: record.assignmentObserved,
|
|
115
|
+
assignmentProtected: record.assignmentProtected,
|
|
116
|
+
assignmentSource: record.assignmentSource,
|
|
117
|
+
sharedGoal: record.sharedGoal || child.sharedGoal || parent.title,
|
|
118
|
+
result: record.result,
|
|
119
|
+
startedAt: record.startedAt,
|
|
120
|
+
completedAt: record.completedAt,
|
|
121
|
+
currentlyRetained: record.currentlyRetained,
|
|
122
|
+
};
|
|
123
|
+
if (!parent.childIds.includes(child.id)) parent.childIds.push(child.id);
|
|
124
|
+
connectCommunications(collaboration, record, child);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function inferredSpawn(parent, child) {
|
|
128
|
+
return {
|
|
129
|
+
callId: `inferred:${child.id}`,
|
|
130
|
+
taskName: child.taskName || collaborationTaskName(child.agentPath) || child.agentName,
|
|
131
|
+
agentPath: child.agentPath,
|
|
132
|
+
childId: child.id,
|
|
133
|
+
assignment: '',
|
|
134
|
+
assignmentObserved: false,
|
|
135
|
+
assignmentProtected: false,
|
|
136
|
+
assignmentSource: 'unavailable',
|
|
137
|
+
sharedGoal: child.sharedGoal || parent.title,
|
|
138
|
+
status: child.status === 'completed' ? 'completed' : (child.status === 'running' ? 'running' : 'idle'),
|
|
139
|
+
startedAt: child.startedAt,
|
|
140
|
+
completedAt: child.completedAt,
|
|
141
|
+
result: child.result || '',
|
|
142
|
+
agentName: child.agentName,
|
|
143
|
+
currentlyRetained: false,
|
|
144
|
+
inferred: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function attachInferredDelegation(parent, child, record) {
|
|
149
|
+
child.delegation = {
|
|
150
|
+
taskName: record.taskName,
|
|
151
|
+
assignment: '',
|
|
152
|
+
assignmentObserved: false,
|
|
153
|
+
assignmentProtected: true,
|
|
154
|
+
assignmentSource: 'unavailable',
|
|
155
|
+
sharedGoal: child.sharedGoal || parent.title,
|
|
156
|
+
result: child.result || '',
|
|
157
|
+
startedAt: child.startedAt,
|
|
158
|
+
completedAt: child.completedAt,
|
|
159
|
+
currentlyRetained: false,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function appendInferredCommunications(parent, collaboration, child, record) {
|
|
164
|
+
const communications = collaboration.communications || (collaboration.communications = []);
|
|
165
|
+
if (communications.some(event => event.childId === child.id)) return;
|
|
166
|
+
communications.push({
|
|
167
|
+
id: `inferred-assignment:${child.id}`,
|
|
168
|
+
kind: 'assignment',
|
|
169
|
+
label: '작업 배정 확인',
|
|
170
|
+
from: parent.agentPath || '/root',
|
|
171
|
+
to: child.agentPath || child.id,
|
|
172
|
+
taskName: record.taskName,
|
|
173
|
+
childId: child.id,
|
|
174
|
+
text: '',
|
|
175
|
+
protected: true,
|
|
176
|
+
timestamp: child.startedAt || parent.updatedAt,
|
|
177
|
+
});
|
|
178
|
+
communications.push({
|
|
179
|
+
id: `inferred-started:${child.id}`,
|
|
180
|
+
kind: 'started',
|
|
181
|
+
label: '서브에이전트 실행 시작',
|
|
182
|
+
from: 'Codex 런타임',
|
|
183
|
+
to: child.agentPath || child.id,
|
|
184
|
+
taskName: record.taskName,
|
|
185
|
+
childId: child.id,
|
|
186
|
+
text: '',
|
|
187
|
+
protected: false,
|
|
188
|
+
timestamp: child.startedAt || parent.updatedAt,
|
|
189
|
+
});
|
|
190
|
+
if (!child.result && child.status !== 'completed') return;
|
|
191
|
+
communications.push({
|
|
192
|
+
id: `inferred-result:${child.id}`,
|
|
193
|
+
kind: 'result',
|
|
194
|
+
label: '결과 반환 확인',
|
|
195
|
+
from: child.agentPath || child.id,
|
|
196
|
+
to: parent.agentPath || '/root',
|
|
197
|
+
taskName: record.taskName,
|
|
198
|
+
childId: child.id,
|
|
199
|
+
text: child.result || '작업을 완료하고 메인 AI에 결과를 반환했습니다.',
|
|
200
|
+
protected: false,
|
|
201
|
+
timestamp: child.completedAt || child.updatedAt,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function inferUnrecordedChildren(parent, collaboration, byId) {
|
|
206
|
+
for (const childId of parent.childIds) {
|
|
207
|
+
const child = byId.get(childId);
|
|
208
|
+
if (!child || collaboration.spawns.some(record => record.childId === child.id)) continue;
|
|
209
|
+
const record = inferredSpawn(parent, child);
|
|
210
|
+
collaboration.spawns.push(record);
|
|
211
|
+
attachInferredDelegation(parent, child, record);
|
|
212
|
+
appendInferredCommunications(parent, collaboration, child, record);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function updateMetrics(collaboration) {
|
|
217
|
+
const spawns = collaboration.spawns;
|
|
218
|
+
collaboration.metrics = {
|
|
219
|
+
cumulativeCreated: spawns.length,
|
|
220
|
+
simultaneousCapacity: Number(collaboration.capacity && collaboration.capacity.subagents || 0),
|
|
221
|
+
currentlyRunning: spawns.filter(record => record.status === 'running').length,
|
|
222
|
+
completedRecords: spawns.filter(record => record.status === 'completed').length,
|
|
223
|
+
retainedCount: collaboration.retainedObserved ? (collaboration.retainedAgents || []).length : null,
|
|
224
|
+
capacitySource: collaboration.capacity && collaboration.capacity.source || 'unknown',
|
|
225
|
+
cumulativeSource: spawns.some(record => !record.inferred) ? 'spawn-events' : 'child-sessions',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return function attachHierarchy(sessions) {
|
|
230
|
+
const byId = indexExistingRelationships(sessions);
|
|
231
|
+
const parents = sessions.filter(session => session.collaboration
|
|
232
|
+
&& ((Array.isArray(session.collaboration.spawns) && session.collaboration.spawns.length)
|
|
233
|
+
|| (Array.isArray(session.childIds) && session.childIds.length)));
|
|
234
|
+
|
|
235
|
+
for (const parent of parents) {
|
|
236
|
+
const collaboration = parent.collaboration;
|
|
237
|
+
const retainedByTask = new Map((collaboration.retainedAgents || []).map(agent => [agent.taskName, agent]));
|
|
238
|
+
for (const record of collaboration.spawns) {
|
|
239
|
+
const child = resolveChild(sessions, byId, parent, record);
|
|
240
|
+
synchronizeSpawn(parent, collaboration, retainedByTask, record, child);
|
|
241
|
+
}
|
|
242
|
+
inferUnrecordedChildren(parent, collaboration, byId);
|
|
243
|
+
updateMetrics(collaboration);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = { createHierarchyAttacher };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const MAX_FILES_PER_PROVIDER = 80;
|
|
7
|
+
const MAX_JSONL_BYTES = 12 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function safeStat(file) {
|
|
10
|
+
try { return fs.statSync(file); } catch (_missingOrUnreadableFile) { return null; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readJson(file, fallback = null) {
|
|
14
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_missingOrPartialJson) { return fallback; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseJsonText(text) {
|
|
18
|
+
try { return JSON.parse(text); } catch (_plainTextPayload) { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readJsonLines(file, maxBytes = MAX_JSONL_BYTES) {
|
|
22
|
+
const stat = safeStat(file);
|
|
23
|
+
if (!stat || !stat.isFile()) return { rows: [], truncated: false };
|
|
24
|
+
const start = Math.max(0, stat.size - maxBytes);
|
|
25
|
+
const length = stat.size - start;
|
|
26
|
+
const fd = fs.openSync(file, 'r');
|
|
27
|
+
const buffer = Buffer.alloc(length);
|
|
28
|
+
let headerLine = '';
|
|
29
|
+
try {
|
|
30
|
+
fs.readSync(fd, buffer, 0, length, start);
|
|
31
|
+
if (start > 0) {
|
|
32
|
+
const headLength = Math.min(stat.size, 2 * 1024 * 1024);
|
|
33
|
+
const head = Buffer.alloc(headLength);
|
|
34
|
+
fs.readSync(fd, head, 0, headLength, 0);
|
|
35
|
+
const newline = head.indexOf(10);
|
|
36
|
+
if (newline >= 0) {
|
|
37
|
+
headerLine = head.subarray(0, newline).toString('utf8').replace(/\r$/, '');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
fs.closeSync(fd);
|
|
42
|
+
}
|
|
43
|
+
let text = buffer.toString('utf8');
|
|
44
|
+
if (start > 0) {
|
|
45
|
+
const newline = text.indexOf('\n');
|
|
46
|
+
text = newline >= 0 ? text.slice(newline + 1) : '';
|
|
47
|
+
}
|
|
48
|
+
const rows = [];
|
|
49
|
+
for (const line of text.split(/\r?\n/)) {
|
|
50
|
+
if (!line.trim()) continue;
|
|
51
|
+
const row = parseJsonText(line);
|
|
52
|
+
if (row) rows.push(row);
|
|
53
|
+
}
|
|
54
|
+
if (headerLine) {
|
|
55
|
+
const header = parseJsonText(headerLine);
|
|
56
|
+
if (header && header.type === 'session_meta') rows.unshift(header);
|
|
57
|
+
}
|
|
58
|
+
return { rows, truncated: start > 0 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function walkRecent(root, predicate, max = MAX_FILES_PER_PROVIDER, maxDepth = 6) {
|
|
62
|
+
if (!root || !fs.existsSync(root)) return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
65
|
+
while (stack.length) {
|
|
66
|
+
const { dir, depth } = stack.pop();
|
|
67
|
+
let entries = [];
|
|
68
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_unreadableDirectory) { continue; }
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const full = path.join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory() && depth < maxDepth) stack.push({ dir: full, depth: depth + 1 });
|
|
72
|
+
if (!entry.isFile() || !predicate(full, entry.name)) continue;
|
|
73
|
+
const stat = safeStat(full);
|
|
74
|
+
if (stat) out.push({ file: full, mtimeMs: stat.mtimeMs, size: stat.size });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, max);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
MAX_FILES_PER_PROVIDER,
|
|
82
|
+
readJson,
|
|
83
|
+
readJsonLines,
|
|
84
|
+
safeStat,
|
|
85
|
+
walkRecent,
|
|
86
|
+
};
|