picocode-core 0.9.180 → 0.9.182

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocode-core",
3
- "version": "0.9.180",
3
+ "version": "0.9.182",
4
4
  "description": "The agent runtime behind pico: sessions, tools, subagents, MCP, memory, and model access",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- import { defaultTitle } from './tools/recorder.js'
1
+ import { defaultTitle, toolResultOutput } from './tools/recorder.js'
2
2
 
3
3
  function parseCallArgs(call) {
4
4
  try {
@@ -30,7 +30,11 @@ function settleTool(tools, event) {
30
30
  if (!item) return
31
31
  item.status = event.type === 'tool_error' ? 'error' : 'done'
32
32
  item.error = event.error ? String(event.error) : null
33
- item.fullOutput = event.result === undefined ? null : typeof event.result === 'string' ? event.result : JSON.stringify(event.result, null, 2)
33
+ item.fullOutput = toolResultOutput(item.name, event.result)
34
+ if (item.name === 'bash') {
35
+ item.exitCode = event.result?.exitCode
36
+ item.background = event.result?.status === 'running'
37
+ }
34
38
  }
35
39
 
36
40
  // a deliberation reads as turns: each turn owns the tool runs its
package/src/agent.js CHANGED
@@ -98,6 +98,10 @@ export async function summarizeText({ text, modelName, auth }) {
98
98
  return getText(out.lastResponse?.content || '').trim()
99
99
  }
100
100
 
101
+ function estimateTokens(value) {
102
+ return Math.ceil(JSON.stringify(value ?? '').length / 4)
103
+ }
104
+
101
105
  export async function runTurn({ history, tools, recorder, modelName, effort, auth, system, signal, onStream, stallMs = STALL_MS }) {
102
106
  const collected = []
103
107
  let roundText = ''
@@ -108,6 +112,20 @@ export async function runTurn({ history, tools, recorder, modelName, effort, aut
108
112
  // of the current context as actually sent
109
113
  let cumulativePrompt = 0
110
114
  let lastPromptTokens = 0
115
+ let estimatedOutputChars = 0
116
+ let estimatedPromptTokens = 0
117
+ const estimatedUsage = () => {
118
+ const exact = usageSeen || {}
119
+ const completionTokens = (exact.completionTokens || 0) + Math.ceil(estimatedOutputChars / 4)
120
+ const prompt = (exact.promptTokens || 0) + estimatedPromptTokens
121
+ return {
122
+ promptTokens: prompt,
123
+ completionTokens,
124
+ totalTokens: prompt + completionTokens,
125
+ cachedTokens: exact.cachedTokens || 0,
126
+ thoughtTokens: exact.thoughtTokens || 0,
127
+ }
128
+ }
111
129
 
112
130
  const internal = new AbortController()
113
131
  const onUserAbort = () => internal.abort()
@@ -137,8 +155,15 @@ export async function runTurn({ history, tools, recorder, modelName, effort, aut
137
155
  arm()
138
156
  if (event.type === 'content') {
139
157
  roundText += event.content
158
+ estimatedOutputChars += String(event.content ?? '').length
159
+ onStream?.({ type: 'usage_estimate', usage: estimatedUsage() })
160
+ } else if (event.type === 'thinking') {
161
+ estimatedOutputChars += String(event.content ?? '').length
162
+ onStream?.({ type: 'usage_estimate', usage: estimatedUsage() })
140
163
  } else if (event.type === 'tool_calls_ready') {
141
164
  collected.push({ role: 'assistant', content: roundText, tool_calls: event.calls })
165
+ estimatedOutputChars += JSON.stringify(event.calls ?? []).length
166
+ onStream?.({ type: 'usage_estimate', usage: estimatedUsage() })
142
167
  roundText = ''
143
168
  } else if (event.type === 'tool_complete') {
144
169
  collected.push({ role: 'tool', tool_call_id: event.call.id, content: JSON.stringify(event.result) })
@@ -149,6 +174,8 @@ export async function runTurn({ history, tools, recorder, modelName, effort, aut
149
174
  const prompt = event.usage?.promptTokens || 0
150
175
  lastPromptTokens = Math.max(0, prompt - cumulativePrompt)
151
176
  cumulativePrompt = prompt
177
+ estimatedPromptTokens = 0
178
+ estimatedOutputChars = 0
152
179
  }
153
180
  onStream?.(event)
154
181
  }
@@ -158,12 +185,15 @@ export async function runTurn({ history, tools, recorder, modelName, effort, aut
158
185
  scope(
159
186
  { inherit: Inherit.Conversation, system, tools, until: noToolsCalled(), stream },
160
187
  async (ctx) => {
188
+ const hydratedHistory = await hydrateImages(ctx.history)
189
+ estimatedPromptTokens = estimateTokens({ system, history: hydratedHistory, tools })
190
+ onStream?.({ type: 'usage_estimate', usage: estimatedUsage() })
161
191
  const out = await model({
162
192
  model: modelName,
163
193
  ...(effort && { effort }),
164
194
  ...(auth?.apiKey && { apiKey: auth.apiKey }),
165
195
  ...(auth?.headers && { headers: auth.headers }),
166
- })({ ...ctx, history: await hydrateImages(ctx.history), abortSignal: internal.signal })
196
+ })({ ...ctx, history: hydratedHistory, abortSignal: internal.signal })
167
197
  // Prepared bytes and omission notices belong only to the request, not
168
198
  // the conversation returned by the provider or subsequent tool rounds.
169
199
  return { ...out, history: [...ctx.history, ...out.history.slice(ctx.history.length)] }
package/src/controller.js CHANGED
@@ -119,6 +119,7 @@ export function createController({ boot }) {
119
119
  startedAt: 0,
120
120
  overlay: [],
121
121
  streaming: null,
122
+ liveUsage: null,
122
123
  queued: [],
123
124
  expedited: [],
124
125
  views: [],
@@ -545,6 +546,15 @@ export function createController({ boot }) {
545
546
  set({ turnPhase: 'thinking' })
546
547
  } else if (event.type === 'content') {
547
548
  set({ turnPhase: 'responding', streaming: (state.streaming || '') + event.content })
549
+ } else if (event.type === 'usage' || event.type === 'usage_estimate') {
550
+ const base = state.derived.usageActive
551
+ set({ liveUsage: {
552
+ promptTokens: base.promptTokens + (event.usage.promptTokens || 0),
553
+ completionTokens: base.completionTokens + (event.usage.completionTokens || 0),
554
+ totalTokens: base.totalTokens + (event.usage.totalTokens || 0),
555
+ cachedTokens: base.cachedTokens + (event.usage.cachedTokens || 0),
556
+ thoughtTokens: base.thoughtTokens + (event.usage.thoughtTokens || 0),
557
+ } })
548
558
  } else if (event.type === 'tool_calls_ready') {
549
559
  set({ turnPhase: 'tools' })
550
560
  updateOverlay((items) => {
@@ -594,7 +604,7 @@ export function createController({ boot }) {
594
604
  const researchAgentLimit = nextResearchAgentLimit || null
595
605
  nextResearchAgentLimit = null
596
606
  emit('turn', 'start')
597
- set({ busy: true, turnPhase: 'responding', startedAt: Date.now() })
607
+ set({ busy: true, turnPhase: 'responding', startedAt: Date.now(), liveUsage: null })
598
608
 
599
609
  const { auth, ok } = await codexAuth()
600
610
  if (!ok) {
@@ -643,7 +653,7 @@ export function createController({ boot }) {
643
653
  })
644
654
  } catch (err) {
645
655
  abort = null
646
- set({ overlay: [], streaming: null, turnPhase: 'idle', busy: false })
656
+ set({ overlay: [], streaming: null, liveUsage: null, turnPhase: 'idle', busy: false })
647
657
  flash(`error: ${errorText(err, 120)}`)
648
658
  return
649
659
  }
@@ -665,6 +675,7 @@ export function createController({ boot }) {
665
675
  state.turnPhase = 'idle'
666
676
  state.busy = false
667
677
  reDerive()
678
+ set({ liveUsage: null })
668
679
  boot.git.refresh()
669
680
  if (result.stalled) {
670
681
  flash('model stalled · turn interrupted')
@@ -22,6 +22,14 @@ export function defaultTitle(name, args = {}) {
22
22
  return raw.length > 60 ? raw.slice(0, 60) + '…' : raw
23
23
  }
24
24
 
25
+ export function toolResultOutput(name, result) {
26
+ if (result === undefined) return null
27
+ if (name === 'bash' && result && typeof result === 'object') {
28
+ return [result.stdout, result.stderr].filter(Boolean).join('\n')
29
+ }
30
+ return typeof result === 'string' ? result : JSON.stringify(result, null, 2)
31
+ }
32
+
25
33
  export function createRecorder(onChange) {
26
34
  return {
27
35
  currentCall: null,
@@ -67,9 +75,9 @@ export function recorded(recorder, name, fn) {
67
75
  recorder.begin(name, args)
68
76
  try {
69
77
  const result = await fn(args)
70
- if (!recorder.pending?.fullOutput && result !== undefined) {
78
+ if (recorder.pending?.fullOutput == null && result !== undefined) {
71
79
  recorder.extra({
72
- fullOutput: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
80
+ fullOutput: toolResultOutput(name, result),
73
81
  })
74
82
  }
75
83
  recorder.done()