Skip to content

[AAASM-3505] 🐛 (node-sdk): Lazy-load grpc in op-control so SDK import stays grpc-free#173

Merged
Chisanan232 merged 3 commits into
masterfrom
v0.0.1/AAASM-3505/fix/lazy_grpc_op_control
Jun 20, 2026
Merged

[AAASM-3505] 🐛 (node-sdk): Lazy-load grpc in op-control so SDK import stays grpc-free#173
Chisanan232 merged 3 commits into
masterfrom
v0.0.1/AAASM-3505/fix/lazy_grpc_op_control

Conversation

@Chisanan232

Copy link
Copy Markdown
Contributor

Target

  • Task summary:

    Fix a regression from AAASM-3500 / PR [AAASM-3500] ✨ (node-sdk): Wire OpControlSubscriber into withAssembly tool path #172: src/op-control.ts is re-exported from the package entrypoint (OpControlSubscriber), and it imported @grpc/grpc-js (the credentials runtime value) and PolicyServiceClient (from ./proto/generated/policy.js, which also imports @grpc/grpc-js) at module top level. So import '@agent-assembly/sdk' eagerly loaded grpc-js at module-load time. In consumers where grpc-js isn't resolvable from op-control.js's location — the agent-assembly integration-tests node fixture (a file:-linked SDK under pnpm) — the SDK module failed to load:

    Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@grpc/grpc-js'
    imported from .../@agent-assembly/sdk/dist/esm/op-control.js
    

    This took down 4 previously-green agent-assembly E2E tests (aa-integration-tests::e2e_sdk_node::real_lang{chain,graph}_*), turning the monorepo Test CI red. This PR makes the grpc-dependent imports lazy so merely importing/exporting OpControlSubscriber no longer pulls grpc; grpc loads only when a subscriber opens a real channel.

  • Task tickets:

  • Key point change:

    • @grpc/grpc-js is now a type-only import at module scope. The runtime credentials namespace and the PolicyServiceClient value are loaded via await import(...) inside a new private openRealChannel, which runs only on the real-connect path (no clientFactory).
    • The OpControlSignal enum was a runtime value imported from the grpc-importing ./proto/generated/policy.js; importing it as a value would have defeated the lazy-load. It's replaced by inlined numeric wire constants (PAUSE=1, RESUME=2, TERMINATE=3); the enum type is kept (type-only) for signatures.
    • resolveOpControlCredentials now takes the grpc credentials namespace as an injected parameter, so it stays synchronous, pure, and grpc-free; the real-connect path passes the lazily-imported namespace.
    • OpControlSubscriber.connect() stays synchronous and the public API is unchanged. On the real path the channel opens asynchronously; close() is safe to call before the async open lands.
    • The clientFactory test seam never triggers the dynamic import — the mock path is fully grpc-free.

Effecting Scope

  • Action Types:
    • 🔧 Fixing bug
  • Scopes:
    • 🪡 API client and transport
    • 🧪 Testing
      • 🧪 Unit testing
  • Additional description:
    No public API change; ESM + CJS both still build. @grpc/grpc-js stays a regular dependency.

Description

  • src/op-control.ts: grpc-js → type-only import; lazy await import('@grpc/grpc-js') + lazy await import('./proto/generated/policy.js') for PolicyServiceClient, both inside openRealChannel; inlined OpControlSignal numeric constants; injected credentials namespace into resolveOpControlCredentials; client nullable + closed guard for the async open.
  • tests/op-control.test.ts: pass the grpc credentials namespace to resolveOpControlCredentials (the one signature change); all existing pause/resume/terminate/op_id behaviors unchanged and green.
  • tests/packaging/op-control-grpc-lazy-load.test.ts (new regression guard): asserts the built dist/esm/op-control.js has no top-level static grpc value import (only await import), the CJS build has no top-level require('@grpc/grpc-js'), and a fresh Node process requiring the built CJS op-control.js (and constructing a subscriber on the test-seam path) leaves @grpc/grpc-js absent from require.cache.

How to verify

pnpm test (322 passed / 2 skipped), pnpm typecheck, pnpm lint, pnpm build (ESM + CJS) — all green.

Closes AAASM-3505.

🤖 Generated with Claude Code

