@tisyn/agent
v0.16.0
Published
`@tisyn/agent` defines the typed capability boundary between Tisyn workflows and the work that actually gets performed. It turns effectful calls into explicit contracts: named agent boundaries, typed operation payloads, and handlers the runtime can dispat
Readme
@tisyn/agent
@tisyn/agent defines the typed capability boundary between Tisyn workflows and the work that actually gets performed. It turns effectful calls into explicit contracts: named agent boundaries, typed operation payloads, and handlers the runtime can dispatch locally or across a transport boundary.
If @tisyn/ir describes what work should happen, @tisyn/agent describes who can do it and how it is called.
Where It Fits
This package sits between authored workflow logic and concrete side effects.
- The compiler lowers authored calls like
yield* Service().method(...)into effectful IR. - The runtime resolves those effects through installed agent handlers.
- The transport layer can expose the same declarations across process, worker, or network boundaries.
@tisyn/agent is the capability layer that gives effect IDs stable names, payload shapes, and implementations.
Core Concepts
agent(id, operations)declares a named capability boundary.operation<Spec>()declares one typed operation on that boundary.Agents.use()binds handlers directly to a declaration in the current scope, installing routing and resolve middleware.Effects.around()installs Effection middleware layers that intercept or route effect invocations.dispatch()performs an effect call through the currentEffectsmiddleware boundary. It accepts either an explicit(effectId, data)pair or a call descriptor object produced byagent().op(args).resolve()queries the Effects middleware chain to check if an agent is bound in the current scope.useAgent()retrieves a typed facade for an agent bound in the current scope viaAgents.use()oruseTransport(). The facade exposes direct methods for each operation plus.around()for per-operation middleware.
Agent declarations are typed metadata plus call helpers. They describe invocations, but do not execute anything by themselves.
Public API
The authoring surface exported from src/index.ts includes:
agent— declare a named agent boundary and its available operationsoperation— declare the typed input/output contract for one operationAgents— setup namespace;Agents.use(declaration, handlers)binds handlers directly in the current scopeimplementAgent— create anAgentImplementationobject for use by protocol servers and transports (internal/advanced)useAgent— retrieve a typed facade for an agent previously bound viaAgents.use()oruseTransport(); returns an object with one method per operation plus.around()
The dispatch-boundary surface lives in @tisyn/effects:
Effects— the Effection middleware context for invocation routing; useEffects.around()to install intercept layersdispatch— perform an effect call through the currentEffectsmiddleware boundary. Accepts either(effectId, data)or a{ effectId, data }descriptor returned byagent().op(args)resolve— query the Effects middleware chain to check if an agent is boundinvoke— invoke another declared operation from inside a handler, with nested-invocation guaranteesinstallCrossBoundaryMiddleware— install an IR function node as the cross-boundary middleware carrier for further remote delegationgetCrossBoundaryMiddleware— read the current cross-boundary middleware carrier from scope (returnsnullif not set)InvalidInvokeCallSiteError,InvalidInvokeInputError,InvalidInvokeOptionError— error classes thrown byinvokeon misuse
These symbols are published only by @tisyn/effects; @tisyn/agent does not re-export them. The workspace-only seam (DispatchContext, evaluateMiddlewareFn) lives on the non-stable @tisyn/effects/internal subpath and is not part of the stable public surface.
Important exported types:
OperationSpec— describe the typed input and result shape of an operationDeclaredAgent— represent the callable declaration returned byagent()AgentDeclaration— structural type for a declared agent contractAgentImplementation— declaration paired with handlers and install logicImplementationHandlers— type the handler map expected byAgents.use()andimplementAgent()ArgsOf— extract the input shape from an operation declarationResultOf— extract the result type from an operation declarationWorkflow— represent the authored workflow return type used in ambient declarationsAgentFacade— typed facade returned byuseAgent(), with per-operation methods and.around()AgentHandle— deprecated alias forAgentFacade
Declare an Agent
import { agent, operation } from "@tisyn/agent";
const orders = agent("orders", {
fetch: operation<{ orderId: string }, { id: string; total: number }>(),
cancel: operation<{ orderId: string }, void>(),
transfer: operation<{ from: string; to: string }, void>(),
});Calling a declared operation produces a call descriptor. It is a typed effect request, not a direct function call.
const request = orders.fetch({ orderId: "ord-1" });Single-parameter ambient methods pass their argument through directly as the operation payload. Multi-parameter ambient methods are still wrapped into a named object keyed by the authored parameter names — transfer(from: string, to: string) lowers to { from, to }.
Bind Handlers Locally
Agents.use() binds typed handlers directly to a declaration in the current scope. It installs both dispatch routing and resolve middleware — useAgent() will succeed for this agent after this call.
import { Agents } from "@tisyn/agent";
yield* Agents.use(orders, {
*fetch({ orderId }) {
return { id: orderId, total: 42 };
},
*cancel() {},
*transfer({ from, to }) {
// multi-parameter methods receive a named object payload
},
});For transport or protocol server use cases, implementAgent() creates an AgentImplementation object with call(opName, payload). This is an internal/advanced API used by @tisyn/transport.
Use an Agent with Per-Operation Middleware
useAgent() returns a facade backed by a per-agent Context API with one operation per declared operation.
import { useAgent } from "@tisyn/agent";
const ordersFacade = yield* useAgent(orders);
// Direct method dispatch
const order = yield* ordersFacade.fetch({ orderId: "ord-1" });
// Per-operation middleware via .around()
yield* ordersFacade.around({
*fetch([args], next) {
console.log("fetching order:", args);
return yield* next(args);
},
});Facade middleware composes before the global Effects middleware chain:
facade.around MW → facade core handler → dispatch() → Effects.around MW → Effects coreMultiple useAgent() calls with the same declaration in the same scope share middleware visibility — middleware installed via one reference is visible to all. Child-scope facade middleware inherits down but does not affect the parent scope.
Dispatch an Operation
import { dispatch } from "@tisyn/effects";
const order = yield* dispatch(
orders.fetch({ orderId: "ord-1" }),
);This is useful when:
- application code wants to call an installed agent directly
- one agent implementation delegates work to another
yield* Agents.use(checkout, {
*complete({ orderId }) {
const order = yield* dispatch(
orders.fetch({ orderId }),
);
return { ok: order.total > 0 };
},
});Mental Model
An agent declaration gives Tisyn a typed, named capability boundary.
- Declarations define what operations exist and what they accept or return.
- Call descriptors — produced by calling a declared operation (e.g.
orders.fetch(args)) — describe one requested operation as a plain{ effectId, data }object. - Implementations attach concrete handlers to those declarations.
- Facades (from
useAgent()) expose per-operation dispatch methods and.around()for per-operation middleware. - Effects middleware decides how dispatched calls are routed.
- Cross-boundary constraints are installed as ordinary
Effects.around()middleware in the execution scope — there is no separate enforcement mechanism.
That routing can stay local, or it can be forwarded through another layer such as a worker or network transport. @tisyn/agent stays focused on the contract and dispatch shape rather than the transport itself.
Relationship to the Rest of Tisyn
- Use
@tisyn/agentwith@tisyn/runtimewhen executing IR against real effect handlers. - Use it with
@tisyn/transportwhen those handlers must be reached across process, worker, or network boundaries. @tisyn/compilerdiscovers effect usage from authored workflow source, but does not execute effects.@tisyn/protocoldefines wire messages for remote execution, but@tisyn/agentitself remains protocol-agnostic.
What This Package Does Not Define
@tisyn/agent does not define:
- the IR language
- durable execution
- replay semantics
- transport or protocol messages
It defines the typed capability layer that those systems rely on.
Summary
Use @tisyn/agent when you want effectful workflow calls to become explicit, typed capability contracts.
It gives Tisyn a stable boundary between workflow intent and concrete execution: declarations name the capability, operations define the payload shape, implementations provide handlers, facades expose per-operation middleware, and dispatch makes invocation routable wherever the work actually happens.
