@agentproto/adapter-pi 0.3.5 → 0.3.6

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/PI-RPC.md CHANGED
@@ -137,11 +137,22 @@ only the final `agent_end{willRetry:false}` emits `turn-end`.
137
137
  ## Known gaps / assumptions
138
138
 
139
139
  - **`usage_update.size` / `.used`** — pi's per-message `Usage` reports token
140
- totals + cost, but **no context-window size**. The mapper surfaces
141
- `usage.totalTokens` as both `size` and `used`. `tokensIn`/`tokensOut` =
140
+ totals + cost, but **no context-window size**. `client.ts` resolves the
141
+ window once per `connect()` from `@agentproto/model-catalog`'s
142
+ `resolveContextWindow(opts.model)` and the mapper surfaces it as `size`;
143
+ when the model isn't in the catalog, `size` is sent as `0` (this
144
+ codebase's "unknown window" sentinel — the runtime only applies `size`
145
+ when `> 0`, so `contextSize` is left unset rather than defaulted to a
146
+ token count). `used` is `usage.totalTokens`. `tokensIn`/`tokensOut` =
142
147
  `input`/`output`; `cost = { amount: usage.cost.total, currency: "USD" }`.
143
- A future improvement could issue `get_session_stats` (which carries
144
- `contextUsage`) for a true window size, at the cost of an extra round-trip.
148
+ Previously this mapper sent `usage.totalTokens` as BOTH `size` and `used`,
149
+ which pinned `contextPct` at 100% on every turn and tripped the
150
+ context-continuity hard stop immediately — fixed alongside a
151
+ `contextSize === contextUsed` guard in the runtime's
152
+ `computeContextPct` (`packages/runtime/src/context-continuity.ts`). A
153
+ further improvement could issue `get_session_stats` (which carries
154
+ `contextUsage`) for a live `used` figure straight from pi, at the cost of
155
+ an extra round-trip.
145
156
  - **`length` → `max_turns`** — pi's `length` stop reason is a token/context
146
157
  cap, not a turn-count cap. `max_turns` is the closest canonical "budget hit"
147
158
  reason; there is no exact equivalent in the `StreamEvent` taxonomy.
package/dist/index.d.ts CHANGED
@@ -160,6 +160,11 @@ declare function mapStopReason(reason: PiStopReason | undefined): "completed" |
160
160
  * Translate one pi session event into zero or more {@link StreamEvent}s,
161
161
  * updating `state` in place. Pure aside from the state mutation.
162
162
  *
163
+ * `contextWindow` is the model's real context-window size (resolved by the
164
+ * caller from `@agentproto/model-catalog`, once per connection — pi's own
165
+ * per-message `Usage` carries no window figure). `undefined` when the
166
+ * model isn't in the catalog; threaded straight into `usageUpdate`.
167
+ *
163
168
  * `agent_end` (with `willRetry` false) is the turn terminator: it flushes a
164
169
  * `turn-end` whose reason reflects the stop reason accumulated over the turn.
165
170
  * An `agent_end` with `willRetry: true` (auto-retry) does NOT close the turn —
@@ -168,7 +173,7 @@ declare function mapStopReason(reason: PiStopReason | undefined): "completed" |
168
173
  * RPC stdout stream (verified empirically against pi 0.80.3), so it cannot be
169
174
  * relied on as the terminator — see PI-RPC.md.
170
175
  */
171
- declare function mapPiEvent(event: PiSessionEvent, sessionId: string, state: PiMapperState): StreamEvent[];
176
+ declare function mapPiEvent(event: PiSessionEvent, sessionId: string, state: PiMapperState, contextWindow: number | undefined): StreamEvent[];
172
177
 
