@kkelly-offical/kkcode 0.1.7 → 0.2.3-preview.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.
Files changed (166) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +474 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/commands/update.mjs +32 -0
  29. package/src/config/defaults.mjs +289 -260
  30. package/src/config/import-config.mjs +1 -1
  31. package/src/config/load-config.mjs +61 -4
  32. package/src/config/schema.mjs +604 -574
  33. package/src/context.mjs +4 -1
  34. package/src/core/constants.mjs +97 -91
  35. package/src/core/types.mjs +1 -1
  36. package/src/github/api.mjs +78 -78
  37. package/src/github/auth.mjs +294 -286
  38. package/src/github/flow.mjs +298 -298
  39. package/src/github/workspace.mjs +225 -212
  40. package/src/index.mjs +87 -82
  41. package/src/knowledge/frontend-aesthetics.txt +38 -38
  42. package/src/mcp/client-http.mjs +139 -141
  43. package/src/mcp/client-sse.mjs +297 -288
  44. package/src/mcp/client-stdio.mjs +534 -533
  45. package/src/mcp/constants.mjs +4 -2
  46. package/src/mcp/registry.mjs +498 -479
  47. package/src/mcp/stdio-framing.mjs +135 -133
  48. package/src/mcp/tool-result.mjs +24 -24
  49. package/src/observability/edit-diagnostics.mjs +449 -0
  50. package/src/observability/index.mjs +42 -42
  51. package/src/observability/metrics.mjs +165 -137
  52. package/src/observability/tracer.mjs +137 -137
  53. package/src/onboarding.mjs +209 -0
  54. package/src/orchestration/background-manager.mjs +567 -372
  55. package/src/orchestration/background-worker.mjs +419 -305
  56. package/src/orchestration/interruption-reason.mjs +21 -0
  57. package/src/orchestration/longagent-manager.mjs +197 -171
  58. package/src/orchestration/stage-scheduler.mjs +733 -728
  59. package/src/orchestration/subagent-router.mjs +7 -1
  60. package/src/orchestration/task-scheduler.mjs +219 -7
  61. package/src/permission/engine.mjs +1 -1
  62. package/src/permission/exec-policy.mjs +370 -370
  63. package/src/permission/file-edit-policy.mjs +108 -0
  64. package/src/permission/prompt.mjs +1 -1
  65. package/src/permission/rules.mjs +116 -7
  66. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  67. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  68. package/src/plugin/hook-bus.mjs +19 -5
  69. package/src/plugin/manifest-loader.mjs +222 -0
  70. package/src/provider/anthropic.mjs +396 -390
  71. package/src/provider/ollama.mjs +7 -1
  72. package/src/provider/openai.mjs +382 -340
  73. package/src/provider/retry-policy.mjs +74 -68
  74. package/src/provider/router.mjs +242 -241
  75. package/src/provider/sse.mjs +104 -104
  76. package/src/provider/wizard.mjs +556 -0
  77. package/src/repl/capability-facade.mjs +30 -0
  78. package/src/repl/command-surface.mjs +23 -0
  79. package/src/repl/controller-entry.mjs +40 -0
  80. package/src/repl/core-shell.mjs +208 -0
  81. package/src/repl/dialog-router.mjs +87 -0
  82. package/src/repl/input-engine.mjs +76 -0
  83. package/src/repl/keymap.mjs +7 -0
  84. package/src/repl/operator-surface.mjs +15 -0
  85. package/src/repl/permission-flow.mjs +49 -0
  86. package/src/repl/runtime-facade.mjs +36 -0
  87. package/src/repl/slash-router.mjs +62 -0
  88. package/src/repl/state-store.mjs +29 -0
  89. package/src/repl/turn-controller.mjs +58 -0
  90. package/src/repl/verification.mjs +23 -0
  91. package/src/repl.mjs +3371 -2981
  92. package/src/rules/load-rules.mjs +3 -3
  93. package/src/runtime.mjs +1 -1
  94. package/src/session/agent-transaction.mjs +86 -0
  95. package/src/session/checkpoint.mjs +302 -302
  96. package/src/session/compaction.mjs +298 -298
  97. package/src/session/engine.mjs +417 -232
  98. package/src/session/longagent-4stage.mjs +467 -460
  99. package/src/session/longagent-hybrid.mjs +1344 -1097
  100. package/src/session/longagent-plan.mjs +376 -365
  101. package/src/session/longagent-project-memory.mjs +53 -53
  102. package/src/session/longagent-scaffold.mjs +291 -291
  103. package/src/session/longagent-task-bus.mjs +138 -54
  104. package/src/session/longagent-utils.mjs +828 -472
  105. package/src/session/longagent.mjs +911 -900
  106. package/src/session/loop.mjs +1005 -930
  107. package/src/session/prompt/agent.txt +25 -25
  108. package/src/session/prompt/anthropic.txt +150 -150
  109. package/src/session/prompt/beast.txt +1 -1
  110. package/src/session/prompt/plan.txt +31 -31
  111. package/src/session/prompt/qwen.txt +46 -46
  112. package/src/session/recovery.mjs +21 -0
  113. package/src/session/rollback.mjs +196 -195
  114. package/src/session/routing-observability.mjs +72 -0
  115. package/src/session/runtime-state.mjs +47 -0
  116. package/src/session/store.mjs +523 -519
  117. package/src/session/system-prompt.mjs +308 -273
  118. package/src/session/task-validator.mjs +267 -267
  119. package/src/session/usability-gates.mjs +2 -2
  120. package/src/skill/builtin/commit.mjs +64 -64
  121. package/src/skill/builtin/design.mjs +76 -76
  122. package/src/skill/generator.mjs +18 -2
  123. package/src/skill/registry.mjs +642 -390
  124. package/src/storage/audit-store.mjs +18 -11
  125. package/src/storage/event-log.mjs +7 -1
  126. package/src/storage/ghost-commit-store.mjs +243 -245
  127. package/src/storage/paths.mjs +17 -0
  128. package/src/theme/default-theme.mjs +1 -1
  129. package/src/theme/markdown.mjs +4 -0
  130. package/src/theme/schema.mjs +1 -1
  131. package/src/theme/status-bar.mjs +162 -158
  132. package/src/tool/audit-wrapper.mjs +18 -2
  133. package/src/tool/edit-transaction.mjs +23 -0
  134. package/src/tool/executor.mjs +26 -1
  135. package/src/tool/file-read-state.mjs +65 -0
  136. package/src/tool/git-auto.mjs +526 -526
  137. package/src/tool/git-full-auto.mjs +487 -478
  138. package/src/tool/mutation-guard.mjs +54 -0
  139. package/src/tool/prompt/edit.txt +3 -3
  140. package/src/tool/prompt/multiedit.txt +1 -0
  141. package/src/tool/prompt/notebookedit.txt +2 -1
  142. package/src/tool/prompt/patch.txt +25 -24
  143. package/src/tool/prompt/read.txt +3 -3
  144. package/src/tool/prompt/sysinfo.txt +29 -0
  145. package/src/tool/prompt/task.txt +66 -4
  146. package/src/tool/prompt/write.txt +2 -2
  147. package/src/tool/question-prompt.mjs +99 -93
  148. package/src/tool/registry.mjs +1701 -1343
  149. package/src/tool/task-tool.mjs +14 -6
  150. package/src/ui/activity-renderer.mjs +667 -664
  151. package/src/ui/repl-background-panel.mjs +7 -0
  152. package/src/ui/repl-capability-panel.mjs +9 -0
  153. package/src/ui/repl-dashboard.mjs +54 -4
  154. package/src/ui/repl-help.mjs +110 -0
  155. package/src/ui/repl-operator-panel.mjs +12 -0
  156. package/src/ui/repl-route-feedback.mjs +35 -0
  157. package/src/ui/repl-status-view.mjs +76 -0
  158. package/src/ui/repl-task-panel.mjs +5 -0
  159. package/src/ui/repl-transcript-panel.mjs +56 -0
  160. package/src/ui/repl-turn-summary.mjs +135 -0
  161. package/src/update/checker.mjs +184 -0
  162. package/src/usage/pricing.mjs +122 -121
  163. package/src/usage/usage-meter.mjs +1 -0
  164. package/src/util/git.mjs +562 -519
  165. package/src/util/template.mjs +6 -1
  166. package/src/version.mjs +3 -0
