Skip to content

feat(wrappers): Phase 2.2 + 2.3 + 2.5 — TypeScript + Python wrappers + cross-language conformance#7

Merged
manojp99 merged 28 commits into
mainfrom
feat/phase-2-2-2-3-2-5-wrappers-and-conformance
May 21, 2026
Merged

feat(wrappers): Phase 2.2 + 2.3 + 2.5 — TypeScript + Python wrappers + cross-language conformance#7
manojp99 merged 28 commits into
mainfrom
feat/phase-2-2-2-3-2-5-wrappers-and-conformance

Conversation

@manojp99

@manojp99 manojp99 commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Phases 2.2 + 2.3 + 2.5 of the AaA v2 design (docs/designs/2026-05-20-aaa-v2-wrapper-and-wire.md). Ships both language wrappers, the shared conformance harness, and the cross-language parity lint that consumes the YAML wire-sequence fixtures from PR #6.

This is the bottom layer of the §4 locked architecture — what host adapters (Phase 2.4 NanoClaw, Phase 2.5 Paperclip) will consume to talk to amplifier-agent without knowing about JSON-RPC, L14 synthesis, version skew, or subprocess plumbing.

Stacked on PR #6. This branch was developed off feat/phase-2-1-wire-spec-hardening while PR #6 was open. After PR #6 merges, this branch must be rebased onto new main (no logical conflicts expected — the file sets are orthogonal).

What ships

