mio-agent-runtime 0.3.3 → 0.3.4
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/observe/observer.js +215 -1
- package/package.json +1 -1
package/observe/observer.js
CHANGED
|
@@ -75,10 +75,22 @@ function loadState(home) {
|
|
|
75
75
|
buffers: parsed.buffers || {},
|
|
76
76
|
openTurns: parsed.openTurns || {},
|
|
77
77
|
contexts: parsed.contexts || {},
|
|
78
|
+
codexFiles: parsed.codexFiles || {},
|
|
79
|
+
codexBuffers: parsed.codexBuffers || {},
|
|
80
|
+
codexTurns: parsed.codexTurns || {},
|
|
78
81
|
}
|
|
79
82
|
}
|
|
80
83
|
} catch (_) {}
|
|
81
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
version: 1,
|
|
86
|
+
files: {},
|
|
87
|
+
buffers: {},
|
|
88
|
+
openTurns: {},
|
|
89
|
+
contexts: {},
|
|
90
|
+
codexFiles: {},
|
|
91
|
+
codexBuffers: {},
|
|
92
|
+
codexTurns: {},
|
|
93
|
+
}
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
function saveState(home, state) {
|
|
@@ -109,6 +121,31 @@ function listTranscripts() {
|
|
|
109
121
|
return out
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
function codexConfigDir() {
|
|
125
|
+
return process.env.CODEX_CONFIG_DIR || path.join(os.homedir(), '.codex')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function listCodexTranscripts() {
|
|
129
|
+
const root = path.join(codexConfigDir(), 'sessions')
|
|
130
|
+
const out = []
|
|
131
|
+
if (!fs.existsSync(root)) return out
|
|
132
|
+
const walk = (dir) => {
|
|
133
|
+
let entries
|
|
134
|
+
try {
|
|
135
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
136
|
+
} catch (_) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const full = path.join(dir, entry.name)
|
|
141
|
+
if (entry.isDirectory()) walk(full)
|
|
142
|
+
else if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) out.push(full)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
walk(root)
|
|
146
|
+
return out
|
|
147
|
+
}
|
|
148
|
+
|
|
112
149
|
function fileMeta(file) {
|
|
113
150
|
const dirName = path.basename(path.dirname(file))
|
|
114
151
|
return {
|
|
@@ -343,6 +380,178 @@ function emitWorkbuddyOutcome(turn, sinks, state) {
|
|
|
343
380
|
return true
|
|
344
381
|
}
|
|
345
382
|
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Codex session turn machine (passive observation)
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
// Codex rollout transcripts are append-only JSONL under
|
|
388
|
+
// ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl. The first session_meta line
|
|
389
|
+
// carries the workspace cwd; task_started / task_complete / turn_aborted mark
|
|
390
|
+
// turn boundaries, agent_message carries the running answer text.
|
|
391
|
+
function createCodexTurn() {
|
|
392
|
+
return {
|
|
393
|
+
id: null,
|
|
394
|
+
ts: null,
|
|
395
|
+
sessionId: null,
|
|
396
|
+
cwd: null,
|
|
397
|
+
lastAgent: null,
|
|
398
|
+
aborted: false,
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function feedCodexLine(turn, obj) {
|
|
403
|
+
if (!obj || typeof obj !== 'object') return {}
|
|
404
|
+
if (obj.type === 'session_meta') {
|
|
405
|
+
const p = obj.payload || {}
|
|
406
|
+
if (!turn.cwd && p.cwd) turn.cwd = p.cwd
|
|
407
|
+
if (!turn.sessionId && (p.session_id || p.id)) turn.sessionId = p.session_id || p.id
|
|
408
|
+
return {}
|
|
409
|
+
}
|
|
410
|
+
if (obj.type !== 'event_msg') return {}
|
|
411
|
+
const p = obj.payload || {}
|
|
412
|
+
if (p.type === 'task_started') {
|
|
413
|
+
// A turn that started earlier but never completed is discarded.
|
|
414
|
+
turn.id = p.turn_id || null
|
|
415
|
+
turn.ts = p.started_at ? Number(p.started_at) * 1000 : null
|
|
416
|
+
turn.lastAgent = null
|
|
417
|
+
turn.aborted = false
|
|
418
|
+
return {}
|
|
419
|
+
}
|
|
420
|
+
if (p.type === 'agent_message' && p.message) {
|
|
421
|
+
turn.lastAgent = p.message
|
|
422
|
+
return {}
|
|
423
|
+
}
|
|
424
|
+
if (p.type === 'task_complete') {
|
|
425
|
+
const finalized = {
|
|
426
|
+
id: turn.id,
|
|
427
|
+
ts: p.completed_at ? Number(p.completed_at) * 1000 : turn.ts,
|
|
428
|
+
sessionId: turn.sessionId,
|
|
429
|
+
cwd: turn.cwd,
|
|
430
|
+
project: path.basename(turn.cwd || '') || 'unknown',
|
|
431
|
+
lastAgent: p.last_agent_message || turn.lastAgent,
|
|
432
|
+
aborted: false,
|
|
433
|
+
}
|
|
434
|
+
turn.id = null
|
|
435
|
+
turn.lastAgent = null
|
|
436
|
+
return { finalized: finalized }
|
|
437
|
+
}
|
|
438
|
+
if (p.type === 'turn_aborted') {
|
|
439
|
+
const finalized = {
|
|
440
|
+
id: turn.id,
|
|
441
|
+
ts: p.completed_at ? Number(p.completed_at) * 1000 : turn.ts,
|
|
442
|
+
sessionId: turn.sessionId,
|
|
443
|
+
cwd: turn.cwd,
|
|
444
|
+
project: path.basename(turn.cwd || '') || 'unknown',
|
|
445
|
+
lastAgent: turn.lastAgent,
|
|
446
|
+
aborted: true,
|
|
447
|
+
}
|
|
448
|
+
turn.id = null
|
|
449
|
+
turn.lastAgent = null
|
|
450
|
+
return { finalized: finalized }
|
|
451
|
+
}
|
|
452
|
+
return {}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function emitCodexOutcome(turn, sinks, state) {
|
|
456
|
+
if (!turn.lastAgent) return false
|
|
457
|
+
const summary = cleanText(turn.lastAgent)
|
|
458
|
+
if (!summary) return false
|
|
459
|
+
const outcome = turn.aborted ? 'aborted' : 'success'
|
|
460
|
+
const turnToken = String(turn.id || 'unknown').slice(0, 8)
|
|
461
|
+
|
|
462
|
+
sinks.trace({
|
|
463
|
+
trace_id: 'codex:' + (turn.sessionId || 'unknown') + ':' + turnToken,
|
|
464
|
+
event_type: 'task_outcome',
|
|
465
|
+
outcome: outcome,
|
|
466
|
+
payload: {
|
|
467
|
+
host: 'codex',
|
|
468
|
+
project: turn.project,
|
|
469
|
+
sessionId: turn.sessionId,
|
|
470
|
+
turnId: turn.id,
|
|
471
|
+
toolCalls: 0,
|
|
472
|
+
errors: 0,
|
|
473
|
+
summary: summary.slice(0, 300),
|
|
474
|
+
source: 'codex-observer',
|
|
475
|
+
},
|
|
476
|
+
agent: 'codex',
|
|
477
|
+
host: 'codex',
|
|
478
|
+
project: turn.project,
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
if (outcome === 'success' && summary.length >= 10) {
|
|
482
|
+
sinks.memory({
|
|
483
|
+
kind: 'note',
|
|
484
|
+
content: '[codex] 任务完成: ' + summary.slice(0, 160),
|
|
485
|
+
tags: ['task-outcome', 'codex', turn.project],
|
|
486
|
+
project: turn.project,
|
|
487
|
+
source: 'codex-observer',
|
|
488
|
+
})
|
|
489
|
+
var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
|
|
490
|
+
updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80))
|
|
491
|
+
}
|
|
492
|
+
return true
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function runCodexCycle(home, state, sinks, opts) {
|
|
496
|
+
const out = { files: 0, lines: 0, outcomes: 0 }
|
|
497
|
+
const files = listCodexTranscripts()
|
|
498
|
+
const now = Date.now()
|
|
499
|
+
const maxAgeMs = (opts && opts.maxAgeMs) || 24 * 60 * 60 * 1000
|
|
500
|
+
|
|
501
|
+
for (const file of files) {
|
|
502
|
+
out.files += 1
|
|
503
|
+
let st
|
|
504
|
+
try {
|
|
505
|
+
st = fs.statSync(file)
|
|
506
|
+
} catch (_) {
|
|
507
|
+
continue
|
|
508
|
+
}
|
|
509
|
+
const info = state.codexFiles[file] || { size: 0 }
|
|
510
|
+
if (!state.codexFiles[file] && now - st.mtimeMs > maxAgeMs) continue
|
|
511
|
+
const size = st.size
|
|
512
|
+
if (size < info.size) {
|
|
513
|
+
info.size = 0
|
|
514
|
+
state.codexBuffers[file] = ''
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (!state.codexTurns[file]) state.codexTurns[file] = createCodexTurn()
|
|
518
|
+
const turn = state.codexTurns[file]
|
|
519
|
+
|
|
520
|
+
if (size > info.size) {
|
|
521
|
+
let chunk = ''
|
|
522
|
+
try {
|
|
523
|
+
const fd = fs.openSync(file, 'r')
|
|
524
|
+
const buf = Buffer.alloc(size - info.size)
|
|
525
|
+
fs.readSync(fd, buf, 0, buf.length, info.size)
|
|
526
|
+
fs.closeSync(fd)
|
|
527
|
+
chunk = buf.toString('utf8')
|
|
528
|
+
} catch (_) {
|
|
529
|
+
continue
|
|
530
|
+
}
|
|
531
|
+
const pending = (state.codexBuffers[file] || '') + chunk
|
|
532
|
+
const lines = pending.split(/\r?\n/)
|
|
533
|
+
state.codexBuffers[file] = lines.pop()
|
|
534
|
+
for (const line of lines) {
|
|
535
|
+
if (!line.trim()) continue
|
|
536
|
+
out.lines += 1
|
|
537
|
+
let obj
|
|
538
|
+
try {
|
|
539
|
+
obj = JSON.parse(line)
|
|
540
|
+
} catch (_) {
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
const result = feedCodexLine(turn, obj)
|
|
544
|
+
if (result.finalized) {
|
|
545
|
+
if (emitCodexOutcome(result.finalized, sinks, state)) out.outcomes += 1
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
state.codexFiles[file] = { size: size, mtimeMs: st.mtimeMs }
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return out
|
|
553
|
+
}
|
|
554
|
+
|
|
346
555
|
// ---------------------------------------------------------------------------
|
|
347
556
|
// cycle
|
|
348
557
|
// ---------------------------------------------------------------------------
|
|
@@ -420,6 +629,11 @@ function runCycle(home, state, sinks, opts) {
|
|
|
420
629
|
}
|
|
421
630
|
}
|
|
422
631
|
|
|
632
|
+
const codexRes = runCodexCycle(home, state, sinks, opts)
|
|
633
|
+
out.files += codexRes.files
|
|
634
|
+
out.lines += codexRes.lines
|
|
635
|
+
out.outcomes += codexRes.outcomes
|
|
636
|
+
|
|
423
637
|
saveState(home, state)
|
|
424
638
|
return out
|
|
425
639
|
}
|