@@ -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.TURN_START) {
63
- inc("turn_count")
64
- if (turnId) {
65
- turnStarts.set(turnId, event.timestamp)
66
- pruneStaleMap(turnStarts)
67
- }
68
- }
69
-
70
- if (type === EVENT_TYPES.TURN_FINISH) {
71
- if (turnId && turnStarts.has(turnId)) {
72
- hist("turn_duration_ms").record(event.timestamp - turnStarts.get(turnId))
73
- turnStarts.delete(turnId)
74
- }
75
- }
76
-
77
- if (type === EVENT_TYPES.TURN_ERROR) {
78
- inc("error_count")
79
- }
80
-
81
- if (type === EVENT_TYPES.TOOL_START) {
82
- inc("tool_call_count")
83
- }
84
-
85
- if (type === EVENT_TYPES.TOOL_ERROR) {
86
- inc("tool_error_count")
87
- }
88
-
89
- if (type === EVENT_TYPES.TURN_USAGE_UPDATE) {
90
- if (payload?.input) inc("token_input", payload.input)
91
- if (payload?.output) inc("token_output", payload.output)
92
- if (payload?.cacheRead) inc("token_cache_read", payload.cacheRead)
93
- }
94
-
95
- if (type === EVENT_TYPES.LONGAGENT_STAGE_STARTED) {
96
- const key = payload?.stageId || sessionId
97
- if (key) {
98
- stageStarts.set(key, event.timestamp)
99
- pruneStaleMap(stageStarts)
100
- }
101
- }
102
-
103
- if (type === EVENT_TYPES.LONGAGENT_STAGE_FINISHED) {
104
- const key = payload?.stageId || sessionId
105
- if (key && stageStarts.has(key)) {
106
- hist("longagent_stage_duration_ms").record(event.timestamp - stageStarts.get(key))
107
- stageStarts.delete(key)
108
- }
109
- if (payload?.retryCount > 0) {
110
- inc("longagent_task_retries", payload.retryCount)
111
- }
112
- }
113
-
114
- if (type === EVENT_TYPES.LONGAGENT_GATE_CHECKED) {
115
- inc("gate_check_count")
116
- if (payload?.status === "pass") inc("gate_pass_count")
117
- }
118
- }
119
-
120
- function getSnapshot() {
121
- const counterSnapshot = new Map(counters)
122
- const histogramSnapshot = new Map()
123
- for (const [name, h] of histograms) {
124
- histogramSnapshot.set(name, h.snapshot())
125
- }
126
- return { counters: counterSnapshot, histograms: histogramSnapshot }
127
- }
128
-
129
- function reset() {
130
- counters.clear()
131
- histograms.clear()
132
- turnStarts.clear()
133
- stageStarts.clear()
134
- }
135
-
136
- return { handleEvent, getSnapshot, reset }
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
+ }