Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/cli-workspace.test.ts && tsx src/cli-output.test.ts && tsx src/open-workspace-capabilities.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-capabilities.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-resolution.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skill-install.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts && tsx src/workflow-contracts.test.ts && tsx src/workflow-errors.test.ts && tsx src/workflow-types.test.ts && tsx src/workflow-store.test.ts && tsx src/workflow-lifecycle.test.ts && tsx src/workflow-view.test.ts && tsx src/workflow-summary.test.ts && tsx src/workflow-tui.test.ts && tsx src/workflow-script.test.ts && tsx src/workflow-sandbox.test.ts && tsx src/workflow-engine.test.ts && tsx src/workflow-files.test.ts && tsx src/workflow-launch.test.ts && tsx src/workflow-replay.test.ts && tsx src/workflow-schema.test.ts",
"test": "tsx src/config.test.ts && tsx src/cli-workspace.test.ts && tsx src/cli-output.test.ts && tsx src/open-workspace-capabilities.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-provider-observations.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-capabilities.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-resolution.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skill-install.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts && tsx src/workflow-contracts.test.ts && tsx src/workflow-errors.test.ts && tsx src/workflow-types.test.ts && tsx src/workflow-store.test.ts && tsx src/workflow-lifecycle.test.ts && tsx src/workflow-view.test.ts && tsx src/workflow-summary.test.ts && tsx src/workflow-tui.test.ts && tsx src/workflow-script.test.ts && tsx src/workflow-sandbox.test.ts && tsx src/workflow-engine.test.ts && tsx src/workflow-files.test.ts && tsx src/workflow-launch.test.ts && tsx src/workflow-replay.test.ts && tsx src/workflow-schema.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down
3 changes: 3 additions & 0 deletions src/cli-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const call: WorkflowAgentCallRecord = {
startedAt: now,
completedAt: now,
updatedAt: now,
finalUsage: { inputTokens: 8, outputTokens: 2, totalTokens: 10 },
};
const runJson = workflowRunOutput(run, [call]);
assert.deepEqual(runJson.result, { ok: true });
Expand All @@ -98,11 +99,13 @@ assert.deepEqual(runJson.calls, {
total: 1,
});
assert.equal("scriptHash" in runJson, false);
assert.deepEqual(runJson.usage, { inputTokens: 8, outputTokens: 2, totalTokens: 10 });

const callJson = workflowCallOutput(call, { detailed: true });
assert.deepEqual(callJson.structured, { bugs: [] });
assert.equal("cacheKey" in callJson, false);
assert.equal("providerSessionId" in callJson, false);
assert.equal("profileFingerprint" in callJson, false);
assert.deepEqual(callJson.usage, { inputTokens: 8, outputTokens: 2, totalTokens: 10 });

