@nexart/agent-kit
v0.5.2
Published
Thin convenience wrappers for building AI agents with NexArt certification, with project bundle ergonomics and high-level workflow orchestration
Maintainers
Readme
@nexart/agent-kit
A thin convenience layer for builders who want agent tool calls and final decisions to produce tamper-evident, verifiable execution records with minimal integration work.
What this is
@nexart/agent-kit wraps @nexart/ai-execution to make it easier to attach Certified Execution Records (CERs) to individual tool calls, agent decisions, and multi-step project workflows. It works with @nexart/signals to bind upstream signal evidence into those records.
It does not change CER hashing, attestation, or verification semantics. Every bundle it produces is a standard cer.ai.execution.v1 or cer.project.bundle.v1 artifact that verifies exactly like any other NexArt artifact.
What this is NOT
- Not an agent framework
- Not an orchestration system
- Not a planning or memory layer
- Not a multi-agent runtime
- Not a re-implementation of project bundle schema or projectHash — those are owned by
@nexart/ai-execution
Installation
npm install @nexart/agent-kit@nexart/ai-execution and @nexart/signals are installed automatically as dependencies. If your own code imports from those packages directly (e.g. to call verifyCer or createSignalCollector), install them explicitly too:
npm install @nexart/agent-kit @nexart/ai-execution @nexart/signalsWhen to use each export
| Use this | When you want to… |
|----------|------------------|
| startWorkflow() | Go from zero to a verified multi-step project bundle with minimal orchestration code — auto-manages executionId, step tracking, CER collection, and bundle construction |
| wrapTool() | Give an individual tool call its own CER — one record per invocation, capturing input, output, and AIEF-06 tool evidence |
| certifyDecision() | Certify the final decision or outcome of an agent workflow — one record for the concluded result, optionally with upstream signal evidence |
| createProjectSession() | Group multiple pre-certified bundles into one cer.project.bundle.v1 record — progressively during a run, or retroactively after the fact |
| exportProjectBundle() | Serialize a project bundle to canonical JSON for storage or transport |
| importProjectBundle() | Parse and verify a project bundle from canonical JSON, throwing on any integrity failure |
Exports
| Export | Description |
|--------|-------------|
| startWorkflow(opts?) | High-level helper — auto-manages the full CER + project bundle lifecycle for a multi-step workflow |
| wrapTool(opts) | Wraps an async function and returns a callable that produces a CER per invocation |
| certifyDecision(params) | Certifies a final agent decision or workflow outcome |
| createProjectSession(opts) | Creates a ProjectSession for assembling a cer.project.bundle.v1 |
| exportProjectBundle(bundle) | Serializes a project bundle to stable canonical JSON |
| importProjectBundle(json) | Parses and verifies a project bundle from JSON; throws CerVerificationError on failure |
| ProjectBundleRegistrationError | Re-exported from @nexart/ai-execution — use in catch clauses without needing to import both packages |
| AGENT_KIT_VERSION | Package version string ('0.5.2') |
| v0.5.0 verification helpers | |
| verifyAiCerBundleDetailed(bundle) | Re-exported from @nexart/ai-execution. Returns a detailed verification report (status, checks, reasonCodes, certificateHash, bundleType, verifiedAt, verifier) for a single CER bundle |
| verifyProjectBundleDetailed(bundle) | New in v0.5.0. Verifies a ProjectBundle envelope and additionally runs verifyAiCerBundleDetailed per step via bundle.embeddedBundles. Returns { ok, code, errors, envelope, steps[] } — see "Detailed project bundle verification" below |
| ReasonCode | Re-exported enum from @nexart/ai-execution — machine-readable reason codes attached to verification reports |
| v0.5.0 context helpers (re-exported from @nexart/ai-execution) | |
| getContextInfo(bundle) | Extracts the CerContextInfo block from a CER bundle |
| computeContextHash(signals) | Computes the canonical hash of a signal array (used internally during certification; exposed for advanced parity-checking) |
| summarizeContext(signals) | Returns a ContextSummary ({ count, types, stepRange }) — same helper used by WorkflowController.getContextSummary() |
| buildContextInfo(signals) | Builds a complete CerContextInfo block from a signal array |
Basic example
import { wrapTool, certifyDecision } from '@nexart/agent-kit';
import { createSignalCollector } from '@nexart/signals';
// ── 1. Wrap a tool ────────────────────────────────────────────────────────────
const search = wrapTool({
name: 'web-search',
source: 'my-agent',
run: async (args: { query: string }) => {
const results = await fetchResults(args.query);
return results;
},
});
// ── 2. Call it — get result + CER ────────────────────────────────────────────
const { result, bundle, certificateHash } = await search({ query: 'nexart' });
console.log(certificateHash); // sha256:<64 hex chars>
// ── 3. Collect signals ────────────────────────────────────────────────────────
const collector = createSignalCollector({ defaultSource: 'github-actions' });
collector.add({ type: 'ci-pass', source: 'github-actions', payload: { build: 'green' } });
collector.add({ type: 'approval', source: 'github', actor: 'alice', payload: { pr: 42 } });
// ── 4. Certify the final decision ─────────────────────────────────────────────
const { bundle: decision, certificateHash: decisionHash } = await certifyDecision({
decision: 'Approve deployment to production',
output: 'APPROVED',
provider: 'openai',
model: 'gpt-4o',
signals: collector.export().signals, // evidence bound into the CER
appId: 'my-app',
workflowId: 'release-flow',
});API reference
startWorkflow(opts?)
The highest-level entry point in @nexart/agent-kit. Handles CER creation, step sequencing, and project bundle assembly for you. Call it once, run your steps, call finish().
function startWorkflow(opts?: WorkflowOptions): WorkflowControllerWhen to use this helper
startWorkflow() is designed for simple, sequential, single-branch workflows where each step runs one after another in a straight line.
If your workflow has branching, parallel branches, or a DAG-structured step graph, use createProjectSession() directly — it accepts explicit parentStepIds per step and gives you full control over the graph shape.
What step() records
step(name, fn) records the following in the CER:
- input: the step
name— a string label describing what the step does - output: the return value of
fn(), JSON-serialized if not already a string
What is NOT recorded: closure variables, fn arguments, intermediate state, or side effects. Only the return value of fn() is certified. Design your steps so that the return value is the meaningful output of that phase.
// ✓ the return value 'billing' is certified as the output
const intent = await wf.step('classify', () => classify(ticket));
// ✓ passing the previous result via closure — 'intent' is captured correctly
const docs = await wf.step('retrieve', () => retrieve(intent));
// The step name 'classify' is recorded as the CER input label.
// The string 'billing' (or JSON of the object) is recorded as the CER output.executionId vs workflowId
These two identifiers serve different roles:
| Identifier | Scope | What it is |
|---|---|---|
| wf.executionId | Workflow-level | Shared ID generated at startWorkflow() time. Stored as workflowId on every step CER. Use this to correlate all steps from the same workflow in logs or audit trails. |
| CER executionId | Per-step | Each step CER gets its own unique executionId. This is required — verifyProjectBundle rejects bundles where two CERs share the same executionId. |
wf.executionId is not the executionId on the CERs. It is the workflowId.
const wf = startWorkflow({ projectTitle: 'My workflow' });
console.log(wf.executionId);
// → 'f3c2a1b0-…' — shared workflowId stored on every step CER
await wf.step('step-a', () => 'a');
await wf.step('step-b', () => 'b');
const { projectBundle } = wf.finish();
// Each step entry in stepRegistry has a stepId.
// The embedded CER for that step lives in embeddedBundles[stepId].
const firstStepId = projectBundle.stepRegistry[0].stepId;
const firstCer = projectBundle.embeddedBundles[firstStepId];
// workflowId ties the step to this workflow:
console.log(firstCer.snapshot.workflowId === wf.executionId); // true
// executionId is unique per CER, not shared:
console.log(firstCer.snapshot.executionId === wf.executionId); // falsefinish() is synchronous
finish() is a synchronous function — it returns WorkflowFinishResult directly, not a Promise. This is intentional.
By the time step() returns, the CER for that step is already sealed. finish() only calls createProjectBundle() from @nexart/ai-execution, which is pure computation (canonical JSON serialization + SHA-256). There is no I/O, no async work, and no network access at finish time.
Linear chain — how steps link
Each step's parentStepIds is automatically set to [stepId of the previous step]:
step-0 (parentStepIds: undefined)
└── step-1 (parentStepIds: [step-0.stepId])
└── step-2 (parentStepIds: [step-1.stepId])This structure is inferred automatically. You do not need to manage step IDs or parent links.
API reference
WorkflowOptions — all fields optional:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| projectTitle | string? | 'Workflow' | Human-readable title written to the project bundle |
| projectGoal | string? | — | Root goal or prompt. Included in projectHash. |
| projectSummary | string? | — | Summary of what the workflow achieved. Included in projectHash. |
| appName | string? | — | Application name. Included in projectHash. |
| frameworkName | string? | — | Orchestration framework name. Included in projectHash. |
| tags | string[]? | — | Classification tags. Included in projectHash. |
| provider | string? | 'agent-kit' | CER provider field written to all step CERs |
| model | string? | 'workflow-step' | CER model field written to all step CERs |
WorkflowController:
| Member | Type | Description |
|--------|------|-------------|
| executionId | string (read-only) | Shared workflow-level ID stored as workflowId on every step CER. Not the CER executionId. |
| stepCount | number (read-only) | Number of steps completed so far |
| step(name, fn) | async (string, () => T \| Promise<T>) => Promise<T> | Run a step, certify its output, return fn result unchanged |
| finish(opts?) | (WorkflowFinishOptions?) => WorkflowFinishResult | Sync. Build and return the sealed project bundle |
| finishAndRegister(opts) | (WorkflowFinishAndRegisterOptions) => Promise<WorkflowFinishAndRegisterResult> | Async. finish() + node registration in one call. Delegates to registerProjectBundle() from @nexart/ai-execution. |
WorkflowFinishOptions — all optional:
| Field | Type | Description |
|-------|------|-------------|
| completedAt | string? | ISO 8601 completion time. Defaults to finish() call time. |
| projectSummary | string? | Override or set the project summary at finish time. |
| finalOutputSummary | string? | Human-readable final output summary. Excluded from projectHash. |
| tags | string[]? | Override or replace tags at finish time. |
WorkflowFinishResult:
| Field | Type | Description |
|-------|------|-------------|
| projectBundle | ProjectBundle | Sealed cer.project.bundle.v1 artifact |
| projectHash | string | Shortcut for projectBundle.integrity.projectHash |
WorkflowFinishAndRegisterOptions:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| register.nodeUrl | string | ✓ | Base URL of the NexArt node (e.g. https://node.nexart.io) |
| register.apiKey | string | ✓ | Bearer token for node authentication |
| register.timeoutMs | number? | — | HTTP request timeout in ms (default: 30 000) |
| finishOptions | WorkflowFinishOptions? | — | Forwarded verbatim to the internal finish() call |
WorkflowFinishAndRegisterResult (extends WorkflowFinishResult):
| Field | Type | Description |
|-------|------|-------------|
| projectBundle | ProjectBundle | Sealed cer.project.bundle.v1 artifact |
| projectHash | string | Shortcut for projectBundle.integrity.projectHash |
| registration | ProjectBundleRegistrationResult | Receipt returned by the node |
finishAndRegister() design properties:
- Internally calls
finish()exactly once — the resulting bundle is what gets registered and returned. - All registration logic lives in
@nexart/ai-execution'sregisterProjectBundle().finishAndRegister()does not reimplement or duplicate any of it. - On registration failure, a
ProjectBundleRegistrationError(re-exported from this package) propagates unmodified. The project bundle is still assembled; catch the error to accesserr.detailsanderr.statusCode. finish()(sync) remains the primary finalization path.finishAndRegister()is a convenience for the common case where registration immediately follows finalization.
Example:
import { startWorkflow, ProjectBundleRegistrationError } from '@nexart/agent-kit';
const wf = startWorkflow({ projectTitle: 'My workflow' });
await wf.step('classify', () => classify(input));
await wf.step('respond', () => respond(input));
try {
const { projectBundle, projectHash, registration } = await wf.finishAndRegister({
register: {
nodeUrl: 'https://node.nexart.io',
apiKey: process.env.NEXART_API_KEY!,
},
});
console.log('Registered:', registration.registrationId);
console.log('At:', registration.registeredAt);
} catch (err) {
if (err instanceof ProjectBundleRegistrationError) {
console.error('Registration failed:', err.message, err.details);
}
}Examples
Minimal — zero options:
import { startWorkflow } from '@nexart/agent-kit';
const wf = startWorkflow();
const r1 = await wf.step('fetch', () => fetchData());
const r2 = await wf.step('process', () => process(r1)); // r1 passed via closure
const { projectBundle } = wf.finish(); // synchronousWith project metadata:
const wf = startWorkflow({
projectTitle: 'Support ticket #4821',
appName: 'support-bot',
projectGoal: 'Resolve billing question',
});
const intent = await wf.step('classify', () => classify(ticket));
const docs = await wf.step('retrieve', () => retrieve(intent));
const reply = await wf.step('respond', () => respond(docs));
const { projectBundle, projectHash } = wf.finish({
projectSummary: 'Resolved via self-service docs',
});
// projectHash = 'sha256:<64 hex chars>'Verification (sync and async both work):
import { verifyProjectBundle, verifyProjectBundleAsync } from '@nexart/ai-execution';
const syncResult = verifyProjectBundle(projectBundle); // Node 18+
const asyncResult = await verifyProjectBundleAsync(projectBundle); // browsers, edge workers
// both: { ok: true, code: 'OK', errors: [] }wrapTool(opts)
Produces a callable that executes the wrapped function and creates a CER for that invocation. Each call gets its own sealed cer.ai.execution.v1 bundle with AIEF-06 tool evidence (snapshot.toolCalls) capturing the input and output hashes.
function wrapTool<TArgs, TResult>(
opts: WrapToolOptions<TArgs, TResult>
): (args: TArgs) => Promise<WrapToolResult<TResult>>Options:
| Field | Type | Description |
|-------|------|-------------|
| name | string | Unique tool name — used as the model identifier in the CER snapshot |
| run | (args: TArgs) => Promise<TResult> | The async function to wrap |
| provider | string? | Provider label. Defaults to 'tool' |
| source | string? | Upstream source tag stored in CER meta |
| tags | string[]? | Tags stored in CER meta |
| appId | string? | Application ID stored in snapshot |
| workflowId | string? | Workflow ID stored in snapshot |
| signals | NexArtSignal[]? | Upstream signals bound as context evidence |
| attestOptions | AttestOptions? | Node attestation config |
Returns WrapToolResult<TResult>:
| Field | Type | Description |
|-------|------|-------------|
| result | TResult | Raw tool output |
| bundle | CerAiExecutionBundle | Sealed cer.ai.execution.v1 bundle |
| certificateHash | string | sha256: hash — primary audit identifier |
| receipt | AttestationReceipt? | Present only when attestOptions is supplied |
certifyDecision(params)
Certifies a final agent decision or workflow outcome. Thin async wrapper around @nexart/ai-execution certifyDecision — it does not duplicate or alter any CER hashing or verification logic.
async function certifyDecision(
params: AgentKitDecisionParams
): Promise<AgentKitDecisionResult>Defaults inference parameters to { temperature: 0, maxTokens: 0, topP: null, seed: null } as stable, low-variance defaults for decision recording. This does not guarantee deterministic model behavior — it is a recording convention, not a model control guarantee.
Key params:
| Field | Type | Description |
|-------|------|-------------|
| decision | string | Natural-language decision description (used as prompt/input in the snapshot) |
| output | string \| object | Decision output or rationale |
| provider | string | AI provider (e.g. 'openai') |
| model | string | Model identifier (e.g. 'gpt-4o') |
| signals | NexArtSignal[]? | Upstream signals bound as context evidence |
| attestOptions | AttestOptions? | Node attestation config |
Returns AgentKitDecisionResult:
| Field | Type | Description |
|-------|------|-------------|
| bundle | CerAiExecutionBundle | Sealed cer.ai.execution.v1 bundle |
| certificateHash | string | sha256: hash — primary audit identifier |
| receipt | AttestationReceipt? | Present only when attestOptions is supplied |
createProjectSession(opts)
Creates a ProjectSession for assembling a cer.project.bundle.v1 artifact from multiple certified bundles.
function createProjectSession(opts: ProjectSessionOptions): ProjectSessionProjectSessionOptions:
| Field | Type | Description |
|-------|------|-------------|
| projectTitle | string | Human-readable project title. Required. |
| projectBundleId | string? | Stable ID. Auto-generated (UUID v4) if omitted. |
| projectGoal | string? | Root goal or prompt that started the project. Included in projectHash. |
| projectSummary | string? | Summary of what the project achieved. Included in projectHash. |
| appName | string? | Application name. Included in projectHash. |
| frameworkName | string? | Orchestration framework name. Included in projectHash. |
| tags | string[]? | Classification tags. Included in projectHash. |
| startedAt | string? | ISO 8601 start time. Defaults to createProjectSession() call time. |
| finalOutputSummary | string? | Human-readable final output summary. Excluded from projectHash. |
ProjectSession interface:
| Member | Description |
|--------|-------------|
| projectBundleId | Stable bundle ID (read-only). |
| stepCount | Number of steps accumulated so far (read-only). |
| addStep(bundle, opts) | Add one step. Chainable. |
| addSteps(steps[]) | Add multiple steps at once. Chainable. |
| finalize(opts?) | Build and return the sealed ProjectBundle. |
ProjectSessionFinalizeOptions (all optional — override session-level values):
| Field | Type | Description |
|-------|------|-------------|
| projectSummary | string? | Overrides opts.projectSummary from session init. |
| tags | string[]? | Overrides opts.tags from session init. |
| completedAt | string? | Overrides the auto-generated completion time. |
| finalOutputSummary | string? | Overrides opts.finalOutputSummary from session init. |
Progressive example:
import { createProjectSession } from '@nexart/agent-kit';
import { verifyProjectBundle } from '@nexart/ai-execution';
const session = createProjectSession({
projectTitle: 'Support resolution run',
projectGoal: 'Resolve customer ticket #4892',
appName: 'support-triage',
});
// Add steps as they complete:
const r1 = await classifyTool({ ticketId: '4892' });
session.addStep(r1.bundle, { stepLabel: 'Classify intent' });
const r2 = await retrieveTool({ intent: 'billing-dispute' });
session.addStep(r2.bundle, {
stepLabel: 'Retrieve knowledge',
parentStepIds: [/* classify stepId */],
});
const { bundle: decisionBundle } = await certifyDecision({
decision: 'Offer partial refund',
output: 'APPROVED',
provider: 'openai',
model: 'gpt-4o',
});
session.addStep(decisionBundle, {
stepLabel: 'Final decision',
stepSummary: 'Partial refund approved',
});
const project = session.finalize({
projectSummary: 'Ticket resolved with partial refund',
});
const result = verifyProjectBundle(project);
// { ok: true, errors: [], code: 'OK' }Retroactive example:
const session = createProjectSession({ projectTitle: 'Q1 client implementations' });
session.addSteps([
{ bundle: bundleA, stepLabel: 'Client A — onboarding' },
{ bundle: bundleB, stepLabel: 'Client B — migration' },
{ bundle: bundleC, stepLabel: 'Client C — audit' },
]);
const project = session.finalize();exportProjectBundle(bundle) / importProjectBundle(json)
function exportProjectBundle(bundle: ProjectBundle): string
function importProjectBundle(json: string): ProjectBundleexportProjectBundleuses the same canonical JSON serializer as@nexart/ai-execution— output is byte-for-byte identical to whatexportCerPackage/exportCerwould produce.importProjectBundlethrowsCerVerificationErroron malformed JSON, wrong discriminant (bundleType), or anyverifyProjectBundlefailure.
import { exportProjectBundle, importProjectBundle } from '@nexart/agent-kit';
const json = exportProjectBundle(project);
await fs.writeFile('project-bundle.json', json);
const loaded = importProjectBundle(json); // throws on any failureVerification
Bundles produced by this package are standard NexArt artifacts. They verify with no special tooling:
import { verifyCer } from '@nexart/ai-execution'; // for cer.ai.execution.v1
import { verifyProjectBundle } from '@nexart/ai-execution'; // for cer.project.bundle.v1
const cerResult = verifyCer(cerBundle);
const projectResult = verifyProjectBundle(projectBundle);
// both: { ok: true, errors: [], code: 'OK' }v0.5.0 — additions at a glance
These are strictly additive — default behavior of all existing APIs is byte-equivalent to v0.4.x.
Deterministic finish() (Phase 0)
The first call to WorkflowController.finish() builds the canonical ProjectBundle, deeply freezes it, and caches it on the controller. Every subsequent call (including the one inside finishAndRegister()) returns the same cached reference and the same projectHash. completedAt is captured exactly once. Options passed on later finish() calls are silently ignored — pass them on the first call.
step() after finish() throws (both before and after a step's await fn() resolves), so a sealed workflow cannot silently lose data.
Opt-in signal chaining (Phase 3)
const wf = startWorkflow({
projectTitle: 'Daily report',
enableSignals: true, // ← new in v0.5.0; default false
});
await wf.step('fetch', async () => /* ... */);
await wf.step('summarize', async () => /* ... */); // CER carries a workflow.chain signal → previous step
await wf.step('publish', async () => /* ... */); // CER carries a workflow.chain signal → previous step
const summary = wf.getContextSummary();
// → { count: 2, types: { 'workflow.chain': 2 }, stepRange: { min: 1, max: 2 } }When enableSignals is true, every step after the first attaches a CerContextSignal of type workflow.chain referencing the previous step's certificateHash. The signal participates in the new step's certificateHash, producing a verifiable chain across the whole workflow. Default-off output is byte-equivalent to v0.4.x.
WorkflowController.getContextSummary() is safe to call before finish() and returns { count: 0, types: {}, stepRange: null } when enableSignals is false.
Detailed project bundle verification (Phase 2)
import { verifyProjectBundleDetailed } from '@nexart/agent-kit';
const detailed = verifyProjectBundleDetailed(projectBundle);
if (!detailed.ok) {
console.error('Envelope errors:', detailed.errors);
}
for (const step of detailed.steps) {
if (step.detailed?.status !== 'VERIFIED') {
console.warn(`step ${step.stepLabel}:`, step.detailed?.reasonCodes);
}
}Returns:
ok,code,errors— mirror the canonicalverifyProjectBundleenvelope result for ergonomic top-level checksenvelope— the full canonicalProjectBundleVerifyResultsteps[]— one entry perstepRegistryentry, each carryingstepId,stepLabel,sequence, anddetailed: CerVerificationResult | nullfromverifyAiCerBundleDetailedagainst the embedded CER bundle
importProjectBundle() is unchanged — still returns ProjectBundle, still verifies via the canonical envelope verifier. The new helper is strictly additive: opt in only when you need per-step granular reports.
Backward compatibility
This package does not change any CER, hashing, attestation, or verification semantics. All previously created CERs continue to verify identically. v0.5.0 default behavior (no enableSignals, single finish(), importProjectBundle only) is byte-equivalent to v0.4.x.
Version
0.5.2
