picocode-core 0.9.119
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/README.md +15 -0
- package/package.json +33 -0
- package/src/agent-transcript.js +80 -0
- package/src/agent.js +170 -0
- package/src/agents.js +213 -0
- package/src/attachments.js +141 -0
- package/src/boot.js +28 -0
- package/src/catalog-snapshot.json +1 -0
- package/src/catalog.js +86 -0
- package/src/codex-models.js +56 -0
- package/src/commands.js +61 -0
- package/src/compaction.js +82 -0
- package/src/completion.js +21 -0
- package/src/config.js +32 -0
- package/src/context.js +82 -0
- package/src/controller.js +1263 -0
- package/src/conversation-search.js +101 -0
- package/src/deliberation-history.js +65 -0
- package/src/deliberation.js +61 -0
- package/src/derive.js +307 -0
- package/src/events.js +54 -0
- package/src/export.js +16 -0
- package/src/files.js +39 -0
- package/src/format.js +6 -0
- package/src/fuzzy.js +41 -0
- package/src/git.js +156 -0
- package/src/history.js +51 -0
- package/src/init.js +25 -0
- package/src/keys.js +26 -0
- package/src/mcp.js +280 -0
- package/src/memory.js +120 -0
- package/src/models.js +14 -0
- package/src/openai-auth.js +204 -0
- package/src/paths.js +67 -0
- package/src/reversible-edit.js +79 -0
- package/src/rewind.js +84 -0
- package/src/session-index.js +264 -0
- package/src/session-lock.js +27 -0
- package/src/session.js +160 -0
- package/src/shells.js +166 -0
- package/src/skills.js +164 -0
- package/src/steer.js +129 -0
- package/src/system-prompt.js +49 -0
- package/src/terminal-theme.js +49 -0
- package/src/tools/bash.js +184 -0
- package/src/tools/diff.js +18 -0
- package/src/tools/edit.js +95 -0
- package/src/tools/glob.js +43 -0
- package/src/tools/grep.js +59 -0
- package/src/tools/index.js +296 -0
- package/src/tools/read.js +49 -0
- package/src/tools/recorder.js +74 -0
- package/src/tools/web.js +84 -0
- package/src/tools/write.js +48 -0
- package/src/update.js +82 -0
- package/src/user-tools.js +59 -0
- package/src/wakeups.js +40 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
function escapeRegex(value) {
|
|
2
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function visibleLines(text, limit) {
|
|
6
|
+
return String(text).split('\n').slice(0, limit).join('\n')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function matchOffsets(text, query) {
|
|
10
|
+
if (!query) return []
|
|
11
|
+
const matches = []
|
|
12
|
+
const pattern = new RegExp(escapeRegex(query), 'gi')
|
|
13
|
+
let match
|
|
14
|
+
while ((match = pattern.exec(String(text))) !== null) matches.push(match.index)
|
|
15
|
+
return matches
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function highlightMatches(text, query, currentIndex = -1, startIndex = 0) {
|
|
19
|
+
if (!query) return text
|
|
20
|
+
const pattern = new RegExp(escapeRegex(query), 'gi')
|
|
21
|
+
const parts = String(text).split(/(\x1b\[[0-9;]*m)/)
|
|
22
|
+
let index = startIndex
|
|
23
|
+
return parts.map((part) => {
|
|
24
|
+
if (part.startsWith('\x1b[')) return part
|
|
25
|
+
return part.replace(pattern, (match) => {
|
|
26
|
+
const current = index++ === currentIndex
|
|
27
|
+
return current
|
|
28
|
+
? `\x1b[7;33m${match}\x1b[27;39m`
|
|
29
|
+
: `\x1b[7m${match}\x1b[27m`
|
|
30
|
+
})
|
|
31
|
+
}).join('')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function searchableFields(item, verbose) {
|
|
35
|
+
if (item.kind === 'tool-group') return item.tools.flatMap((tool) => searchableFields(tool, verbose))
|
|
36
|
+
if (item.kind === 'agent-notice-group') {
|
|
37
|
+
return verbose ? item.notices.map((notice, index) => ({ field: 'text', text: notice.text, lineOffset: 3 + index })) : []
|
|
38
|
+
}
|
|
39
|
+
if (item.kind === 'thoughts') {
|
|
40
|
+
return verbose && item.text ? [{ field: 'text', text: visibleLines(item.text, 300), lineOffset: 3 }] : []
|
|
41
|
+
}
|
|
42
|
+
if (item.kind === 'summary') {
|
|
43
|
+
const fields = [{ field: 'text', text: visibleLines(item.text, verbose ? 500 : 0), lineOffset: 3 }]
|
|
44
|
+
return verbose ? fields : []
|
|
45
|
+
}
|
|
46
|
+
if (item.kind === 'tool') {
|
|
47
|
+
const fields = [{ field: 'title', text: item.title || item.name || '', lineOffset: 1 }]
|
|
48
|
+
if (verbose && item.fullOutput) fields.push({ field: 'fullOutput', text: visibleLines(item.fullOutput, 200), lineOffset: 4 })
|
|
49
|
+
return fields
|
|
50
|
+
}
|
|
51
|
+
return item.text ? [{ field: 'text', text: item.text, lineOffset: 1 }] : []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function conversationMatches(items, query, verbose = false) {
|
|
55
|
+
const matches = []
|
|
56
|
+
items.forEach((item, itemIndex) => {
|
|
57
|
+
searchableFields(item, verbose).forEach(({ field, text, lineOffset }, fieldIndex) => {
|
|
58
|
+
matchOffsets(text, query).forEach((offset, occurrenceIndex) => {
|
|
59
|
+
const line = lineOffset + String(text).slice(0, offset).split('\n').length - 1
|
|
60
|
+
matches.push({ itemIndex, field, fieldIndex, offset, line, occurrenceIndex })
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
return matches
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function highlightConversation(items, query, currentIndex, verbose = false) {
|
|
68
|
+
let startIndex = 0
|
|
69
|
+
function decorate(item) {
|
|
70
|
+
if (item.kind === 'tool-group') return { ...item, tools: item.tools.map(decorate) }
|
|
71
|
+
if (item.kind === 'agent-notice-group') {
|
|
72
|
+
if (!verbose) return item
|
|
73
|
+
return { ...item, notices: item.notices.map(decorate) }
|
|
74
|
+
}
|
|
75
|
+
if (item.kind === 'thoughts' && !verbose) return item
|
|
76
|
+
if (item.kind === 'summary' && !verbose) return item
|
|
77
|
+
if (item.kind === 'tool') {
|
|
78
|
+
const decorated = { ...item }
|
|
79
|
+
const title = item.title || item.name || ''
|
|
80
|
+
const titleCount = matchOffsets(title, query).length
|
|
81
|
+
decorated.title = highlightMatches(title, query, currentIndex, startIndex)
|
|
82
|
+
startIndex += titleCount
|
|
83
|
+
if (verbose && item.fullOutput) {
|
|
84
|
+
const visibleOutput = visibleLines(item.fullOutput, 200)
|
|
85
|
+
const outputCount = matchOffsets(visibleOutput, query).length
|
|
86
|
+
decorated.fullOutput = highlightMatches(item.fullOutput, query, currentIndex, startIndex)
|
|
87
|
+
startIndex += outputCount
|
|
88
|
+
}
|
|
89
|
+
return decorated
|
|
90
|
+
}
|
|
91
|
+
if (!item.text) return item
|
|
92
|
+
const visibleText = item.kind === 'thoughts' ? visibleLines(item.text, 300)
|
|
93
|
+
: item.kind === 'summary' ? visibleLines(item.text, 500)
|
|
94
|
+
: item.text
|
|
95
|
+
const count = matchOffsets(visibleText, query).length
|
|
96
|
+
const decorated = { ...item, text: highlightMatches(item.text, query, currentIndex, startIndex) }
|
|
97
|
+
startIndex += count
|
|
98
|
+
return decorated
|
|
99
|
+
}
|
|
100
|
+
return items.map(decorate)
|
|
101
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function deliberationsFromEvents(events) {
|
|
2
|
+
const byId = new Map()
|
|
3
|
+
|
|
4
|
+
for (const event of events) {
|
|
5
|
+
const data = event.data || {}
|
|
6
|
+
const id = data.deliberationId
|
|
7
|
+
if (!id) continue
|
|
8
|
+
|
|
9
|
+
if (event.type === 'deliberation_dismiss') {
|
|
10
|
+
byId.delete(id)
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (event.type === 'deliberation_start') {
|
|
15
|
+
byId.set(id, {
|
|
16
|
+
id: `deliberation-${id}`,
|
|
17
|
+
deliberationId: id,
|
|
18
|
+
role: 'deliberation',
|
|
19
|
+
description: data.brief,
|
|
20
|
+
prompt: data.brief,
|
|
21
|
+
model: data.model,
|
|
22
|
+
rounds: data.rounds,
|
|
23
|
+
status: 'running',
|
|
24
|
+
startedAt: event.at,
|
|
25
|
+
updatedAt: event.at,
|
|
26
|
+
events: [],
|
|
27
|
+
turns: [],
|
|
28
|
+
timeline: [],
|
|
29
|
+
})
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const item = byId.get(id)
|
|
34
|
+
if (!item) continue
|
|
35
|
+
item.updatedAt = event.at
|
|
36
|
+
|
|
37
|
+
if (event.type === 'deliberation_event') {
|
|
38
|
+
const recorded = { ...data.event, role: data.role, round: data.round, at: event.at }
|
|
39
|
+
item.events.push(recorded)
|
|
40
|
+
item.timeline.push({ kind: 'event', value: recorded })
|
|
41
|
+
} else if (event.type === 'deliberation_turn') {
|
|
42
|
+
const turn = { role: data.role, round: data.round, text: data.text }
|
|
43
|
+
item.turns.push(turn)
|
|
44
|
+
item.timeline.push({ kind: 'turn', value: turn })
|
|
45
|
+
item.usage = mergeUsage(item.usage, data.usage)
|
|
46
|
+
} else if (event.type === 'deliberation_result') {
|
|
47
|
+
item.result = data.result
|
|
48
|
+
item.error = data.error
|
|
49
|
+
item.usage = mergeUsage(item.usage, data.usage)
|
|
50
|
+
item.status = data.error ? 'failed' : data.interrupted ? 'cancelled' : 'completed'
|
|
51
|
+
item.endedAt = event.at
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [...byId.values()].sort((a, b) => Number(b.deliberationId) - Number(a.deliberationId))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mergeUsage(current = {}, next = {}) {
|
|
59
|
+
if (!next) return current
|
|
60
|
+
return {
|
|
61
|
+
promptTokens: (current.promptTokens || 0) + (next.promptTokens || 0),
|
|
62
|
+
completionTokens: (current.completionTokens || 0) + (next.completionTokens || 0),
|
|
63
|
+
totalTokens: (current.totalTokens || 0) + (next.totalTokens || 0),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { resultText } from './agents.js'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_DELIBERATION_ROUNDS = 3
|
|
4
|
+
export const MAX_DELIBERATION_ROUNDS = 5
|
|
5
|
+
|
|
6
|
+
export function validateDeliberation({ brief, rounds = DEFAULT_DELIBERATION_ROUNDS } = {}) {
|
|
7
|
+
if (!brief?.trim()) throw new Error('deliberation brief is required')
|
|
8
|
+
if (!Number.isInteger(rounds) || rounds < 1 || rounds > MAX_DELIBERATION_ROUNDS) {
|
|
9
|
+
throw new Error(`deliberation rounds must be between 1 and ${MAX_DELIBERATION_ROUNDS}`)
|
|
10
|
+
}
|
|
11
|
+
return { brief: brief.trim(), rounds }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function participantPrompt(brief, role, rounds) {
|
|
15
|
+
return `You are the ${role} in a ${rounds}-round deliberation about the following decision:\n\n${brief}\n\nResearch before making claims. Use the available project and web tools whenever they can replace assumption with evidence. Cite URLs and project paths in your response. Challenge weak premises and address the peer's strongest points. Do not seek agreement for its own sake or defend a fixed position. Change your position only when evidence warrants it, and preserve material disagreement and uncertainty. Do not modify project files. Return a concise message addressed to the other participant.`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function peerMessage(role, text, round, rounds) {
|
|
19
|
+
return `Round ${round} of ${rounds}. The ${role} replied:\n\n${text}\n\nResearch and respond to the substance of this message.`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function synthesisPrompt(brief, turns) {
|
|
23
|
+
const transcript = turns.map((turn) => `Round ${turn.round}, ${turn.role}:\n${turn.text}`).join('\n\n')
|
|
24
|
+
return `Synthesize this deliberation into a decision for the main agent. State the recommendation, decisive evidence, unresolved uncertainty, material disagreement, and implementation constraints. Do not manufacture consensus: preserve disagreements that the evidence did not resolve. Preserve useful URLs and project paths. Do not mention the deliberation process unless disagreement remains.\n\nDecision brief:\n${brief}\n\nTranscript:\n${transcript}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runDeliberation({ brief, rounds, runParticipant, runSynthesis, onEvent = () => {}, signal }) {
|
|
28
|
+
const valid = validateDeliberation({ brief, rounds })
|
|
29
|
+
const participants = {
|
|
30
|
+
proposer: [{ role: 'user', content: participantPrompt(valid.brief, 'proposer', valid.rounds) }],
|
|
31
|
+
reviewer: [{ role: 'user', content: participantPrompt(valid.brief, 'reviewer', valid.rounds) }],
|
|
32
|
+
}
|
|
33
|
+
const turns = []
|
|
34
|
+
let peer = null
|
|
35
|
+
|
|
36
|
+
for (let round = 1; round <= valid.rounds; round++) {
|
|
37
|
+
for (const role of ['proposer', 'reviewer']) {
|
|
38
|
+
if (signal?.aborted) return { turns, interrupted: true }
|
|
39
|
+
const history = participants[role]
|
|
40
|
+
if (peer) history.push({ role: 'user', content: peerMessage(peer.role, peer.text, round, valid.rounds) })
|
|
41
|
+
const result = await runParticipant({ role, round, rounds: valid.rounds, history: [...history], signal })
|
|
42
|
+
history.push(...(result.messages || []))
|
|
43
|
+
const text = resultText(result.messages)
|
|
44
|
+
if (result.error) return { turns, interrupted: true, error: result.error }
|
|
45
|
+
if (result.interrupted || !text) return { turns, interrupted: true, error: text ? null : 'deliberation participant returned no response' }
|
|
46
|
+
const turn = { role, round, text, messages: result.messages || [], usage: result.usage || null }
|
|
47
|
+
turns.push(turn)
|
|
48
|
+
peer = turn
|
|
49
|
+
onEvent({ type: 'deliberation_turn', ...turn })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const synthesis = await runSynthesis({ history: [{ role: 'user', content: synthesisPrompt(valid.brief, turns) }], signal })
|
|
54
|
+
return {
|
|
55
|
+
turns,
|
|
56
|
+
result: resultText(synthesis.messages),
|
|
57
|
+
usage: synthesis.usage || null,
|
|
58
|
+
interrupted: !!synthesis.interrupted,
|
|
59
|
+
error: synthesis.error || null,
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/derive.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { continuationMessage } from './compaction.js'
|
|
2
|
+
import { applySteering } from './steer.js'
|
|
3
|
+
|
|
4
|
+
const DROPPING_MODES = ['both', 'chat', 'summary']
|
|
5
|
+
|
|
6
|
+
// context editing: a large tool result keeps getting re-sent with every
|
|
7
|
+
// request long after the model has used it. once the conversation has moved
|
|
8
|
+
// on (two user turns past it), the body is replaced with a short note in the
|
|
9
|
+
// model's view only; the transcript and the jsonl keep the real thing, and
|
|
10
|
+
// the model can re-run the tool if it truly needs the data again
|
|
11
|
+
const ELIDE_MIN_CHARS = 4000
|
|
12
|
+
const ELIDE_AFTER_USER_TURNS = 2
|
|
13
|
+
|
|
14
|
+
function elideStaleToolResults(history) {
|
|
15
|
+
const userIndexes = []
|
|
16
|
+
history.forEach((message, i) => {
|
|
17
|
+
if (message.role === 'user') userIndexes.push(i)
|
|
18
|
+
})
|
|
19
|
+
const cutoff = userIndexes.at(-ELIDE_AFTER_USER_TURNS)
|
|
20
|
+
if (cutoff === undefined) return history
|
|
21
|
+
return history.map((message, i) => {
|
|
22
|
+
if (i >= cutoff || message.role !== 'tool') return message
|
|
23
|
+
const content = String(message.content ?? '')
|
|
24
|
+
if (content.length < ELIDE_MIN_CHARS) return message
|
|
25
|
+
return {
|
|
26
|
+
...message,
|
|
27
|
+
content: `[tool result elided to save context: ${content.length.toLocaleString()} chars. re-run the tool if this is needed again]`,
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function activeRewinds(events) {
|
|
33
|
+
const canceled = new Set()
|
|
34
|
+
for (const e of events) {
|
|
35
|
+
if (e.type === 'rewind_undo') canceled.add(e.data.rewindId)
|
|
36
|
+
}
|
|
37
|
+
return events.filter((e) => e.type === 'rewind' && !canceled.has(e.id))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function droppedIds(events) {
|
|
41
|
+
const dropped = new Set()
|
|
42
|
+
const index = new Map(events.map((e, i) => [e.id, i]))
|
|
43
|
+
for (const rewind of activeRewinds(events)) {
|
|
44
|
+
if (!DROPPING_MODES.includes(rewind.data.mode)) continue
|
|
45
|
+
const from = index.get(rewind.data.target)
|
|
46
|
+
const to = index.get(rewind.id)
|
|
47
|
+
if (from === undefined || to === undefined) continue
|
|
48
|
+
for (let i = from; i < to; i++) dropped.add(events[i].id)
|
|
49
|
+
}
|
|
50
|
+
return dropped
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addUsageInto(total, usage) {
|
|
54
|
+
total.promptTokens += usage.promptTokens || 0
|
|
55
|
+
total.completionTokens += usage.completionTokens || 0
|
|
56
|
+
total.totalTokens += usage.totalTokens || 0
|
|
57
|
+
total.cachedTokens += usage.cachedTokens || 0
|
|
58
|
+
total.thoughtTokens += usage.thoughtTokens || 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function emptyUsage() {
|
|
62
|
+
return { promptTokens: 0, completionTokens: 0, totalTokens: 0, cachedTokens: 0, thoughtTokens: 0 }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseArgs(raw) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(raw)
|
|
68
|
+
} catch {
|
|
69
|
+
return { _raw: raw }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pushHistory(state, message, eventId) {
|
|
74
|
+
state.providerHistory.push(message)
|
|
75
|
+
state.historyEventIds.push(eventId)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function foldMessage(state, event) {
|
|
79
|
+
const message = event.data.message
|
|
80
|
+
if (!message) return
|
|
81
|
+
pushHistory(state, message, event.id)
|
|
82
|
+
if (event.data.hideFromTranscript) {
|
|
83
|
+
for (const call of message.tool_calls || []) {
|
|
84
|
+
state.toolItems.set(call.id, { kind: 'tool', callId: call.id, status: 'done', hidden: true })
|
|
85
|
+
}
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
const base = {
|
|
89
|
+
messageId: event.id,
|
|
90
|
+
eventId: event.id,
|
|
91
|
+
role: message.role,
|
|
92
|
+
locked: event._sourceIndex < state.latestCompactIndex,
|
|
93
|
+
steered: !!event._steered,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (message.role === 'system' || message.role === 'developer') {
|
|
97
|
+
state.transcript.push({ ...base, kind: message.role, text: String(message.content ?? '') })
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (message.role === 'user') {
|
|
101
|
+
const text = Array.isArray(message.content)
|
|
102
|
+
? message.content
|
|
103
|
+
.map((p) => (p.type === 'text' ? p.text : `[image: ${String(p.source?.path || '').split('/').pop() || 'attached'}]`))
|
|
104
|
+
.join('')
|
|
105
|
+
: String(message.content)
|
|
106
|
+
state.transcript.push({ ...base, kind: 'user', text, content: message.content })
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if (message.role === 'assistant') {
|
|
110
|
+
if (message.content) {
|
|
111
|
+
state.transcript.push({ ...base, kind: 'assistant', text: message.content, model: state.model })
|
|
112
|
+
}
|
|
113
|
+
for (const call of message.tool_calls || []) {
|
|
114
|
+
const item = {
|
|
115
|
+
kind: 'tool',
|
|
116
|
+
callId: call.id,
|
|
117
|
+
name: call.function.name,
|
|
118
|
+
args: parseArgs(call.function.arguments),
|
|
119
|
+
title: call.function.name,
|
|
120
|
+
status: 'done',
|
|
121
|
+
eventId: event.id,
|
|
122
|
+
}
|
|
123
|
+
state.transcript.push(item)
|
|
124
|
+
state.toolItems.set(call.id, item)
|
|
125
|
+
}
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
if (message.role === 'tool') {
|
|
129
|
+
const item = state.toolItems.get(message.tool_call_id)
|
|
130
|
+
if (item) item.resultText = String(message.content)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function foldRewind(state, event) {
|
|
135
|
+
const { mode, summaryText, reverted = [] } = event.data
|
|
136
|
+
for (const callId of reverted) {
|
|
137
|
+
const item = state.toolItems.get(callId)
|
|
138
|
+
if (item) item.status = 'reverted'
|
|
139
|
+
}
|
|
140
|
+
if (mode === 'summary' && summaryText) {
|
|
141
|
+
state.transcript.push({ kind: 'summary', source: 'rewind', text: summaryText })
|
|
142
|
+
pushHistory(state, {
|
|
143
|
+
role: 'assistant',
|
|
144
|
+
content: `[summary of the rewound conversation]\n${summaryText}`,
|
|
145
|
+
}, event.id)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function deriveState(events) {
|
|
150
|
+
const effectiveEvents = applySteering(events)
|
|
151
|
+
const dropped = droppedIds(effectiveEvents)
|
|
152
|
+
const latestCompactIndex = effectiveEvents.reduce((latest, event) => event.type === 'clear' ? -1 : event.type === 'compact' ? events.indexOf(event) : latest, -1)
|
|
153
|
+
const canceledUndoTargets = new Set(
|
|
154
|
+
events.filter((e) => e.type === 'rewind_undo').map((e) => e.data.rewindId),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const spentUsage = emptyUsage()
|
|
158
|
+
const spentUsageByModel = {}
|
|
159
|
+
for (const event of events) {
|
|
160
|
+
if (event.type !== 'usage') continue
|
|
161
|
+
addUsageInto(spentUsage, event.data.usage)
|
|
162
|
+
addUsageInto((spentUsageByModel[event.data.model] ||= emptyUsage()), event.data.usage)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const state = {
|
|
166
|
+
transcript: [],
|
|
167
|
+
providerHistory: [],
|
|
168
|
+
historyEventIds: [],
|
|
169
|
+
model: null,
|
|
170
|
+
effort: undefined,
|
|
171
|
+
usage: spentUsage,
|
|
172
|
+
usageByModel: spentUsageByModel,
|
|
173
|
+
usageActive: emptyUsage(),
|
|
174
|
+
usageActiveByModel: {},
|
|
175
|
+
lastPromptTokens: 0,
|
|
176
|
+
lastPromptModel: null,
|
|
177
|
+
loadedContext: new Set(),
|
|
178
|
+
toolItems: new Map(),
|
|
179
|
+
latestCompactIndex,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const event of effectiveEvents) {
|
|
183
|
+
if (event.type === 'usage') {
|
|
184
|
+
if (!dropped.has(event.id)) {
|
|
185
|
+
addUsageInto(state.usageActive, event.data.usage)
|
|
186
|
+
const activeByModel = (state.usageActiveByModel[event.data.model] ||= emptyUsage())
|
|
187
|
+
addUsageInto(activeByModel, event.data.usage)
|
|
188
|
+
// only events that recorded the final request's size can drive the
|
|
189
|
+
// context meter; older cumulative-only events would overstate it
|
|
190
|
+
if (event.data.lastPrompt !== undefined) {
|
|
191
|
+
state.lastPromptTokens = event.data.lastPrompt
|
|
192
|
+
state.lastPromptModel = event.data.model
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
if (dropped.has(event.id)) continue
|
|
198
|
+
|
|
199
|
+
switch (event.type) {
|
|
200
|
+
case 'message':
|
|
201
|
+
foldMessage(state, event)
|
|
202
|
+
break
|
|
203
|
+
case 'turn_transcript':
|
|
204
|
+
for (const item of event.data.items || []) {
|
|
205
|
+
const restored = { ...item, eventId: event.id }
|
|
206
|
+
state.transcript.push(restored)
|
|
207
|
+
if (restored.kind === 'tool' && restored.callId) state.toolItems.set(restored.callId, restored)
|
|
208
|
+
}
|
|
209
|
+
break
|
|
210
|
+
case 'tool_meta': {
|
|
211
|
+
const item = state.toolItems.get(event.data.callId)
|
|
212
|
+
if (item) Object.assign(item, event.data, { kind: 'tool', callId: item.callId })
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
case 'interrupt': {
|
|
216
|
+
const last = state.transcript.at(-1)
|
|
217
|
+
if (last?.kind === 'assistant') last.interrupted = true
|
|
218
|
+
if (last?.kind === 'tool' && last.status === 'running') last.status = 'interrupted'
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
case 'model_switch':
|
|
222
|
+
state.model = event.data.to
|
|
223
|
+
break
|
|
224
|
+
case 'effort':
|
|
225
|
+
state.effort = event.data.to
|
|
226
|
+
break
|
|
227
|
+
case 'title':
|
|
228
|
+
state.title = event.data.text
|
|
229
|
+
break
|
|
230
|
+
case 'color':
|
|
231
|
+
state.color = event.data.value
|
|
232
|
+
break
|
|
233
|
+
case 'rewind':
|
|
234
|
+
if (!canceledUndoTargets.has(event.id)) foldRewind(state, event)
|
|
235
|
+
break
|
|
236
|
+
case 'compact': {
|
|
237
|
+
const { summary, keepFrom, sessionFile } = event.data
|
|
238
|
+
if (keepFrom !== undefined || sessionFile) {
|
|
239
|
+
const idx = keepFrom ? state.historyEventIds.indexOf(keepFrom) : -1
|
|
240
|
+
const kept = idx >= 0 ? state.providerHistory.slice(idx) : []
|
|
241
|
+
const keptIds = idx >= 0 ? state.historyEventIds.slice(idx) : []
|
|
242
|
+
state.providerHistory = [
|
|
243
|
+
{ role: 'user', content: continuationMessage(summary, { sessionFile, recentKept: kept.length > 0 }) },
|
|
244
|
+
...kept,
|
|
245
|
+
]
|
|
246
|
+
state.historyEventIds = [null, ...keptIds]
|
|
247
|
+
} else {
|
|
248
|
+
state.providerHistory = [
|
|
249
|
+
{ role: 'user', content: `[conversation summary]\n${summary}` },
|
|
250
|
+
{ role: 'assistant', content: 'Got it. Continuing from that summary.' },
|
|
251
|
+
]
|
|
252
|
+
state.historyEventIds = [null, null]
|
|
253
|
+
}
|
|
254
|
+
state.transcript.push({ kind: 'summary', source: 'compact', text: summary })
|
|
255
|
+
state.lastPromptTokens = 0
|
|
256
|
+
break
|
|
257
|
+
}
|
|
258
|
+
case 'clear':
|
|
259
|
+
state.transcript = []
|
|
260
|
+
state.providerHistory = []
|
|
261
|
+
state.historyEventIds = []
|
|
262
|
+
state.toolItems = new Map()
|
|
263
|
+
state.lastPromptTokens = 0
|
|
264
|
+
break
|
|
265
|
+
case 'context_file':
|
|
266
|
+
state.loadedContext.add(event.data.path)
|
|
267
|
+
break
|
|
268
|
+
case 'skill':
|
|
269
|
+
state.transcript.push({ kind: 'skill', name: event.data.name })
|
|
270
|
+
break
|
|
271
|
+
case 'thoughts':
|
|
272
|
+
state.transcript.push({ kind: 'thoughts', text: event.data.text })
|
|
273
|
+
break
|
|
274
|
+
case 'shell_note':
|
|
275
|
+
case 'system_note': {
|
|
276
|
+
pushHistory(state, { role: 'user', content: event.data.text }, event.id)
|
|
277
|
+
const lines = event.data.text.split('\n').filter(Boolean)
|
|
278
|
+
const agentCompletions = lines.filter((line) => /^Agent \d+ \(.+\) finished with status /.test(line))
|
|
279
|
+
if (agentCompletions.length) {
|
|
280
|
+
for (const text of agentCompletions) state.transcript.push({ kind: 'notice', text, agentCompletion: true })
|
|
281
|
+
} else {
|
|
282
|
+
state.transcript.push({ kind: 'notice', text: lines[0] || '' })
|
|
283
|
+
}
|
|
284
|
+
break
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
state.providerHistory = elideStaleToolResults(state.providerHistory)
|
|
290
|
+
return state
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function userEntries(state) {
|
|
294
|
+
const entries = []
|
|
295
|
+
state.transcript.forEach((item, index) => {
|
|
296
|
+
if (item.kind === 'user') entries.push({ text: item.text, content: item.content, index, eventId: item.eventId })
|
|
297
|
+
})
|
|
298
|
+
return entries
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function rewindStats(state, index) {
|
|
302
|
+
const tail = state.transcript.slice(index)
|
|
303
|
+
return {
|
|
304
|
+
msgs: tail.length,
|
|
305
|
+
edits: tail.filter((m) => m.kind === 'tool' && m.revert && m.status !== 'reverted'),
|
|
306
|
+
}
|
|
307
|
+
}
|
package/src/events.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { createReadStream } from 'node:fs'
|
|
3
|
+
import { createInterface } from 'node:readline'
|
|
4
|
+
|
|
5
|
+
export const SESSION_VERSION = 1
|
|
6
|
+
|
|
7
|
+
export function makeEvent(type, data = {}) {
|
|
8
|
+
return { id: randomUUID(), at: Date.now(), type, data }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function makeHeader({ cwd, root, forkedFrom }) {
|
|
12
|
+
return {
|
|
13
|
+
type: 'session',
|
|
14
|
+
version: SESSION_VERSION,
|
|
15
|
+
id: randomUUID(),
|
|
16
|
+
cwd,
|
|
17
|
+
root,
|
|
18
|
+
createdAt: Date.now(),
|
|
19
|
+
...(forkedFrom ? { forkedFrom } : {}),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function serializeLine(event) {
|
|
24
|
+
return JSON.stringify(event) + '\n'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseLine(line) {
|
|
28
|
+
const trimmed = line.trim()
|
|
29
|
+
if (!trimmed) return null
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(trimmed)
|
|
32
|
+
return parsed && typeof parsed.type === 'string' ? parsed : null
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseLines(text) {
|
|
39
|
+
return text.split('\n').map(parseLine).filter(Boolean)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function* streamEvents(file) {
|
|
43
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity })
|
|
44
|
+
for await (const line of lines) {
|
|
45
|
+
const event = parseLine(line)
|
|
46
|
+
if (event) yield event
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function readEvents(file) {
|
|
51
|
+
const events = []
|
|
52
|
+
for await (const event of streamEvents(file)) events.push(event)
|
|
53
|
+
return events
|
|
54
|
+
}
|
package/src/export.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function transcriptToMarkdown(transcript, { title = 'pico session' } = {}) {
|
|
2
|
+
const parts = [`# ${title}`, '']
|
|
3
|
+
for (const item of transcript) {
|
|
4
|
+
if (item.kind === 'user') {
|
|
5
|
+
parts.push('## You', '', item.text, '')
|
|
6
|
+
} else if (item.kind === 'assistant') {
|
|
7
|
+
parts.push(item.interrupted ? `${item.text}\n\n*(interrupted)*` : item.text, '')
|
|
8
|
+
} else if (item.kind === 'tool') {
|
|
9
|
+
const label = item.diff ? ` (+${item.diff.additions} -${item.diff.deletions})` : ''
|
|
10
|
+
parts.push(`> ${item.status === 'reverted' ? '↩' : '✓'} \`${item.name}\` ${item.title}${label}`, '')
|
|
11
|
+
} else if (item.kind === 'summary') {
|
|
12
|
+
parts.push(`> summary: ${item.text}`, '')
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return parts.join('\n')
|
|
16
|
+
}
|
package/src/files.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import fg from 'fast-glob'
|
|
3
|
+
|
|
4
|
+
const MAX_FILES = 5000
|
|
5
|
+
const TTL = 5000
|
|
6
|
+
|
|
7
|
+
let cached = { at: 0, cwd: null, files: [] }
|
|
8
|
+
const pending = new Map()
|
|
9
|
+
|
|
10
|
+
function ripgrepFiles(cwd) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const args = ['--files', '--hidden', '--glob', '!**/.git/**', '--sortr=modified']
|
|
13
|
+
execFile('rg', args, { cwd, maxBuffer: 50 * 1024 * 1024 }, (err, stdout) => {
|
|
14
|
+
resolve(err && !stdout ? null : stdout.split('\n').filter(Boolean))
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function listFiles(cwd) {
|
|
20
|
+
if (cached.cwd === cwd && Date.now() - cached.at < TTL) return Promise.resolve(cached.files)
|
|
21
|
+
if (pending.has(cwd)) return pending.get(cwd)
|
|
22
|
+
|
|
23
|
+
const request = (async () => {
|
|
24
|
+
let files = await ripgrepFiles(cwd)
|
|
25
|
+
if (!files) {
|
|
26
|
+
files = await fg('**/*', {
|
|
27
|
+
cwd,
|
|
28
|
+
onlyFiles: true,
|
|
29
|
+
dot: true,
|
|
30
|
+
ignore: ['**/node_modules/**', '**/.git/**'],
|
|
31
|
+
}).catch(() => [])
|
|
32
|
+
}
|
|
33
|
+
cached = { at: Date.now(), cwd, files: files.slice(0, MAX_FILES) }
|
|
34
|
+
return cached.files
|
|
35
|
+
})().finally(() => pending.delete(cwd))
|
|
36
|
+
|
|
37
|
+
pending.set(cwd, request)
|
|
38
|
+
return request
|
|
39
|
+
}
|