console.log("cli-output.test.ts: ok");
13 changes: 13 additions & 0 deletions src/cli-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function workflowRunOutput(
resumedFromRunId: run.resumedFromRunId,
cancelRequested: run.cancelRequested,
calls: calls ? workflowCallCounts(calls) : undefined,
usage: calls ? sumUsage(calls.map((call) => call.finalUsage ?? call.usage)) : undefined,
result: parseStoredJson(run.resultJson),
error: run.error
? { kind: run.errorKind, message: parseStoredJson(run.error) }
Expand All @@ -82,6 +83,7 @@ export function workflowCallOutput(
effort: call.effort,
cached: call.fromCache,
durationMs: workflowCallDurationMs(call),
usage: call.finalUsage ?? call.usage,
isolation: call.isolation,
worktree: call.worktreePath
? { path: call.worktreePath, dirty: call.dirty }
Expand Down Expand Up @@ -130,6 +132,17 @@ function workflowCallDurationMs(call: WorkflowAgentCallRecord): number | undefin
return Math.max(0, Date.parse(call.completedAt) - Date.parse(call.startedAt));
}

function sumUsage(calls: Array<WorkflowAgentCallRecord["usage"]>): WorkflowAgentCallRecord["usage"] | undefined {
const result: NonNullable<WorkflowAgentCallRecord["usage"]> = {};
for (const key of ["inputTokens", "outputTokens", "totalTokens", "cacheReadTokens", "cacheWriteTokens"] as const) {
const values = calls
.map((usage) => usage?.[key])
.filter((value): value is number => value !== undefined);
if (values.length > 0) result[key] = values.reduce((total, value) => total + value, 0);
}
return Object.keys(result).length > 0 ? result : undefined;
}

function parseStoredJson(value: string | undefined): unknown {
if (value === undefined) return undefined;
try {
Expand Down
42 changes: 41 additions & 1 deletion src/local-agent-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Comment on lines +119 to +125

Copy link
Copy Markdown

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. onObservation consumers 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 onObservation receives 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local-agent-adapters.ts` around lines 119 - 125, Update
src/local-agent-adapters.ts lines 119-125 in the Claude adapter to emit one
kind: "usage" observation containing the collected usage after the message loop,
and update lines 290-332 in the ACP adapter to emit the same observation before
returning. Add adapter tests verifying onObservation receives the usage record
for both adapters.

const record = message as Record<string, unknown>;
if (typeof record.session_id === "string") providerSessionId = record.session_id;
if (record.type !== "result") continue;
Expand All @@ -124,6 +141,7 @@ class ClaudeLocalAgentAdapter implements LocalAgentAdapter {
providerSessionId,
finalResponse,
items,
usage,
...(structured !== undefined ? { structured } : {}),
};
} catch (error) {
Expand Down Expand Up @@ -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),
Expand All @@ -237,6 +260,7 @@ class OpencodeLocalAgentAdapter implements LocalAgentAdapter {
providerSessionId: sessionId,
finalResponse,
items: [promptResult, messages],
usage,
};
} finally {
server.close();
Expand All @@ -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");
});
Expand Down Expand Up @@ -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);
Expand All @@ -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()}` : ""}`);
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For @earendil-works/pi-coding-agent version 0.80.3 RPC mode, determine whether get_messages returns the latest cumulative token usage after agent_end.

💡 Result:

In @earendil-works/pi-coding-agent version 0.80.3 RPC mode, the get_messages command returns the list of messages in the current conversation [1][2], but it does not return cumulative token usage [3][4]. To retrieve cumulative token usage in RPC mode, you should use the get_session_stats command [3][5]. The response from get_session_stats includes a tokens object containing input, output, cacheRead, cacheWrite, and total token usage for the session [5][6]. The agent_end event, which is emitted when an agent run completes, is also focused on returning generated messages rather than current session-wide cumulative token metrics [7][5].

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
done

Repository: Waishnav/devspace

Length of output: 14517


🌐 Web query:

site:github.com/earendil-works/pi coding-agent rpc get_messages session messages usage agent_end

💡 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 AgentMessage objects [2]. agent_end This is a critical lifecycle event emitted when a low-level agent run completes [3][4]. - It signals that the current processing turn (which may include multiple message updates or tool executions) has finished [3]. - The event payload includes a messages field, which contains all messages generated during that specific run, and a willRetry boolean indicating whether an automatic retry is scheduled [3][5]. - It is distinct from agent_settled, which signifies that the agent is fully idle with no further retries or background tasks remaining [3][5]. Session Usage in RPC In RPC mode, session state is managed via events streamed to stdout and commands sent to stdin [1]. - The agent emits lifecycle events such as agent_start, agent_end, turn_start, and turn_end [3][6]. - Developers can integrate with these by subscribing to the stream or using an RpcClient [4][7]. - For state-heavy applications, commands like get_messages, get_entries, and get_tree provide access to the historical and current state of the session [2][8]. If you are building an integration, it is recommended to monitor the agent_end event to determine when it is safe to interact with the session or to process the final results of an agent run [3]. Note that during certain operations like compaction, some incoming commands may be acknowledged but dropped if the session is not in a ready state [9].

Citations:


🌐 Web query:

@earendil-works pi coding-agent 0.80.3 RPC usage get_message usage

💡 Result:

In the @earendil-works/pi coding-agent (including version 0.80.3), RPC mode allows for headless operation via a JSON-based protocol over stdin/stdout [1]. The get_messages command is used to retrieve all messages in the current conversation [2][3][1]. To use the get_messages command in RPC mode, you send the following JSON object to the agent's standard input (stdin) [2][3]: {"type": "get_messages"} The agent will respond to standard output (stdout) with a JSON object containing the conversation history [2][3]: { "type": "response", "command": "get_messages", "success": true, "data": { "messages": [...] } } The messages returned are in the form of AgentMessage objects [2][3]. RPC mode is intended for embedding the agent in other applications, IDEs, or custom user interfaces [3][1]. For Node.js/TypeScript integrations, the project documentation recommends using the AgentSession SDK directly instead of spawning a subprocess [1].

Citations:


🌐 Web query:

github.com/earendil-works/pi packages/coding-agent src modes rpc rpc-types.ts AgentMessage usage 0.80.3

💡 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 (packages/coding-agent/src/modes/rpc/rpc-types.ts), AgentMessage is used to structure data in responses, particularly when retrieving message history [4]. For instance, a "get_messages" response includes an array of AgentMessage objects [4]. Key points regarding its usage: 1. Definition: AgentMessage is defined in @earendil-works/pi-agent-core (packages/agent/src/types.ts) [1][3]. It allows for declaration merging, enabling users to add custom message types beyond the standard LLM formats [2][3]. 2. RPC Context: In the RPC mode, which facilitates headless operation via JSON-RPC over stdin/stdout, AgentMessage is the primary type used for message-related commands [4][5]. 3. Transformation: Because LLMs natively only understand standard message roles, the AgentMessage objects must be transformed into LLM-compatible formats using a convertToLlm function, which filters out custom or UI-only messages [1][2][6]. 4. Developer Implementation: Developers using the RPC mode can utilize the provided RpcClient or implement their own client to consume these messages [5][7]. The test file packages/coding-agent/test/rpc-example.ts provides an interactive example of how to handle agent events and messages [7]. For further implementation details, you can refer to the official documentation in the repository at packages/coding-agent/docs/rpc.md and the source definition in packages/agent/src/types.ts [5][1].

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
done

Repository: 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)}));
JS

Repository: Waishnav/devspace

Length of output: 33006


🌐 Web query:

"PiRpcLocalAgentAdapter" "extractPiUsage"

💡 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:

@earendil-works pi-coding-agent 0.80.3 source usage UpdateEvent

💡 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:

@earendil-works pi coding-agent get_session_stats returns usage tokens

💡 Result:

Yes, the get_session_stats function in the @earendil-works/pi coding-agent returns comprehensive usage token information [1][2]. When called, this RPC method returns a payload containing detailed session statistics, including a nested tokens object that breaks down usage by category [1][3]: tokens: { input: number, output: number, cacheRead: number, cacheWrite: number, total: number } These counts represent cumulative usage across the full session, including assistant messages, usage reported by tools, and overhead from compaction or branch-summary generation [1][4]. In addition to these cumulative token totals, the response also includes a contextUsage field, which provides the actual current context-window estimate used for compaction and display [1][5]. This field was explicitly added to allow RPC clients to track current window usage separately from the monotonically increasing cumulative token counts [5]. If no model or context window is available, the contextUsage field may be omitted, or its properties may be null (for example, immediately following compaction) [1][6].

Citations:


Use the later Pi usage payload for the final result.

get_messages is fetched after agent_end, so this precedence can keep an older usage record when both payloads contain usage. Reverse the operands before emitting usage.

Proposed fix
-      usage = extractPiUsage(agentEnd) ?? extractPiUsage(sessionMessages) ?? usage;
+      usage = extractPiUsage(sessionMessages) ?? extractPiUsage(agentEnd) ?? usage;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 });
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(sessionMessages) ?? extractPiUsage(agentEnd) ?? usage;
if (usage) emitObservation({ kind: "usage", usage });
🧰 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)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local-agent-adapters.ts` around lines 461 - 478, Update the final usage
assignment in the request flow around agentEnd and sessionMessages so
extractPiUsage(sessionMessages) takes precedence over extractPiUsage(agentEnd),
while retaining the existing usage fallback. Keep the subsequent usage
observation emission unchanged.

const finalResponse =
extractPiFinalResponse(agentEnd) ||
extractPiFinalResponse(sessionMessages) ||
Expand All @@ -454,6 +493,7 @@ class PiRpcLocalAgentAdapter implements LocalAgentAdapter {
providerSessionId,
finalResponse,
items: [...events, sessionMessages],
usage,
};
} finally {
child.kill();
Expand Down
80 changes: 80 additions & 0 deletions src/local-agent-provider-observations.test.ts
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");
Loading
Loading