@phrony/sdk is the TypeScript client for the Phrony runtime. It targets Node.js 18+, uses @grpc/grpc-js, and maps proto bytes JSON fields through helpers so you work with ordinary JavaScript objects.
Install
Install from your package registry, then connect to a runtime. For a guided tool-binding walkthrough, see Add a tool binding.
Package
pnpm add @phrony/sdk
Requires Node.js 18+.
$ pnpm add @phrony/sdkImport paths
@phrony/sdk and @phrony/sdk/worker
Use the main entry for Phrony, RuntimeClient, and Worker. Use the worker subpath when you only need WorkStream and worker types.
// Full SDK
import { Phrony, PhronyAgent, PhronyBundle, RuntimeClient, Worker } from "@phrony/sdk";
// Worker-only subpath (includes WorkStream)
import { Worker, WorkStream } from "@phrony/sdk/worker";Connect to the runtime
Every SDK entrypoint dials the runtime over gRPC. Set PHRONY_RUNTIME_ADDR or pass an explicit address. After installing the package, use one of the patterns below.
RuntimeClient
RuntimeClient.connect(options?)
Dial the runtime and health service. Options: address, credentials, clientOptions. The resolved address is available on client.address.
import { RuntimeClient } from "@phrony/sdk";
const client = await RuntimeClient.connect({
address: process.env.PHRONY_RUNTIME_ADDR ?? "127.0.0.1:7777",
});
console.log("Connected to", client.address);client.close()
Close runtime and health gRPC clients when finished.
const client = await RuntimeClient.connect();
// ...
client.close();Phrony
Phrony.connect(options?)
High-level facade with the same connection options (runtimeAddr maps to address).
import { Phrony } from "@phrony/sdk";
const phrony = await Phrony.connect({
runtimeAddr: process.env.PHRONY_RUNTIME_ADDR,
});
// Same options as RuntimeClient: credentials, address via runtimeAddr
phrony.close();Address resolution
resolveRuntimeAddr(address?)
Resolve the gRPC target: explicit argument, then PHRONY_RUNTIME_ADDR, then 127.0.0.1:7777.
import { resolveRuntimeAddr, DEFAULT_RUNTIME_ADDR } from "@phrony/sdk";
// Explicit address wins, then PHRONY_RUNTIME_ADDR, then DEFAULT_RUNTIME_ADDR
const addr = resolveRuntimeAddr(); // e.g. "127.0.0.1:7777"
console.log(DEFAULT_RUNTIME_ADDR);dialRuntime(options?)
Low-level dial returning runtime, health, address, and close() without the RuntimeClient wrapper.
import { dialRuntime } from "@phrony/sdk";
const dial = dialRuntime({ address: "127.0.0.1:7777" });
try {
const version = await new Promise((resolve, reject) => {
dial.runtime.getVersion({}, (err, res) => (err ? reject(err) : resolve(res)));
});
console.log(version);
} finally {
dial.close();
}TLS
credentials (optional)
By default the SDK uses insecure credentials. Pass createSsl from @grpc/grpc-js when the runtime requires TLS.
import { credentials } from "@grpc/grpc-js";
import { readFileSync } from "node:fs";
import { RuntimeClient } from "@phrony/sdk";
const client = await RuntimeClient.connect({
address: "runtime.example.com:7777",
credentials: credentials.createSsl(
readFileSync("ca.pem"),
readFileSync("client-key.pem"),
readFileSync("client-cert.pem"),
),
});Health check
client.health()
Standard gRPC health client on the same address as the runtime.
import { RuntimeClient } from "@phrony/sdk";
const client = await RuntimeClient.connect();
const health = client.health();
await new Promise<void>((resolve, reject) => {
health.check({ service: "" }, (err, res) => {
if (err) reject(err);
else {
console.log(res?.status); // SERVING, NOT_SERVING, etc.
resolve();
}
});
});
client.close();Cleanup
Phrony.close() / RuntimeClient.close()
import { Phrony, RuntimeClient } from "@phrony/sdk";
const client = await RuntimeClient.connect();
client.close();
const phrony = await Phrony.connect();
phrony.close();Run agents & bundles
Phrony is the high-level entry point for agent and bundle sessions. Connect first — see Connect to the runtime.
Phrony
new Phrony(options?)
Create a facade. runtimeAddr and optional credentials match RuntimeClientOptions. The client dials lazily on first use.
import { Phrony } from "@phrony/sdk";
const phrony = new Phrony({ runtimeAddr: "127.0.0.1:7777" });
const agent = phrony.agent("default/my-agent");
// Client dials lazily on first run() or runtimeClient()Phrony.connect(options?)
Connect eagerly and return a ready instance.
import { Phrony } from "@phrony/sdk";
const phrony = await Phrony.connect({
runtimeAddr: process.env.PHRONY_RUNTIME_ADDR,
});
// Same options as RuntimeClient: credentials, address via runtimeAddr
phrony.close();phrony.close()
Close the underlying gRPC client.
import { Phrony, RuntimeClient } from "@phrony/sdk";
const client = await RuntimeClient.connect();
client.close();
const phrony = await Phrony.connect();
phrony.close();phrony.agent("namespace/name")
Return a PhronyAgent. Accepts namespace/name@version. Bare names without a slash throw AgentRefParseError.
const phrony = await Phrony.connect();
const pinned = phrony.agent("default/my-agent@0.2.0");
const latest = phrony.agent("default/my-agent");phrony.bundle("namespace/name")
Return a PhronyBundle. Accepts namespace/name@version where version is semver or a lock hash (sha256:…). Bare names without a slash throw BundleRefParseError.
const phrony = await Phrony.connect();
const semver = phrony.bundle("demo/payment-desk@1.0.0");
const lockHash = phrony.bundle("demo/payment-desk@sha256:abc…");
const active = phrony.bundle("demo/payment-desk");phrony.runtimeClient()
Return the lazily connected RuntimeClient.
import { Phrony } from "@phrony/sdk";
const phrony = await Phrony.connect();
const client = await phrony.runtimeClient();
const agents = await client.listAgents();PhronyAgent
run(options?)
Start a session. wait: true (default) streams until completed; wait: false uses unary RunSession and returns immediately.
import { Phrony } from "@phrony/sdk";
const phrony = await Phrony.connect();
const result = await phrony.agent("default/my-agent").run({
input: { claimId: "CLM-48219" },
resolvedSecrets: { openai: process.env.OPENAI_API_KEY },
});
console.log(result.sessionId, result.output, result.stopReason);
phrony.close();run({ wait: false })
Fire-and-forget session start.
import { Phrony } from "@phrony/sdk";
const phrony = await Phrony.connect();
const result = await phrony.agent("default/my-agent").run({
input: { claimId: "CLM-48219" },
wait: false,
});
console.log(result.sessionId, result.status);runInteractive(options?)
Open RunSessionInteractive without waiting. Consume events() and call close() when done.
import { Phrony } from "@phrony/sdk";
const phrony = await Phrony.connect();
const session = await phrony.agent("default/my-agent").runInteractive({
input: { question: "Summarize claim CLM-48219" },
});
for await (const event of session.events()) {
if (event.type === "text_delta") process.stdout.write(event.delta);
}
session.close();PhronyBundle
Multi-agent systems published as a Bundle run through the bundle root member. PhronyBundle mirrors PhronyAgent — same run() options and AgentSessionError on failure.
run(options?)
Start a session on the bundle root. wait: true (default) streams until completed; wait: false uses unary RunSession with bundleRef.
import { Phrony } from "@phrony/sdk";
const phrony = await Phrony.connect();
const result = await phrony.bundle("demo/payment-desk").run({
input: { message: "Pay 500 USD to Acme" },
resolvedSecrets: { stripe: process.env.STRIPE_API_KEY },
});
console.log(result.sessionId, result.output, result.stopReason);
phrony.close();run({ wait: false })
Fire-and-forget bundle session start.
import { Phrony } from "@phrony/sdk";
const phrony = await Phrony.connect();
const result = await phrony.bundle("demo/payment-desk").run({
input: { message: "Pay 500 USD to Acme" },
wait: false,
});
console.log(result.sessionId, result.status);runInteractive(options?)
Open RunSessionInteractive with bundleRef without waiting.
import { Phrony } from "@phrony/sdk";
const phrony = await Phrony.connect();
const session = await phrony.bundle("demo/payment-desk").runInteractive({
input: { message: "Pay 500 USD to Acme" },
});
for await (const event of session.events()) {
if (event.type === "text_delta") process.stdout.write(event.delta);
}
session.close();For streaming event types, see Interactive sessions. On failure with wait: true, run() throws AgentSessionError — see Utilities.
Interactive sessions
InteractiveSession wraps RunSessionInteractive. Open it from RuntimeClient.runSessionInteractive() or PhronyAgent.runInteractive() or PhronyBundle.runInteractive(). The first client message must be start or attach.
Methods
start(options)
New session: agentRef or bundleRef (mutually exclusive), JSON input, optional resolvedSecrets.
import { RuntimeClient } from "@phrony/sdk";
const client = await RuntimeClient.connect();
const session = client.runSessionInteractive();
session.start({
agentRef: { namespace: "default", name: "my-agent", version: "" },
input: { question: "Hello" },
resolvedSecrets: { openai: process.env.OPENAI_API_KEY },
});start({ bundleRef })
Start an interactive session on a deployed bundle.
import { RuntimeClient } from "@phrony/sdk";
const client = await RuntimeClient.connect();
const session = client.runSessionInteractive();
session.start({
bundleRef: { namespace: "demo", name: "payment-desk", version: "" },
input: { message: "Pay 500 USD to Acme" },
resolvedSecrets: { stripe: process.env.STRIPE_API_KEY },
});attach(options)
Reconnect to an existing sessionId.
session.attach({ sessionId: "sess_abc123" });sendUserMessage(text)
Send a user turn after an awaiting_input event.
// After an awaiting_input event:
session.sendUserMessage("Use a shorter summary.");decideToolApproval(options)
Respond to approval_required: approvalId, approved, optional comment and replacement args.
// Inside your events() loop when event.type === "approval_required":
session.decideToolApproval({
approvalId: event.approval.approvalId,
approved: true,
comment: "Approved by operator",
args: { city: "Oslo" },
});events()
Async iterable of InteractiveEvent until the stream ends or errors.
for await (const event of session.events()) {
switch (event.type) {
case "session_started":
console.log(event.session.sessionId);
break;
case "text_delta":
process.stdout.write(event.delta);
break;
case "completed":
console.log(event.output);
break;
case "failed":
throw new Error(event.message);
}
}close()
Half-close the client side of the stream.
session.close();
client.close();InteractiveEvent types
Server events are a discriminated union on type:
Lifecycle
session_started, completed, failed, cancelled, stream_end
Streaming
text_delta, awaiting_input
Tools
tool_call, tool_result, approval_required
Tool workers
Workers implement tools declared on agents over the Work stream. See Tool workers in the runtime docs.
Worker
new Worker(options)
workerId is required. Optional runtimeAddr, workloadIdentity, imageDigest.
import { Worker } from "@phrony/sdk/worker";
const worker = new Worker({
workerId: "weather-worker-1",
runtimeAddr: process.env.PHRONY_RUNTIME_ADDR,
workloadIdentity: "k8s://default/weather-worker",
imageDigest: "sha256:abc…",
});registerTool(options)
Register before connect(). handler receives (args, context) with AbortSignal for cancellation.
worker.registerTool({
tool: "weather.get-forecast",
version: "1.0.0",
maxConcurrency: 4,
handler: async (args: { city: string }, ctx) => {
if (ctx.signal.aborted) throw new Error("cancelled");
return { temp_c: 12, city: args.city };
},
});connect() / close()
connect() blocks until the stream ends; close() aborts in-flight calls and shuts down.
await worker.connect(); // blocks until disconnect
await worker.close();ToolError
Throw from a handler to return a structured tool failure to the runtime.
import { ToolError } from "@phrony/sdk/worker";
handler: async () => {
throw new ToolError("UPSTREAM_TIMEOUT", "Weather API timed out");
}WorkStream
Low-level helper from @phrony/sdk/worker for custom registration or reconnect logic.
WorkStream
sendRegister, run(handlers), sendResult, sendNack, inFlightCalls, close, buildHandlerAdvertisements.
import { RuntimeClient, WorkStream, buildHandlerAdvertisements } from "@phrony/sdk/worker";
const client = await RuntimeClient.connect();
const stream = new WorkStream(client.work());
stream.sendRegister({
workerId: "weather-worker-1",
handlers: buildHandlerAdvertisements([
{ tool: "weather.get-forecast", version: "1.0.0", maxConcurrency: 4 },
]),
inFlight: stream.inFlightCalls(),
});
stream.run({
onRegistered: (leaseTtlMs) => console.log("lease", leaseTtlMs),
onInvoke: (invoke) => {
stream.markCallExecuting(invoke.callId);
stream.sendResult({
callId: invoke.callId,
payload: Buffer.from(JSON.stringify({ ok: true })),
});
},
});
stream.close();
client.close();Runtime client
RuntimeClient exposes every runtime unary RPC. Connect via Connect to the runtime. For agent runs, prefer Phrony.
Version and sessions
getVersion(request?)
const version = await client.getVersion();
console.log(version.version, version.schemaVersion);runSession(request)
Unary session start. Set agentRef or bundleRef (mutually exclusive). Encode input and secrets with jsonBytes / jsonBytesMap.
import { jsonBytes, jsonBytesMap } from "@phrony/sdk";
const response = await client.runSession({
agentRef: { namespace: "default", name: "my-agent", version: "" },
input: jsonBytes({ claimId: "CLM-48219" }),
resolvedSecrets: jsonBytesMap({ openai: process.env.OPENAI_API_KEY }),
});
console.log(response.sessionId, response.status);runSession({ bundleRef })
Start a session on a deployed bundle root member.
import { jsonBytes, jsonBytesMap } from "@phrony/sdk";
const response = await client.runSession({
bundleRef: { namespace: "demo", name: "payment-desk", version: "" },
input: jsonBytes({ message: "Pay 500 USD to Acme" }),
resolvedSecrets: jsonBytesMap({ stripe: process.env.STRIPE_API_KEY }),
});
console.log(response.sessionId, response.status);listSessions(request)
const { sessions } = await client.listSessions({
agentRef: { namespace: "default", name: "my-agent", version: "" },
status: "running",
kind: "", // optional: "agent" or "bundle"
});inspectSession(request)
Full persisted session dump: history, timeline, invocations, approvals, and delegated children.
const { session, timeline } = await client.inspectSession({
sessionId: "sess_abc123",
});
// timeline — unified chronological narrative for the session and descendants
// session.children — delegated child session headers (story lives in timeline)cancelSession(request)
await client.cancelSession({ sessionId: "sess_abc123" });completeSession(request)
await client.completeSession({ sessionId: "sess_abc123" });Publish and deploy — agents
publish(request)
manifest is the raw agent.yaml bytes.
import { readFileSync } from "node:fs";
const manifest = readFileSync("agent.yaml");
const published = await client.publish({
manifest,
actor: "ci@example.com",
});
console.log(published.versionId, published.contentHash);deploy(request)
await client.deploy({
agentRef: { namespace: "default", name: "my-agent", version: "0.2.0" },
actor: "ci@example.com",
});rollback(request)
await client.rollback({
agentRef: { namespace: "default", name: "my-agent", version: "" },
toVersion: "0.1.0",
actor: "ops@example.com",
});getActiveVersion(request)
const active = await client.getActiveVersion({
agentRef: { namespace: "default", name: "my-agent", version: "" },
});
console.log(active.version);listDeployments(request)
const history = await client.listDeployments({
agentRef: { namespace: "default", name: "my-agent", version: "" },
});
for (const d of history.deployments) console.log(d.version, d.deployedAtUnixMs);getAgentVersion(request)
const record = await client.getAgentVersion({
agentRef: { namespace: "default", name: "my-agent", version: "0.2.0" },
});retireAgentVersion(request)
await client.retireAgentVersion({
agentRef: { namespace: "default", name: "my-agent", version: "0.1.0" },
});deprecateAgentVersion(request)
await client.deprecateAgentVersion({
agentRef: { namespace: "default", name: "my-agent", version: "0.1.0" },
});archiveAgent(request)
await client.archiveAgent({
agentRef: { namespace: "default", name: "my-agent", version: "" },
});Publish and deploy — bundles
Bundle lifecycle RPCs mirror the CLI bundles commands. Publish requires the committed bundle.lock.json bytes alongside the bundle manifest.
publishBundle(request)
bundleManifest and committedLock are raw bytes; members lists vendored closure packages.
import { readFileSync } from "node:fs";
const bundleManifest = readFileSync("support/bundle.yaml");
const committedLock = readFileSync("support/bundle.lock.json");
const published = await client.publishBundle({
bundleManifest,
committedLock,
members: [
// BundleMemberPackage entries for each vendored closure member
],
actor: "ci@example.com",
});
console.log(published.bundleVersionId, published.lockHash);deployBundle(request)
bundleRef must include @version — use parseBundleRefVersionRequired for deploy-by-hash.
import { parseBundleRefVersionRequired } from "@phrony/sdk";
await client.deployBundle({
bundleRef: parseBundleRefVersionRequired("demo/payment-desk@1.0.0"),
actor: "ci@example.com",
});getActiveBundle(request)
const active = await client.getActiveBundle({
bundleRef: { namespace: "demo", name: "payment-desk", version: "" },
});
console.log(active.version, active.lockHash);listBundleDeployments(request)
const history = await client.listBundleDeployments({
bundleRef: { namespace: "demo", name: "payment-desk", version: "" },
});
for (const d of history.deployments) console.log(d.version, d.lockHash, d.deployedAt);Catalog
listAgents(request?)
const { agents } = await client.listAgents();
for (const a of agents) console.log(a.namespace, a.name);listAgentVersions(request)
const { versions } = await client.listAgentVersions({
agentRef: { namespace: "default", name: "my-agent", version: "" },
});listBundles(request?)
const { bundles } = await client.listBundles();
for (const b of bundles) console.log(b.namespace, b.name);listBundleVersions(request)
const { versions } = await client.listBundleVersions({
bundleRef: { namespace: "demo", name: "payment-desk", version: "" },
});Approvals
getApproval(request)
const approval = await client.getApproval({ approvalId: "appr_xyz" });listApprovals(request?)
const { approvals } = await client.listApprovals({
status: "pending",
sessionId: "",
route: "",
});decideApproval(request)
import { ApprovalDecision, jsonBytes } from "@phrony/sdk";
await client.decideApproval({
approvalId: "appr_xyz",
decision: ApprovalDecision.APPROVAL_DECISION_APPROVE,
comment: "Looks good",
args: jsonBytes({ city: "Oslo" }),
comprehensionAcknowledged: true,
actor: "ops@example.com",
});Streams
work(metadata?, options?)
Raw grpc-js duplex stream — prefer Worker or WorkStream.
const raw = client.work(); // grpc-js ClientDuplexStream — prefer WorkerrunSessionInteractive(metadata?, options?)
Returns InteractiveSession — see Interactive sessions.
const session = client.runSessionInteractive();Typical usage wraps the client in try/finally:
Pattern
import { RuntimeClient } from "@phrony/sdk";
const client = await RuntimeClient.connect();
try {
// examples below
} finally {
client.close();
}Utilities
Agent references
parseAgentRef(string)
import { parseAgentRef, formatAgentRef } from "@phrony/sdk";
const ref = parseAgentRef("default/my-agent@0.2.0");
// { namespace: "default", name: "my-agent", version: "0.2.0" }
formatAgentRef(ref); // "default/my-agent@0.2.0"formatAgentRef(ref)
import { formatAgentRef } from "@phrony/sdk";
formatAgentRef({
namespace: "default",
name: "my-agent",
version: "0.2.0",
}); // "default/my-agent@0.2.0"Bundle references
parseBundleRef(string)
Version may be semver or a lock hash (sha256:…).
import { parseBundleRef, formatBundleRef } from "@phrony/sdk";
const semver = parseBundleRef("demo/payment-desk@1.0.0");
// { namespace: "demo", name: "payment-desk", version: "1.0.0" }
const lockHash = parseBundleRef("demo/payment-desk@sha256:abc…");
formatBundleRef(lockHash); // "demo/payment-desk@sha256:abc…"formatBundleRef(ref)
import { formatBundleRef } from "@phrony/sdk";
formatBundleRef({
namespace: "demo",
name: "payment-desk",
version: "1.0.0",
}); // "demo/payment-desk@1.0.0"parseBundleRefVersionRequired(string)
Same as parseBundleRef but rejects empty @version — use for deployBundle.
import { parseBundleRefVersionRequired } from "@phrony/sdk";
// Deploy flows require an explicit @version (semver or lock hash)
const ref = parseBundleRefVersionRequired("demo/payment-desk@sha256:abc…");JSON bytes
jsonBytes / parseJsonBytes / jsonBytesMap
Encode and decode proto bytes fields that carry UTF-8 JSON.
import { jsonBytes, parseJsonBytes, jsonBytesMap } from "@phrony/sdk";
const input = jsonBytes({ claimId: "CLM-48219" });
const parsed = parseJsonBytes<{ claimId: string }>(input);
const secrets = jsonBytesMap({ openai: process.env.OPENAI_API_KEY });Errors
PhronyRuntimeError
import { PhronyRuntimeError } from "@phrony/sdk";
try {
await client.getVersion();
} catch (err) {
if (err instanceof PhronyRuntimeError) {
console.error(err.grpcCode, err.action, err.details);
}
}wrapRpcError(action, err)
import { wrapRpcError } from "@phrony/sdk";
try {
await someGrpcCall();
} catch (err) {
throw wrapRpcError("my action", err);
}AgentSessionError
import { Phrony, AgentSessionError } from "@phrony/sdk";
try {
const phrony = await Phrony.connect();
await phrony.agent("default/my-agent").run({ input: {} });
} catch (err) {
if (err instanceof AgentSessionError) {
console.error(err.sessionId, err.message);
}
}AgentRefParseError
import { parseAgentRef, AgentRefParseError } from "@phrony/sdk";
try {
parseAgentRef("my-agent"); // missing namespace/
} catch (err) {
if (err instanceof AgentRefParseError) console.error(err.message);
}BundleRefParseError
import { parseBundleRef, BundleRefParseError } from "@phrony/sdk";
try {
parseBundleRef("payment-desk"); // missing namespace/
} catch (err) {
if (err instanceof BundleRefParseError) console.error(err.message);
}ToolError
import { ToolError } from "@phrony/sdk/worker";
handler: async () => {
throw new ToolError("UPSTREAM_TIMEOUT", "Weather API timed out");
}Constants and helpers
SDK_VERSION, DEFAULT_MAX_CONCURRENCY, handlerKey, heartbeatIntervalMs
import {
SDK_VERSION,
DEFAULT_MAX_CONCURRENCY,
handlerKey,
heartbeatIntervalMs,
} from "@phrony/sdk";
console.log(SDK_VERSION);
console.log(handlerKey("weather.get-forecast", "1.0.0")); // weather.get-forecast@1.0.0
console.log(heartbeatIntervalMs(30_000)); // 15000Generated types
Import proto message types from @phrony/sdk when building requests for RuntimeClient unary methods.
Type imports
import type {
AgentRef,
BundleRef,
BundleSummary,
BundleVersionSummary,
DeployBundleRequest,
PublishBundleRequest,
PublishRequest,
RunSessionRequest,
WorkClientMsg,
} from "@phrony/sdk";
const agentRun: RunSessionRequest = {
agentRef: { namespace: "default", name: "my-agent", version: "" },
input: Buffer.alloc(0),
resolvedSecrets: {},
};
const bundleRun: RunSessionRequest = {
bundleRef: { namespace: "demo", name: "payment-desk", version: "" },
input: Buffer.alloc(0),
resolvedSecrets: {},
};Commonly used types
AgentRef, BundleRef, Approval, ApprovalDecision, PublishRequest, PublishBundleRequest, DeployRequest, DeployBundleRequest, BundleSummary, BundleVersionSummary, RunSessionRequest, WorkClientMsg, RunSessionInteractiveClientMsg