@aidex/sdk
v1.2.0
Published
Aidex SDK — the developer-facing façade over @aidex/core (AI, AIBuilder, AIConfiguration) and @aidex/gateway (AI.gateway()).
Maintainers
Readme
@aidex/sdk
Installation
pnpm add @aidex/sdknpm install @aidex/sdkKernel = architecture. SDK = developer experience.
@aidex/core is the frozen, minimal kernel — new Aidex(config), .use(),
.registerStrategy(), .execute() — deliberately small so it can stay
stable for years. That stability comes at a cost: using it directly means a
developer has to know about providers, strategies, and plugins as separate
concepts before they can generate one line of text.
@aidex/sdk hides that assembly behind a friendly façade. Instead of:
const aidex = new Aidex({ provider: new GeminiProvider(...) });
aidex.use(loggerPlugin);
aidex.registerStrategy(new TextGenerationStrategy());
const result = await aidex.execute({ strategy: 'text-generation', input: 'hi' });an application writes:
import { AIBuilder } from '@aidex/sdk';
const ai = new AIBuilder().provider(new GeminiProvider(...)).build();
const result = await ai.text('hi');@aidex/sdk ships a CommonJS build too — the same example with require:
const { AIBuilder } = require('@aidex/sdk');
const ai = new AIBuilder().provider(new GeminiProvider(...)).build();
ai.text('hi').then(console.log);Contents
AI— the main façade. Holds exactly oneAidexinstance and delegates every call to it:execute(request)passes a rawAidexRequeststraight through;text(input)is a convenience that callskernel.execute({ strategy: 'text-generation', input }). Neither method contains strategy logic of its own — both simply delegate.AIBuilder— a builder overAidex..provider(p)/.plugin(p)accumulate configuration (both returnthis);.build()constructs the underlyingAidex, auto-registers@aidex/strategies'TextGenerationStrategyunder the name"text-generation"(the one piece of wiring this façade hides soai.text()works without the developer ever callingregisterStrategy()), and returns a ready-to-useAI. Throws a descriptiveErrorif no provider was configured. Can also be seeded via its constructor:new AIBuilder({ provider, plugins }).AIBuilder.plugin(plugin)— unchanged lifecycle behavior: pushes onto a privatePlugin[]fed intoAidexConfig.plugins, soAidex's own constructor callsuse(plugin)exactly once per entry, wiring lifecycle hooks the same way it always has. Additionally, if the plugin also satisfies@aidex/plugins'ExtendedPluginshape (i.e. it declares any ofregisterEngines()/registerStrategies()/registerPrompts()/registerTools()), every item each declared method returns is fanned out immediately: engines/prompts/tools are routed through this same builder's own.engine()/.prompt()/.tool()— so they land in the exact sameEngineRegistry/PromptRegistry/ToolRegistrya direct call to those methods would use, with the same duplicate-id validation. Strategies have no public single-item registration method onAIBuilder, so they're collected into a private, SDK-internalMap<string, Strategy>— never exported, never part of any public method signature — keyed bystrategy.name(not anid; this matches the kernel's ownregisterStrategy()convention), with duplicate names rejected immediately via@aidex/core'sDuplicateRegistrationError. That collection is only replayed onto the kernel inside.build(), in registration order, immediately after the auto-registeredTextGenerationStrategy— so a plugin-declared strategy is registered exactly once, at the same point in the lifecycle every other strategy is.AIBuilderdeliberately never instantiates or calls@aidex/plugins'PluginManagerto do this fan-out:PluginManager's constructor requires an already-builtAidexinstance, which doesn't exist yet at the point.plugin()is called —Aidexisn't constructed until.build()runs. Even if it were available sooner, callingPluginManager.use(plugin)would callaidex.use(plugin)a second time, sinceAidex's own constructor already callsuse()once perconfig.pluginsentry — duplicating lifecycle-hook execution for every plugin. This is a permanent design decision, not a gap to close:AIBuilderinstead reproduces onlyPluginManager's registration fan-out, routed through its own existing public methods, without ever holding or delegating to aPluginManagerobject.PluginManageritself remains the correct, unmodified composition point for application code that already holds a rawAidexinstance directly (outside ofAIBuilder).AIConfiguration— the lightweight shapeAIBuilder's constructor accepts:{ provider: Provider; plugins?: Plugin[] }. Developer-facing configuration only — no vendor-specific settings, no application settings.AIBuilder.engine(engine)— accumulatesEngines (from@aidex/engines) into a privateEngineRegistry, fluent (returnsthis, same as.provider()/.plugin()). Duplicate ids throw@aidex/core's reusedDuplicateRegistrationError(viaEngineRegistry.register()— no new validation here).AI.engine(engineId)— returns anEngineHandlescoped to that id.EngineHandle.execute(input?)delegates directly toEngineRegistry.execute(), including its capability validation (UnsupportedProviderCapabilityErrorif the configured provider doesn't support what the engine requires) and itsEngineNotFoundErrorfor an unregistered id — neither is caught or wrapped here.AIBuilder.workflow(workflow)— accumulatesWorkflows (from@aidex/workflow) into a privateWorkflowRegistry, fluent (returnsthis, same as.provider()/.engine()). Duplicate ids throw@aidex/workflow's package-localWorkflowAlreadyRegisteredError(not@aidex/core'sDuplicateRegistrationError— this package deliberately has no dependency on the kernel), viaWorkflowRegistry.register()— no new validation here.AI.workflow(workflowId)— returns aWorkflowHandlescoped to that id.WorkflowHandle.execute(input?)builds and injects the frozenAidexWorkflowContextundercontext.$aidex— carryingcontext.$aidex.provider— before delegating toWorkflowRegistry.execute(), which itself stays provider-unaware. The fields on$aidex(provider,config,logger,metadata) are frozen viaObject.freeze()and typedreadonly, so workflow steps cannot modify them. The$aidexproperty itself is reserved by the SDK; applications should never define or reassign it in workflow state, even though TypeScript does not enforce that at the type level.AIBuilder.prompt(prompt)— accumulatesPromptTemplates (from@aidex/prompts) into a privatePromptRegistry, fluent (returnsthis, same as.provider()/.engine()/.workflow()). Duplicateid@versionpairs throw@aidex/core'sDuplicateRegistrationErrorviaPromptRegistry.register()— no new validation here.AI.renderPrompt(id, variables?, version?)— looks up the prompt (defaulting to whichever version was most recently registered for thatid) and renders it, substitutingvariablesinto the template. Delegates entirely toPromptRegistry.render(); throws@aidex/prompts'PromptNotFoundError/MissingPromptVariableErrorunmodified.AI.prompts()— returns the underlyingPromptRegistryitself, for callers that want.get()/.has()/.listVersions()/.list()instead of the render-only convenience above.AIBuilder.tool(tool)— accumulatesTools (from@aidex/tools) into a privateToolRegistry, fluent (returnsthis, same as.provider()/.engine()/.workflow()/.prompt()). Duplicateids throw@aidex/core's reusedDuplicateRegistrationErrorviaToolRegistry.register()— no new validation here.AI.tools()— returns the underlyingToolRegistryitself, so callers can.get()/.has()/.list()/.listByPermission()/.execute(id, input, grantedPermissions?)— including the registry's own permission-gated execution (ToolPermissionDeniedErrorif a caller doesn't hold every permission a tool declares) — without this façade reimplementing any of it.AI.catalog()— returns the underlyingEngineCatalog(from@aidex/catalog) assembled atbuild()time, so callers can.find(id)/.findByCategory()/.findByTag()/.findByFeaturePack()/.list()engine metadata without ever executing anything.FeaturePackage— a feature pack's complete, hand-assembled manifest:{ name, version, engines?, prompts?, plugins?, metadata?, workflows? }.versionis the manifest's own version string (independent of any individual engine's or prompt'sversionfield).AIBuilder.use(featurePackage)fans aFeaturePackageinto this builder's existing registries in one call — every entry inengines/prompts/plugins/metadatais passed to.engine()/.prompt()/.plugin()/the internal catalog respectively, so.use(pack).use(otherPack)composes the same way calling.engine()/.prompt()individually would.workflowsis deliberately not touched by.use()— feature-pack workflow helpers predate@aidex/workflow'sWorkflow/WorkflowRegistryand have their ownrun(input, provider, options)shape; a manifest'sworkflowsfield exists only so an application can discover and invoke them directly, via each workflow's own native API. EveryEnginein a manifest'senginesis a singleton — the exact same instance is shared across every registry that registers it, so engine implementations must stay stateless: all execution state belongs onExecutionContext, never on theEngineinstance itself.
What this package is not
Not the kernel, not an AI provider, not an MCP implementation. It assembles
the packages that already exist (@aidex/core, @aidex/strategies, @aidex/engines,
and whatever concrete Provider/Plugin the application supplies) into one
convenient surface, and contains no business logic of its own. It knows
nothing about Gemini, OpenAI, Design Platform, or Print Platform.
Public API
import {
AI,
AIBuilder,
EngineHandle,
WorkflowHandle,
AIConfiguration,
type Provider,
type Plugin,
type Engine,
type Workflow,
type AidexWorkflowContext,
type FeaturePackage,
type PromptTemplate,
type PromptRegistry,
type EngineMetadata,
type EngineCatalog,
type Tool,
type ToolRegistry,
} from '@aidex/sdk';Provider and Plugin are re-exported because they're direct parameter
types of this SDK's own public methods (AIBuilder.provider()/.plugin(),
AIConfiguration.provider/.plugins) — a developer authoring a custom
Provider or Plugin to pass into this SDK can now do so without ever
importing @aidex/core directly, closing a gap where the SDK's own
"applications never need to import @aidex/core" promise didn't hold for
exactly the case it exists to support.
Engine is re-exported for the identical reason: it's a direct parameter
type of AIBuilder.engine(). EngineHandle is re-exported because it's
the direct return type of AI.engine() — a developer receiving or working
with that return value doesn't need to import @aidex/engines directly.
Workflow is re-exported for the identical reason: it's a direct parameter
type of AIBuilder.workflow(). WorkflowHandle is re-exported because it's
the direct return type of AI.workflow() — a developer receiving or working
with that return value doesn't need to import @aidex/workflow directly.
AidexWorkflowContext is re-exported because it's the type of the execution
context each workflow step receives. The fields on $aidex (provider, config,
logger, metadata) are frozen via Object.freeze() and typed readonly.
The $aidex property itself is reserved by the SDK; applications should never
define or reassign it in workflow state, even though TypeScript does not enforce
that at the type level.
PromptTemplate is re-exported because it's a direct parameter type of
AIBuilder.prompt() (and of FeaturePackage.prompts); PromptRegistry is
re-exported because it's the direct return type of AI.prompts().
PromptNotFoundError/MissingPromptVariableError/InvalidPromptError are
deliberately not re-exported — same precedent as every other domain
error below; callers who need instanceof on these import @aidex/prompts
directly.
EngineMetadata is re-exported because it's a direct parameter type of
FeaturePackage.metadata; EngineCatalog is re-exported because it's the
direct return type of AI.catalog().
Tool is re-exported because it's a direct parameter type of
AIBuilder.tool() (and, indirectly, of ExtendedPlugin.registerTools(),
consumed internally by .plugin()); ToolRegistry is re-exported because
it's the direct return type of AI.tools(). ToolNotFoundError/
ToolPermissionDeniedError are deliberately not re-exported — same
precedent as every other domain error below; callers who need instanceof
on these import @aidex/tools directly.
FeaturePackage is re-exported because it's the direct parameter type of
AIBuilder.use() — a feature pack authoring its own manifest constant (or
an application assembling one inline) needs this type without importing
@aidex/sdk's internals.
Strategy is deliberately not re-exported: nothing in this SDK's
public surface accepts one today — AIBuilder has no .strategy() method,
only @aidex/strategies' TextGenerationStrategy is auto-registered
internally. This remains true even though .plugin() now understands
@aidex/plugins' ExtendedPlugin.registerStrategies(): that method is
@aidex/plugins' own type surface, consumed by AIBuilder.plugin()
purely internally (see the bulk-registration description above) — it is
never re-exported by @aidex/sdk, and the strategies it yields land in a
private, SDK-internal collection with no public getter. Exporting a type
with no corresponding way to use it directly would itself be exposing an
unnecessary type. Revisit if AIBuilder ever grows a way to register a
custom Strategy directly.
Nothing else is exported — no Aidex, no kernel errors, no internal kernel
classes (Lifecycle, StrategyRegistry, PluginRegistry were never public
in @aidex/core either, and this package doesn't change that). Engine
internals like EngineRegistry, EngineNotFoundError, and
UnsupportedProviderCapabilityError are also not re-exported — callers who
need instanceof checks on those errors import them directly from @aidex/engines,
the same as they would for kernel errors from @aidex/core today. Similarly,
workflow internals like WorkflowRegistry, WorkflowNotFoundError, and
WorkflowAlreadyRegisteredError are not re-exported — callers who need
instanceof checks on those errors import them directly from @aidex/workflow.
The same applies to prompt/catalog internals: PromptNotFoundError,
MissingPromptVariableError, and InvalidPromptError are not re-exported
(import them from @aidex/prompts), and @aidex/catalog's own
DuplicateRegistrationError re-export precedent (it doesn't re-export one
either — see that package's own README) is followed here too. Likewise,
tool internals ToolNotFoundError and ToolPermissionDeniedError are not
re-exported — callers who need instanceof checks on those import them
directly from @aidex/tools, the same as every other domain error above.
Architecture rules this package follows
- Composition only —
AIBuilderandAIeach hold/compose anAidexinstance; no inheritance, no abstract base class. - No singleton, no global mutable state, no factory functions — getting an
AIrequires eithernew AIBuilder(...).build()ornew AI(existingKernel), both plain constructor calls. - Never duplicates logic already implemented by
Aidex, aStrategy, or aPlugin—AIBuilder.build()callsnew Aidex(...)andregisterStrategy()exactly once each;AI.text()/AI.execute()are one-line delegations. - No provider construction inside the SDK —
AIBuilder.provider()only ever accepts an already-constructedProviderthe application supplies.
Executing an Engine
import { AIBuilder } from '@aidex/sdk';
import { GeminiProvider } from '@aidex/providers';
const ai = new AIBuilder()
.provider(new GeminiProvider({ apiKey: '...' }))
.engine({
id: 'document.extract',
name: 'Document Extract',
description: 'Extracts text from a document',
version: '1.0.0',
async execute(context) {
// context.provider, context.request?.input, etc. — the same
// ExecutionContext shape a Strategy receives.
return `extracted: ${context.request?.input}`;
},
})
.build();
const result = await ai.engine('document.extract').execute('report.pdf');Capability Validation
An Engine can declare requiredCapabilities (from @aidex/providers'
ProviderCapability). AI.engine(id).execute() rejects with
UnsupportedProviderCapabilityError (from @aidex/engines) before the
engine ever runs, if the configured provider doesn't support everything
it requires — the same validation EngineRegistry.execute() always
performs, not reimplemented here.
import { ProviderCapability } from '@aidex/providers';
const ai = new AIBuilder()
.provider(new StubProvider()) // only supports text-generation
.engine({
id: 'video.transcribe',
name: 'Video Transcribe',
description: 'Transcribes video audio',
version: '1.0.0',
requiredCapabilities: [ProviderCapability.Streaming],
async execute() {
/* ... */
},
})
.build();
await ai.engine('video.transcribe').execute(someVideo);
// rejects with UnsupportedProviderCapabilityErrorExecuting a Workflow
import { AIBuilder, type AidexWorkflowContext } from '@aidex/sdk';
import { Workflow } from '@aidex/workflow';
import { GeminiProvider } from '@aidex/providers';
interface ResumeReviewState {
documentId: string;
}
const workflow = new Workflow<ResumeReviewState & { $aidex: AidexWorkflowContext }>('resume-review');
workflow.addStep({
name: 'parse',
async execute(context) {
// context.documentId is the value passed to execute() below
// context.$aidex.provider is the exact Provider configured below
},
});
const ai = new AIBuilder()
.provider(new GeminiProvider({ apiKey: '...' }))
.workflow(workflow)
.build();
const result = await ai.workflow('resume-review').execute({ documentId: 'r-1' });Registering a Feature Package
A FeaturePackage bundles everything a feature pack offers — engines,
prompts, plugins, and discoverable metadata — into one manifest that
AIBuilder.use() fans into this builder's existing registries in a single
call. This example builds one inline; a real feature pack (e.g.
@aidex/document) would export a manifest constant of this same shape for
an application to import and pass to .use() directly.
import { AIBuilder, type FeaturePackage } from '@aidex/sdk';
import { GeminiProvider } from '@aidex/providers';
const documentFeaturePack: FeaturePackage = {
name: 'document',
version: '1.0.0',
engines: [
{
id: 'document.extract',
name: 'Document Extract',
description: 'Extracts text from a document',
version: '1.0.0',
async execute(context) {
return `extracted: ${context.request?.input}`;
},
},
],
prompts: [
{
id: 'document.summarize',
version: '1.0.0',
template: 'Summarize the following text: {{text}}',
variables: ['text'],
},
],
metadata: [
{
id: 'document.extract',
name: 'Document Extract',
featurePack: 'document',
version: '1.0.0',
description: 'Extracts text from a document',
requestType: 'string',
responseType: 'string',
tags: ['document'],
category: 'extraction',
},
],
};
const ai = new AIBuilder()
.provider(new GeminiProvider({ apiKey: '...' }))
.use(documentFeaturePack)
.build();
const result = await ai.engine('document.extract').execute('report.pdf');
console.log(ai.catalog().findByCategory('extraction'));
console.log(ai.renderPrompt('document.summarize', { text: '...' }));Every Engine inside a FeaturePackage is constructed once and shared as
a singleton across every registry that registers it — engines must stay
stateless, with all execution state living on ExecutionContext, never on
the engine instance itself. documentFeaturePack.version above is the
manifest's own version, independent of the version field on each
individual engine or prompt entry.
Registering a Tool
import { AIBuilder } from '@aidex/sdk';
import { GeminiProvider } from '@aidex/providers';
const ai = new AIBuilder()
.provider(new GeminiProvider({ apiKey: '...' }))
.tool({
id: 'calculator',
name: 'calculator',
description: 'Evaluates a simple arithmetic expression',
async execute(input: string) {
return String(eval(input));
},
})
.build();
console.log(await ai.tools().execute('calculator', '2 + 2'));(This example is illustrative of the API shape only — it is not asserted
against in any test, matching how other README usage snippets in this file
are presented.) ai.tools() returns the same ToolRegistry .tool()
registers into, so .get()/.has()/.list()/.listByPermission() are
also available on it; .execute()'s third, optional grantedPermissions
argument is only relevant for tools that declare a permissions array —
calculator above declares none, so it runs unconditionally.
Bulk-Registering via a Plugin
A plugin can declare engines, prompts, tools, and strategies all at once by
also satisfying @aidex/plugins' ExtendedPlugin interface — .plugin()
recognizes this automatically, with no separate registration call needed:
import { AIBuilder } from '@aidex/sdk';
import type { ExtendedPlugin } from '@aidex/plugins';
import { GeminiProvider } from '@aidex/providers';
const myPlugin: ExtendedPlugin = {
name: 'my-plugin',
registerTools() {
return [
{
id: 'calculator',
name: 'calculator',
description: 'Evaluates a simple arithmetic expression',
async execute(input: string) {
return String(eval(input));
},
},
];
},
// registerEngines()/registerPrompts()/registerStrategies() are equally
// optional — declare any subset.
};
const ai = new AIBuilder()
.provider(new GeminiProvider({ apiKey: '...' }))
.plugin(myPlugin)
.build();
console.log(await ai.tools().execute('calculator', '2 + 2'));registerEngines()/registerPrompts()/registerTools() entries are
routed through .engine()/.prompt()/.tool() respectively — the exact
same registries and duplicate-id validation a direct call to those methods
would use. registerStrategies() entries have no public single-item
registration method to route through, so they're instead collected into a
private, SDK-internal map keyed by strategy.name and are only applied to
the kernel inside .build(), immediately after the auto-registered
TextGenerationStrategy — see AIBuilder.plugin(plugin) above for why
this is a private collection rather than a public one, and why AIBuilder
reproduces this fan-out itself instead of delegating to
@aidex/plugins' PluginManager.
Dependency direction
@aidex/sdk is the top of the runtime dependency graph. Runtime
dependencies: @aidex/core and @aidex/strategies (for the auto-registered
TextGenerationStrategy), @aidex/engines (for engine registration and
dispatch), @aidex/workflow (for workflow registration and execution),
@aidex/prompts (for prompt registration and rendering), @aidex/catalog
(for engine-metadata discovery), and @aidex/tools (for tool registration
and permission-gated execution). @aidex/plugins is also listed as a
dependency, but only for its ExtendedPlugin type — AIBuilder.ts imports
it with import type, which compiles away completely; nothing in this
package's runtime code calls into @aidex/plugins' own code (its
PluginManager is deliberately never instantiated here — see
AIBuilder.plugin() above). @aidex/providers is a devDependency only,
used by this package's own test suite (StubProvider); the test suite also
imports @aidex/plugins' StubPlugin as a value, which is covered by the
@aidex/plugins dependency already listed above. No dependency on
@aidex/memory or @aidex/observability — those integrate later.
Feature packages (@aidex/document, @aidex/content, @aidex/design,
@aidex/media, @aidex/marketing) declare @aidex/sdk as a dependency
for its exported types only — specifically FeaturePackage, the
manifest shape their featurePackage.ts constant is typed against
(import type { FeaturePackage } from '@aidex/sdk'). This is an
intentional, approved exception, not drift: every such import is
import type, never a runtime value import, so it compiles away
completely and introduces no runtime coupling — nothing in a feature
pack calls into @aidex/sdk code at execution time. The dependency arrow
still points one way: @aidex/sdk never imports any feature package, in
either direction, so the runtime graph stays acyclic — feature pack →
@aidex/sdk (type-only) → @aidex/core/@aidex/engines/@aidex/workflow/
@aidex/prompts/@aidex/catalog, with no edge back from @aidex/sdk to any
feature pack.
