@kkelly-offical/kkcode 0.1.6 → 0.2.1
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/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2929
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +17 -4
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
|
@@ -1,137 +1,165 @@
|
|
|
1
|
-
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
2
|
-
|
|
3
|
-
function createHistogram() {
|
|
4
|
-
const values = []
|
|
5
|
-
return {
|
|
6
|
-
record(v) { values.push(v) },
|
|
7
|
-
snapshot() {
|
|
8
|
-
if (values.length === 0) return { count: 0, sum: 0, min: 0, max: 0, avg: 0, p50: 0, p99: 0 }
|
|
9
|
-
const sorted = [...values].sort((a, b) => a - b)
|
|
10
|
-
const sum = sorted.reduce((s, v) => s + v, 0)
|
|
11
|
-
const count = sorted.length
|
|
12
|
-
return {
|
|
13
|
-
count,
|
|
14
|
-
sum,
|
|
15
|
-
min: sorted[0],
|
|
16
|
-
max: sorted[count - 1],
|
|
17
|
-
avg: sum / count,
|
|
18
|
-
p50: sorted[Math.max(0, Math.ceil(count * 0.5) - 1)] || 0,
|
|
19
|
-
p99: sorted[Math.max(0, Math.ceil(count * 0.99) - 1)] || 0
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
reset() { values.length = 0 }
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function createMetricsCollector() {
|
|
27
|
-
const counters = new Map()
|
|
28
|
-
const histograms = new Map()
|
|
29
|
-
const turnStarts = new Map()
|
|
30
|
-
const stageStarts = new Map()
|
|
31
|
-
|
|
32
|
-
function inc(name, amount = 1) {
|
|
33
|
-
counters.set(name, (counters.get(name) || 0) + amount)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function hist(name) {
|
|
37
|
-
if (!histograms.has(name)) histograms.set(name, createHistogram())
|
|
38
|
-
return histograms.get(name)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const MAX_OPEN_ENTRIES = 500
|
|
42
|
-
|
|
43
|
-
function pruneStaleMap(map) {
|
|
44
|
-
if (map.size <= MAX_OPEN_ENTRIES) return
|
|
45
|
-
const cutoff = Date.now() - 30 * 60 * 1000 // 30 min
|
|
46
|
-
for (const [k, v] of map) {
|
|
47
|
-
if (v < cutoff) map.delete(k)
|
|
48
|
-
}
|
|
49
|
-
// If still over limit, drop oldest half
|
|
50
|
-
if (map.size > MAX_OPEN_ENTRIES) {
|
|
51
|
-
let toDrop = Math.floor(map.size / 2)
|
|
52
|
-
for (const k of map.keys()) {
|
|
53
|
-
if (toDrop-- <= 0) break
|
|
54
|
-
map.delete(k)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function handleEvent(event) {
|
|
60
|
-
const { type, payload, turnId, sessionId } = event
|
|
61
|
-
|
|
62
|
-
if (type === EVENT_TYPES.
|
|
63
|
-
inc("
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
inc("
|
|
116
|
-
if (payload?.
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
1
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
2
|
+
|
|
3
|
+
function createHistogram() {
|
|
4
|
+
const values = []
|
|
5
|
+
return {
|
|
6
|
+
record(v) { values.push(v) },
|
|
7
|
+
snapshot() {
|
|
8
|
+
if (values.length === 0) return { count: 0, sum: 0, min: 0, max: 0, avg: 0, p50: 0, p99: 0 }
|
|
9
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
10
|
+
const sum = sorted.reduce((s, v) => s + v, 0)
|
|
11
|
+
const count = sorted.length
|
|
12
|
+
return {
|
|
13
|
+
count,
|
|
14
|
+
sum,
|
|
15
|
+
min: sorted[0],
|
|
16
|
+
max: sorted[count - 1],
|
|
17
|
+
avg: sum / count,
|
|
18
|
+
p50: sorted[Math.max(0, Math.ceil(count * 0.5) - 1)] || 0,
|
|
19
|
+
p99: sorted[Math.max(0, Math.ceil(count * 0.99) - 1)] || 0
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
reset() { values.length = 0 }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createMetricsCollector() {
|
|
27
|
+
const counters = new Map()
|
|
28
|
+
const histograms = new Map()
|
|
29
|
+
const turnStarts = new Map()
|
|
30
|
+
const stageStarts = new Map()
|
|
31
|
+
|
|
32
|
+
function inc(name, amount = 1) {
|
|
33
|
+
counters.set(name, (counters.get(name) || 0) + amount)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hist(name) {
|
|
37
|
+
if (!histograms.has(name)) histograms.set(name, createHistogram())
|
|
38
|
+
return histograms.get(name)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const MAX_OPEN_ENTRIES = 500
|
|
42
|
+
|
|
43
|
+
function pruneStaleMap(map) {
|
|
44
|
+
if (map.size <= MAX_OPEN_ENTRIES) return
|
|
45
|
+
const cutoff = Date.now() - 30 * 60 * 1000 // 30 min
|
|
46
|
+
for (const [k, v] of map) {
|
|
47
|
+
if (v < cutoff) map.delete(k)
|
|
48
|
+
}
|
|
49
|
+
// If still over limit, drop oldest half
|
|
50
|
+
if (map.size > MAX_OPEN_ENTRIES) {
|
|
51
|
+
let toDrop = Math.floor(map.size / 2)
|
|
52
|
+
for (const k of map.keys()) {
|
|
53
|
+
if (toDrop-- <= 0) break
|
|
54
|
+
map.delete(k)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleEvent(event) {
|
|
60
|
+
const { type, payload, turnId, sessionId } = event
|
|
61
|
+
|
|
62
|
+
if (type === EVENT_TYPES.ROUTE_DECISION) {
|
|
63
|
+
inc("route_decision_count")
|
|
64
|
+
if (payload?.changed) inc("route_changed_count")
|
|
65
|
+
if (payload?.suggestion === "longagent") inc("route_longagent_suggestion_count")
|
|
66
|
+
if (payload?.continuedTransaction) inc("route_continuation_count")
|
|
67
|
+
if (payload?.stayedLocal) inc("route_stayed_local_count")
|
|
68
|
+
if (payload?.deferredLongagent) inc("route_deferred_longagent_count")
|
|
69
|
+
if (payload?.overEscalatedToLongagent) inc("route_over_escalated_longagent_count")
|
|
70
|
+
if (Array.isArray(payload?.evidence)) {
|
|
71
|
+
for (const item of payload.evidence) {
|
|
72
|
+
const key = String(item || "").trim().replace(/[^\w]+/g, "_")
|
|
73
|
+
if (key) inc(`route_evidence_${key}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (type === EVENT_TYPES.AGENT_CONTINUATION_INTERRUPTED) {
|
|
79
|
+
inc("agent_continuation_interrupted_count")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (type === EVENT_TYPES.AGENT_CONTINUATION_RESUMED) {
|
|
83
|
+
inc("agent_continuation_resumed_count")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (type === EVENT_TYPES.TURN_START) {
|
|
87
|
+
inc("turn_count")
|
|
88
|
+
if (turnId) {
|
|
89
|
+
turnStarts.set(turnId, event.timestamp)
|
|
90
|
+
pruneStaleMap(turnStarts)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (type === EVENT_TYPES.TURN_FINISH) {
|
|
95
|
+
if (turnId && turnStarts.has(turnId)) {
|
|
96
|
+
hist("turn_duration_ms").record(event.timestamp - turnStarts.get(turnId))
|
|
97
|
+
turnStarts.delete(turnId)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (type === EVENT_TYPES.TURN_ERROR) {
|
|
102
|
+
inc("error_count")
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (type === EVENT_TYPES.TOOL_START) {
|
|
106
|
+
inc("tool_call_count")
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (type === EVENT_TYPES.TOOL_ERROR) {
|
|
110
|
+
inc("tool_error_count")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (type === EVENT_TYPES.TURN_USAGE_UPDATE) {
|
|
114
|
+
if (payload?.input) inc("token_input", payload.input)
|
|
115
|
+
if (payload?.output) inc("token_output", payload.output)
|
|
116
|
+
if (payload?.cacheRead) inc("token_cache_read", payload.cacheRead)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (type === EVENT_TYPES.AGENT_CONTINUATION_RESUMED) {
|
|
120
|
+
inc("agent_continuation_count")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (type === EVENT_TYPES.LONGAGENT_STAGE_STARTED) {
|
|
124
|
+
const key = payload?.stageId || sessionId
|
|
125
|
+
if (key) {
|
|
126
|
+
stageStarts.set(key, event.timestamp)
|
|
127
|
+
pruneStaleMap(stageStarts)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (type === EVENT_TYPES.LONGAGENT_STAGE_FINISHED) {
|
|
132
|
+
const key = payload?.stageId || sessionId
|
|
133
|
+
if (key && stageStarts.has(key)) {
|
|
134
|
+
hist("longagent_stage_duration_ms").record(event.timestamp - stageStarts.get(key))
|
|
135
|
+
stageStarts.delete(key)
|
|
136
|
+
}
|
|
137
|
+
if (payload?.retryCount > 0) {
|
|
138
|
+
inc("longagent_task_retries", payload.retryCount)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (type === EVENT_TYPES.LONGAGENT_GATE_CHECKED) {
|
|
143
|
+
inc("gate_check_count")
|
|
144
|
+
if (payload?.status === "pass") inc("gate_pass_count")
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getSnapshot() {
|
|
149
|
+
const counterSnapshot = new Map(counters)
|
|
150
|
+
const histogramSnapshot = new Map()
|
|
151
|
+
for (const [name, h] of histograms) {
|
|
152
|
+
histogramSnapshot.set(name, h.snapshot())
|
|
153
|
+
}
|
|
154
|
+
return { counters: counterSnapshot, histograms: histogramSnapshot }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function reset() {
|
|
158
|
+
counters.clear()
|
|
159
|
+
histograms.clear()
|
|
160
|
+
turnStarts.clear()
|
|
161
|
+
stageStarts.clear()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { handleEvent, getSnapshot, reset }
|
|
165
|
+
}
|
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
2
|
-
import { randomUUID } from "node:crypto"
|
|
3
|
-
|
|
4
|
-
function newSpanId() {
|
|
5
|
-
return `span_${randomUUID().slice(0, 12)}`
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function createTracer(options = {}) {
|
|
9
|
-
const maxTraces = options.maxTraces || 100
|
|
10
|
-
const maxOpenSpans = options.maxOpenSpans || 500
|
|
11
|
-
const traces = []
|
|
12
|
-
const openSpans = new Map()
|
|
13
|
-
const phaseSpan = { current: null }
|
|
14
|
-
let currentTraceId = null
|
|
15
|
-
|
|
16
|
-
function pruneOpenSpans() {
|
|
17
|
-
if (openSpans.size <= maxOpenSpans) return
|
|
18
|
-
// Close oldest spans as "expired"
|
|
19
|
-
let toDrop = Math.floor(openSpans.size / 2)
|
|
20
|
-
for (const [key, span] of openSpans) {
|
|
21
|
-
if (toDrop-- <= 0) break
|
|
22
|
-
closeSpan(span, "expired")
|
|
23
|
-
openSpans.delete(key)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function startSpan(name, attributes = {}, parentSpanId = null, timestamp = null) {
|
|
28
|
-
if (!currentTraceId) currentTraceId = `trace_${randomUUID().slice(0, 12)}`
|
|
29
|
-
const span = {
|
|
30
|
-
traceId: currentTraceId,
|
|
31
|
-
spanId: newSpanId(),
|
|
32
|
-
parentSpanId,
|
|
33
|
-
name,
|
|
34
|
-
startTime: timestamp || Date.now(),
|
|
35
|
-
endTime: null,
|
|
36
|
-
duration: null,
|
|
37
|
-
attributes,
|
|
38
|
-
status: "ok"
|
|
39
|
-
}
|
|
40
|
-
return span
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function closeSpan(span, status = "ok", timestamp = null) {
|
|
44
|
-
span.endTime = timestamp || Date.now()
|
|
45
|
-
span.duration = span.endTime - span.startTime
|
|
46
|
-
span.status = status
|
|
47
|
-
traces.push(span)
|
|
48
|
-
if (traces.length > maxTraces) traces.shift()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function handleEvent(event) {
|
|
52
|
-
const { type, payload, turnId, sessionId, timestamp } = event
|
|
53
|
-
|
|
54
|
-
if (type === EVENT_TYPES.TURN_START) {
|
|
55
|
-
const key = `turn:${turnId}`
|
|
56
|
-
if (turnId && openSpans.has(key)) {
|
|
57
|
-
closeSpan(openSpans.get(key), "error", timestamp)
|
|
58
|
-
openSpans.delete(key)
|
|
59
|
-
}
|
|
60
|
-
const span = startSpan("turn", { turnId, sessionId }, null, timestamp)
|
|
61
|
-
if (turnId) openSpans.set(key, span)
|
|
62
|
-
pruneOpenSpans()
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (type === EVENT_TYPES.TURN_FINISH) {
|
|
66
|
-
const key = `turn:${turnId}`
|
|
67
|
-
const span = openSpans.get(key)
|
|
68
|
-
if (span) {
|
|
69
|
-
closeSpan(span, "ok", timestamp)
|
|
70
|
-
openSpans.delete(key)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (type === EVENT_TYPES.TURN_ERROR) {
|
|
75
|
-
const key = `turn:${turnId}`
|
|
76
|
-
const span = openSpans.get(key)
|
|
77
|
-
if (span) {
|
|
78
|
-
span.attributes.error = payload?.error || "unknown"
|
|
79
|
-
closeSpan(span, "error", timestamp)
|
|
80
|
-
openSpans.delete(key)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (type === EVENT_TYPES.LONGAGENT_STAGE_STARTED) {
|
|
85
|
-
const stageId = payload?.stageId
|
|
86
|
-
if (stageId) {
|
|
87
|
-
const span = startSpan("stage", { stageId, sessionId }, null, timestamp)
|
|
88
|
-
openSpans.set(`stage:${stageId}`, span)
|
|
89
|
-
pruneOpenSpans()
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (type === EVENT_TYPES.LONGAGENT_STAGE_FINISHED) {
|
|
94
|
-
const stageId = payload?.stageId
|
|
95
|
-
const key = `stage:${stageId}`
|
|
96
|
-
const span = openSpans.get(key)
|
|
97
|
-
if (span) {
|
|
98
|
-
span.attributes.successCount = payload?.successCount
|
|
99
|
-
span.attributes.failCount = payload?.failCount
|
|
100
|
-
closeSpan(span, payload?.allSuccess ? "ok" : "error", timestamp)
|
|
101
|
-
openSpans.delete(key)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (type === EVENT_TYPES.LONGAGENT_PHASE_CHANGED) {
|
|
106
|
-
if (phaseSpan.current) {
|
|
107
|
-
closeSpan(phaseSpan.current, "ok", timestamp)
|
|
108
|
-
}
|
|
109
|
-
const span = startSpan("phase", {
|
|
110
|
-
phase: payload?.phase || payload?.newPhase,
|
|
111
|
-
sessionId
|
|
112
|
-
}, null, timestamp)
|
|
113
|
-
phaseSpan.current = span
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function getTraces() {
|
|
118
|
-
const result = [...traces]
|
|
119
|
-
if (phaseSpan.current) result.push({ ...phaseSpan.current, status: "open" })
|
|
120
|
-
return result
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function exportTraces(format = "json") {
|
|
124
|
-
const all = getTraces()
|
|
125
|
-
if (format === "json") return JSON.stringify(all, null, 2)
|
|
126
|
-
return JSON.stringify(all)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function reset() {
|
|
130
|
-
traces.length = 0
|
|
131
|
-
openSpans.clear()
|
|
132
|
-
phaseSpan.current = null
|
|
133
|
-
currentTraceId = null
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return { handleEvent, getTraces, exportTraces, reset }
|
|
137
|
-
}
|
|
1
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
2
|
+
import { randomUUID } from "node:crypto"
|
|
3
|
+
|
|
4
|
+
function newSpanId() {
|
|
5
|
+
return `span_${randomUUID().slice(0, 12)}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createTracer(options = {}) {
|
|
9
|
+
const maxTraces = options.maxTraces || 100
|
|
10
|
+
const maxOpenSpans = options.maxOpenSpans || 500
|
|
11
|
+
const traces = []
|
|
12
|
+
const openSpans = new Map()
|
|
13
|
+
const phaseSpan = { current: null }
|
|
14
|
+
let currentTraceId = null
|
|
15
|
+
|
|
16
|
+
function pruneOpenSpans() {
|
|
17
|
+
if (openSpans.size <= maxOpenSpans) return
|
|
18
|
+
// Close oldest spans as "expired"
|
|
19
|
+
let toDrop = Math.floor(openSpans.size / 2)
|
|
20
|
+
for (const [key, span] of openSpans) {
|
|
21
|
+
if (toDrop-- <= 0) break
|
|
22
|
+
closeSpan(span, "expired")
|
|
23
|
+
openSpans.delete(key)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function startSpan(name, attributes = {}, parentSpanId = null, timestamp = null) {
|
|
28
|
+
if (!currentTraceId) currentTraceId = `trace_${randomUUID().slice(0, 12)}`
|
|
29
|
+
const span = {
|
|
30
|
+
traceId: currentTraceId,
|
|
31
|
+
spanId: newSpanId(),
|
|
32
|
+
parentSpanId,
|
|
33
|
+
name,
|
|
34
|
+
startTime: timestamp || Date.now(),
|
|
35
|
+
endTime: null,
|
|
36
|
+
duration: null,
|
|
37
|
+
attributes,
|
|
38
|
+
status: "ok"
|
|
39
|
+
}
|
|
40
|
+
return span
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function closeSpan(span, status = "ok", timestamp = null) {
|
|
44
|
+
span.endTime = timestamp || Date.now()
|
|
45
|
+
span.duration = span.endTime - span.startTime
|
|
46
|
+
span.status = status
|
|
47
|
+
traces.push(span)
|
|
48
|
+
if (traces.length > maxTraces) traces.shift()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleEvent(event) {
|
|
52
|
+
const { type, payload, turnId, sessionId, timestamp } = event
|
|
53
|
+
|
|
54
|
+
if (type === EVENT_TYPES.TURN_START) {
|
|
55
|
+
const key = `turn:${turnId}`
|
|
56
|
+
if (turnId && openSpans.has(key)) {
|
|
57
|
+
closeSpan(openSpans.get(key), "error", timestamp)
|
|
58
|
+
openSpans.delete(key)
|
|
59
|
+
}
|
|
60
|
+
const span = startSpan("turn", { turnId, sessionId }, null, timestamp)
|
|
61
|
+
if (turnId) openSpans.set(key, span)
|
|
62
|
+
pruneOpenSpans()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (type === EVENT_TYPES.TURN_FINISH) {
|
|
66
|
+
const key = `turn:${turnId}`
|
|
67
|
+
const span = openSpans.get(key)
|
|
68
|
+
if (span) {
|
|
69
|
+
closeSpan(span, "ok", timestamp)
|
|
70
|
+
openSpans.delete(key)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (type === EVENT_TYPES.TURN_ERROR) {
|
|
75
|
+
const key = `turn:${turnId}`
|
|
76
|
+
const span = openSpans.get(key)
|
|
77
|
+
if (span) {
|
|
78
|
+
span.attributes.error = payload?.error || "unknown"
|
|
79
|
+
closeSpan(span, "error", timestamp)
|
|
80
|
+
openSpans.delete(key)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (type === EVENT_TYPES.LONGAGENT_STAGE_STARTED) {
|
|
85
|
+
const stageId = payload?.stageId
|
|
86
|
+
if (stageId) {
|
|
87
|
+
const span = startSpan("stage", { stageId, sessionId }, null, timestamp)
|
|
88
|
+
openSpans.set(`stage:${stageId}`, span)
|
|
89
|
+
pruneOpenSpans()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (type === EVENT_TYPES.LONGAGENT_STAGE_FINISHED) {
|
|
94
|
+
const stageId = payload?.stageId
|
|
95
|
+
const key = `stage:${stageId}`
|
|
96
|
+
const span = openSpans.get(key)
|
|
97
|
+
if (span) {
|
|
98
|
+
span.attributes.successCount = payload?.successCount
|
|
99
|
+
span.attributes.failCount = payload?.failCount
|
|
100
|
+
closeSpan(span, payload?.allSuccess ? "ok" : "error", timestamp)
|
|
101
|
+
openSpans.delete(key)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (type === EVENT_TYPES.LONGAGENT_PHASE_CHANGED) {
|
|
106
|
+
if (phaseSpan.current) {
|
|
107
|
+
closeSpan(phaseSpan.current, "ok", timestamp)
|
|
108
|
+
}
|
|
109
|
+
const span = startSpan("phase", {
|
|
110
|
+
phase: payload?.phase || payload?.newPhase,
|
|
111
|
+
sessionId
|
|
112
|
+
}, null, timestamp)
|
|
113
|
+
phaseSpan.current = span
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getTraces() {
|
|
118
|
+
const result = [...traces]
|
|
119
|
+
if (phaseSpan.current) result.push({ ...phaseSpan.current, status: "open" })
|
|
120
|
+
return result
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function exportTraces(format = "json") {
|
|
124
|
+
const all = getTraces()
|
|
125
|
+
if (format === "json") return JSON.stringify(all, null, 2)
|
|
126
|
+
return JSON.stringify(all)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function reset() {
|
|
130
|
+
traces.length = 0
|
|
131
|
+
openSpans.clear()
|
|
132
|
+
phaseSpan.current = null
|
|
133
|
+
currentTraceId = null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { handleEvent, getTraces, exportTraces, reset }
|
|
137
|
+
}
|