@sebastianandreasson/pi-autonomous-agents 0.9.0 → 0.9.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/package.json +1 -1
- package/src/pi-client.mjs +14 -2
- package/src/pi-visualizer-server.mjs +145 -96
package/package.json
CHANGED
package/src/pi-client.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { randomUUID } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
const liveFeedWriteQueues = new Map()
|
|
4
6
|
import {
|
|
5
7
|
appendLog,
|
|
6
8
|
writeTextFile,
|
|
@@ -43,8 +45,18 @@ async function appendLiveFeedEvent(config, event) {
|
|
|
43
45
|
if (!config.runLiveFeedFile) {
|
|
44
46
|
return
|
|
45
47
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
|
|
49
|
+
const filePath = config.runLiveFeedFile
|
|
50
|
+
const previous = liveFeedWriteQueues.get(filePath) ?? Promise.resolve()
|
|
51
|
+
const next = previous
|
|
52
|
+
.catch(() => {})
|
|
53
|
+
.then(async () => {
|
|
54
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
55
|
+
await fs.appendFile(filePath, `${JSON.stringify(event)}\n`, 'utf8')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
liveFeedWriteQueues.set(filePath, next)
|
|
59
|
+
await next
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
async function runMockTurn({ config, sessionId, sessionFile, prompt, reason }) {
|
|
@@ -36,8 +36,9 @@ async function readJsonlTail(filePath, maxItems = 200) {
|
|
|
36
36
|
.split('\n')
|
|
37
37
|
.map((line) => line.trim())
|
|
38
38
|
.filter(Boolean)
|
|
39
|
-
.slice(-maxItems)
|
|
40
39
|
.map((line) => JSON.parse(line))
|
|
40
|
+
.sort((left, right) => String(left?.timestamp ?? '').localeCompare(String(right?.timestamp ?? '')))
|
|
41
|
+
.slice(-maxItems)
|
|
41
42
|
} catch {
|
|
42
43
|
return []
|
|
43
44
|
}
|
|
@@ -78,51 +79,27 @@ async function parseTodos(taskFile, activeTaskText = '') {
|
|
|
78
79
|
if (level === 2) {
|
|
79
80
|
currentPhase = text
|
|
80
81
|
}
|
|
81
|
-
items.push({
|
|
82
|
-
id: `line-${index + 1}`,
|
|
83
|
-
kind: 'heading',
|
|
84
|
-
lineNumber: index + 1,
|
|
85
|
-
level,
|
|
86
|
-
text,
|
|
87
|
-
phase: currentPhase || text,
|
|
88
|
-
raw: line,
|
|
89
|
-
checked: false,
|
|
90
|
-
active: false,
|
|
91
|
-
})
|
|
92
82
|
continue
|
|
93
83
|
}
|
|
94
84
|
|
|
95
85
|
const checkboxMatch = /^\s*[-*]\s+\[( |x)\]\s+(.+)$/.exec(line)
|
|
96
|
-
if (checkboxMatch) {
|
|
97
|
-
const checked = checkboxMatch[1].toLowerCase() === 'x'
|
|
98
|
-
const text = checkboxMatch[2].trim()
|
|
99
|
-
items.push({
|
|
100
|
-
id: `line-${index + 1}`,
|
|
101
|
-
kind: 'task',
|
|
102
|
-
lineNumber: index + 1,
|
|
103
|
-
level: 0,
|
|
104
|
-
text,
|
|
105
|
-
phase: currentPhase,
|
|
106
|
-
raw: line,
|
|
107
|
-
checked,
|
|
108
|
-
active: text === activeTaskText,
|
|
109
|
-
})
|
|
86
|
+
if (!checkboxMatch) {
|
|
110
87
|
continue
|
|
111
88
|
}
|
|
112
89
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
90
|
+
const checked = checkboxMatch[1].toLowerCase() === 'x'
|
|
91
|
+
const text = checkboxMatch[2].trim()
|
|
92
|
+
items.push({
|
|
93
|
+
id: `line-${index + 1}`,
|
|
94
|
+
kind: 'task',
|
|
95
|
+
lineNumber: index + 1,
|
|
96
|
+
level: 0,
|
|
97
|
+
text,
|
|
98
|
+
phase: currentPhase,
|
|
99
|
+
raw: line,
|
|
100
|
+
checked,
|
|
101
|
+
active: text === activeTaskText,
|
|
102
|
+
})
|
|
126
103
|
}
|
|
127
104
|
|
|
128
105
|
return items
|
|
@@ -532,6 +509,19 @@ export function renderHtml() {
|
|
|
532
509
|
let selectedEventId = ''
|
|
533
510
|
let selectedTodoId = ''
|
|
534
511
|
let eventSource = null
|
|
512
|
+
const renderCache = {
|
|
513
|
+
runsKey: '',
|
|
514
|
+
todosKey: '',
|
|
515
|
+
focusKey: '',
|
|
516
|
+
editsKey: '',
|
|
517
|
+
flowKey: '',
|
|
518
|
+
graphKey: '',
|
|
519
|
+
runStateKey: '',
|
|
520
|
+
summaryKey: '',
|
|
521
|
+
outputKey: '',
|
|
522
|
+
feedKey: '',
|
|
523
|
+
timelineKey: '',
|
|
524
|
+
}
|
|
535
525
|
|
|
536
526
|
function normalizeFeedEntry(entry) {
|
|
537
527
|
return {
|
|
@@ -613,17 +603,21 @@ export function renderHtml() {
|
|
|
613
603
|
if (selected && !selectedTodoId) {
|
|
614
604
|
selectedTodoId = selected.id
|
|
615
605
|
}
|
|
606
|
+
const nextKey = JSON.stringify([todos, selected?.id || ''])
|
|
607
|
+
if (renderCache.todosKey === nextKey) {
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
renderCache.todosKey = nextKey
|
|
616
611
|
const list = document.getElementById('todo-list')
|
|
617
612
|
list.innerHTML = todos.length > 0
|
|
618
613
|
? todos.map((item) => {
|
|
619
614
|
const active = selected && item.id === selected.id
|
|
620
|
-
const
|
|
621
|
-
const checkedMark = item.kind === 'task' ? (item.checked ? '✓' : '○') : (item.kind === 'heading' ? '#' : '·')
|
|
615
|
+
const checkedMark = item.checked ? '✓' : '○'
|
|
622
616
|
const checkedClass = item.checked ? 'todo-checked' : ''
|
|
623
617
|
return '<details class="todo-item ' + (active ? 'active' : '') + '" ' + (active ? 'open' : '') + ' data-todo-id="' + esc(item.id) + '">' +
|
|
624
618
|
'<summary class="todo-summary">' +
|
|
625
619
|
'<div class="todo-line">' + esc(String(item.lineNumber)) + '</div>' +
|
|
626
|
-
'<div class="
|
|
620
|
+
'<div class="todo-task ' + checkedClass + '">' + esc(checkedMark + ' ' + item.text) + '</div>' +
|
|
627
621
|
'</summary>' +
|
|
628
622
|
'<div class="todo-open-body">' + esc(item.phase || '') + '</div>' +
|
|
629
623
|
'</details>'
|
|
@@ -634,6 +628,8 @@ export function renderHtml() {
|
|
|
634
628
|
element.addEventListener('toggle', () => {
|
|
635
629
|
if (element.open) {
|
|
636
630
|
selectedTodoId = element.getAttribute('data-todo-id') || ''
|
|
631
|
+
renderCache.todosKey = ''
|
|
632
|
+
renderCache.focusKey = ''
|
|
637
633
|
renderSnapshot(snapshot)
|
|
638
634
|
}
|
|
639
635
|
})
|
|
@@ -642,19 +638,36 @@ export function renderHtml() {
|
|
|
642
638
|
|
|
643
639
|
function renderFocusedTodo(snapshot) {
|
|
644
640
|
const todo = findSelectedTodo(snapshot)
|
|
641
|
+
const nextKey = JSON.stringify({
|
|
642
|
+
todoId: todo?.id || '',
|
|
643
|
+
activeLabel: snapshot?.flow?.activeLabel || '',
|
|
644
|
+
iteration: snapshot?.flow?.iteration || '',
|
|
645
|
+
phase: todo?.phase || snapshot?.summary?.phase || '',
|
|
646
|
+
checked: todo?.checked === true,
|
|
647
|
+
active: todo?.active === true,
|
|
648
|
+
})
|
|
649
|
+
if (renderCache.focusKey === nextKey) {
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
renderCache.focusKey = nextKey
|
|
645
653
|
document.getElementById('todo-focus-title').textContent = todo ? todo.text : 'No todo selected.'
|
|
646
654
|
const stateBar = document.getElementById('todo-state-bar')
|
|
647
655
|
const chips = [
|
|
648
656
|
['Current activity', snapshot?.flow?.activeLabel || 'Idle'],
|
|
649
657
|
['Iteration', snapshot?.flow?.iteration || '—'],
|
|
650
658
|
['Phase', todo?.phase || snapshot?.summary?.phase || '—'],
|
|
651
|
-
['Task status', todo
|
|
659
|
+
['Task status', todo ? (todo.checked ? 'Done' : (todo.active ? 'Active' : 'Pending')) : 'Info'],
|
|
652
660
|
]
|
|
653
661
|
stateBar.innerHTML = chips.map(([label, value]) => '<div class="state-chip">' + esc(label + ': ' + value) + '</div>').join('')
|
|
654
662
|
}
|
|
655
663
|
|
|
656
664
|
function renderCurrentEdits(snapshot) {
|
|
657
665
|
const edits = Array.isArray(snapshot?.currentEdits) ? snapshot.currentEdits : []
|
|
666
|
+
const nextKey = JSON.stringify(edits)
|
|
667
|
+
if (renderCache.editsKey === nextKey) {
|
|
668
|
+
return
|
|
669
|
+
}
|
|
670
|
+
renderCache.editsKey = nextKey
|
|
658
671
|
const target = document.getElementById('edit-list')
|
|
659
672
|
target.innerHTML = edits.length > 0
|
|
660
673
|
? edits.map((entry) => '<details class="edit-item" open><summary class="edit-head">' + esc(entry.file) + '</summary><pre>' + esc(entry.diff || 'No diff available.') + '</pre></details>').join('')
|
|
@@ -692,11 +705,15 @@ export function renderHtml() {
|
|
|
692
705
|
|
|
693
706
|
const select = document.getElementById('run-select')
|
|
694
707
|
const selected = data.config.selectedRunId || ''
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
708
|
+
const runsKey = JSON.stringify([selected, data.runs])
|
|
709
|
+
if (renderCache.runsKey !== runsKey) {
|
|
710
|
+
renderCache.runsKey = runsKey
|
|
711
|
+
select.innerHTML = data.runs.map((run) => {
|
|
712
|
+
const suffix = [run.status, run.phase].filter(Boolean).join(' · ')
|
|
713
|
+
return '<option value="' + esc(run.runId) + '" ' + (selected === run.runId ? 'selected' : '') + '>' +
|
|
714
|
+
esc(run.runId.slice(0, 8) + (suffix ? ' — ' + suffix : '')) + '</option>'
|
|
715
|
+
}).join('')
|
|
716
|
+
}
|
|
700
717
|
if (!select.dataset.bound) {
|
|
701
718
|
select.addEventListener('change', (event) => {
|
|
702
719
|
updateRunQuery(event.target.value)
|
|
@@ -710,28 +727,36 @@ export function renderHtml() {
|
|
|
710
727
|
renderCurrentEdits(data)
|
|
711
728
|
|
|
712
729
|
const flowEl = document.getElementById('flow')
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
'<div class="step
|
|
720
|
-
|
|
721
|
-
|
|
730
|
+
const flowKey = JSON.stringify(data.flow.steps)
|
|
731
|
+
if (renderCache.flowKey !== flowKey) {
|
|
732
|
+
renderCache.flowKey = flowKey
|
|
733
|
+
flowEl.innerHTML = data.flow.steps.map((step) => {
|
|
734
|
+
const latest = step.latestEvent
|
|
735
|
+
const meta = latest ? [latest.kind, latest.status, latest.terminalReason].filter(Boolean).join('\\n') : 'waiting'
|
|
736
|
+
return '<div class="step ' + esc(step.status) + '">' +
|
|
737
|
+
'<div class="step-name">' + esc(step.label) + '</div>' +
|
|
738
|
+
'<div class="step-status">' + esc(step.status) + '</div>' +
|
|
739
|
+
'<div class="step-meta">' + esc(meta) + '</div>' +
|
|
740
|
+
'</div>'
|
|
741
|
+
}).join('')
|
|
742
|
+
}
|
|
722
743
|
|
|
723
744
|
const graphEl = document.getElementById('graph')
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
'<
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
745
|
+
const graphKey = JSON.stringify(data.graph.nodes)
|
|
746
|
+
if (renderCache.graphKey !== graphKey) {
|
|
747
|
+
renderCache.graphKey = graphKey
|
|
748
|
+
graphEl.innerHTML = data.graph.nodes.length > 0
|
|
749
|
+
? data.graph.nodes.map((node) => {
|
|
750
|
+
const retry = node.retryCount > 0 ? 'retry #' + node.retryCount : ''
|
|
751
|
+
const meta = [node.kind, retry, node.role, node.terminalReason].filter(Boolean).join('\\n')
|
|
752
|
+
return '<button type="button" class="graph-node ' + esc(node.status) + '" data-event-id="' + esc(node.id) + '">' +
|
|
753
|
+
'<div class="step-name">' + esc(node.label) + '</div>' +
|
|
754
|
+
'<div class="step-status">' + esc(node.status) + '</div>' +
|
|
755
|
+
'<div class="step-meta">' + esc(meta) + '\\n' + esc(node.notes || '') + '</div>' +
|
|
756
|
+
'</button>'
|
|
757
|
+
}).join('')
|
|
758
|
+
: '<div class="muted">No iteration graph yet.</div>'
|
|
759
|
+
}
|
|
735
760
|
|
|
736
761
|
const runState = [
|
|
737
762
|
['runId', data.activeRun?.runId || data.state?.runId || data.config.selectedRunId || '—'],
|
|
@@ -743,40 +768,64 @@ export function renderHtml() {
|
|
|
743
768
|
['lastStatus', data.activeRun?.lastStatus || data.state?.lastStatus || '—'],
|
|
744
769
|
['lastCompleted', data.activeRun?.lastCompletedIteration || '—'],
|
|
745
770
|
]
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
771
|
+
const runStateKey = JSON.stringify(runState)
|
|
772
|
+
if (renderCache.runStateKey !== runStateKey) {
|
|
773
|
+
renderCache.runStateKey = runStateKey
|
|
774
|
+
document.getElementById('run-state').innerHTML = runState.map(([k, v]) => '<div>' + esc(k) + '</div><div>' + esc(v) + '</div>').join('')
|
|
775
|
+
}
|
|
776
|
+
const summaryText = data.summary ? JSON.stringify(data.summary, null, 2) : 'No iteration summary yet.'
|
|
777
|
+
if (renderCache.summaryKey !== summaryText) {
|
|
778
|
+
renderCache.summaryKey = summaryText
|
|
779
|
+
document.getElementById('summary').textContent = summaryText
|
|
780
|
+
}
|
|
781
|
+
const outputText = data.lastOutput || 'No agent output yet.'
|
|
782
|
+
if (renderCache.outputKey !== outputText) {
|
|
783
|
+
renderCache.outputKey = outputText
|
|
784
|
+
document.getElementById('output').textContent = outputText
|
|
785
|
+
}
|
|
749
786
|
|
|
750
787
|
renderPinnedTool(data)
|
|
751
788
|
const visibleFeed = getVisibleFeedEntries(data)
|
|
789
|
+
const feedKey = JSON.stringify(visibleFeed)
|
|
752
790
|
const feedEl = document.getElementById('feed')
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
'
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
791
|
+
if (renderCache.feedKey !== feedKey) {
|
|
792
|
+
renderCache.feedKey = feedKey
|
|
793
|
+
const distanceFromBottom = feedEl.scrollHeight - feedEl.scrollTop - feedEl.clientHeight
|
|
794
|
+
const stickToBottom = distanceFromBottom < 40
|
|
795
|
+
feedEl.innerHTML = visibleFeed.length > 0
|
|
796
|
+
? visibleFeed.map((entry) => {
|
|
797
|
+
const meta = [entry.role, entry.kind, entry.toolName].filter(Boolean).join(' · ')
|
|
798
|
+
return '<div class="feed-item">' +
|
|
799
|
+
'<div class="feed-head">' +
|
|
800
|
+
'<div class="feed-type ' + esc(entry.type) + '">' + esc(entry.type || 'event') + '</div>' +
|
|
801
|
+
(entry.count > 1 ? '<div class="feed-count">x' + esc(entry.count) + '</div>' : '') +
|
|
802
|
+
'</div>' +
|
|
803
|
+
'<div class="feed-meta">' + esc(new Date(entry.timestamp).toLocaleTimeString()) + (meta ? ' · ' + esc(meta) : '') + '</div>' +
|
|
804
|
+
'<div class="feed-text">' + esc(entry.text || '') + '</div>' +
|
|
805
|
+
'</div>'
|
|
806
|
+
}).join('')
|
|
807
|
+
: '<div class="muted">No live feed yet.</div>'
|
|
808
|
+
if (stickToBottom) {
|
|
809
|
+
feedEl.scrollTop = feedEl.scrollHeight
|
|
810
|
+
}
|
|
811
|
+
}
|
|
767
812
|
|
|
768
813
|
const timelineEvents = [...data.recentTelemetry].reverse()
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
'<
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
814
|
+
const timelineKey = JSON.stringify(timelineEvents)
|
|
815
|
+
if (renderCache.timelineKey !== timelineKey) {
|
|
816
|
+
renderCache.timelineKey = timelineKey
|
|
817
|
+
const timeline = timelineEvents.map((event) => {
|
|
818
|
+
const status = eventStatus(event.status)
|
|
819
|
+
return '<tr data-event-id="' + esc(event._vizId) + '" style="cursor:pointer;">' +
|
|
820
|
+
'<td>' + esc(new Date(event.timestamp).toLocaleTimeString()) + '</td>' +
|
|
821
|
+
'<td>' + esc(event.iteration) + '</td>' +
|
|
822
|
+
'<td>' + esc(event.kind) + '</td>' +
|
|
823
|
+
'<td><span class="' + pillClass(status) + '">' + esc(event.status) + '</span></td>' +
|
|
824
|
+
'<td class="muted">' + esc(event.notes || '') + '</td>' +
|
|
825
|
+
'</tr>'
|
|
826
|
+
}).join('')
|
|
827
|
+
document.getElementById('timeline').innerHTML = timeline || '<tr><td colspan="5" class="muted">No telemetry yet.</td></tr>'
|
|
828
|
+
}
|
|
780
829
|
|
|
781
830
|
bindSelectableEvents()
|
|
782
831
|
renderSelectedEvent()
|