opencode-onboard 0.2.6 → 0.2.7
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.
|
@@ -2,14 +2,29 @@ import fs from "node:fs"
|
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
|
|
4
4
|
const LOG_FILE = ".agents/session-log.json"
|
|
5
|
+
const SPAWN_MATCH_WINDOW_MS = 15000
|
|
6
|
+
const DEBUG = process.env.SESSION_LOG_DEBUG === "true"
|
|
5
7
|
|
|
6
|
-
// Per-session state
|
|
8
|
+
// Per-session state
|
|
7
9
|
const sessionState = new Map()
|
|
8
10
|
|
|
11
|
+
// Lead session -> current team name
|
|
12
|
+
const leadTeamBySession = new Map()
|
|
13
|
+
|
|
14
|
+
// Pending spawn records waiting for a session.created match
|
|
15
|
+
const pendingSpawns = []
|
|
16
|
+
|
|
17
|
+
// Team -> completed session snapshots
|
|
18
|
+
const completedByTeam = new Map()
|
|
19
|
+
|
|
9
20
|
function ts() {
|
|
10
21
|
return new Date().toISOString()
|
|
11
22
|
}
|
|
12
23
|
|
|
24
|
+
function nowMs() {
|
|
25
|
+
return Date.now()
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
function appendEntry(directory, entry) {
|
|
14
29
|
const logPath = path.join(directory, LOG_FILE)
|
|
15
30
|
const dir = path.dirname(logPath)
|
|
@@ -40,35 +55,148 @@ function addSkillToState(state, skillName) {
|
|
|
40
55
|
return true
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
function
|
|
58
|
+
function toNum(v) {
|
|
59
|
+
if (typeof v === "number" && Number.isFinite(v)) return v
|
|
60
|
+
if (typeof v === "string" && v.trim() !== "" && !Number.isNaN(Number(v))) return Number(v)
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractReportedTokens(obj) {
|
|
65
|
+
const out = { input: 0, output: 0, total: 0, found: false }
|
|
66
|
+
const visited = new Set()
|
|
67
|
+
|
|
68
|
+
function walk(node) {
|
|
69
|
+
if (!node || typeof node !== "object") return
|
|
70
|
+
if (visited.has(node)) return
|
|
71
|
+
visited.add(node)
|
|
72
|
+
|
|
73
|
+
for (const [k, v] of Object.entries(node)) {
|
|
74
|
+
const key = String(k).toLowerCase()
|
|
75
|
+
const n = toNum(v)
|
|
76
|
+
|
|
77
|
+
if (n !== null) {
|
|
78
|
+
if ((key.includes("input") || key.includes("prompt")) && key.includes("token")) {
|
|
79
|
+
out.input += n
|
|
80
|
+
out.found = true
|
|
81
|
+
} else if ((key.includes("output") || key.includes("completion")) && key.includes("token")) {
|
|
82
|
+
out.output += n
|
|
83
|
+
out.found = true
|
|
84
|
+
} else if (key.includes("total") && key.includes("token")) {
|
|
85
|
+
out.total += n
|
|
86
|
+
out.found = true
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (v && typeof v === "object") walk(v)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
walk(obj)
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function estimateTokens(state) {
|
|
99
|
+
const inTokens = Math.ceil((state.charIn || 0) / 4)
|
|
100
|
+
const outTokens = Math.ceil((state.charOut || 0) / 4)
|
|
101
|
+
const base = inTokens + outTokens + Math.max(0, (state.toolCalls || 0) * 20)
|
|
102
|
+
const low = Math.max(0, Math.floor(base * 0.7))
|
|
103
|
+
const high = Math.max(low, Math.ceil(base * 1.4))
|
|
104
|
+
return { low, high }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function usagePayload(state) {
|
|
108
|
+
const est = estimateTokens(state)
|
|
109
|
+
const reportedIn = state.reportedInputTokens || 0
|
|
110
|
+
const reportedOut = state.reportedOutputTokens || 0
|
|
111
|
+
const reportedTotalDirect = state.reportedTotalTokens || 0
|
|
112
|
+
const reportedTotalDerived = reportedIn + reportedOut
|
|
113
|
+
const reportedTotal = reportedTotalDirect || reportedTotalDerived || 0
|
|
114
|
+
|
|
115
|
+
let method = "heuristic"
|
|
116
|
+
if (reportedTotal > 0 && (est.low > 0 || est.high > 0)) method = "mixed"
|
|
117
|
+
else if (reportedTotal > 0) method = "reported"
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
inputTokensReported: reportedIn || null,
|
|
121
|
+
outputTokensReported: reportedOut || null,
|
|
122
|
+
totalTokensReported: reportedTotal || null,
|
|
123
|
+
tokenEstimateLow: est.low,
|
|
124
|
+
tokenEstimateHigh: est.high,
|
|
125
|
+
method,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildCompletedSnapshot(state, sessionId) {
|
|
130
|
+
return {
|
|
131
|
+
sessionId,
|
|
132
|
+
agent: state.agentName,
|
|
133
|
+
member: state.member || null,
|
|
134
|
+
agentType: state.agentType || null,
|
|
135
|
+
team: state.teamName || null,
|
|
136
|
+
skills: Array.from(state.skills || []).sort(),
|
|
137
|
+
usage: usagePayload(state),
|
|
138
|
+
filesEdited: state.editCount || 0,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildTeamSkillsSummary(teamName) {
|
|
143
|
+
const rows = completedByTeam.get(teamName) || []
|
|
44
144
|
const byAgent = {}
|
|
45
|
-
for (const
|
|
46
|
-
|
|
47
|
-
if (!byAgent[
|
|
48
|
-
for (const
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
const key = row.member || row.agent || "unknown"
|
|
147
|
+
if (!byAgent[key]) byAgent[key] = { agentType: row.agentType || null, skills: new Set() }
|
|
148
|
+
for (const s of row.skills || []) byAgent[key].skills.add(s)
|
|
49
149
|
}
|
|
150
|
+
const out = {}
|
|
151
|
+
for (const [k, v] of Object.entries(byAgent)) {
|
|
152
|
+
out[k] = {
|
|
153
|
+
agentType: v.agentType,
|
|
154
|
+
skills: Array.from(v.skills).sort(),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return out
|
|
158
|
+
}
|
|
50
159
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
160
|
+
function trackCompletedByTeam(snapshot) {
|
|
161
|
+
if (!snapshot.team) return
|
|
162
|
+
if (!completedByTeam.has(snapshot.team)) completedByTeam.set(snapshot.team, [])
|
|
163
|
+
completedByTeam.get(snapshot.team).push(snapshot)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function enqueuePendingSpawn(leadSessionId, args) {
|
|
167
|
+
pendingSpawns.push({
|
|
168
|
+
leadSessionId,
|
|
169
|
+
at: nowMs(),
|
|
170
|
+
member: args?.name || null,
|
|
171
|
+
agentType: args?.agent || null,
|
|
172
|
+
teamName: leadTeamBySession.get(leadSessionId) || null,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function matchPendingSpawn() {
|
|
177
|
+
const now = nowMs()
|
|
178
|
+
// Drop expired pending spawns first
|
|
179
|
+
for (let i = pendingSpawns.length - 1; i >= 0; i--) {
|
|
180
|
+
if (now - pendingSpawns[i].at > SPAWN_MATCH_WINDOW_MS) pendingSpawns.splice(i, 1)
|
|
54
181
|
}
|
|
55
|
-
return
|
|
182
|
+
if (pendingSpawns.length === 0) return null
|
|
183
|
+
return pendingSpawns.shift()
|
|
56
184
|
}
|
|
57
185
|
|
|
58
|
-
// Maps ensemble tool name
|
|
186
|
+
// Maps ensemble tool name -> function that extracts log entry fields from args
|
|
59
187
|
const ENSEMBLE_TOOL_HANDLERS = {
|
|
60
|
-
team_create: (args) => ({ action: "team-created",
|
|
61
|
-
team_spawn: (args) => ({ action: "teammate-spawned",
|
|
62
|
-
team_shutdown: (args) => ({ action: "teammate-shutdown",
|
|
63
|
-
team_merge: (args) => ({ action: "teammate-merged",
|
|
64
|
-
team_cleanup: ()
|
|
65
|
-
team_status: ()
|
|
66
|
-
team_results: (args) => ({ action: "team-results-read",
|
|
67
|
-
team_message: (args) => ({ action: "team-message",
|
|
68
|
-
team_broadcast: (args) => ({ action: "team-broadcast",
|
|
69
|
-
team_tasks_add: (args) => ({ action: "tasks-added",
|
|
70
|
-
team_tasks_complete: (args) => ({ action: "task-completed",
|
|
71
|
-
team_claim: (args) => ({ action: "task-claimed",
|
|
188
|
+
team_create: (args) => ({ action: "team-created", team: args.name }),
|
|
189
|
+
team_spawn: (args) => ({ action: "teammate-spawned", name: args.name, agentType: args.agent }),
|
|
190
|
+
team_shutdown: (args) => ({ action: "teammate-shutdown", name: args.name }),
|
|
191
|
+
team_merge: (args) => ({ action: "teammate-merged", name: args.name }),
|
|
192
|
+
team_cleanup: () => ({ action: "team-cleanup" }),
|
|
193
|
+
team_status: () => ({ action: "team-status-checked" }),
|
|
194
|
+
team_results: (args) => ({ action: "team-results-read", from: args.from }),
|
|
195
|
+
team_message: (args) => ({ action: "team-message", to: args.to ?? "lead", preview: String(args.text ?? "").slice(0, 120) }),
|
|
196
|
+
team_broadcast: (args) => ({ action: "team-broadcast", preview: String(args.text ?? "").slice(0, 120) }),
|
|
197
|
+
team_tasks_add: (args) => ({ action: "tasks-added", count: Array.isArray(args.tasks) ? args.tasks.length : "?" }),
|
|
198
|
+
team_tasks_complete: (args) => ({ action: "task-completed", taskId: args.task_id }),
|
|
199
|
+
team_claim: (args) => ({ action: "task-claimed", taskId: args.task_id }),
|
|
72
200
|
}
|
|
73
201
|
|
|
74
202
|
export const SessionLogPlugin = async ({ client, directory }) => {
|
|
@@ -81,10 +209,35 @@ export const SessionLogPlugin = async ({ client, directory }) => {
|
|
|
81
209
|
|
|
82
210
|
const res = await client.session.get({ path: { id: sessionId } })
|
|
83
211
|
const session = res?.data
|
|
84
|
-
const
|
|
212
|
+
const fallbackAgent = resolveAgentName(session)
|
|
213
|
+
const spawnMatch = matchPendingSpawn()
|
|
214
|
+
|
|
215
|
+
const state = {
|
|
216
|
+
agentName: spawnMatch?.member || fallbackAgent,
|
|
217
|
+
member: spawnMatch?.member || null,
|
|
218
|
+
agentType: spawnMatch?.agentType || null,
|
|
219
|
+
teamName: spawnMatch?.teamName || null,
|
|
220
|
+
editCount: 0,
|
|
221
|
+
skills: new Set(),
|
|
222
|
+
startedAtMs: nowMs(),
|
|
223
|
+
toolCalls: 0,
|
|
224
|
+
charIn: 0,
|
|
225
|
+
charOut: 0,
|
|
226
|
+
reportedInputTokens: 0,
|
|
227
|
+
reportedOutputTokens: 0,
|
|
228
|
+
reportedTotalTokens: 0,
|
|
229
|
+
}
|
|
85
230
|
|
|
86
|
-
sessionState.set(sessionId,
|
|
87
|
-
appendEntry(directory, {
|
|
231
|
+
sessionState.set(sessionId, state)
|
|
232
|
+
appendEntry(directory, {
|
|
233
|
+
ts: ts(),
|
|
234
|
+
agent: state.agentName,
|
|
235
|
+
member: state.member,
|
|
236
|
+
agentType: state.agentType,
|
|
237
|
+
team: state.teamName,
|
|
238
|
+
action: "started",
|
|
239
|
+
sessionId,
|
|
240
|
+
})
|
|
88
241
|
}
|
|
89
242
|
|
|
90
243
|
if (event?.type === "file.edited") {
|
|
@@ -100,9 +253,21 @@ export const SessionLogPlugin = async ({ client, directory }) => {
|
|
|
100
253
|
const state = sessionState.get(sessionId)
|
|
101
254
|
if (!state) return
|
|
102
255
|
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
appendEntry(directory, {
|
|
256
|
+
const skills = Array.from(state.skills || []).sort()
|
|
257
|
+
const usage = usagePayload(state)
|
|
258
|
+
appendEntry(directory, {
|
|
259
|
+
ts: ts(),
|
|
260
|
+
agent: state.agentName,
|
|
261
|
+
member: state.member,
|
|
262
|
+
agentType: state.agentType,
|
|
263
|
+
team: state.teamName,
|
|
264
|
+
action: "completed",
|
|
265
|
+
filesEdited: state.editCount,
|
|
266
|
+
skills,
|
|
267
|
+
usage,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
trackCompletedByTeam(buildCompletedSnapshot(state, sessionId))
|
|
106
271
|
sessionState.delete(sessionId)
|
|
107
272
|
}
|
|
108
273
|
} catch (_) {}
|
|
@@ -117,13 +282,49 @@ export const SessionLogPlugin = async ({ client, directory }) => {
|
|
|
117
282
|
if (!state) return
|
|
118
283
|
|
|
119
284
|
const tool = input?.tool
|
|
285
|
+
const args = input?.args ?? {}
|
|
286
|
+
|
|
287
|
+
state.toolCalls++
|
|
288
|
+
state.charIn += JSON.stringify(args).length
|
|
289
|
+
state.charOut += JSON.stringify(output ?? {}).length
|
|
290
|
+
|
|
291
|
+
const reportedIn = extractReportedTokens(input)
|
|
292
|
+
const reportedOut = extractReportedTokens(output)
|
|
293
|
+
if (reportedIn.found) {
|
|
294
|
+
state.reportedInputTokens += reportedIn.input
|
|
295
|
+
state.reportedOutputTokens += reportedIn.output
|
|
296
|
+
state.reportedTotalTokens += reportedIn.total
|
|
297
|
+
}
|
|
298
|
+
if (reportedOut.found) {
|
|
299
|
+
state.reportedInputTokens += reportedOut.input
|
|
300
|
+
state.reportedOutputTokens += reportedOut.output
|
|
301
|
+
state.reportedTotalTokens += reportedOut.total
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (DEBUG && !reportedIn.found && !reportedOut.found && tool !== "read") {
|
|
305
|
+
appendEntry(directory, {
|
|
306
|
+
ts: ts(),
|
|
307
|
+
agent: state.agentName,
|
|
308
|
+
action: "debug-no-token-metrics",
|
|
309
|
+
tool,
|
|
310
|
+
})
|
|
311
|
+
}
|
|
120
312
|
|
|
121
313
|
// Track skill loads via skill tool (primary)
|
|
122
314
|
if (tool === "skill") {
|
|
123
315
|
const skillName = input?.args?.name
|
|
124
316
|
const added = addSkillToState(state, skillName)
|
|
125
317
|
if (added) {
|
|
126
|
-
appendEntry(directory, {
|
|
318
|
+
appendEntry(directory, {
|
|
319
|
+
ts: ts(),
|
|
320
|
+
agent: state.agentName,
|
|
321
|
+
member: state.member,
|
|
322
|
+
agentType: state.agentType,
|
|
323
|
+
team: state.teamName,
|
|
324
|
+
action: "skill-loaded",
|
|
325
|
+
skill: skillName,
|
|
326
|
+
source: "skill-tool",
|
|
327
|
+
})
|
|
127
328
|
}
|
|
128
329
|
return
|
|
129
330
|
}
|
|
@@ -136,23 +337,53 @@ export const SessionLogPlugin = async ({ client, directory }) => {
|
|
|
136
337
|
const skillName = match[1]
|
|
137
338
|
const added = addSkillToState(state, skillName)
|
|
138
339
|
if (added) {
|
|
139
|
-
appendEntry(directory, {
|
|
340
|
+
appendEntry(directory, {
|
|
341
|
+
ts: ts(),
|
|
342
|
+
agent: state.agentName,
|
|
343
|
+
member: state.member,
|
|
344
|
+
agentType: state.agentType,
|
|
345
|
+
team: state.teamName,
|
|
346
|
+
action: "skill-loaded",
|
|
347
|
+
skill: skillName,
|
|
348
|
+
source: "read-skill-file",
|
|
349
|
+
})
|
|
140
350
|
}
|
|
141
351
|
}
|
|
142
352
|
return
|
|
143
353
|
}
|
|
144
354
|
|
|
145
|
-
const args = input?.args ?? {}
|
|
146
|
-
|
|
147
355
|
// Track ensemble tool calls
|
|
148
356
|
const ensembleHandler = ENSEMBLE_TOOL_HANDLERS[tool]
|
|
149
357
|
if (!ensembleHandler) return
|
|
150
358
|
|
|
151
|
-
const entry = {
|
|
359
|
+
const entry = {
|
|
360
|
+
ts: ts(),
|
|
361
|
+
agent: state.agentName,
|
|
362
|
+
member: state.member,
|
|
363
|
+
agentType: state.agentType,
|
|
364
|
+
team: state.teamName,
|
|
365
|
+
...ensembleHandler(args),
|
|
366
|
+
}
|
|
152
367
|
appendEntry(directory, entry)
|
|
153
368
|
|
|
369
|
+
if (tool === "team_create") {
|
|
370
|
+
leadTeamBySession.set(sessionId, args?.name || null)
|
|
371
|
+
state.teamName = args?.name || state.teamName
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (tool === "team_spawn") {
|
|
375
|
+
enqueuePendingSpawn(sessionId, args)
|
|
376
|
+
}
|
|
377
|
+
|
|
154
378
|
if (tool === "team_cleanup") {
|
|
155
|
-
|
|
379
|
+
const teamName = state.teamName || leadTeamBySession.get(sessionId)
|
|
380
|
+
appendEntry(directory, {
|
|
381
|
+
ts: ts(),
|
|
382
|
+
agent: state.agentName,
|
|
383
|
+
action: "team-skills-summary",
|
|
384
|
+
team: teamName || null,
|
|
385
|
+
byAgent: teamName ? buildTeamSkillsSummary(teamName) : {},
|
|
386
|
+
})
|
|
156
387
|
}
|
|
157
388
|
} catch (_) {}
|
|
158
389
|
},
|