Skip to content

Commit 64a1d30

Browse files
manojp99Manoj Prabhakar PaidiparthyAmplifier
authored
feat(packaging): root package.json + committed dist for native bun/npm git+install (#11)
* feat(packaging): add root package.json + commit wrapper dist for git+install Enables bun/npm consumers to install amplifier-agent-client-ts via native git URL syntax: bun add github:microsoft/amplifier-agent#main Pattern: a minimal shipping-only package.json at the repo root points (via 'main', 'types', 'exports') at wrappers/typescript/dist/. The wrappers/typescript/package.json remains the dev manifest with scripts, devDependencies, and tsconfig — unchanged. Trade-off: wrappers/typescript/dist/ is now committed to the repo (~108KB, 20 files). The CI workflow added in #10 already builds the wrapper on every push; a follow-up can add an auto-commit step so dist stays current without manual rebuild discipline. For now, the 'wrapper-v*' tag workflow gives consumers stable pinnable refs. Why this pattern (vs alternatives): - npm publish: requires registry account, name commitment, token for consumers in some flows. Deferred until POC graduates. - gitpkg.vercel.app subdir proxy: third-party service, currently disabled (HTTP 402 DEPLOYMENT_DISABLED). - Dist branch (separate refs): requires force-push CI; this is simpler. - Separate repo for wrapper: needs new repo + code-sync overhead. Consumer install path (in any project's package.json): "amplifier-agent-client-ts": "github:microsoft/amplifier-agent#main" bun/npm clones the repo, finds package.json at root, the 'files' field restricts to wrappers/typescript/dist/, the 'exports' field resolves the entry point. No tokens, no proxies, no registries. Co-Authored-By: Amplifier <amplifier@microsoft.com> * fix(packaging): use conditional exports with explicit types resolution Updates /package.json exports from string form: "exports": { ".": "./wrappers/typescript/dist/index.js" } to conditional form with explicit types resolution: "exports": { ".": { "types": "./wrappers/typescript/dist/index.d.ts", "import": "./wrappers/typescript/dist/index.js" } } Modern TypeScript (with moduleResolution: bundler/nodenext) follows the exports field and looks for a 'types' condition before falling back to the top-level 'types' field. The string-form exports left only 'import' resolvable, so consumer projects (e.g. NanoClaw's agent-runner) saw 'McpServerConfig' as 'any' or unresolved, surfacing as TS2339 errors like 'Property transport does not exist on type McpServerConfig'. Verified locally: NC's bun run typecheck went from 5 errors to 0 after this change. Order matters in conditional exports: 'types' MUST appear FIRST so TS checks it before module conditions. This follows the TypeScript handbook guidance for the exports field. Co-Authored-By: Amplifier <amplifier@microsoft.com> --------- Co-authored-by: Manoj Prabhakar Paidiparthy <mpaidiparthy@microsoft.com> Co-authored-by: Amplifier <amplifier@microsoft.com>
1 parent 02bbe60 commit 64a1d30

23 files changed

Lines changed: 1928 additions & 1 deletion

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ __pycache__/
33
*.py[cod]
44
*.egg-info/
55
dist/
6+
!wrappers/typescript/dist/
67
build/
78

89
# Virtualenvs

package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "amplifier-agent-client-ts",
3+
"version": "0.3.0",
4+
"description": "TypeScript client wrapper for the Amplifier agent Mode A wire (POC: consumed via git+install from github.com/microsoft/amplifier-agent).",
5+
"type": "module",
6+
"exports": {
7+
".": {
8+
"types": "./wrappers/typescript/dist/index.d.ts",
9+
"import": "./wrappers/typescript/dist/index.js"
10+
}
11+
},
12+
"types": "./wrappers/typescript/dist/index.d.ts",
13+
"files": [
14+
"wrappers/typescript/dist"
15+
],
16+
"engines": {
17+
"node": ">=20"
18+
},
19+
"license": "MIT",
20+
"repository": {
21+
"type": "git",
22+
"url": "https://github.com/microsoft/amplifier-agent.git",
23+
"directory": "wrappers/typescript"
24+
}
25+
}

