npm package: @nexart/agent-kit
Description: Thin convenience wrappers for building AI agents with NexArt certification, with project bundle ergonomics and high-level workflow orchestration
Author: NexArt
Latest version: 0.3.0

# @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`](https://www.npmjs.com/package/@nexart/ai-execution) to make it easier to attach [Certified Execution Records (CERs)](https://nexart.io) to individual tool calls, agent decisions, and multi-step project workflows. It works with [`@nexart/signals`](https://www.npmjs.com/package/@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

```bash
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:

```bash
npm install @nexart/agent-kit @nexart/ai-execution @nexart/signals
```

## When 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 |
| `AGENT_KIT_VERSION` | Package version string |

## Basic example

```ts
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()`.

```ts
function startWorkflow(opts?: WorkflowOptions): WorkflowController
```

#### When 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()`](#createprojectsessionopts) 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.

```ts
// ✓ 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`.

```ts
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); // false
```

#### finish() 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 |

**`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` |

#### Examples

**Minimal — zero options:**

```ts
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();  // synchronous
```

**With project metadata:**

```ts
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):**

```ts
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.

```ts
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.

```ts
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.

```ts
function createProjectSession(opts: ProjectSessionOptions): ProjectSession
```

**`ProjectSessionOptions`:**

| 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:**

```ts
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:**

```ts
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)`

```ts
function exportProjectBundle(bundle: ProjectBundle): string
function importProjectBundle(json: string): ProjectBundle
```

- `exportProjectBundle` uses the same canonical JSON serializer as `@nexart/ai-execution` — output is byte-for-byte identical to what `exportCerPackage` / `exportCer` would produce.
- `importProjectBundle` throws `CerVerificationError` on malformed JSON, wrong discriminant (`bundleType`), or any `verifyProjectBundle` failure.

```ts
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 failure
```

---

## Verification

Bundles produced by this package are standard NexArt artifacts. They verify with no special tooling:

```ts
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' }
```

## Backward compatibility

This package does not change any CER, hashing, attestation, or verification semantics. All previously created CERs continue to verify identically.

## Version

`0.3.0`