@byok-sdk/client 0.1.0 → 0.2.0

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.
@@ -1,44 +1,17 @@
1
1
  import type { AgentEvent } from '@byok-sdk/protocol';
2
2
  import type { PiRpcMessage } from './rpc-client';
3
3
  /**
4
- * ROOT CAUSE of the 2026-07-16 live GLM run's "finding #2" hang (task stuck
5
- * `Running` forever after pi streamed its final answer; pi process alive,
6
- * idle, no sockets, only stdio pipes): this mapper used to listen for a pi
7
- * event type `agent_settled` to know a whole run had finished. That type
8
- * does not exist anywhere in the real, installed
9
- * `@earendil-works/pi-coding-agent@0.74.2` package — not in its bundled
10
- * `docs/rpc.md`, not in `dist/modes/rpc/rpc-types.d.ts`, not in
11
- * `pi-agent-core`'s own `AgentEvent` union (`dist/types.d.ts`), and not
12
- * observed even once across live probes against real GLM traffic (raw JSONL
13
- * frame capture). The real "whole run is done, pi is idle again" signal is
14
- * `agent_end` — confirmed both by `pi-agent-core`'s own doc comment ("
15
- * `agent_end` is the last event emitted for a run... The agent becomes idle
16
- * only after those listeners finish") and empirically: a live run's frame
17
- * sequence ended `...turn_end (tool call), ..., turn_end (final text),
18
- * agent_end` with nothing further arriving even after an 8s idle-grace
19
- * window. Because this switch had no `agent_end` case, it fell to
20
- * `default: return undefined` — silently dropped — so `task-runner.ts`'s
21
- * `pump()` loop never saw a `turn_end` `AgentEvent` and blocked forever on
22
- * the next one. This repo's own fixture (`fake-pi.mjs`) masked the bug for
23
- * the whole M0/M1 test suite by emitting a fictional `agent_settled` frame
24
- * of its own alongside the real `agent_end` one — fixed alongside this
25
- * change (see fake-pi.mjs's doc comment).
4
+ * Map pi 0.84.1 RPC frames into BYOK's runtime-neutral event contract.
26
5
  *
27
- * `agent_end` (not pi's own per-LLM-turn `turn_end`) is what maps to our
28
- * `turn_end`: a single pi prompt can produce several internal turns (tool
29
- * round-trips, each ending its own real `turn_end`); only once the whole
30
- * run is settled (no auto-retry, compaction-retry, or queued continuation
31
- * left) is the task actually done. Forwarding pi's own `turn_end` 1:1 would
32
- * emit multiple confusing `turn_end`s for what the daemon must treat as one
33
- * task — empirically confirmed: a single-tool-call prompt produced two real
34
- * `turn_end` frames (one after the tool call, one after the final text) and
35
- * exactly one `agent_end`.
6
+ * `agent_settled` is the only whole-task completion authority. `agent_end`
7
+ * ends one low-level agent run, but pi may still perform retry/compaction or
8
+ * consume queued work before it emits `agent_settled`. Pi's own `turn_end`
9
+ * is even narrower: a tool-using prompt can emit several of them. Mapping
10
+ * either earlier boundary to BYOK `turn_end` would acknowledge the task
11
+ * before the runtime is actually idle.
36
12
  *
37
- * Returns undefined for pi messages with no protocol equivalent (session/
38
- * compaction/retry bookkeeping see `ROUTINE_PI_EVENT_TYPES` below —
39
- * extension UI dialogs, which never reach here at all in production since
40
- * `PiRpcClient` answers them itself before they'd ever be queued as an
41
- * event; see rpc-client.ts).
13
+ * `message_update` is delta-only in this contract. The mapper forwards text
14
+ * deltas and never reads the removed cumulative `message`/`partial` fields.
42
15
  */
43
16
  export declare function mapPiMessageToAgentEvent(msg: PiRpcMessage): AgentEvent | undefined;
