AI-Perceivable Core
Build once, invoke by Code or AI. Every call validated, authorized, and evidenced.
A governed runtime for agent-callable capabilities — schema, ACL, approval, and audit enforced at every call.
apcore is an AI-Perceivable module standard that makes every interface naturally perceivable and understandable by AI through enforced Schema definitions and behavioral annotations. It provides schema validation, access control, middleware pipelines, and observability built in.
- Schema-driven modules — Define input/output schemas with TypeBox for runtime validation
- Executor pipeline — Secured execution lifecycle: context → call chain guard → lookup → ACL → approval gate → middleware before → validation → execute → output validation → middleware after → return
- Registry system — File-based module discovery with metadata, dependencies, and topological ordering; multi-class discovery from a single file
- Binding loader — YAML-based module registration for no-code integration
- Access control (ACL) — Pattern-based rules with identity types, roles, and call-depth conditions
- Approval system — Pluggable approval gate in the executor pipeline with sync and async (polling) flows, built-in handlers, and tracing integration
- Middleware — Onion-model middleware with before/after/onError hooks and error recovery; built-in
CircuitBreakerMiddleware(CLOSED/OPEN/HALF_OPEN); tracing is provided by the observabilityTracingMiddleware - Observability — Tracing (spans +
BatchSpanProcessor+ exporters), metrics (counters + histograms + Prometheus export with/metrics//healthz//readyz), structured logging withRedactionConfig - System modules — Built-in
system.*modules for AI bidirectional introspection: health, manifest, usage, and runtime control (update_config,reload_module,toggle_feature). Audit trail viaAuditStore, config persistence viaoverridesPath, usage metrics in Prometheus, bulk reload viapath_filterglob - Event system —
EventEmitterwith subscriber-levelCircuitBreakerWrapper, built-inFileSubscriber,StdoutSubscriber,FilterSubscriber, and pluggable custom types - Async tasks —
AsyncTaskManagerwith injectableTaskStore(bring your own Redis/Postgres backend),RetryConfigwith exponential backoff, and opt-in background reaper - Schema export — JSON/YAML schema export with strict and compact modes
- Caching & pagination annotations —
cacheable,cacheTtl,cacheKeyFieldsfor result caching;paginated,paginationStylefor paginated modules - Config Bus — Namespace-based configuration registry with typed access, env prefix dispatch, hot-reload, and external config mounting (
Config.registerNamespace(),config.namespace(),config.bind<T>(),config.mount())
ExecutionPolicy/PolicyRuleexecution-time governance (#76) — An external policy can force or exempt approval on already-registered modules by ID pattern, make adestructiveannotation imply approval viagateDestructive, and fail closed withstrict: truewhen a gated module has noApprovalHandler. The most specific matching rule wins; on a tie the more restrictive one does. See Execution policy andexamples/execution-policy.ts.- Governance events (#77) —
apcore.approval.decisionandapcore.policy.overrideare published on the event bus alongside the existingapcore.acl.denied.
- Config-driven ACL discovery (#74, D-64) —
ACL.discover(config)resolvesacl.root(default./acl) relative to the config file's own directory and loads an ACL only if that path exists; a missing path attaches no ACL rather than synthesizing a default-deny. Auto-wired in theAPCoreconstructor. Seeexamples/acl-config-driven.ts.
- Per-instance
ToggleStateisolation (#71) — eachAPCoreinstance owns oneToggleState(new APCore({ toggleState? }), read-only viaclient.toggleState) threaded into both the write path (system.control.toggle_feature) and the read path (Executor→ module lookup), so disabling a module on one instance no longer affects another in the same process. - Agent-governance conformance coverage (#72) — drivers for the canonical
toggle_state_isolation.jsonandacl_agent_scoping.jsonfixtures lock the AI-agent tool-governance scenario as a cross-language contract.
- AI error-recovery metadata at the source (#70) — Framework errors now carry a default
userFixableresolved per error code (USER_FIXABLE_BY_CODE), plus filledaiGuidancedefaults, so the recovery contract flows to every surface (MCP/CLI/A2A) from one module/error definition instead of being backfilled per adapter. At parity with apcore-python / apcore-rust 0.23.0, locked by the sharederror_recovery_metadata.jsonconformance fixture.
ContextKey<T>typed context state (#63) — Type-safe accessor forContext.dataslots withget/set/delete/exists/scoped; also available via theapcore-js/context-keyssubpath import for tree-shakeable consumers.StreamingModuleinterface (#62) — Formal streaming contract viaSTREAMING_MARKERsymbol;isStreamingModule()detects implementations and emitsStreamingInterfaceErrorwhen contract is violated.- Middleware duplicate detection (#64) —
MiddlewareManager.add(mw, { allowDuplicate, identityKey })rejects accidental double-registration; opt-out and custom identity keys supported for explicit duplicates. - Event retry + DLQ (#61) —
EventSubscribernow exposes aretryconfig field (max attempts + exponential backoff) and an optionalonFailure(event, error, attemptCount)hook acting as a per-subscriber dead-letter queue. - Registry async deferred-publish (#65) —
Registry.register()returns aPromise<void>; modules with an asynconLoad()stay hidden fromget()/has()until load completes, andapcore.registry.module_load_failedis emitted on rejection. - Reserved-namespace query API (#60) —
Config.reservedNamespacesstatic getter and top-levelRESERVED_NAMESPACESexport let callers pre-validate namespace names before callingConfig.registerNamespace().
See examples/v022-tour.ts for a runnable tour of each surface.
For full documentation, including Quick Start guides and API reference, visit: https://aiperceivable.github.io/apcore/getting-started/
- Node.js >= 20.0.0
- TypeScript >= 5.5
npm install apcore-jsNote: The npm package is published as
apcore-js(theapcorename is reserved on npm). Python usesapcore, Rust uses theapcorecrate.
apcore-js ships dual entry points via package.json conditional exports:
- Node (default):
./dist/index.js— the full surface. - Browser:
./dist/browser/index.js— auto-selected by tree-shaking bundlers (Vite, Webpack, esbuild, Rollup) through the"browser"exports condition.
The browser bundle excludes Node-only surfaces:
Configclass,BindingLoader,SchemaLoader(filesystem-bound)- All
system.*sys-modules (control, manifest, health, usage, audit) - Events runtime:
EventEmitter, all built-in subscribers,AsyncTaskManager,TaskStatus - Observability runtime:
TracingMiddleware,MetricsCollector, exporters,PrometheusExporter RESERVED_NAMESPACES,StreamingInterfaceError,DuplicateModuleIdError
ACL.load(yamlPath) throws ACLRuleError in the browser because the filesystem is unavailable; ACL programmatic construction still works.
Available in the browser: Registry programmatic register/get/list/iter/unregister, Executor, ACL programmatic, Context, middleware pipeline, schema validation, all error classes, and the utility helpers.
The APCore client provides a unified entry point that manages Registry and Executor for you:
import { Type } from '@sinclair/typebox';
import { APCore } from 'apcore-js';
const client = new APCore();
// Register a module
client.module({
id: 'math.add',
description: 'Add two numbers',
inputSchema: Type.Object({ a: Type.Number(), b: Type.Number() }),
outputSchema: Type.Object({ sum: Type.Number() }),
execute: (inputs) => ({ sum: (inputs.a as number) + (inputs.b as number) }),
});
// Call, validate, stream — all from one client
const result = await client.call('math.add', { a: 10, b: 5 });
// => { sum: 15 }
const preflight = await client.validate('math.add', { a: 10, b: 5 });
// => { valid: true, checks: [...], requiresApproval: false, errors: [] }APCore.disable, APCore.enable, APCore.on, and APCore.off are gated on the
optional system-modules subsystem. They require a Config instance with
sys_modules.enabled: true to be passed into the APCore constructor —
otherwise the calls throw with a clear "sys_modules must be enabled" message.
import { APCore, Config } from 'apcore-js';
// Inline config (equivalent to writing the same YAML and calling Config.load):
const config = new Config({
sys_modules: {
enabled: true,
// Optional sub-systems:
events: { enabled: true }, // required for on/off (event emitter)
control: { enabled: true }, // required for disable/enable (toggle)
},
});
const client = new APCore({ config });
// Now safe to call:
await client.disable('math.add');
await client.enable('math.add');
// Enabling/disabling a module fires the canonical apcore.module.toggled event;
// read event.data.enabled to see the new state (see examples/events.ts).
client.on('apcore.module.toggled', (event) => {
console.log('Toggled:', event.data.module_id, 'enabled:', event.data.enabled);
});If you do not need these control/events APIs, omit the Config entirely (as in
the basic Quick Start above).
import { Type } from '@sinclair/typebox';
import { FunctionModule, Registry, Executor } from 'apcore-js';
const greet = new FunctionModule({
execute: (inputs) => ({ greeting: `Hello, ${inputs.name}!` }),
moduleId: 'example.greet',
inputSchema: Type.Object({ name: Type.String() }),
outputSchema: Type.Object({ greeting: Type.String() }),
description: 'Greet a user',
});
const registry = new Registry();
registry.register('example.greet', greet);
const executor = new Executor({ registry });
const result = await executor.call('example.greet', { name: 'World' });
// => { greeting: 'Hello, World!' }| Class | Description |
|---|---|
APCore |
High-level client — register modules, call, stream, validate, listModules, describe, on/off, disable/enable. Note: disable, enable, on, and off require sys_modules.enabled: true in the Config passed to APCore (see Quick Start: Enabling sys_modules). |
Registry |
Module storage — discover, register, get, list, watch |
Executor |
Execution engine — call with middleware pipeline, ACL, approval |
Context |
Request context — trace ID, identity, call chain, cancel token |
Config |
Configuration — load from YAML, namespace bus, get/set/bind values |
ACL |
Access control — rule-based caller/target authorization |
ExecutionPolicy / PolicyRule |
Execution-time governance overrides (#76) — force/exempt approval by ID pattern, gateDestructive, strict fail-closed |
Middleware |
Pipeline hooks — before/after/onError interception |
CircuitBreakerMiddleware |
Per-(module, caller) circuit breaker — CLOSED/OPEN/HALF_OPEN with configurable threshold and cooldown |
TracingMiddleware |
Span tracing — apcore.module.execute spans kept as a stack in _apcore.mw.tracing.spans with parent_span_id links; self-contained, no runtime @opentelemetry/* dependency |
EventEmitter |
Event system — subscribe, emit, flush |
CircuitBreakerWrapper |
Subscriber-level circuit breaker — protects EventEmitter subscribers from cascading failures |
AsyncTaskManager |
Background task execution — injectable store, retry with backoff, opt-in reaper |
PrometheusExporter |
HTTP metrics server — /metrics, /healthz, /readyz; optional usageCollector for usage gauges |
InMemoryAuditStore |
Control module audit log — records actor, action, before/after change for every control call |
Config acts as an ecosystem-level Config Bus. Any package can register a namespace with optional JSON Schema validation, environment variable prefix, and defaults.
import { Config } from 'apcore-js';
// Register a namespace (class-level, shared across all Config instances).
// registerNamespace takes a single options object; `name` is a field on it.
Config.registerNamespace({
name: 'myPlugin',
envPrefix: 'MY_PLUGIN',
defaults: { timeout: 5000, retries: 3 },
schema: {
type: 'object',
properties: {
timeout: { type: 'number' },
retries: { type: 'number' },
},
},
});
const config = Config.load('apcore.yaml');
// Dot-path access with namespace resolution
const timeout = config.get('myPlugin.timeout'); // 5000 (or env override)
// Full namespace subtree
const pluginConfig = config.namespace('myPlugin');
// Typed access — pass a class constructor; its constructor receives the namespace dict
class MyPluginConfig {
timeout: number;
retries: number;
constructor(data: Record<string, unknown>) {
this.timeout = (data['timeout'] as number) ?? 5000;
this.retries = (data['retries'] as number) ?? 3;
}
}
const typed = config.bind('myPlugin', MyPluginConfig);
// Mount an external config source (e.g. an existing config file)
config.mount('myPlugin', { fromFile: './my-plugin.yaml' });
// Or from an in-memory object:
config.mount('myPlugin', { fromDict: { timeout: 10000 } });
// Introspect registered namespaces
// => Array<{ name: string; envPrefix: string | null; hasSchema: boolean }>
const namespaces = Config.registeredNamespaces();Merge priority (highest wins): environment variables > config file > namespace defaults.
Two prefix conventions are supported:
| Convention | Applies to | Example |
|---|---|---|
APCORE_ + KEY_PATH |
Legacy flat keys | APCORE_EXECUTOR_DEFAULT__TIMEOUT=5000 |
APCORE_ + namespace prefix |
apcore sub-package namespaces | APCORE_OBSERVABILITY_TRACING_ENABLED=true |
Within the suffix, a single _ maps to . (a path separator) and __ maps to a
literal _. So APCORE_EXECUTOR_DEFAULT__TIMEOUT sets executor.default_timeout
— writing APCORE_EXECUTOR_DEFAULT_TIMEOUT would instead target the key
executor.default.timeout, which does not exist. This matches apcore-python and
apcore-rust.
Legacy APCORE_* overrides apply in namespace mode too: they are merged into the
apcore namespace (PROTOCOL_SPEC §9.6.2), so the example above also resolves via
config.get('apcore.executor.default_timeout').
apcore pre-registers the following namespaces and env prefixes:
| Namespace | Env prefix | Wraps |
|---|---|---|
observability |
APCORE_OBSERVABILITY |
observability.* — tracing, metrics, logging, error_history, platform_notify |
obs |
APCORE_OBS |
obs.redaction.* — sensitive_keys, regex_patterns, replacement |
sys_modules |
APCORE_SYS |
sys_modules.* — health, manifest, usage, control, events |
Namespace names are snake_case, matching apcore-python and the YAML config keys: use
config.namespace('sys_modules'), not'sysModules'.
Sub-packages use their own APCORE_ prefixed name (e.g. APCORE_MCP for apcore-mcp). The longest-prefix-match dispatch algorithm disambiguates correctly (APCORE_OBS_* vs APCORE_OBSERVABILITY_*).
config.reload() re-reads the source YAML, re-detects legacy/namespace mode, re-applies all namespace defaults and env overrides, re-validates, and re-reads any mounted files.
const config = Config.load('apcore.yaml');
// ... runtime config change on disk ...
config.reload(); // picks up all changesConfiguration files support two modes. Legacy mode (no apcore: key) is fully backward compatible. Namespace mode is activated when an apcore: top-level key is present; each namespace occupies its own top-level section. The _config reserved namespace controls validation behavior.
# Namespace mode
apcore:
version: "0.26.0"
_config:
strict: true
observability:
tracing:
enabled: true
samplingRate: 1.0
myPlugin:
timeout: 10000
retries: 5registerSysModules() auto-registers the built-in system.* modules that let AI agents query, monitor, and control the apcore runtime. Enable them via sys_modules.enabled: true in config, and pass the optional hardening options for production use:
import { registerSysModules, InMemoryAuditStore } from 'apcore-js';
const auditStore = new InMemoryAuditStore();
registerSysModules(registry, executor, config, null, {
failOnError: true, // throw on any registration failure (default: false)
overridesPath: '/etc/apcore/overrides.yaml', // persist runtime changes across restarts
auditStore, // record every control-module action with actor + change
});
// Available system modules:
// system.health.summary / system.health.module — health status + error rates
// system.manifest.module / system.manifest.full — module introspection
// system.usage.summary / system.usage.module — call counts + latency trends
// system.control.update_config — hot-patch config values
// system.control.reload_module — hot-reload from disk; supports path_filter glob
// system.control.toggle_feature — disable/enable modules at runtime
// Query the audit log after control calls:
const entries = auditStore.query({ moduleId: 'system.control.update_config' });
// entries[0] = { timestamp, action, targetModuleId, actorId, actorType, traceId, change }Prometheus usage metrics — wire PrometheusExporter with the UsageCollector returned by registerSysModules:
import { PrometheusExporter, MetricsCollector } from 'apcore-js';
const ctx = registerSysModules(registry, executor, config);
const exporter = new PrometheusExporter({
collector: new MetricsCollector(),
usageCollector: ctx.usageCollector, // adds apcore_usage_* metrics to /metrics
});
exporter.start({ port: 9090 });
// GET /metrics now includes:
// apcore_usage_calls_total{module_id="math.add",status="success"} 5000
// apcore_usage_error_rate{module_id="math.add"} 0.0004
// apcore_usage_p99_latency_ms{module_id="math.add"} 45.0BindingLoader registers modules from a YAML file instead of code — the
no-code integration path.
# format-date.binding.yaml
spec_version: "1.0"
bindings:
- module_id: "utils.format_date"
# "<module path>:<exported symbol>". The module path goes to Node's
# `import()` verbatim, so it must be a package specifier or an absolute
# path — a bare relative name is NOT resolved against the YAML file.
target: "/abs/path/to/format-date.ts:formatDateString"
description: "Format a date string into a specified format"
version: "1.0.0"
auto_schema: trueimport { BindingLoader, Executor, Registry } from 'apcore-js';
const registry = new Registry();
const loader = new BindingLoader();
await loader.loadBindings('./format-date.binding.yaml', registry);
// Or every *.binding.yaml in a directory:
// await loader.loadBindingDir('./bindings', registry);
const executor = new Executor({ registry });
await executor.call('utils.format_date', {
dateString: '2024-01-15',
outputFormat: '%B %d, %Y',
});The target is invoked as func(inputs, context). Schemas come from one of four
mutually exclusive modes: inline input_schema / output_schema, an external
schema_ref, auto_schema (infers from the target module's exported
inputSchema / outputSchema), or — with none specified — a permissive
fallback. Pass new BindingLoader({ trustedPackagePrefixes: [...] }) to refuse
importing any target outside an allowlist.
See examples/bindings/format-date/ for a
runnable end-to-end binding.
ExecutionPolicy (with PolicyRule) applies governance to already-registered
modules at execution time, without touching their code or annotations. It is
consulted by the approval gate (pipeline step 5).
import { APCore, ExecutionPolicy, PolicyRule } from 'apcore-js';
const policy = new ExecutionPolicy(
[
new PolicyRule('orders.delete_*', {
requiresApproval: true,
reason: 'destructive order ops need human sign-off',
}),
// A rule can also exempt: requiresApproval: false.
new PolicyRule('orders.read_*', { requiresApproval: false }),
],
{
// Any module annotated destructive: true also needs approval.
gateDestructive: true,
// A module that needs approval but has no ApprovalHandler fails CLOSED
// (ApprovalDeniedError) instead of silently executing.
strict: true,
},
);
const client = new APCore({ policy, approvalHandler });
// Runtime swap: executor.setPolicy(otherPolicy)Pattern matching reuses the ACL wildcard semantics (Algorithm A08) and specificity scoring (A10): the most specific matching rule wins, and on a tie the more restrictive rule does.
An operator-authored YAML/JSON governance document loads via
ExecutionPolicy.fromObject(parsed):
gate_destructive: true
strict: true
rules:
- pattern: "orders.delete_*"
requires_approval: true
reason: "destructive order operations need human sign-off"Parsing is strict: unknown keys throw, and gate_destructive / strict must
be real booleans — a typo or a "false" string cannot silently disable a
control.
See examples/execution-policy.ts for a
runnable end-to-end demo of all three behaviours.
New error codes added in v0.15.0:
| Code | Description |
|---|---|
CONFIG_NAMESPACE_DUPLICATE |
Config.registerNamespace() called with an already-registered name |
CONFIG_NAMESPACE_RESERVED |
Config.registerNamespace() called with a reserved name (e.g. _config) |
CONFIG_ENV_PREFIX_CONFLICT |
Two namespaces declare the same envPrefix |
CONFIG_MOUNT_ERROR |
config.mount() cannot read or parse the external source |
CONFIG_BIND_ERROR |
config.bind<T>() or config.getTyped<T>() type guard fails |
ERROR_FORMATTER_DUPLICATE |
ErrorFormatterRegistry.register() called for an already-registered surface |
New error codes added in v0.20.0:
| Code | Description |
|---|---|
CIRCUIT_BREAKER_OPEN |
CircuitBreakerMiddleware short-circuited a call because the circuit is OPEN |
MODULE_RELOAD_CONFLICT |
Both module_id and path_filter supplied to system.control.reload_module |
SYS_MODULE_REGISTRATION_FAILED |
registerSysModules() with failOnError: true and a module failed to register |
MODULE_ID_CONFLICT |
Two classes in the same file produce the same module ID segment (discoverMultiClass) |
INVALID_SEGMENT |
A derived class segment does not match ^[a-z][a-z0-9_]*$ |
ID_TOO_LONG |
A derived module ID exceeds 192 characters |
apcore 0.15.0 resolved two event-type collisions in favor of dot-namespaced canonical
names. The legacy short-form aliases (module_health_changed, config_changed) were
emitted during the 0.15.x transition and have been removed as of 0.18.0.
| Event name | Meaning |
|---|---|
"apcore.module.toggled" |
Module enabled/disabled toggle |
"apcore.health.recovered" |
Error-rate recovery after spike |
"apcore.config.updated" |
Config key updated at runtime |
"apcore.module.reloaded" |
Module reloaded from disk |
Naming convention: apcore.* is reserved for core events. Adapter packages use their own prefix (apcore-mcp.*, apcore-a2a.*, apcore-cli.*).
The examples/ directory contains runnable demos:
Initializes an APCore client, registers modules inline, and calls them.
import { Type } from '@sinclair/typebox';
import { APCore } from 'apcore-js';
const client = new APCore();
client.module({
id: 'math.add',
description: 'Add two integers',
inputSchema: Type.Object({ a: Type.Number(), b: Type.Number() }),
outputSchema: Type.Object({ sum: Type.Number() }),
execute: (inputs) => ({ sum: (inputs.a as number) + (inputs.b as number) }),
});
client.module({
id: 'greet',
description: 'Greet a user by name',
inputSchema: Type.Object({
name: Type.String(),
greeting: Type.Optional(Type.String()),
}),
outputSchema: Type.Object({ message: Type.String() }),
execute: (inputs) => ({
message: `${(inputs.greeting as string) || 'Hello'}, ${inputs.name}!`,
}),
});
const result = await client.call('math.add', { a: 10, b: 5 });
console.log(result); // { sum: 15 }
const greetResult = await client.call('greet', { name: 'Alice' });
console.log(greetResult); // { message: 'Hello, Alice!' }Demonstrates the core FunctionModule structure with TypeBox schemas.
import { Type } from '@sinclair/typebox';
import { FunctionModule } from 'apcore-js';
export const greetModule = new FunctionModule({
moduleId: 'greet',
description: 'Greet a user by name',
inputSchema: Type.Object({ name: Type.String() }),
outputSchema: Type.Object({ message: Type.String() }),
execute: (inputs) => ({ message: `Hello, ${inputs.name}!` }),
});Shows behavioral annotations and simulated database lookup.
import { Type } from '@sinclair/typebox';
import { FunctionModule, createAnnotations } from 'apcore-js';
const users: Record<string, { id: string; name: string; email: string }> = {
'user-1': { id: 'user-1', name: 'Alice', email: 'alice@example.com' },
'user-2': { id: 'user-2', name: 'Bob', email: 'bob@example.com' },
};
export const getUserModule = new FunctionModule({
moduleId: 'user.get',
description: 'Get user details by ID',
inputSchema: Type.Object({ userId: Type.String() }),
outputSchema: Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String(),
}),
// `ModuleAnnotations` is a total interface — `createAnnotations()` fills the
// rest (cacheable, cacheTtl, cacheKeyFields, paginated, paginationStyle,
// extra) from `DEFAULT_ANNOTATIONS`. A partial object literal will not
// compile.
annotations: createAnnotations({
readonly: true,
destructive: false,
idempotent: true,
requiresApproval: false,
openWorld: true,
streaming: false,
}),
execute: (inputs) => {
const user = users[inputs.userId as string];
if (!user) {
return { id: inputs.userId as string, name: 'Unknown', email: 'unknown@example.com' };
}
return { ...user };
},
});Demonstrates destructive annotations, ModuleExample for AI-perceivable documentation, metadata, and ContextLogger usage.
import { Type } from '@sinclair/typebox';
import { FunctionModule, ContextLogger, createAnnotations } from 'apcore-js';
import type { Context } from 'apcore-js';
export const sendEmailModule = new FunctionModule({
moduleId: 'email.send',
description: 'Send an email message',
inputSchema: Type.Object({
to: Type.String(),
subject: Type.String(),
body: Type.String(),
// `x-sensitive: true` (or a `_secret_` field-name prefix) is what makes the
// executor redact the value in captured inputs/outputs and logs.
apiKey: Type.String({ 'x-sensitive': true }),
}),
outputSchema: Type.Object({
status: Type.String(),
messageId: Type.String(),
}),
tags: ['email', 'communication', 'external'],
version: '1.2.0',
metadata: { provider: 'example-smtp', maxRetries: 3 },
annotations: createAnnotations({
readonly: false,
destructive: true,
idempotent: false,
requiresApproval: false,
openWorld: true,
streaming: false,
}),
examples: [
{
title: 'Send a welcome email',
inputs: { to: 'user@example.com', subject: 'Welcome!', body: '...', apiKey: 'sk-xxx' },
output: { status: 'sent', messageId: 'msg-12345' },
description: 'Sends a welcome email to a new user.',
},
],
execute: (inputs, context: Context) => {
const logger = ContextLogger.fromContext(context, 'send_email');
logger.info('Sending email', { to: inputs.to as string, subject: inputs.subject as string });
const hash = Math.abs(
(inputs.to as string).split('').reduce((a, c) => a + c.charCodeAt(0), 0) % 100000,
);
const messageId = `msg-${String(hash).padStart(5, '0')}`;
logger.info('Email sent successfully', { messageId });
return { status: 'sent', messageId };
},
});import { Type } from '@sinclair/typebox';
import { module } from 'apcore-js';
export const addModule = module({
id: 'math.add',
description: 'Add two integers',
inputSchema: Type.Object({ a: Type.Number(), b: Type.Number() }),
outputSchema: Type.Object({ sum: Type.Number() }),
execute: (inputs) => ({ sum: (inputs.a as number) + (inputs.b as number) }),
});# Install dependencies
npm install
# Type check
npm run typecheck
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Build
npm run build- Core executor pipeline
- Schema validation (strict mode, type coercion)
- Middleware chain (ordering, transforms, error recovery, circuit breaker)
- ACL enforcement (patterns, conditions, identity types)
- Registry system (scanner, metadata, entry points, dependencies, multi-class discovery)
- Binding loader (YAML loading, target resolution, schema modes)
- Observability (tracing, BatchSpanProcessor, metrics, Prometheus export, structured logging with redaction)
- Event system (circuit breaker wrapper, subscriber types, filter/file/stdout)
- System modules (health, manifest, usage, control, audit trail, overrides persistence, Prometheus usage metrics)
- Async tasks (pluggable store, retry backoff, reaper)
- Cross-language conformance suite (
tests/conformance.test.ts) — canonical JSON fixtures fromapcore/conformance/fixtures/run identically across Python, TypeScript, and Rust SDKs
- Documentation: https://aiperceivable.github.io/apcore/getting-started/
- Specification: https://github.com/aiperceivable/apcore
- GitHub: https://github.com/aiperceivable/apcore-typescript
- npm: https://www.npmjs.com/package/apcore-js
- Issues: https://github.com/aiperceivable/apcore-typescript/issues
Apache-2.0