173
178
  /**
174
179
  * @agentproto/adapter-pi — AIP-45 adapter for **earendil-works/pi**
package/dist/index.mjs CHANGED
@@ -8,6 +8,7 @@ import { tmpdir } from 'os';
8
8
  import { join } from 'path';
9
9
  import { StringDecoder } from 'string_decoder';
10
10
  import { fileURLToPath } from 'url';
11
+ import { resolveContextWindow } from '@agentproto/model-catalog/llm';
11
12
  import process3 from 'process';
12
13
  import { PassThrough } from 'stream';
13
14
 
@@ -18518,21 +18519,25 @@ function mapStopReason(reason) {
18518
18519
  return "completed";
18519
18520
  }
18520
18521
  }
18521
- function usageUpdate(sessionId, usage) {
18522
+ function usageUpdate(sessionId, usage, contextWindow) {
18522
18523
  return {
18523
18524
  kind: "usage_update",
18524
18525
  sessionId,
18525
18526
  // Pi's per-message `Usage` reports token totals but not a context-window
18526
- // size; `totalTokens` is surfaced as both `size` and `used` (documented
18527
- // gap in PI-RPC.md). Cost is pi's own computed USD figure.
18528
- size: usage.totalTokens,
18527
+ // size, so the caller resolves one out-of-band (model catalog, by model
18528
+ // id see client.ts `connect()`). `size: 0` is this codebase's "unknown
18529
+ // window" sentinel (mirrors the `agy` mapper in print-arm.ts): the
18530
+ // runtime's usage_update ingestion only applies `size` when it's `> 0`,
18531
+ // so an unresolved window leaves `contextSize` untouched rather than
18532
+ // being defaulted to `totalTokens` (see PI-RPC.md).
18533
+ size: contextWindow ?? 0,
18529
18534
  used: usage.totalTokens,
18530
18535
  cost: { amount: usage.cost.total, currency: "USD" },
18531
18536
  tokensIn: usage.input,
18532
18537
  tokensOut: usage.output
18533
18538
  };
18534
18539
  }
18535
- function mapPiEvent(event, sessionId, state) {
18540
+ function mapPiEvent(event, sessionId, state, contextWindow) {
18536
18541
  switch (event.type) {
18537
18542
  case "message_update": {
18538
18543
  const inner = event.assistantMessageEvent;
@@ -18586,7 +18591,7 @@ function mapPiEvent(event, sessionId, state) {
18586
18591
  state.lastStopReason = message.stopReason;
18587
18592
  }
18588
18593
  if (message?.usage !== void 0) {
18589
- return [usageUpdate(sessionId, message.usage)];
18594
+ return [usageUpdate(sessionId, message.usage, contextWindow)];
18590
18595
  }
18591
18596
  return [];
18592
18597
  }
@@ -18671,10 +18676,14 @@ function extractPromptText(message) {
18671
18676
  }
18672
18677
  return JSON.stringify(message);
18673
18678
  }
18679
+ function resolvePiContextWindow(modelId) {
18680
+ return modelId ? resolveContextWindow(modelId)?.contextWindow : void 0;
18681
+ }
18674
18682
  function createAgentCliClient(definition) {
18675
18683
  let child;
18676
18684
  let piSessionId;
18677
18685
  let connectEffort;
18686
+ let contextWindow;
18678
18687
  let onActivity;
18679
18688
  let bridgeTempDir;
18680
18689
  const pending = /* @__PURE__ */ new Map();
@@ -18735,7 +18744,7 @@ function createAgentCliClient(definition) {
18735
18744
  onActivity?.();
18736
18745
  const turn = currentTurn;
18737
18746
  if (!turn) return;
18738
- for (const mapped of mapPiEvent(event, piSessionId ?? "", mapperState)) {
18747
+ for (const mapped of mapPiEvent(event, piSessionId ?? "", mapperState, contextWindow)) {
18739
18748
  turn.push(mapped);
18740
18749
  if (mapped.kind === "turn-end") turn.close();
18741
18750
  }
@@ -18784,6 +18793,7 @@ function createAgentCliClient(definition) {
18784
18793
  async connect(opts) {
18785
18794
  onActivity = opts.onActivity;
18786
18795
  connectEffort = opts.effort;
18796
+ contextWindow = resolvePiContextWindow(opts.model);
18787
18797
  const args = ["--mode", "rpc"];
18788
18798
  if (opts.model) args.push("--model", opts.model);
18789
18799
  if (opts.resumeSessionId) args.push("--session", opts.resumeSessionId);