Chisanan232 and others added 2 commits June 20, 2026 21:13
`op-control.ts` is re-exported from the package entrypoint
(`OpControlSubscriber`, AAASM-3500 / PR #172). It imported the
`@grpc/grpc-js` `credentials` value and the `PolicyServiceClient`
value (from `./proto/generated/policy.js`, which also imports
grpc-js) at module top level. So `import '@agent-assembly/sdk'`
eagerly loaded grpc-js at module-load time, breaking consumers
where grpc-js isn't resolvable from this module's location (a
`file:`-linked SDK under pnpm — the agent-assembly integration-tests
node fixture): `ERR_MODULE_NOT_FOUND: Cannot find package
'@grpc/grpc-js'`. That took down 4 agent-assembly `e2e_sdk_node`
E2E tests.

Make the grpc-dependent runtime values lazy:
- grpc-js becomes a type-only import; the `credentials` namespace and
  `PolicyServiceClient` are `await import`ed inside a new private
  `openRealChannel`, run only on the real-connect path.
- `OpControlSignal` enum (a runtime value from the grpc-importing
  policy module) is replaced by inlined numeric wire constants; the
  enum type is kept type-only.
- `resolveOpControlCredentials` takes the grpc `credentials` namespace
  as an injected param so it stays synchronous and grpc-free.
- `connect()` stays synchronous and public-API-unchanged; the real
  channel opens asynchronously and `close()` is safe before it lands.

The `clientFactory` test seam never touches grpc.

Refs AAASM-3505.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a packaging regression test for AAASM-3505. Two assertions:
- The built `dist/esm/op-control.js` has no top-level static value
  import of `@grpc/grpc-js`, and references it only via `await
  import(...)`; the CJS build has no top-level `require("@grpc/grpc-js")`.
- A fresh Node process requires the built CJS `op-control.js`,
  constructs a subscriber on the `clientFactory` test-seam path, and
  asserts `@grpc/grpc-js` is absent from `require.cache` — proving the
  entrypoint stays grpc-free until a real channel is opened.

Mirrors the existing `langchain-lazy-load` packaging guard.

Refs AAASM-3505.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.36842% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/op-control.ts 97.36% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Chisanan232

Copy link
Copy Markdown
Contributor Author

🔎 Review (Claude Code) — ready for approval (CI failures are acceptance-only)

CI: ✅ Functionally green — 14/16 pass. The 2 failures are acceptance-class only and ignorable per policy:

  • codecov/patch — the new lazy real-connect path (openRealChannel / await import("@grpc/grpc-js")) needs a live gRPC channel, so its new lines aren't unit-covered. Coverage, not correctness.
  • SonarCloud Code Analysis — same new-code-coverage signal.

Functional checks (vitest 322 passed, typecheck, lint, ESM+CJS build, CodeQL) are all green.

Scope vs AAASM-3505: ✅ Fixes the regression at the root. op-control.ts no longer eagerly loads grpc: @grpc/grpc-js (credentials) and the PolicyServiceClient value are now await import(...)-ed inside a private openRealChannel, reached only on the real-connect path; the clientFactory test seam never triggers it. The one tricky bit — OpControlSignal is a runtime enum living in the grpc-importing policy.js — is handled by inlining the stable protobuf wire constants (PAUSE=1/RESUME=2/TERMINATE=3) and keeping the enum type import only. Public API of OpControlSubscriber is unchanged; connect() stays sync. A packaging-guard regression test asserts the built op-control.js has no top-level grpc import and a fresh subprocess require of it leaves @grpc/grpc-js absent from require.cache — robust, non-flaky.

Verdict: Ready to approve & merge. This un-breaks the agent-assembly e2e_sdk_node CI (the ERR_MODULE_NOT_FOUND: @grpc/grpc-js) and unblocks AAASM-3497 / PR #1179 — once this merges, re-running #1179's CI should go green. AAASM-3505 stays open until merge + re-verification.

Exercises the branch the clientFactory seam bypasses (and the lines the
lazy-grpc fix added) by mocking only the dynamically-imported
PolicyServiceClient while using the real grpc-js credentials:
- lazy client build after connect() returns,
- loopback target → insecure creds / non-loopback → TLS, reaching the client,
- subscription opened with the agent identity triple,
- signals on the real stream drive waitForOp + close() propagation,
- close()-before-async-open race guard prevents client construction,
- channel-open failure surfaces as a dead stream (fail-safe), not an
  unhandled rejection.

Takes op-control.ts to 100% line coverage; closes the codecov/patch gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@Chisanan232

Copy link
Copy Markdown
Contributor Author

✅ Coverage follow-up — CI now fully green (16/16)

Added tests/op-control-real-channel.test.ts (4 tests) covering the real-connect path (openRealChannel) that the clientFactory seam bypasses — meaningful behaviour, not just %:

  • lazy client build after connect() returns;
  • loopback → insecure / non-loopback → TLS credentials resolved and reaching PolicyServiceClient (real grpc-js creds, only policy.js mocked);
  • subscription opened with the agent identity triple, signals on the real stream drive waitForOp, and close() propagates (client close + stream cancel + dead);
  • close()-before-async-open race guard prevents client construction;
  • channel-open failure → dead stream (fail-safe), not an unhandled rejection.

op-control.ts100% line / 100% func coverage; full suite 326 passed; lint/typecheck/build clean. codecov/patch + SonarCloud are now green → all 16 checks pass. Ready to approve & merge.

@Chisanan232 Chisanan232 merged commit d72f952 into master Jun 20, 2026
16 checks passed
@Chisanan232 Chisanan232 deleted the v0.0.1/AAASM-3505/fix/lazy_grpc_op_control branch June 20, 2026 13:34
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