44
17
  /**
@@ -48,7 +21,7 @@ export declare function mapPiMessageToAgentEvent(msg: PiRpcMessage): AgentEvent
48
21
  * (pi-adapter.ts) calls `PiRpcClient.recordUnmappedFrame` for any message
49
22
  * type that maps to `undefined` AND isn't in this set — i.e. traffic nobody
50
23
  * has ever told this adapter to expect. That distinction is what makes a
51
- * regression like this file's root-cause bug (`agent_end` going unhandled)
24
+ * regression like a changed completion event
52
25
  * self-diagnosing: a warning fires the first time the new/renamed settle
53
26
  * event shows up, instead of the daemon just quietly hanging. `default`-only
54
27
  * unknowns (a type not listed in the switch at all) are equally "not
@@ -8,7 +8,7 @@ export interface PiPermissionMapping {
8
8
  }
9
9
  /**
10
10
  * Map an effective {@link PermissionPolicy} to `pi --mode rpc` CLI args,
11
- * fail-closed. Empirically grounded (see the M0-3 report):
11
+ * fail-closed against the exact pi 0.84.1 CLI contract:
12
12
  *
13
13
  * - `auto` / `readonly` are expressible via `--tools` / `--no-tools`.
14
14
  * - `confirm` and `plan` are NOT expressible: pi ships no built-in per-call
@@ -23,24 +23,8 @@ export interface PiPermissionMapping {
23
23
  * Workspace confinement is NOT a pi flag — the caller spawns pi with
24
24
  * `cwd: ctx.workspaceDir`, the daemon-created per-task directory.
25
25
  *
26
- * SECOND SELF-DISCOVERED FINDING, same class as the pi-adapter.ts
27
- * `--session-id` bug (this task, while hardening argv validation):
28
- * `--exclude-tools` what `denyTools` used to map to — is ALSO not a real
29
- * pi CLI flag. Confirmed against real pi 0.74.2: `pi --mode rpc
30
- * --exclude-tools bash` → `Error: Unknown option: --exclude-tools`, exit
31
- * code 1, before any model call — and it's absent from both `pi --help`'s
32
- * own listing and the CLI's actual arg parser (`dist/cli/args.js`), which
33
- * recognizes only `--tools`/`-t`, `--no-tools`/`-nt`, `--no-builtin-tools`/
34
- * `-nbt` for tool control. This crashed EVERY real pi invocation for any
35
- * policy with a non-empty `denyTools`, unconditionally — never caught by
36
- * this repo's own test suite for the exact same reason as the
37
- * `--session-id` bug (`fake-pi.mjs` never validated argv; fixed alongside
38
- * this change). pi has no "start from the default set, minus these" flag at
39
- * all — `--tools` always REPLACES the active set wholesale — so `denyTools`
40
- * is now resolved to an equivalent allowlist in-process: readonly mode
41
- * intersects with `READONLY_TOOLS` (as before), any other mode starts from
42
- * an explicit `allowTools` or pi's own `DEFAULT_ACTIVE_TOOLS`, and
43
- * `denyTools` is subtracted from that resolved set before it's ever handed
44
- * to pi as a single `--tools`/`--no-tools` pair of args.
26
+ * - `--tools` is an allowlist and `--exclude-tools` is a denylist. Passing
27
+ * both lets pi remain the authority for its active tool registry instead
28
+ * of duplicating pi's default tool list in this adapter.
45
29
  */
46
30
  export declare function mapPermissionPolicyToPiArgs(policy: PermissionPolicy): PiPermissionMapping;
@@ -5,40 +5,34 @@
5
5
  * `@mariozechner/pi` — the identifier this task was originally briefed with —
6
6
  * is NOT the coding agent. On npm it resolves to an unrelated "CLI tool for
7
7
  * managing vLLM deployments on GPU pods" (bin: `pi-pods`). The real coding
8
- * agent was `@mariozechner/pi-coding-agent`, which is now itself deprecated
9
- * in favor of this package (same maintainers: badlogic, mitsuhiko), as of
10
- * literally the day before this was written. `package.json` pins the
11
- * `legacy-node20` dist-tag (0.74.2) rather than `latest` (0.80.7), because
12
- * `latest` requires Node >=22.19 while this SDK's baseline is Node >=20;
13
- * both versions were empirically confirmed to speak the identical RPC-mode
14
- * frame shapes this adapter depends on.
8
+ * agent was `@mariozechner/pi-coding-agent`, which is now deprecated in
9
+ * favor of this package. `package.json` carries the exact supported version
10
+ * as a required dependency; pi is a core BYOK capability, not an optional
11
+ * enhancement or an unversioned global executable.
15
12
  */
