Skip to content

feat(provider): live usage events, available-commands subscription, and slash-command helpers#35

Draft
DaniAkash wants to merge 1 commit into
mainfrom
feat/live-usage-and-slash-commands
Draft

feat(provider): live usage events, available-commands subscription, and slash-command helpers#35
DaniAkash wants to merge 1 commit into
mainfrom
feat/live-usage-and-slash-commands

Conversation

@DaniAkash
Copy link
Copy Markdown
Owner

Draft — depends on openclaw/acpx#345

The runtime fields this PR consumes (cost, breakdown, availableCommands on status events; usage/availableCommands on AcpRuntimeStatus; the four new exported types) land in acpx@0.11.0 via the upstream PR. This downstream PR is wired against that release line — peerDependencies.acpx is bumped to >=0.11.0 and the dev dep is intentionally omitted from package.json until the upstream release lands.

Local install path while the upstream is in review: clone openclaw/acpx#345 somewhere, run pnpm install && pnpm build there, then ln -s <path-to-upstream-worktree> node_modules/acpx in both the workspace root and packages/acpx-ai-provider/. CI will fail at install until acpx@0.11.0 is published — that's expected and the PR is marked draft accordingly.

Summary

Surfaces the new runtime channels to AI SDK consumers and adds the helpers Herbie (or any downstream chat UI) needs to render a live context-window bar and trigger agent-initiated compaction.

const provider = createAcpxProvider({ agent: 'claude' })

provider.events.on('usage', (snapshot) => {
  // { used, size, cost?, breakdown?, at, sessionKey }
  ui.renderContextBar(snapshot.used, snapshot.size)
})

provider.events.on('availableCommands', ({ commands }) => {
  ui.renderCommandPalette(commands) // [{ name, description?, hasInput }]
})

// Trigger /compact when the agent advertises it
if (provider.getAvailableCommands().some((c) => c.name.includes('compact'))) {
  await provider.compact()
}

What changed

AcpxProvider public surface

Addition Purpose
provider.events: EventEmitter<AcpxProviderEvents> Push subscription. 'usage' fires on every usage_update; 'availableCommands' fires on every available_commands_update.
provider.getUsage(sessionKey?) Sync read of the latest AcpxUsageSnapshot for a session.
provider.getAvailableCommands(sessionKey?) Sync read of the latest advertised slash-command list.
provider.runSlashCommand({ name, sessionKey?, agent?, timeoutMs?, signal? }) One-shot prompt whose text is a slash command. Drains the iterator so any follow-on events propagate.
provider.compact({ sessionKey?, agent? }) Resolves the /compact-style command from availableCommands and calls runSlashCommand. Throws when no compact-like command is advertised.
New types: AcpxUsageSnapshot, AcpxProviderEvents Plus re-exports of the four new acpx/runtime types (AcpRuntimeAvailableCommand, AcpRuntimeUsageBreakdown, AcpRuntimeUsageCost, AcpRuntimeSessionUsage).

EventTranslator (in convert-events.ts)

  • New onUsageUpdate(event) and onAvailableCommands(commands) callback hooks on the constructor. The language model wires them into provider.recordUsage / provider.recordAvailableCommands on each turn, which in turn updates the per-session maps and emits on provider.events.
  • handleStatus now translates available_commands_update into a callback fire (previously it was a no-op).
  • lastUsageEvent stores the full usage_update event so finish() can place the context-window ceiling and cost onto providerMetadata.acpx instead of mangling them through LanguageModelV2Usage.

accumulatedUsage() mapping fix

The provider previously transported size (context-window ceiling) through cachedInputTokens. That was a hack — cachedInputTokens is supposed to be the prompt-cache hit count. The new mapping:

Source AI SDK V2 field
_meta.usage.inputTokens inputTokens
_meta.usage.outputTokens outputTokens
_meta.usage.totalTokens (or used fallback) totalTokens
_meta.usage.cachedReadTokens cachedInputTokens
_meta.usage.thoughtTokens reasoningTokens
usage_update.size providerMetadata.acpx.contextWindow
usage_update.cost providerMetadata.acpx.cost

