deuk-agent-flow 4.0.19
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/CHANGELOG.ko.md +223 -0
- package/CHANGELOG.md +227 -0
- package/LICENSE +184 -0
- package/README.ko.md +282 -0
- package/README.md +270 -0
- package/bin/deuk-agent-flow.js +50 -0
- package/bin/deuk-agent-rule.js +2 -0
- package/core-rules/AGENTS.md +153 -0
- package/core-rules/GEMINI.md +7 -0
- package/docs/architecture.ko.md +34 -0
- package/docs/architecture.md +33 -0
- package/docs/assets/architecture-v3.png +0 -0
- package/docs/how-it-works.ko.md +52 -0
- package/docs/how-it-works.md +71 -0
- package/docs/principles.ko.md +68 -0
- package/docs/principles.md +68 -0
- package/docs/usage-guide.ko.md +212 -0
- package/package.json +96 -0
- package/scripts/cli-args.mjs +200 -0
- package/scripts/cli-init-commands.mjs +1799 -0
- package/scripts/cli-init-logic.mjs +64 -0
- package/scripts/cli-prompts.mjs +104 -0
- package/scripts/cli-rule-compiler.mjs +112 -0
- package/scripts/cli-skill-commands.mjs +201 -0
- package/scripts/cli-telemetry-commands.mjs +599 -0
- package/scripts/cli-ticket-commands.mjs +2393 -0
- package/scripts/cli-ticket-index.mjs +298 -0
- package/scripts/cli-ticket-migration.mjs +320 -0
- package/scripts/cli-ticket-parser.mjs +209 -0
- package/scripts/cli-usage-commands.mjs +326 -0
- package/scripts/cli-utils.mjs +587 -0
- package/scripts/cli.mjs +246 -0
- package/scripts/lint-md.mjs +267 -0
- package/scripts/lint-rules.mjs +186 -0
- package/scripts/merge-logic.mjs +44 -0
- package/scripts/plan-parser.mjs +53 -0
- package/scripts/publish-dual-npm.mjs +141 -0
- package/scripts/smoke-npm-docker.mjs +102 -0
- package/scripts/smoke-npm-local.mjs +109 -0
- package/scripts/update-download-badge.mjs +107 -0
- package/templates/MODULE_RULE_TEMPLATE.md +11 -0
- package/templates/PROJECT_RULE.md +47 -0
- package/templates/TICKET_TEMPLATE.ko.md +44 -0
- package/templates/TICKET_TEMPLATE.md +44 -0
- package/templates/project-pilot/CONFORMANCE_GATE_TEMPLATE.md +23 -0
- package/templates/project-pilot/DRIFT_CHECKLIST.md +19 -0
- package/templates/project-pilot/FLOW_CONTRACT_TEMPLATE.md +26 -0
- package/templates/project-pilot/IMPLEMENTATION_MATRIX_TEMPLATE.md +30 -0
- package/templates/project-pilot/INTEGRATION_CONTRACT_TEMPLATE.md +26 -0
- package/templates/project-pilot/OWNER_MAP_TEMPLATE.md +15 -0
- package/templates/project-pilot/PROJECT_PILOT_RULE_TEMPLATE.md +34 -0
- package/templates/project-pilot/REFACTOR_CONTRACT_TEMPLATE.md +32 -0
- package/templates/project-pilot/REMEDIATION_PLAN_TEMPLATE.md +33 -0
- package/templates/rules.d/deukcontext-mcp.md +31 -0
- package/templates/rules.d/platform-coexistence.md +29 -0
- package/templates/skills/context-recall/SKILL.md +25 -0
- package/templates/skills/generated-file-guard/SKILL.md +25 -0
- package/templates/skills/project-pilot/SKILL.md +63 -0
- package/templates/skills/safe-refactor/SKILL.md +25 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { loadInitConfig, AGENT_ROOT_DIR, SPOKE_REGISTRY, toSlug } from "./cli-utils.mjs";
|
|
4
|
+
|
|
5
|
+
const TELEMETRY_FILE = `${AGENT_ROOT_DIR}/telemetry.jsonl`;
|
|
6
|
+
const RAG_RESULTS = new Set(["hit", "weak-hit", "miss", "stale"]);
|
|
7
|
+
const KNOWLEDGE_ACTIONS = new Set(["none", "add_knowledge", "refresh_document"]);
|
|
8
|
+
const TOKEN_QUALITIES = new Set(["useful", "waste", "rework", "saved"]);
|
|
9
|
+
const SESSION_MODES = new Set(["guided", "unguided", "mixed"]);
|
|
10
|
+
const OUTCOMES = new Set(["success", "failure", "partial", "blocked"]);
|
|
11
|
+
const INTERNAL_SOURCE = "internal";
|
|
12
|
+
const WORKFLOW_EVENT_KIND = "workflow_event";
|
|
13
|
+
const CLIENT_LABELS = new Map([
|
|
14
|
+
["antigravity", "Antigravity"],
|
|
15
|
+
["claudecode", "ClaudeCode"],
|
|
16
|
+
["copilot", "Copilot"],
|
|
17
|
+
["cursor", "Cursor"],
|
|
18
|
+
["codex", "Codex"],
|
|
19
|
+
["deukagentflow", "DeukAgentFlow"],
|
|
20
|
+
["jetbrains", "JetBrains"],
|
|
21
|
+
["windsurf", "Windsurf"]
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function normalizeDimensionValue(value) {
|
|
25
|
+
return String(value || "").trim().replace(/\s+/g, " ");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeModelLabel(value) {
|
|
29
|
+
const normalized = normalizeDimensionValue(value);
|
|
30
|
+
if (!normalized) return "";
|
|
31
|
+
return normalized.toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeClientLabel(value) {
|
|
35
|
+
const normalized = normalizeDimensionValue(value);
|
|
36
|
+
if (!normalized) return "";
|
|
37
|
+
const collapsed = toSlug(normalized).replace(/-/g, "");
|
|
38
|
+
return CLIENT_LABELS.get(collapsed) || normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function accumulateTotals(logs, keySelector) {
|
|
42
|
+
return logs.reduce((acc, entry) => {
|
|
43
|
+
const key = keySelector(entry);
|
|
44
|
+
if (!key) return acc;
|
|
45
|
+
acc[key] = (acc[key] || 0) + Number(entry.tokens || 0);
|
|
46
|
+
return acc;
|
|
47
|
+
}, {});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function loadTelemetryEntries(cwd) {
|
|
51
|
+
const absPath = join(cwd, TELEMETRY_FILE);
|
|
52
|
+
if (!existsSync(absPath)) return null;
|
|
53
|
+
const lines = readFileSync(absPath, "utf8").split("\n").filter(l => l.trim());
|
|
54
|
+
return lines.map(l => JSON.parse(l));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildTelemetrySummary(cwd) {
|
|
58
|
+
const logs = loadTelemetryEntries(cwd);
|
|
59
|
+
if (!logs || logs.length === 0) return null;
|
|
60
|
+
|
|
61
|
+
const workflowEvents = logs.filter(isInternalWorkflowEvent);
|
|
62
|
+
const workLogs = logs.filter(l => !isInternalWorkflowEvent(l));
|
|
63
|
+
const missingEventCount = logs.filter(l => !String(l.event || "").trim()).length;
|
|
64
|
+
const eventCoverageRate = rate(logs.length - missingEventCount, logs.length);
|
|
65
|
+
const totalTokens = workLogs.reduce((sum, l) => sum + l.tokens, 0);
|
|
66
|
+
const totalTdwTokens = workLogs.reduce((sum, l) => sum + Number(l.tdw || 0), 0);
|
|
67
|
+
const totalSavedTokens = workLogs.reduce((sum, l) => sum + Number(l.savedTokens || 0), 0);
|
|
68
|
+
const byModel = accumulateTotals(workLogs, l => normalizeModelLabel(l.model));
|
|
69
|
+
const byClient = accumulateTotals(workLogs, l => normalizeClientLabel(l.client));
|
|
70
|
+
const byAgent = accumulateTotals(workLogs, l => normalizeDimensionValue(l.agentId) || normalizeClientLabel(l.client));
|
|
71
|
+
const tdwEntryCount = workLogs.filter(l => Number(l.tdw || 0) > 0).length;
|
|
72
|
+
const tdwAverageTokensPerEntry = tdwEntryCount > 0 ? totalTdwTokens / tdwEntryCount : 0;
|
|
73
|
+
const byRagResult = countBy(workLogs, "ragResult");
|
|
74
|
+
const byTokenQuality = countBy(workLogs, "tokenQuality");
|
|
75
|
+
const byKnowledgeAction = countBy(workLogs, "knowledgeAction");
|
|
76
|
+
const byKnowledgeSourceKind = countBy(workflowEvents, "knowledgeSourceKind");
|
|
77
|
+
const byKnowledgeIngestionCategory = countBy(workflowEvents, "knowledgeIngestionCategory");
|
|
78
|
+
const byKnowledgeCorpus = countBy(workflowEvents, "knowledgeCorpus");
|
|
79
|
+
const byKnowledgeOriginTool = countBy(workflowEvents, "knowledgeOriginTool");
|
|
80
|
+
const localFallbackCount = workLogs.filter(l => l.localFallback).length;
|
|
81
|
+
const ragCalls = Object.values(byRagResult).reduce((sum, n) => sum + n, 0);
|
|
82
|
+
const ragHits = (byRagResult.hit || 0) + (byRagResult["weak-hit"] || 0);
|
|
83
|
+
const ragMisses = byRagResult.miss || 0;
|
|
84
|
+
const staleKnowledge = byRagResult.stale || 0;
|
|
85
|
+
const workflowSummary = summarizeWorkflowEvents(workflowEvents);
|
|
86
|
+
const sessionModeComparison = summarizeSessionModes(workLogs);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
cwd,
|
|
90
|
+
totalTokens,
|
|
91
|
+
totalTdwTokens,
|
|
92
|
+
totalSavedTokens,
|
|
93
|
+
logEntries: workLogs.length,
|
|
94
|
+
totalLogEntries: logs.length,
|
|
95
|
+
missingEventCount,
|
|
96
|
+
eventCoverageRate,
|
|
97
|
+
byModel,
|
|
98
|
+
byClient,
|
|
99
|
+
byAgent,
|
|
100
|
+
tdwEntryCount,
|
|
101
|
+
tdwCoverageRate: rate(tdwEntryCount, workLogs.length),
|
|
102
|
+
tdwTokenShare: rate(totalTdwTokens, totalTokens),
|
|
103
|
+
tdwAverageTokensPerEntry,
|
|
104
|
+
ragCalls,
|
|
105
|
+
ragHitRate: rate(ragHits, ragCalls),
|
|
106
|
+
ragMissRate: rate(ragMisses, ragCalls),
|
|
107
|
+
staleKnowledgeRate: rate(staleKnowledge, ragCalls),
|
|
108
|
+
localFallbackRate: rate(localFallbackCount, workLogs.length),
|
|
109
|
+
byRagResult,
|
|
110
|
+
byTokenQuality,
|
|
111
|
+
byKnowledgeAction,
|
|
112
|
+
byKnowledgeSourceKind,
|
|
113
|
+
byKnowledgeIngestionCategory,
|
|
114
|
+
byKnowledgeCorpus,
|
|
115
|
+
byKnowledgeOriginTool,
|
|
116
|
+
sessionModeComparison,
|
|
117
|
+
workflowEvents: workflowSummary
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getTelemetryCompactSummary(cwd) {
|
|
122
|
+
const summary = buildTelemetrySummary(cwd);
|
|
123
|
+
if (!summary) return "";
|
|
124
|
+
return `telemetry logs ${summary.logEntries}, coverage ${formatRate(summary.totalLogEntries - summary.missingEventCount, summary.totalLogEntries)}, tdw ${formatRate(summary.tdwEntryCount, summary.logEntries)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function runTelemetry(opts) {
|
|
128
|
+
const argv = process.argv.slice(3); // skip 'telemetry' and 'log/sync/summary'
|
|
129
|
+
const action = process.argv[3];
|
|
130
|
+
|
|
131
|
+
if (action === "log") {
|
|
132
|
+
await logAction(opts);
|
|
133
|
+
} else if (action === "sync") {
|
|
134
|
+
await syncAction(opts);
|
|
135
|
+
} else if (action === "summary") {
|
|
136
|
+
await summaryAction(opts);
|
|
137
|
+
} else if (action === "migrate") {
|
|
138
|
+
await migrateAction(opts);
|
|
139
|
+
} else {
|
|
140
|
+
console.error("Unknown telemetry action: " + action);
|
|
141
|
+
console.log("Usage: npx deuk-agent-flow telemetry <log|sync|summary|migrate> [options]");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function logAction(opts) {
|
|
146
|
+
let resolvedClient = opts.client;
|
|
147
|
+
const lowerModel = (opts.model || "").toLowerCase();
|
|
148
|
+
|
|
149
|
+
if (!resolvedClient) {
|
|
150
|
+
if (lowerModel.includes("codex")) resolvedClient = "Codex";
|
|
151
|
+
else if (lowerModel.includes("copilot")) resolvedClient = "Copilot";
|
|
152
|
+
else if (lowerModel.includes("claude")) resolvedClient = "ClaudeCode";
|
|
153
|
+
else {
|
|
154
|
+
const config = loadInitConfig(opts.cwd);
|
|
155
|
+
const tools = config?.agentTools || [];
|
|
156
|
+
if (tools.includes("codex")) resolvedClient = "Codex";
|
|
157
|
+
else if (tools.includes("copilot")) resolvedClient = "Copilot";
|
|
158
|
+
else if (tools.includes("cursor")) resolvedClient = "Cursor";
|
|
159
|
+
else if (tools.includes("claude")) resolvedClient = "ClaudeCode";
|
|
160
|
+
else {
|
|
161
|
+
for (const spoke of SPOKE_REGISTRY) {
|
|
162
|
+
if (spoke.id !== "antigravity" && spoke.detect(opts.cwd, tools)) {
|
|
163
|
+
if (spoke.id === "copilot") { resolvedClient = "Copilot"; break; }
|
|
164
|
+
if (spoke.id === "codex") { resolvedClient = "Codex"; break; }
|
|
165
|
+
if (spoke.id === "cursor") { resolvedClient = "Cursor"; break; }
|
|
166
|
+
if (spoke.id === "claude") { resolvedClient = "ClaudeCode"; break; }
|
|
167
|
+
if (spoke.id === "windsurf") { resolvedClient = "Windsurf"; break; }
|
|
168
|
+
if (spoke.id === "jetbrains") { resolvedClient = "JetBrains"; break; }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!resolvedClient) resolvedClient = "Antigravity";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const entry = appendTelemetryRecord(opts.cwd, {
|
|
177
|
+
source: opts.source || "manual",
|
|
178
|
+
kind: opts.kind || "work",
|
|
179
|
+
event: opts.event || "",
|
|
180
|
+
tokens: Number(opts.tokens || 0),
|
|
181
|
+
tdw: Number(opts.tdw || 0),
|
|
182
|
+
model: opts.model || "UNKNOWN",
|
|
183
|
+
client: resolvedClient,
|
|
184
|
+
agentId: opts.agentId || "",
|
|
185
|
+
ticket: opts.ticket || "",
|
|
186
|
+
action: opts.action || "work",
|
|
187
|
+
file: opts.file || "",
|
|
188
|
+
ragResult: normalizeEnum(opts.ragResult, RAG_RESULTS),
|
|
189
|
+
localFallback: Boolean(opts.localFallback),
|
|
190
|
+
knowledgeAction: normalizeEnum(opts.knowledgeAction, KNOWLEDGE_ACTIONS) || "none",
|
|
191
|
+
tokenQuality: normalizeEnum(opts.tokenQuality, TOKEN_QUALITIES),
|
|
192
|
+
savedTokens: Number(opts.savedTokens || 0),
|
|
193
|
+
sessionMode: normalizeEnum(opts.sessionMode, SESSION_MODES),
|
|
194
|
+
retryCount: Number(opts.retryCount || 0),
|
|
195
|
+
turnCount: Number(opts.turnCount || 0),
|
|
196
|
+
failureCount: Number(opts.failureCount || 0),
|
|
197
|
+
phaseTransitionCount: Number(opts.phaseTransitionCount || 0),
|
|
198
|
+
outcome: normalizeEnum(opts.outcome, OUTCOMES),
|
|
199
|
+
qualityScore: normalizeQualityScore(opts.qualityScore),
|
|
200
|
+
occurredAt: opts.occurredAt || ""
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
console.log(`[TELEMETRY] Logged ${entry.tokens} tokens for ticket ${entry.ticket} in ${TELEMETRY_FILE}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function syncAction(opts) {
|
|
207
|
+
const absPath = join(opts.cwd, TELEMETRY_FILE);
|
|
208
|
+
if (!existsSync(absPath)) {
|
|
209
|
+
console.log("[TELEMETRY] No local logs to sync.");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const config = loadInitConfig(opts.cwd);
|
|
214
|
+
const pipelineUrl = opts.remote || config?.pipelineUrl || "http://localhost:8001/api/telemetry/ingest";
|
|
215
|
+
|
|
216
|
+
const lines = readFileSync(absPath, "utf8").split("\n").filter(l => l.trim());
|
|
217
|
+
const entries = lines.map(l => JSON.parse(l));
|
|
218
|
+
const unsynced = entries.filter(e => !e.synced);
|
|
219
|
+
|
|
220
|
+
if (unsynced.length === 0) {
|
|
221
|
+
console.log("[TELEMETRY] All logs already synced.");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log(`[TELEMETRY] Syncing ${unsynced.length} entries to ${pipelineUrl}...`);
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// In a real environment, we'd use fetch or a pipeline sync tool.
|
|
229
|
+
// For now, we simulate the sync success and mark them as synced.
|
|
230
|
+
// We try to use the built-in fetch if available.
|
|
231
|
+
if (typeof fetch === "function") {
|
|
232
|
+
const res = await fetch(pipelineUrl, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: { "Content-Type": "application/json" },
|
|
235
|
+
body: JSON.stringify({ logs: unsynced })
|
|
236
|
+
});
|
|
237
|
+
if (!res.ok) throw new Error(`HTTP error ${res.status}`);
|
|
238
|
+
} else {
|
|
239
|
+
console.warn("[TELEMETRY] fetch not available, simulating sync...");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const updatedLines = entries.map(e => {
|
|
243
|
+
if (!e.synced) e.synced = true;
|
|
244
|
+
return JSON.stringify(e);
|
|
245
|
+
}).join("\n") + "\n";
|
|
246
|
+
|
|
247
|
+
writeFileSync(absPath, updatedLines, "utf8");
|
|
248
|
+
console.log(`[TELEMETRY] Successfully synced ${unsynced.length} entries.`);
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(`[TELEMETRY] Sync failed: ${err.message}`);
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function summaryAction(opts) {
|
|
256
|
+
const summary = buildTelemetrySummary(opts.cwd);
|
|
257
|
+
if (!summary) {
|
|
258
|
+
console.log("[TELEMETRY] No logs found.");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (opts.json) {
|
|
263
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log(`\n--- Local Telemetry Summary (${opts.cwd}) ---`);
|
|
268
|
+
console.log(`Total Tokens: ${summary.totalTokens}`);
|
|
269
|
+
console.log(`TDW Tokens: ${summary.totalTdwTokens}`);
|
|
270
|
+
console.log(`Saved Tokens: ${summary.totalSavedTokens}`);
|
|
271
|
+
console.log(`Log Entries: ${summary.logEntries}`);
|
|
272
|
+
console.log(`Total Entries: ${summary.totalLogEntries}`);
|
|
273
|
+
console.log(`Event Coverage:`);
|
|
274
|
+
console.log(` - Missing Event Count: ${summary.missingEventCount}`);
|
|
275
|
+
console.log(` - Coverage Rate: ${formatRate(summary.totalLogEntries - summary.missingEventCount, summary.totalLogEntries)}`);
|
|
276
|
+
console.log(`By Model:`);
|
|
277
|
+
Object.entries(summary.byModel).forEach(([m, t]) => {
|
|
278
|
+
console.log(` - ${m}: ${t}`);
|
|
279
|
+
});
|
|
280
|
+
printCounts("By Client", summary.byClient);
|
|
281
|
+
printCounts("By Agent", summary.byAgent);
|
|
282
|
+
console.log(`TDW:`);
|
|
283
|
+
console.log(` - Entries: ${summary.tdwEntryCount}`);
|
|
284
|
+
console.log(` - Coverage Rate: ${formatRate(summary.tdwEntryCount, summary.logEntries)}`);
|
|
285
|
+
console.log(` - Token Share: ${formatRate(summary.totalTdwTokens, summary.totalTokens)}`);
|
|
286
|
+
console.log(` - Average Tokens/Entry: ${summary.tdwAverageTokensPerEntry.toFixed(1)}`);
|
|
287
|
+
console.log(`RAG Quality:`);
|
|
288
|
+
console.log(` - Calls: ${summary.ragCalls}`);
|
|
289
|
+
console.log(` - Hit Rate: ${formatRate((summary.byRagResult.hit || 0) + (summary.byRagResult["weak-hit"] || 0), summary.ragCalls)}`);
|
|
290
|
+
console.log(` - Miss Rate: ${formatRate(summary.byRagResult.miss || 0, summary.ragCalls)}`);
|
|
291
|
+
console.log(` - Stale Rate: ${formatRate(summary.byRagResult.stale || 0, summary.ragCalls)}`);
|
|
292
|
+
console.log(` - Local Fallback Rate: ${formatRate(summary.localFallbackRate * summary.logEntries, summary.logEntries)}`);
|
|
293
|
+
printCounts("By RAG Result", summary.byRagResult);
|
|
294
|
+
printCounts("By Token Quality", summary.byTokenQuality);
|
|
295
|
+
printCounts("By Knowledge Action", summary.byKnowledgeAction);
|
|
296
|
+
printCounts("By Knowledge Source Kind", summary.byKnowledgeSourceKind);
|
|
297
|
+
printCounts("By Knowledge Ingestion Category", summary.byKnowledgeIngestionCategory);
|
|
298
|
+
printCounts("By Knowledge Corpus", summary.byKnowledgeCorpus);
|
|
299
|
+
printCounts("By Knowledge Origin Tool", summary.byKnowledgeOriginTool);
|
|
300
|
+
printSessionModeComparison(summary.sessionModeComparison);
|
|
301
|
+
console.log(`Internal Workflow Events:`);
|
|
302
|
+
console.log(` - Events: ${summary.workflowEvents.eventCount}`);
|
|
303
|
+
console.log(` - Tickets: ${summary.workflowEvents.ticketCount}`);
|
|
304
|
+
console.log(` - Average Time To Phase Move: ${formatDuration(summary.workflowEvents.averageTimeToPhaseMoveMs)}`);
|
|
305
|
+
console.log(` - Average Time To Close: ${formatDuration(summary.workflowEvents.averageTimeToCloseMs)}`);
|
|
306
|
+
console.log(` - Average Time To Archive: ${formatDuration(summary.workflowEvents.averageTimeToArchiveMs)}`);
|
|
307
|
+
printCounts("By Workflow Event", summary.workflowEvents.byEvent);
|
|
308
|
+
console.log("-------------------------------------------\n");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function migrateAction(opts) {
|
|
312
|
+
const absPath = join(opts.cwd, TELEMETRY_FILE);
|
|
313
|
+
if (!existsSync(absPath)) {
|
|
314
|
+
console.log("[TELEMETRY] No logs found.");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const lines = readFileSync(absPath, "utf8").split("\n").filter(l => l.trim());
|
|
319
|
+
const entries = lines.map(l => JSON.parse(l));
|
|
320
|
+
const migrated = entries.map(entry => normalizeTelemetryRecord(entry));
|
|
321
|
+
const changedCount = migrated.filter((entry, index) => JSON.stringify(entry) !== JSON.stringify(entries[index])).length;
|
|
322
|
+
|
|
323
|
+
if (changedCount === 0) {
|
|
324
|
+
console.log("[TELEMETRY] Telemetry logs already normalized.");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
writeFileSync(absPath, migrated.map(entry => JSON.stringify(entry)).join("\n") + "\n", "utf8");
|
|
329
|
+
console.log(`[TELEMETRY] Migrated ${changedCount} telemetry entries.`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function appendTelemetryRecord(cwd, entry = {}) {
|
|
333
|
+
const telemetryDir = join(cwd, AGENT_ROOT_DIR);
|
|
334
|
+
if (!existsSync(telemetryDir)) mkdirSync(telemetryDir, { recursive: true });
|
|
335
|
+
const telemetryPath = join(cwd, TELEMETRY_FILE);
|
|
336
|
+
const occurredAt = entry.occurredAt || new Date().toISOString();
|
|
337
|
+
const event = resolveTelemetryEvent(entry);
|
|
338
|
+
const payload = {
|
|
339
|
+
ts: Math.floor(new Date(occurredAt).getTime() / 1000) || Math.floor(Date.now() / 1000),
|
|
340
|
+
occurredAt,
|
|
341
|
+
source: entry.source || "manual",
|
|
342
|
+
kind: entry.kind || "work",
|
|
343
|
+
event,
|
|
344
|
+
tokens: Number(entry.tokens || 0),
|
|
345
|
+
tdw: Number(entry.tdw || 0),
|
|
346
|
+
model: entry.model || "UNKNOWN",
|
|
347
|
+
client: entry.client || "",
|
|
348
|
+
agentId: entry.agentId || "",
|
|
349
|
+
ticket: entry.ticket || "",
|
|
350
|
+
action: entry.action || "work",
|
|
351
|
+
file: entry.file || "",
|
|
352
|
+
phase: entry.phase ?? "",
|
|
353
|
+
status: entry.status || "",
|
|
354
|
+
ragResult: normalizeEnum(entry.ragResult, RAG_RESULTS),
|
|
355
|
+
localFallback: Boolean(entry.localFallback),
|
|
356
|
+
knowledgeAction: normalizeEnum(entry.knowledgeAction, KNOWLEDGE_ACTIONS) || "",
|
|
357
|
+
knowledgeSourceKind: normalizeText(entry.knowledgeSourceKind),
|
|
358
|
+
knowledgeIngestionCategory: normalizeText(entry.knowledgeIngestionCategory),
|
|
359
|
+
knowledgeCorpus: normalizeText(entry.knowledgeCorpus),
|
|
360
|
+
knowledgeOriginTool: normalizeText(entry.knowledgeOriginTool),
|
|
361
|
+
knowledgeFreshness: normalizeText(entry.knowledgeFreshness),
|
|
362
|
+
tokenQuality: normalizeEnum(entry.tokenQuality, TOKEN_QUALITIES),
|
|
363
|
+
savedTokens: Number(entry.savedTokens || 0),
|
|
364
|
+
sessionMode: normalizeEnum(entry.sessionMode, SESSION_MODES),
|
|
365
|
+
retryCount: Number(entry.retryCount || 0),
|
|
366
|
+
turnCount: Number(entry.turnCount || 0),
|
|
367
|
+
failureCount: Number(entry.failureCount || 0),
|
|
368
|
+
phaseTransitionCount: Number(entry.phaseTransitionCount || 0),
|
|
369
|
+
outcome: normalizeEnum(entry.outcome, OUTCOMES),
|
|
370
|
+
qualityScore: normalizeQualityScore(entry.qualityScore),
|
|
371
|
+
synced: false
|
|
372
|
+
};
|
|
373
|
+
appendFileSync(telemetryPath, JSON.stringify(payload) + "\n", "utf8");
|
|
374
|
+
return payload;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function appendInternalWorkflowEvent(cwd, event = {}) {
|
|
378
|
+
return appendTelemetryRecord(cwd, {
|
|
379
|
+
...event,
|
|
380
|
+
source: INTERNAL_SOURCE,
|
|
381
|
+
kind: WORKFLOW_EVENT_KIND,
|
|
382
|
+
model: event.model || "workflow",
|
|
383
|
+
client: event.client || "DeukAgentFlow",
|
|
384
|
+
tokens: 0,
|
|
385
|
+
tdw: 0,
|
|
386
|
+
action: event.action || event.event || "workflow-event"
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function resolveTelemetryEvent(entry = {}) {
|
|
391
|
+
const explicitEvent = normalizeText(entry.event);
|
|
392
|
+
if (explicitEvent) return explicitEvent;
|
|
393
|
+
|
|
394
|
+
if (isInternalWorkflowEvent({
|
|
395
|
+
source: entry.source || INTERNAL_SOURCE,
|
|
396
|
+
kind: entry.kind || WORKFLOW_EVENT_KIND
|
|
397
|
+
})) {
|
|
398
|
+
return normalizeText(entry.action) || normalizeText(entry.kind) || "workflow-event";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return normalizeText(entry.kind) || "work";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function normalizeTelemetryRecord(entry = {}) {
|
|
405
|
+
const cloned = { ...entry };
|
|
406
|
+
cloned.source = cloned.source || "manual";
|
|
407
|
+
cloned.kind = cloned.kind || "work";
|
|
408
|
+
cloned.action = cloned.action || "work";
|
|
409
|
+
cloned.event = resolveTelemetryEvent(cloned);
|
|
410
|
+
cloned.synced = normalizeSyncedState(cloned.synced);
|
|
411
|
+
return cloned;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function normalizeEnum(value, allowed) {
|
|
415
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
416
|
+
return allowed.has(normalized) ? normalized : "";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function normalizeText(value) {
|
|
420
|
+
return String(value || "").trim();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function normalizeQualityScore(value) {
|
|
424
|
+
const score = Number(value || 0);
|
|
425
|
+
if (!Number.isFinite(score) || score <= 0) return 0;
|
|
426
|
+
if (score > 5) return 5;
|
|
427
|
+
return score;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function normalizeSyncedState(value) {
|
|
431
|
+
return value === true || value === "true";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function countBy(logs, key) {
|
|
435
|
+
return logs.reduce((acc, log) => {
|
|
436
|
+
const value = log[key];
|
|
437
|
+
if (value) acc[value] = (acc[value] || 0) + 1;
|
|
438
|
+
return acc;
|
|
439
|
+
}, {});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function isInternalWorkflowEvent(log) {
|
|
443
|
+
return log?.source === INTERNAL_SOURCE && log?.kind === WORKFLOW_EVENT_KIND;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function summarizeWorkflowEvents(events) {
|
|
447
|
+
const sorted = [...events].sort((a, b) => eventTime(a) - eventTime(b));
|
|
448
|
+
const byTicket = new Map();
|
|
449
|
+
for (const event of sorted) {
|
|
450
|
+
const ticket = event.ticket || "";
|
|
451
|
+
if (!ticket) continue;
|
|
452
|
+
const rows = byTicket.get(ticket) || [];
|
|
453
|
+
rows.push(event);
|
|
454
|
+
byTicket.set(ticket, rows);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const closeDurations = [];
|
|
458
|
+
const archiveDurations = [];
|
|
459
|
+
const phaseMoveDurations = [];
|
|
460
|
+
for (const rows of byTicket.values()) {
|
|
461
|
+
const created = rows.find(e => e.event === "ticket_created");
|
|
462
|
+
const closed = rows.find(e => e.event === "ticket_closed");
|
|
463
|
+
const archived = rows.find(e => e.event === "ticket_archived");
|
|
464
|
+
const firstPhaseMove = rows.find(e => e.event === "ticket_phase_moved");
|
|
465
|
+
if (created && closed) closeDurations.push(eventTime(closed) - eventTime(created));
|
|
466
|
+
if (created && archived) archiveDurations.push(eventTime(archived) - eventTime(created));
|
|
467
|
+
if (created && firstPhaseMove) phaseMoveDurations.push(eventTime(firstPhaseMove) - eventTime(created));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
eventCount: events.length,
|
|
472
|
+
ticketCount: byTicket.size,
|
|
473
|
+
byEvent: countBy(events, "event"),
|
|
474
|
+
averageTimeToCloseMs: average(closeDurations),
|
|
475
|
+
averageTimeToArchiveMs: average(archiveDurations),
|
|
476
|
+
averageTimeToPhaseMoveMs: average(phaseMoveDurations)
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function summarizeSessionModes(logs) {
|
|
481
|
+
const grouped = {};
|
|
482
|
+
for (const log of logs) {
|
|
483
|
+
const mode = normalizeEnum(log.sessionMode, SESSION_MODES);
|
|
484
|
+
if (!mode) continue;
|
|
485
|
+
const row = grouped[mode] || {
|
|
486
|
+
entries: 0,
|
|
487
|
+
tokens: 0,
|
|
488
|
+
tdwTokens: 0,
|
|
489
|
+
savedTokens: 0,
|
|
490
|
+
retries: 0,
|
|
491
|
+
turns: 0,
|
|
492
|
+
failures: 0,
|
|
493
|
+
phaseTransitions: 0,
|
|
494
|
+
successCount: 0,
|
|
495
|
+
outcomeFailureCount: 0,
|
|
496
|
+
blockedCount: 0,
|
|
497
|
+
qualityScoreTotal: 0,
|
|
498
|
+
qualityScoreEntries: 0,
|
|
499
|
+
byOutcome: {},
|
|
500
|
+
byTokenQuality: {}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
row.entries += 1;
|
|
504
|
+
row.tokens += Number(log.tokens || 0);
|
|
505
|
+
row.tdwTokens += Number(log.tdw || 0);
|
|
506
|
+
row.savedTokens += Number(log.savedTokens || 0);
|
|
507
|
+
row.retries += Number(log.retryCount || 0);
|
|
508
|
+
row.turns += Number(log.turnCount || 0);
|
|
509
|
+
row.failures += Number(log.failureCount || 0);
|
|
510
|
+
row.phaseTransitions += Number(log.phaseTransitionCount || 0);
|
|
511
|
+
|
|
512
|
+
const outcome = normalizeEnum(log.outcome, OUTCOMES);
|
|
513
|
+
if (outcome) {
|
|
514
|
+
row.byOutcome[outcome] = (row.byOutcome[outcome] || 0) + 1;
|
|
515
|
+
if (outcome === "success") row.successCount += 1;
|
|
516
|
+
if (outcome === "failure") row.outcomeFailureCount += 1;
|
|
517
|
+
if (outcome === "blocked") row.blockedCount += 1;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const tokenQuality = normalizeEnum(log.tokenQuality, TOKEN_QUALITIES);
|
|
521
|
+
if (tokenQuality) row.byTokenQuality[tokenQuality] = (row.byTokenQuality[tokenQuality] || 0) + 1;
|
|
522
|
+
|
|
523
|
+
const qualityScore = normalizeQualityScore(log.qualityScore);
|
|
524
|
+
if (qualityScore > 0) {
|
|
525
|
+
row.qualityScoreTotal += qualityScore;
|
|
526
|
+
row.qualityScoreEntries += 1;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
grouped[mode] = row;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return Object.fromEntries(Object.entries(grouped).map(([mode, row]) => [mode, {
|
|
533
|
+
entries: row.entries,
|
|
534
|
+
tokens: row.tokens,
|
|
535
|
+
tdwTokens: row.tdwTokens,
|
|
536
|
+
savedTokens: row.savedTokens,
|
|
537
|
+
retries: row.retries,
|
|
538
|
+
turns: row.turns,
|
|
539
|
+
failures: row.failures,
|
|
540
|
+
phaseTransitions: row.phaseTransitions,
|
|
541
|
+
successCount: row.successCount,
|
|
542
|
+
outcomeFailureCount: row.outcomeFailureCount,
|
|
543
|
+
blockedCount: row.blockedCount,
|
|
544
|
+
successRate: rate(row.successCount, row.entries),
|
|
545
|
+
outcomeFailureRate: rate(row.outcomeFailureCount, row.entries),
|
|
546
|
+
averageRetriesPerEntry: rate(row.retries, row.entries),
|
|
547
|
+
averageFailuresPerEntry: rate(row.failures, row.entries),
|
|
548
|
+
averageTokensPerEntry: rate(row.tokens, row.entries),
|
|
549
|
+
averageTurnsPerEntry: rate(row.turns, row.entries),
|
|
550
|
+
averagePhaseTransitionsPerEntry: rate(row.phaseTransitions, row.entries),
|
|
551
|
+
averageQualityScore: rate(row.qualityScoreTotal, row.qualityScoreEntries),
|
|
552
|
+
byOutcome: row.byOutcome,
|
|
553
|
+
byTokenQuality: row.byTokenQuality
|
|
554
|
+
}]));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function eventTime(event) {
|
|
558
|
+
const fromOccurredAt = Date.parse(event?.occurredAt || "");
|
|
559
|
+
if (Number.isFinite(fromOccurredAt)) return fromOccurredAt;
|
|
560
|
+
return Number(event?.ts || 0) * 1000;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function average(values) {
|
|
564
|
+
return values.length > 0 ? values.reduce((sum, n) => sum + n, 0) / values.length : 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function formatDuration(ms) {
|
|
568
|
+
if (!ms) return "0ms";
|
|
569
|
+
if (ms < 1000) return `${ms.toFixed(0)}ms`;
|
|
570
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
571
|
+
if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`;
|
|
572
|
+
return `${(ms / 3600000).toFixed(1)}h`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function rate(value, total) {
|
|
576
|
+
return total > 0 ? value / total : 0;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function formatRate(value, total) {
|
|
580
|
+
return `${(rate(value, total) * 100).toFixed(1)}%`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function printCounts(label, counts) {
|
|
584
|
+
const entries = Object.entries(counts);
|
|
585
|
+
if (entries.length === 0) return;
|
|
586
|
+
console.log(`${label}:`);
|
|
587
|
+
entries.forEach(([name, count]) => {
|
|
588
|
+
console.log(` - ${name}: ${count}`);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function printSessionModeComparison(summary) {
|
|
593
|
+
const entries = Object.entries(summary);
|
|
594
|
+
if (entries.length === 0) return;
|
|
595
|
+
console.log(`Session Mode Comparison:`);
|
|
596
|
+
for (const [mode, row] of entries) {
|
|
597
|
+
console.log(` - ${mode}: entries=${row.entries}, tokens=${row.tokens}, retries=${row.retries}, turns=${row.turns}, failures=${row.failures}, success=${formatRate(row.successCount, row.entries)}, quality=${row.averageQualityScore.toFixed(1)}`);
|
|
598
|
+
}
|
|
599
|
+
}
|