wrappers/typescript/.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
node_modules/
2-
dist/
32
coverage/
43
*.tsbuildinfo
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Approval bridge — in-band, mid-turn JSON-RPC round-trip (§5.2).
3+
*
4+
* makeApprovalHandler(adapter) returns a JSON-RPC request handler for
5+
* 'approval/request' server-initiated requests. Wire it into the
6+
* JsonRpcClient via rpc.onRequest('approval/request', makeApprovalHandler(adapter)).
7+
*
8+
* Decision semantics:
9+
* - 'allow' — adapter accepted the tool call
10+
* - 'deny' — adapter rejected, adapter threw, or no adapter configured
11+
* - 'timeout' — adapter did not resolve within timeoutMs
12+
*
13+
* Pattern reference: Design §5.2 — six-step round-trip.
14+
*/
15+
/** Request sent by the engine when a tool call requires approval. */
16+
export interface ApprovalRequest {
17+
id: string;
18+
tool: string;
19+
args: unknown;
20+
}
21+
/** Response the wrapper sends back to the engine. */
22+
export interface ApprovalResponse {
23+
decision: "allow" | "deny" | "timeout";
24+
reason?: string;
25+
[key: string]: unknown;
26+
}
27+
/** Adapter supplied by the host to handle approval requests. */
28+
export interface ApprovalAdapter {
29+
onRequest: (req: unknown) => Promise<ApprovalResponse>;
30+
timeoutMs: number;
31+
}
32+
/** Handler type matching JsonRpcClient.onRequest signature. */
33+
export type ApprovalHandler = (params: unknown) => Promise<unknown>;
34+
/**
35+
* Create a JSON-RPC request handler for 'approval/request'.
36+
*
37+
* @param adapter - Host-supplied adapter, or undefined for default-deny.
38+
* @returns An async function (params) => ApprovalResponse.
39+
*/
40+
export declare function makeApprovalHandler(adapter: ApprovalAdapter | undefined): ApprovalHandler;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Approval bridge — in-band, mid-turn JSON-RPC round-trip (§5.2).
3+
*
4+
* makeApprovalHandler(adapter) returns a JSON-RPC request handler for
5+
* 'approval/request' server-initiated requests. Wire it into the
6+
* JsonRpcClient via rpc.onRequest('approval/request', makeApprovalHandler(adapter)).
7+
*
8+
* Decision semantics:
9+
* - 'allow' — adapter accepted the tool call
10+
* - 'deny' — adapter rejected, adapter threw, or no adapter configured
11+
* - 'timeout' — adapter did not resolve within timeoutMs
12+
*
13+
* Pattern reference: Design §5.2 — six-step round-trip.
14+
*/
15+
/**
16+
* Create a JSON-RPC request handler for 'approval/request'.
17+
*
18+
* @param adapter - Host-supplied adapter, or undefined for default-deny.
19+
* @returns An async function (params) => ApprovalResponse.
20+
*/
21+
export function makeApprovalHandler(adapter) {
22+
if (!adapter) {
23+
return async (_params) => ({
24+
decision: "deny",
25+
reason: "no_adapter_configured",
26+
});
27+
}
28+
const { onRequest, timeoutMs } = adapter;
29+
return async (params) => {
30+
let timerId;
31+
const timeoutPromise = new Promise((resolve) => {
32+
timerId = setTimeout(() => {
33+
resolve({ decision: "timeout" });
34+
}, timeoutMs);
35+
});
36+
try {
37+
const result = await Promise.race([onRequest(params), timeoutPromise]);
38+
// Clear the timeout timer if onRequest won the race.
39+
if (timerId !== undefined)
40+
clearTimeout(timerId);
41+
return result;
42+
}
43+
catch {
44+
if (timerId !== undefined)
45+
clearTimeout(timerId);
46+
return { decision: "deny", reason: "adapter_error" };
47+
}
48+
};
49+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* argv-builder.ts — pure argv assembly for `amplifier-agent run`.
3+
*
4+
* Mode A v2 (task-5 / A3'): given a fully-resolved AssembleArgvInput, produce
5+
* the exact argv array the wrapper will pass to the engine binary. This
6+
* function performs no I/O and reads no environment — all spilling, env
7+
* resolution, and capability composition happen upstream.
8+
*
9+
* SC-C: the wrapper always passes `-y` to enforce auto-allow at the bundle
10+
* layer; approvals are handled by the orchestrating host, not the engine.
11+
*/
12+
export interface AssembleArgvInput {
13+
/** Session identifier (provided by caller, never generated here). */
14+
sessionId: string;
15+
/** Final user prompt — emitted last as a positional argument. */
16+
prompt: string;
17+
/** Protocol version the wrapper speaks (e.g. "0.1.0"). */
18+
protocolVersion: string;
19+
/** When true, emit `--resume` instead of `--fresh`. */
20+
resume?: boolean;
21+
/** Working directory override; emits `--cwd <cwd>`. */
22+
cwd?: string;
23+
/** Provider override; emits `--provider <providerOverride>`. */
24+
providerOverride?: string;
25+
/**
26+
* Pre-resolved value for `--mcp-servers`. Caller decides whether this is
27+
* inline JSON or an `@/path/to/file.json` spill reference; argv-builder
28+
* threads it through unchanged.
29+
*/
30+
mcpServersFlag?: string;
31+
/** Host capabilities object — emitted as `--host-capabilities <JSON>`. */
32+
hostCapabilities?: unknown;
33+
/** Allowlisted env variable names — emits `--env-allowlist <comma-joined>`. */
34+
envAllowlist?: string[];
35+
/** Extra env entries — emitted as `--env-extra <JSON>`. */
36+
envExtra?: Record<string, string>;
37+
/** When true, emit `--allow-protocol-skew`. */
38+
allowProtocolSkew?: boolean;
39+
}
40+
/**
41+
* Build the argv array for `amplifier-agent run`.
42+
*
43+
* Pure function: no I/O, no env reads, no globals. Order is canonical and
44+
* stable so wrapper integration tests can pin against it.
45+
*/
46+
export declare function assembleArgv(input: AssembleArgvInput): string[];
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* argv-builder.ts — pure argv assembly for `amplifier-agent run`.
3+
*
4+
* Mode A v2 (task-5 / A3'): given a fully-resolved AssembleArgvInput, produce
5+
* the exact argv array the wrapper will pass to the engine binary. This
6+
* function performs no I/O and reads no environment — all spilling, env
7+
* resolution, and capability composition happen upstream.
8+
*
9+
* SC-C: the wrapper always passes `-y` to enforce auto-allow at the bundle
10+
* layer; approvals are handled by the orchestrating host, not the engine.
11+
*/
12+
/**
13+
* Build the argv array for `amplifier-agent run`.
14+
*
15+
* Pure function: no I/O, no env reads, no globals. Order is canonical and
16+
* stable so wrapper integration tests can pin against it.
17+
*/
18+
export function assembleArgv(input) {
19+
const argv = [];
20+
argv.push("run");
21+
argv.push("--session-id", input.sessionId);
22+
argv.push(input.resume ? "--resume" : "--fresh");
23+
if (input.cwd !== undefined) {
24+
argv.push("--cwd", input.cwd);
25+
}
26+
if (input.providerOverride !== undefined) {
27+
argv.push("--provider", input.providerOverride);
28+
}
29+
if (input.mcpServersFlag !== undefined) {
30+
argv.push("--mcp-servers", input.mcpServersFlag);
31+
}
32+
if (input.hostCapabilities !== undefined) {
33+
argv.push("--host-capabilities", JSON.stringify(input.hostCapabilities));
34+
}
35+
if (input.envAllowlist !== undefined && input.envAllowlist.length > 0) {
36+
argv.push("--env-allowlist", input.envAllowlist.join(","));
37+
}
38+
if (input.envExtra !== undefined) {
39+
argv.push("--env-extra", JSON.stringify(input.envExtra));
40+
}
41+
argv.push("--output", "json");
42+
argv.push("--protocol-version", input.protocolVersion);
43+
if (input.allowProtocolSkew === true) {
44+
argv.push("--allow-protocol-skew");
45+
}
46+
// SC-C: wrapper enforces auto-allow at the bundle layer.
47+
argv.push("-y");
48+
// Prompt is the final positional argument.
49+
argv.push(input.prompt);
50+
return argv;
51+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* amplifier-agent-client-ts — public entry point.
3+
*
4+
* Exports the locked public API from design §8.2, narrowed to Mode A v2
5+
* (amendment §5). `spawnAgent` is synchronous-in-spirit: it validates
6+
* parameters, resolves the engine binary path, builds the subprocess
7+
* environment, and constructs a `SessionHandle`. **No subprocess is spawned
8+
* at spawn-time** — the engine is launched per `submit()` (amendment §5.2).
9+
*/
10+
export { AaaError, SessionHandle } from "./session.js";
11+
export type { DisplayEvent, EngineInfo, SessionHandleParams, } from "./session.js";
12+
export type { ApprovalResponse } from "./approval.js";
13+
export type { EngineVersionPayload } from "./spawn.js";
14+
import { SessionHandle } from "./session.js";
15+
import type { DisplayEvent } from "./session.js";
16+
import type { ApprovalResponse } from "./approval.js";
17+
import type { McpServerConfig, HostCapabilities } from "./types.js";
18+
export type { McpServerConfig, HostCapabilities } from "./types.js";
19+
/**
20+
* The protocol version that this TypeScript wrapper requires.
21+
* Forwarded to the engine via `--protocol-version` on every `submit()`.
22+
*/
23+
export declare const PROTOCOL_VERSION_REQUIRED_BY_WRAPPER = "0.1.0";
24+
/** Parameters for spawnAgent(). Signature is locked verbatim by design §8.2. */
25+
export interface SpawnAgentParams {
26+
/** 'burst' reserved; throws AaaError(lifecycle_unsupported) at runtime. */
27+
lifecycle: "one-shot";
28+
sessionId: string;
29+
resume?: boolean;
30+
cwd?: string;
31+
env?: {
32+
allowlist: string[];
33+
extra?: Record<string, string>;
34+
};
35+
providerOverride?: string;
36+
/**
37+
* Mid-turn approval callback.
38+
*
39+
* **NOT SUPPORTED IN v1.** Passing a non-null `onRequest` throws
40+
* `AaaError(approval_not_supported_in_v1)` at spawnAgent() time. The v1 wire
41+
* is Mode A (per-turn subprocess); there is no mid-turn host channel.
42+
*/
43+
approval?: {
44+
onRequest: (req: unknown) => Promise<ApprovalResponse>;
45+
timeoutMs: number;
46+
};
47+
display?: {
48+
onEvent?: (event: DisplayEvent) => void;
49+
subagentEvents?: "all" | "none";
50+
};
51+
/** Default false; opt out of D6 strict-refuse version check. */
52+
allowProtocolSkew?: boolean;
53+
/** Optional MCP servers to forward via `--mcp-servers` (A1). */
54+
mcpServers?: Record<string, McpServerConfig>;
55+
/** Optional host envelope forwarded via `--host-capabilities` (A1). */
56+
host?: {
57+
capabilities?: HostCapabilities;
58+
};
59+
/** Per-submit timeout in ms (default: 10 minutes). */
60+
timeoutMs?: number;
61+
/** Replaces the real resolveBinaryPath() call. */
62+
_binaryResolver?: () => string;
63+
}
64+
/**
65+
* Compose all internal components into the single public entry point.
66+
*
67+
* Mode A v2 flow (amendment §5):
68+
* 1. Guard: lifecycle must be 'one-shot' (D10).
69+
* 2. Reject `approval.onRequest !== undefined` (SC-C — v1 has no mid-turn channel).
70+
* 3. Resolve engine binary path (or inject via `_binaryResolver`).
71+
* 4. Build subprocess environment via `buildEnv`.
72+
* 5. Return `new SessionHandle(params)` — **NO subprocess is spawned here**.
73+
*
74+
* The engine is launched per `submit()` (amendment §5.2). `agent/initialize`
75+
* is gone; protocol-version handshake moves to argv at submit-time. Engine
76+
* metadata (`engineVersion`, `bundleDigest`) is populated lazily once the
77+
* first envelope arrives (TODO: Task-9 wires this from `parseRunOutput`).
78+
*/
79+
export declare function spawnAgent(params: SpawnAgentParams): Promise<SessionHandle>;

0 commit comments

Comments
 (0)