-
-
Notifications
You must be signed in to change notification settings - Fork 395
feat(agents): stream provider observations #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,11 +15,24 @@ import type { LocalAgentProvider } from "./local-agent-profiles.js"; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createCodexSdkLocalAgentRuntime, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createLocalAgentObservationEmitter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isNativeSchemaUnsupportedFailure, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ProviderSchemaUnsupportedError, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type LocalAgentRunInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type LocalAgentRunResult, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "./local-agent-runtime.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractAcpObservations, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractAcpUsage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractClaudeObservations, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractClaudeUsage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractCodexObservations, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractCodexUsage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractOpenCodeObservations, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractOpenCodeUsage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractPiObservations, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractPiUsage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "./local-agent-provider-observations.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface LocalAgentAdapter { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly provider: LocalAgentProvider; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -103,9 +116,13 @@ class ClaudeLocalAgentAdapter implements LocalAgentAdapter { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let providerSessionId = input.providerSessionId ?? null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let finalResponse = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let structured: unknown | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let usage: LocalAgentRunResult["usage"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const emitObservation = createLocalAgentObservationEmitter(input); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const items: unknown[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for await (const message of messages) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items.push(message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const observation of extractClaudeObservations(message)) emitObservation(observation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage = extractClaudeUsage(message) ?? usage; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const record = message as Record<string, unknown>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof record.session_id === "string") providerSessionId = record.session_id; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (record.type !== "result") continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -124,6 +141,7 @@ class ClaudeLocalAgentAdapter implements LocalAgentAdapter { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providerSessionId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finalResponse, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(structured !== undefined ? { structured } : {}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -226,8 +244,13 @@ class OpencodeLocalAgentAdapter implements LocalAgentAdapter { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sessionId = input.providerSessionId ?? await createOpencodeSession(client, input); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const promptResult = await promptOpencodeSession(client, sessionId, input); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const emitObservation = createLocalAgentObservationEmitter(input); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const observation of extractOpenCodeObservations(promptResult)) emitObservation(observation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await waitForOpencodeSession(client, sessionId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const messages = await readOpencodeMessages(client, sessionId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const observation of extractOpenCodeObservations(messages)) emitObservation(observation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const usage = extractOpenCodeUsage(messages) ?? extractOpenCodeUsage(promptResult); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (usage) emitObservation({ kind: "usage", usage }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const finalResponse = requireFinalResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "OpenCode", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractOpenCodeFinalResponse(messages) || extractOpenCodeFinalResponse(promptResult), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -237,6 +260,7 @@ class OpencodeLocalAgentAdapter implements LocalAgentAdapter { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providerSessionId: sessionId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finalResponse, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items: [promptResult, messages], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server.close(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -263,6 +287,8 @@ class AcpLocalAgentAdapter implements LocalAgentAdapter { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertPipedChild(child); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let stderr = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const emitObservation = createLocalAgentObservationEmitter(input); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let usage: LocalAgentRunResult["usage"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| child.stderr.on("data", (chunk: Buffer) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stderr += chunk.toString("utf8"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -302,6 +328,8 @@ class AcpLocalAgentAdapter implements LocalAgentAdapter { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const update = message.update; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const observation of extractAcpObservations(update)) emitObservation(observation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage = extractAcpUsage(update) ?? usage; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (update.sessionUpdate !== "agent_message_chunk") continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const content = update.content; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (content.type === "text") textParts.push(content.text); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -315,6 +343,7 @@ class AcpLocalAgentAdapter implements LocalAgentAdapter { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providerSessionId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finalResponse: finalResponse.trim(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items: [], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`${this.provider} ACP run failed: ${errorMessage(error)}${stderr ? `\n${stderr.trim()}` : ""}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -429,14 +458,24 @@ class PiRpcLocalAgentAdapter implements LocalAgentAdapter { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assertPipedChild(child); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rpc = new JsonLineRpc(child); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const events: unknown[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rpc.onEvent((event) => events.push(event)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const emitObservation = createLocalAgentObservationEmitter(input); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let usage: LocalAgentRunResult["usage"]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rpc.onEvent((event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.push(event); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const observation of extractPiObservations(event)) emitObservation(observation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage = extractPiUsage(event) ?? usage; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const state = await rpc.request({ type: "get_state" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const providerSessionId = readNestedString(state, ["sessionId"]) ?? input.providerSessionId ?? null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const done = rpc.waitForEvent((event) => asRecord(event)?.type === "agent_end", PI_AGENT_TIMEOUT_MS); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await rpc.request({ type: "prompt", message: input.prompt }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const agentEnd = await done; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sessionMessages = await rpc.request({ type: "get_messages" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const observation of extractPiObservations(agentEnd)) emitObservation(observation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const observation of extractPiObservations(sessionMessages)) emitObservation(observation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage = extractPiUsage(agentEnd) ?? extractPiUsage(sessionMessages) ?? usage; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (usage) emitObservation({ kind: "usage", usage }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+461
to
+478
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Files matching local-agent-adapters.ts:\n'
fd -a 'local-agent-adapters\.ts$' . || true
file="$(fd 'local-agent-adapters\.ts$' . | head -n1)"
if [ -z "${file:-}" ]; then
printf 'No src/local-agent-adapters.ts found.\n'
exit 0
fi
printf '\nOutline:\n'
ast-grep outline "$file" --view compact | sed -n '1,220p'
printf '\nRelevant lines 430-495:\n'
cat -n "$file" | sed -n '430,495p'
printf '\nSearch for extractPiUsage and Pi usages:\n'
rg -n "extractPiUsage|agent_end|sessionMessages|get_messages|get_session_stats" "$file"Repository: Waishnav/devspace Length of output: 403 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
for pkg in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
if [ -f "$repo/package.json" ]; then
echo "package.json dependency list:"
node - <<'JS'
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('package.json','utf8'));
for (const section of ['dependencies','devDependencies','peerDependencies','optionalDependencies']) {
console.log(section);
if (p[section]) for (const [k,v] of Object.entries(p[section])) if (k.includes('pi') || k.includes('`@earendil`')) console.log(`${k}: ${v}`);
}
JS
fi
done
printf '\nPackage lock mentions:\n'
for f in pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
[ -f "$repo/$f" ] && rg -n "earendil|pi-coding-agent|pi coding agent|`@earendil-works/pi`" "$repo/$f" || true
doneRepository: Waishnav/devspace Length of output: 14517 🌐 Web query:
💡 Result: In the context of the coding-agent (part of the earendil-works/pi repository), RPC mode allows for headless interaction with the agent via a JSON-based protocol over stdin/stdout [1]. The terms you mentioned relate to the session lifecycle and state management events: get_messages This is an RPC command used to retrieve the current collection of messages in the active session [2][1]. When called, the agent returns a response containing the full list of Citations:
🌐 Web query:
💡 Result: In the Citations:
🌐 Web query:
💡 Result: In the earendil-works/pi repository (specifically version 0.80.3), AgentMessage is a core type used to define the conversation history and message abstraction for the agent [1][2][3]. It serves as a union of standard LLM message types (user, assistant, toolResult) and custom application-specific message types, allowing for extensibility while maintaining type safety [1][2]. Within the coding-agent package's RPC mode ( Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\nResolved package cache/source snippets if available:\n'
for f in \
node_modules/@earendil-works/pi-coding-agent/lib/modes/rpc/rpc-types.js \
node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-types.js \
node_modules/@earendil-works/pi-coding-agent/src/modes/rpc/rpc-types.ts; do
if [ -f "$f" ]; then
echo "--- $f"
rg -n "AgentMessage|usage|AgentRun|agent_end|get_messages|completion" "$f" -C 3 || true
fi
done
printf '\nDownloaded npm tarball schema/source snippets:\n'
tmp="$(mktemp -d)"
curl -fsSL https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.80.3.tgz -o "$tmp/pkg.tgz"
tar -tzf "$tmp/pkg.tgz" | rg 'src/modes/rpc/rpc-types|src/modes/rpc/rpc-client|src/.*agent' | head -n 80
echo "--- rpc-types"
tar -xOzf "$tmp/pkg.tgz" "$tmp/pkg.tgz" 2>/dev/null >/tmp/pkg.tgz >/tmp/.err || true
tar -xOzf "$tmp/pkg.tgz" `tar -tzf "$tmp/pkg.tgz" 2>/dev/null | rg 'src/modes/rpc/rpc-types' | head -n1` 2>/dev/null | rg -n "AgentMessage|usage|agent" -C 3 || true
echo "--- likely agent usage files"
for s in `tar -tzf "$tmp/pkg.tgz" 2>/dev/null | rg 'src|lib|dist' | grep -E '(agent|session|usage|token)' | head -n 20`; do
echo "--- $s"
tar -xOzf "$tmp/pkg.tgz" "$s" 2>/dev/null | rg -n "usage|tokens|AgentMessage|agent_end|get_messages" -C 2 || true
doneRepository: Waishnav/devspace Length of output: 257 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
printf 'Relevant lines src/local-agent-adapters.ts:\n'
cat -n src/local-agent-adapters.ts | sed -n '1,120p;380,520p'
printf '\nDefinitions/usages of extractPiUsage and Pi adapter path:\n'
rg -n "extractPiUsage|extractPiObservations|extract.*Usage|createLocalAgentObservationEmitter|provider: 'pi'|`@earendil`|pi-coding-agent|prompt: input.prompt" src/local-agent-adapters.ts src packages tests -g '*.ts' -g '*.tsx' -C 2 || true
printf '\nBehavioral probe of nullish precedence order:\n'
node - <<'JS'
function usedValue(agentEnd, sessionMessages, usage) {
return agentEnd ?? sessionMessages ?? usage;
}
function preferredValue(sessionMessages, agentEnd, usage) {
return sessionMessages ?? agentEnd ?? usage;
}
const cases = [
{agentEnd: {tokens: 1}, sessionMessages: {tokens: 2}, usage: null},
{agentEnd: null, sessionMessages: {tokens: 2}, usage: {tokens: 0}},
{agentEnd: {tokens: 1}, sessionMessages: {tokens: 2}, usage: {tokens: 3}},
];
for (const c of cases) console.log(JSON.stringify({input: c, first: usedValue(c.agentEnd, c.sessionMessages, c.usage), later: preferredValue(c.sessionMessages, c.agentEnd, c.usage)}));
JSRepository: Waishnav/devspace Length of output: 33006 🌐 Web query:
💡 Result: The identifier PiRpcLocalAgentAdapter does not appear to be a standard class or component in the official Pi coding agent repository, but extractPiUsage is a utility function used in community-developed tools, CLI wrappers, and adapters to aggregate token usage and cost data from Pi session logs [1]. In the context of Pi's headless operation (often via --mode rpc) [2][3], session activity is recorded in JSONL files [4][3]. Community projects and custom adapters use functions like extractPiUsage to parse these logs and generate summaries [1]. Key characteristics of extractPiUsage implementations include: 1. Data Source: It processes an array of JSON records (typically parsed from a Pi session.jsonl file) [1][5]. 2. Functionality: It searches for the latest message containing usage statistics (e.g., input tokens, output tokens, cache reads/writes, and costs) [1]. 3. Output: It returns a summary object (often called UsageSummary) that includes totals for tokens and costs [1]. 4. Integration: It is commonly used in extensions or CLI tools to provide usage dashboards or cost tracking [4][6][7]. If you are encountering this in a specific codebase, it is likely part of a custom adapter (such as a Paperclip adapter or a headless CLI wrapper) designed to interface with the Pi coding agent's RPC mode output [8][1][9]. These tools parse the streaming JSONL output or historical session files to reconstruct token consumption for billing or monitoring purposes [4][6][10]. For further implementation details specific to your environment, check the source code of the adapter or extension where the function is defined, as these are often project-specific utilities [1][11]. Citations:
🌐 Web query:
💡 Result: In the earendil-works/pi coding-agent version 0.80.3, UpdateEvent does not exist as a standalone top-level event type in the agent's RPC event stream [1]. Instead, the agent utilizes specific streaming event types for updates during operation, emitted as JSON lines to stdout [1]. The primary event for streaming updates is message_update, which provides deltas (text, thinking, or tool calls) as the model generates content [1]. Additionally, tool_execution_update is used specifically to report progress and partial output during tool execution [1][2][3]. Key streaming event types for agent operations include [1]: - message_update: Contains partial assistant messages (text chunks, thinking deltas, or tool call arguments). - tool_execution_update: Used for streaming output from tool executions. - queue_update: Signifies changes to pending steering or follow-up queues. In the internal implementation, these events are processed by an agent event sink [2][3]. Developers integrating with the agent or building extensions typically listen for these specific event types rather than a generic UpdateEvent [1][4]. If you are working with RPC integrations or custom extensions, refer to the documentation in packages/coding-agent/docs/rpc.md within the repository for the full schema of these events [1]. Citations:
🌐 Web query:
💡 Result: Yes, the get_session_stats function in the Citations:
Use the later Pi usage payload for the final result.
Proposed fix- usage = extractPiUsage(agentEnd) ?? extractPiUsage(sessionMessages) ?? usage;
+ usage = extractPiUsage(sessionMessages) ?? extractPiUsage(agentEnd) ?? usage;📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.45.0)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const finalResponse = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractPiFinalResponse(agentEnd) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractPiFinalResponse(sessionMessages) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -454,6 +493,7 @@ class PiRpcLocalAgentAdapter implements LocalAgentAdapter { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| providerSessionId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finalResponse, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items: [...events, sessionMessages], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| usage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| child.kill(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import assert from "node:assert/strict"; | ||
| import { | ||
| extractAcpObservations, | ||
| extractAcpUsage, | ||
| extractClaudeObservations, | ||
| extractClaudeUsage, | ||
| extractCodexObservations, | ||
| extractCodexUsage, | ||
| extractOpenCodeObservations, | ||
| extractOpenCodeUsage, | ||
| extractPiObservations, | ||
| extractPiUsage, | ||
| } from "./local-agent-provider-observations.js"; | ||
|
|
||
| { | ||
| const item = { | ||
| type: "command_execution", | ||
| id: "cmd-1", | ||
| command: "npm test", | ||
| status: "completed", | ||
| usage: { input_tokens: 12, output_tokens: 8 }, | ||
| }; | ||
| const codexActivity = extractCodexObservations([item]).find((entry) => entry.kind === "activity"); | ||
| assert.equal(codexActivity?.activityId, "cmd-1"); | ||
| assert.equal(codexActivity?.toolStatus, "completed"); | ||
| assert.deepEqual(extractCodexUsage({ usage: { input_tokens: 12, output_tokens: 8 } }), { | ||
| inputTokens: 12, | ||
| outputTokens: 8, | ||
| totalTokens: 20, | ||
| }); | ||
| } | ||
|
|
||
| { | ||
| const messages = [ | ||
| { type: "assistant", content: [{ type: "tool_use", id: "tool-1", name: "bash", input: { command: "pwd" } }] }, | ||
| { type: "user", content: [{ type: "tool_result", tool_use_id: "tool-1", content: "ok" }] }, | ||
| { type: "result", usage: { input_tokens: 20, output_tokens: 4 } }, | ||
| ]; | ||
| assert.deepEqual(extractClaudeObservations(messages).filter((entry) => entry.kind === "activity").map((entry) => entry.toolStatus), ["started", "completed"]); | ||
| assert.deepEqual(extractClaudeUsage(messages), { inputTokens: 20, outputTokens: 4, totalTokens: 24 }); | ||
| } | ||
|
|
||
| { | ||
| const messages = [{ | ||
| info: { tokens: { input: 30, output: 5 } }, | ||
| parts: [{ type: "tool", callID: "call-1", tool: "grep", state: { status: "completed", output: "match" } }], | ||
| }]; | ||
| const openCodeActivity = extractOpenCodeObservations(messages).find((entry) => entry.kind === "activity"); | ||
| assert.equal(openCodeActivity?.toolName, "grep"); | ||
| assert.equal(openCodeActivity?.toolStatus, "completed"); | ||
| assert.deepEqual(extractOpenCodeUsage(messages), { inputTokens: 30, outputTokens: 5, totalTokens: 35 }); | ||
| } | ||
|
|
||
| { | ||
| const event = { type: "tool_result", toolCallId: "pi-1", toolName: "read", status: "success", usage: { input: 4, output: 3 } }; | ||
| const piActivity = extractPiObservations([event]).find((entry) => entry.kind === "activity"); | ||
| assert.equal(piActivity?.activityId, "pi-1"); | ||
| assert.equal(piActivity?.toolStatus, "completed"); | ||
| assert.deepEqual(extractPiUsage(event), { inputTokens: 4, outputTokens: 3, totalTokens: 7 }); | ||
| } | ||
|
|
||
| for (const provider of ["cursor", "copilot"] as const) { | ||
| const update = { | ||
| sessionUpdate: "tool_call_update", | ||
| toolCallId: `${provider}-1`, | ||
| toolName: "edit", | ||
| status: "completed", | ||
| usage_update: { input_tokens: 9, output_tokens: 2 }, | ||
| }; | ||
| const acpActivity = extractAcpObservations(update).find((entry) => entry.kind === "activity"); | ||
| assert.equal(acpActivity?.toolName, "edit"); | ||
| assert.equal(acpActivity?.toolStatus, "completed"); | ||
| assert.deepEqual(extractAcpUsage(update), { | ||
| inputTokens: 9, | ||
| outputTokens: 2, | ||
| totalTokens: 11, | ||
| }); | ||
| } | ||
|
|
||
| console.log("local-agent-provider-observations.test.ts: ok"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Emit collected usage to observation consumers.
Both adapters retain usage for the final result but never send a
kind: "usage"observation.onObservationconsumers therefore receive tool activity but not token usage.src/local-agent-adapters.ts#L119-L125: emit the final collected usage after the Claude message loop.src/local-agent-adapters.ts#L290-L332: emit the final collected usage before returning from the ACP run.Add adapter tests that assert
onObservationreceives the usage record.🧰 Tools
🪛 ast-grep (0.45.0)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
📍 Affects 1 file
src/local-agent-adapters.ts#L119-L125(this comment)src/local-agent-adapters.ts#L290-L332🤖 Prompt for AI Agents