TypeScript wrapper (wrappers/typescript/)

  • npm package amplifier-agent-client-ts
  • ESM-only; targets Node ≥20
  • Toolchain: pnpm + vitest
  • 11 test files, 39 unit tests
  • Modules: spawn, transport (NDJSON), jsonrpc (per-id), session, approval, display, version, l14, info, types (generated from protocol/schemas/*.schema.json)

Python wrapper (wrappers/python/)

  • PyPI package amplifier-agent-client-py (separate distributable from amplifier_agent_lib)
  • Mirrors TS structure idiomatically; same public API
  • types.py re-exports from amplifier_agent_lib.protocol (single source of truth)

Cross-language conformance (wrappers/conformance/)

  • runner_ts.ts and runner_py.py — scripted-replay drivers that load YAML fixtures from src/amplifier_agent_lib/protocol/conformance/fixtures/, drive their respective wrapper against a real engine subprocess, and capture wire frames
  • parity_lint.py — asserts both runners produce equivalent assertion outcomes against all 5 fixtures
  • Validates L14 synthesis, capability negotiation, sub-agent lineage, version skew (strict-refuse branch), --resume continuity

Engine-side addition

  • New amplifier-agent version --json subcommand at src/amplifier_agent_cli/admin/version_info.py
  • Required so wrappers can probe engine's PROTOCOL_VERSION before spawning a turn
  • ~40 LOC, no behavioral impact on existing flows

Public API (verbatim from design §8)

async function spawnAgent(params: {
  lifecycle: 'one-shot';
  sessionId: string;
  resume?: boolean;
  cwd?: string;
  env?: { allowlist: string[]; extra?: Record<string, string> };
  approval?: { onRequest, timeoutMs };
  display?: { onEvent, subagentEvents?: 'all' | 'none' };
}): Promise<SessionHandle>;

Python wrapper mirrors with snake_case.

Per-task ceremony (carried over from Plan 2)

  • 15 logical tasks → 16 commits (paired-language tasks; one commit per task plus one fixup)
  • Each paired task ships TS + Py implementations + tests in BOTH languages in a single task body
  • ~80–120 lines per task; 3 batched quality checkpoints instead of per-task
  • TDD discipline maintained throughout (test-first, both languages)

Test Plan

  • Python test suite: 440 passed, 2 skipped in 52.95s (uv run python -m pytest tests/ wrappers/python/tests/ -q)
  • Vitest (TS): 39 tests / 11 files passed (pnpm test --run)
  • Ruff: All checks passed! across src/, tests/, wrappers/
  • Pyright: 0 errors, 0 warnings, 0 informations
  • Conformance harnesses: both languages pass all 5 YAML fixtures (with version_skew.yaml strict-refuse branch only; override branch still pending per PR feat(protocol): Phase 2.1 — wire spec hardening (generator, schemas, conformance fixtures) #6 follow-up)
  • Parity lint: TS and Py runners produce equivalent assertion outcomes
  • Exit gate: real-engine E2E test gated by AMPLIFIER_AGENT_E2E env var (intentional skip without it)

Note: Phase 2.1's 4 environmental test failures (test_delegation_e2e, test_phase_2_0c_exit_gate_real_turn_emits_result_events, test_resume_continuity_two_turns_share_context) now pass under this branch. The infra changes (pnpm workspace, new admin command, etc.) appear to have resolved the cache-state drift.

Known follow-ups (NOT blockers)

  • version_skew.yaml override branch still missing (Plan 2 follow-up; tracked).
  • node_modules/ is currently untracked — add to .gitignore if not already excluded.
  • Wrapper publish to npm/PyPI registries is a separate release process (not in this PR).

Out of scope (next plans)

  • NanoClaw adapter integration (Phase 2.4)
  • Paperclip adapter integration (Phase 2.5 — adapter rebuild)
  • Turnkey installers (Phase 2.6)
  • Container Dockerfiles (Phase 2.7)
  • OpenClaw skill drop (Phase 2.8)

References

Generated with Amplifier

Manoj Prabhakar Paidiparthy and others added 28 commits May 20, 2026 20:08
- Add _is_typed_dict(), _discover_typed_dicts(), _write_error_codes_schema()
  to protocol/_gen.py
- Update main() to iterate all TypedDicts across methods, notifications,
  and capabilities modules and write one *.schema.json per TypedDict
- Add error_codes.schema.json enumerating all ErrorCode StrEnum values
- Generate schemas/ directory with 30 TypedDict schemas + error_codes.schema.json
- Add schemas/__init__.py marker file
- Add two new tests: test_gen_emits_schema_for_every_typeddict and
  test_gen_error_codes_schema_is_string_enum
- Add wrappers/python/pyproject.toml: amplifier-agent-client v0.0.0,
  hatchling build backend, pytest-asyncio strict mode, ruff 120/py312
- Add wrappers/python/src/amplifier_agent_client/__init__.py:
  exports only PROTOCOL_VERSION_REQUIRED_BY_WRAPPER = '2026-05-aaa-v0'
- Add wrappers/python/tests/test_smoke.py: async smoke test asserting
  the protocol version constant is importable and correct
- Update root pyproject.toml workspace members to include wrappers/python

Co-authored-by: Amplifier <amplifier@microsoft.com>
Implement Transport class for both TypeScript and Python wrappers.
Each Transport spawns a child process, exchanges JSON frames as NDJSON
over its stdio, drains stderr to an optional sink, and terminates cleanly.

TypeScript (wrappers/typescript/src/transport.ts):
- spawn() with stdio ['pipe','pipe','pipe'], readline on stdout/stderr
- onFrame(cb): register callbacks for parsed JSON frames
- send(obj): writes JSON.stringify(obj) + '\n' to stdin
- terminate(): sends SIGTERM, awaits child 'close' (exitPromise)
- Non-JSON stdout lines logged to stderr sink and dropped silently
- ExitInfo {code, signal}, TransportOptions {command, args, env, cwd?, stderr?}

Python (wrappers/python/src/amplifier_agent_client/transport.py):
- start(): asyncio.create_subprocess_exec with PIPEs
- frames(): async generator via asyncio.Queue + 0.1s poll timeout;
  exits when _stdout_done is set and queue is empty
- send(obj): json.dumps(obj) + '\n' encoded then drain()
- terminate(): proc.terminate(), wait up to 5s, fallback proc.kill()
- _read_stdout: async for loop with try/except; drops non-JSON silently
- _drain_stderr: drains stderr to optional sink

Defensive requirement (MCP-style tolerance): non-JSON stdout lines are
logged to the stderr sink (or process.stderr/sys.stderr) and dropped
silently - never raised. Matches engine pattern at jsonrpc.py.

Tests:
- wrappers/typescript/test/transport.test.ts: 3 vitest cases
- wrappers/python/tests/test_transport.py: 3 pytest-asyncio cases

All 6 tests pass (3/3 TS, 3/3 Py). No regressions.
- TS: JsonRpcClient at wrappers/typescript/src/jsonrpc.ts
  - TransportLike interface (send/onFrame), Notification interface, RequestHandler type
  - call(): allocates request id, creates Promise, sends {jsonrpc:'2.0',id,method,params}
  - dispatch(): routes response→resolve pending, server-request→handler+response, notification→fanout
  - Unknown server methods return -32601 error
  - NC-L16 designed out: each call() has independent Promise row in pending Map
- Py: JsonRpcClient at wrappers/python/src/amplifier_agent_client/jsonrpc.py
  - _TransportLike Protocol (send/on_frame), RequestHandler type alias
  - call(): uses asyncio.get_running_loop().create_future() for per-id isolation
  - _dispatch(): routes by frame keys with task lifecycle management (RUF006)
  - _handle_request: awaits handler, sends result or error back
- Tests: 5 cases each (TS + Py): call resolves, concurrent calls no interference,
  notifications fanout, server request dispatched, unknown method -32601 error
Implements Task 7: SessionHandle.submit(prompt) returns
AsyncIterable<DisplayEvent> (TS) / AsyncIterator[DisplayEvent] (Py).

Each submit() call:
- Sends turn/submit JSON-RPC request
- Yields every display/event notification received
- Terminates when result/final notification arrives OR when
  turn/submit JSON-RPC response arrives (whichever first)
- Throws AaaError (TS) / RuntimeError (Py) on second call (D10 one-shot)

TypeScript (wrappers/typescript/src/session.ts):
- DisplayEvent interface {type, sessionId, turnId, parentTurnId?,
  synthesized?, payload}
- AaaError class extends Error with code/remediation
- TERMINAL_NOTIFICATION = 'result/final'
- SessionDeps {sessionId, terminate}
- SessionHandle with submitted flag
- makeIterable async generator using push-queue + wakeUp pattern

Python (wrappers/python/src/amplifier_agent_client/session.py):
- AaaError exception with code/remediation
- DisplayEvent class with snake_case fields
- SessionHandle with _submitted flag
- _stream async generator with asyncio.Queue sentinel pattern
- submit_task background task with finally sentinel

Tests (both languages):
- (a) yields display events and ends on result/final: drives
  2 result/delta notifs + result/final, verifies collected event
  types == ['result/delta', 'result/delta', 'result/final']
- (b) second submit() raises typed one-shot error matching
  /one-shot|already submitted/i

All 17 TS tests pass. All 15 Python session+unit tests pass.
Implements design §4.6 contract #1: if the engine emits a non-null
reply in its turn/submit response but no result/final notification was
observed first, the wrapper synthesizes a result/final-shaped
DisplayEvent with synthesized: true as the last yielded event.

Pure synthesis functions:
- TS: synthesizeFinalIfMissing({sawFinal, reply, sessionId, turnId}) → DisplayEvent | null
- Py: synthesize_final_if_missing(*, saw_final, reply, session_id, turn_id) → dict | None
Both return null/None when sawFinal=true or reply=null/None.

Session wiring:
- TS session.ts: tracks sawFinal boolean; changes .finally() to
  .then(onFulfilled, onRejected) to capture reply and call synthesis
- Py session.py: uses mutable saw_final_flag={seen: False} shared
  between on_notif and submit_task closures; synthesis in try block

Tests:
- Three-case pure function tests for both languages
- Integration test (Branch B) driving stub through the synthesis path:
  engine sends result/delta events and a turn/submit reply but never
  emits result/final; last event is type=result/final, synthesized=True
- Add wrappers/typescript/src/approval.ts: ApprovalRequest, ApprovalResponse,
  ApprovalAdapter types; makeApprovalHandler(adapter) returns (params)=>Promise.
  No adapter → {decision:'deny', reason:'no_adapter_configured'}. Adapter race
  against setTimeout(timeoutMs) → {decision:'timeout'} on expiry. .catch →
  {decision:'deny', reason:'adapter_error'}.

- Add wrappers/python/src/amplifier_agent_client/approval.py: symmetric
  make_approval_handler(*, on_request, timeout_ms). on_request=None →
  no_adapter_configured. asyncio.wait_for timeout → {decision:'timeout'}.
  Exception → {decision:'deny', reason:'adapter_error'}.

- Update wrappers/typescript/src/session.ts: extend RpcLike with optional
  onRequest?; SessionHandle constructor takes optional ApprovalAdapter; wires
  rpc.onRequest('approval/request', makeApprovalHandler(approval)) if provided.

- Update wrappers/python/src/amplifier_agent_client/session.py: SessionHandle
  constructor takes optional approval_on_request + approval_timeout_ms; wires
  rpc.on_request('approval/request', make_approval_handler(...)) if provided.

- Add wrappers/typescript/test/approval.test.ts: 3 cases (allow response,
  timeout at 50ms, no-adapter deny).
- Add wrappers/python/tests/test_approval.py: same 3 async cases.

All 24 TS + 25 Py tests pass.
- Add display.ts (TS) with SubagentMode, DisplayAdapter interface, and
  applyDisplayFilter() predicate factory
- Add display.py (Py) with apply_display_filter() predicate factory
- Wire display adapter into session.ts: apply filter before yielding to
  iterator AND before invoking display.onEvent push callback
- Wire display adapter into session.py: same dual-path filtering
- Add tests in display.test.ts covering:
  (a) subagentEvents='all' keeps all events including parentTurnId ones
  (b) subagentEvents='none' drops events with parentTurnId
  (c) default (unset) is 'all'
  (d) onEvent push callback receives same events as iterator
  (e) subagentEvents='none' suppresses parentTurnId events from both paths
- Add tests in test_display.py with same five cases

TS: 29/29 tests pass. Py: 30/30 tests pass.
- Add wrappers/conformance/runner_py.py: Python conformance runner using
  amplifier_agent_lib.protocol.conformance.loader and JsonRpcClient.
  ScriptedTransport replays server_to_client frames synchronously in
  send(). L14 synthesis applied after turn/submit if engine omits
  result/final. Emits JSON report {fixture, language, passed, assertions}.

- Add wrappers/conformance/runner_ts.ts: TypeScript port using yaml npm
  package for fixture loading and an inline minimal JSON-RPC client.
  Same ScriptedTransport pattern and L14 logic. Reports language:typescript.

- Add wrappers/conformance/tests/test_runner_py.py: pytest tests verifying
  capability_negotiation.yaml and l14_synthesis.yaml pass (Python).

- Add wrappers/conformance/test/runner-ts.test.ts: vitest tests verifying
  capability_negotiation.yaml and l14_synthesis.yaml pass (TypeScript).

- Add pnpm-workspace.yaml at repo root with members wrappers/typescript
  and wrappers/conformance.

- Add wrappers/conformance/package.json (private package with yaml^2.4.0
  dep, amplifier-agent-client-ts workspace:* dep, vitest devDep).

- Add wrappers/conformance/tsconfig.json and vitest.config.ts.

Assertion kinds supported: notification_emitted, no_notification,
error_returned, response_matches (unknown kinds skipped with ok=True).
source:engine filter on no_notification distinguishes synthesized
events from engine-emitted notifications.

Tests: 2/2 Python, 2/2 TypeScript.
Adds tests/test_conformance_parity.py — parametrised @pytest.mark.integration
test that runs both the TypeScript and Python conformance runners on each of
the 5 YAML fixtures and asserts they produce identical (kind, passed) tuples.

This is the H6 mitigation from design §4.6: prevents the silent failure mode
'TS green / Py green but they are testing different things'.

Design:
  - _REPO_ROOT and _FIXTURE_DIR constants anchor paths relative to repo root
  - _run_py() invokes `uv run python wrappers/conformance/runner_py.py <f>`
  - _run_ts() invokes `pnpm exec tsx runner_ts.ts <f>` from wrappers/conformance
  - On divergence, prints per-assertion diff with Py vs TS column alignment

Fix bundled: wrappers/conformance/runner_ts.ts error_returned handler was using
String(err) which serialises plain objects as '[object Object]', so code checks
against data.code strings always failed. Now uses JSON.stringify(err) for
structured errors, matching Python's str(frame['error']) behaviour.
…r_py

- Guard against None when checking assertion_id in errors dict (line 202)
- Add explicit type annotation for assertion_id in response_matches case (line 216)
- Guard against None in responses.get(assertion_id) call (line 218)
- Run ruff format to fix formatting across both conformance files

This resolves the critical type safety issue where int | None was being
passed to dict operations expecting int keys. The fixes ensure that
fixtures with missing 'id' fields in assertions won't accidentally pass
vacuously as None.
@manojp99 manojp99 changed the title feat(phase-2-2): TS + Py wrappers + cross-language conformance feat(wrappers): Phase 2.2 + 2.3 + 2.5 — TypeScript + Python wrappers + cross-language conformance May 21, 2026
@manojp99 manojp99 merged commit a4d0fe0 into main May 21, 2026
1 check passed
manojp99 pushed a commit that referenced this pull request May 26, 2026
Resolves conflicts from PRs #6 (Phase 2.1 wire spec hardening) and #7
(Phase 2.2/2.3/2.5 wrappers + conformance) landing on main while this
branch carried the Mode A pivot work that supersedes Mode B.

Resolution strategy:
- Wrapper source + tests (TypeScript + Python): take OURS. Mode A
  subprocess-driver implementation supersedes Mode B JSON-RPC version
  per the 2026-05-24 Mode A pivot amendment (CR-C breaking change).
- Mode B-only modules brought in by main (display.{py,ts}, jsonrpc.{py,ts},
  l14.{py,ts}, and their tests): DELETED. Mode A pivot eliminated these
  capabilities; deferred to v1.x per amendment §6.
- Conformance fixtures (5 OLD Mode B JSON-RPC fixtures), conformance
  loader, runner_py.py, runner-ts.test.ts, freshness guard tests: take
  OURS. Debug cycle 3 fixed runner_py.py import + added freshness guard.
- Protocol schemas, spec.md, version_info.py, Phase 2.1 tests: take OURS.
  Our branch is on top of main's Phase 2.1 baseline and has additional
  Mode A pivot-related touches.

Regression check: 453 passed, 3 skipped, 0 failed (uv run pytest tests/ -q).
Same green state as pre-merge HEAD (7bd3a80).

Co-Authored-By: Amplifier <amplifier@microsoft.com>
manojp99 added a commit that referenced this pull request May 28, 2026
… wrappers (#21)

* docs(readme): align with Mode A pivot, protocol 0.2.0, and shipped L3 wrappers

This README was significantly out of date with recent changes:

- PR #8 (Mode A pivot): Replaced stdio JSON-RPC wire protocol with subprocess driver
  model (argv in / JSON envelope out). README still documented Mode B (--stdio flag,
  agent/initialize JSON-RPC methods) as primary interface.

- PR #17: Bumped protocol version 0.1.0 → 0.2.0, changed --mcp-servers (inline JSON)
  to --mcp-config-path (file path) for MCP config delivery. Neither reflected in README.

- PR #20: Changed OpenAI default from gpt-4 to gpt-5.5 (matches extended_thinking=true
  in bundle). README used outdated default.

- PR #7: TypeScript wrapper shipped at 0.4.0 on npm, Python wrapper shipped in
  wrappers/python/. README claimed L3 wrappers were 'designed, implementation next'.

Applied 10 targeted corrections across README:
1. 'What it is' section — removed Mode B bullet, fixed lifecycle
2. 'Why' section — removed JSON-RPC-mirrors-MCP claim
3. 'Quick start' — removed --stdio example, pointed at wrapper SDKs
4. 'Modes' table — collapsed to Mode A only, added historical design-doc note
5. 'Admin commands' — added missing 'prepare' and 'verify' commands
6. 'Approval flow' — removed unimplemented 'c' response, removed stdio paragraph
7. 'Embedding in Python' — replaced broken classmethod example with correct constructor
   and instance method usage
8. 'Architecture diagram' — updated arrow label to reflect Mode A transport
9. 'Wire protocol' section — replaced Mode B docs with Mode A (0.2.0 argv flags, JSON
   envelope schema, stdout/stderr split)
10. 'Status' — updated from 'Phase 1, L3 next' to shipped state (0.3.0 engine, 0.4.0 TS
    wrapper, Python wrapper, 0.2.0 protocol, conformance suite, path-based MCP) with next
    steps (L2 host adapters, container packaging).

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

* docs(readme): add TypeScript SDK section and fix git+URL install command

- Added "TypeScript / Node.js SDK" section documenting the shipped npm package
  (amplifier-agent-ts@0.4.0) with install command, quick-start example, Node ≥ 20
  requirement, and pointers to full README and type definitions.

- Fixed broken Python install commands: replaced bare PyPI references
  (uv tool install / pipx install) with git+URL form pinned to engine-v0.3.0.
  Package is not yet published to PyPI; this fixes the broken install path
  demonstrated by user feedback. Added explanatory note about separate engine
  vs wrapper version tagging (engine-v0.3.0 vs wrapper-v0.4.0).

Both gaps were identified during PR #21 review: TypeScript SDK was effectively
undocumented for consumers despite npm publication, and Python install was
impossible without the git+URL form.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

* docs(readme): replace registry install with from-source instructions

The Python engine hasn't been published to any registry. Removed all
registry-style install commands (uv tool install amplifier-agent, pipx
install amplifier-agent, and git+URL forms with PEP 508 specs) as they
were misleading. From-source install via git clone + uv tool install -e .
is now the only documented path.

Added note that engine and wrapper releases are tagged separately
(engine-v0.3.0 vs wrapper-v0.4.0) so users can git checkout <tag> before
installing if they want a pinned version.

This does not affect the TypeScript section — amplifier-agent-ts is
published to npm and npm install amplifier-agent-ts is correct.

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

* docs(readme): use uv tool install with bare git+URL (no registry needed)

uv tool install works fine with a raw git+https://... argument; no PEP 508
name @ url wrapper, no registry, no separate clone+sync step. Pinning
supported via @<tag>. From-source editable install retained as a
contributor note.

Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>

---------

Co-authored-by: Manoj Prabhakar Paidiparthy <mpaidiparthy@microsoft.com>
Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
manojp99 pushed a commit that referenced this pull request Jun 3, 2026
…igest (#7)

Closes the Task-9 TODO: getEngineInfo() now returns the values
captured during the engine version probe that spawnAgent() runs at
init (Issue #9). Previously both fields were hardcoded empty strings.

  - engineVersion populated from `amplifier-agent version --json`
    payload's `version` field.
  - bundleDigest populated from the probe payload's optional
    `bundleDigest` field. The engine's current `version --json`
    output (from admin/version_info.py) only emits {version,
    protocolVersion} — bundleDigest will be empty string until a
    future engine release exposes it. Forward-compatible: when the
    engine adds it, the wrapper picks it up automatically with no
    further changes.

DONE_WITH_CONCERNS for the bundleDigest follow-up: filed as an
engine-side ask for a future PR. The wrapper does what it can with
the data the engine surface exposes today; the contract is wired
so the field will populate the moment the engine emits it.

Closes #7.
manojp99 pushed a commit that referenced this pull request Jun 3, 2026
Mirrors PR #29 / #31 pattern: dist/ is tracked so consumers installing
from the git tarball get the compiled artifacts without a build step.

Regenerated from npm run build after issues #1, #2, #3, #4, #5, #6, #7,
#9, #10 landed.
manojp99 pushed a commit that referenced this pull request Jun 3, 2026
Wrapper hardening release closing 8 consumer-reported gaps at 0.5.0:
  #1  configPath surface
  #2  stderr NDJSON parsing
  #3  runChildProcess injection
  #4  display.onEvent dispatch
  #5  public re-exports
  #6  Transport dead code (root cause of #2/#4)
  #7  getEngineInfo() implementation
  #9  checkProtocolVersion() wired into init path
  #10 approval API mapped to engine -y/-n + approval.mode

Issue #8 in the consumer report was a misread — InitializeParams.
mcpConfigPath is intentionally retained in protocol-0.3.0. No
type change needed; the schema is canonical and correct.

This is a minor bump per 0.x convention even though some changes
are BREAKING — the wrapper hasn't shipped a 1.0 yet, so breaking
changes ride minor bumps. See CHANGELOG for the BREAKING list.

Engine compatibility: requires amplifier-agent >= 0.4.0.
Pinned protocol: 0.3.0.
manojp99 added a commit that referenced this pull request Jun 3, 2026
…, approval, getEngineInfo, +5 more) (#36)

* feat(wrapper-ts): re-export internal helpers from index.ts (#5)

Adds named re-exports from the package entry point so consumers can
import internal helpers without reaching into private deep paths:

  assembleArgv, AssembleArgvInput
  resolveMcpConfigPath, cleanupSpillFile, McpSpillResult
  buildEnv, resolveBinaryPath, probeEngineVersion,
    DEFAULT_ALLOWLIST, BLOCKED_ENV_KEYS,
    ResolveBinaryPathOptions, BuildEnvOptions
  Transport, TransportOptions, ExitInfo
  checkProtocolVersion, VersionCheckResult, VersionCheckOk,
    VersionCheckFail, CheckProtocolVersionOptions
  parseRunOutput, STDERR_TAIL_BYTES, SubprocessOutcome
  makeApprovalHandler, ApprovalAdapter, ApprovalRequest,
    ApprovalHandler

Each export is annotated @public.

Closes #5.

* feat(wrapper-ts): wire checkProtocolVersion() into init path (#9)

spawnAgent() now probes the engine's protocol version once during
initialization (via amplifier-agent version --json) and runs
checkProtocolVersion() against PROTOCOL_VERSION_REQUIRED_BY_WRAPPER
BEFORE constructing a SessionHandle. Mismatch fails fast wrapper-side
with AaaError(protocol_version_mismatch), saving a full subprocess
roundtrip later.

Adds two new SpawnAgentParams fields:
  - allowProtocolSkew?: boolean — bypass the check (mirrors engine's
    host_config.allowProtocolSkew)
  - _engineVersionProbe?: () => Promise<EngineVersionPayload> —
    test-only injection point for the probe

Also bumps PROTOCOL_VERSION_REQUIRED_BY_WRAPPER from "0.2.0" to
"0.3.0" to match the engine's current wire protocol
(amplifier_agent_lib.protocol.methods.PROTOCOL_VERSION). The wrapper
was shipping with a stale pin; the new check would have surfaced this
at startup.

Closes #9.

* feat(wrapper-ts): add runChildProcess injection point (#3)

Adds SpawnAgentParams.runChildProcess?: ChildProcessFactory — a public
seam to substitute the subprocess factory used inside SessionHandle.
When set, the wrapper invokes the factory in place of
child_process.spawn, preserving the same options shape (detached, stdio,
env, optional cwd).

Useful for:
  - Sandboxing (e.g. wrapping the child in a container or namespace)
  - Test doubles (e.g. EventEmitter fakes that drive scripted outputs)
  - Harness wrapping (e.g. observing the subprocess from outside)

ChildProcessFactory is exported as a @public type from index.ts.

Closes #3.

* feat(wrapper-ts)!: wire Transport NDJSON pipeline + dispatch to display.onEvent (#2, #4, #6)

The engine emits one JSON object per line on the child subprocess's
stderr stream for each wire-protocol notification (progress,
result/delta, result/final, thinking/delta, thinking/final,
tool/started, tool/completed, approval/request, approval/timeout,
plus wire-level error). Before this change the wrapper buffered
stderr as raw text and silently dropped every event — the existing
Transport class implemented NDJSON parsing but was never wired
anywhere (dead code).

This change:

  - Adds parseNdjsonStream(stream, {onJson, onNonJson?}) — a
    standalone helper extracted from the parsing logic Transport
    already had. Resolves when the stream emits 'close'. Exported
    @public.

  - Wires parseNdjsonStream onto child.stderr inside
    SessionHandle.makeIterable(). JSON lines are parsed into
    'notification' DisplayEvents and dispatched to
    params.display?.onEvent. Non-JSON lines (and JSON lines, for
    completeness) are still accumulated into stderrBuf so the
    stderrTail surface on parseRunOutput remains diagnostically
    useful.

  - Extends the DisplayEvent discriminated union with a new
    {type: 'notification', method: string, params: unknown}
    variant. **BREAKING**: existing exhaustive switch statements
    on event.type will no longer be exhaustive without a
    notification branch.

  - Threads SpawnAgentParams.display through to SessionHandle so
    the callback that was previously silently dropped is now
    actually fired (Issue #4).

Closes #2, #4, #6.

BREAKING CHANGE: display.onEvent callbacks are now actually invoked
with wire-event notifications. Callers that registered onEvent
expecting it to be a no-op may observe new event flow. The
DisplayEvent union has a new 'notification' variant; exhaustive
switch statements need a corresponding branch.

* feat(wrapper-ts): surface --config flag via SpawnAgentParams.configPath (#1)

Engine PR #27 / v0.4.0 added the --config <path> flag and the
host_config layer (approval mode, MCP servers, provider defaults,
allowProtocolSkew, etc.). The wrapper had no surface to forward this,
so callers had to fall back to AMPLIFIER_AGENT_CONFIG in env.extra.

This change:

  - Adds SpawnAgentParams.configPath?: string (public, @public TSDoc).
  - Adds AssembleArgvInput.configPath?: string.
  - assembleArgv emits --config <path> when configPath is set.
  - Threads configPath through SessionHandleParams to the per-submit
    argv assembly.

Also drive-by adds approvalMode field to AssembleArgvInput (used by
#10's commit). The argv-builder now reads input.approvalMode and emits
-y / -n / nothing accordingly. Default remains -y for backward compat
with callers that haven't opted into the approval API.

Closes #1.

* feat(wrapper-ts)!: wire approval API to engine -y/-n + approval.mode (#10)

Previously, SpawnAgentParams.approval threw AaaError(
approval_not_supported_in_v1) whenever set because it required the
mid-turn onRequest callback that v1 doesn't support.

This change extends SpawnAgentParams.approval to also accept the
static-policy shape { mode: 'yes' | 'no' | 'prompt' }, which maps to
engine argv:

  - 'yes'    -> -y (auto-allow every tool call)
  - 'no'     -> -n (auto-deny every tool call)
  - 'prompt' -> emit no flag; engine falls back to
                host_config.approval.mode or the bundle's TTY-based
                default. This is how a host hands policy resolution
                back to the engine.

The legacy { onRequest, timeoutMs } form still throws
approval_not_supported_in_v1 — the Mode A wire has no mid-turn
channel. Mid-turn callbacks will return when WG-4 lands.

Engine compatibility: { mode: 'prompt' } requires
amplifier-agent >= 0.4.0 (PR #34 added host_config.approval.mode).

Closes #10.

BREAKING CHANGE: SpawnAgentParams.approval is now a union shape;
callers passing { mode } no longer hit approval_not_supported_in_v1.
Callers that defensively catch that error need to remove the try/catch
when migrating to the mode shape.

* feat(wrapper-ts): implement getEngineInfo() — engineVersion + bundleDigest (#7)

Closes the Task-9 TODO: getEngineInfo() now returns the values
captured during the engine version probe that spawnAgent() runs at
init (Issue #9). Previously both fields were hardcoded empty strings.

  - engineVersion populated from `amplifier-agent version --json`
    payload's `version` field.
  - bundleDigest populated from the probe payload's optional
    `bundleDigest` field. The engine's current `version --json`
    output (from admin/version_info.py) only emits {version,
    protocolVersion} — bundleDigest will be empty string until a
    future engine release exposes it. Forward-compatible: when the
    engine adds it, the wrapper picks it up automatically with no
    further changes.

DONE_WITH_CONCERNS for the bundleDigest follow-up: filed as an
engine-side ask for a future PR. The wrapper does what it can with
the data the engine surface exposes today; the contract is wired
so the field will populate the moment the engine emits it.

Closes #7.

* chore(wrapper-ts): rebuild dist after hardening release changes

Mirrors PR #29 / #31 pattern: dist/ is tracked so consumers installing
from the git tarball get the compiled artifacts without a build step.

Regenerated from npm run build after issues #1, #2, #3, #4, #5, #6, #7,
#9, #10 landed.

* chore(release): bump amplifier-agent-ts to 0.6.0 + CHANGELOG

Wrapper hardening release closing 8 consumer-reported gaps at 0.5.0:
  #1  configPath surface
  #2  stderr NDJSON parsing
  #3  runChildProcess injection
  #4  display.onEvent dispatch
  #5  public re-exports
  #6  Transport dead code (root cause of #2/#4)
  #7  getEngineInfo() implementation
  #9  checkProtocolVersion() wired into init path
  #10 approval API mapped to engine -y/-n + approval.mode

Issue #8 in the consumer report was a misread — InitializeParams.
mcpConfigPath is intentionally retained in protocol-0.3.0. No
type change needed; the schema is canonical and correct.

This is a minor bump per 0.x convention even though some changes
are BREAKING — the wrapper hasn't shipped a 1.0 yet, so breaking
changes ride minor bumps. See CHANGELOG for the BREAKING list.

Engine compatibility: requires amplifier-agent >= 0.4.0.
Pinned protocol: 0.3.0.

---------

Co-authored-by: Manoj Prabhakar Paidiparthy <mpaidiparthy@microsoft.com>
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