picocode-core 0.9.174 → 0.9.176
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/controller.js +30 -0
- package/src/derive.js +14 -1
- package/src/tool-trimming.js +142 -0
package/package.json
CHANGED
package/src/controller.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createAgentManager } from './agents.js'
|
|
|
11
11
|
import { runDeliberation, validateDeliberation } from './deliberation.js'
|
|
12
12
|
import { deliberationsFromEvents } from './deliberation-history.js'
|
|
13
13
|
import { compactionPrompt, formatCompactSummary, summarySections, compactionKeepFrom } from './compaction.js'
|
|
14
|
+
import { completedToolCalls, TOOL_TRIM_VERSION } from './tool-trimming.js'
|
|
14
15
|
import { createToolset } from './tools/index.js'
|
|
15
16
|
import { defaultTitle } from './tools/recorder.js'
|
|
16
17
|
import { scanUserTools } from './user-tools.js'
|
|
@@ -824,6 +825,33 @@ export function createController({ boot }) {
|
|
|
824
825
|
flash(value ? `session color: ${names[values.indexOf(value)] || value}` : 'session color cleared')
|
|
825
826
|
}
|
|
826
827
|
|
|
828
|
+
function changeToolTrims(type, callIds) {
|
|
829
|
+
if (state.busy || state.compacting) return flash('error: finish or interrupt the current turn first')
|
|
830
|
+
if (!Array.isArray(callIds) || !callIds.length || callIds.some((id) => typeof id !== 'string' || !id)) {
|
|
831
|
+
return flash('error: tool call IDs must be a non-empty array of strings')
|
|
832
|
+
}
|
|
833
|
+
const ids = [...new Set(callIds)]
|
|
834
|
+
const completed = completedToolCalls(state.derived.providerHistory)
|
|
835
|
+
const unavailable = ids.filter((id) => !completed.has(id))
|
|
836
|
+
if (unavailable.length) return flash(`error: tool calls are not completed in current context: ${unavailable.join(', ')}`)
|
|
837
|
+
const eligible = type === 'tool_trim'
|
|
838
|
+
? ids.filter((id) => state.derived.toolItems.get(id)?.contextCanTrim)
|
|
839
|
+
: ids.filter((id) => state.derived.toolItems.get(id)?.contextTrimmed)
|
|
840
|
+
if (!eligible.length) return false
|
|
841
|
+
persist(makeEvent(type, { callIds: eligible, version: TOOL_TRIM_VERSION }))
|
|
842
|
+
ensureSession()
|
|
843
|
+
reDerive()
|
|
844
|
+
return true
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function trimTools(callIds) {
|
|
848
|
+
return changeToolTrims('tool_trim', callIds)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function restoreTools(callIds) {
|
|
852
|
+
return changeToolTrims('tool_restore', callIds)
|
|
853
|
+
}
|
|
854
|
+
|
|
827
855
|
function clear() {
|
|
828
856
|
if (state.busy) return flash('finish or interrupt the current turn first')
|
|
829
857
|
persist(makeEvent('clear', {}))
|
|
@@ -1343,6 +1371,8 @@ export function createController({ boot }) {
|
|
|
1343
1371
|
send,
|
|
1344
1372
|
interrupt,
|
|
1345
1373
|
compact,
|
|
1374
|
+
trimTools,
|
|
1375
|
+
restoreTools,
|
|
1346
1376
|
answerQuestion,
|
|
1347
1377
|
cancelQuestion,
|
|
1348
1378
|
recallPending,
|
package/src/derive.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { continuationMessage } from './compaction.js'
|
|
2
2
|
import { applySteering } from './steer.js'
|
|
3
|
+
import { annotateToolContext, applyToolTrims } from './tool-trimming.js'
|
|
3
4
|
|
|
4
5
|
const DROPPING_MODES = ['both', 'chat', 'summary']
|
|
5
6
|
|
|
@@ -178,6 +179,7 @@ export function deriveState(events) {
|
|
|
178
179
|
loadedContext: new Set(),
|
|
179
180
|
toolItems: new Map(),
|
|
180
181
|
latestCompactIndex,
|
|
182
|
+
trimmedToolIds: new Set(),
|
|
181
183
|
}
|
|
182
184
|
|
|
183
185
|
for (const event of effectiveEvents) {
|
|
@@ -256,11 +258,20 @@ export function deriveState(events) {
|
|
|
256
258
|
state.lastPromptTokens = 0
|
|
257
259
|
break
|
|
258
260
|
}
|
|
261
|
+
case 'tool_trim':
|
|
262
|
+
for (const callId of event.data.callIds || []) state.trimmedToolIds.add(callId)
|
|
263
|
+
state.lastPromptTokens = 0
|
|
264
|
+
break
|
|
265
|
+
case 'tool_restore':
|
|
266
|
+
for (const callId of event.data.callIds || []) state.trimmedToolIds.delete(callId)
|
|
267
|
+
state.lastPromptTokens = 0
|
|
268
|
+
break
|
|
259
269
|
case 'clear':
|
|
260
270
|
state.transcript = []
|
|
261
271
|
state.providerHistory = []
|
|
262
272
|
state.historyEventIds = []
|
|
263
273
|
state.toolItems = new Map()
|
|
274
|
+
state.trimmedToolIds = new Set()
|
|
264
275
|
state.lastPromptTokens = 0
|
|
265
276
|
break
|
|
266
277
|
case 'context_file':
|
|
@@ -287,7 +298,9 @@ export function deriveState(events) {
|
|
|
287
298
|
}
|
|
288
299
|
}
|
|
289
300
|
|
|
290
|
-
|
|
301
|
+
const effectiveHistory = elideStaleToolResults(state.providerHistory)
|
|
302
|
+
state.providerHistory = applyToolTrims(effectiveHistory, state.trimmedToolIds)
|
|
303
|
+
annotateToolContext(state, state.providerHistory, state.trimmedToolIds)
|
|
291
304
|
return state
|
|
292
305
|
}
|
|
293
306
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const ALGORITHM = 'tool-trim-v1'
|
|
2
|
+
const ARGUMENT_CHARS = 8000
|
|
3
|
+
const RESULT_CHARS = 12000
|
|
4
|
+
const STRING_CHARS = 6000
|
|
5
|
+
const ARRAY_ITEMS = 40
|
|
6
|
+
const OBJECT_KEYS = 80
|
|
7
|
+
const HEAD_RATIO = 0.6
|
|
8
|
+
|
|
9
|
+
function truncateText(value, limit, label = 'content') {
|
|
10
|
+
const text = String(value ?? '')
|
|
11
|
+
if (text.length <= limit) return text
|
|
12
|
+
const marker = `\n[${label} omitted: ${text.length - limit} or more chars]\n`
|
|
13
|
+
const available = Math.max(0, limit - marker.length)
|
|
14
|
+
const head = Math.floor(available * HEAD_RATIO)
|
|
15
|
+
return text.slice(0, head) + marker + text.slice(text.length - (available - head))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function boundedValue(value, depth = 0) {
|
|
19
|
+
if (typeof value === 'string') return truncateText(value, STRING_CHARS)
|
|
20
|
+
if (value === null || typeof value !== 'object') return value
|
|
21
|
+
if (depth >= 8) return '[nested value omitted]'
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
if (value.length <= ARRAY_ITEMS) return value.map((item) => boundedValue(item, depth + 1))
|
|
24
|
+
const head = Math.ceil(ARRAY_ITEMS * HEAD_RATIO)
|
|
25
|
+
const tail = ARRAY_ITEMS - head
|
|
26
|
+
return [
|
|
27
|
+
...value.slice(0, head).map((item) => boundedValue(item, depth + 1)),
|
|
28
|
+
`[${value.length - ARRAY_ITEMS} array items omitted]`,
|
|
29
|
+
...value.slice(-tail).map((item) => boundedValue(item, depth + 1)),
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
const entries = Object.entries(value)
|
|
33
|
+
const kept = entries.length <= OBJECT_KEYS
|
|
34
|
+
? entries
|
|
35
|
+
: [...entries.slice(0, Math.ceil(OBJECT_KEYS * HEAD_RATIO)), ['_trimmed', `${entries.length - OBJECT_KEYS} object fields omitted`], ...entries.slice(-(OBJECT_KEYS - Math.ceil(OBJECT_KEYS * HEAD_RATIO)))]
|
|
36
|
+
return Object.fromEntries(kept.map(([key, item]) => [key, boundedValue(item, depth + 1)]))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function importantArguments(value) {
|
|
40
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
41
|
+
return Object.fromEntries(['description', 'name', 'outcome'].filter((key) => key in value).map((key) => [key, boundedValue(value[key])]))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function trimArguments(raw) {
|
|
45
|
+
let parsed
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(raw)
|
|
48
|
+
} catch {
|
|
49
|
+
parsed = { _raw: String(raw ?? '') }
|
|
50
|
+
}
|
|
51
|
+
let bounded = boundedValue(parsed)
|
|
52
|
+
let encoded = JSON.stringify(bounded)
|
|
53
|
+
if (encoded.length <= ARGUMENT_CHARS) return encoded
|
|
54
|
+
const important = importantArguments(parsed)
|
|
55
|
+
let previewLimit = ARGUMENT_CHARS
|
|
56
|
+
while (previewLimit > 0) {
|
|
57
|
+
encoded = JSON.stringify({ ...important, _trimmed: truncateText(JSON.stringify(parsed), previewLimit, 'arguments') })
|
|
58
|
+
if (encoded.length <= ARGUMENT_CHARS) return encoded
|
|
59
|
+
previewLimit = Math.floor(previewLimit / 2)
|
|
60
|
+
}
|
|
61
|
+
return JSON.stringify({ _trimmed: truncateText(JSON.stringify(parsed), 1000, 'arguments') })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isImagePart(part) {
|
|
65
|
+
return part && typeof part === 'object' && (part.type === 'image' || part.type === 'image_url' || part.type === 'input_image' || part.source?.type === 'base64')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function trimResult(content) {
|
|
69
|
+
if (!Array.isArray(content)) return truncateText(content, RESULT_CHARS, 'tool result')
|
|
70
|
+
const safe = content.map((part) => isImagePart(part)
|
|
71
|
+
? { type: 'text', text: '[tool image omitted to save context]' }
|
|
72
|
+
: boundedValue(part))
|
|
73
|
+
const encoded = JSON.stringify(safe)
|
|
74
|
+
return encoded.length <= RESULT_CHARS
|
|
75
|
+
? safe
|
|
76
|
+
: [{ type: 'text', text: truncateText(encoded, RESULT_CHARS, 'tool result') }]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function callsIn(history) {
|
|
80
|
+
const calls = new Map()
|
|
81
|
+
history.forEach((message, index) => {
|
|
82
|
+
for (const call of message.tool_calls || []) calls.set(call.id, { call, callIndex: index, result: null, resultIndex: -1 })
|
|
83
|
+
if (message.role === 'tool') {
|
|
84
|
+
const entry = calls.get(message.tool_call_id)
|
|
85
|
+
if (entry) Object.assign(entry, { result: message, resultIndex: index })
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
return calls
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function completedToolCalls(history) {
|
|
92
|
+
return new Map([...callsIn(history)].filter(([, entry]) => entry.result))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function applyToolTrims(history, trimmedIds) {
|
|
96
|
+
if (!trimmedIds.size) return history
|
|
97
|
+
const completed = completedToolCalls(history)
|
|
98
|
+
const callMessages = new Map()
|
|
99
|
+
const resultMessages = new Map()
|
|
100
|
+
for (const id of trimmedIds) {
|
|
101
|
+
const entry = completed.get(id)
|
|
102
|
+
if (!entry) continue
|
|
103
|
+
let callMessage = callMessages.get(entry.callIndex)
|
|
104
|
+
if (!callMessage) {
|
|
105
|
+
callMessage = { ...history[entry.callIndex], tool_calls: history[entry.callIndex].tool_calls.map((call) => ({ ...call, function: { ...call.function } })) }
|
|
106
|
+
callMessages.set(entry.callIndex, callMessage)
|
|
107
|
+
}
|
|
108
|
+
const call = callMessage.tool_calls.find((candidate) => candidate.id === id)
|
|
109
|
+
call.function.arguments = trimArguments(call.function.arguments)
|
|
110
|
+
resultMessages.set(entry.resultIndex, { ...entry.result, content: trimResult(entry.result.content) })
|
|
111
|
+
}
|
|
112
|
+
return history.map((message, index) => callMessages.get(index) || resultMessages.get(index) || message)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resultText(content) {
|
|
116
|
+
if (typeof content === 'string') return content
|
|
117
|
+
if (Array.isArray(content)) return content.filter((part) => part.type === 'text').map((part) => part.text ?? '').join('\n')
|
|
118
|
+
return JSON.stringify(content ?? '')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function entryTokens(args, result) {
|
|
122
|
+
return Math.ceil((String(args ?? '').length + resultText(result).length) / 4)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function annotateToolContext(state, effectiveHistory, trimmedIds) {
|
|
126
|
+
const completed = completedToolCalls(effectiveHistory)
|
|
127
|
+
for (const item of state.toolItems.values()) {
|
|
128
|
+
const entry = completed.get(item.callId)
|
|
129
|
+
item.contextAvailable = !!entry
|
|
130
|
+
item.contextTrimmed = !!entry && trimmedIds.has(item.callId)
|
|
131
|
+
item.contextTokens = entry ? entryTokens(entry.call.function.arguments, entry.result.content) : 0
|
|
132
|
+
item.contextCanTrim = !!entry && !item.contextTrimmed &&
|
|
133
|
+
entryTokens(trimArguments(entry.call.function.arguments), trimResult(entry.result.content)) < item.contextTokens
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const TOOL_TRIM_VERSION = {
|
|
138
|
+
algorithm: ALGORITHM,
|
|
139
|
+
argumentChars: ARGUMENT_CHARS,
|
|
140
|
+
resultChars: RESULT_CHARS,
|
|
141
|
+
stringChars: STRING_CHARS,
|
|
142
|
+
}
|