Behavior change for any consumer reading usage.cachedInputTokens to mean "context window size" — switch to providerMetadata.acpx.contextWindow. Documented in the new README section and the migration callout.

Other

  • packages/acpx-ai-provider/package.json — version 0.0.60.1.0; peerDep acpx >=0.11.0; dev dep on acpx removed pending upstream release.
  • biome.json — adds convert-events.ts to the noExcessiveLinesPerFile: off override list (existing pattern for orchestrator files).
  • README — new "Live usage + slash commands" section with code examples and the migration callout; "Known limitations" updated to remove the "no streaming usage updates" caveat.

Test plan

  • bun run typecheck clean across all three packages.
  • bun run lint clean (one pre-existing warning unrelated to this PR).
  • bun test packages/acpx-ai-provider/test/{unit,integration,contract}197 pass / 0 fail across 17 files.
  • New test/unit/usage-events.test.ts covers: usage event emission with cost + breakdown, getUsage() sync read, availableCommands event emission, runSlashCommand happy path + failed-turn error, compact() throws when no compact command is advertised, compact() sends the advertised name as a slash command.
  • test/integration/{do-stream,do-generate,with-stream-text,with-generate-text}.test.ts updated to assert the new providerMetadata.acpx.contextWindow shape instead of the old cachedInputTokens === size hack.
  • CI green — expected to fail at install until acpx@0.11.0 ships from feat(runtime): surface cost, usage breakdown, and available commands on status events and getStatus openclaw/acpx#345.

Coordination with upstream

Out of scope (deliberate)

  • Auto-compact at threshold. Pure UI logic on top of provider.events.on('usage', ...). Belongs in the consumer (Herbie or other), not here.
  • MCP-style command discovery beyond name + description + hasInput. Matches the upstream PR's choice not to plumb AvailableCommandInput through.
  • Per-agent command-name detection beyond compact / condense. compact() matches those two names; consumers wanting different commands can use runSlashCommand with the explicit name from getAvailableCommands().

…nd slash-command helpers

Builds on the runtime additions in openclaw/acpx#345 (cost, breakdown,
availableCommands on status events + AcpRuntimeStatus). Provider work:

- Add EventEmitter on AcpxProvider with 'usage' and 'availableCommands'
  channels. Provider records each event per session into in-memory maps
  and re-emits to subscribers, so multiple consumers (context bar,
  telemetry, auto-compact watcher) can react independently of which
  stream is currently being consumed.
- Sync getters AcpxProvider.getUsage() and getAvailableCommands() return
  the most recent snapshot for a session without spawning the agent.
- New AcpxProvider.runSlashCommand({ name, sessionKey?, agent? }) sends
  a slash-command prompt and drains the resulting turn so any follow-on
  usage_update / available_commands_update events propagate naturally
  through the same event channel.
- AcpxProvider.compact({ sessionKey? }) wraps runSlashCommand and picks
  up the advertised /compact name from the available-commands list.
- EventTranslator gains onUsageUpdate / onAvailableCommands callback
  hooks. The language model wires them into the provider's record/emit
  methods on every turn.
- accumulatedUsage() now reads the breakdown from _meta.usage when the
  agent populates it and falls back to top-level 'used' for totalTokens
  when it doesn't. The previous transport hack that shoehorned 'size'
  into cachedInputTokens is removed. 'size' (context window ceiling) and
  'cost' now ride on providerMetadata.acpx.contextWindow and .cost on
  the finish part.
- New AcpxUsageSnapshot public type plus AcpxProviderEvents map for the
  EventEmitter generic. The acpx/runtime types
  (AcpRuntimeAvailableCommand, AcpRuntimeUsageBreakdown,
  AcpRuntimeUsageCost, AcpRuntimeSessionUsage) are re-exported from
  this package.

Bumps acpx-ai-provider 0.0.6 -> 0.1.0 and the acpx peer dep to >=0.11.0
(the upstream release that will carry the new contract). The dev dep on
acpx is intentionally omitted in this draft because acpx@0.11.0 is not
on npm yet; once openclaw/acpx#345 merges and releases, the dev dep
should be added back as ^0.11.0.

Closes a downstream gap for live context-window display and
agent-initiated compaction UI on top of acpx-ai-provider.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant