picocode-core 0.9.141 → 0.9.143
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/agent-transcript.js +8 -0
- package/src/attachments.js +28 -1
- package/src/controller.js +33 -8
- package/src/paths.js +4 -0
- package/src/session.js +1 -0
package/package.json
CHANGED
package/src/agent-transcript.js
CHANGED
|
@@ -65,8 +65,16 @@ function deliberationTranscript(agent) {
|
|
|
65
65
|
}
|
|
66
66
|
if (event.type === 'tool_complete' || event.type === 'tool_error') settleTool(tools, event)
|
|
67
67
|
}
|
|
68
|
+
// whoever is speaking right now shows their words as they arrive
|
|
69
|
+
const live = agent.live
|
|
70
|
+
if (live?.text && live.role !== 'synthesis') {
|
|
71
|
+
const turn = turnFor(live.role, live.round)
|
|
72
|
+
if (turn.text == null) turn.text = live.text
|
|
73
|
+
}
|
|
68
74
|
if (agent.result) {
|
|
69
75
|
items.push({ kind: 'deliberation-turn', role: 'synthesis', text: agent.result, tools: [], interrupted: agent.status === 'cancelled' })
|
|
76
|
+
} else if (live?.text && live.role === 'synthesis') {
|
|
77
|
+
items.push({ kind: 'deliberation-turn', role: 'synthesis', text: live.text, tools: [], active: true })
|
|
70
78
|
} else if (agent.error) {
|
|
71
79
|
items.push({ kind: 'assistant', text: agent.error, interrupted: true })
|
|
72
80
|
}
|
package/src/attachments.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
|
-
import {
|
|
2
|
+
import { copyFile, mkdir, stat } from 'node:fs/promises'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { basename, join } from 'node:path'
|
|
3
5
|
|
|
4
6
|
const MEDIA_TYPES = {
|
|
5
7
|
png: 'image/png',
|
|
@@ -146,3 +148,28 @@ export function finalizeUserContent(text, attachments, exists = existsSync) {
|
|
|
146
148
|
if (expanded.length === 1 && expanded[0].type === 'text') return { content: expanded[0].text, used }
|
|
147
149
|
return { content: expanded, used }
|
|
148
150
|
}
|
|
151
|
+
|
|
152
|
+
// images ride into a session by path, and the path is what the log keeps,
|
|
153
|
+
// so each one is copied into the session's own attachments directory
|
|
154
|
+
// before the message is written and the part points at the copy. later
|
|
155
|
+
// turns and later readers then never depend on the original still being
|
|
156
|
+
// where it was
|
|
157
|
+
async function stashImage(part, dir) {
|
|
158
|
+
const path = part.source?.path
|
|
159
|
+
if (!path || part.source.kind !== 'path' || path.startsWith(dir)) return part
|
|
160
|
+
try {
|
|
161
|
+
const { size, mtimeMs } = await stat(path)
|
|
162
|
+
const key = createHash('sha1').update(`${path}\n${size}\n${Math.floor(mtimeMs)}`).digest('hex').slice(0, 10)
|
|
163
|
+
const copy = join(dir, `${key}-${basename(path)}`)
|
|
164
|
+
await mkdir(dir, { recursive: true })
|
|
165
|
+
if (!existsSync(copy)) await copyFile(path, copy)
|
|
166
|
+
return { ...part, source: { ...part.source, path: copy } }
|
|
167
|
+
} catch {
|
|
168
|
+
return part
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function stashImages(content, dir) {
|
|
173
|
+
if (!Array.isArray(content)) return content
|
|
174
|
+
return Promise.all(content.map((part) => (part.type === 'image' ? stashImage(part, dir) : part)))
|
|
175
|
+
}
|
package/src/controller.js
CHANGED
|
@@ -25,11 +25,11 @@ import { findModel, estimateCost } from './models.js'
|
|
|
25
25
|
import { adhocModel } from './catalog.js'
|
|
26
26
|
import { writeConfig } from './config.js'
|
|
27
27
|
import { connectOpenAI, openaiCredentials, disconnectOpenAI } from './openai-auth.js'
|
|
28
|
-
import { agentScratchDir, ensureDir } from './paths.js'
|
|
28
|
+
import { agentScratchDir, ensureDir, sessionAttachmentsDir } from './paths.js'
|
|
29
29
|
import { loadCodexModels } from './codex-models.js'
|
|
30
30
|
import { fuzzyScore } from './fuzzy.js'
|
|
31
31
|
import { MAX_DELIBERATION_ROUNDS } from './deliberation.js'
|
|
32
|
-
import { buildUserContent, finalizeUserContent, inputTextFromContent, mediaTypeFor } from './attachments.js'
|
|
32
|
+
import { buildUserContent, finalizeUserContent, inputTextFromContent, mediaTypeFor, stashImages } from './attachments.js'
|
|
33
33
|
|
|
34
34
|
export const EFFORT_LEVELS = [
|
|
35
35
|
{ key: null, desc: 'let the provider decide how much to think' },
|
|
@@ -225,6 +225,10 @@ export function createController({ boot }) {
|
|
|
225
225
|
},
|
|
226
226
|
})
|
|
227
227
|
|
|
228
|
+
// a running deliberation's current speaker streams into this buffer; it
|
|
229
|
+
// is never persisted, the turn event carries the settled text
|
|
230
|
+
const liveDeliberations = new Map()
|
|
231
|
+
|
|
228
232
|
const deliberations = {
|
|
229
233
|
run: async ({ brief, rounds, signal }) => {
|
|
230
234
|
const options = validateDeliberation({ brief, rounds })
|
|
@@ -240,6 +244,18 @@ export function createController({ boot }) {
|
|
|
240
244
|
if (!sessionId) throw new Error('deliberation requires an active session')
|
|
241
245
|
persist(makeEvent('deliberation_start', { deliberationId: id, brief, rounds, model: modelName }))
|
|
242
246
|
bumpActivity()
|
|
247
|
+
const live = { role: null, round: null, text: '' }
|
|
248
|
+
liveDeliberations.set(id, live)
|
|
249
|
+
const speak = (role, round) => (event) => {
|
|
250
|
+
if (event.type === 'content') {
|
|
251
|
+
if (live.role !== role || live.round !== round) Object.assign(live, { role, round, text: '' })
|
|
252
|
+
live.text += event.content
|
|
253
|
+
bumpActivity()
|
|
254
|
+
} else if (event.type === 'tool_calls_ready' && live.text) {
|
|
255
|
+
live.text = ''
|
|
256
|
+
bumpActivity()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
243
259
|
|
|
244
260
|
const persistDeliberation = (event) => {
|
|
245
261
|
persist(event)
|
|
@@ -281,14 +297,18 @@ export function createController({ boot }) {
|
|
|
281
297
|
history,
|
|
282
298
|
role,
|
|
283
299
|
onStream: (event) => {
|
|
300
|
+
speak(role, round)(event)
|
|
284
301
|
if (['tool_executing', 'tool_complete', 'tool_error'].includes(event.type)) {
|
|
285
302
|
persistDeliberation(makeEvent('deliberation_event', { deliberationId: id, role, round, event }))
|
|
286
303
|
}
|
|
287
304
|
},
|
|
288
305
|
}),
|
|
289
|
-
runSynthesis: ({ history }) => runWorker({ history, role: 'synthesizer', tools: false }),
|
|
290
|
-
onEvent: (event) =>
|
|
291
|
-
|
|
306
|
+
runSynthesis: ({ history }) => runWorker({ history, role: 'synthesizer', tools: false, onStream: speak('synthesis', null) }),
|
|
307
|
+
onEvent: (event) => {
|
|
308
|
+
Object.assign(live, { role: null, round: null, text: '' })
|
|
309
|
+
persistDeliberation(makeEvent('deliberation_turn', { deliberationId: id, ...event }))
|
|
310
|
+
},
|
|
311
|
+
}).finally(() => liveDeliberations.delete(id))
|
|
292
312
|
persistDeliberation(makeEvent('deliberation_result', { deliberationId: id, result: result.result, usage: result.usage, interrupted: result.interrupted, error: result.error }))
|
|
293
313
|
if (result.error) throw new Error(result.error)
|
|
294
314
|
return {
|
|
@@ -370,10 +390,11 @@ export function createController({ boot }) {
|
|
|
370
390
|
// that text must not be scanned for image paths or it attaches twice
|
|
371
391
|
const built = buildUserContent(text, state.attachments)
|
|
372
392
|
const viewed = built.used.length > 0 && built.used.every((placeholder) => deliveredImages.has(placeholder))
|
|
373
|
-
const { content, used } = viewed ? built : finalizeUserContent(text, state.attachments)
|
|
393
|
+
const { content: built2, used } = viewed ? built : finalizeUserContent(text, state.attachments)
|
|
374
394
|
for (const placeholder of used) deliveredImages.delete(placeholder)
|
|
375
|
-
persist(makeEvent('message', { message: { role: 'user', content }, ...(viewed ? { origin: 'view' } : {}) }))
|
|
376
395
|
ensureSession()
|
|
396
|
+
const content = state.session.file ? await stashImages(built2, sessionAttachmentsDir(boot.root, state.session.id)) : built2
|
|
397
|
+
persist(makeEvent('message', { message: { role: 'user', content }, ...(viewed ? { origin: 'view' } : {}) }))
|
|
377
398
|
reDerive()
|
|
378
399
|
await runAgentTurn()
|
|
379
400
|
}
|
|
@@ -1217,7 +1238,11 @@ export function createController({ boot }) {
|
|
|
1217
1238
|
}
|
|
1218
1239
|
|
|
1219
1240
|
function activity() {
|
|
1220
|
-
const
|
|
1241
|
+
const withLive = (item) => {
|
|
1242
|
+
const live = liveDeliberations.get(item.deliberationId)
|
|
1243
|
+
return live?.role ? { ...item, live: { ...live } } : item
|
|
1244
|
+
}
|
|
1245
|
+
const rows = [...agents.list(), ...deliberationsFromEvents(state.events).map(withLive)]
|
|
1221
1246
|
return rows.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))
|
|
1222
1247
|
}
|
|
1223
1248
|
|
package/src/paths.js
CHANGED
|
@@ -33,6 +33,10 @@ export function sessionScratchDir(root, sessionId) {
|
|
|
33
33
|
return join(projectDir(root), 'scratchpads', sessionId)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function sessionAttachmentsDir(root, sessionId) {
|
|
37
|
+
return join(projectDir(root), 'attachments', sessionId)
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
export function agentScratchDir(root, sessionId, agentId) {
|
|
37
41
|
return join(sessionScratchDir(root, sessionId), `agent-${agentId}`)
|
|
38
42
|
}
|
package/src/session.js
CHANGED
|
@@ -145,6 +145,7 @@ export async function deleteSession(file) {
|
|
|
145
145
|
})
|
|
146
146
|
await removeFromSessionIndex(file)
|
|
147
147
|
await rm(join(project, 'scratchpads', sessionId), { recursive: true, force: true })
|
|
148
|
+
await rm(join(project, 'attachments', sessionId), { recursive: true, force: true })
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
export function deleteProjectData(root) {
|