@hypergaas/core
v0.2.0
Published
An open-source agent SDK for B2B SaaS: a typed, multi-tenant action registry over your existing service layer.
Downloads
81
Maintainers
Readme
hypergaas
Add agents to your SaaS — without rewriting your service layer.
The HyperGaaS SDK: a typed, multi-tenant action registry over the service layer
you already have. Add @agentAction() to a method and it becomes a safe agent
action — tenant-scoped, permission-checked, audited, with irreversible calls
paused for approval. The tool schema is derived from your TypeScript types, so
there's no parallel schema to maintain and no plumbing to hand-write.
Status: v0.2. Live on npm. Full docs coming soon.
Install
npm install @hypergaas/coreQuickstart
// 1. Bind your roles to the registry once, at module scope.
import { defineRoles, audience, createActionRegistry, type AgentContext } from "@hypergaas/core";
const roles = defineRoles({
owner: { displayName: "Owner", seniority: 4, canApproveIrreversibleUpTo: "high" },
dispatcher: { displayName: "Dispatcher", seniority: 3, canApproveIrreversibleUpTo: "medium" },
technician: { displayName: "Technician", seniority: 1 },
});
const { agentAction, invoke } = createActionRegistry(roles);
// 2. Decorate a service method you already have. No parallel schema, no handler.
class JobService {
@agentAction({
description: "Get a technician's schedule for a given date",
reversibility: "idempotent",
requiredPermissions: ["schedule:read"],
audienceRoles: ["dispatcher", "owner", audience.self((ctx, p) => ctx.userId === p.techId)],
costWeight: 1,
})
async getTechSchedule(ctx: AgentContext, params: { techId: string; date: Date }) {
return this.db.schedule.find({ tenantId: ctx.tenantId, ...params });
}
}import { createAgentContext, isOk } from "@hypergaas/core";
// 3. Build one context per request — the only source of tenant identity.
const ctx = createAgentContext({
tenantId: "acme-hvac",
userId: "u_marcus",
role: "dispatcher",
permissions: ["schedule:read"],
autonomyLevel: "medium",
});
// 4. Invoke. Permissions and tenant scope are enforced before the body runs;
// the result is typed, not thrown.
const result = await invoke("JobService.getTechSchedule", ctx, {
techId: "u_marcus",
date: new Date("2026-05-25"),
});
if (isOk(result)) {
console.log(result.value); // the schedule, scoped to tenant "acme-hvac"
}By the end you have a permission-checked, tenant-scoped call with a paired
PROPOSED + COMPLETED audit record — and you wrote none of that plumbing. The
default InMemoryAuditLogger swaps for a durable backend behind the same
interface in production.
NestJS / TypeORM / Angular / legacy decorators — use registry.register(...)
@agentAction() is a TC39 standard decorator. It requires standard
decorator mode — that is, experimentalDecorators must be false (or unset)
in your tsconfig.json. NestJS, TypeORM, and Angular decorators (@Injectable,
@Controller, @Entity, @Column, …) require the legacy mode
("experimentalDecorators": true). A TypeScript compilation unit has exactly
one global decorator mode, so @agentAction cannot co-locate with those
framework decorators in the same project. If you put @agentAction on a class
that also uses legacy decorators, you'll hit a tsc TS1241 error and a
runtime decorator kind "undefined" throw.
For those stacks, use the imperative, decorator-free registry.register(...)
escape hatch. Your provider keeps its @Injectable (legacy) decorators and
registers its action methods in the constructor or onModuleInit — no
@agentAction anywhere:
import { Injectable } from "@nestjs/common";
import { createActionRegistry, type AgentContext, type BoundActionRegistry } from "@hypergaas/core";
const registry = createActionRegistry(roles); // module scope
@Injectable()
class BillingService {
constructor(private readonly registry: BoundActionRegistry<typeof roles>) {
this.registry.register({
key: "BillingService.issueCredit",
description: "Issue a goodwill credit to a customer",
reversibility: "irreversible",
requiredPermissions: ["billing:write"],
audienceRoles: ["owner", "dispatcher"],
costWeight: 1,
handler: this.issueCredit.bind(this),
// OPTIONAL: an explicit params schema — see "Build-time schema codegen".
// paramsSchema: { type: "object", properties: { customerId: { type: "string" }, cents: { type: "number" } }, required: ["customerId", "cents"] },
});
}
async issueCredit(ctx: AgentContext, params: { customerId: string; cents: number }) {
// ...
}
}register takes the same metadata @agentAction() does (typed against your
role registry — a non-role in audienceRoles is still a compile error), plus an
explicit key (no decoration site to derive ClassName.methodName from) and the
handler. Permission, audience, reversibility, audit, and idempotent
re-registration all behave identically to the decorated path. The decorator
remains the primary, canonical path; register is the escape hatch for projects
that can't use it.
Build-time schema codegen
The hypergaas-extract CLI walks your tsconfig.json, finds @agentAction()
methods, and emits a hypergaas.actions.json artifact with a tool schema derived
from your method param types — no schema drift, no parallel definitions. It runs
ahead of tsc in this package's build script.
Imperative actions and schemas. The extractor scans @agentAction() call
sites, so an action registered via registry.register(...) has no decoration
site for it to find. In v0.2.0 the imperative path is permissive by default
(any params shape passes — same as a decorated action with no artifact). To
enforce typed arguments on an imperative action, pass an explicit
paramsSchema (the JsonSchema shape the codegen emits) in the register(...)
declaration, or add an entry keyed by the action's key to the
actionsArtifact you feed createActionRegistry(roles, { actionsArtifact }).
(Auto-extracting imperative call sites is a planned follow-on.)