16
13
  export declare const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
17
14
  export interface ResolvedBin {
18
15
  command: string;
19
- source: 'package' | 'path';
16
+ source: 'package' | 'env';
20
17
  }
21
18
  /**
22
- * Resolve the pi CLI executable. Prefers the optionalDependency installed
23
- * alongside @byok-sdk/client (guarantees a known-good, version-matched build);
24
- * falls back to whatever `pi` is on PATH so users with a pre-existing global
25
- * pi install still work even when the optionalDependency didn't install.
19
+ * Resolve the pi CLI executable from the required package installed alongside
20
+ * `@byok-sdk/client`. There is intentionally no automatic PATH fallback: a
21
+ * global `pi` would create a second, unversioned authority for this contract.
26
22
  *
27
- * `BYOK_PI_BIN` overrides both of the above when set: `PiAdapterOptions.resolveBin`
23
+ * `BYOK_PI_BIN` explicitly overrides the package when set: `PiAdapterOptions.resolveBin`
28
24
  * is the injectable seam for in-process tests, but the `byok-agent` CLI bin
29
25
  * only ever constructs `new PiAdapter()` with no options (see `createDaemon`),
30
26
  * so an out-of-process substitution (e.g. examples/basic's e2e run swapping
31
- * in the fake-pi fixture ahead of a real pi install) has no other seam to use.
27
+ * in the fake-pi fixture, or a single-file product injecting its required
28
+ * Node 22.19+ pi sidecar) has no other seam to use.
32
29
  *
33
30
  * Deliberately does NOT use `createRequire(...).resolve()`: this package is
34
31
  * pure ESM with no `require` export condition (`exports["."]` only offers
35
- * `import`), so CJS-style resolution fails outright with
36
- * `ERR_PACKAGE_PATH_NOT_EXPORTED` empirically confirmed, not a hypothetical.
37
- * It also does NOT resolve the `./package.json` subpath directly (also not
38
- * exported by this package); instead it resolves the package's main entry
39
- * via `import.meta.resolve` and walks up parent directories to find the
40
- * package root (identified by a `package.json` whose `name` matches), which
41
- * is robust to `dist/` layout changes and to whether `./package.json` is
42
- * ever exported.
32
+ * `import`), so CJS-style resolution fails with
33
+ * `ERR_PACKAGE_PATH_NOT_EXPORTED`. It also does NOT resolve the
34
+ * `./package.json` subpath directly (also not exported); instead it resolves
35
+ * the package's main entry via `import.meta.resolve` and walks upward to the
36
+ * package root identified by its manifest name.
43
37
  */
44
38
  export declare function resolvePiBin(): ResolvedBin;
@@ -58,10 +58,7 @@ export declare class PiRpcClient {
58
58
  * traffic. Logs once per distinct type (not per occurrence, so a
59
59
  * repeating unmapped type can't spam stdout); the running tally is also
60
60
  * folded into this client's exit-time error message (`buildExitError`) so
61
- * a post-mortem on a failed/hung task has it without needing separate log
62
- * scraping. This is the exact mechanism that would have turned this
63
- * task's root-cause hang (`agent_end` arriving with no mapping) into a
64
- * one-line, immediate warning instead of a silent stall.
61
+ * a post-mortem on a failed/hung task has it without separate log scraping.
65
62
  */
66
63
  recordUnmappedFrame(type: string): void;
67
64
  /** Best-effort teardown. SIGTERM on POSIX; `taskkill /T /F` on Windows to also reap child processes pi itself spawned (e.g. bash). */
@@ -714,7 +714,7 @@ function readPackageJson(dir) {
714
714
  function resolvePiBin() {
715
715
  const override = process.env.BYOK_PI_BIN;
716
716
  if (override) {
717
- return { command: override, source: "path" };
717
+ return { command: override, source: "env" };
718
718
  }
719
719
  try {
720
720
  const mainEntryUrl = import.meta.resolve(PI_PACKAGE_NAME);
@@ -732,14 +732,19 @@ function resolvePiBin() {
732
732
  if (parent === dir) break;
733
733
  dir = parent;
734
734
  }
735
- } catch {
735
+ } catch (cause) {
736
+ throw new Error(
737
+ `Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`,
738
+ { cause }
739
+ );
736
740
  }
737
- return { command: "pi", source: "path" };
741
+ throw new Error(
742
+ `Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`
743
+ );
738
744
  }
739
745
 
740
746
  // src/adapters/pi/permission-mapping.ts
741
747
  var READONLY_TOOLS = ["read", "grep", "find", "ls"];
742
- var DEFAULT_ACTIVE_TOOLS = ["read", "bash", "edit", "write"];
743
748
  function mapPermissionPolicyToPiArgs(policy) {
744
749
  if (policy.network === false) {
745
750
  return {
@@ -758,22 +763,18 @@ function mapPermissionPolicyToPiArgs(policy) {
758
763
  const denyTools = policy.denyTools ?? [];
759
764
  if (policy.mode === "readonly") {
760
765
  const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS.includes(tool)) : [...READONLY_TOOLS];
761
- const effective = subtractDenied(base, denyTools);
762
- return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
763
- }
764
- if (denyTools.length > 0) {
765
- const base = policy.allowTools && policy.allowTools.length > 0 ? policy.allowTools : [...DEFAULT_ACTIVE_TOOLS];
766
- const effective = subtractDenied(base, denyTools);
767
- return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
766
+ if (base.length === 0) return { ok: true, args: ["--no-tools"] };
767
+ return {
768
+ ok: true,
769
+ args: ["--tools", base.join(","), ...denyTools.length > 0 ? ["--exclude-tools", denyTools.join(",")] : []]
770
+ };
768
771
  }
772
+ const args = [];
769
773
  if (policy.allowTools && policy.allowTools.length > 0) {
770
- return { ok: true, args: ["--tools", policy.allowTools.join(",")] };
774
+ args.push("--tools", policy.allowTools.join(","));
771
775
  }
772
- return { ok: true, args: [] };
773
- }
774
- function subtractDenied(tools, denyTools) {
775
- const denied = new Set(denyTools);
776
- return tools.filter((tool) => !denied.has(tool));
776
+ if (denyTools.length > 0) args.push("--exclude-tools", denyTools.join(","));
777
+ return { ok: true, args };
777
778
  }
778
779
 
779
780
  // src/adapters/pi/events.ts
@@ -798,7 +799,7 @@ function mapPiMessageToAgentEvent(msg) {
798
799
  output: { result: msg.result, isError: msg.isError === true }
799
800
  };
800
801
  }
801
- case "agent_end":
802
+ case "agent_settled":
802
803
  return { type: "turn_end" };
803
804
  /**
804
805
  * `artifact` is NOT a real pi RPC message — pi's own `write` tool only
@@ -834,16 +835,22 @@ function mapPiMessageToAgentEvent(msg) {
834
835
  // `recordUnmappedFrame`) can tell "known, expected, silently ignored"
835
836
  // apart from "genuinely never seen before" (falls to `default` below).
836
837
  case "agent_start":
838
+ case "agent_end":
839
+ // one low-level run; `agent_settled` is BYOK completion
837
840
  case "turn_start":
838
841
  case "turn_end":
839
- // pi's own per-LLM-turn boundary, not ours — see `agent_end` above
842
+ // pi's own per-LLM-turn boundary, not ours
840
843
  case "message_start":
841
844
  case "message_end":
845
+ case "bash_execution_update":
842
846
  case "tool_execution_update":
843
847
  case "queue_update":
844
848
  case "compaction_start":
845
849
  case "compaction_end":
846
850
  case "auto_retry_start":
851
+ case "summarization_retry_scheduled":
852
+ case "summarization_retry_attempt_start":
853
+ case "summarization_retry_finished":
847
854
  case "session_info_changed":
848
855
  case "thinking_level_changed":
849
856
  return void 0;
@@ -853,15 +860,20 @@ function mapPiMessageToAgentEvent(msg) {
853
860
  }
854
861
  var ROUTINE_PI_EVENT_TYPES = /* @__PURE__ */ new Set([
855
862
  "agent_start",
863
+ "agent_end",
856
864
  "turn_start",
857
865
  "turn_end",
858
866
  "message_start",
859
867
  "message_end",
868
+ "bash_execution_update",
860
869
  "tool_execution_update",
861
870
  "queue_update",
862
871
  "compaction_start",
863
872
  "compaction_end",
864
873
  "auto_retry_start",
874
+ "summarization_retry_scheduled",
875
+ "summarization_retry_attempt_start",
876
+ "summarization_retry_finished",
865
877
  "session_info_changed",
866
878
  "thinking_level_changed"
867
879
  ]);
@@ -995,10 +1007,7 @@ var PiRpcClient = class {
995
1007
  * traffic. Logs once per distinct type (not per occurrence, so a
996
1008
  * repeating unmapped type can't spam stdout); the running tally is also
997
1009
  * folded into this client's exit-time error message (`buildExitError`) so
998
- * a post-mortem on a failed/hung task has it without needing separate log
999
- * scraping. This is the exact mechanism that would have turned this
1000
- * task's root-cause hang (`agent_end` arriving with no mapping) into a
1001
- * one-line, immediate warning instead of a silent stall.
1010
+ * a post-mortem on a failed/hung task has it without separate log scraping.
1002
1011
  */
1003
1012
  recordUnmappedFrame(type) {
1004
1013
  const next = (this.unmappedFrameCounts.get(type) ?? 0) + 1;
@@ -1139,8 +1148,8 @@ var PiAdapter = class {
1139
1148
  options;
1140
1149
  id = "pi";
1141
1150
  async detect() {
1142
- const bin = this.resolveBin();
1143
1151
  try {
1152
+ const bin = this.resolveBin();
1144
1153
  const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
1145
1154
  const version = stdout.trim() || stderr.trim();
1146
1155
  const authPresent = KNOWN_PROVIDER_ENV_VARS.some((name) => process.env[name] !== void 0);
@@ -1317,7 +1326,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
1317
1326
  }
1318
1327
  if (policy.mode === "readonly") {
1319
1328
  const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS2.includes(tool)) : [...READONLY_TOOLS2];
1320
- const effective = subtractDenied2(base, denyTools);
1329
+ const effective = subtractDenied(base, denyTools);
1321
1330
  return { ok: true, args: ["--permission-mode", "default", "--tools", effective.join(",")] };
1322
1331
  }
1323
1332
  if (denyTools.length > 0) {
@@ -1334,7 +1343,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
1334
1343
  }
1335
1344
  return { ok: true, args };
1336
1345
  }
1337
- function subtractDenied2(tools, denyTools) {
1346
+ function subtractDenied(tools, denyTools) {
1338
1347
  const denied = new Set(denyTools);
1339
1348
  return tools.filter((tool) => !denied.has(tool));
1340
1349
  }
@@ -2274,9 +2283,10 @@ var CodexAdapter = class {
2274
2283
  * Two independently-verified channel gotchas apply here, the "pi lesson"
2275
2284
  * yet again:
2276
2285
  * - `codex login status`'s human-readable "Logged in using ChatGPT"
2277
- * message prints on STDERR, not stdout (the opposite-channel
2278
- * counterpart of pi's own `--version`-goes-to-stderr surprise) both
2279
- * streams are checked here for exactly that reason.
2286
+ * message prints on STDERR, not stdout both streams are checked
2287
+ * here for exactly that reason. pi's `--version` is the same class of
2288
+ * hazard from the other direction: its channel has moved between pi
2289
+ * releases (see ../pi/pi-adapter.ts), so neither stream is assumed.
2280
2290
  * - The NOT-logged-in message/exit-code shape was deliberately never
2281
2291
  * empirically tested: this machine has a real, live ChatGPT login, and
2282
2292
  * running `codex logout` to observe the negative case would have